@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.
@@ -1,6 +1,14 @@
1
- import type { Hooks } from "@opencode-ai/plugin";
2
- import type { SessionBuffer, PluginConfig } from "@p4r4d0xb0x/provider-logger-core";
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 createHooks(buffer: SessionBuffer, config: PluginConfig, flush: FlushFn, log?: LogFn): Hooks;
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 type { Plugin } from "@opencode-ai/plugin";
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 LOG_EVENT_TYPES = /* @__PURE__ */ new Set([
260
- "message.updated",
261
- "session.idle",
262
- "session.status",
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 EVENT_TYPE_CATEGORY_MAP = {
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.compacted": "session.compact"
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
- function createHooks(buffer, config, flush, log) {
284
- const safeRecord = (sessionID, hook, input, output, categoryOverride) => {
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).catch(() => {
298
- });
319
+ void flush(sessionID);
299
320
  }
300
- } catch (err) {
301
- log?.("error", `Hook record failed [${hook}]`, { error: String(err) });
321
+ } catch (error) {
322
+ log?.("error", `Hook record failed [${hook}]`, { error: String(error) });
302
323
  }
303
324
  };
304
- return {
305
- "chat.message": async (input, output) => {
306
- safeRecord(input.sessionID, "chat.message", input, output);
307
- },
308
- "chat.params": async (input, output) => {
309
- safeRecord(input.sessionID, "chat.params", input, output);
310
- },
311
- "experimental.chat.system.transform": async (input, output) => {
312
- const sessionID = input.sessionID ?? "unknown";
313
- safeRecord(sessionID, "experimental.chat.system.transform", input, output);
314
- },
315
- "experimental.chat.messages.transform": async (input, output) => {
316
- const firstMsg = output.messages?.[0]?.info;
317
- const sessionID = firstMsg?.sessionID ?? "unknown";
318
- safeRecord(
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
- import { basename } from "node:path";
354
- var providerLogger = async ({ client }) => {
355
- const config = defaultConfig("opencode");
356
- const buffer = new SessionBuffer();
357
- const writer = new LocalWriter(config);
358
- const log = (level, message, extra) => {
359
- client.app.log({
360
- body: { service: "provider-logger", level, message, extra }
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
- const uploader = new WorkerUploader(config, writer, log);
364
- uploader.retryPending().catch((err) => {
365
- log("warn", "Failed to retry pending uploads", { error: String(err) });
366
- });
367
- uploader.cleanup().catch((err) => {
368
- log("warn", "Failed to cleanup old files", { error: String(err) });
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
- const flush = async (sessionID) => {
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
- const filepath = await writer.write(sessionID, entries);
375
- if (filepath) {
376
- const tsNanos = basename(filepath, ".jsonl");
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
- process.on("beforeExit", () => {
404
- flushAll().catch(() => {
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
- log("info", "Provider logger initialized");
408
- return createHooks(buffer, config, flush, log);
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": "1.2.0",
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 --external:@opencode-ai/sdk",
17
- "test": "bun 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": ">=1.3.0"
21
+ "@opencode-ai/plugin": "0.0.0-beta-18992"
22
22
  },
23
23
  "devDependencies": {
24
- "@opencode-ai/plugin": "1.3.0",
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",