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 new file mode 100644 index 0000000..259fb4d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 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/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": { 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/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/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/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"); 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" }),