@prohost/cli 0.6.0 → 0.8.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/CHANGELOG.md +96 -0
- package/README.md +51 -0
- package/dist/agent/account_runtime.d.ts +75 -0
- package/dist/agent/account_runtime.js +130 -0
- package/dist/agent/accounts.d.ts +162 -0
- package/dist/agent/accounts.js +413 -0
- package/dist/agent/agent_commands.d.ts +22 -0
- package/dist/agent/agent_commands.js +170 -0
- package/dist/agent/api.d.ts +12 -0
- package/dist/agent/api.js +14 -2
- package/dist/agent/claude.d.ts +35 -0
- package/dist/agent/claude.js +205 -0
- package/dist/agent/command.d.ts +18 -1
- package/dist/agent/command.js +88 -4
- package/dist/agent/contract.d.ts +79 -0
- package/dist/agent/contract.js +51 -0
- package/dist/agent/credentials.d.ts +5 -0
- package/dist/agent/events.d.ts +104 -0
- package/dist/agent/events.js +175 -0
- package/dist/agent/list.d.ts +41 -0
- package/dist/agent/list.js +126 -0
- package/dist/agent/lock.d.ts +37 -0
- package/dist/agent/lock.js +91 -0
- package/dist/agent/prompt.js +29 -7
- package/dist/agent/run.d.ts +8 -2
- package/dist/agent/run.js +153 -17
- package/dist/agent/runtime_status.d.ts +70 -4
- package/dist/agent/runtime_status.js +145 -20
- package/dist/agent/workspace.d.ts +40 -0
- package/dist/agent/workspace.js +97 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -5
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent/claude.d.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* session is unresumable, if the JSON is not what we expect — the run still
|
|
15
15
|
* happens and still completes. These are enhancements that must fail soft.
|
|
16
16
|
*/
|
|
17
|
+
import type { ToolEvent } from './contract.js';
|
|
17
18
|
import { MCP_SERVER_NAME } from './mcp.js';
|
|
18
19
|
import type { RuntimeObservation } from './runtime_status.js';
|
|
19
20
|
export { MCP_SERVER_NAME };
|
|
21
|
+
export type { ToolEvent };
|
|
20
22
|
/**
|
|
21
23
|
* What goes to `--allowedTools` so the ProhostAI tools are callable.
|
|
22
24
|
*
|
|
@@ -126,6 +128,13 @@ export declare function parseClaudeOutput(stdout: string): ClaudeOutput;
|
|
|
126
128
|
export declare const MAX_ACTIVITY_CHARS = 120;
|
|
127
129
|
/** Cap on the partial answer salvaged from a run that was killed. */
|
|
128
130
|
export declare const MAX_SALVAGE_CHARS = 500;
|
|
131
|
+
/**
|
|
132
|
+
* Cap on the one-line previews a tool event carries — a headline argument,
|
|
133
|
+
* the head of a result. The server clips again and redacts before showing
|
|
134
|
+
* anything to a person; this bound just keeps a `Read` of a large file from
|
|
135
|
+
* putting the file on the wire.
|
|
136
|
+
*/
|
|
137
|
+
export declare const MAX_TOOL_EVENT_PREVIEW_CHARS = 200;
|
|
129
138
|
/**
|
|
130
139
|
* A run's stdout, read as it is produced.
|
|
131
140
|
*
|
|
@@ -152,6 +161,16 @@ export interface ClaudeStream {
|
|
|
152
161
|
activity(): string | undefined;
|
|
153
162
|
/** The tail of the last thing the agent wrote, for a run that was killed. */
|
|
154
163
|
salvage(): string | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* Tool-call starts and ends seen since the last drain, in stream order.
|
|
166
|
+
*
|
|
167
|
+
* Each `tool_use` block becomes a `calling` event and each `tool_result`
|
|
168
|
+
* block the matching `completed` / `failed`, correlated by Claude Code's
|
|
169
|
+
* own `tool_use.id`. Taken, not copied: a second call returns only what
|
|
170
|
+
* arrived in between. Empty for a stream with no tool calls, and for one
|
|
171
|
+
* that isn't Claude Code's at all.
|
|
172
|
+
*/
|
|
173
|
+
drainToolEvents(): ToolEvent[];
|
|
155
174
|
/**
|
|
156
175
|
* Model, Claude Code version and capacity windows the stream announced —
|
|
157
176
|
* `system/init.{model, claude_code_version}` and the last
|
|
@@ -165,6 +184,22 @@ export interface ClaudeStream {
|
|
|
165
184
|
*/
|
|
166
185
|
finish(stdout: string): ClaudeOutput;
|
|
167
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* What a tool was called with, as a small JSON object the server can render.
|
|
189
|
+
*
|
|
190
|
+
* For the tools whose input has an obvious headline — the command, the path,
|
|
191
|
+
* the pattern, the URL — that field alone; the rest of the input (a `Write`'s
|
|
192
|
+
* whole file content, an `Edit`'s old and new strings) stays on this machine.
|
|
193
|
+
* Anything else is a shallow projection of the input: the first few keys,
|
|
194
|
+
* scalar values capped, lists and objects reduced to their shape — which for
|
|
195
|
+
* an MCP tool is a handful of ids and short strings.
|
|
196
|
+
*
|
|
197
|
+
* Always a JSON **object** string, never bare text: the run-detail page
|
|
198
|
+
* parses the preview as JSON and renders it `key=value` through a key-aware
|
|
199
|
+
* redactor (so a `token` key is masked outright), and a preview it cannot
|
|
200
|
+
* parse is shown as nothing at all.
|
|
201
|
+
*/
|
|
202
|
+
export declare function toolArgsPreview(name: string, input: unknown): string | undefined;
|
|
168
203
|
/**
|
|
169
204
|
* Parse `--output-format stream-json` incrementally.
|
|
170
205
|
*
|
package/dist/agent/claude.js
CHANGED
|
@@ -269,6 +269,21 @@ export function parseClaudeOutput(stdout) {
|
|
|
269
269
|
export const MAX_ACTIVITY_CHARS = 120;
|
|
270
270
|
/** Cap on the partial answer salvaged from a run that was killed. */
|
|
271
271
|
export const MAX_SALVAGE_CHARS = 500;
|
|
272
|
+
/**
|
|
273
|
+
* Cap on the one-line previews a tool event carries — a headline argument,
|
|
274
|
+
* the head of a result. The server clips again and redacts before showing
|
|
275
|
+
* anything to a person; this bound just keeps a `Read` of a large file from
|
|
276
|
+
* putting the file on the wire.
|
|
277
|
+
*/
|
|
278
|
+
export const MAX_TOOL_EVENT_PREVIEW_CHARS = 200;
|
|
279
|
+
/**
|
|
280
|
+
* Cap on tool events held between drains.
|
|
281
|
+
*
|
|
282
|
+
* The reader is drained on every stdout chunk, so this only ever fills if
|
|
283
|
+
* nobody is draining — a caller that doesn't report events at all. Oldest
|
|
284
|
+
* dropped, so memory stays bounded for the whole of a long run.
|
|
285
|
+
*/
|
|
286
|
+
const MAX_PENDING_TOOL_EVENTS = 500;
|
|
272
287
|
/**
|
|
273
288
|
* Cap on a single buffered stdout line.
|
|
274
289
|
*
|
|
@@ -301,6 +316,116 @@ function assistantContent(event) {
|
|
|
301
316
|
}
|
|
302
317
|
return { text: latestText, tool: latestTool };
|
|
303
318
|
}
|
|
319
|
+
/** Whitespace-collapsed, trimmed, capped — or nothing, for nothing. */
|
|
320
|
+
function preview(text) {
|
|
321
|
+
const collapsed = text?.replace(/\s+/g, ' ').trim();
|
|
322
|
+
if (!collapsed)
|
|
323
|
+
return undefined;
|
|
324
|
+
return collapsed.length > MAX_TOOL_EVENT_PREVIEW_CHARS
|
|
325
|
+
? collapsed.slice(0, MAX_TOOL_EVENT_PREVIEW_CHARS)
|
|
326
|
+
: collapsed;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Cap on one value inside a JSON args preview, and on how many keys travel.
|
|
330
|
+
* The server's key-aware redactor reads the preview as an object, so the
|
|
331
|
+
* shape must survive intact: capping values (not the JSON) is what keeps a
|
|
332
|
+
* large MCP argument from truncating the object mid-key into something the
|
|
333
|
+
* reader can no longer parse.
|
|
334
|
+
*/
|
|
335
|
+
const MAX_PREVIEW_VALUE_CHARS = 120;
|
|
336
|
+
const MAX_PREVIEW_KEYS = 6;
|
|
337
|
+
/**
|
|
338
|
+
* What a tool was called with, as a small JSON object the server can render.
|
|
339
|
+
*
|
|
340
|
+
* For the tools whose input has an obvious headline — the command, the path,
|
|
341
|
+
* the pattern, the URL — that field alone; the rest of the input (a `Write`'s
|
|
342
|
+
* whole file content, an `Edit`'s old and new strings) stays on this machine.
|
|
343
|
+
* Anything else is a shallow projection of the input: the first few keys,
|
|
344
|
+
* scalar values capped, lists and objects reduced to their shape — which for
|
|
345
|
+
* an MCP tool is a handful of ids and short strings.
|
|
346
|
+
*
|
|
347
|
+
* Always a JSON **object** string, never bare text: the run-detail page
|
|
348
|
+
* parses the preview as JSON and renders it `key=value` through a key-aware
|
|
349
|
+
* redactor (so a `token` key is masked outright), and a preview it cannot
|
|
350
|
+
* parse is shown as nothing at all.
|
|
351
|
+
*/
|
|
352
|
+
export function toolArgsPreview(name, input) {
|
|
353
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
354
|
+
return undefined;
|
|
355
|
+
const args = input;
|
|
356
|
+
const headline = HEADLINE_FIELDS[name];
|
|
357
|
+
if (headline) {
|
|
358
|
+
for (const key of headline) {
|
|
359
|
+
const value = args[key];
|
|
360
|
+
const text = typeof value === 'string' ? preview(value) : undefined;
|
|
361
|
+
if (text)
|
|
362
|
+
return JSON.stringify({ [key]: text });
|
|
363
|
+
}
|
|
364
|
+
// A headline-only tool with no headline shows nothing rather than its
|
|
365
|
+
// whole input — that input is the file content this field exists to keep
|
|
366
|
+
// off the wire.
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
const projected = {};
|
|
370
|
+
for (const key of Object.keys(args).slice(0, MAX_PREVIEW_KEYS)) {
|
|
371
|
+
const value = args[key];
|
|
372
|
+
if (typeof value === 'string') {
|
|
373
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
374
|
+
projected[key] =
|
|
375
|
+
collapsed.length > MAX_PREVIEW_VALUE_CHARS ? collapsed.slice(0, MAX_PREVIEW_VALUE_CHARS) : collapsed;
|
|
376
|
+
}
|
|
377
|
+
else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
|
|
378
|
+
projected[key] = value;
|
|
379
|
+
}
|
|
380
|
+
else if (Array.isArray(value)) {
|
|
381
|
+
projected[key] = `[${value.length} items]`;
|
|
382
|
+
}
|
|
383
|
+
else if (value && typeof value === 'object') {
|
|
384
|
+
projected[key] = `{${Object.keys(value).length} keys}`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
return JSON.stringify(projected);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The field that IS the call, per tool, in order of preference. Listed tools
|
|
396
|
+
* never fall through to the projection above.
|
|
397
|
+
*/
|
|
398
|
+
const HEADLINE_FIELDS = {
|
|
399
|
+
Bash: ['command'],
|
|
400
|
+
shell: ['command'],
|
|
401
|
+
Read: ['file_path'],
|
|
402
|
+
Edit: ['file_path'],
|
|
403
|
+
MultiEdit: ['file_path'],
|
|
404
|
+
Write: ['file_path'],
|
|
405
|
+
NotebookEdit: ['notebook_path', 'file_path'],
|
|
406
|
+
Grep: ['pattern'],
|
|
407
|
+
Glob: ['pattern'],
|
|
408
|
+
WebFetch: ['url'],
|
|
409
|
+
WebSearch: ['query'],
|
|
410
|
+
Task: ['description'],
|
|
411
|
+
Agent: ['description'],
|
|
412
|
+
};
|
|
413
|
+
/** The text of a `tool_result` block — a string, or its `text` parts joined. */
|
|
414
|
+
function toolResultText(content) {
|
|
415
|
+
if (typeof content === 'string')
|
|
416
|
+
return content;
|
|
417
|
+
if (!Array.isArray(content))
|
|
418
|
+
return undefined;
|
|
419
|
+
const parts = [];
|
|
420
|
+
for (const block of content) {
|
|
421
|
+
if (!block || typeof block !== 'object')
|
|
422
|
+
continue;
|
|
423
|
+
const entry = block;
|
|
424
|
+
if (entry.type === 'text' && typeof entry.text === 'string')
|
|
425
|
+
parts.push(entry.text);
|
|
426
|
+
}
|
|
427
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
428
|
+
}
|
|
304
429
|
/**
|
|
305
430
|
* Parse `--output-format stream-json` incrementally.
|
|
306
431
|
*
|
|
@@ -316,6 +441,74 @@ export function readClaudeStream() {
|
|
|
316
441
|
let lastActivity;
|
|
317
442
|
let lastText;
|
|
318
443
|
let result;
|
|
444
|
+
let toolEvents = [];
|
|
445
|
+
// `tool_use.id` → tool name, so a result can be reported under the name of
|
|
446
|
+
// the call it answers. Deleted on the result: bounded by tools in flight.
|
|
447
|
+
const toolNames = new Map();
|
|
448
|
+
const enqueue = (event) => {
|
|
449
|
+
toolEvents.push(event);
|
|
450
|
+
if (toolEvents.length > MAX_PENDING_TOOL_EVENTS)
|
|
451
|
+
toolEvents.shift();
|
|
452
|
+
};
|
|
453
|
+
/** Every `tool_use` block in an assistant message → a `calling` event. */
|
|
454
|
+
const collectToolUses = (event) => {
|
|
455
|
+
const message = event.message;
|
|
456
|
+
if (!message || typeof message !== 'object')
|
|
457
|
+
return;
|
|
458
|
+
const content = message.content;
|
|
459
|
+
if (!Array.isArray(content))
|
|
460
|
+
return;
|
|
461
|
+
for (const block of content) {
|
|
462
|
+
if (!block || typeof block !== 'object')
|
|
463
|
+
continue;
|
|
464
|
+
const entry = block;
|
|
465
|
+
if (entry.type !== 'tool_use')
|
|
466
|
+
continue;
|
|
467
|
+
const id = text(entry.id);
|
|
468
|
+
const name = text(entry.name);
|
|
469
|
+
if (!id || !name)
|
|
470
|
+
continue;
|
|
471
|
+
toolNames.set(id, name);
|
|
472
|
+
const args = toolArgsPreview(name, entry.input);
|
|
473
|
+
enqueue({
|
|
474
|
+
tool_id: id,
|
|
475
|
+
tool_name: name,
|
|
476
|
+
status: 'calling',
|
|
477
|
+
started_at: new Date().toISOString(),
|
|
478
|
+
...(args ? { args_preview: args } : {}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/** Every `tool_result` block in a user message → the matching terminal event. */
|
|
483
|
+
const collectToolResults = (event) => {
|
|
484
|
+
const message = event.message;
|
|
485
|
+
if (!message || typeof message !== 'object')
|
|
486
|
+
return;
|
|
487
|
+
const content = message.content;
|
|
488
|
+
if (!Array.isArray(content))
|
|
489
|
+
return;
|
|
490
|
+
for (const block of content) {
|
|
491
|
+
if (!block || typeof block !== 'object')
|
|
492
|
+
continue;
|
|
493
|
+
const entry = block;
|
|
494
|
+
if (entry.type !== 'tool_result')
|
|
495
|
+
continue;
|
|
496
|
+
const id = text(entry.tool_use_id);
|
|
497
|
+
if (!id)
|
|
498
|
+
continue;
|
|
499
|
+
const name = toolNames.get(id) ?? 'unknown';
|
|
500
|
+
toolNames.delete(id);
|
|
501
|
+
const summary = preview(toolResultText(entry.content));
|
|
502
|
+
const failed = entry.is_error === true;
|
|
503
|
+
enqueue({
|
|
504
|
+
tool_id: id,
|
|
505
|
+
tool_name: name,
|
|
506
|
+
status: failed ? 'failed' : 'completed',
|
|
507
|
+
ended_at: new Date().toISOString(),
|
|
508
|
+
...(summary ? (failed ? { error_summary: summary } : { result_summary: summary }) : {}),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
};
|
|
319
512
|
const observed = {};
|
|
320
513
|
const handle = (line) => {
|
|
321
514
|
const event = asObject(line);
|
|
@@ -339,8 +532,15 @@ export function readClaudeStream() {
|
|
|
339
532
|
result = event;
|
|
340
533
|
return;
|
|
341
534
|
}
|
|
535
|
+
if (event.type === 'user') {
|
|
536
|
+
// The harness's own prompt arrives as a `user` event too; it carries no
|
|
537
|
+
// `tool_result` blocks and falls straight through.
|
|
538
|
+
collectToolResults(event);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
342
541
|
if (event.type !== 'assistant')
|
|
343
542
|
return;
|
|
543
|
+
collectToolUses(event);
|
|
344
544
|
const { text: written, tool } = assistantContent(event);
|
|
345
545
|
if (written)
|
|
346
546
|
lastText = written;
|
|
@@ -371,6 +571,11 @@ export function readClaudeStream() {
|
|
|
371
571
|
},
|
|
372
572
|
sessionId: () => session,
|
|
373
573
|
activity: () => lastActivity,
|
|
574
|
+
drainToolEvents: () => {
|
|
575
|
+
const drained = toolEvents;
|
|
576
|
+
toolEvents = [];
|
|
577
|
+
return drained;
|
|
578
|
+
},
|
|
374
579
|
observed: () => ({ ...observed }),
|
|
375
580
|
salvage: () => {
|
|
376
581
|
const trimmed = lastText?.trim();
|
package/dist/agent/command.d.ts
CHANGED
|
@@ -29,6 +29,23 @@ export declare const INVALID_DURATION: unique symbol;
|
|
|
29
29
|
* had not.
|
|
30
30
|
*/
|
|
31
31
|
export declare function parseSeconds(raw: string | boolean | undefined): number | undefined | typeof INVALID_DURATION;
|
|
32
|
+
/**
|
|
33
|
+
* Environment the daemon must be given explicitly.
|
|
34
|
+
*
|
|
35
|
+
* launchd starts a job with almost nothing — notably a minimal `PATH` of
|
|
36
|
+
* `/usr/bin:/bin:/usr/sbin:/sbin`, which contains no `claude`. Installed from
|
|
37
|
+
* npm, Homebrew, or the official installer it lives in `~/.local/bin`,
|
|
38
|
+
* `/opt/homebrew/bin`, or `/usr/local/bin`, so without this the daemon
|
|
39
|
+
* installs cleanly, reports success, and then crash-loops on
|
|
40
|
+
* `claude: command not found` in a log nobody is watching.
|
|
41
|
+
*
|
|
42
|
+
* The `PATH` captured is the one in effect when the operator ran
|
|
43
|
+
* `install-daemon` — i.e. the one in which their `--exec` command actually
|
|
44
|
+
* works. `PROHOST_HOME` is pinned for the same reason it is used for the label:
|
|
45
|
+
* a daemon must keep serving the home it was installed from, whatever the
|
|
46
|
+
* environment of a later login happens to say.
|
|
47
|
+
*/
|
|
48
|
+
export declare function daemonEnvironment(): Record<string, string>;
|
|
32
49
|
/** Dispatch an ``agent`` subcommand. Returns the process exit code. */
|
|
33
|
-
export declare function runAgent(subcommand: string | undefined, flags: Flags, printHelp: () => void): Promise<number>;
|
|
50
|
+
export declare function runAgent(subcommand: string | undefined, flags: Flags, printHelp: () => void, args?: string[]): Promise<number>;
|
|
34
51
|
export {};
|
package/dist/agent/command.js
CHANGED
|
@@ -10,8 +10,11 @@ import path from 'node:path';
|
|
|
10
10
|
import process from 'node:process';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { agentLogPath, buildSystemdUnit, harnessArguments, installLaunchdDaemon, launchdLabel, uninstallLaunchdDaemon, } from './daemon.js';
|
|
13
|
+
import { accountCommand, listCommand, workspaceCommand } from './agent_commands.js';
|
|
14
|
+
import { AccountError, loadAccounts, setAgentAccount } from './accounts.js';
|
|
15
|
+
import { acquireRunLock } from './lock.js';
|
|
13
16
|
import { PairError, runPair } from './pair.js';
|
|
14
|
-
import { CredentialsMissingError, ensureWorkspace, loadCredentials, prohostHome } from './credentials.js';
|
|
17
|
+
import { CredentialsMissingError, ensureWorkspace, loadCredentials, prohostHome, tryLoadCredentials, } from './credentials.js';
|
|
15
18
|
import { DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_IDLE_TIMEOUT_MS, runAgentHarness } from './run.js';
|
|
16
19
|
import { announceUpgradeIfAvailable } from '../upgrade.js';
|
|
17
20
|
function flagString(flags, name) {
|
|
@@ -132,10 +135,14 @@ function parseIdleTimeoutSeconds(flags) {
|
|
|
132
135
|
* a daemon must keep serving the home it was installed from, whatever the
|
|
133
136
|
* environment of a later login happens to say.
|
|
134
137
|
*/
|
|
135
|
-
function daemonEnvironment() {
|
|
138
|
+
export function daemonEnvironment() {
|
|
136
139
|
const env = { PROHOST_HOME: prohostHome() };
|
|
137
140
|
if (process.env.PATH)
|
|
138
141
|
env.PATH = process.env.PATH;
|
|
142
|
+
// A registry moved with $PROHOST_ACCOUNTS_ROOT must be the one the daemon
|
|
143
|
+
// reads, or it can't find the account it was installed with.
|
|
144
|
+
if (process.env.PROHOST_ACCOUNTS_ROOT)
|
|
145
|
+
env.PROHOST_ACCOUNTS_ROOT = process.env.PROHOST_ACCOUNTS_ROOT;
|
|
139
146
|
return env;
|
|
140
147
|
}
|
|
141
148
|
/** Absolute path of this CLI's entrypoint, for a daemon to exec. */
|
|
@@ -147,7 +154,7 @@ function cliEntrypoint() {
|
|
|
147
154
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
|
|
148
155
|
}
|
|
149
156
|
/** Dispatch an ``agent`` subcommand. Returns the process exit code. */
|
|
150
|
-
export async function runAgent(subcommand, flags, printHelp) {
|
|
157
|
+
export async function runAgent(subcommand, flags, printHelp, args = []) {
|
|
151
158
|
if (subcommand === 'pair')
|
|
152
159
|
return pairCommand(flags);
|
|
153
160
|
if (subcommand === 'run')
|
|
@@ -156,9 +163,16 @@ export async function runAgent(subcommand, flags, printHelp) {
|
|
|
156
163
|
return installDaemonCommand(flags);
|
|
157
164
|
if (subcommand === 'uninstall-daemon')
|
|
158
165
|
return uninstallDaemonCommand();
|
|
166
|
+
if (subcommand === 'account')
|
|
167
|
+
return accountCommand(args, flags);
|
|
168
|
+
if (subcommand === 'list')
|
|
169
|
+
return listCommand();
|
|
170
|
+
if (subcommand === 'workspace')
|
|
171
|
+
return workspaceCommand(args, flags);
|
|
159
172
|
process.stderr.write(subcommand
|
|
160
173
|
? `Unknown agent subcommand: ${subcommand}\n\n`
|
|
161
|
-
: 'Missing agent subcommand — expected `pair`, `run`, `install-daemon`,
|
|
174
|
+
: 'Missing agent subcommand — expected `pair`, `run`, `install-daemon`, `uninstall-daemon`, ' +
|
|
175
|
+
'`account`, `list`, or `workspace`.\n\n');
|
|
162
176
|
printHelp();
|
|
163
177
|
return 1;
|
|
164
178
|
}
|
|
@@ -184,6 +198,8 @@ async function installDaemonCommand(flags) {
|
|
|
184
198
|
const credentials = loadOrExplain();
|
|
185
199
|
if (!credentials)
|
|
186
200
|
return 1;
|
|
201
|
+
if (!applyAccountFlag(flags))
|
|
202
|
+
return 1;
|
|
187
203
|
// Validated here and not only in `agent run`, because the consequence differs:
|
|
188
204
|
// a bad --timeout typed at a terminal prints an error, while one baked into a
|
|
189
205
|
// plist makes every launch exit on that same validation and KeepAlive restart
|
|
@@ -261,6 +277,16 @@ async function pairCommand(flags) {
|
|
|
261
277
|
process.stderr.write('Missing pairing code. Pass --code or set $PROHOST_PAIRING_CODE.\n');
|
|
262
278
|
return 1;
|
|
263
279
|
}
|
|
280
|
+
// Checked before the code is spent: a typo here must not cost a pairing.
|
|
281
|
+
const account = flagString(flags, 'account');
|
|
282
|
+
if (flags.account === true) {
|
|
283
|
+
process.stderr.write('--account needs a label, e.g. --account work\n');
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
// A re-pair keeps the account the home already runs on unless told otherwise.
|
|
287
|
+
const previousAccount = tryLoadCredentials()?.account;
|
|
288
|
+
if (account && !accountExists(account))
|
|
289
|
+
return 1;
|
|
264
290
|
try {
|
|
265
291
|
await runPair({
|
|
266
292
|
code,
|
|
@@ -268,6 +294,19 @@ async function pairCommand(flags) {
|
|
|
268
294
|
wsUrl: flagString(flags, 'url') ?? process.env.PROHOST_WS_URL,
|
|
269
295
|
webhookUrl: flagString(flags, 'webhook-url'),
|
|
270
296
|
});
|
|
297
|
+
const chosen = account ?? previousAccount;
|
|
298
|
+
if (chosen) {
|
|
299
|
+
try {
|
|
300
|
+
setAgentAccount(chosen, undefined);
|
|
301
|
+
process.stdout.write(` Account ${chosen}\n`);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
// Paired either way; only the carried-over account is gone.
|
|
305
|
+
if (!(err instanceof AccountError))
|
|
306
|
+
throw err;
|
|
307
|
+
process.stderr.write(`! ${err.message} This agent will use the default login.\n`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
271
310
|
return 0;
|
|
272
311
|
}
|
|
273
312
|
catch (err) {
|
|
@@ -293,6 +332,14 @@ async function runCommand(flags) {
|
|
|
293
332
|
const parsedIdleTimeout = parseIdleTimeoutSeconds(flags);
|
|
294
333
|
if (parsedIdleTimeout === INVALID_DURATION)
|
|
295
334
|
return 1;
|
|
335
|
+
// One harness per home: two would both execute every run.
|
|
336
|
+
const lock = acquireRunLock();
|
|
337
|
+
if (!lock.ok) {
|
|
338
|
+
process.stderr.write(`✗ Another \`prohost agent run\` (pid ${lock.holder.pid}, started ${lock.holder.started_at || 'at an unknown time'}) ` +
|
|
339
|
+
`is already serving ${prohostHome()}. Stop it first — or, if it is the daemon, use ` +
|
|
340
|
+
'`prohost agent uninstall-daemon` — or give this agent its own $PROHOST_HOME.\n');
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
296
343
|
// First signal stops accepting work and lets the in-flight run finish
|
|
297
344
|
// reporting itself (so the server doesn't wait on a run we abandoned); a
|
|
298
345
|
// second one means the operator wants out now.
|
|
@@ -335,5 +382,42 @@ async function runCommand(flags) {
|
|
|
335
382
|
finally {
|
|
336
383
|
process.off('SIGINT', onSig);
|
|
337
384
|
process.off('SIGTERM', onSig);
|
|
385
|
+
lock.release();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** Whether an account label exists on this machine (any runtime), explaining if not. */
|
|
389
|
+
function accountExists(label) {
|
|
390
|
+
if (label === 'default')
|
|
391
|
+
return true;
|
|
392
|
+
try {
|
|
393
|
+
// Validates only; the write happens once pairing has succeeded.
|
|
394
|
+
if (loadAccounts().some((a) => a.label === label))
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
/* fall through */
|
|
399
|
+
}
|
|
400
|
+
process.stderr.write(`✗ No account named "${label}" on this machine. Add it first: prohost agent account add ${label} --runtime claude|codex\n`);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
/** `--account <label>` on install-daemon: persist it to agent.json. */
|
|
404
|
+
function applyAccountFlag(flags) {
|
|
405
|
+
if (flags.account === undefined)
|
|
406
|
+
return true;
|
|
407
|
+
const label = flagString(flags, 'account');
|
|
408
|
+
if (!label) {
|
|
409
|
+
process.stderr.write('--account needs a label, e.g. --account work\n');
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
setAgentAccount(label, undefined);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
if (err instanceof AccountError) {
|
|
418
|
+
process.stderr.write(`✗ ${err.message}\n`);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
throw err;
|
|
338
422
|
}
|
|
339
423
|
}
|
package/dist/agent/contract.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - payload shape — `shared/events/payloads/agent_runs.py`
|
|
15
15
|
* - `POST /v1/agent-runs/{id}/complete` — `api_public/routers/agent_runs.py`
|
|
16
16
|
* - `POST /v1/agent-runs/{id}/heartbeat` — `api_public/routers/agent_runs.py`
|
|
17
|
+
* - `POST /v1/agent-runs/{id}/events` — `api_public/routers/agent_runs.py`
|
|
17
18
|
* - `POST /v1/agent-pairing/redeem` — `api_public/routers/agent_pairing.py`
|
|
18
19
|
* - `POST /v1/conversations/{id}/messages` — `api_public/routers/conversations.py`
|
|
19
20
|
*/
|
|
@@ -27,6 +28,27 @@ export declare const EVENT_MESSAGE_TEAM_CHAT = "message.team_chat";
|
|
|
27
28
|
/** Terminal statuses accepted by the run-completion endpoint. */
|
|
28
29
|
export declare const RUN_STATUS_SUCCEEDED = "succeeded";
|
|
29
30
|
export declare const RUN_STATUS_FAILED = "failed";
|
|
31
|
+
/**
|
|
32
|
+
* Completion `outcome` for a run that deliberately posted nothing.
|
|
33
|
+
*
|
|
34
|
+
* Sent with `status: succeeded`: declining is a finished run, not a failure.
|
|
35
|
+
* The server records it the way an in-product `[NO_REPLY]` run is recorded and
|
|
36
|
+
* clears the thinking indicator as a "chose silence" rather than implying a
|
|
37
|
+
* reply landed. A server that predates the field ignores it, so it needs no
|
|
38
|
+
* version negotiation.
|
|
39
|
+
*/
|
|
40
|
+
export declare const RUN_OUTCOME_SILENT = "silent";
|
|
41
|
+
/**
|
|
42
|
+
* What the agent writes as its WHOLE answer to say "no reply".
|
|
43
|
+
*
|
|
44
|
+
* The same token the in-product brain uses, so an agent's instructions read the
|
|
45
|
+
* same on either runtime. Matched on the trimmed output, case-insensitively —
|
|
46
|
+
* and never posted: the literal token landing in a thread is the one outcome
|
|
47
|
+
* that is worse than either replying or not.
|
|
48
|
+
*/
|
|
49
|
+
export declare const NO_REPLY_TOKEN = "[NO_REPLY]";
|
|
50
|
+
/** Whether the agent's output is the no-reply token and nothing else. */
|
|
51
|
+
export declare function isNoReply(output: string): boolean;
|
|
30
52
|
/** Server-side cap on the `error` field of a completion (`MAX_ERROR_CHARS`). */
|
|
31
53
|
export declare const MAX_ERROR_CHARS = 1000;
|
|
32
54
|
/** Server-side cap on the `model` field of a completion (`MAX_MODEL_CHARS`). */
|
|
@@ -48,6 +70,37 @@ export declare const RUN_ERROR_CANCELLED_BY_USER = "cancelled_by_user";
|
|
|
48
70
|
* enormous block of text can't turn a keep-alive into a payload.
|
|
49
71
|
*/
|
|
50
72
|
export declare const MAX_PROGRESS_CHARS = 200;
|
|
73
|
+
/**
|
|
74
|
+
* Server-side cap on events per `POST /v1/agent-runs/{id}/events` call
|
|
75
|
+
* (`shared.agents.external_tool_events.MAX_EVENTS_PER_REQUEST`). The run event
|
|
76
|
+
* also carries it as `events.max_batch`; this is the value for a payload that
|
|
77
|
+
* omits it, and the ceiling either way.
|
|
78
|
+
*/
|
|
79
|
+
export declare const MAX_TOOL_EVENTS_PER_BATCH = 50;
|
|
80
|
+
/**
|
|
81
|
+
* One tool-call lifecycle event, as `POST /v1/agent-runs/{id}/events` takes it.
|
|
82
|
+
*
|
|
83
|
+
* `tool_id` is the runtime's own id for the invocation (Claude Code's
|
|
84
|
+
* `tool_use.id`); the server keeps one timeline row per id, opened by a
|
|
85
|
+
* `calling` and settled by a `completed` / `failed`, and ignores anything that
|
|
86
|
+
* would move a row backwards — which is what makes re-sending a batch safe.
|
|
87
|
+
* Everything else is optional and clipped server-side.
|
|
88
|
+
*/
|
|
89
|
+
export interface ToolEvent {
|
|
90
|
+
tool_id: string;
|
|
91
|
+
tool_name: string;
|
|
92
|
+
status: 'calling' | 'completed' | 'failed';
|
|
93
|
+
/** ISO 8601. */
|
|
94
|
+
started_at?: string;
|
|
95
|
+
/** ISO 8601. */
|
|
96
|
+
ended_at?: string;
|
|
97
|
+
/** The command, the path, the query — one short line. */
|
|
98
|
+
args_preview?: string;
|
|
99
|
+
/** One short line of what came back. */
|
|
100
|
+
result_summary?: string;
|
|
101
|
+
/** Why it failed, on `failed` only. */
|
|
102
|
+
error_summary?: string;
|
|
103
|
+
}
|
|
51
104
|
/** `POST` — exchange a one-time pairing code for a scoped credential. */
|
|
52
105
|
export declare const REDEEM_PATH = "/v1/agent-pairing/redeem";
|
|
53
106
|
/** `POST` — send a message into a conversation as the paired agent. */
|
|
@@ -148,6 +201,8 @@ export declare const MAX_ATTACHMENTS = 10;
|
|
|
148
201
|
* quietly stop half-way through with nothing to explain it.
|
|
149
202
|
*/
|
|
150
203
|
export declare const MAX_SYSTEM_PROMPT_CHARS = 20000;
|
|
204
|
+
/** Ceiling on `trigger.brief` — the server's `MAX_TRIGGER_BRIEF_CHARS`. */
|
|
205
|
+
export declare const MAX_TRIGGER_BRIEF_CHARS = 32000;
|
|
151
206
|
/** The `agent.run_requested` payload, as far as the harness cares about it. */
|
|
152
207
|
export interface AgentRunRequest {
|
|
153
208
|
run_id: string;
|
|
@@ -188,6 +243,21 @@ export interface AgentRunRequest {
|
|
|
188
243
|
*/
|
|
189
244
|
trigger_attachments?: RunAttachment[];
|
|
190
245
|
trigger_user_id?: string;
|
|
246
|
+
/**
|
|
247
|
+
* The server's work order for this run, when it wrote one: a routine's
|
|
248
|
+
* instructions, a heartbeat's standing assignment, or — on a team-chat
|
|
249
|
+
* participant wakeup — the guidance for a message not addressed to the agent.
|
|
250
|
+
* Absent on older servers and on plain mentions.
|
|
251
|
+
*/
|
|
252
|
+
trigger_brief?: string;
|
|
253
|
+
/**
|
|
254
|
+
* `false` when the message was NOT addressed to this agent: it was woken only
|
|
255
|
+
* because it is a member of the thread. Nobody is waiting on it, and the
|
|
256
|
+
* right answer is usually silence. `true` for everything else — including
|
|
257
|
+
* every event from a server that predates the field (parsed events always
|
|
258
|
+
* carry it; absent means `true`).
|
|
259
|
+
*/
|
|
260
|
+
reply_expected?: boolean;
|
|
191
261
|
/** For a trigger fan-out with no surface: the event type that fired it. */
|
|
192
262
|
trigger_type?: string;
|
|
193
263
|
/** For a trigger fan-out with no surface: the entity that event was about. */
|
|
@@ -231,6 +301,15 @@ export interface AgentRunRequest {
|
|
|
231
301
|
* for what happens then.
|
|
232
302
|
*/
|
|
233
303
|
heartbeat_path?: string;
|
|
304
|
+
/**
|
|
305
|
+
* Server-supplied path to report tool events at.
|
|
306
|
+
*
|
|
307
|
+
* Absent on every server built before the `events` block was added beside
|
|
308
|
+
* `heartbeat`. Unlike the heartbeat there is no fallback: with no path the
|
|
309
|
+
* events are simply not sent, and the run behaves exactly as it did before —
|
|
310
|
+
* the heartbeat line is the only narration.
|
|
311
|
+
*/
|
|
312
|
+
events_path?: string;
|
|
234
313
|
/**
|
|
235
314
|
* Earlier messages on this surface, oldest first. Absent on a server that
|
|
236
315
|
* predates the field — which is the case the harness shipped with, so an
|