@tpsdev-ai/flair 0.15.0 → 0.16.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.
@@ -5,9 +5,28 @@
5
5
  // - detect(): boolean - returns true if client is installed
6
6
  // - wire(options: { agentId: string; flairUrl: string }): { ok: boolean; message: string }
7
7
  //
8
+ // Wiring contract (FIX 4 — onboarding dogfood round 1):
9
+ // "wired" MUST mean a config file was actually written. A wire function returns
10
+ // { ok: true } ONLY when it merged the Flair MCP server into the client's real
11
+ // config file. If it cannot (unknown path, write error), it returns
12
+ // { ok: false } with the correct per-OS snippet to paste — never a vague
13
+ // "manual wiring required" while elsewhere the run claims the client is wired.
14
+ // All paths are resolved cross-platform (Linux included) via standard
15
+ // per-client locations under $HOME / $XDG_CONFIG_HOME.
8
16
  // ---- Detection helpers ----------------------------------------------------------
9
17
  import { spawnSync } from "node:child_process";
10
- import { accessSync, constants } from "node:fs";
18
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { dirname, join } from "node:path";
21
+ /**
22
+ * Resolve the user's home dir. Prefer the live HOME/USERPROFILE env over
23
+ * os.homedir(), which caches the value at process start and so ignores a
24
+ * runtime HOME override — same convention as src/cli.ts ("so tests can
25
+ * override"). Production behavior is unchanged (HOME is set on every OS).
26
+ */
27
+ function resolveHome() {
28
+ return process.env.HOME || process.env.USERPROFILE || homedir();
29
+ }
11
30
  /**
12
31
  * Check if a command exists in PATH (cross-platform alternative to `which`).
13
32
  * Does not spawn a child process — pure filesystem check.
@@ -85,94 +104,126 @@ function cursorDetect() {
85
104
  return false;
86
105
  }
87
106
  }
88
- // ---- Internal wiring functions --------------------------------------------------
89
- function _wireClaudeCode(env) {
107
+ // ---- Shared config shapes -------------------------------------------------------
108
+ /** The standard MCP stdio server entry every client (except Codex TOML) uses. */
109
+ function flairMcpEntry(env) {
90
110
  return {
91
- ok: false,
92
- message: `Manual wiring required for Claude Code:\n` +
93
- `1. Locate Claude Code's settings file (usually ~/Library/Application Support/Claude/settings.json on macOS or %APPDATA%/Claude/settings.json on Windows)\n` +
94
- `2. Add or update the "mcpServers" section:\n` +
95
- ` {\n` +
96
- ` "mcpServers": {\n` +
97
- ` "flair": {\n` +
98
- ` "command": "npx",\n` +
99
- ` "args": ["-y", "@tpsdev-ai/flair-mcp"],\n` +
100
- ` "env": {\n` +
101
- ` "FLAIR_AGENT_ID": "${env.FLAIR_AGENT_ID}",\n` +
102
- ` "FLAIR_URL": "${env.FLAIR_URL}"\n` +
103
- ` }\n` +
104
- ` }\n` +
105
- ` }\n` +
106
- ` }\n` +
107
- `3. Restart Claude Code\n` +
108
- `Note: This is a manual step - the Flair CLI cannot automatically modify Claude Code's settings due to security restrictions.`,
111
+ command: "npx",
112
+ args: ["-y", "@tpsdev-ai/flair-mcp"],
113
+ env: { FLAIR_AGENT_ID: env.FLAIR_AGENT_ID, FLAIR_URL: env.FLAIR_URL },
109
114
  };
110
115
  }
