From 773dea3b91c77f1e69c1409df34fba44fb53a212 Mon Sep 17 00:00:00 2001 From: fatwang2 Date: Mon, 12 Oct 2026 15:22:35 +0800 Subject: [PATCH 1/3] Move engine-specific search fields into result metadata --- CHANGELOG.md | 5 +++++ skills/search1api/SKILL.md | 5 +++-- src/sdk.ts | 23 ++++++++++++++--------- tests/sdk.test.mjs | 18 ++++++++++++++++++ 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c729c48 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +Breaking: search/news engine-specific fields move from each result's top level to optional `metadata` (`kind`, `stars`, `language`, `num_comments`, `points`, `story_url`). Read e.g. `result.metadata.stars` instead of `result.stars`. `published_date` stays at the top level. No legacy field aliases. diff --git a/skills/search1api/SKILL.md b/skills/search1api/SKILL.md index 8a02891..335e5ed 100644 --- a/skills/search1api/SKILL.md +++ b/skills/search1api/SKILL.md @@ -135,7 +135,8 @@ s1 search "" [options] Search engines: google, bing, bingcn, duckduckgo, yahoo, yandex, x, reddit, github, youtube, arxiv, wechat, bilibili, imdb, wikipedia, grokipedia Results show `published_date` when the source exposes one; `--json` also -carries `kind`, `stars`, `language`, and `num_comments` for `github` results. +carries an optional `metadata` object with `kind`, `stars`, `language`, and +`num_comments` for `github` results. `published_date` remains at the top level. ### news @@ -145,7 +146,7 @@ s1 news "" [options] Same options as search except `--page`. News services: google, bing, duckduckgo, yahoo, hackernews. Default service: bing. `hackernews` results link to the discussion thread; with `--json` the article -is in `story_url`, with `points` and `num_comments`. +is in `metadata.story_url`, with `metadata.points` and `metadata.num_comments`. When user asks for breaking/latest news, always add `-t day`. diff --git a/src/sdk.ts b/src/sdk.ts index f13e1cd..56775c7 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -54,6 +54,19 @@ export interface NewsOptions extends RequestOptions { timeRange?: TimeRange; } +export interface SearchResultMetadata { + /** GitHub: result type, repository stars/language, or thread comments. */ + kind?: "repo" | "issue" | "pr" | "discussion"; + stars?: number; + language?: string; + /** GitHub threads and Hacker News news results. */ + num_comments?: number; + /** Hacker News: votes and submitted article URL; link is the discussion. */ + points?: number; + story_url?: string; + [key: string]: unknown; +} + export interface SearchResult { title: string; link: string; @@ -61,15 +74,7 @@ export interface SearchResult { content?: string; /** ISO 8601, when the source exposes a date. */ published_date?: string; - /** `github` results. */ - kind?: "repo" | "issue" | "pr" | "discussion"; - stars?: number; - language?: string; - /** `github` threads and `hackernews` news results. */ - num_comments?: number; - /** `hackernews` news results; `link` is the discussion thread. */ - points?: number; - story_url?: string; + metadata?: SearchResultMetadata; } export interface SearchResponse { diff --git a/tests/sdk.test.mjs b/tests/sdk.test.mjs index fcbdcb8..8faf6c6 100644 --- a/tests/sdk.test.mjs +++ b/tests/sdk.test.mjs @@ -68,6 +68,24 @@ test("SDK helpers use stable endpoints and defaults", async () => { } }); +test("search and news preserve nested metadata and top-level dates", async () => { + const response = { + results: [{ + title: "Thread", + link: "https://example.com/thread", + snippet: "Discussion", + published_date: "2026-10-12", + metadata: { num_comments: 0 }, + }], + }; + for (const run of [() => search("q"), () => news("q")]) { + const { result } = await captureRequest(run, response); + assert.deepEqual(result, response); + assert.equal(result.results[0].metadata.num_comments, 0); + assert.equal("num_comments" in result.results[0], false); + } +}); + test("an explicit SDK API key overrides shared CLI credentials", async () => { const { captured } = await captureRequest( () => crawl("https://example.com", { apiKey: "plugin-key" }), From 957976b0b5b837c1d5fe6f137c119dae2f2bc89e Mon Sep 17 00:00:00 2001 From: fatwang2 Date: Mon, 12 Oct 2026 16:16:19 +0800 Subject: [PATCH 2/3] Prepare metadata release versions and migration notes --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1a53465 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 22] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm audit --audit-level=high + - run: npm test + - run: npm pack --dry-run diff --git a/CHANGELOG.md b/CHANGELOG.md index c729c48..cc5ff47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## Unreleased +## 2.0.0 - 2026-10-12 Breaking: search/news engine-specific fields move from each result's top level to optional `metadata` (`kind`, `stars`, `language`, `num_comments`, `points`, `story_url`). Read e.g. `result.metadata.stars` instead of `result.stars`. `published_date` stays at the top level. No legacy field aliases. diff --git a/package-lock.json b/package-lock.json index fca177c..b759f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "search1api-cli", - "version": "1.5.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "search1api-cli", - "version": "1.5.0", + "version": "2.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 934e870..d823bd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "search1api-cli", - "version": "1.5.0", + "version": "2.0.0", "description": "CLI tool for Search1API - web search, news, crawl, sitemap, and trending", "type": "module", "exports": { From 403c5647e121208eee8195cb4a17b4b905f7bcbb Mon Sep 17 00:00:00 2001 From: fatwang2 Date: Mon, 12 Oct 2026 16:23:13 +0800 Subject: [PATCH 3/3] Close one-shot OAuth callback connections on Node 18 --- CHANGELOG.md | 2 ++ src/auth.ts | 6 +++++- tests/oauth-e2e.test.mjs | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5ff47..259fb4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,6 @@ ## 2.0.0 - 2026-10-12 +Fix repeated browser OAuth login on Node 18 by closing callback connections before the temporary server port is reused. Add PR CI for Node 18 and 22. + Breaking: search/news engine-specific fields move from each result's top level to optional `metadata` (`kind`, `stars`, `language`, `num_comments`, `points`, `story_url`). Read e.g. `result.metadata.stars` instead of `result.stars`. `published_date` stays at the top level. No legacy field aliases. diff --git a/src/auth.ts b/src/auth.ts index dd5d34d..f144f4c 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -127,7 +127,11 @@ function escapeHtml(value: string): string { } function respond(res: ServerResponse, status: number, body: string): void { - res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" }); + // The callback server is temporary; a later login may reuse its port. + res.writeHead(status, { + "Content-Type": "text/html; charset=utf-8", + Connection: "close", + }); res.end(body); } diff --git a/tests/oauth-e2e.test.mjs b/tests/oauth-e2e.test.mjs index 4152cdd..2eaf7de 100644 --- a/tests/oauth-e2e.test.mjs +++ b/tests/oauth-e2e.test.mjs @@ -185,6 +185,7 @@ test("runs browser OAuth, PKCE, refresh, retry, and API-key fallback end to end" const callbackResponse = await authorizationRequest; assert.equal(callbackResponse.status, 200); + assert.equal(callbackResponse.headers.get("connection"), "close"); assert.match(await callbackResponse.text(), /Authorization complete/); assert.equal(login.oauth.accessToken, "oauth_access_initial"); assert.equal(login.oauth.refreshToken, "oauth_refresh_123");