@inerrata-corporation/errata 2.0.2-dev.1218 → 2.0.2-dev.1255
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 +305 -6
- package/package.json +1 -1
package/errata.mjs
CHANGED
|
@@ -53370,6 +53370,7 @@ function parseDesignResolutions(text) {
|
|
|
53370
53370
|
}
|
|
53371
53371
|
return out2;
|
|
53372
53372
|
}
|
|
53373
|
+
var OPENCODE_TRANSCRIPT_HEADER_TYPE = "errata-opencode-session";
|
|
53373
53374
|
var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
53374
53375
|
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
53375
53376
|
function isUserTurnBoundary(obj) {
|
|
@@ -53421,6 +53422,9 @@ function detectHostHarness(raw2) {
|
|
|
53421
53422
|
if (obj.type === "gemini" || "$set" in obj || "projectHash" in obj && "sessionId" in obj || obj.type === "user" && !("message" in obj) && "content" in obj) {
|
|
53422
53423
|
return "gemini";
|
|
53423
53424
|
}
|
|
53425
|
+
if (obj.type === OPENCODE_TRANSCRIPT_HEADER_TYPE || obj.type === void 0 && "info" in obj && Array.isArray(obj["parts"]) && typeof obj["info"]?.["role"] === "string") {
|
|
53426
|
+
return "opencode";
|
|
53427
|
+
}
|
|
53424
53428
|
if (obj.type === "user" || obj.type === "assistant" || obj.type === "summary") {
|
|
53425
53429
|
return "claude_code";
|
|
53426
53430
|
}
|
|
@@ -53432,8 +53436,44 @@ function parseAssistantTurns(raw2, includeThinking, skipFirstLineParseFailure =
|
|
|
53432
53436
|
const harness = detectHostHarness(raw2);
|
|
53433
53437
|
if (harness === "codex") return parseCodexAssistantTurns(raw2, skipFirstLineParseFailure);
|
|
53434
53438
|
if (harness === "gemini") return parseGeminiAssistantTurns(raw2, skipFirstLineParseFailure);
|
|
53439
|
+
if (harness === "opencode") return parseOpencodeAssistantTurns(raw2, skipFirstLineParseFailure);
|
|
53435
53440
|
return parseClaudeAssistantTurns(raw2, includeThinking, skipFirstLineParseFailure);
|
|
53436
53441
|
}
|
|
53442
|
+
function parseOpencodeAssistantTurns(raw2, skipFirstLineParseFailure = false) {
|
|
53443
|
+
const lines = raw2.split(/\r?\n/);
|
|
53444
|
+
const byId = /* @__PURE__ */ new Map();
|
|
53445
|
+
let order = 0;
|
|
53446
|
+
let parseFailures = 0;
|
|
53447
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
53448
|
+
const line = lines[i2];
|
|
53449
|
+
if (!line) continue;
|
|
53450
|
+
let obj;
|
|
53451
|
+
try {
|
|
53452
|
+
obj = JSON.parse(line);
|
|
53453
|
+
} catch {
|
|
53454
|
+
if (!(i2 === 0 && skipFirstLineParseFailure)) parseFailures++;
|
|
53455
|
+
continue;
|
|
53456
|
+
}
|
|
53457
|
+
if (!obj || typeof obj !== "object") continue;
|
|
53458
|
+
if (obj["type"] === OPENCODE_TRANSCRIPT_HEADER_TYPE) continue;
|
|
53459
|
+
const info2 = obj["info"];
|
|
53460
|
+
const parts2 = obj["parts"];
|
|
53461
|
+
if (!info2 || typeof info2 !== "object" || !Array.isArray(parts2)) continue;
|
|
53462
|
+
if (info2["role"] !== "assistant") continue;
|
|
53463
|
+
const texts = [];
|
|
53464
|
+
for (const p of parts2) {
|
|
53465
|
+
if (!p || p["type"] !== "text") continue;
|
|
53466
|
+
if (p["synthetic"] === true || p["ignored"] === true) continue;
|
|
53467
|
+
if (typeof p["text"] === "string" && p["text"].length > 0) texts.push(p["text"]);
|
|
53468
|
+
}
|
|
53469
|
+
if (texts.length === 0) continue;
|
|
53470
|
+
const id = typeof info2["id"] === "string" ? info2["id"] : `opencode-${i2}`;
|
|
53471
|
+
const prev = byId.get(id);
|
|
53472
|
+
byId.set(id, { text: texts.join("\n\n"), order: prev ? prev.order : order++ });
|
|
53473
|
+
}
|
|
53474
|
+
const turns = [...byId.entries()].sort((a, b) => a[1].order - b[1].order).map(([uuid3, v]) => ({ uuid: uuid3, text: v.text, hostHarness: "opencode" }));
|
|
53475
|
+
return { turns, parseFailures };
|
|
53476
|
+
}
|
|
53437
53477
|
function parseGeminiAssistantTurns(raw2, skipFirstLineParseFailure = false) {
|
|
53438
53478
|
const lines = raw2.split(/\r?\n/);
|
|
53439
53479
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -53805,6 +53845,71 @@ function geminiTranscriptsForCwd(cwd, opts = {}) {
|
|
|
53805
53845
|
out2.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
53806
53846
|
return out2.slice(0, limit);
|
|
53807
53847
|
}
|
|
53848
|
+
function opencodeSessionRoots(env2 = process.env, home = homedir4()) {
|
|
53849
|
+
const roots = /* @__PURE__ */ new Set();
|
|
53850
|
+
const explicit = env2["ERRATA_OPENCODE_SESSIONS_DIRS"];
|
|
53851
|
+
if (explicit) {
|
|
53852
|
+
for (const d of explicit.split(":")) if (d.trim()) roots.add(d.trim());
|
|
53853
|
+
}
|
|
53854
|
+
if (env2["XDG_DATA_HOME"]) roots.add(join16(env2["XDG_DATA_HOME"], "opencode", "errata"));
|
|
53855
|
+
roots.add(join16(home, ".local", "share", "opencode", "errata"));
|
|
53856
|
+
return [...roots];
|
|
53857
|
+
}
|
|
53858
|
+
function readOpencodeTranscriptDirectory(path2) {
|
|
53859
|
+
let fd;
|
|
53860
|
+
try {
|
|
53861
|
+
fd = openSync(path2, "r");
|
|
53862
|
+
const buf = Buffer.allocUnsafe(8192);
|
|
53863
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
53864
|
+
const nl = buf.indexOf(10);
|
|
53865
|
+
const first = buf.toString("utf8", 0, nl >= 0 && nl < n ? nl : n);
|
|
53866
|
+
const o = JSON.parse(first);
|
|
53867
|
+
if (o?.type !== OPENCODE_TRANSCRIPT_HEADER_TYPE) return null;
|
|
53868
|
+
return typeof o.directory === "string" ? o.directory : null;
|
|
53869
|
+
} catch {
|
|
53870
|
+
return null;
|
|
53871
|
+
} finally {
|
|
53872
|
+
if (fd !== void 0) try {
|
|
53873
|
+
closeSync(fd);
|
|
53874
|
+
} catch {
|
|
53875
|
+
}
|
|
53876
|
+
}
|
|
53877
|
+
}
|
|
53878
|
+
function opencodeTranscriptsForCwd(cwd, opts = {}) {
|
|
53879
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
53880
|
+
const limit = opts.limit ?? 25;
|
|
53881
|
+
const want = /* @__PURE__ */ new Set([cwd, cwd.replace(/\/+$/, "")]);
|
|
53882
|
+
try {
|
|
53883
|
+
want.add(realpathSync(cwd));
|
|
53884
|
+
} catch {
|
|
53885
|
+
}
|
|
53886
|
+
const out2 = [];
|
|
53887
|
+
for (const root of opencodeSessionRoots(opts.env, opts.home)) {
|
|
53888
|
+
if (!existsSync13(root)) continue;
|
|
53889
|
+
let names;
|
|
53890
|
+
try {
|
|
53891
|
+
names = readdirSync6(root);
|
|
53892
|
+
} catch {
|
|
53893
|
+
continue;
|
|
53894
|
+
}
|
|
53895
|
+
for (const name2 of names) {
|
|
53896
|
+
if (!name2.endsWith(".jsonl")) continue;
|
|
53897
|
+
const full = join16(root, name2);
|
|
53898
|
+
let st;
|
|
53899
|
+
try {
|
|
53900
|
+
st = statSync4(full);
|
|
53901
|
+
} catch {
|
|
53902
|
+
continue;
|
|
53903
|
+
}
|
|
53904
|
+
if (!st.isFile() || st.mtimeMs < sinceMs) continue;
|
|
53905
|
+
const dir = readOpencodeTranscriptDirectory(full);
|
|
53906
|
+
if (!dir || !(want.has(dir) || want.has(dir.replace(/\/+$/, "")))) continue;
|
|
53907
|
+
out2.push({ path: full, sessionId: `opencode:${basename3(name2, ".jsonl")}`, mtimeMs: st.mtimeMs });
|
|
53908
|
+
}
|
|
53909
|
+
}
|
|
53910
|
+
out2.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
53911
|
+
return out2.slice(0, limit);
|
|
53912
|
+
}
|
|
53808
53913
|
|
|
53809
53914
|
// src/prior-tags.ts
|
|
53810
53915
|
init_src();
|
|
@@ -57204,7 +57309,7 @@ function createWatchBreaker(deps) {
|
|
|
57204
57309
|
}
|
|
57205
57310
|
|
|
57206
57311
|
// src/engine.ts
|
|
57207
|
-
var DAEMON_VERSION = true ? "2.0.2-dev.
|
|
57312
|
+
var DAEMON_VERSION = true ? "2.0.2-dev.1255" : "2.0.0-alpha.0";
|
|
57208
57313
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
57209
57314
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
57210
57315
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -58855,6 +58960,14 @@ function createWorkspaceEngine(opts) {
|
|
|
58855
58960
|
await yieldToLoop();
|
|
58856
58961
|
await harvestSession(ref.sessionId, ref.path);
|
|
58857
58962
|
}
|
|
58963
|
+
const opencodeRefs = opencodeTranscriptsForCwd(opts.workspaceRoot, {
|
|
58964
|
+
sinceMs: Date.now() - TURN_REPLAY_LOOKBACK_MS,
|
|
58965
|
+
limit: 25
|
|
58966
|
+
});
|
|
58967
|
+
for (const ref of opencodeRefs) {
|
|
58968
|
+
await yieldToLoop();
|
|
58969
|
+
await harvestSession(ref.sessionId, ref.path);
|
|
58970
|
+
}
|
|
58858
58971
|
} catch {
|
|
58859
58972
|
}
|
|
58860
58973
|
})();
|
|
@@ -58866,7 +58979,8 @@ function createWorkspaceEngine(opts) {
|
|
|
58866
58979
|
const since = Date.now() - CODEX_SWEEP_LOOKBACK_MS;
|
|
58867
58980
|
const refs = [
|
|
58868
58981
|
...codexRolloutsForCwd(opts.workspaceRoot, { sinceMs: since, limit: 10 }),
|
|
58869
|
-
...geminiTranscriptsForCwd(opts.workspaceRoot, { sinceMs: since, limit: 10 })
|
|
58982
|
+
...geminiTranscriptsForCwd(opts.workspaceRoot, { sinceMs: since, limit: 10 }),
|
|
58983
|
+
...opencodeTranscriptsForCwd(opts.workspaceRoot, { sinceMs: since, limit: 10 })
|
|
58870
58984
|
];
|
|
58871
58985
|
for (const ref of refs) {
|
|
58872
58986
|
await yieldToLoop();
|
|
@@ -60803,7 +60917,7 @@ var CODEX_WAKE_LOOKBACK_MS = 10 * 6e4;
|
|
|
60803
60917
|
var CODEX_WAKE_BOOT_LOOKBACK_MS = 24 * 60 * 6e4;
|
|
60804
60918
|
function codexWakeCandidates(input) {
|
|
60805
60919
|
if (input.disabled) return [];
|
|
60806
|
-
const find = input.find ?? ((cwd, since) => codexRolloutsForCwd(cwd, { sinceMs: since, limit: 1 }).length + geminiTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length);
|
|
60920
|
+
const find = input.find ?? ((cwd, since) => codexRolloutsForCwd(cwd, { sinceMs: since, limit: 1 }).length + geminiTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length + opencodeTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length);
|
|
60807
60921
|
const out2 = [];
|
|
60808
60922
|
for (const entry of input.entries) {
|
|
60809
60923
|
if (input.liveRoots.has(input.normPath(entry.path))) continue;
|
|
@@ -61377,7 +61491,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
61377
61491
|
});
|
|
61378
61492
|
for (const c of candidates) {
|
|
61379
61493
|
if (attachWorkspace(c.path).attached) {
|
|
61380
|
-
console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini transcript (headless capture)`);
|
|
61494
|
+
console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
|
|
61381
61495
|
}
|
|
61382
61496
|
}
|
|
61383
61497
|
};
|
|
@@ -62924,6 +63038,145 @@ ${CODEX_HOOKS_END}
|
|
|
62924
63038
|
`;
|
|
62925
63039
|
}
|
|
62926
63040
|
|
|
63041
|
+
// src/opencode-plugin.ts
|
|
63042
|
+
var OPENCODE_PLUGIN_MARKER = "// @inerrata opencode plugin \u2014 errata-managed (errata install-hooks opencode)";
|
|
63043
|
+
var OPENCODE_D1_CONTRACT = "Behavioral contract: (1) On ANY error, call search() BEFORE debugging \u2014 costs ~400 tokens vs 5,000\u201350,000 cold. (2) After solving non-trivial problems, contribute back: if your harness emits typed nodes (canonicalId + label + attrs), use ingest \u2014 the typed door; otherwise call contribute(), the prose fallback. (3) Before web search, try search() first \u2014 graph is 2\u201315\xD7 cheaper. (4) At session end, review what you solved and contribute anything novel.";
|
|
63044
|
+
var OPENCODE_TOOL_FAILURE_NUDGE = "[inerrata] This tool call failed \u2014 call search() with the error message BEFORE debugging from scratch (~400 tokens vs 5,000\u201350,000 cold).";
|
|
63045
|
+
function buildOpencodePluginSource(port, opts = {}) {
|
|
63046
|
+
const version2 = opts.version ?? "dev";
|
|
63047
|
+
return `${OPENCODE_PLUGIN_MARKER}
|
|
63048
|
+
// Generated by errata ${version2}. Re-run \`errata install-hooks opencode\` to update; edits here are overwritten.
|
|
63049
|
+
// What it does: mirrors each OpenCode session's OWN message record to a JSONL
|
|
63050
|
+
// file the errata daemon can parse, and tells the daemon on every turn boundary.
|
|
63051
|
+
// It has no dependencies and never throws into OpenCode.
|
|
63052
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
63053
|
+
import { join } from "node:path";
|
|
63054
|
+
import { homedir } from "node:os";
|
|
63055
|
+
|
|
63056
|
+
const DAEMON = "http://127.0.0.1:${port}";
|
|
63057
|
+
const HEADER_TYPE = "errata-opencode-session";
|
|
63058
|
+
const D1_CONTRACT = ${JSON.stringify(OPENCODE_D1_CONTRACT)};
|
|
63059
|
+
const TOOL_FAILURE_NUDGE = ${JSON.stringify(OPENCODE_TOOL_FAILURE_NUDGE)};
|
|
63060
|
+
|
|
63061
|
+
function mirrorRoot() {
|
|
63062
|
+
const explicit = process.env.ERRATA_OPENCODE_SESSIONS_DIRS;
|
|
63063
|
+
if (explicit) { const first = explicit.split(":").map((s) => s.trim()).find(Boolean); if (first) return first; }
|
|
63064
|
+
if (process.env.XDG_DATA_HOME) return join(process.env.XDG_DATA_HOME, "opencode", "errata");
|
|
63065
|
+
return join(homedir(), ".local", "share", "opencode", "errata");
|
|
63066
|
+
}
|
|
63067
|
+
|
|
63068
|
+
async function post(path, body) {
|
|
63069
|
+
try {
|
|
63070
|
+
const res = await fetch(DAEMON + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) });
|
|
63071
|
+
return res.status;
|
|
63072
|
+
} catch { return 0; }
|
|
63073
|
+
}
|
|
63074
|
+
|
|
63075
|
+
export const InerrataPlugin = async (input) => {
|
|
63076
|
+
const directory = input.directory;
|
|
63077
|
+
const projectID = input.project && input.project.id ? input.project.id : null;
|
|
63078
|
+
const root = mirrorRoot();
|
|
63079
|
+
// per-session state: which message ids are already in the mirror file
|
|
63080
|
+
const written = new Map(); // sessionID -> Set<messageId>
|
|
63081
|
+
const files = new Map(); // sessionID -> path
|
|
63082
|
+
|
|
63083
|
+
function fileFor(sessionID) {
|
|
63084
|
+
let p = files.get(sessionID);
|
|
63085
|
+
if (p) return p;
|
|
63086
|
+
try { mkdirSync(root, { recursive: true }); } catch {}
|
|
63087
|
+
p = join(root, sessionID + ".jsonl");
|
|
63088
|
+
files.set(sessionID, p);
|
|
63089
|
+
const seen = new Set();
|
|
63090
|
+
if (existsSync(p)) {
|
|
63091
|
+
// resumed session / plugin re-instantiated: learn what is already mirrored
|
|
63092
|
+
try {
|
|
63093
|
+
for (const line of readFileSync(p, "utf8").split("\\n")) {
|
|
63094
|
+
if (!line) continue;
|
|
63095
|
+
try { const o = JSON.parse(line); if (o && o.info && typeof o.info.id === "string") seen.add(o.info.id); } catch {}
|
|
63096
|
+
}
|
|
63097
|
+
} catch {}
|
|
63098
|
+
} else {
|
|
63099
|
+
appendFileSync(p, JSON.stringify({ type: HEADER_TYPE, version: 1, sessionID, directory, projectID, worktree: input.worktree || null, writtenAt: Date.now() }) + "\\n");
|
|
63100
|
+
}
|
|
63101
|
+
written.set(sessionID, seen);
|
|
63102
|
+
return p;
|
|
63103
|
+
}
|
|
63104
|
+
|
|
63105
|
+
async function mirror(sessionID) {
|
|
63106
|
+
// OpenCode's own record: session.messages() items are {info: Message, parts: Part[]}
|
|
63107
|
+
// (the same objects \`opencode export\` prints). Append only completed assistant
|
|
63108
|
+
// messages + every user message not yet mirrored; ids never repeat.
|
|
63109
|
+
const p = fileFor(sessionID);
|
|
63110
|
+
const seen = written.get(sessionID);
|
|
63111
|
+
let items = [];
|
|
63112
|
+
try {
|
|
63113
|
+
const r = await input.client.session.messages({ path: { id: sessionID } });
|
|
63114
|
+
items = (r && r.data) ? r.data : (Array.isArray(r) ? r : []);
|
|
63115
|
+
} catch { return { path: p, appended: 0 }; }
|
|
63116
|
+
let appended = 0;
|
|
63117
|
+
for (const item of items) {
|
|
63118
|
+
const info = item && item.info;
|
|
63119
|
+
if (!info || typeof info.id !== "string" || seen.has(info.id)) continue;
|
|
63120
|
+
if (info.role === "assistant" && !(info.time && info.time.completed)) continue; // still streaming
|
|
63121
|
+
try {
|
|
63122
|
+
appendFileSync(p, JSON.stringify({ info, parts: Array.isArray(item.parts) ? item.parts : [] }) + "\\n");
|
|
63123
|
+
seen.add(info.id); appended++;
|
|
63124
|
+
} catch {}
|
|
63125
|
+
}
|
|
63126
|
+
return { path: p, appended };
|
|
63127
|
+
}
|
|
63128
|
+
|
|
63129
|
+
const touched = new Set();
|
|
63130
|
+
return {
|
|
63131
|
+
// D1's text reaches OpenCode (\xA711.5 item 1): appended to the SYSTEM prompt
|
|
63132
|
+
// of every request \u2014 the same channel the Claude session-start hook uses
|
|
63133
|
+
// (system-level fires reliably; per-turn nudges measurably do not).
|
|
63134
|
+
// \`experimental.chat.system.transform\` is the hook the 1.18.18 plugin
|
|
63135
|
+
// API exposes for this ({sessionID?, model} \u2192 {system: string[]}).
|
|
63136
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
63137
|
+
try {
|
|
63138
|
+
if (output && Array.isArray(output.system) && !output.system.includes(D1_CONTRACT)) output.system.push(D1_CONTRACT);
|
|
63139
|
+
} catch {}
|
|
63140
|
+
},
|
|
63141
|
+
// Tool-failure nudge (\xA711.5 item 2): when a tool result looks failed, one
|
|
63142
|
+
// advisory line is appended to the OUTPUT the model reads. Failure is a
|
|
63143
|
+
// heuristic \u2014 \`metadata\` is untyped in the plugin API \u2014 pinned to the
|
|
63144
|
+
// shapes observed: metadata.error truthy, or a non-zero exit/exitCode.
|
|
63145
|
+
"tool.execute.after": async (_input, output) => {
|
|
63146
|
+
try {
|
|
63147
|
+
if (!output || typeof output.output !== "string") return;
|
|
63148
|
+
const meta = output.metadata;
|
|
63149
|
+
const failed = !!(meta && (meta.error || (typeof meta.exit === "number" && meta.exit !== 0) || (typeof meta.exitCode === "number" && meta.exitCode !== 0)));
|
|
63150
|
+
if (failed && !output.output.includes(TOOL_FAILURE_NUDGE)) output.output = output.output + "\\n\\n" + TOOL_FAILURE_NUDGE;
|
|
63151
|
+
} catch {}
|
|
63152
|
+
},
|
|
63153
|
+
event: async ({ event }) => {
|
|
63154
|
+
try {
|
|
63155
|
+
if (!event || typeof event.type !== "string") return;
|
|
63156
|
+
const sid = event.properties && event.properties.sessionID;
|
|
63157
|
+
if (event.type === "session.created" && sid) { fileFor(sid); touched.add(sid); return; }
|
|
63158
|
+
if (event.type === "session.idle" && sid) {
|
|
63159
|
+
const { path, appended } = await mirror(sid);
|
|
63160
|
+
touched.add(sid);
|
|
63161
|
+
// turn boundary \u2192 the daemon harvests the mirror (idempotent per turn)
|
|
63162
|
+
await post("/api/turn", { session_id: "opencode:" + sid, transcript_path: path, cwd: directory, hostHarness: "opencode", appended });
|
|
63163
|
+
}
|
|
63164
|
+
} catch {}
|
|
63165
|
+
},
|
|
63166
|
+
dispose: async () => {
|
|
63167
|
+
// OpenCode has no session-end event; process disposal is the closest.
|
|
63168
|
+
for (const sid of touched) {
|
|
63169
|
+
try {
|
|
63170
|
+
const { path } = await mirror(sid);
|
|
63171
|
+
await post("/api/session-end", { session_id: "opencode:" + sid, transcript_path: path, cwd: directory, reason: "opencode-dispose" });
|
|
63172
|
+
} catch {}
|
|
63173
|
+
}
|
|
63174
|
+
},
|
|
63175
|
+
};
|
|
63176
|
+
};
|
|
63177
|
+
`;
|
|
63178
|
+
}
|
|
63179
|
+
|
|
62927
63180
|
// src/cli.ts
|
|
62928
63181
|
var exitCleanOnEpipe = (err2) => {
|
|
62929
63182
|
if (err2.code === "EPIPE") process.exit(0);
|
|
@@ -63106,7 +63359,7 @@ Commands:
|
|
|
63106
63359
|
Written to .errata/report.html. Flag: --future-verbs
|
|
63107
63360
|
install-hooks <harness>
|
|
63108
63361
|
Wire harness hooks \u2192 daemon /api/hook (or MCP).
|
|
63109
|
-
<harness>: claude (default) | cursor | codex | aider
|
|
63362
|
+
<harness>: claude (default) | cursor | codex | opencode | aider
|
|
63110
63363
|
Flags: --port N (default 7891)
|
|
63111
63364
|
mcp Run the MCP stdio server \u2014 the agent's full errata tool
|
|
63112
63365
|
surface (navigation, problems, claims, burst, health)
|
|
@@ -65006,15 +65259,61 @@ async function cmdInstallHooks(args2) {
|
|
|
65006
65259
|
case "codex":
|
|
65007
65260
|
await installCodexHooks(port);
|
|
65008
65261
|
return;
|
|
65262
|
+
case "opencode":
|
|
65263
|
+
await installOpencodePlugin(port);
|
|
65264
|
+
return;
|
|
65009
65265
|
case "aider":
|
|
65010
65266
|
printAiderInstructions();
|
|
65011
65267
|
return;
|
|
65012
65268
|
default:
|
|
65013
65269
|
console.error(`unknown harness: ${harness}`);
|
|
65014
|
-
console.error(` supported: claude, cursor, codex, aider`);
|
|
65270
|
+
console.error(` supported: claude, cursor, codex, opencode, aider`);
|
|
65015
65271
|
process.exit(2);
|
|
65016
65272
|
}
|
|
65017
65273
|
}
|
|
65274
|
+
async function installOpencodePlugin(port) {
|
|
65275
|
+
const { mkdirSync: mkdirSync9, existsSync: existsSync31, readFileSync: readFileSync29, writeFileSync: writeFileSync23 } = await import("node:fs");
|
|
65276
|
+
const { join: join33 } = await import("node:path");
|
|
65277
|
+
const dir = join33(ROOT, ".opencode", "plugin");
|
|
65278
|
+
if (!existsSync31(dir)) mkdirSync9(dir, { recursive: true });
|
|
65279
|
+
const file2 = join33(dir, "inerrata.js");
|
|
65280
|
+
if (existsSync31(file2)) {
|
|
65281
|
+
const head2 = readFileSync29(file2, "utf8").split("\n")[0] ?? "";
|
|
65282
|
+
if (head2.trim() !== OPENCODE_PLUGIN_MARKER) {
|
|
65283
|
+
console.error(`refusing to overwrite ${file2}: not an errata-managed plugin (line 1 lacks the marker)`);
|
|
65284
|
+
process.exit(2);
|
|
65285
|
+
}
|
|
65286
|
+
}
|
|
65287
|
+
writeFileSync23(file2, buildOpencodePluginSource(port, { version: DAEMON_VERSION }), "utf8");
|
|
65288
|
+
const cfgFile = join33(ROOT, "opencode.json");
|
|
65289
|
+
let cfg = { $schema: "https://opencode.ai/config.json" };
|
|
65290
|
+
if (existsSync31(cfgFile)) {
|
|
65291
|
+
try {
|
|
65292
|
+
cfg = JSON.parse(readFileSync29(cfgFile, "utf8"));
|
|
65293
|
+
} catch {
|
|
65294
|
+
console.error(`refusing to touch invalid JSON at ${cfgFile} \u2014 fix it and re-run`);
|
|
65295
|
+
process.exit(2);
|
|
65296
|
+
}
|
|
65297
|
+
}
|
|
65298
|
+
cfg.mcp ??= {};
|
|
65299
|
+
const hadErrata = "errata" in cfg.mcp;
|
|
65300
|
+
if (!hadErrata) {
|
|
65301
|
+
const inv = errataMcpInvocation();
|
|
65302
|
+
cfg.mcp["errata"] = { type: "local", command: [inv.command, ...inv.args] };
|
|
65303
|
+
writeFileSync23(cfgFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
65304
|
+
}
|
|
65305
|
+
console.log(`installed OpenCode plugin \u2192 ${file2}`);
|
|
65306
|
+
if (hadErrata) console.log(` mcp.errata already present in ${cfgFile} \u2014 left untouched`);
|
|
65307
|
+
else console.log(`installed OpenCode MCP server config \u2192 ${cfgFile} (mcp.errata: local \`errata mcp\`)`);
|
|
65308
|
+
console.log(` endpoint: http://127.0.0.1:${port}/api/turn on every session.idle (+ /api/session-end on dispose)`);
|
|
65309
|
+
console.log("");
|
|
65310
|
+
console.log(` OpenCode keeps sessions in SQLite, so the plugin mirrors each session's`);
|
|
65311
|
+
console.log(` own message record to $XDG_DATA_HOME/opencode/errata/<sessionID>.jsonl`);
|
|
65312
|
+
console.log(` (override: ERRATA_OPENCODE_SESSIONS_DIRS) and the daemon parses THAT \u2014`);
|
|
65313
|
+
console.log(` role-typed, attested. Works for headless \`opencode run\` too.`);
|
|
65314
|
+
console.log(` Cloud tools (search/contribute) are the MCP entry in opencode.json:`);
|
|
65315
|
+
console.log(` { "mcp": { "inerrata": { "type": "remote", "url": "https://mcp.inerrata.dev/mcp", "headers": { "Authorization": "Bearer <errk_\u2026>" } } } }`);
|
|
65316
|
+
}
|
|
65018
65317
|
var ERRATA_TAG = "# errata-managed";
|
|
65019
65318
|
function errataMcpInvocation() {
|
|
65020
65319
|
const { cmd: cmd2, args: args2 } = selfArgv("mcp");
|