@pragma-sh/opencode-plugin 0.1.0-alpha.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/assets/opencode.svg +1 -0
- package/dist/index.mjs +1277 -0
- package/dist/pragma-plugin.mjs +586 -0
- package/package.json +47 -0
- package/pragma-plugin.json +14 -0
- package/scripts/install.mjs +23 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
// ../sdk/dist/index.js
|
|
2
|
+
var PRAGMA_ENV_KEYS = {
|
|
3
|
+
gatewayUrl: "PRAGMA_GATEWAY_URL",
|
|
4
|
+
gatewayToken: "PRAGMA_GATEWAY_TOKEN",
|
|
5
|
+
tabId: "PRAGMA_TAB_ID",
|
|
6
|
+
worktreeId: "PRAGMA_WORKTREE_ID"
|
|
7
|
+
};
|
|
8
|
+
function readEnv(name, env) {
|
|
9
|
+
const globalProcess = globalThis.process;
|
|
10
|
+
return env?.[name] ?? globalProcess?.env?.[name];
|
|
11
|
+
}
|
|
12
|
+
function hasPragmaEnvironment(env) {
|
|
13
|
+
return Boolean(readEnv(PRAGMA_ENV_KEYS.gatewayUrl, env) && readEnv(PRAGMA_ENV_KEYS.gatewayToken, env) && readEnv(PRAGMA_ENV_KEYS.tabId, env) && readEnv(PRAGMA_ENV_KEYS.worktreeId, env));
|
|
14
|
+
}
|
|
15
|
+
var routes = {
|
|
16
|
+
health: "/v1/health",
|
|
17
|
+
version: "/v1/version",
|
|
18
|
+
rpc: (method) => `/v1/rpc/${encodeURIComponent(method)}`,
|
|
19
|
+
sessions: "/v1/sessions",
|
|
20
|
+
sessionEvents: (id) => `/v1/sessions/${encodeURIComponent(id)}/events`,
|
|
21
|
+
sessionInput: (id) => `/v1/sessions/${encodeURIComponent(id)}/input`,
|
|
22
|
+
sessionResize: (id) => `/v1/sessions/${encodeURIComponent(id)}/resize`,
|
|
23
|
+
session: (id) => `/v1/sessions/${encodeURIComponent(id)}`,
|
|
24
|
+
agentReports: "/v1/agents/reports",
|
|
25
|
+
agentMessages: "/v1/agents/messages",
|
|
26
|
+
agentAnswers: "/v1/agents/answers",
|
|
27
|
+
agentDecisions: "/v1/agents/decisions",
|
|
28
|
+
agentInputs: "/v1/agents/inputs",
|
|
29
|
+
agentInterrupts: "/v1/agents/interrupts",
|
|
30
|
+
agentCatalog: "/v1/agents/catalog",
|
|
31
|
+
agentEvents: "/v1/agents/events",
|
|
32
|
+
asset: (hash) => `/v1/assets/${encodeURIComponent(hash)}`,
|
|
33
|
+
theme: "/v1/theme",
|
|
34
|
+
scratchpads: "/v1/scratchpads",
|
|
35
|
+
agentsSeen: (tabId) => `/v1/tabs/${encodeURIComponent(tabId)}/agents/seen`,
|
|
36
|
+
subscription: (event) => `/v1/subscriptions/${encodeURIComponent(event)}`,
|
|
37
|
+
control: (method) => `/v1/control/${encodeURIComponent(method)}`,
|
|
38
|
+
pushTokens: "/v1/push/tokens",
|
|
39
|
+
pushTest: "/v1/push/test",
|
|
40
|
+
pushPresence: "/v1/push/presence"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class PragmaGatewayError extends Error {
|
|
44
|
+
code;
|
|
45
|
+
httpStatus;
|
|
46
|
+
details;
|
|
47
|
+
cause;
|
|
48
|
+
constructor(message, info) {
|
|
49
|
+
super(message, { cause: info.cause });
|
|
50
|
+
this.name = "PragmaGatewayError";
|
|
51
|
+
this.code = info.code;
|
|
52
|
+
this.httpStatus = info.httpStatus;
|
|
53
|
+
this.details = info.details;
|
|
54
|
+
this.cause = info.cause;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class PragmaTransportError extends Error {
|
|
59
|
+
cause;
|
|
60
|
+
constructor(message, cause) {
|
|
61
|
+
super(message, { cause });
|
|
62
|
+
this.name = "PragmaTransportError";
|
|
63
|
+
this.cause = cause;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function* ndjsonStream(response, signal) {
|
|
67
|
+
if (!response.body) {
|
|
68
|
+
throw new PragmaTransportError("gateway response is not streamable");
|
|
69
|
+
}
|
|
70
|
+
const reader = response.body.getReader();
|
|
71
|
+
const decoder = new TextDecoder;
|
|
72
|
+
let buffered = "";
|
|
73
|
+
const onAbort = () => {
|
|
74
|
+
reader.cancel();
|
|
75
|
+
};
|
|
76
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
if (signal?.aborted) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const { done, value } = await reader.read();
|
|
83
|
+
if (done) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
buffered += decoder.decode(value, { stream: true });
|
|
87
|
+
let newline = buffered.indexOf(`
|
|
88
|
+
`);
|
|
89
|
+
while (newline >= 0) {
|
|
90
|
+
const line = buffered.slice(0, newline).trim();
|
|
91
|
+
buffered = buffered.slice(newline + 1);
|
|
92
|
+
if (line) {
|
|
93
|
+
yield JSON.parse(line);
|
|
94
|
+
}
|
|
95
|
+
newline = buffered.indexOf(`
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
buffered += decoder.decode();
|
|
100
|
+
if (buffered.trim()) {
|
|
101
|
+
yield JSON.parse(buffered);
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
signal?.removeEventListener("abort", onAbort);
|
|
105
|
+
await reader.cancel().catch(() => {
|
|
106
|
+
return;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class Transport {
|
|
112
|
+
baseUrl;
|
|
113
|
+
token;
|
|
114
|
+
fetch;
|
|
115
|
+
headers;
|
|
116
|
+
constructor(config = {}) {
|
|
117
|
+
const { baseUrl, token } = requireGatewayConfig(config);
|
|
118
|
+
this.baseUrl = baseUrl;
|
|
119
|
+
this.token = token;
|
|
120
|
+
this.fetch = requireFetch(config.fetch);
|
|
121
|
+
this.headers = config.headers ?? {};
|
|
122
|
+
}
|
|
123
|
+
async request(path, options = {}) {
|
|
124
|
+
const response = await this.raw(path, options);
|
|
125
|
+
if (response.status === 204 || response.status === 202) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
return parseJson(response);
|
|
129
|
+
}
|
|
130
|
+
async raw(path, options = {}) {
|
|
131
|
+
const headers = {
|
|
132
|
+
...this.headers,
|
|
133
|
+
...options.headers,
|
|
134
|
+
authorization: `Bearer ${this.token}`
|
|
135
|
+
};
|
|
136
|
+
let body;
|
|
137
|
+
if (options.rawBody) {
|
|
138
|
+
body = options.rawBody;
|
|
139
|
+
} else if (options.body !== undefined) {
|
|
140
|
+
headers["content-type"] = headers["content-type"] ?? "application/json";
|
|
141
|
+
body = JSON.stringify(options.body);
|
|
142
|
+
}
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await this.fetch(urlFor(this.baseUrl, path), {
|
|
146
|
+
method: options.method ?? (body ? "POST" : "GET"),
|
|
147
|
+
headers,
|
|
148
|
+
body,
|
|
149
|
+
signal: options.signal
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new PragmaTransportError("failed to reach Pragma gateway", error);
|
|
153
|
+
}
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw await gatewayError(response);
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function requireGatewayConfig(config) {
|
|
161
|
+
const baseUrl = stripTrailingSlash(config.baseUrl ?? readEnv(PRAGMA_ENV_KEYS.gatewayUrl) ?? "");
|
|
162
|
+
const token = config.token ?? readEnv(PRAGMA_ENV_KEYS.gatewayToken) ?? "";
|
|
163
|
+
if (!baseUrl || !token) {
|
|
164
|
+
throw new PragmaTransportError("Pragma gateway baseUrl and token are required");
|
|
165
|
+
}
|
|
166
|
+
return { baseUrl, token };
|
|
167
|
+
}
|
|
168
|
+
function requireFetch(fetch) {
|
|
169
|
+
const resolved = fetch ?? globalThis.fetch?.bind(globalThis);
|
|
170
|
+
if (!resolved) {
|
|
171
|
+
throw new PragmaTransportError("global fetch is not available");
|
|
172
|
+
}
|
|
173
|
+
return resolved;
|
|
174
|
+
}
|
|
175
|
+
function urlFor(baseUrl, path) {
|
|
176
|
+
return `${stripTrailingSlash(baseUrl)}${path.startsWith("/") ? path : `/${path}`}`;
|
|
177
|
+
}
|
|
178
|
+
async function parseJson(response) {
|
|
179
|
+
try {
|
|
180
|
+
return await response.json();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new PragmaTransportError("gateway returned non-JSON response", error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function gatewayError(response) {
|
|
186
|
+
let body = {};
|
|
187
|
+
try {
|
|
188
|
+
body = await response.json();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new PragmaTransportError("gateway returned non-JSON error response", error);
|
|
191
|
+
}
|
|
192
|
+
return new PragmaGatewayError(body.message ?? `gateway returned ${response.status}`, {
|
|
193
|
+
code: body.code ?? "internal",
|
|
194
|
+
httpStatus: response.status,
|
|
195
|
+
details: body.details
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function stripTrailingSlash(value) {
|
|
199
|
+
return value.replace(/\/+$/, "");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
class AgentsClient {
|
|
203
|
+
transport;
|
|
204
|
+
constructor(transport) {
|
|
205
|
+
this.transport = transport;
|
|
206
|
+
}
|
|
207
|
+
report(payload, options = {}) {
|
|
208
|
+
return this.transport.request(routes.agentReports, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: payload,
|
|
211
|
+
signal: options.signal
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
reportMessage(payload, options = {}) {
|
|
215
|
+
return this.transport.request(routes.agentMessages, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
body: payload,
|
|
218
|
+
signal: options.signal
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
reportDecision(payload, options = {}) {
|
|
222
|
+
return this.transport.request(routes.agentDecisions, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: payload,
|
|
225
|
+
signal: options.signal
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
reportAnswer(payload, options = {}) {
|
|
229
|
+
return this.transport.request(routes.agentAnswers, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
body: payload,
|
|
232
|
+
signal: options.signal
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
reportInput(payload, options = {}) {
|
|
236
|
+
return this.transport.request(routes.agentInputs, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: payload,
|
|
239
|
+
signal: options.signal
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
reportInterrupt(payload, options = {}) {
|
|
243
|
+
return this.transport.request(routes.agentInterrupts, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: payload,
|
|
246
|
+
signal: options.signal
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
reportStarted(options) {
|
|
250
|
+
return reportWithClient(this, options, "running", null);
|
|
251
|
+
}
|
|
252
|
+
reportStopped(options) {
|
|
253
|
+
return reportWithClient(this, options, "done", null);
|
|
254
|
+
}
|
|
255
|
+
reportAttention(options) {
|
|
256
|
+
return reportWithClient(this, options, "attention", options.kind ?? "question");
|
|
257
|
+
}
|
|
258
|
+
reportCleared(options) {
|
|
259
|
+
return reportWithClient(this, options, "cleared", null);
|
|
260
|
+
}
|
|
261
|
+
reportSessionName(options) {
|
|
262
|
+
return reportWithClient(this, options, null, null, options.name);
|
|
263
|
+
}
|
|
264
|
+
catalog(options = {}) {
|
|
265
|
+
return this.transport.request(routes.agentCatalog, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
signal: options.signal
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
launch(payload, options = {}) {
|
|
271
|
+
return this.transport.request(routes.control("agentSessionLaunch"), {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: payload,
|
|
274
|
+
signal: options.signal
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
markAgentsSeen(payload, options = {}) {
|
|
278
|
+
return this.transport.request(routes.agentsSeen(payload.tabId), {
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: {},
|
|
281
|
+
signal: options.signal
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async connect(options) {
|
|
285
|
+
const { agent, tabId, worktreeId } = options;
|
|
286
|
+
const controller = new AbortController;
|
|
287
|
+
const onAbort = () => controller.abort();
|
|
288
|
+
options.signal?.addEventListener("abort", onAbort);
|
|
289
|
+
const readStream = this.readStream.bind(this);
|
|
290
|
+
async function* iterate() {
|
|
291
|
+
try {
|
|
292
|
+
for await (const event of readStream({ signal: controller.signal })) {
|
|
293
|
+
if (matchesAgentTab(event, agent, tabId)) {
|
|
294
|
+
yield event;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const connection = {
|
|
302
|
+
[Symbol.asyncIterator]: iterate,
|
|
303
|
+
send: (text, sendOptions = {}) => this.reportInput({
|
|
304
|
+
agent,
|
|
305
|
+
worktreeId,
|
|
306
|
+
tabId,
|
|
307
|
+
text,
|
|
308
|
+
...sendOptions.requestId ? { requestId: sendOptions.requestId } : {}
|
|
309
|
+
}, { signal: controller.signal }),
|
|
310
|
+
answer: (requestId, reply) => this.reportAnswer({
|
|
311
|
+
agent,
|
|
312
|
+
worktreeId,
|
|
313
|
+
tabId,
|
|
314
|
+
requestId,
|
|
315
|
+
dismissed: reply === null,
|
|
316
|
+
...reply !== null ? { answer: reply } : {}
|
|
317
|
+
}, { signal: controller.signal }),
|
|
318
|
+
decide: (requestId, approved) => this.reportDecision({ agent, worktreeId, tabId, requestId, approved }, { signal: controller.signal }),
|
|
319
|
+
interrupt: (requestId) => this.reportInterrupt({
|
|
320
|
+
agent,
|
|
321
|
+
worktreeId,
|
|
322
|
+
tabId,
|
|
323
|
+
...requestId ? { requestId } : {}
|
|
324
|
+
}, { signal: controller.signal }),
|
|
325
|
+
close: () => controller.abort()
|
|
326
|
+
};
|
|
327
|
+
if (options.prompt !== undefined) {
|
|
328
|
+
await connection.send(options.prompt);
|
|
329
|
+
}
|
|
330
|
+
return connection;
|
|
331
|
+
}
|
|
332
|
+
async* readStream(options = {}) {
|
|
333
|
+
const response = await this.transport.raw(routes.agentEvents, { signal: options.signal });
|
|
334
|
+
yield* ndjsonStream(response, options.signal);
|
|
335
|
+
}
|
|
336
|
+
async awaitDecision(params) {
|
|
337
|
+
const controller = new AbortController;
|
|
338
|
+
const onAbort = () => controller.abort();
|
|
339
|
+
params.signal?.addEventListener("abort", onAbort);
|
|
340
|
+
const timer = params.timeoutMs && params.timeoutMs > 0 ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
|
|
341
|
+
try {
|
|
342
|
+
for await (const event of this.readStream({ signal: controller.signal })) {
|
|
343
|
+
if (event.type === "agentDecision" && event.decision.requestId === params.requestId && event.decision.agent === params.agent) {
|
|
344
|
+
return event.decision.approved;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
} finally {
|
|
351
|
+
if (timer) {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
}
|
|
354
|
+
params.signal?.removeEventListener("abort", onAbort);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async awaitAnswer(params) {
|
|
358
|
+
const controller = new AbortController;
|
|
359
|
+
const onAbort = () => controller.abort();
|
|
360
|
+
params.signal?.addEventListener("abort", onAbort);
|
|
361
|
+
const timer = params.timeoutMs && params.timeoutMs > 0 ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
|
|
362
|
+
try {
|
|
363
|
+
for await (const event of this.readStream({ signal: controller.signal })) {
|
|
364
|
+
if (event.type === "agentAnswer" && event.answer.requestId === params.requestId && event.answer.agent === params.agent) {
|
|
365
|
+
return event.answer.dismissed ? null : event.answer.answer ?? "";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
} finally {
|
|
372
|
+
if (timer) {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
}
|
|
375
|
+
params.signal?.removeEventListener("abort", onAbort);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function matchesAgentTab(event, agent, tabId) {
|
|
380
|
+
switch (event.type) {
|
|
381
|
+
case "agent":
|
|
382
|
+
return event.agent === agent && event.tabId === tabId;
|
|
383
|
+
case "agentMessage":
|
|
384
|
+
return event.message.agent === agent && event.message.tabId === tabId;
|
|
385
|
+
case "agentDecision":
|
|
386
|
+
return event.decision.agent === agent && event.decision.tabId === tabId;
|
|
387
|
+
case "agentAnswer":
|
|
388
|
+
return event.answer.agent === agent && event.answer.tabId === tabId;
|
|
389
|
+
case "agentInput":
|
|
390
|
+
return event.input.agent === agent && event.input.tabId === tabId;
|
|
391
|
+
case "agentInterrupt":
|
|
392
|
+
return event.interrupt.agent === agent && event.interrupt.tabId === tabId;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function reportStarted(options) {
|
|
396
|
+
return reportWithStatus(options, "running", null);
|
|
397
|
+
}
|
|
398
|
+
function reportStopped(options) {
|
|
399
|
+
return reportWithStatus(options, "done", null);
|
|
400
|
+
}
|
|
401
|
+
function reportAttention(options) {
|
|
402
|
+
return reportWithStatus(options, "attention", options.kind ?? "question");
|
|
403
|
+
}
|
|
404
|
+
function reportCleared(options) {
|
|
405
|
+
return reportWithStatus(options, "cleared", null);
|
|
406
|
+
}
|
|
407
|
+
function reportSessionName(options) {
|
|
408
|
+
return reportWithStatus(options, null, null, options.name);
|
|
409
|
+
}
|
|
410
|
+
async function reportMessage(options) {
|
|
411
|
+
if (!options.client && !hasPragmaEnvironment(options.env)) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const agents = options.client?.agents ?? new AgentsClient(new Transport({
|
|
415
|
+
baseUrl: readEnv(PRAGMA_ENV_KEYS.gatewayUrl, options.env),
|
|
416
|
+
token: readEnv(PRAGMA_ENV_KEYS.gatewayToken, options.env)
|
|
417
|
+
}));
|
|
418
|
+
await reportMessageWithClient(agents, options);
|
|
419
|
+
}
|
|
420
|
+
async function reportWithStatus(options, status, attentionKind, sessionName) {
|
|
421
|
+
if (!options.client && !hasPragmaEnvironment(options.env)) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const agents = options.client?.agents ?? new AgentsClient(new Transport({
|
|
425
|
+
baseUrl: readEnv(PRAGMA_ENV_KEYS.gatewayUrl, options.env),
|
|
426
|
+
token: readEnv(PRAGMA_ENV_KEYS.gatewayToken, options.env)
|
|
427
|
+
}));
|
|
428
|
+
await reportWithClient(agents, options, status, attentionKind, sessionName);
|
|
429
|
+
}
|
|
430
|
+
async function reportWithClient(agents, options, status, attentionKind, sessionName) {
|
|
431
|
+
const worktreeId = options.worktreeId ?? readEnv(PRAGMA_ENV_KEYS.worktreeId, options.env);
|
|
432
|
+
const tabId = readEnv(PRAGMA_ENV_KEYS.tabId, options.env);
|
|
433
|
+
if (!worktreeId || !tabId) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await agents.report(reportPayload(options, { worktreeId, tabId }, status, attentionKind, sessionName), { signal: options.signal });
|
|
437
|
+
}
|
|
438
|
+
function reportPayload(options, routing, status, attentionKind, sessionName) {
|
|
439
|
+
return {
|
|
440
|
+
agent: options.agent,
|
|
441
|
+
worktreeId: routing.worktreeId,
|
|
442
|
+
tabId: routing.tabId,
|
|
443
|
+
status,
|
|
444
|
+
attentionKind,
|
|
445
|
+
...sessionName ? { sessionName } : {},
|
|
446
|
+
...options.command ? { command: options.command } : {},
|
|
447
|
+
...options.question ? { question: options.question } : {},
|
|
448
|
+
...options.options && options.options.length > 0 ? { options: options.options } : {},
|
|
449
|
+
...options.questions && options.questions.length > 0 ? { questions: options.questions } : {},
|
|
450
|
+
...options.requestId ? { requestId: options.requestId } : {}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function reportMessageWithClient(agents, options) {
|
|
454
|
+
const worktreeId = options.message.worktreeId ?? options.worktreeId ?? readEnv(PRAGMA_ENV_KEYS.worktreeId, options.env);
|
|
455
|
+
const tabId = options.message.tabId ?? readEnv(PRAGMA_ENV_KEYS.tabId, options.env);
|
|
456
|
+
if (!worktreeId || !tabId) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await agents.reportMessage({
|
|
460
|
+
...options.message,
|
|
461
|
+
agent: options.message.agent ?? options.agent,
|
|
462
|
+
worktreeId,
|
|
463
|
+
tabId
|
|
464
|
+
}, { signal: options.signal });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/hooks.ts
|
|
468
|
+
function messageTextKey(messageID, partID) {
|
|
469
|
+
return partID ? `${messageID}:${partID}` : messageID;
|
|
470
|
+
}
|
|
471
|
+
var CHAT_EVENT_TYPES = new Set(["message.updated", "message.part.updated", "message.part.delta"]);
|
|
472
|
+
function sessionEventInfo(event) {
|
|
473
|
+
if (event.type !== "session.created" && event.type !== "session.updated") {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const properties = event.properties;
|
|
477
|
+
const info = isRecord(properties) && isRecord(properties.info) ? properties.info : null;
|
|
478
|
+
if (!info || typeof info.id !== "string") {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
id: info.id,
|
|
483
|
+
parentId: typeof info.parentID === "string" && info.parentID ? info.parentID : null,
|
|
484
|
+
title: sessionInfoTitle(info)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function sessionInfoTitle(info) {
|
|
488
|
+
const title = typeof info.title === "string" ? info.title.trim() : "";
|
|
489
|
+
return title || null;
|
|
490
|
+
}
|
|
491
|
+
var PRAGMA_ENV_KEYS2 = [
|
|
492
|
+
"PRAGMA_GATEWAY_URL",
|
|
493
|
+
"PRAGMA_GATEWAY_TOKEN",
|
|
494
|
+
"PRAGMA_DAEMON_SOCKET",
|
|
495
|
+
"PRAGMA_TAB_ID",
|
|
496
|
+
"PRAGMA_WORKTREE_ID"
|
|
497
|
+
];
|
|
498
|
+
function createPragmaOpencodeHooks(reporter) {
|
|
499
|
+
let busy = false;
|
|
500
|
+
let attention = false;
|
|
501
|
+
let attentionKind = "command";
|
|
502
|
+
let lastReported = null;
|
|
503
|
+
let reportQueue = Promise.resolve();
|
|
504
|
+
const messageRoles = new Map;
|
|
505
|
+
const partTypes = new Map;
|
|
506
|
+
const messageText = new Map;
|
|
507
|
+
const childSessions = new Map;
|
|
508
|
+
const activeChildSessions = new Set;
|
|
509
|
+
let pendingLegacyQuestion = null;
|
|
510
|
+
const EVENT_HANDLERS = {
|
|
511
|
+
"session.status": applySessionStatusEvent,
|
|
512
|
+
"session.idle": applySessionIdleEvent,
|
|
513
|
+
"session.error": applySessionErrorEvent,
|
|
514
|
+
"session.deleted": applySessionDeletedEvent,
|
|
515
|
+
"server.instance.disposed": () => "clear",
|
|
516
|
+
"permission.replied": applyPermissionRepliedEvent,
|
|
517
|
+
"question.replied": applyQuestionRepliedEvent,
|
|
518
|
+
"question.rejected": applyQuestionRepliedEvent,
|
|
519
|
+
"message.updated": applyMessageUpdatedEvent,
|
|
520
|
+
"message.part.updated": applyMessagePartEvent
|
|
521
|
+
};
|
|
522
|
+
const chatContentHandlers = {
|
|
523
|
+
"message.updated": async (event) => {
|
|
524
|
+
rememberMessageRole(event);
|
|
525
|
+
},
|
|
526
|
+
"message.part.delta": reportChatDelta,
|
|
527
|
+
"message.part.updated": reportUpdatedPart
|
|
528
|
+
};
|
|
529
|
+
let lastSessionName = null;
|
|
530
|
+
async function handleChildSessionEvent(event) {
|
|
531
|
+
if (!isChildSessionEvent(event)) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
const parentId = childSessions.get(sessionIdFromEvent(event) ?? "");
|
|
535
|
+
if (applyChildSessionEvent(event)) {
|
|
536
|
+
await reportSubagentActivity(parentId);
|
|
537
|
+
await sync();
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
async function handlePermissionEvent(event) {
|
|
542
|
+
if (event.type !== "permission.asked" && event.type !== "permission.updated") {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
await raiseCommandApproval(event);
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
async function handleQuestionAskedEvent(event) {
|
|
549
|
+
if (event.type !== "question.asked") {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
cancelPendingLegacyQuestion();
|
|
553
|
+
await raiseQuestionAttention(isRecord(event.properties) ? event.properties : {}, questionRequestId(event));
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
async function handleConsumingEvent(event) {
|
|
557
|
+
return await handleChildSessionEvent(event) || await handlePermissionEvent(event) || await handleQuestionAskedEvent(event);
|
|
558
|
+
}
|
|
559
|
+
async function reportChatAndApplyStatus(event) {
|
|
560
|
+
if (CHAT_EVENT_TYPES.has(event.type)) {
|
|
561
|
+
await reportChatContent(event);
|
|
562
|
+
}
|
|
563
|
+
const action = applyEvent(event);
|
|
564
|
+
if (action === "clear") {
|
|
565
|
+
await clear();
|
|
566
|
+
} else if (action === "sync") {
|
|
567
|
+
await sync();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
event: async ({ event }) => {
|
|
572
|
+
const runtimeEvent = event;
|
|
573
|
+
rememberSessionKind(runtimeEvent);
|
|
574
|
+
await maybeReportSessionName(runtimeEvent);
|
|
575
|
+
if (await handleConsumingEvent(runtimeEvent)) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
await reportChatAndApplyStatus(runtimeEvent);
|
|
579
|
+
},
|
|
580
|
+
"chat.message": async (_input, output) => {
|
|
581
|
+
if (isChildSessionId(_input.sessionID)) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
busy = true;
|
|
585
|
+
const text = textFromParts(output.parts) ?? textFromRecord(output.message);
|
|
586
|
+
await queueReport(() => reporter.message({
|
|
587
|
+
id: messageId("chat"),
|
|
588
|
+
role: "user",
|
|
589
|
+
...text ? { text } : {},
|
|
590
|
+
subAgentsActive: 0,
|
|
591
|
+
ts: Date.now()
|
|
592
|
+
}));
|
|
593
|
+
await sync();
|
|
594
|
+
},
|
|
595
|
+
"command.execute.before": async (input) => {
|
|
596
|
+
if (isChildSessionId(input.sessionID)) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
busy = true;
|
|
600
|
+
await queueReport(() => reporter.message(toolMessage("command", summaryFromRecord(input))));
|
|
601
|
+
await sync();
|
|
602
|
+
},
|
|
603
|
+
"tool.execute.before": async (input, output) => {
|
|
604
|
+
if (isChildSessionId(input.sessionID)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const args = isRecord(output?.args) ? output.args : {};
|
|
608
|
+
if (input.tool === "question") {
|
|
609
|
+
scheduleLegacyQuestionAttention(args, typeof input.callID === "string" ? `opencode-question-${input.callID}` : undefined);
|
|
610
|
+
const parsed = parseQuestionArgs(args);
|
|
611
|
+
await queueReport(() => reporter.message(toolMessage("question", parsed?.[0]?.question ?? "Waiting for an answer")));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
busy = true;
|
|
615
|
+
await queueReport(() => reporter.message(toolMessage(input.tool, toolSummary(input.tool, args))));
|
|
616
|
+
await sync();
|
|
617
|
+
},
|
|
618
|
+
"permission.ask": async (input) => {
|
|
619
|
+
if (isRecord(input) && isChildSessionId(input.sessionID)) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
await raiseCommandApproval(input);
|
|
623
|
+
},
|
|
624
|
+
"shell.env": async (_input, output) => {
|
|
625
|
+
for (const key of PRAGMA_ENV_KEYS2) {
|
|
626
|
+
const value = reporter.env[key];
|
|
627
|
+
if (value) {
|
|
628
|
+
output.env[key] = value;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
dispose: async () => {
|
|
633
|
+
await clear();
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
function rememberSessionKind(event) {
|
|
637
|
+
const info = sessionEventInfo(event);
|
|
638
|
+
if (!info?.parentId) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
childSessions.set(info.id, info.parentId);
|
|
642
|
+
if (event.type === "session.created") {
|
|
643
|
+
activeChildSessions.add(info.id);
|
|
644
|
+
busy = true;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function maybeReportSessionName(event) {
|
|
648
|
+
const info = sessionEventInfo(event);
|
|
649
|
+
if (!info || childSessions.has(info.id)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (!info.title || info.title === lastSessionName) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
lastSessionName = info.title;
|
|
656
|
+
await reporter.sessionName(info.title);
|
|
657
|
+
}
|
|
658
|
+
function applyChildSessionEvent(event) {
|
|
659
|
+
const sessionId = sessionIdFromEvent(event);
|
|
660
|
+
if (!sessionId) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
if (event.type === "session.created") {
|
|
664
|
+
activeChildSessions.add(sessionId);
|
|
665
|
+
busy = true;
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
if (event.type === "session.status") {
|
|
669
|
+
applyChildStatusEvent(sessionId, event);
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
if (event.type === "session.idle" || event.type === "session.error" || event.type === "session.deleted") {
|
|
673
|
+
activeChildSessions.delete(sessionId);
|
|
674
|
+
if (event.type === "session.deleted") {
|
|
675
|
+
childSessions.delete(sessionId);
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
function applyChildStatusEvent(sessionId, event) {
|
|
682
|
+
const properties = isRecord(event.properties) ? event.properties : undefined;
|
|
683
|
+
const status = properties?.status;
|
|
684
|
+
if (isRecord(status) && status.type === "idle") {
|
|
685
|
+
activeChildSessions.delete(sessionId);
|
|
686
|
+
} else {
|
|
687
|
+
activeChildSessions.add(sessionId);
|
|
688
|
+
busy = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function isChildSessionEvent(event) {
|
|
692
|
+
return isChildSessionId(sessionIdFromEvent(event));
|
|
693
|
+
}
|
|
694
|
+
function isChildSessionId(sessionId) {
|
|
695
|
+
return typeof sessionId === "string" && childSessions.has(sessionId);
|
|
696
|
+
}
|
|
697
|
+
async function clear() {
|
|
698
|
+
cancelPendingLegacyQuestion();
|
|
699
|
+
busy = false;
|
|
700
|
+
attention = false;
|
|
701
|
+
childSessions.clear();
|
|
702
|
+
activeChildSessions.clear();
|
|
703
|
+
if (lastReported === "cleared") {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
lastReported = "cleared";
|
|
707
|
+
await queueReport(() => reporter.cleared());
|
|
708
|
+
}
|
|
709
|
+
function currentReport() {
|
|
710
|
+
if (attention) {
|
|
711
|
+
return `attention:${attentionKind}`;
|
|
712
|
+
}
|
|
713
|
+
if (busy) {
|
|
714
|
+
return "started";
|
|
715
|
+
}
|
|
716
|
+
return "stopped";
|
|
717
|
+
}
|
|
718
|
+
async function sync() {
|
|
719
|
+
const next = currentReport();
|
|
720
|
+
if (next === lastReported) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (next === "stopped" && lastReported !== "started") {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
lastReported = next;
|
|
727
|
+
if (next === "started") {
|
|
728
|
+
await queueReport(() => reporter.started());
|
|
729
|
+
} else if (next === "stopped") {
|
|
730
|
+
await queueReport(() => reporter.stopped());
|
|
731
|
+
} else {
|
|
732
|
+
await queueReport(() => reporter.attention(attentionKind));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function queueReport(send) {
|
|
736
|
+
const pending = reportQueue.then(send);
|
|
737
|
+
reportQueue = pending.catch(() => {
|
|
738
|
+
return;
|
|
739
|
+
});
|
|
740
|
+
return pending;
|
|
741
|
+
}
|
|
742
|
+
function applyEvent(event) {
|
|
743
|
+
const handler = EVENT_HANDLERS[event.type];
|
|
744
|
+
return handler ? handler(event) : "none";
|
|
745
|
+
}
|
|
746
|
+
function applySessionStatusEvent(event) {
|
|
747
|
+
applySessionStatus(event);
|
|
748
|
+
return "sync";
|
|
749
|
+
}
|
|
750
|
+
function applySessionIdleEvent(event) {
|
|
751
|
+
const sessionId = sessionIdFromEvent(event);
|
|
752
|
+
busy = hasActiveChildren(sessionId);
|
|
753
|
+
return "sync";
|
|
754
|
+
}
|
|
755
|
+
function applySessionErrorEvent(event) {
|
|
756
|
+
if (isAbortError(event)) {
|
|
757
|
+
return "clear";
|
|
758
|
+
}
|
|
759
|
+
busy = false;
|
|
760
|
+
attention = false;
|
|
761
|
+
removeChildren(sessionIdFromEvent(event));
|
|
762
|
+
return "sync";
|
|
763
|
+
}
|
|
764
|
+
function applySessionDeletedEvent(event) {
|
|
765
|
+
busy = false;
|
|
766
|
+
attention = false;
|
|
767
|
+
removeChildren(sessionIdFromEvent(event));
|
|
768
|
+
return "sync";
|
|
769
|
+
}
|
|
770
|
+
function hasActiveChildren(parentId) {
|
|
771
|
+
for (const childId of activeChildSessions) {
|
|
772
|
+
if (!parentId || childSessions.get(childId) === parentId) {
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
function removeChildren(parentId) {
|
|
779
|
+
if (!parentId) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
for (const [childId, childParentId] of childSessions) {
|
|
783
|
+
if (childParentId === parentId) {
|
|
784
|
+
childSessions.delete(childId);
|
|
785
|
+
activeChildSessions.delete(childId);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function raiseCommandApproval(source) {
|
|
790
|
+
attention = true;
|
|
791
|
+
attentionKind = "command";
|
|
792
|
+
lastReported = "attention:command";
|
|
793
|
+
await queueReport(() => reporter.attentionCommand(commandFromPermission(source), permissionRequestId()));
|
|
794
|
+
}
|
|
795
|
+
async function raiseQuestionAttention(args, requestId) {
|
|
796
|
+
const parsed = parseQuestionArgs(args);
|
|
797
|
+
if (!parsed || parsed.length === 0) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
attention = true;
|
|
801
|
+
attentionKind = "question";
|
|
802
|
+
lastReported = "attention:question";
|
|
803
|
+
const requestIdResolved = requestId ?? permissionRequestId();
|
|
804
|
+
await queueReport(() => {
|
|
805
|
+
if (parsed.length === 1 && parsed[0]) {
|
|
806
|
+
return reporter.attentionQuestion(parsed[0].question, parsed[0].options, requestIdResolved);
|
|
807
|
+
}
|
|
808
|
+
return reporter.attentionQuestions(parsed.map((item) => ({ question: item.question, options: item.options })), requestIdResolved);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
function applyPermissionRepliedEvent() {
|
|
812
|
+
attention = false;
|
|
813
|
+
busy = true;
|
|
814
|
+
return "sync";
|
|
815
|
+
}
|
|
816
|
+
function applyQuestionRepliedEvent() {
|
|
817
|
+
cancelPendingLegacyQuestion();
|
|
818
|
+
attention = false;
|
|
819
|
+
busy = true;
|
|
820
|
+
return "sync";
|
|
821
|
+
}
|
|
822
|
+
function applyMessageUpdatedEvent(event) {
|
|
823
|
+
rememberMessageRole(event);
|
|
824
|
+
return "none";
|
|
825
|
+
}
|
|
826
|
+
function applyMessagePartEvent(event) {
|
|
827
|
+
return applyQuestionPart(event) ? "sync" : "none";
|
|
828
|
+
}
|
|
829
|
+
async function reportSubagentActivity(parentId) {
|
|
830
|
+
if (!parentId)
|
|
831
|
+
return;
|
|
832
|
+
let active = 0;
|
|
833
|
+
for (const childId of activeChildSessions) {
|
|
834
|
+
if (childSessions.get(childId) === parentId)
|
|
835
|
+
active += 1;
|
|
836
|
+
}
|
|
837
|
+
await queueReport(() => reporter.message({
|
|
838
|
+
id: `subagents:${parentId}`,
|
|
839
|
+
role: "system",
|
|
840
|
+
subAgentsActive: active,
|
|
841
|
+
ts: Date.now()
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
async function reportChatContent(event) {
|
|
845
|
+
const handler = chatContentHandlers[event.type];
|
|
846
|
+
if (handler)
|
|
847
|
+
await handler(event);
|
|
848
|
+
}
|
|
849
|
+
async function reportUpdatedPart(event) {
|
|
850
|
+
const part = partFromEvent(event);
|
|
851
|
+
if (!part) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
rememberPartType(part);
|
|
855
|
+
const details = assistantTextPart(part);
|
|
856
|
+
if (!details || messageRoles.get(details.messageID) === "user") {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const key = messageTextKey(details.messageID, details.partID);
|
|
860
|
+
messageText.set(key, details.text);
|
|
861
|
+
await reportAssistantText(details.messageID, details.text, details.partType, details.partID);
|
|
862
|
+
}
|
|
863
|
+
function rememberPartType(part) {
|
|
864
|
+
const partID = typeof part.id === "string" ? part.id : undefined;
|
|
865
|
+
const partType = typeof part.type === "string" ? part.type : undefined;
|
|
866
|
+
if (partID && partType)
|
|
867
|
+
partTypes.set(partID, partType);
|
|
868
|
+
}
|
|
869
|
+
async function reportChatDelta(event) {
|
|
870
|
+
const delta = chatDelta(event, messageRoles);
|
|
871
|
+
if (!delta) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const key = messageTextKey(delta.messageID, delta.partID);
|
|
875
|
+
const text = `${messageText.get(key) ?? ""}${delta.text}`;
|
|
876
|
+
messageText.set(key, text);
|
|
877
|
+
await reportAssistantText(delta.messageID, text, delta.partID ? partTypes.get(delta.partID) : undefined, delta.partID);
|
|
878
|
+
}
|
|
879
|
+
async function reportAssistantText(messageID, text, partType, partID) {
|
|
880
|
+
const isReasoning = partType === "reasoning";
|
|
881
|
+
await queueReport(() => reporter.message({
|
|
882
|
+
id: isReasoning ? `reasoning:${messageID}:${partID ?? "part"}` : `assistant:${messageID}`,
|
|
883
|
+
role: isReasoning ? "system" : "assistant",
|
|
884
|
+
text,
|
|
885
|
+
subAgentsActive: 0,
|
|
886
|
+
ts: Date.now()
|
|
887
|
+
}));
|
|
888
|
+
}
|
|
889
|
+
function rememberMessageRole(event) {
|
|
890
|
+
const properties = event.properties;
|
|
891
|
+
const info = properties?.info;
|
|
892
|
+
if (!isRecord(info) || typeof info.id !== "string") {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (info.role === "user" || info.role === "assistant") {
|
|
896
|
+
messageRoles.set(info.id, info.role);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function isAbortError(event) {
|
|
900
|
+
const error = event.properties?.error;
|
|
901
|
+
return isRecord(error) && error.name === "MessageAbortedError";
|
|
902
|
+
}
|
|
903
|
+
function applySessionStatus(event) {
|
|
904
|
+
const status = event.properties?.status;
|
|
905
|
+
busy = !(isRecord(status) && status.type === "idle");
|
|
906
|
+
}
|
|
907
|
+
function applyQuestionPart(event) {
|
|
908
|
+
const part = partFromEvent(event);
|
|
909
|
+
if (!part || part.type !== "tool" || part.tool !== "question") {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
applyQuestionPartState(part);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
function partFromEvent(event) {
|
|
916
|
+
const properties = event.properties;
|
|
917
|
+
const part = isRecord(properties) && "part" in properties ? properties.part : undefined;
|
|
918
|
+
return isRecord(part) ? part : undefined;
|
|
919
|
+
}
|
|
920
|
+
function applyQuestionPartState(part) {
|
|
921
|
+
const state = isRecord(part.state) ? part.state : undefined;
|
|
922
|
+
if (questionPartFinished(state)) {
|
|
923
|
+
cancelPendingLegacyQuestion();
|
|
924
|
+
attention = false;
|
|
925
|
+
busy = true;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const input = questionPartInput(state, part);
|
|
929
|
+
if (input) {
|
|
930
|
+
scheduleLegacyQuestionAttention(input, typeof part.callID === "string" ? `opencode-question-${part.callID}` : undefined);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function scheduleLegacyQuestionAttention(args, requestId) {
|
|
934
|
+
cancelPendingLegacyQuestion();
|
|
935
|
+
pendingLegacyQuestion = setTimeout(() => {
|
|
936
|
+
pendingLegacyQuestion = null;
|
|
937
|
+
raiseQuestionAttention(args, requestId).catch(() => {
|
|
938
|
+
return;
|
|
939
|
+
});
|
|
940
|
+
}, 50);
|
|
941
|
+
}
|
|
942
|
+
function cancelPendingLegacyQuestion() {
|
|
943
|
+
if (pendingLegacyQuestion !== null) {
|
|
944
|
+
clearTimeout(pendingLegacyQuestion);
|
|
945
|
+
pendingLegacyQuestion = null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function isRecord(value) {
|
|
950
|
+
return typeof value === "object" && value !== null;
|
|
951
|
+
}
|
|
952
|
+
function sessionIdFromEvent(event) {
|
|
953
|
+
if (!isRecord(event.properties)) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const properties = event.properties;
|
|
957
|
+
if (typeof properties.sessionID === "string") {
|
|
958
|
+
return properties.sessionID;
|
|
959
|
+
}
|
|
960
|
+
for (const key of ["info", "part", "permission"]) {
|
|
961
|
+
const nested = properties[key];
|
|
962
|
+
if (isRecord(nested) && typeof nested.sessionID === "string") {
|
|
963
|
+
return nested.sessionID;
|
|
964
|
+
}
|
|
965
|
+
if (key === "info" && isRecord(nested) && typeof nested.id === "string") {
|
|
966
|
+
return nested.id;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
function assistantTextPart(part) {
|
|
972
|
+
const partType = typeof part.type === "string" ? part.type : undefined;
|
|
973
|
+
if (partType !== "text" && partType !== "reasoning")
|
|
974
|
+
return;
|
|
975
|
+
const messageID = typeof part.messageID === "string" ? part.messageID : undefined;
|
|
976
|
+
const text = typeof part.text === "string" ? part.text.trim() : "";
|
|
977
|
+
if (!messageID || !text)
|
|
978
|
+
return;
|
|
979
|
+
return {
|
|
980
|
+
messageID,
|
|
981
|
+
partID: typeof part.id === "string" ? part.id : undefined,
|
|
982
|
+
partType,
|
|
983
|
+
text
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function chatDelta(event, messageRoles) {
|
|
987
|
+
const properties = event.properties;
|
|
988
|
+
if (!isRecord(properties) || !isTextDelta(properties)) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const messageID = typeof properties.messageID === "string" ? properties.messageID : undefined;
|
|
992
|
+
if (!messageID || messageRoles.get(messageID) === "user") {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
messageID,
|
|
997
|
+
partID: typeof properties.partID === "string" ? properties.partID : undefined,
|
|
998
|
+
text: properties.delta
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function isTextDelta(properties) {
|
|
1002
|
+
return (properties.field === undefined || properties.field === "text") && typeof properties.delta === "string" && properties.delta.length > 0;
|
|
1003
|
+
}
|
|
1004
|
+
function questionPartFinished(state) {
|
|
1005
|
+
return state?.status === "completed" || state?.status === "error";
|
|
1006
|
+
}
|
|
1007
|
+
function questionPartInput(state, part) {
|
|
1008
|
+
return isRecord(state?.input) ? state.input : isRecord(part.input) ? part.input : undefined;
|
|
1009
|
+
}
|
|
1010
|
+
function questionRequestId(event) {
|
|
1011
|
+
const properties = event.properties;
|
|
1012
|
+
return isRecord(properties) && typeof properties.id === "string" ? properties.id : undefined;
|
|
1013
|
+
}
|
|
1014
|
+
function permissionRequestId() {
|
|
1015
|
+
return `opencode-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`;
|
|
1016
|
+
}
|
|
1017
|
+
function commandFromPermission(source) {
|
|
1018
|
+
const permission = unwrapPermission(source);
|
|
1019
|
+
const metadata = isRecord(permission.metadata) ? permission.metadata : {};
|
|
1020
|
+
const command = firstNestedString(permission, ["command", "cmd", "script", "shell"]);
|
|
1021
|
+
if (command) {
|
|
1022
|
+
return command;
|
|
1023
|
+
}
|
|
1024
|
+
const filePath = firstNestedString(permission, ["filePath", "filepath", "path", "file"]);
|
|
1025
|
+
if (filePath) {
|
|
1026
|
+
const verb = fileToolVerb(firstString(permission.type, permission.pattern, metadata.tool));
|
|
1027
|
+
return verb ? `${verb} ${filePath}` : filePath;
|
|
1028
|
+
}
|
|
1029
|
+
return firstNestedString(permission, ["url", "uri"]) ?? firstString(permission.title, metadata.title, permission.description, metadata.description, permission.pattern, permission.type) ?? "Run a command";
|
|
1030
|
+
}
|
|
1031
|
+
function unwrapPermission(source) {
|
|
1032
|
+
const outer = isRecord(source) ? source : {};
|
|
1033
|
+
const inner = isRecord(outer.properties) ? outer.properties : outer;
|
|
1034
|
+
return isRecord(inner.permission) ? inner.permission : inner;
|
|
1035
|
+
}
|
|
1036
|
+
function fileToolVerb(type) {
|
|
1037
|
+
switch (type?.toLowerCase()) {
|
|
1038
|
+
case "read":
|
|
1039
|
+
return "Read";
|
|
1040
|
+
case "write":
|
|
1041
|
+
return "Write";
|
|
1042
|
+
case "edit":
|
|
1043
|
+
case "patch":
|
|
1044
|
+
return "Edit";
|
|
1045
|
+
default:
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function firstString(...candidates) {
|
|
1050
|
+
for (const candidate of candidates) {
|
|
1051
|
+
const value = stringValue(candidate);
|
|
1052
|
+
if (value) {
|
|
1053
|
+
return value;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
function firstNestedString(source, keys) {
|
|
1059
|
+
return findNestedString(source, new Set(keys.map((key) => key.toLowerCase())), 4, new Set);
|
|
1060
|
+
}
|
|
1061
|
+
function findNestedString(value, keys, depth, seen) {
|
|
1062
|
+
if (!isRecord(value) || depth < 0 || seen.has(value)) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
seen.add(value);
|
|
1066
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
1067
|
+
if (keys.has(key.toLowerCase())) {
|
|
1068
|
+
const match = stringValue(candidate);
|
|
1069
|
+
if (match) {
|
|
1070
|
+
return match;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const candidate of Object.values(value)) {
|
|
1075
|
+
const match = findNestedString(candidate, keys, depth - 1, seen);
|
|
1076
|
+
if (match) {
|
|
1077
|
+
return match;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
function stringValue(value) {
|
|
1083
|
+
if (typeof value === "string" && value.trim()) {
|
|
1084
|
+
return value.trim();
|
|
1085
|
+
}
|
|
1086
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
1087
|
+
const joined = value.join(" ").trim();
|
|
1088
|
+
return joined || undefined;
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
function toolMessage(name, summary) {
|
|
1093
|
+
const id = messageId(`tool:${name}`);
|
|
1094
|
+
return {
|
|
1095
|
+
id,
|
|
1096
|
+
role: "tool",
|
|
1097
|
+
toolCalls: [{ id, name, status: "running", ...summary ? { summary } : {} }],
|
|
1098
|
+
subAgentsActive: /(?:task|agent)/i.test(name) ? 1 : 0,
|
|
1099
|
+
ts: Date.now()
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function messageId(prefix) {
|
|
1103
|
+
return `${prefix}:${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}:${Math.random()}`}`;
|
|
1104
|
+
}
|
|
1105
|
+
function summaryFromRecord(record) {
|
|
1106
|
+
return textFromRecord(record) ?? compactJson(record);
|
|
1107
|
+
}
|
|
1108
|
+
function toolSummary(tool, args) {
|
|
1109
|
+
return TOOL_SUMMARIES[tool.toLowerCase()]?.(args) ?? summaryFromRecord(args);
|
|
1110
|
+
}
|
|
1111
|
+
var TOOL_SUMMARIES = {
|
|
1112
|
+
bash: commandSummary,
|
|
1113
|
+
shell: commandSummary,
|
|
1114
|
+
read: pathSummary,
|
|
1115
|
+
write: pathSummary,
|
|
1116
|
+
edit: pathSummary,
|
|
1117
|
+
apply_patch: pathSummary,
|
|
1118
|
+
patch: pathSummary,
|
|
1119
|
+
grep: grepSummary,
|
|
1120
|
+
glob: (args) => firstString(args.pattern, args.glob, args.path),
|
|
1121
|
+
webfetch: (args) => firstString(args.url, args.uri),
|
|
1122
|
+
fetch: (args) => firstString(args.url, args.uri),
|
|
1123
|
+
websearch: (args) => firstString(args.query, args.q, args.search),
|
|
1124
|
+
search: (args) => firstString(args.query, args.q, args.search),
|
|
1125
|
+
todowrite: () => "Updating todos",
|
|
1126
|
+
todo: () => "Updating todos",
|
|
1127
|
+
task: taskSummary,
|
|
1128
|
+
agent: taskSummary
|
|
1129
|
+
};
|
|
1130
|
+
function commandSummary(args) {
|
|
1131
|
+
return firstString(args.command, args.cmd, args.script);
|
|
1132
|
+
}
|
|
1133
|
+
function pathSummary(args) {
|
|
1134
|
+
return firstString(args.filePath, args.filepath, args.path, args.file);
|
|
1135
|
+
}
|
|
1136
|
+
function grepSummary(args) {
|
|
1137
|
+
return firstString(args.pattern, args.query, args.regex) ?? firstString(args.path, args.include);
|
|
1138
|
+
}
|
|
1139
|
+
function taskSummary(args) {
|
|
1140
|
+
return firstString(args.description, args.prompt, args.text) ?? "Spawning sub-agent";
|
|
1141
|
+
}
|
|
1142
|
+
function parseQuestionArgs(args) {
|
|
1143
|
+
const questions = Array.isArray(args.questions) ? args.questions : undefined;
|
|
1144
|
+
if (!questions || questions.length === 0) {
|
|
1145
|
+
const single = questionPromptFrom(args);
|
|
1146
|
+
return single ? [single] : undefined;
|
|
1147
|
+
}
|
|
1148
|
+
const parsed = questions.flatMap((item) => {
|
|
1149
|
+
const question = isRecord(item) ? questionPromptFrom(item) : undefined;
|
|
1150
|
+
return question ? [question] : [];
|
|
1151
|
+
});
|
|
1152
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
1153
|
+
}
|
|
1154
|
+
function questionPromptFrom(record) {
|
|
1155
|
+
const question = firstString(record.question, record.header, record.text, record.message, record.prompt) ?? undefined;
|
|
1156
|
+
if (!question) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const options = questionOptions(record.options);
|
|
1160
|
+
return { question, options };
|
|
1161
|
+
}
|
|
1162
|
+
function questionOptions(value) {
|
|
1163
|
+
if (!Array.isArray(value)) {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
const options = [];
|
|
1167
|
+
for (const item of value) {
|
|
1168
|
+
if (typeof item === "string" && item.trim()) {
|
|
1169
|
+
options.push({ label: item.trim() });
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (!isRecord(item)) {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
const label = firstString(item.label, item.text, item.value, item.name, item.description);
|
|
1176
|
+
if (label) {
|
|
1177
|
+
const description = firstString(item.description);
|
|
1178
|
+
options.push({ label, ...description && description !== label ? { description } : {} });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return options;
|
|
1182
|
+
}
|
|
1183
|
+
function textFromRecord(record) {
|
|
1184
|
+
for (const key of ["text", "message", "command", "description", "summary"]) {
|
|
1185
|
+
const value = record[key];
|
|
1186
|
+
if (typeof value === "string" && value.trim()) {
|
|
1187
|
+
return value.trim();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
function textFromParts(parts) {
|
|
1193
|
+
if (!Array.isArray(parts)) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const chunks = [];
|
|
1197
|
+
for (const part of parts) {
|
|
1198
|
+
if (!isRecord(part) || part.type !== "text") {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof part.text === "string" && part.text.trim()) {
|
|
1202
|
+
chunks.push(part.text.trim());
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return chunks.length > 0 ? chunks.join(`
|
|
1206
|
+
`) : undefined;
|
|
1207
|
+
}
|
|
1208
|
+
function compactJson(value) {
|
|
1209
|
+
try {
|
|
1210
|
+
const json = JSON.stringify(value);
|
|
1211
|
+
return json.length > 240 ? `${json.slice(0, 237)}...` : json;
|
|
1212
|
+
} catch {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/index.ts
|
|
1218
|
+
var DEFAULT_AGENT_ID = "opencode";
|
|
1219
|
+
var PragmaOpencodePlugin = async (_input, options) => {
|
|
1220
|
+
const reporter = createSdkReporter(parseOptions(options));
|
|
1221
|
+
reporter.cleared();
|
|
1222
|
+
return createPragmaOpencodeHooks(reporter);
|
|
1223
|
+
};
|
|
1224
|
+
var src_default = PragmaOpencodePlugin;
|
|
1225
|
+
function parseOptions(options) {
|
|
1226
|
+
return options ?? {};
|
|
1227
|
+
}
|
|
1228
|
+
function createSdkReporter(options) {
|
|
1229
|
+
const agent = nonEmpty(options.agent) ?? DEFAULT_AGENT_ID;
|
|
1230
|
+
const env = { ...process.env, ...options.env };
|
|
1231
|
+
const debug = options.quiet === true ? false : env.PRAGMA_OPENCODE_PLUGIN_DEBUG === "1";
|
|
1232
|
+
return {
|
|
1233
|
+
env,
|
|
1234
|
+
started: () => report(() => reportStarted({ agent, env })),
|
|
1235
|
+
stopped: () => report(() => reportStopped({ agent, env })),
|
|
1236
|
+
attention: (kind) => report(() => reportAttention({ agent, env, kind })),
|
|
1237
|
+
attentionCommand: (command, requestId) => report(() => reportAttention({ agent, env, kind: "command", command, requestId })),
|
|
1238
|
+
attentionQuestion: (question, questionOptions2, requestId) => report(() => reportAttention({
|
|
1239
|
+
agent,
|
|
1240
|
+
env,
|
|
1241
|
+
kind: "question",
|
|
1242
|
+
question,
|
|
1243
|
+
...questionOptions2.length > 0 ? { options: questionOptions2 } : {},
|
|
1244
|
+
requestId
|
|
1245
|
+
})),
|
|
1246
|
+
attentionQuestions: (questions, requestId) => report(() => reportAttention({
|
|
1247
|
+
agent,
|
|
1248
|
+
env,
|
|
1249
|
+
kind: "question",
|
|
1250
|
+
...questions.length > 0 ? { questions } : {},
|
|
1251
|
+
requestId
|
|
1252
|
+
})),
|
|
1253
|
+
message: (message) => report(() => reportMessage({ agent, env, message })),
|
|
1254
|
+
cleared: () => report(() => reportCleared({ agent, env })),
|
|
1255
|
+
sessionName: (name) => report(() => reportSessionName({ agent, env, name }))
|
|
1256
|
+
};
|
|
1257
|
+
async function report(run) {
|
|
1258
|
+
if (!hasPragmaEnvironment(env)) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
await run();
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (debug) {
|
|
1265
|
+
console.warn("@pragma-sh/opencode-plugin failed to report status", error);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function nonEmpty(value) {
|
|
1271
|
+
const trimmed = value?.trim();
|
|
1272
|
+
return trimmed ? trimmed : undefined;
|
|
1273
|
+
}
|
|
1274
|
+
export {
|
|
1275
|
+
src_default as default,
|
|
1276
|
+
PragmaOpencodePlugin
|
|
1277
|
+
};
|