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.
- package/dist-app/assets/{canonical-mCpjqoou.js → canonical-BlzQbV3x.js} +2 -2
- package/dist-app/assets/canvas-record-CHx9KBrZ.js +1 -0
- package/dist-app/assets/{claude-code-session-DJkcGfvB.js → claude-code-session-BfwTY3ne.js} +4 -4
- package/dist-app/assets/{codex-session-Crw7-bHo.js → codex-session-BNnk6Ozy.js} +4 -4
- package/dist-app/assets/{dsh-session-DbEtbF5u.js → dsh-session-BXjdZH-4.js} +4 -4
- package/dist-app/assets/{experiment-loop-CkNvS6b1.js → experiment-loop-CBfcRt8t.js} +1 -1
- package/dist-app/assets/{http-bridge-CJvoQLgy.js → http-bridge-CK02uyVL.js} +1 -1
- package/dist-app/assets/index-93ujsGjx.css +1 -0
- package/dist-app/assets/index-F8ykUu-e.js +883 -0
- package/dist-app/assets/{index-CT3R4H-W.js → index-U1FDd3d6.js} +1 -1
- package/dist-app/assets/{index-ZITmlOUw.js → index-Vln2-D4p.js} +3 -3
- package/dist-app/assets/{index-B6lBI8ja.js → index-no3SFEm4.js} +1 -1
- package/dist-app/assets/{pi-session-CBjuXvCc.js → pi-session-DfX1QntM.js} +8 -8
- package/dist-app/assets/{sensitive-scan-CjmPZxkv.js → sensitive-scan-NeBfDua2.js} +1 -1
- package/dist-app/assets/{session-handoff-DGfX02CE.js → session-handoff-Bi6J6owx.js} +2 -2
- package/dist-app/assets/{shared-z6Es4wHy.js → shared-B_zE0vyL.js} +4 -4
- package/dist-app/assets/{turndown-plugin-gfm.cjs-KFp3duAG.js → turndown-plugin-gfm.cjs-t7mTv0d4.js} +3 -3
- package/dist-app/assets/{update-check-D5kNTq-_.js → update-check-CmPLQTC0.js} +1 -1
- package/dist-app/index.html +2 -2
- package/lib/index.js +50 -1
- package/lib/why.mjs +567 -16
- package/package.json +1 -1
- package/dist-app/assets/canvas-record-di62TikJ.js +0 -1
- package/dist-app/assets/index-B-DuMDps.css +0 -1
- package/dist-app/assets/index-BMFFskkL.js +0 -789
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
${
|
|
2955
|
+
${r.question.trim()}
|
|
2780
2956
|
`);
|
|
2781
2957
|
out.push(`## Answer
|
|
2782
2958
|
|
|
2783
|
-
${
|
|
2959
|
+
${r.response.trim() || "(none)"}
|
|
2784
2960
|
`);
|
|
2785
|
-
if (
|
|
2786
|
-
out.push(`## Tools (${
|
|
2961
|
+
if (r.tools.length) {
|
|
2962
|
+
out.push(`## Tools (${r.tools.length})
|
|
2787
2963
|
`);
|
|
2788
|
-
for (const t of
|
|
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
|
-
|
|
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
|
|
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
|
};
|