agentproc 0.2.1 → 0.4.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 +2 -2
- package/src/cli.js +346 -99
- package/src/conformance.test.js +29 -0
- package/src/hub.js +483 -0
- package/src/hub.test.js +345 -0
- package/src/runner.js +239 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentproc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"agentproc": "src/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test src/index.test.js src/runner.test.js"
|
|
11
|
+
"test": "node --test src/index.test.js src/runner.test.js src/hub.test.js src/conformance.test.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": ["agentproc", "agent", "bridge", "protocol", "cli", "ai", "runner"],
|
|
14
14
|
"license": "MIT",
|
package/src/cli.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* agentproc CLI —
|
|
4
|
+
* agentproc CLI — drive any AgentProc profile against a message.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Quick start (hub):
|
|
7
|
+
* agentproc hub list
|
|
8
|
+
* agentproc hub run echo-agent -p "hello"
|
|
9
|
+
* cd ~/projects/my-app && agentproc hub run claude-code -p "explain this"
|
|
10
|
+
*
|
|
11
|
+
* Advanced (local profile YAML, no hub fetch):
|
|
7
12
|
* agentproc --profile <path.yaml> --prompt "hello" [options]
|
|
8
13
|
*
|
|
9
|
-
* Options:
|
|
14
|
+
* Options (local-profile mode):
|
|
10
15
|
* --profile, -p <path> Profile YAML path (required)
|
|
11
16
|
* --prompt <text> User message (required, unless --stdin)
|
|
12
17
|
* --session <id> Previous session id for multi-turn
|
|
@@ -41,6 +46,7 @@ const fs = require('node:fs');
|
|
|
41
46
|
const path = require('node:path');
|
|
42
47
|
|
|
43
48
|
const runner = require('./runner.js');
|
|
49
|
+
const hub = require('./hub.js');
|
|
44
50
|
const { PROTOCOL_VERSION } = runner;
|
|
45
51
|
|
|
46
52
|
const PKG_VERSION = require('../package.json').version;
|
|
@@ -57,7 +63,7 @@ function parseArgs(argv) {
|
|
|
57
63
|
sessionName: 'default',
|
|
58
64
|
from: '',
|
|
59
65
|
cwd: null,
|
|
60
|
-
env:
|
|
66
|
+
env: [], // array of "KEY=VALUE" strings; --env can repeat
|
|
61
67
|
timeout: null,
|
|
62
68
|
stream: true,
|
|
63
69
|
verbose: true,
|
|
@@ -83,20 +89,21 @@ function parseArgs(argv) {
|
|
|
83
89
|
case '--session-name': opts.sessionName = next(); break;
|
|
84
90
|
case '--from': opts.from = next(); break;
|
|
85
91
|
case '--cwd': opts.cwd = next(); break;
|
|
86
|
-
case '--env':
|
|
87
|
-
|
|
88
|
-
const eq = kv.indexOf('=');
|
|
89
|
-
if (eq < 0) throw new Error(`--env expects KEY=VALUE, got: ${kv}`);
|
|
90
|
-
opts.env[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
92
|
+
case '--env':
|
|
93
|
+
opts.env.push(next());
|
|
91
94
|
break;
|
|
92
|
-
}
|
|
93
95
|
case '--timeout': opts.timeout = parseInt(next(), 10); break;
|
|
94
|
-
case '--no-stream': opts.
|
|
96
|
+
case '--no-stream': opts.noStream = true; break;
|
|
95
97
|
case '--verbose': opts.verbose = true; break;
|
|
96
98
|
case '--quiet': opts.verbose = false; break;
|
|
97
99
|
case '--raw': opts.raw = true; break;
|
|
98
100
|
case '--stdin': opts.stdin = true; break;
|
|
99
101
|
default:
|
|
102
|
+
if (a === 'hub' && extras.length === 0) {
|
|
103
|
+
opts.hub = true;
|
|
104
|
+
opts.hubArgs = argv.slice(i + 1);
|
|
105
|
+
return { opts, extras };
|
|
106
|
+
}
|
|
100
107
|
if (a.startsWith('--')) throw new Error(`unknown option: ${a}`);
|
|
101
108
|
extras.push(a);
|
|
102
109
|
}
|
|
@@ -104,9 +111,288 @@ function parseArgs(argv) {
|
|
|
104
111
|
return { opts, extras };
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Hub subcommand dispatcher
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
async function runHubSubcommand(args) {
|
|
119
|
+
const sub = args[0];
|
|
120
|
+
const rest = args.slice(1);
|
|
121
|
+
|
|
122
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
123
|
+
showHubHelp();
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Any subcommand with --help/-h shows the hub help (covers `hub run --help`,
|
|
128
|
+
// `hub install --help`, etc. — useful for muscle-memory discovery).
|
|
129
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
130
|
+
showHubHelp();
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parse hub args. We can't reuse parseArgs() here because it throws on
|
|
135
|
+
// any unknown `--flag`, and hub supports `--refresh` which the runner
|
|
136
|
+
// parser doesn't know about. Instead, walk the args ourselves, separating
|
|
137
|
+
// hub-level flags (--refresh, --help) from runner-level flags (-p/--prompt,
|
|
138
|
+
// --cwd, --env, ...) and positional args (the profile name).
|
|
139
|
+
const refresh = rest.includes('--refresh');
|
|
140
|
+
const positional = [];
|
|
141
|
+
const runnerArgs = [];
|
|
142
|
+
for (let i = 0; i < rest.length; i++) {
|
|
143
|
+
const a = rest[i];
|
|
144
|
+
if (a === '--refresh' || a === '-h' || a === '--help') continue;
|
|
145
|
+
// Runner flags that take a value (next arg): match both long and short.
|
|
146
|
+
// In the `hub run` context, `-p` means `--prompt` (the profile name is
|
|
147
|
+
// positional, not a path), so we normalize it before handing off.
|
|
148
|
+
const takesValue =
|
|
149
|
+
a === '--prompt' || a === '-p' ||
|
|
150
|
+
a === '--session' || a === '--session-name' || a === '--from' ||
|
|
151
|
+
a === '--cwd' || a === '--env' || a === '--timeout';
|
|
152
|
+
if (takesValue) {
|
|
153
|
+
runnerArgs.push(a === '-p' ? '--prompt' : a);
|
|
154
|
+
if (i + 1 < rest.length) runnerArgs.push(rest[++i]);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Other known runner flags (boolean).
|
|
158
|
+
if (a === '--no-stream' || a === '--verbose' || a === '--quiet' ||
|
|
159
|
+
a === '--raw' || a === '--stdin') {
|
|
160
|
+
runnerArgs.push(a);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Anything else starting with - or -- is unknown to us — surface it
|
|
164
|
+
// as an explicit error instead of silently treating it as a positional.
|
|
165
|
+
if (a.startsWith('-')) {
|
|
166
|
+
process.stderr.write(`error: unknown option: ${a}\n\n`);
|
|
167
|
+
showHubHelp();
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
positional.push(a);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sub === 'list') {
|
|
174
|
+
const profiles = await hub.listProfiles({
|
|
175
|
+
onLog: m => process.stderr.write(m + '\n'),
|
|
176
|
+
});
|
|
177
|
+
process.stdout.write('Available profiles in the official hub:\n\n');
|
|
178
|
+
for (const p of profiles) {
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
` ${p.name.padEnd(15)} ${p.tested.padEnd(12)} ${p.description.slice(0, 60)}\n`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write(`\nRun \`agentproc hub run <name> -p "hi"\` to use one.\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (sub === 'show') {
|
|
188
|
+
if (!positional[0]) {
|
|
189
|
+
process.stderr.write('error: hub show requires a profile name\n');
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
const readme = await hub.showReadme(positional[0], {
|
|
193
|
+
refresh,
|
|
194
|
+
onLog: m => process.stderr.write(m + '\n'),
|
|
195
|
+
});
|
|
196
|
+
process.stdout.write(readme);
|
|
197
|
+
if (!readme.endsWith('\n')) process.stdout.write('\n');
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (sub === 'install') {
|
|
202
|
+
if (!positional[0]) {
|
|
203
|
+
process.stderr.write('error: hub install requires a profile name\n');
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
const target = process.cwd();
|
|
207
|
+
const dest = await hub.installProfile(positional[0], target, {
|
|
208
|
+
refresh,
|
|
209
|
+
onLog: m => process.stderr.write(m + '\n'),
|
|
210
|
+
});
|
|
211
|
+
// After-the-fact hint: tell the user exactly what they got and how to
|
|
212
|
+
// run it. Without this, "installed to: ./echo-agent/" leaves them
|
|
213
|
+
// guessing what to type next.
|
|
214
|
+
process.stderr.write(`\n`);
|
|
215
|
+
process.stderr.write(`Next: edit ${path.relative(target, path.join(dest, 'profile.yaml'))} if you want, then run:\n`);
|
|
216
|
+
process.stderr.write(` agentproc --profile ${path.relative(target, path.join(dest, 'profile.yaml'))} --prompt "hi" --cwd <your-project>\n`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (sub === 'run') {
|
|
221
|
+
if (!positional[0]) {
|
|
222
|
+
process.stderr.write('error: hub run requires a profile name\n');
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
const profileName = positional[0];
|
|
226
|
+
const cacheDir = await hub.fetchProfile(profileName, {
|
|
227
|
+
refresh,
|
|
228
|
+
onLog: m => process.stderr.write(m + '\n'),
|
|
229
|
+
});
|
|
230
|
+
const profilePath = path.join(cacheDir, 'profile.yaml');
|
|
231
|
+
|
|
232
|
+
// Parse the runner-level flags we separated out above.
|
|
233
|
+
const { opts: runOpts } = parseArgs(runnerArgs);
|
|
234
|
+
if (!runOpts.prompt && !runOpts.stdin) {
|
|
235
|
+
process.stderr.write('error: hub run requires --prompt <text> or --stdin\n');
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// hub run uses the user's current directory as the agent's cwd when
|
|
240
|
+
// --cwd is not given. This matches the hub docs ("uses your current
|
|
241
|
+
// directory as cwd") and is the right default for AI-CLI profiles
|
|
242
|
+
// where the agent should operate on the user's project.
|
|
243
|
+
if (!runOpts.cwd) {
|
|
244
|
+
runOpts.cwd = process.cwd();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return await runAgent(profilePath, runOpts);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
process.stderr.write(`error: unknown hub subcommand: ${sub}\n\n`);
|
|
251
|
+
showHubHelp();
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function showHubHelp() {
|
|
256
|
+
process.stdout.write(`agentproc hub — manage profiles from the official Hub
|
|
257
|
+
|
|
258
|
+
Usage:
|
|
259
|
+
agentproc hub list List all profiles in the hub
|
|
260
|
+
agentproc hub show <name> Show a profile's README
|
|
261
|
+
agentproc hub install <name> Copy a profile to the current directory
|
|
262
|
+
agentproc hub run <name> [run-options] Fetch (if needed) and run a profile
|
|
263
|
+
|
|
264
|
+
Hub run options (same as the regular --profile runner):
|
|
265
|
+
-p, --prompt <text> User message (or use --stdin)
|
|
266
|
+
--cwd <path> Override profile.cwd (default: current dir)
|
|
267
|
+
--env KEY=VALUE Extra env var (repeatable)
|
|
268
|
+
--session <id> Previous session id for multi-turn
|
|
269
|
+
--timeout <secs> Override profile.timeout_secs
|
|
270
|
+
--no-stream Disable streaming
|
|
271
|
+
--verbose / --quiet Protocol line visibility (default: verbose)
|
|
272
|
+
--stdin Read prompt from stdin
|
|
273
|
+
|
|
274
|
+
Common options:
|
|
275
|
+
--refresh Force re-fetch from GitHub (ignore cache)
|
|
276
|
+
-h, --help Show this help
|
|
277
|
+
|
|
278
|
+
Examples:
|
|
279
|
+
agentproc hub list
|
|
280
|
+
agentproc hub run echo-agent -p "hello"
|
|
281
|
+
cd ~/projects/my-app && agentproc hub run claude-code -p "explain this" --env ANTHROPIC_API_KEY=$KEY
|
|
282
|
+
agentproc hub show codex
|
|
283
|
+
agentproc hub install agy
|
|
284
|
+
|
|
285
|
+
Profiles are cached at ~/.agentproc/cache/hub/<name>/ (24h TTL).
|
|
286
|
+
`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Shared runner logic used by both `agentproc --profile` and `agentproc hub run`.
|
|
291
|
+
* Kept here for the hub subcommand to reuse; the legacy main() path also calls it.
|
|
292
|
+
*/
|
|
293
|
+
async function runAgent(profilePath, opts) {
|
|
294
|
+
let profileRaw;
|
|
295
|
+
const profileDir = path.dirname(path.resolve(profilePath));
|
|
296
|
+
try {
|
|
297
|
+
const yamlText = fs.readFileSync(path.resolve(profilePath), 'utf8');
|
|
298
|
+
profileRaw = parseYaml(yamlText);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
process.stderr.write(`error: failed to read profile ${profilePath}: ${e.message}\n`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Read prompt.
|
|
305
|
+
let prompt = opts.prompt;
|
|
306
|
+
if (opts.stdin) {
|
|
307
|
+
prompt = fs.readFileSync(0, 'utf8').replace(/\n$/, '');
|
|
308
|
+
}
|
|
309
|
+
if (prompt == null) {
|
|
310
|
+
process.stderr.write('error: --prompt (or --stdin) is required\n');
|
|
311
|
+
return 2;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// opts.env is an array of "KEY=VALUE" strings (from repeated --env flags)
|
|
315
|
+
const extraEnv = {};
|
|
316
|
+
for (const kv of opts.env || []) {
|
|
317
|
+
const eq = kv.indexOf('=');
|
|
318
|
+
if (eq < 0) {
|
|
319
|
+
process.stderr.write(`error: --env expects KEY=VALUE, got: ${kv}\n`);
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
extraEnv[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const streaming = opts.noStream ? false : null;
|
|
326
|
+
|
|
327
|
+
if (opts.raw) {
|
|
328
|
+
const r = await runner.run(profileRaw, {
|
|
329
|
+
message: prompt,
|
|
330
|
+
sessionId: opts.session || '',
|
|
331
|
+
sessionName: opts.sessionName || 'default',
|
|
332
|
+
fromUser: opts.from || '',
|
|
333
|
+
streaming,
|
|
334
|
+
cwd: opts.cwd,
|
|
335
|
+
profileDir,
|
|
336
|
+
extraEnv,
|
|
337
|
+
timeoutSecs: opts.timeout,
|
|
338
|
+
});
|
|
339
|
+
process.stdout.write(r.reply);
|
|
340
|
+
if (r.reply && !r.reply.endsWith('\n')) process.stdout.write('\n');
|
|
341
|
+
return r.exitCode === 0 ? 0 : 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// verbose: default true, --verbose keeps it true, --quiet sets it false.
|
|
345
|
+
const verbose = opts.verbose !== false;
|
|
346
|
+
|
|
347
|
+
const r = await runner.run(profileRaw, {
|
|
348
|
+
message: prompt,
|
|
349
|
+
sessionId: opts.session || '',
|
|
350
|
+
sessionName: opts.sessionName || 'default',
|
|
351
|
+
fromUser: opts.from || '',
|
|
352
|
+
streaming,
|
|
353
|
+
cwd: opts.cwd,
|
|
354
|
+
profileDir,
|
|
355
|
+
extraEnv,
|
|
356
|
+
timeoutSecs: opts.timeout,
|
|
357
|
+
onPartial: (t) => { if (verbose) process.stderr.write(`AGENT_PARTIAL:${JSON.stringify(t)}\n`); },
|
|
358
|
+
onSession: (id) => { if (verbose) process.stderr.write(`AGENT_SESSION:${id}\n`); },
|
|
359
|
+
onError: (msg) => { if (verbose) process.stderr.write(`AGENT_ERROR:${JSON.stringify(msg)}\n`); },
|
|
360
|
+
onStderr: (line) => { if (verbose) process.stderr.write(`[agent stderr] ${line}\n`); },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (r.reply) {
|
|
364
|
+
process.stdout.write(r.reply);
|
|
365
|
+
if (!r.reply.endsWith('\n')) process.stdout.write('\n');
|
|
366
|
+
}
|
|
367
|
+
if (r.sessionId) process.stderr.write(`agentproc:session:${r.sessionId}\n`);
|
|
368
|
+
if (r.error) process.stderr.write(`agentproc:error:${r.error}\n`);
|
|
369
|
+
return r.exitCode === 0 ? 0 : 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
107
372
|
function showHelp() {
|
|
108
373
|
process.stdout.write(`agentproc v${PKG_VERSION} (protocol ${PROTOCOL_VERSION})
|
|
109
374
|
|
|
375
|
+
The fastest way in:
|
|
376
|
+
agentproc hub list # see what's available
|
|
377
|
+
agentproc hub run echo-agent -p "hello" # smoke test (no API key)
|
|
378
|
+
cd ~/projects/my-app && agentproc hub run claude-code -p "explain this"
|
|
379
|
+
|
|
380
|
+
The CLI fetches the profile from the GitHub hub on first use, caches it at
|
|
381
|
+
~/.agentproc/cache/hub/<name>/ (24h TTL), and uses your current directory as
|
|
382
|
+
the agent's cwd. Set GITHUB_TOKEN to raise the rate limit (see \`agentproc hub --help\`).
|
|
383
|
+
|
|
384
|
+
Hub subcommands:
|
|
385
|
+
hub list List all profiles in the hub
|
|
386
|
+
hub show <name> Show a profile's README
|
|
387
|
+
hub run <name> [run-options] Fetch (if needed) and run a profile
|
|
388
|
+
hub install <name> Copy a profile to the current directory
|
|
389
|
+
|
|
390
|
+
Run \`agentproc hub --help\` for the full hub reference.
|
|
391
|
+
|
|
392
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
Advanced: run a local profile YAML directly (no hub fetch)
|
|
395
|
+
|
|
110
396
|
Usage:
|
|
111
397
|
agentproc --profile <path.yaml> --prompt "hello" [options]
|
|
112
398
|
|
|
@@ -120,7 +406,8 @@ Session:
|
|
|
120
406
|
--from <user> Sender identifier
|
|
121
407
|
|
|
122
408
|
Execution:
|
|
123
|
-
--cwd <path> Override profile.cwd
|
|
409
|
+
--cwd <path> Override profile.cwd (relative paths resolve
|
|
410
|
+
against the profile.yaml's directory)
|
|
124
411
|
--env KEY=VALUE Extra env var (repeatable)
|
|
125
412
|
--timeout <secs> Override profile.timeout_secs
|
|
126
413
|
--no-stream Set AGENT_STREAMING=0
|
|
@@ -143,9 +430,16 @@ Output semantics:
|
|
|
143
430
|
The final session id is printed on stderr as: agentproc:session:<id>
|
|
144
431
|
|
|
145
432
|
Examples:
|
|
146
|
-
|
|
147
|
-
agentproc
|
|
148
|
-
|
|
433
|
+
# Local profile (relative cwd resolves next to profile.yaml):
|
|
434
|
+
agentproc --profile ./hub/echo-agent/profile.yaml --prompt "hi"
|
|
435
|
+
|
|
436
|
+
# Local claude-code profile, claude runs against your project:
|
|
437
|
+
agentproc --profile ./hub/claude-code/profile.yaml \\
|
|
438
|
+
--prompt "explain this codebase" \\
|
|
439
|
+
--cwd /path/to/your/project
|
|
440
|
+
|
|
441
|
+
# Prompt from stdin:
|
|
442
|
+
cat prompt.txt | agentproc --profile prof.yaml --stdin
|
|
149
443
|
`);
|
|
150
444
|
}
|
|
151
445
|
|
|
@@ -392,6 +686,11 @@ async function main() {
|
|
|
392
686
|
if (opts.help) { showHelp(); process.exit(0); }
|
|
393
687
|
if (opts.version) { showVersion(); process.exit(0); }
|
|
394
688
|
|
|
689
|
+
// `agentproc hub <subcommand>` — defer to hub dispatcher.
|
|
690
|
+
if (opts.hub) {
|
|
691
|
+
return await runHubSubcommand(opts.hubArgs);
|
|
692
|
+
}
|
|
693
|
+
|
|
395
694
|
if (!opts.profile) {
|
|
396
695
|
process.stderr.write('error: --profile is required\n\n');
|
|
397
696
|
showHelp();
|
|
@@ -408,98 +707,46 @@ async function main() {
|
|
|
408
707
|
process.exit(2);
|
|
409
708
|
}
|
|
410
709
|
|
|
411
|
-
// Read & parse profile YAML.
|
|
412
|
-
let profileRaw;
|
|
710
|
+
// Read & parse profile YAML, then delegate to the shared runner path.
|
|
413
711
|
try {
|
|
414
|
-
|
|
415
|
-
profileRaw = parseYaml(yamlText);
|
|
416
|
-
} catch (e) {
|
|
417
|
-
process.stderr.write(`error: failed to read profile ${opts.profile}: ${e.message}\n`);
|
|
418
|
-
process.exit(2);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// ---- Raw mode: spawn agent, pipe stdout through, exit with agent code ----
|
|
422
|
-
if (opts.raw) {
|
|
423
|
-
const { spawn } = require('node:child_process');
|
|
424
|
-
try {
|
|
425
|
-
const r = await runner.run(profileRaw, {
|
|
426
|
-
message: prompt,
|
|
427
|
-
sessionId: opts.session,
|
|
428
|
-
sessionName: opts.sessionName,
|
|
429
|
-
fromUser: opts.from,
|
|
430
|
-
streaming: opts.stream,
|
|
431
|
-
cwd: opts.cwd,
|
|
432
|
-
extraEnv: opts.env,
|
|
433
|
-
timeoutSecs: opts.timeout,
|
|
434
|
-
// No callbacks — we replace stdout forwarding below.
|
|
435
|
-
});
|
|
436
|
-
// The runner buffers reply; for raw mode we want streaming verbatim,
|
|
437
|
-
// so we re-implement with raw pipes. Simpler: just print the reply
|
|
438
|
-
// (which equals the agent's stdout minus protocol lines).
|
|
439
|
-
// For TRUE raw output (including protocol lines), users should use
|
|
440
|
-
// the bridge script directly.
|
|
441
|
-
process.stdout.write(r.reply);
|
|
442
|
-
if (r.reply && !r.reply.endsWith('\n')) process.stdout.write('\n');
|
|
443
|
-
process.exit(r.exitCode === 0 ? 0 : 1);
|
|
444
|
-
} catch (e) {
|
|
445
|
-
process.stderr.write(`error: ${e.message}\n`);
|
|
446
|
-
process.exit(1);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// ---- Default mode: classify and pretty-print ----
|
|
451
|
-
try {
|
|
452
|
-
const r = await runner.run(profileRaw, {
|
|
453
|
-
message: prompt,
|
|
454
|
-
sessionId: opts.session,
|
|
455
|
-
sessionName: opts.sessionName,
|
|
456
|
-
fromUser: opts.from,
|
|
457
|
-
streaming: opts.stream,
|
|
458
|
-
cwd: opts.cwd,
|
|
459
|
-
extraEnv: opts.env,
|
|
460
|
-
timeoutSecs: opts.timeout,
|
|
461
|
-
onPartial: (text) => {
|
|
462
|
-
if (opts.verbose) process.stderr.write(`AGENT_PARTIAL:${JSON.stringify(text)}\n`);
|
|
463
|
-
},
|
|
464
|
-
onSession: (id) => {
|
|
465
|
-
if (opts.verbose) process.stderr.write(`AGENT_SESSION:${id}\n`);
|
|
466
|
-
},
|
|
467
|
-
onError: (msg) => {
|
|
468
|
-
if (opts.verbose) process.stderr.write(`AGENT_ERROR:${JSON.stringify(msg)}\n`);
|
|
469
|
-
},
|
|
470
|
-
onStderr: (line) => {
|
|
471
|
-
if (opts.verbose) process.stderr.write(`[agent stderr] ${line}\n`);
|
|
472
|
-
},
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// Print final reply body to stdout.
|
|
476
|
-
if (r.reply) {
|
|
477
|
-
process.stdout.write(r.reply);
|
|
478
|
-
if (!r.reply.endsWith('\n')) process.stdout.write('\n');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Final session id on stderr (for shell capture).
|
|
482
|
-
if (r.sessionId) {
|
|
483
|
-
process.stderr.write(`agentproc:session:${r.sessionId}\n`);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (r.error) {
|
|
487
|
-
process.stderr.write(`agentproc:error:${r.error}\n`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
process.exit(r.exitCode === 0 ? 0 : 1);
|
|
712
|
+
return await runAgent(opts.profile, opts);
|
|
491
713
|
} catch (e) {
|
|
492
714
|
process.stderr.write(`error: ${e.message}\n`);
|
|
493
|
-
|
|
715
|
+
return 1;
|
|
494
716
|
}
|
|
495
717
|
}
|
|
496
718
|
|
|
497
719
|
// Run main() only when invoked directly as a script, not when required for tests.
|
|
498
720
|
if (require.main === module) {
|
|
499
|
-
main().
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
721
|
+
main().then(
|
|
722
|
+
(code) => {
|
|
723
|
+
// main() returns an explicit exit code from its various return paths;
|
|
724
|
+
// honor it so shell scripts can distinguish success from failure.
|
|
725
|
+
process.exit(typeof code === 'number' ? code : 0);
|
|
726
|
+
},
|
|
727
|
+
(e) => {
|
|
728
|
+
// Friendly handling for known hub errors: print the message + remediation
|
|
729
|
+
// hint, never a raw Node stack trace.
|
|
730
|
+
if (e && e.name === 'HubError') {
|
|
731
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
732
|
+
if (e.hint) process.stderr.write(`\n${e.hint}\n`);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// For fetch() network errors wrapped by hub.js (also HubError, but be
|
|
737
|
+
// defensive in case some path throws a plain TypeError from fetch).
|
|
738
|
+
if (e && typeof e.message === 'string' && /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET/.test(e.message)) {
|
|
739
|
+
process.stderr.write(`error: network error talking to GitHub: ${e.message}\n`);
|
|
740
|
+
process.stderr.write(`\nThis is usually transient. Re-run the command, or run against a local checkout:\n`);
|
|
741
|
+
process.stderr.write(` agentproc --profile ./hub/<name>/profile.yaml --prompt "hi"\n`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Everything else: still avoid dumping the stack. Show the message only.
|
|
746
|
+
process.stderr.write(`error: ${e && (e.message || e)}\n`);
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
);
|
|
503
750
|
}
|
|
504
751
|
|
|
505
752
|
module.exports = { parseArgs, parseYaml, showHelp, main };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-implementation conformance tests.
|
|
4
|
+
*
|
|
5
|
+
* Drives the shared `spec/conformance/cases.json` fixture through the Node
|
|
6
|
+
* runner's `classifyLine` and asserts the result matches the expected
|
|
7
|
+
* {kind, value}. The Python SDK runs the same fixture through its
|
|
8
|
+
* `classify_line` in `sdk/python/tests/test_conformance.py` — together they
|
|
9
|
+
* guarantee the two reference implementations classify stdout identically.
|
|
10
|
+
*
|
|
11
|
+
* When you change the spec's line-recognition rules, add a case to the JSON
|
|
12
|
+
* file first; both SDKs will fail until they agree.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { test } = require('node:test');
|
|
16
|
+
const assert = require('node:assert');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const { classifyLine } = require('./runner.js');
|
|
21
|
+
|
|
22
|
+
const CASES_PATH = path.resolve(__dirname, '../../../spec/conformance/cases.json');
|
|
23
|
+
const data = JSON.parse(fs.readFileSync(CASES_PATH, 'utf8'));
|
|
24
|
+
|
|
25
|
+
for (const c of data.cases) {
|
|
26
|
+
test(`classifyLine: ${c.line.slice(0, 60)}`, () => {
|
|
27
|
+
assert.deepStrictEqual(classifyLine(c.line), c.expect);
|
|
28
|
+
});
|
|
29
|
+
}
|