@manny-est/node-red-flowpilot 0.2.1

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.
@@ -0,0 +1,377 @@
1
+ const http = require("http");
2
+ const https = require("https");
3
+
4
+ function postJson(urlString, headers, body, timeoutMs) {
5
+ return new Promise((resolve, reject) => {
6
+ let url;
7
+ try {
8
+ url = new URL(urlString);
9
+ } catch (err) {
10
+ reject(new Error(`Invalid provider URL: ${urlString}`));
11
+ return;
12
+ }
13
+
14
+ const payload = JSON.stringify(body);
15
+ const transport = url.protocol === "https:" ? https : http;
16
+ const options = {
17
+ method: "POST",
18
+ hostname: url.hostname,
19
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
20
+ path: `${url.pathname}${url.search}`,
21
+ headers: Object.assign({
22
+ "Content-Type": "application/json",
23
+ "Content-Length": Buffer.byteLength(payload)
24
+ }, headers || {})
25
+ };
26
+
27
+ const req = transport.request(options, (res) => {
28
+ let data = "";
29
+ res.setEncoding("utf8");
30
+ res.on("data", chunk => { data += chunk; });
31
+ res.on("end", () => {
32
+ let parsed = null;
33
+ try {
34
+ parsed = data ? JSON.parse(data) : null;
35
+ } catch (err) {
36
+ reject(new Error(`Provider returned non-JSON response (${res.statusCode}): ${data.slice(0, 500)}`));
37
+ return;
38
+ }
39
+
40
+ if (res.statusCode < 200 || res.statusCode >= 300) {
41
+ const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : data;
42
+ reject(new Error(`Provider request failed (${res.statusCode}): ${msg}`));
43
+ return;
44
+ }
45
+
46
+ resolve(parsed);
47
+ });
48
+ });
49
+
50
+ req.on("error", reject);
51
+ req.setTimeout(timeoutMs || 180000, () => {
52
+ req.destroy(new Error(`Provider request timed out after ${timeoutMs || 180000}ms`));
53
+ });
54
+
55
+ req.write(payload);
56
+ req.end();
57
+ });
58
+ }
59
+
60
+ function getJson(urlString, headers, timeoutMs) {
61
+ return new Promise((resolve, reject) => {
62
+ let url;
63
+ try {
64
+ url = new URL(urlString);
65
+ } catch (err) {
66
+ reject(new Error(`Invalid provider URL: ${urlString}`));
67
+ return;
68
+ }
69
+
70
+ const transport = url.protocol === "https:" ? https : http;
71
+ const options = {
72
+ method: "GET",
73
+ hostname: url.hostname,
74
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
75
+ path: `${url.pathname}${url.search}`,
76
+ headers: headers || {}
77
+ };
78
+
79
+ const req = transport.request(options, (res) => {
80
+ let data = "";
81
+ res.setEncoding("utf8");
82
+ res.on("data", chunk => { data += chunk; });
83
+ res.on("end", () => {
84
+ let parsed = null;
85
+ try {
86
+ parsed = data ? JSON.parse(data) : null;
87
+ } catch (err) {
88
+ reject(new Error(`Provider returned non-JSON response (${res.statusCode}): ${data.slice(0, 500)}`));
89
+ return;
90
+ }
91
+
92
+ if (res.statusCode < 200 || res.statusCode >= 300) {
93
+ const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : data;
94
+ reject(new Error(`Provider request failed (${res.statusCode}): ${msg}`));
95
+ return;
96
+ }
97
+
98
+ resolve(parsed);
99
+ });
100
+ });
101
+
102
+ req.on("error", reject);
103
+ req.setTimeout(timeoutMs || 30000, () => {
104
+ req.destroy(new Error(`Provider request timed out after ${timeoutMs || 30000}ms`));
105
+ });
106
+
107
+ req.end();
108
+ });
109
+ }
110
+
111
+ // ---------------------------------------------------------------------
112
+ // Models dropdown: lists models via the OpenAI-compatible GET /v1/models
113
+ // endpoint, so the settings UI can suggest valid model names instead of
114
+ // the user guessing and hitting a 404 from /v1/chat/completions. Unlike
115
+ // chat()/probeTools(), settings.model is NOT required here — this is how
116
+ // the user picks one. Never throws: a provider without /v1/models (or any
117
+ // other failure) just means an empty list with an explanatory error, which
118
+ // the UI shows as a hint while leaving the model field free-text.
119
+ // ---------------------------------------------------------------------
120
+ async function listModels(settings) {
121
+ const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
122
+ if (!baseUrl) throw new Error("Provider base URL is required.");
123
+
124
+ const headers = {};
125
+ if (settings.apiKey) {
126
+ headers.Authorization = `Bearer ${settings.apiKey}`;
127
+ }
128
+
129
+ try {
130
+ const response = await getJson(`${baseUrl}/v1/models`, headers, 30000);
131
+ const data = response && Array.isArray(response.data) ? response.data : [];
132
+ const models = data
133
+ .map(function (m) { return m && m.id; })
134
+ .filter(function (id) { return typeof id === "string" && id; });
135
+ return { models: models };
136
+ } catch (err) {
137
+ return { models: [], error: err.message };
138
+ }
139
+ }
140
+
141
+ // `options.tools` (OpenAI-style tool/function definitions) and
142
+ // `options.toolChoice` are optional — when present, the request asks the
143
+ // provider to call a tool instead of (or in addition to) replying with text.
144
+ // `result.toolCalls` is the raw `message.tool_calls` array (or null), passed
145
+ // through unparsed so the agent loop can validate/dispatch each call itself.
146
+ // A tool-call-only response has no `content`, so the "[No assistant message
147
+ // returned...]" fallback only applies when there are no tool calls either.
148
+ async function chat(settings, messages, options) {
149
+ const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
150
+ if (!baseUrl) throw new Error("Provider base URL is required.");
151
+ if (!settings.model) throw new Error("Model is required.");
152
+
153
+ const headers = {};
154
+ if (settings.apiKey) {
155
+ headers.Authorization = `Bearer ${settings.apiKey}`;
156
+ }
157
+
158
+ const temperature = settings.temperature !== undefined ? Number(settings.temperature) : 0.2;
159
+
160
+ const body = {
161
+ model: settings.model,
162
+ messages,
163
+ temperature,
164
+ stream: false
165
+ };
166
+ if (options && Array.isArray(options.tools) && options.tools.length) {
167
+ body.tools = options.tools;
168
+ body.tool_choice = options.toolChoice || "auto";
169
+ }
170
+
171
+ const startedAt = Date.now();
172
+ const response = await postJson(`${baseUrl}/v1/chat/completions`, headers, body, 180000);
173
+ const totalMs = Date.now() - startedAt;
174
+
175
+ const message = response && response.choices && response.choices[0] && response.choices[0].message;
176
+ const content = message ? message.content : "";
177
+ const toolCalls = message && Array.isArray(message.tool_calls) && message.tool_calls.length
178
+ ? message.tool_calls
179
+ : null;
180
+
181
+ return {
182
+ raw: response,
183
+ content: content || (toolCalls ? "" : "[No assistant message returned by provider]"),
184
+ toolCalls: toolCalls,
185
+ timing: { totalMs },
186
+ usage: (response && response.usage) || null
187
+ };
188
+ }
189
+
190
+ // Streaming variant of chat(): requests `stream: true` and parses the
191
+ // OpenAI-compatible SSE format (lines `data: {...}`, terminated by
192
+ // `data: [DONE]`). Calls onDelta(text) for each content fragment as it
193
+ // arrives and resolves with { content } containing the full concatenated
194
+ // text once the stream ends. Used for chat streaming only.
195
+ function postStream(urlString, headers, body, timeoutMs, onDelta) {
196
+ return new Promise((resolve, reject) => {
197
+ let url;
198
+ try {
199
+ url = new URL(urlString);
200
+ } catch (err) {
201
+ reject(new Error(`Invalid provider URL: ${urlString}`));
202
+ return;
203
+ }
204
+
205
+ const payload = JSON.stringify(body);
206
+ const transport = url.protocol === "https:" ? https : http;
207
+ const options = {
208
+ method: "POST",
209
+ hostname: url.hostname,
210
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
211
+ path: `${url.pathname}${url.search}`,
212
+ headers: Object.assign({
213
+ "Content-Type": "application/json",
214
+ "Content-Length": Buffer.byteLength(payload)
215
+ }, headers || {})
216
+ };
217
+
218
+ const startedAt = Date.now();
219
+ let firstTokenAt = null;
220
+ let usage = null;
221
+
222
+ const req = transport.request(options, (res) => {
223
+ res.setEncoding("utf8");
224
+
225
+ if (res.statusCode < 200 || res.statusCode >= 300) {
226
+ let errData = "";
227
+ res.on("data", chunk => { errData += chunk; });
228
+ res.on("end", () => {
229
+ reject(new Error(`Provider request failed (${res.statusCode}): ${errData.slice(0, 500)}`));
230
+ });
231
+ return;
232
+ }
233
+
234
+ let buf = "";
235
+ let full = "";
236
+ res.on("data", (chunk) => {
237
+ buf += chunk;
238
+ const lines = buf.split("\n");
239
+ buf = lines.pop(); // keep the last (possibly partial) line for next time
240
+
241
+ lines.forEach((line) => {
242
+ line = line.trim();
243
+ if (!line.startsWith("data:")) { return; }
244
+ const dataStr = line.slice(5).trim();
245
+ if (!dataStr || dataStr === "[DONE]") { return; }
246
+
247
+ let evt;
248
+ try {
249
+ evt = JSON.parse(dataStr);
250
+ } catch (e) {
251
+ return; // ignore malformed/partial SSE chunk
252
+ }
253
+
254
+ if (evt && evt.usage) { usage = evt.usage; }
255
+
256
+ const delta = evt && evt.choices && evt.choices[0] && evt.choices[0].delta
257
+ ? evt.choices[0].delta.content
258
+ : "";
259
+ if (delta) {
260
+ if (firstTokenAt === null) { firstTokenAt = Date.now(); }
261
+ full += delta;
262
+ onDelta(delta);
263
+ }
264
+ });
265
+ });
266
+ res.on("end", () => {
267
+ const endedAt = Date.now();
268
+ resolve({
269
+ content: full,
270
+ ttftMs: firstTokenAt !== null ? firstTokenAt - startedAt : null,
271
+ totalMs: endedAt - startedAt,
272
+ usage: usage
273
+ });
274
+ });
275
+ });
276
+
277
+ req.on("error", reject);
278
+ req.setTimeout(timeoutMs || 180000, () => {
279
+ req.destroy(new Error(`Provider request timed out after ${timeoutMs || 180000}ms`));
280
+ });
281
+
282
+ req.write(payload);
283
+ req.end();
284
+ });
285
+ }
286
+
287
+ async function chatStream(settings, messages, onDelta) {
288
+ const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
289
+ if (!baseUrl) throw new Error("Provider base URL is required.");
290
+ if (!settings.model) throw new Error("Model is required.");
291
+
292
+ const headers = {};
293
+ if (settings.apiKey) {
294
+ headers.Authorization = `Bearer ${settings.apiKey}`;
295
+ }
296
+
297
+ const temperature = settings.temperature !== undefined ? Number(settings.temperature) : 0.2;
298
+
299
+ // stream_options.include_usage asks OpenAI-compatible servers to emit a
300
+ // final SSE chunk carrying token usage (no delta) before [DONE]. Providers
301
+ // that don't support it just ignore the option; postStream treats a
302
+ // missing usage field as null either way.
303
+ const result = await postStream(`${baseUrl}/v1/chat/completions`, headers, {
304
+ model: settings.model,
305
+ messages,
306
+ temperature,
307
+ stream: true,
308
+ stream_options: { include_usage: true }
309
+ }, 180000, onDelta);
310
+
311
+ return {
312
+ content: result.content || "",
313
+ timing: { ttftMs: result.ttftMs, totalMs: result.totalMs },
314
+ usage: result.usage
315
+ };
316
+ }
317
+
318
+ // ---------------------------------------------------------------------
319
+ // B-cap1: sends a minimal request with a trivial tool definition and a
320
+ // prompt that should trigger a tool call, to detect whether this provider
321
+ // supports OpenAI-style function/tool calling. Returns
322
+ // { supportsTools: boolean }, never throws — a provider that doesn't
323
+ // understand "tools" typically 400s on the request itself, which is a clean
324
+ // "no" for capability purposes (connectivity is checked separately by
325
+ // chat()).
326
+ //
327
+ // CAUTION: a pass here means the provider is ELIGIBLE for agentic
328
+ // tool-calling features, not a reliability guarantee — the agentic path must
329
+ // still handle malformed mid-conversation tool calls by falling back to the
330
+ // envelope for that turn.
331
+ // ---------------------------------------------------------------------
332
+ async function probeTools(settings) {
333
+ const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
334
+ if (!baseUrl) throw new Error("Provider base URL is required.");
335
+ if (!settings.model) throw new Error("Model is required.");
336
+
337
+ const headers = {};
338
+ if (settings.apiKey) {
339
+ headers.Authorization = `Bearer ${settings.apiKey}`;
340
+ }
341
+
342
+ const tools = [{
343
+ type: "function",
344
+ function: {
345
+ name: "ping",
346
+ description: "Respond to a connectivity probe. Takes no arguments.",
347
+ parameters: { type: "object", properties: {}, additionalProperties: false }
348
+ }
349
+ }];
350
+
351
+ try {
352
+ const response = await postJson(`${baseUrl}/v1/chat/completions`, headers, {
353
+ model: settings.model,
354
+ messages: [
355
+ { role: "system", content: "You are being tested for tool/function-calling support." },
356
+ { role: "user", content: "Call the \"ping\" tool now with no arguments. Do not reply with text." }
357
+ ],
358
+ tools: tools,
359
+ tool_choice: "auto",
360
+ temperature: 0,
361
+ stream: false
362
+ }, 30000);
363
+
364
+ const message = response && response.choices && response.choices[0] && response.choices[0].message;
365
+ const toolCalls = message && Array.isArray(message.tool_calls) ? message.tool_calls : [];
366
+ const wellFormed = toolCalls.length > 0 && toolCalls.every(function (call) {
367
+ return !!(call && call.function && typeof call.function.name === "string" &&
368
+ typeof call.function.arguments === "string");
369
+ });
370
+
371
+ return { supportsTools: wellFormed };
372
+ } catch (err) {
373
+ return { supportsTools: false, error: err.message };
374
+ }
375
+ }
376
+
377
+ module.exports = { chat, chatStream, probeTools, listModels };
package/lib/storage.js ADDED
@@ -0,0 +1,265 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function ensureDir(dir) {
5
+ fs.mkdirSync(dir, { recursive: true });
6
+ }
7
+
8
+ function createStorage(userDir) {
9
+ const baseDir = path.join(userDir, "flowpilot");
10
+ const chatsDir = path.join(baseDir, "chats");
11
+ const backupsDir = path.join(baseDir, "backups");
12
+ const settingsFile = path.join(baseDir, "settings.json");
13
+ const auditFile = path.join(baseDir, "audit.log");
14
+
15
+ // A provider profile. Each has its own model since model names differ
16
+ // across providers (LocalAI vs cloud).
17
+ function defaultProvider() {
18
+ return {
19
+ id: "default",
20
+ providerName: "LocalAI",
21
+ baseUrl: "http://localhost:8080",
22
+ apiKey: "",
23
+ model: "",
24
+ temperature: 0.2
25
+ };
26
+ }
27
+
28
+ const defaultSettings = {
29
+ // Multiple provider profiles; one is active at a time. The list shape is
30
+ // deliberately ready for a future side-by-side compare mode.
31
+ providers: [defaultProvider()],
32
+ activeProviderId: "default",
33
+ maxContextChars: 12000,
34
+ defaultContextMode: "selected",
35
+ allowConfigContext: false,
36
+ logFullContext: false,
37
+ streamingEnabled: true,
38
+ // First-run welcome/warning shows until the user saves settings once.
39
+ firstRunAcknowledged: false,
40
+ // Context-size advisory thresholds, in estimated tokens (~chars/4).
41
+ // Advisory only; never blocks sending.
42
+ contextWarnTokens: 4000,
43
+ contextHighTokens: 8000,
44
+ // How many recent chat exchanges (user+assistant pairs) the frontend
45
+ // includes as history with each request. Older turns are dropped
46
+ // client-side and the model is told when that happened.
47
+ historyMaxExchanges: 10,
48
+ // Lets the user silence the recurring secrets/size reminder bar after
49
+ // typing an explicit acknowledgement in settings.
50
+ suppressContextWarnings: false,
51
+ // User-defined intent buttons: array of { label, text }.
52
+ customIntents: [],
53
+ systemPrompt: require("./default-system-prompt")
54
+ };
55
+
56
+ // Older builds persisted a "Phase 1, READ-ONLY mode" system prompt into
57
+ // settings.json once and never updated it — Object.assign in getSettings
58
+ // lets that stale persisted copy win forever, even after
59
+ // default-system-prompt.js is fixed. Detect that stale text (by a phrase
60
+ // unique to it) and swap in the current default instead. Applied both when
61
+ // reading settings AND when saving them, so a browser tab that still has
62
+ // the stale text loaded in the System Prompt textarea can't re-persist it.
63
+ function fixStaleSystemPrompt(systemPrompt) {
64
+ if (typeof systemPrompt !== "string" || !systemPrompt.trim()) {
65
+ return defaultSettings.systemPrompt;
66
+ }
67
+ if (systemPrompt.indexOf("operating in READ-ONLY mode") !== -1) {
68
+ return defaultSettings.systemPrompt;
69
+ }
70
+ return systemPrompt;
71
+ }
72
+
73
+ // Migrate an old flat-provider settings object (providerName/baseUrl/etc at
74
+ // top level) into the new providers-list shape, preserving the user's
75
+ // configured provider. Idempotent: leaves new-format settings untouched.
76
+ function migrate(parsed) {
77
+ if (!parsed || typeof parsed !== "object") { return parsed; }
78
+ if (Array.isArray(parsed.providers) && parsed.providers.length) {
79
+ return parsed; // already new format
80
+ }
81
+ if (parsed.providerName || parsed.baseUrl || parsed.model) {
82
+ const migrated = Object.assign({}, parsed);
83
+ migrated.providers = [{
84
+ id: "default",
85
+ providerName: parsed.providerName || "LocalAI",
86
+ baseUrl: parsed.baseUrl || "http://localhost:8080",
87
+ apiKey: parsed.apiKey || "",
88
+ model: parsed.model || "",
89
+ temperature: parsed.temperature !== undefined ? parsed.temperature : 0.2
90
+ }];
91
+ migrated.activeProviderId = "default";
92
+ // Remove the now-relocated flat fields.
93
+ delete migrated.providerName;
94
+ delete migrated.baseUrl;
95
+ delete migrated.apiKey;
96
+ delete migrated.model;
97
+ delete migrated.temperature;
98
+ return migrated;
99
+ }
100
+ return parsed;
101
+ }
102
+
103
+ // Returns the currently active provider profile (or the first, or a default).
104
+ function getActiveProvider(settings) {
105
+ const list = Array.isArray(settings.providers) ? settings.providers : [];
106
+ if (!list.length) { return defaultProvider(); }
107
+ const found = list.filter(function (p) { return p.id === settings.activeProviderId; })[0];
108
+ return found || list[0];
109
+ }
110
+
111
+ function init() {
112
+ ensureDir(baseDir);
113
+ ensureDir(chatsDir);
114
+ ensureDir(backupsDir);
115
+
116
+ if (!fs.existsSync(settingsFile)) {
117
+ fs.writeFileSync(settingsFile, JSON.stringify(defaultSettings, null, 2), "utf8");
118
+ }
119
+
120
+ if (!fs.existsSync(auditFile)) {
121
+ fs.writeFileSync(auditFile, "", "utf8");
122
+ }
123
+ }
124
+
125
+ function getSettings() {
126
+ init();
127
+
128
+ try {
129
+ const raw = fs.readFileSync(settingsFile, "utf8");
130
+ const parsed = migrate(JSON.parse(raw));
131
+ // Merge top-level app settings with defaults, but take the providers
132
+ // list verbatim from the file (don't let defaults overwrite it).
133
+ const merged = Object.assign({}, defaultSettings, parsed);
134
+ if (Array.isArray(parsed.providers) && parsed.providers.length) {
135
+ merged.providers = parsed.providers;
136
+ }
137
+ merged.systemPrompt = fixStaleSystemPrompt(merged.systemPrompt);
138
+ return merged;
139
+ } catch (err) {
140
+ return Object.assign({}, defaultSettings, {
141
+ _error: err.message
142
+ });
143
+ }
144
+ }
145
+
146
+ function saveSettings(settings) {
147
+ init();
148
+
149
+ let current = {};
150
+ try {
151
+ const raw = fs.readFileSync(settingsFile, "utf8");
152
+ current = migrate(JSON.parse(raw));
153
+ } catch (err) {
154
+ current = {};
155
+ }
156
+
157
+ const merged = Object.assign({}, defaultSettings, current, settings || {});
158
+ merged.systemPrompt = fixStaleSystemPrompt(merged.systemPrompt);
159
+ delete merged._error;
160
+ // If the caller sent a providers list, it wins outright (Object.assign
161
+ // already did this, but be explicit for clarity/safety).
162
+ if (settings && Array.isArray(settings.providers)) {
163
+ merged.providers = settings.providers;
164
+ }
165
+ // Saving settings is an explicit user action; mark first-run complete so
166
+ // the welcome/warning stops showing.
167
+ merged.firstRunAcknowledged = true;
168
+
169
+ fs.writeFileSync(settingsFile, JSON.stringify(merged, null, 2), "utf8");
170
+
171
+ return merged;
172
+ }
173
+
174
+ function appendAudit(entry) {
175
+ init();
176
+
177
+ const line = JSON.stringify({
178
+ timestamp: new Date().toISOString(),
179
+ ...entry
180
+ });
181
+
182
+ fs.appendFileSync(auditFile, line + "\n", "utf8");
183
+ }
184
+
185
+ // Per-conversation transcripts: one JSON Lines file per conversation,
186
+ // keyed by a frontend-generated conversationId. Callers must pass an
187
+ // already-validated id (flowpilot.js's sanitizeConversationId) — this is
188
+ // just file I/O.
189
+ function transcriptFile(conversationId) {
190
+ return path.join(chatsDir, `${conversationId}.jsonl`);
191
+ }
192
+
193
+ function appendTranscript(conversationId, entry) {
194
+ init();
195
+ fs.appendFileSync(transcriptFile(conversationId), JSON.stringify(entry) + "\n", "utf8");
196
+ }
197
+
198
+ // Removes a conversation's transcript file (e.g. user deletes it from the
199
+ // conversation list). Best-effort — a missing file is not an error.
200
+ function deleteTranscript(conversationId) {
201
+ init();
202
+ try { fs.unlinkSync(transcriptFile(conversationId)); } catch (err) { /* already gone */ }
203
+ }
204
+
205
+ function readTranscript(conversationId) {
206
+ init();
207
+ const file = transcriptFile(conversationId);
208
+ if (!fs.existsSync(file)) { return []; }
209
+
210
+ let raw;
211
+ try {
212
+ raw = fs.readFileSync(file, "utf8");
213
+ } catch (err) {
214
+ return [];
215
+ }
216
+
217
+ return raw.split("\n").filter(Boolean).map(function (line) {
218
+ try { return JSON.parse(line); } catch (err) { return null; }
219
+ }).filter(Boolean);
220
+ }
221
+
222
+ // Used by Recall to search across OTHER conversations' transcripts. Lists
223
+ // every persisted conversationId (one per chats/*.jsonl file).
224
+ function listConversationIds() {
225
+ init();
226
+ let files;
227
+ try {
228
+ files = fs.readdirSync(chatsDir);
229
+ } catch (err) {
230
+ return [];
231
+ }
232
+ return files
233
+ .filter(function (f) { return f.endsWith(".jsonl"); })
234
+ .map(function (f) { return f.slice(0, -6); });
235
+ }
236
+
237
+ // Always the CURRENT contents of lib/default-system-prompt.js — never the
238
+ // stale copy that may be persisted in settings.json. Lets the Settings UI
239
+ // offer a "Reset to default" action that picks up prompt updates shipped
240
+ // in later FlowPilot versions, even though a snapshot was saved once.
241
+ function getDefaultSystemPrompt() {
242
+ return defaultSettings.systemPrompt;
243
+ }
244
+
245
+ init();
246
+
247
+ return {
248
+ baseDir,
249
+ chatsDir,
250
+ backupsDir,
251
+ settingsFile,
252
+ auditFile,
253
+ getSettings,
254
+ saveSettings,
255
+ getActiveProvider,
256
+ getDefaultSystemPrompt,
257
+ appendAudit,
258
+ appendTranscript,
259
+ readTranscript,
260
+ deleteTranscript,
261
+ listConversationIds
262
+ };
263
+ }
264
+
265
+ module.exports = createStorage;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@manny-est/node-red-flowpilot",
3
+ "version": "0.2.1",
4
+ "description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
5
+ "main": "flowpilot.js",
6
+ "keywords": [
7
+ "node-red",
8
+ "ai",
9
+ "localai",
10
+ "openai",
11
+ "flowpilot"
12
+ ],
13
+ "author": "manny-est",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/manny-est/flowpilot.git"
18
+ },
19
+ "homepage": "https://github.com/manny-est/flowpilot#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/manny-est/flowpilot/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "node-red": {
27
+ "version": ">=4.0.0",
28
+ "nodes": {
29
+ "flowpilot": "flowpilot.js"
30
+ },
31
+ "plugins": {
32
+ "flowpilot": "flowpilot.html"
33
+ }
34
+ },
35
+ "engines": {
36
+ "node": ">=16"
37
+ },
38
+ "files": [
39
+ "flowpilot.js",
40
+ "flowpilot.html",
41
+ "lib",
42
+ "icons",
43
+ "examples",
44
+ "README.md",
45
+ "USER-GUIDE.md",
46
+ "PROJECT-OVERVIEW.md",
47
+ "LICENSE"
48
+ ]
49
+ }