@tpsdev-ai/flair 0.41.0 → 0.44.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/config.yaml +19 -90
- package/dist/cli.js +152 -48
- package/dist/doctor-client.js +14 -2
- package/dist/hook-install.js +1 -1
- package/dist/install/clients.js +53 -6
- package/dist/lib/mcp-enable.js +140 -44
- package/docs/deploying-on-fabric.md +40 -7
- package/docs/hosted-on-fabric.md +30 -1
- package/docs/mcp-clients.md +1 -1
- package/docs/quickstart.md +2 -0
- package/docs/troubleshooting.md +11 -2
- package/package.json +1 -1
package/config.yaml
CHANGED
|
@@ -1,100 +1,29 @@
|
|
|
1
1
|
name: flair
|
|
2
2
|
rest: true
|
|
3
|
-
|
|
4
|
-
## Port is configured via CLI (flair init --port) or HTTP_PORT env var.
|
|
5
|
-
## Omitted here to avoid conflicts with different deployment scenarios.
|
|
6
|
-
# http:
|
|
7
|
-
# port: 19926
|
|
8
|
-
|
|
9
|
-
# Harper does not read a component's `.env` implicitly — it only loads env
|
|
10
|
-
# files a component ASKS for, via this plugin. Without this block a `.env`
|
|
11
|
-
# sitting next to config.yaml is inert: the file is present and its values
|
|
12
|
-
# never reach `process.env`. That is exactly what a deployed instance hit —
|
|
13
|
-
# `FLAIR_PUBLIC_URL` was set in the deployed component's `.env` and OAuth
|
|
14
|
-
# discovery kept advertising a loopback issuer (flair#1005, #1000).
|
|
15
|
-
#
|
|
16
|
-
# MUST STAY FIRST. Config keys are iterated in file order by Harper's
|
|
17
|
-
# component loader, and each plugin's initial entry load is awaited before
|
|
18
|
-
# the next key is processed — so declaring this above `jsResource` is what
|
|
19
|
-
# guarantees `process.env` is populated before `dist/resources/*.js` are
|
|
20
|
-
# imported. Most consumers read `process.env` per request and would not care
|
|
21
|
-
# (resources/OAuth.ts, resources/AdminInstance.ts, resources/XAA.ts,
|
|
22
|
-
# resources/a2a-url.ts), but `resources/mcp-oauth.ts` decides at MODULE LOAD
|
|
23
|
-
# whether to mount `/mcp`; move this below `jsResource` and that decision is
|
|
24
|
-
# made against an env that has not been loaded yet.
|
|
25
|
-
#
|
|
26
|
-
# No `.env` is required, which is the case for essentially every local
|
|
27
|
-
# install: when the glob matches nothing the plugin never fires and emits
|
|
28
|
-
# nothing. Measured — a boot log with this block and no `.env` differs from
|
|
29
|
-
# one without the block only in the PID and in non-deterministic table-init
|
|
30
|
-
# ordering. (A MALFORMED declaration is loud, not silent: a pattern
|
|
31
|
-
# containing '..' produced both an `Ignoring invalid loadEnv files pattern`
|
|
32
|
-
# warning and a `Could not load component 'loadEnv'` error, which is the
|
|
33
|
-
# positive control for that silence.)
|
|
34
|
-
#
|
|
35
|
-
# Application variables only. Harper composes its OWN configuration before
|
|
36
|
-
# component `.env` files load, so Harper-level settings cannot be set this
|
|
37
|
-
# way; `HARPER_CONFIG` / `HARPER_DEFAULT_CONFIG` / `HARPER_SET_CONFIG` are
|
|
38
|
-
# refused at the injection point and warned about (harper#1513). Those
|
|
39
|
-
# belong in the process environment or harper-config.yaml.
|
|
40
3
|
loadEnv:
|
|
41
|
-
files:
|
|
42
|
-
|
|
4
|
+
files: .env
|
|
43
5
|
graphqlSchema:
|
|
44
6
|
files: schemas/*.graphql
|
|
45
|
-
|
|
46
7
|
jsResource:
|
|
47
8
|
files: dist/resources/*.js
|
|
48
|
-
|
|
49
|
-
# Phase 1 (flair#504): embeddings now run through Harper's native
|
|
50
|
-
# models.embed() facade, backed by harper-fabric-embeddings registered as the
|
|
51
|
-
# `embedding` backend. That registration is NOT configured here, and (as of
|
|
52
|
-
# flair#694) is no longer config-driven anywhere: a `models:` block in THIS
|
|
53
|
-
# file would silently never be read (this application always loads with
|
|
54
|
-
# `isRoot: false` — components/componentLoader.ts gates `bootstrapModels()`
|
|
55
|
-
# on `isRoot`), and the earlier fix for that — reasserting the block into the
|
|
56
|
-
# Harper INSTANCE-ROOT config via the HARPER_CONFIG env var on every spawn —
|
|
57
|
-
# turned out to PERSIST that block into harper-config.yaml, which an
|
|
58
|
-
# older/downgraded build's boot (never having set the env var) would tear
|
|
59
|
-
# down to an invalid empty shell and refuse to boot against (flair#694; see
|
|
60
|
-
# flair#695 for the invariant this violated). The registration now happens
|
|
61
|
-
# in-process instead: `dist/resources/embeddings-boot.js` (built from
|
|
62
|
-
# resources/embeddings-boot.ts, loaded by the `jsResource` glob below like
|
|
63
|
-
# every other file under resources/) calls harper-fabric-embeddings'
|
|
64
|
-
# `register()` factory directly on every boot — nothing is ever written to
|
|
65
|
-
# the config file, so there is nothing for a downgrade to trip over. This
|
|
66
|
-
# also means a `package:`-style sub-component entry
|
|
67
|
-
# (`'harper-fabric-embeddings': { package: ... }`, which used to sit here and
|
|
68
|
-
# drove `handleApplication`) still must not be reintroduced alongside it —
|
|
69
|
-
# that hook populates a SEPARATE raw-API engine, not `models.embed()`, and
|
|
70
|
-
# running both would double-init (two separate EmbeddingEngine instances:
|
|
71
|
-
# one unused, one backing models.embed).
|
|
72
|
-
|
|
73
9
|
authentication:
|
|
74
|
-
# Default secure (flair#654): a credential-less loopback request to the
|
|
75
|
-
# Harper ops API (:9925) is no longer auto-authorized as super_user. Local
|
|
76
|
-
# admin operations now require a real credential — ~/.flair/admin-pass
|
|
77
|
-
# (written by `flair init`), --admin-pass, or FLAIR_ADMIN_PASS. flair's own
|
|
78
|
-
# application-layer resources were already immune to this forgery (#655's
|
|
79
|
-
# credential-evidence gate); this closes the remaining gap below it, in the
|
|
80
|
-
# raw Harper ops API itself. Set true only for local development, at your
|
|
81
|
-
# own risk.
|
|
82
10
|
authorizeLocal: false
|
|
83
11
|
enableSessions: true
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
12
|
+
'@harperfast/oauth':
|
|
13
|
+
package: '@harperfast/oauth'
|
|
14
|
+
providers:
|
|
15
|
+
github:
|
|
16
|
+
clientId: ${OAUTH_GITHUB_CLIENT_ID}
|
|
17
|
+
clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}
|
|
18
|
+
mcp:
|
|
19
|
+
enabled: false
|
|
20
|
+
issuer: ${FLAIR_MCP_ISSUER}
|
|
21
|
+
resource: ${FLAIR_MCP_ISSUER}/mcp
|
|
22
|
+
accessTokenTtl: 900
|
|
23
|
+
dynamicClientRegistration:
|
|
24
|
+
enabled: false
|
|
25
|
+
clientIdMetadataDocuments:
|
|
26
|
+
allowedHosts:
|
|
27
|
+
- claude.ai
|
|
28
|
+
- claude.com
|
|
29
|
+
signingKeyPem: ${FLAIR_MCP_SIGNING_KEY_PEM}
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { checkServerHandshake, formatHandshakeNudge, invalidateHandshakeCache }
|
|
|
19
19
|
import { probeInstance } from "./probe.js";
|
|
20
20
|
import { sweepFleet, renderFleetSweepTable, FLEET_EXIT_OK, } from "./fleet-verify.js";
|
|
21
21
|
import { markStale, sortOldestVersionFirst } from "./fleet-presence.js";
|
|
22
|
-
import { detectClients, renderWiringSummary, wireClaudeCode, wireCodex, wireGemini, wireCursor } from "./install/clients.js";
|
|
22
|
+
import { detectClients, renderWiringSummary, wireClaudeCode, wireCodex, wireGemini, wireCursor, clientConfigPath, codexConfigHasFlairSection } from "./install/clients.js";
|
|
23
23
|
import { flairCliVersion, mcpServerSpec, unpinnedSpecWarning } from "./lib/mcp-spec.js";
|
|
24
24
|
import { resolveAgentKeyPath, loadEd25519PrivateKeyFromFile, signClientAssertion, buildTokenRequestForm, getMcpAccessToken, McpTokenRequestError, defaultMcpClientId, defaultMcpTokenEndpoint, defaultMcpResource, defaultMcpIssuer, MAX_ASSERTION_LIFETIME_SECONDS, } from "./mcp-client-assertion.js";
|
|
25
25
|
import { enableMcp, disableMcp, mcpStatus, checkLocalOriginRefusal, selfVerifyMcpMetadata, } from "./lib/mcp-enable.js";
|
|
@@ -136,6 +136,7 @@ function shouldShowInlineSecretWarning(optValue, fromEnv, secretFlagNames, flagN
|
|
|
136
136
|
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
137
137
|
const DEFAULT_PORT = 19926;
|
|
138
138
|
const DEFAULT_OPS_PORT = 19925;
|
|
139
|
+
const FABRIC_OPS_PORT = 9925;
|
|
139
140
|
const DEFAULT_ADMIN_USER = "admin";
|
|
140
141
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
141
142
|
const HEALTH_POLL_INTERVAL_MS = 500;
|
|
@@ -1076,10 +1077,15 @@ function resolveOpsTarget(opts) {
|
|
|
1076
1077
|
return opts.opsTarget || process.env.FLAIR_OPS_TARGET || undefined;
|
|
1077
1078
|
}
|
|
1078
1079
|
/** Derive the ops API URL from a Flair base URL.
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1080
|
+
* https with effective port 443 (no explicit port, or explicit :443): returns
|
|
1081
|
+
* <host>:9925 (FABRIC_OPS_PORT) — the Fabric managed case where port-1/:442
|
|
1082
|
+
* is a dead-end.
|
|
1083
|
+
* All other cases unchanged:
|
|
1084
|
+
* https with non-443 explicit port → port-1 (self-hosted TLS: 19926→19925, 8443→8442)
|
|
1085
|
+
* http with explicit port → port-1 (19926→19925)
|
|
1086
|
+
* http with no port → DEFAULT_OPS_PORT (19925)
|
|
1087
|
+
* Bare hosts are normalised to https:// (effective-443 → Fabric path).
|
|
1088
|
+
* Throws on unparseable URLs or out-of-range ports.
|
|
1083
1089
|
*/
|
|
1084
1090
|
/** Compute the effective ops API URL for remote commands.
|
|
1085
1091
|
* - If --ops-target is set, use it directly (no derivation).
|
|
@@ -1099,6 +1105,17 @@ function resolveOpsUrlFromTarget(targetUrl) {
|
|
|
1099
1105
|
// Normalise bare hosts: add https:// prefix so URL parser can handle them.
|
|
1100
1106
|
const normalised = targetUrl.includes("://") ? targetUrl : `https://${targetUrl}`;
|
|
1101
1107
|
const url = new URL(normalised);
|
|
1108
|
+
// https target with effective port 443 (no explicit port, or explicit :443):
|
|
1109
|
+
// this is the Fabric managed case — the ops API is on the well-known Fabric
|
|
1110
|
+
// ops port, never REST-adjacent. The port-1 / :442 logic is a dead-end here.
|
|
1111
|
+
if (url.protocol === "https:" && (url.port === "" || url.port === "443")) {
|
|
1112
|
+
url.port = String(FABRIC_OPS_PORT);
|
|
1113
|
+
return url.toString().replace(/\/$/, "");
|
|
1114
|
+
}
|
|
1115
|
+
// All other cases: unchanged port-1 convention.
|
|
1116
|
+
// https with non-443 explicit port → port-1 (self-hosted TLS: 19926→19925, 8443→8442)
|
|
1117
|
+
// http with explicit port → port-1 (19926→19925)
|
|
1118
|
+
// http with no port → DEFAULT_OPS_PORT (19925)
|
|
1102
1119
|
const port = parseInt(url.port, 10);
|
|
1103
1120
|
if (!isNaN(port) && port > 0 && port <= 65535) {
|
|
1104
1121
|
const opsPort = port - 1;
|
|
@@ -1111,13 +1128,8 @@ function resolveOpsUrlFromTarget(targetUrl) {
|
|
|
1111
1128
|
if (url.port !== "" && url.port !== undefined) {
|
|
1112
1129
|
throw new Error(`Invalid target port: ${url.port} (must be 1-65535)`);
|
|
1113
1130
|
}
|
|
1114
|
-
// No explicit port —
|
|
1115
|
-
|
|
1116
|
-
url.port = "442";
|
|
1117
|
-
}
|
|
1118
|
-
else {
|
|
1119
|
-
url.port = String(DEFAULT_OPS_PORT);
|
|
1120
|
-
}
|
|
1131
|
+
// No explicit port on http — use the default ops port.
|
|
1132
|
+
url.port = String(DEFAULT_OPS_PORT);
|
|
1121
1133
|
return url.toString().replace(/\/$/, "");
|
|
1122
1134
|
}
|
|
1123
1135
|
/**
|
|
@@ -1340,11 +1352,9 @@ function b64url(bytes) {
|
|
|
1340
1352
|
* out of scope for this HTTP/REST auth path.
|
|
1341
1353
|
*/
|
|
1342
1354
|
async function api(method, path, body, options) {
|
|
1343
|
-
// Resolve port
|
|
1344
|
-
//
|
|
1345
|
-
const
|
|
1346
|
-
const defaultUrl = savedPort ? `http://127.0.0.1:${savedPort}` : `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
1347
|
-
const base = options?.baseUrl ?? (process.env.FLAIR_URL || defaultUrl);
|
|
1355
|
+
// Resolve port via the canonical path (flair#1129): options.baseUrl > FLAIR_URL > resolveHttpPort.
|
|
1356
|
+
// api() callers mean the default install, so resolveHttpPort({}) with no --data-dir is correct.
|
|
1357
|
+
const base = options?.baseUrl ?? (process.env.FLAIR_URL || `http://127.0.0.1:${resolveHttpPort({})}`);
|
|
1348
1358
|
// Extract agentId from FLAIR_AGENT_ID env, or the body (POST/PUT) / URL
|
|
1349
1359
|
// query params (GET) — Harper-CLI-request-shape knowledge, not a generic
|
|
1350
1360
|
// auth concern, so it stays here rather than in authedRequest.
|
|
@@ -1784,12 +1794,21 @@ export async function seedFederationInstanceViaOpsApi(opsPortOrUrl, instanceId,
|
|
|
1784
1794
|
export async function callOpsApi(opsUrl, body, user, pass) {
|
|
1785
1795
|
const url = `${opsUrl.replace(/\/$/, "")}/`;
|
|
1786
1796
|
const auth = Buffer.from(`${user}:${pass}`).toString("base64");
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1797
|
+
let res;
|
|
1798
|
+
try {
|
|
1799
|
+
res = await fetch(url, {
|
|
1800
|
+
method: "POST",
|
|
1801
|
+
headers: { "Content-Type": "application/json", ...(auth ? { Authorization: `Basic ${auth}` } : {}) },
|
|
1802
|
+
body: JSON.stringify(body),
|
|
1803
|
+
signal: AbortSignal.timeout(30_000),
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
catch (err) {
|
|
1807
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1808
|
+
throw new Error(`ops API unreachable at ${opsUrl} (derived from --target). ` +
|
|
1809
|
+
`Set --ops-target or FLAIR_OPS_TARGET to override. ` +
|
|
1810
|
+
`(${message})`);
|
|
1811
|
+
}
|
|
1793
1812
|
if (!res.ok) {
|
|
1794
1813
|
const text = await res.text().catch(() => "");
|
|
1795
1814
|
throw new Error(`Ops API call failed (${res.status}): ${text}`);
|
|
@@ -3356,7 +3375,13 @@ program
|
|
|
3356
3375
|
? JSON.parse(readFileSync(claudeJsonPath, "utf-8"))
|
|
3357
3376
|
: {};
|
|
3358
3377
|
const existing = claudeJson.mcpServers?.flair;
|
|
3359
|
-
|
|
3378
|
+
const currentSpec = mcpServerSpec();
|
|
3379
|
+
const existingArgs = existing?.args;
|
|
3380
|
+
const argsMatch = Array.isArray(existingArgs) && existingArgs.includes(currentSpec);
|
|
3381
|
+
const urlAgentMatch = existing && existing.env?.FLAIR_URL === httpUrl && existing.env?.FLAIR_AGENT_ID === agentId;
|
|
3382
|
+
// flair#1135: the pin in `args` must match the current mcpServerSpec().
|
|
3383
|
+
// A matching pin stays a no-op (idempotent); only a stale pin triggers a re-write.
|
|
3384
|
+
if (urlAgentMatch && argsMatch) {
|
|
3360
3385
|
console.log(` ✓ Claude Code already wired in ~/.claude.json`);
|
|
3361
3386
|
wiringResults.push({ client: "claude-code", message: "already wired", wired: true });
|
|
3362
3387
|
}
|
|
@@ -3364,11 +3389,15 @@ program
|
|
|
3364
3389
|
claudeJson.mcpServers = claudeJson.mcpServers || {};
|
|
3365
3390
|
claudeJson.mcpServers.flair = flairMcpConfig;
|
|
3366
3391
|
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3392
|
+
const action = urlAgentMatch ? "refreshed pin in ~/.claude.json"
|
|
3393
|
+
: claudeJsonExisted ? "wired in ~/.claude.json"
|
|
3394
|
+
: "wired in ~/.claude.json (created)";
|
|
3395
|
+
console.log(` ✓ Claude Code ${action} (restart Claude Code to pick it up)`);
|
|
3369
3396
|
wiringResults.push({
|
|
3370
3397
|
client: "claude-code",
|
|
3371
|
-
message:
|
|
3398
|
+
message: urlAgentMatch ? "refreshed pin in ~/.claude.json"
|
|
3399
|
+
: claudeJsonExisted ? "wired ~/.claude.json"
|
|
3400
|
+
: "created and wired ~/.claude.json",
|
|
3372
3401
|
wired: true,
|
|
3373
3402
|
});
|
|
3374
3403
|
}
|
|
@@ -4811,7 +4840,7 @@ mcp
|
|
|
4811
4840
|
const adminPass = dryRun ? (opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "") : resolveLocalAdminPass(opts.adminPass, /* isRemoteTarget */ true);
|
|
4812
4841
|
if (!dryRun && !adminPass) {
|
|
4813
4842
|
console.error("Error: --admin-pass <pass> or --admin-pass-file <path> is required for a REMOTE target " +
|
|
4814
|
-
"(the operations API on the target instance needs it for identity mapping +
|
|
4843
|
+
"(the operations API on the target instance needs it for identity mapping + restart).\n" +
|
|
4815
4844
|
" FLAIR_ADMIN_PASS and ~/.flair/admin-pass are deliberately NOT used here: they are THIS machine's " +
|
|
4816
4845
|
"local admin credentials, and sending them to another instance is how a local secret ends up on someone " +
|
|
4817
4846
|
"else's Harper. Pass the target's own admin password explicitly.");
|
|
@@ -4865,7 +4894,20 @@ mcp
|
|
|
4865
4894
|
process.exit(1);
|
|
4866
4895
|
}
|
|
4867
4896
|
if (!result.ok) {
|
|
4868
|
-
|
|
4897
|
+
if (result.failedStep === "fabric-operator-deploy") {
|
|
4898
|
+
// flair#1136: Fabric deployments require the operator to deploy the
|
|
4899
|
+
// config change — we can't write to harperdb-config.yaml (Fabric
|
|
4900
|
+
// regenerates it on every container restart).
|
|
4901
|
+
console.error(`\n${render.icons.info} ${render.wrap(render.c.bold, "Fabric deployment detected.")}`);
|
|
4902
|
+
console.error(` The @harperfast/oauth block ships in your component config.yaml with mcp.enabled: false.`);
|
|
4903
|
+
console.error(` To activate: set mcp.enabled: true (literal boolean) in your deployed component`);
|
|
4904
|
+
console.error(` config.yaml, ensure the staged secrets are live in the instance's process`);
|
|
4905
|
+
console.error(` environment, and redeploy. Then re-run \`flair mcp enable\` — earlier steps`);
|
|
4906
|
+
console.error(` are idempotent and will be reused.\n`);
|
|
4907
|
+
}
|
|
4908
|
+
else {
|
|
4909
|
+
console.error(`${render.icons.error} enable failed at step "${result.failedStep}" — see detail above for the exact fix, then re-run \`flair mcp enable\` (earlier steps are idempotent and will be reused).`);
|
|
4910
|
+
}
|
|
4869
4911
|
process.exit(1);
|
|
4870
4912
|
}
|
|
4871
4913
|
if (result.dryRun) {
|
|
@@ -9866,6 +9908,55 @@ program
|
|
|
9866
9908
|
authedGet: (path) => verifyAuthedGet(baseUrl, path, defaultKeysDir()),
|
|
9867
9909
|
});
|
|
9868
9910
|
const verdict = decideAfterVerify(verify, previousFlairVersion);
|
|
9911
|
+
// ── Refresh wired MCP client configs (flair#1135) ──────────────────────
|
|
9912
|
+
// After a successful upgrade, the flair-mcp package on disk is newer than
|
|
9913
|
+
// the pinned version in wired client configs. Re-run wiring for
|
|
9914
|
+
// already-wired clients so the pin stays in lockstep with the installed
|
|
9915
|
+
// version. Best-effort: failures warn but never fail the upgrade.
|
|
9916
|
+
const refreshWiredClients = async () => {
|
|
9917
|
+
const agentId = resolveAgentIdOrEnv({}) ?? (() => {
|
|
9918
|
+
try {
|
|
9919
|
+
const keyFiles = readdirSync(defaultKeysDir()).filter((f) => f.endsWith(".key"));
|
|
9920
|
+
return keyFiles.length > 0 ? keyFiles[0].replace(/\.key$/, "") : null;
|
|
9921
|
+
}
|
|
9922
|
+
catch {
|
|
9923
|
+
return null;
|
|
9924
|
+
}
|
|
9925
|
+
})();
|
|
9926
|
+
if (!agentId) {
|
|
9927
|
+
console.log("\n (no agent id known — skip MCP client pin refresh; run `flair init` to refresh manually)");
|
|
9928
|
+
return;
|
|
9929
|
+
}
|
|
9930
|
+
const httpUrl = `http://127.0.0.1:${upgradePort}`;
|
|
9931
|
+
const mcpEnv = { FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl };
|
|
9932
|
+
const detected = detectClients().filter(c => c.detected);
|
|
9933
|
+
if (detected.length === 0)
|
|
9934
|
+
return;
|
|
9935
|
+
console.log("\n Refreshing MCP client pins...");
|
|
9936
|
+
for (const client of detected) {
|
|
9937
|
+
const configPath = clientConfigPath(client.id);
|
|
9938
|
+
if (!existsSync(configPath))
|
|
9939
|
+
continue;
|
|
9940
|
+
// Only refresh clients that are already wired — don't wire new ones.
|
|
9941
|
+
let hasFlair = false;
|
|
9942
|
+
try {
|
|
9943
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
9944
|
+
if (client.id === "codex") {
|
|
9945
|
+
hasFlair = codexConfigHasFlairSection(raw);
|
|
9946
|
+
}
|
|
9947
|
+
else {
|
|
9948
|
+
const cfg = JSON.parse(raw);
|
|
9949
|
+
hasFlair = !!cfg.mcpServers?.flair;
|
|
9950
|
+
}
|
|
9951
|
+
}
|
|
9952
|
+
catch { /* unreadable/malformed — skip */ }
|
|
9953
|
+
if (!hasFlair)
|
|
9954
|
+
continue;
|
|
9955
|
+
const env = { ...mcpEnv, FLAIR_CLIENT: client.id };
|
|
9956
|
+
const result = client.wire(env);
|
|
9957
|
+
console.log(` ${result.ok ? "✓" : "•"} ${result.message}`);
|
|
9958
|
+
}
|
|
9959
|
+
};
|
|
9869
9960
|
if (verdict.kind === "ok") {
|
|
9870
9961
|
// flair#1022: the verified facts are unchanged and still stated — the
|
|
9871
9962
|
// upgrade did land. What changes is the MARKER and the claim around it.
|
|
@@ -9881,6 +9972,7 @@ program
|
|
|
9881
9972
|
else
|
|
9882
9973
|
console.log(line);
|
|
9883
9974
|
}
|
|
9975
|
+
await refreshWiredClients();
|
|
9884
9976
|
return;
|
|
9885
9977
|
}
|
|
9886
9978
|
// flair#741 follow-through: a healthy instance the verifier just couldn't
|
|
@@ -9907,6 +9999,7 @@ program
|
|
|
9907
9999
|
console.error(line);
|
|
9908
10000
|
}
|
|
9909
10001
|
}
|
|
10002
|
+
await refreshWiredClients();
|
|
9910
10003
|
return;
|
|
9911
10004
|
}
|
|
9912
10005
|
console.error(`❌ post-restart verification failed: ${verdict.reason}`);
|
|
@@ -12013,22 +12106,34 @@ program
|
|
|
12013
12106
|
const hook = inspectSessionStartHook(homedir());
|
|
12014
12107
|
if (hook.present) {
|
|
12015
12108
|
if (hook.execution === "broken") {
|
|
12016
|
-
//
|
|
12017
|
-
//
|
|
12018
|
-
//
|
|
12019
|
-
// cold
|
|
12020
|
-
//
|
|
12021
|
-
//
|
|
12022
|
-
//
|
|
12023
|
-
// the
|
|
12024
|
-
//
|
|
12025
|
-
//
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12109
|
+
// Two very different states that share one probe outcome:
|
|
12110
|
+
//
|
|
12111
|
+
// 1. Silenced (current) command that didn't run — the npx cache
|
|
12112
|
+
// is cold, the machine is offline, or the adapter hasn't been
|
|
12113
|
+
// fetched yet. On a fresh install this is NORMAL: the hook is
|
|
12114
|
+
// wired but no Claude Code session has exercised it yet.
|
|
12115
|
+
// Report as informational, not a warning, and never suggest
|
|
12116
|
+
// reinstall — the setup is correct, the environment just
|
|
12117
|
+
// hasn't warmed yet.
|
|
12118
|
+
//
|
|
12119
|
+
// 2. Unsilenced (legacy) command that didn't run — the hook has
|
|
12120
|
+
// been in place long enough that a cold cache is not the
|
|
12121
|
+
// explanation. This IS a genuine failure: warn and name the
|
|
12122
|
+
// actual state with a fitting remedy.
|
|
12123
|
+
if (hook.silenced) {
|
|
12124
|
+
console.log(` ${render.icons.ok} SessionStart hook: wired in ${render.wrap(render.c.dim, hook.path)} — not yet exercised`);
|
|
12125
|
+
console.log(` ${render.wrap(render.c.dim, hook.detail ?? "")}`);
|
|
12126
|
+
console.log(` ${render.wrap(render.c.dim, "The hook is correctly wired but the adapter has not been fetched yet.")}`);
|
|
12127
|
+
console.log(` ${render.wrap(render.c.dim, "This is normal on a fresh install — the first Claude Code session will warm the npx cache.")}`);
|
|
12128
|
+
}
|
|
12129
|
+
else {
|
|
12130
|
+
console.log(` ${render.icons.warn} SessionStart hook: wired in ${render.wrap(render.c.dim, hook.path)}, but its command did not run just now`);
|
|
12131
|
+
console.log(` ${render.wrap(render.c.dim, hook.detail ?? "")}`);
|
|
12132
|
+
console.log(` ${render.wrap(render.c.dim, "The hook command could not be executed. Check that npx can resolve")}`);
|
|
12133
|
+
console.log(` ${render.wrap(render.c.dim, "@tpsdev-ai/flair-mcp — a cold cache, missing global install, or network")}`);
|
|
12134
|
+
console.log(` ${render.wrap(render.c.dim, "issue can prevent the adapter from running on its first invocation.")}`);
|
|
12135
|
+
console.log(` ${render.wrap(render.c.dim, "Fix:")} flair doctor --fix ${render.wrap(render.c.dim, "(rewrites the hook to the current silent-failure form)")}`);
|
|
12136
|
+
}
|
|
12032
12137
|
}
|
|
12033
12138
|
else if (hook.execution === "unknown") {
|
|
12034
12139
|
console.log(` ${render.icons.warn} SessionStart hook: wired in ${render.wrap(render.c.dim, hook.path)}, but could not be verified ${render.wrap(render.c.dim, `(${hook.detail ?? "no detail"})`)}`);
|
|
@@ -15508,8 +15613,7 @@ program
|
|
|
15508
15613
|
if (!dryRun) {
|
|
15509
15614
|
console.log(` Writing to Flair...`);
|
|
15510
15615
|
try {
|
|
15511
|
-
const
|
|
15512
|
-
const httpUrl = `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
15616
|
+
const httpUrl = `http://127.0.0.1:${resolveHttpPort({})}`;
|
|
15513
15617
|
const agentKeyId = `${agentId}.key`;
|
|
15514
15618
|
const keysDir = join(homedir(), ".flair", "keys");
|
|
15515
15619
|
const keyPath = join(keysDir, agentKeyId);
|
|
@@ -15987,7 +16091,7 @@ if (import.meta.main) {
|
|
|
15987
16091
|
// ─── Exported for testing ─────────────────────────────────────────────────────
|
|
15988
16092
|
export { runCli, resolveKeyPath, buildEd25519Auth, readPortFromConfig, readOpsBindFromConfig, readOpsPortFromConfig, writeConfig, resolveHttpPort, resolveOpsPort, resolveOpsBindHost,
|
|
15989
16093
|
// Harper's own config — the per-instance port record (flair#914)
|
|
15990
|
-
harperConfigPath, readHarperConfig, readPortFromHarperConfig, persistDefaultInstallCoordinates, 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,
|
|
16094
|
+
harperConfigPath, readHarperConfig, readPortFromHarperConfig, persistDefaultInstallCoordinates, resolveTarget, resolveOpsTarget, resolveEffectiveOpsUrl, resolveOpsUrlFromTarget, FABRIC_OPS_PORT, 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,
|
|
15991
16095
|
// launchd label (flair#693)
|
|
15992
16096
|
LEGACY_LAUNCHD_LABEL, launchdLabel, launchdPlistPath, cleanupLegacyLaunchdPlist, resolveLaunchdLabel, migrateLegacyLaunchdLabel, ensureLaunchdServiceLoaded,
|
|
15993
16097
|
// launchd management observation (flair#1022)
|
package/dist/doctor-client.js
CHANGED
|
@@ -41,7 +41,7 @@ export const SESSION_START_HOOK_MARKER = "flair-session-start";
|
|
|
41
41
|
//
|
|
42
42
|
// WHY THE INVOCATION IS WRAPPED
|
|
43
43
|
// -----------------------------
|
|
44
|
-
// The hook runs `npx -y @tpsdev-ai/flair-mcp flair-session-start`: it resolves
|
|
44
|
+
// The hook runs `npx -y -p @tpsdev-ai/flair-mcp flair-session-start`: it resolves
|
|
45
45
|
// a package binary through whatever Node runtime the user's shell happens to
|
|
46
46
|
// expose. Under a Node version manager, globally installed packages are
|
|
47
47
|
// per-runtime-version, so a routine and entirely unrelated runtime upgrade
|
|
@@ -117,7 +117,7 @@ export function buildSessionStartHookCommand(agentId, flairUrl) {
|
|
|
117
117
|
throw new Error(`Flair URL '${flairUrl}' contains characters that cannot be safely written into a shell hook command (allowed: letters, digits, . _ : / -)`);
|
|
118
118
|
}
|
|
119
119
|
const env = flairUrl ? `FLAIR_AGENT_ID=${agentId} FLAIR_URL=${flairUrl}` : `FLAIR_AGENT_ID=${agentId}`;
|
|
120
|
-
const invocation = `${env} npx -y @tpsdev-ai/flair-mcp ${SESSION_START_HOOK_MARKER}`;
|
|
120
|
+
const invocation = `${env} npx -y -p @tpsdev-ai/flair-mcp ${SESSION_START_HOOK_MARKER}`;
|
|
121
121
|
return `sh -c 'out=$(${invocation} 2>/dev/null) && printf %s "$out" || true'`;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
@@ -525,6 +525,18 @@ export function inspectSessionStartHook(homeDir, opts = {}) {
|
|
|
525
525
|
const verdict = classifyHookProbe(outcome);
|
|
526
526
|
return { path: found.path, present: true, command, ours, silenced, upgradable, execution: verdict.execution, detail: verdict.detail };
|
|
527
527
|
}
|
|
528
|
+
export function classifyHookReadiness(report) {
|
|
529
|
+
if (!report.present)
|
|
530
|
+
return "absent";
|
|
531
|
+
if (!report.ours)
|
|
532
|
+
return "custom";
|
|
533
|
+
if (report.execution === "runs")
|
|
534
|
+
return "runs";
|
|
535
|
+
if (report.execution === "broken") {
|
|
536
|
+
return report.silenced ? "not-yet-exercised" : "genuinely-broken";
|
|
537
|
+
}
|
|
538
|
+
return "unverified";
|
|
539
|
+
}
|
|
528
540
|
/**
|
|
529
541
|
* Rewrite an existing Flair-authored hook command to the current canonical
|
|
530
542
|
* form, in place, preserving the agent id and URL the entry already carries —
|
package/dist/hook-install.js
CHANGED
|
@@ -344,7 +344,7 @@ export function hookStatus(homeDir, harness) {
|
|
|
344
344
|
}
|
|
345
345
|
const hookEntry = config.hooks.SessionStart[existing.groupIndex].hooks[existing.hookIndex];
|
|
346
346
|
const command = typeof hookEntry?.command === "string" ? hookEntry.command : "";
|
|
347
|
-
const correctShape = hookEntry?.type === "command" && command.includes(`npx -y @tpsdev-ai/flair-mcp ${SESSION_START_HOOK_MARKER}`);
|
|
347
|
+
const correctShape = hookEntry?.type === "command" && command.includes(`npx -y -p @tpsdev-ai/flair-mcp ${SESSION_START_HOOK_MARKER}`);
|
|
348
348
|
const env = parseHookCommandEnv(command);
|
|
349
349
|
return {
|
|
350
350
|
harness, path, wired: true, correctShape,
|
package/dist/install/clients.js
CHANGED
|
@@ -165,6 +165,40 @@ export function appendCodexFlairBlock(raw, env) {
|
|
|
165
165
|
const separator = raw.length === 0 ? "" : raw.endsWith("\n\n") ? "" : raw.endsWith("\n") ? "\n" : "\n\n";
|
|
166
166
|
return raw + separator + tomlSnippet(env) + "\n";
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* flair#1135: does the existing `[mcp_servers.flair]` TOML section carry the
|
|
170
|
+
* CURRENT pinned mcpServerSpec()? Pure string scan — no TOML parser needed
|
|
171
|
+
* (same rationale as codexConfigHasFlairSection).
|
|
172
|
+
*/
|
|
173
|
+
function codexFlairSectionHasCurrentPin(raw) {
|
|
174
|
+
const idx = raw.indexOf("[mcp_servers.flair]");
|
|
175
|
+
if (idx === -1)
|
|
176
|
+
return false;
|
|
177
|
+
const after = raw.slice(idx);
|
|
178
|
+
// Find the end of the section: the next top-level [header] that is NOT a
|
|
179
|
+
// sub-table of mcp_servers.flair (e.g. [mcp_servers.flair.env] is part of
|
|
180
|
+
// the same logical section and must not terminate the scan).
|
|
181
|
+
const nextHeader = after.slice("[mcp_servers.flair]".length).search(/\n\[(?!mcp_servers\.flair\.)/);
|
|
182
|
+
const section = nextHeader === -1 ? after : after.slice(0, "[mcp_servers.flair]".length + nextHeader);
|
|
183
|
+
return section.includes(mcpServerSpec());
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* flair#1135: replace the existing `[mcp_servers.flair]` TOML section with a
|
|
187
|
+
* fresh one carrying the current pin. Preserves everything else in the file.
|
|
188
|
+
*/
|
|
189
|
+
function replaceCodexFlairBlock(raw, env) {
|
|
190
|
+
const idx = raw.indexOf("[mcp_servers.flair]");
|
|
191
|
+
if (idx === -1)
|
|
192
|
+
return appendCodexFlairBlock(raw, env);
|
|
193
|
+
const before = raw.slice(0, idx);
|
|
194
|
+
const after = raw.slice(idx);
|
|
195
|
+
const nextHeader = after.slice("[mcp_servers.flair]".length).search(/\n\[(?!mcp_servers\.flair\.)/);
|
|
196
|
+
const rest = nextHeader === -1 ? "" : after.slice("[mcp_servers.flair]".length + nextHeader);
|
|
197
|
+
const newBlock = tomlSnippet(env) + "\n";
|
|
198
|
+
// Preserve the separator between the new block and whatever follows.
|
|
199
|
+
const sep = rest.length === 0 ? "" : rest.startsWith("\n") ? "" : "\n";
|
|
200
|
+
return before + newBlock + sep + rest;
|
|
201
|
+
}
|
|
168
202
|
/**
|
|
169
203
|
* Merge the Flair MCP server into a JSON config file with an `mcpServers` map.
|
|
170
204
|
* Creates the file (and parent dir) if absent; preserves existing servers and
|
|
@@ -182,13 +216,20 @@ function wireJsonMcp(configPath, label, env) {
|
|
|
182
216
|
}
|
|
183
217
|
config.mcpServers = config.mcpServers || {};
|
|
184
218
|
const existing = config.mcpServers.flair;
|
|
185
|
-
|
|
219
|
+
const currentSpec = mcpServerSpec();
|
|
220
|
+
const existingArgs = existing?.args;
|
|
221
|
+
const argsMatch = Array.isArray(existingArgs) && existingArgs.includes(currentSpec);
|
|
222
|
+
const urlAgentMatch = existing && existing.env?.FLAIR_URL === env.FLAIR_URL && existing.env?.FLAIR_AGENT_ID === env.FLAIR_AGENT_ID;
|
|
223
|
+
// flair#1135: the pin in `args` must match the current mcpServerSpec().
|
|
224
|
+
// A matching pin stays a no-op (idempotent); only a stale pin triggers a re-write.
|
|
225
|
+
if (urlAgentMatch && argsMatch) {
|
|
186
226
|
return { ok: true, message: `${label}: already wired in ${display}` };
|
|
187
227
|
}
|
|
188
228
|
config.mcpServers.flair = flairMcpEntry(env);
|
|
189
229
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
190
230
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
191
|
-
|
|
231
|
+
const action = urlAgentMatch ? "refreshed pin in" : "wired";
|
|
232
|
+
return { ok: true, message: `${label}: ${action} ${display} (restart ${label} to pick it up)` };
|
|
192
233
|
}
|
|
193
234
|
catch (err) {
|
|
194
235
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -250,17 +291,23 @@ function _wireCodex(env) {
|
|
|
250
291
|
// parser, but appending a new top-level table at EOF is safe TOML when the
|
|
251
292
|
// exact header isn't already present (flair#727) — so an existing file only
|
|
252
293
|
// forces the manual-print fallback when it's genuinely unreadable/
|
|
253
|
-
// unwritable (permissions, I/O error), never merely "exists".
|
|
254
|
-
//
|
|
255
|
-
//
|
|
294
|
+
// unwritable (permissions, I/O error), never merely "exists".
|
|
295
|
+
//
|
|
296
|
+
// flair#1135: the "already wired" check is now version-aware — a section
|
|
297
|
+
// with a stale pin triggers a re-write instead of a no-op.
|
|
256
298
|
const path = codexConfigPath();
|
|
257
299
|
const display = "~/.codex/config.toml";
|
|
258
300
|
try {
|
|
259
301
|
if (existsSync(path)) {
|
|
260
302
|
const raw = readFileSync(path, "utf-8");
|
|
261
|
-
if (
|
|
303
|
+
if (codexFlairSectionHasCurrentPin(raw)) {
|
|
262
304
|
return { ok: true, message: `Codex: already wired in ${display}` };
|
|
263
305
|
}
|
|
306
|
+
if (codexConfigHasFlairSection(raw)) {
|
|
307
|
+
// Section exists but pin is stale — replace it.
|
|
308
|
+
writeFileSync(path, replaceCodexFlairBlock(raw, env));
|
|
309
|
+
return { ok: true, message: `Codex: refreshed pin in ${display} (restart Codex to pick it up)` };
|
|
310
|
+
}
|
|
264
311
|
writeFileSync(path, appendCodexFlairBlock(raw, env));
|
|
265
312
|
return { ok: true, message: `Codex: wired ${display} (restart Codex to pick it up)` };
|
|
266
313
|
}
|
package/dist/lib/mcp-enable.js
CHANGED
|
@@ -144,6 +144,7 @@ import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from "n
|
|
|
144
144
|
import { homedir } from "node:os";
|
|
145
145
|
import { join, dirname } from "node:path";
|
|
146
146
|
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
147
|
+
import yaml from "js-yaml";
|
|
147
148
|
// ─── CIMD constants ──────────────────────────────────────────────────────────
|
|
148
149
|
/** Default `clientIdMetadataDocuments.allowedHosts` allowlist — see the
|
|
149
150
|
* module header's "claude.ai CIMD/redirect-URI allowlist hosts" note for
|
|
@@ -285,10 +286,12 @@ export function readSigningKeyFile(path) {
|
|
|
285
286
|
/**
|
|
286
287
|
* The `@harperfast/oauth` config block, matching the installed 2.2.0
|
|
287
288
|
* package's field names (node_modules/@harperfast/oauth/dist/types.d.ts).
|
|
288
|
-
* Secrets are `${ENV_VAR}` placeholders — never literal values
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
289
|
+
* Secrets are `${ENV_VAR}` placeholders — never literal values.
|
|
290
|
+
*
|
|
291
|
+
* flair#1136: set_configuration delivery was removed. Fabric regenerates
|
|
292
|
+
* the root harperdb-config.yaml; the component's own config.yaml is the
|
|
293
|
+
* source of truth for the oauth block. This function builds the block that
|
|
294
|
+
* ships in config.yaml — it is never written to harperdb-config.yaml.
|
|
292
295
|
*
|
|
293
296
|
* flair#756: `dynamicClientRegistration: { enabled: false }` is written
|
|
294
297
|
* EXPLICITLY — never omitted. See the module header's "Leaving
|
|
@@ -303,6 +306,7 @@ export function buildMcpOAuthConfigBlock(params) {
|
|
|
303
306
|
const provider = params.idpProvider;
|
|
304
307
|
const envPrefix = `OAUTH_${provider.toUpperCase()}`;
|
|
305
308
|
const cimdAllowedHosts = params.cimdAllowedHosts ?? DEFAULT_CIMD_ALLOWED_HOSTS;
|
|
309
|
+
const enabled = params.enabled ?? true;
|
|
306
310
|
return {
|
|
307
311
|
"@harperfast/oauth": {
|
|
308
312
|
package: "@harperfast/oauth",
|
|
@@ -313,7 +317,7 @@ export function buildMcpOAuthConfigBlock(params) {
|
|
|
313
317
|
},
|
|
314
318
|
},
|
|
315
319
|
mcp: {
|
|
316
|
-
enabled
|
|
320
|
+
enabled,
|
|
317
321
|
issuer: "${FLAIR_MCP_ISSUER}",
|
|
318
322
|
resource: "${FLAIR_MCP_ISSUER}/mcp",
|
|
319
323
|
accessTokenTtl: REQUIRED_ACCESS_TOKEN_TTL,
|
|
@@ -331,6 +335,85 @@ export function buildMcpOAuthConfigBlock(params) {
|
|
|
331
335
|
},
|
|
332
336
|
};
|
|
333
337
|
}
|
|
338
|
+
// ─── Local config.yaml update (flair#1136) ──────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* Flip mcp.enabled in a local component config.yaml. Best-effort: returns
|
|
341
|
+
* `{ ok: false }` with a reason when the file can't be found or parsed.
|
|
342
|
+
*
|
|
343
|
+
* Looks for config.yaml at `explicitPath`, then `./config.yaml`, then
|
|
344
|
+
* `~/.flair/config.yaml`. When found, replaces `mcp:\n enabled: false`
|
|
345
|
+
* with `mcp:\n enabled: true` (exact string match — avoids a YAML parser
|
|
346
|
+
* dependency for a single boolean flip).
|
|
347
|
+
*/
|
|
348
|
+
export function updateLocalConfigMcpEnabled(enabled, explicitPath) {
|
|
349
|
+
const candidates = explicitPath
|
|
350
|
+
? [explicitPath]
|
|
351
|
+
: ["config.yaml", join(homedir(), ".flair", "config.yaml")];
|
|
352
|
+
let configPath = null;
|
|
353
|
+
for (const p of candidates) {
|
|
354
|
+
if (existsSync(p)) {
|
|
355
|
+
configPath = p;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!configPath) {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
detail: `local config.yaml not found (tried: ${candidates.join(", ")}). ` +
|
|
363
|
+
`Set mcp.enabled: ${enabled} in your component config.yaml manually, then restart.`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
let raw;
|
|
367
|
+
try {
|
|
368
|
+
raw = readFileSync(configPath, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
return { ok: false, detail: `cannot read ${configPath}: ${err.message}` };
|
|
372
|
+
}
|
|
373
|
+
// Parse the YAML to navigate to the exact key — avoids the ambiguity of
|
|
374
|
+
// string-matching `enabled:` when the block has multiple enabled keys
|
|
375
|
+
// (mcp.enabled vs dynamicClientRegistration.enabled).
|
|
376
|
+
let doc;
|
|
377
|
+
try {
|
|
378
|
+
doc = yaml.load(raw);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return { ok: false, detail: `cannot parse ${configPath} as YAML: ${err.message}` };
|
|
382
|
+
}
|
|
383
|
+
if (!doc || typeof doc !== "object") {
|
|
384
|
+
return { ok: false, detail: `${configPath} is empty or not a YAML mapping` };
|
|
385
|
+
}
|
|
386
|
+
const oauth = doc["@harperfast/oauth"];
|
|
387
|
+
if (!oauth || typeof oauth !== "object") {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
detail: `@harperfast/oauth block not found in ${configPath}. ` +
|
|
391
|
+
`Ensure the component block is present with mcp.enabled: ${enabled}.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const mcp = oauth.mcp;
|
|
395
|
+
if (!mcp || typeof mcp !== "object") {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
detail: `mcp key not found under @harperfast/oauth in ${configPath}. ` +
|
|
399
|
+
`Ensure the mcp block is present with enabled: ${enabled}.`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const current = mcp.enabled;
|
|
403
|
+
if (current === enabled) {
|
|
404
|
+
return { ok: true, detail: `mcp.enabled already ${enabled} in ${configPath}` };
|
|
405
|
+
}
|
|
406
|
+
// Mutate the parsed document and re-emit.
|
|
407
|
+
mcp.enabled = enabled;
|
|
408
|
+
const updated = yaml.dump(doc, { lineWidth: -1, noCompatMode: true });
|
|
409
|
+
try {
|
|
410
|
+
writeFileSync(configPath, updated, { encoding: "utf-8" });
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
return { ok: false, detail: `cannot write ${configPath}: ${err.message}` };
|
|
414
|
+
}
|
|
415
|
+
return { ok: true, detail: `mcp.enabled set to ${enabled} in ${configPath}` };
|
|
416
|
+
}
|
|
334
417
|
/** The exact callback URL to hand the operator when they create the IdP
|
|
335
418
|
* OAuth app ("with the exact GitHub callback URL printed"). */
|
|
336
419
|
export function idpCallbackUrl(issuer, idpProvider) {
|
|
@@ -553,32 +636,7 @@ export async function provisionIdpIdentityMapping(params, deps = {}) {
|
|
|
553
636
|
}
|
|
554
637
|
return { principalCreated, credentialId, credentialReused: Boolean(existing) };
|
|
555
638
|
}
|
|
556
|
-
|
|
557
|
-
* (whole-process restart) — the genuine Harper Operations API operations
|
|
558
|
-
* this module's header documents. Throws on either non-2xx response. */
|
|
559
|
-
export async function applyRemoteConfigAndRestart(params, deps = {}) {
|
|
560
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
561
|
-
const opsUrl = opsBaseUrl(params.opsPortOrUrl);
|
|
562
|
-
const authHeader = basicAuthHeader(params.adminUser, params.adminPass);
|
|
563
|
-
const setRes = await fetchImpl(opsUrl, {
|
|
564
|
-
method: "POST",
|
|
565
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
566
|
-
body: JSON.stringify({ operation: "set_configuration", ...params.configBlock }),
|
|
567
|
-
});
|
|
568
|
-
if (!setRes.ok) {
|
|
569
|
-
const text = await setRes.text().catch(() => "");
|
|
570
|
-
throw new Error(`set_configuration failed (HTTP ${setRes.status}): ${text}`);
|
|
571
|
-
}
|
|
572
|
-
const restartRes = await fetchImpl(opsUrl, {
|
|
573
|
-
method: "POST",
|
|
574
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
575
|
-
body: JSON.stringify({ operation: "restart" }),
|
|
576
|
-
});
|
|
577
|
-
if (!restartRes.ok) {
|
|
578
|
-
const text = await restartRes.text().catch(() => "");
|
|
579
|
-
throw new Error(`restart failed (HTTP ${restartRes.status}): ${text}`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
639
|
+
// ─── Restart only ────────────────────────────────────────────────────────────
|
|
582
640
|
/** `restart` only — used by `disableMcp` (flag off + restart, no config
|
|
583
641
|
* rewrite: the `@harperfast/oauth` config block is left in place; it is
|
|
584
642
|
* inert whenever `FLAIR_MCP_OAUTH` is unset, per the byte-identical-boot
|
|
@@ -871,11 +929,13 @@ export async function enableMcp(params, deps = {}) {
|
|
|
871
929
|
currentStep = "signing-key";
|
|
872
930
|
const keyResult = ensureSigningKeyFile(params.signingKeyFilePath, { generate: deps.generateRsaKeyPair });
|
|
873
931
|
push(true, `signing key ${keyResult.reused ? "reused" : "generated"} at ${keyResult.path} (0600)`);
|
|
874
|
-
// ── @harperfast/oauth config
|
|
932
|
+
// ── @harperfast/oauth config (flair#1136: shipped in config.yaml) ──────
|
|
933
|
+
// The block ships uncommented with mcp.enabled: false (inert default).
|
|
934
|
+
// set_configuration is removed — the block lives in the component's own
|
|
935
|
+
// config.yaml, not in harperdb-config.yaml where Fabric would wipe it.
|
|
875
936
|
const cimdAllowedHosts = params.cimdAllowedHosts ?? DEFAULT_CIMD_ALLOWED_HOSTS;
|
|
876
937
|
currentStep = "config-block";
|
|
877
|
-
|
|
878
|
-
push(true, `built the @harperfast/oauth mcp config block (accessTokenTtl=${REQUIRED_ACCESS_TOKEN_TTL}, ` +
|
|
938
|
+
push(true, `@harperfast/oauth config ships in config.yaml (mcp.enabled=false, ` +
|
|
879
939
|
`dynamicClientRegistration.enabled=false, clientIdMetadataDocuments.allowedHosts=${JSON.stringify(cimdAllowedHosts)})`);
|
|
880
940
|
// ── IdP OAuth-app credential intake ───────────────────────────────────────
|
|
881
941
|
currentStep = "idp-credentials";
|
|
@@ -973,18 +1033,54 @@ export async function enableMcp(params, deps = {}) {
|
|
|
973
1033
|
}
|
|
974
1034
|
if (!confirmed) {
|
|
975
1035
|
push(false, `not applied: pass --confirm-secrets-applied once the staged secrets are live on ${params.instance}, then re-run \`flair mcp enable\` (earlier steps are idempotent and will reuse what's already provisioned).`);
|
|
976
|
-
return { ok: false, dryRun, steps, failedStep: "
|
|
1036
|
+
return { ok: false, dryRun, steps, failedStep: "secrets-provisioning", secretsMechanism: secretsResult.mechanism, secretsPath: secretsResult.path };
|
|
977
1037
|
}
|
|
978
|
-
|
|
979
|
-
//
|
|
1038
|
+
// ── flair#1136: config delivery is now SHIPPED in config.yaml ────────────
|
|
1039
|
+
// The @harperfast/oauth block ships uncommented with mcp.enabled: false
|
|
1040
|
+
// (inert default). set_configuration is REMOVED — Fabric regenerates
|
|
1041
|
+
// harperdb-config.yaml on every container restart, so writing the block
|
|
1042
|
+
// there was always a race against the next deploy. Instead:
|
|
1043
|
+
//
|
|
1044
|
+
// - Standalone-local: flip mcp.enabled to true in the local config.yaml,
|
|
1045
|
+
// restart, self-verify.
|
|
1046
|
+
// - Fabric: the operator must set mcp.enabled: true in their deployed
|
|
1047
|
+
// component config.yaml. Report the requirement LOUDLY — never report
|
|
1048
|
+
// success with /mcp still dark.
|
|
1049
|
+
const isFabric = isFabricOrigin(params.instance);
|
|
1050
|
+
if (isFabric) {
|
|
1051
|
+
// ── Fabric: operator-deploy requirement ──────────────────────────────
|
|
1052
|
+
currentStep = "fabric-operator-deploy";
|
|
1053
|
+
const msg = [
|
|
1054
|
+
`Fabric deployment detected (${new URL(params.instance).hostname}).`,
|
|
1055
|
+
`The @harperfast/oauth block ships in config.yaml with mcp.enabled: false.`,
|
|
1056
|
+
`To activate: set mcp.enabled: true (literal boolean) in your deployed component config.yaml,`,
|
|
1057
|
+
`ensure the staged secrets are live in the instance's process environment, and redeploy.`,
|
|
1058
|
+
`Then re-run \`flair mcp enable\` — earlier steps are idempotent and will be reused.`,
|
|
1059
|
+
].join(" ");
|
|
1060
|
+
push(false, msg);
|
|
1061
|
+
return {
|
|
1062
|
+
ok: false,
|
|
1063
|
+
dryRun,
|
|
1064
|
+
steps,
|
|
1065
|
+
failedStep: "fabric-operator-deploy",
|
|
1066
|
+
issuer,
|
|
1067
|
+
resource: `${issuer}/mcp`,
|
|
1068
|
+
secretsMechanism: secretsResult.mechanism,
|
|
1069
|
+
secretsPath: secretsResult.path,
|
|
1070
|
+
signingKeyFilePath: keyResult.path,
|
|
1071
|
+
callbackUrl,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
// ── Standalone (non-Fabric): update local config + restart ────────────
|
|
1075
|
+
currentStep = "local-config-update";
|
|
1076
|
+
const localConfigResult = updateLocalConfigMcpEnabled(true, params.localConfigPath);
|
|
1077
|
+
push(localConfigResult.ok, localConfigResult.detail);
|
|
1078
|
+
// ── Restart ───────────────────────────────────────────────────────────
|
|
1079
|
+
currentStep = "restart";
|
|
980
1080
|
const preDiscriminator = await captureBootDiscriminator(params.instance, params.adminUser, params.adminPass, { fetchImpl: deps.fetchImpl });
|
|
981
|
-
await
|
|
982
|
-
push(true, `
|
|
1081
|
+
await triggerRemoteRestart(params.instance, params.adminUser, params.adminPass, { fetchImpl: deps.fetchImpl });
|
|
1082
|
+
push(true, `restart triggered against ${params.instance}`);
|
|
983
1083
|
// ── Verify the process actually restarted (flair#1120) ──────────────────
|
|
984
|
-
// Poll the ops API until the PID changes — the old process can briefly
|
|
985
|
-
// still answer after a real restart, so a single post-capture is unreliable.
|
|
986
|
-
// waitForOpsApi guarantees PID change (or throws on timeout), so the restart
|
|
987
|
-
// is confirmed when this call returns.
|
|
988
1084
|
currentStep = "verify-restart";
|
|
989
1085
|
const postDiscriminator = await waitForOpsApi(resolveOpsUrl(params.instance), basicAuthHeader(params.adminUser, params.adminPass), preDiscriminator.pid, {
|
|
990
1086
|
fetchImpl: deps.fetchImpl,
|
|
@@ -996,7 +1092,7 @@ export async function enableMcp(params, deps = {}) {
|
|
|
996
1092
|
currentStep = "self-verify";
|
|
997
1093
|
const verify = await selfVerifyMcpMetadata(issuer, { fetchImpl: deps.fetchImpl });
|
|
998
1094
|
if (!verify.ok) {
|
|
999
|
-
push(false, `${verify.detail} — re-run \`flair mcp status\` to check current state, or \`flair mcp enable\` to retry
|
|
1095
|
+
push(false, `${verify.detail} — re-run \`flair mcp status\` to check current state, or \`flair mcp enable\` to retry.`);
|
|
1000
1096
|
return {
|
|
1001
1097
|
ok: false,
|
|
1002
1098
|
dryRun,
|
|
@@ -129,14 +129,17 @@ where nothing answers:
|
|
|
129
129
|
# --target https://<fabric-node>:19926/<instance> → ops derived as :19925 ✓
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
**Fabric's ops API runs on the same hostname at port 9925** <!-- docs-freshness-allow: Fabric ops API port, not legacy data port --> (the deploy/upgrade path already targets this port). For a managed `*.harperfabric.com` instance:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Same hostname, port 9925 — not port 442 <!-- docs-freshness-allow: Fabric ops API -->
|
|
136
|
+
flair init --target https://<cluster>.<org>.harperfabric.com \
|
|
137
|
+
--ops-target https://<cluster>.<org>.harperfabric.com:9925 <!-- docs-freshness-allow: Fabric ops API -->
|
|
138
|
+
```
|
|
139
|
+
|
|
132
140
|
**Pass `--ops-target <url>` explicitly** (or set `FLAIR_OPS_TARGET`) on any command that
|
|
133
141
|
touches the ops API: `init --target`, `agent add --target`, `federation token --target`.
|
|
134
142
|
|
|
135
|
-
> **Gap — needs a Fabric account to verify.** This guide does not state the correct
|
|
136
|
-
> ops-API URL for a managed `*.harperfabric.com` instance, or whether the ops API is
|
|
137
|
-
> reachable remotely there at all. The derivation above is certain (read from source);
|
|
138
|
-
> the right value to pass is not.
|
|
139
|
-
|
|
140
143
|
Precedence: `--target` > `--url` > `FLAIR_TARGET` > `FLAIR_URL` > localhost. For ops:
|
|
141
144
|
`--ops-target` > `FLAIR_OPS_TARGET` > derived > localhost.
|
|
142
145
|
|
|
@@ -250,8 +253,9 @@ and shells out to `lsof`. The command you'd reach for when something breaks is u
|
|
|
250
253
|
here. Unavailable too: `start`, `stop`, `restart`, `snapshot`, `reembed`, `rem`, `bridge`.
|
|
251
254
|
|
|
252
255
|
**Fabric's own cluster topology is invisible.** `fleet verify` sweeps *Flair's* federation
|
|
253
|
-
peer table, not Harper's cluster nodes
|
|
254
|
-
|
|
256
|
+
peer table, not Harper's cluster nodes. **`cluster_status` works on Fabric** — Fabric
|
|
257
|
+
always runs harper-pro (not the OSS harper build), so cluster_status is available over
|
|
258
|
+
the ops API. `0 peers known` means "0 on file", never "0 exist."
|
|
255
259
|
|
|
256
260
|
**There is no disk or quota telemetry.** `flair status` reports usage for two directories:
|
|
257
261
|
no free space, no total, no quota, no warning threshold, walk capped at six levels, no
|
|
@@ -263,6 +267,35 @@ with nothing saying so. The one indirect signal is a migration halting for space
|
|
|
263
267
|
> wouldn't matter: Flair calls it only as a post-failure convergence oracle and discards
|
|
264
268
|
> the `size` field. Component disk usage is invisible structurally.
|
|
265
269
|
|
|
270
|
+
### The `mcp.enabled` operator step
|
|
271
|
+
|
|
272
|
+
MCP is **off by default**. The shipped component `config.yaml` contains
|
|
273
|
+
`@harperfast/oauth` → `mcp` → `enabled: false`. Until [flair#1152](https://github.com/tpsdev-ai/flair/issues/1152)
|
|
274
|
+
lands (interpolate from env — *ON HOLD*), you must flip this manually:
|
|
275
|
+
|
|
276
|
+
1. In your deployed component's `config.yaml`, change:
|
|
277
|
+
```yaml
|
|
278
|
+
'@harperfast/oauth':
|
|
279
|
+
mcp:
|
|
280
|
+
enabled: true # was: false
|
|
281
|
+
```
|
|
282
|
+
2. Re-deploy the component so Harper picks up the new value.
|
|
283
|
+
3. **Verify the `/mcp` surface is actually serving** (the flag alone does not
|
|
284
|
+
guarantee it — a secret that is stored but never decrypted fails at self-verify):
|
|
285
|
+
```bash
|
|
286
|
+
# Check /mcp is reachable and returning MCP protocol (not a loopback proxy or 404)
|
|
287
|
+
curl -sf https://\<cluster\>.\<org\>.harperfabric.com/mcp
|
|
288
|
+
# Should return MCP JSON-RPC content; if you get HTML redirect or 404 the flag
|
|
289
|
+
# is not effective
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**⚠ SECURITY CAVEAT — the upgrade-reverts trap.** Any package update or fleet component
|
|
293
|
+
update re-ships the literal `enabled: false` and silently darkens a live `/mcp` surface.
|
|
294
|
+
You must **re-flip to `true` after every upgrade** and re-deploy. An updated component
|
|
295
|
+
without this re-flip will appear healthy (`/Health` green) while its MCP tools are
|
|
296
|
+
dark to every connected client. If you rely on MCP, add the re-flip to your upgrade
|
|
297
|
+
runbook.
|
|
298
|
+
|
|
266
299
|
### Known hazard: unbounded npm cache
|
|
267
300
|
|
|
268
301
|
**Open — [flair#886](https://github.com/tpsdev-ai/flair/issues/886).** Every deploy runs a
|
package/docs/hosted-on-fabric.md
CHANGED
|
@@ -79,6 +79,35 @@ The staging file is written in every case, so a fallback never strands you mid-r
|
|
|
79
79
|
|
|
80
80
|
`--secrets-mechanism <fabric-env-secrets|env-file>` remains an explicit override and skips the probe entirely.
|
|
81
81
|
|
|
82
|
+
### The `mcp.enabled` operator step (Fabric)
|
|
83
|
+
|
|
84
|
+
MCP is **off by default**. The shipped component `config.yaml` contains
|
|
85
|
+
`@harperfast/oauth` → `mcp` → `enabled: false`. Until [flair#1152](https://github.com/tpsdev-ai/flair/issues/1152)
|
|
86
|
+
lands (interpolate from env — *ON HOLD*), you must flip this manually:
|
|
87
|
+
|
|
88
|
+
1. In your deployed component's `config.yaml`, change:
|
|
89
|
+
```yaml
|
|
90
|
+
'@harperfast/oauth':
|
|
91
|
+
mcp:
|
|
92
|
+
enabled: true # was: false
|
|
93
|
+
```
|
|
94
|
+
2. Re-deploy the component so Harper picks up the new value.
|
|
95
|
+
3. **Verify the `/mcp` surface is actually serving** (the flag alone does not
|
|
96
|
+
guarantee it — a secret that is stored but never decrypted fails at self-verify):
|
|
97
|
+
```bash
|
|
98
|
+
# Check /mcp is reachable and returning MCP protocol (not a loopback proxy or 404)
|
|
99
|
+
curl -sf https://<cluster>.<org>.harperfabric.com/mcp
|
|
100
|
+
# Should return MCP JSON-RPC content; if you get HTML redirect or 404 the flag
|
|
101
|
+
# is not effective
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**⚠ SECURITY CAVEAT — the upgrade-reverts trap.** Any package update or fleet component
|
|
105
|
+
update re-ships the literal `enabled: false` and silently darkens a live `/mcp` surface.
|
|
106
|
+
You must **re-flip to `true` after every upgrade** and re-deploy. An updated component
|
|
107
|
+
without this re-flip will appear healthy (`/Health` green) while its MCP tools are
|
|
108
|
+
dark to every connected client. If you rely on MCP, add the re-flip to your upgrade
|
|
109
|
+
runbook.
|
|
110
|
+
|
|
82
111
|
---
|
|
83
112
|
|
|
84
113
|
## Agent authentication
|
|
@@ -140,7 +169,7 @@ flair fleet verify --target https://<cluster>.<org>.harperfabric.com
|
|
|
140
169
|
|
|
141
170
|
**`flair doctor`** takes no `--target` — it hardcodes localhost, reads a local PID file, and shells out to `lsof`. Unavailable too: `start`, `stop`, `restart`, `snapshot`, `reembed`, `rem`, `bridge`.
|
|
142
171
|
|
|
143
|
-
**Fabric's own cluster topology is invisible.** `fleet verify` sweeps *Flair's* federation peer table, not Harper's cluster nodes.
|
|
172
|
+
**Fabric's own cluster topology is invisible.** `fleet verify` sweeps *Flair's* federation peer table, not Harper's cluster nodes. **`cluster_status` works on Fabric** — Fabric always runs harper-pro (not the OSS harper build), so cluster_status is available over the ops API. `0 peers known` means "0 on file", never "0 exist."
|
|
144
173
|
|
|
145
174
|
---
|
|
146
175
|
|
package/docs/mcp-clients.md
CHANGED
|
@@ -109,7 +109,7 @@ Or wire it by hand — add a `SessionStart` hook to `~/.claude/settings.json`:
|
|
|
109
109
|
"hooks": [
|
|
110
110
|
{
|
|
111
111
|
"type": "command",
|
|
112
|
-
"command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y @tpsdev-ai/flair-mcp flair-session-start 2>/dev/null) && printf %s \"$out\" || true'"
|
|
112
|
+
"command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y -p @tpsdev-ai/flair-mcp flair-session-start 2>/dev/null) && printf %s \"$out\" || true'"
|
|
113
113
|
}
|
|
114
114
|
]
|
|
115
115
|
}
|
package/docs/quickstart.md
CHANGED
|
@@ -154,6 +154,8 @@ flair search --agent local "native addon loading in sandboxed runtimes"
|
|
|
154
154
|
|
|
155
155
|
You searched for a concept, not the keywords. The line under each hit is its creation date, durability tier, and rank score.
|
|
156
156
|
|
|
157
|
+
> **When stdout is not a terminal** — piped to another command, captured in a script, or run in CI — the same `flair search` command emits a JSON array instead of the formatted prose above. Each hit is an object with `id`, `text`, `createdAt`, `durability`, and `_score` fields. Add `--explain` to include an `_explain` ranking breakdown on each hit. Use `flair search --json` to force JSON output even in a terminal, or `flair memory search` for the raw JSON form in all contexts.
|
|
158
|
+
|
|
157
159
|
> The percentage is a **rank-fusion score, not a similarity**. It is normalized so the top result is always near 100%. Read it as ordering within these results, never as confidence that the match is good.
|
|
158
160
|
|
|
159
161
|
Add `--explain` to see the ranking inputs per hit — the raw score, the composite score under `--scoring composite`, and the record's durability, age and usage count. When output is JSON (`--json`, or any time stdout is not a terminal) the same breakdown arrives as an `_explain` object on each hit, so scripts get it too. Use `--limit`, `--tag`, `--since 7d` to narrow the search. `flair memory search` runs the same query but always prints raw JSON — use it when piping to a script.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -23,7 +23,8 @@ If it fails to start:
|
|
|
23
23
|
lsof -i :19926
|
|
24
24
|
|
|
25
25
|
# Check logs (macOS)
|
|
26
|
-
cat ~/.flair/data/log/hdb.log | tail -50
|
|
26
|
+
cat ~/.flair/data/log/hdb.log | tail -50 # Harper ≤ 5.1 only (see below)
|
|
27
|
+
cat ~/.flair/data/log/system.log | tail -50 # Harper 5.2+ (live log)
|
|
27
28
|
|
|
28
29
|
# Check logs (Linux)
|
|
29
30
|
journalctl --user -u flair --since "10 minutes ago"
|
|
@@ -242,6 +243,14 @@ flair --help # all commands
|
|
|
242
243
|
flair <command> -h # command-specific help
|
|
243
244
|
```
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
**Log paths depend on Harper version:**
|
|
247
|
+
|
|
248
|
+
- **Harper ≤ 5.1:** `~/.flair/data/log/hdb.log`
|
|
249
|
+
- **Harper 5.2+:** `~/.flair/data/log/system.log`
|
|
250
|
+
|
|
251
|
+
> ⚠ **On Harper 5.2+, `hdb.log` freezes at the upgrade boundary.** The live log is
|
|
252
|
+
> `system.log`. The Harper ops `read_log` endpoint keeps serving the frozen `hdb.log`,
|
|
253
|
+
> so remote diagnosis reads days-stale entries that look current. Always check
|
|
254
|
+
> `system.log` after upgrading to 5.2+.
|
|
246
255
|
|
|
247
256
|
File issues: [github.com/tpsdev-ai/flair/issues](https://github.com/tpsdev-ai/flair/issues)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"packageManager": "bun@1.3.10",
|
|
5
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
6
6
|
"type": "module",
|