@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 +1 -1
- package/index.ts +197 -17
- package/key-resolver.ts +10 -24
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
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:
|
|
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 —
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
let currentAgentId: string | undefined = isAutoMode ? fallbackAgentId ?? undefined : cfg.agentId;
|
|
160
|
-
const configuredAgentId = cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId :
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
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 ||
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
stored
|
|
363
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/openclaw-flair",
|
|
3
|
-
"version": "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": "
|
|
45
|
-
"@tpsdev-ai/flair-client": "
|
|
47
|
+
"@sinclair/typebox": "0.34.48",
|
|
48
|
+
"@tpsdev-ai/flair-client": "0.5.0"
|
|
46
49
|
}
|
|
47
50
|
}
|