@xiashe/cli 0.1.6 → 0.1.8

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 CHANGED
@@ -20,6 +20,9 @@ xiashe agent issue --name "Local WorkBuddy"
20
20
  xiashe agent connect --token "<token>" --name "Local WorkBuddy"
21
21
  xiashe agent run --ack
22
22
  xiashe chat send --chat "New Chat" --content "帮我处理这个文件" --file ./report.pdf --agent "Local WorkBuddy"
23
+ xiashe skills publish draft . --source xiashe --registry-public-skill-id "<registry.skillId>"
24
+ xiashe skills publish doctor "<submission-id>"
25
+ xiashe skills publish submit "<submission-id>"
23
26
  ```
24
27
 
25
28
  Local state is stored in `~/.xiashe/config.json`.
package/bin/xiashe.mjs CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync } from 'node:fs';
5
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
6
  import os from 'node:os';
7
+ import path from 'node:path';
4
8
 
5
9
  let sdk;
6
10
  try {
@@ -62,6 +66,13 @@ Usage:
62
66
  ${c} skills install <skill> --connection <id-or-name> [--watch]
63
67
  ${c} skills jobs --connection <id-or-name>
64
68
  ${c} skills claim <one-time-token>
69
+ ${c} skills publish draft [skill-dir] --name <name> --entry <entry> --install-id <id> --source <source> [--registry-public-skill-id <id>]
70
+ ${c} skills publish update <submission-id> [skill-dir] [--name <name>] [--entry <entry>]
71
+ ${c} skills publish doctor <submission-id>
72
+ ${c} skills publish submit <submission-id>
73
+ ${c} skills publish list
74
+ ${c} skills publish review <submission-id> --status <in_review|changes_requested|approved|rejected> [--notes <text>]
75
+ ${c} skills publish release <submission-id>
65
76
 
66
77
  ${c} agent issue [--name <name>] [--kind <kind>] [--json]
67
78
  ${c} agent connect --token <token> [--name <name>] [--kind <kind>] [--webhook-url <https-url>] [--webhook-token <secret>]
@@ -538,6 +549,10 @@ async function cmdSkills(sub, args, global) {
538
549
  ]);
539
550
  return;
540
551
  }
552
+ if (sub === 'publish' || sub === 'submission' || sub === 'submissions') {
553
+ await cmdSkillPublish(flags, client, global);
554
+ return;
555
+ }
541
556
  if (sub === 'claim' || sub === 'use') {
542
557
  await claimSkill(flags, client, global);
543
558
  return;
@@ -545,6 +560,106 @@ async function cmdSkills(sub, args, global) {
545
560
  fail(`Unknown skills command: ${sub}`, EXIT_USAGE);
546
561
  }
547
562
 
563
+ async function cmdSkillPublish(flags, client, global) {
564
+ const action = flags._[0] || 'list';
565
+ if (action === 'list' || action === 'ls') {
566
+ const rows = await client.listSkillSubmissions(Number(flags.limit || 20));
567
+ if (global.json) {
568
+ print(rows, global);
569
+ return;
570
+ }
571
+ table(rows, [
572
+ { label: 'id', value: (row) => row._id },
573
+ { label: 'status', value: (row) => row.status },
574
+ { label: 'market', value: (row) => row.market },
575
+ { label: 'name', value: (row) => row.name },
576
+ { label: 'updated', value: (row) => nowIso(row.updatedAt).slice(0, 19) }
577
+ ]);
578
+ return;
579
+ }
580
+ if (action === 'draft' || action === 'create') {
581
+ const skillDir = flags._[1] && !String(flags._[1]).startsWith('--') ? flags._[1] : '.';
582
+ const draft = await inferPublishDraft(skillDir, flags, client);
583
+ const result = await client.createSkillSubmissionDraft(draft);
584
+ if (global.json) {
585
+ print({ ok: true, ...result, draft }, global);
586
+ return;
587
+ }
588
+ print(
589
+ [
590
+ `Created Skill submission draft: ${result.submissionId}`,
591
+ `Name: ${draft.name}`,
592
+ `Market: ${draft.market}`,
593
+ `Source fingerprint: ${draft.packageSha256}`,
594
+ '',
595
+ `Next: ${COMMAND_NAME} skills publish doctor ${result.submissionId}`,
596
+ `Then: ${COMMAND_NAME} skills publish submit ${result.submissionId}`
597
+ ].join('\n'),
598
+ global
599
+ );
600
+ return;
601
+ }
602
+ if (action === 'update') {
603
+ const submissionId = requireArg(flags._[1], 'submission-id');
604
+ const skillDir = flags._[2] && !String(flags._[2]).startsWith('--') ? flags._[2] : '.';
605
+ const patch = await inferPublishDraft(skillDir, flags, client);
606
+ const result = await client.updateSkillSubmissionDraft(submissionId, patch);
607
+ print(global.json ? { ok: true, ...result, patch } : `Updated Skill submission draft: ${result.submissionId}`, global);
608
+ return;
609
+ }
610
+ if (action === 'doctor' || action === 'check') {
611
+ const submissionId = requireArg(flags._[1], 'submission-id');
612
+ const report = await client.runSkillSubmissionSafetyCheck(submissionId);
613
+ printSafetyReport(report, global);
614
+ return;
615
+ }
616
+ if (action === 'submit') {
617
+ const submissionId = requireArg(flags._[1], 'submission-id');
618
+ const result = await client.submitSkillSubmissionForReview(submissionId);
619
+ if (global.json) {
620
+ print(result, global);
621
+ return;
622
+ }
623
+ print(`Submitted Skill for review: ${submissionId}`, global);
624
+ return;
625
+ }
626
+ if (action === 'admin-list') {
627
+ const rows = await client.adminListSkillSubmissions({ status: flags.status || undefined, limit: Number(flags.limit || 50) });
628
+ if (global.json) {
629
+ print(rows, global);
630
+ return;
631
+ }
632
+ table(rows, [
633
+ { label: 'id', value: (row) => row._id },
634
+ { label: 'status', value: (row) => row.status },
635
+ { label: 'market', value: (row) => row.market },
636
+ { label: 'name', value: (row) => row.name },
637
+ { label: 'updated', value: (row) => nowIso(row.updatedAt).slice(0, 19) }
638
+ ]);
639
+ return;
640
+ }
641
+ if (action === 'review' || action === 'approve' || action === 'reject' || action === 'changes') {
642
+ const submissionId = requireArg(flags._[1], 'submission-id');
643
+ const status = action === 'approve'
644
+ ? 'approved'
645
+ : action === 'reject'
646
+ ? 'rejected'
647
+ : action === 'changes'
648
+ ? 'changes_requested'
649
+ : requireArg(flags.status, '--status');
650
+ const result = await client.adminReviewSkillSubmission(submissionId, status, flags.notes || flags.reviewNotes || '');
651
+ print(result, global);
652
+ return;
653
+ }
654
+ if (action === 'release' || action === 'publish') {
655
+ const submissionId = requireArg(flags._[1], 'submission-id');
656
+ const result = await client.publishSkillSubmission(submissionId, flags.status || 'released');
657
+ print(result, global);
658
+ return;
659
+ }
660
+ fail(`Unknown skills publish command: ${action}`, EXIT_USAGE);
661
+ }
662
+
548
663
  async function watchSkillJob(client, connectionId, jobId, global) {
549
664
  const deadline = Date.now() + Number(global.watchTimeoutMs || 180_000);
550
665
  let latest = null;
@@ -1027,6 +1142,135 @@ function parseCsv(input) {
1027
1142
  .filter(Boolean);
1028
1143
  }
1029
1144
 
1145
+ const PUBLISH_IGNORE = new Set(['.git', 'node_modules', '.DS_Store', '.env', '.env.local', 'dist', 'build', 'coverage']);
1146
+ const PUBLISH_MAX_FILE_BYTES = 2 * 1024 * 1024;
1147
+
1148
+ async function readSmallText(filePath) {
1149
+ try {
1150
+ const info = await stat(filePath);
1151
+ if (!info.isFile() || info.size > PUBLISH_MAX_FILE_BYTES) return '';
1152
+ return await readFile(filePath, 'utf8');
1153
+ } catch {
1154
+ return '';
1155
+ }
1156
+ }
1157
+
1158
+ async function readJson(filePath) {
1159
+ const text = await readSmallText(filePath);
1160
+ if (!text) return null;
1161
+ try {
1162
+ return JSON.parse(text);
1163
+ } catch {
1164
+ return null;
1165
+ }
1166
+ }
1167
+
1168
+ async function walkPublishFiles(root, current = root, out = []) {
1169
+ let entries = [];
1170
+ try {
1171
+ entries = await readdir(current, { withFileTypes: true });
1172
+ } catch {
1173
+ return out;
1174
+ }
1175
+ for (const entry of entries) {
1176
+ if (PUBLISH_IGNORE.has(entry.name) || entry.name.endsWith('.log') || entry.name.endsWith('.tmp')) continue;
1177
+ const absolute = path.join(current, entry.name);
1178
+ const relative = path.relative(root, absolute).replaceAll(path.sep, '/');
1179
+ if (entry.isDirectory()) {
1180
+ await walkPublishFiles(root, absolute, out);
1181
+ continue;
1182
+ }
1183
+ if (!entry.isFile()) continue;
1184
+ const info = await stat(absolute).catch(() => null);
1185
+ if (!info || info.size > PUBLISH_MAX_FILE_BYTES) continue;
1186
+ out.push({ absolute, relative, size: info.size });
1187
+ }
1188
+ return out.sort((a, b) => a.relative.localeCompare(b.relative));
1189
+ }
1190
+
1191
+ async function hashPublishDirectory(root) {
1192
+ const files = await walkPublishFiles(root);
1193
+ const hash = createHash('sha256');
1194
+ for (const file of files) {
1195
+ const bytes = await readFile(file.absolute);
1196
+ hash.update(file.relative);
1197
+ hash.update('\0');
1198
+ hash.update(bytes);
1199
+ hash.update('\0');
1200
+ }
1201
+ return { sha256: hash.digest('hex'), fileCount: files.length, totalBytes: files.reduce((sum, file) => sum + file.size, 0) };
1202
+ }
1203
+
1204
+ function firstHeading(text) {
1205
+ return /^#\s+(.+)$/m.exec(text || '')?.[1]?.trim() || '';
1206
+ }
1207
+
1208
+ function firstParagraph(text) {
1209
+ return String(text || '')
1210
+ .split(/\n{2,}/)
1211
+ .map((item) => item.trim())
1212
+ .find((item) => item && !item.startsWith('#') && !item.startsWith('---')) || '';
1213
+ }
1214
+
1215
+ async function inferPublishDraft(skillDir, flags, client) {
1216
+ const root = path.resolve(skillDir || '.');
1217
+ const info = await stat(root).catch(() => null);
1218
+ if (!info?.isDirectory()) {
1219
+ throw new Error(`Skill directory not found: ${root}`);
1220
+ }
1221
+ const packageJson = await readJson(path.join(root, 'package.json'));
1222
+ const skillMd = await readSmallText(path.join(root, 'SKILL.md'));
1223
+ const readme = await readSmallText(path.join(root, 'README.md'));
1224
+ const text = `${skillMd}\n\n${readme}`;
1225
+ const digest = await hashPublishDirectory(root);
1226
+ const name = flags.name || packageJson?.name || firstHeading(text) || path.basename(root);
1227
+ const intro = flags.intro || flags.summary || packageJson?.description || firstParagraph(text);
1228
+ const entrypoint =
1229
+ flags.entry ||
1230
+ flags.entrypoint ||
1231
+ packageJson?.bin && (typeof packageJson.bin === 'string' ? packageJson.bin : Object.values(packageJson.bin)[0]) ||
1232
+ packageJson?.main ||
1233
+ 'SKILL.md';
1234
+ const source = flags.source || (COMMAND_NAME === 'agentpie' ? 'agentpie' : 'xiashe');
1235
+ const installId = flags['install-id'] || flags.installId || packageJson?.name || path.basename(root);
1236
+ return {
1237
+ market: flags.market || client.profile.market || (COMMAND_NAME === 'agentpie' ? 'intl' : 'cn'),
1238
+ name,
1239
+ summary: intro,
1240
+ summaryZh: flags['summary-zh'] || undefined,
1241
+ description: flags.description || intro,
1242
+ descriptionZh: flags['description-zh'] || undefined,
1243
+ entrypoint,
1244
+ installId,
1245
+ source,
1246
+ sourceUrl: flags['source-url'] || flags.repo || undefined,
1247
+ iconImageUrl: flags['icon-url'] || undefined,
1248
+ coverImageUrl: flags['cover-url'] || undefined,
1249
+ previewImageUrls: parseCsv(flags['preview-url'] || flags['preview-urls']),
1250
+ videoUrls: parseCsv(flags['video-url'] || flags['video-urls']),
1251
+ category: flags.category || undefined,
1252
+ tags: parseCsv(flags.tags),
1253
+ registryEntryId: flags['registry-entry'] || flags.registryEntryId || undefined,
1254
+ registryPublicSkillId: flags['registry-public-skill-id'] || flags.registryPublicSkillId || undefined,
1255
+ packageSha256: digest.sha256
1256
+ };
1257
+ }
1258
+
1259
+ function printSafetyReport(report, global) {
1260
+ if (global.json) {
1261
+ print(report, global);
1262
+ return;
1263
+ }
1264
+ const lines = [
1265
+ `Safety: ${report.status} (${report.score}/100)`,
1266
+ ''
1267
+ ];
1268
+ for (const check of report.checks || []) {
1269
+ lines.push(`${String(check.status).toUpperCase()} ${check.label}: ${check.message}`);
1270
+ }
1271
+ print(lines.join('\n'), global);
1272
+ }
1273
+
1030
1274
  function osName() {
1031
1275
  return `${osPlatform()}-${process.arch}`;
1032
1276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xiashe": "bin/xiashe.mjs"
@@ -10,7 +10,7 @@
10
10
  "README.md"
11
11
  ],
12
12
  "dependencies": {
13
- "@xiashe/sdk": "0.1.6"
13
+ "@xiashe/sdk": "0.1.7"
14
14
  },
15
15
  "engines": {
16
16
  "node": ">=20"