uniner-cli 1.0.0 → 1.0.1

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.
@@ -21,6 +21,7 @@ const {
21
21
  rootDir,
22
22
  versionDir,
23
23
  skillDir,
24
+ skillFile,
24
25
  } = require('../lib/paths');
25
26
 
26
27
  function log(msg) { console.log(`[uniner-cli] ${msg}`); }
@@ -40,6 +41,20 @@ function rmRf(p) {
40
41
  }
41
42
  }
42
43
 
44
+ function rmFile(p) {
45
+ try {
46
+ if (fs.existsSync(p)) {
47
+ fs.unlinkSync(p);
48
+ log(`Removed ${p}`);
49
+ return true;
50
+ }
51
+ return false;
52
+ } catch (e) {
53
+ warn(`Could not remove ${p}: ${e.message}`);
54
+ return false;
55
+ }
56
+ }
57
+
43
58
  function main() {
44
59
  console.log('');
45
60
  console.log('==========================================');
@@ -71,14 +86,57 @@ function main() {
71
86
  warn(`Could not inspect ${rootDir()}: ${e.message}`);
72
87
  }
73
88
 
74
- // 3. Remove the Claude Code skill.
75
- if (rmRf(skillDir())) didSomething = true;
89
+ // 3. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
90
+ // because the user's accumulated scenarios/<name>/ subdirectories
91
+ // live there and represent real work they've sedimented over time.
92
+ // If they really want everything gone they can do it manually; we
93
+ // surface the path below so they know where to look.
94
+ const dir = skillDir();
95
+ if (rmFile(skillFile())) didSomething = true;
96
+
97
+ // 4. Look for scenarios/ — if there are any, keep them and tell the user.
98
+ let scenariosKept = 0;
99
+ const scenariosDir = path.join(dir, 'scenarios');
100
+ if (fs.existsSync(scenariosDir)) {
101
+ try {
102
+ scenariosKept = fs.readdirSync(scenariosDir).filter(n => {
103
+ const meta = path.join(scenariosDir, n, 'meta.json');
104
+ return fs.existsSync(meta);
105
+ }).length;
106
+ } catch (_) {
107
+ // best-effort
108
+ }
109
+ }
110
+
111
+ // 5. If skill dir is now genuinely empty, remove it. Otherwise keep it.
112
+ try {
113
+ if (fs.existsSync(dir)) {
114
+ const remaining = fs.readdirSync(dir);
115
+ if (remaining.length === 0) {
116
+ fs.rmdirSync(dir);
117
+ log(`Removed empty ${dir}`);
118
+ didSomething = true;
119
+ }
120
+ }
121
+ } catch (_) {
122
+ // best-effort
123
+ }
76
124
 
77
125
  if (!didSomething) {
78
126
  log('Nothing to clean — already removed.');
79
127
  }
80
128
 
81
129
  console.log('');
130
+ if (scenariosKept > 0) {
131
+ console.log('==========================================');
132
+ console.log(` ${scenariosKept} scenario(s) PRESERVED at:`);
133
+ console.log(` ${scenariosDir}`);
134
+ console.log(' (your accumulated knowledge — not deleted)');
135
+ console.log(' To delete them too:');
136
+ console.log(` rm -rf "${scenariosDir}" # bash`);
137
+ console.log(` Remove-Item "${scenariosDir}" -Recurse -Force # PowerShell`);
138
+ console.log('==========================================');
139
+ }
82
140
  console.log('Cleanup done. To complete uninstall, also run:');
83
141
  console.log(' npm uninstall -g uniner-cli');
84
142
  console.log('');
@@ -179,6 +179,177 @@ uniner-cli excel data write-range --file "{{INSTALL_PATH}}\workdir\out.xlsx" --c
179
179
  uniner-cli web browser close
180
180
  ```
181
181
 
182
+ ## 场景沉淀(重要:完成任务后主动建议保存)
183
+
184
+ 每完成一个完整 RPA 流程后,**主动建议**用户把它沉淀为可重放的场景。
185
+ 之后用户用 `uniner-cli skill run <name>` 即可 **0 token 重放**——
186
+ 不再需要 Claude,直接命令行就能跑、能放进定时任务 / Airflow / RPA 平台。
187
+
188
+ ### 何时建议保存
189
+
190
+ 下面任一情况都该建议:
191
+ - 任务跑完且至少调用了 ≥3 个 uniner-cli 命令
192
+ - 任务里踩过坑(XPath 不稳、参数名错、Element UI 组件等)
193
+ - 用户表达过 "以后还会跑"
194
+
195
+ ### 怎么保存:3 个文件 + 4 步流程
196
+
197
+ 每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
198
+
199
+ ```
200
+ %USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\
201
+ ```
202
+
203
+ **4 步标准流程**:
204
+
205
+ ```bash
206
+ # Step 1: 生成骨架(自动创建目录和模板文件)
207
+ uniner-cli skill init <name>
208
+
209
+ # Step 2: 用 Write 工具改 meta.json / steps.json / SKILL.md(见下方 schema)
210
+
211
+ # Step 3: 校验
212
+ uniner-cli skill validate <name>
213
+
214
+ # Step 4: 试跑一次
215
+ uniner-cli skill run <name> --param ...
216
+ ```
217
+
218
+ ### meta.json schema
219
+
220
+ ```json
221
+ {
222
+ "name": "<kebab-case-name>",
223
+ "version": "1.0",
224
+ "description": "一句话描述(会被用来匹配触发关键词)",
225
+ "params": {
226
+ "URL": { "type": "string", "required": true, "description": "登录页地址" },
227
+ "USERNAME": { "type": "string", "required": true, "description": "账号" },
228
+ "PASSWORD": { "type": "string", "required": true, "sensitive": true, "description": "密码" },
229
+ "DATE_FROM": { "type": "string", "required": false, "default": "today", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
230
+ }
231
+ }
232
+ ```
233
+
234
+ ### steps.json schema
235
+
236
+ ```json
237
+ {
238
+ "version": "1.0",
239
+ "name": "<同 meta.name>",
240
+ "steps": [
241
+ {
242
+ "id": "open-browser",
243
+ "cmd": "web browser open",
244
+ "args": { "url": "${URL}" },
245
+ "timeout_ms": 10000
246
+ },
247
+ {
248
+ "id": "ocr-captcha",
249
+ "cmd": "tool ocr ddddocr",
250
+ "args": { "img": "workdir/captcha.png" },
251
+ "store": "ocrResult",
252
+ "comment": "整个 stdout JSON 存到 ocrResult,下面用 ${ocrResult.data.result} 取"
253
+ },
254
+ {
255
+ "id": "input-captcha",
256
+ "cmd": "web element input",
257
+ "args": { "xpath": "//input[@placeholder='验证码']", "value": "${ocrResult.data.result}" }
258
+ },
259
+ {
260
+ "id": "close-browser",
261
+ "cmd": "web browser close",
262
+ "always_run": true,
263
+ "comment": "无论前面成功与否都跑(清理用)"
264
+ }
265
+ ]
266
+ }
267
+ ```
268
+
269
+ ### 每个 step 支持的字段
270
+
271
+ | 字段 | 必需 | 说明 |
272
+ |---|---|---|
273
+ | `id` | ✅ | 步骤 ID(唯一) |
274
+ | `cmd` | ✅ | uniner-cli 子命令,**不带 `uniner-cli` 前缀**,如 `web element input` |
275
+ | `args` | ✅ | dict 形式参数,对应 CLI 的 `--key value` |
276
+ | `store` | ❌ | 把 stdout JSON 整个存到变量名 |
277
+ | `if` | ❌ | 条件表达式(见下) |
278
+ | `on_fail` | ❌ | `abort`(默认)/ `continue` / `goto:<step_id>` |
279
+ | `retry` | ❌ | 失败重试次数(不含首次) |
280
+ | `retry_delay_ms` | ❌ | 重试间隔(默认 1000) |
281
+ | `timeout_ms` | ❌ | 单步超时(覆盖全局) |
282
+ | `always_run` | ❌ | 即使前面失败也跑(清理用) |
283
+ | `comment` | ❌ | 给人/AI 看的注释 |
284
+
285
+ ### 变量替换语法
286
+
287
+ | 写法 | 含义 |
288
+ |---|---|
289
+ | `${URL}` | 用户传入的 `--param URL=xxx` |
290
+ | `${ocrResult.data.result}` | 上一步 store 的 dict 字段(点号路径) |
291
+ | `${tableData.0.1}` | list 索引(0-based) |
292
+ | `${env:USERPROFILE}` | 环境变量 |
293
+
294
+ ### 条件表达式语法(白名单)
295
+
296
+ | 写法 | 含义 |
297
+ |---|---|
298
+ | `${var}` | 真值判断 |
299
+ | `!${var}` | 取反 |
300
+ | `${var} == 'yes'` | 字符串/数值相等 |
301
+ | `${var} != 'no'` | 不等 |
302
+ | `${var} > 5` | 数值比较(>=, <=, < 同理) |
303
+ | `${var}.includes('login')` | 包含 |
304
+ | `${var}.startsWith('https')` / `endsWith('.xlsx')` | 前/后缀 |
305
+ | `!${var}.includes('admin')` | 取反组合 |
306
+
307
+ **故意不支持** `&&` / `||` / `()`。要复合逻辑用多步 + `if` + `on_fail` 拆开。
308
+
309
+ ### 失败时的客户体验
310
+
311
+ ```
312
+ $ uniner-cli skill run tms-export --param URL=...
313
+ {"success": false, "failed_step": "click-search", ...}
314
+
315
+ ────────── How to debug ──────────
316
+ Step 'click-search' failed: timeout
317
+ Open Claude Code, paste this:
318
+
319
+ I ran 'uniner-cli skill run' and step 'click-search' failed.
320
+ Error: timeout
321
+ Please look at the scenario at:
322
+ C:\Users\xxx\.claude\skills\uniner-cli\scenarios\tms-export
323
+ Especially steps.json. Help me figure out why and suggest a fix.
324
+ ──────────────────────────────────
325
+ ```
326
+
327
+ 客户复制这段给你 → 你 Read steps.json 调试 → 改完 `Write` 回去 → 让客户重跑。
328
+
329
+ ### 给 Claude 的对话范式
330
+
331
+ 完成任务后说类似的话(**主动询问,不等用户开口**):
332
+
333
+ > "这个流程涉及 OCR、Vue 组件、XPath 等不稳定环节,我帮你保存为可重放场景吧?以后你直接 `uniner-cli skill run <name>` 就能 0 token 重放,还能放定时任务里跑。要保存吗?"
334
+
335
+ 得到确认后:
336
+
337
+ 1. 想一个 kebab-case 名字(如 `tms-export-waybills` / `sap-monthly-report`)
338
+ 2. Bash 跑 `uniner-cli skill init <name>`
339
+ 3. Write 写 meta.json(含动态参数 schema)+ steps.json(完整步骤 + 已知坑的避坑配置)+ SKILL.md(经验文档)
340
+ 4. Bash 跑 `uniner-cli skill validate <name>`
341
+ 5. 让用户 `uniner-cli skill run <name> --param ...` 自己跑一次确认 0 token 重放可行
342
+
343
+ ### 已沉淀的场景库
344
+
345
+ ```bash
346
+ uniner-cli skill list # 看所有
347
+ uniner-cli skill show <name> # 看某个的元数据 + 步骤摘要
348
+ uniner-cli skill show <name> --steps # 看完整步骤
349
+ uniner-cli skill where # 看场景库根目录(可手动浏览)
350
+ uniner-cli skill delete <name> --yes # 删除(默认 dry-run)
351
+ ```
352
+
182
353
  ## 不确定时
183
354
 
184
355
  - `uniner-cli --help` 看一级
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "uniner-cli",
3
- "version": "1.0.0",
4
- "description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
5
- "keywords": [
6
- "rpa",
7
- "automation",
8
- "uniner",
9
- "playwright",
10
- "excel",
11
- "ocr",
12
- "claude-code",
13
- "ai-agent"
14
- ],
15
- "homepage": "https://www.uniner.com/",
16
- "bugs": {
17
- "url": "https://www.uniner.com/",
18
- "email": "support@uniner.com"
19
- },
20
- "license": "Proprietary",
21
- "author": "Uniner Tech",
22
- "main": "lib/paths.js",
23
- "bin": {
24
- "uniner-cli": "bin/uniner-cli.js",
25
- "uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
26
- },
27
- "scripts": {
28
- "postinstall": "node scripts/postinstall.js"
29
- },
30
- "engines": {
31
- "node": ">=14"
32
- },
33
- "os": [
34
- "win32"
35
- ],
36
- "cpu": [
37
- "x64"
38
- ],
39
- "files": [
40
- "bin/",
41
- "lib/",
42
- "scripts/",
43
- "README.md",
44
- "LICENSE"
45
- ]
46
- }
1
+ {
2
+ "name": "uniner-cli",
3
+ "version": "1.0.1",
4
+ "description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
5
+ "keywords": [
6
+ "rpa",
7
+ "automation",
8
+ "uniner",
9
+ "playwright",
10
+ "excel",
11
+ "ocr",
12
+ "claude-code",
13
+ "ai-agent"
14
+ ],
15
+ "homepage": "https://www.uniner.com/",
16
+ "bugs": {
17
+ "url": "https://www.uniner.com/",
18
+ "email": "support@uniner.com"
19
+ },
20
+ "license": "Proprietary",
21
+ "author": "Uniner Tech",
22
+ "main": "lib/paths.js",
23
+ "bin": {
24
+ "uniner-cli": "bin/uniner-cli.js",
25
+ "uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
26
+ },
27
+ "scripts": {
28
+ "postinstall": "node scripts/postinstall.js"
29
+ },
30
+ "engines": {
31
+ "node": "\u003e=14"
32
+ },
33
+ "os": [
34
+ "win32"
35
+ ],
36
+ "cpu": [
37
+ "x64"
38
+ ],
39
+ "files": [
40
+ "bin/",
41
+ "lib/",
42
+ "scripts/",
43
+ "README.md",
44
+ "LICENSE"
45
+ ]
46
+ }
@@ -231,8 +231,28 @@ function writeSkillMd(installPath, version) {
231
231
 
232
232
  const dir = skillDir();
233
233
  fs.mkdirSync(dir, { recursive: true });
234
+
235
+ // IMPORTANT: only writeFileSync the single SKILL.md file. Do NOT touch
236
+ // anything else under ~/.claude/skills/uniner-cli/. In particular, the
237
+ // scenarios/ subdirectory is the user's accumulated knowledge — if we
238
+ // ever rmdir or rmSync this directory we'd wipe their library on every
239
+ // upgrade. Single-file write is the contract.
234
240
  fs.writeFileSync(skillFile(), content, 'utf8');
235
241
  log(` SKILL.md written to ${skillFile()}`);
242
+
243
+ // Tell the user we kept their scenarios (if any).
244
+ const scenariosDir = path.join(dir, 'scenarios');
245
+ if (fs.existsSync(scenariosDir)) {
246
+ try {
247
+ const count = fs.readdirSync(scenariosDir).filter(n => {
248
+ const meta = path.join(scenariosDir, n, 'meta.json');
249
+ return fs.existsSync(meta);
250
+ }).length;
251
+ log(` [INFO] Preserved ${count} scenario(s) in ${scenariosDir}`);
252
+ } catch (_) {
253
+ // best-effort; never fail postinstall over a logging stat
254
+ }
255
+ }
236
256
  }
237
257
 
238
258
  // ----------------------------------------------------------------------