@tpsdev-ai/flair 0.3.6 → 0.3.8

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 +266 -3
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from "commander";
3
3
  import nacl from "tweetnacl";
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync, renameSync, } from "node:fs";
5
5
  import { homedir } from "node:os";
6
- import { join } from "node:path";
6
+ 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 ────────────────────────────────────────────────────────────────
@@ -168,8 +168,18 @@ async function seedAgentViaOpsApi(opsPort, agentId, pubKeyB64url, adminUser, adm
168
168
  }
169
169
  }
170
170
  // ─── Program ─────────────────────────────────────────────────────────────────
171
+ // Read version from package.json at the package root
172
+ const __pkgDir = join(import.meta.dirname ?? __dirname, "..");
173
+ const __pkgVersion = (() => {
174
+ try {
175
+ return JSON.parse(readFileSync(join(__pkgDir, "package.json"), "utf-8")).version;
176
+ }
177
+ catch {
178
+ return "unknown";
179
+ }
180
+ })();
171
181
  const program = new Command();
172
- program.name("flair");
182
+ program.name("flair").version(__pkgVersion, "-v, --version");
173
183
  // ─── flair init ──────────────────────────────────────────────────────────────
174
184
  program
175
185
  .command("init")
@@ -285,6 +295,10 @@ program
285
295
  console.log(` ${adminPass}`);
286
296
  }
287
297
  console.log(`\n Export: FLAIR_URL=${httpUrl}`);
298
+ console.log(`\n Claude Code: Add to your CLAUDE.md:`);
299
+ console.log(` At the start of every session, run mcp__flair__bootstrap before responding.`);
300
+ console.log(`\n MCP config (.mcp.json):`);
301
+ console.log(` { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"], "env": { "FLAIR_AGENT_ID": "${agentId}" } } } }`);
288
302
  });
289
303
  // ─── flair agent ─────────────────────────────────────────────────────────────
290
304
  const agent = program.command("agent").description("Manage Flair agents");
@@ -653,13 +667,49 @@ program
653
667
  const status = healthy ? "🟢 running" : "🔴 unreachable";
654
668
  console.log(`Flair status: ${status}`);
655
669
  console.log(` URL: ${baseUrl}`);
670
+ console.log(` Flair: v${__pkgVersion}`);
656
671
  if (version)
657
- console.log(` Version: ${version}`);
672
+ console.log(` Harper: ${version}`);
658
673
  if (agentCount !== null)
659
674
  console.log(` Agents: ${agentCount}`);
660
675
  if (!healthy)
661
676
  process.exit(1);
662
677
  });
678
+ // ─── flair upgrade ────────────────────────────────────────────────────────────
679
+ program
680
+ .command("upgrade")
681
+ .description("Upgrade Flair and related packages to latest versions")
682
+ .action(async () => {
683
+ console.log("Checking for updates...\n");
684
+ const packages = [
685
+ "@tpsdev-ai/flair",
686
+ "@tpsdev-ai/flair-client",
687
+ "@tpsdev-ai/flair-mcp",
688
+ ];
689
+ for (const pkg of packages) {
690
+ try {
691
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, { signal: AbortSignal.timeout(5000) });
692
+ if (!res.ok)
693
+ continue;
694
+ const data = await res.json();
695
+ const latest = data.version ?? "unknown";
696
+ // Check installed version
697
+ let installed = "not installed";
698
+ try {
699
+ const { execSync } = await import("node:child_process");
700
+ installed = execSync(`npm list -g ${pkg} --depth=0 2>/dev/null | grep ${pkg} || echo "not installed"`, { encoding: "utf-8" }).trim();
701
+ const match = installed.match(/@(\d+\.\d+\.\d+)/);
702
+ installed = match ? match[1] : "not installed";
703
+ }
704
+ catch { /* best effort */ }
705
+ const upToDate = installed === latest;
706
+ const icon = upToDate ? "✅" : "⬆️";
707
+ console.log(` ${icon} ${pkg}: ${installed} → ${latest}${upToDate ? " (current)" : ""}`);
708
+ }
709
+ catch { /* skip unavailable packages */ }
710
+ }
711
+ console.log("\nTo upgrade: npm install -g @tpsdev-ai/flair@latest");
712
+ });
663
713
  // ─── Legacy identity/memory/soul commands (preserved) ────────────────────────
