@factiii/runner 0.12.0 → 0.12.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 +329 -161
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
3984
3984
  var require_supports_color = __commonJS({
3985
3985
  "../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
3986
3986
  "use strict";
3987
- var os7 = require("os");
3987
+ var os8 = require("os");
3988
3988
  var hasFlag = require_has_flag();
3989
3989
  var env = process.env;
3990
3990
  var forceColor;
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
4022
4022
  }
4023
4023
  const min = forceColor ? 1 : 0;
4024
4024
  if (process.platform === "win32") {
4025
- const osRelease = os7.release().split(".");
4025
+ const osRelease = os8.release().split(".");
4026
4026
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
4027
4027
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
4028
4028
  }
@@ -6499,7 +6499,7 @@ var require_websocket = __commonJS({
6499
6499
  var http = require("http");
6500
6500
  var net3 = require("net");
6501
6501
  var tls = require("tls");
6502
- var { randomBytes, createHash: createHash2 } = require("crypto");
6502
+ var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
6503
6503
  var { Duplex, Readable } = require("stream");
6504
6504
  var { URL: URL2 } = require("url");
6505
6505
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -7037,7 +7037,7 @@ var require_websocket = __commonJS({
7037
7037
  }
7038
7038
  }
7039
7039
  const defaultPort = isSecure ? 443 : 80;
7040
- const key = randomBytes(16).toString("base64");
7040
+ const key = randomBytes2(16).toString("base64");
7041
7041
  const request = isSecure ? https.request : http.request;
7042
7042
  const protocolSet = /* @__PURE__ */ new Set();
7043
7043
  let perMessageDeflate;
@@ -13805,6 +13805,7 @@ async function teardownSession(s, target) {
13805
13805
 
13806
13806
  // ../../shared/all/helpers/board-agent-core/exec.ts
13807
13807
  var import_child_process2 = require("child_process");
13808
+ var import_crypto = require("crypto");
13808
13809
  var import_fs2 = require("fs");
13809
13810
  var import_promises2 = __toESM(require("fs/promises"));
13810
13811
  var import_net = __toESM(require("net"));
@@ -13845,6 +13846,22 @@ async function allocateSpacePorts(spaceDir) {
13845
13846
  await import_promises2.default.writeFile(portsFilePath(spaceDir), ports.join(","), "utf-8");
13846
13847
  return ports;
13847
13848
  }
13849
+ var SECRET_FILE = ".secret";
13850
+ function spaceSecret(spaceDir) {
13851
+ const file = import_path4.default.join(spaceDir, SECRET_FILE);
13852
+ try {
13853
+ const existing = (0, import_fs2.readFileSync)(file, "utf-8").trim();
13854
+ if (existing) return existing;
13855
+ } catch {
13856
+ }
13857
+ const secret = (0, import_crypto.randomBytes)(24).toString("hex");
13858
+ try {
13859
+ (0, import_fs2.mkdirSync)(spaceDir, { recursive: true });
13860
+ (0, import_fs2.writeFileSync)(file, secret, { encoding: "utf-8", mode: 384 });
13861
+ } catch {
13862
+ }
13863
+ return secret;
13864
+ }
13848
13865
  function readSpacePorts(spaceDir) {
13849
13866
  try {
13850
13867
  return (0, import_fs2.readFileSync)(portsFilePath(spaceDir), "utf-8").split(",").map((p) => Number(p.trim())).filter((p) => Number.isInteger(p) && p > 0);
@@ -14144,6 +14161,18 @@ var import_promises5 = __toESM(require("fs/promises"));
14144
14161
  var import_os2 = __toESM(require("os"));
14145
14162
  var import_path7 = __toESM(require("path"));
14146
14163
 
14164
+ // ../../shared/all/domains/products.ts
14165
+ var TRONS_PER_USD = 1e6;
14166
+ var TRONS_PER_CENT = TRONS_PER_USD / 100;
14167
+ var FOUNDERS_REFUND_WINDOW_DAYS = 60;
14168
+ var FOUNDERS_REFUND_POLICY = `Refundable within ${FOUNDERS_REFUND_WINDOW_DAYS} days of purchase \u2014 after that, all sales are final.`;
14169
+
14170
+ // ../../shared/all/domains/autoTopUp.ts
14171
+ var DAY_MS = 24 * 60 * 60 * 1e3;
14172
+ var MONTH_MS = 30 * DAY_MS;
14173
+ var CLAIM_STALE_MS = 5 * 60 * 1e3;
14174
+ var MIN_THRESHOLD_TRONS = TRONS_PER_CENT;
14175
+
14147
14176
  // ../../shared/all/domains/posts.ts
14148
14177
  var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
14149
14178
  postDataSort2["DAILY"] = "DAILY";
@@ -14346,7 +14375,11 @@ var checkPurchaseSchema = external_exports.object({
14346
14375
  productId: external_exports.string(),
14347
14376
  type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
14348
14377
  receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
14349
- currency: external_exports.string().length(3).default("USD")
14378
+ currency: external_exports.string().length(3).default("USD"),
14379
+ // Explicit consent to keep this card for auto top-up. Card-network rules
14380
+ // require the buyer to agree before a card may be charged while they are not
14381
+ // present, so this must come from a checkbox they ticked — never a default.
14382
+ saveCardForAutoTopUp: external_exports.boolean().default(false)
14350
14383
  });
14351
14384
  var creditPurchaseSchema = external_exports.object({
14352
14385
  orderId: external_exports.string(),
@@ -14649,6 +14682,19 @@ var skillFrontmatterSchema = external_exports.object({
14649
14682
  }).strict();
14650
14683
  var skillAgentSchema = external_exports.enum(["claude", "codex"]);
14651
14684
  var skillScopeSchema = external_exports.enum(["global", "repo"]);
14685
+ var autoTopUpSchema = external_exports.object({
14686
+ enabled: external_exports.boolean(),
14687
+ productId: external_exports.number().int().positive(),
14688
+ thresholdTrons: external_exports.number().int().min(MIN_THRESHOLD_TRONS),
14689
+ dailyCapCents: external_exports.number().int().min(0),
14690
+ monthlyCapCents: external_exports.number().int().min(0),
14691
+ // Alerts. `alertBalanceTrons` is null when the low-balance warning is off;
14692
+ // it works independently of `enabled`, since with auto top-up off it is the
14693
+ // only safety net there is.
14694
+ alertOnTopUp: external_exports.boolean(),
14695
+ alertBalanceTrons: external_exports.number().int().min(MIN_THRESHOLD_TRONS).nullable(),
14696
+ alertNearCap: external_exports.boolean()
14697
+ });
14652
14698
 
14653
14699
  // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14654
14700
  var import_child_process3 = require("child_process");
@@ -16903,7 +16949,7 @@ ${buildPrompt(prompts_default.implement)}`;
16903
16949
  };
16904
16950
 
16905
16951
  // ../../shared/all/helpers/terminal-ai/terminal-mode.ts
16906
- var import_crypto3 = require("crypto");
16952
+ var import_crypto4 = require("crypto");
16907
16953
 
16908
16954
  // ../../shared/all/helpers/board-agent-core/boardTerminal.ts
16909
16955
  function boardTerminalKey(spaceSlug) {
@@ -16959,12 +17005,12 @@ function drainStateMessages(buffer) {
16959
17005
  }
16960
17006
 
16961
17007
  // ../../shared/all/helpers/terminal-ai/digest.ts
16962
- var import_crypto2 = __toESM(require("crypto"));
17008
+ var import_crypto3 = __toESM(require("crypto"));
16963
17009
  var import_promises8 = require("fs/promises");
16964
17010
  var import_path10 = __toESM(require("path"));
16965
17011
 
16966
17012
  // ../../shared/all/helpers/board-agent-core/plugin.ts
16967
- var import_crypto = __toESM(require("crypto"));
17013
+ var import_crypto2 = __toESM(require("crypto"));
16968
17014
  var import_fs4 = require("fs");
16969
17015
  var import_promises7 = require("fs/promises");
16970
17016
  var import_os3 = __toESM(require("os"));
@@ -17013,7 +17059,7 @@ async function treeFiles(dir) {
17013
17059
  return files.sort();
17014
17060
  }
17015
17061
  async function fingerprint(source, url2, version = "") {
17016
- const hash = import_crypto.default.createHash("sha256").update(url2).update(version);
17062
+ const hash = import_crypto2.default.createHash("sha256").update(url2).update(version);
17017
17063
  for (const file of await treeFiles(source)) {
17018
17064
  hash.update(import_path9.default.relative(source, file));
17019
17065
  hash.update(await (0, import_promises7.readFile)(file));
@@ -17131,7 +17177,7 @@ async function pluginContribution() {
17131
17177
  return value2;
17132
17178
  }
17133
17179
  async function terminalDigest(target) {
17134
- const hash = import_crypto2.default.createHash("sha256").update(TMUX_CONF).update(terminalPrelude(CWD_PLACEHOLDER, target.paths.bin));
17180
+ const hash = import_crypto3.default.createHash("sha256").update(TMUX_CONF).update(terminalPrelude(CWD_PLACEHOLDER, target.paths.bin));
17135
17181
  const env = target.stateEnv();
17136
17182
  for (const key of Object.keys(env).sort()) {
17137
17183
  if (VOLATILE_ENV_KEYS.has(key)) continue;
@@ -18330,7 +18376,7 @@ var TerminalModeEngine = class {
18330
18376
  }
18331
18377
  const safeExt = (ext || "png").toLowerCase().replace(/[^a-z0-9]/g, "") || "png";
18332
18378
  const oneShot = !dropId;
18333
- const safeId2 = oneShot ? (0, import_crypto3.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
18379
+ const safeId2 = oneShot ? (0, import_crypto4.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
18334
18380
  if (!safeId2) throw new Error("Invalid drop id.");
18335
18381
  const dropsDir = `${target.paths.root}/drops`;
18336
18382
  const abs = `${dropsDir}/${safeId2}.${safeExt}`;
@@ -18531,15 +18577,7 @@ var TerminalModeEngine = class {
18531
18577
  `${tmuxCli(target)} list-panes -a -F '${TAB_LIST_FORMAT}' 2>/dev/null || true`
18532
18578
  );
18533
18579
  const rows = parseTerminalTabs(stdout, prefix);
18534
- const current = await terminalDigest(target).catch(() => "");
18535
- return rows.map(({ id, name, fp, dead }) => ({
18536
- id,
18537
- name,
18538
- // No stamp means a session older than the stamp, as blind to the
18539
- // current build as a mismatched one.
18540
- stale: !!current && fp !== current,
18541
- dead
18542
- }));
18580
+ return rows.map(({ id, name, dead }) => ({ id, name, dead }));
18543
18581
  } catch {
18544
18582
  return [];
18545
18583
  }
@@ -19195,7 +19233,7 @@ async function listGithubBranches(token, fullName) {
19195
19233
  var import_path13 = __toESM(require("path"));
19196
19234
 
19197
19235
  // ../../shared/all/helpers/board-agent-core/secrets-gate.ts
19198
- var import_crypto4 = __toESM(require("crypto"));
19236
+ var import_crypto5 = __toESM(require("crypto"));
19199
19237
  var DEPLOY_ORIGIN = "Deploy run";
19200
19238
  var UNATTRIBUTED = "This runner did not start the session that asked, so the request was refused. `factiii-secrets` only works inside a session the runner spawned.";
19201
19239
  var SecretsGate = class {
@@ -19220,7 +19258,14 @@ var SecretsGate = class {
19220
19258
  /** Mint a token for one session, labelled with what the owner should see on
19221
19259
  * the prompt - the label is most of the decision, so name the session. */
19222
19260
  issueToken(origin) {
19223
- const token = import_crypto4.default.randomBytes(24).toString("hex");
19261
+ const token = import_crypto5.default.randomBytes(24).toString("hex");
19262
+ this.tokens.set(token, origin);
19263
+ return token;
19264
+ }
19265
+ /** Register a durable token the caller already holds, instead of minting a
19266
+ * fresh one. For the space token: terminals outlive this process, so a
19267
+ * minted one leaves them unattributed after a restart. Idempotent. */
19268
+ adoptToken(token, origin) {
19224
19269
  this.tokens.set(token, origin);
19225
19270
  return token;
19226
19271
  }
@@ -19244,7 +19289,7 @@ var SecretsGate = class {
19244
19289
  return new Promise((resolve2) => {
19245
19290
  this.waiting = {
19246
19291
  request: {
19247
- id: import_crypto4.default.randomUUID(),
19292
+ id: import_crypto5.default.randomUUID(),
19248
19293
  command: opts.command,
19249
19294
  keys: opts.keys,
19250
19295
  origin: opts.origin,
@@ -19288,7 +19333,7 @@ var import_path12 = __toESM(require("path"));
19288
19333
 
19289
19334
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
19290
19335
  var import_child_process4 = require("child_process");
19291
- var import_crypto5 = __toESM(require("crypto"));
19336
+ var import_crypto6 = __toESM(require("crypto"));
19292
19337
  var import_promises9 = __toESM(require("fs/promises"));
19293
19338
  var import_path11 = __toESM(require("path"));
19294
19339
  var import_util6 = require("util");
@@ -19316,7 +19361,7 @@ async function assertBinaries() {
19316
19361
  }
19317
19362
  }
19318
19363
  function deriveKey(password, salt) {
19319
- return import_crypto5.default.scryptSync(password, salt, 32, {
19364
+ return import_crypto6.default.scryptSync(password, salt, 32, {
19320
19365
  N: SCRYPT_N,
19321
19366
  maxmem: 64 * 1024 * 1024
19322
19367
  });
@@ -19345,9 +19390,9 @@ async function initSecrets(configDir, password) {
19345
19390
  if (!identity || !recipient) {
19346
19391
  throw new Error("age-keygen returned no usable keypair.");
19347
19392
  }
19348
- const salt = import_crypto5.default.randomBytes(16);
19349
- const iv = import_crypto5.default.randomBytes(12);
19350
- const cipher = import_crypto5.default.createCipheriv(
19393
+ const salt = import_crypto6.default.randomBytes(16);
19394
+ const iv = import_crypto6.default.randomBytes(12);
19395
+ const cipher = import_crypto6.default.createCipheriv(
19351
19396
  "aes-256-gcm",
19352
19397
  deriveKey(password, salt),
19353
19398
  iv
@@ -19372,7 +19417,7 @@ async function initSecrets(configDir, password) {
19372
19417
  return { success: true };
19373
19418
  }
19374
19419
  function decryptIdentity(box, password) {
19375
- const decipher = import_crypto5.default.createDecipheriv(
19420
+ const decipher = import_crypto6.default.createDecipheriv(
19376
19421
  "aes-256-gcm",
19377
19422
  deriveKey(password, Buffer.from(box.salt, "base64")),
19378
19423
  Buffer.from(box.iv, "base64")
@@ -20037,6 +20082,7 @@ var SpaceCore = class {
20037
20082
  this.runnerToken = options.runnerToken;
20038
20083
  this.serverUrl = options.serverUrl;
20039
20084
  this.mcp = options.mcp;
20085
+ this.mcpToken = options.mcpToken;
20040
20086
  this.requestSudo = options.requestSudo;
20041
20087
  this.activity = new ActivityTracker(this.emitter);
20042
20088
  this.gate.subscribe(() => {
@@ -20131,7 +20177,11 @@ var SpaceCore = class {
20131
20177
  * that DOES know itself - the deploy run - mints its own and overrides this.
20132
20178
  */
20133
20179
  secretsToken() {
20134
- this.spaceSecretsToken ||= this.gate.issueToken("A session on this board");
20180
+ if (this.spaceSecretsToken) return this.spaceSecretsToken;
20181
+ this.spaceSecretsToken = this.gate.adoptToken(
20182
+ spaceSecret(this.spaceDir()),
20183
+ "A session on this board"
20184
+ );
20135
20185
  return this.spaceSecretsToken;
20136
20186
  }
20137
20187
  readConfig() {
@@ -20142,7 +20192,8 @@ var SpaceCore = class {
20142
20192
  target() {
20143
20193
  this.execTarget ??= hostTarget(this.spaceDir(), () => {
20144
20194
  const env = {};
20145
- for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
20195
+ const onedrive = this.mcpToken?.();
20196
+ if (onedrive) env[MCP_TOKEN_ENV] = onedrive;
20146
20197
  const board = this.boardMcp();
20147
20198
  if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
20148
20199
  env.FACTIII_SECRETS_TOKEN = this.secretsToken();
@@ -22306,12 +22357,59 @@ ${c.content.trim() || "(no description)"}`
22306
22357
 
22307
22358
  // ../../shared/all/helpers/board-agent-core/index-service.ts
22308
22359
  var import_child_process5 = require("child_process");
22309
- var import_crypto6 = require("crypto");
22310
- var import_fs5 = require("fs");
22360
+ var import_crypto7 = require("crypto");
22361
+ var import_fs6 = require("fs");
22311
22362
  var import_promises11 = __toESM(require("fs/promises"));
22312
22363
  var import_net3 = require("net");
22313
- var import_path16 = require("path");
22364
+ var import_path17 = require("path");
22314
22365
  var import_util7 = require("util");
22366
+
22367
+ // ../../shared/all/helpers/board-agent-core/node-version.ts
22368
+ var import_fs5 = require("fs");
22369
+ var import_os4 = __toESM(require("os"));
22370
+ var import_path16 = require("path");
22371
+ var MIN_INDEX_NODE_MAJOR = 20;
22372
+ var MAX_INDEX_NODE_MAJOR = 24;
22373
+ var NODE_OVERRIDE_ENV = "FACTIII_INDEX_NODE";
22374
+ function nodeMajor(version) {
22375
+ return Number(version.replace(/^v/, "").split(".")[0]);
22376
+ }
22377
+ function indexNodeSupported(version = process.versions.node) {
22378
+ const major = nodeMajor(version);
22379
+ return !Number.isNaN(major) && major >= MIN_INDEX_NODE_MAJOR && major <= MAX_INDEX_NODE_MAJOR;
22380
+ }
22381
+ function candidateNodes() {
22382
+ const paths = [];
22383
+ const home = import_os4.default.homedir();
22384
+ const nvmRoot = (0, import_path16.join)(home, ".nvm", "versions", "node");
22385
+ try {
22386
+ const versions = (0, import_fs5.readdirSync)(nvmRoot).filter((v) => indexNodeSupported(v)).sort((a, b) => nodeMajor(b) - nodeMajor(a));
22387
+ for (const v of versions) paths.push((0, import_path16.join)(nvmRoot, v, "bin", "node"));
22388
+ } catch {
22389
+ }
22390
+ for (const prefix of ["/opt/homebrew/opt", "/usr/local/opt"]) {
22391
+ for (let major = MAX_INDEX_NODE_MAJOR; major >= MIN_INDEX_NODE_MAJOR; major -= 1) {
22392
+ paths.push((0, import_path16.join)(prefix, `node@${major}`, "bin", "node"));
22393
+ }
22394
+ }
22395
+ return paths;
22396
+ }
22397
+ function findIndexNode() {
22398
+ const override = process.env[NODE_OVERRIDE_ENV];
22399
+ if (override && (0, import_fs5.existsSync)(override)) return override;
22400
+ if (indexNodeSupported()) return process.execPath;
22401
+ return candidateNodes().find((p) => (0, import_fs5.existsSync)(p)) ?? null;
22402
+ }
22403
+ function unsupportedNodeMessage() {
22404
+ return `The OneDrive index needs Node ${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR} (pinned 24.11.1); this host runs Node ${process.versions.node} and has no supported Node installed. It compiles better-sqlite3 from source, and that build fails on Node ${MAX_INDEX_NODE_MAJOR + 1}+.
22405
+
22406
+ Install one: nvm install 24.11.1
22407
+ or point the runner at an existing one: export ${NODE_OVERRIDE_ENV}=/path/to/node
22408
+
22409
+ Everything else on this runner works meanwhile \u2014 only OneDrive search is off.`;
22410
+ }
22411
+
22412
+ // ../../shared/all/helpers/board-agent-core/index-service.ts
22315
22413
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
22316
22414
  var IndexService = class {
22317
22415
  constructor(opts) {
@@ -22324,7 +22422,7 @@ var IndexService = class {
22324
22422
  /** Per-space bearer for MCP + file, derived from the control token (the
22325
22423
  * service computes the same). */
22326
22424
  bearerToken(spaceSlug) {
22327
- return (0, import_crypto6.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
22425
+ return (0, import_crypto7.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
22328
22426
  }
22329
22427
  /** MCP bridge for a space. Null until the service is up (which it kicks
22330
22428
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -22364,27 +22462,25 @@ var IndexService = class {
22364
22462
  await this.waitReady(this.port);
22365
22463
  return;
22366
22464
  }
22367
- await this.installDeps();
22465
+ const nodeBin = findIndexNode();
22466
+ if (!nodeBin) throw new Error(unsupportedNodeMessage());
22467
+ await this.installDeps(nodeBin);
22368
22468
  const port = await this.stablePort();
22369
22469
  const log = await this.openLog();
22370
- this.proc = (0, import_child_process5.spawn)(
22371
- process.execPath,
22372
- [(0, import_path16.join)(this.opts.indexDir, "index.js")],
22373
- {
22374
- cwd: this.opts.indexDir,
22375
- env: {
22376
- ...process.env,
22377
- NODE_ENV: "production",
22378
- PORT: String(port),
22379
- CONTROL_TOKEN: this.opts.controlToken,
22380
- EMBED_URL: this.opts.embedUrl(),
22381
- INDEX_DATA_DIR: this.opts.dataDir
22382
- },
22383
- stdio: log ? ["ignore", log, log] : "ignore",
22384
- // Own process group so stop() reaps it; nothing else reaps orphans here.
22385
- detached: true
22386
- }
22387
- );
22470
+ this.proc = (0, import_child_process5.spawn)(nodeBin, [(0, import_path17.join)(this.opts.indexDir, "index.js")], {
22471
+ cwd: this.opts.indexDir,
22472
+ env: {
22473
+ ...process.env,
22474
+ NODE_ENV: "production",
22475
+ PORT: String(port),
22476
+ CONTROL_TOKEN: this.opts.controlToken,
22477
+ EMBED_URL: this.opts.embedUrl(),
22478
+ INDEX_DATA_DIR: this.opts.dataDir
22479
+ },
22480
+ stdio: log ? ["ignore", log, log] : "ignore",
22481
+ // Own process group so stop() reaps it; nothing else reaps orphans here.
22482
+ detached: true
22483
+ });
22388
22484
  this.proc.on("exit", () => {
22389
22485
  this.proc = null;
22390
22486
  this.ready = null;
@@ -22398,31 +22494,58 @@ var IndexService = class {
22398
22494
  async openLog() {
22399
22495
  try {
22400
22496
  await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true });
22401
- return (0, import_fs5.openSync)((0, import_path16.join)(this.opts.dataDir, "index.log"), "a");
22497
+ return (0, import_fs6.openSync)((0, import_path17.join)(this.opts.dataDir, "index.log"), "a");
22402
22498
  } catch {
22403
22499
  return null;
22404
22500
  }
22405
22501
  }
22406
- /** Install the index's native deps against THIS node on first start, so a
22407
- * platform needing node-gyp fails here rather than at query time. */
22408
- async installDeps() {
22502
+ /** Install the index's native deps against the node that will RUN it, so a
22503
+ * platform needing node-gyp fails here rather than at query time.
22504
+ *
22505
+ * The deps are compiled per ABI, so they are only reusable under the node
22506
+ * that built them. The stamp records which ABI is on disk and forces a
22507
+ * rebuild when the interpreter changes — otherwise a host that switches
22508
+ * node versions loads a binding built for the old one and dies with
22509
+ * "compiled against a different Node.js version". */
22510
+ async installDeps(nodeBin) {
22409
22511
  const dir = this.opts.indexDir;
22410
- if ((0, import_fs5.existsSync)((0, import_path16.join)(dir, "node_modules"))) return;
22512
+ const { stdout } = await execFileAsync4(nodeBin, [
22513
+ "-p",
22514
+ "process.versions.modules"
22515
+ ]);
22516
+ const abi = stdout.trim();
22517
+ const stamp = (0, import_path17.join)(dir, ".node-abi");
22518
+ if ((0, import_fs6.existsSync)((0, import_path17.join)(dir, "node_modules"))) {
22519
+ const built = await import_promises11.default.readFile(stamp, "utf-8").catch(() => "");
22520
+ if (built.trim() === abi) return;
22521
+ await import_promises11.default.rm((0, import_path17.join)(dir, "node_modules"), { recursive: true, force: true });
22522
+ }
22411
22523
  await import_promises11.default.copyFile(
22412
- (0, import_path16.join)(dir, "image-package.json"),
22413
- (0, import_path16.join)(dir, "package.json")
22524
+ (0, import_path17.join)(dir, "image-package.json"),
22525
+ (0, import_path17.join)(dir, "package.json")
22414
22526
  );
22415
22527
  await execFileAsync4(
22416
22528
  "npm",
22417
22529
  ["install", "--omit=dev", "--no-audit", "--no-fund"],
22418
- { cwd: dir }
22530
+ {
22531
+ cwd: dir,
22532
+ // npm resolves node-gyp's target from the node running it, so the
22533
+ // chosen interpreter has to lead PATH or the build targets the wrong ABI.
22534
+ env: {
22535
+ ...process.env,
22536
+ PATH: `${(0, import_path17.dirname)(nodeBin)}:${process.env.PATH ?? ""}`
22537
+ }
22538
+ }
22419
22539
  );
22540
+ await import_promises11.default.writeFile(stamp, `${abi}
22541
+ `, "utf-8").catch(() => {
22542
+ });
22420
22543
  }
22421
22544
  /** The port to listen on, kept stable across restarts. The URL goes into the
22422
22545
  * CLIs' persistent MCP config, so a fresh port each start would leave every
22423
22546
  * registration pointing at a dead one. */
22424
22547
  async stablePort() {
22425
- const file = (0, import_path16.join)(this.opts.dataDir, ".port");
22548
+ const file = (0, import_path17.join)(this.opts.dataDir, ".port");
22426
22549
  const saved = Number(await import_promises11.default.readFile(file, "utf-8").catch(() => ""));
22427
22550
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
22428
22551
  return saved;
@@ -22597,25 +22720,25 @@ async function pairWithBrowser(serverUrl) {
22597
22720
  }
22598
22721
 
22599
22722
  // src/config.ts
22600
- var import_fs9 = __toESM(require("fs"));
22601
- var import_os6 = __toESM(require("os"));
22602
- var import_path21 = __toESM(require("path"));
22723
+ var import_fs10 = __toESM(require("fs"));
22724
+ var import_os7 = __toESM(require("os"));
22725
+ var import_path22 = __toESM(require("path"));
22603
22726
 
22604
22727
  // src/secureFile.ts
22605
- var import_crypto9 = __toESM(require("crypto"));
22606
- var import_fs8 = __toESM(require("fs"));
22607
- var import_path20 = __toESM(require("path"));
22728
+ var import_crypto10 = __toESM(require("crypto"));
22729
+ var import_fs9 = __toESM(require("fs"));
22730
+ var import_path21 = __toESM(require("path"));
22608
22731
 
22609
22732
  // src/keychain.ts
22610
22733
  var import_child_process7 = require("child_process");
22611
- var import_crypto7 = __toESM(require("crypto"));
22612
- var import_fs6 = __toESM(require("fs"));
22613
- var import_os4 = __toESM(require("os"));
22614
- var import_path17 = __toESM(require("path"));
22734
+ var import_crypto8 = __toESM(require("crypto"));
22735
+ var import_fs7 = __toESM(require("fs"));
22736
+ var import_os5 = __toESM(require("os"));
22737
+ var import_path18 = __toESM(require("path"));
22615
22738
  var SERVICE = "factiii-runner";
22616
22739
  var ACCOUNT = "config-encryption-key";
22617
- var DPAPI_KEY_FILE = import_path17.default.join(
22618
- import_os4.default.homedir(),
22740
+ var DPAPI_KEY_FILE = import_path18.default.join(
22741
+ import_os5.default.homedir(),
22619
22742
  ".factiii-runner",
22620
22743
  "config-key.dpapi"
22621
22744
  );
@@ -22695,7 +22818,7 @@ function linuxWrite(key) {
22695
22818
  function winRead() {
22696
22819
  let wrapped;
22697
22820
  try {
22698
- wrapped = import_fs6.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
22821
+ wrapped = import_fs7.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
22699
22822
  } catch {
22700
22823
  return { state: "missing" };
22701
22824
  }
@@ -22720,8 +22843,8 @@ function winWrite(key) {
22720
22843
  "-Command",
22721
22844
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
22722
22845
  ]);
22723
- import_fs6.default.mkdirSync(import_path17.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22724
- import_fs6.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22846
+ import_fs7.default.mkdirSync(import_path18.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22847
+ import_fs7.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22725
22848
  `, { mode: 384 });
22726
22849
  }
22727
22850
  function keychainBackend() {
@@ -22860,7 +22983,7 @@ function resolve() {
22860
22983
  cached = { key: null, status: diagnose() };
22861
22984
  return cached;
22862
22985
  }
22863
- const fresh = import_crypto7.default.randomBytes(32);
22986
+ const fresh = import_crypto8.default.randomBytes(32);
22864
22987
  writeKey(fresh);
22865
22988
  const verify = readKey();
22866
22989
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -22882,23 +23005,23 @@ function keychainStatus() {
22882
23005
  }
22883
23006
 
22884
23007
  // src/secureStore.ts
22885
- var import_crypto8 = __toESM(require("crypto"));
22886
- var import_fs7 = __toESM(require("fs"));
22887
- var import_path19 = __toESM(require("path"));
23008
+ var import_crypto9 = __toESM(require("crypto"));
23009
+ var import_fs8 = __toESM(require("fs"));
23010
+ var import_path20 = __toESM(require("path"));
22888
23011
 
22889
23012
  // src/paths.ts
22890
- var import_os5 = __toESM(require("os"));
22891
- var import_path18 = __toESM(require("path"));
22892
- var CONFIG_DIR = import_path18.default.join(import_os5.default.homedir(), ".factiii-runner");
23013
+ var import_os6 = __toESM(require("os"));
23014
+ var import_path19 = __toESM(require("path"));
23015
+ var CONFIG_DIR = import_path19.default.join(import_os6.default.homedir(), ".factiii-runner");
22893
23016
  function safeSlug(spaceSlug) {
22894
23017
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
22895
23018
  }
22896
23019
  function spaceDirPath(spaceSlug) {
22897
- return import_path18.default.join(CONFIG_DIR, safeSlug(spaceSlug));
23020
+ return import_path19.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22898
23021
  }
22899
23022
 
22900
23023
  // src/secureStore.ts
22901
- var VAULT_KEY_FILE = import_path19.default.join(CONFIG_DIR, "vault-key.json");
23024
+ var VAULT_KEY_FILE = import_path20.default.join(CONFIG_DIR, "vault-key.json");
22902
23025
  var SCRYPT_N2 = 1 << 15;
22903
23026
  var MIN_PASSWORD_LENGTH = 8;
22904
23027
  var SecureStoreError = class extends Error {
@@ -22925,7 +23048,7 @@ function requireKeychainKey() {
22925
23048
  );
22926
23049
  }
22927
23050
  function passwordKey(password, salt) {
22928
- return import_crypto8.default.scryptSync(password, salt, 32, {
23051
+ return import_crypto9.default.scryptSync(password, salt, 32, {
22929
23052
  N: SCRYPT_N2,
22930
23053
  maxmem: 64 * 1024 * 1024
22931
23054
  });
@@ -22933,7 +23056,7 @@ function passwordKey(password, salt) {
22933
23056
  function wrapKey(machineKey, pwKey) {
22934
23057
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
22935
23058
  return Buffer.from(
22936
- import_crypto8.default.hkdfSync(
23059
+ import_crypto9.default.hkdfSync(
22937
23060
  "sha256",
22938
23061
  ikm,
22939
23062
  Buffer.alloc(0),
@@ -22943,8 +23066,8 @@ function wrapKey(machineKey, pwKey) {
22943
23066
  );
22944
23067
  }
22945
23068
  function seal(plaintext, key) {
22946
- const iv = import_crypto8.default.randomBytes(12);
22947
- const cipher = import_crypto8.default.createCipheriv("aes-256-gcm", key, iv);
23069
+ const iv = import_crypto9.default.randomBytes(12);
23070
+ const cipher = import_crypto9.default.createCipheriv("aes-256-gcm", key, iv);
22948
23071
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
22949
23072
  return {
22950
23073
  iv: iv.toString("base64"),
@@ -22954,7 +23077,7 @@ function seal(plaintext, key) {
22954
23077
  }
22955
23078
  function unseal(box, key) {
22956
23079
  try {
22957
- const decipher = import_crypto8.default.createDecipheriv(
23080
+ const decipher = import_crypto9.default.createDecipheriv(
22958
23081
  "aes-256-gcm",
22959
23082
  key,
22960
23083
  Buffer.from(box.iv, "base64")
@@ -22971,7 +23094,7 @@ function unseal(box, key) {
22971
23094
  function readVaultFile() {
22972
23095
  try {
22973
23096
  const parsed = JSON.parse(
22974
- import_fs7.default.readFileSync(VAULT_KEY_FILE, "utf-8")
23097
+ import_fs8.default.readFileSync(VAULT_KEY_FILE, "utf-8")
22975
23098
  );
22976
23099
  return parsed.v === 1 ? parsed : null;
22977
23100
  } catch {
@@ -22979,15 +23102,15 @@ function readVaultFile() {
22979
23102
  }
22980
23103
  }
22981
23104
  function writeVaultFile(file) {
22982
- import_fs7.default.mkdirSync(CONFIG_DIR, { recursive: true });
23105
+ import_fs8.default.mkdirSync(CONFIG_DIR, { recursive: true });
22983
23106
  const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
22984
- import_fs7.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
23107
+ import_fs8.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
22985
23108
  `, { mode: 384 });
22986
- import_fs7.default.renameSync(tmp, VAULT_KEY_FILE);
23109
+ import_fs8.default.renameSync(tmp, VAULT_KEY_FILE);
22987
23110
  }
22988
23111
  var unlockedDek = null;
22989
23112
  function createVault(machineKey) {
22990
- const dek = import_crypto8.default.randomBytes(32);
23113
+ const dek = import_crypto9.default.randomBytes(32);
22991
23114
  writeVaultFile({
22992
23115
  v: 1,
22993
23116
  passwordEnabled: false,
@@ -23082,7 +23205,7 @@ function enablePassword(password) {
23082
23205
  );
23083
23206
  }
23084
23207
  const dek = dataKey();
23085
- const salt = import_crypto8.default.randomBytes(16);
23208
+ const salt = import_crypto9.default.randomBytes(16);
23086
23209
  writeVaultFile({
23087
23210
  v: 1,
23088
23211
  passwordEnabled: true,
@@ -23110,7 +23233,7 @@ function changePassword(current, next) {
23110
23233
  "The current password is not correct."
23111
23234
  );
23112
23235
  }
23113
- const salt = import_crypto8.default.randomBytes(16);
23236
+ const salt = import_crypto9.default.randomBytes(16);
23114
23237
  writeVaultFile({
23115
23238
  v: 1,
23116
23239
  passwordEnabled: true,
@@ -23187,8 +23310,8 @@ function storeStatus() {
23187
23310
  // src/secureFile.ts
23188
23311
  var MAGIC = "FACTIII1";
23189
23312
  function encrypt(plain, key, version) {
23190
- const iv = import_crypto9.default.randomBytes(12);
23191
- const cipher = import_crypto9.default.createCipheriv("aes-256-gcm", key, iv);
23313
+ const iv = import_crypto10.default.randomBytes(12);
23314
+ const cipher = import_crypto10.default.createCipheriv("aes-256-gcm", key, iv);
23192
23315
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
23193
23316
  const envelope = {
23194
23317
  v: version,
@@ -23199,7 +23322,7 @@ function encrypt(plain, key, version) {
23199
23322
  return `${MAGIC}${JSON.stringify(envelope)}`;
23200
23323
  }
23201
23324
  function decrypt(envelope, key) {
23202
- const decipher = import_crypto9.default.createDecipheriv(
23325
+ const decipher = import_crypto10.default.createDecipheriv(
23203
23326
  "aes-256-gcm",
23204
23327
  key,
23205
23328
  Buffer.from(envelope.iv, "base64")
@@ -23220,15 +23343,15 @@ function parseEnvelope(raw) {
23220
23343
  }
23221
23344
  }
23222
23345
  function atomicWrite(filePath, body) {
23223
- import_fs8.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
23346
+ import_fs9.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
23224
23347
  const tmp = `${filePath}.${process.pid}.tmp`;
23225
- import_fs8.default.writeFileSync(tmp, body, { mode: 384 });
23226
- import_fs8.default.renameSync(tmp, filePath);
23348
+ import_fs9.default.writeFileSync(tmp, body, { mode: 384 });
23349
+ import_fs9.default.renameSync(tmp, filePath);
23227
23350
  }
23228
23351
  function readSecureBuffer(filePath) {
23229
23352
  let raw;
23230
23353
  try {
23231
- raw = import_fs8.default.readFileSync(filePath, "utf-8");
23354
+ raw = import_fs9.default.readFileSync(filePath, "utf-8");
23232
23355
  } catch {
23233
23356
  return null;
23234
23357
  }
@@ -23285,7 +23408,7 @@ function writeSecureJson(filePath, value2) {
23285
23408
  function readBootJson(filePath) {
23286
23409
  let raw;
23287
23410
  try {
23288
- raw = import_fs8.default.readFileSync(filePath, "utf-8");
23411
+ raw = import_fs9.default.readFileSync(filePath, "utf-8");
23289
23412
  } catch {
23290
23413
  return null;
23291
23414
  }
@@ -23335,8 +23458,8 @@ function writeBootJson(filePath, value2) {
23335
23458
  }
23336
23459
 
23337
23460
  // src/config.ts
23338
- var CONFIG_DIR2 = import_path21.default.join(import_os6.default.homedir(), ".factiii-runner");
23339
- var CONFIG_FILE = import_path21.default.join(CONFIG_DIR2, "config.json");
23461
+ var CONFIG_DIR2 = import_path22.default.join(import_os7.default.homedir(), ".factiii-runner");
23462
+ var CONFIG_FILE = import_path22.default.join(CONFIG_DIR2, "config.json");
23340
23463
  function readRunnerConfig() {
23341
23464
  return readBootJson(CONFIG_FILE);
23342
23465
  }
@@ -23344,7 +23467,7 @@ function writeRunnerConfig(config) {
23344
23467
  writeBootJson(CONFIG_FILE, config);
23345
23468
  }
23346
23469
  function configExists() {
23347
- return import_fs9.default.existsSync(CONFIG_FILE);
23470
+ return import_fs10.default.existsSync(CONFIG_FILE);
23348
23471
  }
23349
23472
 
23350
23473
  // src/promptPassword.ts
@@ -23437,7 +23560,54 @@ function currentVersion() {
23437
23560
  return "0.0.0";
23438
23561
  }
23439
23562
  }
23563
+ function hostSatisfiesTarget() {
23564
+ let engines = "";
23565
+ try {
23566
+ const res = (0, import_node_child_process.spawnSync)(
23567
+ "npm",
23568
+ ["view", `${PACKAGE_NAME}@latest`, "engines.node"],
23569
+ { encoding: "utf-8", shell: process.platform === "win32" }
23570
+ );
23571
+ if (res.status !== 0) return { ok: true, reason: "" };
23572
+ engines = (res.stdout ?? "").trim();
23573
+ } catch {
23574
+ return { ok: true, reason: "" };
23575
+ }
23576
+ if (!engines) return { ok: true, reason: "" };
23577
+ const major = Number(process.versions.node.split(".")[0]);
23578
+ if (Number.isNaN(major)) return { ok: true, reason: "" };
23579
+ const min = /(?:>=|\^|~)\s*v?(\d+)/.exec(engines);
23580
+ if (min && major < Number(min[1])) {
23581
+ return {
23582
+ ok: false,
23583
+ reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
23584
+ };
23585
+ }
23586
+ const lessThan = /<\s*v?(\d+)/.exec(engines);
23587
+ if (lessThan && major >= Number(lessThan[1])) {
23588
+ return {
23589
+ ok: false,
23590
+ reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
23591
+ };
23592
+ }
23593
+ const atMost = /<=\s*v?(\d+)/.exec(engines);
23594
+ if (atMost && major > Number(atMost[1])) {
23595
+ return {
23596
+ ok: false,
23597
+ reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
23598
+ };
23599
+ }
23600
+ return { ok: true, reason: "" };
23601
+ }
23440
23602
  function installLatest() {
23603
+ const fit = hostSatisfiesTarget();
23604
+ if (!fit.ok) {
23605
+ console.error(
23606
+ `[update] refusing to install ${PACKAGE_NAME}@latest: ${fit.reason}.
23607
+ [update] Staying on ${currentVersion()}. Install a supported Node (nvm install 24.11.1) and restart the runner to update.`
23608
+ );
23609
+ return false;
23610
+ }
23441
23611
  console.log(`[update] installing ${PACKAGE_NAME}@latest...`);
23442
23612
  const res = (0, import_node_child_process.spawnSync)("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
23443
23613
  stdio: "inherit",
@@ -23457,10 +23627,10 @@ function installLatest() {
23457
23627
  var import_node_child_process2 = require("node:child_process");
23458
23628
 
23459
23629
  // src/daemon.ts
23460
- var import_crypto10 = require("crypto");
23461
- var import_fs11 = __toESM(require("fs"));
23630
+ var import_crypto11 = require("crypto");
23631
+ var import_fs12 = __toESM(require("fs"));
23462
23632
  var import_node_datachannel = require("node-datachannel");
23463
- var import_path23 = __toESM(require("path"));
23633
+ var import_path24 = __toESM(require("path"));
23464
23634
 
23465
23635
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
23466
23636
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -26959,18 +27129,18 @@ Object.assign(lookup, {
26959
27129
  });
26960
27130
 
26961
27131
  // src/agent-adapter.ts
26962
- var import_fs10 = __toESM(require("fs"));
26963
- var import_path22 = __toESM(require("path"));
27132
+ var import_fs11 = __toESM(require("fs"));
27133
+ var import_path23 = __toESM(require("path"));
26964
27134
  function credsDir(spaceSlug) {
26965
- return import_path22.default.join(spaceDirPath(spaceSlug), ".creds");
27135
+ return import_path23.default.join(spaceDirPath(spaceSlug), ".creds");
26966
27136
  }
26967
27137
  function githubPath(spaceSlug) {
26968
- return import_path22.default.join(credsDir(spaceSlug), "github.json");
27138
+ return import_path23.default.join(credsDir(spaceSlug), "github.json");
26969
27139
  }
26970
27140
  function connectionPath(spaceSlug) {
26971
- return import_path22.default.join(credsDir(spaceSlug), "onedrive.json");
27141
+ return import_path23.default.join(credsDir(spaceSlug), "onedrive.json");
26972
27142
  }
26973
- var RUNNER_CONFIG_PATH = import_path22.default.join(CONFIG_DIR, "runner-config.json");
27143
+ var RUNNER_CONFIG_PATH = import_path23.default.join(CONFIG_DIR, "runner-config.json");
26974
27144
  function writeJson(filePath, value2) {
26975
27145
  writeSecureJson(filePath, value2);
26976
27146
  }
@@ -26986,14 +27156,14 @@ function fillDefaults(config) {
26986
27156
  }
26987
27157
  function configDirNames() {
26988
27158
  try {
26989
- return import_fs10.default.readdirSync(CONFIG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
27159
+ return import_fs11.default.readdirSync(CONFIG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
26990
27160
  } catch {
26991
27161
  return [];
26992
27162
  }
26993
27163
  }
26994
27164
  function provisionedSpaceSlugs() {
26995
27165
  return configDirNames().filter(
26996
- (name) => import_fs10.default.existsSync(import_path22.default.join(CONFIG_DIR, name, "bin"))
27166
+ (name) => import_fs11.default.existsSync(import_path23.default.join(CONFIG_DIR, name, "bin"))
26997
27167
  );
26998
27168
  }
26999
27169
  var migrated = false;
@@ -27007,7 +27177,7 @@ function migrateToSpaceFolders() {
27007
27177
  }
27008
27178
  }
27009
27179
  function readLegacy(filePath) {
27010
- if (!import_fs10.default.existsSync(filePath)) return { readable: true, value: null };
27180
+ if (!import_fs11.default.existsSync(filePath)) return { readable: true, value: null };
27011
27181
  const value2 = readSecureJson(filePath);
27012
27182
  return { readable: value2 !== null, value: value2 };
27013
27183
  }
@@ -27025,14 +27195,14 @@ function migrateOnce() {
27025
27195
  const consumed = [];
27026
27196
  let strayFiles = [];
27027
27197
  try {
27028
- strayFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
27198
+ strayFiles = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
27029
27199
  } catch {
27030
27200
  strayFiles = [];
27031
27201
  }
27032
27202
  for (const name of strayFiles) {
27033
27203
  const slug = name.slice("space-".length, -".json".length);
27034
27204
  const parsed = readLegacy(
27035
- import_path22.default.join(CONFIG_DIR, name)
27205
+ import_path23.default.join(CONFIG_DIR, name)
27036
27206
  );
27037
27207
  if (!parsed.readable) continue;
27038
27208
  if (parsed.value) strays[slug] = parsed.value;
@@ -27045,7 +27215,7 @@ function migrateOnce() {
27045
27215
  ...provisionedSpaceSlugs()
27046
27216
  ]);
27047
27217
  for (const slug of slugs) {
27048
- if (import_fs10.default.existsSync(githubPath(slug))) continue;
27218
+ if (import_fs11.default.existsSync(githubPath(slug))) continue;
27049
27219
  const over = boards[slug] ?? strays[slug] ?? {};
27050
27220
  if (!Object.keys(over).length && !Object.keys(base).length) continue;
27051
27221
  const merged = fillDefaults({
@@ -27056,32 +27226,32 @@ function migrateOnce() {
27056
27226
  githubToken: over.githubToken || base.githubToken,
27057
27227
  provider: over.provider || base.provider
27058
27228
  });
27059
- import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27229
+ import_fs11.default.mkdirSync(credsDir(slug), { recursive: true });
27060
27230
  writeJson(githubPath(slug), merged);
27061
27231
  }
27062
27232
  let odFiles = [];
27063
27233
  try {
27064
- odFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".onedrive.json"));
27234
+ odFiles = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".onedrive.json"));
27065
27235
  } catch {
27066
27236
  odFiles = [];
27067
27237
  }
27068
27238
  for (const name of odFiles) {
27069
27239
  const slug = name.slice("space-".length, -".onedrive.json".length);
27070
- if (import_fs10.default.existsSync(connectionPath(slug))) {
27240
+ if (import_fs11.default.existsSync(connectionPath(slug))) {
27071
27241
  consumed.push(name);
27072
27242
  continue;
27073
27243
  }
27074
- const conn = readLegacy(import_path22.default.join(CONFIG_DIR, name));
27244
+ const conn = readLegacy(import_path23.default.join(CONFIG_DIR, name));
27075
27245
  if (!conn.readable) continue;
27076
27246
  if (conn.value) {
27077
- import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27247
+ import_fs11.default.mkdirSync(credsDir(slug), { recursive: true });
27078
27248
  writeJson(connectionPath(slug), conn.value);
27079
27249
  }
27080
27250
  consumed.push(name);
27081
27251
  }
27082
27252
  for (const name of [...consumed, "runner-config.json"]) {
27083
27253
  try {
27084
- import_fs10.default.rmSync(import_path22.default.join(CONFIG_DIR, name), { force: true });
27254
+ import_fs11.default.rmSync(import_path23.default.join(CONFIG_DIR, name), { force: true });
27085
27255
  } catch {
27086
27256
  }
27087
27257
  }
@@ -27093,14 +27263,14 @@ function encryptStrayPlaintextConfigs() {
27093
27263
  const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
27094
27264
  let names = [];
27095
27265
  try {
27096
- names = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
27266
+ names = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
27097
27267
  } catch {
27098
27268
  return;
27099
27269
  }
27100
27270
  for (const name of names) {
27101
- const filePath = import_path22.default.join(CONFIG_DIR, name);
27271
+ const filePath = import_path23.default.join(CONFIG_DIR, name);
27102
27272
  try {
27103
- const raw = import_fs10.default.readFileSync(filePath, "utf-8");
27273
+ const raw = import_fs11.default.readFileSync(filePath, "utf-8");
27104
27274
  if (!raw.startsWith("{")) continue;
27105
27275
  writeSecureJson(filePath, JSON.parse(raw));
27106
27276
  } catch {
@@ -27123,7 +27293,7 @@ async function getConfiguredConnections() {
27123
27293
  for (const slug of listConfiguredSlugs()) {
27124
27294
  if (kinds.has("git") && kinds.has("onedrive")) break;
27125
27295
  if (readBoard(slug).githubToken) kinds.add("git");
27126
- if (import_fs10.default.existsSync(connectionPath(slug))) kinds.add("onedrive");
27296
+ if (import_fs11.default.existsSync(connectionPath(slug))) kinds.add("onedrive");
27127
27297
  }
27128
27298
  } catch {
27129
27299
  return [];
@@ -27135,7 +27305,7 @@ function listConfiguredSlugs() {
27135
27305
  try {
27136
27306
  migrateToSpaceFolders();
27137
27307
  return configDirNames().filter(
27138
- (slug) => import_fs10.default.existsSync(githubPath(slug)) || import_fs10.default.existsSync(connectionPath(slug))
27308
+ (slug) => import_fs11.default.existsSync(githubPath(slug)) || import_fs11.default.existsSync(connectionPath(slug))
27139
27309
  );
27140
27310
  } catch {
27141
27311
  return [];
@@ -27154,7 +27324,7 @@ var localConfigProvider = {
27154
27324
  // which is what makes disconnect stick.
27155
27325
  writeConfig(spaceSlug, config) {
27156
27326
  migrateToSpaceFolders();
27157
- import_fs10.default.mkdirSync(credsDir(spaceSlug), { recursive: true });
27327
+ import_fs11.default.mkdirSync(credsDir(spaceSlug), { recursive: true });
27158
27328
  writeJson(githubPath(spaceSlug), config);
27159
27329
  },
27160
27330
  readOneDriveConnection(spaceSlug) {
@@ -27168,7 +27338,7 @@ var localConfigProvider = {
27168
27338
  writeOneDriveConnection(spaceSlug, connection) {
27169
27339
  const filePath = connectionPath(spaceSlug);
27170
27340
  if (!connection) {
27171
- import_fs10.default.rmSync(filePath, { force: true });
27341
+ import_fs11.default.rmSync(filePath, { force: true });
27172
27342
  return;
27173
27343
  }
27174
27344
  writeJson(filePath, connection);
@@ -27193,7 +27363,7 @@ var localConfigProvider = {
27193
27363
  "claude",
27194
27364
  "codex"
27195
27365
  ]) {
27196
- import_fs10.default.mkdirSync(import_path22.default.join(dir, sub), { recursive: true });
27366
+ import_fs11.default.mkdirSync(import_path23.default.join(dir, sub), { recursive: true });
27197
27367
  }
27198
27368
  return dir;
27199
27369
  }
@@ -27367,15 +27537,15 @@ async function startDaemon(config) {
27367
27537
  }
27368
27538
  );
27369
27539
  const indexCandidates = [
27370
- import_path23.default.join(__dirname, "..", "index"),
27371
- import_path23.default.join(__dirname, "..", "..", "index", "image")
27540
+ import_path24.default.join(__dirname, "..", "index"),
27541
+ import_path24.default.join(__dirname, "..", "..", "index", "image")
27372
27542
  ];
27373
- const indexDir = indexCandidates.find((p) => import_fs11.default.existsSync(p)) ?? indexCandidates[0];
27543
+ const indexDir = indexCandidates.find((p) => import_fs12.default.existsSync(p)) ?? indexCandidates[0];
27374
27544
  const odIndex = new IndexService({
27375
- controlToken: (0, import_crypto10.createHash)("sha256").update(config.authToken).digest("hex"),
27545
+ controlToken: (0, import_crypto11.createHash)("sha256").update(config.authToken).digest("hex"),
27376
27546
  embedUrl: () => config.serverUrl,
27377
27547
  indexDir,
27378
- dataDir: import_path23.default.join(CONFIG_DIR, "index-data")
27548
+ dataDir: import_path24.default.join(CONFIG_DIR, "index-data")
27379
27549
  });
27380
27550
  void odIndex.ensureRunning().catch((err) => {
27381
27551
  console.error(
@@ -27472,7 +27642,9 @@ async function startDaemon(config) {
27472
27642
  // Board MCP is server-hosted, so the base URL is all it needs.
27473
27643
  serverUrl: config.serverUrl,
27474
27644
  // Point the agent at the index MCP endpoint (lazy for the port).
27475
- mcp: () => odIndex.mcpBridge(spaceSlug)
27645
+ mcp: () => odIndex.mcpBridge(spaceSlug),
27646
+ // The bearer alone, available before the service has a port for a URL.
27647
+ mcpToken: () => odIndex.bearerToken(spaceSlug)
27476
27648
  });
27477
27649
  engines.set(spaceSlug, engine);
27478
27650
  void engine.reattachAndHydrate();
@@ -27989,27 +28161,24 @@ ${err.fix}`);
27989
28161
  console.error("");
27990
28162
  process.exit(1);
27991
28163
  }
27992
- var MAX_SUPPORTED_NODE_MAJOR = 24;
27993
- function assertSupportedNode() {
27994
- const major = Number(process.versions.node.split(".")[0]);
27995
- if (Number.isNaN(major) || major <= MAX_SUPPORTED_NODE_MAJOR) return;
27996
- console.error(
28164
+ function warnUnsupportedNode() {
28165
+ if (indexNodeSupported()) return;
28166
+ const alternative = findIndexNode();
28167
+ console.warn(
27997
28168
  `
27998
- factiii-runner needs Node 20\u2013${MAX_SUPPORTED_NODE_MAJOR} (pinned 24.11.1); you are on Node ${process.versions.node}.
27999
- The bundled OneDrive index compiles better-sqlite3 from source, and that build fails on Node 25+.
28000
-
28001
- Switch with nvm: nvm install 24.11.1 && nvm use 24.11.1
28002
- then relaunch: factiii-runner start
28003
- `
28169
+ Node ${process.versions.node} is outside the range the OneDrive index supports (${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR}, pinned 24.11.1).` + (alternative ? `
28170
+ The index will run under ${alternative}. Everything else is unaffected.
28171
+ ` : `
28172
+ OneDrive search will be unavailable until a supported Node is installed (nvm install 24.11.1). Everything else works.
28173
+ `)
28004
28174
  );
28005
- process.exit(1);
28006
28175
  }
28007
28176
  import_node_dns.default.setDefaultResultOrder("ipv4first");
28008
28177
  var program2 = new Command();
28009
28178
  program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
28010
28179
  program2.command("setup").description("Verify the host toolchain and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
28011
28180
  try {
28012
- assertSupportedNode();
28181
+ warnUnsupportedNode();
28013
28182
  await setup(opts.server);
28014
28183
  } catch (err) {
28015
28184
  console.error(
@@ -28038,7 +28207,7 @@ program2.command("connect").description("Pair this runner with a Factiii account
28038
28207
  }
28039
28208
  });
28040
28209
  program2.command("start").description("Start the runner daemon").action(async () => {
28041
- assertSupportedNode();
28210
+ warnUnsupportedNode();
28042
28211
  if (!configExists()) {
28043
28212
  console.error('No config found. Run "factiii-runner setup" first.');
28044
28213
  process.exit(1);
@@ -28084,8 +28253,7 @@ program2.command("status").description("Show current runner configuration").acti
28084
28253
  console.log(`Token: \xB7\xB7\xB7\xB7${config.authToken.slice(-4)}`);
28085
28254
  });
28086
28255
  program2.command("doctor").description("Show where each host tool resolves, for a failed environment").action(async () => {
28087
- const nodeMajor = Number(process.versions.node.split(".")[0]);
28088
- const nodeNote = nodeMajor > MAX_SUPPORTED_NODE_MAJOR ? " (UNSUPPORTED \u2014 use 24.11.1)" : "";
28256
+ const nodeNote = indexNodeSupported() ? "" : ` (OneDrive index needs ${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR}; ${findIndexNode() ?? "no supported node found"})`;
28089
28257
  console.log(`${"node".padEnd(14)} ${process.execPath} v${process.versions.node}${nodeNote}`);
28090
28258
  const report = await reportToolchain(hostTarget(CONFIG_DIR));
28091
28259
  for (const tool of report.tools) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factiii/runner",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Factiii Runner, run Board AI agents on a machine you control. Pairs with the Factiii web/mobile clients over WebRTC.",
5
5
  "license": "ISC",
6
6
  "keywords": [