@flowfuse/nr-assistant 0.6.1-f8348b5-202509051340.0 → 0.6.1-fd37d56-202512091447.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/lib/assistant.js CHANGED
@@ -2,6 +2,7 @@
2
2
  'use strict'
3
3
 
4
4
  const { z } = require('zod')
5
+ const auth = require('./auth')
5
6
  const { getLongestUpstreamPath } = require('./flowGraph')
6
7
  const { hasProperty } = require('./utils')
7
8
  const semver = require('semver')
@@ -23,6 +24,7 @@ class Assistant {
23
24
  this.options = null
24
25
  this._loading = false // Flag to indicate if the Assistant is currently loading
25
26
  this._enabled = false // Flag to indicate if the Assistant is enabled
27
+ this._adminRoutesInitialized = false
26
28
 
27
29
  // MCP Client and Server and associated properties
28
30
  /** @type {import('@modelcontextprotocol/sdk/client/index.js').Client} */
@@ -54,38 +56,60 @@ class Assistant {
54
56
  */
55
57
  async init (RED, options = {}) {
56
58
  if (this._loading) {
57
- RED.log.debug('FlowFuse Assistant is busy loading')
59
+ this.RED.log.debug('FlowFuse Expert is busy loading')
58
60
  return
59
61
  }
60
- try {
61
- this._loading = true // Set loading to true when initializing
62
- await this.dispose() // Dispose of any existing instance before initializing a new one
63
- this.RED = RED
64
- this.options = options || {}
65
- this.got = this.options.got || require('got') // got can be passed in for testing purposes
66
-
62
+ const wasStandaloneEnabled = !!this.options?.standalone && this.options?.enabled
63
+ await this.dispose() // Dispose of any existing instance before initializing a new one
64
+ this.RED = RED
65
+ this.options = options || {}
66
+ this.got = this.options.got || require('got') // got can be passed in for testing purposes
67
+
68
+ if (this.options.standalone) {
69
+ if (wasStandaloneEnabled && !this.options.enabled) {
70
+ // The assistant was previously enabled in standalone mode, but is now disabled (auth issue).
71
+ this.RED.log.info('FlowFuse Expert Plugin has been disabled')
72
+ RED.comms.publish('nr-assistant/mcp/ready', { enabled: false }, true /* retain */)
73
+ RED.comms.publish('nr-assistant/completions/ready', { enabled: false }, true /* retain */)
74
+ } else {
75
+ this.RED.log.info('FlowFuse Expert Plugin is running in standalone mode')
76
+ }
77
+ this.initAdminAuthEndpoints(RED)
78
+ } else {
67
79
  if (!this.options.enabled) {
68
- RED.log.info('FlowFuse Assistant Plugin is not enabled')
80
+ RED.log.info('FlowFuse Expert Plugin is not enabled')
69
81
  return
70
82
  }
71
83
  if (!this.options.url || !this.options.token) {
72
- RED.log.warn('FlowFuse Assistant Plugin configuration is missing required options')
84
+ RED.log.warn('FlowFuse Expert Plugin configuration is missing required options')
73
85
  throw new Error('Plugin configuration is missing required options')
74
86
  }
87
+ }
88
+ const clientSettings = {
89
+ enabled: this.options.enabled !== false && !!this.options.url,
90
+ tablesEnabled: this.options.tables?.enabled === true,
91
+ inlineCompletionsEnabled: this.options.completions?.inlineEnabled === true,
92
+ requestTimeout: this.options.requestTimeout || 60000
93
+ }
94
+ if (this.options.standalone) {
95
+ clientSettings.standalone = true
96
+ }
97
+ RED.comms.publish('nr-assistant/initialise', clientSettings, true /* retain */)
75
98
 
99
+ if (this.options.enabled) {
100
+ await this.completeInitialization(clientSettings)
101
+ }
102
+ }
103
+
104
+ async completeInitialization (clientSettings) {
105
+ const RED = this.RED
106
+ try {
107
+ this._loading = true // Set loading to true when initializing
76
108
  const nrVersion = this.RED.version()
77
109
  const nrMajorVersion = semver.major(nrVersion)
78
110
  const nrMinorVersion = semver.minor(nrVersion)
79
111
  const nodeMajorVersion = semver.major(process.versions.node)
80
112
 
81
- const clientSettings = {
82
- enabled: this.options.enabled !== false && !!this.options.url,
83
- tablesEnabled: this.options.tables?.enabled === true,
84
- inlineCompletionsEnabled: this.options.completions?.inlineEnabled === true,
85
- requestTimeout: this.options.requestTimeout || 60000
86
- }
87
- RED.comms.publish('nr-assistant/initialise', clientSettings, true /* retain */)
88
-
89
113
  // ### Initialise Model Context Protocol (MCP)
90
114
  // TODO: If "feature" is disabled, skip loading MCP. See issue #57
91
115
  this.options.mcp = this.options.mcp || { enabled: true }
@@ -99,19 +123,19 @@ class Assistant {
99
123
  this.mcpReady = true
100
124
  // tell frontend that the MCP client is ready so it can add the action(s) to the Action List
101
125
  RED.comms.publish('nr-assistant/mcp/ready', clientSettings, true /* retain */)
102
- RED.log.info('FlowFuse Assistant Model Context Protocol (MCP) loaded')
126
+ RED.log.info('FlowFuse Expert Model Context Protocol (MCP) loaded')
103
127
  } catch (error) {
104
128
  this.mcpReady = false
105
129
  // ESM Support in Node 20 is much better than versions v18-, so lets include a node version
106
130
  // Write a warning to log as a hint/prompt
107
131
  // NOTE: Node 18 is EOL as of writing this
108
- RED.log.warn('FlowFuse Assistant MCP could not be loaded. Assistant features that require MCP will not be available')
132
+ RED.log.warn('FlowFuse Expert MCP could not be loaded. Expert features that require MCP will not be available')
109
133
  if (nodeMajorVersion < 20) {
110
134
  RED.log.debug(`Node.js version ${nodeMajorVersion} may not be supported by MCP Client / Server.`)
111
135
  }
112
136
  }
113
137
  } else if (!mcpFeatureEnabled) {
114
- RED.log.info('FlowFuse Assistant MCP is disabled')
138
+ RED.log.info('FlowFuse Expert MCP is disabled')
115
139
  }
116
140
 
117
141
  // ### Initialise completions (depends on MCP so checks the mcpReady flag)
@@ -122,9 +146,9 @@ class Assistant {
122
146
  const completionsEnabled = completionsFeatureEnabled && this.isInitialized && this.isEnabled
123
147
 
124
148
  if (!completionsSupported) {
125
- RED.log.warn('FlowFuse Assistant Completions require Node-RED 4.1 or greater')
149
+ RED.log.warn('FlowFuse Expert Completions require Node-RED 4.1 or greater')
126
150
  } else if (!completionsFeatureEnabled) {
127
- RED.log.info('FlowFuse Assistant Completions are disabled')
151
+ RED.log.info('FlowFuse Expert Completions are disabled')
128
152
  } else if (this.mcpReady && completionsEnabled && completionsSupported) {
129
153
  // if modelUrl is not set, use the default model URL from this.options.url + 'assets/completions/model.onnx'
130
154
  this.options.completions.modelUrl = this.options.completions.modelUrl || new URL('assets/completions/model.onnx', this.options.url).href
@@ -134,21 +158,21 @@ class Assistant {
134
158
  // RED.events.once('comms:message:nr-assistant/completions/load', async (opts) => {
135
159
  RED.events.once('comms:message:nr-assistant/completions/load', async (opts) => {
136
160
  try {
137
- RED.log.info('FlowFuse Assistant is Loading Advanced Completions...')
161
+ RED.log.info('FlowFuse Expert is Loading Advanced Completions...')
138
162
  await this.loadCompletions()
139
163
  RED.comms.publish('nr-assistant/completions/ready', { enabled: true }, true /* retain */)
140
- RED.log.info('FlowFuse Assistant Completions Loaded')
164
+ RED.log.info('FlowFuse Expert Completions Loaded')
141
165
  this.completionsReady = true
142
166
  } catch (error) {
143
167
  this.completionsReady = false
144
- RED.log.warn('FlowFuse Assistant Advanced Completions could not be loaded.') // degraded functionality
168
+ RED.log.warn('FlowFuse Expert Advanced Completions could not be loaded.') // degraded functionality
145
169
  RED.log.debug(`Completions loading error: ${error.message}`)
146
170
  }
147
171
  })
148
172
  }
149
- this.initAdminEndpoints(RED, { inlineCompletionsEnabled: clientSettings.inlineCompletionsEnabled }) // Initialize the admin endpoints for the Assistant
173
+ this.initAdminEndpoints(RED) // Initialize the admin endpoints for the Assistant
150
174
  const degraded = (mcpEnabled && !this.mcpReady)
151
- RED.log.info('FlowFuse Assistant Plugin loaded' + (degraded ? ' (reduced functionality)' : ''))
175
+ RED.log.info('FlowFuse Expert Plugin loaded' + (degraded ? ' (reduced functionality)' : ''))
152
176
  } finally {
153
177
  this._loading = false // Set loading to false when initialization is complete
154
178
  }
@@ -190,7 +214,14 @@ class Assistant {
190
214
  if (!this.options) {
191
215
  return false
192
216
  }
193
- return !!(this.options.enabled && this.options.url && this.options.token)
217
+ return !!(
218
+ this.options.enabled &&
219
+ this.options.url &&
220
+ (
221
+ (!this.options.standalone && this.options.token) ||
222
+ (this.options.standalone && auth.getUserToken())
223
+ )
224
+ )
194
225
  }
195
226
 
196
227
  async loadCompletions () {
@@ -209,7 +240,7 @@ class Assistant {
209
240
  const response = await this.got(url, {
210
241
  responseType: 'json',
211
242
  headers: {
212
- Authorization: `Bearer ${this.options.token}`,
243
+ Authorization: `Bearer ${auth.getUserToken()}`,
213
244
  'User-Agent': FF_ASSISTANT_USER_AGENT
214
245
  }
215
246
  })
@@ -247,7 +278,7 @@ class Assistant {
247
278
  try {
248
279
  const response = await this.got(url, {
249
280
  headers: {
250
- Authorization: `Bearer ${this.options.token}`,
281
+ Authorization: `Bearer ${auth.getUserToken()}`,
251
282
  'User-Agent': FF_ASSISTANT_USER_AGENT
252
283
  },
253
284
  responseType: 'buffer' // Ensure we get raw binary
@@ -410,7 +441,7 @@ class Assistant {
410
441
 
411
442
  // #region Admin Endpoints & HTTP Handlers
412
443
 
413
- initAdminEndpoints (RED, { inlineCompletionsEnabled }) {
444
+ initAdminEndpoints (RED) {
414
445
  // Hook up routes first ordered by static --> specific --> generic
415
446
 
416
447
  RED.httpAdmin.get('/nr-assistant/mcp/prompts', RED.auth.needsPermission('write'), async function (req, res) {
@@ -425,17 +456,23 @@ class Assistant {
425
456
  return assistant.handlePostToolRequest(req, res)
426
457
  })
427
458
 
428
- if (inlineCompletionsEnabled) {
429
- RED.httpAdmin.post('/nr-assistant/fim/:nodeModule/:nodeType', RED.auth.needsPermission('write'), function (req, res) {
430
- return assistant.handlePostFimRequest(req, res)
431
- })
432
- }
459
+ RED.httpAdmin.post('/nr-assistant/fim/:nodeModule/:nodeType', RED.auth.needsPermission('write'), function (req, res) {
460
+ return assistant.handlePostFimRequest(req, res)
461
+ })
433
462
 
434
463
  RED.httpAdmin.post('/nr-assistant/:method', RED.auth.needsPermission('write'), function (req, res) {
435
464
  return assistant.handlePostMethodRequest(req, res)
436
465
  })
437
466
  }
438
467
 
468
+ initAdminAuthEndpoints (RED) {
469
+ if (!this._adminRoutesInitialized) {
470
+ // Only required in standalone mode
471
+ auth.setupRoutes(this, RED)
472
+ this._adminRoutesInitialized = true
473
+ }
474
+ }
475
+
439
476
  /**
440
477
  * Handles POST requests to the /nr-assistant/:method endpoint.
441
478
  * This is for handling custom methods that the Assistant can perform.
@@ -471,7 +508,7 @@ class Assistant {
471
508
  headers: {
472
509
  Accept: '*/*',
473
510
  'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,es;q=0.7',
474
- Authorization: `Bearer ${this.options.token}`,
511
+ Authorization: `Bearer ${auth.getUserToken()}`,
475
512
  'Content-Type': 'application/json',
476
513
  'User-Agent': FF_ASSISTANT_USER_AGENT
477
514
  },
@@ -491,7 +528,7 @@ class Assistant {
491
528
  // ignore
492
529
  }
493
530
  }
494
- let message = 'FlowFuse Assistant request was unsuccessful'
531
+ let message = 'FlowFuse Expert request was unsuccessful'
495
532
  const errorData = { status: 'error', message, body }
496
533
  const errorCode = (error.response && error.response.statusCode) || 500
497
534
  res.status(errorCode).json(errorData)
@@ -511,7 +548,10 @@ class Assistant {
511
548
  */
512
549
  async handlePostFimRequest (req, res) {
513
550
  if (!this.isInitialized || this.isLoading) {
514
- return res.status(503).send('Assistant is not ready')
551
+ return res.status(503).send('Expert is not ready')
552
+ }
553
+ if (this.options.completions?.inlineEnabled !== true) {
554
+ return res.status(400).send('Inline completions are not enabled')
515
555
  }
516
556
 
517
557
  const nodeModule = req.params.nodeModule
@@ -544,7 +584,7 @@ class Assistant {
544
584
  headers: {
545
585
  Accept: '*/*',
546
586
  'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,es;q=0.7',
547
- Authorization: `Bearer ${this.options.token}`,
587
+ Authorization: `Bearer ${auth.getUserToken()}`,
548
588
  'Content-Type': 'application/json',
549
589
  'User-Agent': FF_ASSISTANT_USER_AGENT
550
590
  },
@@ -557,7 +597,7 @@ class Assistant {
557
597
  })
558
598
  }).catch((_error) => {
559
599
  // fim requests are inline completion opportunities - lets not complain if they fail
560
- const message = 'FlowFuse Assistant FIM request was unsuccessful'
600
+ const message = 'FlowFuse Expert FIM request was unsuccessful'
561
601
  this.RED.log.trace(message, _error)
562
602
  })
563
603
  }
@@ -636,7 +676,7 @@ class Assistant {
636
676
  headers: {
637
677
  Accept: '*/*',
638
678
  'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,es;q=0.7',
639
- Authorization: `Bearer ${this.options.token}`,
679
+ Authorization: `Bearer ${auth.getUserToken()}`,
640
680
  'Content-Type': 'application/json'
641
681
  },
642
682
  json: body
@@ -0,0 +1,277 @@
1
+ const crypto = require('node:crypto')
2
+ const path = require('node:path')
3
+ const { readFile, writeFile } = require('node:fs/promises')
4
+ const { existsSync } = require('node:fs')
5
+
6
+ const base64url = require('base64url')
7
+ const store = require('./store')
8
+ const got = require('got').default
9
+ let baseURL = 'https://app.flowfuse.com'
10
+ const authorizationURL = () => `${baseURL}/account/authorize`
11
+ const tokenURL = () => `${baseURL}/account/token`
12
+
13
+ let activeTokens = { }
14
+ const activeTimers = { }
15
+ let assistant = null
16
+ let tokenFile
17
+ let RED
18
+
19
+ /**
20
+ * Initialise the auth handling for standalone FF Assistant mode
21
+ * @param {*} _RED
22
+ */
23
+ async function init (_RED) {
24
+ RED = _RED
25
+ baseURL = RED.settings.flowfuse?.assistant?.url || 'https://app.flowfuse.com'
26
+ tokenFile = path.join(RED.settings.userDir, '.flowfuse-assistant.json')
27
+ if (existsSync(tokenFile)) {
28
+ try {
29
+ const data = await readFile(tokenFile, 'utf8')
30
+ if (data) {
31
+ activeTokens = JSON.parse(data)
32
+ // We currently only support a single user '_'
33
+ const token = activeTokens._
34
+ if (!token || token.expires_at < Date.now()) {
35
+ RED.log.info('FlowFuse Assistant: access has expired, please log in again')
36
+ await deleteUserToken('_')
37
+ } else {
38
+ setupRefreshTimer('_')
39
+ }
40
+ }
41
+ } catch (err) {
42
+ RED.log.error('FlowFuse Assistant: Failed to load access tokens')
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Write tokens to file
49
+ */
50
+ async function saveTokens () {
51
+ await writeFile(tokenFile, JSON.stringify(activeTokens), 'utf8')
52
+ }
53
+
54
+ /**
55
+ * Update a user token, setup refresh timer and save to file
56
+ * @param {*} user
57
+ * @param {*} token
58
+ */
59
+ async function setUserToken (user, token) {
60
+ activeTokens[user] = token
61
+ setupRefreshTimer(user)
62
+ await saveTokens()
63
+ }
64
+
65
+ /**
66
+ * Setup the refresh timer for the token. User tokens have a relatively short expiry time, but can be refreshed before expiry
67
+ * to provide a longer session.
68
+ * @param {*} user
69
+ */
70
+ function setupRefreshTimer (user) {
71
+ const token = activeTokens[user]
72
+ if (!token.expires_at) {
73
+ token.expires_at = Date.now() + (token.expires_in * 1000)
74
+ }
75
+ const refreshInterval = token.expires_at - Date.now() - 10000 // refresh with 10 secs to spare
76
+ if (refreshInterval > 0) {
77
+ activeTimers[user] = setTimeout(async () => {
78
+ try {
79
+ const newTokens = await refreshToken(token)
80
+ newTokens.expires_at = Date.now() + (newTokens.expires_in * 1000)
81
+ await setUserToken(user, newTokens)
82
+ } catch (err) {
83
+ // Failed to refresh token - remove it and disable the agent
84
+ await deleteUserToken(user)
85
+ assistant.RED.log.warn('Failed to refresh FlowFuse Assistant access token')
86
+ reinitialiseAssistant()
87
+ }
88
+ }, refreshInterval)
89
+ }
90
+ }
91
+
92
+ /**
93
+ * When running in a managed FF environment, the tokens are provided statically via settings.
94
+ * This is used to insert the token into the auth table so we have a single place to retrieve
95
+ * tokens from.
96
+ * @param {*} token
97
+ */
98
+ function setStaticToken (token) {
99
+ // A token provided by settings file - does not need refresh logic
100
+ activeTokens._ = {
101
+ access_token: token
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get the current user token
107
+ */
108
+ function getUserToken () {
109
+ // Only single shared user supported currently
110
+ return activeTokens._?.access_token
111
+ }
112
+
113
+ /**
114
+ * Delete a users token, clears up refresh timers and updates saved tokens
115
+ * @param {*} user
116
+ */
117
+ async function deleteUserToken (user) {
118
+ const token = activeTokens[user]
119
+ if (token) {
120
+ clearTimeout(activeTimers[user])
121
+ }
122
+ delete activeTokens[user]
123
+ delete activeTimers[user]
124
+ saveTokens()
125
+ }
126
+
127
+ /**
128
+ * Make an API request to refresh the user token
129
+ * @param {*} token
130
+ */
131
+ async function refreshToken (token) {
132
+ const params = {
133
+ grant_type: 'refresh_token',
134
+ client_id: 'ff-plugin',
135
+ refresh_token: token.refresh_token
136
+ }
137
+ return got.post(tokenURL(), {
138
+ headers: {
139
+ 'Content-Type': 'application/json'
140
+ },
141
+ json: params
142
+ }).then(async (result) => {
143
+ return JSON.parse(result.body)
144
+ })
145
+ }
146
+
147
+ function setupRoutes (_assistant, RED) {
148
+ assistant = _assistant
149
+ RED.httpAdmin.get('/nr-assistant/auth/authorize', (request, response) => {
150
+ const existingRequest = store.getRequest(request.query.s)
151
+ if (!existingRequest) {
152
+ return response.send(404)
153
+ }
154
+ const verifier = base64url(crypto.randomBytes(32))
155
+ const scope = 'ff-assistant'
156
+ store.storeRequest({ ...existingRequest, verifier, scope })
157
+ const params = {}
158
+ params.client_id = 'ff-plugin'
159
+ params.scope = scope
160
+ params.response_type = 'code'
161
+ params.state = existingRequest.state
162
+ params.code_challenge = base64url(crypto.createHash('sha256').update(verifier).digest())
163
+ params.code_challenge_method = 'S256'
164
+ params.redirect_uri = existingRequest.redirect_uri
165
+ const authURL = new URL(authorizationURL())
166
+ authURL.search = new URLSearchParams(params)
167
+ response.redirect(authURL.toString())
168
+ })
169
+
170
+ RED.httpAdmin.get('/nr-assistant/auth/callback', async (request, response) => {
171
+ if (request.query.error) {
172
+ const postMessage = JSON.stringify({ code: 'flowfuse-auth-error', error: request.query.error, message: request.query.errorDescription })
173
+ response.send(`
174
+ <html><head>
175
+ <script>
176
+ if (window.opener) {
177
+ window.opener.postMessage('${postMessage}', '*')
178
+ window.close()
179
+ }
180
+ </script>
181
+ </head><body>Failed to complete authentication.</body></html>
182
+ `)
183
+ return
184
+ }
185
+ if (!request.query.code || !request.query.state) {
186
+ response.send('Failed to complete authentication')
187
+ return
188
+ }
189
+ const originalRequest = store.getRequest(request.query.state)
190
+ if (!originalRequest) {
191
+ response.send('Failed to complete authentication - unknown state')
192
+ return
193
+ }
194
+
195
+ const params = {}
196
+ params.grant_type = 'authorization_code'
197
+ params.code = request.query.code
198
+ params.redirect_uri = originalRequest.redirect_uri
199
+ params.client_id = 'ff-plugin'
200
+ params.code_verifier = originalRequest.verifier
201
+
202
+ got.post(tokenURL(), {
203
+ headers: {
204
+ 'Content-Type': 'application/json'
205
+ },
206
+ json: params
207
+ }).then(async (result) => {
208
+ const tokens = JSON.parse(result.body)
209
+ await setUserToken(originalRequest.user, tokens)
210
+ const postMessage = JSON.stringify({ code: 'flowfuse-auth-complete', state: originalRequest.state })
211
+ response.send(`
212
+ <html><head>
213
+ <script>
214
+ if (window.opener) {
215
+ window.opener.postMessage('${postMessage}', '*')
216
+ window.close()
217
+ }
218
+ </script>
219
+ </head><body>Success! You're connected to FlowFuse. You can now close this window to continue.</body></html>
220
+ `)
221
+ // Now we have a token for the user, reinitialise the assistant to enable it
222
+ reinitialiseAssistant()
223
+ }).catch((error) => {
224
+ console.warn('request failed', error)
225
+ })
226
+ })
227
+
228
+ RED.httpAdmin.use('/nr-assistant/*', RED.auth.needsPermission('flowfuse.write'))
229
+
230
+ RED.httpAdmin.post('/nr-assistant/auth/start', async (request, response) => {
231
+ // This request is made from the editor, so will have the Node-RED user attached.
232
+ // Generate the login url for the auth pop-up window
233
+ // if (request.body.forgeURL) {
234
+ // request.body.forgeURL = request.body.forgeURL.replace(/\/$/, '')
235
+ // settings.set('forgeURL', request.body.forgeURL)
236
+ // }
237
+
238
+ // Ping the server to check it is responsive and looks like a valid FF endpoint
239
+ got.get(`${baseURL}/api/v1/settings`).then(result => {
240
+ const state = base64url(crypto.randomBytes(16))
241
+ const redirect = request.body.editorURL + (request.body.editorURL.endsWith('/') ? '' : '/') + 'nr-assistant/auth/callback'
242
+ store.storeRequest({
243
+ user: '_',
244
+ state,
245
+ redirect_uri: redirect
246
+ })
247
+ const authPath = 'nr-assistant/auth/authorize?s=' + state
248
+ response.send({ path: authPath, state })
249
+ }).catch(err => {
250
+ RED.log.error(`[nr-assistant] Failed to connect to server: ${err.toString()}`)
251
+ response.send({ error: err.toString(), code: 'connect_failed' })
252
+ })
253
+ })
254
+ RED.httpAdmin.post('/nr-assistant/auth/disconnect', async (request, response) => {
255
+ deleteUserToken('_')
256
+ response.send({ })
257
+ })
258
+ }
259
+
260
+ async function reinitialiseAssistant () {
261
+ if (assistant && assistant.RED) {
262
+ const newSettings = await require('../settings').getSettings(assistant.RED)
263
+ assistant.init(assistant.RED, newSettings).then(() => {
264
+ // All good, the assistant is initialized.
265
+ // Any info messages made during initialization are logged in the assistant module
266
+ }).catch((error) => {
267
+ console.error(error)
268
+ assistant.RED.log.error('Failed to initialize FlowFuse Assistant Plugin:', error)
269
+ })
270
+ }
271
+ }
272
+ module.exports = {
273
+ init,
274
+ setupRoutes,
275
+ getUserToken,
276
+ setStaticToken
277
+ }
@@ -0,0 +1,33 @@
1
+ const pendingRequests = {}
2
+
3
+ function storeRequest (request) {
4
+ pruneRequests()
5
+ request.ttl = Date.now() + 1000 * 60 * 5 // 5 minute ttl
6
+ pendingRequests[request.state] = request
7
+ }
8
+ function getRequest (state) {
9
+ pruneRequests()
10
+ const result = pendingRequests[state]
11
+ delete pendingRequests[state]
12
+ return result
13
+ }
14
+
15
+ function deleteRequest (state) {
16
+ pruneRequests()
17
+ delete pendingRequests[state]
18
+ }
19
+ function pruneRequests () {
20
+ const now = Date.now()
21
+ for (const [key, value] of Object.entries(pendingRequests)) {
22
+ if (value.ttl < now) {
23
+ delete pendingRequests[key]
24
+ }
25
+ }
26
+ }
27
+
28
+ module.exports = {
29
+ getRequest,
30
+ storeRequest,
31
+ deleteRequest,
32
+ export: () => { return { ...pendingRequests } }
33
+ }
package/lib/settings.js CHANGED
@@ -1,3 +1,5 @@
1
+ const auth = require('./auth/index.js')
2
+
1
3
  /**
2
4
  * @typedef {Object} AssistantSettings
3
5
  * @property {boolean} enabled - Whether the Assistant is enabled
@@ -20,26 +22,61 @@ module.exports = {
20
22
  * @param {Object} RED - The RED instance
21
23
  * @returns {AssistantSettings} - The Assistant settings
22
24
  */
23
- getSettings: (RED) => {
24
- const assistantSettings = (RED.settings.flowforge && RED.settings.flowforge.assistant) || {}
25
- if (assistantSettings.enabled !== true) {
26
- assistantSettings.enabled = false
27
- assistantSettings.completions = null // if the assistant is not enabled, completions should not be enabled
28
- }
29
- assistantSettings.mcp = assistantSettings.mcp || {
30
- enabled: true // default to enabled
31
- }
32
- assistantSettings.completions = assistantSettings.completions || {
33
- enabled: true, // default to enabled
34
- modelUrl: null,
35
- vocabularyUrl: null
36
- }
37
- assistantSettings.tables = {
38
- enabled: !!(RED.settings.flowforge?.tables?.token) // for MVP, use the presence of a token is an indicator that tables are enabled
39
- }
40
- assistantSettings.inlineCompletions = {
41
- enabled: !!assistantSettings.completions.inlineEnabled
25
+ getSettings: async (RED) => {
26
+ if (RED.settings.flowforge?.assistant === undefined) {
27
+ // No settings provided via settings.js; enable standalone mode
28
+ const token = auth.getUserToken()
29
+ if (token) {
30
+ // There is a locally stored token, so enable the assistant with default settings
31
+ const baseURL = RED.settings.flowfuse?.assistant?.url || 'https://app.flowfuse.com'
32
+ return {
33
+ url: baseURL + '/api/v1/assistant/',
34
+ enabled: true,
35
+ standalone: true,
36
+ mcp: { enabled: true },
37
+ completions: {
38
+ enabled: true,
39
+ // TODO: what should these be?
40
+ modelUrl: null,
41
+ vocabularyUrl: null,
42
+ inlineEnabled: true
43
+ },
44
+ // Tables is not available outside of FF
45
+ tables: { enabled: false },
46
+ inlineCompletions: { enabled: true }
47
+ }
48
+ } else {
49
+ // Not token, disable the assistant but in standalone mode
50
+ return {
51
+ enabled: false,
52
+ standalone: true
53
+ }
54
+ }
55
+ } else {
56
+ const assistantSettings = RED.settings.flowforge?.assistant || {}
57
+ if (assistantSettings.enabled !== true) {
58
+ assistantSettings.enabled = false
59
+ assistantSettings.completions = null // if the assistant is not enabled, completions should not be enabled
60
+ }
61
+ assistantSettings.mcp = assistantSettings.mcp || {
62
+ enabled: true // default to enabled
63
+ }
64
+ assistantSettings.completions = assistantSettings.completions || {
65
+ enabled: true, // default to enabled
66
+ modelUrl: null,
67
+ vocabularyUrl: null
68
+ }
69
+ assistantSettings.tables = {
70
+ enabled: !!(RED.settings.flowforge?.tables?.token) // for MVP, use the presence of a token is an indicator that tables are enabled
71
+ }
72
+ assistantSettings.inlineCompletions = {
73
+ enabled: !!assistantSettings.completions.inlineEnabled
74
+ }
75
+
76
+ if (assistantSettings.token) {
77
+ auth.setStaticToken(assistantSettings.token)
78
+ }
79
+ return assistantSettings
42
80
  }
43
- return assistantSettings
44
81
  }
45
82
  }