@agent-compose/cli 0.3.3 → 0.3.5

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/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
- import { readFileSync as readFileSync6 } from "node:fs";
6
+ import { readFileSync as readFileSync7 } from "node:fs";
7
7
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8
8
  import { program } from "commander";
9
9
 
@@ -147,6 +147,7 @@ function fallbackDashboardUrl(env) {
147
147
  return env === "local" ? "http://localhost:3000" : "https://platform.agentcompose.ai";
148
148
  }
149
149
  var defaultUrl = process.env.AGENT_COMPOSE_URL ?? _envUrl ?? _global.url ?? "https://api.agentcompose.ai";
150
+ var urlIsBuiltinFallback = !process.env.AGENT_COMPOSE_URL && !_envUrl && !_global.url;
150
151
  var defaultDashboardUrl = process.env.AGENT_COMPOSE_DASHBOARD_URL ?? _envDashboardUrl ?? _global.dashboardUrl ?? fallbackDashboardUrl(_activeEnv);
151
152
  var defaultApiKey = process.env.AGENT_COMPOSE_API_KEY ?? _envKey ?? _global.apiKey ?? "";
152
153
  var defaultFactory = process.env.AGENT_COMPOSE_FACTORY ?? _envFactory ?? "default";
@@ -155,6 +156,10 @@ var projectSettings = _project;
155
156
  var localSettings = _local;
156
157
  var activeEnv = _activeEnv;
