@tpsdev-ai/flair 0.20.1 → 0.22.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 +39 -11
- package/SECURITY.md +52 -20
- package/config.yaml +32 -5
- package/dist/cli.js +2235 -260
- package/dist/deploy.js +96 -15
- package/dist/doctor-client.js +312 -0
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/install/clients.js +18 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/restore.js +1 -1
- package/dist/rem/runner.js +82 -12
- 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 +2 -2
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +14 -0
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +121 -2
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +127 -22
- package/dist/resources/MemoryBootstrap.js +441 -85
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +69 -5
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +268 -17
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +108 -26
- package/dist/resources/SemanticSearch.js +88 -320
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +12 -1
- package/dist/resources/agent-auth.js +61 -3
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +42 -14
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +13 -7
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- package/dist/resources/embeddings-boot.js +149 -0
- package/dist/resources/embeddings-provider.js +364 -106
- package/dist/resources/entity-vocab.js +139 -0
- package/dist/resources/federation-nonce-store.js +195 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/instance-identity.js +53 -0
- package/dist/resources/mcp-client-metadata-fields.js +112 -0
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-tools.js +88 -10
- package/dist/resources/memory-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/memory-visibility.js +37 -0
- package/dist/resources/migration-boot.js +143 -0
- package/dist/resources/migrations/dir-safety.js +71 -0
- package/dist/resources/migrations/embedding-stamp.js +178 -0
- package/dist/resources/migrations/envelope.js +43 -0
- package/dist/resources/migrations/export.js +38 -0
- package/dist/resources/migrations/ledger.js +55 -0
- package/dist/resources/migrations/lock.js +154 -0
- package/dist/resources/migrations/progress.js +31 -0
- package/dist/resources/migrations/registry.js +36 -0
- package/dist/resources/migrations/risk-policy.js +23 -0
- package/dist/resources/migrations/runner.js +456 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +73 -0
- package/dist/resources/migrations/state.js +64 -0
- package/dist/resources/migrations/status.js +39 -0
- package/dist/resources/migrations/synthetic-test-migration.js +93 -0
- package/dist/resources/migrations/types.js +9 -0
- package/dist/resources/presence-internal.js +29 -0
- package/dist/resources/provenance.js +50 -0
- package/dist/resources/rate-limiter.js +8 -1
- package/dist/resources/scoring.js +124 -5
- package/dist/resources/semantic-retrieval-core.js +317 -0
- package/dist/version-check.js +167 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/agent.graphql +7 -0
- package/schemas/event.graphql +5 -0
- package/schemas/federation.graphql +12 -0
- package/schemas/memory.graphql +82 -0
- package/schemas/schema.graphql +5 -43
- package/schemas/workspace.graphql +3 -0
- package/dist/resources/IngestEvents.js +0 -162
- package/dist/resources/IssueTokens.js +0 -19
- package/dist/resources/ObsAgentSnapshot.js +0 -13
- package/dist/resources/ObsEventFeed.js +0 -13
- package/dist/resources/ObsOffice.js +0 -19
- package/dist/resources/ObservationCenter.js +0 -23
- package/ui/observation-center.html +0 -385
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-verify.ts — `flair fleet verify` (flair#636)
|
|
3
|
+
*
|
|
4
|
+
* Fabric deploys tolerate replication errors by design (origin-first, see
|
|
5
|
+
* src/deploy.ts's REPLICATION_FAILURE_RE / --ignore-replication-errors) —
|
|
6
|
+
* but nothing upstream of this module confirms a replicated node actually
|
|
7
|
+
* converged. Harper's own "Successfully deployed" means "origin took it."
|
|
8
|
+
* This sweeps the origin + every known peer and reports a per-node table
|
|
9
|
+
* instead of a single boolean, reusing src/probe.ts's `probeInstance`
|
|
10
|
+
* verbatim (flair#635/#641 — same shape, Fabric admin Basic auth swapped in
|
|
11
|
+
* for the injected `authedGet`, exactly per that module's own docstring).
|
|
12
|
+
*
|
|
13
|
+
* ── What "peer" means here — read before trusting a green sweep ──────────
|
|
14
|
+
* Harper Fabric's OWN cluster-replication peers (the physical nodes a
|
|
15
|
+
* component gets replicated to by `harper deploy`) are NOT enumerable
|
|
16
|
+
* through any operation in the OSS @harperfast/harper build this repo
|
|
17
|
+
* depends on. `cluster_status` — the operation that would answer "what
|
|
18
|
+
* nodes are in this cluster and are they in sync" — is explicitly called
|
|
19
|
+
* out as harper-pro-only in Harper's own source (node_modules/@harperfast/
|
|
20
|
+
* harper/components/mcp/tools/schemas/operationDescriptions.ts: "Out-of-core
|
|
21
|
+
* operations (e.g. harper-pro's cluster_status) cannot have entries here").
|
|
22
|
+
* There is no "list this cluster's replication nodes" call available to this
|
|
23
|
+
* CLI, on the origin or anywhere else — direct AND via-origin cluster
|
|
24
|
+
* topology discovery are both unavailable in this Harper tier.
|
|
25
|
+
*
|
|
26
|
+
* The only node registry this module CAN reach is Flair's OWN federation
|
|
27
|
+
* peer table (resources/Federation.ts's Peer records, via GET
|
|
28
|
+
* /FederationPeers) — a distinct, higher-level concept (cross-instance
|
|
29
|
+
* memory-sync pairing, hub-and-spoke) that happens to double as a fleet
|
|
30
|
+
* registry ONLY where an operator has ALSO paired each Fabric replica as a
|
|
31
|
+
* federation peer of the origin. A replica that was never paired is
|
|
32
|
+
* invisible to this sweep — `0 peers known` means "0 peers ON FILE", never
|
|
33
|
+
* "0 peers exist." renderFleetSweepTable() prints this caveat inline
|
|
34
|
+
* whenever the peer list comes back empty, so it can't be missed by reading
|
|
35
|
+
* a green summary line.
|
|
36
|
+
*
|
|
37
|
+
* Every peer that DOES carry a direct `endpoint` gets a REAL probe (health +
|
|
38
|
+
* authenticated version check, same Fabric admin Basic-auth credentials as
|
|
39
|
+
* the origin — a Fabric cluster shares one admin realm). A peer with no
|
|
40
|
+
* usable endpoint is reported "unverifiable" — never silently dropped, never
|
|
41
|
+
* printed green. See classifyNode()'s "method" field: "direct" (we actually
|
|
42
|
+
* hit it) vs "none" (we could not attempt a check at all, and say why).
|
|
43
|
+
*/
|
|
44
|
+
import { probeInstance } from "./probe.js";
|
|
45
|
+
import * as render from "./render.js";
|
|
46
|
+
// ─── Exit codes (documented in `flair fleet verify --help`) ─────────────────
|
|
47
|
+
/** All nodes verified: healthy, authenticated, and version-matched. */
|
|
48
|
+
export const FLEET_EXIT_OK = 0;
|
|
49
|
+
/** Origin failed — unreachable, unauthenticated, or running the wrong version. Worst case: the cluster's entrypoint itself is broken. */
|
|
50
|
+
export const FLEET_EXIT_ORIGIN_FAILED = 1;
|
|
51
|
+
/** Origin is fine, but at least one reachable peer is running a DIFFERENT version — a mixed-version fleet (the #638 scenario). */
|
|
52
|
+
export const FLEET_EXIT_PEER_SKEW = 2;
|
|
53
|
+
/** Origin is fine, no version skew among reachable peers, but at least one peer could not be verified at all (unreachable, auth rejected, or no endpoint on file). */
|
|
54
|
+
export const FLEET_EXIT_PEER_UNREACHABLE = 3;
|
|
55
|
+
export const FLEET_EXIT_DESCRIPTIONS = {
|
|
56
|
+
[FLEET_EXIT_OK]: "all nodes verified: healthy, authenticated, version-matched",
|
|
57
|
+
[FLEET_EXIT_ORIGIN_FAILED]: "origin failed (unreachable, unauthenticated, or wrong version)",
|
|
58
|
+
[FLEET_EXIT_PEER_SKEW]: "origin OK, but a reachable peer is running a DIFFERENT version (skew)",
|
|
59
|
+
[FLEET_EXIT_PEER_UNREACHABLE]: "origin OK, no skew among reachable peers, but a peer could not be verified at all (unreachable, auth rejected, or no endpoint on file)",
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Priority order when multiple problems exist at once: an origin failure
|
|
63
|
+
* always wins (nothing downstream matters if the entrypoint is broken), then
|
|
64
|
+
* skew (a real mixed-version fleet), then unreachable/unverifiable (we
|
|
65
|
+
* genuinely don't know that node's state).
|
|
66
|
+
*/
|
|
67
|
+
export function decideFleetExitCode(origin, peers) {
|
|
68
|
+
if (origin.status !== "ok")
|
|
69
|
+
return FLEET_EXIT_ORIGIN_FAILED;
|
|
70
|
+
if (peers.some((p) => p.status === "skew"))
|
|
71
|
+
return FLEET_EXIT_PEER_SKEW;
|
|
72
|
+
if (peers.some((p) => p.status !== "ok"))
|
|
73
|
+
return FLEET_EXIT_PEER_UNREACHABLE;
|
|
74
|
+
return FLEET_EXIT_OK;
|
|
75
|
+
}
|
|
76
|
+
// ─── Pure decision logic: ProbeResult → FleetNodeResult ─────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Turn a probe attempt (or the absence of one) into a table row. Pure —
|
|
79
|
+
* no I/O. `expectVersion` is the baseline this node should be running;
|
|
80
|
+
* pass null when there is nothing to compare against (skew can't be
|
|
81
|
+
* evaluated, but health/auth still can).
|
|
82
|
+
*/
|
|
83
|
+
export function classifyNode(role, id, url, method, probe, expectVersion, unverifiableReason) {
|
|
84
|
+
if (method === "none" || probe === null) {
|
|
85
|
+
return {
|
|
86
|
+
id, role, url, method: "none",
|
|
87
|
+
healthy: null, authenticated: null, version: null, versionMatch: null,
|
|
88
|
+
status: "unverifiable",
|
|
89
|
+
detail: unverifiableReason ?? "no way to verify this node",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!probe.healthy) {
|
|
93
|
+
return {
|
|
94
|
+
id, role, url, method,
|
|
95
|
+
healthy: false, authenticated: null, version: null, versionMatch: null,
|
|
96
|
+
status: "unreachable",
|
|
97
|
+
detail: probe.error ?? `${url ?? id} did not answer /Health`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (probe.authenticated === false) {
|
|
101
|
+
return {
|
|
102
|
+
id, role, url, method,
|
|
103
|
+
healthy: true, authenticated: false, version: null, versionMatch: null,
|
|
104
|
+
status: "auth-failed",
|
|
105
|
+
detail: probe.error ?? "authenticated request rejected",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (expectVersion !== null && probe.versionMatch === false) {
|
|
109
|
+
return {
|
|
110
|
+
id, role, url, method,
|
|
111
|
+
healthy: true, authenticated: probe.authenticated, version: probe.version, versionMatch: false,
|
|
112
|
+
status: "skew",
|
|
113
|
+
detail: probe.error ?? `version mismatch: expected ${expectVersion}, reports ${probe.version ?? "unknown"}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id, role, url, method,
|
|
118
|
+
healthy: true, authenticated: probe.authenticated, version: probe.version, versionMatch: probe.versionMatch,
|
|
119
|
+
status: "ok",
|
|
120
|
+
detail: probe.version ? `healthy, running ${probe.version}` : "healthy",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ─── Peer endpoint validation ────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* A federation Peer record may have no `endpoint` at all (tunnel-paired —
|
|
126
|
+
* see src/cli.ts's existing `flair federation ping` handling of the same
|
|
127
|
+
* shape) or an endpoint that isn't a plain http(s) URL. Reject non-http(s)
|
|
128
|
+
* schemes rather than attempting to fetch them (same protocol allowlist as
|
|
129
|
+
* the existing federation-ping probe — Sherlock review on #314).
|
|
130
|
+
*/
|
|
131
|
+
export function validatePeerEndpoint(endpoint) {
|
|
132
|
+
if (!endpoint) {
|
|
133
|
+
return { error: "no endpoint on file (never federation-paired with a direct endpoint, or tunnel-paired)" };
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = new URL(endpoint);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { error: `invalid endpoint URL on file: ${endpoint}` };
|
|
141
|
+
}
|
|
142
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
143
|
+
return { error: `unsupported endpoint protocol on file: ${parsed.protocol}` };
|
|
144
|
+
}
|
|
145
|
+
return { url: endpoint };
|
|
146
|
+
}
|
|
147
|
+
// ─── Fabric admin Basic-auth authedGet (injected into probeInstance) ────────
|
|
148
|
+
/**
|
|
149
|
+
* Builds the authedGet probeInstance needs, using Fabric admin Basic auth —
|
|
150
|
+
* the exact credential shape src/deploy.ts and src/fabric-upgrade.ts already
|
|
151
|
+
* use for the whole cluster (FABRIC_USER/FABRIC_PASSWORD). Assumes a single
|
|
152
|
+
* Fabric cluster shares one admin realm across its nodes; a peer that
|
|
153
|
+
* rejects these credentials (a genuinely separate Harper instance with its
|
|
154
|
+
* own admin) surfaces as "auth-failed" — itself a real, informative finding,
|
|
155
|
+
* not a bug in this probe.
|
|
156
|
+
*
|
|
157
|
+
* NEVER logs the credential — only the Authorization header carries it, and
|
|
158
|
+
* probeInstance/callers only ever see/report the HTTP outcome.
|
|
159
|
+
*/
|
|
160
|
+
export function buildFabricAuthedGet(baseUrl, fabricUser, fabricPassword) {
|
|
161
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
162
|
+
const auth = Buffer.from(`${fabricUser}:${fabricPassword}`).toString("base64");
|
|
163
|
+
return async (path) => {
|
|
164
|
+
const res = await fetch(`${base}${path}`, {
|
|
165
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
166
|
+
signal: AbortSignal.timeout(15_000),
|
|
167
|
+
});
|
|
168
|
+
const text = await res.text();
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
171
|
+
return text ? JSON.parse(text) : {};
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function defaultFetchPeers(authedGet) {
|
|
175
|
+
const body = await authedGet("/FederationPeers");
|
|
176
|
+
return Array.isArray(body?.peers) ? body.peers : [];
|
|
177
|
+
}
|
|
178
|
+
function defaultDeps() {
|
|
179
|
+
return {
|
|
180
|
+
probe: probeInstance,
|
|
181
|
+
fetchPeers: defaultFetchPeers,
|
|
182
|
+
buildAuthedGet: buildFabricAuthedGet,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// ─── Orchestrator ───────────────────────────────────────────────────────────
|
|
186
|
+
export async function sweepFleet(opts, injected) {
|
|
187
|
+
const deps = { ...defaultDeps(), ...injected };
|
|
188
|
+
const log = deps.log ?? (() => { });
|
|
189
|
+
const originAuthedGet = deps.buildAuthedGet(opts.target, opts.fabricUser, opts.fabricPassword);
|
|
190
|
+
log(`Probing origin ${opts.target} ...`);
|
|
191
|
+
const originProbe = await deps.probe(opts.target, {
|
|
192
|
+
expectVersion: opts.expectVersion,
|
|
193
|
+
timeoutMs: opts.timeoutMs,
|
|
194
|
+
authedGet: originAuthedGet,
|
|
195
|
+
});
|
|
196
|
+
const explicitExpect = opts.expectVersion ?? null;
|
|
197
|
+
const expectVersion = explicitExpect ?? originProbe.version ?? null;
|
|
198
|
+
const expectVersionSource = explicitExpect !== null ? "explicit" : expectVersion !== null ? "origin" : "none";
|
|
199
|
+
// Origin is classified against the EXPLICIT expectation only — when none was
|
|
200
|
+
// given, the origin itself defines the baseline and can't be "skewed"
|
|
201
|
+
// relative to its own reading.
|
|
202
|
+
const origin = classifyNode("origin", "origin", opts.target, "direct", originProbe, explicitExpect);
|
|
203
|
+
let peerRecords = [];
|
|
204
|
+
let peerEnumerationError = null;
|
|
205
|
+
try {
|
|
206
|
+
log("Enumerating federation peers ...");
|
|
207
|
+
peerRecords = await deps.fetchPeers(originAuthedGet);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
peerEnumerationError = err?.message ?? String(err);
|
|
211
|
+
}
|
|
212
|
+
// Revoked peers are intentionally decommissioned — reporting them
|
|
213
|
+
// unreachable would be noise, not signal.
|
|
214
|
+
const knownPeers = peerRecords.filter((p) => p.status !== "revoked");
|
|
215
|
+
const peers = [];
|
|
216
|
+
for (const p of knownPeers) {
|
|
217
|
+
const check = validatePeerEndpoint(p.endpoint);
|
|
218
|
+
if ("error" in check) {
|
|
219
|
+
peers.push(classifyNode("peer", p.id, p.endpoint ?? null, "none", null, expectVersion, check.error));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
log(`Probing peer ${p.id} (${check.url}) ...`);
|
|
223
|
+
const peerAuthedGet = deps.buildAuthedGet(check.url, opts.fabricUser, opts.fabricPassword);
|
|
224
|
+
const peerProbe = await deps.probe(check.url, {
|
|
225
|
+
expectVersion: expectVersion ?? undefined,
|
|
226
|
+
timeoutMs: opts.timeoutMs,
|
|
227
|
+
authedGet: peerAuthedGet,
|
|
228
|
+
});
|
|
229
|
+
peers.push(classifyNode("peer", p.id, check.url, "direct", peerProbe, expectVersion));
|
|
230
|
+
}
|
|
231
|
+
if (peerEnumerationError) {
|
|
232
|
+
// A failure to even LIST peers means fleet coverage is unknown — never
|
|
233
|
+
// let that read as "0 peers, all clear."
|
|
234
|
+
peers.push({
|
|
235
|
+
id: "(peer enumeration)",
|
|
236
|
+
role: "peer",
|
|
237
|
+
url: null,
|
|
238
|
+
method: "none",
|
|
239
|
+
healthy: null, authenticated: null, version: null, versionMatch: null,
|
|
240
|
+
status: "unverifiable",
|
|
241
|
+
detail: `GET /FederationPeers on the origin failed: ${peerEnumerationError} — fleet coverage unknown, cannot confirm every peer was even considered`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const exitCode = decideFleetExitCode(origin, peers);
|
|
245
|
+
return { target: opts.target, expectVersion, expectVersionSource, origin, peers, peerEnumerationError, exitCode };
|
|
246
|
+
}
|
|
247
|
+
// ─── Rendering ───────────────────────────────────────────────────────────────
|
|
248
|
+
const STATUS_COLOR = {
|
|
249
|
+
ok: render.c.green,
|
|
250
|
+
skew: render.c.red,
|
|
251
|
+
unreachable: render.c.red,
|
|
252
|
+
"auth-failed": render.c.yellow,
|
|
253
|
+
unverifiable: render.c.yellow,
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Per-node table + the caveats an operator needs to trust it. An "ok" row
|
|
257
|
+
* always means a REAL probe answered; anything else names exactly how it's
|
|
258
|
+
* wrong (see FleetNodeResult.detail) instead of a bare pass/fail.
|
|
259
|
+
*/
|
|
260
|
+
export function renderFleetSweepTable(result) {
|
|
261
|
+
const rows = [result.origin, ...result.peers];
|
|
262
|
+
const cols = [
|
|
263
|
+
{ label: "node", key: "id" },
|
|
264
|
+
{ label: "role", key: "role" },
|
|
265
|
+
{ label: "method", key: "method", format: (v) => (v === "direct" ? "direct" : render.wrap(render.c.dim, "none")) },
|
|
266
|
+
{
|
|
267
|
+
label: "status",
|
|
268
|
+
key: "status",
|
|
269
|
+
format: (v) => render.wrap(STATUS_COLOR[v] ?? render.c.dim, String(v)),
|
|
270
|
+
},
|
|
271
|
+
{ label: "version", key: "version", format: (v) => (v ? String(v) : render.wrap(render.c.dim, "—")) },
|
|
272
|
+
{ label: "detail", key: "detail" },
|
|
273
|
+
];
|
|
274
|
+
const lines = [render.table(cols, rows)];
|
|
275
|
+
if (result.peers.length === 0 || (result.peers.length === 1 && result.peerEnumerationError)) {
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push(render.wrap(render.c.dim, "0 federation peers known to the origin. This does NOT mean the Fabric cluster has no other " +
|
|
278
|
+
"replication nodes — Harper's own cluster peers aren't visible to this tool (cluster_status is a " +
|
|
279
|
+
"harper-pro-only operation, unavailable in the OSS build this CLI ships). It means no other node is " +
|
|
280
|
+
"paired as a Flair federation peer of this origin. If this cluster has replicas, pair them " +
|
|
281
|
+
"(`flair federation pair`) so fleet verify can actually see them."));
|
|
282
|
+
}
|
|
283
|
+
const expectLine = result.expectVersionSource === "explicit"
|
|
284
|
+
? `compared against --expect-version ${result.expectVersion}`
|
|
285
|
+
: result.expectVersionSource === "origin"
|
|
286
|
+
? `compared against the origin's own reported version (${result.expectVersion}) — no --expect-version given`
|
|
287
|
+
: "no version baseline available (origin unreachable/unauthenticated and no --expect-version given) — skew could not be checked";
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push(render.wrap(render.c.dim, expectLine));
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
package/dist/install/clients.js
CHANGED
|
@@ -179,6 +179,24 @@ function geminiConfigPath() {
|
|
|
179
179
|
function codexConfigPath() {
|
|
180
180
|
return join(resolveHome(), ".codex", "config.toml");
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Single dispatcher for "where does this client's MCP config live" — used by
|
|
184
|
+
* `flair doctor`'s client-integration checks (flair#588) to read the config
|
|
185
|
+
* without duplicating the per-client path logic that already lives here.
|
|
186
|
+
* Additive only: does not change existing wire/detect behavior.
|
|
187
|
+
*/
|
|
188
|
+
export function clientConfigPath(id) {
|
|
189
|
+
switch (id) {
|
|
190
|
+
case "claude-code":
|
|
191
|
+
return join(resolveHome(), ".claude.json");
|
|
192
|
+
case "codex":
|
|
193
|
+
return codexConfigPath();
|
|
194
|
+
case "gemini":
|
|
195
|
+
return geminiConfigPath();
|
|
196
|
+
case "cursor":
|
|
197
|
+
return cursorConfigPath();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
182
200
|
// ---- Internal wiring functions --------------------------------------------------
|
|
183
201
|
//
|
|
184
202
|
// Claude Code wiring lives inline in src/cli.ts (it writes ~/.claude.json, the
|