@tpsdev-ai/flair 0.7.0 → 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 CHANGED
@@ -160,7 +160,7 @@ 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](plugins/openclaw-flair/README.md) for details.
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
165
  ### Claude Code / Gemini CLI / OpenAI Codex CLI / Cursor (MCP)
166
166
 
@@ -187,6 +187,17 @@ Your agent's memory **follows it across CLIs** — same Flair instance, same age
187
187
 
188
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
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)**.
200
+
190
201
  ### JavaScript / TypeScript (Client Library)
191
202
 
192
203
  For custom integrations, use the lightweight client — no Harper, no embeddings, just HTTP + auth:
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * Built-in bridge registry.
3
3
  *
4
- * Each built-in ships as a typed `YamlBridgeDescriptor` (see
5
- * `agentic-stack.ts`). The CLI passes this registry to `discover()` so
6
- * built-ins surface in `flair bridge list` alongside user-authored
7
- * bridges, and to the import/export runtime so it knows how to load
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: write `src/bridges/builtins/<name>.ts`
11
- * exporting a `YamlBridgeDescriptor`, then register it here.
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
- function builtin(d) {
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
- descriptor: d,
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
- builtin(agenticStackDescriptor),
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: (input, init) => fetch(input, init),
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
- throw new BridgeRuntimeError({
40
- bridge,
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
- return { kind: "yaml", descriptor: builtin.descriptor, source: discovered };
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: verdict.reason === "not-allowed" ? "allow-listed code plugin" : "approved package at recorded location/digest",
49
- got: verdict.reason,
50
- hint: trustHint(discovered.name, verdict),
51
- context: trustContext(discovered, verdict),
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
  }