@opencomputer/cli 0.6.5 → 0.6.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/dist/commands.js CHANGED
@@ -7,6 +7,8 @@ import { ensureProjectBinding, findOpenComputerProjectRoot, } from "./binding.js
7
7
  import { assertStarterTarget, buildAgentArtifact, findAgentRoot, initializeAgentProject, } from "./project.js";
8
8
  import { developmentAgentReference, parseSessionCommand, resolveProjectAgent, } from "./session-command.js";
9
9
  import { formatSessionEvent } from "./session-prompt.js";
10
+ import { doctorProject } from "./doctor.js";
11
+ import { CLIError } from "./errors.js";
10
12
  export function deploymentAlias(requestedAlias) {
11
13
  return requestedAlias ?? "development";
12
14
  }
@@ -16,6 +18,9 @@ export function shouldBindModelAccessProject(projectReference, currentAgentRoot)
16
18
  function printJSON(value) {
17
19
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
18
20
  }
21
+ function printJSONLine(value) {
22
+ process.stdout.write(`${JSON.stringify(value)}\n`);
23
+ }
19
24
  function flag(args, name) {
20
25
  const index = args.indexOf(name);
21
26
  if (index < 0)
@@ -56,52 +61,33 @@ function consumeModelAccessProvider(args) {
56
61
  }
57
62
  return "codex";
58
63
  }
