@corvio/cli 0.1.0-beta.5 → 0.1.0-beta.6
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/README.md +32 -0
- package/dist/cli.js +380 -3
- package/dist/core.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,3 +17,35 @@ corvio update check --json
|
|
|
17
17
|
When no environment key is configured, run `corvio auth login`. Login creates one user-level `cvu_` credential; it does not freeze the browser's current Workspace. Select each operation's Workspace with `corvio workspaces use`, `--workspace`, or `CORVIO_WORKSPACE_ID`. Legacy Workspace-bound `cvk_` keys remain supported and cannot be widened to another Workspace. The environment key always takes precedence. Local sync stores only credential-free handles and revision/hash baselines in `.corvio/state.json`; Markdown lives below `corvio_docs/`, and conflicts require explicit resolution. Update checks compare npm's published `beta` tag with the API compatibility policy but never self-install. See [the Corvio CLI guide](https://corvio.ai/developers/cli) for the full command and receipt contract.
|
|
18
18
|
|
|
19
19
|
The CLI rejects unknown/duplicate options and malformed integer bounds before network access. Normal API calls have a bounded timeout (`CORVIO_REQUEST_TIMEOUT_MS`, maximum ten minutes); only reads, explicit idempotency contracts, revision guards, and other owner-declared safe operations retry automatically. Document updates preflight the current revision when `--expected-revision` is omitted. Downloads, uploads, and Markdown pulls verify SHA-256 receipts before replacing local files; local writes are atomic, remote default filenames cannot escape the current directory, and Sync refuses symbolic-link traversal while checkpointing each successful push.
|
|
20
|
+
|
|
21
|
+
## External Agent collaboration
|
|
22
|
+
|
|
23
|
+
Create and manage an Agent with the user credential, then run it with the one-time `cvg_` secret. User and Agent credentials are deliberately separate:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
corvio agents create \
|
|
27
|
+
--handle reviewer-a \
|
|
28
|
+
--display-name "Reviewer A" \
|
|
29
|
+
--provider codex \
|
|
30
|
+
--permissions read,comment,edit \
|
|
31
|
+
--json
|
|
32
|
+
|
|
33
|
+
export CORVIO_AGENT_API_KEY='cvg_...'
|
|
34
|
+
corvio agent claim --wait-seconds 30 --json
|
|
35
|
+
corvio agent run --handler ./my-agent-handler --once --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The handler reads one Agent event JSON object from stdin and prints one JSON object:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"instruction":"Inspect the mentioned section and correct unsupported claims.","reply":"Reviewed and updated the section."}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The Runner renews the mailbox lease, sends the instruction and structured event context to Corvio Query, posts the reply under the Agent's own name/avatar, and completes only with durable Query/comment/artifact receipts. Query owns detailed document edits; the handler should decide intent, not implement a second line editor. `CORVIO_API_KEY` is not passed to the handler and Agent runtime commands never fall back to it.
|
|
45
|
+
|
|
46
|
+
Runner replies are causally bound to the claimed event and are idempotent across network retries. Corvio stops repeated Agent routes
|
|
47
|
+
and enforces bounded hop, chain-event, and per-comment target budgets; a stopped handoff remains visible as “Needs attention” but is
|
|
48
|
+
not claimable. For a deliberate standalone Agent-to-Agent request, use `corvio agent comment ... --start-new-chain`. Manual replies
|
|
49
|
+
may instead pass `--source-event-id` with `--source-event-lease-token`; source and new-chain modes must not be mixed.
|
|
50
|
+
|
|
51
|
+
If a laptop sleeps, loses network, or stops the Runner, events remain pending or become reclaimable after lease expiry. Corvio does not claim to power on an offline device. Use a process supervisor for an always-on Runner; pause or revoke the Agent when it should stop receiving new mentions.
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { lstat, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
openBrowser,
|
|
21
22
|
option,
|
|
22
23
|
readTextFile,
|
|
24
|
+
readAgentCredential,
|
|
23
25
|
readWorkspaceContext,
|
|
24
26
|
readWorkspaceCredential,
|
|
25
27
|
requireConfirmation,
|
|
@@ -33,7 +35,7 @@ import {
|
|
|
33
35
|
writeTextFile,
|
|
34
36
|
} from "./core.js";
|
|
35
37
|
|
|
36
|
-
const VERSION = "0.1.0-beta.
|
|
38
|
+
const VERSION = "0.1.0-beta.6";
|
|
37
39
|
const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
|
|
38
40
|
const DEFAULT_APP_BASE = "https://corvio.ai";
|
|
39
41
|
const USER_AGENT = `corvio-cli/${VERSION}`;
|
|
@@ -60,7 +62,20 @@ const COMMAND_OPTIONS = Object.freeze({
|
|
|
60
62
|
"workspaces:use": ["id"],
|
|
61
63
|
"workspaces:create": ["name", "kind", "description", "idempotency-key"],
|
|
62
64
|
"capabilities:": [],
|
|
63
|
-
"ask:": ["prompt", "conversation-id", "sources", "allow-actions", "idempotency-key"],
|
|
65
|
+
"ask:": ["prompt", "conversation-id", "sources", "allow-actions", "context-file", "idempotency-key"],
|
|
66
|
+
"agents:list": ["include-revoked"],
|
|
67
|
+
"agents:create": ["handle", "display-name", "avatar-url", "provider", "permissions"],
|
|
68
|
+
"agents:update": ["id", "display-name", "avatar-url", "status", "permissions"],
|
|
69
|
+
"agents:rotate": ["id"],
|
|
70
|
+
"agent:claim": ["worker-id", "lease-seconds", "wait-seconds"],
|
|
71
|
+
"agent:complete": ["id", "lease-token", "result-file", "question-id", "comment-entry-id", "no-change-reason"],
|
|
72
|
+
"agent:renew": ["id", "lease-token", "lease-seconds"],
|
|
73
|
+
"agent:fail": ["id", "lease-token", "error", "retryable"],
|
|
74
|
+
"agent:comments": ["document-id"],
|
|
75
|
+
"agent:comment": ["document-id", "body", "selection-file", "start-new-chain"],
|
|
76
|
+
"agent:reply": ["document-id", "id", "body", "source-event-id", "source-event-lease-token", "start-new-chain"],
|
|
77
|
+
"agent:status": ["document-id", "id", "status"],
|
|
78
|
+
"agent:run": ["once", "handler", "worker-id", "lease-seconds", "wait-seconds", "sources"],
|
|
64
79
|
"questions:list": ["limit", "cursor", "conversation-id"],
|
|
65
80
|
"questions:get": ["id"],
|
|
66
81
|
"conversations:list": ["limit", "cursor"],
|
|
@@ -97,6 +112,8 @@ Usage:
|
|
|
97
112
|
corvio workspaces list|use|current|create [id]
|
|
98
113
|
corvio capabilities
|
|
99
114
|
corvio ask --prompt <text> [--conversation-id <id>] [--allow-actions]
|
|
115
|
+
corvio agents list|create|update|rotate [id]
|
|
116
|
+
corvio agent claim|renew|complete|fail|comments|comment|reply|status|run [id]
|
|
100
117
|
corvio questions list|get [id]
|
|
101
118
|
corvio conversations list|get [id]
|
|
102
119
|
corvio search <query>
|
|
@@ -112,6 +129,7 @@ Global options:
|
|
|
112
129
|
Authentication:
|
|
113
130
|
CORVIO_API_KEY=cvu_... takes precedence over the stored device-link login.
|
|
114
131
|
Legacy Workspace-bound cvk_ keys remain supported.
|
|
132
|
+
Agent runtime commands use CORVIO_AGENT_API_KEY=cvg_... and never borrow the user's credential.
|
|
115
133
|
Select the target with 'corvio workspaces use <id>', --workspace, or CORVIO_WORKSPACE_ID.
|
|
116
134
|
Run 'corvio auth login' when no environment key is configured.
|
|
117
135
|
`;
|
|
@@ -120,7 +138,9 @@ const COMMAND_HELP = Object.freeze({
|
|
|
120
138
|
auth: `Usage: corvio auth login|status|logout\n\nAuthenticate with a user-level device link, inspect the current principal, or revoke the stored credential.`,
|
|
121
139
|
workspaces: `Usage:\n corvio workspaces list\n corvio workspaces use <workspace-id>\n corvio workspaces current\n corvio workspaces create --name <name> [--kind personal|team]\n\nWorkspace status and blocked capability reasons are returned with each item.`,
|
|
122
140
|
capabilities: `Usage: corvio capabilities [--json]\n\nShow live API resources, principal scopes, selected Workspace status, and CLI compatibility.`,
|
|
123
|
-
ask: `Usage: corvio ask --prompt <text> [--conversation-id <id>] [--sources workspace,web,memory] [--allow-actions] [--idempotency-key <key>]`,
|
|
141
|
+
ask: `Usage: corvio ask --prompt <text> [--conversation-id <id>] [--sources workspace,web,memory] [--allow-actions] [--context-file <json>] [--idempotency-key <key>]`,
|
|
142
|
+
agents: `Usage:\n corvio agents list\n corvio agents create --handle <handle> --display-name <name> [--provider codex|claude_code|custom] [--permissions read,comment,edit]\n corvio agents update <id> [--display-name <name>] [--status active|paused|revoked] [--permissions <list>]\n corvio agents rotate <id>\n\nAgent creation and rotation print the cvg_ secret once.`,
|
|
143
|
+
agent: `Usage:\n corvio agent claim [--worker-id <id>] [--wait-seconds <0-30>]\n corvio agent renew <event-id> --lease-token <token> [--lease-seconds <30-900>]\n corvio agent complete <event-id> --lease-token <token> [--result-file <json>]\n corvio agent fail <event-id> --lease-token <token> --error <message>\n corvio agent comments --document-id <id>\n corvio agent comment --document-id <id> --body <text> [--selection-file <json>]\n corvio agent reply <thread-id> --document-id <id> --body <text>\n corvio agent status <thread-id> --document-id <id> --status open|resolved\n corvio agent run --handler <executable> [--once]\n\nThe runner leases durable events and renews its lease while work is active. The handler reads event JSON from stdin and returns {"instruction":"...","reply":"..."}; Corvio Query owns document changes and returns the durable receipt.`,
|
|
124
144
|
questions: `Usage:\n corvio questions list [--limit <n>] [--cursor <cursor>] [--conversation-id <id>]\n corvio questions get <question-id>`,
|
|
125
145
|
conversations: `Usage:\n corvio conversations list [--limit <n>] [--cursor <cursor>]\n corvio conversations get <conversation-id>`,
|
|
126
146
|
search: `Usage: corvio search <query> [--limit <n>] [--page-id <id>] [--node-id <id>]`,
|
|
@@ -151,6 +171,8 @@ function normalizeInvocation(command, action) {
|
|
|
151
171
|
docs: "list",
|
|
152
172
|
projects: "create",
|
|
153
173
|
files: "list",
|
|
174
|
+
agents: "list",
|
|
175
|
+
agent: "claim",
|
|
154
176
|
sync: "status",
|
|
155
177
|
update: "check",
|
|
156
178
|
};
|
|
@@ -184,6 +206,13 @@ function validateInvocation(options, positionals) {
|
|
|
184
206
|
"files:download",
|
|
185
207
|
"files:organize",
|
|
186
208
|
"files:open",
|
|
209
|
+
"agents:update",
|
|
210
|
+
"agents:rotate",
|
|
211
|
+
"agent:complete",
|
|
212
|
+
"agent:renew",
|
|
213
|
+
"agent:fail",
|
|
214
|
+
"agent:reply",
|
|
215
|
+
"agent:status",
|
|
187
216
|
]);
|
|
188
217
|
assertPositionalCount(positionals, actionsWithId.has(`${command}:${action}`) ? 3 : command === "capabilities" ? 1 : 2);
|
|
189
218
|
}
|
|
@@ -256,6 +285,30 @@ async function client(options, { auth = true, workspace = "required" } = {}) {
|
|
|
256
285
|
};
|
|
257
286
|
}
|
|
258
287
|
|
|
288
|
+
function agentClient(options) {
|
|
289
|
+
const credential = readAgentCredential();
|
|
290
|
+
return new ApiClient({
|
|
291
|
+
baseUrl: baseUrl(options),
|
|
292
|
+
credential: credential.key,
|
|
293
|
+
userAgent: USER_AGENT,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function readJsonObject(path, label) {
|
|
298
|
+
let value;
|
|
299
|
+
try {
|
|
300
|
+
value = JSON.parse(await readFile(resolve(String(path)), "utf8"));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
throw new CliError(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, {
|
|
303
|
+
code: "invalid_json_file",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
307
|
+
throw new CliError(`${label} must contain one JSON object.`, { code: "invalid_json_file" });
|
|
308
|
+
}
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
|
|
259
312
|
async function authLogin(options) {
|
|
260
313
|
const current = await readWorkspaceCredential();
|
|
261
314
|
if (current?.source === "environment" && !options.force) {
|
|
@@ -474,6 +527,9 @@ async function ask(options, positionals) {
|
|
|
474
527
|
const prompt = String(options.prompt || positionals.join(" ")).trim();
|
|
475
528
|
if (!prompt) throw new CliError("Provide --prompt <text> or a positional prompt.");
|
|
476
529
|
const { api } = await client(options);
|
|
530
|
+
const context = options["context-file"]
|
|
531
|
+
? await readJsonObject(options["context-file"], "--context-file")
|
|
532
|
+
: null;
|
|
477
533
|
const result = await api.request("/questions", {
|
|
478
534
|
method: "POST",
|
|
479
535
|
headers: { "idempotency-key": idempotencyKey(options) },
|
|
@@ -482,12 +538,331 @@ async function ask(options, positionals) {
|
|
|
482
538
|
conversation_id: options["conversation-id"] || null,
|
|
483
539
|
mode: options["allow-actions"] ? "allow_actions" : "answer_only",
|
|
484
540
|
sources: listOption(options, "sources", ["workspace", "web", "memory"]),
|
|
541
|
+
context,
|
|
485
542
|
},
|
|
486
543
|
timeoutMs: 330_000,
|
|
487
544
|
});
|
|
488
545
|
writeResult(result, { json: options.json });
|
|
489
546
|
}
|
|
490
547
|
|
|
548
|
+
async function agents(options, action, id) {
|
|
549
|
+
const { api } = await client(options);
|
|
550
|
+
let result;
|
|
551
|
+
if (action === "list") {
|
|
552
|
+
result = await api.request("/agents", { query: { include_revoked: options["include-revoked"] || undefined } });
|
|
553
|
+
} else if (action === "create") {
|
|
554
|
+
result = await api.request("/agents", {
|
|
555
|
+
method: "POST",
|
|
556
|
+
body: {
|
|
557
|
+
handle: option(options, "handle", { required: true }),
|
|
558
|
+
display_name: option(options, "display-name", { required: true }),
|
|
559
|
+
avatar_url: options["avatar-url"] || null,
|
|
560
|
+
provider: options.provider || "custom",
|
|
561
|
+
permissions: listOption(options, "permissions", ["read", "comment"]),
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
} else if (action === "update") {
|
|
565
|
+
const agentId = id || option(options, "id", { required: true });
|
|
566
|
+
const body = {};
|
|
567
|
+
if (options["display-name"] !== undefined) body.display_name = options["display-name"];
|
|
568
|
+
if (options["avatar-url"] !== undefined) body.avatar_url = options["avatar-url"];
|
|
569
|
+
if (options.status !== undefined) body.status = options.status;
|
|
570
|
+
if (options.permissions !== undefined) body.permissions = listOption(options, "permissions");
|
|
571
|
+
if (Object.keys(body).length === 0) throw new CliError("Provide an Agent field to update.", { code: "agent_update_required" });
|
|
572
|
+
result = await api.request(`/agents/${resourcePath(agentId)}`, { method: "PATCH", body });
|
|
573
|
+
} else if (action === "rotate") {
|
|
574
|
+
const agentId = id || option(options, "id", { required: true });
|
|
575
|
+
result = await api.request(`/agents/${resourcePath(agentId)}/credentials:rotate`, { method: "POST" });
|
|
576
|
+
} else {
|
|
577
|
+
throw new CliError("agents action must be list, create, update, or rotate.");
|
|
578
|
+
}
|
|
579
|
+
writeResult(result, { json: options.json });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function claimAgentEvent(api, options, waitFallback = 0) {
|
|
583
|
+
return api.request("/agent-events:claim", {
|
|
584
|
+
method: "POST",
|
|
585
|
+
body: {
|
|
586
|
+
worker_id: options["worker-id"] || `corvio-cli-${process.pid}`,
|
|
587
|
+
lease_seconds: numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 }),
|
|
588
|
+
wait_seconds: numberOption(options, "wait-seconds", waitFallback, { minimum: 0, maximum: 30 }),
|
|
589
|
+
},
|
|
590
|
+
timeoutMs: 45_000,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function runAgentHandler(executable, event) {
|
|
595
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
596
|
+
const handlerEvent = { ...event, delivery: { ...(event?.delivery || {}) } };
|
|
597
|
+
delete handlerEvent.delivery.lease_token;
|
|
598
|
+
const handlerEnv = { ...process.env, CORVIO_AGENT_EVENT_ID: String(event.id || "") };
|
|
599
|
+
delete handlerEnv.CORVIO_API_KEY;
|
|
600
|
+
const child = spawn(String(executable), [], {
|
|
601
|
+
shell: false,
|
|
602
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603
|
+
env: handlerEnv,
|
|
604
|
+
});
|
|
605
|
+
let stdout = "";
|
|
606
|
+
let stderr = "";
|
|
607
|
+
const maximumBytes = 1024 * 1024;
|
|
608
|
+
child.stdout.setEncoding("utf8");
|
|
609
|
+
child.stderr.setEncoding("utf8");
|
|
610
|
+
child.stdout.on("data", (chunk) => {
|
|
611
|
+
stdout += chunk;
|
|
612
|
+
if (Buffer.byteLength(stdout) > maximumBytes) child.kill("SIGTERM");
|
|
613
|
+
});
|
|
614
|
+
child.stderr.on("data", (chunk) => {
|
|
615
|
+
stderr = `${stderr}${chunk}`.slice(-20_000);
|
|
616
|
+
});
|
|
617
|
+
child.on("error", (error) => rejectPromise(new CliError(`Agent handler could not start: ${error.message}`, { code: "agent_handler_failed" })));
|
|
618
|
+
child.on("close", (code) => {
|
|
619
|
+
if (code !== 0) {
|
|
620
|
+
rejectPromise(new CliError(`Agent handler exited with ${code}: ${stderr.trim() || "no error output"}`, { code: "agent_handler_failed" }));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
let decision;
|
|
624
|
+
try {
|
|
625
|
+
decision = JSON.parse(stdout.trim());
|
|
626
|
+
} catch {
|
|
627
|
+
rejectPromise(new CliError("Agent handler must print one JSON object.", { code: "agent_handler_result_invalid" }));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const instruction = String(decision?.instruction || "").trim();
|
|
631
|
+
if (!instruction) {
|
|
632
|
+
rejectPromise(new CliError("Agent handler result requires a non-empty instruction.", { code: "agent_handler_result_invalid" }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
resolvePromise({ instruction, reply: String(decision?.reply || "").trim() || null });
|
|
636
|
+
});
|
|
637
|
+
child.stdin.end(`${JSON.stringify(handlerEvent)}\n`);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function startAgentLeaseHeartbeat(api, event, leaseToken, leaseSeconds) {
|
|
642
|
+
let stopped = false;
|
|
643
|
+
let timer = null;
|
|
644
|
+
let inFlight = null;
|
|
645
|
+
let failure = null;
|
|
646
|
+
const intervalMs = Math.max(10_000, Math.min(60_000, Math.floor(leaseSeconds * 1_000 / 3)));
|
|
647
|
+
const renew = async () => {
|
|
648
|
+
if (stopped || failure) return;
|
|
649
|
+
try {
|
|
650
|
+
await api.request(`/agent-events/${resourcePath(event.id)}:renew`, {
|
|
651
|
+
method: "POST",
|
|
652
|
+
body: { lease_token: leaseToken, lease_seconds: leaseSeconds },
|
|
653
|
+
});
|
|
654
|
+
} catch (error) {
|
|
655
|
+
failure = error;
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!stopped) timer = setTimeout(() => { inFlight = renew(); }, intervalMs);
|
|
659
|
+
};
|
|
660
|
+
inFlight = renew();
|
|
661
|
+
await inFlight;
|
|
662
|
+
if (failure) throw failure;
|
|
663
|
+
return {
|
|
664
|
+
async stop() {
|
|
665
|
+
stopped = true;
|
|
666
|
+
if (timer) clearTimeout(timer);
|
|
667
|
+
if (inFlight) await inFlight;
|
|
668
|
+
},
|
|
669
|
+
assertHealthy() {
|
|
670
|
+
if (failure) throw failure;
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function processClaimedAgentEvent(api, event, options) {
|
|
676
|
+
const leaseToken = String(event?.delivery?.lease_token || "");
|
|
677
|
+
if (!event?.id || !leaseToken) throw new CliError("Claimed event is missing its lease receipt.", { code: "agent_event_invalid" });
|
|
678
|
+
const leaseSeconds = numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 });
|
|
679
|
+
let heartbeat = null;
|
|
680
|
+
try {
|
|
681
|
+
heartbeat = await startAgentLeaseHeartbeat(api, event, leaseToken, leaseSeconds);
|
|
682
|
+
const decision = await runAgentHandler(option(options, "handler", { required: true }), event);
|
|
683
|
+
const question = await api.request("/questions", {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: { "idempotency-key": `agent-event:${event.id}:query` },
|
|
686
|
+
body: {
|
|
687
|
+
prompt: decision.instruction,
|
|
688
|
+
mode: "allow_actions",
|
|
689
|
+
sources: listOption(options, "sources", ["workspace", "web", "memory"]),
|
|
690
|
+
context: {
|
|
691
|
+
agent_event: {
|
|
692
|
+
id: event.id,
|
|
693
|
+
kind: event.kind,
|
|
694
|
+
object_kind: event.object_kind,
|
|
695
|
+
object_id: event.object_id,
|
|
696
|
+
actor_user_id: event.actor_user_id,
|
|
697
|
+
actor_agent_id: event.actor_agent_id,
|
|
698
|
+
lineage: event.lineage,
|
|
699
|
+
},
|
|
700
|
+
collaboration: event.context,
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
timeoutMs: 330_000,
|
|
704
|
+
});
|
|
705
|
+
if (String(question.status || "") !== "completed") {
|
|
706
|
+
throw new CliError(`Corvio Query did not complete durably (status: ${question.status || "unknown"}).`, {
|
|
707
|
+
code: "agent_query_not_completed",
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const documentId = String(event?.context?.document?.document_id || "");
|
|
711
|
+
const threadId = String(event?.context?.comment?.thread_id || "");
|
|
712
|
+
let commentEntryId = null;
|
|
713
|
+
if (documentId && threadId) {
|
|
714
|
+
const reply = String(decision.reply || question.answer || "Completed the requested review.").trim().slice(0, 12_000);
|
|
715
|
+
const thread = await api.request(`/documents/${resourcePath(documentId)}/comments/${resourcePath(threadId)}/replies`, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
body: {
|
|
718
|
+
body: reply,
|
|
719
|
+
entry_point: "external_agent_runner",
|
|
720
|
+
source_event_id: event.id,
|
|
721
|
+
source_event_lease_token: leaseToken,
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
const entries = Array.isArray(thread?.entries) ? thread.entries : [];
|
|
725
|
+
commentEntryId = entries.at(-1)?.id || null;
|
|
726
|
+
}
|
|
727
|
+
await heartbeat.stop();
|
|
728
|
+
heartbeat.assertHealthy();
|
|
729
|
+
const completed = await api.request(`/agent-events/${resourcePath(event.id)}:complete`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
body: {
|
|
732
|
+
lease_token: leaseToken,
|
|
733
|
+
result: {
|
|
734
|
+
question_id: question.id,
|
|
735
|
+
...(commentEntryId ? { comment_entry_id: commentEntryId } : {}),
|
|
736
|
+
status: question.status,
|
|
737
|
+
artifact_ids: Array.isArray(question.artifacts) ? question.artifacts.map((item) => item.document_id).filter(Boolean) : [],
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
return { event: completed, question, comment_entry_id: commentEntryId };
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (heartbeat) {
|
|
744
|
+
try {
|
|
745
|
+
await heartbeat.stop();
|
|
746
|
+
} catch {
|
|
747
|
+
// The original execution error remains the failure authority.
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
await api.request(`/agent-events/${resourcePath(event.id)}:fail`, {
|
|
752
|
+
method: "POST",
|
|
753
|
+
body: { lease_token: leaseToken, error: error instanceof Error ? error.message : String(error), retryable: true },
|
|
754
|
+
});
|
|
755
|
+
} catch {
|
|
756
|
+
// Preserve the original execution failure; the expired lease remains reclaimable.
|
|
757
|
+
}
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function runAgent(options) {
|
|
763
|
+
const api = agentClient(options);
|
|
764
|
+
while (true) {
|
|
765
|
+
const claimed = await claimAgentEvent(api, options, options.once ? 0 : 30);
|
|
766
|
+
if (!claimed.event) {
|
|
767
|
+
if (options.once) return writeResult({ processed: false, reason: "no_event", retry_after_seconds: claimed.retry_after_seconds }, { json: options.json });
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const receipt = await processClaimedAgentEvent(api, claimed.event, options);
|
|
771
|
+
if (options.once) return writeResult({ processed: true, ...receipt }, { json: options.json });
|
|
772
|
+
process.stdout.write(`${JSON.stringify({ processed: true, ...receipt })}\n`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function agentCommand(options, action, id) {
|
|
777
|
+
if (action === "run") return runAgent(options);
|
|
778
|
+
const api = agentClient(options);
|
|
779
|
+
let result;
|
|
780
|
+
if (action === "claim") {
|
|
781
|
+
result = await claimAgentEvent(api, options);
|
|
782
|
+
} else if (action === "renew") {
|
|
783
|
+
const eventId = id || option(options, "id", { required: true });
|
|
784
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:renew`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
body: {
|
|
787
|
+
lease_token: option(options, "lease-token", { required: true }),
|
|
788
|
+
lease_seconds: numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 }),
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
} else if (action === "complete") {
|
|
792
|
+
const eventId = id || option(options, "id", { required: true });
|
|
793
|
+
const receipt = options["result-file"]
|
|
794
|
+
? await readJsonObject(options["result-file"], "--result-file")
|
|
795
|
+
: {
|
|
796
|
+
...(options["question-id"] ? { question_id: options["question-id"] } : {}),
|
|
797
|
+
...(options["comment-entry-id"] ? { comment_entry_id: options["comment-entry-id"] } : {}),
|
|
798
|
+
...(options["no-change-reason"] ? { no_change_reason: options["no-change-reason"] } : {}),
|
|
799
|
+
};
|
|
800
|
+
if (Object.keys(receipt).length === 0) throw new CliError("Provide a durable completion receipt.", { code: "agent_event_receipt_required" });
|
|
801
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:complete`, {
|
|
802
|
+
method: "POST",
|
|
803
|
+
body: { lease_token: option(options, "lease-token", { required: true }), result: receipt },
|
|
804
|
+
});
|
|
805
|
+
} else if (action === "fail") {
|
|
806
|
+
const eventId = id || option(options, "id", { required: true });
|
|
807
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:fail`, {
|
|
808
|
+
method: "POST",
|
|
809
|
+
body: {
|
|
810
|
+
lease_token: option(options, "lease-token", { required: true }),
|
|
811
|
+
error: option(options, "error", { required: true }),
|
|
812
|
+
retryable: String(options.retryable ?? "true").toLowerCase() !== "false",
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
} else if (action === "comments") {
|
|
816
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments`);
|
|
817
|
+
} else if (action === "comment") {
|
|
818
|
+
const selection = options["selection-file"]
|
|
819
|
+
? await readJsonObject(options["selection-file"], "--selection-file")
|
|
820
|
+
: { target_scope: "document" };
|
|
821
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments`, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
body: {
|
|
824
|
+
body: option(options, "body", { required: true }),
|
|
825
|
+
selection,
|
|
826
|
+
entry_point: "external_agent_cli",
|
|
827
|
+
start_new_chain: Boolean(options["start-new-chain"]),
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
} else if (action === "reply") {
|
|
831
|
+
const threadId = id || option(options, "id", { required: true });
|
|
832
|
+
const hasSourceEventId = Boolean(options["source-event-id"]);
|
|
833
|
+
const hasSourceEventLease = Boolean(options["source-event-lease-token"]);
|
|
834
|
+
if (hasSourceEventId !== hasSourceEventLease) {
|
|
835
|
+
throw new CliError("Provide --source-event-id and --source-event-lease-token together.", {
|
|
836
|
+
code: "agent_reply_source_incomplete",
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (hasSourceEventId && options["start-new-chain"]) {
|
|
840
|
+
throw new CliError("A source event reply cannot also start a new chain.", {
|
|
841
|
+
code: "agent_reply_source_conflict",
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments/${resourcePath(threadId)}/replies`, {
|
|
845
|
+
method: "POST",
|
|
846
|
+
body: {
|
|
847
|
+
body: option(options, "body", { required: true }),
|
|
848
|
+
entry_point: "external_agent_cli_reply",
|
|
849
|
+
...(options["source-event-id"] ? { source_event_id: options["source-event-id"] } : {}),
|
|
850
|
+
...(options["source-event-lease-token"] ? { source_event_lease_token: options["source-event-lease-token"] } : {}),
|
|
851
|
+
start_new_chain: Boolean(options["start-new-chain"]),
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
} else if (action === "status") {
|
|
855
|
+
const threadId = id || option(options, "id", { required: true });
|
|
856
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments/${resourcePath(threadId)}/status`, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
body: { status: option(options, "status", { required: true }), entry_point: "external_agent_cli_status" },
|
|
859
|
+
});
|
|
860
|
+
} else {
|
|
861
|
+
throw new CliError("Unknown agent runtime action.");
|
|
862
|
+
}
|
|
863
|
+
writeResult(result, { json: options.json });
|
|
864
|
+
}
|
|
865
|
+
|
|
491
866
|
function pageQuery(options) {
|
|
492
867
|
return {
|
|
493
868
|
limit: numberOption(options, "limit", 20, { maximum: 100 }),
|
|
@@ -1256,6 +1631,8 @@ async function main({ options, positionals }) {
|
|
|
1256
1631
|
}
|
|
1257
1632
|
if (command === "capabilities") return capabilities(options);
|
|
1258
1633
|
if (command === "ask") return ask(options, [action, id, ...rest].filter(Boolean));
|
|
1634
|
+
if (command === "agents") return agents(options, action || "list", id);
|
|
1635
|
+
if (command === "agent") return agentCommand(options, action || "claim", id);
|
|
1259
1636
|
if (command === "questions") return questions(options, action || "list", id);
|
|
1260
1637
|
if (command === "conversations") return conversations(options, action || "list", id);
|
|
1261
1638
|
if (command === "search") return search(options, [action, id, ...rest].filter(Boolean));
|
package/dist/core.js
CHANGED
|
@@ -95,6 +95,9 @@ export function parseArgv(argv) {
|
|
|
95
95
|
"allow-actions",
|
|
96
96
|
"private-link",
|
|
97
97
|
"force",
|
|
98
|
+
"once",
|
|
99
|
+
"include-revoked",
|
|
100
|
+
"start-new-chain",
|
|
98
101
|
"help",
|
|
99
102
|
"version",
|
|
100
103
|
]);
|
|
@@ -585,6 +588,23 @@ export async function readWorkspaceCredential(env = process.env) {
|
|
|
585
588
|
return { key, source: "device_link", profile: profileName, metadata };
|
|
586
589
|
}
|
|
587
590
|
|
|
591
|
+
export function readAgentCredential(env = process.env) {
|
|
592
|
+
const key = String(env.CORVIO_AGENT_API_KEY || "").trim();
|
|
593
|
+
if (!key) {
|
|
594
|
+
throw new CliError(
|
|
595
|
+
"No Agent credential found. Set CORVIO_AGENT_API_KEY to the cvg_ secret returned when the Agent was created.",
|
|
596
|
+
{ code: "agent_auth_required", exitCode: EXIT.AUTH },
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (!key.startsWith("cvg_")) {
|
|
600
|
+
throw new CliError("CORVIO_AGENT_API_KEY must be an Agent cvg_ key.", {
|
|
601
|
+
code: "invalid_agent_key",
|
|
602
|
+
exitCode: EXIT.AUTH,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return { key, source: "environment", profile: null, metadata: null };
|
|
606
|
+
}
|
|
607
|
+
|
|
588
608
|
export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
589
609
|
const path = workspaceCredentialPath(env);
|
|
590
610
|
const root = dirname(path);
|