@tpsdev-ai/flair 0.28.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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/bridges/builtins/index.js +1 -1
  3. package/dist/bridges/discover.js +1 -1
  4. package/dist/bridges/runtime/load-plugin.js +1 -1
  5. package/dist/bridges/scaffold.js +3 -3
  6. package/dist/bridges/types.js +1 -1
  7. package/dist/cli-shim.cjs +1 -1
  8. package/dist/cli.js +357 -130
  9. package/dist/deploy.js +29 -19
  10. package/dist/fabric-upgrade.js +61 -23
  11. package/dist/fleet-verify.js +3 -3
  12. package/dist/lib/mcp-enable.js +2 -2
  13. package/dist/rem/runner.js +2 -2
  14. package/dist/rem/scheduler.js +226 -17
  15. package/dist/resources/A2AAdapter.js +1 -1
  16. package/dist/resources/Admin.js +1 -1
  17. package/dist/resources/AdminConnectors.js +1 -1
  18. package/dist/resources/AdminDashboard.js +1 -1
  19. package/dist/resources/AdminIdp.js +1 -1
  20. package/dist/resources/AdminInstance.js +1 -1
  21. package/dist/resources/AdminMemory.js +1 -1
  22. package/dist/resources/AdminPrincipals.js +1 -1
  23. package/dist/resources/Agent.js +1 -1
  24. package/dist/resources/AgentCard.js +1 -1
  25. package/dist/resources/AgentSeed.js +1 -1
  26. package/dist/resources/AttentionQuery.js +1 -1
  27. package/dist/resources/Credential.js +1 -1
  28. package/dist/resources/Federation.js +1 -1
  29. package/dist/resources/Instance.js +1 -1
  30. package/dist/resources/Integration.js +1 -1
  31. package/dist/resources/MCPClientMetadata.js +1 -1
  32. package/dist/resources/Memory.js +1 -1
  33. package/dist/resources/MemoryBootstrap.js +1 -1
  34. package/dist/resources/MemoryCandidate.js +168 -0
  35. package/dist/resources/MemoryConsolidate.js +1 -1
  36. package/dist/resources/MemoryDedupStats.js +1 -1
  37. package/dist/resources/MemoryFeed.js +1 -1
  38. package/dist/resources/MemoryGrant.js +1 -1
  39. package/dist/resources/MemoryMaintenance.js +1 -1
  40. package/dist/resources/MemoryReflect.js +3 -3
  41. package/dist/resources/MemoryReindex.js +1 -1
  42. package/dist/resources/MemoryUsage.js +1 -1
  43. package/dist/resources/OAuth.js +1 -1
  44. package/dist/resources/OAuthClient.js +1 -1
  45. package/dist/resources/OrgEvent.js +1 -1
  46. package/dist/resources/OrgEventCatchup.js +1 -1
  47. package/dist/resources/OrgEventMaintenance.js +1 -1
  48. package/dist/resources/PairingToken.js +1 -1
  49. package/dist/resources/Peer.js +1 -1
  50. package/dist/resources/Presence.js +3 -3
  51. package/dist/resources/RecordUsage.js +16 -2
  52. package/dist/resources/Relationship.js +1 -1
  53. package/dist/resources/SemanticSearch.js +1 -1
  54. package/dist/resources/SkillScan.js +1 -1
  55. package/dist/resources/Soul.js +1 -1
  56. package/dist/resources/SoulFeed.js +1 -1
  57. package/dist/resources/WorkspaceLatest.js +1 -1
  58. package/dist/resources/WorkspaceState.js +1 -1
  59. package/dist/resources/XAA.js +1 -1
  60. package/dist/resources/a2a-url.js +1 -1
  61. package/dist/resources/agent-auth.js +1 -1
  62. package/dist/resources/agentcard-fields.js +1 -1
  63. package/dist/resources/auth-middleware.js +1 -1
  64. package/dist/resources/embeddings-boot.js +4 -4
  65. package/dist/resources/embeddings-provider.js +13 -43
  66. package/dist/resources/federation-cleanup.js +5 -5
  67. package/dist/resources/federation-nonce-store.js +5 -5
  68. package/dist/resources/health.js +29 -6
  69. package/dist/resources/instance-identity.js +1 -1
  70. package/dist/resources/mcp-client-metadata-fields.js +1 -1
  71. package/dist/resources/mcp-handler.js +1 -1
  72. package/dist/resources/mcp-oauth.js +2 -2
  73. package/dist/resources/mcp-tools.js +9 -1
  74. package/dist/resources/memory-read-scope.js +1 -1
  75. package/dist/resources/memory-reflect-lib.js +4 -4
  76. package/dist/resources/memory-visibility.js +3 -3
  77. package/dist/resources/migration-boot.js +1 -1
  78. package/dist/resources/migrations/embedding-stamp.js +3 -3
  79. package/dist/resources/migrations/graph-heal.js +2 -2
  80. package/dist/resources/migrations/ledger.js +1 -1
  81. package/dist/resources/migrations/registry.js +14 -5
  82. package/dist/resources/migrations/runner.js +64 -11
  83. package/dist/resources/migrations/synthetic-test-migration.js +1 -1
  84. package/dist/resources/migrations/visibility-backfill.js +232 -0
  85. package/dist/resources/models-dir.js +55 -0
  86. package/dist/resources/record-types.js +30 -2
  87. package/dist/resources/rerank-provider.js +24 -4
  88. package/dist/resources/semantic-retrieval-core.js +1 -1
  89. package/dist/resources/usage-recording.js +73 -7
  90. package/dist/resources/version.js +33 -0
  91. package/dist/src/rem/scheduler.js +449 -0
  92. package/docs/assets/flair-cross-orchestrator.cast +1 -1
  93. package/docs/bridges.md +3 -3
  94. package/docs/deployment.md +1 -0
  95. package/docs/mcp-clients.md +3 -1
  96. package/docs/n8n.md +1 -2
  97. package/docs/notes/rem-ux.md +1 -1
  98. package/docs/rem.md +2 -0
  99. package/docs/supply-chain-policy.md +4 -4
  100. package/docs/upgrade.md +2 -2
  101. package/package.json +4 -4
  102. package/schemas/memory.graphql +27 -12
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
@@ -384,6 +482,33 @@ export function detectOpsApiAllInterfacesBind(portValue) {
384
482
  }