116
+ /** Pretty-printed JSON `mcpServers.flair` snippet for copy-paste fallbacks. */
117
+ function jsonSnippet(env) {
118
+ return JSON.stringify({ mcpServers: { flair: flairMcpEntry(env) } }, null, 2);
119
+ }
120
+ /** TOML `[mcp_servers.flair]` snippet (Codex format). */
121
+ function tomlSnippet(env) {
122
+ return [
123
+ `[mcp_servers.flair]`,
124
+ `command = "npx"`,
125
+ `args = ["-y", "@tpsdev-ai/flair-mcp"]`,
126
+ ``,
127
+ `[mcp_servers.flair.env]`,
128
+ `FLAIR_AGENT_ID = "${env.FLAIR_AGENT_ID}"`,
129
+ `FLAIR_URL = "${env.FLAIR_URL}"`,
130
+ ].join("\n");
131
+ }
132
+ /**
133
+ * Merge the Flair MCP server into a JSON config file with an `mcpServers` map.
134
+ * Creates the file (and parent dir) if absent; preserves existing servers and
135
+ * any other top-level keys. Returns ok:true only when the file was written.
136
+ */
137
+ function wireJsonMcp(configPath, label, env) {
138
+ const home = resolveHome();
139
+ const display = configPath.startsWith(home) ? "~" + configPath.slice(home.length) : configPath;
140
+ try {
141
+ let config = {};
142
+ if (existsSync(configPath)) {
143
+ const raw = readFileSync(configPath, "utf-8").trim();
144
+ if (raw)
145
+ config = JSON.parse(raw);
146
+ }
147
+ config.mcpServers = config.mcpServers || {};
148
+ const existing = config.mcpServers.flair;
149
+ if (existing && existing.env?.FLAIR_URL === env.FLAIR_URL && existing.env?.FLAIR_AGENT_ID === env.FLAIR_AGENT_ID) {
150
+ return { ok: true, message: `${label}: already wired in ${display}` };
151
+ }
152
+ config.mcpServers.flair = flairMcpEntry(env);
153
+ mkdirSync(dirname(configPath), { recursive: true });
154
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
155
+ return { ok: true, message: `${label}: wired ${display} (restart ${label} to pick it up)` };
156
+ }
157
+ catch (err) {
158
+ const reason = err instanceof Error ? err.message : String(err);
159
+ return {
160
+ ok: false,
161
+ message: `${label}: manual wiring needed (could not write ${display}: ${reason}).\n` +
162
+ ` Add this to ${display}:\n${indent(jsonSnippet(env))}`,
163
+ };
164
+ }
165
+ }
166
+ function indent(s) {
167
+ return s.split("\n").map((l) => ` ${l}`).join("\n");
168
+ }
169
+ // ---- Per-client config paths (cross-platform, Linux included) --------------------
170
+ /** Cursor: ~/.cursor/mcp.json on every OS. */
171
+ function cursorConfigPath() {
172
+ return join(resolveHome(), ".cursor", "mcp.json");
173
+ }
174
+ /** Gemini CLI: ~/.gemini/settings.json on every OS. */
175
+ function geminiConfigPath() {
176
+ return join(resolveHome(), ".gemini", "settings.json");
177
+ }
178
+ /** Codex CLI: ~/.codex/config.toml on every OS. */
179
+ function codexConfigPath() {
180
+ return join(resolveHome(), ".codex", "config.toml");
181
+ }
182
+ // ---- Internal wiring functions --------------------------------------------------
183
+ //
184
+ // Claude Code wiring lives inline in src/cli.ts (it writes ~/.claude.json, the
185
+ // one client the CLI safely edits, cross-platform). _wireClaudeCode here is the
186
+ // fallback used when something calls the array form; it returns the snippet for
187
+ // ~/.claude.json so the message is unambiguous and correct on every OS.
188
+ function _wireClaudeCode(env) {
189
+ // The real auto-wire is inline in cli.ts. If reached via the array, point at
190
+ // the correct cross-platform path (~/.claude.json — same on macOS/Linux/Win)
191
+ // and give the exact snippet. Never emit macOS-only paths here.
192
+ return wireJsonMcp(join(resolveHome(), ".claude.json"), "Claude Code", env);
193
+ }
111
194
  function _wireCodex(env) {
112
- return {
113
- ok: false,
114
- message: `Manual wiring required for Codex:\n` +
115
- `1. Locate Codex's configuration (check ~/.codex/config or similar)\n` +
116
- `2. Add the Flair MCP server configuration:\n` +
117
- ` {\n` +
118
- ` "mcpServers": {\n` +
119
- ` "flair": {\n` +
120
- ` "command": "npx",\n` +
121
- ` "args": ["-y", "@tpsdev-ai/flair-mcp"],\n` +
122
- ` "env": {\n` +
123
- ` "FLAIR_AGENT_ID": "${env.FLAIR_AGENT_ID}",\n` +
124
- ` "FLAIR_URL": "${env.FLAIR_URL}"\n` +
125
- ` }\n` +
126
- ` }\n` +
127
- ` }\n` +
128
- ` }\n` +
129
- `3. Restart Codex\n` +
130
- `Note: This is a manual step - the Flair CLI cannot automatically modify Codex's configuration due to security restrictions.`,
131
- };
195
+ // Codex uses TOML with a [mcp_servers.flair] table. We don't carry a TOML
196
+ // parser, and blind text-appending risks corrupting/duplicating an existing
197
+ // table so we only auto-write when the file does NOT yet exist (clean
198
+ // create), otherwise emit the exact TOML block to paste.
199
+ const path = codexConfigPath();
200
+ const display = "~/.codex/config.toml";
201
+ try {
202
+ if (existsSync(path)) {
203
+ return {
204
+ ok: false,
205
+ message: `Codex: manual wiring needed — ${display} already exists.\n` +
206
+ ` Add this block to ${display}:\n${indent(tomlSnippet(env))}`,
207
+ };
208
+ }
209
+ mkdirSync(dirname(path), { recursive: true });
210
+ writeFileSync(path, tomlSnippet(env) + "\n");
211
+ return { ok: true, message: `Codex: wired ${display} (restart Codex to pick it up)` };
212
+ }
213
+ catch (err) {
214
+ const reason = err instanceof Error ? err.message : String(err);
215
+ return {
216
+ ok: false,
217
+ message: `Codex: manual wiring needed (could not write ${display}: ${reason}).\n` +
218
+ ` Add this block to ${display}:\n${indent(tomlSnippet(env))}`,
219
+ };
220
+ }
132
221
  }
