| 41 | const SERVICE = "hindsight"; |
| 42 | |
| 43 | export class Logger { |
| 44 | private readonly client?: OpencodeLogClient; |
| 45 | private readonly debugEnabled: boolean; |
| 46 | private readonly silent: boolean; |
| 47 | |
| 48 | constructor(options: LoggerOptions = {}) { |
| 49 | this.client = options.client; |
| 50 | this.debugEnabled = options.debug ?? false; |
| 51 | this.silent = options.silent ?? false; |
| 52 | } |
| 53 | |
| 54 | private emit(level: LogLevel, message: string, extra?: Record<string, unknown>): void { |
| 55 | if (this.silent) return; |
| 56 | |
| 57 | const app = this.client?.app; |
| 58 | if (app && typeof app.log === "function") { |
| 59 | try { |
| 60 | // IMPORTANT: call as a method on `app` so `this` is preserved. |
| 61 | // OpenCode's `app.log` is a class method that uses `this` internally; |
| 62 | // calling a detached reference (`const log = app.log; log(...)`) throws |
| 63 | // `this._client is undefined`, which would otherwise be swallowed below |
| 64 | // and produce no log at all. |
| 65 | const result = app.log({ body: { service: SERVICE, level, message, extra } }); |
| 66 | // Fire-and-forget: a logging failure must never surface to OpenCode. |
| 67 | if (result && typeof (result as Promise<unknown>).then === "function") { |
| 68 | (result as Promise<unknown>).then(undefined, () => {}); |
| 69 | } |
| 70 | return; |
| 71 | } catch { |
| 72 | // Fall through to the console fallback if app.log throws synchronously. |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // Fallback when no OpenCode client is available (or app.log failed). |
| 77 | // OpenCode captures plugin console output into its logs, so this stays |
| 78 | // visible and TUI-safe. |
| 79 | const line = extra |
| 80 | ? `[Hindsight] ${message} ${JSON.stringify(extra)}` |
| 81 | : `[Hindsight] ${message}`; |
| 82 | console.error(line); |
| 83 | } |
| 84 | |
| 85 | error(message: string, error?: unknown): void { |
| 86 | this.emit("error", message, error === undefined ? undefined : { error: errorToString(error) }); |
| 87 | } |
| 88 | |
| 89 | warn(message: string, extra?: Record<string, unknown>): void { |
| 90 | this.emit("warn", message, extra); |
| 91 | } |
| 92 | |
| 93 | info(message: string, extra?: Record<string, unknown>): void { |
| 94 | this.emit("info", message, extra); |
| 95 | } |
| 96 | |
| 97 | debug(message: string, extra?: Record<string, unknown>): void { |
| 98 | if (this.debugEnabled) this.emit("debug", message, extra); |
| 99 | } |
| 100 | } |
nothing calls this directly
no outgoing calls
no test coverage detected