imaclawhub 1.0.2 → 1.0.4

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 +20 -22
  2. package/cli.js +127 -61
  3. package/index.js +94 -10
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,39 +1,37 @@
1
1
  # imaclawhub
2
2
 
3
- 通过 ensure 服务获取技能列表并解压到本地。命令行会先拉取匹配列表,若有多项则提示选择序号后下载(默认解压到与执行命令所在目录平级)。
3
+ 通过 ensure 服务获取技能列表并解压到本地。默认安装到本机 openclaw 的 skills 目录(`~/.openclaw/skills/<名称>`),可通过环境变量 `IMACLAWHUB_OUTPUT_DIR` 设置安装目录。使用默认安装目录时会先检查本机是否已安装 openclaw(存在 `~/.openclaw` 目录),未安装则提示先安装 openclaw 并中断;支持 Mac / Linux / Windows。
4
4
 
5
- ## 安装
5
+ ## 全局安装
6
6
 
7
7
  ```bash
8
- npm install imaclawhub
8
+ npm install imaclawhub -g
9
9
  ```
10
10
 
11
11
  ## 使用
12
12
 
13
- ### 命令行
14
-
13
+ 全局安装后可直接执行
15
14
  ```bash
16
- npx imaclawhub <技能名称或关键词>
15
+ imaclawhub <技能名称>
17
16
  ```
17
+ ## 或者直接使用npx
18
18
 
