@tpsdev-ai/flair 0.29.0 → 0.30.0

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 (85) hide show
  1. package/dist/cli-shim.cjs +1 -1
  2. package/dist/cli.js +231 -104
  3. package/dist/deploy.js +29 -19
  4. package/dist/fabric-upgrade.js +61 -23
  5. package/dist/fleet-verify.js +3 -3
  6. package/dist/lib/mcp-enable.js +2 -2
  7. package/dist/rem/scheduler.js +226 -17
  8. package/dist/resources/A2AAdapter.js +1 -1
  9. package/dist/resources/Admin.js +1 -1
  10. package/dist/resources/AdminConnectors.js +1 -1
  11. package/dist/resources/AdminDashboard.js +1 -1
  12. package/dist/resources/AdminIdp.js +1 -1
  13. package/dist/resources/AdminInstance.js +1 -1
  14. package/dist/resources/AdminMemory.js +1 -1
  15. package/dist/resources/AdminPrincipals.js +1 -1
  16. package/dist/resources/Agent.js +1 -1
  17. package/dist/resources/AgentCard.js +1 -1
  18. package/dist/resources/AgentSeed.js +1 -1
  19. package/dist/resources/AttentionQuery.js +1 -1
  20. package/dist/resources/Credential.js +1 -1
  21. package/dist/resources/Federation.js +1 -1
  22. package/dist/resources/Instance.js +1 -1
  23. package/dist/resources/Integration.js +1 -1
  24. package/dist/resources/MCPClientMetadata.js +1 -1
  25. package/dist/resources/Memory.js +1 -1
  26. package/dist/resources/MemoryBootstrap.js +1 -1
  27. package/dist/resources/MemoryCandidate.js +168 -0
  28. package/dist/resources/MemoryConsolidate.js +1 -1
  29. package/dist/resources/MemoryDedupStats.js +1 -1
  30. package/dist/resources/MemoryFeed.js +1 -1
  31. package/dist/resources/MemoryGrant.js +1 -1
  32. package/dist/resources/MemoryMaintenance.js +1 -1
  33. package/dist/resources/MemoryReflect.js +2 -2
  34. package/dist/resources/MemoryReindex.js +1 -1
  35. package/dist/resources/MemoryUsage.js +1 -1
  36. package/dist/resources/OAuth.js +1 -1
  37. package/dist/resources/OAuthClient.js +1 -1
  38. package/dist/resources/OrgEvent.js +1 -1
  39. package/dist/resources/OrgEventCatchup.js +1 -1
  40. package/dist/resources/OrgEventMaintenance.js +1 -1
  41. package/dist/resources/PairingToken.js +1 -1
  42. package/dist/resources/Peer.js +1 -1
  43. package/dist/resources/Presence.js +3 -3
  44. package/dist/resources/RecordUsage.js +1 -1
  45. package/dist/resources/Relationship.js +1 -1
  46. package/dist/resources/SemanticSearch.js +1 -1
  47. package/dist/resources/SkillScan.js +1 -1
  48. package/dist/resources/Soul.js +1 -1
  49. package/dist/resources/SoulFeed.js +1 -1
  50. package/dist/resources/WorkspaceLatest.js +1 -1
  51. package/dist/resources/WorkspaceState.js +1 -1
  52. package/dist/resources/XAA.js +1 -1
  53. package/dist/resources/a2a-url.js +1 -1
  54. package/dist/resources/agent-auth.js +1 -1
  55. package/dist/resources/agentcard-fields.js +1 -1
  56. package/dist/resources/auth-middleware.js +1 -1
  57. package/dist/resources/embeddings-boot.js +4 -4
  58. package/dist/resources/embeddings-provider.js +5 -5
  59. package/dist/resources/federation-cleanup.js +5 -5
  60. package/dist/resources/federation-nonce-store.js +5 -5
  61. package/dist/resources/health.js +29 -6
  62. package/dist/resources/instance-identity.js +1 -1
  63. package/dist/resources/mcp-client-metadata-fields.js +1 -1
  64. package/dist/resources/mcp-handler.js +1 -1
  65. package/dist/resources/mcp-oauth.js +2 -2
  66. package/dist/resources/memory-read-scope.js +1 -1
  67. package/dist/resources/memory-reflect-lib.js +3 -3
  68. package/dist/resources/memory-visibility.js +3 -3
  69. package/dist/resources/migration-boot.js +1 -1
  70. package/dist/resources/migrations/embedding-stamp.js +3 -3
  71. package/dist/resources/migrations/graph-heal.js +2 -2
  72. package/dist/resources/migrations/ledger.js +1 -1
  73. package/dist/resources/migrations/registry.js +14 -5
  74. package/dist/resources/migrations/runner.js +64 -11
  75. package/dist/resources/migrations/synthetic-test-migration.js +1 -1
  76. package/dist/resources/migrations/visibility-backfill.js +232 -0
  77. package/dist/resources/record-types.js +28 -0
  78. package/dist/resources/semantic-retrieval-core.js +1 -1
  79. package/dist/resources/usage-recording.js +1 -1
  80. package/dist/src/rem/scheduler.js +449 -0
  81. package/docs/deployment.md +1 -0
  82. package/docs/supply-chain-policy.md +4 -4
  83. package/docs/upgrade.md +2 -2
  84. package/package.json +4 -4
  85. package/schemas/memory.graphql +9 -1
