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.
Files changed (143) hide show
  1. package/README.md +198 -193
  2. package/dist/index.d.ts +4 -8
  3. package/dist/index.js +66 -85
  4. package/dist/kernel/protocol.d.ts +20 -0
  5. package/dist/kernel/protocol.js +36 -0
  6. package/dist/kernel/tool.d.ts +5 -0
  7. package/dist/kernel/tool.js +5 -0
  8. package/dist/person/events.d.ts +23 -0
  9. package/dist/person/events.js +113 -0
  10. package/dist/person/form.d.ts +26 -0
  11. package/dist/person/form.js +64 -0
  12. package/dist/person/index.d.ts +17 -0
  13. package/dist/person/index.js +5 -0
  14. package/dist/{relay.d.ts → person/permission.d.ts} +29 -25
  15. package/dist/{relay.js → person/permission.js} +11 -9
  16. package/dist/person/ports.d.ts +58 -0
  17. package/dist/person/ports.js +2 -0
  18. package/dist/{question → person/question}/answer.d.ts +33 -4
  19. package/dist/{question → person/question}/answer.js +49 -17
  20. package/dist/person/question/index.d.ts +7 -0
  21. package/dist/{question → person/question}/index.js +3 -3
  22. package/dist/person/question/lifecycle.d.ts +43 -0
  23. package/dist/{question → person/question}/lifecycle.js +10 -19
  24. package/dist/{question → person/question}/pure.d.ts +4 -6
  25. package/dist/{question → person/question}/pure.js +3 -4
  26. package/dist/{question → person/question}/relay.d.ts +1 -1
  27. package/dist/{question → person/question}/relay.js +27 -35
  28. package/dist/{question → person/question}/runtime.d.ts +2 -3
  29. package/dist/{question → person/question}/runtime.js +2 -3
  30. package/dist/{question → person/question}/shared.d.ts +13 -3
  31. package/dist/person/question/shared.js +52 -0
  32. package/dist/person/question/types.d.ts +121 -0
  33. package/dist/person/question/types.js +1 -0
  34. package/dist/person/rpc-handlers.d.ts +160 -0
  35. package/dist/person/rpc-handlers.js +62 -0
  36. package/dist/person/rpc.d.ts +227 -0
  37. package/dist/person/rpc.js +70 -0
  38. package/dist/person/text.d.ts +68 -0
  39. package/dist/{notices.js → person/text.js} +53 -190
  40. package/dist/person/tools.d.ts +20 -0
  41. package/dist/person/tools.js +55 -0
  42. package/dist/person/tui.d.ts +46 -0
  43. package/dist/platform/bounded.d.ts +12 -0
  44. package/dist/{bounded.js → platform/bounded.js} +6 -4
  45. package/dist/platform/events.d.ts +34 -0
  46. package/dist/platform/events.js +121 -0
  47. package/dist/platform/hub.d.ts +195 -0
  48. package/dist/{hub.js → platform/hub.js} +78 -119
  49. package/dist/platform/json.d.ts +8 -0
  50. package/dist/platform/json.js +15 -0
  51. package/dist/platform/version.d.ts +16 -0
  52. package/dist/{version.js → platform/version.js} +5 -11
  53. package/dist/rpc.d.ts +2 -0
  54. package/dist/rpc.js +3 -0
  55. package/dist/scheduling/index.d.ts +6 -0
  56. package/dist/scheduling/index.js +5 -0
  57. package/dist/{later.d.ts → scheduling/later.d.ts} +13 -9
  58. package/dist/{later.js → scheduling/later.js} +18 -9
  59. package/dist/scheduling/text.d.ts +6 -0
  60. package/dist/scheduling/text.js +10 -0
  61. package/dist/scheduling/tools.d.ts +14 -0
  62. package/dist/scheduling/tools.js +48 -0
  63. package/dist/skill.d.ts +17 -0
  64. package/dist/skill.js +62 -0
  65. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +5 -26
  66. package/dist/{cleanup.js → tree/cleanup.js} +20 -15
  67. package/dist/tree/context.d.ts +15 -0
  68. package/dist/tree/context.js +34 -0
  69. package/dist/tree/domain.d.ts +101 -0
  70. package/dist/tree/domain.js +68 -0
  71. package/dist/tree/events.d.ts +71 -0
  72. package/dist/tree/events.js +204 -0
  73. package/dist/tree/forget.d.ts +24 -0
  74. package/dist/tree/forget.js +59 -0
  75. package/dist/tree/group.d.ts +82 -0
  76. package/dist/tree/group.js +203 -0
  77. package/dist/tree/index.d.ts +10 -0
  78. package/dist/tree/index.js +11 -0
  79. package/dist/tree/limits.d.ts +10 -0
  80. package/dist/tree/limits.js +60 -0
  81. package/dist/tree/panel.d.ts +13 -0
  82. package/dist/tree/panel.js +75 -0
  83. package/dist/tree/ports.d.ts +276 -0
  84. package/dist/tree/ports.js +1 -0
  85. package/dist/tree/report.d.ts +23 -0
  86. package/dist/tree/report.js +33 -0
  87. package/dist/tree/roster.d.ts +27 -0
  88. package/dist/tree/roster.js +62 -0
  89. package/dist/tree/rpc-handlers.d.ts +201 -0
  90. package/dist/tree/rpc-handlers.js +20 -0
  91. package/dist/tree/rpc.d.ts +344 -0
  92. package/dist/tree/rpc.js +83 -0
  93. package/dist/tree/send.d.ts +14 -0
  94. package/dist/tree/send.js +99 -0
  95. package/dist/tree/spawn.d.ts +21 -0
  96. package/dist/tree/spawn.js +173 -0
  97. package/dist/tree/status.d.ts +128 -0
  98. package/dist/tree/status.js +62 -0
  99. package/dist/tree/stop.d.ts +33 -0
  100. package/dist/tree/stop.js +116 -0
  101. package/dist/tree/store.d.ts +24 -0
  102. package/dist/tree/store.js +264 -0
  103. package/dist/tree/subtree.d.ts +101 -0
  104. package/dist/tree/subtree.js +60 -0
  105. package/dist/tree/summary.d.ts +7 -0
  106. package/dist/tree/summary.js +35 -0
  107. package/dist/tree/text.d.ts +77 -0
  108. package/dist/tree/text.js +287 -0
  109. package/dist/tree/tools.d.ts +54 -0
  110. package/dist/tree/tools.js +167 -0
  111. package/dist/tree/tui.d.ts +53 -0
  112. package/dist/tui.d.ts +3 -0
  113. package/dist/tui.js +1219 -0
  114. package/dist/webhook/index.d.ts +5 -0
  115. package/dist/webhook/index.js +3 -0
  116. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +25 -36
  117. package/dist/{webhook.js → webhook/receiver.js} +66 -67
  118. package/dist/webhook/subscriptions.d.ts +29 -0
  119. package/dist/webhook/subscriptions.js +52 -0
  120. package/dist/webhook/text.d.ts +23 -0
  121. package/dist/webhook/text.js +82 -0
  122. package/dist/webhook/tools.d.ts +19 -0
  123. package/dist/webhook/tools.js +46 -0
  124. package/package.json +18 -7
  125. package/dist/bounded.d.ts +0 -10
  126. package/dist/courier.d.ts +0 -95
  127. package/dist/courier.js +0 -131
  128. package/dist/hub.d.ts +0 -325
  129. package/dist/json.d.ts +0 -4
  130. package/dist/json.js +0 -4
  131. package/dist/notices.d.ts +0 -246
  132. package/dist/question/index.d.ts +0 -6
  133. package/dist/question/lifecycle.d.ts +0 -52
  134. package/dist/question/shared.js +0 -24
  135. package/dist/roster.d.ts +0 -81
  136. package/dist/roster.js +0 -175
  137. package/dist/tools.d.ts +0 -74
  138. package/dist/tools.js +0 -228
  139. package/dist/version.d.ts +0 -22
  140. package/dist/watch.d.ts +0 -61
  141. package/dist/watch.js +0 -289
  142. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  143. /package/dist/{storage.js → platform/storage.js} +0 -0
