@leuria/cli 0.1.1 → 0.1.3

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.
@@ -343,6 +343,7 @@ import { createHash } from "crypto";
343
343
  import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
344
344
  import { dirname, join as join3 } from "path";
345
345
  import { promisify as promisify2 } from "util";
346
+ import crossSpawn from "cross-spawn";
346
347
 
347
348
  // src/codex.ts
348
349
  import { execFile } from "child_process";
@@ -374,7 +375,11 @@ async function prepareCodexHome(agent) {
374
375
  "",
375
376
  "[features]",
376
377
  ...features.map((name) => `${name} = ${ENABLED_FEATURES.has(name)}`),
377
- ""
378
+ "",
379
+ // Windows: the sandbox that needs no administrator rights (a restricted token).
380
+ // The other one asks for them the first time: out of the blue, and a dead end
381
+ // on a work computer without them.
382
+ ...process.platform === "win32" ? ["[windows]", 'sandbox = "unelevated"', ""] : []
378
383
  ];
379
384
  writeFileSync2(join2(home, "config.toml"), lines.join("\n"), { mode: 384 });
380
385
  writeFileSync2(stamp, agent.version);
@@ -576,12 +581,7 @@ async function installPackage(dir, spec, args, env) {
576
581
  if (process.versions.bun) {
577
582
  await execFileAsync2(process.execPath, ["add", spec], { cwd: dir, env: { ...process.env, BUN_BE_BUN: "1" }, timeout: 5 * 6e4 });
578
583
  } else {
579
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
580
- await execFileAsync2(npm, ["install", "--no-audit", "--no-fund", "--omit=dev", "--loglevel=error", spec], {
581
- cwd: dir,
582
- timeout: 5 * 6e4,
583
- shell: process.platform === "win32"
584
- });
584
+ await run("npm", ["install", "--no-audit", "--no-fund", "--omit=dev", "--loglevel=error", spec], { cwd: dir, timeout: 5 * 6e4 });
585
585
  }
586
586
  const name = spec.lastIndexOf("@") > 0 ? spec.slice(0, spec.lastIndexOf("@")) : spec;
587
587
  const pkgDir = join3(dir, "node_modules", ...name.split("/"));
@@ -593,6 +593,25 @@ async function installPackage(dir, spec, args, env) {
593
593
  const isScript = /\.[cm]?js$/.test(entry) || /^#!.*\b(node|bun)\b/.test(readFileSync3(entry, "utf-8").slice(0, 100));
594
594
  return isScript ? { command: process.execPath, launchArgs: [entry], args, env } : { command: entry, launchArgs: [], args, env };
595
595
  }
596
+ function run(command, args, options) {
597
+ return new Promise((resolve, reject) => {
598
+ const child = crossSpawn(command, args, { cwd: options.cwd, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
599
+ let stderr = "";
600
+ child.stderr?.on("data", (chunk) => {
601
+ stderr += chunk.toString("utf-8");
602
+ });
603
+ const timer = setTimeout(() => child.kill(), options.timeout);
604
+ child.on("error", (error) => {
605
+ clearTimeout(timer);
606
+ reject(error);
607
+ });
608
+ child.on("close", (code) => {
609
+ clearTimeout(timer);
610
+ if (code === 0) resolve();
611
+ else reject(new Error(`${command} ${args.join(" ")} failed (${code ?? "stopped"})${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
612
+ });
613
+ });
614
+ }
596
615
  async function installBinary(dir, target) {
597
616
  const res = await fetch(target.archive, { signal: AbortSignal.timeout(5 * 6e4) });
598
617
  if (!res.ok) throw new Error(`Download failed (${res.status}): ${target.archive}`);
@@ -635,16 +654,17 @@ function compareVersions(a, b) {
635
654
  }
636
655
 
637
656
  // src/version.ts
638
- var VERSION = true ? "0.1.0" : "0.0.0-dev";
657
+ var VERSION = true ? "0.1.3" : "0.0.0-dev";
639
658
 
640
659
  // src/auth.ts
641
- import { spawn as spawn2 } from "child_process";
642
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
643
- import { tmpdir as tmpdir2 } from "os";
644
- import { join as join5 } from "path";
660
+ import crossSpawn3 from "cross-spawn";
661
+ import { mkdtempSync as mkdtempSync3 } from "fs";
662
+ import { tmpdir as tmpdir3 } from "os";
663
+ import { join as join6 } from "path";
645
664
 
646
665
  // src/acp/acp-client.ts
647
666
  import { spawn } from "child_process";
667
+ import crossSpawn2 from "cross-spawn";
648
668
  import { Readable, Writable } from "stream";
649
669
  var rawParams = {
650
670
  // biome-ignore lint/suspicious/noExplicitAny: ACP payloads are untyped here
@@ -690,6 +710,9 @@ var AcpLiveSession = class {
690
710
  imagesSupported = false;
691
711
  /** `initialize` result, once connected. */
692
712
  info = null;
713
+ /** The sign-in the agent says it uses, from `_auth/status_update` (Claude): "account", "api_key", "none"… */
714
+ authKind;
715
+ authWaiters = [];
693
716
  /** Models the agent offers, from `session/new` (and later updates). */
694
717
  models = null;
695
718
  get isAlive() {
@@ -707,11 +730,11 @@ var AcpLiveSession = class {
707
730
  const env = { ...process.env, ...this.options.envVars };
708
731
  for (const key of REMOVED_ENV_VARS) delete env[key];
709
732
  if (process.versions.bun) env.BUN_BE_BUN = "1";
710
- const child = spawn(this.options.command, this.options.args, {
733
+ const child = crossSpawn2(this.options.command, this.options.args, {
711
734
  stdio: ["pipe", "pipe", "pipe"],
712
735
  env,
713
736
  cwd: this.options.cwd,
714
- shell: process.platform === "win32",
737
+ windowsHide: true,
715
738
  // Its own process group, so close() also stops what the agent starts
716
739
  // (Gemini relaunches itself as a child process).
717
740
  detached: process.platform !== "win32"
@@ -758,6 +781,27 @@ var AcpLiveSession = class {
758
781
  return { error: error instanceof Error ? error.message : String(error) };
759
782
  }
760
783
  }
784
+ /**
785
+ * The sign-in the agent reports (see `authKind`), waiting up to `ms` for its first
786
+ * report. Undefined when the agent doesn't report one, or not in time. Some agents
787
+ * (Claude) open a session while signed out and refuse only the first prompt, so a
788
+ * session alone doesn't prove a sign-in.
789
+ */
790
+ async authStatus(ms) {
791
+ if (this.authKind !== void 0) return this.authKind;
792
+ if (!this.info?.agentCapabilities._meta?.authStatus) return void 0;
793
+ return new Promise((resolve) => {
794
+ const timer = setTimeout(() => {
795
+ this.authWaiters = this.authWaiters.filter((w) => w !== waiter);
796
+ resolve(void 0);
797
+ }, ms);
798
+ const waiter = (kind) => {
799
+ clearTimeout(timer);
800
+ resolve(kind);
801
+ };
802
+ this.authWaiters.push(waiter);
803
+ });
804
+ }
761
805
  /** ACP `authenticate` with one of the advertised methods (the agent runs its own flow). */
762
806
  async authenticate(methodId) {
763
807
  if (!this.connection) return { error: "Not connected" };
@@ -899,7 +943,12 @@ ${file.text}
899
943
  });
900
944
  }
901
945
  };
902
- return sdk.client({ name: "leuria" }).onNotification(
946
+ return sdk.client({ name: "leuria" }).onNotification("_auth/status_update", rawParams, (ctx) => {
947
+ const kind = ctx.params?.authStatus?.kind;
948
+ if (typeof kind !== "string") return;
949
+ this.authKind = kind;
950
+ for (const waiter of this.authWaiters.splice(0)) waiter(kind);
951
+ }).onNotification(
903
952
  "session/update",
904
953
  rawParams,
905
954
  (ctx) => handleSessionUpdate(ctx.params)
@@ -915,7 +964,7 @@ ${file.text}
915
964
  function killTree(child) {
916
965
  try {
917
966
  if (process.platform === "win32" && child.pid) {
918
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
967
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
919
968
  } else if (child.pid) {
920
969
  process.kill(-child.pid, "SIGTERM");
921
970
  } else {
@@ -929,13 +978,105 @@ function killTree(child) {
929
978
  }
930
979
  }
931
980
 
981
+ // src/scratch.ts
982
+ import { rm } from "fs/promises";
983
+ function removeScratch(dir, onFailure) {
984
+ rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 }).catch((error) => onFailure?.(error));
985
+ }
986
+
987
+ // src/terminal-window.ts
988
+ import { spawn as spawn2 } from "child_process";
989
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdtempSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
990
+ import { tmpdir } from "os";
991
+ import { join as join4 } from "path";
992
+ function canOpenTerminalWindow() {
993
+ return process.platform === "win32" || process.platform === "darwin";
994
+ }
995
+ async function runInTerminalWindow(options) {
996
+ if (!canOpenTerminalWindow()) throw new Error("Leuria can't open a terminal window on this system");
997
+ const dir = mkdtempSync(join4(tmpdir(), "leuria-window-"));
998
+ const done = join4(dir, "exit-code");
999
+ try {
1000
+ if (process.platform === "win32") {
1001
+ const script = join4(dir, "sign-in.cmd");
1002
+ writeFileSync4(script, windowsScript(options, done), "utf-8");
1003
+ spawn2("cmd.exe", ["/d", "/c", `start "${options.title.replaceAll('"', "")}" "${script}"`], {
1004
+ detached: true,
1005
+ stdio: "ignore",
1006
+ windowsHide: false,
1007
+ windowsVerbatimArguments: true
1008
+ }).unref();
1009
+ } else {
1010
+ const script = join4(dir, "sign-in.command");
1011
+ writeFileSync4(script, macScript(options, done), "utf-8");
1012
+ chmodSync3(script, 448);
1013
+ spawn2("open", ["-a", "Terminal", script], { detached: true, stdio: "ignore" }).unref();
1014
+ }
1015
+ return await waitFor(done, options.signal, options.timeoutMs ?? 15 * 6e4);
1016
+ } finally {
1017
+ removeScratch(dir);
1018
+ }
1019
+ }
1020
+ function waitFor(file, signal, timeoutMs) {
1021
+ return new Promise((resolve) => {
1022
+ const finish = (code) => {
1023
+ clearInterval(poll);
1024
+ clearTimeout(timer);
1025
+ signal?.removeEventListener("abort", onAbort);
1026
+ resolve(code);
1027
+ };
1028
+ const onAbort = () => finish(null);
1029
+ const poll = setInterval(() => {
1030
+ if (!existsSync4(file)) return;
1031
+ const code = Number.parseInt(readFileSync4(file, "utf-8").trim(), 10);
1032
+ finish(Number.isNaN(code) ? null : code);
1033
+ }, 1e3);
1034
+ const timer = setTimeout(() => finish(null), timeoutMs);
1035
+ if (signal?.aborted) onAbort();
1036
+ else signal?.addEventListener("abort", onAbort, { once: true });
1037
+ });
1038
+ }
1039
+ function windowsScript(options, done) {
1040
+ const text = (value) => value.replaceAll("%", "%%");
1041
+ const quote = (value) => `"${text(value).replaceAll('"', '""')}"`;
1042
+ return [
1043
+ "@echo off",
1044
+ "chcp 65001 >nul",
1045
+ `title ${text(options.title).replace(/[&|<>^]/g, "")}`,
1046
+ `echo ${text(options.title).replace(/[&|<>^]/g, "")}`,
1047
+ "echo Leuria opened this window. Follow the steps below: it closes by itself when you're done.",
1048
+ "echo.",
1049
+ `cd /d ${quote(options.cwd)}`,
1050
+ ...Object.entries(options.env).map(([key, value]) => `set "${key}=${text(value)}"`),
1051
+ [options.command, ...options.args].map(quote).join(" "),
1052
+ `echo %errorlevel%> ${quote(done)}`,
1053
+ ""
1054
+ ].join("\r\n");
1055
+ }
1056
+ function macScript(options, done) {
1057
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
1058
+ return [
1059
+ "#!/bin/bash",
1060
+ "clear",
1061
+ `echo ${quote(options.title)}`,
1062
+ `echo ${quote("Leuria opened this window. Follow the steps below, then close it.")}`,
1063
+ "echo",
1064
+ `cd ${quote(options.cwd)}`,
1065
+ ...Object.entries(options.env).map(([key, value]) => `export ${key}=${quote(value)}`),
1066
+ [options.command, ...options.args].map(quote).join(" "),
1067
+ `echo $? > ${quote(done)}`,
1068
+ `echo; echo ${quote("Done. You can close this window.")}`,
1069
+ ""
1070
+ ].join("\n");
1071
+ }
1072
+
932
1073
  // src/browser-link.ts
933
1074
  import { execFileSync } from "child_process";
934
- import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
935
- import { tmpdir } from "os";
936
- import { delimiter, join as join4 } from "path";
1075
+ import { chmodSync as chmodSync4, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1076
+ import { tmpdir as tmpdir2 } from "os";
1077
+ import { delimiter, join as join5 } from "path";
937
1078
  function realCommand(name) {
938
- if (name === "open" && existsSync4("/usr/bin/open")) return "/usr/bin/open";
1079
+ if (name === "open" && existsSync5("/usr/bin/open")) return "/usr/bin/open";
939
1080
  try {
940
1081
  return execFileSync("/bin/sh", ["-c", `command -v ${name}`], { encoding: "utf-8" }).trim() || null;
941
1082
  } catch {
@@ -946,11 +1087,11 @@ function catchBrowserLinks(onUrl, options = {}) {
946
1087
  if (process.platform === "win32") return null;
947
1088
  const basePath = options.basePath ?? process.env.PATH ?? "";
948
1089
  const findReal = options.real ?? realCommand;
949
- const dir = mkdtempSync(join4(tmpdir(), "leuria-open-"));
950
- const bin = join4(dir, "bin");
951
- const log = join4(dir, "opened.txt");
1090
+ const dir = mkdtempSync2(join5(tmpdir2(), "leuria-open-"));
1091
+ const bin = join5(dir, "bin");
1092
+ const log = join5(dir, "opened.txt");
952
1093
  mkdirSync5(bin);
953
- writeFileSync4(log, "");
1094
+ writeFileSync5(log, "");
954
1095
  for (const name of ["open", "xdg-open"]) {
955
1096
  const real = findReal(name);
956
1097
  if (!real) continue;
@@ -958,14 +1099,14 @@ function catchBrowserLinks(onUrl, options = {}) {
958
1099
  for arg in "$@"; do printf '%s\\n' "$arg" >> '${log}'; done
959
1100
  exec '${real}' "$@"
960
1101
  `;
961
- writeFileSync4(join4(bin, name), script);
962
- chmodSync3(join4(bin, name), 493);
1102
+ writeFileSync5(join5(bin, name), script);
1103
+ chmodSync4(join5(bin, name), 493);
963
1104
  }
964
1105
  const seen = /* @__PURE__ */ new Set();
965
1106
  const timer = setInterval(() => {
966
1107
  let lines;
967
1108
  try {
968
- lines = readFileSync4(log, "utf-8").split("\n");
1109
+ lines = readFileSync5(log, "utf-8").split("\n");
969
1110
  } catch {
970
1111
  return;
971
1112
  }
@@ -1051,7 +1192,7 @@ function preferredMethod(methods, options = {}) {
1051
1192
  }
1052
1193
  async function open(id, onProgress, terminalAuth = false, extraEnv = {}) {
1053
1194
  const agent = await ensureAgent(id, { onProgress });
1054
- const sandbox = mkdtempSync2(join5(tmpdir2(), "leuria-auth-"));
1195
+ const sandbox = mkdtempSync3(join6(tmpdir3(), "leuria-auth-"));
1055
1196
  const custom = profileFor(id)?.configure?.({ sandbox, mcpUrl: "http://127.0.0.1:9/unused", mcpToken: "unused" });
1056
1197
  const env = { ...agent.env, ...custom?.env, ...extraEnv };
1057
1198
  const session = new AcpLiveSession({
@@ -1065,7 +1206,7 @@ async function open(id, onProgress, terminalAuth = false, extraEnv = {}) {
1065
1206
  });
1066
1207
  const close = () => {
1067
1208
  session.close();
1068
- rmSync3(sandbox, { recursive: true, force: true });
1209
+ removeScratch(sandbox);
1069
1210
  };
1070
1211
  return { agent, session, close, env, sandbox };
1071
1212
  }
@@ -1094,7 +1235,7 @@ function signInStallMs() {
1094
1235
  }
1095
1236
  async function checkSignIn(id, onProgress, options = {}) {
1096
1237
  if (isLlmId(id)) return checkLlm(id);
1097
- const { session, close } = await open(id, onProgress);
1238
+ const { session, close } = await open(id, onProgress, true);
1098
1239
  let timer;
1099
1240
  const timedOut = new Promise((resolve) => {
1100
1241
  timer = setTimeout(() => {
@@ -1112,22 +1253,25 @@ async function checkSignIn(id, onProgress, options = {}) {
1112
1253
  async function check() {
1113
1254
  const connected = await session.connect();
1114
1255
  if (connected.error) return { ok: false, detail: `the agent did not start: ${connected.error}`, methods: [] };
1115
- const methods = agentMethods(session.info?.authMethods ?? []);
1256
+ const all = session.info?.authMethods ?? [];
1257
+ const methods = agentMethods(all);
1258
+ const window = canOpenTerminalWindow() && methods.length === 0 && all.some(isTerminal) ? { window: true } : {};
1116
1259
  const created = await session.newSession();
1117
- if (!created.error) return { ok: true, detail: "ready", methods, ...withChoice(id, session.models) };
1118
- return { ok: false, detail: created.authRequired ? "not signed in" : created.error, methods };
1260
+ if (created.error) return { ok: false, detail: created.authRequired ? "not signed in" : created.error, methods, ...window };
1261
+ if (await session.authStatus(5e3) === "none") return { ok: false, detail: "not signed in", methods, ...window };
1262
+ return { ok: true, detail: "ready", methods, ...withChoice(id, session.models) };
1119
1263
  }
1120
1264
  }
1121
1265
  async function signIn(id, options = {}) {
1122
1266
  if (isLlmId(id)) return checkLlm(id);
1123
1267
  let pageOpened = false;
1124
- const link = options.onUrl && !options.terminal ? catchBrowserLinks((url) => {
1268
+ const link = options.onUrl && !options.terminal && !options.window ? catchBrowserLinks((url) => {
1125
1269
  pageOpened = true;
1126
1270
  options.onUrl?.(url);
1127
1271
  }) : null;
1128
1272
  let opened;
1129
1273
  try {
1130
- opened = await open(id, options.onProgress, options.terminal, link?.env);
1274
+ opened = await open(id, options.onProgress, options.terminal || options.window, link?.env);
1131
1275
  } catch (error) {
1132
1276
  link?.stop();
1133
1277
  throw error;
@@ -1161,26 +1305,43 @@ async function signIn(id, options = {}) {
1161
1305
  else options.signal?.addEventListener("abort", onAbort, { once: true });
1162
1306
  });
1163
1307
  try {
1164
- return await Promise.race([cancelled, run()]);
1308
+ return await Promise.race([cancelled, run2()]);
1165
1309
  } finally {
1166
1310
  close();
1167
1311
  }
1168
- async function run() {
1312
+ async function run2() {
1169
1313
  let session = opened.session;
1170
1314
  const connected = await session.connect();
1171
1315
  if (connected.error) return { ok: false, detail: `the agent did not start: ${connected.error}`, methods: [] };
1172
1316
  const all = session.info?.authMethods ?? [];
1173
- const methods = options.terminal ? all : agentMethods(all);
1317
+ const terminal = options.terminal || options.window;
1318
+ const methods = terminal ? all : agentMethods(all);
1174
1319
  if (methods.length === 0) {
1175
1320
  return { ok: false, detail: `${agent.name} offers no sign-in method Leuria can run; sign in with its own tool.`, methods };
1176
1321
  }
1177
- const method = options.methodId ? methods.find((m) => m.id === options.methodId) : preferredMethod(methods, { terminal: options.terminal });
1322
+ const method = options.methodId ? methods.find((m) => m.id === options.methodId) : preferredMethod(methods, { terminal });
1178
1323
  if (!method) return { ok: false, detail: `Unknown method. Available: ${methods.map((m) => m.id).join(", ")}`, methods };
1179
1324
  options.onProgress?.(`Signing in to ${agent.name} with ${method.name}${method.description ? ` (${method.description})` : ""}\u2026`);
1325
+ if (isTerminal(method) && options.window) {
1326
+ session.close();
1327
+ const code = await runInTerminalWindow({
1328
+ title: `Sign in to ${agentName(id)}`,
1329
+ command: agent.command,
1330
+ args: [...agent.launchArgs, ...method.args ?? []],
1331
+ // A compiled engine runs JavaScript agents as plain Bun, as sessions do.
1332
+ env: { ...env, ...method.env ?? {}, ...process.versions.bun ? { BUN_BE_BUN: "1" } : {} },
1333
+ cwd: sandbox,
1334
+ signal: options.signal
1335
+ });
1336
+ if (options.signal?.aborted) return { ok: false, detail: "cancelled", methods };
1337
+ if (code === null) return { ok: false, detail: "the sign-in window didn't finish", methods, window: true };
1338
+ if (code !== 0) return { ok: false, detail: `the sign-in ended with code ${code}`, methods, window: true };
1339
+ return checkSignIn(id, options.onProgress);
1340
+ }
1180
1341
  if (isTerminal(method)) {
1181
1342
  session.close();
1182
1343
  const code = await new Promise((resolve, reject) => {
1183
- const child = spawn2(agent.command, [...agent.launchArgs, ...method.args ?? []], {
1344
+ const child = crossSpawn3(agent.command, [...agent.launchArgs, ...method.args ?? []], {
1184
1345
  stdio: "inherit",
1185
1346
  cwd: sandbox,
1186
1347
  env: { ...process.env, ...env, ...method.env ?? {} }
@@ -1562,9 +1723,9 @@ import cacache from "cacache";
1562
1723
  // src/skill-sources.ts
1563
1724
  import { createHash as createHash3 } from "crypto";
1564
1725
  import { lookup } from "dns/promises";
1565
- import { mkdtemp, readdir, readFile, rm, stat } from "fs/promises";
1566
- import { tmpdir as tmpdir3 } from "os";
1567
- import { extname, join as join6, relative, sep } from "path";
1726
+ import { mkdtemp, readdir, readFile, rm as rm2, stat } from "fs/promises";
1727
+ import { tmpdir as tmpdir4 } from "os";
1728
+ import { extname, join as join7, relative, sep } from "path";
1568
1729
  import { downloadTemplate } from "giget";
1569
1730
  import ipaddr from "ipaddr.js";
1570
1731
  import { parse as parseYaml } from "yaml";
@@ -1638,7 +1799,7 @@ function parseSkillMd(text, files = {}) {
1638
1799
  return { name, description: description.slice(0, MAX_DESCRIPTION), body: match[2].trim(), files };
1639
1800
  }
1640
1801
  async function fromGithub(source) {
1641
- const dir = await mkdtemp(join6(tmpdir3(), "leuria-skills-"));
1802
+ const dir = await mkdtemp(join7(tmpdir4(), "leuria-skills-"));
1642
1803
  try {
1643
1804
  const path2 = source.subpath ? `/${source.subpath}` : "";
1644
1805
  await downloadTemplate(`gh:${source.repo}${path2}#${source.ref ?? "HEAD"}`, {
@@ -1658,21 +1819,21 @@ async function fromGithub(source) {
1658
1819
  continue;
1659
1820
  }
1660
1821
  if (source.skill && skill.name !== source.skill) continue;
1661
- skill.files = await readTextFiles(join6(skillFile, ".."));
1822
+ skill.files = await readTextFiles(join7(skillFile, ".."));
1662
1823
  found.push(skill);
1663
1824
  }
1664
1825
  return found;
1665
1826
  } finally {
1666
- await rm(dir, { recursive: true, force: true });
1827
+ await rm2(dir, { recursive: true, force: true });
1667
1828
  }
1668
1829
  }
1669
1830
  async function findSkillFiles(dir, depth) {
1670
1831
  const entries = await readdir(dir, { withFileTypes: true });
1671
1832
  const own = entries.find((e) => e.isFile() && e.name.toLowerCase() === "skill.md");
1672
- if (own) return [join6(dir, own.name)];
1833
+ if (own) return [join7(dir, own.name)];
1673
1834
  if (depth >= MAX_DEPTH) return [];
1674
1835
  const nested = await Promise.all(
1675
- entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => findSkillFiles(join6(dir, e.name), depth + 1))
1836
+ entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => findSkillFiles(join7(dir, e.name), depth + 1))
1676
1837
  );
1677
1838
  return nested.flat();
1678
1839
  }
@@ -1681,7 +1842,7 @@ async function readTextFiles(dir) {
1681
1842
  let total = 0;
1682
1843
  const walk = async (at, depth) => {
1683
1844
  for (const entry of await readdir(at, { withFileTypes: true })) {
1684
- const full = join6(at, entry.name);
1845
+ const full = join7(at, entry.name);
1685
1846
  if (entry.isDirectory() && depth < MAX_DEPTH) await walk(full, depth + 1);
1686
1847
  if (!entry.isFile() || !TEXT_FILES.has(extname(entry.name).toLowerCase())) continue;
1687
1848
  const path2 = relative(dir, full).split(sep).join("/");
@@ -2412,9 +2573,9 @@ function handleStream(req, res, sm, sessionId) {
2412
2573
 
2413
2574
  // src/session-manager.ts
2414
2575
  import { randomUUID } from "crypto";
2415
- import { mkdirSync as mkdirSync6, rmSync as rmSync4 } from "fs";
2416
- import { tmpdir as tmpdir4 } from "os";
2417
- import { join as join7 } from "path";
2576
+ import { mkdirSync as mkdirSync6 } from "fs";
2577
+ import { tmpdir as tmpdir5 } from "os";
2578
+ import { join as join8 } from "path";
2418
2579
 
2419
2580
  // src/llm/llm-session.ts
2420
2581
  var LlmSession = class {
@@ -2899,7 +3060,7 @@ var SessionManager = class {
2899
3060
  * files (CLAUDE.md, repo) to act on.
2900
3061
  */
2901
3062
  allocateSandbox(sessionId) {
2902
- const dir = join7(tmpdir4(), `leuria-${sessionId}`);
3063
+ const dir = join8(tmpdir5(), `leuria-${sessionId}`);
2903
3064
  mkdirSync6(dir, { recursive: true });
2904
3065
  return dir;
2905
3066
  }
@@ -2911,14 +3072,13 @@ var SessionManager = class {
2911
3072
  session.live = void 0;
2912
3073
  this.options.webMcpServer.removeChannel(session.id);
2913
3074
  setTimeout(() => this.sessions.delete(session.id), FORGET_AFTER_MS).unref();
2914
- try {
2915
- rmSync4(session.cwd, { recursive: true, force: true });
2916
- } catch (err) {
2917
- this.options.logger.warn("failed to remove sandbox", {
3075
+ removeScratch(
3076
+ session.cwd,
3077
+ (err) => this.options.logger.warn("failed to remove sandbox", {
2918
3078
  cwd: session.cwd,
2919
3079
  err: err instanceof Error ? err.message : String(err)
2920
- });
2921
- }
3080
+ })
3081
+ );
2922
3082
  }
2923
3083
  notify(session, event, data) {
2924
3084
  for (const listener of session.listeners) {
package/dist/cli.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  signIn,
41
41
  startEngine,
42
42
  updateAgents
43
- } from "./chunk-ES2YTJPL.js";
43
+ } from "./chunk-JBVFUXX5.js";
44
44
 
45
45
  // src/cli.ts
46
46
  import { spawn } from "child_process";
@@ -551,8 +551,10 @@ function createAdminHandler(ctx) {
551
551
  const result = await signIn(signingInAgent, {
552
552
  signal: controller.signal,
553
553
  methodId: typeof body.methodId === "string" ? body.methodId : void 0,
554
- // The app has no terminal: only agent methods (the agent runs its own flow).
554
+ // The app has no terminal: agent methods (the agent runs its own flow), or
555
+ // a terminal-only sign-in (Claude) in a window Leuria opens.
555
556
  terminal: false,
557
+ window: body.window === true,
556
558
  // Offered in the app too, in case the browser did not come up.
557
559
  onUrl: (url) => ctx.emit({ event: "signin_url", url }),
558
560
  onProgress: (message) => ctx.emit({ event: "progress", stage: "signin", message })
@@ -717,6 +719,7 @@ function createAdminHandler(ctx) {
717
719
  // `message` is for the CLI; the app words each stage itself (no jargon for visitors).
718
720
  onStep: (message) => ctx.emit({ event: "progress", stage: "check", step: ++checkStep, message })
719
721
  });
722
+ if (!result.ok) ctx.logger.warn("check failed", { agent, steps: result.steps.filter((step) => !step.ok) });
720
723
  sendJson(res, 200, result);
721
724
  return true;
722
725
  }
@@ -1086,7 +1089,10 @@ function sites(args) {
1086
1089
  }
1087
1090
  }
1088
1091
  function openInBrowser(url) {
1089
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1092
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : (
1093
+ // Not `cmd /c start`: cmd would read the & between query parameters as a new command.
1094
+ process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]]
1095
+ );
1090
1096
  try {
1091
1097
  const child = spawn(command, args, { stdio: "ignore", detached: true });
1092
1098
  child.on("error", () => void 0);
@@ -1226,13 +1232,19 @@ async function startForApp(config, verbose, devPairing) {
1226
1232
  });
1227
1233
  setTimeout(updateAis, AI_UPDATE_DELAY_MS).unref();
1228
1234
  setInterval(updateAis, AI_UPDATE_EVERY_MS).unref();
1235
+ let stopping = false;
1229
1236
  const shutdown = async () => {
1237
+ if (stopping) return;
1238
+ stopping = true;
1230
1239
  await engine.close();
1231
1240
  process.exit(0);
1232
1241
  };
1233
1242
  process.on("SIGINT", shutdown);
1234
1243
  process.on("SIGTERM", shutdown);
1235
1244
  process.stdin.on("end", shutdown);
1245
+ process.stdin.on("data", (chunk) => {
1246
+ if (chunk.toString("utf-8").split(/\r?\n/).includes("quit")) void shutdown();
1247
+ });
1236
1248
  process.stdin.resume();
1237
1249
  }
1238
1250
  function out(text) {
package/dist/index.d.ts CHANGED
@@ -921,6 +921,11 @@ interface SignInStatus {
921
921
  methods: AgentInfo["authMethods"];
922
922
  /** Models the agent offers once signed in (from `session/new`), with the visitor's choice as `current`. */
923
923
  models?: AgentModels;
924
+ /**
925
+ * The agent signs in only on the command line (Claude): Leuria can open a
926
+ * terminal window for it (`signIn(id, { window: true })`).
927
+ */
928
+ window?: boolean;
924
929
  }
925
930
  /**
926
931
  * Is the visitor signed in to this agent? Opens (and closes) a real ACP session, within a time limit.
@@ -934,6 +939,8 @@ declare function signIn(id: string, options?: {
934
939
  methodId?: string;
935
940
  /** The caller can run an interactive terminal method (a TTY). */
936
941
  terminal?: boolean;
942
+ /** Run the agent's terminal sign-in in a new terminal window (the desktop app has no terminal). */
943
+ window?: boolean;
937
944
  onProgress?: (message: string) => void;
938
945
  /** Cancel a sign-in in progress: the agent is stopped (which frees its login callback). */
939
946
  signal?: AbortSignal;
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  resolveAgentCommand,
19
19
  signIn,
20
20
  startEngine
21
- } from "./chunk-ES2YTJPL.js";
21
+ } from "./chunk-JBVFUXX5.js";
22
22
  export {
23
23
  DEFAULT_PORT,
24
24
  GrantStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leuria/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Bring your own AI to the web: the Leuria engine. Lets websites you approve use the AI on your machine, through tools the page provides.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  "dependencies": {
31
31
  "@agentclientprotocol/sdk": "^1.5.0",
32
32
  "cacache": "^21.0.1",
33
+ "cross-spawn": "^7.0.6",
33
34
  "giget": "^3.3.1",
34
35
  "ipaddr.js": "^2.5.0",
35
36
  "ws": "^8.18.0",
@@ -37,6 +38,7 @@
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/cacache": "^20.0.1",
41
+ "@types/cross-spawn": "^6.0.6",
40
42
  "@types/node": "^22.0.0",
41
43
  "@types/ws": "^8.5.0",
42
44
  "tsup": "^8.4.0",