@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.
- package/dist/{chunk-ES2YTJPL.js → chunk-JBVFUXX5.js} +219 -59
- package/dist/cli.js +15 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/package.json +3 -1
|
@@ -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
|
-
|
|
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.
|
|
657
|
+
var VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
639
658
|
|
|
640
659
|
// src/auth.ts
|
|
641
|
-
import
|
|
642
|
-
import { mkdtempSync as
|
|
643
|
-
import { tmpdir as
|
|
644
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
935
|
-
import { tmpdir } from "os";
|
|
936
|
-
import { delimiter, join as
|
|
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" &&
|
|
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 =
|
|
950
|
-
const bin =
|
|
951
|
-
const log =
|
|
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
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1118
|
-
return { ok: false, detail:
|
|
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,
|
|
1308
|
+
return await Promise.race([cancelled, run2()]);
|
|
1165
1309
|
} finally {
|
|
1166
1310
|
close();
|
|
1167
1311
|
}
|
|
1168
|
-
async function
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1567
|
-
import { extname, join as
|
|
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(
|
|
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(
|
|
1822
|
+
skill.files = await readTextFiles(join7(skillFile, ".."));
|
|
1662
1823
|
found.push(skill);
|
|
1663
1824
|
}
|
|
1664
1825
|
return found;
|
|
1665
1826
|
} finally {
|
|
1666
|
-
await
|
|
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 [
|
|
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(
|
|
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 =
|
|
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
|
|
2416
|
-
import { tmpdir as
|
|
2417
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
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-
|
|
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:
|
|
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]] :
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leuria/cli",
|
|
3
|
-
"version": "0.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",
|