@corvio/cli 0.1.0-beta.5 → 0.1.0-beta.7
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 +418 -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.7";
|
|
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,21 @@ 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:connect": ["handle", "avatar-url", "provider", "permissions"],
|
|
68
|
+
"agents:create": ["handle", "display-name", "avatar-url", "provider", "permissions"],
|
|
69
|
+
"agents:update": ["id", "display-name", "avatar-url", "status", "permissions"],
|
|
70
|
+
"agents:rotate": ["id"],
|
|
71
|
+
"agent:claim": ["worker-id", "lease-seconds", "wait-seconds"],
|
|
72
|
+
"agent:complete": ["id", "lease-token", "result-file", "question-id", "comment-entry-id", "no-change-reason"],
|
|
73
|
+
"agent:renew": ["id", "lease-token", "lease-seconds"],
|
|
74
|
+
"agent:fail": ["id", "lease-token", "error", "retryable"],
|
|
75
|
+
"agent:comments": ["document-id"],
|
|
76
|
+
"agent:comment": ["document-id", "body", "selection-file", "start-new-chain"],
|
|
77
|
+
"agent:reply": ["document-id", "id", "body", "source-event-id", "source-event-lease-token", "start-new-chain"],
|
|
78
|
+
"agent:status": ["document-id", "id", "status"],
|
|
79
|
+
"agent:run": ["once", "handler", "worker-id", "lease-seconds", "wait-seconds", "sources"],
|
|
64
80
|
"questions:list": ["limit", "cursor", "conversation-id"],
|
|
65
81
|
"questions:get": ["id"],
|
|
66
82
|
"conversations:list": ["limit", "cursor"],
|
|
@@ -97,6 +113,8 @@ Usage:
|
|
|
97
113
|
corvio workspaces list|use|current|create [id]
|
|
98
114
|
corvio capabilities
|
|
99
115
|
corvio ask --prompt <text> [--conversation-id <id>] [--allow-actions]
|
|
116
|
+
corvio agents list|connect|create|update|rotate [id]
|
|
117
|
+
corvio agent claim|renew|complete|fail|comments|comment|reply|status|run [id]
|
|
100
118
|
corvio questions list|get [id]
|
|
101
119
|
corvio conversations list|get [id]
|
|
102
120
|
corvio search <query>
|
|
@@ -112,6 +130,7 @@ Global options:
|
|
|
112
130
|
Authentication:
|
|
113
131
|
CORVIO_API_KEY=cvu_... takes precedence over the stored device-link login.
|
|
114
132
|
Legacy Workspace-bound cvk_ keys remain supported.
|
|
133
|
+
Agent runtime commands use CORVIO_AGENT_API_KEY=cvg_... and never borrow the user's credential.
|
|
115
134
|
Select the target with 'corvio workspaces use <id>', --workspace, or CORVIO_WORKSPACE_ID.
|
|
116
135
|
Run 'corvio auth login' when no environment key is configured.
|
|
117
136
|
`;
|
|
@@ -120,7 +139,9 @@ const COMMAND_HELP = Object.freeze({
|
|
|
120
139
|
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
140
|
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
141
|
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>]`,
|
|
142
|
+
ask: `Usage: corvio ask --prompt <text> [--conversation-id <id>] [--sources workspace,web,memory] [--allow-actions] [--context-file <json>] [--idempotency-key <key>]`,
|
|
143
|
+
agents: `Usage:\n corvio agents list\n corvio agents connect --handle <handle> [--provider codex|claude_code|custom] [--permissions read,comment,edit]\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\n'connect' explicitly creates a separate cvg_ Agent principal after user authentication and leaves naming pending in Workspace. It never converts the human cvu_ login into an Agent. Creation and rotation print the cvg_ secret once.`,
|
|
144
|
+
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
145
|
questions: `Usage:\n corvio questions list [--limit <n>] [--cursor <cursor>] [--conversation-id <id>]\n corvio questions get <question-id>`,
|
|
125
146
|
conversations: `Usage:\n corvio conversations list [--limit <n>] [--cursor <cursor>]\n corvio conversations get <conversation-id>`,
|
|
126
147
|
search: `Usage: corvio search <query> [--limit <n>] [--page-id <id>] [--node-id <id>]`,
|
|
@@ -151,6 +172,8 @@ function normalizeInvocation(command, action) {
|
|
|
151
172
|
docs: "list",
|
|
152
173
|
projects: "create",
|
|
153
174
|
files: "list",
|
|
175
|
+
agents: "list",
|
|
176
|
+
agent: "claim",
|
|
154
177
|
sync: "status",
|
|
155
178
|
update: "check",
|
|
156
179
|
};
|
|
@@ -184,6 +207,13 @@ function validateInvocation(options, positionals) {
|
|
|
184
207
|
"files:download",
|
|
185
208
|
"files:organize",
|
|
186
209
|
"files:open",
|
|
210
|
+
"agents:update",
|
|
211
|
+
"agents:rotate",
|
|
212
|
+
"agent:complete",
|
|
213
|
+
"agent:renew",
|
|
214
|
+
"agent:fail",
|
|
215
|
+
"agent:reply",
|
|
216
|
+
"agent:status",
|
|
187
217
|
]);
|
|
188
218
|
assertPositionalCount(positionals, actionsWithId.has(`${command}:${action}`) ? 3 : command === "capabilities" ? 1 : 2);
|
|
189
219
|
}
|
|
@@ -256,6 +286,30 @@ async function client(options, { auth = true, workspace = "required" } = {}) {
|
|
|
256
286
|
};
|
|
257
287
|
}
|
|
258
288
|
|
|
289
|
+
function agentClient(options) {
|
|
290
|
+
const credential = readAgentCredential();
|
|
291
|
+
return new ApiClient({
|
|
292
|
+
baseUrl: baseUrl(options),
|
|
293
|
+
credential: credential.key,
|
|
294
|
+
userAgent: USER_AGENT,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function readJsonObject(path, label) {
|
|
299
|
+
let value;
|
|
300
|
+
try {
|
|
301
|
+
value = JSON.parse(await readFile(resolve(String(path)), "utf8"));
|
|
302
|
+
} catch (error) {
|
|
303
|
+
throw new CliError(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, {
|
|
304
|
+
code: "invalid_json_file",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
308
|
+
throw new CliError(`${label} must contain one JSON object.`, { code: "invalid_json_file" });
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
|
|
259
313
|
async function authLogin(options) {
|
|
260
314
|
const current = await readWorkspaceCredential();
|
|
261
315
|
if (current?.source === "environment" && !options.force) {
|
|
@@ -474,6 +528,9 @@ async function ask(options, positionals) {
|
|
|
474
528
|
const prompt = String(options.prompt || positionals.join(" ")).trim();
|
|
475
529
|
if (!prompt) throw new CliError("Provide --prompt <text> or a positional prompt.");
|
|
476
530
|
const { api } = await client(options);
|
|
531
|
+
const context = options["context-file"]
|
|
532
|
+
? await readJsonObject(options["context-file"], "--context-file")
|
|
533
|
+
: null;
|
|
477
534
|
const result = await api.request("/questions", {
|
|
478
535
|
method: "POST",
|
|
479
536
|
headers: { "idempotency-key": idempotencyKey(options) },
|
|
@@ -482,12 +539,368 @@ async function ask(options, positionals) {
|
|
|
482
539
|
conversation_id: options["conversation-id"] || null,
|
|
483
540
|
mode: options["allow-actions"] ? "allow_actions" : "answer_only",
|
|
484
541
|
sources: listOption(options, "sources", ["workspace", "web", "memory"]),
|
|
542
|
+
context,
|
|
485
543
|
},
|
|
486
544
|
timeoutMs: 330_000,
|
|
487
545
|
});
|
|
488
546
|
writeResult(result, { json: options.json });
|
|
489
547
|
}
|
|
490
548
|
|
|
549
|
+
async function agents(options, action, id) {
|
|
550
|
+
const { api } = await client(options);
|
|
551
|
+
let result;
|
|
552
|
+
if (action === "list") {
|
|
553
|
+
result = await api.request("/agents", { query: { include_revoked: options["include-revoked"] || undefined } });
|
|
554
|
+
} else if (action === "connect") {
|
|
555
|
+
result = await api.request("/agents", {
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: {
|
|
558
|
+
handle: option(options, "handle", { required: true }),
|
|
559
|
+
avatar_url: options["avatar-url"] || null,
|
|
560
|
+
provider: options.provider || "custom",
|
|
561
|
+
permissions: listOption(options, "permissions", ["read", "comment"]),
|
|
562
|
+
pending_naming: true,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
} else if (action === "create") {
|
|
566
|
+
result = await api.request("/agents", {
|
|
567
|
+
method: "POST",
|
|
568
|
+
body: {
|
|
569
|
+
handle: option(options, "handle", { required: true }),
|
|
570
|
+
display_name: option(options, "display-name", { required: true }),
|
|
571
|
+
avatar_url: options["avatar-url"] || null,
|
|
572
|
+
provider: options.provider || "custom",
|
|
573
|
+
permissions: listOption(options, "permissions", ["read", "comment"]),
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
} else if (action === "update") {
|
|
577
|
+
const agentId = id || option(options, "id", { required: true });
|
|
578
|
+
const body = {};
|
|
579
|
+
if (options["display-name"] !== undefined) body.display_name = options["display-name"];
|
|
580
|
+
if (options["avatar-url"] !== undefined) body.avatar_url = options["avatar-url"];
|
|
581
|
+
if (options.status !== undefined) body.status = options.status;
|
|
582
|
+
if (options.permissions !== undefined) body.permissions = listOption(options, "permissions");
|
|
583
|
+
if (Object.keys(body).length === 0) throw new CliError("Provide an Agent field to update.", { code: "agent_update_required" });
|
|
584
|
+
result = await api.request(`/agents/${resourcePath(agentId)}`, { method: "PATCH", body });
|
|
585
|
+
} else if (action === "rotate") {
|
|
586
|
+
const agentId = id || option(options, "id", { required: true });
|
|
587
|
+
result = await api.request(`/agents/${resourcePath(agentId)}/credentials:rotate`, { method: "POST" });
|
|
588
|
+
} else {
|
|
589
|
+
throw new CliError("agents action must be list, connect, create, update, or rotate.");
|
|
590
|
+
}
|
|
591
|
+
writeResult(result, { json: options.json });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function claimAgentEvent(api, options, waitFallback = 0) {
|
|
595
|
+
return api.request("/agent-events:claim", {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: {
|
|
598
|
+
worker_id: options["worker-id"] || `corvio-cli-${process.pid}`,
|
|
599
|
+
lease_seconds: numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 }),
|
|
600
|
+
wait_seconds: numberOption(options, "wait-seconds", waitFallback, { minimum: 0, maximum: 30 }),
|
|
601
|
+
},
|
|
602
|
+
timeoutMs: 45_000,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function runAgentHandler(executable, event) {
|
|
607
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
608
|
+
const handlerEvent = { ...event, delivery: { ...(event?.delivery || {}) } };
|
|
609
|
+
delete handlerEvent.delivery.lease_token;
|
|
610
|
+
const handlerEnv = { ...process.env, CORVIO_AGENT_EVENT_ID: String(event.id || "") };
|
|
611
|
+
delete handlerEnv.CORVIO_API_KEY;
|
|
612
|
+
const child = spawn(String(executable), [], {
|
|
613
|
+
shell: false,
|
|
614
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
615
|
+
env: handlerEnv,
|
|
616
|
+
});
|
|
617
|
+
let stdout = "";
|
|
618
|
+
let stderr = "";
|
|
619
|
+
const maximumBytes = 1024 * 1024;
|
|
620
|
+
child.stdout.setEncoding("utf8");
|
|
621
|
+
child.stderr.setEncoding("utf8");
|
|
622
|
+
child.stdout.on("data", (chunk) => {
|
|
623
|
+
stdout += chunk;
|
|
624
|
+
if (Buffer.byteLength(stdout) > maximumBytes) child.kill("SIGTERM");
|
|
625
|
+
});
|
|
626
|
+
child.stderr.on("data", (chunk) => {
|
|
627
|
+
stderr = `${stderr}${chunk}`.slice(-20_000);
|
|
628
|
+
});
|
|
629
|
+
child.on("error", (error) => rejectPromise(new CliError(`Agent handler could not start: ${error.message}`, { code: "agent_handler_failed" })));
|
|
630
|
+
child.on("close", (code) => {
|
|
631
|
+
if (code !== 0) {
|
|
632
|
+
rejectPromise(new CliError(`Agent handler exited with ${code}: ${stderr.trim() || "no error output"}`, { code: "agent_handler_failed" }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
let decision;
|
|
636
|
+
try {
|
|
637
|
+
decision = JSON.parse(stdout.trim());
|
|
638
|
+
} catch {
|
|
639
|
+
rejectPromise(new CliError("Agent handler must print one JSON object.", { code: "agent_handler_result_invalid" }));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const instruction = String(decision?.instruction || "").trim();
|
|
643
|
+
if (!instruction) {
|
|
644
|
+
rejectPromise(new CliError("Agent handler result requires a non-empty instruction.", { code: "agent_handler_result_invalid" }));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
resolvePromise({ instruction, reply: String(decision?.reply || "").trim() || null });
|
|
648
|
+
});
|
|
649
|
+
child.stdin.end(`${JSON.stringify(handlerEvent)}\n`);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function startAgentLeaseHeartbeat(api, event, leaseToken, leaseSeconds) {
|
|
654
|
+
let stopped = false;
|
|
655
|
+
let timer = null;
|
|
656
|
+
let inFlight = null;
|
|
657
|
+
let failure = null;
|
|
658
|
+
const intervalMs = Math.max(10_000, Math.min(60_000, Math.floor(leaseSeconds * 1_000 / 3)));
|
|
659
|
+
const renew = async () => {
|
|
660
|
+
if (stopped || failure) return;
|
|
661
|
+
try {
|
|
662
|
+
await api.request(`/agent-events/${resourcePath(event.id)}:renew`, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
body: { lease_token: leaseToken, lease_seconds: leaseSeconds },
|
|
665
|
+
});
|
|
666
|
+
} catch (error) {
|
|
667
|
+
failure = error;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!stopped) timer = setTimeout(() => { inFlight = renew(); }, intervalMs);
|
|
671
|
+
};
|
|
672
|
+
inFlight = renew();
|
|
673
|
+
await inFlight;
|
|
674
|
+
if (failure) throw failure;
|
|
675
|
+
return {
|
|
676
|
+
async stop() {
|
|
677
|
+
stopped = true;
|
|
678
|
+
if (timer) clearTimeout(timer);
|
|
679
|
+
if (inFlight) await inFlight;
|
|
680
|
+
},
|
|
681
|
+
assertHealthy() {
|
|
682
|
+
if (failure) throw failure;
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function processClaimedAgentEvent(api, event, options) {
|
|
688
|
+
const leaseToken = String(event?.delivery?.lease_token || "");
|
|
689
|
+
if (!event?.id || !leaseToken) throw new CliError("Claimed event is missing its lease receipt.", { code: "agent_event_invalid" });
|
|
690
|
+
const leaseSeconds = numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 });
|
|
691
|
+
let heartbeat = null;
|
|
692
|
+
try {
|
|
693
|
+
heartbeat = await startAgentLeaseHeartbeat(api, event, leaseToken, leaseSeconds);
|
|
694
|
+
const decision = await runAgentHandler(option(options, "handler", { required: true }), event);
|
|
695
|
+
const question = await api.request("/questions", {
|
|
696
|
+
method: "POST",
|
|
697
|
+
headers: { "idempotency-key": `agent-event:${event.id}:query` },
|
|
698
|
+
body: {
|
|
699
|
+
prompt: decision.instruction,
|
|
700
|
+
mode: "allow_actions",
|
|
701
|
+
sources: listOption(options, "sources", ["workspace", "web", "memory"]),
|
|
702
|
+
context: {
|
|
703
|
+
agent_event: {
|
|
704
|
+
id: event.id,
|
|
705
|
+
kind: event.kind,
|
|
706
|
+
object_kind: event.object_kind,
|
|
707
|
+
object_id: event.object_id,
|
|
708
|
+
actor_user_id: event.actor_user_id,
|
|
709
|
+
actor_agent_id: event.actor_agent_id,
|
|
710
|
+
lineage: event.lineage,
|
|
711
|
+
},
|
|
712
|
+
collaboration: event.context,
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
timeoutMs: 330_000,
|
|
716
|
+
});
|
|
717
|
+
if (String(question.status || "") !== "completed") {
|
|
718
|
+
throw new CliError(`Corvio Query did not complete durably (status: ${question.status || "unknown"}).`, {
|
|
719
|
+
code: "agent_query_not_completed",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const documentId = String(event?.context?.document?.document_id || "");
|
|
723
|
+
const threadId = String(event?.context?.comment?.thread_id || "");
|
|
724
|
+
let commentEntryId = null;
|
|
725
|
+
if (documentId && threadId) {
|
|
726
|
+
const reply = String(decision.reply || question.answer || "Completed the requested review.").trim().slice(0, 12_000);
|
|
727
|
+
const mentionTargets = await resolveAgentMentionTargets(api, documentId, reply);
|
|
728
|
+
const thread = await api.request(`/documents/${resourcePath(documentId)}/comments/${resourcePath(threadId)}/replies`, {
|
|
729
|
+
method: "POST",
|
|
730
|
+
body: {
|
|
731
|
+
body: reply,
|
|
732
|
+
mention_targets: mentionTargets,
|
|
733
|
+
entry_point: "external_agent_runner",
|
|
734
|
+
source_event_id: event.id,
|
|
735
|
+
source_event_lease_token: leaseToken,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
const entries = Array.isArray(thread?.entries) ? thread.entries : [];
|
|
739
|
+
commentEntryId = entries.at(-1)?.id || null;
|
|
740
|
+
}
|
|
741
|
+
await heartbeat.stop();
|
|
742
|
+
heartbeat.assertHealthy();
|
|
743
|
+
const completed = await api.request(`/agent-events/${resourcePath(event.id)}:complete`, {
|
|
744
|
+
method: "POST",
|
|
745
|
+
body: {
|
|
746
|
+
lease_token: leaseToken,
|
|
747
|
+
result: {
|
|
748
|
+
question_id: question.id,
|
|
749
|
+
...(commentEntryId ? { comment_entry_id: commentEntryId } : {}),
|
|
750
|
+
status: question.status,
|
|
751
|
+
artifact_ids: Array.isArray(question.artifacts) ? question.artifacts.map((item) => item.document_id).filter(Boolean) : [],
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
return { event: completed, question, comment_entry_id: commentEntryId };
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (heartbeat) {
|
|
758
|
+
try {
|
|
759
|
+
await heartbeat.stop();
|
|
760
|
+
} catch {
|
|
761
|
+
// The original execution error remains the failure authority.
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
await api.request(`/agent-events/${resourcePath(event.id)}:fail`, {
|
|
766
|
+
method: "POST",
|
|
767
|
+
body: { lease_token: leaseToken, error: error instanceof Error ? error.message : String(error), retryable: true },
|
|
768
|
+
});
|
|
769
|
+
} catch {
|
|
770
|
+
// Preserve the original execution failure; the expired lease remains reclaimable.
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function runAgent(options) {
|
|
777
|
+
const api = agentClient(options);
|
|
778
|
+
while (true) {
|
|
779
|
+
const claimed = await claimAgentEvent(api, options, options.once ? 0 : 30);
|
|
780
|
+
if (!claimed.event) {
|
|
781
|
+
if (options.once) return writeResult({ processed: false, reason: "no_event", retry_after_seconds: claimed.retry_after_seconds }, { json: options.json });
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const receipt = await processClaimedAgentEvent(api, claimed.event, options);
|
|
785
|
+
if (options.once) return writeResult({ processed: true, ...receipt }, { json: options.json });
|
|
786
|
+
process.stdout.write(`${JSON.stringify({ processed: true, ...receipt })}\n`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function resolveAgentMentionTargets(api, documentId, body) {
|
|
791
|
+
const aliases = Array.from(String(body || "").matchAll(/(?:^|\s)@([a-z0-9][a-z0-9._-]{0,63})/gi))
|
|
792
|
+
.map((match) => String(match[1] || "").toLowerCase());
|
|
793
|
+
if (!aliases.length) return [];
|
|
794
|
+
const response = await api.request(`/documents/${resourcePath(documentId)}/mention-candidates`);
|
|
795
|
+
const byHandle = new Map((Array.isArray(response?.data) ? response.data : []).map((candidate) => [
|
|
796
|
+
String(candidate.handle || "").toLowerCase(),
|
|
797
|
+
candidate,
|
|
798
|
+
]));
|
|
799
|
+
const seen = new Set();
|
|
800
|
+
return aliases.flatMap((alias) => {
|
|
801
|
+
const candidate = byHandle.get(alias);
|
|
802
|
+
if (!candidate?.id || seen.has(candidate.id)) return [];
|
|
803
|
+
seen.add(candidate.id);
|
|
804
|
+
return [{ kind: "agent", principal_id: candidate.id, alias: candidate.handle }];
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function agentCommand(options, action, id) {
|
|
809
|
+
if (action === "run") return runAgent(options);
|
|
810
|
+
const api = agentClient(options);
|
|
811
|
+
let result;
|
|
812
|
+
if (action === "claim") {
|
|
813
|
+
result = await claimAgentEvent(api, options);
|
|
814
|
+
} else if (action === "renew") {
|
|
815
|
+
const eventId = id || option(options, "id", { required: true });
|
|
816
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:renew`, {
|
|
817
|
+
method: "POST",
|
|
818
|
+
body: {
|
|
819
|
+
lease_token: option(options, "lease-token", { required: true }),
|
|
820
|
+
lease_seconds: numberOption(options, "lease-seconds", 300, { minimum: 30, maximum: 900 }),
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
} else if (action === "complete") {
|
|
824
|
+
const eventId = id || option(options, "id", { required: true });
|
|
825
|
+
const receipt = options["result-file"]
|
|
826
|
+
? await readJsonObject(options["result-file"], "--result-file")
|
|
827
|
+
: {
|
|
828
|
+
...(options["question-id"] ? { question_id: options["question-id"] } : {}),
|
|
829
|
+
...(options["comment-entry-id"] ? { comment_entry_id: options["comment-entry-id"] } : {}),
|
|
830
|
+
...(options["no-change-reason"] ? { no_change_reason: options["no-change-reason"] } : {}),
|
|
831
|
+
};
|
|
832
|
+
if (Object.keys(receipt).length === 0) throw new CliError("Provide a durable completion receipt.", { code: "agent_event_receipt_required" });
|
|
833
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:complete`, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
body: { lease_token: option(options, "lease-token", { required: true }), result: receipt },
|
|
836
|
+
});
|
|
837
|
+
} else if (action === "fail") {
|
|
838
|
+
const eventId = id || option(options, "id", { required: true });
|
|
839
|
+
result = await api.request(`/agent-events/${resourcePath(eventId)}:fail`, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
body: {
|
|
842
|
+
lease_token: option(options, "lease-token", { required: true }),
|
|
843
|
+
error: option(options, "error", { required: true }),
|
|
844
|
+
retryable: String(options.retryable ?? "true").toLowerCase() !== "false",
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
} else if (action === "comments") {
|
|
848
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments`);
|
|
849
|
+
} else if (action === "comment") {
|
|
850
|
+
const documentId = option(options, "document-id", { required: true });
|
|
851
|
+
const body = option(options, "body", { required: true });
|
|
852
|
+
const selection = options["selection-file"]
|
|
853
|
+
? await readJsonObject(options["selection-file"], "--selection-file")
|
|
854
|
+
: { target_scope: "document" };
|
|
855
|
+
result = await api.request(`/documents/${resourcePath(documentId)}/comments`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
body: {
|
|
858
|
+
body,
|
|
859
|
+
mention_targets: await resolveAgentMentionTargets(api, documentId, body),
|
|
860
|
+
selection,
|
|
861
|
+
entry_point: "external_agent_cli",
|
|
862
|
+
start_new_chain: Boolean(options["start-new-chain"]),
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
} else if (action === "reply") {
|
|
866
|
+
const threadId = id || option(options, "id", { required: true });
|
|
867
|
+
const documentId = option(options, "document-id", { required: true });
|
|
868
|
+
const bodyText = option(options, "body", { required: true });
|
|
869
|
+
const hasSourceEventId = Boolean(options["source-event-id"]);
|
|
870
|
+
const hasSourceEventLease = Boolean(options["source-event-lease-token"]);
|
|
871
|
+
if (hasSourceEventId !== hasSourceEventLease) {
|
|
872
|
+
throw new CliError("Provide --source-event-id and --source-event-lease-token together.", {
|
|
873
|
+
code: "agent_reply_source_incomplete",
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (hasSourceEventId && options["start-new-chain"]) {
|
|
877
|
+
throw new CliError("A source event reply cannot also start a new chain.", {
|
|
878
|
+
code: "agent_reply_source_conflict",
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
result = await api.request(`/documents/${resourcePath(documentId)}/comments/${resourcePath(threadId)}/replies`, {
|
|
882
|
+
method: "POST",
|
|
883
|
+
body: {
|
|
884
|
+
body: bodyText,
|
|
885
|
+
mention_targets: await resolveAgentMentionTargets(api, documentId, bodyText),
|
|
886
|
+
entry_point: "external_agent_cli_reply",
|
|
887
|
+
...(options["source-event-id"] ? { source_event_id: options["source-event-id"] } : {}),
|
|
888
|
+
...(options["source-event-lease-token"] ? { source_event_lease_token: options["source-event-lease-token"] } : {}),
|
|
889
|
+
start_new_chain: Boolean(options["start-new-chain"]),
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
} else if (action === "status") {
|
|
893
|
+
const threadId = id || option(options, "id", { required: true });
|
|
894
|
+
result = await api.request(`/documents/${resourcePath(option(options, "document-id", { required: true }))}/comments/${resourcePath(threadId)}/status`, {
|
|
895
|
+
method: "POST",
|
|
896
|
+
body: { status: option(options, "status", { required: true }), entry_point: "external_agent_cli_status" },
|
|
897
|
+
});
|
|
898
|
+
} else {
|
|
899
|
+
throw new CliError("Unknown agent runtime action.");
|
|
900
|
+
}
|
|
901
|
+
writeResult(result, { json: options.json });
|
|
902
|
+
}
|
|
903
|
+
|
|
491
904
|
function pageQuery(options) {
|
|
492
905
|
return {
|
|
493
906
|
limit: numberOption(options, "limit", 20, { maximum: 100 }),
|
|
@@ -1256,6 +1669,8 @@ async function main({ options, positionals }) {
|
|
|
1256
1669
|
}
|
|
1257
1670
|
if (command === "capabilities") return capabilities(options);
|
|
1258
1671
|
if (command === "ask") return ask(options, [action, id, ...rest].filter(Boolean));
|
|
1672
|
+
if (command === "agents") return agents(options, action || "list", id);
|
|
1673
|
+
if (command === "agent") return agentCommand(options, action || "claim", id);
|
|
1259
1674
|
if (command === "questions") return questions(options, action || "list", id);
|
|
1260
1675
|
if (command === "conversations") return conversations(options, action || "list", id);
|
|
1261
1676
|
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);
|