dsh-thoughtdag 0.4.20 → 0.5.1

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 (27) hide show
  1. package/dist-app/assets/{canonical-mCpjqoou.js → canonical-BAKMmYtu.js} +2 -2
  2. package/dist-app/assets/canvas-record-v0T9A4JC.js +1 -0
  3. package/dist-app/assets/{claude-code-session-DJkcGfvB.js → claude-code-session-WliRGG8W.js} +4 -4
  4. package/dist-app/assets/{codex-session-Crw7-bHo.js → codex-session-CIpFw1w2.js} +4 -4
  5. package/dist-app/assets/dsh-session-rxnDGmEX.js +6 -0
  6. package/dist-app/assets/{experiment-loop-CkNvS6b1.js → experiment-loop-CQaQhKai.js} +1 -1
  7. package/dist-app/assets/{http-bridge-CJvoQLgy.js → http-bridge-CFJoyZSa.js} +1 -1
  8. package/dist-app/assets/index-93ujsGjx.css +1 -0
  9. package/dist-app/assets/{index-B6lBI8ja.js → index-BPz35wb6.js} +1 -1
  10. package/dist-app/assets/index-D0VdUWYE.js +883 -0
  11. package/dist-app/assets/{index-CT3R4H-W.js → index-DE0h-KA1.js} +1 -1
  12. package/dist-app/assets/{index-ZITmlOUw.js → index-DxbomCNl.js} +3 -3
  13. package/dist-app/assets/{pi-session-CBjuXvCc.js → pi-session-BjtG1-5k.js} +8 -8
  14. package/dist-app/assets/{sensitive-scan-CjmPZxkv.js → sensitive-scan-Bu7x2N6Y.js} +1 -1
  15. package/dist-app/assets/{session-handoff-DGfX02CE.js → session-handoff-BYVQEntk.js} +2 -2
  16. package/dist-app/assets/{shared-z6Es4wHy.js → shared-BuierUP1.js} +4 -4
  17. package/dist-app/assets/{turndown-plugin-gfm.cjs-KFp3duAG.js → turndown-plugin-gfm.cjs-CukkoPc7.js} +3 -3
  18. package/dist-app/assets/{update-check-D5kNTq-_.js → update-check-BIhP2KP4.js} +1 -1
  19. package/dist-app/index.html +2 -2
  20. package/lib/index.js +50 -1
  21. package/lib/why.mjs +616 -26
  22. package/package.json +1 -1
  23. package/dist-app/assets/canvas-record-di62TikJ.js +0 -1
  24. package/dist-app/assets/dsh-session-DbEtbF5u.js +0 -6
  25. package/dist-app/assets/index-B-DuMDps.css +0 -1
  26. package/dist-app/assets/index-BMFFskkL.js +0 -789
  27. 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;
@@ -963,6 +964,12 @@ function buildGraphFromTurns2(turns, sessionId, cwd) {
963
964
  }
964
965
 
965
966
  // ../src/lib/adapters/dsh-session.ts
967
+ function resultFailed(data) {
968
+ if (!data) return false;
969
+ if (data.error) return true;
970
+ const parts = data.message?.content;
971
+ return Array.isArray(parts) && parts.some((p) => !!p && typeof p === "object" && p.isError === true);
972
+ }
966
973
  function partsText(content, depth = 0) {
967
974
  if (typeof content === "string") return content;
968
975
  if (!Array.isArray(content) || depth > 2) return "";
@@ -1072,26 +1079,31 @@ ${text}` : text;
1072
1079
  if (!reg) return;
1073
1080
  const res = clipText(partsText(tr.data?.message?.content), TOOL_RESULT_LIMIT);
1074
1081
  const t = this.ensure();
1082
+ const failed = resultFailed(tr.data);
1075
1083
  t.tools.push({
1076
1084
  name: reg.name,
1077
1085
  call: reg.call,
1078
1086
  result: res.text,
1079
1087
  truncated: reg.truncated || res.truncated,
1080
- op: reg.op,
1088
+ op: failed ? "other" : reg.op,
1081
1089
  nativeCallId: callId,
1082
- ...reg.paths.length ? { paths: reg.paths } : {},
1083
- ...reg.url ? { url: reg.url } : {},
1084
- ...reg.locator ? { locator: reg.locator } : {}
1090
+ ...failed ? {} : { ...reg.paths.length ? { paths: reg.paths } : {}, ...reg.url ? { url: reg.url } : {}, ...reg.locator ? { locator: reg.locator } : {} }
1085
1091
  });
1086
1092
  this.pendingTools.delete(callId);
1087
1093
  return;
1088
1094
  }
1089
- if (line.type === "tool/code-dispatch") {
1095
+ if (line.type === "tool/code-dispatch" || line.type === "tool/ptc-dispatch") {
1090
1096
  const d = line.data;
1091
1097
  if (!d?.name || !d.subCallId) return;
1092
1098
  const paths = toolPaths(d.arguments);
1093
1099
  const scope = toolScope(d.arguments);
1094
1100
  if (!paths.length && !scope.url) return;
1101
+ if (d.isError === true) {
1102
+ const attempt = clipText(renderCall(d.name, d.arguments), TOOL_CALL_LIMIT);
1103
+ const err = clipText(partsText(d.content), TOOL_RESULT_LIMIT);
1104
+ this.ensure().tools.push({ name: d.name, call: attempt.text, result: err.text, truncated: attempt.truncated || err.truncated, op: "other", nativeCallId: d.subCallId });
1105
+ return;
1106
+ }
1095
1107
  const op = toolOpOf(d.name);
1096
1108
  const call = clipText(renderCall(d.name, d.arguments), op === "write" || op === "edit" ? ARTIFACT_CALL_LIMIT : TOOL_CALL_LIMIT);
1097
1109
  const res = clipText(partsText(d.content), TOOL_RESULT_LIMIT);
@@ -2008,7 +2020,7 @@ ${a.extractedText.slice(0, 4e3)}`).join("\n\n");
2008
2020
 
