@tpsdev-ai/flair 0.4.1 → 0.4.3
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/dist/cli.js +267 -77
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,12 @@ function defaultDataDir() {
|
|
|
19
19
|
return join(homedir(), ".flair", "data");
|
|
20
20
|
}
|
|
21
21
|
function configPath() {
|
|
22
|
-
|
|
22
|
+
// Check both .yaml and .yml extensions
|
|
23
|
+
const yamlPath = join(homedir(), ".flair", "config.yaml");
|
|
24
|
+
const ymlPath = join(homedir(), ".flair", "config.yml");
|
|
25
|
+
if (existsSync(ymlPath) && !existsSync(yamlPath))
|
|
26
|
+
return ymlPath;
|
|
27
|
+
return yamlPath;
|
|
23
28
|
}
|
|
24
29
|
function readPortFromConfig() {
|
|
25
30
|
try {
|
|
@@ -34,6 +39,46 @@ function readPortFromConfig() {
|
|
|
34
39
|
catch { /* ignore */ }
|
|
35
40
|
return null;
|
|
36
41
|
}
|
|
42
|
+
// Unified port resolution: --port flag > FLAIR_URL env > config file > default
|
|
43
|
+
// Every command that talks to Harper MUST use these helpers.
|
|
44
|
+
function resolveHttpPort(opts) {
|
|
45
|
+
if (opts.port !== undefined && opts.port !== null) {
|
|
46
|
+
const n = Number(opts.port);
|
|
47
|
+
if (!isNaN(n) && n > 0)
|
|
48
|
+
return n;
|
|
49
|
+
}
|
|
50
|
+
const envUrl = process.env.FLAIR_URL;
|
|
51
|
+
if (envUrl) {
|
|
52
|
+
const m = envUrl.match(/:(\d+)/);
|
|
53
|
+
if (m)
|
|
54
|
+
return Number(m[1]);
|
|
55
|
+
}
|
|
56
|
+
return readPortFromConfig() ?? DEFAULT_PORT;
|
|
57
|
+
}
|
|
58
|
+
// Ops port resolution: --ops-port flag > FLAIR_OPS_PORT env > config opsPort > httpPort - 1
|
|
59
|
+
function resolveOpsPort(opts) {
|
|
60
|
+
if (opts.opsPort !== undefined && opts.opsPort !== null) {
|
|
61
|
+
const n = Number(opts.opsPort);
|
|
62
|
+
if (!isNaN(n) && n > 0)
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
const envOps = process.env.FLAIR_OPS_PORT;
|
|
66
|
+
if (envOps)
|
|
67
|
+
return Number(envOps);
|
|
68
|
+
// Try reading from config
|
|
69
|
+
try {
|
|
70
|
+
const p = configPath();
|
|
71
|
+
if (existsSync(p)) {
|
|
72
|
+
const yaml = readFileSync(p, "utf-8");
|
|
73
|
+
const m = yaml.match(/opsPort:\s*(\d+)/);
|
|
74
|
+
if (m)
|
|
75
|
+
return Number(m[1]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
79
|
+
// Default: httpPort - 1
|
|
80
|
+
return resolveHttpPort(opts) - 1;
|
|
81
|
+
}
|
|
37
82
|
function writeConfig(port) {
|
|
38
83
|
const p = configPath();
|
|
39
84
|
mkdirSync(join(homedir(), ".flair"), { recursive: true });
|
|
@@ -246,8 +291,8 @@ program
|
|
|
246
291
|
.option("--skip-soul", "Skip interactive personality setup")
|
|
247
292
|
.action(async (opts) => {
|
|
248
293
|
const agentId = opts.agentId;
|
|
249
|
-
const httpPort =
|
|
250
|
-
const opsPort =
|
|
294
|
+
const httpPort = resolveHttpPort(opts);
|
|
295
|
+
const opsPort = resolveOpsPort(opts);
|
|
251
296
|
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
252
297
|
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
253
298
|
// Admin password: generate if not provided, NEVER written to disk
|
|
@@ -273,6 +318,12 @@ program
|
|
|
273
318
|
if (!bin)
|
|
274
319
|
throw new Error("@harperfast/harper not found in node_modules.\nRun: npm install @harperfast/harper");
|
|
275
320
|
mkdirSync(dataDir, { recursive: true });
|
|
321
|
+
// Detect whether Harper has already been installed in this data dir.
|
|
322
|
+
// harper-config.yaml is created during install — its presence means
|
|
323
|
+
// install already ran. Re-running install against an existing data dir
|
|
324
|
+
// crashes in Harper v5 beta.6+ (checkForExistingInstall queries the
|
|
325
|
+
// database before the env is initialized).
|
|
326
|
+
const alreadyInstalled = existsSync(join(dataDir, "harper-config.yaml"));
|
|
276
327
|
const opsSocket = join(dataDir, "operations-server");
|
|
277
328
|
const harperSetConfig = JSON.stringify({
|
|
278
329
|
rootPath: dataDir,
|
|
@@ -295,25 +346,27 @@ program
|
|
|
295
346
|
OPERATIONSAPI_NETWORK_PORT: String(opsPort),
|
|
296
347
|
LOCAL_STUDIO: "false",
|
|
297
348
|
};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
349
|
+
if (alreadyInstalled) {
|
|
350
|
+
console.log("Existing Harper installation found — skipping install.");
|
|
351
|
+
console.log("If something is wrong, run: flair doctor");
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
console.log("Installing Harper...");
|
|
355
|
+
await new Promise((resolve, reject) => {
|
|
356
|
+
let output = "";
|
|
357
|
+
const install = spawn(process.execPath, [bin, "install"], { cwd: flairPackageDir(), env });
|
|
358
|
+
install.stdout?.on("data", (d) => { output += d.toString(); });
|
|
359
|
+
install.stderr?.on("data", (d) => { output += d.toString(); });
|
|
360
|
+
install.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Harper install failed (${code}): ${output}`)));
|
|
361
|
+
install.on("error", reject);
|
|
362
|
+
setTimeout(() => { install.kill(); reject(new Error(`Harper install timed out: ${output}`)); }, 60_000);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Start Harper with flair loaded as a component (the "." arg).
|
|
366
|
+
// ROOTPATH in env points to the data dir; authorizeLocal and thread
|
|
367
|
+
// count are set via HARPER_SET_CONFIG — no need for dev mode.
|
|
315
368
|
console.log(`Starting Harper on port ${httpPort}...`);
|
|
316
|
-
const proc = spawn(process.execPath, [bin, "
|
|
369
|
+
const proc = spawn(process.execPath, [bin, "run", "."], { cwd: flairPackageDir(), env, detached: true, stdio: "ignore" });
|
|
317
370
|
proc.unref();
|
|
318
371
|
}
|
|
319
372
|
console.log("Waiting for Harper health check...");
|
|
@@ -420,8 +473,8 @@ agent
|
|
|
420
473
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
421
474
|
.option("--ops-port <port>", "Harper operations API port")
|
|
422
475
|
.action(async (id, opts) => {
|
|
423
|
-
const httpPort =
|
|
424
|
-
const opsPort =
|
|
476
|
+
const httpPort = resolveHttpPort(opts);
|
|
477
|
+
const opsPort = resolveOpsPort(opts);
|
|
425
478
|
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
426
479
|
const adminPass = opts.adminPass;
|
|
427
480
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
@@ -457,7 +510,32 @@ agent
|
|
|
457
510
|
agent
|
|
458
511
|
.command("list")
|
|
459
512
|
.description("List all agents")
|
|
460
|
-
.
|
|
513
|
+
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
514
|
+
.option("--port <port>", "Harper HTTP port")
|
|
515
|
+
.action(async (opts) => {
|
|
516
|
+
const port = resolveHttpPort(opts);
|
|
517
|
+
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
518
|
+
if (adminPass) {
|
|
519
|
+
// Use admin basic auth against ops API to list agents directly
|
|
520
|
+
const opsPort = resolveOpsPort(opts);
|
|
521
|
+
const auth = Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64");
|
|
522
|
+
const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: { "Content-Type": "application/json", Authorization: `Basic ${auth}` },
|
|
525
|
+
body: JSON.stringify({ operation: "sql", sql: "SELECT id, name, createdAt FROM flair.Agent ORDER BY createdAt" }),
|
|
526
|
+
});
|
|
527
|
+
if (!res.ok) {
|
|
528
|
+
const text = await res.text().catch(() => "");
|
|
529
|
+
console.error(`Error: ${res.status} ${text}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
// Try agent-authed API (requires FLAIR_AGENT_ID to be set)
|
|
536
|
+
console.log(JSON.stringify(await api("GET", "/Agent"), null, 2));
|
|
537
|
+
}
|
|
538
|
+
});
|
|
461
539
|
agent
|
|
462
540
|
.command("show <id>")
|
|
463
541
|
.description("Show agent details")
|
|
@@ -470,8 +548,8 @@ agent
|
|
|
470
548
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
471
549
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
472
550
|
.action(async (id, opts) => {
|
|
473
|
-
const httpPort =
|
|
474
|
-
const opsPort =
|
|
551
|
+
const httpPort = resolveHttpPort(opts);
|
|
552
|
+
const opsPort = resolveOpsPort(opts);
|
|
475
553
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
476
554
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
477
555
|
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
@@ -549,7 +627,7 @@ agent
|
|
|
549
627
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
550
628
|
.option("--force", "Skip interactive confirmation (required when stdin is not a TTY)")
|
|
551
629
|
.action(async (id, opts) => {
|
|
552
|
-
const opsPort =
|
|
630
|
+
const opsPort = resolveOpsPort(opts);
|
|
553
631
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
554
632
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
555
633
|
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
@@ -660,8 +738,8 @@ program
|
|
|
660
738
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
661
739
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys (for from-agent Ed25519 auth)")
|
|
662
740
|
.action(async (fromAgent, toAgent, opts) => {
|
|
663
|
-
const httpPort =
|
|
664
|
-
const opsPort =
|
|
741
|
+
const httpPort = resolveHttpPort(opts);
|
|
742
|
+
const opsPort = resolveOpsPort(opts);
|
|
665
743
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
666
744
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
667
745
|
const scope = opts.scope ?? "read";
|
|
@@ -708,8 +786,8 @@ program
|
|
|
708
786
|
.option("--ops-port <port>", "Harper operations API port")
|
|
709
787
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
710
788
|
.action(async (fromAgent, toAgent, opts) => {
|
|
711
|
-
const httpPort =
|
|
712
|
-
const opsPort =
|
|
789
|
+
const httpPort = resolveHttpPort(opts);
|
|
790
|
+
const opsPort = resolveOpsPort(opts);
|
|
713
791
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
714
792
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
715
793
|
if (!adminPass) {
|
|
@@ -748,7 +826,7 @@ program
|
|
|
748
826
|
.option("--port <port>", "Harper HTTP port")
|
|
749
827
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
750
828
|
.action(async (opts) => {
|
|
751
|
-
const port =
|
|
829
|
+
const port = resolveHttpPort(opts);
|
|
752
830
|
const baseUrl = opts.url ?? `http://127.0.0.1:${port}`;
|
|
753
831
|
let healthy = false;
|
|
754
832
|
let agentCount = null;
|
|
@@ -826,7 +904,7 @@ program
|
|
|
826
904
|
.description("Stop the running Flair (Harper) instance")
|
|
827
905
|
.option("--port <port>", "Harper HTTP port")
|
|
828
906
|
.action(async (opts) => {
|
|
829
|
-
const port =
|
|
907
|
+
const port = resolveHttpPort(opts);
|
|
830
908
|
const platform = process.platform;
|
|
831
909
|
if (platform === "darwin") {
|
|
832
910
|
// macOS: try launchd first
|
|
@@ -869,7 +947,7 @@ program
|
|
|
869
947
|
.description("Restart the Flair (Harper) instance")
|
|
870
948
|
.option("--port <port>", "Harper HTTP port")
|
|
871
949
|
.action(async (opts) => {
|
|
872
|
-
const port =
|
|
950
|
+
const port = resolveHttpPort(opts);
|
|
873
951
|
const platform = process.platform;
|
|
874
952
|
if (platform === "darwin") {
|
|
875
953
|
const label = "ai.tpsdev.flair";
|
|
@@ -1029,7 +1107,7 @@ program
|
|
|
1029
1107
|
.option("--batch-size <n>", "Records per batch", "50")
|
|
1030
1108
|
.option("--delay-ms <ms>", "Delay between batches (ms)", "100")
|
|
1031
1109
|
.action(async (opts) => {
|
|
1032
|
-
const port =
|
|
1110
|
+
const port = resolveHttpPort(opts);
|
|
1033
1111
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1034
1112
|
const agentId = opts.agent;
|
|
1035
1113
|
const staleOnly = opts.staleOnly ?? false;
|
|
@@ -1110,7 +1188,7 @@ program
|
|
|
1110
1188
|
.requiredOption("--agent <id>", "Agent ID to test with")
|
|
1111
1189
|
.option("--port <port>", "Harper HTTP port")
|
|
1112
1190
|
.action(async (opts) => {
|
|
1113
|
-
const port =
|
|
1191
|
+
const port = resolveHttpPort(opts);
|
|
1114
1192
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1115
1193
|
const agentId = opts.agent;
|
|
1116
1194
|
const keysDir = defaultKeysDir();
|
|
@@ -1190,38 +1268,140 @@ program
|
|
|
1190
1268
|
.command("doctor")
|
|
1191
1269
|
.description("Diagnose common Flair problems and suggest fixes")
|
|
1192
1270
|
.option("--port <port>", "Harper HTTP port")
|
|
1271
|
+
.option("--fix", "Automatically fix issues where possible")
|
|
1272
|
+
.option("--dry-run", "Show what --fix would do without making changes")
|
|
1193
1273
|
.action(async (opts) => {
|
|
1194
|
-
const port =
|
|
1195
|
-
const
|
|
1274
|
+
const port = resolveHttpPort(opts);
|
|
1275
|
+
const autoFix = opts.fix ?? false;
|
|
1276
|
+
const dryRun = opts.dryRun ?? false;
|
|
1277
|
+
if (dryRun && !autoFix) {
|
|
1278
|
+
console.log(" ℹ️ --dry-run only has effect with --fix\n");
|
|
1279
|
+
}
|
|
1280
|
+
let effectivePort = port;
|
|
1281
|
+
let baseUrl = `http://127.0.0.1:${port}`;
|
|
1196
1282
|
let issues = 0;
|
|
1283
|
+
let harperResponding = false;
|
|
1197
1284
|
console.log("\n🩺 Flair Doctor\n");
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1285
|
+
// Helper: try to reach Harper on a given port
|
|
1286
|
+
async function probePort(p) {
|
|
1287
|
+
try {
|
|
1288
|
+
const res = await fetch(`http://127.0.0.1:${p}/Health`, { signal: AbortSignal.timeout(3000) });
|
|
1289
|
+
return res.status > 0;
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
return false;
|
|
1203
1293
|
}
|
|
1204
1294
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
//
|
|
1295
|
+
// Helper: discover what port a Harper PID is listening on
|
|
1296
|
+
async function discoverPortFromPid(pid) {
|
|
1297
|
+
// Defense-in-depth: caller already validates, but re-check here
|
|
1298
|
+
if (!/^\d+$/.test(pid))
|
|
1299
|
+
return null;
|
|
1208
1300
|
try {
|
|
1209
1301
|
const { execSync } = await import("node:child_process");
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1302
|
+
const out = execSync(`lsof -aPi -p ${pid} -sTCP:LISTEN -Fn 2>/dev/null || true`, { encoding: "utf-8" });
|
|
1303
|
+
const match = out.match(/:(\d+)$/m);
|
|
1304
|
+
if (match)
|
|
1305
|
+
return Number(match[1]);
|
|
1306
|
+
}
|
|
1307
|
+
catch { /* ignore */ }
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
// 1. Port check — is something listening?
|
|
1311
|
+
// First, check PID file so we can cross-reference
|
|
1312
|
+
const dataDir0 = defaultDataDir();
|
|
1313
|
+
const pidFile0 = join(dataDir0, "hdb.pid");
|
|
1314
|
+
let pidAlive = false;
|
|
1315
|
+
let pidValue = "";
|
|
1316
|
+
if (existsSync(pidFile0)) {
|
|
1317
|
+
const rawPid = (await import("node:fs")).readFileSync(pidFile0, "utf-8").trim();
|
|
1318
|
+
// Strict integer validation — PID must be purely numeric to prevent injection
|
|
1319
|
+
if (/^\d+$/.test(rawPid)) {
|
|
1320
|
+
pidValue = rawPid;
|
|
1321
|
+
try {
|
|
1322
|
+
process.kill(Number(pidValue), 0);
|
|
1323
|
+
pidAlive = true;
|
|
1324
|
+
}
|
|
1325
|
+
catch { /* dead */ }
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
console.log(` ⚠️ PID file contains non-numeric value: ${pidFile0} — skipping`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (await probePort(port)) {
|
|
1332
|
+
console.log(` ✅ Harper responding on port ${port}`);
|
|
1333
|
+
harperResponding = true;
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
// Port didn't respond — but if PID is alive, try to find the real port
|
|
1337
|
+
let discoveredPort = null;
|
|
1338
|
+
if (pidAlive) {
|
|
1339
|
+
discoveredPort = await discoverPortFromPid(pidValue);
|
|
1340
|
+
if (discoveredPort && discoveredPort !== port && await probePort(discoveredPort)) {
|
|
1341
|
+
console.log(` ⚠️ Harper not on expected port ${port}, but responding on port ${discoveredPort} (PID ${pidValue})`);
|
|
1342
|
+
console.log(` Your config says port ${port} but Harper is actually running on ${discoveredPort}`);
|
|
1343
|
+
if (autoFix) {
|
|
1344
|
+
if (dryRun) {
|
|
1345
|
+
console.log(` Would update config to port ${discoveredPort}`);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
writeConfig(discoveredPort);
|
|
1349
|
+
console.log(` ✅ Updated config to port ${discoveredPort}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
console.log(` Fix: flair doctor --fix (updates config to match running port)`);
|
|
1354
|
+
}
|
|
1355
|
+
effectivePort = discoveredPort;
|
|
1356
|
+
baseUrl = `http://127.0.0.1:${discoveredPort}`;
|
|
1357
|
+
harperResponding = true;
|
|
1358
|
+
issues++;
|
|
1214
1359
|
}
|
|
1215
1360
|
else {
|
|
1216
|
-
console.log(`
|
|
1217
|
-
console.log(` Fix: flair
|
|
1361
|
+
console.log(` ❌ Harper process alive (PID ${pidValue}) but not responding on any detected port`);
|
|
1362
|
+
console.log(` Fix: flair restart`);
|
|
1363
|
+
issues++;
|
|
1218
1364
|
}
|
|
1219
1365
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1366
|
+
else {
|
|
1367
|
+
// No live PID — Harper genuinely isn't running
|
|
1368
|
+
// Check if something else grabbed the port
|
|
1369
|
+
try {
|
|
1370
|
+
const { execSync } = await import("node:child_process");
|
|
1371
|
+
const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
|
|
1372
|
+
if (lsof) {
|
|
1373
|
+
console.log(` ❌ Nothing responding on port ${port} (port occupied by PID ${lsof})`);
|
|
1374
|
+
console.log(` Fix: kill ${lsof} && flair restart`);
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
console.log(` ❌ Harper is not running`);
|
|
1378
|
+
console.log(` Fix: flair restart`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
console.log(` ❌ Harper is not running`);
|
|
1383
|
+
if (autoFix) {
|
|
1384
|
+
if (dryRun) {
|
|
1385
|
+
console.log(` Would run: flair restart`);
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
console.log(` Attempting restart...`);
|
|
1389
|
+
try {
|
|
1390
|
+
const { execSync } = await import("node:child_process");
|
|
1391
|
+
execSync(`${process.argv[0]} ${process.argv[1]} restart --port ${port}`, { stdio: "inherit" });
|
|
1392
|
+
console.log(` ✅ Restart attempted`);
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
console.log(` ❌ Restart failed — try: flair init --agent-id <your-agent>`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
console.log(` Fix: flair restart`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
issues++;
|
|
1223
1404
|
}
|
|
1224
|
-
issues++;
|
|
1225
1405
|
}
|
|
1226
1406
|
// 2. Keys directory
|
|
1227
1407
|
const keysDir = defaultKeysDir();
|
|
@@ -1242,7 +1422,7 @@ program
|
|
|
1242
1422
|
issues++;
|
|
1243
1423
|
}
|
|
1244
1424
|
// 3. Config file
|
|
1245
|
-
const cfgPath =
|
|
1425
|
+
const cfgPath = configPath();
|
|
1246
1426
|
if (existsSync(cfgPath)) {
|
|
1247
1427
|
const savedPort = readPortFromConfig();
|
|
1248
1428
|
console.log(` ✅ Config: ${cfgPath} (port: ${savedPort ?? "default"})`);
|
|
@@ -1250,12 +1430,9 @@ program
|
|
|
1250
1430
|
else {
|
|
1251
1431
|
console.log(` ⚠️ No config file at ${cfgPath} — using defaults`);
|
|
1252
1432
|
}
|
|
1253
|
-
// 4. Embeddings check (only if Harper is
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
|
1433
|
+
// 4. Embeddings check (only if Harper is responding)
|
|
1434
|
+
if (harperResponding) {
|
|
1435
|
+
try {
|
|
1259
1436
|
const testRes = await fetch(`${baseUrl}/SemanticSearch`, {
|
|
1260
1437
|
method: "POST",
|
|
1261
1438
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1275,24 +1452,37 @@ program
|
|
|
1275
1452
|
}
|
|
1276
1453
|
}
|
|
1277
1454
|
else if (testRes.status === 401) {
|
|
1278
|
-
// Auth required — can't test embeddings without an agent key
|
|
1279
1455
|
console.log(` ⚠️ Embeddings: cannot verify (auth required for SemanticSearch)`);
|
|
1280
1456
|
}
|
|
1281
1457
|
}
|
|
1458
|
+
catch { /* fetch error, already flagged */ }
|
|
1282
1459
|
}
|
|
1283
|
-
|
|
1284
|
-
// 5. Stale PID file
|
|
1460
|
+
// 5. Stale PID file (skip if already reported in port check)
|
|
1285
1461
|
const dataDir = defaultDataDir();
|
|
1286
1462
|
const pidFile = join(dataDir, "hdb.pid");
|
|
1287
1463
|
if (existsSync(pidFile)) {
|
|
1288
1464
|
const pidContent = (await import("node:fs")).readFileSync(pidFile, "utf-8").trim();
|
|
1289
1465
|
try {
|
|
1290
|
-
process.kill(Number(pidContent), 0);
|
|
1291
|
-
|
|
1466
|
+
process.kill(Number(pidContent), 0);
|
|
1467
|
+
if (harperResponding) {
|
|
1468
|
+
console.log(` ✅ PID file: ${pidFile} (process ${pidContent} is alive)`);
|
|
1469
|
+
}
|
|
1470
|
+
// If not responding, we already reported the issue in step 1
|
|
1292
1471
|
}
|
|
1293
1472
|
catch {
|
|
1294
1473
|
console.log(` ❌ Stale PID file: ${pidFile} (process ${pidContent} is dead)`);
|
|
1295
|
-
|
|
1474
|
+
if (autoFix) {
|
|
1475
|
+
if (dryRun) {
|
|
1476
|
+
console.log(` Would remove: ${pidFile}`);
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
(await import("node:fs")).unlinkSync(pidFile);
|
|
1480
|
+
console.log(` ✅ Removed stale PID file`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
console.log(` Fix: rm ${pidFile} && flair restart`);
|
|
1485
|
+
}
|
|
1296
1486
|
issues++;
|
|
1297
1487
|
}
|
|
1298
1488
|
}
|
|
@@ -1355,7 +1545,7 @@ program
|
|
|
1355
1545
|
.option("--key <path>", "Ed25519 private key path")
|
|
1356
1546
|
.action(async (query, opts) => {
|
|
1357
1547
|
try {
|
|
1358
|
-
const baseUrl = opts.url || `http://127.0.0.1:${opts
|
|
1548
|
+
const baseUrl = opts.url || `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1359
1549
|
const headers = { "content-type": "application/json" };
|
|
1360
1550
|
const keyPath = opts.key || resolveKeyPath(opts.agent);
|
|
1361
1551
|
if (keyPath) {
|
|
@@ -1399,7 +1589,7 @@ program
|
|
|
1399
1589
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1400
1590
|
.option("--key <path>", "Ed25519 private key path")
|
|
1401
1591
|
.action(async (opts) => {
|
|
1402
|
-
const baseUrl = opts.url || `http://127.0.0.1:${opts
|
|
1592
|
+
const baseUrl = opts.url || `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1403
1593
|
try {
|
|
1404
1594
|
const headers = { "content-type": "application/json" };
|
|
1405
1595
|
const keyPath = opts.key || resolveKeyPath(opts.agent);
|
|
@@ -1449,7 +1639,7 @@ program
|
|
|
1449
1639
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1450
1640
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1451
1641
|
.action(async (opts) => {
|
|
1452
|
-
const baseUrl = opts.url ?? `http://127.0.0.1:${opts
|
|
1642
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1453
1643
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1454
1644
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
1455
1645
|
if (!adminPass) {
|
|
@@ -1529,7 +1719,7 @@ program
|
|
|
1529
1719
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1530
1720
|
.option("--dry-run", "Show what would be imported without making changes")
|
|
1531
1721
|
.action(async (backupPath, opts) => {
|
|
1532
|
-
const baseUrl = opts.url ?? `http://127.0.0.1:${opts
|
|
1722
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1533
1723
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1534
1724
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
1535
1725
|
const dryRun = Boolean(opts.dryRun);
|
|
@@ -1646,7 +1836,7 @@ program
|
|
|
1646
1836
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1647
1837
|
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
1648
1838
|
.action(async (agentId, opts) => {
|
|
1649
|
-
const baseUrl = opts.url ?? `http://127.0.0.1:${opts
|
|
1839
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1650
1840
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1651
1841
|
if (!adminPass) {
|
|
1652
1842
|
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
|
|
@@ -1733,8 +1923,8 @@ program
|
|
|
1733
1923
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1734
1924
|
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
1735
1925
|
.action(async (importPath, opts) => {
|
|
1736
|
-
const baseUrl = opts.url ?? `http://127.0.0.1:${opts
|
|
1737
|
-
const opsPort =
|
|
1926
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
1927
|
+
const opsPort = resolveOpsPort(opts);
|
|
1738
1928
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1739
1929
|
if (!adminPass) {
|
|
1740
1930
|
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
|
|
@@ -1862,7 +2052,7 @@ program
|
|
|
1862
2052
|
const fromDir = opts.from;
|
|
1863
2053
|
const toDir = opts.to;
|
|
1864
2054
|
const dryRun = opts.dryRun ?? false;
|
|
1865
|
-
const opsPort =
|
|
2055
|
+
const opsPort = resolveOpsPort(opts);
|
|
1866
2056
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1867
2057
|
if (!existsSync(fromDir)) {
|
|
1868
2058
|
console.log(`Old keys directory not found: ${fromDir}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|