@agentlogs/opencode 0.0.1 → 0.0.2
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/dist/index.js +1 -199
- package/package.json +2 -2
- package/src/index.ts +126 -180
- package/dist/dev.js +0 -11919
package/dist/index.js
CHANGED
|
@@ -1,199 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "AgentLogs plugin for OpenCode - automatically captures and uploads AI coding session transcripts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentlogs",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"types": "dist/index.d.ts"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target node",
|
|
31
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --define 'process.env.NODE_ENV=\"production\"' --minify",
|
|
32
32
|
"prepublishOnly": "bun run build",
|
|
33
33
|
"check": "tsgo --project tsconfig.json"
|
|
34
34
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentLogs OpenCode Plugin
|
|
3
3
|
*
|
|
4
|
-
* Lightweight plugin that
|
|
5
|
-
*
|
|
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
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
68
|
-
* Passes
|
|
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
|
|
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", "
|
|
76
|
+
args = [...parts.slice(1), "opencode", "hook"];
|
|
83
77
|
} else {
|
|
84
78
|
command = "npx";
|
|
85
|
-
args = ["-y", "agentlogs@latest", "opencode", "
|
|
79
|
+
args = ["-y", "agentlogs@latest", "opencode", "hook"];
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
log("
|
|
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: ["
|
|
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("
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
182
|
+
log("tool.execute.before (git commit)", {
|
|
183
|
+
tool: input.tool,
|
|
184
|
+
sessionID: input.sessionID,
|
|
185
|
+
callID: input.callID,
|
|
186
|
+
});
|
|
254
187
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
};
|