2009
2021
  // ../cli/src/lib.ts
2010
2022
  var readLike = (op) => op === "read" || op === "fetch" || op === "attach";
2011
- var INDEX_VERSION = 10;
2023
+ var INDEX_VERSION = 13;
2012
2024
  var EXCERPT = 200;
2013
2025
  var HOME = process.env.THOUGHTDAG_HOME ?? path.join(os.homedir(), ".thoughtdag");
2014
2026
  var FACT_FILE = path.join(HOME, "fact-index.json");
@@ -2020,6 +2032,25 @@ var CONFIG_FILE = path.join(HOME, "config.json");
2020
2032
  var DSH_ROOT = path.join(process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh"), "sessions");
2021
2033
  var PI_ROOT = path.join(os.homedir(), ".pi", "agent", "sessions");
2022
2034
  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];
2035
+ var MEMORY_ROOTS = (() => {
2036
+ const env = process.env.THOUGHTDAG_MEMORY_ROOTS;
2037
+ if (env !== void 0) {
2038
+ return env.split(path.delimiter).filter(Boolean).flatMap((e) => {
2039
+ const m = /^(claude-code|codex)=(.+)$/.exec(e);
2040
+ return m ? [{ runner: m[1], root: m[2] }] : [];
2041
+ });
2042
+ }
2043
+ if (process.env.THOUGHTDAG_SESSION_ROOTS) return [];
2044
+ return [
2045
+ { runner: "claude-code", root: path.join(os.homedir(), ".claude", "projects") },
2046
+ { runner: "codex", root: path.join(os.homedir(), ".codex", "memories") }
2047
+ ];
2048
+ })();
2049
+ var isMemoryFile = (file, runner) => {
2050
+ if (!file.endsWith(".md")) return false;
2051
+ if (runner === "claude-code") return path.basename(path.dirname(file)) === "memory" && path.basename(file) !== "MEMORY.md";
2052
+ return true;
2053
+ };
2023
2054
  var CANVAS_RECORDS = path.join(HOME, "canvases");
