@tpsdev-ai/flair-mcp 0.10.1 → 0.12.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,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Flair SessionStart hook for Claude Code — auto-recall on session start.
4
+ *
5
+ * Claude Code fires `SessionStart` hooks when a session begins (startup,
6
+ * resume, clear, compact). A hook of `type: "command"` receives the hook
7
+ * payload as JSON on stdin and may print a JSON object whose
8
+ * `hookSpecificOutput.additionalContext` string is injected into the model's
9
+ * context for that session. This binary uses that channel to inject Flair's
10
+ * `bootstrap` context (soul + relevant memories + predicted context), so a
11
+ * fresh Claude Code session starts already warmed with the agent's memory —
12
+ * no manual "call the bootstrap tool" nudge required.
13
+ *
14
+ * It complements the MCP server (`flair-mcp`): the MCP server gives the agent
15
+ * pull tools (memory_search / memory_store / bootstrap on demand) and push
16
+ * recall, while this hook does a one-shot context push at session start.
17
+ *
18
+ * NO-OP-ON-ANY-FAILURE GUARANTEE
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.
25
+ *
26
+ * A hard timeout (FLAIR_HOOK_TIMEOUT_MS, default 8s) wraps the bootstrap call
27
+ * so a stalled Flair daemon can't hang session startup; on timeout we no-op.
28
+ *
29
+ * CONFIG (env, read identically to the MCP server)
30
+ * ------------------------------------------------
31
+ * FLAIR_AGENT_ID (required — absent → no-op) agent identity
32
+ * FLAIR_URL (default http://localhost:19926 via flair-client)
33
+ * FLAIR_KEY_PATH (default ~/.flair/keys/<agent>.key via flair-client)
34
+ * FLAIR_HOOK_TIMEOUT_MS (default 8000; clamped 500..30000)
35
+ *
36
+ * USAGE — register in ~/.claude/settings.json:
37
+ * {
38
+ * "hooks": {
39
+ * "SessionStart": [
40
+ * { "hooks": [ { "type": "command",
41
+ * "command": "FLAIR_AGENT_ID=me npx -y @tpsdev-ai/flair-mcp flair-session-start" } ] }
42
+ * ]
43
+ * }
44
+ * }
45
+ */
46
+ /** Minimal surface of FlairClient this hook depends on (eases testing). */
47
+ interface BootstrapClient {
48
+ bootstrap(opts: {
49
+ maxTokens?: number;
50
+ channel?: string;
51
+ subjects?: string[];
52
+ }): Promise<{
53
+ context?: string;
54
+ } | undefined>;
55
+ }
56
+ /**
57
+ * Core hook logic, with injectable dependencies so it can be unit-tested
58
+ * without a live Flair daemon. Returns the exact string to print to stdout.
59
+ * NEVER throws — every failure path returns NOOP_OUTPUT.
60
+ *
61
+ * @param rawInput the raw stdin string (may be empty / malformed)
62
+ * @param makeClient factory for the bootstrap client (defaults to FlairClient)
63
+ */
64
+ export declare function runHook(rawInput: string, makeClient?: (agentId: string) => BootstrapClient): Promise<string>;
65
+ export {};
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Flair SessionStart hook for Claude Code — auto-recall on session start.
4
+ *
5
+ * Claude Code fires `SessionStart` hooks when a session begins (startup,
6
+ * resume, clear, compact). A hook of `type: "command"` receives the hook
7
+ * payload as JSON on stdin and may print a JSON object whose
8
+ * `hookSpecificOutput.additionalContext` string is injected into the model's
9
+ * context for that session. This binary uses that channel to inject Flair's
10
+ * `bootstrap` context (soul + relevant memories + predicted context), so a
11
+ * fresh Claude Code session starts already warmed with the agent's memory —
12
+ * no manual "call the bootstrap tool" nudge required.
13
+ *
14
+ * It complements the MCP server (`flair-mcp`): the MCP server gives the agent
15
+ * pull tools (memory_search / memory_store / bootstrap on demand) and push
16
+ * recall, while this hook does a one-shot context push at session start.
17
+ *
18
+ * NO-OP-ON-ANY-FAILURE GUARANTEE
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.
25
+ *
26
+ * A hard timeout (FLAIR_HOOK_TIMEOUT_MS, default 8s) wraps the bootstrap call
27
+ * so a stalled Flair daemon can't hang session startup; on timeout we no-op.
28
+ *
29
+ * CONFIG (env, read identically to the MCP server)
30
+ * ------------------------------------------------
31
+ * FLAIR_AGENT_ID (required — absent → no-op) agent identity
32
+ * FLAIR_URL (default http://localhost:19926 via flair-client)
33
+ * FLAIR_KEY_PATH (default ~/.flair/keys/<agent>.key via flair-client)
34
+ * FLAIR_HOOK_TIMEOUT_MS (default 8000; clamped 500..30000)
35
+ *
36
+ * USAGE — register in ~/.claude/settings.json:
37
+ * {
38
+ * "hooks": {
39
+ * "SessionStart": [
40
+ * { "hooks": [ { "type": "command",
41
+ * "command": "FLAIR_AGENT_ID=me npx -y @tpsdev-ai/flair-mcp flair-session-start" } ] }
42
+ * ]
43
+ * }
44
+ * }
45
+ */
46
+ import { FlairClient } from "@tpsdev-ai/flair-client";
47
+ import { basename } from "node:path";
48
+ /** Claude Code SessionStart additionalContext hard limit (chars). */
49
+ const MAX_CHARS = 10_000;
50
+ /** Token budget for the bootstrap call — matches the proven prototype. */
51
+ const BOOTSTRAP_MAX_TOKENS = 2000;
52
+ /** Default hard timeout on the bootstrap call (ms). */
53
+ const DEFAULT_TIMEOUT_MS = 8000;
54
+ const TIMEOUT_FLOOR_MS = 500;
55
+ const TIMEOUT_CEILING_MS = 30_000;
56
+ /** Empty, inert hook output. Printing this is always a safe no-op. */
57
+ const NOOP_OUTPUT = "{}";
58
+ /** Resolve the bootstrap timeout from env, clamped to a sane range. */
59
+ function resolveTimeoutMs() {
60
+ const raw = process.env.FLAIR_HOOK_TIMEOUT_MS;
61
+ const parsed = raw != null ? Number(raw) : NaN;
62
+ return Number.isFinite(parsed) && parsed >= TIMEOUT_FLOOR_MS && parsed <= TIMEOUT_CEILING_MS
63
+ ? parsed
64
+ : DEFAULT_TIMEOUT_MS;
65
+ }
66
+ /** Read all of stdin as a string. Resolves on EOF, with a short fallback for
67
+ * interactive/manual runs where no stdin is piped (so it never hangs). */
68
+ function readStdin() {
69
+ return new Promise((resolve) => {
70
+ let data = "";
71
+ process.stdin.setEncoding("utf8");
72
+ process.stdin.on("data", (chunk) => (data += chunk));
73
+ process.stdin.on("end", () => resolve(data));
74
+ process.stdin.on("error", () => resolve(data));
75
+ // Manual-run fallback: if nothing is piped, don't block forever.
76
+ setTimeout(() => resolve(data), 200).unref?.();
77
+ });
78
+ }
79
+ /** Race a promise against a timeout. Rejects with a timeout error if exceeded. */
80
+ function withTimeout(promise, ms) {
81
+ return new Promise((resolve, reject) => {
82
+ const timer = setTimeout(() => reject(new Error("bootstrap_timeout")), ms);
83
+ timer.unref?.();
84
+ promise.then((value) => {
85
+ clearTimeout(timer);
86
+ resolve(value);
87
+ }, (err) => {
88
+ clearTimeout(timer);
89
+ reject(err);
90
+ });
91
+ });
92
+ }
93
+ /** Build the SessionStart hook output JSON from a context string. */
94
+ function hookOutput(context) {
95
+ return JSON.stringify({
96
+ hookSpecificOutput: {
97
+ hookEventName: "SessionStart",
98
+ additionalContext: context,
99
+ },
100
+ });
101
+ }
102
+ /**
103
+ * Core hook logic, with injectable dependencies so it can be unit-tested
104
+ * without a live Flair daemon. Returns the exact string to print to stdout.
105
+ * NEVER throws — every failure path returns NOOP_OUTPUT.
106
+ *
107
+ * @param rawInput the raw stdin string (may be empty / malformed)
108
+ * @param makeClient factory for the bootstrap client (defaults to FlairClient)
109
+ */
110
+ export async function runHook(rawInput, makeClient = defaultClientFactory) {
111
+ const agentId = process.env.FLAIR_AGENT_ID;
112
+ if (!agentId)
113
+ return NOOP_OUTPUT; // no identity → no-op, never break the session
114
+ let input = {};
115
+ try {
116
+ input = JSON.parse(rawInput || "{}") ?? {};
117
+ }
118
+ catch {
119
+ input = {}; // tolerate malformed stdin
120
+ }
121
+ const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
122
+ const project = basename(cwd) || undefined;
123
+ let context = "";
124
+ try {
125
+ const client = makeClient(agentId);
126
+ const res = await withTimeout(Promise.resolve(client.bootstrap({
127
+ maxTokens: BOOTSTRAP_MAX_TOKENS,
128
+ channel: "claude-code",
129
+ subjects: project ? [project] : undefined,
130
+ })), resolveTimeoutMs());
131
+ context = res && res.context ? String(res.context) : "";
132
+ }
133
+ catch {
134
+ return NOOP_OUTPUT; // flair unreachable / auth error / timeout → no-op
135
+ }
136
+ if (!context.trim())
137
+ return NOOP_OUTPUT;
138
+ if (context.length > MAX_CHARS)
139
+ context = context.slice(0, MAX_CHARS);
140
+ return hookOutput(context);
141
+ }
142
+ /** Default client factory — constructs a real FlairClient from FLAIR_* env,
143
+ * identical to how src/index.ts builds it. */
144
+ function defaultClientFactory(agentId) {
145
+ return new FlairClient({
146
+ agentId,
147
+ url: process.env.FLAIR_URL,
148
+ keyPath: process.env.FLAIR_KEY_PATH,
149
+ });
150
+ }
151
+ /** Entry point. Reads stdin, runs the hook, prints the result, exits 0.
152
+ * Wrapped so that even an unexpected throw degrades to a no-op. */
153
+ async function main() {
154
+ let output = NOOP_OUTPUT;
155
+ try {
156
+ output = await runHook(await readStdin());
157
+ }
158
+ catch {
159
+ output = NOOP_OUTPUT;
160
+ }
161
+ process.stdout.write(output);
162
+ }
163
+ // Only run when executed as a script, not when imported by tests.
164
+ // import.meta.main is set by Bun and Node 22.x; fall back to an argv check
165
+ // for runtimes that don't populate it.
166
+ const importMeta = import.meta;
167
+ const isMain = importMeta.main === true ||
168
+ (typeof process !== "undefined" &&
169
+ process.argv[1] != null &&
170
+ import.meta.url === `file://${process.argv[1]}`);
171
+ if (isMain) {
172
+ // .catch is belt-and-suspenders; main() already swallows everything.
173
+ void main().catch(() => process.stdout.write(NOOP_OUTPUT));
174
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-mcp",
3
- "version": "0.10.1",
3
+ "version": "0.12.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
- "flair-mcp": "dist/index.js"
8
+ "flair-mcp": "dist/index.js",
9
+ "flair-session-start": "dist/session-start-hook.js"
9
10
  },
10
11
  "files": [
11
12
  "dist/",
@@ -14,8 +15,9 @@
14
15
  ],
15
16
  "scripts": {
16
17
  "build": "tsc --noCheck",
18
+ "test": "bun test",
17
19
  "prepublishOnly": "npm run build",
18
- "postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');const p='dist/index.js';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)}\""
20
+ "postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');for(const p of ['dist/index.js','dist/session-start-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)}\""
19
21
  },
20
22
  "publishConfig": {
21
23
  "access": "public"
@@ -25,7 +27,7 @@
25
27
  },
26
28
  "dependencies": {
27
29
  "@modelcontextprotocol/sdk": "1.27.1",
28
- "@tpsdev-ai/flair-client": "0.10.1",
30
+ "@tpsdev-ai/flair-client": "0.12.0",
29
31
  "zod": "4.3.6"
30
32
  },
31
33
  "license": "Apache-2.0",