From da53e12595f0fb5b57700b9653d699fc6009491c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 04:00:41 +0000 Subject: [PATCH 1/2] Add s1 feedback command via the Agentback SDK Mounts agentback/cli's runFeedbackCommand as `s1 feedback`. Reports carry the logged-in account signed by GET /feedback/identity when the lookup succeeds. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01V7jAP9YsE7D4HvphbaKNCc --- README.md | 20 ++++++++++++ package-lock.json | 26 ++++++++++++++++ package.json | 1 + src/commands/feedback.ts | 41 +++++++++++++++++++++++++ src/config.ts | 4 +++ src/index.ts | 3 ++ tests/feedback.test.mjs | 66 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+) create mode 100644 src/commands/feedback.ts create mode 100644 tests/feedback.test.mjs diff --git a/README.md b/README.md index a3dae70..e8cf85b 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,26 @@ Check your remaining API credits. s1 balance ``` +### feedback + +Report a wrong result, an error you could not resolve, or a missing capability to the Search1API team. Agents can use it too. When you are logged in, the report is linked to your account so the team can follow up. + +```bash +s1 feedback "bingcn returned no results for a Chinese query" +s1 feedback --category feature_request --intent "monitor a site" "Need a webhook when a page changes" +echo "details..." | s1 feedback - +``` + +| Option | Description | +|--------|-------------| +| `-c, --category ` | `bug`, `feature_request`, `docs`, or `other` | +| `-i, --intent ` | What you were trying to do | +| `-r, --request-id ` | Request id of the call that had the problem | +| `-m, --model ` | Your model name, if you are an AI agent | +| `--json` | Print the receipt as JSON | + +Do not include credentials or private data in the message. + ### login Authorize in your browser with OAuth 2.1 and PKCE. diff --git a/package-lock.json b/package-lock.json index b759f85..474a085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "agentback": "^0.2.0", "chalk": "^5.4.1", "commander": "^13.1.0", "update-notifier": "^7.3.1" @@ -204,6 +205,31 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/agentback": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/agentback/-/agentback-0.2.0.tgz", + "integrity": "sha512-1cdp9gNM5U9v8qXu3Up40iwHjy2842Fy92VkA6FH0W/aWHodYf3Yq7sbZ8EYRuslf/gxwG4CuHeZUSP4i7Nhjw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.23.0", + "@modelcontextprotocol/server": "^2.0.0", + "zod": "^4.2.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + }, + "@modelcontextprotocol/server": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", diff --git a/package.json b/package.json index d823bd4..14d111f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "scripts" ], "dependencies": { + "agentback": "^0.2.0", "chalk": "^5.4.1", "commander": "^13.1.0", "update-notifier": "^7.3.1" diff --git a/src/commands/feedback.ts b/src/commands/feedback.ts new file mode 100644 index 0000000..20a4170 --- /dev/null +++ b/src/commands/feedback.ts @@ -0,0 +1,41 @@ +import { Command } from "commander"; +import { Agentback, type Identity } from "agentback"; +import { runFeedbackCommand } from "agentback/cli"; +import { fetchWithAuth } from "../auth.js"; +import { AGENTBACK_PROJECT_KEY, API_BASE } from "../config.js"; + +// Signed so Agentback can trust which Search1API user sent the report. Without +// a working credential the report is sent without an account. +async function identify(): Promise { + try { + const res = await fetchWithAuth(`${API_BASE}/feedback/identity`, { + method: "GET", + signal: AbortSignal.timeout(3000), + }); + if (!res.ok) return undefined; + const body = (await res.json()) as Partial; + return typeof body.account === "string" && + typeof body.signature === "string" + ? { account: body.account, signature: body.signature } + : undefined; + } catch { + return undefined; + } +} + +export function registerFeedbackCommand(program: Command): void { + program + .command("feedback") + .description("Report a problem with Search1API to its developers") + // runFeedbackCommand parses its own options and prints its own help. + .helpOption(false) + .allowUnknownOption() + .argument("[args...]") + .action(async (args: string[]) => { + process.exitCode = await runFeedbackCommand( + new Agentback({ projectKey: AGENTBACK_PROJECT_KEY }), + args, + { program: "s1", identify } + ); + }); +} diff --git a/src/config.ts b/src/config.ts index 6f9ee8d..ad4aeb2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,10 @@ const CONFIG_FILE = join(CONFIG_DIR, "config.json"); export const API_BASE = process.env.SEARCH1API_API_BASE ?? "https://api.search1api.com"; +// Agentback project public key for `s1 feedback`. It can only submit feedback, +// so it ships with the CLI like any other public client key. +export const AGENTBACK_PROJECT_KEY = + "af_97385a84ecf95968db2403eb6ef165ddcd8ee6716c1b2ceb72f1a35a69e52b8e"; export const API_RESOURCE_METADATA = process.env.SEARCH1API_OAUTH_RESOURCE_METADATA ?? `${API_BASE}/.well-known/oauth-protected-resource`; diff --git a/src/index.ts b/src/index.ts index 5572a57..c88cbe6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ import { registerConfigCommand } from "./commands/config.js"; import { registerUsageCommand } from "./commands/usage.js"; import { registerLoginCommand } from "./commands/login.js"; import { registerUpdateCommand } from "./commands/update.js"; +import { registerFeedbackCommand } from "./commands/feedback.js"; const program = new Command(); @@ -57,6 +58,7 @@ registerUsageCommand(program); registerLoginCommand(program); registerConfigCommand(program); registerUpdateCommand(program, { pkg, isBinary }); +registerFeedbackCommand(program); if (process.argv.length <= 2) { console.log(` @@ -71,6 +73,7 @@ if (process.argv.length <= 2) { ${chalk.blue("s1 learn")} ${chalk.dim("")} Turn a URL into an Agent Skill ${chalk.blue("s1 trending")} ${chalk.dim("")} Trending on GitHub / HackerNews ${chalk.blue("s1 balance")} Check remaining credits + ${chalk.blue("s1 feedback")} ${chalk.dim('"message"')} Report a problem to the Search1API team ${chalk.bold("Setup:")} ${chalk.blue("s1 login")} Authorize with OAuth in your browser diff --git a/tests/feedback.test.mjs b/tests/feedback.test.mjs new file mode 100644 index 0000000..c39bacc --- /dev/null +++ b/tests/feedback.test.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Command } from "commander"; + +process.env.SEARCH1API_KEY = "test-key"; +process.env.SEARCH1API_API_BASE = "https://api.example.test"; + +const { registerFeedbackCommand } = await import("../dist/commands/feedback.js"); + +async function runFeedback(args, identityResponse) { + const calls = []; + const originalFetch = globalThis.fetch; + const originalWrite = process.stdout.write; + let stdout = ""; + globalThis.fetch = async (input, init) => { + const url = String(input); + calls.push({ url, init }); + if (url === "https://api.example.test/feedback/identity") return identityResponse(); + return Response.json({ id: "fb_1", status: "new" }, { status: 201 }); + }; + process.stdout.write = (text) => { + stdout += text; + return true; + }; + try { + const program = new Command("s1").exitOverride(); + registerFeedbackCommand(program); + await program.parseAsync(["feedback", ...args], { from: "user" }); + return { calls, stdout, exitCode: process.exitCode }; + } finally { + globalThis.fetch = originalFetch; + process.stdout.write = originalWrite; + process.exitCode = undefined; + } +} + +test("feedback is sent under the signed-in Search1API account", async () => { + const { calls, stdout, exitCode } = await runFeedback( + ["-c", "bug", "--intent", "find docs", "bingcn", "returned", "nothing"], + () => Response.json({ account: "user_123", signature: "a".repeat(64) }) + ); + + assert.equal(exitCode, 0); + assert.match(stdout, /Feedback sent \(fb_1\)/); + assert.equal(new Headers(calls[0].init.headers).get("authorization"), "Bearer test-key"); + assert.match(calls[1].url, /^https:\/\/agentback\.app\/f\/af_[a-f0-9]{64}$/); + assert.deepEqual(JSON.parse(calls[1].init.body), { + message: "bingcn returned nothing", + intent: "find docs", + category: "bug", + source: "cli", + account: "user_123", + account_signature: "a".repeat(64), + }); +}); + +test("feedback is still sent when the account lookup fails", async () => { + const { calls, exitCode } = await runFeedback(["--json", "it broke"], () => + Response.json({ error: "Unauthorized" }, { status: 401 }) + ); + + assert.equal(exitCode, 0); + const body = JSON.parse(calls.at(-1).init.body); + assert.equal(body.account, undefined); + assert.equal(body.source, "cli"); +}); From 08ece0608a6bf346747d63b371d25768016adc21 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:52:47 +0000 Subject: [PATCH 2/2] Release 2.1.0 with s1 feedback Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01V7jAP9YsE7D4HvphbaKNCc --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 259fb4d..da1e171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.1.0 - 2026-10-26 + +Add `s1 feedback` to report wrong results, errors and missing capabilities to the Search1API team, built on the public Agentback SDK. Supports `--category`, `--intent`, `--request-id`, `--model`, `--json` and `-` for stdin. When logged in, the report carries the signed account. + ## 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. diff --git a/package-lock.json b/package-lock.json index 474a085..67a71c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "search1api-cli", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "search1api-cli", - "version": "2.0.0", + "version": "2.1.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 14d111f..7078bf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "search1api-cli", - "version": "2.0.0", + "version": "2.1.0", "description": "CLI tool for Search1API - web search, news, crawl, sitemap, and trending", "type": "module", "exports": {