integral-mind 0.1.0
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 +52 -0
- package/dist/cli.js +945 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.js +82 -0
- package/dist/client.js.map +1 -0
- package/dist/config.js +75 -0
- package/dist/config.js.map +1 -0
- package/dist/docs.js +188 -0
- package/dist/docs.js.map +1 -0
- package/dist/exit.js +10 -0
- package/dist/exit.js.map +1 -0
- package/dist/hooks.js +261 -0
- package/dist/hooks.js.map +1 -0
- package/dist/redact.js +77 -0
- package/dist/redact.js.map +1 -0
- package/dist/sync/cli.js +431 -0
- package/dist/sync/cli.js.map +1 -0
- package/dist/sync/mempalace-source.js +136 -0
- package/dist/sync/mempalace-source.js.map +1 -0
- package/dist/sync/mempalace.js +215 -0
- package/dist/sync/mempalace.js.map +1 -0
- package/dist/sync/obsidian-source.js +30 -0
- package/dist/sync/obsidian-source.js.map +1 -0
- package/dist/sync/obsidian.js +52 -0
- package/dist/sync/obsidian.js.map +1 -0
- package/dist/sync/repo-source.js +43 -0
- package/dist/sync/repo-source.js.map +1 -0
- package/dist/sync/repo.js +51 -0
- package/dist/sync/repo.js.map +1 -0
- package/dist/sync/state.js +87 -0
- package/dist/sync/state.js.map +1 -0
- package/dist/sync/tree-sync.js +56 -0
- package/dist/sync/tree-sync.js.map +1 -0
- package/package.json +49 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* im — the Integral Mind CLI. Thin HTTP client: no database, no parsers.
|
|
4
|
+
*
|
|
5
|
+
* Exit codes:
|
|
6
|
+
* 0 success
|
|
7
|
+
* 1 runtime failure (network unreachable, server 5xx)
|
|
8
|
+
* 2 usage error (unknown command, bad flags, missing config)
|
|
9
|
+
* 3 authentication/authorization failure (401/403, bad or unscoped token)
|
|
10
|
+
* 4 not found (404)
|
|
11
|
+
* 5 validation rejected the input (400/422)
|
|
12
|
+
*/
|
|
13
|
+
import { realpathSync } from "node:fs";
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { parseArgs } from "node:util";
|
|
19
|
+
import { checkConfigPermissions, configPath, readConfigFile, resolveConfig, writeConfigFile, } from "./config.js";
|
|
20
|
+
import { redactSecrets, summarizeFindings } from "./redact.js";
|
|
21
|
+
import { ApiError, ImClient } from "./client.js";
|
|
22
|
+
import { CANONICAL_DOC, DROP_IN_BLOCK } from "./docs.js";
|
|
23
|
+
import { defaultSessionEndDeps, installClaudeHook, runClaudeSessionEnd, uninstallClaudeHook, } from "./hooks.js";
|
|
24
|
+
export { EXIT } from "./exit.js";
|
|
25
|
+
import { EXIT } from "./exit.js";
|
|
26
|
+
import { runSyncCommand } from "./sync/cli.js";
|
|
27
|
+
const USAGE = `im — Integral Mind CLI
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
im config set <url|token> <value> Store connection settings (chmod 600)
|
|
31
|
+
im config show Show effective config and its sources
|
|
32
|
+
im push session [options] Push a session envelope (redacted by default)
|
|
33
|
+
im push text <content> [options] Capture plain text
|
|
34
|
+
im push file <path> [options] Capture a file's contents as text
|
|
35
|
+
im push url <url> [options] Capture a web page
|
|
36
|
+
im push youtube <url> [options] Capture a YouTube video
|
|
37
|
+
im search <query> [options] Hybrid search (filters: --type --after --before --code --sessions)
|
|
38
|
+
im sync <connector> [options] Delta-sync a machine-local source (mempalace, obsidian, repo)
|
|
39
|
+
im sync connect|status|disconnect Manage sync connections
|
|
40
|
+
im recent [options] List recently ingested sources
|
|
41
|
+
im stats Corpus statistics
|
|
42
|
+
im projects list|create <name> Manage projects (admin token)
|
|
43
|
+
im sources list|reparse|delete Manage sources (admin token)
|
|
44
|
+
im tokens list|generate|revoke Manage MCP tokens (admin token)
|
|
45
|
+
im docs [--drop-in] Print the canonical manual (or the AGENTS.md block)\n im hooks install|uninstall claude Auto-push Claude Code sessions at session end
|
|
46
|
+
|
|
47
|
+
push session options:
|
|
48
|
+
--file <path> Read the envelope JSON from a file
|
|
49
|
+
--stdin Read the envelope JSON from stdin
|
|
50
|
+
--text <t> Build a text-fallback envelope instead of JSON
|
|
51
|
+
--agent <name> Agent name for --text envelopes (default: cli)
|
|
52
|
+
--title <t> Session title
|
|
53
|
+
--project <uuid> Target project (default: your first project)
|
|
54
|
+
--no-redact Skip secret redaction (NOT recommended)
|
|
55
|
+
|
|
56
|
+
Common options:
|
|
57
|
+
--project <uuid> Target project --limit <n> Result count
|
|
58
|
+
--title <t> Source title --json Raw JSON output
|
|
59
|
+
|
|
60
|
+
Environment:
|
|
61
|
+
INTEGRAL_MIND_URL, INTEGRAL_MIND_TOKEN override the config file.
|
|
62
|
+
|
|
63
|
+
Exit codes: 0 ok · 1 runtime · 2 usage · 3 auth · 4 not found · 5 validation`;
|
|
64
|
+
const EXIT_CODES_HELP = "Exit codes: 0 ok · 1 runtime · 2 usage · 3 auth · 4 not found · 5 validation";
|
|
65
|
+
// Agent-grade per-command help: every entry has copy-paste examples and
|
|
66
|
+
// exit-code semantics. Content mirrors the canonical manual (`im docs`).
|
|
67
|
+
const COMMAND_HELP = {
|
|
68
|
+
config: `im config — store connection settings (file mode 0600)
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
im config set url <https://instance>
|
|
72
|
+
im config set token <imk_...>
|
|
73
|
+
im config show
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
im config set url https://span-vault.com
|
|
77
|
+
im config set token imk_abc123
|
|
78
|
+
INTEGRAL_MIND_URL and INTEGRAL_MIND_TOKEN env vars override the file.
|
|
79
|
+
|
|
80
|
+
${EXIT_CODES_HELP}`,
|
|
81
|
+
push: `im push — store sources in your knowledge base
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
im push session (--file <envelope.json> | --stdin | --text <t>) [--agent a] [--title t] [--project uuid] [--no-redact]
|
|
85
|
+
im push text <content> [--title t] [--project uuid]
|
|
86
|
+
im push file <path> [--title t] [--project uuid]
|
|
87
|
+
im push url <url> [--title t] [--project uuid]
|
|
88
|
+
im push youtube <url> [--title t] [--project uuid]
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
im push session --file transcript.json
|
|
92
|
+
cat transcript.json | im push session --stdin
|
|
93
|
+
im push text "advisory locks coordinate blob GC" --title "GC note"
|
|
94
|
+
im push file ./notes/design.md
|
|
95
|
+
|
|
96
|
+
Session pushes are redacted by default (typed [REDACTED:type]
|
|
97
|
+
placeholders; summary on stderr); --no-redact bypasses. Pushes are
|
|
98
|
+
idempotent by content hash. Run "im docs" for the envelope format.
|
|
99
|
+
|
|
100
|
+
${EXIT_CODES_HELP}`,
|
|
101
|
+
search: `im search — hybrid search returning cited evidence chunks (no LLM)
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
im search <query> [--limit n] [--project uuid] [--type t]... [--after date] [--before date] [--code] [--sessions] [--json]
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
im search "failover postmortem"
|
|
108
|
+
im search "advisory locks" --limit 3 --json
|
|
109
|
+
im search "retry budget" --type session --type text # only these source types
|
|
110
|
+
im search "interlock fix" --after 2026-06-01 # ingested on/after (inclusive)
|
|
111
|
+
im search "interlock fix" --before 2026-07-01 # ingested before (exclusive)
|
|
112
|
+
im search "quorum fence" --sessions # sessions + summaries only
|
|
113
|
+
im search "token verification" --code # synced repos only; cites path:lines
|
|
114
|
+
|
|
115
|
+
Filters combine. --sessions calls the dedicated session_search primitive
|
|
116
|
+
(rows carry session type + push date); --code calls code_search. Both
|
|
117
|
+
accept --after/--before but not --type, and are mutually exclusive.
|
|
118
|
+
|
|
119
|
+
${EXIT_CODES_HELP}`,
|
|
120
|
+
recent: `im recent — list newest sources
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
im recent [--limit n] [--project uuid] [--json]
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
im recent --limit 10
|
|
127
|
+
|
|
128
|
+
${EXIT_CODES_HELP}`,
|
|
129
|
+
stats: `im stats — corpus statistics
|
|
130
|
+
|
|
131
|
+
Usage:
|
|
132
|
+
im stats [--json]
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
im stats --json
|
|
136
|
+
|
|
137
|
+
${EXIT_CODES_HELP}`,
|
|
138
|
+
sync: `im sync — delta-sync machine-local sources into your knowledge base
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
im sync mempalace [--palace <label>] [--full]
|
|
142
|
+
im sync obsidian [--label <name>] [--full]
|
|
143
|
+
im sync repo [--label <name>] [--full]
|
|
144
|
+
im sync connect mempalace --label <name> --path <palace-dir> [--layout project_per_palace|project_per_wing]
|
|
145
|
+
im sync connect obsidian --vault <vault-dir> [--label <name>] [--project <uuid>]
|
|
146
|
+
im sync connect repo --path <repo-dir> [--label <name>] [--project <uuid>]
|
|
147
|
+
im sync status
|
|
148
|
+
im sync disconnect <mempalace|obsidian|repo> <label>
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
im sync connect mempalace --label personal --path ~/.mempalace/palace
|
|
152
|
+
im sync mempalace # delta sync: only changed items push
|
|
153
|
+
im sync connect obsidian --vault ~/vaults/Brain
|
|
154
|
+
im sync obsidian # edited notes re-sync; renames tracked
|
|
155
|
+
im sync connect repo --path ~/projects/my-app
|
|
156
|
+
im sync repo # after a commit, only changed files re-index
|
|
157
|
+
im sync status
|
|
158
|
+
|
|
159
|
+
Per-connector delta state lives under ~/.config/integral-mind/sync/; a
|
|
160
|
+
repeat run with no local changes pushes nothing. connect reuses an
|
|
161
|
+
existing server-side registration with the same label, so reconnecting
|
|
162
|
+
after a reinstall never duplicates sources.
|
|
163
|
+
|
|
164
|
+
${EXIT_CODES_HELP}`,
|
|
165
|
+
hooks: `im hooks — Claude Code session auto-push
|
|
166
|
+
|
|
167
|
+
Usage:
|
|
168
|
+
im hooks install claude Wire the SessionEnd hook into ~/.claude/settings.json
|
|
169
|
+
im hooks uninstall claude Remove it (other hooks untouched)
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
im hooks install claude
|
|
173
|
+
im hooks uninstall claude
|
|
174
|
+
|
|
175
|
+
Install is idempotent. At session end the transcript is reshaped into
|
|
176
|
+
the session envelope, redacted, and pushed; failures never block the
|
|
177
|
+
Claude session (see ~/.local/state/integral-mind/hooks.log).
|
|
178
|
+
|
|
179
|
+
${EXIT_CODES_HELP}`,
|
|
180
|
+
projects: `im projects — manage projects (requires an admin-scoped token)
|
|
181
|
+
|
|
182
|
+
Usage:
|
|
183
|
+
im projects list [--json]
|
|
184
|
+
im projects create <name> [--json]
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
im projects list
|
|
188
|
+
im projects create "Planescape Campaign"
|
|
189
|
+
|
|
190
|
+
${EXIT_CODES_HELP}`,
|
|
191
|
+
sources: `im sources — manage sources (requires an admin-scoped token)
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
im sources list [--project uuid] [--limit n] [--json]
|
|
195
|
+
im sources reparse <source-id>
|
|
196
|
+
im sources delete <source-id> [--force]
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
im sources list --limit 20
|
|
200
|
+
im sources reparse 3fa85f64-5717-4562-b3fc-2c963f66afa6
|
|
201
|
+
im sources delete 3fa85f64-5717-4562-b3fc-2c963f66afa6 # prompts
|
|
202
|
+
im sources delete 3fa85f64-5717-4562-b3fc-2c963f66afa6 --force # scripted
|
|
203
|
+
|
|
204
|
+
delete permanently removes the source, its chunks, and its summaries;
|
|
205
|
+
without --force it asks you to type "yes" on stdin first.
|
|
206
|
+
|
|
207
|
+
${EXIT_CODES_HELP}`,
|
|
208
|
+
tokens: `im tokens — manage MCP tokens (requires an admin-scoped token)
|
|
209
|
+
|
|
210
|
+
Usage:
|
|
211
|
+
im tokens list [--json]
|
|
212
|
+
im tokens generate --name <name> --scopes <capture,search[,admin]> [--json]
|
|
213
|
+
im tokens revoke <token-id> [--force]
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
im tokens list
|
|
217
|
+
im tokens generate --name ci-agent --scopes capture,search
|
|
218
|
+
im tokens revoke 3fa85f64-5717-4562-b3fc-2c963f66afa6 --force
|
|
219
|
+
|
|
220
|
+
generate prints the plaintext token ONCE — store it immediately; only a
|
|
221
|
+
hash is kept server-side. revoke is immediate and permanent; without
|
|
222
|
+
--force it asks you to type "yes" on stdin first.
|
|
223
|
+
|
|
224
|
+
${EXIT_CODES_HELP}`,
|
|
225
|
+
docs: `im docs — print the canonical manual
|
|
226
|
+
|
|
227
|
+
Usage:
|
|
228
|
+
im docs Full manual (setup, commands, envelope, exit codes)
|
|
229
|
+
im docs --drop-in ~10-line block for AGENTS.md / CLAUDE.md
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
im docs --drop-in >> AGENTS.md
|
|
233
|
+
|
|
234
|
+
${EXIT_CODES_HELP}`,
|
|
235
|
+
};
|
|
236
|
+
function maybeCommandHelp(command, args, stdio) {
|
|
237
|
+
// Only tokens before a `--` terminator count as help requests, so literal
|
|
238
|
+
// "-h"/"--help" payloads stay storable: `im push text -- -h`.
|
|
239
|
+
const terminator = args.indexOf("--");
|
|
240
|
+
const scanned = terminator === -1 ? args : args.slice(0, terminator);
|
|
241
|
+
if (!scanned.includes("--help") && !scanned.includes("-h"))
|
|
242
|
+
return false;
|
|
243
|
+
stdio.out(COMMAND_HELP[command] ?? USAGE);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
export async function main(argv, env = process.env, stdio = { out: console.log, err: console.error }, readStdin = defaultReadStdin, readLine = defaultReadLine) {
|
|
247
|
+
const [command, ...rest] = argv;
|
|
248
|
+
try {
|
|
249
|
+
switch (command) {
|
|
250
|
+
case undefined:
|
|
251
|
+
case "help":
|
|
252
|
+
case "--help":
|
|
253
|
+
case "-h":
|
|
254
|
+
stdio.out(USAGE);
|
|
255
|
+
return command === undefined ? EXIT.USAGE : EXIT.OK;
|
|
256
|
+
case "config":
|
|
257
|
+
return await runConfig(rest, env, stdio);
|
|
258
|
+
case "push":
|
|
259
|
+
return await runPush(rest, env, stdio, readStdin);
|
|
260
|
+
case "search":
|
|
261
|
+
return await runSearch(rest, env, stdio);
|
|
262
|
+
case "sync": {
|
|
263
|
+
if (maybeCommandHelp("sync", rest, stdio))
|
|
264
|
+
return EXIT.OK;
|
|
265
|
+
return await runSyncCommand(rest, env, stdio);
|
|
266
|
+
}
|
|
267
|
+
case "recent":
|
|
268
|
+
return await runRecent(rest, env, stdio);
|
|
269
|
+
case "stats":
|
|
270
|
+
return await runStats(rest, env, stdio);
|
|
271
|
+
case "projects":
|
|
272
|
+
return await runProjects(rest, env, stdio);
|
|
273
|
+
case "sources":
|
|
274
|
+
return await runSources(rest, env, stdio, readLine);
|
|
275
|
+
case "tokens":
|
|
276
|
+
return await runTokens(rest, env, stdio, readLine);
|
|
277
|
+
case "hooks":
|
|
278
|
+
return await runHooks(rest, env, stdio, readStdin);
|
|
279
|
+
case "docs": {
|
|
280
|
+
if (maybeCommandHelp("docs", rest, stdio))
|
|
281
|
+
return EXIT.OK;
|
|
282
|
+
let dropIn = false;
|
|
283
|
+
try {
|
|
284
|
+
const parsed = parseArgs({ args: rest, options: { "drop-in": { type: "boolean" } } });
|
|
285
|
+
dropIn = parsed.values["drop-in"] === true;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
stdio.err("Usage: im docs [--drop-in]");
|
|
289
|
+
return EXIT.USAGE;
|
|
290
|
+
}
|
|
291
|
+
stdio.out(dropIn ? DROP_IN_BLOCK : CANONICAL_DOC);
|
|
292
|
+
return EXIT.OK;
|
|
293
|
+
}
|
|
294
|
+
default:
|
|
295
|
+
stdio.err(`Unknown command: ${command}\n`);
|
|
296
|
+
stdio.out(USAGE);
|
|
297
|
+
return EXIT.USAGE;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
return reportError(err, stdio);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function client(env, stdio) {
|
|
305
|
+
const resolved = await resolveConfig(env);
|
|
306
|
+
if ("error" in resolved) {
|
|
307
|
+
stdio.err(resolved.error);
|
|
308
|
+
return EXIT.USAGE;
|
|
309
|
+
}
|
|
310
|
+
const warning = await checkConfigPermissions(env);
|
|
311
|
+
if (warning)
|
|
312
|
+
stdio.err(warning);
|
|
313
|
+
return new ImClient(resolved);
|
|
314
|
+
}
|
|
315
|
+
async function runConfig(args, env, stdio) {
|
|
316
|
+
if (maybeCommandHelp("config", args, stdio))
|
|
317
|
+
return EXIT.OK;
|
|
318
|
+
const [sub, key, value] = args;
|
|
319
|
+
if (sub === "show") {
|
|
320
|
+
const file = await readConfigFile(env);
|
|
321
|
+
const url = env.INTEGRAL_MIND_URL ?? file.url;
|
|
322
|
+
const token = env.INTEGRAL_MIND_TOKEN ?? file.token;
|
|
323
|
+
stdio.out(`config file: ${configPath(env)}`);
|
|
324
|
+
stdio.out(`url: ${url ?? "(unset)"}${env.INTEGRAL_MIND_URL ? " (from env)" : ""}`);
|
|
325
|
+
stdio.out(`token: ${token ? `…${token.slice(-4)}` : "(unset)"}${env.INTEGRAL_MIND_TOKEN ? " (from env)" : ""}`);
|
|
326
|
+
return EXIT.OK;
|
|
327
|
+
}
|
|
328
|
+
if (sub === "set" && (key === "url" || key === "token") && value) {
|
|
329
|
+
const path = await writeConfigFile({ [key]: value }, env);
|
|
330
|
+
stdio.out(`Saved ${key} to ${path}`);
|
|
331
|
+
return EXIT.OK;
|
|
332
|
+
}
|
|
333
|
+
stdio.err("Usage: im config set <url|token> <value> | im config show");
|
|
334
|
+
return EXIT.USAGE;
|
|
335
|
+
}
|
|
336
|
+
async function runPush(args, env, stdio, readStdin) {
|
|
337
|
+
if (maybeCommandHelp("push", args, stdio))
|
|
338
|
+
return EXIT.OK;
|
|
339
|
+
const [kind, ...rest] = args;
|
|
340
|
+
switch (kind) {
|
|
341
|
+
case "session":
|
|
342
|
+
return runPushSession(rest, env, stdio, readStdin);
|
|
343
|
+
case "text":
|
|
344
|
+
case "url":
|
|
345
|
+
case "youtube":
|
|
346
|
+
case "file":
|
|
347
|
+
return runPushCapture(kind, rest, env, stdio);
|
|
348
|
+
default:
|
|
349
|
+
stdio.err("Usage: im push <session|text|file|url|youtube> …");
|
|
350
|
+
return EXIT.USAGE;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function runPushSession(args, env, stdio, readStdin) {
|
|
354
|
+
const { values } = parseArgs({
|
|
355
|
+
args,
|
|
356
|
+
options: {
|
|
357
|
+
file: { type: "string" },
|
|
358
|
+
stdin: { type: "boolean" },
|
|
359
|
+
text: { type: "string" },
|
|
360
|
+
agent: { type: "string" },
|
|
361
|
+
title: { type: "string" },
|
|
362
|
+
project: { type: "string" },
|
|
363
|
+
"no-redact": { type: "boolean" },
|
|
364
|
+
json: { type: "boolean" },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
let envelope;
|
|
368
|
+
if (values.text) {
|
|
369
|
+
envelope = {
|
|
370
|
+
agent: values.agent ?? "cli",
|
|
371
|
+
...(values.title ? { title: values.title } : {}),
|
|
372
|
+
text: values.text,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const raw = values.file
|
|
377
|
+
? await readFile(values.file, "utf8")
|
|
378
|
+
: values.stdin
|
|
379
|
+
? await readStdin()
|
|
380
|
+
: null;
|
|
381
|
+
if (raw === null) {
|
|
382
|
+
stdio.err("push session needs --file <envelope.json>, --stdin, or --text <content>");
|
|
383
|
+
return EXIT.USAGE;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
envelope = JSON.parse(raw);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
stdio.err("Envelope is not valid JSON");
|
|
390
|
+
return EXIT.VALIDATION;
|
|
391
|
+
}
|
|
392
|
+
if (values.title)
|
|
393
|
+
envelope.title = values.title;
|
|
394
|
+
}
|
|
395
|
+
if (!values["no-redact"]) {
|
|
396
|
+
const findings = redactEnvelopeInPlace(envelope);
|
|
397
|
+
stdio.err(summarizeFindings(findings));
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
stdio.err("redaction: SKIPPED (--no-redact)");
|
|
401
|
+
}
|
|
402
|
+
if (values.project)
|
|
403
|
+
envelope.project_id = values.project;
|
|
404
|
+
const im = await client(env, stdio);
|
|
405
|
+
if (typeof im === "number")
|
|
406
|
+
return im;
|
|
407
|
+
const result = await im.pushSession(envelope);
|
|
408
|
+
if (values.json) {
|
|
409
|
+
stdio.out(JSON.stringify(result, null, 2));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
stdio.out(`${result.deduplicated ? "Already known" : "Pushed"} session ${String(result.source_id)} (status: ${String(result.status)})`);
|
|
413
|
+
}
|
|
414
|
+
return EXIT.OK;
|
|
415
|
+
}
|
|
416
|
+
export function redactEnvelopeInPlace(envelope) {
|
|
417
|
+
const totals = new Map();
|
|
418
|
+
const scrub = (value) => {
|
|
419
|
+
const { text, findings } = redactSecrets(value);
|
|
420
|
+
for (const f of findings)
|
|
421
|
+
totals.set(f.type, (totals.get(f.type) ?? 0) + f.count);
|
|
422
|
+
return text;
|
|
423
|
+
};
|
|
424
|
+
if (typeof envelope.text === "string")
|
|
425
|
+
envelope.text = scrub(envelope.text);
|
|
426
|
+
if (typeof envelope.title === "string")
|
|
427
|
+
envelope.title = scrub(envelope.title);
|
|
428
|
+
if (Array.isArray(envelope.messages)) {
|
|
429
|
+
for (const message of envelope.messages) {
|
|
430
|
+
if (typeof message === "object" &&
|
|
431
|
+
message !== null &&
|
|
432
|
+
typeof message.content === "string") {
|
|
433
|
+
message.content = scrub(message.content);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return [...totals.entries()]
|
|
438
|
+
.map(([type, count]) => ({ type, count }))
|
|
439
|
+
.sort((a, b) => a.type.localeCompare(b.type));
|
|
440
|
+
}
|
|
441
|
+
async function runPushCapture(kind, args, env, stdio) {
|
|
442
|
+
const { values, positionals } = parseArgs({
|
|
443
|
+
args,
|
|
444
|
+
allowPositionals: true,
|
|
445
|
+
options: {
|
|
446
|
+
title: { type: "string" },
|
|
447
|
+
project: { type: "string" },
|
|
448
|
+
json: { type: "boolean" },
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
const [subject] = positionals;
|
|
452
|
+
if (!subject) {
|
|
453
|
+
stdio.err(`Usage: im push ${kind} <${kind === "file" ? "path" : kind}> [--title] [--project]`);
|
|
454
|
+
return EXIT.USAGE;
|
|
455
|
+
}
|
|
456
|
+
let type = kind === "file" ? "text" : kind;
|
|
457
|
+
let content = subject;
|
|
458
|
+
let title = values.title;
|
|
459
|
+
if (kind === "file") {
|
|
460
|
+
// Thin client: file contents are captured as a text source; the
|
|
461
|
+
// dedicated multipart file type stays a webapp feature for now.
|
|
462
|
+
content = await readFile(subject, "utf8");
|
|
463
|
+
title ??= basename(subject);
|
|
464
|
+
}
|
|
465
|
+
const im = await client(env, stdio);
|
|
466
|
+
if (typeof im === "number")
|
|
467
|
+
return im;
|
|
468
|
+
const result = await im.callTool("capture", {
|
|
469
|
+
content,
|
|
470
|
+
type,
|
|
471
|
+
...(title ? { title } : {}),
|
|
472
|
+
...(values.project ? { project_id: values.project } : {}),
|
|
473
|
+
});
|
|
474
|
+
if (values.json) {
|
|
475
|
+
stdio.out(JSON.stringify(result, null, 2));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
stdio.out(`Captured ${String(result.type)} source ${String(result.source_id)} (status: ${String(result.status)})`);
|
|
479
|
+
}
|
|
480
|
+
return EXIT.OK;
|
|
481
|
+
}
|
|
482
|
+
async function runSearch(args, env, stdio) {
|
|
483
|
+
if (maybeCommandHelp("search", args, stdio))
|
|
484
|
+
return EXIT.OK;
|
|
485
|
+
const { values, positionals } = parseArgs({
|
|
486
|
+
args,
|
|
487
|
+
allowPositionals: true,
|
|
488
|
+
options: {
|
|
489
|
+
limit: { type: "string" },
|
|
490
|
+
project: { type: "string" },
|
|
491
|
+
type: { type: "string", multiple: true },
|
|
492
|
+
after: { type: "string" },
|
|
493
|
+
before: { type: "string" },
|
|
494
|
+
code: { type: "boolean" },
|
|
495
|
+
sessions: { type: "boolean" },
|
|
496
|
+
json: { type: "boolean" },
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
const query = positionals.join(" ").trim();
|
|
500
|
+
if (!query) {
|
|
501
|
+
stdio.err("Usage: im search <query> [--limit n] [--project uuid] [--type t]... [--after date] [--before date] [--code] [--sessions]");
|
|
502
|
+
return EXIT.USAGE;
|
|
503
|
+
}
|
|
504
|
+
if (values.code && values.sessions) {
|
|
505
|
+
stdio.err("--code and --sessions are mutually exclusive");
|
|
506
|
+
return EXIT.USAGE;
|
|
507
|
+
}
|
|
508
|
+
if ((values.code || values.sessions) && values.type?.length) {
|
|
509
|
+
stdio.err(`--type cannot be combined with ${values.code ? "--code" : "--sessions"}`);
|
|
510
|
+
return EXIT.USAGE;
|
|
511
|
+
}
|
|
512
|
+
const im = await client(env, stdio);
|
|
513
|
+
if (typeof im === "number")
|
|
514
|
+
return im;
|
|
515
|
+
if (values.code)
|
|
516
|
+
return runCodeSearch(im, query, values, stdio);
|
|
517
|
+
if (values.sessions)
|
|
518
|
+
return runSessionSearch(im, query, values, stdio);
|
|
519
|
+
const result = (await im.callTool("search", {
|
|
520
|
+
query,
|
|
521
|
+
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
522
|
+
...(values.project ? { project_id: values.project } : {}),
|
|
523
|
+
...(values.type?.length ? { source_types: values.type } : {}),
|
|
524
|
+
...(values.after ? { after: values.after } : {}),
|
|
525
|
+
...(values.before ? { before: values.before } : {}),
|
|
526
|
+
}));
|
|
527
|
+
const rows = result.results ?? [];
|
|
528
|
+
if (values.json) {
|
|
529
|
+
stdio.out(JSON.stringify(rows, null, 2));
|
|
530
|
+
return EXIT.OK;
|
|
531
|
+
}
|
|
532
|
+
if (rows.length === 0) {
|
|
533
|
+
stdio.out(`No results for "${query}".`);
|
|
534
|
+
return EXIT.OK;
|
|
535
|
+
}
|
|
536
|
+
for (const [i, row] of rows.entries()) {
|
|
537
|
+
stdio.out(`${i + 1}. [${row.source_title ?? row.source_id.slice(0, 8)} · chunk ${row.idx} · score ${row.score.toFixed(3)} · chars ${row.char_start}–${row.char_end}]`);
|
|
538
|
+
stdio.out(` ${row.text.slice(0, 300).replaceAll("\n", "\n ")}`);
|
|
539
|
+
}
|
|
540
|
+
return EXIT.OK;
|
|
541
|
+
}
|
|
542
|
+
async function runCodeSearch(im, query, values, stdio) {
|
|
543
|
+
const result = (await im.callTool("code_search", {
|
|
544
|
+
query,
|
|
545
|
+
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
546
|
+
...(values.project ? { project_id: values.project } : {}),
|
|
547
|
+
...(values.after ? { after: values.after } : {}),
|
|
548
|
+
...(values.before ? { before: values.before } : {}),
|
|
549
|
+
}));
|
|
550
|
+
const rows = result.results ?? [];
|
|
551
|
+
if (values.json) {
|
|
552
|
+
stdio.out(JSON.stringify(rows, null, 2));
|
|
553
|
+
return EXIT.OK;
|
|
554
|
+
}
|
|
555
|
+
if (rows.length === 0) {
|
|
556
|
+
stdio.out(`No code results for "${query}". Index a repo with \`im sync repo\`.`);
|
|
557
|
+
return EXIT.OK;
|
|
558
|
+
}
|
|
559
|
+
for (const [i, row] of rows.entries()) {
|
|
560
|
+
const location = `${row.path ?? row.source_title ?? row.source_id.slice(0, 8)}:${row.line_start ?? "?"}-${row.line_end ?? "?"}`;
|
|
561
|
+
stdio.out(`${i + 1}. [${location} · score ${row.score.toFixed(3)}]`);
|
|
562
|
+
stdio.out(` ${row.text.slice(0, 300).replaceAll("\n", "\n ")}`);
|
|
563
|
+
}
|
|
564
|
+
return EXIT.OK;
|
|
565
|
+
}
|
|
566
|
+
async function runSessionSearch(im, query, values, stdio) {
|
|
567
|
+
const result = (await im.callTool("session_search", {
|
|
568
|
+
query,
|
|
569
|
+
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
570
|
+
...(values.project ? { project_id: values.project } : {}),
|
|
571
|
+
...(values.after ? { after: values.after } : {}),
|
|
572
|
+
...(values.before ? { before: values.before } : {}),
|
|
573
|
+
}));
|
|
574
|
+
const rows = result.results ?? [];
|
|
575
|
+
if (values.json) {
|
|
576
|
+
stdio.out(JSON.stringify(rows, null, 2));
|
|
577
|
+
return EXIT.OK;
|
|
578
|
+
}
|
|
579
|
+
if (rows.length === 0) {
|
|
580
|
+
stdio.out(`No session results for "${query}". Push one with \`im push session\`.`);
|
|
581
|
+
return EXIT.OK;
|
|
582
|
+
}
|
|
583
|
+
for (const [i, row] of rows.entries()) {
|
|
584
|
+
stdio.out(`${i + 1}. [${row.source_title ?? row.source_id.slice(0, 8)} · ${row.source_type} · ${row.ingested_at.slice(0, 10)} · score ${row.score.toFixed(3)}]`);
|
|
585
|
+
stdio.out(` ${row.text.slice(0, 300).replaceAll("\n", "\n ")}`);
|
|
586
|
+
}
|
|
587
|
+
return EXIT.OK;
|
|
588
|
+
}
|
|
589
|
+
async function runRecent(args, env, stdio) {
|
|
590
|
+
if (maybeCommandHelp("recent", args, stdio))
|
|
591
|
+
return EXIT.OK;
|
|
592
|
+
const { values } = parseArgs({
|
|
593
|
+
args,
|
|
594
|
+
options: {
|
|
595
|
+
limit: { type: "string" },
|
|
596
|
+
project: { type: "string" },
|
|
597
|
+
json: { type: "boolean" },
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
const im = await client(env, stdio);
|
|
601
|
+
if (typeof im === "number")
|
|
602
|
+
return im;
|
|
603
|
+
const result = (await im.callTool("browse_recent", {
|
|
604
|
+
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
605
|
+
...(values.project ? { project_id: values.project } : {}),
|
|
606
|
+
}));
|
|
607
|
+
const sources = result.sources ?? [];
|
|
608
|
+
if (values.json) {
|
|
609
|
+
stdio.out(JSON.stringify(sources, null, 2));
|
|
610
|
+
return EXIT.OK;
|
|
611
|
+
}
|
|
612
|
+
if (sources.length === 0) {
|
|
613
|
+
stdio.out("No sources yet. Push one with `im push`.");
|
|
614
|
+
return EXIT.OK;
|
|
615
|
+
}
|
|
616
|
+
for (const s of sources) {
|
|
617
|
+
stdio.out(`• ${s.title ?? "(untitled)"} — ${s.type}, ${s.char_count.toLocaleString()} chars, ${s.parse_status}, ${s.ingested_at}`);
|
|
618
|
+
}
|
|
619
|
+
return EXIT.OK;
|
|
620
|
+
}
|
|
621
|
+
async function runStats(args, env, stdio) {
|
|
622
|
+
if (maybeCommandHelp("stats", args, stdio))
|
|
623
|
+
return EXIT.OK;
|
|
624
|
+
const { values } = parseArgs({ args, options: { json: { type: "boolean" } } });
|
|
625
|
+
const im = await client(env, stdio);
|
|
626
|
+
if (typeof im === "number")
|
|
627
|
+
return im;
|
|
628
|
+
const result = await im.callTool("stats", {});
|
|
629
|
+
if (values.json) {
|
|
630
|
+
stdio.out(JSON.stringify(result, null, 2));
|
|
631
|
+
return EXIT.OK;
|
|
632
|
+
}
|
|
633
|
+
stdio.out(`${String(result.source_count)} sources · ${String(result.chunk_count)} chunks · ${String(result.project_count)} projects`);
|
|
634
|
+
stdio.out(`Total AI cost: $${(Number(result.total_cost_cents ?? 0) / 100).toFixed(4)} · last ingested: ${String(result.last_ingested_at ?? "(never)")}`);
|
|
635
|
+
return EXIT.OK;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Confirmation gate for destructive admin commands: --force skips it;
|
|
639
|
+
* otherwise the user must type "yes" on stdin. Reads a single line (not
|
|
640
|
+
* until EOF), so an interactive prompt returns as soon as Enter is hit.
|
|
641
|
+
* Returns true to proceed.
|
|
642
|
+
*/
|
|
643
|
+
async function confirmDestructive(prompt, force, stdio, readLine) {
|
|
644
|
+
if (force)
|
|
645
|
+
return true;
|
|
646
|
+
stdio.err(`${prompt}\nType "yes" to confirm (or re-run with --force):`);
|
|
647
|
+
const answer = (await readLine()).trim().toLowerCase();
|
|
648
|
+
if (answer === "yes")
|
|
649
|
+
return true;
|
|
650
|
+
stdio.err("Aborted — nothing was changed.");
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
async function runProjects(args, env, stdio) {
|
|
654
|
+
if (maybeCommandHelp("projects", args, stdio))
|
|
655
|
+
return EXIT.OK;
|
|
656
|
+
const [sub, ...rest] = args;
|
|
657
|
+
if (sub === "list") {
|
|
658
|
+
const { values } = parseArgs({ args: rest, options: { json: { type: "boolean" } } });
|
|
659
|
+
const im = await client(env, stdio);
|
|
660
|
+
if (typeof im === "number")
|
|
661
|
+
return im;
|
|
662
|
+
const result = await im.request("GET", "/api/admin/projects");
|
|
663
|
+
if (values.json) {
|
|
664
|
+
stdio.out(JSON.stringify(result.projects, null, 2));
|
|
665
|
+
return EXIT.OK;
|
|
666
|
+
}
|
|
667
|
+
if (result.projects.length === 0) {
|
|
668
|
+
stdio.out("No projects yet. Create one with `im projects create <name>`.");
|
|
669
|
+
return EXIT.OK;
|
|
670
|
+
}
|
|
671
|
+
for (const p of result.projects) {
|
|
672
|
+
stdio.out(`• ${p.name} — ${p.id} · ${p.source_count} sources${p.archived ? " · archived" : ""}${p.tags.length ? ` · tags: ${p.tags.join(", ")}` : ""}`);
|
|
673
|
+
}
|
|
674
|
+
return EXIT.OK;
|
|
675
|
+
}
|
|
676
|
+
if (sub === "create") {
|
|
677
|
+
const { values, positionals } = parseArgs({
|
|
678
|
+
args: rest,
|
|
679
|
+
allowPositionals: true,
|
|
680
|
+
options: { json: { type: "boolean" } },
|
|
681
|
+
});
|
|
682
|
+
const name = positionals.join(" ").trim();
|
|
683
|
+
if (!name) {
|
|
684
|
+
stdio.err("Usage: im projects create <name>");
|
|
685
|
+
return EXIT.USAGE;
|
|
686
|
+
}
|
|
687
|
+
const im = await client(env, stdio);
|
|
688
|
+
if (typeof im === "number")
|
|
689
|
+
return im;
|
|
690
|
+
const result = await im.request("POST", "/api/admin/projects", { name });
|
|
691
|
+
if (values.json)
|
|
692
|
+
stdio.out(JSON.stringify(result.project, null, 2));
|
|
693
|
+
else
|
|
694
|
+
stdio.out(`Created project "${result.project.name}" (${result.project.id})`);
|
|
695
|
+
return EXIT.OK;
|
|
696
|
+
}
|
|
697
|
+
stdio.err("Usage: im projects <list|create> …");
|
|
698
|
+
return EXIT.USAGE;
|
|
699
|
+
}
|
|
700
|
+
async function runSources(args, env, stdio, readLine) {
|
|
701
|
+
if (maybeCommandHelp("sources", args, stdio))
|
|
702
|
+
return EXIT.OK;
|
|
703
|
+
const [sub, ...rest] = args;
|
|
704
|
+
if (sub === "list") {
|
|
705
|
+
const { values } = parseArgs({
|
|
706
|
+
args: rest,
|
|
707
|
+
options: {
|
|
708
|
+
project: { type: "string" },
|
|
709
|
+
limit: { type: "string" },
|
|
710
|
+
json: { type: "boolean" },
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
const im = await client(env, stdio);
|
|
714
|
+
if (typeof im === "number")
|
|
715
|
+
return im;
|
|
716
|
+
const query = new URLSearchParams();
|
|
717
|
+
if (values.project)
|
|
718
|
+
query.set("project_id", values.project);
|
|
719
|
+
if (values.limit)
|
|
720
|
+
query.set("limit", values.limit);
|
|
721
|
+
const qs = query.size > 0 ? `?${query.toString()}` : "";
|
|
722
|
+
const result = await im.request("GET", `/api/admin/sources${qs}`);
|
|
723
|
+
if (values.json) {
|
|
724
|
+
stdio.out(JSON.stringify(result.sources, null, 2));
|
|
725
|
+
return EXIT.OK;
|
|
726
|
+
}
|
|
727
|
+
if (result.sources.length === 0) {
|
|
728
|
+
stdio.out("No sources found.");
|
|
729
|
+
return EXIT.OK;
|
|
730
|
+
}
|
|
731
|
+
for (const s of result.sources) {
|
|
732
|
+
stdio.out(`• ${s.title ?? "(untitled)"} — ${s.id} · ${s.type} · ${s.parse_status} · ${s.char_count.toLocaleString()} chars · ${s.ingested_at}`);
|
|
733
|
+
}
|
|
734
|
+
return EXIT.OK;
|
|
735
|
+
}
|
|
736
|
+
if (sub === "reparse") {
|
|
737
|
+
const { values, positionals } = parseArgs({
|
|
738
|
+
args: rest,
|
|
739
|
+
allowPositionals: true,
|
|
740
|
+
options: { json: { type: "boolean" } },
|
|
741
|
+
});
|
|
742
|
+
const [id] = positionals;
|
|
743
|
+
if (!id) {
|
|
744
|
+
stdio.err("Usage: im sources reparse <source-id>");
|
|
745
|
+
return EXIT.USAGE;
|
|
746
|
+
}
|
|
747
|
+
const im = await client(env, stdio);
|
|
748
|
+
if (typeof im === "number")
|
|
749
|
+
return im;
|
|
750
|
+
const result = await im.request("POST", `/api/admin/sources/${encodeURIComponent(id)}/reparse`);
|
|
751
|
+
if (result.warning)
|
|
752
|
+
stdio.err(`Warning: ${result.warning}`);
|
|
753
|
+
if (values.json)
|
|
754
|
+
stdio.out(JSON.stringify(result.source, null, 2));
|
|
755
|
+
else
|
|
756
|
+
stdio.out(`Re-parsing ${result.source.title ?? "(untitled)"} (${result.source.id}) — status: ${result.source.parse_status}`);
|
|
757
|
+
return EXIT.OK;
|
|
758
|
+
}
|
|
759
|
+
if (sub === "delete") {
|
|
760
|
+
const { values, positionals } = parseArgs({
|
|
761
|
+
args: rest,
|
|
762
|
+
allowPositionals: true,
|
|
763
|
+
options: { force: { type: "boolean" }, json: { type: "boolean" } },
|
|
764
|
+
});
|
|
765
|
+
const [id] = positionals;
|
|
766
|
+
if (!id) {
|
|
767
|
+
stdio.err("Usage: im sources delete <source-id> [--force]");
|
|
768
|
+
return EXIT.USAGE;
|
|
769
|
+
}
|
|
770
|
+
const proceed = await confirmDestructive(`This permanently deletes source ${id}, its chunks, and its summaries.`, values.force === true, stdio, readLine);
|
|
771
|
+
if (!proceed)
|
|
772
|
+
return EXIT.USAGE;
|
|
773
|
+
const im = await client(env, stdio);
|
|
774
|
+
if (typeof im === "number")
|
|
775
|
+
return im;
|
|
776
|
+
const result = await im.request("DELETE", `/api/admin/sources/${encodeURIComponent(id)}`);
|
|
777
|
+
if (values.json)
|
|
778
|
+
stdio.out(JSON.stringify(result.deleted, null, 2));
|
|
779
|
+
else
|
|
780
|
+
stdio.out(`Deleted ${result.deleted.title ?? "(untitled)"} (${result.deleted.id})`);
|
|
781
|
+
return EXIT.OK;
|
|
782
|
+
}
|
|
783
|
+
stdio.err("Usage: im sources <list|reparse|delete> …");
|
|
784
|
+
return EXIT.USAGE;
|
|
785
|
+
}
|
|
786
|
+
async function runTokens(args, env, stdio, readLine) {
|
|
787
|
+
if (maybeCommandHelp("tokens", args, stdio))
|
|
788
|
+
return EXIT.OK;
|
|
789
|
+
const [sub, ...rest] = args;
|
|
790
|
+
if (sub === "list") {
|
|
791
|
+
const { values } = parseArgs({ args: rest, options: { json: { type: "boolean" } } });
|
|
792
|
+
const im = await client(env, stdio);
|
|
793
|
+
if (typeof im === "number")
|
|
794
|
+
return im;
|
|
795
|
+
const result = await im.request("GET", "/api/admin/tokens");
|
|
796
|
+
if (values.json) {
|
|
797
|
+
stdio.out(JSON.stringify(result.tokens, null, 2));
|
|
798
|
+
return EXIT.OK;
|
|
799
|
+
}
|
|
800
|
+
if (result.tokens.length === 0) {
|
|
801
|
+
stdio.out("No tokens. Generate one with `im tokens generate`.");
|
|
802
|
+
return EXIT.OK;
|
|
803
|
+
}
|
|
804
|
+
for (const t of result.tokens) {
|
|
805
|
+
stdio.out(`• ${t.name} — …${t.last_four} · ${t.id} · scopes: ${t.scopes.join(",")}${t.revoked_at ? " · REVOKED" : t.last_used_at ? ` · last used ${t.last_used_at}` : " · never used"}`);
|
|
806
|
+
}
|
|
807
|
+
return EXIT.OK;
|
|
808
|
+
}
|
|
809
|
+
if (sub === "generate") {
|
|
810
|
+
const { values } = parseArgs({
|
|
811
|
+
args: rest,
|
|
812
|
+
options: {
|
|
813
|
+
name: { type: "string" },
|
|
814
|
+
scopes: { type: "string" },
|
|
815
|
+
json: { type: "boolean" },
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
const scopes = (values.scopes ?? "")
|
|
819
|
+
.split(",")
|
|
820
|
+
.map((s) => s.trim())
|
|
821
|
+
.filter(Boolean);
|
|
822
|
+
if (!values.name || scopes.length === 0) {
|
|
823
|
+
stdio.err("Usage: im tokens generate --name <name> --scopes <capture,search[,admin]>");
|
|
824
|
+
return EXIT.USAGE;
|
|
825
|
+
}
|
|
826
|
+
const im = await client(env, stdio);
|
|
827
|
+
if (typeof im === "number")
|
|
828
|
+
return im;
|
|
829
|
+
const result = await im.request("POST", "/api/admin/tokens", { name: values.name, scopes });
|
|
830
|
+
if (values.json) {
|
|
831
|
+
stdio.out(JSON.stringify(result.token, null, 2));
|
|
832
|
+
return EXIT.OK;
|
|
833
|
+
}
|
|
834
|
+
stdio.out(result.token.plaintext);
|
|
835
|
+
stdio.err(`Generated token "${result.token.name}" (${result.token.id}, scopes: ${result.token.scopes.join(",")}).\n` +
|
|
836
|
+
"The plaintext above is shown ONCE — store it now; only a hash is kept.");
|
|
837
|
+
return EXIT.OK;
|
|
838
|
+
}
|
|
839
|
+
if (sub === "revoke") {
|
|
840
|
+
const { values, positionals } = parseArgs({
|
|
841
|
+
args: rest,
|
|
842
|
+
allowPositionals: true,
|
|
843
|
+
options: { force: { type: "boolean" }, json: { type: "boolean" } },
|
|
844
|
+
});
|
|
845
|
+
const [id] = positionals;
|
|
846
|
+
if (!id) {
|
|
847
|
+
stdio.err("Usage: im tokens revoke <token-id> [--force]");
|
|
848
|
+
return EXIT.USAGE;
|
|
849
|
+
}
|
|
850
|
+
const proceed = await confirmDestructive(`This immediately and permanently revokes token ${id}; anything using it loses access.`, values.force === true, stdio, readLine);
|
|
851
|
+
if (!proceed)
|
|
852
|
+
return EXIT.USAGE;
|
|
853
|
+
const im = await client(env, stdio);
|
|
854
|
+
if (typeof im === "number")
|
|
855
|
+
return im;
|
|
856
|
+
const result = await im.request("DELETE", `/api/admin/tokens/${encodeURIComponent(id)}`);
|
|
857
|
+
if (values.json)
|
|
858
|
+
stdio.out(JSON.stringify(result.revoked, null, 2));
|
|
859
|
+
else
|
|
860
|
+
stdio.out(`Revoked token ${result.revoked.id}`);
|
|
861
|
+
return EXIT.OK;
|
|
862
|
+
}
|
|
863
|
+
stdio.err("Usage: im tokens <list|generate|revoke> …");
|
|
864
|
+
return EXIT.USAGE;
|
|
865
|
+
}
|
|
866
|
+
async function runHooks(args, env, stdio, readStdin) {
|
|
867
|
+
if (maybeCommandHelp("hooks", args, stdio))
|
|
868
|
+
return EXIT.OK;
|
|
869
|
+
const [action, target] = args;
|
|
870
|
+
if (action === "claude-session-end") {
|
|
871
|
+
// Invoked by Claude Code, never interactively. Always exits 0.
|
|
872
|
+
return await runClaudeSessionEnd(await readStdin(), defaultSessionEndDeps(env));
|
|
873
|
+
}
|
|
874
|
+
if (action === "install" && target === "claude") {
|
|
875
|
+
const outcome = await installClaudeHook(env);
|
|
876
|
+
stdio.out(outcome === "installed"
|
|
877
|
+
? "Installed the Claude Code SessionEnd hook. Sessions push automatically from now on."
|
|
878
|
+
: "Claude Code SessionEnd hook already installed — nothing to do.");
|
|
879
|
+
return EXIT.OK;
|
|
880
|
+
}
|
|
881
|
+
if (action === "uninstall" && target === "claude") {
|
|
882
|
+
const outcome = await uninstallClaudeHook(env);
|
|
883
|
+
stdio.out(outcome === "uninstalled"
|
|
884
|
+
? "Removed the Claude Code SessionEnd hook."
|
|
885
|
+
: "No Claude Code SessionEnd hook found — nothing to do.");
|
|
886
|
+
return EXIT.OK;
|
|
887
|
+
}
|
|
888
|
+
stdio.err("Usage: im hooks <install|uninstall> claude");
|
|
889
|
+
return EXIT.USAGE;
|
|
890
|
+
}
|
|
891
|
+
function reportError(err, stdio) {
|
|
892
|
+
if (err instanceof ApiError) {
|
|
893
|
+
stdio.err(`Error (${err.code}): ${err.message}`);
|
|
894
|
+
if (err.status === 401 || err.status === 403)
|
|
895
|
+
return EXIT.AUTH;
|
|
896
|
+
if (err.status === 404)
|
|
897
|
+
return EXIT.NOT_FOUND;
|
|
898
|
+
if (err.status === 400 || err.status === 409 || err.status === 422)
|
|
899
|
+
return EXIT.VALIDATION;
|
|
900
|
+
return EXIT.RUNTIME;
|
|
901
|
+
}
|
|
902
|
+
stdio.err(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
903
|
+
return EXIT.RUNTIME;
|
|
904
|
+
}
|
|
905
|
+
async function defaultReadStdin() {
|
|
906
|
+
const chunks = [];
|
|
907
|
+
for await (const chunk of process.stdin)
|
|
908
|
+
chunks.push(chunk);
|
|
909
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Read a single line from stdin — used by confirmation prompts, where
|
|
913
|
+
* waiting for EOF (defaultReadStdin) would hang an interactive terminal
|
|
914
|
+
* after the user presses Enter. Works for piped input too (`echo yes |`).
|
|
915
|
+
*/
|
|
916
|
+
async function defaultReadLine() {
|
|
917
|
+
const rl = createInterface({ input: process.stdin });
|
|
918
|
+
try {
|
|
919
|
+
for await (const line of rl)
|
|
920
|
+
return line;
|
|
921
|
+
return "";
|
|
922
|
+
}
|
|
923
|
+
finally {
|
|
924
|
+
rl.close();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// Run main() only when this module is the executed entrypoint — including
|
|
928
|
+
// npm bin shims, where argv[1] is a symlink (im or integral-mind) that
|
|
929
|
+
// realpaths to this file. Importing the module (tests) never triggers it.
|
|
930
|
+
const isDirectRun = (() => {
|
|
931
|
+
if (!process.argv[1])
|
|
932
|
+
return false;
|
|
933
|
+
try {
|
|
934
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
})();
|
|
940
|
+
if (isDirectRun) {
|
|
941
|
+
main(process.argv.slice(2)).then((code) => {
|
|
942
|
+
process.exitCode = code;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
//# sourceMappingURL=cli.js.map
|