dsh-thoughtdag 0.4.20 → 0.5.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 (26) hide show
  1. package/dist-app/assets/{canonical-mCpjqoou.js → canonical-BlzQbV3x.js} +2 -2
  2. package/dist-app/assets/canvas-record-CHx9KBrZ.js +1 -0
  3. package/dist-app/assets/{claude-code-session-DJkcGfvB.js → claude-code-session-BfwTY3ne.js} +4 -4
  4. package/dist-app/assets/{codex-session-Crw7-bHo.js → codex-session-BNnk6Ozy.js} +4 -4
  5. package/dist-app/assets/{dsh-session-DbEtbF5u.js → dsh-session-BXjdZH-4.js} +4 -4
  6. package/dist-app/assets/{experiment-loop-CkNvS6b1.js → experiment-loop-CBfcRt8t.js} +1 -1
  7. package/dist-app/assets/{http-bridge-CJvoQLgy.js → http-bridge-CK02uyVL.js} +1 -1
  8. package/dist-app/assets/index-93ujsGjx.css +1 -0
  9. package/dist-app/assets/index-F8ykUu-e.js +883 -0
  10. package/dist-app/assets/{index-CT3R4H-W.js → index-U1FDd3d6.js} +1 -1
  11. package/dist-app/assets/{index-ZITmlOUw.js → index-Vln2-D4p.js} +3 -3
  12. package/dist-app/assets/{index-B6lBI8ja.js → index-no3SFEm4.js} +1 -1
  13. package/dist-app/assets/{pi-session-CBjuXvCc.js → pi-session-DfX1QntM.js} +8 -8
  14. package/dist-app/assets/{sensitive-scan-CjmPZxkv.js → sensitive-scan-NeBfDua2.js} +1 -1
  15. package/dist-app/assets/{session-handoff-DGfX02CE.js → session-handoff-Bi6J6owx.js} +2 -2
  16. package/dist-app/assets/{shared-z6Es4wHy.js → shared-B_zE0vyL.js} +4 -4
  17. package/dist-app/assets/{turndown-plugin-gfm.cjs-KFp3duAG.js → turndown-plugin-gfm.cjs-t7mTv0d4.js} +3 -3
  18. package/dist-app/assets/{update-check-D5kNTq-_.js → update-check-CmPLQTC0.js} +1 -1
  19. package/dist-app/index.html +2 -2
  20. package/lib/index.js +50 -1
  21. package/lib/why.mjs +567 -16
  22. package/package.json +1 -1
  23. package/dist-app/assets/canvas-record-di62TikJ.js +0 -1
  24. package/dist-app/assets/index-B-DuMDps.css +0 -1
  25. package/dist-app/assets/index-BMFFskkL.js +0 -789
  26. package/dist-app/assets/live-mirror-D-eP76Rm.js +0 -2
package/lib/why.mjs CHANGED
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline";
7
7
  import path from "node:path";
8
8
  import os from "node:os";
9
9
  import * as zlib from "node:zlib";
10
+ import { createHash } from "node:crypto";
10
11
 
11
12
  // ../src/utils.ts
12
13
  var idCounter = 0;
@@ -1086,7 +1087,7 @@ ${text}` : text;
1086
1087
  this.pendingTools.delete(callId);
1087
1088
  return;
1088
1089
  }
1089
- if (line.type === "tool/code-dispatch") {
1090
+ if (line.type === "tool/code-dispatch" || line.type === "tool/ptc-dispatch") {
1090
1091
  const d = line.data;
1091
1092
  if (!d?.name || !d.subCallId) return;
1092
1093
  const paths = toolPaths(d.arguments);
@@ -2008,7 +2009,7 @@ ${a.extractedText.slice(0, 4e3)}`).join("\n\n");
2008
2009
 
2009
2010
  // ../cli/src/lib.ts
2010
2011
  var readLike = (op) => op === "read" || op === "fetch" || op === "attach";
2011
- var INDEX_VERSION = 10;
2012
+ var INDEX_VERSION = 12;
2012
2013
  var EXCERPT = 200;
2013
2014
  var HOME = process.env.THOUGHTDAG_HOME ?? path.join(os.homedir(), ".thoughtdag");
2014
2015
  var FACT_FILE = path.join(HOME, "fact-index.json");