385
483
  return { allInterfaces: true, boundHost: null };
386
484
  }
485
+ /**
486
+ * Decide the source `flair init` should use for the admin password when no
487
+ * explicit `--admin-pass` / `--admin-pass-file` / env var was given
488
+ * (flair#827).
489
+ *
490
+ * Before this existed, init ALWAYS generated a fresh random password and
491
+ * overwrote `~/.flair/admin-pass` on every run — including a re-run against
492
+ * an install that was already bootstrapped and working (e.g. following
493
+ * `flair doctor`'s ops-bind finding, whose only prescribed remedy is
494
+ * re-running `flair init`). Harper's `HDB_ADMIN_PASSWORD` env var only seeds
495
+ * a brand-new install's user record — it does NOT rotate an existing user's
496
+ * stored password hash on every boot. So overwriting the file desynced it
497
+ * from what Harper actually had persisted, and the very next ops-API call in
498
+ * that SAME init run (seeding the agent) failed with a 401 "Login failed",
499
+ * breaking working auth on an install that had nothing wrong with its
500
+ * credentials.
501
+ *
502
+ * An admin-pass file that already exists on disk IS the working install's
503
+ * password — `flair init` is the only thing that ever writes it — so reuse
504
+ * it instead of generating a new one. That makes re-init idempotent: safe to
505
+ * run again at any time without risking the instance's auth. Deliberately
506
+ * rotating the admin password is a separate operation, not a `flair init`
507
+ * side effect.
508
+ */
509
+ export function resolveInitAdminPasswordSource(adminPassFileExists) {
510
+ return adminPassFileExists ? "reuse-existing" : "generate-new";
511
+ }
387
512
  // ─── Ops-socket permission posture (flair#763) ─────────────────────────────────
388
513
  //
389
514
  // The ops API domain socket (dataDir/operations-server) is protected by a
@@ -666,10 +791,26 @@ function resolveOpsUrlFromTarget(targetUrl) {
666
791
  }
667
792
  return url.toString().replace(/\/$/, "");
668
793
  }
669
- function writeConfig(port) {
670
- const p = configPath();
671
- mkdirSync(join(homedir(), ".flair"), { recursive: true });
672
- 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);
673
814
  }
