@tpsdev-ai/flair-mcp 0.57.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.
@@ -21,9 +21,9 @@
21
21
  * (missing FLAIR_AGENT_ID, malformed stdin, Flair unreachable, auth error, a
22
22
  * hung daemon, an unexpected throw) exits 0. Malformed stdin is treated as
23
23
  * empty input and can still yield bootstrap context. A failed bootstrap yields
24
- * a continuity resume hint only when the separate lookup finds eligible prior
25
- * entries; otherwise stdout is `{}`. The hook attempts one stderr diagnostic
26
- * when bootstrap fails (flair#1943). The Codex command retains stderr and the
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
27
  * Claude Code command discards it; delivery depends on stderr being writable.
28
28
  *
29
29
  * A hard timeout (FLAIR_HOOK_TIMEOUT_MS, default 8s) wraps the bootstrap call
@@ -81,6 +81,7 @@ import { basename } from "node:path";
81
81
  import { deriveActivity, postPresenceSafe, resolvePresenceTimeoutMs } from "./presence.js";
82
82
  import { isProbeMode, readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
83
83
  import { buildResumeHint, discoverResume, prepareContinuityBoot, resolveContinuityTimeoutMs, } from "./continuity.js";
84
+ import { fetchPreCompactRecord, formatPreCompactContext, resolvePreCompactLookup } from "./precompact.js";
84
85
  /** Claude Code SessionStart additionalContext hard limit (chars). */
85
86
  const MAX_CHARS = 10_000;
86
87
  /** Token budget for the bootstrap call — matches the proven prototype. */
@@ -212,7 +213,10 @@ function hookOutput(context) {
212
213
  /**
213
214
  * Core hook logic, with injectable dependencies so it can be unit-tested
214
215
  * without a live Flair daemon. Returns the exact string to print to stdout.
215
- * 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.
216
220
  *
217
221
  * @param rawInput the raw stdin string (may be empty / malformed)
218
222
  * @param makeClient factory for the bootstrap client (defaults to FlairClient)
@@ -264,6 +268,18 @@ export async function runHook(rawInput, makeClient = defaultClientFactory) {
264
268
  const resumeHintDone = continuity.active && typeof client.request === "function"
265
269
  ? withTimeout(discoverResume(client, agentId, continuity.priorPointer).then((result) => buildResumeHint(result)), resolveContinuityTimeoutMs()).catch(() => null)
266
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);
267
283
  let context = "";
268
284
  try {
269
285
  const res = await withTimeout(Promise.resolve(client.bootstrap({
@@ -277,16 +293,21 @@ export async function runHook(rawInput, makeClient = defaultClientFactory) {
277
293
  context = ""; // flair unreachable / auth error / timeout → no bootstrap context
278
294
  // flair#1943: keeping stderr open cannot reveal an error never written to
279
295
  // it. Write ONE line to STDERR (never stdout — that is the hook payload),
280
- // so a real failure stays visible instead of being swallowed. stdout and
281
- // the exit code are unchanged (the no-op payload), so a failure never
282
- // blocks the session.
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.
283
299
  reportBootstrapFailure(err);
284
300
  }
285
301
  const resumeHint = await resumeHintDone;
302
+ const precompactBlock = await precompactDone;
286
303
  await presenceDone;
287
- // Combine: bootstrap context first, then AT MOST one continuity hint line.
288
- // 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.
289
308
  const pieces = [];
309
+ if (precompactBlock)
310
+ pieces.push(precompactBlock);
290
311
  if (context.trim())
291
312
  pieces.push(context);
292
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 and other agents' non-private 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 and other agents' non-private 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.57.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.57.0",
34
+ "@tpsdev-ai/flair-client": "0.58.0",
33
35
  "zod": "4.3.6"
34
36
  },
35
37
  "license": "Apache-2.0",