@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.mjs CHANGED
@@ -1,272 +1,293 @@
1
- // src/client.ts
2
- import WebSocket from "ws";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
3
10
 
4
11
  // src/discovery.ts
12
+ var discovery_exports = {};
13
+ __export(discovery_exports, {
14
+ DiscoveryManager: () => DiscoveryManager
15
+ });
5
16
  import { existsSync, readFileSync, watch } from "fs";
6
17
  import { homedir, platform } from "os";
7
18
  import { join } from "path";
8
19
  import { spawn, execSync } from "child_process";
9
- var DiscoveryManager = class {
10
- connectionInfo = null;
11
- watcher = null;
12
- onChange = null;
13
- pollInterval = null;
14
- /**
15
- * Cached executable path from last successful connection.json read.
16
- * Persists even when Sanqian is not running, for use in auto-launch.
17
- */
18
- cachedExecutable = null;
19
- /**
20
- * Get the path to connection.json
21
- */
22
- getConnectionFilePath() {
23
- return join(homedir(), ".sanqian", "runtime", "connection.json");
24
- }
25
- /**
26
- * Read and validate connection info
27
- *
28
- * Returns null if:
29
- * - File doesn't exist
30
- * - File is invalid JSON
31
- * - Process is not running
32
- */
33
- read() {
34
- const filePath = this.getConnectionFilePath();
35
- if (!existsSync(filePath)) {
36
- return null;
37
- }
38
- try {
39
- const content = readFileSync(filePath, "utf-8");
40
- const info = JSON.parse(content);
41
- if (!info.port || !info.token || !info.pid) {
42
- return null;
43
- }
44
- if (info.executable) {
45
- this.cachedExecutable = info.executable;
46
- }
47
- if (!this.isProcessRunning(info.pid)) {
48
- return null;
20
+ var DiscoveryManager;
21
+ var init_discovery = __esm({
22
+ "src/discovery.ts"() {
23
+ "use strict";
24
+ DiscoveryManager = class {
25
+ connectionInfo = null;
26
+ watcher = null;
27
+ onChange = null;
28
+ pollInterval = null;
29
+ /**
30
+ * Cached executable path from last successful connection.json read.
31
+ * Persists even when Sanqian is not running, for use in auto-launch.
32
+ */
33
+ cachedExecutable = null;
34
+ /**
35
+ * Get the path to connection.json
36
+ */
37
+ getConnectionFilePath() {
38
+ return join(homedir(), ".sanqian", "runtime", "connection.json");
49
39
  }
50
- this.connectionInfo = info;
51
- return info;
52
- } catch {
53
- return null;
54
- }
55
- }
56
- /**
57
- * Start watching for connection file changes
58
- */
59
- startWatching(onChange) {
60
- this.onChange = onChange;
61
- const dir = join(homedir(), ".sanqian", "runtime");
62
- if (!existsSync(dir)) {
63
- this.pollInterval = setInterval(() => {
64
- if (existsSync(dir)) {
65
- if (this.pollInterval) {
66
- clearInterval(this.pollInterval);
67
- this.pollInterval = null;
40
+ /**
41
+ * Read and validate connection info
42
+ *
43
+ * Returns null if:
44
+ * - File doesn't exist
45
+ * - File is invalid JSON
46
+ * - Process is not running
47
+ */
48
+ read() {
49
+ const filePath = this.getConnectionFilePath();
50
+ if (!existsSync(filePath)) {
51
+ return null;
52
+ }
53
+ try {
54
+ const content = readFileSync(filePath, "utf-8");
55
+ const info = JSON.parse(content);
56
+ if (!info.port || !info.token || !info.pid) {
57
+ return null;
58
+ }
59
+ if (info.executable) {
60
+ this.cachedExecutable = info.executable;
68
61
  }
69
- this.setupWatcher(dir);
62
+ if (!this.isProcessRunning(info.pid)) {
63
+ return null;
64
+ }
65
+ this.connectionInfo = info;
66
+ return info;
67
+ } catch {
68
+ return null;
70
69
  }
71
- }, 2e3);
72
- return;
73
- }
74
- this.setupWatcher(dir);
75
- }
76
- setupWatcher(dir) {
77
- try {
78
- this.watcher = watch(dir, (event, filename) => {
79
- if (filename === "connection.json") {
80
- setTimeout(() => {
81
- const newInfo = this.read();
82
- const oldInfoStr = JSON.stringify(this.connectionInfo);
83
- const newInfoStr = JSON.stringify(newInfo);
84
- if (oldInfoStr !== newInfoStr) {
85
- this.connectionInfo = newInfo;
86
- this.onChange?.(newInfo);
70
+ }
71
+ /**
72
+ * Start watching for connection file changes
73
+ */
74
+ startWatching(onChange) {
75
+ this.onChange = onChange;
76
+ const dir = join(homedir(), ".sanqian", "runtime");
77
+ if (!existsSync(dir)) {
78
+ this.pollInterval = setInterval(() => {
79
+ if (existsSync(dir)) {
80
+ if (this.pollInterval) {
81
+ clearInterval(this.pollInterval);
82
+ this.pollInterval = null;
83
+ }
84
+ this.setupWatcher(dir);
87
85
  }
88
- }, 100);
86
+ }, 2e3);
87
+ return;
89
88
  }
90
- });
91
- } catch (e) {
92
- console.error("Failed to watch connection file:", e);
93
- }
94
- }
95
- /**
96
- * Stop watching
97
- */
98
- stopWatching() {
99
- if (this.pollInterval) {
100
- clearInterval(this.pollInterval);
101
- this.pollInterval = null;
102
- }
103
- this.watcher?.close();
104
- this.watcher = null;
105
- this.onChange = null;
106
- }
107
- /**
108
- * Check if a process is running by PID
109
- *
110
- * Cross-platform implementation:
111
- * - macOS/Linux: process.kill(pid, 0) works correctly
112
- * - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
113
- * so we use tasklist command for reliable checking
114
- */
115
- isProcessRunning(pid) {
116
- try {
117
- if (platform() === "win32") {
89
+ this.setupWatcher(dir);
90
+ }
91
+ setupWatcher(dir) {
118
92
  try {
119
- const result = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
120
- encoding: "utf-8",
121
- stdio: ["pipe", "pipe", "pipe"]
93
+ this.watcher = watch(dir, (event, filename) => {
94
+ if (filename === "connection.json") {
95
+ setTimeout(() => {
96
+ const newInfo = this.read();
97
+ const oldInfoStr = JSON.stringify(this.connectionInfo);
98
+ const newInfoStr = JSON.stringify(newInfo);
99
+ if (oldInfoStr !== newInfoStr) {
100
+ this.connectionInfo = newInfo;
101
+ this.onChange?.(newInfo);
102
+ }
103
+ }, 100);
104
+ }
122
105
  });
123
- const trimmed = result.trim();
124
- if (!trimmed || trimmed.toUpperCase().startsWith("INFO")) {
125
- return false;
106
+ } catch (e) {
107
+ console.error("Failed to watch connection file:", e);
108
+ }
109
+ }
110
+ /**
111
+ * Stop watching
112
+ */
113
+ stopWatching() {
114
+ if (this.pollInterval) {
115
+ clearInterval(this.pollInterval);
116
+ this.pollInterval = null;
117
+ }
118
+ this.watcher?.close();
119
+ this.watcher = null;
120
+ this.onChange = null;
121
+ }
122
+ /**
123
+ * Check if a process is running by PID
124
+ *
125
+ * Cross-platform implementation:
126
+ * - macOS/Linux: process.kill(pid, 0) works correctly
127
+ * - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
128
+ * so we use tasklist command for reliable checking
129
+ */
130
+ isProcessRunning(pid) {
131
+ try {
132
+ if (platform() === "win32") {
133
+ try {
134
+ const result = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
135
+ encoding: "utf-8",
136
+ stdio: ["pipe", "pipe", "pipe"]
137
+ });
138
+ const trimmed = result.trim();
139
+ if (!trimmed || trimmed.toUpperCase().startsWith("INFO")) {
140
+ return false;
141
+ }
142
+ return new RegExp(`\\b${pid}\\b`).test(trimmed);
143
+ } catch {
144
+ return false;
145
+ }
146
+ } else {
147
+ process.kill(pid, 0);
148
+ return true;
126
149
  }
127
- return new RegExp(`\\b${pid}\\b`).test(trimmed);
128
150
  } catch {
129
151
  return false;
130
152
  }
131
- } else {
132
- process.kill(pid, 0);
133
- return true;
134
153
  }
135
- } catch {
136
- return false;
137
- }
138
- }
139
- /**
140
- * Get cached connection info (may be stale)
141
- */
142
- getCached() {
143
- return this.connectionInfo;
144
- }
145
- /**
146
- * Build WebSocket URL from connection info
147
- */
148
- buildWebSocketUrl(info) {
149
- return `ws://127.0.0.1:${info.port}${info.ws_path}?token=${info.token}`;
150
- }
151
- /**
152
- * Build HTTP base URL from connection info
153
- */
154
- buildHttpUrl(info) {
155
- return `http://127.0.0.1:${info.port}`;
156
- }
157
- /**
158
- * Find Sanqian executable path
159
- * Searches in standard installation locations for each platform
160
- */
161
- findSanqianPath(customPath) {
162
- if (customPath) {
163
- if (existsSync(customPath)) {
164
- return customPath;
154
+ /**
155
+ * Get cached connection info (may be stale)
156
+ */
157
+ getCached() {
158
+ return this.connectionInfo;
165
159
  }
166
- console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
167
- return null;
168
- }
169
- const os = platform();
170
- const searchPaths = [];
171
- if (os === "darwin") {
172
- searchPaths.push(
173
- // Production: installed app
174
- "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
175
- join(homedir(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
176
- // Development: electron-builder output
177
- join(homedir(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
178
- join(homedir(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
179
- );
180
- } else if (os === "win32") {
181
- const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
182
- const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
183
- const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
184
- searchPaths.push(
185
- // Production: NSIS installer uses lowercase directory name from package.json "name"
186
- join(localAppData, "Programs", "sanqian", "Sanqian.exe"),
187
- // Legacy/alternative paths with uppercase
188
- join(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
189
- join(programFiles, "Sanqian", "Sanqian.exe"),
190
- join(programFilesX86, "Sanqian", "Sanqian.exe"),
191
- // Development: electron-builder output
192
- join(homedir(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
193
- );
194
- } else {
195
- searchPaths.push(
196
- "/usr/bin/sanqian",
197
- "/usr/local/bin/sanqian",
198
- join(homedir(), ".local/bin/sanqian"),
199
- "/opt/Sanqian/sanqian",
200
- // Development
201
- join(homedir(), "dev/sanqian/dist/linux-unpacked/sanqian")
202
- );
203
- }
204
- for (const path of searchPaths) {
205
- if (existsSync(path)) {
206
- return path;
160
+ /**
161
+ * Build WebSocket URL from connection info
162
+ */
163
+ buildWebSocketUrl(info) {
164
+ const wsPath = info.ws_path || "/ws/apps";
165
+ return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
207
166
  }
208
- }
209
- return null;
210
- }
211
- /**
212
- * Launch Sanqian in hidden/tray mode
213
- * Returns true if launch was initiated successfully
214
- *
215
- * Priority for finding Sanqian executable:
216
- * 1. customPath parameter (if provided)
217
- * 2. Cached executable from connection.json (most reliable)
218
- * 3. Search in standard installation locations (fallback)
219
- */
220
- launchSanqian(customPath) {
221
- let sanqianPath = null;
222
- if (customPath) {
223
- sanqianPath = this.findSanqianPath(customPath);
224
- } else if (this.cachedExecutable && existsSync(this.cachedExecutable)) {
225
- sanqianPath = this.cachedExecutable;
226
- console.log(`[SDK] Using cached executable: ${sanqianPath}`);
227
- } else {
228
- sanqianPath = this.findSanqianPath();
229
- }
230
- if (!sanqianPath) {
231
- console.error("[SDK] Sanqian executable not found");
232
- return false;
233
- }
234
- console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
235
- try {
236
- const os = platform();
237
- if (os === "darwin") {
238
- const appPath = sanqianPath.replace(
239
- "/Contents/MacOS/Sanqian",
240
- ""
241
- );
242
- spawn("open", ["-g", "-a", appPath, "--args", "--hidden"], {
243
- detached: true,
244
- stdio: "ignore"
245
- }).unref();
246
- } else {
247
- spawn(sanqianPath, ["--hidden"], {
248
- detached: true,
249
- stdio: "ignore",
250
- // On Windows, hide the console window
251
- ...os === "win32" && {
252
- windowsHide: true,
253
- shell: false
167
+ /**
168
+ * Build HTTP base URL from connection info
169
+ */
170
+ buildHttpUrl(info) {
171
+ return `http://127.0.0.1:${info.port}`;
172
+ }
173
+ /**
174
+ * Find Sanqian executable path
175
+ * Searches in standard installation locations for each platform
176
+ */
177
+ findSanqianPath(customPath) {
178
+ if (customPath) {
179
+ if (existsSync(customPath)) {
180
+ return customPath;
181
+ }
182
+ console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
183
+ return null;
184
+ }
185
+ const os = platform();
186
+ const searchPaths = [];
187
+ if (os === "darwin") {
188
+ searchPaths.push(
189
+ // Production: installed app
190
+ "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
191
+ join(homedir(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
192
+ // Development: electron-builder output
193
+ join(homedir(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
194
+ join(homedir(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
195
+ );
196
+ } else if (os === "win32") {
197
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
198
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
199
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
200
+ searchPaths.push(
201
+ // Production: NSIS installer uses lowercase directory name from package.json "name"
202
+ join(localAppData, "Programs", "sanqian", "Sanqian.exe"),
203
+ // Legacy/alternative paths with uppercase
204
+ join(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
205
+ join(programFiles, "Sanqian", "Sanqian.exe"),
206
+ join(programFilesX86, "Sanqian", "Sanqian.exe"),
207
+ // Development: electron-builder output
208
+ join(homedir(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
209
+ );
210
+ } else {
211
+ searchPaths.push(
212
+ "/usr/bin/sanqian",
213
+ "/usr/local/bin/sanqian",
214
+ join(homedir(), ".local/bin/sanqian"),
215
+ "/opt/Sanqian/sanqian",
216
+ // Development
217
+ join(homedir(), "dev/sanqian/dist/linux-unpacked/sanqian")
218
+ );
219
+ }
220
+ for (const path of searchPaths) {
221
+ if (existsSync(path)) {
222
+ return path;
254
223
  }
255
- }).unref();
224
+ }
225
+ return null;
256
226
  }
257
- return true;
258
- } catch (e) {
259
- console.error("[SDK] Failed to launch Sanqian:", e);
260
- return false;
261
- }
262
- }
263
- /**
264
- * Check if Sanqian is running
265
- */
266
- isSanqianRunning() {
267
- return this.read() !== null;
227
+ /**
228
+ * Launch Sanqian in hidden/tray mode
229
+ * Returns true if launch was initiated successfully
230
+ *
231
+ * Priority for finding Sanqian executable:
232
+ * 1. customPath parameter (if provided)
233
+ * 2. Cached executable from connection.json (most reliable)
234
+ * 3. Search in standard installation locations (fallback)
235
+ */
236
+ launchSanqian(customPath) {
237
+ let sanqianPath = null;
238
+ if (customPath) {
239
+ sanqianPath = this.findSanqianPath(customPath);
240
+ } else if (this.cachedExecutable && existsSync(this.cachedExecutable)) {
241
+ sanqianPath = this.cachedExecutable;
242
+ console.log(`[SDK] Using cached executable: ${sanqianPath}`);
243
+ } else {
244
+ sanqianPath = this.findSanqianPath();
245
+ }
246
+ if (!sanqianPath) {
247
+ console.error("[SDK] Sanqian executable not found");
248
+ return false;
249
+ }
250
+ console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
251
+ try {
252
+ const os = platform();
253
+ if (os === "darwin") {
254
+ const appPath = sanqianPath.replace(
255
+ "/Contents/MacOS/Sanqian",
256
+ ""
257
+ );
258
+ spawn("open", ["-g", "-a", appPath, "--args", "--hidden"], {
259
+ detached: true,
260
+ stdio: "ignore"
261
+ }).unref();
262
+ } else {
263
+ spawn(sanqianPath, ["--hidden"], {
264
+ detached: true,
265
+ stdio: "ignore",
266
+ // On Windows, hide the console window
267
+ ...os === "win32" && {
268
+ windowsHide: true,
269
+ shell: false
270
+ }
271
+ }).unref();
272
+ }
273
+ return true;
274
+ } catch (e) {
275
+ console.error("[SDK] Failed to launch Sanqian:", e);
276
+ return false;
277
+ }
278
+ }
279
+ /**
280
+ * Check if Sanqian is running
281
+ */
282
+ isSanqianRunning() {
283
+ return this.read() !== null;
284
+ }
285
+ };
268
286
  }
269
- };
287
+ });
288
+
289
+ // src/client.ts
290
+ import WebSocket from "isomorphic-ws";
270
291
 
271
292
  // src/errors.ts
272
293
  var SANQIAN_WEBSITE = "https://sanqian.io";
@@ -437,7 +458,8 @@ function createSDKError(code, details) {
437
458
  // src/client.ts
438
459
  var SanqianSDK = class _SanqianSDK {
439
460
  config;
440
- discovery;
461
+ // DiscoveryManager is only initialized in Node.js environments (when connectionInfo is not provided)
462
+ discovery = null;
441
463
  ws = null;
442
464
  connectionInfo = null;
443
465
  state = {
@@ -493,11 +515,10 @@ var SanqianSDK = class _SanqianSDK {
493
515
  autoLaunchSanqian: true,
494
516
  ...config
495
517
  };
496
- this.discovery = new DiscoveryManager();
497
518
  for (const tool of config.tools) {
498
519
  this.toolHandlers.set(tool.name, tool.handler);
499
520
  }
500
- this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
521
+ this.launchedBySanqian = typeof process !== "undefined" && (process.env?.SANQIAN_LAUNCHED === "1" || process.env?.SANQIAN_NO_RECONNECT === "1");
501
522
  if (this.launchedBySanqian) {
502
523
  this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
503
524
  setTimeout(() => {
@@ -507,6 +528,26 @@ var SanqianSDK = class _SanqianSDK {
507
528
  }, 0);
508
529
  }
509
530
  }
531
+ /**
532
+ * Build WebSocket URL from connection info
533
+ * This is inlined to avoid importing DiscoveryManager in browser environments
534
+ */
535
+ buildWebSocketUrl(info) {
536
+ const wsPath = info.ws_path || "/ws/apps";
537
+ return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
538
+ }
539
+ /**
540
+ * Lazily initialize DiscoveryManager (only in Node.js environments)
541
+ * This uses dynamic import to avoid bundling Node.js APIs in browser builds
542
+ */
543
+ async getDiscovery() {
544
+ if (this.discovery) {
545
+ return this.discovery;
546
+ }
547
+ const { DiscoveryManager: DiscoveryManager2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
548
+ this.discovery = new DiscoveryManager2();
549
+ return this.discovery;
550
+ }
510
551
  // ============================================
511
552
  // Lifecycle
512
553
  // ============================================
@@ -530,7 +571,7 @@ var SanqianSDK = class _SanqianSDK {
530
571
  */
531
572
  async connectWithInfo(info) {
532
573
  this.connectionInfo = info;
533
- const url = this.discovery.buildWebSocketUrl(info);
574
+ const url = this.buildWebSocketUrl(info);
534
575
  return new Promise((resolve, reject) => {
535
576
  this.log(`Connecting to ${url}`);
536
577
  this.ws = new WebSocket(url);
@@ -538,7 +579,7 @@ var SanqianSDK = class _SanqianSDK {
538
579
  reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
539
580
  this.ws?.close();
540
581
  }, 1e4);
541
- this.ws.on("open", async () => {
582
+ this.ws.onopen = async () => {
542
583
  clearTimeout(connectTimeout);
543
584
  this.log("WebSocket connected");
544
585
  this.state.connected = true;
@@ -550,24 +591,28 @@ var SanqianSDK = class _SanqianSDK {
550
591
  } catch (e) {
551
592
  reject(e);
552
593
  }
553
- });
554
- this.ws.on("close", (code, reason) => {
555
- this.log(`WebSocket closed: ${code} ${reason.toString()}`);
556
- this.handleDisconnect(reason.toString());
557
- });
558
- this.ws.on("error", (error) => {
594
+ };
595
+ this.ws.onclose = (event) => {
596
+ const reasonRaw = event.reason;
597
+ const reason = typeof reasonRaw === "string" ? reasonRaw : reasonRaw?.toString() || "";
598
+ this.log(`WebSocket closed: ${event.code} ${reason}`);
599
+ this.handleDisconnect(reason);
600
+ };
601
+ this.ws.onerror = (event) => {
602
+ const error = event.error || new Error("WebSocket error");
559
603
  console.error("[SDK] WebSocket error:", error);
560
604
  this.state.lastError = error;
561
605
  this.emit("error", error);
562
- });
563
- this.ws.on("message", (data) => {
606
+ };
607
+ this.ws.onmessage = (event) => {
564
608
  try {
565
- const message = JSON.parse(data.toString());
609
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
610
+ const message = JSON.parse(data);
566
611
  this.handleMessage(message);
567
612
  } catch (e) {
568
613
  this.warn("Failed to parse message:", e);
569
614
  }
570
- });
615
+ };
571
616
  });
572
617
  }
573
618
  /**
@@ -576,7 +621,7 @@ var SanqianSDK = class _SanqianSDK {
576
621
  async disconnect() {
577
622
  this.stopHeartbeat();
578
623
  this.stopReconnect();
579
- this.discovery.stopWatching();
624
+ this.discovery?.stopWatching();
580
625
  if (this.ws) {
581
626
  this.ws.close(1e3, "Client disconnect");
582
627
  this.ws = null;
@@ -952,18 +997,25 @@ var SanqianSDK = class _SanqianSDK {
952
997
  */
953
998
  async doFullConnect() {
954
999
  this.log("Starting full connection flow...");
955
- let info = this.discovery.read();
956
- if (!info) {
957
- if (this.config.autoLaunchSanqian) {
958
- this.log("Sanqian not running, attempting to launch...");
959
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
960
- if (!launched) {
961
- throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
1000
+ let info = null;
1001
+ if (this.config.connectionInfo) {
1002
+ this.log("Using pre-configured connection info (browser mode)");
1003
+ info = this.config.connectionInfo;
1004
+ } else {
1005
+ const discovery = await this.getDiscovery();
1006
+ info = discovery.read();
1007
+ if (!info) {
1008
+ if (this.config.autoLaunchSanqian) {
1009
+ this.log("Sanqian not running, attempting to launch...");
1010
+ const launched = discovery.launchSanqian(this.config.sanqianPath);
1011
+ if (!launched) {
1012
+ throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
1013
+ }
1014
+ this.log("Sanqian launch initiated, waiting for startup...");
1015
+ info = await this.waitForSanqianStartup();
1016
+ } else {
1017
+ throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
962
1018
  }
963
- this.log("Sanqian launch initiated, waiting for startup...");
964
- info = await this.waitForSanqianStartup();
965
- } else {
966
- throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
967
1019
  }
968
1020
  }
969
1021
  await this.connectWithInfo(info);
@@ -974,8 +1026,9 @@ var SanqianSDK = class _SanqianSDK {
974
1026
  async waitForSanqianStartup(timeout = 12e4) {
975
1027
  const startTime = Date.now();
976
1028
  const pollInterval = 500;
1029
+ const discovery = await this.getDiscovery();
977
1030
  while (Date.now() - startTime < timeout) {
978
- const info = this.discovery.read();
1031
+ const info = discovery.read();
979
1032
  if (info) {
980
1033
  this.log("Sanqian started, connection info available");
981
1034
  return info;
@@ -1324,6 +1377,20 @@ var SanqianSDK = class _SanqianSDK {
1324
1377
  });
1325
1378
  return this.on(event, onceWrapper);
1326
1379
  }
1380
+ /**
1381
+ * Remove all event listeners
1382
+ *
1383
+ * Call this to prevent memory leaks when disposing the SDK instance.
1384
+ * If event is specified, only removes listeners for that event.
1385
+ */
1386
+ removeAllListeners(event) {
1387
+ if (event) {
1388
+ this.eventListeners.delete(event);
1389
+ } else {
1390
+ this.eventListeners.clear();
1391
+ }
1392
+ return this;
1393
+ }
1327
1394
  emit(event, ...args) {
1328
1395
  const listeners = this.eventListeners.get(event);
1329
1396
  if (listeners) {
@@ -1424,6 +1491,9 @@ var Conversation = class {
1424
1491
  });
1425
1492
  }
1426
1493
  };
1494
+
1495
+ // src/index.ts
1496
+ init_discovery();
1427
1497
  export {
1428
1498
  Conversation,
1429
1499
  DiscoveryManager,