@p4r4d0xb0x/opencode-provider-logger 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/opencode/hooks.d.ts +11 -3
- package/dist/adapters/opencode/index.d.ts +2 -2
- package/dist/index.js +174 -121
- package/package.json +5 -6
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import type { PluginConfig, SessionBuffer } from "@p4r4d0xb0x/provider-logger-core";
|
|
3
|
+
interface Disposable {
|
|
4
|
+
dispose(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
interface HookRuntime {
|
|
7
|
+
registrations: Disposable[];
|
|
8
|
+
eventLoop: Promise<void>;
|
|
9
|
+
}
|
|
3
10
|
type FlushFn = (sessionID: string) => Promise<void>;
|
|
4
11
|
type LogFn = (level: "error" | "warn", message: string, extra?: Record<string, unknown>) => void;
|
|
5
|
-
export declare function
|
|
12
|
+
export declare function sanitizeForLog(value: unknown, seen?: WeakSet<object>): unknown;
|
|
13
|
+
export declare function registerHooks(context: Plugin.Context, signal: AbortSignal, buffer: SessionBuffer, config: PluginConfig, flush: FlushFn, log?: LogFn): Promise<HookRuntime>;
|
|
6
14
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
declare const providerLogger: Plugin;
|
|
1
|
+
import { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
declare const providerLogger: Plugin.Plugin;
|
|
3
3
|
export default providerLogger;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/adapters/opencode/index.ts
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { Plugin } from "@opencode-ai/plugin";
|
|
4
|
+
|
|
1
5
|
// ../provider-logger-core/dist/types.js
|
|
2
6
|
import { join } from "node:path";
|
|
3
7
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
@@ -256,157 +260,206 @@ function makeEntry(platform, sessionID, category, hookName, input, output, meta)
|
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
// src/adapters/opencode/hooks.ts
|
|
259
|
-
var
|
|
260
|
-
"
|
|
261
|
-
"session.
|
|
262
|
-
"session.
|
|
263
|
-
"session.created",
|
|
264
|
-
"session.compacted"
|
|
265
|
-
]);
|
|
266
|
-
var HOOK_CATEGORY_MAP = {
|
|
267
|
-
"chat.message": "message.updated",
|
|
268
|
-
"chat.params": "prompt.params",
|
|
269
|
-
"experimental.chat.system.transform": "prompt.system",
|
|
270
|
-
"experimental.chat.messages.transform": "prompt.system",
|
|
271
|
-
"experimental.text.complete": "message.assistant",
|
|
263
|
+
var HOOK_CATEGORIES = {
|
|
264
|
+
"session.prompt": "prompt.user",
|
|
265
|
+
"session.context": "prompt.system",
|
|
266
|
+
"session.model.request": "prompt.params",
|
|
272
267
|
"tool.execute.before": "tool.pre",
|
|
273
268
|
"tool.execute.after": "tool.post",
|
|
274
269
|
event: "raw"
|
|
275
270
|
};
|
|
276
|
-
var
|
|
277
|
-
"message.updated": "message.updated",
|
|
278
|
-
"session.idle": "session.idle",
|
|
279
|
-
"session.status": "session.status",
|
|
271
|
+
var EVENT_CATEGORIES = {
|
|
280
272
|
"session.created": "session.created",
|
|
281
|
-
"session.
|
|
273
|
+
"session.status": "session.status",
|
|
274
|
+
"session.idle": "session.idle",
|
|
275
|
+
"session.step.ended": "message.assistant",
|
|
276
|
+
"session.compaction.ended": "session.compact"
|
|
282
277
|
};
|
|
283
|
-
|
|
284
|
-
|
|
278
|
+
var SENSITIVE_KEY = /authorization|api[-_]?key|token|secret|cookie|credential/i;
|
|
279
|
+
function sanitizeForLog(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
280
|
+
if (value === null || typeof value !== "object") return value;
|
|
281
|
+
if (value instanceof Request) {
|
|
282
|
+
return {
|
|
283
|
+
method: value.method,
|
|
284
|
+
url: value.url,
|
|
285
|
+
headers: sanitizeForLog(Object.fromEntries(value.headers), seen)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (value instanceof Response) {
|
|
289
|
+
return {
|
|
290
|
+
status: value.status,
|
|
291
|
+
statusText: value.statusText,
|
|
292
|
+
headers: sanitizeForLog(Object.fromEntries(value.headers), seen)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (value instanceof Error) return { name: value.name, message: value.message };
|
|
296
|
+
if (seen.has(value)) return "[Circular]";
|
|
297
|
+
seen.add(value);
|
|
298
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeForLog(item, seen));
|
|
299
|
+
return Object.fromEntries(
|
|
300
|
+
Object.entries(value).map(([key, item]) => [
|
|
301
|
+
key,
|
|
302
|
+
SENSITIVE_KEY.test(key) ? "[REDACTED]" : sanitizeForLog(item, seen)
|
|
303
|
+
])
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
function makeRecorder(buffer, config, flush, log) {
|
|
307
|
+
return (sessionID, hook, input, output, category = HOOK_CATEGORIES[hook]) => {
|
|
285
308
|
try {
|
|
286
|
-
const category = categoryOverride ?? HOOK_CATEGORY_MAP[hook];
|
|
287
309
|
const entry = makeEntry(
|
|
288
310
|
"opencode",
|
|
289
311
|
sessionID,
|
|
290
312
|
category,
|
|
291
313
|
hook,
|
|
292
|
-
input,
|
|
293
|
-
output
|
|
314
|
+
sanitizeForLog(input),
|
|
315
|
+
sanitizeForLog(output)
|
|
294
316
|
);
|
|
295
317
|
buffer.append(entry);
|
|
296
318
|
if (buffer.sizeOf(sessionID) >= config.flushThresholdBytes) {
|
|
297
|
-
flush(sessionID)
|
|
298
|
-
});
|
|
319
|
+
void flush(sessionID);
|
|
299
320
|
}
|
|
300
|
-
} catch (
|
|
301
|
-
log?.("error", `Hook record failed [${hook}]`, { error: String(
|
|
321
|
+
} catch (error) {
|
|
322
|
+
log?.("error", `Hook record failed [${hook}]`, { error: String(error) });
|
|
302
323
|
}
|
|
303
324
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
sessionID,
|
|
320
|
-
"experimental.chat.messages.transform",
|
|
321
|
-
input,
|
|
322
|
-
output
|
|
323
|
-
);
|
|
324
|
-
},
|
|
325
|
-
"experimental.text.complete": async (input, output) => {
|
|
326
|
-
safeRecord(input.sessionID, "experimental.text.complete", input, output);
|
|
327
|
-
},
|
|
328
|
-
"tool.execute.before": async (input, output) => {
|
|
329
|
-
safeRecord(input.sessionID, "tool.execute.before", input, output);
|
|
330
|
-
},
|
|
331
|
-
"tool.execute.after": async (input, output) => {
|
|
332
|
-
safeRecord(input.sessionID, "tool.execute.after", input, output);
|
|
333
|
-
},
|
|
334
|
-
event: async ({ event }) => {
|
|
335
|
-
try {
|
|
336
|
-
const ev = event;
|
|
337
|
-
if (!LOG_EVENT_TYPES.has(ev.type)) return;
|
|
338
|
-
const sessionID = ev.properties?.sessionID ?? ev.properties?.info?.sessionID;
|
|
339
|
-
if (!sessionID) return;
|
|
340
|
-
const category = EVENT_TYPE_CATEGORY_MAP[ev.type] ?? "raw";
|
|
341
|
-
safeRecord(sessionID, "event", { type: ev.type }, ev.properties, category);
|
|
342
|
-
if (ev.type === "session.idle") {
|
|
343
|
-
await flush(sessionID);
|
|
344
|
-
}
|
|
345
|
-
} catch (err) {
|
|
346
|
-
log?.("error", "Event hook failed", { error: String(err) });
|
|
347
|
-
}
|
|
325
|
+
}
|
|
326
|
+
function sessionIDFromEvent(event) {
|
|
327
|
+
if (!event.data || typeof event.data !== "object") return void 0;
|
|
328
|
+
const sessionID = event.data.sessionID;
|
|
329
|
+
return typeof sessionID === "string" ? sessionID : void 0;
|
|
330
|
+
}
|
|
331
|
+
async function consumeEvents(context, signal, record, flush, log) {
|
|
332
|
+
try {
|
|
333
|
+
for await (const event of context.event.subscribe({ signal })) {
|
|
334
|
+
const category = EVENT_CATEGORIES[event.type];
|
|
335
|
+
if (!category) continue;
|
|
336
|
+
const sessionID = sessionIDFromEvent(event);
|
|
337
|
+
if (!sessionID) continue;
|
|
338
|
+
record(sessionID, "event", { type: event.type }, event.data, category);
|
|
339
|
+
if (event.type === "session.idle") await flush(sessionID);
|
|
348
340
|
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (!signal.aborted) log?.("error", "Event subscription failed", { error: String(error) });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function registerHooks(context, signal, buffer, config, flush, log) {
|
|
346
|
+
const record = makeRecorder(buffer, config, flush, log);
|
|
347
|
+
const registrations = [];
|
|
348
|
+
try {
|
|
349
|
+
registrations.push(await context.session.hook("prompt", (event) => {
|
|
350
|
+
record(event.sessionID, "session.prompt", event, event.prompt);
|
|
351
|
+
}));
|
|
352
|
+
registrations.push(await context.session.hook("context", (event) => {
|
|
353
|
+
record(event.sessionID, "session.context", {
|
|
354
|
+
sessionID: event.sessionID,
|
|
355
|
+
agent: event.agent,
|
|
356
|
+
model: event.model
|
|
357
|
+
}, event);
|
|
358
|
+
}));
|
|
359
|
+
registrations.push(await context.session.hook("model.request", (event) => {
|
|
360
|
+
record(event.sessionID, "session.model.request", {
|
|
361
|
+
sessionID: event.sessionID,
|
|
362
|
+
agent: event.agent,
|
|
363
|
+
model: event.model
|
|
364
|
+
}, { baseURL: event.baseURL, headers: event.headers });
|
|
365
|
+
}));
|
|
366
|
+
registrations.push(await context.tool.hook("execute.before", (event) => {
|
|
367
|
+
record(event.sessionID, "tool.execute.before", event, event.input);
|
|
368
|
+
}));
|
|
369
|
+
registrations.push(await context.tool.hook("execute.after", (event) => {
|
|
370
|
+
const category = event.status === "error" ? "tool.error" : "tool.post";
|
|
371
|
+
record(event.sessionID, "tool.execute.after", event, event, category);
|
|
372
|
+
}));
|
|
373
|
+
} catch (error) {
|
|
374
|
+
await Promise.allSettled(registrations.map((registration) => registration.dispose()));
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
registrations,
|
|
379
|
+
eventLoop: consumeEvents(context, signal, record, flush, log)
|
|
349
380
|
};
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
// src/adapters/opencode/index.ts
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
});
|
|
384
|
+
function createLogger() {
|
|
385
|
+
return (level, message, extra) => {
|
|
386
|
+
const detail = extra ? ` ${JSON.stringify(extra)}` : "";
|
|
387
|
+
const line = `[provider-logger] ${message}${detail}`;
|
|
388
|
+
if (level === "error") console.error(line);
|
|
389
|
+
else if (level === "warn") console.warn(line);
|
|
390
|
+
else if (level === "debug") console.debug(line);
|
|
391
|
+
else console.info(line);
|
|
362
392
|
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
393
|
+
}
|
|
394
|
+
async function persistEntries(sessionID, entries, writer, uploader) {
|
|
395
|
+
if (entries.length === 0) return;
|
|
396
|
+
const filepath = await writer.write(sessionID, entries);
|
|
397
|
+
if (!filepath) return;
|
|
398
|
+
await uploader.upload({
|
|
399
|
+
path: filepath,
|
|
400
|
+
sessionID,
|
|
401
|
+
tsNanos: basename(filepath, ".jsonl")
|
|
369
402
|
});
|
|
370
|
-
|
|
403
|
+
}
|
|
404
|
+
function makeFlush(buffer, writer, uploader, log) {
|
|
405
|
+
return async (sessionID) => {
|
|
371
406
|
if (!buffer.has(sessionID)) return;
|
|
372
|
-
const entries = buffer.flush(sessionID);
|
|
373
407
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
await uploader.upload({ path: filepath, sessionID, tsNanos });
|
|
378
|
-
}
|
|
379
|
-
} catch (err) {
|
|
380
|
-
log("error", `Flush failed for session ${sessionID}`, {
|
|
381
|
-
error: String(err)
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
const flushAll = async () => {
|
|
386
|
-
const all = buffer.flushAll();
|
|
387
|
-
const writes = [];
|
|
388
|
-
for (const [sessionID, entries] of all) {
|
|
389
|
-
if (entries.length === 0) continue;
|
|
390
|
-
writes.push(
|
|
391
|
-
writer.write(sessionID, entries).then(async (filepath) => {
|
|
392
|
-
if (filepath) {
|
|
393
|
-
const tsNanos = basename(filepath, ".jsonl");
|
|
394
|
-
await uploader.upload({ path: filepath, sessionID, tsNanos });
|
|
395
|
-
}
|
|
396
|
-
}).catch((err) => {
|
|
397
|
-
log("error", `Shutdown flush failed for ${sessionID}`, { error: String(err) });
|
|
398
|
-
})
|
|
399
|
-
);
|
|
408
|
+
await persistEntries(sessionID, buffer.flush(sessionID), writer, uploader);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
log("error", `Flush failed for session ${sessionID}`, { error: String(error) });
|
|
400
411
|
}
|
|
401
|
-
await Promise.allSettled(writes);
|
|
402
412
|
};
|
|
403
|
-
|
|
404
|
-
|
|
413
|
+
}
|
|
414
|
+
async function flushAll(buffer, writer, uploader, log) {
|
|
415
|
+
const writes = Array.from(
|
|
416
|
+
buffer.flushAll(),
|
|
417
|
+
([sessionID, entries]) => persistEntries(sessionID, entries, writer, uploader).catch((error) => {
|
|
418
|
+
log("error", `Shutdown flush failed for ${sessionID}`, { error: String(error) });
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
await Promise.allSettled(writes);
|
|
422
|
+
}
|
|
423
|
+
var providerLogger = Plugin.define({
|
|
424
|
+
id: "opencode-provider-logger",
|
|
425
|
+
async setup(context) {
|
|
426
|
+
const config = defaultConfig("opencode");
|
|
427
|
+
const buffer = new SessionBuffer();
|
|
428
|
+
const writer = new LocalWriter(config);
|
|
429
|
+
const log = createLogger();
|
|
430
|
+
const uploader = new WorkerUploader(config, writer, log);
|
|
431
|
+
const flush = makeFlush(buffer, writer, uploader, log);
|
|
432
|
+
void uploader.retryPending().catch((error) => {
|
|
433
|
+
log("warn", "Failed to retry pending uploads", { error: String(error) });
|
|
405
434
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
435
|
+
void uploader.cleanup().catch((error) => {
|
|
436
|
+
log("warn", "Failed to cleanup old files", { error: String(error) });
|
|
437
|
+
});
|
|
438
|
+
const abortController = new AbortController();
|
|
439
|
+
const runtime = await registerHooks(
|
|
440
|
+
context,
|
|
441
|
+
abortController.signal,
|
|
442
|
+
buffer,
|
|
443
|
+
config,
|
|
444
|
+
flush,
|
|
445
|
+
log
|
|
446
|
+
);
|
|
447
|
+
const flushOnBeforeExit = () => {
|
|
448
|
+
void flushAll(buffer, writer, uploader, log);
|
|
449
|
+
};
|
|
450
|
+
process.on("beforeExit", flushOnBeforeExit);
|
|
451
|
+
log("info", "Provider logger initialized");
|
|
452
|
+
return async () => {
|
|
453
|
+
process.off("beforeExit", flushOnBeforeExit);
|
|
454
|
+
abortController.abort();
|
|
455
|
+
await Promise.allSettled([
|
|
456
|
+
...runtime.registrations.map((registration) => registration.dispose()),
|
|
457
|
+
runtime.eventLoop
|
|
458
|
+
]);
|
|
459
|
+
await flushAll(buffer, writer, uploader, log);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
});
|
|
410
463
|
var opencode_default = providerLogger;
|
|
411
464
|
export {
|
|
412
465
|
opencode_default as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@p4r4d0xb0x/opencode-provider-logger",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Provider logger plugin for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,16 +13,15 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": ["dist"],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@opencode-ai/plugin
|
|
17
|
-
"test": "
|
|
16
|
+
"build": "tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@opencode-ai/plugin",
|
|
17
|
+
"test": "node --test 'test/*.test.mjs'",
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@opencode-ai/plugin": "
|
|
21
|
+
"@opencode-ai/plugin": "0.0.0-beta-18992"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@opencode-ai/plugin": "
|
|
25
|
-
"@opencode-ai/sdk": "1.3.0",
|
|
24
|
+
"@opencode-ai/plugin": "0.0.0-beta-18992",
|
|
26
25
|
"@p4r4d0xb0x/provider-logger-core": "*",
|
|
27
26
|
"@types/bun": "^1.3.11",
|
|
28
27
|
"@types/node": "^25.5.0",
|