opencode-logger 0.4.5 → 0.5.1

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/README.md CHANGED
@@ -20,7 +20,7 @@ and add:
20
20
  ```jsonc
21
21
  {
22
22
  "$schema": "https://opencode.ai/config.json",
23
- "plugin": ["opencode-logger"]
23
+ "plugin": ["opencode-logger"],
24
24
  }
25
25
  ```
26
26
 
@@ -28,13 +28,13 @@ and add:
28
28
 
29
29
  You can customize the log directory, filename, and logging scope using environment variables.
30
30
 
31
- | Variable | Description | Default |
32
- |----------|-------------|---------|
33
- | `OPENCODE_LOGGER_DIR` | The directory where logs are stored. Can be absolute or relative to project root. | `logs/opencode` |
34
- | `OPENCODE_LOGGER_FILENAME` | The filename for the log file. | `log.jsonl` |
35
- | `OPENCODE_LOGGER_SCOPE` | Comma-separated list of event types to log. Supports wildcards (e.g., `session.*`). | `*` (Log all events) |
36
-
37
-
31
+ | Variable | Description | Default |
32
+ | ------------------------------- | ----------------------------------------------------------------------------------------- | -------------------- |
33
+ | `OPENCODE_LOGGER_DIR` | The directory where logs are stored. Can be absolute or relative to project root. | `logs/opencode` |
34
+ | `OPENCODE_LOGGER_FILENAME` | The filename for the log file. | `log.jsonl` |
35
+ | `OPENCODE_LOGGER_SCOPE` | Comma-separated list of event types to log. Supports wildcards (e.g., `session.*`). | `*` (Log all events) |
36
+ | `OPENCODE_LOGGER_MAX_FILE_SIZE` | Maximum log file size in bytes before automatic rotation. Set to `0` to disable rotation. | `104857600` (100 MB) |
37
+ | `OPENCODE_LOGGER_MAX_FILES` | Maximum number of rotated log files to keep. Set to `0` to keep all. | `0` (unlimited) |
38
38
 
39
39
  ### Setting via CLI
40
40
 
@@ -68,6 +68,17 @@ It accepts a comma-separated string of event types or patterns.
68
68
  export OPENCODE_LOGGER_SCOPE="session.*,command.executed"
