agentproc 0.1.1 → 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/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "agentproc",
3
- "version": "0.1.1",
4
- "description": "AgentProc Protocol SDK for Node.js — connect any Agent CLI to a messaging platform",
3
+ "version": "0.3.0",
4
+ "description": "AgentProc Protocol SDK + CLI for Node.js — connect any Agent CLI to a messaging platform",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
+ "bin": {
8
+ "agentproc": "src/cli.js"
9
+ },
7
10
  "scripts": {
8
- "test": "node --test src/index.test.js"
11
+ "test": "node --test src/index.test.js src/runner.test.js"
9
12
  },
10
- "keywords": ["agentproc", "agent", "bridge", "protocol", "cli", "ai"],
13
+ "keywords": ["agentproc", "agent", "bridge", "protocol", "cli", "ai", "runner"],
11
14
  "license": "MIT",
12
15
  "engines": {
13
16
  "node": ">=18"
@@ -16,5 +19,5 @@
16
19
  "type": "git",
17
20
  "url": "https://github.com/jeffkit/agentproc.git"
18
21
  },
19
- "homepage": "https://jeffkit.github.io/agentproc/"
22
+ "homepage": "https://agentproc.dev/"
20
23
  }
package/src/cli.js ADDED
@@ -0,0 +1,633 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * agentproc CLI — run any AgentProc profile against a message.
5
+ *
6
+ * Usage:
7
+ * agentproc --profile <path.yaml> --prompt "hello" [options]
8
+ *
9
+ * Options:
10
+ * --profile, -p <path> Profile YAML path (required)
11
+ * --prompt <text> User message (required, unless --stdin)
12
+ * --session <id> Previous session id for multi-turn
13
+ * --session-name <name> Human-readable session name
14
+ * --from <user> Sender identifier
15
+ * --cwd <path> Override profile.cwd
16
+ * --env KEY=VALUE Extra env var (repeatable)
17
+ * --timeout <secs> Override profile.timeout_secs
18
+ * --no-stream Disable streaming (set AGENT_STREAMING=0)
19
+ * --verbose Forward protocol lines to stderr (default)
20
+ * --quiet Suppress protocol lines on stderr
21
+ * --raw Don't parse stdout; forward agent output verbatim
22
+ * --stdin Read prompt from stdin instead of --prompt
23
+ * --version Print version and exit
24
+ * --help, -h Show help
25
+ *
26
+ * Output (default mode):
27
+ * stderr → protocol lines (AGENT_PARTIAL:, AGENT_SESSION:, AGENT_ERROR:) in real time
28
+ * stdout → final reply body (printed after agent exits)
29
+ * exit → 0 success, 1 error, 124 timeout
30
+ *
31
+ * Output (--raw mode):
32
+ * stdout → agent's stdout, verbatim, no parsing
33
+ * exit → agent's exit code
34
+ *
35
+ * The last AGENT_SESSION: id is also printed on stderr at the very end,
36
+ * prefixed with "agentproc:session:" so shell scripts can capture it:
37
+ * session=$(agentproc ... 2>&1 | grep '^agentproc:session:' | cut -d: -f3)
38
+ */
39
+
40
+ const fs = require('node:fs');
41
+ const path = require('node:path');
42
+
43
+ const runner = require('./runner.js');
44
+ const hub = require('./hub.js');
45
+ const { PROTOCOL_VERSION } = runner;
46
+
47
+ const PKG_VERSION = require('../package.json').version;
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Arg parsing — minimal hand-rolled parser, no deps
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function parseArgs(argv) {
54
+ const opts = {
55
+ profile: null,
56
+ prompt: null,
57
+ session: '',
58
+ sessionName: 'default',
59
+ from: '',
60
+ cwd: null,
61
+ env: [], // array of "KEY=VALUE" strings; --env can repeat
62
+ timeout: null,
63
+ stream: true,
64
+ verbose: true,
65
+ raw: false,
66
+ stdin: false,
67
+ help: false,
68
+ version: false,
69
+ };
70
+ const extras = [];
71
+
72
+ for (let i = 0; i < argv.length; i++) {
73
+ const a = argv[i];
74
+ const next = () => {
75
+ if (i + 1 >= argv.length) throw new Error(`option ${a} requires a value`);
76
+ return argv[++i];
77
+ };
78
+ switch (a) {
79
+ case '-h': case '--help': opts.help = true; break;
80
+ case '--version': opts.version = true; break;
81
+ case '--profile': case '-p': opts.profile = next(); break;
82
+ case '--prompt': opts.prompt = next(); break;
83
+ case '--session': opts.session = next(); break;
84
+ case '--session-name': opts.sessionName = next(); break;
85
+ case '--from': opts.from = next(); break;
86
+ case '--cwd': opts.cwd = next(); break;
87
+ case '--env':
88
+ opts.env.push(next());
89
+ break;
90
+ case '--timeout': opts.timeout = parseInt(next(), 10); break;
91
+ case '--no-stream': opts.noStream = true; break;
92
+ case '--verbose': opts.verbose = true; break;
93
+ case '--quiet': opts.verbose = false; break;
94
+ case '--raw': opts.raw = true; break;
95
+ case '--stdin': opts.stdin = true; break;
96
+ default:
97
+ if (a === 'hub' && extras.length === 0) {
98
+ opts.hub = true;
99
+ opts.hubArgs = argv.slice(i + 1);
100
+ return { opts, extras };
101
+ }
102
+ if (a.startsWith('--')) throw new Error(`unknown option: ${a}`);
103
+ extras.push(a);
104
+ }
105
+ }
106
+ return { opts, extras };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Hub subcommand dispatcher
111
+ // ---------------------------------------------------------------------------
112
+
113
+ async function runHubSubcommand(args) {
114
+ const sub = args[0];
115
+ const rest = args.slice(1);
116
+
117
+ if (!sub || sub === '--help' || sub === '-h') {
118
+ showHubHelp();
119
+ return 0;
120
+ }
121
+
122
+ // Parse common flags
123
+ const refresh = rest.includes('--refresh');
124
+ const positional = rest.filter(a => !a.startsWith('--'));
125
+
126
+ if (sub === 'list') {
127
+ const profiles = await hub.listProfiles({
128
+ onLog: m => process.stderr.write(m + '\n'),
129
+ });
130
+ process.stdout.write('Available profiles in the official hub:\n\n');
131
+ for (const p of profiles) {
132
+ process.stdout.write(
133
+ ` ${p.name.padEnd(15)} ${p.tested.padEnd(12)} ${p.description.slice(0, 60)}\n`
134
+ );
135
+ }
136
+ process.stdout.write(`\nRun \`agentproc hub run <name> -p "hi"\` to use one.\n`);
137
+ return 0;
138
+ }
139
+
140
+ if (sub === 'show') {
141
+ if (!positional[0]) {
142
+ process.stderr.write('error: hub show requires a profile name\n');
143
+ return 2;
144
+ }
145
+ const readme = await hub.showReadme(positional[0], {
146
+ refresh,
147
+ onLog: m => process.stderr.write(m + '\n'),
148
+ });
149
+ process.stdout.write(readme);
150
+ if (!readme.endsWith('\n')) process.stdout.write('\n');
151
+ return 0;
152
+ }
153
+
154
+ if (sub === 'install') {
155
+ if (!positional[0]) {
156
+ process.stderr.write('error: hub install requires a profile name\n');
157
+ return 2;
158
+ }
159
+ const target = process.cwd();
160
+ await hub.installProfile(positional[0], target, {
161
+ refresh,
162
+ onLog: m => process.stderr.write(m + '\n'),
163
+ });
164
+ return 0;
165
+ }
166
+
167
+ if (sub === 'run') {
168
+ if (!positional[0]) {
169
+ process.stderr.write('error: hub run requires a profile name\n');
170
+ return 2;
171
+ }
172
+ const profileName = positional[0];
173
+ const cacheDir = await hub.fetchProfile(profileName, {
174
+ refresh,
175
+ onLog: m => process.stderr.write(m + '\n'),
176
+ });
177
+ const profilePath = path.join(cacheDir, 'profile.yaml');
178
+
179
+ // Re-parse the remaining args as the runner options (--prompt, --cwd, etc.).
180
+ const { opts: runOpts } = parseArgs(rest);
181
+ if (!runOpts.prompt && !runOpts.stdin) {
182
+ process.stderr.write('error: hub run requires --prompt <text> or --stdin\n');
183
+ return 2;
184
+ }
185
+
186
+ return await runAgent(profilePath, runOpts);
187
+ }
188
+
189
+ process.stderr.write(`error: unknown hub subcommand: ${sub}\n\n`);
190
+ showHubHelp();
191
+ return 2;
192
+ }
193
+
194
+ function showHubHelp() {
195
+ process.stdout.write(`agentproc hub — manage profiles from the official Hub
196
+
197
+ Usage:
198
+ agentproc hub list List all profiles in the hub
199
+ agentproc hub show <name> Show a profile's README
200
+ agentproc hub install <name> Copy a profile to the current directory
201
+ agentproc hub run <name> [run-options] Fetch (if needed) and run a profile
202
+
203
+ Hub run options (same as the regular --profile runner):
204
+ -p, --prompt <text> User message (or use --stdin)
205
+ --cwd <path> Override profile.cwd (default: current dir)
206
+ --env KEY=VALUE Extra env var (repeatable)
207
+ --session <id> Previous session id for multi-turn
208
+ --timeout <secs> Override profile.timeout_secs
209
+ --no-stream Disable streaming
210
+ --verbose / --quiet Protocol line visibility (default: verbose)
211
+ --stdin Read prompt from stdin
212
+
213
+ Common options:
214
+ --refresh Force re-fetch from GitHub (ignore cache)
215
+ -h, --help Show this help
216
+
217
+ Examples:
218
+ agentproc hub list
219
+ agentproc hub run echo-agent -p "hello"
220
+ cd ~/projects/my-app && agentproc hub run claude-code -p "explain this" --env ANTHROPIC_API_KEY=$KEY
221
+ agentproc hub show codex
222
+ agentproc hub install agy
223
+
224
+ Profiles are cached at ~/.agentproc/cache/hub/<name>/ (24h TTL).
225
+ `);
226
+ }
227
+
228
+ /**
229
+ * Shared runner logic used by both `agentproc --profile` and `agentproc hub run`.
230
+ * Kept here for the hub subcommand to reuse; the legacy main() path also calls it.
231
+ */
232
+ async function runAgent(profilePath, opts) {
233
+ let profileRaw;
234
+ try {
235
+ const yamlText = fs.readFileSync(path.resolve(profilePath), 'utf8');
236
+ profileRaw = parseYaml(yamlText);
237
+ } catch (e) {
238
+ process.stderr.write(`error: failed to read profile ${profilePath}: ${e.message}\n`);
239
+ return 2;
240
+ }
241
+
242
+ // Read prompt.
243
+ let prompt = opts.prompt;
244
+ if (opts.stdin) {
245
+ prompt = fs.readFileSync(0, 'utf8').replace(/\n$/, '');
246
+ }
247
+ if (prompt == null) {
248
+ process.stderr.write('error: --prompt (or --stdin) is required\n');
249
+ return 2;
250
+ }
251
+
252
+ // opts.env is an array of "KEY=VALUE" strings (from repeated --env flags)
253
+ const extraEnv = {};
254
+ for (const kv of opts.env || []) {
255
+ const eq = kv.indexOf('=');
256
+ if (eq < 0) {
257
+ process.stderr.write(`error: --env expects KEY=VALUE, got: ${kv}\n`);
258
+ return 2;
259
+ }
260
+ extraEnv[kv.slice(0, eq)] = kv.slice(eq + 1);
261
+ }
262
+
263
+ const streaming = opts.noStream ? false : null;
264
+
265
+ if (opts.raw) {
266
+ const r = await runner.run(profileRaw, {
267
+ message: prompt,
268
+ sessionId: opts.session || '',
269
+ sessionName: opts.sessionName || 'default',
270
+ fromUser: opts.from || '',
271
+ streaming,
272
+ cwd: opts.cwd,
273
+ extraEnv,
274
+ timeoutSecs: opts.timeout,
275
+ });
276
+ process.stdout.write(r.reply);
277
+ if (r.reply && !r.reply.endsWith('\n')) process.stdout.write('\n');
278
+ return r.exitCode === 0 ? 0 : 1;
279
+ }
280
+
281
+ const verbose = opts.verbose || !opts.quiet || (opts.verbose === undefined && opts.quiet === undefined) || opts.verbose;
282
+
283
+ const r = await runner.run(profileRaw, {
284
+ message: prompt,
285
+ sessionId: opts.session || '',
286
+ sessionName: opts.sessionName || 'default',
287
+ fromUser: opts.from || '',
288
+ streaming,
289
+ cwd: opts.cwd,
290
+ extraEnv,
291
+ timeoutSecs: opts.timeout,
292
+ onPartial: (t) => { if (verbose) process.stderr.write(`AGENT_PARTIAL:${JSON.stringify(t)}\n`); },
293
+ onSession: (id) => { if (verbose) process.stderr.write(`AGENT_SESSION:${id}\n`); },
294
+ onError: (msg) => { if (verbose) process.stderr.write(`AGENT_ERROR:${JSON.stringify(msg)}\n`); },
295
+ onStderr: (line) => { if (verbose) process.stderr.write(`[agent stderr] ${line}\n`); },
296
+ });
297
+
298
+ if (r.reply) {
299
+ process.stdout.write(r.reply);
300
+ if (!r.reply.endsWith('\n')) process.stdout.write('\n');
301
+ }
302
+ if (r.sessionId) process.stderr.write(`agentproc:session:${r.sessionId}\n`);
303
+ if (r.error) process.stderr.write(`agentproc:error:${r.error}\n`);
304
+ return r.exitCode === 0 ? 0 : 1;
305
+ }
306
+
307
+ function showHelp() {
308
+ process.stdout.write(`agentproc v${PKG_VERSION} (protocol ${PROTOCOL_VERSION})
309
+
310
+ Usage:
311
+ agentproc --profile <path.yaml> --prompt "hello" [options]
312
+
313
+ Required:
314
+ --profile, -p <path> Profile YAML path
315
+ --prompt <text> User message (or use --stdin)
316
+
317
+ Session:
318
+ --session <id> Previous session id (multi-turn)
319
+ --session-name <name> Human-readable session name (default: "default")
320
+ --from <user> Sender identifier
321
+
322
+ Execution:
323
+ --cwd <path> Override profile.cwd
324
+ --env KEY=VALUE Extra env var (repeatable)
325
+ --timeout <secs> Override profile.timeout_secs
326
+ --no-stream Set AGENT_STREAMING=0
327
+
328
+ Output:
329
+ --verbose Forward protocol lines to stderr (default)
330
+ --quiet Suppress protocol lines on stderr
331
+ --raw Don't parse stdout; forward agent output verbatim
332
+ --stdin Read prompt from stdin instead of --prompt
333
+
334
+ Other:
335
+ --version Print version and exit
336
+ --help, -h Show this help
337
+
338
+ Output semantics:
339
+ stderr → protocol lines (AGENT_PARTIAL:, AGENT_SESSION:, AGENT_ERROR:)
340
+ stdout → final reply body (non-protocol lines)
341
+ exit → 0 success · 1 error · 124 timeout (per spec)
342
+
343
+ The final session id is printed on stderr as: agentproc:session:<id>
344
+
345
+ Examples:
346
+ agentproc --profile hub/echo-agent/profile.yaml --prompt "hi"
347
+ agentproc -p hub/claude-code/profile.yaml --prompt "hello" --verbose
348
+ cat prompt.txt | agentproc -p prof.yaml --stdin
349
+ `);
350
+ }
351
+
352
+ function showVersion() {
353
+ process.stdout.write(`agentproc ${PKG_VERSION} (protocol ${PROTOCOL_VERSION})\n`);
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // YAML parsing — minimal hand-rolled, supports the subset hub profiles use
358
+ // ---------------------------------------------------------------------------
359
+
360
+ /**
361
+ * Parse a YAML profile file into a JS object.
362
+ *
363
+ * We deliberately avoid a YAML dependency to keep the SDK zero-dep.
364
+ * The subset we parse: nested maps, scalar values, block scalars (|), arrays
365
+ * of scalars (under `args:` and `tags:`). This covers every hub/ profile
366
+ * and every spec/protocol.md example.
367
+ *
368
+ * For anything more complex, users are encouraged to pre-parse their YAML
369
+ * and pass the object directly to the runner.
370
+ */
371
+ function parseYamlSimple(text) {
372
+ // First try JSON (also valid YAML for simple cases).
373
+ try { return JSON.parse(text); } catch {}
374
+
375
+ const lines = text.split(/\r?\n/);
376
+ const root = {};
377
+ const stack = [{ indent: -1, obj: root, key: null }];
378
+
379
+ function currentContainer(minIndent) {
380
+ while (stack.length > 1 && stack[stack.length - 1].indent >= minIndent) {
381
+ stack.pop();
382
+ }
383
+ return stack[stack.length - 1];
384
+ }
385
+
386
+ for (let i = 0; i < lines.length; i++) {
387
+ const raw = lines[i];
388
+ if (raw.trim() === '' || raw.trim().startsWith('#')) continue;
389
+ const indent = raw.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length;
390
+ const content = raw.trim();
391
+
392
+ // key: value
393
+ const m = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
394
+ if (!m) {
395
+ // Skip unparseable lines (e.g., complex flow scalars); don't crash.
396
+ continue;
397
+ }
398
+ const [, key, val] = m;
399
+ const container = currentContainer(indent);
400
+
401
+ if (val === '') {
402
+ // Could be a nested map, a block scalar, or a sequence. Look ahead.
403
+ const nextRaw = lines[i + 1] || '';
404
+ const nextIndent = nextRaw.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length;
405
+ if (nextIndent > indent) {
406
+ if (nextRaw.trim().startsWith('- ')) {
407
+ // Sequence
408
+ const arr = [];
409
+ container.obj[key] = arr;
410
+ stack.push({ indent, obj: { __seq: arr }, key });
411
+ } else {
412
+ // Nested map
413
+ const child = {};
414
+ container.obj[key] = child;
415
+ stack.push({ indent, obj: child, key });
416
+ }
417
+ } else {
418
+ // Empty value, no children
419
+ container.obj[key] = '';
420
+ }
421
+ } else if (val === '|' || val === '|-') {
422
+ // Block scalar — consume subsequent more-indented lines.
423
+ const blockLines = [];
424
+ let j = i + 1;
425
+ for (; j < lines.length; j++) {
426
+ const nr = lines[j];
427
+ if (nr.trim() === '' && j === lines.length - 1) break;
428
+ const ni = nr.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length;
429
+ if (ni <= indent && nr.trim() !== '') break;
430
+ blockLines.push(nr.slice(Math.min(indent + 2, nr.length)));
431
+ }
432
+ container.obj[key] = blockLines.join('\n').replace(/\n+$/, val === '|' ? '\n' : '');
433
+ i = j - 1;
434
+ } else if (val.startsWith('- ')) {
435
+ // Inline sequence element on same line — rare but handle it.
436
+ if (!Array.isArray(container.obj[key])) container.obj[key] = [];
437
+ container.obj[key].push(stripScalar(val.slice(2)));
438
+ } else {
439
+ container.obj[key] = stripScalar(val);
440
+ }
441
+ }
442
+
443
+ // Post-process: walk and lift any __seq arrays.
444
+ function liftSeqs(o) {
445
+ if (Array.isArray(o)) {
446
+ o.forEach(liftSeqs);
447
+ } else if (o && typeof o === 'object') {
448
+ for (const k of Object.keys(o)) {
449
+ const v = o[k];
450
+ if (v && typeof v === 'object' && '__seq' in v) {
451
+ o[k] = v.__seq;
452
+ liftSeqs(o[k]);
453
+ } else {
454
+ liftSeqs(v);
455
+ }
456
+ }
457
+ }
458
+ }
459
+ liftSeqs(root);
460
+ return root;
461
+ }
462
+
463
+ function stripScalar(v) {
464
+ // Quoted string — return inner content verbatim.
465
+ if ((v.startsWith('"') && v.endsWith('"')) ||
466
+ (v.startsWith("'") && v.endsWith("'"))) {
467
+ return v.slice(1, -1);
468
+ }
469
+ // Flow sequence: [a, b, c]
470
+ if (v.startsWith('[') && v.endsWith(']')) {
471
+ const inner = v.slice(1, -1).trim();
472
+ if (inner === '') return [];
473
+ return inner.split(',').map(s => stripScalar(s.trim()));
474
+ }
475
+ // Booleans / null
476
+ const lv = v.toLowerCase();
477
+ if (lv === 'true') return true;
478
+ if (lv === 'false') return false;
479
+ if (lv === 'null' || lv === '~') return null;
480
+ // Numbers (int / float, optional sign)
481
+ if (/^[+-]?\d+$/.test(v)) return parseInt(v, 10);
482
+ if (/^[+-]?\d+\.\d+$/.test(v)) return parseFloat(v);
483
+ return v;
484
+ }
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // Sequence continuation: collect "- ..." entries under a key whose value
488
+ // became { __seq: [...] }. Done above. But standalone "- ..." lines (when
489
+ // the container's current key already has an array) need handling.
490
+ // ---------------------------------------------------------------------------
491
+
492
+ // Re-do the parsing with proper sequence handling.
493
+ function parseYaml(text) {
494
+ try { return JSON.parse(text); } catch {}
495
+
496
+ // Use a line-based state machine that handles sequences better.
497
+ const lines = text.split(/\r?\n/);
498
+ const root = {};
499
+ /** @type {Array<{indent:number, obj:Object|Array, parentKey:string|null, parent:Object|null}>} */
500
+ const stack = [{ indent: -1, obj: root, parentKey: null, parent: null }];
501
+
502
+ function top() { return stack[stack.length - 1]; }
503
+ function popUntil(minIndent) {
504
+ while (stack.length > 1 && top().indent >= minIndent) stack.pop();
505
+ return top();
506
+ }
507
+
508
+ function getIndent(s) {
509
+ return s.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length;
510
+ }
511
+
512
+ for (let i = 0; i < lines.length; i++) {
513
+ const raw = lines[i];
514
+ if (raw.trim() === '' || raw.trim().startsWith('#')) continue;
515
+ const indent = getIndent(raw);
516
+ const content = raw.slice(indent).replace(/\r$/, '');
517
+ const cont = popUntil(indent);
518
+
519
+ // Sequence item: "- value" or "-"
520
+ if (content.startsWith('- ') || content === '-') {
521
+ // Find the parent object that has a key awaiting a sequence value.
522
+ // Strategy: if top().obj is an array, push to it; else we need to
523
+ // convert — but our key: line lookahead already created the array.
524
+ if (Array.isArray(cont.obj)) {
525
+ const rest = content === '-' ? '' : content.slice(2);
526
+ if (rest.trim() === '') {
527
+ // Map under sequence — rare in our profiles, skip gracefully.
528
+ continue;
529
+ }
530
+ cont.obj.push(stripScalar(rest));
531
+ }
532
+ continue;
533
+ }
534
+
535
+ // key: value
536
+ const m = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
537
+ if (!m) continue;
538
+ const [, key, val] = m;
539
+
540
+ if (val === '') {
541
+ // Look ahead: is the next non-empty, more-indented line a sequence?
542
+ let j = i + 1;
543
+ while (j < lines.length && (lines[j].trim() === '' || lines[j].trim().startsWith('#'))) j++;
544
+ const nextRaw = lines[j] || '';
545
+ const nextIndent = getIndent(nextRaw);
546
+ const nextContent = nextRaw.slice(nextIndent);
547
+ if (nextIndent > indent && (nextContent.startsWith('- ') || nextContent === '-')) {
548
+ const arr = [];
549
+ cont.obj[key] = arr;
550
+ stack.push({ indent, obj: arr, parentKey: key, parent: cont.obj });
551
+ } else if (nextIndent > indent) {
552
+ const child = {};
553
+ cont.obj[key] = child;
554
+ stack.push({ indent, obj: child, parentKey: key, parent: cont.obj });
555
+ } else {
556
+ cont.obj[key] = '';
557
+ }
558
+ } else if (val === '|' || val === '|-' || val === '>') {
559
+ const blockLines = [];
560
+ let j = i + 1;
561
+ for (; j < lines.length; j++) {
562
+ const nr = lines[j];
563
+ const ni = getIndent(nr);
564
+ if (nr.trim() === '') { blockLines.push(''); continue; }
565
+ if (ni <= indent) break;
566
+ blockLines.push(nr.slice(Math.min(indent + 2, nr.length)));
567
+ }
568
+ const joined = blockLines.join('\n');
569
+ container_set(cont.obj, key, val === '|'
570
+ ? joined.replace(/\n*$/, '\n')
571
+ : joined.replace(/\n*$/, ''));
572
+ i = j - 1;
573
+ } else {
574
+ container_set(cont.obj, key, stripScalar(val));
575
+ }
576
+ }
577
+
578
+ return root;
579
+ }
580
+
581
+ function container_set(obj, key, value) {
582
+ obj[key] = value;
583
+ }
584
+
585
+ // ---------------------------------------------------------------------------
586
+ // Main
587
+ // ---------------------------------------------------------------------------
588
+
589
+ async function main() {
590
+ const { opts } = parseArgs(process.argv.slice(2));
591
+
592
+ if (opts.help) { showHelp(); process.exit(0); }
593
+ if (opts.version) { showVersion(); process.exit(0); }
594
+
595
+ // `agentproc hub <subcommand>` — defer to hub dispatcher.
596
+ if (opts.hub) {
597
+ return await runHubSubcommand(opts.hubArgs);
598
+ }
599
+
600
+ if (!opts.profile) {
601
+ process.stderr.write('error: --profile is required\n\n');
602
+ showHelp();
603
+ process.exit(2);
604
+ }
605
+
606
+ // Read prompt from --stdin if requested.
607
+ let prompt = opts.prompt;
608
+ if (opts.stdin) {
609
+ prompt = fs.readFileSync(0, 'utf8').replace(/\n$/, '');
610
+ }
611
+ if (prompt == null) {
612
+ process.stderr.write('error: --prompt (or --stdin) is required\n');
613
+ process.exit(2);
614
+ }
615
+
616
+ // Read & parse profile YAML, then delegate to the shared runner path.
617
+ try {
618
+ return await runAgent(opts.profile, opts);
619
+ } catch (e) {
620
+ process.stderr.write(`error: ${e.message}\n`);
621
+ return 1;
622
+ }
623
+ }
624
+
625
+ // Run main() only when invoked directly as a script, not when required for tests.
626
+ if (require.main === module) {
627
+ main().catch(e => {
628
+ process.stderr.write(`[agentproc] unhandled error: ${e && (e.stack || e)}\n`);
629
+ process.exit(1);
630
+ });
631
+ }
632
+
633
+ module.exports = { parseArgs, parseYaml, showHelp, main };