@tpsdev-ai/flair 0.4.8 → 0.4.12

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 CHANGED
@@ -377,6 +377,68 @@ program
377
377
  console.log("Waiting for Harper health check...");
378
378
  await waitForHealth(httpPort, adminUser, adminPass, STARTUP_TIMEOUT_MS);
379
379
  console.log("Harper is healthy ✓");
380
+ // Register launchd service on macOS so Harper survives reboots
381
+ // and `flair restart` / `flair stop` work via launchctl.
382
+ if (process.platform === "darwin") {
383
+ const label = "ai.tpsdev.flair";
384
+ const plistDir = join(homedir(), "Library", "LaunchAgents");
385
+ mkdirSync(plistDir, { recursive: true });
386
+ const plistPath = join(plistDir, `${label}.plist`);
387
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
388
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
389
+ <plist version="1.0">
390
+ <dict>
391
+ <key>Label</key><string>${label}</string>
392
+ <key>ProgramArguments</key>
393
+ <array>
394
+ <string>${process.execPath}</string>
395
+ <string>${bin}</string>
396
+ <string>run</string>
397
+ <string>.</string>
398
+ </array>
399
+ <key>WorkingDirectory</key><string>${flairPackageDir()}</string>
400
+ <key>EnvironmentVariables</key>
401
+ <dict>
402
+ <key>ROOTPATH</key><string>${dataDir}</string>
403
+ <key>HARPER_SET_CONFIG</key><string>${harperSetConfig.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;")}</string>
404
+ <key>DEFAULTS_MODE</key><string>dev</string>
405
+ <key>HDB_ADMIN_USERNAME</key><string>${adminUser}</string>
406
+ <key>HDB_ADMIN_PASSWORD</key><string>${adminPass}</string>
407
+ <key>THREADS_COUNT</key><string>1</string>
408
+ <key>NODE_HOSTNAME</key><string>localhost</string>
409
+ <key>HTTP_PORT</key><string>${httpPort}</string>
410
+ <key>OPERATIONSAPI_NETWORK_PORT</key><string>${opsPort}</string>
411
+ <key>LOCAL_STUDIO</key><string>false</string>
412
+ </dict>
413
+ <key>RunAtLoad</key><true/>
414
+ <key>KeepAlive</key><true/>
415
+ <key>StandardOutPath</key><string>${join(dataDir, "log", "launchd-stdout.log")}</string>
416
+ <key>StandardErrorPath</key><string>${join(dataDir, "log", "launchd-stderr.log")}</string>
417
+ </dict>
418
+ </plist>`;
419
+ writeFileSync(plistPath, plist);
420
+ try {
421
+ const { execSync } = await import("node:child_process");
422
+ // Stop the detached process — launchd will manage it from here
423
+ try {
424
+ const lsof = execSync(`lsof -ti :${httpPort}`, { encoding: "utf-8" }).trim();
425
+ if (lsof)
426
+ for (const pid of lsof.split("\n"))
427
+ try {
428
+ process.kill(Number(pid.trim()), "SIGTERM");
429
+ }
430
+ catch { }
431
+ await new Promise(r => setTimeout(r, 1000));
432
+ }
433
+ catch { }
434
+ execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
435
+ await waitForHealth(httpPort, adminUser, adminPass, STARTUP_TIMEOUT_MS);
436
+ console.log("Launchd service registered ✓");
437
+ }
438
+ catch (err) {
439
+ console.log(`Note: launchd registration failed (${err.message}) — Harper is running but won't auto-start on reboot`);
440
+ }
441
+ }
380
442
  }
381
443
  // Persist port to config so other commands can find this instance
382
444
  writeConfig(httpPort);
@@ -462,10 +524,39 @@ program
462
524
  }
463
525
  console.log(`\n Claude Code: Add to your CLAUDE.md:`);
464
526
  console.log(` At the start of every session, run mcp__flair__bootstrap before responding.`);
