@botcord/daemon 0.2.55 → 0.2.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +25 -1
- package/dist/diagnostics.d.ts +30 -0
- package/dist/diagnostics.js +286 -0
- package/dist/gateway/gateway.d.ts +6 -0
- package/dist/gateway/gateway.js +8 -0
- package/dist/gateway/runtimes/claude-code.js +79 -5
- package/dist/gateway/runtimes/codex.js +67 -8
- package/dist/index.js +16 -1
- package/dist/provision.js +68 -0
- package/dist/working-memory.js +5 -0
- package/package.json +1 -1
- package/src/__tests__/diagnostics.test.ts +46 -0
- package/src/__tests__/provision.test.ts +45 -0
- package/src/__tests__/working-memory.test.ts +9 -1
- package/src/daemon.ts +25 -1
- package/src/diagnostics.ts +348 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +35 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +80 -4
- package/src/gateway/gateway.ts +9 -0
- package/src/gateway/runtimes/claude-code.ts +76 -4
- package/src/gateway/runtimes/codex.ts +66 -11
- package/src/index.ts +17 -1
- package/src/provision.ts +86 -0
- package/src/working-memory.ts +5 -0
package/dist/daemon.js
CHANGED
|
@@ -18,6 +18,7 @@ import { composeBotCordUserTurn } from "./turn-text.js";
|
|
|
18
18
|
import { UserAuthManager } from "./user-auth.js";
|
|
19
19
|
import { PolicyResolver } from "./gateway/policy-resolver.js";
|
|
20
20
|
import { scanMention } from "./mention-scan.js";
|
|
21
|
+
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
21
22
|
/**
|
|
22
23
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
23
24
|
* tasks routinely exceed 10 minutes, so daemon-hosted agents get a larger
|
|
@@ -401,7 +402,30 @@ export async function startDaemon(opts) {
|
|
|
401
402
|
const provisioner = createProvisioner({ gateway, policyResolver, onAgentInstalled });
|
|
402
403
|
controlChannel = new ControlChannel({
|
|
403
404
|
auth: userAuth,
|
|
404
|
-
handle:
|
|
405
|
+
handle: async (frame) => {
|
|
406
|
+
if (frame.type === "collect_diagnostics") {
|
|
407
|
+
logger.info("diagnostics: collect requested", { frameId: frame.id });
|
|
408
|
+
const bundle = await createDiagnosticBundle();
|
|
409
|
+
const upload = await uploadDiagnosticBundle({ auth: userAuth, bundle });
|
|
410
|
+
logger.info("diagnostics: uploaded", {
|
|
411
|
+
frameId: frame.id,
|
|
412
|
+
bundleId: upload.bundleId,
|
|
413
|
+
sizeBytes: upload.sizeBytes,
|
|
414
|
+
localPath: bundle.path,
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
ok: true,
|
|
418
|
+
result: {
|
|
419
|
+
bundle_id: upload.bundleId,
|
|
420
|
+
filename: upload.filename,
|
|
421
|
+
size_bytes: upload.sizeBytes,
|
|
422
|
+
expires_at: upload.expiresAt ?? null,
|
|
423
|
+
local_path: bundle.path,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return provisioner(frame);
|
|
428
|
+
},
|
|
405
429
|
});
|
|
406
430
|
try {
|
|
407
431
|
await controlChannel.start();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type UserAuthManager } from "./user-auth.js";
|
|
2
|
+
declare const DIAGNOSTICS_DIR: string;
|
|
3
|
+
export interface CreateDiagnosticBundleOptions {
|
|
4
|
+
diagnosticsDir?: string;
|
|
5
|
+
logFile?: string;
|
|
6
|
+
configFile?: string;
|
|
7
|
+
snapshotFile?: string;
|
|
8
|
+
doctor?: {
|
|
9
|
+
text: string;
|
|
10
|
+
json: unknown;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface DiagnosticBundleResult {
|
|
14
|
+
path: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
sizeBytes: number;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DiagnosticUploadResult {
|
|
20
|
+
bundleId: string;
|
|
21
|
+
filename: string;
|
|
22
|
+
sizeBytes: number;
|
|
23
|
+
expiresAt?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createDiagnosticBundle(opts?: CreateDiagnosticBundleOptions): Promise<DiagnosticBundleResult>;
|
|
26
|
+
export declare function uploadDiagnosticBundle(opts: {
|
|
27
|
+
auth: UserAuthManager;
|
|
28
|
+
bundle: DiagnosticBundleResult;
|
|
29
|
+
}): Promise<DiagnosticUploadResult>;
|
|
30
|
+
export { DIAGNOSTICS_DIR };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, hostname, platform, release, arch } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { deflateRawSync } from "node:zlib";
|
|
6
|
+
import { AUTH_EXPIRED_FLAG_PATH, USER_AUTH_PATH, } from "./user-auth.js";
|
|
7
|
+
import { CONFIG_FILE_PATH, PID_PATH, SNAPSHOT_PATH, loadConfig, } from "./config.js";
|
|
8
|
+
import { LOG_FILE_PATH } from "./log.js";
|
|
9
|
+
import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
|
|
10
|
+
import { detectRuntimes } from "./adapters/runtimes.js";
|
|
11
|
+
const DIAGNOSTICS_DIR = path.join(homedir(), ".botcord", "diagnostics");
|
|
12
|
+
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
13
|
+
const SECRET_PATTERNS = [
|
|
14
|
+
[/(Authorization:\s*Bearer\s+)[^\s"']+/gi, "$1[REDACTED]"],
|
|
15
|
+
[/("?(?:accessToken|access_token|refreshToken|refresh_token|token|privateKey|private_key|secret)"?\s*:\s*")[^"]+(")/gi, "$1[REDACTED]$2"],
|
|
16
|
+
[/(drt_)[A-Za-z0-9_-]+/g, "$1[REDACTED]"],
|
|
17
|
+
[/(dit_)[A-Za-z0-9_-]+/g, "$1[REDACTED]"],
|
|
18
|
+
[/([?&](?:token|access_token|refresh_token|install_token)=)[^&\s"']+/gi, "$1[REDACTED]"],
|
|
19
|
+
];
|
|
20
|
+
function redact(input) {
|
|
21
|
+
let out = input;
|
|
22
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
23
|
+
out = out.replace(pattern, replacement);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function safeReadText(file) {
|
|
28
|
+
if (!existsSync(file))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
return redact(readFileSync(file, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return `read failed: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function readUserAuthSummary() {
|
|
38
|
+
const raw = safeReadText(USER_AUTH_PATH);
|
|
39
|
+
if (!raw)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return {
|
|
44
|
+
userId: typeof parsed.userId === "string" ? parsed.userId : null,
|
|
45
|
+
daemonInstanceId: typeof parsed.daemonInstanceId === "string" ? parsed.daemonInstanceId : null,
|
|
46
|
+
hubUrl: typeof parsed.hubUrl === "string" ? parsed.hubUrl : null,
|
|
47
|
+
expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
|
|
48
|
+
loggedInAt: typeof parsed.loggedInAt === "string" ? parsed.loggedInAt : null,
|
|
49
|
+
label: typeof parsed.label === "string" ? parsed.label : null,
|
|
50
|
+
authExpiredFlagPresent: existsSync(AUTH_EXPIRED_FLAG_PATH),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
error: `user-auth summary failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
|
+
authExpiredFlagPresent: existsSync(AUTH_EXPIRED_FLAG_PATH),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const fsFileReader = {
|
|
61
|
+
readFile(p) {
|
|
62
|
+
if (!existsSync(p))
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
return readFileSync(p, "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
async function buildDoctorEntries() {
|
|
73
|
+
const entries = detectRuntimes();
|
|
74
|
+
let channels = [];
|
|
75
|
+
let cfgForEndpoints = null;
|
|
76
|
+
try {
|
|
77
|
+
cfgForEndpoints = loadConfig();
|
|
78
|
+
channels = channelsFromDaemonConfig(cfgForEndpoints);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
channels = [];
|
|
82
|
+
}
|
|
83
|
+
if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
|
|
84
|
+
const { collectRuntimeSnapshotAsync } = await import("./provision.js");
|
|
85
|
+
const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
|
|
86
|
+
const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
const r = byId.get(e.id);
|
|
89
|
+
if (r?.endpoints)
|
|
90
|
+
e.endpoints = r.endpoints;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const input = await runDoctor(entries, channels, {
|
|
94
|
+
credentialsPath: (accountId) => path.join(homedir(), ".botcord", "credentials", `${accountId}.json`),
|
|
95
|
+
fileReader: fsFileReader,
|
|
96
|
+
fetcher: defaultHttpFetcher,
|
|
97
|
+
timeoutMs: 5_000,
|
|
98
|
+
});
|
|
99
|
+
return { text: renderDoctor(input), json: input };
|
|
100
|
+
}
|
|
101
|
+
function crc32(buf) {
|
|
102
|
+
let crc = 0xffffffff;
|
|
103
|
+
for (const b of buf) {
|
|
104
|
+
crc ^= b;
|
|
105
|
+
for (let i = 0; i < 8; i += 1) {
|
|
106
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
function dosDateTime(date) {
|
|
112
|
+
const year = Math.max(1980, date.getFullYear());
|
|
113
|
+
return {
|
|
114
|
+
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
115
|
+
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function u16(n) {
|
|
119
|
+
const b = Buffer.alloc(2);
|
|
120
|
+
b.writeUInt16LE(n & 0xffff, 0);
|
|
121
|
+
return b;
|
|
122
|
+
}
|
|
123
|
+
function u32(n) {
|
|
124
|
+
const b = Buffer.alloc(4);
|
|
125
|
+
b.writeUInt32LE(n >>> 0, 0);
|
|
126
|
+
return b;
|
|
127
|
+
}
|
|
128
|
+
function createZip(entries) {
|
|
129
|
+
const localParts = [];
|
|
130
|
+
const centralParts = [];
|
|
131
|
+
let offset = 0;
|
|
132
|
+
const now = new Date();
|
|
133
|
+
const dt = dosDateTime(now);
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const name = Buffer.from(entry.name.replace(/^\/+/, ""), "utf8");
|
|
136
|
+
const data = Buffer.isBuffer(entry.data)
|
|
137
|
+
? entry.data
|
|
138
|
+
: Buffer.from(entry.data, "utf8");
|
|
139
|
+
const compressed = deflateRawSync(data, { level: 9 });
|
|
140
|
+
const crc = crc32(data);
|
|
141
|
+
const local = Buffer.concat([
|
|
142
|
+
u32(0x04034b50),
|
|
143
|
+
u16(20),
|
|
144
|
+
u16(0),
|
|
145
|
+
u16(8),
|
|
146
|
+
u16(dt.time),
|
|
147
|
+
u16(dt.date),
|
|
148
|
+
u32(crc),
|
|
149
|
+
u32(compressed.length),
|
|
150
|
+
u32(data.length),
|
|
151
|
+
u16(name.length),
|
|
152
|
+
u16(0),
|
|
153
|
+
name,
|
|
154
|
+
compressed,
|
|
155
|
+
]);
|
|
156
|
+
localParts.push(local);
|
|
157
|
+
centralParts.push(Buffer.concat([
|
|
158
|
+
u32(0x02014b50),
|
|
159
|
+
u16(20),
|
|
160
|
+
u16(20),
|
|
161
|
+
u16(0),
|
|
162
|
+
u16(8),
|
|
163
|
+
u16(dt.time),
|
|
164
|
+
u16(dt.date),
|
|
165
|
+
u32(crc),
|
|
166
|
+
u32(compressed.length),
|
|
167
|
+
u32(data.length),
|
|
168
|
+
u16(name.length),
|
|
169
|
+
u16(0),
|
|
170
|
+
u16(0),
|
|
171
|
+
u16(0),
|
|
172
|
+
u16(0),
|
|
173
|
+
u32(0),
|
|
174
|
+
u32(offset),
|
|
175
|
+
name,
|
|
176
|
+
]));
|
|
177
|
+
offset += local.length;
|
|
178
|
+
}
|
|
179
|
+
const central = Buffer.concat(centralParts);
|
|
180
|
+
const end = Buffer.concat([
|
|
181
|
+
u32(0x06054b50),
|
|
182
|
+
u16(0),
|
|
183
|
+
u16(0),
|
|
184
|
+
u16(entries.length),
|
|
185
|
+
u16(entries.length),
|
|
186
|
+
u32(central.length),
|
|
187
|
+
u32(offset),
|
|
188
|
+
u16(0),
|
|
189
|
+
]);
|
|
190
|
+
return Buffer.concat([...localParts, central, end]);
|
|
191
|
+
}
|
|
192
|
+
export async function createDiagnosticBundle(opts = {}) {
|
|
193
|
+
const createdAt = new Date();
|
|
194
|
+
const stamp = createdAt.toISOString().replace(/[:.]/g, "-");
|
|
195
|
+
const filename = `botcord-daemon-diagnostics-${stamp}.zip`;
|
|
196
|
+
const diagnosticsDir = opts.diagnosticsDir ?? DIAGNOSTICS_DIR;
|
|
197
|
+
const logFile = opts.logFile ?? LOG_FILE_PATH;
|
|
198
|
+
const configFile = opts.configFile ?? CONFIG_FILE_PATH;
|
|
199
|
+
const snapshotFile = opts.snapshotFile ?? SNAPSHOT_PATH;
|
|
200
|
+
mkdirSync(diagnosticsDir, { recursive: true, mode: 0o700 });
|
|
201
|
+
const doctor = opts.doctor ?? await buildDoctorEntries();
|
|
202
|
+
const status = {
|
|
203
|
+
createdAt: createdAt.toISOString(),
|
|
204
|
+
host: hostname(),
|
|
205
|
+
platform: platform(),
|
|
206
|
+
release: release(),
|
|
207
|
+
arch: arch(),
|
|
208
|
+
node: process.version,
|
|
209
|
+
pidPath: PID_PATH,
|
|
210
|
+
pid: process.pid,
|
|
211
|
+
configPath: configFile,
|
|
212
|
+
snapshotPath: snapshotFile,
|
|
213
|
+
logPath: logFile,
|
|
214
|
+
diagnosticsDir,
|
|
215
|
+
userAuth: readUserAuthSummary(),
|
|
216
|
+
};
|
|
217
|
+
const entries = [
|
|
218
|
+
{ name: "README.txt", data: "BotCord daemon diagnostics bundle. Sensitive tokens are redacted before packaging.\n" },
|
|
219
|
+
{ name: "status.json", data: JSON.stringify(status, null, 2) + "\n" },
|
|
220
|
+
{ name: "doctor.txt", data: doctor.text + "\n" },
|
|
221
|
+
{ name: "doctor.json", data: JSON.stringify(doctor.json, null, 2) + "\n" },
|
|
222
|
+
];
|
|
223
|
+
const log = safeReadText(logFile);
|
|
224
|
+
entries.push({
|
|
225
|
+
name: "daemon.log",
|
|
226
|
+
data: log ?? `no log file at ${logFile}\n`,
|
|
227
|
+
});
|
|
228
|
+
const config = safeReadText(configFile);
|
|
229
|
+
entries.push({
|
|
230
|
+
name: "config.json.redacted",
|
|
231
|
+
data: config ?? `no config file at ${configFile}\n`,
|
|
232
|
+
});
|
|
233
|
+
const snapshot = safeReadText(snapshotFile);
|
|
234
|
+
entries.push({
|
|
235
|
+
name: "snapshot.json",
|
|
236
|
+
data: snapshot ?? `no snapshot file at ${snapshotFile}\n`,
|
|
237
|
+
});
|
|
238
|
+
const zip = createZip(entries);
|
|
239
|
+
const out = path.join(diagnosticsDir, filename);
|
|
240
|
+
writeFileSync(out, zip, { mode: 0o600 });
|
|
241
|
+
return {
|
|
242
|
+
path: out,
|
|
243
|
+
filename,
|
|
244
|
+
sizeBytes: zip.length,
|
|
245
|
+
createdAt: createdAt.toISOString(),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export async function uploadDiagnosticBundle(opts) {
|
|
249
|
+
const record = opts.auth.current;
|
|
250
|
+
if (!record)
|
|
251
|
+
throw new Error("daemon not logged in");
|
|
252
|
+
const data = readFileSync(opts.bundle.path);
|
|
253
|
+
if (data.length > MAX_UPLOAD_BYTES) {
|
|
254
|
+
throw new Error(`diagnostic bundle is too large (${data.length} bytes, max ${MAX_UPLOAD_BYTES})`);
|
|
255
|
+
}
|
|
256
|
+
const token = await opts.auth.ensureAccessToken();
|
|
257
|
+
const url = `${record.hubUrl.replace(/\/+$/, "")}/daemon/diagnostics/upload`;
|
|
258
|
+
const resp = await fetch(url, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: {
|
|
261
|
+
Authorization: `Bearer ${token}`,
|
|
262
|
+
"Content-Type": "application/zip",
|
|
263
|
+
"X-BotCord-Filename": opts.bundle.filename,
|
|
264
|
+
},
|
|
265
|
+
body: data,
|
|
266
|
+
});
|
|
267
|
+
const json = await resp.json().catch(() => null);
|
|
268
|
+
if (!resp.ok) {
|
|
269
|
+
const detail = typeof json?.detail === "string"
|
|
270
|
+
? json.detail
|
|
271
|
+
: typeof json?.error === "string"
|
|
272
|
+
? json.error
|
|
273
|
+
: `HTTP ${resp.status}`;
|
|
274
|
+
throw new Error(`diagnostic upload failed: ${detail}`);
|
|
275
|
+
}
|
|
276
|
+
const bundleId = typeof json?.bundle_id === "string" ? json.bundle_id : null;
|
|
277
|
+
if (!bundleId)
|
|
278
|
+
throw new Error("diagnostic upload response missing bundle_id");
|
|
279
|
+
return {
|
|
280
|
+
bundleId,
|
|
281
|
+
filename: typeof json?.filename === "string" ? json.filename : opts.bundle.filename,
|
|
282
|
+
sizeBytes: typeof json?.size_bytes === "number" ? json.size_bytes : data.length,
|
|
283
|
+
...(typeof json?.expires_at === "string" ? { expiresAt: json.expires_at } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
export { DIAGNOSTICS_DIR };
|
|
@@ -117,4 +117,10 @@ export declare class Gateway {
|
|
|
117
117
|
* No-op on unknown id.
|
|
118
118
|
*/
|
|
119
119
|
removeChannel(id: string, reason?: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Inject a daemon-internal inbound message into the normal dispatcher.
|
|
122
|
+
* Control-plane wakeups use this path so scheduled turns share the same
|
|
123
|
+
* routing, queueing, transcript, and runtime behavior as channel messages.
|
|
124
|
+
*/
|
|
125
|
+
injectInbound(message: GatewayInboundMessage): Promise<void>;
|
|
120
126
|
}
|
package/dist/gateway/gateway.js
CHANGED
|
@@ -166,4 +166,12 @@ export class Gateway {
|
|
|
166
166
|
if (idx >= 0)
|
|
167
167
|
this.config.channels.splice(idx, 1);
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Inject a daemon-internal inbound message into the normal dispatcher.
|
|
171
|
+
* Control-plane wakeups use this path so scheduled turns share the same
|
|
172
|
+
* routing, queueing, transcript, and runtime behavior as channel messages.
|
|
173
|
+
*/
|
|
174
|
+
async injectInbound(message) {
|
|
175
|
+
await this.dispatcher.handle({ message });
|
|
176
|
+
}
|
|
169
177
|
}
|
|
@@ -18,6 +18,79 @@ function isValidClaudeSessionId(sessionId) {
|
|
|
18
18
|
function invalidClaudeSessionIdError() {
|
|
19
19
|
return "claude-code: invalid sessionId (expected non-control text not starting with '-')";
|
|
20
20
|
}
|
|
21
|
+
const CLAUDE_FOREIGN_FLAGS_WITH_VALUE = new Set([
|
|
22
|
+
"--color",
|
|
23
|
+
"--config",
|
|
24
|
+
"--disable",
|
|
25
|
+
"--enable",
|
|
26
|
+
"--image",
|
|
27
|
+
"--local-provider",
|
|
28
|
+
"--output-last-message",
|
|
29
|
+
"--output-schema",
|
|
30
|
+
"--profile",
|
|
31
|
+
"--sandbox",
|
|
32
|
+
"-i",
|
|
33
|
+
"-o",
|
|
34
|
+
"-p",
|
|
35
|
+
"-s",
|
|
36
|
+
]);
|
|
37
|
+
const CLAUDE_FOREIGN_BOOLEAN_FLAGS = new Set([
|
|
38
|
+
"--all",
|
|
39
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
40
|
+
"--ephemeral",
|
|
41
|
+
"--full-auto",
|
|
42
|
+
"--ignore-rules",
|
|
43
|
+
"--ignore-user-config",
|
|
44
|
+
"--json",
|
|
45
|
+
"--last",
|
|
46
|
+
"--oss",
|
|
47
|
+
"--print",
|
|
48
|
+
"--skip-git-repo-check",
|
|
49
|
+
]);
|
|
50
|
+
function extraFlagName(arg) {
|
|
51
|
+
if (!arg.startsWith("-"))
|
|
52
|
+
return arg;
|
|
53
|
+
const eq = arg.indexOf("=");
|
|
54
|
+
return eq === -1 ? arg : arg.slice(0, eq);
|
|
55
|
+
}
|
|
56
|
+
function nextExtraValue(args, index) {
|
|
57
|
+
const next = args[index + 1];
|
|
58
|
+
if (typeof next !== "string")
|
|
59
|
+
return undefined;
|
|
60
|
+
if (!next.startsWith("-"))
|
|
61
|
+
return next;
|
|
62
|
+
return /^-\d/.test(next) ? next : undefined;
|
|
63
|
+
}
|
|
64
|
+
function sanitizeClaudeExtraArgs(extraArgs) {
|
|
65
|
+
if (!extraArgs?.length)
|
|
66
|
+
return [];
|
|
67
|
+
const out = [];
|
|
68
|
+
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
69
|
+
const arg = extraArgs[i];
|
|
70
|
+
const name = extraFlagName(arg);
|
|
71
|
+
if (arg === "-c") {
|
|
72
|
+
const value = nextExtraValue(extraArgs, i);
|
|
73
|
+
if (value !== undefined)
|
|
74
|
+
i += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (name === "--config" || name === "--sandbox") {
|
|
78
|
+
if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
|
|
79
|
+
i += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (CLAUDE_FOREIGN_FLAGS_WITH_VALUE.has(name)) {
|
|
83
|
+
if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
|
|
84
|
+
i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (CLAUDE_FOREIGN_BOOLEAN_FLAGS.has(name)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out.push(arg);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
21
94
|
/** Resolve the Claude Code CLI path on PATH or the macOS desktop bundle fallback. */
|
|
22
95
|
export function resolveClaudeCommand(deps = {}) {
|
|
23
96
|
const onPath = resolveCommandOnPath("claude", deps);
|
|
@@ -75,11 +148,12 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
|
75
148
|
return this.resolvedBinary;
|
|
76
149
|
}
|
|
77
150
|
buildArgs(opts) {
|
|
151
|
+
const extraArgs = sanitizeClaudeExtraArgs(opts.extraArgs);
|
|
78
152
|
const args = ["-p", opts.text, "--output-format", "stream-json", "--verbose"];
|
|
79
153
|
// Headless `-p` mode does not load project `.claude/` by default, so
|
|
80
154
|
// per-agent skills seeded at `<workspace>/.claude/skills/` are invisible
|
|
81
155
|
// unless we opt in. `extraArgs` wins so operators can still override.
|
|
82
|
-
if (!
|
|
156
|
+
if (!extraArgs.some((a) => a.startsWith("--setting-sources"))) {
|
|
83
157
|
args.push("--setting-sources", "project");
|
|
84
158
|
}
|
|
85
159
|
if (opts.sessionId) {
|
|
@@ -93,17 +167,17 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
|
93
167
|
// MCP) because there is no prompt relay back to the user yet. Default to
|
|
94
168
|
// bypassPermissions for every trust tier; operators who need a stricter
|
|
95
169
|
// posture can still override with route/defaultRoute extraArgs.
|
|
96
|
-
if (!
|
|
170
|
+
if (!extraArgs.some((a) => a.startsWith("--permission-mode"))) {
|
|
97
171
|
args.push("--permission-mode", "bypassPermissions");
|
|
98
172
|
}
|
|
99
173
|
// Claude Code's `--append-system-prompt` is applied per invocation and NOT
|
|
100
174
|
// persisted in the resumed session transcript — ideal for memory / digest
|
|
101
175
|
// content that should re-evaluate every turn.
|
|
102
|
-
if (opts.systemContext && !
|
|
176
|
+
if (opts.systemContext && !extraArgs.includes("--append-system-prompt")) {
|
|
103
177
|
args.push("--append-system-prompt", opts.systemContext);
|
|
104
178
|
}
|
|
105
|
-
if (
|
|
106
|
-
args.push(...
|
|
179
|
+
if (extraArgs.length)
|
|
180
|
+
args.push(...extraArgs);
|
|
107
181
|
return args;
|
|
108
182
|
}
|
|
109
183
|
handleEvent(raw, ctx) {
|
|
@@ -8,6 +8,69 @@ import { firstExistingPath, readCommandVersion, resolveCommandOnPath, } from "./
|
|
|
8
8
|
const CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
|
|
9
9
|
/** Codex UUIDv7 / v4 session ids are 36-char dashed hex; reject anything else to keep argv safe. */
|
|
10
10
|
const CODEX_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
11
|
+
const CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE = new Set([
|
|
12
|
+
"--append-system-prompt",
|
|
13
|
+
"--permission-mode",
|
|
14
|
+
]);
|
|
15
|
+
const CODEX_SANDBOX_MODES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
|
16
|
+
function extraFlagName(arg) {
|
|
17
|
+
if (!arg.startsWith("-"))
|
|
18
|
+
return arg;
|
|
19
|
+
const eq = arg.indexOf("=");
|
|
20
|
+
return eq === -1 ? arg : arg.slice(0, eq);
|
|
21
|
+
}
|
|
22
|
+
function nextExtraValue(args, index) {
|
|
23
|
+
const next = args[index + 1];
|
|
24
|
+
if (typeof next !== "string")
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!next.startsWith("-"))
|
|
27
|
+
return next;
|
|
28
|
+
return /^-\d/.test(next) ? next : undefined;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeCodexExtraArgs(extraArgs) {
|
|
31
|
+
if (!extraArgs?.length)
|
|
32
|
+
return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
35
|
+
const arg = extraArgs[i];
|
|
36
|
+
const name = extraFlagName(arg);
|
|
37
|
+
if (CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE.has(name)) {
|
|
38
|
+
if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
|
|
39
|
+
i += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (name === "-s" || name === "--sandbox") {
|
|
43
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : nextExtraValue(extraArgs, i);
|
|
44
|
+
if (!arg.includes("=") && value !== undefined)
|
|
45
|
+
i += 1;
|
|
46
|
+
if (value && CODEX_SANDBOX_MODES.has(value)) {
|
|
47
|
+
out.push("-c", `sandbox_mode="${value}"`);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === "--full-auto") {
|
|
52
|
+
out.push("--dangerously-bypass-approvals-and-sandbox");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
out.push(arg);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function hasCodexSandboxOverride(args) {
|
|
60
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg === "--dangerously-bypass-approvals-and-sandbox" ||
|
|
63
|
+
arg.startsWith("-c sandbox_mode=") ||
|
|
64
|
+
arg.startsWith("-csandbox_mode=") ||
|
|
65
|
+
arg.startsWith("--config=sandbox_mode=")) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if ((arg === "-c" || arg === "--config") && args[i + 1]?.startsWith("sandbox_mode=")) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
11
74
|
/** Resolve the Codex CLI executable via PATH or macOS desktop bundle. */
|
|
12
75
|
export function resolveCodexCommand(deps = {}) {
|
|
13
76
|
const onPath = resolveCommandOnPath("codex", deps);
|
|
@@ -156,6 +219,7 @@ export class CodexAdapter extends NdjsonStreamAdapter {
|
|
|
156
219
|
*/
|
|
157
220
|
buildArgs(opts) {
|
|
158
221
|
const tail = [];
|
|
222
|
+
const extraArgs = sanitizeCodexExtraArgs(opts.extraArgs);
|
|
159
223
|
// Sandbox / approval policy. Expressed as `-c` overrides because
|
|
160
224
|
// `codex exec resume` rejects `-s` / `--full-auto`. `-c` works on both
|
|
161
225
|
// the fresh `exec` and `exec resume` paths.
|
|
@@ -165,18 +229,13 @@ export class CodexAdapter extends NdjsonStreamAdapter {
|
|
|
165
229
|
// relay back to the user yet. Default to bypassing both approvals and the
|
|
166
230
|
// sandbox for every trust tier; operators who need a stricter posture can
|
|
167
231
|
// still override with route/defaultRoute extraArgs.
|
|
168
|
-
const hasSandboxOverride =
|
|
169
|
-
a.startsWith("--sandbox") ||
|
|
170
|
-
a === "--full-auto" ||
|
|
171
|
-
a === "--dangerously-bypass-approvals-and-sandbox" ||
|
|
172
|
-
a.startsWith("-c sandbox_mode=") ||
|
|
173
|
-
a.startsWith("-csandbox_mode=")) ?? false;
|
|
232
|
+
const hasSandboxOverride = hasCodexSandboxOverride(extraArgs);
|
|
174
233
|
if (!hasSandboxOverride) {
|
|
175
234
|
tail.push("-c", 'sandbox_mode="danger-full-access"', "-c", 'approval_policy="never"');
|
|
176
235
|
}
|
|
177
236
|
tail.push("--skip-git-repo-check", "--json");
|
|
178
|
-
if (
|
|
179
|
-
tail.push(...
|
|
237
|
+
if (extraArgs.length)
|
|
238
|
+
tail.push(...extraArgs);
|
|
180
239
|
// `--` separates flags from positionals so a prompt starting with `-`
|
|
181
240
|
// can never be parsed as an option. `systemContext` is NOT prepended to
|
|
182
241
|
// the prompt any more — it lives in `<CODEX_HOME>/AGENTS.md` written by
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { renderStatus } from "./status-render.js";
|
|
|
15
15
|
import { appendNextParam } from "./url-utils.js";
|
|
16
16
|
import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
|
|
17
17
|
import { clearWorkingMemory, readWorkingMemory, resolveMemoryDir, updateWorkingMemory, DEFAULT_SECTION, } from "./working-memory.js";
|
|
18
|
+
import { createDiagnosticBundle } from "./diagnostics.js";
|
|
18
19
|
import { resolveStartAuthAction } from "./start-auth.js";
|
|
19
20
|
import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscoveryConfigEnabled, } from "./openclaw-discovery.js";
|
|
20
21
|
const ADAPTER_LIST = listAdapterIds().join("|");
|
|
@@ -81,7 +82,9 @@ Commands:
|
|
|
81
82
|
route list
|
|
82
83
|
route remove --room <rm_xxx>|--prefix <rm_xxx>
|
|
83
84
|
config Print resolved config
|
|
84
|
-
doctor [--json]
|
|
85
|
+
doctor [--json] [--bundle] Scan local runtimes (${ADAPTER_LIST});
|
|
86
|
+
--bundle also writes a zip under
|
|
87
|
+
~/.botcord/diagnostics/
|
|
85
88
|
memory get [--agent <ag_xxx>] [--json] Show current working memory
|
|
86
89
|
memory set [--agent <ag_xxx>] --goal <text>
|
|
87
90
|
Pin/update the agent's work goal
|
|
@@ -105,6 +108,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
105
108
|
"f",
|
|
106
109
|
"follow",
|
|
107
110
|
"json",
|
|
111
|
+
"bundle",
|
|
108
112
|
"help",
|
|
109
113
|
"h",
|
|
110
114
|
"mentioned",
|
|
@@ -1211,6 +1215,17 @@ const fsFileReader = {
|
|
|
1211
1215
|
},
|
|
1212
1216
|
};
|
|
1213
1217
|
async function cmdDoctor(args) {
|
|
1218
|
+
if (args.flags.bundle === true) {
|
|
1219
|
+
const bundle = await createDiagnosticBundle();
|
|
1220
|
+
if (args.flags.json === true) {
|
|
1221
|
+
console.log(JSON.stringify({ bundle }, null, 2));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
console.log(`diagnostic bundle written: ${bundle.path}`);
|
|
1225
|
+
console.log(`size: ${bundle.sizeBytes} bytes`);
|
|
1226
|
+
console.log("Send this zip file to the BotCord developer/support contact.");
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1214
1229
|
const entries = detectRuntimes();
|
|
1215
1230
|
// Doctor should not hard-fail when no config exists yet; channel probes
|
|
1216
1231
|
// simply produce an empty list in that case.
|