@primusclaw/connector 0.1.0 → 0.1.2
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/LICENSE +37 -37
- package/README.md +438 -419
- package/dist/cli.js +223 -145
- package/package.json +35 -35
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs14 from "node:fs";
|
|
5
|
+
import os5 from "node:os";
|
|
6
|
+
import path15 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
@@ -335,11 +335,11 @@ var RevokedError = class extends Error {
|
|
|
335
335
|
}
|
|
336
336
|
};
|
|
337
337
|
var HttpError = class extends Error {
|
|
338
|
-
constructor(status, code,
|
|
339
|
-
super(`${
|
|
338
|
+
constructor(status, code, path16) {
|
|
339
|
+
super(`${path16} answered ${status} ${code}`);
|
|
340
340
|
this.status = status;
|
|
341
341
|
this.code = code;
|
|
342
|
-
this.path =
|
|
342
|
+
this.path = path16;
|
|
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, path16, body, signal) {
|
|
367
|
+
const url = this.o.server + path16;
|
|
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} ${path16}: ${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, path16);
|
|
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, path16, body) {
|
|
392
|
+
const res = await this.send(method, path16, body);
|
|
393
|
+
if (!res.ok) throw new HttpError(res.status, await errorCode(res), path16);
|
|
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 path16 = `/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 + path16, { 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 ${path16}: ${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, path16);
|
|
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), path16);
|
|
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 fs13 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;
|
|
@@ -1789,7 +1860,7 @@ function fitResumable(base, items, limit = REMOTE_AGENT_LIMITS.maxCapabilitiesBy
|
|
|
1789
1860
|
}
|
|
1790
1861
|
|
|
1791
1862
|
// src/session.ts
|
|
1792
|
-
import
|
|
1863
|
+
import path11 from "node:path";
|
|
1793
1864
|
|
|
1794
1865
|
// src/permissions.ts
|
|
1795
1866
|
import { randomUUID } from "node:crypto";
|
|
@@ -2135,7 +2206,7 @@ var Session = class {
|
|
|
2135
2206
|
sessionId: this.id,
|
|
2136
2207
|
cwd: this.cwd,
|
|
2137
2208
|
hooks: this.hooks(),
|
|
2138
|
-
logFile:
|
|
2209
|
+
logFile: path11.join(this.env.logDir, `adapter-${this.agent.id}.log`),
|
|
2139
2210
|
version: this.env.version
|
|
2140
2211
|
}).catch((e) => {
|
|
2141
2212
|
throw new SessionRefusal("adapter_start_failed", errMessage(e));
|
|
@@ -2835,8 +2906,8 @@ var SessionManager = class {
|
|
|
2835
2906
|
};
|
|
2836
2907
|
|
|
2837
2908
|
// src/outbox.ts
|
|
2838
|
-
import
|
|
2839
|
-
import
|
|
2909
|
+
import fs11 from "node:fs";
|
|
2910
|
+
import path12 from "node:path";
|
|
2840
2911
|
var DROPPABLE_TYPES = /* @__PURE__ */ new Set([
|
|
2841
2912
|
"agent_message_chunk",
|
|
2842
2913
|
"agent_thought_chunk",
|
|
@@ -2863,35 +2934,35 @@ var Outbox = class {
|
|
|
2863
2934
|
this.dir = opts.dir;
|
|
2864
2935
|
this.maxBytes = opts.maxBytesPerSession ?? 5 * 1024 * 1024;
|
|
2865
2936
|
this.now = opts.now ?? Date.now;
|
|
2866
|
-
|
|
2937
|
+
fs11.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
2867
2938
|
this.load();
|
|
2868
2939
|
}
|
|
2869
|
-
eventsFile = (sid) =>
|
|
2870
|
-
metaFile = (sid) =>
|
|
2940
|
+
eventsFile = (sid) => path12.join(this.dir, `${safeName(sid)}.jsonl`);
|
|
2941
|
+
metaFile = (sid) => path12.join(this.dir, `${safeName(sid)}.meta.json`);
|
|
2871
2942
|
writeMeta(sid) {
|
|
2872
2943
|
const tmp = this.metaFile(sid) + ".tmp";
|
|
2873
|
-
|
|
2874
|
-
|
|
2944
|
+
fs11.writeFileSync(tmp, JSON.stringify(this.meta.get(sid)), { mode: 384 });
|
|
2945
|
+
fs11.renameSync(tmp, this.metaFile(sid));
|
|
2875
2946
|
}
|
|
2876
2947
|
/** Rebuild the queue from disk. The highest sequence number ever used is the larger of the meta file and the log itself. */
|
|
2877
2948
|
load() {
|
|
2878
2949
|
const loaded = [];
|
|
2879
2950
|
const bases = /* @__PURE__ */ new Set();
|
|
2880
|
-
for (const name of
|
|
2951
|
+
for (const name of fs11.readdirSync(this.dir)) {
|
|
2881
2952
|
if (name.endsWith(".meta.json")) bases.add(name.slice(0, -".meta.json".length));
|
|
2882
2953
|
else if (name.endsWith(".jsonl")) bases.add(name.slice(0, -".jsonl".length));
|
|
2883
2954
|
}
|
|
2884
2955
|
for (const base of bases) {
|
|
2885
2956
|
let meta = { lastSeq: 0, acked: 0 };
|
|
2886
2957
|
try {
|
|
2887
|
-
meta = JSON.parse(
|
|
2958
|
+
meta = JSON.parse(fs11.readFileSync(path12.join(this.dir, `${base}.meta.json`), "utf8"));
|
|
2888
2959
|
} catch {
|
|
2889
2960
|
}
|
|
2890
2961
|
let sid = null;
|
|
2891
|
-
const file =
|
|
2962
|
+
const file = path12.join(this.dir, `${base}.jsonl`);
|
|
2892
2963
|
let text = "";
|
|
2893
2964
|
try {
|
|
2894
|
-
text =
|
|
2965
|
+
text = fs11.readFileSync(file, "utf8");
|
|
2895
2966
|
} catch {
|
|
2896
2967
|
}
|
|
2897
2968
|
for (const line of text.split("\n")) {
|
|
@@ -2909,8 +2980,8 @@ var Outbox = class {
|
|
|
2909
2980
|
if (sid === null) {
|
|
2910
2981
|
sid = base;
|
|
2911
2982
|
try {
|
|
2912
|
-
if (this.now() -
|
|
2913
|
-
for (const f of [`${base}.meta.json`, `${base}.jsonl`])
|
|
2983
|
+
if (this.now() - fs11.statSync(path12.join(this.dir, `${base}.meta.json`)).mtimeMs > TOMBSTONE_MS) {
|
|
2984
|
+
for (const f of [`${base}.meta.json`, `${base}.jsonl`]) fs11.rmSync(path12.join(this.dir, f), { force: true });
|
|
2914
2985
|
continue;
|
|
2915
2986
|
}
|
|
2916
2987
|
} catch {
|
|
@@ -2932,7 +3003,7 @@ var Outbox = class {
|
|
|
2932
3003
|
meta.lastSeq = ev.seq;
|
|
2933
3004
|
this.meta.set(sessionId, meta);
|
|
2934
3005
|
const line = JSON.stringify(ev);
|
|
2935
|
-
|
|
3006
|
+
fs11.appendFileSync(this.eventsFile(sessionId), line + "\n", { mode: 384 });
|
|
2936
3007
|
if (ev.seq === 1) this.writeMeta(sessionId);
|
|
2937
3008
|
this.queue.push(ev);
|
|
2938
3009
|
this.bytes.set(sessionId, (this.bytes.get(sessionId) ?? 0) + line.length);
|
|
@@ -2961,8 +3032,8 @@ var Outbox = class {
|
|
|
2961
3032
|
this.writeMeta(sessionId);
|
|
2962
3033
|
const mine = this.queue.filter((e) => e.session_id === sessionId);
|
|
2963
3034
|
const tmp = this.eventsFile(sessionId) + ".tmp";
|
|
2964
|
-
|
|
2965
|
-
|
|
3035
|
+
fs11.writeFileSync(tmp, mine.map((e) => JSON.stringify(e) + "\n").join(""), { mode: 384 });
|
|
3036
|
+
fs11.renameSync(tmp, this.eventsFile(sessionId));
|
|
2966
3037
|
}
|
|
2967
3038
|
/** The oldest events not yet acknowledged, in the order they were appended. */
|
|
2968
3039
|
pending(max) {
|
|
@@ -3000,7 +3071,7 @@ var Outbox = class {
|
|
|
3000
3071
|
this.writeMeta(sid);
|
|
3001
3072
|
const left = this.queue.filter((e) => e.session_id === sid);
|
|
3002
3073
|
if (left.length === 0) {
|
|
3003
|
-
|
|
3074
|
+
fs11.writeFileSync(this.eventsFile(sid), "", { mode: 384 });
|
|
3004
3075
|
this.bytes.set(sid, 0);
|
|
3005
3076
|
if (this.closed.has(sid)) this.forget(sid);
|
|
3006
3077
|
} else if (left.length < this.queue.length / 2 || meta.acked > 1e3) {
|
|
@@ -3019,7 +3090,7 @@ var Outbox = class {
|
|
|
3019
3090
|
* repeat. What stays is the small meta file, a tombstone, removed after {@link TOMBSTONE_MS}.
|
|
3020
3091
|
*/
|
|
3021
3092
|
forget(sid) {
|
|
3022
|
-
|
|
3093
|
+
fs11.rmSync(this.eventsFile(sid), { force: true });
|
|
3023
3094
|
if (this.meta.has(sid)) this.writeMeta(sid);
|
|
3024
3095
|
this.bytes.delete(sid);
|
|
3025
3096
|
this.closed.delete(sid);
|
|
@@ -3027,40 +3098,40 @@ var Outbox = class {
|
|
|
3027
3098
|
};
|
|
3028
3099
|
|
|
3029
3100
|
// src/paths.ts
|
|
3030
|
-
import
|
|
3031
|
-
import
|
|
3032
|
-
function resolveDirs(env = process.env, home =
|
|
3101
|
+
import os4 from "node:os";
|
|
3102
|
+
import path13 from "node:path";
|
|
3103
|
+
function resolveDirs(env = process.env, home = os4.homedir()) {
|
|
3033
3104
|
const one = env.CLAW_CONNECTOR_HOME?.trim();
|
|
3034
|
-
if (one) return { config:
|
|
3035
|
-
const cfg = env.XDG_CONFIG_HOME?.trim() ||
|
|
3036
|
-
const st = env.XDG_STATE_HOME?.trim() ||
|
|
3037
|
-
return { config:
|
|
3038
|
-
}
|
|
3039
|
-
var configFile = (d) =>
|
|
3040
|
-
var credentialsFile = (d) =>
|
|
3041
|
-
var identityFile = (d) =>
|
|
3042
|
-
var sessionsFile = (d) =>
|
|
3043
|
-
var childrenFile = (d) =>
|
|
3044
|
-
var pidFile = (d) =>
|
|
3045
|
-
var pauseFile = (d) =>
|
|
3046
|
-
var auditFile = (d) =>
|
|
3047
|
-
var outboxDir = (d) =>
|
|
3048
|
-
var logDir = (d) =>
|
|
3105
|
+
if (one) return { config: path13.join(one, "config"), state: path13.join(one, "state") };
|
|
3106
|
+
const cfg = env.XDG_CONFIG_HOME?.trim() || path13.join(home, ".config");
|
|
3107
|
+
const st = env.XDG_STATE_HOME?.trim() || path13.join(home, ".local", "state");
|
|
3108
|
+
return { config: path13.join(cfg, "claw-connector"), state: path13.join(st, "claw-connector") };
|
|
3109
|
+
}
|
|
3110
|
+
var configFile = (d) => path13.join(d.config, "agents.toml");
|
|
3111
|
+
var credentialsFile = (d) => path13.join(d.config, "credentials.json");
|
|
3112
|
+
var identityFile = (d) => path13.join(d.state, "connector.json");
|
|
3113
|
+
var sessionsFile = (d) => path13.join(d.state, "sessions.json");
|
|
3114
|
+
var childrenFile = (d) => path13.join(d.state, "children.json");
|
|
3115
|
+
var pidFile = (d) => path13.join(d.state, "connector.pid");
|
|
3116
|
+
var pauseFile = (d) => path13.join(d.state, "paused");
|
|
3117
|
+
var auditFile = (d) => path13.join(d.state, "audit.jsonl");
|
|
3118
|
+
var outboxDir = (d) => path13.join(d.state, "outbox");
|
|
3119
|
+
var logDir = (d) => path13.join(d.state, "logs");
|
|
3049
3120
|
|
|
3050
3121
|
// src/session-store.ts
|
|
3051
|
-
import
|
|
3052
|
-
import
|
|
3122
|
+
import fs12 from "node:fs";
|
|
3123
|
+
import path14 from "node:path";
|
|
3053
3124
|
var SessionStore = class {
|
|
3054
3125
|
constructor(file) {
|
|
3055
3126
|
this.file = file;
|
|
3056
3127
|
this.pendingFile = `${file}.pending`;
|
|
3057
3128
|
try {
|
|
3058
|
-
const raw = JSON.parse(
|
|
3129
|
+
const raw = JSON.parse(fs12.readFileSync(this.pendingFile, "utf8"));
|
|
3059
3130
|
if (Array.isArray(raw)) this.pending = raw.filter((p) => p && typeof p.session_id === "string" && typeof p.action_id === "string");
|
|
3060
3131
|
} catch {
|
|
3061
3132
|
}
|
|
3062
3133
|
try {
|
|
3063
|
-
const raw = JSON.parse(
|
|
3134
|
+
const raw = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
3064
3135
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) this.recs = raw;
|
|
3065
3136
|
} catch {
|
|
3066
3137
|
}
|
|
@@ -3103,19 +3174,19 @@ var SessionStore = class {
|
|
|
3103
3174
|
return out;
|
|
3104
3175
|
}
|
|
3105
3176
|
savePending() {
|
|
3106
|
-
|
|
3177
|
+
fs12.mkdirSync(path14.dirname(this.pendingFile), { recursive: true, mode: 448 });
|
|
3107
3178
|
const tmp = `${this.pendingFile}.${process.pid}.tmp`;
|
|
3108
|
-
|
|
3109
|
-
|
|
3179
|
+
fs12.writeFileSync(tmp, JSON.stringify(this.pending), { mode: 384 });
|
|
3180
|
+
fs12.renameSync(tmp, this.pendingFile);
|
|
3110
3181
|
}
|
|
3111
3182
|
ids() {
|
|
3112
3183
|
return Object.keys(this.recs);
|
|
3113
3184
|
}
|
|
3114
3185
|
save() {
|
|
3115
|
-
|
|
3186
|
+
fs12.mkdirSync(path14.dirname(this.file), { recursive: true, mode: 448 });
|
|
3116
3187
|
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
3117
|
-
|
|
3118
|
-
|
|
3188
|
+
fs12.writeFileSync(tmp, JSON.stringify(this.recs), { mode: 384 });
|
|
3189
|
+
fs12.renameSync(tmp, this.file);
|
|
3119
3190
|
}
|
|
3120
3191
|
};
|
|
3121
3192
|
|
|
@@ -3255,7 +3326,7 @@ var Connector = class {
|
|
|
3255
3326
|
// --- identity -------------------------------------------------------------------
|
|
3256
3327
|
readIdentity() {
|
|
3257
3328
|
try {
|
|
3258
|
-
const raw = JSON.parse(
|
|
3329
|
+
const raw = JSON.parse(fs13.readFileSync(identityFile(this.d.dirs), "utf8"));
|
|
3259
3330
|
if (isObject(raw) && raw.server === this.d.server && typeof raw.connector_id === "string") {
|
|
3260
3331
|
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
3332
|
}
|
|
@@ -3266,9 +3337,9 @@ var Connector = class {
|
|
|
3266
3337
|
writeIdentity() {
|
|
3267
3338
|
const st = { server: this.d.server, connector_id: this.cid, cursor: this.ledger.cursor, handled: this.ledger.recentIds, ...this.rowRevoked ? { revoked: true } : {} };
|
|
3268
3339
|
const file = identityFile(this.d.dirs);
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3340
|
+
fs13.mkdirSync(this.d.dirs.state, { recursive: true, mode: 448 });
|
|
3341
|
+
fs13.writeFileSync(`${file}.tmp`, JSON.stringify(st), { mode: 384 });
|
|
3342
|
+
fs13.renameSync(`${file}.tmp`, file);
|
|
3272
3343
|
}
|
|
3273
3344
|
/** Use the connector id this machine already has, or register a new one. */
|
|
3274
3345
|
async identify(forceNew = false) {
|
|
@@ -3358,7 +3429,7 @@ var Connector = class {
|
|
|
3358
3429
|
}
|
|
3359
3430
|
async pauseWatcher() {
|
|
3360
3431
|
while (!this.ctl.signal.aborted) {
|
|
3361
|
-
const want =
|
|
3432
|
+
const want = fs13.existsSync(pauseFile(this.d.dirs));
|
|
3362
3433
|
if (want && !this.paused) {
|
|
3363
3434
|
this.paused = true;
|
|
3364
3435
|
this.d.audit.record("paused");
|
|
@@ -3514,15 +3585,15 @@ var Connector = class {
|
|
|
3514
3585
|
}
|
|
3515
3586
|
/** Run until stopped or revoked. Always leaves no adapter process behind. */
|
|
3516
3587
|
async run() {
|
|
3517
|
-
|
|
3588
|
+
fs13.mkdirSync(this.d.dirs.state, { recursive: true, mode: 448 });
|
|
3518
3589
|
await this.registry.killAll();
|
|
3519
3590
|
for (const p of this.store.takePending()) {
|
|
3520
3591
|
this.d.audit.record("permission_orphaned", { session_id: p.session_id, action_id: p.action_id });
|
|
3521
3592
|
this.emit(p.session_id, "permission_resolved", { actionId: p.action_id, optionId: null, outcome: "cancelled", by: "connector_restart" });
|
|
3522
3593
|
}
|
|
3523
3594
|
const pidf = pidFile(this.d.dirs);
|
|
3524
|
-
|
|
3525
|
-
this.paused =
|
|
3595
|
+
fs13.writeFileSync(pidf, JSON.stringify({ pid: process.pid, start: processStart(process.pid) ?? "" }), { mode: 384 });
|
|
3596
|
+
this.paused = fs13.existsSync(pauseFile(this.d.dirs));
|
|
3526
3597
|
try {
|
|
3527
3598
|
const up = await this.startup();
|
|
3528
3599
|
if (up !== "ok") return up;
|
|
@@ -3535,7 +3606,7 @@ var Connector = class {
|
|
|
3535
3606
|
await Promise.all([this.sender(), this.heartbeater(), this.streamLoop(), this.pauseWatcher()]);
|
|
3536
3607
|
} finally {
|
|
3537
3608
|
await this.manager.killAll(this.revoked ? "revoked" : "stopped").catch(() => void 0);
|
|
3538
|
-
|
|
3609
|
+
fs13.rmSync(pidf, { force: true });
|
|
3539
3610
|
}
|
|
3540
3611
|
return this.revoked ? "revoked" : "stopped";
|
|
3541
3612
|
}
|
|
@@ -3551,7 +3622,7 @@ var Connector = class {
|
|
|
3551
3622
|
};
|
|
3552
3623
|
|
|
3553
3624
|
// src/version.ts
|
|
3554
|
-
var VERSION = "0.1.
|
|
3625
|
+
var VERSION = "0.1.2";
|
|
3555
3626
|
|
|
3556
3627
|
// src/commands.ts
|
|
3557
3628
|
var EXIT = { ok: 0, failure: 1, usage: 2, revoked: 3 };
|
|
@@ -3582,8 +3653,9 @@ Files: ${"$"}XDG_CONFIG_HOME/claw-connector (agents.toml, credentials.json) and
|
|
|
3582
3653
|
(audit.jsonl, logs, outbox); set CLAW_CONNECTOR_HOME to keep everything under one directory.
|
|
3583
3654
|
`;
|
|
3584
3655
|
function loadCfg(ctx) {
|
|
3585
|
-
return loadConfig(configFile(ctx.dirs), { hostname: ctx.hostname ??
|
|
3656
|
+
return loadConfig(configFile(ctx.dirs), { hostname: ctx.hostname ?? os5.hostname() });
|
|
3586
3657
|
}
|
|
3658
|
+
var noAsk = (ctx, cfg) => noAskWarnings(cfg.agents, ctx.env, { managedFile: ctx.claudeManagedSettings, euid: ctx.euid });
|
|
3587
3659
|
function serverUrl(ctx, flag, cfg, creds) {
|
|
3588
3660
|
const v = flag ?? ctx.env.CLAW_URL ?? cfg?.server ?? creds?.server ?? null;
|
|
3589
3661
|
return v ? secureUrl(v.trim(), "the Claw server URL") : null;
|
|
@@ -3659,9 +3731,9 @@ async function cmdInit(args, ctx) {
|
|
|
3659
3731
|
return EXIT.usage;
|
|
3660
3732
|
}
|
|
3661
3733
|
const file = configFile(ctx.dirs);
|
|
3662
|
-
|
|
3734
|
+
fs14.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
|
|
3663
3735
|
try {
|
|
3664
|
-
|
|
3736
|
+
fs14.writeFileSync(file, sampleConfig(approval), { mode: 384, flag: "wx" });
|
|
3665
3737
|
ctx.out(`wrote ${file}: edit the roots (the directories an agent may work in), then run: claw-connector login`);
|
|
3666
3738
|
const { unattended, parked } = sampleApprovals(approval);
|
|
3667
3739
|
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 +3744,7 @@ async function cmdInit(args, ctx) {
|
|
|
3672
3744
|
}
|
|
3673
3745
|
if (!values["no-hooks"]) {
|
|
3674
3746
|
const dir = defaultClaudeDir(ctx);
|
|
3675
|
-
if (!
|
|
3747
|
+
if (!fs14.existsSync(dir)) ctx.out(`no Claude Code directory at ${dir}; once you use Claude Code, run: claw-connector hooks install`);
|
|
3676
3748
|
else installForInit(ctx, dir);
|
|
3677
3749
|
}
|
|
3678
3750
|
return EXIT.ok;
|
|
@@ -3684,19 +3756,19 @@ function installForInit(ctx, dir) {
|
|
|
3684
3756
|
ctx.err(`hooks: ${errMessage(e)} (everything else is set up; fix it and run: claw-connector hooks install)`);
|
|
3685
3757
|
}
|
|
3686
3758
|
}
|
|
3687
|
-
var defaultClaudeDir = (ctx) =>
|
|
3759
|
+
var defaultClaudeDir = (ctx) => path15.resolve(ctx.env.CLAUDE_CONFIG_DIR?.trim() || path15.join(os5.homedir(), ".claude"));
|
|
3688
3760
|
function claudeDirs(ctx, flag) {
|
|
3689
|
-
if (flag !== void 0) return [
|
|
3761
|
+
if (flag !== void 0) return [path15.resolve(flag)];
|
|
3690
3762
|
let dirs = [];
|
|
3691
3763
|
try {
|
|
3692
|
-
dirs = loadCfg(ctx).agents.filter((a) => a.attach).map((a) =>
|
|
3764
|
+
dirs = loadCfg(ctx).agents.filter((a) => a.attach).map((a) => path15.resolve(a.env.CLAUDE_CONFIG_DIR ?? ctx.env.CLAUDE_CONFIG_DIR?.trim() ?? path15.join(os5.homedir(), ".claude")));
|
|
3693
3765
|
} catch {
|
|
3694
3766
|
}
|
|
3695
3767
|
return dirs.length ? [...new Set(dirs)] : [defaultClaudeDir(ctx)];
|
|
3696
3768
|
}
|
|
3697
3769
|
function hookScript(ctx) {
|
|
3698
3770
|
const script = ctx.hookScript ?? fileURLToPath(new URL("./terminal-hook.js", import.meta.url));
|
|
3699
|
-
if (!
|
|
3771
|
+
if (!fs14.existsSync(script)) throw new HooksError(`${script} does not exist (a source checkout needs "npm run build" first)`);
|
|
3700
3772
|
return script;
|
|
3701
3773
|
}
|
|
3702
3774
|
function installOne(ctx, dir) {
|
|
@@ -3716,7 +3788,7 @@ function cmdHooks(args, ctx) {
|
|
|
3716
3788
|
for (const dir of dirs) {
|
|
3717
3789
|
try {
|
|
3718
3790
|
if (sub === "install") {
|
|
3719
|
-
if (values["config-dir"] === void 0 && !
|
|
3791
|
+
if (values["config-dir"] === void 0 && !fs14.existsSync(dir)) {
|
|
3720
3792
|
ctx.err(`no Claude Code directory at ${dir}; pass --config-dir to name one`);
|
|
3721
3793
|
code = EXIT.failure;
|
|
3722
3794
|
continue;
|
|
@@ -3741,11 +3813,11 @@ function forgetRevokedIdentity(ctx) {
|
|
|
3741
3813
|
const file = identityFile(ctx.dirs);
|
|
3742
3814
|
let revoked = false;
|
|
3743
3815
|
try {
|
|
3744
|
-
const raw = JSON.parse(
|
|
3816
|
+
const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
3745
3817
|
revoked = isObject(raw) && raw.revoked === true;
|
|
3746
3818
|
} catch {
|
|
3747
3819
|
}
|
|
3748
|
-
if (revoked)
|
|
3820
|
+
if (revoked) fs14.rmSync(file, { force: true });
|
|
3749
3821
|
}
|
|
3750
3822
|
async function cmdLogin(args, ctx) {
|
|
3751
3823
|
const { values } = parseArgs({
|
|
@@ -3778,7 +3850,7 @@ async function cmdLogin(args, ctx) {
|
|
|
3778
3850
|
ctx.err("login needs --identity-url <identity service URL> (or CLAW_IDENTITY_URL, or identity_url = in agents.toml)");
|
|
3779
3851
|
return EXIT.usage;
|
|
3780
3852
|
}
|
|
3781
|
-
const deviceName = values.name ?? cfg?.name ?? ctx.hostname ??
|
|
3853
|
+
const deviceName = values.name ?? cfg?.name ?? ctx.hostname ?? os5.hostname();
|
|
3782
3854
|
const tok = await deviceLogin({ identityUrl, deviceName, fetch: ctx.fetch, print: ctx.out });
|
|
3783
3855
|
saveCredentials(file, {
|
|
3784
3856
|
v: 1,
|
|
@@ -3796,7 +3868,7 @@ async function cmdLogin(args, ctx) {
|
|
|
3796
3868
|
}
|
|
3797
3869
|
function runningPid(ctx) {
|
|
3798
3870
|
try {
|
|
3799
|
-
const raw = JSON.parse(
|
|
3871
|
+
const raw = JSON.parse(fs14.readFileSync(pidFile(ctx.dirs), "utf8"));
|
|
3800
3872
|
if (isObject(raw) && typeof raw.pid === "number" && typeof raw.start === "string" && raw.start !== "" && processStart(raw.pid) === raw.start) return raw.pid;
|
|
3801
3873
|
} catch {
|
|
3802
3874
|
}
|
|
@@ -3826,6 +3898,7 @@ async function cmdRun(_args, ctx) {
|
|
|
3826
3898
|
const connector2 = new Connector({ config: cfg, dirs: ctx.dirs, client: client2, server, audit, log, version: VERSION, timing: ctx.timing });
|
|
3827
3899
|
ctx.onConnector?.(connector2);
|
|
3828
3900
|
ctx.out(`connecting ${cfg.agents.map((a) => a.unattended ? `${a.id} (no per-step approval)` : a.id).join(", ")} to ${server} as "${cfg.name}"`);
|
|
3901
|
+
for (const w of noAsk(ctx, cfg)) ctx.out(`warning: ${w}`);
|
|
3829
3902
|
try {
|
|
3830
3903
|
const result = await connector2.run();
|
|
3831
3904
|
if (result === "revoked") {
|
|
@@ -3853,16 +3926,18 @@ function cmdStatus(args, ctx) {
|
|
|
3853
3926
|
let agents = [];
|
|
3854
3927
|
let unattended = [];
|
|
3855
3928
|
let configProblem = null;
|
|
3929
|
+
let warnings = [];
|
|
3856
3930
|
try {
|
|
3857
3931
|
const cfg = loadCfg(ctx);
|
|
3858
3932
|
agents = cfg.agents.map((a) => a.id);
|
|
3859
3933
|
unattended = cfg.agents.filter((a) => a.unattended).map((a) => a.id);
|
|
3934
|
+
warnings = noAsk(ctx, cfg);
|
|
3860
3935
|
} catch (e) {
|
|
3861
3936
|
configProblem = errMessage(e);
|
|
3862
3937
|
}
|
|
3863
3938
|
let connectorId = null;
|
|
3864
3939
|
try {
|
|
3865
|
-
const raw = JSON.parse(
|
|
3940
|
+
const raw = JSON.parse(fs14.readFileSync(identityFile(ctx.dirs), "utf8"));
|
|
3866
3941
|
if (isObject(raw) && typeof raw.connector_id === "string") connectorId = raw.connector_id;
|
|
3867
3942
|
} catch {
|
|
3868
3943
|
}
|
|
@@ -3870,14 +3945,16 @@ function cmdStatus(args, ctx) {
|
|
|
3870
3945
|
const status = {
|
|
3871
3946
|
version: VERSION,
|
|
3872
3947
|
running: runningPid(ctx),
|
|
3873
|
-
paused:
|
|
3948
|
+
paused: fs14.existsSync(pauseFile(ctx.dirs)),
|
|
3874
3949
|
signed_in: creds ? { kind: creds.kind, server: creds.server, device_name: creds.device_name ?? null } : null,
|
|
3875
3950
|
connector_id: connectorId,
|
|
3876
3951
|
agents,
|
|
3877
3952
|
/** Agents whose sessions start without asking before a write or a command. */
|
|
3878
3953
|
unattended,
|
|
3879
3954
|
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)
|
|
3955
|
+
problems: [credsProblem, configProblem].filter((x) => !!x),
|
|
3956
|
+
/** Agents whose sessions will be refused as things stand (Claude's own settings start them in a mode that never asks). */
|
|
3957
|
+
warnings
|
|
3881
3958
|
};
|
|
3882
3959
|
if (values.json) {
|
|
3883
3960
|
ctx.out(JSON.stringify(status, null, 2));
|
|
@@ -3888,11 +3965,12 @@ function cmdStatus(args, ctx) {
|
|
|
3888
3965
|
ctx.out(`agents: ${agents.map((a) => unattended.includes(a) ? `${a} (no per-step approval)` : a).join(", ") || "none configured"}`);
|
|
3889
3966
|
ctx.out(`adapters: ${status.adapters.length ? status.adapters.map((a) => `${a.agent} pid ${a.pid}`).join(", ") : "none running"}`);
|
|
3890
3967
|
for (const p of status.problems) ctx.out(`problem: ${p}`);
|
|
3968
|
+
for (const w of status.warnings) ctx.out(`warning: ${w}`);
|
|
3891
3969
|
return EXIT.ok;
|
|
3892
3970
|
}
|
|
3893
3971
|
async function cmdPause(ctx) {
|
|
3894
|
-
|
|
3895
|
-
|
|
3972
|
+
fs14.mkdirSync(ctx.dirs.state, { recursive: true, mode: 448 });
|
|
3973
|
+
fs14.writeFileSync(pauseFile(ctx.dirs), (/* @__PURE__ */ new Date()).toISOString() + "\n", { mode: 384 });
|
|
3896
3974
|
if (runningPid(ctx) === null) {
|
|
3897
3975
|
const left = await new ChildRegistry(childrenFile(ctx.dirs)).killAll();
|
|
3898
3976
|
ctx.out(`paused (the connector is not running${left.killed.length ? `; stopped ${left.killed.length} leftover agent process group(s)` : ""})`);
|
|
@@ -3902,7 +3980,7 @@ async function cmdPause(ctx) {
|
|
|
3902
3980
|
return EXIT.ok;
|
|
3903
3981
|
}
|
|
3904
3982
|
function cmdResume(ctx) {
|
|
3905
|
-
|
|
3983
|
+
fs14.rmSync(pauseFile(ctx.dirs), { force: true });
|
|
3906
3984
|
ctx.out(runningPid(ctx) === null ? "resumed; start it with: claw-connector run" : "resumed: the running connector reconnects now");
|
|
3907
3985
|
return EXIT.ok;
|
|
3908
3986
|
}
|
|
@@ -3942,7 +4020,7 @@ async function cmdLogout(ctx) {
|
|
|
3942
4020
|
ctx.err(`could not revoke the sign-in at the identity service (${errMessage(e)}); it was removed from this machine`);
|
|
3943
4021
|
}
|
|
3944
4022
|
}
|
|
3945
|
-
|
|
4023
|
+
fs14.rmSync(file, { force: true });
|
|
3946
4024
|
forgetRevokedIdentity(ctx);
|
|
3947
4025
|
ctx.out("signed out");
|
|
3948
4026
|
return EXIT.ok;
|