@tpsdev-ai/flair 0.3.19 → 0.4.1
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 +19 -6
- package/config.yaml +4 -2
- package/dist/cli.js +567 -60
- package/dist/resources/Memory.js +52 -3
- package/dist/resources/MemoryBootstrap.js +7 -1
- package/dist/resources/SemanticSearch.js +129 -56
- package/dist/resources/auth-middleware.js +37 -10
- package/dist/resources/content-safety.js +62 -0
- package/dist/resources/embeddings-provider.js +86 -7
- package/dist/resources/rate-limiter.js +118 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ import { join, resolve as resolvePath } from "node:path";
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { createPrivateKey, sign as nodeCryptoSign, randomUUID } from "node:crypto";
|
|
9
9
|
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
10
|
-
const DEFAULT_PORT =
|
|
11
|
-
const DEFAULT_OPS_PORT =
|
|
10
|
+
const DEFAULT_PORT = 19926;
|
|
11
|
+
const DEFAULT_OPS_PORT = 19925;
|
|
12
12
|
const DEFAULT_ADMIN_USER = "admin";
|
|
13
13
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
14
14
|
const HEALTH_POLL_INTERVAL_MS = 500;
|
|
@@ -18,6 +18,27 @@ function defaultKeysDir() {
|
|
|
18
18
|
function defaultDataDir() {
|
|
19
19
|
return join(homedir(), ".flair", "data");
|
|
20
20
|
}
|
|
21
|
+
function configPath() {
|
|
22
|
+
return join(homedir(), ".flair", "config.yaml");
|
|
23
|
+
}
|
|
24
|
+
function readPortFromConfig() {
|
|
25
|
+
try {
|
|
26
|
+
const p = configPath();
|
|
27
|
+
if (existsSync(p)) {
|
|
28
|
+
const yaml = readFileSync(p, "utf-8");
|
|
29
|
+
const m = yaml.match(/port:\s*(\d+)/);
|
|
30
|
+
if (m)
|
|
31
|
+
return Number(m[1]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function writeConfig(port) {
|
|
38
|
+
const p = configPath();
|
|
39
|
+
mkdirSync(join(homedir(), ".flair"), { recursive: true });
|
|
40
|
+
writeFileSync(p, `# Flair configuration\nport: ${port}\n`);
|
|
41
|
+
}
|
|
21
42
|
function privKeyPath(agentId, keysDir) {
|
|
22
43
|
return join(keysDir, `${agentId}.key`);
|
|
23
44
|
}
|
|
@@ -48,17 +69,8 @@ function b64url(bytes) {
|
|
|
48
69
|
}
|
|
49
70
|
async function api(method, path, body) {
|
|
50
71
|
// Resolve port: FLAIR_URL env > ~/.flair/config.yaml > default 9926
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const configPath = join(homedir(), ".flair", "config.yaml");
|
|
54
|
-
if (existsSync(configPath)) {
|
|
55
|
-
const yaml = readFileSync(configPath, "utf-8");
|
|
56
|
-
const portMatch = yaml.match(/port:\s*(\d+)/);
|
|
57
|
-
if (portMatch)
|
|
58
|
-
defaultUrl = `http://127.0.0.1:${portMatch[1]}`;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch { /* ignore config read errors */ }
|
|
72
|
+
const savedPort = readPortFromConfig();
|
|
73
|
+
const defaultUrl = savedPort ? `http://127.0.0.1:${savedPort}` : `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
62
74
|
const base = process.env.FLAIR_URL || defaultUrl;
|
|
63
75
|
// Auth resolution order:
|
|
64
76
|
// 1. FLAIR_TOKEN env → Bearer token (backward compat)
|
|
@@ -261,9 +273,19 @@ program
|
|
|
261
273
|
if (!bin)
|
|
262
274
|
throw new Error("@harperfast/harper not found in node_modules.\nRun: npm install @harperfast/harper");
|
|
263
275
|
mkdirSync(dataDir, { recursive: true });
|
|
276
|
+
const opsSocket = join(dataDir, "operations-server");
|
|
277
|
+
const harperSetConfig = JSON.stringify({
|
|
278
|
+
rootPath: dataDir,
|
|
279
|
+
http: { port: httpPort, cors: true, corsAccessList: [`http://127.0.0.1:${httpPort}`, `http://localhost:${httpPort}`] },
|
|
280
|
+
operationsApi: { network: { port: opsPort, cors: true }, domainSocket: opsSocket },
|
|
281
|
+
mqtt: { network: { port: null }, webSocket: false },
|
|
282
|
+
localStudio: { enabled: false },
|
|
283
|
+
authentication: { authorizeLocal: true, enableSessions: true },
|
|
284
|
+
});
|
|
264
285
|
const env = {
|
|
265
286
|
...process.env,
|
|
266
287
|
ROOTPATH: dataDir,
|
|
288
|
+
HARPER_SET_CONFIG: harperSetConfig,
|
|
267
289
|
DEFAULTS_MODE: "dev",
|
|
268
290
|
HDB_ADMIN_USERNAME: adminUser,
|
|
269
291
|
HDB_ADMIN_PASSWORD: adminPass,
|
|
@@ -273,7 +295,10 @@ program
|
|
|
273
295
|
OPERATIONSAPI_NETWORK_PORT: String(opsPort),
|
|
274
296
|
LOCAL_STUDIO: "false",
|
|
275
297
|
};
|
|
276
|
-
// Install
|
|
298
|
+
// Install Harper (creates system database, admin user, config file).
|
|
299
|
+
// IMPORTANT: Do NOT pre-create harper-config.yaml — Harper's install checks
|
|
300
|
+
// for its existence to detect existing installations. If found, it skips
|
|
301
|
+
// install and tries to read the (empty) database, causing a crash.
|
|
277
302
|
console.log("Installing Harper...");
|
|
278
303
|
await new Promise((resolve, reject) => {
|
|
279
304
|
let output = "";
|
|
@@ -282,17 +307,21 @@ program
|
|
|
282
307
|
install.stderr?.on("data", (d) => { output += d.toString(); });
|
|
283
308
|
install.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Harper install failed (${code}): ${output}`)));
|
|
284
309
|
install.on("error", reject);
|
|
285
|
-
setTimeout(() => { install.kill(); reject(new Error(`Harper install timed out: ${output}`)); },
|
|
310
|
+
setTimeout(() => { install.kill(); reject(new Error(`Harper install timed out: ${output}`)); }, 60_000);
|
|
286
311
|
});
|
|
287
|
-
// Start (detached)
|
|
312
|
+
// Start Harper in dev mode (detached). Dev mode sets authorizeLocal=true
|
|
313
|
+
// which allows our Ed25519 middleware to handle auth while internal
|
|
314
|
+
// cross-resource calls (e.g. SemanticSearch → Memory) pass through.
|
|
288
315
|
console.log(`Starting Harper on port ${httpPort}...`);
|
|
289
|
-
const proc = spawn(process.execPath, [bin, "
|
|
316
|
+
const proc = spawn(process.execPath, [bin, "dev", "."], { cwd: flairPackageDir(), env, detached: true, stdio: "ignore" });
|
|
290
317
|
proc.unref();
|
|
291
318
|
}
|
|
292
319
|
console.log("Waiting for Harper health check...");
|
|
293
320
|
await waitForHealth(httpPort, adminUser, adminPass, STARTUP_TIMEOUT_MS);
|
|
294
321
|
console.log("Harper is healthy ✓");
|
|
295
322
|
}
|
|
323
|
+
// Persist port to config so other commands can find this instance
|
|
324
|
+
writeConfig(httpPort);
|
|
296
325
|
// Generate or reuse keypair
|
|
297
326
|
mkdirSync(keysDir, { recursive: true });
|
|
298
327
|
const privPath = privKeyPath(agentId, keysDir);
|
|
@@ -375,8 +404,10 @@ program
|
|
|
375
404
|
}
|
|
376
405
|
console.log(`\n Claude Code: Add to your CLAUDE.md:`);
|
|
377
406
|
console.log(` At the start of every session, run mcp__flair__bootstrap before responding.`);
|
|
407
|
+
const mcpEnv = { FLAIR_AGENT_ID: agentId };
|
|
408
|
+
mcpEnv.FLAIR_URL = httpUrl;
|
|
378
409
|
console.log(`\n MCP config (.mcp.json):`);
|
|
379
|
-
console.log(` { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"], "env":
|
|
410
|
+
console.log(` { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"], "env": ${JSON.stringify(mcpEnv)} } } }`);
|
|
380
411
|
});
|
|
381
412
|
// ─── flair agent ─────────────────────────────────────────────────────────────
|
|
382
413
|
const agent = program.command("agent").description("Manage Flair agents");
|
|
@@ -384,7 +415,7 @@ agent
|
|
|
384
415
|
.command("add <id>")
|
|
385
416
|
.description("Register a new agent in a running Flair instance")
|
|
386
417
|
.option("--name <name>", "Display name (defaults to id)")
|
|
387
|
-
.option("--port <port>", "Harper HTTP port"
|
|
418
|
+
.option("--port <port>", "Harper HTTP port")
|
|
388
419
|
.option("--admin-pass <pass>", "Admin password for registration")
|
|
389
420
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
390
421
|
.option("--ops-port <port>", "Harper operations API port")
|
|
@@ -434,7 +465,7 @@ agent
|
|
|
434
465
|
agent
|
|
435
466
|
.command("rotate-key <id>")
|
|
436
467
|
.description("Rotate an agent's Ed25519 keypair")
|
|
437
|
-
.option("--port <port>", "Harper HTTP port"
|
|
468
|
+
.option("--port <port>", "Harper HTTP port")
|
|
438
469
|
.option("--ops-port <port>", "Harper operations API port")
|
|
439
470
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
440
471
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
@@ -512,7 +543,7 @@ agent
|
|
|
512
543
|
.command("remove <id>")
|
|
513
544
|
.description("Remove an agent and all its data from Flair")
|
|
514
545
|
.option("--keep-keys", "Do not delete key files from disk")
|
|
515
|
-
.option("--port <port>", "Harper HTTP port"
|
|
546
|
+
.option("--port <port>", "Harper HTTP port")
|
|
516
547
|
.option("--ops-port <port>", "Harper operations API port")
|
|
517
548
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
518
549
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
@@ -624,7 +655,7 @@ program
|
|
|
624
655
|
.command("grant <from-agent> <to-agent>")
|
|
625
656
|
.description("Grant an agent read access to another agent's memories")
|
|
626
657
|
.option("--scope <scope>", "Grant scope: read or search", "read")
|
|
627
|
-
.option("--port <port>", "Harper HTTP port"
|
|
658
|
+
.option("--port <port>", "Harper HTTP port")
|
|
628
659
|
.option("--ops-port <port>", "Harper operations API port")
|
|
629
660
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
630
661
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys (for from-agent Ed25519 auth)")
|
|
@@ -673,7 +704,7 @@ program
|
|
|
673
704
|
program
|
|
674
705
|
.command("revoke <from-agent> <to-agent>")
|
|
675
706
|
.description("Revoke a memory grant between two agents")
|
|
676
|
-
.option("--port <port>", "Harper HTTP port"
|
|
707
|
+
.option("--port <port>", "Harper HTTP port")
|
|
677
708
|
.option("--ops-port <port>", "Harper operations API port")
|
|
678
709
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
679
710
|
.action(async (fromAgent, toAgent, opts) => {
|
|
@@ -714,10 +745,11 @@ program
|
|
|
714
745
|
program
|
|
715
746
|
.command("status")
|
|
716
747
|
.description("Check Flair (Harper) instance health and agent count")
|
|
717
|
-
.option("--port <port>", "Harper HTTP port"
|
|
748
|
+
.option("--port <port>", "Harper HTTP port")
|
|
718
749
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
719
750
|
.action(async (opts) => {
|
|
720
|
-
const
|
|
751
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
752
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${port}`;
|
|
721
753
|
let healthy = false;
|
|
722
754
|
let agentCount = null;
|
|
723
755
|
let version = null;
|
|
@@ -788,36 +820,511 @@ program
|
|
|
788
820
|
}
|
|
789
821
|
console.log("\nTo upgrade: npm install -g @tpsdev-ai/flair@latest");
|
|
790
822
|
});
|
|
791
|
-
// ───
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
.
|
|
795
|
-
.
|
|
796
|
-
.option("--role <role>")
|
|
823
|
+
// ─── flair stop ───────────────────────────────────────────────────────────────
|
|
824
|
+
program
|
|
825
|
+
.command("stop")
|
|
826
|
+
.description("Stop the running Flair (Harper) instance")
|
|
827
|
+
.option("--port <port>", "Harper HTTP port")
|
|
797
828
|
.action(async (opts) => {
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
829
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
830
|
+
const platform = process.platform;
|
|
831
|
+
if (platform === "darwin") {
|
|
832
|
+
// macOS: try launchd first
|
|
833
|
+
const label = "ai.tpsdev.flair";
|
|
834
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
835
|
+
if (existsSync(plistPath)) {
|
|
836
|
+
try {
|
|
837
|
+
const { execSync } = await import("node:child_process");
|
|
838
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
839
|
+
console.log("✅ Flair stopped (launchd service unloaded)");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// launchd unload failed, try PID fallback
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Fallback: find process by port
|
|
848
|
+
try {
|
|
849
|
+
const { execSync } = await import("node:child_process");
|
|
850
|
+
const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
|
|
851
|
+
if (lsof) {
|
|
852
|
+
const pids = lsof.split("\n").map(p => p.trim()).filter(Boolean);
|
|
853
|
+
for (const pid of pids) {
|
|
854
|
+
process.kill(Number(pid), "SIGTERM");
|
|
855
|
+
}
|
|
856
|
+
console.log(`✅ Flair stopped (killed PID${pids.length > 1 ? "s" : ""}: ${pids.join(", ")})`);
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
console.log("Flair is not running.");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
console.log("Flair is not running (nothing found on port " + port + ").");
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
// ─── flair restart ────────────────────────────────────────────────────────────
|
|
867
|
+
program
|
|
868
|
+
.command("restart")
|
|
869
|
+
.description("Restart the Flair (Harper) instance")
|
|
870
|
+
.option("--port <port>", "Harper HTTP port")
|
|
871
|
+
.action(async (opts) => {
|
|
872
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
873
|
+
const platform = process.platform;
|
|
874
|
+
if (platform === "darwin") {
|
|
875
|
+
const label = "ai.tpsdev.flair";
|
|
876
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
877
|
+
if (existsSync(plistPath)) {
|
|
878
|
+
try {
|
|
879
|
+
const { execSync } = await import("node:child_process");
|
|
880
|
+
const uid = process.getuid?.() ?? 501;
|
|
881
|
+
execSync(`launchctl kickstart -k user/${uid}/${label}`, { stdio: "pipe" });
|
|
882
|
+
console.log("✅ Flair restarted (launchd kickstart)");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
console.error(`launchd restart failed: ${err.message}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
console.error("❌ No launchd service found. Run 'flair init' first.");
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
// Linux: stop + start via init
|
|
896
|
+
console.log("Stopping...");
|
|
897
|
+
try {
|
|
898
|
+
const { execSync } = await import("node:child_process");
|
|
899
|
+
const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
|
|
900
|
+
if (lsof) {
|
|
901
|
+
for (const pid of lsof.split("\n")) {
|
|
902
|
+
try {
|
|
903
|
+
process.kill(Number(pid.trim()), "SIGTERM");
|
|
904
|
+
}
|
|
905
|
+
catch { }
|
|
906
|
+
}
|
|
907
|
+
// Wait briefly for shutdown
|
|
908
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch { /* not running */ }
|
|
912
|
+
console.log("Starting...");
|
|
913
|
+
const bin = harperBin();
|
|
914
|
+
if (!bin) {
|
|
915
|
+
console.error("❌ Harper binary not found. Run 'flair init' first.");
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
const dataDir = defaultDataDir();
|
|
919
|
+
const adminPass = process.env.HDB_ADMIN_PASSWORD ?? "";
|
|
920
|
+
const env = {
|
|
921
|
+
...process.env,
|
|
922
|
+
ROOTPATH: dataDir,
|
|
923
|
+
DEFAULTS_MODE: "dev",
|
|
924
|
+
HDB_ADMIN_USERNAME: DEFAULT_ADMIN_USER,
|
|
925
|
+
HDB_ADMIN_PASSWORD: adminPass,
|
|
926
|
+
HTTP_PORT: String(port),
|
|
927
|
+
LOCAL_STUDIO: "false",
|
|
928
|
+
};
|
|
929
|
+
const proc = spawn(process.execPath, [bin, "run", "."], {
|
|
930
|
+
cwd: flairPackageDir(), env, detached: true, stdio: "ignore",
|
|
931
|
+
});
|
|
932
|
+
proc.unref();
|
|
933
|
+
try {
|
|
934
|
+
await waitForHealth(port, DEFAULT_ADMIN_USER, adminPass, STARTUP_TIMEOUT_MS);
|
|
935
|
+
console.log("✅ Flair restarted");
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
console.error("❌ Flair failed to restart within timeout");
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
// ─── flair uninstall ──────────────────────────────────────────────────────────
|
|
944
|
+
program
|
|
945
|
+
.command("uninstall")
|
|
946
|
+
.description("Stop Flair and remove the launchd/systemd service")
|
|
947
|
+
.option("--purge", "Also remove data and keys (destructive)")
|
|
948
|
+
.action(async (opts) => {
|
|
949
|
+
const platform = process.platform;
|
|
950
|
+
const port = readPortFromConfig() ?? DEFAULT_PORT;
|
|
951
|
+
// Stop first
|
|
952
|
+
if (platform === "darwin") {
|
|
953
|
+
const label = "ai.tpsdev.flair";
|
|
954
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
955
|
+
if (existsSync(plistPath)) {
|
|
956
|
+
try {
|
|
957
|
+
const { execSync } = await import("node:child_process");
|
|
958
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
959
|
+
}
|
|
960
|
+
catch { /* best effort */ }
|
|
961
|
+
const { unlinkSync } = await import("node:fs");
|
|
962
|
+
unlinkSync(plistPath);
|
|
963
|
+
console.log("✅ Launchd service removed");
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
console.log("No launchd service found — skipping");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
// Linux: kill by port
|
|
971
|
+
try {
|
|
972
|
+
const { execSync } = await import("node:child_process");
|
|
973
|
+
const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
|
|
974
|
+
if (lsof) {
|
|
975
|
+
for (const pid of lsof.split("\n")) {
|
|
976
|
+
try {
|
|
977
|
+
process.kill(Number(pid.trim()), "SIGTERM");
|
|
978
|
+
}
|
|
979
|
+
catch { }
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
catch { /* not running */ }
|
|
984
|
+
console.log("✅ Flair process stopped");
|
|
985
|
+
}
|
|
986
|
+
// Remove config
|
|
987
|
+
const cfgPath = configPath();
|
|
988
|
+
if (existsSync(cfgPath)) {
|
|
989
|
+
const { unlinkSync } = await import("node:fs");
|
|
990
|
+
unlinkSync(cfgPath);
|
|
991
|
+
console.log("✅ Config removed");
|
|
992
|
+
}
|
|
993
|
+
if (opts.purge) {
|
|
994
|
+
const { rmSync } = await import("node:fs");
|
|
995
|
+
const dataDir = defaultDataDir();
|
|
996
|
+
const keysDir = defaultKeysDir();
|
|
997
|
+
const flairDir = join(homedir(), ".flair");
|
|
998
|
+
if (existsSync(dataDir)) {
|
|
999
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
1000
|
+
console.log("✅ Data removed: " + dataDir);
|
|
1001
|
+
}
|
|
1002
|
+
if (existsSync(keysDir)) {
|
|
1003
|
+
rmSync(keysDir, { recursive: true, force: true });
|
|
1004
|
+
console.log("✅ Keys removed: " + keysDir);
|
|
1005
|
+
}
|
|
1006
|
+
// Remove .flair dir if empty
|
|
1007
|
+
try {
|
|
1008
|
+
const { readdirSync, rmdirSync } = await import("node:fs");
|
|
1009
|
+
if (existsSync(flairDir) && readdirSync(flairDir).length === 0) {
|
|
1010
|
+
rmdirSync(flairDir);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch { /* non-empty, that's fine */ }
|
|
1014
|
+
console.log("\n🗑️ Flair fully purged");
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
console.log("\nData and keys preserved at ~/.flair/");
|
|
1018
|
+
console.log("To remove everything: flair uninstall --purge");
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
// ─── flair reembed ────────────────────────────────────────────────────────────
|
|
1022
|
+
program
|
|
1023
|
+
.command("reembed")
|
|
1024
|
+
.description("Re-generate embeddings for memories with stale or missing model tags")
|
|
1025
|
+
.requiredOption("--agent <id>", "Agent ID to re-embed memories for")
|
|
1026
|
+
.option("--stale-only", "Only re-embed memories with mismatched model tag")
|
|
1027
|
+
.option("--dry-run", "Show count without modifying")
|
|
1028
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1029
|
+
.option("--batch-size <n>", "Records per batch", "50")
|
|
1030
|
+
.option("--delay-ms <ms>", "Delay between batches (ms)", "100")
|
|
1031
|
+
.action(async (opts) => {
|
|
1032
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
1033
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1034
|
+
const agentId = opts.agent;
|
|
1035
|
+
const staleOnly = opts.staleOnly ?? false;
|
|
1036
|
+
const dryRun = opts.dryRun ?? false;
|
|
1037
|
+
const batchSize = Number(opts.batchSize);
|
|
1038
|
+
const delayMs = Number(opts.delayMs);
|
|
1039
|
+
const currentModel = process.env.FLAIR_EMBEDDING_MODEL ?? "nomic-embed-text-v1.5-Q4_K_M";
|
|
1040
|
+
console.log(`Re-embedding memories for agent: ${agentId}`);
|
|
1041
|
+
console.log(`Current model: ${currentModel}`);
|
|
1042
|
+
if (staleOnly)
|
|
1043
|
+
console.log("Mode: stale-only (skipping up-to-date memories)");
|
|
1044
|
+
if (dryRun)
|
|
1045
|
+
console.log("Mode: dry-run (no modifications)");
|
|
1046
|
+
console.log("");
|
|
1047
|
+
const keysDir = defaultKeysDir();
|
|
1048
|
+
const privPath = privKeyPath(agentId, keysDir);
|
|
1049
|
+
if (!existsSync(privPath)) {
|
|
1050
|
+
console.error(`❌ Key not found: ${privPath}`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
const searchRes = await authFetch(baseUrl, agentId, privPath, "POST", "/SemanticSearch", {
|
|
1054
|
+
agentId, limit: 10000,
|
|
1055
|
+
});
|
|
1056
|
+
if (!searchRes.ok) {
|
|
1057
|
+
console.error(`❌ Failed to fetch memories: ${searchRes.status}`);
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
const data = await searchRes.json();
|
|
1061
|
+
const allMemories = data.results ?? [];
|
|
1062
|
+
const candidates = allMemories.filter((m) => {
|
|
1063
|
+
if (!m.content)
|
|
1064
|
+
return false;
|
|
1065
|
+
if (staleOnly)
|
|
1066
|
+
return !m.embeddingModel || m.embeddingModel !== currentModel;
|
|
1067
|
+
return true;
|
|
803
1068
|
});
|
|
804
|
-
|
|
1069
|
+
const total = candidates.length;
|
|
1070
|
+
const skipped = allMemories.length - total;
|
|
1071
|
+
console.log(`Total memories: ${allMemories.length}`);
|
|
1072
|
+
console.log(`Candidates for re-embedding: ${total}`);
|
|
1073
|
+
if (skipped > 0)
|
|
1074
|
+
console.log(`Skipped (up-to-date): ${skipped}`);
|
|
1075
|
+
if (dryRun || total === 0) {
|
|
1076
|
+
if (total === 0)
|
|
1077
|
+
console.log("\n✅ All memories are up-to-date!");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
console.log("");
|
|
1081
|
+
let processed = 0;
|
|
1082
|
+
let errors = 0;
|
|
1083
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
1084
|
+
const batch = candidates.slice(i, i + batchSize);
|
|
1085
|
+
for (const memory of batch) {
|
|
1086
|
+
try {
|
|
1087
|
+
const updateRes = await authFetch(baseUrl, agentId, privPath, "PUT", `/Memory/${memory.id}`, {
|
|
1088
|
+
id: memory.id, content: memory.content, embedding: undefined, embeddingModel: undefined,
|
|
1089
|
+
});
|
|
1090
|
+
if (updateRes.ok)
|
|
1091
|
+
processed++;
|
|
1092
|
+
else
|
|
1093
|
+
errors++;
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
errors++;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const pct = Math.round(((i + batch.length) / total) * 100);
|
|
1100
|
+
process.stdout.write(`\rRe-embedded ${processed}/${total} (${pct}%)${errors > 0 ? ` [${errors} errors]` : ""}`);
|
|
1101
|
+
if (i + batchSize < candidates.length)
|
|
1102
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
1103
|
+
}
|
|
1104
|
+
console.log(`\n\n✅ Re-embedding complete: ${processed} updated, ${errors} errors`);
|
|
805
1105
|
});
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
.
|
|
810
|
-
.requiredOption("--
|
|
811
|
-
.
|
|
1106
|
+
// ─── flair test ───────────────────────────────────────────────────────────────
|
|
1107
|
+
program
|
|
1108
|
+
.command("test")
|
|
1109
|
+
.description("Verify Flair is working: store, search, bootstrap, cleanup")
|
|
1110
|
+
.requiredOption("--agent <id>", "Agent ID to test with")
|
|
1111
|
+
.option("--port <port>", "Harper HTTP port")
|
|
812
1112
|
.action(async (opts) => {
|
|
813
|
-
const
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1113
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
1114
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1115
|
+
const agentId = opts.agent;
|
|
1116
|
+
const keysDir = defaultKeysDir();
|
|
1117
|
+
const privPath = privKeyPath(agentId, keysDir);
|
|
1118
|
+
if (!existsSync(privPath)) {
|
|
1119
|
+
console.error(`❌ Key not found: ${privPath}`);
|
|
1120
|
+
console.error(` Run: flair init --agent-id ${agentId}`);
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
const testId = `test-${agentId}-${Date.now()}`;
|
|
1124
|
+
const testContent = `Flair test memory (${new Date().toISOString()})`;
|
|
1125
|
+
let passed = 0;
|
|
1126
|
+
let failed = 0;
|
|
1127
|
+
const check = async (name, fn) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const ok = await fn();
|
|
1130
|
+
if (ok) {
|
|
1131
|
+
console.log(` ✅ ${name}`);
|
|
1132
|
+
passed++;
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
console.log(` ❌ ${name}`);
|
|
1136
|
+
failed++;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
console.log(` ❌ ${name}: ${e.message?.slice(0, 100)}`);
|
|
1141
|
+
failed++;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
console.log(`\nFlair test (agent: ${agentId}, url: ${baseUrl})\n`);
|
|
1145
|
+
// 1. Health
|
|
1146
|
+
await check("Health check", async () => {
|
|
1147
|
+
const res = await fetch(`${baseUrl}/Health`, { signal: AbortSignal.timeout(5000) });
|
|
1148
|
+
return res.status > 0;
|
|
818
1149
|
});
|
|
819
|
-
|
|
1150
|
+
// 2. Store
|
|
1151
|
+
await check("Memory store", async () => {
|
|
1152
|
+
const res = await authFetch(baseUrl, agentId, privPath, "PUT", `/Memory/${testId}`, {
|
|
1153
|
+
id: testId, agentId, content: testContent, durability: "ephemeral",
|
|
1154
|
+
createdAt: new Date().toISOString(), archived: false,
|
|
1155
|
+
});
|
|
1156
|
+
return res.ok;
|
|
1157
|
+
});
|
|
1158
|
+
// 3. Search
|
|
1159
|
+
await check("Semantic search", async () => {
|
|
1160
|
+
await new Promise(r => setTimeout(r, 2000)); // wait for indexing
|
|
1161
|
+
const res = await authFetch(baseUrl, agentId, privPath, "POST", "/SemanticSearch", {
|
|
1162
|
+
agentId, q: "flair test memory", limit: 5,
|
|
1163
|
+
});
|
|
1164
|
+
if (!res.ok)
|
|
1165
|
+
return false;
|
|
1166
|
+
const data = await res.json();
|
|
1167
|
+
return (data.results?.length ?? 0) > 0;
|
|
1168
|
+
});
|
|
1169
|
+
// 4. Bootstrap
|
|
1170
|
+
await check("Bootstrap context", async () => {
|
|
1171
|
+
const res = await authFetch(baseUrl, agentId, privPath, "POST", "/BootstrapMemories", {
|
|
1172
|
+
agentId, maxTokens: 1000,
|
|
1173
|
+
});
|
|
1174
|
+
if (!res.ok)
|
|
1175
|
+
return false;
|
|
1176
|
+
const data = await res.json();
|
|
1177
|
+
return (data.context?.length ?? 0) > 0;
|
|
1178
|
+
});
|
|
1179
|
+
// 5. Cleanup
|
|
1180
|
+
await check("Memory delete", async () => {
|
|
1181
|
+
const res = await authFetch(baseUrl, agentId, privPath, "DELETE", `/Memory/${testId}`);
|
|
1182
|
+
return res.ok || res.status === 204;
|
|
1183
|
+
});
|
|
1184
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
1185
|
+
if (failed > 0)
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
});
|
|
1188
|
+
// ─── flair doctor ─────────────────────────────────────────────────────────────
|
|
1189
|
+
program
|
|
1190
|
+
.command("doctor")
|
|
1191
|
+
.description("Diagnose common Flair problems and suggest fixes")
|
|
1192
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1193
|
+
.action(async (opts) => {
|
|
1194
|
+
const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
|
|
1195
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1196
|
+
let issues = 0;
|
|
1197
|
+
console.log("\n🩺 Flair Doctor\n");
|
|
1198
|
+
// 1. Port check — is something listening?
|
|
1199
|
+
try {
|
|
1200
|
+
const res = await fetch(`${baseUrl}/Health`, { signal: AbortSignal.timeout(3000) });
|
|
1201
|
+
if (res.status > 0) {
|
|
1202
|
+
console.log(` ✅ Harper responding on port ${port}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
console.log(` ❌ Nothing responding on port ${port}`);
|
|
1207
|
+
// Check if port is in use by something else
|
|
1208
|
+
try {
|
|
1209
|
+
const { execSync } = await import("node:child_process");
|
|
1210
|
+
const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
|
|
1211
|
+
if (lsof) {
|
|
1212
|
+
console.log(` Port ${port} is in use by PID ${lsof} — might be a stale process`);
|
|
1213
|
+
console.log(` Fix: kill ${lsof} && flair restart`);
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
console.log(` Harper is not running`);
|
|
1217
|
+
console.log(` Fix: flair init --agent-id <your-agent>`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
console.log(` Harper is not running`);
|
|
1222
|
+
console.log(` Fix: flair init --agent-id <your-agent>`);
|
|
1223
|
+
}
|
|
1224
|
+
issues++;
|
|
1225
|
+
}
|
|
1226
|
+
// 2. Keys directory
|
|
1227
|
+
const keysDir = defaultKeysDir();
|
|
1228
|
+
if (existsSync(keysDir)) {
|
|
1229
|
+
const keyFiles = (await import("node:fs")).readdirSync(keysDir).filter((f) => f.endsWith(".key"));
|
|
1230
|
+
if (keyFiles.length > 0) {
|
|
1231
|
+
console.log(` ✅ Keys found: ${keyFiles.length} agent(s) in ${keysDir}`);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
console.log(` ❌ Keys directory exists but no .key files found`);
|
|
1235
|
+
console.log(` Fix: flair init --agent-id <your-agent>`);
|
|
1236
|
+
issues++;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
console.log(` ❌ Keys directory missing: ${keysDir}`);
|
|
1241
|
+
console.log(` Fix: flair init --agent-id <your-agent>`);
|
|
1242
|
+
issues++;
|
|
1243
|
+
}
|
|
1244
|
+
// 3. Config file
|
|
1245
|
+
const cfgPath = join(homedir(), ".flair", "config.yaml");
|
|
1246
|
+
if (existsSync(cfgPath)) {
|
|
1247
|
+
const savedPort = readPortFromConfig();
|
|
1248
|
+
console.log(` ✅ Config: ${cfgPath} (port: ${savedPort ?? "default"})`);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
console.log(` ⚠️ No config file at ${cfgPath} — using defaults`);
|
|
1252
|
+
}
|
|
1253
|
+
// 4. Embeddings check (only if Harper is running)
|
|
1254
|
+
try {
|
|
1255
|
+
const res = await fetch(`${baseUrl}/Health`, { signal: AbortSignal.timeout(3000) });
|
|
1256
|
+
if (res.status > 0) {
|
|
1257
|
+
// Try a test embedding via SemanticSearch with a dummy query
|
|
1258
|
+
// If embedding mode is "none", search will include _warning
|
|
1259
|
+
const testRes = await fetch(`${baseUrl}/SemanticSearch`, {
|
|
1260
|
+
method: "POST",
|
|
1261
|
+
headers: { "Content-Type": "application/json" },
|
|
1262
|
+
body: JSON.stringify({ q: "test", limit: 1 }),
|
|
1263
|
+
signal: AbortSignal.timeout(10000),
|
|
1264
|
+
});
|
|
1265
|
+
if (testRes.ok) {
|
|
1266
|
+
const data = await testRes.json();
|
|
1267
|
+
if (data._warning) {
|
|
1268
|
+
console.log(` ⚠️ Embeddings: keyword-only (${data._warning})`);
|
|
1269
|
+
console.log(` Semantic search quality is degraded`);
|
|
1270
|
+
console.log(` Check: ls ~/.npm-global/lib/node_modules/@tpsdev-ai/flair/models/`);
|
|
1271
|
+
issues++;
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
console.log(` ✅ Embeddings: semantic search operational`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
else if (testRes.status === 401) {
|
|
1278
|
+
// Auth required — can't test embeddings without an agent key
|
|
1279
|
+
console.log(` ⚠️ Embeddings: cannot verify (auth required for SemanticSearch)`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
catch { /* Harper not running, already flagged above */ }
|
|
1284
|
+
// 5. Stale PID file
|
|
1285
|
+
const dataDir = defaultDataDir();
|
|
1286
|
+
const pidFile = join(dataDir, "hdb.pid");
|
|
1287
|
+
if (existsSync(pidFile)) {
|
|
1288
|
+
const pidContent = (await import("node:fs")).readFileSync(pidFile, "utf-8").trim();
|
|
1289
|
+
try {
|
|
1290
|
+
process.kill(Number(pidContent), 0); // check if process exists
|
|
1291
|
+
console.log(` ✅ PID file: ${pidFile} (process ${pidContent} is alive)`);
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
console.log(` ❌ Stale PID file: ${pidFile} (process ${pidContent} is dead)`);
|
|
1295
|
+
console.log(` Fix: rm ${pidFile} && flair restart`);
|
|
1296
|
+
issues++;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// 6. Data directory
|
|
1300
|
+
if (existsSync(dataDir)) {
|
|
1301
|
+
console.log(` ✅ Data directory: ${dataDir}`);
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
// Check ~/harper/ (common alternative)
|
|
1305
|
+
const altDir = join(homedir(), "harper");
|
|
1306
|
+
if (existsSync(altDir)) {
|
|
1307
|
+
console.log(` ⚠️ Data at ~/harper/ (not ~/.flair/data) — old install location`);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
console.log(` ❌ No data directory found`);
|
|
1311
|
+
console.log(` Fix: flair init --agent-id <your-agent>`);
|
|
1312
|
+
issues++;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Summary
|
|
1316
|
+
console.log("");
|
|
1317
|
+
if (issues === 0) {
|
|
1318
|
+
console.log(" 🟢 No issues found");
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
console.log(` 🔴 ${issues} issue${issues > 1 ? "s" : ""} found — see fixes above`);
|
|
1322
|
+
}
|
|
1323
|
+
console.log("");
|
|
1324
|
+
if (issues > 0)
|
|
1325
|
+
process.exit(1);
|
|
820
1326
|
});
|
|
1327
|
+
// ─── Memory and Soul commands ────────────────────────────────────────────────
|
|
821
1328
|
const memory = program.command("memory").description("Manage agent memories");
|
|
822
1329
|
memory.command("add").requiredOption("--agent <id>").requiredOption("--content <text>")
|
|
823
1330
|
.option("--durability <d>", "standard").option("--tags <csv>")
|
|
@@ -843,7 +1350,7 @@ program
|
|
|
843
1350
|
.description("Search memories by meaning (shortcut for memory search)")
|
|
844
1351
|
.requiredOption("--agent <id>", "Agent ID")
|
|
845
1352
|
.option("--limit <n>", "Max results", "5")
|
|
846
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1353
|
+
.option("--port <port>", "Harper HTTP port")
|
|
847
1354
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
848
1355
|
.option("--key <path>", "Ed25519 private key path")
|
|
849
1356
|
.action(async (query, opts) => {
|
|
@@ -888,7 +1395,7 @@ program
|
|
|
888
1395
|
.description("Cold-start context: get soul + recent memories as formatted text")
|
|
889
1396
|
.requiredOption("--agent <id>", "Agent ID")
|
|
890
1397
|
.option("--max-tokens <n>", "Maximum tokens in output", "4000")
|
|
891
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1398
|
+
.option("--port <port>", "Harper HTTP port")
|
|
892
1399
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
893
1400
|
.option("--key <path>", "Ed25519 private key path")
|
|
894
1401
|
.action(async (opts) => {
|
|
@@ -938,7 +1445,7 @@ program
|
|
|
938
1445
|
.description("Export agents, memories, and souls to a JSON archive")
|
|
939
1446
|
.option("--output <path>", "Output file path (default: ~/.flair/backups/flair-backup-<timestamp>.json)")
|
|
940
1447
|
.option("--agents <ids>", "Comma-separated agent IDs to include (default: all)")
|
|
941
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1448
|
+
.option("--port <port>", "Harper HTTP port")
|
|
942
1449
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
943
1450
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
944
1451
|
.action(async (opts) => {
|
|
@@ -1017,7 +1524,7 @@ program
|
|
|
1017
1524
|
.description("Import a Flair backup archive")
|
|
1018
1525
|
.option("--merge", "Add/update records without deleting existing (default)")
|
|
1019
1526
|
.option("--replace", "Delete all existing data for backed-up agents first, then import")
|
|
1020
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1527
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1021
1528
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1022
1529
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1023
1530
|
.option("--dry-run", "Show what would be imported without making changes")
|
|
@@ -1134,7 +1641,7 @@ program
|
|
|
1134
1641
|
.description("Export a single agent's identity (soul + memories) to a portable file")
|
|
1135
1642
|
.option("--output <path>", "Output file path")
|
|
1136
1643
|
.option("--include-key", "Include private key in export (UNENCRYPTED — keep the output file secure)")
|
|
1137
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1644
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1138
1645
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1139
1646
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1140
1647
|
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
@@ -1220,7 +1727,7 @@ program
|
|
|
1220
1727
|
program
|
|
1221
1728
|
.command("import <path>")
|
|
1222
1729
|
.description("Import an agent from an export file into this Flair instance")
|
|
1223
|
-
.option("--port <port>", "Harper HTTP port"
|
|
1730
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1224
1731
|
.option("--ops-port <port>", "Harper operations API port")
|
|
1225
1732
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1226
1733
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
@@ -1344,11 +1851,11 @@ program
|
|
|
1344
1851
|
// ─── flair migrate-keys ───────────────────────────────────────────────────────
|
|
1345
1852
|
program
|
|
1346
1853
|
.command("migrate-keys")
|
|
1347
|
-
.description("Migrate agent keys from
|
|
1348
|
-
.option("--from <dir>", "
|
|
1854
|
+
.description("Migrate agent keys from old path (~/.tps/secrets/flair/) to ~/.flair/keys/")
|
|
1855
|
+
.option("--from <dir>", "Old keys directory", join(homedir(), ".tps", "secrets", "flair"))
|
|
1349
1856
|
.option("--to <dir>", "New keys directory", defaultKeysDir())
|
|
1350
1857
|
.option("--dry-run", "Show what would be migrated without copying")
|
|
1351
|
-
.option("--port <port>", "Harper HTTP port
|
|
1858
|
+
.option("--port <port>", "Harper HTTP port")
|
|
1352
1859
|
.option("--ops-port <port>", "Harper operations API port")
|
|
1353
1860
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1354
1861
|
.action(async (opts) => {
|
|
@@ -1358,7 +1865,7 @@ program
|
|
|
1358
1865
|
const opsPort = opts.opsPort ? Number(opts.opsPort) : Number(opts.port) + 1;
|
|
1359
1866
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1360
1867
|
if (!existsSync(fromDir)) {
|
|
1361
|
-
console.log(`
|
|
1868
|
+
console.log(`Old keys directory not found: ${fromDir}`);
|
|
1362
1869
|
console.log("Nothing to migrate.");
|
|
1363
1870
|
process.exit(0);
|
|
1364
1871
|
}
|
|
@@ -1400,7 +1907,7 @@ program
|
|
|
1400
1907
|
else {
|
|
1401
1908
|
console.log(`\n✅ Migration complete: ${migrated} migrated, ${skipped} skipped`);
|
|
1402
1909
|
if (migrated > 0) {
|
|
1403
|
-
console.log(`\
|
|
1910
|
+
console.log(`\nOld keys preserved at ${fromDir} (delete manually when confirmed working).`);
|
|
1404
1911
|
// Optionally verify keys match Flair records
|
|
1405
1912
|
if (adminPass) {
|
|
1406
1913
|
console.log("\nVerifying migrated keys against Flair...");
|