@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.
- package/README.md +1 -1
- package/dist/bridges/builtins/index.js +1 -1
- package/dist/bridges/discover.js +1 -1
- package/dist/bridges/runtime/load-plugin.js +1 -1
- package/dist/bridges/scaffold.js +3 -3
- package/dist/bridges/types.js +1 -1
- package/dist/cli-shim.cjs +1 -1
- package/dist/cli.js +357 -130
- package/dist/deploy.js +29 -19
- package/dist/fabric-upgrade.js +61 -23
- package/dist/fleet-verify.js +3 -3
- package/dist/lib/mcp-enable.js +2 -2
- package/dist/rem/runner.js +2 -2
- package/dist/rem/scheduler.js +226 -17
- package/dist/resources/A2AAdapter.js +1 -1
- package/dist/resources/Admin.js +1 -1
- package/dist/resources/AdminConnectors.js +1 -1
- package/dist/resources/AdminDashboard.js +1 -1
- package/dist/resources/AdminIdp.js +1 -1
- package/dist/resources/AdminInstance.js +1 -1
- package/dist/resources/AdminMemory.js +1 -1
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +1 -1
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +1 -1
- package/dist/resources/Credential.js +1 -1
- package/dist/resources/Federation.js +1 -1
- package/dist/resources/Instance.js +1 -1
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +1 -1
- package/dist/resources/Memory.js +1 -1
- package/dist/resources/MemoryBootstrap.js +1 -1
- package/dist/resources/MemoryCandidate.js +168 -0
- package/dist/resources/MemoryConsolidate.js +1 -1
- package/dist/resources/MemoryDedupStats.js +1 -1
- package/dist/resources/MemoryFeed.js +1 -1
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryMaintenance.js +1 -1
- package/dist/resources/MemoryReflect.js +3 -3
- package/dist/resources/MemoryReindex.js +1 -1
- package/dist/resources/MemoryUsage.js +1 -1
- package/dist/resources/OAuth.js +1 -1
- package/dist/resources/OAuthClient.js +1 -1
- package/dist/resources/OrgEvent.js +1 -1
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/OrgEventMaintenance.js +1 -1
- package/dist/resources/PairingToken.js +1 -1
- package/dist/resources/Peer.js +1 -1
- package/dist/resources/Presence.js +3 -3
- package/dist/resources/RecordUsage.js +16 -2
- package/dist/resources/Relationship.js +1 -1
- package/dist/resources/SemanticSearch.js +1 -1
- package/dist/resources/SkillScan.js +1 -1
- package/dist/resources/Soul.js +1 -1
- package/dist/resources/SoulFeed.js +1 -1
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +1 -1
- package/dist/resources/XAA.js +1 -1
- package/dist/resources/a2a-url.js +1 -1
- package/dist/resources/agent-auth.js +1 -1
- package/dist/resources/agentcard-fields.js +1 -1
- package/dist/resources/auth-middleware.js +1 -1
- package/dist/resources/embeddings-boot.js +4 -4
- package/dist/resources/embeddings-provider.js +13 -43
- package/dist/resources/federation-cleanup.js +5 -5
- package/dist/resources/federation-nonce-store.js +5 -5
- package/dist/resources/health.js +29 -6
- package/dist/resources/instance-identity.js +1 -1
- package/dist/resources/mcp-client-metadata-fields.js +1 -1
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-oauth.js +2 -2
- package/dist/resources/mcp-tools.js +9 -1
- package/dist/resources/memory-read-scope.js +1 -1
- package/dist/resources/memory-reflect-lib.js +4 -4
- package/dist/resources/memory-visibility.js +3 -3
- package/dist/resources/migration-boot.js +1 -1
- package/dist/resources/migrations/embedding-stamp.js +3 -3
- package/dist/resources/migrations/graph-heal.js +2 -2
- package/dist/resources/migrations/ledger.js +1 -1
- package/dist/resources/migrations/registry.js +14 -5
- package/dist/resources/migrations/runner.js +64 -11
- package/dist/resources/migrations/synthetic-test-migration.js +1 -1
- package/dist/resources/migrations/visibility-backfill.js +232 -0
- package/dist/resources/models-dir.js +55 -0
- package/dist/resources/record-types.js +30 -2
- package/dist/resources/rerank-provider.js +24 -4
- package/dist/resources/semantic-retrieval-core.js +1 -1
- package/dist/resources/usage-recording.js +73 -7
- package/dist/resources/version.js +33 -0
- package/dist/src/rem/scheduler.js +449 -0
- package/docs/assets/flair-cross-orchestrator.cast +1 -1
- package/docs/bridges.md +3 -3
- package/docs/deployment.md +1 -0
- package/docs/mcp-clients.md +3 -1
- package/docs/n8n.md +1 -2
- package/docs/notes/rem-ux.md +1 -1
- package/docs/rem.md +2 -0
- package/docs/supply-chain-policy.md +4 -4
- package/docs/upgrade.md +2 -2
- package/package.json +4 -4
- 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 >
|
|
324
|
-
// env >
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// the
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 (
|
|
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
|
-
* (
|
|
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:
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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
|
-
|
|
2169
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
2404
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
if (
|
|
6342
|
-
|
|
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
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
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 +
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
8578
|
-
//
|
|
8579
|
-
//
|
|
8580
|
-
//
|
|
8581
|
-
//
|
|
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
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, };
|