19
- 会请求 ensure 接口获取匹配列表:仅一项时自动下载;多项时列出序号,输入数字选择后下载。示例:`npx imaclawhub claw`。全局安装后可直接执行 `imaclawhub <关键词>`。
20
-
21
- ### 作为库使用
22
-
23
- ```javascript
24
- import { ensureToDesktop, ensureSkillList, getDesktopPath } from 'imaclawhub';
19
+ ```bash
20
+ npx imaclawhub <技能名称>
21
+ ```
25
22
 
26
- // 获取列表后按序号下载(仅一项时自动下载;每次调用都会重新拉取列表)
27
- const result = await ensureToDesktop('my-skill');
28
- if (result.needSelection) {
29
- await ensureToDesktop('my-skill', { selectedIndex: 0 });
30
- } else if (result.ok) console.log('解压路径:', result.path);
23
+ 会请求 ensure 接口获取匹配列表,列出序号后输入数字选择并下载。示例:`npx imaclawhub claw`。。
31
24
 
32
- // 解压到指定目录,如桌面
33
- await ensureToDesktop('my-skill', { outputDir: getDesktopPath(), selectedIndex: 0 });
25
+ **设置安装目录**(可选项,一般不需要设置)
26
+ 已安装 openclaw 时,技能会默认安装到 `~/.openclaw/skills`,无需额外配置。仅在需要安装到其他目录(如未安装 openclaw 或自定义路径)时再设置:
34
27
 
35
- // 获取技能列表(每项含 name、slug、downloadUrl 等)
36
- const { list } = await ensureSkillList('claw');
28
+ - 命令:`imaclawhub set dir '<路径>'`,会写入 `~/.imaclawhub/config.json`,之后安装**直接**使用该目录。
29
+ ```bash
30
+ imaclawhub set dir /path/to/my/skills
31
+ imaclawhub set dir "/path/with spaces"
37
32
  ```
33
+ - 清空已设定目录:`imaclawhub set dir --clear`,恢复为默认(需已安装 openclaw)。
34
+ - 或环境变量:`IMACLAWHUB_OUTPUT_DIR`(优先级高于 set dir 配置)。
35
+ 优先级:环境变量 > `imaclawhub set dir` 配置 > `~/.openclaw/skills`。直接运行 `imaclawhub` 不加参数可查看当前安装目录。
38
36
 
39
- 选项:`ensureToDesktop(skillName, options)` 支持 `outputDir`、`subdir`、`selectedIndex`、`item`。每次调用都会重新请求 ensure 拉取列表。
37
+ **检查 openclaw 是否已安装**:`imaclawhub check openclaw`,会输出「已安装」或「未安装」及默认技能目录。
package/cli.js CHANGED
@@ -1,74 +1,140 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createInterface } from 'readline';
4
- import { ensureSkillList, ensureToDesktop } from './index.js';
4
+ import {
5
+ ensureSkillList,
6
+ ensureToDesktop,
7
+ getOpenClawSkillsDir,
8
+ getConfiguredOutputDir,
9
+ setConfiguredOutputDir,
10
+ clearConfiguredOutputDir,
11
+ isOpenClawInstalled,
12
+ } from './index.js';
5
13
 
6
- // 只取第一个参数;名称带空格时请用引号:imaclawhub "AI 衣橱搭配"
7
- const skillName = (process.argv[2] || '').trim();
8
- if (!skillName) {
9
- console.error('用法: imaclawhub <技能名称或关键词>');
10
- console.error('示例: imaclawhub my-skill 或 imaclawhub "AI 衣橱搭配"');
11
- process.exit(1);
12
- }
14
+ // 安装目录优先级:环境变量 > imaclawhub set dir 配置 > ~/.openclaw/skills
15
+ const outputDir = process.env.IMACLAWHUB_OUTPUT_DIR || getConfiguredOutputDir() || getOpenClawSkillsDir();
13
16
 
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);
17
+ // 命令:imaclawhub check openclaw | imaclawhub set dir '<path>'
18
+ const arg0 = process.argv[2];
19
+ const arg1 = process.argv[3];
20
+ if (arg0 === 'check' && arg1 === 'openclaw') {
21
+ const installed = isOpenClawInstalled();
22
+ const skillsDir = getOpenClawSkillsDir();
23
+ if (installed) {
24
+ console.log('openclaw: 已安装');
25
+ console.log('默认技能目录: ' + skillsDir);
26
+ } else {
27
+ console.log('openclaw: 未安装');
28
+ console.log('期望目录: ' + skillsDir + '(请先安装 openclaw 或使用 imaclawhub set dir <路径> 指定安装目录)');
29
+ }
30
+ process.exit(installed ? 0 : 1);
31
+ } else if (arg0 === 'set' && arg1 === 'dir') {
32
+ const dir = (process.argv[4] != null ? process.argv.slice(4).join(' ') : '').trim();
33
+ if (dir === '--clear' || dir === '--unset') {
34
+ clearConfiguredOutputDir()
35
+ .then(() => {
36
+ console.log('已清空安装目录设置,将使用默认目录(需已安装 openclaw)。');
37
+ process.exit(0);
38
+ })
39
+ .catch((err) => {
40
+ console.error('清空失败:', err.message || err);
26
41
  process.exit(1);
27
- }
28
- })
29
- .catch((err) => {
30
- console.error(err.message || err);
42
+ });
43
+ // flow stops here (async)
44
+ } else if (!dir) {
45
+ console.error('用法: imaclawhub set dir <安装目录路径>');
46
+ console.error(' imaclawhub set dir --clear # 清空已设定的安装目录');
47
+ console.error('示例: imaclawhub set dir /path/to/skills 或 imaclawhub set dir "C:\\My Skills"');
48
+ process.exit(1);
49
+ } else {
50
+ setConfiguredOutputDir(dir)
51
+ .then(() => {
52
+ console.log('已设置安装目录: ' + dir);
53
+ process.exit(0);
54
+ })
55
+ .catch((err) => {
56
+ console.error('设置失败:', err.message || err);
57
+ process.exit(1);
58
+ });
59
+ }
60
+ } else {
61
+ // 只取第一个参数;名称带空格时请用引号:imaclawhub "AI 衣橱搭配"
62
+ const skillName = (process.argv[2] || '').trim();
63
+ if (!skillName) {
64
+ console.error('用法: imaclawhub <技能名称或关键词>');
65
+ console.error(' imaclawhub check openclaw # 检查是否已安装 openclaw');
66
+ console.error(' imaclawhub set dir <路径> # 设定安装目录(已设置则直接安装到该目录)');
67
+ console.error(' imaclawhub set dir --clear # 清空已设定的安装目录');
68
+ console.error('示例: imaclawhub my-skill 或 imaclawhub "AI 衣橱搭配"');
69
+ console.error('安装目录: ' + outputDir + (isOpenClawInstalled() ? ' (已检测到 openclaw)' : ' (未检测到 openclaw,请先安装或 imaclawhub set dir <路径>)'));
70
+ process.exit(1);
71
+ }
72
+
73
+ function run(selectedIndex) {
74
+ const options = { outputDir };
75
+ if (selectedIndex != null) options.selectedIndex = selectedIndex;
76
+ ensureToDesktop(skillName, options)
77
+ .then((result) => {
78
+ if (result.ok && result.needSelection) {
79
+ showSelection(result.list, run);
80
+ return;
81
+ }
82
+ if (result.ok) {
83
+ console.log('安装成功');
84
+ } else {
85
+ console.error('失败:', result.error);
86
+ process.exit(1);
87
+ }
88
+ })
89
+ .catch((err) => {
90
+ console.error(err.message || err);
91
+ process.exit(1);
92
+ });
93
+ }
94
+
95
+ function showSelection(list, onSelect) {
96
+ if (!list || list.length === 0) {
97
+ console.error('没有可选项');
31
98
  process.exit(1);
99
+ }
100
+ console.log('\n请选择要下载的技能:\n');
101
+ list.forEach((item, i) => {
102
+ const version = item.version ? ` (${item.version})` : '';
103
+ const err = item.error ? ` - ${item.error}` : '';
104
+ console.log(` ${i + 1}. ${item.name || item.slug}${version}${err}`);
105
+ });
106
+ console.log('');
107
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
108
+ rl.question(`输入序号 (1-${list.length}): `, (answer) => {
109
+ rl.close();
110
+ const num = parseInt(answer.trim(), 10);
111
+ if (Number.isNaN(num) || num < 1 || num > list.length) {
112
+ console.error('无效序号');
113
+ process.exit(1);
114
+ }
115
+ onSelect(num - 1);
32
116
  });
33
- }
34
-
35
- function showSelection(list, onSelect) {
36
- if (!list || list.length === 0) {
37
- console.error('没有可选项');
38
- process.exit(1);
39
117
  }
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('无效序号');
118
+
119
+ (async () => {
120
+ const usingDefaultOpenClawDir = !process.env.IMACLAWHUB_OUTPUT_DIR && !getConfiguredOutputDir();
121
+ if (usingDefaultOpenClawDir && !isOpenClawInstalled()) {
122
+ console.error('未检测到 openclaw,请先安装 openclaw 或使用 imaclawhub set dir <路径> 指定安装目录。');
123
+ process.exit(1);
124
+ }
125
+ const { list, error } = await ensureSkillList(skillName);
126
+ if (error) {
127
+ console.error('失败:', error);
53
128
  process.exit(1);
54
129
  }
55
- onSelect(num - 1);
130
+ const downloadable = list.filter((i) => i.downloadUrl && !i.error);
131
+ if (downloadable.length === 0) {
132
+ console.error(list.length ? '列表中暂无可用下载链接' : '未找到匹配的技能');
133
+ process.exit(1);
134
+ }
135
+ run(null);
136
+ })().catch((err) => {
137
+ console.error(err.message || err);
138
+ process.exit(1);
56
139
  });
57
140
  }
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
@@ -1,21 +1,105 @@
1
1
  import { join } from 'path';
2
2
  import { homedir, platform } from 'os';
3
- import { mkdir } from 'fs/promises';
4
- import { cwd } from 'process';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { mkdir, writeFile, unlink } from 'fs/promises';
5
5
  import AdmZip from 'adm-zip';
6
6
 
7
7
  // const DEFAULT_BASE_URL = 'http://localhost:8848';
8
8
  const DEFAULT_BASE_URL = 'https://imahub.imaclaw.ai';
9
+
9
10
  /**
10
- * 获取桌面目录路径(跨平台)
11
+ * 获取 openclaw 根目录(skills 的父目录)
12
+ * Mac / Linux: ~/.openclaw,Windows: %USERPROFILE%\.openclaw
11
13
  * @returns {string}
12
14
  */
13
- export function getDesktopPath() {
15
+ function getOpenClawBaseDir() {
16
+ const home = homedir();
17
+ if (platform() === 'win32') {
18
+ const base = process.env.USERPROFILE || home;
19
+ return join(base, '.openclaw');
20
+ }
21
+ return join(home, '.openclaw');
22
+ }
23
+
24
+ /**
25
+ * 检查当前电脑是否已存在 openclaw 目录(即是否“已安装” openclaw 环境)
26
+ * @returns {boolean}
27
+ */
28
+ export function isOpenClawInstalled() {
29
+ return existsSync(getOpenClawBaseDir());
30
+ }
31
+
32
+ /**
33
+ * 获取本机 openclaw 的 skills 目录(默认安装目录)
34
+ * Mac / Linux: ~/.openclaw/skills,Windows: %USERPROFILE%\.openclaw\skills
35
+ * @returns {string}
36
+ */
37
+ export function getOpenClawSkillsDir() {
38
+ return join(getOpenClawBaseDir(), 'skills');
39
+ }
40
+
41
+ /**
42
+ * 确保 openclaw skills 目录存在;若不存在则创建并返回目录路径
43
+ * @returns {Promise<string>}
44
+ */
45
+ export async function ensureOpenClawSkillsDir() {
46
+ const dir = getOpenClawSkillsDir();
47
+ await mkdir(dir, { recursive: true });
48
+ return dir;
49
+ }
50
+
51
+ /** imaclawhub 配置目录:~/.imaclawhub(用于保存 set dir 等配置) */
52
+ function getImaclawhubConfigDir() {
14
53
  const home = homedir();
15
54
  if (platform() === 'win32') {
16
- return process.env.USERPROFILE ? join(process.env.USERPROFILE, 'Desktop') : join(home, 'Desktop');
55
+ const base = process.env.USERPROFILE || home;
56
+ return join(base, '.imaclawhub');
57
+ }
58
+ return join(home, '.imaclawhub');
59
+ }
60
+
61
+ const CONFIG_FILENAME = 'config.json';
62
+
63
+ /**
64
+ * 读取通过 imaclawhub set dir 设置的安装目录,未设置则返回 null
65
+ * @returns {string | null}
66
+ */
67
+ export function getConfiguredOutputDir() {
68
+ try {
69
+ const configPath = join(getImaclawhubConfigDir(), CONFIG_FILENAME);
70
+ if (!existsSync(configPath)) return null;
71
+ const raw = readFileSync(configPath, 'utf-8');
72
+ const data = JSON.parse(raw);
73
+ const dir = data?.outputDir;
74
+ return typeof dir === 'string' && dir.trim() ? dir.trim() : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 保存安装目录(供 imaclawhub set dir 使用)
82
+ * @param {string} dir - 绝对或相对路径
83
+ * @returns {Promise<void>}
84
+ */
85
+ export async function setConfiguredOutputDir(dir) {
86
+ const path = dir.trim();
87
+ if (!path) throw new Error('安装目录不能为空');
88
+ const configDir = getImaclawhubConfigDir();
89
+ await mkdir(configDir, { recursive: true });
90
+ const configPath = join(configDir, CONFIG_FILENAME);
91
+ await writeFile(configPath, JSON.stringify({ outputDir: path }, null, 2), 'utf-8');
92
+ }
93
+
94
+ /**
95
+ * 清空已设置的安装目录(删除 ~/.imaclawhub/config.json 中的 outputDir)
96
+ * @returns {Promise<void>}
97
+ */
98
+ export async function clearConfiguredOutputDir() {
99
+ const configPath = join(getImaclawhubConfigDir(), CONFIG_FILENAME);
100
+ if (existsSync(configPath)) {
101
+ await unlink(configPath);
17
102
  }
18
- return join(home, 'Desktop');
19
103
  }
20
104
 
21
105
  /**
@@ -61,10 +145,10 @@ export async function downloadAndExtractZip(zipUrl, outDir) {
61
145
 
62
146
  /**
63
147
  * 通过 ensure 接口获取技能列表,按选中项下载并解压到指定目录
64
- * 默认解压到「执行命令所在目录」的平级目录(即上级目录下的 <slug> 文件夹)
148
+ * 默认解压到本机 openclaw 的 skills 目录(~/.openclaw/skills/<名称>)
65
149
  * @param {string} skillName - 技能名称或关键词
66
150
  * @param {{ outputDir?: string, subdir?: string, selectedIndex?: number, item?: { slug: string, name?: string, downloadUrl?: string } }} options
67
- * - outputDir: 解压目标父目录,默认 process.cwd() 的上一级
151
+ * - outputDir: 解压目标父目录,默认 getOpenClawSkillsDir()(~/.openclaw/skills)
68
152
  * - subdir: 解压后的文件夹名,默认使用输入的名称(如「AI 衣橱搭配」),为空则用 slug
69
153
  * - selectedIndex: 列表中的序号(0-based),与 item 二选一
70
154
  * - item: 直接指定要下载的项(含 downloadUrl、slug),与 selectedIndex 二选一
@@ -99,9 +183,9 @@ export async function ensureToDesktop(skillName, options = {}) {
99
183
  return { ok: false, error: item.error || '该项无下载链接', slug: item.slug };
100
184
  }
101
185
 
102
- const baseDir = options.outputDir ?? join(cwd(), '..');
186
+ const baseDir = options.outputDir ?? getOpenClawSkillsDir();
103
187
  // 优先使用用户输入的名称作为文件夹名,如 imaclawhub 'AI 衣橱搭配' → 解压到 .../AI 衣橱搭配
104
- const subdir = options.subdir ?? options.desktopSubdir ?? (skillName.replace(/\s+/g, ' ').trim() || item.slug);
188
+ const subdir = options.subdir ?? (skillName.replace(/\s+/g, ' ').trim() || item.slug);
105
189
  const outDir = join(baseDir, subdir);
106
190
  const extractResult = await downloadAndExtractZip(downloadUrl, outDir);
107
191
  if (!extractResult.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imaclawhub",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Fetch skill from local ensure API and extract to Desktop",
5
5
  "type": "module",
6
6
  "main": "index.js",