465
- const mcpEnv = { FLAIR_AGENT_ID: agentId };
466
- mcpEnv.FLAIR_URL = httpUrl;
467
- console.log(`\n MCP config (.mcp.json):`);
468
- console.log(` { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"], "env": ${JSON.stringify(mcpEnv)} } } }`);
527
+ // Auto-wire MCP config into ~/.claude.json if Claude Code is installed
528
+ const claudeJsonPath = join(homedir(), ".claude.json");
529
+ const mcpEnv = { FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl };
530
+ const flairMcpConfig = {
531
+ type: "stdio",
532
+ command: "flair-mcp",
533
+ args: [],
534
+ env: mcpEnv,
535
+ };
536
+ try {
537
+ if (existsSync(claudeJsonPath)) {
538
+ const claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
539
+ const existing = claudeJson.mcpServers?.flair;
540
+ if (existing && existing.env?.FLAIR_URL === httpUrl && existing.env?.FLAIR_AGENT_ID === agentId) {
541
+ console.log(`\n MCP config already set in ~/.claude.json ✓`);
542
+ }
543
+ else {
544
+ claudeJson.mcpServers = claudeJson.mcpServers || {};
545
+ claudeJson.mcpServers.flair = flairMcpConfig;
546
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
547
+ console.log(`\n MCP config written to ~/.claude.json ✓`);
548
+ console.log(` Restart Claude Code to pick up the new config.`);
549
+ }
550
+ }
551
+ else {
552
+ console.log(`\n MCP config (add to ~/.claude.json):`);
553
+ console.log(` { "mcpServers": { "flair": ${JSON.stringify(flairMcpConfig)} } }`);
554
+ }
555
+ }
556
+ catch {
557
+ console.log(`\n MCP config (add manually to ~/.claude.json):`);
558
+ console.log(` { "mcpServers": { "flair": ${JSON.stringify(flairMcpConfig)} } }`);
559
+ }
469
560
  });
470
561
  // ─── flair agent ─────────────────────────────────────────────────────────────
471
562
  const agent = program.command("agent").description("Manage Flair agents");
@@ -89,12 +89,10 @@ export class SemanticSearch extends Resource {
89
89
  if (!qEmb && q) {
90
90
  // Always attempt embedding generation — getEmbedding() handles init internally.
91
91
  // Don't gate on getMode() which may return "none" before init completes in worker threads.
92
- {
93
- try {
94
- qEmb = await getEmbedding(String(q).slice(0, 8000));
95
- }
96
- catch { }
92
+ try {
93
+ qEmb = await getEmbedding(String(q).slice(0, 8000));
97
94
  }
95
+ catch { }
98
96
  }
99
97
  // ─── Temporal intent detection ────────────────────────────────────────────
100
98
  let sinceDate = since ? new Date(since) : null;
@@ -145,8 +143,6 @@ export class SemanticSearch extends Resource {
145
143
  agentConditions.push({ attribute: "visibility", comparator: "equals", value: "office" });
146
144
  conditions.push({ operator: "or", conditions: agentConditions });
147
145
  }
148
- // Exclude archived records. Use "not equals true" instead of "equals false"
149
- // so records without the archived field (default: not archived) are included.
150
146
  // Exclude archived records. Use "not_equal" (Harper v5 comparator) instead of
151
147
  // "equals false" so records without the archived field are included.
152
148
  conditions.push({ attribute: "archived", comparator: "not_equal", value: true });
@@ -1,9 +1,29 @@
1
1
  import { databases } from "@harperfast/harper";
2
+ function enforceAgentScope(self, data) {
3
+ const authenticatedAgent = self.request?.headers?.get?.("x-tps-agent");
4
+ const callerIsAdmin = self.request?.tpsAgentIsAdmin === true;
5
+ if (authenticatedAgent && !callerIsAdmin && data?.agentId && data.agentId !== authenticatedAgent) {
6
+ return new Response(JSON.stringify({
7
+ error: "forbidden: agentId must match authenticated agent",
8
+ }), { status: 403, headers: { "Content-Type": "application/json" } });
9
+ }
10
+ return null;
11
+ }
2
12
  export class Soul extends databases.flair.Soul {
3
13
  async post(content, context) {
14
+ const denied = enforceAgentScope(this, content);
15
+ if (denied)
16
+ return denied;
4
17
  content.durability ||= "permanent";
5
18
  content.createdAt = new Date().toISOString();
6
19
  content.updatedAt = content.createdAt;
7
20
  return super.post(content, context);
8
21
  }
22
+ async put(content, context) {
23
+ const denied = enforceAgentScope(this, content);
24
+ if (denied)
25
+ return denied;
26
+ content.updatedAt = new Date().toISOString();
27
+ return super.put(content, context);
28
+ }
9
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.4.8",
3
+ "version": "0.4.12",
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",