@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.
Files changed (2) hide show
  1. package/dist/cli.js +262 -10
  2. 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 Save your API key locally.
6
- * stitch logout Remove the saved key.
7
- * stitch whoami Show the configured key (masked).
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 agent register <name> Create a new agent identity.
13
- * stitch agent list List your agents (online status).
14
- * stitch agent run --id <agent_id> Hold open a control channel + execute dispatched commands.
15
- * stitch agent dispatch <id> <prompt> Send a command to a connected agent.
16
- * stitch agent revoke <id> Revoke an agent.
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> Set the default workspace.
32
+ * stitch workspace use <id>
21
33
  *
22
- * stitch mcp-install [--id <agent_id>] Print the `claude mcp add stitch …` command for your workspace.
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {