@yushaw/sanqian-sdk 0.1.0

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 ADDED
@@ -0,0 +1,1108 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Conversation: () => Conversation,
34
+ DiscoveryManager: () => DiscoveryManager,
35
+ SanqianSDK: () => SanqianSDK
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/client.ts
40
+ var import_ws = __toESM(require("ws"));
41
+
42
+ // src/discovery.ts
43
+ var import_fs = require("fs");
44
+ var import_os = require("os");
45
+ var import_path = require("path");
46
+ var import_child_process = require("child_process");
47
+ var DiscoveryManager = class {
48
+ connectionInfo = null;
49
+ watcher = null;
50
+ onChange = null;
51
+ pollInterval = null;
52
+ /**
53
+ * Get the path to connection.json
54
+ */
55
+ getConnectionFilePath() {
56
+ return (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime", "connection.json");
57
+ }
58
+ /**
59
+ * Read and validate connection info
60
+ *
61
+ * Returns null if:
62
+ * - File doesn't exist
63
+ * - File is invalid JSON
64
+ * - Process is not running
65
+ */
66
+ read() {
67
+ const filePath = this.getConnectionFilePath();
68
+ if (!(0, import_fs.existsSync)(filePath)) {
69
+ return null;
70
+ }
71
+ try {
72
+ const content = (0, import_fs.readFileSync)(filePath, "utf-8");
73
+ const info = JSON.parse(content);
74
+ if (!info.port || !info.token || !info.pid) {
75
+ return null;
76
+ }
77
+ if (!this.isProcessRunning(info.pid)) {
78
+ return null;
79
+ }
80
+ this.connectionInfo = info;
81
+ return info;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Start watching for connection file changes
88
+ */
89
+ startWatching(onChange) {
90
+ this.onChange = onChange;
91
+ const dir = (0, import_path.join)((0, import_os.homedir)(), ".sanqian", "runtime");
92
+ if (!(0, import_fs.existsSync)(dir)) {
93
+ this.pollInterval = setInterval(() => {
94
+ if ((0, import_fs.existsSync)(dir)) {
95
+ if (this.pollInterval) {
96
+ clearInterval(this.pollInterval);
97
+ this.pollInterval = null;
98
+ }
99
+ this.setupWatcher(dir);
100
+ }
101
+ }, 2e3);
102
+ return;
103
+ }
104
+ this.setupWatcher(dir);
105
+ }
106
+ setupWatcher(dir) {
107
+ try {
108
+ this.watcher = (0, import_fs.watch)(dir, (event, filename) => {
109
+ if (filename === "connection.json") {
110
+ setTimeout(() => {
111
+ const newInfo = this.read();
112
+ const oldInfoStr = JSON.stringify(this.connectionInfo);
113
+ const newInfoStr = JSON.stringify(newInfo);
114
+ if (oldInfoStr !== newInfoStr) {
115
+ this.connectionInfo = newInfo;
116
+ this.onChange?.(newInfo);
117
+ }
118
+ }, 100);
119
+ }
120
+ });
121
+ } catch (e) {
122
+ console.error("Failed to watch connection file:", e);
123
+ }
124
+ }
125
+ /**
126
+ * Stop watching
127
+ */
128
+ stopWatching() {
129
+ if (this.pollInterval) {
130
+ clearInterval(this.pollInterval);
131
+ this.pollInterval = null;
132
+ }
133
+ this.watcher?.close();
134
+ this.watcher = null;
135
+ this.onChange = null;
136
+ }
137
+ /**
138
+ * Check if a process is running by PID
139
+ */
140
+ isProcessRunning(pid) {
141
+ try {
142
+ process.kill(pid, 0);
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+ /**
149
+ * Get cached connection info (may be stale)
150
+ */
151
+ getCached() {
152
+ return this.connectionInfo;
153
+ }
154
+ /**
155
+ * Build WebSocket URL from connection info
156
+ */
157
+ buildWebSocketUrl(info) {
158
+ return `ws://127.0.0.1:${info.port}${info.ws_path}?token=${info.token}`;
159
+ }
160
+ /**
161
+ * Build HTTP base URL from connection info
162
+ */
163
+ buildHttpUrl(info) {
164
+ return `http://127.0.0.1:${info.port}`;
165
+ }
166
+ /**
167
+ * Find Sanqian executable path
168
+ * Searches in standard installation locations for each platform
169
+ */
170
+ findSanqianPath(customPath) {
171
+ if (customPath) {
172
+ if ((0, import_fs.existsSync)(customPath)) {
173
+ return customPath;
174
+ }
175
+ console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
176
+ return null;
177
+ }
178
+ const os = (0, import_os.platform)();
179
+ const searchPaths = [];
180
+ if (os === "darwin") {
181
+ searchPaths.push(
182
+ // Production: installed app
183
+ "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
184
+ (0, import_path.join)((0, import_os.homedir)(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
185
+ // Development: electron-builder output
186
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
187
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
188
+ );
189
+ } else if (os === "win32") {
190
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
191
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
192
+ const localAppData = process.env.LOCALAPPDATA || (0, import_path.join)((0, import_os.homedir)(), "AppData", "Local");
193
+ searchPaths.push(
194
+ // Production: installed app
195
+ (0, import_path.join)(programFiles, "Sanqian", "Sanqian.exe"),
196
+ (0, import_path.join)(programFilesX86, "Sanqian", "Sanqian.exe"),
197
+ (0, import_path.join)(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
198
+ // Development: electron-builder output
199
+ (0, import_path.join)((0, import_os.homedir)(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
200
+ );
201
+ } else {
202
+ searchPaths.push(
203
+ "/usr/bin/sanqian",
204
+ "/usr/local/bin/sanqian",
205
+ (0, import_path.join)((0, import_os.homedir)(), ".local/bin/sanqian"),
206
+ "/opt/Sanqian/sanqian",
207
+ // Development
208
+ (0, import_path.join)((0, import_os.homedir)(), "dev/sanqian/dist/linux-unpacked/sanqian")
209
+ );
210
+ }
211
+ for (const path of searchPaths) {
212
+ if ((0, import_fs.existsSync)(path)) {
213
+ return path;
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ /**
219
+ * Launch Sanqian in hidden/tray mode
220
+ * Returns true if launch was initiated successfully
221
+ */
222
+ launchSanqian(customPath) {
223
+ const sanqianPath = this.findSanqianPath(customPath);
224
+ if (!sanqianPath) {
225
+ console.error("[SDK] Sanqian executable not found");
226
+ return false;
227
+ }
228
+ console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
229
+ try {
230
+ const os = (0, import_os.platform)();
231
+ if (os === "darwin") {
232
+ const appPath = sanqianPath.replace(
233
+ "/Contents/MacOS/Sanqian",
234
+ ""
235
+ );
236
+ (0, import_child_process.spawn)("open", ["-a", appPath, "--args", "--hidden"], {
237
+ detached: true,
238
+ stdio: "ignore"
239
+ }).unref();
240
+ } else {
241
+ (0, import_child_process.spawn)(sanqianPath, ["--hidden"], {
242
+ detached: true,
243
+ stdio: "ignore",
244
+ // On Windows, hide the console window
245
+ ...os === "win32" && {
246
+ windowsHide: true,
247
+ shell: false
248
+ }
249
+ }).unref();
250
+ }
251
+ return true;
252
+ } catch (e) {
253
+ console.error("[SDK] Failed to launch Sanqian:", e);
254
+ return false;
255
+ }
256
+ }
257
+ /**
258
+ * Check if Sanqian is running
259
+ */
260
+ isSanqianRunning() {
261
+ return this.read() !== null;
262
+ }
263
+ };
264
+
265
+ // src/client.ts
266
+ var SanqianSDK = class {
267
+ config;
268
+ discovery;
269
+ ws = null;
270
+ connectionInfo = null;
271
+ state = {
272
+ connected: false,
273
+ registering: false,
274
+ registered: false,
275
+ reconnectAttempts: 0
276
+ };
277
+ // Tool handlers by name
278
+ toolHandlers = /* @__PURE__ */ new Map();
279
+ // Pending request futures
280
+ pendingRequests = /* @__PURE__ */ new Map();
281
+ // Timers
282
+ heartbeatTimer = null;
283
+ reconnectTimer = null;
284
+ // Event listeners
285
+ eventListeners = /* @__PURE__ */ new Map();
286
+ constructor(config) {
287
+ this.config = {
288
+ reconnectInterval: 5e3,
289
+ heartbeatInterval: 3e4,
290
+ toolExecutionTimeout: 3e4,
291
+ ...config
292
+ };
293
+ this.discovery = new DiscoveryManager();
294
+ for (const tool of config.tools) {
295
+ this.toolHandlers.set(tool.name, tool.handler);
296
+ }
297
+ }
298
+ // ============================================
299
+ // Lifecycle
300
+ // ============================================
301
+ /**
302
+ * Connect to Sanqian
303
+ *
304
+ * Reads connection info, establishes WebSocket, and registers app.
305
+ * Returns when registration is complete.
306
+ *
307
+ * If autoLaunchSanqian is enabled and Sanqian is not running,
308
+ * SDK will attempt to start it in hidden/tray mode.
309
+ */
310
+ async connect() {
311
+ const info = this.discovery.read();
312
+ if (!info) {
313
+ if (this.config.autoLaunchSanqian) {
314
+ console.log("[SDK] Sanqian not running, attempting to launch...");
315
+ const launched = this.discovery.launchSanqian(this.config.sanqianPath);
316
+ if (launched) {
317
+ console.log("[SDK] Sanqian launch initiated, waiting for startup...");
318
+ } else {
319
+ console.warn("[SDK] Failed to launch Sanqian, will wait for manual start");
320
+ }
321
+ }
322
+ return new Promise((resolve, reject) => {
323
+ console.log("[SDK] Waiting for Sanqian...");
324
+ const timeout = setTimeout(() => {
325
+ this.discovery.stopWatching();
326
+ reject(new Error("Sanqian connection timeout"));
327
+ }, 6e4);
328
+ this.discovery.startWatching(async (newInfo) => {
329
+ if (newInfo) {
330
+ clearTimeout(timeout);
331
+ this.discovery.stopWatching();
332
+ try {
333
+ await this.connectWithInfo(newInfo);
334
+ resolve();
335
+ } catch (e) {
336
+ reject(e);
337
+ }
338
+ }
339
+ });
340
+ });
341
+ }
342
+ await this.connectWithInfo(info);
343
+ }
344
+ /**
345
+ * Connect with known connection info
346
+ */
347
+ async connectWithInfo(info) {
348
+ this.connectionInfo = info;
349
+ const url = this.discovery.buildWebSocketUrl(info);
350
+ return new Promise((resolve, reject) => {
351
+ console.log(`[SDK] Connecting to ${url}`);
352
+ this.ws = new import_ws.default(url);
353
+ const connectTimeout = setTimeout(() => {
354
+ reject(new Error("WebSocket connection timeout"));
355
+ this.ws?.close();
356
+ }, 1e4);
357
+ this.ws.on("open", async () => {
358
+ clearTimeout(connectTimeout);
359
+ console.log("[SDK] WebSocket connected");
360
+ this.state.connected = true;
361
+ this.state.reconnectAttempts = 0;
362
+ this.emit("connected");
363
+ try {
364
+ await this.register();
365
+ resolve();
366
+ } catch (e) {
367
+ reject(e);
368
+ }
369
+ });
370
+ this.ws.on("close", (code, reason) => {
371
+ console.log(`[SDK] WebSocket closed: ${code} ${reason.toString()}`);
372
+ this.handleDisconnect(reason.toString());
373
+ });
374
+ this.ws.on("error", (error) => {
375
+ console.error("[SDK] WebSocket error:", error);
376
+ this.state.lastError = error;
377
+ this.emit("error", error);
378
+ });
379
+ this.ws.on("message", (data) => {
380
+ try {
381
+ const message = JSON.parse(data.toString());
382
+ this.handleMessage(message);
383
+ } catch (e) {
384
+ console.error("[SDK] Failed to parse message:", e);
385
+ }
386
+ });
387
+ });
388
+ }
389
+ /**
390
+ * Disconnect from Sanqian
391
+ */
392
+ async disconnect() {
393
+ this.stopHeartbeat();
394
+ this.stopReconnect();
395
+ this.discovery.stopWatching();
396
+ if (this.ws) {
397
+ this.ws.close(1e3, "Client disconnect");
398
+ this.ws = null;
399
+ }
400
+ this.state = {
401
+ connected: false,
402
+ registering: false,
403
+ registered: false,
404
+ reconnectAttempts: 0
405
+ };
406
+ }
407
+ // ============================================
408
+ // Registration
409
+ // ============================================
410
+ async register() {
411
+ this.state.registering = true;
412
+ const msgId = this.generateId();
413
+ const message = {
414
+ id: msgId,
415
+ type: "register",
416
+ app: {
417
+ name: this.config.appName,
418
+ version: this.config.appVersion,
419
+ display_name: this.config.displayName,
420
+ launch_command: this.config.launchCommand
421
+ },
422
+ tools: this.config.tools.map((t) => ({
423
+ name: `${this.config.appName}:${t.name}`,
424
+ description: t.description,
425
+ parameters: t.parameters
426
+ }))
427
+ };
428
+ try {
429
+ const response = await this.sendAndWait(
430
+ message,
431
+ msgId,
432
+ 1e4
433
+ );
434
+ if (!response.success) {
435
+ throw new Error(response.error || "Registration failed");
436
+ }
437
+ this.state.registering = false;
438
+ this.state.registered = true;
439
+ this.startHeartbeat();
440
+ this.emit("registered");
441
+ console.log(`[SDK] Registered as '${this.config.appName}'`);
442
+ } catch (e) {
443
+ this.state.registering = false;
444
+ throw e;
445
+ }
446
+ }
447
+ // ============================================
448
+ // Message Handling
449
+ // ============================================
450
+ handleMessage(message) {
451
+ const { id, type } = message;
452
+ if (id && this.pendingRequests.has(id)) {
453
+ const pending = this.pendingRequests.get(id);
454
+ this.pendingRequests.delete(id);
455
+ pending.resolve(message);
456
+ return;
457
+ }
458
+ switch (type) {
459
+ case "tool_call":
460
+ this.handleToolCall(message);
461
+ break;
462
+ case "heartbeat_ack":
463
+ break;
464
+ case "chat_stream":
465
+ this.handleChatStream(message);
466
+ break;
467
+ default:
468
+ console.warn(`[SDK] Unknown message type: ${type}`);
469
+ }
470
+ }
471
+ handleChatStream(message) {
472
+ const { id, event, content, tool_call, tool_result, conversation_id, usage, error } = message;
473
+ if (!id) return;
474
+ const handler = this.streamHandlers.get(id);
475
+ if (!handler) {
476
+ console.warn(`[SDK] No stream handler for message ${id}`);
477
+ return;
478
+ }
479
+ switch (event) {
480
+ case "text":
481
+ handler.onEvent({ type: "text", content });
482
+ break;
483
+ case "tool_call":
484
+ handler.onEvent({ type: "tool_call", tool_call });
485
+ break;
486
+ case "tool_result":
487
+ if (tool_result) {
488
+ handler.onEvent({
489
+ type: "tool_call",
490
+ tool_call: {
491
+ id: tool_result.call_id,
492
+ type: "function",
493
+ function: {
494
+ name: "tool_result",
495
+ arguments: JSON.stringify(tool_result)
496
+ }
497
+ }
498
+ });
499
+ }
500
+ break;
501
+ case "done":
502
+ handler.onDone({
503
+ message: message.message || { role: "assistant", content: "" },
504
+ conversationId: conversation_id || "",
505
+ usage
506
+ });
507
+ break;
508
+ case "error":
509
+ handler.onError(new Error(error || "Unknown stream error"));
510
+ break;
511
+ }
512
+ }
513
+ async handleToolCall(message) {
514
+ console.log(`[SDK] handleToolCall received:`, JSON.stringify(message));
515
+ const { id, call_id, name, arguments: args } = message;
516
+ const msgId = id || call_id;
517
+ const toolName = name.includes(":") ? name.split(":")[1] : name;
518
+ console.log(`[SDK] Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
519
+ const handler = this.toolHandlers.get(toolName);
520
+ this.emit("tool_call", { name: toolName, arguments: args });
521
+ if (!handler) {
522
+ await this.sendToolResult(msgId, call_id, false, void 0, `Tool '${toolName}' not found`);
523
+ return;
524
+ }
525
+ try {
526
+ console.log(`[SDK] Executing tool '${toolName}' with args:`, args);
527
+ const result = await Promise.race([
528
+ handler(args),
529
+ this.createTimeout(this.config.toolExecutionTimeout)
530
+ ]);
531
+ console.log(`[SDK] Tool '${toolName}' completed successfully`);
532
+ await this.sendToolResult(msgId, call_id, true, result);
533
+ } catch (e) {
534
+ const error = e instanceof Error ? e.message : String(e);
535
+ console.log(`[SDK] Tool '${toolName}' failed:`, error);
536
+ await this.sendToolResult(msgId, call_id, false, void 0, error);
537
+ }
538
+ }
539
+ async sendToolResult(id, callId, success, result, error) {
540
+ const message = {
541
+ id,
542
+ type: "tool_result",
543
+ call_id: callId,
544
+ success,
545
+ result,
546
+ error
547
+ };
548
+ console.log(`[SDK] Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
549
+ this.send(message);
550
+ }
551
+ // ============================================
552
+ // Connection Management
553
+ // ============================================
554
+ handleDisconnect(reason) {
555
+ this.stopHeartbeat();
556
+ this.state.connected = false;
557
+ this.state.registered = false;
558
+ this.emit("disconnected", reason);
559
+ for (const [, pending] of this.pendingRequests) {
560
+ pending.reject(new Error("Disconnected"));
561
+ }
562
+ this.pendingRequests.clear();
563
+ this.scheduleReconnect();
564
+ }
565
+ scheduleReconnect() {
566
+ if (this.reconnectTimer) return;
567
+ const delay = Math.min(
568
+ 1e3 * Math.pow(2, this.state.reconnectAttempts),
569
+ 3e4
570
+ );
571
+ console.log(`[SDK] Scheduling reconnect in ${delay}ms`);
572
+ this.reconnectTimer = setTimeout(async () => {
573
+ this.reconnectTimer = null;
574
+ this.state.reconnectAttempts++;
575
+ const info = this.discovery.read();
576
+ if (info) {
577
+ try {
578
+ await this.connectWithInfo(info);
579
+ } catch (e) {
580
+ console.error("[SDK] Reconnect failed:", e);
581
+ this.scheduleReconnect();
582
+ }
583
+ } else {
584
+ this.scheduleReconnect();
585
+ }
586
+ }, delay);
587
+ }
588
+ stopReconnect() {
589
+ if (this.reconnectTimer) {
590
+ clearTimeout(this.reconnectTimer);
591
+ this.reconnectTimer = null;
592
+ }
593
+ }
594
+ // ============================================
595
+ // Heartbeat
596
+ // ============================================
597
+ startHeartbeat() {
598
+ this.stopHeartbeat();
599
+ this.heartbeatTimer = setInterval(() => {
600
+ const message = {
601
+ type: "heartbeat",
602
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
603
+ };
604
+ this.send(message);
605
+ }, this.config.heartbeatInterval);
606
+ }
607
+ stopHeartbeat() {
608
+ if (this.heartbeatTimer) {
609
+ clearInterval(this.heartbeatTimer);
610
+ this.heartbeatTimer = null;
611
+ }
612
+ }
613
+ // ============================================
614
+ // Communication
615
+ // ============================================
616
+ send(message) {
617
+ if (this.ws && this.ws.readyState === import_ws.default.OPEN) {
618
+ const data = JSON.stringify(message);
619
+ console.log(`[SDK] WebSocket send:`, data.substring(0, 200));
620
+ this.ws.send(data);
621
+ } else {
622
+ console.error(`[SDK] WebSocket not open, cannot send:`, message);
623
+ }
624
+ }
625
+ sendAndWait(message, id, timeout) {
626
+ return new Promise((resolve, reject) => {
627
+ const timer = setTimeout(() => {
628
+ this.pendingRequests.delete(id);
629
+ reject(new Error("Request timeout"));
630
+ }, timeout);
631
+ this.pendingRequests.set(id, {
632
+ resolve: (value) => {
633
+ clearTimeout(timer);
634
+ resolve(value);
635
+ },
636
+ reject: (error) => {
637
+ clearTimeout(timer);
638
+ reject(error);
639
+ }
640
+ });
641
+ this.send(message);
642
+ });
643
+ }
644
+ // ============================================
645
+ // Public API
646
+ // ============================================
647
+ /**
648
+ * Get current connection state
649
+ */
650
+ getState() {
651
+ return { ...this.state };
652
+ }
653
+ /**
654
+ * Check if connected and registered
655
+ */
656
+ isConnected() {
657
+ return this.state.connected && this.state.registered;
658
+ }
659
+ /**
660
+ * Update tool list dynamically
661
+ */
662
+ async updateTools(tools) {
663
+ this.toolHandlers.clear();
664
+ for (const tool of tools) {
665
+ this.toolHandlers.set(tool.name, tool.handler);
666
+ }
667
+ if (this.isConnected()) {
668
+ const msgId = this.generateId();
669
+ const message = {
670
+ id: msgId,
671
+ type: "tools_update",
672
+ tools: tools.map((t) => ({
673
+ name: `${this.config.appName}:${t.name}`,
674
+ description: t.description,
675
+ parameters: t.parameters
676
+ }))
677
+ };
678
+ await this.sendAndWait(message, msgId, 5e3);
679
+ }
680
+ }
681
+ // ============================================
682
+ // Private Agent API
683
+ // ============================================
684
+ /**
685
+ * Create or update a private agent
686
+ *
687
+ * Private agents are only visible to this SDK app, not in Sanqian UI.
688
+ * If agent with same ID exists, it will be updated.
689
+ *
690
+ * @param config - Agent configuration
691
+ * @returns Full agent info (or agent_id string for backward compatibility)
692
+ */
693
+ async createAgent(config) {
694
+ if (!this.isConnected()) {
695
+ throw new Error("Not connected to Sanqian");
696
+ }
697
+ const msgId = this.generateId();
698
+ const message = {
699
+ id: msgId,
700
+ type: "create_agent",
701
+ agent: config
702
+ };
703
+ const response = await this.sendAndWait(message, msgId, 1e4);
704
+ if (!response.success) {
705
+ throw new Error(response.error || "Failed to create agent");
706
+ }
707
+ if (response.agent) {
708
+ return response.agent;
709
+ }
710
+ return {
711
+ agent_id: response.agent_id,
712
+ name: config.name,
713
+ description: config.description,
714
+ tools: config.tools || []
715
+ };
716
+ }
717
+ /**
718
+ * List all private agents owned by this app
719
+ */
720
+ async listAgents() {
721
+ if (!this.isConnected()) {
722
+ throw new Error("Not connected to Sanqian");
723
+ }
724
+ const msgId = this.generateId();
725
+ const message = {
726
+ id: msgId,
727
+ type: "list_agents"
728
+ };
729
+ const response = await this.sendAndWait(message, msgId, 1e4);
730
+ if (response.error) {
731
+ throw new Error(response.error);
732
+ }
733
+ return response.agents;
734
+ }
735
+ /**
736
+ * Delete a private agent
737
+ */
738
+ async deleteAgent(agentId) {
739
+ if (!this.isConnected()) {
740
+ throw new Error("Not connected to Sanqian");
741
+ }
742
+ const msgId = this.generateId();
743
+ const message = {
744
+ id: msgId,
745
+ type: "delete_agent",
746
+ agent_id: agentId
747
+ };
748
+ const response = await this.sendAndWait(message, msgId, 1e4);
749
+ if (!response.success) {
750
+ throw new Error(response.error || "Failed to delete agent");
751
+ }
752
+ }
753
+ /**
754
+ * Update a private agent
755
+ *
756
+ * Only updates the fields that are provided.
757
+ * @param agentId - Agent ID (short name or full)
758
+ * @param updates - Fields to update
759
+ * @returns Updated agent info
760
+ */
761
+ async updateAgent(agentId, updates) {
762
+ if (!this.isConnected()) {
763
+ throw new Error("Not connected to Sanqian");
764
+ }
765
+ const msgId = this.generateId();
766
+ const message = {
767
+ id: msgId,
768
+ type: "update_agent",
769
+ agent: {
770
+ agent_id: agentId,
771
+ ...updates
772
+ }
773
+ };
774
+ const response = await this.sendAndWait(message, msgId, 1e4);
775
+ if (!response.success || !response.agent) {
776
+ throw new Error(response.error || "Failed to update agent");
777
+ }
778
+ return response.agent;
779
+ }
780
+ // ============================================
781
+ // Conversation API
782
+ // ============================================
783
+ /**
784
+ * List conversations for this app
785
+ */
786
+ async listConversations(options) {
787
+ if (!this.isConnected()) {
788
+ throw new Error("Not connected to Sanqian");
789
+ }
790
+ const msgId = this.generateId();
791
+ const message = {
792
+ id: msgId,
793
+ type: "list_conversations",
794
+ agent_id: options?.agentId,
795
+ limit: options?.limit,
796
+ offset: options?.offset
797
+ };
798
+ const response = await this.sendAndWait(message, msgId, 1e4);
799
+ if (response.error) {
800
+ throw new Error(response.error);
801
+ }
802
+ return {
803
+ conversations: response.conversations,
804
+ total: response.total
805
+ };
806
+ }
807
+ /**
808
+ * Get conversation details with messages
809
+ */
810
+ async getConversation(conversationId, options) {
811
+ if (!this.isConnected()) {
812
+ throw new Error("Not connected to Sanqian");
813
+ }
814
+ const msgId = this.generateId();
815
+ const message = {
816
+ id: msgId,
817
+ type: "get_conversation",
818
+ conversation_id: conversationId,
819
+ include_messages: options?.includeMessages ?? true,
820
+ message_limit: options?.messageLimit,
821
+ message_offset: options?.messageOffset
822
+ };
823
+ const response = await this.sendAndWait(message, msgId, 1e4);
824
+ if (!response.success || !response.conversation) {
825
+ throw new Error(response.error || "Failed to get conversation");
826
+ }
827
+ return response.conversation;
828
+ }
829
+ /**
830
+ * Delete a conversation
831
+ */
832
+ async deleteConversation(conversationId) {
833
+ if (!this.isConnected()) {
834
+ throw new Error("Not connected to Sanqian");
835
+ }
836
+ const msgId = this.generateId();
837
+ const message = {
838
+ id: msgId,
839
+ type: "delete_conversation",
840
+ conversation_id: conversationId
841
+ };
842
+ const response = await this.sendAndWait(message, msgId, 1e4);
843
+ if (!response.success) {
844
+ throw new Error(response.error || "Failed to delete conversation");
845
+ }
846
+ }
847
+ // ============================================
848
+ // Chat API
849
+ // ============================================
850
+ // Pending stream handlers for streaming chat
851
+ streamHandlers = /* @__PURE__ */ new Map();
852
+ /**
853
+ * Send a chat message to an agent (non-streaming)
854
+ *
855
+ * Supports two modes:
856
+ * - Stateless (no conversationId): Caller maintains message history
857
+ * - Stateful (with conversationId): Server stores messages in conversations table
858
+ *
859
+ * @param agentId - Agent ID (short name or full, SDK auto-prefixes)
860
+ * @param messages - Messages to send
861
+ * @param options - Optional settings including conversationId and remoteTools
862
+ * @returns Chat response with assistant message and conversation ID
863
+ */
864
+ async chat(agentId, messages, options) {
865
+ if (!this.isConnected()) {
866
+ throw new Error("Not connected to Sanqian");
867
+ }
868
+ const msgId = this.generateId();
869
+ const message = {
870
+ id: msgId,
871
+ type: "chat",
872
+ agent_id: agentId,
873
+ messages,
874
+ conversation_id: options?.conversationId,
875
+ stream: false,
876
+ remote_tools: options?.remoteTools?.map((t) => ({
877
+ name: t.name,
878
+ description: t.description,
879
+ parameters: t.parameters
880
+ }))
881
+ };
882
+ const response = await this.sendAndWait(message, msgId, 6e5);
883
+ if (!response.success) {
884
+ throw new Error(response.error || "Chat request failed");
885
+ }
886
+ return {
887
+ message: response.message,
888
+ conversationId: response.conversation_id,
889
+ usage: response.usage
890
+ };
891
+ }
892
+ /**
893
+ * Send a chat message to an agent with streaming response
894
+ *
895
+ * Supports two modes:
896
+ * - Stateless (no conversationId): Caller maintains message history
897
+ * - Stateful (with conversationId): Server stores messages in conversations table
898
+ *
899
+ * @param agentId - Agent ID (short name or full, SDK auto-prefixes)
900
+ * @param messages - Messages to send
901
+ * @param options - Optional settings including conversationId and remoteTools
902
+ * @returns AsyncIterable of stream events
903
+ */
904
+ async *chatStream(agentId, messages, options) {
905
+ if (!this.isConnected()) {
906
+ throw new Error("Not connected to Sanqian");
907
+ }
908
+ const msgId = this.generateId();
909
+ const message = {
910
+ id: msgId,
911
+ type: "chat",
912
+ agent_id: agentId,
913
+ messages,
914
+ conversation_id: options?.conversationId,
915
+ stream: true,
916
+ remote_tools: options?.remoteTools?.map((t) => ({
917
+ name: t.name,
918
+ description: t.description,
919
+ parameters: t.parameters
920
+ }))
921
+ };
922
+ const eventQueue = [];
923
+ let done = false;
924
+ let error = null;
925
+ let resolveNext = null;
926
+ this.streamHandlers.set(msgId, {
927
+ onEvent: (event) => {
928
+ eventQueue.push(event);
929
+ resolveNext?.();
930
+ },
931
+ onDone: (response) => {
932
+ eventQueue.push({
933
+ type: "done",
934
+ conversationId: response.conversationId
935
+ });
936
+ done = true;
937
+ resolveNext?.();
938
+ },
939
+ onError: (e) => {
940
+ error = e;
941
+ resolveNext?.();
942
+ }
943
+ });
944
+ try {
945
+ this.send(message);
946
+ while (!done && !error) {
947
+ if (eventQueue.length > 0) {
948
+ yield eventQueue.shift();
949
+ } else {
950
+ await new Promise((resolve) => {
951
+ resolveNext = resolve;
952
+ });
953
+ resolveNext = null;
954
+ }
955
+ }
956
+ while (eventQueue.length > 0) {
957
+ yield eventQueue.shift();
958
+ }
959
+ if (error) {
960
+ throw error;
961
+ }
962
+ } finally {
963
+ this.streamHandlers.delete(msgId);
964
+ }
965
+ }
966
+ /**
967
+ * Start a new conversation with an agent
968
+ *
969
+ * Returns a Conversation object for convenient multi-turn chat.
970
+ *
971
+ * @example
972
+ * ```typescript
973
+ * const conv = sdk.startConversation('assistant')
974
+ * const r1 = await conv.send('Hello')
975
+ * const r2 = await conv.send('Follow up question')
976
+ * console.log(conv.id) // conversation ID
977
+ * ```
978
+ */
979
+ startConversation(agentId) {
980
+ return new Conversation(this, agentId);
981
+ }
982
+ // ============================================
983
+ // Events
984
+ // ============================================
985
+ /**
986
+ * Add event listener
987
+ */
988
+ on(event, listener) {
989
+ if (!this.eventListeners.has(event)) {
990
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
991
+ }
992
+ this.eventListeners.get(event).add(listener);
993
+ return this;
994
+ }
995
+ /**
996
+ * Remove event listener
997
+ */
998
+ off(event, listener) {
999
+ this.eventListeners.get(event)?.delete(listener);
1000
+ return this;
1001
+ }
1002
+ emit(event, ...args) {
1003
+ const listeners = this.eventListeners.get(event);
1004
+ if (listeners) {
1005
+ for (const listener of listeners) {
1006
+ try {
1007
+ listener(...args);
1008
+ } catch (e) {
1009
+ console.error(`[SDK] Event listener error for '${event}':`, e);
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ // ============================================
1015
+ // Utilities
1016
+ // ============================================
1017
+ generateId() {
1018
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1019
+ }
1020
+ createTimeout(ms) {
1021
+ return new Promise((_, reject) => {
1022
+ setTimeout(() => reject(new Error("Timeout")), ms);
1023
+ });
1024
+ }
1025
+ };
1026
+ var Conversation = class {
1027
+ sdk;
1028
+ agentId;
1029
+ _conversationId = null;
1030
+ constructor(sdk, agentId, conversationId) {
1031
+ this.sdk = sdk;
1032
+ this.agentId = agentId;
1033
+ this._conversationId = conversationId || null;
1034
+ }
1035
+ /**
1036
+ * Get the conversation ID (available after first message)
1037
+ */
1038
+ get id() {
1039
+ return this._conversationId;
1040
+ }
1041
+ /**
1042
+ * Send a message and get a response
1043
+ *
1044
+ * First call creates a new conversation, subsequent calls continue it.
1045
+ */
1046
+ async send(content, options) {
1047
+ const response = await this.sdk.chat(
1048
+ this.agentId,
1049
+ [{ role: "user", content }],
1050
+ {
1051
+ conversationId: this._conversationId || void 0,
1052
+ remoteTools: options?.remoteTools
1053
+ }
1054
+ );
1055
+ if (response.conversationId && !this._conversationId) {
1056
+ this._conversationId = response.conversationId;
1057
+ }
1058
+ return response;
1059
+ }
1060
+ /**
1061
+ * Send a message with streaming response
1062
+ */
1063
+ async *sendStream(content, options) {
1064
+ const stream = this.sdk.chatStream(
1065
+ this.agentId,
1066
+ [{ role: "user", content }],
1067
+ {
1068
+ conversationId: this._conversationId || void 0,
1069
+ remoteTools: options?.remoteTools
1070
+ }
1071
+ );
1072
+ for await (const event of stream) {
1073
+ if (event.type === "done" && event.conversationId && !this._conversationId) {
1074
+ this._conversationId = event.conversationId;
1075
+ }
1076
+ yield event;
1077
+ }
1078
+ }
1079
+ /**
1080
+ * Delete this conversation
1081
+ */
1082
+ async delete() {
1083
+ if (!this._conversationId) {
1084
+ throw new Error("No conversation to delete");
1085
+ }
1086
+ await this.sdk.deleteConversation(this._conversationId);
1087
+ this._conversationId = null;
1088
+ }
1089
+ /**
1090
+ * Get conversation details including message history
1091
+ */
1092
+ async getDetails(options) {
1093
+ if (!this._conversationId) {
1094
+ throw new Error("No conversation to get details for");
1095
+ }
1096
+ return this.sdk.getConversation(this._conversationId, {
1097
+ includeMessages: true,
1098
+ messageLimit: options?.messageLimit
1099
+ });
1100
+ }
1101
+ };
1102
+ // Annotate the CommonJS export names for ESM import in node:
1103
+ 0 && (module.exports = {
1104
+ Conversation,
1105
+ DiscoveryManager,
1106
+ SanqianSDK
1107
+ });
1108
+ //# sourceMappingURL=index.js.map