cruo-agent 0.1.5 → 0.1.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.
Files changed (3) hide show
  1. package/dist/VERSION +1 -1
  2. package/dist/cli.js +443 -79
  3. package/package.json +1 -1
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.5
1
+ 0.1.6
package/dist/cli.js CHANGED
@@ -16166,7 +16166,7 @@ var require_FunctionsClient = __commonJS({
16166
16166
  let timeoutId;
16167
16167
  let timeoutController;
16168
16168
  try {
16169
- const { headers, method, body: functionArgs, signal, timeout } = options2;
16169
+ const { headers, method, body: functionArgs, signal: signal2, timeout } = options2;
16170
16170
  let _headers = {};
16171
16171
  let { region } = options2;
16172
16172
  if (!region) {
@@ -16199,13 +16199,13 @@ var require_FunctionsClient = __commonJS({
16199
16199
  body = functionArgs;
16200
16200
  }
16201
16201
  }
16202
- let effectiveSignal = signal;
16202
+ let effectiveSignal = signal2;
16203
16203
  if (timeout) {
16204
16204
  timeoutController = new AbortController();
16205
16205
  timeoutId = setTimeout(() => timeoutController.abort(), timeout);
16206
- if (signal) {
16206
+ if (signal2) {
16207
16207
  effectiveSignal = timeoutController.signal;
16208
- signal.addEventListener("abort", () => timeoutController.abort());
16208
+ signal2.addEventListener("abort", () => timeoutController.abort());
16209
16209
  } else {
16210
16210
  effectiveSignal = timeoutController.signal;
16211
16211
  }
@@ -16291,21 +16291,21 @@ var require_main = __commonJS({
16291
16291
  });
16292
16292
 
16293
16293
  // ../../node_modules/.pnpm/@supabase+postgrest-js@2.110.7/node_modules/@supabase/postgrest-js/dist/index.mjs
16294
- function sleep(ms2, signal) {
16294
+ function sleep(ms2, signal2) {
16295
16295
  return new Promise((resolve2) => {
16296
- if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
16296
+ if (signal2 === null || signal2 === void 0 ? void 0 : signal2.aborted) {
16297
16297
  resolve2();
16298
16298
  return;
16299
16299
  }
16300
16300
  const id = setTimeout(() => {
16301
- signal === null || signal === void 0 || signal.removeEventListener("abort", onAbort);
16301
+ signal2 === null || signal2 === void 0 || signal2.removeEventListener("abort", onAbort);
16302
16302
  resolve2();
16303
16303
  }, ms2);
16304
16304
  function onAbort() {
16305
16305
  clearTimeout(id);
16306
16306
  resolve2();
16307
16307
  }
16308
- signal === null || signal === void 0 || signal.addEventListener("abort", onAbort);
16308
+ signal2 === null || signal2 === void 0 || signal2.addEventListener("abort", onAbort);
16309
16309
  });
16310
16310
  }
16311
16311
  function shouldRetry(method, status, attemptCount, retryEnabled) {
@@ -17334,8 +17334,8 @@ ${cause.stack}`;
17334
17334
  *
17335
17335
  * ```
17336
17336
  */
17337
- abortSignal(signal) {
17338
- this.signal = signal;
17337
+ abortSignal(signal2) {
17338
+ this.signal = signal2;
17339
17339
  return this;
17340
17340
  }
17341
17341
  /**
@@ -23494,8 +23494,8 @@ var require_RealtimeChannel = __commonJS({
23494
23494
  }
23495
23495
  /** @internal */
23496
23496
  _notThisChannelEvent(event, ref) {
23497
- const { close, error: error51, leave, join: join4 } = constants_1.CHANNEL_EVENTS;
23498
- const events = [close, error51, leave, join4];
23497
+ const { close, error: error51, leave, join: join5 } = constants_1.CHANNEL_EVENTS;
23498
+ const events = [close, error51, leave, join5];
23499
23499
  return ref && events.includes(event) && ref !== this.joinPush.ref;
23500
23500
  }
23501
23501
  /** @internal */
@@ -30517,7 +30517,7 @@ var require_webauthn = __commonJS({
30517
30517
  * @see {@link https://w3c.github.io/webauthn/#sctn-credential-creation W3C WebAuthn Spec - Credential Creation}
30518
30518
  * @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying Assertion}
30519
30519
  */
30520
- async _challenge({ factorId, webauthn, friendlyName, signal }, overrides) {
30520
+ async _challenge({ factorId, webauthn, friendlyName, signal: signal2 }, overrides) {
30521
30521
  var _a3;
30522
30522
  try {
30523
30523
  const { data: challengeResponse, error: challengeError } = await this.client.mfa.challenge({
@@ -30527,7 +30527,7 @@ var require_webauthn = __commonJS({
30527
30527
  if (!challengeResponse) {
30528
30528
  return { data: null, error: challengeError };
30529
30529
  }
30530
- const abortSignal = signal !== null && signal !== void 0 ? signal : exports.webAuthnAbortService.createNewAbortSignal();
30530
+ const abortSignal = signal2 !== null && signal2 !== void 0 ? signal2 : exports.webAuthnAbortService.createNewAbortSignal();
30531
30531
  if (challengeResponse.webauthn.type === "create") {
30532
30532
  const { user } = challengeResponse.webauthn.credential_options.publicKey;
30533
30533
  if (!user.name) {
@@ -30631,7 +30631,7 @@ var require_webauthn = __commonJS({
30631
30631
  * @see {@link https://w3c.github.io/webauthn/#sctn-authentication W3C WebAuthn Spec - Authentication Ceremony}
30632
30632
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions MDN - PublicKeyCredentialRequestOptions}
30633
30633
  */
30634
- async _authenticate({ factorId, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal } = {} }, overrides) {
30634
+ async _authenticate({ factorId, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal: signal2 } = {} }, overrides) {
30635
30635
  if (!rpId) {
30636
30636
  return {
30637
30637
  data: null,
@@ -30648,7 +30648,7 @@ var require_webauthn = __commonJS({
30648
30648
  const { data: challengeResponse, error: challengeError } = await this.challenge({
30649
30649
  factorId,
30650
30650
  webauthn: { rpId, rpOrigins },
30651
- signal
30651
+ signal: signal2
30652
30652
  }, { request: overrides });
30653
30653
  if (!challengeResponse) {
30654
30654
  return { data: null, error: challengeError };
@@ -30689,7 +30689,7 @@ var require_webauthn = __commonJS({
30689
30689
  * @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registration Ceremony}
30690
30690
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions MDN - PublicKeyCredentialCreationOptions}
30691
30691
  */
30692
- async _register({ friendlyName, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal } = {} }, overrides) {
30692
+ async _register({ friendlyName, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal: signal2 } = {} }, overrides) {
30693
30693
  if (!rpId) {
30694
30694
  return {
30695
30695
  data: null,
@@ -30717,7 +30717,7 @@ var require_webauthn = __commonJS({
30717
30717
  factorId: factor.id,
30718
30718
  friendlyName: factor.friendly_name,
30719
30719
  webauthn: { rpId, rpOrigins },
30720
- signal
30720
+ signal: signal2
30721
30721
  }, {
30722
30722
  create: overrides
30723
30723
  });
@@ -35593,10 +35593,10 @@ var require_GoTrueClient = __commonJS({
35593
35593
  return this._returnResult({ data: null, error: optionsError });
35594
35594
  }
35595
35595
  const publicKeyOptions = (0, webauthn_1.deserializeCredentialRequestOptions)(options2.options);
35596
- const signal = (_c = (_b = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : webauthn_1.webAuthnAbortService.createNewAbortSignal();
35596
+ const signal2 = (_c = (_b = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : webauthn_1.webAuthnAbortService.createNewAbortSignal();
35597
35597
  const { data: credential, error: credentialError } = await (0, webauthn_1.getCredential)({
35598
35598
  publicKey: publicKeyOptions,
35599
- signal
35599
+ signal: signal2
35600
35600
  });
35601
35601
  if (credentialError || !credential) {
35602
35602
  return this._returnResult({
@@ -35641,10 +35641,10 @@ var require_GoTrueClient = __commonJS({
35641
35641
  return this._returnResult({ data: null, error: optionsError });
35642
35642
  }
35643
35643
  const publicKeyOptions = (0, webauthn_1.deserializeCredentialCreationOptions)(options2.options);
35644
- const signal = (_b = (_a3 = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a3 === void 0 ? void 0 : _a3.signal) !== null && _b !== void 0 ? _b : webauthn_1.webAuthnAbortService.createNewAbortSignal();
35644
+ const signal2 = (_b = (_a3 = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a3 === void 0 ? void 0 : _a3.signal) !== null && _b !== void 0 ? _b : webauthn_1.webAuthnAbortService.createNewAbortSignal();
35645
35645
  const { data: credential, error: credentialError } = await (0, webauthn_1.createCredential)({
35646
35646
  publicKey: publicKeyOptions,
35647
- signal
35647
+ signal: signal2
35648
35648
  });
35649
35649
  if (credentialError || !credential) {
35650
35650
  return this._returnResult({
@@ -36027,8 +36027,8 @@ function parseTraceParent(traceparent) {
36027
36027
  isSampled: (parseInt(traceFlags, 16) & 1) === 1
36028
36028
  };
36029
36029
  }
36030
- function shouldPropagateToTarget(targetUrl, targets) {
36031
- if (!targetUrl || !targets || targets.length === 0) return false;
36030
+ function shouldPropagateToTarget(targetUrl, targets2) {
36031
+ if (!targetUrl || !targets2 || targets2.length === 0) return false;
36032
36032
  let url3;
36033
36033
  if (targetUrl instanceof URL) url3 = targetUrl;
36034
36034
  else try {
@@ -36036,7 +36036,7 @@ function shouldPropagateToTarget(targetUrl, targets) {
36036
36036
  } catch (error51) {
36037
36037
  return false;
36038
36038
  }
36039
- for (const target of targets) try {
36039
+ for (const target of targets2) try {
36040
36040
  if (typeof target === "string") {
36041
36041
  if (matchStringTarget(url3.hostname, target)) return true;
36042
36042
  } else if (target instanceof RegExp) {
@@ -36060,15 +36060,15 @@ function matchStringTarget(hostname3, target) {
36060
36060
  return false;
36061
36061
  }
36062
36062
  function getDefaultPropagationTargets(supabaseUrl) {
36063
- const targets = [];
36063
+ const targets2 = [];
36064
36064
  try {
36065
36065
  const url3 = new URL(supabaseUrl);
36066
- targets.push(url3.hostname);
36066
+ targets2.push(url3.hostname);
36067
36067
  } catch (error51) {
36068
36068
  }
36069
- targets.push("*.supabase.co", "*.supabase.in");
36070
- targets.push("localhost", "127.0.0.1", "[::1]");
36071
- return targets;
36069
+ targets2.push("*.supabase.co", "*.supabase.in");
36070
+ targets2.push("localhost", "127.0.0.1", "[::1]");
36071
+ return targets2;
36072
36072
  }
36073
36073
  function _typeof3(o) {
36074
36074
  "@babel/helpers - typeof";
@@ -36121,8 +36121,8 @@ function _objectSpread23(e) {
36121
36121
  }
36122
36122
  return e;
36123
36123
  }
36124
- async function getTraceHeaders(input, targets, respectSampling) {
36125
- if (!shouldPropagateToTarget(typeof input === "string" ? input : input instanceof URL ? input : input.url, targets)) return null;
36124
+ async function getTraceHeaders(input, targets2, respectSampling) {
36125
+ if (!shouldPropagateToTarget(typeof input === "string" ? input : input instanceof URL ? input : input.url, targets2)) return null;
36126
36126
  const traceContext = await extractTraceContext();
36127
36127
  if (!traceContext || !traceContext.traceparent) return null;
36128
36128
  if (respectSampling) {
@@ -45957,6 +45957,110 @@ var init_options = __esm({
45957
45957
  }
45958
45958
  });
45959
45959
 
45960
+ // src/process.ts
45961
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
45962
+ import { execFile } from "node:child_process";
45963
+ import { homedir } from "node:os";
45964
+ import { join } from "node:path";
45965
+ import { promisify } from "node:util";
45966
+ function safeName(agent) {
45967
+ const cleaned = agent.toLowerCase().replace(/[^a-z0-9._-]/g, "").replace(/^\.+/, "");
45968
+ return cleaned === "" ? "default" : cleaned;
45969
+ }
45970
+ async function processStartedAt(pid) {
45971
+ try {
45972
+ const { stdout } = await exec("ps", ["-p", String(pid), "-o", "lstart="]);
45973
+ const value = stdout.trim();
45974
+ return value === "" ? null : value;
45975
+ } catch {
45976
+ return null;
45977
+ }
45978
+ }
45979
+ function isAlive(pid) {
45980
+ try {
45981
+ process.kill(pid, 0);
45982
+ return true;
45983
+ } catch (error51) {
45984
+ return error51.code === "EPERM";
45985
+ }
45986
+ }
45987
+ async function writeRecord(record2) {
45988
+ await mkdir(runDir(), { recursive: true });
45989
+ await writeFile(recordPath(record2.agent), JSON.stringify(record2, null, 2) + "\n", {
45990
+ mode: 384
45991
+ });
45992
+ }
45993
+ async function removeRecord(agent) {
45994
+ await rm(recordPath(agent), { force: true });
45995
+ }
45996
+ async function readRecord(agent) {
45997
+ try {
45998
+ const parsed = JSON.parse(await readFile(recordPath(agent), "utf8"));
45999
+ return typeof parsed?.pid === "number" ? parsed : null;
46000
+ } catch {
46001
+ return null;
46002
+ }
46003
+ }
46004
+ async function liveness(agent, tidy = false) {
46005
+ const record2 = await readRecord(agent);
46006
+ if (!record2) return { state: "absent" };
46007
+ if (!isAlive(record2.pid)) {
46008
+ if (tidy) await removeRecord(agent);
46009
+ return { state: "gone", record: record2 };
46010
+ }
46011
+ const now = await processStartedAt(record2.pid);
46012
+ if (record2.procStartedAt !== null && now !== null && now !== record2.procStartedAt) {
46013
+ if (tidy) await removeRecord(agent);
46014
+ return { state: "recycled", record: record2 };
46015
+ }
46016
+ return { state: "running", record: record2 };
46017
+ }
46018
+ async function listRecords() {
46019
+ let names;
46020
+ try {
46021
+ names = (await readdir(runDir())).filter((n2) => n2.endsWith(".json"));
46022
+ } catch {
46023
+ return [];
46024
+ }
46025
+ const out = [];
46026
+ for (const file2 of names.sort()) {
46027
+ const agent = file2.slice(0, -".json".length);
46028
+ out.push({ agent, live: await liveness(agent) });
46029
+ }
46030
+ return out;
46031
+ }
46032
+ function signal(pid, which) {
46033
+ try {
46034
+ process.kill(pid, SIGNALS[which]);
46035
+ return true;
46036
+ } catch {
46037
+ return false;
46038
+ }
46039
+ }
46040
+ function since(iso) {
46041
+ const ms2 = Date.now() - Date.parse(iso);
46042
+ if (!Number.isFinite(ms2) || ms2 < 0) return "?";
46043
+ if (ms2 < 6e4) return `${Math.round(ms2 / 1e3)}s`;
46044
+ if (ms2 < 36e5) return `${Math.round(ms2 / 6e4)}m`;
46045
+ if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
46046
+ return `${Math.round(ms2 / 864e5)}d`;
46047
+ }
46048
+ var exec, runDir, logDir, recordPath, SIGNALS;
46049
+ var init_process = __esm({
46050
+ "src/process.ts"() {
46051
+ "use strict";
46052
+ exec = promisify(execFile);
46053
+ runDir = () => join(homedir(), ".cruo", "run");
46054
+ logDir = () => join(homedir(), ".cruo", "logs");
46055
+ recordPath = (agent) => join(runDir(), `${safeName(agent)}.json`);
46056
+ SIGNALS = {
46057
+ stop: "SIGTERM",
46058
+ pause: "SIGUSR1",
46059
+ resume: "SIGUSR2"
46060
+ };
46061
+ }
46062
+ });
46063
+
45960
46064
  // src/env.ts
45961
46065
  function applyCloudDefaults() {
45962
46066
  const url3 = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -46048,8 +46152,8 @@ function compact(value) {
46048
46152
  }
46049
46153
  async function reportUsage(opts) {
46050
46154
  const ctx = await contextOrExplain();
46051
- const since = new Date(Date.now() - opts.days * 864e5).toISOString();
46052
- let query = ctx.client.from("harness_runs").select("*").gte("started_at", since).order("started_at", { ascending: false });
46155
+ const since2 = new Date(Date.now() - opts.days * 864e5).toISOString();
46156
+ let query = ctx.client.from("harness_runs").select("*").gte("started_at", since2).order("started_at", { ascending: false });
46053
46157
  let issueTitle = null;
46054
46158
  if (opts.issue) {
46055
46159
  const [key, num3] = opts.issue.split("-");
@@ -46363,12 +46467,12 @@ var init_pacing = __esm({
46363
46467
  });
46364
46468
 
46365
46469
  // src/worktree.ts
46366
- import { execFile } from "node:child_process";
46367
- import { mkdir, readdir, rm, stat } from "node:fs/promises";
46368
- import { join } from "node:path";
46369
- import { promisify } from "node:util";
46470
+ import { execFile as execFile2 } from "node:child_process";
46471
+ import { mkdir as mkdir2, readdir as readdir2, rm as rm2, stat } from "node:fs/promises";
46472
+ import { join as join2 } from "node:path";
46473
+ import { promisify as promisify2 } from "node:util";
46370
46474
  async function git(repo, args) {
46371
- const { stdout } = await exec("git", ["-C", repo, ...args], { maxBuffer: 8 * 1024 * 1024 });
46475
+ const { stdout } = await exec2("git", ["-C", repo, ...args], { maxBuffer: 8 * 1024 * 1024 });
46372
46476
  return stdout.trim();
46373
46477
  }
46374
46478
  async function hasRef(repo, ref) {
@@ -46407,16 +46511,16 @@ async function remoteUrl(repo, remote) {
46407
46511
  }
46408
46512
  async function createWorktree(ref, config3) {
46409
46513
  const branch = branchFor(ref);
46410
- const path = join(config3.root, ref);
46411
- await mkdir(config3.root, { recursive: true });
46412
- await rm(path, { recursive: true, force: true });
46514
+ const path = join2(config3.root, ref);
46515
+ await mkdir2(config3.root, { recursive: true });
46516
+ await rm2(path, { recursive: true, force: true });
46413
46517
  await git(config3.repo, ["worktree", "prune"]);
46414
46518
  const existing = await hasRef(config3.repo, branch);
46415
46519
  const startPoint = existing ? [branch] : ["-b", branch, config3.base];
46416
46520
  await git(config3.repo, ["worktree", "add", path, ...startPoint]);
46417
46521
  const startCommit = await git(config3.repo, ["rev-parse", branch]);
46418
46522
  if (config3.prepare) {
46419
- await exec("/bin/sh", ["-c", config3.prepare], {
46523
+ await exec2("/bin/sh", ["-c", config3.prepare], {
46420
46524
  cwd: path,
46421
46525
  maxBuffer: 32 * 1024 * 1024,
46422
46526
  timeout: config3.prepareTimeoutMs ?? 15 * 6e4,
@@ -46459,7 +46563,7 @@ Cruo-Wip: ${reason}
46459
46563
  },
46460
46564
  async remove() {
46461
46565
  await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
46462
- await rm(path, { recursive: true, force: true });
46566
+ await rm2(path, { recursive: true, force: true });
46463
46567
  });
46464
46568
  await git(config3.repo, ["worktree", "prune"]);
46465
46569
  }
@@ -46469,14 +46573,14 @@ async function sweep(config3, maxRunMs = 0) {
46469
46573
  await git(config3.repo, ["worktree", "prune"]);
46470
46574
  let entries;
46471
46575
  try {
46472
- entries = await readdir(config3.root);
46576
+ entries = await readdir2(config3.root);
46473
46577
  } catch {
46474
46578
  return [];
46475
46579
  }
46476
46580
  const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
46477
46581
  const removed = [];
46478
46582
  for (const name of entries) {
46479
- const path = join(config3.root, name);
46583
+ const path = join2(config3.root, name);
46480
46584
  let touched;
46481
46585
  try {
46482
46586
  touched = (await stat(path)).mtimeMs;
@@ -46485,18 +46589,18 @@ async function sweep(config3, maxRunMs = 0) {
46485
46589
  }
46486
46590
  if (touched >= cutoff) continue;
46487
46591
  await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
46488
- await rm(path, { recursive: true, force: true });
46592
+ await rm2(path, { recursive: true, force: true });
46489
46593
  });
46490
46594
  removed.push(name);
46491
46595
  }
46492
46596
  if (removed.length) await git(config3.repo, ["worktree", "prune"]);
46493
46597
  return removed;
46494
46598
  }
46495
- var exec;
46599
+ var exec2;
46496
46600
  var init_worktree = __esm({
46497
46601
  "src/worktree.ts"() {
46498
46602
  "use strict";
46499
- exec = promisify(execFile);
46603
+ exec2 = promisify2(execFile2);
46500
46604
  }
46501
46605
  });
46502
46606
 
@@ -46504,9 +46608,9 @@ var init_worktree = __esm({
46504
46608
  var supervisor_exports = {};
46505
46609
  import { spawn } from "node:child_process";
46506
46610
  import { createRequire } from "node:module";
46507
- import { mkdtemp, readdir as readdir2, readFile, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
46508
- import { homedir, tmpdir } from "node:os";
46509
- import { join as join2 } from "node:path";
46611
+ import { mkdtemp, readdir as readdir3, readFile as readFile2, rm as rm3, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
46612
+ import { homedir as homedir2, tmpdir } from "node:os";
46613
+ import { join as join3 } from "node:path";
46510
46614
  import { fileURLToPath } from "node:url";
46511
46615
  async function identify(ctx) {
46512
46616
  const { data: member, error: error51 } = await ctx.client.schema("public").from("members").select("kind, job_function, capabilities").eq("workspace_id", ctx.workspaceId).eq("user_id", ctx.userId).maybeSingle();
@@ -46531,7 +46635,7 @@ function resolveWorktreeRoot(identity) {
46531
46635
  if (options.worktreeRoot) return options.worktreeRoot;
46532
46636
  const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
46533
46637
  const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
46534
- return join2(homedir(), ".cruo-work", name);
46638
+ return join3(homedir2(), ".cruo-work", name);
46535
46639
  }
46536
46640
  async function pollAssigned(ctx) {
46537
46641
  const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
@@ -46679,13 +46783,13 @@ async function markRead(ctx, notificationIds) {
46679
46783
  }
46680
46784
  async function writeMcpConfig() {
46681
46785
  const session = await childSession();
46682
- const dir = await mkdtemp(join2(tmpdir(), CONFIG_DIR_PREFIX));
46683
- const path = join2(dir, "mcp.json");
46786
+ const dir = await mkdtemp(join3(tmpdir(), CONFIG_DIR_PREFIX));
46787
+ const path = join3(dir, "mcp.json");
46684
46788
  const here = fileURLToPath(new URL(".", import.meta.url));
46685
46789
  const bundled = /[/\\]dist[/\\]?$/.test(here);
46686
- const server = join2(here, bundled ? "index.js" : "index.ts");
46790
+ const server = join3(here, bundled ? "index.js" : "index.ts");
46687
46791
  const loader = bundled ? null : createRequire(import.meta.url).resolve("tsx");
46688
- await writeFile(
46792
+ await writeFile2(
46689
46793
  path,
46690
46794
  JSON.stringify(
46691
46795
  {
@@ -46709,7 +46813,7 @@ async function writeMcpConfig() {
46709
46813
  ),
46710
46814
  { mode: 384 }
46711
46815
  );
46712
- return { path, cleanup: () => rm2(dir, { recursive: true, force: true }) };
46816
+ return { path, cleanup: () => rm3(dir, { recursive: true, force: true }) };
46713
46817
  }
46714
46818
  async function sweepStaleConfigs() {
46715
46819
  const dir = tmpdir();
@@ -46717,18 +46821,18 @@ async function sweepStaleConfigs() {
46717
46821
  const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
46718
46822
  let entries;
46719
46823
  try {
46720
- entries = await readdir2(dir);
46824
+ entries = await readdir3(dir);
46721
46825
  } catch {
46722
46826
  return 0;
46723
46827
  }
46724
46828
  let removed = 0;
46725
46829
  for (const name of entries) {
46726
46830
  if (!name.startsWith(CONFIG_DIR_PREFIX)) continue;
46727
- const path = join2(dir, name);
46831
+ const path = join3(dir, name);
46728
46832
  try {
46729
46833
  const info = await stat2(path);
46730
46834
  if (info.mtimeMs >= cutoff) continue;
46731
- await rm2(path, { recursive: true, force: true });
46835
+ await rm3(path, { recursive: true, force: true });
46732
46836
  removed += 1;
46733
46837
  } catch {
46734
46838
  }
@@ -46738,7 +46842,7 @@ async function sweepStaleConfigs() {
46738
46842
  async function preflight() {
46739
46843
  const { path: mcpConfig, cleanup } = await writeMcpConfig();
46740
46844
  try {
46741
- const parsed = JSON.parse(await readFile(mcpConfig, "utf8"));
46845
+ const parsed = JSON.parse(await readFile2(mcpConfig, "utf8"));
46742
46846
  const { command, args, env } = parsed.mcpServers.cruo;
46743
46847
  await new Promise((resolve2, reject) => {
46744
46848
  const child = spawn(command, args, {
@@ -47010,8 +47114,8 @@ async function clearAttempts(ctx, hit) {
47010
47114
  const { error: error51 } = await ctx.client.from("issue_attempts").delete().eq("issue_id", hit.issue.id).eq("agent_id", ctx.userId).eq("reason", hit.reason);
47011
47115
  if (error51) log(` could not clear attempts on ${hit.ref}: ${error51.message}`);
47012
47116
  }
47013
- async function repliedSince(ctx, hit, since) {
47014
- const { count, error: error51 } = await ctx.client.from("comments").select("id", { count: "exact", head: true }).eq("issue_id", hit.issue.id).eq("author_id", ctx.userId).gt("created_at", since);
47117
+ async function repliedSince(ctx, hit, since2) {
47118
+ const { count, error: error51 } = await ctx.client.from("comments").select("id", { count: "exact", head: true }).eq("issue_id", hit.issue.id).eq("author_id", ctx.userId).gt("created_at", since2);
47015
47119
  if (error51) {
47016
47120
  log(` could not check for a reply on ${hit.ref}: ${error51.message}`);
47017
47121
  return false;
@@ -47046,11 +47150,29 @@ async function beat(ctx, fields) {
47046
47150
  last_seen_at: (/* @__PURE__ */ new Date()).toISOString(),
47047
47151
  holding: fields.holding,
47048
47152
  dead_ticks: fields.deadTicks,
47049
- poll_interval_ms: fields.intervalMs
47153
+ poll_interval_ms: fields.intervalMs,
47154
+ // CRA-102. Without this the board cannot tell an agent someone paused
47155
+ // from one that is wading through a backlog, nor one stopped on purpose
47156
+ // from one whose laptop shut its lid — all of them simply went quiet.
47157
+ status: fields.status ?? (paused ? "paused" : "running")
47050
47158
  },
47051
47159
  { onConflict: "user_id,workspace_id" }
47052
47160
  );
47053
47161
  if (error51) log(` could not record a heartbeat: ${error51.message}`);
47162
+ lastHolding = fields.holding;
47163
+ }
47164
+ function announce(status) {
47165
+ void (async () => {
47166
+ try {
47167
+ await beat(await getContext(), {
47168
+ holding: lastHolding,
47169
+ deadTicks: 0,
47170
+ intervalMs: options.intervalMs,
47171
+ status
47172
+ });
47173
+ } catch {
47174
+ }
47175
+ })();
47054
47176
  }
47055
47177
  async function recordRun(ctx, hit, run, outcome) {
47056
47178
  const { error: error51 } = await ctx.client.from("harness_runs").insert({
@@ -47249,6 +47371,35 @@ async function tick(ctx, identity, deadTicks, allowance) {
47249
47371
  });
47250
47372
  return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
47251
47373
  }
47374
+ function installControls() {
47375
+ const requestStop = (why) => {
47376
+ if (stopping && Date.now() - stopRequestedAt < 500) return;
47377
+ if (stopping) {
47378
+ log(` ${why} again \u2014 leaving now; a harness still running will be orphaned`);
47379
+ process.exit(130);
47380
+ }
47381
+ stopping = true;
47382
+ stopRequestedAt = Date.now();
47383
+ log(` ${why} \u2014 finishing the current run, then stopping. Again to leave now.`);
47384
+ };
47385
+ process.on("SIGTERM", () => requestStop("stop requested"));
47386
+ process.on("SIGINT", () => requestStop("interrupted"));
47387
+ process.on("SIGUSR1", () => {
47388
+ if (paused) return;
47389
+ paused = true;
47390
+ log(` paused \u2014 still polling and still reporting, but starting no new runs`);
47391
+ announce("paused");
47392
+ });
47393
+ process.on("SIGUSR2", () => {
47394
+ if (!paused) return;
47395
+ paused = false;
47396
+ log(` resumed \u2014 taking work again`);
47397
+ announce("running");
47398
+ });
47399
+ }
47400
+ function identityName() {
47401
+ return safeName(process.env.CRUO_IDENTITY?.trim() || "default");
47402
+ }
47252
47403
  async function main() {
47253
47404
  log(
47254
47405
  `board: ${config2.url}${config2.url === CRUO_CLOUD.apiUrl ? " (Cruo cloud)" : " (configured)"}`
@@ -47262,6 +47413,18 @@ async function main() {
47262
47413
  }
47263
47414
  const ctx = await contextOrExplain();
47264
47415
  const identity = await identify(ctx);
47416
+ installControls();
47417
+ const runName = identityName();
47418
+ if (!options.once && !options.dryRun) {
47419
+ await writeRecord({
47420
+ agent: runName,
47421
+ pid: process.pid,
47422
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
47423
+ procStartedAt: await processStartedAt(process.pid),
47424
+ log: join3(logDir(), `${runName}.log`),
47425
+ argv: process.argv.slice(2)
47426
+ });
47427
+ }
47265
47428
  try {
47266
47429
  await preflight();
47267
47430
  } catch (error51) {
@@ -47315,7 +47478,7 @@ async function main() {
47315
47478
  await getContext(),
47316
47479
  identity,
47317
47480
  pacing.deadTicks,
47318
- allowanceAt(pacing, Date.now())
47481
+ paused ? "held" : allowanceAt(pacing, Date.now())
47319
47482
  );
47320
47483
  if (result.suppressed) {
47321
47484
  const now = Date.now();
@@ -47342,10 +47505,12 @@ async function main() {
47342
47505
  log(`poll error: ${e instanceof Error ? e.message : String(e)}`);
47343
47506
  }
47344
47507
  if (options.once) return;
47508
+ if (stopping) return;
47345
47509
  await new Promise((r) => setTimeout(r, options.intervalMs));
47510
+ if (stopping) return;
47346
47511
  }
47347
47512
  }
47348
- var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, warnedAboutSpendTable;
47513
+ var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, lastHolding, warnedAboutSpendTable, stopping, paused, stopRequestedAt;
47349
47514
  var init_supervisor = __esm({
47350
47515
  "src/supervisor.ts"() {
47351
47516
  "use strict";
@@ -47355,6 +47520,7 @@ var init_supervisor = __esm({
47355
47520
  init_auth();
47356
47521
  init_options();
47357
47522
  init_harness_signal();
47523
+ init_process();
47358
47524
  init_pacing();
47359
47525
  init_worktree();
47360
47526
  argv = process.argv.slice(2);
@@ -47526,9 +47692,30 @@ cruo-supervisor: ${error51.message}
47526
47692
  "CRUO_SESSION"
47527
47693
  ];
47528
47694
  HEAD_STORED = 200;
47695
+ lastHolding = 0;
47529
47696
  warnedAboutSpendTable = false;
47530
- main().catch((e) => {
47697
+ stopping = false;
47698
+ paused = false;
47699
+ stopRequestedAt = 0;
47700
+ main().then(async () => {
47701
+ if (!options.once && !options.dryRun) {
47702
+ try {
47703
+ const ctx = await getContext();
47704
+ await beat(ctx, {
47705
+ holding: 0,
47706
+ deadTicks: 0,
47707
+ intervalMs: options.intervalMs,
47708
+ status: "stopped"
47709
+ });
47710
+ } catch {
47711
+ }
47712
+ await removeRecord(identityName()).catch(() => {
47713
+ });
47714
+ }
47715
+ }).catch(async (e) => {
47531
47716
  console.error(e instanceof Error ? e.message : e);
47717
+ await removeRecord(identityName()).catch(() => {
47718
+ });
47532
47719
  process.exit(1);
47533
47720
  });
47534
47721
  }
@@ -47538,11 +47725,12 @@ cruo-supervisor: ${error51.message}
47538
47725
  init_dist();
47539
47726
  init_dist7();
47540
47727
  init_options();
47541
- import { chmod, mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
47542
- import { homedir as homedir2 } from "node:os";
47543
- import { join as join3 } from "node:path";
47544
- var CONFIG_DIR = join3(homedir2(), ".cruo");
47545
- var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
47728
+ init_process();
47729
+ import { chmod, mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
47730
+ import { homedir as homedir3 } from "node:os";
47731
+ import { join as join4 } from "node:path";
47732
+ var CONFIG_DIR = join4(homedir3(), ".cruo");
47733
+ var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
47546
47734
  function nameFromEmail(email3) {
47547
47735
  const local = email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, "");
47548
47736
  return local && local.length > 0 ? local : null;
@@ -47553,9 +47741,22 @@ var USAGE = `cruo \u2014 run a Cruo agent
47553
47741
  cruo agents list what is remembered (never the tokens)
47554
47742
  cruo usage [--days 7] what the agents have been spending
47555
47743
  cruo logout [name] forget one, or --all
47556
- cruo [options] run the agent's supervisor
47744
+ cruo [options] run the agent's supervisor, in this terminal
47557
47745
  cruo --as <name> \u2026 run a particular one
47558
47746
 
47747
+ Running one in the background
47748
+ cruo start [options] same, detached \u2014 survives closing the terminal
47749
+ cruo ps what is running on this machine
47750
+ cruo stop [name] ask it to finish the run it is on, then exit
47751
+ cruo pause [name] keep watching the board, start nothing new
47752
+ cruo resume [name] take work again
47753
+ cruo logs [name] [-f] what a detached agent has been saying
47754
+ \u2026 --all stop/pause/resume every one of them
47755
+
47756
+ \`stop\` is a request, not a kill: a harness that is mid-run is allowed to
47757
+ finish, so the work it has done is not thrown away. Say it twice to give up
47758
+ waiting. macOS and Linux only \u2014 these are signals and pid files.
47759
+
47559
47760
  Getting a token
47560
47761
  Cruo \u2192 Settings \u2192 Members \u2192 Add an agent. It is shown once.
47561
47762
 
@@ -47581,7 +47782,7 @@ var EMPTY = { version: 2, default: null, agents: {} };
47581
47782
  async function readConfig2() {
47582
47783
  let parsed;
47583
47784
  try {
47584
- parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47785
+ parsed = JSON.parse(await readFile3(CONFIG_PATH, "utf8"));
47585
47786
  } catch {
47586
47787
  return { ...EMPTY };
47587
47788
  }
@@ -47600,8 +47801,8 @@ async function readConfig2() {
47600
47801
  return { ...EMPTY };
47601
47802
  }
47602
47803
  async function writeConfig(config3) {
47603
- await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
47604
- await writeFile2(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
47804
+ await mkdir3(CONFIG_DIR, { recursive: true, mode: 448 });
47805
+ await writeFile3(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
47605
47806
  await chmod(CONFIG_PATH, 384);
47606
47807
  }
47607
47808
  async function identify2(token) {
@@ -47661,7 +47862,7 @@ async function logout(which, all) {
47661
47862
  const config3 = await readConfig2();
47662
47863
  const names = Object.keys(config3.agents);
47663
47864
  if (all) {
47664
- await rm3(CONFIG_PATH, { force: true });
47865
+ await rm4(CONFIG_PATH, { force: true });
47665
47866
  console.log(
47666
47867
  names.length === 0 ? `
47667
47868
  Nothing was stored.
@@ -47673,7 +47874,7 @@ Forgot ${names.length} token(s): ${names.join(", ")}. ${CONFIG_PATH} is gone.
47673
47874
  }
47674
47875
  if (!which) {
47675
47876
  if (names.length <= 1) {
47676
- await rm3(CONFIG_PATH, { force: true });
47877
+ await rm4(CONFIG_PATH, { force: true });
47677
47878
  console.log(
47678
47879
  names.length === 0 ? `
47679
47880
  Nothing was stored.
@@ -47729,6 +47930,156 @@ No tokens stored. \`cruo login <token>\` to add one.
47729
47930
  * is what a bare \`cruo\` runs. Use \`--as <name>\` for another.
47730
47931
  `);
47731
47932
  }
47933
+ async function targetName(explicit, asName) {
47934
+ if (explicit) return safeName(explicit);
47935
+ if (asName) return safeName(asName);
47936
+ const config3 = await readConfig2();
47937
+ return safeName(config3.default ?? "default");
47938
+ }
47939
+ async function targets(explicit, asName, all) {
47940
+ if (all) {
47941
+ const rows = await listRecords();
47942
+ return rows.filter((r) => r.live.state === "running").map((r) => r.live.record);
47943
+ }
47944
+ const name = await targetName(explicit, asName);
47945
+ const live = await liveness(name, true);
47946
+ if (live.state === "running") return [live.record];
47947
+ if (live.state === "gone") {
47948
+ console.error(`
47949
+ ${name} is not running (it left a record behind; cleared).
47950
+ `);
47951
+ } else if (live.state === "recycled") {
47952
+ console.error(`
47953
+ ${name} is not running \u2014 its pid now belongs to something else. Cleared.
47954
+ `);
47955
+ } else {
47956
+ console.error(`
47957
+ ${name} is not running. Start it with \`cruo start --as ${name}\`.
47958
+ `);
47959
+ }
47960
+ return [];
47961
+ }
47962
+ async function startDetached(argv2, asName) {
47963
+ const { spawn: spawn2 } = await import("node:child_process");
47964
+ const { open, mkdir: mkdir4 } = await import("node:fs/promises");
47965
+ const name = await targetName(null, asName);
47966
+ const already = await liveness(name, true);
47967
+ if (already.state === "running") {
47968
+ console.error(
47969
+ `
47970
+ ${name} is already running (pid ${already.record.pid}, up ${since(already.record.startedAt)}).
47971
+ Stop it first, or use --as <name> to run a different agent.
47972
+ `
47973
+ );
47974
+ process.exit(1);
47975
+ }
47976
+ await mkdir4(logDir(), { recursive: true });
47977
+ const logPath = join4(logDir(), `${name}.log`);
47978
+ const handle = await open(logPath, "a");
47979
+ const forwarded = argv2.slice(1).filter((a) => a !== "--all");
47980
+ const child = spawn2(process.execPath, [process.argv[1], ...forwarded], {
47981
+ detached: true,
47982
+ stdio: ["ignore", handle.fd, handle.fd],
47983
+ env: process.env
47984
+ });
47985
+ child.unref();
47986
+ await handle.close();
47987
+ console.log(
47988
+ `
47989
+ ${name} started (pid ${child.pid}).
47990
+
47991
+ logs cruo logs ${name} -f
47992
+ check cruo ps
47993
+ stop cruo stop ${name}
47994
+
47995
+ It survives this terminal closing. It does not survive the machine
47996
+ sleeping or shutting down \u2014 for that, see the launchd notes in
47997
+ https://cruo.space/docs#agents.
47998
+ `
47999
+ );
48000
+ }
48001
+ async function listProcesses() {
48002
+ const rows = await listRecords();
48003
+ const running = rows.filter((r) => r.live.state === "running");
48004
+ if (rows.length === 0) {
48005
+ console.log(`
48006
+ Nothing running. Start one with \`cruo start\`.
48007
+ `);
48008
+ return;
48009
+ }
48010
+ console.log("");
48011
+ console.log(" agent pid up started with");
48012
+ for (const { agent, live } of rows) {
48013
+ if (live.state === "running") {
48014
+ const flags = live.record.argv.filter((a) => a.startsWith("--")).join(" ") || "(no flags)";
48015
+ console.log(
48016
+ ` ${agent.padEnd(14)} ${String(live.record.pid).padEnd(8)} ${since(live.record.startedAt).padEnd(8)} ${flags}`
48017
+ );
48018
+ } else if (live.state === "gone") {
48019
+ console.log(` ${agent.padEnd(14)} \u2014 \u2014 died or was killed (record cleared)`);
48020
+ await removeRecord(agent);
48021
+ } else if (live.state === "recycled") {
48022
+ console.log(` ${agent.padEnd(14)} \u2014 \u2014 gone; its pid is now someone else's (cleared)`);
48023
+ await removeRecord(agent);
48024
+ }
48025
+ }
48026
+ console.log(
48027
+ `
48028
+ ${running.length} running. Whether each is actually picking up work is on the board, not here \u2014
48029
+ Cruo \u2192 Settings \u2192 Members shows what it is holding and when it last looked.
48030
+ `
48031
+ );
48032
+ }
48033
+ async function control(which, explicit, asName, all) {
48034
+ const records = await targets(explicit, asName, all);
48035
+ if (records.length === 0) {
48036
+ if (all) console.log(`
48037
+ Nothing running.
48038
+ `);
48039
+ process.exitCode = all ? 0 : 1;
48040
+ return;
48041
+ }
48042
+ for (const record2 of records) {
48043
+ const sent = signal(record2.pid, which);
48044
+ if (!sent) {
48045
+ console.error(` ${record2.agent}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
48046
+ await removeRecord(record2.agent);
48047
+ continue;
48048
+ }
48049
+ if (which === "stop") {
48050
+ console.log(
48051
+ ` ${record2.agent}: asked to stop. It will finish the run it is on first \u2014
48052
+ a harness mid-run keeps its work, which is the point of asking rather than killing.
48053
+ Watch it leave with: cruo logs ${record2.agent} -f`
48054
+ );
48055
+ } else if (which === "pause") {
48056
+ console.log(` ${record2.agent}: paused. Still watching the board and still reporting; starts nothing new.`);
48057
+ } else {
48058
+ console.log(` ${record2.agent}: resumed.`);
48059
+ }
48060
+ }
48061
+ console.log("");
48062
+ }
48063
+ async function showLogs(explicit, asName, follow) {
48064
+ const { spawn: spawn2 } = await import("node:child_process");
48065
+ const name = await targetName(explicit, asName);
48066
+ const record2 = await readRecord(name);
48067
+ const path = record2?.log ?? join4(logDir(), `${name}.log`);
48068
+ const { access } = await import("node:fs/promises");
48069
+ try {
48070
+ await access(path);
48071
+ } catch {
48072
+ console.error(`
48073
+ No log for ${name} at ${path}.
48074
+ A detached run writes one; a foreground run prints to its terminal.
48075
+ `);
48076
+ process.exit(1);
48077
+ }
48078
+ const child = spawn2("tail", follow ? ["-f", "-n", "40", path] : ["-n", "200", path], {
48079
+ stdio: "inherit"
48080
+ });
48081
+ await new Promise((resolve2) => child.on("close", resolve2));
48082
+ }
47732
48083
  async function main2() {
47733
48084
  const argv2 = process.argv.slice(2);
47734
48085
  const [first] = argv2;
@@ -47751,12 +48102,25 @@ Usage: cruo login <token> [--as <name>]
47751
48102
  return logout(named, argv2.includes("--all"));
47752
48103
  }
47753
48104
  if (first === "agents" || first === "whoami") return listAgents();
48105
+ {
48106
+ const named = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
48107
+ const all = argv2.includes("--all");
48108
+ const asName2 = flagValue("--as");
48109
+ if (first === "ps") return listProcesses();
48110
+ if (first === "stop") return control("stop", named, asName2, all);
48111
+ if (first === "pause") return control("pause", named, asName2, all);
48112
+ if (first === "resume") return control("resume", named, asName2, all);
48113
+ if (first === "logs") {
48114
+ return showLogs(named, asName2, argv2.includes("-f") || argv2.includes("--follow"));
48115
+ }
48116
+ }
47754
48117
  if (first === "usage") {
47755
48118
  }
47756
48119
  if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
47757
48120
  console.log(USAGE);
47758
48121
  return;
47759
48122
  }
48123
+ if (first === "start") return startDetached(argv2, flagValue("--as"));
47760
48124
  const flagIndex = argv2.indexOf("--token");
47761
48125
  const fromFlag = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
47762
48126
  if (fromFlag) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",