59
- async function readSecretValue() {
60
- if (!process.stdin.isTTY) {
61
- const chunks = [];
62
- for await (const chunk of process.stdin) {
63
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
64
- }
65
- const value = Buffer.concat(chunks)
66
- .toString("utf8")
67
- .replace(/\r?\n$/, "");
68
- if (!value)
69
- throw new Error("Secret value was empty");
70
- return value;
71
- }
72
- process.stderr.write("Secret value (hidden): ");
73
- process.stdin.setRawMode(true);
74
- process.stdin.resume();
75
- return new Promise((resolve, reject) => {
76
- let value = "";
77
- const finish = (error) => {
78
- process.stdin.off("data", onData);
79
- process.stdin.setRawMode(false);
80
- process.stdin.pause();
81
- process.stderr.write("\n");
82
- if (error)
83
- reject(error);
84
- else if (!value)
85
- reject(new Error("Secret value was empty"));
86
- else
87
- resolve(value);
88
- };
89
- const onData = (chunk) => {
90
- for (const byte of chunk) {
91
- if (byte === 3)
92
- return finish(new Error("Secret entry cancelled"));
93
- if (byte === 10 || byte === 13)
94
- return finish();
95
- if (byte === 8 || byte === 127)
96
- value = value.slice(0, -1);
97
- else
98
- value += String.fromCharCode(byte);
99
- }
100
- };
101
- process.stdin.on("data", onData);
102
- });
64
+ async function readStdinValue(enabled) {
65
+ if (!enabled) {
66
+ throw new CLIError("value_stdin_required", "A value must be supplied through standard input.", "Pipe the value into this command and add `--value-stdin`.");
67
+ }
68
+ if (process.stdin.isTTY) {
69
+ throw new CLIError("value_stdin_required", "--value-stdin requires piped standard input.", "Use `printf %s \"$VALUE\" | opencomputer ... --value-stdin`.");
70
+ }
71
+ const chunks = [];
72
+ for await (const chunk of process.stdin) {
73
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
74
+ }
75
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
76
+ if (!value)
77
+ throw new Error("Standard-input value was empty");
78
+ return value;
103
79
  }
104
- async function selectedProject(client, config, reference, interactive = true) {
80
+ function printDoctor(result, json) {
81
+ if (json)
82
+ return printJSON(result);
83
+ for (const item of result.diagnostics) {
84
+ process.stdout.write(`${item.severity.toUpperCase()} ${item.code} ${item.file}${item.line ? `:${item.line}` : ""}\n` +
85
+ ` ${item.message}\n fix: ${item.hint}\n`);
86
+ }
87
+ process.stdout.write(`${result.ok ? "Doctor passed" : "Doctor failed"}: ${result.summary.errors} errors, ` +
88
+ `${result.summary.warnings} warnings in ${result.durationMs}ms.\n`);
89
+ }
90
+ async function selectedProject(client, config, reference) {
105
91
  if (reference) {
106
92
  const project = (await client.projects()).find((candidate) => candidate.id === reference || candidate.slug === reference);
107
93
  if (!project)
@@ -112,9 +98,7 @@ async function selectedProject(client, config, reference, interactive = true) {
112
98
  return { projectId: project.id, agentId: agent.id };
113
99
  }
114
100
  const root = await findOpenComputerProjectRoot(process.cwd());
115
- const binding = await ensureProjectBinding(client, config, root, {
116
- interactive,
117
- });
101
+ const binding = await ensureProjectBinding(client, config, root);
118
102
  return { projectId: binding.projectId, agentId: binding.agentId };
119
103
  }
120
104
  async function selectedSessionAgent(client, project, selector) {
@@ -200,7 +184,7 @@ function printAgentEvent(event, json) {
200
184
  printToolProgress(event);
201
185
  }
202
186
  }
203
- async function sendAgentTurn(client, sessionId, prompt, keep, json) {
187
+ async function sendAgentTurn(client, sessionId, prompt, keep, json, idempotencyKey) {
204
188
  const existing = await client.events(sessionId, 0);
205
189
  let cursor = existing.at(-1)?.seq ?? 0;
206
190
  const session = await client.session(sessionId);
@@ -210,7 +194,7 @@ async function sendAgentTurn(client, sessionId, prompt, keep, json) {
210
194
  const connected = await waitForEvent(client, sessionId, cursor, (event) => event.type === "runtime.connected", () => undefined, 90_000);
211
195
  cursor = connected.cursor;
212
196
  }
213
- const turn = await client.createTurn(sessionId, prompt);
197
+ const turn = await client.createTurn(sessionId, prompt, idempotencyKey);
214
198
  let streamedText = "";
215
199
  let completedText = "";
216
200
  const completed = await waitForEvent(client, sessionId, cursor, (event) => event.type === "turn.completed" || event.type === "turn.failed", (event) => {
@@ -274,6 +258,33 @@ async function attachSession(client, sessionId, json) {
274
258
  process.off("SIGINT", stop);
275
259
  }
276
260
  }
261
+ async function tailSession(client, sessionId, after, follow, json) {
262
+ let cursor = after;
263
+ let stopped = false;
264
+ const stop = () => {
265
+ stopped = true;
266
+ };
267
+ process.once("SIGINT", stop);
268
+ try {
269
+ do {
270
+ const events = await client.events(sessionId, cursor);
271
+ for (const event of events) {
272
+ cursor = Math.max(cursor, event.seq);
273
+ if (json)
274
+ printJSONLine({ sessionId, cursor: event.seq, ...event });
275
+ else {
276
+ process.stdout.write(`${event.seq} ${event.type} ${JSON.stringify(event.data)}\n`);
277
+ }
278
+ }
279
+ if (follow && !stopped) {
280
+ await new Promise((resolve) => setTimeout(resolve, 500));
281
+ }
282
+ } while (follow && !stopped);
283
+ }
284
+ finally {
285
+ process.off("SIGINT", stop);
286
+ }
287
+ }
277
288
  export function nextAgentEventDeadline(deadline, timeoutMs, receivedEvents, now = Date.now()) {
278
289
  return receivedEvents > 0 ? now + timeoutMs : deadline;
279
290
  }
@@ -301,11 +312,11 @@ async function waitForEvent(client, sessionId, after, terminal, onEvent, timeout
301
312
  }
302
313
  throw new Error("Timed out waiting for the agent.");
303
314
  }
304
- async function runAgent(client, agent, prompt, keep, json, verbose) {
315
+ async function runAgent(client, agent, prompt, keep, json, verbose, idempotencyKey) {
305
316
  const created = await client.createSession(agent);
306
317
  process.stderr.write(`Starting ${agent}…\n`);
307
318
  const connected = await waitForEvent(client, created.session.id, 0, (event) => event.type === "runtime.connected", (event) => printSessionProgress(event, json, verbose), 90_000);
308
- const turn = await client.createTurn(created.session.id, prompt);
319
+ const turn = await client.createTurn(created.session.id, prompt, idempotencyKey);
309
320
  let streamed = false;
310
321
  let streamedText = "";
311
322
  let completedText = "";
@@ -348,7 +359,7 @@ async function runAgent(client, agent, prompt, keep, json, verbose) {
348
359
  export async function runCommand(command, rawArgs, globals) {
349
360
  const args = [...rawArgs];
350
361
  const config = await resolveConfig(globals);
351
- const client = new OpenComputerClient(config);
362
+ const client = new OpenComputerClient(config, globals.idempotencyKey);
352
363
  if (command === "login") {
353
364
  const identity = await login(config, {
354
365
  noBrowser: flag(args, "--no-browser"),
@@ -409,7 +420,7 @@ export async function runCommand(command, rawArgs, globals) {
409
420
  const enterDirectory = directory === "." ? "" : ` cd ${directory}\n`;
410
421
  process.stdout.write(`Created the ${initialized.manifest.name} OpenComputer app\n` +
411
422
  `Directory: ${initialized.root}\n` +
412
- `Project: choose or create one on the first watched deployment\n` +
423
+ `Project: link explicitly with --project or --create-project\n` +
413
424
  `Agents: opencomputer/\n` +
414
425
  (spa
415
426
  ? `Web app: src/ (separate lifecycle)\n\n`
@@ -442,13 +453,28 @@ export async function runCommand(command, rawArgs, globals) {
442
453
  }
443
454
  return;
444
455
  }
456
+ if (command === "doctor") {
457
+ if (args.length)
458
+ throw new Error(`Unexpected argument: ${args[0]}`);
459
+ const root = await findOpenComputerProjectRoot(process.cwd());
460
+ const result = await doctorProject(root);
461
+ if (!result.ok) {
462
+ if (!globals.json)
463
+ printDoctor(result, false);
464
+ throw new CLIError("doctor_failed", "Local project diagnostics failed.", "Fix the reported errors and rerun `opencomputer doctor --json`.", result);
465
+ }
466
+ printDoctor(result, globals.json);
467
+ return;
468
+ }
445
469
  if (command === "link") {
470
+ const project = option(args, "--project");
471
+ const createProjectName = option(args, "--create-project");
446
472
  if (args.length)
447
473
  throw new Error(`Unexpected argument: ${args[0]}`);
448
474
  const root = await findOpenComputerProjectRoot(process.cwd());
449
475
  const binding = await ensureProjectBinding(client, config, root, {
450
- interactive: !globals.json,
451
- select: true,
476
+ project,
477
+ createProjectName,
452
478
  });
453
479
  if (globals.json)
454
480
  printJSON(binding);
@@ -468,6 +494,10 @@ export async function runCommand(command, rawArgs, globals) {
468
494
  if (!root) {
469
495
  throw new Error("No OpenComputer project found. Run `opencomputer init <directory>` first.");
470
496
  }
497
+ const diagnosis = await doctorProject(root);
498
+ if (!diagnosis.ok) {
499
+ throw new CLIError("doctor_failed", "Deploy stopped because local project diagnostics failed.", "Run `opencomputer doctor --json`, fix the errors, and deploy again.", diagnosis);
500
+ }
471
501
  if (watch) {
472
502
  if (requestedAlias && requestedAlias !== "development") {
473
503
  throw new Error("--watch deploys only to development; omit --alias or use --alias development");
@@ -475,7 +505,6 @@ export async function runCommand(command, rawArgs, globals) {
475
505
  await runDeploymentWatch(client, config, root, {
476
506
  project,
477
507
  createProjectName,
478
- interactive: !globals.json,
479
508
  });
480
509
  return;
481
510
  }
@@ -483,10 +512,7 @@ export async function runCommand(command, rawArgs, globals) {
483
512
  throw new Error("--project and --create-project require --watch");
484
513
  }
485
514
  const alias = deploymentAlias(requestedAlias);
486
- const binding = await ensureProjectBinding(client, config, root, {
487
- interactive: !globals.json,
488
- select: true,
489
- });
515
+ const binding = await ensureProjectBinding(client, config, root);
490
516
  const results = await publishProjectDeployment(client, root, binding, alias);
491
517
  for (const { built } of results) {
492
518
  process.stderr.write(`Built ${built.agentId} in ${String(built.elapsedMs)}ms\n`);
@@ -509,7 +535,7 @@ export async function runCommand(command, rawArgs, globals) {
509
535
  if (!agent || !prompt) {
510
536
  throw new Error("Usage: opencomputer run <agent> <prompt>");
511
537
  }
512
- const result = await runAgent(client, agent, prompt, keep, globals.json, globals.verbose === true);
538
+ const result = await runAgent(client, agent, prompt, keep, globals.json, globals.verbose === true, globals.idempotencyKey);
513
539
  if (globals.json)
514
540
  printJSON(result);
515
541
  return;
@@ -520,10 +546,14 @@ export async function runCommand(command, rawArgs, globals) {
520
546
  if (args.length)
521
547
  throw new Error(`Unexpected argument: ${args[0]}`);
522
548
  process.stderr.write("`opencomputer dev` is deprecated. Use `opencomputer deploy --watch`; start any web app separately.\n");
523
- await runCloudDevelopment(client, config, await findOpenComputerProjectRoot(process.cwd()), {
549
+ const root = await findOpenComputerProjectRoot(process.cwd());
550
+ const diagnosis = await doctorProject(root);
551
+ if (!diagnosis.ok) {
552
+ throw new CLIError("doctor_failed", "Deploy stopped because local project diagnostics failed.", "Run `opencomputer doctor --json`, fix the errors, and deploy again.", diagnosis);
553
+ }
554
+ await runCloudDevelopment(client, config, root, {
524
555
  project,
525
556
  createProjectName,
526
- interactive: !globals.json,
527
557
  });
528
558
  return;
529
559
  }
@@ -532,7 +562,7 @@ export async function runCommand(command, rawArgs, globals) {
532
562
  const projectReference = option(args, "--project");
533
563
  const agentOption = option(args, "--agent");
534
564
  const environment = environmentOption(option(args, "--environment"));
535
- const project = await selectedProject(client, config, projectReference, !globals.json);
565
+ const project = await selectedProject(client, config, projectReference);
536
566
  const agentId = agentOption
537
567
  ? agentOption === "current"
538
568
  ? project.agentId
@@ -565,6 +595,7 @@ export async function runCommand(command, rawArgs, globals) {
565
595
  }
566
596
  if (action === "set") {
567
597
  const explicitOrigins = options(args, "--allow-origin");
598
+ const valueStdin = flag(args, "--value-stdin");
568
599
  if (args.length)
569
600
  throw new Error(`Unexpected argument: ${args[0]}`);
570
601
  let allowedOrigins = explicitOrigins;
@@ -584,7 +615,7 @@ export async function runCommand(command, rawArgs, globals) {
584
615
  const secret = await client.putSecret({
585
616
  projectId: project.projectId,
586
617
  name,
587
- value: await readSecretValue(),
618
+ value: await readStdinValue(valueStdin),
588
619
  environment,
589
620
  ...(agentId ? { agentId } : {}),
590
621
  allowedOrigins,
@@ -636,7 +667,7 @@ export async function runCommand(command, rawArgs, globals) {
636
667
  throw new Error(`Unexpected argument: ${args[0]}`);
637
668
  const currentAgentRoot = projectReference ? null : await findAgentRoot();
638
669
  const project = shouldBindModelAccessProject(projectReference, currentAgentRoot)
639
- ? await selectedProject(client, config, projectReference, false)
670
+ ? await selectedProject(client, config, projectReference)
640
671
  : undefined;
641
672
  const environments = project
642
673
  ? ["development", "production"]
@@ -709,7 +740,7 @@ export async function runCommand(command, rawArgs, globals) {
709
740
  const projectReference = option(args, "--project");
710
741
  const agentOption = option(args, "--agent");
711
742
  const environment = environmentOption(option(args, "--environment"));
712
- const project = await selectedProject(client, config, projectReference, !globals.json);
743
+ const project = await selectedProject(client, config, projectReference);
713
744
  const agentId = agentOption
714
745
  ? agentOption === "current"
715
746
  ? project.agentId
@@ -740,12 +771,13 @@ export async function runCommand(command, rawArgs, globals) {
740
771
  throw new Error("Use `opencomputer env set|list|remove <name>`.");
741
772
  }
742
773
  if (action === "set") {
774
+ const valueStdin = flag(args, "--value-stdin");
743
775
  if (args.length)
744
776
  throw new Error(`Unexpected argument: ${args[0]}`);
745
777
  const variable = await client.putRuntimeVariable({
746
778
  projectId: project.projectId,
747
779
  name,
748
- value: await readSecretValue(),
780
+ value: await readStdinValue(valueStdin),
749
781
  environment,
750
782
  ...(agentId ? { agentId } : {}),
751
783
  });
@@ -779,7 +811,7 @@ export async function runCommand(command, rawArgs, globals) {
779
811
  const projectReference = option(args, "--project");
780
812
  const agentOption = option(args, "--agent");
781
813
  const environment = environmentOption(option(args, "--environment"));
782
- const project = await selectedProject(client, config, projectReference, !globals.json);
814
+ const project = await selectedProject(client, config, projectReference);
783
815
  const agentId = await selectedSessionAgent(client, project, !agentOption || agentOption === "current" ? undefined : agentOption);
784
816
  if (action === "list") {
785
817
  if (args.length)
@@ -806,19 +838,26 @@ export async function runCommand(command, rawArgs, globals) {
806
838
  if (!name || args.length) {
807
839
  throw new Error("Use `opencomputer webhooks create <name>`.");
808
840
  }
809
- const webhook = await client.createWebhook({
841
+ const existing = (await client.webhooks({
810
842
  projectId: project.projectId,
811
- name,
812
843
  environment,
813
844
  agentId,
814
- });
845
+ })).find((candidate) => candidate.name === name);
846
+ const webhook = existing ??
847
+ (await client.createWebhook({
848
+ projectId: project.projectId,
849
+ name,
850
+ environment,
851
+ agentId,
852
+ }));
815
853
  if (globals.json)
816
- printJSON(webhook);
854
+ printJSON({ ...webhook, reused: Boolean(existing) });
817
855
  else {
818
- process.stdout.write(`Created ${webhook.name} (${webhook.id}) for ${agentId}@${environment}.\n` +
856
+ process.stdout.write(`${existing ? "Reused" : "Created"} ${webhook.name} (${webhook.id}) for ${agentId}@${environment}.\n` +
819
857
  `URL: ${webhook.invocationUrl}\n` +
820
- `Token: ${webhook.token ?? "unavailable"}\n` +
821
- "Save this token now. It will not be shown again.\n");
858
+ (existing
859
+ ? "The existing token remains unchanged.\n"
860
+ : `Token: ${webhook.token ?? "unavailable"}\nSave this token now. It will not be shown again.\n`));
822
861
  }
823
862
  return;
824
863
  }
@@ -886,7 +925,7 @@ export async function runCommand(command, rawArgs, globals) {
886
925
  insideProject = false;
887
926
  }
888
927
  if (insideProject) {
889
- agentId = (await selectedProject(client, config, undefined, !globals.json)).agentId;
928
+ agentId = (await selectedProject(client, config)).agentId;
890
929
  }
891
930
  }
892
931
  let cursor = "";
@@ -916,7 +955,50 @@ export async function runCommand(command, rawArgs, globals) {
916
955
  }
917
956
  return;
918
957
  }
919
- if (command === "session") {
958
+ if (command === "channels") {
959
+ const action = args.shift();
960
+ if (action !== "status") {
961
+ throw new Error("Use `opencomputer channels status`.");
962
+ }
963
+ const projectReference = option(args, "--project");
964
+ const agentOption = option(args, "--agent");
965
+ const environment = environmentOption(option(args, "--environment"));
966
+ if (args.length)
967
+ throw new Error(`Unexpected argument: ${args[0]}`);
968
+ const project = await selectedProject(client, config, projectReference);
969
+ const agentId = await selectedSessionAgent(client, project, !agentOption || agentOption === "current" ? undefined : agentOption);
970
+ const channels = (await client.channels()).filter((channel) => channel.agentId === agentId && channel.alias === environment);
971
+ if (globals.json)
972
+ printJSON({ projectId: project.projectId, environment, channels });
973
+ else if (!channels.length)
974
+ process.stdout.write("No matching channels.\n");
975
+ else {
976
+ for (const channel of channels) {
977
+ process.stdout.write(`${channel.id} ${channel.status} ${channel.channel} ${channel.agentId}@${channel.alias}\n` +
978
+ ` last event: ${channel.lastEventAt ?? "—"}\n` +
979
+ ` last delivery: ${channel.lastDelivery ? `${channel.lastDelivery.status} at ${channel.lastDelivery.at}` : "—"}\n` +
980
+ ` last error: ${channel.lastError ? `${channel.lastError.category} at ${channel.lastError.at}` : "—"}\n`);
981
+ }
982
+ }
983
+ return;
984
+ }
985
+ if (command === "session" || command === "sessions") {
986
+ if (args[0] === "tail") {
987
+ args.shift();
988
+ const sessionId = args.shift();
989
+ const afterValue = option(args, "--after");
990
+ const follow = !flag(args, "--no-follow");
991
+ if (!sessionId)
992
+ throw new Error("A session ID is required.");
993
+ if (args.length)
994
+ throw new Error(`Unexpected argument: ${args[0]}`);
995
+ const after = afterValue ? Number.parseInt(afterValue, 10) : 0;
996
+ if (!Number.isFinite(after) || after < 0) {
997
+ throw new Error("--after must be a non-negative event cursor");
998
+ }
999
+ await tailSession(client, sessionId, after, follow, globals.json);
1000
+ return;
1001
+ }
920
1002
  const session = parseSessionCommand(args);
921
1003
  const sessionArgs = session.args;
922
1004
  if (session.action === "list") {
@@ -933,11 +1015,11 @@ export async function runCommand(command, rawArgs, globals) {
933
1015
  }
934
1016
  if (session.action === "create") {
935
1017
  const prompt = sessionArgs.join(" ").trim();
936
- const project = await selectedProject(client, config, undefined, !globals.json);
1018
+ const project = await selectedProject(client, config, undefined);
937
1019
  const agentId = await selectedSessionAgent(client, project, session.agent);
938
1020
  const agent = developmentAgentReference(agentId);
939
1021
  if (prompt) {
940
- const result = await runAgent(client, agent, prompt, session.keep, globals.json, globals.verbose === true);
1022
+ const result = await runAgent(client, agent, prompt, session.keep, globals.json, globals.verbose === true, globals.idempotencyKey);
941
1023
  if (globals.json)
942
1024
  printJSON(result);
943
1025
  return;
@@ -983,7 +1065,7 @@ export async function runCommand(command, rawArgs, globals) {
983
1065
  const prompt = sessionArgs.join(" ").trim();
984
1066
  if (!prompt)
985
1067
  throw new Error("A prompt is required.");
986
- const result = await sendAgentTurn(client, sessionId, prompt, session.keep, globals.json);
1068
+ const result = await sendAgentTurn(client, sessionId, prompt, session.keep, globals.json, globals.idempotencyKey);
987
1069
  if (globals.json) {
988
1070
  printJSON({ sessionId, ...result, status: "completed" });
989
1071
  }