@tpsdev-ai/flair 0.44.7 → 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 +111 -27
- package/dist/doctor-client.js +55 -1
- package/dist/install/clients.js +50 -2
- package/dist/resources/MemoryBootstrap.js +286 -33
- package/dist/resources/mcp-tools.js +9 -1
- package/dist/resources/trust-block.js +16 -1
- 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++;
|
|
@@ -14328,7 +14406,13 @@ program
|
|
|
14328
14406
|
console.error(`${render.icons.error} No context available.`);
|
|
14329
14407
|
process.exit(1);
|
|
14330
14408
|
}
|
|
14331
|
-
|
|
14409
|
+
// flair#1199 — the budget footer reflects the PROSE the human injects
|
|
14410
|
+
// (stdout = result.context), not the full serialized payload. tokenEstimate
|
|
14411
|
+
// now measures the whole response (structured containers + prose), which
|
|
14412
|
+
// the CLI's structured fields the human doesn't read would inflate.
|
|
14413
|
+
const tokensUsed = typeof result.context === "string" && result.context.length > 0
|
|
14414
|
+
? Math.ceil(result.context.length / 4)
|
|
14415
|
+
: (result.tokenEstimate ?? 0);
|
|
14332
14416
|
const maxTokens = parseInt(opts.maxTokens, 10);
|
|
14333
14417
|
const included = result.memoriesIncluded ?? 0;
|
|
14334
14418
|
const truncated = result.memoriesTruncated ?? 0;
|
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
|
+
}
|
|
@@ -59,7 +59,11 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
59
59
|
*
|
|
60
60
|
* Request:
|
|
61
61
|
* { agentId, currentTask?, maxTokens?, includeSoul?, since?,
|
|
62
|
-
* channel?, surface?, subjects?, entities? }
|
|
62
|
+
* channel?, surface?, subjects?, entities?, includeContext? }
|
|
63
|
+
* `includeContext` (flair#1199): whether to assemble the prose `context`
|
|
64
|
+
* mirror. Default true here (the resource/REST/CLI path); the /mcp bootstrap
|
|
65
|
+
* wrapper passes false so a token-budgeted connector — which reads the
|
|
66
|
+
* structured containers — never receives the same bodies twice.
|
|
63
67
|
* `entities` (flair#681): the caller's own declared attention-plane
|
|
64
68
|
* vocabulary strings (see resources/entity-vocab.ts) for collision
|
|
65
69
|
* surfacing's entity-overlap join. Invalid entries are silently dropped
|
|
@@ -69,12 +73,37 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
69
73
|
*
|
|
70
74
|
* Response:
|
|
71
75
|
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable,
|
|
72
|
-
*
|
|
76
|
+
* teammateFindingsIncluded, teammateFindingsTruncated, agentId, scope, soul,
|
|
77
|
+
* memories, predicted, teammateFindings, events[, currentTaskHint][, predictedHint] }
|
|
73
78
|
* The self-describing keys (flair#1182 part 1) — `agentId` (resolved caller),
|
|
74
79
|
* `scope` (read model applied to the caller), `soul`/`memories`/`predicted`
|
|
75
80
|
* (the caller's OWN records as structured containers), and `currentTaskHint`
|
|
76
81
|
* (present only when currentTask is absent/blank) — are ALWAYS emitted so a
|
|
77
82
|
* client can tell an empty instance from one that doesn't support them.
|
|
83
|
+
* flair#1199 — the structured containers are CANONICAL; `context` is a prose
|
|
84
|
+
* mirror (opt-in via includeContext). Cross-agent teammate findings ship in
|
|
85
|
+
* the `teammateFindings` container (own memories in `memories`), counted by
|
|
86
|
+
* `teammateFindingsIncluded` (a separate denominator from `memoriesIncluded`,
|
|
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.
|
|
105
|
+
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
106
|
+
* came back empty.
|
|
78
107
|
*/
|
|
79
108
|
// Collision surfacing (flair#681) tunables.
|
|
80
109
|
const COLLISION_WINDOW_DAYS = 7;
|
|
@@ -110,6 +139,19 @@ const MAX_CANDIDATE_POOL = 100;
|
|
|
110
139
|
// `minScore` request param) — preserved verbatim from the original raw
|
|
111
140
|
// JS dot-product scan's `.filter((s) => s.score > 0.3)`.
|
|
112
141
|
const TASK_RELEVANCE_FLOOR = 0.3;
|
|
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.
|
|
113
155
|
// Rough token estimate: ~4 chars per token for English text
|
|
114
156
|
function estimateTokens(text) {
|
|
115
157
|
return Math.ceil(text.length / 4);
|
|
@@ -151,7 +193,16 @@ export class BootstrapMemories extends Resource {
|
|
|
151
193
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
152
194
|
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
153
195
|
abstain = false, // flair#744 slice 2 — opt-in task-relevance abstention
|
|
154
|
-
|
|
196
|
+
// flair#1199 — whether to assemble the prose `context` string. The
|
|
197
|
+
// structured containers (soul/memories/predicted/teammateFindings) are the
|
|
198
|
+
// CANONICAL payload; `context` is a human/agent-readable MIRROR of the same
|
|
199
|
+
// bytes. Default TRUE here (the resource/REST/CLI path has always emitted
|
|
200
|
+
// prose, and every direct caller reads it) — but the /mcp bootstrap wrapper
|
|
201
|
+
// (resources/mcp-tools.ts) passes `false` by default, so a token-budgeted
|
|
202
|
+
// connector, which consumes the structured fields, never receives the same
|
|
203
|
+
// bodies twice. When false, `context` is a compact structural pointer (no
|
|
204
|
+
// bodies), so nothing crosses the wire twice on that path.
|
|
205
|
+
includeContext = true, } = data || {};
|
|
155
206
|
// Authenticated identity lives on getContext().request — `this.request` is
|
|
156
207
|
// NOT populated on Harper v5 Resources. Reading it returned undefined and
|
|
157
208
|
// the scope check was silently bypassed, letting a non-admin agent read
|
|
@@ -189,27 +240,80 @@ export class BootstrapMemories extends Resource {
|
|
|
189
240
|
collision: [],
|
|
190
241
|
events: [],
|
|
191
242
|
};
|
|
192
|
-
|
|
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).
|
|
253
|
+
// Single shared budget across soul + every memory section (soul used to be
|
|
254
|
+
// budgeted SEPARATELY and added ON TOP, so context alone could reach
|
|
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);
|
|
259
|
+
// Own memories included in the payload (permanent + recent + predicted +
|
|
260
|
+
// own task-relevant). Denominator is `memoriesAvailable` (own-scoped), so
|
|
261
|
+
// memoriesIncluded ≤ memoriesAvailable always holds (#1199 coherent
|
|
262
|
+
// counters). Cross-agent teammate findings are counted separately below.
|
|
193
263
|
let memoriesIncluded = 0;
|
|
194
264
|
let memoriesAvailable = 0;
|
|
195
265
|
let memoriesTruncated = 0;
|
|
266
|
+
// flair#1199 — cross-agent teammate findings included (a DIFFERENT
|
|
267
|
+
// denominator than own memories): counting these into `memoriesIncluded`
|
|
268
|
+
// is what let one client see included(9) > available(3).
|
|
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;
|
|
196
277
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
197
278
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
198
279
|
// the caller has nothing), so a client can tell an *empty* instance from
|
|
199
280
|
// one that doesn't support these keys at all — and can read the caller's
|
|
200
281
|
// own soul/memories as structured data instead of parsing the `context`
|
|
201
|
-
// markdown string.
|
|
202
|
-
// (permanent/recent/relevant/predicted are all agentId==self
|
|
203
|
-
//
|
|
204
|
-
//
|
|
282
|
+
// markdown string. `soul`/`memories`/`predicted` are scoped to the CALLER'S
|
|
283
|
+
// OWN records only (permanent/recent/relevant/predicted are all agentId==self
|
|
284
|
+
// reads). flair#1199 — cross-agent teammate findings now have their OWN
|
|
285
|
+
// structured container (`teammateFindings`, below), attributed via `source`,
|
|
286
|
+
// rather than living only in the prose `context` (which is opt-in as of
|
|
287
|
+
// #1199); the own-only containers still carry no other agent's data.
|
|
205
288
|
const soulMap = {};
|
|
206
289
|
const includedOwnMemories = [];
|
|
207
290
|
const includedPredicted = [];
|
|
291
|
+
// flair#1199 — teammate (cross-agent) findings get their OWN structured
|
|
292
|
+
// container. Before #1199 they lived ONLY in the prose `context`; now that
|
|
293
|
+
// `context` is an opt-in mirror (default off on the /mcp path), they need a
|
|
294
|
+
// structured home so a connector that consumes the containers still sees
|
|
295
|
+
// them. Kept SEPARATE from `memories`/`predicted` (which stay own-only per
|
|
296
|
+
// the #1182 boundary) and clearly attributed via `source`.
|
|
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 = [];
|
|
208
309
|
const leanMemory = (m, section) => ({
|
|
209
310
|
id: m.id,
|
|
210
311
|
content: m.content,
|
|
211
312
|
durability: m.durability ?? null,
|
|
212
313
|
createdAt: m.createdAt ?? null,
|
|
314
|
+
// #1201 — the record's own last-write time, so a structured consumer can
|
|
315
|
+
// compute freshness off the same anchor the trust block's ageDays uses.
|
|
316
|
+
updatedAt: m.updatedAt ?? null,
|
|
213
317
|
agentId: m.agentId ?? agentId,
|
|
214
318
|
subject: m.subject ?? null,
|
|
215
319
|
section,
|
|
@@ -257,12 +361,15 @@ export class BootstrapMemories extends Resource {
|
|
|
257
361
|
if (maxChars > 100) {
|
|
258
362
|
const truncated = `**${entry.key}:** ${entry.line.slice(entry.key.length + 6, entry.key.length + 6 + maxChars)}…(truncated)`;
|
|
259
363
|
sections.soul.push(truncated);
|
|
260
|
-
|
|
364
|
+
const cost = estimateTokens(truncated);
|
|
365
|
+
soulTokens += cost;
|
|
366
|
+
tokenBudget -= cost; // #1199 — soul draws from the shared budget
|
|
261
367
|
}
|
|
262
368
|
continue;
|
|
263
369
|
}
|
|
264
370
|
sections.soul.push(entry.line);
|
|
265
371
|
soulTokens += entry.tokens;
|
|
372
|
+
tokenBudget -= entry.tokens; // #1199 — soul draws from the shared budget
|
|
266
373
|
}
|
|
267
374
|
}
|
|
268
375
|
// --- 1b. Skill assignments (ordered by priority, conflict detection) ---
|
|
@@ -388,14 +495,23 @@ export class BootstrapMemories extends Resource {
|
|
|
388
495
|
// requested, so a non-trust bootstrap fetches (and returns) exactly what it
|
|
389
496
|
// did before. These records are never returned raw when the block is off,
|
|
390
497
|
// so widening the select cannot change the off-path response bytes.
|
|
498
|
+
// #1201 — `updatedAt` is projected on BOTH paths (not just the trust path):
|
|
499
|
+
// the structured `memories`/`predicted` containers carry it so a consumer
|
|
500
|
+
// can compute freshness, and the trust block's ageDays keys off it.
|
|
391
501
|
const OWN_SELECT = includeTrust
|
|
392
|
-
? ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags", "provenance", "usageCount", "validFrom"]
|
|
393
|
-
: ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
502
|
+
? ["id", "agentId", "content", "durability", "createdAt", "updatedAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags", "provenance", "usageCount", "validFrom"]
|
|
503
|
+
: ["id", "agentId", "content", "durability", "createdAt", "updatedAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
394
504
|
// flair#744 slice 1 — the Memory records that became visible lines in the
|
|
395
505
|
// memory-bearing sections (permanent/recent/predicted/relevant/teammate),
|
|
396
506
|
// collected as they're added so the opt-in `trust` array below can carry a
|
|
397
507
|
// self-contained block per included memory. Stays empty (and unused) when
|
|
398
|
-
// includeTrust is off.
|
|
508
|
+
// includeTrust is off. flair#1201 — each entry carries the SECTION it landed
|
|
509
|
+
// in, so a trust entry's `matchQuality` is legible: null on a lifecycle
|
|
510
|
+
// section (permanent/recent/predicted — not a retrieval surface) reads as
|
|
511
|
+
// "not scored", not as a scoring failure, and a band on a retrieval section
|
|
512
|
+
// (relevant/teammate) is applied by the SAME rule to own and teammate
|
|
513
|
+
// records. Fixes the "own recent → null while teammate → strong looks like
|
|
514
|
+
// my own records scored worse" misread.
|
|
399
515
|
const includedTrustMemories = [];
|
|
400
516
|
// flair#744 slice 2 — the best absolute semantic similarity seen while
|
|
401
517
|
// scoring the task-relevant candidate pool (section 4). Drives the opt-in
|
|
@@ -451,12 +567,12 @@ export class BootstrapMemories extends Resource {
|
|
|
451
567
|
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
452
568
|
for (const m of permanent) {
|
|
453
569
|
const line = formatMemory(m, agentId);
|
|
454
|
-
const cost = estimateTokens(line);
|
|
570
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
455
571
|
if (cost <= tokenBudget) {
|
|
456
572
|
sections.permanent.push(line);
|
|
457
573
|
includedOwnMemories.push(leanMemory(m, "permanent"));
|
|
458
574
|
if (includeTrust)
|
|
459
|
-
includedTrustMemories.push(m);
|
|
575
|
+
includedTrustMemories.push({ m, section: "permanent" });
|
|
460
576
|
tokenBudget -= cost;
|
|
461
577
|
memoriesIncluded++;
|
|
462
578
|
}
|
|
@@ -523,7 +639,7 @@ export class BootstrapMemories extends Resource {
|
|
|
523
639
|
let recentSpent = 0;
|
|
524
640
|
for (const m of recent) {
|
|
525
641
|
const line = formatMemory(m, agentId);
|
|
526
|
-
const cost = estimateTokens(line);
|
|
642
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
527
643
|
if (recentSpent + cost > recentBudget) {
|
|
528
644
|
memoriesTruncated++;
|
|
529
645
|
continue;
|
|
@@ -531,7 +647,7 @@ export class BootstrapMemories extends Resource {
|
|
|
531
647
|
sections.recent.push(line);
|
|
532
648
|
includedOwnMemories.push(leanMemory(m, "recent"));
|
|
533
649
|
if (includeTrust)
|
|
534
|
-
includedTrustMemories.push(m);
|
|
650
|
+
includedTrustMemories.push({ m, section: "recent" });
|
|
535
651
|
recentSpent += cost;
|
|
536
652
|
tokenBudget -= cost;
|
|
537
653
|
memoriesIncluded++;
|
|
@@ -566,7 +682,7 @@ export class BootstrapMemories extends Resource {
|
|
|
566
682
|
let predictedSpent = 0;
|
|
567
683
|
for (const m of subjectMemories) {
|
|
568
684
|
const line = formatMemory(m, agentId);
|
|
569
|
-
const cost = estimateTokens(line);
|
|
685
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
570
686
|
if (predictedSpent + cost > predictedBudget) {
|
|
571
687
|
memoriesTruncated++;
|
|
572
688
|
continue;
|
|
@@ -574,7 +690,7 @@ export class BootstrapMemories extends Resource {
|
|
|
574
690
|
sections.predicted.push(line);
|
|
575
691
|
includedPredicted.push(leanMemory(m, "predicted"));
|
|
576
692
|
if (includeTrust)
|
|
577
|
-
includedTrustMemories.push(m);
|
|
693
|
+
includedTrustMemories.push({ m, section: "predicted" });
|
|
578
694
|
predictedSpent += cost;
|
|
579
695
|
tokenBudget -= cost;
|
|
580
696
|
memoriesIncluded++;
|
|
@@ -741,22 +857,52 @@ export class BootstrapMemories extends Resource {
|
|
|
741
857
|
// section double-spends.
|
|
742
858
|
for (const { memory: m } of scored) {
|
|
743
859
|
const line = formatMemory(m, agentId);
|
|
744
|
-
const cost = estimateTokens(line);
|
|
745
|
-
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++;
|
|
746
871
|
continue;
|
|
872
|
+
}
|
|
747
873
|
if (m._source) {
|
|
748
874
|
sections.teammate.push(line);
|
|
875
|
+
// flair#1199 — cross-agent teammate findings join their OWN
|
|
876
|
+
// structured container (attributed via `source`), so a connector
|
|
877
|
+
// consuming the containers still sees them when prose `context` is
|
|
878
|
+
// off. Counted separately (teammateFindingsIncluded), NOT into
|
|
879
|
+
// memoriesIncluded — that different-denominator mix is what let
|
|
880
|
+
// included exceed available.
|
|
881
|
+
includedTeammateFindings.push({
|
|
882
|
+
id: m.id,
|
|
883
|
+
content: m.content,
|
|
884
|
+
durability: m.durability ?? null,
|
|
885
|
+
createdAt: m.createdAt ?? null,
|
|
886
|
+
updatedAt: m.updatedAt ?? null,
|
|
887
|
+
subject: m.subject ?? null,
|
|
888
|
+
source: m._source,
|
|
889
|
+
section: "teammate",
|
|
890
|
+
});
|
|
891
|
+
if (includeTrust)
|
|
892
|
+
includedTrustMemories.push({ m, section: "teammate" });
|
|
893
|
+
tokenBudget -= cost;
|
|
894
|
+
teammateFindingsIncluded++;
|
|
749
895
|
}
|
|
750
896
|
else {
|
|
751
897
|
sections.relevant.push(line);
|
|
752
898
|
// flair#1182 — own task-relevant records join the `memories`
|
|
753
|
-
// container
|
|
899
|
+
// container.
|
|
754
900
|
includedOwnMemories.push(leanMemory(m, "relevant"));
|
|
901
|
+
if (includeTrust)
|
|
902
|
+
includedTrustMemories.push({ m, section: "relevant" });
|
|
903
|
+
tokenBudget -= cost;
|
|
904
|
+
memoriesIncluded++;
|
|
755
905
|
}
|
|
756
|
-
if (includeTrust)
|
|
757
|
-
includedTrustMemories.push(m);
|
|
758
|
-
tokenBudget -= cost;
|
|
759
|
-
memoriesIncluded++;
|
|
760
906
|
}
|
|
761
907
|
}
|
|
762
908
|
}
|
|
@@ -905,12 +1051,52 @@ export class BootstrapMemories extends Resource {
|
|
|
905
1051
|
continue;
|
|
906
1052
|
eventResults.push(event);
|
|
907
1053
|
}
|
|
908
|
-
|
|
909
|
-
|
|
1054
|
+
// flair#1200 — collapse byte-identical duplicate events before rendering.
|
|
1055
|
+
// The same logical event can land in the table more than once (a producer
|
|
1056
|
+
// that double-fires, or the same broadcast emitted from two paths); each
|
|
1057
|
+
// physical row has a distinct id/createdAt (OrgEvent.post keys the id off
|
|
1058
|
+
// a millisecond timestamp), so they aren't caught by primary-key upsert
|
|
1059
|
+
// and render as exact dupes. Org-event slots are scarce (10), so dedup
|
|
1060
|
+
// BEFORE the slice — otherwise ~half the slots are wasted on duplicates.
|
|
1061
|
+
// Keyed on the CONTENT (kind + summary + detail + targets), keeping the
|
|
1062
|
+
// most-recent occurrence per signature.
|
|
1063
|
+
const eventBySignature = new Map();
|
|
1064
|
+
for (const evt of eventResults) {
|
|
1065
|
+
const sig = JSON.stringify([
|
|
1066
|
+
evt.kind ?? "",
|
|
1067
|
+
evt.summary ?? "",
|
|
1068
|
+
evt.detail ?? "",
|
|
1069
|
+
Array.isArray(evt.targetIds) ? [...evt.targetIds].sort() : (evt.targetIds ?? null),
|
|
1070
|
+
]);
|
|
1071
|
+
const prev = eventBySignature.get(sig);
|
|
1072
|
+
if (!prev || (evt.createdAt || "") > (prev.createdAt || ""))
|
|
1073
|
+
eventBySignature.set(sig, evt);
|
|
1074
|
+
}
|
|
1075
|
+
const dedupedEvents = [...eventBySignature.values()]
|
|
1076
|
+
.sort((a, b) => (a.createdAt || "").localeCompare(b.createdAt || ""));
|
|
1077
|
+
for (const evt of dedupedEvents.slice(0, 10)) {
|
|
910
1078
|
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
911
1079
|
const mins = Math.floor(elapsed / 60_000);
|
|
912
1080
|
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
913
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
|
+
});
|
|
914
1100
|
}
|
|
915
1101
|
}
|
|
916
1102
|
catch {
|
|
@@ -959,9 +1145,27 @@ export class BootstrapMemories extends Resource {
|
|
|
959
1145
|
if (sections.events.length > 0) {
|
|
960
1146
|
parts.push("## Recent Org Events\n" + sections.events.join("\n"));
|
|
961
1147
|
}
|
|
962
|
-
const
|
|
1148
|
+
const fullContext = parts.join("\n\n");
|
|
1149
|
+
// flair#1199 — the structured containers are canonical; `context` is a prose
|
|
1150
|
+
// MIRROR of the same bytes. When includeContext is off (the /mcp default),
|
|
1151
|
+
// ship a compact structural pointer instead of re-embedding every body — so
|
|
1152
|
+
// no field's bytes cross the wire twice. Always a string, so a client can
|
|
1153
|
+
// still tell an empty instance from an unsupported one. When on, the prose
|
|
1154
|
+
// is the full assembled context (the resource/REST/CLI behaviour, unchanged).
|
|
1155
|
+
const context = includeContext
|
|
1156
|
+
? fullContext
|
|
1157
|
+
: (fullContext.length === 0
|
|
1158
|
+
? ""
|
|
1159
|
+
: `Structured payload in soul/memories/predicted/teammateFindings `
|
|
1160
|
+
+ `(${memoriesIncluded} own + ${teammateFindingsIncluded} teammate memories, `
|
|
1161
|
+
+ `${sections.soul.length} soul entries). Pass includeContext:true for the assembled prose context.`);
|
|
963
1162
|
const soulTokens = sections.soul.reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
964
|
-
|
|
1163
|
+
// #1199 — memory-line token spend (informational breakdown), independent of
|
|
1164
|
+
// the reserve/soul now sharing the budget. Sum of the rendered memory lines.
|
|
1165
|
+
const memoryTokens = [
|
|
1166
|
+
...sections.permanent, ...sections.recent, ...sections.predicted,
|
|
1167
|
+
...sections.relevant, ...sections.teammate,
|
|
1168
|
+
].reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
965
1169
|
// flair#744 slice 1 — opt-in per-memory trust block. Bootstrap renders
|
|
966
1170
|
// memories as text lines rather than result objects, so the block is
|
|
967
1171
|
// surfaced as a `trust` array of self-contained entries (each carries its
|
|
@@ -969,9 +1173,12 @@ export class BootstrapMemories extends Resource {
|
|
|
969
1173
|
// HERE, in the response tail, strictly after all read-scope resolution and
|
|
970
1174
|
// purely for the response — never consulted for any authority decision
|
|
971
1175
|
// (#735-spirit zero-authority invariant). Default OFF ⇒ the `trust` key is
|
|
972
|
-
// absent ⇒ the response is byte-identical to pre-slice-1.
|
|
1176
|
+
// absent ⇒ the response is byte-identical to pre-slice-1. flair#1201 — each
|
|
1177
|
+
// entry carries its `section` so `matchQuality: null` on a lifecycle section
|
|
1178
|
+
// reads as "not a retrieval surface", not as a scoring failure on the
|
|
1179
|
+
// caller's own records.
|
|
973
1180
|
const trust = includeTrust
|
|
974
|
-
? includedTrustMemories.map((m) => ({ id: m.id, ...buildTrustBlock(m) }))
|
|
1181
|
+
? includedTrustMemories.map(({ m, section }) => ({ id: m.id, section, ...buildTrustBlock(m) }))
|
|
975
1182
|
: undefined;
|
|
976
1183
|
// flair#744 slice 2 — opt-in abstention verdict for the task-relevance
|
|
977
1184
|
// surface. Present ONLY when `abstain` is requested (byte-identical to
|
|
@@ -1000,7 +1207,17 @@ export class BootstrapMemories extends Resource {
|
|
|
1000
1207
|
const currentTaskHint = taskProvided
|
|
1001
1208
|
? undefined
|
|
1002
1209
|
: "No currentTask was provided. Pass currentTask (a short description of what you're working on) to enable task-relevant memory retrieval, teammate findings, and collision surfacing.";
|
|
1003
|
-
|
|
1210
|
+
// flair#1199 — when `subjects` were provided but nothing surfaced in
|
|
1211
|
+
// `predicted`, say WHY (like currentTaskHint), so an empty `predicted: []`
|
|
1212
|
+
// next to a non-empty `subjects` doesn't read as broken. Predicted fills
|
|
1213
|
+
// from your OWN non-permanent memories whose `subject` matches one of the
|
|
1214
|
+
// provided subjects; it stays empty until you've tagged memories that way.
|
|
1215
|
+
const predictedHint = (predictedSubjects.length > 0 && includedPredicted.length === 0)
|
|
1216
|
+
? `No memories tagged with the requested subjects (${predictedSubjects.join(", ")}) were found. `
|
|
1217
|
+
+ `predicted surfaces your own non-permanent memories whose subject matches one of the provided `
|
|
1218
|
+
+ `subjects — it fills as you store memories tagged with these subjects.`
|
|
1219
|
+
: undefined;
|
|
1220
|
+
const responseBody = {
|
|
1004
1221
|
context,
|
|
1005
1222
|
// flair#1182 (part 1) — always-present self-describing keys: who the
|
|
1006
1223
|
// server resolved the caller as, the read model applied, and the caller's
|
|
@@ -1011,7 +1228,19 @@ export class BootstrapMemories extends Resource {
|
|
|
1011
1228
|
soul: soulMap,
|
|
1012
1229
|
memories: includedOwnMemories,
|
|
1013
1230
|
predicted: includedPredicted,
|
|
1231
|
+
// flair#1199 — cross-agent teammate findings as a structured container
|
|
1232
|
+
// (always present, `[]` when none), so a connector that consumes the
|
|
1233
|
+
// containers still gets them when prose `context` is off.
|
|
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,
|
|
1014
1242
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1243
|
+
...(predictedHint ? { predictedHint } : {}),
|
|
1015
1244
|
...(trust ? { trust } : {}),
|
|
1016
1245
|
...(abstention ? { abstention } : {}),
|
|
1017
1246
|
sections: {
|
|
@@ -1027,12 +1256,36 @@ export class BootstrapMemories extends Resource {
|
|
|
1027
1256
|
collision: sections.collision.length,
|
|
1028
1257
|
events: sections.events.length,
|
|
1029
1258
|
},
|
|
1030
|
-
tokenEstimate: soulTokens + memoryTokens,
|
|
1031
1259
|
soulTokens,
|
|
1032
1260
|
memoryTokens,
|
|
1261
|
+
// flair#1199 — own memories included (denominator: memoriesAvailable, also
|
|
1262
|
+
// own-scoped), so memoriesIncluded ≤ memoriesAvailable always holds.
|
|
1033
1263
|
memoriesIncluded,
|
|
1034
1264
|
memoriesAvailable,
|
|
1265
|
+
// Cross-agent teammate findings included — a SEPARATE denominator, labelled
|
|
1266
|
+
// so it's never confused with the own-memory counters.
|
|
1267
|
+
teammateFindingsIncluded,
|
|
1035
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,
|
|
1036
1274
|
};
|
|
1275
|
+
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1276
|
+
// caller receives (the structured containers included), not just the prose
|
|
1277
|
+
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1278
|
+
// string, so it under-reported by ~2× once the structured fields shipped
|
|
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`.
|
|
1288
|
+
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1289
|
+
return { ...responseBody, tokenEstimate };
|
|
1037
1290
|
}
|
|
1038
1291
|
}
|
|
@@ -364,6 +364,13 @@ async function bootstrap(agent, args) {
|
|
|
364
364
|
surface: args?.surface,
|
|
365
365
|
subjects: args?.subjects,
|
|
366
366
|
entities: args?.entities,
|
|
367
|
+
// flair#1199 — a /mcp connector consumes the STRUCTURED containers
|
|
368
|
+
// (soul/memories/predicted/teammateFindings), so the prose `context` mirror
|
|
369
|
+
// is OFF by default here: shipping both doubled the payload past maxTokens
|
|
370
|
+
// (the reported ~2× overrun). The resource itself defaults includeContext
|
|
371
|
+
// true (the REST/CLI prose path); this wrapper flips it for the connector,
|
|
372
|
+
// and forwards an explicit true when a caller wants the prose anyway.
|
|
373
|
+
includeContext: args?.includeContext === true,
|
|
367
374
|
};
|
|
368
375
|
// flair#744 slice 1 — opt-in per-memory trust block array. Forwarded ONLY
|
|
369
376
|
// when requested so a plain bootstrap delegates a byte-identical body.
|
|
@@ -624,7 +631,7 @@ export const TOOLS = {
|
|
|
624
631
|
inputSchema: {
|
|
625
632
|
type: "object",
|
|
626
633
|
properties: {
|
|
627
|
-
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." },
|
|
628
635
|
currentTask: { type: "string", description: "Current task — enables semantic search for relevant memories" },
|
|
629
636
|
channel: { type: "string", description: "Channel name (discord, tps-mail, claude-code)" },
|
|
630
637
|
surface: { type: "string", description: "Surface name (tps-build, tps-review, cli-session)" },
|
|
@@ -636,6 +643,7 @@ export const TOOLS = {
|
|
|
636
643
|
},
|
|
637
644
|
includeTrust: { type: "boolean", description: "Also return a `trust` array with a per-included-memory trust-evidence block (provenance, author, usage, freshness, supersession). Default false." },
|
|
638
645
|
abstain: { type: "boolean", description: "Opt into a task-relevance abstention verdict: also return an `abstention` object ({ abstained, bestScore, threshold }) reporting whether any memory covered `currentTask` above a global confidence threshold. Default false." },
|
|
646
|
+
includeContext: { type: "boolean", description: "Also return the prose `context` string — a human-readable mirror of the structured soul/memories/predicted/teammateFindings containers (which are the canonical payload). Default false here: the structured fields already carry everything, so shipping the prose too would double the payload." },
|
|
639
647
|
},
|
|
640
648
|
},
|
|
641
649
|
},
|
|
@@ -141,6 +141,7 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
141
141
|
const validFrom = typeof record.validFrom === "string" ? record.validFrom : null;
|
|
142
142
|
const validTo = typeof record.validTo === "string" ? record.validTo : null;
|
|
143
143
|
const createdAt = typeof record.createdAt === "string" ? record.createdAt : null;
|
|
144
|
+
const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : null;
|
|
144
145
|
const validToMs = parseTime(validTo);
|
|
145
146
|
const validFromMs = parseTime(validFrom);
|
|
146
147
|
let validityStatus = "valid";
|
|
@@ -150,10 +151,22 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
150
151
|
else if (Number.isFinite(validFromMs) && validFromMs > now) {
|
|
151
152
|
validityStatus = "future";
|
|
152
153
|
}
|
|
153
|
-
|
|
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);
|
|
154
163
|
const ageDays = Number.isFinite(createdMs)
|
|
155
164
|
? Math.max(0, Math.floor((now - createdMs) / MS_PER_DAY))
|
|
156
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))
|
|
169
|
+
: null;
|
|
157
170
|
return {
|
|
158
171
|
author: typeof record.agentId === "string" ? record.agentId : null,
|
|
159
172
|
provenanceStatus: verifiedAuthor ? "verified" : "unattributed",
|
|
@@ -165,7 +178,9 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
165
178
|
validFrom,
|
|
166
179
|
validTo,
|
|
167
180
|
createdAt,
|
|
181
|
+
updatedAt,
|
|
168
182
|
ageDays,
|
|
183
|
+
staleDays,
|
|
169
184
|
supersedes: typeof record.supersedes === "string" ? record.supersedes : null,
|
|
170
185
|
// flair#744 refinement — confidence band from the result's absolute
|
|
171
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",
|