@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.
Files changed (2) hide show
  1. package/dist/cli.js +77 -58
  2. 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.0" : "0.0.0-dev";
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 query6 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
2643
- if (!query6.ok) return { state: "not-installed", detail: "No scheduled task installed." };
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(query6.out) ? "running" : "installed",
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
- ...existingConfig,
3068
- apiKey: approved.apiKey || existingConfig?.apiKey || "",
3069
- projectId: approved.projectId || projectId,
3070
- runnerId: approved.runnerId,
3071
- // THE CREDENTIALS (PRD §8.1) — one Ship key per approved Ship, handed over on this one hop
3072
- // and nowhere else. Merged over any we already held so that approving a SECOND Ship does not
3073
- // silently revoke the first: this response only carries keys for what was just approved.
3074
- shipKeys: { ...existingConfig?.shipKeys ?? {}, ...approved.shipKeys },
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 = approved.approvedShips[0];
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 { collectionGroup, doc as doc9, getDoc as getDoc7, getDocs as getDocs5, query as query4, where as where4 } from "firebase/firestore";
3268
+ import { doc as doc9, getDoc as getDoc7 } from "firebase/firestore";
3275
3269
  async function listMyShips() {
3276
- const { fb, user } = await openSession();
3277
- const memberships = await getDocs5(
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
- shipIds.map(async (id) => {
3283
- const snap = await getDoc7(doc9(fb.db, COLLECTIONS.ships, id));
3284
- return snap.exists() ? { id: snap.id, ...snap.data() } : null;
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("You are not a member of any Ship yet \u2014 create one in the web app first.");
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 getDocs6,
3403
- query as query5,
3404
- where as where5
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
- query5(collection7(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
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 getDocs6(collection7(shipRef, COLLECTIONS.engineLimits));
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.0",
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.",