@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
package/dist/install/clients.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// ─── Client Detection & Wiring ──────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
// Detects locally installed
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Detects locally installed clients and wires them to Flair. Most are MCP
|
|
4
|
+
// clients (kind: "mcp" — wired via an mcpServers block/TOML table running
|
|
5
|
+
// @tpsdev-ai/flair-mcp); pi is a native-extension host (kind:
|
|
6
|
+
// "native-extension" — wired via pi's own settings.json `packages` key,
|
|
7
|
+
// flair#1342). Each client has:
|
|
8
|
+
// - detection: `bin` on PATH, optionally widened by a declared detect() override
|
|
9
|
+
// - wire(env): { ok: boolean; message: string }
|
|
7
10
|
//
|
|
8
11
|
// Wiring contract (FIX 4 — onboarding dogfood round 1):
|
|
9
12
|
// "wired" MUST mean a config file was actually written. A wire function returns
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
21
|
import { homedir } from "node:os";
|
|
19
22
|
import { dirname, join } from "node:path";
|
|
20
|
-
import { mcpServerSpec } from "../lib/mcp-spec.js";
|
|
23
|
+
import { flairCliVersion, isResolvedVersion, mcpServerSpec } from "../lib/mcp-spec.js";
|
|
21
24
|
/**
|
|
22
25
|
* Resolve the user's home dir. Prefer the live HOME/USERPROFILE env over
|
|
23
26
|
* os.homedir(), which caches the value at process start and so ignores a
|
|
@@ -263,6 +266,282 @@ function geminiConfigPath() {
|
|
|
263
266
|
function codexConfigPath() {
|
|
264
267
|
return join(resolveHome(), ".codex", "config.toml");
|
|
265
268
|
}
|
|
269
|
+
// ---- pi (native extension — NOT an MCP client) ------------------------------------
|
|
270
|
+
//
|
|
271
|
+
// pi has no MCP client support (packages/pi-flair/README "Design Decision"), so
|
|
272
|
+
// Flair ships @tpsdev-ai/pi-flair as a NATIVE pi extension. Wiring pi therefore
|
|
273
|
+
// means editing pi's OWN settings, not writing an mcpServers block (flair#1342):
|
|
274
|
+
//
|
|
275
|
+
// ~/.pi/agent/settings.json (user scope; pi's getSettingsPath() —
|
|
276
|
+
// agent dir overridable via the
|
|
277
|
+
// PI_CODING_AGENT_DIR env var, honored here)
|
|
278
|
+
// <project>/.pi/settings.json (project scope)
|
|
279
|
+
//
|
|
280
|
+
// Two settings keys matter, and confusing them is the flair#1346 field failure:
|
|
281
|
+
//
|
|
282
|
+
// "packages" — package SOURCES (`npm:`, `git:`, local paths, or
|
|
283
|
+
// { source, ...filters } objects). pi parses these through
|
|
284
|
+
// parseSource() and AUTO-INSTALLS a missing/mismatched npm
|
|
285
|
+
// package at resource collection (package-manager.js
|
|
286
|
+
// resolvePackageSources), honoring an exact `@<version>` pin.
|
|
287
|
+
// This is where npm:@tpsdev-ai/pi-flair belongs.
|
|
288
|
+
// "extensions" — local FILE PATHS only. An `npm:` spec here is treated as a
|
|
289
|
+
// path, fails existsSync, and is dropped WITHOUT ERROR — the
|
|
290
|
+
// user believes they are wired and pi registers zero tools.
|
|
291
|
+
// (Verified against pi 0.84.2's package-manager.js: the
|
|
292
|
+
// extensions override list feeds resolvePathFromBase/
|
|
293
|
+
// collectFilesFromPaths, never parseSource.)
|
|
294
|
+
//
|
|
295
|
+
// pi-flair reads FLAIR_AGENT_ID / FLAIR_URL / FLAIR_KEY_PATH from the process
|
|
296
|
+
// environment of the pi that loads it — pi settings carry NO per-package env
|
|
297
|
+
// block, so wiring here cannot pin an agent identity the way the MCP clients'
|
|
298
|
+
// env blocks do. Wire messages say so instead of pretending.
|
|
299
|
+
/** The npm package pi loads as its Flair extension. */
|
|
300
|
+
export const PI_FLAIR_PACKAGE = "@tpsdev-ai/pi-flair";
|
|
301
|
+
/**
|
|
302
|
+
* pi-flair's own DEFAULT_FLAIR_URL (packages/pi-flair/src/index.ts). Duplicated
|
|
303
|
+
* as a value rather than imported — the CLI does not depend on the pi-flair
|
|
304
|
+
* workspace package — by the same convention as doctor-client's
|
|
305
|
+
* FLAIR_CLIENT_DEFAULT_URL; a unit test (pi-client.test.ts) asserts this
|
|
306
|
+
* literal matches pi-flair's source so the two cannot drift silently.
|
|
307
|
+
*/
|
|
308
|
+
export const PI_FLAIR_DEFAULT_URL = "http://127.0.0.1:19926";
|
|
309
|
+
/**
|
|
310
|
+
* The `packages` entry a wired pi gets. PINNED for the same reason as
|
|
311
|
+
* mcpServerSpec (flair#907): pi re-resolves an unpinned npm source to latest
|
|
312
|
+
* when (re)installing, and pi-flair ships in version lockstep with the CLI.
|
|
313
|
+
* Falls back to the bare spec when the CLI cannot read its own version — the
|
|
314
|
+
* same condition and caller-owed warning as mcpServerSpec.
|
|
315
|
+
*/
|
|
316
|
+
export function piFlairSpec(version = flairCliVersion()) {
|
|
317
|
+
return isResolvedVersion(version)
|
|
318
|
+
? `npm:${PI_FLAIR_PACKAGE}@${version}`
|
|
319
|
+
: `npm:${PI_FLAIR_PACKAGE}`;
|
|
320
|
+
}
|
|
321
|
+
/** pi's agent config dir: $PI_CODING_AGENT_DIR, else ~/.pi/agent (pi config.js). */
|
|
322
|
+
function piAgentDir() {
|
|
323
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
324
|
+
if (envDir)
|
|
325
|
+
return envDir;
|
|
326
|
+
return join(resolveHome(), ".pi", "agent");
|
|
327
|
+
}
|
|
328
|
+
/** pi user-scope settings: <agent dir>/settings.json. */
|
|
329
|
+
export function piSettingsPath() {
|
|
330
|
+
return join(piAgentDir(), "settings.json");
|
|
331
|
+
}
|
|
332
|
+
/** A pi `packages` array entry is a source string or `{ source, ...filters }`. */
|
|
333
|
+
export function piPackageEntrySource(entry) {
|
|
334
|
+
if (typeof entry === "string")
|
|
335
|
+
return entry;
|
|
336
|
+
if (entry && typeof entry === "object" && typeof entry.source === "string") {
|
|
337
|
+
return entry.source;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
/** Does this source string name the pi-flair package as an npm source
|
|
342
|
+
* (bare `npm:@tpsdev-ai/pi-flair` or any `npm:@tpsdev-ai/pi-flair@<spec>`)? */
|
|
343
|
+
export function isPiFlairNpmSource(source) {
|
|
344
|
+
if (typeof source !== "string" || !source.startsWith("npm:"))
|
|
345
|
+
return false;
|
|
346
|
+
const spec = source.slice("npm:".length).trim();
|
|
347
|
+
return spec === PI_FLAIR_PACKAGE || spec.startsWith(`${PI_FLAIR_PACKAGE}@`);
|
|
348
|
+
}
|
|
349
|
+
/** The version text of a pinned pi-flair npm source, or null when bare. */
|
|
350
|
+
export function extractPiFlairPin(source) {
|
|
351
|
+
if (!isPiFlairNpmSource(source))
|
|
352
|
+
return null;
|
|
353
|
+
const spec = source.slice("npm:".length).trim();
|
|
354
|
+
const version = spec.slice(`${PI_FLAIR_PACKAGE}@`.length);
|
|
355
|
+
return spec.startsWith(`${PI_FLAIR_PACKAGE}@`) && version ? version : null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Does this `extensions` entry point at pi-flair BY PATH (the documented
|
|
359
|
+
* pre-0.49 workaround: a local path to the installed dist/index.js)? A
|
|
360
|
+
* substring heuristic on the package/directory name — the entry is user-
|
|
361
|
+
* written free text, so this is deliberately loose in the direction of
|
|
362
|
+
* REPORTING (doctor names the entry it matched); it never gates anything
|
|
363
|
+
* destructive.
|
|
364
|
+
*/
|
|
365
|
+
export function isPiFlairExtensionPath(entry) {
|
|
366
|
+
if (typeof entry !== "string" || entry.startsWith("npm:") || entry.startsWith("git:"))
|
|
367
|
+
return false;
|
|
368
|
+
return entry.includes("pi-flair");
|
|
369
|
+
}
|
|
370
|
+
export function scanPiSettings(raw) {
|
|
371
|
+
const empty = { parsed: false, pinnedVersion: null, extensionFilePaths: [], misconfiguredNpmUnderExtensions: [] };
|
|
372
|
+
if (!raw || !raw.trim())
|
|
373
|
+
return empty;
|
|
374
|
+
let config;
|
|
375
|
+
try {
|
|
376
|
+
config = JSON.parse(raw);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return empty;
|
|
380
|
+
}
|
|
381
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
382
|
+
return empty;
|
|
383
|
+
const cfg = config;
|
|
384
|
+
let packagesSpec;
|
|
385
|
+
let pinnedVersion = null;
|
|
386
|
+
if (Array.isArray(cfg.packages)) {
|
|
387
|
+
for (const entry of cfg.packages) {
|
|
388
|
+
const source = piPackageEntrySource(entry);
|
|
389
|
+
if (source && isPiFlairNpmSource(source)) {
|
|
390
|
+
packagesSpec = source;
|
|
391
|
+
pinnedVersion = extractPiFlairPin(source);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const extensionFilePaths = [];
|
|
397
|
+
const misconfiguredNpmUnderExtensions = [];
|
|
398
|
+
if (Array.isArray(cfg.extensions)) {
|
|
399
|
+
for (const entry of cfg.extensions) {
|
|
400
|
+
if (typeof entry !== "string")
|
|
401
|
+
continue;
|
|
402
|
+
if (isPiFlairNpmSource(entry))
|
|
403
|
+
misconfiguredNpmUnderExtensions.push(entry);
|
|
404
|
+
else if (isPiFlairExtensionPath(entry))
|
|
405
|
+
extensionFilePaths.push(entry);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { parsed: true, packagesSpec, pinnedVersion, extensionFilePaths, misconfiguredNpmUnderExtensions };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve a pi `extensions` path entry the way pi's loader will: `~/` against
|
|
412
|
+
* the home dir, a relative path against the settings file's own base dir, an
|
|
413
|
+
* absolute path as-is. `baseDir` is the directory pi treats as the scope's
|
|
414
|
+
* base (user scope: the agent dir).
|
|
415
|
+
*/
|
|
416
|
+
export function resolvePiExtensionPath(entry, homeDir, baseDir) {
|
|
417
|
+
if (entry.startsWith("~/") || entry === "~")
|
|
418
|
+
return join(homeDir, entry.slice(1));
|
|
419
|
+
if (entry.startsWith("/"))
|
|
420
|
+
return entry;
|
|
421
|
+
return join(baseDir, entry);
|
|
422
|
+
}
|
|
423
|
+
/** Pretty-printed minimal settings snippet for copy-paste fallbacks. */
|
|
424
|
+
function piJsonSnippet() {
|
|
425
|
+
return JSON.stringify({ packages: [piFlairSpec()] }, null, 2);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Wire pi by editing ~/.pi/agent/settings.json `packages` (flair#1342) — the
|
|
429
|
+
* same merge/idempotence/preservation contract as wireJsonMcp: sibling keys
|
|
430
|
+
* and entries survive byte-identical, a current entry is a no-op, a stale pin
|
|
431
|
+
* is refreshed, and ok:true means the file was actually written (or already
|
|
432
|
+
* correct). Two pi-specific rules on top:
|
|
433
|
+
*
|
|
434
|
+
* • an `npm:` pi-flair spec under `extensions` is MOVED to `packages` — that
|
|
435
|
+
* misplacement is silently ignored by pi (flair#1346), so leaving it while
|
|
436
|
+
* adding a packages entry would preserve a decoy;
|
|
437
|
+
* • an existing FILE-PATH `extensions` entry that resolves to a real file is
|
|
438
|
+
* honored as already-wired (the documented pre-0.49 workaround) — the user
|
|
439
|
+
* may deliberately be running a local build, so it is reported, not
|
|
440
|
+
* rewritten.
|
|
441
|
+
*
|
|
442
|
+
* `env` is used for the launch-environment hint only: pi settings have no
|
|
443
|
+
* per-package env block, so FLAIR_AGENT_ID/FLAIR_URL must be exported by
|
|
444
|
+
* whatever shell launches pi — the message says so rather than implying the
|
|
445
|
+
* wiring carried them.
|
|
446
|
+
*/
|
|
447
|
+
function _wirePi(env) {
|
|
448
|
+
const path = piSettingsPath();
|
|
449
|
+
const home = resolveHome();
|
|
450
|
+
const display = path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
451
|
+
const spec = piFlairSpec();
|
|
452
|
+
const envHint = `pi settings carry no env — export FLAIR_AGENT_ID=${env.FLAIR_AGENT_ID} in the shell that launches pi`;
|
|
453
|
+
try {
|
|
454
|
+
let config = {};
|
|
455
|
+
if (existsSync(path)) {
|
|
456
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
457
|
+
if (raw)
|
|
458
|
+
config = JSON.parse(raw);
|
|
459
|
+
}
|
|
460
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
461
|
+
throw new Error("settings.json is not a JSON object");
|
|
462
|
+
}
|
|
463
|
+
if (config.packages !== undefined && !Array.isArray(config.packages)) {
|
|
464
|
+
throw new Error(`"packages" exists but is not an array — not rewriting it`);
|
|
465
|
+
}
|
|
466
|
+
if (config.extensions !== undefined && !Array.isArray(config.extensions)) {
|
|
467
|
+
throw new Error(`"extensions" exists but is not an array — not rewriting it`);
|
|
468
|
+
}
|
|
469
|
+
// The #1346 trap: npm: pi-flair specs under `extensions`. Collect + drop.
|
|
470
|
+
let movedFromExtensions = false;
|
|
471
|
+
if (Array.isArray(config.extensions)) {
|
|
472
|
+
const kept = config.extensions.filter((e) => !(typeof e === "string" && isPiFlairNpmSource(e)));
|
|
473
|
+
movedFromExtensions = kept.length !== config.extensions.length;
|
|
474
|
+
if (movedFromExtensions)
|
|
475
|
+
config.extensions = kept;
|
|
476
|
+
}
|
|
477
|
+
// Existing packages entry?
|
|
478
|
+
let entryIndex = -1;
|
|
479
|
+
let entrySource = null;
|
|
480
|
+
if (Array.isArray(config.packages)) {
|
|
481
|
+
for (let i = 0; i < config.packages.length; i++) {
|
|
482
|
+
const source = piPackageEntrySource(config.packages[i]);
|
|
483
|
+
if (source && isPiFlairNpmSource(source)) {
|
|
484
|
+
entryIndex = i;
|
|
485
|
+
entrySource = source;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (!movedFromExtensions && entrySource === spec) {
|
|
491
|
+
return { ok: true, message: `pi: already wired in ${display} (${spec})` };
|
|
492
|
+
}
|
|
493
|
+
if (!movedFromExtensions && entryIndex === -1) {
|
|
494
|
+
// No packages entry and nothing misplaced — honor a working file-path
|
|
495
|
+
// extensions entry (pre-0.49 workaround) instead of double-wiring.
|
|
496
|
+
const scan = scanPiSettings(JSON.stringify(config));
|
|
497
|
+
const workingPath = scan.extensionFilePaths.find((p) => existsSync(resolvePiExtensionPath(p, home, piAgentDir())));
|
|
498
|
+
if (workingPath) {
|
|
499
|
+
return {
|
|
500
|
+
ok: true,
|
|
501
|
+
message: `pi: already wired via a file-path extension in ${display} (${workingPath}) — ` +
|
|
502
|
+
`the pre-0.49 workaround; the canonical form is a "packages" entry: ${spec}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
config.packages = Array.isArray(config.packages) ? config.packages : [];
|
|
507
|
+
let action;
|
|
508
|
+
if (entryIndex >= 0) {
|
|
509
|
+
const entry = config.packages[entryIndex];
|
|
510
|
+
if (typeof entry === "string")
|
|
511
|
+
config.packages[entryIndex] = spec;
|
|
512
|
+
else
|
|
513
|
+
entry.source = spec; // object entry: refresh source, keep filters
|
|
514
|
+
action = movedFromExtensions
|
|
515
|
+
? `moved ${PI_FLAIR_PACKAGE} out of "extensions" and refreshed the "packages" pin in`
|
|
516
|
+
: "refreshed pin in";
|
|
517
|
+
}
|
|
518
|
+
else if (movedFromExtensions) {
|
|
519
|
+
config.packages.push(spec);
|
|
520
|
+
action = `moved ${PI_FLAIR_PACKAGE} from "extensions" to "packages" in`;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
config.packages.push(spec);
|
|
524
|
+
action = "wired";
|
|
525
|
+
}
|
|
526
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
527
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
|
|
528
|
+
const trapNote = movedFromExtensions
|
|
529
|
+
? ` — pi silently ignores npm: specs under "extensions" (flair#1346)`
|
|
530
|
+
: "";
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
message: `pi: ${action} ${display} (${spec} — pi installs the package on next launch; ${envHint})${trapNote}`,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
538
|
+
return {
|
|
539
|
+
ok: false,
|
|
540
|
+
message: `pi: manual wiring needed (could not update ${display}: ${reason}).\n` +
|
|
541
|
+
` Add this to ${display} (${envHint}):\n${indent(piJsonSnippet())}`,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
266
545
|
/**
|
|
267
546
|
* Antigravity CLI (`agy`) + Antigravity 2.0 IDE + SDK: they share ONE central
|
|
268
547
|
* MCP config at ~/.gemini/config/mcp_config.json on every OS (flair#1209).
|
|
@@ -298,6 +577,11 @@ export function clientConfigPath(id) {
|
|
|
298
577
|
return cursorConfigPath();
|
|
299
578
|
case "antigravity":
|
|
300
579
|
return antigravityConfigPath();
|
|
580
|
+
case "pi":
|
|
581
|
+
// NOT an MCP config: pi's own settings.json, where the pi-flair
|
|
582
|
+
// native-extension wiring lives (flair#1342). readClientMcpBlock over
|
|
583
|
+
// this file correctly reports "no MCP block" — pi never has one.
|
|
584
|
+
return piSettingsPath();
|
|
301
585
|
}
|
|
302
586
|
}
|
|
303
587
|
// ---- Internal wiring functions --------------------------------------------------
|
|
@@ -374,24 +658,28 @@ export const ALL_CLIENTS = [
|
|
|
374
658
|
id: "claude-code",
|
|
375
659
|
label: "Claude Code",
|
|
376
660
|
bin: "claude",
|
|
661
|
+
kind: "mcp",
|
|
377
662
|
wire: _wireClaudeCode,
|
|
378
663
|
},
|
|
379
664
|
{
|
|
380
665
|
id: "codex",
|
|
381
666
|
label: "Codex",
|
|
382
667
|
bin: "codex",
|
|
668
|
+
kind: "mcp",
|
|
383
669
|
wire: _wireCodex,
|
|
384
670
|
},
|
|
385
671
|
{
|
|
386
672
|
id: "gemini",
|
|
387
673
|
label: "Gemini",
|
|
388
674
|
bin: "gemini",
|
|
675
|
+
kind: "mcp",
|
|
389
676
|
wire: _wireGemini,
|
|
390
677
|
},
|
|
391
678
|
{
|
|
392
679
|
id: "cursor",
|
|
393
680
|
label: "Cursor",
|
|
394
681
|
bin: "cursor",
|
|
682
|
+
kind: "mcp",
|
|
395
683
|
wire: _wireCursor,
|
|
396
684
|
},
|
|
397
685
|
{
|
|
@@ -399,8 +687,23 @@ export const ALL_CLIENTS = [
|
|
|
399
687
|
label: "Antigravity",
|
|
400
688
|
// Google's Antigravity CLI — the executable is `agy` (flair#1209).
|
|
401
689
|
bin: "agy",
|
|
690
|
+
kind: "mcp",
|
|
402
691
|
wire: _wireAntigravity,
|
|
403
692
|
},
|
|
693
|
+
{
|
|
694
|
+
id: "pi",
|
|
695
|
+
label: "pi",
|
|
696
|
+
bin: "pi",
|
|
697
|
+
// NOT an MCP client — pi loads @tpsdev-ai/pi-flair as a native extension
|
|
698
|
+
// via its settings.json `packages` key (flair#1342). Consumers doing
|
|
699
|
+
// MCP-shaped work must filter on `kind`.
|
|
700
|
+
kind: "native-extension",
|
|
701
|
+
// pi is also detected by its settings file: a configured pi whose binary
|
|
702
|
+
// isn't on THIS shell's PATH (version manager, launchd context) is still
|
|
703
|
+
// a pi whose wiring is worth checking/fixing. Pure fs check, both legs.
|
|
704
|
+
detect: () => detectBin("pi") || existsSync(piSettingsPath()),
|
|
705
|
+
wire: _wirePi,
|
|
706
|
+
},
|
|
404
707
|
];
|
|
405
708
|
/**
|
|
406
709
|
* The summary `flair init` prints LAST.
|
|
@@ -455,14 +758,17 @@ export function renderWiringSummary(results, opts = {}) {
|
|
|
455
758
|
return lines;
|
|
456
759
|
}
|
|
457
760
|
/**
|
|
458
|
-
* Detect every known client. One rule (`bin` on PATH) applied uniformly
|
|
459
|
-
* client added to ALL_CLIENTS is detected by declaring its executable
|
|
460
|
-
*
|
|
761
|
+
* Detect every known client. One rule (`bin` on PATH) applied uniformly — a
|
|
762
|
+
* client added to ALL_CLIENTS is detected by declaring its executable, with no
|
|
763
|
+
* per-client branch here to forget to extend. A client may widen that with a
|
|
764
|
+
* declared `detect` override (still a pure fs check — pi adds its settings
|
|
765
|
+
* file as a second signal, flair#1342); the override lives on the registry
|
|
766
|
+
* entry, so this function stays branch-free.
|
|
461
767
|
*/
|
|
462
768
|
export function detectClients() {
|
|
463
769
|
return ALL_CLIENTS.map((client) => ({
|
|
464
770
|
...client,
|
|
465
|
-
detected: detectBin(client.bin),
|
|
771
|
+
detected: client.detect ? client.detect() : detectBin(client.bin),
|
|
466
772
|
}));
|
|
467
773
|
}
|
|
468
774
|
export function wireClaudeCode(env) {
|
|
@@ -480,3 +786,6 @@ export function wireCursor(env) {
|
|
|
480
786
|
export function wireAntigravity(env) {
|
|
481
787
|
return _wireAntigravity(env);
|
|
482
788
|
}
|
|
789
|
+
export function wirePi(env) {
|
|
790
|
+
return _wirePi(env);
|
|
791
|
+
}
|
package/dist/lib/auth-resolve.js
CHANGED
|
@@ -119,6 +119,34 @@ export function readAdminPassFileSecure(path) {
|
|
|
119
119
|
export function defaultAdminPassPath() {
|
|
120
120
|
return join(homedir(), ".flair", "admin-pass");
|
|
121
121
|
}
|
|
122
|
+
/** The admin username Harper's bootstrap creates and every Basic-auth path
|
|
123
|
+
* historically hardcoded. Kept as the default; overridable per call via
|
|
124
|
+
* `resolveAdminUser` (flair#1345). */
|
|
125
|
+
export const DEFAULT_ADMIN_USER = "admin";
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the admin USERNAME for Basic auth against a Flair/Harper instance
|
|
128
|
+
* (flair#1345).
|
|
129
|
+
*
|
|
130
|
+
* Precedence: explicit value (an `--admin-user` flag) → `FLAIR_ADMIN_USER`
|
|
131
|
+
* env → `"admin"`. Always returns a usable name — unlike the password there
|
|
132
|
+
* is no "missing" state, because `admin` is the correct answer for every
|
|
133
|
+
* instance `flair init` ever bootstrapped.
|
|
134
|
+
*
|
|
135
|
+
* Why it exists: with `authorizeLocal: false` (the #604/#610 hardening) the
|
|
136
|
+
* CLI must send real Basic admin auth, and on an instance whose superuser is
|
|
137
|
+
* not named `admin` there was no way to say so — `--admin-pass` existed,
|
|
138
|
+
* the username didn't. A wrong username and a wrong password produce the
|
|
139
|
+
* SAME 401 "Login failed" from Harper, so this also feeds the 401 hint text
|
|
140
|
+
* (see opsAuth401Hint in src/cli.ts).
|
|
141
|
+
*
|
|
142
|
+
* Unlike the admin PASSWORD (see resolveLocalAdminPass's remote-target
|
|
143
|
+
* guard), the env leg is honored for remote targets too: a username is not a
|
|
144
|
+
* secret — sending the wrong one to a third-party host leaks nothing and
|
|
145
|
+
* fails closed with a 401.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveAdminUser(explicit) {
|
|
148
|
+
return explicit || process.env.FLAIR_ADMIN_USER || DEFAULT_ADMIN_USER;
|
|
149
|
+
}
|
|
122
150
|
export function defaultKeysDir() {
|
|
123
151
|
return join(homedir(), ".flair", "keys");
|
|
124
152
|
}
|
|
@@ -400,10 +428,13 @@ export async function tryAgentKeyFloor(baseUrl, method, path, body, keysDir) {
|
|
|
400
428
|
export async function authedRequest(method, path, body, opts) {
|
|
401
429
|
const isLocal = opts.isLocal ?? isLocalBase(opts.baseUrl);
|
|
402
430
|
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
431
|
+
// One username for every Basic tier: flag > FLAIR_ADMIN_USER env > "admin"
|
|
432
|
+
// (flair#1345 — these three sites used to hardcode the literal `admin`).
|
|
433
|
+
const adminUser = resolveAdminUser(opts.adminUser);
|
|
403
434
|
let authHeader;
|
|
404
435
|
// Tier 1: explicit — caller-resolved flag material always wins.
|
|
405
436
|
if (opts.explicitAdminPass) {
|
|
406
|
-
authHeader = `Basic ${Buffer.from(
|
|
437
|
+
authHeader = `Basic ${Buffer.from(`${adminUser}:${opts.explicitAdminPass}`).toString("base64")}`;
|
|
407
438
|
}
|
|
408
439
|
else if (opts.explicitKeyPath && opts.agentId) {
|
|
409
440
|
try {
|
|
@@ -421,7 +452,7 @@ export async function authedRequest(method, path, body, opts) {
|
|
|
421
452
|
}
|
|
422
453
|
else if (process.env.FLAIR_ADMIN_PASS || process.env.HDB_ADMIN_PASSWORD) {
|
|
423
454
|
const adminPass = process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD;
|
|
424
|
-
authHeader = `Basic ${Buffer.from(
|
|
455
|
+
authHeader = `Basic ${Buffer.from(`${adminUser}:${adminPass}`).toString("base64")}`;
|
|
425
456
|
}
|
|
426
457
|
}
|
|
427
458
|
// Tier 3: a PINNED agent identity — sign specifically as this agent via
|
|
@@ -444,7 +475,7 @@ export async function authedRequest(method, path, body, opts) {
|
|
|
444
475
|
try {
|
|
445
476
|
const filePass = resolveLocalAdminPass(undefined, !isLocal);
|
|
446
477
|
if (filePass) {
|
|
447
|
-
authHeader = `Basic ${Buffer.from(
|
|
478
|
+
authHeader = `Basic ${Buffer.from(`${adminUser}:${filePass}`).toString("base64")}`;
|
|
448
479
|
}
|
|
449
480
|
}
|
|
450
481
|
catch (err) {
|