@great-arrow/cli 0.1.47 → 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 +18 -0
- package/dist/hooks/gad-suggest-tools.mjs +98 -0
- package/dist/index.js +629 -246
- package/package.json +1 -1
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);
|