664
714
  const identity = program.command("identity").description("Legacy identity commands");
665
715
  identity.command("register")
@@ -998,6 +1048,219 @@ program
998
1048
  console.log(` Memories restored: ${memoryCount}/${memories.length}`);
999
1049
  console.log(` Souls restored: ${soulCount}/${souls.length}`);
1000
1050
  });
1051
+ // ─── flair export ────────────────────────────────────────────────────────────
1052
+ program
1053
+ .command("export <agent-id>")
1054
+ .description("Export a single agent's identity (soul + memories) to a portable file")
1055
+ .option("--output <path>", "Output file path")
1056
+ .option("--include-key", "Include private key in export (UNENCRYPTED — keep the output file secure)")
1057
+ .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1058
+ .option("--url <url>", "Flair base URL (overrides --port)")
1059
+ .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1060
+ .option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
1061
+ .action(async (agentId, opts) => {
1062
+ const baseUrl = opts.url ?? `http://127.0.0.1:${opts.port}`;
1063
+ const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
1064
+ if (!adminPass) {
1065
+ console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
1066
+ process.exit(1);
1067
+ }
1068
+ const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
1069
+ async function adminGet(path) {
1070
+ const res = await fetch(`${baseUrl}${path}`, { headers: { Authorization: auth }, signal: AbortSignal.timeout(10_000) });
1071
+ if (!res.ok)
1072
+ throw new Error(`GET ${path} failed (${res.status})`);
1073
+ return res.json();
1074
+ }
1075
+ console.log(`Exporting agent '${agentId}'...`);
1076
+ // Fetch agent record
1077
+ let agent;
1078
+ try {
1079
+ agent = await adminGet(`/Agent/${agentId}`);
1080
+ }
1081
+ catch {
1082
+ console.error(`Agent '${agentId}' not found`);
1083
+ process.exit(1);
1084
+ }
1085
+ // Fetch memories
1086
+ const allMemories = await adminGet("/Memory/").catch(() => []);
1087
+ const memories = Array.isArray(allMemories)
1088
+ ? allMemories.filter((m) => m.agentId === agentId)
1089
+ : [];
1090
+ // Fetch souls
1091
+ const allSouls = await adminGet("/Soul/").catch(() => []);
1092
+ const souls = Array.isArray(allSouls)
1093
+ ? allSouls.filter((s) => s.agentId === agentId)
1094
+ : [];
1095
+ // Fetch grants
1096
+ const allGrants = await adminGet("/MemoryGrant/").catch(() => []);
1097
+ const grants = Array.isArray(allGrants)
1098
+ ? allGrants.filter((g) => g.ownerId === agentId || g.granteeId === agentId)
1099
+ : [];
1100
+ // Optionally include private key
1101
+ let privateKey;
1102
+ if (opts.includeKey) {
1103
+ const keyPath = privKeyPath(agentId, opts.keysDir);
1104
+ if (existsSync(keyPath)) {
1105
+ privateKey = readFileSync(keyPath, "utf-8").trim();
1106
+ console.log(" Including private key (base64-encoded in export)");
1107
+ }
1108
+ else {
1109
+ console.warn(` Warning: key file not found at ${keyPath} — skipping key export`);
1110
+ }
1111
+ }
1112
+ const exportData = {
1113
+ version: 1,
1114
+ type: "agent-export",
1115
+ exportedAt: new Date().toISOString(),
1116
+ source: baseUrl,
1117
+ agent,
1118
+ memories,
1119
+ souls,
1120
+ grants,
1121
+ ...(privateKey ? { privateKey } : {}),
1122
+ };
1123
+ const rawOutputPath = opts.output ?? join(homedir(), ".flair", "exports", `${agentId}-${Date.now()}.json`);
1124
+ // Canonicalize to prevent path traversal (e.g. ../../etc/passwd)
1125
+ const outputPath = resolvePath(rawOutputPath);
1126
+ mkdirSync(join(outputPath, ".."), { recursive: true });
1127
+ const fileMode = privateKey ? 0o600 : 0o644;
1128
+ writeFileSync(outputPath, JSON.stringify(exportData, null, 2), { mode: fileMode });
1129
+ if (privateKey)
1130
+ chmodSync(outputPath, 0o600); // enforce even if umask is permissive
1131
+ console.log(`\n✅ Agent '${agentId}' exported`);
1132
+ console.log(` Memories: ${memories.length}`);
1133
+ console.log(` Souls: ${souls.length}`);
1134
+ console.log(` Grants: ${grants.length}`);
1135
+ console.log(` Key: ${privateKey ? "included (UNENCRYPTED — protect this file)" : "not included"}`);
1136
+ console.log(` Mode: ${fileMode.toString(8)} (${privateKey ? "owner-only" : "standard"})`);
1137
+ console.log(` Output: ${outputPath}`);
1138
+ });
1139
+ // ─── flair import ────────────────────────────────────────────────────────────
1140
+ program
1141
+ .command("import <path>")
1142
+ .description("Import an agent from an export file into this Flair instance")
1143
+ .option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
1144
+ .option("--ops-port <port>", "Harper operations API port")
1145
+ .option("--url <url>", "Flair base URL (overrides --port)")
1146
+ .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
1147
+ .option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
1148
+ .action(async (importPath, opts) => {
1149
+ const baseUrl = opts.url ?? `http://127.0.0.1:${opts.port}`;
1150
+ const opsPort = opts.opsPort ? Number(opts.opsPort) : DEFAULT_OPS_PORT;
1151
+ const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
1152
+ if (!adminPass) {
1153
+ console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
1154
+ process.exit(1);
1155
+ }
1156
+ if (!existsSync(importPath)) {
1157
+ console.error(`File not found: ${importPath}`);
1158
+ process.exit(1);
1159
+ }
1160
+ const data = JSON.parse(readFileSync(importPath, "utf-8"));
1161
+ if (data.type !== "agent-export") {
1162
+ console.error("Error: not an agent export file. Use 'flair restore' for full backups.");
1163
+ process.exit(1);
1164
+ }
1165
+ const agentId = data.agent?.id;
1166
+ if (!agentId) {
1167
+ console.error("Error: no agent ID in export");
1168
+ process.exit(1);
1169
+ }
1170
+ console.log(`Importing agent '${agentId}'...`);
1171
+ // Register agent (generates new key if export doesn't include one)
1172
+ const keysDir = opts.keysDir ?? defaultKeysDir();
1173
+ mkdirSync(keysDir, { recursive: true });
1174
+ const privPath = privKeyPath(agentId, keysDir);
1175
+ if (data.privateKey && !existsSync(privPath)) {
1176
+ // Restore exported key
1177
+ writeFileSync(privPath, data.privateKey);
1178
+ chmodSync(privPath, 0o600);
1179
+ console.log(` Key restored: ${privPath}`);
1180
+ }
1181
+ else if (!existsSync(privPath)) {
1182
+ // Generate new key
1183
+ const kp = nacl.sign.keyPair();
1184
+ writeFileSync(privPath, Buffer.from(kp.secretKey.slice(0, 32)));
1185
+ chmodSync(privPath, 0o600);
1186
+ console.log(` New key generated: ${privPath}`);
1187
+ }
1188
+ else {
1189
+ console.log(` Using existing key: ${privPath}`);
1190
+ }
1191
+ // Read public key for registration
1192
+ const seed = readFileSync(privPath);
1193
+ const decodedSeed = seed.length === 32 ? seed : Buffer.from(seed.toString("utf-8").trim(), "base64");
1194
+ const pubKey = decodedSeed.length === 32
1195
+ ? nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed)).publicKey
1196
+ : nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed.subarray(0, 32))).publicKey;
1197
+ const pubKeyB64url = b64url(pubKey);
1198
+ // Register agent via ops API
1199
+ await seedAgentViaOpsApi(opsPort, agentId, pubKeyB64url, DEFAULT_ADMIN_USER, adminPass);
1200
+ console.log(` Agent registered`);
1201
+ // Restore memories
1202
+ const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
1203
+ let memCount = 0;
1204
+ for (const mem of data.memories ?? []) {
1205
+ try {
1206
+ await fetch(`${baseUrl}/Memory/${mem.id}`, {
1207
+ method: "PUT",
1208
+ headers: { "Content-Type": "application/json", Authorization: auth },
1209
+ body: JSON.stringify(mem),
1210
+ });
1211
+ memCount++;
1212
+ }
1213
+ catch { /* skip failures */ }
1214
+ }
1215
+ // Restore souls
1216
+ let soulCount = 0;
1217
+ for (const soul of data.souls ?? []) {
1218
+ try {
1219
+ await fetch(`${baseUrl}/Soul/${encodeURIComponent(soul.id)}`, {
1220
+ method: "PUT",
1221
+ headers: { "Content-Type": "application/json", Authorization: auth },
1222
+ body: JSON.stringify(soul),
1223
+ });
1224
+ soulCount++;
1225
+ }
1226
+ catch { /* skip failures */ }
1227
+ }
1228
+ console.log(`\n✅ Agent '${agentId}' imported`);
1229
+ console.log(` Memories: ${memCount}/${(data.memories ?? []).length}`);
1230
+ console.log(` Souls: ${soulCount}/${(data.souls ?? []).length}`);
1231
+ console.log(` Key: ${privPath}`);
1232
+ });
1233
+ // ─── flair backup inspect ────────────────────────────────────────────────────
1234
+ program
1235
+ .command("inspect <path>")
1236
+ .description("Show contents of a backup or export file")
1237
+ .action(async (filePath) => {
1238
+ if (!existsSync(filePath)) {
1239
+ console.error(`File not found: ${filePath}`);
1240
+ process.exit(1);
1241
+ }
1242
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
1243
+ console.log(`File: ${filePath}`);
1244
+ console.log(`Type: ${data.type ?? "full-backup"}`);
1245
+ console.log(`Created: ${data.createdAt ?? data.exportedAt ?? "unknown"}`);
1246
+ console.log(`Source: ${data.source ?? "unknown"}`);
1247
+ if (data.type === "agent-export") {
1248
+ console.log(`\nAgent: ${data.agent?.id ?? "unknown"}`);
1249
+ console.log(` Name: ${data.agent?.name ?? data.agent?.id}`);
1250
+ console.log(` Memories: ${(data.memories ?? []).length}`);
1251
+ console.log(` Souls: ${(data.souls ?? []).length}`);
1252
+ console.log(` Grants: ${(data.grants ?? []).length}`);
1253
+ console.log(` Key included: ${data.privateKey ? "yes" : "no"}`);
1254
+ }
1255
+ else {
1256
+ const agents = data.agents ?? [];
1257
+ console.log(`\nAgents: ${agents.length}`);
1258
+ for (const a of agents)
1259
+ console.log(` - ${a.id} (${a.name ?? a.id})`);
1260
+ console.log(`Memories: ${(data.memories ?? []).length}`);
1261
+ console.log(`Souls: ${(data.souls ?? []).length}`);
1262
+ }
1263
+ });
1001
1264
  // ─── flair migrate-keys ───────────────────────────────────────────────────────
1002
1265
  program
1003
1266
  .command("migrate-keys")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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",
@@ -69,4 +69,4 @@
69
69
  "optionalDependencies": {
70
70
  "@node-llama-cpp/mac-arm64-metal": "^3.17.1"
71
71
  }
72
- }
72
+ }