@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.
- package/README.md +13 -0
- package/index.mjs +845 -0
- 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