cueloop 0.1.0-alpha.7 → 0.1.0-alpha.71
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/package.json +28 -23
- package/src/actions-command.ts +49 -0
- package/src/args.ts +21 -10
- package/src/dev-command.ts +85 -0
- package/src/main.ts +330 -91
- package/src/open-target.test.ts +241 -0
- package/src/open-target.ts +128 -0
- package/src/pr.ts +35 -5
- package/src/refine-command.test.ts +158 -0
- package/src/refine-command.ts +262 -0
- package/src/session-commands.ts +464 -0
- package/src/share-command.test.ts +322 -0
- package/src/share-command.ts +145 -0
- package/src/update-command.test.ts +163 -0
- package/src/update-command.ts +177 -0
- package/src/version.ts +5 -0
- package/src/wake-command.test.ts +14 -0
- package/src/wake-command.ts +44 -0
- package/src/session-cmds.ts +0 -107
- package/src/working-tree.ts +0 -27
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cueloop session *` - mirrors the daemon socket API 1:1. One surface,
|
|
3
|
+
* three consumers: agent adapters, the dev loop, and integrations. Output is
|
|
4
|
+
* JSON on stdout; exit code 0 unless the daemon returned an error.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ARTIFACT_TYPES,
|
|
9
|
+
isArtifactType,
|
|
10
|
+
newAnnotationId,
|
|
11
|
+
type Anchor,
|
|
12
|
+
type Annotation,
|
|
13
|
+
} from "@cueloop/schema";
|
|
14
|
+
import * as v from "valibot";
|
|
15
|
+
import { DaemonClient } from "@cueloop/daemon/client";
|
|
16
|
+
import { openHerdrPaneForReview } from "@cueloop/daemon/herdr-pane";
|
|
17
|
+
import { openReview, verdictResponse } from "@cueloop/daemon/review";
|
|
18
|
+
import { loadConfig, quickActionBody, resolveQuickAction } from "@cueloop/client/config";
|
|
19
|
+
import { slashItemsFrom } from "@cueloop/client/slash-palette";
|
|
20
|
+
import { parseArgs, stringFlag } from "./args";
|
|
21
|
+
|
|
22
|
+
async function readStdin(): Promise<string> {
|
|
23
|
+
return await new Response(Bun.stdin.stream()).text();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function out(value: Parameters<typeof JSON.stringify>[0]): void {
|
|
27
|
+
console.log(JSON.stringify(value, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ReviewNotesSchema = v.array(v.object({ path: v.string(), body: v.string() }));
|
|
31
|
+
const SessionStatusSchema = v.picklist(["pending", "resolved"]);
|
|
32
|
+
const VerdictKindSchema = v.picklist(["comment", "approve", "request_changes"]);
|
|
33
|
+
|
|
34
|
+
type SessionFlags = Record<string, string | boolean>;
|
|
35
|
+
type SessionContext = { client: DaemonClient; positional: string[]; flags: SessionFlags };
|
|
36
|
+
type SessionVerbHandler = (context: SessionContext) => Promise<number>;
|
|
37
|
+
|
|
38
|
+
async function sessionCreate({ client, flags }: SessionContext): Promise<number> {
|
|
39
|
+
const type = stringFlag(flags, "type") ?? "plan";
|
|
40
|
+
|
|
41
|
+
if (!isArtifactType(type)) {
|
|
42
|
+
console.error(`unknown artifact type "${type}" - one of: ${ARTIFACT_TYPES.join(", ")}`);
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
const contentFile = stringFlag(flags, "content-file");
|
|
46
|
+
const content = contentFile ? await Bun.file(contentFile).text() : await readStdin();
|
|
47
|
+
// per-file agent notes for diff sessions: a JSON array of { path, body }
|
|
48
|
+
const notesFile = stringFlag(flags, "notes-file");
|
|
49
|
+
const notes = notesFile
|
|
50
|
+
? v.parse(ReviewNotesSchema, JSON.parse(await Bun.file(notesFile).text()))
|
|
51
|
+
: undefined;
|
|
52
|
+
const review = await openReview(client, {
|
|
53
|
+
type,
|
|
54
|
+
content,
|
|
55
|
+
cwd: stringFlag(flags, "cwd"),
|
|
56
|
+
agent: stringFlag(flags, "agent"),
|
|
57
|
+
agentSessionId: stringFlag(flags, "agent-session-id"),
|
|
58
|
+
planPath: stringFlag(flags, "plan-path"),
|
|
59
|
+
prototypePath: stringFlag(flags, "prototype-path"),
|
|
60
|
+
title: stringFlag(flags, "title"),
|
|
61
|
+
notes,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// herdr auto-open: render the review in a tab (no-op outside herdr).
|
|
65
|
+
await openHerdrPaneForReview(review.session, client);
|
|
66
|
+
out(review.session);
|
|
67
|
+
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function sessionGetCommand({ client, positional }: SessionContext): Promise<number> {
|
|
72
|
+
out(await client.sessionGet(required(positional[1], "session id")));
|
|
73
|
+
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function sessionListCommand({ client, flags }: SessionContext): Promise<number> {
|
|
78
|
+
const rawStatus = stringFlag(flags, "status");
|
|
79
|
+
const status = rawStatus ? v.parse(SessionStatusSchema, rawStatus) : undefined;
|
|
80
|
+
|
|
81
|
+
out(await client.sessionList(status ? { status } : undefined));
|
|
82
|
+
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function sessionWaitCommand({ client, positional, flags }: SessionContext): Promise<number> {
|
|
87
|
+
const id = required(positional[1], "session id");
|
|
88
|
+
const timeoutMs = Number(stringFlag(flags, "timeout-ms") ?? "60000");
|
|
89
|
+
const session = await client.sessionWait(id, timeoutMs);
|
|
90
|
+
|
|
91
|
+
if (session === null) {
|
|
92
|
+
out({ status: "pending" });
|
|
93
|
+
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const { allow, feedback } = verdictResponse(session);
|
|
97
|
+
|
|
98
|
+
out({ status: "resolved", allow, verdict: session.verdict!.kind, feedback });
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The anchor an annotate call means: a reply borrows its root comment's
|
|
105
|
+
* anchor, a prototype comment names an element by selector, and anything
|
|
106
|
+
* else quotes the passage.
|
|
107
|
+
*/
|
|
108
|
+
async function annotateAnchor(
|
|
109
|
+
flags: Record<string, string | boolean>,
|
|
110
|
+
client: DaemonClient,
|
|
111
|
+
id: string,
|
|
112
|
+
): Promise<{ anchor: Anchor; replyTo?: string }> {
|
|
113
|
+
const replyTo = stringFlag(flags, "reply-to");
|
|
114
|
+
|
|
115
|
+
if (replyTo !== undefined) {
|
|
116
|
+
const session = await client.sessionGet(id);
|
|
117
|
+
const root = session.annotations.find((annotation) => annotation.id === replyTo);
|
|
118
|
+
|
|
119
|
+
if (!root) throw new Error(`no comment ${replyTo} to reply to`);
|
|
120
|
+
|
|
121
|
+
// a reply to a reply still hangs off the discussion's root comment
|
|
122
|
+
return { anchor: root.anchor, replyTo: root.replyTo ?? root.id };
|
|
123
|
+
}
|
|
124
|
+
const selector = stringFlag(flags, "selector");
|
|
125
|
+
const quote =
|
|
126
|
+
selector === undefined
|
|
127
|
+
? required(stringFlag(flags, "quote"), "--quote")
|
|
128
|
+
: (stringFlag(flags, "quote") ?? "");
|
|
129
|
+
const anchor: Anchor = {
|
|
130
|
+
quote,
|
|
131
|
+
prefix: stringFlag(flags, "prefix") ?? "",
|
|
132
|
+
suffix: stringFlag(flags, "suffix") ?? "",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (selector !== undefined) anchor.selector = selector;
|
|
136
|
+
|
|
137
|
+
return { anchor };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sessionAnnotateCommand({
|
|
141
|
+
client,
|
|
142
|
+
positional,
|
|
143
|
+
flags,
|
|
144
|
+
}: SessionContext): Promise<number> {
|
|
145
|
+
const id = required(positional[1], "session id");
|
|
146
|
+
const author = stringFlag(flags, "author");
|
|
147
|
+
const body = await annotateBody(flags, client, id);
|
|
148
|
+
const { anchor, replyTo } = await annotateAnchor(flags, client, id);
|
|
149
|
+
const annotation: Omit<Annotation, "createdAt"> = {
|
|
150
|
+
id: stringFlag(flags, "annotation-id") ?? newAnnotationId(),
|
|
151
|
+
kind: stringFlag(flags, "kind") ?? "comment",
|
|
152
|
+
anchor,
|
|
153
|
+
body,
|
|
154
|
+
author,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (replyTo !== undefined) annotation.replyTo = replyTo;
|
|
158
|
+
out(await client.sessionAnnotate(id, annotation, stringFlag(flags, "author-name")));
|
|
159
|
+
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Remove a comment; a non-owner connection removes only the comments of the author it is bound to. */
|
|
164
|
+
async function sessionRemoveCommand({ client, positional }: SessionContext): Promise<number> {
|
|
165
|
+
const id = required(positional[1], "session id");
|
|
166
|
+
const annotationId = required(positional[2], "annotation id");
|
|
167
|
+
|
|
168
|
+
out(await client.sessionRemoveAnnotation(id, annotationId));
|
|
169
|
+
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A 0-based block or line index as the command line carries it. */
|
|
174
|
+
const BlockIndexSchema = v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(0));
|
|
175
|
+
|
|
176
|
+
/** The reject decisions of a diff review as `--rejections` carries them. */
|
|
177
|
+
const RejectionsSchema = v.array(
|
|
178
|
+
v.object({
|
|
179
|
+
path: v.pipe(v.string(), v.minLength(1)),
|
|
180
|
+
hunkIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
181
|
+
changeIndex: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
/** Cut the given block of the working copy (a 0-based index into its blocks). */
|
|
186
|
+
async function sessionCutCommand({ client, positional }: SessionContext): Promise<number> {
|
|
187
|
+
const id = required(positional[1], "session id");
|
|
188
|
+
const blockIndex = v.parse(BlockIndexSchema, required(positional[2], "block index"));
|
|
189
|
+
|
|
190
|
+
out(await client.sessionCutBlock(id, blockIndex));
|
|
191
|
+
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Re-insert a cut block of the submitted revision, before --line (default: the end). */
|
|
196
|
+
async function sessionRestoreCommand({
|
|
197
|
+
client,
|
|
198
|
+
positional,
|
|
199
|
+
flags,
|
|
200
|
+
}: SessionContext): Promise<number> {
|
|
201
|
+
const id = required(positional[1], "session id");
|
|
202
|
+
const baseBlockIndex = v.parse(BlockIndexSchema, required(positional[2], "block index"));
|
|
203
|
+
const line = stringFlag(flags, "line");
|
|
204
|
+
|
|
205
|
+
out(
|
|
206
|
+
await client.sessionRestoreBlock(
|
|
207
|
+
id,
|
|
208
|
+
baseBlockIndex,
|
|
209
|
+
line === undefined ? undefined : v.parse(BlockIndexSchema, line),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Replace a diff review's reject decisions with the JSON list given. */
|
|
217
|
+
async function sessionCurateCommand({
|
|
218
|
+
client,
|
|
219
|
+
positional,
|
|
220
|
+
flags,
|
|
221
|
+
}: SessionContext): Promise<number> {
|
|
222
|
+
const id = required(positional[1], "session id");
|
|
223
|
+
const rejections = v.parse(
|
|
224
|
+
RejectionsSchema,
|
|
225
|
+
JSON.parse(required(stringFlag(flags, "rejections"), "--rejections")),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
out(await client.sessionCurate(id, rejections));
|
|
229
|
+
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Move a branch's tip back to an entry on its path; --summary records what was left behind, --branch stands on that branch first. */
|
|
234
|
+
async function sessionNavigateCommand({
|
|
235
|
+
client,
|
|
236
|
+
positional,
|
|
237
|
+
flags,
|
|
238
|
+
}: SessionContext): Promise<number> {
|
|
239
|
+
const id = required(positional[1], "session id");
|
|
240
|
+
const entryId = required(positional[2], "entry id");
|
|
241
|
+
|
|
242
|
+
out(
|
|
243
|
+
await client.sessionNavigate(
|
|
244
|
+
id,
|
|
245
|
+
entryId,
|
|
246
|
+
stringFlag(flags, "summary"),
|
|
247
|
+
stringFlag(flags, "branch"),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Start a branch at the current tip and switch to it. */
|
|
255
|
+
async function sessionBranchCommand({ client, positional }: SessionContext): Promise<number> {
|
|
256
|
+
const id = required(positional[1], "session id");
|
|
257
|
+
|
|
258
|
+
out(await client.sessionBranch(id, required(positional[2], "branch name")));
|
|
259
|
+
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Show another branch's path. */
|
|
264
|
+
async function sessionSwitchCommand({ client, positional }: SessionContext): Promise<number> {
|
|
265
|
+
const id = required(positional[1], "session id");
|
|
266
|
+
|
|
267
|
+
out(await client.sessionSwitch(id, required(positional[2], "branch name")));
|
|
268
|
+
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Name the current tip as a checkpoint. */
|
|
273
|
+
async function sessionLabelCommand({ client, positional }: SessionContext): Promise<number> {
|
|
274
|
+
const id = required(positional[1], "session id");
|
|
275
|
+
|
|
276
|
+
out(await client.sessionLabel(id, required(positional[2], "label")));
|
|
277
|
+
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Copy the current path into a new session and print the fork. */
|
|
282
|
+
async function sessionForkCommand({ client, positional }: SessionContext): Promise<number> {
|
|
283
|
+
out(await client.sessionFork(required(positional[1], "session id")));
|
|
284
|
+
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Mark files of a diff review as viewed - the guided walk's state, from a script. */
|
|
289
|
+
async function sessionSetViewedCommand({ client, positional }: SessionContext): Promise<number> {
|
|
290
|
+
const id = required(positional[1], "session id");
|
|
291
|
+
|
|
292
|
+
out(await client.sessionSetViewed(id, positional.slice(2)));
|
|
293
|
+
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Register the display name of a participant: how a collaborator or an agent names itself. */
|
|
298
|
+
async function sessionNameSelfCommand({
|
|
299
|
+
client,
|
|
300
|
+
positional,
|
|
301
|
+
flags,
|
|
302
|
+
}: SessionContext): Promise<number> {
|
|
303
|
+
const id = required(positional[1], "session id");
|
|
304
|
+
const name = required(positional[2], "name");
|
|
305
|
+
const author = required(stringFlag(flags, "author"), "--author");
|
|
306
|
+
|
|
307
|
+
out(await client.sessionSetParticipantName(id, author, name));
|
|
308
|
+
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Follow a session live: one JSON line per event, with the entry the change
|
|
314
|
+
* appended, until the process ends. `--once` prints the first event and exits.
|
|
315
|
+
* The subscription is in place before the session is read, so nothing that
|
|
316
|
+
* lands after the read can slip between the two.
|
|
317
|
+
*/
|
|
318
|
+
async function sessionEventsCommand({
|
|
319
|
+
client,
|
|
320
|
+
positional,
|
|
321
|
+
flags,
|
|
322
|
+
}: SessionContext): Promise<number> {
|
|
323
|
+
const id = required(positional[1], "session id");
|
|
324
|
+
const once = flags.once === true;
|
|
325
|
+
const stream = new Promise<number>((resolve) => {
|
|
326
|
+
client.onEvent((event) => {
|
|
327
|
+
if (event.sessionId !== id) return;
|
|
328
|
+
out(event);
|
|
329
|
+
if (once) resolve(0);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await client.subscribe();
|
|
334
|
+
await client.sessionGet(id);
|
|
335
|
+
|
|
336
|
+
return stream;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function sessionResolveCommand({
|
|
340
|
+
client,
|
|
341
|
+
positional,
|
|
342
|
+
flags,
|
|
343
|
+
}: SessionContext): Promise<number> {
|
|
344
|
+
const id = required(positional[1], "session id");
|
|
345
|
+
const kind = v.parse(VerdictKindSchema, required(stringFlag(flags, "verdict"), "--verdict"));
|
|
346
|
+
// expand /name quick-action references in TUI-authored comments, so a CLI resolve sends the agent
|
|
347
|
+
// the action bodies too, not the bare references
|
|
348
|
+
const session = await client.sessionGet(id);
|
|
349
|
+
const actions = loadConfig({ repoRoot: session.workspace.repoRoot }).actions;
|
|
350
|
+
const actionBodies = Object.fromEntries(
|
|
351
|
+
slashItemsFrom(actions).map((item) => [item.name, item.body]),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
out(await client.sessionResolve(id, kind, stringFlag(flags, "summary") ?? "", actionBodies));
|
|
355
|
+
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function sessionSubmitRevisionCommand({
|
|
360
|
+
client,
|
|
361
|
+
positional,
|
|
362
|
+
flags,
|
|
363
|
+
}: SessionContext): Promise<number> {
|
|
364
|
+
const id = required(positional[1], "session id");
|
|
365
|
+
const contentFile = stringFlag(flags, "content-file");
|
|
366
|
+
const content = contentFile ? await Bun.file(contentFile).text() : await readStdin();
|
|
367
|
+
// --addressed a_1,a_2 - the annotation ids this revision acted on
|
|
368
|
+
const addressed = (stringFlag(flags, "addressed") ?? "")
|
|
369
|
+
.split(",")
|
|
370
|
+
.map((annotationId) => annotationId.trim())
|
|
371
|
+
.filter(Boolean);
|
|
372
|
+
|
|
373
|
+
out(await client.sessionSubmitRevision(id, content, addressed));
|
|
374
|
+
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
interface SessionVerbHandlers {
|
|
379
|
+
[primitive: string]: SessionVerbHandler;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sessionVerbHandlers: SessionVerbHandlers = {
|
|
383
|
+
create: sessionCreate,
|
|
384
|
+
get: sessionGetCommand,
|
|
385
|
+
list: sessionListCommand,
|
|
386
|
+
wait: sessionWaitCommand,
|
|
387
|
+
annotate: sessionAnnotateCommand,
|
|
388
|
+
remove: sessionRemoveCommand,
|
|
389
|
+
cut: sessionCutCommand,
|
|
390
|
+
restore: sessionRestoreCommand,
|
|
391
|
+
curate: sessionCurateCommand,
|
|
392
|
+
"set-viewed": sessionSetViewedCommand,
|
|
393
|
+
navigate: sessionNavigateCommand,
|
|
394
|
+
branch: sessionBranchCommand,
|
|
395
|
+
switch: sessionSwitchCommand,
|
|
396
|
+
label: sessionLabelCommand,
|
|
397
|
+
fork: sessionForkCommand,
|
|
398
|
+
"name-self": sessionNameSelfCommand,
|
|
399
|
+
events: sessionEventsCommand,
|
|
400
|
+
resolve: sessionResolveCommand,
|
|
401
|
+
"submit-revision": sessionSubmitRevisionCommand,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export async function sessionCommand(argv: string[]): Promise<number> {
|
|
405
|
+
const { positional, flags } = parseArgs(argv);
|
|
406
|
+
const primitive = positional[0];
|
|
407
|
+
// --role agent (or collaborator) caps this connection to read + annotate; a
|
|
408
|
+
// review-side agent passes it so the daemon rejects any escalation attempt.
|
|
409
|
+
// and --author binds the identity its comments, removals, and name carry
|
|
410
|
+
const role = stringFlag(flags, "role") === "agent" ? "agent" : undefined;
|
|
411
|
+
const client = await DaemonClient.connect({
|
|
412
|
+
autostart: true,
|
|
413
|
+
role,
|
|
414
|
+
author: role === undefined ? undefined : stringFlag(flags, "author"),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const handler = primitive !== undefined ? sessionVerbHandlers[primitive] : undefined;
|
|
419
|
+
|
|
420
|
+
if (handler === undefined) {
|
|
421
|
+
console.error(
|
|
422
|
+
"usage: cueloop session <create|get|list|wait|annotate|remove|cut|restore|curate|set-viewed|navigate|branch|switch|label|fork|name-self|events|resolve|submit-revision> [flags]",
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return 2;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return await handler({ client, positional, flags });
|
|
429
|
+
} finally {
|
|
430
|
+
client.close();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function required<T>(value: T | undefined, description: string): T {
|
|
435
|
+
if (value === undefined) throw new Error(`missing ${description}`);
|
|
436
|
+
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The annotation body: an explicit `--body`, else the `--action <index|name>`
|
|
442
|
+
* quick-action expanded through the shared vocabulary. The vocabulary is loaded
|
|
443
|
+
* from the reviewed session's own repo, not the caller's cwd, so an agent
|
|
444
|
+
* annotating from elsewhere still sees that session's actions.
|
|
445
|
+
*/
|
|
446
|
+
async function annotateBody(
|
|
447
|
+
flags: Record<string, string | boolean>,
|
|
448
|
+
client: DaemonClient,
|
|
449
|
+
id: string,
|
|
450
|
+
): Promise<string> {
|
|
451
|
+
const explicit = stringFlag(flags, "body");
|
|
452
|
+
|
|
453
|
+
if (explicit !== undefined) return explicit;
|
|
454
|
+
const actionRef = stringFlag(flags, "action");
|
|
455
|
+
|
|
456
|
+
if (actionRef === undefined) return "";
|
|
457
|
+
const session = await client.sessionGet(id);
|
|
458
|
+
const actions = loadConfig({ repoRoot: session.workspace.repoRoot }).actions;
|
|
459
|
+
const action = resolveQuickAction(actions, actionRef);
|
|
460
|
+
|
|
461
|
+
if (!action) throw new Error(`no quick action ${actionRef} - see: cueloop actions list`);
|
|
462
|
+
|
|
463
|
+
return quickActionBody(action);
|
|
464
|
+
}
|