cruo-agent 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/VERSION +1 -1
  2. package/dist/cli.js +539 -80
  3. package/package.json +1 -1
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.5
1
+ 0.1.7
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) {
@@ -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);
@@ -45957,6 +45979,110 @@ var init_options = __esm({
45957
45979
  }
45958
45980
  });
45959
45981
 
45982
+ // src/process.ts
45983
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
45984
+ import { execFile } from "node:child_process";
45985
+ import { homedir } from "node:os";
45986
+ import { join } from "node:path";
45987
+ import { promisify } from "node:util";
45988
+ function safeName(agent) {
45989
+ const cleaned = agent.toLowerCase().replace(/[^a-z0-9._-]/g, "").replace(/^\.+/, "");
45990
+ return cleaned === "" ? "default" : cleaned;
45991
+ }
45992
+ async function processStartedAt(pid) {
45993
+ try {
45994
+ const { stdout } = await exec("ps", ["-p", String(pid), "-o", "lstart="]);
45995
+ const value = stdout.trim();
45996
+ return value === "" ? null : value;
45997
+ } catch {
45998
+ return null;
45999
+ }
46000
+ }
46001
+ function isAlive(pid) {
46002
+ try {
46003
+ process.kill(pid, 0);
46004
+ return true;
46005
+ } catch (error51) {
46006
+ return error51.code === "EPERM";
46007
+ }
46008
+ }
46009
+ async function writeRecord(record2) {
46010
+ await mkdir(runDir(), { recursive: true });
46011
+ await writeFile(recordPath(record2.agent), JSON.stringify(record2, null, 2) + "\n", {
46012
+ mode: 384
46013
+ });
46014
+ }
46015
+ async function removeRecord(agent) {
46016
+ await rm(recordPath(agent), { force: true });
46017
+ }
46018
+ async function readRecord(agent) {
46019
+ try {
46020
+ const parsed = JSON.parse(await readFile(recordPath(agent), "utf8"));
46021
+ return typeof parsed?.pid === "number" ? parsed : null;
46022
+ } catch {
46023
+ return null;
46024
+ }
46025
+ }
46026
+ async function liveness(agent, tidy = false) {
46027
+ const record2 = await readRecord(agent);
46028
+ if (!record2) return { state: "absent" };
46029
+ if (!isAlive(record2.pid)) {
46030
+ if (tidy) await removeRecord(agent);
46031
+ return { state: "gone", record: record2 };
46032
+ }
46033
+ const now = await processStartedAt(record2.pid);
46034
+ if (record2.procStartedAt !== null && now !== null && now !== record2.procStartedAt) {
46035
+ if (tidy) await removeRecord(agent);
46036
+ return { state: "recycled", record: record2 };
46037
+ }
46038
+ return { state: "running", record: record2 };
46039
+ }
46040
+ async function listRecords() {
46041
+ let names;
46042
+ try {
46043
+ names = (await readdir(runDir())).filter((n2) => n2.endsWith(".json"));
46044
+ } catch {
46045
+ return [];
46046
+ }
46047
+ const out = [];
46048
+ for (const file2 of names.sort()) {
46049
+ const agent = file2.slice(0, -".json".length);
46050
+ out.push({ agent, live: await liveness(agent) });
46051
+ }
46052
+ return out;
46053
+ }
46054
+ function signal(pid, which) {
46055
+ try {
46056
+ process.kill(pid, SIGNALS[which]);
46057
+ return true;
46058
+ } catch {
46059
+ return false;
46060
+ }
46061
+ }
46062
+ function since(iso) {
46063
+ const ms2 = Date.now() - Date.parse(iso);
46064
+ if (!Number.isFinite(ms2) || ms2 < 0) return "?";
46065
+ if (ms2 < 6e4) return `${Math.round(ms2 / 1e3)}s`;
46066
+ if (ms2 < 36e5) return `${Math.round(ms2 / 6e4)}m`;
46067
+ if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
46068
+ return `${Math.round(ms2 / 864e5)}d`;
46069
+ }
46070
+ var exec, runDir, logDir, recordPath, SIGNALS;
46071
+ var init_process = __esm({
46072
+ "src/process.ts"() {
46073
+ "use strict";
46074
+ exec = promisify(execFile);
46075
+ runDir = () => join(homedir(), ".cruo", "run");
46076
+ logDir = () => join(homedir(), ".cruo", "logs");
46077
+ recordPath = (agent) => join(runDir(), `${safeName(agent)}.json`);
46078
+ SIGNALS = {
46079
+ stop: "SIGTERM",
46080
+ pause: "SIGUSR1",
46081
+ resume: "SIGUSR2"
46082
+ };
46083
+ }
46084
+ });
46085
+
45960
46086
  // src/env.ts
