botversion-sdk 1.0.4 → 1.0.6

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/prompts.js CHANGED
@@ -89,53 +89,53 @@ async function promptEntryPoint() {
89
89
  return filePath;
90
90
  }
91
91
 
92
- async function promptAuthLibrary() {
93
- const choices = [
94
- {
95
- label: "next-auth v4",
96
- value: { name: "next-auth", version: "v4", supported: true },
97
- },
98
- {
99
- label: "next-auth v5 (Auth.js)",
100
- value: { name: "next-auth", version: "v5", supported: true },
101
- },
102
- { label: "Clerk", value: { name: "clerk", supported: true } },
103
- { label: "Supabase Auth", value: { name: "supabase", supported: true } },
104
- { label: "Passport.js", value: { name: "passport", supported: true } },
105
- { label: "JWT (jsonwebtoken)", value: { name: "jwt", supported: true } },
106
- {
107
- label: "express-session",
108
- value: { name: "express-session", supported: true },
109
- },
110
- { label: "Other / Custom", value: { name: "custom", supported: false } },
111
- { label: "No auth", value: { name: null, supported: false } },
112
- ];
113
-
114
- const choice = await askChoice(
115
- "We couldn't detect your auth library. Which one are you using?",
116
- choices,
117
- );
118
-
119
- return choice.value;
120
- }
121
-
122
- async function promptNextAuthConfigPath() {
123
- console.log(
124
- "\n ⚠️ We couldn't find your authOptions location automatically.",
125
- );
126
- const filePath = await ask(
127
- " Enter the path to your authOptions file (e.g. lib/auth.ts): ",
128
- );
129
- return filePath;
130
- }
131
-
132
92
  async function promptForce(conflictFile) {
133
93
  console.log(`\n ⚠️ File already exists: ${conflictFile}`);
134
94
  return confirm(" Overwrite it? (a backup will be created)", false);
135
95
  }
136
96
 
