botversion-sdk 1.0.4 → 1.0.5

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/cli/writer.js CHANGED
@@ -30,6 +30,11 @@ function injectBeforeListen(filePath, codeToInject, appVarName) {
30
30
  const lines = content.split("\n");
31
31
  const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
32
32
 
33
+ // Check if BotVersion already exists in file
34
+ if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
35
+ return { success: false, reason: "already_exists" };
36
+ }
37
+
33
38
  let listenLineIndex = -1;
34
39
  for (let i = 0; i < lines.length; i++) {
35
40
  if (listenRegex.test(lines[i])) {
@@ -39,7 +44,6 @@ function injectBeforeListen(filePath, codeToInject, appVarName) {
39
44
  }
40
45
 
41
46
  if (listenLineIndex === -1) {
42
- // app.listen not found — append to end of file
43
47
  return {
44
48
  success: false,
45
49
  reason: "no_listen",
@@ -47,22 +51,64 @@ function injectBeforeListen(filePath, codeToInject, appVarName) {
47
51
  };
48
52
  }
49
53
 
50
- // Check if BotVersion already exists in file
51
- if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
52
- return { success: false, reason: "already_exists" };
53
- }
54
+ // ── NEW: Check if app.listen() is inside if (require.main === module) ──
55
+ // If so, inject before that if block instead of inside it
56
+ let insertIndex = listenLineIndex;
57
+
58
+ for (let i = listenLineIndex; i >= 0; i--) {
59
+ const trimmed = lines[i].trim();
60
+
61
+ // Found if (require.main === module) block
62
+ if (
63
+ /if\s*\(\s*require\.main\s*===?\s*module\s*\)/.test(trimmed) ||
64
+ /if\s*\(\s*module\s*===?\s*require\.main\s*\)/.test(trimmed)
65
+ ) {
66
+ insertIndex = i;
67
+ // Keep walking back to check for 404/error handlers before this
68
+ continue;
69
+ }
54
70
 
55
- // Insert the code block before app.listen()
56
- const before = lines.slice(0, listenLineIndex);
57
- const after = lines.slice(listenLineIndex);
71
+ // Found a 404 handler — inject before this instead
72
+ if (
73
+ /app\.use\s*\(\s*\(req,\s*res\)\s*=>/.test(trimmed) ||
74
+ /app\.use\s*\(\s*function\s*\(req,\s*res\)/.test(trimmed)
75
+ ) {
76
+ insertIndex = i;
77
+ continue;
78
+ }
58
79
 
59
- const injectedLines = ["", ...codeToInject.split("\n"), ""];
80
+ // Found a global error handler — inject before this instead
81
+ if (
82
+ /app\.use\s*\(\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed) ||
83
+ /app\.use\s*\(\s*function\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed)
84
+ ) {
85
+ insertIndex = i;
86
+ continue;
87
+ }
60
88
 
89
+ // Stop walking back if we hit a top-level non-empty statement
90
+ if (
91
+ i < listenLineIndex &&
92
+ trimmed !== "" &&
93
+ trimmed !== "}" &&
94
+ trimmed !== "})" &&
95
+ trimmed !== "});" &&
96
+ lines[i].match(/^[^\s]/)
97
+ ) {
98
+ break;
99
+ }
100
+ }
101
+
102
+ // Insert the code block at the correct position
103
+ const before = lines.slice(0, insertIndex);
104
+ const after = lines.slice(insertIndex);
105
+ const injectedLines = ["", ...codeToInject.split("\n"), ""];
61
106
  const newContent = [...before, ...injectedLines, ...after].join("\n");
107
+
62
108
  const backup = backupFile(filePath);
63
109
  fs.writeFileSync(filePath, newContent, "utf8");
64
110
 
65
- return { success: true, lineNumber: listenLineIndex + 1, backup };
111
+ return { success: true, lineNumber: insertIndex + 1, backup };
66
112
  }
67
113
 
68
114
  // ─── APPEND CODE TO END OF FILE ──────────────────────────────────────────────
@@ -175,30 +221,6 @@ function injectBeforeExport(filePath, codeToInject, appVarName) {
175
221
  return { success: true, backup };
176
222
  }
177
223
 
178
- // ─── MERGE INTO EXISTING MIDDLEWARE (Next.js) ────────────────────────────────
179
-
180
- function mergeIntoMiddleware(middlewarePath, authName) {
181
- const content = fs.readFileSync(middlewarePath, "utf8");
182
-
183
- // If BotVersion already there, skip
184
- if (content.includes("botversion")) {
185
- return { success: false, reason: "already_exists" };
186
- }
187
-
188
- // We don't auto-modify complex middleware — too risky
189
- // Instead return a comment to add manually
190
- const comment = `
191
- // BotVersion: No changes needed to middleware.
192
- // Your auth (${authName}) is handled in pages/api/botversion/chat.js
193
- `;
194
-
195
- return {
196
- success: true,
197
- noModification: true,
198
- note: comment.trim(),
199
- };
200
- }
201
-
202
224
  // ─── WRITE SUMMARY OF ALL CHANGES ────────────────────────────────────────────
203
225
 
204
226
  function writeSummary(changes) {
@@ -212,25 +234,25 @@ function writeSummary(changes) {
212
234
 
213
235
  if (changes.modified && changes.modified.length > 0) {
214
236
  lines.push(" Modified files:");
215
- changes.modified.forEach((f) => lines.push(` ✏️ ${f}`));
237
+ changes.modified.forEach((f) => lines.push(` [Modified] ${f}`));
216
238
  lines.push("");
217
239
  }
218
240
 
219
241
  if (changes.created && changes.created.length > 0) {
220
242
  lines.push(" Created files:");
221
- changes.created.forEach((f) => lines.push(` ✅ ${f}`));
243
+ changes.created.forEach((f) => lines.push(` [Created] ${f}`));
222
244
  lines.push("");
223
245
  }
224
246
 
225
247
  if (changes.backups && changes.backups.length > 0) {
226
248
  lines.push(" Backups created:");
227
- changes.backups.forEach((f) => lines.push(` 💾 ${f}`));
249
+ changes.backups.forEach((f) => lines.push(` [Backup] ${f}`));
228
250
  lines.push("");
229
251
  }
230
252
 
231
253
  if (changes.manual && changes.manual.length > 0) {
232
- lines.push(" ⚠️ Manual steps needed:");
233
- changes.manual.forEach((m) => lines.push(` → ${m}`));
254
+ lines.push(" Manual steps needed:");
255
+ changes.manual.forEach((m) => lines.push(` [Manual] ${m}`));
234
256
  lines.push("");
235
257
  }
236
258
 
@@ -305,7 +327,6 @@ function injectIntoNextApp(filePath, content, scriptTag, backup) {
305
327
  data-api-url="${extractAttr(scriptTag, "data-api-url")}"
306
328
  data-project-id="${extractAttr(scriptTag, "data-project-id")}"
307
329
  data-public-key="${extractAttr(scriptTag, "data-public-key")}"
308
- data-proxy-url="/api/botversion/chat"
309
330
  strategy="afterInteractive"
310
331
  />`;
311
332
 
@@ -393,7 +414,6 @@ function injectIntoNextLayout(filePath, content, scriptTag, backup) {
393
414
  data-api-url="${extractAttr(scriptTag, "data-api-url")}"
394
415
  data-project-id="${extractAttr(scriptTag, "data-project-id")}"
395
416
  data-public-key="${extractAttr(scriptTag, "data-public-key")}"
396
- data-proxy-url="/api/botversion/chat"
397
417
  strategy="afterInteractive"
398
418
  />`;
399
419
 
@@ -415,6 +435,19 @@ function injectIntoNextLayout(filePath, content, scriptTag, backup) {
415
435
  return { success: true, backup };
416
436
  }
417
437
 
438
+ function injectAtTop(filePath, codeToInject) {
439
+ const content = fs.readFileSync(filePath, "utf8");
440
+
441
+ if (content.includes("dotenv")) {
442
+ return { success: false, reason: "already_exists" };
443
+ }
444
+
445
+ const newContent = codeToInject + "\n" + content;
446
+ backupFile(filePath);
447
+ fs.writeFileSync(filePath, newContent, "utf8");
448
+ return { success: true };
449
+ }
450
+
418
451
  // ─── HELPER: extract attribute value from script tag string ──────────────────
419
452
 
420
453
  function extractAttr(scriptTag, attr) {
@@ -422,14 +455,57 @@ function extractAttr(scriptTag, attr) {
422
455
  return match ? match[1] : "";
423
456
  }
424
457
 
458
+ // ─── WRITE API KEY TO .ENV FILE ───────────────────────────────────────────────
459
+
460
+ function writeEnvKey(cwd, key, value, framework) {
461
+ const envCandidates =
462
+ framework === "next"
463
+ ? [".env.local", ".env", ".env.development", ".env.development.local"]
464
+ : [".env", ".env.development", ".env.local"];
465
+
466
+ let envPath = null;
467
+
468
+ // Use existing .env file if found
469
+ for (const candidate of envCandidates) {
470
+ const fullPath = path.join(cwd, candidate);
471
+ if (fs.existsSync(fullPath)) {
472
+ envPath = fullPath;
473
+ break;
474
+ }
475
+ }
476
+
477
+ // No .env file found — create .env
478
+ if (!envPath) {
479
+ envPath = path.join(cwd, ".env");
480
+ fs.writeFileSync(envPath, "", "utf8");
481
+ }
482
+
483
+ const content = fs.readFileSync(envPath, "utf8");
484
+
485
+ // Key already exists — skip
486
+ if (content.includes(key)) {
487
+ return { success: false, reason: "already_exists", path: envPath };
488
+ }
489
+
490
+ // Append key to file
491
+ const newLine =
492
+ content === "" || content.endsWith("\n")
493
+ ? `${key}=${value}\n`
494
+ : `\n${key}=${value}\n`;
495
+
496
+ fs.appendFileSync(envPath, newLine, "utf8");
497
+ return { success: true, path: envPath };
498
+ }
499
+
425
500
  module.exports = {
426
501
  writeFile,
427
502
  backupFile,
428
503
  injectBeforeListen,
429
504
  appendToFile,
430
505
  createFile,
431
- mergeIntoMiddleware,
432
506
  writeSummary,
433
507
  injectBeforeExport,
434
508
  injectScriptTag,
509
+ injectAtTop,
510
+ writeEnvKey,
435
511
  };
package/client.js CHANGED
@@ -11,22 +11,33 @@ const url = require("url");
11
11
  */
12
12
  function BotVersionClient(options) {
13
13
  this.apiKey = options.apiKey;
14
- this.chatbotId = options.chatbotId;
15
- this.platformUrl =
16
- options.platformUrl || "https://chatbusiness-two.vercel.app";
14
+ this.platformUrl = options.platformUrl || "http://localhost:3000";
17
15
  this.debug = options.debug || false;
18
16
  this.timeout = options.timeout || 30000;
19
17
 
20
18
  // Batch queue for endpoint registration
21
19
  this._queue = [];
22
20
  this._flushTimer = null;
23
- this._flushDelay = options.flushDelay || 3000; // batch every 3 seconds
21
+ this._flushDelay = options.flushDelay || 3000;
22
+ this._ws = null;
23
+ this._executor = null;
24
24
  var self = this;
25
25
  process.on("beforeExit", function () {
26
26
  if (self._queue.length > 0) self._flush();
27
27
  });
28
28
  }
29
29
 
30
+ /**
31
+ * Register the executor function — called by interceptor
32
+ * So WebSocket can forward calls to user's backend
33
+ */
34
+ BotVersionClient.prototype.setExecutor = function (executorFn) {
35
+ this._executor = executorFn;
36
+ if (this.debug) {
37
+ console.log("[BotVersion SDK] ✅ Executor registered");
38
+ }
39
+ };
40
+
30
41
  /**
31
42
  * Register multiple endpoints at once (batched)
32
43
  */
@@ -43,10 +54,8 @@ BotVersionClient.prototype.registerEndpoints = function (endpoints) {
43
54
  );
44
55
  }
45
56
 
46
- // Add to queue
47
57
  self._queue = self._queue.concat(endpoints);
48
58
 
49
- // Schedule flush
50
59
  if (!self._flushTimer) {
51
60
  self._flushTimer = setTimeout(function () {
52
61
  self._flush();
@@ -56,6 +65,37 @@ BotVersionClient.prototype.registerEndpoints = function (endpoints) {
56
65
  return Promise.resolve();
57
66
  };
58
67
 
68
+ /**
69
+ * Register endpoints immediately — no batching
70
+ * Used during initial scan
71
+ */
72
+ BotVersionClient.prototype.registerEndpointsNow = function (endpoints) {
73
+ var self = this;
74
+ if (!endpoints || endpoints.length === 0) return Promise.resolve();
75
+
76
+ return self
77
+ ._post("/api/sdk/register-endpoints", {
78
+ workspaceKey: self.apiKey,
79
+ endpoints: endpoints,
80
+ })
81
+ .then(function (data) {
82
+ if (self.debug) {
83
+ console.log(
84
+ "[BotVersion SDK] ✅ Registered",
85
+ endpoints.length,
86
+ "endpoints immediately",
87
+ );
88
+ }
89
+ return data;
90
+ })
91
+ .catch(function (err) {
92
+ console.warn(
93
+ "[BotVersion SDK] ⚠ Failed to register endpoints:",
94
+ err.message,
95
+ );
96
+ });
97
+ };
98
+
59
99
  /**
60
100
  * Flush the queue — send all batched endpoints at once
61
101
  */
@@ -116,6 +156,33 @@ BotVersionClient.prototype.updateEndpoint = function (endpoint) {
116
156
  });
117
157
  };
118
158
 
159
+ BotVersionClient.prototype.registerRoutePatterns = function (patterns) {
160
+ var self = this;
161
+ if (!patterns || patterns.length === 0) return Promise.resolve();
162
+
163
+ if (self.debug) {
164
+ console.log(
165
+ "[BotVersion SDK] Sending",
166
+ patterns.length,
167
+ "route patterns to platform",
168
+ );
169
+ }
170
+
171
+ return self
172
+ ._post("/api/sdk/register-route-patterns", {
173
+ workspaceKey: self.apiKey,
174
+ patterns: patterns,
175
+ })
176
+ .catch(function (err) {
177
+ if (self.debug) {
178
+ console.warn(
179
+ "[BotVersion SDK] Failed to register route patterns:",
180
+ err.message,
181
+ );
182
+ }
183
+ });
184
+ };
185
+
119
186
  /**
120
187
  * Get all registered endpoints for this workspace
121
188
  */
@@ -127,37 +194,6 @@ BotVersionClient.prototype.getEndpoints = function () {
127
194
  );
128
195
  };
129
196
 
130
- // Send user message to agent
131
- BotVersionClient.prototype.agentChat = function (payload) {
132
- console.log("[BotVersion] agentChat payload:", JSON.stringify(payload));
133
- console.log("[BotVersion] this.chatbotId:", this.chatbotId);
134
- console.log("[BotVersion] this.apiKey:", this.apiKey);
135
- return this._post(
136
- "/api/chatbot/widget-chat",
137
- Object.assign({
138
- chatbotId: payload.chatbotId, // ← use payload, not this.chatbotId
139
- publicKey: payload.publicKey,
140
- query: payload.message,
141
- previousChats: payload.conversationHistory,
142
- pageContext: payload.pageContext,
143
- userContext: payload.userContext,
144
- }),
145
- );
146
- };
147
-
148
- // Send API call result back to agent
149
- BotVersionClient.prototype.agentToolResult = function (
150
- sessionToken,
151
- result,
152
- sessionData,
153
- ) {
154
- return this._post("/api/sdk/agent-tool-result", {
155
- sessionToken,
156
- sessionData: sessionData || null,
157
- result,
158
- });
159
- };
160
-
161
197
  /**
162
198
  * Make a POST request to the platform
163
199
  */
@@ -177,7 +213,7 @@ BotVersionClient.prototype._post = function (path, data) {
177
213
  method: "POST",
178
214
  headers: {
179
215
  "Content-Type": "application/json",
180
- "Content-Length": Buffer.byteLength(body),
216
+ "Content-Length": String(Buffer.byteLength(body)),
181
217
  "X-BotVersion-SDK": "1.0.0",
182
218
  },
183
219
  timeout: self.timeout,
@@ -288,4 +324,145 @@ BotVersionClient.prototype._get = function (path) {
288
324
  });
289
325
  };
290
326
 
327
+ /**
328
+ * Connect to platform via WebSocket — establishes persistent connection
329
+ * Auto-reconnects every 5 seconds if connection drops
330
+ */
331
+ BotVersionClient.prototype.connect = function () {
332
+ var self = this;
333
+ // Run in next tick so it doesn't block initialization
334
+ setImmediate(function () {
335
+ self._wsLoop();
336
+ });
337
+ };
338
+
339
+ BotVersionClient.prototype._wsLoop = function () {
340
+ var self = this;
341
+
342
+ var wsUrl = self.platformUrl
343
+ .replace("https://", "wss://")
344
+ .replace("http://", "ws://")
345
+ .replace(":3000", ":3001");
346
+ wsUrl = wsUrl + "?apiKey=" + encodeURIComponent(self.apiKey);
347
+
348
+ var WebSocket;
349
+ try {
350
+ WebSocket = require("ws");
351
+ } catch (e) {
352
+ console.error(
353
+ "[BotVersion SDK] ❌ 'ws' package not installed. Run: npm install ws",
354
+ );
355
+ return;
356
+ }
357
+
358
+ function attempt() {
359
+ var ws = new WebSocket(wsUrl);
360
+ self._ws = ws;
361
+
362
+ ws.on("open", function () {
363
+ if (self.debug) {
364
+ console.log("[BotVersion SDK] ✅ WebSocket connected to platform");
365
+ }
366
+ // Identify this SDK to the platform
367
+ ws.send(
368
+ JSON.stringify({
369
+ type: "IDENTIFY",
370
+ apiKey: self.apiKey,
371
+ }),
372
+ );
373
+ });
374
+
375
+ ws.on("message", function (data) {
376
+ try {
377
+ var msg = JSON.parse(data.toString());
378
+
379
+ if (msg.type === "EXECUTE_CALL") {
380
+ self._handleExecuteCall(msg);
381
+ }
382
+ } catch (e) {
383
+ if (self.debug) {
384
+ console.warn("[BotVersion SDK] ⚠ Error parsing message:", e.message);
385
+ }
386
+ }
387
+ });
388
+
389
+ ws.on("error", function (err) {
390
+ if (self.debug) {
391
+ console.warn("[BotVersion SDK] ⚠ WebSocket error:", err.message);
392
+ }
393
+ });
394
+
395
+ ws.on("close", function () {
396
+ if (self.debug) {
397
+ console.log(
398
+ "[BotVersion SDK] WebSocket closed — reconnecting in 5s...",
399
+ );
400
+ }
401
+ self._ws = null;
402
+ // Auto-reconnect after 5 seconds
403
+ setTimeout(attempt, 5000);
404
+ });
405
+ }
406
+
407
+ attempt();
408
+ };
409
+
410
+ BotVersionClient.prototype._handleExecuteCall = function (data) {
411
+ var self = this;
412
+
413
+ var callId = data.callId;
414
+ var method = data.method;
415
+ var path = data.path;
416
+ var body = data.body;
417
+ var cookies = data.cookies || "";
418
+ var headers = data.headers || {};
419
+ var baseUrl = data.baseUrl || "http://127.0.0.1:3000";
420
+
421
+ if (!self._executor) {
422
+ self._sendCallResult(callId, {
423
+ status: 500,
424
+ ok: false,
425
+ data: { error: "No executor registered" },
426
+ });
427
+ return;
428
+ }
429
+
430
+ self
431
+ ._executor(method, path, body, cookies, headers, baseUrl)
432
+ .then(function (result) {
433
+ self._sendCallResult(callId, result);
434
+ })
435
+ .catch(function (err) {
436
+ self._sendCallResult(callId, {
437
+ status: 500,
438
+ ok: false,
439
+ data: { error: err.message },
440
+ });
441
+ });
442
+ };
443
+
444
+ BotVersionClient.prototype._sendCallResult = function (callId, result) {
445
+ var self = this;
446
+ try {
447
+ if (self._ws && self._ws.readyState === 1) {
448
+ // 1 = OPEN
449
+ self._ws.send(
450
+ JSON.stringify({
451
+ type: "CALL_RESULT",
452
+ callId: callId,
453
+ result: result,
454
+ }),
455
+ );
456
+ } else if (self.debug) {
457
+ console.warn(
458
+ "[BotVersion SDK] ⚠ WebSocket not open — could not send CALL_RESULT",
459
+ );
460
+ }
461
+ } catch (e) {
462
+ if (self.debug) {
463
+ console.warn("[BotVersion SDK] ⚠ Failed to send CALL_RESULT:", e.message);
464
+ }
465
+ }
466
+ };
467
+
291
468
  module.exports = BotVersionClient;