@tpsdev-ai/flair 0.4.0 → 0.4.2

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.
Files changed (2) hide show
  1. package/dist/cli.js +272 -45
  2. 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
- return join(homedir(), ".flair", "config.yaml");
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 {
@@ -415,7 +420,7 @@ agent
415
420
  .command("add <id>")
416
421
  .description("Register a new agent in a running Flair instance")
417
422
  .option("--name <name>", "Display name (defaults to id)")
418
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
423
+ .option("--port <port>", "Harper HTTP port")
419
424
  .option("--admin-pass <pass>", "Admin password for registration")
420
425
  .option("--keys-dir <dir>", "Directory for Ed25519 keys")
421
426
  .option("--ops-port <port>", "Harper operations API port")
@@ -465,7 +470,7 @@ agent
465
470
  agent
466
471
  .command("rotate-key <id>")
467
472
  .description("Rotate an agent's Ed25519 keypair")
468
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
473
+ .option("--port <port>", "Harper HTTP port")
469
474
  .option("--ops-port <port>", "Harper operations API port")
470
475
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
471
476
  .option("--keys-dir <dir>", "Directory for Ed25519 keys")
@@ -543,7 +548,7 @@ agent
543
548
  .command("remove <id>")
544
549
  .description("Remove an agent and all its data from Flair")
545
550
  .option("--keep-keys", "Do not delete key files from disk")
546
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
551
+ .option("--port <port>", "Harper HTTP port")
547
552
  .option("--ops-port <port>", "Harper operations API port")
548
553
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
549
554
  .option("--keys-dir <dir>", "Directory for Ed25519 keys")
@@ -655,7 +660,7 @@ program
655
660
  .command("grant <from-agent> <to-agent>")
656
661
  .description("Grant an agent read access to another agent's memories")
657
662
  .option("--scope <scope>", "Grant scope: read or search", "read")
658
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
663
+ .option("--port <port>", "Harper HTTP port")
659
664
  .option("--ops-port <port>", "Harper operations API port")
660
665
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
661
666
  .option("--keys-dir <dir>", "Directory for Ed25519 keys (for from-agent Ed25519 auth)")
@@ -704,7 +709,7 @@ program
704
709
  program
705
710
  .command("revoke <from-agent> <to-agent>")
706
711
  .description("Revoke a memory grant between two agents")
707
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
712
+ .option("--port <port>", "Harper HTTP port")
708
713
  .option("--ops-port <port>", "Harper operations API port")
709
714
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
710
715
  .action(async (fromAgent, toAgent, opts) => {
@@ -1185,36 +1190,258 @@ program
1185
1190
  if (failed > 0)
1186
1191
  process.exit(1);
1187
1192
  });
1188
- // ─── Legacy identity/memory/soul commands (preserved) ────────────────────────
1189
- const identity = program.command("identity").description("Legacy identity commands");
1190
- identity.command("register")
1191
- .requiredOption("--id <id>")
1192
- .requiredOption("--name <name>")
1193
- .option("--role <role>")
1194
- .action(async (opts) => {
1195
- const kp = nacl.sign.keyPair();
1196
- const now = new Date().toISOString();
1197
- const agentRecord = await api("POST", "/Agent", {
1198
- id: opts.id, name: opts.name, role: opts.role,
1199
- publicKey: b64(kp.publicKey), createdAt: now, updatedAt: now,
1200
- });
1201
- console.log(JSON.stringify({ agent: agentRecord, privateKey: b64(kp.secretKey) }, null, 2));
1202
- });
1203
- identity.command("show").argument("<id>").action(async (id) => console.log(JSON.stringify(await api("GET", `/Agent/${id}`), null, 2)));
1204
- identity.command("list").action(async () => console.log(JSON.stringify(await api("GET", "/Agent"), null, 2)));
1205
- identity.command("add-integration")
1206
- .requiredOption("--agent <agentId>")
1207
- .requiredOption("--platform <platform>")
1208
- .requiredOption("--encrypted-credential <ciphertext>")
1193
+ // ─── flair doctor ─────────────────────────────────────────────────────────────
1194
+ program
1195
+ .command("doctor")
1196
+ .description("Diagnose common Flair problems and suggest fixes")
1197
+ .option("--port <port>", "Harper HTTP port")
1198
+ .option("--fix", "Automatically fix issues where possible")
1199
+ .option("--dry-run", "Show what --fix would do without making changes")
1209
1200
  .action(async (opts) => {
1210
- const now = new Date().toISOString();
1211
- const out = await api("POST", "/Integration", {
1212
- id: `${opts.agent}:${opts.platform}`, agentId: opts.agent,
1213
- platform: opts.platform, encryptedCredential: opts.encryptedCredential,
1214
- createdAt: now, updatedAt: now,
1215
- });
1216
- console.log(JSON.stringify(out, null, 2));
1201
+ const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
1202
+ const autoFix = opts.fix ?? false;
1203
+ const dryRun = opts.dryRun ?? false;
1204
+ if (dryRun && !autoFix) {
1205
+ console.log(" ℹ️ --dry-run only has effect with --fix\n");
1206
+ }
1207
+ let effectivePort = port;
1208
+ let baseUrl = `http://127.0.0.1:${port}`;
1209
+ let issues = 0;
1210
+ let harperResponding = false;
1211
+ console.log("\n🩺 Flair Doctor\n");
1212
+ // Helper: try to reach Harper on a given port
1213
+ async function probePort(p) {
1214
+ try {
1215
+ const res = await fetch(`http://127.0.0.1:${p}/Health`, { signal: AbortSignal.timeout(3000) });
1216
+ return res.status > 0;
1217
+ }
1218
+ catch {
1219
+ return false;
1220
+ }
1221
+ }
1222
+ // Helper: discover what port a Harper PID is listening on
1223
+ async function discoverPortFromPid(pid) {
1224
+ // Defense-in-depth: caller already validates, but re-check here
1225
+ if (!/^\d+$/.test(pid))
1226
+ return null;
1227
+ try {
1228
+ const { execSync } = await import("node:child_process");
1229
+ const out = execSync(`lsof -aPi -p ${pid} -sTCP:LISTEN -Fn 2>/dev/null || true`, { encoding: "utf-8" });
1230
+ const match = out.match(/:(\d+)$/m);
1231
+ if (match)
1232
+ return Number(match[1]);
1233
+ }
1234
+ catch { /* ignore */ }
1235
+ return null;
1236
+ }
1237
+ // 1. Port check — is something listening?
1238
+ // First, check PID file so we can cross-reference
1239
+ const dataDir0 = defaultDataDir();
1240
+ const pidFile0 = join(dataDir0, "hdb.pid");
1241
+ let pidAlive = false;
1242
+ let pidValue = "";
1243
+ if (existsSync(pidFile0)) {
1244
+ const rawPid = (await import("node:fs")).readFileSync(pidFile0, "utf-8").trim();
1245
+ // Strict integer validation — PID must be purely numeric to prevent injection
1246
+ if (/^\d+$/.test(rawPid)) {
1247
+ pidValue = rawPid;
1248
+ try {
1249
+ process.kill(Number(pidValue), 0);
1250
+ pidAlive = true;
1251
+ }
1252
+ catch { /* dead */ }
1253
+ }
1254
+ else {
1255
+ console.log(` ⚠️ PID file contains non-numeric value: ${pidFile0} — skipping`);
1256
+ }
1257
+ }
1258
+ if (await probePort(port)) {
1259
+ console.log(` ✅ Harper responding on port ${port}`);
1260
+ harperResponding = true;
1261
+ }
1262
+ else {
1263
+ // Port didn't respond — but if PID is alive, try to find the real port
1264
+ let discoveredPort = null;
1265
+ if (pidAlive) {
1266
+ discoveredPort = await discoverPortFromPid(pidValue);
1267
+ if (discoveredPort && discoveredPort !== port && await probePort(discoveredPort)) {
1268
+ console.log(` ⚠️ Harper not on expected port ${port}, but responding on port ${discoveredPort} (PID ${pidValue})`);
1269
+ console.log(` Your config says port ${port} but Harper is actually running on ${discoveredPort}`);
1270
+ if (autoFix) {
1271
+ if (dryRun) {
1272
+ console.log(` Would update config to port ${discoveredPort}`);
1273
+ }
1274
+ else {
1275
+ writeConfig(discoveredPort);
1276
+ console.log(` ✅ Updated config to port ${discoveredPort}`);
1277
+ }
1278
+ }
1279
+ else {
1280
+ console.log(` Fix: flair doctor --fix (updates config to match running port)`);
1281
+ }
1282
+ effectivePort = discoveredPort;
1283
+ baseUrl = `http://127.0.0.1:${discoveredPort}`;
1284
+ harperResponding = true;
1285
+ issues++;
1286
+ }
1287
+ else {
1288
+ console.log(` ❌ Harper process alive (PID ${pidValue}) but not responding on any detected port`);
1289
+ console.log(` Fix: flair restart`);
1290
+ issues++;
1291
+ }
1292
+ }
1293
+ else {
1294
+ // No live PID — Harper genuinely isn't running
1295
+ // Check if something else grabbed the port
1296
+ try {
1297
+ const { execSync } = await import("node:child_process");
1298
+ const lsof = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
1299
+ if (lsof) {
1300
+ console.log(` ❌ Nothing responding on port ${port} (port occupied by PID ${lsof})`);
1301
+ console.log(` Fix: kill ${lsof} && flair restart`);
1302
+ }
1303
+ else {
1304
+ console.log(` ❌ Harper is not running`);
1305
+ console.log(` Fix: flair restart`);
1306
+ }
1307
+ }
1308
+ catch {
1309
+ console.log(` ❌ Harper is not running`);
1310
+ if (autoFix) {
1311
+ if (dryRun) {
1312
+ console.log(` Would run: flair restart`);
1313
+ }
1314
+ else {
1315
+ console.log(` Attempting restart...`);
1316
+ try {
1317
+ const { execSync } = await import("node:child_process");
1318
+ execSync(`${process.argv[0]} ${process.argv[1]} restart --port ${port}`, { stdio: "inherit" });
1319
+ console.log(` ✅ Restart attempted`);
1320
+ }
1321
+ catch {
1322
+ console.log(` ❌ Restart failed — try: flair init --agent-id <your-agent>`);
1323
+ }
1324
+ }
1325
+ }
1326
+ else {
1327
+ console.log(` Fix: flair restart`);
1328
+ }
1329
+ }
1330
+ issues++;
1331
+ }
1332
+ }
1333
+ // 2. Keys directory
1334
+ const keysDir = defaultKeysDir();
1335
+ if (existsSync(keysDir)) {
1336
+ const keyFiles = (await import("node:fs")).readdirSync(keysDir).filter((f) => f.endsWith(".key"));
1337
+ if (keyFiles.length > 0) {
1338
+ console.log(` ✅ Keys found: ${keyFiles.length} agent(s) in ${keysDir}`);
1339
+ }
1340
+ else {
1341
+ console.log(` ❌ Keys directory exists but no .key files found`);
1342
+ console.log(` Fix: flair init --agent-id <your-agent>`);
1343
+ issues++;
1344
+ }
1345
+ }
1346
+ else {
1347
+ console.log(` ❌ Keys directory missing: ${keysDir}`);
1348
+ console.log(` Fix: flair init --agent-id <your-agent>`);
1349
+ issues++;
1350
+ }
1351
+ // 3. Config file
1352
+ const cfgPath = configPath();
1353
+ if (existsSync(cfgPath)) {
1354
+ const savedPort = readPortFromConfig();
1355
+ console.log(` ✅ Config: ${cfgPath} (port: ${savedPort ?? "default"})`);
1356
+ }
1357
+ else {
1358
+ console.log(` ⚠️ No config file at ${cfgPath} — using defaults`);
1359
+ }
1360
+ // 4. Embeddings check (only if Harper is responding)
1361
+ if (harperResponding) {
1362
+ try {
1363
+ const testRes = await fetch(`${baseUrl}/SemanticSearch`, {
1364
+ method: "POST",
1365
+ headers: { "Content-Type": "application/json" },
1366
+ body: JSON.stringify({ q: "test", limit: 1 }),
1367
+ signal: AbortSignal.timeout(10000),
1368
+ });
1369
+ if (testRes.ok) {
1370
+ const data = await testRes.json();
1371
+ if (data._warning) {
1372
+ console.log(` ⚠️ Embeddings: keyword-only (${data._warning})`);
1373
+ console.log(` Semantic search quality is degraded`);
1374
+ console.log(` Check: ls ~/.npm-global/lib/node_modules/@tpsdev-ai/flair/models/`);
1375
+ issues++;
1376
+ }
1377
+ else {
1378
+ console.log(` ✅ Embeddings: semantic search operational`);
1379
+ }
1380
+ }
1381
+ else if (testRes.status === 401) {
1382
+ console.log(` ⚠️ Embeddings: cannot verify (auth required for SemanticSearch)`);
1383
+ }
1384
+ }
1385
+ catch { /* fetch error, already flagged */ }
1386
+ }
1387
+ // 5. Stale PID file (skip if already reported in port check)
1388
+ const dataDir = defaultDataDir();
1389
+ const pidFile = join(dataDir, "hdb.pid");
1390
+ if (existsSync(pidFile)) {
1391
+ const pidContent = (await import("node:fs")).readFileSync(pidFile, "utf-8").trim();
1392
+ try {
1393
+ process.kill(Number(pidContent), 0);
1394
+ if (harperResponding) {
1395
+ console.log(` ✅ PID file: ${pidFile} (process ${pidContent} is alive)`);
1396
+ }
1397
+ // If not responding, we already reported the issue in step 1
1398
+ }
1399
+ catch {
1400
+ console.log(` ❌ Stale PID file: ${pidFile} (process ${pidContent} is dead)`);
1401
+ if (autoFix) {
1402
+ if (dryRun) {
1403
+ console.log(` Would remove: ${pidFile}`);
1404
+ }
1405
+ else {
1406
+ (await import("node:fs")).unlinkSync(pidFile);
1407
+ console.log(` ✅ Removed stale PID file`);
1408
+ }
1409
+ }
1410
+ else {
1411
+ console.log(` Fix: rm ${pidFile} && flair restart`);
1412
+ }
1413
+ issues++;
1414
+ }
1415
+ }
1416
+ // 6. Data directory
1417
+ if (existsSync(dataDir)) {
1418
+ console.log(` ✅ Data directory: ${dataDir}`);
1419
+ }
1420
+ else {
1421
+ // Check ~/harper/ (common alternative)
1422
+ const altDir = join(homedir(), "harper");
1423
+ if (existsSync(altDir)) {
1424
+ console.log(` ⚠️ Data at ~/harper/ (not ~/.flair/data) — old install location`);
1425
+ }
1426
+ else {
1427
+ console.log(` ❌ No data directory found`);
1428
+ console.log(` Fix: flair init --agent-id <your-agent>`);
1429
+ issues++;
1430
+ }
1431
+ }
1432
+ // Summary
1433
+ console.log("");
1434
+ if (issues === 0) {
1435
+ console.log(" 🟢 No issues found");
1436
+ }
1437
+ else {
1438
+ console.log(` 🔴 ${issues} issue${issues > 1 ? "s" : ""} found — see fixes above`);
1439
+ }
1440
+ console.log("");
1441
+ if (issues > 0)
1442
+ process.exit(1);
1217
1443
  });
1444
+ // ─── Memory and Soul commands ────────────────────────────────────────────────
1218
1445
  const memory = program.command("memory").description("Manage agent memories");
1219
1446
  memory.command("add").requiredOption("--agent <id>").requiredOption("--content <text>")
1220
1447
  .option("--durability <d>", "standard").option("--tags <csv>")
@@ -1240,7 +1467,7 @@ program
1240
1467
  .description("Search memories by meaning (shortcut for memory search)")
1241
1468
  .requiredOption("--agent <id>", "Agent ID")
1242
1469
  .option("--limit <n>", "Max results", "5")
1243
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1470
+ .option("--port <port>", "Harper HTTP port")
1244
1471
  .option("--url <url>", "Flair base URL (overrides --port)")
1245
1472
  .option("--key <path>", "Ed25519 private key path")
1246
1473
  .action(async (query, opts) => {
@@ -1285,7 +1512,7 @@ program
1285
1512
  .description("Cold-start context: get soul + recent memories as formatted text")
1286
1513
  .requiredOption("--agent <id>", "Agent ID")
1287
1514
  .option("--max-tokens <n>", "Maximum tokens in output", "4000")
1288
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1515
+ .option("--port <port>", "Harper HTTP port")
1289
1516
  .option("--url <url>", "Flair base URL (overrides --port)")
1290
1517
  .option("--key <path>", "Ed25519 private key path")
1291
1518
  .action(async (opts) => {
@@ -1335,7 +1562,7 @@ program
1335
1562
  .description("Export agents, memories, and souls to a JSON archive")
1336
1563
  .option("--output <path>", "Output file path (default: ~/.flair/backups/flair-backup-<timestamp>.json)")
1337
1564
  .option("--agents <ids>", "Comma-separated agent IDs to include (default: all)")
1338
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1565
+ .option("--port <port>", "Harper HTTP port")
1339
1566
  .option("--url <url>", "Flair base URL (overrides --port)")
1340
1567
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1341
1568
  .action(async (opts) => {
@@ -1414,7 +1641,7 @@ program
1414
1641
  .description("Import a Flair backup archive")
1415
1642
  .option("--merge", "Add/update records without deleting existing (default)")
1416
1643
  .option("--replace", "Delete all existing data for backed-up agents first, then import")
1417
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1644
+ .option("--port <port>", "Harper HTTP port")
1418
1645
  .option("--url <url>", "Flair base URL (overrides --port)")
1419
1646
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1420
1647
  .option("--dry-run", "Show what would be imported without making changes")
@@ -1531,7 +1758,7 @@ program
1531
1758
  .description("Export a single agent's identity (soul + memories) to a portable file")
1532
1759
  .option("--output <path>", "Output file path")
1533
1760
  .option("--include-key", "Include private key in export (UNENCRYPTED — keep the output file secure)")
1534
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1761
+ .option("--port <port>", "Harper HTTP port")
1535
1762
  .option("--url <url>", "Flair base URL (overrides --port)")
1536
1763
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1537
1764
  .option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
@@ -1617,7 +1844,7 @@ program
1617
1844
  program
1618
1845
  .command("import <path>")
1619
1846
  .description("Import an agent from an export file into this Flair instance")
1620
- .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1847
+ .option("--port <port>", "Harper HTTP port")
1621
1848
  .option("--ops-port <port>", "Harper operations API port")
1622
1849
  .option("--url <url>", "Flair base URL (overrides --port)")
1623
1850
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
@@ -1741,11 +1968,11 @@ program
1741
1968
  // ─── flair migrate-keys ───────────────────────────────────────────────────────
1742
1969
  program
1743
1970
  .command("migrate-keys")
1744
- .description("Migrate agent keys from legacy path (~/.tps/secrets/flair/) to ~/.flair/keys/")
1745
- .option("--from <dir>", "Legacy keys directory", join(homedir(), ".tps", "secrets", "flair"))
1971
+ .description("Migrate agent keys from old path (~/.tps/secrets/flair/) to ~/.flair/keys/")
1972
+ .option("--from <dir>", "Old keys directory", join(homedir(), ".tps", "secrets", "flair"))
1746
1973
  .option("--to <dir>", "New keys directory", defaultKeysDir())
1747
1974
  .option("--dry-run", "Show what would be migrated without copying")
1748
- .option("--port <port>", "Harper HTTP port (for agent list)", String(DEFAULT_PORT))
1975
+ .option("--port <port>", "Harper HTTP port")
1749
1976
  .option("--ops-port <port>", "Harper operations API port")
1750
1977
  .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1751
1978
  .action(async (opts) => {
@@ -1755,7 +1982,7 @@ program
1755
1982
  const opsPort = opts.opsPort ? Number(opts.opsPort) : Number(opts.port) + 1;
1756
1983
  const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
1757
1984
  if (!existsSync(fromDir)) {
1758
- console.log(`Legacy keys directory not found: ${fromDir}`);
1985
+ console.log(`Old keys directory not found: ${fromDir}`);
1759
1986
  console.log("Nothing to migrate.");
1760
1987
  process.exit(0);
1761
1988
  }
@@ -1797,7 +2024,7 @@ program
1797
2024
  else {
1798
2025
  console.log(`\n✅ Migration complete: ${migrated} migrated, ${skipped} skipped`);
1799
2026
  if (migrated > 0) {
1800
- console.log(`\nLegacy keys preserved at ${fromDir} (delete manually when confirmed working).`);
2027
+ console.log(`\nOld keys preserved at ${fromDir} (delete manually when confirmed working).`);
1801
2028
  // Optionally verify keys match Flair records
1802
2029
  if (adminPass) {
1803
2030
  console.log("\nVerifying migrated keys against Flair...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",