@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.mjs CHANGED
@@ -11,6 +11,11 @@ var DiscoveryManager = class {
11
11
  watcher = null;
12
12
  onChange = null;
13
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;
14
19
  /**
15
20
  * Get the path to connection.json
16
21
  */
@@ -36,6 +41,9 @@ var DiscoveryManager = class {
36
41
  if (!info.port || !info.token || !info.pid) {
37
42
  return null;
38
43
  }
44
+ if (info.executable) {
45
+ this.cachedExecutable = info.executable;
46
+ }
39
47
  if (!this.isProcessRunning(info.pid)) {
40
48
  return null;
41
49
  }
@@ -197,9 +205,22 @@ var DiscoveryManager = class {
197
205
  /**
198
206
  * Launch Sanqian in hidden/tray mode
199
207
  * Returns true if launch was initiated successfully
208
+ *
209
+ * Priority for finding Sanqian executable:
210
+ * 1. customPath parameter (if provided)
211
+ * 2. Cached executable from connection.json (most reliable)
212
+ * 3. Search in standard installation locations (fallback)
200
213
  */
201
214
  launchSanqian(customPath) {
202
- const sanqianPath = this.findSanqianPath(customPath);
215
+ let sanqianPath = null;
216
+ if (customPath) {
217
+ sanqianPath = this.findSanqianPath(customPath);
218
+ } else if (this.cachedExecutable && existsSync(this.cachedExecutable)) {
219
+ sanqianPath = this.cachedExecutable;
220
+ console.log(`[SDK] Using cached executable: ${sanqianPath}`);
221
+ } else {
222
+ sanqianPath = this.findSanqianPath();
223
+ }
203
224
  if (!sanqianPath) {
204
225
  console.error("[SDK] Sanqian executable not found");
205
226
  return false;
@@ -263,6 +284,8 @@ var SanqianSDK = class _SanqianSDK {
263
284
  heartbeatAckPending = false;
264
285
  missedHeartbeats = 0;
265
286
  static MAX_MISSED_HEARTBEATS = 2;
287
+ // Connection promise for deduplication (prevents multiple concurrent connect attempts)
288
+ connectingPromise = null;
266
289
  // Event listeners
267
290
  eventListeners = /* @__PURE__ */ new Map();
268
291
  constructor(config) {
@@ -288,40 +311,12 @@ var SanqianSDK = class _SanqianSDK {
288
311
  *
289
312
  * If autoLaunchSanqian is enabled and Sanqian is not running,
290
313
  * SDK will attempt to start it in hidden/tray mode.
314
+ *
315
+ * @throws Error if Sanqian executable cannot be found (when autoLaunchSanqian is enabled)
316
+ * @throws Error if connection times out after 2 minutes
291
317
  */
292
318
  async connect() {
293
- const info = this.discovery.read();
294
- if (!info) {
295
- if (this.config.autoLaunchSanqian) {
296
- console.log("[SDK] Sanqian not running, attempting to launch...");
297
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
298
- if (launched) {
299
- console.log("[SDK] Sanqian launch initiated, waiting for startup...");
300
- } else {
301
- console.warn("[SDK] Failed to launch Sanqian, will wait for manual start");
302
- }
303
- }
304
- return new Promise((resolve, reject) => {
305
- console.log("[SDK] Waiting for Sanqian...");
306
- const timeout = setTimeout(() => {
307
- this.discovery.stopWatching();
308
- reject(new Error("Sanqian connection timeout"));
309
- }, 6e4);
310
- this.discovery.startWatching(async (newInfo) => {
311
- if (newInfo) {
312
- clearTimeout(timeout);
313
- this.discovery.stopWatching();
314
- try {
315
- await this.connectWithInfo(newInfo);
316
- resolve();
317
- } catch (e) {
318
- reject(e);
319
- }
320
- }
321
- });
322
- });
323
- }
324
- await this.connectWithInfo(info);
319
+ return this.ensureReady();
325
320
  }
326
321
  /**
327
322
  * Connect with known connection info
@@ -675,57 +670,77 @@ var SanqianSDK = class _SanqianSDK {
675
670
  return this.state.connected && this.state.registered;
676
671
  }
677
672
  /**
678
- * Wait for connection to be established.
679
- * Used internally by chat() and chatStream() for auto-reconnect.
680
- * Relies on background scheduleReconnect() to do the actual reconnection.
673
+ * Ensure SDK is ready for API calls.
674
+ *
675
+ * This is the unified entry point for all API methods that require a connection.
676
+ * It handles:
677
+ * - Already connected: returns immediately
678
+ * - Connection in progress: waits for existing attempt (deduplication)
679
+ * - Not connected: initiates connection (with optional auto-launch)
680
+ *
681
+ * Design pattern: gRPC wait-for-ready + ClientFactory promise deduplication
682
+ *
683
+ * @throws Error if connection fails or times out
681
684
  */
682
- async waitForConnection(timeout = 3e4) {
685
+ async ensureReady() {
683
686
  if (this.isConnected()) {
684
687
  return;
685
688
  }
686
- console.log("[SDK] Not connected, waiting for reconnection...");
687
- return new Promise((resolve, reject) => {
688
- let resolved = false;
689
- const timer = setTimeout(() => {
690
- if (!resolved) {
691
- resolved = true;
692
- reject(new Error("Connection timeout. Please ensure Sanqian is running."));
693
- }
694
- }, timeout);
695
- this.once("registered", () => {
696
- if (!resolved) {
697
- resolved = true;
698
- clearTimeout(timer);
699
- resolve();
700
- }
701
- });
702
- if (this.isConnected()) {
703
- if (!resolved) {
704
- resolved = true;
705
- clearTimeout(timer);
706
- resolve();
689
+ if (this.connectingPromise) {
690
+ console.log("[SDK] Connection already in progress, waiting...");
691
+ return this.connectingPromise;
692
+ }
693
+ this.connectingPromise = this.doFullConnect();
694
+ try {
695
+ await this.connectingPromise;
696
+ } finally {
697
+ this.connectingPromise = null;
698
+ }
699
+ }
700
+ /**
701
+ * Internal: Perform full connection flow
702
+ * Handles launching Sanqian if needed, then connecting and registering.
703
+ *
704
+ * Note: This method is protected by connectingPromise deduplication in ensureReady(),
705
+ * so there's no need for additional launch-in-progress tracking within a single instance.
706
+ */
707
+ async doFullConnect() {
708
+ console.log("[SDK] Starting full connection flow...");
709
+ let info = this.discovery.read();
710
+ if (!info) {
711
+ if (this.config.autoLaunchSanqian) {
712
+ console.log("[SDK] Sanqian not running, attempting to launch...");
713
+ const launched = this.discovery.launchSanqian(this.config.sanqianPath);
714
+ if (!launched) {
715
+ throw new Error(
716
+ "Sanqian executable not found. Please ensure Sanqian is installed, or provide a custom path via sanqianPath config option."
717
+ );
707
718
  }
708
- return;
719
+ console.log("[SDK] Sanqian launch initiated, waiting for startup...");
720
+ info = await this.waitForSanqianStartup();
721
+ } else {
722
+ throw new Error("Sanqian is not running. Please start it manually.");
709
723
  }
710
- if (!this.reconnectTimer) {
711
- const info = this.discovery.read();
712
- if (!info && this.config.autoLaunchSanqian) {
713
- console.log("[SDK] Sanqian not running, attempting to launch...");
714
- const launched = this.discovery.launchSanqian(this.config.sanqianPath);
715
- if (launched) {
716
- this.scheduleReconnect();
717
- }
718
- } else if (!info) {
719
- if (!resolved) {
720
- resolved = true;
721
- clearTimeout(timer);
722
- reject(new Error("Sanqian is not running. Please start it manually."));
723
- }
724
- } else {
725
- this.scheduleReconnect();
726
- }
724
+ }
725
+ await this.connectWithInfo(info);
726
+ }
727
+ /**
728
+ * Wait for Sanqian to start and connection.json to become available
729
+ */
730
+ async waitForSanqianStartup(timeout = 12e4) {
731
+ const startTime = Date.now();
732
+ const pollInterval = 500;
733
+ while (Date.now() - startTime < timeout) {
734
+ const info = this.discovery.read();
735
+ if (info) {
736
+ console.log("[SDK] Sanqian started, connection info available");
737
+ return info;
727
738
  }
728
- });
739
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
740
+ }
741
+ throw new Error(
742
+ "Sanqian startup timeout (2 minutes). Please check if Sanqian started correctly."
743
+ );
729
744
  }
730
745
  /**
731
746
  * Update tool list dynamically
@@ -762,9 +777,7 @@ var SanqianSDK = class _SanqianSDK {
762
777
  * @returns Full agent info (or agent_id string for backward compatibility)
763
778
  */
764
779
  async createAgent(config) {
765
- if (!this.isConnected()) {
766
- throw new Error("Not connected to Sanqian");
767
- }
780
+ await this.ensureReady();
768
781
  const msgId = this.generateId();
769
782
  const message = {
770
783
  id: msgId,
@@ -789,9 +802,7 @@ var SanqianSDK = class _SanqianSDK {
789
802
  * List all private agents owned by this app
790
803
  */
791
804
  async listAgents() {
792
- if (!this.isConnected()) {
793
- throw new Error("Not connected to Sanqian");
794
- }
805
+ await this.ensureReady();
795
806
  const msgId = this.generateId();
796
807
  const message = {
797
808
  id: msgId,
@@ -807,9 +818,7 @@ var SanqianSDK = class _SanqianSDK {
807
818
  * Delete a private agent
808
819
  */
809
820
  async deleteAgent(agentId) {
810
- if (!this.isConnected()) {
811
- throw new Error("Not connected to Sanqian");
812
- }
821
+ await this.ensureReady();
813
822
  const msgId = this.generateId();
814
823
  const message = {
815
824
  id: msgId,
@@ -830,9 +839,7 @@ var SanqianSDK = class _SanqianSDK {
830
839
  * @returns Updated agent info
831
840
  */
832
841
  async updateAgent(agentId, updates) {
833
- if (!this.isConnected()) {
834
- throw new Error("Not connected to Sanqian");
835
- }
842
+ await this.ensureReady();
836
843
  const msgId = this.generateId();
837
844
  const message = {
838
845
  id: msgId,
@@ -853,9 +860,7 @@ var SanqianSDK = class _SanqianSDK {
853
860
  * List conversations for this app
854
861
  */
855
862
  async listConversations(options) {
856
- if (!this.isConnected()) {
857
- throw new Error("Not connected to Sanqian");
858
- }
863
+ await this.ensureReady();
859
864
  const msgId = this.generateId();
860
865
  const message = {
861
866
  id: msgId,
@@ -877,9 +882,7 @@ var SanqianSDK = class _SanqianSDK {
877
882
  * Get conversation details with messages
878
883
  */
879
884
  async getConversation(conversationId, options) {
880
- if (!this.isConnected()) {
881
- throw new Error("Not connected to Sanqian");
882
- }
885
+ await this.ensureReady();
883
886
  const msgId = this.generateId();
884
887
  const message = {
885
888
  id: msgId,
@@ -899,9 +902,7 @@ var SanqianSDK = class _SanqianSDK {
899
902
  * Delete a conversation
900
903
  */
901
904
  async deleteConversation(conversationId) {
902
- if (!this.isConnected()) {
903
- throw new Error("Not connected to Sanqian");
904
- }
905
+ await this.ensureReady();
905
906
  const msgId = this.generateId();
906
907
  const message = {
907
908
  id: msgId,
@@ -931,10 +932,7 @@ var SanqianSDK = class _SanqianSDK {
931
932
  * @returns Chat response with assistant message and conversation ID
932
933
  */
933
934
  async chat(agentId, messages, options) {
934
- if (!this.isConnected()) {
935
- console.log("[SDK] Not connected, attempting to reconnect...");
936
- await this.waitForConnection();
937
- }
935
+ await this.ensureReady();
938
936
  const msgId = this.generateId();
939
937
  const message = {
940
938
  id: msgId,
@@ -973,10 +971,7 @@ var SanqianSDK = class _SanqianSDK {
973
971
  * @returns AsyncIterable of stream events
974
972
  */
975
973
  async *chatStream(agentId, messages, options) {
976
- if (!this.isConnected()) {
977
- console.log("[SDK] Not connected, attempting to reconnect...");
978
- await this.waitForConnection();
979
- }
974
+ await this.ensureReady();
980
975
  const msgId = this.generateId();
981
976
  const message = {
982
977
  id: msgId,