imaclawhub 1.0.1 → 1.0.2

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 (4) hide show
  1. package/README.md +12 -10
  2. package/cli.js +64 -14
  3. package/index.js +46 -37
  4. package/package.json +5 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # imaclawhub
2
2
 
3
- 通过 ensure 服务获取技能压缩包并解压到本地(默认解压到与执行命令所在目录平级的文件夹)。
3
+ 通过 ensure 服务获取技能列表并解压到本地。命令行会先拉取匹配列表,若有多项则提示选择序号后下载(默认解压到与执行命令所在目录平级)。
4
4
 
5
5
  ## 安装
6
6
 
@@ -13,25 +13,27 @@ npm install imaclawhub
13
13
  ### 命令行
14
14
 
15
15
  ```bash
16
- npx imaclawhub <技能名称或slug>
16
+ npx imaclawhub <技能名称或关键词>
17
17
  ```
18
18
 
19
- 示例:`npx imaclawhub my-skill`。若已全局安装(`npm install -g imaclawhub`),可直接执行 `imaclawhub my-skill`。
19
+ 会请求 ensure 接口获取匹配列表:仅一项时自动下载;多项时列出序号,输入数字选择后下载。示例:`npx imaclawhub claw`。全局安装后可直接执行 `imaclawhub <关键词>`。
20
20
 
21
21
  ### 作为库使用
22
22
 
23
23
  ```javascript
24
- import { ensureToDesktop, ensureSkill, getDesktopPath } from 'imaclawhub';
24
+ import { ensureToDesktop, ensureSkillList, getDesktopPath } from 'imaclawhub';
25
25
 
26
- // 获取技能并解压(默认解压到与当前目录平级)
26
+ // 获取列表后按序号下载(仅一项时自动下载;每次调用都会重新拉取列表)
27
27
  const result = await ensureToDesktop('my-skill');
28
- if (result.ok) console.log('解压路径:', result.path);
28
+ if (result.needSelection) {
29
+ await ensureToDesktop('my-skill', { selectedIndex: 0 });
30
+ } else if (result.ok) console.log('解压路径:', result.path);
29
31
 
30
32
  // 解压到指定目录,如桌面
31
- await ensureToDesktop('my-skill', { outputDir: getDesktopPath() });
33
+ await ensureToDesktop('my-skill', { outputDir: getDesktopPath(), selectedIndex: 0 });
32
34
 
33
- // 仅获取技能信息(含 downloadUrl
34
- const info = await ensureSkill('my-skill');
35
+ // 获取技能列表(每项含 name、slug、downloadUrl 等)
36
+ const { list } = await ensureSkillList('claw');
35
37
  ```
36
38
 
37
- 选项:`ensureToDesktop(skillName, options)` 支持 `outputDir`、`subdir`。
39
+ 选项:`ensureToDesktop(skillName, options)` 支持 `outputDir`、`subdir`、`selectedIndex`、`item`。每次调用都会重新请求 ensure 拉取列表。
package/cli.js CHANGED
@@ -1,24 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { ensureToDesktop } from './index.js';
3
+ import { createInterface } from 'readline';
4
+ import { ensureSkillList, ensureToDesktop } from './index.js';
4
5
 
5
- const skillName = process.argv[2];
6
+ // 只取第一个参数;名称带空格时请用引号:imaclawhub "AI 衣橱搭配"
7
+ const skillName = (process.argv[2] || '').trim();
6
8
  if (!skillName) {
7
- console.error('用法: imaclawhub <技能名称或slug>');
8
- console.error('示例: imaclawhub my-skill');
9
+ console.error('用法: imaclawhub <技能名称或关键词>');
10
+ console.error('示例: imaclawhub my-skill 或 imaclawhub "AI 衣橱搭配"');
9
11
  process.exit(1);
10
12
  }
11
13
 