package/dist/cli-shim.cjs CHANGED
@@ -9,7 +9,7 @@
9
9
  * The real CLI (dist/cli.js) is an ES module. In ESM, every top-level
10
10
  * `import` is HOISTED and the whole module graph is LINKED + EVALUATED before
11
11
  * the first statement in the file body runs. Flair's deps (harper-fabric-
12
- * embeddings requires Node >=22, @harperfast/harper / commander require >=20)
12
+ * embeddings requires Node >=22, harper / commander require >=20)
13
13
  * fail to load on an older engine — so a Node-version check placed even at the
14
14
  * very top of cli.ts never executes: the import graph crashes first. That is
15
15
  * exactly the silent-onboarding-failure bug (a Harper dev got zero output and
package/dist/cli.js CHANGED
@@ -261,6 +261,40 @@ function readPortFromConfig() {
261
261
  catch { /* ignore */ }
262
262
  return null;
263
263
  }
264
+ /**
265
+ * Read the persisted ops-API bind host from ~/.flair/config.yaml (flair#863).
266
+ * Line-anchored so it can't be satisfied by some other key that merely ends
267
+ * in `opsBind`, and quote-tolerant because a hand-edited config may quote it.
268
+ *
269
+ * `path` is injected the same way launchdPlistPath's `launchAgentsDir` is —
270
+ * so tests exercise the real parse against a temp file instead of the
271
+ * developer's own ~/.flair.
272
+ */
273
+ function readOpsBindFromConfig(path = configPath()) {
274
+ try {
275
+ if (existsSync(path)) {
276
+ const yaml = readFileSync(path, "utf-8");
277
+ const m = yaml.match(/^\s*opsBind:\s*["']?([^"'\s#]+)["']?/m);
278
+ if (m && m[1])
279
+ return m[1];
280
+ }
281
+ }
282
+ catch { /* ignore */ }
283
+ return null;
284
+ }
285
+ /** Read the persisted ops-API port from ~/.flair/config.yaml (flair#863). */
286
+ function readOpsPortFromConfig(path = configPath()) {
287
+ try {
288
+ if (existsSync(path)) {
289
+ const yaml = readFileSync(path, "utf-8");
290
+ const m = yaml.match(/^\s*opsPort:\s*(\d+)/m);
291
+ if (m)
292
+ return Number(m[1]);
293
+ }
294
+ }
295
+ catch { /* ignore */ }
296
+ return null;
297
+ }
264
298
  // Unified port resolution: --port flag > FLAIR_URL env > config file > default
265
299
  // Every command that talks to Harper MUST use these helpers.
266
300
  function resolveHttpPort(opts) {
@@ -320,29 +354,60 @@ function resolveOpsPort(opts) {
320
354
  // Default: httpPort - 1
321
355
  return resolveHttpPort(opts) - 1;
322
356
  }
323
- // Ops API bind-host resolution (flair#670): --ops-bind flag > FLAIR_OPS_BIND
324
- // env > loopback default. This is the escape hatch — deployments that
325
- // genuinely need remote ops access (multi-host / Fabric) pass an explicit
326
- // wider address (e.g. `--ops-bind 0.0.0.0`) to opt back in; everything else
327
- // gets the loopback-only single-host default. FLAIR_OPS_BIND (not just the
328
- // flag) matters for `flair start`'s non-launchd fallback spawn, which has no
329
- // --ops-bind flag of its own — see the comment at its OPERATIONSAPI_NETWORK_PORT
330
- // assignment for why it re-resolves this on every start instead of trusting
331
- // the persisted config alone.
332
- function resolveOpsBindHost(opts) {
333
- if (opts.opsBind !== undefined && opts.opsBind !== null && String(opts.opsBind).trim() !== "") {
334
- return String(opts.opsBind).trim();
335
- }
336
- const envBind = process.env.FLAIR_OPS_BIND;
357
+ // Ops API bind-host resolution (flair#670, #863): --ops-bind flag >
358
+ // FLAIR_OPS_BIND env > persisted `opsBind` in ~/.flair/config.yaml > loopback
359
+ // default. This is the escape hatch deployments that genuinely need remote
360
+ // ops access (multi-host / Fabric) pass an explicit wider address (e.g.
361
+ // `--ops-bind 0.0.0.0`) to opt back in; everything else gets the loopback-only
362
+ // single-host default.
363
+ //
364
+ // The config-file rung (flair#863) is what makes `--ops-bind` durable. Every
365
+ // Harper spawn re-asserts the bind (see opsNetworkPortValue's doc comment for
366
+ // why it MUST), and those spawns happen in `flair start` / `flair restart` /
367
+ // `flair upgrade`, none of which take an `--ops-bind` flag. Without a
368
+ // flair-owned persisted value, a one-off `flair init --ops-bind 0.0.0.0` would
369
+ // be silently reverted to the loopback default by the very next restart — the
370
+ // widening case would be as broken as the narrowing one. Mirrors
371
+ // resolveOpsPort's config rung.
372
+ export function resolveOpsBindHostFrom(flag, envBind, configuredBind) {
373
+ if (flag !== undefined && flag !== null && String(flag).trim() !== "")
374
+ return String(flag).trim();
337
375
  if (envBind && envBind.trim() !== "")
338
376
  return envBind.trim();
377
+ if (configuredBind && configuredBind.trim() !== "")
378
+ return configuredBind.trim();
339
379
  return DEFAULT_OPS_BIND_HOST;
340
380
  }
381
+ function resolveOpsBindHost(opts) {
382
+ return resolveOpsBindHostFrom(opts.opsBind, process.env.FLAIR_OPS_BIND, readOpsBindFromConfig());
383
+ }
384
+ /**
385
+ * The one place that renders Harper's `operationsApi.network.port` value
386
+ * (flair#863). Used by both the HARPER_SET_CONFIG block
387
+ * (buildOperationsApiConfig) and every `OPERATIONSAPI_NETWORK_PORT` env var
388
+ * flair sets, so the two can never disagree about whether the bind host is
389
+ * present.
390
+ *
391
+ * Why every spawn has to carry the host-qualified form, not a bare number:
392
+ * Harper's HARPER_SET_CONFIG handling (harper
393
+ * config/harperConfigEnvVars.ts) records, for each key it force-sets, the
394
+ * value that key had BEFORE the force — `state.originalValues` in
395
+ * `<rootPath>/backup/.harper-config-state.json` — and on the next boot where
396
+ * HARPER_SET_CONFIG is absent, `cleanupRemovedEnvVar` RESTORES those
397
+ * originals into harper-config.yaml. So a bare `OPERATIONSAPI_NETWORK_PORT`
398
+ * anywhere in flair's spawn chain doesn't just lose on that boot: it is
399
+ * latched as the "original" and silently re-widens the bind to all interfaces
400
+ * on the first later boot that omits HARPER_SET_CONFIG. That is exactly what
401
+ * `flair restart` / `flair upgrade` do on the non-launchd path.
402
+ */
403
+ export function opsNetworkPortValue(opsBindHost, opsPort) {
404
+ return `${opsBindHost}:${opsPort}`;
405
+ }
341
406
  /**
342
407
  * Build the `operationsApi` block for Harper's HARPER_SET_CONFIG (flair#670).
343
408
  *
344
409
  * `network.port` uses Harper's "host:port" string form — Harper's server
345
- * bootstrap (@harperfast/harper dist/server/threads/threadServer.js,
410
+ * bootstrap (harper dist/server/threads/threadServer.js,
346
411
  * listenOnPorts/listenOnPortsBun) splits a config port value on its last
347
412
  * `:` into an explicit bind host + port when present, and falls back to
348
413
  * binding all interfaces (0.0.0.0 / ::) when given a bare number. A colon-free
@@ -350,15 +415,48 @@ function resolveOpsBindHost(opts) {
350
415
  * it with a host is the only config-level way to narrow the bind.
351
416
  *
352
417
  * `domainSocket` lives under `network` per Harper's own config schema
353
- * (@harperfast/harper/config-root.schema.json → properties.operationsApi
418
+ * (harper/config-root.schema.json → properties.operationsApi
354
419
  * .properties.network.properties.domainSocket, and
355
420
  * dist/validation/configValidator.js's `operationsApi.network.domainSocket`
356
421
  * Joi path) — nested here, not as a sibling of `network`.
357
422
  */
358
423
  export function buildOperationsApiConfig(opsPort, opsSocket, opsBindHost) {
359
424
  return {
360
- network: { port: `${opsBindHost}:${opsPort}`, cors: true, domainSocket: opsSocket },
425
+ network: { port: opsNetworkPortValue(opsBindHost, opsPort), cors: true, domainSocket: opsSocket },
426
+ };
427
+ }
428
+ /**
429
+ * Build the flair-owned environment overrides for a DIRECT (non-launchd)
430
+ * Harper spawn (flair#863) — shared by `flair start`'s fallback path and
431
+ * startFlairProcess() (which backs `flair restart` and `flair upgrade`).
432
+ * Callers spread this over `process.env`.
433
+ *
434
+ * These two sites used to build the env inline and had drifted: `start` set a
435
+ * host-qualified OPERATIONSAPI_NETWORK_PORT, `startFlairProcess` set none at
436
+ * all. Neither path sets HARPER_SET_CONFIG, and Harper restores the
437
+ * pre-SET_CONFIG original for every key SET_CONFIG had forced whenever that
438
+ * variable is absent — so the site that omitted the var silently re-widened
439
+ * the ops API to all interfaces on every restart/upgrade, and persisted it.
440
+ * One builder means the next spawn site cannot reintroduce that gap.
441
+ *
442
+ * Deliberately omits HDB_ADMIN_PASSWORD when no password is in hand: an empty
443
+ * string would strip Harper's auth on an existing install.
444
+ */
445
+ export function buildDirectSpawnEnv(opts) {
446
+ const env = {
447
+ ROOTPATH: opts.dataDir,
448
+ // The embedding backend self-registers in-process at boot
449
+ // (resources/embeddings-boot.ts); this only tells it where the model lives.
450
+ FLAIR_MODELS_DIR: opts.modelsDir,
451
+ DEFAULTS_MODE: "dev",
452
+ HDB_ADMIN_USERNAME: opts.adminUser,
453
+ HTTP_PORT: String(opts.httpPort),
454
+ OPERATIONSAPI_NETWORK_PORT: opsNetworkPortValue(opts.opsBindHost, opts.opsPort),
455
+ LOCAL_STUDIO: "false",
361
456
  };
457
+ if (opts.adminPass)
458
+ env.HDB_ADMIN_PASSWORD = opts.adminPass;
459
+ return env;
362
460
  }
363
461
  /**
364
462
  * Decide whether a persisted `operationsApi.network.port` value (read back
@@ -693,10 +791,26 @@ function resolveOpsUrlFromTarget(targetUrl) {
693
791
  }
694
792
  return url.toString().replace(/\/$/, "");
695
793
  }
696
- function writeConfig(port) {
697
- const p = configPath();
698
- mkdirSync(join(homedir(), ".flair"), { recursive: true });
699
- writeFileSync(p, `# Flair configuration\nport: ${port}\n`);
794
+ /**
795
+ * Persist the instance coordinates other commands need to find and re-assert
796
+ * this install (flair#863).
797
+ *
798
+ * This rewrites the file wholesale, so `opsPort`/`opsBind` default to whatever
799
+ * is already persisted rather than being dropped — otherwise an unrelated
800
+ * caller (e.g. `flair doctor --fix` correcting a drifted HTTP port) would
801
+ * silently erase the operator's `--ops-bind` choice and the next restart would
802
+ * revert the bind.
803
+ */
804
+ function writeConfig(port, opsPort, opsBind, path = configPath()) {
805
+ const resolvedOpsPort = opsPort ?? readOpsPortFromConfig(path);
806
+ const resolvedOpsBind = opsBind ?? readOpsBindFromConfig(path);
807
+ mkdirSync(dirname(path), { recursive: true });
808
+ let body = `# Flair configuration\nport: ${port}\n`;
809
+ if (resolvedOpsPort !== null && resolvedOpsPort !== undefined)
810
+ body += `opsPort: ${resolvedOpsPort}\n`;
811
+ if (resolvedOpsBind)
812
+ body += `opsBind: ${resolvedOpsBind}\n`;
813
+ writeFileSync(path, body);
700
814
  }
701
815
  function privKeyPath(agentId, keysDir) {
702
816
  return join(keysDir, `${agentId}.key`);
@@ -709,11 +823,25 @@ function flairPackageDir() {
709
823
  return join(import.meta.dirname ?? __dirname, "..");
710
824
  }
711
825
  function harperBin() {
712
- // Resolve relative to this file's location (dist/cli.js → ../node_modules/...)
713
- const candidates = [
714
- join(import.meta.dirname ?? __dirname, "..", "node_modules", "@harperfast", "harper", "dist", "bin", "harper.js"),
715
- join(process.cwd(), "node_modules", "@harperfast", "harper", "dist", "bin", "harper.js"),
826
+ // Resolve relative to this file's location (dist/cli.js → ../node_modules/...).
827
+ //
828
+ // flair#870: flair depends on the BARE `harper` package name. The scoped
829
+ // `@harperfast/harper` segments are kept as a fallback so an in-place upgrade
830
+ // over a pre-#870 install (whose node_modules still holds the scoped copy,
831
+ // and whose Harper is the one currently serving the data dir) keeps booting
832
+ // until the next clean install. Bare name is tried first so a tree that has
833
+ // BOTH resolves to the one flair actually declares.
834
+ const roots = [
835
+ join(import.meta.dirname ?? __dirname, ".."),
836
+ process.cwd(),
716
837
  ];
838
+ const pkgSegments = [["harper"], ["@harperfast", "harper"]];
839
+ const candidates = [];
840
+ for (const root of roots) {
841
+ for (const segs of pkgSegments) {
842
+ candidates.push(join(root, "node_modules", ...segs, "dist", "bin", "harper.js"));
843
+ }
844
+ }
717
845
  for (const c of candidates)
718
846
  if (existsSync(c))
719
847
  return c;
@@ -2302,8 +2430,13 @@ program
2302
2430
  catch { /* not running */ }
2303
2431
  if (!alreadyRunning) {
2304
2432
  const bin = harperBin();
2305
- if (!bin)
2306
- throw new Error("@harperfast/harper not found in node_modules.\nRun: npm install @harperfast/harper");
2433
+ if (!bin) {
2434
+ throw new Error("Harper CLI not found: no dist/bin/harper.js under node_modules/harper " +
2435
+ "(or the legacy node_modules/@harperfast/harper) next to this flair " +
2436
+ `install or in ${process.cwd()}.\n` +
2437
+ "Flair ships Harper as a dependency, so this normally means a partial " +
2438
+ "or interrupted install.\nFix: reinstall flair — npm install -g @tpsdev-ai/flair");
2439
+ }
2307
2440
  mkdirSync(dataDir, { recursive: true });
2308
2441
  // Detect whether Harper has already been installed in this data dir.
2309
2442
  // harper-config.yaml is created during install — its presence means
@@ -2338,7 +2471,12 @@ program
2338
2471
  THREADS_COUNT: "1",
2339
2472
  NODE_HOSTNAME: "localhost",
2340
2473
  HTTP_PORT: String(httpPort),
2341
- OPERATIONSAPI_NETWORK_PORT: String(opsPort),
2474
+ // flair#863: host-qualified, NOT a bare port. A bare value here is
2475
+ // what Harper latches as `originalValues["operationsApi.network.port"]`
2476
+ // when HARPER_SET_CONFIG force-sets the same key — and restores on the
2477
+ // first later boot without HARPER_SET_CONFIG, re-widening the bind to
2478
+ // all interfaces. See opsNetworkPortValue's doc comment.
2479
+ OPERATIONSAPI_NETWORK_PORT: opsNetworkPortValue(opsBindHost, opsPort),
2342
2480
  LOCAL_STUDIO: "false",
2343
2481
  };
2344
2482
  // models (flair#504 Phase 1): the embedding backend registers itself
@@ -2478,7 +2616,7 @@ program
2478
2616
  <key>THREADS_COUNT</key><string>1</string>
2479
2617
  <key>NODE_HOSTNAME</key><string>localhost</string>
2480
2618
  <key>HTTP_PORT</key><string>${httpPort}</string>
2481
- <key>OPERATIONSAPI_NETWORK_PORT</key><string>${opsPort}</string>
2619
+ <key>OPERATIONSAPI_NETWORK_PORT</key><string>${escapeXml(opsNetworkPortValue(opsBindHost, opsPort))}</string>
2482
2620
  <key>LOCAL_STUDIO</key><string>false</string>
2483
2621
  </dict>
2484
2622
  <key>RunAtLoad</key><true/>
@@ -2492,8 +2630,15 @@ program
2492
2630
  }
2493
2631
  }
2494
2632
  }
2495
- // Persist port to config so other commands can find this instance
2496
- writeConfig(httpPort);
2633
+ // Persist the instance coordinates so other commands can find AND
2634
+ // re-assert this instance. flair#863: `opsBind` in particular has to be
2635
+ // persisted here, not just handed to this run's spawn — `flair start` /
2636
+ // `flair restart` / `flair upgrade` re-assert the bind on every spawn and
2637
+ // have no `--ops-bind` flag of their own, so this is the only thing that
2638
+ // survives an `--ops-bind` choice past the next restart. It also runs on
2639
+ // the already-running path (where init skips the spawn entirely), which is
2640
+ // what makes doctor's `flair init && flair restart` remedy actually apply.
2641
+ writeConfig(httpPort, opsPort, opsBindHost);
2497
2642
  if (agentId) {
2498
2643
  // Generate or reuse keypair
2499
2644
  mkdirSync(keysDir, { recursive: true });
@@ -6423,27 +6568,17 @@ remNightly
6423
6568
  const minute = parseInt(match[2], 10);
6424
6569
  const port = readPortFromConfig() ?? DEFAULT_PORT;
6425
6570
  const flairUrl = opts.flairUrl || process.env.FLAIR_URL || `http://127.0.0.1:${port}`;
6426
- const { enableScheduler } = await import("./rem/scheduler.js");
6571
+ const { enableScheduler, formatEnableReport } = await import("./rem/scheduler.js");
6427
6572
  try {
6428
6573
  const r = enableScheduler({ agentId, flairUrl, hour, minute });
6429
- console.log(`✅ REM nightly scheduler enabled (${r.platform})`);
6430
- console.log(` Schedule: ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} local time`);
6431
- console.log(` Scheduler: ${r.schedulerPath}`);
6432
- console.log(` Shim: ${r.shimPath}`);
6433
- console.log(` Agent: ${agentId}`);
6434
- console.log(` Flair URL: ${flairUrl}`);
6435
- if (r.loadResult) {
6436
- if (r.loadResult.code === 0) {
6437
- console.log(` Load: ${r.loadCommand.join(" ")} → ok`);
6438
- }
6439
- else {
6440
- console.log(` Load: ${r.loadCommand.join(" ")} → code ${r.loadResult.code}`);
6441
- if (r.loadResult.stderr)
6442
- console.log(` stderr: ${r.loadResult.stderr.trim()}`);
6443
- }
6444
- }
6445
- console.log(`\nTip: run \`flair rem nightly run-once --dry-run\` to verify the cycle works`);
6446
- console.log(` before the first scheduled fire. Disable with \`flair rem nightly disable\`.`);
6574
+ // formatEnableReport() owns the success-vs-failure decision (flair#850:
6575
+ // do not print a success headline before activation is known to have
6576
+ // succeeded) — see src/rem/scheduler.ts for the unit-tested logic.
6577
+ const { lines, ok } = formatEnableReport(r, { hour, minute, agentId, flairUrl });
6578
+ for (const line of lines)
6579
+ console.log(line);
6580
+ if (!ok)
6581
+ process.exit(1);
6447
6582
  }
6448
6583
  catch (err) {
6449
6584
  console.error(`Error: ${err.message}`);
@@ -6481,18 +6616,14 @@ remNightly
6481
6616
  });
6482
6617
  remNightly
6483
6618
  .command("status")
6484
- .description("Show whether the nightly scheduler is installed")
6619
+ .description("Show whether the nightly scheduler is installed and genuinely active")
6485
6620
  .action(async () => {
6486
- const { schedulerStatus } = await import("./rem/scheduler.js");
6621
+ const { schedulerStatus, formatStatusReport } = await import("./rem/scheduler.js");
6487
6622
  try {
6488
6623
  const s = schedulerStatus();
6489
- console.log(`REM nightly scheduler (${s.platform}):`);
6490
- console.log(` Installed: ${s.installed ? "yes" : "no"}`);
6491
- console.log(` Scheduler: ${s.schedulerPath}`);
6492
- console.log(` Shim: ${s.shimPath}${s.shimExists ? "" : " (missing)"}`);
6493
- if (!s.installed) {
6494
- console.log(`\nEnable with: flair rem nightly enable --agent <id> [--at HH:MM]`);
6495
- }
6624
+ const { lines } = formatStatusReport(s);
6625
+ for (const line of lines)
6626
+ console.log(line);
6496
6627
  }
6497
6628
  catch (err) {
6498
6629
  console.error(`Error: ${err.message}`);
@@ -7650,7 +7781,7 @@ export function resolveFabricCredentials(opts) {
7650
7781
  // Mirrors `flair deploy`'s credential handling (FABRIC_USER/FABRIC_PASSWORD env
7651
7782
  // fallbacks, password-via-flag warning, --fabric-password-file — see
7652
7783
  // resolveFabricCredentials above) and NEVER prints credentials. The
7653
- // version-resolution + @harperfast/harper pin + reuse of deploy() lives in
7784
+ // version-resolution + harper pin + reuse of deploy() lives in
7654
7785
  // src/fabric-upgrade.ts; this wrapper only does CLI plumbing + the confirm.
7655
7786
  async function runFabricUpgrade(opts) {
7656
7787
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -7758,7 +7889,7 @@ async function runFabricUpgrade(opts) {
7758
7889
  // before the package swap, with a keep-last-3 retention policy.
7759
7890
  //
7760
7891
  // Native-backup alternative considered and rejected: Harper ships a
7761
- // `get_backup` operation (@harperfast/harper's dataLayer/getBackup.ts,
7892
+ // `get_backup` operation (harper's dataLayer/getBackup.ts,
7762
7893
  // wired in server/serverHelpers/serverUtilities.ts, documented in
7763
7894
  // components/mcp/tools/schemas/operationDescriptions.ts) that streams a
7764
7895
  // live backup over the running HTTP operations API. It's available in this
@@ -8104,13 +8235,13 @@ program
8104
8235
  // ── Fabric upgrade (--target) ────────────────────────────────────────────
8105
8236
  // When --target is passed, upgrade the Flair component DEPLOYED to that
8106
8237
  // Harper Fabric URL instead of the local npm install. Reuses `flair deploy`
8107
- // under the hood with the @harperfast/harper pin baked in (flair#513).
8238
+ // under the hood with the harper pin baked in (flair#513).
8108
8239
  .option("--target <url>", "Upgrade the Flair deployed to this Fabric URL (not the local install)")
8109
8240
  .option("--fabric-user <user>", "Fabric admin username — for --target (env: FABRIC_USER preferred; inline leaks to shell history)")
8110
8241
  .option("--fabric-password <pass>", "Fabric admin password — for --target (prefer FABRIC_PASSWORD env or --fabric-password-file; inline leaks to shell history)")
8111
8242
  .option("--fabric-password-file <path>", "Read the Fabric admin password from a file (chmod 600) — for --target")
8112
8243
  .option("--version <semver>", "Flair version to deploy with --target (default: latest published @tpsdev-ai/flair)")
8113
- .option("--harper-version <semver>", "Pin @harperfast/harper to this version for --target (default: registry latest, floored at the flair#513 fix)")
8244
+ .option("--harper-version <semver>", "Pin harper to this version for --target (default: registry latest, floored at the flair#513 fix)")
8114
8245
  .option("--project <name>", "Fabric component name for --target", "flair")
8115
8246
  .option("--no-replicated", "Disable cluster-wide replication for --target (default: replicated=true)")
8116
8247
  .option("--yes", "Skip the confirmation prompt for --target")
@@ -8668,34 +8799,23 @@ program
8668
8799
  process.exit(1);
8669
8800
  }
8670
8801
  const adminPass = process.env.HDB_ADMIN_PASSWORD || process.env.FLAIR_ADMIN_PASS || "";
8671
- const opsPort = resolveOpsPort(opts);
8672
- // flair#670: this fallback path (no launchd plist) sets no HARPER_SET_CONFIG,
8673
- // so OPERATIONSAPI_NETWORK_PORT applies unfiltered on top of whatever init
8674
- // persisted to harper-config.yaml. A bare port number here would silently
8675
- // strip a loopback (or escape-hatch) bind host back to all-interfaces on
8676
- // every plain `flair start`. Re-resolving the same host:port form init uses
8677
- // keeps this re-assertion consistent instead of regressing it — the
8678
- // escape hatch on this path is FLAIR_OPS_BIND (no --ops-bind flag on `start`).
8679
- const opsBindHost = resolveOpsBindHost({});
8680
- const modelsDir = process.env.FLAIR_MODELS_DIR ?? join(dataDir, "models");
8802
+ // flair#670/#863: this fallback path (no launchd plist) sets no
8803
+ // HARPER_SET_CONFIG, so the ops bind has to be re-asserted explicitly on
8804
+ // every spawn see buildDirectSpawnEnv. The escape hatch on this path is
8805
+ // FLAIR_OPS_BIND or the `opsBind` that `flair init --ops-bind` persisted to
8806
+ // ~/.flair/config.yaml; there is no --ops-bind flag on `start`.
8681
8807
  const env = {
8682
8808
  ...process.env,
8683
- ROOTPATH: dataDir,
8684
- // See the matching comment at the install-time spawn site above.
8685
- FLAIR_MODELS_DIR: modelsDir,
8686
- DEFAULTS_MODE: "dev",
8687
- HDB_ADMIN_USERNAME: DEFAULT_ADMIN_USER,
8688
- HTTP_PORT: String(port),
8689
- OPERATIONSAPI_NETWORK_PORT: `${opsBindHost}:${opsPort}`,
8690
- LOCAL_STUDIO: "false",
8809
+ ...buildDirectSpawnEnv({
8810
+ dataDir,
8811
+ modelsDir: process.env.FLAIR_MODELS_DIR ?? join(dataDir, "models"),
8812
+ httpPort: port,
8813
+ opsPort: resolveOpsPort(opts),
8814
+ opsBindHost: resolveOpsBindHost({}),
8815
+ adminUser: DEFAULT_ADMIN_USER,
8816
+ adminPass,
8817
+ }),
8691
8818
  };
8692
- // Only set HDB_ADMIN_PASSWORD if we have a real value — empty string
8693
- // would strip Harper's auth on an existing install
8694
- if (adminPass) {
8695
- env.HDB_ADMIN_PASSWORD = adminPass;
8696
- }
8697
- // models (flair#504 Phase 1): no env var needed — resources/embeddings-boot.ts
8698
- // self-registers the backend in-process on every boot (flair#694).
8699
8819
  const proc = spawn(process.execPath, [bin, "run", "."], {
8700
8820
  cwd: flairPackageDir(), env, detached: true, stdio: "ignore",
8701
8821
  });
@@ -8820,22 +8940,29 @@ async function startFlairProcess(port) {
8820
8940
  // to the initial Harper spawn) followed by `flair restart` would silently
8821
8941
  // drop admin credentials — any subsequent auth'd call returns 401.
8822
8942
  const adminPass = process.env.HDB_ADMIN_PASSWORD || process.env.FLAIR_ADMIN_PASS || "";
8823
- const modelsDir = process.env.FLAIR_MODELS_DIR ?? join(dataDir, "models");
8943
+ // flair#863: the same env `flair start` builds, from the same builder — this
8944
+ // path sets no HARPER_SET_CONFIG, and Harper RESTORES the pre-SET_CONFIG
8945
+ // original for every key SET_CONFIG had forced whenever that variable is
8946
+ // absent (cleanupRemovedEnvVar; see opsNetworkPortValue). This site used to
8947
+ // set no OPERATIONSAPI_NETWORK_PORT at all, so every `flair restart` /
8948
+ // `flair upgrade` on the non-launchd path silently reverted
8949
+ // `operationsApi.network.port` to a bare number in harper-config.yaml and
8950
+ // re-bound the ops API to all interfaces — permanently, since the reverted
8951
+ // value is what the next boot reads. It also made doctor's
8952
+ // `flair init && flair restart` remedy a no-op: the restart undid whatever
8953
+ // init had just written.
8824
8954
  const env = {
8825
8955
  ...process.env,
8826
- ROOTPATH: dataDir,
8827
- // See the matching comment at the install-time spawn site above.
8828
- FLAIR_MODELS_DIR: modelsDir,
8829
- DEFAULTS_MODE: "dev",
8830
- HDB_ADMIN_USERNAME: DEFAULT_ADMIN_USER,
8831
- HTTP_PORT: String(port),
8832
- LOCAL_STUDIO: "false",
8956
+ ...buildDirectSpawnEnv({
8957
+ dataDir,
8958
+ modelsDir: process.env.FLAIR_MODELS_DIR ?? join(dataDir, "models"),
8959
+ httpPort: port,
8960
+ opsPort: resolveOpsPort({ port }),
8961
+ opsBindHost: resolveOpsBindHost({}),
8962
+ adminUser: DEFAULT_ADMIN_USER,
8963
+ adminPass,
8964
+ }),
8833
8965
  };
8834
- if (adminPass) {
8835
- env.HDB_ADMIN_PASSWORD = adminPass;
8836
- }
8837
- // models (flair#504 Phase 1): no env var needed — resources/embeddings-boot.ts
8838
- // self-registers the backend in-process on every boot (flair#694).
8839
8966
  const proc = spawn(process.execPath, [bin, "run", "."], {
8840
8967
  cwd: flairPackageDir(), env, detached: true, stdio: "ignore",
8841
8968
  });
@@ -9052,7 +9179,7 @@ program
9052
9179
  // through the HNSW cosine index, which throws "Cosine distance comparison
9053
9180
  // requires an array" against rows whose stored embedding shape is
9054
9181
  // incompatible with the running Harper version (e.g. data written under
9055
- // @harperfast/harper@5.0.1 read under 5.0.9). The ops API bypasses the
9182
+ // harper@5.0.1 read under 5.0.9). The ops API bypasses the
9056
9183
  // vector index — exactly what we need when the goal is to replace every
9057
9184
  // embedding with a freshly-computed one. Without this path, `flair
9058
9185
  // reembed` could not recover from the very condition it exists to fix.
@@ -9487,7 +9614,7 @@ Exit codes:
9487
9614
 
9488
9615
  "peer" here means a Flair federation peer (GET /FederationPeers on the
9489
9616
  origin) — NOT Harper's own cluster-replication nodes, which the OSS
9490
- @harperfast/harper build this CLI ships does not expose (cluster_status is
9617
+ harper build this CLI ships does not expose (cluster_status is
9491
9618
  a harper-pro-only operation). A Fabric replica that was never
9492
9619
  federation-paired (\`flair federation pair\`) is invisible to this sweep —
9493
9620
  see src/fleet-verify.ts's file header for the full caveat.`)
@@ -13862,6 +13989,6 @@ if (import.meta.main) {
13862
13989
  await runCli();
13863
13990
  }
13864
13991
  // ─── Exported for testing ─────────────────────────────────────────────────────
13865
- export { runCli, resolveKeyPath, buildEd25519Auth, readPortFromConfig, resolveHttpPort, resolveOpsPort, resolveOpsBindHost, resolveTarget, resolveOpsTarget, resolveEffectiveOpsUrl, resolveOpsUrlFromTarget, signRequestBody, b64, b64url, program, api, VALID_PRESENCE_ACTIVITIES, MAX_TASK_LENGTH, MAX_WORKSPACE_FIELD_LENGTH, MAX_ORGEVENT_SUMMARY_LENGTH, MAX_ORGEVENT_DETAIL_LENGTH, isLocalBase, isLikelyRealSecret, shouldShowInlineSecretWarning, parseTokenFromFile, resolveLocalAdminPass, readAdminPassFileSecure,
13992
+ export { runCli, resolveKeyPath, buildEd25519Auth, readPortFromConfig, readOpsBindFromConfig, readOpsPortFromConfig, writeConfig, resolveHttpPort, resolveOpsPort, resolveOpsBindHost, resolveTarget, resolveOpsTarget, resolveEffectiveOpsUrl, resolveOpsUrlFromTarget, signRequestBody, b64, b64url, program, api, VALID_PRESENCE_ACTIVITIES, MAX_TASK_LENGTH, MAX_WORKSPACE_FIELD_LENGTH, MAX_ORGEVENT_SUMMARY_LENGTH, MAX_ORGEVENT_DETAIL_LENGTH, isLocalBase, isLikelyRealSecret, shouldShowInlineSecretWarning, parseTokenFromFile, resolveLocalAdminPass, readAdminPassFileSecure,
13866
13993
  // launchd label (flair#693)
13867
13994
  LEGACY_LAUNCHD_LABEL, launchdLabel, launchdPlistPath, resolveLaunchdLabel, migrateLegacyLaunchdLabel, ensureLaunchdServiceLoaded, };
package/dist/deploy.js CHANGED
@@ -156,28 +156,38 @@ export function deriveVerifyResources(packageRoot) {
156
156
  names.sort();
157
157
  return names.length ? names : [FALLBACK_VERIFY_RESOURCE];
158
158
  }
159
+ // flair#870: flair declares the BARE `harper` package name, so that is probed
160
+ // first. `@harperfast/harper` is a permanent lockstep publish of the same
161
+ // source under the legacy scoped name; it stays in the probe list because
162
+ // `packageRoot` can be a staged install of an OLDER published flair (see
163
+ // src/fabric-upgrade.ts) that declared the scoped name.
164
+ const HARPER_PKG_NAMES = ["harper", "@harperfast/harper"];
159
165
  function resolveHarperBin(packageRoot) {
160
- const local = join(packageRoot, "node_modules/@harperfast/harper/dist/bin/harper.js");
161
- if (existsSync(local))
162
- return local;
163
- try {
164
- const req = createRequire(join(packageRoot, "package.json"));
165
- const mainPath = req.resolve("@harperfast/harper");
166
- let dir = dirname(mainPath);
167
- for (let i = 0; i < 6; i++) {
168
- const candidate = join(dir, "dist/bin/harper.js");
169
- if (existsSync(candidate))
170
- return candidate;
171
- const parent = dirname(dir);
172
- if (parent === dir)
173
- break;
174
- dir = parent;
175
- }
166
+ for (const pkg of HARPER_PKG_NAMES) {
167
+ const local = join(packageRoot, "node_modules", pkg, "dist/bin/harper.js");
168
+ if (existsSync(local))
169
+ return local;
176
170
  }
177
- catch {
178
- /* fall through */
171
+ for (const pkg of HARPER_PKG_NAMES) {
172
+ try {
173
+ const req = createRequire(join(packageRoot, "package.json"));
174
+ const mainPath = req.resolve(pkg);
175
+ let dir = dirname(mainPath);
176
+ for (let i = 0; i < 6; i++) {
177
+ const candidate = join(dir, "dist/bin/harper.js");
178
+ if (existsSync(candidate))
179
+ return candidate;
180
+ const parent = dirname(dir);
181
+ if (parent === dir)
182
+ break;
183
+ dir = parent;
184
+ }
185
+ }
186
+ catch {
187
+ /* try the next package name */
188
+ }
179
189
  }
180
- throw new Error("Could not locate Harper CLI binary (@harperfast/harper). " +
190
+ throw new Error(`Could not locate Harper CLI binary (tried ${HARPER_PKG_NAMES.join(", ")}). ` +
181
191
  "Flair deploy requires Harper to be installed alongside Flair.");
182
192
  }
183
193
  // Pure arg-array builder — separated from spawnHarper so the timeout