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.
- package/dist/VERSION +1 -1
- package/dist/cli.js +539 -80
- package/package.json +1 -1
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
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 =
|
|
16202
|
+
let effectiveSignal = signal2;
|
|
16203
16203
|
if (timeout) {
|
|
16204
16204
|
timeoutController = new AbortController();
|
|
16205
16205
|
timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
16206
|
-
if (
|
|
16206
|
+
if (signal2) {
|
|
16207
16207
|
effectiveSignal = timeoutController.signal;
|
|
16208
|
-
|
|
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,
|
|
16294
|
+
function sleep(ms2, signal2) {
|
|
16295
16295
|
return new Promise((resolve2) => {
|
|
16296
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
17338
|
-
this.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:
|
|
23498
|
-
const events = [close, error51, leave,
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
36031
|
-
if (!targetUrl || !
|
|
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
|
|
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
|
|
36063
|
+
const targets2 = [];
|
|
36064
36064
|
try {
|
|
36065
36065
|
const url3 = new URL(supabaseUrl);
|
|
36066
|
-
|
|
36066
|
+
targets2.push(url3.hostname);
|
|
36067
36067
|
} catch (error51) {
|
|
36068
36068
|
}
|
|
36069
|
-
|
|
36070
|
-
|
|
36071
|
-
return
|
|
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,
|
|
36125
|
-
if (!shouldPropagateToTarget(typeof input === "string" ? input : input instanceof URL ? input : input.url,
|
|
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
|
|
46052
|
-
let query = ctx.client.from("harness_runs").select("*").gte("started_at",
|
|
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
|
|
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 =
|
|
46411
|
-
await
|
|
46412
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
46621
|
+
var exec2;
|
|
46496
46622
|
var init_worktree = __esm({
|
|
46497
46623
|
"src/worktree.ts"() {
|
|
46498
46624
|
"use strict";
|
|
46499
|
-
|
|
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
|
|
46508
|
-
import { homedir, tmpdir } from "node:os";
|
|
46509
|
-
import { join as
|
|
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
|
|
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(
|
|
46683
|
-
const path =
|
|
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 =
|
|
46812
|
+
const server = join3(here, bundled ? "index.js" : "index.ts");
|
|
46687
46813
|
const loader = bundled ? null : createRequire(import.meta.url).resolve("tsx");
|
|
46688
|
-
await
|
|
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: () =>
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
47542
|
-
import {
|
|
47543
|
-
import {
|
|
47544
|
-
|
|
47545
|
-
var
|
|
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
|
|
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
|
|
47604
|
-
await
|
|
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
|
|
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
|
|
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