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/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 fileURLToPath2 } from "node:url";
5
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
6
6
 
7
7
  // src/config.ts
8
- import path4 from "node:path";
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 path3 from "node:path";
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 path2 from "node:path";
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: () => path2.join(userDataDir(), EXERCISED_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 path3.resolve(folder);
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 [path3.join(os2.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
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) => path4.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(),
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) => path4.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
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" || path4.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
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 path4.resolve(set);
613
- if (packaged) return path4.join(userDataDir(), "ledung.config.json");
614
- return path4.join(appDir(), "ledung.config.local.json");
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 fs3 from "node:fs";
1167
+ import fs6 from "node:fs";
629
1168
  import http from "node:http";
630
- import path5 from "node:path";
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 (!fs3.existsSync(configFile)) return DEFAULT_PORT;
647
- const port = JSON.parse(fs3.readFileSync(configFile, "utf8")).port;
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 = fs3.readFileSync(path5.join(dataDir, "auth-token"), "utf8").trim();
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 fs4 from "node:fs";
702
- import path6 from "node:path";
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 path6.join(dataDir, LOG_FILE);
1260
+ return path9.join(dataDir, LOG_FILE);
722
1261
  }
723
1262
  function rotateLog(file, maxBytes = MAX_LOG_BYTES) {
724
- const size = fs4.statSync(file, { throwIfNoEntry: false })?.size ?? 0;
1263
+ const size = fs7.statSync(file, { throwIfNoEntry: false })?.size ?? 0;
725
1264
  if (size <= maxBytes) return;
726
- const old = path6.join(path6.dirname(file), "ledung.old.log");
727
- fs4.rmSync(old, { force: true });
728
- fs4.renameSync(file, old);
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 fs4.readFileSync(file, "utf8").trimEnd().split(/\r?\n/).slice(-lines).join("\n");
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 = fs4.openSync(file, "a", 384);
744
- if (process.platform !== "win32") fs4.fchmodSync(out, 384);
745
- fs4.writeSync(out, `
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
- fs4.closeSync(out);
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: [fileURLToPath2(import.meta.url), "--foreground", ...passthrough], configFile: configPath() }));
1509
+ process.exit(await startInBackground({ args: [fileURLToPath3(import.meta.url), "--foreground", ...passthrough], configFile: configPath() }));
828
1510
  }
829
- var entry = fileURLToPath2(new URL(PACKAGED ? "./main.js" : "./main.ts", import.meta.url));
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;