@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.
- package/README.md +66 -9
- package/dist/cli.js +379 -406
- package/dist/install/clients.js +132 -81
- package/dist/resources/embeddings-provider.js +41 -6
- package/package.json +8 -8
package/dist/install/clients.js
CHANGED
|
@@ -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
|
-
// ----
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
33
|
-
//
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
59
|
-
"@types/js-yaml": "
|
|
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": "
|
|
63
|
-
"js-yaml": "
|
|
64
|
-
"tar": "
|
|
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": "
|
|
77
|
+
"@playwright/test": "1.59.1",
|
|
78
78
|
"@types/node": "24.11.0",
|
|
79
|
-
"@types/tar": "
|
|
79
|
+
"@types/tar": "7.0.87",
|
|
80
80
|
"bun-types": "1.3.11",
|
|
81
81
|
"typescript": "5.9.3"
|
|
82
82
|
},
|