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/cleanup.js
CHANGED
|
@@ -1,46 +1,30 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
state.changes.length ? `${count(state.changes.length, "uncommitted change")} (${preview(state.changes)})` : "",
|
|
8
|
-
state.commits.length
|
|
9
|
-
? `${count(state.commits.length, "commit")} on no branch, tag or remote (${preview(state.commits)})`
|
|
10
|
-
: "",
|
|
11
|
-
].filter(Boolean);
|
|
12
|
-
return reasons.length ? reasons.join(" and ") : undefined;
|
|
13
|
-
}
|
|
14
|
-
function count(n, noun) {
|
|
15
|
-
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
16
|
-
}
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { markLeft, membershipOf } from "./group.js";
|
|
5
|
+
import { keepReason } from "./notices.js";
|
|
6
|
+
import { remove, rosterKey } from "./roster.js";
|
|
17
7
|
/** At most this many changes and commits are returned; the reason still counts them all. */
|
|
18
8
|
export const MAX_LISTED = 50;
|
|
19
|
-
function preview(items) {
|
|
20
|
-
return items.length > 5 ? `${items.slice(0, 5).join(", ")}, ...` : items.join(", ");
|
|
21
|
-
}
|
|
22
9
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* result says what is in it.
|
|
10
|
+
* Forgets a child the parent started, removing its worktree if it is isolated. A worktree with uncommitted changes
|
|
11
|
+
* or commits found nowhere else is kept unless `force` is set; the result says so.
|
|
26
12
|
*/
|
|
27
13
|
export async function cleanup(ports, parentID, input) {
|
|
28
14
|
const entry = (await ports.storage.get(rosterKey(parentID, input.sessionID)));
|
|
29
15
|
if (!entry)
|
|
30
16
|
throw new Error(`${input.sessionID} is not on the courier_children list of ${parentID}.`);
|
|
31
|
-
if (!entry.isolated)
|
|
32
|
-
throw new Error(`${input.sessionID} ran in ${entry.directory}, not in a worktree of its own; there is nothing to remove.`);
|
|
33
17
|
const { directory } = entry;
|
|
34
|
-
const forget = () => ports
|
|
18
|
+
const forget = () => forgetChild(ports, entry);
|
|
19
|
+
if (!entry.isolated)
|
|
20
|
+
return { sessionID: input.sessionID, directory, outcome: "forgotten", ...(await forget()) };
|
|
35
21
|
// With force the state only decides whether there is anything left to remove, so a worktree git
|
|
36
22
|
// can no longer read is still removed.
|
|
37
23
|
const state = await ports
|
|
38
24
|
.inspect(directory, entry.base)
|
|
39
25
|
.catch((error) => (input.force ? { changes: [], commits: [] } : Promise.reject(error)));
|
|
40
|
-
if (!state)
|
|
41
|
-
await forget();
|
|
42
|
-
return { sessionID: input.sessionID, directory, outcome: "gone" };
|
|
43
|
-
}
|
|
26
|
+
if (!state)
|
|
27
|
+
return { sessionID: input.sessionID, directory, outcome: "gone", ...(await forget()) };
|
|
44
28
|
const reason = keepReason(state);
|
|
45
29
|
if (reason && !input.force)
|
|
46
30
|
return {
|
|
@@ -56,16 +40,63 @@ export async function cleanup(ports, parentID, input) {
|
|
|
56
40
|
directory,
|
|
57
41
|
force: input.force === true,
|
|
58
42
|
});
|
|
59
|
-
await forget();
|
|
60
|
-
|
|
43
|
+
return { sessionID: input.sessionID, directory, outcome: "removed", ...(await forget()) };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Drops a member still out from its group as `dropped`, so the group is released without it, then the roster entry
|
|
47
|
+
* and report state. A held report stays with its group. Returns the group it was dropped from.
|
|
48
|
+
*/
|
|
49
|
+
async function forgetChild(ports, entry) {
|
|
50
|
+
const member = entry.group ? await membershipOf(ports.storage, entry.parentID, entry.group, entry.sessionID) : undefined;
|
|
51
|
+
if (member?.out && (await markLeft(ports.storage, member, "dropped", ports.now())))
|
|
52
|
+
ports.nudge();
|
|
53
|
+
await remove(ports.storage, entry.parentID, entry.sessionID);
|
|
54
|
+
return member?.out ? { group: member.group } : {};
|
|
55
|
+
}
|
|
56
|
+
/** Names the git to run, by absolute path, overriding the places `GIT_LOCATIONS` lists. */
|
|
57
|
+
export const GIT_ENV = "OPENCODE_COURIER_GIT";
|
|
58
|
+
/**
|
|
59
|
+
* Where git is looked for, in order: its usual install locations, never `PATH`, so a writable
|
|
60
|
+
* directory early in `PATH` cannot put another program in its place.
|
|
61
|
+
*/
|
|
62
|
+
export const GIT_LOCATIONS = {
|
|
63
|
+
posix: [
|
|
64
|
+
"/usr/bin/git",
|
|
65
|
+
"/usr/local/bin/git",
|
|
66
|
+
"/opt/homebrew/bin/git",
|
|
67
|
+
// NixOS, and nix-darwin.
|
|
68
|
+
"/run/current-system/sw/bin/git",
|
|
69
|
+
],
|
|
70
|
+
win32: [String.raw `C:\Program Files\Git\cmd\git.exe`, String.raw `C:\Program Files (x86)\Git\cmd\git.exe`],
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The git to run: `OPENCODE_COURIER_GIT` when it is an absolute path, otherwise the first of
|
|
74
|
+
* `GIT_LOCATIONS` that exists. Throws when there is none.
|
|
75
|
+
*/
|
|
76
|
+
export function findGit(env = process.env, platform = process.platform, exists = existsSync) {
|
|
77
|
+
const named = env[GIT_ENV]?.trim();
|
|
78
|
+
if (named) {
|
|
79
|
+
if (!isAbsolute(named))
|
|
80
|
+
throw new Error(`${GIT_ENV} must be an absolute path, not ${named}.`);
|
|
81
|
+
return named;
|
|
82
|
+
}
|
|
83
|
+
const locations = GIT_LOCATIONS[platform === "win32" ? "win32" : "posix"];
|
|
84
|
+
const found = locations.find((path) => exists(path));
|
|
85
|
+
if (!found)
|
|
86
|
+
throw new Error(`git is in none of ${locations.join(", ")}; set ${GIT_ENV} to its absolute path.`);
|
|
87
|
+
return found;
|
|
61
88
|
}
|
|
62
89
|
function git(directory, args) {
|
|
63
|
-
return new Promise((resolve, reject) => execFile(
|
|
90
|
+
return new Promise((resolve, reject) => execFile(findGit(), ["-C", directory, ...args], { maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => error ? reject(new Error(`git ${args[0]} in ${directory}: ${stderr.trim() || error.message}`)) : resolve(stdout)));
|
|
64
91
|
}
|
|
65
92
|
/** The commit a worktree is on, or undefined when git cannot tell. */
|
|
66
93
|
export function headOf(directory) {
|
|
67
94
|
return git(directory, ["rev-parse", "HEAD"]).then((out) => out.trim() || undefined, () => undefined);
|
|
68
95
|
}
|
|
96
|
+
/** Whether a worktree has uncommitted changes, untracked files included; undefined when git cannot tell. */
|
|
97
|
+
export function dirtyOf(directory) {
|
|
98
|
+
return git(directory, ["status", "--porcelain"]).then((out) => out.trim().length > 0, () => undefined);
|
|
99
|
+
}
|
|
69
100
|
/**
|
|
70
101
|
* Reads a worktree's state with git; undefined when the directory is gone. Commits reachable from
|
|
71
102
|
* `base`, the commit the worktree was made from, are not its own work and are not listed.
|
package/dist/courier.d.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin";
|
|
2
|
-
import type
|
|
3
|
-
import { type
|
|
2
|
+
import { type Limits, type SpawnGate } from "./limits.js";
|
|
3
|
+
import { type Artifacts, type Prompt, type Sent, type Status } from "./notices.js";
|
|
4
|
+
import { type RosterEntry, type RosterStorage } from "./roster.js";
|
|
4
5
|
type Context = Plugin.Context;
|
|
5
6
|
/** The slice of the plugin context the courier tools use; tests pass a fake. */
|
|
6
7
|
export interface CourierPorts {
|
|
7
|
-
readonly session: Pick<Context["session"], "create" | "prompt" | "synthetic" | "get" | "context">;
|
|
8
|
+
readonly session: Pick<Context["session"], "create" | "prompt" | "synthetic" | "get" | "context" | "interrupt">;
|
|
8
9
|
readonly agent: Pick<Context["agent"], "get">;
|
|
9
10
|
readonly worktree: Pick<Context["worktree"], "create" | "remove">;
|
|
10
11
|
/** The project of the plugin's location; an isolated child's worktree is made in it, and removed from it. */
|
|
11
12
|
readonly projectID: string;
|
|
12
13
|
/** The commit a directory's checkout is on, or undefined; recorded as an isolated child's base. */
|
|
13
14
|
readonly head: (directory: string) => Promise<string | undefined>;
|
|
15
|
+
/** Whether a directory's checkout has uncommitted changes, or undefined when git cannot tell. */
|
|
16
|
+
readonly dirty: (directory: string) => Promise<boolean | undefined>;
|
|
14
17
|
readonly storage: RosterStorage;
|
|
15
18
|
readonly directory: string;
|
|
16
19
|
readonly now: () => number;
|
|
17
20
|
/** The permission requests and questions a session waits on, wherever they are pending. */
|
|
18
21
|
readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
|
|
22
|
+
readonly limits: Limits;
|
|
23
|
+
readonly gate: SpawnGate;
|
|
24
|
+
/** The depths the `context` hook remembers; a parent's is dropped when it starts a child. */
|
|
25
|
+
readonly roles: Map<string, number | null>;
|
|
26
|
+
/** Has the scheduler deliver what is due now rather than at its next tick: a group a report has just completed. */
|
|
27
|
+
readonly nudge: () => void;
|
|
28
|
+
readonly log: (message: string) => void;
|
|
19
29
|
}
|
|
20
30
|
/** A request a session waits on until someone answers it: a permission request, or a question it asked. */
|
|
21
31
|
export type Pending = {
|
|
@@ -36,10 +46,13 @@ export interface SpawnInput {
|
|
|
36
46
|
readonly title?: string;
|
|
37
47
|
readonly agent?: string;
|
|
38
48
|
readonly isolate?: boolean;
|
|
49
|
+
readonly group?: string;
|
|
39
50
|
}
|
|
40
51
|
export interface SendInput {
|
|
41
52
|
readonly sessionID: string;
|
|
42
53
|
readonly message: string;
|
|
54
|
+
readonly status?: Status;
|
|
55
|
+
readonly artifacts?: Artifacts;
|
|
43
56
|
readonly queue?: boolean;
|
|
44
57
|
}
|
|
45
58
|
export interface StatusInput {
|
|
@@ -48,52 +61,140 @@ export interface StatusInput {
|
|
|
48
61
|
export interface ChildrenInput {
|
|
49
62
|
readonly sessionID?: string;
|
|
50
63
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/** Creates a child session, hands it the task and returns at once; the child reports back with courier_send. */
|
|
64
|
+
/**
|
|
65
|
+
* Creates a child session, hands it the task and returns at once; the child reports back with
|
|
66
|
+
* courier_send. Refused past a limit, whether or not the `context` hook hid the tool.
|
|
67
|
+
*/
|
|
56
68
|
export declare function spawn(ports: CourierPorts, parentID: string, input: SpawnInput): Promise<{
|
|
69
|
+
uncommitted?: boolean | undefined;
|
|
70
|
+
fromParent?: boolean | undefined;
|
|
71
|
+
groupError?: string | undefined;
|
|
57
72
|
rosterError?: string | undefined;
|
|
73
|
+
group?: string | undefined;
|
|
58
74
|
sessionID: string;
|
|
59
75
|
directory: string;
|
|
60
76
|
}>;
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Drops a message into another session's inbox; OpenCode wakes that session if it is idle. From a spawned session
|
|
79
|
+
* to the one that started it, it is its report or its progress (`noteReport`); a group member's final report is held (`hold`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function send(ports: CourierPorts, from: string, input: SendInput): Promise<Sent>;
|
|
65
82
|
/** A one-off look at a session, for check-ins; not meant to be called in a loop. */
|
|
66
83
|
export declare function status(ports: CourierPorts, input: StatusInput): Promise<Partial<{
|
|
67
84
|
sessionID: string;
|
|
68
85
|
title: string | undefined;
|
|
69
86
|
parentID: string | undefined;
|
|
70
|
-
outcome: "failed" | "
|
|
87
|
+
outcome: "failed" | "interrupted" | "succeeded" | undefined;
|
|
71
88
|
updated: number;
|
|
72
89
|
idle: number | undefined;
|
|
73
90
|
lastText: string | undefined;
|
|
74
91
|
pending: readonly Pending[] | undefined;
|
|
75
92
|
}>>;
|
|
76
|
-
/** The
|
|
93
|
+
/** The text of a session's last reply, from its context's messages, or undefined when it has none. */
|
|
94
|
+
export declare function lastReply(messages: Awaited<ReturnType<Context["session"]["context"]>>): string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* The sessions a parent started, each with what courier_status reports, or the error it gave; a child in a
|
|
97
|
+
* group with the group's name and its standing there: `held`, `out`, how it left, `released` or `unknown`.
|
|
98
|
+
*/
|
|
77
99
|
export declare function listChildren(ports: CourierPorts, parentID: string): Promise<({
|
|
100
|
+
group: {
|
|
101
|
+
name: string;
|
|
102
|
+
report: string;
|
|
103
|
+
};
|
|
104
|
+
directory: string;
|
|
105
|
+
isolated: boolean;
|
|
106
|
+
created: number;
|
|
107
|
+
sessionID?: string | undefined;
|
|
108
|
+
title?: string | undefined;
|
|
109
|
+
parentID?: string | undefined;
|
|
110
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
111
|
+
updated?: number | undefined;
|
|
112
|
+
idle?: number | undefined;
|
|
113
|
+
lastText?: string | undefined;
|
|
114
|
+
pending?: readonly Pending[] | undefined;
|
|
115
|
+
} | {
|
|
116
|
+
group?: undefined;
|
|
78
117
|
directory: string;
|
|
79
118
|
isolated: boolean;
|
|
80
119
|
created: number;
|
|
81
120
|
sessionID?: string | undefined;
|
|
82
121
|
title?: string | undefined;
|
|
83
122
|
parentID?: string | undefined;
|
|
84
|
-
outcome?: "failed" | "
|
|
123
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
85
124
|
updated?: number | undefined;
|
|
86
125
|
idle?: number | undefined;
|
|
87
126
|
lastText?: string | undefined;
|
|
88
127
|
pending?: readonly Pending[] | undefined;
|
|
89
128
|
} | {
|
|
90
129
|
error: string;
|
|
130
|
+
group: {
|
|
131
|
+
name: string;
|
|
132
|
+
report: string;
|
|
133
|
+
};
|
|
134
|
+
directory: string;
|
|
135
|
+
isolated: boolean;
|
|
136
|
+
created: number;
|
|
137
|
+
sessionID: string;
|
|
138
|
+
title: string;
|
|
139
|
+
} | {
|
|
140
|
+
error: string;
|
|
141
|
+
group?: undefined;
|
|
91
142
|
directory: string;
|
|
92
143
|
isolated: boolean;
|
|
93
144
|
created: number;
|
|
94
145
|
sessionID: string;
|
|
95
146
|
title: string;
|
|
96
147
|
})[]>;
|
|
148
|
+
/** One roster entry as courier_children shows it: what courier_status reports with its directory and group standing, or the error it gave. */
|
|
149
|
+
export declare function described(ports: CourierPorts, entry: RosterEntry): Promise<{
|
|
150
|
+
group: {
|
|
151
|
+
name: string;
|
|
152
|
+
report: string;
|
|
153
|
+
};
|
|
154
|
+
directory: string;
|
|
155
|
+
isolated: boolean;
|
|
156
|
+
created: number;
|
|
157
|
+
sessionID?: string | undefined;
|
|
158
|
+
title?: string | undefined;
|
|
159
|
+
parentID?: string | undefined;
|
|
160
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
161
|
+
updated?: number | undefined;
|
|
162
|
+
idle?: number | undefined;
|
|
163
|
+
lastText?: string | undefined;
|
|
164
|
+
pending?: readonly Pending[] | undefined;
|
|
165
|
+
} | {
|
|
166
|
+
group?: undefined;
|
|
167
|
+
directory: string;
|
|
168
|
+
isolated: boolean;
|
|
169
|
+
created: number;
|
|
170
|
+
sessionID?: string | undefined;
|
|
171
|
+
title?: string | undefined;
|
|
172
|
+
parentID?: string | undefined;
|
|
173
|
+
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
174
|
+
updated?: number | undefined;
|
|
175
|
+
idle?: number | undefined;
|
|
176
|
+
lastText?: string | undefined;
|
|
177
|
+
pending?: readonly Pending[] | undefined;
|
|
178
|
+
} | {
|
|
179
|
+
error: string;
|
|
180
|
+
group: {
|
|
181
|
+
name: string;
|
|
182
|
+
report: string;
|
|
183
|
+
};
|
|
184
|
+
directory: string;
|
|
185
|
+
isolated: boolean;
|
|
186
|
+
created: number;
|
|
187
|
+
sessionID: string;
|
|
188
|
+
title: string;
|
|
189
|
+
} | {
|
|
190
|
+
error: string;
|
|
191
|
+
group?: undefined;
|
|
192
|
+
directory: string;
|
|
193
|
+
isolated: boolean;
|
|
194
|
+
created: number;
|
|
195
|
+
sessionID: string;
|
|
196
|
+
title: string;
|
|
197
|
+
}>;
|
|
97
198
|
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
98
199
|
export declare function describeFailure(tool: string, error: unknown): Error;
|
|
99
200
|
export {};
|