674
815
  function privKeyPath(agentId, keysDir) {
675
816
  return join(keysDir, `${agentId}.key`);
@@ -682,11 +823,25 @@ function flairPackageDir() {
682
823
  return join(import.meta.dirname ?? __dirname, "..");
683
824
  }
684
825
  function harperBin() {
685
- // Resolve relative to this file's location (dist/cli.js → ../node_modules/...)
686
- const candidates = [
687
- join(import.meta.dirname ?? __dirname, "..", "node_modules", "@harperfast", "harper", "dist", "bin", "harper.js"),
688
- 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(),
689
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
+ }
690
845
  for (const c of candidates)
691
846
  if (existsSync(c))
692
847
  return c;
@@ -1846,6 +2001,39 @@ const __pkgVersion = (() => {
1846
2001
  return "unknown";
1847
2002
  }
1848
2003
  })();
2004
+ /**
2005
+ * The `@tpsdev-ai/flair-mcp` spec written into a client's MCP config.
2006
+ *
2007
+ * PINNED, deliberately. A bare `npx -y @tpsdev-ai/flair-mcp` re-resolves to
2008
+ * whatever is currently published on EVERY agent session — so a single bad
2009
+ * publish (stolen credentials, a malicious commit that clears review, or a
2010
+ * compromised dependency of the MCP package) reaches every wired user
2011
+ * silently, with no lockfile and no review step in the path. The postmark-mcp
2012
+ * incident was exactly this shape: a legitimate publish by the legitimate
2013
+ * owner, propagating for 16 days before anyone noticed. Worse, a yank does
2014
+ * not help — unpinned clients keep resolving latest.
2015
+ *
2016
+ * Our publish side is already hardened (OIDC staged publish, human 2FA at the
2017
+ * release gate), but that defends against credential theft, not against a bad
2018
+ * version being published legitimately. The consumer side is where that gap
2019
+ * closes, and pinning is what closes it: a wired client keeps running the
2020
+ * exact version that was current when it was wired, and moving forward
2021
+ * becomes a deliberate act.
2022
+ *
2023
+ * flair and flair-mcp ship in version lockstep from this monorepo, so the
2024
+ * running CLI's own version is the correct pin. `flair init` re-run rewires
2025
+ * to the then-current version; see the upgrade-rewire follow-up issue for
2026
+ * making `flair upgrade` do the same.
2027
+ *
2028
+ * Falls back to the unpinned spec only when the version can't be read, which
2029
+ * is the same condition under which `--version` reports "unknown" — a broken
2030
+ * install, where a working MCP wiring matters more than a precise pin.
2031
+ */
2032
+ export function mcpServerSpec(version = __pkgVersion) {
2033
+ return version && version !== "unknown"
2034
+ ? `@tpsdev-ai/flair-mcp@${version}`
2035
+ : "@tpsdev-ai/flair-mcp";
2036
+ }
1849
2037
  const program = new Command();
1850
2038
  program.name("flair").version(__pkgVersion, "-v, --version");
1851
2039
  // ─── CLI↔server version handshake (flair#695 §B) ────────────────────────────
@@ -2117,9 +2305,10 @@ program
2117
2305
  selectedClients.push(clientOpt);
2118
2306
  }
2119
2307
  // Admin password: determine from opts, env, or generate
2120
- // Priority: 1) --admin-pass-file, 2) env vars, 3) generate new
2308
+ // Priority: 1) --admin-pass-file, 2) env vars, 3) reuse existing file, 4) generate new
2121
2309
  let adminPass;
2122
2310
  let passwordSource = "generated";
2311
+ let reusedExistingAdminPass = false;
2123
2312
  // Warn if --admin-pass is passed inline (not from env)