133
222
  function _wireGemini(env) {
134
- return {
135
- ok: false,
136
- message: `Manual wiring required for Gemini:\n` +
137
- `1. Locate Gemini's configuration (check ~/.gemini/config or similar)\n` +
138
- `2. Add the Flair MCP server configuration:\n` +
139
- ` {\n` +
140
- ` "mcpServers": {\n` +
141
- ` "flair": {\n` +
142
- ` "command": "npx",\n` +
143
- ` "args": ["-y", "@tpsdev-ai/flair-mcp"],\n` +
144
- ` "env": {\n` +
145
- ` "FLAIR_AGENT_ID": "${env.FLAIR_AGENT_ID}",\n` +
146
- ` "FLAIR_URL": "${env.FLAIR_URL}"\n` +
147
- ` }\n` +
148
- ` }\n` +
149
- ` }\n` +
150
- ` }\n` +
151
- `3. Restart Gemini\n` +
152
- `Note: This is a manual step - the Flair CLI cannot automatically modify Gemini's configuration due to security restrictions.`,
153
- };
223
+ return wireJsonMcp(geminiConfigPath(), "Gemini", env);
154
224
  }
155
225
  function _wireCursor(env) {
156
- return {
157
- ok: false,
158
- message: `Manual wiring required for Cursor:\n` +
159
- `1. Locate Cursor's settings file (usually ~/.cursor/settings.json)\n` +
160
- `2. Add or update the "mcpServers" section:\n` +
161
- ` {\n` +
162
- ` "mcpServers": {\n` +
163
- ` "flair": {\n` +
164
- ` "command": "npx",\n` +
165
- ` "args": ["-y", "@tpsdev-ai/flair-mcp"],\n` +
166
- ` "env": {\n` +
167
- ` "FLAIR_AGENT_ID": "${env.FLAIR_AGENT_ID}",\n` +
168
- ` "FLAIR_URL": "${env.FLAIR_URL}"\n` +
169
- ` }\n` +
170
- ` }\n` +
171
- ` }\n` +
172
- ` }\n` +
173
- `3. Restart Cursor\n` +
174
- `Note: This is a manual step - the Flair CLI cannot automatically modify Cursor's settings due to security restrictions.`,
175
- };
226
+ return wireJsonMcp(cursorConfigPath(), "Cursor", env);
176
227
  }
177
228
  // ---- Exported detection & wiring array ------------------------------------------
