@agentlogs/opencode 0.0.1 → 0.0.3

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.
Files changed (4) hide show
  1. package/dist/index.js +1 -199
  2. package/package.json +3 -4
  3. package/src/index.ts +126 -180
  4. package/dist/dev.js +0 -11919
package/dist/index.js CHANGED
@@ -1,199 +1 @@
1
- // src/index.ts
2
- import { appendFileSync } from "node:fs";
3
- import { spawn } from "node:child_process";
4
- var LOG_FILE = "/tmp/agentlogs-opencode.log";
5
- function log(message, data) {
6
- const timestamp = new Date().toISOString();
7
- const logLine = data ? `[${timestamp}] ${message}
8
- ${JSON.stringify(data, null, 2)}
9
- ` : `[${timestamp}] ${message}
10
- `;
11
- try {
12
- appendFileSync(LOG_FILE, logLine);
13
- } catch {}
14
- }
15
- async function uploadViaCli(sessionId, cwd) {
16
- const cliPath = process.env.VI_CLI_PATH;
17
- let command;
18
- let args;
19
- if (cliPath) {
20
- const parts = cliPath.split(" ");
21
- command = parts[0];
22
- args = [...parts.slice(1), "opencode", "upload", sessionId];
23
- } else {
24
- command = "npx";
25
- args = ["-y", "agentlogs@latest", "opencode", "upload", sessionId];
26
- }
27
- log("Spawning CLI", { command, args, sessionId });
28
- return new Promise((resolve) => {
29
- const proc = spawn(command, args, {
30
- cwd,
31
- stdio: ["ignore", "pipe", "pipe"]
32
- });
33
- let stdout = "";
34
- let stderr = "";
35
- proc.stdout.on("data", (data) => {
36
- stdout += data.toString();
37
- });
38
- proc.stderr.on("data", (data) => {
39
- stderr += data.toString();
40
- });
41
- proc.on("close", (code) => {
42
- log("CLI process exited", { code, stdout, stderr });
43
- if (code === 0) {
44
- try {
45
- const result = JSON.parse(stdout.trim());
46
- resolve({
47
- success: true,
48
- transcriptId: result.transcriptId,
49
- transcriptUrl: result.transcriptUrl
50
- });
51
- } catch {
52
- resolve({
53
- success: true,
54
- error: "Failed to parse CLI output"
55
- });
56
- }
57
- } else {
58
- resolve({
59
- success: false,
60
- error: stderr || `CLI exited with code ${code}`
61
- });
62
- }
63
- });
64
- proc.on("error", (err) => {
65
- log("CLI spawn error", { error: String(err) });
66
- resolve({
67
- success: false,
68
- error: `Failed to spawn agentlogs CLI: ${err.message}`
69
- });
70
- });
71
- });
72
- }
73
- function isGitCommitCommand(input) {
74
- if (!input || typeof input !== "object")
75
- return false;
76
- const record = input;
77
- const cmd = Array.isArray(record.command) ? record.command.join(" ") : typeof record.command === "string" ? record.command : "";
78
- return /\bgit\s+commit\b/.test(cmd);
79
- }
80
- function appendTranscriptLinkToCommit(input, transcriptUrl) {
81
- if (!input || typeof input !== "object")
82
- return null;
83
- const record = { ...input };
84
- let cmdString;
85
- if (Array.isArray(record.command)) {
86
- cmdString = record.command.join(" ");
87
- } else if (typeof record.command === "string") {
88
- cmdString = record.command;
89
- } else {
90
- return null;
91
- }
92
- const messageMatch = cmdString.match(/-m\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
93
- const existingMessage = messageMatch?.[1] || messageMatch?.[2] || messageMatch?.[3];
94
- if (!existingMessage)
95
- return null;
96
- const newMessage = `${existingMessage}
97
-
98
- Transcript: ${transcriptUrl}`;
99
- if (Array.isArray(record.command)) {
100
- const cmdArray = [...record.command];
101
- for (let i = 0;i < cmdArray.length; i++) {
102
- if (cmdArray[i] === "-m" && i + 1 < cmdArray.length) {
103
- cmdArray[i + 1] = newMessage;
104
- break;
105
- }
106
- }
107
- record.command = cmdArray;
108
- } else {
109
- record.command = cmdString.replace(/-m\s+(?:"[^"]+"|'[^']+'|\S+)/, `-m "${newMessage.replace(/"/g, "\\\"")}"`);
110
- }
111
- return record;
112
- }
113
- var agentLogsPlugin = async (ctx) => {
114
- const state = {
115
- sessions: new Map,
116
- uploading: new Set
117
- };
118
- log("Plugin initialized", {
119
- directory: ctx.directory,
120
- projectId: ctx.project?.id
121
- });
122
- return {
123
- event: async (rawEvent) => {
124
- const event = rawEvent?.event ?? rawEvent;
125
- const eventType = event?.type;
126
- const properties = event?.properties;
127
- log(`Event: ${eventType}`, properties);
128
- if (eventType === "session.created") {
129
- const sessionId = properties?.info?.id;
130
- const parentId = properties?.info?.parentID;
131
- const isSubagent = !!parentId;
132
- if (sessionId) {
133
- state.sessions.set(sessionId, {
134
- isSubagent,
135
- transcriptUrl: null
136
- });
137
- log("Session created", { sessionId, isSubagent, parentId });
138
- }
139
- }
140
- if (eventType === "session.idle") {
141
- const sessionId = properties?.sessionID;
142
- if (!sessionId)
143
- return;
144
- const session = state.sessions.get(sessionId);
145
- if (session?.isSubagent) {
146
- log("Skipping subagent session", { sessionId });
147
- return;
148
- }
149
- if (state.uploading.has(sessionId)) {
150
- log("Already uploading session", { sessionId });
151
- return;
152
- }
153
- state.uploading.add(sessionId);
154
- log("Session idle, uploading", { sessionId });
155
- try {
156
- const result = await uploadViaCli(sessionId, ctx.directory);
157
- if (result.success && result.transcriptUrl) {
158
- if (session) {
159
- session.transcriptUrl = result.transcriptUrl;
160
- }
161
- log("Upload success", { sessionId, url: result.transcriptUrl });
162
- } else {
163
- log("Upload failed", { sessionId, error: result.error });
164
- }
165
- } catch (error) {
166
- log("Session idle error", { sessionId, error: String(error) });
167
- } finally {
168
- state.uploading.delete(sessionId);
169
- }
170
- }
171
- },
172
- tool: {
173
- execute: {
174
- before: async (args) => {
175
- let transcriptUrl = null;
176
- for (const [, session] of state.sessions) {
177
- if (!session.isSubagent && session.transcriptUrl) {
178
- transcriptUrl = session.transcriptUrl;
179
- }
180
- }
181
- if ((args.name === "shell" || args.name === "bash") && isGitCommitCommand(args.input) && transcriptUrl) {
182
- log("Intercepting git commit", { transcriptUrl });
183
- const modified = appendTranscriptLinkToCommit(args.input, transcriptUrl);
184
- if (modified) {
185
- log("Added transcript link to commit");
186
- return { ...args, input: modified };
187
- }
188
- }
189
- return args;
190
- }
191
- }
192
- }
193
- };
194
- };
195
- var src_default = agentLogsPlugin;
196
- export {
197
- src_default as default,
198
- agentLogsPlugin
199
- };
1
+ import{appendFileSync as U}from"node:fs";import{spawn as V}from"node:child_process";var W="/tmp/agentlogs-opencode.log";function D(q,J){return}async function N(q,J){let b=process.env.VI_CLI_PATH,j,B;if(b){let z=b.split(" ");j=z[0],B=[...z.slice(1),"opencode","hook"]}else j="npx",B=["-y","agentlogs@latest","opencode","hook"];return D("Running hook",{command:j,args:B,payload:q.hook_event_name}),new Promise((z)=>{let A=V(j,B,{cwd:J,stdio:["pipe","pipe","pipe"]}),K="",Q="";A.stdout.on("data",(M)=>{K+=M.toString()}),A.stderr.on("data",(M)=>{Q+=M.toString()}),A.stdin.write(JSON.stringify(q)),A.stdin.end(),A.on("close",(M)=>{if(D("Hook process exited",{code:M,stdout:K.slice(0,500),stderr:Q.slice(0,500)}),M===0&&K.trim())try{let R=JSON.parse(K.trim());z(R)}catch{z({modified:!1})}else z({modified:!1})}),A.on("error",(M)=>{D("Hook spawn error",{error:String(M)}),z({modified:!1})})})}var X=async(q)=>{D("Plugin initialized",{directory:q.directory,projectId:q.project?.id});let J=new Set;return{event:async(b)=>{let j=b?.event??b,B=j?.type,z=j?.properties;if(B==="session.idle"){let A=z?.sessionID;if(!A)return;D("session.idle",{sessionId:A}),N({hook_event_name:"session.idle",session_id:A,cwd:q.directory},q.directory).catch((K)=>D("session.idle hook error",{error:String(K)}))}},"tool.execute.before":async(b,j)=>{if(b.tool!=="bash")return;let B=j.args?.command;if(typeof B!=="string"||!/\bgit\s+commit\b/.test(B))return;D("tool.execute.before (git commit)",{tool:b.tool,sessionID:b.sessionID,callID:b.callID});let z=await N({hook_event_name:"tool.execute.before",session_id:b.sessionID,call_id:b.callID,tool:b.tool,tool_input:j.args,cwd:q.directory},q.directory);if(z.modified&&z.args)D("tool.execute.before: args modified",{modified:!0}),J.add(b.callID),Object.assign(j.args,z.args)},"tool.execute.after":async(b,j)=>{if(b.tool!=="bash")return;let B=J.has(b.callID),A=(j.output||"").includes("agentlogs.ai/s/");if(!B&&!A)return;J.delete(b.callID),N({hook_event_name:"tool.execute.after",session_id:b.sessionID,call_id:b.callID,tool:b.tool,tool_output:j,cwd:q.directory},q.directory).catch((K)=>D("tool.execute.after hook error",{error:String(K)}))}}},G=X;export{G as default,X as agentLogsPlugin};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlogs/opencode",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "AgentLogs plugin for OpenCode - automatically captures and uploads AI coding session transcripts",
5
5
  "keywords": [
6
6
  "agentlogs",
@@ -16,8 +16,7 @@
16
16
  "directory": "packages/opencode"
17
17
  },
18
18
  "files": [
19
- "dist",
20
- "src"
19
+ "dist"
21
20
  ],
22
21
  "type": "module",
23
22
  "main": "src/index.ts",
@@ -28,7 +27,7 @@
28
27
  "types": "dist/index.d.ts"
29
28
  },
30
29
  "scripts": {
31
- "build": "bun build ./src/index.ts --outdir ./dist --target node",
30
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --define 'process.env.NODE_ENV=\"production\"' --minify",
32
31
  "prepublishOnly": "bun run build",
33
32
  "check": "tsgo --project tsconfig.json"
34
33
  },
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * AgentLogs OpenCode Plugin
3
3
  *
4
- * Lightweight plugin that captures OpenCode session data and uploads via the agentlogs CLI.
5
- * No external dependencies - all heavy lifting is done by the CLI.
4
+ * Lightweight plugin that shells out to the agentlogs CLI for all processing.
5
+ * The CLI handles transcript uploads, git commit interception, and commit tracking.
6
6
  *
7
7
  * @example
8
8
  * // opencode.json
@@ -13,12 +13,13 @@ import { appendFileSync } from "node:fs";
13
13
  import { spawn } from "node:child_process";
14
14
 
15
15
  // ============================================================================
16
- // Debug Logging
16
+ // Debug Logging (compiled out in production builds)
17
17
  // ============================================================================
18
18
 
19
19
  const LOG_FILE = "/tmp/agentlogs-opencode.log";
20
20
 
21
21
  function log(message: string, data?: unknown): void {
22
+ if (process.env.NODE_ENV === "production") return;
22
23
  const timestamp = new Date().toISOString();
23
24
  const logLine = data
24
25
  ? `[${timestamp}] ${message}\n${JSON.stringify(data, null, 2)}\n`
@@ -31,7 +32,7 @@ function log(message: string, data?: unknown): void {
31
32
  }
32
33
 
33
34
  // ============================================================================
34
- // Types (minimal, no external deps)
35
+ // Types
35
36
  // ============================================================================
36
37
 
37
38
  interface PluginContext {
@@ -40,37 +41,30 @@ interface PluginContext {
40
41
  project?: { id: string; path: string };
41
42
  }
42
43
 
43
- interface SessionInfo {
44
- isSubagent: boolean;
45
- transcriptUrl: string | null;
44
+ interface HookPayload {
45
+ hook_event_name: string;
46
+ session_id: string;
47
+ call_id?: string;
48
+ tool?: string;
49
+ cwd?: string;
50
+ tool_input?: Record<string, unknown>;
51
+ tool_output?: Record<string, unknown>;
46
52
  }
47
53
 
48
- interface PluginState {
49
- // Track sessions: sessionId → info
50
- sessions: Map<string, SessionInfo>;
51
- // Currently uploading session IDs (to prevent concurrent uploads)
52
- uploading: Set<string>;
54
+ interface HookResponse {
55
+ modified: boolean;
56
+ args?: Record<string, unknown>;
53
57
  }
54
58
 
55
59
  // ============================================================================
56
60
  // CLI Integration
57
61
  // ============================================================================
58
62
 
59
- interface UploadResult {
60
- success: boolean;
61
- transcriptId?: string;
62
- transcriptUrl?: string;
63
- error?: string;
64
- }
65
-
66
63
  /**
67
- * Upload transcript by shelling out to the agentlogs CLI.
68
- * Passes session ID - CLI reads directly from OpenCode storage.
69
- * Uses $VI_CLI_PATH if set, otherwise falls back to npx.
64
+ * Shell out to the agentlogs CLI hook command.
65
+ * Passes hook data via stdin, receives response via stdout.
70
66
  */
71
- async function uploadViaCli(sessionId: string, cwd: string): Promise<UploadResult> {
72
- // Use VI_CLI_PATH if set, otherwise fall back to npx
73
- // VI_CLI_PATH can be "bun /path/to/cli.ts" or just "/path/to/agentlogs"
67
+ async function runHook(payload: HookPayload, cwd: string): Promise<HookResponse> {
74
68
  const cliPath = process.env.VI_CLI_PATH;
75
69
 
76
70
  let command: string;
@@ -79,18 +73,18 @@ async function uploadViaCli(sessionId: string, cwd: string): Promise<UploadResul
79
73
  if (cliPath) {
80
74
  const parts = cliPath.split(" ");
81
75
  command = parts[0];
82
- args = [...parts.slice(1), "opencode", "upload", sessionId];
76
+ args = [...parts.slice(1), "opencode", "hook"];
83
77
  } else {
84
78
  command = "npx";
85
- args = ["-y", "agentlogs@latest", "opencode", "upload", sessionId];
79
+ args = ["-y", "agentlogs@latest", "opencode", "hook"];
86
80
  }
87
81
 
88
- log("Spawning CLI", { command, args, sessionId });
82
+ log("Running hook", { command, args, payload: payload.hook_event_name });
89
83
 
90
84
  return new Promise((resolve) => {
91
85
  const proc = spawn(command, args, {
92
86
  cwd,
93
- stdio: ["ignore", "pipe", "pipe"],
87
+ stdio: ["pipe", "pipe", "pipe"],
94
88
  });
95
89
 
96
90
  let stdout = "";
@@ -103,199 +97,151 @@ async function uploadViaCli(sessionId: string, cwd: string): Promise<UploadResul
103
97
  stderr += data.toString();
104
98
  });
105
99
 
100
+ // Send payload via stdin
101
+ proc.stdin.write(JSON.stringify(payload));
102
+ proc.stdin.end();
103
+
106
104
  proc.on("close", (code) => {
107
- log("CLI process exited", { code, stdout, stderr });
105
+ log("Hook process exited", { code, stdout: stdout.slice(0, 500), stderr: stderr.slice(0, 500) });
108
106
 
109
- if (code === 0) {
110
- // Parse JSON output from CLI
107
+ if (code === 0 && stdout.trim()) {
111
108
  try {
112
- const result = JSON.parse(stdout.trim());
113
- resolve({
114
- success: true,
115
- transcriptId: result.transcriptId,
116
- transcriptUrl: result.transcriptUrl,
117
- });
109
+ const response = JSON.parse(stdout.trim()) as HookResponse;
110
+ resolve(response);
118
111
  } catch {
119
- resolve({
120
- success: true,
121
- error: "Failed to parse CLI output",
122
- });
112
+ resolve({ modified: false });
123
113
  }
124
114
  } else {
125
- resolve({
126
- success: false,
127
- error: stderr || `CLI exited with code ${code}`,
128
- });
115
+ resolve({ modified: false });
129
116
  }
130
117
  });
131
118
 
132
119
  proc.on("error", (err) => {
133
- log("CLI spawn error", { error: String(err) });
134
- resolve({
135
- success: false,
136
- error: `Failed to spawn agentlogs CLI: ${err.message}`,
137
- });
120
+ log("Hook spawn error", { error: String(err) });
121
+ resolve({ modified: false });
138
122
  });
139
123
  });
140
124
  }
141
125
 
142
- // ============================================================================
143
- // Git Utilities
144
- // ============================================================================
145
-
146
- function isGitCommitCommand(input: unknown): boolean {
147
- if (!input || typeof input !== "object") return false;
148
- const record = input as Record<string, unknown>;
149
-
150
- const cmd = Array.isArray(record.command)
151
- ? record.command.join(" ")
152
- : typeof record.command === "string"
153
- ? record.command
154
- : "";
155
-
156
- return /\bgit\s+commit\b/.test(cmd);
157
- }
158
-
159
- function appendTranscriptLinkToCommit(input: unknown, transcriptUrl: string): Record<string, unknown> | null {
160
- if (!input || typeof input !== "object") return null;
161
- const record = { ...(input as Record<string, unknown>) };
162
-
163
- let cmdString: string;
164
- if (Array.isArray(record.command)) {
165
- cmdString = record.command.join(" ");
166
- } else if (typeof record.command === "string") {
167
- cmdString = record.command;
168
- } else {
169
- return null;
170
- }
171
-
172
- // Extract existing message
173
- const messageMatch = cmdString.match(/-m\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
174
- const existingMessage = messageMatch?.[1] || messageMatch?.[2] || messageMatch?.[3];
175
- if (!existingMessage) return null;
176
-
177
- const newMessage = `${existingMessage}\n\nTranscript: ${transcriptUrl}`;
178
-
179
- if (Array.isArray(record.command)) {
180
- const cmdArray = [...(record.command as string[])];
181
- for (let i = 0; i < cmdArray.length; i++) {
182
- if (cmdArray[i] === "-m" && i + 1 < cmdArray.length) {
183
- cmdArray[i + 1] = newMessage;
184
- break;
185
- }
186
- }
187
- record.command = cmdArray;
188
- } else {
189
- record.command = cmdString.replace(/-m\s+(?:"[^"]+"|'[^']+'|\S+)/, `-m "${newMessage.replace(/"/g, '\\"')}"`);
190
- }
191
-
192
- return record;
193
- }
194
-
195
126
  // ============================================================================
196
127
  // Main Plugin
197
128
  // ============================================================================
198
129
 
199
130
  export const agentLogsPlugin = async (ctx: PluginContext) => {
200
- const state: PluginState = {
201
- sessions: new Map(),
202
- uploading: new Set(),
203
- };
204
-
205
131
  log("Plugin initialized", {
206
132
  directory: ctx.directory,
207
133
  projectId: ctx.project?.id,
208
134
  });
209
135
 
136
+ // Track callIds where we intercepted a git commit
137
+ // Used to know when to call CLI in after hook (git output may not include our link)
138
+ const interceptedCallIds = new Set<string>();
139
+
210
140
  return {
141
+ // Handle session events (for session.idle upload)
211
142
  event: async (rawEvent: any) => {
212
- // OpenCode wraps events: { event: { type, properties } }
213
143
  const event = rawEvent?.event ?? rawEvent;
214
144
  const eventType = event?.type;
215
145
  const properties = event?.properties;
216
146
 
217
- log(`Event: ${eventType}`, properties);
218
-
219
- if (eventType === "session.created") {
220
- const sessionId = properties?.info?.id;
221
- const parentId = properties?.info?.parentID;
222
- const isSubagent = !!parentId;
223
-
224
- if (sessionId) {
225
- state.sessions.set(sessionId, {
226
- isSubagent,
227
- transcriptUrl: null,
228
- });
229
- log("Session created", { sessionId, isSubagent, parentId });
230
- }
231
- }
232
-
147
+ // Only handle session.idle for uploads
233
148
  if (eventType === "session.idle") {
234
- // Use session ID from the event, not from state
235
149
  const sessionId = properties?.sessionID;
236
150
  if (!sessionId) return;
237
151
 
238
- const session = state.sessions.get(sessionId);
152
+ log("session.idle", { sessionId });
153
+
154
+ // Fire and forget - CLI handles the upload
155
+ runHook(
156
+ {
157
+ hook_event_name: "session.idle",
158
+ session_id: sessionId,
159
+ cwd: ctx.directory,
160
+ },
161
+ ctx.directory,
162
+ ).catch((err) => log("session.idle hook error", { error: String(err) }));
163
+ }
164
+ },
239
165
 
240
- // Skip subagent sessions
241
- if (session?.isSubagent) {
242
- log("Skipping subagent session", { sessionId });
243
- return;
244
- }
166
+ // Hook: Called before any tool executes
167
+ "tool.execute.before": async (
168
+ input: { tool: string; sessionID: string; callID: string },
169
+ output: { args: any },
170
+ ) => {
171
+ // Only intercept bash/shell tools
172
+ if (input.tool !== "bash") {
173
+ return;
174
+ }
245
175
 
246
- // Skip if already uploading this session
247
- if (state.uploading.has(sessionId)) {
248
- log("Already uploading session", { sessionId });
249
- return;
250
- }
176
+ // Quick check: skip if not a git commit
177
+ const command = output.args?.command;
178
+ if (typeof command !== "string" || !/\bgit\s+commit\b/.test(command)) {
179
+ return;
180
+ }
251
181
 
252
- state.uploading.add(sessionId);
253
- log("Session idle, uploading", { sessionId });
182
+ log("tool.execute.before (git commit)", {
183
+ tool: input.tool,
184
+ sessionID: input.sessionID,
185
+ callID: input.callID,
186
+ });
254
187
 
255
- try {
256
- // Upload via CLI - it reads directly from OpenCode storage
257
- const result = await uploadViaCli(sessionId, ctx.directory);
258
-
259
- if (result.success && result.transcriptUrl) {
260
- // Store transcript URL for this session (for git commit linking)
261
- if (session) {
262
- session.transcriptUrl = result.transcriptUrl;
263
- }
264
- log("Upload success", { sessionId, url: result.transcriptUrl });
265
- } else {
266
- log("Upload failed", { sessionId, error: result.error });
267
- }
268
- } catch (error) {
269
- log("Session idle error", { sessionId, error: String(error) });
270
- } finally {
271
- state.uploading.delete(sessionId);
272
- }
188
+ const response = await runHook(
189
+ {
190
+ hook_event_name: "tool.execute.before",
191
+ session_id: input.sessionID,
192
+ call_id: input.callID,
193
+ tool: input.tool,
194
+ tool_input: output.args,
195
+ cwd: ctx.directory,
196
+ },
197
+ ctx.directory,
198
+ );
199
+
200
+ if (response.modified && response.args) {
201
+ log("tool.execute.before: args modified", { modified: true });
202
+ // Track this callId so we know to call CLI in after hook
203
+ interceptedCallIds.add(input.callID);
204
+ // Mutate in place - don't replace the reference, as OpenCode passes { args } by reference
205
+ Object.assign(output.args, response.args);
273
206
  }
274
207
  },
275
208
 
276
- tool: {
277
- execute: {
278
- before: async (args: { name: string; input: unknown }) => {
279
- // Intercept git commits to add transcript link
280
- // Use the most recent non-subagent session's transcript URL
281
- let transcriptUrl: string | null = null;
282
- for (const [, session] of state.sessions) {
283
- if (!session.isSubagent && session.transcriptUrl) {
284
- transcriptUrl = session.transcriptUrl;
285
- }
286
- }
287
-
288
- if ((args.name === "shell" || args.name === "bash") && isGitCommitCommand(args.input) && transcriptUrl) {
289
- log("Intercepting git commit", { transcriptUrl });
290
- const modified = appendTranscriptLinkToCommit(args.input, transcriptUrl);
291
- if (modified) {
292
- log("Added transcript link to commit");
293
- return { ...args, input: modified };
294
- }
295
- }
296
- return args;
209
+ // Hook: Called after any tool executes
210
+ "tool.execute.after": async (
211
+ input: { tool: string; sessionID: string; callID: string },
212
+ output: { title: string; output: string; metadata: any },
213
+ ) => {
214
+ // Only handle bash tool
215
+ if (input.tool !== "bash") {
216
+ return;
217
+ }
218
+
219
+ // Check if we should call CLI:
220
+ // 1. This callId was intercepted in before hook (we modified the commit command)
221
+ // 2. Output contains our transcript link (fallback check)
222
+ const wasIntercepted = interceptedCallIds.has(input.callID);
223
+ const cmdOutput = output.output || "";
224
+ const hasLink = cmdOutput.includes("agentlogs.ai/s/");
225
+
226
+ if (!wasIntercepted && !hasLink) {
227
+ return;
228
+ }
229
+
230
+ // Clean up tracked callId
231
+ interceptedCallIds.delete(input.callID);
232
+
233
+ // Fire and forget - CLI handles commit tracking
234
+ runHook(
235
+ {
236
+ hook_event_name: "tool.execute.after",
237
+ session_id: input.sessionID,
238
+ call_id: input.callID,
239
+ tool: input.tool,
240
+ tool_output: output,
241
+ cwd: ctx.directory,
297
242
  },
298
- },
243
+ ctx.directory,
244
+ ).catch((err) => log("tool.execute.after hook error", { error: String(err) }));
299
245
  },
300
246
  };
301
247
  };