@tpsdev-ai/n8n-nodes-flair 0.8.1 → 0.8.2

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.
@@ -55,9 +55,17 @@ class FlairApi {
55
55
  ];
56
56
  authenticate = {
57
57
  type: 'generic',
58
+ // Use n8n's built-in HTTP Basic auth handling — it base64-encodes
59
+ // username:password internally. Avoids relying on Buffer being in n8n's
60
+ // expression sandbox (it isn't always, depending on n8n version).
61
+ // Earlier the credential used a custom expression with `Buffer.from(...)`
62
+ // which silently produced an empty Authorization header on installs
63
+ // where Buffer wasn't whitelisted in the expression engine, causing
64
+ // "Authorization failed" with valid credentials (2026-05-11 incident).
58
65
  properties: {
59
- headers: {
60
- Authorization: "=Basic {{ Buffer.from('admin:' + $credentials.adminPassword).toString('base64') }}",
66
+ auth: {
67
+ username: 'admin',
68
+ password: '={{ $credentials.adminPassword }}',
61
69
  },
62
70
  },
63
71
  };
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlairChatMemory = void 0;
4
4
  const memory_1 = require("@langchain/classic/memory");
5
5
  const n8n_workflow_1 = require("n8n-workflow");
6
- const flair_client_1 = require("@tpsdev-ai/flair-client");
7
6
  const FlairChatMessageHistory_1 = require("./FlairChatMessageHistory");
8
7
  class FlairChatMemory {
9
8
  description = {
@@ -70,7 +69,11 @@ class FlairChatMemory {
70
69
  const sessionKey = this.getNodeParameter("sessionKey", itemIndex, "");
71
70
  const k = this.getNodeParameter("contextWindowLength", itemIndex, 10);
72
71
  const composedSubject = sessionKey ? `${subject}:${sessionKey}` : subject;
73
- const flair = new flair_client_1.FlairClient({
72
+ // See FlairWrite.node.ts for the rationale on Function-wrapped dynamic
73
+ // import (prevents TSC from downleveling to require() under
74
+ // module: "CommonJS").
75
+ const flairMod = await new Function("return import('@tpsdev-ai/flair-client')")();
76
+ const flair = new flairMod.FlairClient({
74
77
  url: credentials.baseUrl,
75
78
  agentId: credentials.agentId,
76
79
  adminUser: "admin",
@@ -4,9 +4,13 @@ exports.FlairSearch = void 0;
4
4
  const tools_1 = require("@langchain/core/tools");
5
5
  const zod_1 = require("zod");
6
6
  const n8n_workflow_1 = require("n8n-workflow");
7
- const flair_client_1 = require("@tpsdev-ai/flair-client");
8
- function makeClient(credentials) {
9
- return new flair_client_1.FlairClient({
7
+ // See FlairWrite.node.ts for the rationale on Function-wrapped dynamic
8
+ // import (prevents TSC from downleveling to require() under
9
+ // module: "CommonJS").
10
+ const importFlairClient = () => new Function("return import('@tpsdev-ai/flair-client')")();
11
+ async function makeClient(credentials) {
12
+ const mod = await importFlairClient();
13
+ return new mod.FlairClient({
10
14
  url: credentials.baseUrl,
11
15
  agentId: credentials.agentId,
12
16
  adminUser: "admin",
@@ -126,7 +130,7 @@ class FlairSearch {
126
130
  const credentials = (await this.getCredentials("flairApi"));
127
131
  const operation = this.getNodeParameter("operation", itemIndex);
128
132
  const limit = this.getNodeParameter("limit", itemIndex, 5);
129
- const flair = makeClient(credentials);
133
+ const flair = await makeClient(credentials);
130
134
  if (operation === "search") {
131
135
  const tool = new tools_1.DynamicStructuredTool({
132
136
  name: "flair_search",
@@ -159,7 +163,7 @@ class FlairSearch {
159
163
  }
160
164
  async execute() {
161
165
  const credentials = (await this.getCredentials("flairApi"));
162
- const flair = makeClient(credentials);
166
+ const flair = await makeClient(credentials);
163
167
  const inputs = this.getInputData();
164
168
  const out = [];
165
169
  for (let i = 0; i < inputs.length; i++) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * FlairWrite — pipeline-mode node that writes a memory into Flair.
3
+ *
4
+ * Complement to FlairSearch (read-only AI Tool) and FlairChatMemory
5
+ * (LangChain BaseMemory adapter). FlairWrite is a regular Main-input /
6
+ * Main-output node intended for capture-and-archive workflows: take an
7
+ * incoming item (a TPS mail, a webhook payload, a parsed document) and
8
+ * persist its content as a Flair memory with operator-chosen tags,
9
+ * subject, and durability.
10
+ *
11
+ * Why a separate node from FlairSearch:
12
+ * - FlairSearch is wired into the AI Tool socket; it's read-only by
13
+ * design (write-as-LLM-tool needs guardrails we haven't designed
14
+ * yet — content sanitization, rate limiting, audit trail).
15
+ * - FlairWrite is operator-driven. Side effects are intentional and
16
+ * scoped to the workflow author's choices, not the LLM's.
17
+ * - Splitting keeps the AI-Tool-only contract on FlairSearch clean
18
+ * and lets each node evolve independently.
19
+ *
20
+ * The dual-purpose alternative (one node with both AI Tool and Main
21
+ * sockets) is possible in n8n but harder to reason about — the same
22
+ * configuration would surface differently depending on which socket
23
+ * the user wires it to. Two narrow nodes are easier to discover and
24
+ * easier to dogfood.
25
+ */
26
+ import { type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription } from "n8n-workflow";
27
+ export declare class FlairWrite implements INodeType {
28
+ description: INodeTypeDescription;
29
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
30
+ }
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /**
3
+ * FlairWrite — pipeline-mode node that writes a memory into Flair.
4
+ *
5
+ * Complement to FlairSearch (read-only AI Tool) and FlairChatMemory
6
+ * (LangChain BaseMemory adapter). FlairWrite is a regular Main-input /
7
+ * Main-output node intended for capture-and-archive workflows: take an
8
+ * incoming item (a TPS mail, a webhook payload, a parsed document) and
9
+ * persist its content as a Flair memory with operator-chosen tags,
10
+ * subject, and durability.
11
+ *
12
+ * Why a separate node from FlairSearch:
13
+ * - FlairSearch is wired into the AI Tool socket; it's read-only by
14
+ * design (write-as-LLM-tool needs guardrails we haven't designed
15
+ * yet — content sanitization, rate limiting, audit trail).
16
+ * - FlairWrite is operator-driven. Side effects are intentional and
17
+ * scoped to the workflow author's choices, not the LLM's.
18
+ * - Splitting keeps the AI-Tool-only contract on FlairSearch clean
19
+ * and lets each node evolve independently.
20
+ *
21
+ * The dual-purpose alternative (one node with both AI Tool and Main
22
+ * sockets) is possible in n8n but harder to reason about — the same
23
+ * configuration would surface differently depending on which socket
24
+ * the user wires it to. Two narrow nodes are easier to discover and
25
+ * easier to dogfood.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.FlairWrite = void 0;
29
+ const n8n_workflow_1 = require("n8n-workflow");
30
+ // Wrap dynamic import in Function() so TypeScript (compiled to CommonJS
31
+ // for n8n consumption) doesn't downlevel `await import(...)` to a `require()`
32
+ // call. The downleveled require() hits flair-client's ESM-only exports map
33
+ // and Node 24+ rejects it (which is what bit us in n8n at runtime —
34
+ // commit 31dd2b3 "fixed" this via dynamic import but TSC compiled it right
35
+ // back to require under `module: "CommonJS"`).
36
+ // The Function() trick keeps the import as a true native dynamic import in
37
+ // the emitted JS, which Node honors as ESM regardless of caller's module
38
+ // type. Standard CJS-to-ESM interop pattern.
39
+ const importFlairClient = () => new Function("return import('@tpsdev-ai/flair-client')")();
40
+ async function makeClient(credentials) {
41
+ const mod = await importFlairClient();
42
+ return new mod.FlairClient({
43
+ url: credentials.baseUrl,
44
+ agentId: credentials.agentId,
45
+ adminUser: "admin",
46
+ adminPassword: credentials.adminPassword,
47
+ });
48
+ }
49
+ class FlairWrite {
50
+ description = {
51
+ displayName: "Flair Write",
52
+ name: "flairWrite",
53
+ icon: "file:flair.svg",
54
+ group: ["output"],
55
+ version: [1],
56
+ description: "Write a memory into Flair from a workflow item. Use for capture-and-archive flows (mail → memory, webhook → memory, parsed-doc → memory).",
57
+ defaults: {
58
+ name: "Flair Write",
59
+ },
60
+ credentials: [
61
+ {
62
+ name: "flairApi",
63
+ required: true,
64
+ },
65
+ ],
66
+ codex: {
67
+ categories: ["AI"],
68
+ subcategories: {
69
+ AI: ["Memory"],
70
+ },
71
+ resources: {
72
+ primaryDocumentation: [
73
+ {
74
+ url: "https://github.com/tpsdev-ai/flair#n8n",
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
80
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
81
+ properties: [
82
+ {
83
+ displayName: "Content",
84
+ name: "content",
85
+ type: "string",
86
+ typeOptions: { rows: 4 },
87
+ default: "={{ $json.content }}",
88
+ required: true,
89
+ description: "The memory content to store. Defaults to the input item's `content` field; override with any expression that produces a string.",
90
+ },
91
+ {
92
+ displayName: "Subject",
93
+ name: "subject",
94
+ type: "string",
95
+ default: "",
96
+ description: "Optional subject (the entity / conversation / topic this memory is about). Maps to Flair's `subject` field. Leave blank to omit.",
97
+ },
98
+ {
99
+ displayName: "Tags",
100
+ name: "tags",
101
+ type: "string",
102
+ default: "",
103
+ description: 'Comma-separated tags (e.g. "source:tps-mail, kind:review, agent:kern"). Whitespace around commas is trimmed.',
104
+ },
105
+ {
106
+ displayName: "Durability",
107
+ name: "durability",
108
+ type: "options",
109
+ options: [
110
+ { name: "Standard (default)", value: "standard" },
111
+ { name: "Persistent (key decisions)", value: "persistent" },
112
+ { name: "Permanent (inviolable)", value: "permanent" },
113
+ { name: "Ephemeral (auto-expires 72h)", value: "ephemeral" },
114
+ ],
115
+ default: "standard",
116
+ description: "Durability tier. See Flair docs for the semantic differences.",
117
+ },
118
+ {
119
+ displayName: "Type",
120
+ name: "type",
121
+ type: "options",
122
+ options: [
123
+ { name: "Session (default)", value: "session" },
124
+ { name: "Lesson", value: "lesson" },
125
+ { name: "Decision", value: "decision" },
126
+ { name: "Preference", value: "preference" },
127
+ { name: "Fact", value: "fact" },
128
+ { name: "Goal", value: "goal" },
129
+ ],
130
+ default: "session",
131
+ description: "Memory type. Used by Flair's downstream filters and ranking.",
132
+ },
133
+ {
134
+ displayName: "Skip Empty Content",
135
+ name: "skipEmpty",
136
+ type: "boolean",
137
+ default: true,
138
+ description: "If on, items whose content evaluates to an empty/whitespace-only string are passed through without writing. Off = the node throws on empty content (fail-loud for required-field workflows).",
139
+ },
140
+ ],
141
+ };
142
+ async execute() {
143
+ const credentials = (await this.getCredentials("flairApi"));
144
+ const flair = await makeClient(credentials);
145
+ const inputs = this.getInputData();
146
+ const out = [];
147
+ for (let i = 0; i < inputs.length; i++) {
148
+ const content = this.getNodeParameter("content", i);
149
+ const subject = this.getNodeParameter("subject", i, "");
150
+ const tagsStr = this.getNodeParameter("tags", i, "");
151
+ const durability = this.getNodeParameter("durability", i, "standard");
152
+ const type = this.getNodeParameter("type", i, "session");
153
+ const skipEmpty = this.getNodeParameter("skipEmpty", i, true);
154
+ const trimmedContent = (content ?? "").trim();
155
+ if (trimmedContent === "") {
156
+ if (skipEmpty) {
157
+ // Pass-through: surface that we skipped, but don't error.
158
+ out.push({
159
+ json: { ...inputs[i].json, _flair_skipped: "empty content" },
160
+ pairedItem: { item: i },
161
+ });
162
+ continue;
163
+ }
164
+ throw new Error(`FlairWrite: empty content on item ${i} (skipEmpty is off). ` +
165
+ `Content evaluated to "${content ?? ""}".`);
166
+ }
167
+ const tags = tagsStr
168
+ .split(",")
169
+ .map((t) => t.trim())
170
+ .filter((t) => t.length > 0);
171
+ const result = await flair.memory.write(trimmedContent, {
172
+ type,
173
+ durability,
174
+ tags: tags.length > 0 ? tags : undefined,
175
+ subject: subject || undefined,
176
+ });
177
+ out.push({
178
+ json: {
179
+ ...inputs[i].json,
180
+ _flair_id: result.id,
181
+ _flair_subject: subject || null,
182
+ _flair_tags: tags,
183
+ _flair_durability: durability,
184
+ _flair_type: type,
185
+ },
186
+ pairedItem: { item: i },
187
+ });
188
+ }
189
+ return [out];
190
+ }
191
+ }
192
+ exports.FlairWrite = FlairWrite;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/n8n-nodes-flair",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "n8n community node — use Flair as your AI Agent's memory backend. Includes FlairChatMemory (Memory port) and FlairSearch (Tool port).",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,8 @@
24
24
  ],
25
25
  "nodes": [
26
26
  "dist/nodes/FlairChatMemory/FlairChatMemory.node.js",
27
- "dist/nodes/FlairSearch/FlairSearch.node.js"
27
+ "dist/nodes/FlairSearch/FlairSearch.node.js",
28
+ "dist/nodes/FlairWrite/FlairWrite.node.js"
28
29
  ]
29
30
  },
30
31
  "engines": {
@@ -47,7 +48,7 @@
47
48
  "langchain"
48
49
  ],
49
50
  "dependencies": {
50
- "@tpsdev-ai/flair-client": "0.8.1",
51
+ "@tpsdev-ai/flair-client": "0.8.2",
51
52
  "zod": "3.25.76"
52
53
  },
53
54
  "peerDependencies": {