2024
2055
  async function canvasRoots() {
2025
2056
  const env = process.env.THOUGHTDAG_CANVAS_ROOTS?.split(path.delimiter).filter(Boolean);
@@ -2062,6 +2093,11 @@ async function listSources() {
2062
2093
  const files = [];
2063
2094
  for (const root of ROOTS) await walk(root, 0, files, isSessionFile);
2064
2095
  for (const root of await canvasRoots()) await walk(root, 0, files, isCanvasFile);
2096
+ for (const { runner, root } of MEMORY_ROOTS) {
2097
+ const md = [];
2098
+ await walk(root, 0, md, (name) => name.endsWith(".md"));
2099
+ for (const m of md) if (isMemoryFile(m.file, runner)) files.push({ ...m, memory: runner });
2100
+ }
2065
2101
  return files;
2066
2102
  }
2067
2103
  async function firstLine(file) {
@@ -2220,6 +2256,7 @@ async function eventsOf(file, sourceId) {
2220
2256
  return { runner, nativeId: s.sessionId, title: s.title, ...anchor ? { anchor } : {}, ...cwd ? { cwd } : {}, ...subagent ? { subagent } : {}, events, texts, manifest: MANIFESTS[runner], turns: s.turns };
2221
2257
  }
2222
2258
  async function parseSession(f) {
2259
+ if (f.memory) return parseMemory(f, f.memory);
2223
2260
  const memo = /* @__PURE__ */ new Map();
2224
2261
  const sourceId = await canonicalPath(f.file, memo);
2225
2262
  const p = await eventsOf(f.file, sourceId);
@@ -2274,6 +2311,136 @@ async function parseSession(f) {
2274
2311
  bundles
2275
2312
  };
2276
2313
  }
2314
+ function frontmatterOf(text) {
2315
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
2316
+ if (!m) return { meta: {}, body: text };
2317
+ const meta = {};
2318
+ for (const line of m[1].split(/\r?\n/)) {
2319
+ const kv = /^\s*([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
2320
+ if (kv && kv[2].trim()) meta[kv[1]] = kv[2].trim();
2321
+ }
2322
+ return { meta, body: text.slice(m[0].length) };
2323
+ }
2324
+ function sectionsOf(text, level) {
2325
+ const out = [];
2326
+ const lines = text.split(/\r?\n/);
2327
+ let cur = null;
2328
+ const isBoundary = (line) => {
2329
+ const m = /^(#{1,6})\s+(.*)$/.exec(line);
2330
+ return m && m[1].length <= level ? m[1].length : null;
2331
+ };
2332
+ for (const line of lines) {
2333
+ const lvl = isBoundary(line);
2334
+ if (lvl === level) {
2335
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2336
+ cur = { heading: line.replace(/^#+\s+/, "").trim(), body: [] };
2337
+ continue;
2338
+ }
2339
+ if (lvl !== null && lvl < level) {
2340
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2341
+ cur = null;
2342
+ continue;
2343
+ }
2344
+ if (cur) cur.body.push(line);
2345
+ }
2346
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n").trim() });
2347
+ return out;
2348
+ }
2349
+ var metaLine = (body, key) => {
2350
+ const m = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(body);
2351
+ return m ? m[1].trim() : void 0;
2352
+ };
2353
+ var claudeCwdMemo = /* @__PURE__ */ new Map();
2354
+ function claudeProjectCwd(projectDir) {
2355
+ let p = claudeCwdMemo.get(projectDir);
2356
+ if (!p) {
2357
+ p = (async () => {
2358
+ let entries = [];
2359
+ try {
2360
+ entries = (await fsp.readdir(projectDir)).filter((n) => n.endsWith(".jsonl")).sort();
2361
+ } catch {
2362
+ return "";
2363
+ }
2364
+ for (const n of entries) {
2365
+ try {
2366
+ const o = JSON.parse(await firstLine(path.join(projectDir, n)));
2367
+ if (typeof o.cwd === "string" && o.cwd) return o.cwd;
2368
+ } catch {
2369
+ }
2370
+ }
2371
+ return "";
2372
+ })();
2373
+ claudeCwdMemo.set(projectDir, p);
2374
+ }
2375
+ return p;
2376
+ }
2377
+ async function memoryFile(file, runner) {
2378
+ let text;
2379
+ try {
2380
+ text = await fsp.readFile(file, "utf8");
2381
+ } catch {
2382
+ return null;
2383
+ }
2384
+ const base = path.basename(file);
2385
+ if (runner === "claude-code") {
2386
+ const projectDir = path.dirname(path.dirname(file));
2387
+ const cwd = await claudeProjectCwd(projectDir);
2388
+ const { meta, body } = frontmatterOf(text);
2389
+ const heading = meta.description || meta.name || base.replace(/\.md$/, "");
2390
+ const project = path.basename(cwd || projectDir);
2391
+ return { title: `memory \xB7 ${project} \xB7 ${base.replace(/\.md$/, "")}`, cwd, units: body.trim() ? [{ heading, body: body.trim() }] : [] };
2392
+ }
2393
+ if (base === "MEMORY.md") {
2394
+ const units = [];
2395
+ for (const g of sectionsOf(text, 1)) {
2396
+ const cwd = /cwd=([^;\n]+)/.exec(metaLine(g.body, "applies_to") ?? "")?.[1]?.trim();
2397
+ const subs2 = sectionsOf(g.body, 2);
2398
+ if (!subs2.length) {
2399
+ units.push({ heading: g.heading, body: g.body, ...cwd ? { cwd } : {} });
2400
+ continue;
2401
+ }
2402
+ for (const u of subs2) units.push({ heading: `${g.heading} \u203A ${u.heading}`, body: u.body, ...cwd ? { cwd } : {} });
2403
+ }
2404
+ return { title: "memory \xB7 Codex \xB7 MEMORY.md", cwd: "", units };
2405
+ }
2406
+ if (base === "raw_memories.md") {
2407
+ const units = sectionsOf(text, 2).map((u) => {
2408
+ const cwd = metaLine(u.body, "cwd");
2409
+ const at = metaLine(u.body, "updated_at");
2410
+ const heading = cwd ? `${u.heading} \xB7 ${path.basename(cwd)}` : u.heading;
2411
+ return { heading, body: u.body, ...cwd ? { cwd } : {}, ...at ? { at: new Date(at).toISOString() } : {} };
2412
+ });
2413
+ return { title: "memory \xB7 Codex \xB7 raw_memories.md", cwd: "", units };
2414
+ }
2415
+ const subs = sectionsOf(text, 2);
2416
+ if (subs.length) return { title: `memory \xB7 Codex \xB7 ${base}`, cwd: "", units: subs };
2417
+ const h1 = sectionsOf(text, 1)[0];
2418
+ return { title: `memory \xB7 Codex \xB7 ${base}`, cwd: "", units: text.trim() ? [{ heading: h1?.heading ?? base, body: text.trim() }] : [] };
2419
+ }
2420
+ var shortHash = (s) => createHash("sha1").update(s).digest("hex").slice(0, 12);
2421
+ async function parseMemory(f, runner) {
2422
+ const mf = await memoryFile(f.file, runner);
2423
+ if (!mf || !mf.units.length) return null;
2424
+ const memo = /* @__PURE__ */ new Map();
2425
+ const cwd = mf.cwd ? await canonicalPath(mf.cwd, memo) : "";
2426
+ const workspace = cwd ? await workspaceOf(cwd) : "";
2427
+ const at = new Date(f.mtime).toISOString();
2428
+ const turns = [];
2429
+ const text = {};
2430
+ for (const [i, u] of mf.units.entries()) {
2431
+ const unitCwd = u.cwd ? await canonicalPath(u.cwd, memo) : "";
2432
+ turns.push({ i, t: `memory#${i}`, at: u.at ?? at, q: questionExcerpt(u.heading), ops: {}, ...unitCwd && unitCwd !== cwd ? { cwd: unitCwd } : {} });
2433
+ text[String(i)] = { q: u.heading, a: u.body };
2434
+ }
2435
+ const id = `mem-${runner === "codex" ? "cx" : "cc"}-${shortHash(await canonicalPath(f.file, memo))}`;
2436
+ return {
2437
+ fact: { id, runner, kind: "memory", file: f.file, mtime: f.mtime, size: f.size, cwd, workspace, title: mf.title, turns },
2438
+ cache: {},
2439
+ text,
2440
+ names: {},
2441
+ bundles: {}
2442
+ };
2443
+ }
2277
2444
  var emptyFacts = () => ({ version: INDEX_VERSION, builtAt: "", sessions: {}, skipped: {}, names: {}, bundles: {} });
2278
2445
  var emptyCache = () => ({ version: INDEX_VERSION, sessions: {} });
2279
2446
  var emptyText = () => ({ version: INDEX_VERSION, sessions: {} });
@@ -2288,21 +2455,21 @@ async function readJson(file, empty) {
2288
2455
  var tmpFor = (file) => `${file}.${process.pid}.${Date.now().toString(36)}.tmp`;
2289
2456
  var LOCK_FILE = path.join(HOME, "index.lock");
2290
2457
  var LOCK_STALE_MS = 10 * 6e4;
2291
- async function acquireLock() {
2458
+ async function acquireLock(file = LOCK_FILE, staleMs = LOCK_STALE_MS) {
2292
2459
  await fsp.mkdir(HOME, { recursive: true, mode: 448 });
2293
2460
  for (let attempt = 0; attempt < 2; attempt++) {
2294
2461
  try {
2295
- const fh = await fsp.open(LOCK_FILE, "wx", 384);
2462
+ const fh = await fsp.open(file, "wx", 384);
2296
2463
  await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
2297
2464
  await fh.close();
2298
2465
  return async () => {
2299
- await fsp.rm(LOCK_FILE, { force: true }).catch(() => void 0);
2466
+ await fsp.rm(file, { force: true }).catch(() => void 0);
2300
2467
  };
2301
2468
  } catch (err) {
2302
2469
  if (err.code !== "EEXIST") throw err;
2303
2470
  let holder = {};
2304
2471
  try {
2305
- holder = JSON.parse(await fsp.readFile(LOCK_FILE, "utf8"));
2472
+ holder = JSON.parse(await fsp.readFile(file, "utf8"));
2306
2473
  } catch {
2307
2474
  }
2308
2475
  const alive = (() => {
@@ -2314,9 +2481,9 @@ async function acquireLock() {
2314
2481
  return false;
2315
2482
  }
2316
2483
  })();
2317
- const stale = !alive || !holder.at || Date.now() - holder.at > LOCK_STALE_MS;
2484
+ const stale = !alive || !holder.at || Date.now() - holder.at > staleMs;
2318
2485
  if (!stale) return null;
2319
- await fsp.rm(LOCK_FILE, { force: true }).catch(() => void 0);
2486
+ await fsp.rm(file, { force: true }).catch(() => void 0);
2320
2487
  }
2321
2488
  }
2322
2489
  return null;
@@ -2501,7 +2668,8 @@ function summarize(facts, cache) {
2501
2668
  }
2502
2669
  }
2503
2670
  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 };
2671
+ const mem = Object.values(facts.sessions).filter((s) => s.kind === "memory");
2672
+ 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
2673
  }
2506
2674
  var allPaths = (facts) => {
2507
2675
  const all = /* @__PURE__ */ new Set();
@@ -2560,6 +2728,7 @@ function anchorStatus(facts, a) {
2560
2728
  return b.node === a.node ? "matched" : "mismatch";
2561
2729
  }
2562
2730
  function openLink(h) {
2731
+ if (h.session.kind === "memory") return `thoughtdag://open?memory=${encodeURIComponent(h.session.file)}&entry=${h.turn.i}`;
2563
2732
  if (h.session.runner === "thoughtdag") return `thoughtdag://open?canvas=${encodeURIComponent(h.session.id)}&node=${encodeURIComponent(h.turn.t.split("#").pop() ?? "")}`;
2564
2733
  return `thoughtdag://open?session=${h.session.id}${h.turn.item ? `&turn=${encodeURIComponent(h.turn.item)}` : ""}`;
2565
2734
  }
@@ -2701,7 +2870,7 @@ async function findHits(facts, phrase, scope) {
2701
2870
  if (!body) continue;
2702
2871
  if (scope !== "all" && where.toLowerCase() !== scope) continue;
2703
2872
  if (!body.toLowerCase().includes(needle)) continue;
2704
- raw.push({ session, sourceKey: t.k, turn, where, snippet: snippetAround(body, needle) });
2873
+ raw.push({ session, sourceKey: t.k, turn, where, snippet: snippetAround(body, needle), kind: session.kind === "memory" ? "memory" : "turn" });
2705
2874
  break;
2706
2875
  }
2707
2876
  }
@@ -2726,7 +2895,7 @@ function renderFind(facts, phrase, hits, limit, json) {
2726
2895
  turns: hits.length,
2727
2896
  sessions,
2728
2897
  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) }))
2898
+ 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
2899
  }, null, 1));
2731
2900
  return out.join("\n");
2732
2901
  }
