@tpsdev-ai/flair 0.48.0 → 0.50.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 +2 -0
- package/dist/bridges/runtime/roundtrip.js +91 -2
- package/dist/build-info.json +3 -3
- package/dist/cli.js +903 -226
- package/dist/component-env.js +52 -4
- package/dist/deploy.js +20 -3
- package/dist/doctor-client.js +105 -32
- package/dist/federation/scheduler.js +24 -3
- package/dist/hook-install.js +96 -16
- package/dist/install/clients.js +318 -9
- package/dist/lib/auth-resolve.js +34 -3
- package/dist/lib/mcp-enable.js +134 -26
- package/dist/lib/scheduler-platform.js +132 -10
- package/dist/lib/scratch-owner.js +49 -0
- package/dist/rem/scheduler.js +23 -5
- package/dist/resources/AgentSeed.js +2 -0
- package/dist/resources/Memory.js +24 -5
- package/dist/resources/MemoryBootstrap.js +8 -4
- package/dist/resources/MemoryFeed.js +3 -0
- package/dist/resources/MemoryMaintenance.js +11 -2
- package/dist/resources/bm25-index-service.js +257 -0
- package/dist/resources/bm25-index.js +631 -0
- package/dist/resources/bm25.js +31 -1
- package/dist/resources/embeddings-boot.js +45 -3
- package/dist/resources/health.js +52 -7
- package/dist/resources/mcp-tools.js +1 -0
- package/dist/resources/memory-read-scope.js +2 -0
- package/dist/resources/search-readiness.js +100 -0
- package/dist/resources/semantic-retrieval-core.js +102 -23
- package/dist/resources/sort-comparators.js +45 -0
- package/dist/src/lib/scheduler-platform.js +132 -10
- package/dist/src/rem/scheduler.js +23 -5
- package/dist/version-check.js +59 -13
- package/docs/auth.md +5 -0
- package/docs/claude-code.md +10 -3
- package/docs/deepseek-harness.md +1 -1
- package/docs/deployment.md +11 -1
- package/docs/hosted-on-fabric.md +2 -0
- package/docs/integrations.md +78 -5
- package/docs/mcp-clients.md +85 -15
- package/docs/notes/mcp-oauth-model2.md +31 -13
- package/docs/quickstart-fabric.md +1 -1
- package/docs/quickstart.md +9 -9
- package/docs/standalone-local.md +3 -0
- package/docs/troubleshooting.md +25 -0
- package/package.json +4 -3
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* in `verifyFirstRun()`: success may not be claimed until the thing the
|
|
21
21
|
* operator asked for has been observed to happen once.
|
|
22
22
|
*/
|
|
23
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
24
|
-
import { resolve, dirname, isAbsolute } from "node:path";
|
|
25
|
-
import { platform } from "node:os";
|
|
23
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, realpathSync } from "node:fs";
|
|
24
|
+
import { resolve, dirname, isAbsolute, basename } from "node:path";
|
|
25
|
+
import { platform, userInfo } from "node:os";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
/**
|
|
28
28
|
* 30s ceiling on launchctl/systemctl invocations so a hung service manager
|
|
@@ -90,20 +90,71 @@ export function interpretActiveResult(plat, code, stdout, stderr) {
|
|
|
90
90
|
return null; // spawn itself failed — inconclusive
|
|
91
91
|
return false; // covers the no-bus case: empty stdout, nonzero/failed exit
|
|
92
92
|
}
|
|
93
|
+
/** True when this session already has the env `systemctl --user` needs. */
|
|
94
|
+
export function sessionHasUserBusEnv(env = process.env) {
|
|
95
|
+
return Boolean(env.XDG_RUNTIME_DIR?.trim() && env.DBUS_SESSION_BUS_ADDRESS?.trim());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads whether lingering is already enabled for the current user.
|
|
99
|
+
* `loginctl show-user … Linger=yes` is the official answer; the stamp file
|
|
100
|
+
* `loginctl enable-linger` creates is the fallback when loginctl is missing
|
|
101
|
+
* or inconclusive. A failed probe is `null`, never linger-off — inventing
|
|
102
|
+
* linger-off would repeat the linger remedy after it already ran (#1107).
|
|
103
|
+
*/
|
|
104
|
+
export function probeUserLingerEnabled(opts = {}) {
|
|
105
|
+
const run = opts.run ?? spawnReport;
|
|
106
|
+
const lingerStampExists = opts.lingerStampExists ?? ((u) => existsSync(`/var/lib/systemd/linger/${u}`));
|
|
107
|
+
let user = "";
|
|
108
|
+
try {
|
|
109
|
+
user = userInfo().username;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
user = process.env.USER || process.env.LOGNAME || "";
|
|
113
|
+
}
|
|
114
|
+
if (!user)
|
|
115
|
+
return null;
|
|
116
|
+
const r = run(["loginctl", "show-user", user, "--property=Linger"], STATUS_CHECK_TIMEOUT_MS);
|
|
117
|
+
const m = /^Linger=(yes|no)\s*$/m.exec(r.stdout ?? "");
|
|
118
|
+
if (m)
|
|
119
|
+
return m[1] === "yes";
|
|
120
|
+
if (lingerStampExists(user))
|
|
121
|
+
return true;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
93
124
|
/**
|
|
94
|
-
* Human remedy text for a failed scheduler-load attempt (flair#850).
|
|
95
|
-
* the
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
125
|
+
* Human remedy text for a failed scheduler-load attempt (flair#850, #1107).
|
|
126
|
+
* Covers the traced "no systemd user session bus" failure, which blocks
|
|
127
|
+
* `systemctl --user` entirely in ssh-without-lingering, container, and CI
|
|
128
|
+
* contexts. Two cases that used to share one remedy:
|
|
129
|
+
* (a) lingering genuinely off — print `loginctl enable-linger`
|
|
130
|
+
* (b) linger already on, this session has no user-bus env — print the
|
|
131
|
+
* `XDG_RUNTIME_DIR` / `DBUS_SESSION_BUS_ADDRESS` export lines
|
|
132
|
+
* Repeating (a) after the operator has applied it is the #1107 lie.
|
|
133
|
+
* Returns null when the failure doesn't match a known pattern — the caller
|
|
134
|
+
* already prints the raw stderr, so the operator still has something to go on.
|
|
100
135
|
*
|
|
101
136
|
* `enableCommand` is the caller's own enable invocation, named in the remedy
|
|
102
137
|
* so the operator is told to re-run the command they actually ran.
|
|
103
138
|
*/
|
|
104
|
-
export function describeLoadFailure(plat, loadResult, enableCommand) {
|
|
139
|
+
export function describeLoadFailure(plat, loadResult, enableCommand, session) {
|
|
105
140
|
const stderr = loadResult.stderr || "";
|
|
106
141
|
if (plat === "linux" && /failed to connect to bus/i.test(stderr)) {
|
|
142
|
+
if (session?.lingerEnabled === true) {
|
|
143
|
+
const env = session.env ?? process.env;
|
|
144
|
+
if (!sessionHasUserBusEnv(env)) {
|
|
145
|
+
return ("No systemd user session bus is available in this session. Lingering is already enabled — " +
|
|
146
|
+
"do not re-run `loginctl enable-linger`. The remaining gap is this session's user-bus environment. " +
|
|
147
|
+
"Export:\n" +
|
|
148
|
+
" export XDG_RUNTIME_DIR=/run/user/$(id -u)\n" +
|
|
149
|
+
" export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus\n" +
|
|
150
|
+
` then re-run \`${enableCommand}\`.`);
|
|
151
|
+
}
|
|
152
|
+
return ("No systemd user session bus is available in this session. Lingering is already enabled and " +
|
|
153
|
+
"this session already has XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS — " +
|
|
154
|
+
"do not re-run `loginctl enable-linger` or re-export those variables. " +
|
|
155
|
+
"Check that `$XDG_RUNTIME_DIR/bus` exists (the systemd --user instance may not be running), " +
|
|
156
|
+
`then re-run \`${enableCommand}\`.`);
|
|
157
|
+
}
|
|
107
158
|
return ("No systemd user session bus is available in this session (common over ssh without lingering, " +
|
|
108
159
|
"in containers, or under CI). Fix: enable lingering for this user — `loginctl enable-linger <user>` " +
|
|
109
160
|
`— then re-run \`${enableCommand}\`.`);
|
|
@@ -172,6 +223,77 @@ export function resolveNodeBin(explicit) {
|
|
|
172
223
|
"enable time — refusing to install a shim that would resolve `node` from the service manager's PATH " +
|
|
173
224
|
"at run time. Install node (or put it on PATH for this shell) and re-run enable.");
|
|
174
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Resolves the path enable will bake as FLAIR_BIN, and whether that path is
|
|
228
|
+
* the stable public `flair` entry (flair#1279).
|
|
229
|
+
*
|
|
230
|
+
* Resolution order:
|
|
231
|
+
* 1. `explicit` — caller/test override. Relatives are resolved against cwd.
|
|
232
|
+
* 2. `hooks.argv1` / `process.argv[1]` — whatever launched enable.
|
|
233
|
+
* 3. The public `flair` on PATH, only when (1) and (2) are empty.
|
|
234
|
+
* Nothing absolute resolvable ⇒ throw. A bare `"flair"` is not an exec
|
|
235
|
+
* target under #1231's `exec <node> <script>` form (`node flair` looks in
|
|
236
|
+
* cwd, not PATH).
|
|
237
|
+
*/
|
|
238
|
+
export function resolveFlairBin(explicit, hooks) {
|
|
239
|
+
const publicBin = hooks && "publicBin" in hooks ? (hooks.publicBin ?? null) : lookupPublicFlairBin();
|
|
240
|
+
const captured = explicit ?? hooks?.argv1 ?? process.argv[1];
|
|
241
|
+
let path;
|
|
242
|
+
if (typeof captured === "string" && captured.length > 0) {
|
|
243
|
+
path = isAbsolute(captured) ? captured : resolve(captured);
|
|
244
|
+
}
|
|
245
|
+
else if (publicBin) {
|
|
246
|
+
path = publicBin;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
throw new Error("unable to resolve an absolute path to the flair CLI (process.argv[1] was empty and `command -v flair` " +
|
|
250
|
+
"found nothing). The scheduler shim bakes this path in at enable time — refusing to install a shim " +
|
|
251
|
+
"whose exec target is unknown. Re-run enable via the `flair` command.");
|
|
252
|
+
}
|
|
253
|
+
return { path, publicBin, canonical: isCanonicalFlairBin(path, publicBin) };
|
|
254
|
+
}
|
|
255
|
+
/** True when `baked` is the public `flair` entry, not a working-tree capture. */
|
|
256
|
+
export function isCanonicalFlairBin(baked, publicBin) {
|
|
257
|
+
if (basename(baked) === "flair")
|
|
258
|
+
return true;
|
|
259
|
+
if (publicBin && pathsReferToSameFile(baked, publicBin))
|
|
260
|
+
return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The enable-report lines for a non-canonical FLAIR_BIN. Empty when the
|
|
265
|
+
* baked path is the public entry — callers should not print a warning then.
|
|
266
|
+
*/
|
|
267
|
+
export function formatFlairBinWarning(baked, publicBin, enableCommand) {
|
|
268
|
+
if (isCanonicalFlairBin(baked, publicBin))
|
|
269
|
+
return [];
|
|
270
|
+
const lines = [
|
|
271
|
+
`⚠️ FLAIR_BIN is ${baked} — that is the process that ran enable, not a stable public entry.`,
|
|
272
|
+
` A later blue/green directory swap, or deleting this working tree, will strand the scheduler unit.`,
|
|
273
|
+
];
|
|
274
|
+
if (publicBin) {
|
|
275
|
+
lines.push(` Public \`flair\` on PATH: ${publicBin}. Re-run \`${enableCommand}\` as the \`flair\` command to bake that path instead.`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
lines.push(` No \`flair\` on PATH. Re-run \`${enableCommand}\` via the installed \`flair\` command (or a stable symlink) so the baked path survives a tree swap.`);
|
|
279
|
+
}
|
|
280
|
+
return lines;
|
|
281
|
+
}
|
|
282
|
+
function lookupPublicFlairBin() {
|
|
283
|
+
const r = spawnReport(["/bin/sh", "-c", "command -v flair"], STATUS_CHECK_TIMEOUT_MS);
|
|
284
|
+
const found = r.stdout.trim().split("\n")[0]?.trim() ?? "";
|
|
285
|
+
if (r.code === 0 && found && isAbsolute(found) && existsSync(found))
|
|
286
|
+
return found;
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function pathsReferToSameFile(a, b) {
|
|
290
|
+
try {
|
|
291
|
+
return realpathSync(a) === realpathSync(b);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return resolve(a) === resolve(b);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
175
297
|
// ─── first-run verification (flair#1231) ────────────────────────────────────
|
|
176
298
|
// A load/bootstrap command exiting 0 proves the service manager accepted the
|
|
177
299
|
// job — not that the job can run. The only vantage that exercises the real
|
|
@@ -19,7 +19,7 @@ import { homedir } from "node:os";
|
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { escapeXml } from "../lib/xml-escape.js";
|
|
22
|
-
import { detectPlatform as detectPlatformFor, spawnReport, readTemplate as readTemplateFrom, renderTemplateWith, writeFileWithDir, interpretActiveResult, describeLoadFailure as describeLoadFailureFor, describeExitCode, resolveNodeBin, verifyFirstRun, STATUS_CHECK_TIMEOUT_MS, } from "../lib/scheduler-platform.js";
|
|
22
|
+
import { detectPlatform as detectPlatformFor, spawnReport, readTemplate as readTemplateFrom, renderTemplateWith, writeFileWithDir, interpretActiveResult, describeLoadFailure as describeLoadFailureFor, describeExitCode, resolveNodeBin, resolveFlairBin, formatFlairBinWarning, verifyFirstRun, probeUserLingerEnabled, STATUS_CHECK_TIMEOUT_MS, } from "../lib/scheduler-platform.js";
|
|
23
23
|
// Re-exported so this module's public surface is unchanged by the extraction
|
|
24
24
|
// into src/lib/scheduler-platform.ts (a second scheduler — `flair federation
|
|
25
25
|
// sync enable` — needs the identical launchctl/systemctl interpretation, and
|
|
@@ -180,8 +180,8 @@ export async function queryActiveStateAsync(plat, timeoutMs = STATUS_CHECK_TIMEO
|
|
|
180
180
|
* known pattern — the caller already prints the raw stderr, so the operator
|
|
181
181
|
* still has something to go on.
|
|
182
182
|
*/
|
|
183
|
-
export function describeLoadFailure(plat, loadResult) {
|
|
184
|
-
return describeLoadFailureFor(plat, loadResult, "flair rem nightly enable");
|
|
183
|
+
export function describeLoadFailure(plat, loadResult, session) {
|
|
184
|
+
return describeLoadFailureFor(plat, loadResult, "flair rem nightly enable", session);
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
187
|
* Formats the `flair rem nightly enable` report from an `EnableResult`.
|
|
@@ -199,6 +199,15 @@ export function describeLoadFailure(plat, loadResult) {
|
|
|
199
199
|
* happen once. A missing `loadResult`/`firstRun` (test-only skipLoad shape)
|
|
200
200
|
* therefore withholds the headline too, instead of being treated as success.
|
|
201
201
|
*/
|
|
202
|
+
function appendFlairBinWarning(lines, r) {
|
|
203
|
+
if (r.flairBinCanonical !== false || !r.flairBin)
|
|
204
|
+
return;
|
|
205
|
+
const warning = formatFlairBinWarning(r.flairBin, r.flairBinPublic ?? null, "flair rem nightly enable");
|
|
206
|
+
if (warning.length === 0)
|
|
207
|
+
return;
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(...warning);
|
|
210
|
+
}
|
|
202
211
|
export function formatEnableReport(r, input) {
|
|
203
212
|
const { hour, minute, agentId, flairUrl } = input;
|
|
204
213
|
const scheduleTime = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
@@ -216,11 +225,15 @@ export function formatEnableReport(r, input) {
|
|
|
216
225
|
];
|
|
217
226
|
if (lr.stderr)
|
|
218
227
|
lines.push(` stderr: ${lr.stderr.trim()}`);
|
|
219
|
-
const
|
|
228
|
+
const lingerEnabled = input.lingerEnabled !== undefined
|
|
229
|
+
? input.lingerEnabled
|
|
230
|
+
: (r.platform === "linux" ? (input.probeLinger ?? probeUserLingerEnabled)() : undefined);
|
|
231
|
+
const remedy = describeLoadFailure(r.platform, lr, { lingerEnabled, env: input.env });
|
|
220
232
|
lines.push("");
|
|
221
233
|
lines.push(remedy ? ` ${remedy}` : ` Re-run the activation command above manually to see the full diagnostic.`);
|
|
222
234
|
lines.push("");
|
|
223
235
|
lines.push(` Nothing is scheduled until activation succeeds. Check anytime with: flair rem nightly status`);
|
|
236
|
+
appendFlairBinWarning(lines, r);
|
|
224
237
|
return { lines, ok: false };
|
|
225
238
|
}
|
|
226
239
|
if (!r.firstRunVerified) {
|
|
@@ -272,6 +285,7 @@ export function formatEnableReport(r, input) {
|
|
|
272
285
|
}
|
|
273
286
|
lines.push("");
|
|
274
287
|
lines.push(` Check anytime with: flair rem nightly status`);
|
|
288
|
+
appendFlairBinWarning(lines, r);
|
|
275
289
|
return { lines, ok: false };
|
|
276
290
|
}
|
|
277
291
|
const lines = [
|
|
@@ -288,6 +302,7 @@ export function formatEnableReport(r, input) {
|
|
|
288
302
|
lines.push(` First run: completed through the service manager, exit 0`);
|
|
289
303
|
lines.push("");
|
|
290
304
|
lines.push(`Disable with \`flair rem nightly disable\`.`);
|
|
305
|
+
appendFlairBinWarning(lines, r);
|
|
291
306
|
return { lines, ok: true };
|
|
292
307
|
}
|
|
293
308
|
/**
|
|
@@ -329,7 +344,8 @@ export function formatStatusReport(s) {
|
|
|
329
344
|
*/
|
|
330
345
|
export function enableScheduler(opts) {
|
|
331
346
|
const plat = detectPlatform(opts.platformOverride);
|
|
332
|
-
const
|
|
347
|
+
const resolvedFlair = resolveFlairBin(opts.flairBin);
|
|
348
|
+
const flairBin = resolvedFlair.path;
|
|
333
349
|
const nodeBin = resolveNodeBin(opts.nodeBin);
|
|
334
350
|
const shimPath = opts.shimPathOverride ?? SHIM_PATH_DEFAULT;
|
|
335
351
|
const templateRoot = opts.templateRootOverride ?? defaultTemplateRoot();
|
|
@@ -383,6 +399,7 @@ export function enableScheduler(opts) {
|
|
|
383
399
|
return {
|
|
384
400
|
platform: plat, shimPath, schedulerPath: plistPath, loadCommand, loadResult,
|
|
385
401
|
firstRunVerified: firstRun?.verified === true, firstRun,
|
|
402
|
+
flairBin, flairBinCanonical: resolvedFlair.canonical, flairBinPublic: resolvedFlair.publicBin,
|
|
386
403
|
};
|
|
387
404
|
}
|
|
388
405
|
// Linux: systemd user units.
|
|
@@ -408,6 +425,7 @@ export function enableScheduler(opts) {
|
|
|
408
425
|
return {
|
|
409
426
|
platform: plat, shimPath, schedulerPath: timerPath, loadCommand, loadResult,
|
|
410
427
|
firstRunVerified: firstRun?.verified === true, firstRun,
|
|
428
|
+
flairBin, flairBinCanonical: resolvedFlair.canonical, flairBinPublic: resolvedFlair.publicBin,
|
|
411
429
|
};
|
|
412
430
|
}
|
|
413
431
|
/**
|
package/dist/version-check.js
CHANGED
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
* - No advisory data — we don't know which release fixed which CVE, so the
|
|
19
19
|
* severity heuristic is purely the version GAP (major/minor count), not
|
|
20
20
|
* "did this release carry a security fix". See classifyGap().
|
|
21
|
+
*
|
|
22
|
+
* Honest-numbers refinement (flair#1341): the TTL fast-path is only taken
|
|
23
|
+
* when the cached answer implies NOTHING will be printed. When a cached
|
|
24
|
+
* answer would produce a nudge, we spend one fresh fetch (same short timeout,
|
|
25
|
+
* same failure tolerance) so the printed fact is current whenever possible —
|
|
26
|
+
* nudges are rare, so the TTL still protects the common up-to-date path. If
|
|
27
|
+
* that fetch fails, the nudge falls back to the cached value but SAYS so
|
|
28
|
+
* ("latest known (checked 9h ago): …") instead of stating a stale number as
|
|
29
|
+
* current fact.
|
|
21
30
|
*/
|
|
22
31
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
32
|
import { homedir } from "node:os";
|
|
@@ -85,13 +94,23 @@ export function defaultVersionCheckDeps() {
|
|
|
85
94
|
* Resolve the latest published @tpsdev-ai/flair version, preferring a fresh
|
|
86
95
|
* cache hit over a network round trip, and falling back to a stale cache (or
|
|
87
96
|
* giving up quietly) when the registry is unreachable. NEVER throws.
|
|
97
|
+
*
|
|
98
|
+
* flair#1341: the cache fast-path applies only when the cached answer implies
|
|
99
|
+
* no nudge. A cached answer that WOULD nudge triggers one fresh fetch (same
|
|
100
|
+
* timeout, same failure tolerance) so the printed fact is current whenever
|
|
101
|
+
* the network allows; on failure it falls back to the cache, age attached.
|
|
88
102
|
*/
|
|
89
103
|
export async function checkVersion(installed, injected = {}) {
|
|
90
104
|
const deps = { ...defaultVersionCheckDeps(), ...injected };
|
|
91
105
|
const nowMs = deps.now();
|
|
92
106
|
const cached = deps.readCache(deps.cachePath);
|
|
93
107
|
if (cached && nowMs - cached.checkedAt < deps.ttlMs) {
|
|
94
|
-
|
|
108
|
+
if (classifyGap(installed, cached.latest).severity === "none") {
|
|
109
|
+
return { installed, latest: cached.latest, source: "cache", checkedAgoMs: nowMs - cached.checkedAt };
|
|
110
|
+
}
|
|
111
|
+
// The cached answer would print a nudge — fall through to one fresh
|
|
112
|
+
// fetch so we present a CURRENT fact when possible. The failure path
|
|
113
|
+
// below still falls back to this same cache (offline tolerance intact).
|
|
95
114
|
}
|
|
96
115
|
// Defense-in-depth: the default fetchLatest already catches everything
|
|
97
116
|
// internally (network error, timeout, non-2xx, bad JSON) and resolves
|
|
@@ -109,10 +128,10 @@ export async function checkVersion(installed, injected = {}) {
|
|
|
109
128
|
deps.writeCache(deps.cachePath, { latest: fetched, checkedAt: nowMs });
|
|
110
129
|
return { installed, latest: fetched, source: "network" };
|
|
111
130
|
}
|
|
112
|
-
// Registry unreachable/timed out — fall back to
|
|
131
|
+
// Registry unreachable/timed out — fall back to the cache rather than
|
|
113
132
|
// reporting nothing, but never block or throw trying to get a fresh one.
|
|
114
133
|
if (cached) {
|
|
115
|
-
return { installed, latest: cached.latest, source: "cache" };
|
|
134
|
+
return { installed, latest: cached.latest, source: "cache", checkedAgoMs: nowMs - cached.checkedAt };
|
|
116
135
|
}
|
|
117
136
|
return { installed, latest: null, source: "unavailable" };
|
|
118
137
|
}
|
|
@@ -129,7 +148,7 @@ export function primeVersionCheckCache(latest, injected = {}) {
|
|
|
129
148
|
const now = injected.now ?? (() => Date.now());
|
|
130
149
|
writeCacheFile(cachePath, { latest, checkedAt: now() });
|
|
131
150
|
}
|
|
132
|
-
const NO_GAP = { severity: "none", majorBehind: false,
|
|
151
|
+
const NO_GAP = { severity: "none", majorBehind: false, unit: null, versionsBehind: 0 };
|
|
133
152
|
/**
|
|
134
153
|
* Classify how far `installed` is behind `latest` using major.minor.patch
|
|
135
154
|
* math only — we don't have advisory data, so:
|
|
@@ -146,24 +165,43 @@ export function classifyGap(installed, latest) {
|
|
|
146
165
|
const [aMaj, aMin, aPatch] = a;
|
|
147
166
|
const [bMaj, bMin, bPatch] = b;
|
|
148
167
|
if (bMaj > aMaj)
|
|
149
|
-
return { severity: "red", majorBehind: true,
|
|
168
|
+
return { severity: "red", majorBehind: true, unit: null, versionsBehind: 0 };
|
|
150
169
|
if (bMaj < aMaj)
|
|
151
170
|
return NO_GAP; // installed is ahead (e.g. local/pre-release build)
|
|
152
171
|
if (bMin > aMin) {
|
|
153
|
-
const
|
|
154
|
-
return { severity:
|
|
172
|
+
const versionsBehind = bMin - aMin;
|
|
173
|
+
return { severity: versionsBehind >= 2 ? "red" : "yellow", majorBehind: false, unit: "minor", versionsBehind };
|
|
155
174
|
}
|
|
156
175
|
if (bMin < aMin)
|
|
157
176
|
return NO_GAP; // ahead on minor
|
|
158
|
-
if (bPatch > aPatch)
|
|
159
|
-
return { severity: "yellow", majorBehind: false,
|
|
177
|
+
if (bPatch > aPatch) {
|
|
178
|
+
return { severity: "yellow", majorBehind: false, unit: "patch", versionsBehind: bPatch - aPatch };
|
|
179
|
+
}
|
|
160
180
|
return NO_GAP; // equal, or ahead on patch
|
|
161
181
|
}
|
|
182
|
+
/** Compact human age for "checked … ago" — coarse on purpose (a nudge, not a log). */
|
|
183
|
+
function formatCheckedAgo(ms) {
|
|
184
|
+
const minutes = Math.round(ms / 60_000);
|
|
185
|
+
if (minutes < 60)
|
|
186
|
+
return `${Math.max(1, minutes)}m`;
|
|
187
|
+
const hours = Math.round(ms / 3_600_000);
|
|
188
|
+
if (hours < 48)
|
|
189
|
+
return `${hours}h`;
|
|
190
|
+
return `${Math.round(ms / 86_400_000)}d`;
|
|
191
|
+
}
|
|
162
192
|
/**
|
|
163
193
|
* Build the human-readable nudge line for `flair status`/`flair doctor`, or
|
|
164
194
|
* null when there's nothing worth printing — current, ahead (local/dev
|
|
165
195
|
* build), or we couldn't determine latest at all (offline with no cache).
|
|
166
196
|
* Callers own icon/color; this returns plain text plus a severity to color by.
|
|
197
|
+
*
|
|
198
|
+
* flair#1341 honest-numbers contract:
|
|
199
|
+
* - A cache-sourced answer is labelled as such ("latest known (checked 9h
|
|
200
|
+
* ago): X"), never stated as current fact.
|
|
201
|
+
* - The count names its unit ("N minor versions behind" / "M patch
|
|
202
|
+
* releases behind") — it must say what classifyGap actually counted.
|
|
203
|
+
* - The suggested command is our paved path, `flair upgrade` (refreshes
|
|
204
|
+
* MCP pins, verifies restart — see flair#1324), not a bare npm install.
|
|
167
205
|
*/
|
|
168
206
|
export function formatVersionNudge(result) {
|
|
169
207
|
if (!result.latest)
|
|
@@ -171,11 +209,19 @@ export function formatVersionNudge(result) {
|
|
|
171
209
|
const gap = classifyGap(result.installed, result.latest);
|
|
172
210
|
if (gap.severity === "none")
|
|
173
211
|
return null;
|
|
212
|
+
const latestClaim = result.source === "cache"
|
|
213
|
+
? result.checkedAgoMs != null
|
|
214
|
+
? `latest known (checked ${formatCheckedAgo(result.checkedAgoMs)} ago): ${result.latest}`
|
|
215
|
+
: `latest known: ${result.latest}`
|
|
216
|
+
: `latest is ${result.latest}`;
|
|
217
|
+
const plural = gap.versionsBehind === 1 ? "" : "s";
|
|
174
218
|
const countHint = gap.majorBehind
|
|
175
|
-
? "major version"
|
|
176
|
-
:
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
? "major version behind"
|
|
220
|
+
: gap.unit === "patch"
|
|
221
|
+
? `${gap.versionsBehind} patch release${plural} behind`
|
|
222
|
+
: `${gap.versionsBehind} minor version${plural} behind`;
|
|
223
|
+
const message = `flair ${result.installed} is behind — ${latestClaim} (${countHint}). ` +
|
|
224
|
+
`Run: flair upgrade`;
|
|
179
225
|
return { severity: gap.severity, message };
|
|
180
226
|
}
|
|
181
227
|
/**
|
package/docs/auth.md
CHANGED
|
@@ -39,6 +39,11 @@ flair agent add myagent
|
|
|
39
39
|
|
|
40
40
|
This is the default and recommended auth for single-instance deployments.
|
|
41
41
|
|
|
42
|
+
Adapter or hosted-Flair 404? Identity is three things (keyfile + agent id +
|
|
43
|
+
the `Agent` row on **that** instance). A by-id 404 is fail-closed ownership,
|
|
44
|
+
never an existence signal. The adapter write-up is
|
|
45
|
+
[integrations.md — Hosted Flair auth](integrations.md#hosted-flair-auth--your-agent-got-a-404).
|
|
46
|
+
|
|
42
47
|
## Deployment shapes: personal vs org
|
|
43
48
|
|
|
44
49
|
Flair has no `mode`/`shape` config setting — the shape you get is emergent from *how you provision principals*, not something you declare:
|
package/docs/claude-code.md
CHANGED
|
@@ -43,10 +43,15 @@ Copy this into your project's `CLAUDE.md` (or `.claude/settings.md`, `AGENTS.md`
|
|
|
43
43
|
|
|
44
44
|
Run this FIRST, before doing anything else:
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
mcp__flair__bootstrap
|
|
47
47
|
|
|
48
|
+
(`mcp__flair__bootstrap` is Claude Code's namespaced name for the server's `bootstrap` tool.)
|
|
48
49
|
Read the output — that's your soul and recent memories.
|
|
49
50
|
|
|
51
|
+
Use the CLI variant when MCP is not wired — previewing context yourself, a script, or any agent that can run a shell command:
|
|
52
|
+
|
|
53
|
+
flair bootstrap --agent my-project --max-tokens 4000
|
|
54
|
+
|
|
50
55
|
### During work
|
|
51
56
|
|
|
52
57
|
- Remember something: `flair memory add --agent my-project --content "what you learned"`
|
|
@@ -121,10 +126,12 @@ export FLAIR_URL=http://localhost:19926 # default, only needed if custom
|
|
|
121
126
|
Then the CLAUDE.md simplifies to:
|
|
122
127
|
|
|
123
128
|
## Memory
|
|
124
|
-
- Bootstrap: `
|
|
129
|
+
- Bootstrap: `mcp__flair__bootstrap`
|
|
125
130
|
- Remember: `flair memory add --content "what you learned"`
|
|
126
131
|
- Search: `flair search "your query"`
|
|
127
132
|
|
|
133
|
+
Use `flair bootstrap` when MCP is not wired.
|
|
134
|
+
|
|
128
135
|
## Soul (Personality / Context)
|
|
129
136
|
|
|
130
137
|
Want Claude Code to have consistent personality or project context? Set soul entries:
|
|
@@ -143,7 +150,7 @@ flair soul set --agent my-project --key review \
|
|
|
143
150
|
--value "Check for: error handling, edge cases, performance implications, security."
|
|
144
151
|
```
|
|
145
152
|
|
|
146
|
-
Soul entries are included in every
|
|
153
|
+
Soul entries are included in every bootstrap — they're the persistent context that shapes how Claude Code thinks about your project.
|
|
147
154
|
|
|
148
155
|
## Remote Flair
|
|
149
156
|
|
package/docs/deepseek-harness.md
CHANGED
|
@@ -4,7 +4,7 @@ Give DeepSeek Harness (DSH) sessions persistent, portable memory — no plugin c
|
|
|
4
4
|
|
|
5
5
|
> **Verified against DSH as of 2026-08-20** (`deepseek-ai/deepseek-harness`, branch `master`). DSH is a developer preview and its own README promises compatibility-breaking changes. If wiring fails after a DSH upgrade, re-check the config field names against [their MCP client README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md) before suspecting Flair.
|
|
6
6
|
|
|
7
|
-
The same
|
|
7
|
+
The same twelve tools every other MCP client gets ([full table in mcp-clients.md](mcp-clients.md#what-the-mcp-server-exposes)) appear to the model under DSH's server-qualified names: `mcp__flair__memory_search`, `mcp__flair__memory_store`, `mcp__flair__bootstrap`, and so on — the same `mcp__<server>__<tool>` convention Claude Code uses.
|
|
8
8
|
|
|
9
9
|
Two caveats up front, both structural to DSH's bridge (details below):
|
|
10
10
|
|
package/docs/deployment.md
CHANGED
|
@@ -108,7 +108,7 @@ EXPOSE 19926
|
|
|
108
108
|
CMD ["flair", "start", "--foreground"]
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Note: embeddings run on CPU in Docker (no Metal acceleration). Performance is acceptable for small-to-medium memory stores (< 10K memories).
|
|
111
|
+
Note: embeddings run on CPU in Docker (no Metal acceleration). Performance is acceptable for small-to-medium memory stores (< 10K memories). Thread count follows `FLAIR_EMBED_THREADS` (default `max(1, availableParallelism() − 1)`); pin it if the container CPU quota and the host you want to use disagree.
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
@@ -187,6 +187,16 @@ Set these in the Flair process environment (`~/Library/LaunchAgents/ai.tpsdev.fl
|
|
|
187
187
|
| `HTTP_PORT` | Override the Harper HTTP port. Useful for sandboxes; production deployments should configure the port in `config.yaml` instead. | Rare. |
|
|
188
188
|
| `FLAIR_OPS_BIND` | Bind address for the Harper **ops API**. Resolution order: `flair init --ops-bind` > this variable > the `opsBind` key `flair init` persists in `~/.flair/config.yaml` > `127.0.0.1`. Every Flair-managed Harper start re-asserts the resolved value, so the persisted key is what makes a choice survive `flair restart` / `flair upgrade`. | Only for deployments that genuinely need remote ops admin (multi-host / Fabric) — set it to `0.0.0.0`, or record it once with `flair init --ops-bind 0.0.0.0`. Single-host installs want the loopback default. |
|
|
189
189
|
|
|
190
|
+
### Performance-related environment variables
|
|
191
|
+
|
|
192
|
+
These are read by the Harper process at boot (same places as the table above: launchd plist, systemd unit, component `.env` / Fabric env). They are **not** `config.yaml` keys — embedding registration is in-process and must not persist into Harper's config file.
|
|
193
|
+
|
|
194
|
+
| Variable | Default | What it does |
|
|
195
|
+
|----------|---------|--------------|
|
|
196
|
+
| `FLAIR_EMBED_THREADS` | `max(1, availableParallelism() − 1)` | CPU threads for in-process embedding (harper-fabric-embeddings / llama.cpp). Host-aware so a 4-core box does not inherit HFE's fixed 6, and an 8-vCPU ingest host is not stuck at 6 idle cores. One core is left for Harper's event loop and the OS. `availableParallelism()` respects a container CPU quota. Set a positive integer to pin. Invalid values fall back to the default. |
|
|
197
|
+
| `FLAIR_HYBRID_RETRIEVAL` | `true` | Hybrid BM25 + vector retrieval. Set `false` / `0` / `off` to revert to the legacy HNSW + keyword-bump path. |
|
|
198
|
+
| `FLAIR_MODELS_DIR` | `<data-dir>/models` | Directory the embedding GGUF is loaded from (and downloaded into on first boot). Point this at a pre-seeded directory to skip the HuggingFace download; see [troubleshooting.md](troubleshooting.md). |
|
|
199
|
+
|
|
190
200
|
---
|
|
191
201
|
|
|
192
202
|
## Backup & Restore
|
package/docs/hosted-on-fabric.md
CHANGED
|
@@ -138,6 +138,8 @@ flair agent add mybot --target "$FLAIR_URL" --ops-target <ops-url>
|
|
|
138
138
|
|
|
139
139
|
Auth is the same protocol as standalone: Ed25519 signature of `agentId:timestamp:nonce:METHOD:/path`, 30-second replay window, nonce deduplication. The difference is purely the transport — HTTPS instead of localhost HTTP.
|
|
140
140
|
|
|
141
|
+
An adapter that just got a 404 is almost never "Harper wants a different verb." Check the three identity pieces (keyfile, agent id, `Agent` row on **this** instance) and treat by-id 404 as fail-closed ownership, not an existence signal: [integrations.md — Hosted Flair auth](integrations.md#hosted-flair-auth--your-agent-got-a-404).
|
|
142
|
+
|
|
141
143
|
See [secrets-and-keys.md](secrets-and-keys.md) for the full threat model.
|
|
142
144
|
|
|
143
145
|
---
|
package/docs/integrations.md
CHANGED
|
@@ -22,7 +22,9 @@ Where Flair already runs. Each integration shown here is a working surface — t
|
|
|
22
22
|
| **OpenClaw** | [`openclaw-flair`](#openclaw) | Ed25519 | Native plugin + context engine |
|
|
23
23
|
| **n8n** | [`n8n-nodes-flair`](#n8n) | FlairApi credential | Three nodes (chat memory, search, store) |
|
|
24
24
|
| **Hermes Agent** | [`hermes-flair`](#hermes-agent) | Ed25519 | Python `MemoryProvider` |
|
|
25
|
-
| **Pi agent** | [`pi-flair`](#pi-agent) | Ed25519 |
|
|
25
|
+
| **Pi agent** | [`pi-flair`](#pi-agent) | Ed25519 | Native pi extension (pi has no MCP support); `flair init --client pi` wires it, `flair doctor` checks it |
|
|
26
|
+
| **Google ADK** (Python) | [`adk-flair`](../packages/adk-flair/README.md) | Ed25519 | `BaseMemoryService`; see [hosted auth](#hosted-flair-auth--your-agent-got-a-404) if you just got a 404 |
|
|
27
|
+
| **Google ADK** (JS/TS) | [`@tpsdev-ai/adk-flair`](../packages/adk-flair-js/README.md) | Ed25519 | Same identity model as the Python package |
|
|
26
28
|
|
|
27
29
|
Don't see your harness? If it speaks **MCP** — Flair already works with `flair-mcp`. If it has a **custom memory protocol** like LangGraph's `BaseStore` or CrewAI's `RAGStorage`, an adapter is a ~200-line package; [open an issue](https://github.com/tpsdev-ai/flair/issues) or [send a PR](https://github.com/tpsdev-ai/flair).
|
|
28
30
|
|
|
@@ -32,6 +34,56 @@ Don't see your harness? If it speaks **MCP** — Flair already works with `flair
|
|
|
32
34
|
|
|
33
35
|
---
|
|
34
36
|
|
|
37
|
+
## Hosted Flair auth — your agent got a 404
|
|
38
|
+
|
|
39
|
+
Written for the person whose adapter just got a 404 against a hosted Flair (Harper Fabric or any non-localhost URL). Laptop `flair init` on `127.0.0.1:19926` is a different machine from the one signing the request.
|
|
40
|
+
|
|
41
|
+
The protocol lives in [auth.md](auth.md). Key lifecycle lives in [secrets-and-keys.md](secrets-and-keys.md). Fabric registration (including the ops port trap) lives in [quickstart-fabric.md](quickstart-fabric.md). This section is only the identity check.
|
|
42
|
+
|
|
43
|
+
### Identity is three things that must match
|
|
44
|
+
|
|
45
|
+
Ed25519 agent auth is not a password. The server accepts a request only when all three line up:
|
|
46
|
+
|
|
47
|
+
1. **Agent id** — `FLAIR_AGENT_ID`. This is the string inside the signature.
|
|
48
|
+
2. **Keyfile on the machine that signs** — `FLAIR_KEYFILE` (adk-flair / adk-flair-js) or `FLAIR_KEY_PATH` (flair-mcp, Hermes, pi). The private key never leaves that host. `flair agent add` writes `~/.flair/keys/<id>.key`.
|
|
49
|
+
3. **Server-side `Agent` row on the instance at `FLAIR_URL`** whose `publicKey` matches that keyfile. Registration is per instance. A key minted against localhost is not registered on Fabric until you run `flair agent add <id> --target` at that URL.
|
|
50
|
+
|
|
51
|
+
The signed payload is `agentId:timestamp:nonce:METHOD:/path` (30-second replay window).
|
|
52
|
+
|
|
53
|
+
Do not "fix" a 401 or 404 by pasting the Harper admin password into the agent's standing environment. Admin Basic auth is for registration, once. The first signed call against an unregistered id is **401 `unknown_agent`** — that is fail-closed working as designed.
|
|
54
|
+
|
|
55
|
+
### The three failure shapes
|
|
56
|
+
|
|
57
|
+
| Shape | What is true | What you see | What to do |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| **Record missing** | No `Agent` row for this id on **this** instance | `401 {"error":"unknown_agent"}` on every signed route | Register against the hosted URL: `flair agent add <id> --target "$FLAIR_URL" --ops-target <ops-url> --admin-pass-file <path>`. Fabric ops is not `data-port − 1` — see [quickstart-fabric.md](quickstart-fabric.md). |
|
|
60
|
+
| **Key mismatch** | The id exists; the public key on the server is not the one in your keyfile | `401 {"error":"invalid_signature"}` | Same id, wrong key — copied from another host, rotated on one side only, or the env pointing at a different agent's file. Point the env at the key that matches **this** instance, or re-seed the hosted `Agent` row from the key on this machine: `flair agent add <id> --target "$FLAIR_URL" --ops-target <ops-url> --admin-pass-file <path>` (reuses the local keyfile). `flair agent rotate-key` is localhost-only. Restart the adapter so it reloads the key. |
|
|
61
|
+
| **Config wrong** | Identity may be fine; you are not talking to the Flair you think | Timeouts, connection errors, or **404 from Harper's catch-all** | `FLAIR_URL` must be the origin the **signing process** can open (cloud-agent localhost is the VM, not your laptop). adk-flair also needs `FLAIR_ALLOW_REMOTE_URL=1` and a raised `FLAIR_HTTP_TIMEOUT` (defaults are localhost fail-fast). A `FLAIR_URL` with a path prefix sends every request to a route that does not exist. `/Health` can be 200 while `/Memory` is still 404 if the Flair app is not loaded yet. |
|
|
62
|
+
|
|
63
|
+
Clock skew is a fourth, rarer 401: `timestamp_out_of_window`.
|
|
64
|
+
|
|
65
|
+
`flair agent list` talks to **localhost**. It cannot tell you whether the hosted instance has your Agent row. The discriminator is the 401 body on a signed request.
|
|
66
|
+
|
|
67
|
+
### 404 on by-id routes is not an existence signal
|
|
68
|
+
|
|
69
|
+
`GET /Memory/{id}`, `PUT /Memory/{id}`, and the adapter/MCP wrappers (`memory_get`, a by-id update) return **404** both when the id is absent **and** when the record exists but your principal may not see it (another agent's `private` memory, or outside your read scope). Same status, same body shape. That is fail-closed ownership ([flair#1264](https://github.com/tpsdev-ai/flair/issues/1264)) — a 403 would confirm the id exists and can name the owner.
|
|
70
|
+
|
|
71
|
+
**Do not treat that 404 as "the record is missing, so create it" or "Harper rejected the verb."** Creates go to `POST /Memory/` (id in the body). A by-id 404 after a write usually means you are not the owner the server thinks you are — go back to the three shapes above — not that you should switch PUT for POST.
|
|
72
|
+
|
|
73
|
+
A verified agent that is not allowed the row gets 404, never 403. Anonymous by-id reads are denied at the gate.
|
|
74
|
+
|
|
75
|
+
### Per-adapter env
|
|
76
|
+
|
|
77
|
+
| Adapter | URL | Agent id | Keyfile | Hosted extras |
|
|
78
|
+
|---|---|---|---|---|
|
|
79
|
+
| **adk-flair** / **adk-flair-js** | `FLAIR_URL` | `FLAIR_AGENT_ID` | `FLAIR_KEYFILE` | `FLAIR_ALLOW_REMOTE_URL=1`, `FLAIR_HTTP_TIMEOUT` — [adk-flair README](../packages/adk-flair/README.md#hosted-flair) |
|
|
80
|
+
| **flair-mcp** / Cursor plugin | `FLAIR_URL` | `FLAIR_AGENT_ID` | `FLAIR_KEY_PATH` (optional; auto-resolved) | Key must be on the **npx host** |
|
|
81
|
+
| **Hermes / pi / LangGraph** | `FLAIR_URL` | `FLAIR_AGENT_ID` | `FLAIR_KEY_PATH` or client `keyPath` | Same Ed25519 model |
|
|
82
|
+
|
|
83
|
+
n8n still uses Harper admin Basic auth — it is not this path. See [n8n.md](n8n.md#security).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
35
87
|
## Claude Code, Cursor, Codex, Gemini CLI, Continue.dev, Goose — via `flair-mcp`
|
|
36
88
|
|
|
37
89
|
[`@tpsdev-ai/flair-mcp`](https://www.npmjs.com/package/@tpsdev-ai/flair-mcp) is a [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes Flair as a memory tool to any MCP-speaking client. One server, every MCP client.
|
|
@@ -168,13 +220,34 @@ Auth: TPS-Ed25519 (the same model the rest of Flair uses) — writes are isolate
|
|
|
168
220
|
|
|
169
221
|
## Pi agent
|
|
170
222
|
|
|
171
|
-
[`@tpsdev-ai/pi-flair`](https://www.npmjs.com/package/@tpsdev-ai/pi-flair) is the
|
|
223
|
+
[`@tpsdev-ai/pi-flair`](https://www.npmjs.com/package/@tpsdev-ai/pi-flair) is the **native pi extension** for the [Pi coding agent](https://github.com/mariozechner/pi-coding-agent) — pi has no MCP client support, so this is a first-party plugin, not an MCP bridge. Memory + identity (`memory_search`, `memory_store`, `bootstrap`) for the pi runtime.
|
|
224
|
+
|
|
225
|
+
Wire it (either form is equivalent):
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
flair init --client pi # writes a pinned "packages" entry into ~/.pi/agent/settings.json
|
|
229
|
+
# or
|
|
230
|
+
pi install npm:@tpsdev-ai/pi-flair
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Which produces:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"packages": ["npm:@tpsdev-ai/pi-flair@<version>"]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Known trap:** the `extensions` settings key takes local file paths only — an `npm:` spec there is *silently ignored* by pi, so the tools never register ([#1346](https://github.com/tpsdev-ai/flair/issues/1346)). Package sources belong under `packages`. `flair doctor` detects pi, verifies the wiring, calls this exact misconfiguration out, and `flair doctor --fix` moves the entry.
|
|
242
|
+
|
|
243
|
+
pi settings carry no per-package env, so identity comes from the environment that launches pi:
|
|
172
244
|
|
|
173
245
|
```bash
|
|
174
|
-
|
|
246
|
+
export FLAIR_AGENT_ID=my-agent # per host/purpose
|
|
247
|
+
pi
|
|
175
248
|
```
|
|
176
249
|
|
|
177
|
-
|
|
250
|
+
Full details (tools, env reference, auto-recall/auto-capture flags, security notes): [`packages/pi-flair/README.md`](../packages/pi-flair/README.md).
|
|
178
251
|
|
|
179
252
|
---
|
|
180
253
|
|
|
@@ -188,7 +261,6 @@ If it has a custom memory protocol, the adapter pattern is small (~200 lines). L
|
|
|
188
261
|
- CrewAI (Python `BaseRAGStorage` protocol)
|
|
189
262
|
- AG2 / AutoGen (Python)
|
|
190
263
|
- Mastra (TS, denser thread model)
|
|
191
|
-
- ADK (Google, Python + TS)
|
|
192
264
|
|
|
193
265
|
[Open an issue](https://github.com/tpsdev-ai/flair/issues) describing the harness and we'll triage. PRs welcome — see [`packages/langgraph-flair`](../packages/langgraph-flair) as the smallest-shape reference.
|
|
194
266
|
|
|
@@ -198,6 +270,7 @@ If it has a custom memory protocol, the adapter pattern is small (~200 lines). L
|
|
|
198
270
|
|
|
199
271
|
- [Quickstart](quickstart.md) — `flair init` to working memory on a laptop
|
|
200
272
|
- [Fabric Quickstart](quickstart-fabric.md) — `flair deploy` to a reachable Harper Fabric URL
|
|
273
|
+
- [Hosted Flair auth](#hosted-flair-auth--your-agent-got-a-404) — Ed25519 identity, the three 401/404 shapes, why a by-id 404 is not an existence signal
|
|
201
274
|
- [Embedding in a Harper app](embedding-in-a-harper-app.md) — run Flair as a component of your own Harper instance and call it in-process
|
|
202
275
|
- [Memory bridges](bridges.md) — import/export Flair ↔ Mem0, ChatGPT, claude-project, markdown, agentic-stack (five bridges shipped)
|
|
203
276
|
- [Federation](federation.md) — pair instances peer-to-peer for cross-machine sync
|