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/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/main.ts
2
2
  import fs38 from "node:fs";
3
3
  import os16 from "node:os";
4
- import path40 from "node:path";
4
+ import path42 from "node:path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs10 from "node:fs";
8
- import path13 from "node:path";
8
+ import path14 from "node:path";
9
9
  import { z as z6 } from "zod";
10
10
 
11
11
  // src/platform/paths.ts
@@ -494,8 +494,8 @@ async function commentSource(url, ctx) {
494
494
  const offline = ctx.offline();
495
495
  if (offline) throw new Error(offline);
496
496
  const repo = `${link.owner}/${link.name}`;
497
- const path41 = link.what === "comment" ? `repos/${repo}/issues/comments/${link.id}` : link.what === "review comment" ? `repos/${repo}/pulls/comments/${link.id}` : `repos/${repo}/pulls/${link.number}/reviews/${link.id}`;
498
- const answer = await ctx.ghApi([path41], GH_TIMEOUT_MS);
497
+ const path43 = link.what === "comment" ? `repos/${repo}/issues/comments/${link.id}` : link.what === "review comment" ? `repos/${repo}/pulls/comments/${link.id}` : `repos/${repo}/pulls/${link.number}/reviews/${link.id}`;
498
+ const answer = await ctx.ghApi([path43], GH_TIMEOUT_MS);
499
499
  if (!answer.ok) throw new Error(`GitHub did not give it: ${ghReason(answer)}`);
500
500
  let c;
501
501
  try {
@@ -2067,20 +2067,21 @@ var USAGE_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
2067
2067
  // src/accounts/store.ts
2068
2068
  import fs9 from "node:fs";
2069
2069
  import os5 from "node:os";
2070
- import path12 from "node:path";
2070
+ import path13 from "node:path";
2071
2071
 
2072
2072
  // src/accounts/about-file.ts
2073
2073
  import fs8 from "node:fs";
2074
- import path11 from "node:path";
2074
+ import path12 from "node:path";
2075
2075
 
2076
2076
  // src/accounts/signin-store.ts
2077
2077
  import { createHash } from "node:crypto";
2078
2078
  import fs7 from "node:fs";
2079
2079
  import os4 from "node:os";
2080
- import path10 from "node:path";
2080
+ import path11 from "node:path";
2081
2081
 
2082
2082
  // src/accounts/signin.ts
2083
2083
  import { execFile as execFile2 } from "node:child_process";
2084
+ import path10 from "node:path";
2084
2085
 
2085
2086
  // src/runners/claude-program.ts
2086
2087
  import { execFile } from "node:child_process";
@@ -2395,6 +2396,16 @@ async function chooseClaude(setting = "installed", look = realLookup()) {
2395
2396
  }
2396
2397
 
2397
2398
  // src/accounts/signin.ts
2399
+ function signInKind(method2, subscriptionType) {
2400
+ if (method2 === "oauth_token") return "token";
2401
+ if (method2 === "api_key") return "key";
2402
+ if (method2 !== "claude.ai") return null;
2403
+ if (subscriptionType === null) return "token";
2404
+ return typeof subscriptionType === "string" && subscriptionType ? "login" : null;
2405
+ }
2406
+ function shownSignIn(found) {
2407
+ return found?.kind ?? (found?.state === "in" ? "other" : null);
2408
+ }
2398
2409
  function readAuthStatus(result) {
2399
2410
  const at2 = Date.now();
2400
2411
  if (result.error) return { state: "unknown", method: null, detail: result.error, checkedAt: at2 };
@@ -2405,6 +2416,12 @@ function readAuthStatus(result) {
2405
2416
  const method2 = typeof parsed.authMethod === "string" && parsed.authMethod !== "none" ? parsed.authMethod : null;
2406
2417
  return parsed.loggedIn ? { state: "in", method: method2, detail: null, checkedAt: at2 } : { state: "out", method: null, detail: null, checkedAt: at2 };
2407
2418
  }
2419
+ function readSignIn(result) {
2420
+ const found = readAuthStatus(result);
2421
+ if (found.state !== "in") return found;
2422
+ const said = jsonOf(result.stdout);
2423
+ return { ...found, kind: signInKind(said.authMethod, said.subscriptionType) };
2424
+ }
2408
2425
  function jsonOf(text2) {
2409
2426
  try {
2410
2427
  const v = JSON.parse(text2);
@@ -2427,7 +2444,7 @@ async function checkSignIn(env, opts = {}) {
2427
2444
  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() };
2428
2445
  const settings = opts.where?.settings && Object.keys(opts.where.settings).length ? ["--settings", JSON.stringify(opts.where.settings)] : [];
2429
2446
  const { command, args } = commandFor(binary);
2430
- return readAuthStatus(await (opts.run ?? runCommand)(command, [...args, ...settings, "auth", "status", "--json"], env, opts.timeoutMs ?? 15e3, opts.where?.cwd));
2447
+ return readSignIn(await (opts.run ?? runCommand)(command, [...args, ...settings, "auth", "status", "--json"], env, opts.timeoutMs ?? 15e3, opts.where?.cwd));
2431
2448
  }
2432
2449
  function shellFolder(folder) {
2433
2450
  return folder.startsWith("~/") ? `"$HOME/${folder.slice(2)}"` : `'${folder}'`;
@@ -2438,7 +2455,7 @@ function dockerExecArgs(place, interactive = null) {
2438
2455
  async function checkContainerSignIn(place, folder, opts = {}) {
2439
2456
  const script = folder === null ? 'D=$(mktemp -d) || exit 1; CLAUDE_SECURESTORAGE_CONFIG_DIR="$D" env -u ANTHROPIC_API_KEY -u CLAUDE_CODE_OAUTH_TOKEN claude auth status --json; r=$?; rm -rf "$D"; exit $r' : `CLAUDE_SECURESTORAGE_CONFIG_DIR=${shellFolder(folder)} exec env -u ANTHROPIC_API_KEY -u CLAUDE_CODE_OAUTH_TOKEN claude auth status --json`;
2440
2457
  const result = await (opts.run ?? runCommand)("docker", [...dockerExecArgs(place), "sh", "-c", script], process.env, opts.timeoutMs ?? 2e4);
2441
- return readAuthStatus(result);
2458
+ return readSignIn(result);
2442
2459
  }
2443
2460
  function signInHelp(account, platform = process.platform) {
2444
2461
  if (account.kind === "default") {
@@ -2451,6 +2468,11 @@ function signInHelp(account, platform = process.platform) {
2451
2468
  function signInCommand(dir, platform = process.platform) {
2452
2469
  return platform === "win32" ? `$env:CLAUDE_SECURESTORAGE_CONFIG_DIR="${dir}"; claude` : `CLAUDE_SECURESTORAGE_CONFIG_DIR="${dir}" claude`;
2453
2470
  }
2471
+ function tokenCommand(name, how) {
2472
+ const run8 = how.packaged ? "ledung" : `node "${path10.join(how.appDir, "src", "cli.ts")}"`;
2473
+ const data = !how.dataDir ? "" : how.platform === "win32" ? `$env:LEDUNG_DATA_DIR="${how.dataDir}"; ` : `LEDUNG_DATA_DIR="${how.dataDir}" `;
2474
+ return `${data}${run8} account token ${name}`;
2475
+ }
2454
2476
  function containerSignInCommand(place, folder) {
2455
2477
  const dir = folder.startsWith("~/") ? `$HOME/${folder.slice(2)}` : folder;
2456
2478
  return `docker ${dockerExecArgs(place, "-it").join(" ")} sh -c 'CLAUDE_SECURESTORAGE_CONFIG_DIR=${dir} exec claude'`;
@@ -2496,7 +2518,14 @@ async function ghApiAnswer(command, args, input, timeoutMs) {
2496
2518
 
2497
2519
  // src/accounts/signin-store.ts
2498
2520
  var CREDENTIALS = ".credentials.json";
2499
- function keychainEntry(folder, user = os4.userInfo().username) {
2521
+ function localUser(read = () => os4.userInfo().username) {
2522
+ try {
2523
+ return read();
2524
+ } catch {
2525
+ return process.env.USER ?? process.env.LOGNAME ?? "";
2526
+ }
2527
+ }
2528
+ function keychainEntry(folder, user = localUser()) {
2500
2529
  return { service: `Claude Code-credentials-${createHash("sha256").update(folder.normalize("NFC")).digest("hex").slice(0, 8)}`, account: user };
2501
2530
  }
2502
2531
  async function removeSignIn(place, platform = process.platform, run8 = runWithInput) {
@@ -2507,7 +2536,7 @@ async function removeSignIn(place, platform = process.platform, run8 = runWithIn
2507
2536
  const { service, account } = keychainEntry(place.folder);
2508
2537
  await run8("security", ["delete-generic-password", "-a", account, "-s", service], null, 2e4);
2509
2538
  } else {
2510
- fs7.rmSync(path10.join(place.folder, CREDENTIALS), { force: true });
2539
+ fs7.rmSync(path11.join(place.folder, CREDENTIALS), { force: true });
2511
2540
  }
2512
2541
  } catch {
2513
2542
  }
@@ -2541,7 +2570,7 @@ function aboutText(a, platform = process.platform) {
2541
2570
  return lines.join(platform === "win32" ? "\r\n" : "\n") + (platform === "win32" ? "\r\n" : "\n");
2542
2571
  }
2543
2572
  function writeAboutFile(a, platform = process.platform) {
2544
- const file = path11.join(a.configDir, ABOUT_FILE);
2573
+ const file = path12.join(a.configDir, ABOUT_FILE);
2545
2574
  const text2 = aboutText(a, platform);
2546
2575
  try {
2547
2576
  if (!fs8.statSync(a.configDir).isDirectory()) return false;
@@ -2559,11 +2588,12 @@ var DEFAULT_ACCOUNT = "default";
2559
2588
  var AccountError = class extends Error {
2560
2589
  };
2561
2590
  var SIGN_IN_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
2591
+ var LOGIN_ENV = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", SIGN_IN_ENV];
2562
2592
  function defaultContainerDir(name) {
2563
2593
  return `~/.ledung-accounts/${name}`;
2564
2594
  }
2565
2595
  function canonicalFolder(folder) {
2566
- return path12.resolve(folder);
2596
+ return path13.resolve(folder);
2567
2597
  }
2568
2598
  function canonicalContainerFolder(folder) {
2569
2599
  const f = folder.trim().replace(/\/+$/, "");
@@ -2576,14 +2606,14 @@ function sameFolder(a, b, platform = process.platform) {
2576
2606
  return platform === "win32" || platform === "darwin" ? x.toLowerCase() === y.toLowerCase() : x === y;
2577
2607
  }
2578
2608
  function sharedLoginFolders() {
2579
- return [path12.join(os5.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
2609
+ return [path13.join(os5.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
2580
2610
  }
2581
2611
  var SHARED_CONTAINER_FOLDERS = ["~/.claude"];
2582
2612
  var AccountStore = class {
2583
2613
  db;
2584
2614
  /** Ledung's own accounts folder: the default login folders. */
2585
2615
  dir;
2586
- constructor(db, dir = path12.join(userDataDir(), "accounts")) {
2616
+ constructor(db, dir = path13.join(userDataDir(), "accounts")) {
2587
2617
  this.db = db;
2588
2618
  this.dir = dir;
2589
2619
  }
@@ -2596,7 +2626,7 @@ var AccountStore = class {
2596
2626
  }
2597
2627
  /** The default login folder on this computer for an account of this name. */
2598
2628
  defaultFolder(name) {
2599
- return canonicalFolder(path12.join(this.dir, name));
2629
+ return canonicalFolder(path13.join(this.dir, name));
2600
2630
  }
2601
2631
  /** Adds an account: its login folder is made, empty until someone signs in to it. */
2602
2632
  add(a, whatItIsFor) {
@@ -2844,7 +2874,7 @@ function withoutNotes(raw) {
2844
2874
  }
2845
2875
  var ClaudeAccountSetting = z6.preprocess(withoutNotes, z6.object({
2846
2876
  whatItIsFor: z6.string().trim().max(200).optional(),
2847
- signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path13.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(),
2877
+ signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path14.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(),
2848
2878
  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()
2849
2879
  }).strict());
2850
2880
  var ClaudeAccountsSetting = z6.preprocess(withoutNotes, z6.record(z6.string(), ClaudeAccountSetting)).default({}).superRefine((accounts, ctx) => {
@@ -2893,7 +2923,7 @@ var LocalProject = z6.object({
2893
2923
  * Folders outside the project folder where its sessions may also work, such as where its git worktrees
2894
2924
  * go (start_session's cwd, decision of 2026-10-14). None: only the project folder and folders inside it.
2895
2925
  */
2896
- extraFolders: z6.array(z6.string().min(1).refine((f) => path13.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
2926
+ extraFolders: z6.array(z6.string().min(1).refine((f) => path14.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
2897
2927
  model: z6.string().optional(),
2898
2928
  /** Its manager's model; none: the same as its sessions (decision of 2026-10-13). */
2899
2929
  managerModel: z6.string().optional(),
@@ -3010,7 +3040,7 @@ var Config = z6.object({
3010
3040
  * One that is missing or older than the SDK needs is never run: sessions are refused and the page says how to fix it
3011
3041
  * (Martin, 2026-10-27). "bundled" only when set here. Containers run their own.
3012
3042
  */
3013
- claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path13.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
3043
+ claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path14.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
3014
3044
  /** The shell a "!" command from the message box runs in on Windows (issue #212): Git Bash, as Claude's Bash tool, or PowerShell. */
3015
3045
  windowsShell: z6.enum(["bash", "powershell"]).default("bash"),
3016
3046
  board: BoardSetting,
@@ -3043,9 +3073,9 @@ function boardConfig(config) {
3043
3073
  }
3044
3074
  function configPath(packaged = PACKAGED) {
3045
3075
  const set = envSetting("CONFIG");
3046
- if (set) return path13.resolve(set);
3047
- if (packaged) return path13.join(userDataDir(), "ledung.config.json");
3048
- return path13.join(appDir(), "ledung.config.local.json");
3076
+ if (set) return path14.resolve(set);
3077
+ if (packaged) return path14.join(userDataDir(), "ledung.config.json");
3078
+ return path14.join(appDir(), "ledung.config.local.json");
3049
3079
  }
3050
3080
  function parseConfig(raw) {
3051
3081
  const result = Config.safeParse(raw);
@@ -3084,7 +3114,7 @@ var FIRST_CONFIG = {
3084
3114
  port: DEFAULT_PORT2
3085
3115
  };
3086
3116
  function createDefaultConfig(file = configPath()) {
3087
- fs10.mkdirSync(path13.dirname(file), { recursive: true });
3117
+ fs10.mkdirSync(path14.dirname(file), { recursive: true });
3088
3118
  try {
3089
3119
  fs10.writeFileSync(file, `${JSON.stringify(FIRST_CONFIG, null, 2)}
3090
3120
  `, { flag: "wx" });
@@ -3163,7 +3193,7 @@ function configProblem(text2) {
3163
3193
 
3164
3194
  // src/config-watch.ts
3165
3195
  import fs11 from "node:fs";
3166
- import path14 from "node:path";
3196
+ import path15 from "node:path";
3167
3197
  var exercised358 = false;
3168
3198
  var ConfigWatcher = class {
3169
3199
  opts;
@@ -3302,9 +3332,9 @@ function realFile(file) {
3302
3332
  full = fs11.realpathSync.native(file);
3303
3333
  } catch (e) {
3304
3334
  if (e.code !== "ENOENT") throw e;
3305
- return { dir: fs11.realpathSync.native(path14.dirname(file)), name: path14.basename(file) };
3335
+ return { dir: fs11.realpathSync.native(path15.dirname(file)), name: path15.basename(file) };
3306
3336
  }
3307
- return { dir: path14.dirname(full), name: path14.basename(full) };
3337
+ return { dir: path15.dirname(full), name: path15.basename(full) };
3308
3338
  }
3309
3339
 
3310
3340
  // src/platform/open-browser.ts
@@ -3336,12 +3366,12 @@ function lanNames(setting) {
3336
3366
  import { execFile as execFile4 } from "node:child_process";
3337
3367
  import fs13 from "node:fs";
3338
3368
  import os7 from "node:os";
3339
- import path16 from "node:path";
3369
+ import path17 from "node:path";
3340
3370
 
3341
3371
  // src/server/security.ts
3342
3372
  import crypto3 from "node:crypto";
3343
3373
  import fs12 from "node:fs";
3344
- import path15 from "node:path";
3374
+ import path16 from "node:path";
3345
3375
 
3346
3376
  // src/platform/running.ts
3347
3377
  import crypto from "node:crypto";
@@ -3540,7 +3570,7 @@ var Devices = class {
3540
3570
 
3541
3571
  // src/server/security.ts
3542
3572
  var COOKIE_NAME = "ledung_auth";
3543
- function loadOrCreateToken(file = path15.join(userDataDir(), "auth-token")) {
3573
+ function loadOrCreateToken(file = path16.join(userDataDir(), "auth-token")) {
3544
3574
  if (fs12.existsSync(file)) {
3545
3575
  const token2 = fs12.readFileSync(file, "utf8").trim();
3546
3576
  if (/^[0-9a-f]{64}$/.test(token2)) return token2;
@@ -3732,18 +3762,18 @@ var LABELS = { desktop: "Desktop", "start-menu": "Start menu", applications: "Ap
3732
3762
  var ENV_FILE = "shortcut.env";
3733
3763
  var LOG_FILE_SETTING = "LEDUNG_LOG_FILE";
3734
3764
  function launchTarget(dataDir = userDataDir(), app = appDir(), packaged = PACKAGED) {
3735
- const releaseLauncher = path16.join(dataDir, "launcher.mjs");
3736
- const fromRelease = !path16.relative(path16.join(dataDir, "app"), app).startsWith("..") && fs13.existsSync(releaseLauncher);
3737
- const script = fromRelease ? releaseLauncher : path16.join(app, packaged ? path16.join("dist", "cli.js") : path16.join("src", "cli.ts"));
3765
+ const releaseLauncher = path17.join(dataDir, "launcher.mjs");
3766
+ const fromRelease = !path17.relative(path17.join(dataDir, "app"), app).startsWith("..") && fs13.existsSync(releaseLauncher);
3767
+ const script = fromRelease ? releaseLauncher : path17.join(app, packaged ? path17.join("dist", "cli.js") : path17.join("src", "cli.ts"));
3738
3768
  const env = {};
3739
3769
  for (const name of ["DATA_DIR", "CONFIG"]) {
3740
3770
  const value = envSetting(name);
3741
- if (value) env[`LEDUNG_${name}`] = path16.resolve(value);
3771
+ if (value) env[`LEDUNG_${name}`] = path17.resolve(value);
3742
3772
  }
3743
3773
  return Object.keys(env).length ? { node: process.execPath, script, env } : { node: process.execPath, script };
3744
3774
  }
3745
3775
  function startsInBackground(launch) {
3746
- return path16.basename(launch.script) !== "launcher.mjs";
3776
+ return path17.basename(launch.script) !== "launcher.mjs";
3747
3777
  }
3748
3778
  function envFileText(env) {
3749
3779
  return Object.entries(env).map(([name, value]) => {
@@ -3765,9 +3795,9 @@ function starterText(head, tail2) {
3765
3795
  ].join("\r\n");
3766
3796
  }
3767
3797
  function iconFiles(app = appDir()) {
3768
- const built = path16.join(app, "web", "dist");
3769
- const dir = fs13.existsSync(path16.join(built, "icon-256.png")) ? built : path16.join(app, "web", "public");
3770
- return { png256: path16.join(dir, "icon-256.png"), png512: path16.join(dir, "icon-512.png") };
3798
+ const built = path17.join(app, "web", "dist");
3799
+ const dir = fs13.existsSync(path17.join(built, "icon-256.png")) ? built : path17.join(app, "web", "public");
3800
+ return { png256: path17.join(dir, "icon-256.png"), png512: path17.join(dir, "icon-512.png") };
3771
3801
  }
3772
3802
  function powershell(script, env) {
3773
3803
  return new Promise((resolve, reject) => {
@@ -3783,20 +3813,20 @@ async function windowsFolders() {
3783
3813
  return { desktop, "start-menu": programs, startup };
3784
3814
  }
3785
3815
  function macFolders() {
3786
- const startup = path16.join(os7.homedir(), "Library", "LaunchAgents");
3816
+ const startup = path17.join(os7.homedir(), "Library", "LaunchAgents");
3787
3817
  try {
3788
3818
  fs13.accessSync("/Applications", fs13.constants.W_OK);
3789
3819
  return { applications: "/Applications", startup };
3790
3820
  } catch {
3791
- return { applications: path16.join(os7.homedir(), "Applications"), startup };
3821
+ return { applications: path17.join(os7.homedir(), "Applications"), startup };
3792
3822
  }
3793
3823
  }
3794
3824
  function linuxFolders() {
3795
- return { startup: path16.join(process.env.XDG_CONFIG_HOME ?? path16.join(os7.homedir(), ".config"), "autostart") };
3825
+ return { startup: path17.join(process.env.XDG_CONFIG_HOME ?? path17.join(os7.homedir(), ".config"), "autostart") };
3796
3826
  }
3797
3827
  function ownDataFolders(platform) {
3798
- const root = path16.join(userDataDir(), "shortcuts");
3799
- return platform === "win32" ? { desktop: path16.join(root, "Desktop"), "start-menu": path16.join(root, "Start menu"), startup: path16.join(root, "Startup") } : platform === "darwin" ? { applications: path16.join(root, "Applications"), startup: path16.join(root, "LaunchAgents") } : { startup: path16.join(root, "autostart") };
3828
+ const root = path17.join(userDataDir(), "shortcuts");
3829
+ return platform === "win32" ? { desktop: path17.join(root, "Desktop"), "start-menu": path17.join(root, "Start menu"), startup: path17.join(root, "Startup") } : platform === "darwin" ? { applications: path17.join(root, "Applications"), startup: path17.join(root, "LaunchAgents") } : { startup: path17.join(root, "autostart") };
3800
3830
  }
3801
3831
  var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
3802
3832
  var winQuote = (s) => `"${s}"`;
@@ -3871,7 +3901,7 @@ var Shortcuts = class {
3871
3901
  return ids.flatMap((id) => {
3872
3902
  const folder = folders[id];
3873
3903
  if (!folder) return [];
3874
- const file = path16.join(folder, kind === "windows" ? "Ledung.lnk" : "Ledung.app");
3904
+ const file = path17.join(folder, kind === "windows" ? "Ledung.lnk" : "Ledung.app");
3875
3905
  return [{ id, label: LABELS[id], file, present: kind === "windows" ? fs13.existsSync(file) : isOurApp(file) }];
3876
3906
  });
3877
3907
  }
@@ -3880,7 +3910,7 @@ var Shortcuts = class {
3880
3910
  const folder = (await this.folders()).startup;
3881
3911
  if (!folder) return null;
3882
3912
  const kind = this.kind();
3883
- return path16.join(folder, SIGN_IN_FILES[kind]);
3913
+ return path17.join(folder, SIGN_IN_FILES[kind]);
3884
3914
  }
3885
3915
  async signInView() {
3886
3916
  const label = SIGN_IN_LABELS[this.kind()];
@@ -3891,7 +3921,7 @@ var Shortcuts = class {
3891
3921
  async view() {
3892
3922
  const kind = this.kind();
3893
3923
  const asked = this.opts.meta.get(ASKED_KEY) !== null;
3894
- const log = { file: this.logFile(), active: envSetting("LOG_FILE") !== void 0 && path16.resolve(envSetting("LOG_FILE")) === path16.resolve(this.logFile()) };
3924
+ const log = { file: this.logFile(), active: envSetting("LOG_FILE") !== void 0 && path17.resolve(envSetting("LOG_FILE")) === path17.resolve(this.logFile()) };
3895
3925
  const signIn = await this.signInView();
3896
3926
  if (kind === "other") {
3897
3927
  return { kind, available: false, reason: "Shortcuts are made on Windows and macOS. Here, start Ledung with ledung.sh or the ledung command.", asked, places: [], signIn, log };
@@ -3918,7 +3948,7 @@ var Shortcuts = class {
3918
3948
  if (this.kind() === "windows") await this.addWindows(places.filter((p) => !p.present));
3919
3949
  else {
3920
3950
  for (const p of places.filter((p2) => !p2.present)) {
3921
- if (fs13.existsSync(p.file)) throw new Error(`There is already a Ledung.app in ${path16.dirname(p.file)} that Ledung didn't make. Rename or remove it, then add the shortcut again.`);
3951
+ if (fs13.existsSync(p.file)) throw new Error(`There is already a Ledung.app in ${path17.dirname(p.file)} that Ledung didn't make. Rename or remove it, then add the shortcut again.`);
3922
3952
  this.addMac(p.file);
3923
3953
  }
3924
3954
  }
@@ -3931,16 +3961,16 @@ var Shortcuts = class {
3931
3961
  return this.view();
3932
3962
  }
3933
3963
  icoFile() {
3934
- return path16.join(this.opts.iconDir, "Ledung.ico");
3964
+ return path17.join(this.opts.iconDir, "Ledung.ico");
3935
3965
  }
3936
3966
  envFile() {
3937
- return path16.join(this.opts.iconDir, ENV_FILE);
3967
+ return path17.join(this.opts.iconDir, ENV_FILE);
3938
3968
  }
3939
3969
  starterFile() {
3940
- return path16.join(this.opts.iconDir, STARTER_FILE);
3970
+ return path17.join(this.opts.iconDir, STARTER_FILE);
3941
3971
  }
3942
3972
  logFile() {
3943
- return this.opts.logFile ?? path16.join(this.opts.iconDir, LOG_FILE);
3973
+ return this.opts.logFile ?? path17.join(this.opts.iconDir, LOG_FILE);
3944
3974
  }
3945
3975
  /** The env file and the start script, which the icons and the start at sign-in share. */
3946
3976
  removeShared() {
@@ -3972,11 +4002,11 @@ var Shortcuts = class {
3972
4002
  }
3973
4003
  /** The Windows Script Host that runs the start script with no window, and its arguments. */
3974
4004
  windowsStart(extra) {
3975
- const wscript = path16.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
4005
+ const wscript = path17.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
3976
4006
  return { target: wscript, arguments: ["//Nologo", this.starterFile(), ...extra].map(winQuote).join(" ") };
3977
4007
  }
3978
4008
  async writeLinks(files, extra, icon, description) {
3979
- for (const file of files) fs13.mkdirSync(path16.dirname(file), { recursive: true });
4009
+ for (const file of files) fs13.mkdirSync(path17.dirname(file), { recursive: true });
3980
4010
  const spec = { files, ...this.windowsStart(extra), workdir: os7.homedir(), icon: icon ? `${icon},0` : "", description };
3981
4011
  await powershell([
3982
4012
  "$spec = $env:SHORTCUT_SPEC | ConvertFrom-Json",
@@ -4007,7 +4037,7 @@ var Shortcuts = class {
4007
4037
  return;
4008
4038
  }
4009
4039
  this.writeEnvFile();
4010
- fs13.mkdirSync(path16.dirname(file), { recursive: true });
4040
+ fs13.mkdirSync(path17.dirname(file), { recursive: true });
4011
4041
  const { node, script } = this.opts.launch;
4012
4042
  const kind = this.kind();
4013
4043
  if (kind === "windows") {
@@ -4037,18 +4067,18 @@ var Shortcuts = class {
4037
4067
  */
4038
4068
  addMac(app) {
4039
4069
  const { node, script } = this.opts.launch;
4040
- const contents = path16.join(app, "Contents");
4041
- fs13.mkdirSync(path16.join(contents, "MacOS"), { recursive: true });
4042
- fs13.mkdirSync(path16.join(contents, "Resources"), { recursive: true });
4070
+ const contents = path17.join(app, "Contents");
4071
+ fs13.mkdirSync(path17.join(contents, "MacOS"), { recursive: true });
4072
+ fs13.mkdirSync(path17.join(contents, "Resources"), { recursive: true });
4043
4073
  let version = "1";
4044
4074
  try {
4045
- version = String(JSON.parse(fs13.readFileSync(path16.join(appDir(), "package.json"), "utf8")).version ?? "1");
4075
+ version = String(JSON.parse(fs13.readFileSync(path17.join(appDir(), "package.json"), "utf8")).version ?? "1");
4046
4076
  } catch {
4047
4077
  }
4048
- fs13.writeFileSync(path16.join(contents, "Info.plist"), PLIST(version));
4049
- fs13.writeFileSync(path16.join(contents, "Resources", "Ledung.icns"), icnsFromPngs([fs13.readFileSync(this.opts.icons.png256), fs13.readFileSync(this.opts.icons.png512)]));
4078
+ fs13.writeFileSync(path17.join(contents, "Info.plist"), PLIST(version));
4079
+ fs13.writeFileSync(path17.join(contents, "Resources", "Ledung.icns"), icnsFromPngs([fs13.readFileSync(this.opts.icons.png256), fs13.readFileSync(this.opts.icons.png512)]));
4050
4080
  const run8 = [...this.nodeOptions(), script].map(shQuote).join(" ");
4051
- fs13.writeFileSync(path16.join(contents, "MacOS", "Ledung"), [
4081
+ fs13.writeFileSync(path17.join(contents, "MacOS", "Ledung"), [
4052
4082
  "#!/bin/sh",
4053
4083
  "# Opens a running Ledung's page, or starts Ledung in the background. Made by Ledung's Settings (issues #415, #456).",
4054
4084
  ...findNode(node),
@@ -4093,7 +4123,7 @@ function signInScript(node, run8, fallbackShell) {
4093
4123
  }
4094
4124
  function isOurApp(app) {
4095
4125
  try {
4096
- return fs13.readFileSync(path16.join(app, "Contents", "Info.plist"), "utf8").includes(`<string>${BUNDLE_ID}</string>`);
4126
+ return fs13.readFileSync(path17.join(app, "Contents", "Info.plist"), "utf8").includes(`<string>${BUNDLE_ID}</string>`);
4097
4127
  } catch {
4098
4128
  return false;
4099
4129
  }
@@ -4105,13 +4135,13 @@ import fs32 from "node:fs";
4105
4135
  import http from "node:http";
4106
4136
  import os15 from "node:os";
4107
4137
  import tls3 from "node:tls";
4108
- import path33 from "node:path";
4138
+ import path35 from "node:path";
4109
4139
  import { duplexPair } from "node:stream";
4110
4140
  import { WebSocketServer } from "ws";
4111
4141
 
4112
4142
  // src/server/source.ts
4113
4143
  import fs14 from "node:fs";
4114
- import path17 from "node:path";
4144
+ import path18 from "node:path";
4115
4145
 
4116
4146
  // src/shared/version.ts
4117
4147
  function displayVersion(version) {
@@ -4154,7 +4184,7 @@ function sourceOffer(o) {
4154
4184
  }
4155
4185
  function readRepository(appDir2) {
4156
4186
  try {
4157
- return repositoryUrl(JSON.parse(fs14.readFileSync(path17.join(appDir2, "package.json"), "utf8"))) ?? DEFAULT_REPOSITORY;
4187
+ return repositoryUrl(JSON.parse(fs14.readFileSync(path18.join(appDir2, "package.json"), "utf8"))) ?? DEFAULT_REPOSITORY;
4158
4188
  } catch {
4159
4189
  return DEFAULT_REPOSITORY;
4160
4190
  }
@@ -4755,7 +4785,7 @@ ${lines.join("\n")}`;
4755
4785
  // src/sessions/manager.ts
4756
4786
  import fs27 from "node:fs";
4757
4787
  import os14 from "node:os";
4758
- import path28 from "node:path";
4788
+ import path29 from "node:path";
4759
4789
 
4760
4790
  // src/sessions/remote-control.ts
4761
4791
  function remoteControlName(role, project, enabled) {
@@ -4828,7 +4858,7 @@ var RemoteControl = class {
4828
4858
  // src/runners/shell-run.ts
4829
4859
  import { spawn as spawn2 } from "node:child_process";
4830
4860
  import fs15 from "node:fs";
4831
- import path18 from "node:path";
4861
+ import path19 from "node:path";
4832
4862
 
4833
4863
  // src/platform/process-tree.ts
4834
4864
  import { execFile as execFile5 } from "node:child_process";
@@ -4901,11 +4931,11 @@ function gitBashPath(env = process.env, exists = fs15.existsSync) {
4901
4931
  if (set && exists(set)) return set;
4902
4932
  const dirs = (env.PATH ?? env.Path ?? "").split(";").filter(Boolean);
4903
4933
  for (const dir of dirs) {
4904
- if (!exists(path18.join(dir, "git.exe"))) continue;
4905
- for (const bash of [path18.join(dir, "..", "bin", "bash.exe"), path18.join(dir, "bash.exe")]) if (exists(bash)) return path18.resolve(bash);
4934
+ if (!exists(path19.join(dir, "git.exe"))) continue;
4935
+ for (const bash of [path19.join(dir, "..", "bin", "bash.exe"), path19.join(dir, "bash.exe")]) if (exists(bash)) return path19.resolve(bash);
4906
4936
  }
4907
- for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && path18.join(env.LOCALAPPDATA, "Programs")]) {
4908
- const bash = root && path18.join(root, "Git", "bin", "bash.exe");
4937
+ for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && path19.join(env.LOCALAPPDATA, "Programs")]) {
4938
+ const bash = root && path19.join(root, "Git", "bin", "bash.exe");
4909
4939
  if (bash && exists(bash)) return bash;
4910
4940
  }
4911
4941
  return null;
@@ -5060,16 +5090,16 @@ function bangRefusal(project, folder, others) {
5060
5090
  import { execFile as execFile6 } from "node:child_process";
5061
5091
  import fs16 from "node:fs";
5062
5092
  import os8 from "node:os";
5063
- import path19 from "node:path";
5093
+ import path20 from "node:path";
5064
5094
  import { promisify as promisify2 } from "node:util";
5065
5095
  var run2 = promisify2(execFile6);
5066
5096
  var BrowseError = class extends Error {
5067
5097
  };
5068
5098
  function defaultRoots(configured) {
5069
- return (configured?.length ? configured : [os8.homedir()]).map((r) => path19.resolve(r));
5099
+ return (configured?.length ? configured : [os8.homedir()]).map((r) => path20.resolve(r));
5070
5100
  }
5071
5101
  function defaultProjectsFolder(configured) {
5072
- return path19.resolve(configured ?? path19.join(os8.homedir(), "Projects"));
5102
+ return path20.resolve(configured ?? path20.join(os8.homedir(), "Projects"));
5073
5103
  }
5074
5104
  function real(p) {
5075
5105
  try {
@@ -5084,12 +5114,12 @@ function isInside(folder, roots) {
5084
5114
  return roots.some((r) => {
5085
5115
  const root = real(r);
5086
5116
  if (!root) return false;
5087
- const rel = path19.relative(root, target);
5088
- return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
5117
+ const rel = path20.relative(root, target);
5118
+ return rel === "" || !rel.startsWith("..") && !path20.isAbsolute(rel);
5089
5119
  });
5090
5120
  }
5091
5121
  function gitBranch(folder) {
5092
- const dotGit = path19.join(folder, ".git");
5122
+ const dotGit = path20.join(folder, ".git");
5093
5123
  let stat;
5094
5124
  try {
5095
5125
  stat = fs16.statSync(dotGit);
@@ -5098,7 +5128,7 @@ function gitBranch(folder) {
5098
5128
  }
5099
5129
  if (!stat.isDirectory()) return "";
5100
5130
  try {
5101
- const head = fs16.readFileSync(path19.join(dotGit, "HEAD"), "utf8").trim();
5131
+ const head = fs16.readFileSync(path20.join(dotGit, "HEAD"), "utf8").trim();
5102
5132
  return head.startsWith("ref: refs/heads/") ? head.slice("ref: refs/heads/".length) : "";
5103
5133
  } catch {
5104
5134
  return "";
@@ -5106,7 +5136,7 @@ function gitBranch(folder) {
5106
5136
  }
5107
5137
  var sameFolder2 = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
5108
5138
  function listLocal(folder, roots, projects) {
5109
- const dir = path19.resolve(folder || roots[0]);
5139
+ const dir = path20.resolve(folder || roots[0]);
5110
5140
  if (!isInside(dir, roots)) throw new BrowseError("That folder is outside the folders Ledung may show.");
5111
5141
  let names;
5112
5142
  try {
@@ -5117,7 +5147,7 @@ function listLocal(folder, roots, projects) {
5117
5147
  const entries = [];
5118
5148
  for (const d of names) {
5119
5149
  if (d.name.startsWith(".") || d.name.startsWith("$")) continue;
5120
- const full = path19.join(dir, d.name);
5150
+ const full = path20.join(dir, d.name);
5121
5151
  if (d.isSymbolicLink()) {
5122
5152
  if (!isInside(full, roots)) continue;
5123
5153
  try {
@@ -5126,10 +5156,10 @@ function listLocal(folder, roots, projects) {
5126
5156
  continue;
5127
5157
  }
5128
5158
  } else if (!d.isDirectory()) continue;
5129
- entries.push({ name: d.name, path: full, git: gitBranch(full), project: projects.find((p) => sameFolder2(path19.resolve(p.path), full))?.name ?? null });
5159
+ entries.push({ name: d.name, path: full, git: gitBranch(full), project: projects.find((p) => sameFolder2(path20.resolve(p.path), full))?.name ?? null });
5130
5160
  }
5131
5161
  entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
5132
- const up = path19.dirname(dir);
5162
+ const up = path20.dirname(dir);
5133
5163
  const isRoot = roots.some((r) => sameFolder2(r, dir));
5134
5164
  return { path: dir, parent: !isRoot && up !== dir && isInside(up, roots) ? up : null, roots, entries };
5135
5165
  }
@@ -5180,7 +5210,7 @@ async function listContainer(container, user, folder, projects) {
5180
5210
  const [dir = "/", ...lines] = out.split("\n").map((l) => l.replace(/\r$/, ""));
5181
5211
  const entries = lines.filter(Boolean).map((l) => {
5182
5212
  const [kind, name = "", branch = ""] = l.split(" ");
5183
- const full = path19.posix.join(dir, name);
5213
+ const full = path20.posix.join(dir, name);
5184
5214
  return {
5185
5215
  name,
5186
5216
  path: full,
@@ -5188,12 +5218,12 @@ async function listContainer(container, user, folder, projects) {
5188
5218
  project: projects.find((p) => p.container === container && p.path === full)?.name ?? null
5189
5219
  };
5190
5220
  });
5191
- return { path: dir, parent: dir === "/" ? null : path19.posix.dirname(dir), roots: ["/"], entries };
5221
+ return { path: dir, parent: dir === "/" ? null : path20.posix.dirname(dir), roots: ["/"], entries };
5192
5222
  }
5193
5223
  async function createProjectFolder(parent, name, allowed, github) {
5194
- const parentDir = path19.resolve(parent);
5224
+ const parentDir = path20.resolve(parent);
5195
5225
  if (!isInside(parentDir, allowed)) throw new BrowseError("That folder is outside the folders Ledung may use.");
5196
- const dir = path19.join(parentDir, name);
5226
+ const dir = path20.join(parentDir, name);
5197
5227
  if (fs16.existsSync(dir)) throw new BrowseError(`There is already a folder named "${name}" there.`);
5198
5228
  fs16.mkdirSync(dir);
5199
5229
  try {
@@ -5221,19 +5251,19 @@ import crypto4 from "node:crypto";
5221
5251
  import { EventEmitter } from "node:events";
5222
5252
  import fs17 from "node:fs";
5223
5253
  import net from "node:net";
5224
- import path20 from "node:path";
5254
+ import path21 from "node:path";
5225
5255
  import readline from "node:readline";
5226
5256
  import { PassThrough } from "node:stream";
5227
5257
  function hostsDir() {
5228
- const dir = path20.join(userDataDir(), "hosts");
5258
+ const dir = path21.join(userDataDir(), "hosts");
5229
5259
  fs17.mkdirSync(dir, { recursive: true });
5230
5260
  return dir;
5231
5261
  }
5232
- var recordFile = (sessionId) => path20.join(hostsDir(), `${sessionId}.json`);
5262
+ var recordFile = (sessionId) => path21.join(hostsDir(), `${sessionId}.json`);
5233
5263
  function listHostRecords() {
5234
5264
  return fs17.readdirSync(hostsDir()).filter((f) => f.endsWith(".json")).flatMap((f) => {
5235
5265
  try {
5236
- return [JSON.parse(fs17.readFileSync(path20.join(hostsDir(), f), "utf8"))];
5266
+ return [JSON.parse(fs17.readFileSync(path21.join(hostsDir(), f), "utf8"))];
5237
5267
  } catch {
5238
5268
  return [];
5239
5269
  }
@@ -5256,7 +5286,7 @@ function startHost(sessionId, launch) {
5256
5286
  sessionId,
5257
5287
  pipe: pipePath(sessionId),
5258
5288
  graceMs: GRACE_MS,
5259
- logFile: path20.join(hostsDir(), `${sessionId}.log`),
5289
+ logFile: path21.join(hostsDir(), `${sessionId}.log`),
5260
5290
  secret: crypto4.randomBytes(32).toString("hex")
5261
5291
  };
5262
5292
  const file = recordFile(sessionId);
@@ -5851,7 +5881,7 @@ var DockerRunner = class {
5851
5881
  import { execFile as execFile9 } from "node:child_process";
5852
5882
  import fs18 from "node:fs";
5853
5883
  import os9 from "node:os";
5854
- import path21 from "node:path";
5884
+ import path22 from "node:path";
5855
5885
  import { promisify as promisify5 } from "node:util";
5856
5886
  async function readStart(file, bytes) {
5857
5887
  try {
@@ -5910,24 +5940,24 @@ var LocalRunner = class {
5910
5940
  return ghApiAnswer("gh", ["api", ...args], input, timeoutMs);
5911
5941
  }
5912
5942
  async pluginSettingsTexts(configDir) {
5913
- const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path21.join(os9.homedir(), ".claude");
5914
- const files = [...PLUGIN_FILES.config.map((f) => path21.join(dir, f)), ...PLUGIN_FILES.project.map((f) => path21.join(this.cwd, f))];
5943
+ const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path22.join(os9.homedir(), ".claude");
5944
+ const files = [...PLUGIN_FILES.config.map((f) => path22.join(dir, f)), ...PLUGIN_FILES.project.map((f) => path22.join(this.cwd, f))];
5915
5945
  const texts = await Promise.all(files.map((f) => fs18.promises.readFile(f, "utf8").catch(() => null)));
5916
5946
  return texts.filter((t) => t !== null);
5917
5947
  }
5918
5948
  async skillFiles(configDir) {
5919
- const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path21.join(os9.homedir(), ".claude");
5920
- const folders = async (rel) => (await fs18.promises.readdir(path21.join(dir, rel), { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => `${rel}/${e.name}`);
5949
+ const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path22.join(os9.homedir(), ".claude");
5950
+ const folders = async (rel) => (await fs18.promises.readdir(path22.join(dir, rel), { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => `${rel}/${e.name}`);
5921
5951
  const personal = (await folders("skills")).filter((f) => f !== "skills/synced");
5922
5952
  const synced = (await Promise.all((await folders("skills/synced")).map(folders))).flat();
5923
5953
  const files = await Promise.all([...personal, ...synced].map(async (f) => {
5924
- const text2 = await readStart(path21.join(dir, f, "SKILL.md"), SKILL_HEAD_BYTES);
5954
+ const text2 = await readStart(path22.join(dir, f, "SKILL.md"), SKILL_HEAD_BYTES);
5925
5955
  return text2 === null ? null : { path: `${f}/SKILL.md`, text: text2 };
5926
5956
  }));
5927
5957
  return files.filter((f) => f !== null);
5928
5958
  }
5929
5959
  async readProjectFile(relative) {
5930
- return fs18.promises.readFile(path21.join(this.cwd, relative), "utf8").catch(() => null);
5960
+ return fs18.promises.readFile(path22.join(this.cwd, relative), "utf8").catch(() => null);
5931
5961
  }
5932
5962
  async pullRequestView(pr, repo, fields, timeoutMs) {
5933
5963
  try {
@@ -6342,12 +6372,12 @@ function cpuTimes() {
6342
6372
 
6343
6373
  // src/sessions/work-folder.ts
6344
6374
  import fs20 from "node:fs";
6345
- import path22 from "node:path";
6375
+ import path23 from "node:path";
6346
6376
  var WorkFolderError = class extends Error {
6347
6377
  };
6348
6378
  function within2(root, real2) {
6349
- const inside = path22.relative(root, real2);
6350
- return inside === "" || !inside.startsWith("..") && !path22.isAbsolute(inside);
6379
+ const inside = path23.relative(root, real2);
6380
+ return inside === "" || !inside.startsWith("..") && !path23.isAbsolute(inside);
6351
6381
  }
6352
6382
  function realFolder(folder) {
6353
6383
  try {
@@ -6361,11 +6391,11 @@ function resolveWorkFolder(projectPath, extraRoots, wanted) {
6361
6391
  if (!wanted.trim()) throw new WorkFolderError("No folder given.");
6362
6392
  const project = realFolder(projectPath);
6363
6393
  if (!project) throw new WorkFolderError(`The project folder ${projectPath} does not exist.`);
6364
- const given = path22.resolve(project, wanted.trim());
6394
+ const given = path23.resolve(project, wanted.trim());
6365
6395
  if (!fs20.existsSync(given)) throw new WorkFolderError(`The folder ${given} does not exist.`);
6366
6396
  const real2 = realFolder(given);
6367
6397
  if (!real2) throw new WorkFolderError(`${given} is not a folder.`);
6368
- if (within2(project, real2)) return path22.relative(project, real2) === "" ? null : real2;
6398
+ if (within2(project, real2)) return path23.relative(project, real2) === "" ? null : real2;
6369
6399
  for (const root of extraRoots) {
6370
6400
  const realRoot = realFolder(root);
6371
6401
  if (realRoot && within2(realRoot, real2)) return real2;
@@ -6378,7 +6408,7 @@ function resolveWorkFolder(projectPath, extraRoots, wanted) {
6378
6408
 
6379
6409
  // src/sessions/files.ts
6380
6410
  import fs21 from "node:fs";
6381
- import path23 from "node:path";
6411
+ import path24 from "node:path";
6382
6412
  var MAX_BYTES2 = 2 * 1024 * 1024;
6383
6413
  var FileError = class extends Error {
6384
6414
  };
@@ -6393,16 +6423,16 @@ function resolveInside(root, link) {
6393
6423
  let real2;
6394
6424
  try {
6395
6425
  realRoot = fs21.realpathSync(root);
6396
- real2 = fs21.realpathSync(path23.resolve(realRoot, wanted));
6426
+ real2 = fs21.realpathSync(path24.resolve(realRoot, wanted));
6397
6427
  } catch {
6398
6428
  throw new FileError(`There is no file ${wanted} in the project.`);
6399
6429
  }
6400
- const inside = path23.relative(realRoot, real2);
6401
- if (!inside || inside.startsWith("..") || path23.isAbsolute(inside)) {
6430
+ const inside = path24.relative(realRoot, real2);
6431
+ if (!inside || inside.startsWith("..") || path24.isAbsolute(inside)) {
6402
6432
  throw new FileError("Only files inside the project folder can be shown.");
6403
6433
  }
6404
6434
  if (fs21.statSync(real2).isDirectory()) throw new FileError(`${inside} is a folder; only files can be shown.`);
6405
- return { real: real2, inside: inside.split(path23.sep).join("/") };
6435
+ return { real: real2, inside: inside.split(path24.sep).join("/") };
6406
6436
  }
6407
6437
  function hiddenProblem(root, link, roots, refused = []) {
6408
6438
  const { real: typed, inside } = resolveInside(root, link);
@@ -6417,14 +6447,14 @@ function hiddenProblem(root, link, roots, refused = []) {
6417
6447
  for (const folder of refused) {
6418
6448
  const at2 = longName2(folder);
6419
6449
  if (!at2) continue;
6420
- const rel = path23.relative(at2, real2);
6421
- if (rel === "" || !rel.startsWith("..") && !path23.isAbsolute(rel)) return `${inside} is not shown: it is inside ${folder}, where settings and sign-ins are kept.`;
6450
+ const rel = path24.relative(at2, real2);
6451
+ if (rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel)) return `${inside} is not shown: it is inside ${folder}, where settings and sign-ins are kept.`;
6422
6452
  }
6423
6453
  const from = (r) => {
6424
6454
  const at2 = longName2(r);
6425
- return at2 === null ? null : path23.relative(at2, real2);
6455
+ return at2 === null ? null : path24.relative(at2, real2);
6426
6456
  };
6427
- const under = roots.map(from).find((rel) => rel !== null && rel !== "" && !rel.startsWith("..") && !path23.isAbsolute(rel));
6457
+ const under = roots.map(from).find((rel) => rel !== null && rel !== "" && !rel.startsWith("..") && !path24.isAbsolute(rel));
6428
6458
  const parts2 = (under ?? from(root) ?? inside).split(/[\\/]/);
6429
6459
  return parts2.some((p) => p.startsWith(".")) ? `${inside} is in a hidden folder or is a hidden file; a chat's file viewer doesn't show those.` : null;
6430
6460
  }
@@ -6469,7 +6499,7 @@ function handoffProblem(root, file, now = Date.now()) {
6469
6499
  throw e;
6470
6500
  }
6471
6501
  if (!found.content.trim()) return `The handoff file ${found.path} is empty; write the handoff first.`;
6472
- const age = now - fs21.statSync(path23.join(fs21.realpathSync(root), found.path)).mtimeMs;
6502
+ const age = now - fs21.statSync(path24.join(fs21.realpathSync(root), found.path)).mtimeMs;
6473
6503
  if (age > HANDOFF_MAX_AGE_MS) {
6474
6504
  return `The handoff file ${found.path} was last changed ${Math.round(age / 6e4)} minutes ago; write the handoff first (it must be changed within the last hour).`;
6475
6505
  }
@@ -6478,7 +6508,7 @@ function handoffProblem(root, file, now = Date.now()) {
6478
6508
 
6479
6509
  // src/sessions/tree.ts
6480
6510
  import fs22 from "node:fs";
6481
- import path24 from "node:path";
6511
+ import path25 from "node:path";
6482
6512
 
6483
6513
  // src/sessions/git-read.ts
6484
6514
  import { spawn as spawn5 } from "node:child_process";
@@ -6560,18 +6590,18 @@ function longName(p) {
6560
6590
  }
6561
6591
  }
6562
6592
  function within3(root, real2) {
6563
- const rel = path24.relative(root, real2);
6564
- return rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel) ? rel : null;
6593
+ const rel = path25.relative(root, real2);
6594
+ return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel) ? rel : null;
6565
6595
  }
6566
6596
  function problem(realRoot, real2, rules, refused) {
6567
6597
  const inside = within3(realRoot, real2);
6568
6598
  if (inside === null) return "Only folders inside the session's folder can be listed.";
6569
- if (inside.split(path24.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
6599
+ if (inside.split(path25.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
6570
6600
  if (refused.some((r) => within3(r, real2) !== null)) return "Folders where settings and sign-ins are kept are not listed.";
6571
6601
  for (const r of rules.hiddenFrom) {
6572
6602
  const at2 = longName(r);
6573
6603
  const rel = at2 === null ? null : within3(at2, real2);
6574
- if (rel && rel.split(path24.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
6604
+ if (rel && rel.split(path25.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
6575
6605
  }
6576
6606
  return null;
6577
6607
  }
@@ -6579,9 +6609,9 @@ async function listTree(root, rel, rules) {
6579
6609
  const realRoot = longName(root);
6580
6610
  if (!realRoot) throw new FileError(`The folder ${root} does not exist.`);
6581
6611
  const refused = rules.refused.map(longName).filter((r) => r !== null);
6582
- if (path24.isAbsolute(rel) && /[^\\/]/.test(rel) && within3(realRoot, longName(rel) ?? path24.resolve(rel)) === null) throw new FileError("Only folders inside the session's folder can be listed.");
6612
+ if (path25.isAbsolute(rel) && /[^\\/]/.test(rel) && within3(realRoot, longName(rel) ?? path25.resolve(rel)) === null) throw new FileError("Only folders inside the session's folder can be listed.");
6583
6613
  const wanted = rel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
6584
- const real2 = longName(path24.resolve(realRoot, wanted));
6614
+ const real2 = longName(path25.resolve(realRoot, wanted));
6585
6615
  if (!real2) throw new FileError(`There is no folder ${wanted} in the session's folder.`);
6586
6616
  const why = problem(realRoot, real2, rules, refused);
6587
6617
  if (why) throw new FileError(why);
@@ -6592,11 +6622,11 @@ async function listTree(root, rel, rules) {
6592
6622
  } catch (e) {
6593
6623
  throw new FileError(`Can't read that folder: ${e.code ?? e.message}`);
6594
6624
  }
6595
- const at2 = within3(realRoot, real2).split(path24.sep).filter(Boolean).join("/");
6625
+ const at2 = within3(realRoot, real2).split(path25.sep).filter(Boolean).join("/");
6596
6626
  const entries = [];
6597
6627
  for (const d of dirents) {
6598
6628
  if (hiddenName(d.name)) continue;
6599
- const full = path24.join(real2, d.name);
6629
+ const full = path25.join(real2, d.name);
6600
6630
  let dir;
6601
6631
  let size = null;
6602
6632
  const target = d.isSymbolicLink() ? longName(full) : full;
@@ -6745,15 +6775,15 @@ function checkLineComments(value) {
6745
6775
  for (const [i, c] of value.entries()) {
6746
6776
  const bad = (what) => ({ problem: `Line comment ${i + 1}: ${what}.` });
6747
6777
  if (!c || typeof c !== "object") return bad("not a comment");
6748
- const { path: path41, line, side, lineText, commit, body } = c;
6749
- if (typeof path41 !== "string" || !path41.trim() || path41.length > 500 || /[\r\n]/.test(path41)) return bad("the file is missing");
6778
+ const { path: path43, line, side, lineText, commit, body } = c;
6779
+ if (typeof path43 !== "string" || !path43.trim() || path43.length > 500 || /[\r\n]/.test(path43)) return bad("the file is missing");
6750
6780
  if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return bad("the line number is missing");
6751
6781
  if (side !== "new" && side !== "old") return bad('the side is "new" or "old"');
6752
6782
  if (typeof lineText !== "string") return bad("the line's text is missing");
6753
6783
  if (typeof commit !== "string" || !/^[0-9a-f]{7,64}$/.test(commit)) return bad("the commit is missing");
6754
6784
  if (typeof body !== "string" || !body.trim()) return bad("it is empty");
6755
6785
  if (body.length > MAX_BODY) return bad(`it is longer than ${MAX_BODY} characters`);
6756
- comments.push({ path: path41, line, side, lineText: lineText.slice(0, 2e3), commit, body: body.trim() });
6786
+ comments.push({ path: path43, line, side, lineText: lineText.slice(0, 2e3), commit, body: body.trim() });
6757
6787
  }
6758
6788
  return { comments };
6759
6789
  }
@@ -7254,15 +7284,15 @@ ${note.trim()}` : "");
7254
7284
  return about?.shots && this.store.getSession(about.sessionId)?.parentId === sessionId ? about.shots : null;
7255
7285
  }
7256
7286
  return shots.map((s, i) => {
7257
- let path41;
7287
+ let path43;
7258
7288
  try {
7259
- path41 = this.ctx.shotPath(sessionId, s.file.trim().replace(/\\/g, "/"));
7289
+ path43 = this.ctx.shotPath(sessionId, s.file.trim().replace(/\\/g, "/"));
7260
7290
  } catch (e) {
7261
7291
  throw new ManagementError(`Screenshot ${i + 1} (${s.file}) can't be attached: ${e.message} Nothing was asked.`);
7262
7292
  }
7263
7293
  const clean = (t) => t?.trim() || null;
7264
7294
  return {
7265
- path: path41,
7295
+ path: path43,
7266
7296
  // One line: the group heads the strip and its shots' lines in the answer.
7267
7297
  group: clean(s.group && oneLine(s.group)) ?? "Screenshots",
7268
7298
  lookFor: s.look_for.trim(),
@@ -8835,7 +8865,6 @@ var InputQueue = class {
8835
8865
  }
8836
8866
  }
8837
8867
  };
8838
- var LOGIN_ENV = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "CLAUDE_SECURESTORAGE_CONFIG_DIR"];
8839
8868
  function claudeEnvironment(base, accountEnv, ledungInstructions) {
8840
8869
  const env = {};
8841
8870
  for (const [k, v] of Object.entries(base)) if (v !== void 0 && !(accountEnv && LOGIN_ENV.includes(k))) env[k] = v;
@@ -10094,7 +10123,7 @@ function unaskedTool(toolName, alwaysAsked) {
10094
10123
  // src/board/attachments.ts
10095
10124
  import crypto5 from "node:crypto";
10096
10125
  import fs24 from "node:fs";
10097
- import path25 from "node:path";
10126
+ import path26 from "node:path";
10098
10127
  var MAX_ATTACHMENTS = 5;
10099
10128
  var MAX_IMAGE_BYTES2 = 5 * 1024 * 1024;
10100
10129
  var MAX_TEXT_BYTES = 512 * 1024;
@@ -10111,7 +10140,7 @@ function newPostKey() {
10111
10140
  return crypto5.randomBytes(6).toString("hex");
10112
10141
  }
10113
10142
  function safeName(name) {
10114
- const base = path25.basename(name.replace(/\\/g, "/")).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
10143
+ const base = path26.basename(name.replace(/\\/g, "/")).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
10115
10144
  return (base || "file").slice(-80);
10116
10145
  }
10117
10146
  function evidencePath(threadId, postKey, n, kind) {
@@ -10163,7 +10192,7 @@ function readEvidence(root, link) {
10163
10192
  function outboxFile(cacheDir, repo, key, n, kind) {
10164
10193
  if (!/^[0-9a-f]{12}$/.test(key) || !Number.isInteger(n) || n < 1 || n > MAX_ATTACHMENTS) throw new FileError("Not a held attachment.");
10165
10194
  const rel = `outbox/${key}-${n}.${EXT[kind]}`;
10166
- return { file: path25.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
10195
+ return { file: path26.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
10167
10196
  }
10168
10197
  function contentProblem(ref, bytes) {
10169
10198
  if (bytes.length !== ref.size) return `its size is ${bytes.length} bytes, not the ${ref.size} the post says`;
@@ -10176,17 +10205,17 @@ function contentProblem(ref, bytes) {
10176
10205
  function repoCacheDir(cacheDir, repo) {
10177
10206
  const parts2 = repo.split("/");
10178
10207
  if (parts2.length !== 2 || parts2.some((p) => !/^[A-Za-z0-9_.-]+$/.test(p) || /^\.+$/.test(p))) throw new FileError(`"${repo}" is not a board repo.`);
10179
- return path25.join(cacheDir, parts2[0], parts2[1]);
10208
+ return path26.join(cacheDir, parts2[0], parts2[1]);
10180
10209
  }
10181
10210
  function cacheFile(cacheDir, repo, threadId, postId, n, kind) {
10182
10211
  if (!THREAD_ID.test(threadId)) throw new FileError(`"${threadId}" is not a thread id.`);
10183
10212
  const post2 = postId.replace(/[^A-Za-z0-9]+/g, "-").slice(0, 60);
10184
10213
  if (!post2 || !Number.isInteger(n) || n < 1 || n > MAX_ATTACHMENTS) throw new FileError("Not an attachment of a post.");
10185
10214
  const rel = `${threadId}/${post2}-${n}.${EXT[kind]}`;
10186
- return { file: path25.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
10215
+ return { file: path26.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
10187
10216
  }
10188
10217
  function writeCache(file, bytes) {
10189
- fs24.mkdirSync(path25.dirname(file), { recursive: true });
10218
+ fs24.mkdirSync(path26.dirname(file), { recursive: true });
10190
10219
  const tmp = `${file}.${process.pid}.tmp`;
10191
10220
  fs24.writeFileSync(tmp, bytes);
10192
10221
  fs24.renameSync(tmp, file);
@@ -10292,7 +10321,7 @@ function sensitiveInText(text2, rules) {
10292
10321
 
10293
10322
  // src/board/board.ts
10294
10323
  import fs25 from "node:fs";
10295
- import path26 from "node:path";
10324
+ import path27 from "node:path";
10296
10325
 
10297
10326
  // src/board/store.ts
10298
10327
  var unsentId = (repoPath) => `unsent-${repoPath}`;
@@ -10958,7 +10987,7 @@ ${fenced(p.text)}${this.attachmentText(p.id)}`;
10958
10987
  if (a.kind !== "text") return `- ${what}: to show it to ${this.ctx.userName}, give ask_user's screenshots file "${this.boardFile(a.cacheFile)}", or in a review its evidence file.`;
10959
10988
  let content = "";
10960
10989
  try {
10961
- content = fs25.readFileSync(path26.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), "utf8");
10990
+ content = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), "utf8");
10962
10991
  } catch {
10963
10992
  }
10964
10993
  const cut2 = content.length > TEXT_INLINE_CHARS ? `${content.slice(0, TEXT_INLINE_CHARS)}
@@ -11023,7 +11052,7 @@ ${lines.join("\n")}`;
11023
11052
  if (!a.snapshot || !a.kind || !this.ctx.cacheDir) throw new BoardError("access", "it was held without a copy of its files; nothing was sent. Post again to ask anew.");
11024
11053
  let bytes;
11025
11054
  try {
11026
- bytes = fs25.readFileSync(path26.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")));
11055
+ bytes = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")));
11027
11056
  } catch {
11028
11057
  throw new BoardError("access", `the copy of ${a.file} ${this.ctx.userName} was shown is gone; nothing was sent. Post again to ask anew.`);
11029
11058
  }
@@ -11041,7 +11070,7 @@ ${lines.join("\n")}`;
11041
11070
  for (const a of item.attachments) {
11042
11071
  if (!a.snapshot) continue;
11043
11072
  try {
11044
- fs25.rmSync(path26.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")), { force: true });
11073
+ fs25.rmSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")), { force: true });
11045
11074
  } catch (e) {
11046
11075
  console.error(`Board: could not remove ${a.snapshot} from the cache: ${e.message}`);
11047
11076
  }
@@ -11123,7 +11152,7 @@ ${lines.join("\n")}`;
11123
11152
  let bytes = null;
11124
11153
  if (same) {
11125
11154
  try {
11126
- bytes = fs25.readFileSync(path26.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...same.split("/")));
11155
+ bytes = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...same.split("/")));
11127
11156
  } catch {
11128
11157
  }
11129
11158
  }
@@ -11153,7 +11182,7 @@ ${lines.join("\n")}`;
11153
11182
  for (const a of due) {
11154
11183
  if (a.cacheFile && this.ctx.cacheDir && !this.store.cacheFileInUse(a.cacheFile)) {
11155
11184
  try {
11156
- fs25.rmSync(path26.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), { force: true });
11185
+ fs25.rmSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), { force: true });
11157
11186
  } catch (e) {
11158
11187
  console.error(`Board: could not remove ${a.cacheFile} from the cache: ${e.message}`);
11159
11188
  }
@@ -11803,7 +11832,7 @@ var MacroList = z8.array(z8.object({
11803
11832
 
11804
11833
  // src/sessions/workflow-profiles.ts
11805
11834
  import fs26 from "node:fs";
11806
- import path27 from "node:path";
11835
+ import path28 from "node:path";
11807
11836
  import { z as z9 } from "zod";
11808
11837
  var WorkflowProfilesFile = z9.preprocess(withoutComments, z9.object({
11809
11838
  version: z9.literal(1, { message: "version must be 1" }),
@@ -11829,7 +11858,7 @@ function settingsKeyProblem(profiles) {
11829
11858
  return null;
11830
11859
  }
11831
11860
  function profilesFilePath(workflow) {
11832
- return workflow.profilesFile ? path27.resolve(workflow.folder, workflow.profilesFile) : null;
11861
+ return workflow.profilesFile ? path28.resolve(workflow.folder, workflow.profilesFile) : null;
11833
11862
  }
11834
11863
  function parseWorkflowProfiles(file, text2) {
11835
11864
  if (text2 === null) return { file, problem: `The workflow's profiles file ${file} was not found.` };
@@ -11908,6 +11937,7 @@ function resumedState(info) {
11908
11937
  return { update: { status: "starting", endedAt: null, endReason: null, archivedAt: null, paused: false }, events };
11909
11938
  }
11910
11939
  var exercised425 = false;
11940
+ var exercised96 = false;
11911
11941
  var SessionManager = class _SessionManager {
11912
11942
  live = /* @__PURE__ */ new Map();
11913
11943
  /** Replaced when the config file changes (issue #353), so it is read when needed, never copied at start. */
@@ -12404,7 +12434,7 @@ var SessionManager = class _SessionManager {
12404
12434
  if (!info) return "No such session.";
12405
12435
  const project = this.configOf(info);
12406
12436
  if (project.runner !== "local") {
12407
- return path28.isAbsolute(file) || file.split("/").includes("..") ? "Give the handoff file relative to the project folder, inside it." : null;
12437
+ return path29.isAbsolute(file) || file.split("/").includes("..") ? "Give the handoff file relative to the project folder, inside it." : null;
12408
12438
  }
12409
12439
  return handoffProblem(info.workDir ?? project.path, file);
12410
12440
  }
@@ -12784,11 +12814,13 @@ var SessionManager = class _SessionManager {
12784
12814
  accountsEvent() {
12785
12815
  const raw = this.store.getMeta(_SessionManager.IDENTITY_KEY);
12786
12816
  const inFile = this.config.claudeAccounts;
12787
- const accounts = this.accounts.list().map((a) => inFile[a.name] ? { ...a, inConfigFile: true, whatItIsFor: inFile[a.name].whatItIsFor ?? null } : a);
12817
+ const accounts = this.accounts.list().map((a) => this.accountView(inFile[a.name] ? { ...a, inConfigFile: true, whatItIsFor: inFile[a.name].whatItIsFor ?? null } : a));
12818
+ const identity = raw ? JSON.parse(raw) : null;
12819
+ const defaultKind = shownSignIn(this.signIns.get(DEFAULT_ACCOUNT));
12788
12820
  return {
12789
12821
  type: "accounts",
12790
12822
  accounts,
12791
- defaultIdentity: raw ? JSON.parse(raw) : null,
12823
+ defaultIdentity: defaultKind ? { email: null, organization: null, plan: null, seenAt: null, ...identity, signInKind: defaultKind } : identity,
12792
12824
  choices: this.accountChoices(),
12793
12825
  signedOut: this.signedOut(),
12794
12826
  claudeProblem: this.claudeProblem,
@@ -12796,6 +12828,16 @@ var SessionManager = class _SessionManager {
12796
12828
  configFileProblems: this.accountFileProblems
12797
12829
  };
12798
12830
  }
12831
+ /** An account as the page shows it: how it is signed in (from the last check) and the commands to sign it in (#85). */
12832
+ accountView(a) {
12833
+ const data = envSetting("DATA_DIR");
12834
+ return {
12835
+ ...a,
12836
+ signInKind: shownSignIn(this.signIns.get(a.name)),
12837
+ loginCommand: signInCommand(a.configDir),
12838
+ tokenCommand: tokenCommand(a.name, { packaged: PACKAGED, appDir: appDir(), dataDir: data ? path29.resolve(data) : void 0, platform: process.platform })
12839
+ };
12840
+ }
12799
12841
  /** What the config file's accounts block asked that couldn't be done, from its last read (issue #425). */
12800
12842
  accountFileProblems = [];
12801
12843
  /**
@@ -12896,7 +12938,7 @@ var SessionManager = class _SessionManager {
12896
12938
  const found = await Promise.all(places.map((p) => this.checkPlace(p)));
12897
12939
  const out = places.filter((_, i) => found[i].state === "out");
12898
12940
  const state = out.length ? "out" : found.some((f) => f.state === "unknown") ? "unknown" : "in";
12899
- const combined = { state, method: found.find((f) => f.method)?.method ?? null, detail: found.find((f) => f.detail)?.detail ?? null, checkedAt: Date.now() };
12941
+ const combined = { state, method: found.find((f) => f.method)?.method ?? null, kind: found.find((f) => f.kind)?.kind ?? null, detail: found.find((f) => f.detail)?.detail ?? null, checkedAt: Date.now() };
12900
12942
  this.signIns.set(name, { ...combined, help: out.map((p) => this.placeHelp(name, p)).join(" ") });
12901
12943
  return combined;
12902
12944
  }
@@ -12987,7 +13029,7 @@ var SessionManager = class _SessionManager {
12987
13029
  const added = this.accounts.add(a);
12988
13030
  this.publishAccounts();
12989
13031
  void this.checkSignIns([added.name]);
12990
- return added;
13032
+ return this.accountView(added);
12991
13033
  } catch (e) {
12992
13034
  throw e instanceof AccountError ? new UserError(e.message) : e;
12993
13035
  }
@@ -13039,12 +13081,12 @@ var SessionManager = class _SessionManager {
13039
13081
  */
13040
13082
  workflowCheck(folder, projectConfig2, name) {
13041
13083
  if (folder) {
13042
- folder = path28.resolve(folder);
13084
+ folder = path29.resolve(folder);
13043
13085
  if (!isInside(folder, this.allowedFolders())) throw new UserError("That folder is outside the folders Ledung may use.");
13044
13086
  }
13045
13087
  const mayLook = (file) => {
13046
13088
  let at2 = file;
13047
- while (!fs27.existsSync(at2) && path28.dirname(at2) !== at2) at2 = path28.dirname(at2);
13089
+ while (!fs27.existsSync(at2) && path29.dirname(at2) !== at2) at2 = path29.dirname(at2);
13048
13090
  return isInside(at2, this.allowedFolders());
13049
13091
  };
13050
13092
  const workflows = workflowsOf(this.config);
@@ -13058,7 +13100,7 @@ var SessionManager = class _SessionManager {
13058
13100
  const local = this.projectStore.list().flatMap((p) => p.config.runner === "local" ? [{ name: p.config.name, path: p.config.path }] : []);
13059
13101
  const roots = defaultRoots(this.config.browseRoots);
13060
13102
  const projects = this.projectsFolder;
13061
- const starts = fs27.existsSync(projects) && !roots.some((r) => path28.resolve(r) === projects) ? [...roots, projects] : roots;
13103
+ const starts = fs27.existsSync(projects) && !roots.some((r) => path29.resolve(r) === projects) ? [...roots, projects] : roots;
13062
13104
  try {
13063
13105
  return { ...listLocal(folder, this.allowedFolders().filter((f) => fs27.existsSync(f)), local), roots: starts };
13064
13106
  } catch (e) {
@@ -13086,10 +13128,10 @@ var SessionManager = class _SessionManager {
13086
13128
  * GitHub repository; then the project, and its manager started with the task, if one is given.
13087
13129
  */
13088
13130
  async createProject(opts) {
13089
- const parent = path28.resolve(opts.parent || this.projectsFolder);
13131
+ const parent = path29.resolve(opts.parent || this.projectsFolder);
13090
13132
  let settings;
13091
13133
  try {
13092
- settings = parseProject({ ...withSuggestedSensitive(opts.settings), runner: "local", path: path28.join(parent, String(opts.settings.name ?? "")) });
13134
+ settings = parseProject({ ...withSuggestedSensitive(opts.settings), runner: "local", path: path29.join(parent, String(opts.settings.name ?? "")) });
13093
13135
  } catch (e) {
13094
13136
  throw new UserError(e.message);
13095
13137
  }
@@ -13110,7 +13152,7 @@ var SessionManager = class _SessionManager {
13110
13152
  /** Adds an existing project; its manager is started when first needed (decided 2026-10-11). */
13111
13153
  addProject(config) {
13112
13154
  if (config?.runner === "local") {
13113
- const folder = path28.resolve(String(config.path ?? ""));
13155
+ const folder = path29.resolve(String(config.path ?? ""));
13114
13156
  if (!fs27.existsSync(folder) || !fs27.statSync(folder).isDirectory()) throw new UserError("That folder doesn't exist.");
13115
13157
  if (!isInside(folder, this.allowedFolders())) throw new UserError("That folder is outside the folders Ledung may use.");
13116
13158
  config = { ...config, path: folder };
@@ -13167,16 +13209,25 @@ var SessionManager = class _SessionManager {
13167
13209
  for (const s of this.sessions()) if (s.project === p.config.name) this.publish({ type: "session", session: s });
13168
13210
  return this.projectInfo(this.projectStore.get(p.config.name));
13169
13211
  }
13170
- /** What updateProject refuses, checked without writing anything, so a rename can check first (review-116 round 2). */
13212
+ /**
13213
+ * What updateProject refuses, checked without writing anything, so a rename can check first (review-116 round 2).
13214
+ * The page's save doesn't send where a project is, so it is filled in as the store does: every check sees the project
13215
+ * that would be stored. Without it a project's workflow, and so that workflow's profiles, looked absent (#96).
13216
+ */
13171
13217
  checkSettings(name, config) {
13172
13218
  const current = this.projectStore.get(name);
13173
- if (current) this.checkAccount({ ...config, runner: current.config.runner });
13174
- this.checkProfiles(config);
13175
- this.checkWorkflow(config, current?.config);
13176
- if (current) {
13177
- const where = current.config.runner === "local" ? { runner: "local", path: current.config.path } : { runner: "docker", container: current.config.container, pathInContainer: current.config.pathInContainer, user: current.config.user };
13178
- this.checkLockdownOf({ ...config, ...where, name });
13219
+ if (!current) return;
13220
+ const where = current.config.runner === "local" ? { runner: "local", path: current.config.path } : { runner: "docker", container: current.config.container, pathInContainer: current.config.pathInContainer, user: current.config.user };
13221
+ const full = { ...config, ...where, name };
13222
+ this.checkAccount(full);
13223
+ this.checkProfiles(full);
13224
+ const namesProfile = config.profiles?.manager ?? config.profiles?.session;
13225
+ if (!exercised96 && namesProfile && config.runner === void 0) {
13226
+ exercised96 = true;
13227
+ exercisedLine("EXERCISED #96 a project naming a profile checked on the page's save with its stored place");
13179
13228
  }
13229
+ this.checkWorkflow(full, current.config);
13230
+ this.checkLockdownOf(full);
13180
13231
  }
13181
13232
  /** A project's name colours (issue #83), set from its manager's ⚙ menu; only palette colours. */
13182
13233
  setProjectColours(name, colours) {
@@ -13377,7 +13428,7 @@ var SessionManager = class _SessionManager {
13377
13428
  chats() {
13378
13429
  const c = this.config.chats;
13379
13430
  return {
13380
- folder: path28.resolve(c.folder ?? os14.homedir()),
13431
+ folder: path29.resolve(c.folder ?? os14.homedir()),
13381
13432
  permissionMode: c.permissionMode,
13382
13433
  sensitive: this.chatsSensitive(),
13383
13434
  fromConfig: { commandPatterns: c.sensitive.commandPatterns, pathPatterns: c.sensitive.pathPatterns, mcpTools: c.sensitive.mcpTools },
@@ -13455,7 +13506,7 @@ var SessionManager = class _SessionManager {
13455
13506
  * start and on every resume, so a link changed or a project locked since can't move it.
13456
13507
  */
13457
13508
  chatFolder(wanted) {
13458
- const given = path28.resolve(this.chats().folder, wanted?.trim() || ".");
13509
+ const given = path29.resolve(this.chats().folder, wanted?.trim() || ".");
13459
13510
  let real2;
13460
13511
  try {
13461
13512
  real2 = fs27.realpathSync(given);
@@ -13547,7 +13598,7 @@ var SessionManager = class _SessionManager {
13547
13598
  checkWorkDir(project, info) {
13548
13599
  if (!info.workDir) return;
13549
13600
  const again = info.project === CHATS ? this.chatFolder(info.workDir) : this.workFolder(project, info.workDir);
13550
- if (again === null || path28.relative(again, info.workDir) !== "") {
13601
+ if (again === null || path29.relative(again, info.workDir) !== "") {
13551
13602
  throw new UserError(`The session's folder ${info.workDir} no longer leads to the same folder; start a new session.`);
13552
13603
  }
13553
13604
  }
@@ -13703,7 +13754,7 @@ ${firstMessage}`);
13703
13754
  if (info.project === CHATS) return start;
13704
13755
  const call = this.store.lastToolCall(info.id, ["EnterWorktree", "ExitWorktree"]);
13705
13756
  if (!call || call.name !== "EnterWorktree" || call.isError) return start;
13706
- const given = typeof call.input.path === "string" && call.input.path ? path28.resolve(start, call.input.path) : /worktree at (.+?) on branch /.exec(call.result ?? "")?.[1] ?? (typeof call.input.name === "string" ? path28.join(start, ".claude", "worktrees", call.input.name) : null);
13757
+ const given = typeof call.input.path === "string" && call.input.path ? path29.resolve(start, call.input.path) : /worktree at (.+?) on branch /.exec(call.result ?? "")?.[1] ?? (typeof call.input.name === "string" ? path29.join(start, ".claude", "worktrees", call.input.name) : null);
13707
13758
  if (!given) return start;
13708
13759
  try {
13709
13760
  return resolveWorkFolder(project.path, project.extraFolders ?? [], given) ?? project.path;
@@ -13713,7 +13764,7 @@ ${firstMessage}`);
13713
13764
  }
13714
13765
  /** The folders a chat's file viewer and tree never show from (Martin, question #317). */
13715
13766
  settingsFolders() {
13716
- return [path28.join(os14.homedir(), "AppData"), path28.join(os14.homedir(), "Library"), userDataDir()];
13767
+ return [path29.join(os14.homedir(), "AppData"), path29.join(os14.homedir(), "Library"), userDataDir()];
13717
13768
  }
13718
13769
  /** The folders the Files pane offers: where the session works, and the project folder when that is another one. */
13719
13770
  paneFolders(sessionId) {
@@ -13722,7 +13773,7 @@ ${firstMessage}`);
13722
13773
  const folders = [{ key: "session", path: own, label: info.project === CHATS ? "This chat's folder" : "This session's folder" }];
13723
13774
  const same = (a, b) => {
13724
13775
  try {
13725
- return path28.relative(fs27.realpathSync(a), fs27.realpathSync(b)) === "";
13776
+ return path29.relative(fs27.realpathSync(a), fs27.realpathSync(b)) === "";
13726
13777
  } catch {
13727
13778
  return a === b;
13728
13779
  }
@@ -13790,7 +13841,7 @@ ${firstMessage}`);
13790
13841
  const hidden = hiddenProblem(root, link, this.allowedFolders(), this.settingsFolders());
13791
13842
  if (hidden) throw new UserError(hidden);
13792
13843
  }
13793
- if (folder && this.lockedFoldersBesides(info.project).some((f) => within(f, path28.resolve(root, decodeURIComponent(link.replace(/[?#].*$/, "")))))) {
13844
+ if (folder && this.lockedFoldersBesides(info.project).some((f) => within(f, path29.resolve(root, decodeURIComponent(link.replace(/[?#].*$/, "")))))) {
13794
13845
  throw new UserError("That file is in a locked project's folder.");
13795
13846
  }
13796
13847
  return read(root, link);
@@ -14268,7 +14319,7 @@ function challengeIn(res) {
14268
14319
  // src/server/push.ts
14269
14320
  import crypto7 from "node:crypto";
14270
14321
  import fs28 from "node:fs";
14271
- import path29 from "node:path";
14322
+ import path30 from "node:path";
14272
14323
 
14273
14324
  // src/shared/waiting.ts
14274
14325
  var questionWaits = (q) => q.status === "open" && q.addressedTo === "user";
@@ -14326,7 +14377,7 @@ function loadOrCreateVapid(file) {
14326
14377
  const { privateKey } = crypto7.generateKeyPairSync("ec", { namedCurve: "P-256" });
14327
14378
  const jwk = privateKey.export({ format: "jwk" });
14328
14379
  const publicKey = b64(Buffer.concat([Buffer.from([4]), unb64(jwk.x), unb64(jwk.y)]));
14329
- fs28.mkdirSync(path29.dirname(file), { recursive: true, mode: 448 });
14380
+ fs28.mkdirSync(path30.dirname(file), { recursive: true, mode: 448 });
14330
14381
  fs28.writeFileSync(file, JSON.stringify({ publicKey, privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) }), { mode: 384 });
14331
14382
  return { publicKey, privateKey };
14332
14383
  }
@@ -14485,7 +14536,7 @@ var Push = class {
14485
14536
  // src/update/updater.ts
14486
14537
  import { spawn as spawn6 } from "node:child_process";
14487
14538
  import fs29 from "node:fs";
14488
- import path30 from "node:path";
14539
+ import path31 from "node:path";
14489
14540
  var BRANCH = "main";
14490
14541
  var TAIL_LINES = 15;
14491
14542
  var MAX_CHANGES = 50;
@@ -14498,7 +14549,7 @@ var PACKAGE = "ledung";
14498
14549
  var CHANNEL_KEY = "update-channel";
14499
14550
  var devCheckTraced = false;
14500
14551
  function readRunningRelease(appDir2) {
14501
- const own = readJson(path30.join(appDir2, "release.json"));
14552
+ const own = readJson(path31.join(appDir2, "release.json"));
14502
14553
  if (own && typeof own.source === "string") {
14503
14554
  return { source: own.source, commit: typeof own.commit === "string" ? own.commit : null, releasedAt: str2(own.releasedAt) };
14504
14555
  }
@@ -14672,7 +14723,7 @@ ${r.tail}`);
14672
14723
  }
14673
14724
  async compare(running) {
14674
14725
  const src = running.source;
14675
- if (!fs29.existsSync(path30.join(src, ".git"))) throw new Error(`The source folder ${src} is not a git clone.`);
14726
+ if (!fs29.existsSync(path31.join(src, ".git"))) throw new Error(`The source folder ${src} is not a git clone.`);
14676
14727
  const fetched = await this.run({ cmd: "git", args: ["fetch", "origin", BRANCH] }, src);
14677
14728
  if (fetched.code !== 0) throw new Error(`git fetch origin ${BRANCH} failed:
14678
14729
  ${fetched.tail}`);
@@ -14755,7 +14806,7 @@ ${fetched.tail}`);
14755
14806
  }
14756
14807
  /** Why the source folder can't be updated: changes, another branch, or main has gone its own way. */
14757
14808
  async precheck(src) {
14758
- if (!fs29.existsSync(path30.join(src, ".git"))) return { reason: `The source folder ${src} is not a git clone.` };
14809
+ if (!fs29.existsSync(path31.join(src, ".git"))) return { reason: `The source folder ${src} is not a git clone.` };
14759
14810
  const status = await this.run({ cmd: "git", args: ["status", "--porcelain", "--untracked-files=no"] }, src);
14760
14811
  if (status.code !== 0) return { reason: "git status failed in the source folder.", output: status.tail };
14761
14812
  if (status.out.trim()) {
@@ -14864,7 +14915,7 @@ function typicalCosts(turns) {
14864
14915
  // src/server/tunnel-tls.ts
14865
14916
  import crypto8 from "node:crypto";
14866
14917
  import fs30 from "node:fs";
14867
- import path31 from "node:path";
14918
+ import path32 from "node:path";
14868
14919
  import tls from "node:tls";
14869
14920
  var TLS_KEY = "tls.key";
14870
14921
  var TLS_CERT = "tls.crt";
@@ -14878,8 +14929,8 @@ var CertificateFiles = class {
14878
14929
  }
14879
14930
  /** What changed since the last call: nothing, gone, unusable, or a new key and certificate to use. */
14880
14931
  check() {
14881
- const keyFile = path31.join(this.dir, TLS_KEY);
14882
- const certFile = path31.join(this.dir, TLS_CERT);
14932
+ const keyFile = path32.join(this.dir, TLS_KEY);
14933
+ const certFile = path32.join(this.dir, TLS_CERT);
14883
14934
  let stamp;
14884
14935
  try {
14885
14936
  stamp = [fs30.statSync(keyFile), fs30.statSync(certFile)].map((s) => `${s.size}:${s.mtimeMs}`).join("|");
@@ -15046,22 +15097,56 @@ function protocols(body) {
15046
15097
 
15047
15098
  // src/platform/open-folder.ts
15048
15099
  import { spawn as spawn7 } from "node:child_process";
15100
+ import path33 from "node:path";
15101
+ var STILL_RUNNING_MS = 3e3;
15102
+ var MAX_REASON = 500;
15049
15103
  function openFolderCommand(folder, platform = process.platform) {
15050
- return platform === "win32" ? ["explorer.exe", [folder]] : platform === "darwin" ? ["open", [folder]] : ["xdg-open", [folder]];
15104
+ return platform === "win32" ? ["explorer.exe", [path33.win32.normalize(folder)]] : platform === "darwin" ? ["open", [folder]] : ["xdg-open", [folder]];
15051
15105
  }
15052
- function openFolder(folder) {
15053
- const [command, args] = openFolderCommand(folder);
15054
- const child = spawn7(command, args, { stdio: "ignore", detached: true, windowsHide: true });
15055
- child.on("error", () => {
15106
+ function openRun(platform = process.platform) {
15107
+ return { exitCodeCounts: platform !== "win32", windowsHide: false, stillRunningMs: STILL_RUNNING_MS };
15108
+ }
15109
+ function runOpener(command, args, run8) {
15110
+ return new Promise((resolve, reject) => {
15111
+ const child = spawn7(command, args, { stdio: ["ignore", "ignore", "pipe"], detached: true, windowsHide: run8.windowsHide });
15112
+ let stderr = "";
15113
+ child.stderr.setEncoding("utf8");
15114
+ child.stderr.on("data", (text2) => {
15115
+ if (stderr.length < MAX_REASON) stderr += text2;
15116
+ });
15117
+ const settled = () => {
15118
+ clearTimeout(timer);
15119
+ child.stderr.unref();
15120
+ child.unref();
15121
+ };
15122
+ const timer = setTimeout(() => {
15123
+ settled();
15124
+ resolve();
15125
+ }, run8.stillRunningMs);
15126
+ child.on("error", (e) => {
15127
+ settled();
15128
+ reject(e);
15129
+ });
15130
+ child.on("close", (code, signal) => {
15131
+ settled();
15132
+ if (!run8.exitCodeCounts || code === 0) resolve();
15133
+ else reject(new Error(stderr.trim().slice(0, MAX_REASON) || `${command} stopped with ${code ?? signal}`));
15134
+ });
15056
15135
  });
15057
- child.unref();
15136
+ }
15137
+ var exercised = false;
15138
+ async function openFolder(folder) {
15139
+ const [command, args] = openFolderCommand(folder);
15140
+ await runOpener(command, args, openRun());
15141
+ if (!exercised) exercisedLine(`EXERCISED #83 ${command} was waited for and opened the folder`);
15142
+ exercised = true;
15058
15143
  }
15059
15144
 
15060
15145
  // src/remote/linked.ts
15061
15146
  import crypto9 from "node:crypto";
15062
15147
  import fs31 from "node:fs";
15063
15148
  import https from "node:https";
15064
- import path32 from "node:path";
15149
+ import path34 from "node:path";
15065
15150
  import tls2 from "node:tls";
15066
15151
  var LINKS_FILE = "links.json";
15067
15152
  var LINK_CODE_MS = 10 * 60 * 1e3;
@@ -15120,7 +15205,7 @@ var LinkedComputers = class {
15120
15205
  pending = /* @__PURE__ */ new Map();
15121
15206
  constructor(opts) {
15122
15207
  this.opts = opts;
15123
- this.file = path32.join(opts.dir, LINKS_FILE);
15208
+ this.file = path34.join(opts.dir, LINKS_FILE);
15124
15209
  this.now = opts.now ?? Date.now;
15125
15210
  }
15126
15211
  list() {
@@ -15270,7 +15355,7 @@ var LinkedComputers = class {
15270
15355
  }
15271
15356
  }
15272
15357
  write(links) {
15273
- fs31.mkdirSync(path32.dirname(this.file), { recursive: true });
15358
+ fs31.mkdirSync(path34.dirname(this.file), { recursive: true });
15274
15359
  const temp = `${this.file}.${process.pid}.new`;
15275
15360
  fs31.writeFileSync(temp, JSON.stringify({ links }, null, 2), { mode: 384 });
15276
15361
  fs31.renameSync(temp, this.file);
@@ -15423,7 +15508,7 @@ var LedungServer = class {
15423
15508
  this.push?.seed(opts.manager.questions(), opts.store.pendingPermissions());
15424
15509
  this.guard = new Guard(opts.token, opts.port, opts.lanNames ?? [], opts.lanOpen ?? false);
15425
15510
  this.headers = {};
15426
- this.webRoot = opts.webRoot ?? path33.join(appDir(), "web", "dist");
15511
+ this.webRoot = opts.webRoot ?? path35.join(appDir(), "web", "dist");
15427
15512
  this.setPublicName(opts.publicName ?? null, opts.tunnelTls ?? null);
15428
15513
  opts.manager.setSettings(this.settingsPort());
15429
15514
  }
@@ -15752,7 +15837,7 @@ Connection: close\r
15752
15837
  packaged: PACKAGED,
15753
15838
  version: appVersion(),
15754
15839
  release: PACKAGED ? null : readRunningRelease(appDir()),
15755
- build: path33.basename(appDir())
15840
+ build: path35.basename(appDir())
15756
15841
  });
15757
15842
  return this.sourceCache;
15758
15843
  }
@@ -15762,7 +15847,7 @@ Connection: close\r
15762
15847
  return {
15763
15848
  type: "hello",
15764
15849
  // A release is named by its folder; the npm package's folder never changes, so by its version (#105).
15765
- build: PACKAGED ? `ledung@${appVersion()}` : path33.basename(appDir()),
15850
+ build: PACKAGED ? `ledung@${appVersion()}` : path35.basename(appDir()),
15766
15851
  canRestart: !!this.opts.onRestart,
15767
15852
  source: this.source(),
15768
15853
  userName: manager.userName,
@@ -16215,6 +16300,14 @@ Connection: close\r
16215
16300
  requirePageForManager(req, sessionId) {
16216
16301
  if (sessionId && this.opts.store.getSession(sessionId)?.role === "manager") this.requireOpenPage(req);
16217
16302
  }
16303
+ /** Says why, with the folder so it can be opened by hand; the page shows it (issue #83). */
16304
+ async openAccountFolder(folder) {
16305
+ try {
16306
+ await (this.opts.openFolder ?? openFolder)(folder);
16307
+ } catch (e) {
16308
+ throw new UserError(`Couldn't open ${folder}: ${e.message}`);
16309
+ }
16310
+ }
16218
16311
  /** Paired devices are managed only on this computer: a device can't pair others or list them. */
16219
16312
  requireLocal(req, what = "Devices are managed") {
16220
16313
  if (this.guard.isPublic(req)) throw new HttpError(403, `${what} in Settings on the computer Ledung runs on.`);
@@ -16477,7 +16570,7 @@ Connection: close\r
16477
16570
  case "POST accounts/:id/open-folder":
16478
16571
  this.requireLocal(req, "Folders are opened");
16479
16572
  await this.requirePage(req, url);
16480
- (this.opts.openFolder ?? openFolder)(manager.accountFolder(decodeURIComponent(id)));
16573
+ await this.openAccountFolder(manager.accountFolder(decodeURIComponent(id)));
16481
16574
  return { ok: true };
16482
16575
  case "POST accounts/:id/remove":
16483
16576
  return await manager.removeAccount(decodeURIComponent(id));
@@ -16947,19 +17040,19 @@ Connection: close\r
16947
17040
  return this.opts.updater;
16948
17041
  }
16949
17042
  serveStatic(pathname, res, signedIn, remote) {
16950
- const root = path33.resolve(this.webRoot);
16951
- let file = path33.resolve(root, "." + decodeURIComponent(pathname));
16952
- if (!file.startsWith(root + path33.sep) && file !== root) throw new HttpError(404, "Not found.");
17043
+ const root = path35.resolve(this.webRoot);
17044
+ let file = path35.resolve(root, "." + decodeURIComponent(pathname));
17045
+ if (!file.startsWith(root + path35.sep) && file !== root) throw new HttpError(404, "Not found.");
16953
17046
  if (!fs32.existsSync(file) || fs32.statSync(file).isDirectory()) {
16954
17047
  if (!signedIn) {
16955
17048
  res.writeHead(401, { ...this.headersFor(res), "Content-Type": "text/html; charset=utf-8" });
16956
17049
  res.end(`<!doctype html><title>Ledung</title><div style="font-family:sans-serif;max-width:30em;margin:2em auto"><p>${remote ? NOT_PAIRED : "Not signed in. Open Ledung through the link it prints when it starts."}</p>${remote ? PAIR_CODE_FORM : ""}</div>`);
16957
17050
  return;
16958
17051
  }
16959
- file = path33.join(root, "index.html");
17052
+ file = path35.join(root, "index.html");
16960
17053
  if (!fs32.existsSync(file)) throw new HttpError(500, 'The page has not been built. Run "npm run build".');
16961
17054
  }
16962
- const type = CONTENT_TYPES[path33.extname(file).toLowerCase()] ?? "application/octet-stream";
17055
+ const type = CONTENT_TYPES[path35.extname(file).toLowerCase()] ?? "application/octet-stream";
16963
17056
  res.writeHead(200, { ...this.headersFor(res), "Content-Type": type });
16964
17057
  fs32.createReadStream(file).pipe(res);
16965
17058
  }
@@ -17059,7 +17152,7 @@ async function adminCall(fn) {
17059
17152
  }
17060
17153
 
17061
17154
  // src/store/database.ts
17062
- import path34 from "node:path";
17155
+ import path36 from "node:path";
17063
17156
  import { DatabaseSync } from "node:sqlite";
17064
17157
  var DAILY_COLUMNS = ["at", "account", "machine", "model", "project", "role", "trigger", "input", "cache_write", "cache_read", "output", "thinking", "cost", "subagent"];
17065
17158
  var dailyDay = (r) => `date(${r}.at / 1000, 'unixepoch', 'localtime')`;
@@ -17632,7 +17725,7 @@ var MIGRATIONS = [
17632
17725
  `
17633
17726
  ];
17634
17727
  function databaseFile(dir = userDataDir()) {
17635
- return path34.join(dir, "ledung.db");
17728
+ return path36.join(dir, "ledung.db");
17636
17729
  }
17637
17730
  function openDatabase(file = databaseFile()) {
17638
17731
  const db = new DatabaseSync(file);
@@ -17687,7 +17780,7 @@ function quietTexts(entries) {
17687
17780
 
17688
17781
  // src/store/trim.ts
17689
17782
  import fs33 from "node:fs";
17690
- import path35 from "node:path";
17783
+ import path37 from "node:path";
17691
17784
  var RESULT_LIMIT = 64 * 1024;
17692
17785
  var OLD_RESULT_LIMIT = 2e3;
17693
17786
  var DAY2 = 24 * 60 * 60 * 1e3;
@@ -17903,7 +17996,7 @@ function diskFreeFor(db) {
17903
17996
  const main2 = db.prepare("PRAGMA database_list").all().find((d) => d.name === "main");
17904
17997
  if (!main2?.file) return null;
17905
17998
  try {
17906
- const s = fs33.statfsSync(path35.dirname(main2.file));
17999
+ const s = fs33.statfsSync(path37.dirname(main2.file));
17907
18000
  return Number(s.bavail) * Number(s.bsize);
17908
18001
  } catch {
17909
18002
  return null;
@@ -18666,9 +18759,9 @@ var GitHubBackend = class {
18666
18759
  this.now = opts.now ?? Date.now;
18667
18760
  }
18668
18761
  /** A call to GitHub; conditional (If-None-Match) when `conditional`, answering a 304 from what was kept. */
18669
- async call(method2, path41, body, conditional = false, accept = "application/vnd.github+json") {
18762
+ async call(method2, path43, body, conditional = false, accept = "application/vnd.github+json") {
18670
18763
  if (!this.tokenValue) this.tokenValue = await this.token();
18671
- const url = path41.startsWith("http") ? path41 : `${API}${path41}`;
18764
+ const url = path43.startsWith("http") ? path43 : `${API}${path43}`;
18672
18765
  const kept = conditional ? this.etags.get(url) : void 0;
18673
18766
  const headers = {
18674
18767
  authorization: `Bearer ${this.tokenValue}`,
@@ -18767,10 +18860,10 @@ var GitHubBackend = class {
18767
18860
  };
18768
18861
  }
18769
18862
  /** All pages of a list, following Link: rel="next"; every page conditional. Fresh: anything changed. */
18770
- async list(path41) {
18863
+ async list(path43) {
18771
18864
  const items = [];
18772
18865
  let fresh = false;
18773
- let next = path41;
18866
+ let next = path43;
18774
18867
  for (let page = 0; next && page < 20; page++) {
18775
18868
  const r = await this.call("GET", next, void 0, true);
18776
18869
  fresh ||= r.fresh;
@@ -18927,8 +19020,8 @@ ${lines.join("\n")}`;
18927
19020
  for (const f of files.slice(1)) entries.push({ path: f.path, sha: (await blob(f.bytes)).body.sha });
18928
19021
  await this.replaceEvidence(entries, message2);
18929
19022
  }
18930
- async getFile(path41) {
18931
- const url = `/repos/${this.repo}/contents/${path41.split("/").map(encodeURIComponent).join("/")}?ref=${EVIDENCE_BRANCH}`;
19023
+ async getFile(path43) {
19024
+ const url = `/repos/${this.repo}/contents/${path43.split("/").map(encodeURIComponent).join("/")}?ref=${EVIDENCE_BRANCH}`;
18932
19025
  let body;
18933
19026
  try {
18934
19027
  body = (await this.call("GET", url, void 0, false, RAW)).body;
@@ -18936,11 +19029,11 @@ ${lines.join("\n")}`;
18936
19029
  if (e instanceof BoardError && e.kind === "access") throw new BoardError("access", "it is not on the board (pruned, or never uploaded)");
18937
19030
  throw e;
18938
19031
  }
18939
- if (!Buffer.isBuffer(body)) throw new BoardError("unreachable", `GitHub did not send the file ${path41}.`);
19032
+ if (!Buffer.isBuffer(body)) throw new BoardError("unreachable", `GitHub did not send the file ${path43}.`);
18940
19033
  return body;
18941
19034
  }
18942
19035
  async deleteFiles(paths, message2) {
18943
- if (paths.length) await this.replaceEvidence(paths.map((path41) => ({ path: path41, sha: null })), message2);
19036
+ if (paths.length) await this.replaceEvidence(paths.map((path43) => ({ path: path43, sha: null })), message2);
18944
19037
  }
18945
19038
  /**
18946
19039
  * People for the allowlist picker (decision 5): the repo's collaborators with name and picture (needs
@@ -18993,14 +19086,14 @@ var GitHubRepos = class _GitHubRepos {
18993
19086
  this.token = opts.token ?? ghToken;
18994
19087
  this.now = opts.now ?? Date.now;
18995
19088
  }
18996
- async call(method2, path41, body) {
19089
+ async call(method2, path43, body) {
18997
19090
  if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
18998
19091
  throw new BoardError("signin", SIGN_IN2);
18999
19092
  });
19000
19093
  const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung" };
19001
19094
  let res;
19002
19095
  try {
19003
- res = await this.http(method2, path41.startsWith("http") ? path41 : `${API2}${path41}`, headers, body);
19096
+ res = await this.http(method2, path43.startsWith("http") ? path43 : `${API2}${path43}`, headers, body);
19004
19097
  } catch (e) {
19005
19098
  throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
19006
19099
  }
@@ -19062,8 +19155,8 @@ var GitHubRepos = class _GitHubRepos {
19062
19155
  const owners = await this.owners();
19063
19156
  const found = owners.find((o) => o.login.toLowerCase() === owner.toLowerCase());
19064
19157
  if (!found) throw new BoardError("access", `You can't create repositories under "${owner}": it is neither your account nor an organisation you belong to.`);
19065
- const path41 = found.kind === "user" ? "/user/repos" : `/orgs/${encodeURIComponent(found.login)}/repos`;
19066
- const res = await this.call("POST", path41, { name, private: true, description: "Message board for Ledung managers" });
19158
+ const path43 = found.kind === "user" ? "/user/repos" : `/orgs/${encodeURIComponent(found.login)}/repos`;
19159
+ const res = await this.call("POST", path43, { name, private: true, description: "Message board for Ledung managers" });
19067
19160
  if (res.status === 422) {
19068
19161
  const why = _GitHubRepos.message(res.body);
19069
19162
  if (/already exists|name already/i.test(why)) throw new BoardError("access", `${found.login}/${name} already exists. Pick another name, or choose it from the list.`);
@@ -19083,7 +19176,7 @@ var GitHubRepos = class _GitHubRepos {
19083
19176
  // src/usage/queries.ts
19084
19177
  var DAY3 = 24 * 60 * 60 * 1e3;
19085
19178
  var tokensOf = (r) => r.input + r.cache_write + r.cache_read + r.output;
19086
- var exercised = false;
19179
+ var exercised2 = false;
19087
19180
  function push(groups, key, r) {
19088
19181
  const list2 = groups.get(key);
19089
19182
  if (!list2) {
@@ -19091,8 +19184,8 @@ function push(groups, key, r) {
19091
19184
  return;
19092
19185
  }
19093
19186
  list2.push(r);
19094
- if (!exercised) {
19095
- exercised = true;
19187
+ if (!exercised2) {
19188
+ exercised2 = true;
19096
19189
  exercisedLine("EXERCISED #356 usage report grouped calls in place");
19097
19190
  }
19098
19191
  }
@@ -19318,7 +19411,7 @@ var UsageQueries = class {
19318
19411
  // src/usage/publisher.ts
19319
19412
  import crypto12 from "node:crypto";
19320
19413
  import fs34 from "node:fs";
19321
- import path36 from "node:path";
19414
+ import path38 from "node:path";
19322
19415
 
19323
19416
  // src/usage/publish-file.ts
19324
19417
  import crypto11 from "node:crypto";
@@ -19464,14 +19557,14 @@ var UsageRepo = class {
19464
19557
  this.http = opts.http ?? fetchHttp;
19465
19558
  this.token = opts.token ?? ghToken;
19466
19559
  }
19467
- async call(method2, path41, body, extra = {}) {
19560
+ async call(method2, path43, body, extra = {}) {
19468
19561
  if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
19469
19562
  throw new BoardError("signin", SIGN_IN3);
19470
19563
  });
19471
19564
  const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung", ...extra };
19472
19565
  let res;
19473
19566
  try {
19474
- res = await this.http(method2, `${API3}${path41}`, headers, body);
19567
+ res = await this.http(method2, `${API3}${path43}`, headers, body);
19475
19568
  } catch (e) {
19476
19569
  throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
19477
19570
  }
@@ -19489,8 +19582,8 @@ var UsageRepo = class {
19489
19582
  if (res.status === 403 || res.status === 404) return new BoardError("access", `GitHub refused ${what}: ${this.repo} is missing, or the login can't write to it${why ? ` (${why})` : ""}.`);
19490
19583
  return new BoardError("unreachable", `GitHub answered ${res.status} for ${what}${why ? ` (${why})` : ""}.`);
19491
19584
  }
19492
- async ok(what, method2, path41, body) {
19493
- const res = await this.call(method2, path41, body);
19585
+ async ok(what, method2, path43, body) {
19586
+ const res = await this.call(method2, path43, body);
19494
19587
  if (res.status < 200 || res.status >= 300) throw this.failure(what, res);
19495
19588
  return res.body;
19496
19589
  }
@@ -19672,7 +19765,7 @@ var UsagePublisher = class {
19672
19765
  }
19673
19766
  // --- The usage key ---------------------------------------------------------------------------------------
19674
19767
  keyFile() {
19675
- return path36.join(this.opts.keyDir, USAGE_KEY_FILE);
19768
+ return path38.join(this.opts.keyDir, USAGE_KEY_FILE);
19676
19769
  }
19677
19770
  /** The usage key, or null when this computer has none yet. */
19678
19771
  key() {
@@ -20105,13 +20198,13 @@ function originOf(folder) {
20105
20198
 
20106
20199
  // src/remote/link.ts
20107
20200
  import fs36 from "node:fs";
20108
- import path38 from "node:path";
20201
+ import path40 from "node:path";
20109
20202
 
20110
20203
  // src/server/acme.ts
20111
20204
  import crypto13 from "node:crypto";
20112
20205
  import dns from "node:dns";
20113
20206
  import fs35 from "node:fs";
20114
- import path37 from "node:path";
20207
+ import path39 from "node:path";
20115
20208
  import acme from "acme-client";
20116
20209
  var ACCOUNT_KEY = "acme-account.key";
20117
20210
  var ACME_STATE = "acme.json";
@@ -20342,8 +20435,8 @@ var CertificateManager = class {
20342
20435
  const chain = Buffer.from(await client.getCertificate(finalized));
20343
20436
  const problem2 = certificateProblem({ key, cert: chain }, name);
20344
20437
  if (problem2) throw new Error(problem2);
20345
- writePrivate(path37.join(this.opts.dir, TLS_KEY), key);
20346
- writePrivate(path37.join(this.opts.dir, TLS_CERT), chain);
20438
+ writePrivate(path39.join(this.opts.dir, TLS_KEY), key);
20439
+ writePrivate(path39.join(this.opts.dir, TLS_CERT), chain);
20347
20440
  this.save({ issuedBy: this.opts.directoryUrl });
20348
20441
  }
20349
20442
  /** The certificate in remoteDir() if it can serve the name, has not run out and is this directory's; null otherwise. */
@@ -20351,7 +20444,7 @@ var CertificateManager = class {
20351
20444
  const issuedBy = this.saved().issuedBy;
20352
20445
  if (issuedBy !== null && issuedBy !== this.opts.directoryUrl) return null;
20353
20446
  try {
20354
- const files = { key: fs35.readFileSync(path37.join(this.opts.dir, TLS_KEY)), cert: fs35.readFileSync(path37.join(this.opts.dir, TLS_CERT)) };
20447
+ const files = { key: fs35.readFileSync(path39.join(this.opts.dir, TLS_KEY)), cert: fs35.readFileSync(path39.join(this.opts.dir, TLS_CERT)) };
20355
20448
  if (certificateProblem(files, this.opts.publicName)) return null;
20356
20449
  const life = lifetime(files.cert);
20357
20450
  return this.now() < life.validUntil ? life : null;
@@ -20373,20 +20466,20 @@ var CertificateManager = class {
20373
20466
  function readSaved(dir) {
20374
20467
  const empty = { accounts: {}, issuedBy: null, failures: 0, nextTry: null, error: null };
20375
20468
  try {
20376
- const raw = JSON.parse(fs35.readFileSync(path37.join(dir, ACME_STATE), "utf8"));
20469
+ const raw = JSON.parse(fs35.readFileSync(path39.join(dir, ACME_STATE), "utf8"));
20377
20470
  return { ...empty, ...raw, accounts: { ...raw.accounts } };
20378
20471
  } catch {
20379
20472
  return empty;
20380
20473
  }
20381
20474
  }
20382
20475
  function writeSaved(dir, change) {
20383
- writePrivate(path37.join(dir, ACME_STATE), Buffer.from(JSON.stringify({ ...readSaved(dir), ...change }, null, 2)));
20476
+ writePrivate(path39.join(dir, ACME_STATE), Buffer.from(JSON.stringify({ ...readSaved(dir), ...change }, null, 2)));
20384
20477
  }
20385
20478
  function saveAccount(dir, directoryUrl, accountUrl) {
20386
20479
  writeSaved(dir, { accounts: { ...readSaved(dir).accounts, [directoryUrl]: accountUrl } });
20387
20480
  }
20388
20481
  async function acmeAccount(opts) {
20389
- const keyFile = path37.join(opts.dir, ACCOUNT_KEY);
20482
+ const keyFile = path39.join(opts.dir, ACCOUNT_KEY);
20390
20483
  let key;
20391
20484
  try {
20392
20485
  key = fs35.readFileSync(keyFile);
@@ -20902,11 +20995,11 @@ var RemoteLink = class {
20902
20995
  return { name: answer.name, host: answer.host };
20903
20996
  }
20904
20997
  savedFile() {
20905
- return path38.join(this.opts.dir, SAVED_REMOTE);
20998
+ return path40.join(this.opts.dir, SAVED_REMOTE);
20906
20999
  }
20907
21000
  };
20908
21001
  function savedRemote(dir, log = (line) => console.log(line)) {
20909
- const file = path38.join(dir, SAVED_REMOTE);
21002
+ const file = path40.join(dir, SAVED_REMOTE);
20910
21003
  try {
20911
21004
  return parseRemote(JSON.parse(fs36.readFileSync(file, "utf8")));
20912
21005
  } catch (e) {
@@ -20923,7 +21016,7 @@ function writePrivate2(file, data) {
20923
21016
  // src/remote/admin.ts
20924
21017
  import crypto15 from "node:crypto";
20925
21018
  import fs37 from "node:fs";
20926
- import path39 from "node:path";
21019
+ import path41 from "node:path";
20927
21020
 
20928
21021
  // src/remote/cloudflare.ts
20929
21022
  var API4 = "https://api.cloudflare.com/client/v4";
@@ -20959,10 +21052,10 @@ var Cloudflare = class {
20959
21052
  }
20960
21053
  throw new Error(`The Cloudflare token reaches no zone holding ${name}.`);
20961
21054
  }
20962
- async call(method2, path41, body) {
21055
+ async call(method2, path43, body) {
20963
21056
  let res;
20964
21057
  try {
20965
- res = await this.fetch(`${API4}${path41}`, {
21058
+ res = await this.fetch(`${API4}${path43}`, {
20966
21059
  method: method2,
20967
21060
  headers: { Authorization: `Bearer ${this.token}`, ...body ? { "Content-Type": "application/json" } : {} },
20968
21061
  body: body ? JSON.stringify(body) : void 0,
@@ -21155,7 +21248,7 @@ var RelayAdmin = class {
21155
21248
  return answer;
21156
21249
  }
21157
21250
  file() {
21158
- return path39.join(this.opts.dir, ADMIN_FILE);
21251
+ return path41.join(this.opts.dir, ADMIN_FILE);
21159
21252
  }
21160
21253
  saved() {
21161
21254
  try {
@@ -21204,7 +21297,7 @@ async function main() {
21204
21297
  let watcher = null;
21205
21298
  const manager = new SessionManager(config, store, (event) => server?.broadcast(event));
21206
21299
  const pollMs = boardConfig(config).pollSeconds * 1e3;
21207
- manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(), path40.join(userDataDir(), "board-cache"));
21300
+ manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(), path42.join(userDataDir(), "board-cache"));
21208
21301
  const attached = await manager.attachRunningHosts();
21209
21302
  const leftover = store.markUnfinishedSessionsEnded("Ledung stopped before this session ended. Resume to continue.", attached);
21210
21303
  manager.away.begin();
@@ -21278,7 +21371,7 @@ async function main() {
21278
21371
  computerSettings: config.computers,
21279
21372
  linkConnect: config.remote?.linkVia ? connectVia(config.remote.linkVia) : void 0,
21280
21373
  // The VAPID keys stay on this computer, next to the relay's (issue #246).
21281
- push: { keyFile: path40.join(remoteDir(), "push-vapid.json") }
21374
+ push: { keyFile: path42.join(remoteDir(), "push-vapid.json") }
21282
21375
  });
21283
21376
  link = new RemoteLink({ server, dir: remoteDir(), configured: config.remote, joinAcme: config.relayJoin });
21284
21377
  server.attachRemote(link);