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 +8 -5
- package/src/cli.js +633 -0
- package/src/hub.js +310 -0
- package/src/hub.test.js +327 -0
- package/src/runner.js +395 -0
- package/src/runner.test.js +439 -0
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentproc",
|
|
3
|
-
"version": "0.
|
|
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://
|
|
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 };
|