Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
5 changes: 3 additions & 2 deletions skills/search1api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ s1 search "<query>" [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

Expand All @@ -145,7 +146,7 @@ s1 news "<query>" [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`.

Expand Down
6 changes: 5 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
23 changes: 14 additions & 9 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,27 @@ 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;
snippet: string;
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 {
Expand Down
1 change: 1 addition & 0 deletions tests/oauth-e2e.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
18 changes: 18 additions & 0 deletions tests/sdk.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" }),
Expand Down
Loading