45961
46087
  function applyCloudDefaults() {
45962
46088
  const url3 = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -46048,8 +46174,8 @@ function compact(value) {
46048
46174
  }
46049
46175
  async function reportUsage(opts) {
46050
46176
  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 });
46177
+ const since2 = new Date(Date.now() - opts.days * 864e5).toISOString();
46178
+ let query = ctx.client.from("harness_runs").select("*").gte("started_at", since2).order("started_at", { ascending: false });
46053
46179
  let issueTitle = null;
46054
46180
  if (opts.issue) {
46055
46181
  const [key, num3] = opts.issue.split("-");
@@ -46363,12 +46489,12 @@ var init_pacing = __esm({
46363
46489
  });
46364
46490
 
46365
46491
  // 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";
46492
+ import { execFile as execFile2 } from "node:child_process";
46493
+ import { mkdir as mkdir2, readdir as readdir2, rm as rm2, stat } from "node:fs/promises";
46494
+ import { join as join2 } from "node:path";
46495
+ import { promisify as promisify2 } from "node:util";
46370
46496
  async function git(repo, args) {
46371
- const { stdout } = await exec("git", ["-C", repo, ...args], { maxBuffer: 8 * 1024 * 1024 });
46497
+ const { stdout } = await exec2("git", ["-C", repo, ...args], { maxBuffer: 8 * 1024 * 1024 });
46372
46498
  return stdout.trim();
46373
46499
  }
46374
46500
  async function hasRef(repo, ref) {
@@ -46407,16 +46533,16 @@ async function remoteUrl(repo, remote) {
46407
46533
  }
46408
46534
  async function createWorktree(ref, config3) {
46409
46535
  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 });
46536
+ const path = join2(config3.root, ref);
46537
+ await mkdir2(config3.root, { recursive: true });
46538
+ await rm2(path, { recursive: true, force: true });
46413
46539
  await git(config3.repo, ["worktree", "prune"]);
46414
46540
  const existing = await hasRef(config3.repo, branch);
46415
46541
  const startPoint = existing ? [branch] : ["-b", branch, config3.base];
46416
46542
  await git(config3.repo, ["worktree", "add", path, ...startPoint]);
46417
46543
  const startCommit = await git(config3.repo, ["rev-parse", branch]);
46418
46544
  if (config3.prepare) {
46419
- await exec("/bin/sh", ["-c", config3.prepare], {
46545
+ await exec2("/bin/sh", ["-c", config3.prepare], {
46420
46546
  cwd: path,
46421
46547
  maxBuffer: 32 * 1024 * 1024,
46422
46548
  timeout: config3.prepareTimeoutMs ?? 15 * 6e4,
@@ -46459,7 +46585,7 @@ Cruo-Wip: ${reason}
46459
46585
  },
46460
46586
  async remove() {
46461
46587
  await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
46462
- await rm(path, { recursive: true, force: true });
46588
+ await rm2(path, { recursive: true, force: true });
46463
46589
  });
46464
46590
  await git(config3.repo, ["worktree", "prune"]);
46465
46591
  }
@@ -46469,14 +46595,14 @@ async function sweep(config3, maxRunMs = 0) {
46469
46595
  await git(config3.repo, ["worktree", "prune"]);
46470
46596
  let entries;
46471
46597
  try {
46472
- entries = await readdir(config3.root);
46598
+ entries = await readdir2(config3.root);
46473
46599
  } catch {
46474
46600
  return [];
46475
46601
  }
46476
46602
  const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
46477
46603
  const removed = [];
46478
46604
  for (const name of entries) {
46479
- const path = join(config3.root, name);
46605
+ const path = join2(config3.root, name);
46480
46606
  let touched;
46481
46607
  try {
46482
46608
  touched = (await stat(path)).mtimeMs;
@@ -46485,18 +46611,18 @@ async function sweep(config3, maxRunMs = 0) {
46485
46611
  }
46486
46612
  if (touched >= cutoff) continue;
46487
46613
  await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
46488
- await rm(path, { recursive: true, force: true });
46614
+ await rm2(path, { recursive: true, force: true });
46489
46615
  });
46490
46616
  removed.push(name);
46491
46617
  }
46492
46618
  if (removed.length) await git(config3.repo, ["worktree", "prune"]);
46493
46619
  return removed;
46494
46620
  }
46495
- var exec;
46621
+ var exec2;
46496
46622
  var init_worktree = __esm({
46497
46623
  "src/worktree.ts"() {
46498
46624
  "use strict";
46499
- exec = promisify(execFile);
46625
+ exec2 = promisify2(execFile2);
46500
46626
  }
46501
46627
  });
46502
46628
 
@@ -46504,9 +46630,9 @@ var init_worktree = __esm({
46504
46630
  var supervisor_exports = {};
46505
46631
  import { spawn } from "node:child_process";
46506
46632
  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";
46633
+ import { mkdtemp, readdir as readdir3, readFile as readFile2, rm as rm3, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
46634
+ import { homedir as homedir2, tmpdir } from "node:os";
46635
+ import { join as join3 } from "node:path";
46510
46636
  import { fileURLToPath } from "node:url";
46511
46637
  async function identify(ctx) {
46512
46638
  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 +46657,7 @@ function resolveWorktreeRoot(identity) {
46531
46657
  if (options.worktreeRoot) return options.worktreeRoot;
46532
46658
  const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
46533
46659
  const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
46534
- return join2(homedir(), ".cruo-work", name);
46660
+ return join3(homedir2(), ".cruo-work", name);
46535
46661
  }
46536
46662
  async function pollAssigned(ctx) {
46537
46663
  const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
@@ -46679,13 +46805,13 @@ async function markRead(ctx, notificationIds) {
46679
46805
  }
46680
46806
  async function writeMcpConfig() {
46681
46807
  const session = await childSession();
46682
- const dir = await mkdtemp(join2(tmpdir(), CONFIG_DIR_PREFIX));
46683
- const path = join2(dir, "mcp.json");
46808
+ const dir = await mkdtemp(join3(tmpdir(), CONFIG_DIR_PREFIX));
46809
+ const path = join3(dir, "mcp.json");
46684
46810
  const here = fileURLToPath(new URL(".", import.meta.url));
46685
46811
  const bundled = /[/\\]dist[/\\]?$/.test(here);
46686
- const server = join2(here, bundled ? "index.js" : "index.ts");
46812
+ const server = join3(here, bundled ? "index.js" : "index.ts");
46687
46813
  const loader = bundled ? null : createRequire(import.meta.url).resolve("tsx");
46688
- await writeFile(
46814
+ await writeFile2(
46689
46815
  path,
46690
46816
  JSON.stringify(
46691
46817
  {
@@ -46709,7 +46835,7 @@ async function writeMcpConfig() {
46709
46835
  ),
46710
46836
  { mode: 384 }
46711
46837
  );
46712
- return { path, cleanup: () => rm2(dir, { recursive: true, force: true }) };
46838
+ return { path, cleanup: () => rm3(dir, { recursive: true, force: true }) };
46713
46839
  }
46714
46840
  async function sweepStaleConfigs() {
46715
46841
  const dir = tmpdir();
@@ -46717,18 +46843,18 @@ async function sweepStaleConfigs() {
46717
46843
  const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
46718
46844
  let entries;
46719
46845
  try {
46720
- entries = await readdir2(dir);
46846
+ entries = await readdir3(dir);
46721
46847
  } catch {
46722
46848
  return 0;
46723
46849
  }
46724
46850
  let removed = 0;
46725
46851
  for (const name of entries) {
46726
46852
  if (!name.startsWith(CONFIG_DIR_PREFIX)) continue;
46727
- const path = join2(dir, name);
46853
+ const path = join3(dir, name);
46728
46854
  try {
46729
46855
  const info = await stat2(path);
46730
46856
  if (info.mtimeMs >= cutoff) continue;
46731
- await rm2(path, { recursive: true, force: true });
46857
+ await rm3(path, { recursive: true, force: true });
46732
46858
  removed += 1;
46733
46859
  } catch {
46734
46860
  }
@@ -46738,7 +46864,7 @@ async function sweepStaleConfigs() {
46738
46864
  async function preflight() {
46739
46865
  const { path: mcpConfig, cleanup } = await writeMcpConfig();
46740
46866
  try {
46741
- const parsed = JSON.parse(await readFile(mcpConfig, "utf8"));
46867
+ const parsed = JSON.parse(await readFile2(mcpConfig, "utf8"));
46742
46868
  const { command, args, env } = parsed.mcpServers.cruo;
46743
46869
  await new Promise((resolve2, reject) => {
46744
46870
  const child = spawn(command, args, {
@@ -46874,11 +47000,22 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46874
47000
  const cwd = worktree?.path ?? options.cwd;
46875
47001
  const { path: mcpConfig, cleanup } = await writeMcpConfig();
46876
47002
  const streaming = options.usage === true;
47003
+ const builtinTools = [
47004
+ ...new Set(
47005
+ options.allow.split(",").map((t) => t.trim()).filter((t) => t !== "" && !t.startsWith("mcp__")).map((t) => t.replace(/\(.*$/, ""))
47006
+ )
47007
+ ].join(",");
46877
47008
  const args = [
46878
47009
  "-p",
46879
47010
  userPrompt(hit),
46880
47011
  "--mcp-config",
46881
47012
  mcpConfig,
47013
+ // Both, always. See the note above: without `--restricted` the harness
47014
+ // inherits the operator's settings, and without `--tools` it keeps every
47015
+ // built-in that `--restricted` does not itself remove.
47016
+ "--restricted",
47017
+ "--tools",
47018
+ builtinTools,
46882
47019
  // Only the server we just described. Without this the harness would also
46883
47020
  // load the developer's own .mcp.json — including, on this machine, a Cruo
46884
47021
  // token belonging to a human.
@@ -47010,8 +47147,8 @@ async function clearAttempts(ctx, hit) {
47010
47147
  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
47148
  if (error51) log(` could not clear attempts on ${hit.ref}: ${error51.message}`);
47012
47149
  }
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);
47150
+ async function repliedSince(ctx, hit, since2) {
47151
+ 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
47152
  if (error51) {
47016
47153
  log(` could not check for a reply on ${hit.ref}: ${error51.message}`);
47017
47154
  return false;
@@ -47046,11 +47183,29 @@ async function beat(ctx, fields) {
47046
47183
  last_seen_at: (/* @__PURE__ */ new Date()).toISOString(),
47047
47184
  holding: fields.holding,
47048
47185
  dead_ticks: fields.deadTicks,
47049
- poll_interval_ms: fields.intervalMs
47186
+ poll_interval_ms: fields.intervalMs,
47187
+ // CRA-102. Without this the board cannot tell an agent someone paused
47188
+ // from one that is wading through a backlog, nor one stopped on purpose
47189
+ // from one whose laptop shut its lid — all of them simply went quiet.
47190
+ status: fields.status ?? (paused ? "paused" : "running")
47050
47191
  },
47051
47192
  { onConflict: "user_id,workspace_id" }
47052
47193
  );
47053
47194
  if (error51) log(` could not record a heartbeat: ${error51.message}`);
47195
+ lastHolding = fields.holding;
47196
+ }
47197
+ function announce(status) {
47198
+ void (async () => {
47199
+ try {
47200
+ await beat(await getContext(), {
47201
+ holding: lastHolding,
47202
+ deadTicks: 0,
47203
+ intervalMs: options.intervalMs,
47204
+ status
47205
+ });
47206
+ } catch {
47207
+ }
47208
+ })();
47054
47209
  }
47055
47210
  async function recordRun(ctx, hit, run, outcome) {
47056
47211
  const { error: error51 } = await ctx.client.from("harness_runs").insert({
@@ -47249,6 +47404,35 @@ async function tick(ctx, identity, deadTicks, allowance) {
47249
47404
  });
47250
47405
  return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
47251
47406
  }
47407
+ function installControls() {
47408
+ const requestStop = (why) => {
47409
+ if (stopping && Date.now() - stopRequestedAt < 500) return;
47410
+ if (stopping) {
47411
+ log(` ${why} again \u2014 leaving now; a harness still running will be orphaned`);
47412
+ process.exit(130);
47413
+ }
47414
+ stopping = true;
47415
+ stopRequestedAt = Date.now();
47416
+ log(` ${why} \u2014 finishing the current run, then stopping. Again to leave now.`);
47417
+ };
47418
+ process.on("SIGTERM", () => requestStop("stop requested"));
47419
+ process.on("SIGINT", () => requestStop("interrupted"));
47420
+ process.on("SIGUSR1", () => {
47421
+ if (paused) return;
47422
+ paused = true;
47423
+ log(` paused \u2014 still polling and still reporting, but starting no new runs`);
47424
+ announce("paused");
47425
+ });
47426
+ process.on("SIGUSR2", () => {
47427
+ if (!paused) return;
47428
+ paused = false;
47429
+ log(` resumed \u2014 taking work again`);
47430
+ announce("running");
47431
+ });
47432
+ }
47433
+ function identityName() {
47434
+ return safeName(process.env.CRUO_IDENTITY?.trim() || "default");
47435
+ }
47252
47436
  async function main() {
47253
47437
  log(
47254
47438
  `board: ${config2.url}${config2.url === CRUO_CLOUD.apiUrl ? " (Cruo cloud)" : " (configured)"}`
@@ -47262,6 +47446,34 @@ async function main() {
47262
47446
  }
47263
47447
  const ctx = await contextOrExplain();
47264
47448
  const identity = await identify(ctx);
47449
+ installControls();
47450
+ const runName = identityName();
47451
+ if (!options.once && !options.dryRun) {
47452
+ const existing = await liveness(runName, true);
47453
+ if (existing.state === "running") {
47454
+ console.error(
47455
+ `
47456
+ ${runName} is already running here (pid ${existing.record.pid}, up ${since(existing.record.startedAt)}).
47457
+
47458
+ Two supervisors for one agent share a token, a queue and a heartbeat,
47459
+ and the second would take over the first one's record \u2014 leaving the
47460
+ first with no way to be stopped except by pid.
47461
+
47462
+ stop it cruo stop ${runName}
47463
+ look at it cruo ps
47464
+ `
47465
+ );
47466
+ process.exit(2);
47467
+ }
47468
+ await writeRecord({
47469
+ agent: runName,
47470
+ pid: process.pid,
47471
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
47472
+ procStartedAt: await processStartedAt(process.pid),
47473
+ log: join3(logDir(), `${runName}.log`),
47474
+ argv: process.argv.slice(2)
47475
+ });
47476
+ }
47265
47477
  try {
47266
47478
  await preflight();
47267
47479
  } catch (error51) {
@@ -47315,7 +47527,7 @@ async function main() {
47315
47527
  await getContext(),
47316
47528
  identity,
47317
47529
  pacing.deadTicks,
47318
- allowanceAt(pacing, Date.now())
47530
+ paused ? "held" : allowanceAt(pacing, Date.now())
47319
47531
  );
47320
47532
  if (result.suppressed) {
47321
47533
  const now = Date.now();
@@ -47342,10 +47554,12 @@ async function main() {
47342
47554
  log(`poll error: ${e instanceof Error ? e.message : String(e)}`);
47343
47555
  }
47344
47556
  if (options.once) return;
47557
+ if (stopping) return;
47345
47558
  await new Promise((r) => setTimeout(r, options.intervalMs));
47559
+ if (stopping) return;
47346
47560
  }
47347
47561
  }
47348
- var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, warnedAboutSpendTable;
47562
+ 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
47563
  var init_supervisor = __esm({
47350
47564
  "src/supervisor.ts"() {
47351
47565
  "use strict";
@@ -47355,6 +47569,7 @@ var init_supervisor = __esm({
47355
47569
  init_auth();
47356
47570
  init_options();
47357
47571
  init_harness_signal();
47572
+ init_process();
47358
47573
  init_pacing();
47359
47574
  init_worktree();
47360
47575
  argv = process.argv.slice(2);
@@ -47526,9 +47741,30 @@ cruo-supervisor: ${error51.message}
47526
47741
  "CRUO_SESSION"
47527
47742
  ];
47528
47743
  HEAD_STORED = 200;
47744
+ lastHolding = 0;
47529
47745
  warnedAboutSpendTable = false;
47530
- main().catch((e) => {
47746
+ stopping = false;
47747
+ paused = false;
47748
+ stopRequestedAt = 0;
47749
+ main().then(async () => {
47750
+ if (!options.once && !options.dryRun) {
47751
+ try {
47752
+ const ctx = await getContext();
47753
+ await beat(ctx, {
47754
+ holding: 0,
47755
+ deadTicks: 0,
47756
+ intervalMs: options.intervalMs,
47757
+ status: "stopped"
47758
+ });
47759
+ } catch {
47760
+ }
47761
+ await removeRecord(identityName()).catch(() => {
47762
+ });
47763
+ }
47764
+ }).catch(async (e) => {
47531
47765
  console.error(e instanceof Error ? e.message : e);
47766
+ await removeRecord(identityName()).catch(() => {
47767
+ });
47532
47768
  process.exit(1);
47533
47769
  });
47534
47770
  }
@@ -47538,11 +47774,12 @@ cruo-supervisor: ${error51.message}
47538
47774
  init_dist();
47539
47775
  init_dist7();
47540
47776
  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");
47777
+ init_process();
47778
+ import { chmod, mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
47779
+ import { homedir as homedir3 } from "node:os";
47780
+ import { join as join4 } from "node:path";
47781
+ var CONFIG_DIR = join4(homedir3(), ".cruo");
47782
+ var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
47546
47783
  function nameFromEmail(email3) {
47547
47784
  const local = email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, "");
47548
47785
  return local && local.length > 0 ? local : null;
@@ -47553,9 +47790,22 @@ var USAGE = `cruo \u2014 run a Cruo agent
47553
47790
  cruo agents list what is remembered (never the tokens)
47554
47791
  cruo usage [--days 7] what the agents have been spending
47555
47792
  cruo logout [name] forget one, or --all
47556
- cruo [options] run the agent's supervisor
47793
+ cruo [options] run the agent's supervisor, in this terminal
47557
47794
  cruo --as <name> \u2026 run a particular one
47558
47795
 
47796
+ Running one in the background
47797
+ cruo start [options] same, detached \u2014 survives closing the terminal
47798
+ cruo ps what is running on this machine
47799
+ cruo stop [name] ask it to finish the run it is on, then exit
47800
+ cruo pause [name] keep watching the board, start nothing new
47801
+ cruo resume [name] take work again
47802
+ cruo logs [name] [-f] what a detached agent has been saying
47803
+ \u2026 --all stop/pause/resume every one of them
47804
+
47805
+ \`stop\` is a request, not a kill: a harness that is mid-run is allowed to
47806
+ finish, so the work it has done is not thrown away. Say it twice to give up
47807
+ waiting. macOS and Linux only \u2014 these are signals and pid files.
47808
+
47559
47809
  Getting a token
47560
47810
  Cruo \u2192 Settings \u2192 Members \u2192 Add an agent. It is shown once.
47561
47811
 
@@ -47581,7 +47831,7 @@ var EMPTY = { version: 2, default: null, agents: {} };
47581
47831
  async function readConfig2() {
47582
47832
  let parsed;
47583
47833
  try {
47584
- parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47834
+ parsed = JSON.parse(await readFile3(CONFIG_PATH, "utf8"));
47585
47835
  } catch {
47586
47836
  return { ...EMPTY };
47587
47837
  }
@@ -47600,8 +47850,8 @@ async function readConfig2() {
47600
47850
  return { ...EMPTY };
47601
47851
  }
47602
47852
  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 });
47853
+ await mkdir3(CONFIG_DIR, { recursive: true, mode: 448 });
47854
+ await writeFile3(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
47605
47855
  await chmod(CONFIG_PATH, 384);
47606
47856
  }
47607
47857
  async function identify2(token) {
@@ -47661,7 +47911,7 @@ async function logout(which, all) {
47661
47911
  const config3 = await readConfig2();
47662
47912
  const names = Object.keys(config3.agents);
47663
47913
  if (all) {
47664
- await rm3(CONFIG_PATH, { force: true });
47914
+ await rm4(CONFIG_PATH, { force: true });
47665
47915
  console.log(
47666
47916
  names.length === 0 ? `
47667
47917
  Nothing was stored.
@@ -47673,7 +47923,7 @@ Forgot ${names.length} token(s): ${names.join(", ")}. ${CONFIG_PATH} is gone.
47673
47923
  }
47674
47924
  if (!which) {
47675
47925
  if (names.length <= 1) {
47676
- await rm3(CONFIG_PATH, { force: true });
47926
+ await rm4(CONFIG_PATH, { force: true });
47677
47927
  console.log(
47678
47928
  names.length === 0 ? `
47679
47929
  Nothing was stored.
@@ -47729,6 +47979,188 @@ No tokens stored. \`cruo login <token>\` to add one.
47729
47979
  * is what a bare \`cruo\` runs. Use \`--as <name>\` for another.
47730
47980
  `);
47731
47981
  }
47982
+ async function targetName(explicit, asName) {
47983
+ if (explicit) return safeName(explicit);
47984
+ if (asName) return safeName(asName);
47985
+ const config3 = await readConfig2();
47986
+ return safeName(config3.default ?? "default");
47987
+ }
47988
+ async function targets(explicit, asName, all) {
47989
+ if (all) {
47990
+ const rows = await listRecords();
47991
+ return rows.filter((r) => r.live.state === "running").map((r) => r.live.record);
47992
+ }
47993
+ const name = await targetName(explicit, asName);
47994
+ const live = await liveness(name, true);
47995
+ if (live.state === "running") return [live.record];
47996
+ if (live.state === "gone") {
47997
+ console.error(`
47998
+ ${name} is not running (it left a record behind; cleared).
47999
+ `);
48000
+ } else if (live.state === "recycled") {
48001
+ console.error(`
48002
+ ${name} is not running \u2014 its pid now belongs to something else. Cleared.
48003
+ `);
48004
+ } else {
48005
+ console.error(`
48006
+ ${name} is not running. Start it with \`cruo start --as ${name}\`.
48007
+ `);
48008
+ }
48009
+ return [];
48010
+ }
48011
+ async function startDetached(argv2, asName) {
48012
+ const { spawn: spawn2 } = await import("node:child_process");
48013
+ const { open, mkdir: mkdir4 } = await import("node:fs/promises");
48014
+ const positional = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
48015
+ if (positional && asName && safeName(positional) !== safeName(asName)) {
48016
+ console.error(
48017
+ `
48018
+ Two different agents named: \`${positional}\` and \`--as ${asName}\`.
48019
+
48020
+ Say one: cruo start ${positional} \u2026
48021
+ or cruo start --as ${asName} \u2026
48022
+ `
48023
+ );
48024
+ process.exit(2);
48025
+ }
48026
+ const name = await targetName(positional, asName);
48027
+ const already = await liveness(name, true);
48028
+ if (already.state === "running") {
48029
+ console.error(
48030
+ `
48031
+ ${name} is already running (pid ${already.record.pid}, up ${since(already.record.startedAt)}).
48032
+ Stop it first, or use --as <name> to run a different agent.
48033
+ `
48034
+ );
48035
+ process.exit(1);
48036
+ }
48037
+ const config3 = await readConfig2();
48038
+ const storedKey = Object.keys(config3.agents).find((k) => safeName(k) === name) ?? null;
48039
+ const hasToken = Boolean(process.env.CRUO_TOKEN?.trim()) || argv2.includes("--token") || storedKey !== null;
48040
+ if (!hasToken) {
48041
+ const stored = Object.keys(config3.agents);
48042
+ console.error(
48043
+ `
48044
+ Nothing stored for \`${name}\`, so there is no token to run it with.
48045
+
48046
+ ` + (stored.length > 0 ? `Stored: ${stored.join(", ")}
48047
+ Either \`cruo start ${stored[0]}\`, or add this one:
48048
+ ` : `Nothing is stored at all. Add one:
48049
+ `) + ` cruo login <token>
48050
+ `
48051
+ );
48052
+ process.exit(2);
48053
+ }
48054
+ await mkdir4(logDir(), { recursive: true });
48055
+ const logPath = join4(logDir(), `${name}.log`);
48056
+ const handle = await open(logPath, "a");
48057
+ const rest = argv2.slice(positional ? 2 : 1).filter((a) => a !== "--all");
48058
+ const chosen = storedKey ?? positional ?? asName;
48059
+ const withoutAs = rest.filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
48060
+ const forwarded = chosen ? ["--as", chosen, ...withoutAs] : withoutAs;
48061
+ const child = spawn2(process.execPath, [process.argv[1], ...forwarded], {
48062
+ detached: true,
48063
+ stdio: ["ignore", handle.fd, handle.fd],
48064
+ env: process.env
48065
+ });
48066
+ child.unref();
48067
+ await handle.close();
48068
+ console.log(
48069
+ `
48070
+ ${name} started (pid ${child.pid}).
48071
+
48072
+ logs cruo logs ${name} -f
48073
+ check cruo ps
48074
+ stop cruo stop ${name}
48075
+
48076
+ It survives this terminal closing. It does not survive the machine
48077
+ sleeping or shutting down \u2014 for that, see the launchd notes in
48078
+ https://cruo.space/docs#agents.
48079
+ `
48080
+ );
48081
+ }
48082
+ async function listProcesses() {
48083
+ const rows = await listRecords();
48084
+ const running = rows.filter((r) => r.live.state === "running");
48085
+ if (rows.length === 0) {
48086
+ console.log(`
48087
+ Nothing running. Start one with \`cruo start\`.
48088
+ `);
48089
+ return;
48090
+ }
48091
+ console.log("");
48092
+ console.log(" agent pid up started with");
48093
+ for (const { agent, live } of rows) {
48094
+ if (live.state === "running") {
48095
+ const flags = live.record.argv.filter((a) => a.startsWith("--")).join(" ") || "(no flags)";
48096
+ console.log(
48097
+ ` ${agent.padEnd(14)} ${String(live.record.pid).padEnd(8)} ${since(live.record.startedAt).padEnd(8)} ${flags}`
48098
+ );
48099
+ } else if (live.state === "gone") {
48100
+ console.log(` ${agent.padEnd(14)} \u2014 \u2014 died or was killed (record cleared)`);
48101
+ await removeRecord(agent);
48102
+ } else if (live.state === "recycled") {
48103
+ console.log(` ${agent.padEnd(14)} \u2014 \u2014 gone; its pid is now someone else's (cleared)`);
48104
+ await removeRecord(agent);
48105
+ }
48106
+ }
48107
+ console.log(
48108
+ `
48109
+ ${running.length} running. Whether each is actually picking up work is on the board, not here \u2014
48110
+ Cruo \u2192 Settings \u2192 Members shows what it is holding and when it last looked.
48111
+ `
48112
+ );
48113
+ }
48114
+ async function control(which, explicit, asName, all) {
48115
+ const records = await targets(explicit, asName, all);
48116
+ if (records.length === 0) {
48117
+ if (all) console.log(`
48118
+ Nothing running.
48119
+ `);
48120
+ process.exitCode = all ? 0 : 1;
48121
+ return;
48122
+ }
48123
+ for (const record2 of records) {
48124
+ const sent = signal(record2.pid, which);
48125
+ if (!sent) {
48126
+ console.error(` ${record2.agent}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
48127
+ await removeRecord(record2.agent);
48128
+ continue;
48129
+ }
48130
+ if (which === "stop") {
48131
+ console.log(
48132
+ ` ${record2.agent}: asked to stop. It will finish the run it is on first \u2014
48133
+ a harness mid-run keeps its work, which is the point of asking rather than killing.
48134
+ Watch it leave with: cruo logs ${record2.agent} -f`
48135
+ );
48136
+ } else if (which === "pause") {
48137
+ console.log(` ${record2.agent}: paused. Still watching the board and still reporting; starts nothing new.`);
48138
+ } else {
48139
+ console.log(` ${record2.agent}: resumed.`);
48140
+ }
48141
+ }
48142
+ console.log("");
48143
+ }
48144
+ async function showLogs(explicit, asName, follow) {
48145
+ const { spawn: spawn2 } = await import("node:child_process");
48146
+ const name = await targetName(explicit, asName);
48147
+ const record2 = await readRecord(name);
48148
+ const path = record2?.log ?? join4(logDir(), `${name}.log`);
48149
+ const { access } = await import("node:fs/promises");
48150
+ try {
48151
+ await access(path);
48152
+ } catch {
48153
+ console.error(`
48154
+ No log for ${name} at ${path}.
48155
+ A detached run writes one; a foreground run prints to its terminal.
48156
+ `);
48157
+ process.exit(1);
48158
+ }
48159
+ const child = spawn2("tail", follow ? ["-f", "-n", "40", path] : ["-n", "200", path], {
48160
+ stdio: "inherit"
48161
+ });
48162
+ await new Promise((resolve2) => child.on("close", resolve2));
48163
+ }
47732
48164
  async function main2() {
47733
48165
  const argv2 = process.argv.slice(2);
47734
48166
  const [first] = argv2;
@@ -47751,12 +48183,39 @@ Usage: cruo login <token> [--as <name>]
47751
48183
  return logout(named, argv2.includes("--all"));
47752
48184
  }
47753
48185
  if (first === "agents" || first === "whoami") return listAgents();
48186
+ {
48187
+ const named = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
48188
+ const all = argv2.includes("--all");
48189
+ const asName2 = flagValue("--as");
48190
+ if (first === "ps") return listProcesses();
48191
+ if (first === "stop") return control("stop", named, asName2, all);
48192
+ if (first === "pause") return control("pause", named, asName2, all);
48193
+ if (first === "resume") return control("resume", named, asName2, all);
48194
+ if (first === "logs") {
48195
+ return showLogs(named, asName2, argv2.includes("-f") || argv2.includes("--follow"));
48196
+ }
48197
+ }
47754
48198
  if (first === "usage") {
47755
48199
  }
47756
48200
  if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
47757
48201
  console.log(USAGE);
47758
48202
  return;
47759
48203
  }
48204
+ if (!isKnownCommandWord(first)) {
48205
+ const near = nearestCommands(first);
48206
+ console.error(
48207
+ `
48208
+ \`${first}\` is not a cruo command.
48209
+
48210
+ ` + (near.length > 0 ? `Did you mean: ${near.map((k) => `cruo ${k}`).join(" ")}
48211
+
48212
+ ` : "") + `To run the agent, give no command: cruo [options]
48213
+ Everything it takes: cruo --help
48214
+ `
48215
+ );
48216
+ process.exit(2);
48217
+ }
48218
+ if (first === "start") return startDetached(argv2, flagValue("--as"));
47760
48219
  const flagIndex = argv2.indexOf("--token");
47761
48220
  const fromFlag = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
47762
48221
  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.7",
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",