69
69
  ```
70
70
 
71
+ ### Log Rotation
72
+
73
+ You can control when log files are rotated and how many archived files are retained.
71
74
 
75
+ - `OPENCODE_LOGGER_MAX_FILE_SIZE`: Once the active log file reaches this size (in bytes), it is rotated. Set to `0` to disable rotation entirely.
76
+ - `OPENCODE_LOGGER_MAX_FILES`: After rotation, only this many archived files are kept (oldest deleted first). Set to `0` to keep all rotated files indefinitely.
72
77
 
78
+ **Example:**
73
79
 
80
+ ```bash
81
+ # Rotate at 10 MB, keep last 5 archived files
82
+ export OPENCODE_LOGGER_MAX_FILE_SIZE=10485760
83
+ export OPENCODE_LOGGER_MAX_FILES=5
84
+ ```
@@ -6,6 +6,17 @@ export declare const DEFAULT_LOG_DIRECTORY = "logs/opencode";
6
6
  * Filename for the log file.
7
7
  */
8
8
  export declare const DEFAULT_LOG_FILENAME = "log.jsonl";
9
+ /**
10
+ * Default maximum log file size in bytes before rotation occurs (100 MB).
11
+ * Set OPENCODE_LOGGER_MAX_FILE_SIZE to override. Set to 0 to disable rotation.
12
+ */
13
+ export declare const DEFAULT_MAX_FILE_SIZE = 104857600;
14
+ /**
15
+ * Default maximum number of rotated log files to retain.
16
+ * 0 means unlimited — all rotated files are kept.
17
+ * Set OPENCODE_LOGGER_MAX_FILES to override.
18
+ */
19
+ export declare const DEFAULT_MAX_FILES = 0;
9
20
  /**
10
21
  * List of event types supported by the logger plugin.
11
22
  * These events correspond to various lifecycle hooks and actions within the Opencode environment.
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,ulBA2CnB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,YAAY,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,gBAAgB,ulBA2CnB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC"}
package/dist/constants.js CHANGED
@@ -6,6 +6,17 @@ export const DEFAULT_LOG_DIRECTORY = "logs/opencode";
6
6
  * Filename for the log file.
7
7
  */
8
8
  export const DEFAULT_LOG_FILENAME = "log.jsonl";
9
+ /**
10
+ * Default maximum log file size in bytes before rotation occurs (100 MB).
11
+ * Set OPENCODE_LOGGER_MAX_FILE_SIZE to override. Set to 0 to disable rotation.
12
+ */
13
+ export const DEFAULT_MAX_FILE_SIZE = 104857600;
14
+ /**
15
+ * Default maximum number of rotated log files to retain.
16
+ * 0 means unlimited — all rotated files are kept.
17
+ * Set OPENCODE_LOGGER_MAX_FILES to override.
18
+ */
19
+ export const DEFAULT_MAX_FILES = 0;
9
20
  /**
10
21
  * List of event types supported by the logger plugin.
11
22
  * These events correspond to various lifecycle hooks and actions within the Opencode environment.
@@ -12,10 +12,17 @@ export interface LogEntry {
12
12
  }
13
13
  /**
14
14
  * Handles initialization of the log directory and writing log entries to the file system.
15
+ * Supports automatic log file rotation when the active file exceeds a configurable size.
15
16
  */
16
17
  export declare class FileLogger {
18
+ private logDir;
19
+ private baseFilename;
17
20
  private logFilePath;
21
+ private maxFileSize;
22
+ private maxFiles;
18
23
  private pluginInput;
24
+ /** Mutex: each log() call chains onto this promise, serialising all writes. */
25
+ private _lock;
19
26
  /**
20
27
  * Creates a new instance of FileLogger.
21
28
  * @param projectRoot - The absolute path to the root of the project.
@@ -24,6 +31,7 @@ export declare class FileLogger {
24
31
  constructor(projectRoot: string, pluginInput: PluginInput);
25
32
  private resolveLogDirectory;
26
33
  private getLogFilename;
34
+ private parseEnvInt;
27
35
  /**
28
36
  * Initializes the logger by ensuring the log directory exists.
29
37
  * This method must be called before attempting to log any events.
@@ -31,9 +39,30 @@ export declare class FileLogger {
31
39
  init(): Promise<void>;
32
40
  /**
33
41
  * Logs an event and its payload to the log file in JSONL format.
42
+ * Automatically rotates the file if the configured maximum size has been reached.
43
+ *
44
+ * Concurrent calls are serialised via an async mutex so that the
45
+ * check-and-rotate step is never interleaved between two callers.
46
+ *
34
47
  * @param eventType - The type of event to log.
35
48
  * @param payload - The data associated with the event.
36
49
  */
37
50
  log(eventType: string, payload: unknown): Promise<void>;
51
+ /** Performs the actual write; always called serially through the mutex. */
52
+ private _doLog;
53
+ /**
54
+ * Checks whether the active log file has exceeded the maximum size and, if so, rotates it.
55
+ * If maxFileSize is 0, rotation is disabled.
56
+ */
57
+ private checkAndRotate;
58
+ /**
59
+ * Renames the active log file to a timestamped archive name and resets the active path.
60
+ * Prunes oldest rotated files if maxFiles is configured.
61
+ */
62
+ private rotate;
63
+ /**
64
+ * Deletes the oldest rotated log files when the number of archived files exceeds maxFiles.
65
+ */
66
+ private pruneOldFiles;
38
67
  }
39
68
  //# sourceMappingURL=file-logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-logger.d.ts","sourceRoot":"","sources":["../src/file-logger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAc;IAEjC;;;;OAIG;gBACS,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW;IAQzD,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,cAAc;IAItB;;;OAGG;IACG,IAAI;IASV;;;;OAIG;IACG,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;CAwB7C"}
1
+ {"version":3,"file":"file-logger.d.ts","sourceRoot":"","sources":["../src/file-logger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQvD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,+EAA+E;IAC/E,OAAO,CAAC,KAAK,CAAoC;IAEjD;;;;OAIG;gBACS,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW;IAezD,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IAOnB;;;OAGG;IACG,IAAI;IAQV;;;;;;;;;OASG;IACG,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7D,2EAA2E;YAC7D,MAAM;IA0BpB;;;OAGG;YACW,cAAc;IAiB5B;;;OAGG;YACW,MAAM;IA+BpB;;OAEG;YACW,aAAa;CA+B3B"}
@@ -1,22 +1,31 @@
1
- import { appendFile, mkdir } from "node:fs/promises";
2
- import { isAbsolute, join, resolve } from "node:path";
3
- import { DEFAULT_LOG_DIRECTORY, DEFAULT_LOG_FILENAME } from "./constants.js";
1
+ import { appendFile, mkdir, readdir, rename, stat, unlink, } from "node:fs/promises";
2
+ import { basename, extname, isAbsolute, join, resolve } from "node:path";
3
+ import { DEFAULT_LOG_DIRECTORY, DEFAULT_LOG_FILENAME, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_FILES, } from "./constants.js";
4
4
  /**
5
5
  * Handles initialization of the log directory and writing log entries to the file system.
6
+ * Supports automatic log file rotation when the active file exceeds a configurable size.
6
7
  */
7
8
  export class FileLogger {
9
+ logDir;
10
+ baseFilename;
8
11
  logFilePath;
12
+ maxFileSize;
13
+ maxFiles;
9
14
  pluginInput;
15
+ /** Mutex: each log() call chains onto this promise, serialising all writes. */
16
+ _lock = Promise.resolve();
10
17
  /**
11
18
  * Creates a new instance of FileLogger.
12
19
  * @param projectRoot - The absolute path to the root of the project.
13
20
  * @param pluginInput - Opencode plugin input
14
21
  */
15
22
  constructor(projectRoot, pluginInput) {
16
- const logDir = this.resolveLogDirectory(projectRoot);
17
- const logFilename = this.getLogFilename();
18
- this.logFilePath = join(logDir, logFilename);
23
+ this.logDir = this.resolveLogDirectory(projectRoot);
24
+ this.baseFilename = this.getLogFilename();
25
+ this.logFilePath = join(this.logDir, this.baseFilename);
19
26
  this.pluginInput = pluginInput;
27
+ this.maxFileSize = this.parseEnvInt("OPENCODE_LOGGER_MAX_FILE_SIZE", DEFAULT_MAX_FILE_SIZE);
28
+ this.maxFiles = this.parseEnvInt("OPENCODE_LOGGER_MAX_FILES", DEFAULT_MAX_FILES);
20
29
  }
21
30
  resolveLogDirectory(projectRoot) {
22
31
  const logDir = process.env["OPENCODE_LOGGER_DIR"] ||
@@ -29,14 +38,20 @@ export class FileLogger {
29
38
  getLogFilename() {
30
39
  return process.env["OPENCODE_LOGGER_FILENAME"] || DEFAULT_LOG_FILENAME;
31
40
  }
41
+ parseEnvInt(varName, defaultValue) {
42
+ const raw = process.env[varName];
43
+ if (raw === undefined || raw === "")
44
+ return defaultValue;
45
+ const parsed = Number.parseInt(raw, 10);
46
+ return Number.isNaN(parsed) ? defaultValue : parsed;
47
+ }
32
48
  /**
33
49
  * Initializes the logger by ensuring the log directory exists.
34
50
  * This method must be called before attempting to log any events.
35
51
  */
36
52
  async init() {
37
53
  try {
38
- const dirPath = join(this.logFilePath, "..");
39
- await mkdir(dirPath, { recursive: true });
54
+ await mkdir(this.logDir, { recursive: true });
40
55
  }
41
56
  catch (error) {
42
57
  console.error(`[Opencode-logger]: Failed to initialize.\n${error}`);
@@ -44,10 +59,22 @@ export class FileLogger {
44
59
  }
45
60
  /**
46
61
  * Logs an event and its payload to the log file in JSONL format.
62
+ * Automatically rotates the file if the configured maximum size has been reached.
63
+ *
64
+ * Concurrent calls are serialised via an async mutex so that the
65
+ * check-and-rotate step is never interleaved between two callers.
66
+ *
47
67
  * @param eventType - The type of event to log.
48
68
  * @param payload - The data associated with the event.
49
69
  */
50
70
  async log(eventType, payload) {
71
+ // Chain this write onto the tail of the lock so all log() calls execute
72
+ // sequentially, regardless of how many are in-flight at once.
73
+ this._lock = this._lock.then(() => this._doLog(eventType, payload));
74
+ return this._lock;
75
+ }
76
+ /** Performs the actual write; always called serially through the mutex. */
77
+ async _doLog(eventType, payload) {
51
78
  const entry = {
52
79
  timestamp: new Date().toISOString(),
53
80
  eventType,
@@ -55,6 +82,7 @@ export class FileLogger {
55
82
  };
56
83
  const line = `${JSON.stringify(entry)}\n`;
57
84
  try {
85
+ await this.checkAndRotate();
58
86
  await appendFile(this.logFilePath, line, "utf-8");
59
87
  }
60
88
  catch (error) {
@@ -70,4 +98,91 @@ export class FileLogger {
70
98
  });
71
99
  }
72
100
  }
101
+ /**
102
+ * Checks whether the active log file has exceeded the maximum size and, if so, rotates it.
103
+ * If maxFileSize is 0, rotation is disabled.
104
+ */
105
+ async checkAndRotate() {
106
+ if (this.maxFileSize === 0)
107
+ return;
108
+ let fileSize;
109
+ try {
110
+ const fileStat = await stat(this.logFilePath);
111
+ fileSize = fileStat.size;
112
+ }
113
+ catch {
114
+ // File does not exist yet — no rotation needed
115
+ return;
116
+ }
117
+ if (fileSize >= this.maxFileSize) {
118
+ await this.rotate();
119
+ }
120
+ }
121
+ /**
122
+ * Renames the active log file to a timestamped archive name and resets the active path.
123
+ * Prunes oldest rotated files if maxFiles is configured.
124
+ */
125
+ async rotate() {
126
+ const ext = extname(this.baseFilename);
127
+ const base = basename(this.baseFilename, ext);
128
+ const timestamp = new Date()
129
+ .toISOString()
130
+ .replace(/:/g, "-")
131
+ .replace(/\..+$/, "");
132
+ const shortId = crypto.randomUUID().slice(0, 8);
133
+ const rotatedFilename = `${base}.${timestamp}-${shortId}${ext}`;
134
+ const rotatedFilePath = join(this.logDir, rotatedFilename);
135
+ try {
136
+ await rename(this.logFilePath, rotatedFilePath);
137
+ }
138
+ catch (error) {
139
+ await this.pluginInput.client.app.log({
140
+ body: {
141
+ service: "opencode-logger",
142
+ level: "error",
143
+ message: "Failed to rotate log file.",
144
+ extra: { error },
145
+ },
146
+ });
147
+ return;
148
+ }
149
+ // logFilePath stays pointing to the base filename — next appendFile will create a fresh file
150
+ if (this.maxFiles > 0) {
151
+ await this.pruneOldFiles();
152
+ }
153
+ }
154
+ /**
155
+ * Deletes the oldest rotated log files when the number of archived files exceeds maxFiles.
156
+ */
157
+ async pruneOldFiles() {
158
+ const ext = extname(this.baseFilename);
159
+ const base = basename(this.baseFilename, ext);
160
+ // Rotated files match: <base>.<timestamp>-<shortId><ext>
161
+ const rotationPattern = new RegExp(`^${escapeRegExp(base)}\\..+${escapeRegExp(ext)}$`);
162
+ let entries;
163
+ try {
164
+ entries = await readdir(this.logDir);
165
+ }
166
+ catch {
167
+ return;
168
+ }
169
+ const rotatedFiles = entries
170
+ .filter((name) => rotationPattern.test(name))
171
+ .sort(); // ISO timestamp prefix sorts lexicographically = chronologically
172
+ const excess = rotatedFiles.length - this.maxFiles;
173
+ if (excess <= 0)
174
+ return;
175
+ const toDelete = rotatedFiles.slice(0, excess);
176
+ for (const filename of toDelete) {
177
+ try {
178
+ await unlink(join(this.logDir, filename));
179
+ }
180
+ catch {
181
+ // Best-effort: ignore individual deletion failures
182
+ }
183
+ }
184
+ }
185
+ }
186
+ function escapeRegExp(str) {
187
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
188
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAKzD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,MAkB1B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,MAe1B,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { SUPPORTED_EVENTS } from "./constants.js";
2
1
  import { FileLogger } from "./file-logger.js";
3
2
  import { shouldLogEvent } from "./utils.js";
4
3
  /**
@@ -14,8 +13,7 @@ export const loggerPlugin = async (ctx) => {
14
13
  console.log("[Opencode Logger] Plugin initialized!");
15
14
  const hooks = {
16
15
  event: async ({ event }) => {
17
- if (SUPPORTED_EVENTS.includes(event.type) &&
18
- shouldLogEvent(event.type, process.env["OPENCODE_LOGGER_SCOPE"])) {
16
+ if (shouldLogEvent(event.type, process.env["OPENCODE_LOGGER_SCOPE"])) {
19
17
  await logger.log(event.type, event);
20
18
  }
21
19
  },
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-logger",
3
3
  "main": "dist/index.js",
4
4
  "types": "dist/index.d.ts",
5
- "version": "0.4.5",
5
+ "version": "0.5.1",
6
6
  "repository": {
7
7
  "url": "git+https://github.com/radekBednarik/opencode-logger.git"
8
8
  },
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "private": false,
35
35
  "scripts": {
36
+ "test": "bun test",
36
37
  "build-dist": "bunx tsc",
37
38
  "lint": "bunx biome lint --write ./src",
38
39
  "format": "bunx biome format --write",
@@ -42,7 +43,7 @@
42
43
  },
43
44
  "type": "module",
44
45
  "lint-staged": {
45
- "*.{js, ts}": [
46
+ "*.{js,ts}": [
46
47
  "bun forlint"
47
48
  ],
48
49
  "*.{ts,json}": [