@tpsdev-ai/flair 0.50.0 → 0.51.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 +15 -4
- package/dist/build-info.json +3 -3
- package/dist/cli.js +188 -67
- package/dist/doctor-client.js +3 -3
- package/dist/install/clients.js +28 -17
- package/dist/lib/doctor-run.js +481 -0
- package/dist/lib/launchd-management.js +7 -26
- package/dist/resources/Federation.js +42 -20
- package/dist/resources/RecordUsage.js +13 -6
- package/dist/resources/SemanticSearch.js +8 -1
- package/dist/resources/federation-classify.js +90 -0
- package/dist/resources/health.js +9 -10
- package/dist/resources/mcp-tools.js +9 -6
- package/dist/resources/search-readiness.js +33 -10
- package/dist/resources/semantic-retrieval-core.js +39 -20
- package/dist/resources/usage-ids.js +63 -0
- package/docs/federation.md +11 -0
- package/docs/supply-chain-policy.md +1 -1
- package/docs/upgrade.md +17 -1
- package/package.json +2 -2
- package/schemas/federation.graphql +1 -1
package/README.md
CHANGED
|
@@ -16,17 +16,28 @@ Runs on a laptop, a VPS, or anywhere Node does. Needs **Node.js 22+**.
|
|
|
16
16
|
# 1. Install the CLI (no sudo)
|
|
17
17
|
npm install -g @tpsdev-ai/flair
|
|
18
18
|
|
|
19
|
-
# 2.
|
|
19
|
+
# 2. Verify the command is on your PATH
|
|
20
|
+
flair --version
|
|
21
|
+
|
|
22
|
+
# 3. Bootstrap the instance and register an agent
|
|
20
23
|
flair init --agent mybot
|
|
21
24
|
|
|
22
|
-
#
|
|
25
|
+
# 4. Write a memory
|
|
23
26
|
flair memory add --agent mybot "Harper v5 sandbox blocks node:module but process.dlopen works"
|
|
24
27
|
|
|
25
|
-
#
|
|
28
|
+
# 5. Find it back by meaning, not by keyword
|
|
26
29
|
flair search --agent mybot "native addon loading in sandboxed runtimes"
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
If `flair --version` prints `command not found`, your npm global bin directory is not on `PATH`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export PATH="$(npm prefix -g)/bin:$PATH"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Re-run `flair --version` to confirm, then continue.
|
|
39
|
+
|
|
40
|
+
Step 5 finds the memory you never keyword-matched:
|
|
30
41
|
|
|
31
42
|
```
|
|
32
43
|
Harper v5 sandbox blocks node:module but process.dlopen works
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"commit": "
|
|
4
|
-
"builtAt": "2026-08-
|
|
2
|
+
"version": "0.51.0",
|
|
3
|
+
"commit": "92b5d12cc3aafe892e90af657f073ef88b180ba6",
|
|
4
|
+
"builtAt": "2026-08-30T16:10:24.457Z",
|
|
5
5
|
"builder": "tsc"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,8 @@ import { resolveSigningIdentity, emitSigningIdentityDebug, } from "./lib/signing
|
|
|
32
32
|
import { validateSnapshotArchive, extractSnapshotSafely } from "./lib/safe-snapshot-extract.js";
|
|
33
33
|
import { entityFormatHint, parseEntitiesCsv } from "./lib/entity-vocab-cli.js";
|
|
34
34
|
import { escapeXml, unescapeXml } from "./lib/xml-escape.js";
|
|
35
|
-
import { assessLaunchdManagement, diagnoseLaunchdPlistPaths, isDetached, pickInstancePid, renderDetachedWarning,
|
|
35
|
+
import { assessLaunchdManagement, diagnoseLaunchdPlistPaths, isDetached, pickInstancePid, renderDetachedWarning, LAUNCHCTL_QUERY_TIMEOUT_MS, } from "./lib/launchd-management.js";
|
|
36
|
+
import { applyUpgradeHookConsent, catalogIssueDelta, renderCatalogDoctorLines, renderVerifiedSummary, runDoctorChecks, } from "./lib/doctor-run.js";
|
|
36
37
|
// Value-only static import so `--interval`'s advertised default cannot drift
|
|
37
38
|
// from the one the scheduler actually validates against. The module itself is
|
|
38
39
|
// still loaded lazily at call time (the `await import()`s below) for the
|
|
@@ -60,14 +61,18 @@ function signBody(body, secretKey) {
|
|
|
60
61
|
const sig = nacl.sign.detached(message, secretKey);
|
|
61
62
|
return Buffer.from(sig).toString("base64url");
|
|
62
63
|
}
|
|
63
|
-
// Per-record principalId (federation-edge-hardening slice 3a
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
64
|
+
// Per-record principalId (federation-edge-hardening slice 3a / flair#1416).
|
|
65
|
+
// Sourced from the write-time provenance stamp (memory-provenance slice 1,
|
|
66
|
+
// Memory.ts's buildProvenance) when present. `provenance` is persisted as
|
|
67
|
+
// a JSON STRING (not an object), so it must be parsed — a raw
|
|
68
|
+
// `row.provenance?.verified?.agentId` would silently always be undefined.
|
|
69
|
+
// Soul/Agent/Relationship rows never carry a provenance stamp today, so
|
|
70
|
+
// this is a no-op for them (those tables are not principal-owning).
|
|
71
|
+
//
|
|
72
|
+
// As of v:2 this value is IN the signed payload. The receiver validates
|
|
73
|
+
// it against data.agentId for Memory (PRINCIPAL_OWNING_TABLES); it is
|
|
74
|
+
// no longer informational-only. Credential.principalId is an unrelated
|
|
75
|
+
// owner field — do not grep that path when changing this one.
|
|
71
76
|
function principalIdFromRow(row) {
|
|
72
77
|
if (typeof row?.provenance !== "string" || row.provenance.length === 0)
|
|
73
78
|
return undefined;
|
|
@@ -3002,7 +3007,7 @@ program
|
|
|
3002
3007
|
.option("--no-mcp", "Skip MCP client wiring (instance + agent only)")
|
|
3003
3008
|
.option("--skip-smoke", "Skip the MCP smoke test")
|
|
3004
3009
|
.option("--skip-claude-md", "Skip appending the Flair bootstrap line to CLAUDE.md (claude-code only)")
|
|
3005
|
-
.option("--skip-hook", "Skip installing the flair-session-start SessionStart hook (claude-code
|
|
3010
|
+
.option("--skip-hook", "Skip installing the flair-session-start SessionStart hook (claude-code and Codex)")
|
|
3006
3011
|
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET)")
|
|
3007
3012
|
.option("--remote", "When used with --target, init as hub for remote federation")
|
|
3008
3013
|
.option("--ops-target <url>", "Explicit ops API URL (env: FLAIR_OPS_TARGET; bypasses port derivation)")
|
|
@@ -3864,6 +3869,18 @@ program
|
|
|
3864
3869
|
}
|
|
3865
3870
|
wiringResults.push({ client: clientId, message: result.message, wired: result.ok });
|
|
3866
3871
|
console.log(` ${result.ok ? "✓" : "•"} ${result.message}`);
|
|
3872
|
+
// Codex SessionStart hook (flair#1148 / #1439) — the hook is not
|
|
3873
|
+
// optional on Codex (no CLAUDE.md alternative). Init is the
|
|
3874
|
+
// consent to set up the client, same as the Claude Code hook
|
|
3875
|
+
// applied above. --skip-hook opts out and prints the JSON.
|
|
3876
|
+
if (clientId === "codex" && result.ok) {
|
|
3877
|
+
const hookResult = applyOrReportSessionStartHook(homedir(), agentId, !!opts.skipHook, hookSettingsPath(homedir(), "codex"));
|
|
3878
|
+
console.log(` ${hookResult.ok ? "✓" : "•"} ${hookResult.message}`);
|
|
3879
|
+
if (hookResult.hint) {
|
|
3880
|
+
for (const line of hookResult.hint.split("\n"))
|
|
3881
|
+
console.log(` ${line}`);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3867
3884
|
}
|
|
3868
3885
|
}
|
|
3869
3886
|
}
|
|
@@ -7030,22 +7047,40 @@ export async function runFederationSyncOnce(opts) {
|
|
|
7030
7047
|
// batch. Closes the hub-relay forgery hole — see
|
|
7031
7048
|
// resources/Federation.ts FederationSync.post's verification gate.
|
|
7032
7049
|
//
|
|
7033
|
-
// CONTRACT — must match
|
|
7034
|
-
// byte-for-byte
|
|
7035
|
-
//
|
|
7036
|
-
//
|
|
7037
|
-
//
|
|
7038
|
-
// field set ever changes, so an old signature fails closed instead of
|
|
7039
|
-
// silently mis-verifying under a new form.
|
|
7050
|
+
// CONTRACT — must match reconstructRecordVerifyBody
|
|
7051
|
+
// (resources/federation-classify.ts) byte-for-byte. canonicalize()
|
|
7052
|
+
// sorts keys, so field ORDER doesn't matter, but the field SET and
|
|
7053
|
+
// values do. `v` versions the canonical form itself: a v:1
|
|
7054
|
+
// signature cannot verify as v:2 (principalId in the field set).
|
|
7040
7055
|
//
|
|
7041
|
-
//
|
|
7042
|
-
// `
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
//
|
|
7046
|
-
// receiver
|
|
7047
|
-
//
|
|
7056
|
+
// v: 2 puts principalId in the signed payload when the row carries
|
|
7057
|
+
// a provenance stamp, and puts `v` on the wire so Phase 1
|
|
7058
|
+
// receivers (`const v = record.v ?? 1`) don't default these
|
|
7059
|
+
// records back to 1. Soul/Agent/Relationship have no stamp and
|
|
7060
|
+
// omit principalId; Memory without a stamp also omits it (the
|
|
7061
|
+
// receiver then skips Memory as principal_mismatch — absent is
|
|
7062
|
+
// not an accept).
|
|
7048
7063
|
const principalId = principalIdFromRow(row);
|
|
7064
|
+
const signedPayload = {
|
|
7065
|
+
v: 2,
|
|
7066
|
+
table,
|
|
7067
|
+
id: row.id,
|
|
7068
|
+
data: row,
|
|
7069
|
+
updatedAt,
|
|
7070
|
+
originatorInstanceId,
|
|
7071
|
+
};
|
|
7072
|
+
if (principalId)
|
|
7073
|
+
signedPayload.principalId = principalId;
|
|
7074
|
+
const signature = signBody(signedPayload, secretKey);
|
|
7075
|
+
const sr = {
|
|
7076
|
+
v: 2,
|
|
7077
|
+
table,
|
|
7078
|
+
id: row.id,
|
|
7079
|
+
data: row,
|
|
7080
|
+
updatedAt,
|
|
7081
|
+
originatorInstanceId,
|
|
7082
|
+
signature,
|
|
7083
|
+
};
|
|
7049
7084
|
if (principalId)
|
|
7050
7085
|
sr.principalId = principalId;
|
|
7051
7086
|
const srBytes = JSON.stringify(sr).length;
|
|
@@ -10276,6 +10311,7 @@ program
|
|
|
10276
10311
|
.option("--project <name>", "Fabric component name for --target", "flair")
|
|
10277
10312
|
.option("--no-replicated", "Disable cluster-wide replication for --target (default: replicated=true)")
|
|
10278
10313
|
.option("--yes", "Skip the confirmation prompt for --target")
|
|
10314
|
+
.option("--install-hooks", "Consent to installing missing SessionStart hooks (claude-code / Codex) during upgrade. The hook executes at every session start — upgrade will not write it unprompted. Interactive runs prompt; non-interactive runs state the gap and withhold ✅ unless this flag is passed.")
|
|
10279
10315
|
.option("--no-fleet-verify", "Skip the automatic post-upgrade fleet convergence sweep for --target (default: sweep runs — see flair#636)")
|
|
10280
10316
|
// ── flair#878 ─────────────────────────────────────────────────────────────
|
|
10281
10317
|
// These existed on `flair deploy` but stopped at the upgrade boundary, so
|
|
@@ -10941,20 +10977,16 @@ program
|
|
|
10941
10977
|
});
|
|
10942
10978
|
const verdict = decideAfterVerify(verify, previousFlairVersion);
|
|
10943
10979
|
if (verdict.kind === "ok") {
|
|
10944
|
-
// flair#
|
|
10945
|
-
//
|
|
10946
|
-
//
|
|
10947
|
-
//
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
console.error(line);
|
|
10955
|
-
else
|
|
10956
|
-
console.log(line);
|
|
10957
|
-
}
|
|
10980
|
+
// flair#1439: the success marker is the doctor runner's verdict, not
|
|
10981
|
+
// a second, narrower notion of "verified". Launchd detach is one
|
|
10982
|
+
// catalog member; the Codex SessionStart hook is another. Adding a
|
|
10983
|
+
// doctor check widens this claim automatically.
|
|
10984
|
+
const run = await doctorRunAfterUpgrade({
|
|
10985
|
+
management,
|
|
10986
|
+
port,
|
|
10987
|
+
installHooksFlag: !!opts.installHooks,
|
|
10988
|
+
});
|
|
10989
|
+
printVerifiedSummary(renderVerifiedSummary(verify.version, run));
|
|
10958
10990
|
return;
|
|
10959
10991
|
}
|
|
10960
10992
|
// flair#741 follow-through: a healthy instance the verifier just couldn't
|
|
@@ -10966,21 +10998,23 @@ program
|
|
|
10966
10998
|
// "print an honest note but roll back anyway" branch that used to sit below
|
|
10967
10999
|
// is gone — that credentials case can no longer reach the rollback path.)
|
|
10968
11000
|
if (verdict.kind === "healthy-unverified") {
|
|
10969
|
-
//
|
|
10970
|
-
//
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
11001
|
+
// Same doctor runner as the "ok" branch — an unverified version must
|
|
11002
|
+
// not restore the unqualified ✅ while a catalog member is failing.
|
|
11003
|
+
const run = await doctorRunAfterUpgrade({
|
|
11004
|
+
management,
|
|
11005
|
+
port,
|
|
11006
|
+
installHooksFlag: !!opts.installHooks,
|
|
11007
|
+
});
|
|
11008
|
+
const versionNote = expectedFlairVersion ? ` on @tpsdev-ai/flair@${expectedFlairVersion}` : "";
|
|
11009
|
+
if (run.healthy) {
|
|
11010
|
+
console.log(`✅ upgrade complete: the instance is up and healthy${versionNote}.`);
|
|
11011
|
+
}
|
|
11012
|
+
else {
|
|
11013
|
+
printVerifiedSummary(renderVerifiedSummary(verify.version, run, { authenticated: false }));
|
|
11014
|
+
}
|
|
10976
11015
|
console.log(` The version could not be verified — the checker couldn't authenticate to /HealthDetail (${verdict.reason}).`);
|
|
10977
11016
|
console.log(" The server is confirmed running (public /Health passed); this is a verification gap, not an upgrade failure — nothing was rolled back.");
|
|
10978
11017
|
console.log(" To enable full post-upgrade verification: set FLAIR_ADMIN_PASS, or run `flair init` to provision ~/.flair/admin-pass or an agent key.");
|
|
10979
|
-
if (detached) {
|
|
10980
|
-
for (const line of renderDetachedWarning(management, "The instance is NOT running under launchd.")) {
|
|
10981
|
-
console.error(line);
|
|
10982
|
-
}
|
|
10983
|
-
}
|
|
10984
11018
|
return;
|
|
10985
11019
|
}
|
|
10986
11020
|
console.error(`❌ post-restart verification failed: ${verdict.reason}`);
|
|
@@ -11299,6 +11333,83 @@ function resolveInstanceServingPid(dataDir, port) {
|
|
|
11299
11333
|
listeningPids,
|
|
11300
11334
|
});
|
|
11301
11335
|
}
|
|
11336
|
+
/** Agent signing-key ids under `keysDir` — node-scoped federation keys excluded. */
|
|
11337
|
+
function collectKeyAgentIds(keysDir) {
|
|
11338
|
+
if (!existsSync(keysDir))
|
|
11339
|
+
return [];
|
|
11340
|
+
try {
|
|
11341
|
+
const keyFiles = readdirSync(keysDir).filter((f) => f.endsWith(".key"));
|
|
11342
|
+
const { agentKeyIds } = partitionKeyIds(keyFiles.map((f) => f.replace(/\.key$/, "")), keysDir);
|
|
11343
|
+
return agentKeyIds;
|
|
11344
|
+
}
|
|
11345
|
+
catch {
|
|
11346
|
+
return [];
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
async function confirmYes(question) {
|
|
11350
|
+
if (!process.stdin.isTTY)
|
|
11351
|
+
return false;
|
|
11352
|
+
const { createInterface } = await import("node:readline");
|
|
11353
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11354
|
+
const answer = await new Promise((res) => rl.question(question, (a) => { rl.close(); res(a); }));
|
|
11355
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
11356
|
+
}
|
|
11357
|
+
/**
|
|
11358
|
+
* flair#1439 — run the enumerable doctor catalog after upgrade's instance
|
|
11359
|
+
* probe, and offer a consented SessionStart-hook install when that check
|
|
11360
|
+
* fails. Silent writes are refused: `--install-hooks` or an interactive
|
|
11361
|
+
* yes is the only consent. The consent→write composition lives in
|
|
11362
|
+
* applyUpgradeHookConsent so tests can drive the path that actually
|
|
11363
|
+
* writes (or does not write) the hook file.
|
|
11364
|
+
*/
|
|
11365
|
+
async function doctorRunAfterUpgrade(args) {
|
|
11366
|
+
const homeDir = homedir();
|
|
11367
|
+
const keysDir = defaultKeysDir();
|
|
11368
|
+
const ctx = {
|
|
11369
|
+
homeDir,
|
|
11370
|
+
cwd: process.cwd(),
|
|
11371
|
+
detectedClientIds: detectClients().filter((c) => c.detected).map((c) => c.id),
|
|
11372
|
+
launchd: args.management,
|
|
11373
|
+
keysDir,
|
|
11374
|
+
keyAgentIds: collectKeyAgentIds(keysDir),
|
|
11375
|
+
};
|
|
11376
|
+
const run = runDoctorChecks(ctx);
|
|
11377
|
+
const apply = (promptAccepted) => applyUpgradeHookConsent({
|
|
11378
|
+
homeDir,
|
|
11379
|
+
ctx,
|
|
11380
|
+
run,
|
|
11381
|
+
installHooksFlag: args.installHooksFlag,
|
|
11382
|
+
interactive: !!process.stdin.isTTY,
|
|
11383
|
+
promptAccepted,
|
|
11384
|
+
port: args.port,
|
|
11385
|
+
});
|
|
11386
|
+
let outcome = apply();
|
|
11387
|
+
if (outcome.consent === "prompt" && outcome.prompt) {
|
|
11388
|
+
console.log("");
|
|
11389
|
+
for (const line of outcome.prompt.preamble)
|
|
11390
|
+
console.log(` ${line}`);
|
|
11391
|
+
outcome = apply(await confirmYes(outcome.prompt.question));
|
|
11392
|
+
}
|
|
11393
|
+
if (outcome.consent === "install") {
|
|
11394
|
+
for (const w of outcome.writes) {
|
|
11395
|
+
console.log(` ${w.ok ? "✓" : "•"} ${w.message}`);
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
if (outcome.consent === "skip-noninteractive") {
|
|
11399
|
+
for (const line of outcome.messages) {
|
|
11400
|
+
console.error(` ${line}`);
|
|
11401
|
+
}
|
|
11402
|
+
}
|
|
11403
|
+
return outcome.run;
|
|
11404
|
+
}
|
|
11405
|
+
function printVerifiedSummary(summary) {
|
|
11406
|
+
for (const line of summary.lines) {
|
|
11407
|
+
if (summary.degraded)
|
|
11408
|
+
console.error(line);
|
|
11409
|
+
else
|
|
11410
|
+
console.log(line);
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11302
11413
|
/**
|
|
11303
11414
|
* Observe whether `dataDir`'s instance is running under launchd right now.
|
|
11304
11415
|
*
|
|
@@ -13057,6 +13168,21 @@ program
|
|
|
13057
13168
|
return /^y(es)?$/i.test(answer.trim());
|
|
13058
13169
|
}
|
|
13059
13170
|
const detectedClients = detectClients().filter((c) => c.detected);
|
|
13171
|
+
// flair#1439 — install-health (MCP, FLAIR_URL, CLAUDE.md, SessionStart
|
|
13172
|
+
// hook, verified-read plan, keys classification, launchd) is the same
|
|
13173
|
+
// catalog upgrade asserts. Adding a check to DOCTOR_CHECK_IDS widens
|
|
13174
|
+
// both. Extra doctor UX (pi, --fix, execution probe, continuity,
|
|
13175
|
+
// agent registration) stays below and does not redefine those checks.
|
|
13176
|
+
const doctorCtx = {
|
|
13177
|
+
homeDir: homedir(),
|
|
13178
|
+
cwd: process.cwd(),
|
|
13179
|
+
detectedClientIds: detectedClients.map((c) => c.id),
|
|
13180
|
+
launchd: observeLaunchdManagement(defaultDataDir(), effectivePort),
|
|
13181
|
+
keysDir,
|
|
13182
|
+
keyAgentIds,
|
|
13183
|
+
agentFlag: typeof opts.agent === "string" ? opts.agent : undefined,
|
|
13184
|
+
};
|
|
13185
|
+
const catalogBefore = runDoctorChecks(doctorCtx);
|
|
13060
13186
|
if (detectedClients.length === 0) {
|
|
13061
13187
|
console.log(` ${render.icons.info} No MCP client detected — skipping client-integration checks`);
|
|
13062
13188
|
}
|
|
@@ -13291,7 +13417,6 @@ program
|
|
|
13291
13417
|
wireCursor(wireEnv);
|
|
13292
13418
|
console.log(` ${wireResult.ok ? render.icons.ok : render.icons.warn} ${wireResult.message}`);
|
|
13293
13419
|
if (wireResult.ok) {
|
|
13294
|
-
fixed++;
|
|
13295
13420
|
if (client.id === "claude-code")
|
|
13296
13421
|
claudeCodeAgentId = fixAgentId;
|
|
13297
13422
|
if (client.id === "codex")
|
|
@@ -13311,7 +13436,6 @@ program
|
|
|
13311
13436
|
const agentHint = knownAgentId ? "" : fixCommandAgentHint(keyAgentIds);
|
|
13312
13437
|
console.log(` ${render.wrap(render.c.dim, "Fix:")} flair doctor --fix${agentHint} ${render.wrap(render.c.dim, `(wires ${client.label} automatically)`)}`);
|
|
13313
13438
|
}
|
|
13314
|
-
issues++;
|
|
13315
13439
|
continue;
|
|
13316
13440
|
}
|
|
13317
13441
|
console.log(` ${render.icons.ok} ${client.label}: MCP server configured (${render.wrap(render.c.dim, block.configPath)})`);
|
|
@@ -13369,15 +13493,12 @@ program
|
|
|
13369
13493
|
else {
|
|
13370
13494
|
const fixRes = fixClaudeMdBootstrap(process.cwd());
|
|
13371
13495
|
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
13372
|
-
if (fixRes.ok)
|
|
13373
|
-
fixed++;
|
|
13374
13496
|
}
|
|
13375
13497
|
}
|
|
13376
13498
|
}
|
|
13377
13499
|
else {
|
|
13378
13500
|
console.log(` ${render.wrap(render.c.dim, "Fix:")} flair doctor --fix ${render.wrap(render.c.dim, "(adds the mcp__flair__bootstrap line to ./CLAUDE.md)")}`);
|
|
13379
13501
|
}
|
|
13380
|
-
issues++;
|
|
13381
13502
|
}
|
|
13382
13503
|
// flair#1007: presence was never the problem — the failing entry was
|
|
13383
13504
|
// perfectly well-formed. inspectSessionStartHook() additionally RUNS
|
|
@@ -13446,8 +13567,6 @@ program
|
|
|
13446
13567
|
else {
|
|
13447
13568
|
const upgrade = upgradeSessionStartHookCommand(homedir());
|
|
13448
13569
|
console.log(` ${upgrade.ok ? render.icons.ok : render.icons.warn} ${upgrade.message}`);
|
|
13449
|
-
if (upgrade.ok && upgrade.changed)
|
|
13450
|
-
fixed++;
|
|
13451
13570
|
}
|
|
13452
13571
|
}
|
|
13453
13572
|
}
|
|
@@ -13458,7 +13577,6 @@ program
|
|
|
13458
13577
|
else {
|
|
13459
13578
|
console.log(` ${render.wrap(render.c.dim, "This hook was hand-edited, so Flair will not rewrite it. To adopt the current form:")} flair hook install`);
|
|
13460
13579
|
}
|
|
13461
|
-
issues++;
|
|
13462
13580
|
}
|
|
13463
13581
|
}
|
|
13464
13582
|
else {
|
|
@@ -13476,15 +13594,12 @@ program
|
|
|
13476
13594
|
const fixAgentId = claudeCodeAgentId || opts.agent || process.env.FLAIR_AGENT_ID;
|
|
13477
13595
|
const fixRes = fixSessionStartHook(homedir(), fixAgentId);
|
|
13478
13596
|
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
13479
|
-
if (fixRes.ok)
|
|
13480
|
-
fixed++;
|
|
13481
13597
|
}
|
|
13482
13598
|
}
|
|
13483
13599
|
}
|
|
13484
13600
|
else {
|
|
13485
13601
|
console.log(` ${render.wrap(render.c.dim, "Fix:")} flair doctor --fix ${render.wrap(render.c.dim, "(adds the flair-session-start SessionStart hook)")}`);
|
|
13486
13602
|
}
|
|
13487
|
-
issues++;
|
|
13488
13603
|
}
|
|
13489
13604
|
// flair#1257 slice 2 — continuity capture pair (the check-5 twin of
|
|
13490
13605
|
// the SessionStart check above: installed / absent / stale-form).
|
|
@@ -13584,8 +13699,6 @@ program
|
|
|
13584
13699
|
else {
|
|
13585
13700
|
const upgrade = upgradeSessionStartHookCommand(homedir(), hook.path);
|
|
13586
13701
|
console.log(` ${upgrade.ok ? render.icons.ok : render.icons.warn} ${upgrade.message}`);
|
|
13587
|
-
if (upgrade.ok && upgrade.changed)
|
|
13588
|
-
fixed++;
|
|
13589
13702
|
}
|
|
13590
13703
|
}
|
|
13591
13704
|
}
|
|
@@ -13596,7 +13709,6 @@ program
|
|
|
13596
13709
|
else {
|
|
13597
13710
|
console.log(` ${render.wrap(render.c.dim, "This hook was hand-edited, so Flair will not rewrite it. To adopt the current form:")} flair hook install --harness codex`);
|
|
13598
13711
|
}
|
|
13599
|
-
issues++;
|
|
13600
13712
|
}
|
|
13601
13713
|
}
|
|
13602
13714
|
else {
|
|
@@ -13614,18 +13726,27 @@ program
|
|
|
13614
13726
|
const fixAgentId = resolveHookAgentId({ agent: opts.agent }, homedir(), "codex");
|
|
13615
13727
|
const fixRes = fixSessionStartHook(homedir(), fixAgentId, hook.path);
|
|
13616
13728
|
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
13617
|
-
if (fixRes.ok)
|
|
13618
|
-
fixed++;
|
|
13619
13729
|
}
|
|
13620
13730
|
}
|
|
13621
13731
|
}
|
|
13622
13732
|
else {
|
|
13623
13733
|
console.log(` ${render.wrap(render.c.dim, "Fix:")} flair hook install --harness codex`);
|
|
13624
13734
|
}
|
|
13625
|
-
issues++;
|
|
13626
13735
|
}
|
|
13627
13736
|
}
|
|
13628
13737
|
}
|
|
13738
|
+
// Catalog is the install-health verdict — count fail/unrun here, not
|
|
13739
|
+
// via a second issues++ on MCP / CLAUDE.md / SessionStart hook above.
|
|
13740
|
+
// --fix that cleared a catalog member shows up in the found→fixed delta.
|
|
13741
|
+
const catalogAfter = autoFix ? runDoctorChecks(doctorCtx) : catalogBefore;
|
|
13742
|
+
const catalogDelta = catalogIssueDelta(catalogBefore, catalogAfter);
|
|
13743
|
+
issues += catalogDelta.found;
|
|
13744
|
+
if (autoFix)
|
|
13745
|
+
fixed += catalogDelta.fixed;
|
|
13746
|
+
console.log(`\n ${render.wrap(render.c.bold, "Install health")}`);
|
|
13747
|
+
for (const row of renderCatalogDoctorLines(catalogAfter)) {
|
|
13748
|
+
console.log(` ${render.icons[row.icon]} ${row.line}`);
|
|
13749
|
+
}
|
|
13629
13750
|
// 7a. Resolve which agent identities the two verified-read sections below
|
|
13630
13751
|
// (Fleet presence, Migrations) iterate (flair#722). Previously both
|
|
13631
13752
|
// sections required --agent explicitly; doctor already enumerates every
|
package/dist/doctor-client.js
CHANGED
|
@@ -1117,8 +1117,8 @@ function sessionStartHookHint(agentId, path) {
|
|
|
1117
1117
|
* after the hook is present (whether from a prior call or already there)
|
|
1118
1118
|
* reports ok:true, applied:false — safe to call on every `flair init`.
|
|
1119
1119
|
*/
|
|
1120
|
-
export function applyOrReportSessionStartHook(homeDir, agentId, skip) {
|
|
1121
|
-
const existing = checkSessionStartHook(homeDir);
|
|
1120
|
+
export function applyOrReportSessionStartHook(homeDir, agentId, skip, settingsPath) {
|
|
1121
|
+
const existing = checkSessionStartHook(homeDir, settingsPath);
|
|
1122
1122
|
if (existing.present) {
|
|
1123
1123
|
return { applied: false, ok: true, message: `SessionStart hook already wired in ${existing.path}` };
|
|
1124
1124
|
}
|
|
@@ -1126,7 +1126,7 @@ export function applyOrReportSessionStartHook(homeDir, agentId, skip) {
|
|
|
1126
1126
|
if (skip) {
|
|
1127
1127
|
return { applied: false, ok: false, message: "SessionStart hook skipped (--skip-hook)", hint };
|
|
1128
1128
|
}
|
|
1129
|
-
const fix = fixSessionStartHook(homeDir, agentId);
|
|
1129
|
+
const fix = fixSessionStartHook(homeDir, agentId, settingsPath ?? existing.path);
|
|
1130
1130
|
return { applied: fix.ok, ok: fix.ok, message: fix.message, hint: fix.ok ? undefined : hint };
|
|
1131
1131
|
}
|
|
1132
1132
|
// ── check 5: per-agent iteration for verified-read sections (flair#722) ────
|
package/dist/install/clients.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// @tpsdev-ai/flair-mcp); pi is a native-extension host (kind:
|
|
6
6
|
// "native-extension" — wired via pi's own settings.json `packages` key,
|
|
7
7
|
// flair#1342). Each client has:
|
|
8
|
-
// - detection: `bin` on PATH
|
|
8
|
+
// - detection: `bin` on PATH OR the client's known config path exists
|
|
9
|
+
// (flair#1417 — GUI-only Cursor, PATH-quirky installs). A declared
|
|
10
|
+
// detect() override remains the exception (pi).
|
|
9
11
|
// - wire(env): { ok: boolean; message: string }
|
|
10
12
|
//
|
|
11
13
|
// Wiring contract (FIX 4 — onboarding dogfood round 1):
|
|
@@ -89,12 +91,13 @@ function binInPath(name) {
|
|
|
89
91
|
* elsewhere — the same defect already fixed for `flair upgrade`'s presence
|
|
90
92
|
* probes (see "Upgrade presence probes" in src/cli.ts).
|
|
91
93
|
*
|
|
92
|
-
* Nothing is lost by dropping
|
|
93
|
-
* into the prefix's bin directory, which is on PATH by
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
94
|
+
* Nothing is lost by dropping the npm-list fallback: an `npm install -g` links
|
|
95
|
+
* the package's bin into the prefix's bin directory, which is on PATH by
|
|
96
|
+
* construction. Detection still asks PATH first. A second signal is the
|
|
97
|
+
* client's known config path (detectClients, flair#1417): a GUI install whose
|
|
98
|
+
* shell command is opt-in (Cursor) is still present when its config file
|
|
99
|
+
* exists. `flair init --client <name>` still wires a client explicitly,
|
|
100
|
+
* bypassing detection entirely, so an exotic install is never locked out.
|
|
98
101
|
*/
|
|
99
102
|
function detectBin(bin) {
|
|
100
103
|
try {
|
|
@@ -560,10 +563,12 @@ function antigravityConfigPath() {
|
|
|
560
563
|
return join(resolveHome(), ".gemini", "config", "mcp_config.json");
|
|
561
564
|
}
|
|
562
565
|
/**
|
|
563
|
-
* Single dispatcher for "where does this client's
|
|
566
|
+
* Single dispatcher for "where does this client's config live" — used by
|
|
564
567
|
* `flair doctor`'s client-integration checks (flair#588) to read the config
|
|
565
|
-
* without duplicating the per-client path logic that already lives here
|
|
566
|
-
*
|
|
568
|
+
* without duplicating the per-client path logic that already lives here,
|
|
569
|
+
* and by detectClients() as the config-path presence signal (flair#1417).
|
|
570
|
+
* Every current ClientId has a path; a future id must extend this switch
|
|
571
|
+
* or stay binary-only via a `detect` override — do not invent a path.
|
|
567
572
|
*/
|
|
568
573
|
export function clientConfigPath(id) {
|
|
569
574
|
switch (id) {
|
|
@@ -758,17 +763,23 @@ export function renderWiringSummary(results, opts = {}) {
|
|
|
758
763
|
return lines;
|
|
759
764
|
}
|
|
760
765
|
/**
|
|
761
|
-
* Detect every known client. One rule
|
|
762
|
-
* client
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
766
|
+
* Detect every known client. One rule applied uniformly: `bin` on PATH OR
|
|
767
|
+
* the client's known config path exists (flair#1417). A client added to
|
|
768
|
+
* ALL_CLIENTS is detected by declaring its executable; clientConfigPath()
|
|
769
|
+
* already names every current id, so the config-path fallback is not a
|
|
770
|
+
* per-client branch to forget. A declared `detect` override remains the
|
|
771
|
+
* exception (still a pure fs check — pi, flair#1342). Detection never
|
|
772
|
+
* spawns a subprocess (flair#946).
|
|
773
|
+
*
|
|
774
|
+
* A future client with no config path helper stays binary-only until it
|
|
775
|
+
* grows one here or a `detect` override — do not invent a path.
|
|
767
776
|
*/
|
|
768
777
|
export function detectClients() {
|
|
769
778
|
return ALL_CLIENTS.map((client) => ({
|
|
770
779
|
...client,
|
|
771
|
-
detected: client.detect
|
|
780
|
+
detected: client.detect
|
|
781
|
+
? client.detect()
|
|
782
|
+
: detectBin(client.bin) || existsSync(clientConfigPath(client.id)),
|
|
772
783
|
}));
|
|
773
784
|
}
|
|
774
785
|
export function wireClaudeCode(env) {
|