@tpsdev-ai/flair-mcp 0.56.0 → 0.58.0

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,28 @@
1
+ /**
2
+ * record-id-path.ts — the one rule for a record id used as the single dynamic
3
+ * path segment of a `/Memory/<id>` request built inside flair-mcp (flair#1970).
4
+ *
5
+ * Self-contained on purpose: this module must load and typecheck WITHOUT
6
+ * @tpsdev-ai/flair-client's built dist present (the root `bun test` and strict
7
+ * test-suite typecheck lanes run before the client is built), so it does not
8
+ * import the client's own `encodeRecordId` — it states the same rule here.
9
+ */
10
+ /**
11
+ * Percent-encode an id so it reaches the server as ONE path segment that
12
+ * decodes back to the id, addressing exactly that record. REFUSES an id that is
13
+ * exactly `.` or `..`: percent-encoding leaves those unchanged and URL
14
+ * normalization collapses `/Memory/.` to `/Memory/` and `/Memory/..` to `/`, so
15
+ * the sent path would not be the id (nor the signed path). Such an id cannot
16
+ * address its record, so it is an error, not a request.
17
+ */
18
+ export function encodeRecordId(id) {
19
+ if (id === "." || id === "..") {
20
+ throw new Error(`record id ${JSON.stringify(id)} is a URL path dot-segment ("." or ".."); ` +
21
+ `it cannot be addressed as one path segment of /Memory/<id>. Use a different id.`);
22
+ }
23
+ return encodeURIComponent(id);
24
+ }
25
+ /** The PUT path for a row: its id as ONE percent-encoded path segment. */
26
+ export function memoryPutPath(id) {
27
+ return `/Memory/${encodeRecordId(id)}`;
28
+ }
@@ -17,11 +17,14 @@
17
17
  *
18
18
  * NO-OP-ON-ANY-FAILURE GUARANTEE
19
19
  * ------------------------------
