@great-arrow/cli 0.1.48 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -159,6 +159,24 @@ gad mcp install codex
159
159
  gad mcp install gemini
160
160
  ```
161
161
 
162
+ #### Optional: tool suggestions in Claude Code (off by default)
163
+
164
+ ```bash
165
+ gad mcp suggest-tools on # shows what is sent, then asks [y/N]
166
+ gad mcp suggest-tools on --yes # non-interactive opt-in
167
+ gad mcp suggest-tools off # removes only this hook's entry
168
+ gad mcp suggest-tools status
169
+ gad mcp install claude --suggest-tools # or --no-suggest-tools
170
+ ```
171
+
172
+ When on, a Claude Code `UserPromptSubmit` hook sends each prompt's text (up to
173
+ 2,000 characters, nothing else) to greatarrow.ai over your own token and adds
174
+ up to three Great Arrow tool names that may help. The prompt is not stored or
175
+ logged; the hook gives up after 1.5 seconds and never blocks a prompt. It adds
176
+ one entry to `~/.claude/settings.json` and reads its token from
177
+ `~/.great-arrow/hook-config.json` — never from argv or settings.json.
178
+ `gad doctor claude` reports whether it is on.
179
+
162
180
  `gad mcp install codex` writes Codex's native `~/.codex/config.toml` server
163
181
  entry and stores the bearer token in `~/.codex/.env`. The entry launches the
164
182
  stdio bridge, which detects the current checkout's git origin for every Codex
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit (opt-in): name up to three Great Arrow tools that may help
4
+ * with the prompt the user just typed.
5
+ *
6
+ * CONTRACT — this runs on EVERY prompt, so it must never block or break one:
7
+ * 1. Sends ONLY `{ prompt }` — never the session id, transcript path or cwd
8
+ * the hook input also carries.
9
+ * 2. One request, 1.5s deadline; a 2s watchdog bounds the whole run, so
10
+ * even a stdin that never closes costs at most that.
11
+ * 3. On ANY failure (no token, bad stdin, timeout, non-2xx, bad JSON, no
12
+ * tools) it prints nothing and exits 0.
13
+ *
14
+ * Credential: GAD_API_TOKEN, else the `{ url, token }` in
15
+ * ~/.great-arrow/hook-config.json that every Great Arrow Claude Code hook
16
+ * reads (written by `gad mcp suggest-tools on` / `gad doctor claude
17
+ * --enforce`) — never argv, never settings.json. GAD_BASE_URL overrides the
18
+ * host. Installed opt-in by `@great-arrow/cli`; what it sends is documented
19
+ * in docs/MCP_VERSIONING.md → "Opt-in prompt hook".
20
+ */
21
+
22
+ import { readFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join } from "node:path";
25
+
26
+ const DEADLINE_MS = 1_500;
27
+ // Watchdog: a host that never closes stdin would otherwise hang the prompt.
28
+ // unref() so it never holds a finished process open.
29
+ setTimeout(() => process.exit(0), 2_000).unref();
30
+
31
+ async function readStdin() {
32
+ let data = "";
33
+ for await (const chunk of process.stdin) data += chunk;
34
+ return data;
35
+ }
36
+
37
+ function hookConfig() {
38
+ try {
39
+ const cfg = JSON.parse(
40
+ readFileSync(join(homedir(), ".great-arrow", "hook-config.json"), "utf8"),
41
+ );
42
+ return cfg && typeof cfg === "object" ? cfg : {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+
48
+ async function main() {
49
+ const cfg = process.env.GAD_API_TOKEN?.trim() ? {} : hookConfig();
50
+ const token = (
51
+ process.env.GAD_API_TOKEN ||
52
+ (typeof cfg.token === "string" ? cfg.token : "")
53
+ ).trim();
54
+ if (!token) return;
55
+ const input = JSON.parse(await readStdin());
56
+ const prompt = typeof input?.prompt === "string" ? input.prompt : "";
57
+ if (!prompt.trim()) return;
58
+
59
+ const base = (
60
+ process.env.GAD_BASE_URL ||
61
+ (typeof cfg.url === "string" && cfg.url) ||
62
+ "https://www.greatarrow.ai"
63
+ ).replace(/\/$/, "");
64
+ const res = await fetch(`${base}/api/mcp/tools-suggest`, {
65
+ method: "POST",
66
+ headers: {
67
+ authorization: `Bearer ${token}`,
68
+ "content-type": "application/json",
69
+ },
70
+ body: JSON.stringify({ prompt }),
71
+ signal: AbortSignal.timeout(DEADLINE_MS),
72
+ });
73
+ if (!res.ok) return;
74
+ const body = await res.json();
75
+ const names = (Array.isArray(body?.tools) ? body.tools : [])
76
+ .map((t) => t?.name)
77
+ .filter((n) => typeof n === "string" && /^[a-z0-9_]+$/.test(n))
78
+ .slice(0, 3);
79
+ if (names.length === 0) return;
80
+
81
+ return JSON.stringify({
82
+ hookSpecificOutput: {
83
+ hookEventName: "UserPromptSubmit",
84
+ additionalContext: `Great Arrow tools that may help: ${names.join(", ")} (call via gad_call_read / gad_call_write or tools_enable)`,
85
+ },
86
+ });
87
+ }
88
+
89
+ let out = "";
90
+ try {
91
+ out = (await main()) ?? "";
92
+ } catch {
93
+ // silent-degrade: a suggestion is never worth a broken prompt
94
+ }
95
+ // Exit only once the write has flushed: stdout to a pipe is asynchronous on
96
+ // macOS, and exiting first would drop the context.
97
+ if (out) process.stdout.write(out, () => process.exit(0));
98
+ else process.exit(0);