@stitchdb/cli 0.10.1 → 0.11.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 +87 -9
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -761,19 +761,97 @@ async function cmdHook(args) {
761
761
  content = content.trim();
762
762
  if (!content)
763
763
  return;
764
+ // For UserPromptSubmit: log the turn AND fetch task-relevant memory in
765
+ // parallel, so the model sees only what's actually relevant to *this*
766
+ // prompt — not 8 random preloaded memories from session start. Task
767
+ // changes self-surface because the recall hits change with the prompt.
768
+ if (eventName === 'UserPromptSubmit') {
769
+ await Promise.all([
770
+ (async () => {
771
+ try {
772
+ const stitch = client(cfg);
773
+ await stitch.thread(threadName).append({ role: role, content });
774
+ }
775
+ catch { /* silent */ }
776
+ })(),
777
+ handleUserPromptInjection(cfg, content),
778
+ ]);
779
+ return;
780
+ }
781
+ // Stop: log the assistant turn, then maybe-distill.
764
782
  try {
765
783
  const stitch = client(cfg);
766
- await stitch.thread(threadName).append({ role, content });
784
+ await stitch.thread(threadName).append({ role: role, content });
767
785
  }
768
- catch {
769
- /* silent */
770
- }
771
- // After Stop, opportunistically kick off a distillation pass in the
772
- // background (fire-and-forget). Won't block the session; debouncing
773
- // (cooldown + min-new-turns) is enforced inside maybeAutoDistill.
774
- if (eventName === 'Stop') {
775
- maybeAutoDistill(threadName).catch(() => { });
786
+ catch { /* silent */ }
787
+ maybeAutoDistill(threadName).catch(() => { });
788
+ }
789
+ /**
790
+ * Per-prompt smart context injection. Runs `recall(prompt)` against the
791
+ * project workspace + the user's `_global` workspace, score-gates the hits,
792
+ * and writes a Claude-Code-flavoured `additionalContext` JSON object to
793
+ * stdout. Time-bounded so a slow proxy doesn't delay the user's prompt.
794
+ *
795
+ * Why per-prompt: a static SessionStart block must guess what'll matter
796
+ * across the whole session. A per-prompt recall sees the actual question
797
+ * and pulls memory tailored to *that* — much higher signal per token.
798
+ * A task switch ("now let's do the dashboard") naturally surfaces the
799
+ * dashboard memories; an unrelated follow-up ("ok run the tests") returns
800
+ * low scores and we inject nothing — no noise.
801
+ */
802
+ const PROMPT_INJECTION_TIMEOUT_MS = 2000;
803
+ const PROMPT_INJECTION_MIN_SCORE = 0.4;
804
+ async function handleUserPromptInjection(cfg, prompt) {
805
+ if (!prompt || prompt.length < 10)
806
+ return;
807
+ try {
808
+ const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
809
+ const stitch = client(cfg);
810
+ const tOut = new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), PROMPT_INJECTION_TIMEOUT_MS));
811
+ // Project recall + global recall in parallel; tolerate either failing.
812
+ const projectHitsP = stitch.recall(prompt, { k: 3 }).catch(() => []);
813
+ const globalHitsP = (async () => {
814
+ try {
815
+ const ws = await stitch.workspaces.list();
816
+ const g = ws.find((w) => w.name === '_global');
817
+ if (!g)
818
+ return [];
819
+ const gc = new Stitch({ apiKey: cfg.apiKey, baseUrl, workspace: g.id });
820
+ return gc.recall(prompt, { k: 2 }).catch(() => []);
821
+ }
822
+ catch {
823
+ return [];
824
+ }
825
+ })();
826
+ const [projectHits, globalHits] = await Promise.race([
827
+ Promise.all([projectHitsP, globalHitsP]),
828
+ tOut,
829
+ ]);
830
+ const project = projectHits.filter((h) => h.score >= PROMPT_INJECTION_MIN_SCORE).slice(0, 2);
831
+ const global = globalHits.filter((h) => h.score >= PROMPT_INJECTION_MIN_SCORE).slice(0, 1);
832
+ if (project.length === 0 && global.length === 0)
833
+ return;
834
+ const lines = ['<stitch-recall>'];
835
+ if (global.length > 0) {
836
+ lines.push('User-level rules relevant here:');
837
+ for (const h of global) {
838
+ const txt = String(h.content || '').replace(/\n+/g, ' ').slice(0, 250);
839
+ lines.push(`- [${h.kind}] ${txt}`);
840
+ }
841
+ }
842
+ if (project.length > 0) {
843
+ lines.push('Project memory relevant to this prompt:');
844
+ for (const h of project) {
845
+ const txt = String(h.content || '').replace(/\n+/g, ' ').slice(0, 300);
846
+ const src = h.source_thread_id ? ' _(thread receipt available — call thread_recall to dig)_' : '';
847
+ lines.push(`- [${h.kind}] (score ${Number(h.score).toFixed(2)}) ${txt}${src}`);
848
+ }
849
+ }
850
+ lines.push('</stitch-recall>');
851
+ const payload = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: lines.join('\n') } };
852
+ process.stdout.write(JSON.stringify(payload));
776
853
  }
854
+ catch { /* silent — never break a prompt */ }
777
855
  }
778
856
  /**
779
857
  * Derive a thread name for the project at `cwd`. Strategy:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {