cruo-agent 0.1.3 → 0.1.4

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 +675 -163
  3. package/package.json +1 -1
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.1.4
package/dist/cli.js CHANGED
@@ -45,64 +45,6 @@ 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
-
106
48
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
107
49
  // @__NO_SIDE_EFFECTS__
108
50
  function $constructor(name, initializer3, params) {
@@ -14032,17 +13974,17 @@ var init_schemas2 = __esm({
14032
13974
  inst._zod.processJSONSchema = (ctx, json2, params) => arrayProcessor(inst, ctx, json2, params);
14033
13975
  inst.element = def.element;
14034
13976
  _installLazyMethods(inst, "ZodArray", {
14035
- min(n, params) {
14036
- return this.check(_minLength(n, params));
13977
+ min(n2, params) {
13978
+ return this.check(_minLength(n2, params));
14037
13979
  },
14038
13980
  nonempty(params) {
14039
13981
  return this.check(_minLength(1, params));
14040
13982
  },
14041
- max(n, params) {
14042
- return this.check(_maxLength(n, params));
13983
+ max(n2, params) {
13984
+ return this.check(_maxLength(n2, params));
14043
13985
  },
14044
- length(n, params) {
14045
- return this.check(_length(n, params));
13986
+ length(n2, params) {
13987
+ return this.check(_length(n2, params));
14046
13988
  },
14047
13989
  unwrap() {
14048
13990
  return this.element;
@@ -15360,8 +15302,8 @@ var init_dist = __esm({
15360
15302
 
15361
15303
  // ../../packages/mcp-kit/dist/env.js
15362
15304
  function pick2(...names) {
15363
- for (const n of names) {
15364
- const v = process.env[n];
15305
+ for (const n2 of names) {
15306
+ const v = process.env[n2];
15365
15307
  if (v && v.trim())
15366
15308
  return v.trim();
15367
15309
  }
@@ -15581,9 +15523,9 @@ function __generator(thisArg, body) {
15581
15523
  return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
15582
15524
  return this;
15583
15525
  }), g;
15584
- function verb(n) {
15526
+ function verb(n2) {
15585
15527
  return function(v) {
15586
- return step([n, v]);
15528
+ return step([n2, v]);
15587
15529
  };
15588
15530
  }
15589
15531
  function step(op) {
@@ -15656,12 +15598,12 @@ function __values(o) {
15656
15598
  };
15657
15599
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
15658
15600
  }
15659
- function __read(o, n) {
15601
+ function __read(o, n2) {
15660
15602
  var m = typeof Symbol === "function" && o[Symbol.iterator];
15661
15603
  if (!m) return o;
15662
15604
  var i = m.call(o), r, ar = [], e;
15663
15605
  try {
15664
- while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
15606
+ while ((n2 === void 0 || n2-- > 0) && !(r = i.next()).done) ar.push(r.value);
15665
15607
  } catch (error51) {
15666
15608
  e = { error: error51 };
15667
15609
  } finally {
@@ -15708,19 +15650,19 @@ function __asyncGenerator(thisArg, _arguments, generator) {
15708
15650
  return Promise.resolve(v).then(f, reject);
15709
15651
  };
15710
15652
  }
15711
- function verb(n, f) {
15712
- if (g[n]) {
15713
- i[n] = function(v) {
15653
+ function verb(n2, f) {
15654
+ if (g[n2]) {
15655
+ i[n2] = function(v) {
15714
15656
  return new Promise(function(a, b) {
15715
- q.push([n, v, a, b]) > 1 || resume(n, v);
15657
+ q.push([n2, v, a, b]) > 1 || resume(n2, v);
15716
15658
  });
15717
15659
  };
15718
- if (f) i[n] = f(i[n]);
15660
+ if (f) i[n2] = f(i[n2]);
15719
15661
  }
15720
15662
  }
15721
- function resume(n, v) {
15663
+ function resume(n2, v) {
15722
15664
  try {
15723
- step(g[n](v));
15665
+ step(g[n2](v));
15724
15666
  } catch (e) {
15725
15667
  settle(q[0][3], e);
15726
15668
  }
@@ -15745,9 +15687,9 @@ function __asyncDelegator(o) {
15745
15687
  }), verb("return"), i[Symbol.iterator] = function() {
15746
15688
  return this;
15747
15689
  }, i;
15748
- function verb(n, f) {
15749
- i[n] = o[n] ? function(v) {
15750
- return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v;
15690
+ function verb(n2, f) {
15691
+ i[n2] = o[n2] ? function(v) {
15692
+ return (p = !p) ? { value: __await(o[n2](v)), done: false } : f ? f(v) : v;
15751
15693
  } : f;
15752
15694
  }
15753
15695
  }
@@ -15757,10 +15699,10 @@ function __asyncValues(o) {
15757
15699
  return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() {
15758
15700
  return this;
15759
15701
  }, i);
15760
- function verb(n) {
15761
- i[n] = o[n] && function(v) {
15702
+ function verb(n2) {
15703
+ i[n2] = o[n2] && function(v) {
15762
15704
  return new Promise(function(resolve2, reject) {
15763
- v = o[n](v), settle(resolve2, reject, v.done, v.value);
15705
+ v = o[n2](v), settle(resolve2, reject, v.done, v.value);
15764
15706
  });
15765
15707
  };
15766
15708
  }
@@ -15879,7 +15821,7 @@ var init_tslib_es6 = __esm({
15879
15821
  };
15880
15822
  __assign = function() {
15881
15823
  __assign = Object.assign || function __assign2(t) {
15882
- for (var s, i = 1, n = arguments.length; i < n; i++) {
15824
+ for (var s, i = 1, n2 = arguments.length; i < n2; i++) {
15883
15825
  s = arguments[i];
15884
15826
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
15885
15827
  }
@@ -31064,8 +31006,8 @@ var require_GoTrueClient = __commonJS({
31064
31006
  const result = await this.initializePromise;
31065
31007
  const queue = (_a3 = this._pendingInitNotifications) !== null && _a3 !== void 0 ? _a3 : [];
31066
31008
  this._pendingInitNotifications = null;
31067
- for (const n of queue) {
31068
- await this._notifyAllSubscribers(n.event, n.session, n.broadcast);
31009
+ for (const n2 of queue) {
31010
+ await this._notifyAllSubscribers(n2.event, n2.session, n2.broadcast);
31069
31011
  }
31070
31012
  return result;
31071
31013
  }
@@ -39415,17 +39357,17 @@ var require_codegen = __commonJS({
39415
39357
  this.nodes = nodes;
39416
39358
  }
39417
39359
  render(opts) {
39418
- return this.nodes.reduce((code, n) => code + n.render(opts), "");
39360
+ return this.nodes.reduce((code, n2) => code + n2.render(opts), "");
39419
39361
  }
39420
39362
  optimizeNodes() {
39421
39363
  const { nodes } = this;
39422
39364
  let i = nodes.length;
39423
39365
  while (i--) {
39424
- const n = nodes[i].optimizeNodes();
39425
- if (Array.isArray(n))
39426
- nodes.splice(i, 1, ...n);
39427
- else if (n)
39428
- nodes[i] = n;
39366
+ const n2 = nodes[i].optimizeNodes();
39367
+ if (Array.isArray(n2))
39368
+ nodes.splice(i, 1, ...n2);
39369
+ else if (n2)
39370
+ nodes[i] = n2;
39429
39371
  else
39430
39372
  nodes.splice(i, 1);
39431
39373
  }
@@ -39435,16 +39377,16 @@ var require_codegen = __commonJS({
39435
39377
  const { nodes } = this;
39436
39378
  let i = nodes.length;
39437
39379
  while (i--) {
39438
- const n = nodes[i];
39439
- if (n.optimizeNames(names, constants))
39380
+ const n2 = nodes[i];
39381
+ if (n2.optimizeNames(names, constants))
39440
39382
  continue;
39441
- subtractNames(names, n.names);
39383
+ subtractNames(names, n2.names);
39442
39384
  nodes.splice(i, 1);
39443
39385
  }
39444
39386
  return nodes.length > 0 ? this : void 0;
39445
39387
  }
39446
39388
  get names() {
39447
- return this.nodes.reduce((names, n) => addNames(names, n.names), {});
39389
+ return this.nodes.reduce((names, n2) => addNames(names, n2.names), {});
39448
39390
  }
39449
39391
  };
39450
39392
  var BlockNode = class extends ParentNode {
@@ -39855,8 +39797,8 @@ var require_codegen = __commonJS({
39855
39797
  endFunc() {
39856
39798
  return this._endBlockNode(Func);
39857
39799
  }
39858
- optimize(n = 1) {
39859
- while (n-- > 0) {
39800
+ optimize(n2 = 1) {
39801
+ while (n2-- > 0) {
39860
39802
  this._root.optimizeNodes();
39861
39803
  this._root.optimizeNames(this._root.names, this._constants);
39862
39804
  }
@@ -39870,19 +39812,19 @@ var require_codegen = __commonJS({
39870
39812
  this._nodes.push(node);
39871
39813
  }
39872
39814
  _endBlockNode(N1, N2) {
39873
- const n = this._currNode;
39874
- if (n instanceof N1 || N2 && n instanceof N2) {
39815
+ const n2 = this._currNode;
39816
+ if (n2 instanceof N1 || N2 && n2 instanceof N2) {
39875
39817
  this._nodes.pop();
39876
39818
  return this;
39877
39819
  }
39878
39820
  throw new Error(`CodeGen: not in block "${N2 ? `${N1.kind}/${N2.kind}` : N1.kind}"`);
39879
39821
  }
39880
39822
  _elseNode(node) {
39881
- const n = this._currNode;
39882
- if (!(n instanceof If)) {
39823
+ const n2 = this._currNode;
39824
+ if (!(n2 instanceof If)) {
39883
39825
  throw new Error('CodeGen: "else" without "if"');
39884
39826
  }
39885
- this._currNode = n.else = node;
39827
+ this._currNode = n2.else = node;
39886
39828
  return this;
39887
39829
  }
39888
39830
  get _root() {
@@ -39899,8 +39841,8 @@ var require_codegen = __commonJS({
39899
39841
  };
39900
39842
  exports.CodeGen = CodeGen;
39901
39843
  function addNames(names, from) {
39902
- for (const n in from)
39903
- names[n] = (names[n] || 0) + (from[n] || 0);
39844
+ for (const n2 in from)
39845
+ names[n2] = (names[n2] || 0) + (from[n2] || 0);
39904
39846
  return names;
39905
39847
  }
39906
39848
  function addExprNames(names, from) {
@@ -39920,11 +39862,11 @@ var require_codegen = __commonJS({
39920
39862
  items.push(c);
39921
39863
  return items;
39922
39864
  }, []));
39923
- function replaceName(n) {
39924
- const c = constants[n.str];
39925
- if (c === void 0 || names[n.str] !== 1)
39926
- return n;
39927
- delete names[n.str];
39865
+ function replaceName(n2) {
39866
+ const c = constants[n2.str];
39867
+ if (c === void 0 || names[n2.str] !== 1)
39868
+ return n2;
39869
+ delete names[n2.str];
39928
39870
  return c;
39929
39871
  }
39930
39872
  function canOptimize(e) {
@@ -39932,8 +39874,8 @@ var require_codegen = __commonJS({
39932
39874
  }
39933
39875
  }
39934
39876
  function subtractNames(names, from) {
39935
- for (const n in from)
39936
- names[n] = (names[n] || 0) - (from[n] || 0);
39877
+ for (const n2 in from)
39878
+ names[n2] = (names[n2] || 0) - (from[n2] || 0);
39937
39879
  }
39938
39880
  function not(x) {
39939
39881
  return typeof x == "boolean" || typeof x == "number" || x === null ? !x : (0, code_1._)`!${par(x)}`;
@@ -45949,6 +45891,64 @@ var init_dist7 = __esm({
45949
45891
  }
45950
45892
  });
45951
45893
 
45894
+ // src/options.ts
45895
+ function stripTokenFlag(argv2) {
45896
+ const at = argv2.indexOf("--token");
45897
+ if (at < 0) return [...argv2];
45898
+ return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
45899
+ }
45900
+ function numericIn(argv2, name, spec = {}) {
45901
+ const i = argv2.indexOf(`--${name}`);
45902
+ if (i < 0) return spec.fallback;
45903
+ const raw = argv2[i + 1];
45904
+ if (raw === void 0 || raw.startsWith("--")) {
45905
+ throw new OptionError(
45906
+ `--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
45907
+ );
45908
+ }
45909
+ const value = raw.trim() === "" ? NaN : Number(raw);
45910
+ const what = `--${name} ${JSON.stringify(raw)}`;
45911
+ if (!Number.isFinite(value)) {
45912
+ throw new OptionError(
45913
+ `${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
45914
+ // negative and falls to the range check below, which is the accurate
45915
+ // refusal for it — this hint is for the case where the leading dash is
45916
+ // the reason it did not parse at all.
45917
+ (raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
45918
+ );
45919
+ }
45920
+ if (value <= 0) {
45921
+ throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
45922
+ }
45923
+ if (spec.integer && !Number.isInteger(value)) {
45924
+ throw new OptionError(`${what} must be a whole number.`);
45925
+ }
45926
+ return value;
45927
+ }
45928
+ function requiredNumericIn(argv2, name, fallback, spec = {}) {
45929
+ return numericIn(argv2, name, { ...spec, fallback });
45930
+ }
45931
+ function secondsIn(argv2, name, fallbackSeconds) {
45932
+ return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
45933
+ }
45934
+ var OptionError, flagIn, optIn;
45935
+ var init_options = __esm({
45936
+ "src/options.ts"() {
45937
+ "use strict";
45938
+ OptionError = class extends Error {
45939
+ constructor(message) {
45940
+ super(message);
45941
+ this.name = "OptionError";
45942
+ }
45943
+ };
45944
+ flagIn = (argv2, name) => argv2.includes(`--${name}`);
45945
+ optIn = (argv2, name, fallback) => {
45946
+ const i = argv2.indexOf(`--${name}`);
45947
+ return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
45948
+ };
45949
+ }
45950
+ });
45951
+
45952
45952
  // src/env.ts
45953
45953
  function applyCloudDefaults() {
45954
45954
  const url3 = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -46001,6 +46001,21 @@ function childSession() {
46001
46001
  const endpoint = sessionEndpoint();
46002
46002
  return endpoint ? fetchDelegatedSession(endpoint, token) : mintDelegatedSession({ config: config2, token, product: "projects", mintedAt: MINTED_AT });
46003
46003
  }
46004
+ async function contextOrExplain() {
46005
+ try {
46006
+ return await getContext();
46007
+ } catch (error51) {
46008
+ const label = process.env.CRUO_IDENTITY?.trim();
46009
+ if (!label) throw error51;
46010
+ throw new Error(
46011
+ `${error51 instanceof Error ? error51.message : String(error51)}
46012
+
46013
+ That is the token stored for \`${label}\`.
46014
+ Replace it: Cruo \u2192 Settings \u2192 Members \u2192 ${label} \u2192 new token,
46015
+ then: cruo login <token> --as ${label}`
46016
+ );
46017
+ }
46018
+ }
46004
46019
  var stdioResolver;
46005
46020
  var init_auth = __esm({
46006
46021
  "src/auth.ts"() {
@@ -46012,6 +46027,176 @@ var init_auth = __esm({
46012
46027
  }
46013
46028
  });
46014
46029
 
46030
+ // src/usage.ts
46031
+ var usage_exports = {};
46032
+ __export(usage_exports, {
46033
+ reportUsage: () => reportUsage
46034
+ });
46035
+ function compact(value) {
46036
+ if (value === 0) return "\u2014";
46037
+ if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
46038
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
46039
+ return String(value);
46040
+ }
46041
+ async function reportUsage(opts) {
46042
+ const ctx = await contextOrExplain();
46043
+ const since = new Date(Date.now() - opts.days * 864e5).toISOString();
46044
+ let query = ctx.client.from("harness_runs").select("*").gte("started_at", since).order("started_at", { ascending: false });
46045
+ let issueTitle = null;
46046
+ if (opts.issue) {
46047
+ const [key, num3] = opts.issue.split("-");
46048
+ const { data: project } = await ctx.client.from("projects").select("id").eq("key", (key ?? "").toUpperCase()).maybeSingle();
46049
+ const { data: issue2 } = project ? await ctx.client.from("issues").select("id, title").eq("project_id", project.id).eq("number", Number(num3)).maybeSingle() : { data: null };
46050
+ if (!issue2) {
46051
+ console.error(`
46052
+ No issue ${opts.issue} on this board.
46053
+ `);
46054
+ process.exit(2);
46055
+ }
46056
+ issueTitle = issue2.title;
46057
+ query = query.eq("issue_id", issue2.id);
46058
+ }
46059
+ const { data, error: error51 } = await query;
46060
+ if (error51) {
46061
+ console.error(`
46062
+ Could not read run costs: ${error51.message}
46063
+ `);
46064
+ process.exit(1);
46065
+ }
46066
+ const rows = data ?? [];
46067
+ console.log(
46068
+ `
46069
+ ${opts.issue ? `${opts.issue} \u2014 ${issueTitle}` : ctx.workspaceName} \xB7 last ${opts.days} day${opts.days === 1 ? "" : "s"}`
46070
+ );
46071
+ if (rows.length === 0) {
46072
+ console.log(
46073
+ `
46074
+ No runs recorded. Either no agent has run in that window, or the runs
46075
+ predate cost recording (\`CRA-89\`, 2026-08-29).
46076
+ `
46077
+ );
46078
+ return;
46079
+ }
46080
+ const ids = [...new Set(rows.map((r) => r.agent_id))];
46081
+ const { data: people } = await ctx.client.schema("public").from("profiles").select("id, display_name, email").in("id", ids);
46082
+ const nameOf = (id) => {
46083
+ const p = (people ?? []).find((x) => x.id === id);
46084
+ return p?.display_name || p?.email || id.slice(0, 8);
46085
+ };
46086
+ const blank = () => ({
46087
+ runs: 0,
46088
+ productive: 0,
46089
+ neverRan: 0,
46090
+ ms: 0,
46091
+ bytes: 0,
46092
+ input: 0,
46093
+ output: 0,
46094
+ cacheRead: 0,
46095
+ cacheWrite: 0,
46096
+ cost: 0,
46097
+ measured: 0
46098
+ });
46099
+ const fold = (a, r) => {
46100
+ a.runs += 1;
46101
+ if (r.outcome === "productive") a.productive += 1;
46102
+ if (r.outcome === "never-ran") a.neverRan += 1;
46103
+ a.ms += r.duration_ms;
46104
+ a.bytes += n(r.stdout_bytes);
46105
+ a.input += n(r.input_tokens);
46106
+ a.output += n(r.output_tokens);
46107
+ a.cacheRead += n(r.cache_read_tokens);
46108
+ a.cacheWrite += n(r.cache_write_tokens);
46109
+ a.cost += n(r.cost_usd);
46110
+ if (r.input_tokens !== null || r.cost_usd !== null) a.measured += 1;
46111
+ };
46112
+ const byAgent = /* @__PURE__ */ new Map();
46113
+ const total = blank();
46114
+ for (const r of rows) {
46115
+ if (!byAgent.has(r.agent_id)) byAgent.set(r.agent_id, blank());
46116
+ fold(byAgent.get(r.agent_id), r);
46117
+ fold(total, r);
46118
+ }
46119
+ const w = Math.max(12, ...[...byAgent.keys()].map((id) => nameOf(id).length));
46120
+ console.log(
46121
+ `
46122
+ ${pad("agent", w)} ${rpad("runs", 5)} ${rpad("useful", 7)} ${rpad("dead", 5)} ${rpad("time", 7)} ${rpad("in", 8)} ${rpad("out", 8)} ${rpad("cost", 9)}`
46123
+ );
46124
+ for (const [id, a] of [...byAgent].sort((x, y) => y[1].runs - x[1].runs)) {
46125
+ console.log(
46126
+ ` ${pad(nameOf(id), w)} ${rpad(String(a.runs), 5)} ${rpad(String(a.productive), 7)} ${rpad(String(a.neverRan), 5)} ${rpad(duration3(a.ms), 7)} ${rpad(compact(a.input + a.cacheRead), 8)} ${rpad(compact(a.output), 8)} ${rpad(a.cost > 0 ? `$${a.cost.toFixed(2)}` : "\u2014", 9)}`
46127
+ );
46128
+ }
46129
+ if (!opts.issue) {
46130
+ const byIssue = /* @__PURE__ */ new Map();
46131
+ for (const r of rows) {
46132
+ if (!r.issue_id) continue;
46133
+ if (!byIssue.has(r.issue_id)) byIssue.set(r.issue_id, blank());
46134
+ fold(byIssue.get(r.issue_id), r);
46135
+ }
46136
+ const worst = [...byIssue].sort((x, y) => y[1].runs - x[1].runs).slice(0, 8);
46137
+ if (worst.length > 0) {
46138
+ const { data: issues } = await ctx.client.from("issues").select("id, number, title, project_id").in("id", worst.map(([id]) => id));
46139
+ const { data: projects } = await ctx.client.from("projects").select("id, key");
46140
+ const refOf = (id) => {
46141
+ const i = (issues ?? []).find((x) => x.id === id);
46142
+ if (!i) return id.slice(0, 8);
46143
+ const key = (projects ?? []).find((p) => p.id === i.project_id)?.key ?? "?";
46144
+ return `${key}-${i.number}`;
46145
+ };
46146
+ const titleOf = (id) => (issues ?? []).find((x) => x.id === id)?.title ?? "";
46147
+ console.log(`
46148
+ most-worked cards`);
46149
+ for (const [id, a] of worst) {
46150
+ console.log(
46151
+ ` ${rpad(String(a.runs), 3)} run${a.runs === 1 ? " " : "s"} ${pad(refOf(id), 8)} ${rpad(duration3(a.ms), 6)} ${titleOf(id).slice(0, 52)}`
46152
+ );
46153
+ }
46154
+ }
46155
+ } else {
46156
+ console.log(`
46157
+ run by run`);
46158
+ for (const r of rows) {
46159
+ console.log(
46160
+ ` ${r.started_at.slice(0, 16).replace("T", " ")} ${pad(r.outcome, 13)} ${rpad(duration3(r.duration_ms), 6)} ${rpad(compact(n(r.output_tokens)), 7)} out ${nameOf(r.agent_id)}`
46161
+ );
46162
+ }
46163
+ }
46164
+ console.log(
46165
+ `
46166
+ ${total.runs} run(s), ${total.productive} productive, ${total.neverRan} never reached a model.
46167
+ ${duration3(total.ms)} of wall clock, ${compact(total.bytes)} bytes of output.`
46168
+ );
46169
+ if (total.measured === 0) {
46170
+ console.log(
46171
+ `
46172
+ No token or cost figures: this harness does not report them. Runs, time and
46173
+ output are measured for any harness; tokens need one that says what it spent \u2014
46174
+ e.g. --harness "claude -p --output-format json".
46175
+ `
46176
+ );
46177
+ } else if (total.measured < total.runs) {
46178
+ console.log(
46179
+ `
46180
+ ${total.measured} of ${total.runs} runs reported tokens; the rest did not, so the
46181
+ figures above are a floor rather than a total.
46182
+ `
46183
+ );
46184
+ } else {
46185
+ console.log("");
46186
+ }
46187
+ }
46188
+ var n, pad, rpad, duration3;
46189
+ var init_usage = __esm({
46190
+ "src/usage.ts"() {
46191
+ "use strict";
46192
+ init_auth();
46193
+ n = (v) => v === null || v === void 0 ? 0 : Number(v);
46194
+ pad = (s, w) => s.padEnd(w);
46195
+ rpad = (s, w) => s.padStart(w);
46196
+ duration3 = (ms2) => ms2 >= 36e5 ? `${(ms2 / 36e5).toFixed(1)}h` : ms2 >= 6e4 ? `${(ms2 / 6e4).toFixed(1)}m` : `${Math.round(ms2 / 1e3)}s`;
46197
+ }
46198
+ });
46199
+
46015
46200
  // src/harness-signal.ts
46016
46201
  function refusalIn(stdout) {
46017
46202
  if (!stdout.trim()) return null;
@@ -46023,7 +46208,36 @@ function refusalIn(stdout) {
46023
46208
  function neverRan(run) {
46024
46209
  return run.refusal !== null || run.stdoutBytes === 0;
46025
46210
  }
46026
- var REFUSALS;
46211
+ function usageIn(text) {
46212
+ const trimmed = text.trimEnd();
46213
+ if (!trimmed.endsWith("}")) return null;
46214
+ let parsed = null;
46215
+ for (let i = trimmed.length - 1; i >= 0; i -= 1) {
46216
+ if (trimmed[i] !== "{") continue;
46217
+ try {
46218
+ parsed = JSON.parse(trimmed.slice(i));
46219
+ break;
46220
+ } catch {
46221
+ }
46222
+ }
46223
+ if (!parsed || typeof parsed !== "object") return null;
46224
+ const root = parsed;
46225
+ const usage = root.usage ?? root;
46226
+ const out = {
46227
+ model: typeof root.model === "string" ? root.model : void 0,
46228
+ // Both spellings. The snake_case ones are the API's; a harness that passes
46229
+ // its response through unchanged emits those, and one that tidies them
46230
+ // emits camelCase.
46231
+ inputTokens: num(usage.input_tokens) ?? num(usage.inputTokens),
46232
+ outputTokens: num(usage.output_tokens) ?? num(usage.outputTokens),
46233
+ cacheReadTokens: num(usage.cache_read_input_tokens) ?? num(usage.cacheReadInputTokens),
46234
+ cacheWriteTokens: num(usage.cache_creation_input_tokens) ?? num(usage.cacheCreationInputTokens),
46235
+ costUsd: num(root.total_cost_usd) ?? num(root.totalCostUsd) ?? num(root.cost_usd)
46236
+ };
46237
+ const measured = Object.values(out).some((v) => v !== void 0);
46238
+ return measured ? out : null;
46239
+ }
46240
+ var REFUSALS, num;
46027
46241
  var init_harness_signal = __esm({
46028
46242
  "src/harness-signal.ts"() {
46029
46243
  "use strict";
@@ -46034,6 +46248,47 @@ var init_harness_signal = __esm({
46034
46248
  [/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
46035
46249
  [/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
46036
46250
  ];
46251
+ num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
46252
+ }
46253
+ });
46254
+
46255
+ // src/pacing.ts
46256
+ function afterAttempt(p, attempt, limits, now) {
46257
+ const allDead = attempt.invoked > 0 && attempt.failedToRun === attempt.invoked;
46258
+ const deadTicks = allDead ? p.deadTicks + 1 : 0;
46259
+ if (deadTicks < 2) {
46260
+ return {
46261
+ next: { deadTicks, invokeAfter: 0, backedOff: false },
46262
+ event: p.backedOff ? "recovered" : null,
46263
+ cooldownMs: 0
46264
+ };
46265
+ }
46266
+ const cooldownMs = Math.min(
46267
+ limits.intervalMs * 2 ** (deadTicks - 1),
46268
+ limits.maxIntervalMs
46269
+ );
46270
+ return {
46271
+ next: { deadTicks, invokeAfter: now + cooldownMs, backedOff: true },
46272
+ // Said once per descent, not once per tick. The ceiling is reached in a few
46273
+ // ticks and held for hours; repeating the line there would bury the
46274
+ // `recovered` that matters.
46275
+ event: p.backedOff ? null : "backing-off",
46276
+ cooldownMs
46277
+ };
46278
+ }
46279
+ var freshPacing, allowanceAt;
46280
+ var init_pacing = __esm({
46281
+ "src/pacing.ts"() {
46282
+ "use strict";
46283
+ freshPacing = () => ({
46284
+ deadTicks: 0,
46285
+ invokeAfter: 0,
46286
+ backedOff: false
46287
+ });
46288
+ allowanceAt = (p, now) => {
46289
+ if (!p.backedOff) return "free";
46290
+ return now >= p.invokeAfter ? "probe" : "held";
46291
+ };
46037
46292
  }
46038
46293
  });
46039
46294
 
@@ -46264,10 +46519,10 @@ async function pollMentions(ctx, identity) {
46264
46519
  if (error51) throw new Error(`Mention poll failed: ${error51.message}`);
46265
46520
  if (!notes || notes.length === 0) return [];
46266
46521
  const byIssue = /* @__PURE__ */ new Map();
46267
- for (const n of notes) {
46268
- const list = byIssue.get(n.issue_id) ?? [];
46269
- list.push(n.id);
46270
- byIssue.set(n.issue_id, list);
46522
+ for (const n2 of notes) {
46523
+ const list = byIssue.get(n2.issue_id) ?? [];
46524
+ list.push(n2.id);
46525
+ byIssue.set(n2.issue_id, list);
46271
46526
  }
46272
46527
  const { data: issues, error: issueErr } = await ctx.client.from("issues").select("*").in("id", [...byIssue.keys()]);
46273
46528
  if (issueErr) throw new Error(`Mention issue lookup failed: ${issueErr.message}`);
@@ -46551,11 +46806,14 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46551
46806
  cwd,
46552
46807
  env: harnessEnv()
46553
46808
  });
46809
+ const startedAtMs = Date.now();
46554
46810
  let stdoutBytes = 0;
46811
+ let tail = "";
46555
46812
  let head2 = "";
46556
46813
  child.stdout?.on("data", (c) => {
46557
46814
  stdoutBytes += c.length;
46558
46815
  if (head2.length < 2048) head2 += c.toString("utf8").slice(0, 2048 - head2.length);
46816
+ tail = (tail + c.toString("utf8")).slice(-4096);
46559
46817
  process.stdout.write(c);
46560
46818
  });
46561
46819
  child.stderr?.on("data", (c) => process.stderr.write(c));
@@ -46567,11 +46825,23 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46567
46825
  child.on("error", (e) => {
46568
46826
  clearTimeout(timer);
46569
46827
  log(` harness failed to start: ${e.message}`);
46570
- resolve2({ code: 127, stdoutBytes, refusal: refusalIn(head2) });
46828
+ resolve2({
46829
+ code: 127,
46830
+ stdoutBytes,
46831
+ refusal: refusalIn(head2),
46832
+ durationMs: Date.now() - startedAtMs,
46833
+ usage: null
46834
+ });
46571
46835
  });
46572
46836
  child.on("close", (code) => {
46573
46837
  clearTimeout(timer);
46574
- resolve2({ code: code ?? 1, stdoutBytes, refusal: refusalIn(head2) });
46838
+ resolve2({
46839
+ code: code ?? 1,
46840
+ stdoutBytes,
46841
+ refusal: refusalIn(head2),
46842
+ durationMs: Date.now() - startedAtMs,
46843
+ usage: usageIn(tail)
46844
+ });
46575
46845
  });
46576
46846
  });
46577
46847
  } finally {
@@ -46654,7 +46924,29 @@ async function beat(ctx, fields) {
46654
46924
  );
46655
46925
  if (error51) log(` could not record a heartbeat: ${error51.message}`);
46656
46926
  }
46657
- async function tick(ctx, identity, deadTicks) {
46927
+ async function recordRun(ctx, hit, run, outcome) {
46928
+ const { error: error51 } = await ctx.client.from("harness_runs").insert({
46929
+ agent_id: ctx.userId,
46930
+ workspace_id: ctx.workspaceId,
46931
+ issue_id: hit.issue.id,
46932
+ reason: hit.reason,
46933
+ duration_ms: run.durationMs,
46934
+ exit_code: run.code,
46935
+ stdout_bytes: run.stdoutBytes,
46936
+ outcome,
46937
+ model: run.usage?.model ?? null,
46938
+ input_tokens: run.usage?.inputTokens ?? null,
46939
+ output_tokens: run.usage?.outputTokens ?? null,
46940
+ cache_read_tokens: run.usage?.cacheReadTokens ?? null,
46941
+ cache_write_tokens: run.usage?.cacheWriteTokens ?? null,
46942
+ cost_usd: run.usage?.costUsd ?? null
46943
+ });
46944
+ if (error51 && !warnedAboutSpendTable) {
46945
+ warnedAboutSpendTable = true;
46946
+ log(` (not recording run costs: ${error51.message})`);
46947
+ }
46948
+ }
46949
+ async function tick(ctx, identity, deadTicks, allowance) {
46658
46950
  const mentions = await pollMentions(ctx, identity);
46659
46951
  const [assigned, stateWork] = await Promise.all([pollAssigned(ctx), poll(ctx, identity)]);
46660
46952
  const seen = /* @__PURE__ */ new Set();
@@ -46675,7 +46967,20 @@ async function tick(ctx, identity, deadTicks) {
46675
46967
  if (free.length > 0) log(`${free.length} issue(s) waiting, all set aside`);
46676
46968
  else if (heldByOthers.size > 0) log(`nothing free \u2014 everything waiting is claimed`);
46677
46969
  await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
46678
- return { invoked: 0, failedToRun: 0 };
46970
+ return { invoked: 0, failedToRun: 0, suppressed: false, waiting: 0 };
46971
+ }
46972
+ if (allowance === "held") {
46973
+ await beat(ctx, {
46974
+ holding: actionable.length,
46975
+ deadTicks,
46976
+ intervalMs: options.intervalMs
46977
+ });
46978
+ return {
46979
+ invoked: 0,
46980
+ failedToRun: 0,
46981
+ suppressed: true,
46982
+ waiting: actionable.length
46983
+ };
46679
46984
  }
46680
46985
  await beat(ctx, {
46681
46986
  holding: actionable.length,
@@ -46688,9 +46993,12 @@ async function tick(ctx, identity, deadTicks) {
46688
46993
  log(
46689
46994
  `${actionable.length} issue(s) for "${identity.jobFunction}"` + (mentionCount > 0 ? ` \u2014 ${mentionCount} from unread mentions` : "")
46690
46995
  );
46691
- const taking = options.limit ? actionable.slice(0, options.limit) : actionable;
46996
+ const cap = allowance === "probe" ? 1 : options.limit;
46997
+ const taking = cap ? actionable.slice(0, cap) : actionable;
46692
46998
  if (taking.length < actionable.length) {
46693
- log(` taking ${taking.length} this tick (--limit ${options.limit})`);
46999
+ log(
47000
+ allowance === "probe" ? ` probing with ${taking.length} of ${actionable.length} \u2014 one card is enough to learn whether the harness works again` : ` taking ${taking.length} this tick (--limit ${options.limit})`
47001
+ );
46694
47002
  }
46695
47003
  for (const hit of taking) {
46696
47004
  const why = hit.reason === "mention" ? "mentioned" : hit.reason === "assigned" ? `assigned \xB7 ${hit.state.name}` : hit.state.name;
@@ -46730,6 +47038,7 @@ async function tick(ctx, identity, deadTicks) {
46730
47038
  const code = run.code;
46731
47039
  if (neverRan(run)) {
46732
47040
  failedToRun += 1;
47041
+ await recordRun(ctx, hit, run, "never-ran");
46733
47042
  if (run.refusal) {
46734
47043
  if (run.refusal !== lastRefusal) {
46735
47044
  log(`! the harness is refusing to work: ${run.refusal}`);
@@ -46746,6 +47055,7 @@ async function tick(ctx, identity, deadTicks) {
46746
47055
  const { data: after } = await ctx.client.from("issues").select("status_id, assignee_id").eq("id", hit.issue.id).maybeSingle();
46747
47056
  const moved = !after || after.status_id !== before.status || after.assignee_id !== before.assignee;
46748
47057
  const productive = hit.reason === "mention" ? await repliedSince(ctx, hit, startedAt) || moved : moved || committed > 0;
47058
+ await recordRun(ctx, hit, run, productive ? "productive" : "unproductive");
46749
47059
  if (productive) {
46750
47060
  await clearAttempts(ctx, hit);
46751
47061
  if (hit.reason === "mention") {
@@ -46761,9 +47071,9 @@ async function tick(ctx, identity, deadTicks) {
46761
47071
  }
46762
47072
  } else {
46763
47073
  const what = hit.reason === "mention" ? "no reply written" : "unchanged";
46764
- const n = await recordAttempt(ctx, hit, `${what} (exit ${code})`, attemptsFor(hit));
47074
+ const n2 = await recordAttempt(ctx, hit, `${what} (exit ${code})`, attemptsFor(hit));
46765
47075
  log(
46766
- ` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n}/${options.maxAttempts}` + (n >= options.maxAttempts ? "; setting it aside, a human should look" : "")
47076
+ ` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n2}/${options.maxAttempts}` + (n2 >= options.maxAttempts ? "; setting it aside, a human should look" : "")
46767
47077
  );
46768
47078
  }
46769
47079
  if (worktree) {
@@ -46788,7 +47098,7 @@ async function tick(ctx, identity, deadTicks) {
46788
47098
  deadTicks: allDead ? deadTicks + 1 : 0,
46789
47099
  intervalMs: options.intervalMs
46790
47100
  });
46791
- return { invoked, failedToRun };
47101
+ return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
46792
47102
  }
46793
47103
  async function main() {
46794
47104
  log(
@@ -46801,7 +47111,7 @@ async function main() {
46801
47111
  if (staleConfigs > 0) {
46802
47112
  log(` cleared ${staleConfigs} temp config(s) left by a killed run`);
46803
47113
  }
46804
- const ctx = await getContext();
47114
+ const ctx = await contextOrExplain();
46805
47115
  const identity = await identify(ctx);
46806
47116
  try {
46807
47117
  await preflight();
@@ -46848,24 +47158,45 @@ async function main() {
46848
47158
  log(
46849
47159
  options.once ? "single pass" : `polling every ${options.intervalMs / 1e3}s \xB7 ctrl-c to stop`
46850
47160
  );
46851
- let deadTicks = 0;
47161
+ let pacing = freshPacing();
47162
+ let lastHeldLog = 0;
46852
47163
  for (; ; ) {
46853
47164
  try {
46854
- const result = await tick(await getContext(), identity, deadTicks);
46855
- const allDead = result.invoked > 0 && result.failedToRun === result.invoked;
46856
- deadTicks = allDead ? deadTicks + 1 : 0;
47165
+ const result = await tick(
47166
+ await getContext(),
47167
+ identity,
47168
+ pacing.deadTicks,
47169
+ allowanceAt(pacing, Date.now())
47170
+ );
47171
+ if (result.suppressed) {
47172
+ const now = Date.now();
47173
+ if (now - lastHeldLog >= 6e4) {
47174
+ lastHeldLog = now;
47175
+ const left = Math.max(0, Math.round((pacing.invokeAfter - now) / 1e3));
47176
+ log(
47177
+ `! ${result.waiting} issue(s) waiting \u2014 not invoking after ${pacing.deadTicks} dead ticks; next attempt in ${left}s`
47178
+ );
47179
+ }
47180
+ } else {
47181
+ const step = afterAttempt(pacing, result, options, Date.now());
47182
+ pacing = step.next;
47183
+ if (step.event === "backing-off") {
47184
+ lastHeldLog = Date.now();
47185
+ log(
47186
+ ` backing off \u2014 ${pacing.deadTicks} ticks with nothing reaching a model; still polling every ${options.intervalMs / 1e3}s, next invocation in ${step.cooldownMs / 1e3}s`
47187
+ );
47188
+ } else if (step.event === "recovered") {
47189
+ log(` recovered \u2014 a run reached a model; invoking normally again`);
47190
+ }
47191
+ }
46857
47192
  } catch (e) {
46858
47193
  log(`poll error: ${e instanceof Error ? e.message : String(e)}`);
46859
47194
  }
46860
47195
  if (options.once) return;
46861
- const wait = deadTicks < 2 ? options.intervalMs : Math.min(options.intervalMs * 2 ** (deadTicks - 1), options.maxIntervalMs);
46862
- if (wait !== options.intervalMs) {
46863
- log(` backing off \u2014 ${deadTicks} ticks with nothing reaching a model; next in ${wait / 1e3}s`);
46864
- }
46865
- await new Promise((r) => setTimeout(r, wait));
47196
+ await new Promise((r) => setTimeout(r, options.intervalMs));
46866
47197
  }
46867
47198
  }
46868
- var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
47199
+ var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, warnedAboutSpendTable;
46869
47200
  var init_supervisor = __esm({
46870
47201
  "src/supervisor.ts"() {
46871
47202
  "use strict";
@@ -46875,11 +47206,12 @@ var init_supervisor = __esm({
46875
47206
  init_auth();
46876
47207
  init_options();
46877
47208
  init_harness_signal();
47209
+ init_pacing();
46878
47210
  init_worktree();
46879
47211
  argv = process.argv.slice(2);
46880
47212
  flag = (name) => flagIn(argv, name);
46881
47213
  opt = (name, fallback) => optIn(argv, name, fallback);
46882
- num = (name, spec) => numericIn(argv, name, spec);
47214
+ num2 = (name, spec) => numericIn(argv, name, spec);
46883
47215
  ms = (name, fallbackSeconds) => secondsIn(argv, name, fallbackSeconds);
46884
47216
  readOptions = () => ({
46885
47217
  /** Override the function to poll for. Defaults to this agent's own. */
@@ -46895,7 +47227,7 @@ var init_supervisor = __esm({
46895
47227
  * moment they have the least reason to trust three. Unset means all of them,
46896
47228
  * which is the right steady-state behaviour.
46897
47229
  */
46898
- limit: num("limit", { integer: true }),
47230
+ limit: num2("limit", { integer: true }),
46899
47231
  intervalMs: ms("interval", 20),
46900
47232
  once: flag("once"),
46901
47233
  dryRun: flag("dry-run"),
@@ -46925,11 +47257,15 @@ var init_supervisor = __esm({
46925
47257
  * `pm.issue_attempts` since CRA-7 — it was in-memory, which meant a restart
46926
47258
  * forgot every set-aside and nothing outside the process could see one.
46927
47259
  */
46928
- maxAttempts: num("max-attempts", { fallback: 3, integer: true }),
47260
+ maxAttempts: num2("max-attempts", { fallback: 3, integer: true }),
46929
47261
  /**
46930
- * Ceiling for the backoff below. Ten minutes: long enough that a broken
46931
- * supervisor stops being a wall of identical lines, short enough that fixing
46932
- * the cause is noticed without restarting anything.
47262
+ * Ceiling on how long invocation is held back after a run of dead ticks.
47263
+ * Ten minutes: long enough that a broken harness stops being spawned over and
47264
+ * over, short enough that fixing the cause is noticed without restarting
47265
+ * anything.
47266
+ *
47267
+ * It is not a poll ceiling. Polling stays at `--interval` throughout, so this
47268
+ * bounds spend, not how quickly a recovery is seen (`CRA-87`).
46933
47269
  */
46934
47270
  maxIntervalMs: ms("max-interval", 600),
46935
47271
  /**
@@ -47014,6 +47350,7 @@ cruo-supervisor: ${error51.message}
47014
47350
  // stands in for. Cheap to list, and wrong to discover the hard way.
47015
47351
  "CRUO_SESSION"
47016
47352
  ];
47353
+ warnedAboutSpendTable = false;
47017
47354
  main().catch((e) => {
47018
47355
  console.error(e instanceof Error ? e.message : e);
47019
47356
  process.exit(1);
@@ -47022,17 +47359,26 @@ cruo-supervisor: ${error51.message}
47022
47359
  });
47023
47360
 
47024
47361
  // src/cli.ts
47362
+ init_dist();
47363
+ init_dist7();
47025
47364
  init_options();
47026
47365
  import { chmod, mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
47027
47366
  import { homedir as homedir2 } from "node:os";
47028
47367
  import { join as join3 } from "node:path";
47029
47368
  var CONFIG_DIR = join3(homedir2(), ".cruo");
47030
47369
  var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
47370
+ function nameFromEmail(email3) {
47371
+ const local = email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, "");
47372
+ return local && local.length > 0 ? local : null;
47373
+ }
47031
47374
  var USAGE = `cruo \u2014 run a Cruo agent
47032
47375
 
47033
47376
  cruo login <token> remember an agent's token on this machine
47034
- cruo logout forget it
47377
+ cruo agents list what is remembered (never the tokens)
47378
+ cruo usage [--days 7] what the agents have been spending
47379
+ cruo logout [name] forget one, or --all
47035
47380
  cruo [options] run the agent's supervisor
47381
+ cruo --as <name> \u2026 run a particular one
47036
47382
 
47037
47383
  Getting a token
47038
47384
  Cruo \u2192 Settings \u2192 Members \u2192 Add an agent. It is shown once.
@@ -47048,52 +47394,188 @@ Common options
47048
47394
 
47049
47395
  Full list and what each one is for: https://cruo.space/docs#agents
47050
47396
 
47397
+ More than one agent on one machine: \`login\` files each token under the name the
47398
+ board shows, so the chain is \`cruo --as jarvis\` and \`cruo --as luna\` rather than
47399
+ a CRUO_TOKEN export per terminal.
47400
+
47051
47401
  The token comes from CRUO_TOKEN, then --token, then whatever \`login\` stored.
47052
47402
  `;
47053
- async function storedToken() {
47403
+ var EMPTY = { version: 2, default: null, agents: {} };
47404
+ async function readConfig2() {
47405
+ let parsed;
47406
+ try {
47407
+ parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47408
+ } catch {
47409
+ return { ...EMPTY };
47410
+ }
47411
+ const raw = parsed;
47412
+ if (raw.agents && typeof raw.agents === "object") {
47413
+ return {
47414
+ version: 2,
47415
+ default: typeof raw.default === "string" ? raw.default : null,
47416
+ agents: raw.agents
47417
+ };
47418
+ }
47419
+ const legacy = raw.token?.trim();
47420
+ if (legacy) {
47421
+ return { version: 2, default: "default", agents: { default: { token: legacy } } };
47422
+ }
47423
+ return { ...EMPTY };
47424
+ }
47425
+ async function writeConfig(config3) {
47426
+ await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
47427
+ await writeFile2(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
47428
+ await chmod(CONFIG_PATH, 384);
47429
+ }
47430
+ async function identify2(token) {
47431
+ const endpoint = process.env.CRUO_SESSION_URL?.trim() || CRUO_CLOUD.sessionUrl;
47054
47432
  try {
47055
- const parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47056
- return parsed.token?.trim() || null;
47433
+ const session = await fetchDelegatedSession(endpoint, token);
47434
+ return { email: session.email, workspace: session.workspaceName };
47057
47435
  } catch {
47058
47436
  return null;
47059
47437
  }
47060
47438
  }
47061
- async function login(token) {
47439
+ async function login(token, as) {
47062
47440
  if (!token.startsWith("cruo_pat_")) {
47063
47441
  console.error(`
47064
47442
  That does not look like a Cruo token \u2014 they begin "cruo_pat_".
47065
47443
  `);
47066
47444
  process.exit(2);
47067
47445
  }
47068
- await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
47069
- await writeFile2(CONFIG_PATH, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
47070
- await chmod(CONFIG_PATH, 384);
47071
- console.log(`
47072
- Saved to ${CONFIG_PATH} (readable only by you).
47446
+ const who = as ? null : await identify2(token);
47447
+ const name = as ?? (who ? nameFromEmail(who.email) : null);
47448
+ if (!name) {
47449
+ console.error(
47450
+ `
47451
+ Could not work out whose token that is, so it needs a name:
47452
+
47453
+ cruo login <token> --as jarvis
47454
+
47455
+ That happens when the board cannot be reached \u2014 a local stack, or no
47456
+ network. The token is fine; only the lookup failed.
47457
+ `
47458
+ );
47459
+ process.exit(2);
47460
+ }
47461
+ const config3 = await readConfig2();
47462
+ const replacing = config3.agents[name];
47463
+ config3.agents[name] = {
47464
+ token,
47465
+ email: who?.email,
47466
+ workspace: who?.workspace,
47467
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
47468
+ };
47469
+ config3.default ??= name;
47470
+ await writeConfig(config3);
47471
+ const where = who ? ` (${who.email} in ${who.workspace})` : "";
47472
+ console.log(
47473
+ `
47474
+ ${replacing ? "Replaced" : "Saved"} the token for \`${name}\`${where}.
47475
+ ${CONFIG_PATH} is readable only by you.
47476
+
47477
+ ` + (config3.default === name ? `Run \`cruo --dry-run\` to see what it would pick up.
47478
+ ` : `Run \`cruo --as ${name} --dry-run\` to see what it would pick up.
47479
+ A bare \`cruo\` still runs \`${config3.default}\`.
47480
+ `)
47481
+ );
47482
+ }
47483
+ async function logout(which, all) {
47484
+ const config3 = await readConfig2();
47485
+ const names = Object.keys(config3.agents);
47486
+ if (all) {
47487
+ await rm3(CONFIG_PATH, { force: true });
47488
+ console.log(
47489
+ names.length === 0 ? `
47490
+ Nothing was stored.
47491
+ ` : `
47492
+ Forgot ${names.length} token(s): ${names.join(", ")}. ${CONFIG_PATH} is gone.
47493
+ `
47494
+ );
47495
+ return;
47496
+ }
47497
+ if (!which) {
47498
+ if (names.length <= 1) {
47499
+ await rm3(CONFIG_PATH, { force: true });
47500
+ console.log(
47501
+ names.length === 0 ? `
47502
+ Nothing was stored.
47503
+ ` : `
47504
+ Forgot the token for \`${names[0]}\`. ${CONFIG_PATH} is gone.
47505
+ `
47506
+ );
47507
+ return;
47508
+ }
47509
+ console.error(
47510
+ `
47511
+ ${names.length} tokens are stored: ${names.join(", ")}
47073
47512
 
47074
- Run \`cruo --dry-run\` to see what it would pick up.
47513
+ Say which: cruo logout <name>
47514
+ Or all: cruo logout --all
47515
+ `
47516
+ );
47517
+ process.exit(2);
47518
+ }
47519
+ if (!config3.agents[which]) {
47520
+ console.error(`
47521
+ Nothing stored for \`${which}\`. Stored: ${names.join(", ") || "(none)"}
47075
47522
  `);
47523
+ process.exit(2);
47524
+ }
47525
+ delete config3.agents[which];
47526
+ if (config3.default === which) config3.default = Object.keys(config3.agents)[0] ?? null;
47527
+ await writeConfig(config3);
47528
+ console.log(
47529
+ `
47530
+ Forgot the token for \`${which}\`.` + (config3.default ? ` A bare \`cruo\` now runs \`${config3.default}\`.
47531
+ ` : ` Nothing is stored now.
47532
+ `)
47533
+ );
47076
47534
  }
47077
- async function logout() {
47078
- await rm3(CONFIG_PATH, { force: true });
47535
+ async function listAgents() {
47536
+ const config3 = await readConfig2();
47537
+ const names = Object.keys(config3.agents);
47538
+ if (names.length === 0) {
47539
+ console.log(`
47540
+ No tokens stored. \`cruo login <token>\` to add one.
47541
+ `);
47542
+ return;
47543
+ }
47544
+ console.log("");
47545
+ for (const name of names) {
47546
+ const a = config3.agents[name];
47547
+ const mark = config3.default === name ? "*" : " ";
47548
+ const who = a.email ? ` \u2014 ${a.email}${a.workspace ? ` in ${a.workspace}` : ""}` : "";
47549
+ console.log(` ${mark} ${name}${who}`);
47550
+ }
47079
47551
  console.log(`
47080
- Forgotten. ${CONFIG_PATH} is gone.
47552
+ * is what a bare \`cruo\` runs. Use \`--as <name>\` for another.
47081
47553
  `);
47082
47554
  }
47083
47555
  async function main2() {
47084
47556
  const argv2 = process.argv.slice(2);
47085
47557
  const [first] = argv2;
47558
+ const flagValue = (name) => {
47559
+ const i = argv2.indexOf(name);
47560
+ return i >= 0 ? argv2[i + 1] ?? null : null;
47561
+ };
47086
47562
  if (first === "login") {
47087
47563
  const token2 = argv2[1];
47088
- if (!token2) {
47564
+ if (!token2 || token2.startsWith("--")) {
47089
47565
  console.error(`
47090
- Usage: cruo login <token>
47566
+ Usage: cruo login <token> [--as <name>]
47091
47567
  `);
47092
47568
  process.exit(2);
47093
47569
  }
47094
- return login(token2);
47570
+ return login(token2, flagValue("--as"));
47571
+ }
47572
+ if (first === "logout") {
47573
+ const named = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
47574
+ return logout(named, argv2.includes("--all"));
47575
+ }
47576
+ if (first === "agents" || first === "whoami") return listAgents();
47577
+ if (first === "usage") {
47095
47578
  }
47096
- if (first === "logout") return logout();
47097
47579
  if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
47098
47580
  console.log(USAGE);
47099
47581
  return;
@@ -47106,7 +47588,22 @@ Usage: cruo login <token>
47106
47588
  Prefer CRUO_TOKEN in the environment, or \`cruo login\` once.`
47107
47589
  );
47108
47590
  }
47109
- const token = process.env.CRUO_TOKEN?.trim() || fromFlag || await storedToken();
47591
+ const asName = flagValue("--as");
47592
+ const config3 = await readConfig2();
47593
+ const stored = asName ? config3.agents[asName] : config3.default ? config3.agents[config3.default] : void 0;
47594
+ if (asName && !stored) {
47595
+ const names = Object.keys(config3.agents);
47596
+ console.error(
47597
+ `
47598
+ Nothing stored for \`${asName}\`.
47599
+
47600
+ ` + (names.length > 0 ? `Stored: ${names.join(", ")}
47601
+ ` : `Nothing is stored at all \u2014 \`cruo login <token>\` first.
47602
+ `)
47603
+ );
47604
+ process.exit(2);
47605
+ }
47606
+ const token = process.env.CRUO_TOKEN?.trim() || fromFlag || stored?.token;
47110
47607
  if (!token) {
47111
47608
  console.error(
47112
47609
  `
@@ -47117,9 +47614,24 @@ Get one from Cruo \u2192 Settings \u2192 Members \u2192 Add an agent.
47117
47614
  );
47118
47615
  process.exit(2);
47119
47616
  }
47617
+ const usingStored = !process.env.CRUO_TOKEN?.trim() && !fromFlag && stored;
47618
+ if (usingStored) {
47619
+ process.env.CRUO_IDENTITY = asName ?? config3.default ?? "";
47620
+ }
47120
47621
  process.env.CRUO_TOKEN = token;
47121
- const passthrough = stripTokenFlag(argv2);
47622
+ const passthrough = stripTokenFlag(argv2).filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
47122
47623
  process.argv = [process.argv[0], process.argv[1], ...passthrough];
47624
+ if (first === "usage") {
47625
+ const { reportUsage: reportUsage2 } = await Promise.resolve().then(() => (init_usage(), usage_exports));
47626
+ const days = Number(flagValue("--days") ?? 7);
47627
+ if (!Number.isFinite(days) || days <= 0) {
47628
+ console.error(`
47629
+ --days must be a positive number.
47630
+ `);
47631
+ process.exit(2);
47632
+ }
47633
+ return reportUsage2({ days, issue: flagValue("--issue") });
47634
+ }
47123
47635
  await Promise.resolve().then(() => (init_supervisor(), supervisor_exports));
47124
47636
  }
47125
47637
  main2().catch((error51) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",