@yushaw/sanqian-sdk 0.2.0 → 0.2.1

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.d.mts CHANGED
@@ -63,6 +63,8 @@ interface ConnectionInfo {
63
63
  token: string;
64
64
  pid: number;
65
65
  started_at: string;
66
+ /** Path to Sanqian executable (for SDK auto-launch) */
67
+ executable?: string;
66
68
  }
67
69
  interface ConnectionState {
68
70
  connected: boolean;
@@ -215,6 +217,7 @@ declare class SanqianSDK {
215
217
  private heartbeatAckPending;
216
218
  private missedHeartbeats;
217
219
  private static readonly MAX_MISSED_HEARTBEATS;
220
+ private connectingPromise;
218
221
  private eventListeners;
219
222
  constructor(config: SDKConfig);
220
223
  /**
@@ -225,6 +228,9 @@ declare class SanqianSDK {
225
228
  *
226
229
  * If autoLaunchSanqian is enabled and Sanqian is not running,
227
230
  * SDK will attempt to start it in hidden/tray mode.
231
+ *
232
+ * @throws Error if Sanqian executable cannot be found (when autoLaunchSanqian is enabled)
233
+ * @throws Error if connection times out after 2 minutes
228
234
  */
229
235
  connect(): Promise<void>;
230
236
  /**
@@ -256,11 +262,31 @@ declare class SanqianSDK {
256
262
  */
257
263
  isConnected(): boolean;
258
264
  /**
259
- * Wait for connection to be established.
260
- * Used internally by chat() and chatStream() for auto-reconnect.
261
- * Relies on background scheduleReconnect() to do the actual reconnection.
265
+ * Ensure SDK is ready for API calls.
266
+ *
267
+ * This is the unified entry point for all API methods that require a connection.
268
+ * It handles:
269
+ * - Already connected: returns immediately
270
+ * - Connection in progress: waits for existing attempt (deduplication)
271
+ * - Not connected: initiates connection (with optional auto-launch)
272
+ *
273
+ * Design pattern: gRPC wait-for-ready + ClientFactory promise deduplication
274
+ *
275
+ * @throws Error if connection fails or times out
276
+ */
277
+ ensureReady(): Promise<void>;
278
+ /**
279
+ * Internal: Perform full connection flow
280
+ * Handles launching Sanqian if needed, then connecting and registering.
281
+ *
282
+ * Note: This method is protected by connectingPromise deduplication in ensureReady(),
283
+ * so there's no need for additional launch-in-progress tracking within a single instance.
284
+ */
285
+ private doFullConnect;
286
+ /**
287
+ * Wait for Sanqian to start and connection.json to become available
262
288
  */
263
- private waitForConnection;
289
+ private waitForSanqianStartup;
264
290
  /**
265
291
  * Update tool list dynamically
266
292
  */
@@ -431,6 +457,11 @@ declare class DiscoveryManager {
431
457
  private watcher;
432
458
  private onChange;
433
459
  private pollInterval;
460
+ /**
461
+ * Cached executable path from last successful connection.json read.
462
+ * Persists even when Sanqian is not running, for use in auto-launch.
463
+ */
464
+ private cachedExecutable;
434
465
  /**
435
466
  * Get the path to connection.json
436
467
  */
@@ -482,6 +513,11 @@ declare class DiscoveryManager {
482
513
  /**
483
514
  * Launch Sanqian in hidden/tray mode
484
515
  * Returns true if launch was initiated successfully
516
+ *
517
+ * Priority for finding Sanqian executable:
518
+ * 1. customPath parameter (if provided)
519
+ * 2. Cached executable from connection.json (most reliable)
520
+ * 3. Search in standard installation locations (fallback)
485
521
  */
486
522
  launchSanqian(customPath?: string): boolean;
487
523
  /**
package/dist/index.d.ts CHANGED
@@ -63,6 +63,8 @@ interface ConnectionInfo {
63
63
  token: string;
64
64
  pid: number;
65
65
  started_at: string;
66
+ /** Path to Sanqian executable (for SDK auto-launch) */
67
+ executable?: string;
66
68
  }
67
69
  interface ConnectionState {
68
70
  connected: boolean;
@@ -215,6 +217,7 @@ declare class SanqianSDK {
215
217
  private heartbeatAckPending;
216
218
  private missedHeartbeats;
217
219
  private static readonly MAX_MISSED_HEARTBEATS;
220
+ private connectingPromise;
218
221
  private eventListeners;
219
222
  constructor(config: SDKConfig);
220
223
  /**
@@ -225,6 +228,9 @@ declare class SanqianSDK {
225
228
  *
226
229
  * If autoLaunchSanqian is enabled and Sanqian is not running,
227
230
  * SDK will attempt to start it in hidden/tray mode.
231
+ *
232
+ * @throws Error if Sanqian executable cannot be found (when autoLaunchSanqian is enabled)
233
+ * @throws Error if connection times out after 2 minutes
228
234
  */
229
235
  connect(): Promise<void>;
230
236
  /**
@@ -256,11 +262,31 @@ declare class SanqianSDK {
256
262
  */
257
263
  isConnected(): boolean;
258
264
  /**
259
- * Wait for connection to be established.
260
- * Used internally by chat() and chatStream() for auto-reconnect.
261
- * Relies on background scheduleReconnect() to do the actual reconnection.
265
+ * Ensure SDK is ready for API calls.
266
+ *
267
+ * This is the unified entry point for all API methods that require a connection.
268
+ * It handles:
269
+ * - Already connected: returns immediately
270
+ * - Connection in progress: waits for existing attempt (deduplication)
271
+ * - Not connected: initiates connection (with optional auto-launch)
272
+ *
273
+ * Design pattern: gRPC wait-for-ready + ClientFactory promise deduplication
274
+ *
275
+ * @throws Error if connection fails or times out
276
+ */
277
+ ensureReady(): Promise<void>;
278
+ /**
279
+ * Internal: Perform full connection flow
280
+ * Handles launching Sanqian if needed, then connecting and registering.
281
+ *
282
+ * Note: This method is protected by connectingPromise deduplication in ensureReady(),
283
+ * so there's no need for additional launch-in-progress tracking within a single instance.
284
+ */
285
+ private doFullConnect;
286
+ /**
287
+ * Wait for Sanqian to start and connection.json to become available
262
288
  */
263
- private waitForConnection;
289
+ private waitForSanqianStartup;
264
290
  /**
265
291
  * Update tool list dynamically
266
292
  */
@@ -431,6 +457,11 @@ declare class DiscoveryManager {
431
457
  private watcher;
432
458
  private onChange;
433
459
  private pollInterval;
460
+ /**
461
+ * Cached executable path from last successful connection.json read.
462
+ * Persists even when Sanqian is not running, for use in auto-launch.
463
+ */
464
+ private cachedExecutable;
434
465
  /**
435
466
  * Get the path to connection.json
436
467
  */
@@ -482,6 +513,11 @@ declare class DiscoveryManager {
482
513
  /**
483
514
  * Launch Sanqian in hidden/tray mode
484
515
  * Returns true if launch was initiated successfully
516
+ *
517
+ * Priority for finding Sanqian executable:
518
+ * 1. customPath parameter (if provided)
519
+ * 2. Cached executable from connection.json (most reliable)
520
+ * 3. Search in standard installation locations (fallback)
485
521
  */
486
522
  launchSanqian(customPath?: string): boolean;
487
523
  /**
package/dist/index.js CHANGED
@@ -49,6 +49,11 @@ var DiscoveryManager = class {
49
49
  watcher = null;
50
50
  onChange = null;
51
51
  pollInterval = null;
52
+ /**
53
+ * Cached executable path from last successful connection.json read.
54
+ * Persists even when Sanqian is not running, for use in auto-launch.
55
+ */
56
+ cachedExecutable = null;
52
57
  /**
53
58
  * Get the path to connection.json
54
59
  */
@@ -74,6 +79,9 @@ var DiscoveryManager = class {
74
79
  if (!info.port || !info.token || !info.pid) {
75
80
  return null;
76
81
  }
82
+ if (info.executable) {
83
+ this.cachedExecutable = info.executable;
84
+ }
77
85
  if (!this.isProcessRunning(info.pid)) {
78
86
  return null;
79
87
  }
@@ -235,9 +243,22 @@ var DiscoveryManager = class {
235
243
  /**
236
244
  * Launch Sanqian in hidden/tray mode
237
245
  * Returns true if launch was initiated successfully
246
+ *
247
+ * Priority for finding Sanqian executable:
248
+ * 1. customPath parameter (if provided)
249
+ * 2. Cached executable from connection.json (most reliable)
250
+ * 3. Search in standard installation locations (fallback)
238
251
  */
239
252
  launchSanqian(customPath) {
240
- const sanqianPath = this.findSanqianPath(customPath);
253
+ let sanqianPath = null;
254
+ if (customPath) {
255
+ sanqianPath = this.findSanqianPath(customPath);
256
+ } else if (this.cachedExecutable && (0, import_fs.existsSync)(this.cachedExecutable)) {
257
+ sanqianPath = this.cachedExecutable;
258
+ console.log(`[SDK] Using cached executable: ${sanqianPath}`);
259
+ } else {
260
+ sanqianPath = this.findSanqianPath();
261
+ }
241
262
  if (!sanqianPath) {
242
263
  console.error("[SDK] Sanqian executable not found");
243
264
  return false;
@@ -301,6 +322,8 @@ var SanqianSDK = class _SanqianSDK {
301
322
  heartbeatAckPending = false;
302
323
  missedHeartbeats = 0;
303
324
  static MAX_MISSED_HEARTBEATS = 2;
325
+ // Connection promise for deduplication (prevents multiple concurrent connect attempts)
326
+ connectingPromise = null;
304
327
  // Event listeners
305
328
  eventListeners = /* @__PURE__ */ new Map();
306
329
  constructor(config) {
@@ -326,40 +349,12 @@ var SanqianSDK = class _SanqianSDK {
326
349
  *
327
350
  * If autoLaunchSanqian is enabled and Sanqian is not running,
328
351
  * SDK will attempt to start it in hidden/tray mode.
352
+ *
353
+ * @throws Error if Sanqian executable cannot be found (when autoLaunchSanqian is enabled)
354
+ * @throws Error if connection times out after 2 minutes
329
355
  */
330
356
  async connect() {
331
- const info = this.discovery.read();
332
- if (!info) {
333
- if (this.config.autoLaunchSanqian) {
334
- console.log("[SDK] Sanqian not running, attempting to launch...");
335
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
336
- if (launched) {
337
- console.log("[SDK] Sanqian launch initiated, waiting for startup...");
338
- } else {
339
- console.warn("[SDK] Failed to launch Sanqian, will wait for manual start");
340
- }
341
- }
342
- return new Promise((resolve, reject) => {
343
- console.log("[SDK] Waiting for Sanqian...");
344
- const timeout = setTimeout(() => {
345
- this.discovery.stopWatching();
346
- reject(new Error("Sanqian connection timeout"));
347
- }, 6e4);
348
- this.discovery.startWatching(async (newInfo) => {
349
- if (newInfo) {
350
- clearTimeout(timeout);
351
- this.discovery.stopWatching();
352
- try {
353
- await this.connectWithInfo(newInfo);
354
- resolve();
355
- } catch (e) {
356
- reject(e);
357
- }
358
- }
359
- });
360
- });
361
- }
362
- await this.connectWithInfo(info);
357
+ return this.ensureReady();
363
358
  }
364
359
  /**
365
360
  * Connect with known connection info
@@ -713,57 +708,77 @@ var SanqianSDK = class _SanqianSDK {
713
708
  return this.state.connected && this.state.registered;
714
709
  }
715
710
  /**
716
- * Wait for connection to be established.
717
- * Used internally by chat() and chatStream() for auto-reconnect.
718
- * Relies on background scheduleReconnect() to do the actual reconnection.
711
+ * Ensure SDK is ready for API calls.
712
+ *
713
+ * This is the unified entry point for all API methods that require a connection.
714
+ * It handles:
715
+ * - Already connected: returns immediately
716
+ * - Connection in progress: waits for existing attempt (deduplication)
717
+ * - Not connected: initiates connection (with optional auto-launch)
718
+ *
719
+ * Design pattern: gRPC wait-for-ready + ClientFactory promise deduplication
720
+ *
721
+ * @throws Error if connection fails or times out
719
722
  */
720
- async waitForConnection(timeout = 3e4) {
723
+ async ensureReady() {
721
724
  if (this.isConnected()) {
722
725
  return;
723
726
  }
724
- console.log("[SDK] Not connected, waiting for reconnection...");
725
- return new Promise((resolve, reject) => {
726
- let resolved = false;
727
- const timer = setTimeout(() => {
728
- if (!resolved) {
729
- resolved = true;
730
- reject(new Error("Connection timeout. Please ensure Sanqian is running."));
731
- }
732
- }, timeout);
733
- this.once("registered", () => {
734
- if (!resolved) {
735
- resolved = true;
736
- clearTimeout(timer);
737
- resolve();
738
- }
739
- });
740
- if (this.isConnected()) {
741
- if (!resolved) {
742
- resolved = true;
743
- clearTimeout(timer);
744
- resolve();
727
+ if (this.connectingPromise) {
728
+ console.log("[SDK] Connection already in progress, waiting...");
729
+ return this.connectingPromise;
730
+ }
731
+ this.connectingPromise = this.doFullConnect();
732
+ try {
733
+ await this.connectingPromise;
734
+ } finally {
735
+ this.connectingPromise = null;
736
+ }
737
+ }
738
+ /**
739
+ * Internal: Perform full connection flow
740
+ * Handles launching Sanqian if needed, then connecting and registering.
741
+ *
742
+ * Note: This method is protected by connectingPromise deduplication in ensureReady(),
743
+ * so there's no need for additional launch-in-progress tracking within a single instance.
744
+ */
745
+ async doFullConnect() {
746
+ console.log("[SDK] Starting full connection flow...");
747
+ let info = this.discovery.read();
748
+ if (!info) {
749
+ if (this.config.autoLaunchSanqian) {
750
+ console.log("[SDK] Sanqian not running, attempting to launch...");
751
+ const launched = this.discovery.launchSanqian(this.config.sanqianPath);
752
+ if (!launched) {
753
+ throw new Error(
754
+ "Sanqian executable not found. Please ensure Sanqian is installed, or provide a custom path via sanqianPath config option."
755
+ );
745
756
  }
746
- return;
757
+ console.log("[SDK] Sanqian launch initiated, waiting for startup...");
758
+ info = await this.waitForSanqianStartup();
759
+ } else {
760
+ throw new Error("Sanqian is not running. Please start it manually.");
747
761
  }
748
- if (!this.reconnectTimer) {
749
- const info = this.discovery.read();
750
- if (!info && this.config.autoLaunchSanqian) {
751
- console.log("[SDK] Sanqian not running, attempting to launch...");
752
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
753
- if (launched) {
754
- this.scheduleReconnect();
755
- }
756
- } else if (!info) {
757
- if (!resolved) {
758
- resolved = true;
759
- clearTimeout(timer);
760
- reject(new Error("Sanqian is not running. Please start it manually."));
761
- }
762
- } else {
763
- this.scheduleReconnect();
764
- }
762
+ }
763
+ await this.connectWithInfo(info);
764
+ }
765
+ /**
766
+ * Wait for Sanqian to start and connection.json to become available
767
+ */
768
+ async waitForSanqianStartup(timeout = 12e4) {
769
+ const startTime = Date.now();
770
+ const pollInterval = 500;
771
+ while (Date.now() - startTime < timeout) {
772
+ const info = this.discovery.read();
773
+ if (info) {
774
+ console.log("[SDK] Sanqian started, connection info available");
775
+ return info;
765
776
  }
766
- });
777
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
778
+ }
779
+ throw new Error(
780
+ "Sanqian startup timeout (2 minutes). Please check if Sanqian started correctly."
781
+ );
767
782
  }
768
783
  /**
769
784
  * Update tool list dynamically
@@ -800,9 +815,7 @@ var SanqianSDK = class _SanqianSDK {
800
815
  * @returns Full agent info (or agent_id string for backward compatibility)
801
816
  */
802
817
  async createAgent(config) {
803
- if (!this.isConnected()) {
804
- throw new Error("Not connected to Sanqian");
805
- }
818
+ await this.ensureReady();
806
819
  const msgId = this.generateId();
807
820
  const message = {
808
821
  id: msgId,
@@ -827,9 +840,7 @@ var SanqianSDK = class _SanqianSDK {
827
840
  * List all private agents owned by this app
828
841
  */
829
842
  async listAgents() {
830
- if (!this.isConnected()) {
831
- throw new Error("Not connected to Sanqian");
832
- }
843
+ await this.ensureReady();
833
844
  const msgId = this.generateId();
834
845
  const message = {
835
846
  id: msgId,
@@ -845,9 +856,7 @@ var SanqianSDK = class _SanqianSDK {
845
856
  * Delete a private agent
846
857
  */
847
858
  async deleteAgent(agentId) {
848
- if (!this.isConnected()) {
849
- throw new Error("Not connected to Sanqian");
850
- }
859
+ await this.ensureReady();
851
860
  const msgId = this.generateId();
852
861
  const message = {
853
862
  id: msgId,
@@ -868,9 +877,7 @@ var SanqianSDK = class _SanqianSDK {
868
877
  * @returns Updated agent info
869
878
  */
870
879
  async updateAgent(agentId, updates) {
871
- if (!this.isConnected()) {
872
- throw new Error("Not connected to Sanqian");
873
- }
880
+ await this.ensureReady();
874
881
  const msgId = this.generateId();
875
882
  const message = {
876
883
  id: msgId,
@@ -891,9 +898,7 @@ var SanqianSDK = class _SanqianSDK {
891
898
  * List conversations for this app
892
899
  */
893
900
  async listConversations(options) {
894
- if (!this.isConnected()) {
895
- throw new Error("Not connected to Sanqian");
896
- }
901
+ await this.ensureReady();
897
902
  const msgId = this.generateId();
898
903
  const message = {
899
904
  id: msgId,
@@ -915,9 +920,7 @@ var SanqianSDK = class _SanqianSDK {
915
920
  * Get conversation details with messages
916
921
  */
917
922
  async getConversation(conversationId, options) {
918
- if (!this.isConnected()) {
919
- throw new Error("Not connected to Sanqian");
920
- }
923
+ await this.ensureReady();
921
924
  const msgId = this.generateId();
922
925
  const message = {
923
926
  id: msgId,
@@ -937,9 +940,7 @@ var SanqianSDK = class _SanqianSDK {
937
940
  * Delete a conversation
938
941
  */
939
942
  async deleteConversation(conversationId) {
940
- if (!this.isConnected()) {
941
- throw new Error("Not connected to Sanqian");
942
- }
943
+ await this.ensureReady();
943
944
  const msgId = this.generateId();
944
945
  const message = {
945
946
  id: msgId,
@@ -969,10 +970,7 @@ var SanqianSDK = class _SanqianSDK {
969
970
  * @returns Chat response with assistant message and conversation ID
970
971
  */
971
972
  async chat(agentId, messages, options) {
972
- if (!this.isConnected()) {
973
- console.log("[SDK] Not connected, attempting to reconnect...");
974
- await this.waitForConnection();
975
- }
973
+ await this.ensureReady();
976
974
  const msgId = this.generateId();
977
975
  const message = {
978
976
  id: msgId,
@@ -1011,10 +1009,7 @@ var SanqianSDK = class _SanqianSDK {
1011
1009
  * @returns AsyncIterable of stream events
1012
1010
  */
1013
1011
  async *chatStream(agentId, messages, options) {
1014
- if (!this.isConnected()) {
1015
- console.log("[SDK] Not connected, attempting to reconnect...");
1016
- await this.waitForConnection();
1017
- }
1012
+ await this.ensureReady();
1018
1013
  const msgId = this.generateId();
1019
1014
  const message = {
1020
1015
  id: msgId,