@stitchdb/cli 0.3.1 → 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.
- package/dist/cli.js +126 -8
- package/package.json +1 -1
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
|
|
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':
|