@tpsdev-ai/flair 0.44.8 → 0.44.9
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/dist/cli.js +104 -26
- package/dist/doctor-client.js +55 -1
- package/dist/install/clients.js +50 -2
- package/dist/resources/MemoryBootstrap.js +119 -34
- package/dist/resources/mcp-tools.js +1 -1
- package/dist/resources/trust-block.js +16 -7
- package/docs/integrations.md +3 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,11 +19,11 @@ 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, clientConfigPath, codexConfigHasFlairSection } from "./install/clients.js";
|
|
23
|
-
import { flairCliVersion, clearFlairCliVersionCache, mcpServerSpec, unpinnedSpecWarning } from "./lib/mcp-spec.js";
|
|
22
|
+
import { detectClients, renderWiringSummary, wireClaudeCode, wireCodex, wireGemini, wireCursor, wireAntigravity, clientConfigPath, codexConfigHasFlairSection } from "./install/clients.js";
|
|
23
|
+
import { flairCliVersion, clearFlairCliVersionCache, mcpServerSpec, unpinnedSpecWarning, FLAIR_MCP_PACKAGE } 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";
|
|
26
|
-
import { readClientMcpBlock, checkClaudeMdBootstrap, inspectSessionStartHook, upgradeSessionStartHookCommand, fixClaudeMdBootstrap, fixSessionStartHook, applyOrReportClaudeMdBootstrap, applyOrReportSessionStartHook, resolveWireFlairUrl, planAgentIterations, fixCommandAgentHint, isNodeKeyId, partitionKeyIds, resolveFixAgentId, describeAgentGateFinding, embeddingsSkipRemedy, classifyKeyFile, resolveCollisionSafeName, pruneDateStamp, PRUNED_DIR_NAME, } from "./doctor-client.js";
|
|
26
|
+
import { readClientMcpBlock, checkClaudeMdBootstrap, detectWiredFlairMcp, inspectSessionStartHook, upgradeSessionStartHookCommand, fixClaudeMdBootstrap, fixSessionStartHook, applyOrReportClaudeMdBootstrap, applyOrReportSessionStartHook, resolveWireFlairUrl, planAgentIterations, fixCommandAgentHint, isNodeKeyId, partitionKeyIds, resolveFixAgentId, describeAgentGateFinding, embeddingsSkipRemedy, classifyKeyFile, resolveCollisionSafeName, pruneDateStamp, PRUNED_DIR_NAME, } from "./doctor-client.js";
|
|
27
27
|
import { installHook, uninstallHook, hookStatus, isSupportedHarness, SUPPORTED_HARNESSES, } from "./hook-install.js";
|
|
28
28
|
import { readSecretFileSecure, readAdminPassFileSecure, defaultAdminPassPath, defaultKeysDir, resolveLocalAdminPass, resolveKeyPath, buildEd25519Auth, authFetch, KeyLoadError, isLocalBase, authedRequest, } from "./lib/auth-resolve.js";
|
|
29
29
|
import { resolveSigningIdentity, emitSigningIdentityDebug, } from "./lib/signing-identity.js";
|
|
@@ -2394,14 +2394,58 @@ export function upgradeStatusSuffix(name, status) {
|
|
|
2394
2394
|
if (status === "current")
|
|
2395
2395
|
return " (current)";
|
|
2396
2396
|
if (status === "missing") {
|
|
2397
|
-
return name ===
|
|
2397
|
+
return name === FLAIR_MCP_PACKAGE
|
|
2398
2398
|
? " (zero-install via npx — run: flair doctor --fix)"
|
|
2399
2399
|
: " (run: npm install -g)";
|
|
2400
2400
|
}
|
|
2401
2401
|
if (status === "optional")
|
|
2402
2402
|
return " (install via: openclaw plugins install @tpsdev-ai/openclaw-flair)";
|
|
2403
|
+
// flair-mcp is refreshed by re-pinning its wiring (`flair doctor --fix` /
|
|
2404
|
+
// the post-upgrade pin refresh), never `npm install -g` — a global bin does
|
|
2405
|
+
// nothing for an `npx -y -p @tpsdev-ai/flair-mcp` invocation (flair#1208).
|
|
2406
|
+
if (status === "outdated" && name === FLAIR_MCP_PACKAGE) {
|
|
2407
|
+
return " (npx-wired — run: flair doctor --fix to re-pin)";
|
|
2408
|
+
}
|
|
2403
2409
|
return "";
|
|
2404
2410
|
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Resolve the `flair upgrade` finding for flair-mcp from its ACTUAL wiring,
|
|
2413
|
+
* not a global-install probe (flair#1208).
|
|
2414
|
+
*
|
|
2415
|
+
* flair-mcp is zero-install via npx (#1168): a correctly-wired machine never
|
|
2416
|
+
* installs it globally, so the global bin/lib probe returning null is the
|
|
2417
|
+
* NORMAL state, not "missing". Its real installed version is the pin its wiring
|
|
2418
|
+
* carries (a client MCP config's args, refreshed by `flair doctor --fix`).
|
|
2419
|
+
*
|
|
2420
|
+
* Resolution order:
|
|
2421
|
+
* 1. Legacy global install — the bin/lib probe found a version. Honor it.
|
|
2422
|
+
* 2. Not wired anywhere — genuinely missing; the remedy (upgradeStatusSuffix)
|
|
2423
|
+
* is `flair doctor --fix`, never `npm install -g`.
|
|
2424
|
+
* 3. Wired with a concrete pin — that pin IS the installed version
|
|
2425
|
+
* (current when it equals latest, else outdated → re-pin via doctor).
|
|
2426
|
+
* 4. Wired but unpinned (a bare npx spec / the SessionStart hook) — `npx -y`
|
|
2427
|
+
* re-resolves latest every session, so the effective version IS latest →
|
|
2428
|
+
* current.
|
|
2429
|
+
*/
|
|
2430
|
+
export function resolveFlairMcpFinding(globalProbe, latest, wiring) {
|
|
2431
|
+
// 1. Legacy global install.
|
|
2432
|
+
if (globalProbe !== null) {
|
|
2433
|
+
return { installed: globalProbe, status: globalProbe === latest ? "current" : "outdated" };
|
|
2434
|
+
}
|
|
2435
|
+
// 2. Not wired anywhere.
|
|
2436
|
+
if (!wiring.wired) {
|
|
2437
|
+
return { installed: null, status: "missing" };
|
|
2438
|
+
}
|
|
2439
|
+
// 3. Wired with a pin.
|
|
2440
|
+
if (wiring.pinnedVersion) {
|
|
2441
|
+
return {
|
|
2442
|
+
installed: wiring.pinnedVersion,
|
|
2443
|
+
status: wiring.pinnedVersion === latest ? "current" : "outdated",
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
// 4. Wired but unpinned — npx resolves latest on every session.
|
|
2447
|
+
return { installed: latest, status: "current" };
|
|
2448
|
+
}
|
|
2405
2449
|
/**
|
|
2406
2450
|
* Pure flag resolution for `flair upgrade`'s restart/verify defaults
|
|
2407
2451
|
* (flair#635 decision: restart is now the default; `--no-restart` opts
|
|
@@ -2714,7 +2758,7 @@ program
|
|
|
2714
2758
|
.option("--data-dir <dir>", "Harper data directory")
|
|
2715
2759
|
.option("--skip-start", "Skip Harper startup (assume already running)")
|
|
2716
2760
|
.option("--skip-soul", "Skip interactive personality setup")
|
|
2717
|
-
.option("--client <client>", "MCP client(s) to wire: claude-code, codex, gemini, cursor, all, or none")
|
|
2761
|
+
.option("--client <client>", "MCP client(s) to wire: claude-code, codex, gemini, cursor, antigravity, all, or none")
|
|
2718
2762
|
.option("--no-mcp", "Skip MCP client wiring (instance + agent only)")
|
|
2719
2763
|
.option("--skip-smoke", "Skip the MCP smoke test")
|
|
2720
2764
|
.option("--skip-claude-md", "Skip appending the Flair bootstrap line to CLAUDE.md (claude-code only)")
|
|
@@ -2933,9 +2977,9 @@ program
|
|
|
2933
2977
|
const noMcp = opts.mcp === false;
|
|
2934
2978
|
const selectedClients = [];
|
|
2935
2979
|
if (clientOpt && clientOpt !== "all" && clientOpt !== "none" && !noMcp) {
|
|
2936
|
-
const valid = ["claude-code", "codex", "gemini", "cursor"];
|
|
2980
|
+
const valid = ["claude-code", "codex", "gemini", "cursor", "antigravity"];
|
|
2937
2981
|
if (!valid.includes(clientOpt)) {
|
|
2938
|
-
console.error(`Unknown client: ${clientOpt}. Valid: claude-code, codex, gemini, cursor, all, none`);
|
|
2982
|
+
console.error(`Unknown client: ${clientOpt}. Valid: claude-code, codex, gemini, cursor, antigravity, all, none`);
|
|
2939
2983
|
process.exit(1);
|
|
2940
2984
|
}
|
|
2941
2985
|
selectedClients.push(clientOpt);
|
|
@@ -3536,6 +3580,9 @@ program
|
|
|
3536
3580
|
case "cursor":
|
|
3537
3581
|
result = wireCursor({ ...mcpEnv, FLAIR_CLIENT: "cursor" });
|
|
3538
3582
|
break;
|
|
3583
|
+
case "antigravity":
|
|
3584
|
+
result = wireAntigravity({ ...mcpEnv, FLAIR_CLIENT: "antigravity" });
|
|
3585
|
+
break;
|
|
3539
3586
|
default: result = { ok: false, message: `Unknown client: ${clientId}` };
|
|
3540
3587
|
}
|
|
3541
3588
|
wiringResults.push({ client: clientId, message: result.message, wired: result.ok });
|
|
@@ -9517,18 +9564,30 @@ program
|
|
|
9517
9564
|
}
|
|
9518
9565
|
catch { /* best-effort */ }
|
|
9519
9566
|
}
|
|
9520
|
-
const
|
|
9567
|
+
const globalProbe = probe();
|
|
9568
|
+
let installed;
|
|
9521
9569
|
let status;
|
|
9522
|
-
if (
|
|
9523
|
-
//
|
|
9524
|
-
//
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
status =
|
|
9570
|
+
if (name === FLAIR_MCP_PACKAGE) {
|
|
9571
|
+
// flair-mcp is zero-install via npx (#1168) — a null global probe is
|
|
9572
|
+
// the NORMAL state, not "missing". Resolve it from its actual wiring
|
|
9573
|
+
// (the pin in a client MCP config / the SessionStart hook) so the
|
|
9574
|
+
// listing is truthful and the remedy actually works (flair#1208).
|
|
9575
|
+
const home = process.env.HOME ?? homedir();
|
|
9576
|
+
({ installed, status } = resolveFlairMcpFinding(globalProbe, latest, detectWiredFlairMcp(home)));
|
|
9529
9577
|
}
|
|
9530
9578
|
else {
|
|
9531
|
-
|
|
9579
|
+
installed = globalProbe;
|
|
9580
|
+
if (installed === null) {
|
|
9581
|
+
// openclaw-plugin packages are optional — if openclaw isn't
|
|
9582
|
+
// installed, don't surface a misleading "install with npm" advice.
|
|
9583
|
+
status = kind === "openclaw-plugin" ? "optional" : "missing";
|
|
9584
|
+
}
|
|
9585
|
+
else if (installed === latest) {
|
|
9586
|
+
status = "current";
|
|
9587
|
+
}
|
|
9588
|
+
else {
|
|
9589
|
+
status = "outdated";
|
|
9590
|
+
}
|
|
9532
9591
|
}
|
|
9533
9592
|
findings.push({ name, installed, latest, status, kind });
|
|
9534
9593
|
// Suppress the line for openclaw plugins that are optional-because-
|
|
@@ -9553,12 +9612,19 @@ program
|
|
|
9553
9612
|
console.log("\nScope: npm-global packages (flair, flair-mcp) + openclaw plugins. Other integrations (pi-flair, langgraph-flair, n8n-nodes-flair, hermes-flair) upgrade in their own ecosystems (pi / pip / n8n).");
|
|
9554
9613
|
const outdated = findings.filter((f) => f.status === "outdated");
|
|
9555
9614
|
const missing = findings.filter((f) => f.status === "missing");
|
|
9615
|
+
// flair-mcp is refreshed by re-pinning its wiring (`flair doctor --fix` /
|
|
9616
|
+
// the post-upgrade pin refresh below), NEVER `npm install -g` — a global
|
|
9617
|
+
// bin does nothing for an `npx -y -p @tpsdev-ai/flair-mcp` invocation
|
|
9618
|
+
// (#1168/#1208). So a stale-pinned flair-mcp drives a remedy line, not the
|
|
9619
|
+
// npm-install + restart transaction. It is kept out of npmUpgrades here and
|
|
9620
|
+
// surfaced separately below.
|
|
9621
|
+
const flairMcpOutdated = outdated.find((f) => f.name === FLAIR_MCP_PACKAGE) ?? null;
|
|
9556
9622
|
// openclaw plugins upgrade through `openclaw plugins install`, not `npm
|
|
9557
9623
|
// install -g` (npm-installed wouldn't connect to OpenClaw's gateway slot).
|
|
9558
9624
|
// Split outdated into npm-upgradeable vs openclaw-plugin so we can use
|
|
9559
9625
|
// the right command for each.
|
|
9560
9626
|
const npmUpgrades = outdated
|
|
9561
|
-
.filter((f) => f.kind !== "openclaw-plugin")
|
|
9627
|
+
.filter((f) => f.kind !== "openclaw-plugin" && f.name !== FLAIR_MCP_PACKAGE)
|
|
9562
9628
|
.map(({ name, installed, latest }) => ({ pkg: name, installed: installed ?? "unknown", latest }));
|
|
9563
9629
|
const openclawUpgrades = outdated
|
|
9564
9630
|
.filter((f) => f.kind === "openclaw-plugin")
|
|
@@ -9568,15 +9634,26 @@ program
|
|
|
9568
9634
|
console.log("\n✅ Everything is up to date.");
|
|
9569
9635
|
return;
|
|
9570
9636
|
}
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9637
|
+
// Nothing to install via npm/openclaw. What is left is advisory: packages
|
|
9638
|
+
// not detected (missing) and/or a flair-mcp whose wired pin is behind latest
|
|
9639
|
+
// — both fixed by re-wiring (`flair doctor --fix`), never by the
|
|
9640
|
+
// npm-install + restart transaction below (#1168/#1208). Print the remedies
|
|
9641
|
+
// and stop.
|
|
9642
|
+
if (totalUpgrades === 0) {
|
|
9643
|
+
if (missing.length > 0) {
|
|
9644
|
+
const npmMissing = missing.filter((f) => f.name !== FLAIR_MCP_PACKAGE);
|
|
9645
|
+
const mcpMissing = missing.some((f) => f.name === FLAIR_MCP_PACKAGE);
|
|
9646
|
+
console.log(`\n❔ ${missing.length} package${missing.length > 1 ? "s" : ""} not detected — all detected packages are up to date.`);
|
|
9647
|
+
if (npmMissing.length > 0) {
|
|
9648
|
+
console.log(` Install missing: npm install -g ${npmMissing.map((f) => f.name).join(" ")}`);
|
|
9649
|
+
}
|
|
9650
|
+
if (mcpMissing) {
|
|
9651
|
+
console.log(` flair-mcp is zero-install via npx — run: flair doctor --fix to wire the hook`);
|
|
9652
|
+
}
|
|
9577
9653
|
}
|
|
9578
|
-
if (
|
|
9579
|
-
console.log(
|
|
9654
|
+
if (flairMcpOutdated) {
|
|
9655
|
+
console.log(`\n⬆️ flair-mcp is wired via npx (pinned ${flairMcpOutdated.installed} → latest ${flairMcpOutdated.latest}).`);
|
|
9656
|
+
console.log(` Re-pin it: flair doctor --fix`);
|
|
9580
9657
|
}
|
|
9581
9658
|
return;
|
|
9582
9659
|
}
|
|
@@ -12154,7 +12231,8 @@ program
|
|
|
12154
12231
|
const wireResult = client.id === "claude-code" ? wireClaudeCode(wireEnv) :
|
|
12155
12232
|
client.id === "codex" ? wireCodex(wireEnv) :
|
|
12156
12233
|
client.id === "gemini" ? wireGemini(wireEnv) :
|
|
12157
|
-
|
|
12234
|
+
client.id === "antigravity" ? wireAntigravity(wireEnv) :
|
|
12235
|
+
wireCursor(wireEnv);
|
|
12158
12236
|
console.log(` ${wireResult.ok ? render.icons.ok : render.icons.warn} ${wireResult.message}`);
|
|
12159
12237
|
if (wireResult.ok)
|
|
12160
12238
|
fixed++;
|
package/dist/doctor-client.js
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
|
-
import { clientConfigPath } from "./install/clients.js";
|
|
25
|
+
import { ALL_CLIENTS, clientConfigPath } from "./install/clients.js";
|
|
26
|
+
import { FLAIR_MCP_PACKAGE } from "./lib/mcp-spec.js";
|
|
26
27
|
// The exact substring `flair init` writes into CLAUDE.md (src/cli.ts, the
|
|
27
28
|
// `init` action) and that the doctor check + fix both key off of.
|
|
28
29
|
export const CLAUDE_MD_BOOTSTRAP_MARKER = "mcp__flair__bootstrap";
|
|
@@ -365,6 +366,59 @@ export function checkSessionStartHook(homeDir) {
|
|
|
365
366
|
return { present: false, path };
|
|
366
367
|
}
|
|
367
368
|
}
|
|
369
|
+
// ── flair-mcp presence by WIRING, not global install (flair#1208) ───────────
|
|
370
|
+
//
|
|
371
|
+
// flair-mcp is zero-install via npx by design (#1168): a correctly-wired
|
|
372
|
+
// machine invokes it as `npx -y -p @tpsdev-ai/flair-mcp` and NEVER installs it
|
|
373
|
+
// globally, so `flair upgrade`'s global bin/lib probe finds nothing and
|
|
374
|
+
// mis-reports it "not detected." Its real "installed version" is the pin its
|
|
375
|
+
// wiring carries — the mcpServerSpec() written into a client's MCP config
|
|
376
|
+
// (pinned since #1135). Detect it there instead.
|
|
377
|
+
/**
|
|
378
|
+
* Extract a pinned `@tpsdev-ai/flair-mcp` version from any wiring string — a
|
|
379
|
+
* client MCP `args` array, a Codex TOML args line, or a SessionStart hook
|
|
380
|
+
* command. Returns the version when the spec is written
|
|
381
|
+
* `@tpsdev-ai/flair-mcp@<ver>`; null for a bare/unpinned spec.
|
|
382
|
+
*
|
|
383
|
+
* The SessionStart hook is deliberately unpinned (`npx -y -p
|
|
384
|
+
* @tpsdev-ai/flair-mcp`, buildSessionStartHookCommand above), so a hook
|
|
385
|
+
* establishes that flair-mcp is wired but never carries a version — the pin
|
|
386
|
+
* comes from the client MCP config.
|
|
387
|
+
*/
|
|
388
|
+
export function extractFlairMcpPin(text) {
|
|
389
|
+
if (typeof text !== "string")
|
|
390
|
+
return null;
|
|
391
|
+
// `@tpsdev-ai/flair-mcp@<version>`; the version token runs until the first
|
|
392
|
+
// character that can't appear in a spec embedded in JSON args / TOML.
|
|
393
|
+
const m = text.match(/@tpsdev-ai\/flair-mcp@([0-9A-Za-z][^\s"'\],]*)/);
|
|
394
|
+
return m ? m[1] : null;
|
|
395
|
+
}
|
|
396
|
+
export function detectWiredFlairMcp(homeDir) {
|
|
397
|
+
let wired = false;
|
|
398
|
+
let pinnedVersion = null;
|
|
399
|
+
// The package name only ever appears in a Flair MCP wiring block, so its
|
|
400
|
+
// presence in a config's text is a reliable "flair-mcp is wired here" signal.
|
|
401
|
+
const note = (text) => {
|
|
402
|
+
if (!text || !text.includes(FLAIR_MCP_PACKAGE))
|
|
403
|
+
return;
|
|
404
|
+
wired = true;
|
|
405
|
+
if (!pinnedVersion) {
|
|
406
|
+
const pin = extractFlairMcpPin(text);
|
|
407
|
+
if (pin)
|
|
408
|
+
pinnedVersion = pin;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
// 1. The SessionStart hook (claude-code). Establishes wiring; unpinned by design.
|
|
412
|
+
const hook = checkSessionStartHook(homeDir);
|
|
413
|
+
if (hook.present && isFlairHookCommand(hook.command ?? ""))
|
|
414
|
+
note(hook.command);
|
|
415
|
+
// 2. Every known client's MCP config — a wired flair block carries the spec.
|
|
416
|
+
for (const client of ALL_CLIENTS) {
|
|
417
|
+
const configPath = withHome(homeDir, () => clientConfigPath(client.id));
|
|
418
|
+
note(readTextFile(configPath));
|
|
419
|
+
}
|
|
420
|
+
return { wired, pinnedVersion };
|
|
421
|
+
}
|
|
368
422
|
/**
|
|
369
423
|
* Merge-safe insert of a Flair SessionStart hook group into
|
|
370
424
|
* ~/.claude/settings.json — creates the file/array if absent, preserves any
|
package/dist/install/clients.js
CHANGED
|
@@ -204,9 +204,16 @@ function replaceCodexFlairBlock(raw, env) {
|
|
|
204
204
|
* Creates the file (and parent dir) if absent; preserves existing servers and
|
|
205
205
|
* any other top-level keys. Returns ok:true only when the file was written.
|
|
206
206
|
*/
|
|
207
|
-
function wireJsonMcp(configPath, label, env
|
|
207
|
+
function wireJsonMcp(configPath, label, env,
|
|
208
|
+
// The parenthetical appended to a successful wire/refresh message. Defaults to
|
|
209
|
+
// the confident "restart <label> to pick it up". A client whose end-to-end
|
|
210
|
+
// pickup Flair has NOT verified (Antigravity — flair#1209) passes an honest
|
|
211
|
+
// note instead, so the message claims only what it did (wrote the config), not
|
|
212
|
+
// that the client will read it.
|
|
213
|
+
pickupNote) {
|
|
208
214
|
const home = resolveHome();
|
|
209
215
|
const display = configPath.startsWith(home) ? "~" + configPath.slice(home.length) : configPath;
|
|
216
|
+
const note = pickupNote ?? `restart ${label} to pick it up`;
|
|
210
217
|
try {
|
|
211
218
|
let config = {};
|
|
212
219
|
if (existsSync(configPath)) {
|
|
@@ -229,7 +236,7 @@ function wireJsonMcp(configPath, label, env) {
|
|
|
229
236
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
230
237
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
231
238
|
const action = urlAgentMatch ? "refreshed pin in" : "wired";
|
|
232
|
-
return { ok: true, message: `${label}: ${action} ${display} (
|
|
239
|
+
return { ok: true, message: `${label}: ${action} ${display} (${note})` };
|
|
233
240
|
}
|
|
234
241
|
catch (err) {
|
|
235
242
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -256,6 +263,23 @@ function geminiConfigPath() {
|
|
|
256
263
|
function codexConfigPath() {
|
|
257
264
|
return join(resolveHome(), ".codex", "config.toml");
|
|
258
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Antigravity CLI (`agy`) + Antigravity 2.0 IDE + SDK: they share ONE central
|
|
268
|
+
* MCP config at ~/.gemini/config/mcp_config.json on every OS (flair#1209).
|
|
269
|
+
*
|
|
270
|
+
* This is a SIBLING of, and distinct from, Gemini CLI's ~/.gemini/settings.json
|
|
271
|
+
* (geminiConfigPath above) — both tools live under ~/.gemini but read different
|
|
272
|
+
* files, so wiring one never touches the other. Same standard `mcpServers`
|
|
273
|
+
* stdio schema (command/args/env) the JSON clients above use.
|
|
274
|
+
*
|
|
275
|
+
* Path per Antigravity's own docs (antigravity.google/docs/mcp) and a Google
|
|
276
|
+
* Developer Advocate write-up (atamel.dev "Where does Antigravity look for MCP
|
|
277
|
+
* Servers?"). NOTE: the end-to-end wiring has NOT been verified against a real
|
|
278
|
+
* `agy` install — see the PR body.
|
|
279
|
+
*/
|
|
280
|
+
function antigravityConfigPath() {
|
|
281
|
+
return join(resolveHome(), ".gemini", "config", "mcp_config.json");
|
|
282
|
+
}
|
|
259
283
|
/**
|
|
260
284
|
* Single dispatcher for "where does this client's MCP config live" — used by
|
|
261
285
|
* `flair doctor`'s client-integration checks (flair#588) to read the config
|
|
@@ -272,6 +296,8 @@ export function clientConfigPath(id) {
|
|
|
272
296
|
return geminiConfigPath();
|
|
273
297
|
case "cursor":
|
|
274
298
|
return cursorConfigPath();
|
|
299
|
+
case "antigravity":
|
|
300
|
+
return antigravityConfigPath();
|
|
275
301
|
}
|
|
276
302
|
}
|
|
277
303
|
// ---- Internal wiring functions --------------------------------------------------
|
|
@@ -330,6 +356,18 @@ function _wireGemini(env) {
|
|
|
330
356
|
function _wireCursor(env) {
|
|
331
357
|
return wireJsonMcp(cursorConfigPath(), "Cursor", env);
|
|
332
358
|
}
|
|
359
|
+
// Antigravity uses the same standard JSON `mcpServers` stdio schema as Gemini/
|
|
360
|
+
// Cursor (command/args/env), so wireJsonMcp merges into it byte-identically —
|
|
361
|
+
// only the config PATH differs (flair#1209).
|
|
362
|
+
//
|
|
363
|
+
// The success message deliberately does NOT claim "restart Antigravity to pick
|
|
364
|
+
// it up": Flair writes the config to the documented path, but has not verified
|
|
365
|
+
// end-to-end that a live `agy` reads it. So the message claims only the write,
|
|
366
|
+
// and asks the user to confirm pickup (flair#1209 review — honesty on an
|
|
367
|
+
// unverified integration).
|
|
368
|
+
function _wireAntigravity(env) {
|
|
369
|
+
return wireJsonMcp(antigravityConfigPath(), "Antigravity", env, "wiring unverified against a real agy — restart Antigravity and confirm the flair tools appear");
|
|
370
|
+
}
|
|
333
371
|
// ---- Exported detection & wiring array ------------------------------------------
|
|
334
372
|
export const ALL_CLIENTS = [
|
|
335
373
|
{
|
|
@@ -356,6 +394,13 @@ export const ALL_CLIENTS = [
|
|
|
356
394
|
bin: "cursor",
|
|
357
395
|
wire: _wireCursor,
|
|
358
396
|
},
|
|
397
|
+
{
|
|
398
|
+
id: "antigravity",
|
|
399
|
+
label: "Antigravity",
|
|
400
|
+
// Google's Antigravity CLI — the executable is `agy` (flair#1209).
|
|
401
|
+
bin: "agy",
|
|
402
|
+
wire: _wireAntigravity,
|
|
403
|
+
},
|
|
359
404
|
];
|
|
360
405
|
/**
|
|
361
406
|
* The summary `flair init` prints LAST.
|
|
@@ -432,3 +477,6 @@ export function wireGemini(env) {
|
|
|
432
477
|
export function wireCursor(env) {
|
|
433
478
|
return _wireCursor(env);
|
|
434
479
|
}
|
|
480
|
+
export function wireAntigravity(env) {
|
|
481
|
+
return _wireAntigravity(env);
|
|
482
|
+
}
|
|
@@ -73,8 +73,8 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
73
73
|
*
|
|
74
74
|
* Response:
|
|
75
75
|
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable,
|
|
76
|
-
* teammateFindingsIncluded, agentId, scope, soul,
|
|
77
|
-
* teammateFindings[, currentTaskHint][, predictedHint] }
|
|
76
|
+
* teammateFindingsIncluded, teammateFindingsTruncated, agentId, scope, soul,
|
|
77
|
+
* memories, predicted, teammateFindings, events[, currentTaskHint][, predictedHint] }
|
|
78
78
|
* The self-describing keys (flair#1182 part 1) — `agentId` (resolved caller),
|
|
79
79
|
* `scope` (read model applied to the caller), `soul`/`memories`/`predicted`
|
|
80
80
|
* (the caller's OWN records as structured containers), and `currentTaskHint`
|
|
@@ -84,8 +84,24 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
84
84
|
* mirror (opt-in via includeContext). Cross-agent teammate findings ship in
|
|
85
85
|
* the `teammateFindings` container (own memories in `memories`), counted by
|
|
86
86
|
* `teammateFindingsIncluded` (a separate denominator from `memoriesIncluded`,
|
|
87
|
-
* which is own-scoped so it never exceeds `memoriesAvailable`).
|
|
88
|
-
*
|
|
87
|
+
* which is own-scoped so it never exceeds `memoriesAvailable`). flair#1206 —
|
|
88
|
+
* org events ship in their OWN structured `events` container (ALWAYS present,
|
|
89
|
+
* `[]` when none), so a connector reading the structured payload gets them even
|
|
90
|
+
* when the prose `context` is off (the /mcp default); before #1206 they lived
|
|
91
|
+
* ONLY in the prose string and were orphaned at includeContext=false.
|
|
92
|
+
*
|
|
93
|
+
* flair#1199 CAP CONTRACT (corrected): `maxTokens` is the HARD cap on CONTENT
|
|
94
|
+
* SELECTION — the shared `tokenBudget` starts at `maxTokens` and every admitted
|
|
95
|
+
* soul/memory/finding line is gated against the remaining budget, so the sum of
|
|
96
|
+
* selected CONTENT never exceeds `maxTokens`. `tokenEstimate` HONESTLY reports
|
|
97
|
+
* the real serialized payload (`JSON.stringify(responseBody)`), which includes
|
|
98
|
+
* the structured-container JSON scaffolding and so MAY exceed `maxTokens` by
|
|
99
|
+
* that overhead — measurement (honest reporting) is deliberately decoupled from
|
|
100
|
+
* budgeting (what to select). This is the flair#1207 fix: #1199 had folded a
|
|
101
|
+
* per-item structured overhead + a scaffolding reserve INTO the selection
|
|
102
|
+
* budget, which silently shrank recall below 0.44.6 for the same `maxTokens`;
|
|
103
|
+
* the overhead is a reporting concern (already captured by `tokenEstimate`),
|
|
104
|
+
* never a selection constraint, so it no longer shrinks the content budget.
|
|
89
105
|
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
90
106
|
* came back empty.
|
|
91
107
|
*/
|
|
@@ -123,17 +139,19 @@ const MAX_CANDIDATE_POOL = 100;
|
|
|
123
139
|
// `minScore` request param) — preserved verbatim from the original raw
|
|
124
140
|
// JS dot-product scan's `.filter((s) => s.score > 0.3)`.
|
|
125
141
|
const TASK_RELEVANCE_FLOOR = 0.3;
|
|
126
|
-
// flair#
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
142
|
+
// flair#1207 — the per-item structured-payload overhead that #1199 charged
|
|
143
|
+
// against the content-selection budget (a `+ STRUCT_ITEM_OVERHEAD_TOKENS = 70`
|
|
144
|
+
// added to every item's cost, PLUS a `structOverheadReserve` pre-deducted from
|
|
145
|
+
// the starting budget) has been REMOVED. It conflated measurement with
|
|
146
|
+
// budgeting: `tokenEstimate` already measures the real serialized payload
|
|
147
|
+
// (JSON.stringify(responseBody)) — the structured JSON scaffolding overhead is
|
|
148
|
+
// captured there, honestly. Folding it into the SELECTION budget too
|
|
149
|
+
// double-penalized and silently shrank recall below 0.44.6 for the same
|
|
150
|
+
// `maxTokens` (6 findings → 3). The content budget is now `maxTokens` again
|
|
151
|
+
// (0.44.6 selection capacity), and each item's cost is just the rendered prose
|
|
152
|
+
// line — while `tokenEstimate` keeps reporting the true serialized size, which
|
|
153
|
+
// may exceed `maxTokens` by the scaffolding overhead. See the module-doc CAP
|
|
154
|
+
// CONTRACT above.
|
|
137
155
|
// Rough token estimate: ~4 chars per token for English text
|
|
138
156
|
function estimateTokens(text) {
|
|
139
157
|
return Math.ceil(text.length / 4);
|
|
@@ -222,19 +240,22 @@ export class BootstrapMemories extends Resource {
|
|
|
222
240
|
collision: [],
|
|
223
241
|
events: [],
|
|
224
242
|
};
|
|
225
|
-
// flair#
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// scaffolding
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
243
|
+
// flair#1207 — the content-SELECTION budget is `maxTokens`, matching 0.44.6
|
|
244
|
+
// capacity. #1199 pre-deducted a `structOverheadReserve` (min(600, 15% of
|
|
245
|
+
// maxTokens)) here to "reserve headroom" for the structured-container JSON
|
|
246
|
+
// scaffolding, on the theory that `maxTokens` should bound the serialized
|
|
247
|
+
// payload. That silently shrank the content budget and, combined with the
|
|
248
|
+
// per-item overhead (also removed), cut recall below 0.44.6 for the same
|
|
249
|
+
// `maxTokens` (#1207). The reserve is gone: `maxTokens` is the HARD cap on
|
|
250
|
+
// CONTENT SELECTION only. `tokenEstimate` (below) still reports the real
|
|
251
|
+
// serialized size honestly — which may exceed `maxTokens` by the scaffolding
|
|
252
|
+
// overhead, exactly as 0.44.6's payload did (0.44.6 just under-measured it).
|
|
233
253
|
// Single shared budget across soul + every memory section (soul used to be
|
|
234
254
|
// budgeted SEPARATELY and added ON TOP, so context alone could reach
|
|
235
|
-
// 1.4×maxTokens; #1199
|
|
236
|
-
//
|
|
237
|
-
|
|
255
|
+
// 1.4×maxTokens; #1199 folded soul into this shared budget — that part
|
|
256
|
+
// stays). Every admitted line is gated against the remaining budget, so the
|
|
257
|
+
// sum of selected CONTENT never exceeds `maxTokens`.
|
|
258
|
+
let tokenBudget = Math.max(0, maxTokens);
|
|
238
259
|
// Own memories included in the payload (permanent + recent + predicted +
|
|
239
260
|
// own task-relevant). Denominator is `memoriesAvailable` (own-scoped), so
|
|
240
261
|
// memoriesIncluded ≤ memoriesAvailable always holds (#1199 coherent
|
|
@@ -246,6 +267,13 @@ export class BootstrapMemories extends Resource {
|
|
|
246
267
|
// denominator than own memories): counting these into `memoriesIncluded`
|
|
247
268
|
// is what let one client see included(9) > available(3).
|
|
248
269
|
let teammateFindingsIncluded = 0;
|
|
270
|
+
// flair#1207 — teammate findings SKIPPED for size in the task-relevant
|
|
271
|
+
// packing loop. That loop `continue`s past an over-budget record silently;
|
|
272
|
+
// without a counter, a client can't tell "no relevant teammate finding" from
|
|
273
|
+
// "a relevant one existed but didn't fit the budget" (Sherlock's #1207
|
|
274
|
+
// self-describing-size-skip note; own-memory size-skips already increment
|
|
275
|
+
// `memoriesTruncated`, but that loop never did — now both do).
|
|
276
|
+
let teammateFindingsTruncated = 0;
|
|
249
277
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
250
278
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
251
279
|
// the caller has nothing), so a client can tell an *empty* instance from
|
|
@@ -267,6 +295,17 @@ export class BootstrapMemories extends Resource {
|
|
|
267
295
|
// them. Kept SEPARATE from `memories`/`predicted` (which stay own-only per
|
|
268
296
|
// the #1182 boundary) and clearly attributed via `source`.
|
|
269
297
|
const includedTeammateFindings = [];
|
|
298
|
+
// flair#1206 — org events get their OWN structured container. Before #1206
|
|
299
|
+
// they lived ONLY in the prose `context` string ("## Recent Org Events"),
|
|
300
|
+
// so at includeContext=false (the /mcp default) they were counted in
|
|
301
|
+
// `sections.events` and measured into `tokenEstimate` (when prose was on) but
|
|
302
|
+
// NEVER delivered in any field a connector could read — orphaned. Populated
|
|
303
|
+
// from the SAME deduped+sliced set the prose lines are (so count, charge and
|
|
304
|
+
// delivery all key off one thing), ALWAYS emitted (`[]` when none), and the
|
|
305
|
+
// targetIds relevance filter is already applied upstream (see the OrgEvent
|
|
306
|
+
// read below). Declared out here so it is in scope for the response body even
|
|
307
|
+
// if the events read (in a try/catch) yields nothing.
|
|
308
|
+
const includedEvents = [];
|
|
270
309
|
const leanMemory = (m, section) => ({
|
|
271
310
|
id: m.id,
|
|
272
311
|
content: m.content,
|
|
@@ -528,7 +567,7 @@ export class BootstrapMemories extends Resource {
|
|
|
528
567
|
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
529
568
|
for (const m of permanent) {
|
|
530
569
|
const line = formatMemory(m, agentId);
|
|
531
|
-
const cost = estimateTokens(line)
|
|
570
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
532
571
|
if (cost <= tokenBudget) {
|
|
533
572
|
sections.permanent.push(line);
|
|
534
573
|
includedOwnMemories.push(leanMemory(m, "permanent"));
|
|
@@ -600,7 +639,7 @@ export class BootstrapMemories extends Resource {
|
|
|
600
639
|
let recentSpent = 0;
|
|
601
640
|
for (const m of recent) {
|
|
602
641
|
const line = formatMemory(m, agentId);
|
|
603
|
-
const cost = estimateTokens(line)
|
|
642
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
604
643
|
if (recentSpent + cost > recentBudget) {
|
|
605
644
|
memoriesTruncated++;
|
|
606
645
|
continue;
|
|
@@ -643,7 +682,7 @@ export class BootstrapMemories extends Resource {
|
|
|
643
682
|
let predictedSpent = 0;
|
|
644
683
|
for (const m of subjectMemories) {
|
|
645
684
|
const line = formatMemory(m, agentId);
|
|
646
|
-
const cost = estimateTokens(line)
|
|
685
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
647
686
|
if (predictedSpent + cost > predictedBudget) {
|
|
648
687
|
memoriesTruncated++;
|
|
649
688
|
continue;
|
|
@@ -818,9 +857,19 @@ export class BootstrapMemories extends Resource {
|
|
|
818
857
|
// section double-spends.
|
|
819
858
|
for (const { memory: m } of scored) {
|
|
820
859
|
const line = formatMemory(m, agentId);
|
|
821
|
-
const cost = estimateTokens(line)
|
|
822
|
-
if (cost > tokenBudget)
|
|
860
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
861
|
+
if (cost > tokenBudget) {
|
|
862
|
+
// flair#1207 — a size-skip in the score-ordered task-relevant loop
|
|
863
|
+
// is no longer silent: record it on the counter matching the record's
|
|
864
|
+
// denominator (own → memoriesTruncated, teammate → the separate
|
|
865
|
+
// teammateFindingsTruncated), so a client can distinguish "no relevant
|
|
866
|
+
// finding" from "a relevant finding didn't fit the budget".
|
|
867
|
+
if (m._source)
|
|
868
|
+
teammateFindingsTruncated++;
|
|
869
|
+
else
|
|
870
|
+
memoriesTruncated++;
|
|
823
871
|
continue;
|
|
872
|
+
}
|
|
824
873
|
if (m._source) {
|
|
825
874
|
sections.teammate.push(line);
|
|
826
875
|
// flair#1199 — cross-agent teammate findings join their OWN
|
|
@@ -1030,6 +1079,24 @@ export class BootstrapMemories extends Resource {
|
|
|
1030
1079
|
const mins = Math.floor(elapsed / 60_000);
|
|
1031
1080
|
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
1032
1081
|
sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
|
|
1082
|
+
// flair#1206 — the SAME deduped+sliced event, structured, so a connector
|
|
1083
|
+
// reading the containers gets it when prose `context` is off (the /mcp
|
|
1084
|
+
// default). Same set as the prose line ⇒ `sections.events` (the count),
|
|
1085
|
+
// the `tokenEstimate` charge (this array is always in the body), and the
|
|
1086
|
+
// delivery all key off one thing. Optional fields (detail/targetIds/scope)
|
|
1087
|
+
// are omitted when absent so the object stays lean. The targetIds
|
|
1088
|
+
// relevance filter and #1200 content-signature dedup were already applied
|
|
1089
|
+
// upstream (eventResults → eventBySignature), so this is a pure move from
|
|
1090
|
+
// prose to structured — no scope widening, no re-introduced duplicates.
|
|
1091
|
+
includedEvents.push({
|
|
1092
|
+
id: evt.id,
|
|
1093
|
+
kind: evt.kind,
|
|
1094
|
+
summary: evt.summary,
|
|
1095
|
+
...(evt.detail != null ? { detail: evt.detail } : {}),
|
|
1096
|
+
...(Array.isArray(evt.targetIds) && evt.targetIds.length > 0 ? { targetIds: evt.targetIds } : {}),
|
|
1097
|
+
createdAt: evt.createdAt ?? null,
|
|
1098
|
+
...(evt.scope != null ? { scope: evt.scope } : {}),
|
|
1099
|
+
});
|
|
1033
1100
|
}
|
|
1034
1101
|
}
|
|
1035
1102
|
catch {
|
|
@@ -1165,6 +1232,13 @@ export class BootstrapMemories extends Resource {
|
|
|
1165
1232
|
// (always present, `[]` when none), so a connector that consumes the
|
|
1166
1233
|
// containers still gets them when prose `context` is off.
|
|
1167
1234
|
teammateFindings: includedTeammateFindings,
|
|
1235
|
+
// flair#1206 — org events as a structured container (always present, `[]`
|
|
1236
|
+
// when none, same self-describing-empty-state pattern as the containers
|
|
1237
|
+
// above). Before #1206 events lived ONLY in the prose `context`, so at the
|
|
1238
|
+
// /mcp default (includeContext=false) they were counted+measured but never
|
|
1239
|
+
// delivered. Deduped (#1200) and targetIds-scoped (same set as the prose
|
|
1240
|
+
// "## Recent Org Events" lines), so count/charge/delivery all agree.
|
|
1241
|
+
events: includedEvents,
|
|
1168
1242
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1169
1243
|
...(predictedHint ? { predictedHint } : {}),
|
|
1170
1244
|
...(trust ? { trust } : {}),
|
|
@@ -1192,14 +1266,25 @@ export class BootstrapMemories extends Resource {
|
|
|
1192
1266
|
// so it's never confused with the own-memory counters.
|
|
1193
1267
|
teammateFindingsIncluded,
|
|
1194
1268
|
memoriesTruncated,
|
|
1269
|
+
// flair#1207 — teammate findings skipped for size in the task-relevant loop
|
|
1270
|
+
// (own size-skips there increment memoriesTruncated). Surfacing this makes
|
|
1271
|
+
// a size-skip self-describing: "a relevant teammate finding didn't fit"
|
|
1272
|
+
// is now distinguishable from "no relevant teammate finding".
|
|
1273
|
+
teammateFindingsTruncated,
|
|
1195
1274
|
};
|
|
1196
1275
|
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1197
1276
|
// caller receives (the structured containers included), not just the prose
|
|
1198
1277
|
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1199
1278
|
// string, so it under-reported by ~2× once the structured fields shipped
|
|
1200
|
-
// alongside
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1279
|
+
// alongside. Measured over the assembled body (the ~1-line tokenEstimate
|
|
1280
|
+
// field it omits is negligible). flair#1207 CAP CONTRACT: this is an HONEST
|
|
1281
|
+
// report of the real serialized size, NOT a value bounded by `maxTokens`.
|
|
1282
|
+
// `maxTokens` is the hard cap on CONTENT SELECTION (the shared tokenBudget);
|
|
1283
|
+
// the structured-container JSON scaffolding is genuine payload the caller
|
|
1284
|
+
// pays for, so tokenEstimate MAY exceed `maxTokens` by that overhead. Do not
|
|
1285
|
+
// "fix" an over-maxTokens tokenEstimate by shrinking selection — that is the
|
|
1286
|
+
// exact #1199→#1207 regression (it dropped relevant findings). If the real
|
|
1287
|
+
// payload consistently overruns for a use case, raise `maxTokens`.
|
|
1203
1288
|
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1204
1289
|
return { ...responseBody, tokenEstimate };
|
|
1205
1290
|
}
|
|
@@ -631,7 +631,7 @@ export const TOOLS = {
|
|
|
631
631
|
inputSchema: {
|
|
632
632
|
type: "object",
|
|
633
633
|
properties: {
|
|
634
|
-
maxTokens: { type: "number", description: "
|
|
634
|
+
maxTokens: { type: "number", description: "Content-selection budget in tokens (default 4000): the hard cap on how much soul/memory/finding CONTENT is selected. The actual serialized response (reported by tokenEstimate) may exceed this by the structured-container JSON scaffolding — maxTokens bounds what is selected, not the raw output size. Raise it to include more content." },
|
|
635
635
|
currentTask: { type: "string", description: "Current task — enables semantic search for relevant memories" },
|
|
636
636
|
channel: { type: "string", description: "Channel name (discord, tps-mail, claude-code)" },
|
|
637
637
|
surface: { type: "string", description: "Surface name (tps-build, tps-review, cli-session)" },
|
|
@@ -151,13 +151,21 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
151
151
|
else if (Number.isFinite(validFromMs) && validFromMs > now) {
|
|
152
152
|
validityStatus = "future";
|
|
153
153
|
}
|
|
154
|
-
// #1201 —
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
// flair#1201 (refined) — carry BOTH temporal signals rather than collapsing
|
|
155
|
+
// to one. `ageDays` is TRUE AGE (days since `createdAt`, fallback updatedAt);
|
|
156
|
+
// `staleDays` is FRESHNESS (days since `updatedAt`, fallback createdAt). The
|
|
157
|
+
// first #1201 pass keyed ageDays off updatedAt only, which overcorrected — a
|
|
158
|
+
// record created weeks ago but edited today then read as "0 days old", losing
|
|
159
|
+
// its true age. Both are the record's OWN fields (never a superseded
|
|
160
|
+
// predecessor's — #1189), so neither reintroduces lineage-inheritance. For a
|
|
161
|
+
// never-updated record updatedAt == createdAt, so ageDays == staleDays.
|
|
162
|
+
const createdMs = parseTime(createdAt ?? updatedAt);
|
|
163
|
+
const ageDays = Number.isFinite(createdMs)
|
|
164
|
+
? Math.max(0, Math.floor((now - createdMs) / MS_PER_DAY))
|
|
165
|
+
: null;
|
|
166
|
+
const updatedMs = parseTime(updatedAt ?? createdAt);
|
|
167
|
+
const staleDays = Number.isFinite(updatedMs)
|
|
168
|
+
? Math.max(0, Math.floor((now - updatedMs) / MS_PER_DAY))
|
|
161
169
|
: null;
|
|
162
170
|
return {
|
|
163
171
|
author: typeof record.agentId === "string" ? record.agentId : null,
|
|
@@ -172,6 +180,7 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
172
180
|
createdAt,
|
|
173
181
|
updatedAt,
|
|
174
182
|
ageDays,
|
|
183
|
+
staleDays,
|
|
175
184
|
supersedes: typeof record.supersedes === "string" ? record.supersedes : null,
|
|
176
185
|
// flair#744 refinement — confidence band from the result's absolute
|
|
177
186
|
// similarity (null when there is no signal to judge). Pure, global,
|
package/docs/integrations.md
CHANGED
|
@@ -15,6 +15,7 @@ Where Flair already runs. Each integration shown here is a working surface — t
|
|
|
15
15
|
| **Continue.dev** | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Standard MCP server |
|
|
16
16
|
| **OpenAI Codex CLI** | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Standard MCP server |
|
|
17
17
|
| **Gemini CLI** | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Standard MCP server |
|
|
18
|
+
| **Antigravity CLI** (`agy`) | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | `~/.gemini/config/mcp_config.json`; pickup by a live `agy` pending verification |
|
|
18
19
|
| **Goose** (block/goose) | [`flair-mcp`](#claude-code-cursor-codex-gemini-cli-continuedev-via-flair-mcp) | MCP config | Goose ships native MCP support |
|
|
19
20
|
| **LangGraph (TS)** | [`langgraph-flair`](#langgraph-typescript) | FlairClient | Drop-in `BaseStore` |
|
|
20
21
|
| **OpenClaw** | [`openclaw-flair`](#openclaw) | Ed25519 | Native plugin + context engine |
|
|
@@ -69,6 +70,8 @@ FLAIR_AGENT_ID = "codex"
|
|
|
69
70
|
|
|
70
71
|
**Gemini CLI** (`~/.gemini/settings.json`): same shape as Cursor.
|
|
71
72
|
|
|
73
|
+
**Antigravity CLI** (`agy`) (`~/.gemini/config/mcp_config.json` — Antigravity's own MCP config, separate from Gemini CLI's `settings.json`): same shape as Cursor. Newly added; the config path follows Antigravity's documentation, but Flair has not yet verified end-to-end pickup by a live `agy` — after wiring, restart Antigravity and confirm the flair tools appear.
|
|
74
|
+
|
|
72
75
|
**Continue.dev** (`~/.continue/config.json`):
|
|
73
76
|
```json
|
|
74
77
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.9",
|
|
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",
|