@tpsdev-ai/flair 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +84 -81
- package/dist/resources/Memory.js +22 -4
- package/dist/resources/SemanticSearch.js +15 -2
- package/package.json +1 -1
- package/schemas/memory.graphql +13 -0
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"commit": "
|
|
4
|
-
"builtAt": "2026-08-
|
|
2
|
+
"version": "0.48.0",
|
|
3
|
+
"commit": "a6cc5ad0f58d9b2549ffd6214028120955c87349",
|
|
4
|
+
"builtAt": "2026-08-22T19:11:54.849Z",
|
|
5
5
|
"builder": "tsc"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2561,11 +2561,13 @@ export function upgradeStatusSuffix(name, status) {
|
|
|
2561
2561
|
}
|
|
2562
2562
|
if (status === "optional")
|
|
2563
2563
|
return " (install via: openclaw plugins install @tpsdev-ai/openclaw-flair)";
|
|
2564
|
-
// flair-mcp is refreshed by re-pinning its wiring
|
|
2565
|
-
//
|
|
2566
|
-
//
|
|
2564
|
+
// flair-mcp is refreshed by re-pinning its wiring, never `npm install -g` —
|
|
2565
|
+
// a global bin does nothing for an `npx -y -p @tpsdev-ai/flair-mcp`
|
|
2566
|
+
// invocation (flair#1208). The re-pin is `flair upgrade`'s own job (the
|
|
2567
|
+
// #1135/#1167 pin refresh) — never advise `doctor --fix` for it
|
|
2568
|
+
// (flair#1324).
|
|
2567
2569
|
if (status === "outdated" && name === FLAIR_MCP_PACKAGE) {
|
|
2568
|
-
return " (npx-wired —
|
|
2570
|
+
return " (npx-wired — flair upgrade refreshes the pin)";
|
|
2569
2571
|
}
|
|
2570
2572
|
return "";
|
|
2571
2573
|
}
|
|
@@ -10220,11 +10222,69 @@ program
|
|
|
10220
10222
|
console.log("\n✅ Everything is up to date.");
|
|
10221
10223
|
return;
|
|
10222
10224
|
}
|
|
10223
|
-
//
|
|
10224
|
-
//
|
|
10225
|
-
//
|
|
10226
|
-
//
|
|
10227
|
-
//
|
|
10225
|
+
// ONE pin-refresh implementation, two callers (flair#1324): the post-
|
|
10226
|
+
// install refresh below (#1135/#1167), and the stale-pin-only path — when
|
|
10227
|
+
// flair-mcp's wired pin is behind latest but no package needs installing,
|
|
10228
|
+
// `flair upgrade` refreshes the pin itself instead of advising a
|
|
10229
|
+
// `doctor --fix` round-trip. Only refreshes clients that are ALREADY
|
|
10230
|
+
// wired — never wires new ones. Best-effort: failures warn but never fail
|
|
10231
|
+
// the upgrade.
|
|
10232
|
+
async function refreshWiredMcpClientPins(targetPort) {
|
|
10233
|
+
const agentId = resolveAgentIdOrEnv({}) ?? (() => {
|
|
10234
|
+
try {
|
|
10235
|
+
const kd = defaultKeysDir();
|
|
10236
|
+
const keyFiles = readdirSync(kd).filter((f) => f.endsWith(".key"));
|
|
10237
|
+
// Node-scoped federation keys aren't agents (flair#1193) — never
|
|
10238
|
+
// pin-refresh a connector as one.
|
|
10239
|
+
const agentKeyFile = keyFiles.find((f) => !isNodeKeyId(f.replace(/\.key$/, ""), kd));
|
|
10240
|
+
return agentKeyFile ? agentKeyFile.replace(/\.key$/, "") : null;
|
|
10241
|
+
}
|
|
10242
|
+
catch {
|
|
10243
|
+
return null;
|
|
10244
|
+
}
|
|
10245
|
+
})();
|
|
10246
|
+
if (!agentId) {
|
|
10247
|
+
console.log("\n (no agent id known — skip MCP client pin refresh; run `flair init` to refresh manually)");
|
|
10248
|
+
return;
|
|
10249
|
+
}
|
|
10250
|
+
const httpUrl = `http://127.0.0.1:${targetPort}`;
|
|
10251
|
+
const mcpEnv = { FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl };
|
|
10252
|
+
const detected = detectClients().filter(c => c.detected);
|
|
10253
|
+
if (detected.length === 0)
|
|
10254
|
+
return;
|
|
10255
|
+
console.log("\n Refreshing MCP client pins...");
|
|
10256
|
+
for (const client of detected) {
|
|
10257
|
+
const configPath = clientConfigPath(client.id);
|
|
10258
|
+
if (!existsSync(configPath))
|
|
10259
|
+
continue;
|
|
10260
|
+
// Only refresh clients that are already wired — don't wire new ones.
|
|
10261
|
+
let hasFlair = false;
|
|
10262
|
+
try {
|
|
10263
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
10264
|
+
if (client.id === "codex") {
|
|
10265
|
+
hasFlair = codexConfigHasFlairSection(raw);
|
|
10266
|
+
}
|
|
10267
|
+
else {
|
|
10268
|
+
const cfg = JSON.parse(raw);
|
|
10269
|
+
hasFlair = !!cfg.mcpServers?.flair;
|
|
10270
|
+
}
|
|
10271
|
+
}
|
|
10272
|
+
catch { /* unreadable/malformed — skip */ }
|
|
10273
|
+
if (!hasFlair)
|
|
10274
|
+
continue;
|
|
10275
|
+
const env = { ...mcpEnv, FLAIR_CLIENT: client.id };
|
|
10276
|
+
const result = client.wire(env);
|
|
10277
|
+
console.log(` ${result.ok ? "✓" : "•"} ${result.message}`);
|
|
10278
|
+
}
|
|
10279
|
+
}
|
|
10280
|
+
// Nothing to install via npm/openclaw. What is left is advisory (packages
|
|
10281
|
+
// not detected) and/or a flair-mcp whose wired pin is behind latest. The
|
|
10282
|
+
// stale pin is `flair upgrade`'s OWN job (flair#1324): refresh it right
|
|
10283
|
+
// here rather than bouncing the user to `flair doctor --fix` — advice
|
|
10284
|
+
// that was both roundabout and, until #1324, routed every upgrading user
|
|
10285
|
+
// through doctor's consent hazard. Under --check, only say what a real
|
|
10286
|
+
// run will do. `npm install -g` remains wrong for flair-mcp either way
|
|
10287
|
+
// (#1168/#1208).
|
|
10228
10288
|
if (totalUpgrades === 0) {
|
|
10229
10289
|
if (missing.length > 0) {
|
|
10230
10290
|
const npmMissing = missing.filter((f) => f.name !== FLAIR_MCP_PACKAGE);
|
|
@@ -10239,7 +10299,12 @@ program
|
|
|
10239
10299
|
}
|
|
10240
10300
|
if (flairMcpOutdated) {
|
|
10241
10301
|
console.log(`\n⬆️ flair-mcp is wired via npx (pinned ${flairMcpOutdated.installed} → latest ${flairMcpOutdated.latest}).`);
|
|
10242
|
-
|
|
10302
|
+
if (checkOnly) {
|
|
10303
|
+
console.log(" Run: flair upgrade (refreshes the pin)");
|
|
10304
|
+
}
|
|
10305
|
+
else {
|
|
10306
|
+
await refreshWiredMcpClientPins(resolveHttpPort({}));
|
|
10307
|
+
}
|
|
10243
10308
|
}
|
|
10244
10309
|
return;
|
|
10245
10310
|
}
|
|
@@ -10460,54 +10525,7 @@ program
|
|
|
10460
10525
|
// version. Runs BEFORE the restart so --no-restart and --no-verify paths
|
|
10461
10526
|
// also get the refresh (flair#1167). Best-effort: failures warn but never
|
|
10462
10527
|
// fail the upgrade.
|
|
10463
|
-
await (
|
|
10464
|
-
const agentId = resolveAgentIdOrEnv({}) ?? (() => {
|
|
10465
|
-
try {
|
|
10466
|
-
const kd = defaultKeysDir();
|
|
10467
|
-
const keyFiles = readdirSync(kd).filter((f) => f.endsWith(".key"));
|
|
10468
|
-
// Node-scoped federation keys aren't agents (flair#1193) — never
|
|
10469
|
-
// pin-refresh a connector as one.
|
|
10470
|
-
const agentKeyFile = keyFiles.find((f) => !isNodeKeyId(f.replace(/\.key$/, ""), kd));
|
|
10471
|
-
return agentKeyFile ? agentKeyFile.replace(/\.key$/, "") : null;
|
|
10472
|
-
}
|
|
10473
|
-
catch {
|
|
10474
|
-
return null;
|
|
10475
|
-
}
|
|
10476
|
-
})();
|
|
10477
|
-
if (!agentId) {
|
|
10478
|
-
console.log("\n (no agent id known — skip MCP client pin refresh; run `flair init` to refresh manually)");
|
|
10479
|
-
return;
|
|
10480
|
-
}
|
|
10481
|
-
const httpUrl = `http://127.0.0.1:${upgradePort}`;
|
|
10482
|
-
const mcpEnv = { FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl };
|
|
10483
|
-
const detected = detectClients().filter(c => c.detected);
|
|
10484
|
-
if (detected.length === 0)
|
|
10485
|
-
return;
|
|
10486
|
-
console.log("\n Refreshing MCP client pins...");
|
|
10487
|
-
for (const client of detected) {
|
|
10488
|
-
const configPath = clientConfigPath(client.id);
|
|
10489
|
-
if (!existsSync(configPath))
|
|
10490
|
-
continue;
|
|
10491
|
-
// Only refresh clients that are already wired — don't wire new ones.
|
|
10492
|
-
let hasFlair = false;
|
|
10493
|
-
try {
|
|
10494
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
10495
|
-
if (client.id === "codex") {
|
|
10496
|
-
hasFlair = codexConfigHasFlairSection(raw);
|
|
10497
|
-
}
|
|
10498
|
-
else {
|
|
10499
|
-
const cfg = JSON.parse(raw);
|
|
10500
|
-
hasFlair = !!cfg.mcpServers?.flair;
|
|
10501
|
-
}
|
|
10502
|
-
}
|
|
10503
|
-
catch { /* unreadable/malformed — skip */ }
|
|
10504
|
-
if (!hasFlair)
|
|
10505
|
-
continue;
|
|
10506
|
-
const env = { ...mcpEnv, FLAIR_CLIENT: client.id };
|
|
10507
|
-
const result = client.wire(env);
|
|
10508
|
-
console.log(` ${result.ok ? "✓" : "•"} ${result.message}`);
|
|
10509
|
-
}
|
|
10510
|
-
})();
|
|
10528
|
+
await refreshWiredMcpClientPins(upgradePort);
|
|
10511
10529
|
// ── Restart + verify + rollback (flair#635) ─────────────────────────────
|
|
10512
10530
|
// Decision (2026-07-08): restart is now the default post-upgrade step —
|
|
10513
10531
|
// installing new code without restarting leaves the OLD process serving
|
|
@@ -13094,35 +13112,20 @@ program
|
|
|
13094
13112
|
// the SessionStart check above: installed / absent / stale-form).
|
|
13095
13113
|
// Continuity is OPT-IN — installing the PostToolUse+Stop pair IS the
|
|
13096
13114
|
// opt-in — so "absent" renders as informational "not enabled": NEVER
|
|
13097
|
-
// a pass (an unrun check must not look green)
|
|
13098
|
-
// issue
|
|
13099
|
-
//
|
|
13100
|
-
//
|
|
13101
|
-
//
|
|
13115
|
+
// a pass (an unrun check must not look green), never counted as an
|
|
13116
|
+
// issue, and NEVER wired by --fix (flair#1324: doctor's fixable set
|
|
13117
|
+
// is broken state; initiating an opt-in the user hasn't made is not a
|
|
13118
|
+
// fix — a y/N prompt auto-answers yes in every non-TTY run, so it was
|
|
13119
|
+
// no consent gate at all; enablement is `flair hook install
|
|
13120
|
+
// --continuity` only). A partial or stale pair IS evidence of a prior
|
|
13121
|
+
// opt-in, so repairing it to the complete current form remains a
|
|
13122
|
+
// legitimate --fix.
|
|
13102
13123
|
const continuity = checkContinuityCaptureHooks(homedir());
|
|
13103
13124
|
if (continuity.state === "installed") {
|
|
13104
13125
|
console.log(` ${render.icons.ok} Continuity capture hooks: PostToolUse + Stop wired in ${render.wrap(render.c.dim, continuity.path)}`);
|
|
13105
13126
|
}
|
|
13106
13127
|
else if (continuity.state === "absent") {
|
|
13107
13128
|
console.log(` ${render.icons.info} Continuity capture hooks: not enabled ${render.wrap(render.c.dim, "(opt-in — auto-journal working state into the ephemeral memory tier; enable: flair hook install --continuity)")}`);
|
|
13108
|
-
if (autoFix) {
|
|
13109
|
-
if (dryRun) {
|
|
13110
|
-
console.log(` ${render.wrap(render.c.dim, "Would wire the continuity capture hooks (PostToolUse + Stop) in")} ${continuity.path}`);
|
|
13111
|
-
}
|
|
13112
|
-
else {
|
|
13113
|
-
const proceed = await confirmFix(` Enable continuity capture (PostToolUse + Stop hooks in ${continuity.path})? [y/N] `);
|
|
13114
|
-
if (!proceed) {
|
|
13115
|
-
console.log(` Skipped.`);
|
|
13116
|
-
}
|
|
13117
|
-
else {
|
|
13118
|
-
const fixAgentId = claudeCodeAgentId || opts.agent || process.env.FLAIR_AGENT_ID;
|
|
13119
|
-
const fixRes = fixContinuityCaptureHooks(homedir(), fixAgentId);
|
|
13120
|
-
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
13121
|
-
if (fixRes.ok && fixRes.changed)
|
|
13122
|
-
fixed++;
|
|
13123
|
-
}
|
|
13124
|
-
}
|
|
13125
|
-
}
|
|
13126
13129
|
}
|
|
13127
13130
|
else {
|
|
13128
13131
|
const continuityDetail = continuity.state === "partial"
|
package/dist/resources/Memory.js
CHANGED
|
@@ -614,8 +614,22 @@ export class Memory extends databases.flair.Memory {
|
|
|
614
614
|
}
|
|
615
615
|
}
|
|
616
616
|
content.durability ||= "standard";
|
|
617
|
-
|
|
618
|
-
|
|
617
|
+
// ── flair#1336: honor a caller-supplied createdAt (parity with put()) ──
|
|
618
|
+
// put() — the other HTTP-reachable create path — has always preserved the
|
|
619
|
+
// caller's createdAt (`content.createdAt ?? now`), and adk-flair's
|
|
620
|
+
// add_memory forwards MemoryEntry.timestamp through it for historical
|
|
621
|
+
// imports. When #1336 moved client creates onto POST, this line's
|
|
622
|
+
// unconditional re-stamp silently discarded those timestamps (caught by
|
|
623
|
+
// the #1334 list-pagination live test: rows written with backdated
|
|
624
|
+
// timestamps came back stamped "now"). Honoring the caller grants no new
|
|
625
|
+
// capability — PUT already accepted arbitrary createdAt from the same
|
|
626
|
+
// principals. validFrom below keys off createdAt and follows it, exactly
|
|
627
|
+
// as on the put() path; updatedAt stays the true write moment; the
|
|
628
|
+
// ephemeral expiresAt stamp keys off Date.now(), so a backdated create
|
|
629
|
+
// cannot stretch the #1257 exposure window.
|
|
630
|
+
const nowIso = new Date().toISOString();
|
|
631
|
+
content.createdAt = content.createdAt ?? nowIso;
|
|
632
|
+
content.updatedAt = nowIso;
|
|
619
633
|
content.archived = content.archived ?? false;
|
|
620
634
|
// ─── Default visibility (durability-keyed) — Layer 1, part A ────────────
|
|
621
635
|
// post() only ever creates a NEW record — patchRecord/supersede-close/
|
|
@@ -658,9 +672,13 @@ export class Memory extends databases.flair.Memory {
|
|
|
658
672
|
if (content.visibility === undefined || content.visibility === null) {
|
|
659
673
|
content.visibility = defaultVisibilityForDurability(content.durability);
|
|
660
674
|
}
|
|
661
|
-
// Validate derivedFrom source IDs exist (best-effort, non-blocking)
|
|
675
|
+
// Validate derivedFrom source IDs exist (best-effort, non-blocking).
|
|
676
|
+
// lastReflected keys off updatedAt (the write moment), NOT createdAt —
|
|
677
|
+
// since #1336 a create may carry a backdated caller createdAt, and the
|
|
678
|
+
// reflection bookkeeping must record when the derivation actually ran.
|
|
679
|
+
// (Pre-#1336 the two were always identical here.)
|
|
662
680
|
if (Array.isArray(content.derivedFrom) && content.derivedFrom.length > 0) {
|
|
663
|
-
const now = content.
|
|
681
|
+
const now = content.updatedAt;
|
|
664
682
|
for (const sourceId of content.derivedFrom) {
|
|
665
683
|
try {
|
|
666
684
|
const src = await databases.flair.Memory.get(sourceId);
|
|
@@ -61,7 +61,7 @@ export class SemanticSearch extends Resource {
|
|
|
61
61
|
// recall-harness (test/bench/recall-harness/run.ts) and `recall-eval.mjs`
|
|
62
62
|
// before reconsidering this default if the compositeScore formula or
|
|
63
63
|
// corpus changes.
|
|
64
|
-
const { agentId: bodyAgentId, q, queryEmbedding, tag, subject, subjects, limit = 10, includeSuperseded = false, scoring = "raw", minScore = 0, since, asOf, includeTrust = false, abstain = false, explain = false } = data || {};
|
|
64
|
+
const { agentId: bodyAgentId, q, queryEmbedding, tag, subject, subjects, limit = 10, includeSuperseded = false, scoring = "raw", minScore = 0, since, asOf, includeTrust = false, includeMetadata = false, abstain = false, explain = false } = data || {};
|
|
65
65
|
// Authenticated identity lives on the Harper Resource context (getContext().request).
|
|
66
66
|
// `this.request` is NOT populated on Harper v5 Resources — prior reads here
|
|
67
67
|
// silently returned undefined and the defense-in-depth scope check below
|
|
@@ -234,7 +234,20 @@ export class SemanticSearch extends Resource {
|
|
|
234
234
|
// default projection omits. Widen the select ONLY when the caller opts
|
|
235
235
|
// in — passing undefined otherwise keeps the default (no `provenance`)
|
|
236
236
|
// so a non-trust recall response stays byte-identical.
|
|
237
|
-
|
|
237
|
+
//
|
|
238
|
+
// flair#1332: same idiom for the client-writable `metadata` JSON blob
|
|
239
|
+
// (ADK custom_metadata store-and-return). DEFAULT_SELECT deliberately
|
|
240
|
+
// does NOT grow it (K&S projection ruling — the shared retrieval core
|
|
241
|
+
// serves every consumer, and none of the others should pay result-size
|
|
242
|
+
// for an opaque blob they never read); adk-flair opts in per-request
|
|
243
|
+
// with `includeMetadata: true`. `subject` needs no widening — it is
|
|
244
|
+
// already in DEFAULT_SELECT. Neither flag ⇒ select stays undefined ⇒
|
|
245
|
+
// response bytes unchanged.
|
|
246
|
+
select: (includeTrust || includeMetadata)
|
|
247
|
+
? [...DEFAULT_SELECT,
|
|
248
|
+
...(includeTrust ? ["provenance"] : []),
|
|
249
|
+
...(includeMetadata ? ["metadata"] : [])]
|
|
250
|
+
: undefined,
|
|
238
251
|
// flair#744 slice 2 + confidence-band refinement: attach the absolute
|
|
239
252
|
// per-result cosine confidence when the caller opts into abstention OR
|
|
240
253
|
// the trust block — abstention reads the best of it for its verdict, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"packageManager": "bun@1.3.10",
|
|
5
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
6
6
|
"type": "module",
|
package/schemas/memory.graphql
CHANGED
|
@@ -85,6 +85,19 @@ type Memory @table(database: "flair") {
|
|
|
85
85
|
# stamped by the ORIGINATING instance itself and preserved (never re-stamped) as the record flows through
|
|
86
86
|
# sync merges. @indexed for the later sync push-query filter (per-record signature verification and the
|
|
87
87
|
# classifier org-gate are separate, later slices — not built here).
|
|
88
|
+
metadata: String # JSON blob (flair#1332/#1202): ADK custom_metadata, store-and-return. CLIENT-WRITABLE
|
|
89
|
+
# (unlike provenance above, which is server-stamped) and OPAQUE TO THE SERVER by
|
|
90
|
+
# contract: stored verbatim, returned verbatim, never parsed server-side, never
|
|
91
|
+
# queryable (Harper cannot query into a JSON blob), and NO key inside this blob
|
|
92
|
+
# ever influences any server decision — visibility, read-scope, durability,
|
|
93
|
+
# expiry, federation, ranking, nothing. A blob carrying {"visibility":"shared"}
|
|
94
|
+
# leaves the record's ACTUAL visibility at its default (contract-tested in
|
|
95
|
+
# packages/adk-flair/tests/test_metadata_and_list.py). If a specific key ever
|
|
96
|
+
# needs to be filterable, promote THAT key to its own @indexed scalar column
|
|
97
|
+
# (the #1202-documented add-on pattern; `subject` below is the first example) —
|
|
98
|
+
# never parse this blob. Size/shape caps are enforced at the writing client
|
|
99
|
+
# (adk-flair: 64KB serialized, depth ≤ 16, ≤ 512 keys). Nullable/additive —
|
|
100
|
+
# existing rows read null, unchanged behavior (clean-upgrade-path gate).
|
|
88
101
|
entities: [String] @indexed # attention-plane vocabulary strings (flair#675). Added in v1 (not v2) per K&S
|
|
89
102
|
# verdict on FLAIR-ATTENTION-PLANE.md — gives the future attention query
|
|
90
103
|
# uniform index pushdown across Memory/WorkspaceState/OrgEvent instead of a
|