@stitchdb/cli 0.7.1 → 0.7.3
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 +80 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -495,7 +495,18 @@ async function cmdHook(args) {
|
|
|
495
495
|
await handlePostReadHook(cfg, event, cwd || process.cwd()).catch(() => { });
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
498
|
-
// ── SessionStart: inject prior context
|
|
498
|
+
// ── SessionStart: self-heal hook config + inject prior context ─────────
|
|
499
|
+
// Self-heal: a CLI upgrade often introduces new hooks (e.g. PreToolUse on
|
|
500
|
+
// Read in 0.7.0). Without re-running `stitch install`, the old settings.json
|
|
501
|
+
// would silently miss them. Each SessionStart we (re)wire any missing
|
|
502
|
+
// Stitch hook entries — never touching non-Stitch ones.
|
|
503
|
+
if (eventName === 'SessionStart') {
|
|
504
|
+
ensureHooksUpToDate();
|
|
505
|
+
// Catch-up distillation: if a previous session ended with undistilled
|
|
506
|
+
// turns (user exited under threshold), distill them now in the
|
|
507
|
+
// background so they surface in this session's context very soon.
|
|
508
|
+
maybeAutoDistill(threadName, { force: true }).catch(() => { });
|
|
509
|
+
}
|
|
499
510
|
// Strategy: prefer distilled memories (dense facts) over raw turns. Only
|
|
500
511
|
// include raw turns for the last 5 to give the agent immediate continuation.
|
|
501
512
|
if (eventName === 'SessionStart') {
|
|
@@ -957,9 +968,13 @@ async function cmdLink(args) {
|
|
|
957
968
|
// Triggered manually (`stitch distill`), and automatically by the Stop hook
|
|
958
969
|
// when conditions are met (cooldown + new-turn threshold).
|
|
959
970
|
const DISTILL_STATE_FILE = path.join(CONFIG_DIR, 'distill-state.json');
|
|
960
|
-
|
|
961
|
-
|
|
971
|
+
// No time-based cooldown — purely turn-count driven. The previous time gate
|
|
972
|
+
// could leave turns undistilled if the user exited Claude before the next
|
|
973
|
+
// firing window. Now: every Stop checks the pending-turn count, and if the
|
|
974
|
+
// user exits below threshold, the next SessionStart catches up.
|
|
975
|
+
const DISTILL_MIN_NEW_TURNS = 5; // distill once 5+ new turns are pending
|
|
962
976
|
const DISTILL_BATCH_SIZE = 30; // turns per distillation pass
|
|
977
|
+
const DISTILL_DEBOUNCE_MS = 90 * 1000; // small in-flight guard so a flurry of Stops doesn't fire N spawns
|
|
963
978
|
function loadDistillState() {
|
|
964
979
|
try {
|
|
965
980
|
return JSON.parse(fs.readFileSync(DISTILL_STATE_FILE, 'utf8'));
|
|
@@ -1196,14 +1211,24 @@ async function distillClear(args) {
|
|
|
1196
1211
|
}
|
|
1197
1212
|
console.log(`Cleared ${deleted} memories.`);
|
|
1198
1213
|
}
|
|
1199
|
-
// Triggered from
|
|
1200
|
-
|
|
1214
|
+
// Triggered from Stop hook AND SessionStart catch-up. Fire-and-forget.
|
|
1215
|
+
//
|
|
1216
|
+
// Decision logic:
|
|
1217
|
+
// - If `pending = currentTurns - lastDistilledTurns` >= DISTILL_MIN_NEW_TURNS,
|
|
1218
|
+
// spawn a distill pass. No time-based cooldown — leaving turns undistilled
|
|
1219
|
+
// because the user happened to exit before the next firing window is the
|
|
1220
|
+
// wrong behaviour.
|
|
1221
|
+
// - SessionStart calls this with `force = true` when ANY pending turns exist
|
|
1222
|
+
// so a session that ended mid-batch (e.g. 3 pending) still gets caught up.
|
|
1223
|
+
// - DISTILL_DEBOUNCE_MS is a tiny in-process guard so a flurry of Stop
|
|
1224
|
+
// events from a single agent run doesn't trigger N parallel claude -p
|
|
1225
|
+
// spawns; once one is in flight, others within the window become no-ops.
|
|
1226
|
+
async function maybeAutoDistill(thread, opts = {}) {
|
|
1201
1227
|
const state = loadDistillState();
|
|
1202
1228
|
const meta = state.threads[thread] || { lastDistilledAt: 0, lastTurnAt: 0, lastTurnCount: 0 };
|
|
1203
|
-
//
|
|
1204
|
-
if (Date.now() - meta.lastDistilledAt <
|
|
1229
|
+
// In-flight debounce: if we just spawned one, skip.
|
|
1230
|
+
if (Date.now() - meta.lastDistilledAt < DISTILL_DEBOUNCE_MS)
|
|
1205
1231
|
return;
|
|
1206
|
-
// Need at least N new turns since last pass.
|
|
1207
1232
|
const cfg = loadConfig();
|
|
1208
1233
|
const stitch = client(cfg);
|
|
1209
1234
|
let recallSize = 0;
|
|
@@ -1214,17 +1239,21 @@ async function maybeAutoDistill(thread) {
|
|
|
1214
1239
|
catch {
|
|
1215
1240
|
return;
|
|
1216
1241
|
}
|
|
1217
|
-
|
|
1242
|
+
const pending = recallSize - meta.lastTurnCount;
|
|
1243
|
+
// Default path: only distill once enough turns have piled up.
|
|
1244
|
+
// SessionStart catch-up: distill if there's ANY pending turn.
|
|
1245
|
+
const threshold = opts.force ? 1 : DISTILL_MIN_NEW_TURNS;
|
|
1246
|
+
if (pending < threshold)
|
|
1218
1247
|
return;
|
|
1219
1248
|
// Mark BEFORE running so we don't double-fire on overlapping Stop events.
|
|
1220
1249
|
state.threads[thread] = { lastDistilledAt: Date.now(), lastTurnAt: Date.now(), lastTurnCount: recallSize };
|
|
1221
1250
|
saveDistillState(state);
|
|
1222
|
-
// Detach: spawn a background process so the
|
|
1223
|
-
// The detached child runs `stitch distill` for this thread.
|
|
1251
|
+
// Detach: spawn a background process so the hook returns immediately.
|
|
1224
1252
|
try {
|
|
1225
1253
|
const child = spawn(process.argv[0], [process.argv[1] || (await import('node:url')).fileURLToPath(import.meta.url), 'distill', '--thread', thread, '--n', String(DISTILL_BATCH_SIZE)], {
|
|
1226
1254
|
detached: true,
|
|
1227
1255
|
stdio: 'ignore',
|
|
1256
|
+
env: { ...process.env, STITCH_HOOKS_DISABLED: '1' },
|
|
1228
1257
|
});
|
|
1229
1258
|
child.unref();
|
|
1230
1259
|
}
|
|
@@ -1676,6 +1705,46 @@ function mergeHook(existing, entry) {
|
|
|
1676
1705
|
filtered.push(entry);
|
|
1677
1706
|
return filtered;
|
|
1678
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Idempotent re-wiring of every Stitch hook into ~/.claude/settings.json.
|
|
1710
|
+
* Called on every SessionStart so a CLI upgrade auto-installs any new hooks
|
|
1711
|
+
* (e.g. PreToolUse:Read added in 0.7.0) without the user re-running
|
|
1712
|
+
* `stitch install`. Never touches non-Stitch hook entries.
|
|
1713
|
+
*/
|
|
1714
|
+
function ensureHooksUpToDate() {
|
|
1715
|
+
try {
|
|
1716
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1717
|
+
if (!fs.existsSync(path.dirname(settingsPath)))
|
|
1718
|
+
return;
|
|
1719
|
+
let existing = {};
|
|
1720
|
+
try {
|
|
1721
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1722
|
+
}
|
|
1723
|
+
catch { /* corrupt — skip */
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
existing.hooks = existing.hooks || {};
|
|
1727
|
+
const expected = [
|
|
1728
|
+
['SessionStart', STITCH_SESSION_START_HOOK],
|
|
1729
|
+
['UserPromptSubmit', STITCH_USER_HOOK],
|
|
1730
|
+
['Stop', STITCH_STOP_HOOK],
|
|
1731
|
+
['PreToolUse', STITCH_PRE_READ_HOOK],
|
|
1732
|
+
['PostToolUse', STITCH_POST_READ_HOOK],
|
|
1733
|
+
];
|
|
1734
|
+
let changed = false;
|
|
1735
|
+
for (const [key, entry] of expected) {
|
|
1736
|
+
const next = mergeHook(existing.hooks[key], entry);
|
|
1737
|
+
if (JSON.stringify(next) !== JSON.stringify(existing.hooks[key])) {
|
|
1738
|
+
existing.hooks[key] = next;
|
|
1739
|
+
changed = true;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (changed) {
|
|
1743
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
catch { /* never break a session start */ }
|
|
1747
|
+
}
|
|
1679
1748
|
const STITCH_CLAUDE_MD_BLOCK = `<!-- stitch:auto -->
|
|
1680
1749
|
## Stitch memory
|
|
1681
1750
|
|