cruo-agent 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
package/dist/cli.js CHANGED
@@ -45,6 +45,64 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
45
45
  ));
46
46
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
47
47
 
48
+ // src/options.ts
49
+ function stripTokenFlag(argv2) {
50
+ const at = argv2.indexOf("--token");
51
+ if (at < 0) return [...argv2];
52
+ return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
53
+ }
54
+ function numericIn(argv2, name, spec = {}) {
55
+ const i = argv2.indexOf(`--${name}`);
56
+ if (i < 0) return spec.fallback;
57
+ const raw = argv2[i + 1];
58
+ if (raw === void 0 || raw.startsWith("--")) {
59
+ throw new OptionError(
60
+ `--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
61
+ );
62
+ }
63
+ const value = raw.trim() === "" ? NaN : Number(raw);
64
+ const what = `--${name} ${JSON.stringify(raw)}`;
65
+ if (!Number.isFinite(value)) {
66
+ throw new OptionError(
67
+ `${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
68
+ // negative and falls to the range check below, which is the accurate
69
+ // refusal for it — this hint is for the case where the leading dash is
70
+ // the reason it did not parse at all.
71
+ (raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
72
+ );
73
+ }
74
+ if (value <= 0) {
75
+ throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
76
+ }
77
+ if (spec.integer && !Number.isInteger(value)) {
78
+ throw new OptionError(`${what} must be a whole number.`);
79
+ }
80
+ return value;
81
+ }
82
+ function requiredNumericIn(argv2, name, fallback, spec = {}) {
83
+ return numericIn(argv2, name, { ...spec, fallback });
84
+ }
85
+ function secondsIn(argv2, name, fallbackSeconds) {
86
+ return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
87
+ }
88
+ var OptionError, flagIn, optIn;
89
+ var init_options = __esm({
90
+ "src/options.ts"() {
91
+ "use strict";
92
+ OptionError = class extends Error {
93
+ constructor(message) {
94
+ super(message);
95
+ this.name = "OptionError";
96
+ }
97
+ };
98
+ flagIn = (argv2, name) => argv2.includes(`--${name}`);
99
+ optIn = (argv2, name, fallback) => {
100
+ const i = argv2.indexOf(`--${name}`);
101
+ return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
102
+ };
103
+ }
104
+ });
105
+
48
106
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
49
107
  // @__NO_SIDE_EFFECTS__
50
108
  function $constructor(name, initializer3, params) {
@@ -15238,10 +15296,13 @@ var init_plans = __esm({
15238
15296
  });
15239
15297
 
15240
15298
  // ../../packages/core/dist/mcp-connect.js
15241
- var CRUO_CLOUD;
15299
+ var DOCS_URL, AGENT_DOCS_URL, MCP_DOCS_URL, CRUO_CLOUD;
15242
15300
  var init_mcp_connect = __esm({
15243
15301
  "../../packages/core/dist/mcp-connect.js"() {
15244
15302
  "use strict";
15303
+ DOCS_URL = "https://cruo.space/docs";
15304
+ AGENT_DOCS_URL = `${DOCS_URL}#agents`;
15305
+ MCP_DOCS_URL = `${DOCS_URL}#mcp`;
15245
15306
  CRUO_CLOUD = {
15246
15307
  apiUrl: "https://szsutuujxbldkykcvdrm.supabase.co",
15247
15308
  publishableKey: "sb_publishable_hpnQFQ8IV0JXTch51XAstA_-S8H1eaD",
@@ -45940,62 +46001,34 @@ var init_auth = __esm({
45940
46001
  }
45941
46002
  });
45942
46003
 
45943
- // src/options.ts
45944
- function numericIn(argv2, name, spec = {}) {
45945
- const i = argv2.indexOf(`--${name}`);
45946
- if (i < 0) return spec.fallback;
45947
- const raw = argv2[i + 1];
45948
- if (raw === void 0 || raw.startsWith("--")) {
45949
- throw new OptionError(
45950
- `--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
45951
- );
45952
- }
45953
- const value = raw.trim() === "" ? NaN : Number(raw);
45954
- const what = `--${name} ${JSON.stringify(raw)}`;
45955
- if (!Number.isFinite(value)) {
45956
- throw new OptionError(
45957
- `${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
45958
- // negative and falls to the range check below, which is the accurate
45959
- // refusal for it — this hint is for the case where the leading dash is
45960
- // the reason it did not parse at all.
45961
- (raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
45962
- );
45963
- }
45964
- if (value <= 0) {
45965
- throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
45966
- }
45967
- if (spec.integer && !Number.isInteger(value)) {
45968
- throw new OptionError(`${what} must be a whole number.`);
46004
+ // src/harness-signal.ts
46005
+ function refusalIn(stdout) {
46006
+ if (!stdout.trim()) return null;
46007
+ for (const [pattern, reason] of REFUSALS) {
46008
+ if (pattern.test(stdout)) return reason;
45969
46009
  }
45970
- return value;
46010
+ return null;
45971
46011
  }
45972
- function requiredNumericIn(argv2, name, fallback, spec = {}) {
45973
- return numericIn(argv2, name, { ...spec, fallback });
45974
- }
45975
- function secondsIn(argv2, name, fallbackSeconds) {
45976
- return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
46012
+ function neverRan(run) {
46013
+ return run.refusal !== null || run.stdoutBytes === 0;
45977
46014
  }
45978
- var OptionError, flagIn, optIn;
45979
- var init_options = __esm({
45980
- "src/options.ts"() {
46015
+ var REFUSALS;
46016
+ var init_harness_signal = __esm({
46017
+ "src/harness-signal.ts"() {
45981
46018
  "use strict";
45982
- OptionError = class extends Error {
45983
- constructor(message) {
45984
- super(message);
45985
- this.name = "OptionError";
45986
- }
45987
- };
45988
- flagIn = (argv2, name) => argv2.includes(`--${name}`);
45989
- optIn = (argv2, name, fallback) => {
45990
- const i = argv2.indexOf(`--${name}`);
45991
- return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
45992
- };
46019
+ REFUSALS = [
46020
+ [/hit your (monthly |weekly |daily )?(spend|usage) limit/i, "the account's spend limit is reached"],
46021
+ [/insufficient credit|out of credit|credit balance is too low/i, "the account is out of credit"],
46022
+ [/upgrade to (a paid plan|claude pro)/i, "the plan does not cover this"],
46023
+ [/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
46024
+ [/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
46025
+ ];
45993
46026
  }
45994
46027
  });
45995
46028
 
45996
46029
  // src/worktree.ts
45997
46030
  import { execFile } from "node:child_process";
45998
- import { mkdir, readdir, rm } from "node:fs/promises";
46031
+ import { mkdir, readdir, rm, stat } from "node:fs/promises";
45999
46032
  import { join } from "node:path";
46000
46033
  import { promisify } from "node:util";
46001
46034
  async function git(repo, args) {
@@ -46078,7 +46111,7 @@ async function createWorktree(ref, config3) {
46078
46111
  }
46079
46112
  };
46080
46113
  }
46081
- async function sweep(config3) {
46114
+ async function sweep(config3, maxRunMs = 0) {
46082
46115
  await git(config3.repo, ["worktree", "prune"]);
46083
46116
  let entries;
46084
46117
  try {
@@ -46086,9 +46119,17 @@ async function sweep(config3) {
46086
46119
  } catch {
46087
46120
  return [];
46088
46121
  }
46122
+ const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
46089
46123
  const removed = [];
46090
46124
  for (const name of entries) {
46091
46125
  const path = join(config3.root, name);
46126
+ let touched;
46127
+ try {
46128
+ touched = (await stat(path)).mtimeMs;
46129
+ } catch {
46130
+ continue;
46131
+ }
46132
+ if (touched >= cutoff) continue;
46092
46133
  await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
46093
46134
  await rm(path, { recursive: true, force: true });
46094
46135
  });
@@ -46109,7 +46150,7 @@ var init_worktree = __esm({
46109
46150
  var supervisor_exports = {};
46110
46151
  import { spawn } from "node:child_process";
46111
46152
  import { createRequire } from "node:module";
46112
- import { mkdtemp, readdir as readdir2, readFile, rm as rm2, stat, writeFile } from "node:fs/promises";
46153
+ import { mkdtemp, readdir as readdir2, readFile, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
46113
46154
  import { homedir, tmpdir } from "node:os";
46114
46155
  import { join as join2 } from "node:path";
46115
46156
  import { fileURLToPath } from "node:url";
@@ -46132,6 +46173,12 @@ async function identify(ctx) {
46132
46173
  capabilities: member.capabilities
46133
46174
  };
46134
46175
  }
46176
+ function resolveWorktreeRoot(identity) {
46177
+ if (options.worktreeRoot) return options.worktreeRoot;
46178
+ const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
46179
+ const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
46180
+ return join2(homedir(), ".cruo-work", name);
46181
+ }
46135
46182
  async function pollAssigned(ctx) {
46136
46183
  const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
46137
46184
  if (error51) throw new Error(`Assignment poll failed: ${error51.message}`);
@@ -46317,7 +46364,7 @@ async function sweepStaleConfigs() {
46317
46364
  if (!name.startsWith(CONFIG_DIR_PREFIX)) continue;
46318
46365
  const path = join2(dir, name);
46319
46366
  try {
46320
- const info = await stat(path);
46367
+ const info = await stat2(path);
46321
46368
  if (info.mtimeMs >= cutoff) continue;
46322
46369
  await rm2(path, { recursive: true, force: true });
46323
46370
  removed += 1;
@@ -46447,9 +46494,6 @@ function userPrompt(hit) {
46447
46494
  `which your function owns. Pick it up and do your part.`
46448
46495
  ].join(" ");
46449
46496
  }
46450
- function neverRan(run) {
46451
- return run.code !== 0 && run.stdoutBytes === 0;
46452
- }
46453
46497
  async function invokeHarness(ctx, identity, hit, worktree) {
46454
46498
  const cwd = worktree?.path ?? options.cwd;
46455
46499
  const { path: mcpConfig, cleanup } = await writeMcpConfig();
@@ -46479,8 +46523,10 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46479
46523
  env: harnessEnv()
46480
46524
  });
46481
46525
  let stdoutBytes = 0;
46526
+ let head2 = "";
46482
46527
  child.stdout?.on("data", (c) => {
46483
46528
  stdoutBytes += c.length;
46529
+ if (head2.length < 2048) head2 += c.toString("utf8").slice(0, 2048 - head2.length);
46484
46530
  process.stdout.write(c);
46485
46531
  });
46486
46532
  child.stderr?.on("data", (c) => process.stderr.write(c));
@@ -46492,11 +46538,11 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46492
46538
  child.on("error", (e) => {
46493
46539
  clearTimeout(timer);
46494
46540
  log(` harness failed to start: ${e.message}`);
46495
- resolve2({ code: 127, stdoutBytes });
46541
+ resolve2({ code: 127, stdoutBytes, refusal: refusalIn(head2) });
46496
46542
  });
46497
46543
  child.on("close", (code) => {
46498
46544
  clearTimeout(timer);
46499
- resolve2({ code: code ?? 1, stdoutBytes });
46545
+ resolve2({ code: code ?? 1, stdoutBytes, refusal: refusalIn(head2) });
46500
46546
  });
46501
46547
  });
46502
46548
  } finally {
@@ -46645,9 +46691,17 @@ async function tick(ctx, identity, deadTicks) {
46645
46691
  const code = run.code;
46646
46692
  if (neverRan(run)) {
46647
46693
  failedToRun += 1;
46648
- log(
46649
- ` ${hit.ref} the harness exited ${code} without reaching a model \u2014 not counted against this issue`
46650
- );
46694
+ if (run.refusal) {
46695
+ if (run.refusal !== lastRefusal) {
46696
+ log(`! the harness is refusing to work: ${run.refusal}`);
46697
+ log(` No card is charged for this, and polling backs off until it changes.`);
46698
+ lastRefusal = run.refusal;
46699
+ }
46700
+ } else {
46701
+ log(
46702
+ ` ${hit.ref} the harness exited ${code} without reaching a model \u2014 not counted against this issue`
46703
+ );
46704
+ }
46651
46705
  continue;
46652
46706
  }
46653
46707
  const { data: after } = await ctx.client.from("issues").select("status_id, assignee_id").eq("id", hit.issue.id).maybeSingle();
@@ -46684,6 +46738,7 @@ async function tick(ctx, identity, deadTicks) {
46684
46738
  }
46685
46739
  }
46686
46740
  }
46741
+ if (failedToRun < invoked) lastRefusal = null;
46687
46742
  const allDead = invoked > 0 && failedToRun === invoked;
46688
46743
  if (allDead) {
46689
46744
  log(`! nothing reached a model this tick (${invoked}/${invoked} failed to run).`);
@@ -46722,20 +46777,20 @@ async function main() {
46722
46777
  const base = await resolveBase(options.repo, options.base);
46723
46778
  worktreeConfig = {
46724
46779
  repo: options.repo,
46725
- root: options.worktreeRoot,
46780
+ root: resolveWorktreeRoot(identity),
46726
46781
  base,
46727
46782
  prepare: options.prepare,
46728
46783
  prepareTimeoutMs: options.prepareTimeout,
46729
46784
  keepFailed: options.keepFailed
46730
46785
  };
46731
- log(`worktrees in ${options.worktreeRoot}, branching from ${base} in ${options.repo}`);
46786
+ log(`worktrees in ${worktreeConfig.root}, branching from ${base} in ${options.repo}`);
46732
46787
  if (base === "HEAD") {
46733
46788
  log(`! no origin/main or main \u2014 branching from HEAD, so tasks inherit your working state`);
46734
46789
  }
46735
46790
  if (!options.prepare) {
46736
46791
  log(` no --prepare: a fresh worktree has no dependencies and no built packages`);
46737
46792
  }
46738
- const swept = await sweep(worktreeConfig);
46793
+ const swept = await sweep(worktreeConfig, options.harnessTimeoutMs + options.prepareTimeout);
46739
46794
  if (swept.length) log(` swept ${swept.length} worktree(s) left by an earlier run: ${swept.join(", ")}`);
46740
46795
  if (options.push) log(` publishing to ${options.pushRemote} after any run that commits`);
46741
46796
  if (options.allow === "mcp__cruo") {
@@ -46770,7 +46825,7 @@ async function main() {
46770
46825
  await new Promise((r) => setTimeout(r, wait));
46771
46826
  }
46772
46827
  }
46773
- var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
46828
+ var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
46774
46829
  var init_supervisor = __esm({
46775
46830
  "src/supervisor.ts"() {
46776
46831
  "use strict";
@@ -46779,6 +46834,7 @@ var init_supervisor = __esm({
46779
46834
  init_env2();
46780
46835
  init_auth();
46781
46836
  init_options();
46837
+ init_harness_signal();
46782
46838
  init_worktree();
46783
46839
  argv = process.argv.slice(2);
46784
46840
  flag = (name) => flagIn(argv, name);
@@ -46845,7 +46901,13 @@ var init_supervisor = __esm({
46845
46901
  * legitimate setup, and so is a PM agent with no filesystem at all.
46846
46902
  */
46847
46903
  worktree: flag("worktree"),
46848
- worktreeRoot: opt("worktree-root", join2(homedir(), ".cruo-work")),
46904
+ /**
46905
+ * Where worktrees are created. Defaults to `~/.cruo-work/<agent>` — per
46906
+ * AGENT, not one shared directory — which needs this agent's name and so is
46907
+ * resolved in `main` rather than here. Two supervisors sharing a root is a
46908
+ * setup someone has to ask for; see `resolveWorktreeRoot`.
46909
+ */
46910
+ worktreeRoot: opt("worktree-root"),
46849
46911
  /** The repo to branch from. Defaults to wherever the supervisor was started. */
46850
46912
  repo: opt("repo", process.cwd()),
46851
46913
  /** Ref new task branches are cut from. Resolved at startup; see `resolveBase`. */
@@ -46890,6 +46952,7 @@ cruo-supervisor: ${error51.message}
46890
46952
  })();
46891
46953
  log = (...parts) => console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}]`, ...parts);
46892
46954
  worktreeConfig = null;
46955
+ lastRefusal = null;
46893
46956
  PRIORITY_RANK = {
46894
46957
  urgent: 0,
46895
46958
  high: 1,
@@ -46919,6 +46982,7 @@ cruo-supervisor: ${error51.message}
46919
46982
  });
46920
46983
 
46921
46984
  // src/cli.ts
46985
+ init_options();
46922
46986
  import { chmod, mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
46923
46987
  import { homedir as homedir2 } from "node:os";
46924
46988
  import { join as join3 } from "node:path";
@@ -47014,7 +47078,7 @@ Get one from Cruo \u2192 Settings \u2192 Members \u2192 Add an agent.
47014
47078
  process.exit(2);
47015
47079
  }
47016
47080
  process.env.CRUO_TOKEN = token;
47017
- const passthrough = argv2.filter((a, i) => i !== flagIndex && i !== flagIndex + 1);
47081
+ const passthrough = stripTokenFlag(argv2);
47018
47082
  process.argv = [process.argv[0], process.argv[1], ...passthrough];
47019
47083
  await Promise.resolve().then(() => (init_supervisor(), supervisor_exports));
47020
47084
  }
package/dist/index.js CHANGED
@@ -34986,6 +34986,9 @@ var signupSchema = external_exports.object({
34986
34986
  });
34987
34987
 
34988
34988
  // ../../packages/core/dist/mcp-connect.js
34989
+ var DOCS_URL = "https://cruo.space/docs";
34990
+ var AGENT_DOCS_URL = `${DOCS_URL}#agents`;
34991
+ var MCP_DOCS_URL = `${DOCS_URL}#mcp`;
34989
34992
  var CRUO_CLOUD = {
34990
34993
  apiUrl: "https://szsutuujxbldkykcvdrm.supabase.co",
34991
34994
  publishableKey: "sb_publishable_hpnQFQ8IV0JXTch51XAstA_-S8H1eaD",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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/agents",
@@ -29,7 +29,8 @@
29
29
  "access": "public"
30
30
  },
31
31
  "scripts": {
32
- "build": "node scripts/build.mjs"
32
+ "build": "node scripts/build.mjs",
33
+ "prepublishOnly": "node scripts/build.mjs"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@cruo/mcp": "workspace:*"