ledung 1.4.0 → 1.6.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/CHANGELOG.md +7 -0
- package/README.md +3 -1
- package/dist/cli.js +714 -32
- package/dist/main.js +328 -235
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/web/dist/assets/{highlightCore-g6NEEnZr.js → highlightCore-Cci4B5B5.js} +1 -1
- package/web/dist/assets/{index-D8eK9o3f.css → index-1KospU2G.css} +1 -1
- package/web/dist/assets/{index-DcsDS7Ir.js → index-C_l4DtEI.js} +56 -56
- package/web/dist/index.html +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
-
import { fileURLToPath as
|
|
5
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
|
-
import
|
|
8
|
+
import fs5 from "node:fs";
|
|
9
|
+
import path7 from "node:path";
|
|
9
10
|
import { z as z6 } from "zod";
|
|
10
11
|
|
|
11
12
|
// src/platform/paths.ts
|
|
@@ -98,7 +99,19 @@ var FORMS = [
|
|
|
98
99
|
]
|
|
99
100
|
];
|
|
100
101
|
|
|
102
|
+
// src/sessions/sensitive.ts
|
|
103
|
+
function validateSensitive(project) {
|
|
104
|
+
for (const p of [...project.commandPatterns ?? [], ...project.pathPatterns ?? []]) {
|
|
105
|
+
try {
|
|
106
|
+
new RegExp(p, "i");
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(`The sensitive pattern /${p}/ is not a valid regular expression: ${e.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
// src/sessions/profiles.ts
|
|
114
|
+
import path2 from "node:path";
|
|
102
115
|
import { z as z4 } from "zod";
|
|
103
116
|
|
|
104
117
|
// src/sessions/tools.ts
|
|
@@ -213,6 +226,8 @@ var projectSettings = {
|
|
|
213
226
|
};
|
|
214
227
|
|
|
215
228
|
// src/sessions/profiles.ts
|
|
229
|
+
var FULL_PROFILE = "full";
|
|
230
|
+
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
|
216
231
|
var McpServer = z4.union([
|
|
217
232
|
z4.object({ type: z4.literal("stdio").optional(), command: z4.string().min(1), args: z4.array(z4.string()).optional(), env: z4.record(z4.string(), z4.string()).optional() }),
|
|
218
233
|
z4.object({ type: z4.enum(["http", "sse"]), url: z4.string().min(1), headers: z4.record(z4.string(), z4.string()).optional() })
|
|
@@ -243,7 +258,53 @@ var ProfileSetting = z4.object({
|
|
|
243
258
|
var withoutComments = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([k]) => !k.startsWith("$"))) : v;
|
|
244
259
|
var ProfilesSetting = z4.preprocess(withoutComments, z4.record(z4.string(), ProfilePatchSetting)).default({});
|
|
245
260
|
var RoleProfilesSetting = z4.object({ manager: z4.string().optional(), session: z4.string().optional() });
|
|
261
|
+
var ledungTool = (name) => name === `mcp__${LEDUNG_SERVER}` || name.startsWith(`mcp__${LEDUNG_SERVER}__`);
|
|
246
262
|
var LEDUNG_NAMES = [`mcp__${LEDUNG_SERVER}`, `mcp__${LEDUNG_SERVER}__ask_user`];
|
|
263
|
+
var takesLedungTool = (rule) => {
|
|
264
|
+
const tool2 = rule.trim().replace(/\(.*$/s, "").toLowerCase();
|
|
265
|
+
if (!tool2.includes("*")) return ledungTool(tool2);
|
|
266
|
+
const glob = new RegExp(`^${tool2.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
|
|
267
|
+
return LEDUNG_NAMES.some((name) => glob.test(name));
|
|
268
|
+
};
|
|
269
|
+
function permissionRules(settings) {
|
|
270
|
+
const p = settings?.permissions;
|
|
271
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) return [];
|
|
272
|
+
return ["deny", "ask"].flatMap((list2) => {
|
|
273
|
+
const rules = p[list2];
|
|
274
|
+
return Array.isArray(rules) ? rules.filter((r) => typeof r === "string").map((rule) => ({ list: list2, rule })) : [];
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function validateProfiles(profiles, defaults) {
|
|
278
|
+
for (const [name, p] of Object.entries(profiles)) {
|
|
279
|
+
if (!PROFILE_NAME.test(name)) throw new Error(`profiles: "${name}" is not a usable name (letters, digits, ".", "_" or "-", at most 32).`);
|
|
280
|
+
if (name === FULL_PROFILE) throw new Error(`profiles: "${FULL_PROFILE}" is built in (the user's whole setup) and can't be redefined.`);
|
|
281
|
+
const taken = (p.disallowedTools ?? []).filter(takesLedungTool);
|
|
282
|
+
if (taken.length) throw new Error(`profiles.${name}.disallowedTools: Ledung's own tools always stay (${taken.join(", ")}).`);
|
|
283
|
+
const rules = permissionRules(p.settings).filter((r) => takesLedungTool(r.rule));
|
|
284
|
+
if (rules.length) throw new Error(`profiles.${name}.settings.permissions.${rules[0].list}: Ledung's own tools always stay (${rules.map((r) => r.rule).join(", ")}).`);
|
|
285
|
+
if (p.settingsFile !== void 0) {
|
|
286
|
+
const problem = settingsFileProblem(p.settingsFile);
|
|
287
|
+
if (problem) throw new Error(`profiles.${name}.settingsFile: ${problem}`);
|
|
288
|
+
}
|
|
289
|
+
if (typeof p.mcpServers === "object") {
|
|
290
|
+
const clash = Object.keys(p.mcpServers).filter((k) => k === LEDUNG_SERVER);
|
|
291
|
+
if (clash.length) throw new Error(`profiles.${name}.mcpServers: "${clash[0]}" is Ledung's own server's name.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const role of ["manager", "session"]) {
|
|
295
|
+
if (!defaults) break;
|
|
296
|
+
const name = defaults[role];
|
|
297
|
+
if (name !== void 0 && name !== FULL_PROFILE && !profiles[name]) throw new Error(`defaultProfiles.${role}: no profile named "${name}".`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function settingsFileProblem(file) {
|
|
301
|
+
if (path2.posix.isAbsolute(file) || path2.win32.isAbsolute(file) || /^[A-Za-z]:/.test(file)) return `"${file}" must be relative to the project folder.`;
|
|
302
|
+
const normal = settingsFilePath(file);
|
|
303
|
+
if (normal === ".." || normal.startsWith("../")) return `"${file}" must stay inside the project folder.`;
|
|
304
|
+
if (normal === "." || normal.endsWith("/")) return `"${file}" is a folder, not a file.`;
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
var settingsFilePath = (file) => path2.posix.normalize(file.split("\\").join("/"));
|
|
247
308
|
|
|
248
309
|
// src/sessions/skills.ts
|
|
249
310
|
import { z as z5 } from "zod";
|
|
@@ -266,17 +327,34 @@ var STALE_AFTER_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
266
327
|
var USAGE_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
267
328
|
|
|
268
329
|
// src/accounts/store.ts
|
|
330
|
+
import os4 from "node:os";
|
|
331
|
+
import path6 from "node:path";
|
|
332
|
+
|
|
333
|
+
// src/accounts/signin-store.ts
|
|
334
|
+
import { createHash } from "node:crypto";
|
|
335
|
+
import fs4 from "node:fs";
|
|
336
|
+
import os3 from "node:os";
|
|
337
|
+
import path5 from "node:path";
|
|
338
|
+
|
|
339
|
+
// src/accounts/signin.ts
|
|
340
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
341
|
+
|
|
342
|
+
// src/runners/claude-program.ts
|
|
343
|
+
import { execFile } from "node:child_process";
|
|
344
|
+
import fs3 from "node:fs";
|
|
345
|
+
import { createRequire } from "node:module";
|
|
269
346
|
import os2 from "node:os";
|
|
270
|
-
import
|
|
347
|
+
import path4 from "node:path";
|
|
348
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
271
349
|
|
|
272
350
|
// src/platform/exercised.ts
|
|
273
351
|
import fs2 from "node:fs";
|
|
274
|
-
import
|
|
352
|
+
import path3 from "node:path";
|
|
275
353
|
var EXERCISED_FILE = "exercised.log";
|
|
276
354
|
var MAX_BYTES = 256 * 1024;
|
|
277
355
|
var DEFAULT_TARGET = {
|
|
278
356
|
console: !PACKAGED,
|
|
279
|
-
file: () =>
|
|
357
|
+
file: () => path3.join(userDataDir(), EXERCISED_FILE)
|
|
280
358
|
};
|
|
281
359
|
function exercisedLine(line, target = DEFAULT_TARGET) {
|
|
282
360
|
if (target.console) console.log(line);
|
|
@@ -292,11 +370,417 @@ function exercisedLine(line, target = DEFAULT_TARGET) {
|
|
|
292
370
|
}
|
|
293
371
|
}
|
|
294
372
|
|
|
373
|
+
// src/runners/claude-program.ts
|
|
374
|
+
var SDK = "@anthropic-ai/claude-agent-sdk";
|
|
375
|
+
function sdkClaudeBinary(resolve = createRequire(import.meta.resolve(SDK)).resolve) {
|
|
376
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
377
|
+
const arch = process.arch;
|
|
378
|
+
let packages = [`${SDK}-${process.platform}-${arch}`];
|
|
379
|
+
if (process.platform === "linux") {
|
|
380
|
+
const report = process.report?.getReport?.();
|
|
381
|
+
const musl = report !== void 0 && report.header?.glibcVersionRuntime === void 0;
|
|
382
|
+
packages = musl ? [`${SDK}-linux-${arch}-musl`, `${SDK}-linux-${arch}`] : [`${SDK}-linux-${arch}`, `${SDK}-linux-${arch}-musl`];
|
|
383
|
+
}
|
|
384
|
+
for (const p of packages) {
|
|
385
|
+
try {
|
|
386
|
+
return resolve(`${p}/claude${exe}`);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function sdkClaudeVersion() {
|
|
393
|
+
try {
|
|
394
|
+
const sdk = path4.dirname(fileURLToPath2(import.meta.resolve(SDK)));
|
|
395
|
+
const v = JSON.parse(fs3.readFileSync(path4.join(sdk, "package.json"), "utf8")).claudeCodeVersion;
|
|
396
|
+
return typeof v === "string" ? v : null;
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function parseVersion(text) {
|
|
402
|
+
return text.trim().match(/^v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
403
|
+
}
|
|
404
|
+
function atLeast(have, need) {
|
|
405
|
+
const a = have.split(".").map(Number);
|
|
406
|
+
const b = need.split(".").map(Number);
|
|
407
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
408
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
function commandFor(program) {
|
|
413
|
+
return /\.(c|m)?js$/i.test(program) ? { command: process.execPath, args: [program] } : { command: program, args: [] };
|
|
414
|
+
}
|
|
415
|
+
var runVersion = (program) => new Promise((resolve) => {
|
|
416
|
+
const { command, args } = commandFor(program);
|
|
417
|
+
execFile(command, [...args, "--version"], { windowsHide: true, timeout: 15e3 }, (err, stdout) => resolve(err ? null : String(stdout)));
|
|
418
|
+
});
|
|
419
|
+
function regValue(text, name, env) {
|
|
420
|
+
const row = text.split(/\r?\n/).map((l) => l.trim().split(/\s{2,}|\t/)).find((cols) => cols[0]?.toLowerCase() === name.toLowerCase() && /^REG_(EXPAND_)?SZ$/i.test(cols[1] ?? ""));
|
|
421
|
+
const value = row?.slice(2).join(" ");
|
|
422
|
+
if (!value) return null;
|
|
423
|
+
return value.replace(/%([^%]+)%/g, (whole, v) => {
|
|
424
|
+
const key = Object.keys(env).find((k) => k.toLowerCase() === v.toLowerCase());
|
|
425
|
+
return key ? env[key] ?? whole : whole;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
var regQuery = (key) => new Promise((resolve) => {
|
|
429
|
+
execFile("reg", ["query", key, "/v", "Path"], { windowsHide: true, timeout: 5e3 }, (err, stdout) => resolve(err ? "" : String(stdout)));
|
|
430
|
+
});
|
|
431
|
+
async function windowsPath() {
|
|
432
|
+
const [machine, user] = await Promise.all([
|
|
433
|
+
regQuery("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"),
|
|
434
|
+
regQuery("HKCU\\Environment")
|
|
435
|
+
]);
|
|
436
|
+
const parts = [regValue(machine, "Path", process.env), regValue(user, "Path", process.env)].filter((v) => !!v);
|
|
437
|
+
return parts.length ? parts.join(";") : null;
|
|
438
|
+
}
|
|
439
|
+
function realLookup() {
|
|
440
|
+
return {
|
|
441
|
+
platform: process.platform,
|
|
442
|
+
env: process.env,
|
|
443
|
+
home: os2.homedir(),
|
|
444
|
+
stat: (file) => {
|
|
445
|
+
try {
|
|
446
|
+
const s = fs3.statSync(file);
|
|
447
|
+
return s.isFile() ? { size: s.size, mtimeMs: s.mtimeMs } : null;
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
readText: (file) => {
|
|
453
|
+
try {
|
|
454
|
+
return fs3.readFileSync(file, "utf8");
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
realpath: (file) => {
|
|
460
|
+
try {
|
|
461
|
+
return fs3.realpathSync(file);
|
|
462
|
+
} catch {
|
|
463
|
+
return file;
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
versionText: runVersion,
|
|
467
|
+
listDir: (dir) => {
|
|
468
|
+
try {
|
|
469
|
+
return fs3.readdirSync(dir);
|
|
470
|
+
} catch {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
...process.platform === "win32" ? { freshPath: windowsPath } : {},
|
|
475
|
+
bundled: sdkClaudeBinary(),
|
|
476
|
+
required: sdkClaudeVersion(),
|
|
477
|
+
packaged: PACKAGED
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function noBundledProblem(look) {
|
|
481
|
+
if (!look.packaged) return `Ledung has no Claude Code of its own for ${look.platform}-${process.arch} ("claudeExecutable": "bundled" in the config). Install Claude Code from ${INSTALL_LINK} and set it back to "installed".`;
|
|
482
|
+
return `Ledung installed from npm has no Claude Code of its own ("claudeExecutable": "bundled" in the config). Install Claude Code from ${INSTALL_LINK}, then set "claudeExecutable" back to "installed" in the config file (Settings \u2192 Config file says where it is).`;
|
|
483
|
+
}
|
|
484
|
+
function shimTarget(shim, look) {
|
|
485
|
+
const text = look.readText(shim);
|
|
486
|
+
if (!text) return null;
|
|
487
|
+
const dir = path4.win32.dirname(shim);
|
|
488
|
+
const found = [...text.matchAll(/"%~?dp0%?\\?([^"%]+?\.(?:exe|c?js|mjs))"/gi)].map((m) => path4.win32.join(dir, m[1]));
|
|
489
|
+
return found.reverse().find((f) => look.stat(f) !== null) ?? null;
|
|
490
|
+
}
|
|
491
|
+
function knownLocations(look) {
|
|
492
|
+
const { home, env } = look;
|
|
493
|
+
if (look.platform === "win32") {
|
|
494
|
+
const p2 = path4.win32;
|
|
495
|
+
const appData = env.APPDATA ?? p2.join(home, "AppData", "Roaming");
|
|
496
|
+
const localAppData = env.LOCALAPPDATA ?? p2.join(home, "AppData", "Local");
|
|
497
|
+
return [
|
|
498
|
+
p2.join(home, ".local", "bin", "claude.exe"),
|
|
499
|
+
p2.join(appData, "npm", "claude.cmd"),
|
|
500
|
+
...env.npm_config_prefix ? [p2.join(env.npm_config_prefix, "claude.cmd")] : [],
|
|
501
|
+
// `winget install Anthropic.ClaudeCode` links it here, for the user or for the whole computer (#455).
|
|
502
|
+
p2.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
503
|
+
p2.join(env.ProgramFiles ?? "C:\\Program Files", "WinGet", "Links", "claude.exe")
|
|
504
|
+
];
|
|
505
|
+
}
|
|
506
|
+
const p = path4.posix;
|
|
507
|
+
return [
|
|
508
|
+
p.join(home, ".local", "bin", "claude"),
|
|
509
|
+
p.join(home, ".claude", "local", "claude"),
|
|
510
|
+
"/opt/homebrew/bin/claude",
|
|
511
|
+
"/usr/local/bin/claude",
|
|
512
|
+
"/home/linuxbrew/.linuxbrew/bin/claude",
|
|
513
|
+
p.join(home, ".npm-global", "bin", "claude"),
|
|
514
|
+
...env.npm_config_prefix ? [p.join(env.npm_config_prefix, "bin", "claude")] : []
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
function programOf(file, look) {
|
|
518
|
+
if (look.platform === "win32") {
|
|
519
|
+
if (/\.exe$/i.test(file)) return file;
|
|
520
|
+
return /\.cmd$/i.test(file) ? shimTarget(file, look) : null;
|
|
521
|
+
}
|
|
522
|
+
const real = look.realpath(file);
|
|
523
|
+
return /\.(c|m)?js$/i.test(real) ? real : file;
|
|
524
|
+
}
|
|
525
|
+
function isDesktopApp(file) {
|
|
526
|
+
return /[\\/]AnthropicClaude[\\/](app-[^\\/]+[\\/])?claude\.exe$/i.test(file) || /[\\/]Claude\.app[\\/]Contents[\\/]MacOS[\\/]/.test(file);
|
|
527
|
+
}
|
|
528
|
+
function searchInstalled(look, pathVar = look.env.PATH ?? look.env.Path ?? "") {
|
|
529
|
+
const win = look.platform === "win32";
|
|
530
|
+
const sep = win ? ";" : ":";
|
|
531
|
+
const names = win ? ["claude.exe", "claude.cmd"] : ["claude"];
|
|
532
|
+
const onPath = pathVar.split(sep).filter(Boolean).flatMap((dir) => names.map((n) => (win ? path4.win32 : path4.posix).join(dir, n)));
|
|
533
|
+
const bundled = look.bundled ? look.realpath(look.bundled) : null;
|
|
534
|
+
const same = (a, b) => win ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
535
|
+
const looked = [];
|
|
536
|
+
for (const file of [...onPath, ...knownLocations(look)]) {
|
|
537
|
+
if (looked.some((l) => same(l, file))) continue;
|
|
538
|
+
looked.push(file);
|
|
539
|
+
if (!look.stat(file)) continue;
|
|
540
|
+
const program = programOf(file, look);
|
|
541
|
+
if (!program || !look.stat(program)) continue;
|
|
542
|
+
const real = look.realpath(program);
|
|
543
|
+
if (bundled && same(real, bundled)) continue;
|
|
544
|
+
if (/[\\/]node_modules[\\/]@anthropic-ai[\\/]claude-agent-sdk/i.test(real)) continue;
|
|
545
|
+
if (isDesktopApp(real)) continue;
|
|
546
|
+
return { program, looked };
|
|
547
|
+
}
|
|
548
|
+
return { program: null, looked };
|
|
549
|
+
}
|
|
550
|
+
function desktopAppFolders(look) {
|
|
551
|
+
const { home, env } = look;
|
|
552
|
+
if (look.platform === "win32") {
|
|
553
|
+
const p2 = path4.win32;
|
|
554
|
+
const appData = env.APPDATA ?? p2.join(home, "AppData", "Roaming");
|
|
555
|
+
const packages = p2.join(env.LOCALAPPDATA ?? p2.join(home, "AppData", "Local"), "Packages");
|
|
556
|
+
const store = (look.listDir?.(packages) ?? []).filter((n) => /^Claude_/i.test(n)).map((n) => p2.join(packages, n, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
557
|
+
return [p2.join(appData, "Claude", "claude-code"), ...store];
|
|
558
|
+
}
|
|
559
|
+
const p = path4.posix;
|
|
560
|
+
if (look.platform === "darwin") return [p.join(home, "Library", "Application Support", "Claude", "claude-code")];
|
|
561
|
+
return [p.join(env.XDG_CONFIG_HOME ?? p.join(home, ".config"), "Claude", "claude-code")];
|
|
562
|
+
}
|
|
563
|
+
function appProgramIn(look, versionDir, hash) {
|
|
564
|
+
if (look.platform === "win32") return path4.win32.join(versionDir, "claude.exe");
|
|
565
|
+
if (look.platform !== "darwin") return path4.posix.join(versionDir, "claude");
|
|
566
|
+
const inBundle = (h) => path4.posix.join(versionDir, h, "claude.app", "Contents", "MacOS", "claude");
|
|
567
|
+
if (hash) return inBundle(hash);
|
|
568
|
+
return (look.listDir?.(versionDir) ?? []).sort().map(inBundle).find((f) => look.stat(f)) ?? null;
|
|
569
|
+
}
|
|
570
|
+
function desktopAppCopy(look) {
|
|
571
|
+
const p = look.platform === "win32" ? path4.win32 : path4.posix;
|
|
572
|
+
const looked = [];
|
|
573
|
+
let best = null;
|
|
574
|
+
for (const folder of desktopAppFolders(look)) {
|
|
575
|
+
looked.push(appProgramIn(look, p.join(folder, "<version>"), "<hash>"));
|
|
576
|
+
for (const name of look.listDir?.(folder) ?? []) {
|
|
577
|
+
const version = parseVersion(name);
|
|
578
|
+
const program = version ? appProgramIn(look, p.join(folder, name)) : null;
|
|
579
|
+
if (!version || !program || !look.stat(program)) continue;
|
|
580
|
+
if (!best || !atLeast(best.version, version)) best = { program, version };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { program: best?.program ?? null, looked };
|
|
584
|
+
}
|
|
585
|
+
var versions = /* @__PURE__ */ new Map();
|
|
586
|
+
async function programVersion(program, look) {
|
|
587
|
+
const s = look.stat(program);
|
|
588
|
+
if (!s) return null;
|
|
589
|
+
const known = versions.get(program);
|
|
590
|
+
if (known && known.size === s.size && known.mtimeMs === s.mtimeMs) return known.version;
|
|
591
|
+
const version = parseVersion(await look.versionText(program) ?? "");
|
|
592
|
+
versions.set(program, { ...s, version });
|
|
593
|
+
return version;
|
|
594
|
+
}
|
|
595
|
+
var INSTALL_LINK = "https://code.claude.com/docs/en/setup";
|
|
596
|
+
function installCommand(platform) {
|
|
597
|
+
return platform === "win32" ? "irm https://claude.ai/install.ps1 | iex" : "curl -fsSL https://claude.ai/install.sh | bash";
|
|
598
|
+
}
|
|
599
|
+
var appCopyLogged = false;
|
|
600
|
+
async function chooseClaude(setting = "installed", look = realLookup()) {
|
|
601
|
+
const required = look.required;
|
|
602
|
+
const bundledVersion = look.bundled ? await programVersion(look.bundled, look) ?? required : null;
|
|
603
|
+
const bundled = look.bundled ? { kind: "bundled", path: look.bundled, version: bundledVersion } : null;
|
|
604
|
+
const base = { setting, required, bundled };
|
|
605
|
+
if (setting === "bundled") {
|
|
606
|
+
return { ...base, installed: null, program: bundled, problem: bundled ? null : noBundledProblem(look) };
|
|
607
|
+
}
|
|
608
|
+
const given = setting !== "installed";
|
|
609
|
+
let found;
|
|
610
|
+
let looked;
|
|
611
|
+
let app = false;
|
|
612
|
+
if (given) {
|
|
613
|
+
found = look.stat(setting) ? programOf(setting, look) : null;
|
|
614
|
+
looked = [setting];
|
|
615
|
+
} else {
|
|
616
|
+
({ program: found, looked } = searchInstalled(look));
|
|
617
|
+
const fresh = found || !look.freshPath ? null : await look.freshPath();
|
|
618
|
+
if (fresh) {
|
|
619
|
+
const again = searchInstalled(look, fresh);
|
|
620
|
+
found = again.program;
|
|
621
|
+
looked = [...looked, ...again.looked.filter((l) => !looked.includes(l))];
|
|
622
|
+
}
|
|
623
|
+
if (!found) {
|
|
624
|
+
const copy = desktopAppCopy(look);
|
|
625
|
+
found = copy.program;
|
|
626
|
+
looked = [...looked, ...copy.looked];
|
|
627
|
+
app = found !== null;
|
|
628
|
+
if (app && !appCopyLogged) {
|
|
629
|
+
appCopyLogged = true;
|
|
630
|
+
exercisedLine("EXERCISED #455 Claude Code found as the desktop app's own copy");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const version = found ? await programVersion(found, look) : null;
|
|
635
|
+
const installed = found ? { kind: given ? "path" : "installed", path: found, version, ...app ? { app: true } : {} } : null;
|
|
636
|
+
const install = `install Claude Code (\`${installCommand(look.platform)}\`, or see ${INSTALL_LINK}), then press Check again`;
|
|
637
|
+
let problem = null;
|
|
638
|
+
if (!required) {
|
|
639
|
+
problem = "Ledung could not read which Claude Code version its Agent SDK needs (the SDK's package.json). Install Ledung again (`npm install`).";
|
|
640
|
+
} else if (!found) {
|
|
641
|
+
problem = given ? `Ledung runs your Claude Code, and there is none at ${setting} (the config's "claudeExecutable"). Install it from ${INSTALL_LINK}, or correct the path.` : `Ledung runs your Claude Code, and it isn't installed on this computer. Install it from ${INSTALL_LINK}, then press Check again.`;
|
|
642
|
+
} else if (app && !version) {
|
|
643
|
+
problem = `Ledung runs your Claude Code, and the only one on this computer is the Claude desktop app's own copy at ${found}, whose version couldn't be read. The app keeps that copy for itself: ${install}.`;
|
|
644
|
+
} else if (app && !atLeast(version, required)) {
|
|
645
|
+
problem = `Ledung runs your Claude Code, and the only one on this computer is the Claude desktop app's own copy, ${version} at ${found}; Ledung needs ${required} or newer. The app keeps that copy for itself: ${install}.`;
|
|
646
|
+
} else if (!version) {
|
|
647
|
+
problem = `Ledung runs your Claude Code, and the version of the one at ${found} couldn't be read. Update it (\`claude update\`) or install it again from ${INSTALL_LINK}.`;
|
|
648
|
+
} else if (!atLeast(version, required)) {
|
|
649
|
+
problem = `Ledung runs your Claude Code, and yours is ${version}; Ledung needs ${required} or newer. Update it (\`claude update\`), then press Check again.`;
|
|
650
|
+
}
|
|
651
|
+
return { ...base, installed, program: problem ? null : installed, problem, looked };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/accounts/signin.ts
|
|
655
|
+
function signInKind(method, subscriptionType) {
|
|
656
|
+
if (method === "oauth_token") return "token";
|
|
657
|
+
if (method === "api_key") return "key";
|
|
658
|
+
if (method !== "claude.ai") return null;
|
|
659
|
+
if (subscriptionType === null) return "token";
|
|
660
|
+
return typeof subscriptionType === "string" && subscriptionType ? "login" : null;
|
|
661
|
+
}
|
|
662
|
+
function readAuthStatus(result) {
|
|
663
|
+
const at = Date.now();
|
|
664
|
+
if (result.error) return { state: "unknown", method: null, detail: result.error, checkedAt: at };
|
|
665
|
+
const parsed = jsonOf(result.stdout);
|
|
666
|
+
if (!parsed || typeof parsed.loggedIn !== "boolean") {
|
|
667
|
+
return { state: "unknown", method: null, detail: `claude auth status gave no answer (exit code ${result.code ?? "none"})`, checkedAt: at };
|
|
668
|
+
}
|
|
669
|
+
const method = typeof parsed.authMethod === "string" && parsed.authMethod !== "none" ? parsed.authMethod : null;
|
|
670
|
+
return parsed.loggedIn ? { state: "in", method, detail: null, checkedAt: at } : { state: "out", method: null, detail: null, checkedAt: at };
|
|
671
|
+
}
|
|
672
|
+
function readSignIn(result) {
|
|
673
|
+
const found = readAuthStatus(result);
|
|
674
|
+
if (found.state !== "in") return found;
|
|
675
|
+
const said = jsonOf(result.stdout);
|
|
676
|
+
return { ...found, kind: signInKind(said.authMethod, said.subscriptionType) };
|
|
677
|
+
}
|
|
678
|
+
function jsonOf(text) {
|
|
679
|
+
try {
|
|
680
|
+
const v = JSON.parse(text);
|
|
681
|
+
return v && typeof v === "object" ? v : null;
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
var runCommand = (command, args, env, timeoutMs, cwd) => new Promise((resolve) => {
|
|
687
|
+
execFile2(command, args, { env, cwd, windowsHide: true, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
688
|
+
const e = err;
|
|
689
|
+
if (!e) return resolve({ code: 0, stdout: String(stdout) });
|
|
690
|
+
if (e.killed) return resolve({ code: null, stdout: "", error: `Claude Code did not answer within ${Math.round(timeoutMs / 1e3)} s` });
|
|
691
|
+
if (typeof e.code === "number") return resolve({ code: e.code, stdout: String(stdout ?? "") });
|
|
692
|
+
resolve({ code: null, stdout: "", error: `Claude Code could not be run (${e.code ?? e.message})` });
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
async function checkSignIn(env, opts = {}) {
|
|
696
|
+
const binary = opts.binary !== void 0 ? opts.binary : sdkClaudeBinary();
|
|
697
|
+
if (!binary) return { state: "unknown", method: null, detail: "no Claude Code to ask: it is not installed, or too old (see Settings \u2192 Claude Code)", checkedAt: Date.now() };
|
|
698
|
+
const settings = opts.where?.settings && Object.keys(opts.where.settings).length ? ["--settings", JSON.stringify(opts.where.settings)] : [];
|
|
699
|
+
const { command, args } = commandFor(binary);
|
|
700
|
+
return readSignIn(await (opts.run ?? runCommand)(command, [...args, ...settings, "auth", "status", "--json"], env, opts.timeoutMs ?? 15e3, opts.where?.cwd));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/runners/runner.ts
|
|
704
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
705
|
+
function runWithInput(command, args, input, timeoutMs) {
|
|
706
|
+
return new Promise((resolve, reject) => {
|
|
707
|
+
const child = execFile3(command, args, { windowsHide: true, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
708
|
+
if (err) reject(Object.assign(err, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
|
|
709
|
+
else resolve({ stdout: String(stdout) });
|
|
710
|
+
});
|
|
711
|
+
child.stdin?.on("error", () => {
|
|
712
|
+
});
|
|
713
|
+
child.stdin?.end(input ?? "");
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/accounts/signin-store.ts
|
|
718
|
+
var CREDENTIALS = ".credentials.json";
|
|
719
|
+
function localUser(read = () => os3.userInfo().username) {
|
|
720
|
+
try {
|
|
721
|
+
return read();
|
|
722
|
+
} catch {
|
|
723
|
+
return process.env.USER ?? process.env.LOGNAME ?? "";
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function keychainEntry(folder, user = localUser()) {
|
|
727
|
+
return { service: `Claude Code-credentials-${createHash("sha256").update(folder.normalize("NFC")).digest("hex").slice(0, 8)}`, account: user };
|
|
728
|
+
}
|
|
729
|
+
function signInRecord(token) {
|
|
730
|
+
return JSON.stringify({ claudeAiOauth: { accessToken: token, refreshToken: null, expiresAt: null, scopes: ["user:inference"], subscriptionType: null, rateLimitTier: null } });
|
|
731
|
+
}
|
|
732
|
+
function localStore(platform = process.platform, run = runWithInput) {
|
|
733
|
+
if (platform === "darwin") {
|
|
734
|
+
return {
|
|
735
|
+
// `security` exits 44 when there is no such entry; without -w it prints no secret.
|
|
736
|
+
hasSignIn: async (folder) => {
|
|
737
|
+
const { service, account } = keychainEntry(folder);
|
|
738
|
+
try {
|
|
739
|
+
await run("security", ["find-generic-password", "-a", account, "-s", service], null, 2e4);
|
|
740
|
+
return true;
|
|
741
|
+
} catch (e) {
|
|
742
|
+
if (e.code === 44) return false;
|
|
743
|
+
throw new Error("the Keychain could not be read; if it is locked, `security unlock-keychain` unlocks it");
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
// Through `security -i`, so the record is on its standard input, not a command line anyone can list; as hex
|
|
747
|
+
// (-X), so no quoting can break it. -U replaces the entry that is there, and `security -i` exits non-zero
|
|
748
|
+
// when a line fails (probe of 2026-11-02).
|
|
749
|
+
put: async (folder, record) => {
|
|
750
|
+
const { service, account } = keychainEntry(folder);
|
|
751
|
+
const hex = Buffer.from(record, "utf8").toString("hex");
|
|
752
|
+
try {
|
|
753
|
+
await run("security", ["-i"], `add-generic-password -U -a "${account}" -s "${service}" -X ${hex}
|
|
754
|
+
`, 2e4);
|
|
755
|
+
} catch {
|
|
756
|
+
throw new Error("the Keychain could not be written; if it is locked, `security unlock-keychain` unlocks it");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
hasSignIn: async (folder) => fs4.existsSync(path5.join(folder, CREDENTIALS)),
|
|
763
|
+
// Whole to a file of its own, readable only by the user, then renamed over the one that is there.
|
|
764
|
+
put: async (folder, record) => {
|
|
765
|
+
fs4.mkdirSync(folder, { recursive: true });
|
|
766
|
+
const temp = path5.join(folder, `${CREDENTIALS}.ledung-${process.pid}`);
|
|
767
|
+
try {
|
|
768
|
+
fs4.writeFileSync(temp, record, { mode: 384, flag: "wx" });
|
|
769
|
+
fs4.renameSync(temp, path5.join(folder, CREDENTIALS));
|
|
770
|
+
} finally {
|
|
771
|
+
fs4.rmSync(temp, { force: true });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
295
777
|
// src/accounts/store.ts
|
|
296
778
|
var ACCOUNT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
297
779
|
var DEFAULT_ACCOUNT = "default";
|
|
780
|
+
var SIGN_IN_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
781
|
+
var LOGIN_ENV = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", SIGN_IN_ENV];
|
|
298
782
|
function canonicalFolder(folder) {
|
|
299
|
-
return
|
|
783
|
+
return path6.resolve(folder);
|
|
300
784
|
}
|
|
301
785
|
function canonicalContainerFolder(folder) {
|
|
302
786
|
const f = folder.trim().replace(/\/+$/, "");
|
|
@@ -309,7 +793,7 @@ function sameFolder(a, b, platform = process.platform) {
|
|
|
309
793
|
return platform === "win32" || platform === "darwin" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
310
794
|
}
|
|
311
795
|
function sharedLoginFolders() {
|
|
312
|
-
return [
|
|
796
|
+
return [path6.join(os4.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
|
|
313
797
|
}
|
|
314
798
|
|
|
315
799
|
// src/config.ts
|
|
@@ -413,7 +897,7 @@ function withoutNotes(raw) {
|
|
|
413
897
|
}
|
|
414
898
|
var ClaudeAccountSetting = z6.preprocess(withoutNotes, z6.object({
|
|
415
899
|
whatItIsFor: z6.string().trim().max(200).optional(),
|
|
416
|
-
signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) =>
|
|
900
|
+
signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path7.isAbsolute(f), "give the whole path, such as D:\\Claude sign-ins\\work or /Users/you/claude-signins/work").refine((f) => !sharedLoginFolders().some((s) => sameFolder(s, f)), "that is this computer's own Claude Code folder; an account needs a folder of its own").optional(),
|
|
417
901
|
signInFolderInContainers: z6.string().trim().min(1, "give a folder, or leave the field out for ~/.ledung-accounts/<name>").refine((f) => canonicalContainerFolder(f) !== null, "starts with ~/ or /, of letters, digits and . _ / + - only").refine((f) => canonicalContainerFolder(f) !== "~/.claude", "that is the container user's own Claude Code folder; an account needs a folder of its own").optional()
|
|
418
902
|
}).strict());
|
|
419
903
|
var ClaudeAccountsSetting = z6.preprocess(withoutNotes, z6.record(z6.string(), ClaudeAccountSetting)).default({}).superRefine((accounts, ctx) => {
|
|
@@ -462,7 +946,7 @@ var LocalProject = z6.object({
|
|
|
462
946
|
* Folders outside the project folder where its sessions may also work, such as where its git worktrees
|
|
463
947
|
* go (start_session's cwd, decision of 2026-10-14). None: only the project folder and folders inside it.
|
|
464
948
|
*/
|
|
465
|
-
extraFolders: z6.array(z6.string().min(1).refine((f) =>
|
|
949
|
+
extraFolders: z6.array(z6.string().min(1).refine((f) => path7.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
|
|
466
950
|
model: z6.string().optional(),
|
|
467
951
|
/** Its manager's model; none: the same as its sessions (decision of 2026-10-13). */
|
|
468
952
|
managerModel: z6.string().optional(),
|
|
@@ -579,7 +1063,7 @@ var Config = z6.object({
|
|
|
579
1063
|
* One that is missing or older than the SDK needs is never run: sessions are refused and the page says how to fix it
|
|
580
1064
|
* (Martin, 2026-10-27). "bundled" only when set here. Containers run their own.
|
|
581
1065
|
*/
|
|
582
|
-
claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" ||
|
|
1066
|
+
claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path7.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
|
|
583
1067
|
/** The shell a "!" command from the message box runs in on Windows (issue #212): Git Bash, as Claude's Bash tool, or PowerShell. */
|
|
584
1068
|
windowsShell: z6.enum(["bash", "powershell"]).default("bash"),
|
|
585
1069
|
board: BoardSetting,
|
|
@@ -609,13 +1093,68 @@ function workflowsOf(config) {
|
|
|
609
1093
|
}
|
|
610
1094
|
function configPath(packaged = PACKAGED) {
|
|
611
1095
|
const set = envSetting("CONFIG");
|
|
612
|
-
if (set) return
|
|
613
|
-
if (packaged) return
|
|
614
|
-
return
|
|
1096
|
+
if (set) return path7.resolve(set);
|
|
1097
|
+
if (packaged) return path7.join(userDataDir(), "ledung.config.json");
|
|
1098
|
+
return path7.join(appDir(), "ledung.config.local.json");
|
|
1099
|
+
}
|
|
1100
|
+
function parseConfig(raw) {
|
|
1101
|
+
const result = Config.safeParse(raw);
|
|
1102
|
+
if (!result.success) {
|
|
1103
|
+
const problems = result.error.issues.map((i) => ` - ${i.path.join(".") || "(top level)"}: ${i.message}`).join("\n");
|
|
1104
|
+
throw new Error(`The config has problems:
|
|
1105
|
+
${problems}`);
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
validateSensitive(result.data.sensitive);
|
|
1109
|
+
validateSensitive(result.data.chats.sensitive);
|
|
1110
|
+
for (const p of result.data.projects) validateSensitive(p.sensitive);
|
|
1111
|
+
const fromWorkflow = workflowsOf(result.data).some((w) => w.profilesFile);
|
|
1112
|
+
validateProfiles(result.data.profiles, fromWorkflow ? null : result.data.defaultProfiles);
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
throw new Error(`The config has problems:
|
|
1115
|
+
- ${e.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
const names = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const p of result.data.projects) {
|
|
1119
|
+
if (names.has(p.name)) throw new Error(`The config has two projects named "${p.name}".`);
|
|
1120
|
+
names.add(p.name);
|
|
1121
|
+
}
|
|
1122
|
+
return result.data;
|
|
1123
|
+
}
|
|
1124
|
+
function loadConfig(file = configPath()) {
|
|
1125
|
+
if (!fs5.existsSync(file)) {
|
|
1126
|
+
throw new Error(`No config file at ${file}. Copy ledung.config.example.json to ledung.config.local.json and fill it in.`);
|
|
1127
|
+
}
|
|
1128
|
+
let raw;
|
|
1129
|
+
try {
|
|
1130
|
+
raw = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
throw new Error(`The config file ${file} is not valid JSON: ${e.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
return parseConfig(raw);
|
|
615
1135
|
}
|
|
616
1136
|
|
|
617
1137
|
// src/platform/node-warnings.ts
|
|
618
1138
|
import { pathToFileURL } from "node:url";
|
|
1139
|
+
var exercised = false;
|
|
1140
|
+
function isSqliteWarning(warning, typeOrOptions) {
|
|
1141
|
+
const type = typeof typeOrOptions === "string" ? typeOrOptions : warning instanceof Error ? warning.name : typeOrOptions?.type;
|
|
1142
|
+
const text = warning instanceof Error ? warning.message : warning;
|
|
1143
|
+
return type === "ExperimentalWarning" && typeof text === "string" && text.startsWith("SQLite is an experimental feature");
|
|
1144
|
+
}
|
|
1145
|
+
function dropSqliteWarning() {
|
|
1146
|
+
const emit = process.emitWarning;
|
|
1147
|
+
process.emitWarning = function(warning, ...rest) {
|
|
1148
|
+
if (isSqliteWarning(warning, rest[0])) {
|
|
1149
|
+
if (!exercised) {
|
|
1150
|
+
exercised = true;
|
|
1151
|
+
exercisedLine("EXERCISED #428 Node's SQLite warning kept off the console");
|
|
1152
|
+
}
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
return emit.call(this, warning, ...rest);
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
619
1158
|
function coreArgs(entry2, passthrough2, restarted2, packaged = PACKAGED) {
|
|
620
1159
|
const preload = new URL(packaged ? "./preload.js" : "./preload.ts", pathToFileURL(entry2)).href;
|
|
621
1160
|
const args = ["--import", preload, entry2, ...passthrough2];
|
|
@@ -625,9 +1164,9 @@ function coreArgs(entry2, passthrough2, restarted2, packaged = PACKAGED) {
|
|
|
625
1164
|
|
|
626
1165
|
// src/platform/running.ts
|
|
627
1166
|
import crypto from "node:crypto";
|
|
628
|
-
import
|
|
1167
|
+
import fs6 from "node:fs";
|
|
629
1168
|
import http from "node:http";
|
|
630
|
-
import
|
|
1169
|
+
import path8 from "node:path";
|
|
631
1170
|
|
|
632
1171
|
// src/platform/open-browser.ts
|
|
633
1172
|
import { spawn } from "node:child_process";
|
|
@@ -643,8 +1182,8 @@ function openBrowser(url) {
|
|
|
643
1182
|
var NOT_RUNNING_CODE = 3;
|
|
644
1183
|
function configuredPort(configFile) {
|
|
645
1184
|
try {
|
|
646
|
-
if (!
|
|
647
|
-
const port = JSON.parse(
|
|
1185
|
+
if (!fs6.existsSync(configFile)) return DEFAULT_PORT;
|
|
1186
|
+
const port = JSON.parse(fs6.readFileSync(configFile, "utf8")).port;
|
|
648
1187
|
return port === void 0 ? DEFAULT_PORT : typeof port === "number" && Number.isInteger(port) ? port : null;
|
|
649
1188
|
} catch {
|
|
650
1189
|
return null;
|
|
@@ -652,7 +1191,7 @@ function configuredPort(configFile) {
|
|
|
652
1191
|
}
|
|
653
1192
|
function savedToken(dataDir = userDataDir()) {
|
|
654
1193
|
try {
|
|
655
|
-
const token =
|
|
1194
|
+
const token = fs6.readFileSync(path8.join(dataDir, "auth-token"), "utf8").trim();
|
|
656
1195
|
return /^[0-9a-f]{64}$/.test(token) ? token : null;
|
|
657
1196
|
} catch {
|
|
658
1197
|
return null;
|
|
@@ -698,8 +1237,8 @@ async function openIfRunning(opts) {
|
|
|
698
1237
|
|
|
699
1238
|
// src/platform/background.ts
|
|
700
1239
|
import { spawn as spawn2 } from "node:child_process";
|
|
701
|
-
import
|
|
702
|
-
import
|
|
1240
|
+
import fs7 from "node:fs";
|
|
1241
|
+
import path9 from "node:path";
|
|
703
1242
|
|
|
704
1243
|
// src/server/devices.ts
|
|
705
1244
|
var PAIR_CODE_MS = 10 * 60 * 1e3;
|
|
@@ -718,18 +1257,18 @@ var MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
|
718
1257
|
var READY_MS = 6e4;
|
|
719
1258
|
var STOPPED_MS = 6e4;
|
|
720
1259
|
function logFile(dataDir = userDataDir()) {
|
|
721
|
-
return
|
|
1260
|
+
return path9.join(dataDir, LOG_FILE);
|
|
722
1261
|
}
|
|
723
1262
|
function rotateLog(file, maxBytes = MAX_LOG_BYTES) {
|
|
724
|
-
const size =
|
|
1263
|
+
const size = fs7.statSync(file, { throwIfNoEntry: false })?.size ?? 0;
|
|
725
1264
|
if (size <= maxBytes) return;
|
|
726
|
-
const old =
|
|
727
|
-
|
|
728
|
-
|
|
1265
|
+
const old = path9.join(path9.dirname(file), "ledung.old.log");
|
|
1266
|
+
fs7.rmSync(old, { force: true });
|
|
1267
|
+
fs7.renameSync(file, old);
|
|
729
1268
|
}
|
|
730
1269
|
function logTail(file, lines = 20) {
|
|
731
1270
|
try {
|
|
732
|
-
return
|
|
1271
|
+
return fs7.readFileSync(file, "utf8").trimEnd().split(/\r?\n/).slice(-lines).join("\n");
|
|
733
1272
|
} catch {
|
|
734
1273
|
return "";
|
|
735
1274
|
}
|
|
@@ -740,9 +1279,9 @@ async function startInBackground(opts) {
|
|
|
740
1279
|
const dataDir = opts.dataDir ?? userDataDir();
|
|
741
1280
|
const file = logFile(dataDir);
|
|
742
1281
|
rotateLog(file);
|
|
743
|
-
const out =
|
|
744
|
-
if (process.platform !== "win32")
|
|
745
|
-
|
|
1282
|
+
const out = fs7.openSync(file, "a", 384);
|
|
1283
|
+
if (process.platform !== "win32") fs7.fchmodSync(out, 384);
|
|
1284
|
+
fs7.writeSync(out, `
|
|
746
1285
|
--- ${(/* @__PURE__ */ new Date()).toISOString()} Ledung starting in the background
|
|
747
1286
|
`);
|
|
748
1287
|
const child = spawn2(process.execPath, opts.args, {
|
|
@@ -753,7 +1292,7 @@ async function startInBackground(opts) {
|
|
|
753
1292
|
// Shown in Settings next to the log's path: this Ledung's output goes there.
|
|
754
1293
|
env: { ...process.env, LEDUNG_LOG_FILE: file }
|
|
755
1294
|
});
|
|
756
|
-
|
|
1295
|
+
fs7.closeSync(out);
|
|
757
1296
|
let exited = null;
|
|
758
1297
|
child.on("exit", (code) => {
|
|
759
1298
|
exited = code ?? 1;
|
|
@@ -812,6 +1351,148 @@ async function stopRunning(opts) {
|
|
|
812
1351
|
return 1;
|
|
813
1352
|
}
|
|
814
1353
|
|
|
1354
|
+
// src/accounts/token-command.ts
|
|
1355
|
+
import path10 from "node:path";
|
|
1356
|
+
var USAGE = "Use: ledung account token <account name>. The token is asked for afterwards, or read from a pipe; it never goes on the command line.";
|
|
1357
|
+
var TOKEN = /^sk-ant-oat\d{2}-[A-Za-z0-9_-]{40,900}$/;
|
|
1358
|
+
function cleanToken(given) {
|
|
1359
|
+
const lines = given.trim().split(/\r\n|\r|\n/).map((line) => line.trim());
|
|
1360
|
+
if (lines.some((line) => !line || /\s/.test(line))) return null;
|
|
1361
|
+
const token = lines.join("");
|
|
1362
|
+
return TOKEN.test(token) && !token.includes("sk-ant-", 1) ? token : null;
|
|
1363
|
+
}
|
|
1364
|
+
function applyKeys(held, piece) {
|
|
1365
|
+
let text = held;
|
|
1366
|
+
const keys = piece.replace(/\x1b\[[0-9;?]*[A-Za-z~]/g, "");
|
|
1367
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1368
|
+
const key = keys[i];
|
|
1369
|
+
if (key === "") return { held: "", done: "stop" };
|
|
1370
|
+
if (key === "") return { held: text, done: "enter" };
|
|
1371
|
+
if (key === "\x7F" || key === "\b") text = text.slice(0, -1);
|
|
1372
|
+
else if (key === "\r" || key === "\n") {
|
|
1373
|
+
if (i === keys.length - 1) return { held: text, done: "enter" };
|
|
1374
|
+
text += key;
|
|
1375
|
+
} else if (key >= " ") text += key;
|
|
1376
|
+
}
|
|
1377
|
+
return { held: text, done: null };
|
|
1378
|
+
}
|
|
1379
|
+
function readHidden(input) {
|
|
1380
|
+
return new Promise((resolve) => {
|
|
1381
|
+
let held = "";
|
|
1382
|
+
const finish = (value) => {
|
|
1383
|
+
input.off("data", onData);
|
|
1384
|
+
input.off("end", onEnd);
|
|
1385
|
+
input.setRawMode(false);
|
|
1386
|
+
input.pause();
|
|
1387
|
+
resolve(value);
|
|
1388
|
+
};
|
|
1389
|
+
const onData = (chunk) => {
|
|
1390
|
+
const next = applyKeys(held, String(chunk));
|
|
1391
|
+
held = next.held;
|
|
1392
|
+
if (next.done) finish(next.done === "stop" ? null : held);
|
|
1393
|
+
};
|
|
1394
|
+
const onEnd = () => finish(held);
|
|
1395
|
+
input.setRawMode(true);
|
|
1396
|
+
input.on("data", onData);
|
|
1397
|
+
input.on("end", onEnd);
|
|
1398
|
+
input.resume();
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
async function readPipe(input) {
|
|
1402
|
+
let text = "";
|
|
1403
|
+
for await (const chunk of input) text += String(chunk);
|
|
1404
|
+
return text;
|
|
1405
|
+
}
|
|
1406
|
+
async function findInDatabase(name, file) {
|
|
1407
|
+
dropSqliteWarning();
|
|
1408
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
1409
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
1410
|
+
try {
|
|
1411
|
+
const rows = db.prepare("SELECT name, config_dir FROM accounts ORDER BY name").all();
|
|
1412
|
+
const row = rows.find((r) => r.name === name);
|
|
1413
|
+
return row ? { folder: row.config_dir } : { names: rows.map((r) => r.name) };
|
|
1414
|
+
} finally {
|
|
1415
|
+
db.close();
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function realDeps() {
|
|
1419
|
+
const store = localStore();
|
|
1420
|
+
return {
|
|
1421
|
+
findAccount: async (name) => {
|
|
1422
|
+
const file = path10.join(userDataDir(), "ledung.db");
|
|
1423
|
+
try {
|
|
1424
|
+
return await findInDatabase(name, file);
|
|
1425
|
+
} catch {
|
|
1426
|
+
return { names: [] };
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
readToken: async () => {
|
|
1430
|
+
if (!process.stdin.isTTY) return readPipe(process.stdin);
|
|
1431
|
+
process.stderr.write("Paste the token from `claude setup-token` and press Enter (nothing is shown): ");
|
|
1432
|
+
const token = await readHidden(process.stdin);
|
|
1433
|
+
process.stderr.write("\n");
|
|
1434
|
+
return token;
|
|
1435
|
+
},
|
|
1436
|
+
store,
|
|
1437
|
+
check: async (folder) => {
|
|
1438
|
+
let setting = "installed";
|
|
1439
|
+
try {
|
|
1440
|
+
setting = loadConfig(configPath()).claudeExecutable;
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
const binary = (await chooseClaude(setting)).program?.path ?? null;
|
|
1444
|
+
return checkSignIn(folderOnlyEnv(process.env, folder), { binary });
|
|
1445
|
+
},
|
|
1446
|
+
print: (line) => console.log(line)
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function folderOnlyEnv(base, folder) {
|
|
1450
|
+
const env = {};
|
|
1451
|
+
for (const [k, v] of Object.entries(base)) if (v !== void 0 && !LOGIN_ENV.includes(k) && !k.startsWith("LEDUNG_")) env[k] = v;
|
|
1452
|
+
return { ...env, [SIGN_IN_ENV]: folder };
|
|
1453
|
+
}
|
|
1454
|
+
async function accountCommand(args, deps = realDeps()) {
|
|
1455
|
+
const [what, name, ...rest] = args;
|
|
1456
|
+
if (what !== "token" || !name || rest.length || !ACCOUNT_NAME.test(name)) {
|
|
1457
|
+
deps.print(USAGE);
|
|
1458
|
+
return 2;
|
|
1459
|
+
}
|
|
1460
|
+
const found = await deps.findAccount(name);
|
|
1461
|
+
if (!("folder" in found)) {
|
|
1462
|
+
deps.print(`There is no account named "${name}" in Ledung. ${found.names.length ? `Its accounts: ${found.names.join(", ")}.` : "Add it under Settings \u2192 Claude & usage \u2192 Accounts first."}`);
|
|
1463
|
+
return 1;
|
|
1464
|
+
}
|
|
1465
|
+
const given = await deps.readToken();
|
|
1466
|
+
if (given === null) {
|
|
1467
|
+
deps.print("Stopped. Nothing was changed.");
|
|
1468
|
+
return 1;
|
|
1469
|
+
}
|
|
1470
|
+
const token = cleanToken(given);
|
|
1471
|
+
if (!token) {
|
|
1472
|
+
deps.print("That is not a long-lived token: `claude setup-token` prints one starting with sk-ant-oat. Nothing was changed.");
|
|
1473
|
+
return 1;
|
|
1474
|
+
}
|
|
1475
|
+
let replaced;
|
|
1476
|
+
try {
|
|
1477
|
+
replaced = await deps.store.hasSignIn(found.folder);
|
|
1478
|
+
await deps.store.put(found.folder, signInRecord(token));
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
deps.print(`The token could not be stored: ${e.message}. Nothing was changed.`);
|
|
1481
|
+
return 1;
|
|
1482
|
+
}
|
|
1483
|
+
deps.print(`The token is in Claude Code's sign-in for "${name}"${replaced ? ", in place of the sign-in that was there" : ""}.`);
|
|
1484
|
+
const after = await deps.check(found.folder);
|
|
1485
|
+
if (after.state === "in" && after.method === "claude.ai" && after.kind === "token") {
|
|
1486
|
+
deps.print("Claude Code sees it: signed in with a long-lived token. It reads what is stored without trying it, so the first session is what proves the token.");
|
|
1487
|
+
deps.print("Press Check again under Settings \u2192 Claude & usage \u2192 Accounts to see it in Ledung.");
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
if (after.state === "unknown") deps.print(`Claude Code could not be asked whether it worked: ${after.detail ?? "no answer"}.`);
|
|
1491
|
+
else if (after.state === "out") deps.print("It did NOT work: Claude Code says this account is not signed in.");
|
|
1492
|
+
else deps.print(`It did NOT work: Claude Code answers with something else for this account (${after.method ?? "an unknown sign-in"}${after.kind === "login" ? ", a /login" : ""}).`);
|
|
1493
|
+
return 1;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
815
1496
|
// src/cli.ts
|
|
816
1497
|
var RESTART_CODE = 75;
|
|
817
1498
|
var OWN = ["--open-only", "--foreground"];
|
|
@@ -821,12 +1502,13 @@ if (passthrough.includes("--version") || passthrough.includes("-v")) {
|
|
|
821
1502
|
process.exit(0);
|
|
822
1503
|
}
|
|
823
1504
|
if (passthrough[0] === "stop") process.exit(await stopRunning({ configFile: configPath() }));
|
|
1505
|
+
if (passthrough[0] === "account") process.exit(await accountCommand(passthrough.slice(1)));
|
|
824
1506
|
if (await openIfRunning({ configFile: configPath(), open: !passthrough.includes("--no-open") })) process.exit(0);
|
|
825
1507
|
if (process.argv.includes("--open-only")) process.exit(NOT_RUNNING_CODE);
|
|
826
1508
|
if (!process.argv.includes("--foreground")) {
|
|
827
|
-
process.exit(await startInBackground({ args: [
|
|
1509
|
+
process.exit(await startInBackground({ args: [fileURLToPath3(import.meta.url), "--foreground", ...passthrough], configFile: configPath() }));
|
|
828
1510
|
}
|
|
829
|
-
var entry =
|
|
1511
|
+
var entry = fileURLToPath3(new URL(PACKAGED ? "./main.js" : "./main.ts", import.meta.url));
|
|
830
1512
|
for (const signal of ["SIGINT", "SIGTERM", "SIGBREAK"]) process.on(signal, () => {
|
|
831
1513
|
});
|
|
832
1514
|
var restarted = false;
|