@inerrata-corporation/errata 2.0.23-dev.4779 → 2.0.23-dev.4796
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/errata.mjs
CHANGED
|
@@ -37887,26 +37887,26 @@ function buildFileLexicon(store) {
|
|
|
37887
37887
|
for (const n of store.findNodeFieldsByLabel("File", ["relPath"])) {
|
|
37888
37888
|
const relPath = typeof n.attrs["relPath"] === "string" && n.attrs["relPath"].length > 0 ? n.attrs["relPath"] : n.description;
|
|
37889
37889
|
if (!relPath) continue;
|
|
37890
|
-
const
|
|
37891
|
-
if (!
|
|
37892
|
-
entries.push({ relPath, basename:
|
|
37893
|
-
basenameCount.set(
|
|
37894
|
-
const stem =
|
|
37890
|
+
const basename11 = relPath.split("/").pop();
|
|
37891
|
+
if (!basename11.includes(".")) continue;
|
|
37892
|
+
entries.push({ relPath, basename: basename11 });
|
|
37893
|
+
basenameCount.set(basename11, (basenameCount.get(basename11) ?? 0) + 1);
|
|
37894
|
+
const stem = basename11.slice(0, basename11.indexOf("."));
|
|
37895
37895
|
if (isBareMatchableStem(stem)) {
|
|
37896
37896
|
let s = stemRelPaths.get(stem);
|
|
37897
37897
|
if (!s) stemRelPaths.set(stem, s = /* @__PURE__ */ new Set());
|
|
37898
37898
|
s.add(relPath);
|
|
37899
37899
|
}
|
|
37900
37900
|
}
|
|
37901
|
-
for (const { relPath, basename:
|
|
37901
|
+
for (const { relPath, basename: basename11 } of entries) {
|
|
37902
37902
|
const entry = { relPath };
|
|
37903
|
-
if (relPath !==
|
|
37904
|
-
const conventional = CONVENTION_FILENAMES.has(
|
|
37905
|
-
if (!conventional && !byKey.has(
|
|
37906
|
-
const list = byBasename.get(
|
|
37903
|
+
if (relPath !== basename11) byKey.set(relPath, entry);
|
|
37904
|
+
const conventional = CONVENTION_FILENAMES.has(basename11) || (basenameCount.get(basename11) ?? 0) >= CONVENTION_BASENAME_MIN_FILES;
|
|
37905
|
+
if (!conventional && !byKey.has(basename11)) byKey.set(basename11, entry);
|
|
37906
|
+
const list = byBasename.get(basename11);
|
|
37907
37907
|
if (list) list.push(entry);
|
|
37908
|
-
else byBasename.set(
|
|
37909
|
-
const stem =
|
|
37908
|
+
else byBasename.set(basename11, [entry]);
|
|
37909
|
+
const stem = basename11.slice(0, basename11.indexOf("."));
|
|
37910
37910
|
if (isBareMatchableStem(stem) && !CONVENTION_STEMS.has(stem) && !byStem.has(stem)) {
|
|
37911
37911
|
const dirStems = new Set(
|
|
37912
37912
|
[...stemRelPaths.get(stem) ?? []].map((p) => {
|
|
@@ -37923,8 +37923,8 @@ function resolveFileToken(token, lexicon) {
|
|
|
37923
37923
|
const exact = lexicon.byKey.get(token);
|
|
37924
37924
|
if (exact) return exact;
|
|
37925
37925
|
if (!token.includes("/")) return null;
|
|
37926
|
-
const
|
|
37927
|
-
for (const entry of lexicon.byBasename.get(
|
|
37926
|
+
const basename11 = token.split("/").pop();
|
|
37927
|
+
for (const entry of lexicon.byBasename.get(basename11) ?? []) {
|
|
37928
37928
|
if (entry.relPath === token || entry.relPath.endsWith(`/${token}`)) return entry;
|
|
37929
37929
|
}
|
|
37930
37930
|
return null;
|
|
@@ -37949,9 +37949,9 @@ function fileRolePhrase(relPath, level = 1) {
|
|
|
37949
37949
|
return "a source file";
|
|
37950
37950
|
}
|
|
37951
37951
|
function isSourceFileToken(token) {
|
|
37952
|
-
const
|
|
37953
|
-
if (CONVENTION_FILENAMES.has(
|
|
37954
|
-
const ext =
|
|
37952
|
+
const basename11 = token.split(/[/\\]/).pop() ?? token;
|
|
37953
|
+
if (CONVENTION_FILENAMES.has(basename11)) return false;
|
|
37954
|
+
const ext = basename11.split(".").pop();
|
|
37955
37955
|
return ext !== void 0 && SOURCE_EXTENSIONS.has(ext.toLowerCase());
|
|
37956
37956
|
}
|
|
37957
37957
|
function generalizeFileRefs(text, fileLexicon, level = 1) {
|
|
@@ -57729,6 +57729,7 @@ var init_message_accumulator = __esm({
|
|
|
57729
57729
|
var webui_exports = {};
|
|
57730
57730
|
__export(webui_exports, {
|
|
57731
57731
|
ContextBaselines: () => ContextBaselines,
|
|
57732
|
+
FOCUS_NOTICE_FIELD: () => FOCUS_NOTICE_FIELD,
|
|
57732
57733
|
SUBAGENT_REFRESH_MIN_MS: () => SUBAGENT_REFRESH_MIN_MS,
|
|
57733
57734
|
asFailedPostToolUse: () => asFailedPostToolUse,
|
|
57734
57735
|
buildWebUi: () => buildWebUi,
|
|
@@ -58438,7 +58439,9 @@ ${delta}${cue}`;
|
|
|
58438
58439
|
block = own ?? readContextFileWithHash(deps.paths.root);
|
|
58439
58440
|
storedCard = own ? own.card : block ? readContextCard(deps.paths.root, block.hash) : null;
|
|
58440
58441
|
}
|
|
58441
|
-
const
|
|
58442
|
+
const focusNotice = typeof body2[FOCUS_NOTICE_FIELD] === "string" ? body2[FOCUS_NOTICE_FIELD] : null;
|
|
58443
|
+
if (focusNotice && contextKey2) lastInjectedHash.reset(contextKey2, "focus");
|
|
58444
|
+
const decided = decideContextInject({
|
|
58442
58445
|
event: hookEvent,
|
|
58443
58446
|
source: String(body2["source"] ?? ""),
|
|
58444
58447
|
sessionId: contextKey2,
|
|
@@ -58448,6 +58451,9 @@ ${delta}${cue}`;
|
|
|
58448
58451
|
// A subagent's injections land in its own transcript, not the one posted.
|
|
58449
58452
|
...subagent === null && typeof body2["transcript_path"] === "string" && body2["transcript_path"] ? { transcriptPath: body2["transcript_path"] } : {}
|
|
58450
58453
|
});
|
|
58454
|
+
const decision = focusNotice ? { hookEventName: decided?.hookEventName ?? hookEvent, additionalContext: decided ? `${focusNotice}
|
|
58455
|
+
|
|
58456
|
+
${decided.additionalContext}` : focusNotice } : decided;
|
|
58451
58457
|
if (decision && subagent !== null) lastSubagentInjectAt.set(contextKey2, Date.now());
|
|
58452
58458
|
let systemMessage = null;
|
|
58453
58459
|
const delivered = decision !== null && decision.additionalContext !== PROMPT_RECUE;
|
|
@@ -58646,7 +58652,7 @@ function proposalToIngest(p, profile, daemonVersion) {
|
|
|
58646
58652
|
payloadDigest: p.id
|
|
58647
58653
|
};
|
|
58648
58654
|
}
|
|
58649
|
-
var messageAccumulator, GENERIC_ERROR_WORDS, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, SUBAGENT_REFRESH_MIN_MS, WINDOW_RESET_SOURCES, FULL_BLOCK_PREFIX, META_LABELS;
|
|
58655
|
+
var messageAccumulator, GENERIC_ERROR_WORDS, wsPackagesCache, WS_PACKAGES_TTL_MS, loggedToolSessions, SUBAGENT_REFRESH_MIN_MS, WINDOW_RESET_SOURCES, FULL_BLOCK_PREFIX, FOCUS_NOTICE_FIELD, META_LABELS;
|
|
58650
58656
|
var init_webui = __esm({
|
|
58651
58657
|
"src/webui.ts"() {
|
|
58652
58658
|
"use strict";
|
|
@@ -58713,6 +58719,7 @@ var init_webui = __esm({
|
|
|
58713
58719
|
SUBAGENT_REFRESH_MIN_MS = 6e4;
|
|
58714
58720
|
WINDOW_RESET_SOURCES = /* @__PURE__ */ new Set(["compact", "clear", "resume"]);
|
|
58715
58721
|
FULL_BLOCK_PREFIX = "The errata daemon has updated your live workspace context. The block below REPLACES any earlier errata context block in this conversation \u2014 treat it as the current source of truth:\n\n";
|
|
58722
|
+
FOCUS_NOTICE_FIELD = "errata_focus_notice";
|
|
58716
58723
|
META_LABELS = /* @__PURE__ */ new Set([
|
|
58717
58724
|
"Pattern",
|
|
58718
58725
|
"Technique",
|
|
@@ -78858,10 +78865,10 @@ var ReaddirpStream = class extends Readable2 {
|
|
|
78858
78865
|
}
|
|
78859
78866
|
async _formatEntry(dirent, path3) {
|
|
78860
78867
|
let entry;
|
|
78861
|
-
const
|
|
78868
|
+
const basename11 = this._isDirent ? dirent.name : dirent;
|
|
78862
78869
|
try {
|
|
78863
|
-
const fullPath = presolve(pjoin(path3,
|
|
78864
|
-
entry = { path: prelative(this._root, fullPath), fullPath, basename:
|
|
78870
|
+
const fullPath = presolve(pjoin(path3, basename11));
|
|
78871
|
+
entry = { path: prelative(this._root, fullPath), fullPath, basename: basename11 };
|
|
78865
78872
|
entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
|
|
78866
78873
|
} catch (err2) {
|
|
78867
78874
|
this._onError(err2);
|
|
@@ -79400,9 +79407,9 @@ var NodeFsHandler = class {
|
|
|
79400
79407
|
_watchWithNodeFs(path3, listener) {
|
|
79401
79408
|
const opts = this.fsw.options;
|
|
79402
79409
|
const directory = sysPath.dirname(path3);
|
|
79403
|
-
const
|
|
79410
|
+
const basename11 = sysPath.basename(path3);
|
|
79404
79411
|
const parent = this.fsw._getWatchedDir(directory);
|
|
79405
|
-
parent.add(
|
|
79412
|
+
parent.add(basename11);
|
|
79406
79413
|
const absolutePath = sysPath.resolve(path3);
|
|
79407
79414
|
const options = {
|
|
79408
79415
|
persistent: opts.persistent
|
|
@@ -79412,7 +79419,7 @@ var NodeFsHandler = class {
|
|
|
79412
79419
|
let closer;
|
|
79413
79420
|
if (opts.usePolling) {
|
|
79414
79421
|
const enableBin = opts.interval !== opts.binaryInterval;
|
|
79415
|
-
options.interval = enableBin && isBinaryPath(
|
|
79422
|
+
options.interval = enableBin && isBinaryPath(basename11) ? opts.binaryInterval : opts.interval;
|
|
79416
79423
|
closer = setFsWatchFileListener(path3, absolutePath, options, {
|
|
79417
79424
|
listener,
|
|
79418
79425
|
rawEmitter: this.fsw._emitRaw
|
|
@@ -79435,10 +79442,10 @@ var NodeFsHandler = class {
|
|
|
79435
79442
|
return;
|
|
79436
79443
|
}
|
|
79437
79444
|
const dirname21 = sysPath.dirname(file2);
|
|
79438
|
-
const
|
|
79445
|
+
const basename11 = sysPath.basename(file2);
|
|
79439
79446
|
const parent = this.fsw._getWatchedDir(dirname21);
|
|
79440
79447
|
let prevStats = stats;
|
|
79441
|
-
if (parent.has(
|
|
79448
|
+
if (parent.has(basename11))
|
|
79442
79449
|
return;
|
|
79443
79450
|
const listener = async (path3, newStats) => {
|
|
79444
79451
|
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
|
|
@@ -79463,9 +79470,9 @@ var NodeFsHandler = class {
|
|
|
79463
79470
|
prevStats = newStats2;
|
|
79464
79471
|
}
|
|
79465
79472
|
} catch (error48) {
|
|
79466
|
-
this.fsw._remove(dirname21,
|
|
79473
|
+
this.fsw._remove(dirname21, basename11);
|
|
79467
79474
|
}
|
|
79468
|
-
} else if (parent.has(
|
|
79475
|
+
} else if (parent.has(basename11)) {
|
|
79469
79476
|
const at = newStats.atimeMs;
|
|
79470
79477
|
const mt = newStats.mtimeMs;
|
|
79471
79478
|
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
@@ -87520,7 +87527,7 @@ function createWatchBreaker(deps) {
|
|
|
87520
87527
|
}
|
|
87521
87528
|
|
|
87522
87529
|
// src/engine.ts
|
|
87523
|
-
var DAEMON_VERSION = true ? "2.0.23-dev.
|
|
87530
|
+
var DAEMON_VERSION = true ? "2.0.23-dev.4796" : "2.0.0-alpha.0";
|
|
87524
87531
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
87525
87532
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
87526
87533
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -87563,15 +87570,43 @@ function loadTurnOffsets(path3) {
|
|
|
87563
87570
|
return /* @__PURE__ */ new Map();
|
|
87564
87571
|
}
|
|
87565
87572
|
}
|
|
87566
|
-
function
|
|
87573
|
+
function loadTurnMoves(path3) {
|
|
87574
|
+
try {
|
|
87575
|
+
const raw2 = JSON.parse(readFileSync33(path3, "utf8"));
|
|
87576
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
87577
|
+
for (const [k, v] of Object.entries(raw2)) {
|
|
87578
|
+
const to = typeof v === "object" && v !== null ? v.movedTo : void 0;
|
|
87579
|
+
if (typeof to === "string" && to) out2.set(k, to);
|
|
87580
|
+
}
|
|
87581
|
+
return out2;
|
|
87582
|
+
} catch {
|
|
87583
|
+
return /* @__PURE__ */ new Map();
|
|
87584
|
+
}
|
|
87585
|
+
}
|
|
87586
|
+
function saveTurnCursors(path3, cursors, offsets, moves = /* @__PURE__ */ new Map()) {
|
|
87567
87587
|
try {
|
|
87568
87588
|
const merged = {};
|
|
87569
87589
|
for (const [k, uuid3] of cursors) merged[k] = { uuid: uuid3, offset: offsets.get(k) ?? 0 };
|
|
87570
87590
|
for (const [k, offset] of offsets) if (!merged[k]) merged[k] = { uuid: "", offset };
|
|
87591
|
+
for (const [k, movedTo] of moves) merged[k] = { ...merged[k] ?? { uuid: "", offset: 0 }, movedTo };
|
|
87571
87592
|
writeFileSync24(path3, JSON.stringify(merged), "utf8");
|
|
87572
87593
|
} catch {
|
|
87573
87594
|
}
|
|
87574
87595
|
}
|
|
87596
|
+
var turnCursorFile = (configDir) => join37(configDir, "turn-cursors.json");
|
|
87597
|
+
function turnCursorOnDisk(configDir, sessionId) {
|
|
87598
|
+
const path3 = turnCursorFile(configDir);
|
|
87599
|
+
const offset = loadTurnOffsets(path3).get(sessionId);
|
|
87600
|
+
const movedTo = loadTurnMoves(path3).get(sessionId);
|
|
87601
|
+
if (offset === void 0 && movedTo === void 0) return null;
|
|
87602
|
+
return { uuid: loadTurnCursors(path3).get(sessionId) ?? "", offset: offset ?? 0, ...movedTo ? { movedTo } : {} };
|
|
87603
|
+
}
|
|
87604
|
+
function releaseTurnCursorOnDisk(configDir, sessionId, to) {
|
|
87605
|
+
const path3 = turnCursorFile(configDir);
|
|
87606
|
+
const moves = loadTurnMoves(path3);
|
|
87607
|
+
moves.set(sessionId, to);
|
|
87608
|
+
saveTurnCursors(path3, loadTurnCursors(path3), loadTurnOffsets(path3), moves);
|
|
87609
|
+
}
|
|
87575
87610
|
function gitSourceWatchTargets(root) {
|
|
87576
87611
|
const SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i;
|
|
87577
87612
|
try {
|
|
@@ -88578,9 +88613,10 @@ function createWorkspaceEngine(opts) {
|
|
|
88578
88613
|
resultSummary: { exitCode: e.exitCode ?? 1, errorTokens: e.errorTokens }
|
|
88579
88614
|
});
|
|
88580
88615
|
};
|
|
88581
|
-
const turnCursorPath =
|
|
88616
|
+
const turnCursorPath = turnCursorFile(paths.configDir);
|
|
88582
88617
|
const lastTurnUuid = loadTurnCursors(turnCursorPath);
|
|
88583
88618
|
const turnOffset = loadTurnOffsets(turnCursorPath);
|
|
88619
|
+
const turnMovedTo = loadTurnMoves(turnCursorPath);
|
|
88584
88620
|
const readLosses = { parseFailures: 0, ioFailures: 0, bytesUnreachable: 0, sliceGuardHits: 0 };
|
|
88585
88621
|
const sessionLastProblem = /* @__PURE__ */ new Map();
|
|
88586
88622
|
const sessionThreads = /* @__PURE__ */ new Map();
|
|
@@ -89588,7 +89624,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89588
89624
|
lastTurnUuid.set(sessionId, turns[turns.length - 1].uuid);
|
|
89589
89625
|
await harvestTexts(sessionId, fresh);
|
|
89590
89626
|
}
|
|
89591
|
-
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
|
|
89627
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89592
89628
|
if (slice >= 256) {
|
|
89593
89629
|
readLosses.sliceGuardHits++;
|
|
89594
89630
|
console.warn("[errata] harvest slice guard hit \u2014 backlog resumes next tick");
|
|
@@ -89605,7 +89641,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89605
89641
|
if (next.losses.ioFailed) readLosses.ioFailures++;
|
|
89606
89642
|
if (next.turns.length === 0 && !next.more) {
|
|
89607
89643
|
turnOffset.set(sessionId, next.nextOffset);
|
|
89608
|
-
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
|
|
89644
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89609
89645
|
break;
|
|
89610
89646
|
}
|
|
89611
89647
|
turns = next.turns;
|
|
@@ -89659,6 +89695,7 @@ function createWorkspaceEngine(opts) {
|
|
|
89659
89695
|
}
|
|
89660
89696
|
};
|
|
89661
89697
|
const harvestSession = async (sessionId, transcriptPath) => {
|
|
89698
|
+
if (turnMovedTo.has(sessionId)) return;
|
|
89662
89699
|
await harvestTurns(sessionId, transcriptPath);
|
|
89663
89700
|
for (const sub of subagentTranscripts(transcriptPath, sessionId)) {
|
|
89664
89701
|
await yieldToLoop();
|
|
@@ -89894,6 +89931,28 @@ B: ${b}`,
|
|
|
89894
89931
|
storeReconciled: () => storeReconciled,
|
|
89895
89932
|
lastGitEventAt: () => lastGitEventTs,
|
|
89896
89933
|
quarantinedSessions: () => sessions.originSessions(),
|
|
89934
|
+
turnCursor(sessionId) {
|
|
89935
|
+
const offset = turnOffset.get(sessionId);
|
|
89936
|
+
const movedTo = turnMovedTo.get(sessionId);
|
|
89937
|
+
if (offset === void 0 && movedTo === void 0) return null;
|
|
89938
|
+
return { uuid: lastTurnUuid.get(sessionId) ?? "", offset: offset ?? 0, ...movedTo ? { movedTo } : {} };
|
|
89939
|
+
},
|
|
89940
|
+
releaseTurnCursor(sessionId, to) {
|
|
89941
|
+
turnMovedTo.set(sessionId, to);
|
|
89942
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89943
|
+
},
|
|
89944
|
+
adoptTurnCursor(sessionId, cursor) {
|
|
89945
|
+
turnMovedTo.delete(sessionId);
|
|
89946
|
+
if (cursor && cursor.offset > (turnOffset.get(sessionId) ?? -1)) {
|
|
89947
|
+
turnOffset.set(sessionId, cursor.offset);
|
|
89948
|
+
if (cursor.uuid) lastTurnUuid.set(sessionId, cursor.uuid);
|
|
89949
|
+
}
|
|
89950
|
+
saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset, turnMovedTo);
|
|
89951
|
+
},
|
|
89952
|
+
async settleTurns(sessionId) {
|
|
89953
|
+
await (harvestInFlight.get(sessionId) ?? Promise.resolve()).catch(() => {
|
|
89954
|
+
});
|
|
89955
|
+
},
|
|
89897
89956
|
markContextDirty() {
|
|
89898
89957
|
contextDirty = true;
|
|
89899
89958
|
refreshContextNow();
|
|
@@ -90943,7 +91002,7 @@ function invalidateInstanceReceipt(current, destination, failedId, mirrorLegacy)
|
|
|
90943
91002
|
init_dist();
|
|
90944
91003
|
init_src5();
|
|
90945
91004
|
import { existsSync as existsSync34, readFileSync as readFileSync40, writeFileSync as writeFileSync29 } from "node:fs";
|
|
90946
|
-
import { dirname as dirname17, join as join47, resolve as pathResolve } from "node:path";
|
|
91005
|
+
import { basename as basename10, dirname as dirname17, join as join47, resolve as pathResolve } from "node:path";
|
|
90947
91006
|
|
|
90948
91007
|
// src/principle-sync.ts
|
|
90949
91008
|
init_src5();
|
|
@@ -95425,6 +95484,15 @@ function absolutePathsInCommand(command, platform = process.platform) {
|
|
|
95425
95484
|
}
|
|
95426
95485
|
return out2;
|
|
95427
95486
|
}
|
|
95487
|
+
function leadingCdTarget(command, platform = process.platform) {
|
|
95488
|
+
if (typeof command !== "string") return void 0;
|
|
95489
|
+
const m = /^\s*(?:cd|pushd|Set-Location)\s+("[^"]+"|'[^']+'|[^\s;&|]+)\s*(?:&&|;|\|\||$)/.exec(command);
|
|
95490
|
+
if (!m) return void 0;
|
|
95491
|
+
const target = m[1].replace(/^["']|["']$/g, "");
|
|
95492
|
+
if (/^[a-zA-Z]:[\\/]/.test(target)) return target;
|
|
95493
|
+
if (platform === "win32") return /^\/[a-zA-Z](\/|$)/.test(target) ? msysToWindowsPath(target, platform) : void 0;
|
|
95494
|
+
return target.startsWith("/") ? target : void 0;
|
|
95495
|
+
}
|
|
95428
95496
|
function hookPathCandidates(body2, platform = process.platform) {
|
|
95429
95497
|
const input = body2["tool_input"] ?? {};
|
|
95430
95498
|
const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -95470,6 +95538,205 @@ var SessionAffinity = class {
|
|
|
95470
95538
|
if (typeof sessionId === "string") this.bySession.delete(sessionId);
|
|
95471
95539
|
}
|
|
95472
95540
|
};
|
|
95541
|
+
var FOCUS_WINDOW = 5;
|
|
95542
|
+
var FOCUS_ROOT_HEADER = "x-errata-focus-root";
|
|
95543
|
+
var FOCUS_QUORUM = 3;
|
|
95544
|
+
var FOCUS_TTL_MS = 6 * 60 * 6e4;
|
|
95545
|
+
var SessionFocus = class {
|
|
95546
|
+
constructor(ttlMs = FOCUS_TTL_MS, maxSessions = 500) {
|
|
95547
|
+
this.ttlMs = ttlMs;
|
|
95548
|
+
this.maxSessions = maxSessions;
|
|
95549
|
+
}
|
|
95550
|
+
bySession = /* @__PURE__ */ new Map();
|
|
95551
|
+
live(sessionId, now) {
|
|
95552
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95553
|
+
const s = this.bySession.get(sessionId);
|
|
95554
|
+
if (!s) return null;
|
|
95555
|
+
if (now - s.at > this.ttlMs) {
|
|
95556
|
+
this.bySession.delete(sessionId);
|
|
95557
|
+
return null;
|
|
95558
|
+
}
|
|
95559
|
+
return s;
|
|
95560
|
+
}
|
|
95561
|
+
touch(sessionId, s, now) {
|
|
95562
|
+
s.at = now;
|
|
95563
|
+
this.bySession.delete(sessionId);
|
|
95564
|
+
this.bySession.set(sessionId, s);
|
|
95565
|
+
while (this.bySession.size > this.maxSessions) {
|
|
95566
|
+
const oldest = this.bySession.keys().next().value;
|
|
95567
|
+
if (oldest === void 0) break;
|
|
95568
|
+
this.bySession.delete(oldest);
|
|
95569
|
+
}
|
|
95570
|
+
}
|
|
95571
|
+
/** The workspace this session's hooks should go to instead of its cwd's, if any. */
|
|
95572
|
+
focusFor(sessionId, now = Date.now()) {
|
|
95573
|
+
return this.live(sessionId, now)?.focus ?? null;
|
|
95574
|
+
}
|
|
95575
|
+
pinnedFor(sessionId, now = Date.now()) {
|
|
95576
|
+
return this.live(sessionId, now)?.pinned ?? false;
|
|
95577
|
+
}
|
|
95578
|
+
/** A tool hook's locator reached `root`; `cwdRoot` is where the session's cwd routes. */
|
|
95579
|
+
observe(sessionId, root, cwdRoot, now = Date.now()) {
|
|
95580
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95581
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95582
|
+
s.reached.add(root);
|
|
95583
|
+
s.recent.push(root);
|
|
95584
|
+
if (s.recent.length > FOCUS_WINDOW) s.recent.shift();
|
|
95585
|
+
this.touch(sessionId, s, now);
|
|
95586
|
+
if (s.pinned) return null;
|
|
95587
|
+
const current = s.focus ?? cwdRoot;
|
|
95588
|
+
if (root === current) return null;
|
|
95589
|
+
if (s.recent.filter((r) => r === root).length < FOCUS_QUORUM) return null;
|
|
95590
|
+
s.focus = root === cwdRoot ? null : root;
|
|
95591
|
+
return { from: current, to: root, reason: "inferred" };
|
|
95592
|
+
}
|
|
95593
|
+
pin(sessionId, root, cwdRoot, now = Date.now()) {
|
|
95594
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
95595
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95596
|
+
const current = s.focus ?? cwdRoot;
|
|
95597
|
+
s.pinned = true;
|
|
95598
|
+
s.focus = root;
|
|
95599
|
+
s.recent = [];
|
|
95600
|
+
this.touch(sessionId, s, now);
|
|
95601
|
+
return root === current ? null : { from: current, to: root, reason: "pinned" };
|
|
95602
|
+
}
|
|
95603
|
+
/** Drop the pin and any inferred focus: the session follows its cwd again. */
|
|
95604
|
+
unpin(sessionId, cwdRoot, now = Date.now()) {
|
|
95605
|
+
const s = this.live(sessionId, now);
|
|
95606
|
+
if (!s) return null;
|
|
95607
|
+
const current = s.focus ?? cwdRoot;
|
|
95608
|
+
s.pinned = false;
|
|
95609
|
+
s.focus = null;
|
|
95610
|
+
s.recent = [];
|
|
95611
|
+
this.touch(sessionId, s, now);
|
|
95612
|
+
return cwdRoot && current !== cwdRoot ? { from: current, to: cwdRoot, reason: "unpinned" } : null;
|
|
95613
|
+
}
|
|
95614
|
+
/** A tool ran in the session's cwd without naming a path (a shell command, a search). */
|
|
95615
|
+
ranIn(sessionId, root, now = Date.now()) {
|
|
95616
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return;
|
|
95617
|
+
const s = this.live(sessionId, now) ?? { recent: [], focus: null, pinned: false, at: now, reached: /* @__PURE__ */ new Set() };
|
|
95618
|
+
s.reached.add(root);
|
|
95619
|
+
this.touch(sessionId, s, now);
|
|
95620
|
+
}
|
|
95621
|
+
/** Whether the session's tools ever worked in `root`; undefined for a session this daemon has not watched. */
|
|
95622
|
+
reached(sessionId, root, now = Date.now()) {
|
|
95623
|
+
return this.live(sessionId, now)?.reached.has(root);
|
|
95624
|
+
}
|
|
95625
|
+
forget(sessionId) {
|
|
95626
|
+
if (typeof sessionId === "string") this.bySession.delete(sessionId);
|
|
95627
|
+
}
|
|
95628
|
+
/** Every live focus, for `errata focus` to confirm a pin landed. */
|
|
95629
|
+
entries(now = Date.now()) {
|
|
95630
|
+
const out2 = [];
|
|
95631
|
+
for (const [sessionId, s] of this.bySession) {
|
|
95632
|
+
if (now - s.at > this.ttlMs || !s.focus) continue;
|
|
95633
|
+
out2.push({ sessionId, root: s.focus, pinned: s.pinned, at: s.at });
|
|
95634
|
+
}
|
|
95635
|
+
return out2;
|
|
95636
|
+
}
|
|
95637
|
+
};
|
|
95638
|
+
function misfiledTarget(roots, home) {
|
|
95639
|
+
if (roots.includes(home)) return null;
|
|
95640
|
+
const tally = /* @__PURE__ */ new Map();
|
|
95641
|
+
for (const r of roots) tally.set(r, (tally.get(r) ?? 0) + 1);
|
|
95642
|
+
const ranked = [...tally].sort((a, b) => b[1] - a[1]);
|
|
95643
|
+
const [top, next] = ranked;
|
|
95644
|
+
if (!top || top[1] < FOCUS_QUORUM || next && next[1] === top[1]) return null;
|
|
95645
|
+
return top[0];
|
|
95646
|
+
}
|
|
95647
|
+
function focusCommandIn(command) {
|
|
95648
|
+
if (typeof command !== "string") return void 0;
|
|
95649
|
+
const m = /(?:^|[\s;&|(])errata(?:\.cmd|\.ps1)?\s+focus(?:\s+("[^"]+"|'[^']+'|[^\s;&|)]+))?/.exec(command);
|
|
95650
|
+
if (!m) return void 0;
|
|
95651
|
+
const arg = m[1]?.replace(/^["']|["']$/g, "");
|
|
95652
|
+
if (!arg) return void 0;
|
|
95653
|
+
if (arg === "--clear" || arg === "clear" || arg === "off") return { ref: null };
|
|
95654
|
+
return arg.startsWith("-") ? void 0 : { ref: arg };
|
|
95655
|
+
}
|
|
95656
|
+
function resolveWorkspaceRef(ref, entries, ownerRoot) {
|
|
95657
|
+
const byName = entries.filter((e) => e.name.toLowerCase() === ref.toLowerCase());
|
|
95658
|
+
if (byName.length === 1) return byName[0];
|
|
95659
|
+
if (byName.length > 1) return null;
|
|
95660
|
+
const root = ownerRoot(entries.map((e) => e.path), msysToWindowsPath(ref));
|
|
95661
|
+
return root ? entries.find((e) => e.path === root) ?? null : null;
|
|
95662
|
+
}
|
|
95663
|
+
|
|
95664
|
+
// src/rehome.ts
|
|
95665
|
+
init_src2();
|
|
95666
|
+
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
95667
|
+
function sourcesOf(n) {
|
|
95668
|
+
const s = n.attrs["sources"];
|
|
95669
|
+
return Array.isArray(s) ? s.filter((x) => typeof x === "string") : [];
|
|
95670
|
+
}
|
|
95671
|
+
function rehomeSessionNodes(from, to, sessionIds, opts) {
|
|
95672
|
+
const sessions = new Set(sessionIds);
|
|
95673
|
+
const now = opts.now ?? Date.now();
|
|
95674
|
+
const report = { moved: [], copied: [], vocabulary: [], edgesCarried: 0, edgesDropped: 0, cloudContributed: [] };
|
|
95675
|
+
const picked = /* @__PURE__ */ new Map();
|
|
95676
|
+
for (const row of from.selectNodes({ labels: SEMANTIC_NODE_LABELS, attrs: ["sources"] })) {
|
|
95677
|
+
const sources = sourcesOf(row);
|
|
95678
|
+
if (!sources.some((s) => sessions.has(s))) continue;
|
|
95679
|
+
const node2 = from.getNode(row.id);
|
|
95680
|
+
if (!node2) continue;
|
|
95681
|
+
picked.set(node2.id, node2);
|
|
95682
|
+
(sources.every((s) => sessions.has(s)) ? report.moved : report.copied).push(node2.id);
|
|
95683
|
+
if (node2.attrs["cloudNodeId"]) report.cloudContributed.push(node2.id);
|
|
95684
|
+
}
|
|
95685
|
+
const edges = /* @__PURE__ */ new Map();
|
|
95686
|
+
for (const id of picked.keys()) {
|
|
95687
|
+
for (const e of [...from.outEdges(id), ...from.inEdges(id)]) edges.set(e.id, e);
|
|
95688
|
+
}
|
|
95689
|
+
const vocabulary = /* @__PURE__ */ new Map();
|
|
95690
|
+
const carried = [];
|
|
95691
|
+
for (const e of edges.values()) {
|
|
95692
|
+
const other = picked.has(e.from) ? e.to : e.from;
|
|
95693
|
+
if (picked.has(other)) {
|
|
95694
|
+
carried.push(e);
|
|
95695
|
+
continue;
|
|
95696
|
+
}
|
|
95697
|
+
const node2 = from.getNode(other);
|
|
95698
|
+
if (!node2 || isCodeLabel(node2.label)) {
|
|
95699
|
+
report.edgesDropped++;
|
|
95700
|
+
continue;
|
|
95701
|
+
}
|
|
95702
|
+
if (!to.getNode(other)) vocabulary.set(other, node2);
|
|
95703
|
+
carried.push(e);
|
|
95704
|
+
}
|
|
95705
|
+
report.vocabulary = [...vocabulary.keys()];
|
|
95706
|
+
report.edgesCarried = carried.length;
|
|
95707
|
+
if (!opts.apply) return report;
|
|
95708
|
+
to.transaction(() => {
|
|
95709
|
+
for (const n of [...vocabulary.values(), ...picked.values()]) to.mergeNode(n);
|
|
95710
|
+
for (const e of carried) to.mergeEdge(e);
|
|
95711
|
+
});
|
|
95712
|
+
from.transaction(() => {
|
|
95713
|
+
for (const id of report.moved) {
|
|
95714
|
+
const n = picked.get(id);
|
|
95715
|
+
from.updateNode(id, { attrs: { ...n.attrs, rehomedAt: now } });
|
|
95716
|
+
from.closeNode(id, now);
|
|
95717
|
+
}
|
|
95718
|
+
});
|
|
95719
|
+
return report;
|
|
95720
|
+
}
|
|
95721
|
+
function captureWorkPaths(store, home) {
|
|
95722
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
95723
|
+
for (const row of store.selectNodes({ labels: ["Problem"], attrs: ["sources", "anchorHint"] })) {
|
|
95724
|
+
const sources = sourcesOf(row);
|
|
95725
|
+
if (sources.length === 0) continue;
|
|
95726
|
+
let where = null;
|
|
95727
|
+
if (store.outEdges(row.id, ["ANCHORED_AT"]).length > 0) where = home;
|
|
95728
|
+
else {
|
|
95729
|
+
const hint = row.attrs["anchorHint"];
|
|
95730
|
+
if (typeof hint?.path === "string" && hint.path.length > 0) where = isAbsolute6(hint.path) ? hint.path : home;
|
|
95731
|
+
}
|
|
95732
|
+
for (const s of sources) {
|
|
95733
|
+
const list = out2.get(s) ?? [];
|
|
95734
|
+
if (where) list.push(where);
|
|
95735
|
+
out2.set(s, list);
|
|
95736
|
+
}
|
|
95737
|
+
}
|
|
95738
|
+
return out2;
|
|
95739
|
+
}
|
|
95473
95740
|
|
|
95474
95741
|
// src/multi.ts
|
|
95475
95742
|
init_checkout_root();
|
|
@@ -95527,6 +95794,14 @@ function pickOwnerRoot(roots, absPath) {
|
|
|
95527
95794
|
}
|
|
95528
95795
|
return best;
|
|
95529
95796
|
}
|
|
95797
|
+
function withAdditionalContext(response, hookEventName, extra) {
|
|
95798
|
+
const out2 = response["hookSpecificOutput"] ?? {};
|
|
95799
|
+
const prior = typeof out2.additionalContext === "string" && out2.additionalContext ? `${out2.additionalContext}
|
|
95800
|
+
|
|
95801
|
+
` : "";
|
|
95802
|
+
const { ok: _ok, event: _event, tool: _tool, recognizedFile: _file2, ...rest2 } = response;
|
|
95803
|
+
return { ...rest2, hookSpecificOutput: { ...out2, hookEventName, additionalContext: `${prior}${extra}` } };
|
|
95804
|
+
}
|
|
95530
95805
|
function ownerOf(records, absPath) {
|
|
95531
95806
|
const root = pickOwnerRoot(records.map((r) => r.root), absPath);
|
|
95532
95807
|
return root == null ? null : records.find((r) => r.root === root) ?? null;
|
|
@@ -95933,6 +96208,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
95933
96208
|
const app = new Hono2();
|
|
95934
96209
|
const enqueueWakeIndex = serialQueue();
|
|
95935
96210
|
const attaching = /* @__PURE__ */ new Map();
|
|
96211
|
+
let afterAttach = () => {
|
|
96212
|
+
};
|
|
95936
96213
|
const attachWorkspace = (root) => {
|
|
95937
96214
|
const norm = normPath(root);
|
|
95938
96215
|
const pending = attaching.get(norm);
|
|
@@ -95982,6 +96259,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
95982
96259
|
});
|
|
95983
96260
|
}
|
|
95984
96261
|
console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
|
|
96262
|
+
afterAttach(rec);
|
|
95985
96263
|
return { attached: true, id: rec.id };
|
|
95986
96264
|
};
|
|
95987
96265
|
app.post("/api/attach", async (c) => {
|
|
@@ -96106,6 +96384,105 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96106
96384
|
})
|
|
96107
96385
|
);
|
|
96108
96386
|
const sessionAffinity = new SessionAffinity();
|
|
96387
|
+
const sessionFocus = new SessionFocus();
|
|
96388
|
+
const pendingSwitch = /* @__PURE__ */ new Map();
|
|
96389
|
+
const turnHome = /* @__PURE__ */ new Map();
|
|
96390
|
+
const registryRoots = () => listWorkspaceEntries().map((e) => e.path);
|
|
96391
|
+
const rootOfPath = (p, roots) => p ? ownerOf(records, p)?.root ?? pickOwnerRoot((roots ?? registryRoots)(), p) : null;
|
|
96392
|
+
const workspaceName = (root) => root ? listWorkspaceEntries().find((e) => e.path === root)?.name ?? basename10(root) : "its working directory";
|
|
96393
|
+
const recordFor = async (root) => {
|
|
96394
|
+
const running = records.find((r) => r.root === root);
|
|
96395
|
+
if (running) return running;
|
|
96396
|
+
return (await attachWorkspace(root)).attached ? records.find((r) => r.root === root) ?? null : null;
|
|
96397
|
+
};
|
|
96398
|
+
const focusNotice = (change, cwdRoot) => {
|
|
96399
|
+
const to = workspaceName(change.to);
|
|
96400
|
+
const from = workspaceName(change.from);
|
|
96401
|
+
const where = `Captures from here on go to ${to}'s graph, and the block below is ${to}'s context: it replaces any earlier errata block.`;
|
|
96402
|
+
if (change.reason === "pinned") return `errata: this session is now pinned to ${to} (\`errata focus\`). ${where} \`errata focus off\` returns it to ${workspaceName(cwdRoot)}.`;
|
|
96403
|
+
if (change.reason === "unpinned") return `errata: focus cleared; this session follows its working directory, ${to}, again. ${where}`;
|
|
96404
|
+
return `errata: this session's work moved from ${from} to ${to}: ${FOCUS_QUORUM} of the last ${FOCUS_WINDOW} paths your tools touched are in ${to}. ${where} Wrong workspace? Run \`errata focus ${from === "its working directory" ? "off" : from}\`.`;
|
|
96405
|
+
};
|
|
96406
|
+
const noteFocus = (sid, change) => {
|
|
96407
|
+
if (!change) return;
|
|
96408
|
+
pendingSwitch.set(sid, change);
|
|
96409
|
+
console.log(`[errata] session ${sid.slice(0, 8)} focus ${workspaceName(change.from)} \u2192 ${workspaceName(change.to)} (${change.reason})`);
|
|
96410
|
+
};
|
|
96411
|
+
const observeFocus = (sid, body2) => {
|
|
96412
|
+
const event = String(body2["hook_event_name"] ?? "");
|
|
96413
|
+
const cwd = typeof body2["cwd"] === "string" ? msysToWindowsPath(body2["cwd"]) : void 0;
|
|
96414
|
+
const cwdRoot = rootOfPath(cwd);
|
|
96415
|
+
const input = body2["tool_input"] ?? {};
|
|
96416
|
+
if (event === "PreToolUse") {
|
|
96417
|
+
const pin = focusCommandIn(input["command"]);
|
|
96418
|
+
if (!pin) return;
|
|
96419
|
+
if (pin.ref === null) return noteFocus(sid, sessionFocus.unpin(sid, cwdRoot));
|
|
96420
|
+
const entry = resolveWorkspaceRef(pin.ref, listWorkspaceEntries(), pickOwnerRoot);
|
|
96421
|
+
if (entry) noteFocus(sid, sessionFocus.pin(sid, entry.path, cwdRoot));
|
|
96422
|
+
return;
|
|
96423
|
+
}
|
|
96424
|
+
if (event !== "PostToolUse" && event !== "PostToolUseFailure") return;
|
|
96425
|
+
let registered = null;
|
|
96426
|
+
const roots = () => registered ??= registryRoots();
|
|
96427
|
+
const located = hookPathCandidates(body2).filter((p) => p !== cwd).map((p) => rootOfPath(p, roots)).find((r) => r != null);
|
|
96428
|
+
if (located) noteFocus(sid, sessionFocus.observe(sid, located, cwdRoot));
|
|
96429
|
+
else if (cwdRoot && (typeof input["command"] === "string" || ["Grep", "Glob", "LS"].includes(String(body2["tool_name"])))) {
|
|
96430
|
+
sessionFocus.ranIn(sid, cwdRoot);
|
|
96431
|
+
}
|
|
96432
|
+
};
|
|
96433
|
+
const repositoryOf = (root) => {
|
|
96434
|
+
const gitRoot = nearestGitRoot(root);
|
|
96435
|
+
return normPath((gitRoot ? mainCheckoutOfWorktree(gitRoot) : null) ?? root);
|
|
96436
|
+
};
|
|
96437
|
+
const evidenceWorkspace = (home) => {
|
|
96438
|
+
let registered = null;
|
|
96439
|
+
const known = () => registered ??= registryRoots();
|
|
96440
|
+
return (p) => {
|
|
96441
|
+
if (p === home) return p;
|
|
96442
|
+
const direct = rootOfPath(p, known);
|
|
96443
|
+
if (direct) return direct;
|
|
96444
|
+
const gitRoot = nearestGitRoot(p);
|
|
96445
|
+
const main2 = gitRoot ? mainCheckoutOfWorktree(gitRoot) : null;
|
|
96446
|
+
return main2 ? rootOfPath(main2, known) : null;
|
|
96447
|
+
};
|
|
96448
|
+
};
|
|
96449
|
+
const rehomeMisfiled = async (sid, from, to, why) => {
|
|
96450
|
+
if (repositoryOf(from.root) === repositoryOf(to.root)) return;
|
|
96451
|
+
await from.engine.settleTurns(sid);
|
|
96452
|
+
const workspaceOf = evidenceWorkspace(from.root);
|
|
96453
|
+
if ((captureWorkPaths(from.engine.store, from.root).get(sid) ?? []).some((p) => workspaceOf(p) === from.root)) return;
|
|
96454
|
+
const report = rehomeSessionNodes(from.engine.store, to.engine.store, [sid], { apply: true });
|
|
96455
|
+
if (report.moved.length + report.copied.length === 0) return;
|
|
96456
|
+
from.engine.markContextDirty();
|
|
96457
|
+
to.engine.markContextDirty();
|
|
96458
|
+
console.log(
|
|
96459
|
+
`[errata] session ${sid.slice(0, 8)} captures ${from.entry.name} \u2192 ${to.entry.name} (${why}): ${report.moved.length} moved, ${report.copied.length} copied, ${report.edgesDropped} code edges dropped` + (report.cloudContributed.length > 0 ? `, ${report.cloudContributed.length} already in the cloud under ${from.entry.name}` : "")
|
|
96460
|
+
);
|
|
96461
|
+
};
|
|
96462
|
+
const handOffTurns = (sid, owner, cwd) => {
|
|
96463
|
+
const mine = owner.engine.turnCursor(sid);
|
|
96464
|
+
let fromRoot = mine?.movedTo ?? turnHome.get(sid) ?? null;
|
|
96465
|
+
if (!fromRoot && !mine) fromRoot = rootOfPath(cwd);
|
|
96466
|
+
if (fromRoot && fromRoot !== owner.root) {
|
|
96467
|
+
const from = records.find((r) => r.root === fromRoot);
|
|
96468
|
+
const configDir = workspacePaths(fromRoot).configDir;
|
|
96469
|
+
const cursor = from ? from.engine.turnCursor(sid) : turnCursorOnDisk(configDir, sid);
|
|
96470
|
+
if (from) from.engine.releaseTurnCursor(sid, owner.root);
|
|
96471
|
+
else releaseTurnCursorOnDisk(configDir, sid, owner.root);
|
|
96472
|
+
owner.engine.adoptTurnCursor(sid, cursor?.movedTo ? null : cursor);
|
|
96473
|
+
console.log(`[errata] session ${sid.slice(0, 8)} turns: ${workspaceName(fromRoot)} \u2192 ${owner.entry.name}`);
|
|
96474
|
+
if (from && sessionFocus.focusFor(sid) === owner.root && sessionFocus.reached(sid, fromRoot) === false) {
|
|
96475
|
+
void rehomeMisfiled(sid, from, owner, "its tools never worked there").catch(
|
|
96476
|
+
(err2) => console.warn(`[errata] rehome of session ${sid.slice(0, 8)} failed: ${err2 instanceof Error ? err2.message : err2}`)
|
|
96477
|
+
);
|
|
96478
|
+
}
|
|
96479
|
+
} else if (mine?.movedTo) {
|
|
96480
|
+
owner.engine.adoptTurnCursor(sid, null);
|
|
96481
|
+
}
|
|
96482
|
+
turnHome.delete(sid);
|
|
96483
|
+
turnHome.set(sid, owner.root);
|
|
96484
|
+
if (turnHome.size > 500) turnHome.delete(turnHome.keys().next().value);
|
|
96485
|
+
};
|
|
96109
96486
|
const forward = async (c, pathInBody, opts2 = {}) => {
|
|
96110
96487
|
const raw2 = await c.req.text();
|
|
96111
96488
|
let body2 = {};
|
|
@@ -96115,9 +96492,13 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96115
96492
|
return c.json({ error: "bad-json" }, 400);
|
|
96116
96493
|
}
|
|
96117
96494
|
lastHook = { at: Date.now(), event: typeof body2["hook_event_name"] === "string" ? body2["hook_event_name"] : c.req.path };
|
|
96495
|
+
const sid = typeof body2["session_id"] === "string" ? body2["session_id"] : "";
|
|
96496
|
+
if (sid && opts2.affinity === "note") observeFocus(sid, body2);
|
|
96497
|
+
const focusRoot = sid && opts2.affinity === "follow" ? sessionFocus.focusFor(sid) : null;
|
|
96118
96498
|
const primary = pathInBody(body2);
|
|
96119
96499
|
const others = (opts2.extraPaths?.(body2) ?? []).filter((p) => p !== primary);
|
|
96120
|
-
let owner =
|
|
96500
|
+
let owner = focusRoot ? await recordFor(focusRoot) : null;
|
|
96501
|
+
if (!owner) owner = ownerOf(records, primary);
|
|
96121
96502
|
for (const p of others) {
|
|
96122
96503
|
if (owner) break;
|
|
96123
96504
|
owner = ownerOf(records, p);
|
|
@@ -96136,8 +96517,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96136
96517
|
}
|
|
96137
96518
|
}
|
|
96138
96519
|
if (!owner && others.length > 0) {
|
|
96139
|
-
const
|
|
96140
|
-
const registered = others.map((p) => pickOwnerRoot(
|
|
96520
|
+
const registryRoots2 = listWorkspaceEntries().map((e) => e.path);
|
|
96521
|
+
const registered = others.map((p) => pickOwnerRoot(registryRoots2, p)).find((r) => r != null);
|
|
96141
96522
|
if (registered && (await attachWorkspace(registered)).attached) {
|
|
96142
96523
|
owner = records.find((r) => r.root === registered) ?? null;
|
|
96143
96524
|
if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
|
|
@@ -96166,32 +96547,109 @@ async function startMultiDaemon(opts = {}) {
|
|
|
96166
96547
|
if (opts2.affinity === "note") sessionAffinity.note(body2["session_id"], owner.root);
|
|
96167
96548
|
owner.lastHookAt = Date.now();
|
|
96168
96549
|
const sub = c.req.path;
|
|
96550
|
+
const cwd = typeof body2["cwd"] === "string" ? msysToWindowsPath(body2["cwd"]) : void 0;
|
|
96551
|
+
if (sid && (sub === "/api/turn" || sub === "/api/session-end")) handOffTurns(sid, owner, cwd);
|
|
96552
|
+
const subagentHook = typeof body2["agent_id"] === "string" && body2["agent_id"] !== "";
|
|
96553
|
+
const pending = sid && !subagentHook ? pendingSwitch.get(sid) : void 0;
|
|
96554
|
+
let raw22 = raw2;
|
|
96555
|
+
if (pending && sub === "/api/context-inject" && pending.to === owner.root) {
|
|
96556
|
+
raw22 = JSON.stringify({ ...body2, [FOCUS_NOTICE_FIELD]: focusNotice(pending, rootOfPath(cwd)) });
|
|
96557
|
+
pendingSwitch.delete(sid);
|
|
96558
|
+
}
|
|
96169
96559
|
const tag = (v) => typeof v === "string" ? ` ${v.slice(0, 40)}` : "";
|
|
96170
96560
|
const done = markPass(`${sub}:${owner.entry.name}${tag(body2["hook_event_name"])}${tag(body2["tool_name"])}`);
|
|
96561
|
+
const post = async (target, path3, payload) => await target.webApp.fetch(
|
|
96562
|
+
new Request(`http://local${path3}`, {
|
|
96563
|
+
method: "POST",
|
|
96564
|
+
// The origin header (harness/fixture quarantine, session-registry.ts)
|
|
96565
|
+
// is the one caller header the sub-app needs; nothing else crosses.
|
|
96566
|
+
headers: {
|
|
96567
|
+
"content-type": "application/json",
|
|
96568
|
+
...c.req.header(ORIGIN_HEADER) ? { [ORIGIN_HEADER]: c.req.header(ORIGIN_HEADER) } : {}
|
|
96569
|
+
},
|
|
96570
|
+
body: payload
|
|
96571
|
+
})
|
|
96572
|
+
);
|
|
96573
|
+
const withFocusHeader = (res) => {
|
|
96574
|
+
const focusedRoot = sid ? sessionFocus.focusFor(sid) : null;
|
|
96575
|
+
if (!focusedRoot) return res;
|
|
96576
|
+
const headers = new Headers(res.headers);
|
|
96577
|
+
headers.set(FOCUS_ROOT_HEADER, focusedRoot);
|
|
96578
|
+
return new Response(res.body, { status: res.status, headers });
|
|
96579
|
+
};
|
|
96171
96580
|
try {
|
|
96172
|
-
|
|
96173
|
-
|
|
96174
|
-
|
|
96175
|
-
|
|
96176
|
-
|
|
96177
|
-
|
|
96178
|
-
|
|
96179
|
-
|
|
96180
|
-
|
|
96181
|
-
|
|
96182
|
-
|
|
96183
|
-
|
|
96581
|
+
const res = await post(owner, sub, raw22);
|
|
96582
|
+
if (sub === "/api/session-end" && sid) {
|
|
96583
|
+
sessionFocus.forget(sid);
|
|
96584
|
+
pendingSwitch.delete(sid);
|
|
96585
|
+
}
|
|
96586
|
+
const hookEvent = String(body2["hook_event_name"] ?? "");
|
|
96587
|
+
if (pending && pendingSwitch.has(sid) && sub === "/api/hook" && ["PreToolUse", "PostToolUse", "PostToolUseFailure"].includes(hookEvent)) {
|
|
96588
|
+
const target = await recordFor(pending.to);
|
|
96589
|
+
if (target) {
|
|
96590
|
+
pendingSwitch.delete(sid);
|
|
96591
|
+
const inject = await post(target, "/api/context-inject", JSON.stringify({
|
|
96592
|
+
session_id: sid,
|
|
96593
|
+
hook_event_name: hookEvent,
|
|
96594
|
+
cwd: target.root,
|
|
96595
|
+
...typeof body2["transcript_path"] === "string" ? { transcript_path: body2["transcript_path"] } : {},
|
|
96596
|
+
[FOCUS_NOTICE_FIELD]: focusNotice(pending, rootOfPath(cwd))
|
|
96597
|
+
}));
|
|
96598
|
+
const extra = (await inject.json().catch(() => ({}))).hookSpecificOutput?.additionalContext;
|
|
96599
|
+
if (extra) {
|
|
96600
|
+
const base = res.status === 204 ? {} : await res.json().catch(() => ({}));
|
|
96601
|
+
return withFocusHeader(c.json(withAdditionalContext(base, hookEvent, extra)));
|
|
96602
|
+
}
|
|
96603
|
+
}
|
|
96604
|
+
}
|
|
96605
|
+
return withFocusHeader(res);
|
|
96184
96606
|
} finally {
|
|
96185
96607
|
done();
|
|
96186
96608
|
}
|
|
96187
96609
|
};
|
|
96610
|
+
const MISFILED_SWEEP_DELAY_MS = 2 * 6e4;
|
|
96611
|
+
const sweepMisfiled = async (rec) => {
|
|
96612
|
+
const checkedPath = join47(workspacePaths(rec.root).configDir, "rehome-checked.json");
|
|
96613
|
+
let checked;
|
|
96614
|
+
try {
|
|
96615
|
+
checked = new Set(JSON.parse(readFileSync40(checkedPath, "utf8")));
|
|
96616
|
+
} catch {
|
|
96617
|
+
checked = /* @__PURE__ */ new Set();
|
|
96618
|
+
}
|
|
96619
|
+
const workspaceOf = evidenceWorkspace(rec.root);
|
|
96620
|
+
let changed = false;
|
|
96621
|
+
for (const [sid, paths] of captureWorkPaths(rec.engine.store, rec.root)) {
|
|
96622
|
+
if (stopping) break;
|
|
96623
|
+
if (checked.has(sid) || turnHome.has(sid) || sessionFocus.reached(sid, rec.root) !== void 0) continue;
|
|
96624
|
+
checked.add(sid);
|
|
96625
|
+
changed = true;
|
|
96626
|
+
const target = misfiledTarget(paths.map(workspaceOf).filter((r) => r != null), rec.root);
|
|
96627
|
+
const to = target ? await recordFor(target) : null;
|
|
96628
|
+
if (to) await rehomeMisfiled(sid, rec, to, "its captures were raised in its files");
|
|
96629
|
+
await new Promise((r) => setImmediate(r));
|
|
96630
|
+
}
|
|
96631
|
+
if (changed) writeFileSync29(checkedPath, JSON.stringify([...checked]), "utf8");
|
|
96632
|
+
};
|
|
96633
|
+
afterAttach = (rec) => {
|
|
96634
|
+
const t = setTimeout(() => {
|
|
96635
|
+
void sweepMisfiled(rec).catch(
|
|
96636
|
+
(err2) => console.warn(`[errata] misfiled-session check of ${rec.entry.name} failed: ${err2 instanceof Error ? err2.message : err2}`)
|
|
96637
|
+
);
|
|
96638
|
+
}, MISFILED_SWEEP_DELAY_MS);
|
|
96639
|
+
t.unref?.();
|
|
96640
|
+
};
|
|
96641
|
+
for (const rec of records) afterAttach(rec);
|
|
96642
|
+
app.get(
|
|
96643
|
+
"/api/focus",
|
|
96644
|
+
(c) => c.json({ sessions: sessionFocus.entries().map((f) => ({ ...f, name: workspaceName(f.root) })) })
|
|
96645
|
+
);
|
|
96188
96646
|
app.post(
|
|
96189
96647
|
"/api/hook",
|
|
96190
96648
|
(c) => forward(c, (b) => {
|
|
96191
96649
|
const input = b["tool_input"] ?? {};
|
|
96192
96650
|
const fp = input["file_path"];
|
|
96193
96651
|
const cwd = b["cwd"];
|
|
96194
|
-
return (typeof fp === "string" ? fp : void 0) ?? (typeof cwd === "string" ? cwd : void 0);
|
|
96652
|
+
return (typeof fp === "string" ? fp : void 0) ?? leadingCdTarget(input["command"]) ?? (typeof cwd === "string" ? cwd : void 0);
|
|
96195
96653
|
}, { extraPaths: (b) => hookPathCandidates(b), affinity: "note" })
|
|
96196
96654
|
);
|
|
96197
96655
|
app.post(
|
|
@@ -98368,6 +98826,9 @@ try {
|
|
|
98368
98826
|
// a changed snapshot can go out as only what changed (the daemon renders it).
|
|
98369
98827
|
const bodyPath = statePath && statePath.replace(/\.json$/, '.body.md');
|
|
98370
98828
|
let emittedBody = null;
|
|
98829
|
+
// Set when the daemon routes this session to another workspace (errata focus):
|
|
98830
|
+
// that workspace's block is the current one.
|
|
98831
|
+
let focusRoot = null;
|
|
98371
98832
|
const post = async (endpoint, relay = false) => {
|
|
98372
98833
|
try {
|
|
98373
98834
|
const response = await fetch('http://127.0.0.1:' + port + '/api/' + endpoint, {
|
|
@@ -98375,6 +98836,8 @@ try {
|
|
|
98375
98836
|
body: JSON.stringify(payload), signal: AbortSignal.timeout(2000),
|
|
98376
98837
|
});
|
|
98377
98838
|
if (!response.ok) return false;
|
|
98839
|
+
const focused = response.headers.get('x-errata-focus-root');
|
|
98840
|
+
if (focused) focusRoot = focused;
|
|
98378
98841
|
if (!relay) return true;
|
|
98379
98842
|
const result = await response.json();
|
|
98380
98843
|
const context = result.hookSpecificOutput?.additionalContext;
|
|
@@ -98395,17 +98858,20 @@ try {
|
|
|
98395
98858
|
}
|
|
98396
98859
|
if (workspaceRoot && ['SessionStart', 'UserPromptSubmit', 'PostToolUse'].includes(event)) {
|
|
98397
98860
|
let body = '';
|
|
98398
|
-
|
|
98861
|
+
const blockRoot = focusRoot || workspaceRoot;
|
|
98862
|
+
try { body = boundedRead(join(blockRoot, '.errata', 'context.md'), 128 * 1024).trim(); } catch {}
|
|
98399
98863
|
if (body) {
|
|
98400
98864
|
const digest = hash(body.replace(/\r\n?/g, '\n')).slice(0, 16);
|
|
98865
|
+
// A different workspace's block than the one last sent: a delta between them is noise.
|
|
98866
|
+
const switched = (previous.root || workspaceRoot) !== blockRoot;
|
|
98401
98867
|
// Receipt is local to this graph + session, survives daemon restarts, and
|
|
98402
98868
|
// records stdout emission, not a server's intention to send a response.
|
|
98403
|
-
if (event === 'SessionStart' || previous.hash !== digest) {
|
|
98869
|
+
if (event === 'SessionStart' || switched || previous.hash !== digest) {
|
|
98404
98870
|
const normalized = body.replace(/\r\n?/g, '\n');
|
|
98405
98871
|
// Every SessionStart (startup, resume, compact, clear) sends the whole
|
|
98406
98872
|
// block: the window may no longer hold the last one.
|
|
98407
98873
|
let delta = null;
|
|
98408
|
-
if (event !== 'SessionStart' && previous.hash && bodyPath) {
|
|
98874
|
+
if (event !== 'SessionStart' && !switched && previous.hash && bodyPath) {
|
|
98409
98875
|
let prior = '';
|
|
98410
98876
|
try { prior = boundedRead(bodyPath, 512 * 1024); } catch {}
|
|
98411
98877
|
if (prior && hash(prior).slice(0, 16) === previous.hash) {
|
|
@@ -98423,7 +98889,7 @@ try {
|
|
|
98423
98889
|
// Only bookkeeping moved: nothing to send, but this snapshot is now what the window holds.
|
|
98424
98890
|
try {
|
|
98425
98891
|
mkdirSync(stateDir, { recursive: true });
|
|
98426
|
-
writeFileSync(statePath, JSON.stringify({ hash: digest, at: Date.now() }), { mode: 0o600 });
|
|
98892
|
+
writeFileSync(statePath, JSON.stringify({ hash: digest, at: Date.now(), root: blockRoot }), { mode: 0o600 });
|
|
98427
98893
|
writeFileSync(bodyPath, normalized, { mode: 0o600 });
|
|
98428
98894
|
} catch { /* the next change just goes whole */ }
|
|
98429
98895
|
} else {
|
|
@@ -98432,11 +98898,11 @@ try {
|
|
|
98432
98898
|
: 'Errata loaded the maintained local workspace context. This block REPLACES any earlier errata context block in this conversation. It is the latest saved snapshot; the daemon maintains it as work proceeds.\n\n' + body);
|
|
98433
98899
|
if (!/^(0|off|false|no)$/i.test((process.env.ERRATA_RECALL_CARD || '').trim())) {
|
|
98434
98900
|
try {
|
|
98435
|
-
const card = JSON.parse(boundedRead(join(
|
|
98901
|
+
const card = JSON.parse(boundedRead(join(blockRoot, '.errata', 'context-card.json'), 128 * 1024));
|
|
98436
98902
|
if (card.hash === digest && typeof card.line === 'string' && card.line) messages.push(card.line);
|
|
98437
98903
|
} catch { /* Never show a card for a different snapshot. */ }
|
|
98438
98904
|
}
|
|
98439
|
-
receipt = { hash: digest, at: Date.now() };
|
|
98905
|
+
receipt = { hash: digest, at: Date.now(), root: blockRoot };
|
|
98440
98906
|
emittedBody = normalized;
|
|
98441
98907
|
}
|
|
98442
98908
|
}
|
|
@@ -99962,6 +100428,10 @@ async function main() {
|
|
|
99962
100428
|
return cmdUpdate(rest);
|
|
99963
100429
|
case "projects":
|
|
99964
100430
|
return cmdProjects();
|
|
100431
|
+
case "focus":
|
|
100432
|
+
return cmdFocus(rest);
|
|
100433
|
+
case "rehome":
|
|
100434
|
+
return cmdRehome(rest);
|
|
99965
100435
|
case "dash":
|
|
99966
100436
|
return cmdDash(rest);
|
|
99967
100437
|
case "notify-test":
|
|
@@ -99985,6 +100455,14 @@ function printHelp() {
|
|
|
99985
100455
|
Commands:
|
|
99986
100456
|
init Auto-detect this workspace's profile, write .errata/workspace.json
|
|
99987
100457
|
projects List every initialized project + whether its daemon is running
|
|
100458
|
+
focus <workspace|off>
|
|
100459
|
+
Run from an agent's shell tool: that session's captures and
|
|
100460
|
+
context follow the named workspace (name or path). Sessions
|
|
100461
|
+
also follow on their own once most of the paths their tools
|
|
100462
|
+
touch are in another workspace; this pins it.
|
|
100463
|
+
rehome --session <id> --from <ws> --to <ws> [--apply]
|
|
100464
|
+
Move what a session captured into the workspace its work was
|
|
100465
|
+
about (a session started in the wrong repo). Dry run by default.
|
|
99988
100466
|
dash [--port N] [--no-reindex] [--embed]
|
|
99989
100467
|
One daemon watching ALL initialized projects, routing hooks
|
|
99990
100468
|
to each project's graph (default port 7891). JSON endpoints
|
|
@@ -103046,6 +103524,82 @@ async function cmdProjects() {
|
|
|
103046
103524
|
console.log(` ${w.path}`);
|
|
103047
103525
|
}
|
|
103048
103526
|
}
|
|
103527
|
+
async function cmdFocus(args2) {
|
|
103528
|
+
const entries = listWorkspaceEntries();
|
|
103529
|
+
const ref = args2.find((a) => !a.startsWith("-"));
|
|
103530
|
+
const clear = args2.includes("--clear") || ref === "off" || ref === "clear";
|
|
103531
|
+
const port = resolveHookPort();
|
|
103532
|
+
const focused = async () => {
|
|
103533
|
+
try {
|
|
103534
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/focus`, { signal: AbortSignal.timeout(2e3) });
|
|
103535
|
+
return r.ok ? (await r.json()).sessions : [];
|
|
103536
|
+
} catch {
|
|
103537
|
+
return [];
|
|
103538
|
+
}
|
|
103539
|
+
};
|
|
103540
|
+
if (!ref && !clear) {
|
|
103541
|
+
const now = await focused();
|
|
103542
|
+
console.log("usage: errata focus <workspace name or path> | errata focus off");
|
|
103543
|
+
console.log("Run it from an agent's shell tool: the session that runs it follows that workspace.\n");
|
|
103544
|
+
if (now.length === 0) console.log("No session is focused away from its working directory.");
|
|
103545
|
+
for (const f of now) console.log(` ${f.sessionId.slice(0, 8)} ${f.pinned ? "pinned to" : "following"} ${f.name} (${f.root})`);
|
|
103546
|
+
return;
|
|
103547
|
+
}
|
|
103548
|
+
if (clear) {
|
|
103549
|
+
console.log("focus cleared for the agent session that ran this: it follows its working directory again.");
|
|
103550
|
+
return;
|
|
103551
|
+
}
|
|
103552
|
+
const entry = resolveWorkspaceRef(ref, entries, pickOwnerRoot);
|
|
103553
|
+
if (!entry) {
|
|
103554
|
+
const same = entries.filter((e) => e.name.toLowerCase() === ref.toLowerCase());
|
|
103555
|
+
console.error(same.length > 1 ? `"${ref}" names ${same.length} workspaces; use a path instead:
|
|
103556
|
+
${same.map((e) => ` ${e.path}`).join("\n")}` : `no workspace named or containing "${ref}". Known workspaces:
|
|
103557
|
+
${entries.map((e) => ` ${e.name.padEnd(24)} ${e.path}`).join("\n")}`);
|
|
103558
|
+
process.exit(2);
|
|
103559
|
+
}
|
|
103560
|
+
const pinned = (await focused()).filter((f) => f.pinned && f.root === entry.path && Date.now() - f.at < 3e4);
|
|
103561
|
+
if (pinned.length === 0) {
|
|
103562
|
+
console.error(`${entry.name} (${entry.path}) is a workspace, but no agent session was pinned to it.`);
|
|
103563
|
+
console.error("Run this from an agent's shell tool, with the errata daemon running: the daemon pins the session whose hook reports the command.");
|
|
103564
|
+
process.exit(1);
|
|
103565
|
+
}
|
|
103566
|
+
console.log(`focus: ${entry.name} (${entry.path}), pinned for session ${pinned.map((f) => f.sessionId.slice(0, 8)).join(", ")}.`);
|
|
103567
|
+
console.log("Captures and context now come from this workspace. `errata focus off` returns the session to its working directory.");
|
|
103568
|
+
}
|
|
103569
|
+
async function cmdRehome(args2) {
|
|
103570
|
+
const values = (flag) => args2.flatMap((a, i2) => a === flag && args2[i2 + 1] ? [args2[i2 + 1]] : []);
|
|
103571
|
+
const sessions = values("--session");
|
|
103572
|
+
const [fromRef] = values("--from");
|
|
103573
|
+
const [toRef] = values("--to");
|
|
103574
|
+
const apply = args2.includes("--apply");
|
|
103575
|
+
if (sessions.length === 0 || !fromRef || !toRef) {
|
|
103576
|
+
console.error("usage: errata rehome --session <id> [--session <id> \u2026] --from <workspace> --to <workspace> [--apply]");
|
|
103577
|
+
process.exit(2);
|
|
103578
|
+
}
|
|
103579
|
+
const entries = listWorkspaceEntries();
|
|
103580
|
+
const from = resolveWorkspaceRef(fromRef, entries, pickOwnerRoot);
|
|
103581
|
+
const to = resolveWorkspaceRef(toRef, entries, pickOwnerRoot);
|
|
103582
|
+
if (!from || !to || from.path === to.path) {
|
|
103583
|
+
console.error(`--from and --to must name two different workspaces (got ${from?.name ?? fromRef} \u2192 ${to?.name ?? toRef}).`);
|
|
103584
|
+
process.exit(2);
|
|
103585
|
+
}
|
|
103586
|
+
const { openGraphStore: openGraphStore2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
|
|
103587
|
+
const src = openGraphStore2({ path: workspacePaths(from.path).castalia });
|
|
103588
|
+
const dst = openGraphStore2({ path: workspacePaths(to.path).castalia });
|
|
103589
|
+
try {
|
|
103590
|
+
const r = rehomeSessionNodes(src, dst, sessions, { apply });
|
|
103591
|
+
const verb = apply ? "" : "would ";
|
|
103592
|
+
console.log(`${from.name} \u2192 ${to.name}${apply ? "" : " (dry run; --apply to write)"}`);
|
|
103593
|
+
console.log(` ${verb}move ${r.moved.length} node(s) (closed in ${from.name}), ${verb}copy ${r.copied.length} shared with other sessions`);
|
|
103594
|
+
console.log(` ${verb}carry ${r.edgesCarried} edge(s), ${verb}add ${r.vocabulary.length} vocabulary node(s) to ${to.name}; ${r.edgesDropped} edge(s) into ${from.name}'s code dropped`);
|
|
103595
|
+
if (r.cloudContributed.length > 0) {
|
|
103596
|
+
console.log(` ${r.cloudContributed.length} of them were already contributed to the cloud under ${from.name}'s project; those cloud copies are not moved.`);
|
|
103597
|
+
}
|
|
103598
|
+
} finally {
|
|
103599
|
+
src.close();
|
|
103600
|
+
dst.close();
|
|
103601
|
+
}
|
|
103602
|
+
}
|
|
103049
103603
|
async function cmdNotifications(state) {
|
|
103050
103604
|
const s = state.toLowerCase();
|
|
103051
103605
|
if (s !== "on" && s !== "off") {
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
65e39d2985c41acb67f59e3b44bd9eeff584addcae3c7cf6db9006fa89c0bea8 .codex-plugin/plugin.json
|
|
2
2
|
a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
|
|
3
3
|
fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
|
|
4
4
|
73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js
|