@xiashe/cli 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 +31 -0
  2. package/bin/xiashe.mjs +928 -0
  3. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @xiashe/cli
2
+
3
+ `xiashe` exposes XiaShe core workflows from a local terminal or any shell-capable Agent.
4
+
5
+ ## Local development
6
+
7
+ ```bash
8
+ node packages/xiashe-cli/bin/xiashe.mjs --help
9
+ node packages/xiashe-cli/bin/xiashe.mjs config path
10
+ node packages/xiashe-cli/bin/xiashe.mjs env use cn
11
+ node packages/xiashe-cli/bin/xiashe.mjs login --email you@example.com --password '********' --convex-url https://...
12
+ ```
13
+
14
+ ## Core workflows
15
+
16
+ ```bash
17
+ xiashe skills list
18
+ xiashe skills install copywriting --connection MaxClaw-1 --watch
19
+ xiashe agent issue --name "Local WorkBuddy"
20
+ xiashe agent connect --token "<token>" --name "Local WorkBuddy"
21
+ xiashe agent tasks watch --ack
22
+ xiashe chat send --chat "New Chat" --content "帮我处理这个文件" --file ./report.pdf --agent "Local WorkBuddy"
23
+ ```
24
+
25
+ Local state is stored in `~/.xiashe/config.json`.
26
+
27
+ Production defaults:
28
+
29
+ - Convex: `https://api.xiashe.chat`
30
+ - Actions: `https://actions.xiashe.chat`
31
+ - Relay: `https://relay.xiashe.chat`
package/bin/xiashe.mjs ADDED
@@ -0,0 +1,928 @@
1
+ #!/usr/bin/env node
2
+
3
+ import os from 'node:os';
4
+
5
+ let sdk;
6
+ try {
7
+ sdk = await import('@xiashe/sdk');
8
+ } catch {
9
+ try {
10
+ sdk = await import('xiashe-sdk');
11
+ } catch {
12
+ sdk = await import('../../xiashe-sdk/index.mjs');
13
+ }
14
+ }
15
+
16
+ const {
17
+ VERSION,
18
+ DEFAULTS,
19
+ XiasheClient,
20
+ configPath,
21
+ readConfig,
22
+ updateProfile,
23
+ useProfile,
24
+ useEnvironment,
25
+ loadProfile,
26
+ saveLoginSession,
27
+ saveAgentSession,
28
+ printableAgentPrompt,
29
+ createIdempotencyKey,
30
+ postJson,
31
+ mcpCall,
32
+ decodeMcpToolResult,
33
+ nowIso
34
+ } = sdk;
35
+
36
+ const EXIT_USAGE = 2;
37
+ const EXIT_AUTH = 3;
38
+ const EXIT_NETWORK = 4;
39
+ const EXIT_RUNTIME = 5;
40
+ const COMMAND_NAME = process.env.XIASHE_CLI_NAME || 'xiashe';
41
+ const PRODUCT_NAME = process.env.XIASHE_PRODUCT_NAME || (COMMAND_NAME === 'agentpie' ? 'AgentPie' : 'XiaShe');
42
+
43
+ function usage() {
44
+ const c = COMMAND_NAME;
45
+ return `${c} ${VERSION}
46
+
47
+ Usage:
48
+ ${c} login --email <email> --password <password> --convex-url <url>
49
+ ${c} signup --email <email> --password <password> --name <name>
50
+ ${c} whoami
51
+
52
+ ${c} config path|get|set <key> <value>|use <profile>
53
+ ${c} env use cn|global
54
+
55
+ ${c} connections list
56
+ ${c} claw pair --connection <id-or-name>
57
+ ${c} claw repair --connection <id-or-name>
58
+ ${c} claw check --connection <id-or-name>
59
+
60
+ ${c} skills list [query]
61
+ ${c} skills detail <skill>
62
+ ${c} skills install <skill> --connection <id-or-name> [--watch]
63
+ ${c} skills jobs --connection <id-or-name>
64
+ ${c} skills claim <one-time-token>
65
+
66
+ ${c} agent issue [--name <name>] [--kind <kind>] [--json]
67
+ ${c} agent connect --token <token> [--name <name>] [--kind <kind>]
68
+ ${c} agent heartbeat
69
+ ${c} agent list
70
+ ${c} agent tasks list|watch|ack <taskId>|complete <taskId> [--content <text>]
71
+ ${c} agent task create --agent <id-or-name> --chat <id-or-title> --content <text>
72
+
73
+ ${c} chat list
74
+ ${c} chat create --connection <id-or-name> --title <title>
75
+ ${c} chat history --chat <id-or-title>
76
+ ${c} chat send --chat <id-or-title> --content <text> [--file <path>] [--agent <id-or-name>]
77
+
78
+ ${c} files upload <path>
79
+ ${c} files list
80
+
81
+ ${c} ops doctor
82
+
83
+ Global flags:
84
+ --profile <name> Local config profile. Default: active profile.
85
+ --convex-url <url> Convex deployment URL.
86
+ --actions-url <url> ${PRODUCT_NAME} action site URL.
87
+ --market <cn|intl> Skill market.
88
+ --locale <zh-CN|en> Catalog locale.
89
+ --json Print JSON output.
90
+ --timeout-ms <ms> HTTP timeout. Default: 20000.
91
+ --help Show help.
92
+ --version Show version.
93
+ `;
94
+ }
95
+
96
+ function fail(message, code = EXIT_RUNTIME) {
97
+ console.error(message);
98
+ process.exit(code);
99
+ }
100
+
101
+ function parseGlobal(argv) {
102
+ const rest = [];
103
+ const global = {
104
+ json: false,
105
+ profile: '',
106
+ convexUrl: '',
107
+ actionsUrl: '',
108
+ market: '',
109
+ locale: '',
110
+ timeoutMs: 20_000
111
+ };
112
+ for (let i = 0; i < argv.length; i += 1) {
113
+ const value = argv[i];
114
+ const next = argv[i + 1];
115
+ if (value === '--help' || value === '-h') {
116
+ console.log(usage());
117
+ process.exit(0);
118
+ }
119
+ if (value === '--version' || value === '-v') {
120
+ console.log(VERSION);
121
+ process.exit(0);
122
+ }
123
+ if (value === '--json') {
124
+ global.json = true;
125
+ continue;
126
+ }
127
+ if (value === '--profile') {
128
+ global.profile = requireValue(value, next);
129
+ i += 1;
130
+ continue;
131
+ }
132
+ if (value === '--convex-url') {
133
+ global.convexUrl = requireValue(value, next);
134
+ i += 1;
135
+ continue;
136
+ }
137
+ if (value === '--actions-url') {
138
+ global.actionsUrl = requireValue(value, next);
139
+ i += 1;
140
+ continue;
141
+ }
142
+ if (value === '--market') {
143
+ global.market = requireValue(value, next);
144
+ i += 1;
145
+ continue;
146
+ }
147
+ if (value === '--locale') {
148
+ global.locale = requireValue(value, next);
149
+ i += 1;
150
+ continue;
151
+ }
152
+ if (value === '--timeout-ms') {
153
+ global.timeoutMs = Number(requireValue(value, next));
154
+ if (!Number.isFinite(global.timeoutMs) || global.timeoutMs < 1000) {
155
+ fail('Invalid --timeout-ms value.', EXIT_USAGE);
156
+ }
157
+ i += 1;
158
+ continue;
159
+ }
160
+ rest.push(value);
161
+ }
162
+ return { global, argv: rest };
163
+ }
164
+
165
+ function parseFlags(argv) {
166
+ const out = { _: [] };
167
+ for (let i = 0; i < argv.length; i += 1) {
168
+ const value = argv[i];
169
+ if (!value.startsWith('--')) {
170
+ out._.push(value);
171
+ continue;
172
+ }
173
+ const eqIndex = value.indexOf('=');
174
+ if (eqIndex > 0) {
175
+ out[value.slice(2, eqIndex)] = value.slice(eqIndex + 1);
176
+ continue;
177
+ }
178
+ const key = value.slice(2);
179
+ const next = argv[i + 1];
180
+ if (!next || next.startsWith('--')) {
181
+ out[key] = true;
182
+ continue;
183
+ }
184
+ out[key] = next;
185
+ i += 1;
186
+ }
187
+ return out;
188
+ }
189
+
190
+ function requireValue(key, value) {
191
+ if (!value || String(value).startsWith('--')) {
192
+ fail(`Missing value for ${key}.`, EXIT_USAGE);
193
+ }
194
+ return value;
195
+ }
196
+
197
+ function requireArg(value, label) {
198
+ if (!value) {
199
+ fail(`Missing ${label}.\n\n${usage()}`, EXIT_USAGE);
200
+ }
201
+ return value;
202
+ }
203
+
204
+ function publicError(error) {
205
+ const message = error instanceof Error ? error.message : String(error || '');
206
+ const code = error?.code || message.match(/\b[A-Z][A-Z0-9_]{4,}\b/)?.[0] || '';
207
+ return {
208
+ message: message || 'XIASHE_CLI_FAILED',
209
+ code: code || 'XIASHE_CLI_FAILED',
210
+ status: error?.status
211
+ };
212
+ }
213
+
214
+ function inferExitCode(error) {
215
+ const normalized = `${error?.code || ''} ${error?.message || ''}`.toUpperCase();
216
+ if (normalized.includes('AUTH')) return EXIT_AUTH;
217
+ if (normalized.includes('NETWORK') || normalized.includes('TIMEOUT') || normalized.includes('HTTP')) return EXIT_NETWORK;
218
+ if (normalized.includes('REQUIRED') || normalized.includes('MISSING') || normalized.includes('NOT_FOUND')) return EXIT_USAGE;
219
+ return EXIT_RUNTIME;
220
+ }
221
+
222
+ function print(value, global = {}) {
223
+ if (global.json) {
224
+ console.log(JSON.stringify(value, null, 2));
225
+ return;
226
+ }
227
+ if (typeof value === 'string') {
228
+ console.log(value);
229
+ return;
230
+ }
231
+ console.log(JSON.stringify(value, null, 2));
232
+ }
233
+
234
+ function table(rows, columns) {
235
+ if (!rows.length) {
236
+ console.log('No rows.');
237
+ return;
238
+ }
239
+ const widths = columns.map((column) =>
240
+ Math.max(column.label.length, ...rows.map((row) => String(column.value(row) ?? '').length))
241
+ );
242
+ console.log(columns.map((column, index) => column.label.padEnd(widths[index])).join(' '));
243
+ console.log(widths.map((width) => '-'.repeat(width)).join(' '));
244
+ for (const row of rows) {
245
+ console.log(columns.map((column, index) => String(column.value(row) ?? '').padEnd(widths[index])).join(' '));
246
+ }
247
+ }
248
+
249
+ async function makeClient(global) {
250
+ const loaded = await loadProfile({
251
+ profile: global.profile || undefined,
252
+ convexUrl: global.convexUrl || undefined,
253
+ actionsUrl: global.actionsUrl || undefined,
254
+ market: global.market || undefined,
255
+ locale: global.locale || undefined
256
+ });
257
+ return {
258
+ profileName: loaded.profileName,
259
+ client: new XiasheClient(loaded.profile, { profileName: loaded.profileName, timeoutMs: global.timeoutMs })
260
+ };
261
+ }
262
+
263
+ async function persistGlobalOverrides(profileName, global) {
264
+ const patch = {};
265
+ if (global.convexUrl) patch.convexUrl = global.convexUrl;
266
+ if (global.actionsUrl) {
267
+ patch.actionsUrl = global.actionsUrl.replace(/\/+$/, '');
268
+ patch.mcpUrl = `${patch.actionsUrl}/mcp/xiashe`;
269
+ patch.agentClaimUrl = `${patch.actionsUrl}/agent-gateway/claim`;
270
+ }
271
+ if (global.market) patch.market = global.market;
272
+ if (global.locale) patch.locale = global.locale;
273
+ if (Object.keys(patch).length > 0) {
274
+ await updateProfile(profileName, patch);
275
+ }
276
+ }
277
+
278
+ async function cmdConfig(sub, args, global) {
279
+ const flags = parseFlags(args);
280
+ if (sub === 'path') {
281
+ print(configPath(), global);
282
+ return;
283
+ }
284
+ if (sub === 'get' || !sub) {
285
+ const config = await readConfig();
286
+ print(config, global);
287
+ return;
288
+ }
289
+ if (sub === 'use') {
290
+ const profile = requireArg(flags._[0], 'profile name');
291
+ await useProfile(profile);
292
+ print(`Active profile: ${profile}`, global);
293
+ return;
294
+ }
295
+ if (sub === 'set') {
296
+ const key = requireArg(flags._[0], 'config key');
297
+ const value = requireArg(flags._[1], 'config value');
298
+ const loaded = await loadProfile({ profile: global.profile || undefined });
299
+ await updateProfile(loaded.profileName, { [key]: value });
300
+ print(`Saved ${key} for profile ${loaded.profileName}.`, global);
301
+ return;
302
+ }
303
+ fail(`Unknown config command: ${sub}`, EXIT_USAGE);
304
+ }
305
+
306
+ async function cmdEnv(sub, args, global) {
307
+ const flags = parseFlags(args);
308
+ if (sub !== 'use') {
309
+ fail('Usage: xiashe env use cn|global', EXIT_USAGE);
310
+ }
311
+ const envName = requireArg(flags._[0], 'environment');
312
+ const loaded = await loadProfile({ profile: global.profile || undefined });
313
+ const defaults = await useEnvironment(envName, loaded.profileName);
314
+ print({ ok: true, profile: loaded.profileName, ...defaults }, global);
315
+ }
316
+
317
+ async function cmdLogin(args, global) {
318
+ const flags = parseFlags(args);
319
+ const email = requireArg(flags.email, '--email');
320
+ const password = requireArg(flags.password, '--password');
321
+ const { client, profileName } = await makeClient(global);
322
+ const auth = await client.signInWithPassword(email, password);
323
+ await persistGlobalOverrides(profileName, global);
324
+ await saveLoginSession(profileName, auth);
325
+ print(
326
+ global.json
327
+ ? { ok: true, profile: profileName, userId: auth.userId, email: auth.email, expiresAt: auth.sessionExpiresAt }
328
+ : `Signed in as ${auth.email}. Profile: ${profileName}`,
329
+ global
330
+ );
331
+ }
332
+
333
+ async function cmdSignup(args, global) {
334
+ const flags = parseFlags(args);
335
+ const email = requireArg(flags.email, '--email');
336
+ const password = requireArg(flags.password, '--password');
337
+ const name = requireArg(flags.name || flags['display-name'], '--name');
338
+ const { client, profileName } = await makeClient(global);
339
+ const auth = await client.registerWithPassword(email, password, name);
340
+ await persistGlobalOverrides(profileName, global);
341
+ await saveLoginSession(profileName, auth);
342
+ print(global.json ? { ok: true, profile: profileName, userId: auth.userId, email: auth.email } : `Signed up as ${auth.email}.`, global);
343
+ }
344
+
345
+ async function cmdAuth(sub, args, global) {
346
+ const flags = parseFlags(args);
347
+ const { client, profileName } = await makeClient(global);
348
+ if (sub === 'request-code') {
349
+ const result = await client.requestEmailCode(requireArg(flags.email, '--email'), flags.mode || 'login');
350
+ print(result, global);
351
+ return;
352
+ }
353
+ if (sub === 'verify-code') {
354
+ const result = await client.verifyEmailCode(
355
+ requireArg(flags.email, '--email'),
356
+ requireArg(flags.code, '--code'),
357
+ flags.mode || 'login',
358
+ flags.name || ''
359
+ );
360
+ await persistGlobalOverrides(profileName, global);
361
+ await saveLoginSession(profileName, result);
362
+ print(global.json ? { ok: true, profile: profileName, userId: result.userId, email: result.email } : `Signed in as ${result.email}.`, global);
363
+ return;
364
+ }
365
+ fail('Usage: xiashe auth request-code|verify-code', EXIT_USAGE);
366
+ }
367
+
368
+ async function cmdWhoami(global) {
369
+ const { client, profileName } = await makeClient(global);
370
+ const user = await client.whoami();
371
+ print(global.json ? { profile: profileName, ...user } : `${user.email} (${user._id})\nProfile: ${profileName}`, global);
372
+ }
373
+
374
+ async function cmdLogout(global) {
375
+ const loaded = await loadProfile({ profile: global.profile || undefined });
376
+ await updateProfile(loaded.profileName, { session: undefined });
377
+ print(`Signed out profile ${loaded.profileName}.`, global);
378
+ }
379
+
380
+ async function cmdConnections(sub, _args, global) {
381
+ if (sub && sub !== 'list') {
382
+ fail(`Unknown connections command: ${sub}`, EXIT_USAGE);
383
+ }
384
+ const { client } = await makeClient(global);
385
+ const rows = await client.listConnections();
386
+ if (global.json) {
387
+ print(rows, global);
388
+ return;
389
+ }
390
+ table(rows, [
391
+ { label: 'id', value: (row) => row._id || row.id },
392
+ { label: 'name', value: (row) => row.name },
393
+ { label: 'type', value: (row) => row.connectorType },
394
+ { label: 'status', value: (row) => row.status },
395
+ { label: 'gateway', value: (row) => row.relayGatewayId || row.gatewayUrl || '' }
396
+ ]);
397
+ }
398
+
399
+ async function cmdClaw(sub, args, global) {
400
+ const flags = parseFlags(args);
401
+ const { client } = await makeClient(global);
402
+ if (sub === 'list' || !sub) {
403
+ return cmdConnections('list', [], global);
404
+ }
405
+ const connection = await client.resolveConnection(flags.connection || flags.c || flags._[0]);
406
+ if (sub === 'pair' || sub === 'repair') {
407
+ const result = sub === 'pair'
408
+ ? await client.issueRelayPairCode(connection._id || connection.id)
409
+ : await client.createRelayPairCommand(connection._id || connection.id, { forceBootstrap: flags.bootstrap === true });
410
+ if (global.json) {
411
+ print(result, global);
412
+ return;
413
+ }
414
+ if (result.pairCode) {
415
+ print(`Pair code: ${result.pairCode}\nExpires: ${result.expiresAt ? nowIso(result.expiresAt) : 'unknown'}`, global);
416
+ } else {
417
+ print(result.pairCommand || JSON.stringify(result, null, 2), global);
418
+ }
419
+ return;
420
+ }
421
+ if (sub === 'check') {
422
+ const health = await probeConnection(connection, flags.token || process.env.XIASHE_GATEWAY_TOKEN, global.timeoutMs);
423
+ print(health, global);
424
+ return;
425
+ }
426
+ fail(`Unknown claw command: ${sub}`, EXIT_USAGE);
427
+ }
428
+
429
+ async function probeConnection(connection, token, timeoutMs) {
430
+ const gatewayUrl = String(connection.gatewayUrl || '').replace(/\/+$/, '');
431
+ const result = {
432
+ id: connection._id || connection.id,
433
+ name: connection.name,
434
+ status: connection.status,
435
+ connectorType: connection.connectorType,
436
+ gatewayUrl,
437
+ relayGatewayId: connection.relayGatewayId || null,
438
+ checks: []
439
+ };
440
+ if (!gatewayUrl) {
441
+ result.checks.push({ name: 'gatewayUrl', ok: false, error: 'missing gatewayUrl' });
442
+ return result;
443
+ }
444
+ for (const pathname of ['/health', '/api/health', '/healthz']) {
445
+ try {
446
+ const response = await fetch(`${gatewayUrl}${pathname}`, {
447
+ headers: token ? { authorization: `Bearer ${token}` } : {},
448
+ signal: AbortSignal.timeout(timeoutMs)
449
+ });
450
+ result.checks.push({ name: pathname, ok: response.ok, status: response.status });
451
+ if (response.ok) break;
452
+ } catch (error) {
453
+ result.checks.push({ name: pathname, ok: false, error: error.message });
454
+ }
455
+ }
456
+ return result;
457
+ }
458
+
459
+ async function cmdSkills(sub, args, global) {
460
+ const flags = parseFlags(args);
461
+ const { client } = await makeClient(global);
462
+ if (sub === 'list' || sub === 'search' || !sub) {
463
+ const search = flags._.join(' ') || flags.search || '';
464
+ const connection = flags.connection ? await client.resolveConnection(flags.connection) : null;
465
+ const result = await client.listSkills({ search, connectionId: connection?._id || connection?.id });
466
+ if (global.json) {
467
+ print(result, global);
468
+ return;
469
+ }
470
+ table(result.skills || [], [
471
+ { label: 'id', value: (row) => row.id },
472
+ { label: 'skill', value: (row) => row.skillId },
473
+ { label: 'name', value: (row) => row.name },
474
+ { label: 'free', value: (row) => (row.isFree ? 'yes' : 'no') },
475
+ { label: 'ready', value: (row) => (row.installReady ? 'yes' : 'no') }
476
+ ]);
477
+ return;
478
+ }
479
+ if (sub === 'detail') {
480
+ const skill = await client.resolveSkill(requireArg(flags._[0], 'skill'), { search: flags._[0] });
481
+ const detail = await client.getSkillDetail(skill.id);
482
+ print(detail, global);
483
+ return;
484
+ }
485
+ if (sub === 'install') {
486
+ const selector = requireArg(flags._[0], 'skill');
487
+ const connection = await client.resolveConnection(flags.connection || flags.c);
488
+ const skill = await client.resolveSkill(selector, { search: selector, connectionId: connection._id || connection.id });
489
+ const result = await client.createSkillInstallJob(connection._id || connection.id, skill.id, {
490
+ idempotencyKey: flags.idempotencyKey || createIdempotencyKey('cli-skill-install', [connection._id || connection.id, skill.id])
491
+ });
492
+ if (flags.watch) {
493
+ const final = await watchSkillJob(client, connection._id || connection.id, result.jobId, global);
494
+ print(global.json ? { queued: result, final } : `Install ${final.status}: ${final.skillName || skill.name} (${final.phase || final.status})`, global);
495
+ return;
496
+ }
497
+ print(result, global);
498
+ return;
499
+ }
500
+ if (sub === 'jobs' || sub === 'status') {
501
+ const connection = await client.resolveConnection(flags.connection || flags.c);
502
+ const rows = await client.listSkillInstallJobs(connection._id || connection.id, Number(flags.limit || 20));
503
+ if (global.json) {
504
+ print(rows, global);
505
+ return;
506
+ }
507
+ table(rows, [
508
+ { label: 'job', value: (row) => row._id },
509
+ { label: 'skill', value: (row) => row.skillName || row.skillId },
510
+ { label: 'status', value: (row) => row.status },
511
+ { label: 'phase', value: (row) => row.phase || '' },
512
+ { label: 'updated', value: (row) => nowIso(row.updatedAt).slice(0, 19) }
513
+ ]);
514
+ return;
515
+ }
516
+ if (sub === 'claim' || sub === 'use') {
517
+ await claimSkill(flags, client, global);
518
+ return;
519
+ }
520
+ fail(`Unknown skills command: ${sub}`, EXIT_USAGE);
521
+ }
522
+
523
+ async function watchSkillJob(client, connectionId, jobId, global) {
524
+ const deadline = Date.now() + Number(global.watchTimeoutMs || 180_000);
525
+ let latest = null;
526
+ while (Date.now() < deadline) {
527
+ const rows = await client.listSkillInstallJobs(connectionId, 30);
528
+ latest = rows.find((row) => row._id === jobId) || latest || rows[0];
529
+ if (latest && !global.json) {
530
+ process.stderr.write(`\r${latest.status} ${latest.phase || ''} ${latest.phaseMessage || ''}`.slice(0, 160));
531
+ }
532
+ if (latest && ['succeeded', 'failed', 'needs_action', 'cancelled'].includes(latest.status)) {
533
+ if (!global.json) process.stderr.write('\n');
534
+ return latest;
535
+ }
536
+ await new Promise((resolve) => setTimeout(resolve, 2500));
537
+ }
538
+ if (!global.json) process.stderr.write('\n');
539
+ return latest || { status: 'unknown' };
540
+ }
541
+
542
+ async function claimSkill(flags, client, global) {
543
+ const token = requireArg(flags._[0] || flags.token, 'one-time token');
544
+ const claimUrl = flags['claim-url'] || client.profile.mcpUrl.replace(/\/?$/, '/claim');
545
+ const mcpUrl = flags['mcp-url'] || client.profile.mcpUrl;
546
+ const claim = await postJson(
547
+ claimUrl,
548
+ {
549
+ token,
550
+ agentName: flags.name || flags['agent-name'] || 'XiaShe CLI Agent',
551
+ agentKind: flags.kind || 'cli',
552
+ agentRef: flags.ref || `${process.platform}:${process.env.USER || 'local'}`
553
+ },
554
+ { timeoutMs: global.timeoutMs }
555
+ );
556
+ if (!claim.ok || !claim.runtimeToken) {
557
+ throw new Error(claim.error || 'CLAIM_FAILED');
558
+ }
559
+ const tools = await mcpCall(mcpUrl, '', 'tools/list', {}, global.timeoutMs);
560
+ const prompt = decodeMcpToolResult(
561
+ await mcpCall(mcpUrl, claim.runtimeToken, 'tools/call', { name: 'xiashe.get_skill_prompt', arguments: {} }, global.timeoutMs)
562
+ );
563
+ const status = decodeMcpToolResult(
564
+ await mcpCall(mcpUrl, claim.runtimeToken, 'tools/call', { name: 'xiashe.skill_status', arguments: {} }, global.timeoutMs)
565
+ );
566
+ const receiptCallId = prompt?.receipt?.callId;
567
+ const ack = receiptCallId
568
+ ? decodeMcpToolResult(
569
+ await mcpCall(
570
+ mcpUrl,
571
+ claim.runtimeToken,
572
+ 'tools/call',
573
+ {
574
+ name: 'xiashe.ack_skill_call',
575
+ arguments: {
576
+ callId: receiptCallId,
577
+ outcome: 'received',
578
+ idempotencyKey: `xiashe-cli:${receiptCallId}:received`
579
+ }
580
+ },
581
+ global.timeoutMs
582
+ )
583
+ )
584
+ : null;
585
+ if (global.json) {
586
+ print({ ok: true, claim: { ...claim, runtimeToken: undefined }, tools, prompt, status, ack }, global);
587
+ return;
588
+ }
589
+ print(
590
+ [
591
+ `Skill: ${claim.skill?.name || prompt.skill?.name || 'Unknown'}`,
592
+ `Connection: ${claim.connection?.name || status.connection?.name || 'Unknown'}`,
593
+ '',
594
+ prompt.prompt || prompt.instructions || prompt.text || '',
595
+ '',
596
+ `Status: ${status.status || status.session?.status || 'active'}`
597
+ ].join('\n'),
598
+ global
599
+ );
600
+ }
601
+
602
+ async function cmdAgent(sub, args, global) {
603
+ const flags = parseFlags(args);
604
+ const { client, profileName } = await makeClient(global);
605
+ if (sub === 'issue' || sub === 'invite') {
606
+ const issue = await client.issueAgentConnectToken({
607
+ agentName: flags.name || flags['agent-name'] || undefined,
608
+ agentKind: flags.kind || undefined,
609
+ transport: flags.transport || undefined
610
+ });
611
+ if (global.json) {
612
+ print(issue, global);
613
+ return;
614
+ }
615
+ print(printableAgentPrompt(issue), global);
616
+ return;
617
+ }
618
+ if (sub === 'connect' || sub === 'claim') {
619
+ const token = requireArg(flags.token || flags._[0], '--token');
620
+ const claim = await client.claimAgentConnectToken(token, {
621
+ claimUrl: flags['claim-url'] || undefined,
622
+ agentName: flags.name || flags['agent-name'] || 'XiaShe CLI Agent',
623
+ agentKind: flags.kind || 'cli',
624
+ agentRef: flags.ref || `${osName()} ${process.cwd()}`,
625
+ transport: flags.transport || 'npx'
626
+ });
627
+ await saveAgentSession(profileName, claim);
628
+ print(global.json ? { ok: true, ...claim, runtimeToken: undefined } : `Connected Agent: ${claim.displayName}`, global);
629
+ return;
630
+ }
631
+ if (sub === 'heartbeat') {
632
+ const result = await client.agentHeartbeat({
633
+ status: flags.status || 'online',
634
+ capabilities: parseCsv(flags.capabilities || 'chat,files,tasks,skills')
635
+ });
636
+ print(result, global);
637
+ return;
638
+ }
639
+ if (sub === 'list' || sub === 'agents' || !sub) {
640
+ const rows = await client.listAgents(Number(flags.limit || 50));
641
+ if (global.json) {
642
+ print(rows, global);
643
+ return;
644
+ }
645
+ table(rows, [
646
+ { label: 'id', value: (row) => row.id },
647
+ { label: 'name', value: (row) => row.displayName },
648
+ { label: 'kind', value: (row) => row.agentKind },
649
+ { label: 'status', value: (row) => row.status },
650
+ { label: 'pending', value: (row) => row.pendingTaskCount },
651
+ { label: 'lastSeen', value: (row) => (row.lastSeenAt ? nowIso(row.lastSeenAt).slice(0, 19) : '') }
652
+ ]);
653
+ return;
654
+ }
655
+ if (sub === 'tasks') {
656
+ await cmdAgentTasks(flags, client, global);
657
+ return;
658
+ }
659
+ if (sub === 'task') {
660
+ const action = requireArg(flags._[0], 'agent task action');
661
+ if (action !== 'create') fail('Usage: xiashe agent task create --agent <id-or-name> --chat <id-or-title> --content <text>', EXIT_USAGE);
662
+ const agent = await resolveAgent(client, requireArg(flags.agent, '--agent'));
663
+ const chat = await client.resolveChat(requireArg(flags.chat, '--chat'));
664
+ const result = await client.createAgentChatTask({
665
+ chatId: chat._id || chat.id,
666
+ targetAgentConnectionId: agent.id,
667
+ content: readContent(flags),
668
+ idempotencyKey: flags.idempotencyKey || createIdempotencyKey('cli-agent-task', [chat._id || chat.id, agent.id, readContent(flags)])
669
+ });
670
+ print(result, global);
671
+ return;
672
+ }
673
+ fail(`Unknown agent command: ${sub}`, EXIT_USAGE);
674
+ }
675
+
676
+ async function cmdAgentTasks(flags, client, global) {
677
+ const action = flags._[0] || 'list';
678
+ if (action === 'list') {
679
+ const result = await client.agentListTasks({ limit: Number(flags.limit || 20) });
680
+ print(result, global);
681
+ return;
682
+ }
683
+ if (action === 'watch') {
684
+ await client.agentHeartbeat({ capabilities: parseCsv(flags.capabilities || 'chat,files,tasks,skills') }).catch(() => undefined);
685
+ const seen = new Set();
686
+ const deadline = Date.now() + Number(flags['timeout-ms'] || 300_000);
687
+ while (Date.now() < deadline) {
688
+ const result = await client.agentListTasks({ limit: Number(flags.limit || 20) });
689
+ const tasks = result.tasks || result.items || [];
690
+ for (const task of tasks) {
691
+ const id = task.taskId || task.id;
692
+ if (!id || seen.has(id)) continue;
693
+ seen.add(id);
694
+ console.log(JSON.stringify(task, null, 2));
695
+ if (flags.ack) await client.agentAckTask(id, 'running');
696
+ }
697
+ await new Promise((resolve) => setTimeout(resolve, 2500));
698
+ }
699
+ return;
700
+ }
701
+ if (action === 'ack') {
702
+ const taskId = requireArg(flags._[1], 'task id');
703
+ print(await client.agentAckTask(taskId, flags.state || 'running'), global);
704
+ return;
705
+ }
706
+ if (action === 'complete' || action === 'fail') {
707
+ const taskId = requireArg(flags._[1], 'task id');
708
+ const state = action === 'fail' ? 'failed' : (flags.state || 'completed');
709
+ const content = readContent(flags);
710
+ print(await client.agentCompleteTask(taskId, { content, error: flags.error || undefined }, state), global);
711
+ return;
712
+ }
713
+ fail(`Unknown agent tasks command: ${action}`, EXIT_USAGE);
714
+ }
715
+
716
+ async function resolveAgent(client, selector) {
717
+ const rows = await client.listAgents(100);
718
+ const needle = String(selector || '').toLowerCase();
719
+ const found =
720
+ rows.find((row) => row.id === selector) ||
721
+ rows.find((row) => String(row.displayName || '').toLowerCase() === needle) ||
722
+ rows.find((row) => String(row.displayName || '').toLowerCase().includes(needle));
723
+ if (!found) throw new Error(`Agent not found: ${selector}`);
724
+ return found;
725
+ }
726
+
727
+ async function cmdChat(sub, args, global) {
728
+ const flags = parseFlags(args);
729
+ const { client } = await makeClient(global);
730
+ if (sub === 'list' || !sub) {
731
+ const rows = await client.listChats();
732
+ if (global.json) {
733
+ print(rows, global);
734
+ return;
735
+ }
736
+ table(rows, [
737
+ { label: 'id', value: (row) => row._id || row.id },
738
+ { label: 'title', value: (row) => row.title },
739
+ { label: 'connection', value: (row) => row.connectionName || row.connection?.name || row.connectionId },
740
+ { label: 'updated', value: (row) => nowIso(row.updatedAt || row.lastActivityAt).slice(0, 19) }
741
+ ]);
742
+ return;
743
+ }
744
+ if (sub === 'create') {
745
+ const connection = await client.resolveConnection(requireArg(flags.connection || flags.c, '--connection'));
746
+ const result = await client.createChat(connection._id || connection.id, flags.title || 'New Chat');
747
+ print(global.json ? { chatId: result } : `Created chat: ${result}`, global);
748
+ return;
749
+ }
750
+ if (sub === 'history') {
751
+ const chat = await client.resolveChat(requireArg(flags.chat || flags._[0], '--chat'));
752
+ const rows = await client.listMessages(chat._id || chat.id);
753
+ if (global.json) {
754
+ print(rows, global);
755
+ return;
756
+ }
757
+ for (const message of rows) {
758
+ console.log(`\n[${message.role}] ${nowIso(message.createdAt).slice(0, 19)}`);
759
+ console.log(message.content || '');
760
+ if (message.attachments?.length) console.log(`attachments: ${message.attachments.map((a) => a.name).join(', ')}`);
761
+ }
762
+ return;
763
+ }
764
+ if (sub === 'send') {
765
+ const chat = await client.resolveChat(requireArg(flags.chat, '--chat'));
766
+ const content = readContent(flags);
767
+ const attachments = [];
768
+ if (flags.file) {
769
+ const uploaded = await client.uploadFile(flags.file, { source: 'chat_attachment' });
770
+ attachments.push({
771
+ type: String(uploaded.mime || '').startsWith('image/') ? 'photo' : 'file',
772
+ name: uploaded.fileName,
773
+ mime: uploaded.mime,
774
+ size: uploaded.size,
775
+ fileObjectId: uploaded.fileObjectId,
776
+ remoteUrl: uploaded.downloadUrl,
777
+ status: 'uploaded'
778
+ });
779
+ }
780
+ const messageId = await client.sendUserMessage(chat._id || chat.id, content, attachments);
781
+ if (flags.agent) {
782
+ const agent = await resolveAgent(client, flags.agent);
783
+ const task = await client.createAgentChatTask({
784
+ chatId: chat._id || chat.id,
785
+ targetAgentConnectionId: agent.id,
786
+ messageId,
787
+ content,
788
+ attachments: attachments.map((attachment) => ({
789
+ fileObjectId: attachment.fileObjectId,
790
+ name: attachment.name,
791
+ mime: attachment.mime,
792
+ size: attachment.size
793
+ })),
794
+ idempotencyKey: createIdempotencyKey('cli-chat-agent', [chat._id || chat.id, agent.id, messageId])
795
+ });
796
+ print({ messageId, agentTask: task }, global);
797
+ return;
798
+ }
799
+ print(global.json ? { messageId } : `Message saved: ${messageId}`, global);
800
+ return;
801
+ }
802
+ fail(`Unknown chat command: ${sub}`, EXIT_USAGE);
803
+ }
804
+
805
+ async function cmdFiles(sub, args, global) {
806
+ const flags = parseFlags(args);
807
+ const { client } = await makeClient(global);
808
+ if (sub === 'upload') {
809
+ const result = await client.uploadFile(requireArg(flags._[0] || flags.path, 'file path'), {
810
+ fileName: flags.name || undefined,
811
+ mime: flags.mime || undefined,
812
+ source: flags.source || 'manual_upload'
813
+ });
814
+ print(result, global);
815
+ return;
816
+ }
817
+ if (sub === 'list' || !sub) {
818
+ const rows = await client.listFiles();
819
+ if (global.json) {
820
+ print(rows, global);
821
+ return;
822
+ }
823
+ table(rows, [
824
+ { label: 'id', value: (row) => row.id },
825
+ { label: 'name', value: (row) => row.fileName },
826
+ { label: 'mime', value: (row) => row.mime },
827
+ { label: 'size', value: (row) => row.size },
828
+ { label: 'created', value: (row) => nowIso(row.createdAt).slice(0, 19) }
829
+ ]);
830
+ return;
831
+ }
832
+ fail(`Unknown files command: ${sub}`, EXIT_USAGE);
833
+ }
834
+
835
+ async function cmdOps(sub, args, global) {
836
+ const flags = parseFlags(args);
837
+ const { client } = await makeClient(global);
838
+ if (sub !== 'doctor' && sub !== 'relay-doctor' && sub !== 'prewarm') {
839
+ fail('Usage: xiashe ops doctor', EXIT_USAGE);
840
+ }
841
+ if (sub === 'prewarm') {
842
+ const url = `${client.profile.actionsUrl.replace(/\/+$/, '')}/agent-gateway/skills/prewarm`;
843
+ const targets = parseCsv(flags.targets || flags._.join(','));
844
+ const result = await postJson(url, { targets }, { timeoutMs: global.timeoutMs });
845
+ print(result, global);
846
+ return;
847
+ }
848
+ const checks = {
849
+ profile: client.profileName,
850
+ convexUrl: client.profile.convexUrl || null,
851
+ actionsUrl: client.profile.actionsUrl || null,
852
+ relayUrl: client.profile.relayUrl || null,
853
+ signedIn: Boolean(client.profile.session?.userId && client.profile.session?.sessionToken),
854
+ agentConnected: Boolean(client.profile.agent?.runtimeToken),
855
+ probes: []
856
+ };
857
+ for (const url of [client.profile.actionsUrl, client.profile.relayUrl].filter(Boolean)) {
858
+ for (const pathname of ['/health', '/healthz']) {
859
+ try {
860
+ const response = await fetch(`${url.replace(/\/+$/, '')}${pathname}`, { signal: AbortSignal.timeout(global.timeoutMs) });
861
+ checks.probes.push({ url: `${url}${pathname}`, ok: response.ok, status: response.status });
862
+ if (response.ok) break;
863
+ } catch (error) {
864
+ checks.probes.push({ url: `${url}${pathname}`, ok: false, error: error.message });
865
+ }
866
+ }
867
+ }
868
+ print(checks, global);
869
+ }
870
+
871
+ function readContent(flags) {
872
+ if (flags.content !== undefined) return String(flags.content);
873
+ if (flags.text !== undefined) return String(flags.text);
874
+ if (flags._?.length > 1) return flags._.slice(1).join(' ');
875
+ return '';
876
+ }
877
+
878
+ function parseCsv(input) {
879
+ return String(input || '')
880
+ .split(',')
881
+ .map((item) => item.trim())
882
+ .filter(Boolean);
883
+ }
884
+
885
+ function osName() {
886
+ return `${osPlatform()}-${process.arch}`;
887
+ }
888
+
889
+ function osPlatform() {
890
+ return process.platform;
891
+ }
892
+
893
+ async function main() {
894
+ const parsed = parseGlobal(process.argv.slice(2));
895
+ const [command, sub, ...args] = parsed.argv;
896
+ const global = parsed.global;
897
+ if (!command) {
898
+ console.log(usage());
899
+ return;
900
+ }
901
+ try {
902
+ if (command === 'config') return await cmdConfig(sub, args, global);
903
+ if (command === 'env') return await cmdEnv(sub, args, global);
904
+ if (command === 'login') return await cmdLogin([sub, ...args].filter(Boolean), global);
905
+ if (command === 'signup') return await cmdSignup([sub, ...args].filter(Boolean), global);
906
+ if (command === 'auth') return await cmdAuth(sub, args, global);
907
+ if (command === 'whoami') return await cmdWhoami(global);
908
+ if (command === 'logout') return await cmdLogout(global);
909
+ if (command === 'connections' || command === 'connection') return await cmdConnections(sub, args, global);
910
+ if (command === 'claw') return await cmdClaw(sub, args, global);
911
+ if (command === 'skills' || command === 'skill') return await cmdSkills(sub, args, global);
912
+ if (command === 'agent' || command === 'agents') return await cmdAgent(sub, args, global);
913
+ if (command === 'chat' || command === 'chats') return await cmdChat(sub, args, global);
914
+ if (command === 'files' || command === 'file') return await cmdFiles(sub, args, global);
915
+ if (command === 'ops') return await cmdOps(sub, args, global);
916
+ fail(`Unknown command: ${command}\n\n${usage()}`, EXIT_USAGE);
917
+ } catch (error) {
918
+ const normalized = publicError(error);
919
+ if (global.json) {
920
+ console.error(JSON.stringify({ ok: false, ...normalized }, null, 2));
921
+ } else {
922
+ console.error(`xiashe: ${normalized.message}`);
923
+ }
924
+ process.exit(inferExitCode(error));
925
+ }
926
+ }
927
+
928
+ void main();
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@xiashe/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "xiashe": "bin/xiashe.mjs"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "README.md"
11
+ ],
12
+ "dependencies": {
13
+ "@xiashe/sdk": "0.1.0"
14
+ },
15
+ "engines": {
16
+ "node": ">=20"
17
+ }
18
+ }