@tpsdev-ai/openclaw-flair 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -47,7 +47,7 @@ In your OpenClaw config (`openclaw.json`):
47
47
  "memory-flair": {
48
48
  "enabled": true,
49
49
  "config": {
50
- "url": "http://localhost:9926",
50
+ "url": "http://localhost:19926",
51
51
  "agentId": "auto",
52
52
  "autoCapture": true,
53
53
  "autoRecall": true,
package/index.ts CHANGED
@@ -34,7 +34,7 @@ interface FlairMemoryConfig {
34
34
  maxBootstrapTokens?: number;
35
35
  }
36
36
 
37
- const DEFAULT_URL = "http://127.0.0.1:9926";
37
+ const DEFAULT_URL = "http://127.0.0.1:19926";
38
38
  const DEFAULT_MAX_RECALL = 5;
39
39
  const DEFAULT_MAX_BOOTSTRAP_TOKENS = 4000;
40
40
 
@@ -107,6 +107,130 @@ function excerptForCapture(text: string, maxChars = 500): string {
107
107
  return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
108
108
  }
109
109
 
110
+ // ─── Entity detection ────────────────────────────────────────────────────────
111
+ // Passive entity extraction from conversation content. No setup wizards,
112
+ // no "tell me about yourself" — Flair learns from natural conversation.
113
+
114
+ interface DetectedEntity {
115
+ name: string;
116
+ kind: "person" | "project" | "service" | "org" | "concept";
117
+ confidence: number;
118
+ }
119
+
120
+ interface DetectedRelationship {
121
+ subject: string;
122
+ predicate: string;
123
+ object: string;
124
+ confidence: number;
125
+ }
126
+
127
+ // Person detection: "Nathan said", "ask @Kern", "my name is X", "X is the founder"
128
+ const PERSON_PATTERNS = [
129
+ /\b([A-Z][a-z]{2,})\s+(?:said|asked|mentioned|decided|approved|rejected|thinks|wants|needs|prefers)\b/g,
130
+ /\b(?:ask|ping|tell|check with|talk to)\s+(?:@)?([A-Z][a-z]{2,})\b/g,
131
+ /\b(?:my name is|i'm|call me)\s+([A-Z][a-z]{2,})\b/ig,
132
+ /\b([A-Z][a-z]{2,})\s+(?:is the|is our|is a|was the|was our)\s+(\w+(?:\s+\w+)?)\b/g,
133
+ ];
134
+
135
+ // Project/service detection: repo references, "the X project", service names
136
+ const PROJECT_PATTERNS = [
137
+ /\b(?:tpsdev-ai|github\.com)\/([a-z0-9-]+)\b/g,
138
+ /\b(?:the|our)\s+([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\s+(?:project|repo|service|system|app|tool|plugin)\b/g,
139
+ ];
140
+
141
+ // Relationship detection: "X manages Y", "X owns Y", "X depends on Y"
142
+ const RELATIONSHIP_PATTERNS = [
143
+ { re: /\b([A-Z][a-z]{2,})\s+(?:manages|leads|runs|owns)\s+(?:the\s+)?([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b/g, predicate: "manages" },
144
+ { re: /\b([A-Z][a-z]{2,})\s+(?:works on|is working on|maintains)\s+(?:the\s+)?([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b/g, predicate: "works_on" },
145
+ { re: /\b([A-Z][a-z]{2,})\s+(?:reviews|reviewed)\s+(?:the\s+)?([A-Z][a-z]+(?:'s)?(?:\s\w+)?)\b/g, predicate: "reviews" },
146
+ { re: /\b([A-Z][a-z]+)\s+(?:depends on|requires|needs)\s+([A-Z][a-z]+)\b/g, predicate: "depends_on" },
147
+ { re: /\b([A-Z][a-z]+)\s+(?:replaces|supersedes)\s+([A-Z][a-z]+)\b/g, predicate: "replaces" },
148
+ ];
149
+
150
+ // Common words that look like names but aren't
151
+ const ENTITY_STOPWORDS = new Set([
152
+ "the", "this", "that", "with", "from", "into", "also", "just", "here",
153
+ "there", "what", "when", "where", "which", "while", "should", "would",
154
+ "could", "will", "does", "have", "been", "being", "make", "made",
155
+ "take", "taken", "like", "look", "good", "well", "much", "many",
156
+ "some", "each", "every", "both", "other", "such", "only", "same",
157
+ "than", "then", "now", "how", "all", "any", "few", "most", "very",
158
+ "after", "before", "between", "under", "over", "through", "during",
159
+ "about", "against", "above", "below", "off", "down", "out",
160
+ "let", "set", "get", "put", "run", "use", "try", "see", "new",
161
+ "old", "big", "end", "way", "day", "man", "did", "got", "had",
162
+ "yes", "not", "but", "for", "are", "was", "can", "may", "one",
163
+ "two", "its", "his", "her", "our", "has", "him", "her", "per",
164
+ "via", "bug", "fix", "add", "api", "url", "cli", "tcp", "ssh",
165
+ "keep", "next", "last", "best", "sure", "okay", "done", "want",
166
+ "need", "know", "think", "start", "stop", "check", "update",
167
+ "instead", "currently", "actually", "already", "however", "because",
168
+ "since", "until", "still", "right", "first", "great", "sounds",
169
+ "interesting", "important", "note", "issue", "pull", "push",
170
+ "merge", "branch", "commit", "deploy", "build", "test", "spec",
171
+ ]);
172
+
173
+ function isValidEntity(name: string): boolean {
174
+ if (name.length < 3 || name.length > 30) return false;
175
+ if (ENTITY_STOPWORDS.has(name.toLowerCase())) return false;
176
+ if (/^\d+$/.test(name)) return false;
177
+ return true;
178
+ }
179
+
180
+ function detectEntities(text: string): DetectedEntity[] {
181
+ const entities = new Map<string, DetectedEntity>();
182
+
183
+ for (const pattern of PERSON_PATTERNS) {
184
+ pattern.lastIndex = 0;
185
+ let match: RegExpExecArray | null;
186
+ while ((match = pattern.exec(text)) !== null) {
187
+ const name = match[1];
188
+ if (!isValidEntity(name)) continue;
189
+ const key = name.toLowerCase();
190
+ if (!entities.has(key) || entities.get(key)!.confidence < 0.7) {
191
+ entities.set(key, { name, kind: "person", confidence: 0.7 });
192
+ }
193
+ }
194
+ }
195
+
196
+ for (const pattern of PROJECT_PATTERNS) {
197
+ pattern.lastIndex = 0;
198
+ let match: RegExpExecArray | null;
199
+ while ((match = pattern.exec(text)) !== null) {
200
+ const name = match[1];
201
+ if (!isValidEntity(name)) continue;
202
+ const key = name.toLowerCase();
203
+ if (!entities.has(key)) {
204
+ entities.set(key, { name, kind: "project", confidence: 0.8 });
205
+ }
206
+ }
207
+ }
208
+
209
+ return [...entities.values()];
210
+ }
211
+
212
+ function detectRelationships(text: string): DetectedRelationship[] {
213
+ const relationships: DetectedRelationship[] = [];
214
+
215
+ for (const { re, predicate } of RELATIONSHIP_PATTERNS) {
216
+ re.lastIndex = 0;
217
+ let match: RegExpExecArray | null;
218
+ while ((match = re.exec(text)) !== null) {
219
+ const subject = match[1];
220
+ const object = match[2];
221
+ if (!isValidEntity(subject) || !isValidEntity(object)) continue;
222
+ relationships.push({
223
+ subject: subject.toLowerCase(),
224
+ predicate,
225
+ object: object.toLowerCase(),
226
+ confidence: 0.6,
227
+ });
228
+ }
229
+ }
230
+
231
+ return relationships;
232
+ }
233
+
110
234
  // ─── Plugin export ────────────────────────────────────────────────────────────
111
235
 
112
236
  export default {
@@ -124,8 +248,8 @@ export default {
124
248
  const fallbackAgentId = resolveAgentId();
125
249
 
126
250
  function getClient(agentId?: string): FlairClient {
127
- const id = agentId || cfg.agentId || fallbackAgentId;
128
- if (!id || id === "auto") throw new Error("no agentId available — set agentId in plugin config, FLAIR_AGENT_ID env var, or ensure OpenClaw provides it via session context");
251
+ const id = agentId || (cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId : null) || currentAgentId || fallbackAgentId;
252
+ if (!id || id === "auto") throw new Error("no agentId available — set agentId in plugin config, FLAIR_AGENT_ID env var, or ensure OpenClaw provides it via session context (before_agent_start)");
129
253
  let client = clientPool.get(id);
130
254
  if (!client) {
131
255
  client = new FlairClient({
@@ -153,17 +277,22 @@ export default {
153
277
  const autoCapture = cfg.autoCapture ?? false; // opt-in — trust the LLM to use memory_store
154
278
  const autoRecall = cfg.autoRecall ?? true;
155
279
 
156
- // Per-session agentId — set from config, env, or session context.
157
- // IMPORTANT: Only update from before_agent_start if it matches our configured agent,
158
- // NOT from other agents' cron jobs sharing the same gateway.
159
- let currentAgentId: string | undefined = isAutoMode ? fallbackAgentId ?? undefined : cfg.agentId;
160
- const configuredAgentId = cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId : fallbackAgentId;
280
+ // Per-session agentId — resolved from session context at runtime.
281
+ // In auto mode, each session gets its own agentId from before_agent_start.
282
+ // With explicit config, all sessions use the configured agentId.
283
+ let currentAgentId: string | undefined = isAutoMode ? (fallbackAgentId ?? undefined) : cfg.agentId;
284
+ const configuredAgentId = cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId : null;
161
285
 
162
286
  api.on("before_agent_start", async (event: any, ctx: any) => {
163
287
  const eventAgentId = ctx?.agentId || (event as any).agentId;
164
- // Only adopt the session agentId if we don't already have one configured,
165
- // or if it matches our configured agent. Don't let kern's cron overwrite flint's agentId.
166
- if (eventAgentId && (!configuredAgentId || eventAgentId === configuredAgentId)) {
288
+ if (!eventAgentId) return;
289
+
290
+ if (isAutoMode) {
291
+ // Auto mode: always adopt the session's agentId — each session is its own agent
292
+ currentAgentId = eventAgentId;
293
+ api.logger.info(`openclaw-flair: session agentId="${eventAgentId}"`);
294
+ } else if (eventAgentId === configuredAgentId) {
295
+ // Explicit mode: only accept matching agentId
167
296
  currentAgentId = eventAgentId;
168
297
  }
169
298
 
@@ -353,16 +482,67 @@ export default {
353
482
  const client = getCurrentClient();
354
483
  const messages = (event.messages ?? []) as Array<{ role: string; content?: string }>;
355
484
  let stored = 0;
485
+ const allEntities = new Map<string, DetectedEntity>();
486
+ const allRelationships: DetectedRelationship[] = [];
487
+
356
488
  for (const msg of messages) {
357
489
  if (msg.role !== "user" && msg.role !== "assistant") continue;
358
490
  const text = typeof msg.content === "string" ? msg.content : "";
359
- if (!text || !shouldCapture(text)) continue;
360
- const excerpt = excerptForCapture(text);
361
- await client.memory.write(excerpt, { type: "session", tags: ["auto-captured"] });
362
- stored++;
363
- if (stored >= 3) break;
491
+ if (!text || text.length < MIN_CAPTURE_LENGTH) continue;
492
+
493
+ // Traditional trigger-based capture
494
+ if (shouldCapture(text) && stored < 3) {
495
+ const excerpt = excerptForCapture(text);
496
+ // Tag with detected subject if available
497
+ const entities = detectEntities(text);
498
+ const subject = entities.length > 0 ? entities[0].name.toLowerCase() : undefined;
499
+ await client.memory.write(excerpt, {
500
+ type: "session",
501
+ tags: ["auto-captured"],
502
+ subject,
503
+ });
504
+ stored++;
505
+ }
506
+
507
+ // Entity detection — accumulate across all messages
508
+ for (const entity of detectEntities(text)) {
509
+ const key = entity.name.toLowerCase();
510
+ const existing = allEntities.get(key);
511
+ if (!existing || existing.confidence < entity.confidence) {
512
+ allEntities.set(key, entity);
513
+ }
514
+ }
515
+
516
+ // Relationship detection
517
+ for (const rel of detectRelationships(text)) {
518
+ allRelationships.push(rel);
519
+ }
520
+ }
521
+
522
+ // Store detected relationships via Flair's Relationship API
523
+ let relStored = 0;
524
+ for (const rel of allRelationships) {
525
+ if (relStored >= 5) break; // cap per session
526
+ try {
527
+ await client.request("PUT", `/Relationship/${Date.now()}-${relStored}`, {
528
+ subject: rel.subject,
529
+ predicate: rel.predicate,
530
+ object: rel.object,
531
+ confidence: rel.confidence,
532
+ source: "auto-detected",
533
+ });
534
+ relStored++;
535
+ } catch {
536
+ // best effort — don't fail the session over relationship storage
537
+ }
538
+ }
539
+
540
+ const total = stored + relStored;
541
+ if (total > 0) {
542
+ api.logger.info(
543
+ `openclaw-flair: auto-captured ${stored} memories, ${relStored} relationships, ${allEntities.size} entities detected`
544
+ );
364
545
  }
365
- if (stored > 0) api.logger.info(`openclaw-flair: auto-captured ${stored} memories`);
366
546
  } catch (err: any) {
367
547
  api.logger.warn(`openclaw-flair: auto-capture failed: ${err.message}`);
368
548
  }
package/key-resolver.ts CHANGED
@@ -2,36 +2,22 @@
2
2
  * OpenClaw-specific identity resolution.
3
3
  *
4
4
  * Key path and private key loading are now handled by @tpsdev-ai/flair-client.
5
- * This file only contains resolveAgentId() which reads OpenClaw config something
6
- * the generic flair-client shouldn't know about.
5
+ * This file only contains resolveAgentId() which reads environment variables
6
+ * something the generic flair-client shouldn't know about.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync } from "node:fs";
10
- import { homedir } from "node:os";
11
- import { resolve } from "node:path";
12
-
13
9
  /**
14
- * Resolve agent ID when not explicitly configured.
15
- * Priority: FLAIR_AGENT_ID env > OpenClaw config file > null
10
+ * Resolve agent ID from environment when not explicitly configured.
11
+ *
12
+ * IMPORTANT: Does NOT read from openclaw.json agents list. On a multi-agent
13
+ * gateway, the first agent in the list is NOT necessarily the current session's
14
+ * agent. The plugin must get the real agentId from the session context via
15
+ * before_agent_start, not from config guessing.
16
+ *
17
+ * Priority: FLAIR_AGENT_ID env > null (let session context provide it)
16
18
  */
17
19
  export function resolveAgentId(): string | null {
18
- // 1. Explicit env var
19
20
  const envId = process.env.FLAIR_AGENT_ID;
20
21
  if (envId) return envId;
21
-
22
- // 2. Read from OpenClaw config — first agent name
23
- try {
24
- const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
25
- if (!existsSync(configPath)) return null;
26
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
27
- const agents = config?.agents?.list;
28
- if (Array.isArray(agents) && agents.length > 0) {
29
- const name = agents[0]?.name;
30
- if (typeof name === "string" && name) return name.toLowerCase();
31
- }
32
- } catch {
33
- // Config unreadable — fall through
34
- }
35
-
36
22
  return null;
37
23
  }
@@ -4,7 +4,7 @@
4
4
  "uiHints": {
5
5
  "url": {
6
6
  "label": "Flair URL",
7
- "placeholder": "http://localhost:9926",
7
+ "placeholder": "http://localhost:19926",
8
8
  "help": "Base URL for the Flair server"
9
9
  },
10
10
  "agentId": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/openclaw-flair",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenClaw memory plugin for Flair \u2014 agent identity and semantic memory",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -34,6 +34,9 @@
34
34
  "./index.ts"
35
35
  ]
36
36
  },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
37
40
  "peerDependencies": {
38
41
  "openclaw": ">=2026.3.7"
39
42
  },
@@ -41,7 +44,7 @@
41
44
  "node": ">=22"
42
45
  },
43
46
  "dependencies": {
44
- "@sinclair/typebox": "^0.34.0",
45
- "@tpsdev-ai/flair-client": "^0.2.0"
47
+ "@sinclair/typebox": "0.34.48",
48
+ "@tpsdev-ai/flair-client": "0.5.0"
46
49
  }
47
50
  }