snapshot of c000b74a5b09c433f96a69fa102c5c8f6583dff0 Annotations about the code that implements koment.

integrations/agent-plugins/opencode/index.js

1 import { spawn } from "node:child_process";
2 import { readFileSync } from "node:fs";
3
4 const packageVersion = JSON.parse(
5 readFileSync(new URL("./package.json", import.meta.url), "utf8")
6 ).version;
7
8 export function connectMCP(child) {
9 const pending = new Map();
10 let nextID = 1;
11 let buffer = "";
12 let terminalError;
13
14 function failConnection(error) {
15 if (terminalError) return;
16 terminalError = error;
17 for (const waiter of pending.values()) waiter.reject(error);
18 pending.clear();
19 }
20
21 child.stderr.on("data", (chunk) => {
22 const text = chunk.toString("utf8");
23 for (const line of text.split("\n")) {
24 if (line) process.stderr.write(`[koment-mcp] ${line}\n`);
25 }
26 });
27
28 child.stdout.on("data", (chunk) => {
29 buffer += chunk.toString("utf8");
30 let newline;
31 while ((newline = buffer.indexOf("\n")) !== -1) {
32 const line = buffer.slice(0, newline);
33 buffer = buffer.slice(newline + 1);
34 if (!line) continue;
35 let message;
36 try {
37 message = JSON.parse(line);
38 } catch (error) {
39 failConnection(new Error("koment MCP emitted invalid JSON", { cause: error }));
40 child.kill();
41 return;
42 }
43 if (!("id" in message)) {
44 continue;
45 }
46 const id = message.id;
47 const waiter = pending.get(id);
48 if (!waiter) {
49 failConnection(new Error(`koment MCP returned unexpected response id ${id}`));
50 child.kill();
51 return;
52 }
53 pending.delete(id);
54 if (message.error) {
55 waiter.reject(new Error(message.error.message || "mcp error"));
56 } else {
57 waiter.resolve(message.result ?? {});
58 }
59 }
60 });
61
62 child.on("error", failConnection);
63 child.stdin.on("error", failConnection);
64 child.stdout.on("error", failConnection);
65 child.stderr.on("error", failConnection);
66 child.on("exit", (code, signal) => {
67 const outcome = signal ? `signal=${signal}` : `code=${code}`;
68 failConnection(new Error(`koment mcp exited (${outcome})`));
69 });
70
71 function request(method, params) {
72 if (terminalError) return Promise.reject(terminalError);
73 const id = nextID++;
74 const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params: params || {} }) + "\n";
75 return new Promise((resolve, reject) => {
76 pending.set(id, { resolve, reject });
77 child.stdin.write(payload, (err) => {
78 if (err) failConnection(err);
79 });
80 });
81 }
82
83 function notify(method, params) {
84 if (terminalError) return Promise.reject(terminalError);
85 const payload = JSON.stringify({ jsonrpc: "2.0", method, params: params || {} }) + "\n";
86 return new Promise((resolve, reject) => {
87 child.stdin.write(payload, (error) => {
88 if (error) {
89 failConnection(error);
90 reject(error);
91 return;
92 }
93 resolve();
94 });
95 });
96 }
97
98 return {
99 child,
100 request,
101 initialize() {
102 return request("initialize", {
103 protocolVersion: "2024-11-05",
104 capabilities: {},
105 clientInfo: { name: "@koment/opencode-koment", version: packageVersion },
106 });
107 },
108 notify,
109 async callTool(name, args) {
110 const result = await request("tools/call", { name, arguments: args || {} });
111 if (Array.isArray(result.content)) {
112 for (const block of result.content) {
113 if (block && block.type === "text" && block.text) {
114 process.stderr.write(`[koment-mcp:${name}] ${block.text}\n`);
115 }
116 }
117 }
118 return result;
119 },
120 close() {
121 if (terminalError) return Promise.reject(terminalError);
122 return new Promise((resolve, reject) => {
123 child.stdin.end((error) => {
124 if (error) reject(error);
125 else resolve();
126 });
127 });
128 },
129 };
130 }
131
132 function startMCP(directory) {
133 return connectMCP(
134 spawn("koment", ["mcp", "--write"], {
135 cwd: directory,
136 env: process.env,
137 stdio: ["pipe", "pipe", "pipe"],
138 })
139 );
140 }
141
142 function run(cmd, args, { cwd, stdin } = {}) {
143 return new Promise((resolve, reject) => {
144 const child = spawn(cmd, args, {
145 cwd,
146 env: process.env,
147 stdio: ["pipe", "pipe", "pipe"],
148 });
149 let stdout = "";
150 let stderr = "";
151 child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
152 child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8")));
153 child.on("error", reject);
154 if (stdin !== undefined && stdin !== null) {
155 child.stdin.end(stdin);
156 } else {
157 child.stdin.end();
158 }
159 child.on("close", (code) => {
160 if (code === 0) resolve({ stdout, stderr });
161 else {
162 const error = new Error("koment " + args.join(" ") + " exited with code " + code);
163 error.stdout = stdout;
164 error.stderr = stderr;
165 error.code = code;
166 reject(error);
167 }
168 });
169 });
170 }
171
172 function deny(reason) {
173 throw new Error(reason);
174 }
175
176 export default async ({ directory }) => {
177 let mcp;
178 try {
179 mcp = startMCP(directory);
180 await mcp.initialize();
181 await mcp.notify("notifications/initialized");
182 } catch (err) {
183 throw new Error(
184 `koment plugin failed to start the MCP server in ${directory}:\n` +
185 (err && err.message ? err.message : String(err)) +
186 "\nInstall the \`koment\` binary on PATH: https://github.com/koment-dev/koment/releases"
187 );
188 }
189
190 return {
191 "tool.execute.before": async (input, output) => {
192 const tool = input.tool;
193 if (tool !== "edit" && tool !== "write") return;
194 const args = output.args ?? {};
195 const filePath = args.filePath ?? args.path ?? args.file ?? "";
196 const content = args.content ?? args.newContent ?? args.text ?? "";
197 if (!filePath || typeof content !== "string") return;
198 try {
199 const result = await mcp.callTool("koment_pre_tool", {
200 tool_name: "opencode_edit",
201 filePath,
202 content,
203 });
204 const structured = result.structuredContent || {};
205 if (structured.decision === "deny") {
206 deny(structured.reason || "koment policy denied this edit");
207 }
208 } catch (err) {
209 deny("koment pre-tool MCP call failed: " + (err && err.message ? err.message : String(err)));
210 }
211 },
212 dispose: async () => {
213 const failures = [];
214 for (const command of [
215 ["check"],
216 ["comments", "check"],
217 ["agents", "check"],
218 ]) {
219 try {
220 await run("koment", command, { cwd: directory });
221 } catch (err) {
222 failures.push(
223 "koment " + command.join(" ") + ":\n" + (err.stderr || err.message || String(err))
224 );
225 }
226 }
227 try {
228 await mcp.close();
229 } catch (err) {
230 failures.push("close koment mcp:\n" + (err.message || String(err)));
231 }
232 if (failures.length > 0) {
233 deny("koment policy gate failed:\n" + failures.join("\n"));
234 }
235 },
236 };
237 };

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close