@yushaw/sanqian-sdk 0.2.9 → 0.2.11

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/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,289 +30,300 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- Conversation: () => Conversation,
34
- DiscoveryManager: () => DiscoveryManager,
35
- ErrorMessages: () => ErrorMessages,
36
- SANQIAN_WEBSITE: () => SANQIAN_WEBSITE,
37
- SDKErrorCode: () => SDKErrorCode,
38
- SanqianSDK: () => SanqianSDK,
39
- SanqianSDKError: () => SanqianSDKError,
40
- createSDKError: () => createSDKError
41
- });
42
- module.exports = __toCommonJS(index_exports);
43
-
44
- // src/client.ts
45
- var import_ws = __toESM(require("ws"));
46
-
47
33
  // src/discovery.ts
48
- var import_fs = require("fs");
49
- var import_os = require("os");
50
- var import_path = require("path");
51
- var import_child_process = require("child_process");
52
- var DiscoveryManager = class {
53
- connectionInfo = null;
54
- watcher = null;
55
- onChange = null;
56
- pollInterval = null;
57
- /**
58
- * Cached executable path from last successful connection.json read.
59
- * Persists even when Sanqian is not running, for use in auto-launch.
60
- */
61
- cachedExecutable = null;
62
- /**
63
- * Get the path to connection.json
64
- */
65
- getConnectionFilePath() {
66
- return (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime", "connection.json");
67
- }
68
- /**
69
- * Read and validate connection info
70
- *
71
- * Returns null if:
72
- * - File doesn't exist
73
- * - File is invalid JSON
74
- * - Process is not running
75
- */
76
- read() {
77
- const filePath = this.getConnectionFilePath();
78
- if (!(0, import_fs.existsSync)(filePath)) {
79
- return null;
80
- }
81
- try {
82
- const content = (0, import_fs.readFileSync)(filePath, "utf-8");
83
- const info = JSON.parse(content);
84
- if (!info.port || !info.token || !info.pid) {
85
- return null;
86
- }
87
- if (info.executable) {
88
- this.cachedExecutable = info.executable;
89
- }
90
- if (!this.isProcessRunning(info.pid)) {
91
- return null;
34
+ var discovery_exports = {};
35
+ __export(discovery_exports, {
36
+ DiscoveryManager: () => DiscoveryManager
37
+ });
38
+ var import_fs, import_os, import_path, import_child_process, DiscoveryManager;
39
+ var init_discovery = __esm({
40
+ "src/discovery.ts"() {
41
+ "use strict";
42
+ import_fs = require("fs");
43
+ import_os = require("os");
44
+ import_path = require("path");
45
+ import_child_process = require("child_process");
46
+ DiscoveryManager = class {
47
+ connectionInfo = null;
48
+ watcher = null;
49
+ onChange = null;
50
+ pollInterval = null;
51
+ /**
52
+ * Cached executable path from last successful connection.json read.
53
+ * Persists even when Sanqian is not running, for use in auto-launch.
54
+ */
55
+ cachedExecutable = null;
56
+ /**
57
+ * Get the path to connection.json
58
+ */
59
+ getConnectionFilePath() {
60
+ return (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime", "connection.json");
92
61
  }
93
- this.connectionInfo = info;
94
- return info;
95
- } catch {
96
- return null;
97
- }
98
- }
99
- /**
100
- * Start watching for connection file changes
101
- */
102
- startWatching(onChange) {
103
- this.onChange = onChange;
104
- const dir = (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime");
105
- if (!(0, import_fs.existsSync)(dir)) {
106
- this.pollInterval = setInterval(() => {
107
- if ((0, import_fs.existsSync)(dir)) {
108
- if (this.pollInterval) {
109
- clearInterval(this.pollInterval);
110
- this.pollInterval = null;
62
+ /**
63
+ * Read and validate connection info
64
+ *
65
+ * Returns null if:
66
+ * - File doesn't exist
67
+ * - File is invalid JSON
68
+ * - Process is not running
69
+ */
70
+ read() {
71
+ const filePath = this.getConnectionFilePath();
72
+ if (!(0, import_fs.existsSync)(filePath)) {
73
+ return null;
74
+ }
75
+ try {
76
+ const content = (0, import_fs.readFileSync)(filePath, "utf-8");
77
+ const info = JSON.parse(content);
78
+ if (!info.port || !info.token || !info.pid) {
79
+ return null;
80
+ }
81
+ if (info.executable) {
82
+ this.cachedExecutable = info.executable;
111
83
  }
112
- this.setupWatcher(dir);
84
+ if (!this.isProcessRunning(info.pid)) {
85
+ return null;
86
+ }
87
+ this.connectionInfo = info;
88
+ return info;
89
+ } catch {
90
+ return null;
113
91
  }
114
- }, 2e3);
115
- return;
116
- }
117
- this.setupWatcher(dir);
118
- }
119
- setupWatcher(dir) {
120
- try {
121
- this.watcher = (0, import_fs.watch)(dir, (event, filename) => {
122
- if (filename === "connection.json") {
123
- setTimeout(() => {
124
- const newInfo = this.read();
125
- const oldInfoStr = JSON.stringify(this.connectionInfo);
126
- const newInfoStr = JSON.stringify(newInfo);
127
- if (oldInfoStr !== newInfoStr) {
128
- this.connectionInfo = newInfo;
129
- this.onChange?.(newInfo);
92
+ }
93
+ /**
94
+ * Start watching for connection file changes
95
+ */
96
+ startWatching(onChange) {
97
+ this.onChange = onChange;
98
+ const dir = (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime");
99
+ if (!(0, import_fs.existsSync)(dir)) {
100
+ this.pollInterval = setInterval(() => {
101
+ if ((0, import_fs.existsSync)(dir)) {
102
+ if (this.pollInterval) {
103
+ clearInterval(this.pollInterval);
104
+ this.pollInterval = null;
105
+ }
106
+ this.setupWatcher(dir);
130
107
  }
131
- }, 100);
108
+ }, 2e3);
109
+ return;
132
110
  }
133
- });
134
- } catch (e) {
135
- console.error("Failed to watch connection file:", e);
136
- }
137
- }
138
- /**
139
- * Stop watching
140
- */
141
- stopWatching() {
142
- if (this.pollInterval) {
143
- clearInterval(this.pollInterval);
144
- this.pollInterval = null;
145
- }
146
- this.watcher?.close();
147
- this.watcher = null;
148
- this.onChange = null;
149
- }
150
- /**
151
- * Check if a process is running by PID
152
- *
153
- * Cross-platform implementation:
154
- * - macOS/Linux: process.kill(pid, 0) works correctly
155
- * - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
156
- * so we use tasklist command for reliable checking
157
- */
158
- isProcessRunning(pid) {
159
- try {
160
- if ((0, import_os.platform)() === "win32") {
111
+ this.setupWatcher(dir);
112
+ }
113
+ setupWatcher(dir) {
161
114
  try {
162
- const result = (0, import_child_process.execSync)(`tasklist /FI "PID eq ${pid}" /NH`, {
163
- encoding: "utf-8",
164
- stdio: ["pipe", "pipe", "pipe"]
115
+ this.watcher = (0, import_fs.watch)(dir, (event, filename) => {
116
+ if (filename === "connection.json") {
117
+ setTimeout(() => {
118
+ const newInfo = this.read();
119
+ const oldInfoStr = JSON.stringify(this.connectionInfo);
120
+ const newInfoStr = JSON.stringify(newInfo);
121
+ if (oldInfoStr !== newInfoStr) {
122
+ this.connectionInfo = newInfo;
123
+ this.onChange?.(newInfo);
124
+ }
125
+ }, 100);
126
+ }
165
127
  });
166
- const trimmed = result.trim();
167
- if (!trimmed || trimmed.toUpperCase().startsWith("INFO")) {
168
- return false;
128
+ } catch (e) {
129
+ console.error("Failed to watch connection file:", e);
130
+ }
131
+ }
132
+ /**
133
+ * Stop watching
134
+ */
135
+ stopWatching() {
136
+ if (this.pollInterval) {
137
+ clearInterval(this.pollInterval);
138
+ this.pollInterval = null;
139
+ }
140
+ this.watcher?.close();
141
+ this.watcher = null;
142
+ this.onChange = null;
143
+ }
144
+ /**
145
+ * Check if a process is running by PID
146
+ *
147
+ * Cross-platform implementation:
148
+ * - macOS/Linux: process.kill(pid, 0) works correctly
149
+ * - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
150
+ * so we use tasklist command for reliable checking
151
+ */
152
+ isProcessRunning(pid) {
153
+ try {
154
+ if ((0, import_os.platform)() === "win32") {
155
+ try {
156
+ const result = (0, import_child_process.execSync)(`tasklist /FI "PID eq ${pid}" /NH`, {
157
+ encoding: "utf-8",
158
+ stdio: ["pipe", "pipe", "pipe"]
159
+ });
160
+ const trimmed = result.trim();
161
+ if (!trimmed || trimmed.toUpperCase().startsWith("INFO")) {
162
+ return false;
163
+ }
164
+ return new RegExp(`\\b${pid}\\b`).test(trimmed);
165
+ } catch {
166
+ return false;
167
+ }
168
+ } else {
169
+ process.kill(pid, 0);
170
+ return true;
169
171
  }
170
- return new RegExp(`\\b${pid}\\b`).test(trimmed);
171
172
  } catch {
172
173
  return false;
173
174
  }
174
- } else {
175
- process.kill(pid, 0);
176
- return true;
177
175
  }
178
- } catch {
179
- return false;
180
- }
181
- }
182
- /**
183
- * Get cached connection info (may be stale)
184
- */
185
- getCached() {
186
- return this.connectionInfo;
187
- }
188
- /**
189
- * Build WebSocket URL from connection info
190
- */
191
- buildWebSocketUrl(info) {
192
- return `ws://127.0.0.1:${info.port}${info.ws_path}?token=${info.token}`;
193
- }
194
- /**
195
- * Build HTTP base URL from connection info
196
- */
197
- buildHttpUrl(info) {
198
- return `http://127.0.0.1:${info.port}`;
199
- }
200
- /**
201
- * Find Sanqian executable path
202
- * Searches in standard installation locations for each platform
203
- */
204
- findSanqianPath(customPath) {
205
- if (customPath) {
206
- if ((0, import_fs.existsSync)(customPath)) {
207
- return customPath;
176
+ /**
177
+ * Get cached connection info (may be stale)
178
+ */
179
+ getCached() {
180
+ return this.connectionInfo;
208
181
  }
209
- console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
210
- return null;
211
- }
212
- const os = (0, import_os.platform)();
213
- const searchPaths = [];
214
- if (os === "darwin") {
215
- searchPaths.push(
216
- // Production: installed app
217
- "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
218
- (0, import_path.join)((0, import_os.homedir)(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
219
- // Development: electron-builder output
220
- (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
221
- (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
222
- );
223
- } else if (os === "win32") {
224
- const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
225
- const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
226
- const localAppData = process.env.LOCALAPPDATA || (0, import_path.join)((0, import_os.homedir)(), "AppData", "Local");
227
- searchPaths.push(
228
- // Production: NSIS installer uses lowercase directory name from package.json "name"
229
- (0, import_path.join)(localAppData, "Programs", "sanqian", "Sanqian.exe"),
230
- // Legacy/alternative paths with uppercase
231
- (0, import_path.join)(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
232
- (0, import_path.join)(programFiles, "Sanqian", "Sanqian.exe"),
233
- (0, import_path.join)(programFilesX86, "Sanqian", "Sanqian.exe"),
234
- // Development: electron-builder output
235
- (0, import_path.join)((0, import_os.homedir)(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
236
- );
237
- } else {
238
- searchPaths.push(
239
- "/usr/bin/sanqian",
240
- "/usr/local/bin/sanqian",
241
- (0, import_path.join)((0, import_os.homedir)(), ".local/bin/sanqian"),
242
- "/opt/Sanqian/sanqian",
243
- // Development
244
- (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/linux-unpacked/sanqian")
245
- );
246
- }
247
- for (const path of searchPaths) {
248
- if ((0, import_fs.existsSync)(path)) {
249
- return path;
182
+ /**
183
+ * Build WebSocket URL from connection info
184
+ */
185
+ buildWebSocketUrl(info) {
186
+ const wsPath = info.ws_path || "/ws/apps";
187
+ return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
250
188
  }
251
- }
252
- return null;
253
- }
254
- /**
255
- * Launch Sanqian in hidden/tray mode
256
- * Returns true if launch was initiated successfully
257
- *
258
- * Priority for finding Sanqian executable:
259
- * 1. customPath parameter (if provided)
260
- * 2. Cached executable from connection.json (most reliable)
261
- * 3. Search in standard installation locations (fallback)
262
- */
263
- launchSanqian(customPath) {
264
- let sanqianPath = null;
265
- if (customPath) {
266
- sanqianPath = this.findSanqianPath(customPath);
267
- } else if (this.cachedExecutable && (0, import_fs.existsSync)(this.cachedExecutable)) {
268
- sanqianPath = this.cachedExecutable;
269
- console.log(`[SDK] Using cached executable: ${sanqianPath}`);
270
- } else {
271
- sanqianPath = this.findSanqianPath();
272
- }
273
- if (!sanqianPath) {
274
- console.error("[SDK] Sanqian executable not found");
275
- return false;
276
- }
277
- console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
278
- try {
279
- const os = (0, import_os.platform)();
280
- if (os === "darwin") {
281
- const appPath = sanqianPath.replace(
282
- "/Contents/MacOS/Sanqian",
283
- ""
284
- );
285
- (0, import_child_process.spawn)("open", ["-g", "-a", appPath, "--args", "--hidden"], {
286
- detached: true,
287
- stdio: "ignore"
288
- }).unref();
289
- } else {
290
- (0, import_child_process.spawn)(sanqianPath, ["--hidden"], {
291
- detached: true,
292
- stdio: "ignore",
293
- // On Windows, hide the console window
294
- ...os === "win32" && {
295
- windowsHide: true,
296
- shell: false
189
+ /**
190
+ * Build HTTP base URL from connection info
191
+ */
192
+ buildHttpUrl(info) {
193
+ return `http://127.0.0.1:${info.port}`;
194
+ }
195
+ /**
196
+ * Find Sanqian executable path
197
+ * Searches in standard installation locations for each platform
198
+ */
199
+ findSanqianPath(customPath) {
200
+ if (customPath) {
201
+ if ((0, import_fs.existsSync)(customPath)) {
202
+ return customPath;
203
+ }
204
+ console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
205
+ return null;
206
+ }
207
+ const os = (0, import_os.platform)();
208
+ const searchPaths = [];
209
+ if (os === "darwin") {
210
+ searchPaths.push(
211
+ // Production: installed app
212
+ "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
213
+ (0, import_path.join)((0, import_os.homedir)(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
214
+ // Development: electron-builder output
215
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
216
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
217
+ );
218
+ } else if (os === "win32") {
219
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
220
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
221
+ const localAppData = process.env.LOCALAPPDATA || (0, import_path.join)((0, import_os.homedir)(), "AppData", "Local");
222
+ searchPaths.push(
223
+ // Production: NSIS installer uses lowercase directory name from package.json "name"
224
+ (0, import_path.join)(localAppData, "Programs", "sanqian", "Sanqian.exe"),
225
+ // Legacy/alternative paths with uppercase
226
+ (0, import_path.join)(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
227
+ (0, import_path.join)(programFiles, "Sanqian", "Sanqian.exe"),
228
+ (0, import_path.join)(programFilesX86, "Sanqian", "Sanqian.exe"),
229
+ // Development: electron-builder output
230
+ (0, import_path.join)((0, import_os.homedir)(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
231
+ );
232
+ } else {
233
+ searchPaths.push(
234
+ "/usr/bin/sanqian",
235
+ "/usr/local/bin/sanqian",
236
+ (0, import_path.join)((0, import_os.homedir)(), ".local/bin/sanqian"),
237
+ "/opt/Sanqian/sanqian",
238
+ // Development
239
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/linux-unpacked/sanqian")
240
+ );
241
+ }
242
+ for (const path of searchPaths) {
243
+ if ((0, import_fs.existsSync)(path)) {
244
+ return path;
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+ /**
250
+ * Launch Sanqian in hidden/tray mode
251
+ * Returns true if launch was initiated successfully
252
+ *
253
+ * Priority for finding Sanqian executable:
254
+ * 1. customPath parameter (if provided)
255
+ * 2. Cached executable from connection.json (most reliable)
256
+ * 3. Search in standard installation locations (fallback)
257
+ */
258
+ launchSanqian(customPath) {
259
+ let sanqianPath = null;
260
+ if (customPath) {
261
+ sanqianPath = this.findSanqianPath(customPath);
262
+ } else if (this.cachedExecutable && (0, import_fs.existsSync)(this.cachedExecutable)) {
263
+ sanqianPath = this.cachedExecutable;
264
+ console.log(`[SDK] Using cached executable: ${sanqianPath}`);
265
+ } else {
266
+ sanqianPath = this.findSanqianPath();
267
+ }
268
+ if (!sanqianPath) {
269
+ console.error("[SDK] Sanqian executable not found");
270
+ return false;
271
+ }
272
+ console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
273
+ try {
274
+ const os = (0, import_os.platform)();
275
+ if (os === "darwin") {
276
+ const appPath = sanqianPath.replace(
277
+ "/Contents/MacOS/Sanqian",
278
+ ""
279
+ );
280
+ (0, import_child_process.spawn)("open", ["-g", "-a", appPath, "--args", "--hidden"], {
281
+ detached: true,
282
+ stdio: "ignore"
283
+ }).unref();
284
+ } else {
285
+ (0, import_child_process.spawn)(sanqianPath, ["--hidden"], {
286
+ detached: true,
287
+ stdio: "ignore",
288
+ // On Windows, hide the console window
289
+ ...os === "win32" && {
290
+ windowsHide: true,
291
+ shell: false
292
+ }
293
+ }).unref();
297
294
  }
298
- }).unref();
295
+ return true;
296
+ } catch (e) {
297
+ console.error("[SDK] Failed to launch Sanqian:", e);
298
+ return false;
299
+ }
299
300
  }
300
- return true;
301
- } catch (e) {
302
- console.error("[SDK] Failed to launch Sanqian:", e);
303
- return false;
304
- }
305
- }
306
- /**
307
- * Check if Sanqian is running
308
- */
309
- isSanqianRunning() {
310
- return this.read() !== null;
301
+ /**
302
+ * Check if Sanqian is running
303
+ */
304
+ isSanqianRunning() {
305
+ return this.read() !== null;
306
+ }
307
+ };
311
308
  }
312
- };
309
+ });
310
+
311
+ // src/index.ts
312
+ var src_exports = {};
313
+ __export(src_exports, {
314
+ Conversation: () => Conversation,
315
+ DiscoveryManager: () => DiscoveryManager,
316
+ ErrorMessages: () => ErrorMessages,
317
+ SANQIAN_WEBSITE: () => SANQIAN_WEBSITE,
318
+ SDKErrorCode: () => SDKErrorCode,
319
+ SanqianSDK: () => SanqianSDK,
320
+ SanqianSDKError: () => SanqianSDKError,
321
+ createSDKError: () => createSDKError
322
+ });
323
+ module.exports = __toCommonJS(src_exports);
324
+
325
+ // src/client.ts
326
+ var import_isomorphic_ws = __toESM(require("isomorphic-ws"));
313
327
 
314
328
  // src/errors.ts
315
329
  var SANQIAN_WEBSITE = "https://sanqian.io";
@@ -480,7 +494,8 @@ function createSDKError(code, details) {
480
494
  // src/client.ts
481
495
  var SanqianSDK = class _SanqianSDK {
482
496
  config;
483
- discovery;
497
+ // DiscoveryManager is only initialized in Node.js environments (when connectionInfo is not provided)
498
+ discovery = null;
484
499
  ws = null;
485
500
  connectionInfo = null;
486
501
  state = {
@@ -536,11 +551,10 @@ var SanqianSDK = class _SanqianSDK {
536
551
  autoLaunchSanqian: true,
537
552
  ...config
538
553
  };
539
- this.discovery = new DiscoveryManager();
540
554
  for (const tool of config.tools) {
541
555
  this.toolHandlers.set(tool.name, tool.handler);
542
556
  }
543
- this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
557
+ this.launchedBySanqian = typeof process !== "undefined" && (process.env?.SANQIAN_LAUNCHED === "1" || process.env?.SANQIAN_NO_RECONNECT === "1");
544
558
  if (this.launchedBySanqian) {
545
559
  this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
546
560
  setTimeout(() => {
@@ -550,6 +564,26 @@ var SanqianSDK = class _SanqianSDK {
550
564
  }, 0);
551
565
  }
552
566
  }
567
+ /**
568
+ * Build WebSocket URL from connection info
569
+ * This is inlined to avoid importing DiscoveryManager in browser environments
570
+ */
571
+ buildWebSocketUrl(info) {
572
+ const wsPath = info.ws_path || "/ws/apps";
573
+ return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
574
+ }
575
+ /**
576
+ * Lazily initialize DiscoveryManager (only in Node.js environments)
577
+ * This uses dynamic import to avoid bundling Node.js APIs in browser builds
578
+ */
579
+ async getDiscovery() {
580
+ if (this.discovery) {
581
+ return this.discovery;
582
+ }
583
+ const { DiscoveryManager: DiscoveryManager2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
584
+ this.discovery = new DiscoveryManager2();
585
+ return this.discovery;
586
+ }
553
587
  // ============================================
554
588
  // Lifecycle
555
589
  // ============================================
@@ -573,15 +607,15 @@ var SanqianSDK = class _SanqianSDK {
573
607
  */
574
608
  async connectWithInfo(info) {
575
609
  this.connectionInfo = info;
576
- const url = this.discovery.buildWebSocketUrl(info);
610
+ const url = this.buildWebSocketUrl(info);
577
611
  return new Promise((resolve, reject) => {
578
612
  this.log(`Connecting to ${url}`);
579
- this.ws = new import_ws.default(url);
613
+ this.ws = new import_isomorphic_ws.default(url);
580
614
  const connectTimeout = setTimeout(() => {
581
615
  reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
582
616
  this.ws?.close();
583
617
  }, 1e4);
584
- this.ws.on("open", async () => {
618
+ this.ws.onopen = async () => {
585
619
  clearTimeout(connectTimeout);
586
620
  this.log("WebSocket connected");
587
621
  this.state.connected = true;
@@ -593,24 +627,28 @@ var SanqianSDK = class _SanqianSDK {
593
627
  } catch (e) {
594
628
  reject(e);
595
629
  }
596
- });
597
- this.ws.on("close", (code, reason) => {
598
- this.log(`WebSocket closed: ${code} ${reason.toString()}`);
599
- this.handleDisconnect(reason.toString());
600
- });
601
- this.ws.on("error", (error) => {
630
+ };
631
+ this.ws.onclose = (event) => {
632
+ const reasonRaw = event.reason;
633
+ const reason = typeof reasonRaw === "string" ? reasonRaw : reasonRaw?.toString() || "";
634
+ this.log(`WebSocket closed: ${event.code} ${reason}`);
635
+ this.handleDisconnect(reason);
636
+ };
637
+ this.ws.onerror = (event) => {
638
+ const error = event.error || new Error("WebSocket error");
602
639
  console.error("[SDK] WebSocket error:", error);
603
640
  this.state.lastError = error;
604
641
  this.emit("error", error);
605
- });
606
- this.ws.on("message", (data) => {
642
+ };
643
+ this.ws.onmessage = (event) => {
607
644
  try {
608
- const message = JSON.parse(data.toString());
645
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
646
+ const message = JSON.parse(data);
609
647
  this.handleMessage(message);
610
648
  } catch (e) {
611
649
  this.warn("Failed to parse message:", e);
612
650
  }
613
- });
651
+ };
614
652
  });
615
653
  }
616
654
  /**
@@ -619,7 +657,7 @@ var SanqianSDK = class _SanqianSDK {
619
657
  async disconnect() {
620
658
  this.stopHeartbeat();
621
659
  this.stopReconnect();
622
- this.discovery.stopWatching();
660
+ this.discovery?.stopWatching();
623
661
  if (this.ws) {
624
662
  this.ws.close(1e3, "Client disconnect");
625
663
  this.ws = null;
@@ -907,7 +945,7 @@ var SanqianSDK = class _SanqianSDK {
907
945
  // Communication
908
946
  // ============================================
909
947
  send(message) {
910
- if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
948
+ if (!this.ws || this.ws.readyState !== import_isomorphic_ws.default.OPEN) {
911
949
  const error = new Error(
912
950
  `WebSocket not open (state: ${this.ws?.readyState ?? "null"}), cannot send message`
913
951
  );
@@ -995,18 +1033,25 @@ var SanqianSDK = class _SanqianSDK {
995
1033
  */
996
1034
  async doFullConnect() {
997
1035
  this.log("Starting full connection flow...");
998
- let info = this.discovery.read();
999
- if (!info) {
1000
- if (this.config.autoLaunchSanqian) {
1001
- this.log("Sanqian not running, attempting to launch...");
1002
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
1003
- if (!launched) {
1004
- throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
1036
+ let info = null;
1037
+ if (this.config.connectionInfo) {
1038
+ this.log("Using pre-configured connection info (browser mode)");
1039
+ info = this.config.connectionInfo;
1040
+ } else {
1041
+ const discovery = await this.getDiscovery();
1042
+ info = discovery.read();
1043
+ if (!info) {
1044
+ if (this.config.autoLaunchSanqian) {
1045
+ this.log("Sanqian not running, attempting to launch...");
1046
+ const launched = discovery.launchSanqian(this.config.sanqianPath);
1047
+ if (!launched) {
1048
+ throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
1049
+ }
1050
+ this.log("Sanqian launch initiated, waiting for startup...");
1051
+ info = await this.waitForSanqianStartup();
1052
+ } else {
1053
+ throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
1005
1054
  }
1006
- this.log("Sanqian launch initiated, waiting for startup...");
1007
- info = await this.waitForSanqianStartup();
1008
- } else {
1009
- throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
1010
1055
  }
1011
1056
  }
1012
1057
  await this.connectWithInfo(info);
@@ -1017,8 +1062,9 @@ var SanqianSDK = class _SanqianSDK {
1017
1062
  async waitForSanqianStartup(timeout = 12e4) {
1018
1063
  const startTime = Date.now();
1019
1064
  const pollInterval = 500;
1065
+ const discovery = await this.getDiscovery();
1020
1066
  while (Date.now() - startTime < timeout) {
1021
- const info = this.discovery.read();
1067
+ const info = discovery.read();
1022
1068
  if (info) {
1023
1069
  this.log("Sanqian started, connection info available");
1024
1070
  return info;
@@ -1367,6 +1413,20 @@ var SanqianSDK = class _SanqianSDK {
1367
1413
  });
1368
1414
  return this.on(event, onceWrapper);
1369
1415
  }
1416
+ /**
1417
+ * Remove all event listeners
1418
+ *
1419
+ * Call this to prevent memory leaks when disposing the SDK instance.
1420
+ * If event is specified, only removes listeners for that event.
1421
+ */
1422
+ removeAllListeners(event) {
1423
+ if (event) {
1424
+ this.eventListeners.delete(event);
1425
+ } else {
1426
+ this.eventListeners.clear();
1427
+ }
1428
+ return this;
1429
+ }
1370
1430
  emit(event, ...args) {
1371
1431
  const listeners = this.eventListeners.get(event);
1372
1432
  if (listeners) {
@@ -1467,6 +1527,9 @@ var Conversation = class {
1467
1527
  });
1468
1528
  }
1469
1529
  };
1530
+
1531
+ // src/index.ts
1532
+ init_discovery();
1470
1533
  // Annotate the CommonJS export names for ESM import in node:
1471
1534
  0 && (module.exports = {
1472
1535
  Conversation,