@@ -2734,7 +2903,7 @@ function renderFind(facts, phrase, hits, limit, json) {
2734
2903
  `);
2735
2904
  for (const h of shown) {
2736
2905
  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)}`);
2906
+ 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
2907
  out.push(` ${h.where}: ${h.snippet}`);
2739
2908
  }
2740
2909
  if (!shown.length) out.push("(nothing asked, answered or attached in those words \u2014 try another wording; matching is exact)");
@@ -2758,13 +2927,19 @@ function renderCheck(facts, arg, file, hits, json) {
2758
2927
  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
2928
  return { text: `${arg}: ${parts.join(" \xB7 ")} \u2192 thoughtdag why ${arg}`, has };
2760
2929
  }
2761
- async function renderRecall(facts, sidPrefix, n) {
2762
- const out = [];
2930
+ async function recallTurn(facts, sidPrefix, n) {
2763
2931
  const any = Object.values(facts.sessions).find((x) => x.id.startsWith(sidPrefix));
2764
2932
  if (!any) throw new Error(`no session starts with ${sidPrefix}`);
2765
2933
  const frag = fragmentsOf(facts, any.id).find((f) => n >= f.offset && n < f.offset + f.s.turns.length);
2766
2934
  const s = frag?.s ?? any;
2767
2935
  const local = frag ? n - frag.offset : n;
2936
+ const base = { runner: s.runner, session: s.id, title: s.title, turn: n, file: s.file };
2937
+ if (s.kind === "memory") {
2938
+ const mf = await memoryFile(s.file, s.runner);
2939
+ const u = mf?.units[local];
2940
+ if (!u) throw new Error(`memory ${s.id.slice(0, 8)} has no entry #${n}`);
2941
+ return { ...base, kind: "memory", at: s.turns[local]?.at, cwd: s.turns[local]?.cwd ?? s.cwd, question: u.heading, response: u.body, tools: [] };
2942
+ }
2768
2943
  const p = await eventsOf(s.file);
2769
2944
  const turn = p?.turns?.[local] ?? (() => {
2770
2945
  const tid = s.turns[local]?.t;
@@ -2772,31 +2947,429 @@ async function renderRecall(facts, sidPrefix, n) {
2772
2947
  return text ? { question: text.question, response: text.response, tools: [], at: s.turns[local]?.at } : void 0;
2773
2948
  })();
2774
2949
  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", " ")}` : ""}
2950
+ return { ...base, kind: "turn", at: turn.at, cwd: s.cwd, question: turn.question, response: turn.response, tools: turn.tools };
2951
+ }
2952
+ async function renderRecall(facts, sidPrefix, n) {
2953
+ const r = await recallTurn(facts, sidPrefix, n);
2954
+ const out = [];
2955
+ 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", " ")}` : ""}
2956
+ `);
2957
+ if (r.kind === "memory") {
2958
+ out.push(`## ${r.question.trim()}
2959
+
2960
+ ${r.response.trim() || "(empty)"}
2776
2961
  `);
2962
+ return out.join("\n");
2963
+ }
2777
2964
  out.push(`## Question
2778
2965
 
2779
- ${turn.question.trim()}
2966
+ ${r.question.trim()}
2780
2967
  `);
2781
2968
  out.push(`## Answer
2782
2969
 
2783
- ${turn.response.trim() || "(none)"}
2970
+ ${r.response.trim() || "(none)"}
2784
2971
  `);
2785
- if (turn.tools.length) {
2786
- out.push(`## Tools (${turn.tools.length})
2972
+ if (r.tools.length) {
2973
+ out.push(`## Tools (${r.tools.length})
2787
2974
  `);
2788
- for (const t of turn.tools) {
2975
+ for (const t of r.tools) {
2789
2976
  out.push(`- ${t.name}${t.paths?.length ? ` ${t.paths.join(", ")}` : ""}`);
2790
2977
  if (t.op === "edit" || t.op === "write") out.push(t.call.slice(0, 1200).split("\n").map((l) => ` ${l}`).join("\n"));
2978
+ else if (t.op === "other" && /^error\b/i.test(t.result.trimStart())) out.push(` \u2717 ${t.result.trim().split("\n")[0].slice(0, 200)}`);
2791
2979
  }
2792
2980
  }
2793
2981
  return out.join("\n");
2794
2982
  }
2795
- var CLI_VERSION = "0.1.0";
2983
+ async function findJson(phrase, opts = {}) {
2984
+ const facts = await ensureFresh();
2985
+ let hits = await findHits(facts, phrase, opts.scope ?? "all");
2986
+ if (opts.cwd) {
2987
+ const want = opts.cwd.replace(/[\\/]+$/, "");
2988
+ hits = hits.filter((h) => {
2989
+ const c = h.turn.cwd ?? h.session.cwd;
2990
+ return c === want || c.startsWith(want + path.sep);
2991
+ });
2992
+ }
2993
+ const limit = opts.limit ?? 20;
2994
+ const shown = [...hits.filter((h) => h.kind === "memory").slice(0, limit), ...hits.filter((h) => h.kind === "turn").slice(0, limit)];
2995
+ return {
2996
+ phrase,
2997
+ turns: hits.length,
2998
+ sessions: new Set(hits.map((h) => h.session.id)).size,
2999
+ 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) }))
3000
+ };
3001
+ }
3002
+ async function recallJson(session, n) {
3003
+ return recallTurn(await ensureFresh(), session, n);
3004
+ }
3005
+ var vocabCache = null;
3006
+ async function vocabulary() {
3007
+ let stamp = "";
3008
+ try {
3009
+ const st = await fsp.stat(TEXT_LINES);
3010
+ stamp = `${st.mtimeMs}:${st.size}`;
3011
+ } catch {
3012
+ return /* @__PURE__ */ new Map();
3013
+ }
3014
+ if (vocabCache && vocabCache.builtFrom === stamp) return vocabCache.counts;
3015
+ const counts = /* @__PURE__ */ new Map();
3016
+ const lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3017
+ for await (const line of lines) {
3018
+ let t;
3019
+ try {
3020
+ t = JSON.parse(line);
3021
+ } catch {
3022
+ continue;
3023
+ }
3024
+ for (const body of [t.q, t.a, t.m]) {
3025
+ if (!body) continue;
3026
+ for (const m of body.matchAll(/[A-Za-z][A-Za-z0-9_-]{3,}/g)) {
3027
+ const w = m[0].toLowerCase();
3028
+ counts.set(w, (counts.get(w) ?? 0) + 1);
3029
+ }
3030
+ }
3031
+ }
3032
+ vocabCache = { builtFrom: stamp, counts };
3033
+ return counts;
3034
+ }
3035
+ function editDistance(a, b, max) {
3036
+ if (Math.abs(a.length - b.length) > max) return max + 1;
3037
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3038
+ for (let i = 1; i <= a.length; i++) {
3039
+ const cur = [i];
3040
+ let rowMin = i;
3041
+ for (let j = 1; j <= b.length; j++) {
3042
+ const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
3043
+ cur.push(v);
3044
+ if (v < rowMin) rowMin = v;
3045
+ }
3046
+ if (rowMin > max) return max + 1;
3047
+ prev = cur;
3048
+ }
3049
+ return prev[b.length];
3050
+ }
3051
+ async function suggestJson(term, k = 8) {
3052
+ const t = term.toLowerCase().trim();
3053
+ const counts = await vocabulary();
3054
+ const known = counts.get(t) ?? 0;
3055
+ if (!t || t.length < 3) return { term, known, suggestions: [] };
3056
+ const max = t.length >= 6 ? 2 : 1;
3057
+ const out = [];
3058
+ for (const [w, count] of counts) {
3059
+ if (w === t) continue;
3060
+ const d = editDistance(t, w, max);
3061
+ if (d <= max) out.push({ term: w, count, distance: d });
3062
+ }
3063
+ out.sort((a, b) => a.distance - b.distance || b.count - a.count);
3064
+ return { term, known, suggestions: out.slice(0, k) };
3065
+ }
3066
+ var TOPICS_FILE = path.join(HOME, "topics.json");
3067
+ var TOPICS_VERSION = 1;
3068
+ var emptyTopics = () => ({ version: TOPICS_VERSION, topics: [], labels: {}, labeledAt: {}, updatedAt: "" });
3069
+ var loadTopics = async () => {
3070
+ try {
3071
+ const v = JSON.parse(await fsp.readFile(TOPICS_FILE, "utf8"));
3072
+ return v.version === TOPICS_VERSION ? { ...emptyTopics(), ...v } : emptyTopics();
3073
+ } catch {
3074
+ return emptyTopics();
3075
+ }
3076
+ };
3077
+ var turnKey = (k, i) => `${k}#${i}`;
3078
+ var cleanForJudge = (s) => s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
3079
+ async function systemOne(call, state, questions) {
3080
+ const body = call.wrap === "cloudflare" ? { model: call.model ?? "typesafe/jev", input: { state, questions } } : { ...call.model ? { model: call.model } : {}, state, questions };
3081
+ const r = await fetch(call.url, { method: "POST", headers: { "Content-Type": "application/json", ...call.headers }, body: JSON.stringify(body), signal: AbortSignal.timeout(9e4) });
3082
+ const text = await r.text();
3083
+ let j = {};
3084
+ try {
3085
+ j = JSON.parse(text);
3086
+ } catch {
3087
+ throw new Error(`judge answered ${r.status} with non-JSON`);
3088
+ }
3089
+ if (!r.ok) throw new Error(typeof j.error === "string" ? j.error : j.error?.message ?? `HTTP ${r.status}`);
3090
+ const answers = (call.wrap === "cloudflare" ? j.result?.answers : j.answers) ?? {};
3091
+ return answers;
3092
+ }
3093
+ var labelStatus = { running: false, done: 0, total: 0, labeled: 0, errors: 0, startedAt: null, finishedAt: null, lastError: null, stopRequested: false };
3094
+ async function topicsJson() {
3095
+ const tf = await loadTopics();
3096
+ const text = await loadText();
3097
+ const turns = Object.values(text.sessions).reduce((n, c) => n + c, 0);
3098
+ const counts = /* @__PURE__ */ new Map();
3099
+ 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);
3100
+ return { topics: tf.topics.map((t) => ({ ...t, count: counts.get(t.id) ?? 0 })), labeled: Object.keys(tf.labels).length, turns, status: { ...labelStatus } };
3101
+ }
3102
+ async function setTopics(topics) {
3103
+ const tf = await loadTopics();
3104
+ 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) }));
3105
+ const keep = new Set(clean.map((t) => t.id));
3106
+ for (const [k, per] of Object.entries(tf.labels)) {
3107
+ for (const id of Object.keys(per)) if (!keep.has(id)) delete per[id];
3108
+ if (!Object.keys(per).length) {
3109
+ delete tf.labels[k];
3110
+ delete tf.labeledAt[k];
3111
+ }
3112
+ }
3113
+ tf.topics = clean;
3114
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3115
+ await writePrivate(TOPICS_FILE, tf);
3116
+ return clean;
3117
+ }
3118
+ async function labelStart(call, opts = {}) {
3119
+ if (labelStatus.running) return { ...labelStatus };
3120
+ const tf = await loadTopics();
3121
+ if (!tf.topics.length) throw new Error("no topics to label with");
3122
+ const batch = Math.max(1, Math.min(16, opts.batch ?? 8));
3123
+ Object.assign(labelStatus, { running: true, done: 0, total: 0, labeled: 0, errors: 0, startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, lastError: null, stopRequested: false });
3124
+ void (async () => {
3125
+ try {
3126
+ const needs = (t) => {
3127
+ const have = tf.labels[turnKey(t.k, t.i)];
3128
+ if (have && tf.topics.every((tp) => tp.id in have)) return false;
3129
+ return (t.q?.trim().length ?? 0) + (t.a?.trim().length ?? 0) >= 40;
3130
+ };
3131
+ const open = async () => {
3132
+ try {
3133
+ await fsp.access(TEXT_LINES);
3134
+ return createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3135
+ } catch {
3136
+ return null;
3137
+ }
3138
+ };
3139
+ const keys = [];
3140
+ const first = await open();
3141
+ if (first) for await (const line of first) {
3142
+ let t;
3143
+ try {
3144
+ t = JSON.parse(line);
3145
+ } catch {
3146
+ continue;
3147
+ }
3148
+ if (needs(t)) keys.push(turnKey(t.k, t.i));
3149
+ }
3150
+ const chosen = new Set(opts.max ? keys.slice(-opts.max) : keys);
3151
+ const pending = [];
3152
+ const second = chosen.size ? await open() : null;
3153
+ if (second) for await (const line of second) {
3154
+ let t;
3155
+ try {
3156
+ t = JSON.parse(line);
3157
+ } catch {
3158
+ continue;
3159
+ }
3160
+ if (chosen.has(turnKey(t.k, t.i))) pending.push(t);
3161
+ }
3162
+ pending.reverse();
3163
+ labelStatus.total = pending.length;
3164
+ let sinceWrite = 0;
3165
+ for (let at = 0; at < pending.length && !labelStatus.stopRequested; at += batch) {
3166
+ const group = pending.slice(at, at + batch);
3167
+ const state = {};
3168
+ const questions = {};
3169
+ group.forEach((t, gi) => {
3170
+ state[`t${gi}`] = { q: cleanForJudge((t.q ?? "").slice(0, 300)), a: cleanForJudge((t.a ?? "").slice(0, 700)) };
3171
+ 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})` : ""}?` };
3172
+ });
3173
+ try {
3174
+ const answers = await systemOne(call, state, questions);
3175
+ group.forEach((t, gi) => {
3176
+ const key = turnKey(t.k, t.i);
3177
+ const per = tf.labels[key] ?? {};
3178
+ for (const tp of tf.topics) {
3179
+ const p = answers[`t${gi}_${tp.id}`]?.noul;
3180
+ if (typeof p === "number") per[tp.id] = Math.round(p * 100) / 100;
3181
+ }
3182
+ tf.labels[key] = per;
3183
+ tf.labeledAt[key] = (/* @__PURE__ */ new Date()).toISOString();
3184
+ labelStatus.labeled++;
3185
+ });
3186
+ } catch (e) {
3187
+ labelStatus.errors++;
3188
+ labelStatus.lastError = e instanceof Error ? e.message : String(e);
3189
+ if (labelStatus.errors >= 5 && labelStatus.labeled === 0) break;
3190
+ await new Promise((r) => setTimeout(r, 1500));
3191
+ }
3192
+ labelStatus.done = Math.min(pending.length, at + group.length);
3193
+ sinceWrite += group.length;
3194
+ if (sinceWrite >= 80) {
3195
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3196
+ await writePrivate(TOPICS_FILE, tf);
3197
+ sinceWrite = 0;
3198
+ }
3199
+ }
3200
+ tf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3201
+ await writePrivate(TOPICS_FILE, tf);
3202
+ } catch (e) {
3203
+ labelStatus.lastError = e instanceof Error ? e.message : String(e);
3204
+ } finally {
3205
+ labelStatus.running = false;
3206
+ labelStatus.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3207
+ }
3208
+ })();
3209
+ return { ...labelStatus };
3210
+ }
3211
+ function labelStop() {
3212
+ if (labelStatus.running) labelStatus.stopRequested = true;
3213
+ return { ...labelStatus };
3214
+ }
3215
+ async function byTopicJson(topicIds, opts = {}) {
3216
+ const tf = await loadTopics();
3217
+ const facts = await loadFacts();
3218
+ const minP = opts.minP ?? 0.6;
3219
+ const want = new Set(topicIds);
3220
+ const rows = [];
3221
+ for (const [key, per] of Object.entries(tf.labels)) {
3222
+ let score = 0;
3223
+ const topics = {};
3224
+ for (const [id, p] of Object.entries(per)) if (want.has(id) && p >= minP) {
3225
+ score = Math.max(score, p);
3226
+ topics[id] = p;
3227
+ }
3228
+ if (!score) continue;
3229
+ const hash = key.lastIndexOf("#");
3230
+ rows.push({ key, k: key.slice(0, hash), i: Number(key.slice(hash + 1)), score, topics });
3231
+ }
3232
+ const hits = [];
3233
+ for (const r of rows) {
3234
+ const session = facts.sessions[r.k];
3235
+ const turn = session?.turns.find((t) => t.i === r.i);
3236
+ if (!session || !turn) continue;
3237
+ 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 });
3238
+ }
3239
+ hits.sort((a, b) => (b._score ?? 0) - (a._score ?? 0) || (b.at ?? "").localeCompare(a.at ?? ""));
3240
+ const total = hits.length;
3241
+ return { hits: hits.slice(0, opts.limit ?? 60).map((h) => {
3242
+ const { _score, ...rest } = h;
3243
+ void _score;
3244
+ return rest;
3245
+ }), total };
3246
+ }
3247
+ var DOSSIERS_FILE = path.join(HOME, "dossiers.json");
3248
+ var DOSSIERS_VERSION = 1;
3249
+ var emptyDossiers = () => ({ version: DOSSIERS_VERSION, byTopic: {} });
3250
+ var loadDossiers = async () => {
3251
+ try {
3252
+ const v = JSON.parse(await fsp.readFile(DOSSIERS_FILE, "utf8"));
3253
+ return v.version === DOSSIERS_VERSION ? { ...emptyDossiers(), ...v } : emptyDossiers();
3254
+ } catch {
3255
+ return emptyDossiers();
3256
+ }
3257
+ };
3258
+ var emptyDossier = (topicId) => ({ topicId, sections: { what: [], decisions: [], now: [], open: [] }, sources: {}, covered: [], pending: [], changelog: [], builtAt: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
3259
+ async function dossiersJson() {
3260
+ const [tf, df] = await Promise.all([loadTopics(), loadDossiers()]);
3261
+ const out = [];
3262
+ for (const tp of tf.topics) {
3263
+ const d = df.byTopic[tp.id];
3264
+ const labeled = (await byTopicJson([tp.id], { limit: 5e3 })).hits.map((h) => `${h.session}#${h.turn}`);
3265
+ const covered = new Set(d?.covered ?? []);
3266
+ const s = d?.sections;
3267
+ const sentences = s ? s.what.length + s.decisions.length + s.now.length + s.open.length : 0;
3268
+ 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 });
3269
+ }
3270
+ return out;
3271
+ }
3272
+ async function dossierJson(topicId) {
3273
+ return (await loadDossiers()).byTopic[topicId] ?? null;
3274
+ }
3275
+ var DOSSIERS_LOCK = path.join(HOME, "dossiers.lock");
3276
+ var DOSSIERS_LOCK_WAIT_MS = 5e3;
3277
+ var dossierWrites = Promise.resolve();
3278
+ async function withDossiers(fn) {
3279
+ const run = async () => {
3280
+ let release = null;
3281
+ const deadline = Date.now() + DOSSIERS_LOCK_WAIT_MS;
3282
+ while (!(release = await acquireLock(DOSSIERS_LOCK, 3e4)) && Date.now() < deadline) await new Promise((r) => setTimeout(r, 60));
3283
+ try {
3284
+ const df = await loadDossiers();
3285
+ const out = fn(df);
3286
+ await writePrivate(DOSSIERS_FILE, df);
3287
+ return out;
3288
+ } finally {
3289
+ await release?.();
3290
+ }
3291
+ };
3292
+ const p = dossierWrites.then(run, run);
3293
+ dossierWrites = p.catch(() => void 0);
3294
+ return p;
3295
+ }
3296
+ async function setDossier(topicId, d) {
3297
+ return withDossiers((df) => {
3298
+ const cur = df.byTopic[topicId] ?? emptyDossier(topicId);
3299
+ const { consumePending, ...rest } = d;
3300
+ const patch = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== void 0));
3301
+ const next = { ...cur, ...patch, topicId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
3302
+ if (consumePending) {
3303
+ const read = new Set(consumePending);
3304
+ next.pending = cur.pending.filter((p) => !read.has(p.id));
3305
+ }
3306
+ df.byTopic[topicId] = next;
3307
+ return next;
3308
+ });
3309
+ }
3310
+ async function deleteDossier(topicId) {
3311
+ await withDossiers((df) => {
3312
+ delete df.byTopic[topicId];
3313
+ });
3314
+ }
3315
+ async function dossierAddPending(topicId, item) {
3316
+ return withDossiers((df) => {
3317
+ const cur = df.byTopic[topicId] ?? emptyDossier(topicId);
3318
+ cur.pending.push({ id: shortHash(`${Date.now()}:${cur.pending.length}:${item.text}`), text: item.text.slice(0, 400), at: (/* @__PURE__ */ new Date()).toISOString(), ...item.from ? { from: item.from } : {} });
3319
+ cur.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3320
+ df.byTopic[topicId] = cur;
3321
+ return cur;
3322
+ });
3323
+ }
3324
+ async function dossierNewTurns(topicId, opts = {}) {
3325
+ const d = (await loadDossiers()).byTopic[topicId];
3326
+ const covered = new Set(d?.covered ?? []);
3327
+ const all = await byTopicJson([topicId], { limit: 5e3 });
3328
+ const fresh = all.hits.filter((h) => !covered.has(`${h.session}#${h.turn}`));
3329
+ const sorted = fresh.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? "")).slice(0, opts.limit ?? 120);
3330
+ const excerpts = [];
3331
+ for (const h of sorted) {
3332
+ try {
3333
+ const r = await recallJson(h.session, h.turn);
3334
+ 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) });
3335
+ } catch {
3336
+ }
3337
+ }
3338
+ return { hits: sorted, excerpts, total: fresh.length };
3339
+ }
3340
+ async function sampleQuestions(n = 120) {
3341
+ const out = [];
3342
+ let lines = null;
3343
+ try {
3344
+ await fsp.access(TEXT_LINES);
3345
+ lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
3346
+ } catch {
3347
+ return out;
3348
+ }
3349
+ const all = [];
3350
+ for await (const line of lines) {
3351
+ let t;
3352
+ try {
3353
+ t = JSON.parse(line);
3354
+ } catch {
3355
+ continue;
3356
+ }
3357
+ const q = (t.q ?? "").trim();
3358
+ if (q.length >= 20 && q.length <= 400) all.push(q);
3359
+ }
3360
+ const step = Math.max(1, Math.floor(all.length / n));
3361
+ for (let i = 0; i < all.length && out.length < n; i += step) out.push(all[i].slice(0, 200));
3362
+ return out;
3363
+ }
3364
+ async function memoriesJson() {
3365
+ const facts = await ensureFresh();
3366
+ 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) }));
3367
+ }
3368
+ var CLI_VERSION = "0.2.1";
2796
3369
  var MCP_TOOLS = [
2797
3370
  { 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
3371
  { 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"] } },
3372
+ { 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
3373
  { 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
3374
  ];
2802
3375
  async function mcpCall(name, a) {
@@ -2835,21 +3408,38 @@ export {
2835
3408
  TEXT_LINES,
2836
3409
  artifactsLine,
2837
3410
  buildIndex,
3411
+ byTopicJson,
3412
+ deleteDossier,
3413
+ dossierAddPending,
3414
+ dossierJson,
3415
+ dossierNewTurns,
3416
+ dossiersJson,
2838
3417
  ensureFresh,
2839
3418
  eventsOf,
2840
3419
  factsForCheck,
2841
3420
  findHits,
3421
+ findJson,
2842
3422
  hitsFor,
3423
+ labelStart,
3424
+ labelStop,
2843
3425
  loadCache,
2844
3426
  loadFacts,
2845
3427
  mcpCall,
3428
+ memoriesJson,
3429
+ recallJson,
3430
+ recallTurn,
2846
3431
  renderCheck,
2847
3432
  renderFind,
2848
3433
  renderRecall,
2849
3434
  renderWhy,
2850
3435
  resolveQuery,
3436
+ sampleQuestions,
3437
+ setDossier,
2851
3438
  setQuiet,
3439
+ setTopics,
3440
+ suggestJson,
2852
3441
  summarize,
3442
+ topicsJson,
2853
3443
  when,
2854
3444
  zstdSkipped
2855
3445
  };