@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.
- package/dist/credentials/FlairApi.credentials.js +10 -2
- package/dist/nodes/FlairChatMemory/FlairChatMemory.node.js +5 -2
- package/dist/nodes/FlairSearch/FlairSearch.node.js +9 -5
- package/dist/nodes/FlairWrite/FlairWrite.node.d.ts +30 -0
- package/dist/nodes/FlairWrite/FlairWrite.node.js +192 -0
- package/package.json +4 -3
|
@@ -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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
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.
|
|
51
|
+
"@tpsdev-ai/flair-client": "0.8.2",
|
|
51
52
|
"zod": "3.25.76"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|