@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,45 @@
|
|
|
1
|
+
import * as render from "../render.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
export function register(program) {
|
|
4
|
+
// ─── flair backup inspect ────────────────────────────────────────────────────
|
|
5
|
+
program
|
|
6
|
+
.command("inspect <path>")
|
|
7
|
+
.description("Show contents of a backup or export file")
|
|
8
|
+
.option("--json", "Emit raw JSON of the file (also: pipe + FLAIR_OUTPUT=json)")
|
|
9
|
+
.action(async (filePath, opts) => {
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
console.error(`${render.icons.error} File not found: ${render.wrap(render.c.dim, filePath)}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
15
|
+
const mode = render.resolveOutputMode(opts);
|
|
16
|
+
if (mode === "json") {
|
|
17
|
+
console.log(render.asJSON(data));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`${render.wrap(render.c.bold, "File:")} ${render.wrap(render.c.dim, filePath)}`);
|
|
21
|
+
const type = data.type ?? "full-backup";
|
|
22
|
+
const typeColor = type === "agent-export" ? render.c.cyan : render.c.magenta;
|
|
23
|
+
console.log(render.kv("Type", render.wrap(typeColor, type)));
|
|
24
|
+
console.log(render.kv("Created", String(data.createdAt ?? data.exportedAt ?? render.wrap(render.c.dim, "unknown"))));
|
|
25
|
+
console.log(render.kv("Source", String(data.source ?? render.wrap(render.c.dim, "unknown"))));
|
|
26
|
+
if (data.type === "agent-export") {
|
|
27
|
+
console.log(`\n${render.wrap(render.c.bold, "Agent")}: ${render.wrap(render.c.bold, data.agent?.id ?? "unknown")}`);
|
|
28
|
+
console.log(render.kv("Name", String(data.agent?.name ?? data.agent?.id ?? "—")));
|
|
29
|
+
console.log(render.kv("Memories", render.wrap(render.c.bold, String((data.memories ?? []).length))));
|
|
30
|
+
console.log(render.kv("Souls", render.wrap(render.c.bold, String((data.souls ?? []).length))));
|
|
31
|
+
console.log(render.kv("Grants", render.wrap(render.c.bold, String((data.grants ?? []).length))));
|
|
32
|
+
const keyText = data.privateKey ? render.wrap(render.c.magenta, "yes") : render.wrap(render.c.dim, "no");
|
|
33
|
+
console.log(render.kv("Key included", keyText));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const agents = data.agents ?? [];
|
|
37
|
+
console.log(`\n${render.wrap(render.c.bold, "Agents")}: ${render.wrap(render.c.bold, String(agents.length))}`);
|
|
38
|
+
for (const a of agents) {
|
|
39
|
+
console.log(` ${render.wrap(render.c.dim, "·")} ${render.wrap(render.c.bold, a.id)} ${render.wrap(render.c.dim, `(${a.name ?? a.id})`)}`);
|
|
40
|
+
}
|
|
41
|
+
console.log(render.kv("Memories", render.wrap(render.c.bold, String((data.memories ?? []).length))));
|
|
42
|
+
console.log(render.kv("Souls", render.wrap(render.c.bold, String((data.souls ?? []).length))));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as render from "../render.js";
|
|
4
|
+
import { loadEd25519PrivateKeyFromFile } from "../mcp-client-assertion.js";
|
|
5
|
+
import { defaultKeysDir } from "../lib/auth-resolve.js";
|
|
6
|
+
import { classifyKeyFile, resolveCollisionSafeName, pruneDateStamp, PRUNED_DIR_NAME, } from "../doctor-client.js";
|
|
7
|
+
let cli;
|
|
8
|
+
/** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
|
|
9
|
+
export function bindCli(fns) {
|
|
10
|
+
cli = fns;
|
|
11
|
+
}
|
|
12
|
+
const checkAgentRegistered = (baseUrl, agentId, keysDir) => cli.checkAgentRegistered(baseUrl, agentId, keysDir);
|
|
13
|
+
const probeFlairReachable = (url, timeoutMs) => cli.probeFlairReachable(url, timeoutMs);
|
|
14
|
+
const resolveBaseUrl = (opts) => cli.resolveBaseUrl(opts);
|
|
15
|
+
/** Best-effort seed-validity check for a `.key` file: does it parse via any
|
|
16
|
+
* of the formats loadEd25519PrivateKeyFromFile (src/mcp-client-assertion.ts
|
|
17
|
+
* — the same loader `flair mcp token` uses) accepts? Never throws — used
|
|
18
|
+
* only to decide "unidentified" vs. "worth a registration check", not to
|
|
19
|
+
* actually sign anything. An unparseable file is not junk: the keys dir is
|
|
20
|
+
* also where FileKeyStore writes AES-256-GCM blobs (flair#1026). */
|
|
21
|
+
function isValidPrivateKeySeedFile(keyPath) {
|
|
22
|
+
try {
|
|
23
|
+
loadEd25519PrivateKeyFromFile(keyPath);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify every entry in `keysDir` for `flair keys prune`. Pure READ —
|
|
32
|
+
* never writes or moves anything; see applyKeyPrune below for the actual
|
|
33
|
+
* move. Directories (including keysDir's own `.pruned` archive, PRUNED_DIR_NAME)
|
|
34
|
+
* and files not ending in `.key` are "ignored" without any network call.
|
|
35
|
+
* `.key` files with an unparseable seed are "unidentified" without a network
|
|
36
|
+
* call either — reported, never pruned (flair#1026). Only a `.key` file that
|
|
37
|
+
* DOES parse triggers a signed `GET /Agent/:id` against `baseUrl`
|
|
38
|
+
* (checkAgentRegistered above, the exact same check doctor's registration
|
|
39
|
+
* gate uses).
|
|
40
|
+
*
|
|
41
|
+
* If that check EVER reports "unreachable" — the instance couldn't be
|
|
42
|
+
* confirmed up for that key — the WHOLE run aborts immediately
|
|
43
|
+
* (`aborted: true`, `entries: []`, short-circuiting the loop): never
|
|
44
|
+
* classify anything, prunable or not, while registration state can't be
|
|
45
|
+
* verified. A missing keysDir is treated as "nothing to classify" (fresh
|
|
46
|
+
* install), not an error — matches `flair doctor`'s own "Keys directory
|
|
47
|
+
* missing" being a separate, non-fatal finding.
|
|
48
|
+
*/
|
|
49
|
+
export async function classifyKeysDir(keysDir, baseUrl) {
|
|
50
|
+
if (!existsSync(keysDir))
|
|
51
|
+
return { aborted: false, entries: [] };
|
|
52
|
+
const dirents = readdirSync(keysDir, { withFileTypes: true });
|
|
53
|
+
const entries = [];
|
|
54
|
+
const candidates = [];
|
|
55
|
+
for (const d of dirents) {
|
|
56
|
+
if (d.isDirectory()) {
|
|
57
|
+
entries.push({
|
|
58
|
+
name: d.name,
|
|
59
|
+
class: "ignored",
|
|
60
|
+
reason: d.name === PRUNED_DIR_NAME ? "prune archive directory" : "directory (not a key file)",
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!d.name.endsWith(".key")) {
|
|
65
|
+
entries.push({ name: d.name, class: "ignored", reason: "not a .key file" });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
candidates.push({ name: d.name, agentId: d.name.slice(0, -".key".length) });
|
|
69
|
+
}
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
const keyPath = join(keysDir, c.name);
|
|
72
|
+
if (!isValidPrivateKeySeedFile(keyPath)) {
|
|
73
|
+
const decision = classifyKeyFile(c.agentId, false, null, baseUrl);
|
|
74
|
+
entries.push({ name: c.name, class: decision.class, reason: decision.reason, agentId: c.agentId });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const reg = await checkAgentRegistered(baseUrl, c.agentId, keysDir);
|
|
78
|
+
if (reg.state === "unreachable") {
|
|
79
|
+
return {
|
|
80
|
+
aborted: true,
|
|
81
|
+
abortReason: `could not reach ${baseUrl} to verify agent '${c.agentId}' is registered` +
|
|
82
|
+
`${reg.detail ? ` (${reg.detail})` : ""} — aborting; nothing was classified or moved. ` +
|
|
83
|
+
`Pass --instance <url> to target a different instance.`,
|
|
84
|
+
entries: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// flair#1023 added "key-unreadable". It cannot occur here — this key's
|
|
88
|
+
// seed already parsed via isValidPrivateKeySeedFile above — but is
|
|
89
|
+
// handled explicitly rather than folded into the else: a key that will
|
|
90
|
+
// not load is "unidentified", not prunable "invalid" (flair#1026).
|
|
91
|
+
const decision = reg.state === "key-unreadable"
|
|
92
|
+
? classifyKeyFile(c.agentId, false, null, baseUrl)
|
|
93
|
+
: classifyKeyFile(c.agentId, true, { state: reg.state, detail: reg.detail }, baseUrl);
|
|
94
|
+
entries.push({ name: c.name, class: decision.class, reason: decision.reason, agentId: c.agentId });
|
|
95
|
+
}
|
|
96
|
+
return { aborted: false, entries };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Move every "stale" or "invalid" entry from `keysDir` into
|
|
100
|
+
* `<keysDir>/.pruned/<dateStamp>/`, creating the archive dir as needed —
|
|
101
|
+
* MOVE, never delete, so a bad classification is always recoverable. Only
|
|
102
|
+
* ever called with entries classifyKeysDir already decided are prunable; a
|
|
103
|
+
* "keep" or "ignored" entry passed in here is simply skipped (defense in
|
|
104
|
+
* depth — a registered agent's key must never move, even if a caller bug
|
|
105
|
+
* fed it in). Collisions (same filename already archived from an earlier
|
|
106
|
+
* prune run today) get a numeric suffix (resolveCollisionSafeName) rather
|
|
107
|
+
* than silently overwriting the earlier archive.
|
|
108
|
+
*/
|
|
109
|
+
export function applyKeyPrune(keysDir, entries, dateStamp) {
|
|
110
|
+
const prunable = entries.filter((e) => e.class === "stale" || e.class === "invalid");
|
|
111
|
+
if (prunable.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const destDir = join(keysDir, PRUNED_DIR_NAME, dateStamp);
|
|
114
|
+
mkdirSync(destDir, { recursive: true });
|
|
115
|
+
const existing = new Set(readdirSync(destDir));
|
|
116
|
+
const moved = [];
|
|
117
|
+
for (const e of prunable) {
|
|
118
|
+
const destName = resolveCollisionSafeName(existing, e.name);
|
|
119
|
+
existing.add(destName);
|
|
120
|
+
const from = join(keysDir, e.name);
|
|
121
|
+
const to = join(destDir, destName);
|
|
122
|
+
renameSync(from, to);
|
|
123
|
+
moved.push({ name: e.name, movedTo: to });
|
|
124
|
+
}
|
|
125
|
+
return moved;
|
|
126
|
+
}
|
|
127
|
+
/** Register the `flair keys` command group (flair#1629). */
|
|
128
|
+
export function register(program) {
|
|
129
|
+
const keys = program.command("keys").description("Manage Ed25519 key files in the key directory");
|
|
130
|
+
keys
|
|
131
|
+
.command("prune")
|
|
132
|
+
.description("Move stale/unregistered/invalid keys to <keysDir>/.pruned/<date>/ — dry-run by default")
|
|
133
|
+
.option("--apply", "Actually move prunable keys (default: dry-run, prints what would move and why)")
|
|
134
|
+
.option("--keys-dir <dir>", "Directory to scan for key files (else FLAIR_KEY_DIR, ~/.flair/keys)")
|
|
135
|
+
.option("--instance <url>", "Flair instance to check registration against (else FLAIR_TARGET/FLAIR_URL/config)")
|
|
136
|
+
.option("--port <port>", "Harper HTTP port (used when --instance/FLAIR_URL/FLAIR_TARGET are not set)")
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const keysDir = opts.keysDir ?? process.env.FLAIR_KEY_DIR ?? defaultKeysDir();
|
|
139
|
+
const baseUrl = resolveBaseUrl({ target: opts.instance, port: opts.port });
|
|
140
|
+
const apply = !!opts.apply;
|
|
141
|
+
console.log(`\n${render.wrap(render.c.bold, "🔑 Flair Keys Prune")}${apply ? "" : render.wrap(render.c.dim, " (dry run)")}\n`);
|
|
142
|
+
console.log(` Keys directory: ${render.wrap(render.c.dim, keysDir)}`);
|
|
143
|
+
console.log(` Instance: ${render.wrap(render.c.dim, baseUrl)}\n`);
|
|
144
|
+
const result = await classifyKeysDir(keysDir, baseUrl);
|
|
145
|
+
if (result.aborted) {
|
|
146
|
+
console.error(` ${render.icons.error} ${result.abortReason}`);
|
|
147
|
+
console.log("");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const stale = result.entries.filter((e) => e.class === "stale");
|
|
151
|
+
const invalid = result.entries.filter((e) => e.class === "invalid");
|
|
152
|
+
const unidentified = result.entries.filter((e) => e.class === "unidentified");
|
|
153
|
+
const kept = result.entries.filter((e) => e.class === "keep");
|
|
154
|
+
const ignored = result.entries.filter((e) => e.class === "ignored");
|
|
155
|
+
const prunable = [...stale, ...invalid];
|
|
156
|
+
if (stale.length + invalid.length + unidentified.length + kept.length === 0) {
|
|
157
|
+
console.log(` ${render.icons.ok} No key files found in ${render.wrap(render.c.dim, keysDir)} — nothing to prune.`);
|
|
158
|
+
console.log("");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const e of prunable) {
|
|
162
|
+
const icon = e.class === "invalid" ? render.icons.error : render.icons.warn;
|
|
163
|
+
console.log(` ${icon} ${render.wrap(render.c.bold, e.name)} — ${e.class}: ${e.reason}`);
|
|
164
|
+
}
|
|
165
|
+
for (const e of unidentified) {
|
|
166
|
+
console.log(` ${render.icons.warn} ${render.wrap(render.c.bold, e.name)} — unidentified: ${e.reason}`);
|
|
167
|
+
}
|
|
168
|
+
for (const e of kept) {
|
|
169
|
+
console.log(` ${render.icons.ok} ${e.name} — registered, keeping`);
|
|
170
|
+
}
|
|
171
|
+
if (!apply) {
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log(` ${render.wrap(render.c.dim, `${prunable.length} prunable (${stale.length} stale, ${invalid.length} invalid), ${kept.length} kept, ${unidentified.length} unidentified (left in place), ${ignored.length} ignored`)}`);
|
|
174
|
+
if (prunable.length > 0) {
|
|
175
|
+
console.log(` ${render.wrap(render.c.dim, "Run with --apply to move prunable keys to")} ${join(keysDir, PRUNED_DIR_NAME, pruneDateStamp())}`);
|
|
176
|
+
}
|
|
177
|
+
console.log("");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const moved = applyKeyPrune(keysDir, result.entries, pruneDateStamp());
|
|
181
|
+
console.log("");
|
|
182
|
+
for (const m of moved) {
|
|
183
|
+
console.log(` ${render.icons.ok} moved ${m.name} -> ${m.movedTo}`);
|
|
184
|
+
}
|
|
185
|
+
console.log(`\n ${render.wrap(render.c.bold, String(moved.length))} moved, ${kept.length} kept, ${unidentified.length} unidentified (left in place), ${ignored.length} ignored\n`);
|
|
186
|
+
});
|
|
187
|
+
}
|