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.
- package/dist-app/assets/{canonical-mCpjqoou.js → canonical-BAKMmYtu.js} +2 -2
- package/dist-app/assets/canvas-record-v0T9A4JC.js +1 -0
- package/dist-app/assets/{claude-code-session-DJkcGfvB.js → claude-code-session-WliRGG8W.js} +4 -4
- package/dist-app/assets/{codex-session-Crw7-bHo.js → codex-session-CIpFw1w2.js} +4 -4
- package/dist-app/assets/dsh-session-rxnDGmEX.js +6 -0
- package/dist-app/assets/{experiment-loop-CkNvS6b1.js → experiment-loop-CQaQhKai.js} +1 -1
- package/dist-app/assets/{http-bridge-CJvoQLgy.js → http-bridge-CFJoyZSa.js} +1 -1
- package/dist-app/assets/index-93ujsGjx.css +1 -0
- package/dist-app/assets/{index-B6lBI8ja.js → index-BPz35wb6.js} +1 -1
- package/dist-app/assets/index-D0VdUWYE.js +883 -0
- package/dist-app/assets/{index-CT3R4H-W.js → index-DE0h-KA1.js} +1 -1
- package/dist-app/assets/{index-ZITmlOUw.js → index-DxbomCNl.js} +3 -3
- package/dist-app/assets/{pi-session-CBjuXvCc.js → pi-session-BjtG1-5k.js} +8 -8
- package/dist-app/assets/{sensitive-scan-CjmPZxkv.js → sensitive-scan-Bu7x2N6Y.js} +1 -1
- package/dist-app/assets/{session-handoff-DGfX02CE.js → session-handoff-BYVQEntk.js} +2 -2
- package/dist-app/assets/{shared-z6Es4wHy.js → shared-BuierUP1.js} +4 -4
- package/dist-app/assets/{turndown-plugin-gfm.cjs-KFp3duAG.js → turndown-plugin-gfm.cjs-CukkoPc7.js} +3 -3
- package/dist-app/assets/{update-check-D5kNTq-_.js → update-check-BIhP2KP4.js} +1 -1
- package/dist-app/index.html +2 -2
- package/lib/index.js +50 -1
- package/lib/why.mjs +616 -26
- package/package.json +1 -1
- package/dist-app/assets/canvas-record-di62TikJ.js +0 -1
- package/dist-app/assets/dsh-session-DbEtbF5u.js +0 -6
- 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;
|
|
@@ -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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 >
|
|
2484
|
+
const stale = !alive || !holder.at || Date.now() - holder.at > staleMs;
|
|
2318
2485
|
if (!stale) return null;
|
|
2319
|
-
await fsp.rm(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
${
|
|
2966
|
+
${r.question.trim()}
|
|
2780
2967
|
`);
|
|
2781
2968
|
out.push(`## Answer
|
|
2782
2969
|
|
|
2783
|
-
${
|
|
2970
|
+
${r.response.trim() || "(none)"}
|
|
2784
2971
|
`);
|
|
2785
|
-
if (
|
|
2786
|
-
out.push(`## Tools (${
|
|
2972
|
+
if (r.tools.length) {
|
|
2973
|
+
out.push(`## Tools (${r.tools.length})
|
|
2787
2974
|
`);
|
|
2788
|
-
for (const t of
|
|
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
|
-
|
|
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
|
|
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
|
};
|