@stitchdb/cli 0.1.0 → 0.2.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/dist/cli.js +262 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,24 +2,36 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `stitch` — CLI for the Stitch memory + agent control plane.
|
|
4
4
|
*
|
|
5
|
-
* stitch login
|
|
6
|
-
* stitch logout
|
|
7
|
-
* stitch whoami
|
|
5
|
+
* stitch login Save your API key locally.
|
|
6
|
+
* stitch logout Remove the saved key.
|
|
7
|
+
* stitch whoami Show the configured key (masked).
|
|
8
|
+
*
|
|
9
|
+
* stitch install One-shot: install Stitch as an MCP
|
|
10
|
+
* server in Claude Code, wire auto-log
|
|
11
|
+
* hooks, write a CLAUDE.md instruction
|
|
12
|
+
* in the current project so Claude
|
|
13
|
+
* auto-recalls past context at session start.
|
|
8
14
|
*
|
|
9
15
|
* stitch remember <text> [--kind X] [--tag a,b]
|
|
10
16
|
* stitch recall <query> [-k 5]
|
|
11
17
|
*
|
|
12
|
-
* stitch
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* stitch
|
|
16
|
-
* stitch
|
|
18
|
+
* stitch thread append --role <r> [--thread <t>] [--content <c>]
|
|
19
|
+
* Append a turn (or read content from stdin).
|
|
20
|
+
* Used by Claude Code hooks for auto-logging.
|
|
21
|
+
* stitch thread recall [--thread <t>] [--last 10] [--semantic <q>]
|
|
22
|
+
* stitch thread current Print the thread name for the current repo / cwd.
|
|
23
|
+
*
|
|
24
|
+
* stitch agent register <name> Create a new agent identity.
|
|
25
|
+
* stitch agent list List your agents (online status).
|
|
26
|
+
* stitch agent run --id <agent_id> Hold open a control channel.
|
|
27
|
+
* stitch agent dispatch <id> <prompt> Send a command to a connected agent.
|
|
28
|
+
* stitch agent revoke <id> Revoke an agent.
|
|
17
29
|
*
|
|
18
30
|
* stitch workspace create <name>
|
|
19
31
|
* stitch workspace list
|
|
20
|
-
* stitch workspace use <id>
|
|
32
|
+
* stitch workspace use <id>
|
|
21
33
|
*
|
|
22
|
-
* stitch mcp-install
|
|
34
|
+
* stitch mcp-install Print the `claude mcp add stitch …` command.
|
|
23
35
|
*/
|
|
24
36
|
import * as fs from 'node:fs';
|
|
25
37
|
import * as path from 'node:path';
|
|
@@ -306,6 +318,232 @@ async function cmdAgent(args) {
|
|
|
306
318
|
console.error('Usage: stitch agent [register|list|run|dispatch|revoke] …');
|
|
307
319
|
process.exit(2);
|
|
308
320
|
}
|
|
321
|
+
// ── Threads — append / recall / current ───────────────────────────────────
|
|
322
|
+
function inferThread() {
|
|
323
|
+
// Reasonable default: <git-repo-name>/<branch> if we're in a git repo,
|
|
324
|
+
// else the cwd's basename. Hooks call this to get a stable thread per project.
|
|
325
|
+
try {
|
|
326
|
+
const dir = process.cwd();
|
|
327
|
+
let cur = dir;
|
|
328
|
+
for (let i = 0; i < 8; i++) {
|
|
329
|
+
if (fs.existsSync(path.join(cur, '.git'))) {
|
|
330
|
+
const parent = path.dirname(cur);
|
|
331
|
+
const repoName = path.basename(cur);
|
|
332
|
+
let branch = 'main';
|
|
333
|
+
try {
|
|
334
|
+
const head = fs.readFileSync(path.join(cur, '.git', 'HEAD'), 'utf8').trim();
|
|
335
|
+
const m = head.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
336
|
+
if (m)
|
|
337
|
+
branch = m[1];
|
|
338
|
+
}
|
|
339
|
+
catch { /* detached */ }
|
|
340
|
+
return `${repoName}/${branch}`;
|
|
341
|
+
void parent;
|
|
342
|
+
}
|
|
343
|
+
const next = path.dirname(cur);
|
|
344
|
+
if (next === cur)
|
|
345
|
+
break;
|
|
346
|
+
cur = next;
|
|
347
|
+
}
|
|
348
|
+
return path.basename(dir);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return 'default';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function cmdThread(args) {
|
|
355
|
+
const cfg = loadConfig();
|
|
356
|
+
const stitch = client(cfg);
|
|
357
|
+
const sub = args[0];
|
|
358
|
+
const rest = args.slice(1);
|
|
359
|
+
if (sub === 'append') {
|
|
360
|
+
const role = parseFlag(rest, ['--role']);
|
|
361
|
+
let thread = parseFlag(rest, ['--thread', '-t']) || inferThread();
|
|
362
|
+
let content = parseFlag(rest, ['--content', '-c']);
|
|
363
|
+
if (!content) {
|
|
364
|
+
// If no --content given, read from stdin (so a hook can pipe in the message).
|
|
365
|
+
content = await readStdinAll();
|
|
366
|
+
}
|
|
367
|
+
if (!role) {
|
|
368
|
+
console.error('Usage: stitch thread append --role user|assistant --content <text> [--thread name]');
|
|
369
|
+
process.exit(2);
|
|
370
|
+
}
|
|
371
|
+
if (!content) {
|
|
372
|
+
console.error('No content provided (use --content or pipe stdin).');
|
|
373
|
+
process.exit(2);
|
|
374
|
+
}
|
|
375
|
+
const out = await stitch.thread(thread).append({ role, content });
|
|
376
|
+
console.log(`Appended ${out.turn_id} to ${thread} (${out.thread_id})`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (sub === 'recall') {
|
|
380
|
+
const positionals = positional(rest);
|
|
381
|
+
const thread = parseFlag(rest, ['--thread', '-t']) || positionals[0] || inferThread();
|
|
382
|
+
const last = Number(parseFlag(rest, ['--last']) || '10');
|
|
383
|
+
const semantic = parseFlag(rest, ['--semantic']);
|
|
384
|
+
const out = await stitch.thread(thread).recall({ last, semantic, semantic_k: 5 });
|
|
385
|
+
if (!out.thread_id) {
|
|
386
|
+
console.log('Thread not found.');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (out.recent.length === 0 && out.semantic.length === 0) {
|
|
390
|
+
console.log('Empty thread.');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (out.recent.length) {
|
|
394
|
+
console.log(`# Recent (${out.recent.length})`);
|
|
395
|
+
for (const t of out.recent)
|
|
396
|
+
console.log(`- ${t.role}: ${t.content}`);
|
|
397
|
+
}
|
|
398
|
+
if (out.semantic.length) {
|
|
399
|
+
console.log(`\n# Semantic matches`);
|
|
400
|
+
for (const t of out.semantic)
|
|
401
|
+
console.log(`- ${t.role} (score ${(t.score ?? 0).toFixed(3)}): ${t.content}`);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (sub === 'current') {
|
|
406
|
+
console.log(inferThread());
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.error('Usage: stitch thread [append|recall|current] …');
|
|
410
|
+
process.exit(2);
|
|
411
|
+
}
|
|
412
|
+
function readStdinAll() {
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
let data = '';
|
|
415
|
+
if (process.stdin.isTTY) {
|
|
416
|
+
resolve('');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
process.stdin.setEncoding('utf8');
|
|
420
|
+
process.stdin.on('data', (c) => (data += c));
|
|
421
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// ── One-shot install: MCP server + auto-log hooks + project CLAUDE.md ─────
|
|
425
|
+
async function cmdInstall(args) {
|
|
426
|
+
const cfg = loadConfig();
|
|
427
|
+
if (!cfg.apiKey) {
|
|
428
|
+
console.error('Run `stitch login` first.');
|
|
429
|
+
process.exit(2);
|
|
430
|
+
}
|
|
431
|
+
const stitch = client(cfg);
|
|
432
|
+
const ws = await stitch.resolveWorkspace();
|
|
433
|
+
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
434
|
+
const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
|
|
435
|
+
const noMcp = hasFlag(args, ['--no-mcp']);
|
|
436
|
+
const noHooks = hasFlag(args, ['--no-hooks']);
|
|
437
|
+
const noClaudeMd = hasFlag(args, ['--no-claude-md']);
|
|
438
|
+
// 1. claude mcp add
|
|
439
|
+
if (!noMcp) {
|
|
440
|
+
const claudePath = process.env.STITCH_CLAUDE_BIN || 'claude';
|
|
441
|
+
process.stdout.write('• Adding Stitch as an MCP server in Claude Code… ');
|
|
442
|
+
const { exit_code, stderr } = await runSilent(claudePath, [
|
|
443
|
+
'mcp', 'add', '--transport', 'http', '--scope', 'user', 'stitch', mcpUrl,
|
|
444
|
+
'-H', `Authorization: Bearer ${cfg.apiKey}`,
|
|
445
|
+
]);
|
|
446
|
+
if (exit_code === 0)
|
|
447
|
+
console.log('ok');
|
|
448
|
+
else if (stderr.includes('already exists'))
|
|
449
|
+
console.log('already configured');
|
|
450
|
+
else
|
|
451
|
+
console.log(`failed (${stderr.trim().slice(0, 120)})`);
|
|
452
|
+
}
|
|
453
|
+
// 2. Hooks for auto-logging conversations
|
|
454
|
+
if (!noHooks) {
|
|
455
|
+
process.stdout.write('• Wiring auto-log hooks (UserPromptSubmit + Stop)… ');
|
|
456
|
+
try {
|
|
457
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
458
|
+
const existing = fs.existsSync(settingsPath)
|
|
459
|
+
? JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
460
|
+
: {};
|
|
461
|
+
existing.hooks = existing.hooks || {};
|
|
462
|
+
existing.hooks.UserPromptSubmit = mergeHook(existing.hooks.UserPromptSubmit, STITCH_USER_HOOK);
|
|
463
|
+
existing.hooks.Stop = mergeHook(existing.hooks.Stop, STITCH_STOP_HOOK);
|
|
464
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
465
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
466
|
+
console.log('ok');
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
console.log(`skipped (${e.message})`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 3. CLAUDE.md instruction in cwd so Claude auto-recalls at session start
|
|
473
|
+
if (!noClaudeMd) {
|
|
474
|
+
const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
475
|
+
const block = STITCH_CLAUDE_MD_BLOCK;
|
|
476
|
+
let body = '';
|
|
477
|
+
let action = 'created';
|
|
478
|
+
if (fs.existsSync(claudeMd)) {
|
|
479
|
+
body = fs.readFileSync(claudeMd, 'utf8');
|
|
480
|
+
if (body.includes('<!-- stitch:auto -->')) {
|
|
481
|
+
action = 'already present';
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
body = body.trimEnd() + '\n\n' + block + '\n';
|
|
485
|
+
action = 'appended';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
body = block + '\n';
|
|
490
|
+
}
|
|
491
|
+
if (action !== 'already present')
|
|
492
|
+
fs.writeFileSync(claudeMd, body);
|
|
493
|
+
console.log(`• ${action === 'created' ? 'Wrote' : action === 'appended' ? 'Appended to' : 'Found'} ./CLAUDE.md (${action})`);
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
console.log('Done. Open a fresh `claude` session — Stitch is wired.');
|
|
497
|
+
console.log(' • remember/recall tools are available');
|
|
498
|
+
console.log(' • every turn auto-logs to the thread for this repo');
|
|
499
|
+
console.log(' • Claude auto-pulls past context at session start (per CLAUDE.md)');
|
|
500
|
+
}
|
|
501
|
+
const STITCH_USER_HOOK = {
|
|
502
|
+
matcher: '*',
|
|
503
|
+
hooks: [
|
|
504
|
+
{ type: 'command', command: `bash -c 'jq -r .prompt | stitch thread append --role user --thread "$(stitch thread current)" 2>/dev/null || true'` },
|
|
505
|
+
],
|
|
506
|
+
};
|
|
507
|
+
const STITCH_STOP_HOOK = {
|
|
508
|
+
matcher: '*',
|
|
509
|
+
hooks: [
|
|
510
|
+
{ type: 'command', command: `bash -c 'jq -r .response.text 2>/dev/null | stitch thread append --role assistant --thread "$(stitch thread current)" 2>/dev/null || true'` },
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
function mergeHook(existing, entry) {
|
|
514
|
+
const arr = Array.isArray(existing) ? existing.slice() : [];
|
|
515
|
+
// Replace any earlier Stitch entry; identify by marker substring.
|
|
516
|
+
const isStitch = (h) => JSON.stringify(h).includes('stitch thread append');
|
|
517
|
+
const filtered = arr.filter((h) => !isStitch(h));
|
|
518
|
+
filtered.push(entry);
|
|
519
|
+
return filtered;
|
|
520
|
+
}
|
|
521
|
+
const STITCH_CLAUDE_MD_BLOCK = `<!-- stitch:auto -->
|
|
522
|
+
## Stitch memory
|
|
523
|
+
|
|
524
|
+
This project is wired to Stitch (memory layer for AI agents).
|
|
525
|
+
|
|
526
|
+
When a session starts here, **call \`recall\` from the \`stitch\` MCP server first**
|
|
527
|
+
to pull any prior facts, decisions, or discussion threads about this repo before
|
|
528
|
+
answering. The thread name for this repo is reported by \`stitch thread current\`.
|
|
529
|
+
|
|
530
|
+
When you learn something durable about this codebase or its conventions, call
|
|
531
|
+
\`remember\` to save it — \`fact\`, \`decision\`, \`snippet\`, or \`preference\`.
|
|
532
|
+
|
|
533
|
+
Every user message and assistant reply is also auto-logged to a per-repo thread
|
|
534
|
+
via Claude Code hooks, so future sessions can \`thread_recall\` to resume context.
|
|
535
|
+
<!-- /stitch:auto -->`;
|
|
536
|
+
async function runSilent(cmd, args) {
|
|
537
|
+
return new Promise((resolve) => {
|
|
538
|
+
const child = spawn(cmd, args);
|
|
539
|
+
let stdout = '';
|
|
540
|
+
let stderr = '';
|
|
541
|
+
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
542
|
+
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
543
|
+
child.on('error', (e) => resolve({ stdout, stderr: stderr + '\n' + e.message, exit_code: 127 }));
|
|
544
|
+
child.on('close', (code) => resolve({ stdout, stderr, exit_code: code ?? 0 }));
|
|
545
|
+
});
|
|
546
|
+
}
|
|
309
547
|
async function cmdMcpInstall(args) {
|
|
310
548
|
const cfg = loadConfig();
|
|
311
549
|
if (!cfg.apiKey) {
|
|
@@ -445,9 +683,21 @@ function help() {
|
|
|
445
683
|
stitch whoami Show the configured key.
|
|
446
684
|
stitch logout
|
|
447
685
|
|
|
686
|
+
stitch install [--no-mcp] [--no-hooks] [--no-claude-md]
|
|
687
|
+
One-shot: register Stitch as an MCP
|
|
688
|
+
server in Claude Code, wire up auto-log
|
|
689
|
+
hooks, drop a CLAUDE.md instruction in
|
|
690
|
+
the current project so Claude auto-recalls
|
|
691
|
+
at session start.
|
|
692
|
+
|
|
448
693
|
stitch remember <text> [--kind X] [--tag a,b]
|
|
449
694
|
stitch recall <query> [-k 5]
|
|
450
695
|
|
|
696
|
+
stitch thread append --role user|assistant [--thread <name>] [--content <text>]
|
|
697
|
+
stitch thread recall [--thread <name>] [--last 10] [--semantic <q>]
|
|
698
|
+
stitch thread current Print the auto-derived thread name
|
|
699
|
+
for the current repo / cwd.
|
|
700
|
+
|
|
451
701
|
stitch workspace [list | create <name> | use <id>]
|
|
452
702
|
|
|
453
703
|
stitch agent register <name> Create an agent identity (id only).
|
|
@@ -469,6 +719,8 @@ async function main(argv) {
|
|
|
469
719
|
case 'whoami': return cmdWhoami();
|
|
470
720
|
case 'remember': return cmdRemember(rest);
|
|
471
721
|
case 'recall': return cmdRecall(rest);
|
|
722
|
+
case 'thread': return cmdThread(rest);
|
|
723
|
+
case 'install': return cmdInstall(rest);
|
|
472
724
|
case 'workspace':
|
|
473
725
|
case 'ws': return cmdWorkspace(rest);
|
|
474
726
|
case 'agent': return cmdAgent(rest);
|