2124
2313
  if (shouldShowInlineSecretWarning(opts.adminPass, false, new Set(["--admin-pass"]), "--admin-pass")) {
2125
2314
  console.error("warning: --admin-pass passed inline. Consider --admin-pass-file <path> or FLAIR_ADMIN_PASS env " +
@@ -2151,34 +2340,65 @@ program
2151
2340
  passwordSource = "env"; // Treat same as env for display purposes
2152
2341
  }
2153
2342
  else {
2154
- // Generate new password and write to file atomically
2155
- adminPass = Buffer.from(nacl.randomBytes(18)).toString("base64url");
2156
- passwordSource = "generated";
2157
- // Atomic write: create temp file in same dir, then rename
2158
2343
  const flairDir = join(homedir(), ".flair");
2159
- mkdirSync(flairDir, { recursive: true });
2160
2344
  const adminPassPath = join(flairDir, "admin-pass");
2161
- const tempPath = mkdtempSync(join(flairDir, ".admin-pass.tmp-"));
2162
- const finalTempPath = join(tempPath, "admin-pass");
2163
- try {
2164
- writeFileSync(finalTempPath, adminPass + "\n", { mode: 0o600 });
2165
- renameSync(finalTempPath, adminPassPath);
2166
- rmSync(tempPath, { recursive: true, force: true });
2345
+ passwordSource = "generated";
2346
+ if (resolveInitAdminPasswordSource(existsSync(adminPassPath)) === "reuse-existing") {
2347
+ // flair#827: an admin-pass file already on disk means a PRIOR `flair
2348
+ // init` already bootstrapped Harper's admin user with this password.
2349
+ // HDB_ADMIN_PASSWORD only seeds a brand-new install — Harper does
2350
+ // NOT rotate an existing user's stored password hash from env on
2351
+ // every boot. Generating and overwriting the file here would desync
2352
+ // it from what Harper actually has persisted, breaking ops-API auth
2353
+ // (401 "Login failed") on THIS SAME init run (the agent-seeding call
2354
+ // below) without fixing whatever the re-run was meant to fix — e.g.
2355
+ // `flair doctor`'s ops-bind finding, whose only prescribed remedy is
2356
+ // re-running `flair init`. Re-init must be idempotent here: reuse
2357
+ // the existing password so it's always safe to re-run against a
2358
+ // working install. Rotating the admin password on purpose is a
2359
+ // separate, deliberate operation (see the ops runbook), not a side
2360
+ // effect of re-init.
2361
+ try {
2362
+ adminPass = readAdminPassFileSecure(adminPassPath);
2363
+ }
2364
+ catch (err) {
2365
+ console.error(`Error: ${err.message}`);
2366
+ process.exit(1);
2367
+ }
2368
+ reusedExistingAdminPass = true;
2167
2369
  }
2168
- catch (err) {
2169
- // Clean up temp dir on failure
2370
+ else {
2371
+ // Generate new password and write to file atomically
2372
+ adminPass = Buffer.from(nacl.randomBytes(18)).toString("base64url");
2373
+ // Atomic write: create temp file in same dir, then rename
2374
+ mkdirSync(flairDir, { recursive: true });
2375
+ const tempPath = mkdtempSync(join(flairDir, ".admin-pass.tmp-"));
2376
+ const finalTempPath = join(tempPath, "admin-pass");
2170
2377
  try {
2378
+ writeFileSync(finalTempPath, adminPass + "\n", { mode: 0o600 });
2379
+ renameSync(finalTempPath, adminPassPath);
2171
2380
  rmSync(tempPath, { recursive: true, force: true });
2172
2381
  }
2173
- catch { }
2174
- throw err;
2382
+ catch (err) {
2383
+ // Clean up temp dir on failure
2384
+ try {
2385
+ rmSync(tempPath, { recursive: true, force: true });
2386
+ }
2387
+ catch { }
2388
+ throw err;
2389
+ }
2175
2390
  }
2176
2391
  }
2177
2392
  const adminUser = DEFAULT_ADMIN_USER;
2178
- // If we generated the password, report where it was saved
2393
+ // If we generated (or reused) the password, report where it lives
2179
2394
  if (passwordSource === "generated") {
2180
2395
  const adminPassPath = join(homedir(), ".flair", "admin-pass");
2181
- console.log(`Admin password saved to: ${adminPassPath}`);
2396
+ if (reusedExistingAdminPass) {
2397
+ console.log(`Reusing existing admin password from: ${adminPassPath} (flair#827: re-init never rotates it — see the ops runbook to change it deliberately)`);
2398
+ }
2399
+ else {
2400
+ console.log(`Admin password saved to: ${adminPassPath}`);
2401
+ }
2182
2402
  }
2183
2403
  // Check Node.js version
2184
2404
  const major = parseInt(process.version.slice(1), 10);
@@ -2210,8 +2430,13 @@ program
2210
2430
  catch { /* not running */ }
2211
2431
  if (!alreadyRunning) {
2212
2432
  const bin = harperBin();
2213
- if (!bin)
2214
- 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
+ }
2215
2440
  mkdirSync(dataDir, { recursive: true });
2216
2441
  // Detect whether Harper has already been installed in this data dir.
2217
2442
  // harper-config.yaml is created during install — its presence means
@@ -2246,7 +2471,12 @@ program
2246
2471
  THREADS_COUNT: "1",
2247
2472
  NODE_HOSTNAME: "localhost",
2248
2473
  HTTP_PORT: String(httpPort),
2249
- 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),
2250
2480
  LOCAL_STUDIO: "false",
2251
2481
  };
2252
2482
  // models (flair#504 Phase 1): the embedding backend registers itself
@@ -2386,7 +2616,7 @@ program
2386
2616
  <key>THREADS_COUNT</key><string>1</string>
2387
2617
  <key>NODE_HOSTNAME</key><string>localhost</string>
2388
2618
  <key>HTTP_PORT</key><string>${httpPort}</string>
2389
- <key>OPERATIONSAPI_NETWORK_PORT</key><string>${opsPort}</string>
2619
+ <key>OPERATIONSAPI_NETWORK_PORT</key><string>${escapeXml(opsNetworkPortValue(opsBindHost, opsPort))}</string>
2390
2620
  <key>LOCAL_STUDIO</key><string>false</string>
2391
2621
  </dict>
2392
2622
  <key>RunAtLoad</key><true/>
@@ -2400,8 +2630,15 @@ program
2400
2630
  }
