@stitchdb/cli 0.3.0 → 0.3.2

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 +126 -8
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -440,6 +440,123 @@ function readStdinAll() {
440
440
  process.stdin.on('end', () => resolve(data.trim()));
441
441
  });
442
442
  }
443
+ // ── Claude Code hook handler ──────────────────────────────────────────────
444
+ // Reads hook event JSON from stdin, figures out what to log, posts to Stitch.
445
+ // Designed to never fail loudly — Claude Code shouldn't be interrupted because
446
+ // our memory layer hiccupped.
447
+ async function cmdHook(args) {
448
+ const cfg = loadConfig();
449
+ if (!cfg.apiKey)
450
+ return; // not logged in, silently skip
451
+ let raw = '';
452
+ try {
453
+ raw = await readStdinAll();
454
+ }
455
+ catch {
456
+ return;
457
+ }
458
+ if (!raw)
459
+ return;
460
+ let event;
461
+ try {
462
+ event = JSON.parse(raw);
463
+ }
464
+ catch {
465
+ return;
466
+ }
467
+ const eventName = event?.hook_event_name || args[0] || '';
468
+ const cwd = event?.cwd;
469
+ const threadName = inferThreadFor(cwd) || 'default';
470
+ let role = null;
471
+ let content = '';
472
+ if (eventName === 'UserPromptSubmit') {
473
+ role = 'user';
474
+ content = String(event?.prompt || '');
475
+ }
476
+ else if (eventName === 'Stop') {
477
+ role = 'assistant';
478
+ content = lastAssistantTextFromTranscript(event?.transcript_path);
479
+ }
480
+ else {
481
+ return;
482
+ }
483
+ content = content.trim();
484
+ if (!content)
485
+ return;
486
+ try {
487
+ const stitch = client(cfg);
488
+ await stitch.thread(threadName).append({ role, content });
489
+ }
490
+ catch {
491
+ /* silent */
492
+ }
493
+ }
494
+ function inferThreadFor(cwd) {
495
+ const dir = cwd && fs.existsSync(cwd) ? cwd : process.cwd();
496
+ let cur = dir;
497
+ for (let i = 0; i < 8; i++) {
498
+ if (fs.existsSync(path.join(cur, '.git'))) {
499
+ const repoName = path.basename(cur);
500
+ let branch = 'main';
501
+ try {
502
+ const head = fs.readFileSync(path.join(cur, '.git', 'HEAD'), 'utf8').trim();
503
+ const m = head.match(/^ref:\s+refs\/heads\/(.+)$/);
504
+ if (m)
505
+ branch = m[1];
506
+ }
507
+ catch { /* detached */ }
508
+ return `${repoName}/${branch}`;
509
+ }
510
+ const next = path.dirname(cur);
511
+ if (next === cur)
512
+ break;
513
+ cur = next;
514
+ }
515
+ return path.basename(dir);
516
+ }
517
+ function lastAssistantTextFromTranscript(transcriptPath) {
518
+ if (!transcriptPath || !fs.existsSync(transcriptPath))
519
+ return '';
520
+ let text = '';
521
+ try {
522
+ const raw = fs.readFileSync(transcriptPath, 'utf8');
523
+ const lines = raw.split('\n').filter((l) => l.trim());
524
+ // Walk from the end, find the last entry whose role is 'assistant'.
525
+ for (let i = lines.length - 1; i >= 0; i--) {
526
+ let entry;
527
+ try {
528
+ entry = JSON.parse(lines[i]);
529
+ }
530
+ catch {
531
+ continue;
532
+ }
533
+ const msg = entry?.message ?? entry;
534
+ const role = msg?.role ?? entry?.role ?? entry?.type;
535
+ if (role !== 'assistant')
536
+ continue;
537
+ const content = msg?.content ?? msg?.text ?? entry?.content;
538
+ if (typeof content === 'string') {
539
+ text = content;
540
+ }
541
+ else if (Array.isArray(content)) {
542
+ const parts = [];
543
+ for (const c of content) {
544
+ if (typeof c === 'string')
545
+ parts.push(c);
546
+ else if (c?.type === 'text' && typeof c.text === 'string')
547
+ parts.push(c.text);
548
+ else if (typeof c?.text === 'string')
549
+ parts.push(c.text);
550
+ }
551
+ text = parts.join('\n').trim();
552
+ }
553
+ if (text)
554
+ break;
555
+ }
556
+ }
557
+ catch { /* ignore */ }
558
+ return text;
559
+ }
443
560
  function loadUpdateCache() {
444
561
  try {
445
562
  return JSON.parse(fs.readFileSync(UPDATE_CACHE, 'utf8'));
@@ -596,22 +713,22 @@ async function cmdInstall(args) {
596
713
  console.log(' • every turn auto-logs to the thread for this repo');
597
714
  console.log(' • Claude auto-pulls past context at session start (per CLAUDE.md)');
598
715
  }
716
+ // The hook calls a single CLI subcommand (`stitch _hook`) that reads the event
717
+ // JSON from stdin, picks the right field (or reads transcript_path for Stop),
718
+ // and posts to the right per-repo thread. Keeps user settings.json minimal and
719
+ // lets us evolve the parsing without touching their config later.
599
720
  const STITCH_USER_HOOK = {
600
721
  matcher: '*',
601
- hooks: [
602
- { type: 'command', command: `bash -c 'jq -r .prompt | stitch thread append --role user --thread "$(stitch thread current)" 2>/dev/null || true'` },
603
- ],
722
+ hooks: [{ type: 'command', command: 'stitch _hook' }],
604
723
  };
605
724
  const STITCH_STOP_HOOK = {
606
725
  matcher: '*',
607
- hooks: [
608
- { 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'` },
609
- ],
726
+ hooks: [{ type: 'command', command: 'stitch _hook' }],
610
727
  };
611
728
  function mergeHook(existing, entry) {
612
729
  const arr = Array.isArray(existing) ? existing.slice() : [];
613
- // Replace any earlier Stitch entry; identify by marker substring.
614
- const isStitch = (h) => JSON.stringify(h).includes('stitch thread append');
730
+ // Replace any earlier Stitch entry; identify by the marker.
731
+ const isStitch = (h) => JSON.stringify(h).includes('stitch _hook') || JSON.stringify(h).includes('stitch thread append');
615
732
  const filtered = arr.filter((h) => !isStitch(h));
616
733
  filtered.push(entry);
617
734
  return filtered;
@@ -822,6 +939,7 @@ async function main(argv) {
822
939
  case 'recall': return cmdRecall(rest);
823
940
  case 'thread': return cmdThread(rest);
824
941
  case 'install': return cmdInstall(rest);
942
+ case '_hook': return cmdHook(rest);
825
943
  case 'update':
826
944
  case 'upgrade': return cmdUpdate(rest);
827
945
  case 'version':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "license": "MIT",
17
17
  "engines": { "node": ">=20" },
18
18
  "dependencies": {
19
- "@stitchdb/agent": "^0.1.0"
19
+ "@stitchdb/agent": "^0.1.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "^5.4.0",