codemem 0.1.0 → 0.20.0-alpha.2
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/.opencode/lib/compat.js +171 -0
- package/.opencode/plugin/codemem.js +1851 -0
- package/LICENSE +21 -0
- package/dist/commands/claude-hook-ingest.d.ts +15 -0
- package/dist/commands/claude-hook-ingest.d.ts.map +1 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/enqueue-raw-event.d.ts +3 -0
- package/dist/commands/enqueue-raw-event.d.ts.map +1 -0
- package/dist/commands/export-memories.d.ts +3 -0
- package/dist/commands/export-memories.d.ts.map +1 -0
- package/dist/commands/import-memories.d.ts +3 -0
- package/dist/commands/import-memories.d.ts.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/memory.d.ts +10 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/pack.d.ts +3 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/recent.d.ts +3 -0
- package/dist/commands/recent.d.ts.map +1 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/stats.d.ts +3 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/help-style.d.ts +9 -0
- package/dist/help-style.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1076 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -15
- package/README.md +0 -5
- package/index.js +0 -6
|
@@ -0,0 +1,1851 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isVersionAtLeast,
|
|
8
|
+
parseBackendUpdatePolicy,
|
|
9
|
+
parseSemver,
|
|
10
|
+
resolveAutoUpdatePlan,
|
|
11
|
+
resolveUpgradeGuidance,
|
|
12
|
+
} from "../lib/compat.js";
|
|
13
|
+
|
|
14
|
+
const TRUTHY_VALUES = ["1", "true", "yes"];
|
|
15
|
+
const DISABLED_VALUES = ["0", "false", "off"];
|
|
16
|
+
const PINNED_BACKEND_VERSION = "0.20.0-alpha.2";
|
|
17
|
+
const DEFAULT_UVX_SOURCE = `codemem==${PINNED_BACKEND_VERSION}`;
|
|
18
|
+
|
|
19
|
+
const normalizeEnvValue = (value) => (value || "").toLowerCase();
|
|
20
|
+
const envHasValue = (value, truthyValues) =>
|
|
21
|
+
truthyValues.includes(normalizeEnvValue(value));
|
|
22
|
+
const envNotDisabled = (value) =>
|
|
23
|
+
!DISABLED_VALUES.includes(normalizeEnvValue(value));
|
|
24
|
+
|
|
25
|
+
const resolveLogPath = (logPathEnvRaw, cwd, homeDir) => {
|
|
26
|
+
const logPathEnv = normalizeEnvValue(logPathEnvRaw);
|
|
27
|
+
const logEnabled = !!logPathEnvRaw && !DISABLED_VALUES.includes(logPathEnv);
|
|
28
|
+
if (!logEnabled) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (["true", "yes", "1"].includes(logPathEnv)) {
|
|
32
|
+
return `${homeDir || cwd}/.codemem/plugin.log`;
|
|
33
|
+
}
|
|
34
|
+
return logPathEnvRaw;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createLogLine = (logPath) => async (line) => {
|
|
38
|
+
if (!logPath) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
43
|
+
await appendFile(logPath, `${new Date().toISOString()} ${line}\n`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// ignore logging failures
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createDebugLogger = ({ debug, client, logTimeoutMs, getLogLine }) =>
|
|
50
|
+
async (level, message, extra = {}) => {
|
|
51
|
+
if (!debug) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const logPromise = client.app.log({
|
|
56
|
+
service: "codemem",
|
|
57
|
+
level,
|
|
58
|
+
message,
|
|
59
|
+
extra,
|
|
60
|
+
});
|
|
61
|
+
if (!Number.isFinite(logTimeoutMs) || logTimeoutMs <= 0) {
|
|
62
|
+
await logPromise;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
await Promise.race([
|
|
67
|
+
logPromise,
|
|
68
|
+
new Promise((resolve) =>
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
timedOut = true;
|
|
71
|
+
resolve();
|
|
72
|
+
}, logTimeoutMs)
|
|
73
|
+
),
|
|
74
|
+
]);
|
|
75
|
+
if (timedOut) {
|
|
76
|
+
await getLogLine()("debug log timed out");
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// ignore debug logging failures
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const extractApplyPatchPaths = (patchText) => {
|
|
84
|
+
if (!patchText || typeof patchText !== "string") {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const paths = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const lines = patchText.split(/\r?\n/);
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const match = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/);
|
|
92
|
+
if (!match) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const path = String(match[1] || "").trim();
|
|
96
|
+
if (!path || seen.has(path)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seen.add(path);
|
|
100
|
+
paths.push(path);
|
|
101
|
+
}
|
|
102
|
+
return paths;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const appendWorkingSetFileArgs = (args, workingSetFiles) => {
|
|
106
|
+
if (!Array.isArray(workingSetFiles) || workingSetFiles.length === 0) {
|
|
107
|
+
return args;
|
|
108
|
+
}
|
|
109
|
+
for (const file of workingSetFiles) {
|
|
110
|
+
const normalized = String(file || "").trim();
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
args.push("--working-set-file", normalized.slice(0, 400));
|
|
115
|
+
}
|
|
116
|
+
return args;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const mapOpencodeEventTypeToAdapterType = (eventType) => {
|
|
120
|
+
if (eventType === "user_prompt") {
|
|
121
|
+
return "prompt";
|
|
122
|
+
}
|
|
123
|
+
if (eventType === "assistant_message") {
|
|
124
|
+
return "assistant";
|
|
125
|
+
}
|
|
126
|
+
if (eventType === "tool.execute.after") {
|
|
127
|
+
return "tool_result";
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const buildOpencodeAdapterPayload = (event) => {
|
|
133
|
+
const eventType = event?.type;
|
|
134
|
+
if (eventType === "user_prompt") {
|
|
135
|
+
const text = String(event?.prompt_text || "").trim();
|
|
136
|
+
if (!text) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
text,
|
|
141
|
+
prompt_number:
|
|
142
|
+
typeof event?.prompt_number === "number" ? event.prompt_number : null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (eventType === "assistant_message") {
|
|
147
|
+
const text = String(event?.assistant_text || "").trim();
|
|
148
|
+
if (!text) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return { text };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (eventType === "tool.execute.after") {
|
|
155
|
+
const toolName = String(event?.tool || "unknown");
|
|
156
|
+
return {
|
|
157
|
+
tool_name: toolName,
|
|
158
|
+
status: event?.error ? "error" : "ok",
|
|
159
|
+
tool_input: event?.args || {},
|
|
160
|
+
tool_output: event?.result ?? null,
|
|
161
|
+
error: event?.error ?? null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const stableStringify = (value) => {
|
|
169
|
+
if (value === null || typeof value !== "object") {
|
|
170
|
+
return JSON.stringify(value);
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
174
|
+
}
|
|
175
|
+
const keys = Object.keys(value).sort();
|
|
176
|
+
return `{${keys
|
|
177
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
178
|
+
.join(",")}}`;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const stableDigest = (value) =>
|
|
182
|
+
createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 20);
|
|
183
|
+
|
|
184
|
+
const sanitizeIdPart = (value, fallback, maxChars) => {
|
|
185
|
+
const normalized = String(value || "")
|
|
186
|
+
.replace(/[^A-Za-z0-9._:-]/g, "_")
|
|
187
|
+
.slice(0, maxChars);
|
|
188
|
+
return normalized || fallback;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const buildAdapterEventId = ({ sessionID, eventType, event, payload, ts }) => {
|
|
192
|
+
const safeSessionID = sanitizeIdPart(sessionID, "unknown", 48);
|
|
193
|
+
const safeType = sanitizeIdPart(eventType, "event", 24);
|
|
194
|
+
const rawTimestamp =
|
|
195
|
+
typeof event?.timestamp === "string" && event.timestamp.trim()
|
|
196
|
+
? event.timestamp.trim()
|
|
197
|
+
: ts;
|
|
198
|
+
const digest = stableDigest({
|
|
199
|
+
session_id: String(sessionID || ""),
|
|
200
|
+
event_type: String(eventType || ""),
|
|
201
|
+
raw_event_type: String(event?.type || ""),
|
|
202
|
+
timestamp: rawTimestamp,
|
|
203
|
+
payload,
|
|
204
|
+
});
|
|
205
|
+
return `oc:${safeSessionID}:${safeType}:${digest}`.slice(0, 128);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const buildOpencodeAdapterEvent = ({ sessionID, event }) => {
|
|
209
|
+
if (!sessionID || !event || typeof event !== "object") {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const adapterType = mapOpencodeEventTypeToAdapterType(event.type);
|
|
213
|
+
if (!adapterType) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const payload = buildOpencodeAdapterPayload(event);
|
|
217
|
+
if (!payload) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const ts = typeof event.timestamp === "string" ? event.timestamp : new Date().toISOString();
|
|
221
|
+
return {
|
|
222
|
+
schema_version: "1.0",
|
|
223
|
+
source: "opencode",
|
|
224
|
+
session_id: String(sessionID),
|
|
225
|
+
event_id: buildAdapterEventId({
|
|
226
|
+
sessionID,
|
|
227
|
+
eventType: adapterType,
|
|
228
|
+
event,
|
|
229
|
+
payload,
|
|
230
|
+
ts,
|
|
231
|
+
}),
|
|
232
|
+
event_type: adapterType,
|
|
233
|
+
ts,
|
|
234
|
+
ordering_confidence: "low",
|
|
235
|
+
payload,
|
|
236
|
+
meta: {
|
|
237
|
+
original_event_type: String(event.type || "unknown"),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const resolveProjectName = (project) =>
|
|
243
|
+
project?.name ||
|
|
244
|
+
(project?.root
|
|
245
|
+
? String(project.root).split(/[/\\]/).filter(Boolean).pop()
|
|
246
|
+
: null) ||
|
|
247
|
+
null;
|
|
248
|
+
|
|
249
|
+
const selectRawEventId = ({ payload, nextEventId }) => {
|
|
250
|
+
const fromPayload =
|
|
251
|
+
payload &&
|
|
252
|
+
typeof payload === "object" &&
|
|
253
|
+
payload._raw_event_id;
|
|
254
|
+
return String(fromPayload || nextEventId());
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const buildRawEventEnvelope = ({
|
|
258
|
+
sessionID,
|
|
259
|
+
type,
|
|
260
|
+
payload,
|
|
261
|
+
cwd,
|
|
262
|
+
project,
|
|
263
|
+
startedAt,
|
|
264
|
+
nowMs,
|
|
265
|
+
nowMono,
|
|
266
|
+
nextEventId,
|
|
267
|
+
}) => ({
|
|
268
|
+
session_stream_id: sessionID,
|
|
269
|
+
session_id: sessionID,
|
|
270
|
+
opencode_session_id: sessionID,
|
|
271
|
+
event_id: selectRawEventId({ payload, nextEventId }),
|
|
272
|
+
event_type: type,
|
|
273
|
+
ts_wall_ms: nowMs,
|
|
274
|
+
ts_mono_ms: nowMono,
|
|
275
|
+
payload,
|
|
276
|
+
cwd,
|
|
277
|
+
project,
|
|
278
|
+
started_at: startedAt,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const trimEventQueue = ({ events, maxEvents, hardMaxEvents, onUnsentPressure, onForcedDrop }) => {
|
|
282
|
+
if (!Number.isFinite(maxEvents) || maxEvents <= 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
while (events.length > maxEvents) {
|
|
286
|
+
const droppableIndex = events.findIndex(
|
|
287
|
+
(queued) => queued && typeof queued === "object" && queued._raw_enqueued
|
|
288
|
+
);
|
|
289
|
+
if (droppableIndex >= 0) {
|
|
290
|
+
events.splice(droppableIndex, 1);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (typeof onUnsentPressure === "function") {
|
|
294
|
+
onUnsentPressure(events.length, maxEvents);
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
Number.isFinite(hardMaxEvents) &&
|
|
298
|
+
hardMaxEvents > 0 &&
|
|
299
|
+
events.length > hardMaxEvents
|
|
300
|
+
) {
|
|
301
|
+
const dropped = events.shift();
|
|
302
|
+
if (typeof onForcedDrop === "function") {
|
|
303
|
+
onForcedDrop(dropped, events.length, hardMaxEvents);
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const attachAdapterEvent = ({ sessionID, event }) => {
|
|
312
|
+
if (!event || typeof event !== "object") {
|
|
313
|
+
return event;
|
|
314
|
+
}
|
|
315
|
+
let adapterEvent = null;
|
|
316
|
+
try {
|
|
317
|
+
adapterEvent = buildOpencodeAdapterEvent({ sessionID, event });
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return event;
|
|
320
|
+
}
|
|
321
|
+
if (!adapterEvent) {
|
|
322
|
+
return event;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
...event,
|
|
326
|
+
_adapter: adapterEvent,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const asNonNegativeCount = (value) => {
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
return value.length;
|
|
333
|
+
}
|
|
334
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
335
|
+
return Math.max(0, Math.trunc(value));
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const asFiniteNonNegativeInt = (value) => {
|
|
341
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
if (value < 0) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return Math.trunc(value);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const parsePositiveInt = (value, fallback) => {
|
|
351
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
352
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
353
|
+
return fallback;
|
|
354
|
+
}
|
|
355
|
+
return parsed;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const buildInjectionToastMessage = (metrics) => {
|
|
359
|
+
const items = asFiniteNonNegativeInt(metrics?.items);
|
|
360
|
+
const packTokens = asFiniteNonNegativeInt(metrics?.pack_tokens);
|
|
361
|
+
const avoided = asFiniteNonNegativeInt(metrics?.avoided_work_tokens);
|
|
362
|
+
const avoidedUnknown = asNonNegativeCount(metrics?.avoided_work_unknown_items);
|
|
363
|
+
const avoidedKnown = asNonNegativeCount(metrics?.avoided_work_known_items);
|
|
364
|
+
const addedCount = asNonNegativeCount(metrics?.added_ids);
|
|
365
|
+
const removedCount = asNonNegativeCount(metrics?.removed_ids);
|
|
366
|
+
const deltaAvailable = metrics?.pack_delta_available === true;
|
|
367
|
+
|
|
368
|
+
const messageParts = ["codemem injected"];
|
|
369
|
+
if (items !== null) messageParts.push(`${items} items`);
|
|
370
|
+
if (packTokens !== null) messageParts.push(`~${packTokens} tokens`);
|
|
371
|
+
if (
|
|
372
|
+
avoided !== null
|
|
373
|
+
&& avoided > 0
|
|
374
|
+
&& avoidedKnown !== null
|
|
375
|
+
&& avoidedUnknown !== null
|
|
376
|
+
&& avoidedKnown >= avoidedUnknown
|
|
377
|
+
) {
|
|
378
|
+
messageParts.push(`avoided work ~${avoided} tokens`);
|
|
379
|
+
}
|
|
380
|
+
if (deltaAvailable && (addedCount !== null || removedCount !== null)) {
|
|
381
|
+
messageParts.push(`delta +${addedCount || 0}/-${removedCount || 0}`);
|
|
382
|
+
}
|
|
383
|
+
return messageParts.join(" · ");
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const detectRunner = ({ cwd, envRunner }) => {
|
|
387
|
+
if (envRunner) {
|
|
388
|
+
return envRunner;
|
|
389
|
+
}
|
|
390
|
+
// Check if we're in the codemem repo (dev mode)
|
|
391
|
+
try {
|
|
392
|
+
const pyproject = Bun.file(`${cwd}/pyproject.toml`);
|
|
393
|
+
if (pyproject.size > 0) {
|
|
394
|
+
const content = require("fs").readFileSync(
|
|
395
|
+
`${cwd}/pyproject.toml`,
|
|
396
|
+
"utf-8"
|
|
397
|
+
);
|
|
398
|
+
if (content.includes('name = "codemem"')) {
|
|
399
|
+
return "uv";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
// Not in dev mode
|
|
404
|
+
}
|
|
405
|
+
return "uvx";
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if the TS CLI is available at the given path.
|
|
410
|
+
* Used by the "node" runner to verify the built CLI exists.
|
|
411
|
+
*/
|
|
412
|
+
const tsCliAvailable = (cliPath) => {
|
|
413
|
+
try {
|
|
414
|
+
return require("fs").existsSync(cliPath);
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const buildRunnerArgs = ({ runner, runnerFrom, runnerFromExplicit }) => {
|
|
421
|
+
if (runner === "uvx") {
|
|
422
|
+
if (runnerFromExplicit) {
|
|
423
|
+
return ["--from", runnerFrom, "codemem"];
|
|
424
|
+
}
|
|
425
|
+
return [runnerFrom || "codemem"];
|
|
426
|
+
}
|
|
427
|
+
if (runner === "uv") {
|
|
428
|
+
return ["run", "--directory", runnerFrom, "codemem"];
|
|
429
|
+
}
|
|
430
|
+
if (runner === "node") {
|
|
431
|
+
// TS CLI — runnerFrom points to the built CLI entry or repo root.
|
|
432
|
+
// Default: packages/cli/dist/index.js relative to repo root.
|
|
433
|
+
const cliPath = runnerFromExplicit
|
|
434
|
+
? runnerFrom
|
|
435
|
+
: require("path").join(runnerFrom, "packages/cli/dist/index.js");
|
|
436
|
+
return [cliPath];
|
|
437
|
+
}
|
|
438
|
+
if (runner === "npx") {
|
|
439
|
+
const pkg = runnerFromExplicit ? runnerFrom : "@codemem/cli";
|
|
440
|
+
return ["-y", pkg];
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export const OpencodeMemPlugin = async ({
|
|
446
|
+
project,
|
|
447
|
+
client,
|
|
448
|
+
directory,
|
|
449
|
+
worktree,
|
|
450
|
+
}) => {
|
|
451
|
+
const events = [];
|
|
452
|
+
const maxEvents = parsePositiveInt(process.env.CODEMEM_PLUGIN_MAX_EVENTS, 200);
|
|
453
|
+
const maxChars = Number.parseInt(
|
|
454
|
+
process.env.CODEMEM_PLUGIN_MAX_EVENT_CHARS || "8000",
|
|
455
|
+
10
|
|
456
|
+
);
|
|
457
|
+
const cwd = worktree || directory || process.cwd();
|
|
458
|
+
const debug = envHasValue(process.env.CODEMEM_PLUGIN_DEBUG, TRUTHY_VALUES);
|
|
459
|
+
const debugExtraction = envHasValue(
|
|
460
|
+
process.env.CODEMEM_DEBUG_EXTRACTION,
|
|
461
|
+
TRUTHY_VALUES
|
|
462
|
+
);
|
|
463
|
+
const logTimeoutMs = Number.parseInt(
|
|
464
|
+
process.env.CODEMEM_PLUGIN_LOG_TIMEOUT_MS || "1500",
|
|
465
|
+
10
|
|
466
|
+
);
|
|
467
|
+
const logPathEnvRaw = process.env.CODEMEM_PLUGIN_LOG || "";
|
|
468
|
+
const logPath = resolveLogPath(logPathEnvRaw, cwd, process.env.HOME);
|
|
469
|
+
const logLine = createLogLine(logPath);
|
|
470
|
+
const log = createDebugLogger({
|
|
471
|
+
debug,
|
|
472
|
+
client,
|
|
473
|
+
logTimeoutMs,
|
|
474
|
+
getLogLine: () => logLine,
|
|
475
|
+
});
|
|
476
|
+
const pluginIgnored = envHasValue(
|
|
477
|
+
process.env.CODEMEM_PLUGIN_IGNORE,
|
|
478
|
+
TRUTHY_VALUES
|
|
479
|
+
);
|
|
480
|
+
if (pluginIgnored) {
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Determine runner mode:
|
|
485
|
+
// - If CODEMEM_RUNNER is set, use that ("uvx", "uv", "node", "npx")
|
|
486
|
+
// - If we're in a directory with pyproject.toml containing codemem, use "uv" (dev mode)
|
|
487
|
+
// - Otherwise, use "uvx" with a package source pinned to plugin version
|
|
488
|
+
const runner = detectRunner({
|
|
489
|
+
cwd,
|
|
490
|
+
envRunner: process.env.CODEMEM_RUNNER,
|
|
491
|
+
});
|
|
492
|
+
const runnerFromExplicit = Boolean(String(process.env.CODEMEM_RUNNER_FROM || "").trim());
|
|
493
|
+
const defaultRunnerFrom = runner === "uvx" ? DEFAULT_UVX_SOURCE : cwd;
|
|
494
|
+
const runnerFrom = process.env.CODEMEM_RUNNER_FROM || defaultRunnerFrom;
|
|
495
|
+
const runnerArgs = buildRunnerArgs({ runner, runnerFrom, runnerFromExplicit });
|
|
496
|
+
const viewerEnabled = envNotDisabled(process.env.CODEMEM_VIEWER || "1");
|
|
497
|
+
const viewerAutoStart = envNotDisabled(
|
|
498
|
+
process.env.CODEMEM_VIEWER_AUTO || "1"
|
|
499
|
+
);
|
|
500
|
+
const viewerAutoStop = envNotDisabled(
|
|
501
|
+
process.env.CODEMEM_VIEWER_AUTO_STOP || "1"
|
|
502
|
+
);
|
|
503
|
+
const viewerHost = process.env.CODEMEM_VIEWER_HOST || "127.0.0.1";
|
|
504
|
+
const viewerPort = process.env.CODEMEM_VIEWER_PORT || "38888";
|
|
505
|
+
const commandTimeout = Number.parseInt(
|
|
506
|
+
process.env.CODEMEM_PLUGIN_CMD_TIMEOUT || "20000",
|
|
507
|
+
10
|
|
508
|
+
);
|
|
509
|
+
const backendUpdatePolicy = parseBackendUpdatePolicy(
|
|
510
|
+
process.env.CODEMEM_BACKEND_UPDATE_POLICY || "notify"
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const parseNumber = (value, fallback) => {
|
|
514
|
+
const parsed = Number.parseInt(value, 10);
|
|
515
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
516
|
+
};
|
|
517
|
+
const injectEnabled = envNotDisabled(
|
|
518
|
+
process.env.CODEMEM_INJECT_CONTEXT || "1"
|
|
519
|
+
);
|
|
520
|
+
// Only use env overrides if explicitly set; otherwise CLI uses config defaults
|
|
521
|
+
const injectLimitEnv = process.env.CODEMEM_INJECT_LIMIT;
|
|
522
|
+
const injectLimit = injectLimitEnv ? parseNumber(injectLimitEnv, null) : null;
|
|
523
|
+
const injectTokenBudgetEnv = process.env.CODEMEM_INJECT_TOKEN_BUDGET;
|
|
524
|
+
const injectTokenBudget = injectTokenBudgetEnv ? parseNumber(injectTokenBudgetEnv, null) : null;
|
|
525
|
+
const injectedSessions = new Map();
|
|
526
|
+
const injectionToastShown = new Set();
|
|
527
|
+
let sessionStartedAt = null;
|
|
528
|
+
let activeSessionID = null;
|
|
529
|
+
let viewerStarted = false;
|
|
530
|
+
let promptCounter = 0;
|
|
531
|
+
let lastPromptText = null;
|
|
532
|
+
let lastAssistantText = null;
|
|
533
|
+
const assistantUsageCaptured = new Set();
|
|
534
|
+
|
|
535
|
+
// Track message roles and accumulated text by messageID
|
|
536
|
+
const messageRoles = new Map();
|
|
537
|
+
const messageTexts = new Map();
|
|
538
|
+
let debugLogCount = 0;
|
|
539
|
+
|
|
540
|
+
const rawEventsEnabled = envNotDisabled(
|
|
541
|
+
process.env.CODEMEM_RAW_EVENTS || "1"
|
|
542
|
+
);
|
|
543
|
+
const rawEventsUrl = `http://${viewerHost}:${viewerPort}/api/raw-events`;
|
|
544
|
+
const rawEventsStatusUrl = `http://${viewerHost}:${viewerPort}/api/raw-events/status?limit=1`;
|
|
545
|
+
const rawEventsBackoffMs = parseNumber(
|
|
546
|
+
process.env.CODEMEM_RAW_EVENTS_BACKOFF_MS || "10000",
|
|
547
|
+
10000
|
|
548
|
+
);
|
|
549
|
+
const rawEventsStatusCheckMs = parseNumber(
|
|
550
|
+
process.env.CODEMEM_RAW_EVENTS_STATUS_CHECK_MS || "30000",
|
|
551
|
+
30000
|
|
552
|
+
);
|
|
553
|
+
const rawEventsHardMax = parseNumber(
|
|
554
|
+
process.env.CODEMEM_RAW_EVENTS_HARD_MAX || "2000",
|
|
555
|
+
2000
|
|
556
|
+
);
|
|
557
|
+
let streamUnavailableUntil = 0;
|
|
558
|
+
let streamErrorNoted = false;
|
|
559
|
+
let fallbackFailureNoted = false;
|
|
560
|
+
let lastStatusCheckAt = 0;
|
|
561
|
+
let lastStatusAvailable = true;
|
|
562
|
+
const nextEventId = () => {
|
|
563
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
564
|
+
return crypto.randomUUID();
|
|
565
|
+
}
|
|
566
|
+
return `${Date.now()}-${Math.random()}`;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const queueRawEventViaCli = async (body) => {
|
|
570
|
+
const result = await runCli(["enqueue-raw-event"], {
|
|
571
|
+
stdinText: JSON.stringify(body),
|
|
572
|
+
});
|
|
573
|
+
if (result?.exitCode !== 0) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`enqueue-raw-event failed (${result?.exitCode ?? "unknown"})`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const lastToastAtBySession = new Map();
|
|
582
|
+
const shouldToast = (sessionID) => {
|
|
583
|
+
const now = Date.now();
|
|
584
|
+
const last = lastToastAtBySession.get(sessionID) || 0;
|
|
585
|
+
if (now - last < 60000) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
lastToastAtBySession.set(sessionID, now);
|
|
589
|
+
return true;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const emitRawEvent = async ({ sessionID, type, payload }) => {
|
|
593
|
+
if (!rawEventsEnabled) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
if (!sessionID || !type) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const now = Date.now();
|
|
600
|
+
const body = buildRawEventEnvelope({
|
|
601
|
+
sessionID,
|
|
602
|
+
type,
|
|
603
|
+
payload,
|
|
604
|
+
cwd,
|
|
605
|
+
project: resolveProjectName(project),
|
|
606
|
+
startedAt: sessionStartedAt,
|
|
607
|
+
nowMs: now,
|
|
608
|
+
nowMono:
|
|
609
|
+
typeof performance !== "undefined" && performance.now
|
|
610
|
+
? performance.now()
|
|
611
|
+
: null,
|
|
612
|
+
nextEventId,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (now < streamUnavailableUntil) {
|
|
616
|
+
try {
|
|
617
|
+
await queueRawEventViaCli(body);
|
|
618
|
+
fallbackFailureNoted = false;
|
|
619
|
+
if (payload && typeof payload === "object") {
|
|
620
|
+
payload._raw_enqueued = true;
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
} catch (fallbackErr) {
|
|
624
|
+
await logLine(
|
|
625
|
+
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
626
|
+
fallbackErr
|
|
627
|
+
).slice(0, 200)}`
|
|
628
|
+
);
|
|
629
|
+
if (!fallbackFailureNoted) {
|
|
630
|
+
fallbackFailureNoted = true;
|
|
631
|
+
try {
|
|
632
|
+
await client.app.log({
|
|
633
|
+
service: "codemem",
|
|
634
|
+
level: "error",
|
|
635
|
+
message: "codemem fallback enqueue failed during stream backoff",
|
|
636
|
+
extra: {
|
|
637
|
+
sessionID,
|
|
638
|
+
backoffMs: rawEventsBackoffMs,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
} catch (logErr) {
|
|
642
|
+
// best-effort logging only
|
|
643
|
+
}
|
|
644
|
+
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
645
|
+
try {
|
|
646
|
+
await client.tui.showToast({
|
|
647
|
+
body: {
|
|
648
|
+
message: "codemem: fallback enqueue failed while stream is down",
|
|
649
|
+
variant: "error",
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
} catch (toastErr) {
|
|
653
|
+
// best-effort only
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
if (now - lastStatusCheckAt >= Math.max(1000, rawEventsStatusCheckMs)) {
|
|
662
|
+
const statusResp = await fetch(rawEventsStatusUrl, { method: "GET" });
|
|
663
|
+
if (!statusResp.ok) {
|
|
664
|
+
throw new Error(`raw-events status failed (${statusResp.status})`);
|
|
665
|
+
}
|
|
666
|
+
const statusJson = await statusResp.json();
|
|
667
|
+
lastStatusAvailable = statusJson?.ingest?.available !== false;
|
|
668
|
+
lastStatusCheckAt = now;
|
|
669
|
+
}
|
|
670
|
+
if (!lastStatusAvailable) {
|
|
671
|
+
throw new Error("raw-events ingest unavailable");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const postResp = await fetch(rawEventsUrl, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/json" },
|
|
677
|
+
body: JSON.stringify(body),
|
|
678
|
+
});
|
|
679
|
+
if (!postResp.ok) {
|
|
680
|
+
throw new Error(`raw-events post failed (${postResp.status})`);
|
|
681
|
+
}
|
|
682
|
+
streamUnavailableUntil = 0;
|
|
683
|
+
streamErrorNoted = false;
|
|
684
|
+
fallbackFailureNoted = false;
|
|
685
|
+
lastStatusAvailable = true;
|
|
686
|
+
if (payload && typeof payload === "object") {
|
|
687
|
+
payload._raw_enqueued = true;
|
|
688
|
+
}
|
|
689
|
+
return true;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
streamUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
692
|
+
await logLine(`raw_events.error sessionID=${sessionID} type=${type} err=${String(err).slice(0, 200)}`);
|
|
693
|
+
try {
|
|
694
|
+
await client.app.log({
|
|
695
|
+
service: "codemem",
|
|
696
|
+
level: "error",
|
|
697
|
+
message: "Failed to stream raw events to codemem viewer",
|
|
698
|
+
extra: {
|
|
699
|
+
sessionID,
|
|
700
|
+
type,
|
|
701
|
+
viewerHost,
|
|
702
|
+
viewerPort,
|
|
703
|
+
error: String(err),
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
} catch (logErr) {
|
|
707
|
+
// best-effort logging only
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let fallbackOk = false;
|
|
711
|
+
try {
|
|
712
|
+
await queueRawEventViaCli(body);
|
|
713
|
+
fallbackOk = true;
|
|
714
|
+
} catch (fallbackErr) {
|
|
715
|
+
await logLine(
|
|
716
|
+
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
717
|
+
fallbackErr
|
|
718
|
+
).slice(0, 200)}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (fallbackOk) {
|
|
723
|
+
fallbackFailureNoted = false;
|
|
724
|
+
if (payload && typeof payload === "object") {
|
|
725
|
+
payload._raw_enqueued = true;
|
|
726
|
+
}
|
|
727
|
+
if (!streamErrorNoted) {
|
|
728
|
+
streamErrorNoted = true;
|
|
729
|
+
try {
|
|
730
|
+
await client.app.log({
|
|
731
|
+
service: "codemem",
|
|
732
|
+
level: "warn",
|
|
733
|
+
message: "codemem stream unavailable; queued raw event via CLI fallback",
|
|
734
|
+
extra: {
|
|
735
|
+
sessionID,
|
|
736
|
+
backoffMs: rawEventsBackoffMs,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
} catch (logErr) {
|
|
740
|
+
// best-effort logging only
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
744
|
+
try {
|
|
745
|
+
await client.tui.showToast({
|
|
746
|
+
body: {
|
|
747
|
+
message: "codemem: viewer stream unavailable; queue fallback active",
|
|
748
|
+
variant: "warning",
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
} catch (toastErr) {
|
|
752
|
+
// best-effort only
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!streamErrorNoted) {
|
|
759
|
+
streamErrorNoted = true;
|
|
760
|
+
try {
|
|
761
|
+
await client.app.log({
|
|
762
|
+
service: "codemem",
|
|
763
|
+
level: "error",
|
|
764
|
+
message: "codemem stream unavailable; fallback enqueue failed",
|
|
765
|
+
extra: {
|
|
766
|
+
sessionID,
|
|
767
|
+
backoffMs: rawEventsBackoffMs,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
} catch (logErr) {
|
|
771
|
+
// best-effort logging only
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
776
|
+
try {
|
|
777
|
+
await client.tui.showToast({
|
|
778
|
+
body: {
|
|
779
|
+
message: `codemem: stream unavailable (${viewerHost}:${viewerPort}); fallback failed`,
|
|
780
|
+
variant: "error",
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
} catch (toastErr) {
|
|
784
|
+
// best-effort only
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const extractSessionID = (event) => {
|
|
792
|
+
if (!event) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
return event?.properties?.sessionID || null;
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// Session context tracking for comprehensive memories
|
|
799
|
+
const sessionContext = {
|
|
800
|
+
firstPrompt: null,
|
|
801
|
+
promptCount: 0,
|
|
802
|
+
toolCount: 0,
|
|
803
|
+
startTime: null,
|
|
804
|
+
filesModified: new Set(),
|
|
805
|
+
filesRead: new Set(),
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const resetSessionContext = () => {
|
|
809
|
+
sessionContext.firstPrompt = null;
|
|
810
|
+
sessionContext.promptCount = 0;
|
|
811
|
+
sessionContext.toolCount = 0;
|
|
812
|
+
sessionContext.startTime = null;
|
|
813
|
+
sessionContext.filesModified = new Set();
|
|
814
|
+
sessionContext.filesRead = new Set();
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// Check if we should force flush immediately (threshold-based)
|
|
818
|
+
const shouldForceFlush = () => {
|
|
819
|
+
const { toolCount, promptCount } = sessionContext;
|
|
820
|
+
// Force flush if we've accumulated a lot of work
|
|
821
|
+
if (toolCount >= 50 || promptCount >= 15) {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
// Force flush if session has been running for 10+ minutes
|
|
825
|
+
if (sessionContext.startTime) {
|
|
826
|
+
const sessionDurationMs = Date.now() - sessionContext.startTime;
|
|
827
|
+
if (sessionDurationMs >= 600000) { // 10 minutes
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
const updateActivity = () => {};
|
|
836
|
+
|
|
837
|
+
const extractPromptText = (event) => {
|
|
838
|
+
if (!event) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// For message.updated events, track the role and check if we have buffered text
|
|
843
|
+
if (event.type === "message.updated" && event.properties?.info) {
|
|
844
|
+
const info = event.properties.info;
|
|
845
|
+
if (info.id && info.role) {
|
|
846
|
+
messageRoles.set(info.id, info.role);
|
|
847
|
+
|
|
848
|
+
// If we have buffered text for this message and it's a user message, return it
|
|
849
|
+
if (info.role === "user" && messageTexts.has(info.id)) {
|
|
850
|
+
const text = messageTexts.get(info.id);
|
|
851
|
+
messageTexts.delete(info.id); // Clean up
|
|
852
|
+
if (debugExtraction) {
|
|
853
|
+
logLine(
|
|
854
|
+
`user prompt captured from buffered text id=${info.id.slice(
|
|
855
|
+
-8
|
|
856
|
+
)} len=${text.length}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
return text;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// For message.part.updated events, accumulate or return text based on known role
|
|
866
|
+
if (event.type === "message.part.updated" && event.properties?.part) {
|
|
867
|
+
const part = event.properties.part;
|
|
868
|
+
if (part.type !== "text" || !part.text) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const role = messageRoles.get(part.messageID);
|
|
873
|
+
if (role === "user") {
|
|
874
|
+
// We know it's a user message, return the text immediately
|
|
875
|
+
if (debugExtraction) {
|
|
876
|
+
logLine(
|
|
877
|
+
`user prompt captured immediately id=${part.messageID.slice(
|
|
878
|
+
-8
|
|
879
|
+
)} len=${part.text.length}`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
return part.text.trim() || null;
|
|
883
|
+
} else if (!role) {
|
|
884
|
+
// Buffer this text until we know the role
|
|
885
|
+
const existing = messageTexts.get(part.messageID) || "";
|
|
886
|
+
messageTexts.set(part.messageID, existing + part.text);
|
|
887
|
+
if (debugExtraction) {
|
|
888
|
+
logLine(
|
|
889
|
+
`buffering text for unknown role id=${part.messageID.slice(
|
|
890
|
+
-8
|
|
891
|
+
)} len=${(existing + part.text).length}`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return null;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const extractAssistantText = (event) => {
|
|
901
|
+
if (!event) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Only capture assistant messages when complete (message.updated with finish)
|
|
906
|
+
if (event.type === "message.updated" && event.properties?.info) {
|
|
907
|
+
const info = event.properties.info;
|
|
908
|
+
if (info.id && info.role) {
|
|
909
|
+
messageRoles.set(info.id, info.role);
|
|
910
|
+
|
|
911
|
+
// Log when we see an assistant message.updated (debug only)
|
|
912
|
+
if (debugExtraction && info.role === "assistant") {
|
|
913
|
+
logLine(
|
|
914
|
+
`assistant message.updated id=${info.id.slice(
|
|
915
|
+
-8
|
|
916
|
+
)} finish=${!!info.finish} hasText=${messageTexts.has(
|
|
917
|
+
info.id
|
|
918
|
+
)} textLen=${messageTexts.get(info.id)?.length || 0}`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Only return assistant text when message is finished
|
|
923
|
+
if (
|
|
924
|
+
info.role === "assistant" &&
|
|
925
|
+
info.finish &&
|
|
926
|
+
messageTexts.has(info.id)
|
|
927
|
+
) {
|
|
928
|
+
const text = messageTexts.get(info.id);
|
|
929
|
+
messageTexts.delete(info.id); // Clean up
|
|
930
|
+
return text.trim() || null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// For message.part.updated, store the latest text (don't capture yet)
|
|
937
|
+
// Store for ALL messages regardless of role - role might not be known yet
|
|
938
|
+
if (event.type === "message.part.updated" && event.properties?.part) {
|
|
939
|
+
const part = event.properties.part;
|
|
940
|
+
if (part.type === "text" && part.text) {
|
|
941
|
+
// Store latest text, will be captured on finish (for assistant) or on role discovery (for user)
|
|
942
|
+
if (debugExtraction) {
|
|
943
|
+
const prevLen = messageTexts.get(part.messageID)?.length || 0;
|
|
944
|
+
logLine(
|
|
945
|
+
`text part stored id=${part.messageID.slice(
|
|
946
|
+
-8
|
|
947
|
+
)} prevLen=${prevLen} newLen=${part.text.length} role=${
|
|
948
|
+
messageRoles.get(part.messageID) || "unknown"
|
|
949
|
+
}`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
messageTexts.set(part.messageID, part.text);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return null;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const normalizeUsage = (usage) => {
|
|
960
|
+
if (!usage || typeof usage !== "object") {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
const inputTokens = Number(usage.input_tokens || 0);
|
|
964
|
+
const outputTokens = Number(usage.output_tokens || 0);
|
|
965
|
+
const cacheCreationTokens = Number(usage.cache_creation_input_tokens || 0);
|
|
966
|
+
const cacheReadTokens = Number(usage.cache_read_input_tokens || 0);
|
|
967
|
+
const total = inputTokens + outputTokens + cacheCreationTokens;
|
|
968
|
+
if (!Number.isFinite(total) || total <= 0) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
input_tokens: inputTokens,
|
|
973
|
+
output_tokens: outputTokens,
|
|
974
|
+
cache_creation_input_tokens: cacheCreationTokens,
|
|
975
|
+
cache_read_input_tokens: cacheReadTokens,
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const extractAssistantUsage = (event) => {
|
|
980
|
+
if (!event || event.type !== "message.updated" || !event.properties?.info) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
const info = event.properties.info;
|
|
984
|
+
if (!info.id || info.role !== "assistant" || !info.finish) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
if (assistantUsageCaptured.has(info.id)) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
const usage = normalizeUsage(
|
|
991
|
+
info.usage || event.properties?.usage || event.usage
|
|
992
|
+
);
|
|
993
|
+
if (!usage) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
assistantUsageCaptured.add(info.id);
|
|
997
|
+
return { usage, id: info.id };
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const startViewer = () => {
|
|
1001
|
+
if (!viewerEnabled || !viewerAutoStart || viewerStarted) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
viewerStarted = true;
|
|
1005
|
+
log("info", "starting codemem viewer", { cwd });
|
|
1006
|
+
Bun.spawn({
|
|
1007
|
+
cmd: [runner, ...runnerArgs, "serve", "--background"],
|
|
1008
|
+
cwd,
|
|
1009
|
+
env: process.env,
|
|
1010
|
+
stdout: "pipe",
|
|
1011
|
+
stderr: "pipe",
|
|
1012
|
+
});
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const runCommand = async (cmd, options = {}) => {
|
|
1016
|
+
const { stdinText = null } = options;
|
|
1017
|
+
const proc = Bun.spawn({
|
|
1018
|
+
cmd,
|
|
1019
|
+
cwd,
|
|
1020
|
+
env: process.env,
|
|
1021
|
+
stdin: "pipe",
|
|
1022
|
+
stdout: "pipe",
|
|
1023
|
+
stderr: "pipe",
|
|
1024
|
+
});
|
|
1025
|
+
let stdinFailure = null;
|
|
1026
|
+
if (typeof stdinText === "string") {
|
|
1027
|
+
try {
|
|
1028
|
+
proc.stdin.write(stdinText);
|
|
1029
|
+
} catch (stdinErr) {
|
|
1030
|
+
stdinFailure = `stdin write failed: ${String(stdinErr)}`;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
proc.stdin.end();
|
|
1035
|
+
} catch (stdinErr) {
|
|
1036
|
+
if (!stdinFailure) {
|
|
1037
|
+
stdinFailure = `stdin close failed: ${String(stdinErr)}`;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (stdinFailure) {
|
|
1041
|
+
try {
|
|
1042
|
+
proc.kill();
|
|
1043
|
+
} catch (killErr) {
|
|
1044
|
+
// ignore
|
|
1045
|
+
}
|
|
1046
|
+
return { exitCode: 1, stdout: "", stderr: stdinFailure };
|
|
1047
|
+
}
|
|
1048
|
+
const resultPromise = Promise.all([
|
|
1049
|
+
proc.exited,
|
|
1050
|
+
new Response(proc.stdout).text(),
|
|
1051
|
+
new Response(proc.stderr).text(),
|
|
1052
|
+
]).then(([exitCode, stdout, stderr]) => ({ exitCode, stdout, stderr }));
|
|
1053
|
+
if (!Number.isFinite(commandTimeout) || commandTimeout <= 0) {
|
|
1054
|
+
return resultPromise;
|
|
1055
|
+
}
|
|
1056
|
+
let timer = null;
|
|
1057
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1058
|
+
timer = setTimeout(() => {
|
|
1059
|
+
try {
|
|
1060
|
+
proc.kill();
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
// ignore
|
|
1063
|
+
}
|
|
1064
|
+
resolve({ exitCode: null, stdout: "", stderr: "timeout" });
|
|
1065
|
+
}, commandTimeout);
|
|
1066
|
+
});
|
|
1067
|
+
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
1068
|
+
if (timer) {
|
|
1069
|
+
clearTimeout(timer);
|
|
1070
|
+
}
|
|
1071
|
+
return result;
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
const runCli = async (args, options = {}) =>
|
|
1075
|
+
runCommand([runner, ...runnerArgs, ...args], options);
|
|
1076
|
+
|
|
1077
|
+
const showToast = async (message, variant = "warning") => {
|
|
1078
|
+
if (backendUpdatePolicy === "off") {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (!client.tui?.showToast) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
await client.tui.showToast({
|
|
1086
|
+
body: {
|
|
1087
|
+
message,
|
|
1088
|
+
variant,
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
} catch (toastErr) {
|
|
1092
|
+
// best-effort only
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
const restartViewerAfterAutoUpdate = async () => {
|
|
1097
|
+
if (!viewerEnabled || !viewerAutoStart || !viewerStarted) {
|
|
1098
|
+
return { attempted: false, ok: false };
|
|
1099
|
+
}
|
|
1100
|
+
const restartResult = await runCli(["serve", "--restart"]);
|
|
1101
|
+
if (restartResult?.exitCode === 0) {
|
|
1102
|
+
await logLine("compat.auto_update_viewer_restart ok");
|
|
1103
|
+
return { attempted: true, ok: true };
|
|
1104
|
+
}
|
|
1105
|
+
await logLine(
|
|
1106
|
+
`compat.auto_update_viewer_restart_failed exit=${restartResult?.exitCode ?? "unknown"} stderr=${redactLog(
|
|
1107
|
+
(restartResult?.stderr || "").trim()
|
|
1108
|
+
)}`
|
|
1109
|
+
);
|
|
1110
|
+
return { attempted: true, ok: false };
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const verifyCliCompatibility = async () => {
|
|
1114
|
+
const minVersion = process.env.CODEMEM_MIN_VERSION || "0.9.20";
|
|
1115
|
+
const versionResult = await runCli(["version"]);
|
|
1116
|
+
if (!versionResult || versionResult.exitCode !== 0) {
|
|
1117
|
+
await logLine(
|
|
1118
|
+
`compat.version_check_failed exit=${versionResult?.exitCode ?? "unknown"} stderr=${
|
|
1119
|
+
versionResult?.stderr ? redactLog(versionResult.stderr.trim()) : ""
|
|
1120
|
+
}`
|
|
1121
|
+
);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const currentVersion = (versionResult.stdout || "").trim();
|
|
1126
|
+
const parsedCurrent = parseSemver(currentVersion);
|
|
1127
|
+
const parsedMinimum = parseSemver(minVersion);
|
|
1128
|
+
if (!parsedCurrent || !parsedMinimum) {
|
|
1129
|
+
const guidance = resolveUpgradeGuidance({ runner, runnerFrom });
|
|
1130
|
+
await logLine(
|
|
1131
|
+
`compat.version_unparsed current=${redactLog(currentVersion || "")} required=${redactLog(minVersion)}`
|
|
1132
|
+
);
|
|
1133
|
+
await log("warn", "codemem compatibility check could not parse versions", {
|
|
1134
|
+
currentVersion,
|
|
1135
|
+
minVersion,
|
|
1136
|
+
runner,
|
|
1137
|
+
runnerFromSet: Boolean(String(runnerFrom || "").trim()),
|
|
1138
|
+
upgradeMode: guidance.mode,
|
|
1139
|
+
});
|
|
1140
|
+
await showToast(
|
|
1141
|
+
`codemem compatibility check could not parse versions (cli='${currentVersion || "unknown"}', required='${minVersion}'). Suggested action: ${guidance.action}`,
|
|
1142
|
+
"warning"
|
|
1143
|
+
);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (isVersionAtLeast(currentVersion, minVersion)) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const guidance = resolveUpgradeGuidance({ runner, runnerFrom });
|
|
1152
|
+
const message = `codemem CLI ${currentVersion || "unknown"} is older than required ${minVersion}`;
|
|
1153
|
+
await log("warn", message, {
|
|
1154
|
+
currentVersion,
|
|
1155
|
+
minVersion,
|
|
1156
|
+
runner,
|
|
1157
|
+
runnerFromSet: Boolean(String(runnerFrom || "").trim()),
|
|
1158
|
+
upgradeMode: guidance.mode,
|
|
1159
|
+
upgradeAction: guidance.action,
|
|
1160
|
+
});
|
|
1161
|
+
await logLine(
|
|
1162
|
+
`compat.version_mismatch current=${currentVersion} required=${minVersion} mode=${guidance.mode} note=${redactLog(guidance.note)}`
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
const autoPlan = resolveAutoUpdatePlan({ runner, runnerFrom });
|
|
1166
|
+
if (backendUpdatePolicy === "auto") {
|
|
1167
|
+
if (autoPlan.allowed && Array.isArray(autoPlan.command) && autoPlan.command.length > 0) {
|
|
1168
|
+
const commandText = autoPlan.commandText || autoPlan.command.join(" ");
|
|
1169
|
+
await logLine(`compat.auto_update_start cmd=${redactLog(commandText)}`);
|
|
1170
|
+
const updateResult = await runCommand(autoPlan.command);
|
|
1171
|
+
await logLine(
|
|
1172
|
+
`compat.auto_update_result exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog(
|
|
1173
|
+
(updateResult?.stderr || "").trim()
|
|
1174
|
+
)}`
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
const refreshedResult = await runCli(["version"]);
|
|
1178
|
+
const refreshedVersion = (refreshedResult?.stdout || "").trim();
|
|
1179
|
+
if (
|
|
1180
|
+
updateResult?.exitCode === 0
|
|
1181
|
+
&& refreshedResult?.exitCode === 0
|
|
1182
|
+
&& isVersionAtLeast(refreshedVersion, minVersion)
|
|
1183
|
+
) {
|
|
1184
|
+
const viewerRestart = await restartViewerAfterAutoUpdate();
|
|
1185
|
+
await logLine(
|
|
1186
|
+
`compat.auto_update_success before=${currentVersion} after=${refreshedVersion}`
|
|
1187
|
+
);
|
|
1188
|
+
await showToast(
|
|
1189
|
+
`Updated codemem backend from ${currentVersion || "unknown"} to ${refreshedVersion}.`,
|
|
1190
|
+
"success"
|
|
1191
|
+
);
|
|
1192
|
+
if (viewerRestart.attempted && !viewerRestart.ok) {
|
|
1193
|
+
await showToast(
|
|
1194
|
+
"Backend updated, but viewer restart failed. Run `codemem serve --restart`.",
|
|
1195
|
+
"warning"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
await showToast(
|
|
1202
|
+
`${message}. Auto-update did not resolve it. Suggested action: ${guidance.action}`,
|
|
1203
|
+
"warning"
|
|
1204
|
+
);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
await logLine(
|
|
1209
|
+
`compat.auto_update_skipped reason=${autoPlan.reason || "not-eligible"}`
|
|
1210
|
+
);
|
|
1211
|
+
await showToast(
|
|
1212
|
+
`${message}. Auto-update skipped (${autoPlan.reason || "not eligible"}). Suggested action: ${guidance.action}`,
|
|
1213
|
+
"warning"
|
|
1214
|
+
);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
await showToast(`${message}. Suggested action: ${guidance.action}`, "warning");
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const resolveInjectQuery = () => {
|
|
1222
|
+
const parts = [];
|
|
1223
|
+
|
|
1224
|
+
// First prompt captures session intent (most stable signal)
|
|
1225
|
+
if (sessionContext.firstPrompt && sessionContext.firstPrompt.trim()) {
|
|
1226
|
+
parts.push(sessionContext.firstPrompt.trim());
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Latest prompt adds current focus (skip if same as first, or trivial)
|
|
1230
|
+
if (
|
|
1231
|
+
lastPromptText &&
|
|
1232
|
+
lastPromptText.trim() &&
|
|
1233
|
+
lastPromptText.trim() !== (sessionContext.firstPrompt || "").trim() &&
|
|
1234
|
+
lastPromptText.trim().length > 5
|
|
1235
|
+
) {
|
|
1236
|
+
parts.push(lastPromptText.trim());
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Project name for scoping
|
|
1240
|
+
const projectName = resolveProjectName(project);
|
|
1241
|
+
if (projectName) {
|
|
1242
|
+
parts.push(projectName);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Recently modified files signal what area of the codebase we're in
|
|
1246
|
+
if (sessionContext.filesModified.size > 0) {
|
|
1247
|
+
const recentFiles = Array.from(sessionContext.filesModified)
|
|
1248
|
+
.slice(-5)
|
|
1249
|
+
.map((f) => f.split("/").pop())
|
|
1250
|
+
.join(" ");
|
|
1251
|
+
parts.push(recentFiles);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (parts.length === 0) {
|
|
1255
|
+
return "recent work";
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Cap total length to avoid overly long CLI args
|
|
1259
|
+
const query = parts.join(" ");
|
|
1260
|
+
return query.length > 500 ? query.slice(0, 500) : query;
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
const buildPackArgs = (query) => {
|
|
1264
|
+
const workingSetFiles = Array.from(sessionContext.filesModified)
|
|
1265
|
+
.slice(-8)
|
|
1266
|
+
.map((value) => String(value || "").trim())
|
|
1267
|
+
.filter(Boolean);
|
|
1268
|
+
const args = ["pack", query];
|
|
1269
|
+
if (injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0) {
|
|
1270
|
+
args.push("--limit", String(injectLimit));
|
|
1271
|
+
}
|
|
1272
|
+
if (injectTokenBudget !== null && Number.isFinite(injectTokenBudget) && injectTokenBudget > 0) {
|
|
1273
|
+
args.push("--token-budget", String(injectTokenBudget));
|
|
1274
|
+
}
|
|
1275
|
+
appendWorkingSetFileArgs(args, workingSetFiles);
|
|
1276
|
+
return args;
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const parsePackText = (stdout) => {
|
|
1280
|
+
if (!stdout || !stdout.trim()) {
|
|
1281
|
+
return "";
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
const payload = JSON.parse(stdout);
|
|
1285
|
+
return (payload?.pack_text || "").trim();
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
return "";
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const parsePackMetrics = (stdout) => {
|
|
1292
|
+
if (!stdout || !stdout.trim()) {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
const payload = JSON.parse(stdout);
|
|
1297
|
+
return payload?.metrics || null;
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const redactLog = (value, limit = 400) => {
|
|
1304
|
+
if (!value) return "";
|
|
1305
|
+
const masked = String(value).replace(/(Bearer\s+)[^\s]+/gi, "$1[redacted]");
|
|
1306
|
+
return masked.length > limit ? `${masked.slice(0, limit)}…` : masked;
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
const buildInjectedContext = async (query) => {
|
|
1310
|
+
const packArgs = buildPackArgs(query);
|
|
1311
|
+
const result = await runCli(packArgs);
|
|
1312
|
+
if (!result || result.exitCode !== 0) {
|
|
1313
|
+
const exitCode = result?.exitCode ?? "unknown";
|
|
1314
|
+
const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
|
|
1315
|
+
const stdout = redactLog(result?.stdout ? result.stdout.trim() : "");
|
|
1316
|
+
const cmd = [runner, ...runnerArgs, ...packArgs].join(" ");
|
|
1317
|
+
await logLine(
|
|
1318
|
+
`inject.pack.error ${exitCode} cmd=${cmd}` +
|
|
1319
|
+
`${stderr ? ` stderr=${stderr}` : ""}` +
|
|
1320
|
+
`${stdout ? ` stdout=${stdout}` : ""}`
|
|
1321
|
+
);
|
|
1322
|
+
return "";
|
|
1323
|
+
}
|
|
1324
|
+
const packText = parsePackText(result.stdout);
|
|
1325
|
+
if (!packText) {
|
|
1326
|
+
return "";
|
|
1327
|
+
}
|
|
1328
|
+
const metrics = parsePackMetrics(result.stdout);
|
|
1329
|
+
if (metrics) {
|
|
1330
|
+
return {
|
|
1331
|
+
text: `[codemem context]\n${packText}`,
|
|
1332
|
+
metrics,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
return { text: `[codemem context]\n${packText}` };
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const stopViewer = async () => {
|
|
1339
|
+
if (!viewerEnabled || !viewerAutoStop || !viewerStarted) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
viewerStarted = false;
|
|
1343
|
+
await logLine("viewer stop requested");
|
|
1344
|
+
await runCli(["serve", "--stop"]);
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// Get version info (commit hash) for debugging
|
|
1348
|
+
let version = "unknown";
|
|
1349
|
+
try {
|
|
1350
|
+
const gitProc = Bun.spawn({
|
|
1351
|
+
cmd: ["git", "rev-parse", "--short", "HEAD"],
|
|
1352
|
+
cwd: runnerFrom,
|
|
1353
|
+
stdout: "pipe",
|
|
1354
|
+
stderr: "pipe",
|
|
1355
|
+
});
|
|
1356
|
+
const gitResult = await Promise.race([
|
|
1357
|
+
new Response(gitProc.stdout).text(),
|
|
1358
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 500)),
|
|
1359
|
+
]);
|
|
1360
|
+
if (typeof gitResult === "string" && gitResult !== "timeout") {
|
|
1361
|
+
version = gitResult.trim();
|
|
1362
|
+
}
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
// Ignore - version will remain 'unknown'
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
await log("info", "codemem plugin initialized", { cwd, version });
|
|
1368
|
+
await logLine(`plugin initialized cwd=${cwd} version=${version}`);
|
|
1369
|
+
startViewer();
|
|
1370
|
+
void verifyCliCompatibility().catch(async (err) => {
|
|
1371
|
+
await logLine(
|
|
1372
|
+
`compat.version_check_error message=${String(err?.message || err || "unknown")}`
|
|
1373
|
+
);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
const truncate = (value) => {
|
|
1377
|
+
if (value === undefined || value === null) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
const text = String(value);
|
|
1381
|
+
if (Number.isNaN(maxChars) || maxChars <= 0) {
|
|
1382
|
+
return "";
|
|
1383
|
+
}
|
|
1384
|
+
if (text.length <= maxChars) {
|
|
1385
|
+
return text;
|
|
1386
|
+
}
|
|
1387
|
+
return `${text.slice(0, maxChars)}\n[codemem] event truncated\n`;
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
const safeStringify = (value) => {
|
|
1391
|
+
if (value === undefined || value === null) {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
if (typeof value === "string") {
|
|
1395
|
+
return value;
|
|
1396
|
+
}
|
|
1397
|
+
try {
|
|
1398
|
+
return JSON.stringify(value);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
return String(value);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
const recordEvent = (event) => {
|
|
1405
|
+
events.push(event);
|
|
1406
|
+
trimEventQueue({
|
|
1407
|
+
events,
|
|
1408
|
+
maxEvents,
|
|
1409
|
+
hardMaxEvents: Math.max(maxEvents, rawEventsHardMax),
|
|
1410
|
+
onUnsentPressure: (queuedCount, cap) => {
|
|
1411
|
+
void logLine(`queue.pressure unsent_preserved queued=${queuedCount} max_events=${cap}`);
|
|
1412
|
+
},
|
|
1413
|
+
onForcedDrop: (dropped, queuedCount, hardCap) => {
|
|
1414
|
+
void logLine(
|
|
1415
|
+
`queue.drop hard_cap event_id=${dropped?._raw_event_id || "unknown"} queued=${queuedCount} hard_max=${hardCap}`
|
|
1416
|
+
);
|
|
1417
|
+
},
|
|
1418
|
+
});
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
const captureEvent = (sessionID, event) => {
|
|
1422
|
+
const normalizedSessionID =
|
|
1423
|
+
typeof sessionID === "string" && sessionID.trim() ? sessionID.trim() : null;
|
|
1424
|
+
if (normalizedSessionID) {
|
|
1425
|
+
activeSessionID = normalizedSessionID;
|
|
1426
|
+
}
|
|
1427
|
+
const effectiveSessionID = normalizedSessionID || activeSessionID;
|
|
1428
|
+
const resolvedSessionID =
|
|
1429
|
+
effectiveSessionID || `missing:${Date.now()}:${String(nextEventId()).slice(0, 8)}`;
|
|
1430
|
+
if (!effectiveSessionID) {
|
|
1431
|
+
activeSessionID = resolvedSessionID;
|
|
1432
|
+
void logLine(`capture.fallback_session_id ${resolvedSessionID}`);
|
|
1433
|
+
}
|
|
1434
|
+
const adapterAnnotatedEvent = attachAdapterEvent({
|
|
1435
|
+
sessionID: resolvedSessionID,
|
|
1436
|
+
event,
|
|
1437
|
+
});
|
|
1438
|
+
const rawEventId =
|
|
1439
|
+
adapterAnnotatedEvent?._adapter?.event_id ||
|
|
1440
|
+
(adapterAnnotatedEvent && adapterAnnotatedEvent._raw_event_id) ||
|
|
1441
|
+
nextEventId();
|
|
1442
|
+
const queuedEvent = {
|
|
1443
|
+
...adapterAnnotatedEvent,
|
|
1444
|
+
_raw_event_id: rawEventId,
|
|
1445
|
+
_raw_session_id: resolvedSessionID,
|
|
1446
|
+
_raw_retry_count: 0,
|
|
1447
|
+
};
|
|
1448
|
+
recordEvent(queuedEvent);
|
|
1449
|
+
void emitRawEvent({
|
|
1450
|
+
sessionID: resolvedSessionID,
|
|
1451
|
+
type: queuedEvent?.type || "unknown",
|
|
1452
|
+
payload: queuedEvent,
|
|
1453
|
+
});
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
const flushEvents = async () => {
|
|
1457
|
+
if (!events.length) {
|
|
1458
|
+
await logLine("flush.skip empty");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const batch = events.splice(0, events.length);
|
|
1463
|
+
if (!batch.length) {
|
|
1464
|
+
await logLine("flush.skip empty");
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const failed = [];
|
|
1469
|
+
for (const queuedEvent of batch) {
|
|
1470
|
+
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_enqueued) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
const queuedSessionID =
|
|
1474
|
+
queuedEvent?._raw_session_id ||
|
|
1475
|
+
queuedEvent?.properties?.sessionID ||
|
|
1476
|
+
null;
|
|
1477
|
+
const ok = await emitRawEvent({
|
|
1478
|
+
sessionID: queuedSessionID,
|
|
1479
|
+
type: queuedEvent?.type || "unknown",
|
|
1480
|
+
payload: queuedEvent,
|
|
1481
|
+
});
|
|
1482
|
+
if (!ok) {
|
|
1483
|
+
const currentRetry =
|
|
1484
|
+
typeof queuedEvent?._raw_retry_count === "number" && Number.isFinite(queuedEvent._raw_retry_count)
|
|
1485
|
+
? queuedEvent._raw_retry_count
|
|
1486
|
+
: 0;
|
|
1487
|
+
const nextRetry = currentRetry + 1;
|
|
1488
|
+
failed.push({
|
|
1489
|
+
...queuedEvent,
|
|
1490
|
+
_raw_retry_count: nextRetry,
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (failed.length) {
|
|
1495
|
+
events.unshift(...failed);
|
|
1496
|
+
await logLine(`flush.retry_deferred count=${failed.length}`);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Calculate session duration
|
|
1501
|
+
const durationMs = sessionContext.startTime
|
|
1502
|
+
? Date.now() - sessionContext.startTime
|
|
1503
|
+
: 0;
|
|
1504
|
+
await logLine(
|
|
1505
|
+
`flush.stream_only finalize count=${batch.length} tools=${sessionContext.toolCount} prompts=${sessionContext.promptCount} duration=${Math.round(durationMs / 1000)}s`
|
|
1506
|
+
);
|
|
1507
|
+
await logLine(`flush.ok count=${batch.length}`);
|
|
1508
|
+
sessionStartedAt = null;
|
|
1509
|
+
resetSessionContext();
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
return {
|
|
1513
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
1514
|
+
if (!injectEnabled) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const query = resolveInjectQuery();
|
|
1518
|
+
if (debug) {
|
|
1519
|
+
await logLine(
|
|
1520
|
+
`inject.transform sessionID=${input.sessionID} query_len=${
|
|
1521
|
+
query ? query.length : 0
|
|
1522
|
+
} tui_toast=${Boolean(client.tui?.showToast)}`
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const cached = injectedSessions.get(input.sessionID);
|
|
1526
|
+
let contextText = cached?.text || "";
|
|
1527
|
+
if (!contextText || cached?.query !== query) {
|
|
1528
|
+
const injected = await buildInjectedContext(query);
|
|
1529
|
+
if (injected?.text) {
|
|
1530
|
+
injectedSessions.set(input.sessionID, {
|
|
1531
|
+
query,
|
|
1532
|
+
text: injected.text,
|
|
1533
|
+
metrics: injected.metrics || null,
|
|
1534
|
+
});
|
|
1535
|
+
contextText = injected.text;
|
|
1536
|
+
|
|
1537
|
+
if (!injectionToastShown.has(input.sessionID) && client.tui?.showToast) {
|
|
1538
|
+
injectionToastShown.add(input.sessionID);
|
|
1539
|
+
try {
|
|
1540
|
+
await client.tui.showToast({
|
|
1541
|
+
body: {
|
|
1542
|
+
message: buildInjectionToastMessage(injected.metrics),
|
|
1543
|
+
variant: "info",
|
|
1544
|
+
},
|
|
1545
|
+
});
|
|
1546
|
+
} catch (toastErr) {
|
|
1547
|
+
// best-effort only
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (!contextText) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (!Array.isArray(output.system)) {
|
|
1556
|
+
output.system = [];
|
|
1557
|
+
}
|
|
1558
|
+
output.system.push(contextText);
|
|
1559
|
+
},
|
|
1560
|
+
event: async ({ event }) => {
|
|
1561
|
+
const eventType = event?.type || "unknown";
|
|
1562
|
+
const sessionID = extractSessionID(event);
|
|
1563
|
+
|
|
1564
|
+
// Always log session-related events for debugging /new
|
|
1565
|
+
if (eventType.startsWith("session.")) {
|
|
1566
|
+
await logLine(`SESSION EVENT: ${eventType}`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (debugExtraction) {
|
|
1570
|
+
await logLine(`event ${eventType}`);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Debug: log event structure for message events (only when debug enabled)
|
|
1574
|
+
if (
|
|
1575
|
+
debugExtraction &&
|
|
1576
|
+
[
|
|
1577
|
+
"message.updated",
|
|
1578
|
+
"message.created",
|
|
1579
|
+
"message.appended",
|
|
1580
|
+
"message.part.updated",
|
|
1581
|
+
].includes(eventType)
|
|
1582
|
+
) {
|
|
1583
|
+
// Log full event structure for debugging (only first few times per event type)
|
|
1584
|
+
if (!global.eventLogCount) global.eventLogCount = {};
|
|
1585
|
+
if (!global.eventLogCount[eventType])
|
|
1586
|
+
global.eventLogCount[eventType] = 0;
|
|
1587
|
+
if (global.eventLogCount[eventType] < 2) {
|
|
1588
|
+
global.eventLogCount[eventType]++;
|
|
1589
|
+
await logLine(
|
|
1590
|
+
`FULL EVENT (${eventType}): ${JSON.stringify(
|
|
1591
|
+
event,
|
|
1592
|
+
null,
|
|
1593
|
+
2
|
|
1594
|
+
).substring(0, 3000)}`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
await logLine(
|
|
1599
|
+
`event payload keys: ${Object.keys(event || {}).join(", ")}`
|
|
1600
|
+
);
|
|
1601
|
+
if (event?.properties) {
|
|
1602
|
+
await logLine(
|
|
1603
|
+
`event properties keys: ${Object.keys(event.properties).join(", ")}`
|
|
1604
|
+
);
|
|
1605
|
+
if (event.properties.role) {
|
|
1606
|
+
await logLine(`event role: ${event.properties.role}`);
|
|
1607
|
+
}
|
|
1608
|
+
if (event.properties.message) {
|
|
1609
|
+
await logLine(`event has properties.message`);
|
|
1610
|
+
}
|
|
1611
|
+
if (event.properties.info) {
|
|
1612
|
+
const infoKeys = Object.keys(event.properties.info);
|
|
1613
|
+
await logLine(`event properties.info keys: ${infoKeys.join(", ")}`);
|
|
1614
|
+
if (event.properties.info.role) {
|
|
1615
|
+
await logLine(`event info.role: ${event.properties.info.role}`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (
|
|
1622
|
+
[
|
|
1623
|
+
"message.updated",
|
|
1624
|
+
"message.created",
|
|
1625
|
+
"message.appended",
|
|
1626
|
+
"message.part.updated",
|
|
1627
|
+
].includes(eventType)
|
|
1628
|
+
) {
|
|
1629
|
+
const promptText = extractPromptText(event);
|
|
1630
|
+
if (promptText) {
|
|
1631
|
+
// Update activity tracking
|
|
1632
|
+
updateActivity();
|
|
1633
|
+
|
|
1634
|
+
// Track session context
|
|
1635
|
+
if (!sessionContext.firstPrompt) {
|
|
1636
|
+
sessionContext.firstPrompt = promptText;
|
|
1637
|
+
sessionContext.startTime = Date.now();
|
|
1638
|
+
}
|
|
1639
|
+
sessionContext.promptCount++;
|
|
1640
|
+
|
|
1641
|
+
// Check for /new command and flush before session reset
|
|
1642
|
+
if (
|
|
1643
|
+
promptText.trim() === "/new" ||
|
|
1644
|
+
promptText.trim().startsWith("/new ")
|
|
1645
|
+
) {
|
|
1646
|
+
await logLine("detected /new command, flushing events");
|
|
1647
|
+
await flushEvents();
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (promptText !== lastPromptText) {
|
|
1651
|
+
promptCounter += 1;
|
|
1652
|
+
// promptCount incremented when capturing user_prompt
|
|
1653
|
+
|
|
1654
|
+
lastPromptText = promptText;
|
|
1655
|
+
captureEvent(sessionID, {
|
|
1656
|
+
type: "user_prompt",
|
|
1657
|
+
prompt_number: promptCounter,
|
|
1658
|
+
prompt_text: promptText,
|
|
1659
|
+
timestamp: new Date().toISOString(),
|
|
1660
|
+
});
|
|
1661
|
+
await logLine(
|
|
1662
|
+
`user_prompt captured #${promptCounter}: ${promptText.substring(
|
|
1663
|
+
0,
|
|
1664
|
+
50
|
|
1665
|
+
)}`
|
|
1666
|
+
);
|
|
1667
|
+
|
|
1668
|
+
// Check if we should force flush due to threshold
|
|
1669
|
+
if (shouldForceFlush()) {
|
|
1670
|
+
await logLine(`force flush triggered: tools=${sessionContext.toolCount}, prompts=${sessionContext.promptCount}, duration=${Math.round((Date.now() - (sessionContext.startTime || Date.now())) / 1000)}s`);
|
|
1671
|
+
await flushEvents();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const assistantText = extractAssistantText(event);
|
|
1677
|
+
if (assistantText && assistantText !== lastAssistantText) {
|
|
1678
|
+
updateActivity();
|
|
1679
|
+
lastAssistantText = assistantText;
|
|
1680
|
+
captureEvent(sessionID, {
|
|
1681
|
+
type: "assistant_message",
|
|
1682
|
+
assistant_text: assistantText,
|
|
1683
|
+
timestamp: new Date().toISOString(),
|
|
1684
|
+
});
|
|
1685
|
+
await logLine(
|
|
1686
|
+
`assistant_message captured: ${assistantText.substring(0, 50)}`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const assistantUsage = extractAssistantUsage(event);
|
|
1691
|
+
if (assistantUsage) {
|
|
1692
|
+
updateActivity();
|
|
1693
|
+
captureEvent(sessionID, {
|
|
1694
|
+
type: "assistant_usage",
|
|
1695
|
+
message_id: assistantUsage.id,
|
|
1696
|
+
usage: assistantUsage.usage,
|
|
1697
|
+
timestamp: new Date().toISOString(),
|
|
1698
|
+
});
|
|
1699
|
+
await logLine(
|
|
1700
|
+
`assistant_usage captured id=${assistantUsage.id.slice(-8)}`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// NEW ACCUMULATION STRATEGY
|
|
1706
|
+
// Only flush on:
|
|
1707
|
+
// - session.error (immediate error boundary)
|
|
1708
|
+
// - session.idle AFTER delay (scheduled via timeout)
|
|
1709
|
+
// - /new command (handled above)
|
|
1710
|
+
// - session.created (session boundary)
|
|
1711
|
+
//
|
|
1712
|
+
// REMOVED: session.compacted, session.compacting (too frequent)
|
|
1713
|
+
if (eventType === "session.error") {
|
|
1714
|
+
await logLine("session.error detected, flushing immediately");
|
|
1715
|
+
await flushEvents();
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
if (eventType === "session.idle") {
|
|
1719
|
+
await logLine(
|
|
1720
|
+
`session.idle detected, flushing immediately (tools=${sessionContext.toolCount}, prompts=${sessionContext.promptCount})`
|
|
1721
|
+
);
|
|
1722
|
+
await flushEvents();
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (eventType === "session.created") {
|
|
1726
|
+
if (events.length) {
|
|
1727
|
+
await flushEvents();
|
|
1728
|
+
}
|
|
1729
|
+
activeSessionID = sessionID || null;
|
|
1730
|
+
sessionStartedAt = new Date().toISOString();
|
|
1731
|
+
promptCounter = 0;
|
|
1732
|
+
lastPromptText = null;
|
|
1733
|
+
lastAssistantText = null;
|
|
1734
|
+
resetSessionContext();
|
|
1735
|
+
startViewer();
|
|
1736
|
+
}
|
|
1737
|
+
if (eventType === "session.deleted") {
|
|
1738
|
+
activeSessionID = null;
|
|
1739
|
+
await stopViewer();
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
"tool.execute.after": async (input, output) => {
|
|
1743
|
+
const args = output?.args ?? input?.args ?? {};
|
|
1744
|
+
const result = output?.result ?? output?.output ?? output?.data ?? null;
|
|
1745
|
+
const error = output?.error ?? null;
|
|
1746
|
+
const toolName = input?.tool || output?.tool || "unknown";
|
|
1747
|
+
|
|
1748
|
+
// Update activity and session context
|
|
1749
|
+
updateActivity();
|
|
1750
|
+
sessionContext.toolCount++;
|
|
1751
|
+
|
|
1752
|
+
// Track files from tool events
|
|
1753
|
+
const filePath = args.filePath || args.path;
|
|
1754
|
+
if (filePath) {
|
|
1755
|
+
const lowerTool = toolName.toLowerCase();
|
|
1756
|
+
if (lowerTool === "edit" || lowerTool === "write") {
|
|
1757
|
+
sessionContext.filesModified.add(filePath);
|
|
1758
|
+
} else if (lowerTool === "read") {
|
|
1759
|
+
sessionContext.filesRead.add(filePath);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (toolName.toLowerCase() === "apply_patch") {
|
|
1763
|
+
const patchPaths = extractApplyPatchPaths(args.patchText);
|
|
1764
|
+
for (const path of patchPaths) {
|
|
1765
|
+
sessionContext.filesModified.add(path);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
captureEvent(input?.sessionID || null, {
|
|
1770
|
+
type: "tool.execute.after",
|
|
1771
|
+
tool: toolName,
|
|
1772
|
+
args,
|
|
1773
|
+
result: truncate(safeStringify(result)),
|
|
1774
|
+
error: truncate(safeStringify(error)),
|
|
1775
|
+
timestamp: new Date().toISOString(),
|
|
1776
|
+
});
|
|
1777
|
+
await logLine(`tool.execute.after ${toolName} queued=${events.length} tools=${sessionContext.toolCount}`);
|
|
1778
|
+
|
|
1779
|
+
// Check if we should force flush due to threshold
|
|
1780
|
+
if (shouldForceFlush()) {
|
|
1781
|
+
await logLine(`force flush triggered: tools=${sessionContext.toolCount}, prompts=${sessionContext.promptCount}, duration=${Math.round((Date.now() - (sessionContext.startTime || Date.now())) / 1000)}s`);
|
|
1782
|
+
await flushEvents();
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
tool: {
|
|
1786
|
+
"mem-status": tool({
|
|
1787
|
+
description: "Show codemem stats and recent entries",
|
|
1788
|
+
args: {},
|
|
1789
|
+
async execute() {
|
|
1790
|
+
const stats = await runCli(["stats"]);
|
|
1791
|
+
const recent = await runCli(["recent", "--limit", "5"]);
|
|
1792
|
+
const lines = [
|
|
1793
|
+
`viewer: http://${viewerHost}:${viewerPort}`,
|
|
1794
|
+
`log: ${logPath || "disabled"}`,
|
|
1795
|
+
];
|
|
1796
|
+
if (stats.exitCode === 0 && stats.stdout.trim()) {
|
|
1797
|
+
lines.push("", "stats:", stats.stdout.trim());
|
|
1798
|
+
}
|
|
1799
|
+
if (recent.exitCode === 0 && recent.stdout.trim()) {
|
|
1800
|
+
lines.push("", "recent:", recent.stdout.trim());
|
|
1801
|
+
}
|
|
1802
|
+
return lines.join("\n");
|
|
1803
|
+
},
|
|
1804
|
+
}),
|
|
1805
|
+
|
|
1806
|
+
"mem-recent": tool({
|
|
1807
|
+
description: "Show recent codemem entries",
|
|
1808
|
+
args: {
|
|
1809
|
+
limit: tool.schema.number().optional(),
|
|
1810
|
+
},
|
|
1811
|
+
async execute({ limit }) {
|
|
1812
|
+
const safeLimit = Number.isFinite(limit) ? String(limit) : "5";
|
|
1813
|
+
const recent = await runCli(["recent", "--limit", safeLimit]);
|
|
1814
|
+
if (recent.exitCode === 0) {
|
|
1815
|
+
return recent.stdout.trim() || "No recent memories.";
|
|
1816
|
+
}
|
|
1817
|
+
return `Failed to fetch recent: ${recent.stderr || recent.exitCode}`;
|
|
1818
|
+
},
|
|
1819
|
+
}),
|
|
1820
|
+
|
|
1821
|
+
"mem-stats": tool({
|
|
1822
|
+
description: "Show codemem stats",
|
|
1823
|
+
args: {},
|
|
1824
|
+
async execute() {
|
|
1825
|
+
const stats = await runCli(["stats"]);
|
|
1826
|
+
if (stats.exitCode === 0) {
|
|
1827
|
+
return stats.stdout.trim() || "No stats yet.";
|
|
1828
|
+
}
|
|
1829
|
+
return `Failed to fetch stats: ${stats.stderr || stats.exitCode}`;
|
|
1830
|
+
},
|
|
1831
|
+
}),
|
|
1832
|
+
},
|
|
1833
|
+
};
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
export default OpencodeMemPlugin;
|
|
1837
|
+
export const __testUtils = {
|
|
1838
|
+
PINNED_BACKEND_VERSION,
|
|
1839
|
+
DEFAULT_UVX_SOURCE,
|
|
1840
|
+
buildRunnerArgs,
|
|
1841
|
+
appendWorkingSetFileArgs,
|
|
1842
|
+
extractApplyPatchPaths,
|
|
1843
|
+
mapOpencodeEventTypeToAdapterType,
|
|
1844
|
+
buildOpencodeAdapterPayload,
|
|
1845
|
+
buildOpencodeAdapterEvent,
|
|
1846
|
+
attachAdapterEvent,
|
|
1847
|
+
selectRawEventId,
|
|
1848
|
+
buildRawEventEnvelope,
|
|
1849
|
+
trimEventQueue,
|
|
1850
|
+
parsePositiveInt,
|
|
1851
|
+
};
|