@tpsdev-ai/flair 0.4.1 → 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 +150 -33
  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 {
@@ -1190,38 +1195,140 @@ program
1190
1195
  .command("doctor")
1191
1196
  .description("Diagnose common Flair problems and suggest fixes")
1192
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")
1193
1200
  .action(async (opts) => {
1194
1201
  const port = opts.port ? Number(opts.port) : (readPortFromConfig() ?? DEFAULT_PORT);
1195
- const baseUrl = `http://127.0.0.1:${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}`;
1196
1209
  let issues = 0;
1210
+ let harperResponding = false;
1197
1211
  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}`);
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;
1203
1220
  }
1204
1221
  }
1205
- catch {
1206
- console.log(` ❌ Nothing responding on port ${port}`);
1207
- // Check if port is in use by something else
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;
1208
1227
  try {
1209
1228
  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`);
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++;
1214
1286
  }
1215
1287
  else {
1216
- console.log(` Harper is not running`);
1217
- console.log(` Fix: flair init --agent-id <your-agent>`);
1288
+ console.log(`Harper process alive (PID ${pidValue}) but not responding on any detected port`);
1289
+ console.log(` Fix: flair restart`);
1290
+ issues++;
1218
1291
  }
1219
1292
  }
1220
- catch {
1221
- console.log(` Harper is not running`);
1222
- console.log(` Fix: flair init --agent-id <your-agent>`);
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++;
1223
1331
  }
1224
- issues++;
1225
1332
  }
1226
1333
  // 2. Keys directory
1227
1334
  const keysDir = defaultKeysDir();
@@ -1242,7 +1349,7 @@ program
1242
1349
  issues++;
1243
1350
  }
1244
1351
  // 3. Config file
1245
- const cfgPath = join(homedir(), ".flair", "config.yaml");
1352
+ const cfgPath = configPath();
1246
1353
  if (existsSync(cfgPath)) {
1247
1354
  const savedPort = readPortFromConfig();
1248
1355
  console.log(` ✅ Config: ${cfgPath} (port: ${savedPort ?? "default"})`);
@@ -1250,12 +1357,9 @@ program
1250
1357
  else {
1251
1358
  console.log(` ⚠️ No config file at ${cfgPath} — using defaults`);
1252
1359
  }
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
1360
+ // 4. Embeddings check (only if Harper is responding)
1361
+ if (harperResponding) {
1362
+ try {
1259
1363
  const testRes = await fetch(`${baseUrl}/SemanticSearch`, {
1260
1364
  method: "POST",
1261
1365
  headers: { "Content-Type": "application/json" },
@@ -1275,24 +1379,37 @@ program
1275
1379
  }
1276
1380
  }
1277
1381
  else if (testRes.status === 401) {
1278
- // Auth required — can't test embeddings without an agent key
1279
1382
  console.log(` ⚠️ Embeddings: cannot verify (auth required for SemanticSearch)`);
1280
1383
  }
1281
1384
  }
1385
+ catch { /* fetch error, already flagged */ }
1282
1386
  }
1283
- catch { /* Harper not running, already flagged above */ }
1284
- // 5. Stale PID file
1387
+ // 5. Stale PID file (skip if already reported in port check)
1285
1388
  const dataDir = defaultDataDir();
1286
1389
  const pidFile = join(dataDir, "hdb.pid");
1287
1390
  if (existsSync(pidFile)) {
1288
1391
  const pidContent = (await import("node:fs")).readFileSync(pidFile, "utf-8").trim();
1289
1392
  try {
1290
- process.kill(Number(pidContent), 0); // check if process exists
1291
- console.log(` ✅ PID file: ${pidFile} (process ${pidContent} is alive)`);
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
1292
1398
  }
1293
1399
  catch {
1294
1400
  console.log(` ❌ Stale PID file: ${pidFile} (process ${pidContent} is dead)`);
1295
- console.log(` Fix: rm ${pidFile} && flair restart`);
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
+ }
1296
1413
  issues++;
1297
1414
  }
1298
1415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.4.1",
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",