@lumi.ai/runner 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +77 -58
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -532,7 +532,7 @@ function mcpUrl(config2) {
|
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
// src/version.ts
|
|
535
|
-
var RUNNER_VERSION = true ? "0.3.
|
|
535
|
+
var RUNNER_VERSION = true ? "0.3.2" : "0.0.0-dev";
|
|
536
536
|
|
|
537
537
|
// src/auth.ts
|
|
538
538
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2639,10 +2639,10 @@ function serviceStatus() {
|
|
|
2639
2639
|
};
|
|
2640
2640
|
}
|
|
2641
2641
|
if (process.platform === "win32") {
|
|
2642
|
-
const
|
|
2643
|
-
if (!
|
|
2642
|
+
const query5 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
|
|
2643
|
+
if (!query5.ok) return { state: "not-installed", detail: "No scheduled task installed." };
|
|
2644
2644
|
return {
|
|
2645
|
-
state: /\bRunning\b/i.test(
|
|
2645
|
+
state: /\bRunning\b/i.test(query5.out) ? "running" : "installed",
|
|
2646
2646
|
detail: "Scheduled task installed (runs at logon).",
|
|
2647
2647
|
unitPath: WINDOWS_TASK
|
|
2648
2648
|
};
|
|
@@ -2751,21 +2751,6 @@ async function openShipSession(shipId) {
|
|
|
2751
2751
|
const user = await signInToShip(fb, config2, shipId);
|
|
2752
2752
|
return { shipId, config: config2, fb, user };
|
|
2753
2753
|
}
|
|
2754
|
-
async function openSession() {
|
|
2755
|
-
const config2 = requireConfig();
|
|
2756
|
-
if (config2.ships.length === 0) {
|
|
2757
|
-
throw new AuthError("No Ships assigned. Run: lumi-runner ship add <shipId>");
|
|
2758
|
-
}
|
|
2759
|
-
let last;
|
|
2760
|
-
for (const shipId of config2.ships) {
|
|
2761
|
-
try {
|
|
2762
|
-
return await openShipSession(shipId);
|
|
2763
|
-
} catch (e) {
|
|
2764
|
-
last = e;
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
throw last instanceof Error ? last : new AuthError("Could not sign in to any Ship.");
|
|
2768
|
-
}
|
|
2769
2754
|
|
|
2770
2755
|
// src/cli/commands/doctor.ts
|
|
2771
2756
|
var ok = (id, label, detail) => ({ id, label, level: "ok", detail });
|
|
@@ -3001,6 +2986,26 @@ function report(checks) {
|
|
|
3001
2986
|
import { spawn as spawn4 } from "node:child_process";
|
|
3002
2987
|
import os5 from "node:os";
|
|
3003
2988
|
import { signInWithCustomToken as signInWithCustomToken2 } from "firebase/auth";
|
|
2989
|
+
function buildLoginResult(approved, existing, fallbackProjectId, mcpUrl2) {
|
|
2990
|
+
const approvedShips = approved.approvedShips ?? Object.keys(approved.shipKeys ?? {});
|
|
2991
|
+
const selectedShips = approved.selectedShips ?? [];
|
|
2992
|
+
const config2 = {
|
|
2993
|
+
...existing,
|
|
2994
|
+
apiKey: approved.apiKey || existing?.apiKey || "",
|
|
2995
|
+
projectId: approved.projectId || fallbackProjectId,
|
|
2996
|
+
runnerId: approved.runnerId || existing?.runnerId || "",
|
|
2997
|
+
shipKeys: { ...existing?.shipKeys ?? {}, ...approved.shipKeys ?? {} },
|
|
2998
|
+
ships: selectedShips.length > 0 ? selectedShips : existing?.ships ?? [],
|
|
2999
|
+
...mcpUrl2 ? { mcpUrl: mcpUrl2 } : {}
|
|
3000
|
+
};
|
|
3001
|
+
return {
|
|
3002
|
+
config: config2,
|
|
3003
|
+
approvedShips,
|
|
3004
|
+
// Fall back to deriving it, so an older backend that omits the field reports accurately
|
|
3005
|
+
// rather than crashing or claiming nothing is pending.
|
|
3006
|
+
pendingShips: (approved.pendingShips ?? selectedShips.filter((id) => !approvedShips.includes(id))).filter((id) => config2.ships.includes(id))
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3004
3009
|
function openBrowser(url) {
|
|
3005
3010
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
3006
3011
|
try {
|
|
@@ -3063,36 +3068,25 @@ Code: ${pc.bold(start.displayCode)}`,
|
|
|
3063
3068
|
throw new CliError("The approval window expired. Run `lumi-runner login` again.");
|
|
3064
3069
|
}
|
|
3065
3070
|
progress.stop("Approved.");
|
|
3066
|
-
const config2 =
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
projectId
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
// What the human PICKED on the approval page. Their choice replaces whatever this machine
|
|
3076
|
-
// was serving — that page is where the decision is now made.
|
|
3077
|
-
ships: approved.selectedShips.length > 0 ? approved.selectedShips : existingConfig?.ships ?? [],
|
|
3078
|
-
...options.mcpUrl ? { mcpUrl: options.mcpUrl } : {}
|
|
3079
|
-
};
|
|
3071
|
+
const { config: config2, approvedShips, pendingShips } = buildLoginResult(
|
|
3072
|
+
approved,
|
|
3073
|
+
existingConfig,
|
|
3074
|
+
projectId,
|
|
3075
|
+
options.mcpUrl
|
|
3076
|
+
);
|
|
3077
|
+
if (!config2.runnerId) {
|
|
3078
|
+
throw new CliError("The approval did not name this machine. Run `lumi-runner login` again.");
|
|
3079
|
+
}
|
|
3080
3080
|
if (!config2.apiKey) {
|
|
3081
3081
|
throw new CliError(
|
|
3082
3082
|
"The approval did not include a Firebase apiKey. Update the web app, or pass --api-key with --token."
|
|
3083
3083
|
);
|
|
3084
3084
|
}
|
|
3085
3085
|
const fb = initFirebase(config2);
|
|
3086
|
-
const first =
|
|
3086
|
+
const first = approvedShips[0];
|
|
3087
3087
|
const uid2 = first ? (await signInWithCustomToken2(fb.auth, (await exchange(config2, first)).customToken)).user.uid : "(no Ship approved yet)";
|
|
3088
3088
|
saveConfig(config2);
|
|
3089
|
-
return report2({
|
|
3090
|
-
runnerId: config2.runnerId,
|
|
3091
|
-
uid: uid2,
|
|
3092
|
-
ships: config2.ships,
|
|
3093
|
-
approvedShips: approved.approvedShips,
|
|
3094
|
-
pendingShips: approved.pendingShips.filter((id) => config2.ships.includes(id))
|
|
3095
|
-
});
|
|
3089
|
+
return report2({ runnerId: config2.runnerId, uid: uid2, ships: config2.ships, approvedShips, pendingShips });
|
|
3096
3090
|
}
|
|
3097
3091
|
async function exchange(config2, shipId) {
|
|
3098
3092
|
return callPublicFunction(
|
|
@@ -3271,17 +3265,19 @@ async function runUninstall(options) {
|
|
|
3271
3265
|
}
|
|
3272
3266
|
|
|
3273
3267
|
// src/cli/commands/ship.ts
|
|
3274
|
-
import {
|
|
3268
|
+
import { doc as doc9, getDoc as getDoc7 } from "firebase/firestore";
|
|
3275
3269
|
async function listMyShips() {
|
|
3276
|
-
const
|
|
3277
|
-
const
|
|
3278
|
-
query4(collectionGroup(fb.db, COLLECTIONS.members), where4("uid", "==", user.uid))
|
|
3279
|
-
);
|
|
3280
|
-
const shipIds = memberships.docs.map((d) => d.ref.parent.parent?.id).filter((id) => !!id);
|
|
3270
|
+
const config2 = requireConfig();
|
|
3271
|
+
const ids = Object.keys(config2.shipKeys ?? {});
|
|
3281
3272
|
const ships = await Promise.all(
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3273
|
+
ids.map(async (id) => {
|
|
3274
|
+
try {
|
|
3275
|
+
const { fb } = await openShipSession(id);
|
|
3276
|
+
const snap = await getDoc7(doc9(fb.db, COLLECTIONS.ships, id));
|
|
3277
|
+
return { id, ...snap.data() };
|
|
3278
|
+
} catch {
|
|
3279
|
+
return null;
|
|
3280
|
+
}
|
|
3285
3281
|
})
|
|
3286
3282
|
);
|
|
3287
3283
|
return ships.filter((s) => s !== null).sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
@@ -3289,7 +3285,9 @@ async function listMyShips() {
|
|
|
3289
3285
|
async function pickShips(current) {
|
|
3290
3286
|
const ships = await listMyShips();
|
|
3291
3287
|
if (ships.length === 0) {
|
|
3292
|
-
throw new CliError(
|
|
3288
|
+
throw new CliError(
|
|
3289
|
+
"This machine holds no Ship keys. Run `lumi-runner login` and have a captain approve it \u2014 a daemon can only serve Ships it was approved for."
|
|
3290
|
+
);
|
|
3293
3291
|
}
|
|
3294
3292
|
return promptMultiSelect({
|
|
3295
3293
|
message: "Which Ships should this machine serve?",
|
|
@@ -3399,9 +3397,9 @@ import {
|
|
|
3399
3397
|
doc as doc10,
|
|
3400
3398
|
getCountFromServer as getCountFromServer2,
|
|
3401
3399
|
getDoc as getDoc8,
|
|
3402
|
-
getDocs as
|
|
3403
|
-
query as
|
|
3404
|
-
where as
|
|
3400
|
+
getDocs as getDocs5,
|
|
3401
|
+
query as query4,
|
|
3402
|
+
where as where4
|
|
3405
3403
|
} from "firebase/firestore";
|
|
3406
3404
|
async function runStatus() {
|
|
3407
3405
|
const config2 = loadConfig();
|
|
@@ -3415,16 +3413,33 @@ async function runStatus() {
|
|
|
3415
3413
|
}
|
|
3416
3414
|
const progress = spinner2();
|
|
3417
3415
|
progress.start("Reading Ship state\u2026");
|
|
3418
|
-
const { fb } = await openSession();
|
|
3419
3416
|
const ships = [];
|
|
3420
3417
|
for (const shipId of config2.ships) {
|
|
3418
|
+
let fb;
|
|
3419
|
+
try {
|
|
3420
|
+
({ fb } = await openShipSession(shipId));
|
|
3421
|
+
} catch {
|
|
3422
|
+
ships.push({
|
|
3423
|
+
shipId,
|
|
3424
|
+
enrolled: false,
|
|
3425
|
+
approved: false,
|
|
3426
|
+
online: false,
|
|
3427
|
+
lastSeenAt: null,
|
|
3428
|
+
currentJob: null,
|
|
3429
|
+
queued: 0,
|
|
3430
|
+
today: { ...EMPTY_USAGE_TOTALS },
|
|
3431
|
+
engineLimits: [],
|
|
3432
|
+
unreachable: true
|
|
3433
|
+
});
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3421
3436
|
const shipRef = doc10(fb.db, COLLECTIONS.ships, shipId);
|
|
3422
3437
|
const mirrorSnap = await getDoc8(doc10(shipRef, COLLECTIONS.runners, config2.runnerId));
|
|
3423
3438
|
const mirror = mirrorSnap.data();
|
|
3424
3439
|
let queued = 0;
|
|
3425
3440
|
try {
|
|
3426
3441
|
const counted = await getCountFromServer2(
|
|
3427
|
-
|
|
3442
|
+
query4(collection7(shipRef, COLLECTIONS.jobs), where4("status", "==", "queued"))
|
|
3428
3443
|
);
|
|
3429
3444
|
queued = counted.data().count;
|
|
3430
3445
|
} catch {
|
|
@@ -3433,7 +3448,7 @@ async function runStatus() {
|
|
|
3433
3448
|
const usageSnap = await getDoc8(doc10(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
|
|
3434
3449
|
const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
3435
3450
|
const now = Date.now();
|
|
3436
|
-
const limitsSnap = await
|
|
3451
|
+
const limitsSnap = await getDocs5(collection7(shipRef, COLLECTIONS.engineLimits));
|
|
3437
3452
|
const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
|
|
3438
3453
|
ships.push({
|
|
3439
3454
|
shipId,
|
|
@@ -3470,8 +3485,12 @@ async function runStatus() {
|
|
|
3470
3485
|
return 0;
|
|
3471
3486
|
}
|
|
3472
3487
|
for (const ship2 of ships) {
|
|
3473
|
-
const state = !ship2.enrolled ? pc.dim("not enrolled") : !ship2.approved ? pc.yellow("awaiting approval") : ship2.online ? pc.green("online") : pc.red("offline");
|
|
3488
|
+
const state = ship2.unreachable ? pc.red("no access") : !ship2.enrolled ? pc.dim("not enrolled") : !ship2.approved ? pc.yellow("awaiting approval") : ship2.online ? pc.green("online") : pc.red("offline");
|
|
3474
3489
|
say.line(` ${pc.bold(ship2.shipId)} ${state}`);
|
|
3490
|
+
if (ship2.unreachable) {
|
|
3491
|
+
say.line(` ${pc.dim("this machine\u2019s key for that Ship was revoked \u2014 re-approve it on the Daemons page")}`);
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3475
3494
|
if (ship2.lastSeenAt) say.line(` last seen ${new Date(ship2.lastSeenAt).toLocaleString()}`);
|
|
3476
3495
|
say.line(
|
|
3477
3496
|
ship2.currentJob ? ` running job ${ship2.currentJob.jobId} on task ${ship2.currentJob.taskId} (agent ${ship2.currentJob.agentId})` : " running nothing"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|