@bman654/clodex 1.3.0 → 2.1.0
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 +1 -1
- package/dist/chunk-QLGIKUKC.js +1171 -0
- package/dist/chunk-QLGIKUKC.js.map +1 -0
- package/dist/claude-wrapper.js +4 -27
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +447 -886
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +3 -0
- package/package.json +1 -1
- package/dist/chunk-ZN5X7YFE.js +0 -544
- package/dist/chunk-ZN5X7YFE.js.map +0 -1
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server-runtime.ts
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync
|
|
13
|
+
} from "fs";
|
|
14
|
+
import { dirname, join as join2 } from "path";
|
|
15
|
+
|
|
16
|
+
// src/paths.ts
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var APP_DIR_NAME = "clodex";
|
|
20
|
+
function userHome(env = process.env) {
|
|
21
|
+
return env.HOME ?? env.USERPROFILE ?? homedir();
|
|
22
|
+
}
|
|
23
|
+
function resolveAppHomeOverride(env = process.env) {
|
|
24
|
+
const override = env.CLODEX_HOME;
|
|
25
|
+
return override?.trim() || void 0;
|
|
26
|
+
}
|
|
27
|
+
function getAppHome(env = process.env) {
|
|
28
|
+
const override = resolveAppHomeOverride(env);
|
|
29
|
+
if (override) return override;
|
|
30
|
+
return join(userHome(env), `.${APP_DIR_NAME}`);
|
|
31
|
+
}
|
|
32
|
+
function getConfigPath(env = process.env) {
|
|
33
|
+
return join(getAppHome(env), "config.json");
|
|
34
|
+
}
|
|
35
|
+
function getProvidersPath(env = process.env) {
|
|
36
|
+
return join(getAppHome(env), "providers.json");
|
|
37
|
+
}
|
|
38
|
+
function getCredentialCleanupPath(env = process.env) {
|
|
39
|
+
return join(getAppHome(env), "credential-cleanup.json");
|
|
40
|
+
}
|
|
41
|
+
function getLogsPath(env = process.env) {
|
|
42
|
+
return join(getAppHome(env), "logs");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/server-runtime.ts
|
|
46
|
+
function getServerRuntimePath(env = process.env) {
|
|
47
|
+
return join2(getAppHome(env), "server-runtime.json");
|
|
48
|
+
}
|
|
49
|
+
function getServerRuntimeLockPath(env = process.env) {
|
|
50
|
+
return join2(getAppHome(env), "server-runtime.lock");
|
|
51
|
+
}
|
|
52
|
+
function isDiscoveryDisabled(flag, env = process.env) {
|
|
53
|
+
if (flag !== void 0) return flag;
|
|
54
|
+
const raw = env.CLODEX_NO_DISCOVERY?.trim().toLowerCase();
|
|
55
|
+
return raw === "1" || raw === "true";
|
|
56
|
+
}
|
|
57
|
+
function isPort(value) {
|
|
58
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
59
|
+
}
|
|
60
|
+
function parseServerRuntimeRecord(value) {
|
|
61
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
62
|
+
const record = value;
|
|
63
|
+
const mode = record["mode"];
|
|
64
|
+
if (mode !== "endpoint" && mode !== "proxy") return null;
|
|
65
|
+
if (!isPort(record["port"])) return null;
|
|
66
|
+
const pid = record["pid"];
|
|
67
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return null;
|
|
68
|
+
const startedAt = typeof record["startedAt"] === "string" ? record["startedAt"] : "";
|
|
69
|
+
const caPath = record["caPath"];
|
|
70
|
+
if (mode === "proxy") {
|
|
71
|
+
if (typeof caPath !== "string" || !caPath.trim()) return null;
|
|
72
|
+
return { mode, port: record["port"], pid, caPath, startedAt };
|
|
73
|
+
}
|
|
74
|
+
return { mode, port: record["port"], pid, startedAt };
|
|
75
|
+
}
|
|
76
|
+
function parseServerRuntimeStates(raw) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
84
|
+
const states = [];
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
const state = parseServerRuntimeRecord(item);
|
|
87
|
+
if (state) states.push(state);
|
|
88
|
+
}
|
|
89
|
+
return states;
|
|
90
|
+
}
|
|
91
|
+
function isPidAlive(pid, kill = process.kill.bind(process)) {
|
|
92
|
+
try {
|
|
93
|
+
kill(pid, 0);
|
|
94
|
+
return true;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return err?.code === "EPERM";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
var RUNTIME_LOCK_STALE_MS = 1e4;
|
|
100
|
+
var RUNTIME_LOCK_WAIT_MS = 500;
|
|
101
|
+
var RUNTIME_LOCK_RETRY_MS = 25;
|
|
102
|
+
function tryAcquireRuntimeLock(lockPath, opts = {}) {
|
|
103
|
+
const now = opts.now ?? Date.now();
|
|
104
|
+
const alive = opts.isAlive ?? isPidAlive;
|
|
105
|
+
mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
|
|
106
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
const fd = openSync(lockPath, "wx");
|
|
109
|
+
const content = { pid: process.pid, startedAt: now };
|
|
110
|
+
writeFileSync(fd, JSON.stringify(content));
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
return () => {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(lockPath);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
let stale = false;
|
|
120
|
+
try {
|
|
121
|
+
const existing = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
122
|
+
stale = !existing.pid || !alive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > RUNTIME_LOCK_STALE_MS;
|
|
123
|
+
} catch {
|
|
124
|
+
stale = true;
|
|
125
|
+
}
|
|
126
|
+
if (!stale) return null;
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(lockPath);
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function sleepSync(ms) {
|
|
136
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
137
|
+
}
|
|
138
|
+
function withRuntimeWriteLock(env, mutate) {
|
|
139
|
+
const lockPath = getServerRuntimeLockPath(env);
|
|
140
|
+
let release = null;
|
|
141
|
+
const deadline = Date.now() + RUNTIME_LOCK_WAIT_MS;
|
|
142
|
+
for (; ; ) {
|
|
143
|
+
release = tryAcquireRuntimeLock(lockPath);
|
|
144
|
+
if (release || Date.now() >= deadline) break;
|
|
145
|
+
sleepSync(RUNTIME_LOCK_RETRY_MS);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
mutate();
|
|
149
|
+
} finally {
|
|
150
|
+
release?.();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function readAllRecords(env) {
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = readFileSync(getServerRuntimePath(env), "utf8");
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return parseServerRuntimeStates(raw);
|
|
161
|
+
}
|
|
162
|
+
function atomicWriteRecords(path, records) {
|
|
163
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
164
|
+
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
165
|
+
writeFileSync(tmpPath, `${JSON.stringify(records, null, 2)}
|
|
166
|
+
`, { encoding: "utf8", mode: 384 });
|
|
167
|
+
renameSync(tmpPath, path);
|
|
168
|
+
}
|
|
169
|
+
function registerServerRuntimeState(state, env = process.env, options = {}) {
|
|
170
|
+
const alive = options.isAlive ?? isPidAlive;
|
|
171
|
+
try {
|
|
172
|
+
withRuntimeWriteLock(env, () => {
|
|
173
|
+
const records = readAllRecords(env).filter(
|
|
174
|
+
(record) => record.pid !== state.pid && alive(record.pid)
|
|
175
|
+
);
|
|
176
|
+
records.push(state);
|
|
177
|
+
atomicWriteRecords(getServerRuntimePath(env), records);
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function unregisterServerRuntimeState(pid = process.pid, env = process.env, options = {}) {
|
|
183
|
+
const alive = options.isAlive ?? isPidAlive;
|
|
184
|
+
try {
|
|
185
|
+
withRuntimeWriteLock(env, () => {
|
|
186
|
+
const records = readAllRecords(env).filter(
|
|
187
|
+
(record) => record.pid !== pid && alive(record.pid)
|
|
188
|
+
);
|
|
189
|
+
if (records.length === 0) {
|
|
190
|
+
rmSync(getServerRuntimePath(env), { force: true });
|
|
191
|
+
} else {
|
|
192
|
+
atomicWriteRecords(getServerRuntimePath(env), records);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function readLiveServerRuntimeStates(env = process.env, options = {}) {
|
|
199
|
+
const alive = options.isAlive ?? isPidAlive;
|
|
200
|
+
return readAllRecords(env).filter((state) => alive(state.pid));
|
|
201
|
+
}
|
|
202
|
+
function orderWrapperServerCandidates(records) {
|
|
203
|
+
return [...records].sort((a, b) => {
|
|
204
|
+
if (a.mode !== b.mode) return a.mode === "proxy" ? -1 : 1;
|
|
205
|
+
return (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/wrapper-env.ts
|
|
210
|
+
var PROXY_ENV_VARS = ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"];
|
|
211
|
+
var REQUIRE_SERVER_ENV = "CLODEX_REQUIRE_SERVER";
|
|
212
|
+
function removeAnthropicProxyBypass(env) {
|
|
213
|
+
const noProxyValues = [env["NO_PROXY"], env["no_proxy"]].filter((value) => value !== void 0);
|
|
214
|
+
if (noProxyValues.length === 0) return;
|
|
215
|
+
const filtered = [...new Set(noProxyValues.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).filter((value) => {
|
|
216
|
+
const entry = value.toLowerCase().replace(/^https?:\/\//, "");
|
|
217
|
+
const host = entry.replace(/:\d+$/, "");
|
|
218
|
+
if (host === "*") return false;
|
|
219
|
+
const suffix = host.startsWith("*.") ? host.slice(1) : host;
|
|
220
|
+
const bypassesAnthropic = suffix.startsWith(".") ? "api.anthropic.com".endsWith(suffix) : "api.anthropic.com" === suffix || "api.anthropic.com".endsWith(`.${suffix}`);
|
|
221
|
+
return !bypassesAnthropic;
|
|
222
|
+
}))].join(",");
|
|
223
|
+
if (filtered) {
|
|
224
|
+
env["NO_PROXY"] = filtered;
|
|
225
|
+
env["no_proxy"] = filtered;
|
|
226
|
+
} else {
|
|
227
|
+
delete env["NO_PROXY"];
|
|
228
|
+
delete env["no_proxy"];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
var LOCAL_GATEWAY_API_KEY = "clodex-local";
|
|
232
|
+
function wrapperRequiresServer(env) {
|
|
233
|
+
return env[REQUIRE_SERVER_ENV] === "1";
|
|
234
|
+
}
|
|
235
|
+
function computeWrapperEnv(baseEnv, state) {
|
|
236
|
+
const env = { ...baseEnv };
|
|
237
|
+
if (!state) return env;
|
|
238
|
+
if (state.mode === "proxy") {
|
|
239
|
+
const proxyUrl = `http://127.0.0.1:${state.port}`;
|
|
240
|
+
delete env["ANTHROPIC_BASE_URL"];
|
|
241
|
+
for (const name of PROXY_ENV_VARS) env[name] = proxyUrl;
|
|
242
|
+
if (state.caPath) env["NODE_EXTRA_CA_CERTS"] = state.caPath;
|
|
243
|
+
removeAnthropicProxyBypass(env);
|
|
244
|
+
return env;
|
|
245
|
+
}
|
|
246
|
+
for (const name of PROXY_ENV_VARS) delete env[name];
|
|
247
|
+
env["ANTHROPIC_BASE_URL"] = `http://127.0.0.1:${state.port}/anthropic`;
|
|
248
|
+
env["ANTHROPIC_API_KEY"] = LOCAL_GATEWAY_API_KEY;
|
|
249
|
+
return env;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/config.ts
|
|
253
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
254
|
+
import { readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
255
|
+
|
|
256
|
+
// src/registry/io.ts
|
|
257
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
258
|
+
import {
|
|
259
|
+
chmodSync,
|
|
260
|
+
closeSync as closeSync3,
|
|
261
|
+
copyFileSync,
|
|
262
|
+
existsSync,
|
|
263
|
+
fsyncSync as fsyncSync2,
|
|
264
|
+
mkdirSync as mkdirSync3,
|
|
265
|
+
openSync as openSync3,
|
|
266
|
+
readFileSync as readFileSync3,
|
|
267
|
+
renameSync as renameSync2,
|
|
268
|
+
unlinkSync as unlinkSync3,
|
|
269
|
+
writeSync
|
|
270
|
+
} from "fs";
|
|
271
|
+
import { dirname as dirname3 } from "path";
|
|
272
|
+
|
|
273
|
+
// src/registry/types.ts
|
|
274
|
+
var REGISTRY_SCHEMA_VERSION = 1;
|
|
275
|
+
|
|
276
|
+
// src/registry/lock.ts
|
|
277
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
278
|
+
import { createHash, randomUUID } from "crypto";
|
|
279
|
+
import {
|
|
280
|
+
closeSync as closeSync2,
|
|
281
|
+
fstatSync,
|
|
282
|
+
fsyncSync,
|
|
283
|
+
linkSync,
|
|
284
|
+
mkdirSync as mkdirSync2,
|
|
285
|
+
openSync as openSync2,
|
|
286
|
+
readFileSync as readFileSync2,
|
|
287
|
+
statSync,
|
|
288
|
+
unlinkSync as unlinkSync2,
|
|
289
|
+
writeFileSync as writeFileSync2
|
|
290
|
+
} from "fs";
|
|
291
|
+
import { dirname as dirname2 } from "path";
|
|
292
|
+
var DEFAULT_WAIT_MS = 3e4;
|
|
293
|
+
var DEFAULT_CREDENTIAL_MUTATION_WAIT_MS = 15e4;
|
|
294
|
+
var DEFAULT_RETRY_MS = 25;
|
|
295
|
+
var registryLockContext = new AsyncLocalStorage();
|
|
296
|
+
var RegistryLockLostError = class extends Error {
|
|
297
|
+
constructor(lockPath) {
|
|
298
|
+
super(`Provider registry lock ownership was lost before write: ${lockPath}`);
|
|
299
|
+
this.name = "RegistryLockLostError";
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function getRegistryLockPath() {
|
|
303
|
+
return `${getProvidersPath()}.lock`;
|
|
304
|
+
}
|
|
305
|
+
function isPidAlive2(pid) {
|
|
306
|
+
try {
|
|
307
|
+
process.kill(pid, 0);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return err.code === "EPERM";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function parseLockOwner(raw) {
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(raw);
|
|
316
|
+
if (!Number.isInteger(parsed.pid) || (parsed.pid ?? 0) <= 0) return null;
|
|
317
|
+
if (typeof parsed.startedAt !== "number" || !Number.isFinite(parsed.startedAt))
|
|
318
|
+
return null;
|
|
319
|
+
if (typeof parsed.token !== "string" || parsed.token.length === 0)
|
|
320
|
+
return null;
|
|
321
|
+
return parsed;
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function createLockRecord(lockPath, owner) {
|
|
327
|
+
const raw = JSON.stringify(owner);
|
|
328
|
+
const tempPath = `${lockPath}.${process.pid}.${owner.token}.tmp`;
|
|
329
|
+
let fd;
|
|
330
|
+
try {
|
|
331
|
+
fd = openSync2(tempPath, "wx", 384);
|
|
332
|
+
writeFileSync2(fd, raw);
|
|
333
|
+
fsyncSync(fd);
|
|
334
|
+
const stats = fstatSync(fd);
|
|
335
|
+
try {
|
|
336
|
+
linkSync(tempPath, lockPath);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err.code === "EEXIST") return null;
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
raw,
|
|
343
|
+
device: stats.dev,
|
|
344
|
+
inode: stats.ino,
|
|
345
|
+
modifiedAt: stats.mtimeMs
|
|
346
|
+
};
|
|
347
|
+
} finally {
|
|
348
|
+
if (fd !== void 0) closeSync2(fd);
|
|
349
|
+
try {
|
|
350
|
+
unlinkSync2(tempPath);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
if (err.code !== "ENOENT") throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function lockFileMatchesLease(lease) {
|
|
357
|
+
let fd;
|
|
358
|
+
try {
|
|
359
|
+
fd = openSync2(lease.lockPath, "r");
|
|
360
|
+
const openedStats = fstatSync(fd);
|
|
361
|
+
const owner = parseLockOwner(readFileSync2(fd, "utf8"));
|
|
362
|
+
const pathStats = statSync(lease.lockPath);
|
|
363
|
+
return owner?.token === lease.token && openedStats.dev === lease.device && openedStats.ino === lease.inode && pathStats.dev === lease.device && pathStats.ino === lease.inode;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (err.code === "ENOENT") return false;
|
|
366
|
+
throw err;
|
|
367
|
+
} finally {
|
|
368
|
+
if (fd !== void 0) closeSync2(fd);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function createLease(lockPath, owner, snapshot) {
|
|
372
|
+
const lease = {
|
|
373
|
+
active: true,
|
|
374
|
+
lockPath,
|
|
375
|
+
token: owner.token,
|
|
376
|
+
device: snapshot.device,
|
|
377
|
+
inode: snapshot.inode,
|
|
378
|
+
assertOwned: () => {
|
|
379
|
+
if (!lease.active || !lockFileMatchesLease(lease)) {
|
|
380
|
+
lease.active = false;
|
|
381
|
+
throw new RegistryLockLostError(lockPath);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
release: () => {
|
|
385
|
+
if (!lease.active) return;
|
|
386
|
+
lease.active = false;
|
|
387
|
+
if (lockFileMatchesLease(lease)) unlinkSync2(lockPath);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
return lease;
|
|
391
|
+
}
|
|
392
|
+
function assertRegistryWriteOwnership(registryPath = getProvidersPath()) {
|
|
393
|
+
const lockPath = `${registryPath}.lock`;
|
|
394
|
+
const lease = registryLockContext.getStore()?.leases.get(lockPath);
|
|
395
|
+
if (!lease) throw new RegistryLockLostError(lockPath);
|
|
396
|
+
lease.assertOwned();
|
|
397
|
+
}
|
|
398
|
+
function getStaleLockSnapshot(lockPath, alive) {
|
|
399
|
+
const raw = readFileSync2(lockPath, "utf8");
|
|
400
|
+
const stats = statSync(lockPath);
|
|
401
|
+
const snapshot = {
|
|
402
|
+
raw,
|
|
403
|
+
device: stats.dev,
|
|
404
|
+
inode: stats.ino,
|
|
405
|
+
modifiedAt: stats.mtimeMs
|
|
406
|
+
};
|
|
407
|
+
const owner = parseLockOwner(raw);
|
|
408
|
+
if (owner) return alive(owner.pid) ? null : snapshot;
|
|
409
|
+
return snapshot;
|
|
410
|
+
}
|
|
411
|
+
function removeStaleLock(lockPath, expected) {
|
|
412
|
+
try {
|
|
413
|
+
if (expected) {
|
|
414
|
+
const raw = readFileSync2(lockPath, "utf8");
|
|
415
|
+
const stats = statSync(lockPath);
|
|
416
|
+
if (raw !== expected.raw || stats.dev !== expected.device || stats.ino !== expected.inode || stats.mtimeMs !== expected.modifiedAt)
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
unlinkSync2(lockPath);
|
|
420
|
+
return true;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (err.code !== "ENOENT") throw err;
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function tryAcquireReaperGuard(lockPath, now, alive) {
|
|
427
|
+
const guardPath = `${lockPath}.reap`;
|
|
428
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
429
|
+
const owner = {
|
|
430
|
+
pid: process.pid,
|
|
431
|
+
startedAt: now,
|
|
432
|
+
token: randomUUID()
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const snapshot = createLockRecord(guardPath, owner);
|
|
436
|
+
if (snapshot) return createLease(guardPath, owner, snapshot);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err.code === "ENOENT") continue;
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
let stale = null;
|
|
442
|
+
try {
|
|
443
|
+
stale = getStaleLockSnapshot(guardPath, alive);
|
|
444
|
+
} catch (readErr) {
|
|
445
|
+
if (readErr.code === "ENOENT") continue;
|
|
446
|
+
throw readErr;
|
|
447
|
+
}
|
|
448
|
+
if (!stale) return null;
|
|
449
|
+
if (!removeStaleLock(guardPath, stale)) continue;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function tryAcquireRegistryLock(lockPath = getRegistryLockPath(), options = {}) {
|
|
454
|
+
const now = options.now?.() ?? Date.now();
|
|
455
|
+
const alive = options.isAlive ?? isPidAlive2;
|
|
456
|
+
mkdirSync2(dirname2(lockPath), { recursive: true, mode: 448 });
|
|
457
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
458
|
+
const owner = {
|
|
459
|
+
pid: process.pid,
|
|
460
|
+
startedAt: now,
|
|
461
|
+
token: randomUUID()
|
|
462
|
+
};
|
|
463
|
+
try {
|
|
464
|
+
const snapshot = createLockRecord(lockPath, owner);
|
|
465
|
+
if (snapshot) return createLease(lockPath, owner, snapshot);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
if (err.code === "ENOENT") continue;
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
let stale = null;
|
|
471
|
+
try {
|
|
472
|
+
stale = getStaleLockSnapshot(lockPath, alive);
|
|
473
|
+
} catch (readErr) {
|
|
474
|
+
if (readErr.code === "ENOENT") continue;
|
|
475
|
+
throw readErr;
|
|
476
|
+
}
|
|
477
|
+
if (!stale) return null;
|
|
478
|
+
const reaperLease = tryAcquireReaperGuard(lockPath, now, alive);
|
|
479
|
+
if (!reaperLease) return null;
|
|
480
|
+
try {
|
|
481
|
+
let currentStale = null;
|
|
482
|
+
try {
|
|
483
|
+
currentStale = getStaleLockSnapshot(lockPath, alive);
|
|
484
|
+
} catch (readErr) {
|
|
485
|
+
if (readErr.code === "ENOENT") continue;
|
|
486
|
+
throw readErr;
|
|
487
|
+
}
|
|
488
|
+
if (!currentStale) return null;
|
|
489
|
+
if (!removeStaleLock(lockPath, currentStale)) continue;
|
|
490
|
+
} finally {
|
|
491
|
+
reaperLease.release();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
function sleep(ms) {
|
|
497
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
498
|
+
}
|
|
499
|
+
function sleepSync2(ms) {
|
|
500
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
501
|
+
}
|
|
502
|
+
function lockTimeoutError(lockPath, waitMs, alive) {
|
|
503
|
+
let owner = null;
|
|
504
|
+
try {
|
|
505
|
+
owner = parseLockOwner(readFileSync2(lockPath, "utf8"));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
if (err.code !== "ENOENT") throw err;
|
|
508
|
+
}
|
|
509
|
+
if (owner && alive(owner.pid)) {
|
|
510
|
+
return new Error(
|
|
511
|
+
`Timed out after ${waitMs}ms waiting for lock held by clodex process (pid ${owner.pid}): ${lockPath}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return new Error(
|
|
515
|
+
`Timed out after ${waitMs}ms waiting for lock: ${lockPath}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
async function withRegistryWriteLock(operation, options = {}) {
|
|
519
|
+
const lockPath = options.lockPath ?? getRegistryLockPath();
|
|
520
|
+
const inheritedLeases = registryLockContext.getStore()?.leases;
|
|
521
|
+
if (inheritedLeases?.get(lockPath)?.active) return operation();
|
|
522
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
523
|
+
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
524
|
+
const now = options.now ?? Date.now;
|
|
525
|
+
const deadline = now() + waitMs;
|
|
526
|
+
let lease = null;
|
|
527
|
+
while (!lease) {
|
|
528
|
+
lease = tryAcquireRegistryLock(lockPath, {
|
|
529
|
+
now,
|
|
530
|
+
isAlive: options.isAlive
|
|
531
|
+
});
|
|
532
|
+
if (lease) break;
|
|
533
|
+
if (now() >= deadline)
|
|
534
|
+
throw lockTimeoutError(lockPath, waitMs, options.isAlive ?? isPidAlive2);
|
|
535
|
+
await sleep(retryMs);
|
|
536
|
+
}
|
|
537
|
+
const leases = new Map(inheritedLeases);
|
|
538
|
+
leases.set(lockPath, lease);
|
|
539
|
+
const context = { leases };
|
|
540
|
+
return registryLockContext.run(context, async () => {
|
|
541
|
+
try {
|
|
542
|
+
return await operation();
|
|
543
|
+
} finally {
|
|
544
|
+
lease.release();
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function withRegistryWriteLockSync(operation, options = {}) {
|
|
549
|
+
const lockPath = options.lockPath ?? getRegistryLockPath();
|
|
550
|
+
const inheritedLeases = registryLockContext.getStore()?.leases;
|
|
551
|
+
if (inheritedLeases?.get(lockPath)?.active) return operation();
|
|
552
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
553
|
+
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
554
|
+
const now = options.now ?? Date.now;
|
|
555
|
+
const deadline = now() + waitMs;
|
|
556
|
+
let lease = null;
|
|
557
|
+
while (!lease) {
|
|
558
|
+
lease = tryAcquireRegistryLock(lockPath, {
|
|
559
|
+
now,
|
|
560
|
+
isAlive: options.isAlive
|
|
561
|
+
});
|
|
562
|
+
if (lease) break;
|
|
563
|
+
if (now() >= deadline)
|
|
564
|
+
throw lockTimeoutError(lockPath, waitMs, options.isAlive ?? isPidAlive2);
|
|
565
|
+
sleepSync2(retryMs);
|
|
566
|
+
}
|
|
567
|
+
const leases = new Map(inheritedLeases);
|
|
568
|
+
leases.set(lockPath, lease);
|
|
569
|
+
const context = { leases };
|
|
570
|
+
return registryLockContext.run(context, () => {
|
|
571
|
+
try {
|
|
572
|
+
return operation();
|
|
573
|
+
} finally {
|
|
574
|
+
lease.release();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function getCredentialMutationLockPath(authRef) {
|
|
579
|
+
const digest = createHash("sha256").update("clodex-credential-mutation\0").update(authRef).digest("hex");
|
|
580
|
+
return `${getProvidersPath()}.credential-${digest}.lock`;
|
|
581
|
+
}
|
|
582
|
+
function withCredentialMutationLock(authRef, operation, options = {}) {
|
|
583
|
+
return withRegistryWriteLock(operation, {
|
|
584
|
+
...options,
|
|
585
|
+
lockPath: getCredentialMutationLockPath(authRef),
|
|
586
|
+
waitMs: options.waitMs ?? DEFAULT_CREDENTIAL_MUTATION_WAIT_MS
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/registry/migrate.ts
|
|
591
|
+
function migrateOAuthOpenAiProvider(registry) {
|
|
592
|
+
if (registry.providers.some((p) => p.id === "openai-oauth")) return false;
|
|
593
|
+
const idx = registry.providers.findIndex(
|
|
594
|
+
(p) => p.id === "openai" && p.authType === "oauth"
|
|
595
|
+
);
|
|
596
|
+
if (idx < 0) return false;
|
|
597
|
+
const existing = registry.providers[idx];
|
|
598
|
+
registry.providers[idx] = {
|
|
599
|
+
...existing,
|
|
600
|
+
id: "openai-oauth",
|
|
601
|
+
templateId: existing.templateId || "openai",
|
|
602
|
+
name: existing.name === "OpenAI" ? "OpenAI (ChatGPT)" : existing.name
|
|
603
|
+
};
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/registry/validate.ts
|
|
608
|
+
var PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
609
|
+
function isValidProviderId(id) {
|
|
610
|
+
return PROVIDER_ID_PATTERN.test(id);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/registry/io.ts
|
|
614
|
+
var DIR_MODE = 448;
|
|
615
|
+
var FILE_MODE = 384;
|
|
616
|
+
function ensureSecureAppHome() {
|
|
617
|
+
const home = getAppHome();
|
|
618
|
+
mkdirSync3(home, { recursive: true, mode: DIR_MODE });
|
|
619
|
+
try {
|
|
620
|
+
chmodSync(home, DIR_MODE);
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function writeSecureFile(path, content) {
|
|
625
|
+
ensureSecureAppHome();
|
|
626
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: DIR_MODE });
|
|
627
|
+
const fd = openSync3(path, "wx", FILE_MODE);
|
|
628
|
+
try {
|
|
629
|
+
const payload = Buffer.from(content);
|
|
630
|
+
let offset = 0;
|
|
631
|
+
while (offset < payload.length) {
|
|
632
|
+
const written = writeSync(fd, payload, offset, payload.length - offset);
|
|
633
|
+
if (written <= 0) {
|
|
634
|
+
throw new Error(`Could not complete secure file write: ${path}`);
|
|
635
|
+
}
|
|
636
|
+
offset += written;
|
|
637
|
+
}
|
|
638
|
+
fsyncSync2(fd);
|
|
639
|
+
} finally {
|
|
640
|
+
closeSync3(fd);
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
chmodSync(path, FILE_MODE);
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function syncParentDirectory(path) {
|
|
648
|
+
let fd;
|
|
649
|
+
try {
|
|
650
|
+
fd = openSync3(dirname3(path), "r");
|
|
651
|
+
fsyncSync2(fd);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
const code = error.code;
|
|
654
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
|
|
655
|
+
} finally {
|
|
656
|
+
if (fd !== void 0) closeSync3(fd);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function parseProvider(raw) {
|
|
660
|
+
if (!raw || typeof raw !== "object") return null;
|
|
661
|
+
const p = raw;
|
|
662
|
+
if (typeof p.id !== "string" || !isValidProviderId(p.id)) return null;
|
|
663
|
+
if (typeof p.templateId !== "string" || !p.templateId) return null;
|
|
664
|
+
if (typeof p.name !== "string" || !p.name) return null;
|
|
665
|
+
if (typeof p.enabled !== "boolean") return null;
|
|
666
|
+
if (typeof p.authRef !== "string" || !p.authRef) return null;
|
|
667
|
+
if (typeof p.addedAt !== "string" || !p.addedAt) return null;
|
|
668
|
+
const api = p.api;
|
|
669
|
+
if (!api || typeof api !== "object") return null;
|
|
670
|
+
const provider = {
|
|
671
|
+
id: p.id,
|
|
672
|
+
templateId: p.templateId,
|
|
673
|
+
name: p.name,
|
|
674
|
+
enabled: p.enabled,
|
|
675
|
+
authRef: p.authRef,
|
|
676
|
+
api,
|
|
677
|
+
addedAt: p.addedAt
|
|
678
|
+
};
|
|
679
|
+
if (p.subscriptionFilter === "free") {
|
|
680
|
+
provider.subscriptionFilter = p.subscriptionFilter;
|
|
681
|
+
}
|
|
682
|
+
if (p.authType === "api" || p.authType === "oauth" || p.authType === "none") {
|
|
683
|
+
provider.authType = p.authType;
|
|
684
|
+
}
|
|
685
|
+
if (typeof p.refreshedAt === "string") provider.refreshedAt = p.refreshedAt;
|
|
686
|
+
if (p.modelsCache && typeof p.modelsCache === "object") {
|
|
687
|
+
const cache = p.modelsCache;
|
|
688
|
+
if (typeof cache.fetchedAt === "string" && Array.isArray(cache.models)) {
|
|
689
|
+
provider.modelsCache = {
|
|
690
|
+
fetchedAt: cache.fetchedAt,
|
|
691
|
+
models: cache.models.filter((m) => m && typeof m === "object")
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return provider;
|
|
696
|
+
}
|
|
697
|
+
function hasOwn(record, key) {
|
|
698
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
699
|
+
}
|
|
700
|
+
function hasValidStrictProviderFields(raw) {
|
|
701
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
702
|
+
const provider = raw;
|
|
703
|
+
if (hasOwn(provider, "subscriptionFilter") && provider.subscriptionFilter !== "free") {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
if (hasOwn(provider, "authType") && provider.authType !== "api" && provider.authType !== "oauth" && provider.authType !== "none") {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
if (hasOwn(provider, "refreshedAt") && typeof provider.refreshedAt !== "string") {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (hasOwn(provider, "modelsCache")) {
|
|
713
|
+
const cache = provider.modelsCache;
|
|
714
|
+
if (!cache || typeof cache !== "object" || Array.isArray(cache)) return false;
|
|
715
|
+
const fields = cache;
|
|
716
|
+
if (typeof fields.fetchedAt !== "string" || !Array.isArray(fields.models)) {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
if (fields.models.some((model) => !model || typeof model !== "object" || Array.isArray(model))) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
function parseRegistry(raw) {
|
|
726
|
+
const empty = { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
727
|
+
if (!raw || typeof raw !== "object") return empty;
|
|
728
|
+
const data = raw;
|
|
729
|
+
const providers = [];
|
|
730
|
+
if (Array.isArray(data.providers)) {
|
|
731
|
+
for (const entry of data.providers) {
|
|
732
|
+
const parsed = parseProvider(entry);
|
|
733
|
+
if (parsed) providers.push(parsed);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const registry = {
|
|
737
|
+
schemaVersion: typeof data.schemaVersion === "number" ? data.schemaVersion : REGISTRY_SCHEMA_VERSION,
|
|
738
|
+
providers
|
|
739
|
+
};
|
|
740
|
+
if (typeof data.importedAt === "string") registry.importedAt = data.importedAt;
|
|
741
|
+
if (typeof data.pricingCacheAt === "string") registry.pricingCacheAt = data.pricingCacheAt;
|
|
742
|
+
return registry;
|
|
743
|
+
}
|
|
744
|
+
function parseRegistryStrict(raw) {
|
|
745
|
+
if (!raw || typeof raw !== "object") {
|
|
746
|
+
throw new Error("Provider registry must be a JSON object.");
|
|
747
|
+
}
|
|
748
|
+
const data = raw;
|
|
749
|
+
if (data.schemaVersion !== REGISTRY_SCHEMA_VERSION) {
|
|
750
|
+
throw new Error("Provider registry has an unsupported schema version.");
|
|
751
|
+
}
|
|
752
|
+
if (!Array.isArray(data.providers)) {
|
|
753
|
+
throw new Error("Provider registry is missing its providers list.");
|
|
754
|
+
}
|
|
755
|
+
for (const entry of data.providers) {
|
|
756
|
+
if (!parseProvider(entry) || !hasValidStrictProviderFields(entry)) {
|
|
757
|
+
throw new Error("Provider registry contains an invalid provider entry.");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return parseRegistry(raw);
|
|
761
|
+
}
|
|
762
|
+
function readRegistryStrict(path) {
|
|
763
|
+
return parseRegistryStrict(JSON.parse(readFileSync3(path, "utf8")));
|
|
764
|
+
}
|
|
765
|
+
function loadRegistry(path = getProvidersPath()) {
|
|
766
|
+
if (!existsSync(path)) {
|
|
767
|
+
return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
771
|
+
const registry = parseRegistry(raw);
|
|
772
|
+
const migrated = migrateOAuthOpenAiProvider(registry);
|
|
773
|
+
if (migrated) {
|
|
774
|
+
try {
|
|
775
|
+
withRegistryWriteLockSync(() => {
|
|
776
|
+
if (!existsSync(path)) return;
|
|
777
|
+
const current = readRegistryStrict(path);
|
|
778
|
+
if (migrateOAuthOpenAiProvider(current)) saveRegistry(current, path);
|
|
779
|
+
}, { lockPath: `${path}.lock` });
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return registry;
|
|
784
|
+
} catch {
|
|
785
|
+
return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function loadRegistryStrict(path = getProvidersPath()) {
|
|
789
|
+
if (!existsSync(path)) {
|
|
790
|
+
return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
791
|
+
}
|
|
792
|
+
const registry = readRegistryStrict(path);
|
|
793
|
+
migrateOAuthOpenAiProvider(registry);
|
|
794
|
+
return registry;
|
|
795
|
+
}
|
|
796
|
+
function saveRegistry(registry, path = getProvidersPath()) {
|
|
797
|
+
assertRegistryWriteOwnership(path);
|
|
798
|
+
const payload = `${JSON.stringify(registry, null, 2)}
|
|
799
|
+
`;
|
|
800
|
+
const backup = `${path}.bak`;
|
|
801
|
+
if (existsSync(path)) {
|
|
802
|
+
try {
|
|
803
|
+
copyFileSync(path, backup);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
808
|
+
try {
|
|
809
|
+
writeSecureFile(tmp, payload);
|
|
810
|
+
assertRegistryWriteOwnership(path);
|
|
811
|
+
renameSync2(tmp, path);
|
|
812
|
+
syncParentDirectory(path);
|
|
813
|
+
} finally {
|
|
814
|
+
try {
|
|
815
|
+
unlinkSync3(tmp);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
if (err.code !== "ENOENT") throw err;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/config.ts
|
|
823
|
+
function readJsonFile(path) {
|
|
824
|
+
try {
|
|
825
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
826
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
827
|
+
} catch {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function readConfig() {
|
|
832
|
+
return readJsonFile(getConfigPath()) ?? {};
|
|
833
|
+
}
|
|
834
|
+
function writeConfig(config) {
|
|
835
|
+
const configPath = getConfigPath();
|
|
836
|
+
assertRegistryWriteOwnership(configPath);
|
|
837
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
838
|
+
`;
|
|
839
|
+
const tmp = `${configPath}.${process.pid}.${randomUUID3()}.tmp`;
|
|
840
|
+
try {
|
|
841
|
+
writeSecureFile(tmp, payload);
|
|
842
|
+
assertRegistryWriteOwnership(configPath);
|
|
843
|
+
renameSync3(tmp, configPath);
|
|
844
|
+
syncParentDirectory(configPath);
|
|
845
|
+
} finally {
|
|
846
|
+
try {
|
|
847
|
+
unlinkSync4(tmp);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
if (error.code !== "ENOENT") throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function updateConfig(mutate) {
|
|
854
|
+
const configPath = getConfigPath();
|
|
855
|
+
return withRegistryWriteLockSync(() => {
|
|
856
|
+
const config = readJsonFile(configPath) ?? {};
|
|
857
|
+
const result = mutate(config);
|
|
858
|
+
writeConfig(config);
|
|
859
|
+
return result;
|
|
860
|
+
}, { lockPath: `${configPath}.lock` });
|
|
861
|
+
}
|
|
862
|
+
async function updateConfigAsync(mutate) {
|
|
863
|
+
const configPath = getConfigPath();
|
|
864
|
+
return withRegistryWriteLock(async () => {
|
|
865
|
+
const config = readJsonFile(configPath) ?? {};
|
|
866
|
+
const update = await mutate(config);
|
|
867
|
+
if (update.write) writeConfig(config);
|
|
868
|
+
return update.result;
|
|
869
|
+
}, { lockPath: `${configPath}.lock` });
|
|
870
|
+
}
|
|
871
|
+
function loadPreferences() {
|
|
872
|
+
const config = readConfig();
|
|
873
|
+
return {
|
|
874
|
+
lastModel: config.lastModel,
|
|
875
|
+
lastProvider: config.lastProvider,
|
|
876
|
+
recentModelsByProvider: config.recentModelsByProvider,
|
|
877
|
+
favoriteModels: config.favoriteModels,
|
|
878
|
+
modelAliases: config.modelAliases,
|
|
879
|
+
claudeBridgeMode: config.claudeBridgeMode,
|
|
880
|
+
serverBridgeMode: config.serverBridgeMode,
|
|
881
|
+
appPathOverrides: config.appPathOverrides,
|
|
882
|
+
recentLaunchFolders: config.recentLaunchFolders,
|
|
883
|
+
server: config.server
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function savePreferences(prefs) {
|
|
887
|
+
updateConfig((config) => {
|
|
888
|
+
if (prefs.lastModel !== void 0) config.lastModel = prefs.lastModel;
|
|
889
|
+
if (prefs.lastProvider !== void 0) config.lastProvider = prefs.lastProvider;
|
|
890
|
+
if (prefs.recentModelsByProvider !== void 0) config.recentModelsByProvider = prefs.recentModelsByProvider;
|
|
891
|
+
if (prefs.favoriteModels !== void 0) config.favoriteModels = prefs.favoriteModels;
|
|
892
|
+
if (prefs.modelAliases !== void 0) config.modelAliases = prefs.modelAliases;
|
|
893
|
+
if (prefs.claudeBridgeMode !== void 0) config.claudeBridgeMode = prefs.claudeBridgeMode;
|
|
894
|
+
if (prefs.serverBridgeMode !== void 0) config.serverBridgeMode = prefs.serverBridgeMode;
|
|
895
|
+
if (prefs.appPathOverrides !== void 0) config.appPathOverrides = prefs.appPathOverrides;
|
|
896
|
+
if (prefs.recentLaunchFolders !== void 0) config.recentLaunchFolders = prefs.recentLaunchFolders;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function getAppPathOverride(appId) {
|
|
900
|
+
const value = loadPreferences().appPathOverrides?.[appId];
|
|
901
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
902
|
+
}
|
|
903
|
+
function resolveBridgeMode(command, explicit, opts = {}) {
|
|
904
|
+
const key = command === "claude" ? "claudeBridgeMode" : "serverBridgeMode";
|
|
905
|
+
if (explicit) {
|
|
906
|
+
if (opts.persist === true) savePreferences({ [key]: explicit });
|
|
907
|
+
return explicit;
|
|
908
|
+
}
|
|
909
|
+
return loadPreferences()[key] ?? "proxy";
|
|
910
|
+
}
|
|
911
|
+
var MAX_RECENT_MODELS = 3;
|
|
912
|
+
function recordLaunchSelection(_agent, providerId, modelId, prefs) {
|
|
913
|
+
const prevRecent = prefs.recentModelsByProvider?.[providerId] ?? [];
|
|
914
|
+
const updatedRecent = [modelId, ...prevRecent.filter((id) => id !== modelId)].slice(0, MAX_RECENT_MODELS);
|
|
915
|
+
savePreferences({
|
|
916
|
+
lastProvider: providerId,
|
|
917
|
+
lastModel: modelId,
|
|
918
|
+
recentModelsByProvider: { ...prefs.recentModelsByProvider, [providerId]: updatedRecent }
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
var SERVER_PASSWORD_SERVICE = "clodex-server-password";
|
|
922
|
+
var SERVER_PASSWORD_ACCOUNT = "server-password";
|
|
923
|
+
async function getServerPasswordKeyring() {
|
|
924
|
+
try {
|
|
925
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
926
|
+
return new Entry(SERVER_PASSWORD_SERVICE, SERVER_PASSWORD_ACCOUNT);
|
|
927
|
+
} catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function getSavedServerPassword() {
|
|
932
|
+
const keyring = await getServerPasswordKeyring();
|
|
933
|
+
if (!keyring) return readConfig().server?.savedPassword ?? null;
|
|
934
|
+
const savedPassword = await updateConfigAsync(async (config) => {
|
|
935
|
+
const server = config.server;
|
|
936
|
+
const password = server?.savedPassword;
|
|
937
|
+
if (!password) return { result: null, write: false };
|
|
938
|
+
try {
|
|
939
|
+
await keyring.setPassword(password);
|
|
940
|
+
delete server.savedPassword;
|
|
941
|
+
if (Object.keys(server).length === 0) delete config.server;
|
|
942
|
+
return { result: password, write: true };
|
|
943
|
+
} catch {
|
|
944
|
+
return { result: password, write: false };
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
if (savedPassword) return savedPassword;
|
|
948
|
+
try {
|
|
949
|
+
return await keyring.getPassword();
|
|
950
|
+
} catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function setSavedServerPassword(password) {
|
|
955
|
+
const keyring = await getServerPasswordKeyring();
|
|
956
|
+
if (keyring) {
|
|
957
|
+
try {
|
|
958
|
+
await keyring.setPassword(password);
|
|
959
|
+
return;
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
await updateConfigAsync((config) => {
|
|
964
|
+
config.server = {
|
|
965
|
+
...config.server ?? {},
|
|
966
|
+
savedPassword: password
|
|
967
|
+
};
|
|
968
|
+
return { result: void 0, write: true };
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
function getServerExposedProviders() {
|
|
972
|
+
const list = readConfig().server?.exposedProviders;
|
|
973
|
+
return list && list.length > 0 ? list : null;
|
|
974
|
+
}
|
|
975
|
+
function setServerExposedProviders(providerIds) {
|
|
976
|
+
updateConfig((config) => {
|
|
977
|
+
config.server = {
|
|
978
|
+
...config.server ?? {},
|
|
979
|
+
exposedProviders: providerIds
|
|
980
|
+
};
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function getServerMaskGatewayIds() {
|
|
984
|
+
return readConfig().server?.maskGatewayIds ?? true;
|
|
985
|
+
}
|
|
986
|
+
function setServerMaskGatewayIds(mask) {
|
|
987
|
+
updateConfig((config) => {
|
|
988
|
+
config.server = {
|
|
989
|
+
...config.server ?? {},
|
|
990
|
+
maskGatewayIds: mask
|
|
991
|
+
};
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
function getServerFavoritesOnly() {
|
|
995
|
+
return readConfig().server?.favoritesOnly ?? false;
|
|
996
|
+
}
|
|
997
|
+
function setServerFavoritesOnly(favoritesOnly) {
|
|
998
|
+
updateConfig((config) => {
|
|
999
|
+
config.server = {
|
|
1000
|
+
...config.server ?? {},
|
|
1001
|
+
favoritesOnly
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
function getServerListenMode() {
|
|
1006
|
+
return readConfig().server?.listenMode === "network" ? "network" : "local";
|
|
1007
|
+
}
|
|
1008
|
+
function setServerListenMode(listenMode) {
|
|
1009
|
+
updateConfig((config) => {
|
|
1010
|
+
config.server = {
|
|
1011
|
+
...config.server ?? {},
|
|
1012
|
+
listenMode
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/launch.ts
|
|
1018
|
+
import { execSync, spawn } from "child_process";
|
|
1019
|
+
import { existsSync as existsSync3, appendFileSync } from "fs";
|
|
1020
|
+
import { homedir as homedir2 } from "os";
|
|
1021
|
+
import { join as join3 } from "path";
|
|
1022
|
+
|
|
1023
|
+
// src/binary-lookup.ts
|
|
1024
|
+
import { execFileSync } from "child_process";
|
|
1025
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1026
|
+
function findBinaryOnPath(name, fallbackPaths, options = {}) {
|
|
1027
|
+
const isWindows2 = options.isWindows ?? process.platform === "win32";
|
|
1028
|
+
const exists = options.exists ?? existsSync2;
|
|
1029
|
+
const runWhich = options.runWhich ?? ((binary, win) => execFileSync(win ? "where.exe" : "which", [binary], {
|
|
1030
|
+
encoding: "utf8",
|
|
1031
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1032
|
+
}));
|
|
1033
|
+
try {
|
|
1034
|
+
const lines = runWhich(name, isWindows2).trim().split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1035
|
+
const path = (isWindows2 ? lines.find((line) => line.toLowerCase().endsWith(".cmd")) : null) ?? lines[0];
|
|
1036
|
+
if (path && (!options.verifyWhichResult || exists(path))) return path;
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
for (const path of fallbackPaths) {
|
|
1040
|
+
if (exists(path)) return path;
|
|
1041
|
+
}
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/launch.ts
|
|
1046
|
+
var isWindows = process.platform === "win32";
|
|
1047
|
+
var FALLBACK_PATHS = isWindows ? [
|
|
1048
|
+
join3(process.env["APPDATA"] ?? homedir2(), "npm", "claude.cmd"),
|
|
1049
|
+
join3(process.env["APPDATA"] ?? homedir2(), "npm", "claude"),
|
|
1050
|
+
join3(homedir2(), "AppData", "Roaming", "npm", "claude.cmd")
|
|
1051
|
+
] : [
|
|
1052
|
+
join3(homedir2(), ".local", "bin", "claude"),
|
|
1053
|
+
join3(homedir2(), ".npm", "bin", "claude"),
|
|
1054
|
+
"/usr/local/bin/claude",
|
|
1055
|
+
"/opt/homebrew/bin/claude"
|
|
1056
|
+
];
|
|
1057
|
+
function findClaudeBinary() {
|
|
1058
|
+
const environmentOverride = process.env["CLODEX_CLAUDE_PATH"];
|
|
1059
|
+
if (environmentOverride?.trim()) {
|
|
1060
|
+
return existsSync3(environmentOverride) ? environmentOverride : null;
|
|
1061
|
+
}
|
|
1062
|
+
const override = getAppPathOverride("claude");
|
|
1063
|
+
if (override) return existsSync3(override) ? override : null;
|
|
1064
|
+
return findBinaryOnPath("claude", FALLBACK_PATHS);
|
|
1065
|
+
}
|
|
1066
|
+
function getInstalledClaudeVersion() {
|
|
1067
|
+
try {
|
|
1068
|
+
const claudePath = findClaudeBinary();
|
|
1069
|
+
if (!claudePath) return "2.1.183";
|
|
1070
|
+
const result = execSync(`${isWindows ? `"${claudePath}"` : claudePath} --version`, {
|
|
1071
|
+
encoding: "utf8",
|
|
1072
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1073
|
+
});
|
|
1074
|
+
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
1075
|
+
if (match) return match[1];
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
return "2.1.183";
|
|
1079
|
+
}
|
|
1080
|
+
function buildClaudeArgs(model, extraArgs) {
|
|
1081
|
+
return model ? ["--model", model, ...extraArgs] : [...extraArgs];
|
|
1082
|
+
}
|
|
1083
|
+
function launchClaude(env, model, extraArgs) {
|
|
1084
|
+
return new Promise((resolve) => {
|
|
1085
|
+
const claudePath = findClaudeBinary();
|
|
1086
|
+
const args = buildClaudeArgs(model, extraArgs);
|
|
1087
|
+
const debugFileIdx = extraArgs.indexOf("--debug-file");
|
|
1088
|
+
const debugLogPath = debugFileIdx !== -1 && extraArgs[debugFileIdx + 1] ? extraArgs[debugFileIdx + 1] : void 0;
|
|
1089
|
+
const originalStdoutWrite = process.stdout.write;
|
|
1090
|
+
const originalStderrWrite = process.stderr.write;
|
|
1091
|
+
const muteWrite = (chunk, encoding, callback) => {
|
|
1092
|
+
if (typeof encoding === "function") {
|
|
1093
|
+
callback = encoding;
|
|
1094
|
+
}
|
|
1095
|
+
if (debugLogPath) {
|
|
1096
|
+
try {
|
|
1097
|
+
const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
1098
|
+
appendFileSync(debugLogPath, `[parent] ${str}`);
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (callback) callback();
|
|
1103
|
+
return true;
|
|
1104
|
+
};
|
|
1105
|
+
process.stdout.write = muteWrite;
|
|
1106
|
+
process.stderr.write = muteWrite;
|
|
1107
|
+
const restore = () => {
|
|
1108
|
+
process.stdout.write = originalStdoutWrite;
|
|
1109
|
+
process.stderr.write = originalStderrWrite;
|
|
1110
|
+
};
|
|
1111
|
+
const child = spawn(claudePath, args, {
|
|
1112
|
+
stdio: "inherit",
|
|
1113
|
+
env,
|
|
1114
|
+
shell: isWindows
|
|
1115
|
+
});
|
|
1116
|
+
const forward = (signal) => {
|
|
1117
|
+
child.kill(signal);
|
|
1118
|
+
};
|
|
1119
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
1120
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
1121
|
+
child.on("exit", (code) => {
|
|
1122
|
+
restore();
|
|
1123
|
+
resolve(code ?? 0);
|
|
1124
|
+
});
|
|
1125
|
+
child.on("error", (err) => {
|
|
1126
|
+
restore();
|
|
1127
|
+
resolve(1);
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export {
|
|
1133
|
+
getAppHome,
|
|
1134
|
+
getCredentialCleanupPath,
|
|
1135
|
+
getLogsPath,
|
|
1136
|
+
isDiscoveryDisabled,
|
|
1137
|
+
registerServerRuntimeState,
|
|
1138
|
+
unregisterServerRuntimeState,
|
|
1139
|
+
readLiveServerRuntimeStates,
|
|
1140
|
+
orderWrapperServerCandidates,
|
|
1141
|
+
removeAnthropicProxyBypass,
|
|
1142
|
+
wrapperRequiresServer,
|
|
1143
|
+
computeWrapperEnv,
|
|
1144
|
+
assertRegistryWriteOwnership,
|
|
1145
|
+
withRegistryWriteLock,
|
|
1146
|
+
withRegistryWriteLockSync,
|
|
1147
|
+
withCredentialMutationLock,
|
|
1148
|
+
isValidProviderId,
|
|
1149
|
+
ensureSecureAppHome,
|
|
1150
|
+
loadRegistry,
|
|
1151
|
+
loadRegistryStrict,
|
|
1152
|
+
saveRegistry,
|
|
1153
|
+
loadPreferences,
|
|
1154
|
+
savePreferences,
|
|
1155
|
+
resolveBridgeMode,
|
|
1156
|
+
recordLaunchSelection,
|
|
1157
|
+
getSavedServerPassword,
|
|
1158
|
+
setSavedServerPassword,
|
|
1159
|
+
getServerExposedProviders,
|
|
1160
|
+
setServerExposedProviders,
|
|
1161
|
+
getServerMaskGatewayIds,
|
|
1162
|
+
setServerMaskGatewayIds,
|
|
1163
|
+
getServerFavoritesOnly,
|
|
1164
|
+
setServerFavoritesOnly,
|
|
1165
|
+
getServerListenMode,
|
|
1166
|
+
setServerListenMode,
|
|
1167
|
+
findClaudeBinary,
|
|
1168
|
+
getInstalledClaudeVersion,
|
|
1169
|
+
launchClaude
|
|
1170
|
+
};
|
|
1171
|
+
//# sourceMappingURL=chunk-QLGIKUKC.js.map
|