opencode-courier 0.2.2 → 0.3.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 +247 -108
- package/dist/bounded.d.ts +8 -0
- package/dist/bounded.js +19 -0
- package/dist/cleanup.d.ts +21 -21
- package/dist/cleanup.js +61 -30
- package/dist/courier.d.ts +116 -15
- package/dist/courier.js +239 -74
- package/dist/group.d.ts +86 -0
- package/dist/group.js +247 -0
- package/dist/hub.d.ts +300 -0
- package/dist/hub.js +343 -0
- package/dist/index.d.ts +5 -13
- package/dist/index.js +82 -313
- package/dist/json.d.ts +8 -0
- package/dist/json.js +15 -0
- package/dist/later.d.ts +7 -7
- package/dist/later.js +8 -7
- package/dist/limits.d.ts +65 -0
- package/dist/limits.js +109 -0
- package/dist/notices.d.ts +356 -0
- package/dist/notices.js +678 -0
- package/dist/question/answer.d.ts +35 -0
- package/dist/question/answer.js +214 -0
- package/dist/question/index.d.ts +6 -0
- package/dist/question/index.js +7 -0
- package/dist/question/lifecycle.d.ts +43 -0
- package/dist/question/lifecycle.js +91 -0
- package/dist/question/pure.d.ts +48 -0
- package/dist/question/pure.js +74 -0
- package/dist/question/relay.d.ts +5 -0
- package/dist/question/relay.js +269 -0
- package/dist/question/runtime.d.ts +20 -0
- package/dist/question/runtime.js +74 -0
- package/dist/question/shared.d.ts +24 -0
- package/dist/question/shared.js +24 -0
- package/dist/relay.d.ts +32 -36
- package/dist/relay.js +6 -56
- package/dist/report.d.ts +48 -0
- package/dist/report.js +88 -0
- package/dist/roster.d.ts +48 -8
- package/dist/roster.js +140 -17
- package/dist/skill.d.ts +13 -0
- package/dist/skill.js +9 -0
- package/dist/stop.d.ts +41 -0
- package/dist/stop.js +120 -0
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +8 -5
- package/dist/tools.d.ts +94 -0
- package/dist/tools.js +297 -0
- package/dist/tree.d.ts +102 -0
- package/dist/tree.js +58 -0
- package/dist/version.d.ts +4 -10
- package/dist/version.js +4 -10
- package/dist/watch.d.ts +70 -42
- package/dist/watch.js +346 -51
- package/dist/webhook.d.ts +11 -28
- package/dist/webhook.js +46 -146
- package/package.json +16 -5
- package/dist/question.d.ts +0 -158
- package/dist/question.js +0 -768
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import { type CleanupPorts } from "./cleanup.js";
|
|
4
|
+
import { type StopPorts } from "./stop.js";
|
|
5
|
+
import { type CourierPorts } from "./courier.js";
|
|
6
|
+
import { type LaterPorts } from "./later.js";
|
|
7
|
+
import { type QuestionPorts } from "./question/index.js";
|
|
8
|
+
import { type AnswerPorts, type Waiting } from "./relay.js";
|
|
9
|
+
import { type WebhookPorts } from "./webhook.js";
|
|
10
|
+
/**
|
|
11
|
+
* The courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
12
|
+
* model-facing like the texts in `notices.ts`, and snapshotted with them in `test/notices.test.ts`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SpawnInput: Schema.Struct<{
|
|
15
|
+
readonly task: Schema.String;
|
|
16
|
+
readonly title: Schema.optional<Schema.String>;
|
|
17
|
+
readonly agent: Schema.optional<Schema.String>;
|
|
18
|
+
readonly isolate: Schema.optional<Schema.Boolean>;
|
|
19
|
+
readonly group: Schema.optional<Schema.String>;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const SendInput: Schema.Struct<{
|
|
22
|
+
readonly sessionID: Schema.String;
|
|
23
|
+
readonly message: Schema.String;
|
|
24
|
+
readonly status: Schema.optional<Schema.Literals<readonly ["done", "partial", "blocked", "failed"]>>;
|
|
25
|
+
readonly artifacts: Schema.optional<Schema.Struct<{
|
|
26
|
+
readonly branch: Schema.optional<Schema.String>;
|
|
27
|
+
readonly commits: Schema.optional<Schema.$Array<Schema.String>>;
|
|
28
|
+
readonly files: Schema.optional<Schema.$Array<Schema.String>>;
|
|
29
|
+
readonly checks: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
30
|
+
readonly command: Schema.String;
|
|
31
|
+
readonly result: Schema.String;
|
|
32
|
+
}>>>;
|
|
33
|
+
}>>;
|
|
34
|
+
readonly queue: Schema.optional<Schema.Boolean>;
|
|
35
|
+
}>;
|
|
36
|
+
export declare const StatusInput: Schema.Struct<{
|
|
37
|
+
readonly sessionID: Schema.String;
|
|
38
|
+
}>;
|
|
39
|
+
export declare const ChildrenInput: Schema.Struct<{
|
|
40
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
41
|
+
}>;
|
|
42
|
+
export declare const TreeInput: Schema.Struct<{
|
|
43
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
44
|
+
}>;
|
|
45
|
+
export declare const StopInput: Schema.Struct<{
|
|
46
|
+
readonly sessionID: Schema.String;
|
|
47
|
+
readonly cleanup: Schema.optional<Schema.Boolean>;
|
|
48
|
+
}>;
|
|
49
|
+
export declare const CleanupInput: Schema.Struct<{
|
|
50
|
+
readonly sessionID: Schema.String;
|
|
51
|
+
readonly force: Schema.optional<Schema.Boolean>;
|
|
52
|
+
}>;
|
|
53
|
+
export declare const LaterInput: Schema.Struct<{
|
|
54
|
+
readonly message: Schema.String;
|
|
55
|
+
readonly delayMinutes: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String]>>;
|
|
56
|
+
readonly at: Schema.optional<Schema.String>;
|
|
57
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
58
|
+
}>;
|
|
59
|
+
export declare const AnswerInput: Schema.Struct<{
|
|
60
|
+
readonly sessionID: Schema.String;
|
|
61
|
+
readonly requestID: Schema.String;
|
|
62
|
+
readonly reply: Schema.optional<Schema.Literals<readonly ["once", "always", "reject"]>>;
|
|
63
|
+
readonly message: Schema.optional<Schema.String>;
|
|
64
|
+
readonly answers: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>>>;
|
|
65
|
+
}>;
|
|
66
|
+
export declare const CancelInput: Schema.Struct<{
|
|
67
|
+
readonly id: Schema.String;
|
|
68
|
+
}>;
|
|
69
|
+
export declare const SubscribeInput: Schema.Struct<{
|
|
70
|
+
readonly topic: Schema.String;
|
|
71
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
72
|
+
}>;
|
|
73
|
+
export declare const UnsubscribeInput: Schema.Struct<{
|
|
74
|
+
readonly topic: Schema.optional<Schema.String>;
|
|
75
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
76
|
+
}>;
|
|
77
|
+
/** The editor OpenCode hands a tool transform. */
|
|
78
|
+
export type ToolEditor = Parameters<Parameters<Plugin.Context["tool"]["transform"]>[0]>[0];
|
|
79
|
+
/** What the tools run on: one plugin instance's ports. */
|
|
80
|
+
export interface ToolPorts {
|
|
81
|
+
readonly courier: CourierPorts;
|
|
82
|
+
readonly cleanup: CleanupPorts;
|
|
83
|
+
readonly stop: StopPorts;
|
|
84
|
+
readonly later: LaterPorts;
|
|
85
|
+
readonly answer: AnswerPorts;
|
|
86
|
+
readonly questions: QuestionPorts;
|
|
87
|
+
readonly hooks: WebhookPorts;
|
|
88
|
+
/** The permission requests sessions were told about and have not answered. */
|
|
89
|
+
readonly waiting: Waiting;
|
|
90
|
+
/** Whether a webhook receiver runs in this server. */
|
|
91
|
+
readonly receiving: () => Promise<boolean>;
|
|
92
|
+
}
|
|
93
|
+
/** Adds the courier tools, each a direct tool rather than a code-mode one. */
|
|
94
|
+
export declare function addTools(tools: ToolEditor, ports: ToolPorts): void;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { cleanup } from "./cleanup.js";
|
|
3
|
+
import { stop } from "./stop.js";
|
|
4
|
+
import { tree } from "./tree.js";
|
|
5
|
+
import { describeFailure, listChildren, send, spawn, status } from "./courier.js";
|
|
6
|
+
import { cancel, schedule } from "./later.js";
|
|
7
|
+
import { answerText, cancelText, childrenText, cleanupText, laterText, REPLIES, sendText, spawnText, STATUSES, statusText, stopText, subscribeText, treeText, unsubscribeText, } from "./notices.js";
|
|
8
|
+
import { answerQuestion, isQuestion } from "./question/index.js";
|
|
9
|
+
import { answer } from "./relay.js";
|
|
10
|
+
import { subscribe, unsubscribe } from "./webhook.js";
|
|
11
|
+
/**
|
|
12
|
+
* The courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
13
|
+
* model-facing like the texts in `notices.ts`, and snapshotted with them in `test/notices.test.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export const SpawnInput = Schema.Struct({
|
|
16
|
+
task: Schema.String.annotate({ description: "What the new session should do. It is told who started it and how to report back." }),
|
|
17
|
+
title: Schema.optional(Schema.String.annotate({ description: "Session title; defaults to the task's first line." })),
|
|
18
|
+
agent: Schema.optional(Schema.String.annotate({ description: "Agent to run the session with; defaults to the default agent." })),
|
|
19
|
+
isolate: Schema.optional(Schema.Boolean.annotate({ description: "Run the session in its own git worktree so parallel sessions don't share files. From a session that is itself " +
|
|
20
|
+
"isolated, the worktree starts from your current commit, not your uncommitted changes: commit first. The session " +
|
|
21
|
+
"commits on a branch named courier/<its session ID> and names it in its report; merge that branch yourself.",
|
|
22
|
+
})),
|
|
23
|
+
group: Schema.optional(Schema.String.annotate({
|
|
24
|
+
description: "A join group: start the sessions of one batch with the same name, and their reports are held and delivered " +
|
|
25
|
+
"to you in one message once every one of them has reported and your turn has ended, instead of one turn per " +
|
|
26
|
+
"report. A blocked report, a question, a permission request or a failed turn still reaches you at once. Letters, " +
|
|
27
|
+
"digits, dots, dashes and underscores, up to 60; the name is free again once the group has been delivered.",
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
export const SendInput = Schema.Struct({
|
|
31
|
+
sessionID: Schema.String.annotate({ description: "The session to deliver to." }),
|
|
32
|
+
message: Schema.String.annotate({ description: "The message text." }),
|
|
33
|
+
status: Schema.optional(Schema.Literals(STATUSES).annotate({
|
|
34
|
+
description: "For a report to the session that started you: done, partial (some of the task is left), blocked (you need a " +
|
|
35
|
+
"decision from it; say what) or failed. A message without a status is progress, not your report.",
|
|
36
|
+
})),
|
|
37
|
+
artifacts: Schema.optional(Schema.Struct({
|
|
38
|
+
branch: Schema.optional(Schema.String.annotate({ description: "The branch your work is on." })),
|
|
39
|
+
commits: Schema.optional(Schema.Array(Schema.String).annotate({ description: "The commits you made, as short hashes or subjects." })),
|
|
40
|
+
files: Schema.optional(Schema.Array(Schema.String).annotate({ description: "The files you changed." })),
|
|
41
|
+
checks: Schema.optional(Schema.Array(Schema.Struct({
|
|
42
|
+
command: Schema.String.annotate({ description: "A command you ran, such as a test suite or a linter." }),
|
|
43
|
+
result: Schema.String.annotate({ description: "What it gave, in a few words." }),
|
|
44
|
+
})).annotate({ description: "The checks you ran and their results." })),
|
|
45
|
+
}).annotate({ description: "With a report: what you produced, listed in the message after your text in a fixed layout." })),
|
|
46
|
+
queue: Schema.optional(Schema.Boolean.annotate({ description: "Wait until the target's current turn ends instead of steering it now." })),
|
|
47
|
+
});
|
|
48
|
+
export const StatusInput = Schema.Struct({
|
|
49
|
+
sessionID: Schema.String.annotate({ description: "The session to look at." }),
|
|
50
|
+
});
|
|
51
|
+
export const ChildrenInput = Schema.Struct({
|
|
52
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The session whose children to list; defaults to this one." })),
|
|
53
|
+
});
|
|
54
|
+
export const TreeInput = Schema.Struct({
|
|
55
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The session whose subtree to list; defaults to this one." })),
|
|
56
|
+
});
|
|
57
|
+
export const StopInput = Schema.Struct({
|
|
58
|
+
sessionID: Schema.String.annotate({ description: "The session to stop, with every session under it. It must have been started, directly or through others, from this one." }),
|
|
59
|
+
cleanup: Schema.optional(Schema.Boolean.annotate({
|
|
60
|
+
description: "Also remove the worktrees of the stopped sessions that ran in one of their own, as courier_cleanup does: " +
|
|
61
|
+
"one with uncommitted changes or commits on no branch is kept and listed.",
|
|
62
|
+
})),
|
|
63
|
+
});
|
|
64
|
+
export const CleanupInput = Schema.Struct({
|
|
65
|
+
sessionID: Schema.String.annotate({ description: "The session you started to drop, with its worktree if it is isolated." }),
|
|
66
|
+
force: Schema.optional(Schema.Boolean.annotate({
|
|
67
|
+
description: "Remove it even with uncommitted changes or commits on no branch; that work is lost.",
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
export const LaterInput = Schema.Struct({
|
|
71
|
+
message: Schema.String.annotate({ description: "The message to deliver." }),
|
|
72
|
+
delayMinutes: Schema.optional(
|
|
73
|
+
// Some models send the number as a string ("3", "3.0") whatever the schema says, and resend it
|
|
74
|
+
// unchanged after the error, so a string is accepted and schedule reads it as a number.
|
|
75
|
+
Schema.Union([Schema.Number, Schema.String]).annotate({
|
|
76
|
+
description: "Deliver this many minutes from now. Give this or at.",
|
|
77
|
+
})),
|
|
78
|
+
at: Schema.optional(Schema.String.annotate({ description: "Deliver at this ISO 8601 time. Give this or delayMinutes." })),
|
|
79
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to deliver to; defaults to this one." })),
|
|
80
|
+
});
|
|
81
|
+
export const AnswerInput = Schema.Struct({
|
|
82
|
+
sessionID: Schema.String.annotate({ description: "The session you started that is waiting, as its notice names it." }),
|
|
83
|
+
requestID: Schema.String.annotate({ description: "The request id from the notice." }),
|
|
84
|
+
reply: Schema.optional(Schema.Literals(REPLIES).annotate({
|
|
85
|
+
description: "For a permission request: the choice the person made, once, always (only when the notice offers it) or reject.",
|
|
86
|
+
})),
|
|
87
|
+
message: Schema.optional(Schema.String.annotate({ description: "For a permission request, with reject: the person's reason, passed on with the refusal." })),
|
|
88
|
+
answers: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])).annotate({
|
|
89
|
+
description: "For a question: one entry per question, in order, each the label the person chose or the text they gave, " +
|
|
90
|
+
"or a list of labels where the question allows several.",
|
|
91
|
+
})),
|
|
92
|
+
});
|
|
93
|
+
export const CancelInput = Schema.Struct({
|
|
94
|
+
id: Schema.String.annotate({ description: "The id courier_later returned." }),
|
|
95
|
+
});
|
|
96
|
+
export const SubscribeInput = Schema.Struct({
|
|
97
|
+
topic: Schema.String.annotate({
|
|
98
|
+
description: "owner/repo for every event of a GitHub repository, owner/repo#<number> for one pull request or issue " +
|
|
99
|
+
"(reviews, comments, completed CI runs), or a plain name for deliveries posted to /hook/<name>.",
|
|
100
|
+
}),
|
|
101
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to subscribe; defaults to this one." })),
|
|
102
|
+
});
|
|
103
|
+
export const UnsubscribeInput = Schema.Struct({
|
|
104
|
+
topic: Schema.optional(Schema.String.annotate({ description: "The topic to drop; all of this session's when omitted." })),
|
|
105
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to unsubscribe; defaults to this one." })),
|
|
106
|
+
});
|
|
107
|
+
const rethrow = (tool) => (error) => {
|
|
108
|
+
throw describeFailure(tool, error);
|
|
109
|
+
};
|
|
110
|
+
// One tool answers both: a question's id is the plugin's own, a permission request's OpenCode's.
|
|
111
|
+
async function answerRequest(ports, callerID, input) {
|
|
112
|
+
const { sessionID, requestID } = input;
|
|
113
|
+
if (isQuestion(requestID)) {
|
|
114
|
+
if (input.reply !== undefined || input.message !== undefined)
|
|
115
|
+
throw new Error(`${requestID} is a question: pass the person's answers in answers, not reply or message.`);
|
|
116
|
+
if (input.answers === undefined)
|
|
117
|
+
throw new Error(`${requestID} is a question: answers is required, one entry per question.`);
|
|
118
|
+
return answerQuestion(ports.questions, callerID, { sessionID, requestID, answers: input.answers });
|
|
119
|
+
}
|
|
120
|
+
if (input.answers !== undefined)
|
|
121
|
+
throw new Error(`${requestID} is a permission request: pass the person's choice in reply (once, always or reject), not answers.`);
|
|
122
|
+
if (input.reply === undefined)
|
|
123
|
+
throw new Error(`${requestID} is a permission request: reply is required, once, always or reject.`);
|
|
124
|
+
return answer(ports.answer, ports.waiting, callerID, {
|
|
125
|
+
sessionID,
|
|
126
|
+
requestID,
|
|
127
|
+
reply: input.reply,
|
|
128
|
+
...(input.message !== undefined ? { message: input.message } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Adds the courier tools, each a direct tool rather than a code-mode one. */
|
|
132
|
+
export function addTools(tools, ports) {
|
|
133
|
+
tools.add({
|
|
134
|
+
name: "courier_spawn",
|
|
135
|
+
options: { codemode: false },
|
|
136
|
+
description: "Start a new OpenCode session on a task and return immediately. It runs on your model. The session reports " +
|
|
137
|
+
"back with courier_send, which wakes this session, and you are told if its turn fails or ends without a report " +
|
|
138
|
+
"instead, it waits for a permission or it asks a question. Give a batch of parallel sessions one group name to " +
|
|
139
|
+
"be woken once, with all their reports, instead of once per report. DO NOT poll it or call courier_status in a " +
|
|
140
|
+
"loop: once you have started the sessions you need, end your turn by replying without calling more tools; each " +
|
|
141
|
+
"report, or each group's reports, starts a new turn in which you carry on. For long tasks, also courier_later a " +
|
|
142
|
+
"check-in for yourself in case it never reports, and courier_cancel it when it does.",
|
|
143
|
+
input: SpawnInput,
|
|
144
|
+
execute: async (input, context) => {
|
|
145
|
+
const child = await spawn(ports.courier, context.sessionID, input).catch(rethrow("courier_spawn"));
|
|
146
|
+
return { content: spawnText(child), metadata: child };
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
tools.add({
|
|
150
|
+
name: "courier_send",
|
|
151
|
+
options: { codemode: false },
|
|
152
|
+
description: "Deliver a message to another OpenCode session. If that session is idle, OpenCode starts a new turn for it. " +
|
|
153
|
+
"Use it to report back to the session that started you, with a status (done, partial, blocked or failed) and " +
|
|
154
|
+
"the artifacts you produced, or to steer a session you started. The status is carried as an attribute of the " +
|
|
155
|
+
"message; without one, a message to the session that started you is progress, not your report. If you were " +
|
|
156
|
+
"started in a group, a report with status done, partial or failed is held and delivered with the group's other " +
|
|
157
|
+
"reports; the result says so, and nothing more is needed from you.",
|
|
158
|
+
input: SendInput,
|
|
159
|
+
execute: async (input, context) => {
|
|
160
|
+
const delivered = await send(ports.courier, context.sessionID, input).catch(rethrow("courier_send"));
|
|
161
|
+
return { content: sendText(input.sessionID, delivered), metadata: delivered };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
tools.add({
|
|
165
|
+
name: "courier_status",
|
|
166
|
+
options: { codemode: false },
|
|
167
|
+
description: "Look once at a session's state and its last reply, e.g. on a scheduled check-in. Not for waiting: " +
|
|
168
|
+
"sessions you started report back on their own.",
|
|
169
|
+
input: StatusInput,
|
|
170
|
+
execute: async (input) => {
|
|
171
|
+
const result = await status(ports.courier, input).catch(rethrow("courier_status"));
|
|
172
|
+
return { content: statusText(result), metadata: result };
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
tools.add({
|
|
176
|
+
name: "courier_children",
|
|
177
|
+
options: { codemode: false },
|
|
178
|
+
description: "List the sessions this one started with courier_spawn, with each one's state and last reply, and for one " +
|
|
179
|
+
"started in a group, its group and whether its report is held, still out, or released, e.g. after a " +
|
|
180
|
+
"compaction or restart. Like courier_status, for a one-off look, not for waiting.",
|
|
181
|
+
input: ChildrenInput,
|
|
182
|
+
execute: async (input, context) => {
|
|
183
|
+
const listed = await listChildren(ports.courier, input.sessionID || context.sessionID).catch(rethrow("courier_children"));
|
|
184
|
+
return { content: childrenText(listed), metadata: { children: listed } };
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
tools.add({
|
|
188
|
+
name: "courier_tree",
|
|
189
|
+
options: { codemode: false },
|
|
190
|
+
description: "List the whole tree of sessions under this one (or under sessionID): every session started with courier_spawn " +
|
|
191
|
+
"from it, directly or through others, each with its depth, what courier_children shows, and whether it still " +
|
|
192
|
+
"owes a report, sent progress, or ended without one. For a one-off look, not for waiting; long trees are cut at maxTotal sessions.",
|
|
193
|
+
input: TreeInput,
|
|
194
|
+
execute: async (input, context) => {
|
|
195
|
+
const result = await tree(ports.courier, input.sessionID || context.sessionID).catch(rethrow("courier_tree"));
|
|
196
|
+
return { content: treeText(result), metadata: result };
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
tools.add({
|
|
200
|
+
name: "courier_stop",
|
|
201
|
+
options: { codemode: false },
|
|
202
|
+
description: "Stop a session you started, directly or through others, and every session under it: each running turn is " +
|
|
203
|
+
"interrupted, deepest first, and the messages scheduled for them with courier_later are cancelled. Use it when a " +
|
|
204
|
+
"subtree went wrong or is no longer needed. They do not report back, and you are not told of what you stopped; a " +
|
|
205
|
+
"session between you and the target is. A stopped session can be woken again with courier_send. With " +
|
|
206
|
+
"cleanup: true their worktrees are removed too, unless they hold work that is on no branch.",
|
|
207
|
+
input: StopInput,
|
|
208
|
+
execute: async (input, context) => {
|
|
209
|
+
const result = await stop(ports.stop, context.sessionID, input).catch(rethrow("courier_stop"));
|
|
210
|
+
return { content: stopText(result), metadata: result };
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
tools.add({
|
|
214
|
+
name: "courier_cleanup",
|
|
215
|
+
options: { codemode: false },
|
|
216
|
+
description: "Remove the git worktree of a session you started with isolate: true, once you have what you need from it, " +
|
|
217
|
+
"and drop it from courier_children. A worktree with uncommitted changes or commits on no branch is kept and " +
|
|
218
|
+
"the result lists them; commit or branch what you want, or pass force: true to discard it. A session without " +
|
|
219
|
+
"isolate has no worktree and is only dropped from courier_children. Either way, one in a join group that has not " +
|
|
220
|
+
"reported is dropped from it, so the group's other reports reach you without it; use this for a member you no " +
|
|
221
|
+
"longer need a report from.",
|
|
222
|
+
input: CleanupInput,
|
|
223
|
+
execute: async (input, context) => {
|
|
224
|
+
const result = await cleanup(ports.cleanup, context.sessionID, input).catch(rethrow("courier_cleanup"));
|
|
225
|
+
return { content: cleanupText(result), metadata: result };
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
tools.add({
|
|
229
|
+
name: "courier_answer",
|
|
230
|
+
options: { codemode: false },
|
|
231
|
+
description: "Pass on the answer to a permission request or a question that a session you started with courier_spawn (or " +
|
|
232
|
+
"one started from it) waits on, after a notice from it named the request. The answer is the person's, not yours: " +
|
|
233
|
+
"first ask the person you are working with, offering the choices the notice lists, then call this with what " +
|
|
234
|
+
"they chose: reply for a permission request, answers for a question. Never choose for them. The session " +
|
|
235
|
+
"carries on once it has the answer.",
|
|
236
|
+
input: AnswerInput,
|
|
237
|
+
execute: async (input, context) => {
|
|
238
|
+
const result = await answerRequest(ports, context.sessionID, input).catch(rethrow("courier_answer"));
|
|
239
|
+
return { content: answerText(result), metadata: result };
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
tools.add({
|
|
243
|
+
name: "courier_later",
|
|
244
|
+
options: { codemode: false },
|
|
245
|
+
description: "Schedule a message for a session (this one by default), delivered when due and waking it if idle. " +
|
|
246
|
+
"Use it as a safety net when you start sessions: schedule one check-in, end your turn by replying without " +
|
|
247
|
+
"calling more tools, and cancel it with courier_cancel if the child reports first. Survives server restarts; " +
|
|
248
|
+
"may arrive up to ~15 seconds late.",
|
|
249
|
+
input: LaterInput,
|
|
250
|
+
execute: async (input, context) => {
|
|
251
|
+
const entry = await schedule(ports.later, context.sessionID, input).catch(rethrow("courier_later"));
|
|
252
|
+
const fireAt = new Date(entry.fireAt).toISOString();
|
|
253
|
+
return {
|
|
254
|
+
content: laterText(entry, fireAt, entry.sessionID === context.sessionID),
|
|
255
|
+
metadata: { id: entry.id, fireAt, sessionID: entry.sessionID },
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
tools.add({
|
|
260
|
+
name: "courier_cancel",
|
|
261
|
+
options: { codemode: false },
|
|
262
|
+
description: "Cancel a message scheduled with courier_later, e.g. because the child it was waiting for reported.",
|
|
263
|
+
input: CancelInput,
|
|
264
|
+
execute: async (input) => {
|
|
265
|
+
const cancelled = await cancel(ports.later, input.id).catch(rethrow("courier_cancel"));
|
|
266
|
+
return { content: cancelText(input.id, cancelled), metadata: { id: input.id, cancelled } };
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
tools.add({
|
|
270
|
+
name: "courier_subscribe",
|
|
271
|
+
options: { codemode: false },
|
|
272
|
+
description: "Wake a session (this one by default) when a webhook arrives for a topic: a GitHub repository, one of its " +
|
|
273
|
+
"pull requests or issues (reviews, comments, completed CI runs), or a named generic hook. Each matching " +
|
|
274
|
+
"delivery arrives as a message, queued behind any running turn. End your turn and wait; do not poll.",
|
|
275
|
+
input: SubscribeInput,
|
|
276
|
+
execute: async (input, context) => {
|
|
277
|
+
const sessionID = input.sessionID ?? context.sessionID;
|
|
278
|
+
const subscription = await subscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_subscribe"));
|
|
279
|
+
const receiving = await ports.receiving();
|
|
280
|
+
return {
|
|
281
|
+
content: subscribeText(sessionID, subscription.topic, receiving),
|
|
282
|
+
metadata: { sessionID, topic: subscription.topic, receiver: receiving },
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
tools.add({
|
|
287
|
+
name: "courier_unsubscribe",
|
|
288
|
+
options: { codemode: false },
|
|
289
|
+
description: "Stop webhook deliveries for a topic, or all of a session's topics, e.g. once its pull request is merged.",
|
|
290
|
+
input: UnsubscribeInput,
|
|
291
|
+
execute: async (input, context) => {
|
|
292
|
+
const sessionID = input.sessionID ?? context.sessionID;
|
|
293
|
+
const dropped = await unsubscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_unsubscribe"));
|
|
294
|
+
return { content: unsubscribeText(sessionID, dropped), metadata: { sessionID, dropped } };
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type CourierPorts } from "./courier.js";
|
|
2
|
+
import { type RosterEntry, type RosterStorage } from "./roster.js";
|
|
3
|
+
/** A session below the one a subtree is read from, with how many levels below it is: a child is at depth 1. */
|
|
4
|
+
export interface Below {
|
|
5
|
+
readonly entry: RosterEntry;
|
|
6
|
+
readonly depth: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
|
|
10
|
+
* (`truncated` if more), in the order a tree is drawn.
|
|
11
|
+
*/
|
|
12
|
+
export declare function subtree(storage: RosterStorage, rootID: string, max?: number): Promise<{
|
|
13
|
+
nodes: Below[];
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
|
|
18
|
+
* its parent and its report state.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tree(ports: CourierPorts, rootID: string): Promise<{
|
|
21
|
+
truncated?: number | undefined;
|
|
22
|
+
sessionID: string;
|
|
23
|
+
sessions: ({
|
|
24
|
+
report?: {
|
|
25
|
+
status?: "done" | "partial" | "blocked" | "failed" | undefined;
|
|
26
|
+
owes: boolean;
|
|
27
|
+
progressed: boolean;
|
|
28
|
+
ended: boolean;
|
|
29
|
+
} | undefined;
|
|
30
|
+
parentID: string;
|
|
31
|
+
depth: number;
|
|
32
|
+
group: {
|
|
33
|
+
name: string;
|
|
34
|
+
report: string;
|
|
35
|
+
};
|
|
36
|
+
directory: string;
|
|
37
|
+
isolated: boolean;
|
|
38
|
+
created: number;
|
|
39
|
+
sessionID?: string | undefined;
|
|
40
|
+
title?: string | undefined;
|
|
41
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
42
|
+
updated?: number | undefined;
|
|
43
|
+
idle?: number | undefined;
|
|
44
|
+
lastText?: string | undefined;
|
|
45
|
+
pending?: readonly import("./courier.js").Pending[] | undefined;
|
|
46
|
+
} | {
|
|
47
|
+
report?: {
|
|
48
|
+
status?: "done" | "partial" | "blocked" | "failed" | undefined;
|
|
49
|
+
owes: boolean;
|
|
50
|
+
progressed: boolean;
|
|
51
|
+
ended: boolean;
|
|
52
|
+
} | undefined;
|
|
53
|
+
parentID: string;
|
|
54
|
+
depth: number;
|
|
55
|
+
group?: undefined;
|
|
56
|
+
directory: string;
|
|
57
|
+
isolated: boolean;
|
|
58
|
+
created: number;
|
|
59
|
+
sessionID?: string | undefined;
|
|
60
|
+
title?: string | undefined;
|
|
61
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
62
|
+
updated?: number | undefined;
|
|
63
|
+
idle?: number | undefined;
|
|
64
|
+
lastText?: string | undefined;
|
|
65
|
+
pending?: readonly import("./courier.js").Pending[] | undefined;
|
|
66
|
+
} | {
|
|
67
|
+
report?: {
|
|
68
|
+
status?: "done" | "partial" | "blocked" | "failed" | undefined;
|
|
69
|
+
owes: boolean;
|
|
70
|
+
progressed: boolean;
|
|
71
|
+
ended: boolean;
|
|
72
|
+
} | undefined;
|
|
73
|
+
parentID: string;
|
|
74
|
+
depth: number;
|
|
75
|
+
error: string;
|
|
76
|
+
group: {
|
|
77
|
+
name: string;
|
|
78
|
+
report: string;
|
|
79
|
+
};
|
|
80
|
+
directory: string;
|
|
81
|
+
isolated: boolean;
|
|
82
|
+
created: number;
|
|
83
|
+
sessionID: string;
|
|
84
|
+
title: string;
|
|
85
|
+
} | {
|
|
86
|
+
report?: {
|
|
87
|
+
status?: "done" | "partial" | "blocked" | "failed" | undefined;
|
|
88
|
+
owes: boolean;
|
|
89
|
+
progressed: boolean;
|
|
90
|
+
ended: boolean;
|
|
91
|
+
} | undefined;
|
|
92
|
+
parentID: string;
|
|
93
|
+
depth: number;
|
|
94
|
+
error: string;
|
|
95
|
+
group?: undefined;
|
|
96
|
+
directory: string;
|
|
97
|
+
isolated: boolean;
|
|
98
|
+
created: number;
|
|
99
|
+
sessionID: string;
|
|
100
|
+
title: string;
|
|
101
|
+
})[];
|
|
102
|
+
}>;
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { described } from "./courier.js";
|
|
2
|
+
import { lastStatus, reportOf } from "./report.js";
|
|
3
|
+
import { children } from "./roster.js";
|
|
4
|
+
/**
|
|
5
|
+
* The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
|
|
6
|
+
* (`truncated` if more), in the order a tree is drawn.
|
|
7
|
+
*/
|
|
8
|
+
export async function subtree(storage, rootID, max = Number.POSITIVE_INFINITY) {
|
|
9
|
+
const byParent = new Map();
|
|
10
|
+
const seen = new Set([rootID]);
|
|
11
|
+
let truncated = false;
|
|
12
|
+
const expand = async (level, depth) => {
|
|
13
|
+
if (!level.length || truncated)
|
|
14
|
+
return;
|
|
15
|
+
const found = await Promise.all(level.map((parentID) => children(storage, parentID)));
|
|
16
|
+
const next = [];
|
|
17
|
+
for (const [index, entries] of found.entries()) {
|
|
18
|
+
const fresh = entries.filter((entry) => !seen.has(entry.sessionID));
|
|
19
|
+
const kept = fresh.slice(0, Math.max(0, max - (seen.size - 1)));
|
|
20
|
+
truncated ||= kept.length < fresh.length;
|
|
21
|
+
for (const entry of kept)
|
|
22
|
+
seen.add(entry.sessionID);
|
|
23
|
+
if (kept.length)
|
|
24
|
+
byParent.set(level[index], kept.map((entry) => ({ entry, depth })));
|
|
25
|
+
next.push(...kept.map((entry) => entry.sessionID));
|
|
26
|
+
}
|
|
27
|
+
return expand(next, depth + 1);
|
|
28
|
+
};
|
|
29
|
+
await expand([rootID], 1);
|
|
30
|
+
return { nodes: drawn(byParent, rootID), truncated };
|
|
31
|
+
}
|
|
32
|
+
/** The nodes in the order a tree is drawn: each followed by what it started. */
|
|
33
|
+
function drawn(byParent, parentID) {
|
|
34
|
+
return (byParent.get(parentID) ?? []).flatMap((node) => [node, ...drawn(byParent, node.entry.sessionID)]);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
|
|
38
|
+
* its parent and its report state.
|
|
39
|
+
*/
|
|
40
|
+
export async function tree(ports, rootID) {
|
|
41
|
+
const { nodes, truncated } = await subtree(ports.storage, rootID, ports.limits.maxTotal);
|
|
42
|
+
const listed = await Promise.all(nodes.map(async ({ entry, depth }) => {
|
|
43
|
+
const [info, report, last] = await Promise.all([
|
|
44
|
+
described(ports, entry),
|
|
45
|
+
reportOf(ports.storage, entry.sessionID).catch(() => undefined),
|
|
46
|
+
lastStatus(ports.storage, entry.sessionID).catch(() => undefined),
|
|
47
|
+
]);
|
|
48
|
+
return {
|
|
49
|
+
...info,
|
|
50
|
+
parentID: entry.parentID,
|
|
51
|
+
depth,
|
|
52
|
+
...(report
|
|
53
|
+
? { report: { owes: report.owes, progressed: report.progressed, ended: report.ended, ...(last ? { status: last } : {}) } }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
return { sessionID: rootID, sessions: listed, ...(truncated ? { truncated: ports.limits.maxTotal } : {}) };
|
|
58
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -5,18 +5,12 @@ export interface BuiltVersions {
|
|
|
5
5
|
readonly opencode: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Reads
|
|
9
|
-
*
|
|
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.
|
|
8
|
+
* Reads from the package's own package.json, next to `dist/` as to `src/`: `version` and the exact
|
|
9
|
+
* `@opencode/plugin` devDependency version, which the peer repeats. Throws if either is missing.
|
|
14
10
|
*/
|
|
15
11
|
export declare function builtVersions(): BuiltVersions;
|
|
16
12
|
/**
|
|
17
|
-
* The line for
|
|
18
|
-
*
|
|
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.
|
|
13
|
+
* The server-log line for an OpenCode other than the plugin was built against, undefined when it is;
|
|
14
|
+
* logged once per load, never in a tool result. A context without a version is still told the pin.
|
|
21
15
|
*/
|
|
22
16
|
export declare function versionNotice(built: BuiltVersions, app: Partial<Pick<App, "version" | "channel">> | undefined): string | undefined;
|
package/dist/version.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
const README_TABLE = "https://github.com/ivopogace/opencode-courier#supported-opencode-version";
|
|
3
3
|
/**
|
|
4
|
-
* Reads
|
|
5
|
-
*
|
|
6
|
-
* `@opencode/plugin` version under devDependencies, the one installed for the build and both test
|
|
7
|
-
* suites; the peer dependency names the same exact version, as the copy of the plugin API that
|
|
8
|
-
* `opencode plugin add` installs next to the plugin. Throws when the file cannot be read or does
|
|
9
|
-
* not hold both.
|
|
4
|
+
* Reads from the package's own package.json, next to `dist/` as to `src/`: `version` and the exact
|
|
5
|
+
* `@opencode/plugin` devDependency version, which the peer repeats. Throws if either is missing.
|
|
10
6
|
*/
|
|
11
7
|
export function builtVersions() {
|
|
12
8
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -19,10 +15,8 @@ export function builtVersions() {
|
|
|
19
15
|
return { plugin, opencode };
|
|
20
16
|
}
|
|
21
17
|
/**
|
|
22
|
-
* The line for
|
|
23
|
-
*
|
|
24
|
-
* whose plugin context does not report a version, as one from before the pinned API or after a
|
|
25
|
-
* change to it may not, is the case the line is for, so it still names the pin then.
|
|
18
|
+
* The server-log line for an OpenCode other than the plugin was built against, undefined when it is;
|
|
19
|
+
* logged once per load, never in a tool result. A context without a version is still told the pin.
|
|
26
20
|
*/
|
|
27
21
|
export function versionNotice(built, app) {
|
|
28
22
|
const version = typeof app?.version === "string" ? app.version : undefined;
|