12
- ensureToDesktop(skillName)
13
- .then((result) => {
14
- if (result.ok) {
15
- console.log('解压成功:', result.path);
16
- } else {
17
- console.error('失败:', result.error);
14
+ function run(selectedIndex) {
15
+ const options = selectedIndex != null ? { selectedIndex } : {};
16
+ ensureToDesktop(skillName, options)
17
+ .then((result) => {
18
+ if (result.ok && result.needSelection) {
19
+ showSelection(result.list, run);
20
+ return;
21
+ }
22
+ if (result.ok) {
23
+ console.log('解压成功:', result.path);
24
+ } else {
25
+ console.error('失败:', result.error);
26
+ process.exit(1);
27
+ }
28
+ })
29
+ .catch((err) => {
30
+ console.error(err.message || err);
18
31
  process.exit(1);
19
- }
20
- })
21
- .catch((err) => {
22
- console.error(err.message || err);
32
+ });
33
+ }
34
+
35
+ function showSelection(list, onSelect) {
36
+ if (!list || list.length === 0) {
37
+ console.error('没有可选项');
23
38
  process.exit(1);
39
+ }
40
+ console.log('\n请选择要下载的技能:\n');
41
+ list.forEach((item, i) => {
42
+ const version = item.version ? ` (${item.version})` : '';
43
+ const err = item.error ? ` - ${item.error}` : '';
44
+ console.log(` ${i + 1}. ${item.name || item.slug}${version}${err}`);
45
+ });
46
+ console.log('');
47
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
48
+ rl.question(`输入序号 (1-${list.length}): `, (answer) => {
49
+ rl.close();
50
+ const num = parseInt(answer.trim(), 10);
51
+ if (Number.isNaN(num) || num < 1 || num > list.length) {
52
+ console.error('无效序号');
53
+ process.exit(1);
54
+ }
55
+ onSelect(num - 1);
24
56
  });
57
+ }
58
+
59
+ // 先拉列表,始终弹出列表让用户选择后再下载
60
+ ensureSkillList(skillName).then(({ list, error }) => {
61
+ if (error) {
62
+ console.error('失败:', error);
63
+ process.exit(1);
64
+ }
65
+ const downloadable = list.filter((i) => i.downloadUrl && !i.error);
66
+ if (downloadable.length === 0) {
67
+ console.error(list.length ? '列表中暂无可用下载链接' : '未找到匹配的技能');
68
+ process.exit(1);
69
+ }
70
+ run(null);
71
+ }).catch((err) => {
72
+ console.error(err.message || err);
73
+ process.exit(1);
74
+ });
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { mkdir } from 'fs/promises';
4
4
  import { cwd } from 'process';
5
5
  import AdmZip from 'adm-zip';
6
6
 
7
+ // const DEFAULT_BASE_URL = 'http://localhost:8848';
7
8
  const DEFAULT_BASE_URL = 'https://imahub.imaclaw.ai';
8
9
  /**
9
10
  * 获取桌面目录路径(跨平台)
@@ -18,26 +19,26 @@ export function getDesktopPath() {
18
19
  }
19
20
 
20
21
  /**
21
- * 调用 ensure 接口获取技能信息(含下载 URL)
22
- * @param {string} skillName - 技能名称或 slug
23
- * @returns {Promise<{ found: boolean, downloadUrl?: string, slug?: string, name?: string, error?: string }>}
22
+ * 调用 ensure 接口获取技能列表(每项含 downloadUrl 等)
23
+ * @param {string} skillName - 技能名称或关键词(模糊匹配)
24
+ * @returns {Promise<{ list: Array<{ name: string, slug: string, version?: string, downloadUrl?: string, error?: string }>, error?: string }>}
24
25
  */
25
- export async function ensureSkill(skillName) {
26
+ export async function ensureSkillList(skillName) {
26
27
  const baseUrl = DEFAULT_BASE_URL.replace(/\/$/, '');
27
- const name = String(skillName).trim();
28
+ // 兼容多个空格:规范为单个空格后再 trim
29
+ const name = String(skillName).replace(/\s+/g, ' ').trim();
28
30
  if (!name) {
29
- return { found: false, error: '缺少技能名称' };
31
+ return { list: [], error: '缺少技能名称或关键词' };
30
32
  }
31
33
  const url = `${baseUrl}/skills/ensure?name=${encodeURIComponent(name)}`;
32
34
  const res = await fetch(url);
33
35
  const data = await res.json().catch(() => ({}));
34
36
  if (!res.ok) {
35
- return {
36
- found: false,
37
- error: data.error || `HTTP ${res.status}`,
38
- };
37
+ return { list: [], error: data.error || `HTTP ${res.status}` };
39
38
  }
40
- return data;
39
+ // 支持接口直接返回数组,或 { list } / { items }
40
+ const list = Array.isArray(data) ? data : (data.list || data.items || []);
41
+ return { list };
41
42
  }
42
43
 
43
44
  /**
@@ -59,44 +60,52 @@ export async function downloadAndExtractZip(zipUrl, outDir) {
59
60
  }
60
61
 
61
62
  /**
62
- * 通过 ensure 接口获取技能 zip URL,下载并解压到指定目录
63
+ * 通过 ensure 接口获取技能列表,按选中项下载并解压到指定目录
63
64
  * 默认解压到「执行命令所在目录」的平级目录(即上级目录下的 <slug> 文件夹)
64
- * @param {string} skillName - 技能名称或 slug
65
- * @param {{ outputDir?: string, subdir?: string }} options
66
- * - outputDir: 解压目标父目录,默认 process.cwd() 的上一级(与当前目录平级)
67
- * - subdir: 解压后的文件夹名,默认使用 slug
68
- * @returns {Promise<{ ok: boolean, path?: string, slug?: string, error?: string }>}
65
+ * @param {string} skillName - 技能名称或关键词
66
+ * @param {{ outputDir?: string, subdir?: string, selectedIndex?: number, item?: { slug: string, name?: string, downloadUrl?: string } }} options
67
+ * - outputDir: 解压目标父目录,默认 process.cwd() 的上一级
68
+ * - subdir: 解压后的文件夹名,默认使用输入的名称(如「AI 衣橱搭配」),为空则用 slug
69
+ * - selectedIndex: 列表中的序号(0-based),与 item 二选一
70
+ * - item: 直接指定要下载的项(含 downloadUrl、slug),与 selectedIndex 二选一
71
+ * @returns {Promise<{ ok: boolean, path?: string, slug?: string, needSelection?: boolean, list?: Array, error?: string }>}
69
72
  */
70
73
  export async function ensureToDesktop(skillName, options = {}) {
71
- const ensureResult = await ensureSkill(skillName);
72
- if (!ensureResult.found || ensureResult.error) {
74
+ // 每次安装都重新拉取列表
75
+ const result = await ensureSkillList(skillName);
76
+ if (result.error) return { ok: false, error: result.error };
77
+ const list = result.list;
78
+ const downloadable = list.filter((i) => i.downloadUrl && !i.error);
79
+ if (downloadable.length === 0) {
73
80
  return {
74
81
  ok: false,
75
- error: ensureResult.error || '未找到该技能',
82
+ error: list.length ? '列表中暂无可用下载链接' : '未找到匹配的技能',
83
+ list,
76
84
  };
77
85
  }
78
- const downloadUrl = ensureResult.downloadUrl;
86
+
87
+ let item = options.item;
88
+ if (!item && options.selectedIndex != null) {
89
+ const idx = Number(options.selectedIndex);
90
+ if (idx >= 0 && idx < downloadable.length) item = downloadable[idx];
91
+ }
92
+ // 未传 selectedIndex 时始终返回列表让用户选择,不自动选中唯一项
93
+ if (!item) {
94
+ return { ok: true, needSelection: true, list: downloadable };
95
+ }
96
+
97
+ const downloadUrl = item.downloadUrl;
79
98
  if (!downloadUrl) {
80
- return {
81
- ok: false,
82
- error: 'ensure 接口未返回 downloadUrl',
83
- };
99
+ return { ok: false, error: item.error || '该项无下载链接', slug: item.slug };
84
100
  }
85
- // 默认:与执行命令的目录平级(上级目录下的 <slug>)
101
+
86
102
  const baseDir = options.outputDir ?? join(cwd(), '..');
87
- const subdir = options.subdir ?? options.desktopSubdir ?? ensureResult.slug ?? skillName.trim();
103
+ // 优先使用用户输入的名称作为文件夹名,如 imaclawhub 'AI 衣橱搭配' 解压到 .../AI 衣橱搭配
104
+ const subdir = options.subdir ?? options.desktopSubdir ?? (skillName.replace(/\s+/g, ' ').trim() || item.slug);
88
105
  const outDir = join(baseDir, subdir);
89
106
  const extractResult = await downloadAndExtractZip(downloadUrl, outDir);
90
107
  if (!extractResult.ok) {
91
- return {
92
- ok: false,
93
- error: extractResult.error,
94
- slug: ensureResult.slug,
95
- };
108
+ return { ok: false, error: extractResult.error, slug: item.slug };
96
109
  }
97
- return {
98
- ok: true,
99
- path: extractResult.path,
100
- slug: ensureResult.slug,
101
- };
110
+ return { ok: true, path: extractResult.path, slug: item.slug };
102
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imaclawhub",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Fetch skill from local ensure API and extract to Desktop",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -15,7 +15,10 @@
15
15
  "cli.js",
16
16
  "README.md"
17
17
  ],
18
- "scripts": {},
18
+ "scripts": {
19
+ "dev": "node cli.js",
20
+ "start": "node cli.js"
21
+ },
19
22
  "keywords": [
20
23
  "clawhub",
21
24
  "skills",