157
158
  function makeClient({ url, apiKey }) {
159
+ if (urlIsBuiltinFallback && url === "https://api.agentcompose.ai") {
160
+ console.error(`[agentc] No server configured here — targeting https://api.agentcompose.ai (built-in default).
161
+ ` + " If you meant another environment: pass --url, set AGENT_COMPOSE_URL, or run from a repo with .agentc settings.");
162
+ }
158
163
  if (!apiKey) {
159
164
  console.error(`API key required. Provide via:
160
165
  ` + ` • Project: .agentc/settings.local.json
@@ -184,13 +189,17 @@ var registerCommand = new Command("register").description("Register a workflow w
184
189
  console.error(`Error: workflow not found at ${workflowPath}`);
185
190
  process.exit(1);
186
191
  }
187
- const { source, manifest, description, networkPolicy, placeholders, snapshots, memory, postRunHooks, inputSchema, outputSchema, workflowPlan } = await bundleWorkflow(workflowPath);
192
+ const { source, manifest, description, networkPolicy, placeholders, snapshots, resources, inputSchema, outputSchema, workflowPlan, connectors, connectorOperation, invokePolicy } = await bundleWorkflow(workflowPath);
193
+ if (connectors)
194
+ console.log(`[register] Connectors required: ${Object.keys(connectors).join(", ")} — tokens are injected at the network layer at dispatch`);
188
195
  if (networkPolicy)
189
196
  console.log(`[register] Network policy detected — credentials will be brokered via Vercel firewall`);
190
197
  if (snapshots?.bootFrom)
191
198
  console.log(`[register] Boots from: ${formatBootFrom(snapshots.bootFrom)}`);
192
199
  if (snapshots?.saveLatest)
193
200
  console.log(`[register] Captures snapshot per step${snapshots.retainSteps ? " (retain: every step)" : ""}`);
201
+ if (resources?.size)
202
+ console.log(`[register] Sandbox size: ${resources.size}`);
194
203
  const client = makeClient(opts);
195
204
  console.log(`[register] Registering "${name}" → factory "${opts.factory}"…`);
196
205
  const result = await client.register({
@@ -205,18 +214,15 @@ var registerCommand = new Command("register").description("Register a workflow w
205
214
  ...networkPolicy ? { networkPolicy } : {},
206
215
  ...placeholders ? { placeholders } : {},
207
216
  ...snapshots !== undefined ? { snapshots } : {},
208
- ...memory !== undefined ? { memory } : {},
209
- ...postRunHooks !== undefined ? { postRunHooks } : {},
217
+ ...resources !== undefined ? { resources } : {},
210
218
  ...inputSchema !== undefined ? { inputSchema } : {},
211
- ...outputSchema !== undefined ? { outputSchema } : {}
219
+ ...outputSchema !== undefined ? { outputSchema } : {},
220
+ ...connectors !== undefined ? { connectors } : {},
221
+ ...connectorOperation !== undefined ? { connectorOperation } : {},
222
+ ...invokePolicy !== undefined ? { invokePolicy } : {}
212
223
  });
213
224
  const scheduleNote = opts.schedule ? ` — schedule: ${opts.schedule}` : "";
214
225
  console.log(`✓ Workflow: ${result.name}@${result.version} (${result.id})${scheduleNote}`);
215
- if (result.warnings && result.warnings.length > 0) {
216
- for (const w of result.warnings) {
217
- console.warn(`! warning: ${w}`);
218
- }
219
- }
220
226
  if (!opts.build)
221
227
  return;
222
228
  console.log(`[build] Invoking "${name}" with snapshots:{ saveLatest: true } to capture a snapshot…`);
@@ -242,7 +248,7 @@ var registerCommand = new Command("register").description("Register a workflow w
242
248
  process.exit(1);
243
249
  });
244
250
  function formatBootFrom(b) {
245
- return `snapshot ${b.snapshotId}`;
251
+ return b === "reuse" ? "reuse (this workflow's own latest snapshot)" : `snapshot ${b.snapshotId}`;
246
252
  }
247
253
 
248
254
  // src/commands/invoke.ts
@@ -519,7 +525,8 @@ var logsCommand = new Command2("logs").description("Stream logs for a run").argu
519
525
  });
520
526
 
521
527
  // src/commands/invoke.ts
522
- var invokeCommand = new Command3("invoke").description("Invoke a registered workflow template").argument("<template>", "Template name").option("-i, --input <json>", "Input as JSON string").option("-f, --follow", "Stream logs after invoking").option("--factory <slug>", `Factory the template lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (template, opts) => {
528
+ var SANDBOX_SIZES = ["2vcpu-4gb", "4vcpu-8gb", "8vcpu-16gb", "32vcpu-64gb"];
529
+ var invokeCommand = new Command3("invoke").description("Invoke a registered workflow template").argument("<template>", "Template name").option("-i, --input <json>", "Input as JSON string").option("-f, --follow", "Stream logs after invoking").option("--factory <slug>", `Factory the template lives in (default: ${defaultFactory})`, defaultFactory).option("--size <size>", `Sandbox machine size for this run: ${SANDBOX_SIZES.join(" | ")} (overrides the template default)`).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (template, opts) => {
523
530
  let input;
524
531
  if (opts.input) {
525
532
  try {
@@ -529,8 +536,15 @@ var invokeCommand = new Command3("invoke").description("Invoke a registered work
529
536
  process.exit(1);
530
537
  }
531
538
  }
539
+ if (opts.size && !SANDBOX_SIZES.includes(opts.size)) {
540
+ console.error(`Error: --size must be one of: ${SANDBOX_SIZES.join(", ")}`);
541
+ process.exit(1);
542
+ }
532
543
  const client = makeClient(opts);
533
- const { id } = await client.invoke(template, input, { factorySlug: opts.factory });
544
+ const { id } = await client.invoke(template, input, {
545
+ factorySlug: opts.factory,
546
+ ...opts.size ? { size: opts.size } : {}
547
+ });
534
548
  console.log(`Run started: ${id}`);
535
549
  if (opts.follow)
536
550
  await streamLogs(id, opts);
@@ -550,8 +564,108 @@ var listCommand = new Command4("list").description("List registered workflow tem
550
564
  console.log(`${t.name} ${t.version}`);
551
565
  });
552
566
 
567
+ // src/commands/pause.ts
568
+ import { Command as Command5, InvalidArgumentError as InvalidArgumentError2 } from "commander";
569
+ import pc2 from "picocolors";
570
+ function readPauseEnv() {
571
+ const runId = process.env.RUN_ID;
572
+ const url = process.env.AGENT_COMPOSE_URL;
573
+ const apiKey = process.env.AGENT_COMPOSE_API_KEY;
574
+ const agentId = process.env.AGENT_COMPOSE_AGENT_ID || undefined;
575
+ const missing = [
576
+ !runId && "RUN_ID",
577
+ !url && "AGENT_COMPOSE_URL",
578
+ !apiKey && "AGENT_COMPOSE_API_KEY"
579
+ ].filter(Boolean);
580
+ if (missing.length) {
581
+ console.error(`Error: agentc pause only works inside a run sandbox — missing ${missing.join(", ")}.`);
582
+ process.exit(1);
583
+ }
584
+ return { runId, url, apiKey, agentId };
585
+ }
586
+ function parseMs(raw) {
587
+ const n = Number(raw);
588
+ if (!Number.isFinite(n) || n <= 0) {
589
+ throw new InvalidArgumentError2(`expected a positive number of milliseconds, got "${raw}".`);
590
+ }
591
+ return n;
592
+ }
593
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
594
+ var pauseCommand = new Command5("pause").description("Ask the human a question and block until they answer (run-sandbox only)").requiredOption("--reason <text>", "The question / ask to show the human").option("--option <value>", "A typed choice (repeatable; omit for a free-form answer)", (v, acc) => [...acc, v], []).option("--ttl <ms>", "Auto-expire the pause after this many milliseconds", parseMs).option("--poll-interval <ms>", "How often to poll for an answer", parseMs, 3000).action(async (opts) => {
595
+ const env = readPauseEnv();
596
+ const base = env.url.replace(/\/+$/, "");
597
+ const createBody = { reason: opts.reason };
598
+ if (opts.option.length)
599
+ createBody.options = opts.option;
600
+ if (env.agentId)
601
+ createBody.agentId = env.agentId;
602
+ if (opts.ttl !== undefined)
603
+ createBody.ttlMs = opts.ttl;
604
+ let createRes;
605
+ try {
606
+ createRes = await fetch(`${base}/api/v1/runs/${env.runId}/pauses`, {
607
+ method: "POST",
608
+ headers: {
609
+ Authorization: `Bearer ${env.apiKey}`,
610
+ "Content-Type": "application/json"
611
+ },
612
+ body: JSON.stringify(createBody)
613
+ });
614
+ } catch (err) {
615
+ console.error(`Error: failed to create pause: ${err instanceof Error ? err.message : String(err)}`);
616
+ process.exit(1);
617
+ }
618
+ if (!createRes.ok) {
619
+ const text = await createRes.text().catch(() => "");
620
+ console.error(`Error: create pause returned ${createRes.status}${text ? `: ${text}` : ""}`);
621
+ process.exit(1);
622
+ }
623
+ const { id } = await createRes.json();
624
+ console.error(`${pc2.yellow("⏸")} paused for human input (pause ${id}) — waiting…`);
625
+ for (;; ) {
626
+ await sleep(opts.pollInterval);
627
+ let pause;
628
+ let terminalError;
629
+ try {
630
+ const res = await fetch(`${base}/api/v1/runs/${env.runId}/pauses/${id}`, {
631
+ headers: { Authorization: `Bearer ${env.apiKey}` }
632
+ });
633
+ if (res.ok) {
634
+ pause = await res.json();
635
+ } else {
636
+ const text = await res.text().catch(() => "");
637
+ const retryable = res.status >= 500 || res.status === 408 || res.status === 429;
638
+ if (retryable) {
639
+ console.error(`${pc2.dim(`poll ${id} returned ${res.status}${text ? `: ${text}` : ""} — retrying…`)}`);
640
+ } else {
641
+ terminalError = `poll ${id} returned ${res.status}${text ? `: ${text}` : ""} — giving up.`;
642
+ }
643
+ }
644
+ } catch (err) {
645
+ console.error(`${pc2.dim(`poll ${id} failed (${err instanceof Error ? err.message : String(err)}) — retrying…`)}`);
646
+ }
647
+ if (terminalError) {
648
+ console.error(`Error: ${terminalError}`);
649
+ process.exit(1);
650
+ }
651
+ if (!pause)
652
+ continue;
653
+ if (pause.status === "pending")
654
+ continue;
655
+ if (pause.status === "resolved") {
656
+ const payload = pause.resumePayload;
657
+ const decision = payload && typeof payload.decision === "string" ? payload.decision : undefined;
658
+ process.stdout.write(`${decision ?? JSON.stringify(payload)}
659
+ `);
660
+ process.exit(0);
661
+ }
662
+ console.error(`Error: pause ${id} ${pause.status} before it was answered.`);
663
+ process.exit(1);
664
+ }
665
+ });
666
+
553
667
  // src/commands/init.ts
554
- import { Command as Command5 } from "commander";
668
+ import { Command as Command6 } from "commander";
555
669
  import {
556
670
  existsSync as existsSync3,
557
671
  mkdirSync,
@@ -630,7 +744,7 @@ function loadSource(dir) {
630
744
  const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
631
745
  return { dir, files };
632
746
  }
633
- var initCommand = new Command5("init").description("Install agentc Claude Code skills and ensure PATH is configured").option("--dev", "Also install dev-only skills from cli/skills-dev/ (clone install only)").action((opts) => {
747
+ var initCommand = new Command6("init").description("Install agentc Claude Code skills and ensure PATH is configured").option("--dev", "Also install dev-only skills from cli/skills-dev/ (clone install only)").action((opts) => {
634
748
  ensureBunInPath();
635
749
  const publicSrc = loadSource(locateSkillsDir("skills"));
636
750
  if (!publicSrc) {
@@ -683,7 +797,7 @@ ${wanted.length} skill(s) installed to ${targetRoot}${note}`);
683
797
  });
684
798
 
685
799
  // src/commands/keys.ts
686
- import { Command as Command6 } from "commander";
800
+ import { Command as Command7 } from "commander";
687
801
  import { AgentComposeClient as AgentComposeClient2 } from "@agent-compose/sdk";
688
802
  var DURATION_UNITS = {
689
803
  s: 1000,
@@ -709,7 +823,7 @@ function requireAdminKey(adminKey) {
709
823
  ` + "Mint your first admin key by signing into the dashboard.");
710
824
  process.exit(1);
711
825
  }
712
- var keysCommand = new Command6("keys").description("Manage API keys");
826
+ var keysCommand = new Command7("keys").description("Manage API keys");
713
827
  keysCommand.command("create <name>").description("Create a new API key (requires admin-scoped ac_… key)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--admin-key <key>", "Admin-scoped ac_… key (or AGENT_COMPOSE_ADMIN_KEY env var)", process.env.AGENT_COMPOSE_ADMIN_KEY ?? "").option("--scopes <list>", "Comma-separated scope list: read, invoke, admin, events:read, events:write. Default (empty) = full team access.").option("--expires-in <duration>", `Expire the key after this duration — e.g. "30d", "2h", "7d12h", "1w", "90m". Default: never.`).option("--factory <slug>", "Restrict the key to a single factory (slug). Default: team-wide.").action(async (name, opts) => {
714
828
  requireAdminKey(opts.adminKey);
715
829
  const scopes = opts.scopes ? opts.scopes.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
@@ -763,8 +877,8 @@ keysCommand.command("list").description("List API keys for your team (requires a
763
877
  });
764
878
 
765
879
  // src/commands/auth.ts
766
- import { Command as Command7 } from "commander";
767
- var authCommand = new Command7("auth").description("Manage authentication");
880
+ import { Command as Command8 } from "commander";
881
+ var authCommand = new Command8("auth").description("Manage authentication");
768
882
  authCommand.command("login <key>").description("Save an API key for the active environment").action(async (key) => {
769
883
  if (projectDir && activeEnv) {
770
884
  if (!projectSettings.envs?.[activeEnv]) {
@@ -845,9 +959,9 @@ Run: agentc auth login YOUR_KEY`);
845
959
  });
846
960
 
847
961
  // src/commands/secrets.ts
848
- import { Command as Command8 } from "commander";
962
+ import { Command as Command9 } from "commander";
849
963
  var sharedOpts = (cmd) => cmd.option("--factory <slug>", `Factory the workflow lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
850
- var secretsCommand = new Command8("secrets").description("Manage workflow secrets stored in GCP Secret Manager");
964
+ var secretsCommand = new Command9("secrets").description("Manage workflow secrets stored in GCP Secret Manager");
851
965
  sharedOpts(secretsCommand.command("set <workflow> <key> <value>").description("Create or update a secret for a workflow")).action(async (workflow, key, value, opts) => {
852
966
  const client = makeClient(opts);
853
967
  await client.setSecret(workflow, key, value, { factorySlug: opts.factory });
@@ -872,7 +986,7 @@ sharedOpts(secretsCommand.command("delete <workflow> <key>").description("Delete
872
986
  });
873
987
 
874
988
  // src/commands/usage.ts
875
- import { Command as Command9 } from "commander";
989
+ import { Command as Command10 } from "commander";
876
990
  function parseMonth(s) {
877
991
  const m = /^(\d{4})-(\d{1,2})$/.exec(s);
878
992
  if (!m)
@@ -891,7 +1005,7 @@ function fmtSeconds(s) {
891
1005
  const h = Math.floor(s / 3600);
892
1006
  return `${h}h ${Math.floor(s % 3600 / 60)}m`;
893
1007
  }
894
- var usageCommand = new Command9("usage").description("Show billable usage — runs + sandbox seconds — for the current team").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--month <yyyy-mm>", "Calendar month to report (default: current month, UTC)").option("--from <iso>", "Window start (ISO 8601) — overrides --month").option("--to <iso>", "Window end (ISO 8601) — overrides --month").action(async (opts) => {
1008
+ var usageCommand = new Command10("usage").description("Show billable usage — runs + sandbox seconds — for the current team").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--month <yyyy-mm>", "Calendar month to report (default: current month, UTC)").option("--from <iso>", "Window start (ISO 8601) — overrides --month").option("--to <iso>", "Window end (ISO 8601) — overrides --month").action(async (opts) => {
895
1009
  let from, to;
896
1010
  if (opts.from || opts.to) {
897
1011
  if (!opts.from || !opts.to) {
@@ -946,8 +1060,8 @@ Usage ${from.toISOString().slice(0, 10)} → ${to.toISOString().slice(0, 10)}
946
1060
  });
947
1061
 
948
1062
  // src/commands/snapshot.ts
949
- import { Command as Command10 } from "commander";
950
- var snapshotCommand = new Command10("snapshot").description("Manage captured sandbox snapshots");
1063
+ import { Command as Command11 } from "commander";
1064
+ var snapshotCommand = new Command11("snapshot").description("Manage captured sandbox snapshots");
951
1065
  snapshotCommand.command("list").description("List captured sandbox snapshots in a factory").option("-w, --workflow <name>", "Filter to a single workflow").option("--limit <n>", "Max rows to show (default 50, max 500)", (v) => parseInt(v, 10)).option("--before <cursor>", "Pagination cursor from a previous page").option("--factory <slug>", "Factory slug", defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
952
1066
  const page = await makeClient(opts).listSnapshotsPage({
953
1067
  factorySlug: opts.factory,
@@ -994,9 +1108,9 @@ snapshotCommand.command("show").description("List every snapshot held for a run
994
1108
  });
995
1109
 
996
1110
  // src/commands/factory.ts
997
- import { Command as Command11 } from "commander";
1111
+ import { Command as Command12 } from "commander";
998
1112
  import { AgentComposeError as AgentComposeError3 } from "@agent-compose/sdk";
999
- var factoryCommand = new Command11("factory").description("Manage factories (projects within your team)");
1113
+ var factoryCommand = new Command12("factory").description("Manage factories (projects within your team)");
1000
1114
  var SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
1001
1115
  var SLUG_MAX_LEN = 64;
1002
1116
  function validateSlugOrExit(slug) {
@@ -1084,25 +1198,79 @@ factoryCommand.command("show").description("Show the active factory + how it's r
1084
1198
  console.log(`Active factory: ${defaultFactory} (default fallback)`);
1085
1199
  });
1086
1200
 
1201
+ // src/commands/members.ts
1202
+ import { Command as Command13 } from "commander";
1203
+ var membersCommand = new Command13("members").description("List teammates and flag them for attention (@-mentions)");
1204
+ membersCommand.command("list").description("List the human members of your team (name, email, role, user id)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
1205
+ const client = makeClient(opts);
1206
+ const members = await client.listMembers().catch(reportSdkError);
1207
+ if (members.length === 0) {
1208
+ console.log("No members found.");
1209
+ return;
1210
+ }
1211
+ const nameW = Math.max(4, ...members.map((m) => (m.name || "").length));
1212
+ const emailW = Math.max(5, ...members.map((m) => m.email.length));
1213
+ const roleW = Math.max(4, ...members.map((m) => m.role.length));
1214
+ console.log(`${"NAME".padEnd(nameW)} ${"EMAIL".padEnd(emailW)} ${"ROLE".padEnd(roleW)} USER ID`);
1215
+ for (const m of members) {
1216
+ console.log(`${(m.name || "—").padEnd(nameW)} ${m.email.padEnd(emailW)} ${m.role.padEnd(roleW)} ${m.userId}`);
1217
+ }
1218
+ });
1219
+ var collect = (val, acc) => [...acc ?? [], val];
1220
+ membersCommand.command("notify").description("Flag teammates for attention — a ping in their Workbench").requiredOption("-u, --user <id-or-email>", "Teammate to flag (repeatable)", collect).requiredOption("-m, --text <message>", "What to flag them about").option("-k, --context-kind <kind>", "doc | comment | plan | run", "plan").option("--context-path <path>", "Factory-relative file path or thread id").option("--context-url <url>", "Dashboard URL the Workbench card links to").option("--factory <slug>", `Factory the ping belongs to (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
1221
+ const kind = opts.contextKind;
1222
+ if (!["doc", "comment", "plan", "run"].includes(kind)) {
1223
+ console.error(`Error: --context-kind must be one of doc | comment | plan | run (got "${kind}")`);
1224
+ process.exit(1);
1225
+ }
1226
+ const client = makeClient(opts);
1227
+ const members = await client.listMembers().catch(reportSdkError);
1228
+ const byEmail = new Map(members.map((m) => [m.email.toLowerCase(), m.userId]));
1229
+ const ids = new Set(members.map((m) => m.userId));
1230
+ const resolved = [];
1231
+ const unknown = [];
1232
+ for (const u of opts.user) {
1233
+ if (ids.has(u))
1234
+ resolved.push(u);
1235
+ else if (byEmail.has(u.toLowerCase()))
1236
+ resolved.push(byEmail.get(u.toLowerCase()));
1237
+ else
1238
+ unknown.push(u);
1239
+ }
1240
+ if (unknown.length > 0) {
1241
+ console.error(`Error: not on this team: ${unknown.join(", ")}. Run \`agentc members list\`.`);
1242
+ process.exit(1);
1243
+ }
1244
+ const mentions = await client.createMentions({
1245
+ mentionedUserIds: [...new Set(resolved)],
1246
+ text: opts.text,
1247
+ contextKind: kind,
1248
+ ...opts.contextPath ? { contextPath: opts.contextPath } : {},
1249
+ ...opts.contextUrl ? { contextUrl: opts.contextUrl } : {},
1250
+ factorySlug: opts.factory
1251
+ }).catch(reportSdkError);
1252
+ console.log(`✓ Flagged ${mentions.length} teammate${mentions.length === 1 ? "" : "s"} in factory "${opts.factory}".`);
1253
+ });
1254
+
1087
1255
  // src/commands/cancel.ts
1088
- import { Command as Command12 } from "commander";
1089
- import pc2 from "picocolors";
1090
- var cancelCommand = new Command12("cancel").description("Cancel an in-progress run").argument("<run-id>", "Run ID").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--wait", "Stream the run's events until it terminates").action(async (runId, opts) => {
1256
+ import { Command as Command14 } from "commander";
1257
+ import pc3 from "picocolors";
1258
+ var cancelCommand = new Command14("cancel").description("Cancel an in-progress run").argument("<run-id>", "Run ID").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--wait", "Stream the run's events until it terminates").action(async (runId, opts) => {
1091
1259
  const client = makeClient(opts);
1092
1260
  const res = await client.cancelRun(runId).catch(reportSdkError);
1093
1261
  if (res.status === "canceled") {
1094
- console.log(`${pc2.yellow("⊘")} canceled ${runId} at ${new Date(res.canceledAt).toISOString()}`);
1262
+ console.log(`${pc3.yellow("⊘")} canceled ${runId} at ${new Date(res.canceledAt).toISOString()}`);
1095
1263
  } else {
1096
- console.log(`${pc2.dim("⊘")} run ${runId} already ${res.status} at ${new Date(res.canceledAt).toISOString()}`);
1264
+ console.log(`${pc3.dim("⊘")} run ${runId} already ${res.status} at ${new Date(res.canceledAt).toISOString()}`);
1097
1265
  }
1098
1266
  if (opts.wait)
1099
1267
  await streamLogs(runId, opts);
1100
1268
  });
1101
1269
 
1102
1270
  // src/commands/events.ts
1103
- import { Command as Command13 } from "commander";
1271
+ import { Command as Command15 } from "commander";
1104
1272
  import { readFileSync as readFileSync3 } from "node:fs";
1105
- import pc3 from "picocolors";
1273
+ import pc4 from "picocolors";
1106
1274
  function parseBody(raw) {
1107
1275
  if (raw === undefined || raw === "")
1108
1276
  return {};
@@ -1144,7 +1312,7 @@ function parseLimit(raw) {
1144
1312
  }
1145
1313
  function fmtBodyPreview(body) {
1146
1314
  if (body === null || body === undefined)
1147
- return pc3.dim("—");
1315
+ return pc4.dim("—");
1148
1316
  if (typeof body === "string")
1149
1317
  return body.length > 60 ? body.slice(0, 57) + "…" : body;
1150
1318
  try {
@@ -1154,7 +1322,7 @@ function fmtBodyPreview(body) {
1154
1322
  return String(body);
1155
1323
  }
1156
1324
  }
1157
- var sendCommand = new Command13("send").description("Send an event to an existing run (requires events:write or invoke scope)").argument("<run-id>", "Target run id (uuid)").argument("<name>", "Event name (e.g. quality.accepted, deploy.completed)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--body <json>", "Event body — inline JSON or @file (default: {})").option("--summary <s>", "One-line summary (≤2000 chars)").option("--confidence <0..1>", "Confidence score, [0, 1]", parseConfidence).option("-a, --attribute <key=value>", "Attribute (repeatable; value parsed as JSON if possible)", parseAttribute, {}).option("--idempotency-key <k>", "Replay-safe key: identical (run, name, key) returns the original event").option("--no-propagate", "Mark event as non-propagating").option("--timestamp <iso>", "Event timestamp (ISO 8601, defaults to server-now)").option("--json", "Output the raw event JSON instead of the one-line summary").action(async (runId, name, opts) => {
1325
+ var sendCommand = new Command15("send").description("Send an event to an existing run (requires events:write or invoke scope)").argument("<run-id>", "Target run id (uuid)").argument("<name>", "Event name (e.g. quality.accepted, deploy.completed)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--body <json>", "Event body — inline JSON or @file (default: {})").option("--summary <s>", "One-line summary (≤2000 chars)").option("--confidence <0..1>", "Confidence score, [0, 1]", parseConfidence).option("-a, --attribute <key=value>", "Attribute (repeatable; value parsed as JSON if possible)", parseAttribute, {}).option("--idempotency-key <k>", "Replay-safe key: identical (run, name, key) returns the original event").option("--no-propagate", "Mark event as non-propagating").option("--timestamp <iso>", "Event timestamp (ISO 8601, defaults to server-now)").option("--json", "Output the raw event JSON instead of the one-line summary").action(async (runId, name, opts) => {
1158
1326
  let body;
1159
1327
  try {
1160
1328
  body = parseBody(opts.body);
@@ -1177,9 +1345,9 @@ var sendCommand = new Command13("send").description("Send an event to an existin
1177
1345
  console.log(JSON.stringify(event, null, 2));
1178
1346
  return;
1179
1347
  }
1180
- console.log(`${pc3.green("✓")} event ${pc3.bold(event.id.slice(0, 8))} ${event.name} ${pc3.dim(`run ${runId.slice(0, 8)}`)}`);
1348
+ console.log(`${pc4.green("✓")} event ${pc4.bold(event.id.slice(0, 8))} ${event.name} ${pc4.dim(`run ${runId.slice(0, 8)}`)}`);
1181
1349
  });
1182
- var listCommand2 = new Command13("list").description("List events for a run, or factory-wide with --factory <slug>").argument("[run-id]", "Run id (uuid). If omitted, requires --factory.").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--factory <slug>", "List events across a factory instead of a single run").option("--limit <n>", "Max rows (factory mode only; server clamps)", parseLimit).option("--name <pattern>", "Filter by event name (factory mode only)").option("--json", "Output raw JSON instead of the columnar view").action(async (runId, opts) => {
1350
+ var listCommand2 = new Command15("list").description("List events for a run, or factory-wide with --factory <slug>").argument("[run-id]", "Run id (uuid). If omitted, requires --factory.").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--factory <slug>", "List events across a factory instead of a single run").option("--limit <n>", "Max rows (factory mode only; server clamps)", parseLimit).option("--name <pattern>", "Filter by event name (factory mode only)").option("--json", "Output raw JSON instead of the columnar view").action(async (runId, opts) => {
1183
1351
  if (!runId && !opts.factory) {
1184
1352
  console.error("Error: provide either <run-id> or --factory <slug>");
1185
1353
  process.exit(1);
@@ -1207,17 +1375,44 @@ var listCommand2 = new Command13("list").description("List events for a run, or
1207
1375
  for (const ev of sorted) {
1208
1376
  const ts = ev.timestamp.replace(/\.\d+Z$/, "Z");
1209
1377
  const detail = ev.summary ?? fmtBodyPreview(ev.body);
1210
- const runHint = runId ? "" : pc3.dim(` run ${(ev.runId ?? "—").slice(0, 8)}`);
1211
- console.log(`${pc3.dim(ts)} ${pc3.bold(ev.name.padEnd(28))} ${detail}${runHint}`);
1378
+ const runHint = runId ? "" : pc4.dim(` run ${(ev.runId ?? "—").slice(0, 8)}`);
1379
+ console.log(`${pc4.dim(ts)} ${pc4.bold(ev.name.padEnd(28))} ${detail}${runHint}`);
1380
+ }
1381
+ });
1382
+ var eventsCommand = new Command15("events").description("Send or list run events (Events API)").addCommand(sendCommand).addCommand(listCommand2);
1383
+
1384
+ // src/commands/files.ts
1385
+ import { readFileSync as readFileSync4, writeFileSync } from "node:fs";
1386
+ import { Command as Command16 } from "commander";
1387
+ var sharedOpts2 = (cmd) => cmd.option("--factory <slug>", `Factory whose drive to target (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1388
+ var filesCommand = new Command16("files").description("Read/write documents on a factory's drive");
1389
+ sharedOpts2(filesCommand.command("put <local-file> <drive-path>").description("Upload a local file to the factory drive (creates or overwrites)").option("--content-type <mime>", "Content type", "text/plain; charset=utf-8")).action(async (localFile, drivePath, opts) => {
1390
+ const client = makeClient(opts);
1391
+ const result = await client.putFactoryFile(drivePath, readFileSync4(localFile), {
1392
+ factorySlug: opts.factory,
1393
+ contentType: opts.contentType
1394
+ });
1395
+ console.log(`✓ ${result.created ? "created" : "updated"} ${result.path} (${result.sizeBytes} bytes)`);
1396
+ });
1397
+ sharedOpts2(filesCommand.command("get <drive-path>").description("Print a drive file's content (or save with -o)").option("-o, --out <local-file>", "Write to a local file instead of stdout").option("--revision <n>", "Read a specific revision id")).action(async (drivePath, opts) => {
1398
+ const client = makeClient(opts);
1399
+ const content = await client.getFactoryFile(drivePath, {
1400
+ factorySlug: opts.factory,
1401
+ ...opts.revision !== undefined ? { revision: Number(opts.revision) } : {}
1402
+ });
1403
+ if (opts.out) {
1404
+ writeFileSync(opts.out, content);
1405
+ console.log(`✓ ${drivePath} → ${opts.out}`);
1406
+ } else {
1407
+ process.stdout.write(content);
1212
1408
  }
1213
1409
  });
1214
- var eventsCommand = new Command13("events").description("Send or list run events (Events API)").addCommand(sendCommand).addCommand(listCommand2);
1215
1410
 
1216
1411
  // src/commands/schedule.ts
1217
- import { Command as Command14 } from "commander";
1218
- var sharedOpts2 = (cmd) => cmd.option("--factory <slug>", `Factory the schedule lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1219
- var scheduleCommand = new Command14("schedule").description("Manage cron schedules attached to registered workflows");
1220
- sharedOpts2(scheduleCommand.command("create <name>").description("Create a new schedule for a workflow").requiredOption("--workflow <name>", "The registered workflow this schedule should fire").requiredOption("--cron <expr>", "Cron expression (UTC) — e.g. '0 9 * * *'")).action(async (name, opts) => {
1412
+ import { Command as Command17 } from "commander";
1413
+ var sharedOpts3 = (cmd) => cmd.option("--factory <slug>", `Factory the schedule lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1414
+ var scheduleCommand = new Command17("schedule").description("Manage cron schedules attached to registered workflows");
1415
+ sharedOpts3(scheduleCommand.command("create <name>").description("Create a new schedule for a workflow").requiredOption("--workflow <name>", "The registered workflow this schedule should fire").requiredOption("--cron <expr>", "Cron expression (UTC) — e.g. '0 9 * * *'")).action(async (name, opts) => {
1221
1416
  const client = makeClient(opts);
1222
1417
  const created = await client.createSchedule({
1223
1418
  name,
@@ -1227,7 +1422,7 @@ sharedOpts2(scheduleCommand.command("create <name>").description("Create a new s
1227
1422
  });
1228
1423
  console.log(`✓ Schedule "${created.name}" → workflow "${created.workflow}" @ "${created.cron}" (${created.id})`);
1229
1424
  });
1230
- sharedOpts2(scheduleCommand.command("list").description("List schedules in a factory")).action(async (opts) => {
1425
+ sharedOpts3(scheduleCommand.command("list").description("List schedules in a factory")).action(async (opts) => {
1231
1426
  const client = makeClient(opts);
1232
1427
  const rows = await client.listSchedules(opts.factory);
1233
1428
  if (rows.length === 0) {
@@ -1240,21 +1435,21 @@ sharedOpts2(scheduleCommand.command("list").description("List schedules in a fac
1240
1435
  console.log(` ${s.name.padEnd(24)} ${s.workflowName.padEnd(20)} ${s.cron.padEnd(16)} next: ${next} ${s.id}`);
1241
1436
  }
1242
1437
  });
1243
- sharedOpts2(scheduleCommand.command("delete <id>").description("Delete a schedule by id")).action(async (id, opts) => {
1438
+ sharedOpts3(scheduleCommand.command("delete <id>").description("Delete a schedule by id")).action(async (id, opts) => {
1244
1439
  const client = makeClient(opts);
1245
1440
  await client.deleteSchedule(id, opts.factory);
1246
1441
  console.log(`✓ Schedule ${id} deleted from factory "${opts.factory}"`);
1247
1442
  });
1248
1443
 
1249
1444
  // src/commands/upgrade.ts
1250
- import { Command as Command15 } from "commander";
1445
+ import { Command as Command18 } from "commander";
1251
1446
  import { spawnSync } from "node:child_process";
1252
- import { readFileSync as readFileSync4 } from "node:fs";
1447
+ import { readFileSync as readFileSync5 } from "node:fs";
1253
1448
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1254
- import pc4 from "picocolors";
1449
+ import pc5 from "picocolors";
1255
1450
  var PACKAGE_NAME = "@agent-compose/cli";
1256
- var upgradeCommand = new Command15("upgrade").description("Update the agentc CLI to the latest published version").option("--check", "Check for updates without installing", false).action(async (opts) => {
1257
- const pkg = JSON.parse(readFileSync4(fileURLToPath2(new URL("../../package.json", import.meta.url)), "utf8"));
1451
+ var upgradeCommand = new Command18("upgrade").description("Update the agentc CLI to the latest published version").option("--check", "Check for updates without installing", false).action(async (opts) => {
1452
+ const pkg = JSON.parse(readFileSync5(fileURLToPath2(new URL("../../package.json", import.meta.url)), "utf8"));
1258
1453
  const current = pkg.version;
1259
1454
  const controller = new AbortController;
1260
1455
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -1273,34 +1468,34 @@ var upgradeCommand = new Command15("upgrade").description("Update the agentc CLI
1273
1468
  clearTimeout(timeout);
1274
1469
  }
1275
1470
  if (!latest) {
1276
- console.error(`${pc4.red("✗")} Couldn't reach the npm registry. Check your connection and try again.`);
1471
+ console.error(`${pc5.red("✗")} Couldn't reach the npm registry. Check your connection and try again.`);
1277
1472
  process.exit(1);
1278
1473
  }
1279
1474
  if (latest === current) {
1280
- console.log(`${pc4.green("✓")} You're on the latest version (${pc4.bold(current)}).`);
1475
+ console.log(`${pc5.green("✓")} You're on the latest version (${pc5.bold(current)}).`);
1281
1476
  return;
1282
1477
  }
1283
- console.log(`${pc4.yellow("●")} agentc ${pc4.bold(latest)} is available (you have ${current}).`);
1478
+ console.log(`${pc5.yellow("●")} agentc ${pc5.bold(latest)} is available (you have ${current}).`);
1284
1479
  if (opts.check) {
1285
- console.log(` Run ${pc4.cyan("agentc upgrade")} to install, or ${pc4.cyan("npm install -g " + PACKAGE_NAME + "@latest")} manually.`);
1480
+ console.log(` Run ${pc5.cyan("agentc upgrade")} to install, or ${pc5.cyan("npm install -g " + PACKAGE_NAME + "@latest")} manually.`);
1286
1481
  return;
1287
1482
  }
1288
- console.log(` Running ${pc4.cyan("npm install -g " + PACKAGE_NAME + "@latest")} …`);
1483
+ console.log(` Running ${pc5.cyan("npm install -g " + PACKAGE_NAME + "@latest")} …`);
1289
1484
  const result = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], { stdio: "inherit" });
1290
1485
  if (result.status !== 0) {
1291
- console.error(`${pc4.red("✗")} Install failed. Run the command above manually, or check for permission issues (npm prefix / sudo).`);
1486
+ console.error(`${pc5.red("✗")} Install failed. Run the command above manually, or check for permission issues (npm prefix / sudo).`);
1292
1487
  process.exit(1);
1293
1488
  }
1294
- console.log(`${pc4.green("✓")} Installed ${pc4.bold(`agentc ${latest}`)}.`);
1489
+ console.log(`${pc5.green("✓")} Installed ${pc5.bold(`agentc ${latest}`)}.`);
1295
1490
  });
1296
1491
 
1297
1492
  // src/update-check.ts
1298
1493
  import { spawnSync as spawnSync2 } from "node:child_process";
1299
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync } from "node:fs";
1494
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1300
1495
  import { homedir as homedir3 } from "node:os";
1301
1496
  import { join as join3 } from "node:path";
1302
1497
  import { createInterface } from "node:readline/promises";
1303
- import pc5 from "picocolors";
1498
+ import pc6 from "picocolors";
1304
1499
  var PACKAGE_NAME2 = "@agent-compose/cli";
1305
1500
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
1306
1501
  var FETCH_TIMEOUT_MS = 2000;
@@ -1336,7 +1531,7 @@ function isNewer(latest, current) {
1336
1531
  }
1337
1532
  function readCache() {
1338
1533
  try {
1339
- const raw = readFileSync5(cachePath(), "utf8");
1534
+ const raw = readFileSync6(cachePath(), "utf8");
1340
1535
  const parsed = JSON.parse(raw);
1341
1536
  if (typeof parsed.checkedAt !== "number")
1342
1537
  return null;
@@ -1352,7 +1547,7 @@ function writeCache(entry) {
1352
1547
  try {
1353
1548
  const path = cachePath();
1354
1549
  mkdirSync2(join3(path, ".."), { recursive: true });
1355
- writeFileSync(path, JSON.stringify(entry, null, 2) + `
1550
+ writeFileSync2(path, JSON.stringify(entry, null, 2) + `
1356
1551
  `, "utf8");
1357
1552
  } catch {}
1358
1553
  }
@@ -1398,34 +1593,34 @@ async function checkForUpdate(currentVersion) {
1398
1593
  const latest = await resolveLatest();
1399
1594
  if (!latest || !isNewer(latest, currentVersion))
1400
1595
  return;
1401
- const headline = `${pc5.yellow("●")} agentc ${pc5.bold(latest)} is available (you have ${currentVersion}).`;
1596
+ const headline = `${pc6.yellow("●")} agentc ${pc6.bold(latest)} is available (you have ${currentVersion}).`;
1402
1597
  process.stderr.write(`${headline}
1403
1598
  `);
1404
1599
  if (!process.stdin.isTTY) {
1405
- process.stderr.write(` Run ${pc5.cyan("npm install -g " + PACKAGE_NAME2 + "@latest")} to update.
1600
+ process.stderr.write(` Run ${pc6.cyan("npm install -g " + PACKAGE_NAME2 + "@latest")} to update.
1406
1601
  `);
1407
1602
  return;
1408
1603
  }
1409
- const confirm = await promptYesNo(` Install now? [y/${pc5.bold("N")}] `);
1604
+ const confirm = await promptYesNo(` Install now? [y/${pc6.bold("N")}] `);
1410
1605
  if (!confirm)
1411
1606
  return;
1412
- process.stderr.write(` Running ${pc5.cyan("npm install -g " + PACKAGE_NAME2 + "@latest")} …
1607
+ process.stderr.write(` Running ${pc6.cyan("npm install -g " + PACKAGE_NAME2 + "@latest")} …
1413
1608
  `);
1414
1609
  const result = spawnSync2("npm", ["install", "-g", `${PACKAGE_NAME2}@latest`], {
1415
1610
  stdio: "inherit"
1416
1611
  });
1417
1612
  if (result.status !== 0) {
1418
- process.stderr.write(`${pc5.red("✗")} Install failed. Run the command above manually.
1613
+ process.stderr.write(`${pc6.red("✗")} Install failed. Run the command above manually.
1419
1614
  `);
1420
1615
  return;
1421
1616
  }
1422
- process.stderr.write(`${pc5.green("✓")} Installed ${pc5.bold(`agentc ${latest}`)}. ` + `Re-run your command to use the new version.
1617
+ process.stderr.write(`${pc6.green("✓")} Installed ${pc6.bold(`agentc ${latest}`)}. ` + `Re-run your command to use the new version.
1423
1618
  `);
1424
1619
  process.exit(0);
1425
1620
  }
1426
1621
 
1427
1622
  // src/index.ts
1428
- var pkg = JSON.parse(readFileSync6(fileURLToPath3(new URL("../package.json", import.meta.url)), "utf8"));
1623
+ var pkg = JSON.parse(readFileSync7(fileURLToPath3(new URL("../package.json", import.meta.url)), "utf8"));
1429
1624
  program.name("agentc").description("Coordinate agent production lines.").version(pkg.version);
1430
1625
  program.hook("preAction", async () => {
1431
1626
  try {
@@ -1436,10 +1631,13 @@ program.addCommand(registerCommand);
1436
1631
  program.addCommand(invokeCommand);
1437
1632
  program.addCommand(listCommand);
1438
1633
  program.addCommand(cancelCommand);
1634
+ program.addCommand(pauseCommand);
1439
1635
  program.addCommand(scheduleCommand);
1440
1636
  program.addCommand(logsCommand);
1441
1637
  program.addCommand(eventsCommand);
1638
+ program.addCommand(filesCommand);
1442
1639
  program.addCommand(factoryCommand);
1640
+ program.addCommand(membersCommand);
1443
1641
  program.addCommand(snapshotCommand);
1444
1642
  program.addCommand(secretsCommand);
1445
1643
  program.addCommand(keysCommand);
@@ -1452,7 +1650,8 @@ Topics:
1452
1650
  Workflows register, invoke, list, cancel
1453
1651
  Scheduling schedule
1454
1652
  Run inspection logs, events
1455
- Resources factory, snapshot, secrets
1653
+ Factory drive files
1654
+ Resources factory, members, snapshot, secrets
1456
1655
  Account keys, auth, usage
1457
1656
  Setup init, upgrade
1458
1657
 
@@ -1461,8 +1660,9 @@ Examples:
1461
1660
  $ agentc invoke pipeline --follow
1462
1661
  $ agentc schedule create nightly --workflow pipeline --cron '0 9 * * *'
1463
1662
  $ agentc logs <run-id>
1663
+ $ agentc files put report.md runs/abc123/report.md
1464
1664
 
1465
1665
  Docs:
1466
1666
  https://github.com/Layr-Labs/agent-compose
1467
1667
  `);
1468
- program.parse();
1668
+ await program.parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Command-line interface for agent-compose — register, invoke, and monitor workflows from your terminal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@agent-compose/sdk": "^0.5.2",
47
+ "@agent-compose/sdk": "^0.5.8",
48
48
  "commander": "^12.0.0",
49
49
  "picocolors": "^1.1.1"
50
50
  },
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: ac:files
3
+ description: Read and write documents on a factory's drive from the CLI.
4
+ allowed-tools: Bash(agentc *)
5
+ effort: low
6
+ ---
7
+
8
+ # Files
9
+
10
+ Put files onto a factory's shared drive and read them back — all through
11
+ `agentc`, no raw HTTP. The drive is where agent-written documents, briefs,
12
+ and run artifacts live; the dashboard's **Files** tab renders them, and
13
+ writes are indexed (revisions, search, attribution). Two subcommands:
14
+ `agentc files put` and `agentc files get`.
15
+
16
+ The primary consumer is an **agent working inside a run sandbox**: publishing
17
+ work to the dashboard while the run is going. Inside a sandbox the env already
18
+ carries `AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY`,
19
+ and the CLI attaches the run-callback token automatically so the write is
20
+ **attributed to the run** (it shows on the file and in the run's Artifacts
21
+ card). Works identically from a dev machine with saved credentials.
22
+
23
+ > Requires an API key with `invoke` (writes) / `read` (reads). See `/ac:setup`.
24
+
25
+ ## When to use
26
+
27
+ - **Publish run output** — an agent saves a report/brief so a human sees it
28
+ in the dashboard mid-run.
29
+ - **Share between runs** — write a file other runs of the same factory read.
30
+ - **Pull a file locally** — fetch a drive document to inspect or edit.
31
+
32
+ ## Upload a file
33
+
34
+ ```bash
35
+ # Create or overwrite a drive file from a local file.
36
+ # Convention: agents write under runs/<short-run-id>/...
37
+ agentc files put report.md runs/abc123/report.md
38
+
39
+ # Override the content type (default text/plain; charset=utf-8):
40
+ agentc files put index.html sites/demo/index.html --content-type "text/html; charset=utf-8"
41
+
42
+ # Target a non-default factory:
43
+ agentc files put notes.md briefs/notes.md --factory my-factory
44
+ ```
45
+
46
+ ## Download / read a file
47
+
48
+ ```bash
49
+ # Print a drive file to stdout:
50
+ agentc files get briefs/site-7b7954fc.md
51
+
52
+ # Save to a local file:
53
+ agentc files get runs/abc123/report.md -o ./report.md
54
+
55
+ # Read a specific historical revision:
56
+ agentc files get briefs/notes.md --revision 12
57
+ ```
58
+
59
+ ## Browsing the drive
60
+
61
+ There is no `agentc files list` — browse via the dashboard's **Files** tab,
62
+ or (inside a sandbox whose factory has a provisioned Archil disk)
63
+ `ls /factory` on the POSIX mount.
64
+
65
+ ## Authoring from inside a workflow
66
+
67
+ The CLI is for agents + ad-hoc use. Workflow code uses the SDK, which attaches
68
+ the run token the same way:
69
+
70
+ ```ts
71
+ import { AgentComposeClient } from "@agent-compose/sdk";
72
+ const client = new AgentComposeClient({ apiKey, baseUrl });
73
+ await client.putFactoryFile("runs/abc123/report.md", contents, {
74
+ contentType: "text/markdown; charset=utf-8",
75
+ });
76
+ ```
77
+
78
+ ## The drive vs. /factory mount
79
+
80
+ If the factory has a provisioned Archil disk, the drive is also POSIX-mounted
81
+ at `/factory` inside sandboxes — fine for reading and for scratch shared
82
+ between sibling runs. But a direct `/factory` write is **not indexed** (no
83
+ revision/attribution), so to make a file appear in the dashboard Files tab use
84
+ `agentc files put` (or the SDK), which goes through the indexed files API.
@@ -60,7 +60,7 @@ about "step-form vs run-form" — that's an internal call.
60
60
  - step-form vs run-form
61
61
  - sandbox environments / dependency installation
62
62
  - snapshots: `saveLatest`, `retainSteps`, `bootFrom`
63
- - `memory` / `postRunHooks` / `processors`
63
+ - `processors`
64
64
 
65
65
  Decide those internally based on what they described (see the
66
66
  "Internal decisions" section below).
@@ -69,28 +69,44 @@ Decide those internally based on what they described (see the
69
69
 
70
70
  After answering the questions above, decide the shape WITHOUT asking:
71
71
 
72
- ### Pick the workflow shape
73
-
74
- - **Step-form** (`defineWorkflow({...}).step(s1).step(s2).build()` with
75
- `defineStep(...)` objects) pick this when the body decomposes
76
- cleanly into named phases with typed handoffs. Each step's output
77
- threads into the next step's input. The dashboard renders each step
78
- as a typed phase with its own duration. Examples: ETL pipelines,
79
- classification enrichment publish, fetch score rank.
80
-
81
- - **Run-form** (one `async (ctx, sandbox)` body with `agent({...})`
82
- calls inside) pick this when the body is dominated by one or more
83
- agent loops. Decomposing an LLM iteration into typed engine steps is
84
- the wrong shape. Use `ctx.step("phase-name", () => …)` inside the
85
- body for observability sub-events when there's pre-agent setup
86
- worth tracing on the run timeline.
87
-
88
- When the user's description says "agent", "LLM", "the model decides",
89
- "reasons through", "writes a summary based on", "navigates the
90
- website" that's run-form. When they describe deterministic phases
91
- that each transform structured data — that's step-form. If genuinely
92
- mixed, default to run-form and use `ctx.step` for the deterministic
93
- phases.
72
+ ### Pick the workflow shape — DEFAULT TO STEP-FORM
73
+
74
+ **Default to step-form, even for agent-driven workflows.** Each meaningful
75
+ phaseincluding an agent pass becomes a `defineStep(...)`, and
76
+ `snapshots: { saveLatest: true }` makes the engine capture a snapshot AFTER
77
+ EACH step (latest-only). That durability is the whole point: if a later step's
78
+ sandbox dies, or a step needs retrying, the engine resumes from the last
79
+ completed step's snapshot instead of re-running the entire (often expensive,
80
+ multi-agent) pipeline from the top.
81
+
82
+ A multi-phase agent pipeline written **run-form is ONE engine step** a single
83
+ snapshot at the very end so ANY failure throws away all the work and restarts
84
+ at phase one. That's the brittleness step-form avoids.
85
+
86
+ - **Step-form** (`defineWorkflow({ id, input, output, snapshots: { saveLatest: true } }).step(s1).step(s2).build()`
87
+ with `defineStep(...)` objects) — the default. Each phase (fetch, an agent
88
+ pass, a transform, a deploy) is a step. **Steps DO receive a `sandbox`** and
89
+ run `agent({ sandbox: ctx.sandbox, })`, `ctx.sandbox.commands.run(…)`, etc.
90
+ they are NOT limited to pure data transforms. Each step's output threads
91
+ into the next step's input. The dashboard renders each as a typed,
92
+ separately-timed, separately-snapshotted phase. Examples: content imagery
93
+ polish → verify → deploy; fetch → score → rank → publish.
94
+
95
+ - **Run-form** (one `async (ctx, sandbox)` body) — LEGACY; being phased out in
96
+ favor of step-form everywhere. Only acceptable when the work is a SINGLE
97
+ indivisible agent loop with no meaningful phase boundaries. You give up
98
+ per-step snapshots (one capture at the very end). Use `ctx.step("phase", () => …)`
99
+ inside for timeline observability — it is NOT a snapshot boundary.
100
+ ⚠️ Replay semantics: a paused run-form workflow RESUMES BY RE-EXECUTING the
101
+ whole function from the top — only `ctx.requestDecision` results are
102
+ memoized. Every pre-pause side effect re-fires on resume, so writes must be
103
+ create-only/non-clobbering (an unconditional re-write destroyed a human's
104
+ pause-time edits in production testing). If the workflow pauses at all,
105
+ author it step-form: completed steps structurally never replay.
106
+
107
+ If the work has more than one phase — especially multiple agent passes, or any
108
+ step whose work you'd hate to lose on a failure — it's step-form. Each such
109
+ phase is its own step.
94
110
 
95
111
  ### Should it have a sandbox environment?
96
112
 
@@ -126,6 +142,21 @@ Most workflows DO NOT need this. Default to no sandbox env.
126
142
  IO panels; without them, fields show only their type.
127
143
  - `.describe("...")` on the root schema too where helpful — surfaces
128
144
  at the panel header.
145
+ - **Persist outputs to the factory drive — `$AGENT_COMPOSE_RUN_DIR`.**
146
+ Anything the workflow produces that should survive the run (the
147
+ deliverable, agent artifacts, reports) MUST be written under the run's
148
+ factory directory, exposed in the sandbox as `$AGENT_COMPOSE_RUN_DIR`
149
+ (`/factory/files/<workflow>/<version>/<run-id>/`). Files written only
150
+ to `/workspace` are **ephemeral** — they vanish when the sandbox is
151
+ reclaimed, never appear in the factory's files, and aren't available
152
+ as prior-run context to future runs. Read the dir from the sandbox
153
+ shell (it's injected there), not at module top-level (the workflow VM
154
+ has no `process.env`): e.g. in a step,
155
+ `const runDir = (await ctx.sandbox.commands.run('printf %s "${AGENT_COMPOSE_RUN_DIR:-/workspace}"')).stdout.trim()`,
156
+ then write the deliverable to `${runDir}/...` and tell any agents to do
157
+ the same. Use `/workspace` only for throwaway scratch. Sibling dirs
158
+ under `$AGENT_COMPOSE_FACTORY_DIR/<workflow>/` hold prior runs' outputs —
159
+ read them for continuity instead of re-deriving.
129
160
 
130
161
  ## Templates
131
162
 
@@ -196,6 +227,7 @@ export default defineWorkflow({
196
227
  description: "Pulls open PRs from a GitHub repo, scores each one by review urgency, surfaces the top five.",
197
228
  input: InputSchema,
198
229
  output: OutputSchema,
230
+ snapshots: { saveLatest: true }, // snapshot after each step → resume from the last one, not the top
199
231
  })
200
232
  .step(fetchStep)
201
233
  .step(scoreStep)
@@ -206,10 +238,18 @@ export default defineWorkflow({
206
238
  Notes:
207
239
  - Each step's `output` schema must satisfy the next step's `input`
208
240
  schema (the SDK enforces this at `defineWorkflow().step(...)` time
209
- via TS inference). Reshape inside the upstream step's `run` body,
210
- not at the boundary.
211
- - Step bodies don't receive a `sandbox`. If a step needs to run code
212
- inside the runner VM, that's the agent-driven shape use run-form.
241
+ via TS inference), and the FINAL step's `output` must be the same
242
+ zod instance as the workflow's declared `output`. Reshape inside the
243
+ upstream step's `run` body, not at the boundary.
244
+ - **Step bodies DO receive a `sandbox`**`ctx.sandbox` is present
245
+ whenever the run executes in a sandbox-backed workspace. Run
246
+ `agent({ sandbox: ctx.sandbox, … })`, `ctx.sandbox.commands.run(…)`,
247
+ `ctx.sandbox.files.write(…)` inside any step. (The example above uses
248
+ pure transforms, but an agent pass is a perfectly good step — and the
249
+ preferred shape, because each step is independently snapshotted.)
250
+ - `snapshots: { saveLatest: true }` captures the sandbox after EACH
251
+ step (latest-only); add `retainSteps: true` only if you need every
252
+ step's snapshot kept for fork/replay (linear storage cost).
213
253
 
214
254
  ### Template B — run-form (agent-driven body)
215
255
 
@@ -281,13 +321,6 @@ export default defineWorkflow({
281
321
  // saveLatest: true,
282
322
  // retainSteps: false,
283
323
  // },
284
- //
285
- // memory — opt-in. Requires `workflow-memory` to be registered in
286
- // this factory.
287
- // memory: true,
288
- //
289
- // postRunHooks — workflows that run after this one completes:
290
- // postRunHooks: ["audit-trail", "notify-slack"],
291
324
  });
292
325
  ```
293
326
 
@@ -347,20 +380,3 @@ export default defineWorkflow({
347
380
  "To run it on the schedule: `agentc schedule create pr-triage-daily
348
381
  --workflow pr-triage --cron '<expr>'` — the cron pattern we
349
382
  worked out earlier.")
350
-
351
- ## When the user asks for memory extraction
352
-
353
- The built-in memory extractor (`workflow-memory`) is a separate
354
- workflow that must be registered in the same factory. Walk them
355
- through it in user terms:
356
-
357
- 1. "I'll add the `workflow-memory` recipe to your project."
358
- Copy from `.agentc/templates/workflow-memory.ts`.
359
- 2. "Run `agentc register ./workflow-memory.ts` once to install it."
360
- 3. "Now any workflow with `memory: true` will trigger it after each
361
- successful run."
362
-
363
- For custom post-run workflows (analytics, audit, notifications)
364
- without the built-in extractor, use `postRunHooks: [...]` instead.
365
- Each post-hook runs in declaration order with the source run's full
366
- context.