@tpsdev-ai/flair 0.3.7 → 0.3.9
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 +287 -4
- package/package.json +1 -1
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 ────────────────────────────────────────────────────────────────
|
|
@@ -60,12 +60,36 @@ async function api(method, path, body) {
|
|
|
60
60
|
}
|
|
61
61
|
catch { /* ignore config read errors */ }
|
|
62
62
|
const base = process.env.FLAIR_URL || defaultUrl;
|
|
63
|
+
// Auth resolution order:
|
|
64
|
+
// 1. FLAIR_TOKEN env → Bearer token (backward compat)
|
|
65
|
+
// 2. FLAIR_AGENT_ID env + key file → Ed25519 signature (standard)
|
|
66
|
+
// 3. --agent flag extracted from body.agentId + key file → Ed25519 signature
|
|
67
|
+
// 4. No auth (will 401 on any authenticated endpoint)
|
|
68
|
+
let authHeader;
|
|
63
69
|
const token = process.env.FLAIR_TOKEN;
|
|
70
|
+
if (token) {
|
|
71
|
+
authHeader = `Bearer ${token}`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const agentId = process.env.FLAIR_AGENT_ID || (body && typeof body === "object" ? body.agentId : undefined);
|
|
75
|
+
if (agentId) {
|
|
76
|
+
const keyPath = resolveKeyPath(agentId);
|
|
77
|
+
if (keyPath) {
|
|
78
|
+
try {
|
|
79
|
+
authHeader = buildEd25519Auth(agentId, method, path, keyPath);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Key exists but auth build failed — warn and continue without auth
|
|
83
|
+
console.error(`Warning: Ed25519 auth failed for agent '${agentId}': ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
64
88
|
const res = await fetch(`${base}${path}`, {
|
|
65
89
|
method,
|
|
66
90
|
headers: {
|
|
67
91
|
"content-type": "application/json",
|
|
68
|
-
...(
|
|
92
|
+
...(authHeader ? { authorization: authHeader } : {}),
|
|
69
93
|
},
|
|
70
94
|
body: body ? JSON.stringify(body) : undefined,
|
|
71
95
|
});
|
|
@@ -168,8 +192,18 @@ async function seedAgentViaOpsApi(opsPort, agentId, pubKeyB64url, adminUser, adm
|
|
|
168
192
|
}
|
|
169
193
|
}
|
|
170
194
|
// ─── Program ─────────────────────────────────────────────────────────────────
|
|
195
|
+
// Read version from package.json at the package root
|
|
196
|
+
const __pkgDir = join(import.meta.dirname ?? __dirname, "..");
|
|
197
|
+
const __pkgVersion = (() => {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(readFileSync(join(__pkgDir, "package.json"), "utf-8")).version;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return "unknown";
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
171
205
|
const program = new Command();
|
|
172
|
-
program.name("flair");
|
|
206
|
+
program.name("flair").version(__pkgVersion, "-v, --version");
|
|
173
207
|
// ─── flair init ──────────────────────────────────────────────────────────────
|
|
174
208
|
program
|
|
175
209
|
.command("init")
|
|
@@ -657,13 +691,49 @@ program
|
|
|
657
691
|
const status = healthy ? "🟢 running" : "🔴 unreachable";
|
|
658
692
|
console.log(`Flair status: ${status}`);
|
|
659
693
|
console.log(` URL: ${baseUrl}`);
|
|
694
|
+
console.log(` Flair: v${__pkgVersion}`);
|
|
660
695
|
if (version)
|
|
661
|
-
console.log(`
|
|
696
|
+
console.log(` Harper: ${version}`);
|
|
662
697
|
if (agentCount !== null)
|
|
663
698
|
console.log(` Agents: ${agentCount}`);
|
|
664
699
|
if (!healthy)
|
|
665
700
|
process.exit(1);
|
|
666
701
|
});
|
|
702
|
+
// ─── flair upgrade ────────────────────────────────────────────────────────────
|
|
703
|
+
program
|
|
704
|
+
.command("upgrade")
|
|
705
|
+
.description("Upgrade Flair and related packages to latest versions")
|
|
706
|
+
.action(async () => {
|
|
707
|
+
console.log("Checking for updates...\n");
|
|
708
|
+
const packages = [
|
|
709
|
+
"@tpsdev-ai/flair",
|
|
710
|
+
"@tpsdev-ai/flair-client",
|
|
711
|
+
"@tpsdev-ai/flair-mcp",
|
|
712
|
+
];
|
|
713
|
+
for (const pkg of packages) {
|
|
714
|
+
try {
|
|
715
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, { signal: AbortSignal.timeout(5000) });
|
|
716
|
+
if (!res.ok)
|
|
717
|
+
continue;
|
|
718
|
+
const data = await res.json();
|
|
719
|
+
const latest = data.version ?? "unknown";
|
|
720
|
+
// Check installed version
|
|
721
|
+
let installed = "not installed";
|
|
722
|
+
try {
|
|
723
|
+
const { execSync } = await import("node:child_process");
|
|
724
|
+
installed = execSync(`npm list -g ${pkg} --depth=0 2>/dev/null | grep ${pkg} || echo "not installed"`, { encoding: "utf-8" }).trim();
|
|
725
|
+
const match = installed.match(/@(\d+\.\d+\.\d+)/);
|
|
726
|
+
installed = match ? match[1] : "not installed";
|
|
727
|
+
}
|
|
728
|
+
catch { /* best effort */ }
|
|
729
|
+
const upToDate = installed === latest;
|
|
730
|
+
const icon = upToDate ? "✅" : "⬆️";
|
|
731
|
+
console.log(` ${icon} ${pkg}: ${installed} → ${latest}${upToDate ? " (current)" : ""}`);
|
|
732
|
+
}
|
|
733
|
+
catch { /* skip unavailable packages */ }
|
|
734
|
+
}
|
|
735
|
+
console.log("\nTo upgrade: npm install -g @tpsdev-ai/flair@latest");
|
|
736
|
+
});
|
|
667
737
|
// ─── Legacy identity/memory/soul commands (preserved) ────────────────────────
|
|
668
738
|
const identity = program.command("identity").description("Legacy identity commands");
|
|
669
739
|
identity.command("register")
|
|
@@ -1002,6 +1072,219 @@ program
|
|
|
1002
1072
|
console.log(` Memories restored: ${memoryCount}/${memories.length}`);
|
|
1003
1073
|
console.log(` Souls restored: ${soulCount}/${souls.length}`);
|
|
1004
1074
|
});
|
|
1075
|
+
// ─── flair export ────────────────────────────────────────────────────────────
|
|
1076
|
+
program
|
|
1077
|
+
.command("export <agent-id>")
|
|
1078
|
+
.description("Export a single agent's identity (soul + memories) to a portable file")
|
|
1079
|
+
.option("--output <path>", "Output file path")
|
|
1080
|
+
.option("--include-key", "Include private key in export (UNENCRYPTED — keep the output file secure)")
|
|
1081
|
+
.option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
|
|
1082
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1083
|
+
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1084
|
+
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
1085
|
+
.action(async (agentId, opts) => {
|
|
1086
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${opts.port}`;
|
|
1087
|
+
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1088
|
+
if (!adminPass) {
|
|
1089
|
+
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
|
|
1093
|
+
async function adminGet(path) {
|
|
1094
|
+
const res = await fetch(`${baseUrl}${path}`, { headers: { Authorization: auth }, signal: AbortSignal.timeout(10_000) });
|
|
1095
|
+
if (!res.ok)
|
|
1096
|
+
throw new Error(`GET ${path} failed (${res.status})`);
|
|
1097
|
+
return res.json();
|
|
1098
|
+
}
|
|
1099
|
+
console.log(`Exporting agent '${agentId}'...`);
|
|
1100
|
+
// Fetch agent record
|
|
1101
|
+
let agent;
|
|
1102
|
+
try {
|
|
1103
|
+
agent = await adminGet(`/Agent/${agentId}`);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
console.error(`Agent '${agentId}' not found`);
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
// Fetch memories
|
|
1110
|
+
const allMemories = await adminGet("/Memory/").catch(() => []);
|
|
1111
|
+
const memories = Array.isArray(allMemories)
|
|
1112
|
+
? allMemories.filter((m) => m.agentId === agentId)
|
|
1113
|
+
: [];
|
|
1114
|
+
// Fetch souls
|
|
1115
|
+
const allSouls = await adminGet("/Soul/").catch(() => []);
|
|
1116
|
+
const souls = Array.isArray(allSouls)
|
|
1117
|
+
? allSouls.filter((s) => s.agentId === agentId)
|
|
1118
|
+
: [];
|
|
1119
|
+
// Fetch grants
|
|
1120
|
+
const allGrants = await adminGet("/MemoryGrant/").catch(() => []);
|
|
1121
|
+
const grants = Array.isArray(allGrants)
|
|
1122
|
+
? allGrants.filter((g) => g.ownerId === agentId || g.granteeId === agentId)
|
|
1123
|
+
: [];
|
|
1124
|
+
// Optionally include private key
|
|
1125
|
+
let privateKey;
|
|
1126
|
+
if (opts.includeKey) {
|
|
1127
|
+
const keyPath = privKeyPath(agentId, opts.keysDir);
|
|
1128
|
+
if (existsSync(keyPath)) {
|
|
1129
|
+
privateKey = readFileSync(keyPath, "utf-8").trim();
|
|
1130
|
+
console.log(" Including private key (base64-encoded in export)");
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
console.warn(` Warning: key file not found at ${keyPath} — skipping key export`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const exportData = {
|
|
1137
|
+
version: 1,
|
|
1138
|
+
type: "agent-export",
|
|
1139
|
+
exportedAt: new Date().toISOString(),
|
|
1140
|
+
source: baseUrl,
|
|
1141
|
+
agent,
|
|
1142
|
+
memories,
|
|
1143
|
+
souls,
|
|
1144
|
+
grants,
|
|
1145
|
+
...(privateKey ? { privateKey } : {}),
|
|
1146
|
+
};
|
|
1147
|
+
const rawOutputPath = opts.output ?? join(homedir(), ".flair", "exports", `${agentId}-${Date.now()}.json`);
|
|
1148
|
+
// Canonicalize to prevent path traversal (e.g. ../../etc/passwd)
|
|
1149
|
+
const outputPath = resolvePath(rawOutputPath);
|
|
1150
|
+
mkdirSync(join(outputPath, ".."), { recursive: true });
|
|
1151
|
+
const fileMode = privateKey ? 0o600 : 0o644;
|
|
1152
|
+
writeFileSync(outputPath, JSON.stringify(exportData, null, 2), { mode: fileMode });
|
|
1153
|
+
if (privateKey)
|
|
1154
|
+
chmodSync(outputPath, 0o600); // enforce even if umask is permissive
|
|
1155
|
+
console.log(`\n✅ Agent '${agentId}' exported`);
|
|
1156
|
+
console.log(` Memories: ${memories.length}`);
|
|
1157
|
+
console.log(` Souls: ${souls.length}`);
|
|
1158
|
+
console.log(` Grants: ${grants.length}`);
|
|
1159
|
+
console.log(` Key: ${privateKey ? "included (UNENCRYPTED — protect this file)" : "not included"}`);
|
|
1160
|
+
console.log(` Mode: ${fileMode.toString(8)} (${privateKey ? "owner-only" : "standard"})`);
|
|
1161
|
+
console.log(` Output: ${outputPath}`);
|
|
1162
|
+
});
|
|
1163
|
+
// ─── flair import ────────────────────────────────────────────────────────────
|
|
1164
|
+
program
|
|
1165
|
+
.command("import <path>")
|
|
1166
|
+
.description("Import an agent from an export file into this Flair instance")
|
|
1167
|
+
.option("--port <port>", "Harper HTTP port", String(DEFAULT_PORT))
|
|
1168
|
+
.option("--ops-port <port>", "Harper operations API port")
|
|
1169
|
+
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
1170
|
+
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
1171
|
+
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
1172
|
+
.action(async (importPath, opts) => {
|
|
1173
|
+
const baseUrl = opts.url ?? `http://127.0.0.1:${opts.port}`;
|
|
1174
|
+
const opsPort = opts.opsPort ? Number(opts.opsPort) : DEFAULT_OPS_PORT;
|
|
1175
|
+
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
1176
|
+
if (!adminPass) {
|
|
1177
|
+
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
if (!existsSync(importPath)) {
|
|
1181
|
+
console.error(`File not found: ${importPath}`);
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
const data = JSON.parse(readFileSync(importPath, "utf-8"));
|
|
1185
|
+
if (data.type !== "agent-export") {
|
|
1186
|
+
console.error("Error: not an agent export file. Use 'flair restore' for full backups.");
|
|
1187
|
+
process.exit(1);
|
|
1188
|
+
}
|
|
1189
|
+
const agentId = data.agent?.id;
|
|
1190
|
+
if (!agentId) {
|
|
1191
|
+
console.error("Error: no agent ID in export");
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
console.log(`Importing agent '${agentId}'...`);
|
|
1195
|
+
// Register agent (generates new key if export doesn't include one)
|
|
1196
|
+
const keysDir = opts.keysDir ?? defaultKeysDir();
|
|
1197
|
+
mkdirSync(keysDir, { recursive: true });
|
|
1198
|
+
const privPath = privKeyPath(agentId, keysDir);
|
|
1199
|
+
if (data.privateKey && !existsSync(privPath)) {
|
|
1200
|
+
// Restore exported key
|
|
1201
|
+
writeFileSync(privPath, data.privateKey);
|
|
1202
|
+
chmodSync(privPath, 0o600);
|
|
1203
|
+
console.log(` Key restored: ${privPath}`);
|
|
1204
|
+
}
|
|
1205
|
+
else if (!existsSync(privPath)) {
|
|
1206
|
+
// Generate new key
|
|
1207
|
+
const kp = nacl.sign.keyPair();
|
|
1208
|
+
writeFileSync(privPath, Buffer.from(kp.secretKey.slice(0, 32)));
|
|
1209
|
+
chmodSync(privPath, 0o600);
|
|
1210
|
+
console.log(` New key generated: ${privPath}`);
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
console.log(` Using existing key: ${privPath}`);
|
|
1214
|
+
}
|
|
1215
|
+
// Read public key for registration
|
|
1216
|
+
const seed = readFileSync(privPath);
|
|
1217
|
+
const decodedSeed = seed.length === 32 ? seed : Buffer.from(seed.toString("utf-8").trim(), "base64");
|
|
1218
|
+
const pubKey = decodedSeed.length === 32
|
|
1219
|
+
? nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed)).publicKey
|
|
1220
|
+
: nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed.subarray(0, 32))).publicKey;
|
|
1221
|
+
const pubKeyB64url = b64url(pubKey);
|
|
1222
|
+
// Register agent via ops API
|
|
1223
|
+
await seedAgentViaOpsApi(opsPort, agentId, pubKeyB64url, DEFAULT_ADMIN_USER, adminPass);
|
|
1224
|
+
console.log(` Agent registered`);
|
|
1225
|
+
// Restore memories
|
|
1226
|
+
const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
|
|
1227
|
+
let memCount = 0;
|
|
1228
|
+
for (const mem of data.memories ?? []) {
|
|
1229
|
+
try {
|
|
1230
|
+
await fetch(`${baseUrl}/Memory/${mem.id}`, {
|
|
1231
|
+
method: "PUT",
|
|
1232
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
1233
|
+
body: JSON.stringify(mem),
|
|
1234
|
+
});
|
|
1235
|
+
memCount++;
|
|
1236
|
+
}
|
|
1237
|
+
catch { /* skip failures */ }
|
|
1238
|
+
}
|
|
1239
|
+
// Restore souls
|
|
1240
|
+
let soulCount = 0;
|
|
1241
|
+
for (const soul of data.souls ?? []) {
|
|
1242
|
+
try {
|
|
1243
|
+
await fetch(`${baseUrl}/Soul/${encodeURIComponent(soul.id)}`, {
|
|
1244
|
+
method: "PUT",
|
|
1245
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
1246
|
+
body: JSON.stringify(soul),
|
|
1247
|
+
});
|
|
1248
|
+
soulCount++;
|
|
1249
|
+
}
|
|
1250
|
+
catch { /* skip failures */ }
|
|
1251
|
+
}
|
|
1252
|
+
console.log(`\n✅ Agent '${agentId}' imported`);
|
|
1253
|
+
console.log(` Memories: ${memCount}/${(data.memories ?? []).length}`);
|
|
1254
|
+
console.log(` Souls: ${soulCount}/${(data.souls ?? []).length}`);
|
|
1255
|
+
console.log(` Key: ${privPath}`);
|
|
1256
|
+
});
|
|
1257
|
+
// ─── flair backup inspect ────────────────────────────────────────────────────
|
|
1258
|
+
program
|
|
1259
|
+
.command("inspect <path>")
|
|
1260
|
+
.description("Show contents of a backup or export file")
|
|
1261
|
+
.action(async (filePath) => {
|
|
1262
|
+
if (!existsSync(filePath)) {
|
|
1263
|
+
console.error(`File not found: ${filePath}`);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
1267
|
+
console.log(`File: ${filePath}`);
|
|
1268
|
+
console.log(`Type: ${data.type ?? "full-backup"}`);
|
|
1269
|
+
console.log(`Created: ${data.createdAt ?? data.exportedAt ?? "unknown"}`);
|
|
1270
|
+
console.log(`Source: ${data.source ?? "unknown"}`);
|
|
1271
|
+
if (data.type === "agent-export") {
|
|
1272
|
+
console.log(`\nAgent: ${data.agent?.id ?? "unknown"}`);
|
|
1273
|
+
console.log(` Name: ${data.agent?.name ?? data.agent?.id}`);
|
|
1274
|
+
console.log(` Memories: ${(data.memories ?? []).length}`);
|
|
1275
|
+
console.log(` Souls: ${(data.souls ?? []).length}`);
|
|
1276
|
+
console.log(` Grants: ${(data.grants ?? []).length}`);
|
|
1277
|
+
console.log(` Key included: ${data.privateKey ? "yes" : "no"}`);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
const agents = data.agents ?? [];
|
|
1281
|
+
console.log(`\nAgents: ${agents.length}`);
|
|
1282
|
+
for (const a of agents)
|
|
1283
|
+
console.log(` - ${a.id} (${a.name ?? a.id})`);
|
|
1284
|
+
console.log(`Memories: ${(data.memories ?? []).length}`);
|
|
1285
|
+
console.log(`Souls: ${(data.souls ?? []).length}`);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1005
1288
|
// ─── flair migrate-keys ───────────────────────────────────────────────────────
|
|
1006
1289
|
program
|
|
1007
1290
|
.command("migrate-keys")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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",
|