@tpsdev-ai/openclaw-flair 0.2.0 → 0.4.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/index.ts +75 -164
- package/key-resolver.ts +5 -52
- package/package.json +8 -5
package/index.ts
CHANGED
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
* - agent_end hook → auto-capture from conversation
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { Type } from "@sinclair/typebox";
|
|
21
|
+
import { FlairClient } from "@tpsdev-ai/flair-client";
|
|
21
22
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
22
|
-
import {
|
|
23
|
+
import { resolveAgentId } from "./key-resolver.js";
|
|
23
24
|
|
|
24
25
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -37,129 +38,8 @@ const DEFAULT_URL = "http://127.0.0.1:9926";
|
|
|
37
38
|
const DEFAULT_MAX_RECALL = 5;
|
|
38
39
|
const DEFAULT_MAX_BOOTSTRAP_TOKENS = 4000;
|
|
39
40
|
|
|
40
|
-
// ─── Flair HTTP Client ────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
class FlairMemoryClient {
|
|
43
|
-
private readonly baseUrl: string;
|
|
44
|
-
private readonly agentId: string;
|
|
45
|
-
private readonly keyPath: string | null;
|
|
46
|
-
|
|
47
|
-
constructor(config: FlairMemoryConfig) {
|
|
48
|
-
this.baseUrl = (config.url ?? DEFAULT_URL).replace(/\/$/, "");
|
|
49
|
-
this.agentId = config.agentId;
|
|
50
|
-
this.keyPath = resolveKeyPath(config.agentId, config.keyPath);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private buildAuthHeader(method: string, path: string): Record<string, string> {
|
|
54
|
-
if (!this.keyPath) return {};
|
|
55
|
-
try {
|
|
56
|
-
const { sign: ed25519Sign, randomUUID: rv } = require("node:crypto");
|
|
57
|
-
const privateKey = loadPrivateKey(this.keyPath);
|
|
58
|
-
if (!privateKey) return {};
|
|
59
|
-
const ts = Date.now().toString();
|
|
60
|
-
const nonce = rv();
|
|
61
|
-
const payload = `${this.agentId}:${ts}:${nonce}:${method}:${path}`;
|
|
62
|
-
const sig = ed25519Sign(null, Buffer.from(payload), privateKey);
|
|
63
|
-
return { Authorization: `TPS-Ed25519 ${this.agentId}:${ts}:${nonce}:${sig.toString("base64")}` };
|
|
64
|
-
} catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
70
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
71
|
-
method,
|
|
72
|
-
headers: {
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
...this.buildAuthHeader(method, path),
|
|
75
|
-
},
|
|
76
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
77
|
-
signal: AbortSignal.timeout(10_000),
|
|
78
|
-
});
|
|
79
|
-
if (!res.ok) {
|
|
80
|
-
const text = await res.text().catch(() => "");
|
|
81
|
-
throw new Error(`Flair ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
|
|
82
|
-
}
|
|
83
|
-
const text = await res.text();
|
|
84
|
-
return text ? (JSON.parse(text) as T) : ({} as T);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async writeMemory(id: string, content: string, opts: { durability?: string; type?: string; tags?: string[]; supersedes?: string } = {}): Promise<void> {
|
|
88
|
-
const record: Record<string, unknown> = {
|
|
89
|
-
id,
|
|
90
|
-
agentId: this.agentId,
|
|
91
|
-
content,
|
|
92
|
-
durability: opts.durability ?? "standard",
|
|
93
|
-
type: opts.type ?? "session",
|
|
94
|
-
createdAt: new Date().toISOString(),
|
|
95
|
-
};
|
|
96
|
-
if (opts.tags?.length) record.tags = opts.tags;
|
|
97
|
-
if (opts.supersedes) record.supersedes = opts.supersedes;
|
|
98
|
-
await this.request("PUT", `/Memory/${id}`, record);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async searchMemories(query: string, limit: number): Promise<Array<{ id: string; content: string; score: number; tags?: string[] }>> {
|
|
102
|
-
const result = await this.request<{ results?: Array<{ id: string; content: string; score?: number; similarity?: number; memory?: { id: string; content: string; tags?: string[] } }> }>(
|
|
103
|
-
"POST",
|
|
104
|
-
"/SemanticSearch",
|
|
105
|
-
{ agentId: this.agentId, q: query, limit },
|
|
106
|
-
);
|
|
107
|
-
return (result.results ?? []).map((r) => ({
|
|
108
|
-
id: r.id ?? r.memory?.id ?? "",
|
|
109
|
-
content: r.content ?? r.memory?.content ?? "",
|
|
110
|
-
score: r.score ?? r.similarity ?? 0,
|
|
111
|
-
tags: r.memory?.tags,
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async getMemory(id: string): Promise<{ id: string; content: string; createdAt?: string } | null> {
|
|
116
|
-
try {
|
|
117
|
-
return await this.request("GET", `/Memory/${id}`);
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async bootstrap(opts: { days?: number } = {}): Promise<string> {
|
|
124
|
-
try {
|
|
125
|
-
const days = opts.days ?? 7;
|
|
126
|
-
const since = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
127
|
-
const result = await this.request<{ context?: string; text?: string }>(
|
|
128
|
-
"POST",
|
|
129
|
-
"/BootstrapMemories",
|
|
130
|
-
{ agentId: this.agentId, since },
|
|
131
|
-
);
|
|
132
|
-
return result.context ?? result.text ?? "";
|
|
133
|
-
} catch {
|
|
134
|
-
return "";
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async getSoul(key: string): Promise<{ id: string; key: string; value: string; contentHash?: string } | null> {
|
|
139
|
-
try {
|
|
140
|
-
return await this.request("GET", `/Soul/${this.agentId}-${key}`);
|
|
141
|
-
} catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async writeSoul(key: string, value: string, contentHash?: string): Promise<void> {
|
|
147
|
-
const record: Record<string, unknown> = {
|
|
148
|
-
id: `${this.agentId}-${key}`,
|
|
149
|
-
agentId: this.agentId,
|
|
150
|
-
key,
|
|
151
|
-
value,
|
|
152
|
-
durability: "permanent",
|
|
153
|
-
createdAt: new Date().toISOString(),
|
|
154
|
-
};
|
|
155
|
-
if (contentHash) record.contentHash = contentHash;
|
|
156
|
-
await this.request("PUT", `/Soul/${this.agentId}-${key}`, record);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
41
|
// ─── Workspace sync helpers ───────────────────────────────────────────────────
|
|
161
42
|
|
|
162
|
-
/** Files to sync from workspace to Flair soul entries (spec: OPS-workspace-sync) */
|
|
163
43
|
const WORKSPACE_SOUL_FILES: Record<string, string> = {
|
|
164
44
|
"SOUL.md": "soul",
|
|
165
45
|
"IDENTITY.md": "identity",
|
|
@@ -167,19 +47,14 @@ const WORKSPACE_SOUL_FILES: Record<string, string> = {
|
|
|
167
47
|
"AGENTS.md": "workspace-rules",
|
|
168
48
|
};
|
|
169
49
|
|
|
170
|
-
/** Max size for a single soul entry (chars). Files larger are truncated. */
|
|
171
50
|
const MAX_SOUL_VALUE = 8000;
|
|
172
51
|
|
|
173
52
|
function hashContent(content: string): string {
|
|
174
53
|
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
175
54
|
}
|
|
176
55
|
|
|
177
|
-
/**
|
|
178
|
-
* Sync workspace files to Flair soul entries.
|
|
179
|
-
* Only writes when content has changed (hash comparison).
|
|
180
|
-
*/
|
|
181
56
|
async function syncWorkspaceToFlair(
|
|
182
|
-
client:
|
|
57
|
+
client: FlairClient,
|
|
183
58
|
agentId: string,
|
|
184
59
|
logger: { info: Function; warn: Function },
|
|
185
60
|
): Promise<number> {
|
|
@@ -197,12 +72,10 @@ async function syncWorkspaceToFlair(
|
|
|
197
72
|
if (content.length > MAX_SOUL_VALUE) content = content.slice(0, MAX_SOUL_VALUE) + "\n…(truncated)";
|
|
198
73
|
|
|
199
74
|
const newHash = hashContent(content);
|
|
75
|
+
const existing = await client.soul.get(soulKey);
|
|
76
|
+
if ((existing as any)?.contentHash === newHash) continue;
|
|
200
77
|
|
|
201
|
-
|
|
202
|
-
const existing = await client.getSoul(soulKey);
|
|
203
|
-
if (existing?.contentHash === newHash) continue; // unchanged
|
|
204
|
-
|
|
205
|
-
await client.writeSoul(soulKey, content, newHash);
|
|
78
|
+
await client.soul.set(soulKey, content);
|
|
206
79
|
synced++;
|
|
207
80
|
logger.info(`openclaw-flair: synced ${filename} → soul:${soulKey} (hash=${newHash})`);
|
|
208
81
|
} catch (err: any) {
|
|
@@ -214,13 +87,19 @@ async function syncWorkspaceToFlair(
|
|
|
214
87
|
|
|
215
88
|
// ─── Auto-capture helpers ─────────────────────────────────────────────────────
|
|
216
89
|
|
|
90
|
+
// Auto-capture triggers — conservative patterns that indicate genuinely
|
|
91
|
+
// important context, not casual conversation. The LLM has memory_store
|
|
92
|
+
// for explicit saves; auto-capture is a safety net for things it misses.
|
|
217
93
|
const CAPTURE_TRIGGERS = [
|
|
218
|
-
/\b(remember|note
|
|
219
|
-
/\b(
|
|
220
|
-
/\b(decided|agreed|
|
|
94
|
+
/\b(remember this|note for future|important lesson|key decision|for the record)\b/i,
|
|
95
|
+
/\b(my name is|call me|i go by)\b/i,
|
|
96
|
+
/\b(we decided|final decision|agreed to|commitment:)\b/i,
|
|
221
97
|
];
|
|
222
98
|
|
|
99
|
+
const MIN_CAPTURE_LENGTH = 30; // skip very short messages
|
|
100
|
+
|
|
223
101
|
function shouldCapture(text: string): boolean {
|
|
102
|
+
if (text.length < MIN_CAPTURE_LENGTH) return false;
|
|
224
103
|
return CAPTURE_TRIGGERS.some((re) => re.test(text));
|
|
225
104
|
}
|
|
226
105
|
|
|
@@ -238,24 +117,27 @@ export default {
|
|
|
238
117
|
const cfg = (api.pluginConfig ?? {}) as FlairMemoryConfig;
|
|
239
118
|
const isAutoMode = !cfg.agentId || cfg.agentId === "auto";
|
|
240
119
|
|
|
241
|
-
// Client pool: one
|
|
242
|
-
const clientPool = new Map<string,
|
|
243
|
-
|
|
120
|
+
// Client pool: one FlairClient per agentId, created lazily
|
|
121
|
+
const clientPool = new Map<string, FlairClient>();
|
|
122
|
+
|
|
244
123
|
// Resolve fallback agentId once at registration time
|
|
245
124
|
const fallbackAgentId = resolveAgentId();
|
|
246
125
|
|
|
247
|
-
function getClient(agentId?: string):
|
|
126
|
+
function getClient(agentId?: string): FlairClient {
|
|
248
127
|
const id = agentId || cfg.agentId || fallbackAgentId;
|
|
249
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");
|
|
250
129
|
let client = clientPool.get(id);
|
|
251
130
|
if (!client) {
|
|
252
|
-
client = new
|
|
131
|
+
client = new FlairClient({
|
|
132
|
+
url: cfg.url ?? DEFAULT_URL,
|
|
133
|
+
agentId: id,
|
|
134
|
+
keyPath: cfg.keyPath,
|
|
135
|
+
});
|
|
253
136
|
clientPool.set(id, client);
|
|
254
137
|
}
|
|
255
138
|
return client;
|
|
256
139
|
}
|
|
257
140
|
|
|
258
|
-
// For non-auto mode, pre-create the client
|
|
259
141
|
if (!isAutoMode) {
|
|
260
142
|
getClient(cfg.agentId);
|
|
261
143
|
}
|
|
@@ -268,17 +150,23 @@ export default {
|
|
|
268
150
|
api.logger.info("openclaw-flair: auto mode — agentId will be resolved from session context");
|
|
269
151
|
}
|
|
270
152
|
const maxRecall = cfg.maxRecallResults ?? DEFAULT_MAX_RECALL;
|
|
271
|
-
const autoCapture = cfg.autoCapture ??
|
|
153
|
+
const autoCapture = cfg.autoCapture ?? false; // opt-in — trust the LLM to use memory_store
|
|
272
154
|
const autoRecall = cfg.autoRecall ?? true;
|
|
273
155
|
|
|
274
|
-
//
|
|
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.
|
|
275
159
|
let currentAgentId: string | undefined = isAutoMode ? fallbackAgentId ?? undefined : cfg.agentId;
|
|
276
|
-
|
|
160
|
+
const configuredAgentId = cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId : fallbackAgentId;
|
|
161
|
+
|
|
277
162
|
api.on("before_agent_start", async (event: any, ctx: any) => {
|
|
278
163
|
const eventAgentId = ctx?.agentId || (event as any).agentId;
|
|
279
|
-
if
|
|
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)) {
|
|
167
|
+
currentAgentId = eventAgentId;
|
|
168
|
+
}
|
|
280
169
|
|
|
281
|
-
// Sync workspace files → Flair soul entries (hash-based, only on change)
|
|
282
170
|
if (eventAgentId) {
|
|
283
171
|
try {
|
|
284
172
|
const client = getClient(eventAgentId);
|
|
@@ -289,9 +177,8 @@ export default {
|
|
|
289
177
|
}
|
|
290
178
|
}
|
|
291
179
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
function getCurrentClient(): FlairMemoryClient {
|
|
180
|
+
|
|
181
|
+
function getCurrentClient(): FlairClient {
|
|
295
182
|
return getClient(currentAgentId);
|
|
296
183
|
}
|
|
297
184
|
|
|
@@ -314,7 +201,7 @@ export default {
|
|
|
314
201
|
const { query, limit = maxRecall } = params as { query: string; limit?: number };
|
|
315
202
|
try {
|
|
316
203
|
const client = getCurrentClient();
|
|
317
|
-
const results = await client.
|
|
204
|
+
const results = await client.memory.search(query, { limit });
|
|
318
205
|
if (results.length === 0) {
|
|
319
206
|
return { content: [{ type: "text", text: "No relevant memories found." }], details: { count: 0 } };
|
|
320
207
|
}
|
|
@@ -367,13 +254,39 @@ export default {
|
|
|
367
254
|
durability?: string; type?: string; supersedes?: string;
|
|
368
255
|
};
|
|
369
256
|
try {
|
|
370
|
-
const agentId = currentAgentId || cfg.agentId;
|
|
371
257
|
const client = getCurrentClient();
|
|
372
|
-
const memId = `${agentId}-${Date.now()}`;
|
|
373
|
-
|
|
258
|
+
const memId = `${client.agentId}-${Date.now()}`;
|
|
259
|
+
// If superseding an old memory, archive it
|
|
260
|
+
if (supersedes) {
|
|
261
|
+
try {
|
|
262
|
+
const old = await client.memory.get(supersedes);
|
|
263
|
+
if (old) {
|
|
264
|
+
await client.request("PUT", `/Memory/${supersedes}`, {
|
|
265
|
+
...old,
|
|
266
|
+
archived: true,
|
|
267
|
+
archivedAt: new Date().toISOString(),
|
|
268
|
+
supersededBy: memId,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Old memory not found — continue with the write
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = await client.memory.write(text, {
|
|
277
|
+
id: memId,
|
|
278
|
+
tags,
|
|
279
|
+
durability: durability as any,
|
|
280
|
+
type: type as any,
|
|
281
|
+
dedup: !supersedes, // skip dedup when explicitly superseding
|
|
282
|
+
dedupThreshold: 0.7,
|
|
283
|
+
});
|
|
284
|
+
const wasDeduped = result.id !== memId;
|
|
374
285
|
return {
|
|
375
|
-
content: [{ type: "text", text:
|
|
376
|
-
|
|
286
|
+
content: [{ type: "text", text: wasDeduped
|
|
287
|
+
? `Similar memory already exists (id: ${result.id}): ${result.content?.slice(0, 200)}`
|
|
288
|
+
: `Memory stored (id: ${memId})` }],
|
|
289
|
+
details: { id: result.id, deduplicated: wasDeduped },
|
|
377
290
|
};
|
|
378
291
|
} catch (err: any) {
|
|
379
292
|
api.logger.warn(`openclaw-flair: store failed: ${err.message}`);
|
|
@@ -398,7 +311,7 @@ export default {
|
|
|
398
311
|
const { id } = params as { id: string };
|
|
399
312
|
try {
|
|
400
313
|
const client = getCurrentClient();
|
|
401
|
-
const mem = await client.
|
|
314
|
+
const mem = await client.memory.get(id);
|
|
402
315
|
if (!mem) return { content: [{ type: "text", text: `Memory ${id} not found.` }], details: {} };
|
|
403
316
|
return {
|
|
404
317
|
content: [{ type: "text", text: mem.content }],
|
|
@@ -417,10 +330,10 @@ export default {
|
|
|
417
330
|
if (autoRecall) {
|
|
418
331
|
api.on("before_agent_start", async (event: any, ctx: any) => {
|
|
419
332
|
try {
|
|
420
|
-
// Ensure agentId is set (in case this fires before the tracking hook)
|
|
421
333
|
if (ctx?.agentId && !currentAgentId) currentAgentId = ctx.agentId;
|
|
422
334
|
const client = getCurrentClient();
|
|
423
|
-
const
|
|
335
|
+
const result = await client.bootstrap({ maxTokens: cfg.maxBootstrapTokens ?? DEFAULT_MAX_BOOTSTRAP_TOKENS });
|
|
336
|
+
const context = result.context;
|
|
424
337
|
if (context && typeof context === "string" && context.trim().length > 0) {
|
|
425
338
|
const truncated = context.slice(0, (cfg.maxBootstrapTokens ?? DEFAULT_MAX_BOOTSTRAP_TOKENS) * 4);
|
|
426
339
|
event.injectContext?.(`\n## Memory Context (from Flair)\n\n${truncated}\n`);
|
|
@@ -437,7 +350,6 @@ export default {
|
|
|
437
350
|
if (autoCapture) {
|
|
438
351
|
api.on("agent_end", async (event) => {
|
|
439
352
|
try {
|
|
440
|
-
const agentId = currentAgentId || cfg.agentId;
|
|
441
353
|
const client = getCurrentClient();
|
|
442
354
|
const messages = (event.messages ?? []) as Array<{ role: string; content?: string }>;
|
|
443
355
|
let stored = 0;
|
|
@@ -446,10 +358,9 @@ export default {
|
|
|
446
358
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
447
359
|
if (!text || !shouldCapture(text)) continue;
|
|
448
360
|
const excerpt = excerptForCapture(text);
|
|
449
|
-
|
|
450
|
-
await client.writeMemory(memId, excerpt, { type: "session", tags: ["auto-captured"] });
|
|
361
|
+
await client.memory.write(excerpt, { type: "session", tags: ["auto-captured"] });
|
|
451
362
|
stored++;
|
|
452
|
-
if (stored >= 3) break;
|
|
363
|
+
if (stored >= 3) break;
|
|
453
364
|
}
|
|
454
365
|
if (stored > 0) api.logger.info(`openclaw-flair: auto-captured ${stored} memories`);
|
|
455
366
|
} catch (err: any) {
|
package/key-resolver.ts
CHANGED
|
@@ -1,61 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* OpenClaw-specific identity resolution.
|
|
3
|
+
*
|
|
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
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
10
|
import { homedir } from "node:os";
|
|
9
11
|
import { resolve } from "node:path";
|
|
10
12
|
|
|
11
|
-
/**
|
|
12
|
-
* Resolve the Flair key path for an agent.
|
|
13
|
-
* Priority: explicit keyPath > FLAIR_KEY_DIR env > ~/.flair/keys/<agent>.key
|
|
14
|
-
*/
|
|
15
|
-
export function resolveKeyPath(agentId: string, explicitPath?: string): string | null {
|
|
16
|
-
if (explicitPath) {
|
|
17
|
-
return resolve(explicitPath.replace(/^~/, homedir()));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 1. FLAIR_KEY_DIR env var
|
|
21
|
-
const keyDirEnv = process.env.FLAIR_KEY_DIR;
|
|
22
|
-
if (keyDirEnv) {
|
|
23
|
-
const envPath = resolve(keyDirEnv, `${agentId}.key`);
|
|
24
|
-
if (existsSync(envPath)) return envPath;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 2. ~/.flair/keys/<agent>.key (standard — use `flair agent add` to generate)
|
|
28
|
-
const standard = resolve(homedir(), ".flair", "keys", `${agentId}.key`);
|
|
29
|
-
if (existsSync(standard)) return standard;
|
|
30
|
-
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Load and parse an Ed25519 private key from a key file.
|
|
36
|
-
* Supports both raw 32-byte binary seeds and base64-encoded PKCS8 DER.
|
|
37
|
-
*/
|
|
38
|
-
export function loadPrivateKey(keyPath: string): ReturnType<typeof import("node:crypto").createPrivateKey> | null {
|
|
39
|
-
if (!existsSync(keyPath)) return null;
|
|
40
|
-
try {
|
|
41
|
-
const { createPrivateKey } = require("node:crypto");
|
|
42
|
-
const fileBuf = readFileSync(keyPath);
|
|
43
|
-
let rawBuf: Buffer;
|
|
44
|
-
if (fileBuf.length === 32) {
|
|
45
|
-
rawBuf = fileBuf;
|
|
46
|
-
} else {
|
|
47
|
-
rawBuf = Buffer.from(fileBuf.toString("utf-8").trim(), "base64");
|
|
48
|
-
}
|
|
49
|
-
if (rawBuf.length === 32) {
|
|
50
|
-
const pkcs8Header = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
51
|
-
return createPrivateKey({ key: Buffer.concat([pkcs8Header, rawBuf]), format: "der", type: "pkcs8" });
|
|
52
|
-
}
|
|
53
|
-
return createPrivateKey({ key: rawBuf, format: "der", type: "pkcs8" });
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
13
|
/**
|
|
60
14
|
* Resolve agent ID when not explicitly configured.
|
|
61
15
|
* Priority: FLAIR_AGENT_ID env > OpenClaw config file > null
|
|
@@ -70,7 +24,6 @@ export function resolveAgentId(): string | null {
|
|
|
70
24
|
const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
|
|
71
25
|
if (!existsSync(configPath)) return null;
|
|
72
26
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
73
|
-
// agents.list[].name is the standard OpenClaw agent config
|
|
74
27
|
const agents = config?.agents?.list;
|
|
75
28
|
if (Array.isArray(agents) && agents.length > 0) {
|
|
76
29
|
const name = agents[0]?.name;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/openclaw-flair",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenClaw memory plugin for Flair
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "OpenClaw memory plugin for Flair \u2014 agent identity and semantic memory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/tpsdev-ai/flair/tree/main/plugins/openclaw-flair",
|
|
32
32
|
"openclaw": {
|
|
33
|
-
"extensions": [
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts"
|
|
35
|
+
]
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"openclaw": ">=2026.3.7"
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"node": ">=22"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@sinclair/typebox": "^0.34.0"
|
|
44
|
+
"@sinclair/typebox": "^0.34.0",
|
|
45
|
+
"@tpsdev-ai/flair-client": "^0.2.0"
|
|
43
46
|
}
|
|
44
|
-
}
|
|
47
|
+
}
|