@tpsdev-ai/flair 0.53.0 → 0.54.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +1791 -15648
- package/dist/commands/agent.js +453 -0
- package/dist/commands/attention.js +121 -0
- package/dist/commands/backup.js +115 -0
- package/dist/commands/bootstrap.js +91 -0
- package/dist/commands/bridge.js +608 -0
- package/dist/commands/deploy.js +180 -0
- package/dist/commands/doctor.js +1665 -0
- package/dist/commands/export.js +110 -0
- package/dist/commands/federation.js +1575 -0
- package/dist/commands/fleet.js +73 -0
- package/dist/commands/grant.js +109 -0
- package/dist/commands/hook.js +193 -0
- package/dist/commands/idp.js +193 -0
- package/dist/commands/import.js +134 -0
- package/dist/commands/init.js +1203 -0
- package/dist/commands/inspect.js +45 -0
- package/dist/commands/keys.js +187 -0
- package/dist/commands/mcp.js +707 -0
- package/dist/commands/memory.js +501 -0
- package/dist/commands/migrate-harness-memory.js +270 -0
- package/dist/commands/orgevent.js +138 -0
- package/dist/commands/presence.js +76 -0
- package/dist/commands/principal.js +338 -0
- package/dist/commands/quality.js +1164 -0
- package/dist/commands/reembed.js +296 -0
- package/dist/commands/relationship.js +76 -0
- package/dist/commands/rem.js +1048 -0
- package/dist/commands/restore.js +130 -0
- package/dist/commands/search.js +244 -0
- package/dist/commands/service.js +315 -0
- package/dist/commands/session.js +184 -0
- package/dist/commands/soul.js +155 -0
- package/dist/commands/status.js +931 -0
- package/dist/commands/test.js +93 -0
- package/dist/commands/uninstall.js +143 -0
- package/dist/commands/upgrade.js +1628 -0
- package/dist/commands/workspace.js +114 -0
- package/dist/deploy.js +24 -0
- package/dist/engine-version.js +12 -4
- package/dist/fabric-npm-install.js +87 -0
- package/dist/fabric-upgrade.js +30 -15
- package/dist/federation-verify.js +498 -0
- package/dist/fleet-verify.js +144 -21
- package/dist/install/clients.js +167 -0
- package/dist/lib/auth-resolve.js +76 -1
- package/dist/lib/daemon-liveness.js +131 -2
- package/dist/lib/doctor-config-path.js +61 -0
- package/dist/lib/doctor-federation-driver.js +189 -0
- package/dist/lib/doctor-run.js +40 -0
- package/dist/lib/entity-vocab-cli.js +3 -3
- package/dist/lib/federation-pair-identity.js +47 -0
- package/dist/lib/launchd-repair.js +5 -4
- package/dist/lib/npm-registry.js +578 -0
- package/dist/lib/ops-api-bind.js +115 -0
- package/dist/lib/owned-pins.js +219 -0
- package/dist/lib/uninstall-purge.js +218 -0
- package/dist/rem/restore.js +8 -10
- package/dist/resources/AgentReadPosition.js +74 -0
- package/dist/resources/Federation.js +8 -2
- package/dist/resources/Memory.js +4 -3
- package/dist/resources/MemoryBootstrap.js +41 -25
- package/dist/resources/MemoryCandidate.js +5 -6
- package/dist/resources/OrgEventCatchup.js +126 -47
- package/dist/resources/agent-read-position-lib.js +83 -0
- package/dist/resources/agent-read-position.js +120 -0
- package/dist/resources/embeddings-boot.js +32 -0
- package/dist/resources/federation-peer-liveness.js +73 -0
- package/dist/resources/health.js +68 -19
- package/dist/resources/mcp-tools.js +48 -279
- package/dist/resources/memory-visibility.js +3 -3
- package/dist/resources/migration-boot.js +59 -18
- package/dist/resources/migrations/embedding-stamp.js +20 -1
- package/dist/resources/migrations/recheck.js +43 -0
- package/dist/resources/migrations/runner.js +6 -1
- package/dist/resources/migrations/stamp-outstanding.js +171 -0
- package/dist/resources/migrations/visibility-backfill.js +2 -2
- package/dist/resources/org-event-catchup-lib.js +47 -0
- package/dist/resources/record-owner-guard.js +1 -0
- package/dist/resources/tool-descriptors/index.js +669 -0
- package/dist/stamp-migration-verify.js +163 -0
- package/dist/stamp-outstanding.js +144 -0
- package/dist/version-check.js +29 -8
- package/docs/api-reference.md +4 -2
- package/docs/deploying-on-fabric.md +11 -10
- package/docs/deployment.md +3 -1
- package/docs/federation.md +19 -0
- package/docs/hosted-on-fabric.md +3 -3
- package/docs/quickstart.md +2 -1
- package/docs/releasing.md +20 -6
- package/docs/spoke-bringup.md +10 -5
- package/docs/standalone-local.md +3 -1
- package/docs/upgrade.md +25 -6
- package/package.json +4 -4
- package/schemas/agent.graphql +15 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { authFetch, defaultKeysDir, resolveAdminUser } from "../lib/auth-resolve.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
let cli;
|
|
4
|
+
/** Bind the cli-locals this module depends on. */
|
|
5
|
+
export function bindCli(fns) {
|
|
6
|
+
cli = fns;
|
|
7
|
+
}
|
|
8
|
+
function privKeyPath(...args) {
|
|
9
|
+
return cli.privKeyPath(...args);
|
|
10
|
+
}
|
|
11
|
+
function resolveHttpPort(...args) {
|
|
12
|
+
return cli.resolveHttpPort(...args);
|
|
13
|
+
}
|
|
14
|
+
function resolveOpsPort(...args) {
|
|
15
|
+
return cli.resolveOpsPort(...args);
|
|
16
|
+
}
|
|
17
|
+
export function register(program) {
|
|
18
|
+
// ─── flair reembed ────────────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// ROOT-CAUSE GUARD — recall graph correctness (recall-hnsw-graph-heal).
|
|
21
|
+
// `flair reembed` replaces the stored embedding of many rows IN PLACE (it
|
|
22
|
+
// clears embedding/embeddingModel, then re-PUTs through Memory.put()'s regen
|
|
23
|
+
// branch — the same bulk in-place re-embed path resources/migrations/
|
|
24
|
+
// embedding-stamp.ts uses). Historically, an OLDER (pre-fix) Harper's
|
|
25
|
+
// INCREMENTAL HNSW update left stale/asymmetric reverse edges under bulk
|
|
26
|
+
// re-embed, which collapsed prod recall in July. That engine bug is FIXED in
|
|
27
|
+
// the Harper this ships against (5.1.22) — its update path reconstructs the
|
|
28
|
+
// prior vector and does the reverse-edge cleanup the old build skipped — so a
|
|
29
|
+
// bulk re-embed no longer corrupts the graph. DEFENSE-IN-DEPTH RULE (a
|
|
30
|
+
// prudent, version-independent default, not a workaround for a live bug): pair
|
|
31
|
+
// any BULK re-embed with a structural graph REBUILD trigger rather than relying
|
|
32
|
+
// on incremental HNSW updates to converge (today: the
|
|
33
|
+
// `@indexed(type:"HNSW", M:16)` descriptor bump in schemas/memory.graphql,
|
|
34
|
+
// which makes Harper clear + rebuild the graph cleanly from the stored vectors
|
|
35
|
+
// on the next boot — see resources/migrations/graph-heal.ts). Do NOT use
|
|
36
|
+
// resources/MemoryReindex.ts's `_reindex` for graph correctness (it re-PUTs the
|
|
37
|
+
// same vector through the incremental path and rebuilds nothing). If a `flair
|
|
38
|
+
// reembed` run ever materially changes the vector space (e.g. a model swap),
|
|
39
|
+
// follow it with a deploy that trips the structural reindex (bump the HNSW
|
|
40
|
+
// descriptor / restart after a schema change).
|
|
41
|
+
program
|
|
42
|
+
.command("reembed")
|
|
43
|
+
.description("Re-generate embeddings for memories with stale or missing model tags")
|
|
44
|
+
.option("--agent <id>", "Agent ID to re-embed memories for (defaults to all agents with stale rows)")
|
|
45
|
+
.option("--stale-only", "Only re-embed memories with mismatched model tag")
|
|
46
|
+
.option("--dry-run", "Show count without modifying")
|
|
47
|
+
.option("--port <port>", "Harper HTTP port")
|
|
48
|
+
.option("--batch-size <n>", "Records per batch", "50")
|
|
49
|
+
.option("--delay-ms <ms>", "Delay between batches (ms)", "100")
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
const port = resolveHttpPort(opts);
|
|
52
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
53
|
+
const agentId = opts.agent;
|
|
54
|
+
const staleOnly = opts.staleOnly ?? false;
|
|
55
|
+
const dryRun = opts.dryRun ?? false;
|
|
56
|
+
const batchSize = Number(opts.batchSize);
|
|
57
|
+
const delayMs = Number(opts.delayMs);
|
|
58
|
+
// flair#504 Phase 2: MUST match resources/embeddings-provider.ts's
|
|
59
|
+
// getModelId() — including THE GATE (EMBEDDING_PREFIXES_ENABLED), not
|
|
60
|
+
// just the suffix. Duplicated as literals, not imported, because
|
|
61
|
+
// src/cli.ts and resources/**.ts are separate build targets —
|
|
62
|
+
// tsconfig.cli.json's rootDir is "src" and only includes src/cli.ts +
|
|
63
|
+
// src/cli-shim.cts, and the published CLI package ships only dist/ built
|
|
64
|
+
// from that config (package.json's "files"), so resources/ isn't
|
|
65
|
+
// reachable from (or bundled into) the CLI binary. THE GATE is now ON
|
|
66
|
+
// (flipped, re-baselined through the ratchet gate — see
|
|
67
|
+
// embeddings-provider.ts's file header and PR #689 for the park history
|
|
68
|
+
// this flip revisits), so `currentModel` here is `<base>+searchprefix` —
|
|
69
|
+
// matching getModelId()'s gate-on return exactly. If
|
|
70
|
+
// EMBEDDING_PREFIXES_ENABLED or EMBEDDING_VARIANT ever changes in
|
|
71
|
+
// embeddings-provider.ts, update this block too — a drift here silently
|
|
72
|
+
// breaks `--stale-only`: it would compare every row's embeddingModel
|
|
73
|
+
// against the WRONG current-model string, so rows would read as already
|
|
74
|
+
// "current" (or as needing re-embed) out of sync with what getModelId()
|
|
75
|
+
// is actually stamping new writes with.
|
|
76
|
+
const EMBEDDING_PREFIXES_ENABLED = true; // MUST mirror resources/embeddings-provider.ts's gate
|
|
77
|
+
const EMBEDDING_VARIANT = "searchprefix";
|
|
78
|
+
// embedding-space-guard slice 1: getModelId() now stamps the ENGINE-QUALIFIED
|
|
79
|
+
// id `<engine>:<base>[+searchprefix]`. Duplicated as a literal here (separate
|
|
80
|
+
// build target — see above). A row is CURRENT-SPACE iff its stamp is the
|
|
81
|
+
// qualified id OR its one-time bare-name equivalent (today's corpus, stamped
|
|
82
|
+
// before the qualifier). Treat BOTH as current so `--stale-only` never
|
|
83
|
+
// re-embeds an already-correct bare-stamped row — that would loop forever
|
|
84
|
+
// (Memory.put re-stamps it QUALIFIED, still "!= bare" under a single-value
|
|
85
|
+
// check). Keep in lockstep with resources/embeddings-provider.ts's
|
|
86
|
+
// getModelId()/EMBEDDING_ENGINE and resources/embedding-space-guard.ts's
|
|
87
|
+
// normalizeStamp().
|
|
88
|
+
const EMBEDDING_ENGINE = "gguf";
|
|
89
|
+
const baseModel = process.env.FLAIR_EMBEDDING_MODEL ?? "nomic-embed-text-v1.5-Q4_K_M";
|
|
90
|
+
const bareCurrentModel = EMBEDDING_PREFIXES_ENABLED ? `${baseModel}+${EMBEDDING_VARIANT}` : baseModel;
|
|
91
|
+
const currentModel = `${EMBEDDING_ENGINE}:${bareCurrentModel}`;
|
|
92
|
+
const isCurrentSpace = (stamp) => stamp === currentModel || stamp === bareCurrentModel;
|
|
93
|
+
if (agentId) {
|
|
94
|
+
console.log(`Re-embedding memories for agent: ${agentId}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log("Re-embedding memories for all agents with stale rows");
|
|
98
|
+
}
|
|
99
|
+
console.log(`Current model: ${currentModel}`);
|
|
100
|
+
if (staleOnly)
|
|
101
|
+
console.log("Mode: stale-only (skipping up-to-date memories)");
|
|
102
|
+
if (dryRun)
|
|
103
|
+
console.log("Mode: dry-run (no modifications)");
|
|
104
|
+
console.log("");
|
|
105
|
+
// When no agent specified, use admin auth to fetch all memories
|
|
106
|
+
if (!agentId) {
|
|
107
|
+
const adminPass = process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD;
|
|
108
|
+
if (!adminPass) {
|
|
109
|
+
console.error("❌ Admin password required when --agent is not specified (set FLAIR_ADMIN_PASS or HDB_ADMIN_PASSWORD)");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
// Fetch every memory via the Harper ops API (search_by_conditions on the
|
|
113
|
+
// Memory table) rather than POST /SemanticSearch. SemanticSearch goes
|
|
114
|
+
// through the HNSW cosine index, which throws "Cosine distance comparison
|
|
115
|
+
// requires an array" against rows whose stored embedding shape is
|
|
116
|
+
// incompatible with the running Harper version (e.g. data written under
|
|
117
|
+
// harper@5.0.1 read under 5.0.9). The ops API bypasses the
|
|
118
|
+
// vector index — exactly what we need when the goal is to replace every
|
|
119
|
+
// embedding with a freshly-computed one. Without this path, `flair
|
|
120
|
+
// reembed` could not recover from the very condition it exists to fix.
|
|
121
|
+
const opsPort = resolveOpsPort(opts);
|
|
122
|
+
const opsAuth = `Basic ${Buffer.from(`${resolveAdminUser(undefined)}:${adminPass}`).toString("base64")}`;
|
|
123
|
+
// Harper rejects empty-value conditions ("not indexed for nulls"). Use
|
|
124
|
+
// `createdAt > 1970-01-01` as the "select all" pattern: every Memory row
|
|
125
|
+
// has a createdAt, the index is built, and the comparison is total.
|
|
126
|
+
const searchRes = await fetch(`http://127.0.0.1:${opsPort}/`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json", Authorization: opsAuth },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
operation: "search_by_conditions",
|
|
131
|
+
database: "flair",
|
|
132
|
+
table: "Memory",
|
|
133
|
+
operator: "and",
|
|
134
|
+
conditions: [{ search_attribute: "createdAt", search_type: "greater_than", search_value: "1970-01-01" }],
|
|
135
|
+
get_attributes: ["*"],
|
|
136
|
+
limit: 100000,
|
|
137
|
+
}),
|
|
138
|
+
signal: AbortSignal.timeout(60_000),
|
|
139
|
+
});
|
|
140
|
+
if (!searchRes.ok) {
|
|
141
|
+
console.error(`❌ Failed to fetch memories via ops API: ${searchRes.status}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const raw = await searchRes.json();
|
|
145
|
+
const allMemories = Array.isArray(raw) ? raw : (raw?.results ?? []);
|
|
146
|
+
// Group by agentId
|
|
147
|
+
const byAgent = new Map();
|
|
148
|
+
for (const m of allMemories) {
|
|
149
|
+
if (!m.content)
|
|
150
|
+
continue;
|
|
151
|
+
if (staleOnly && isCurrentSpace(m.embeddingModel))
|
|
152
|
+
continue;
|
|
153
|
+
const agent = m.agentId || "unknown";
|
|
154
|
+
if (!byAgent.has(agent))
|
|
155
|
+
byAgent.set(agent, []);
|
|
156
|
+
byAgent.get(agent).push(m);
|
|
157
|
+
}
|
|
158
|
+
// Process each agent
|
|
159
|
+
let totalProcessed = 0;
|
|
160
|
+
let totalErrors = 0;
|
|
161
|
+
const agentCount = byAgent.size;
|
|
162
|
+
let agentIndex = 0;
|
|
163
|
+
for (const [agent, memories] of byAgent) {
|
|
164
|
+
agentIndex++;
|
|
165
|
+
console.log(`\nAgent ${agentIndex}/${agentCount}: ${agent}`);
|
|
166
|
+
console.log(` Memories to re-embed: ${memories.length}`);
|
|
167
|
+
const keysDir = defaultKeysDir();
|
|
168
|
+
const privPath = privKeyPath(agent, keysDir);
|
|
169
|
+
if (!existsSync(privPath)) {
|
|
170
|
+
console.error(` ❌ Key not found: ${privPath} — skipping`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (dryRun)
|
|
174
|
+
continue;
|
|
175
|
+
let processed = 0;
|
|
176
|
+
let errors = 0;
|
|
177
|
+
for (let i = 0; i < memories.length; i += batchSize) {
|
|
178
|
+
const batch = memories.slice(i, i + batchSize);
|
|
179
|
+
for (const memory of batch) {
|
|
180
|
+
try {
|
|
181
|
+
const updateRes = await authFetch(baseUrl, agent, privPath, "PUT", `/Memory/${memory.id}`, {
|
|
182
|
+
id: memory.id, content: memory.content, embedding: undefined, embeddingModel: undefined, agentId: memory.agentId || agent,
|
|
183
|
+
});
|
|
184
|
+
if (updateRes.ok)
|
|
185
|
+
processed++;
|
|
186
|
+
else
|
|
187
|
+
errors++;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
errors++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const pct = Math.round(((i + batch.length) / memories.length) * 100);
|
|
194
|
+
process.stdout.write(` \r Re-embedded ${processed}/${memories.length} (${pct}%)${errors > 0 ? ` [${errors} errors]` : ""}`);
|
|
195
|
+
if (i + batchSize < memories.length)
|
|
196
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
197
|
+
}
|
|
198
|
+
console.log(`\n ✅ Agent ${agent}: ${processed} updated, ${errors} errors`);
|
|
199
|
+
totalProcessed += processed;
|
|
200
|
+
totalErrors += errors;
|
|
201
|
+
}
|
|
202
|
+
console.log(`\n\n✅ Re-embedding complete: ${totalProcessed} updated, ${totalErrors} errors`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Single-agent path. Same rationale as above: fetch via the ops API
|
|
206
|
+
// (search_by_value on agentId) so the vector index isn't in the read path.
|
|
207
|
+
// This requires admin pass — fall back to the old SemanticSearch fetch only
|
|
208
|
+
// if no admin pass is available, since that path still works on
|
|
209
|
+
// version-matched data and requires only the agent's own key.
|
|
210
|
+
const keysDir = defaultKeysDir();
|
|
211
|
+
const privPath = privKeyPath(agentId, keysDir);
|
|
212
|
+
if (!existsSync(privPath)) {
|
|
213
|
+
console.error(`❌ Key not found: ${privPath}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const adminPassSingle = process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD;
|
|
217
|
+
let allMemories = [];
|
|
218
|
+
if (adminPassSingle) {
|
|
219
|
+
const opsPort = resolveOpsPort(opts);
|
|
220
|
+
const opsAuth = `Basic ${Buffer.from(`${resolveAdminUser(undefined)}:${adminPassSingle}`).toString("base64")}`;
|
|
221
|
+
const searchRes = await fetch(`http://127.0.0.1:${opsPort}/`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "Content-Type": "application/json", Authorization: opsAuth },
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
operation: "search_by_value",
|
|
226
|
+
database: "flair",
|
|
227
|
+
table: "Memory",
|
|
228
|
+
search_attribute: "agentId",
|
|
229
|
+
search_value: agentId,
|
|
230
|
+
get_attributes: ["*"],
|
|
231
|
+
}),
|
|
232
|
+
signal: AbortSignal.timeout(60_000),
|
|
233
|
+
});
|
|
234
|
+
if (!searchRes.ok) {
|
|
235
|
+
console.error(`❌ Failed to fetch memories via ops API: ${searchRes.status}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
const raw = await searchRes.json();
|
|
239
|
+
allMemories = Array.isArray(raw) ? raw : (raw?.results ?? []);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const searchRes = await authFetch(baseUrl, agentId, privPath, "POST", "/SemanticSearch", {
|
|
243
|
+
agentId, limit: 10000,
|
|
244
|
+
});
|
|
245
|
+
if (!searchRes.ok) {
|
|
246
|
+
console.error(`❌ Failed to fetch memories: ${searchRes.status}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const data = await searchRes.json();
|
|
250
|
+
allMemories = data.results ?? [];
|
|
251
|
+
}
|
|
252
|
+
const candidates = allMemories.filter((m) => {
|
|
253
|
+
if (!m.content)
|
|
254
|
+
return false;
|
|
255
|
+
if (staleOnly)
|
|
256
|
+
return !m.embeddingModel || !isCurrentSpace(m.embeddingModel);
|
|
257
|
+
return true;
|
|
258
|
+
});
|
|
259
|
+
const total = candidates.length;
|
|
260
|
+
const skipped = allMemories.length - total;
|
|
261
|
+
console.log(`Total memories: ${allMemories.length}`);
|
|
262
|
+
console.log(`Candidates for re-embedding: ${total}`);
|
|
263
|
+
if (skipped > 0)
|
|
264
|
+
console.log(`Skipped (up-to-date): ${skipped}`);
|
|
265
|
+
if (dryRun || total === 0) {
|
|
266
|
+
if (total === 0)
|
|
267
|
+
console.log("\n✅ All memories are up-to-date!");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.log("");
|
|
271
|
+
let processed = 0;
|
|
272
|
+
let errors = 0;
|
|
273
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
274
|
+
const batch = candidates.slice(i, i + batchSize);
|
|
275
|
+
for (const memory of batch) {
|
|
276
|
+
try {
|
|
277
|
+
const updateRes = await authFetch(baseUrl, agentId, privPath, "PUT", `/Memory/${memory.id}`, {
|
|
278
|
+
id: memory.id, content: memory.content, embedding: undefined, embeddingModel: undefined, agentId: memory.agentId || opts.agent,
|
|
279
|
+
});
|
|
280
|
+
if (updateRes.ok)
|
|
281
|
+
processed++;
|
|
282
|
+
else
|
|
283
|
+
errors++;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
errors++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const pct = Math.round(((i + batch.length) / total) * 100);
|
|
290
|
+
process.stdout.write(`\rRe-embedded ${processed}/${total} (${pct}%)${errors > 0 ? ` [${errors} errors]` : ""}`);
|
|
291
|
+
if (i + batchSize < candidates.length)
|
|
292
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
293
|
+
}
|
|
294
|
+
console.log(`\n\n✅ Re-embedding complete: ${processed} updated, ${errors} errors`);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
let cli;
|
|
3
|
+
/** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
|
|
4
|
+
export function bindCli(fns) {
|
|
5
|
+
cli = fns;
|
|
6
|
+
}
|
|
7
|
+
const api = (method, path, body, options) => cli.api(method, path, body, options);
|
|
8
|
+
const resolveSigningAgentId = (opts, command) => cli.resolveSigningAgentId(opts, command);
|
|
9
|
+
// ─── flair relationship add ──────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// Ergonomic agent-directed write surface for the Relationship graph
|
|
12
|
+
// (relationship-write-path spec): an explicit subject/predicate/object triple
|
|
13
|
+
// ("record that <subject> <predicate> <object>"), distinct from a free-text
|
|
14
|
+
// Memory. Mirrors `flair memory add`'s shape (--agent required, signed via
|
|
15
|
+
// the shared `api()` helper — see api()'s doc above for the Ed25519
|
|
16
|
+
// resolution order) rather than hand-rolling a signer, per this repo's
|
|
17
|
+
// existing convention (flair orgevent does hand-roll one because OrgEvent.put()
|
|
18
|
+
// self-verifies authorId against the signature; Relationship doesn't need that
|
|
19
|
+
// — the server stamps agentId from the verdict regardless of what's sent).
|
|
20
|
+
//
|
|
21
|
+
// PUTs to the CANONICAL id (see canonicalRelationshipId below), not a random
|
|
22
|
+
// one — re-running this command with the SAME subject/predicate/object
|
|
23
|
+
// UPSERTS the existing row (confidence/validTo/source refresh) instead of
|
|
24
|
+
// creating a duplicate. This mirrors flair-client's RelationshipApi.write()
|
|
25
|
+
// (packages/flair-client/src/client.ts) BYTE FOR BYTE — the CLI can't import
|
|
26
|
+
// that workspace package into the published @tpsdev-ai/flair bundle (same
|
|
27
|
+
// reasoning as the existing Memory-id-generation mirroring a few thousand
|
|
28
|
+
// lines up), so the algorithm is duplicated here rather than shared. A
|
|
29
|
+
// cross-check test (test/unit/cli-relationship-add.test.ts) pins the two
|
|
30
|
+
// implementations to identical output so they can't silently drift apart —
|
|
31
|
+
// a drift here would mean the CLI and the MCP tool/RelationshipApi land the
|
|
32
|
+
// SAME triple at TWO different ids, defeating the whole dedup guarantee.
|
|
33
|
+
function canonicalRelationshipId(agentId, subject, predicate, object) {
|
|
34
|
+
const material = [agentId, subject, predicate, object].join("\u0000").toLowerCase();
|
|
35
|
+
return createHash("sha256").update(material, "utf8").digest().subarray(0, 16).toString("base64url");
|
|
36
|
+
}
|
|
37
|
+
/** Register the `flair relationship` command group (flair#1634). */
|
|
38
|
+
export function register(program) {
|
|
39
|
+
const relationship = program.command("relationship").description("Manage agent relationship triples (knowledge graph)");
|
|
40
|
+
relationship.command("add")
|
|
41
|
+
.description("Record that <subject> <predicate> <object> — an explicit entity-to-entity relationship triple. " +
|
|
42
|
+
"Re-asserting the SAME triple (same subject/predicate/object) UPSERTS the existing row rather than " +
|
|
43
|
+
"duplicating it. Predicate is free text; recommended vocabulary: manages, works_on, reviews, depends_on, " +
|
|
44
|
+
"replaces, owns, reports_to, advises. To CONTRADICT a prior relationship: changing the predicate creates " +
|
|
45
|
+
"a SEPARATE row and does NOT auto-close the old one — re-assert the OLD triple with --valid-to set to now " +
|
|
46
|
+
"(or delete it) before/after writing the new one.")
|
|
47
|
+
.requiredOption("--agent <id>")
|
|
48
|
+
.requiredOption("--subject <text>", "Source entity (e.g. 'nathan')")
|
|
49
|
+
.requiredOption("--predicate <text>", "Relationship type, free text (e.g. 'manages')")
|
|
50
|
+
.requiredOption("--object <text>", "Target entity (e.g. 'flair')")
|
|
51
|
+
.option("--confidence <n>", "0.0-1.0, how certain (default 1.0 = explicitly stated)")
|
|
52
|
+
.option("--valid-from <iso>", "ISO timestamp this relationship became true (default: now)")
|
|
53
|
+
.option("--valid-to <iso>", "ISO timestamp this relationship ended (leave unset for an active relationship)")
|
|
54
|
+
.option("--source <text>", "Where this was learned from (a memory ID, conversation, etc.)")
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const { agentId, source } = resolveSigningAgentId(opts, "relationship add");
|
|
57
|
+
const id = canonicalRelationshipId(opts.agent, opts.subject, opts.predicate, opts.object);
|
|
58
|
+
const body = {
|
|
59
|
+
id,
|
|
60
|
+
agentId: opts.agent,
|
|
61
|
+
subject: opts.subject,
|
|
62
|
+
predicate: opts.predicate,
|
|
63
|
+
object: opts.object,
|
|
64
|
+
};
|
|
65
|
+
if (opts.confidence !== undefined)
|
|
66
|
+
body.confidence = Number(opts.confidence);
|
|
67
|
+
if (opts.validFrom)
|
|
68
|
+
body.validFrom = opts.validFrom;
|
|
69
|
+
if (opts.validTo)
|
|
70
|
+
body.validTo = opts.validTo;
|
|
71
|
+
if (opts.source)
|
|
72
|
+
body.source = opts.source;
|
|
73
|
+
const out = await api("PUT", `/Relationship/${id}`, body, { agentId, agentIdSource: source });
|
|
74
|
+
console.log(JSON.stringify(out, null, 2));
|
|
75
|
+
});
|
|
76
|
+
}
|