opencode-courier 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -193
- package/dist/index.d.ts +4 -8
- package/dist/index.js +66 -85
- package/dist/kernel/protocol.d.ts +20 -0
- package/dist/kernel/protocol.js +36 -0
- package/dist/kernel/tool.d.ts +5 -0
- package/dist/kernel/tool.js +5 -0
- package/dist/person/events.d.ts +23 -0
- package/dist/person/events.js +113 -0
- package/dist/person/form.d.ts +26 -0
- package/dist/person/form.js +64 -0
- package/dist/person/index.d.ts +17 -0
- package/dist/person/index.js +5 -0
- package/dist/{relay.d.ts → person/permission.d.ts} +29 -25
- package/dist/{relay.js → person/permission.js} +11 -9
- package/dist/person/ports.d.ts +58 -0
- package/dist/person/ports.js +2 -0
- package/dist/{question → person/question}/answer.d.ts +33 -4
- package/dist/{question → person/question}/answer.js +49 -17
- package/dist/person/question/index.d.ts +7 -0
- package/dist/{question → person/question}/index.js +3 -3
- package/dist/person/question/lifecycle.d.ts +43 -0
- package/dist/{question → person/question}/lifecycle.js +10 -19
- package/dist/{question → person/question}/pure.d.ts +4 -6
- package/dist/{question → person/question}/pure.js +3 -4
- package/dist/{question → person/question}/relay.d.ts +1 -1
- package/dist/{question → person/question}/relay.js +27 -35
- package/dist/{question → person/question}/runtime.d.ts +2 -3
- package/dist/{question → person/question}/runtime.js +2 -3
- package/dist/{question → person/question}/shared.d.ts +13 -3
- package/dist/person/question/shared.js +52 -0
- package/dist/person/question/types.d.ts +121 -0
- package/dist/person/question/types.js +1 -0
- package/dist/person/rpc-handlers.d.ts +160 -0
- package/dist/person/rpc-handlers.js +62 -0
- package/dist/person/rpc.d.ts +227 -0
- package/dist/person/rpc.js +70 -0
- package/dist/person/text.d.ts +68 -0
- package/dist/{notices.js → person/text.js} +53 -190
- package/dist/person/tools.d.ts +20 -0
- package/dist/person/tools.js +55 -0
- package/dist/person/tui.d.ts +46 -0
- package/dist/platform/bounded.d.ts +12 -0
- package/dist/{bounded.js → platform/bounded.js} +6 -4
- package/dist/platform/events.d.ts +34 -0
- package/dist/platform/events.js +121 -0
- package/dist/platform/hub.d.ts +195 -0
- package/dist/{hub.js → platform/hub.js} +78 -119
- package/dist/platform/json.d.ts +8 -0
- package/dist/platform/json.js +15 -0
- package/dist/platform/version.d.ts +16 -0
- package/dist/{version.js → platform/version.js} +5 -11
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +3 -0
- package/dist/scheduling/index.d.ts +6 -0
- package/dist/scheduling/index.js +5 -0
- package/dist/{later.d.ts → scheduling/later.d.ts} +13 -9
- package/dist/{later.js → scheduling/later.js} +18 -9
- package/dist/scheduling/text.d.ts +6 -0
- package/dist/scheduling/text.js +10 -0
- package/dist/scheduling/tools.d.ts +14 -0
- package/dist/scheduling/tools.js +48 -0
- package/dist/skill.d.ts +17 -0
- package/dist/skill.js +62 -0
- package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +5 -26
- package/dist/{cleanup.js → tree/cleanup.js} +20 -15
- package/dist/tree/context.d.ts +15 -0
- package/dist/tree/context.js +34 -0
- package/dist/tree/domain.d.ts +101 -0
- package/dist/tree/domain.js +68 -0
- package/dist/tree/events.d.ts +71 -0
- package/dist/tree/events.js +204 -0
- package/dist/tree/forget.d.ts +24 -0
- package/dist/tree/forget.js +59 -0
- package/dist/tree/group.d.ts +82 -0
- package/dist/tree/group.js +203 -0
- package/dist/tree/index.d.ts +10 -0
- package/dist/tree/index.js +11 -0
- package/dist/tree/limits.d.ts +10 -0
- package/dist/tree/limits.js +60 -0
- package/dist/tree/panel.d.ts +13 -0
- package/dist/tree/panel.js +75 -0
- package/dist/tree/ports.d.ts +276 -0
- package/dist/tree/ports.js +1 -0
- package/dist/tree/report.d.ts +23 -0
- package/dist/tree/report.js +33 -0
- package/dist/tree/roster.d.ts +27 -0
- package/dist/tree/roster.js +62 -0
- package/dist/tree/rpc-handlers.d.ts +201 -0
- package/dist/tree/rpc-handlers.js +20 -0
- package/dist/tree/rpc.d.ts +344 -0
- package/dist/tree/rpc.js +83 -0
- package/dist/tree/send.d.ts +14 -0
- package/dist/tree/send.js +99 -0
- package/dist/tree/spawn.d.ts +21 -0
- package/dist/tree/spawn.js +173 -0
- package/dist/tree/status.d.ts +128 -0
- package/dist/tree/status.js +62 -0
- package/dist/tree/stop.d.ts +33 -0
- package/dist/tree/stop.js +116 -0
- package/dist/tree/store.d.ts +24 -0
- package/dist/tree/store.js +264 -0
- package/dist/tree/subtree.d.ts +101 -0
- package/dist/tree/subtree.js +60 -0
- package/dist/tree/summary.d.ts +7 -0
- package/dist/tree/summary.js +35 -0
- package/dist/tree/text.d.ts +77 -0
- package/dist/tree/text.js +287 -0
- package/dist/tree/tools.d.ts +54 -0
- package/dist/tree/tools.js +167 -0
- package/dist/tree/tui.d.ts +53 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1219 -0
- package/dist/webhook/index.d.ts +5 -0
- package/dist/webhook/index.js +3 -0
- package/dist/{webhook.d.ts → webhook/receiver.d.ts} +25 -36
- package/dist/{webhook.js → webhook/receiver.js} +66 -67
- package/dist/webhook/subscriptions.d.ts +29 -0
- package/dist/webhook/subscriptions.js +52 -0
- package/dist/webhook/text.d.ts +23 -0
- package/dist/webhook/text.js +82 -0
- package/dist/webhook/tools.d.ts +19 -0
- package/dist/webhook/tools.js +46 -0
- package/package.json +18 -7
- package/dist/bounded.d.ts +0 -10
- package/dist/courier.d.ts +0 -95
- package/dist/courier.js +0 -131
- package/dist/hub.d.ts +0 -325
- package/dist/json.d.ts +0 -4
- package/dist/json.js +0 -4
- package/dist/notices.d.ts +0 -246
- package/dist/question/index.d.ts +0 -6
- package/dist/question/lifecycle.d.ts +0 -52
- package/dist/question/shared.js +0 -24
- package/dist/roster.d.ts +0 -81
- package/dist/roster.js +0 -175
- package/dist/tools.d.ts +0 -74
- package/dist/tools.js +0 -228
- package/dist/version.d.ts +0 -22
- package/dist/watch.d.ts +0 -61
- package/dist/watch.js +0 -289
- /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
- /package/dist/{storage.js → platform/storage.js} +0 -0
package/dist/tools.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
import { cleanup } from "./cleanup.js";
|
|
3
|
-
import { describeFailure, listChildren, send, spawn, status } from "./courier.js";
|
|
4
|
-
import { cancel, schedule } from "./later.js";
|
|
5
|
-
import { answerText, cancelText, childrenText, cleanupText, laterText, REPLIES, sendText, spawnText, statusText, subscribeText, unsubscribeText, } from "./notices.js";
|
|
6
|
-
import { answerQuestion, isQuestion } from "./question/index.js";
|
|
7
|
-
import { answer } from "./relay.js";
|
|
8
|
-
import { subscribe, unsubscribe } from "./webhook.js";
|
|
9
|
-
/**
|
|
10
|
-
* The courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
11
|
-
* model-facing like the texts in `notices.ts`, and snapshotted with them in `test/notices.test.ts`.
|
|
12
|
-
*/
|
|
13
|
-
export const SpawnInput = Schema.Struct({
|
|
14
|
-
task: Schema.String.annotate({ description: "What the new session should do. It is told who started it and how to report back." }),
|
|
15
|
-
title: Schema.optional(Schema.String.annotate({ description: "Session title; defaults to the task's first line." })),
|
|
16
|
-
agent: Schema.optional(Schema.String.annotate({ description: "Agent to run the session with; defaults to the default agent." })),
|
|
17
|
-
isolate: Schema.optional(Schema.Boolean.annotate({ description: "Run the session in its own git worktree so parallel sessions don't share files." })),
|
|
18
|
-
});
|
|
19
|
-
export const SendInput = Schema.Struct({
|
|
20
|
-
sessionID: Schema.String.annotate({ description: "The session to deliver to." }),
|
|
21
|
-
message: Schema.String.annotate({ description: "The message text." }),
|
|
22
|
-
queue: Schema.optional(Schema.Boolean.annotate({ description: "Wait until the target's current turn ends instead of steering it now." })),
|
|
23
|
-
});
|
|
24
|
-
export const StatusInput = Schema.Struct({
|
|
25
|
-
sessionID: Schema.String.annotate({ description: "The session to look at." }),
|
|
26
|
-
});
|
|
27
|
-
export const ChildrenInput = Schema.Struct({
|
|
28
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session whose children to list; defaults to this one." })),
|
|
29
|
-
});
|
|
30
|
-
export const CleanupInput = Schema.Struct({
|
|
31
|
-
sessionID: Schema.String.annotate({ description: "The isolated child whose worktree to remove." }),
|
|
32
|
-
force: Schema.optional(Schema.Boolean.annotate({
|
|
33
|
-
description: "Remove it even with uncommitted changes or commits on no branch; that work is lost.",
|
|
34
|
-
})),
|
|
35
|
-
});
|
|
36
|
-
export const LaterInput = Schema.Struct({
|
|
37
|
-
message: Schema.String.annotate({ description: "The message to deliver." }),
|
|
38
|
-
delayMinutes: Schema.optional(
|
|
39
|
-
// Some models send the number as a string ("3", "3.0") whatever the schema says, and resend it
|
|
40
|
-
// unchanged after the error, so a string is accepted and schedule reads it as a number.
|
|
41
|
-
Schema.Union([Schema.Number, Schema.String]).annotate({
|
|
42
|
-
description: "Deliver this many minutes from now. Give this or at.",
|
|
43
|
-
})),
|
|
44
|
-
at: Schema.optional(Schema.String.annotate({ description: "Deliver at this ISO 8601 time. Give this or delayMinutes." })),
|
|
45
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to deliver to; defaults to this one." })),
|
|
46
|
-
});
|
|
47
|
-
export const AnswerInput = Schema.Struct({
|
|
48
|
-
sessionID: Schema.String.annotate({ description: "The session you started that is waiting, as its notice names it." }),
|
|
49
|
-
requestID: Schema.String.annotate({ description: "The request id from the notice." }),
|
|
50
|
-
reply: Schema.optional(Schema.Literals(REPLIES).annotate({
|
|
51
|
-
description: "For a permission request: the choice the person made, once, always (only when the notice offers it) or reject.",
|
|
52
|
-
})),
|
|
53
|
-
message: Schema.optional(Schema.String.annotate({ description: "For a permission request, with reject: the person's reason, passed on with the refusal." })),
|
|
54
|
-
answers: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])).annotate({
|
|
55
|
-
description: "For a question: one entry per question, in order, each the label the person chose or the text they gave, " +
|
|
56
|
-
"or a list of labels where the question allows several.",
|
|
57
|
-
})),
|
|
58
|
-
});
|
|
59
|
-
export const CancelInput = Schema.Struct({
|
|
60
|
-
id: Schema.String.annotate({ description: "The id courier_later returned." }),
|
|
61
|
-
});
|
|
62
|
-
export const SubscribeInput = Schema.Struct({
|
|
63
|
-
topic: Schema.String.annotate({
|
|
64
|
-
description: "owner/repo for every event of a GitHub repository, owner/repo#<number> for one pull request or issue " +
|
|
65
|
-
"(reviews, comments, completed CI runs), or a plain name for deliveries posted to /hook/<name>.",
|
|
66
|
-
}),
|
|
67
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to subscribe; defaults to this one." })),
|
|
68
|
-
});
|
|
69
|
-
export const UnsubscribeInput = Schema.Struct({
|
|
70
|
-
topic: Schema.optional(Schema.String.annotate({ description: "The topic to drop; all of this session's when omitted." })),
|
|
71
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to unsubscribe; defaults to this one." })),
|
|
72
|
-
});
|
|
73
|
-
const rethrow = (tool) => (error) => {
|
|
74
|
-
throw describeFailure(tool, error);
|
|
75
|
-
};
|
|
76
|
-
// One tool answers both: a question's id is the plugin's own, a permission request's OpenCode's.
|
|
77
|
-
async function answerRequest(ports, callerID, input) {
|
|
78
|
-
const { sessionID, requestID } = input;
|
|
79
|
-
if (isQuestion(requestID)) {
|
|
80
|
-
if (input.reply !== undefined || input.message !== undefined)
|
|
81
|
-
throw new Error(`${requestID} is a question: pass the person's answers in answers, not reply or message.`);
|
|
82
|
-
if (input.answers === undefined)
|
|
83
|
-
throw new Error(`${requestID} is a question: answers is required, one entry per question.`);
|
|
84
|
-
return answerQuestion(ports.questions, callerID, { sessionID, requestID, answers: input.answers });
|
|
85
|
-
}
|
|
86
|
-
if (input.answers !== undefined)
|
|
87
|
-
throw new Error(`${requestID} is a permission request: pass the person's choice in reply (once, always or reject), not answers.`);
|
|
88
|
-
if (input.reply === undefined)
|
|
89
|
-
throw new Error(`${requestID} is a permission request: reply is required, once, always or reject.`);
|
|
90
|
-
return answer(ports.answer, ports.waiting, callerID, {
|
|
91
|
-
sessionID,
|
|
92
|
-
requestID,
|
|
93
|
-
reply: input.reply,
|
|
94
|
-
...(input.message !== undefined ? { message: input.message } : {}),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
/** Adds the courier tools, each a direct tool rather than a code-mode one. */
|
|
98
|
-
export function addTools(tools, ports) {
|
|
99
|
-
tools.add({
|
|
100
|
-
name: "courier_spawn",
|
|
101
|
-
options: { codemode: false },
|
|
102
|
-
description: "Start a new OpenCode session on a task and return immediately. It runs on your model. The session reports " +
|
|
103
|
-
"back with courier_send, which wakes this session, and you are told if its turn fails instead, it waits for a " +
|
|
104
|
-
"permission or it asks a question. DO NOT poll it or call courier_status in a loop: once you have started the sessions " +
|
|
105
|
-
"you need, end your turn by replying without calling more tools; each report starts a new turn in which you " +
|
|
106
|
-
"carry on. For long tasks, also courier_later a check-in for yourself in case it never reports, and " +
|
|
107
|
-
"courier_cancel it when it does.",
|
|
108
|
-
input: SpawnInput,
|
|
109
|
-
execute: async (input, context) => {
|
|
110
|
-
const child = await spawn(ports.courier, context.sessionID, input).catch(rethrow("courier_spawn"));
|
|
111
|
-
return { content: spawnText(child), metadata: child };
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
tools.add({
|
|
115
|
-
name: "courier_send",
|
|
116
|
-
options: { codemode: false },
|
|
117
|
-
description: "Deliver a message to another OpenCode session. If that session is idle, OpenCode starts a new turn for it. " +
|
|
118
|
-
"Use it to report back to the session that started you, or to steer a session you started.",
|
|
119
|
-
input: SendInput,
|
|
120
|
-
execute: async (input, context) => {
|
|
121
|
-
const delivered = await send(ports.courier, context.sessionID, input).catch(rethrow("courier_send"));
|
|
122
|
-
return { content: sendText(input.sessionID), metadata: delivered };
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
tools.add({
|
|
126
|
-
name: "courier_status",
|
|
127
|
-
options: { codemode: false },
|
|
128
|
-
description: "Look once at a session's state and its last reply, e.g. on a scheduled check-in. Not for waiting: " +
|
|
129
|
-
"sessions you started report back on their own.",
|
|
130
|
-
input: StatusInput,
|
|
131
|
-
execute: async (input) => {
|
|
132
|
-
const result = await status(ports.courier, input).catch(rethrow("courier_status"));
|
|
133
|
-
return { content: statusText(result), metadata: result };
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
tools.add({
|
|
137
|
-
name: "courier_children",
|
|
138
|
-
options: { codemode: false },
|
|
139
|
-
description: "List the sessions this one started with courier_spawn, with each one's state and last reply, e.g. after a " +
|
|
140
|
-
"compaction or restart. Like courier_status, for a one-off look, not for waiting.",
|
|
141
|
-
input: ChildrenInput,
|
|
142
|
-
execute: async (input, context) => {
|
|
143
|
-
const listed = await listChildren(ports.courier, input.sessionID || context.sessionID).catch(rethrow("courier_children"));
|
|
144
|
-
return { content: childrenText(listed), metadata: { children: listed } };
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
tools.add({
|
|
148
|
-
name: "courier_cleanup",
|
|
149
|
-
options: { codemode: false },
|
|
150
|
-
description: "Remove the git worktree of a session you started with isolate: true, once you have what you need from it, " +
|
|
151
|
-
"and drop it from courier_children. A worktree with uncommitted changes or commits on no branch is kept and " +
|
|
152
|
-
"the result lists them; commit or branch what you want, or pass force: true to discard it.",
|
|
153
|
-
input: CleanupInput,
|
|
154
|
-
execute: async (input, context) => {
|
|
155
|
-
const result = await cleanup(ports.cleanup, context.sessionID, input).catch(rethrow("courier_cleanup"));
|
|
156
|
-
return { content: cleanupText(result), metadata: result };
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
tools.add({
|
|
160
|
-
name: "courier_answer",
|
|
161
|
-
options: { codemode: false },
|
|
162
|
-
description: "Pass on the answer to a permission request or a question that a session you started with courier_spawn (or " +
|
|
163
|
-
"one started from it) waits on, after a notice from it named the request. The answer is the person's, not yours: " +
|
|
164
|
-
"first ask the person you are working with, offering the choices the notice lists, then call this with what " +
|
|
165
|
-
"they chose: reply for a permission request, answers for a question. Never choose for them. The session " +
|
|
166
|
-
"carries on once it has the answer.",
|
|
167
|
-
input: AnswerInput,
|
|
168
|
-
execute: async (input, context) => {
|
|
169
|
-
const result = await answerRequest(ports, context.sessionID, input).catch(rethrow("courier_answer"));
|
|
170
|
-
return { content: answerText(result), metadata: result };
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
tools.add({
|
|
174
|
-
name: "courier_later",
|
|
175
|
-
options: { codemode: false },
|
|
176
|
-
description: "Schedule a message for a session (this one by default), delivered when due and waking it if idle. " +
|
|
177
|
-
"Use it as a safety net when you start sessions: schedule one check-in, end your turn by replying without " +
|
|
178
|
-
"calling more tools, and cancel it with courier_cancel if the child reports first. Survives server restarts; " +
|
|
179
|
-
"may arrive up to ~15 seconds late.",
|
|
180
|
-
input: LaterInput,
|
|
181
|
-
execute: async (input, context) => {
|
|
182
|
-
const entry = await schedule(ports.later, context.sessionID, input).catch(rethrow("courier_later"));
|
|
183
|
-
const fireAt = new Date(entry.fireAt).toISOString();
|
|
184
|
-
return {
|
|
185
|
-
content: laterText(entry, fireAt, entry.sessionID === context.sessionID),
|
|
186
|
-
metadata: { id: entry.id, fireAt, sessionID: entry.sessionID },
|
|
187
|
-
};
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
tools.add({
|
|
191
|
-
name: "courier_cancel",
|
|
192
|
-
options: { codemode: false },
|
|
193
|
-
description: "Cancel a message scheduled with courier_later, e.g. because the child it was waiting for reported.",
|
|
194
|
-
input: CancelInput,
|
|
195
|
-
execute: async (input) => {
|
|
196
|
-
const cancelled = await cancel(ports.later, input.id).catch(rethrow("courier_cancel"));
|
|
197
|
-
return { content: cancelText(input.id, cancelled), metadata: { id: input.id, cancelled } };
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
tools.add({
|
|
201
|
-
name: "courier_subscribe",
|
|
202
|
-
options: { codemode: false },
|
|
203
|
-
description: "Wake a session (this one by default) when a webhook arrives for a topic: a GitHub repository, one of its " +
|
|
204
|
-
"pull requests or issues (reviews, comments, completed CI runs), or a named generic hook. Each matching " +
|
|
205
|
-
"delivery arrives as a message, queued behind any running turn. End your turn and wait; do not poll.",
|
|
206
|
-
input: SubscribeInput,
|
|
207
|
-
execute: async (input, context) => {
|
|
208
|
-
const sessionID = input.sessionID ?? context.sessionID;
|
|
209
|
-
const subscription = await subscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_subscribe"));
|
|
210
|
-
const receiving = await ports.receiving();
|
|
211
|
-
return {
|
|
212
|
-
content: subscribeText(sessionID, subscription.topic, receiving),
|
|
213
|
-
metadata: { sessionID, topic: subscription.topic, receiver: receiving },
|
|
214
|
-
};
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
tools.add({
|
|
218
|
-
name: "courier_unsubscribe",
|
|
219
|
-
options: { codemode: false },
|
|
220
|
-
description: "Stop webhook deliveries for a topic, or all of a session's topics, e.g. once its pull request is merged.",
|
|
221
|
-
input: UnsubscribeInput,
|
|
222
|
-
execute: async (input, context) => {
|
|
223
|
-
const sessionID = input.sessionID ?? context.sessionID;
|
|
224
|
-
const dropped = await unsubscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_unsubscribe"));
|
|
225
|
-
return { content: unsubscribeText(sessionID, dropped), metadata: { sessionID, dropped } };
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
}
|
package/dist/version.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { App } from "@opencode/plugin/app";
|
|
2
|
-
/** The versions a build of the plugin carries: its own, and the OpenCode it was built and tested against. */
|
|
3
|
-
export interface BuiltVersions {
|
|
4
|
-
readonly plugin: string;
|
|
5
|
-
readonly opencode: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Reads the versions from the package's own package.json, which sits next to `dist/` in the
|
|
9
|
-
* published package as it does next to `src/` in the repository: `version`, and the exact
|
|
10
|
-
* `@opencode/plugin` version under devDependencies, the one installed for the build and both test
|
|
11
|
-
* suites; the peer dependency names the same exact version, as the copy of the plugin API that
|
|
12
|
-
* `opencode plugin add` installs next to the plugin. Throws when the file cannot be read or does
|
|
13
|
-
* not hold both.
|
|
14
|
-
*/
|
|
15
|
-
export declare function builtVersions(): BuiltVersions;
|
|
16
|
-
/**
|
|
17
|
-
* The line for the server log when the running OpenCode is not the one the plugin was built
|
|
18
|
-
* against; undefined when it is. Logged once per plugin load, never in a tool result. An OpenCode
|
|
19
|
-
* whose plugin context does not report a version, as one from before the pinned API or after a
|
|
20
|
-
* change to it may not, is the case the line is for, so it still names the pin then.
|
|
21
|
-
*/
|
|
22
|
-
export declare function versionNotice(built: BuiltVersions, app: Partial<Pick<App, "version" | "channel">> | undefined): string | undefined;
|
package/dist/watch.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { Hub, WatchPorts, WatchState } from "./hub.js";
|
|
2
|
-
import { type ExecutionError } from "./notices.js";
|
|
3
|
-
import { type FormCreated, type FormSettled, type PermissionAsked, type PermissionReplied } from "./relay.js";
|
|
4
|
-
export type { FormsTold, WatchPorts, WatchState } from "./hub.js";
|
|
5
|
-
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
6
|
-
export declare const RESUBSCRIBE_MS = 5000;
|
|
7
|
-
/** The part of OpenCode's `session.execution.failed` event the notice is made from. */
|
|
8
|
-
export interface ExecutionFailed {
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly data: {
|
|
11
|
-
readonly sessionID: string;
|
|
12
|
-
readonly error: ExecutionError;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
|
|
17
|
-
* call courier_send. Returns the parents told.
|
|
18
|
-
*/
|
|
19
|
-
export declare function reportFailure(ports: WatchPorts, seen: Set<string>, event: ExecutionFailed): Promise<string[]>;
|
|
20
|
-
/**
|
|
21
|
-
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
22
|
-
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
23
|
-
*/
|
|
24
|
-
export declare function reportAsked(ports: WatchPorts, state: WatchState, event: PermissionAsked): Promise<string[]>;
|
|
25
|
-
/**
|
|
26
|
-
* Tells the session that was told about a request that it has been answered some other way, in the
|
|
27
|
-
* child's own session or along with another answer, so it does not pass on a stale question.
|
|
28
|
-
* Requests answered through courier_answer are no longer waiting and are skipped.
|
|
29
|
-
*/
|
|
30
|
-
export declare function reportReplied(ports: WatchPorts, state: WatchState, event: PermissionReplied): Promise<string[]>;
|
|
31
|
-
/**
|
|
32
|
-
* Tells the session at the top about a form OpenCode shows in a spawned session, such as web
|
|
33
|
-
* search asking for its provider, which only the person can answer there. Question forms are
|
|
34
|
-
* left to the question relay, which passes every question of a spawned session on.
|
|
35
|
-
*/
|
|
36
|
-
export declare function reportForm(ports: WatchPorts, state: WatchState, event: FormCreated): Promise<string[]>;
|
|
37
|
-
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
38
|
-
export declare function reportFormSettled(ports: WatchPorts, state: WatchState, event: FormSettled): Promise<string[]>;
|
|
39
|
-
/**
|
|
40
|
-
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
41
|
-
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
42
|
-
*/
|
|
43
|
-
export declare function relayPending(ports: WatchPorts, state: WatchState): Promise<void>;
|
|
44
|
-
/**
|
|
45
|
-
* Follows OpenCode's events until `signal` aborts, telling parents when a spawned child's turn
|
|
46
|
-
* fails, when it waits for a permission or on a form and when that is answered without them, and
|
|
47
|
-
* noting for the question relay the question forms shown, which it waits for, and the locations
|
|
48
|
-
* shutting down, whose withdrawn forms must not pass for dismissals. Once `signal` aborts it ends
|
|
49
|
-
* as soon as the event it is handling, and the relay of the requests already pending, are done,
|
|
50
|
-
* without waiting for the stream to close: an unloading instance's leave waits for that.
|
|
51
|
-
*/
|
|
52
|
-
export declare function watchChildren(ports: WatchPorts, state: WatchState, signal: AbortSignal, retryMs?: number): Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
* Makes the hub start its subscriptions to OpenCode's events with this copy's watcher, the copy loaded
|
|
55
|
-
* last's, as after an update; `join` starts them. Every instance in the process is sent every event,
|
|
56
|
-
* so one subscription would serve them all; the second is a standby, already connected when the
|
|
57
|
-
* first one's member leaves, so no event goes unseen then. Both handle every event, and the claim
|
|
58
|
-
* sets tell each once. Like any (re)subscription, a new one relays the requests already pending,
|
|
59
|
-
* which the claim sets tell once too.
|
|
60
|
-
*/
|
|
61
|
-
export declare function watchFromHub(hub: Pick<Hub, "subscribe">, state: WatchState, retryMs?: number): void;
|
package/dist/watch.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { addBounded, setBounded } from "./bounded.js";
|
|
2
|
-
import { envelope, failureNotice, formNotice, formSettledNotice, kindOf, permissionNotice, permissionSettledNotice, } from "./notices.js";
|
|
3
|
-
import { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, locationClosing } from "./question/index.js";
|
|
4
|
-
import { listEverywhere, QUESTION_FORM, } from "./relay.js";
|
|
5
|
-
import { allEntries, entriesOf, lineage } from "./roster.js";
|
|
6
|
-
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
7
|
-
export const RESUBSCRIBE_MS = 5_000;
|
|
8
|
-
/** How many handled event ids, and waiting or answered requests, are remembered. */
|
|
9
|
-
const SEEN_MAX = 1_000;
|
|
10
|
-
/** Claims a value in one of the shared sets; false when it was claimed already. */
|
|
11
|
-
const claim = (set, value) => addBounded(set, value, SEEN_MAX);
|
|
12
|
-
/**
|
|
13
|
-
* Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
|
|
14
|
-
* call courier_send. Returns the parents told.
|
|
15
|
-
*/
|
|
16
|
-
export async function reportFailure(ports, seen, event) {
|
|
17
|
-
if (!claim(seen, event.id))
|
|
18
|
-
return [];
|
|
19
|
-
const { sessionID, error } = event.data;
|
|
20
|
-
const entries = await entriesOf(ports.storage, sessionID);
|
|
21
|
-
await Promise.all(entries.map((entry) => ports.session.synthetic({
|
|
22
|
-
sessionID: entry.parentID,
|
|
23
|
-
text: envelope(sessionID, failureNotice(entry.title, error), { failed: error.type }),
|
|
24
|
-
description: `Session ${sessionID} failed`,
|
|
25
|
-
metadata: { source: "courier", from: sessionID, failed: true },
|
|
26
|
-
delivery: "steer",
|
|
27
|
-
})));
|
|
28
|
-
return entries.map((entry) => entry.parentID);
|
|
29
|
-
}
|
|
30
|
-
/** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
|
|
31
|
-
const topOf = (chain) => chain.at(-1).parentID;
|
|
32
|
-
/**
|
|
33
|
-
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
34
|
-
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
35
|
-
*/
|
|
36
|
-
export async function reportAsked(ports, state, event) {
|
|
37
|
-
if (!claim(state.seen, event.id))
|
|
38
|
-
return [];
|
|
39
|
-
const request = event.data;
|
|
40
|
-
const chain = await lineage(ports.storage, request.sessionID);
|
|
41
|
-
// Claimed only now, after the lookup: a request answered meanwhile is in `answered`, and one
|
|
42
|
-
// already relayed, from the event or from the pending list, is in `waiting`.
|
|
43
|
-
if (!chain.length || state.answered.has(request.id) || !claim(state.waiting, request.id))
|
|
44
|
-
return [];
|
|
45
|
-
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
46
|
-
await ports.session.synthetic({
|
|
47
|
-
sessionID: topOf(chain),
|
|
48
|
-
text: envelope(request.sessionID, permissionNotice(chain[0].title, request, startedBy), {
|
|
49
|
-
asks: "permission",
|
|
50
|
-
request: request.id,
|
|
51
|
-
}),
|
|
52
|
-
description: `Session ${request.sessionID} asks for permission`,
|
|
53
|
-
metadata: { source: "courier", from: request.sessionID, asks: "permission", requestID: request.id },
|
|
54
|
-
delivery: "steer",
|
|
55
|
-
});
|
|
56
|
-
return [topOf(chain)];
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Tells the session that was told about a request that it has been answered some other way, in the
|
|
60
|
-
* child's own session or along with another answer, so it does not pass on a stale question.
|
|
61
|
-
* Requests answered through courier_answer are no longer waiting and are skipped.
|
|
62
|
-
*/
|
|
63
|
-
export async function reportReplied(ports, state, event) {
|
|
64
|
-
if (!claim(state.seen, event.id))
|
|
65
|
-
return [];
|
|
66
|
-
const { sessionID, requestID, reply } = event.data;
|
|
67
|
-
if (!state.waiting.delete(requestID)) {
|
|
68
|
-
claim(state.answered, requestID);
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
const chain = await lineage(ports.storage, sessionID);
|
|
72
|
-
if (!chain.length)
|
|
73
|
-
return [];
|
|
74
|
-
await ports.session.synthetic({
|
|
75
|
-
sessionID: topOf(chain),
|
|
76
|
-
text: envelope(sessionID, permissionSettledNotice(chain[0].title, requestID, reply), { answered: reply, request: requestID }),
|
|
77
|
-
description: `Session ${sessionID} no longer asks for permission`,
|
|
78
|
-
metadata: { source: "courier", from: sessionID, answered: reply, requestID },
|
|
79
|
-
delivery: "steer",
|
|
80
|
-
});
|
|
81
|
-
return [topOf(chain)];
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Tells the session at the top about a form OpenCode shows in a spawned session, such as web
|
|
85
|
-
* search asking for its provider, which only the person can answer there. Question forms are
|
|
86
|
-
* left to the question relay, which passes every question of a spawned session on.
|
|
87
|
-
*/
|
|
88
|
-
export async function reportForm(ports, state, event) {
|
|
89
|
-
if (!claim(state.seen, event.id))
|
|
90
|
-
return [];
|
|
91
|
-
const form = event.data.form;
|
|
92
|
-
if (kindOf(form) === QUESTION_FORM)
|
|
93
|
-
return [];
|
|
94
|
-
const chain = await lineage(ports.storage, form.sessionID);
|
|
95
|
-
// As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
|
|
96
|
-
if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
|
|
97
|
-
return [];
|
|
98
|
-
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
99
|
-
const kind = kindOf(form);
|
|
100
|
-
const telling = ports.session.synthetic({
|
|
101
|
-
sessionID: topOf(chain),
|
|
102
|
-
text: envelope(form.sessionID, formNotice(chain[0].title, form, startedBy), {
|
|
103
|
-
asks: "form",
|
|
104
|
-
form: form.id,
|
|
105
|
-
...(kind ? { kind } : {}),
|
|
106
|
-
}),
|
|
107
|
-
description: `Session ${form.sessionID} shows a form`,
|
|
108
|
-
metadata: { source: "courier", from: form.sessionID, asks: "form", formID: form.id, ...(kind ? { kind } : {}) },
|
|
109
|
-
delivery: "steer",
|
|
110
|
-
});
|
|
111
|
-
setBounded(state.forms.told, form.id, telling, SEEN_MAX);
|
|
112
|
-
await telling;
|
|
113
|
-
return [topOf(chain)];
|
|
114
|
-
}
|
|
115
|
-
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
116
|
-
export async function reportFormSettled(ports, state, event) {
|
|
117
|
-
if (!claim(state.seen, event.id))
|
|
118
|
-
return [];
|
|
119
|
-
const { id, sessionID } = event.data;
|
|
120
|
-
const telling = state.forms.told.get(id);
|
|
121
|
-
if (!telling) {
|
|
122
|
-
claim(state.forms.settled, id);
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
state.forms.told.delete(id);
|
|
126
|
-
const settled = event.type === "form.replied" ? "answered" : "cancelled";
|
|
127
|
-
// After the notice that the form is shown, which another instance may still be sending, so the
|
|
128
|
-
// settling never arrives first; and not at all when that notice did not go out.
|
|
129
|
-
const [chain, told] = await Promise.all([lineage(ports.storage, sessionID), telling.then(() => true, () => false)]);
|
|
130
|
-
if (!chain.length || !told)
|
|
131
|
-
return [];
|
|
132
|
-
await ports.session.synthetic({
|
|
133
|
-
sessionID: topOf(chain),
|
|
134
|
-
text: envelope(sessionID, formSettledNotice(chain[0].title, id, settled), { settled, form: id }),
|
|
135
|
-
description: `Session ${sessionID} no longer shows a form`,
|
|
136
|
-
metadata: { source: "courier", from: sessionID, settled, formID: id },
|
|
137
|
-
delivery: "steer",
|
|
138
|
-
});
|
|
139
|
-
return [topOf(chain)];
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
143
|
-
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
144
|
-
*/
|
|
145
|
-
export async function relayPending(ports, state) {
|
|
146
|
-
const sessions = new Set((await allEntries(ports.storage)).map((entry) => entry.sessionID));
|
|
147
|
-
const domains = [...ports.permissions()];
|
|
148
|
-
// Each request is claimed before its notice goes out, so relaying them all at once tells each
|
|
149
|
-
// once, however many locations list it; a location that cannot be read lists none.
|
|
150
|
-
const relay = async (sessionID) => {
|
|
151
|
-
const listed = (await listEverywhere(domains, sessionID)).flatMap((found) => found.requests);
|
|
152
|
-
await Promise.all(listed.map((request) => reportAsked(ports, state, { id: `pending:${request.id}`, data: request })));
|
|
153
|
-
};
|
|
154
|
-
await Promise.all([...sessions].map(relay));
|
|
155
|
-
}
|
|
156
|
-
const pause = (ms, signal) => new Promise((resolve) => {
|
|
157
|
-
const timer = setTimeout(done, ms);
|
|
158
|
-
function done() {
|
|
159
|
-
clearTimeout(timer);
|
|
160
|
-
signal.removeEventListener("abort", done);
|
|
161
|
-
resolve();
|
|
162
|
-
}
|
|
163
|
-
signal.addEventListener("abort", done);
|
|
164
|
-
});
|
|
165
|
-
/**
|
|
166
|
-
* Handles one event of OpenCode's stream; those that cannot concern a spawned child, or a question
|
|
167
|
-
* relayed for one, are ignored.
|
|
168
|
-
*/
|
|
169
|
-
async function handle(ports, state, event) {
|
|
170
|
-
if (event.type === "session.execution.failed")
|
|
171
|
-
return reportFailure(ports, state.seen, event);
|
|
172
|
-
if (event.type === "permission.asked")
|
|
173
|
-
return reportAsked(ports, state, event);
|
|
174
|
-
if (event.type === "permission.replied")
|
|
175
|
-
return reportReplied(ports, state, event);
|
|
176
|
-
if (event.type === "form.created") {
|
|
177
|
-
// Not claimed: the hub's other subscription, or another copy's watcher, may resolve the same
|
|
178
|
-
// waiting call, which is harmless.
|
|
179
|
-
formShown(event);
|
|
180
|
-
return reportForm(ports, state, event);
|
|
181
|
-
}
|
|
182
|
-
if (event.type === "form.replied" || event.type === "form.cancelled")
|
|
183
|
-
return reportFormSettled(ports, state, event);
|
|
184
|
-
// Not claimed either: a location closing withdraws its forms, which must not pass for dismissals.
|
|
185
|
-
// The event's location is optional; without one, the shutdown counts for every location.
|
|
186
|
-
if (event.type === "location.shutdown") {
|
|
187
|
-
const directory = event.location?.directory;
|
|
188
|
-
locationClosing(ports.now(), typeof directory === "string" ? directory : undefined);
|
|
189
|
-
}
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
const ENDED = { done: true, value: undefined };
|
|
193
|
-
/**
|
|
194
|
-
* The events of `events` until it ends or `signal` aborts, whichever comes first; the stream is not
|
|
195
|
-
* closed. Each wait for the next event listens for the abort only while it waits, so a watcher that
|
|
196
|
-
* follows events for the life of the process holds nothing per event it has seen.
|
|
197
|
-
*/
|
|
198
|
-
const until = (events, signal) => ({
|
|
199
|
-
[Symbol.asyncIterator]: () => ({
|
|
200
|
-
next: () => new Promise((resolve, reject) => {
|
|
201
|
-
if (signal.aborted)
|
|
202
|
-
return resolve(ENDED);
|
|
203
|
-
const stop = () => resolve(ENDED);
|
|
204
|
-
signal.addEventListener("abort", stop, { once: true });
|
|
205
|
-
// Once the signal has aborted, whatever the stream ends this wait with is not wanted.
|
|
206
|
-
events.next().then((next) => {
|
|
207
|
-
signal.removeEventListener("abort", stop);
|
|
208
|
-
resolve(next);
|
|
209
|
-
}, (error) => {
|
|
210
|
-
signal.removeEventListener("abort", stop);
|
|
211
|
-
reject(error);
|
|
212
|
-
});
|
|
213
|
-
}),
|
|
214
|
-
}),
|
|
215
|
-
});
|
|
216
|
-
/** Relays the requests already pending, kept in `relaying` until done, for the watcher's end to wait for. */
|
|
217
|
-
function relayAlongside(ports, state, relaying) {
|
|
218
|
-
const relayed = relayPending(ports, state)
|
|
219
|
-
.catch((error) => ports.log(`courier watch: could not relay pending requests: ${String(error)}`))
|
|
220
|
-
.finally(() => relaying.delete(relayed));
|
|
221
|
-
relaying.add(relayed);
|
|
222
|
-
}
|
|
223
|
-
/** Handles one event, logging a failure rather than ending the watch. */
|
|
224
|
-
function handleLogged(ports, state, event) {
|
|
225
|
-
return handle(ports, state, event).catch((error) => {
|
|
226
|
-
const data = event.data;
|
|
227
|
-
const sessionID = data?.sessionID ?? data?.form?.sessionID;
|
|
228
|
-
ports.log(`courier watch: could not handle ${event.type} of ${sessionID}: ${String(error)}`);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Follows OpenCode's events until `signal` aborts, telling parents when a spawned child's turn
|
|
233
|
-
* fails, when it waits for a permission or on a form and when that is answered without them, and
|
|
234
|
-
* noting for the question relay the question forms shown, which it waits for, and the locations
|
|
235
|
-
* shutting down, whose withdrawn forms must not pass for dismissals. Once `signal` aborts it ends
|
|
236
|
-
* as soon as the event it is handling, and the relay of the requests already pending, are done,
|
|
237
|
-
* without waiting for the stream to close: an unloading instance's leave waits for that.
|
|
238
|
-
*/
|
|
239
|
-
export async function watchChildren(ports, state, signal, retryMs = RESUBSCRIBE_MS) {
|
|
240
|
-
const relaying = new Set();
|
|
241
|
-
while (!signal.aborted) {
|
|
242
|
-
let following = false;
|
|
243
|
-
let events;
|
|
244
|
-
try {
|
|
245
|
-
events = ports.event.subscribe({ signal })[Symbol.asyncIterator]();
|
|
246
|
-
// Question forms shown while no instance followed the events were not seen: released now,
|
|
247
|
-
// and again on the first event, by when the stream is surely connected.
|
|
248
|
-
let missed = eventsFollowed();
|
|
249
|
-
following = true;
|
|
250
|
-
// Alongside the new subscription; a request both relays see is relayed once.
|
|
251
|
-
relayAlongside(ports, state, relaying);
|
|
252
|
-
for await (const event of until(events, signal)) {
|
|
253
|
-
if (missed)
|
|
254
|
-
formsMayHaveBeenMissed();
|
|
255
|
-
missed = false;
|
|
256
|
-
await handleLogged(ports, state, event);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
if (!signal.aborted)
|
|
261
|
-
ports.log(`courier watch: event stream broke: ${String(error)}`);
|
|
262
|
-
}
|
|
263
|
-
finally {
|
|
264
|
-
if (following)
|
|
265
|
-
eventsLeft();
|
|
266
|
-
// Closed without waiting: the subscription's own signal has aborted, or the stream ended, and
|
|
267
|
-
// closing the adapter's stream waits on OpenCode's side, which may not answer while a location
|
|
268
|
-
// closes; the leave that waits for this watcher must not wait for that too.
|
|
269
|
-
void events?.return?.().catch(() => { });
|
|
270
|
-
}
|
|
271
|
-
if (!signal.aborted)
|
|
272
|
-
await pause(retryMs, signal);
|
|
273
|
-
}
|
|
274
|
-
await Promise.allSettled(relaying);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Makes the hub start its subscriptions to OpenCode's events with this copy's watcher, the copy loaded
|
|
278
|
-
* last's, as after an update; `join` starts them. Every instance in the process is sent every event,
|
|
279
|
-
* so one subscription would serve them all; the second is a standby, already connected when the
|
|
280
|
-
* first one's member leaves, so no event goes unseen then. Both handle every event, and the claim
|
|
281
|
-
* sets tell each once. Like any (re)subscription, a new one relays the requests already pending,
|
|
282
|
-
* which the claim sets tell once too.
|
|
283
|
-
*/
|
|
284
|
-
export function watchFromHub(hub, state, retryMs = RESUBSCRIBE_MS) {
|
|
285
|
-
hub.subscribe = (member) => {
|
|
286
|
-
const stop = new AbortController();
|
|
287
|
-
return { member, stop, done: watchChildren(member.watch, state, stop.signal, retryMs) };
|
|
288
|
-
};
|
|
289
|
-
}
|
|
File without changes
|
|
File without changes
|