cruo-agent 0.1.6 → 0.1.8

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.
Files changed (3) hide show
  1. package/dist/VERSION +1 -1
  2. package/dist/cli.js +231 -12
  3. package/package.json +1 -1
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.1.8
package/dist/cli.js CHANGED
@@ -45905,6 +45905,14 @@ function stripTokenFlag(argv2) {
45905
45905
  if (at < 0) return [...argv2];
45906
45906
  return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
45907
45907
  }
45908
+ function isKnownCommandWord(first) {
45909
+ if (first === void 0) return true;
45910
+ if (first.startsWith("-")) return true;
45911
+ return CRUO_COMMANDS.includes(first);
45912
+ }
45913
+ function nearestCommands(first) {
45914
+ return CRUO_COMMANDS.filter((k) => k[0] === first[0]).slice(0, 3);
45915
+ }
45908
45916
  function numericIn(argv2, name, spec = {}) {
45909
45917
  const i = argv2.indexOf(`--${name}`);
45910
45918
  if (i < 0) return spec.fallback;
@@ -45939,10 +45947,24 @@ function requiredNumericIn(argv2, name, fallback, spec = {}) {
45939
45947
  function secondsIn(argv2, name, fallbackSeconds) {
45940
45948
  return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
45941
45949
  }
45942
- var OptionError, flagIn, optIn;
45950
+ var CRUO_COMMANDS, OptionError, flagIn, optIn;
45943
45951
  var init_options = __esm({
45944
45952
  "src/options.ts"() {
45945
45953
  "use strict";
45954
+ CRUO_COMMANDS = [
45955
+ "login",
45956
+ "logout",
45957
+ "agents",
45958
+ "whoami",
45959
+ "usage",
45960
+ "help",
45961
+ "start",
45962
+ "ps",
45963
+ "stop",
45964
+ "pause",
45965
+ "resume",
45966
+ "logs"
45967
+ ];
45946
45968
  OptionError = class extends Error {
45947
45969
  constructor(message) {
45948
45970
  super(message);
@@ -46206,7 +46228,12 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
46206
46228
  cacheRead: 0,
46207
46229
  cacheWrite: 0,
46208
46230
  cost: 0,
46209
- measured: 0
46231
+ measured: 0,
46232
+ opening: 0,
46233
+ openingMeasured: 0,
46234
+ sysChars: 0,
46235
+ userChars: 0,
46236
+ charsMeasured: 0
46210
46237
  });
46211
46238
  const fold = (a, r) => {
46212
46239
  a.runs += 1;
@@ -46220,6 +46247,16 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
46220
46247
  a.cacheWrite += n(r.cache_write_tokens);
46221
46248
  a.cost += n(r.cost_usd);
46222
46249
  if (r.input_tokens !== null || r.cost_usd !== null) a.measured += 1;
46250
+ const openingKnown = r.opening_context_input_tokens !== null || r.opening_context_cache_read_tokens !== null || r.opening_context_cache_write_tokens !== null;
46251
+ if (openingKnown) {
46252
+ a.opening += n(r.opening_context_input_tokens) + n(r.opening_context_cache_read_tokens) + n(r.opening_context_cache_write_tokens);
46253
+ a.openingMeasured += 1;
46254
+ }
46255
+ if (r.system_prompt_chars !== null || r.user_prompt_chars !== null) {
46256
+ a.sysChars += n(r.system_prompt_chars);
46257
+ a.userChars += n(r.user_prompt_chars);
46258
+ a.charsMeasured += 1;
46259
+ }
46223
46260
  };
46224
46261
  const byAgent = /* @__PURE__ */ new Map();
46225
46262
  const total = blank();
@@ -46231,11 +46268,12 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
46231
46268
  const w = Math.max(12, ...[...byAgent.keys()].map((id) => nameOf(id).length));
46232
46269
  console.log(
46233
46270
  `
46234
- ${pad("agent", w)} ${rpad("runs", 5)} ${rpad("useful", 7)} ${rpad("dead", 5)} ${rpad("time", 7)} ${rpad("in", 8)} ${rpad("out", 8)} ${rpad("cost", 9)}`
46271
+ ${pad("agent", w)} ${rpad("runs", 5)} ${rpad("useful", 7)} ${rpad("dead", 5)} ${rpad("time", 7)} ${rpad("in", 8)} ${rpad("out", 8)} ${rpad("opening", 8)} ${rpad("cost", 9)}`
46235
46272
  );
46236
46273
  for (const [id, a] of [...byAgent].sort((x, y) => y[1].runs - x[1].runs)) {
46274
+ const openingAvg = a.openingMeasured > 0 ? compact(Math.round(a.opening / a.openingMeasured)) : "\u2014";
46237
46275
  console.log(
46238
- ` ${pad(nameOf(id), w)} ${rpad(String(a.runs), 5)} ${rpad(String(a.productive), 7)} ${rpad(String(a.neverRan), 5)} ${rpad(duration3(a.ms), 7)} ${rpad(compact(a.input + a.cacheRead), 8)} ${rpad(compact(a.output), 8)} ${rpad(a.cost > 0 ? `$${a.cost.toFixed(2)}` : "\u2014", 9)}`
46276
+ ` ${pad(nameOf(id), w)} ${rpad(String(a.runs), 5)} ${rpad(String(a.productive), 7)} ${rpad(String(a.neverRan), 5)} ${rpad(duration3(a.ms), 7)} ${rpad(compact(a.input + a.cacheRead), 8)} ${rpad(compact(a.output), 8)} ${rpad(openingAvg, 8)} ${rpad(a.cost > 0 ? `$${a.cost.toFixed(2)}` : "\u2014", 9)}`
46239
46277
  );
46240
46278
  }
46241
46279
  if (!opts.issue) {
@@ -46268,8 +46306,12 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
46268
46306
  console.log(`
46269
46307
  run by run`);
46270
46308
  for (const r of rows) {
46309
+ const openingKnown = r.opening_context_input_tokens !== null || r.opening_context_cache_read_tokens !== null || r.opening_context_cache_write_tokens !== null;
46310
+ const opening = openingKnown ? compact(
46311
+ n(r.opening_context_input_tokens) + n(r.opening_context_cache_read_tokens) + n(r.opening_context_cache_write_tokens)
46312
+ ) : "\u2014";
46271
46313
  console.log(
46272
- ` ${r.started_at.slice(0, 16).replace("T", " ")} ${pad(r.outcome, 13)} ${rpad(duration3(r.duration_ms), 6)} ${rpad(compact(n(r.output_tokens)), 7)} out ${nameOf(r.agent_id)}`
46314
+ ` ${r.started_at.slice(0, 16).replace("T", " ")} ${pad(r.outcome, 13)} ${rpad(duration3(r.duration_ms), 6)} ${rpad(compact(n(r.output_tokens)), 7)} out ${rpad(opening, 7)} opening ${nameOf(r.agent_id)}`
46273
46315
  );
46274
46316
  }
46275
46317
  }
@@ -46296,6 +46338,35 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
46296
46338
  } else {
46297
46339
  console.log("");
46298
46340
  }
46341
+ if (total.openingMeasured === 0) {
46342
+ console.log(
46343
+ ` No opening-context figure: needs --usage AND a harness that reports
46344
+ per-turn usage on its own events, which is stricter than reporting a
46345
+ final total \u2014 some harnesses do the second and not the first.
46346
+ `
46347
+ );
46348
+ } else {
46349
+ const avg = Math.round(total.opening / total.openingMeasured);
46350
+ console.log(
46351
+ ` opening context \u2014 what was loaded before any work happened, not a slice of "in":
46352
+ avg ${compact(avg)} tokens over ${total.openingMeasured} of ${total.runs} run(s) that reported a first turn.`
46353
+ );
46354
+ if (total.charsMeasured > 0) {
46355
+ const sysAvg = Math.round(total.sysChars / total.charsMeasured);
46356
+ const userAvg = Math.round(total.userChars / total.charsMeasured);
46357
+ console.log(
46358
+ ` Known by source: the system prompt averaged ${sysAvg} chars, the user prompt
46359
+ ${userAvg} \u2014 CHARACTERS, not tokens, because no tokenizer runs here, so this is
46360
+ not subtracted from the ${compact(avg)}-token figure above. The rest of the
46361
+ opening context \u2014 CLAUDE.md, the docs it points at, the card and its comments,
46362
+ and the MCP tool schemas \u2014 is read by the harness itself, off a filesystem and
46363
+ a tool list the supervisor cannot see, and is NOT broken down any further.
46364
+ `
46365
+ );
46366
+ } else {
46367
+ console.log("");
46368
+ }
46369
+ }
46299
46370
  }
46300
46371
  var n, pad, rpad, duration3;
46301
46372
  var init_usage = __esm({
@@ -46349,6 +46420,29 @@ function usageIn(text) {
46349
46420
  const measured = Object.values(out).some((v) => v !== void 0);
46350
46421
  return measured ? out : null;
46351
46422
  }
46423
+ function turnUsageIn(line) {
46424
+ let event;
46425
+ try {
46426
+ event = JSON.parse(line);
46427
+ } catch {
46428
+ return null;
46429
+ }
46430
+ if (typeof event !== "object" || event === null) return null;
46431
+ const e = event;
46432
+ if (e.type !== "assistant") return null;
46433
+ const message = e.message;
46434
+ const usage = message?.usage;
46435
+ if (!usage || typeof usage !== "object") return null;
46436
+ const out = {
46437
+ model: typeof message?.model === "string" ? message.model : void 0,
46438
+ inputTokens: num(usage.input_tokens) ?? num(usage.inputTokens),
46439
+ outputTokens: num(usage.output_tokens) ?? num(usage.outputTokens),
46440
+ cacheReadTokens: num(usage.cache_read_input_tokens) ?? num(usage.cacheReadInputTokens),
46441
+ cacheWriteTokens: num(usage.cache_creation_input_tokens) ?? num(usage.cacheCreationInputTokens)
46442
+ };
46443
+ const measured = Object.values(out).some((v) => v !== void 0);
46444
+ return measured ? out : null;
46445
+ }
46352
46446
  function renderEvent(line) {
46353
46447
  let event;
46354
46448
  try {
@@ -46466,6 +46560,17 @@ var init_pacing = __esm({
46466
46560
  }
46467
46561
  });
46468
46562
 
46563
+ // src/queries.ts
46564
+ function blocksAgentPickup(state) {
46565
+ return state.requires_human === true;
46566
+ }
46567
+ var init_queries = __esm({
46568
+ "src/queries.ts"() {
46569
+ "use strict";
46570
+ init_dist();
46571
+ }
46572
+ });
46573
+
46469
46574
  // src/worktree.ts
46470
46575
  import { execFile as execFile2 } from "node:child_process";
46471
46576
  import { mkdir as mkdir2, readdir as readdir2, rm as rm2, stat } from "node:fs/promises";
@@ -46658,6 +46763,12 @@ async function pollAssigned(ctx) {
46658
46763
  const project = byProject.get(issue2.project_id);
46659
46764
  if (!state || !project) return [];
46660
46765
  if (options.project && project.key !== options.project) return [];
46766
+ if (blocksAgentPickup(state)) {
46767
+ log(
46768
+ ` ${project.key}-${issue2.number} is in ${state.name}, which requires a human \u2014 not starting`
46769
+ );
46770
+ return [];
46771
+ }
46661
46772
  if (state.category === "completed" || state.category === "canceled") return [];
46662
46773
  const said = lastSaid.get(issue2.id);
46663
46774
  if (said && issue2.updated_at && said > issue2.updated_at) return [];
@@ -46692,6 +46803,12 @@ async function poll(ctx, identity) {
46692
46803
  const state = byState.get(issue2.status_id);
46693
46804
  if (!project || !state) return [];
46694
46805
  if (options.project && project.key !== options.project) return [];
46806
+ if (blocksAgentPickup(state)) {
46807
+ log(
46808
+ ` ${project.key}-${issue2.number} is in ${state.name}, which requires a human \u2014 not starting`
46809
+ );
46810
+ return [];
46811
+ }
46695
46812
  return [
46696
46813
  {
46697
46814
  ref: `${project.key}-${issue2.number}`,
@@ -46978,11 +47095,24 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46978
47095
  const cwd = worktree?.path ?? options.cwd;
46979
47096
  const { path: mcpConfig, cleanup } = await writeMcpConfig();
46980
47097
  const streaming = options.usage === true;
47098
+ const builtinTools = [
47099
+ ...new Set(
47100
+ options.allow.split(",").map((t) => t.trim()).filter((t) => t !== "" && !t.startsWith("mcp__")).map((t) => t.replace(/\(.*$/, ""))
47101
+ )
47102
+ ].join(",");
47103
+ const userPromptText = userPrompt(hit);
47104
+ const systemPromptText = systemPrompt(ctx, identity, worktree);
46981
47105
  const args = [
46982
47106
  "-p",
46983
- userPrompt(hit),
47107
+ userPromptText,
46984
47108
  "--mcp-config",
46985
47109
  mcpConfig,
47110
+ // Both, always. See the note above: without `--restricted` the harness
47111
+ // inherits the operator's settings, and without `--tools` it keeps every
47112
+ // built-in that `--restricted` does not itself remove.
47113
+ "--restricted",
47114
+ "--tools",
47115
+ builtinTools,
46986
47116
  // Only the server we just described. Without this the harness would also
46987
47117
  // load the developer's own .mcp.json — including, on this machine, a Cruo
46988
47118
  // token belonging to a human.
@@ -46990,7 +47120,7 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46990
47120
  "--allowedTools",
46991
47121
  options.allow,
46992
47122
  "--system-prompt",
46993
- systemPrompt(ctx, identity, worktree),
47123
+ systemPromptText,
46994
47124
  ...options.model ? ["--model", options.model] : [],
46995
47125
  // `--verbose` is not optional alongside stream-json: without it the harness
46996
47126
  // emits only the final result and the live view is a blank terminal for the
@@ -47012,6 +47142,7 @@ async function invokeHarness(ctx, identity, hit, worktree) {
47012
47142
  let tail = "";
47013
47143
  let head2 = "";
47014
47144
  let pending = "";
47145
+ let openingContext = null;
47015
47146
  const addHead = (text) => {
47016
47147
  if (head2.length < 2048) head2 += text.slice(0, 2048 - head2.length);
47017
47148
  };
@@ -47030,6 +47161,7 @@ async function invokeHarness(ctx, identity, hit, worktree) {
47030
47161
  for (const line of lines) {
47031
47162
  if (line.trim() === "") continue;
47032
47163
  tail = line;
47164
+ if (openingContext === null) openingContext = turnUsageIn(line);
47033
47165
  const rendered = renderEvent(line);
47034
47166
  if (rendered.text) addHead(rendered.text + "\n");
47035
47167
  if (rendered.show) process.stdout.write(rendered.show + "\n");
@@ -47053,13 +47185,17 @@ async function invokeHarness(ctx, identity, hit, worktree) {
47053
47185
  durationMs: Date.now() - startedAtMs,
47054
47186
  usage: null,
47055
47187
  killed,
47056
- stdoutHead: headFor(head2)
47188
+ stdoutHead: headFor(head2),
47189
+ openingContext: null,
47190
+ systemPromptChars: systemPromptText.length,
47191
+ userPromptChars: userPromptText.length
47057
47192
  });
47058
47193
  });
47059
47194
  child.on("close", (code) => {
47060
47195
  clearTimeout(timer);
47061
47196
  if (streaming && pending.trim() !== "") {
47062
47197
  tail = pending;
47198
+ if (openingContext === null) openingContext = turnUsageIn(pending);
47063
47199
  const rendered = renderEvent(pending);
47064
47200
  if (rendered.text) addHead(rendered.text + "\n");
47065
47201
  if (rendered.show) process.stdout.write(rendered.show + "\n");
@@ -47072,7 +47208,10 @@ async function invokeHarness(ctx, identity, hit, worktree) {
47072
47208
  durationMs: Date.now() - startedAtMs,
47073
47209
  usage: usageIn(tail),
47074
47210
  killed,
47075
- stdoutHead: headFor(head2)
47211
+ stdoutHead: headFor(head2),
47212
+ openingContext,
47213
+ systemPromptChars: systemPromptText.length,
47214
+ userPromptChars: userPromptText.length
47076
47215
  });
47077
47216
  });
47078
47217
  });
@@ -47174,6 +47313,16 @@ function announce(status) {
47174
47313
  }
47175
47314
  })();
47176
47315
  }
47316
+ function beatWhileBusy(ctx, deadTicks) {
47317
+ const timer = setInterval(() => {
47318
+ void beat(ctx, { holding: lastHolding, deadTicks, intervalMs: options.intervalMs }).catch(
47319
+ () => {
47320
+ }
47321
+ );
47322
+ }, options.intervalMs);
47323
+ timer.unref?.();
47324
+ return () => clearInterval(timer);
47325
+ }
47177
47326
  async function recordRun(ctx, hit, run, outcome) {
47178
47327
  const { error: error51 } = await ctx.client.from("harness_runs").insert({
47179
47328
  agent_id: ctx.userId,
@@ -47190,7 +47339,12 @@ async function recordRun(ctx, hit, run, outcome) {
47190
47339
  output_tokens: run.usage?.outputTokens ?? null,
47191
47340
  cache_read_tokens: run.usage?.cacheReadTokens ?? null,
47192
47341
  cache_write_tokens: run.usage?.cacheWriteTokens ?? null,
47193
- cost_usd: run.usage?.costUsd ?? null
47342
+ cost_usd: run.usage?.costUsd ?? null,
47343
+ opening_context_input_tokens: run.openingContext?.inputTokens ?? null,
47344
+ opening_context_cache_read_tokens: run.openingContext?.cacheReadTokens ?? null,
47345
+ opening_context_cache_write_tokens: run.openingContext?.cacheWriteTokens ?? null,
47346
+ system_prompt_chars: run.systemPromptChars,
47347
+ user_prompt_chars: run.userPromptChars
47194
47348
  });
47195
47349
  if (error51 && !warnedAboutSpendTable) {
47196
47350
  warnedAboutSpendTable = true;
@@ -47281,6 +47435,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
47281
47435
  let run;
47282
47436
  let committed = 0;
47283
47437
  let salvaged = null;
47438
+ const stopBusyBeat = beatWhileBusy(ctx, deadTicks);
47284
47439
  try {
47285
47440
  run = await invokeHarness(ctx, identity, hit, worktree);
47286
47441
  if (worktree) committed = await worktree.commits();
@@ -47303,6 +47458,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
47303
47458
  );
47304
47459
  }
47305
47460
  } finally {
47461
+ stopBusyBeat();
47306
47462
  await release(ctx, hit);
47307
47463
  }
47308
47464
  const code = run.code;
@@ -47416,6 +47572,22 @@ async function main() {
47416
47572
  installControls();
47417
47573
  const runName = identityName();
47418
47574
  if (!options.once && !options.dryRun) {
47575
+ const existing = await liveness(runName, true);
47576
+ if (existing.state === "running") {
47577
+ console.error(
47578
+ `
47579
+ ${runName} is already running here (pid ${existing.record.pid}, up ${since(existing.record.startedAt)}).
47580
+
47581
+ Two supervisors for one agent share a token, a queue and a heartbeat,
47582
+ and the second would take over the first one's record \u2014 leaving the
47583
+ first with no way to be stopped except by pid.
47584
+
47585
+ stop it cruo stop ${runName}
47586
+ look at it cruo ps
47587
+ `
47588
+ );
47589
+ process.exit(2);
47590
+ }
47419
47591
  await writeRecord({
47420
47592
  agent: runName,
47421
47593
  pid: process.pid,
@@ -47522,6 +47694,7 @@ var init_supervisor = __esm({
47522
47694
  init_harness_signal();
47523
47695
  init_process();
47524
47696
  init_pacing();
47697
+ init_queries();
47525
47698
  init_worktree();
47526
47699
  argv = process.argv.slice(2);
47527
47700
  flag = (name) => flagIn(argv, name);
@@ -47962,7 +48135,19 @@ ${name} is not running. Start it with \`cruo start --as ${name}\`.
47962
48135
  async function startDetached(argv2, asName) {
47963
48136
  const { spawn: spawn2 } = await import("node:child_process");
47964
48137
  const { open, mkdir: mkdir4 } = await import("node:fs/promises");
47965
- const name = await targetName(null, asName);
48138
+ const positional = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
48139
+ if (positional && asName && safeName(positional) !== safeName(asName)) {
48140
+ console.error(
48141
+ `
48142
+ Two different agents named: \`${positional}\` and \`--as ${asName}\`.
48143
+
48144
+ Say one: cruo start ${positional} \u2026
48145
+ or cruo start --as ${asName} \u2026
48146
+ `
48147
+ );
48148
+ process.exit(2);
48149
+ }
48150
+ const name = await targetName(positional, asName);
47966
48151
  const already = await liveness(name, true);
47967
48152
  if (already.state === "running") {
47968
48153
  console.error(
@@ -47973,10 +48158,30 @@ Stop it first, or use --as <name> to run a different agent.
47973
48158
  );
47974
48159
  process.exit(1);
47975
48160
  }
48161
+ const config3 = await readConfig2();
48162
+ const storedKey = Object.keys(config3.agents).find((k) => safeName(k) === name) ?? null;
48163
+ const hasToken = Boolean(process.env.CRUO_TOKEN?.trim()) || argv2.includes("--token") || storedKey !== null;
48164
+ if (!hasToken) {
48165
+ const stored = Object.keys(config3.agents);
48166
+ console.error(
48167
+ `
48168
+ Nothing stored for \`${name}\`, so there is no token to run it with.
48169
+
48170
+ ` + (stored.length > 0 ? `Stored: ${stored.join(", ")}
48171
+ Either \`cruo start ${stored[0]}\`, or add this one:
48172
+ ` : `Nothing is stored at all. Add one:
48173
+ `) + ` cruo login <token>
48174
+ `
48175
+ );
48176
+ process.exit(2);
48177
+ }
47976
48178
  await mkdir4(logDir(), { recursive: true });
47977
48179
  const logPath = join4(logDir(), `${name}.log`);
47978
48180
  const handle = await open(logPath, "a");
47979
- const forwarded = argv2.slice(1).filter((a) => a !== "--all");
48181
+ const rest = argv2.slice(positional ? 2 : 1).filter((a) => a !== "--all");
48182
+ const chosen = storedKey ?? positional ?? asName;
48183
+ const withoutAs = rest.filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
48184
+ const forwarded = chosen ? ["--as", chosen, ...withoutAs] : withoutAs;
47980
48185
  const child = spawn2(process.execPath, [process.argv[1], ...forwarded], {
47981
48186
  detached: true,
47982
48187
  stdio: ["ignore", handle.fd, handle.fd],
@@ -48120,6 +48325,20 @@ Usage: cruo login <token> [--as <name>]
48120
48325
  console.log(USAGE);
48121
48326
  return;
48122
48327
  }
48328
+ if (!isKnownCommandWord(first)) {
48329
+ const near = nearestCommands(first);
48330
+ console.error(
48331
+ `
48332
+ \`${first}\` is not a cruo command.
48333
+
48334
+ ` + (near.length > 0 ? `Did you mean: ${near.map((k) => `cruo ${k}`).join(" ")}
48335
+
48336
+ ` : "") + `To run the agent, give no command: cruo [options]
48337
+ Everything it takes: cruo --help
48338
+ `
48339
+ );
48340
+ process.exit(2);
48341
+ }
48123
48342
  if (first === "start") return startDetached(argv2, flagValue("--as"));
48124
48343
  const flagIndex = argv2.indexOf("--token");
48125
48344
  const fromFlag = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Run a Cruo agent: it watches your board, picks up the cards you assign it, and works them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://cruo.space/docs#agents",