@@ -2020,6 +2021,25 @@ var CONFIG_FILE = path.join(HOME, "config.json");
2020
2021
  var DSH_ROOT = path.join(process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh"), "sessions");
2021
2022
  var PI_ROOT = path.join(os.homedir(), ".pi", "agent", "sessions");
2022
2023
  var ROOTS = process.env.THOUGHTDAG_SESSION_ROOTS?.split(path.delimiter).filter(Boolean) ?? [path.join(os.homedir(), ".claude", "projects"), path.join(os.homedir(), ".codex", "sessions"), DSH_ROOT, PI_ROOT];
2024
+ var MEMORY_ROOTS = (() => {
2025
+ const env = process.env.THOUGHTDAG_MEMORY_ROOTS;
2026
+ if (env !== void 0) {
2027
+ return env.split(path.delimiter).filter(Boolean).flatMap((e) => {
2028
+ const m = /^(claude-code|codex)=(.+)$/.exec(e);
2029
+ return m ? [{ runner: m[1], root: m[2] }] : [];
2030
+ });
2031
+ }
2032
+ if (process.env.THOUGHTDAG_SESSION_ROOTS) return [];
2033
+ return [
2034
+ { runner: "claude-code", root: path.join(os.homedir(), ".claude", "projects") },
2035
+ { runner: "codex", root: path.join(os.homedir(), ".codex", "memories") }
2036
+ ];
2037
+ })();
2038
+ var isMemoryFile = (file, runner) => {
2039
+ if (!file.endsWith(".md")) return false;
2040
+ if (runner === "claude-code") return path.basename(path.dirname(file)) === "memory" && path.basename(file) !== "MEMORY.md";
2041
+ return true;
2042
+ };
2023
2043
  var CANVAS_RECORDS = path.join(HOME, "canvases");
2024
2044
  async function canvasRoots() {
2025
2045
  const env = process.env.THOUGHTDAG_CANVAS_ROOTS?.split(path.delimiter).filter(Boolean);
@@ -2062,6 +2082,11 @@ async function listSources() {
2062
2082
  const files = [];
2063
2083
  for (const root of ROOTS) await walk(root, 0, files, isSessionFile);
2064
2084
  for (const root of await canvasRoots()) await walk(root, 0, files, isCanvasFile);
2085
+ for (const { runner, root } of MEMORY_ROOTS) {
2086
+ const md = [];
2087
+ await walk(root, 0, md, (name) => name.endsWith(".md"));
2088
+ for (const m of md) if (isMemoryFile(m.file, runner)) files.push({ ...m, memory: runner });
2089
+ }
2065
2090
  return files;
2066
2091
  }
2067
2092
  async function firstLine(file) {
@@ -2220,6 +2245,7 @@ async function eventsOf(file, sourceId) {
2220
2245
  return { runner, nativeId: s.sessionId, title: s.title, ...anchor ? { anchor } : {}, ...cwd ? { cwd } : {}, ...subagent ? { subagent } : {}, events, texts, manifest: MANIFESTS[runner], turns: s.turns };
2221
2246
  }
2222
2247
  async function parseSession(f) {
2248
+ if (f.memory) return parseMemory(f, f.memory);
2223
2249
  const memo = /* @__PURE__ */ new Map();
2224
2250
  const sourceId = await canonicalPath(f.file, memo);
2225
2251
  const p = await eventsOf(f.file, sourceId);
@@ -2274,6 +2300,136 @@ async function parseSession(f) {
2274
2300
  bundles
2275
2301
  };
2276
2302
  }
2303
+ function frontmatterOf(text) {
2304
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
2305
+ if (!m) return { meta: {}, body: text };
2306
+ const meta = {};
2307
+ for (const line of m[1].split(/\r?\n/)) {
2308
+ const kv = /^\s*([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
2309
+ if (kv && kv[2].trim()) meta[kv[1]] = kv[2].trim();
2310
+ }
2311
+ return { meta, body: text.slice(m[0].length) };
2312
+ }
2313
+ function sectionsOf(text, level) {
2314
+ const out = [];
2315
+ const lines = text.split(/\r?\n/);
2316
+ let cur = null;
2317
+ const isBoundary = (line) => {
2318
+ const m = /^(#{1,6})\s+(.*)$/.exec(line);
2319
+ return m && m[1].length <= level ? m[1].length : null;
2320
+ };
2321
+ for (const line of lines) {
2322
+ const lvl = isBoundary(line);
2323
+ if (lvl === level) {
2324
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2325
+ cur = { heading: line.replace(/^#+\s+/, "").trim(), body: [] };
2326
+ continue;
2327
+ }
2328
+ if (lvl !== null && lvl < level) {
2329
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2330
+ cur = null;
2331
+ continue;
2332
+ }
2333
+ if (cur) cur.body.push(line);
2334
+ }
2335
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2336
+ return out;
2337
+ }
2338
+ var metaLine = (body, key) => {
2339
+ const m = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(body);
2340
+ return m ? m[1].trim() : void 0;
2341
+ };
2342
+ var claudeCwdMemo = /* @__PURE__ */ new Map();
2343
+ function claudeProjectCwd(projectDir) {
2344
+ let p = claudeCwdMemo.get(projectDir);
2345
+ if (!p) {
2346
+ p = (async () => {
2347
+ let entries = [];
2348
+ try {
2349
+ entries = (await fsp.readdir(projectDir)).filter((n) => n.endsWith(".jsonl")).sort();
2350
+ } catch {
2351
+ return "";
2352
+ }
2353
+ for (const n of entries) {
2354
+ try {
2355
+ const o = JSON.parse(await firstLine(path.join(projectDir, n)));
2356
+ if (typeof o.cwd === "string" && o.cwd) return o.cwd;
2357
+ } catch {
2358
+ }
2359
+ }
2360
+ return "";
2361
+ })();
2362
+ claudeCwdMemo.set(projectDir, p);
2363
+ }
2364
+ return p;
2365
+ }
2366
+ async function memoryFile(file, runner) {
2367
+ let text;
2368
+ try {
2369
+ text = await fsp.readFile(file, "utf8");
2370
+ } catch {
2371
+ return null;
2372
+ }
2373
+ const base = path.basename(file);
2374
+ if (runner === "claude-code") {
2375
+ const projectDir = path.dirname(path.dirname(file));
2376
+ const cwd = await claudeProjectCwd(projectDir);
2377
+ const { meta, body } = frontmatterOf(text);
2378
+ const heading = meta.description || meta.name || base.replace(/\.md$/, "");
2379
+ const project = path.basename(cwd || projectDir);
2380
+ return { title: `memory \xB7 ${project} \xB7 ${base.replace(/\.md$/, "")}`, cwd, units: body.trim() ? [{ heading, body: body.trim() }] : [] };
2381
+ }
2382
+ if (base === "MEMORY.md") {
2383
+ const units = [];
2384
+ for (const g of sectionsOf(text, 1)) {
2385
+ const cwd = /cwd=([^;\n]+)/.exec(metaLine(g.body, "applies_to") ?? "")?.[1]?.trim();
2386
+ const subs2 = sectionsOf(g.body, 2);
2387
+ if (!subs2.length) {
2388
+ units.push({ heading: g.heading, body: g.body, ...cwd ? { cwd } : {} });
2389
+ continue;
2390
+ }
2391
+ for (const u of subs2) units.push({ heading: `${g.heading} \u203A ${u.heading}`, body: u.body, ...cwd ? { cwd } : {} });
2392
+ }
2393
+ return { title: "memory \xB7 Codex \xB7 MEMORY.md", cwd: "", units };
2394
+ }
2395
+ if (base === "raw_memories.md") {
2396
+ const units = sectionsOf(text, 2).map((u) => {
2397
+ const cwd = metaLine(u.body, "cwd");
2398
+ const at = metaLine(u.body, "updated_at");
2399
+ const heading = cwd ? `${u.heading} \xB7 ${path.basename(cwd)}` : u.heading;
2400
+ return { heading, body: u.body, ...cwd ? { cwd } : {}, ...at ? { at: new Date(at).toISOString() } : {} };
2401
+ });
2402
+ return { title: "memory \xB7 Codex \xB7 raw_memories.md", cwd: "", units };
2403
+ }
2404
+ const subs = sectionsOf(text, 2);
2405
+ if (subs.length) return { title: `memory \xB7 Codex \xB7 ${base}`, cwd: "", units: subs };
2406
+ const h1 = sectionsOf(text, 1)[0];
2407
+ return { title: `memory \xB7 Codex \xB7 ${base}`, cwd: "", units: text.trim() ? [{ heading: h1?.heading ?? base, body: text.trim() }] : [] };
2408
+ }
2409
+ var shortHash = (s) => createHash("sha1").update(s).digest("hex").slice(0, 12);
2410
+ async function parseMemory(f, runner) {
2411
+ const mf = await memoryFile(f.file, runner);
2412
+ if (!mf || !mf.units.length) return null;
2413
+ const memo = /* @__PURE__ */ new Map();
2414
+ const cwd = mf.cwd ? await canonicalPath(mf.cwd, memo) : "";
2415
+ const workspace = cwd ? await workspaceOf(cwd) : "";
2416
+ const at = new Date(f.mtime).toISOString();
2417
+ const turns = [];
2418
+ const text = {};
2419
+ for (const [i, u] of mf.units.entries()) {
2420
+ const unitCwd = u.cwd ? await canonicalPath(u.cwd, memo) : "";
2421
+ turns.push({ i, t: `memory#${i}`, at: u.at ?? at, q: questionExcerpt(u.heading), ops: {}, ...unitCwd && unitCwd !== cwd ? { cwd: unitCwd } : {} });
2422
+ text[String(i)] = { q: u.heading, a: u.body };
2423
+ }
2424
+ const id = `mem-${runner === "codex" ? "cx" : "cc"}-${shortHash(await canonicalPath(f.file, memo))}`;
2425
+ return {
2426
+ fact: { id, runner, kind: "memory", file: f.file, mtime: f.mtime, size: f.size, cwd, workspace, title: mf.title, turns },
2427
+ cache: {},
2428
+ text,
2429
+ names: {},
2430
+ bundles: {}
2431
+ };
2432
+ }
2277
2433
  var emptyFacts = () => ({ version: INDEX_VERSION, builtAt: "", sessions: {}, skipped: {}, names: {}, bundles: {} });
2278
2434
  var emptyCache = () => ({ version: INDEX_VERSION, sessions: {} });
2279
2435
  var emptyText = () => ({ version: INDEX_VERSION, sessions: {} });
@@ -2501,7 +2657,8 @@ function summarize(facts, cache) {
2501
2657
  }
2502
2658
  }
2503
2659
  const sessions = new Set(Object.values(facts.sessions).map((s) => s.id)).size;
2504
- return { sessions, sources: Object.keys(facts.sessions).length, turns, artifacts, touches, changes, withChangeHead, withMention };
2660
+ const mem = Object.values(facts.sessions).filter((s) => s.kind === "memory");
2661
+ return { sessions, sources: Object.keys(facts.sessions).length, turns, artifacts, touches, changes, withChangeHead, withMention, memoryFiles: mem.length, memoryEntries: mem.reduce((n, s) => n + s.turns.length, 0) };
2505
2662
  }
2506
2663
  var allPaths = (facts) => {
2507
2664
  const all = /* @__PURE__ */ new Set();
@@ -2560,6 +2717,7 @@ function anchorStatus(facts, a) {
2560
2717
  return b.node === a.node ? "matched" : "mismatch";
2561
2718
  }
2562
2719
  function openLink(h) {
2720
+ if (h.session.kind === "memory") return `thoughtdag://open?memory=${encodeURIComponent(h.session.file)}&entry=${h.turn.i}`;
2563
2721
  if (h.session.runner === "thoughtdag") return `thoughtdag://open?canvas=${encodeURIComponent(h.session.id)}&node=${encodeURIComponent(h.turn.t.split("#").pop() ?? "")}`;
2564
2722
  return `thoughtdag://open?session=${h.session.id}${h.turn.item ? `&turn=${encodeURIComponent(h.turn.item)}` : ""}`;
2565
2723
  }
@@ -2701,7 +2859,7 @@ async function findHits(facts, phrase, scope) {
2701
2859
  if (!body) continue;
2702
2860
  if (scope !== "all" && where.toLowerCase() !== scope) continue;
2703
2861
  if (!body.toLowerCase().includes(needle)) continue;
2704
- raw.push({ session, sourceKey: t.k, turn, where, snippet: snippetAround(body, needle) });
2862
+ raw.push({ session, sourceKey: t.k, turn, where, snippet: snippetAround(body, needle), kind: session.kind === "memory" ? "memory" : "turn" });
2705
2863
  break;
2706
2864
  }
2707
2865
  }
@@ -2726,7 +2884,7 @@ function renderFind(facts, phrase, hits, limit, json) {
2726
2884
  turns: hits.length,
2727
2885
  sessions,
2728
2886
  evidence: { Q: "observed (a question, verbatim)", A: "observed (an answer, verbatim)", M: "observed (an attached material's text, verbatim)" },
2729
- hits: shown.map((h) => ({ session: h.session.id, runner: h.session.runner, title: h.session.title, turn: turnNumber(facts, h), at: h.turn.at ?? null, where: h.where, snippet: h.snippet, open: openLink(h) }))
2887
+ hits: shown.map((h) => ({ kind: h.kind, session: h.session.id, runner: h.session.runner, title: h.session.title, cwd: h.turn.cwd ?? h.session.cwd, file: h.session.file, turn: turnNumber(facts, h), at: h.turn.at ?? null, where: h.where, snippet: h.snippet, open: openLink(h) }))
2730
2888
  }, null, 1));
2731
2889
  return out.join("\n");
2732
2890
  }
@@ -2734,7 +2892,7 @@ function renderFind(facts, phrase, hits, limit, json) {
2734
2892
  `);
2735
2893
  for (const h of shown) {
2736
2894
  const s = h.session;
2737
- out.push(`${when(h.turn, s)} ${s.runner} \u300C${s.title.slice(0, 60)}\u300D #${turnNumber(facts, h)} ${openLink(h)}`);
2895
+ out.push(`${when(h.turn, s)} ${s.runner}${h.kind === "memory" ? " memory" : ""} \u300C${s.title.slice(0, 60)}\u300D #${turnNumber(facts, h)} ${openLink(h)}`);
2738
2896
  out.push(` ${h.where}: ${h.snippet}`);
2739
2897
  }
2740
2898
  if (!shown.length) out.push("(nothing asked, answered or attached in those words \u2014 try another wording; matching is exact)");
@@ -2758,13 +2916,19 @@ function renderCheck(facts, arg, file, hits, json) {
2758
2916
  const parts = [`${hits.length} turn${hits.length === 1 ? "" : "s"} in ${sessions} session${sessions === 1 ? "" : "s"}`, changes ? `${changes} edit${changes === 1 ? "" : "s"}/write${changes === 1 ? "" : "s"}` : "", reads ? `${reads} read${reads === 1 ? "" : "s"}` : "", latest ? `latest ${latest.slice(0, 10)}` : ""].filter(Boolean);
2759
2917
  return { text: `${arg}: ${parts.join(" \xB7 ")} \u2192 thoughtdag why ${arg}`, has };
2760
2918
  }
2761
- async function renderRecall(facts, sidPrefix, n) {
2762
- const out = [];
2919
+ async function recallTurn(facts, sidPrefix, n) {
2763
2920
  const any = Object.values(facts.sessions).find((x) => x.id.startsWith(sidPrefix));
2764
2921
  if (!any) throw new Error(`no session starts with ${sidPrefix}`);
2765
2922
  const frag = fragmentsOf(facts, any.id).find((f) => n >= f.offset && n < f.offset + f.s.turns.length);
2766
2923
  const s = frag?.s ?? any;
2767
2924
  const local = frag ? n - frag.offset : n;
2925
+ const base = { runner: s.runner, session: s.id, title: s.title, turn: n, file: s.file };
2926
+ if (s.kind === "memory") {
2927
+ const mf = await memoryFile(s.file, s.runner);
2928
+ const u = mf?.units[local];
2929
+ if (!u) throw new Error(`memory ${s.id.slice(0, 8)} has no entry #${n}`);
2930
+ return { ...base, kind: "memory", at: s.turns[local]?.at, cwd: s.turns[local]?.cwd ?? s.cwd, question: u.heading, response: u.body, tools: [] };
2931
+ }
2768
2932
  const p = await eventsOf(s.file);
2769
2933
  const turn = p?.turns?.[local] ?? (() => {
2770
2934
  const tid = s.turns[local]?.t;
@@ -2772,31 +2936,401 @@ async function renderRecall(facts, sidPrefix, n) {
2772
2936
  return text ? { question: text.question, response: text.response, tools: [], at: s.turns[local]?.at } : void 0;
2773
2937
  })();
2774
2938
  if (!turn) throw new Error(`session ${s.id.slice(0, 8)} has no turn #${n}`);
2775
- out.push(`${s.runner} \u300C${s.title}\u300D turn #${n}${turn.at ? ` ${turn.at.slice(0, 16).replace("T", " ")}` : ""}
2939
+ return { ...base, kind: "turn", at: turn.at, cwd: s.cwd, question: turn.question, response: turn.response, tools: turn.tools };
2940
+ }
2941
+ async function renderRecall(facts, sidPrefix, n) {
2942
+ const r = await recallTurn(facts, sidPrefix, n);
2943
+ const out = [];
2944
+ out.push(`${r.runner}${r.kind === "memory" ? " memory" : ""} \u300C${r.title}\u300D ${r.kind === "memory" ? "entry" : "turn"} #${n}${r.at ? ` ${r.at.slice(0, 16).replace("T", " ")}` : ""}
2776
2945
  `);
2946
+ if (r.kind === "memory") {
2947
+ out.push(`## ${r.question.trim()}
2948
+
2949
+ ${r.response.trim() || "(empty)"}
2950
+ `);
2951
+ return out.join("\n");
2952
+ }
2777
2953
  out.push(`## Question
2778
2954
 
2779
- ${turn.question.trim()}
2955
+ ${r.question.trim()}
2780
2956
  `);
2781
2957
  out.push(`## Answer
2782
2958
 
2783
- ${turn.response.trim() || "(none)"}
2959
+ ${r.response.trim() || "(none)"}
2784
2960
  `);
2785
- if (turn.tools.length) {
2786
- out.push(`## Tools (${turn.tools.length})
2961
+ if (r.tools.length) {
2962
+ out.push(`## Tools (${r.tools.length})
2787
2963
  `);
2788
- for (const t of turn.tools) {
2964
+ for (const t of r.tools) {
2789
2965
  out.push(`- ${t.name}${t.paths?.length ? ` ${t.paths.join(", ")}` : ""}`);
2790
2966
  if (t.op === "edit" || t.op === "write") out.push(t.call.slice(0, 1200).split("\n").map((l) => ` ${l}`).join("\n"));
2791
2967
  }
2792
2968
  }
2793
2969
  return out.join("\n");
2794
2970
  }
2795
- var CLI_VERSION = "0.1.0";
2971
+ async function findJson(phrase, opts = {}) {
2972
+ const facts = await ensureFresh();
2973
+ let hits = await findHits(facts, phrase, opts.scope ?? "all");
2974
+ if (opts.cwd) {
2975
+ const want = opts.cwd.replace(/[\\/]+$/, "");
2976
+ hits = hits.filter((h) => {
2977
+ const c = h.turn.cwd ?? h.session.cwd;
2978
+ return c === want || c.startsWith(want + path.sep);
2979
+ });
2980
+ }
2981
+ const limit = opts.limit ?? 20;
2982
+ const shown = [...hits.filter((h) => h.kind === "memory").slice(0, limit), ...hits.filter((h) => h.kind === "turn").slice(0, limit)];
2983
+ return {
2984
+ phrase,
2985
+ turns: hits.length,
2986
+ sessions: new Set(hits.map((h) => h.session.id)).size,
2987
+ hits: shown.map((h) => ({ kind: h.kind, session: h.session.id, runner: h.session.runner, title: h.session.title, cwd: h.turn.cwd ?? h.session.cwd, file: h.session.file, turn: turnNumber(facts, h), at: h.turn.at ?? null, where: h.where, snippet: h.snippet, open: openLink(h) }))
2988
+ };
2989
+ }
2990
+ async function recallJson(session, n) {
2991
+ return recallTurn(await ensureFresh(), session, n);
2992
+ }
2993
+ var vocabCache = null;
2994
+ async function vocabulary() {
2995
+ let stamp = "";
2996
+ try {
2997
+ const st = await fsp.stat(TEXT_LINES);
2998
+ stamp = `${st.mtimeMs}:${st.size}`;
2999
+ } catch {
3000
+ return /* @__PURE__ */ new Map();
3001
+ }
3002
+ if (vocabCache && vocabCache.builtFrom === stamp) return vocabCache.counts;
3003
+ const counts = /* @__PURE__ */ new Map();
3004
+ const lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3005
+ for await (const line of lines) {
3006
+ let t;
3007
+ try {
3008
+ t = JSON.parse(line);
3009
+ } catch {
3010
+ continue;
3011
+ }
3012
+ for (const body of [t.q, t.a, t.m]) {
3013
+ if (!body) continue;
3014
+ for (const m of body.matchAll(/[A-Za-z][A-Za-z0-9_-]{3,}/g)) {
3015
+ const w = m[0].toLowerCase();
3016
+ counts.set(w, (counts.get(w) ?? 0) + 1);
3017
+ }
3018
+ }
3019
+ }
3020
+ vocabCache = { builtFrom: stamp, counts };
3021
+ return counts;
3022
+ }
3023
+ function editDistance(a, b, max) {
3024
+ if (Math.abs(a.length - b.length) > max) return max + 1;
3025
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3026
+ for (let i = 1; i <= a.length; i++) {
3027
+ const cur = [i];
3028
+ let rowMin = i;
3029
+ for (let j = 1; j <= b.length; j++) {
3030
+ const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
3031
+ cur.push(v);
3032
+ if (v < rowMin) rowMin = v;
3033
+ }
3034
+ if (rowMin > max) return max + 1;
3035
+ prev = cur;
3036
+ }
3037
+ return prev[b.length];
3038
+ }
3039
+ async function suggestJson(term, k = 8) {
3040
+ const t = term.toLowerCase().trim();
3041
+ const counts = await vocabulary();
3042
+ const known = counts.get(t) ?? 0;
3043
+ if (!t || t.length < 3) return { term, known, suggestions: [] };
3044
+ const max = t.length >= 6 ? 2 : 1;
3045
+ const out = [];
3046
+ for (const [w, count] of counts) {
3047
+ if (w === t) continue;
3048
+ const d = editDistance(t, w, max);
3049
+ if (d <= max) out.push({ term: w, count, distance: d });
3050
+ }
3051
+ out.sort((a, b) => a.distance - b.distance || b.count - a.count);
3052
+ return { term, known, suggestions: out.slice(0, k) };
3053
+ }
3054
+ var TOPICS_FILE = path.join(HOME, "topics.json");
3055
+ var TOPICS_VERSION = 1;
3056
+ var emptyTopics = () => ({ version: TOPICS_VERSION, topics: [], labels: {}, labeledAt: {}, updatedAt: "" });
3057
+ var loadTopics = async () => {
3058
+ try {
3059
+ const v = JSON.parse(await fsp.readFile(TOPICS_FILE, "utf8"));
3060
+ return v.version === TOPICS_VERSION ? { ...emptyTopics(), ...v } : emptyTopics();
3061
+ } catch {
3062
+ return emptyTopics();
3063
+ }
3064
+ };
3065
+ var turnKey = (k, i) => `${k}#${i}`;
3066
+ var cleanForJudge = (s) => s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
3067
+ async function systemOne(call, state, questions) {
3068
+ const body = call.wrap === "cloudflare" ? { model: call.model ?? "typesafe/jev", input: { state, questions } } : { ...call.model ? { model: call.model } : {}, state, questions };
3069
+ const r = await fetch(call.url, { method: "POST", headers: { "Content-Type": "application/json", ...call.headers }, body: JSON.stringify(body), signal: AbortSignal.timeout(9e4) });
3070
+ const text = await r.text();
3071
+ let j = {};
3072
+ try {
3073
+ j = JSON.parse(text);
3074
+ } catch {
3075
+ throw new Error(`judge answered ${r.status} with non-JSON`);
3076
+ }
3077
+ if (!r.ok) throw new Error(typeof j.error === "string" ? j.error : j.error?.message ?? `HTTP ${r.status}`);
3078
+ const answers = (call.wrap === "cloudflare" ? j.result?.answers : j.answers) ?? {};
3079
+ return answers;
3080
+ }
3081
+ var labelStatus = { running: false, done: 0, total: 0, labeled: 0, errors: 0, startedAt: null, finishedAt: null, lastError: null, stopRequested: false };
3082
+ async function topicsJson() {
3083
+ const tf = await loadTopics();
3084
+ const text = await loadText();
3085
+ const turns = Object.values(text.sessions).reduce((n, c) => n + c, 0);
3086
+ const counts = /* @__PURE__ */ new Map();
3087
+ for (const per of Object.values(tf.labels)) for (const [id, p] of Object.entries(per)) if (p >= 0.6) counts.set(id, (counts.get(id) ?? 0) + 1);
3088
+ return { topics: tf.topics.map((t) => ({ ...t, count: counts.get(t.id) ?? 0 })), labeled: Object.keys(tf.labels).length, turns, status: { ...labelStatus } };
3089
+ }
3090
+ async function setTopics(topics) {
3091
+ const tf = await loadTopics();
3092
+ const clean = topics.filter((t) => t.name?.trim()).map((t) => ({ id: t.id && /^[a-z0-9_-]{1,40}$/.test(t.id) ? t.id : shortHash(t.name.trim().toLowerCase()), name: t.name.trim().slice(0, 60), description: (t.description ?? "").trim().slice(0, 240) }));
3093
+ const keep = new Set(clean.map((t) => t.id));
3094
+ for (const [k, per] of Object.entries(tf.labels)) {
3095
+ for (const id of Object.keys(per)) if (!keep.has(id)) delete per[id];
3096
+ if (!Object.keys(per).length) {
3097
+ delete tf.labels[k];
3098
+ delete tf.labeledAt[k];
3099
+ }
3100
+ }
3101
+ tf.topics = clean;
3102
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3103
+ await writePrivate(TOPICS_FILE, tf);
3104
+ return clean;
3105
+ }
3106
+ async function labelStart(call, opts = {}) {
3107
+ if (labelStatus.running) return { ...labelStatus };
3108
+ const tf = await loadTopics();
3109
+ if (!tf.topics.length) throw new Error("no topics to label with");
3110
+ const batch = Math.max(1, Math.min(16, opts.batch ?? 8));
3111
+ Object.assign(labelStatus, { running: true, done: 0, total: 0, labeled: 0, errors: 0, startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, lastError: null, stopRequested: false });
3112
+ void (async () => {
3113
+ try {
3114
+ const needs = (t) => {
3115
+ const have = tf.labels[turnKey(t.k, t.i)];
3116
+ if (have && tf.topics.every((tp) => tp.id in have)) return false;
3117
+ return (t.q?.trim().length ?? 0) + (t.a?.trim().length ?? 0) >= 40;
3118
+ };
3119
+ const open = async () => {
3120
+ try {
3121
+ await fsp.access(TEXT_LINES);
3122
+ return createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3123
+ } catch {
3124
+ return null;
3125
+ }
3126
+ };
3127
+ const keys = [];
3128
+ const first = await open();
3129
+ if (first) for await (const line of first) {
3130
+ let t;
3131
+ try {
3132
+ t = JSON.parse(line);
3133
+ } catch {
3134
+ continue;
3135
+ }
3136
+ if (needs(t)) keys.push(turnKey(t.k, t.i));
3137
+ }
3138
+ const chosen = new Set(opts.max ? keys.slice(-opts.max) : keys);
3139
+ const pending = [];
3140
+ const second = chosen.size ? await open() : null;
3141
+ if (second) for await (const line of second) {
3142
+ let t;
3143
+ try {
3144
+ t = JSON.parse(line);
3145
+ } catch {
3146
+ continue;
3147
+ }
3148
+ if (chosen.has(turnKey(t.k, t.i))) pending.push(t);
3149
+ }
3150
+ pending.reverse();
3151
+ labelStatus.total = pending.length;
3152
+ let sinceWrite = 0;
3153
+ for (let at = 0; at < pending.length && !labelStatus.stopRequested; at += batch) {
3154
+ const group = pending.slice(at, at + batch);
3155
+ const state = {};
3156
+ const questions = {};
3157
+ group.forEach((t, gi) => {
3158
+ state[`t${gi}`] = { q: cleanForJudge((t.q ?? "").slice(0, 300)), a: cleanForJudge((t.a ?? "").slice(0, 700)) };
3159
+ for (const tp of tf.topics) questions[`t${gi}_${tp.id}`] = { type: "noul", instructions: `Is the turn \`t${gi}\` about the topic "${tp.name}"${tp.description ? ` (${tp.description})` : ""}?` };
3160
+ });
3161
+ try {
3162
+ const answers = await systemOne(call, state, questions);
3163
+ group.forEach((t, gi) => {
3164
+ const key = turnKey(t.k, t.i);
3165
+ const per = tf.labels[key] ?? {};
3166
+ for (const tp of tf.topics) {
3167
+ const p = answers[`t${gi}_${tp.id}`]?.noul;
3168
+ if (typeof p === "number") per[tp.id] = Math.round(p * 100) / 100;
3169
+ }
3170
+ tf.labels[key] = per;
3171
+ tf.labeledAt[key] = (/* @__PURE__ */ new Date()).toISOString();
3172
+ labelStatus.labeled++;
3173
+ });
3174
+ } catch (e) {
3175
+ labelStatus.errors++;
3176
+ labelStatus.lastError = e instanceof Error ? e.message : String(e);
3177
+ if (labelStatus.errors >= 5 && labelStatus.labeled === 0) break;
3178
+ await new Promise((r) => setTimeout(r, 1500));
3179
+ }
3180
+ labelStatus.done = Math.min(pending.length, at + group.length);
3181
+ sinceWrite += group.length;
3182
+ if (sinceWrite >= 80) {
3183
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3184
+ await writePrivate(TOPICS_FILE, tf);
3185
+ sinceWrite = 0;
3186
+ }
3187
+ }
3188
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3189
+ await writePrivate(TOPICS_FILE, tf);
3190
+ } catch (e) {
3191
+ labelStatus.lastError = e instanceof Error ? e.message : String(e);
3192
+ } finally {
3193
+ labelStatus.running = false;
3194
+ labelStatus.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3195
+ }
3196
+ })();
3197
+ return { ...labelStatus };
3198
+ }
3199
+ function labelStop() {
3200
+ if (labelStatus.running) labelStatus.stopRequested = true;
3201
+ return { ...labelStatus };
3202
+ }
3203
+ async function byTopicJson(topicIds, opts = {}) {
3204
+ const tf = await loadTopics();
3205
+ const facts = await loadFacts();
3206
+ const minP = opts.minP ?? 0.6;
3207
+ const want = new Set(topicIds);
3208
+ const rows = [];
3209
+ for (const [key, per] of Object.entries(tf.labels)) {
3210
+ let score = 0;
3211
+ const topics = {};
3212
+ for (const [id, p] of Object.entries(per)) if (want.has(id) && p >= minP) {
3213
+ score = Math.max(score, p);
3214
+ topics[id] = p;
3215
+ }
3216
+ if (!score) continue;
3217
+ const hash = key.lastIndexOf("#");
3218
+ rows.push({ key, k: key.slice(0, hash), i: Number(key.slice(hash + 1)), score, topics });
3219
+ }
3220
+ const hits = [];
3221
+ for (const r of rows) {
3222
+ const session = facts.sessions[r.k];
3223
+ const turn = session?.turns.find((t) => t.i === r.i);
3224
+ if (!session || !turn) continue;
3225
+ hits.push({ kind: session.kind === "memory" ? "memory" : "turn", session: session.id, runner: session.runner, title: session.title, cwd: turn.cwd ?? session.cwd, file: session.file, turn: turnNumber(facts, { session, sourceKey: r.k, turn }), at: turn.at ?? null, where: "Q", snippet: turn.q.slice(0, 200), open: openLink({ session, turn }), topics: r.topics, _score: r.score });
3226
+ }
3227
+ hits.sort((a, b) => (b._score ?? 0) - (a._score ?? 0) || (b.at ?? "").localeCompare(a.at ?? ""));
3228
+ const total = hits.length;
3229
+ return { hits: hits.slice(0, opts.limit ?? 60).map((h) => {
3230
+ const { _score, ...rest } = h;
3231
+ void _score;
3232
+ return rest;
3233
+ }), total };
3234
+ }
3235
+ var DOSSIERS_FILE = path.join(HOME, "dossiers.json");
3236
+ var DOSSIERS_VERSION = 1;
3237
+ var emptyDossiers = () => ({ version: DOSSIERS_VERSION, byTopic: {} });
3238
+ var loadDossiers = async () => {
3239
+ try {
3240
+ const v = JSON.parse(await fsp.readFile(DOSSIERS_FILE, "utf8"));
3241
+ return v.version === DOSSIERS_VERSION ? { ...emptyDossiers(), ...v } : emptyDossiers();
3242
+ } catch {
3243
+ return emptyDossiers();
3244
+ }
3245
+ };
3246
+ var emptyDossier = (topicId) => ({ topicId, sections: { what: [], decisions: [], now: [], open: [] }, sources: {}, covered: [], pending: [], changelog: [], builtAt: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
3247
+ async function dossiersJson() {
3248
+ const [tf, df] = await Promise.all([loadTopics(), loadDossiers()]);
3249
+ const out = [];
3250
+ for (const tp of tf.topics) {
3251
+ const d = df.byTopic[tp.id];
3252
+ const labeled = (await byTopicJson([tp.id], { limit: 5e3 })).hits.map((h) => `${h.session}#${h.turn}`);
3253
+ const covered = new Set(d?.covered ?? []);
3254
+ const s = d?.sections;
3255
+ const sentences = s ? s.what.length + s.decisions.length + s.now.length + s.open.length : 0;
3256
+ out.push({ topicId: tp.id, name: tp.name, built: !!d?.builtAt, builtAt: d?.builtAt ?? null, updatedAt: d?.updatedAt ?? "", lead: s?.what[0]?.text ?? s?.now[0]?.text ?? "", sentences, covered: covered.size, pending: d?.pending.length ?? 0, newTurns: labeled.filter((k) => !covered.has(k)).length, labeled: labeled.length });
3257
+ }
3258
+ return out;
3259
+ }
3260
+ async function dossierJson(topicId) {
3261
+ return (await loadDossiers()).byTopic[topicId] ?? null;
3262
+ }
3263
+ async function setDossier(topicId, d) {
3264
+ const df = await loadDossiers();
3265
+ const cur = df.byTopic[topicId] ?? emptyDossier(topicId);
3266
+ const next = { ...cur, ...d, topicId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
3267
+ df.byTopic[topicId] = next;
3268
+ await writePrivate(DOSSIERS_FILE, df);
3269
+ return next;
3270
+ }
3271
+ async function deleteDossier(topicId) {
3272
+ const df = await loadDossiers();
3273
+ delete df.byTopic[topicId];
3274
+ await writePrivate(DOSSIERS_FILE, df);
3275
+ }
3276
+ async function dossierAddPending(topicId, item) {
3277
+ const df = await loadDossiers();
3278
+ const cur = df.byTopic[topicId] ?? emptyDossier(topicId);
3279
+ cur.pending.push({ id: shortHash(`${Date.now()}:${item.text}`), text: item.text.slice(0, 400), at: (/* @__PURE__ */ new Date()).toISOString(), ...item.from ? { from: item.from } : {} });
3280
+ cur.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3281
+ df.byTopic[topicId] = cur;
3282
+ await writePrivate(DOSSIERS_FILE, df);
3283
+ return cur;
3284
+ }
3285
+ async function dossierNewTurns(topicId, opts = {}) {
3286
+ const d = (await loadDossiers()).byTopic[topicId];
3287
+ const covered = new Set(d?.covered ?? []);
3288
+ const all = await byTopicJson([topicId], { limit: 5e3 });
3289
+ const fresh = all.hits.filter((h) => !covered.has(`${h.session}#${h.turn}`));
3290
+ const sorted = fresh.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? "")).slice(0, opts.limit ?? 120);
3291
+ const excerpts = [];
3292
+ for (const h of sorted) {
3293
+ try {
3294
+ const r = await recallJson(h.session, h.turn);
3295
+ excerpts.push({ key: `${h.session}#${h.turn}`, q: r.question.replace(/\s+/g, " ").slice(0, 300), a: r.response.replace(/\s+/g, " ").slice(0, 600) });
3296
+ } catch {
3297
+ }
3298
+ }
3299
+ return { hits: sorted, excerpts, total: fresh.length };
3300
+ }
3301
+ async function sampleQuestions(n = 120) {
3302
+ const out = [];
3303
+ let lines = null;
3304
+ try {
3305
+ await fsp.access(TEXT_LINES);
3306
+ lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3307
+ } catch {
3308
+ return out;
3309
+ }
3310
+ const all = [];
3311
+ for await (const line of lines) {
3312
+ let t;
3313
+ try {
3314
+ t = JSON.parse(line);
3315
+ } catch {
3316
+ continue;
3317
+ }
3318
+ const q = (t.q ?? "").trim();
3319
+ if (q.length >= 20 && q.length <= 400) all.push(q);
3320
+ }
3321
+ const step = Math.max(1, Math.floor(all.length / n));
3322
+ for (let i = 0; i < all.length && out.length < n; i += step) out.push(all[i].slice(0, 200));
3323
+ return out;
3324
+ }
3325
+ async function memoriesJson() {
3326
+ const facts = await ensureFresh();
3327
+ return Object.values(facts.sessions).filter((x) => x.kind === "memory").sort((a, b) => b.mtime - a.mtime).map((x) => ({ id: x.id, runner: x.runner, file: x.file, title: x.title, cwd: x.cwd, entries: x.turns.length, mtime: x.mtime, headings: x.turns.map((t) => t.q) }));
3328
+ }
3329
+ var CLI_VERSION = "0.2.0";
2796
3330
  var MCP_TOOLS = [
2797
3331
  { name: "why_check", description: "Cheap first question before editing a file: does this artifact have any history in local agent sessions? One line; history true/false.", inputSchema: { type: "object", properties: { path: { type: "string", description: "file path (absolute or relative to cwd), URL, or arxiv:<id>" } }, required: ["path"] } },
2798
3332
  { name: "why_file", description: "The turns across local Claude Code, Codex, DeepSeek Harness, Pi and ThoughtDAG sessions that touched a file, URL or paper: when, what changed (\u0394, observed), what was asked, what the answer said about it (\u2248, a candidate explanation, not a verified reason). Each hit carries a deep link.", inputSchema: { type: "object", properties: { path: { type: "string" }, include_read: { type: "boolean", description: "also list turns that only read it (default false)" }, limit: { type: "number", description: "max hits (default 10)" } }, required: ["path"] } },
2799
- { name: "find", description: "Where these exact words were asked (Q), answered (A) or attached (M) across local sessions and canvases. Exact, case-insensitive match; every hit is a verbatim snippet with a pointer.", inputSchema: { type: "object", properties: { phrase: { type: "string" }, in: { type: "string", enum: ["q", "a", "m", "all"] }, limit: { type: "number" } }, required: ["phrase"] } },
3333
+ { name: "find", description: "Where these exact words were asked (Q), answered (A) or attached (M) across local sessions, canvases and the memories Claude Code and Codex keep for themselves. Exact, case-insensitive match; every hit is a verbatim snippet with a pointer.", inputSchema: { type: "object", properties: { phrase: { type: "string" }, in: { type: "string", enum: ["q", "a", "m", "all"] }, limit: { type: "number" } }, required: ["phrase"] } },
2800
3334
  { name: "recall_turn", description: "One turn in full \u2014 the question, the answer, the tool calls with their diffs \u2014 by session id (or prefix) and turn number as shown by why_file.", inputSchema: { type: "object", properties: { session: { type: "string" }, turn: { type: "number" } }, required: ["session", "turn"] } }
2801
3335
  ];
2802
3336
  async function mcpCall(name, a) {
@@ -2835,21 +3369,38 @@ export {
2835
3369
  TEXT_LINES,
2836
3370
  artifactsLine,
2837
3371
  buildIndex,
3372
+ byTopicJson,
3373
+ deleteDossier,
3374
+ dossierAddPending,
3375
+ dossierJson,
3376
+ dossierNewTurns,
3377
+ dossiersJson,
2838
3378
  ensureFresh,
2839
3379
  eventsOf,
2840
3380
  factsForCheck,
2841
3381
  findHits,
3382
+ findJson,
2842
3383
  hitsFor,
3384
+ labelStart,
3385
+ labelStop,
2843
3386
  loadCache,
2844
3387
  loadFacts,
2845
3388
  mcpCall,
3389
+ memoriesJson,
3390
+ recallJson,
3391
+ recallTurn,
2846
3392
  renderCheck,
2847
3393
  renderFind,
2848
3394
  renderRecall,
2849
3395
  renderWhy,
2850
3396
  resolveQuery,
3397
+ sampleQuestions,
3398
+ setDossier,
2851
3399
  setQuiet,
3400
+ setTopics,
3401
+ suggestJson,
2852
3402
  summarize,
3403
+ topicsJson,
2853
3404
  when,
2854
3405
  zstdSkipped
2855
3406
  };