@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,130 @@
|
|
|
1
|
+
import { resolveAdminUser } from "../lib/auth-resolve.js";
|
|
2
|
+
import * as render from "../render.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
let cli;
|
|
5
|
+
/** Bind the cli-locals this module depends on. */
|
|
6
|
+
export function bindCli(fns) {
|
|
7
|
+
cli = fns;
|
|
8
|
+
}
|
|
9
|
+
function resolveHttpPort(...args) {
|
|
10
|
+
return cli.resolveHttpPort(...args);
|
|
11
|
+
}
|
|
12
|
+
export function register(program) {
|
|
13
|
+
// ─── flair restore ────────────────────────────────────────────────────────────
|
|
14
|
+
program
|
|
15
|
+
.command("restore <path>")
|
|
16
|
+
.description("Import a Flair backup archive")
|
|
17
|
+
.option("--merge", "Add/update records without deleting existing (default)")
|
|
18
|
+
.option("--replace", "Delete all existing data for backed-up agents first, then import")
|
|
19
|
+
.option("--port <port>", "Harper HTTP port")
|
|
20
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
21
|
+
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
22
|
+
.option("--admin-user <name>", "Admin username for Basic auth (env: FLAIR_ADMIN_USER; default: admin)")
|
|
23
|
+
.option("--dry-run", "Show what would be imported without making changes")
|
|
24
|
+
.action(async (backupPath, opts) => {
|
|
25
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
26
|
+
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
27
|
+
const adminUser = resolveAdminUser(opts.adminUser);
|
|
28
|
+
const dryRun = Boolean(opts.dryRun);
|
|
29
|
+
const mode = opts.replace ? "replace" : "merge";
|
|
30
|
+
if (!adminPass) {
|
|
31
|
+
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required for restore");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (!existsSync(backupPath)) {
|
|
35
|
+
console.error(`Error: backup file not found: ${backupPath}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const backup = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
39
|
+
if (backup.version !== 1) {
|
|
40
|
+
console.error(`Error: unsupported backup version: ${backup.version}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const { agents = [], memories = [], souls = [] } = backup;
|
|
44
|
+
const auth = `Basic ${Buffer.from(`${adminUser}:${adminPass}`).toString("base64")}`;
|
|
45
|
+
console.log(`Restoring from: ${backupPath}`);
|
|
46
|
+
console.log(`Mode: ${mode}${dryRun ? " (dry run)" : ""}`);
|
|
47
|
+
console.log(` Agents: ${agents.length}`);
|
|
48
|
+
console.log(` Memories: ${memories.length}`);
|
|
49
|
+
console.log(` Souls: ${souls.length}`);
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log("\n✅ Dry run complete — no changes made");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
async function adminPut(path, body) {
|
|
55
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
58
|
+
body: JSON.stringify(body),
|
|
59
|
+
signal: AbortSignal.timeout(10_000),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => "");
|
|
63
|
+
throw new Error(`PUT ${path} failed (${res.status}): ${text}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function adminDelete(path) {
|
|
67
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
68
|
+
method: "DELETE",
|
|
69
|
+
headers: { Authorization: auth },
|
|
70
|
+
signal: AbortSignal.timeout(10_000),
|
|
71
|
+
});
|
|
72
|
+
// 404 is fine — already gone
|
|
73
|
+
if (!res.ok && res.status !== 404) {
|
|
74
|
+
const text = await res.text().catch(() => "");
|
|
75
|
+
throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Replace mode: delete existing data for these agents first
|
|
79
|
+
if (mode === "replace") {
|
|
80
|
+
console.log("\nDeleting existing data (replace mode)...");
|
|
81
|
+
for (const memory of memories) {
|
|
82
|
+
if (memory.id)
|
|
83
|
+
await adminDelete(`/Memory/${memory.id}`).catch((e) => console.warn(` warn: ${e.message}`));
|
|
84
|
+
}
|
|
85
|
+
for (const soul of souls) {
|
|
86
|
+
if (soul.id)
|
|
87
|
+
await adminDelete(`/Soul/${soul.id}`).catch((e) => console.warn(` warn: ${e.message}`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Restore agents
|
|
91
|
+
console.log("\nRestoring agents...");
|
|
92
|
+
let agentCount = 0;
|
|
93
|
+
for (const agent of agents) {
|
|
94
|
+
try {
|
|
95
|
+
await adminPut(`/Agent/${agent.id}`, agent);
|
|
96
|
+
agentCount++;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn(` warn: agent ${agent.id}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Restore souls before memories: refuseLearnedSoulWrite matches Memory text.
|
|
103
|
+
console.log("Restoring souls...");
|
|
104
|
+
let soulCount = 0;
|
|
105
|
+
for (const soul of souls) {
|
|
106
|
+
try {
|
|
107
|
+
await adminPut(`/Soul/${soul.id}`, soul);
|
|
108
|
+
soulCount++;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.warn(` warn: soul ${soul.id}: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log("Restoring memories...");
|
|
115
|
+
let memoryCount = 0;
|
|
116
|
+
for (const memory of memories) {
|
|
117
|
+
try {
|
|
118
|
+
await adminPut(`/Memory/${memory.id}`, memory);
|
|
119
|
+
memoryCount++;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.warn(` warn: memory ${memory.id}: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log(`\n${render.icons.ok} ${render.wrap(render.c.green, "Restore complete")}`);
|
|
126
|
+
console.log(render.kv("Agents restored", `${render.wrap(render.c.bold, String(agentCount))}${render.wrap(render.c.dim, `/${agents.length}`)}`));
|
|
127
|
+
console.log(render.kv("Memories restored", `${render.wrap(render.c.bold, String(memoryCount))}${render.wrap(render.c.dim, `/${memories.length}`)}`));
|
|
128
|
+
console.log(render.kv("Souls restored", `${render.wrap(render.c.bold, String(soulCount))}${render.wrap(render.c.dim, `/${souls.length}`)}`));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { buildEd25519Auth, resolveKeyPath } from "../lib/auth-resolve.js";
|
|
2
|
+
import * as render from "../render.js";
|
|
3
|
+
let cli;
|
|
4
|
+
/** Bind the cli-locals this module depends on. */
|
|
5
|
+
export function bindCli(fns) {
|
|
6
|
+
cli = fns;
|
|
7
|
+
}
|
|
8
|
+
function resolveBaseUrl(...args) {
|
|
9
|
+
return cli.resolveBaseUrl(...args);
|
|
10
|
+
}
|
|
11
|
+
function resolveSigningAgentId(...args) {
|
|
12
|
+
return cli.resolveSigningAgentId(...args);
|
|
13
|
+
}
|
|
14
|
+
function parseRelativeOrIso(input) {
|
|
15
|
+
if (!input)
|
|
16
|
+
return null;
|
|
17
|
+
const m = input.match(/^(\d+)([smhdw])$/);
|
|
18
|
+
if (!m)
|
|
19
|
+
return input;
|
|
20
|
+
const n = Number.parseInt(m[1], 10);
|
|
21
|
+
const unit = m[2];
|
|
22
|
+
const multMs = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
23
|
+
return new Date(Date.now() - n * (multMs[unit] ?? 0)).toISOString();
|
|
24
|
+
}
|
|
25
|
+
export function searchScoringFormula(scoring) {
|
|
26
|
+
return scoring === "composite"
|
|
27
|
+
? "semantic × durability-weight × recency-decay × usage-boost"
|
|
28
|
+
: "cosine similarity only";
|
|
29
|
+
}
|
|
30
|
+
export function buildSearchExplain(record, scoring, now = Date.now()) {
|
|
31
|
+
const score = typeof record?._score === "number" ? record._score : undefined;
|
|
32
|
+
const rawScore = typeof record?._rawScore === "number" ? record._rawScore : undefined;
|
|
33
|
+
// composite mode: server sends both (_rawScore = pre-composite semantic).
|
|
34
|
+
// raw mode: server sends only _score, and that IS the raw score.
|
|
35
|
+
const raw = scoring === "composite" ? rawScore : score;
|
|
36
|
+
const composite = scoring === "composite" ? score : undefined;
|
|
37
|
+
let ageDays;
|
|
38
|
+
if (record?.createdAt) {
|
|
39
|
+
const created = new Date(String(record.createdAt)).getTime();
|
|
40
|
+
if (Number.isFinite(created))
|
|
41
|
+
ageDays = Math.max(0, Math.floor((now - created) / 86_400_000));
|
|
42
|
+
}
|
|
43
|
+
const explain = {
|
|
44
|
+
scoring,
|
|
45
|
+
formula: searchScoringFormula(scoring),
|
|
46
|
+
durability: record?.durability ?? "standard",
|
|
47
|
+
usageCount: typeof record?.usageCount === "number" ? record.usageCount : 0,
|
|
48
|
+
};
|
|
49
|
+
if (typeof raw === "number")
|
|
50
|
+
explain.raw = raw;
|
|
51
|
+
if (typeof composite === "number")
|
|
52
|
+
explain.composite = composite;
|
|
53
|
+
if (typeof ageDays === "number")
|
|
54
|
+
explain.ageDays = ageDays;
|
|
55
|
+
return explain;
|
|
56
|
+
}
|
|
57
|
+
// Human one-liner for a hit's breakdown. Scoring terms come from the shared
|
|
58
|
+
// builder; the trailing tags/subject/supersedes are record context that json
|
|
59
|
+
// mode already carries at top level, so they're appended here only.
|
|
60
|
+
export function formatSearchExplain(explain, record) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (typeof explain.raw === "number")
|
|
63
|
+
parts.push(`raw=${explain.raw.toFixed(3)}`);
|
|
64
|
+
if (typeof explain.composite === "number")
|
|
65
|
+
parts.push(`composite=${explain.composite.toFixed(3)}`);
|
|
66
|
+
parts.push(`durability=${explain.durability}`);
|
|
67
|
+
if (typeof explain.ageDays === "number")
|
|
68
|
+
parts.push(`age=${explain.ageDays}d`);
|
|
69
|
+
parts.push(`usage=${explain.usageCount}`);
|
|
70
|
+
if (Array.isArray(record?.tags) && record.tags.length > 0)
|
|
71
|
+
parts.push(`tags=[${record.tags.join(",")}]`);
|
|
72
|
+
if (record?.subject)
|
|
73
|
+
parts.push(`subject=${record.subject}`);
|
|
74
|
+
if (record?.supersedes)
|
|
75
|
+
parts.push(`supersedes=${record.supersedes}`);
|
|
76
|
+
return parts.join(" · ");
|
|
77
|
+
}
|
|
78
|
+
export function register(program) {
|
|
79
|
+
// ─── flair search (top-level shortcut) ───────────────────────────────────────
|
|
80
|
+
// Parse --since / --as-of: accept ISO 8601 OR relative expressions
|
|
81
|
+
// ("1h", "7d", "30m"). Returns ISO 8601 string or null if input is empty.
|
|
82
|
+
// Returns the original string unchanged if it looks like a date (caller
|
|
83
|
+
// passes through to server, which validates).
|
|
84
|
+
program
|
|
85
|
+
.command("search <query>")
|
|
86
|
+
.description("Search memories by meaning (shortcut for memory search) — filterable, with --explain ranking")
|
|
87
|
+
.option("--agent <id>", "Agent ID (or set FLAIR_AGENT_ID env)")
|
|
88
|
+
.option("--limit <n>", "Max results", "5")
|
|
89
|
+
.option("--port <port>", "Harper HTTP port")
|
|
90
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
91
|
+
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET; alias for --url)")
|
|
92
|
+
.option("--key <path>", "Ed25519 private key path")
|
|
93
|
+
// Server-side filters (forwarded to /SemanticSearch payload)
|
|
94
|
+
.option("--tag <tag>", "Filter to memories carrying this tag")
|
|
95
|
+
.option("--subject <subject>", "Filter to memories carrying this subject (case-insensitive)")
|
|
96
|
+
.option("--subjects <list>", "Comma-separated list of subjects to OR-filter (case-insensitive)")
|
|
97
|
+
.option("--since <iso-or-relative>", "Only memories created after this point (ISO 8601 or '7d'/'24h'/'30m')")
|
|
98
|
+
.option("--as-of <iso>", "Temporal validity: only memories valid at this point (ISO 8601)")
|
|
99
|
+
.option("--include-superseded", "Include memories that have been superseded")
|
|
100
|
+
.option("--scoring <mode>", "Scoring mode: raw (default) uses cosine similarity/BM25 only; composite re-ranks by durability/recency/retrieval (measurably hurts precision as of flair#623 — opt-in only)", "raw")
|
|
101
|
+
.option("--min-score <n>", "Drop results below this score (0..1)", "0")
|
|
102
|
+
// Client-side filters (applied after server response)
|
|
103
|
+
.option("--durability <level>", "Filter to permanent|persistent|standard|ephemeral (client-side)")
|
|
104
|
+
.option("--source <name>", "Filter by source/agentId (client-side)")
|
|
105
|
+
// Output modes
|
|
106
|
+
.option("--explain", "Show score breakdown (raw, composite, durability, age, usage) per hit — also added to --json output as _explain")
|
|
107
|
+
.option("--json", "Output raw JSON array")
|
|
108
|
+
.action(async (query, opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const { agentId } = resolveSigningAgentId(opts, "search");
|
|
111
|
+
if (!agentId) {
|
|
112
|
+
console.error("error: --agent <id> required (or set FLAIR_AGENT_ID)");
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
116
|
+
const headers = { "content-type": "application/json" };
|
|
117
|
+
const keyPath = opts.key || resolveKeyPath(agentId);
|
|
118
|
+
if (keyPath) {
|
|
119
|
+
headers["authorization"] = buildEd25519Auth(agentId, "POST", "/SemanticSearch", keyPath);
|
|
120
|
+
}
|
|
121
|
+
// Build payload from CLI options. Server validates types.
|
|
122
|
+
const payload = {
|
|
123
|
+
agentId,
|
|
124
|
+
q: query,
|
|
125
|
+
limit: Number.parseInt(opts.limit, 10) || 5,
|
|
126
|
+
scoring: opts.scoring === "composite" ? "composite" : "raw",
|
|
127
|
+
};
|
|
128
|
+
if (opts.tag)
|
|
129
|
+
payload.tag = opts.tag;
|
|
130
|
+
if (opts.subject)
|
|
131
|
+
payload.subject = opts.subject;
|
|
132
|
+
if (opts.subjects)
|
|
133
|
+
payload.subjects = String(opts.subjects).split(",").map((s) => s.trim()).filter(Boolean);
|
|
134
|
+
const since = parseRelativeOrIso(opts.since);
|
|
135
|
+
if (since)
|
|
136
|
+
payload.since = since;
|
|
137
|
+
if (opts.asOf)
|
|
138
|
+
payload.asOf = opts.asOf;
|
|
139
|
+
if (opts.includeSuperseded)
|
|
140
|
+
payload.includeSuperseded = true;
|
|
141
|
+
const minScore = Number.parseFloat(opts.minScore ?? "0");
|
|
142
|
+
if (Number.isFinite(minScore) && minScore > 0)
|
|
143
|
+
payload.minScore = minScore;
|
|
144
|
+
const res = await fetch(`${baseUrl}/SemanticSearch`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers,
|
|
147
|
+
body: JSON.stringify(payload),
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok)
|
|
150
|
+
throw new Error(await res.text());
|
|
151
|
+
const result = (await res.json());
|
|
152
|
+
let results = result.results || result || [];
|
|
153
|
+
if (!Array.isArray(results))
|
|
154
|
+
results = [];
|
|
155
|
+
// Client-side filters: durability + source. Server doesn't expose these
|
|
156
|
+
// as conditions, so we filter after fetch.
|
|
157
|
+
if (opts.durability) {
|
|
158
|
+
const allowed = new Set(String(opts.durability).split(",").map((d) => d.trim()));
|
|
159
|
+
results = results.filter((r) => allowed.has(r.durability ?? "standard"));
|
|
160
|
+
}
|
|
161
|
+
if (opts.source) {
|
|
162
|
+
const allowed = new Set(String(opts.source).split(",").map((s) => s.trim()));
|
|
163
|
+
results = results.filter((r) => allowed.has(r._source ?? r.agentId ?? ""));
|
|
164
|
+
}
|
|
165
|
+
const mode = render.resolveOutputMode(opts);
|
|
166
|
+
const scoringMode = payload.scoring === "composite" ? "composite" : "raw";
|
|
167
|
+
if (mode === "json") {
|
|
168
|
+
// flair#992: --explain must be honoured here, not silently dropped.
|
|
169
|
+
// This branch is what every non-TTY caller lands in. The breakdown
|
|
170
|
+
// rides ALONG the json contract as an opt-in `_explain` key — present
|
|
171
|
+
// only when the caller typed --explain, so default output is unchanged
|
|
172
|
+
// — rather than switching output mode behind the caller's back.
|
|
173
|
+
const out = opts.explain
|
|
174
|
+
? results.map((r) => ({ ...r, _explain: buildSearchExplain(r, scoringMode) }))
|
|
175
|
+
: results;
|
|
176
|
+
console.log(render.asJSON(out));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (results.length === 0) {
|
|
180
|
+
console.log(`${render.icons.info} ${render.wrap(render.c.dim, "No results found.")}`);
|
|
181
|
+
const filters = [];
|
|
182
|
+
if (opts.tag)
|
|
183
|
+
filters.push(`tag=${opts.tag}`);
|
|
184
|
+
if (opts.subject)
|
|
185
|
+
filters.push(`subject=${opts.subject}`);
|
|
186
|
+
if (opts.subjects)
|
|
187
|
+
filters.push(`subjects=${opts.subjects}`);
|
|
188
|
+
if (opts.since)
|
|
189
|
+
filters.push(`since=${opts.since}`);
|
|
190
|
+
if (opts.durability)
|
|
191
|
+
filters.push(`durability=${opts.durability}`);
|
|
192
|
+
if (opts.source)
|
|
193
|
+
filters.push(`source=${opts.source}`);
|
|
194
|
+
if (filters.length > 0) {
|
|
195
|
+
console.log(` ${render.wrap(render.c.dim, "Filters:")} ${filters.join(render.wrap(render.c.dim, " · "))}`);
|
|
196
|
+
console.log(` ${render.icons.arrow} ${render.wrap(render.c.dim, "Try removing a filter or:")} flair search "${query}" --agent ${agentId}`);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const durabilityColor = (d) => {
|
|
201
|
+
if (d === "permanent")
|
|
202
|
+
return render.c.magenta;
|
|
203
|
+
if (d === "persistent")
|
|
204
|
+
return render.c.blue;
|
|
205
|
+
if (d === "ephemeral")
|
|
206
|
+
return render.c.gray;
|
|
207
|
+
return render.c.cyan;
|
|
208
|
+
};
|
|
209
|
+
for (const r of results) {
|
|
210
|
+
const date = r.createdAt ? String(r.createdAt).slice(0, 10) : "";
|
|
211
|
+
const scoreVal = typeof r._score === "number" ? r._score : 0;
|
|
212
|
+
const scorePct = typeof r._score === "number" ? `${(scoreVal * 100).toFixed(0)}%` : "";
|
|
213
|
+
const scoreColor = scoreVal >= 0.7 ? render.c.green : scoreVal >= 0.4 ? render.c.yellow : render.c.dim;
|
|
214
|
+
const durability = r.durability ?? "standard";
|
|
215
|
+
const metaParts = [];
|
|
216
|
+
if (date)
|
|
217
|
+
metaParts.push(render.wrap(render.c.dim, date));
|
|
218
|
+
metaParts.push(render.wrap(durabilityColor(durability), durability));
|
|
219
|
+
if (scorePct)
|
|
220
|
+
metaParts.push(render.wrap(scoreColor, scorePct));
|
|
221
|
+
if (r._source)
|
|
222
|
+
metaParts.push(render.wrap(render.c.cyan, `from:${r._source}`));
|
|
223
|
+
const meta = metaParts.join(render.wrap(render.c.dim, " · "));
|
|
224
|
+
console.log(` ${r.content}`);
|
|
225
|
+
if (meta)
|
|
226
|
+
console.log(` ${render.wrap(render.c.dim, "(")} ${meta} ${render.wrap(render.c.dim, ")")}`);
|
|
227
|
+
if (opts.explain) {
|
|
228
|
+
const line = formatSearchExplain(buildSearchExplain(r, scoringMode), r);
|
|
229
|
+
if (line) {
|
|
230
|
+
console.log(` ${render.wrap(render.c.gray, "└─")} ${render.wrap(render.c.dim, line)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
}
|
|
235
|
+
if (opts.explain) {
|
|
236
|
+
console.log(`${render.wrap(render.c.dim, "Scoring:")} ${render.wrap(render.c.bold, scoringMode)} ${render.wrap(render.c.dim, `(${searchScoringFormula(scoringMode)})`)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
console.error(`${render.icons.error} Search failed: ${err.message}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|