@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,501 @@
|
|
|
1
|
+
import * as render from "../render.js";
|
|
2
|
+
import { resolveAdminUser } from "../lib/auth-resolve.js";
|
|
3
|
+
let cli;
|
|
4
|
+
/** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
|
|
5
|
+
export function bindCli(fns) {
|
|
6
|
+
cli = fns;
|
|
7
|
+
}
|
|
8
|
+
function api(...args) {
|
|
9
|
+
return cli.api(...args);
|
|
10
|
+
}
|
|
11
|
+
function resolveBaseUrl(opts) {
|
|
12
|
+
return cli.resolveBaseUrl(opts);
|
|
13
|
+
}
|
|
14
|
+
function resolveSigningAgentId(opts, command) {
|
|
15
|
+
return cli.resolveSigningAgentId(opts, command);
|
|
16
|
+
}
|
|
17
|
+
function applyAdminPassFile(opts) {
|
|
18
|
+
cli.applyAdminPassFile(opts);
|
|
19
|
+
}
|
|
20
|
+
function addSharedCredentialOptions(cmd) {
|
|
21
|
+
return cli.addSharedCredentialOptions(cmd);
|
|
22
|
+
}
|
|
23
|
+
function addSharedIdentityOption(cmd) {
|
|
24
|
+
return cli.addSharedIdentityOption(cmd);
|
|
25
|
+
}
|
|
26
|
+
function resolveOpsPort(opts) {
|
|
27
|
+
return cli.resolveOpsPort(opts);
|
|
28
|
+
}
|
|
29
|
+
function parseEntitiesOptionOrExit(csv) {
|
|
30
|
+
return cli.parseEntitiesOptionOrExit(csv);
|
|
31
|
+
}
|
|
32
|
+
// Exported for unit testing — keeps the predicate logic separable from
|
|
33
|
+
// the CLI plumbing, ops-API fetching, and confirmation flow.
|
|
34
|
+
export const HYGIENE_TEST_CONTENT_PATTERNS = [
|
|
35
|
+
/quick brown fox/i,
|
|
36
|
+
/flair\s*251\s*test/i,
|
|
37
|
+
/^upgrade-smoke-(pre|post)-marker$/i,
|
|
38
|
+
];
|
|
39
|
+
/** Categorize a single memory row against the enabled hygiene patterns.
|
|
40
|
+
* Returns the list of categories the row matches; empty array means clean.
|
|
41
|
+
* Pure function — easy to unit test and reason about. */
|
|
42
|
+
export function categorizeForHygiene(row, opts) {
|
|
43
|
+
const cats = [];
|
|
44
|
+
if (opts.enabled.has("compact-id") && typeof row.id === "string" && row.id.includes("-compact-")) {
|
|
45
|
+
cats.push("compact-id");
|
|
46
|
+
}
|
|
47
|
+
if (opts.enabled.has("test-content") && typeof row.content === "string" && HYGIENE_TEST_CONTENT_PATTERNS.some((p) => p.test(row.content))) {
|
|
48
|
+
cats.push("test-content");
|
|
49
|
+
}
|
|
50
|
+
if (opts.enabled.has("tiny") && typeof row.content === "string" && row.content.length < opts.tinyThreshold) {
|
|
51
|
+
cats.push("tiny");
|
|
52
|
+
}
|
|
53
|
+
return cats;
|
|
54
|
+
}
|
|
55
|
+
export function register(program) {
|
|
56
|
+
// ─── flair memory ──────────────────────────────────────────────────────────
|
|
57
|
+
const ENTITIES_OPTION_DESCRIPTION = cli.ENTITIES_OPTION_DESCRIPTION;
|
|
58
|
+
const memory = program.command("memory").description("Manage agent memories");
|
|
59
|
+
addSharedCredentialOptions(addSharedIdentityOption(memory.command("add [content]")
|
|
60
|
+
.description("Write a new memory row for an agent (content via positional arg or --content)")))
|
|
61
|
+
.option("--content <text>", "memory content (alias for positional arg)")
|
|
62
|
+
.option("--durability <d>", "permanent|persistent|standard|ephemeral (default standard). Also decides the default visibility when --visibility is omitted: permanent/persistent -> shared, standard/ephemeral -> private").option("--tags <csv>")
|
|
63
|
+
.option("--summary <text>", "agent-set multi-sentence dense compression (3-tier chain: subject → summary → content)")
|
|
64
|
+
.option("--subject <text>", "one-line title / entity this memory is about")
|
|
65
|
+
.option("--derived-from <csv>", "Comma-separated source Memory IDs this memory was distilled/reflected from (sets Memory.derivedFrom; used by the `rem rapid` reflection loop)")
|
|
66
|
+
.option("--visibility <value>", "Writer-controlled sharing intent (sets Memory.visibility): 'private' (owner-only, never visible to any other agent) or 'shared' (visible to owner + every other agent on this instance — open within the org, not gated by a MemoryGrant). Omit to use the server's durability-keyed default: permanent/persistent -> shared, standard/ephemeral -> private (flair#509)")
|
|
67
|
+
.option("--entities <csv>", ENTITIES_OPTION_DESCRIPTION)
|
|
68
|
+
.action(async (contentArg, opts) => {
|
|
69
|
+
const content = contentArg ?? opts.content;
|
|
70
|
+
if (!content) {
|
|
71
|
+
console.error("error: content required (positional arg or --content)");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
applyAdminPassFile(opts);
|
|
75
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory add");
|
|
76
|
+
if (!agentId) {
|
|
77
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
const memId = `${agentId}-${Date.now()}`;
|
|
81
|
+
const body = {
|
|
82
|
+
id: memId, agentId, content, durability: opts.durability || "standard",
|
|
83
|
+
tags: opts.tags ? String(opts.tags).split(",").map((x) => x.trim()).filter(Boolean) : undefined,
|
|
84
|
+
type: "memory", createdAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
if (opts.summary)
|
|
87
|
+
body.summary = opts.summary;
|
|
88
|
+
if (opts.subject)
|
|
89
|
+
body.subject = opts.subject;
|
|
90
|
+
// flair#991: reject an unrecognized --visibility instead of writing it.
|
|
91
|
+
// `visibility` is a free-form String server-side and the read scope asks
|
|
92
|
+
// isPrivateVisibility() — an exact match on the literal "private" — so
|
|
93
|
+
// ANY other string, `--visibility prvate` included, persists a row the
|
|
94
|
+
// user believes is owner-only and that every agent on the instance can
|
|
95
|
+
// in fact read. A typo must never widen who can read a memory.
|
|
96
|
+
if (opts.visibility) {
|
|
97
|
+
const visibility = String(opts.visibility).trim();
|
|
98
|
+
if (visibility !== "private" && visibility !== "shared") {
|
|
99
|
+
console.error(`error: --visibility must be 'private' or 'shared' (got: ${visibility})`);
|
|
100
|
+
console.error(" omit it to use the durability-keyed default: permanent/persistent -> shared, standard/ephemeral -> private");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
body.visibility = visibility;
|
|
104
|
+
}
|
|
105
|
+
if (opts.derivedFrom) {
|
|
106
|
+
body.derivedFrom = String(opts.derivedFrom).split(",").map((x) => x.trim()).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
// flair#1288: validated client-side; exits 1 with the canonical
|
|
109
|
+
// format-and-type-set message on any malformed value.
|
|
110
|
+
if (opts.entities) {
|
|
111
|
+
const entities = parseEntitiesOptionOrExit(String(opts.entities));
|
|
112
|
+
if (entities.length > 0)
|
|
113
|
+
body.entities = entities;
|
|
114
|
+
}
|
|
115
|
+
const out = await api("PUT", `/Memory/${memId}`, body, {
|
|
116
|
+
agentId,
|
|
117
|
+
agentIdSource: source,
|
|
118
|
+
explicitAdminPass: opts.adminPass,
|
|
119
|
+
adminUser: opts.adminUser,
|
|
120
|
+
});
|
|
121
|
+
console.log(JSON.stringify(out, null, 2));
|
|
122
|
+
});
|
|
123
|
+
// ─── flair memory write-task-summary ────────────────────────────────────────
|
|
124
|
+
// Slice 1 of FLAIR-AGENT-CONTEXT-TIERS-B. Standalone
|
|
125
|
+
// helper that any agent harness (or a manual operator) can invoke at task
|
|
126
|
+
// close to capture a structured task summary as a persistent Memory row
|
|
127
|
+
// before resetting the session.
|
|
128
|
+
//
|
|
129
|
+
// The shape of this row matters: tags=['task-summary','auto-on-reset'] +
|
|
130
|
+
// subject='task:<beads-id>' + summary populated. Slice 3+4 (harness
|
|
131
|
+
// integrations) will call this as part of the reset pipeline; slice 5+6
|
|
132
|
+
// (operator surfaces) will surface promote/restore controls. Today, this
|
|
133
|
+
// command is independently useful — operator can capture a manual summary
|
|
134
|
+
// at any time.
|
|
135
|
+
//
|
|
136
|
+
// Returns the memory id on stdout (single line, parseable) so the harness
|
|
137
|
+
// can plumb it into the next-dispatch system message.
|
|
138
|
+
memory.command("write-task-summary")
|
|
139
|
+
.description("Capture a structured task summary as a persistent Memory row (used by session-reset harness; standalone-callable by operators)")
|
|
140
|
+
.requiredOption("--agent <id>", "Agent the summary belongs to")
|
|
141
|
+
.requiredOption("--beads <ops-id>", "Bead/PR/task identifier this summary is about")
|
|
142
|
+
.requiredOption("--outcome <s>", "Outcome of the task: merged | rejected | abandoned")
|
|
143
|
+
.option("--summary <text>", "Multi-sentence dense compression (populates Memory.summary; will be the agent's read-time view)")
|
|
144
|
+
.option("--files-touched <csv>", "Comma-separated list of files touched during the task (becomes part of content)")
|
|
145
|
+
.option("--lessons <text>", "Lessons learned during the task (becomes part of content)")
|
|
146
|
+
.option("--derived-from <csv>", "Comma-separated list of source Memory IDs this summary was distilled from")
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
const validOutcomes = new Set(["merged", "rejected", "abandoned"]);
|
|
149
|
+
if (!validOutcomes.has(opts.outcome)) {
|
|
150
|
+
console.error(`Error: --outcome must be one of: merged, rejected, abandoned (got: ${opts.outcome})`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
if (!opts.summary && !opts.lessons && !opts.filesTouched) {
|
|
154
|
+
console.error("Error: at least one of --summary, --lessons, --files-touched is required (otherwise the summary has no content)");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
// Build the structured content block. Format chosen to be parseable + readable
|
|
158
|
+
// — the agent reads it back on bootstrap of the next session.
|
|
159
|
+
const lines = [];
|
|
160
|
+
lines.push(`task: ${opts.beads}`);
|
|
161
|
+
lines.push(`outcome: ${opts.outcome}`);
|
|
162
|
+
if (opts.filesTouched)
|
|
163
|
+
lines.push(`files: ${opts.filesTouched}`);
|
|
164
|
+
if (opts.lessons) {
|
|
165
|
+
lines.push("");
|
|
166
|
+
lines.push("lessons:");
|
|
167
|
+
lines.push(opts.lessons);
|
|
168
|
+
}
|
|
169
|
+
if (opts.summary) {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("summary:");
|
|
172
|
+
lines.push(opts.summary);
|
|
173
|
+
}
|
|
174
|
+
const content = lines.join("\n");
|
|
175
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory write-task-summary");
|
|
176
|
+
const memId = `${opts.agent}-task-${opts.beads}-${Date.now()}`;
|
|
177
|
+
const body = {
|
|
178
|
+
id: memId,
|
|
179
|
+
agentId: opts.agent,
|
|
180
|
+
content,
|
|
181
|
+
durability: "persistent",
|
|
182
|
+
tags: ["task-summary", "auto-on-reset"],
|
|
183
|
+
subject: `task:${opts.beads}`,
|
|
184
|
+
type: "task-summary",
|
|
185
|
+
createdAt: new Date().toISOString(),
|
|
186
|
+
};
|
|
187
|
+
if (opts.summary)
|
|
188
|
+
body.summary = opts.summary;
|
|
189
|
+
if (opts.derivedFrom) {
|
|
190
|
+
body.derivedFrom = String(opts.derivedFrom).split(",").map((x) => x.trim()).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
const out = await api("PUT", `/Memory/${encodeURIComponent(memId)}`, body, { agentId, agentIdSource: source });
|
|
193
|
+
if (out?.error) {
|
|
194
|
+
console.error(`Error writing task summary: ${out.error}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
// Print just the memory id on stdout so the harness can capture it
|
|
198
|
+
// without parsing a JSON blob.
|
|
199
|
+
console.log(memId);
|
|
200
|
+
});
|
|
201
|
+
memory.command("search [query]")
|
|
202
|
+
.description("Semantic search over an agent's memories (query via positional arg or --q)")
|
|
203
|
+
.option("--agent <id>", "Agent ID (or set FLAIR_AGENT_ID env)")
|
|
204
|
+
.option("--admin-pass <pass>", "Admin password — sign as admin while --agent names whose memories to search (flair#1500: a flag-pinned agent with no key no longer falls back to FLAIR_ADMIN_PASS)")
|
|
205
|
+
.option("--q <query>", "search query (alias for positional arg)")
|
|
206
|
+
.option("--limit <n>", "Max results", "5")
|
|
207
|
+
.option("--tag <tag>")
|
|
208
|
+
.option("--include-archived", "Include basemented (archived) memories in results (default: excluded)")
|
|
209
|
+
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET; alias for --url)")
|
|
210
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
211
|
+
.option("--port <port>", "Harper HTTP port")
|
|
212
|
+
.action(async (queryArg, opts) => {
|
|
213
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory search");
|
|
214
|
+
if (!agentId) {
|
|
215
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
const q = queryArg ?? opts.q;
|
|
219
|
+
if (!q) {
|
|
220
|
+
console.error("error: query required (positional arg or --q)");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const body = { agentId, q, limit: parseInt(opts.limit, 10) || 5 };
|
|
224
|
+
if (opts.tag)
|
|
225
|
+
body.tag = opts.tag;
|
|
226
|
+
if (opts.includeArchived)
|
|
227
|
+
body.includeArchived = true;
|
|
228
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
229
|
+
const res = await api("POST", "/SemanticSearch", body, { baseUrl, agentId, agentIdSource: source, explicitAdminPass: opts.adminPass });
|
|
230
|
+
console.log(JSON.stringify(res, null, 2));
|
|
231
|
+
});
|
|
232
|
+
// ─── flair memory basement / restore ────────────────────────────────────────
|
|
233
|
+
// flair#1472 Deliverable A — the user-facing archive action. `basement` sends a
|
|
234
|
+
// memory to the basement (archived=true + stamps archivedAt); `restore`
|
|
235
|
+
// un-basements it (clears archived/archivedAt/archivedBy). Both are GLOBAL and
|
|
236
|
+
// deliberate: restore un-retires the memory for EVERY session, not a
|
|
237
|
+
// session-local view (per-session reuse is drawers, Deliverable B, which does
|
|
238
|
+
// not exist yet). Scoped to the caller's own memories (own-lane write).
|
|
239
|
+
memory.command("basement <id>")
|
|
240
|
+
.description("Send a memory to the basement (archive it). Removes it from bootstrap + default search; still retrievable via `memory search --include-archived`. GLOBAL and deliberate — scoped to your own memories.")
|
|
241
|
+
.option("--agent <id>", "Agent ID (or set FLAIR_AGENT_ID env)")
|
|
242
|
+
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET; alias for --url)")
|
|
243
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
244
|
+
.option("--port <port>", "Harper HTTP port")
|
|
245
|
+
.action(async (id, opts) => {
|
|
246
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory basement");
|
|
247
|
+
if (!agentId) {
|
|
248
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
249
|
+
process.exit(2);
|
|
250
|
+
}
|
|
251
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
252
|
+
const res = await api("POST", "/MemoryArchive", { id, action: "basement" }, { baseUrl, agentId, agentIdSource: source });
|
|
253
|
+
console.log(JSON.stringify(res, null, 2));
|
|
254
|
+
});
|
|
255
|
+
memory.command("restore <id>")
|
|
256
|
+
.description("Restore a basemented (archived) memory. Clears archived/archivedAt/archivedBy. GLOBAL and deliberate — this un-retires the memory for EVERY session, not a session-local view (per-session reuse is drawers, which do not exist yet). Scoped to your own memories.")
|
|
257
|
+
.option("--agent <id>", "Agent ID (or set FLAIR_AGENT_ID env)")
|
|
258
|
+
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET; alias for --url)")
|
|
259
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
260
|
+
.option("--port <port>", "Harper HTTP port")
|
|
261
|
+
.action(async (id, opts) => {
|
|
262
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory restore");
|
|
263
|
+
if (!agentId) {
|
|
264
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
265
|
+
process.exit(2);
|
|
266
|
+
}
|
|
267
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
268
|
+
const res = await api("POST", "/MemoryArchive", { id, action: "restore" }, { baseUrl, agentId, agentIdSource: source });
|
|
269
|
+
console.log(JSON.stringify(res, null, 2));
|
|
270
|
+
});
|
|
271
|
+
memory.command("list")
|
|
272
|
+
.description("List an agent's memories (optionally filtered by --tag or embedding-backfill triage)")
|
|
273
|
+
.option("--agent <id>", "Agent ID (or set FLAIR_AGENT_ID env)")
|
|
274
|
+
.option("--tag <tag>")
|
|
275
|
+
.option("--hash-fallback", "Only memories with missing or hash-fallback embeddings (for backfill triage)")
|
|
276
|
+
.option("--limit <n>", "Max rows when using --hash-fallback", "50")
|
|
277
|
+
.option("--json", "Emit raw JSON array (also: pipe + FLAIR_OUTPUT=json)")
|
|
278
|
+
.action(async (opts) => {
|
|
279
|
+
const { agentId, source } = resolveSigningAgentId(opts, "memory list");
|
|
280
|
+
if (!agentId) {
|
|
281
|
+
console.error(`${render.icons.error} --agent <id> required (or set FLAIR_AGENT_ID)`);
|
|
282
|
+
process.exit(2);
|
|
283
|
+
}
|
|
284
|
+
const q = new URLSearchParams({ agentId, ...(opts.tag ? { tag: opts.tag } : {}) }).toString();
|
|
285
|
+
const raw = await api("GET", `/Memory?${q}`, undefined, { agentId, agentIdSource: source });
|
|
286
|
+
const mode = render.resolveOutputMode(opts);
|
|
287
|
+
// hashFallback flag changes the lens: instead of all memories, show
|
|
288
|
+
// only those that need re-embedding. Keep that surface separate.
|
|
289
|
+
if (opts.hashFallback) {
|
|
290
|
+
const all = Array.isArray(raw) ? raw : (raw?.results ?? raw?.items ?? []);
|
|
291
|
+
const fallback = all.filter((m) => !m.embeddingModel || m.embeddingModel === "hash-512d");
|
|
292
|
+
if (mode === "json") {
|
|
293
|
+
console.log(render.asJSON(fallback));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (fallback.length === 0) {
|
|
297
|
+
console.log(`${render.icons.ok} ${render.wrap(render.c.green, "All memories embedded")} ${render.wrap(render.c.dim, `(agent ${agentId})`)}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const limit = Math.max(1, parseInt(opts.limit, 10) || 50);
|
|
301
|
+
const rows = fallback
|
|
302
|
+
.slice()
|
|
303
|
+
.sort((a, b) => {
|
|
304
|
+
const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
305
|
+
const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
306
|
+
return tb - ta;
|
|
307
|
+
})
|
|
308
|
+
.slice(0, limit);
|
|
309
|
+
console.log(`${render.icons.warn} ${render.wrap(render.c.yellow, String(fallback.length))} hash-fallback memories for agent ${render.wrap(render.c.bold, agentId)} ${render.wrap(render.c.dim, `(showing ${rows.length})`)}\n`);
|
|
310
|
+
const cols = [
|
|
311
|
+
{ label: "id", key: "id" },
|
|
312
|
+
{
|
|
313
|
+
label: "created_at",
|
|
314
|
+
key: "createdAt",
|
|
315
|
+
format: (v) => (v ? String(v).slice(0, 19).replace("T", " ") : "—"),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
label: "preview",
|
|
319
|
+
key: "content",
|
|
320
|
+
format: (v) => String(v ?? "").replace(/\s+/g, " ").slice(0, 80),
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
console.log(render.table(cols, rows));
|
|
324
|
+
if (fallback.length > rows.length) {
|
|
325
|
+
console.log(`\n${render.wrap(render.c.dim, `... ${fallback.length - rows.length} more (raise with --limit). To backfill:`)} flair reembed --agent ${agentId} --stale-only`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
console.log(`\n${render.wrap(render.c.dim, "To backfill:")} flair reembed --agent ${agentId} --stale-only`);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Default lens: all memories for the agent.
|
|
333
|
+
const all = Array.isArray(raw) ? raw : (raw?.results ?? raw?.items ?? []);
|
|
334
|
+
if (mode === "json") {
|
|
335
|
+
console.log(render.asJSON(all));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (all.length === 0) {
|
|
339
|
+
console.log(`${render.icons.info} ${render.wrap(render.c.dim, `No memories for agent ${agentId}`)}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.log(`${render.wrap(render.c.bold, String(all.length))} memories for agent ${render.wrap(render.c.bold, agentId)}${opts.tag ? ` ${render.wrap(render.c.dim, `(tag=${opts.tag})`)}` : ""}\n`);
|
|
343
|
+
const sorted = all
|
|
344
|
+
.slice()
|
|
345
|
+
.sort((a, b) => {
|
|
346
|
+
const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
347
|
+
const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
348
|
+
return tb - ta;
|
|
349
|
+
});
|
|
350
|
+
const durabilityColor = (d) => {
|
|
351
|
+
if (d === "permanent")
|
|
352
|
+
return render.c.magenta;
|
|
353
|
+
if (d === "persistent")
|
|
354
|
+
return render.c.blue;
|
|
355
|
+
if (d === "ephemeral")
|
|
356
|
+
return render.c.gray;
|
|
357
|
+
return render.c.cyan;
|
|
358
|
+
};
|
|
359
|
+
const cols = [
|
|
360
|
+
{
|
|
361
|
+
label: "created_at",
|
|
362
|
+
key: "createdAt",
|
|
363
|
+
format: (v) => (v ? render.wrap(render.c.dim, String(v).slice(0, 10)) : render.wrap(render.c.dim, "—")),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
label: "durability",
|
|
367
|
+
key: "durability",
|
|
368
|
+
format: (v) => {
|
|
369
|
+
const d = String(v ?? "standard");
|
|
370
|
+
return render.wrap(durabilityColor(d), d);
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
label: "preview",
|
|
375
|
+
key: "content",
|
|
376
|
+
format: (v) => String(v ?? "").replace(/\s+/g, " ").slice(0, 80),
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
console.log(render.table(cols, sorted));
|
|
380
|
+
});
|
|
381
|
+
// ─── flair memory hygiene ────────────────────────────────────────────────────
|
|
382
|
+
// Detect + remove junk memory rows that accumulate over time. Surfaced from
|
|
383
|
+
// a 2026-05-07 manual cleanup: an instance had 627 records, ~250 of
|
|
384
|
+
// them were noise — `*-compact-*` ID fragments from an old pipeline, pangram
|
|
385
|
+
// test content ("the quick brown fox..." / "Flair 251 test ..."), and
|
|
386
|
+
// near-empty rows (<25 chars). We did the cleanup ad-hoc with raw curl + jq;
|
|
387
|
+
// this command bundles those patterns + future ones as an operator tool that
|
|
388
|
+
// dry-runs by default.
|
|
389
|
+
//
|
|
390
|
+
// Three pattern categories, each toggle-able:
|
|
391
|
+
// --pattern compact-id : ids matching /-compact-/ (legacy pipeline output)
|
|
392
|
+
// --pattern test-content : content matching pangram / known test strings
|
|
393
|
+
// --pattern tiny : content shorter than 25 chars
|
|
394
|
+
//
|
|
395
|
+
// Default is all three, dry-run. Flip --apply to actually delete. Always
|
|
396
|
+
// requires admin pass to read across agent scopes (uses ops API).
|
|
397
|
+
//
|
|
398
|
+
// Federation note: this only deletes on the local instance. Federation
|
|
399
|
+
// distributed-delete via tombstones is the systemic answer for
|
|
400
|
+
// fan-out — until that lands, run `flair memory hygiene` on each peer.
|
|
401
|
+
// Exported for unit testing — keeps the predicate logic separable from
|
|
402
|
+
// the CLI plumbing, ops-API fetching, and confirmation flow.
|
|
403
|
+
memory.command("hygiene")
|
|
404
|
+
.description("Detect and (with --apply) remove junk memory rows from the local instance")
|
|
405
|
+
.option("--apply", "Actually delete the matched rows (default: dry-run)")
|
|
406
|
+
.option("--pattern <list>", "Comma-separated patterns to match: compact-id,test-content,tiny (default: all)")
|
|
407
|
+
.option("--tiny-threshold <n>", "Char length below which content is 'tiny'", "25")
|
|
408
|
+
.option("--port <port>", "Harper HTTP port")
|
|
409
|
+
.option("--ops-port <port>", "Harper ops API port (default: HTTP - 1)")
|
|
410
|
+
.action(async (opts) => {
|
|
411
|
+
const opsPort = resolveOpsPort(opts);
|
|
412
|
+
const adminPass = process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD;
|
|
413
|
+
if (!adminPass) {
|
|
414
|
+
console.error("❌ Admin password required (set FLAIR_ADMIN_PASS or HDB_ADMIN_PASSWORD).");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
const enabled = new Set((opts.pattern ?? "compact-id,test-content,tiny").split(",").map((s) => s.trim()).filter(Boolean));
|
|
418
|
+
const tinyThreshold = Math.max(0, Number(opts.tinyThreshold) || 25);
|
|
419
|
+
const apply = !!opts.apply;
|
|
420
|
+
const opsAuth = `Basic ${Buffer.from(`${resolveAdminUser(undefined)}:${adminPass}`).toString("base64")}`;
|
|
421
|
+
async function ops(body) {
|
|
422
|
+
const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: { "Content-Type": "application/json", Authorization: opsAuth },
|
|
425
|
+
body: JSON.stringify(body),
|
|
426
|
+
signal: AbortSignal.timeout(60_000),
|
|
427
|
+
});
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
throw new Error(`ops API failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
430
|
+
}
|
|
431
|
+
return res.json();
|
|
432
|
+
}
|
|
433
|
+
// Fetch all rows via ops API. Bypasses /SemanticSearch (vector index) the
|
|
434
|
+
// same way `flair reembed` does, so this command works even when the
|
|
435
|
+
// cosine path is broken — exactly the conditions hygiene is most needed.
|
|
436
|
+
console.log("Scanning Memory table...");
|
|
437
|
+
const raw = await ops({
|
|
438
|
+
operation: "search_by_conditions",
|
|
439
|
+
database: "flair",
|
|
440
|
+
table: "Memory",
|
|
441
|
+
operator: "and",
|
|
442
|
+
conditions: [{ search_attribute: "createdAt", search_type: "greater_than", search_value: "1970-01-01" }],
|
|
443
|
+
get_attributes: ["id", "agentId", "content", "createdAt"],
|
|
444
|
+
limit: 100000,
|
|
445
|
+
});
|
|
446
|
+
const rows = Array.isArray(raw) ? raw : (raw?.results ?? []);
|
|
447
|
+
console.log(` ${rows.length} total memories scanned.`);
|
|
448
|
+
// Match each pattern. Counts by category, single id list for the delete.
|
|
449
|
+
const matched = new Map();
|
|
450
|
+
const allIds = new Set();
|
|
451
|
+
const hygieneOpts = { enabled: enabled, tinyThreshold };
|
|
452
|
+
for (const r of rows) {
|
|
453
|
+
const categories = categorizeForHygiene(r, hygieneOpts);
|
|
454
|
+
for (const c of categories) {
|
|
455
|
+
if (!matched.has(c))
|
|
456
|
+
matched.set(c, new Set());
|
|
457
|
+
matched.get(c).add(r.id);
|
|
458
|
+
allIds.add(r.id);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
console.log("");
|
|
462
|
+
console.log(`Match summary (${apply ? "APPLY" : "dry-run"}):`);
|
|
463
|
+
const allCategories = ["compact-id", "test-content", "tiny"];
|
|
464
|
+
for (const c of allCategories) {
|
|
465
|
+
const n = matched.get(c)?.size ?? 0;
|
|
466
|
+
const enabledMark = enabled.has(c) ? "✓" : "·";
|
|
467
|
+
console.log(` ${enabledMark} ${c.padEnd(13)} ${n.toString().padStart(5)} rows`);
|
|
468
|
+
}
|
|
469
|
+
console.log(` ────────────────────────────`);
|
|
470
|
+
console.log(` total unique ${allIds.size.toString().padStart(5)} rows`);
|
|
471
|
+
if (allIds.size === 0) {
|
|
472
|
+
console.log("\n✅ Nothing to clean.");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!apply) {
|
|
476
|
+
console.log("\n(dry-run) — re-run with --apply to delete the matched rows.");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// Delete in chunks (Harper accepts batches of hash_values).
|
|
480
|
+
const ids = Array.from(allIds);
|
|
481
|
+
const chunkSize = 200;
|
|
482
|
+
let deleted = 0;
|
|
483
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
484
|
+
const batch = ids.slice(i, i + chunkSize);
|
|
485
|
+
const result = await ops({
|
|
486
|
+
operation: "delete",
|
|
487
|
+
database: "flair",
|
|
488
|
+
table: "Memory",
|
|
489
|
+
hash_values: batch,
|
|
490
|
+
});
|
|
491
|
+
const m = /(\d+)\s*of\s*\d+\s*records/.exec(result.message ?? "");
|
|
492
|
+
deleted += m ? Number(m[1]) : batch.length;
|
|
493
|
+
process.stdout.write(`\r Deleting ${deleted}/${ids.length} (${Math.round((deleted / ids.length) * 100)}%)`);
|
|
494
|
+
}
|
|
495
|
+
console.log(`\n\n✅ Deleted ${deleted} rows.`);
|
|
496
|
+
console.log("");
|
|
497
|
+
console.log("Note: this is a local-instance delete. Federated peers will keep their copies until");
|
|
498
|
+
console.log("tombstone-based distributed delete lands. Until then, run `flair memory");
|
|
499
|
+
console.log("hygiene --apply` on each peer to fan out.");
|
|
500
|
+
});
|
|
501
|
+
}
|