178
229
  export const ALL_CLIENTS = [
@@ -9,6 +9,39 @@
9
9
  * the VM linker entirely.
10
10
  */
11
11
  import { join } from "node:path";
12
+ import { existsSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ /**
15
+ * Resolve the directory the embeddings model lives in / downloads into.
16
+ *
17
+ * Resolution order (everything writable, never the read-only package dir):
18
+ * 1. FLAIR_MODELS_DIR — explicit operator/docker override.
19
+ * 2. <ROOTPATH>/models — Harper's data dir (Flair passes ROOTPATH =
20
+ * ~/.flair/data when it spawns Harper). User-
21
+ * owned and writable even on sudo-global installs.
22
+ * 3. <cwd>/models — ONLY if a model already lives there. Backward
23
+ * compat for existing writable installs that
24
+ * downloaded into the package dir before this fix;
25
+ * never used as a download target on fresh installs.
26
+ * 4. ~/.flair/data/models — last-resort default when ROOTPATH is unset.
27
+ *
28
+ * The chosen dir is always writable, so the embeddings engine can download the
29
+ * model on first use without hitting EACCES on a root-owned package dir.
30
+ */
31
+ export function resolveModelsDir() {
32
+ const override = process.env.FLAIR_MODELS_DIR;
33
+ if (override)
34
+ return override;
35
+ const rootPath = process.env.ROOTPATH;
36
+ if (rootPath)
37
+ return join(rootPath, "models");
38
+ // Backward compat: a prior (writable) install may have the model cached in
39
+ // the package dir. Reuse it rather than re-downloading — but only if present.
40
+ const cwdModels = join(process.cwd(), "models");
41
+ if (existsSync(cwdModels))
42
+ return cwdModels;
43
+ return join(homedir(), ".flair", "data", "models");
44
+ }
12
45
  let _state = "uninitialized";
13
46
  let _initError;
14
47
  let _warnedOnce = false;
@@ -29,19 +62,21 @@ async function ensureInit() {
29
62
  return;
30
63
  }
31
64
  catch {
32
- // Not initialized — init with modelsDir pointing to where Harper's
33
- // plugin loader downloaded the model (process.cwd() is the app dir).
65
+ // Not initialized — init with modelsDir pointing at a USER-WRITABLE
66
+ // location. On a sudo/root-owned global install the Flair package dir
67
+ // (process.cwd()) is root-owned, so a model download into <cwd>/models
68
+ // fails with EACCES and semantic search silently dies (ops-am0v). The
69
+ // model — and everything else Flair writes — must live under ~/.flair.
70
+ //
34
71
  // NOTE: import.meta.dirname and __dirname are both undefined in Harper v5's
35
- // VM sandbox / worker threads, so we use process.cwd() which points to the
36
- // Flair application directory.
72
+ // VM sandbox / worker threads, so we resolve from env + process.cwd().
37
73
  try {
38
74
  if (!_hfe) {
39
75
  _hfe = await import("harper-fabric-embeddings");
40
76
  }
41
- const modelsDir = join(process.cwd(), "models");
77
+ const modelsDir = resolveModelsDir();
42
78
  // Find the native addon binary explicitly to avoid __dirname-dependent
43
79
  // discovery in @node-llama-cpp which fails in Harper's VM sandbox.
44
- const { existsSync } = await import("node:fs");
45
80
  const platforms = ["linux-x64", "mac-arm64-metal", "mac-arm64", "win-x64"];
46
81
  let addonPath;
47
82
  for (const platform of platforms) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -55,13 +55,13 @@
55
55
  "node": ">=22"
56
56
  },
57
57
  "dependencies": {
58
- "@harperfast/harper": "5.0.21",
59
- "@types/js-yaml": "^4.0.9",
58
+ "@harperfast/harper": "5.1.14",
59
+ "@types/js-yaml": "4.0.9",
60
60
  "commander": "14.0.3",
61
61
  "harper-fabric-embeddings": "0.2.3",
62
- "jose": "^6.2.2",
63
- "js-yaml": "^4.1.1",
64
- "tar": "^7.5.13",
62
+ "jose": "6.2.2",
63
+ "js-yaml": "4.1.1",
64
+ "tar": "7.5.13",
65
65
  "tweetnacl": "1.0.3"
66
66
  },
67
67
  "optionalDependencies": {
@@ -74,9 +74,9 @@
74
74
  "@node-llama-cpp/win-x64": "3.18.1"
75
75
  },
76
76
  "devDependencies": {
77
- "@playwright/test": "^1.59.1",
77
+ "@playwright/test": "1.59.1",
78
78
  "@types/node": "24.11.0",
79
- "@types/tar": "^7.0.87",
79
+ "@types/tar": "7.0.87",
80
80
  "bun-types": "1.3.11",
81
81
  "typescript": "5.9.3"
82
82
  },