97
+ const CONFIG_FILES = [
98
+ "next.config.js",
99
+ "next.config.ts",
100
+ "vite.config.js",
101
+ "vite.config.ts",
102
+ "webpack.config.js",
103
+ "webpack.config.ts",
104
+ "nuxt.config.js",
105
+ "nuxt.config.ts",
106
+ ];
107
+
137
108
  async function promptMissingListenCall(entryPoint) {
109
+ const path = require("path");
110
+ const entryFileName = path.basename(entryPoint || "");
111
+ const isConfigFile = CONFIG_FILES.includes(entryFileName);
112
+
138
113
  console.log(`\n ⚠️ We couldn't find app.listen() in ${entryPoint}`);
114
+
115
+ if (isConfigFile) {
116
+ console.log(
117
+ `\n ❌ "${entryFileName}" is a config file, not a server file.`,
118
+ );
119
+ console.log(" Appending server code here would break your project.");
120
+ console.log(" Options:");
121
+
122
+ const choices = [
123
+ {
124
+ label: "Enter the correct server file path manually",
125
+ value: "manual_path",
126
+ },
127
+ { label: "Skip — I'll add it manually", value: "skip" },
128
+ ];
129
+
130
+ const choice = await askChoice("How would you like to proceed?", choices);
131
+ if (choice.value === "manual_path") {
132
+ const filePath = await ask(" Enter file path: ");
133
+ return { action: "manual_path", filePath };
134
+ }
135
+ return { action: choice.value };
136
+ }
137
+
138
+ // Normal flow — entry point is not a config file
139
139
  console.log(" Options:");
140
140
  const choices = [
141
141
  { label: "Append to end of file", value: "append" },
@@ -158,8 +158,6 @@ module.exports = {
158
158
  confirm,
159
159
  promptMonorepoPackage,
160
160
  promptEntryPoint,
161
- promptAuthLibrary,
162
- promptNextAuthConfigPath,
163
161
  promptForce,
164
162
  promptMissingListenCall,
165
163
  };
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,30 +234,30 @@ 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
 
237
259
  lines.push(" Next: Restart your server and test the chat widget.");
238
- lines.push(" Docs: https://docs.botversion.com");
260
+ lines.push(" Docs: https://botversion.com/docs");
239
261
  lines.push("");
240
262
 
241
263
  return lines.join("\n");
@@ -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,16 +11,14 @@ 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 || "https://botversion.com";
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;
24
22
  var self = this;
25
23
  process.on("beforeExit", function () {
26
24
  if (self._queue.length > 0) self._flush();
@@ -35,18 +33,8 @@ BotVersionClient.prototype.registerEndpoints = function (endpoints) {
35
33
 
36
34
  if (!endpoints || endpoints.length === 0) return Promise.resolve();
37
35
 
38
- if (self.debug) {
39
- console.log(
40
- "[BotVersion SDK] Queuing",
41
- endpoints.length,
42
- "endpoints for registration",
43
- );
44
- }
45
-
46
- // Add to queue
47
36
  self._queue = self._queue.concat(endpoints);
48
37
 
49
- // Schedule flush
50
38
  if (!self._flushTimer) {
51
39
  self._flushTimer = setTimeout(function () {
52
40
  self._flush();
@@ -56,6 +44,24 @@ BotVersionClient.prototype.registerEndpoints = function (endpoints) {
56
44
  return Promise.resolve();
57
45
  };
58
46
 
47
+ /**
48
+ * Register endpoints immediately — no batching
49
+ * Used during initial scan
50
+ */
51
+ BotVersionClient.prototype.registerEndpointsNow = function (endpoints) {
52
+ var self = this;
53
+ if (!endpoints || endpoints.length === 0) return Promise.resolve();
54
+
55
+ return self
56
+ ._post("/api/sdk/register-endpoints", {
57
+ workspaceKey: self.apiKey,
58
+ endpoints: endpoints,
59
+ })
60
+ .then(function (data) {
61
+ return data;
62
+ });
63
+ };
64
+
59
65
  /**
60
66
  * Flush the queue — send all batched endpoints at once
61
67
  */
@@ -68,35 +74,14 @@ BotVersionClient.prototype._flush = function () {
68
74
  var toSend = self._queue.slice();
69
75
  self._queue = [];
70
76
 
71
- if (self.debug) {
72
- console.log(
73
- "[BotVersion SDK] Flushing",
74
- toSend.length,
75
- "endpoints to platform",
76
- );
77
- }
78
-
79
77
  self
80
78
  ._post("/api/sdk/register-endpoints", {
81
79
  workspaceKey: self.apiKey,
82
80
  endpoints: toSend,
83
81
  })
84
- .then(function (data) {
85
- if (self.debug) {
86
- console.log(
87
- "[BotVersion SDK] Registered",
88
- data.succeeded,
89
- "endpoints successfully",
90
- );
91
- }
92
- })
82
+ .then(function (data) {})
93
83
  .catch(function (err) {
94
- if (self.debug) {
95
- console.warn(
96
- "[BotVersion SDK] Failed to register endpoints:",
97
- err.message,
98
- );
99
- }
84
+ console.error("[botversion] flush failed:", err.message);
100
85
  });
101
86
  };
102
87
 
@@ -116,6 +101,16 @@ BotVersionClient.prototype.updateEndpoint = function (endpoint) {
116
101
  });
117
102
  };
118
103
 
104
+ BotVersionClient.prototype.registerRoutePatterns = function (patterns) {
105
+ var self = this;
106
+ if (!patterns || patterns.length === 0) return Promise.resolve();
107
+
108
+ return self._post("/api/sdk/register-route-patterns", {
109
+ workspaceKey: self.apiKey,
110
+ patterns: patterns,
111
+ });
112
+ };
113
+
119
114
  /**
120
115
  * Get all registered endpoints for this workspace
121
116
  */
@@ -127,37 +122,6 @@ BotVersionClient.prototype.getEndpoints = function () {
127
122
  );
128
123
  };
129
124
 
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
125
  /**
162
126
  * Make a POST request to the platform
163
127
  */
@@ -177,7 +141,7 @@ BotVersionClient.prototype._post = function (path, data) {
177
141
  method: "POST",
178
142
  headers: {
179
143
  "Content-Type": "application/json",
180
- "Content-Length": Buffer.byteLength(body),
144
+ "Content-Length": String(Buffer.byteLength(body)),
181
145
  "X-BotVersion-SDK": "1.0.0",
182
146
  },
183
147
  timeout: self.timeout,