@tpsdev-ai/flair 0.52.0 → 0.54.1
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 +1914 -15328
- 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 +1654 -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 +914 -0
- package/dist/commands/test.js +93 -0
- package/dist/commands/uninstall.js +143 -0
- package/dist/commands/upgrade.js +1592 -0
- package/dist/commands/workspace.js +114 -0
- package/dist/deploy.js +24 -0
- package/dist/fabric-npm-install.js +87 -0
- 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 +199 -0
- package/dist/lib/ops-api-bind.js +115 -0
- package/dist/lib/owned-pins.js +219 -0
- package/dist/lib/stabilize-mqtt-network.js +123 -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 +43 -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/stamp-migration-verify.js +163 -0
- package/dist/stamp-outstanding.js +144 -0
- 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 +15 -7
- package/docs/spoke-bringup.md +10 -5
- package/docs/standalone-local.md +3 -1
- package/docs/upgrade.md +25 -6
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
- package/package.json +9 -4
- package/schemas/agent.graphql +15 -0
- package/templates/launchd/start-flair-with-admin-pass.sh +73 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import * as render from "../render.js";
|
|
2
|
+
import { resolveKeyPath, buildEd25519Auth } 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
|
+
const api = (method, path, body, options) => cli.api(method, path, body, options);
|
|
9
|
+
const resolveHttpPort = (opts, mode) => cli.resolveHttpPort(opts, mode);
|
|
10
|
+
/** Register the `flair bridge` command group. */
|
|
11
|
+
export function register(program) {
|
|
12
|
+
// ─── flair bridge ────────────────────────────────────────────────────────────
|
|
13
|
+
// Slice 1: discovery + scaffold. Slice 2: YAML runtime + `import` for Shape A
|
|
14
|
+
// + agentic-stack reference adapter as a built-in.
|
|
15
|
+
// `test` and `export` are still stubbed; Shape B (npm code plugins) too.
|
|
16
|
+
// See docs/bridges.md.
|
|
17
|
+
const bridge = program.command("bridge").description("Manage memory bridges (import/export between Flair and foreign systems)");
|
|
18
|
+
bridge
|
|
19
|
+
.command("list")
|
|
20
|
+
.description("List installed bridges across project YAML, user YAML, npm packages, and built-ins")
|
|
21
|
+
.option("--json", "Output as JSON")
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const { discover } = await import("../bridges/discover.js");
|
|
24
|
+
const { builtinDiscoveryRecords } = await import("../bridges/builtins/index.js");
|
|
25
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
26
|
+
const mode = render.resolveOutputMode(opts);
|
|
27
|
+
if (mode === "json") {
|
|
28
|
+
console.log(render.asJSON(found));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (found.length === 0) {
|
|
32
|
+
console.log(`${render.icons.info} ${render.wrap(render.c.dim, "No bridges installed.")}`);
|
|
33
|
+
console.log(`${render.wrap(render.c.dim, " Add one with:")} flair bridge scaffold <name> --file`);
|
|
34
|
+
console.log(`${render.wrap(render.c.dim, " Or install from npm:")} npm install flair-bridge-<name>`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`${render.wrap(render.c.bold, String(found.length))} bridge${found.length === 1 ? "" : "s"}\n`);
|
|
38
|
+
const cols = [
|
|
39
|
+
{ label: "name", key: "name", format: (v) => render.wrap(render.c.bold, String(v ?? "—")) },
|
|
40
|
+
{
|
|
41
|
+
label: "kind",
|
|
42
|
+
key: "kind",
|
|
43
|
+
format: (v) => {
|
|
44
|
+
const k = String(v ?? "—");
|
|
45
|
+
return render.wrap(k === "yaml" ? render.c.cyan : k === "api" ? render.c.magenta : render.c.dim, k);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "source",
|
|
50
|
+
key: "source",
|
|
51
|
+
format: (v) => {
|
|
52
|
+
const s = String(v ?? "—");
|
|
53
|
+
return render.wrap(s === "builtin" ? render.c.green : render.c.dim, s);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{ label: "description", key: "description", format: (v) => String(v ?? "") },
|
|
57
|
+
];
|
|
58
|
+
console.log(render.table(cols, found));
|
|
59
|
+
});
|
|
60
|
+
bridge
|
|
61
|
+
.command("scaffold <name>")
|
|
62
|
+
.description("Emit starter files for a new bridge. Choose --file (YAML, declarative) or --api (TS code plugin)")
|
|
63
|
+
.option("--file", "YAML file-format bridge (shape A)")
|
|
64
|
+
.option("--api", "TypeScript API bridge (shape B)")
|
|
65
|
+
.option("--force", "Overwrite existing files")
|
|
66
|
+
.action(async (name, opts) => {
|
|
67
|
+
if (opts.file && opts.api) {
|
|
68
|
+
console.error("Pick one: --file or --api.");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const { BUILTIN_BY_NAME } = await import("../bridges/builtins/index.js");
|
|
72
|
+
if (BUILTIN_BY_NAME.has(name)) {
|
|
73
|
+
console.error(`"${name}" is a built-in bridge name and can't be scaffolded — pick a different name.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const kind = opts.api ? "api" : "file"; // --file is default
|
|
77
|
+
const { scaffold } = await import("../bridges/scaffold.js");
|
|
78
|
+
try {
|
|
79
|
+
const result = await scaffold({ name, kind, force: !!opts.force });
|
|
80
|
+
if (result.createdFiles.length > 0) {
|
|
81
|
+
console.log(`Created ${result.createdFiles.length} file(s):`);
|
|
82
|
+
for (const p of result.createdFiles)
|
|
83
|
+
console.log(` + ${p}`);
|
|
84
|
+
}
|
|
85
|
+
if (result.skippedFiles.length > 0) {
|
|
86
|
+
console.log(`Skipped ${result.skippedFiles.length} existing file(s) (pass --force to overwrite):`);
|
|
87
|
+
for (const p of result.skippedFiles)
|
|
88
|
+
console.log(` · ${p}`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\n${result.summary}`);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error(`Scaffold failed: ${err.message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
bridge
|
|
98
|
+
.command("import <name> [src]")
|
|
99
|
+
.description("Import memories from a foreign system into Flair via a bridge (Shape A YAML / built-in)")
|
|
100
|
+
.option("--agent <id>", "Default agent ID for memories that don't carry one (or set FLAIR_AGENT_ID)")
|
|
101
|
+
.option("--cwd <dir>", "Filesystem root the descriptor's relative paths resolve against (default: cwd)")
|
|
102
|
+
.option("--dry-run", "Validate + count, don't write to Flair")
|
|
103
|
+
.option("--port <port>", "Harper HTTP port")
|
|
104
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
105
|
+
.option("--key <path>", "Ed25519 private key path (default: resolved from agent)")
|
|
106
|
+
.option("--source <path>", "Source directory (for directory-based imports like markdown)")
|
|
107
|
+
.action(async (name, srcArg, opts) => {
|
|
108
|
+
const agentId = opts.agent ?? process.env.FLAIR_AGENT_ID;
|
|
109
|
+
const cwd = opts.cwd ?? srcArg ?? process.cwd();
|
|
110
|
+
const { discover } = await import("../bridges/discover.js");
|
|
111
|
+
const { builtinDiscoveryRecords } = await import("../bridges/builtins/index.js");
|
|
112
|
+
const { loadBridge } = await import("../bridges/runtime/load-bridge.js");
|
|
113
|
+
const { runImport } = await import("../bridges/runtime/import-runner.js");
|
|
114
|
+
const { makeContext } = await import("../bridges/runtime/context.js");
|
|
115
|
+
const { BridgeRuntimeError } = await import("../bridges/types.js");
|
|
116
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
117
|
+
const target = found.find((b) => b.name === name);
|
|
118
|
+
if (!target) {
|
|
119
|
+
console.error(`No bridge named "${name}" — run \`flair bridge list\` to see installed bridges.`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
let loaded;
|
|
123
|
+
try {
|
|
124
|
+
loaded = await loadBridge(target);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
printBridgeError(err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
131
|
+
const ctx = makeContext({ bridge: name });
|
|
132
|
+
// Memory POST: Ed25519-signed when an agent key is available, fall back
|
|
133
|
+
// to the shared `api()` helper otherwise. Mirrors how `flair memory add`
|
|
134
|
+
// works (see the `memory.command("add")` handler above).
|
|
135
|
+
const putMemory = async (body) => {
|
|
136
|
+
const headers = { "content-type": "application/json" };
|
|
137
|
+
const keyPath = opts.key ?? resolveKeyPath(body.agentId);
|
|
138
|
+
if (keyPath) {
|
|
139
|
+
headers["authorization"] = buildEd25519Auth(body.agentId, "PUT", `/Memory/${body.id}`, keyPath);
|
|
140
|
+
}
|
|
141
|
+
const res = await fetch(`${baseUrl}/Memory/${encodeURIComponent(body.id)}`, {
|
|
142
|
+
method: "PUT",
|
|
143
|
+
headers,
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const text = await res.text().catch(() => "");
|
|
148
|
+
throw new Error(`PUT /Memory/${body.id} → ${res.status}: ${text || res.statusText}`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
let lastReportedAt = Date.now();
|
|
152
|
+
let lastReportedOrdinal = 0;
|
|
153
|
+
const onProgress = (ev) => {
|
|
154
|
+
// Throttle in-progress chatter to at most one line every 2s + the
|
|
155
|
+
// final summary. Avoids flooding stdout for big imports.
|
|
156
|
+
if (ev.type === "done") {
|
|
157
|
+
const noun = (n) => `${n} ${n === 1 ? "memory" : "memories"}`;
|
|
158
|
+
if (opts.dryRun) {
|
|
159
|
+
console.log(`\n${target.name}: would import ${noun(ev.total)}. Re-run without --dry-run to write to Flair.`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(`\n${target.name}: imported ${ev.imported}/${ev.total} memories${ev.skipped > 0 ? ` (${ev.skipped} skipped)` : ""}.`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
if (now - lastReportedAt < 2000 && ev.ordinal - lastReportedOrdinal < 25)
|
|
168
|
+
return;
|
|
169
|
+
lastReportedAt = now;
|
|
170
|
+
lastReportedOrdinal = ev.ordinal;
|
|
171
|
+
if (ev.type === "memory-imported") {
|
|
172
|
+
process.stdout.write(`\r ${ev.ordinal} imported (${ev.foreignId ?? ev.flairId})`.padEnd(80));
|
|
173
|
+
}
|
|
174
|
+
else if (ev.type === "memory-skipped") {
|
|
175
|
+
process.stdout.write(`\r ${ev.ordinal} skipped (${ev.reason})`.padEnd(80));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
if (loaded.kind === "yaml") {
|
|
180
|
+
await runImport({
|
|
181
|
+
bridgeName: target.name,
|
|
182
|
+
descriptor: loaded.descriptor,
|
|
183
|
+
cwd,
|
|
184
|
+
agentId,
|
|
185
|
+
dryRun: !!opts.dryRun,
|
|
186
|
+
putMemory,
|
|
187
|
+
onProgress,
|
|
188
|
+
ctx,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Code plugin: invoke bridge.import(opts, ctx) directly; the plugin
|
|
193
|
+
// returns an AsyncIterable of BridgeMemory that runImport processes.
|
|
194
|
+
if (!loaded.plugin.import) {
|
|
195
|
+
console.error(`Bridge "${name}" is a code plugin without an import() function — can only export through it.`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
// Code-plugin options: pass through all --X flags as a single object.
|
|
199
|
+
// The plugin's declared `options` descriptor validates what it actually cares about.
|
|
200
|
+
const pluginOpts = { ...opts };
|
|
201
|
+
const source = loaded.plugin.import(pluginOpts, ctx);
|
|
202
|
+
await runImport({
|
|
203
|
+
bridgeName: target.name,
|
|
204
|
+
source,
|
|
205
|
+
cwd,
|
|
206
|
+
agentId,
|
|
207
|
+
dryRun: !!opts.dryRun,
|
|
208
|
+
putMemory,
|
|
209
|
+
onProgress,
|
|
210
|
+
ctx,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err instanceof BridgeRuntimeError) {
|
|
216
|
+
printBridgeError(err);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
console.error(`Bridge import failed: ${err?.message ?? err}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
bridge
|
|
224
|
+
.command("export <name> <dst>")
|
|
225
|
+
.description("Export memories from Flair to a foreign system via a bridge (Shape A YAML / built-in)")
|
|
226
|
+
.requiredOption("--agent <id>", "Agent ID to export memories for (or set FLAIR_AGENT_ID)")
|
|
227
|
+
.option("--source <tag>", "Filter to memories with a matching `source:` tag (typical for round-tripping a single bridge's data)")
|
|
228
|
+
.option("--subject <subj>", "Filter to memories with a matching `subject:` tag")
|
|
229
|
+
.option("--since <iso>", "Only memories with createdAt >= this ISO-8601 timestamp")
|
|
230
|
+
.option("--cwd <dir>", "Filesystem root the descriptor's relative target paths resolve against (default: cwd)")
|
|
231
|
+
.option("--dry-run", "Validate + count + apply maps, don't write to the target")
|
|
232
|
+
.option("--port <port>", "Harper HTTP port")
|
|
233
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
234
|
+
.option("--key <path>", "Ed25519 private key path (default: resolved from agent)")
|
|
235
|
+
.action(async (name, dst, opts) => {
|
|
236
|
+
const agentId = opts.agent ?? process.env.FLAIR_AGENT_ID;
|
|
237
|
+
if (!agentId) {
|
|
238
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const cwd = opts.cwd ?? dst;
|
|
242
|
+
const { discover } = await import("../bridges/discover.js");
|
|
243
|
+
const { builtinDiscoveryRecords } = await import("../bridges/builtins/index.js");
|
|
244
|
+
const { loadBridge } = await import("../bridges/runtime/load-bridge.js");
|
|
245
|
+
const { runExport } = await import("../bridges/runtime/export-runner.js");
|
|
246
|
+
const { makeContext } = await import("../bridges/runtime/context.js");
|
|
247
|
+
const { BridgeRuntimeError } = await import("../bridges/types.js");
|
|
248
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
249
|
+
const target = found.find((b) => b.name === name);
|
|
250
|
+
if (!target) {
|
|
251
|
+
console.error(`No bridge named "${name}" — run \`flair bridge list\` to see installed bridges.`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
let loaded;
|
|
255
|
+
try {
|
|
256
|
+
loaded = await loadBridge(target);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
printBridgeError(err);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
if (loaded.kind === "yaml" && !loaded.descriptor.export) {
|
|
263
|
+
console.error(`Bridge "${name}" has no export block — cannot export through it.`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (loaded.kind === "code" && !loaded.plugin.export) {
|
|
267
|
+
console.error(`Bridge "${name}" is a code plugin without an export() function — can only import through it.`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
271
|
+
const ctx = makeContext({ bridge: name });
|
|
272
|
+
// Memory fetcher — paginates GET /Memory?agentId=... applying any
|
|
273
|
+
// descriptor + caller-side filters in memory. Slice 3a does the
|
|
274
|
+
// simplest thing: one round trip, no streaming. Slice 3b can move
|
|
275
|
+
// to cursor-paginated streaming if real corpora warrant it.
|
|
276
|
+
const fetchMemories = async function* (filters) {
|
|
277
|
+
const params = new URLSearchParams({ agentId });
|
|
278
|
+
if (opts.subject)
|
|
279
|
+
params.set("subject", opts.subject);
|
|
280
|
+
const headers = { "content-type": "application/json" };
|
|
281
|
+
const keyPath = opts.key ?? resolveKeyPath(agentId);
|
|
282
|
+
const path = `/Memory?${params.toString()}`;
|
|
283
|
+
if (keyPath)
|
|
284
|
+
headers["authorization"] = buildEd25519Auth(agentId, "GET", path, keyPath);
|
|
285
|
+
const res = await fetch(`${baseUrl}${path}`, { headers });
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
const text = await res.text().catch(() => "");
|
|
288
|
+
throw new Error(`GET /Memory → ${res.status}: ${text || res.statusText}`);
|
|
289
|
+
}
|
|
290
|
+
const raw = await res.json();
|
|
291
|
+
const all = Array.isArray(raw) ? raw : (raw?.results ?? raw?.items ?? []);
|
|
292
|
+
const sourceFilter = opts.source;
|
|
293
|
+
const sinceMs = opts.since ? new Date(opts.since).getTime() : null;
|
|
294
|
+
for (const m of all) {
|
|
295
|
+
if (sourceFilter && m.source !== sourceFilter)
|
|
296
|
+
continue;
|
|
297
|
+
if (sinceMs !== null && m.createdAt && new Date(m.createdAt).getTime() < sinceMs)
|
|
298
|
+
continue;
|
|
299
|
+
yield m;
|
|
300
|
+
}
|
|
301
|
+
void filters;
|
|
302
|
+
};
|
|
303
|
+
let lastReportedAt = Date.now();
|
|
304
|
+
const onProgress = (ev) => {
|
|
305
|
+
if (ev.type === "done") {
|
|
306
|
+
if (opts.dryRun) {
|
|
307
|
+
console.log(`\n${target.name}: would export ${ev.exported} memor${ev.exported === 1 ? "y" : "ies"} from ${ev.total} total. Re-run without --dry-run to write.`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.log(`\n${target.name}: exported ${ev.exported} memor${ev.exported === 1 ? "y" : "ies"} from ${ev.total} total.`);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (ev.type === "target-write") {
|
|
315
|
+
console.log(` ✓ ${ev.path} (${ev.written} record${ev.written === 1 ? "" : "s"})`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (ev.type === "target-skipped") {
|
|
319
|
+
console.log(` · ${ev.path} skipped (${ev.reason})`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// memory-skipped events throttled to one line every 2s
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
if (now - lastReportedAt < 2000)
|
|
325
|
+
return;
|
|
326
|
+
lastReportedAt = now;
|
|
327
|
+
process.stdout.write(`\r filtering memory ${ev.ordinal}...`.padEnd(60));
|
|
328
|
+
};
|
|
329
|
+
try {
|
|
330
|
+
if (loaded.kind === "yaml") {
|
|
331
|
+
await runExport({
|
|
332
|
+
descriptor: loaded.descriptor,
|
|
333
|
+
cwd,
|
|
334
|
+
fetchMemories,
|
|
335
|
+
filters: { agentId, subject: opts.subject, source: opts.source, since: opts.since },
|
|
336
|
+
dryRun: !!opts.dryRun,
|
|
337
|
+
ctx,
|
|
338
|
+
onProgress,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Code plugin export: invoke plugin.export(memoryStream, opts, ctx) directly.
|
|
343
|
+
// Plugin writes to its target however it likes (HTTP, file, etc.).
|
|
344
|
+
if (opts.dryRun) {
|
|
345
|
+
console.log(`${target.name}: dry-run not supported for code-plugin exports; aborting before invoking plugin.export().`);
|
|
346
|
+
process.exit(2);
|
|
347
|
+
}
|
|
348
|
+
const pluginOpts = { ...opts };
|
|
349
|
+
await loaded.plugin.export(fetchMemories({ agentId, subject: opts.subject, source: opts.source, since: opts.since }), pluginOpts, ctx);
|
|
350
|
+
console.log(`${target.name}: code-plugin export completed. Record count not reported by the plugin.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
if (err instanceof BridgeRuntimeError) {
|
|
355
|
+
printBridgeError(err);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
console.error(`Bridge export failed: ${err?.message ?? err}`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
bridge
|
|
363
|
+
.command("test <name>")
|
|
364
|
+
.description("Round-trip a bridge through its fixture: import → export → re-import → diff. Pass iff the stable fields (content/subject/tags/durability) match.")
|
|
365
|
+
.option("--fixture <path>", "Override the import source path (defaults to descriptor's import.sources[0].path)")
|
|
366
|
+
.option("--cwd <dir>", "Filesystem root the descriptor's relative paths resolve against (default: cwd)")
|
|
367
|
+
.option("--json", "Emit the full RoundTripResult as JSON on stdout")
|
|
368
|
+
.action(async (name, opts) => {
|
|
369
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
370
|
+
const { discover } = await import("../bridges/discover.js");
|
|
371
|
+
const { builtinDiscoveryRecords } = await import("../bridges/builtins/index.js");
|
|
372
|
+
const { loadBridge } = await import("../bridges/runtime/load-bridge.js");
|
|
373
|
+
const { runRoundTrip } = await import("../bridges/runtime/roundtrip.js");
|
|
374
|
+
const { BridgeRuntimeError } = await import("../bridges/types.js");
|
|
375
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
376
|
+
const target = found.find((b) => b.name === name);
|
|
377
|
+
if (!target) {
|
|
378
|
+
console.error(`No bridge named "${name}" — run \`flair bridge list\` to see installed bridges.`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
let loaded;
|
|
382
|
+
try {
|
|
383
|
+
loaded = await loadBridge(target);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
printBridgeError(err);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
if (loaded.kind === "code") {
|
|
390
|
+
console.error(`Bridge "${name}" is a code plugin — round-trip testing for code plugins lands in slice 3d (requires mocked fetch transport).`);
|
|
391
|
+
console.error(`For now, code plugins should ship their own tests alongside the npm package.`);
|
|
392
|
+
process.exit(2);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const result = await runRoundTrip({
|
|
396
|
+
descriptor: loaded.descriptor,
|
|
397
|
+
cwd,
|
|
398
|
+
fixturePath: opts.fixture,
|
|
399
|
+
// Keep the intermediate export so a failure can print a live path.
|
|
400
|
+
// The next harness start sweeps leftovers older than a minute (flair#1032).
|
|
401
|
+
retainTmpDir: true,
|
|
402
|
+
});
|
|
403
|
+
if (opts.json) {
|
|
404
|
+
console.log(JSON.stringify(result, null, 2));
|
|
405
|
+
process.exit(result.passed ? 0 : 1);
|
|
406
|
+
}
|
|
407
|
+
if (result.passed) {
|
|
408
|
+
console.log(`✅ ${target.name} round-trip passed (${result.expectedCount} record${result.expectedCount === 1 ? "" : "s"}).`);
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
console.log(`❌ ${target.name} round-trip failed.`);
|
|
412
|
+
console.log(` expected ${result.expectedCount} records, got ${result.actualCount} back.`);
|
|
413
|
+
if (result.missingInPass2.length > 0) {
|
|
414
|
+
console.log(` missing from re-import (${result.missingInPass2.length}):`);
|
|
415
|
+
for (const m of result.missingInPass2.slice(0, 5))
|
|
416
|
+
console.log(` - ${m.key}`);
|
|
417
|
+
if (result.missingInPass2.length > 5)
|
|
418
|
+
console.log(` ... ${result.missingInPass2.length - 5} more`);
|
|
419
|
+
}
|
|
420
|
+
if (result.unexpectedInPass2.length > 0) {
|
|
421
|
+
console.log(` unexpected extras in re-import (${result.unexpectedInPass2.length}):`);
|
|
422
|
+
for (const m of result.unexpectedInPass2.slice(0, 5))
|
|
423
|
+
console.log(` - ${m.key}`);
|
|
424
|
+
if (result.unexpectedInPass2.length > 5)
|
|
425
|
+
console.log(` ... ${result.unexpectedInPass2.length - 5} more`);
|
|
426
|
+
}
|
|
427
|
+
if (result.mismatches.length > 0) {
|
|
428
|
+
console.log(` field mismatches (${result.mismatches.length}):`);
|
|
429
|
+
for (const m of result.mismatches.slice(0, 10)) {
|
|
430
|
+
console.log(` - record ${m.ordinal} (${m.key}) field ${m.field}: expected ${JSON.stringify(m.expected)}, got ${JSON.stringify(m.got)}`);
|
|
431
|
+
}
|
|
432
|
+
if (result.mismatches.length > 10)
|
|
433
|
+
console.log(` ... ${result.mismatches.length - 10} more`);
|
|
434
|
+
}
|
|
435
|
+
console.log(`\n Intermediate export at: ${result.tmpExportPath}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
if (err instanceof BridgeRuntimeError) {
|
|
440
|
+
printBridgeError(err);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
console.error(`Bridge test failed: ${err?.message ?? err}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
bridge
|
|
448
|
+
.command("allow <name>")
|
|
449
|
+
.description("Approve an npm code-plugin bridge for execution. Approval is pinned to the package's location and package.json contents — a malicious package squatting on the same name in a different node_modules tree will be refused at load-time.")
|
|
450
|
+
.action(async (name) => {
|
|
451
|
+
const { discover } = await import("../bridges/discover.js");
|
|
452
|
+
const { builtinDiscoveryRecords } = await import("../bridges/builtins/index.js");
|
|
453
|
+
const { allow } = await import("../bridges/runtime/allow-list.js");
|
|
454
|
+
const found = await discover({ builtins: builtinDiscoveryRecords() });
|
|
455
|
+
const target = found.find((b) => b.name === name);
|
|
456
|
+
if (!target) {
|
|
457
|
+
console.error(`No bridge named "${name}" — run \`flair bridge list\` to see installed bridges.`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
if (target.source !== "npm-package") {
|
|
461
|
+
console.error(`"${name}" is a ${target.source} bridge; only npm code plugins require allow-list approval.`);
|
|
462
|
+
console.error(`YAML and built-in bridges run via the descriptor runtime and don't execute arbitrary JS.`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const result = await allow(name, target.path);
|
|
467
|
+
if (result.alreadyAllowed) {
|
|
468
|
+
console.log(`${name} was already allowed at ${result.entry.packageDir} — no change.`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const verb = result.updated ? "re-approved" : "allowed";
|
|
472
|
+
console.log(`✓ ${name} ${verb}.`);
|
|
473
|
+
console.log(` location: ${result.entry.packageDir}`);
|
|
474
|
+
console.log(` version: ${result.entry.version ?? "(not declared)"}`);
|
|
475
|
+
console.log(` digest: ${result.entry.packageJsonSha256.slice(0, 16)}…`);
|
|
476
|
+
console.log(` If the package later moves or its package.json content changes, execution is refused until you re-run this command.`);
|
|
477
|
+
console.log(` Revoke anytime with: flair bridge revoke ${name}`);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
console.error(`Failed to approve "${name}": ${err?.message ?? err}`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
bridge
|
|
485
|
+
.command("revoke <name>")
|
|
486
|
+
.description("Revoke approval for an npm code-plugin bridge (future invocations will require `flair bridge allow <name>` again)")
|
|
487
|
+
.action(async (name) => {
|
|
488
|
+
const { revoke } = await import("../bridges/runtime/allow-list.js");
|
|
489
|
+
const result = await revoke(name);
|
|
490
|
+
if (!result.wasAllowed) {
|
|
491
|
+
console.log(`${name} was not on the allow-list — no change.`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
console.log(`✓ ${name} revoked. Future invocations require \`flair bridge allow ${name}\` again.`);
|
|
495
|
+
});
|
|
496
|
+
bridge
|
|
497
|
+
.command("allow-list")
|
|
498
|
+
.description("Show the allow-listed code-plugin bridges")
|
|
499
|
+
.option("--json", "Emit raw JSON")
|
|
500
|
+
.action(async (opts) => {
|
|
501
|
+
const { list: listAllowed } = await import("../bridges/runtime/allow-list.js");
|
|
502
|
+
const entries = await listAllowed();
|
|
503
|
+
const mode = render.resolveOutputMode(opts);
|
|
504
|
+
if (mode === "json") {
|
|
505
|
+
console.log(render.asJSON(entries));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (entries.length === 0) {
|
|
509
|
+
console.log(`${render.icons.info} ${render.wrap(render.c.dim, "No code-plugin bridges are allow-listed yet.")}`);
|
|
510
|
+
console.log(`${render.wrap(render.c.dim, " Allow one with:")} flair bridge allow <name>`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
console.log(`${render.wrap(render.c.bold, String(entries.length))} allow-listed code-plugin bridge${entries.length === 1 ? "" : "s"}\n`);
|
|
514
|
+
for (const e of entries) {
|
|
515
|
+
console.log(`${render.wrap(render.c.bold, e.name)} ${render.wrap(render.c.dim, `(${e.version ?? "—"})`)} ${render.wrap(render.c.green, "✓ allowed")} ${render.wrap(render.c.dim, e.allowedAt)}`);
|
|
516
|
+
console.log(render.kv("location", render.wrap(render.c.dim, e.packageDir)));
|
|
517
|
+
console.log(render.kv("digest", render.wrap(render.c.dim, `sha256:${e.packageJsonSha256.slice(0, 16)}…`)));
|
|
518
|
+
console.log();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
function printBridgeError(err) {
|
|
522
|
+
// Pretty-print BridgeRuntimeError as the structured shape from §10 of the
|
|
523
|
+
// spec, plus a one-line human summary so the operator gets both.
|
|
524
|
+
const detail = err?.detail;
|
|
525
|
+
if (detail && typeof detail === "object") {
|
|
526
|
+
// Trust-check failures get a dedicated, operator-facing rendering.
|
|
527
|
+
// Dumping the full spec-§10 JSON is useful when an operator is
|
|
528
|
+
// debugging a broken YAML descriptor; for trust errors it buries the
|
|
529
|
+
// one thing that matters — the command to re-approve.
|
|
530
|
+
if (detail.field === "(trust)") {
|
|
531
|
+
printTrustError(detail);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
console.error(`Bridge error: ${detail.hint ?? err.message}`);
|
|
535
|
+
console.error(JSON.stringify(detail, null, 2));
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
console.error(`Bridge error: ${err.message ?? String(err)}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function printTrustError(detail) {
|
|
542
|
+
const name = detail.bridge ?? "(unknown)";
|
|
543
|
+
const ctx = detail.context ?? {};
|
|
544
|
+
const reapprove = ` flair bridge allow ${name}`;
|
|
545
|
+
const bar = "─".repeat(60);
|
|
546
|
+
const header = (title) => {
|
|
547
|
+
console.error("");
|
|
548
|
+
console.error(`⚠ ${title} — ${name}`);
|
|
549
|
+
console.error(bar);
|
|
550
|
+
};
|
|
551
|
+
const footer = (label) => {
|
|
552
|
+
console.error("");
|
|
553
|
+
console.error(`${label}:`);
|
|
554
|
+
console.error(reapprove);
|
|
555
|
+
console.error("");
|
|
556
|
+
};
|
|
557
|
+
switch (detail.got) {
|
|
558
|
+
case "not-allowed":
|
|
559
|
+
header("Approval required");
|
|
560
|
+
console.error("This bridge is an npm code plugin — it runs arbitrary JavaScript.");
|
|
561
|
+
console.error("First-use approval is required before Flair will execute it.");
|
|
562
|
+
footer("Approve it with");
|
|
563
|
+
return;
|
|
564
|
+
case "path-mismatch":
|
|
565
|
+
header("Trust check failed: package location changed");
|
|
566
|
+
console.error("A different package with the same name was discovered. This is how");
|
|
567
|
+
console.error("local squatting attacks present — a planted `node_modules/flair-bridge-*`");
|
|
568
|
+
console.error("in an unrelated project tree.");
|
|
569
|
+
console.error("");
|
|
570
|
+
console.error(` approved: ${ctx.approvedPath ?? "(unknown)"}`);
|
|
571
|
+
console.error(` version ${ctx.approvedVersion ?? "?"} at ${ctx.approvedAt ?? "?"}`);
|
|
572
|
+
console.error(` now: ${ctx.observedPath ?? "(unknown)"}`);
|
|
573
|
+
footer("If the new location is intentional, re-approve");
|
|
574
|
+
return;
|
|
575
|
+
case "digest-mismatch":
|
|
576
|
+
header("Trust check failed: package contents changed");
|
|
577
|
+
console.error("The package.json at the approved location has changed since you");
|
|
578
|
+
console.error("approved this bridge. This fires on every upgrade — it's a trust");
|
|
579
|
+
console.error("event, not an error. If the update is intentional, re-approve.");
|
|
580
|
+
console.error("");
|
|
581
|
+
console.error(` location: ${ctx.packagePath ?? "(unknown)"}`);
|
|
582
|
+
console.error(` approved version: ${ctx.approvedVersion ?? "?"} (at ${ctx.approvedAt ?? "?"})`);
|
|
583
|
+
console.error(` approved digest: sha256:${(ctx.approvedDigest ?? "").slice(0, 16)}…`);
|
|
584
|
+
console.error(` observed digest: sha256:${(ctx.observedDigest ?? "").slice(0, 16)}…`);
|
|
585
|
+
footer("Re-approve");
|
|
586
|
+
return;
|
|
587
|
+
case "entry-incomplete":
|
|
588
|
+
header("Trust check failed: approval record is incomplete");
|
|
589
|
+
console.error("The allow-list entry for this bridge is missing a location or digest.");
|
|
590
|
+
console.error("This usually means the record was created by a pre-fix Flair version");
|
|
591
|
+
console.error("(0.6.0 / 0.6.1) that only stored the name. Re-approve to upgrade.");
|
|
592
|
+
footer("Re-approve");
|
|
593
|
+
return;
|
|
594
|
+
case "package-missing":
|
|
595
|
+
header("Trust check failed: approved package missing on disk");
|
|
596
|
+
console.error("The package location recorded at allow-time is no longer readable.");
|
|
597
|
+
console.error("");
|
|
598
|
+
console.error(` approved at: ${ctx.approvedPath ?? "(unknown)"}`);
|
|
599
|
+
console.error(` discovered: ${ctx.discoveredPath ?? "(unknown)"}`);
|
|
600
|
+
footer("Reinstall the package, then re-approve");
|
|
601
|
+
return;
|
|
602
|
+
default:
|
|
603
|
+
// Unknown trust sub-reason — fall back to the raw structured print.
|
|
604
|
+
console.error(`Bridge error (trust): ${detail.hint ?? detail.got ?? "unknown"}`);
|
|
605
|
+
console.error(JSON.stringify(detail, null, 2));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|