opencode-agentlens 0.1.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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # opencode-agentlens
2
+
3
+ OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/opencode-agentlens.svg)](https://www.npmjs.com/package/opencode-agentlens)
6
+ [![license](https://img.shields.io/npm/l/opencode-agentlens.svg)](https://github.com/repi/agentlens/blob/main/LICENSE)
7
+
8
+ ## Requirements
9
+
10
+ - OpenCode >= 1.1.0
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install opencode-agentlens
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ ### Environment Variables
21
+
22
+ | Variable | Required | Default | Description |
23
+ |---|---|---|---|
24
+ | `AGENTLENS_API_KEY` | Yes | — | Your AgentLens API key. |
25
+ | `AGENTLENS_ENDPOINT` | No | AgentLens cloud | API endpoint URL. |
26
+ | `AGENTLENS_ENABLED` | No | `true` | Set to `false` to disable tracing. |
27
+ | `AGENTLENS_CAPTURE_CONTENT` | No | `true` | Capture message and tool output content. |
28
+ | `AGENTLENS_MAX_OUTPUT_LENGTH` | No | `10000` | Max characters to capture per output. |
29
+ | `AGENTLENS_FLUSH_INTERVAL` | No | `5000` | Flush interval in milliseconds. |
30
+ | `AGENTLENS_BATCH_SIZE` | No | `100` | Max items per batch before auto-flush. |
31
+
32
+ ### OpenCode Setup
33
+
34
+ Add the plugin to your OpenCode configuration at `~/.config/opencode/opencode.json`:
35
+
36
+ ```json
37
+ {
38
+ "plugins": [
39
+ {
40
+ "name": "agentlens",
41
+ "module": "opencode-agentlens"
42
+ }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ Set your API key:
48
+
49
+ ```bash
50
+ export AGENTLENS_API_KEY="your-api-key"
51
+ ```
52
+
53
+ The plugin activates automatically when OpenCode starts. No code changes required.
54
+
55
+ ## What Gets Captured
56
+
57
+ The plugin hooks into OpenCode's event system and records:
58
+
59
+ - **Sessions** — Full session lifecycle from start to finish, including duration and metadata.
60
+ - **Tool calls** — Every tool invocation with input arguments and output results (e.g., file reads, shell commands, code edits).
61
+ - **LLM calls** — Chat messages sent to and received from the model, including token usage.
62
+ - **Permission flows** — When the agent requests permission and whether it was granted or denied.
63
+ - **File edits** — File paths and change summaries produced by the agent.
64
+
65
+ All data is sent to your AgentLens instance where you can inspect traces, replay sessions, and analyze agent behavior.
66
+
67
+ ## How It Works
68
+
69
+ The plugin registers handlers for OpenCode's event hooks:
70
+
71
+ | Event | What is recorded |
72
+ |---|---|
73
+ | Session start/end | Trace lifecycle, session metadata |
74
+ | `tool.execute.before` | Tool name, input arguments |
75
+ | `tool.execute.after` | Tool output, duration, success/failure |
76
+ | `chat.message` | LLM responses and assistant messages |
77
+ | `chat.params` | Model parameters and prompt configuration |
78
+ | `permission.ask` | Permission requests and user decisions |
79
+
80
+ Each OpenCode session maps to a single AgentLens trace. Tool calls and LLM interactions become spans within that trace.
81
+
82
+ ## Documentation
83
+
84
+ Full documentation: [agentlens.vectry.tech/docs/opencode-plugin](https://agentlens.vectry.tech/docs/opencode-plugin)
85
+
86
+ ## License
87
+
88
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AgentLensPlugin: () => plugin,
24
+ default: () => index_default,
25
+ loadConfig: () => loadConfig
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_agentlens_sdk2 = require("agentlens-sdk");
29
+
30
+ // src/config.ts
31
+ function loadConfig() {
32
+ const apiKey = process.env["AGENTLENS_API_KEY"] ?? "";
33
+ if (!apiKey) {
34
+ console.warn(
35
+ "[agentlens] AGENTLENS_API_KEY not set \u2014 plugin will be disabled"
36
+ );
37
+ }
38
+ return {
39
+ apiKey,
40
+ endpoint: process.env["AGENTLENS_ENDPOINT"] ?? "https://agentlens.vectry.tech",
41
+ enabled: (process.env["AGENTLENS_ENABLED"] ?? "true") === "true",
42
+ captureContent: (process.env["AGENTLENS_CAPTURE_CONTENT"] ?? "false") === "true",
43
+ maxOutputLength: parseInt(
44
+ process.env["AGENTLENS_MAX_OUTPUT_LENGTH"] ?? "2000",
45
+ 10
46
+ ),
47
+ flushInterval: parseInt(
48
+ process.env["AGENTLENS_FLUSH_INTERVAL"] ?? "5000",
49
+ 10
50
+ ),
51
+ maxBatchSize: parseInt(process.env["AGENTLENS_BATCH_SIZE"] ?? "10", 10)
52
+ };
53
+ }
54
+
55
+ // src/state.ts
56
+ var import_agentlens_sdk = require("agentlens-sdk");
57
+
58
+ // src/utils.ts
59
+ function truncate(str, maxLength) {
60
+ if (str.length <= maxLength) return str;
61
+ return str.slice(0, maxLength) + "... [truncated]";
62
+ }
63
+ function extractToolMetadata(tool, args) {
64
+ const a = args;
65
+ if (!a || typeof a !== "object") return {};
66
+ switch (tool) {
67
+ case "read":
68
+ case "mcp_read":
69
+ return { filePath: a["filePath"] };
70
+ case "write":
71
+ case "mcp_write":
72
+ return { filePath: a["filePath"] };
73
+ case "edit":
74
+ case "mcp_edit":
75
+ return { filePath: a["filePath"] };
76
+ case "bash":
77
+ case "mcp_bash":
78
+ return {
79
+ command: truncate(String(a["command"] ?? ""), 200)
80
+ };
81
+ case "glob":
82
+ case "mcp_glob":
83
+ return { pattern: a["pattern"] };
84
+ case "grep":
85
+ case "mcp_grep":
86
+ return { pattern: a["pattern"], path: a["path"] };
87
+ case "task":
88
+ case "mcp_task":
89
+ return {
90
+ category: a["category"],
91
+ description: a["description"]
92
+ };
93
+ default:
94
+ return {};
95
+ }
96
+ }
97
+ function safeJsonValue(value) {
98
+ if (value === null || value === void 0) return null;
99
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
100
+ return value;
101
+ }
102
+ if (Array.isArray(value)) {
103
+ return value.map((v) => safeJsonValue(v));
104
+ }
105
+ if (typeof value === "object") {
106
+ const result = {};
107
+ for (const [k, v] of Object.entries(value)) {
108
+ result[k] = safeJsonValue(v);
109
+ }
110
+ return result;
111
+ }
112
+ return String(value);
113
+ }
114
+
115
+ // src/state.ts
116
+ var SessionState = class {
117
+ traces = /* @__PURE__ */ new Map();
118
+ toolCalls = /* @__PURE__ */ new Map();
119
+ rootSpans = /* @__PURE__ */ new Map();
120
+ startSession(sessionId, metadata) {
121
+ const trace = new import_agentlens_sdk.TraceBuilder("opencode-session", {
122
+ sessionId,
123
+ tags: ["opencode", "coding-agent"],
124
+ metadata: metadata ? safeJsonValue(metadata) : void 0
125
+ });
126
+ const rootSpanId = trace.addSpan({
127
+ name: "session",
128
+ type: import_agentlens_sdk.SpanType.AGENT,
129
+ startedAt: (0, import_agentlens_sdk.nowISO)(),
130
+ metadata: metadata ? safeJsonValue(metadata) : void 0
131
+ });
132
+ this.traces.set(sessionId, trace);
133
+ this.rootSpans.set(sessionId, rootSpanId);
134
+ return trace;
135
+ }
136
+ getTrace(sessionId) {
137
+ return this.traces.get(sessionId);
138
+ }
139
+ endSession(sessionId, status) {
140
+ const trace = this.traces.get(sessionId);
141
+ if (!trace) return;
142
+ const rootSpanId = this.rootSpans.get(sessionId);
143
+ if (rootSpanId) {
144
+ trace.addSpan({
145
+ id: rootSpanId,
146
+ name: "session",
147
+ type: import_agentlens_sdk.SpanType.AGENT,
148
+ status: status === "ERROR" ? import_agentlens_sdk.SpanStatus.ERROR : import_agentlens_sdk.SpanStatus.COMPLETED,
149
+ endedAt: (0, import_agentlens_sdk.nowISO)()
150
+ });
151
+ }
152
+ trace.end({ status: status ?? "COMPLETED" });
153
+ this.traces.delete(sessionId);
154
+ this.rootSpans.delete(sessionId);
155
+ }
156
+ startToolCall(callID, tool, args, sessionID) {
157
+ this.toolCalls.set(callID, {
158
+ startTime: Date.now(),
159
+ tool,
160
+ args,
161
+ sessionID
162
+ });
163
+ }
164
+ endToolCall(callID, output, title, metadata) {
165
+ const call = this.toolCalls.get(callID);
166
+ if (!call) return;
167
+ this.toolCalls.delete(callID);
168
+ const trace = this.traces.get(call.sessionID);
169
+ if (!trace) return;
170
+ const durationMs = Date.now() - call.startTime;
171
+ const rootSpanId = this.rootSpans.get(call.sessionID);
172
+ const toolMeta = extractToolMetadata(call.tool, call.args);
173
+ trace.addSpan({
174
+ name: title,
175
+ type: import_agentlens_sdk.SpanType.TOOL_CALL,
176
+ parentSpanId: rootSpanId,
177
+ input: safeJsonValue(call.args),
178
+ output,
179
+ durationMs,
180
+ status: import_agentlens_sdk.SpanStatus.COMPLETED,
181
+ startedAt: new Date(call.startTime).toISOString(),
182
+ endedAt: (0, import_agentlens_sdk.nowISO)(),
183
+ metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata })
184
+ });
185
+ trace.addDecision({
186
+ type: import_agentlens_sdk.DecisionType.TOOL_SELECTION,
187
+ chosen: call.tool,
188
+ alternatives: [],
189
+ reasoning: title,
190
+ durationMs,
191
+ parentSpanId: rootSpanId
192
+ });
193
+ }
194
+ recordLLMCall(sessionId, options) {
195
+ const trace = this.traces.get(sessionId);
196
+ if (!trace) return;
197
+ const rootSpanId = this.rootSpans.get(sessionId);
198
+ const agentName = options.agent ?? "assistant";
199
+ const modelName = options.model?.modelID ?? "unknown";
200
+ trace.addSpan({
201
+ name: `${agentName} \u2192 ${modelName}`,
202
+ type: import_agentlens_sdk.SpanType.LLM_CALL,
203
+ parentSpanId: rootSpanId,
204
+ status: import_agentlens_sdk.SpanStatus.COMPLETED,
205
+ startedAt: (0, import_agentlens_sdk.nowISO)(),
206
+ endedAt: (0, import_agentlens_sdk.nowISO)(),
207
+ metadata: safeJsonValue({
208
+ provider: options.model?.providerID,
209
+ model: options.model?.modelID,
210
+ agent: options.agent,
211
+ messageID: options.messageID
212
+ })
213
+ });
214
+ }
215
+ recordPermission(sessionId, permission, status) {
216
+ const trace = this.traces.get(sessionId);
217
+ if (!trace) return;
218
+ const rootSpanId = this.rootSpans.get(sessionId);
219
+ const p = permission;
220
+ const title = p?.["title"] ?? "permission";
221
+ const permType = p?.["type"] ?? "unknown";
222
+ trace.addDecision({
223
+ type: import_agentlens_sdk.DecisionType.ESCALATION,
224
+ chosen: safeJsonValue({ action: status }),
225
+ alternatives: [
226
+ "allow",
227
+ "deny",
228
+ "ask"
229
+ ],
230
+ reasoning: `${permType}: ${title}`,
231
+ parentSpanId: rootSpanId
232
+ });
233
+ }
234
+ getRootSpanId(sessionId) {
235
+ return this.rootSpans.get(sessionId);
236
+ }
237
+ };
238
+
239
+ // src/index.ts
240
+ var plugin = async ({ project, directory, worktree }) => {
241
+ const config = loadConfig();
242
+ if (!config.enabled || !config.apiKey) {
243
+ console.log("[agentlens] Plugin disabled \u2014 missing AGENTLENS_API_KEY");
244
+ return {};
245
+ }
246
+ (0, import_agentlens_sdk2.init)({
247
+ apiKey: config.apiKey,
248
+ endpoint: config.endpoint,
249
+ flushInterval: config.flushInterval,
250
+ maxBatchSize: config.maxBatchSize
251
+ });
252
+ const state = new SessionState();
253
+ return {
254
+ event: async ({ event }) => {
255
+ const type = event.type;
256
+ const props = event.properties;
257
+ if (type === "session.created" && props?.["id"]) {
258
+ state.startSession(String(props["id"]), {
259
+ project: project.id,
260
+ directory,
261
+ worktree
262
+ });
263
+ }
264
+ if (type === "session.idle") {
265
+ const sessionId = props?.["sessionID"] ?? props?.["id"];
266
+ if (sessionId) await (0, import_agentlens_sdk2.flush)();
267
+ }
268
+ if (type === "session.error") {
269
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
270
+ if (sessionId) {
271
+ const trace = state.getTrace(sessionId);
272
+ if (trace) {
273
+ trace.addEvent({
274
+ type: import_agentlens_sdk2.EventType.ERROR,
275
+ name: String(props?.["error"] ?? "session error"),
276
+ metadata: safeJsonValue(props)
277
+ });
278
+ }
279
+ }
280
+ }
281
+ if (type === "session.deleted") {
282
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
283
+ if (sessionId) state.endSession(sessionId);
284
+ }
285
+ if (type === "session.diff") {
286
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
287
+ if (sessionId) {
288
+ const trace = state.getTrace(sessionId);
289
+ if (trace) {
290
+ trace.setMetadata({
291
+ diff: truncate(String(props?.["diff"] ?? ""), 5e3)
292
+ });
293
+ }
294
+ }
295
+ }
296
+ if (type === "file.edited") {
297
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
298
+ const trace = sessionId ? state.getTrace(sessionId) : void 0;
299
+ if (trace) {
300
+ trace.addEvent({
301
+ type: import_agentlens_sdk2.EventType.CUSTOM,
302
+ name: "file.edited",
303
+ metadata: safeJsonValue({
304
+ filePath: props?.["filePath"]
305
+ })
306
+ });
307
+ }
308
+ }
309
+ },
310
+ "tool.execute.before": async (input, output) => {
311
+ state.startToolCall(
312
+ input.callID,
313
+ input.tool,
314
+ output.args,
315
+ input.sessionID
316
+ );
317
+ },
318
+ "tool.execute.after": async (input, output) => {
319
+ state.endToolCall(
320
+ input.callID,
321
+ truncate(output.output ?? "", config.maxOutputLength),
322
+ output.title ?? input.tool,
323
+ output.metadata
324
+ );
325
+ },
326
+ "chat.message": async (input) => {
327
+ if (input.model) {
328
+ state.recordLLMCall(input.sessionID, {
329
+ model: input.model,
330
+ agent: input.agent,
331
+ messageID: input.messageID
332
+ });
333
+ }
334
+ },
335
+ "chat.params": async (input, output) => {
336
+ const trace = state.getTrace(input.sessionID);
337
+ if (trace) {
338
+ trace.addEvent({
339
+ type: import_agentlens_sdk2.EventType.CUSTOM,
340
+ name: "chat.params",
341
+ metadata: safeJsonValue({
342
+ agent: input.agent,
343
+ model: input.model.id,
344
+ provider: input.provider.info.id,
345
+ temperature: output.temperature,
346
+ topP: output.topP,
347
+ topK: output.topK
348
+ })
349
+ });
350
+ }
351
+ },
352
+ "permission.ask": async (input, output) => {
353
+ state.recordPermission(input.sessionID, input, output.status);
354
+ }
355
+ };
356
+ };
357
+ var index_default = plugin;
358
+ // Annotate the CommonJS export names for ESM import in node:
359
+ 0 && (module.exports = {
360
+ AgentLensPlugin,
361
+ loadConfig
362
+ });
363
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/state.ts","../src/utils.ts"],"sourcesContent":["import type { Plugin } from \"@opencode-ai/plugin\";\nimport type { JsonValue } from \"agentlens-sdk\";\nimport { init, flush, EventType as EventTypeValues } from \"agentlens-sdk\";\nimport { loadConfig } from \"./config.js\";\nimport { SessionState } from \"./state.js\";\nimport { truncate, safeJsonValue } from \"./utils.js\";\n\nconst plugin: Plugin = async ({ project, directory, worktree }) => {\n const config = loadConfig();\n\n if (!config.enabled || !config.apiKey) {\n console.log(\"[agentlens] Plugin disabled — missing AGENTLENS_API_KEY\");\n return {};\n }\n\n init({\n apiKey: config.apiKey,\n endpoint: config.endpoint,\n flushInterval: config.flushInterval,\n maxBatchSize: config.maxBatchSize,\n });\n\n const state = new SessionState();\n\n return {\n event: async ({ event }) => {\n const type = event.type;\n const props = (event as Record<string, unknown>).properties as\n | Record<string, unknown>\n | undefined;\n\n if (type === \"session.created\" && props?.[\"id\"]) {\n state.startSession(String(props[\"id\"]), {\n project: project.id,\n directory,\n worktree,\n });\n }\n\n if (type === \"session.idle\") {\n const sessionId = props?.[\"sessionID\"] ?? props?.[\"id\"];\n if (sessionId) await flush();\n }\n\n if (type === \"session.error\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) {\n const trace = state.getTrace(sessionId);\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.ERROR,\n name: String(props?.[\"error\"] ?? \"session error\"),\n metadata: safeJsonValue(props) as JsonValue,\n });\n }\n }\n }\n\n if (type === \"session.deleted\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) state.endSession(sessionId);\n }\n\n if (type === \"session.diff\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) {\n const trace = state.getTrace(sessionId);\n if (trace) {\n trace.setMetadata({\n diff: truncate(String(props?.[\"diff\"] ?? \"\"), 5000),\n });\n }\n }\n }\n\n if (type === \"file.edited\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n const trace = sessionId ? state.getTrace(sessionId) : undefined;\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.CUSTOM,\n name: \"file.edited\",\n metadata: safeJsonValue({\n filePath: props?.[\"filePath\"],\n }) as JsonValue,\n });\n }\n }\n },\n\n \"tool.execute.before\": async (input, output) => {\n state.startToolCall(\n input.callID,\n input.tool,\n output.args as unknown,\n input.sessionID,\n );\n },\n\n \"tool.execute.after\": async (input, output) => {\n state.endToolCall(\n input.callID,\n truncate(output.output ?? \"\", config.maxOutputLength),\n output.title ?? input.tool,\n output.metadata as unknown,\n );\n },\n\n \"chat.message\": async (input) => {\n if (input.model) {\n state.recordLLMCall(input.sessionID, {\n model: input.model,\n agent: input.agent,\n messageID: input.messageID,\n });\n }\n },\n\n \"chat.params\": async (input, output) => {\n const trace = state.getTrace(input.sessionID);\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.CUSTOM,\n name: \"chat.params\",\n metadata: safeJsonValue({\n agent: input.agent,\n model: input.model.id,\n provider: input.provider.info.id,\n temperature: output.temperature,\n topP: output.topP,\n topK: output.topK,\n }) as JsonValue,\n });\n }\n },\n\n \"permission.ask\": async (input, output) => {\n state.recordPermission(input.sessionID, input, output.status);\n },\n };\n};\n\nexport default plugin;\nexport { plugin as AgentLensPlugin };\nexport type { PluginConfig } from \"./config.js\";\nexport { loadConfig } from \"./config.js\";\n","export interface PluginConfig {\n apiKey: string;\n endpoint: string;\n enabled: boolean;\n /** Opt-in: capture full message content in traces */\n captureContent: boolean;\n /** Maximum characters for tool output before truncation */\n maxOutputLength: number;\n /** Milliseconds between automatic flushes */\n flushInterval: number;\n /** Maximum traces per batch */\n maxBatchSize: number;\n}\n\nexport function loadConfig(): PluginConfig {\n const apiKey = process.env[\"AGENTLENS_API_KEY\"] ?? \"\";\n\n if (!apiKey) {\n console.warn(\n \"[agentlens] AGENTLENS_API_KEY not set — plugin will be disabled\",\n );\n }\n\n return {\n apiKey,\n endpoint:\n process.env[\"AGENTLENS_ENDPOINT\"] ?? \"https://agentlens.vectry.tech\",\n enabled: (process.env[\"AGENTLENS_ENABLED\"] ?? \"true\") === \"true\",\n captureContent:\n (process.env[\"AGENTLENS_CAPTURE_CONTENT\"] ?? \"false\") === \"true\",\n maxOutputLength: parseInt(\n process.env[\"AGENTLENS_MAX_OUTPUT_LENGTH\"] ?? \"2000\",\n 10,\n ),\n flushInterval: parseInt(\n process.env[\"AGENTLENS_FLUSH_INTERVAL\"] ?? \"5000\",\n 10,\n ),\n maxBatchSize: parseInt(process.env[\"AGENTLENS_BATCH_SIZE\"] ?? \"10\", 10),\n };\n}\n","import {\n TraceBuilder,\n SpanType,\n SpanStatus,\n DecisionType,\n nowISO,\n} from \"agentlens-sdk\";\nimport type { JsonValue, TraceStatus } from \"agentlens-sdk\";\nimport { extractToolMetadata, safeJsonValue } from \"./utils.js\";\n\ninterface ToolCallState {\n startTime: number;\n tool: string;\n args: unknown;\n sessionID: string;\n}\n\nexport class SessionState {\n private traces = new Map<string, TraceBuilder>();\n private toolCalls = new Map<string, ToolCallState>();\n private rootSpans = new Map<string, string>();\n\n startSession(\n sessionId: string,\n metadata?: Record<string, unknown>,\n ): TraceBuilder {\n const trace = new TraceBuilder(\"opencode-session\", {\n sessionId,\n tags: [\"opencode\", \"coding-agent\"],\n metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,\n });\n\n const rootSpanId = trace.addSpan({\n name: \"session\",\n type: SpanType.AGENT,\n startedAt: nowISO(),\n metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,\n });\n\n this.traces.set(sessionId, trace);\n this.rootSpans.set(sessionId, rootSpanId);\n return trace;\n }\n\n getTrace(sessionId: string): TraceBuilder | undefined {\n return this.traces.get(sessionId);\n }\n\n endSession(sessionId: string, status?: TraceStatus): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n if (rootSpanId) {\n trace.addSpan({\n id: rootSpanId,\n name: \"session\",\n type: SpanType.AGENT,\n status: status === \"ERROR\" ? SpanStatus.ERROR : SpanStatus.COMPLETED,\n endedAt: nowISO(),\n });\n }\n\n trace.end({ status: status ?? \"COMPLETED\" });\n this.traces.delete(sessionId);\n this.rootSpans.delete(sessionId);\n }\n\n startToolCall(\n callID: string,\n tool: string,\n args: unknown,\n sessionID: string,\n ): void {\n this.toolCalls.set(callID, {\n startTime: Date.now(),\n tool,\n args,\n sessionID,\n });\n }\n\n endToolCall(\n callID: string,\n output: string,\n title: string,\n metadata: unknown,\n ): void {\n const call = this.toolCalls.get(callID);\n if (!call) return;\n this.toolCalls.delete(callID);\n\n const trace = this.traces.get(call.sessionID);\n if (!trace) return;\n\n const durationMs = Date.now() - call.startTime;\n const rootSpanId = this.rootSpans.get(call.sessionID);\n const toolMeta = extractToolMetadata(call.tool, call.args);\n\n trace.addSpan({\n name: title,\n type: SpanType.TOOL_CALL,\n parentSpanId: rootSpanId,\n input: safeJsonValue(call.args),\n output: output as JsonValue,\n durationMs,\n status: SpanStatus.COMPLETED,\n startedAt: new Date(call.startTime).toISOString(),\n endedAt: nowISO(),\n metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata }),\n });\n\n trace.addDecision({\n type: DecisionType.TOOL_SELECTION,\n chosen: call.tool as JsonValue,\n alternatives: [],\n reasoning: title,\n durationMs,\n parentSpanId: rootSpanId,\n });\n }\n\n recordLLMCall(\n sessionId: string,\n options: {\n model?: { providerID: string; modelID: string };\n agent?: string;\n messageID?: string;\n },\n ): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n const agentName = options.agent ?? \"assistant\";\n const modelName = options.model?.modelID ?? \"unknown\";\n\n trace.addSpan({\n name: `${agentName} → ${modelName}`,\n type: SpanType.LLM_CALL,\n parentSpanId: rootSpanId,\n status: SpanStatus.COMPLETED,\n startedAt: nowISO(),\n endedAt: nowISO(),\n metadata: safeJsonValue({\n provider: options.model?.providerID,\n model: options.model?.modelID,\n agent: options.agent,\n messageID: options.messageID,\n }),\n });\n }\n\n recordPermission(\n sessionId: string,\n permission: unknown,\n status: string,\n ): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n const p = permission as Record<string, unknown> | null;\n const title = (p?.[\"title\"] as string) ?? \"permission\";\n const permType = (p?.[\"type\"] as string) ?? \"unknown\";\n\n trace.addDecision({\n type: DecisionType.ESCALATION,\n chosen: safeJsonValue({ action: status }),\n alternatives: [\n \"allow\" as JsonValue,\n \"deny\" as JsonValue,\n \"ask\" as JsonValue,\n ],\n reasoning: `${permType}: ${title}`,\n parentSpanId: rootSpanId,\n });\n }\n\n getRootSpanId(sessionId: string): string | undefined {\n return this.rootSpans.get(sessionId);\n }\n}\n","import type { JsonValue } from \"agentlens-sdk\";\n\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str;\n return str.slice(0, maxLength) + \"... [truncated]\";\n}\n\nexport function extractToolMetadata(\n tool: string,\n args: unknown,\n): Record<string, unknown> {\n const a = args as Record<string, unknown> | null | undefined;\n if (!a || typeof a !== \"object\") return {};\n\n switch (tool) {\n case \"read\":\n case \"mcp_read\":\n return { filePath: a[\"filePath\"] };\n\n case \"write\":\n case \"mcp_write\":\n return { filePath: a[\"filePath\"] };\n\n case \"edit\":\n case \"mcp_edit\":\n return { filePath: a[\"filePath\"] };\n\n case \"bash\":\n case \"mcp_bash\":\n return {\n command: truncate(String(a[\"command\"] ?? \"\"), 200),\n };\n\n case \"glob\":\n case \"mcp_glob\":\n return { pattern: a[\"pattern\"] };\n\n case \"grep\":\n case \"mcp_grep\":\n return { pattern: a[\"pattern\"], path: a[\"path\"] };\n\n case \"task\":\n case \"mcp_task\":\n return {\n category: a[\"category\"],\n description: a[\"description\"],\n };\n\n default:\n return {};\n }\n}\n\nconst MODEL_COSTS: Record<string, { input: number; output: number }> = {\n \"gpt-5.2\": { input: 1.75, output: 14 },\n \"gpt-5.1\": { input: 1.25, output: 10 },\n \"gpt-5\": { input: 1.25, output: 10 },\n \"gpt-5-mini\": { input: 0.25, output: 2 },\n \"gpt-5-nano\": { input: 0.05, output: 0.4 },\n \"gpt-4.1\": { input: 2, output: 8 },\n \"gpt-4.1-mini\": { input: 0.4, output: 1.6 },\n \"gpt-4.1-nano\": { input: 0.1, output: 0.4 },\n \"o3\": { input: 2, output: 8 },\n \"o3-mini\": { input: 1.1, output: 4.4 },\n \"o4-mini\": { input: 1.1, output: 4.4 },\n \"o1\": { input: 15, output: 60 },\n \"gpt-4o\": { input: 2.5, output: 10 },\n \"gpt-4o-mini\": { input: 0.15, output: 0.6 },\n \"gpt-4-turbo\": { input: 10, output: 30 },\n \"gpt-4\": { input: 30, output: 60 },\n \"claude-opus-4-6\": { input: 5, output: 25 },\n \"claude-opus-4-20250514\": { input: 15, output: 75 },\n \"claude-sonnet-4-20250514\": { input: 3, output: 15 },\n \"claude-4.5-opus\": { input: 5, output: 25 },\n \"claude-4.5-sonnet\": { input: 3, output: 15 },\n \"claude-4.5-haiku\": { input: 1, output: 5 },\n \"claude-3-5-sonnet\": { input: 3, output: 15 },\n \"claude-3-5-haiku\": { input: 0.8, output: 4 },\n \"claude-3-opus\": { input: 15, output: 75 },\n \"claude-3-haiku\": { input: 0.25, output: 1.25 },\n};\n\nexport function getModelCost(\n modelId: string,\n): { input: number; output: number } | undefined {\n const direct = MODEL_COSTS[modelId];\n if (direct) return direct;\n\n for (const [key, cost] of Object.entries(MODEL_COSTS)) {\n if (modelId.includes(key)) return cost;\n }\n return undefined;\n}\n\n/** Coerce arbitrary values into SDK-compatible `JsonValue`, stringifying unknowns. */\nexport function safeJsonValue(value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((v) => safeJsonValue(v));\n }\n if (typeof value === \"object\") {\n const result: Record<string, JsonValue> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = safeJsonValue(v);\n }\n return result;\n }\n return String(value);\n}\n"],"mappings":"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","names":["import_agentlens_sdk","EventTypeValues"]}
@@ -0,0 +1,20 @@
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+
3
+ interface PluginConfig {
4
+ apiKey: string;
5
+ endpoint: string;
6
+ enabled: boolean;
7
+ /** Opt-in: capture full message content in traces */
8
+ captureContent: boolean;
9
+ /** Maximum characters for tool output before truncation */
10
+ maxOutputLength: number;
11
+ /** Milliseconds between automatic flushes */
12
+ flushInterval: number;
13
+ /** Maximum traces per batch */
14
+ maxBatchSize: number;
15
+ }
16
+ declare function loadConfig(): PluginConfig;
17
+
18
+ declare const plugin: Plugin;
19
+
20
+ export { plugin as AgentLensPlugin, type PluginConfig, plugin as default, loadConfig };
@@ -0,0 +1,20 @@
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+
3
+ interface PluginConfig {
4
+ apiKey: string;
5
+ endpoint: string;
6
+ enabled: boolean;
7
+ /** Opt-in: capture full message content in traces */
8
+ captureContent: boolean;
9
+ /** Maximum characters for tool output before truncation */
10
+ maxOutputLength: number;
11
+ /** Milliseconds between automatic flushes */
12
+ flushInterval: number;
13
+ /** Maximum traces per batch */
14
+ maxBatchSize: number;
15
+ }
16
+ declare function loadConfig(): PluginConfig;
17
+
18
+ declare const plugin: Plugin;
19
+
20
+ export { plugin as AgentLensPlugin, type PluginConfig, plugin as default, loadConfig };
package/dist/index.js ADDED
@@ -0,0 +1,343 @@
1
+ // src/index.ts
2
+ import { init, flush, EventType as EventTypeValues } from "agentlens-sdk";
3
+
4
+ // src/config.ts
5
+ function loadConfig() {
6
+ const apiKey = process.env["AGENTLENS_API_KEY"] ?? "";
7
+ if (!apiKey) {
8
+ console.warn(
9
+ "[agentlens] AGENTLENS_API_KEY not set \u2014 plugin will be disabled"
10
+ );
11
+ }
12
+ return {
13
+ apiKey,
14
+ endpoint: process.env["AGENTLENS_ENDPOINT"] ?? "https://agentlens.vectry.tech",
15
+ enabled: (process.env["AGENTLENS_ENABLED"] ?? "true") === "true",
16
+ captureContent: (process.env["AGENTLENS_CAPTURE_CONTENT"] ?? "false") === "true",
17
+ maxOutputLength: parseInt(
18
+ process.env["AGENTLENS_MAX_OUTPUT_LENGTH"] ?? "2000",
19
+ 10
20
+ ),
21
+ flushInterval: parseInt(
22
+ process.env["AGENTLENS_FLUSH_INTERVAL"] ?? "5000",
23
+ 10
24
+ ),
25
+ maxBatchSize: parseInt(process.env["AGENTLENS_BATCH_SIZE"] ?? "10", 10)
26
+ };
27
+ }
28
+
29
+ // src/state.ts
30
+ import {
31
+ TraceBuilder,
32
+ SpanType,
33
+ SpanStatus,
34
+ DecisionType,
35
+ nowISO
36
+ } from "agentlens-sdk";
37
+
38
+ // src/utils.ts
39
+ function truncate(str, maxLength) {
40
+ if (str.length <= maxLength) return str;
41
+ return str.slice(0, maxLength) + "... [truncated]";
42
+ }
43
+ function extractToolMetadata(tool, args) {
44
+ const a = args;
45
+ if (!a || typeof a !== "object") return {};
46
+ switch (tool) {
47
+ case "read":
48
+ case "mcp_read":
49
+ return { filePath: a["filePath"] };
50
+ case "write":
51
+ case "mcp_write":
52
+ return { filePath: a["filePath"] };
53
+ case "edit":
54
+ case "mcp_edit":
55
+ return { filePath: a["filePath"] };
56
+ case "bash":
57
+ case "mcp_bash":
58
+ return {
59
+ command: truncate(String(a["command"] ?? ""), 200)
60
+ };
61
+ case "glob":
62
+ case "mcp_glob":
63
+ return { pattern: a["pattern"] };
64
+ case "grep":
65
+ case "mcp_grep":
66
+ return { pattern: a["pattern"], path: a["path"] };
67
+ case "task":
68
+ case "mcp_task":
69
+ return {
70
+ category: a["category"],
71
+ description: a["description"]
72
+ };
73
+ default:
74
+ return {};
75
+ }
76
+ }
77
+ function safeJsonValue(value) {
78
+ if (value === null || value === void 0) return null;
79
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
80
+ return value;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ return value.map((v) => safeJsonValue(v));
84
+ }
85
+ if (typeof value === "object") {
86
+ const result = {};
87
+ for (const [k, v] of Object.entries(value)) {
88
+ result[k] = safeJsonValue(v);
89
+ }
90
+ return result;
91
+ }
92
+ return String(value);
93
+ }
94
+
95
+ // src/state.ts
96
+ var SessionState = class {
97
+ traces = /* @__PURE__ */ new Map();
98
+ toolCalls = /* @__PURE__ */ new Map();
99
+ rootSpans = /* @__PURE__ */ new Map();
100
+ startSession(sessionId, metadata) {
101
+ const trace = new TraceBuilder("opencode-session", {
102
+ sessionId,
103
+ tags: ["opencode", "coding-agent"],
104
+ metadata: metadata ? safeJsonValue(metadata) : void 0
105
+ });
106
+ const rootSpanId = trace.addSpan({
107
+ name: "session",
108
+ type: SpanType.AGENT,
109
+ startedAt: nowISO(),
110
+ metadata: metadata ? safeJsonValue(metadata) : void 0
111
+ });
112
+ this.traces.set(sessionId, trace);
113
+ this.rootSpans.set(sessionId, rootSpanId);
114
+ return trace;
115
+ }
116
+ getTrace(sessionId) {
117
+ return this.traces.get(sessionId);
118
+ }
119
+ endSession(sessionId, status) {
120
+ const trace = this.traces.get(sessionId);
121
+ if (!trace) return;
122
+ const rootSpanId = this.rootSpans.get(sessionId);
123
+ if (rootSpanId) {
124
+ trace.addSpan({
125
+ id: rootSpanId,
126
+ name: "session",
127
+ type: SpanType.AGENT,
128
+ status: status === "ERROR" ? SpanStatus.ERROR : SpanStatus.COMPLETED,
129
+ endedAt: nowISO()
130
+ });
131
+ }
132
+ trace.end({ status: status ?? "COMPLETED" });
133
+ this.traces.delete(sessionId);
134
+ this.rootSpans.delete(sessionId);
135
+ }
136
+ startToolCall(callID, tool, args, sessionID) {
137
+ this.toolCalls.set(callID, {
138
+ startTime: Date.now(),
139
+ tool,
140
+ args,
141
+ sessionID
142
+ });
143
+ }
144
+ endToolCall(callID, output, title, metadata) {
145
+ const call = this.toolCalls.get(callID);
146
+ if (!call) return;
147
+ this.toolCalls.delete(callID);
148
+ const trace = this.traces.get(call.sessionID);
149
+ if (!trace) return;
150
+ const durationMs = Date.now() - call.startTime;
151
+ const rootSpanId = this.rootSpans.get(call.sessionID);
152
+ const toolMeta = extractToolMetadata(call.tool, call.args);
153
+ trace.addSpan({
154
+ name: title,
155
+ type: SpanType.TOOL_CALL,
156
+ parentSpanId: rootSpanId,
157
+ input: safeJsonValue(call.args),
158
+ output,
159
+ durationMs,
160
+ status: SpanStatus.COMPLETED,
161
+ startedAt: new Date(call.startTime).toISOString(),
162
+ endedAt: nowISO(),
163
+ metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata })
164
+ });
165
+ trace.addDecision({
166
+ type: DecisionType.TOOL_SELECTION,
167
+ chosen: call.tool,
168
+ alternatives: [],
169
+ reasoning: title,
170
+ durationMs,
171
+ parentSpanId: rootSpanId
172
+ });
173
+ }
174
+ recordLLMCall(sessionId, options) {
175
+ const trace = this.traces.get(sessionId);
176
+ if (!trace) return;
177
+ const rootSpanId = this.rootSpans.get(sessionId);
178
+ const agentName = options.agent ?? "assistant";
179
+ const modelName = options.model?.modelID ?? "unknown";
180
+ trace.addSpan({
181
+ name: `${agentName} \u2192 ${modelName}`,
182
+ type: SpanType.LLM_CALL,
183
+ parentSpanId: rootSpanId,
184
+ status: SpanStatus.COMPLETED,
185
+ startedAt: nowISO(),
186
+ endedAt: nowISO(),
187
+ metadata: safeJsonValue({
188
+ provider: options.model?.providerID,
189
+ model: options.model?.modelID,
190
+ agent: options.agent,
191
+ messageID: options.messageID
192
+ })
193
+ });
194
+ }
195
+ recordPermission(sessionId, permission, status) {
196
+ const trace = this.traces.get(sessionId);
197
+ if (!trace) return;
198
+ const rootSpanId = this.rootSpans.get(sessionId);
199
+ const p = permission;
200
+ const title = p?.["title"] ?? "permission";
201
+ const permType = p?.["type"] ?? "unknown";
202
+ trace.addDecision({
203
+ type: DecisionType.ESCALATION,
204
+ chosen: safeJsonValue({ action: status }),
205
+ alternatives: [
206
+ "allow",
207
+ "deny",
208
+ "ask"
209
+ ],
210
+ reasoning: `${permType}: ${title}`,
211
+ parentSpanId: rootSpanId
212
+ });
213
+ }
214
+ getRootSpanId(sessionId) {
215
+ return this.rootSpans.get(sessionId);
216
+ }
217
+ };
218
+
219
+ // src/index.ts
220
+ var plugin = async ({ project, directory, worktree }) => {
221
+ const config = loadConfig();
222
+ if (!config.enabled || !config.apiKey) {
223
+ console.log("[agentlens] Plugin disabled \u2014 missing AGENTLENS_API_KEY");
224
+ return {};
225
+ }
226
+ init({
227
+ apiKey: config.apiKey,
228
+ endpoint: config.endpoint,
229
+ flushInterval: config.flushInterval,
230
+ maxBatchSize: config.maxBatchSize
231
+ });
232
+ const state = new SessionState();
233
+ return {
234
+ event: async ({ event }) => {
235
+ const type = event.type;
236
+ const props = event.properties;
237
+ if (type === "session.created" && props?.["id"]) {
238
+ state.startSession(String(props["id"]), {
239
+ project: project.id,
240
+ directory,
241
+ worktree
242
+ });
243
+ }
244
+ if (type === "session.idle") {
245
+ const sessionId = props?.["sessionID"] ?? props?.["id"];
246
+ if (sessionId) await flush();
247
+ }
248
+ if (type === "session.error") {
249
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
250
+ if (sessionId) {
251
+ const trace = state.getTrace(sessionId);
252
+ if (trace) {
253
+ trace.addEvent({
254
+ type: EventTypeValues.ERROR,
255
+ name: String(props?.["error"] ?? "session error"),
256
+ metadata: safeJsonValue(props)
257
+ });
258
+ }
259
+ }
260
+ }
261
+ if (type === "session.deleted") {
262
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
263
+ if (sessionId) state.endSession(sessionId);
264
+ }
265
+ if (type === "session.diff") {
266
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
267
+ if (sessionId) {
268
+ const trace = state.getTrace(sessionId);
269
+ if (trace) {
270
+ trace.setMetadata({
271
+ diff: truncate(String(props?.["diff"] ?? ""), 5e3)
272
+ });
273
+ }
274
+ }
275
+ }
276
+ if (type === "file.edited") {
277
+ const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
278
+ const trace = sessionId ? state.getTrace(sessionId) : void 0;
279
+ if (trace) {
280
+ trace.addEvent({
281
+ type: EventTypeValues.CUSTOM,
282
+ name: "file.edited",
283
+ metadata: safeJsonValue({
284
+ filePath: props?.["filePath"]
285
+ })
286
+ });
287
+ }
288
+ }
289
+ },
290
+ "tool.execute.before": async (input, output) => {
291
+ state.startToolCall(
292
+ input.callID,
293
+ input.tool,
294
+ output.args,
295
+ input.sessionID
296
+ );
297
+ },
298
+ "tool.execute.after": async (input, output) => {
299
+ state.endToolCall(
300
+ input.callID,
301
+ truncate(output.output ?? "", config.maxOutputLength),
302
+ output.title ?? input.tool,
303
+ output.metadata
304
+ );
305
+ },
306
+ "chat.message": async (input) => {
307
+ if (input.model) {
308
+ state.recordLLMCall(input.sessionID, {
309
+ model: input.model,
310
+ agent: input.agent,
311
+ messageID: input.messageID
312
+ });
313
+ }
314
+ },
315
+ "chat.params": async (input, output) => {
316
+ const trace = state.getTrace(input.sessionID);
317
+ if (trace) {
318
+ trace.addEvent({
319
+ type: EventTypeValues.CUSTOM,
320
+ name: "chat.params",
321
+ metadata: safeJsonValue({
322
+ agent: input.agent,
323
+ model: input.model.id,
324
+ provider: input.provider.info.id,
325
+ temperature: output.temperature,
326
+ topP: output.topP,
327
+ topK: output.topK
328
+ })
329
+ });
330
+ }
331
+ },
332
+ "permission.ask": async (input, output) => {
333
+ state.recordPermission(input.sessionID, input, output.status);
334
+ }
335
+ };
336
+ };
337
+ var index_default = plugin;
338
+ export {
339
+ plugin as AgentLensPlugin,
340
+ index_default as default,
341
+ loadConfig
342
+ };
343
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/state.ts","../src/utils.ts"],"sourcesContent":["import type { Plugin } from \"@opencode-ai/plugin\";\nimport type { JsonValue } from \"agentlens-sdk\";\nimport { init, flush, EventType as EventTypeValues } from \"agentlens-sdk\";\nimport { loadConfig } from \"./config.js\";\nimport { SessionState } from \"./state.js\";\nimport { truncate, safeJsonValue } from \"./utils.js\";\n\nconst plugin: Plugin = async ({ project, directory, worktree }) => {\n const config = loadConfig();\n\n if (!config.enabled || !config.apiKey) {\n console.log(\"[agentlens] Plugin disabled — missing AGENTLENS_API_KEY\");\n return {};\n }\n\n init({\n apiKey: config.apiKey,\n endpoint: config.endpoint,\n flushInterval: config.flushInterval,\n maxBatchSize: config.maxBatchSize,\n });\n\n const state = new SessionState();\n\n return {\n event: async ({ event }) => {\n const type = event.type;\n const props = (event as Record<string, unknown>).properties as\n | Record<string, unknown>\n | undefined;\n\n if (type === \"session.created\" && props?.[\"id\"]) {\n state.startSession(String(props[\"id\"]), {\n project: project.id,\n directory,\n worktree,\n });\n }\n\n if (type === \"session.idle\") {\n const sessionId = props?.[\"sessionID\"] ?? props?.[\"id\"];\n if (sessionId) await flush();\n }\n\n if (type === \"session.error\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) {\n const trace = state.getTrace(sessionId);\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.ERROR,\n name: String(props?.[\"error\"] ?? \"session error\"),\n metadata: safeJsonValue(props) as JsonValue,\n });\n }\n }\n }\n\n if (type === \"session.deleted\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) state.endSession(sessionId);\n }\n\n if (type === \"session.diff\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n if (sessionId) {\n const trace = state.getTrace(sessionId);\n if (trace) {\n trace.setMetadata({\n diff: truncate(String(props?.[\"diff\"] ?? \"\"), 5000),\n });\n }\n }\n }\n\n if (type === \"file.edited\") {\n const sessionId = String(props?.[\"sessionID\"] ?? props?.[\"id\"] ?? \"\");\n const trace = sessionId ? state.getTrace(sessionId) : undefined;\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.CUSTOM,\n name: \"file.edited\",\n metadata: safeJsonValue({\n filePath: props?.[\"filePath\"],\n }) as JsonValue,\n });\n }\n }\n },\n\n \"tool.execute.before\": async (input, output) => {\n state.startToolCall(\n input.callID,\n input.tool,\n output.args as unknown,\n input.sessionID,\n );\n },\n\n \"tool.execute.after\": async (input, output) => {\n state.endToolCall(\n input.callID,\n truncate(output.output ?? \"\", config.maxOutputLength),\n output.title ?? input.tool,\n output.metadata as unknown,\n );\n },\n\n \"chat.message\": async (input) => {\n if (input.model) {\n state.recordLLMCall(input.sessionID, {\n model: input.model,\n agent: input.agent,\n messageID: input.messageID,\n });\n }\n },\n\n \"chat.params\": async (input, output) => {\n const trace = state.getTrace(input.sessionID);\n if (trace) {\n trace.addEvent({\n type: EventTypeValues.CUSTOM,\n name: \"chat.params\",\n metadata: safeJsonValue({\n agent: input.agent,\n model: input.model.id,\n provider: input.provider.info.id,\n temperature: output.temperature,\n topP: output.topP,\n topK: output.topK,\n }) as JsonValue,\n });\n }\n },\n\n \"permission.ask\": async (input, output) => {\n state.recordPermission(input.sessionID, input, output.status);\n },\n };\n};\n\nexport default plugin;\nexport { plugin as AgentLensPlugin };\nexport type { PluginConfig } from \"./config.js\";\nexport { loadConfig } from \"./config.js\";\n","export interface PluginConfig {\n apiKey: string;\n endpoint: string;\n enabled: boolean;\n /** Opt-in: capture full message content in traces */\n captureContent: boolean;\n /** Maximum characters for tool output before truncation */\n maxOutputLength: number;\n /** Milliseconds between automatic flushes */\n flushInterval: number;\n /** Maximum traces per batch */\n maxBatchSize: number;\n}\n\nexport function loadConfig(): PluginConfig {\n const apiKey = process.env[\"AGENTLENS_API_KEY\"] ?? \"\";\n\n if (!apiKey) {\n console.warn(\n \"[agentlens] AGENTLENS_API_KEY not set — plugin will be disabled\",\n );\n }\n\n return {\n apiKey,\n endpoint:\n process.env[\"AGENTLENS_ENDPOINT\"] ?? \"https://agentlens.vectry.tech\",\n enabled: (process.env[\"AGENTLENS_ENABLED\"] ?? \"true\") === \"true\",\n captureContent:\n (process.env[\"AGENTLENS_CAPTURE_CONTENT\"] ?? \"false\") === \"true\",\n maxOutputLength: parseInt(\n process.env[\"AGENTLENS_MAX_OUTPUT_LENGTH\"] ?? \"2000\",\n 10,\n ),\n flushInterval: parseInt(\n process.env[\"AGENTLENS_FLUSH_INTERVAL\"] ?? \"5000\",\n 10,\n ),\n maxBatchSize: parseInt(process.env[\"AGENTLENS_BATCH_SIZE\"] ?? \"10\", 10),\n };\n}\n","import {\n TraceBuilder,\n SpanType,\n SpanStatus,\n DecisionType,\n nowISO,\n} from \"agentlens-sdk\";\nimport type { JsonValue, TraceStatus } from \"agentlens-sdk\";\nimport { extractToolMetadata, safeJsonValue } from \"./utils.js\";\n\ninterface ToolCallState {\n startTime: number;\n tool: string;\n args: unknown;\n sessionID: string;\n}\n\nexport class SessionState {\n private traces = new Map<string, TraceBuilder>();\n private toolCalls = new Map<string, ToolCallState>();\n private rootSpans = new Map<string, string>();\n\n startSession(\n sessionId: string,\n metadata?: Record<string, unknown>,\n ): TraceBuilder {\n const trace = new TraceBuilder(\"opencode-session\", {\n sessionId,\n tags: [\"opencode\", \"coding-agent\"],\n metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,\n });\n\n const rootSpanId = trace.addSpan({\n name: \"session\",\n type: SpanType.AGENT,\n startedAt: nowISO(),\n metadata: metadata ? (safeJsonValue(metadata) as JsonValue) : undefined,\n });\n\n this.traces.set(sessionId, trace);\n this.rootSpans.set(sessionId, rootSpanId);\n return trace;\n }\n\n getTrace(sessionId: string): TraceBuilder | undefined {\n return this.traces.get(sessionId);\n }\n\n endSession(sessionId: string, status?: TraceStatus): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n if (rootSpanId) {\n trace.addSpan({\n id: rootSpanId,\n name: \"session\",\n type: SpanType.AGENT,\n status: status === \"ERROR\" ? SpanStatus.ERROR : SpanStatus.COMPLETED,\n endedAt: nowISO(),\n });\n }\n\n trace.end({ status: status ?? \"COMPLETED\" });\n this.traces.delete(sessionId);\n this.rootSpans.delete(sessionId);\n }\n\n startToolCall(\n callID: string,\n tool: string,\n args: unknown,\n sessionID: string,\n ): void {\n this.toolCalls.set(callID, {\n startTime: Date.now(),\n tool,\n args,\n sessionID,\n });\n }\n\n endToolCall(\n callID: string,\n output: string,\n title: string,\n metadata: unknown,\n ): void {\n const call = this.toolCalls.get(callID);\n if (!call) return;\n this.toolCalls.delete(callID);\n\n const trace = this.traces.get(call.sessionID);\n if (!trace) return;\n\n const durationMs = Date.now() - call.startTime;\n const rootSpanId = this.rootSpans.get(call.sessionID);\n const toolMeta = extractToolMetadata(call.tool, call.args);\n\n trace.addSpan({\n name: title,\n type: SpanType.TOOL_CALL,\n parentSpanId: rootSpanId,\n input: safeJsonValue(call.args),\n output: output as JsonValue,\n durationMs,\n status: SpanStatus.COMPLETED,\n startedAt: new Date(call.startTime).toISOString(),\n endedAt: nowISO(),\n metadata: safeJsonValue({ ...toolMeta, rawMetadata: metadata }),\n });\n\n trace.addDecision({\n type: DecisionType.TOOL_SELECTION,\n chosen: call.tool as JsonValue,\n alternatives: [],\n reasoning: title,\n durationMs,\n parentSpanId: rootSpanId,\n });\n }\n\n recordLLMCall(\n sessionId: string,\n options: {\n model?: { providerID: string; modelID: string };\n agent?: string;\n messageID?: string;\n },\n ): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n const agentName = options.agent ?? \"assistant\";\n const modelName = options.model?.modelID ?? \"unknown\";\n\n trace.addSpan({\n name: `${agentName} → ${modelName}`,\n type: SpanType.LLM_CALL,\n parentSpanId: rootSpanId,\n status: SpanStatus.COMPLETED,\n startedAt: nowISO(),\n endedAt: nowISO(),\n metadata: safeJsonValue({\n provider: options.model?.providerID,\n model: options.model?.modelID,\n agent: options.agent,\n messageID: options.messageID,\n }),\n });\n }\n\n recordPermission(\n sessionId: string,\n permission: unknown,\n status: string,\n ): void {\n const trace = this.traces.get(sessionId);\n if (!trace) return;\n\n const rootSpanId = this.rootSpans.get(sessionId);\n const p = permission as Record<string, unknown> | null;\n const title = (p?.[\"title\"] as string) ?? \"permission\";\n const permType = (p?.[\"type\"] as string) ?? \"unknown\";\n\n trace.addDecision({\n type: DecisionType.ESCALATION,\n chosen: safeJsonValue({ action: status }),\n alternatives: [\n \"allow\" as JsonValue,\n \"deny\" as JsonValue,\n \"ask\" as JsonValue,\n ],\n reasoning: `${permType}: ${title}`,\n parentSpanId: rootSpanId,\n });\n }\n\n getRootSpanId(sessionId: string): string | undefined {\n return this.rootSpans.get(sessionId);\n }\n}\n","import type { JsonValue } from \"agentlens-sdk\";\n\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str;\n return str.slice(0, maxLength) + \"... [truncated]\";\n}\n\nexport function extractToolMetadata(\n tool: string,\n args: unknown,\n): Record<string, unknown> {\n const a = args as Record<string, unknown> | null | undefined;\n if (!a || typeof a !== \"object\") return {};\n\n switch (tool) {\n case \"read\":\n case \"mcp_read\":\n return { filePath: a[\"filePath\"] };\n\n case \"write\":\n case \"mcp_write\":\n return { filePath: a[\"filePath\"] };\n\n case \"edit\":\n case \"mcp_edit\":\n return { filePath: a[\"filePath\"] };\n\n case \"bash\":\n case \"mcp_bash\":\n return {\n command: truncate(String(a[\"command\"] ?? \"\"), 200),\n };\n\n case \"glob\":\n case \"mcp_glob\":\n return { pattern: a[\"pattern\"] };\n\n case \"grep\":\n case \"mcp_grep\":\n return { pattern: a[\"pattern\"], path: a[\"path\"] };\n\n case \"task\":\n case \"mcp_task\":\n return {\n category: a[\"category\"],\n description: a[\"description\"],\n };\n\n default:\n return {};\n }\n}\n\nconst MODEL_COSTS: Record<string, { input: number; output: number }> = {\n \"gpt-5.2\": { input: 1.75, output: 14 },\n \"gpt-5.1\": { input: 1.25, output: 10 },\n \"gpt-5\": { input: 1.25, output: 10 },\n \"gpt-5-mini\": { input: 0.25, output: 2 },\n \"gpt-5-nano\": { input: 0.05, output: 0.4 },\n \"gpt-4.1\": { input: 2, output: 8 },\n \"gpt-4.1-mini\": { input: 0.4, output: 1.6 },\n \"gpt-4.1-nano\": { input: 0.1, output: 0.4 },\n \"o3\": { input: 2, output: 8 },\n \"o3-mini\": { input: 1.1, output: 4.4 },\n \"o4-mini\": { input: 1.1, output: 4.4 },\n \"o1\": { input: 15, output: 60 },\n \"gpt-4o\": { input: 2.5, output: 10 },\n \"gpt-4o-mini\": { input: 0.15, output: 0.6 },\n \"gpt-4-turbo\": { input: 10, output: 30 },\n \"gpt-4\": { input: 30, output: 60 },\n \"claude-opus-4-6\": { input: 5, output: 25 },\n \"claude-opus-4-20250514\": { input: 15, output: 75 },\n \"claude-sonnet-4-20250514\": { input: 3, output: 15 },\n \"claude-4.5-opus\": { input: 5, output: 25 },\n \"claude-4.5-sonnet\": { input: 3, output: 15 },\n \"claude-4.5-haiku\": { input: 1, output: 5 },\n \"claude-3-5-sonnet\": { input: 3, output: 15 },\n \"claude-3-5-haiku\": { input: 0.8, output: 4 },\n \"claude-3-opus\": { input: 15, output: 75 },\n \"claude-3-haiku\": { input: 0.25, output: 1.25 },\n};\n\nexport function getModelCost(\n modelId: string,\n): { input: number; output: number } | undefined {\n const direct = MODEL_COSTS[modelId];\n if (direct) return direct;\n\n for (const [key, cost] of Object.entries(MODEL_COSTS)) {\n if (modelId.includes(key)) return cost;\n }\n return undefined;\n}\n\n/** Coerce arbitrary values into SDK-compatible `JsonValue`, stringifying unknowns. */\nexport function safeJsonValue(value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((v) => safeJsonValue(v));\n }\n if (typeof value === \"object\") {\n const result: Record<string, JsonValue> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = safeJsonValue(v);\n }\n return result;\n }\n return String(value);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "opencode-agentlens",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin for AgentLens — trace your coding agent's decisions, tool calls, and sessions",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "dependencies": {
29
+ "agentlens-sdk": "*"
30
+ },
31
+ "devDependencies": {
32
+ "@opencode-ai/plugin": "^1.1.53",
33
+ "tsup": "^8.3.0",
34
+ "typescript": "^5.7.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@opencode-ai/plugin": ">=1.1.0"
38
+ },
39
+ "keywords": [
40
+ "opencode",
41
+ "agentlens",
42
+ "observability",
43
+ "tracing",
44
+ "coding-agent"
45
+ ],
46
+ "license": "MIT",
47
+ "author": "Vectry <hunter@repi.fun>",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://gitea.repi.fun/repi/agentlens",
51
+ "directory": "packages/opencode-plugin"
52
+ },
53
+ "homepage": "https://agentlens.vectry.tech",
54
+ "bugs": {
55
+ "url": "https://gitea.repi.fun/repi/agentlens/issues"
56
+ }
57
+ }