20
- * This hook can never block or break Claude Code startup. Every failure mode —
21
- * missing FLAIR_AGENT_ID, malformed stdin, Flair unreachable, auth error, a
22
- * hung daemon, an unexpected throw — degrades to printing `{}` (an empty,
23
- * inert hook output) and exiting 0. It never throws, never writes to stderr in
24
- * a way that surfaces to the user, and never exits non-zero.
20
+ * This hook can never block or break Claude Code startup. Every failure mode
21
+ * (missing FLAIR_AGENT_ID, malformed stdin, Flair unreachable, auth error, a
22
+ * hung daemon, an unexpected throw) exits 0. Malformed stdin is treated as
23
+ * empty input and can still yield bootstrap context. A failed bootstrap yields
24
+ * a pre-compaction record and/or a continuity resume hint only when their
25
+ * separate lookups find one; otherwise stdout is `{}`. The hook attempts one
26
+ * stderr diagnostic when bootstrap fails (flair#1943). The Codex command retains stderr and the
27
+ * Claude Code command discards it; delivery depends on stderr being writable.
25
28
  *
26
29
  * A hard timeout (FLAIR_HOOK_TIMEOUT_MS, default 8s) wraps the bootstrap call
27
30
  * so a stalled Flair daemon can't hang session startup; on timeout we no-op.
@@ -96,10 +99,30 @@ interface BootstrapClient extends Partial<PresencePoster> {
96
99
  * sharing it; re-exported here unchanged so existing importers keep working.
97
100
  */
98
101
  export { isProbeMode };
102
+ /** The hook's OWN bootstrap-timer rejection (flair#1943). A dedicated class so
103
+ * the classifier recognises its own timeout by IDENTITY, never by reading a
104
+ * message. */
105
+ export declare class BootstrapTimeoutError extends Error {
106
+ constructor();
107
+ }
108
+ export type BootstrapFailureKind = "auth" | "timeout" | "unreachable" | `http-${number}`;
109
+ /**
110
+ * flair#1943 — classify a bootstrap failure for the one stderr line. Reads a
111
+ * numeric HTTP status FIRST (`status`, what FlairError carries, then
112
+ * `status_code`, then `statusCode`); when a status exists the message is never
113
+ * consulted. With no status, the ONLY timeout is the hook's own bootstrap
114
+ * timer (a BootstrapTimeoutError) or an error whose name is exactly
115
+ * `TimeoutError`; everything else is `unreachable`. No kind is ever decided
116
+ * from message text. Never reads or includes credentials.
117
+ */
118
+ export declare function classifyBootstrapFailure(err: unknown): BootstrapFailureKind;
99
119
  /**
100
120
  * Core hook logic, with injectable dependencies so it can be unit-tested
101
121
  * without a live Flair daemon. Returns the exact string to print to stdout.
102
- * NEVER throws — every failure path returns NOOP_OUTPUT.
122
+ * A failed bootstrap can still return a pre-compaction record and a
123
+ * continuity resume hint. Without bootstrap context, a pre-compaction record
124
+ * or a resume hint, this returns NOOP_OUTPUT. The entry
125
+ * point catches unexpected exceptions.
103
126
  *
104
127
  * @param rawInput the raw stdin string (may be empty / malformed)
105
128
  * @param makeClient factory for the bootstrap client (defaults to FlairClient)
@@ -17,11 +17,14 @@
17
17
  *
18
18
  * NO-OP-ON-ANY-FAILURE GUARANTEE
19
19
  * ------------------------------
20
- * This hook can never block or break Claude Code startup. Every failure mode —
21
- * missing FLAIR_AGENT_ID, malformed stdin, Flair unreachable, auth error, a
22
- * hung daemon, an unexpected throw — degrades to printing `{}` (an empty,
23
- * inert hook output) and exiting 0. It never throws, never writes to stderr in
24
- * a way that surfaces to the user, and never exits non-zero.
20
+ * This hook can never block or break Claude Code startup. Every failure mode
21
+ * (missing FLAIR_AGENT_ID, malformed stdin, Flair unreachable, auth error, a
22
+ * hung daemon, an unexpected throw) exits 0. Malformed stdin is treated as
23
+ * empty input and can still yield bootstrap context. A failed bootstrap yields
24
+ * a pre-compaction record and/or a continuity resume hint only when their
25
+ * separate lookups find one; otherwise stdout is `{}`. The hook attempts one
26
+ * stderr diagnostic when bootstrap fails (flair#1943). The Codex command retains stderr and the
27
+ * Claude Code command discards it; delivery depends on stderr being writable.
25
28
  *
26
29
  * A hard timeout (FLAIR_HOOK_TIMEOUT_MS, default 8s) wraps the bootstrap call
27
30
  * so a stalled Flair daemon can't hang session startup; on timeout we no-op.
@@ -78,6 +81,7 @@ import { basename } from "node:path";
78
81
  import { deriveActivity, postPresenceSafe, resolvePresenceTimeoutMs } from "./presence.js";
79
82
  import { isProbeMode, readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
80
83
  import { buildResumeHint, discoverResume, prepareContinuityBoot, resolveContinuityTimeoutMs, } from "./continuity.js";
84
+ import { fetchPreCompactRecord, formatPreCompactContext, resolvePreCompactLookup } from "./precompact.js";
81
85
  /** Claude Code SessionStart additionalContext hard limit (chars). */
82
86
  const MAX_CHARS = 10_000;
83
87
  /** Token budget for the bootstrap call — matches the proven prototype. */
@@ -120,10 +124,19 @@ function readStdin() {
120
124
  setTimeout(() => resolve(data), 200).unref?.();
121
125
  });
122
126
  }