package/dist/notices.d.ts DELETED
@@ -1,246 +0,0 @@
1
- /** Closes a tool result after which the caller most likely has nothing left to do. */
2
- export declare const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
3
- export declare function childBrief(parentID: string, task: string): string;
4
- export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
5
- export declare function spawnText(child: {
6
- readonly sessionID: string;
7
- readonly directory: string;
8
- readonly rosterError?: string;
9
- }): string;
10
- export declare const sendText: (sessionID: string) => string;
11
- /** courier_status's result: the session's state, as JSON. */
12
- export declare const statusText: (status: unknown) => string;
13
- export declare const childrenText: (listed: ReadonlyArray<unknown>) => string;
14
- /** What courier_cleanup did: removed the worktree, found it gone, or kept it and why. */
15
- export type CleanupResult = {
16
- readonly sessionID: string;
17
- readonly directory: string;
18
- readonly outcome: "removed";
19
- } | {
20
- readonly sessionID: string;
21
- readonly directory: string;
22
- readonly outcome: "gone";
23
- } | {
24
- readonly sessionID: string;
25
- readonly directory: string;
26
- readonly outcome: "kept";
27
- readonly reason: string;
28
- readonly changes: readonly string[];
29
- readonly commits: readonly string[];
30
- };
31
- export declare function cleanupText(result: CleanupResult): string;
32
- /** Why a worktree in this state must be kept, or undefined when removing it loses nothing. */
33
- export declare function keepReason(state: {
34
- readonly changes: readonly string[];
35
- readonly commits: readonly string[];
36
- }): string | undefined;
37
- /**
38
- * What courier_answer did: a permission request's result carries the reply, a question's whether its
39
- * answers went out as the result of its call or, since that call had been cut off, as a message.
40
- */
41
- export type AnswerResult = PermissionAnswered | QuestionAnswered;
42
- /**
43
- * What answering a permission request did; `answered` is false when no location loaded in this
44
- * server held the request: it was answered, the session stopped waiting, or it waits in another
45
- * OpenCode server on the same data directory.
46
- */
47
- export interface PermissionAnswered {
48
- readonly sessionID: string;
49
- readonly requestID: string;
50
- readonly reply: Reply;
51
- readonly answered: boolean;
52
- }
53
- /** What answering a question did; `answered` is false when it no longer waited. */
54
- export type QuestionAnswered = {
55
- readonly sessionID: string;
56
- readonly requestID: string;
57
- readonly answered: false;
58
- } | {
59
- readonly sessionID: string;
60
- readonly requestID: string;
61
- readonly answered: true;
62
- readonly by: "result" | "message";
63
- readonly answers: Answers;
64
- };
65
- export declare function answerText(result: AnswerResult): string;
66
- /** `own` when the message goes to the session that scheduled it. */
67
- export declare function laterText(entry: {
68
- readonly id: string;
69
- readonly sessionID: string;
70
- }, fireAt: string, own: boolean): string;
71
- export declare const cancelText: (id: string, cancelled: boolean) => string;
72
- /** `receiving` says whether a webhook receiver runs in this server. */
73
- export declare function subscribeText(sessionID: string, topic: string, receiving: boolean): string;
74
- export declare const unsubscribeText: (sessionID: string, dropped: readonly string[]) => string;
75
- /** The error of OpenCode's `session.execution.failed` event. */
76
- export interface ExecutionError {
77
- readonly type: string;
78
- readonly message: string;
79
- readonly status?: number;
80
- }
81
- export declare function failureNotice(title: string, error: ExecutionError): string;
82
- /** The answers to a permission request, as OpenCode's own prompt offers them. */
83
- export declare const REPLIES: readonly ["once", "always", "reject"];
84
- export type Reply = (typeof REPLIES)[number];
85
- /** A permission request, as OpenCode's `permission.asked` event carries it. */
86
- export interface PermissionRequest {
87
- readonly id: string;
88
- readonly sessionID: string;
89
- readonly action: string;
90
- readonly resources: ReadonlyArray<string>;
91
- readonly save?: ReadonlyArray<string>;
92
- readonly message?: string;
93
- }
94
- /**
95
- * Sent with a rejection that has no reason of its own. OpenCode ends the child's turn on a bare
96
- * rejection, and the child would never report back; with a message, the call fails and it carries on.
97
- */
98
- export declare const REJECTED = "Refused by the session that started you. Do without it, or report back why you cannot.";
99
- /** How a notice names where the asking session came from, to the session at the top of its lineage. */
100
- export declare const origin: (startedBy?: string) => string;
101
- /** For a notice that a session's turn ended without it finishing. */
102
- export declare const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
103
- /**
104
- * What the session at the top is told. `startedBy` names the session that started the asking one
105
- * when that is not the top session itself, but one started from it.
106
- */
107
- export declare function permissionNotice(title: string, request: PermissionRequest, startedBy?: string): string;
108
- /** What the session told about a permission request is told once it is answered without courier_answer. */
109
- export declare function permissionSettledNotice(title: string, requestID: string, reply: Reply): string;
110
- /** The `metadata.kind` of the forms OpenCode's web search asks its provider with. */
111
- export declare const WEBSEARCH_FORM = "websearch.provider";
112
- /** One field of an OpenCode form, as much of it as a notice shows. */
113
- export interface FormField {
114
- readonly key: string;
115
- readonly type: string;
116
- readonly title?: string;
117
- readonly description?: string;
118
- readonly options?: ReadonlyArray<{
119
- readonly value: string;
120
- readonly label: string;
121
- readonly description?: string;
122
- }>;
123
- readonly url?: string;
124
- /** Not shown to the person. */
125
- readonly hidden?: boolean;
126
- /** Conditions on earlier answers; the field is shown only when they hold. */
127
- readonly when?: ReadonlyArray<unknown>;
128
- }
129
- /** An OpenCode form, as its `form.created` event carries it. */
130
- export interface Form {
131
- readonly id: string;
132
- readonly sessionID: string;
133
- readonly title: string;
134
- readonly metadata?: Readonly<Record<string, unknown>>;
135
- readonly fields: ReadonlyArray<FormField>;
136
- }
137
- /** A form's `metadata.kind`, when it has one that can go in a notice's envelope. */
138
- export declare const kindOf: (form: Form) => string | undefined;
139
- /**
140
- * What the session at the top is told about a form OpenCode shows in a session started with
141
- * courier_spawn, which the plugin cannot pass on or answer: only the person can, in that session.
142
- */
143
- export declare function formNotice(title: string, form: Form, startedBy?: string): string;
144
- /** What the session told about a form is told once it is answered or withdrawn. */
145
- export declare function formSettledNotice(title: string, formID: string, settled: "answered" | "cancelled"): string;
146
- /** One question of OpenCode's question tool, as the asking model wrote it. */
147
- export interface Prompt {
148
- readonly question: string;
149
- readonly header: string;
150
- readonly options: ReadonlyArray<{
151
- readonly label: string;
152
- readonly description: string;
153
- }>;
154
- readonly multiple?: boolean;
155
- }
156
- /** The answers to a question call, as the question tool returns them: one list per question. */
157
- export type Answers = string[][];
158
- /** A question a spawned session asked with its question tool, stored until it is answered. */
159
- export interface Asked {
160
- readonly requestID: string;
161
- readonly sessionID: string;
162
- /** The session told, at the top of the sessions started with courier_spawn; it alone answers. */
163
- readonly top: string;
164
- readonly title: string;
165
- /** The session that started the asking one, when that is not the top session itself. */
166
- readonly startedBy?: string;
167
- readonly questions: ReadonlyArray<Prompt>;
168
- readonly askedAt: number;
169
- }
170
- /** How a question was settled without the top session: in the asking session, or not at all. */
171
- export type Elsewhere = {
172
- readonly by: "child";
173
- readonly answers: Answers;
174
- } | {
175
- readonly by: "dismissed";
176
- } | {
177
- readonly by: "failed";
178
- readonly error: string;
179
- };
180
- /** What the top session's answer does to a call that waits: answers it, or dismisses it. */
181
- export type Outcome = {
182
- readonly answers: Answers;
183
- } | {
184
- readonly dismissed: true;
185
- };
186
- /**
187
- * Why a linked call is withdrawn: its question was settled elsewhere, or the top session's outcome
188
- * reached it another way (courier_answer, or a dismissal through an earlier call linked to it).
189
- */
190
- export type Withdrawal = Elsewhere | {
191
- readonly by: "top";
192
- readonly outcome: Outcome;
193
- };
194
- /** What OpenCode's question tool tells the asking model once it has the answers. */
195
- export declare function answeredText(questions: ReadonlyArray<Prompt>, answers: ReadonlyArray<ReadonlyArray<string>>): string;
196
- declare const CUT_OFF: {
197
- stopped: string;
198
- restarted: string;
199
- };
200
- /** Why a question's call was cut off: its turn was stopped, or OpenCode restarted or closed its location. */
201
- export type CutOff = keyof typeof CUT_OFF;
202
- /** What the top session is told about a question, when it is asked or after its call was cut off. */
203
- export declare function questionNotice(asked: Asked, cutOff?: CutOff): string;
204
- /** What the top session is told about a question settled without it. */
205
- export declare function questionSettledNotice(asked: Asked, how: Elsewhere): string;
206
- /** The result of a spawned session's question call that the person dismissed through the top session. */
207
- export declare const DISMISSED = "The person dismissed this question without answering it. Carry on without the answers, or report back with courier_send to the session that started you why you cannot.";
208
- /** The message a spawned session whose question call was cut off gets, with the top session's outcome. */
209
- export declare function cutOffAnswer(asked: Asked, outcome: Outcome): string;
210
- /**
211
- * What a top session's question call that no waiting question was linked to adds to its result, when
212
- * questions with the same choices wait: `waiting` lists them.
213
- */
214
- export declare function unlinkedNote(waiting: ReadonlyArray<Pick<Asked, "sessionID" | "requestID">>): string;
215
- /** The result of a linked call withdrawn because its question was settled without it. */
216
- export declare function withdrawnText(linked: Asked, how: Withdrawal): string;
217
- /**
218
- * What a linked call adds to the person's answers about passing them on: `passed` is what passing
219
- * them on gave ("result", "message", or undefined when it no longer waited), or the error as
220
- * `error: <message>`.
221
- */
222
- export declare function passedNote(linked: Asked, passed: string | undefined): string;
223
- export declare function clipText(text: string, max?: number): string;
224
- /** The GitHub CI events, with how a summary names each. */
225
- export declare const CI_EVENTS: Record<string, string>;
226
- /** What every GitHub delivery carries, read once for the summary of its event. */
227
- export interface Delivery {
228
- readonly name: string;
229
- readonly body: Record<string, any>;
230
- readonly repo: string;
231
- readonly action: string | undefined;
232
- readonly sender: string | undefined;
233
- /** ` by <sender>`, or nothing when the delivery names no sender. */
234
- readonly by: string;
235
- /** The pull request and issue numbers it concerns, which `webhook.ts` reads for its topics. */
236
- readonly numbers: readonly number[];
237
- }
238
- /** The summary of a GitHub delivery that wakes its subscribers. */
239
- export declare function githubSummary(delivery: Delivery): string;
240
- /** The summary of a delivery to `/hook/<name>`: its text, clipped. */
241
- export declare const hookSummary: (text: string) => string;
242
- /** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
243
- export declare function defuse(text: string): string;
244
- /** The text a subscribed session is woken with for a webhook delivery, before its envelope. */
245
- export declare const webhookText: (summary: string) => string;
246
- export {};
@@ -1,6 +0,0 @@
1
- export { answerQuestion, noticeCutOff, pendingQuestions } from "./answer.js";
2
- export { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, joinRelay, locationClosing, shutdownReportedAt } from "./lifecycle.js";
3
- export { byCodeUnit, type QuestionAnswerInput } from "./pure.js";
4
- export { relayQuestions } from "./relay.js";
5
- export { isQuestion, MAX_STORED, QUESTION_TOOL } from "./shared.js";
6
- export type { QuestionPorts, QuestionTiming } from "../hub.js";
@@ -1,52 +0,0 @@
1
- import { Effect } from "effect";
2
- import type { QuestionPorts } from "../hub.js";
3
- /**
4
- * For tests: when the location at `directory` was last reported shutting down, if at all; a shutdown
5
- * reported without a location counts for every directory, and with none given, any location's counts.
6
- */
7
- export declare const shutdownReportedAt: (directory?: string) => number | undefined;
8
- /**
9
- * Called for OpenCode's `location.shutdown`: the location at `directory` is closing, which
10
- * withdraws its open forms as if the person had dismissed them (before unloading the plugin
11
- * there, since OpenCode 2.0.22). The event's location is optional in the schema; without it,
12
- * the shutdown counts for every location. `at` is when, by the watcher's clock (epoch milliseconds).
13
- */
14
- export declare function locationClosing(at: number, directory?: string): void;
15
- /**
16
- * Whether a dismissal just seen in the location at `directory` was that location closing rather
17
- * than the person: true when it shut down within the dismissal grace before, or does so, or this
18
- * instance unloads, within the grace from now, by the clock of `ports`. A shutdown reported without a location counts for every
19
- * location, and with the directory unknown, any location's shutdown counts.
20
- */
21
- export declare const closingSoon: (ports: QuestionPorts, loaded: () => boolean, directory: string | undefined) => Effect.Effect<boolean>;
22
- /**
23
- * Adds a plugin instance's ports to `loaded`, which copies of other versions read to tell a cut-off
24
- * question through it, until the returned function is called. A location loading again forgets the
25
- * shutdown recorded for it, so a dismissal there is not mistaken for that shutdown. One recorded for
26
- * every location is left alone, since a dismissal held in another location may still need it; it
27
- * ages out with the grace.
28
- */
29
- export declare function joinRelay(ports: QuestionPorts): () => void;
30
- /** Called for OpenCode's `form.created`: a question call's form is shown, so its question can be relayed. */
31
- export declare function formShown(event: {
32
- readonly data: {
33
- readonly form: {
34
- readonly sessionID: string;
35
- readonly metadata?: unknown;
36
- };
37
- };
38
- }): void;
39
- /**
40
- * Called when an event stream may have missed forms being shown: every call still waiting for its
41
- * form is taken as shown. One still waiting for its permission check is relayed early, which is the
42
- * lesser harm.
43
- */
44
- export declare function formsMayHaveBeenMissed(): void;
45
- /**
46
- * Called when an instance starts following OpenCode's events. Every instance is sent every event,
47
- * so a form shown is missed only while none follows them; then the calls waiting are released, and
48
- * true is returned, for the caller to release them again on its first event, once it is connected.
49
- */
50
- export declare function eventsFollowed(): boolean;
51
- /** Called when an instance's event stream ended or broke. */
52
- export declare function eventsLeft(): void;
@@ -1,24 +0,0 @@
1
- import { addBounded } from "../bounded.js";
2
- import { hub } from "../hub.js";
3
- /** OpenCode's own question tool, which the relay wraps. */
4
- export const QUESTION_TOOL = "question";
5
- export const PREFIX = "question/";
6
- /** At most this many questions are kept in storage; the oldest go first. */
7
- export const MAX_STORED = 100;
8
- export const storedOf = ({ directory: _directory, call: _call, link: _link, relaying: _relaying, settling: _settling, ...asked }) => asked;
9
- export const isPassing = (requestID) => shared.passing.has(requestID) || shared.answered.has(requestID);
10
- // Shared by every plugin instance in the process, since a child and the session it asks can be in
11
- // different locations.
12
- export const shared = hub.questions;
13
- /**
14
- * The ports of an instance still loaded, if any: a member of this copy's hub, or else one that a
15
- * copy of another hub version, or a release before the hub, keeps in `loaded`.
16
- */
17
- export const anyLoaded = () => hub.members.values().next().value?.questions ?? shared.loaded.values().next().value;
18
- export const keyOf = (requestID) => `${PREFIX}${requestID}`;
19
- /** Whether a request id is a question's rather than a permission request's. */
20
- export const isQuestion = (requestID) => requestID.startsWith("question_");
21
- /** How many noticed and answered questions are remembered. */
22
- const REMEMBERED = 1_000;
23
- /** Adds to one of the shared bounded sets. */
24
- export const claim = (set, value) => addBounded(set, value, REMEMBERED);
package/dist/roster.d.ts DELETED
@@ -1,81 +0,0 @@
1
- import { type Storage } from "./storage.js";
2
- /**
3
- * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
4
- * except isolated children whose worktree is still there: those stay until courier_cleanup.
5
- */
6
- export declare const RETENTION_MS: number;
7
- /** A session started with courier_spawn, recorded under the session that started it. */
8
- export interface RosterEntry {
9
- readonly sessionID: string;
10
- readonly parentID: string;
11
- readonly title: string;
12
- readonly directory: string;
13
- readonly isolated: boolean;
14
- readonly createdAt: number;
15
- /** For an isolated child, the directory its worktree was made from; for the record, nothing acts on it. */
16
- readonly source?: string;
17
- /** For an isolated child, the project its worktree belongs to; courier_cleanup removes it from there. */
18
- readonly project?: string;
19
- /** For an isolated child, the commit its worktree was made from; its own work is what came after. */
20
- readonly base?: string;
21
- }
22
- /**
23
- * What the reverse index holds for a child: the sessions above it, its parent first and the session
24
- * at the top, which courier_spawn did not start, last. Its roster entry is under the first, and each
25
- * of the others is the parent of the one before it.
26
- */
27
- export interface ReverseEntry {
28
- readonly ancestors: ReadonlyArray<string>;
29
- }
30
- export type RosterStorage = Storage;
31
- export declare function rosterKey(parentID: string, sessionID: string): string;
32
- export declare function reverseKey(sessionID: string): string;
33
- /**
34
- * Records a child under its parent, then indexes it. The entry is what counts: a failed index
35
- * write leaves it to be found by a scan, and to be indexed when the plugin is next loaded.
36
- */
37
- export declare function record(storage: RosterStorage, entry: RosterEntry): Promise<void>;
38
- /**
39
- * Removes a child's roster entry and its reverse key. Only the entry's removal can fail it: a
40
- * reverse key left behind leads nowhere, and the next load drops it.
41
- */
42
- export declare function remove(storage: RosterStorage, parentID: string, sessionID: string): Promise<void>;
43
- /** Removes a child from its parent's roster; false when it was not there. */
44
- export declare function forget(storage: RosterStorage, parentID: string, sessionID: string): Promise<boolean>;
45
- /** A parent's children, oldest first. */
46
- export declare function children(storage: RosterStorage, parentID: string): Promise<RosterEntry[]>;
47
- /** Every parent's children. */
48
- export declare function allEntries(storage: RosterStorage): Promise<RosterEntry[]>;
49
- /** A session's roster entries: one for the parent that started it, none if courier_spawn did not. */
50
- export declare function entriesOf(storage: RosterStorage, sessionID: string): Promise<RosterEntry[]>;
51
- /**
52
- * The roster entries from a session up to the top, its own first: the session's, its parent's, and
53
- * so on, ending with the entry whose parent courier_spawn did not start. Empty when courier_spawn
54
- * did not start the session.
55
- */
56
- export declare function lineage(storage: RosterStorage, sessionID: string): Promise<RosterEntry[]>;
57
- /** `lineage` over roster entries already read. */
58
- export declare function lineageIn(entries: ReadonlyArray<RosterEntry>, sessionID: string): RosterEntry[];
59
- /**
60
- * The session at the top of a spawned session's lineage, where its permission requests and
61
- * questions go, since that is where the person is; only it may answer them. Throws when the
62
- * session was not started with courier_spawn or `callerID` is another session. `what` names
63
- * what is being answered.
64
- */
65
- export declare function answeringTop(storage: RosterStorage, sessionID: string, callerID: string, what: string): Promise<string>;
66
- /** Whether a directory still exists; tests pass a fake. */
67
- export type Exists = (directory: string) => boolean;
68
- /** A parent's children, oldest first, after dropping the expired ones. */
69
- export declare function current(storage: RosterStorage, parentID: string, now: number, exists?: Exists): Promise<RosterEntry[]>;
70
- /**
71
- * On loading: drops expired entries of every parent, so parents that never list their children
72
- * don't keep them forever, then brings the reverse index in line with what is left (`backfill`).
73
- */
74
- export declare function pruneExpired(storage: RosterStorage, now: number, exists?: Exists): Promise<void>;
75
- /**
76
- * Writes the reverse keys missing for the given entries, or naming another parent, all of the roster, such as those an older
77
- * copy of the plugin recorded, and drops the reverse keys whose entry is gone, such as those of an
78
- * entry an older copy removed. Keys that lead to their entry are left as they are, so running it
79
- * again changes nothing.
80
- */
81
- export declare function backfill(storage: RosterStorage, entries: ReadonlyArray<RosterEntry>): Promise<void>;
package/dist/roster.js DELETED
@@ -1,175 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { scanAll, scanEntries } from "./storage.js";
3
- /**
4
- * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
5
- * except isolated children whose worktree is still there: those stay until courier_cleanup.
6
- */
7
- export const RETENTION_MS = 14 * 24 * 60 * 60_000;
8
- const PREFIX = "roster/";
9
- /**
10
- * The reverse index: under `roster-by-child/<sessionID>`, a child's `ReverseEntry`, written with its
11
- * roster entry. It is only an index: an entry without one, written by an older copy of the plugin,
12
- * is found by scanning `roster/`, and every entry it leads to is read from `roster/` to check that
13
- * it is still there.
14
- */
15
- const BY_CHILD = "roster-by-child/";
16
- export function rosterKey(parentID, sessionID) {
17
- return `${PREFIX}${parentID}/${sessionID}`;
18
- }
19
- export function reverseKey(sessionID) {
20
- return `${BY_CHILD}${sessionID}`;
21
- }
22
- /**
23
- * Records a child under its parent, then indexes it. The entry is what counts: a failed index
24
- * write leaves it to be found by a scan, and to be indexed when the plugin is next loaded.
25
- */
26
- export async function record(storage, entry) {
27
- await storage.set(rosterKey(entry.parentID, entry.sessionID), { ...entry });
28
- await index(storage, entry).catch(() => undefined);
29
- }
30
- async function index(storage, entry) {
31
- const above = await storage.get(reverseKey(entry.parentID));
32
- const ancestors = isReverse(above) ? above.ancestors : (await lineage(storage, entry.parentID)).map((parent) => parent.parentID);
33
- await storage.set(reverseKey(entry.sessionID), { ancestors: [entry.parentID, ...ancestors] });
34
- }
35
- function isReverse(value) {
36
- const ancestors = value?.ancestors;
37
- return Array.isArray(ancestors) && ancestors.length > 0 && ancestors.every((id) => typeof id === "string");
38
- }
39
- /**
40
- * Removes a child's roster entry and its reverse key. Only the entry's removal can fail it: a
41
- * reverse key left behind leads nowhere, and the next load drops it.
42
- */
43
- export async function remove(storage, parentID, sessionID) {
44
- await Promise.all([storage.remove(rosterKey(parentID, sessionID)), storage.remove(reverseKey(sessionID)).catch(() => undefined)]);
45
- }
46
- /** Removes a child from its parent's roster; false when it was not there. */
47
- export async function forget(storage, parentID, sessionID) {
48
- if ((await storage.get(rosterKey(parentID, sessionID))) === undefined)
49
- return false;
50
- await remove(storage, parentID, sessionID);
51
- return true;
52
- }
53
- /** A parent's children, oldest first. */
54
- export async function children(storage, parentID) {
55
- const entries = await scanAll(storage, `${PREFIX}${parentID}/`);
56
- return entries.sort((a, b) => a.createdAt - b.createdAt);
57
- }
58
- /** Every parent's children. */
59
- export async function allEntries(storage) {
60
- return scanAll(storage, PREFIX);
61
- }
62
- /** A session's roster entries: one for the parent that started it, none if courier_spawn did not. */
63
- export async function entriesOf(storage, sessionID) {
64
- const indexed = await storage.get(reverseKey(sessionID));
65
- if (!isReverse(indexed))
66
- return (await scanAll(storage, PREFIX)).filter((entry) => entry.sessionID === sessionID);
67
- const entry = await storage.get(rosterKey(indexed.ancestors[0], sessionID));
68
- return entry === undefined ? [] : [entry];
69
- }
70
- /**
71
- * The roster entries from a session up to the top, its own first: the session's, its parent's, and
72
- * so on, ending with the entry whose parent courier_spawn did not start. Empty when courier_spawn
73
- * did not start the session.
74
- */
75
- export async function lineage(storage, sessionID) {
76
- const first = await storage.get(reverseKey(sessionID));
77
- if (!isReverse(first))
78
- return lineageIn(await allEntries(storage), sessionID);
79
- let indexed = first;
80
- const chain = [];
81
- let below = sessionID;
82
- // One read per level, all at once, and one more for the top's own reverse key, in case it was
83
- // recorded after the key that names it; the chain ends where an entry is gone, as a scan's would.
84
- for (;;) {
85
- const { ancestors } = indexed;
86
- const top = ancestors.at(-1);
87
- const [entries, above] = await Promise.all([
88
- Promise.all(ancestors.map((parentID, level) => storage.get(rosterKey(parentID, level ? ancestors[level - 1] : below)))),
89
- storage.get(reverseKey(top)),
90
- ]);
91
- for (const entry of entries) {
92
- if (!entry || chain.some((known) => known.sessionID === entry.sessionID))
93
- return chain;
94
- chain.push(entry);
95
- }
96
- if (!isReverse(above))
97
- return chain;
98
- below = top;
99
- indexed = above;
100
- }
101
- }
102
- /** `lineage` over roster entries already read. */
103
- export function lineageIn(entries, sessionID) {
104
- const bySession = new Map(entries.map((entry) => [entry.sessionID, entry]));
105
- const chain = [];
106
- for (let entry = bySession.get(sessionID); entry && !chain.includes(entry); entry = bySession.get(entry.parentID))
107
- chain.push(entry);
108
- return chain;
109
- }
110
- /**
111
- * The session at the top of a spawned session's lineage, where its permission requests and
112
- * questions go, since that is where the person is; only it may answer them. Throws when the
113
- * session was not started with courier_spawn or `callerID` is another session. `what` names
114
- * what is being answered.
115
- */
116
- export async function answeringTop(storage, sessionID, callerID, what) {
117
- const chain = await lineage(storage, sessionID);
118
- if (!chain.length)
119
- throw new Error(`${sessionID} was not started with courier_spawn, so courier_answer cannot answer for it.`);
120
- const top = chain.at(-1).parentID;
121
- if (top !== callerID)
122
- throw new Error(`${sessionID}'s ${what} go to ${top}, the session at the top of the sessions started from it with courier_spawn; ${callerID} cannot answer them.`);
123
- return top;
124
- }
125
- /**
126
- * Removes the given entries that were recorded more than `RETENTION_MS` before `now`, and returns
127
- * the rest. An isolated child is kept while its worktree exists, so it can still be cleaned up.
128
- */
129
- async function dropExpired(storage, entries, now, exists) {
130
- const expired = new Set(entries.filter((entry) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory))));
131
- await Promise.all([...expired].map((entry) => remove(storage, entry.parentID, entry.sessionID)));
132
- return entries.filter((entry) => !expired.has(entry));
133
- }
134
- /** A parent's children, oldest first, after dropping the expired ones. */
135
- export async function current(storage, parentID, now, exists = existsSync) {
136
- return dropExpired(storage, await children(storage, parentID), now, exists);
137
- }
138
- /**
139
- * On loading: drops expired entries of every parent, so parents that never list their children
140
- * don't keep them forever, then brings the reverse index in line with what is left (`backfill`).
141
- */
142
- export async function pruneExpired(storage, now, exists = existsSync) {
143
- await backfill(storage, await dropExpired(storage, await scanAll(storage, PREFIX), now, exists));
144
- }
145
- /** Whether a reverse key's value leads to this entry. */
146
- function indexes(value, entry) {
147
- return isReverse(value) && value.ancestors[0] === entry.parentID;
148
- }
149
- /**
150
- * Writes the reverse keys missing for the given entries, or naming another parent, all of the roster, such as those an older
151
- * copy of the plugin recorded, and drops the reverse keys whose entry is gone, such as those of an
152
- * entry an older copy removed. Keys that lead to their entry are left as they are, so running it
153
- * again changes nothing.
154
- */
155
- export async function backfill(storage, entries) {
156
- const indexed = new Map((await scanEntries(storage, BY_CHILD)).map(({ key, value }) => [key, value]));
157
- const sessions = new Set(entries.map((entry) => entry.sessionID));
158
- await Promise.all([
159
- ...entries
160
- .filter((entry) => !indexes(indexed.get(entry.sessionID), entry))
161
- .map((entry) => storage.set(reverseKey(entry.sessionID), { ancestors: lineageIn(entries, entry.sessionID).map((above) => above.parentID) })),
162
- ...[...indexed]
163
- .filter(([sessionID]) => !sessions.has(sessionID))
164
- .map(([sessionID, value]) => dropDangling(storage, sessionID, value)),
165
- ]);
166
- }
167
- /**
168
- * Drops a reverse key whose entry was not in the roster read, unless that entry is there now:
169
- * another instance may have recorded the child since, and it writes the entry before the key.
170
- */
171
- async function dropDangling(storage, sessionID, value) {
172
- if (isReverse(value) && (await storage.get(rosterKey(value.ancestors[0], sessionID))) !== undefined)
173
- return;
174
- await storage.remove(reverseKey(sessionID));
175
- }
package/dist/tools.d.ts DELETED
@@ -1,74 +0,0 @@
1
- import type { Plugin } from "@opencode/plugin";
2
- import { Schema } from "effect";
3
- import { type CleanupPorts } from "./cleanup.js";
4
- import { type CourierPorts } from "./courier.js";
5
- import { type LaterPorts } from "./later.js";
6
- import { type QuestionPorts } from "./question/index.js";
7
- import { type AnswerPorts, type Waiting } from "./relay.js";
8
- import { type WebhookPorts } 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 declare const SpawnInput: Schema.Struct<{
14
- readonly task: Schema.String;
15
- readonly title: Schema.optional<Schema.String>;
16
- readonly agent: Schema.optional<Schema.String>;
17
- readonly isolate: Schema.optional<Schema.Boolean>;
18
- }>;
19
- export declare const SendInput: Schema.Struct<{
20
- readonly sessionID: Schema.String;
21
- readonly message: Schema.String;
22
- readonly queue: Schema.optional<Schema.Boolean>;
23
- }>;
24
- export declare const StatusInput: Schema.Struct<{
25
- readonly sessionID: Schema.String;
26
- }>;
27
- export declare const ChildrenInput: Schema.Struct<{
28
- readonly sessionID: Schema.optional<Schema.String>;
29
- }>;
30
- export declare const CleanupInput: Schema.Struct<{
31
- readonly sessionID: Schema.String;
32
- readonly force: Schema.optional<Schema.Boolean>;
33
- }>;
34
- export declare const LaterInput: Schema.Struct<{
35
- readonly message: Schema.String;
36
- readonly delayMinutes: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String]>>;
37
- readonly at: Schema.optional<Schema.String>;
38
- readonly sessionID: Schema.optional<Schema.String>;
39
- }>;
40
- export declare const AnswerInput: Schema.Struct<{
41
- readonly sessionID: Schema.String;
42
- readonly requestID: Schema.String;
43
- readonly reply: Schema.optional<Schema.Literals<readonly ["once", "always", "reject"]>>;
44
- readonly message: Schema.optional<Schema.String>;
45
- readonly answers: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>>>;
46
- }>;
47
- export declare const CancelInput: Schema.Struct<{
48
- readonly id: Schema.String;
49
- }>;
50
- export declare const SubscribeInput: Schema.Struct<{
51
- readonly topic: Schema.String;
52
- readonly sessionID: Schema.optional<Schema.String>;
53
- }>;
54
- export declare const UnsubscribeInput: Schema.Struct<{
55
- readonly topic: Schema.optional<Schema.String>;
56
- readonly sessionID: Schema.optional<Schema.String>;
57
- }>;
58
- /** The editor OpenCode hands a tool transform. */
59
- export type ToolEditor = Parameters<Parameters<Plugin.Context["tool"]["transform"]>[0]>[0];
60
- /** What the tools run on: one plugin instance's ports. */
61
- export interface ToolPorts {
62
- readonly courier: CourierPorts;
63
- readonly cleanup: CleanupPorts;
64
- readonly later: LaterPorts;
65
- readonly answer: AnswerPorts;
66
- readonly questions: QuestionPorts;
67
- readonly hooks: WebhookPorts;
68
- /** The permission requests sessions were told about and have not answered. */
69
- readonly waiting: Waiting;
70
- /** Whether a webhook receiver runs in this server. */
71
- readonly receiving: () => Promise<boolean>;
72
- }
73
- /** Adds the courier tools, each a direct tool rather than a code-mode one. */
74
- export declare function addTools(tools: ToolEditor, ports: ToolPorts): void;