2401
2631
  }
2402
2632
  }
2403
- // Persist port to config so other commands can find this instance
2404
- 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);
2405
2642
  if (agentId) {
2406
2643
  // Generate or reuse keypair
2407
2644
  mkdirSync(keysDir, { recursive: true });
@@ -2515,7 +2752,8 @@ program
2515
2752
  }
2516
2753
  // ── MCP client wiring ────────────────────────────────────────────────
2517
2754
  // The full one-command front door: detect installed MCP clients and wire
2518
- // each to the zero-install `npx -y @tpsdev-ai/flair-mcp` server. Claude
2755
+ // each to the zero-install `npx -y @tpsdev-ai/flair-mcp@<version>` server
2756
+ // (pinned — see mcpServerSpec()). Claude
2519
2757
  // Code is auto-wired into ~/.claude.json (the only client the CLI can
2520
2758
  // safely modify); other clients get copy-paste snippets. `--no-mcp`
2521
2759
  // skips wiring entirely; `--client <name>` targets one client; the
@@ -2551,7 +2789,7 @@ program
2551
2789
  const flairMcpConfig = {
2552
2790
  type: "stdio",
2553
2791
  command: "npx",
2554
- args: ["-y", "@tpsdev-ai/flair-mcp"],
2792
+ args: ["-y", mcpServerSpec()],
2555
2793
  // flair#718 authorship-provenance: each client's wired env block
2556
2794
  // gets its OWN FLAIR_CLIENT label (never the shared mcpEnv
2557
2795
  // object directly — that would stamp the same label into every
@@ -2640,7 +2878,9 @@ program
2640
2878
  if (!opts.skipSmoke && !noMcp && clientOpt !== "none" && wiringResults.length > 0) {
2641
2879
  console.log("\n Smoke-testing MCP server...");
2642
2880
  try {
2643
- const mcpProc = spawn("npx", ["-y", "@tpsdev-ai/flair-mcp"], {
2881
+ // Same spec that gets WIRED above — the smoke test must exercise the
2882
+ // exact version the user will run, not whatever npm resolves latest to.
2883
+ const mcpProc = spawn("npx", ["-y", mcpServerSpec()], {
2644
2884
  env: { ...process.env, FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl },
2645
2885
  stdio: ["pipe", "pipe", "pipe"],
2646
2886
  });
@@ -5863,8 +6103,7 @@ export function describeReflectError(message) {
5863
6103
  return { kind: "other", text: message };
5864
6104
  }
5865
6105
  // ─── flair rem rapid ──────────────────────────────────────────────────────────
5866
- // Executes by default (specs/FLAIR-NIGHTLY-REM-SLICE-2-DISTILLATION.md § 3C,
5867
- // issue #707): distills server-side via /ReflectMemories execute:true and
6106
+ // Executes by default (§3C, issue #707): distills server-side via /ReflectMemories execute:true and
5868
6107
  // stages MemoryCandidate rows, printing a staged-candidate summary. --prompt-only
5869
6108
  // preserves the pre-#710 handoff behavior byte-for-byte, for the bring-your-
5870
6109
  // own-model workflow.
@@ -6329,27 +6568,17 @@ remNightly
6329
6568
  const minute = parseInt(match[2], 10);
6330
6569
  const port = readPortFromConfig() ?? DEFAULT_PORT;
6331
6570
  const flairUrl = opts.flairUrl || process.env.FLAIR_URL || `http://127.0.0.1:${port}`;
6332
- const { enableScheduler } = await import("./rem/scheduler.js");
6571
+ const { enableScheduler, formatEnableReport } = await import("./rem/scheduler.js");
6333
6572
  try {
6334
6573
  const r = enableScheduler({ agentId, flairUrl, hour, minute });
6335
- console.log(`✅ REM nightly scheduler enabled (${r.platform})`);
6336
- console.log(` Schedule: ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} local time`);
6337
- console.log(` Scheduler: ${r.schedulerPath}`);
6338
- console.log(` Shim: ${r.shimPath}`);
6339
- console.log(` Agent: ${agentId}`);
6340
- console.log(` Flair URL: ${flairUrl}`);
6341
- if (r.loadResult) {
6342
- if (r.loadResult.code === 0) {
6343
- console.log(` Load: ${r.loadCommand.join(" ")} → ok`);
6344
- }
6345
- else {
6346
- console.log(` Load: ${r.loadCommand.join(" ")} → code ${r.loadResult.code}`);
6347
- if (r.loadResult.stderr)
6348
- console.log(` stderr: ${r.loadResult.stderr.trim()}`);
6349
- }
6350
- }
6351
- console.log(`\nTip: run \`flair rem nightly run-once --dry-run\` to verify the cycle works`);
6352
- 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);
6353
6582
  }
6354
6583
  catch (err) {
6355
6584
  console.error(`Error: ${err.message}`);
@@ -6387,18 +6616,14 @@ remNightly
6387
6616
  });
6388
6617
  remNightly
6389
6618
  .command("status")
6390
- .description("Show whether the nightly scheduler is installed")
6619
+ .description("Show whether the nightly scheduler is installed and genuinely active")
6391
6620
  .action(async () => {
6392
- const { schedulerStatus } = await import("./rem/scheduler.js");
6621
+ const { schedulerStatus, formatStatusReport } = await import("./rem/scheduler.js");
6393
6622
  try {
6394
6623
  const s = schedulerStatus();
6395
- console.log(`REM nightly scheduler (${s.platform}):`);
6396
- console.log(` Installed: ${s.installed ? "yes" : "no"}`);
6397
- console.log(` Scheduler: ${s.schedulerPath}`);
6398
- console.log(` Shim: ${s.shimPath}${s.shimExists ? "" : " (missing)"}`);
6399
- if (!s.installed) {
6400
- console.log(`\nEnable with: flair rem nightly enable --agent <id> [--at HH:MM]`);
6401
- }
6624
+ const { lines } = formatStatusReport(s);
6625
+ for (const line of lines)
6626
+ console.log(line);
6402
6627
  }
6403
6628
  catch (err) {
6404
6629
  console.error(`Error: ${err.message}`);
@@ -7556,7 +7781,7 @@ export function resolveFabricCredentials(opts) {
7556
7781
  // Mirrors `flair deploy`'s credential handling (FABRIC_USER/FABRIC_PASSWORD env
7557
7782
  // fallbacks, password-via-flag warning, --fabric-password-file — see
7558
7783
  // resolveFabricCredentials above) and NEVER prints credentials. The
7559
- // version-resolution + @harperfast/harper pin + reuse of deploy() lives in
7784
+ // version-resolution + harper pin + reuse of deploy() lives in
7560
7785
  // src/fabric-upgrade.ts; this wrapper only does CLI plumbing + the confirm.
7561
7786
  async function runFabricUpgrade(opts) {
7562
7787
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -7664,7 +7889,7 @@ async function runFabricUpgrade(opts) {
7664
7889
  // before the package swap, with a keep-last-3 retention policy.
7665
7890
  //
7666
7891
  // Native-backup alternative considered and rejected: Harper ships a
7667
- // `get_backup` operation (@harperfast/harper's dataLayer/getBackup.ts,
7892
+ // `get_backup` operation (harper's dataLayer/getBackup.ts,
7668
7893
  // wired in server/serverHelpers/serverUtilities.ts, documented in
7669
7894
  // components/mcp/tools/schemas/operationDescriptions.ts) that streams a
7670
7895
  // live backup over the running HTTP operations API. It's available in this
@@ -8010,13 +8235,13 @@ program
8010
8235
  // ── Fabric upgrade (--target) ────────────────────────────────────────────
8011
8236
  // When --target is passed, upgrade the Flair component DEPLOYED to that
8012
8237
  // Harper Fabric URL instead of the local npm install. Reuses `flair deploy`
8013
- // under the hood with the @harperfast/harper pin baked in (flair#513).
8238
+ // under the hood with the harper pin baked in (flair#513).
8014
8239
  .option("--target <url>", "Upgrade the Flair deployed to this Fabric URL (not the local install)")
8015
8240
  .option("--fabric-user <user>", "Fabric admin username — for --target (env: FABRIC_USER preferred; inline leaks to shell history)")
8016
8241
  .option("--fabric-password <pass>", "Fabric admin password — for --target (prefer FABRIC_PASSWORD env or --fabric-password-file; inline leaks to shell history)")
8017
8242
  .option("--fabric-password-file <path>", "Read the Fabric admin password from a file (chmod 600) — for --target")
8018
8243
  .option("--version <semver>", "Flair version to deploy with --target (default: latest published @tpsdev-ai/flair)")
8019
- .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)")
8020
8245
  .option("--project <name>", "Fabric component name for --target", "flair")
8021
8246
  .option("--no-replicated", "Disable cluster-wide replication for --target (default: replicated=true)")
8022
8247
  .option("--yes", "Skip the confirmation prompt for --target")
@@ -8574,34 +8799,23 @@ program
8574
8799
  process.exit(1);
8575
8800
  }
8576
8801
  const adminPass = process.env.HDB_ADMIN_PASSWORD || process.env.FLAIR_ADMIN_PASS || "";
8577
- const opsPort = resolveOpsPort(opts);
8578
- // flair#670: this fallback path (no launchd plist) sets no HARPER_SET_CONFIG,
8579
- // so OPERATIONSAPI_NETWORK_PORT applies unfiltered on top of whatever init
8580
- // persisted to harper-config.yaml. A bare port number here would silently
8581
- // strip a loopback (or escape-hatch) bind host back to all-interfaces on
8582
- // every plain `flair start`. Re-resolving the same host:port form init uses
8583
- // keeps this re-assertion consistent instead of regressing it — the
8584
- // escape hatch on this path is FLAIR_OPS_BIND (no --ops-bind flag on `start`).
8585
- const opsBindHost = resolveOpsBindHost({});
8586
- 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`.
8587
8807
  const env = {
8588
8808
  ...process.env,
8589
- ROOTPATH: dataDir,
8590
- // See the matching comment at the install-time spawn site above.
8591
- FLAIR_MODELS_DIR: modelsDir,
8592
- DEFAULTS_MODE: "dev",
8593
- HDB_ADMIN_USERNAME: DEFAULT_ADMIN_USER,
8594
- HTTP_PORT: String(port),
8595
- OPERATIONSAPI_NETWORK_PORT: `${opsBindHost}:${opsPort}`,
8596
- 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
+ }),
8597
8818
  };
8598
- // Only set HDB_ADMIN_PASSWORD if we have a real value — empty string
8599
- // would strip Harper's auth on an existing install
8600
- if (adminPass) {
8601
- env.HDB_ADMIN_PASSWORD = adminPass;
8602
- }
8603
- // models (flair#504 Phase 1): no env var needed — resources/embeddings-boot.ts
8604
- // self-registers the backend in-process on every boot (flair#694).
8605
8819
  const proc = spawn(process.execPath, [bin, "run", "."], {
8606
8820
  cwd: flairPackageDir(), env, detached: true, stdio: "ignore",
8607
8821
  });
@@ -8726,22 +8940,29 @@ async function startFlairProcess(port) {
8726
8940
  // to the initial Harper spawn) followed by `flair restart` would silently
8727
8941
  // drop admin credentials — any subsequent auth'd call returns 401.
8728
8942
  const adminPass = process.env.HDB_ADMIN_PASSWORD || process.env.FLAIR_ADMIN_PASS || "";
8729
- 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.
8730
8954
  const env = {
8731
8955
  ...process.env,
8732
- ROOTPATH: dataDir,
8733
- // See the matching comment at the install-time spawn site above.
8734
- FLAIR_MODELS_DIR: modelsDir,
8735
- DEFAULTS_MODE: "dev",
8736
- HDB_ADMIN_USERNAME: DEFAULT_ADMIN_USER,
8737
- HTTP_PORT: String(port),
8738
- 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
+ }),
8739
8965
  };
8740
- if (adminPass) {
8741
- env.HDB_ADMIN_PASSWORD = adminPass;
8742
- }
8743
- // models (flair#504 Phase 1): no env var needed — resources/embeddings-boot.ts
8744
- // self-registers the backend in-process on every boot (flair#694).
8745
8966
  const proc = spawn(process.execPath, [bin, "run", "."], {
8746
8967
  cwd: flairPackageDir(), env, detached: true, stdio: "ignore",
8747
8968
  });
@@ -8958,7 +9179,7 @@ program
8958
9179
  // through the HNSW cosine index, which throws "Cosine distance comparison
8959
9180
  // requires an array" against rows whose stored embedding shape is
8960
9181
  // incompatible with the running Harper version (e.g. data written under
8961
- // @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
8962
9183
  // vector index — exactly what we need when the goal is to replace every
8963
9184
  // embedding with a freshly-computed one. Without this path, `flair
8964
9185
  // reembed` could not recover from the very condition it exists to fix.
@@ -9393,7 +9614,7 @@ Exit codes:
9393
9614
 
9394
9615
  "peer" here means a Flair federation peer (GET /FederationPeers on the
9395
9616
  origin) — NOT Harper's own cluster-replication nodes, which the OSS
9396
- @harperfast/harper build this CLI ships does not expose (cluster_status is
9617
+ harper build this CLI ships does not expose (cluster_status is
9397
9618
  a harper-pro-only operation). A Fabric replica that was never
9398
9619
  federation-paired (\`flair federation pair\`) is invisible to this sweep —
9399
9620
  see src/fleet-verify.ts's file header for the full caveat.`)
@@ -9707,7 +9928,13 @@ program
9707
9928
  // 3b. Ops API bind (flair#670) — report-only finding, never auto-fixed.
9708
9929
  // Rebinding the ops API requires a Harper restart to take effect, so
9709
9930
  // `doctor --fix` deliberately does not touch it here; the fix is
9710
- // `flair init` (re-run) or a manual harper-config.yaml edit + restart.
9931
+ // `flair init` (re-run, then `flair restart` to apply it) or a manual
9932
+ // harper-config.yaml edit + restart. flair#827: re-running `flair init`
9933
+ // used to regenerate ~/.flair/admin-pass unconditionally, desyncing it
9934
+ // from Harper's already-persisted credential and breaking admin auth on
9935
+ // the very re-run this remedy prescribed — resolveInitAdminPasswordSource
9936
+ // (see its doc comment) now reuses the existing password instead, so this
9937
+ // remedy is safe to follow on a working install.
9711
9938
  try {
9712
9939
  const harperConfigPath = join(defaultDataDir(), "harper-config.yaml");
9713
9940
  if (existsSync(harperConfigPath)) {
@@ -9716,7 +9943,7 @@ program
9716
9943
  const bind = detectOpsApiAllInterfacesBind(opsPortValue);
9717
9944
  if (bind.allInterfaces) {
9718
9945
  console.log(` ${render.icons.error} Ops API bound to ${render.wrap(render.c.bold, "all interfaces")} (${render.wrap(render.c.dim, String(opsPortValue))})`);
9719
- console.log(` ${render.wrap(render.c.dim, "Single-host installs don't need this reachable off-box. Fix:")} flair init ${render.wrap(render.c.dim, "(rebinds to loopback + domain socket on next start; pass --ops-bind for deliberate remote admin)")}`);
9946
+ console.log(` ${render.wrap(render.c.dim, "Single-host installs don't need this reachable off-box. Fix:")} flair init && flair restart ${render.wrap(render.c.dim, "(rebinds to loopback + domain socket; re-init reuses your existing admin password, so this is safe on a running install — pass --ops-bind for deliberate remote admin)")}`);
9720
9947
  issues++;
9721
9948
  }
9722
9949
  }
@@ -12082,7 +12309,7 @@ soul.command("list")
12082
12309
  // Slice 1: discovery + scaffold. Slice 2: YAML runtime + `import` for Shape A
12083
12310
  // + agentic-stack reference adapter as a built-in.
12084
12311
  // `test` and `export` are still stubbed; Shape B (npm code plugins) too.
12085
- // See specs/FLAIR-BRIDGES.md.
12312
+ // See docs/bridges.md.
12086
12313
  const bridge = program.command("bridge").description("Manage memory bridges (import/export between Flair and foreign systems)");
12087
12314
  bridge
12088
12315
  .command("list")
@@ -13762,6 +13989,6 @@ if (import.meta.main) {
13762
13989
  await runCli();
13763
13990
  }
13764
13991
  // ─── Exported for testing ─────────────────────────────────────────────────────
13765
- 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,
13766
13993
  // launchd label (flair#693)
13767
13994
  LEGACY_LAUNCHD_LABEL, launchdLabel, launchdPlistPath, resolveLaunchdLabel, migrateLegacyLaunchdLabel, ensureLaunchdServiceLoaded, };