@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 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.5";
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corvio/cli",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Official Corvio Workspace CLI for questions, knowledge search, documents, and Agent workflows.",
5
5
  "type": "module",
6
6
  "bin": {