@xiashe/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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/index.mjs +845 -0
  3. package/package.json +15 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @xiashe/sdk
2
+
3
+ Shared Node SDK for XiaShe CLI, Agent Gateway clients, local agents, and operational scripts.
4
+
5
+ The SDK wraps:
6
+
7
+ - Convex auth and user-scoped function calls.
8
+ - Skill catalog and background install jobs.
9
+ - Claw connection pairing helpers.
10
+ - Agent Gateway claim, heartbeat, task, and MCP calls.
11
+ - Unified file upload/listing.
12
+
13
+ It stores local CLI state in `~/.xiashe/config.json` by default.
package/index.mjs ADDED
@@ -0,0 +1,845 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, writeFile, stat } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { ConvexHttpClient } from 'convex/browser';
7
+ import { anyApi as api } from 'convex/server';
8
+
9
+ export const VERSION = '0.1.0';
10
+
11
+ export const DEFAULTS = {
12
+ cn: {
13
+ market: 'cn',
14
+ locale: 'zh-CN',
15
+ convexUrl: 'https://api.xiashe.chat',
16
+ actionsUrl: 'https://actions.xiashe.chat',
17
+ mcpUrl: 'https://actions.xiashe.chat/mcp/xiashe',
18
+ agentClaimUrl: 'https://actions.xiashe.chat/agent-gateway/claim',
19
+ relayUrl: 'https://relay.xiashe.chat'
20
+ },
21
+ global: {
22
+ market: 'intl',
23
+ locale: 'en',
24
+ convexUrl: 'https://adept-porcupine-970.convex.cloud',
25
+ actionsUrl: 'https://actions.agentpie.app',
26
+ mcpUrl: 'https://actions.agentpie.app/mcp/xiashe',
27
+ agentClaimUrl: 'https://actions.agentpie.app/agent-gateway/claim',
28
+ relayUrl: 'https://relay.agentpie.app'
29
+ }
30
+ };
31
+
32
+ const DEFAULT_ENV = process.env.XIASHE_DEFAULT_ENV === 'global' || process.env.XIASHE_DEFAULT_ENV === 'intl'
33
+ ? 'global'
34
+ : 'cn';
35
+ const DEFAULT_PRODUCT_NAME = process.env.XIASHE_PRODUCT_NAME || (DEFAULT_ENV === 'global' ? 'AgentPie' : 'XiaShe');
36
+ const DEFAULT_CLI_NAME = process.env.XIASHE_CLI_NAME || (DEFAULT_ENV === 'global' ? 'agentpie' : 'xiashe');
37
+ const CONFIG_DIR = process.env.XIASHE_CONFIG_DIR || path.join(os.homedir(), DEFAULT_ENV === 'global' ? '.agentpie' : '.xiashe');
38
+ const CONFIG_PATH = process.env.XIASHE_CONFIG_PATH || path.join(CONFIG_DIR, 'config.json');
39
+ const DEFAULT_PROFILE = 'default';
40
+ const HTTP_TIMEOUT_MS = 20_000;
41
+
42
+ export class XiasheError extends Error {
43
+ constructor(message, options = {}) {
44
+ super(message);
45
+ this.name = 'XiasheError';
46
+ this.code = options.code || message;
47
+ this.status = options.status;
48
+ this.details = options.details;
49
+ }
50
+ }
51
+
52
+ export function configPath() {
53
+ return CONFIG_PATH;
54
+ }
55
+
56
+ export function configDir() {
57
+ return CONFIG_DIR;
58
+ }
59
+
60
+ export function normalizeBaseUrl(value) {
61
+ return String(value || '').trim().replace(/\/+$/, '');
62
+ }
63
+
64
+ export function nowIso(value = Date.now()) {
65
+ if (!value) return '';
66
+ return new Date(Number(value)).toISOString();
67
+ }
68
+
69
+ export function createIdempotencyKey(prefix, parts = []) {
70
+ const hash = createHash('sha256')
71
+ .update([prefix, ...parts.map((part) => String(part ?? ''))].join('\n'))
72
+ .digest('hex')
73
+ .slice(0, 24);
74
+ return `${prefix}:${hash}`;
75
+ }
76
+
77
+ export function inferMime(filePath) {
78
+ const lower = filePath.toLowerCase();
79
+ if (lower.endsWith('.png')) return 'image/png';
80
+ if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
81
+ if (lower.endsWith('.gif')) return 'image/gif';
82
+ if (lower.endsWith('.webp')) return 'image/webp';
83
+ if (lower.endsWith('.pdf')) return 'application/pdf';
84
+ if (lower.endsWith('.json')) return 'application/json';
85
+ if (lower.endsWith('.csv')) return 'text/csv';
86
+ if (lower.endsWith('.md')) return 'text/markdown';
87
+ if (lower.endsWith('.txt')) return 'text/plain';
88
+ return 'application/octet-stream';
89
+ }
90
+
91
+ function freshConfig() {
92
+ const defaults = DEFAULTS[DEFAULT_ENV];
93
+ return {
94
+ profile: DEFAULT_PROFILE,
95
+ profiles: {
96
+ [DEFAULT_PROFILE]: {
97
+ name: DEFAULT_PROFILE,
98
+ market: defaults.market,
99
+ locale: defaults.locale,
100
+ convexUrl: defaults.convexUrl,
101
+ actionsUrl: defaults.actionsUrl,
102
+ mcpUrl: defaults.mcpUrl,
103
+ agentClaimUrl: defaults.agentClaimUrl,
104
+ relayUrl: defaults.relayUrl
105
+ }
106
+ }
107
+ };
108
+ }
109
+
110
+ export async function readConfig() {
111
+ if (!existsSync(CONFIG_PATH)) {
112
+ return freshConfig();
113
+ }
114
+ const raw = await readFile(CONFIG_PATH, 'utf8');
115
+ try {
116
+ const parsed = JSON.parse(raw);
117
+ const base = freshConfig();
118
+ const profiles = { ...base.profiles };
119
+ for (const [name, profile] of Object.entries(parsed.profiles || {})) {
120
+ profiles[name] = {
121
+ ...base.profiles[DEFAULT_PROFILE],
122
+ ...(profile || {}),
123
+ name
124
+ };
125
+ }
126
+ return {
127
+ ...base,
128
+ ...parsed,
129
+ profiles
130
+ };
131
+ } catch (error) {
132
+ throw new XiasheError(`Invalid ${DEFAULT_PRODUCT_NAME} config JSON at ${CONFIG_PATH}`, { code: 'CONFIG_INVALID_JSON', details: error });
133
+ }
134
+ }
135
+
136
+ export async function writeConfig(config) {
137
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
138
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
139
+ return config;
140
+ }
141
+
142
+ export async function getProfile(name) {
143
+ const config = await readConfig();
144
+ const profileName = name || config.profile || DEFAULT_PROFILE;
145
+ return {
146
+ config,
147
+ profileName,
148
+ profile: {
149
+ ...freshConfig().profiles[DEFAULT_PROFILE],
150
+ ...(config.profiles?.[profileName] || {})
151
+ }
152
+ };
153
+ }
154
+
155
+ export async function updateProfile(name, patch) {
156
+ const config = await readConfig();
157
+ const profileName = name || config.profile || DEFAULT_PROFILE;
158
+ config.profiles = config.profiles || {};
159
+ config.profiles[profileName] = {
160
+ ...(config.profiles[profileName] || {}),
161
+ ...patch,
162
+ name: profileName
163
+ };
164
+ return writeConfig(config);
165
+ }
166
+
167
+ export async function useProfile(name) {
168
+ const config = await readConfig();
169
+ config.profile = name || DEFAULT_PROFILE;
170
+ config.profiles = config.profiles || {};
171
+ config.profiles[config.profile] = config.profiles[config.profile] || { name: config.profile };
172
+ return writeConfig(config);
173
+ }
174
+
175
+ export async function useEnvironment(envName, profileName) {
176
+ const normalized = String(envName || '').toLowerCase();
177
+ const defaults = normalized === 'global' || normalized === 'intl' ? DEFAULTS.global : DEFAULTS.cn;
178
+ await updateProfile(profileName, {
179
+ market: defaults.market,
180
+ locale: defaults.locale,
181
+ convexUrl: defaults.convexUrl,
182
+ actionsUrl: defaults.actionsUrl,
183
+ mcpUrl: defaults.mcpUrl,
184
+ agentClaimUrl: defaults.agentClaimUrl,
185
+ relayUrl: defaults.relayUrl
186
+ });
187
+ return defaults;
188
+ }
189
+
190
+ function envProfile(profile) {
191
+ const defaults = DEFAULTS[DEFAULT_ENV];
192
+ const actionsUrl = normalizeBaseUrl(process.env.XIASHE_ACTIONS_URL || process.env.AGENTPIE_ACTIONS_URL || profile.actionsUrl);
193
+ const mcpUrl = process.env.XIASHE_MCP_URL || process.env.AGENTPIE_MCP_URL || (actionsUrl ? `${actionsUrl}/mcp/xiashe` : profile.mcpUrl);
194
+ const agentClaimUrl =
195
+ process.env.XIASHE_AGENT_CLAIM_URL || process.env.AGENTPIE_AGENT_CLAIM_URL || (actionsUrl ? `${actionsUrl}/agent-gateway/claim` : profile.agentClaimUrl);
196
+ return {
197
+ ...profile,
198
+ convexUrl: process.env.XIASHE_CONVEX_URL || process.env.AGENTPIE_CONVEX_URL || profile.convexUrl,
199
+ actionsUrl,
200
+ mcpUrl,
201
+ agentClaimUrl,
202
+ relayUrl: process.env.XIASHE_RELAY_URL || process.env.AGENTPIE_RELAY_URL || profile.relayUrl,
203
+ market: process.env.XIASHE_MARKET || process.env.AGENTPIE_MARKET || profile.market || defaults.market,
204
+ locale: process.env.XIASHE_LOCALE || process.env.AGENTPIE_LOCALE || profile.locale || defaults.locale,
205
+ session: {
206
+ ...(profile.session || {}),
207
+ ...(process.env.XIASHE_USER_ID || process.env.XIASHE_SESSION_TOKEN
208
+ ? {
209
+ userId: process.env.XIASHE_USER_ID || profile.session?.userId,
210
+ sessionToken: process.env.XIASHE_SESSION_TOKEN || profile.session?.sessionToken
211
+ }
212
+ : {})
213
+ },
214
+ agent: {
215
+ ...(profile.agent || {}),
216
+ ...(process.env.XIASHE_RUNTIME_TOKEN
217
+ ? {
218
+ runtimeToken: process.env.XIASHE_RUNTIME_TOKEN
219
+ }
220
+ : {})
221
+ }
222
+ };
223
+ }
224
+
225
+ function normalizeConvexError(error) {
226
+ const message = error instanceof Error ? error.message : String(error || '');
227
+ if (/fetch failed|failed to fetch|network request failed|econnrefused|enotfound|eai_again/i.test(message)) {
228
+ return new XiasheError('Unable to reach Convex deployment.', {
229
+ code: 'CONVEX_NETWORK_UNAVAILABLE',
230
+ details: error
231
+ });
232
+ }
233
+ if (/timed out|timeout|abort/i.test(message)) {
234
+ return new XiasheError('Convex request timed out.', {
235
+ code: 'CONVEX_TIMEOUT',
236
+ details: error
237
+ });
238
+ }
239
+ return error;
240
+ }
241
+
242
+ export async function loadProfile(options = {}) {
243
+ const { config, profileName, profile } = await getProfile(options.profile);
244
+ const cleanOptions = Object.fromEntries(
245
+ Object.entries(options).filter(([key, value]) => key !== 'profile' && value !== undefined && value !== '')
246
+ );
247
+ return {
248
+ config,
249
+ profileName,
250
+ profile: envProfile({
251
+ ...profile,
252
+ ...cleanOptions
253
+ })
254
+ };
255
+ }
256
+
257
+ async function withTimeout(promiseFactory, timeoutMs = HTTP_TIMEOUT_MS) {
258
+ const controller = new AbortController();
259
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
260
+ try {
261
+ return await promiseFactory(controller.signal);
262
+ } catch (error) {
263
+ if (error?.name === 'AbortError') {
264
+ throw new XiasheError('Request timed out.', { code: 'REQUEST_TIMEOUT' });
265
+ }
266
+ throw error;
267
+ } finally {
268
+ clearTimeout(timer);
269
+ }
270
+ }
271
+
272
+ export async function postJson(url, body, options = {}) {
273
+ const timeoutMs = options.timeoutMs || HTTP_TIMEOUT_MS;
274
+ const headers = options.headers || {};
275
+ const response = await withTimeout(
276
+ (signal) =>
277
+ fetch(url, {
278
+ method: 'POST',
279
+ headers: {
280
+ 'content-type': 'application/json',
281
+ ...headers
282
+ },
283
+ body: JSON.stringify(body ?? {}),
284
+ signal
285
+ }),
286
+ timeoutMs
287
+ );
288
+ const text = await response.text();
289
+ let payload = {};
290
+ try {
291
+ payload = text ? JSON.parse(text) : {};
292
+ } catch {
293
+ throw new XiasheError(`Bad JSON response from ${url}`, { code: 'HTTP_BAD_JSON', status: response.status });
294
+ }
295
+ if (!response.ok) {
296
+ throw new XiasheError(payload.error || payload.message || `HTTP ${response.status}`, {
297
+ code: payload.error || `HTTP_${response.status}`,
298
+ status: response.status,
299
+ details: payload
300
+ });
301
+ }
302
+ return payload;
303
+ }
304
+
305
+ export async function mcpCall(mcpUrl, runtimeToken, method, params = {}, timeoutMs = HTTP_TIMEOUT_MS) {
306
+ const payload = await postJson(
307
+ mcpUrl,
308
+ {
309
+ jsonrpc: '2.0',
310
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
311
+ method,
312
+ params
313
+ },
314
+ {
315
+ timeoutMs,
316
+ headers: runtimeToken ? { authorization: `Bearer ${runtimeToken}` } : {}
317
+ }
318
+ );
319
+ if (payload.error) {
320
+ throw new XiasheError(payload.error.message || 'MCP error.', {
321
+ code: payload.error.code || 'MCP_ERROR',
322
+ details: payload.error
323
+ });
324
+ }
325
+ return payload.result;
326
+ }
327
+
328
+ export function decodeMcpToolResult(result) {
329
+ const text = result?.content?.find?.((item) => item?.type === 'text')?.text;
330
+ if (typeof text !== 'string') {
331
+ return result;
332
+ }
333
+ try {
334
+ return JSON.parse(text);
335
+ } catch {
336
+ return { text };
337
+ }
338
+ }
339
+
340
+ export class XiasheClient {
341
+ constructor(profile, options = {}) {
342
+ this.profile = envProfile(profile || {});
343
+ this.profileName = options.profileName || DEFAULT_PROFILE;
344
+ this.timeoutMs = options.timeoutMs || HTTP_TIMEOUT_MS;
345
+ this._convex = null;
346
+ }
347
+
348
+ static async fromConfig(options = {}) {
349
+ const loaded = await loadProfile(options);
350
+ return new XiasheClient(loaded.profile, {
351
+ profileName: loaded.profileName,
352
+ timeoutMs: options.timeoutMs
353
+ });
354
+ }
355
+
356
+ convex() {
357
+ if (!this.profile.convexUrl) {
358
+ throw new XiasheError('Missing Convex URL. Run `xiashe config set convexUrl <url>` or pass --convex-url.', {
359
+ code: 'CONVEX_URL_REQUIRED'
360
+ });
361
+ }
362
+ if (!this._convex) {
363
+ this._convex = new ConvexHttpClient(this.profile.convexUrl);
364
+ }
365
+ return this._convex;
366
+ }
367
+
368
+ authArgs(extra = {}) {
369
+ const userId = this.profile.session?.userId;
370
+ const sessionToken = this.profile.session?.sessionToken;
371
+ if (!userId || !sessionToken) {
372
+ throw new XiasheError(`Not signed in. Run \`${DEFAULT_CLI_NAME} login --email <email> --password <password>\` first.`, {
373
+ code: 'AUTH_REQUIRED'
374
+ });
375
+ }
376
+ return {
377
+ requesterUserId: userId,
378
+ sessionToken,
379
+ ...extra
380
+ };
381
+ }
382
+
383
+ async query(ref, args = {}) {
384
+ try {
385
+ return await this.convex().query(ref, args);
386
+ } catch (error) {
387
+ throw normalizeConvexError(error);
388
+ }
389
+ }
390
+
391
+ async mutation(ref, args = {}) {
392
+ try {
393
+ return await this.convex().mutation(ref, args);
394
+ } catch (error) {
395
+ throw normalizeConvexError(error);
396
+ }
397
+ }
398
+
399
+ async action(ref, args = {}) {
400
+ try {
401
+ return await this.convex().action(ref, args);
402
+ } catch (error) {
403
+ throw normalizeConvexError(error);
404
+ }
405
+ }
406
+
407
+ async signInWithPassword(email, password) {
408
+ return this.mutation(api.auth.signInWithPassword, { email, password });
409
+ }
410
+
411
+ async registerWithPassword(email, password, displayName = '') {
412
+ return this.mutation(api.auth.registerWithPassword, { email, password, displayName });
413
+ }
414
+
415
+ async requestEmailCode(email, mode = 'login') {
416
+ return this.action(api.auth.requestEmailCode, { email, mode });
417
+ }
418
+
419
+ async verifyEmailCode(email, code, mode = 'login', displayName = '') {
420
+ return this.mutation(api.auth.verifyEmailCode, { email, code, mode, displayName });
421
+ }
422
+
423
+ async whoami() {
424
+ return this.query(api.users.getById, this.authArgs({ userId: this.profile.session.userId }));
425
+ }
426
+
427
+ async listConnections() {
428
+ return this.query(api.connections.listForUser, this.authArgs());
429
+ }
430
+
431
+ async resolveConnection(selector) {
432
+ const rows = await this.listConnections();
433
+ const needle = String(selector || '').trim();
434
+ if (!needle) {
435
+ if (rows.length === 1) return rows[0];
436
+ throw new XiasheError('Please pass --connection <id-or-name>.', { code: 'CONNECTION_REQUIRED' });
437
+ }
438
+ const found =
439
+ rows.find((row) => row._id === needle || row.id === needle) ||
440
+ rows.find((row) => String(row.name || '').toLowerCase() === needle.toLowerCase()) ||
441
+ rows.find((row) => String(row.name || '').toLowerCase().includes(needle.toLowerCase()));
442
+ if (!found) {
443
+ throw new XiasheError(`Connection not found: ${needle}`, { code: 'CONNECTION_NOT_FOUND' });
444
+ }
445
+ return found;
446
+ }
447
+
448
+ async createRelayPairCommand(connectionId, options = {}) {
449
+ return this.action(
450
+ api.connections.createRelayPairCommandForConnection,
451
+ this.authArgs({ connectionId, forceBootstrap: Boolean(options.forceBootstrap) })
452
+ );
453
+ }
454
+
455
+ async issueRelayPairCode(connectionId) {
456
+ return this.action(api.connections.issueRelayPairCodeForConnection, this.authArgs({ connectionId }));
457
+ }
458
+
459
+ async listSkills(options = {}) {
460
+ const market = options.market || this.profile.market || DEFAULTS.cn.market;
461
+ const locale = options.locale || this.profile.locale || DEFAULTS.cn.locale;
462
+ return this.query(
463
+ api.skillsCatalog.listMarketplace,
464
+ this.authArgs({
465
+ search: options.search || undefined,
466
+ connectionId: options.connectionId || undefined,
467
+ market,
468
+ locale
469
+ })
470
+ );
471
+ }
472
+
473
+ async resolveSkill(selector, options = {}) {
474
+ const marketplace = await this.listSkills({ ...options, search: options.search || selector });
475
+ const needle = String(selector || '').trim();
476
+ const skills = marketplace.skills || [];
477
+ const found =
478
+ skills.find((skill) => skill.id === needle || skill._id === needle || skill.skillId === needle) ||
479
+ skills.find((skill) => String(skill.name || '').toLowerCase() === needle.toLowerCase()) ||
480
+ skills.find((skill) => String(skill.skillId || '').toLowerCase() === needle.toLowerCase()) ||
481
+ skills.find((skill) => String(skill.name || '').toLowerCase().includes(needle.toLowerCase()));
482
+ if (!found) {
483
+ throw new XiasheError(`Skill not found: ${needle}`, { code: 'SKILL_NOT_FOUND' });
484
+ }
485
+ return found;
486
+ }
487
+
488
+ async getSkillDetail(skillCatalogId, options = {}) {
489
+ return this.query(
490
+ api.skillsCatalog.getSkillDetail,
491
+ this.authArgs({
492
+ skillCatalogId,
493
+ connectionId: options.connectionId || undefined,
494
+ market: options.market || this.profile.market || DEFAULTS.cn.market,
495
+ locale: options.locale || this.profile.locale || DEFAULTS.cn.locale
496
+ })
497
+ );
498
+ }
499
+
500
+ async createSkillInstallJob(connectionId, skillCatalogId, options = {}) {
501
+ return this.mutation(
502
+ api.skillInstallJobs.createJob,
503
+ this.authArgs({
504
+ connectionId,
505
+ skillCatalogId,
506
+ attemptId: options.attemptId || undefined,
507
+ idempotencyKey:
508
+ options.idempotencyKey || createIdempotencyKey('cli-skill-install', [connectionId, skillCatalogId])
509
+ })
510
+ );
511
+ }
512
+
513
+ async listSkillInstallJobs(connectionId, limit = 20) {
514
+ return this.query(api.skillInstallJobs.listForConnection, this.authArgs({ connectionId, limit }));
515
+ }
516
+
517
+ async issueAgentConnectToken(options = {}) {
518
+ return this.mutation(
519
+ api.agentGateway.issueConnectToken,
520
+ this.authArgs({
521
+ agentName: options.agentName || undefined,
522
+ agentKind: options.agentKind || undefined,
523
+ transport: options.transport || undefined
524
+ })
525
+ );
526
+ }
527
+
528
+ async listAgents(limit = 50) {
529
+ return this.query(api.agentGateway.listForUser, this.authArgs({ limit }));
530
+ }
531
+
532
+ async createAgentChatTask(options = {}) {
533
+ return this.mutation(
534
+ api.agentGateway.createChatTask,
535
+ this.authArgs({
536
+ chatId: options.chatId,
537
+ targetAgentConnectionId: options.targetAgentConnectionId,
538
+ content: options.content,
539
+ messageId: options.messageId || undefined,
540
+ attachments: options.attachments || undefined,
541
+ idempotencyKey: options.idempotencyKey || undefined
542
+ })
543
+ );
544
+ }
545
+
546
+ async listChats() {
547
+ return this.query(api.chats.listForUserSidebar, this.authArgs());
548
+ }
549
+
550
+ async resolveChat(selector) {
551
+ const rows = await this.listChats();
552
+ const needle = String(selector || '').trim();
553
+ if (!needle) {
554
+ if (rows.length === 1) return rows[0];
555
+ throw new XiasheError('Please pass --chat <id-or-title>.', { code: 'CHAT_REQUIRED' });
556
+ }
557
+ const found =
558
+ rows.find((row) => row._id === needle || row.id === needle) ||
559
+ rows.find((row) => String(row.title || '').toLowerCase() === needle.toLowerCase()) ||
560
+ rows.find((row) => String(row.title || '').toLowerCase().includes(needle.toLowerCase()));
561
+ if (!found) {
562
+ throw new XiasheError(`Chat not found: ${needle}`, { code: 'CHAT_NOT_FOUND' });
563
+ }
564
+ return found;
565
+ }
566
+
567
+ async createChat(connectionId, title, options = {}) {
568
+ return this.mutation(
569
+ api.chats.createChat,
570
+ this.authArgs({
571
+ connectionId,
572
+ title,
573
+ personaId: options.personaId || undefined,
574
+ personaPrompt: options.personaPrompt || undefined
575
+ })
576
+ );
577
+ }
578
+
579
+ async listMessages(chatId, limit = 80) {
580
+ const rows = await this.query(api.messages.listByChatDetailed, this.authArgs({ chatId }));
581
+ return rows.slice(Math.max(0, rows.length - limit));
582
+ }
583
+
584
+ async sendUserMessage(chatId, content, attachments = []) {
585
+ return this.mutation(
586
+ api.messages.sendUserMessage,
587
+ this.authArgs({
588
+ chatId,
589
+ content,
590
+ attachments: attachments.length > 0 ? attachments : undefined
591
+ })
592
+ );
593
+ }
594
+
595
+ async uploadFile(filePath, options = {}) {
596
+ const absolutePath = path.resolve(filePath);
597
+ const info = await stat(absolutePath);
598
+ if (!info.isFile()) {
599
+ throw new XiasheError(`Not a file: ${absolutePath}`, { code: 'UPLOAD_NOT_FILE' });
600
+ }
601
+ const fileName = options.fileName || path.basename(absolutePath);
602
+ const mime = options.mime || inferMime(absolutePath);
603
+ const source = options.source || 'manual_upload';
604
+ const ticket = await this.mutation(
605
+ api.files.createUploadUrl,
606
+ this.authArgs({
607
+ source,
608
+ fileName,
609
+ mime,
610
+ size: info.size
611
+ })
612
+ );
613
+ const bytes = await readFile(absolutePath);
614
+ const uploadResponse = await withTimeout(
615
+ (signal) =>
616
+ fetch(ticket.uploadUrl, {
617
+ method: 'POST',
618
+ headers: {
619
+ 'content-type': mime
620
+ },
621
+ body: bytes,
622
+ signal
623
+ }),
624
+ this.timeoutMs
625
+ );
626
+ const uploadText = await uploadResponse.text();
627
+ let uploadPayload = {};
628
+ try {
629
+ uploadPayload = uploadText ? JSON.parse(uploadText) : {};
630
+ } catch {
631
+ throw new XiasheError('File upload returned bad JSON.', { code: 'UPLOAD_BAD_JSON' });
632
+ }
633
+ if (!uploadResponse.ok || !uploadPayload.storageId) {
634
+ throw new XiasheError(uploadPayload.error || `Upload failed with HTTP ${uploadResponse.status}`, {
635
+ code: uploadPayload.error || `UPLOAD_HTTP_${uploadResponse.status}`,
636
+ status: uploadResponse.status
637
+ });
638
+ }
639
+ const checksumSha256 = createHash('sha256').update(bytes).digest('hex');
640
+ return this.mutation(
641
+ api.files.commitUploadedFile,
642
+ this.authArgs({
643
+ storageId: uploadPayload.storageId,
644
+ source,
645
+ fileName,
646
+ mime,
647
+ size: info.size,
648
+ checksumSha256
649
+ })
650
+ );
651
+ }
652
+
653
+ async listFiles() {
654
+ return this.query(api.files.listMyFiles, this.authArgs());
655
+ }
656
+
657
+ async claimAgentConnectToken(token, options = {}) {
658
+ const claimUrl = options.claimUrl || this.profile.agentClaimUrl;
659
+ const payload = await postJson(
660
+ claimUrl,
661
+ {
662
+ token,
663
+ agentName: options.agentName || undefined,
664
+ agentKind: options.agentKind || undefined,
665
+ agentRef: options.agentRef || undefined,
666
+ transport: options.transport || undefined
667
+ },
668
+ { timeoutMs: this.timeoutMs }
669
+ );
670
+ if (!payload.ok || !payload.runtimeToken) {
671
+ throw new XiasheError(payload.error || 'Agent claim failed.', { code: payload.error || 'AGENT_CLAIM_FAILED' });
672
+ }
673
+ return payload;
674
+ }
675
+
676
+ async agentRest(pathname, body = {}, options = {}) {
677
+ const baseUrl = normalizeBaseUrl(options.restBaseUrl || this.profile.agent?.restBaseUrl || this.profile.actionsUrl);
678
+ const runtimeToken = options.runtimeToken || this.profile.agent?.runtimeToken;
679
+ if (!runtimeToken) {
680
+ throw new XiasheError('Missing Agent runtime token. Run `xiashe agent connect --token <token>` first.', {
681
+ code: 'AGENT_RUNTIME_TOKEN_REQUIRED'
682
+ });
683
+ }
684
+ const url = `${baseUrl}${pathname.startsWith('/') ? pathname : `/${pathname}`}`;
685
+ return postJson(url, body, {
686
+ timeoutMs: this.timeoutMs,
687
+ headers: { authorization: `Bearer ${runtimeToken}` }
688
+ });
689
+ }
690
+
691
+ async agentHeartbeat(options = {}) {
692
+ return this.agentRest('/agent-gateway/heartbeat', {
693
+ status: options.status || 'online',
694
+ capabilities: options.capabilities || undefined
695
+ });
696
+ }
697
+
698
+ async agentListTasks(options = {}) {
699
+ return this.agentRest('/agent-gateway/tasks/list', {
700
+ limit: options.limit || 20
701
+ });
702
+ }
703
+
704
+ async agentAckTask(taskId, state = 'running') {
705
+ return this.agentRest('/agent-gateway/tasks/ack', { taskId, state });
706
+ }
707
+
708
+ async agentCompleteTask(taskId, output = {}, state = 'completed') {
709
+ return this.agentRest('/agent-gateway/tasks/complete', { taskId, state, output });
710
+ }
711
+
712
+ async agentListPeers() {
713
+ return this.agentRest('/agent-gateway/agents/list', {});
714
+ }
715
+
716
+ async agentCreateTask(options = {}) {
717
+ return this.agentRest('/agent-gateway/tasks/create', {
718
+ targetAgentConnectionId: options.targetAgentConnectionId,
719
+ chatId: options.chatId,
720
+ content: options.content,
721
+ attachments: options.attachments || undefined,
722
+ idempotencyKey: options.idempotencyKey || undefined
723
+ });
724
+ }
725
+
726
+ async mcp(method, params = {}, options = {}) {
727
+ return mcpCall(
728
+ options.mcpUrl || this.profile.mcpUrl,
729
+ options.runtimeToken || this.profile.agent?.runtimeToken || '',
730
+ method,
731
+ params,
732
+ this.timeoutMs
733
+ );
734
+ }
735
+
736
+ async mcpTool(name, args = {}, options = {}) {
737
+ return decodeMcpToolResult(
738
+ await this.mcp('tools/call', {
739
+ name,
740
+ arguments: args
741
+ }, options)
742
+ );
743
+ }
744
+ }
745
+
746
+ export async function saveLoginSession(profileName, authResult) {
747
+ const session = {
748
+ userId: authResult.userId,
749
+ email: authResult.email,
750
+ displayName: authResult.displayName || authResult.name || '',
751
+ sessionToken: authResult.sessionToken,
752
+ sessionExpiresAt: authResult.sessionExpiresAt,
753
+ defaultChatId: authResult.defaultChatId || null
754
+ };
755
+ await updateProfile(profileName, { session });
756
+ return session;
757
+ }
758
+
759
+ export async function saveAgentSession(profileName, claimResult) {
760
+ const agent = {
761
+ runtimeToken: claimResult.runtimeToken,
762
+ agentConnectionId: claimResult.agentConnectionId,
763
+ agentSessionId: claimResult.agentSessionId,
764
+ displayName: claimResult.displayName,
765
+ agentKind: claimResult.agentKind,
766
+ protocolVersion: claimResult.protocolVersion,
767
+ mcpUrl: claimResult.mcpUrl,
768
+ restBaseUrl: claimResult.restBaseUrl,
769
+ expiresAt: claimResult.expiresAt
770
+ };
771
+ await updateProfile(profileName, { agent });
772
+ return agent;
773
+ }
774
+
775
+ export function printableAgentPrompt(issue) {
776
+ const claimUrl = issue.claimUrl || issue.restBaseUrl?.replace(/\/+$/, '') + '/agent-gateway/claim';
777
+ if (DEFAULT_ENV === 'global') {
778
+ return [
779
+ 'Connect this Agent to AgentPie. Please complete the setup automatically or call the HTTP endpoint directly. Do not expose the token to the end user.',
780
+ '',
781
+ 'Preferred path: if you can make HTTP requests, POST to the Claim URL:',
782
+ '',
783
+ `POST ${claimUrl}`,
784
+ 'Content-Type: application/json',
785
+ '',
786
+ JSON.stringify(
787
+ {
788
+ token: issue.token,
789
+ agentName: '<your agent name>',
790
+ agentKind: '<codex|claude|coze|workbuddy|local|other>',
791
+ transport: '<mcp|http|npx|manual>',
792
+ agentRef: '<optional: device, workspace, or instance id>'
793
+ },
794
+ null,
795
+ 2
796
+ ),
797
+ '',
798
+ 'After claiming, save the returned runtimeToken and periodically POST heartbeat events to /agent-gateway/heartbeat on the REST Base URL.',
799
+ '',
800
+ 'If you support MCP, add the MCP URL as a tool server and use the runtimeToken as a Bearer token.',
801
+ '',
802
+ `Claim URL: ${claimUrl}`,
803
+ `Token: ${issue.token}`,
804
+ `MCP URL: ${issue.mcpUrl}`,
805
+ `REST Base URL: ${issue.restBaseUrl}`,
806
+ '',
807
+ 'After the connection is complete, the Agent appears in AgentPie and can receive mentions, tasks, and files.'
808
+ ].join('\n');
809
+ }
810
+ return [
811
+ '把这个 Agent 连接到虾舍。请你自动完成配置或调用 HTTP 接口,不要把 Token 暴露给最终用户。',
812
+ '',
813
+ '优先方式:如果你能发起 HTTP 请求,请直接 POST 到 Claim URL:',
814
+ '',
815
+ `POST ${claimUrl}`,
816
+ 'Content-Type: application/json',
817
+ '',
818
+ JSON.stringify(
819
+ {
820
+ token: issue.token,
821
+ agentName: '<你的 Agent 名称>',
822
+ agentKind: '<codex|claude|coze|workbuddy|local|other>',
823
+ transport: '<mcp|http|npx|manual>',
824
+ agentRef: '<可选:设备名、工作区或实例标识>'
825
+ },
826
+ null,
827
+ 2
828
+ ),
829
+ '',
830
+ '成功后保存返回的 runtimeToken,并定期 POST 心跳到 REST Base URL 的 /agent-gateway/heartbeat。',
831
+ '',
832
+ '如果你支持 MCP,请把下面的 MCP URL 加为工具服务,并在调用时使用 runtimeToken 作为 Bearer Token。',
833
+ '',
834
+ `Claim URL: ${claimUrl}`,
835
+ `Token: ${issue.token}`,
836
+ `MCP URL: ${issue.mcpUrl}`,
837
+ `REST Base URL: ${issue.restBaseUrl}`,
838
+ '',
839
+ '连接完成后,你会出现在虾舍的 Agent 工作台里,可以接收 @提及、任务和文件。'
840
+ ].join('\n');
841
+ }
842
+
843
+ export function stableTaskId() {
844
+ return `xiashe-cli-${randomUUID()}`;
845
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@xiashe/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.mjs",
6
+ "exports": {
7
+ ".": "./index.mjs"
8
+ },
9
+ "dependencies": {
10
+ "convex": "^1.34.0"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ }
15
+ }