@ryan_nookpi/pi-extension-subagent 0.1.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/cli.ts ADDED
@@ -0,0 +1,549 @@
1
+ /**
2
+ * CLI-style command parser for the subagent tool.
3
+ *
4
+ * LLM-facing interface: { command: "subagent ..." }
5
+ */
6
+
7
+ type ContextMode = "main" | "isolated";
8
+
9
+ type BatchOrChainBlock = {
10
+ agent: string;
11
+ task: string;
12
+ };
13
+
14
+ export type SubagentCliParseResult =
15
+ | { type: "help" }
16
+ | { type: "agents" }
17
+ | { type: "params"; params: Record<string, unknown> }
18
+ | { type: "error"; message: string };
19
+
20
+ export const SUBAGENT_CLI_HELP_TEXT = [
21
+ "Subagent CLI (LLM interface)",
22
+ "",
23
+ 'Always call with: { command: "..." }',
24
+ "",
25
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
26
+ "📌 KEY RULES",
27
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
28
+ "",
29
+ "1. Task separator `--` is REQUIRED for run/continue:",
30
+ " ✓ subagent run worker -- perform the task",
31
+ " ✗ subagent run worker perform the task ← Missing `--`",
32
+ "",
33
+ "2. RUN vs CONTINUE:",
34
+ " • run: Start a NEW subagent execution (must specify agent name)",
35
+ " • continue: Resume an EXISTING run's session by its runId; reuses conversation context",
36
+ " but does NOT automatically sync the latest main context (provide it explicitly if needed)",
37
+ "",
38
+ "3. BATCH vs CHAIN:",
39
+ " • batch: Launch MULTIPLE independent runs in parallel",
40
+ " • chain: Launch MULTIPLE dependent steps sequentially; previous output is passed as reference",
41
+ " Each block must be exactly: --agent <agent> --task <task>",
42
+ "",
43
+ "4. Follow-up policy:",
44
+ " • After a launch, do NOT call `subagent status/detail` to poll right away.",
45
+ " • Stop making subagent calls and wait for the automatic completion/failure follow-up.",
46
+ " • Use `status/detail` only when the USER explicitly asks (or for one-off manual inspection).",
47
+ "",
48
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
49
+ "COMMANDS",
50
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
51
+ "",
52
+ " Info & Listing:",
53
+ " subagent help",
54
+ " subagent agents",
55
+ " subagent runs",
56
+ " subagent status <runId>",
57
+ " subagent detail <runId>",
58
+ "",
59
+ " Execution:",
60
+ " subagent run <agent> [--main|--isolated] -- <task>",
61
+ " subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>",
62
+ " subagent batch [--main|--isolated] --agent <agent> --task <task> --agent <agent> --task <task> ...",
63
+ " subagent chain [--main|--isolated] --agent <agent> --task <task> --agent <agent> --task <task> ...",
64
+ "",
65
+ " Cleanup:",
66
+ " subagent abort <runId|runId,runId|all>",
67
+ " subagent remove <runId|runId,runId|all>",
68
+ "",
69
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
70
+ "EXAMPLES",
71
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
72
+ "",
73
+ " New run:",
74
+ " subagent run worker -- improve login performance",
75
+ "",
76
+ " Continue existing run (runId 22):",
77
+ " subagent continue 22 -- finish the previous work and commit it",
78
+ "",
79
+ " Parallel batch:",
80
+ ' subagent batch --main --agent worker --task "implement feature A" --agent reviewer --task "review code B"',
81
+ "",
82
+ " Sequential chain:",
83
+ ' subagent chain --main --agent worker --task "implement the login API" --agent reviewer --task "review the previous result"',
84
+ "",
85
+ " Manual status & cleanup (occasional checks):",
86
+ " subagent runs",
87
+ " subagent status 22",
88
+ " subagent detail 22",
89
+ " subagent abort 22",
90
+ " subagent remove all",
91
+ "",
92
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
93
+ "",
94
+ "💡 Tips:",
95
+ " • Runs notify you when done automatically.",
96
+ " • Batch waits for the whole group; chain waits for the whole pipeline.",
97
+ " • After launch, end the turn and wait for follow-up (no status/detail polling loops).",
98
+ " • Use `--main` to share context with the main agent; `--isolated` for a fresh scope.",
99
+ " • When using `continue`, the main context is NOT auto-synced. Include recent changes in the task text.",
100
+ " • Long task? Write context to a temp file and reference it in the task:",
101
+ ' e.g. subagent run worker -- "read /tmp/task-ctx.md and follow the instructions"',
102
+ "",
103
+ ].join("\n");
104
+
105
+ type TokenizeResult = { tokens: string[] } | { error: string };
106
+
107
+ function tokenizeCli(input: string): TokenizeResult {
108
+ const tokens: string[] = [];
109
+ let current = "";
110
+ let quote: '"' | "'" | null = null;
111
+ let escaped = false;
112
+
113
+ for (let i = 0; i < input.length; i++) {
114
+ const ch = input[i];
115
+
116
+ if (escaped) {
117
+ current += ch;
118
+ escaped = false;
119
+ continue;
120
+ }
121
+
122
+ if (ch === "\\") {
123
+ escaped = true;
124
+ continue;
125
+ }
126
+
127
+ if (quote) {
128
+ if (ch === quote) {
129
+ quote = null;
130
+ } else {
131
+ current += ch;
132
+ }
133
+ continue;
134
+ }
135
+
136
+ if (ch === '"' || ch === "'") {
137
+ quote = ch;
138
+ continue;
139
+ }
140
+
141
+ if (/\s/.test(ch)) {
142
+ if (current) {
143
+ tokens.push(current);
144
+ current = "";
145
+ }
146
+ continue;
147
+ }
148
+
149
+ current += ch;
150
+ }
151
+
152
+ if (escaped) {
153
+ current += "\\";
154
+ }
155
+
156
+ if (quote) {
157
+ return { error: "Unclosed quote in command." };
158
+ }
159
+
160
+ if (current) tokens.push(current);
161
+ return { tokens };
162
+ }
163
+
164
+ function parseInteger(raw: string): number | null {
165
+ if (!/^\d+$/.test(raw)) return null;
166
+ const value = Number.parseInt(raw, 10);
167
+ return Number.isInteger(value) ? value : null;
168
+ }
169
+
170
+ function parseRunTarget(
171
+ raw: string,
172
+ knownRunIds: number[] | undefined,
173
+ ): { runId: number } | { runIds: number[] } | { error: string } {
174
+ if (!raw) return { error: "Missing run target." };
175
+
176
+ if (raw.toLowerCase() === "all") {
177
+ const unique = Array.from(new Set((knownRunIds ?? []).filter((id) => Number.isInteger(id))));
178
+ if (unique.length === 0) {
179
+ return { error: "No runs available for target `all`." };
180
+ }
181
+ return { runIds: unique };
182
+ }
183
+
184
+ if (raw.includes(",")) {
185
+ const parts = raw.split(",").map((part) => part.trim());
186
+ const ids = parts.map((part) => parseInteger(part));
187
+ if (ids.some((id) => id === null)) return { error: `Invalid run target: ${raw}` };
188
+ const unique = Array.from(new Set(ids.filter((id): id is number => id !== null)));
189
+ return unique.length === 1 ? { runId: unique[0] } : { runIds: unique };
190
+ }
191
+
192
+ const runId = parseInteger(raw);
193
+ if (runId === null) return { error: `Invalid runId: ${raw}` };
194
+ return { runId };
195
+ }
196
+
197
+ function parseRunLike(
198
+ verb: "run" | "continue",
199
+ args: string[],
200
+ ): { params: Record<string, unknown> } | { error: string } {
201
+ const sepIndex = args.indexOf("--");
202
+ if (sepIndex === -1) {
203
+ const example =
204
+ verb === "run"
205
+ ? "subagent run worker -- perform the task"
206
+ : "subagent continue 22 -- continue with the next step";
207
+ return {
208
+ error: `❌ Missing task separator \`--\`\n\nThe \`--\` is REQUIRED to separate options from task text.\n\n✓ Correct: ${example}\n✗ Wrong: subagent ${verb} ${args.join(" ")}`,
209
+ };
210
+ }
211
+
212
+ const head = args.slice(0, sepIndex);
213
+ const task = args
214
+ .slice(sepIndex + 1)
215
+ .join(" ")
216
+ .trim();
217
+ if (!task)
218
+ return {
219
+ error: `❌ Empty task after \`--\`\n\nProvide a non-empty task description after the separator.\n\n✓ Correct: subagent ${verb} ${head.join(" ")} -- <your task here>`,
220
+ };
221
+
222
+ let runId: number | undefined;
223
+ let agent: string | undefined;
224
+ let contextMode: ContextMode | undefined;
225
+
226
+ for (let i = 0; i < head.length; i++) {
227
+ const token = head[i];
228
+
229
+ if (token === "--main") {
230
+ contextMode = "main";
231
+ continue;
232
+ }
233
+ if (token === "--isolated") {
234
+ contextMode = "isolated";
235
+ continue;
236
+ }
237
+ if (token === "--async" || token === "--sync") {
238
+ return {
239
+ error: `❌ ${token} is no longer supported\n\nSubagent run/continue commands are async-only, so you should omit execution-mode flags entirely. Wait for the automatic follow-up message after launch.\n\n✓ Correct: ${verb === "continue" ? "subagent continue 22 -- <task>" : "subagent run worker -- <task>"}`,
240
+ };
241
+ }
242
+ if (token === "--agent") {
243
+ const value = head[i + 1];
244
+ if (!value)
245
+ return {
246
+ error: `❌ --agent requires a value\n\n✓ Correct: subagent continue 22 --agent worker -- <task>\n✓ Or: subagent continue 22 --agent=worker -- <task>`,
247
+ };
248
+ agent = value;
249
+ i++;
250
+ continue;
251
+ }
252
+ if (token.startsWith("--agent=")) {
253
+ agent = token.slice("--agent=".length);
254
+ continue;
255
+ }
256
+
257
+ if (token.startsWith("--")) {
258
+ return {
259
+ error: `❌ Unknown option: ${token}\n\nValid options: --main, --isolated${verb === "continue" ? ", --agent" : ""}\n\n✓ Example: subagent ${verb} ${token === "--main" ? "" : verb === "continue" ? "22 " : ""}${token} -- <task>`,
260
+ };
261
+ }
262
+
263
+ if (verb === "continue") {
264
+ if (runId === undefined) {
265
+ const parsed = parseInteger(token);
266
+ if (parsed === null)
267
+ return {
268
+ error: `❌ continue requires numeric runId, got: "${token}"\n\nThe runId must be a number (see 'subagent runs' to list all run IDs).\n\n✓ Correct: subagent continue 22 -- <task>`,
269
+ };
270
+ runId = parsed;
271
+ continue;
272
+ }
273
+ return {
274
+ error: `❌ Unexpected argument: ${token}\n\nAfter runId, only options (--main, --isolated, --agent) or the separator \`--\` are allowed.\n\n✓ Correct: subagent continue ${runId} --main -- <task>`,
275
+ };
276
+ }
277
+
278
+ if (!agent) {
279
+ agent = token;
280
+ continue;
281
+ }
282
+ return {
283
+ error: `❌ Unexpected argument: ${token}\n\nAfter agent name, only options (--main, --isolated) or the separator \`--\` are allowed.\n\n✓ Correct: subagent run ${agent} --main -- <task>`,
284
+ };
285
+ }
286
+
287
+ if (verb === "continue" && runId === undefined) {
288
+ return {
289
+ error: `❌ continue requires <runId>\n\nYou must specify a runId (numeric). Use 'subagent runs' to list all.\n\n✓ Example: subagent continue 22 -- <task>`,
290
+ };
291
+ }
292
+
293
+ const params: Record<string, unknown> = { task };
294
+ if (verb === "continue") {
295
+ params.runId = runId;
296
+ if (agent) params.agent = agent;
297
+ } else {
298
+ params.agent = agent ?? "worker";
299
+ }
300
+ if (contextMode) params.contextMode = contextMode;
301
+
302
+ return { params };
303
+ }
304
+
305
+ function parseBatchOrChain(
306
+ verb: "batch" | "chain",
307
+ args: string[],
308
+ ): { params: Record<string, unknown> } | { error: string } {
309
+ let contextMode: ContextMode | undefined;
310
+ const blocks: BatchOrChainBlock[] = [];
311
+ let index = 0;
312
+ let sawBlock = false;
313
+
314
+ while (index < args.length) {
315
+ const token = args[index];
316
+
317
+ if (!sawBlock && (token === "--main" || token === "--isolated")) {
318
+ contextMode = token === "--main" ? "main" : "isolated";
319
+ index++;
320
+ continue;
321
+ }
322
+
323
+ if (token !== "--agent") {
324
+ if (token === "--task") {
325
+ return {
326
+ error:
327
+ `❌ ${verb} blocks must start with \`--agent <agent> --task <task>\`\n\n` +
328
+ `Found \`--task\` before \`--agent\`.\n\n` +
329
+ `✓ Example: subagent ${verb} --main --agent worker --task "task A" --agent reviewer --task "task B"`,
330
+ };
331
+ }
332
+ if (token.startsWith("--")) {
333
+ return {
334
+ error:
335
+ `❌ Unknown or misplaced option: ${token}\n\n` +
336
+ `Valid ${verb} syntax: subagent ${verb} [--main|--isolated] --agent <agent> --task <task> --agent <agent> --task <task> ...`,
337
+ };
338
+ }
339
+ return {
340
+ error:
341
+ `❌ ${verb} does not allow free text outside \`--task\` blocks\n\n` +
342
+ `Unexpected token: ${token}\n\n` +
343
+ `✓ Example: subagent ${verb} --agent worker --task "task A" --agent reviewer --task "task B"`,
344
+ };
345
+ }
346
+
347
+ sawBlock = true;
348
+ const agent = args[index + 1];
349
+ if (!agent || agent.startsWith("--")) {
350
+ return {
351
+ error:
352
+ `❌ ${verb} requires \`--agent <value>\`\n\n` +
353
+ `✓ Example: subagent ${verb} --agent worker --task "task A" --agent reviewer --task "task B"`,
354
+ };
355
+ }
356
+
357
+ const taskFlag = args[index + 2];
358
+ if (taskFlag !== "--task") {
359
+ return {
360
+ error:
361
+ `❌ ${verb} blocks must be exactly \`--agent <agent> --task <task>\`\n\n` +
362
+ `After \`--agent ${agent}\`, expected \`--task\`.`,
363
+ };
364
+ }
365
+
366
+ const task = args[index + 3];
367
+ if (!task || task.startsWith("--")) {
368
+ return {
369
+ error:
370
+ `❌ ${verb} requires \`--task <value>\`\n\n` +
371
+ `✓ Example: subagent ${verb} --agent worker --task "task A" --agent reviewer --task "task B"`,
372
+ };
373
+ }
374
+
375
+ blocks.push({ agent, task });
376
+ index += 4;
377
+ }
378
+
379
+ if (blocks.length < 2) {
380
+ return {
381
+ error:
382
+ `❌ ${verb} requires at least 2 blocks\n\n` +
383
+ `Use repeated \`--agent <agent> --task <task>\` blocks.\n\n` +
384
+ `✓ Example: subagent ${verb} --agent worker --task "task A" --agent reviewer --task "task B"`,
385
+ };
386
+ }
387
+
388
+ return {
389
+ params: {
390
+ asyncAction: verb,
391
+ ...(contextMode ? { contextMode } : {}),
392
+ ...(verb === "batch" ? { runs: blocks } : { steps: blocks }),
393
+ },
394
+ };
395
+ }
396
+
397
+ function extractVerb(tokens: string[]): { verb: string; args: string[] } {
398
+ if (tokens.length === 0) return { verb: "help", args: [] };
399
+ if (tokens[0] === "subagent") {
400
+ if (tokens.length === 1) return { verb: "help", args: [] };
401
+ return { verb: tokens[1], args: tokens.slice(2) };
402
+ }
403
+ return { verb: tokens[0], args: tokens.slice(1) };
404
+ }
405
+
406
+ export function parseSubagentCommandVerb(command: unknown): string | null {
407
+ if (typeof command !== "string") return null;
408
+ const trimmed = command.trim();
409
+ if (!trimmed) return null;
410
+ const tokenized = tokenizeCli(trimmed);
411
+ if ("error" in tokenized) return null;
412
+ return extractVerb(tokenized.tokens).verb;
413
+ }
414
+
415
+ export function isSubagentAsyncLaunchCommand(command: unknown): boolean {
416
+ if (typeof command !== "string") return false;
417
+ const trimmed = command.trim();
418
+ if (!trimmed) return false;
419
+ const tokenized = tokenizeCli(trimmed);
420
+ if ("error" in tokenized) return false;
421
+ const { verb } = extractVerb(tokenized.tokens);
422
+ return verb === "run" || verb === "continue" || verb === "batch" || verb === "chain";
423
+ }
424
+
425
+ export function parseSubagentToolCommand(
426
+ command: unknown,
427
+ options: { knownRunIds?: number[] } = {},
428
+ ): SubagentCliParseResult {
429
+ if (typeof command !== "string") {
430
+ return {
431
+ type: "error",
432
+ message: `❌ Missing or invalid command parameter\n\nThe 'command' parameter must be a string.\n\n✓ Correct: { command: "subagent help" }\n✗ Wrong: { command: 123 }\n\nTry: subagent help`,
433
+ };
434
+ }
435
+
436
+ const trimmed = command.trim();
437
+ if (!trimmed) {
438
+ return {
439
+ type: "error",
440
+ message: `❌ Empty command\n\nYou must provide a valid subagent command.\n\n✓ Try: subagent help\n✓ Try: subagent runs\n✓ Try: subagent run worker -- task description`,
441
+ };
442
+ }
443
+
444
+ const tokenized = tokenizeCli(trimmed);
445
+ if ("error" in tokenized) {
446
+ return {
447
+ type: "error",
448
+ message: `❌ Syntax error: ${tokenized.error}\n\nCheck that quotes are balanced and the command is well-formed.\n\n✓ Correct: subagent run worker -- "task with spaces"`,
449
+ };
450
+ }
451
+
452
+ const { verb, args } = extractVerb(tokenized.tokens);
453
+
454
+ switch (verb) {
455
+ case "help":
456
+ return { type: "help" };
457
+
458
+ case "agents":
459
+ return { type: "agents" };
460
+
461
+ case "runs":
462
+ return { type: "params", params: { asyncAction: "list" } };
463
+
464
+ case "status": {
465
+ const runIdRaw = args[0];
466
+ if (!runIdRaw)
467
+ return {
468
+ type: "error",
469
+ message: `❌ status requires <runId>\n\n✓ Example: subagent status 22\n\nSee all runs with: subagent runs`,
470
+ };
471
+ const runId = parseInteger(runIdRaw);
472
+ if (runId === null)
473
+ return {
474
+ type: "error",
475
+ message: `❌ Invalid runId: "${runIdRaw}"\n\nThe runId must be a number. See all runs with: subagent runs`,
476
+ };
477
+ return { type: "params", params: { asyncAction: "status", runId } };
478
+ }
479
+
480
+ case "detail": {
481
+ const runIdRaw = args[0];
482
+ if (!runIdRaw)
483
+ return {
484
+ type: "error",
485
+ message: `❌ detail requires <runId>\n\n✓ Example: subagent detail 22\n\nSee all runs with: subagent runs`,
486
+ };
487
+ const runId = parseInteger(runIdRaw);
488
+ if (runId === null)
489
+ return {
490
+ type: "error",
491
+ message: `❌ Invalid runId: "${runIdRaw}"\n\nThe runId must be a number. See all runs with: subagent runs`,
492
+ };
493
+ return { type: "params", params: { asyncAction: "detail", runId } };
494
+ }
495
+
496
+ case "abort":
497
+ case "remove": {
498
+ const target = args[0];
499
+ if (!target)
500
+ return {
501
+ type: "error",
502
+ message: `❌ ${verb} requires <runId|runId,runId|all>\n\n✓ Examples:\n subagent ${verb} 22\n subagent ${verb} 22,23,24\n subagent ${verb} all`,
503
+ };
504
+ const parsedTarget = parseRunTarget(target, options.knownRunIds);
505
+ if ("error" in parsedTarget)
506
+ return {
507
+ type: "error",
508
+ message: `❌ Invalid target: "${target}"\n\n${parsedTarget.error}`,
509
+ };
510
+ return {
511
+ type: "params",
512
+ params: {
513
+ asyncAction: verb,
514
+ ...parsedTarget,
515
+ },
516
+ };
517
+ }
518
+
519
+ case "run": {
520
+ const parsed = parseRunLike("run", args);
521
+ if ("error" in parsed) return { type: "error", message: parsed.error };
522
+ return { type: "params", params: parsed.params };
523
+ }
524
+
525
+ case "continue": {
526
+ const parsed = parseRunLike("continue", args);
527
+ if ("error" in parsed) return { type: "error", message: parsed.error };
528
+ return { type: "params", params: parsed.params };
529
+ }
530
+
531
+ case "batch": {
532
+ const parsed = parseBatchOrChain("batch", args);
533
+ if ("error" in parsed) return { type: "error", message: parsed.error };
534
+ return { type: "params", params: parsed.params };
535
+ }
536
+
537
+ case "chain": {
538
+ const parsed = parseBatchOrChain("chain", args);
539
+ if ("error" in parsed) return { type: "error", message: parsed.error };
540
+ return { type: "params", params: parsed.params };
541
+ }
542
+
543
+ default:
544
+ return {
545
+ type: "error",
546
+ message: `❌ Unknown subcommand: "${verb}"\n\nValid commands: help, agents, run, continue, batch, chain, runs, status, detail, abort, remove\n\n✓ Try: subagent help`,
547
+ };
548
+ }
549
+ }