@primusclaw/connector 0.1.1 → 0.1.3
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/README.md +71 -13
- package/dist/cli.js +877 -176
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { parseArgs } from "node:util";
|
|
4
|
+
import fs16 from "node:fs";
|
|
5
|
+
import os6 from "node:os";
|
|
6
|
+
import path17 from "node:path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
9
9
|
|
|
10
10
|
// src/audit.ts
|
|
11
11
|
import fs from "node:fs";
|
|
@@ -335,11 +335,11 @@ var RevokedError = class extends Error {
|
|
|
335
335
|
}
|
|
336
336
|
};
|
|
337
337
|
var HttpError = class extends Error {
|
|
338
|
-
constructor(
|
|
339
|
-
super(`${
|
|
340
|
-
this.status =
|
|
338
|
+
constructor(status2, code, path18) {
|
|
339
|
+
super(`${path18} answered ${status2} ${code}`);
|
|
340
|
+
this.status = status2;
|
|
341
341
|
this.code = code;
|
|
342
|
-
this.path =
|
|
342
|
+
this.path = path18;
|
|
343
343
|
}
|
|
344
344
|
status;
|
|
345
345
|
code;
|
|
@@ -363,8 +363,8 @@ var ClawClient = class {
|
|
|
363
363
|
...extra
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
|
-
async send(method,
|
|
367
|
-
const url = this.o.server +
|
|
366
|
+
async send(method, path18, body, signal) {
|
|
367
|
+
const url = this.o.server + path18;
|
|
368
368
|
for (let attempt = 0; ; attempt++) {
|
|
369
369
|
const headers = await this.headers(body === void 0 ? {} : { "content-type": "application/json" });
|
|
370
370
|
let res;
|
|
@@ -376,7 +376,7 @@ var ClawClient = class {
|
|
|
376
376
|
signal: signal ?? AbortSignal.timeout(this.o.requestTimeoutMs ?? 2e4)
|
|
377
377
|
});
|
|
378
378
|
} catch (e) {
|
|
379
|
-
throw new TransportError(`${method} ${
|
|
379
|
+
throw new TransportError(`${method} ${path18}: ${errMessage(e)}`);
|
|
380
380
|
}
|
|
381
381
|
if (res.status !== 401) return res;
|
|
382
382
|
const err = await errorCode(res);
|
|
@@ -385,12 +385,12 @@ var ClawClient = class {
|
|
|
385
385
|
this.o.tokens.invalidate();
|
|
386
386
|
continue;
|
|
387
387
|
}
|
|
388
|
-
throw new HttpError(401, err,
|
|
388
|
+
throw new HttpError(401, err, path18);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
-
async json(method,
|
|
392
|
-
const res = await this.send(method,
|
|
393
|
-
if (!res.ok) throw new HttpError(res.status, await errorCode(res),
|
|
391
|
+
async json(method, path18, body) {
|
|
392
|
+
const res = await this.send(method, path18, body);
|
|
393
|
+
if (!res.ok) throw new HttpError(res.status, await errorCode(res), path18);
|
|
394
394
|
const parsed = await res.json().catch(() => null);
|
|
395
395
|
return isObject(parsed) && isObject(parsed.data) ? parsed.data : {};
|
|
396
396
|
}
|
|
@@ -428,21 +428,21 @@ var ClawClient = class {
|
|
|
428
428
|
};
|
|
429
429
|
try {
|
|
430
430
|
arm();
|
|
431
|
-
const
|
|
431
|
+
const path18 = `/v1/remote-agents/connectors/${cid}/commands`;
|
|
432
432
|
const headers = await this.headers({ accept: "text/event-stream", ...opts.lastEventId ? { "last-event-id": String(opts.lastEventId) } : {} });
|
|
433
433
|
let res;
|
|
434
434
|
try {
|
|
435
|
-
res = await this.f(this.o.server +
|
|
435
|
+
res = await this.f(this.o.server + path18, { headers, signal: stop.signal });
|
|
436
436
|
} catch (e) {
|
|
437
|
-
throw new TransportError(timedOut ? `no response for ${opts.watchdogMs} ms` : `GET ${
|
|
437
|
+
throw new TransportError(timedOut ? `no response for ${opts.watchdogMs} ms` : `GET ${path18}: ${errMessage(e)}`);
|
|
438
438
|
}
|
|
439
439
|
if (res.status === 401) {
|
|
440
440
|
const err = await errorCode(res);
|
|
441
441
|
if (err === "revoked") throw new RevokedError();
|
|
442
442
|
this.o.tokens.invalidate();
|
|
443
|
-
throw new HttpError(401, err,
|
|
443
|
+
throw new HttpError(401, err, path18);
|
|
444
444
|
}
|
|
445
|
-
if (!res.ok || !res.body) throw new HttpError(res.status, await errorCode(res),
|
|
445
|
+
if (!res.ok || !res.body) throw new HttpError(res.status, await errorCode(res), path18);
|
|
446
446
|
try {
|
|
447
447
|
for await (const frame of readSse(res.body, arm)) yield frame;
|
|
448
448
|
} catch (e) {
|
|
@@ -992,22 +992,93 @@ async function deviceLogin(o) {
|
|
|
992
992
|
return pollDeviceToken(o, auth);
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
+
// src/claude-mode.ts
|
|
996
|
+
import fs6 from "node:fs";
|
|
997
|
+
import os2 from "node:os";
|
|
998
|
+
import path6 from "node:path";
|
|
999
|
+
var isClaude = (a) => !a.attach && [a.command, ...a.args].some((s) => s.includes("claude-agent-acp"));
|
|
1000
|
+
function managedSettingsFile(platform = process.platform) {
|
|
1001
|
+
if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
|
1002
|
+
if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
|
|
1003
|
+
return "/etc/claude-code/managed-settings.json";
|
|
1004
|
+
}
|
|
1005
|
+
function readSettings(file, unreadable) {
|
|
1006
|
+
let text;
|
|
1007
|
+
try {
|
|
1008
|
+
text = fs6.readFileSync(file, "utf8");
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
if (e.code !== "ENOENT") unreadable.push(file);
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const v = JSON.parse(text);
|
|
1015
|
+
return isObject(v) ? v : null;
|
|
1016
|
+
} catch {
|
|
1017
|
+
unreadable.push(file);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function claudeStartMode(o) {
|
|
1022
|
+
const home = o.env.HOME?.trim() || os2.homedir();
|
|
1023
|
+
const configDir = o.env.CLAUDE_CONFIG_DIR?.trim() || path6.join(home, ".claude");
|
|
1024
|
+
const files = [
|
|
1025
|
+
["user", path6.join(configDir, "settings.json")],
|
|
1026
|
+
...o.cwd === null ? [] : [
|
|
1027
|
+
["project", path6.join(o.cwd, ".claude", "settings.json")],
|
|
1028
|
+
["local", path6.join(o.cwd, ".claude", "settings.local.json")]
|
|
1029
|
+
],
|
|
1030
|
+
["managed", o.managedFile ?? managedSettingsFile()]
|
|
1031
|
+
];
|
|
1032
|
+
const unreadable = [];
|
|
1033
|
+
let raw;
|
|
1034
|
+
let from = null;
|
|
1035
|
+
let disable;
|
|
1036
|
+
for (const [source, file] of files) {
|
|
1037
|
+
const s = readSettings(file, unreadable);
|
|
1038
|
+
const perms = s && isObject(s.permissions) ? s.permissions : null;
|
|
1039
|
+
if (!perms) continue;
|
|
1040
|
+
if (perms.defaultMode !== void 0) {
|
|
1041
|
+
raw = perms.defaultMode;
|
|
1042
|
+
from = [source, file];
|
|
1043
|
+
}
|
|
1044
|
+
if (perms.disableBypassPermissionsMode !== void 0) disable = perms.disableBypassPermissionsMode;
|
|
1045
|
+
}
|
|
1046
|
+
let bypass = raw === "bypassPermissions" && from !== null && from[0] !== "project" && from[0] !== "local";
|
|
1047
|
+
if (bypass) {
|
|
1048
|
+
const euid = o.euid === void 0 ? process.geteuid?.() ?? null : o.euid;
|
|
1049
|
+
if (euid === 0 && !o.env.IS_SANDBOX || disable === "disable") bypass = false;
|
|
1050
|
+
}
|
|
1051
|
+
return { bypass, file: bypass ? from[1] : null, unreadable };
|
|
1052
|
+
}
|
|
1053
|
+
function noAskWarnings(agents, env, opts = {}) {
|
|
1054
|
+
const out = [];
|
|
1055
|
+
for (const a of agents) {
|
|
1056
|
+
if (!isClaude(a) || a.allowUnsafeModes) continue;
|
|
1057
|
+
const r = claudeStartMode({ env: { ...env, ...a.env }, cwd: null, ...opts });
|
|
1058
|
+
if (r.bypass) {
|
|
1059
|
+
out.push(`${a.id}: sessions will be refused (mode_not_enforced): Claude defaults to bypassPermissions here (permissions.defaultMode in ${r.file}); set approval = "never" on this agent or change defaultMode`);
|
|
1060
|
+
}
|
|
1061
|
+
for (const f of r.unreadable) out.push(`${a.id}: cannot tell whether Claude defaults to bypassPermissions: ${f} is not readable JSON`);
|
|
1062
|
+
}
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
995
1066
|
// src/hooks.ts
|
|
996
|
-
import
|
|
997
|
-
import
|
|
1067
|
+
import fs8 from "node:fs";
|
|
1068
|
+
import path8 from "node:path";
|
|
998
1069
|
|
|
999
1070
|
// src/terminals.ts
|
|
1000
|
-
import
|
|
1001
|
-
import
|
|
1071
|
+
import fs7 from "node:fs";
|
|
1072
|
+
import path7 from "node:path";
|
|
1002
1073
|
var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1003
1074
|
var TERMINALS_DIR = "claw-connector-terminals";
|
|
1004
|
-
var terminalsDir = (configDir) =>
|
|
1075
|
+
var terminalsDir = (configDir) => path7.join(configDir, TERMINALS_DIR);
|
|
1005
1076
|
var UNREADABLE_GRACE_MS = 6e4;
|
|
1006
1077
|
var MAX_STDIN = 1 << 20;
|
|
1007
1078
|
function localPidDomain() {
|
|
1008
1079
|
if (process.platform !== "linux") return null;
|
|
1009
1080
|
try {
|
|
1010
|
-
return `linux:${
|
|
1081
|
+
return `linux:${fs7.readFileSync("/etc/machine-id", "utf8").trim()}:${fs7.readlinkSync("/proc/self/ns/pid")}`;
|
|
1011
1082
|
} catch {
|
|
1012
1083
|
return null;
|
|
1013
1084
|
}
|
|
@@ -1038,7 +1109,7 @@ function readTerminals(configDir, localDomain, now = Date.now()) {
|
|
|
1038
1109
|
const dir = terminalsDir(configDir);
|
|
1039
1110
|
let names;
|
|
1040
1111
|
try {
|
|
1041
|
-
names =
|
|
1112
|
+
names = fs7.readdirSync(dir);
|
|
1042
1113
|
} catch (e) {
|
|
1043
1114
|
if (e.code === "ENOENT") return [];
|
|
1044
1115
|
throw e;
|
|
@@ -1046,15 +1117,15 @@ function readTerminals(configDir, localDomain, now = Date.now()) {
|
|
|
1046
1117
|
const out = [];
|
|
1047
1118
|
for (const name of names) {
|
|
1048
1119
|
if (!name.endsWith(".json")) continue;
|
|
1049
|
-
const file =
|
|
1120
|
+
const file = path7.join(dir, name);
|
|
1050
1121
|
let doc;
|
|
1051
1122
|
try {
|
|
1052
|
-
doc = JSON.parse(
|
|
1123
|
+
doc = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
1053
1124
|
} catch (e) {
|
|
1054
1125
|
if (e.code === "ENOENT") continue;
|
|
1055
1126
|
let age = Infinity;
|
|
1056
1127
|
try {
|
|
1057
|
-
age = now -
|
|
1128
|
+
age = now - fs7.statSync(file).mtimeMs;
|
|
1058
1129
|
} catch {
|
|
1059
1130
|
}
|
|
1060
1131
|
if (age < UNREADABLE_GRACE_MS) throw new Error(`${file} is unreadable (${e.message.split("\n")[0]})`);
|
|
@@ -1081,19 +1152,19 @@ var shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
|
1081
1152
|
function hookCommand(node, script, arg, configDir) {
|
|
1082
1153
|
return `${shellQuote(node)} ${shellQuote(script)} ${arg} --dir ${shellQuote(configDir)} ${MANAGED_MARK}`;
|
|
1083
1154
|
}
|
|
1084
|
-
var settingsFile = (configDir) =>
|
|
1155
|
+
var settingsFile = (configDir) => path8.join(configDir, "settings.json");
|
|
1085
1156
|
var isObject3 = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1086
1157
|
var isOurs = (h) => isObject3(h) && h.type === "command" && typeof h.command === "string" && h.command.endsWith(` ${MANAGED_MARK}`) && OURS_RE.test(h.command);
|
|
1087
1158
|
function load(configDir) {
|
|
1088
1159
|
const given = settingsFile(configDir);
|
|
1089
1160
|
let file = given;
|
|
1090
1161
|
try {
|
|
1091
|
-
file =
|
|
1162
|
+
file = fs8.realpathSync(given);
|
|
1092
1163
|
} catch {
|
|
1093
1164
|
}
|
|
1094
1165
|
let text;
|
|
1095
1166
|
try {
|
|
1096
|
-
text =
|
|
1167
|
+
text = fs8.readFileSync(file, "utf8");
|
|
1097
1168
|
} catch (e) {
|
|
1098
1169
|
if (e.code === "ENOENT") return { doc: {}, text: null, file };
|
|
1099
1170
|
throw new HooksError(`cannot read ${given}: ${e.message}`);
|
|
@@ -1111,18 +1182,18 @@ function load(configDir) {
|
|
|
1111
1182
|
function save(l, doc) {
|
|
1112
1183
|
const indent = l.text?.match(/^([ \t]+)"/m)?.[1] ?? " ";
|
|
1113
1184
|
const body = JSON.stringify(doc, null, indent) + "\n";
|
|
1114
|
-
|
|
1185
|
+
fs8.mkdirSync(path8.dirname(l.file), { recursive: true, mode: 448 });
|
|
1115
1186
|
let mode = 384;
|
|
1116
1187
|
try {
|
|
1117
|
-
mode =
|
|
1188
|
+
mode = fs8.statSync(l.file).mode & 511;
|
|
1118
1189
|
} catch {
|
|
1119
1190
|
}
|
|
1120
1191
|
const tmp = `${l.file}.claw-connector.${process.pid}.tmp`;
|
|
1121
|
-
|
|
1192
|
+
fs8.writeFileSync(tmp, body, { mode });
|
|
1122
1193
|
try {
|
|
1123
|
-
|
|
1194
|
+
fs8.renameSync(tmp, l.file);
|
|
1124
1195
|
} catch (e) {
|
|
1125
|
-
|
|
1196
|
+
fs8.rmSync(tmp, { force: true });
|
|
1126
1197
|
throw e;
|
|
1127
1198
|
}
|
|
1128
1199
|
}
|
|
@@ -1202,7 +1273,7 @@ function uninstallHooks(configDir) {
|
|
|
1202
1273
|
if (touchedAny && Object.keys(hooks).length === 0) delete doc.hooks;
|
|
1203
1274
|
}
|
|
1204
1275
|
if (touchedAny) save(l, doc);
|
|
1205
|
-
|
|
1276
|
+
fs8.rmSync(terminalsDir(configDir), { recursive: true, force: true });
|
|
1206
1277
|
return { changed: touchedAny, file: l.file };
|
|
1207
1278
|
}
|
|
1208
1279
|
function hooksStatus(configDir) {
|
|
@@ -1225,7 +1296,7 @@ function hooksStatus(configDir) {
|
|
|
1225
1296
|
const m = /^'((?:[^']|'\\'')*)' '((?:[^']|'\\'')*)' /.exec(String(x.command));
|
|
1226
1297
|
if (!m) continue;
|
|
1227
1298
|
for (const f of [m[1], m[2]].map((s) => s.replace(/'\\''/g, "'"))) {
|
|
1228
|
-
if (!
|
|
1299
|
+
if (!fs8.existsSync(f)) {
|
|
1229
1300
|
healthy = false;
|
|
1230
1301
|
problem ??= `${f} (named by the ${h.event} hook) does not exist; run: claw-connector hooks install`;
|
|
1231
1302
|
}
|
|
@@ -1236,19 +1307,19 @@ function hooksStatus(configDir) {
|
|
|
1236
1307
|
}
|
|
1237
1308
|
|
|
1238
1309
|
// src/connector.ts
|
|
1239
|
-
import
|
|
1310
|
+
import fs14 from "node:fs";
|
|
1240
1311
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1241
1312
|
|
|
1242
1313
|
// src/attach.ts
|
|
1243
1314
|
import { execFile, execFileSync as execFileSync2 } from "node:child_process";
|
|
1244
|
-
import
|
|
1245
|
-
import
|
|
1246
|
-
import
|
|
1315
|
+
import fs10 from "node:fs";
|
|
1316
|
+
import os3 from "node:os";
|
|
1317
|
+
import path10 from "node:path";
|
|
1247
1318
|
|
|
1248
1319
|
// src/adapter.ts
|
|
1249
1320
|
import { spawn } from "node:child_process";
|
|
1250
|
-
import
|
|
1251
|
-
import
|
|
1321
|
+
import fs9 from "node:fs";
|
|
1322
|
+
import path9 from "node:path";
|
|
1252
1323
|
import { Readable, Writable } from "node:stream";
|
|
1253
1324
|
import * as acp from "@agentclientprotocol/sdk";
|
|
1254
1325
|
var AdapterProcess = class _AdapterProcess {
|
|
@@ -1264,8 +1335,8 @@ var AdapterProcess = class _AdapterProcess {
|
|
|
1264
1335
|
exited = false;
|
|
1265
1336
|
conn;
|
|
1266
1337
|
static async start(o) {
|
|
1267
|
-
|
|
1268
|
-
const log =
|
|
1338
|
+
fs9.mkdirSync(path9.dirname(o.logFile), { recursive: true, mode: 448 });
|
|
1339
|
+
const log = fs9.openSync(o.logFile, "a", 384);
|
|
1269
1340
|
const child = spawn(o.cfg.command, o.cfg.args, {
|
|
1270
1341
|
cwd: o.cwd,
|
|
1271
1342
|
env: { ...process.env, ...o.cfg.env },
|
|
@@ -1277,7 +1348,7 @@ var AdapterProcess = class _AdapterProcess {
|
|
|
1277
1348
|
child.once("spawn", () => resolve());
|
|
1278
1349
|
child.once("error", (e) => reject(new Error(`could not start ${o.cfg.command}: ${errMessage(e)}`)));
|
|
1279
1350
|
}).catch((e) => {
|
|
1280
|
-
|
|
1351
|
+
fs9.closeSync(log);
|
|
1281
1352
|
throw e;
|
|
1282
1353
|
});
|
|
1283
1354
|
const pid = child.pid;
|
|
@@ -1287,7 +1358,7 @@ var AdapterProcess = class _AdapterProcess {
|
|
|
1287
1358
|
const flushLine = (line) => {
|
|
1288
1359
|
const text = redactText(line).text;
|
|
1289
1360
|
try {
|
|
1290
|
-
|
|
1361
|
+
fs9.writeSync(log, text + "\n");
|
|
1291
1362
|
} catch {
|
|
1292
1363
|
}
|
|
1293
1364
|
try {
|
|
@@ -1313,7 +1384,7 @@ var AdapterProcess = class _AdapterProcess {
|
|
|
1313
1384
|
tail = "";
|
|
1314
1385
|
}
|
|
1315
1386
|
try {
|
|
1316
|
-
|
|
1387
|
+
fs9.closeSync(log);
|
|
1317
1388
|
} catch {
|
|
1318
1389
|
}
|
|
1319
1390
|
});
|
|
@@ -1449,7 +1520,7 @@ var AttachSupport = class {
|
|
|
1449
1520
|
constructor(agent, d) {
|
|
1450
1521
|
this.agent = agent;
|
|
1451
1522
|
this.d = d;
|
|
1452
|
-
this.configDir =
|
|
1523
|
+
this.configDir = path10.resolve(agent.env.CLAUDE_CONFIG_DIR ?? d.env.CLAUDE_CONFIG_DIR ?? path10.join(os3.homedir(), ".claude"));
|
|
1453
1524
|
}
|
|
1454
1525
|
agent;
|
|
1455
1526
|
d;
|
|
@@ -1506,9 +1577,9 @@ var AttachSupport = class {
|
|
|
1506
1577
|
cfg: this.agent,
|
|
1507
1578
|
registry: this.d.registry,
|
|
1508
1579
|
sessionId: `list-${this.agent.id}`,
|
|
1509
|
-
cwd: this.agent.roots[0] ??
|
|
1580
|
+
cwd: this.agent.roots[0] ?? os3.homedir(),
|
|
1510
1581
|
hooks: NO_HOOKS,
|
|
1511
|
-
logFile:
|
|
1582
|
+
logFile: path10.join(this.d.logDir, `adapter-${this.agent.id}.log`),
|
|
1512
1583
|
version: this.d.version,
|
|
1513
1584
|
initTimeoutMs: 3e4
|
|
1514
1585
|
});
|
|
@@ -1602,7 +1673,7 @@ var AttachSupport = class {
|
|
|
1602
1673
|
const file = this.transcriptFile(want);
|
|
1603
1674
|
let age = null;
|
|
1604
1675
|
try {
|
|
1605
|
-
if (file) age = this.now() -
|
|
1676
|
+
if (file) age = this.now() - fs10.statSync(file).mtimeMs;
|
|
1606
1677
|
} catch {
|
|
1607
1678
|
}
|
|
1608
1679
|
if (age !== null && age < this.d.recentWriteMs) return { ok: false, reason: "recently_written", detail: `its transcript was written ${Math.max(0, Math.round(age / 1e3))} s ago` };
|
|
@@ -1657,7 +1728,7 @@ var AttachSupport = class {
|
|
|
1657
1728
|
transcriptSize(ref) {
|
|
1658
1729
|
const file = SESSION_REF_RE.test(ref) ? this.transcriptFile(ref.toLowerCase()) : null;
|
|
1659
1730
|
try {
|
|
1660
|
-
return file ?
|
|
1731
|
+
return file ? fs10.statSync(file).size : null;
|
|
1661
1732
|
} catch {
|
|
1662
1733
|
return null;
|
|
1663
1734
|
}
|
|
@@ -1665,24 +1736,24 @@ var AttachSupport = class {
|
|
|
1665
1736
|
/** `<config dir>/projects/<working directory with every non-alphanumeric turned into "-">/<id>.jsonl`, found by name when that guess misses. */
|
|
1666
1737
|
transcriptFile(ref) {
|
|
1667
1738
|
const cached = this.transcripts.get(ref);
|
|
1668
|
-
if (cached &&
|
|
1739
|
+
if (cached && fs10.existsSync(cached)) return cached;
|
|
1669
1740
|
this.transcripts.delete(ref);
|
|
1670
|
-
const projects =
|
|
1741
|
+
const projects = path10.join(this.configDir, "projects");
|
|
1671
1742
|
const remember = (f) => {
|
|
1672
1743
|
this.transcripts.set(ref, f);
|
|
1673
1744
|
return f;
|
|
1674
1745
|
};
|
|
1675
1746
|
const known = this.listed.find((l) => l.ref === ref);
|
|
1676
1747
|
if (known) {
|
|
1677
|
-
const guess =
|
|
1678
|
-
if (
|
|
1748
|
+
const guess = path10.join(projects, known.cwd.replace(/[^A-Za-z0-9]/g, "-"), `${ref}.jsonl`);
|
|
1749
|
+
if (fs10.existsSync(guess)) return remember(guess);
|
|
1679
1750
|
}
|
|
1680
1751
|
try {
|
|
1681
1752
|
let n = 0;
|
|
1682
|
-
for (const dir of
|
|
1753
|
+
for (const dir of fs10.readdirSync(projects)) {
|
|
1683
1754
|
if (++n > MAX_SCANNED_PROJECT_DIRS) break;
|
|
1684
|
-
const f =
|
|
1685
|
-
if (
|
|
1755
|
+
const f = path10.join(projects, dir, `${ref}.jsonl`);
|
|
1756
|
+
if (fs10.existsSync(f)) return remember(f);
|
|
1686
1757
|
}
|
|
1687
1758
|
} catch {
|
|
1688
1759
|
}
|
|
@@ -1728,24 +1799,24 @@ function parseAgentsJson(text) {
|
|
|
1728
1799
|
return out;
|
|
1729
1800
|
}
|
|
1730
1801
|
function readRegistry(configDir, localDomain, now = Date.now()) {
|
|
1731
|
-
const dir =
|
|
1802
|
+
const dir = path10.join(configDir, "sessions");
|
|
1732
1803
|
let names;
|
|
1733
1804
|
try {
|
|
1734
|
-
names =
|
|
1805
|
+
names = fs10.readdirSync(dir);
|
|
1735
1806
|
} catch (e) {
|
|
1736
1807
|
throw new TwinCheckError(`${dir}: ${errMessage(e)}`);
|
|
1737
1808
|
}
|
|
1738
1809
|
const out = [];
|
|
1739
1810
|
for (const name of names) {
|
|
1740
1811
|
if (!name.endsWith(".json")) continue;
|
|
1741
|
-
const file =
|
|
1812
|
+
const file = path10.join(dir, name);
|
|
1742
1813
|
let doc;
|
|
1743
1814
|
try {
|
|
1744
|
-
doc = JSON.parse(
|
|
1815
|
+
doc = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
1745
1816
|
} catch (e) {
|
|
1746
1817
|
let age = Infinity;
|
|
1747
1818
|
try {
|
|
1748
|
-
age = now -
|
|
1819
|
+
age = now - fs10.statSync(file).mtimeMs;
|
|
1749
1820
|
} catch {
|
|
1750
1821
|
}
|
|
1751
1822
|
if (age < UNREADABLE_GRACE_MS2) throw new TwinCheckError(`${file} is unreadable (${errMessage(e).split("\n")[0]})`);
|
|
@@ -1765,7 +1836,7 @@ function readRegistry(configDir, localDomain, now = Date.now()) {
|
|
|
1765
1836
|
function processGroup(pid) {
|
|
1766
1837
|
try {
|
|
1767
1838
|
if (process.platform === "linux") {
|
|
1768
|
-
const stat =
|
|
1839
|
+
const stat = fs10.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1769
1840
|
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[2]) || null;
|
|
1770
1841
|
}
|
|
1771
1842
|
return Number(execFileSync2("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()) || null;
|
|
@@ -1788,8 +1859,106 @@ function fitResumable(base, items, limit = REMOTE_AGENT_LIMITS.maxCapabilitiesBy
|
|
|
1788
1859
|
}
|
|
1789
1860
|
}
|
|
1790
1861
|
|
|
1862
|
+
// src/health.ts
|
|
1863
|
+
import fs11 from "node:fs";
|
|
1864
|
+
import path12 from "node:path";
|
|
1865
|
+
|
|
1866
|
+
// src/paths.ts
|
|
1867
|
+
import os4 from "node:os";
|
|
1868
|
+
import path11 from "node:path";
|
|
1869
|
+
function resolveDirs(env = process.env, home = os4.homedir()) {
|
|
1870
|
+
const one = env.CLAW_CONNECTOR_HOME?.trim();
|
|
1871
|
+
if (one) return { config: path11.join(one, "config"), state: path11.join(one, "state") };
|
|
1872
|
+
const cfg = env.XDG_CONFIG_HOME?.trim() || path11.join(home, ".config");
|
|
1873
|
+
const st = env.XDG_STATE_HOME?.trim() || path11.join(home, ".local", "state");
|
|
1874
|
+
return { config: path11.join(cfg, "claw-connector"), state: path11.join(st, "claw-connector") };
|
|
1875
|
+
}
|
|
1876
|
+
var configFile = (d) => path11.join(d.config, "agents.toml");
|
|
1877
|
+
var credentialsFile = (d) => path11.join(d.config, "credentials.json");
|
|
1878
|
+
var identityFile = (d) => path11.join(d.state, "connector.json");
|
|
1879
|
+
var sessionsFile = (d) => path11.join(d.state, "sessions.json");
|
|
1880
|
+
var childrenFile = (d) => path11.join(d.state, "children.json");
|
|
1881
|
+
var pidFile = (d) => path11.join(d.state, "connector.pid");
|
|
1882
|
+
var pauseFile = (d) => path11.join(d.state, "paused");
|
|
1883
|
+
var auditFile = (d) => path11.join(d.state, "audit.jsonl");
|
|
1884
|
+
var outboxDir = (d) => path11.join(d.state, "outbox");
|
|
1885
|
+
var logDir = (d) => path11.join(d.state, "logs");
|
|
1886
|
+
var disconnectsFile = (d) => path11.join(d.state, "disconnects.jsonl");
|
|
1887
|
+
var needsLoginFile = (d) => path11.join(d.state, "needs-login.json");
|
|
1888
|
+
var supervisorPidFile = (d) => path11.join(d.state, "supervisor.pid");
|
|
1889
|
+
var runLogFile = (d) => path11.join(d.state, "run.log");
|
|
1890
|
+
var keepaliveFile = (d) => path11.join(d.state, "keepalive.sh");
|
|
1891
|
+
var serviceFile = (d) => path11.join(d.state, "service.json");
|
|
1892
|
+
|
|
1893
|
+
// src/health.ts
|
|
1894
|
+
var MAX_BYTES = 128 * 1024;
|
|
1895
|
+
function recordDisconnect(dirs, d) {
|
|
1896
|
+
const file = disconnectsFile(dirs);
|
|
1897
|
+
try {
|
|
1898
|
+
fs11.mkdirSync(path12.dirname(file), { recursive: true, mode: 448 });
|
|
1899
|
+
const fd = fs11.openSync(file, "a+", 384);
|
|
1900
|
+
let lines = null;
|
|
1901
|
+
try {
|
|
1902
|
+
fs11.writeSync(fd, JSON.stringify({ at: d.at ?? (/* @__PURE__ */ new Date()).toISOString(), reason: d.reason, ...d.detail ? { detail: d.detail.slice(0, 300) } : {} }) + "\n");
|
|
1903
|
+
const size = fs11.fstatSync(fd).size;
|
|
1904
|
+
if (size > MAX_BYTES) {
|
|
1905
|
+
const buf = Buffer.alloc(size);
|
|
1906
|
+
let got = 0;
|
|
1907
|
+
while (got < size) {
|
|
1908
|
+
const n = fs11.readSync(fd, buf, got, size - got, got);
|
|
1909
|
+
if (n === 0) break;
|
|
1910
|
+
got += n;
|
|
1911
|
+
}
|
|
1912
|
+
lines = buf.subarray(0, got).toString("utf8").split("\n").filter(Boolean);
|
|
1913
|
+
}
|
|
1914
|
+
} finally {
|
|
1915
|
+
fs11.closeSync(fd);
|
|
1916
|
+
}
|
|
1917
|
+
if (lines) {
|
|
1918
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1919
|
+
fs11.writeFileSync(tmp, lines.slice(Math.floor(lines.length / 2)).join("\n") + "\n", { mode: 384 });
|
|
1920
|
+
fs11.renameSync(tmp, file);
|
|
1921
|
+
}
|
|
1922
|
+
} catch {
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function recentDisconnects(dirs, n = 1) {
|
|
1926
|
+
try {
|
|
1927
|
+
const out = [];
|
|
1928
|
+
for (const line of fs11.readFileSync(disconnectsFile(dirs), "utf8").split("\n").filter(Boolean).slice(-n)) {
|
|
1929
|
+
try {
|
|
1930
|
+
const v = JSON.parse(line);
|
|
1931
|
+
if (isObject(v) && typeof v.at === "string" && typeof v.reason === "string") out.push({ at: v.at, reason: v.reason, ...typeof v.detail === "string" ? { detail: v.detail } : {} });
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return out;
|
|
1936
|
+
} catch {
|
|
1937
|
+
return [];
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function markNeedsLogin(dirs, reason) {
|
|
1941
|
+
try {
|
|
1942
|
+
fs11.mkdirSync(dirs.state, { recursive: true, mode: 448 });
|
|
1943
|
+
fs11.writeFileSync(needsLoginFile(dirs), JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason.slice(0, 300) }), { mode: 384 });
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function needsLogin(dirs) {
|
|
1948
|
+
try {
|
|
1949
|
+
const v = JSON.parse(fs11.readFileSync(needsLoginFile(dirs), "utf8"));
|
|
1950
|
+
if (isObject(v) && typeof v.reason === "string") return { at: typeof v.at === "string" ? v.at : "", reason: v.reason };
|
|
1951
|
+
return { at: "", reason: "unreadable needs-login.json" };
|
|
1952
|
+
} catch (e) {
|
|
1953
|
+
return e.code === "ENOENT" ? null : { at: "", reason: "unreadable needs-login.json" };
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function clearNeedsLogin(dirs) {
|
|
1957
|
+
fs11.rmSync(needsLoginFile(dirs), { force: true });
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1791
1960
|
// src/session.ts
|
|
1792
|
-
import
|
|
1961
|
+
import path13 from "node:path";
|
|
1793
1962
|
|
|
1794
1963
|
// src/permissions.ts
|
|
1795
1964
|
import { randomUUID } from "node:crypto";
|
|
@@ -2135,7 +2304,7 @@ var Session = class {
|
|
|
2135
2304
|
sessionId: this.id,
|
|
2136
2305
|
cwd: this.cwd,
|
|
2137
2306
|
hooks: this.hooks(),
|
|
2138
|
-
logFile:
|
|
2307
|
+
logFile: path13.join(this.env.logDir, `adapter-${this.agent.id}.log`),
|
|
2139
2308
|
version: this.env.version
|
|
2140
2309
|
}).catch((e) => {
|
|
2141
2310
|
throw new SessionRefusal("adapter_start_failed", errMessage(e));
|
|
@@ -2835,8 +3004,8 @@ var SessionManager = class {
|
|
|
2835
3004
|
};
|
|
2836
3005
|
|
|
2837
3006
|
// src/outbox.ts
|
|
2838
|
-
import
|
|
2839
|
-
import
|
|
3007
|
+
import fs12 from "node:fs";
|
|
3008
|
+
import path14 from "node:path";
|
|
2840
3009
|
var DROPPABLE_TYPES = /* @__PURE__ */ new Set([
|
|
2841
3010
|
"agent_message_chunk",
|
|
2842
3011
|
"agent_thought_chunk",
|
|
@@ -2863,35 +3032,35 @@ var Outbox = class {
|
|
|
2863
3032
|
this.dir = opts.dir;
|
|
2864
3033
|
this.maxBytes = opts.maxBytesPerSession ?? 5 * 1024 * 1024;
|
|
2865
3034
|
this.now = opts.now ?? Date.now;
|
|
2866
|
-
|
|
3035
|
+
fs12.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
2867
3036
|
this.load();
|
|
2868
3037
|
}
|
|
2869
|
-
eventsFile = (sid) =>
|
|
2870
|
-
metaFile = (sid) =>
|
|
3038
|
+
eventsFile = (sid) => path14.join(this.dir, `${safeName(sid)}.jsonl`);
|
|
3039
|
+
metaFile = (sid) => path14.join(this.dir, `${safeName(sid)}.meta.json`);
|
|
2871
3040
|
writeMeta(sid) {
|
|
2872
3041
|
const tmp = this.metaFile(sid) + ".tmp";
|
|
2873
|
-
|
|
2874
|
-
|
|
3042
|
+
fs12.writeFileSync(tmp, JSON.stringify(this.meta.get(sid)), { mode: 384 });
|
|
3043
|
+
fs12.renameSync(tmp, this.metaFile(sid));
|
|
2875
3044
|
}
|
|
2876
3045
|
/** Rebuild the queue from disk. The highest sequence number ever used is the larger of the meta file and the log itself. */
|
|
2877
3046
|
load() {
|
|
2878
3047
|
const loaded = [];
|
|
2879
3048
|
const bases = /* @__PURE__ */ new Set();
|
|
2880
|
-
for (const name of
|
|
3049
|
+
for (const name of fs12.readdirSync(this.dir)) {
|
|
2881
3050
|
if (name.endsWith(".meta.json")) bases.add(name.slice(0, -".meta.json".length));
|
|
2882
3051
|
else if (name.endsWith(".jsonl")) bases.add(name.slice(0, -".jsonl".length));
|
|
2883
3052
|
}
|
|
2884
3053
|
for (const base of bases) {
|
|
2885
3054
|
let meta = { lastSeq: 0, acked: 0 };
|
|
2886
3055
|
try {
|
|
2887
|
-
meta = JSON.parse(
|
|
3056
|
+
meta = JSON.parse(fs12.readFileSync(path14.join(this.dir, `${base}.meta.json`), "utf8"));
|
|
2888
3057
|
} catch {
|
|
2889
3058
|
}
|
|
2890
3059
|
let sid = null;
|
|
2891
|
-
const file =
|
|
3060
|
+
const file = path14.join(this.dir, `${base}.jsonl`);
|
|
2892
3061
|
let text = "";
|
|
2893
3062
|
try {
|
|
2894
|
-
text =
|
|
3063
|
+
text = fs12.readFileSync(file, "utf8");
|
|
2895
3064
|
} catch {
|
|
2896
3065
|
}
|
|
2897
3066
|
for (const line of text.split("\n")) {
|
|
@@ -2909,8 +3078,8 @@ var Outbox = class {
|
|
|
2909
3078
|
if (sid === null) {
|
|
2910
3079
|
sid = base;
|
|
2911
3080
|
try {
|
|
2912
|
-
if (this.now() -
|
|
2913
|
-
for (const f of [`${base}.meta.json`, `${base}.jsonl`])
|
|
3081
|
+
if (this.now() - fs12.statSync(path14.join(this.dir, `${base}.meta.json`)).mtimeMs > TOMBSTONE_MS) {
|
|
3082
|
+
for (const f of [`${base}.meta.json`, `${base}.jsonl`]) fs12.rmSync(path14.join(this.dir, f), { force: true });
|
|
2914
3083
|
continue;
|
|
2915
3084
|
}
|
|
2916
3085
|
} catch {
|
|
@@ -2932,7 +3101,7 @@ var Outbox = class {
|
|
|
2932
3101
|
meta.lastSeq = ev.seq;
|
|
2933
3102
|
this.meta.set(sessionId, meta);
|
|
2934
3103
|
const line = JSON.stringify(ev);
|
|
2935
|
-
|
|
3104
|
+
fs12.appendFileSync(this.eventsFile(sessionId), line + "\n", { mode: 384 });
|
|
2936
3105
|
if (ev.seq === 1) this.writeMeta(sessionId);
|
|
2937
3106
|
this.queue.push(ev);
|
|
2938
3107
|
this.bytes.set(sessionId, (this.bytes.get(sessionId) ?? 0) + line.length);
|
|
@@ -2961,8 +3130,8 @@ var Outbox = class {
|
|
|
2961
3130
|
this.writeMeta(sessionId);
|
|
2962
3131
|
const mine = this.queue.filter((e) => e.session_id === sessionId);
|
|
2963
3132
|
const tmp = this.eventsFile(sessionId) + ".tmp";
|
|
2964
|
-
|
|
2965
|
-
|
|
3133
|
+
fs12.writeFileSync(tmp, mine.map((e) => JSON.stringify(e) + "\n").join(""), { mode: 384 });
|
|
3134
|
+
fs12.renameSync(tmp, this.eventsFile(sessionId));
|
|
2966
3135
|
}
|
|
2967
3136
|
/** The oldest events not yet acknowledged, in the order they were appended. */
|
|
2968
3137
|
pending(max) {
|
|
@@ -3000,7 +3169,7 @@ var Outbox = class {
|
|
|
3000
3169
|
this.writeMeta(sid);
|
|
3001
3170
|
const left = this.queue.filter((e) => e.session_id === sid);
|
|
3002
3171
|
if (left.length === 0) {
|
|
3003
|
-
|
|
3172
|
+
fs12.writeFileSync(this.eventsFile(sid), "", { mode: 384 });
|
|
3004
3173
|
this.bytes.set(sid, 0);
|
|
3005
3174
|
if (this.closed.has(sid)) this.forget(sid);
|
|
3006
3175
|
} else if (left.length < this.queue.length / 2 || meta.acked > 1e3) {
|
|
@@ -3019,48 +3188,27 @@ var Outbox = class {
|
|
|
3019
3188
|
* repeat. What stays is the small meta file, a tombstone, removed after {@link TOMBSTONE_MS}.
|
|
3020
3189
|
*/
|
|
3021
3190
|
forget(sid) {
|
|
3022
|
-
|
|
3191
|
+
fs12.rmSync(this.eventsFile(sid), { force: true });
|
|
3023
3192
|
if (this.meta.has(sid)) this.writeMeta(sid);
|
|
3024
3193
|
this.bytes.delete(sid);
|
|
3025
3194
|
this.closed.delete(sid);
|
|
3026
3195
|
}
|
|
3027
3196
|
};
|
|
3028
3197
|
|
|
3029
|
-
// src/paths.ts
|
|
3030
|
-
import os3 from "node:os";
|
|
3031
|
-
import path12 from "node:path";
|
|
3032
|
-
function resolveDirs(env = process.env, home = os3.homedir()) {
|
|
3033
|
-
const one = env.CLAW_CONNECTOR_HOME?.trim();
|
|
3034
|
-
if (one) return { config: path12.join(one, "config"), state: path12.join(one, "state") };
|
|
3035
|
-
const cfg = env.XDG_CONFIG_HOME?.trim() || path12.join(home, ".config");
|
|
3036
|
-
const st = env.XDG_STATE_HOME?.trim() || path12.join(home, ".local", "state");
|
|
3037
|
-
return { config: path12.join(cfg, "claw-connector"), state: path12.join(st, "claw-connector") };
|
|
3038
|
-
}
|
|
3039
|
-
var configFile = (d) => path12.join(d.config, "agents.toml");
|
|
3040
|
-
var credentialsFile = (d) => path12.join(d.config, "credentials.json");
|
|
3041
|
-
var identityFile = (d) => path12.join(d.state, "connector.json");
|
|
3042
|
-
var sessionsFile = (d) => path12.join(d.state, "sessions.json");
|
|
3043
|
-
var childrenFile = (d) => path12.join(d.state, "children.json");
|
|
3044
|
-
var pidFile = (d) => path12.join(d.state, "connector.pid");
|
|
3045
|
-
var pauseFile = (d) => path12.join(d.state, "paused");
|
|
3046
|
-
var auditFile = (d) => path12.join(d.state, "audit.jsonl");
|
|
3047
|
-
var outboxDir = (d) => path12.join(d.state, "outbox");
|
|
3048
|
-
var logDir = (d) => path12.join(d.state, "logs");
|
|
3049
|
-
|
|
3050
3198
|
// src/session-store.ts
|
|
3051
|
-
import
|
|
3052
|
-
import
|
|
3199
|
+
import fs13 from "node:fs";
|
|
3200
|
+
import path15 from "node:path";
|
|
3053
3201
|
var SessionStore = class {
|
|
3054
3202
|
constructor(file) {
|
|
3055
3203
|
this.file = file;
|
|
3056
3204
|
this.pendingFile = `${file}.pending`;
|
|
3057
3205
|
try {
|
|
3058
|
-
const raw = JSON.parse(
|
|
3206
|
+
const raw = JSON.parse(fs13.readFileSync(this.pendingFile, "utf8"));
|
|
3059
3207
|
if (Array.isArray(raw)) this.pending = raw.filter((p) => p && typeof p.session_id === "string" && typeof p.action_id === "string");
|
|
3060
3208
|
} catch {
|
|
3061
3209
|
}
|
|
3062
3210
|
try {
|
|
3063
|
-
const raw = JSON.parse(
|
|
3211
|
+
const raw = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
3064
3212
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) this.recs = raw;
|
|
3065
3213
|
} catch {
|
|
3066
3214
|
}
|
|
@@ -3103,19 +3251,19 @@ var SessionStore = class {
|
|
|
3103
3251
|
return out;
|
|
3104
3252
|
}
|
|
3105
3253
|
savePending() {
|
|
3106
|
-
|
|
3254
|
+
fs13.mkdirSync(path15.dirname(this.pendingFile), { recursive: true, mode: 448 });
|
|
3107
3255
|
const tmp = `${this.pendingFile}.${process.pid}.tmp`;
|
|
3108
|
-
|
|
3109
|
-
|
|
3256
|
+
fs13.writeFileSync(tmp, JSON.stringify(this.pending), { mode: 384 });
|
|
3257
|
+
fs13.renameSync(tmp, this.pendingFile);
|
|
3110
3258
|
}
|
|
3111
3259
|
ids() {
|
|
3112
3260
|
return Object.keys(this.recs);
|
|
3113
3261
|
}
|
|
3114
3262
|
save() {
|
|
3115
|
-
|
|
3263
|
+
fs13.mkdirSync(path15.dirname(this.file), { recursive: true, mode: 448 });
|
|
3116
3264
|
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
3117
|
-
|
|
3118
|
-
|
|
3265
|
+
fs13.writeFileSync(tmp, JSON.stringify(this.recs), { mode: 384 });
|
|
3266
|
+
fs13.renameSync(tmp, this.file);
|
|
3119
3267
|
}
|
|
3120
3268
|
};
|
|
3121
3269
|
|
|
@@ -3131,7 +3279,9 @@ var DEFAULT_TIMING = {
|
|
|
3131
3279
|
pausePollMs: 1e3,
|
|
3132
3280
|
catalogMs: 12e4,
|
|
3133
3281
|
attachCliMs: 8e3,
|
|
3134
|
-
recentWriteMs: 1e4
|
|
3282
|
+
recentWriteMs: 1e4,
|
|
3283
|
+
authFailLimit: 4,
|
|
3284
|
+
authFailWindowMs: 12e4
|
|
3135
3285
|
};
|
|
3136
3286
|
var CommandLedger = class {
|
|
3137
3287
|
inflight = /* @__PURE__ */ new Set();
|
|
@@ -3171,6 +3321,7 @@ var CommandLedger = class {
|
|
|
3171
3321
|
return this.highest;
|
|
3172
3322
|
}
|
|
3173
3323
|
};
|
|
3324
|
+
var isAuthRefusal = (e) => e instanceof HttpError && (e.status === 401 || e.status === 403);
|
|
3174
3325
|
function isTransient(e) {
|
|
3175
3326
|
return e instanceof TransientAuthError || e instanceof TransportError || e instanceof HttpError && (e.status >= 500 || e.status === 429);
|
|
3176
3327
|
}
|
|
@@ -3223,6 +3374,13 @@ var Connector = class {
|
|
|
3223
3374
|
/** The connector row itself was revoked (not just the sign-in): recorded in connector.json for `login`. */
|
|
3224
3375
|
rowRevoked = false;
|
|
3225
3376
|
paused = false;
|
|
3377
|
+
/** The credential was refused `authFailLimit` times in a row (see {@link ConnectorTiming.authFailLimit}). */
|
|
3378
|
+
authFailed = false;
|
|
3379
|
+
authFailures = 0;
|
|
3380
|
+
authFirstAt = 0;
|
|
3381
|
+
authWhy = "";
|
|
3382
|
+
/** The last event on the command stream, to tell why the server ended it (`superseded`, `token_expired`, a release). */
|
|
3383
|
+
lastFrame = "";
|
|
3226
3384
|
wakeSender = null;
|
|
3227
3385
|
wakeHeartbeat = null;
|
|
3228
3386
|
streamCtl = null;
|
|
@@ -3255,7 +3413,7 @@ var Connector = class {
|
|
|
3255
3413
|
// --- identity -------------------------------------------------------------------
|
|
3256
3414
|
readIdentity() {
|
|
3257
3415
|
try {
|
|
3258
|
-
const raw = JSON.parse(
|
|
3416
|
+
const raw = JSON.parse(fs14.readFileSync(identityFile(this.d.dirs), "utf8"));
|
|
3259
3417
|
if (isObject(raw) && raw.server === this.d.server && typeof raw.connector_id === "string") {
|
|
3260
3418
|
return { server: raw.server, connector_id: raw.connector_id, cursor: typeof raw.cursor === "number" ? raw.cursor : 0, handled: Array.isArray(raw.handled) ? raw.handled.filter((x) => typeof x === "string") : [] };
|
|
3261
3419
|
}
|
|
@@ -3266,9 +3424,9 @@ var Connector = class {
|
|
|
3266
3424
|
writeIdentity() {
|
|
3267
3425
|
const st = { server: this.d.server, connector_id: this.cid, cursor: this.ledger.cursor, handled: this.ledger.recentIds, ...this.rowRevoked ? { revoked: true } : {} };
|
|
3268
3426
|
const file = identityFile(this.d.dirs);
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3427
|
+
fs14.mkdirSync(this.d.dirs.state, { recursive: true, mode: 448 });
|
|
3428
|
+
fs14.writeFileSync(`${file}.tmp`, JSON.stringify(st), { mode: 384 });
|
|
3429
|
+
fs14.renameSync(`${file}.tmp`, file);
|
|
3272
3430
|
}
|
|
3273
3431
|
/** Use the connector id this machine already has, or register a new one. */
|
|
3274
3432
|
async identify(forceNew = false) {
|
|
@@ -3353,12 +3511,29 @@ var Connector = class {
|
|
|
3353
3511
|
this.streamCtl?.abort();
|
|
3354
3512
|
await this.manager.killAll("revoked");
|
|
3355
3513
|
}
|
|
3514
|
+
/** One more refusal of the credential; the run ends once `authFailLimit` come in a row. Returns whether it ended. */
|
|
3515
|
+
async authRefused(e) {
|
|
3516
|
+
if (this.authFailures++ === 0) this.authFirstAt = Date.now();
|
|
3517
|
+
if (this.authFailed) return true;
|
|
3518
|
+
if (this.authFailures < this.timing.authFailLimit || Date.now() - this.authFirstAt < this.timing.authFailWindowMs) return false;
|
|
3519
|
+
this.authFailed = true;
|
|
3520
|
+
this.authWhy = errMessage(e);
|
|
3521
|
+
this.d.log.error("Claw keeps refusing this sign-in: stopping every agent; run login again", { why: this.authWhy, refusals: this.authFailures });
|
|
3522
|
+
this.d.audit.record("auth_failed", { why: this.authWhy });
|
|
3523
|
+
this.ctl.abort();
|
|
3524
|
+
this.streamCtl?.abort();
|
|
3525
|
+
await this.manager.killAll("stopped");
|
|
3526
|
+
return true;
|
|
3527
|
+
}
|
|
3528
|
+
get authFailure() {
|
|
3529
|
+
return this.authWhy;
|
|
3530
|
+
}
|
|
3356
3531
|
isRevocation(e) {
|
|
3357
3532
|
return e instanceof RevokedError || e instanceof GrantRevokedError;
|
|
3358
3533
|
}
|
|
3359
3534
|
async pauseWatcher() {
|
|
3360
3535
|
while (!this.ctl.signal.aborted) {
|
|
3361
|
-
const want =
|
|
3536
|
+
const want = fs14.existsSync(pauseFile(this.d.dirs));
|
|
3362
3537
|
if (want && !this.paused) {
|
|
3363
3538
|
this.paused = true;
|
|
3364
3539
|
this.d.audit.record("paused");
|
|
@@ -3429,14 +3604,17 @@ var Connector = class {
|
|
|
3429
3604
|
if (this.paused) continue;
|
|
3430
3605
|
try {
|
|
3431
3606
|
await this.heartbeat();
|
|
3607
|
+
this.authFailures = 0;
|
|
3432
3608
|
this.writeIdentity();
|
|
3433
3609
|
} catch (e) {
|
|
3434
3610
|
if (this.isRevocation(e)) return void await this.onRevoked(e);
|
|
3611
|
+
if (isAuthRefusal(e) && await this.authRefused(e)) return;
|
|
3435
3612
|
this.d.log.warn("heartbeat failed", { error: errMessage(e) });
|
|
3436
3613
|
}
|
|
3437
3614
|
}
|
|
3438
3615
|
}
|
|
3439
3616
|
onFrame(f, reconnect) {
|
|
3617
|
+
if (f.event) this.lastFrame = f.event;
|
|
3440
3618
|
switch (f.event) {
|
|
3441
3619
|
case "ready":
|
|
3442
3620
|
if (reconnect) this.manager.resendPending();
|
|
@@ -3485,17 +3663,26 @@ var Connector = class {
|
|
|
3485
3663
|
try {
|
|
3486
3664
|
const sig = AbortSignal.any([this.ctl.signal, this.streamCtl.signal]);
|
|
3487
3665
|
let got = false;
|
|
3666
|
+
this.lastFrame = "";
|
|
3488
3667
|
for await (const f of this.d.client.commands(this.cid, { lastEventId: this.ledger.highestSeen || void 0, signal: sig, watchdogMs: this.timing.watchdogMs })) {
|
|
3489
3668
|
if (!got) {
|
|
3490
3669
|
got = true;
|
|
3491
3670
|
attempt = 0;
|
|
3671
|
+
this.authFailures = 0;
|
|
3492
3672
|
}
|
|
3493
3673
|
this.onFrame(f, reconnect);
|
|
3494
3674
|
if (f.event === "ready") reconnect = true;
|
|
3495
3675
|
}
|
|
3676
|
+
if (!this.ctl.signal.aborted && !this.paused) {
|
|
3677
|
+
const why = this.lastFrame === "superseded" || this.lastFrame === "token_expired" ? this.lastFrame : "the server closed the stream (a Claw release or restart)";
|
|
3678
|
+
this.d.log.info("command stream ended by the server; reconnecting", { why });
|
|
3679
|
+
recordDisconnect(this.d.dirs, { reason: "server_closed", detail: why });
|
|
3680
|
+
}
|
|
3496
3681
|
} catch (e) {
|
|
3497
3682
|
if (this.isRevocation(e)) return void await this.onRevoked(e);
|
|
3498
3683
|
if (this.ctl.signal.aborted) break;
|
|
3684
|
+
if (isAuthRefusal(e) && await this.authRefused(e)) return;
|
|
3685
|
+
recordDisconnect(this.d.dirs, { reason: "stream_lost", detail: errMessage(e) });
|
|
3499
3686
|
if (e instanceof HttpError && e.status === 404) {
|
|
3500
3687
|
try {
|
|
3501
3688
|
await this.identify(true);
|
|
@@ -3514,18 +3701,34 @@ var Connector = class {
|
|
|
3514
3701
|
}
|
|
3515
3702
|
/** Run until stopped or revoked. Always leaves no adapter process behind. */
|
|
3516
3703
|
async run() {
|
|
3517
|
-
|
|
3704
|
+
fs14.mkdirSync(this.d.dirs.state, { recursive: true, mode: 448 });
|
|
3518
3705
|
await this.registry.killAll();
|
|
3519
3706
|
for (const p of this.store.takePending()) {
|
|
3520
3707
|
this.d.audit.record("permission_orphaned", { session_id: p.session_id, action_id: p.action_id });
|
|
3521
3708
|
this.emit(p.session_id, "permission_resolved", { actionId: p.action_id, optionId: null, outcome: "cancelled", by: "connector_restart" });
|
|
3522
3709
|
}
|
|
3523
3710
|
const pidf = pidFile(this.d.dirs);
|
|
3524
|
-
|
|
3525
|
-
|
|
3711
|
+
let leftover = false;
|
|
3712
|
+
try {
|
|
3713
|
+
fs14.readFileSync(pidf);
|
|
3714
|
+
leftover = true;
|
|
3715
|
+
} catch {
|
|
3716
|
+
}
|
|
3717
|
+
if (leftover) {
|
|
3718
|
+
let last;
|
|
3719
|
+
try {
|
|
3720
|
+
last = fs14.statSync(identityFile(this.d.dirs)).mtime.toISOString();
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
recordDisconnect(this.d.dirs, { reason: "process_died", detail: `the previous run ended without shutting down (killed, crashed, or the machine restarted)${last ? `; last heartbeat ${last}` : ""}`, ...last ? { at: last } : {} });
|
|
3724
|
+
}
|
|
3725
|
+
fs14.writeFileSync(pidf, JSON.stringify({ pid: process.pid, start: processStart(process.pid) ?? "" }), { mode: 384 });
|
|
3726
|
+
this.paused = fs14.existsSync(pauseFile(this.d.dirs));
|
|
3727
|
+
let result = "stopped";
|
|
3728
|
+
let failure = "";
|
|
3526
3729
|
try {
|
|
3527
3730
|
const up = await this.startup();
|
|
3528
|
-
if (up !== "ok") return up;
|
|
3731
|
+
if (up !== "ok") return result = up;
|
|
3529
3732
|
this.d.audit.record("connector_online", {
|
|
3530
3733
|
connector_id: this.cid,
|
|
3531
3734
|
agents: this.d.config.agents.map((a) => a.id),
|
|
@@ -3533,11 +3736,17 @@ var Connector = class {
|
|
|
3533
3736
|
unattended_agents: this.d.config.agents.filter((a) => a.unattended).map((a) => a.id)
|
|
3534
3737
|
});
|
|
3535
3738
|
await Promise.all([this.sender(), this.heartbeater(), this.streamLoop(), this.pauseWatcher()]);
|
|
3739
|
+
result = this.revoked ? "revoked" : this.authFailed ? "auth_failed" : "stopped";
|
|
3740
|
+
} catch (e) {
|
|
3741
|
+
failure = errMessage(e);
|
|
3742
|
+
throw e;
|
|
3536
3743
|
} finally {
|
|
3537
3744
|
await this.manager.killAll(this.revoked ? "revoked" : "stopped").catch(() => void 0);
|
|
3538
|
-
|
|
3745
|
+
const reason = this.revoked ? "revoked" : this.authFailed ? "auth_failed" : failure ? "failed" : this.paused ? "stopped_while_paused" : result;
|
|
3746
|
+
recordDisconnect(this.d.dirs, { reason, detail: this.authWhy || failure || void 0 });
|
|
3747
|
+
fs14.rmSync(pidf, { force: true });
|
|
3539
3748
|
}
|
|
3540
|
-
return
|
|
3749
|
+
return result;
|
|
3541
3750
|
}
|
|
3542
3751
|
/** For tests: wait until every event is acknowledged. */
|
|
3543
3752
|
async drained(timeoutMs = 5e3) {
|
|
@@ -3550,11 +3759,432 @@ var Connector = class {
|
|
|
3550
3759
|
}
|
|
3551
3760
|
};
|
|
3552
3761
|
|
|
3762
|
+
// src/service.ts
|
|
3763
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
3764
|
+
import fs15 from "node:fs";
|
|
3765
|
+
import os5 from "node:os";
|
|
3766
|
+
import path16 from "node:path";
|
|
3767
|
+
import { fileURLToPath } from "node:url";
|
|
3768
|
+
import { parseArgs } from "node:util";
|
|
3769
|
+
var SUPERVISORS = ["systemd", "launchd", "script"];
|
|
3770
|
+
var UNIT = "claw-connector.service";
|
|
3771
|
+
var LABEL = "com.primusclaw.connector";
|
|
3772
|
+
var RESTART_SEC = 5;
|
|
3773
|
+
var NEEDS_PERSON = 78;
|
|
3774
|
+
var CARRIED_ENV = [
|
|
3775
|
+
"PATH",
|
|
3776
|
+
"CLAW_CONNECTOR_HOME",
|
|
3777
|
+
"XDG_CONFIG_HOME",
|
|
3778
|
+
"XDG_STATE_HOME",
|
|
3779
|
+
"CLAUDE_CONFIG_DIR",
|
|
3780
|
+
"CODEX_HOME",
|
|
3781
|
+
"NODE_EXTRA_CA_CERTS",
|
|
3782
|
+
"SSL_CERT_FILE",
|
|
3783
|
+
"HTTPS_PROXY",
|
|
3784
|
+
"https_proxy",
|
|
3785
|
+
"HTTP_PROXY",
|
|
3786
|
+
"http_proxy",
|
|
3787
|
+
"NO_PROXY",
|
|
3788
|
+
"no_proxy",
|
|
3789
|
+
"LANG"
|
|
3790
|
+
];
|
|
3791
|
+
var SECRET_NAME = /KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL/i;
|
|
3792
|
+
function deps(ctx) {
|
|
3793
|
+
const d = ctx.service ?? {};
|
|
3794
|
+
return {
|
|
3795
|
+
platform: d.platform ?? process.platform,
|
|
3796
|
+
home: d.home ?? os5.homedir(),
|
|
3797
|
+
uid: d.uid ?? (process.getuid?.() ?? 0),
|
|
3798
|
+
user: d.user ?? (ctx.env.USER || os5.userInfo().username),
|
|
3799
|
+
exec: d.exec ?? ((cmd, args) => {
|
|
3800
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
3801
|
+
return { code: r.error ? null : r.status, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
3802
|
+
}),
|
|
3803
|
+
node: d.node ?? ctx.node ?? process.execPath,
|
|
3804
|
+
cli: d.cli ?? fileURLToPath(import.meta.url),
|
|
3805
|
+
systemdBooted: d.systemdBooted ?? (() => fs15.existsSync("/run/systemd/system"))
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
var systemdUnitPath = (ctx, d) => path16.join(ctx.env.XDG_CONFIG_HOME?.trim() || path16.join(d.home, ".config"), "systemd", "user", UNIT);
|
|
3809
|
+
var launchdPlistPath = (d) => path16.join(d.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
3810
|
+
function detectSupervisor(d) {
|
|
3811
|
+
if (d.platform === "darwin") return "launchd";
|
|
3812
|
+
if (!d.systemdBooted()) return "script";
|
|
3813
|
+
const state = d.exec("systemctl", ["--user", "is-system-running"]).out.trim();
|
|
3814
|
+
return ["running", "degraded", "starting", "initializing"].includes(state) ? "systemd" : "script";
|
|
3815
|
+
}
|
|
3816
|
+
var hasUserinfo = (v) => {
|
|
3817
|
+
try {
|
|
3818
|
+
const u = new URL(v);
|
|
3819
|
+
return u.username !== "" || u.password !== "";
|
|
3820
|
+
} catch {
|
|
3821
|
+
return /\/\/[^/]*@/.test(v);
|
|
3822
|
+
}
|
|
3823
|
+
};
|
|
3824
|
+
function carriedEnv(env, extra = []) {
|
|
3825
|
+
const vars = [];
|
|
3826
|
+
const refused = [];
|
|
3827
|
+
const withheld = [];
|
|
3828
|
+
for (const name of /* @__PURE__ */ new Set([...CARRIED_ENV, ...extra])) {
|
|
3829
|
+
if (!CARRIED_ENV.includes(name) && SECRET_NAME.test(name)) {
|
|
3830
|
+
refused.push(name);
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
const v = env[name];
|
|
3834
|
+
if (v === void 0 || v === "") continue;
|
|
3835
|
+
if (/proxy$/i.test(name) && hasUserinfo(v) && !extra.includes(name)) {
|
|
3836
|
+
withheld.push(name);
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
vars.push([name, v]);
|
|
3840
|
+
}
|
|
3841
|
+
return { vars, refused, withheld };
|
|
3842
|
+
}
|
|
3843
|
+
var sdEnv = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
|
|
3844
|
+
var sdQuote = (s) => sdEnv(s).replace(/\$/g, "$$$$");
|
|
3845
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3846
|
+
var sh = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3847
|
+
function systemdUnit(d, vars) {
|
|
3848
|
+
return `# Written by "claw-connector service install"; remove with "claw-connector service uninstall".
|
|
3849
|
+
[Unit]
|
|
3850
|
+
Description=Claw connector (claw-connector run)
|
|
3851
|
+
Wants=network-online.target
|
|
3852
|
+
After=network-online.target
|
|
3853
|
+
# Never give up: a machine that was offline for an hour must still come back by itself.
|
|
3854
|
+
StartLimitIntervalSec=0
|
|
3855
|
+
|
|
3856
|
+
[Service]
|
|
3857
|
+
Type=simple
|
|
3858
|
+
ExecStart=${sdQuote(d.node)} ${sdQuote(d.cli)} run --supervised
|
|
3859
|
+
${vars.map(([k, v]) => `Environment=${sdEnv(`${k}=${v}`)}`).join("\n")}
|
|
3860
|
+
Restart=on-failure
|
|
3861
|
+
RestartSec=${RESTART_SEC}
|
|
3862
|
+
# 78: revoked, sign-in refused, or no usable config. Restarting cannot help; "claw-connector login" can.
|
|
3863
|
+
RestartPreventExitStatus=${NEEDS_PERSON}
|
|
3864
|
+
# "claw-connector stop" and "systemctl --user stop" both end it cleanly (exit 0), which is not restarted.
|
|
3865
|
+
KillMode=mixed
|
|
3866
|
+
TimeoutStopSec=20
|
|
3867
|
+
|
|
3868
|
+
[Install]
|
|
3869
|
+
WantedBy=default.target
|
|
3870
|
+
`;
|
|
3871
|
+
}
|
|
3872
|
+
function launchdPlist(d, vars, log) {
|
|
3873
|
+
const env = [...vars, ["CLAW_CONNECTOR_SUPERVISOR", "launchd"]];
|
|
3874
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3875
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3876
|
+
<!-- Written by "claw-connector service install"; remove with "claw-connector service uninstall". -->
|
|
3877
|
+
<plist version="1.0">
|
|
3878
|
+
<dict>
|
|
3879
|
+
<key>Label</key><string>${LABEL}</string>
|
|
3880
|
+
<key>ProgramArguments</key>
|
|
3881
|
+
<array><string>${xml(d.node)}</string><string>${xml(d.cli)}</string><string>run</string><string>--supervised</string></array>
|
|
3882
|
+
<key>EnvironmentVariables</key>
|
|
3883
|
+
<dict>
|
|
3884
|
+
${env.map(([k, v]) => ` <key>${xml(k)}</key><string>${xml(v)}</string>`).join("\n")}
|
|
3885
|
+
</dict>
|
|
3886
|
+
<key>RunAtLoad</key><true/>
|
|
3887
|
+
<!-- Restarted after any exit but a clean one. After a revocation a restarted run sees needs-login.json and exits 0. -->
|
|
3888
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
3889
|
+
<key>ThrottleInterval</key><integer>${RESTART_SEC}</integer>
|
|
3890
|
+
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
3891
|
+
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
3892
|
+
</dict>
|
|
3893
|
+
</plist>
|
|
3894
|
+
`;
|
|
3895
|
+
}
|
|
3896
|
+
function keepaliveScript(d, vars) {
|
|
3897
|
+
return `#!/bin/sh
|
|
3898
|
+
# Written by "claw-connector service install --supervisor script". Idempotent: starts the connector's supervisor
|
|
3899
|
+
# unless it is already running, and does nothing after a revocation until "claw-connector login".
|
|
3900
|
+
# Call it from the container's startup hook and every 5 minutes, for example:
|
|
3901
|
+
# @reboot sh ${"$"}HOME/.../keepalive.sh */5 * * * * sh ${"$"}HOME/.../keepalive.sh
|
|
3902
|
+
${vars.map(([k, v]) => `export ${k}=${sh(v)}`).join("\n")}
|
|
3903
|
+
exec ${sh(d.node)} ${sh(d.cli)} service ensure
|
|
3904
|
+
`;
|
|
3905
|
+
}
|
|
3906
|
+
function readPidRec(file) {
|
|
3907
|
+
try {
|
|
3908
|
+
const raw = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
3909
|
+
if (isObject(raw) && typeof raw.pid === "number" && typeof raw.start === "string" && raw.start !== "") return { pid: raw.pid, start: raw.start };
|
|
3910
|
+
} catch {
|
|
3911
|
+
}
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
function supervisorPid(ctx) {
|
|
3915
|
+
const r = readPidRec(supervisorPidFile(ctx.dirs));
|
|
3916
|
+
return r && processStart(r.pid) === r.start ? r.pid : null;
|
|
3917
|
+
}
|
|
3918
|
+
async function supervise(ctx, o = {}) {
|
|
3919
|
+
const d = deps(ctx);
|
|
3920
|
+
const [cmd, args] = o.command ?? [d.node, [d.cli, "run", "--supervised"]];
|
|
3921
|
+
const restartMs = o.restartMs ?? RESTART_SEC * 1e3;
|
|
3922
|
+
const pidf = supervisorPidFile(ctx.dirs);
|
|
3923
|
+
const other = supervisorPid(ctx);
|
|
3924
|
+
if (other !== null && other !== process.pid) {
|
|
3925
|
+
ctx.err(`a supervisor is already running (pid ${other})`);
|
|
3926
|
+
return 1;
|
|
3927
|
+
}
|
|
3928
|
+
fs15.mkdirSync(ctx.dirs.state, { recursive: true, mode: 448 });
|
|
3929
|
+
fs15.writeFileSync(pidf, JSON.stringify({ pid: process.pid, start: processStart(process.pid) ?? "" }), { mode: 384 });
|
|
3930
|
+
let stopping2 = false;
|
|
3931
|
+
let child = null;
|
|
3932
|
+
const wake = new AbortController();
|
|
3933
|
+
const onSig = (sig) => {
|
|
3934
|
+
stopping2 = true;
|
|
3935
|
+
wake.abort();
|
|
3936
|
+
child?.kill(sig);
|
|
3937
|
+
};
|
|
3938
|
+
const sigs = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
3939
|
+
const handlers = sigs.map((s) => [s, () => onSig(s)]);
|
|
3940
|
+
for (const [s, h] of handlers) process.on(s, h);
|
|
3941
|
+
o.signal?.addEventListener("abort", () => onSig("SIGTERM"), { once: true });
|
|
3942
|
+
try {
|
|
3943
|
+
for (let round = 0; !stopping2; round++) {
|
|
3944
|
+
child = spawn2(cmd, args, { stdio: ["ignore", "inherit", "inherit"], env: ctx.env });
|
|
3945
|
+
const c = child;
|
|
3946
|
+
const { code, signal } = await new Promise((resolve) => {
|
|
3947
|
+
c.once("exit", (code2, signal2) => resolve({ code: code2, signal: signal2 }));
|
|
3948
|
+
c.once("error", (e) => {
|
|
3949
|
+
ctx.err(`supervisor: cannot start the connector: ${errMessage(e)}`);
|
|
3950
|
+
resolve({ code: 1, signal: null });
|
|
3951
|
+
});
|
|
3952
|
+
});
|
|
3953
|
+
child = null;
|
|
3954
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3955
|
+
if (stopping2) return 0;
|
|
3956
|
+
if (code === 0) {
|
|
3957
|
+
ctx.err(`${at} supervisor: the connector stopped on purpose (exit 0); not restarting`);
|
|
3958
|
+
return 0;
|
|
3959
|
+
}
|
|
3960
|
+
if (code === NEEDS_PERSON) {
|
|
3961
|
+
ctx.err(`${at} supervisor: the connector needs a new sign-in (exit 78); not restarting until: claw-connector login`);
|
|
3962
|
+
return NEEDS_PERSON;
|
|
3963
|
+
}
|
|
3964
|
+
const how = signal ? `killed by ${signal}` : `exit ${code}`;
|
|
3965
|
+
if (signal) recordDisconnect(ctx.dirs, { reason: "process_died", detail: `the connector was ${how}; restarted by the supervisor` });
|
|
3966
|
+
ctx.err(`${at} supervisor: the connector ended (${how}); restarting in ${restartMs / 1e3} s`);
|
|
3967
|
+
await sleep(restartMs, wake.signal);
|
|
3968
|
+
}
|
|
3969
|
+
return 0;
|
|
3970
|
+
} finally {
|
|
3971
|
+
for (const [s, h] of handlers) process.off(s, h);
|
|
3972
|
+
const rec = readPidRec(pidf);
|
|
3973
|
+
if (rec?.pid === process.pid) fs15.rmSync(pidf, { force: true });
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
async function cmdSupervise(_args, ctx) {
|
|
3977
|
+
return supervise(ctx);
|
|
3978
|
+
}
|
|
3979
|
+
async function ensure(ctx, d, h) {
|
|
3980
|
+
const sup = supervisorPid(ctx);
|
|
3981
|
+
if (sup !== null) {
|
|
3982
|
+
ctx.out(`supervisor running (pid ${sup})`);
|
|
3983
|
+
return 0;
|
|
3984
|
+
}
|
|
3985
|
+
const problem = h.preflight(ctx);
|
|
3986
|
+
if (problem) {
|
|
3987
|
+
ctx.err(`not starting: ${problem}`);
|
|
3988
|
+
return NEEDS_PERSON;
|
|
3989
|
+
}
|
|
3990
|
+
const run2 = h.runningPid(ctx);
|
|
3991
|
+
if (run2 !== null) {
|
|
3992
|
+
ctx.out(`the connector is running (pid ${run2}) without the supervisor; leaving it`);
|
|
3993
|
+
return 0;
|
|
3994
|
+
}
|
|
3995
|
+
fs15.mkdirSync(ctx.dirs.state, { recursive: true, mode: 448 });
|
|
3996
|
+
const log = fs15.openSync(runLogFile(ctx.dirs), "a", 384);
|
|
3997
|
+
try {
|
|
3998
|
+
const p = spawn2(d.node, [d.cli, "supervise"], { detached: true, cwd: d.home, stdio: ["ignore", log, log], env: ctx.env });
|
|
3999
|
+
p.unref();
|
|
4000
|
+
ctx.out(`started the supervisor (pid ${p.pid}); log: ${runLogFile(ctx.dirs)}`);
|
|
4001
|
+
} finally {
|
|
4002
|
+
fs15.closeSync(log);
|
|
4003
|
+
}
|
|
4004
|
+
return 0;
|
|
4005
|
+
}
|
|
4006
|
+
async function cmdService(args, ctx, h) {
|
|
4007
|
+
const [sub, ...rest] = args;
|
|
4008
|
+
const usage = "usage: claw-connector service install|uninstall|status [--supervisor systemd|launchd|script] [--env NAME ...]";
|
|
4009
|
+
if (!["install", "uninstall", "status", "ensure"].includes(sub ?? "")) {
|
|
4010
|
+
ctx.err(usage);
|
|
4011
|
+
return 2;
|
|
4012
|
+
}
|
|
4013
|
+
const { values } = parseArgs({ args: rest, strict: true, options: { supervisor: { type: "string" }, env: { type: "string", multiple: true }, json: { type: "boolean" } } });
|
|
4014
|
+
const d = deps(ctx);
|
|
4015
|
+
if (sub === "ensure") return ensure(ctx, d, h);
|
|
4016
|
+
let sup;
|
|
4017
|
+
if (values.supervisor !== void 0) {
|
|
4018
|
+
if (!SUPERVISORS.includes(values.supervisor)) {
|
|
4019
|
+
ctx.err(usage);
|
|
4020
|
+
return 2;
|
|
4021
|
+
}
|
|
4022
|
+
sup = values.supervisor;
|
|
4023
|
+
} else {
|
|
4024
|
+
sup = sub === "install" ? detectSupervisor(d) : installedSupervisor(ctx) ?? detectSupervisor(d);
|
|
4025
|
+
}
|
|
4026
|
+
if (sub === "install") return install(ctx, d, h, sup, values.env ?? []);
|
|
4027
|
+
if (sub === "uninstall") return uninstall(ctx, d, sup);
|
|
4028
|
+
return status(ctx, d, h, values.json === true);
|
|
4029
|
+
}
|
|
4030
|
+
function installedSupervisor(ctx) {
|
|
4031
|
+
try {
|
|
4032
|
+
const raw = JSON.parse(fs15.readFileSync(serviceFile(ctx.dirs), "utf8"));
|
|
4033
|
+
if (isObject(raw) && typeof raw.supervisor === "string" && SUPERVISORS.includes(raw.supervisor)) return raw.supervisor;
|
|
4034
|
+
} catch {
|
|
4035
|
+
}
|
|
4036
|
+
return null;
|
|
4037
|
+
}
|
|
4038
|
+
function startAfterLogin(ctx, h) {
|
|
4039
|
+
const sup = installedSupervisor(ctx);
|
|
4040
|
+
if (!sup) return;
|
|
4041
|
+
const d = deps(ctx);
|
|
4042
|
+
if (h.runningPid(ctx) !== null) return;
|
|
4043
|
+
let r;
|
|
4044
|
+
if (sup === "systemd") r = d.exec("systemctl", ["--user", "restart", UNIT]);
|
|
4045
|
+
else if (sup === "launchd") r = d.exec("launchctl", ["kickstart", "-k", `gui/${d.uid}/${LABEL}`]);
|
|
4046
|
+
else {
|
|
4047
|
+
void ensure(ctx, d, h);
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
ctx.out(r.code === 0 ? `started the ${sup} service again` : `could not start the ${sup} service (${r.out || `exit ${r.code}`}); run: claw-connector service install`);
|
|
4051
|
+
}
|
|
4052
|
+
function writeFile600(file, body, mode = 384) {
|
|
4053
|
+
fs15.mkdirSync(path16.dirname(file), { recursive: true, mode: 448 });
|
|
4054
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
4055
|
+
fs15.writeFileSync(tmp, body, { mode });
|
|
4056
|
+
fs15.renameSync(tmp, file);
|
|
4057
|
+
}
|
|
4058
|
+
async function install(ctx, d, h, sup, extra) {
|
|
4059
|
+
const problem = h.preflight(ctx);
|
|
4060
|
+
if (problem) {
|
|
4061
|
+
ctx.err(`cannot install the service yet: ${problem}`);
|
|
4062
|
+
return NEEDS_PERSON;
|
|
4063
|
+
}
|
|
4064
|
+
if (!fs15.existsSync(d.cli)) {
|
|
4065
|
+
ctx.err(`${d.cli} does not exist (a source checkout needs "npm run build" first)`);
|
|
4066
|
+
return 1;
|
|
4067
|
+
}
|
|
4068
|
+
const { vars, refused, withheld } = carriedEnv(ctx.env, extra);
|
|
4069
|
+
for (const r of refused) ctx.err(`--env ${r}: not carried into the service (it looks like a credential)`);
|
|
4070
|
+
for (const w of withheld) ctx.err(`${w}: not carried into the service because it holds a password; if the connector needs it, install again with --env ${w} (it is then written into the 0600 unit file)`);
|
|
4071
|
+
const manual = h.runningPid(ctx);
|
|
4072
|
+
if (manual !== null && supervisorPid(ctx) === null) {
|
|
4073
|
+
await h.stop(ctx);
|
|
4074
|
+
ctx.out(`stopped the connector that was running by hand (pid ${manual}) so the service takes over`);
|
|
4075
|
+
}
|
|
4076
|
+
ctx.out(`carrying into the service: ${vars.map(([k]) => k).join(", ") || "nothing"}`);
|
|
4077
|
+
if (sup === "systemd") {
|
|
4078
|
+
const unit = systemdUnitPath(ctx, d);
|
|
4079
|
+
writeFile600(unit, systemdUnit(d, vars));
|
|
4080
|
+
recordInstalled(ctx, "systemd");
|
|
4081
|
+
if (!d.systemdBooted()) {
|
|
4082
|
+
ctx.err("systemd is not this machine's init (no /run/systemd/system): use --supervisor script");
|
|
4083
|
+
return 1;
|
|
4084
|
+
}
|
|
4085
|
+
for (const a of [["--user", "daemon-reload"], ["--user", "enable", UNIT], ["--user", "restart", UNIT]]) {
|
|
4086
|
+
const r = d.exec("systemctl", a);
|
|
4087
|
+
if (r.code !== 0) {
|
|
4088
|
+
ctx.err(`systemctl ${a.join(" ")} failed: ${r.out || `exit ${r.code}`}`);
|
|
4089
|
+
return 1;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
const st = d.exec("systemctl", ["--user", "is-active", UNIT]).out.trim();
|
|
4093
|
+
if (st !== "active" && st !== "activating") {
|
|
4094
|
+
ctx.err(`the unit did not start (is-active: ${st || "nothing"}); see: journalctl --user -u ${UNIT}`);
|
|
4095
|
+
return 1;
|
|
4096
|
+
}
|
|
4097
|
+
ctx.out(`installed ${unit} and started it (systemctl --user status ${UNIT}; logs: journalctl --user -u ${UNIT})`);
|
|
4098
|
+
const linger = d.exec("loginctl", ["show-user", d.user, "-p", "Linger", "--value"]);
|
|
4099
|
+
if (linger.out.trim() !== "yes") ctx.out(`to keep it running when you are not logged in and start it at boot, run once: loginctl enable-linger ${d.user}`);
|
|
4100
|
+
} else if (sup === "launchd") {
|
|
4101
|
+
const plist = launchdPlistPath(d);
|
|
4102
|
+
writeFile600(plist, launchdPlist(d, vars, runLogFile(ctx.dirs)));
|
|
4103
|
+
recordInstalled(ctx, "launchd");
|
|
4104
|
+
d.exec("launchctl", ["bootout", `gui/${d.uid}/${LABEL}`]);
|
|
4105
|
+
const r = d.exec("launchctl", ["bootstrap", `gui/${d.uid}`, plist]);
|
|
4106
|
+
if (r.code !== 0) {
|
|
4107
|
+
ctx.err(`launchctl bootstrap failed: ${r.out || `exit ${r.code}`}`);
|
|
4108
|
+
return 1;
|
|
4109
|
+
}
|
|
4110
|
+
ctx.out(`installed ${plist} and started it (launchctl print gui/${d.uid}/${LABEL}; log: ${runLogFile(ctx.dirs)})`);
|
|
4111
|
+
} else {
|
|
4112
|
+
const script = keepaliveFile(ctx.dirs);
|
|
4113
|
+
writeFile600(script, keepaliveScript(d, vars), 448);
|
|
4114
|
+
recordInstalled(ctx, "script");
|
|
4115
|
+
const code = await ensure(ctx, d, h);
|
|
4116
|
+
if (code !== 0) return code;
|
|
4117
|
+
ctx.out(`wrote ${script}. This machine has no systemd: the supervisor restarts the connector 5 s after it dies, but`);
|
|
4118
|
+
ctx.out("nothing starts the supervisor after a reboot or a container restart. Add both of these (crontab -e, or your container's startup hook):");
|
|
4119
|
+
ctx.out(` @reboot sh ${script}`);
|
|
4120
|
+
ctx.out(` */5 * * * * sh ${script}`);
|
|
4121
|
+
}
|
|
4122
|
+
return 0;
|
|
4123
|
+
}
|
|
4124
|
+
function recordInstalled(ctx, sup) {
|
|
4125
|
+
writeFile600(serviceFile(ctx.dirs), JSON.stringify({ supervisor: sup, installed_at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
4126
|
+
}
|
|
4127
|
+
async function uninstall(ctx, d, sup) {
|
|
4128
|
+
fs15.rmSync(serviceFile(ctx.dirs), { force: true });
|
|
4129
|
+
if (sup === "systemd") {
|
|
4130
|
+
const unit = systemdUnitPath(ctx, d);
|
|
4131
|
+
d.exec("systemctl", ["--user", "disable", "--now", UNIT]);
|
|
4132
|
+
const had = fs15.existsSync(unit);
|
|
4133
|
+
fs15.rmSync(unit, { force: true });
|
|
4134
|
+
d.exec("systemctl", ["--user", "daemon-reload"]);
|
|
4135
|
+
ctx.out(had ? `stopped and removed ${unit}` : `no unit at ${unit}`);
|
|
4136
|
+
} else if (sup === "launchd") {
|
|
4137
|
+
const plist = launchdPlistPath(d);
|
|
4138
|
+
d.exec("launchctl", ["bootout", `gui/${d.uid}/${LABEL}`]);
|
|
4139
|
+
const had = fs15.existsSync(plist);
|
|
4140
|
+
fs15.rmSync(plist, { force: true });
|
|
4141
|
+
ctx.out(had ? `stopped and removed ${plist}` : `no LaunchAgent at ${plist}`);
|
|
4142
|
+
} else {
|
|
4143
|
+
const pid = supervisorPid(ctx);
|
|
4144
|
+
if (pid !== null) {
|
|
4145
|
+
process.kill(pid, "SIGTERM");
|
|
4146
|
+
const end = Date.now() + 2e4;
|
|
4147
|
+
while (Date.now() < end && isAlive(pid) && processStart(pid) !== null) await sleep(100);
|
|
4148
|
+
}
|
|
4149
|
+
const script = keepaliveFile(ctx.dirs);
|
|
4150
|
+
const had = fs15.existsSync(script);
|
|
4151
|
+
fs15.rmSync(script, { force: true });
|
|
4152
|
+
ctx.out(`${pid !== null ? `stopped the supervisor (pid ${pid}) and the connector; ` : ""}${had ? `removed ${script}` : "no keep-alive script"}. Remove the @reboot and */5 lines that call it.`);
|
|
4153
|
+
}
|
|
4154
|
+
return 0;
|
|
4155
|
+
}
|
|
4156
|
+
function status(ctx, d, h, json) {
|
|
4157
|
+
const sup = installedSupervisor(ctx);
|
|
4158
|
+
let active = null;
|
|
4159
|
+
if (sup === "systemd") active = d.exec("systemctl", ["--user", "is-active", UNIT]).out.trim() || null;
|
|
4160
|
+
else if (sup === "launchd") active = d.exec("launchctl", ["print", `gui/${d.uid}/${LABEL}`]).code === 0 ? "loaded" : "not loaded";
|
|
4161
|
+
else if (sup === "script") {
|
|
4162
|
+
const p = supervisorPid(ctx);
|
|
4163
|
+
active = p !== null ? `supervisor pid ${p}` : "supervisor not running";
|
|
4164
|
+
}
|
|
4165
|
+
const st = {
|
|
4166
|
+
installed: sup,
|
|
4167
|
+
active,
|
|
4168
|
+
running: h.runningPid(ctx),
|
|
4169
|
+
needs_login: needsLogin(ctx.dirs),
|
|
4170
|
+
recent_disconnects: recentDisconnects(ctx.dirs, 5)
|
|
4171
|
+
};
|
|
4172
|
+
if (json) {
|
|
4173
|
+
ctx.out(JSON.stringify(st, null, 2));
|
|
4174
|
+
return 0;
|
|
4175
|
+
}
|
|
4176
|
+
ctx.out(`service: ${sup ? `${sup} (${active})` : "not installed; run: claw-connector service install"}`);
|
|
4177
|
+
ctx.out(`connector: ${st.running !== null ? `running (pid ${st.running})` : "not running"}`);
|
|
4178
|
+
if (st.needs_login) ctx.out(`problem: needs a new sign-in: ${st.needs_login.reason}; run: claw-connector login`);
|
|
4179
|
+
for (const x of st.recent_disconnects) ctx.out(`down: ${x.at} ${x.reason}${x.detail ? ` (${x.detail})` : ""}`);
|
|
4180
|
+
return 0;
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3553
4183
|
// src/version.ts
|
|
3554
|
-
var VERSION = "0.1.
|
|
4184
|
+
var VERSION = "0.1.3";
|
|
3555
4185
|
|
|
3556
4186
|
// src/commands.ts
|
|
3557
|
-
var EXIT = { ok: 0, failure: 1, usage: 2, revoked:
|
|
4187
|
+
var EXIT = { ok: 0, failure: 1, usage: 2, needsPerson: 78, revoked: 78 };
|
|
3558
4188
|
var HELP = `claw-connector ${VERSION}: connect the coding agents on this machine to Claw (outbound HTTPS only).
|
|
3559
4189
|
|
|
3560
4190
|
Usage: claw-connector <command> [options]
|
|
@@ -3570,8 +4200,13 @@ Usage: claw-connector <command> [options]
|
|
|
3570
4200
|
directories, else $CLAUDE_CONFIG_DIR or ~/.claude); only its own entries are ever changed
|
|
3571
4201
|
login sign in (device flow); --server URL --identity-url URL [--name HOST]
|
|
3572
4202
|
development only: --key <api key> (or CLAW_CONNECTOR_KEY)
|
|
3573
|
-
run connect and serve (Ctrl-C or SIGTERM stops it and every agent it started)
|
|
3574
|
-
|
|
4203
|
+
run connect and serve (Ctrl-C or SIGTERM stops it and every agent it started). Exit code 78:
|
|
4204
|
+
revoked, sign-in refused, or no usable config; starting it again will not help
|
|
4205
|
+
service install|uninstall|status [--supervisor systemd|launchd|script] [--env NAME ...]
|
|
4206
|
+
keep "run" up: started at login/boot, restarted 5 s after it dies, never restarted
|
|
4207
|
+
after exit 78. systemd --user on Linux, launchd on macOS; "script" (containers without
|
|
4208
|
+
systemd) writes a keep-alive script for a startup hook and a 5-minute cron line
|
|
4209
|
+
status [--json] what is running, signed in, paused, the last disconnect and why
|
|
3575
4210
|
pause stop every agent now and stay offline until "resume"
|
|
3576
4211
|
resume undo "pause"
|
|
3577
4212
|
stop stop the running connector and every agent it started
|
|
@@ -3582,8 +4217,9 @@ Files: ${"$"}XDG_CONFIG_HOME/claw-connector (agents.toml, credentials.json) and
|
|
|
3582
4217
|
(audit.jsonl, logs, outbox); set CLAW_CONNECTOR_HOME to keep everything under one directory.
|
|
3583
4218
|
`;
|
|
3584
4219
|
function loadCfg(ctx) {
|
|
3585
|
-
return loadConfig(configFile(ctx.dirs), { hostname: ctx.hostname ??
|
|
4220
|
+
return loadConfig(configFile(ctx.dirs), { hostname: ctx.hostname ?? os6.hostname() });
|
|
3586
4221
|
}
|
|
4222
|
+
var noAsk = (ctx, cfg) => noAskWarnings(cfg.agents, ctx.env, { managedFile: ctx.claudeManagedSettings, euid: ctx.euid });
|
|
3587
4223
|
function serverUrl(ctx, flag, cfg, creds) {
|
|
3588
4224
|
const v = flag ?? ctx.env.CLAW_URL ?? cfg?.server ?? creds?.server ?? null;
|
|
3589
4225
|
return v ? secureUrl(v.trim(), "the Claw server URL") : null;
|
|
@@ -3609,8 +4245,14 @@ async function main(argv, ctx) {
|
|
|
3609
4245
|
return cmdHooks(rest, ctx);
|
|
3610
4246
|
case "login":
|
|
3611
4247
|
return await cmdLogin(rest, ctx);
|
|
3612
|
-
case "run":
|
|
3613
|
-
|
|
4248
|
+
case "run": {
|
|
4249
|
+
const code = await cmdRun(rest, ctx);
|
|
4250
|
+
return code === EXIT.needsPerson && ctx.env.CLAW_CONNECTOR_SUPERVISOR === "launchd" ? EXIT.ok : code;
|
|
4251
|
+
}
|
|
4252
|
+
case "service":
|
|
4253
|
+
return await cmdService(rest, ctx, serviceHelpers);
|
|
4254
|
+
case "supervise":
|
|
4255
|
+
return await cmdSupervise(rest, ctx);
|
|
3614
4256
|
case "status":
|
|
3615
4257
|
return cmdStatus(rest, ctx);
|
|
3616
4258
|
case "pause":
|
|
@@ -3652,16 +4294,16 @@ function parseApproval(values) {
|
|
|
3652
4294
|
return out;
|
|
3653
4295
|
}
|
|
3654
4296
|
async function cmdInit(args, ctx) {
|
|
3655
|
-
const { values } =
|
|
4297
|
+
const { values } = parseArgs2({ args, strict: true, options: { approval: { type: "string", multiple: true }, "no-hooks": { type: "boolean" } } });
|
|
3656
4298
|
const approval = parseApproval(values.approval ?? []);
|
|
3657
4299
|
if (typeof approval === "string") {
|
|
3658
4300
|
ctx.err(approval);
|
|
3659
4301
|
return EXIT.usage;
|
|
3660
4302
|
}
|
|
3661
4303
|
const file = configFile(ctx.dirs);
|
|
3662
|
-
|
|
4304
|
+
fs16.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
|
|
3663
4305
|
try {
|
|
3664
|
-
|
|
4306
|
+
fs16.writeFileSync(file, sampleConfig(approval), { mode: 384, flag: "wx" });
|
|
3665
4307
|
ctx.out(`wrote ${file}: edit the roots (the directories an agent may work in), then run: claw-connector login`);
|
|
3666
4308
|
const { unattended, parked } = sampleApprovals(approval);
|
|
3667
4309
|
ctx.out(unattended.length ? `no per-step approval for: ${unattended.join(", ")}. Those agents write files and run commands without asking; the phone shows such a session as unattended. To go back, set approval = "ask" on the agent.` : `every agent in this file asks before it writes a file or runs a command. To let phone sessions run like your terminal instead: claw-connector init --approval never (on a new file), or set approval = "never" on an agent.`);
|
|
@@ -3672,7 +4314,7 @@ async function cmdInit(args, ctx) {
|
|
|
3672
4314
|
}
|
|
3673
4315
|
if (!values["no-hooks"]) {
|
|
3674
4316
|
const dir = defaultClaudeDir(ctx);
|
|
3675
|
-
if (!
|
|
4317
|
+
if (!fs16.existsSync(dir)) ctx.out(`no Claude Code directory at ${dir}; once you use Claude Code, run: claw-connector hooks install`);
|
|
3676
4318
|
else installForInit(ctx, dir);
|
|
3677
4319
|
}
|
|
3678
4320
|
return EXIT.ok;
|
|
@@ -3684,19 +4326,19 @@ function installForInit(ctx, dir) {
|
|
|
3684
4326
|
ctx.err(`hooks: ${errMessage(e)} (everything else is set up; fix it and run: claw-connector hooks install)`);
|
|
3685
4327
|
}
|
|
3686
4328
|
}
|
|
3687
|
-
var defaultClaudeDir = (ctx) =>
|
|
4329
|
+
var defaultClaudeDir = (ctx) => path17.resolve(ctx.env.CLAUDE_CONFIG_DIR?.trim() || path17.join(os6.homedir(), ".claude"));
|
|
3688
4330
|
function claudeDirs(ctx, flag) {
|
|
3689
|
-
if (flag !== void 0) return [
|
|
4331
|
+
if (flag !== void 0) return [path17.resolve(flag)];
|
|
3690
4332
|
let dirs = [];
|
|
3691
4333
|
try {
|
|
3692
|
-
dirs = loadCfg(ctx).agents.filter((a) => a.attach).map((a) =>
|
|
4334
|
+
dirs = loadCfg(ctx).agents.filter((a) => a.attach).map((a) => path17.resolve(a.env.CLAUDE_CONFIG_DIR ?? ctx.env.CLAUDE_CONFIG_DIR?.trim() ?? path17.join(os6.homedir(), ".claude")));
|
|
3693
4335
|
} catch {
|
|
3694
4336
|
}
|
|
3695
4337
|
return dirs.length ? [...new Set(dirs)] : [defaultClaudeDir(ctx)];
|
|
3696
4338
|
}
|
|
3697
4339
|
function hookScript(ctx) {
|
|
3698
|
-
const script = ctx.hookScript ??
|
|
3699
|
-
if (!
|
|
4340
|
+
const script = ctx.hookScript ?? fileURLToPath2(new URL("./terminal-hook.js", import.meta.url));
|
|
4341
|
+
if (!fs16.existsSync(script)) throw new HooksError(`${script} does not exist (a source checkout needs "npm run build" first)`);
|
|
3700
4342
|
return script;
|
|
3701
4343
|
}
|
|
3702
4344
|
function installOne(ctx, dir) {
|
|
@@ -3709,14 +4351,14 @@ function cmdHooks(args, ctx) {
|
|
|
3709
4351
|
ctx.err("usage: claw-connector hooks install|uninstall|status [--config-dir DIR]");
|
|
3710
4352
|
return EXIT.usage;
|
|
3711
4353
|
}
|
|
3712
|
-
const { values } =
|
|
4354
|
+
const { values } = parseArgs2({ args: rest, strict: true, options: { "config-dir": { type: "string" }, json: { type: "boolean" } } });
|
|
3713
4355
|
const dirs = claudeDirs(ctx, values["config-dir"]);
|
|
3714
4356
|
let code = EXIT.ok;
|
|
3715
4357
|
const report = [];
|
|
3716
4358
|
for (const dir of dirs) {
|
|
3717
4359
|
try {
|
|
3718
4360
|
if (sub === "install") {
|
|
3719
|
-
if (values["config-dir"] === void 0 && !
|
|
4361
|
+
if (values["config-dir"] === void 0 && !fs16.existsSync(dir)) {
|
|
3720
4362
|
ctx.err(`no Claude Code directory at ${dir}; pass --config-dir to name one`);
|
|
3721
4363
|
code = EXIT.failure;
|
|
3722
4364
|
continue;
|
|
@@ -3741,14 +4383,14 @@ function forgetRevokedIdentity(ctx) {
|
|
|
3741
4383
|
const file = identityFile(ctx.dirs);
|
|
3742
4384
|
let revoked = false;
|
|
3743
4385
|
try {
|
|
3744
|
-
const raw = JSON.parse(
|
|
4386
|
+
const raw = JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
3745
4387
|
revoked = isObject(raw) && raw.revoked === true;
|
|
3746
4388
|
} catch {
|
|
3747
4389
|
}
|
|
3748
|
-
if (revoked)
|
|
4390
|
+
if (revoked) fs16.rmSync(file, { force: true });
|
|
3749
4391
|
}
|
|
3750
4392
|
async function cmdLogin(args, ctx) {
|
|
3751
|
-
const { values } =
|
|
4393
|
+
const { values } = parseArgs2({
|
|
3752
4394
|
args,
|
|
3753
4395
|
strict: true,
|
|
3754
4396
|
options: { server: { type: "string" }, "identity-url": { type: "string" }, key: { type: "string" }, name: { type: "string" } }
|
|
@@ -3769,7 +4411,9 @@ async function cmdLogin(args, ctx) {
|
|
|
3769
4411
|
if (key) {
|
|
3770
4412
|
saveCredentials(file, { v: 1, server, kind: "key", key, saved_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3771
4413
|
forgetRevokedIdentity(ctx);
|
|
4414
|
+
clearNeedsLogin(ctx.dirs);
|
|
3772
4415
|
ctx.out("saved a development key. This is for trying things out: a device sign-in is the normal way (omit --key).");
|
|
4416
|
+
startAfterLogin(ctx, serviceHelpers);
|
|
3773
4417
|
return EXIT.ok;
|
|
3774
4418
|
}
|
|
3775
4419
|
const identityRaw = (values["identity-url"] ?? ctx.env.CLAW_IDENTITY_URL ?? cfg?.identityUrl ?? "").trim();
|
|
@@ -3778,7 +4422,7 @@ async function cmdLogin(args, ctx) {
|
|
|
3778
4422
|
ctx.err("login needs --identity-url <identity service URL> (or CLAW_IDENTITY_URL, or identity_url = in agents.toml)");
|
|
3779
4423
|
return EXIT.usage;
|
|
3780
4424
|
}
|
|
3781
|
-
const deviceName = values.name ?? cfg?.name ?? ctx.hostname ??
|
|
4425
|
+
const deviceName = values.name ?? cfg?.name ?? ctx.hostname ?? os6.hostname();
|
|
3782
4426
|
const tok = await deviceLogin({ identityUrl, deviceName, fetch: ctx.fetch, print: ctx.out });
|
|
3783
4427
|
saveCredentials(file, {
|
|
3784
4428
|
v: 1,
|
|
@@ -3791,27 +4435,47 @@ async function cmdLogin(args, ctx) {
|
|
|
3791
4435
|
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3792
4436
|
});
|
|
3793
4437
|
forgetRevokedIdentity(ctx);
|
|
3794
|
-
ctx.
|
|
4438
|
+
clearNeedsLogin(ctx.dirs);
|
|
4439
|
+
ctx.out(`signed in as ${deviceName}. Next: claw-connector service install (or run)`);
|
|
4440
|
+
startAfterLogin(ctx, serviceHelpers);
|
|
3795
4441
|
return EXIT.ok;
|
|
3796
4442
|
}
|
|
3797
4443
|
function runningPid(ctx) {
|
|
3798
4444
|
try {
|
|
3799
|
-
const raw = JSON.parse(
|
|
4445
|
+
const raw = JSON.parse(fs16.readFileSync(pidFile(ctx.dirs), "utf8"));
|
|
3800
4446
|
if (isObject(raw) && typeof raw.pid === "number" && typeof raw.start === "string" && raw.start !== "" && processStart(raw.pid) === raw.start) return raw.pid;
|
|
3801
4447
|
} catch {
|
|
3802
4448
|
}
|
|
3803
4449
|
return null;
|
|
3804
4450
|
}
|
|
3805
|
-
async function cmdRun(
|
|
3806
|
-
|
|
4451
|
+
async function cmdRun(args, ctx) {
|
|
4452
|
+
parseArgs2({ args, strict: true, options: { supervised: { type: "boolean" } } });
|
|
4453
|
+
{
|
|
4454
|
+
const nl = needsLogin(ctx.dirs);
|
|
4455
|
+
if (nl) {
|
|
4456
|
+
ctx.err(`not starting: ${nl.reason}${nl.at ? ` (${nl.at})` : ""}. Sign in again (claw-connector login); the service then starts it.`);
|
|
4457
|
+
return EXIT.needsPerson;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
let cfg;
|
|
4461
|
+
let creds;
|
|
4462
|
+
try {
|
|
4463
|
+
cfg = loadCfg(ctx);
|
|
4464
|
+
creds = loadCredentials(credentialsFile(ctx.dirs));
|
|
4465
|
+
} catch (e) {
|
|
4466
|
+
if (e instanceof ConfigError || e instanceof CredentialsError) {
|
|
4467
|
+
ctx.err(e.message);
|
|
4468
|
+
return EXIT.needsPerson;
|
|
4469
|
+
}
|
|
4470
|
+
throw e;
|
|
4471
|
+
}
|
|
3807
4472
|
if (cfg.agents.length === 0) {
|
|
3808
4473
|
ctx.err("agents.toml defines no agents; add an [[agents]] table");
|
|
3809
|
-
return EXIT.
|
|
4474
|
+
return EXIT.needsPerson;
|
|
3810
4475
|
}
|
|
3811
|
-
const creds = loadCredentials(credentialsFile(ctx.dirs));
|
|
3812
4476
|
if (!creds) {
|
|
3813
4477
|
ctx.err("not signed in: run claw-connector login");
|
|
3814
|
-
return EXIT.
|
|
4478
|
+
return EXIT.needsPerson;
|
|
3815
4479
|
}
|
|
3816
4480
|
const running = runningPid(ctx);
|
|
3817
4481
|
if (running !== null) {
|
|
@@ -3826,15 +4490,27 @@ async function cmdRun(_args, ctx) {
|
|
|
3826
4490
|
const connector2 = new Connector({ config: cfg, dirs: ctx.dirs, client: client2, server, audit, log, version: VERSION, timing: ctx.timing });
|
|
3827
4491
|
ctx.onConnector?.(connector2);
|
|
3828
4492
|
ctx.out(`connecting ${cfg.agents.map((a) => a.unattended ? `${a.id} (no per-step approval)` : a.id).join(", ")} to ${server} as "${cfg.name}"`);
|
|
4493
|
+
for (const w of noAsk(ctx, cfg)) ctx.out(`warning: ${w}`);
|
|
3829
4494
|
try {
|
|
3830
4495
|
const result = await connector2.run();
|
|
3831
4496
|
if (result === "revoked") {
|
|
4497
|
+
markNeedsLogin(ctx.dirs, "this connector was revoked");
|
|
3832
4498
|
ctx.err("this connector was revoked: every agent was stopped. Run login again to connect it anew.");
|
|
3833
4499
|
return EXIT.revoked;
|
|
3834
4500
|
}
|
|
4501
|
+
if (result === "auth_failed") {
|
|
4502
|
+
markNeedsLogin(ctx.dirs, `Claw refused the sign-in (${connector2.authFailure})`);
|
|
4503
|
+
ctx.err(`Claw keeps refusing this sign-in (${connector2.authFailure}): every agent was stopped. Run login again.`);
|
|
4504
|
+
return EXIT.needsPerson;
|
|
4505
|
+
}
|
|
3835
4506
|
return EXIT.ok;
|
|
3836
4507
|
} catch (e) {
|
|
3837
|
-
if (e
|
|
4508
|
+
if (isAuthRefusal(e) || e instanceof RevokedError || e instanceof GrantRevokedError || e instanceof CredentialsError) {
|
|
4509
|
+
markNeedsLogin(ctx.dirs, e instanceof GrantRevokedError || e instanceof RevokedError ? e.message : `Claw refused the sign-in (${errMessage(e)})`);
|
|
4510
|
+
ctx.err(`error: ${errMessage(e)}. Run login again.`);
|
|
4511
|
+
return EXIT.needsPerson;
|
|
4512
|
+
}
|
|
4513
|
+
if (e instanceof HttpError || e instanceof TransportError) {
|
|
3838
4514
|
ctx.err(`error: ${e.message}`);
|
|
3839
4515
|
return EXIT.failure;
|
|
3840
4516
|
}
|
|
@@ -3842,7 +4518,7 @@ async function cmdRun(_args, ctx) {
|
|
|
3842
4518
|
}
|
|
3843
4519
|
}
|
|
3844
4520
|
function cmdStatus(args, ctx) {
|
|
3845
|
-
const { values } =
|
|
4521
|
+
const { values } = parseArgs2({ args, strict: true, options: { json: { type: "boolean" } } });
|
|
3846
4522
|
let creds = null;
|
|
3847
4523
|
let credsProblem = null;
|
|
3848
4524
|
try {
|
|
@@ -3853,46 +4529,56 @@ function cmdStatus(args, ctx) {
|
|
|
3853
4529
|
let agents = [];
|
|
3854
4530
|
let unattended = [];
|
|
3855
4531
|
let configProblem = null;
|
|
4532
|
+
let warnings = [];
|
|
3856
4533
|
try {
|
|
3857
4534
|
const cfg = loadCfg(ctx);
|
|
3858
4535
|
agents = cfg.agents.map((a) => a.id);
|
|
3859
4536
|
unattended = cfg.agents.filter((a) => a.unattended).map((a) => a.id);
|
|
4537
|
+
warnings = noAsk(ctx, cfg);
|
|
3860
4538
|
} catch (e) {
|
|
3861
4539
|
configProblem = errMessage(e);
|
|
3862
4540
|
}
|
|
3863
4541
|
let connectorId = null;
|
|
3864
4542
|
try {
|
|
3865
|
-
const raw = JSON.parse(
|
|
4543
|
+
const raw = JSON.parse(fs16.readFileSync(identityFile(ctx.dirs), "utf8"));
|
|
3866
4544
|
if (isObject(raw) && typeof raw.connector_id === "string") connectorId = raw.connector_id;
|
|
3867
4545
|
} catch {
|
|
3868
4546
|
}
|
|
3869
4547
|
const reg = new ChildRegistry(childrenFile(ctx.dirs));
|
|
3870
|
-
const
|
|
4548
|
+
const status2 = {
|
|
3871
4549
|
version: VERSION,
|
|
3872
4550
|
running: runningPid(ctx),
|
|
3873
|
-
paused:
|
|
4551
|
+
paused: fs16.existsSync(pauseFile(ctx.dirs)),
|
|
3874
4552
|
signed_in: creds ? { kind: creds.kind, server: creds.server, device_name: creds.device_name ?? null } : null,
|
|
3875
4553
|
connector_id: connectorId,
|
|
3876
4554
|
agents,
|
|
3877
4555
|
/** Agents whose sessions start without asking before a write or a command. */
|
|
3878
4556
|
unattended,
|
|
3879
4557
|
adapters: reg.list().filter((r) => reg.isOurs(r)).map((r) => ({ pid: r.pid, agent: r.agent_id, session: r.session_id })),
|
|
3880
|
-
problems: [credsProblem, configProblem].filter((x) => !!x)
|
|
4558
|
+
problems: [credsProblem, configProblem].filter((x) => !!x),
|
|
4559
|
+
/** Agents whose sessions will be refused as things stand (Claude's own settings start them in a mode that never asks). */
|
|
4560
|
+
warnings,
|
|
4561
|
+
/** Why the connection last went down, and whether only a new sign-in can bring it back. */
|
|
4562
|
+
last_disconnect: recentDisconnects(ctx.dirs, 1)[0] ?? null,
|
|
4563
|
+
needs_login: needsLogin(ctx.dirs)
|
|
3881
4564
|
};
|
|
3882
4565
|
if (values.json) {
|
|
3883
|
-
ctx.out(JSON.stringify(
|
|
4566
|
+
ctx.out(JSON.stringify(status2, null, 2));
|
|
3884
4567
|
return EXIT.ok;
|
|
3885
4568
|
}
|
|
3886
|
-
ctx.out(`connector: ${
|
|
4569
|
+
ctx.out(`connector: ${status2.running ? `running (pid ${status2.running})` : "not running"}${status2.paused ? ", PAUSED" : ""}`);
|
|
3887
4570
|
ctx.out(`signed in: ${creds ? `${creds.kind} to ${creds.server}` : "no"}`);
|
|
3888
4571
|
ctx.out(`agents: ${agents.map((a) => unattended.includes(a) ? `${a} (no per-step approval)` : a).join(", ") || "none configured"}`);
|
|
3889
|
-
ctx.out(`adapters: ${
|
|
3890
|
-
|
|
4572
|
+
ctx.out(`adapters: ${status2.adapters.length ? status2.adapters.map((a) => `${a.agent} pid ${a.pid}`).join(", ") : "none running"}`);
|
|
4573
|
+
if (status2.last_disconnect) ctx.out(`last down: ${status2.last_disconnect.at} ${status2.last_disconnect.reason}${status2.last_disconnect.detail ? ` (${status2.last_disconnect.detail})` : ""}`);
|
|
4574
|
+
if (status2.needs_login) ctx.out(`problem: needs a new sign-in: ${status2.needs_login.reason}; run: claw-connector login`);
|
|
4575
|
+
for (const p of status2.problems) ctx.out(`problem: ${p}`);
|
|
4576
|
+
for (const w of status2.warnings) ctx.out(`warning: ${w}`);
|
|
3891
4577
|
return EXIT.ok;
|
|
3892
4578
|
}
|
|
3893
4579
|
async function cmdPause(ctx) {
|
|
3894
|
-
|
|
3895
|
-
|
|
4580
|
+
fs16.mkdirSync(ctx.dirs.state, { recursive: true, mode: 448 });
|
|
4581
|
+
fs16.writeFileSync(pauseFile(ctx.dirs), (/* @__PURE__ */ new Date()).toISOString() + "\n", { mode: 384 });
|
|
3896
4582
|
if (runningPid(ctx) === null) {
|
|
3897
4583
|
const left = await new ChildRegistry(childrenFile(ctx.dirs)).killAll();
|
|
3898
4584
|
ctx.out(`paused (the connector is not running${left.killed.length ? `; stopped ${left.killed.length} leftover agent process group(s)` : ""})`);
|
|
@@ -3902,7 +4588,7 @@ async function cmdPause(ctx) {
|
|
|
3902
4588
|
return EXIT.ok;
|
|
3903
4589
|
}
|
|
3904
4590
|
function cmdResume(ctx) {
|
|
3905
|
-
|
|
4591
|
+
fs16.rmSync(pauseFile(ctx.dirs), { force: true });
|
|
3906
4592
|
ctx.out(runningPid(ctx) === null ? "resumed; start it with: claw-connector run" : "resumed: the running connector reconnects now");
|
|
3907
4593
|
return EXIT.ok;
|
|
3908
4594
|
}
|
|
@@ -3942,11 +4628,26 @@ async function cmdLogout(ctx) {
|
|
|
3942
4628
|
ctx.err(`could not revoke the sign-in at the identity service (${errMessage(e)}); it was removed from this machine`);
|
|
3943
4629
|
}
|
|
3944
4630
|
}
|
|
3945
|
-
|
|
4631
|
+
fs16.rmSync(file, { force: true });
|
|
3946
4632
|
forgetRevokedIdentity(ctx);
|
|
3947
4633
|
ctx.out("signed out");
|
|
3948
4634
|
return EXIT.ok;
|
|
3949
4635
|
}
|
|
4636
|
+
var serviceHelpers = {
|
|
4637
|
+
runningPid,
|
|
4638
|
+
stop: (ctx) => cmdStop(ctx, true),
|
|
4639
|
+
preflight: (ctx) => {
|
|
4640
|
+
try {
|
|
4641
|
+
const cfg = loadCfg(ctx);
|
|
4642
|
+
if (cfg.agents.length === 0) return "agents.toml defines no agents; add an [[agents]] table";
|
|
4643
|
+
if (!loadCredentials(credentialsFile(ctx.dirs))) return "not signed in: run claw-connector login";
|
|
4644
|
+
} catch (e) {
|
|
4645
|
+
return errMessage(e);
|
|
4646
|
+
}
|
|
4647
|
+
const nl = needsLogin(ctx.dirs);
|
|
4648
|
+
return nl ? `${nl.reason}: run claw-connector login first` : null;
|
|
4649
|
+
}
|
|
4650
|
+
};
|
|
3950
4651
|
|
|
3951
4652
|
// src/cli.ts
|
|
3952
4653
|
var connector = null;
|