@tpsdev-ai/flair 0.6.2 → 0.8.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 +19 -6
- package/dist/bridges/builtins/index.js +28 -11
- package/dist/bridges/builtins/markdown.js +66 -0
- package/dist/bridges/runtime/context.js +2 -1
- package/dist/bridges/runtime/formats.js +97 -9
- package/dist/bridges/runtime/load-bridge.js +15 -5
- package/dist/cli.js +3113 -298
- package/dist/install/clients.js +225 -0
- package/dist/resources/Federation.js +87 -24
- package/dist/resources/Memory.js +17 -24
- package/dist/resources/MemoryBootstrap.js +11 -2
- package/dist/resources/auth-middleware.js +66 -11
- package/dist/resources/content-safety.js +21 -0
- package/dist/resources/federation-cleanup.js +161 -0
- package/dist/resources/federation-crypto.js +100 -1
- package/dist/resources/health.js +1 -1
- package/package.json +8 -6
- package/schemas/memory.graphql +28 -0
- package/ui/observation-center.html +356 -62
package/README.md
CHANGED
|
@@ -160,19 +160,19 @@ One command. Zero config.
|
|
|
160
160
|
openclaw plugins install @tpsdev-ai/openclaw-flair
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
The plugin auto-detects your agent identity, provides `memory_store`/`memory_recall`/`memory_get` tools, and injects relevant memories at session start. See the [plugin README](
|
|
163
|
+
The plugin auto-detects your agent identity, provides `memory_store`/`memory_recall`/`memory_get` tools, and injects relevant memories at session start. See the [plugin README](packages/openclaw-flair/README.md) for details.
|
|
164
164
|
|
|
165
|
-
### Claude Code / Codex / Cursor (MCP)
|
|
165
|
+
### Claude Code / Gemini CLI / OpenAI Codex CLI / Cursor (MCP)
|
|
166
166
|
|
|
167
|
-
Install the MCP server for native tool integration:
|
|
167
|
+
One MCP server, many CLIs. Install the MCP server for native tool integration in any MCP-capable client:
|
|
168
168
|
|
|
169
169
|
```json
|
|
170
|
-
// .mcp.json in your project root
|
|
170
|
+
// .mcp.json in your project root (Claude Code / Cursor format)
|
|
171
171
|
{
|
|
172
172
|
"mcpServers": {
|
|
173
173
|
"flair": {
|
|
174
174
|
"command": "npx",
|
|
175
|
-
"args": ["@tpsdev-ai/flair-mcp"],
|
|
175
|
+
"args": ["-y", "@tpsdev-ai/flair-mcp"],
|
|
176
176
|
"env": { "FLAIR_AGENT_ID": "mybot" }
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -183,7 +183,20 @@ Add to your `CLAUDE.md`:
|
|
|
183
183
|
|
|
184
184
|
At the start of every session, run mcp__flair__bootstrap before responding.
|
|
185
185
|
|
|
186
|
-
Claude Code
|
|
186
|
+
Your agent's memory **follows it across CLIs** — same Flair instance, same agent identity, switch from Claude Code to Gemini CLI to Codex CLI without losing state. The MCP server exposes `memory_store`, `memory_search`, `memory_get`, `memory_delete`, `bootstrap`, `soul_set`, `soul_get`.
|
|
187
|
+
|
|
188
|
+
For per-CLI config snippets (Gemini CLI's `~/.gemini/settings.json`, Codex CLI's `~/.codex/config.toml`, etc.), see **[docs/mcp-clients.md](docs/mcp-clients.md)**. For a deeper Claude Code walk-through with `CLAUDE.md` patterns, see [docs/claude-code.md](docs/claude-code.md).
|
|
189
|
+
|
|
190
|
+
### n8n (community node)
|
|
191
|
+
|
|
192
|
+
Use Flair as the memory backend for n8n's AI Agent. Same memories readable from Claude Code and OpenClaw — that's the point.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# In n8n: Settings → Community Nodes → Install
|
|
196
|
+
@tpsdev-ai/n8n-nodes-flair
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Two nodes ship: **Flair Chat Memory** (Memory port, conversation buffer) and **Flair Search** (Tool port, semantic search + get-by-subject). Setup walkthrough, subject/sessionId patterns, and security guidance in **[docs/n8n.md](docs/n8n.md)**.
|
|
187
200
|
|
|
188
201
|
### JavaScript / TypeScript (Client Library)
|
|
189
202
|
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in bridge registry.
|
|
3
3
|
*
|
|
4
|
-
* Each built-in ships as a
|
|
5
|
-
* `
|
|
6
|
-
* built-ins surface in `flair bridge list` alongside
|
|
7
|
-
* bridges, and to the import/export runtime so it knows how to
|
|
8
|
-
* the descriptor for a built-in name.
|
|
4
|
+
* Each built-in ships as either a `YamlBridgeDescriptor` (_shape A_: declarative YAML)
|
|
5
|
+
* or a `MemoryBridge` (shape B: TypeScript code plugin). The CLI passes this
|
|
6
|
+
* registry to `discover()` so built-ins surface in `flair bridge list` alongside
|
|
7
|
+
* user-authored bridges, and to the import/export runtime so it knows how to
|
|
8
|
+
* load the descriptor/plugin for a built-in name.
|
|
9
9
|
*
|
|
10
|
-
* To add a new built-in:
|
|
11
|
-
* exporting a `YamlBridgeDescriptor
|
|
10
|
+
* To add a new built-in:
|
|
11
|
+
* - YAML: write `src/bridges/builtins/<name>.ts` exporting a `YamlBridgeDescriptor`
|
|
12
|
+
* - Code plugin: write `src/bridges/builtins/<name>.ts` exporting a `MemoryBridge`
|
|
13
|
+
* Then register it here. See specs/FLAIR-BRIDGES.md §2 for shapes A/B.
|
|
12
14
|
*/
|
|
13
15
|
import { agenticStackDescriptor } from "./agentic-stack.js";
|
|
14
|
-
|
|
16
|
+
import { markdownMemoryBridge } from "./markdown.js";
|
|
17
|
+
function builtinDescriptor(d) {
|
|
15
18
|
return {
|
|
16
19
|
discovered: {
|
|
17
20
|
name: d.name,
|
|
@@ -21,14 +24,28 @@ function builtin(d) {
|
|
|
21
24
|
description: d.description,
|
|
22
25
|
version: d.version,
|
|
23
26
|
},
|
|
24
|
-
|
|
27
|
+
descriptorOrPlugin: d,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function builtinPlugin(p) {
|
|
31
|
+
return {
|
|
32
|
+
discovered: {
|
|
33
|
+
name: p.name,
|
|
34
|
+
kind: p.kind,
|
|
35
|
+
source: "builtin",
|
|
36
|
+
path: `(builtin:${p.name})`,
|
|
37
|
+
description: p.description,
|
|
38
|
+
version: p.version,
|
|
39
|
+
},
|
|
40
|
+
descriptorOrPlugin: p,
|
|
25
41
|
};
|
|
26
42
|
}
|
|
27
43
|
/** All bridges shipped inside @tpsdev-ai/flair. Order doesn't matter. */
|
|
28
44
|
export const BUILTINS = [
|
|
29
|
-
|
|
45
|
+
builtinDescriptor(agenticStackDescriptor),
|
|
46
|
+
builtinPlugin(markdownMemoryBridge),
|
|
30
47
|
];
|
|
31
|
-
/** Map name → descriptor for O(1) runtime lookup. */
|
|
48
|
+
/** Map name → descriptor/plugin for O(1) runtime lookup. */
|
|
32
49
|
export const BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.discovered.name, b]));
|
|
33
50
|
/** What `discover()` consumes for the `builtins` option. */
|
|
34
51
|
export function builtinDiscoveryRecords() {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in bridge: markdown
|
|
3
|
+
*
|
|
4
|
+
* Imports markdown files from a directory into Flair memories.
|
|
5
|
+
* Each .md file becomes one memory with content sans front-matter,
|
|
6
|
+
* type/tags/subject/createdAt derived from front-matter fields.
|
|
7
|
+
*
|
|
8
|
+
* Round-trip stable: export produces equivalent markdown files.
|
|
9
|
+
*/
|
|
10
|
+
import { promises as fsp } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { parseRecords } from "../runtime/formats.js";
|
|
14
|
+
import { BridgeRuntimeError } from "../types.js";
|
|
15
|
+
export async function* importFromDirectory(opts, ctx) {
|
|
16
|
+
// Resolve source directory
|
|
17
|
+
const sourceDir = typeof opts.source === "string" ? opts.source : join(homedir(), "notes");
|
|
18
|
+
ctx.log.info(`scanning markdown directory`, { path: sourceDir });
|
|
19
|
+
// Scan directory for .md files
|
|
20
|
+
let files;
|
|
21
|
+
try {
|
|
22
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
23
|
+
files = entries
|
|
24
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
25
|
+
.map((e) => join(sourceDir, e.name));
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err?.code === "ENOENT") {
|
|
29
|
+
ctx.log.warn(`source directory does not exist`, { path: sourceDir });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw new BridgeRuntimeError({
|
|
33
|
+
bridge: "markdown",
|
|
34
|
+
op: "import",
|
|
35
|
+
field: "source",
|
|
36
|
+
expected: "readable directory",
|
|
37
|
+
got: err?.code ?? "error",
|
|
38
|
+
hint: `could not read source directory ${sourceDir}: ${err?.message ?? err}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
ctx.log.info(`found markdown files`, { count: files.length });
|
|
42
|
+
for (const filePath of files) {
|
|
43
|
+
ctx.log.debug(`processing file`, { path: filePath });
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = await fsp.readFile(filePath, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
ctx.log.warn(`could not read file, skipping`, { path: filePath, error: err?.message });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Use parseRecords from formats.ts for consistent parsing
|
|
53
|
+
const parser = parseRecords("markdown", filePath, "markdown-frontmatter");
|
|
54
|
+
for await (const { record } of parser) {
|
|
55
|
+
yield record;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Export as a code-plugin MemoryBridge (Shape B)
|
|
60
|
+
export const markdownMemoryBridge = {
|
|
61
|
+
name: "markdown",
|
|
62
|
+
version: 1,
|
|
63
|
+
kind: "file",
|
|
64
|
+
description: "Import markdown files with front-matter into Flair memories",
|
|
65
|
+
import: importFromDirectory,
|
|
66
|
+
};
|
|
@@ -28,8 +28,9 @@ export function makeContext(opts) {
|
|
|
28
28
|
timestamp: new Date().toISOString(),
|
|
29
29
|
});
|
|
30
30
|
};
|
|
31
|
+
const wrappedFetch = ((input, init) => fetch(input, init));
|
|
31
32
|
return {
|
|
32
|
-
fetch:
|
|
33
|
+
fetch: wrappedFetch,
|
|
33
34
|
log: {
|
|
34
35
|
debug: log("debug"),
|
|
35
36
|
info: log("info"),
|
|
@@ -36,15 +36,8 @@ export async function* parseRecords(bridge, path, format) {
|
|
|
36
36
|
yield* parseYamlRecords(bridge, path, raw);
|
|
37
37
|
return;
|
|
38
38
|
case "markdown-frontmatter":
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
op: "import",
|
|
42
|
-
path,
|
|
43
|
-
field: "format",
|
|
44
|
-
expected: "jsonl | json | yaml",
|
|
45
|
-
got: "markdown-frontmatter",
|
|
46
|
-
hint: "markdown-frontmatter parser lands in slice 2b along with its reference adapter",
|
|
47
|
-
});
|
|
39
|
+
yield* parseMarkdownFrontmatter(bridge, path, raw);
|
|
40
|
+
return;
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
43
|
// ─── jsonl ────────────────────────────────────────────────────────────────────
|
|
@@ -134,3 +127,98 @@ function* parseYamlRecords(bridge, path, raw) {
|
|
|
134
127
|
function truncate(s, n) {
|
|
135
128
|
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
136
129
|
}
|
|
130
|
+
// ─── markdown-frontmatter ─────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Parse a markdown file with optional front-matter.
|
|
133
|
+
* Front-matter is YAML delimited by --- at the start of the file.
|
|
134
|
+
* Returns an object with content, type, tags, subject, createdAt, derivedFrom.
|
|
135
|
+
*/
|
|
136
|
+
function* parseMarkdownFrontmatter(bridge, path, raw) {
|
|
137
|
+
const lines = raw.split(/\r?\n/);
|
|
138
|
+
// Check for front-matter delimiter
|
|
139
|
+
if (!lines[0] || lines[0].trim() !== "---") {
|
|
140
|
+
// No front-matter - treat entire file as content
|
|
141
|
+
yield {
|
|
142
|
+
record: {
|
|
143
|
+
content: raw,
|
|
144
|
+
type: "fact",
|
|
145
|
+
subject: path.split("/").pop()?.replace(/\.md$/i, "") ?? "",
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
derivedFrom: [path],
|
|
148
|
+
foreignId: path,
|
|
149
|
+
},
|
|
150
|
+
recordIndex: 1,
|
|
151
|
+
};
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Skip the opening ---
|
|
155
|
+
let i = 1;
|
|
156
|
+
// Find closing ---
|
|
157
|
+
let fmLines = [];
|
|
158
|
+
while (i < lines.length) {
|
|
159
|
+
if (lines[i]?.trim() === "---") {
|
|
160
|
+
i++; // skip closing ---
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
fmLines.push(lines[i]);
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
// Parse front-matter YAML
|
|
167
|
+
let fm;
|
|
168
|
+
try {
|
|
169
|
+
const fmText = fmLines.join("\n");
|
|
170
|
+
const parsed = yaml.load(fmText);
|
|
171
|
+
if (!parsed || typeof parsed !== "object") {
|
|
172
|
+
throw new Error("front-matter is not a valid YAML object");
|
|
173
|
+
}
|
|
174
|
+
fm = parsed;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
throw new BridgeRuntimeError({
|
|
178
|
+
bridge,
|
|
179
|
+
op: "import",
|
|
180
|
+
path,
|
|
181
|
+
record: 1,
|
|
182
|
+
field: "front-matter",
|
|
183
|
+
expected: "valid YAML",
|
|
184
|
+
got: err?.name ?? "parse error",
|
|
185
|
+
hint: `front-matter parse failed: ${err?.message ?? err}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Extract content (everything after front-matter)
|
|
189
|
+
const content = lines.slice(i).join("\n");
|
|
190
|
+
// Build record from front-matter with defaults
|
|
191
|
+
let createdAt;
|
|
192
|
+
if (fm.date instanceof Date) {
|
|
193
|
+
createdAt = fm.date.toISOString();
|
|
194
|
+
}
|
|
195
|
+
else if (typeof fm.date === "string") {
|
|
196
|
+
createdAt = fm.date;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
createdAt = new Date().toISOString();
|
|
200
|
+
}
|
|
201
|
+
const record = {
|
|
202
|
+
content,
|
|
203
|
+
type: typeof fm.type === "string" ? fm.type : "fact",
|
|
204
|
+
subject: typeof fm.title === "string" ? fm.title : path.split("/").pop()?.replace(/\.md$/i, "") ?? "",
|
|
205
|
+
createdAt,
|
|
206
|
+
derivedFrom: [path],
|
|
207
|
+
foreignId: path, // Stable identifier for idempotent imports
|
|
208
|
+
};
|
|
209
|
+
// Handle tags - can be array or string
|
|
210
|
+
if (typeof fm.tags === "string") {
|
|
211
|
+
// Scalar string - split on comma or treat as single tag
|
|
212
|
+
const tagStr = fm.tags.trim();
|
|
213
|
+
if (tagStr.includes(",")) {
|
|
214
|
+
record.tags = tagStr.split(/,\s*/).filter((t) => t.length > 0);
|
|
215
|
+
}
|
|
216
|
+
else if (tagStr.length > 0) {
|
|
217
|
+
record.tags = [tagStr];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (Array.isArray(fm.tags)) {
|
|
221
|
+
record.tags = fm.tags.filter((t) => typeof t === "string");
|
|
222
|
+
}
|
|
223
|
+
yield { record, recordIndex: 1 };
|
|
224
|
+
}
|
|
@@ -29,7 +29,15 @@ export async function loadBridge(discovered, opts = {}) {
|
|
|
29
29
|
hint: `built-in "${discovered.name}" not in the registry — likely code drift between discover.ts and builtins/index.ts`,
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
// Dispatch on the type of descriptorOrPlugin
|
|
33
|
+
if (typeof builtin.descriptorOrPlugin.import === "function") {
|
|
34
|
+
// Code plugin (MemoryBridge)
|
|
35
|
+
return { kind: "code", plugin: builtin.descriptorOrPlugin, source: discovered };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// YAML descriptor
|
|
39
|
+
return { kind: "yaml", descriptor: builtin.descriptorOrPlugin, source: discovered };
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
case "project-yaml":
|
|
35
43
|
case "user-yaml": {
|
|
@@ -40,15 +48,17 @@ export async function loadBridge(discovered, opts = {}) {
|
|
|
40
48
|
if (!opts.skipAllowCheck) {
|
|
41
49
|
const verdict = await verifyAllow(discovered, { path: opts.allowListPath });
|
|
42
50
|
if (!verdict.ok) {
|
|
51
|
+
const reason = verdict.reason;
|
|
52
|
+
const failVerdict = verdict;
|
|
43
53
|
throw new BridgeRuntimeError({
|
|
44
54
|
bridge: discovered.name,
|
|
45
55
|
op: "import",
|
|
46
56
|
path: discovered.path,
|
|
47
57
|
field: "(trust)",
|
|
48
|
-
expected:
|
|
49
|
-
got:
|
|
50
|
-
hint: trustHint(discovered.name,
|
|
51
|
-
context: trustContext(discovered,
|
|
58
|
+
expected: reason === "not-allowed" ? "allow-listed code plugin" : "approved package at recorded location/digest",
|
|
59
|
+
got: reason,
|
|
60
|
+
hint: trustHint(discovered.name, failVerdict),
|
|
61
|
+
context: trustContext(discovered, failVerdict),
|
|
52
62
|
});
|
|
53
63
|
}
|
|
54
64
|
}
|