123
- /** Race a promise against a timeout. Rejects with a timeout error if exceeded. */
127
+ /** The hook's OWN bootstrap-timer rejection (flair#1943). A dedicated class so
128
+ * the classifier recognises its own timeout by IDENTITY, never by reading a
129
+ * message. */
130
+ export class BootstrapTimeoutError extends Error {
131
+ constructor() {
132
+ super("bootstrap timeout");
133
+ this.name = "BootstrapTimeoutError";
134
+ }
135
+ }
136
+ /** Race a promise against a timeout. Rejects with a BootstrapTimeoutError if exceeded. */
124
137
  function withTimeout(promise, ms) {
125
138
  return new Promise((resolve, reject) => {
126
- const timer = setTimeout(() => reject(new Error("bootstrap_timeout")), ms);
139
+ const timer = setTimeout(() => reject(new BootstrapTimeoutError()), ms);
127
140
  timer.unref?.();
128
141
  promise.then((value) => {
129
142
  clearTimeout(timer);
@@ -134,6 +147,60 @@ function withTimeout(promise, ms) {
134
147
  });
135
148
  });
136
149
  }
150
+ /**
151
+ * flair#1943 — classify a bootstrap failure for the one stderr line. Reads a
152
+ * numeric HTTP status FIRST (`status`, what FlairError carries, then
153
+ * `status_code`, then `statusCode`); when a status exists the message is never
154
+ * consulted. With no status, the ONLY timeout is the hook's own bootstrap
155
+ * timer (a BootstrapTimeoutError) or an error whose name is exactly
156
+ * `TimeoutError`; everything else is `unreachable`. No kind is ever decided
157
+ * from message text. Never reads or includes credentials.
158
+ */
159
+ export function classifyBootstrapFailure(err) {
160
+ const e = err;
161
+ const status = numericStatus(e);
162
+ if (status !== undefined)
163
+ return status === 401 || status === 403 ? "auth" : `http-${status}`;
164
+ if (err instanceof BootstrapTimeoutError)
165
+ return "timeout";
166
+ if (typeof e?.name === "string" && e.name === "TimeoutError")
167
+ return "timeout";
168
+ return "unreachable";
169
+ }
170
+ /** The first NUMERIC HTTP status the error carries, checked `status` →
171
+ * `status_code` → `statusCode` (flair#1943). */
172
+ function numericStatus(e) {
173
+ for (const key of ["status", "status_code", "statusCode"]) {
174
+ const v = e?.[key];
175
+ if (typeof v === "number" && Number.isFinite(v))
176
+ return v;
177
+ }
178
+ return undefined;
179
+ }
180
+ // flair#1943: one no-op 'error' listener per process, so repeated failed
181
+ // runs in one process never add listeners (and never trigger Node's
182
+ // max-listeners warning on stderr).
183
+ let stderrErrorAbsorbed = false;
184
+ /** The stderr diagnostic for a failed bootstrap. NAMES the actor, the state and
185
+ * the remedy; never contains a key, token, password or Authorization value. */
186
+ function reportBootstrapFailure(err) {
187
+ const kind = classifyBootstrapFailure(err);
188
+ const line = `flair session-start: bootstrap failed (${kind}); this session starts without bootstrap context. Next: run \`flair doctor\`, and check FLAIR_URL and this agent's key.\n`;
189
+ try {
190
+ // Best-effort (flair#1943): a failed stderr write (a closed pipe → EPIPE)
191
+ // must not change stdout or the exit code. The write may throw
192
+ // SYNCHRONOUSLY or surface later as an 'error' event on the stream; absorb
193
+ // both, so the hook still prints its payload and exits 0.
194
+ if (!stderrErrorAbsorbed) {
195
+ process.stderr.on("error", () => { });
196
+ stderrErrorAbsorbed = true;
197
+ }
198
+ process.stderr.write(line);
199
+ }
200
+ catch {
201
+ // ignore — the diagnostic is best-effort
202
+ }
203
+ }
137
204
  /** Build the SessionStart hook output JSON from a context string. */
138
205
  function hookOutput(context) {
139
206
  return JSON.stringify({
@@ -146,7 +213,10 @@ function hookOutput(context) {
146
213
  /**
147
214
  * Core hook logic, with injectable dependencies so it can be unit-tested
148
215
  * without a live Flair daemon. Returns the exact string to print to stdout.
149
- * NEVER throws — every failure path returns NOOP_OUTPUT.
216
+ * A failed bootstrap can still return a pre-compaction record and a
217
+ * continuity resume hint. Without bootstrap context, a pre-compaction record
218
+ * or a resume hint, this returns NOOP_OUTPUT. The entry
219
+ * point catches unexpected exceptions.
150
220
  *
151
221
  * @param rawInput the raw stdin string (may be empty / malformed)
152
222
  * @param makeClient factory for the bootstrap client (defaults to FlairClient)
@@ -198,6 +268,18 @@ export async function runHook(rawInput, makeClient = defaultClientFactory) {
198
268
  const resumeHintDone = continuity.active && typeof client.request === "function"
199
269
  ? withTimeout(discoverResume(client, agentId, continuity.priorPointer).then((result) => buildResumeHint(result)), resolveContinuityTimeoutMs()).catch(() => null)
200
270
  : Promise.resolve(null);
271
+ // Pre-compaction record (flair#2069): decided locally, fetched concurrently,
272
+ // the marker read and the fetch both bounded by the same continuity timeout;
273
+ // null (nothing shown) on any failure.
274
+ const precompactDone = typeof client.request === "function"
275
+ ? withTimeout((async () => {
276
+ const lookup = await resolvePreCompactLookup(input, agentId, continuity);
277
+ if (!lookup)
278
+ return null;
279
+ const record = await fetchPreCompactRecord(client, agentId, lookup);
280
+ return record ? formatPreCompactContext(record) : null;
281
+ })(), resolveContinuityTimeoutMs()).catch(() => null)
282
+ : Promise.resolve(null);
201
283
  let context = "";
202
284
  try {
203
285
  const res = await withTimeout(Promise.resolve(client.bootstrap({
@@ -207,14 +289,25 @@ export async function runHook(rawInput, makeClient = defaultClientFactory) {
207
289
  })), resolveTimeoutMs());
208
290
  context = res && res.context ? String(res.context) : "";
209
291
  }
210
- catch {
292
+ catch (err) {
211
293
  context = ""; // flair unreachable / auth error / timeout → no bootstrap context
294
+ // flair#1943: keeping stderr open cannot reveal an error never written to
295
+ // it. Write ONE line to STDERR (never stdout — that is the hook payload),
296
+ // so a real failure stays visible instead of being swallowed. A failed
297
+ // bootstrap contributes no bootstrap context; a continuity resume hint may
298
+ // still be returned. The entry point preserves a successful exit.
299
+ reportBootstrapFailure(err);
212
300
  }
213
301
  const resumeHint = await resumeHintDone;
302
+ const precompactBlock = await precompactDone;
214
303
  await presenceDone;
215
- // Combine: bootstrap context first, then AT MOST one continuity hint line.
216
- // Either piece may be absent; both absent ⇒ the inert no-op output.
304
+ // Combine: the pre-compaction record FIRST (bounded, so the MAX_CHARS cut
305
+ // below can only shorten what follows it), then the bootstrap context, then
306
+ // AT MOST one continuity hint line. Any piece may be absent; all absent ⇒
307
+ // the inert no-op output.
217
308
  const pieces = [];
309
+ if (precompactBlock)
310
+ pieces.push(precompactBlock);
218
311
  if (context.trim())
219
312
  pieces.push(context);
220
313
  if (resumeHint)
@@ -51,7 +51,7 @@ export function descriptorNames(descriptors) {
51
51
  export const TOOL_DESCRIPTORS = [
52
52
  {
53
53
  "name": "memory_search",
54
- "description": "Search memories by meaning. Understands temporal queries like 'what happened today'. Scoped to your agent's own + granted memories.",
54
+ "description": "Search memories by meaning. Understands temporal queries like 'what happened today'. Non-admin callers are scoped to their own and other agents' non-private memories; administrator requests may have broader access.",
55
55
  "inputSchema": {
56
56
  "type": "object",
57
57
  "properties": {
@@ -80,7 +80,7 @@ export const TOOL_DESCRIPTORS = [
80
80
  "query"
81
81
  ]
82
82
  },
83
- "outputShape": "{ results: MemoryRecord[] } — semantic hits scoped to the caller's own + granted memories; each hit carries content, never the raw embedding.",
83
+ "outputShape": "{ results: MemoryRecord[] } — semantic hits subject to the caller's read scope; each hit carries content, never the raw embedding.",
84
84
  "annotations": {
85
85
  "readOnlyHint": true
86
86
  },
@@ -135,7 +135,7 @@ export const TOOL_DESCRIPTORS = [
135
135
  "private",
136
136
  "shared"
137
137
  ],
138
- "description": "Writer-controlled sharing intent. Omit to use the server's durability-keyed default: permanent/persistent -> shared, standard/ephemeral -> private. private — owner-only, never visible to another agent, even one holding a memory grant. shared — visible to the owner and every other agent on this instance. The visibility the write actually landed on is returned in the result."
138
+ "description": "Writer-controlled sharing intent. Omit to use the server's durability-keyed default: permanent/persistent -> shared, standard/ephemeral -> private. private — readable by its owner and administrators; other non-admin agents cannot read it, including through a memory grant. shared — visible to the owner and every other agent on this instance. The visibility the write actually landed on is returned in the result."
139
139
  },
140
140
  "usedMemoryIds": {
141
141
  "type": "array",
@@ -189,7 +189,7 @@ export const TOOL_DESCRIPTORS = [
189
189
  },
190
190
  {
191
191
  "name": "skill_search",
192
- "description": "Find skills (reusable capabilities/procedures) that apply to a task. Ranks skill-tagged memories by their `trigger` ('when to use') against your task text. Returns a lightweight CATALOG — id, name, trigger, description, tags, agentId — NOT the full procedure (fetch that with skill_get). Scoped to your own + shared skills; another agent's private skill is never returned.",
192
+ "description": "Find skills (reusable capabilities/procedures) that apply to a task. Ranks skill-tagged memories by their `trigger` ('when to use') against your task text. Returns a lightweight CATALOG — id, name, trigger, description, tags, agentId — NOT the full procedure (fetch that with skill_get). Non-admin callers can retrieve their own and other agents' non-private skills; administrators can also retrieve private skills.",
193
193
  "inputSchema": {
194
194
  "type": "object",
195
195
  "properties": {
@@ -206,14 +206,14 @@ export const TOOL_DESCRIPTORS = [
206
206
  "task"
207
207
  ]
208
208
  },
209
- "outputShape": "{ results: SkillCard[] } — the skill catalog (lightweight id/name/trigger/description/tags/agentId, ranked by trigger match); the full procedure and the raw embedding are never on a card. Scoped to the caller's own + non-private skills; another agent's private skill is never returned.",
209
+ "outputShape": "{ results: SkillCard[] } — the skill catalog (lightweight id/name/trigger/description/tags/agentId, ranked by trigger match); the full procedure and the raw embedding are never on a card. Non-admin callers can retrieve their own and other agents' non-private skills; administrators can also retrieve private skills.",
210
210
  "annotations": {
211
211
  "readOnlyHint": true
212
212
  }
213
213
  },
214
214
  {
215
215
  "name": "skill_get",
216
- "description": "Retrieve a full skill by ID — the complete procedure (`content`) plus trigger and metadata. The disclosure step after skill_search's catalog. Read-scoped: you can only get your own or a shared skill, never another agent's private skill. A non-skill id returns not-found. The raw embedding vector is never returned.",
216
+ "description": "Retrieve a full skill by ID — the complete procedure (`content`) plus trigger and metadata. The disclosure step after skill_search's catalog. Reads follow the caller's authorization: non-admin callers can retrieve their own and other agents' non-private skills; administrators can also retrieve private skills. A non-skill id returns not-found. The raw embedding vector is never returned.",
217
217
  "inputSchema": {
218
218
  "type": "object",
219
219
  "properties": {
@@ -226,7 +226,7 @@ export const TOOL_DESCRIPTORS = [
226
226
  "id"
227
227
  ]
228
228
  },
229
- "outputShape": "The full skill record { id, agentId, content, trigger, tags, durability, metadata, createdAt, ... } for a skill readable under the caller's read-scope — embedding + embeddingModel always stripped. A non-owner cannot read another agent's private skill, and a readable non-skill id is not found (both 404).",
229
+ "outputShape": "The full skill record { id, agentId, content, trigger, tags, durability, metadata, createdAt, ... } for a skill readable under the caller's read-scope — embedding + embeddingModel always stripped. A non-admin caller cannot read another agent's private skill; administrators retain access. A readable non-skill id is reported as not found.",
230
230
  "annotations": {
231
231
  "readOnlyHint": true
232
232
  }
@@ -268,7 +268,7 @@ export const TOOL_DESCRIPTORS = [
268
268
  },
269
269
  {
270
270
  "name": "memory_basement",
271
- "description": "Send a memory to the basement (archive it). Sets archived=true and stamps archivedAt. The memory is removed from bootstrap and default search but remains retrievable via memory_get and memory_search(includeArchived:true). Deliberate and GLOBAL — this is a visibility flag, not a deletion: provenance and history are untouched. Scoped to your own memories only.",
271
+ "description": "Send a memory to the basement (archive it). Sets archived=true and stamps archivedAt. The memory is removed from bootstrap and default search but remains retrievable via memory_get and memory_search(includeArchived:true). Deliberate and GLOBAL — this is a visibility flag, not a deletion: provenance and history are untouched. Non-admin callers can modify only their own memories; administrators can also modify other agents' memories.",
272
272
  "inputSchema": {
273
273
  "type": "object",
274
274
  "properties": {
@@ -286,7 +286,7 @@ export const TOOL_DESCRIPTORS = [
286
286
  },
287
287
  {
288
288
  "name": "memory_restore",
289
- "description": "Restore a basemented (archived) memory. Clears archived and archivedAt. Deliberate and GLOBAL — this un-retires the memory for EVERY session, not a session-local view (per-session reuse is drawers, which do not exist yet). Scoped to your own memories only.",
289
+ "description": "Restore a basemented (archived) memory. Clears archived and archivedAt. Deliberate and GLOBAL — this un-retires the memory for EVERY session, not a session-local view (per-session reuse is drawers, which do not exist yet). Non-admin callers can modify only their own memories; administrators can also modify other agents' memories.",
290
290
  "inputSchema": {
291
291
  "type": "object",
292
292
  "properties": {
@@ -325,7 +325,7 @@ export const TOOL_DESCRIPTORS = [
325
325
  "id"
326
326
  ]
327
327
  },
328
- "outputShape": "The full memory record { id, agentId, content, durability, createdAt, ... } for the caller's own id — embedding + embeddingModel stripped by default.",
328
+ "outputShape": "The full memory record { id, agentId, content, durability, createdAt, ... } for the requested ID, subject to the caller's read scope; embedding and embeddingModel are stripped by default.",
329
329
  "annotations": {
330
330
  "readOnlyHint": true
331
331
  },
@@ -336,7 +336,7 @@ export const TOOL_DESCRIPTORS = [
336
336
  },
337
337
  {
338
338
  "name": "memory_delete",
339
- "description": "Delete a memory by ID. You can only delete your own memories.",
339
+ "description": "Delete a memory by ID. Non-admin callers can delete only their own memories; administrators can also delete other agents' memories.",
340
340
  "inputSchema": {
341
341
  "type": "object",
342
342
  "properties": {
@@ -349,7 +349,7 @@ export const TOOL_DESCRIPTORS = [
349
349
  "id"
350
350
  ]
351
351
  },
352
- "outputShape": "Deletes the caller's own memory at any durability tier (success echo is thin). Cross-owner deletion returns { error, status:403 } for a non-admin; a deleted row round-trips as gone via memory_get.",
352
+ "outputShape": "Deletes a memory by ID when authorized, at any durability tier (success echo is thin). Cross-owner deletion returns { error, status:403 } for a non-admin; a deleted row round-trips as gone via memory_get.",
353
353
  "annotations": {
354
354
  "destructiveHint": true
355
355
  }
@@ -490,7 +490,7 @@ export const TOOL_DESCRIPTORS = [
490
490
  ]
491
491
  },
492
492
  "outputShape": "Refuses runtime Soul writes, including admin-agent delegation, with { error, status:403 }. Operators use the authenticated REST or CLI path.",
493
- "stdioDescription": "Set a personality or project context entry. Included in every bootstrap."
493
+ "stdioDescription": "Set a personality or project context entry, included in every bootstrap. Soul writes require verified administrator Basic credentials; Ed25519 agent requests are refused. Operators should use the REST API or CLI."
494
494
  },
495
495
  {
496
496
  "name": "soul_get",
@@ -588,7 +588,7 @@ export const TOOL_DESCRIPTORS = [
588
588
  },
589
589
  {
590
590
  "name": "flair_catchup",
591
- "description": "Drain YOUR OWN catch-up feed — org events directed to you (or broadcast) after your durable watermark. Returns a page plus a `nextAfter` cursor: page with `after`, then advance the watermark with `ack`. Owner-scoped: the participant is your signed identity, so you can only ever read your own feed — there is no agentId parameter and any other feed is refused (403). At-least-once: an event may arrive twice (re-delivery is safe), an acked event does not re-deliver, and an un-acked event survives a restart.",
591
+ "description": "Drain the catch-up feed for the configured `FLAIR_AGENT_ID`: directed or broadcast org events after the effective cursor. Omit `after` to start at that agent's durable watermark, or pass `after` to choose an exclusive cursor. Pass `ack` to advance the watermark before this call reads a page; the response includes `nextAfter` for paging. The tool has no argument to change the agent id. Agent requests are signed for the configured id; verified administrator Basic credentials may read that configured feed. Unacknowledged events remain eligible after restart; acknowledged events are skipped by default, but an explicit older `after` can replay them.",
592
592
  "inputSchema": {
593
593
  "type": "object",
594
594
  "properties": {
@@ -606,7 +606,7 @@ export const TOOL_DESCRIPTORS = [
606
606
  }
607
607
  }
608
608
  },
609
- "outputShape": "{ events: OrgEvent[], after, nextAfter, watermark, hasMore, pageSize, acked? } — the caller's own directed + broadcast events after its durable watermark, paged; `ack` advances the watermark monotonically (at-least-once — re-delivery is safe).",
609
+ "outputShape": "{ events: OrgEvent[], after, nextAfter, watermark, hasMore, pageSize, acked? } — the configured agent's directed and broadcast events after the effective cursor; `ack` advances that agent's durable watermark monotonically.",
610
610
  "native": false
611
611
  },
612
612
  {
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-mcp",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "MCP server for Flair — persistent memory for Claude Code, Cursor, and any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "flair-mcp": "dist/mcp-shim.cjs",
9
+ "flair-precompact": "dist/precompact-hook.js",
9
10
  "flair-session-start": "dist/session-start-hook.js",
10
- "flair-continuity-capture": "dist/continuity-capture-hook.js"
11
+ "flair-continuity-capture": "dist/continuity-capture-hook.js",
12
+ "flair-prompt-recall": "dist/prompt-recall-hook.js"
11
13
  },
12
14
  "files": [
13
15
  "dist/",
@@ -19,7 +21,7 @@
19
21
  "prebuild": "node ../../scripts/vendor-tool-descriptors.mjs packages/flair-mcp/src/tool-descriptors",
20
22
  "test": "bun test",
21
23
  "prepack": "npm run build",
22
- "postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');for(const p of ['dist/mcp-shim.cjs','dist/index.js','dist/session-start-hook.js','dist/continuity-capture-hook.js']){try{if(statSync(p).isFile()){chmodSync(p,0o755);console.error('@tpsdev-ai/flair-mcp: chmod +x ' + p + ' OK')}}catch(e){if(e.code!=='ENOENT')console.error('postinstall warn:',e.message)}}}catch(e){console.error('postinstall warn:',e.message)}\""
24
+ "postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');for(const p of ['dist/mcp-shim.cjs','dist/index.js','dist/session-start-hook.js','dist/continuity-capture-hook.js','dist/prompt-recall-hook.js','dist/precompact-hook.js']){try{if(statSync(p).isFile()){chmodSync(p,0o755);console.error('@tpsdev-ai/flair-mcp: chmod +x ' + p + ' OK')}}catch(e){if(e.code!=='ENOENT')console.error('postinstall warn:',e.message)}}}catch(e){console.error('postinstall warn:',e.message)}\""
23
25
  },
24
26
  "publishConfig": {
25
27
  "access": "public"
@@ -29,7 +31,7 @@
29
31
  },
30
32
  "dependencies": {
31
33
  "@modelcontextprotocol/sdk": "1.27.1",
32
- "@tpsdev-ai/flair-client": "0.56.0",
34
+ "@tpsdev-ai/flair-client": "0.58.0",
33
35
  "zod": "4.3.6"
34
36
  },
35
37
  "license": "Apache-2.0",