@tpsdev-ai/flair-mcp 0.53.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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Stdio adapter bindings (flair#1580).
3
+ *
4
+ * The tool SET is derived from STDIO_TOOL_DESCRIPTORS. This module only
5
+ * supplies FlairClient HTTP handlers — one per stdio descriptor. A new
6
+ * descriptor with no handler (or a handler with no descriptor) fails at
7
+ * registration, so the surfaces cannot drift by omission.
8
+ */
9
+ import { STDIO_TOOL_DESCRIPTORS, toStdioMcpToolDef, } from "@tpsdev-ai/flair-tool-descriptors";
10
+ import { buildCatchupRequest, summarizeCatchup } from "./catchup.js";
11
+ import { classifyError } from "./errors.js";
12
+ import { jsonSchemaToZodShape } from "./json-schema-zod.js";
13
+ import { deriveActivity } from "./presence.js";
14
+ import { buildRecordUsageBody, citationIds, withCiteNudge } from "./usage.js";
15
+ import { buildSkillSearchBody, buildSkillStoreBody, formatSkillCatalog, isSkillRecord, projectSkillSearchResponse, stripInternalMemoryFields, } from "./skills.js";
16
+ function errorResult(err, flairUrl) {
17
+ return { content: [{ type: "text", text: classifyError(err, flairUrl) }], isError: true };
18
+ }
19
+ const memory_search = async ({ query, limit }, { flair, heartbeat }) => {
20
+ heartbeat();
21
+ try {
22
+ const results = await flair.memory.search(query, { limit: limit ?? 5 });
23
+ if (results.length === 0) {
24
+ return { content: [{ type: "text", text: "No relevant memories found." }] };
25
+ }
26
+ const text = results
27
+ .map((r, i) => {
28
+ const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
29
+ const idStr = r.id ? `id:${r.id}` : "";
30
+ const meta = [date, r.type, idStr].filter(Boolean).join(", ");
31
+ return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
32
+ })
33
+ .join("\n");
34
+ return { content: [{ type: "text", text: withCiteNudge(text) }] };
35
+ }
36
+ catch (err) {
37
+ return errorResult(err, flair.url);
38
+ }
39
+ };
40
+ const memory_store = async ({ content, type, durability, tags, visibility, usedMemoryIds }, { flair, heartbeat }) => {
41
+ heartbeat();
42
+ try {
43
+ const result = await flair.memory.write(content, {
44
+ type: (type ?? "session"),
45
+ durability: (durability ?? "standard"),
46
+ tags,
47
+ visibility: visibility,
48
+ dedup: true,
49
+ dedupThreshold: 0.95,
50
+ usedMemoryIds: citationIds(usedMemoryIds),
51
+ });
52
+ const deduplicated = result.deduplicated === true;
53
+ const matchedId = result.matchedId;
54
+ const effectiveVisibility = result.visibility;
55
+ const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
56
+ const tagStr = tags && tags.length > 0 ? tags.join(", ") : "none";
57
+ const lines = [
58
+ `Memory stored (id: ${result.id})`,
59
+ `Preview: ${preview}`,
60
+ `Size: ${content.length} chars`,
61
+ `Tags: ${tagStr}`,
62
+ `Type: ${type ?? "session"}, Durability: ${durability ?? "standard"}, Visibility: ${effectiveVisibility ?? "(server default)"}`,
63
+ ];
64
+ if (deduplicated && matchedId) {
65
+ lines.push("", `Note: similar to existing memory id=${matchedId} — both are kept. ` +
66
+ `If this was meant to UPDATE that memory rather than add a new one, use memory_update instead.`);
67
+ }
68
+ return {
69
+ content: [{ type: "text", text: lines.join("\n") }],
70
+ structuredContent: { deduplicated, id: result.id, written: true, ...(deduplicated ? { matchedId } : {}) },
71
+ };
72
+ }
73
+ catch (err) {
74
+ return errorResult(err, flair.url);
75
+ }
76
+ };
77
+ const memory_update = async ({ id, content, preserveHistory, usedMemoryIds }, { flair, heartbeat }) => {
78
+ heartbeat();
79
+ try {
80
+ const result = await flair.memory.update(id, content, {
81
+ preserveHistory,
82
+ usedMemoryIds: citationIds(usedMemoryIds),
83
+ });
84
+ const text = preserveHistory
85
+ ? `Memory updated: new version stored (id: ${result.id}), supersedes ${id}.`
86
+ : `Memory updated (id: ${id}).`;
87
+ return {
88
+ content: [{ type: "text", text }],
89
+ structuredContent: { id: result.id, supersedes: preserveHistory ? id : undefined, written: true },
90
+ };
91
+ }
92
+ catch (err) {
93
+ return errorResult(err, flair.url);
94
+ }
95
+ };
96
+ const memory_get = async ({ id }, { flair, heartbeat }) => {
97
+ heartbeat();
98
+ try {
99
+ const mem = await flair.memory.get(id);
100
+ if (!mem)
101
+ return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
102
+ return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
103
+ }
104
+ catch (err) {
105
+ return errorResult(err, flair.url);
106
+ }
107
+ };
108
+ const memory_delete = async ({ id }, { flair, heartbeat }) => {
109
+ heartbeat();
110
+ try {
111
+ await flair.memory.delete(id);
112
+ return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
113
+ }
114
+ catch (err) {
115
+ return errorResult(err, flair.url);
116
+ }
117
+ };
118
+ const relationship_store = async ({ subject, predicate, object, confidence, validFrom, validTo, source }, { flair, heartbeat }) => {
119
+ heartbeat();
120
+ try {
121
+ const result = await flair.relationship.write({ subject, predicate, object, confidence, validFrom, validTo, source });
122
+ const confStr = confidence !== undefined ? ` (confidence: ${confidence})` : "";
123
+ return {
124
+ content: [{ type: "text", text: `Relationship recorded: ${subject} → ${predicate} → ${object}${confStr} (id: ${result.id})` }],
125
+ structuredContent: { id: result.id, subject, predicate, object, written: true },
126
+ };
127
+ }
128
+ catch (err) {
129
+ return errorResult(err, flair.url);
130
+ }
131
+ };
132
+ const bootstrap = async ({ maxTokens, currentTask, channel, surface, subjects }, { flair, heartbeat, rememberTask }) => {
133
+ if (currentTask)
134
+ rememberTask(currentTask);
135
+ heartbeat(deriveActivity({ channel, surface }));
136
+ try {
137
+ const result = await flair.bootstrap({ maxTokens, currentTask, channel, surface, subjects });
138
+ if (!result.context) {
139
+ return { content: [{ type: "text", text: "No context available." }] };
140
+ }
141
+ return { content: [{ type: "text", text: withCiteNudge(result.context) }] };
142
+ }
143
+ catch (err) {
144
+ return errorResult(err, flair.url);
145
+ }
146
+ };
147
+ const soul_set = async ({ key, value }, { flair, heartbeat }) => {
148
+ heartbeat();
149
+ try {
150
+ await flair.soul.set(key, value);
151
+ return { content: [{ type: "text", text: `Soul entry '${key}' set.` }] };
152
+ }
153
+ catch (err) {
154
+ return errorResult(err, flair.url);
155
+ }
156
+ };
157
+ const soul_get = async ({ key }, { flair, heartbeat }) => {
158
+ heartbeat();
159
+ try {
160
+ const entry = await flair.soul.get(key);
161
+ if (!entry)
162
+ return { content: [{ type: "text", text: `No soul entry for '${key}'.` }] };
163
+ return { content: [{ type: "text", text: entry.value }] };
164
+ }
165
+ catch (err) {
166
+ return errorResult(err, flair.url);
167
+ }
168
+ };
169
+ const flair_workspace_set = async ({ ref, label, provider, task, phase, summary }, { flair, agentId, heartbeat }) => {
170
+ heartbeat();
171
+ try {
172
+ const body = {
173
+ id: `${agentId}:${ref}`,
174
+ ref,
175
+ provider: provider ?? "mcp",
176
+ timestamp: new Date().toISOString(),
177
+ };
178
+ if (label)
179
+ body.label = label;
180
+ if (task)
181
+ body.taskId = task;
182
+ if (phase)
183
+ body.phase = phase;
184
+ if (summary)
185
+ body.summary = summary;
186
+ await flair.request("POST", "/WorkspaceState", body);
187
+ return { content: [{ type: "text", text: `Workspace state set: ref=${ref}${phase ? `, phase=${phase}` : ""} (attributed to ${agentId}).` }] };
188
+ }
189
+ catch (err) {
190
+ return errorResult(err, flair.url);
191
+ }
192
+ };
193
+ const flair_orgevent = async ({ kind, summary, detail, scope, targets }, { flair, agentId, heartbeat }) => {
194
+ heartbeat();
195
+ try {
196
+ const body = { kind, summary };
197
+ if (detail)
198
+ body.detail = detail;
199
+ if (scope)
200
+ body.scope = scope;
201
+ if (targets && targets.length > 0)
202
+ body.targetIds = targets;
203
+ const result = await flair.request("POST", "/OrgEvent", body);
204
+ const targetStr = targets && targets.length > 0 ? ` → ${targets.join(", ")}` : "";
205
+ const idStr = result?.id ? ` (id: ${result.id})` : "";
206
+ return { content: [{ type: "text", text: `OrgEvent published: kind=${kind}${targetStr} (attributed to ${agentId})${idStr}.` }] };
207
+ }
208
+ catch (err) {
209
+ return errorResult(err, flair.url);
210
+ }
211
+ };
212
+ const flair_catchup = async (args, { flair, agentId, heartbeat }) => {
213
+ heartbeat();
214
+ try {
215
+ // Owner-scope by construction: the request path is built from the CALLER's
216
+ // own agentId (identity), never a tool argument — `args` is consulted only
217
+ // for after/limit/ack. There is deliberately no agentId/participantId
218
+ // parameter, so a caller cannot name another feed (the server also refuses
219
+ // a cross-agent read with 403).
220
+ const request = buildCatchupRequest(agentId, args);
221
+ if (request.ackPosition) {
222
+ // Advance-on-ack (monotonic, re-ack safe) BEFORE the read, so a caller
223
+ // draining page N while acking page N-1 reads page N — and an ack never
224
+ // hides an event that was in the same response.
225
+ await flair.request("POST", request.ackPath, { position: request.ackPosition });
226
+ }
227
+ const page = await flair.request("GET", request.getPath);
228
+ const { text, structuredContent } = summarizeCatchup(page, request.ackPosition);
229
+ return { content: [{ type: "text", text }], structuredContent };
230
+ }
231
+ catch (err) {
232
+ return errorResult(err, flair.url);
233
+ }
234
+ };
235
+ const record_usage = async ({ memoryId, memoryIds, attribution }, { flair, heartbeat }) => {
236
+ heartbeat();
237
+ try {
238
+ const body = buildRecordUsageBody({ memoryId, memoryIds, attribution });
239
+ if (!body) {
240
+ return {
241
+ content: [{ type: "text", text: "record_usage requires memoryId or memoryIds." }],
242
+ isError: true,
243
+ };
244
+ }
245
+ const result = await flair.request("POST", "/RecordUsage", body);
246
+ const text = result?.recorded === true ? "Usage recorded." : "Usage request accepted.";
247
+ return {
248
+ content: [{ type: "text", text }],
249
+ structuredContent: { recorded: result?.recorded === true },
250
+ };
251
+ }
252
+ catch (err) {
253
+ return errorResult(err, flair.url);
254
+ }
255
+ };
256
+ const skill_store = async ({ content, trigger, name, description, tags }, { flair, heartbeat }) => {
257
+ heartbeat();
258
+ try {
259
+ const { id, body } = buildSkillStoreBody({
260
+ agentId: flair.agentId,
261
+ content,
262
+ trigger,
263
+ name,
264
+ description,
265
+ tags,
266
+ claimedClient: flair.claimedClient,
267
+ });
268
+ const result = await flair.request("PUT", `/Memory/${id}`, body);
269
+ const writtenId = typeof result?.id === "string" && result.id.length > 0 ? result.id : id;
270
+ const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
271
+ const lines = [
272
+ `Skill stored (id: ${writtenId})`,
273
+ `Preview: ${preview}`,
274
+ name ? `Name: ${name}` : undefined,
275
+ trigger ? `Trigger: ${trigger}` : undefined,
276
+ ].filter((line) => line != null);
277
+ return {
278
+ content: [{ type: "text", text: lines.join("\n") }],
279
+ structuredContent: { id: writtenId, written: true },
280
+ };
281
+ }
282
+ catch (err) {
283
+ return errorResult(err, flair.url);
284
+ }
285
+ };
286
+ const skill_search = async ({ task, limit }, { flair, heartbeat }) => {
287
+ heartbeat();
288
+ try {
289
+ const raw = await flair.request("POST", "/SemanticSearch", buildSkillSearchBody({ task, limit: limit ?? 5 }));
290
+ const projected = projectSkillSearchResponse(raw);
291
+ if (!projected || typeof projected !== "object" || !Array.isArray(projected.results)) {
292
+ return { content: [{ type: "text", text: "No matching skills found." }] };
293
+ }
294
+ const results = projected.results;
295
+ return {
296
+ content: [{ type: "text", text: formatSkillCatalog(results) }],
297
+ structuredContent: { results },
298
+ };
299
+ }
300
+ catch (err) {
301
+ return errorResult(err, flair.url);
302
+ }
303
+ };
304
+ const skill_get = async ({ id }, { flair, heartbeat }) => {
305
+ heartbeat();
306
+ try {
307
+ const mem = await flair.memory.get(id);
308
+ if (!mem || !isSkillRecord(mem)) {
309
+ return { content: [{ type: "text", text: `Skill ${id} not found.` }] };
310
+ }
311
+ const record = stripInternalMemoryFields(mem);
312
+ const trigger = typeof record.trigger === "string" && record.trigger.length > 0 ? record.trigger : "";
313
+ const text = [
314
+ record.content,
315
+ "",
316
+ `(id: ${record.id}${trigger ? `, trigger: ${trigger}` : ""}, tags: ${Array.isArray(record.tags) ? record.tags.join(", ") : "skill"}, created: ${record.createdAt ?? ""})`,
317
+ ].join("\n");
318
+ return {
319
+ content: [{ type: "text", text }],
320
+ structuredContent: record,
321
+ };
322
+ }
323
+ catch (err) {
324
+ return errorResult(err, flair.url);
325
+ }
326
+ };
327
+ /** FlairClient bindings keyed by descriptor name — the adapter-side impl map. */
328
+ export const STDIO_TOOL_HANDLERS = {
329
+ memory_search,
330
+ memory_store,
331
+ memory_update,
332
+ memory_get,
333
+ memory_delete,
334
+ relationship_store,
335
+ bootstrap,
336
+ soul_set,
337
+ soul_get,
338
+ flair_workspace_set,
339
+ flair_orgevent,
340
+ flair_catchup,
341
+ record_usage,
342
+ skill_store,
343
+ skill_search,
344
+ skill_get,
345
+ };
346
+ export function stdioHandlerNames() {
347
+ return Object.keys(STDIO_TOOL_HANDLERS).sort();
348
+ }
349
+ /**
350
+ * Register every stdio descriptor on the MCP server. The advertised set is
351
+ * STDIO_TOOL_DESCRIPTORS — not a hand-written per-tool literal list.
352
+ */
353
+ export function registerStdioTools(server, ctx) {
354
+ const registered = [];
355
+ const missing = [];
356
+ for (const d of STDIO_TOOL_DESCRIPTORS) {
357
+ const handler = STDIO_TOOL_HANDLERS[d.name];
358
+ if (!handler) {
359
+ missing.push(d.name);
360
+ continue;
361
+ }
362
+ const def = toStdioMcpToolDef(d);
363
+ const shape = jsonSchemaToZodShape(def.inputSchema);
364
+ const cb = async (args) => handler(args, ctx);
365
+ if (d.annotations) {
366
+ server.tool(d.name, def.description, shape, d.annotations, cb);
367
+ }
368
+ else {
369
+ server.tool(d.name, def.description, shape, cb);
370
+ }
371
+ registered.push(d.name);
372
+ }
373
+ if (missing.length > 0) {
374
+ throw new Error(`stdio adapter missing FlairClient bindings for descriptors: ${missing.join(", ")}`);
375
+ }
376
+ const extra = Object.keys(STDIO_TOOL_HANDLERS).filter((n) => !registered.includes(n)).sort();
377
+ if (extra.length > 0) {
378
+ throw new Error(`stdio adapter bindings have no stdio descriptor: ${extra.join(", ")}`);
379
+ }
380
+ return registered;
381
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * catchup.ts — pure helpers for the flair_catchup stdio binding (flair#1583).
3
+ *
4
+ * Owner-scope is enforced BY CONSTRUCTION: the participantId in the request
5
+ * path is always the caller's own agentId (from `FLAIR_AGENT_ID` / the signed
6
+ * identity), never a tool argument. The descriptor advertises no `agentId` /
7
+ * `participantId` property, so there is nothing to name another agent's feed
8
+ * with — and the server independently refuses a cross-agent read (403). This
9
+ * module is HTTP-/Harper-free (plain helpers + types) so the flair-mcp
10
+ * package stays FlairClient-only.
11
+ */
12
+ /**
13
+ * `GET /OrgEventCatchup/{participantId}` response shape
14
+ * (resources/OrgEventCatchup.ts). `position` is stamped onto every event
15
+ * (see org-event-catchup-lib.ts `withEventPosition`), and is what a caller
16
+ * passes back as `ack` once it has processed the event.
17
+ */
18
+ export interface CatchupPage {
19
+ events?: Array<Record<string, unknown>> | null;
20
+ /** Resolved exclusive cursor this page was read after. */
21
+ after?: string | null;
22
+ /** Cursor to continue a drain — the last event's position (or `after` when the page is empty). */
23
+ nextAfter?: string | null;
24
+ /** Durable watermark at read time (null when the caller has none yet). */
25
+ watermark?: string | null;
26
+ hasMore?: boolean;
27
+ pageSize?: number;
28
+ }
29
+ export interface CatchupArgs {
30
+ after?: unknown;
31
+ limit?: unknown;
32
+ ack?: unknown;
33
+ }
34
+ export interface CatchupRequest {
35
+ /** Owner-scoped base path — the caller's own participantId. */
36
+ path: string;
37
+ /** GET path (base + optional query). */
38
+ getPath: string;
39
+ /** POST path for the ack (same owner-scoped base). */
40
+ ackPath: string;
41
+ /** Non-empty ack position, or null when the caller did not ack. */
42
+ ackPosition: string | null;
43
+ }
44
+ /**
45
+ * Build the owner-scoped catchup request from tool args. `participantId` is
46
+ * ALWAYS `agentId` — the args cannot redirect it.
47
+ */
48
+ export declare function buildCatchupRequest(agentId: string, args: CatchupArgs): CatchupRequest;
49
+ export interface CatchupSummary {
50
+ text: string;
51
+ structuredContent: Record<string, unknown>;
52
+ }
53
+ /**
54
+ * Project a catchup page into the caller-facing text summary plus the
55
+ * structured echo (the machine-readable payload). Never throws on a
56
+ * malformed/absent page — it degrades to "no new events".
57
+ */
58
+ export declare function summarizeCatchup(page: CatchupPage | undefined, acked: string | null): CatchupSummary;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * catchup.ts — pure helpers for the flair_catchup stdio binding (flair#1583).
3
+ *
4
+ * Owner-scope is enforced BY CONSTRUCTION: the participantId in the request
5
+ * path is always the caller's own agentId (from `FLAIR_AGENT_ID` / the signed
6
+ * identity), never a tool argument. The descriptor advertises no `agentId` /
7
+ * `participantId` property, so there is nothing to name another agent's feed
8
+ * with — and the server independently refuses a cross-agent read (403). This
9
+ * module is HTTP-/Harper-free (plain helpers + types) so the flair-mcp
10
+ * package stays FlairClient-only.
11
+ */
12
+ function nonEmptyString(value) {
13
+ return typeof value === "string" && value.length > 0 ? value : null;
14
+ }
15
+ /**
16
+ * Build the owner-scoped catchup request from tool args. `participantId` is
17
+ * ALWAYS `agentId` — the args cannot redirect it.
18
+ */
19
+ export function buildCatchupRequest(agentId, args) {
20
+ const path = `/OrgEventCatchup/${encodeURIComponent(agentId)}`;
21
+ const params = new URLSearchParams();
22
+ const after = nonEmptyString(args.after);
23
+ if (after)
24
+ params.set("after", after);
25
+ if (typeof args.limit === "number" && Number.isFinite(args.limit)) {
26
+ params.set("limit", String(Math.trunc(args.limit)));
27
+ }
28
+ const query = params.toString();
29
+ return {
30
+ path,
31
+ getPath: query ? `${path}?${query}` : path,
32
+ ackPath: path,
33
+ ackPosition: nonEmptyString(args.ack),
34
+ };
35
+ }
36
+ /**
37
+ * Project a catchup page into the caller-facing text summary plus the
38
+ * structured echo (the machine-readable payload). Never throws on a
39
+ * malformed/absent page — it degrades to "no new events".
40
+ */
41
+ export function summarizeCatchup(page, acked) {
42
+ const events = Array.isArray(page?.events) ? page?.events : [];
43
+ const after = page?.after ?? null;
44
+ const nextAfter = page?.nextAfter ?? after;
45
+ const watermark = page?.watermark ?? null;
46
+ const hasMore = page?.hasMore === true;
47
+ const pageSize = page?.pageSize;
48
+ const header = events.length === 0
49
+ ? `Catchup: no new events after ${after ?? "(your watermark)"}.`
50
+ : `Catchup: ${events.length} event(s) after ${after ?? "(your watermark)"}${hasMore ? " (more available)" : ""}.`;
51
+ const lines = events.map((event, index) => {
52
+ const kind = typeof event.kind === "string" ? event.kind : "?";
53
+ const summary = typeof event.summary === "string" ? event.summary : "";
54
+ const id = typeof event.id === "string" ? `id:${event.id}` : "";
55
+ const position = typeof event.position === "string" ? `position:${event.position}` : "";
56
+ const targets = Array.isArray(event.targetIds) && event.targetIds.length > 0
57
+ ? `targets:${event.targetIds.join(",")}`
58
+ : "";
59
+ const meta = [id, position, targets].filter(Boolean).join(", ");
60
+ return `${index + 1}. [${kind}] ${summary}${meta ? ` (${meta})` : ""}`;
61
+ });
62
+ const cursorLines = [];
63
+ if (nextAfter)
64
+ cursorLines.push(`nextAfter: ${nextAfter}`);
65
+ if (acked)
66
+ cursorLines.push(`acked: ${acked}`);
67
+ if (nextAfter) {
68
+ cursorLines.push(hasMore
69
+ ? `More available — page again with after="${nextAfter}", then ack="${nextAfter}" once drained.`
70
+ : `Ack with ack="${nextAfter}" once you have processed these events to advance your watermark.`);
71
+ }
72
+ const body = [header, ...lines, ...(cursorLines.length > 0 ? ["", ...cursorLines] : [])].join("\n");
73
+ const structuredContent = {
74
+ events,
75
+ after,
76
+ nextAfter,
77
+ watermark,
78
+ hasMore,
79
+ };
80
+ if (typeof pageSize === "number")
81
+ structuredContent.pageSize = pageSize;
82
+ if (acked)
83
+ structuredContent.acked = acked;
84
+ return { text: body, structuredContent };
85
+ }
@@ -0,0 +1 @@
1
+ export declare function classifyError(err: unknown, flairUrl: string): string;
package/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ import { FlairError, formatKeyLookup, inspectKeyLookup } from "@tpsdev-ai/flair-client";
2
+ import { readEnvOrUnset } from "./env-guard.js";
3
+ export function classifyError(err, flairUrl) {
4
+ if (err instanceof FlairError) {
5
+ const { status, body } = err;
6
+ if (status === 400)
7
+ return `validation_error: ${body}`;
8
+ if (status === 401 || status === 403) {
9
+ // flair#1271: name the agent, the paths that were looked in, and the
10
+ // remedy. A cached-miss / wrong-HOME 401 is not a daemon-restart hint.
11
+ const lookup = err.keyLookup ?? {
12
+ ...inspectKeyLookup(readEnvOrUnset("FLAIR_AGENT_ID") ?? "", readEnvOrUnset("FLAIR_KEY_PATH")),
13
+ signed: false,
14
+ authMethod: "none",
15
+ };
16
+ return `auth_error: ${body}\n${formatKeyLookup(lookup)}`;
17
+ }
18
+ if (status === 413)
19
+ return `payload_too_large: ${body}`;
20
+ if (status === 429)
21
+ return "rate_limited — retry after a moment";
22
+ if (status >= 500)
23
+ return `server_error (retriable): ${body}`;
24
+ return `http_error (${status}): ${body}`;
25
+ }
26
+ if (err instanceof Error) {
27
+ if (err.name.includes("Abort") || err.name.includes("Timeout")) {
28
+ return "timeout — the server took too long. This often happens with large content that requires embedding. Try shorter content or retry.";
29
+ }
30
+ if (err instanceof TypeError && err.message.includes("fetch")) {
31
+ return `connection_error (retriable): could not reach Flair at ${flairUrl}. Is it running?\n` +
32
+ `(Diagnostics:\n` +
33
+ ` - 'curl ${flairUrl}/Health' — if this responds 200 or 401, daemon is up + this is an auth issue not a connection one.\n` +
34
+ ` - 'launchctl list | grep flair' (macOS) or 'systemctl status flair' (Linux).)`;
35
+ }
36
+ return `unexpected_error: ${err.message}`;
37
+ }
38
+ return `unexpected_error: ${String(err)}`;
39
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Flair MCP Server — persistent memory for Claude Code and any MCP client.
4
4
  *
5
- * Tools:
5
+ * Tools (derived from @tpsdev-ai/flair-tool-descriptors — flair#1580):
6
6
  * - memory_search — semantic search across memories
7
7
  * - memory_store — save a memory with type + durability
8
8
  * - memory_update — update an existing memory by ID (dedup-bypassed)
@@ -14,6 +14,7 @@
14
14
  * - soul_get — get a personality/context entry
15
15
  * - flair_workspace_set — write own WorkspaceState (Office Space coordination)
16
16
  * - flair_orgevent — publish an OrgEvent attributed to self (no forging)
17
+ * - flair_catchup — drain + ack self's own OrgEventCatchup feed (owner-scoped)
17
18
  * - record_usage — report that recalled memories were actually used (flair#1147)
18
19
  * - skill_store — write a skill-tagged memory (trigger + procedure)
19
20
  * - skill_search — catalog skills that apply to a task (not the procedure)
@@ -42,5 +43,5 @@
42
43
  * — the silent `npx -y @tpsdev-ai/flair-mcp` failure. The shim checks the Node
43
44
  * version FIRST, then dynamically imports this module and calls runMcp().
44
45
  */
45
- export declare function classifyError(err: unknown, flairUrl: string): string;
46
+ export { classifyError } from "./errors.js";
46
47
  export declare function runMcp(): Promise<void>;