@jiangyuan1209/yuan-claw 0.1.2 → 0.1.3

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
@@ -1,3 +1,4 @@
1
+ ```md
1
2
  # yuan-claw
2
3
 
3
4
  一个基于 **Node.js + TypeScript** 的本地 CLI Agent。
@@ -14,7 +15,8 @@
14
15
  - 🖥️ 支持交互式 REPL
15
16
  - ✅ 支持工具执行前确认(approval)
16
17
  - ⌨️ 支持 `↑ / ↓ / Enter` 选择确认项
17
- - 🔁 支持会话级“总是允许”模式
18
+ - 🔁 支持会话级”总是允许”模式
19
+ - 📦 支持本地 Skill 插件扩展能力
18
20
  - 🔌 支持代理配置
19
21
  - 🛠️ 基于 TypeScript,便于二次开发
20
22
 
@@ -29,80 +31,236 @@
29
31
 
30
32
  ## Installation
31
33
 
34
+ ### 方式一:通过 npm 全局安装(推荐)
35
+
36
+ ```bash
37
+ npm install -g @jiangyuan1209/yuan-claw
38
+ ```
39
+
40
+ 安装完成后可直接使用:
41
+
42
+ ```bash
43
+ yuan-claw
44
+ ```
45
+
46
+ 卸载命令:
47
+
48
+ ```bash
49
+ npm uninstall -g @jiangyuan1209/yuan-claw
50
+ ```
51
+
52
+ 如需安装指定版本:
53
+
54
+ ```bash
55
+ npm install -g @jiangyuan1209/yuan-claw@0.1.2
56
+ ```
57
+
58
+ 升级到最新版:
59
+
60
+ ```bash
61
+ npm install -g @jiangyuan1209/yuan-claw@latest
62
+ ```
63
+
64
+ ---
65
+
66
+ ### 方式二:从源码安装
67
+
32
68
  ```bash
33
69
  git clone https://github.com/your-name/yuan-claw.git
34
70
  cd yuan-claw
35
71
  npm install
72
+ npm run build
73
+ ```
74
+
75
+ 源码模式下运行:
76
+
77
+ ```bash
78
+ npm run dev
79
+ ```
80
+
81
+ 或:
82
+
83
+ ```bash
84
+ npm run start
36
85
  ```
37
86
 
38
87
  ---
39
88
 
40
89
  ## Quick Start
41
90
 
42
- ### 1. 配置环境变量
91
+ ### 1. 首次运行初始化配置
43
92
 
44
- 在项目根目录创建 `.env` 文件:
93
+ 安装完成后,先执行一次:
45
94
 
46
- ```env
47
- MODEL_API_KEY=
48
- MODEL_BASE_URL=
49
- MODEL_NAME=
95
+ ```bash
96
+ yuan-claw
97
+ ```
50
98
 
51
- BRAVE_SEARCH_API_KEY=
99
+ 程序会在你的用户目录下自动初始化配置文件:
52
100
 
53
- HTTP_PROXY=
54
- HTTPS_PROXY=
101
+ ```bash
102
+ ~/.yuan-claw/settings.json
103
+ ```
104
+
105
+ 在 macOS / Linux 上通常类似:
106
+
107
+ ```bash
108
+ /Users/你的用户名/.yuan-claw/settings.json
109
+ ```
110
+
111
+ 如果你使用的是 Windows,则通常位于用户目录下对应的 `.yuan-claw` 文件夹中。
112
+
113
+ > 注意:第一次运行通常只是创建配置文件。
114
+ > 你需要手动填写配置后,再次执行 `yuan-claw` 才会真正生效。
115
+
116
+ ---
117
+
118
+ ### 2. 编辑 `~/.yuan-claw/settings.json`
119
+
120
+ 示例:
121
+
122
+ ```json
123
+ {
124
+ "MODEL_API_KEY": "your_api_key",
125
+ "MODEL_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
126
+ "MODEL_NAME": "qwen3-max-2026-01-23",
127
+ "BRAVE_SEARCH_API_KEY": "your_brave_search_api_key",
128
+ "HTTP_PROXY": "http://127.0.0.1:33210",
129
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
130
+ }
55
131
  ```
56
132
 
57
133
  最小可用配置通常只需要:
58
134
 
59
- ```env
60
- MODEL_API_KEY=your_api_key
61
- MODEL_BASE_URL=https://api.openai.com/v1
62
- MODEL_NAME=gpt-4o-mini
135
+ ```json
136
+ {
137
+ "MODEL_API_KEY": "your_api_key",
138
+ "MODEL_BASE_URL": "https://api.openai.com/v1",
139
+ "MODEL_NAME": "gpt-4o-mini"
140
+ }
141
+ ```
142
+
143
+ 如果你希望启用网页搜索,还可以配置:
144
+
145
+ ```json
146
+ {
147
+ "BRAVE_SEARCH_API_KEY": "your_brave_search_api_key"
148
+ }
149
+ ```
150
+
151
+ 如需使用代理,可以配置:
152
+
153
+ ```json
154
+ {
155
+ "HTTP_PROXY": "http://127.0.0.1:33210",
156
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
157
+ }
158
+ ```
159
+
160
+ 保存后,重新执行:
161
+
162
+ ```bash
163
+ yuan-claw
63
164
  ```
64
165
 
65
166
  ---
66
167
 
67
- ### 2. 开发模式运行
168
+ ### 3. 开始使用
169
+
170
+ #### 交互式 REPL
171
+
172
+ ```bash
173
+ yuan-claw
174
+ ```
68
175
 
69
176
  #### 单次命令
70
177
 
71
178
  ```bash
72
- npm run dev -- "帮我搜索 OpenAI 最新消息"
179
+ yuan-claw "帮我搜索 OpenAI 最新消息"
73
180
  ```
74
181
 
75
- #### 交互式 REPL
182
+ ---
183
+
184
+ ## Configuration
185
+
186
+ 项目优先从用户目录中的配置文件读取配置:
76
187
 
77
188
  ```bash
78
- npm run dev
189
+ ~/.yuan-claw/settings.json
79
190
  ```
80
191
 
81
- 启动后你会看到:
192
+ ### 支持的配置项
82
193
 
83
- ```txt
84
- Welcome to yuan-claw!
85
- Type /help for commands, /exit to quit.
86
- Approval mode is shown in the prompt: [ask] or [always].
194
+ - `MODEL_API_KEY`
195
+ - `MODEL_BASE_URL`
196
+ - `MODEL_NAME`
197
+ - `OPENAI_API_KEY`
198
+ - `OPENAI_BASE_URL`
199
+ - `OPENAI_MODEL`
200
+ - `BRAVE_SEARCH_API_KEY`
201
+ - `HTTP_PROXY`
202
+ - `HTTPS_PROXY`
203
+ - `http_proxy`
204
+ - `https_proxy`
87
205
 
88
- yuan-claw[ask]>
206
+ ### 推荐配置示例
207
+
208
+ #### OpenAI-compatible 通用配置
209
+
210
+ ```json
211
+ {
212
+ "MODEL_API_KEY": "your_api_key",
213
+ "MODEL_BASE_URL": "https://api.openai.com/v1",
214
+ "MODEL_NAME": "gpt-4o-mini"
215
+ }
216
+ ```
217
+
218
+ #### DashScope 示例
219
+
220
+ ```json
221
+ {
222
+ "MODEL_API_KEY": "your_dashscope_api_key",
223
+ "MODEL_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
224
+ "MODEL_NAME": "qwen3-max-2026-01-23",
225
+ "BRAVE_SEARCH_API_KEY": "your_brave_search_api_key",
226
+ "HTTP_PROXY": "http://127.0.0.1:33210",
227
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
228
+ }
89
229
  ```
90
230
 
91
231
  ---
92
232
 
93
- ### 3. 编译后运行
233
+ ## Environment Variables(兼容方式)
94
234
 
95
- ```bash
96
- npm run build
97
- npm run start
235
+ 除了 `~/.yuan-claw/settings.json` 外,开发者也可以在项目根目录通过 `.env` 配置:
236
+
237
+ ```env
238
+ MODEL_API_KEY=
239
+ MODEL_BASE_URL=
240
+ MODEL_NAME=
241
+
242
+ BRAVE_SEARCH_API_KEY=
243
+
244
+ HTTP_PROXY=
245
+ HTTPS_PROXY=
98
246
  ```
99
247
 
100
- 单次命令模式:
248
+ 也支持 OpenAI 风格变量名:
101
249
 
102
- ```bash
103
- npm run start -- "帮我总结这个项目的功能"
250
+ ```env
251
+ OPENAI_API_KEY=
252
+ OPENAI_BASE_URL=
253
+ OPENAI_MODEL=
104
254
  ```
105
255
 
256
+ > 普通 CLI 用户更推荐使用:
257
+ >
258
+ > ```bash
259
+ > ~/.yuan-claw/settings.json
260
+ > ```
261
+ >
262
+ > `.env` 更适合源码开发或本地调试。
263
+
106
264
  ---
107
265
 
108
266
  ## REPL Usage
@@ -163,86 +321,37 @@ yuan-claw[always]>
163
321
 
164
322
  ---
165
323
 
166
- ## Scripts
167
-
168
- ```json
169
- {
170
- "scripts": {
171
- "dev": "tsx src/cli/main.ts",
172
- "build": "tsc -p tsconfig.json",
173
- "start": "node dist/cli/main.js",
174
- "check": "tsc --noEmit"
175
- }
176
- }
177
- ```
178
-
179
- ### 脚本说明
180
-
181
- - `npm run dev`:开发模式运行源码
182
- - `npm run build`:编译到 `dist/`
183
- - `npm run start`:运行编译后的 CLI
184
- - `npm run check`:执行 TypeScript 类型检查
185
-
186
- 说明:
187
-
188
- - `npm run dev` / `npm run start`
189
- - 不传参数:进入 REPL
190
- - 传入参数:执行单次命令
191
-
192
- ---
193
-
194
324
  ## CLI Usage
195
325
 
196
- 项目在 `package.json` 中定义了可执行命令:
197
-
198
- ```json
199
- "bin": {
200
- "yuan-claw": "./dist/cli/main.js"
201
- }
202
- ```
203
-
204
- 构建并安装后,可以直接使用:
326
+ 全局安装后可直接运行:
205
327
 
206
328
  ```bash
207
329
  yuan-claw
208
330
  ```
209
331
 
210
- 或:
332
+ 单次命令模式:
211
333
 
212
334
  ```bash
213
335
  yuan-claw "帮我搜索 AI 新闻"
214
336
  ```
215
337
 
216
- ---
217
-
218
- ## Environment Variables
219
-
220
- 项目使用 **OpenAI-compatible API**,按以下优先级读取配置:
338
+ 如果你是源码模式开发,则可以使用:
221
339
 
222
- ### API Key
223
-
224
- 1. `MODEL_API_KEY`
225
- 2. `OPENAI_API_KEY`
226
-
227
- ### Base URL
228
-
229
- 1. `MODEL_BASE_URL`
230
- 2. `OPENAI_BASE_URL`
231
-
232
- ### Model Name
340
+ ```bash
341
+ npm run dev
342
+ ```
233
343
 
234
- 1. `MODEL_NAME`
235
- 2. `OPENAI_MODEL`
236
- 3. 默认值:`gpt-4o-mini`
344
+ 或:
237
345
 
238
- ### OpenAI 风格兼容变量
346
+ ```bash
347
+ npm run dev -- "帮我总结这个项目的功能"
348
+ ```
239
349
 
240
- 如果你更习惯 OpenAI 风格变量名,也可以使用:
350
+ 编译后运行:
241
351
 
242
- ```env
243
- OPENAI_API_KEY=
244
- OPENAI_BASE_URL=
245
- OPENAI_MODEL=
352
+ ```bash
353
+ npm run build
354
+ npm run start
246
355
  ```
247
356
 
248
357
  ---
@@ -251,8 +360,10 @@ OPENAI_MODEL=
251
360
 
252
361
  如果你希望启用网页搜索工具,请配置:
253
362
 
254
- ```env
255
- BRAVE_SEARCH_API_KEY=your_brave_search_api_key
363
+ ```json
364
+ {
365
+ "BRAVE_SEARCH_API_KEY": "your_brave_search_api_key"
366
+ }
256
367
  ```
257
368
 
258
369
  未配置时,`web_search` 工具会被禁用。
@@ -263,12 +374,14 @@ BRAVE_SEARCH_API_KEY=your_brave_search_api_key
263
374
 
264
375
  如需通过代理访问模型服务或外部网站,可以配置:
265
376
 
266
- ```env
267
- HTTP_PROXY=http://127.0.0.1:33210
268
- HTTPS_PROXY=http://127.0.0.1:33210
377
+ ```json
378
+ {
379
+ "HTTP_PROXY": "http://127.0.0.1:33210",
380
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
381
+ }
269
382
  ```
270
383
 
271
- 程序会自动读取以下变量:
384
+ 程序会自动读取以下配置项:
272
385
 
273
386
  - `HTTP_PROXY`
274
387
  - `HTTPS_PROXY`
@@ -277,41 +390,124 @@ HTTPS_PROXY=http://127.0.0.1:33210
277
390
 
278
391
  ---
279
392
 
280
- ## Recommended `.env.example`
393
+ ## Skills / 本地插件扩展
281
394
 
282
- 建议在仓库中提供如下 `.env.example`:
395
+ yuan-claw 支持通过本地 Skill(技能)文件扩展 Agent 的能力。Skill 是存放在固定目录下的 Markdown 文件,Agent 会在运行时根据用户输入自动匹配并加载相关 Skill 的提示词,从而获得领域知识或操作指引。
283
396
 
284
- ```env
285
- # =========================
286
- # 模型服务配置(推荐)
287
- # =========================
397
+ ### Skill 目录结构
288
398
 
289
- MODEL_API_KEY=
290
- MODEL_BASE_URL=
291
- MODEL_NAME=
399
+ 所有 Skill 文件存放在:
292
400
 
293
- # =========================
294
- # OpenAI 风格兼容变量(可选)
295
- # =========================
401
+ ```bash
402
+ ~/.yuan-claw/skills/
403
+ ```
296
404
 
297
- OPENAI_API_KEY=
298
- OPENAI_BASE_URL=
299
- OPENAI_MODEL=
405
+ 每个 Skill 是一个子目录,其中必须包含一个 `SKILL.md` 文件:
300
406
 
301
- # =========================
302
- # 网页搜索配置
303
- # =========================
407
+ ```
408
+ ~/.yuan-claw/skills/
409
+ ├── pdf/
410
+ │ └── SKILL.md
411
+ ├── frontend-design/
412
+ │ └── SKILL.md
413
+ └── my-skill/
414
+ └── SKILL.md
415
+ ```
304
416
 
305
- BRAVE_SEARCH_API_KEY=
417
+ ### SKILL.md 文件格式
306
418
 
307
- # =========================
308
- # 代理配置(可选)
309
- # =========================
419
+ `SKILL.md` 使用 YAML frontmatter + Markdown body 的格式:
310
420
 
311
- HTTP_PROXY=
312
- HTTPS_PROXY=
421
+ ```markdown
422
+ ---
423
+ name: pdf
424
+ description: PDF 文件处理技能,支持提取文本、表格、OCR 等
425
+ tags: [pdf, document, ocr]
426
+ license: MIT
427
+ version: 1.0.0
428
+ ---
429
+
430
+ ## 使用指南
431
+
432
+ 当用户需要处理 PDF 文件时:
433
+ 1. 使用 pdfplumber 提取文本...
434
+ 2. 对于扫描件,使用 OCR...
313
435
  ```
314
436
 
437
+ Frontmatter 字段说明:
438
+
439
+ - `name`(可选):Skill 名称,用于匹配和展示。未填写时使用目录名
440
+ - `description`(可选):简短描述,用于匹配和展示
441
+ - `tags`(可选):标签数组,用于关键词匹配
442
+ - `license`(可选):许可证
443
+ - `version`(可选):版本号
444
+
445
+ Markdown body 是 Skill 的实际提示词内容,会被注入到 system prompt 中指导 Agent 行为。
446
+
447
+ ### 匹配机制
448
+
449
+ Agent 会根据用户输入自动匹配最相关的 Skill(最多匹配 3 个),匹配依据包括:
450
+
451
+ - 用户输入中是否包含 Skill 名称
452
+ - 用户输入中是否包含 Skill 的标签
453
+ - 用户输入分词后与 Skill 描述的关键词重合度
454
+
455
+ 匹配到的 Skill 内容会被组装到 system prompt 中,指导 Agent 使用相应的知识和流程。
456
+
457
+ ### 添加自定义 Skill
458
+
459
+ 1. 在 `~/.yuan-claw/skills/` 下创建新目录:
460
+
461
+ ```bash
462
+ mkdir -p ~/.yuan-claw/skills/my-skill
463
+ ```
464
+
465
+ 2. 创建 `SKILL.md` 文件:
466
+
467
+ ```bash
468
+ cat > ~/.yuan-claw/skills/my-skill/SKILL.md << 'EOF'
469
+ ---
470
+ name: my-skill
471
+ description: 我的自定义技能
472
+ tags: [custom]
473
+ ---
474
+
475
+ 当用户问到 XXX 时,请按以下步骤操作:
476
+ 1. ...
477
+ 2. ...
478
+ EOF
479
+ ```
480
+
481
+ 3. 下次运行 `yuan-claw` 时,该 Skill 会被自动发现和加载。
482
+
483
+ ---
484
+
485
+ ## Scripts
486
+
487
+ ```json
488
+ {
489
+ "scripts": {
490
+ "dev": "tsx src/cli/main.ts",
491
+ "build": "tsc -p tsconfig.json",
492
+ "start": "node dist/cli/main.js",
493
+ "check": "tsc --noEmit"
494
+ }
495
+ }
496
+ ```
497
+
498
+ ### 脚本说明
499
+
500
+ - `npm run dev`:开发模式运行源码
501
+ - `npm run build`:编译到 `dist/`
502
+ - `npm run start`:运行编译后的 CLI
503
+ - `npm run check`:执行 TypeScript 类型检查
504
+
505
+ 说明:
506
+
507
+ - `npm run dev` / `npm run start`
508
+ - 不传参数:进入 REPL
509
+ - 传入参数:执行单次命令
510
+
315
511
  ---
316
512
 
317
513
  ## Examples
@@ -319,17 +515,23 @@ HTTPS_PROXY=
319
515
  ### 普通问答
320
516
 
321
517
  ```bash
322
- npm run dev -- "帮我总结这个项目的功能"
518
+ yuan-claw "帮我总结这个项目的功能"
323
519
  ```
324
520
 
325
521
  ### 搜索最新消息
326
522
 
327
523
  ```bash
328
- npm run dev -- "帮我搜索 OpenAI 最新消息"
524
+ yuan-claw "帮我搜索 OpenAI 最新消息"
329
525
  ```
330
526
 
331
527
  ### 进入 REPL
332
528
 
529
+ ```bash
530
+ yuan-claw
531
+ ```
532
+
533
+ ### 源码开发模式
534
+
333
535
  ```bash
334
536
  npm run dev
335
537
  ```
@@ -345,18 +547,42 @@ npm run start -- "帮我搜索 AI 新闻"
345
547
 
346
548
  ## Troubleshooting
347
549
 
348
- ### `Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables.`
550
+ ### 第一次运行后为什么没有立即生效?
349
551
 
350
- 说明模型 API Key 未配置。请至少设置其一:
552
+ 因为第一次执行:
351
553
 
352
- ```env
353
- MODEL_API_KEY=your_api_key
554
+ ```bash
555
+ yuan-claw
354
556
  ```
355
557
 
356
- 或:
558
+ 通常只是为了初始化配置文件:
357
559
 
358
- ```env
359
- OPENAI_API_KEY=your_api_key
560
+ ```bash
561
+ ~/.yuan-claw/settings.json
562
+ ```
563
+
564
+ 你需要手动填写配置项并保存,然后再次执行:
565
+
566
+ ```bash
567
+ yuan-claw
568
+ ```
569
+
570
+ ---
571
+
572
+ ### `Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables.`
573
+
574
+ 说明模型 API Key 尚未正确配置。请至少在以下任一位置填写:
575
+
576
+ - `~/.yuan-claw/settings.json`
577
+ - 系统环境变量
578
+ - 项目根目录 `.env`
579
+
580
+ 例如:
581
+
582
+ ```json
583
+ {
584
+ "MODEL_API_KEY": "your_api_key"
585
+ }
360
586
  ```
361
587
 
362
588
  ---
@@ -365,19 +591,23 @@ OPENAI_API_KEY=your_api_key
365
591
 
366
592
  说明未配置 Brave Search API Key。请添加:
367
593
 
368
- ```env
369
- BRAVE_SEARCH_API_KEY=your_key
594
+ ```json
595
+ {
596
+ "BRAVE_SEARCH_API_KEY": "your_brave_search_api_key"
597
+ }
370
598
  ```
371
599
 
372
600
  ---
373
601
 
374
602
  ### 无法访问外部服务 / 请求超时
375
603
 
376
- 请检查是否需要代理:
604
+ 请检查是否需要代理,例如:
377
605
 
378
- ```env
379
- HTTP_PROXY=http://127.0.0.1:33210
380
- HTTPS_PROXY=http://127.0.0.1:33210
606
+ ```json
607
+ {
608
+ "HTTP_PROXY": "http://127.0.0.1:33210",
609
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
610
+ }
381
611
  ```
382
612
 
383
613
  ---
@@ -405,13 +635,33 @@ dist/cli/main.js
405
635
 
406
636
  ---
407
637
 
638
+ ### 全局命令 `yuan-claw` 不存在
639
+
640
+ 如果你是通过 npm 全局安装,请确认已成功安装:
641
+
642
+ ```bash
643
+ npm install -g @jiangyuan1209/yuan-claw
644
+ ```
645
+
646
+ 如仍有问题,可尝试重新安装:
647
+
648
+ ```bash
649
+ npm uninstall -g @jiangyuan1209/yuan-claw
650
+ npm install -g @jiangyuan1209/yuan-claw@latest
651
+ ```
652
+
653
+ ---
654
+
408
655
  ## Development
409
656
 
410
657
  ```bash
658
+ npm install
411
659
  npm run check
412
660
  npm run dev
413
661
  ```
414
662
 
663
+ 如果你使用源码开发方式,也可以在项目根目录创建 `.env` 文件辅助调试。
664
+
415
665
  ---
416
666
 
417
667
  ## Roadmap
@@ -428,14 +678,4 @@ npm run dev
428
678
 
429
679
  ## License
430
680
 
431
- MIT
432
- ```
433
-
434
-
435
- ## `.gitignore`
436
-
437
- ```gitignore
438
- node_modules
439
- dist
440
- .env
441
- ```
681
+ MIT
@@ -9,7 +9,10 @@ function formatToolsForPrompt(tools) {
9
9
  })
10
10
  .join("\n");
11
11
  }
12
- export function buildSystemPrompt(tools) {
12
+ export function buildSystemPrompt(tools, skillsPrompt) {
13
+ const skillsSection = skillsPrompt
14
+ ? ["", "LOCAL SKILLS:", skillsPrompt].join("\n")
15
+ : "";
13
16
  return [
14
17
  "You are a local CLI coding agent.",
15
18
  "You do not have direct filesystem, shell, git, or network access unless you use the provided tools.",
@@ -37,6 +40,7 @@ export function buildSystemPrompt(tools) {
37
40
  "",
38
41
  "AVAILABLE TOOLS:",
39
42
  formatToolsForPrompt(tools),
43
+ skillsSection,
40
44
  "",
41
45
  "Examples:",
42
46
  '{"type":"tool_call","toolName":"read_file","args":{"path":"package.json"}}',
@@ -1,5 +1,6 @@
1
1
  import { buildSystemPrompt } from "./build-system-prompt.js";
2
2
  import { parseAgentResponse } from "./parse-agent-response.js";
3
+ import { SkillsRuntime } from "../skills/runtime.js";
3
4
  function stringifyForModel(value) {
4
5
  try {
5
6
  return JSON.stringify(value);
@@ -30,11 +31,15 @@ export async function runLocalAgentLoop(params) {
30
31
  type: "run_start",
31
32
  input: userInput,
32
33
  });
34
+ // Load and match skills based on user input
35
+ const skillsRuntime = new SkillsRuntime();
36
+ await skillsRuntime.reload();
37
+ const skillsPrompt = skillsRuntime.buildPromptForInput(userInput);
33
38
  const historyMessages = previousMessages.filter((message) => message.role !== "system");
34
39
  const messages = [
35
40
  {
36
41
  role: "system",
37
- content: buildSystemPrompt(Array.from(tools.values())),
42
+ content: buildSystemPrompt(Array.from(tools.values()), skillsPrompt),
38
43
  },
39
44
  ...historyMessages,
40
45
  {
package/dist/cli/main.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "dotenv/config";
3
2
  import { select, cancel, isCancel } from "@clack/prompts";
4
3
  import { initGlobalProxy } from "../lib/initGlobalProxy.js";
5
4
  import { parseCliArgs } from "./parse-args.js";
@@ -27,20 +27,5 @@ async function loadSettingsFile() {
27
27
  }
28
28
  }
29
29
  export async function loadAppConfig() {
30
- const fileConfig = await loadSettingsFile();
31
- return {
32
- ...fileConfig,
33
- MODEL_API_KEY: process.env.MODEL_API_KEY ?? fileConfig.MODEL_API_KEY,
34
- MODEL_BASE_URL: process.env.MODEL_BASE_URL ?? fileConfig.MODEL_BASE_URL,
35
- MODEL_NAME: process.env.MODEL_NAME ?? fileConfig.MODEL_NAME,
36
- OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? fileConfig.OPENAI_API_KEY,
37
- OPENAI_BASE_URL: process.env.OPENAI_BASE_URL ?? fileConfig.OPENAI_BASE_URL,
38
- OPENAI_MODEL: process.env.OPENAI_MODEL ?? fileConfig.OPENAI_MODEL,
39
- BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY ?? fileConfig.BRAVE_SEARCH_API_KEY,
40
- BRAVE_API_KEY: process.env.BRAVE_API_KEY ?? fileConfig.BRAVE_API_KEY,
41
- HTTP_PROXY: process.env.HTTP_PROXY ?? fileConfig.HTTP_PROXY,
42
- HTTPS_PROXY: process.env.HTTPS_PROXY ?? fileConfig.HTTPS_PROXY,
43
- http_proxy: process.env.http_proxy ?? fileConfig.http_proxy,
44
- https_proxy: process.env.https_proxy ?? fileConfig.https_proxy,
45
- };
30
+ return await loadSettingsFile();
46
31
  }
@@ -1,20 +1,9 @@
1
1
  import OpenAI from "openai";
2
2
  export function createOpenAICompatibleClient(options = {}) {
3
3
  const config = options.config ?? {};
4
- const apiKey = config.MODEL_API_KEY ??
5
- config.OPENAI_API_KEY ??
6
- process.env.MODEL_API_KEY ??
7
- process.env.OPENAI_API_KEY;
8
- const baseURL = config.MODEL_BASE_URL ??
9
- config.OPENAI_BASE_URL ??
10
- process.env.MODEL_BASE_URL ??
11
- process.env.OPENAI_BASE_URL;
12
- const model = options.model ??
13
- config.MODEL_NAME ??
14
- config.OPENAI_MODEL ??
15
- process.env.MODEL_NAME ??
16
- process.env.OPENAI_MODEL ??
17
- "gpt-4o-mini";
4
+ const apiKey = config.MODEL_API_KEY;
5
+ const baseURL = config.MODEL_BASE_URL;
6
+ const model = config.MODEL_NAME ?? "gpt-4o-mini";
18
7
  if (!apiKey) {
19
8
  throw new Error("Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables or ~/.my-agent/settings.json.");
20
9
  }
@@ -3,6 +3,11 @@ export function resolveWorkspaceRoot(workspaceRoot) {
3
3
  return path.resolve(workspaceRoot ?? process.cwd());
4
4
  }
5
5
  export function resolveSafePath(workspaceRoot, targetPath) {
6
+ // If the target is an absolute path, allow reading it directly (anywhere on disk)
7
+ if (path.isAbsolute(targetPath)) {
8
+ return path.resolve(targetPath);
9
+ }
10
+ // Relative paths are resolved against the workspace root
6
11
  const root = path.resolve(workspaceRoot);
7
12
  const fullPath = path.resolve(root, targetPath);
8
13
  const relative = path.relative(root, fullPath);
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  const DEFAULT_DENY_PATTERNS = [
2
3
  /\brm\s+-rf\s+\//i,
3
4
  /\bsudo\b/i,
@@ -22,6 +23,14 @@ const DEFAULT_ALLOW_PREFIXES = [
22
23
  "npm",
23
24
  "node",
24
25
  "git",
26
+ "python3",
27
+ "python",
28
+ "pip3",
29
+ "pip",
30
+ "pdftotext",
31
+ "qpdf",
32
+ "pandoc",
33
+ "which",
25
34
  ];
26
35
  export function validateShellCommand(command) {
27
36
  const trimmed = command.trim();
@@ -34,7 +43,11 @@ export function validateShellCommand(command) {
34
43
  }
35
44
  }
36
45
  const firstToken = trimmed.split(/\s+/)[0];
37
- if (!DEFAULT_ALLOW_PREFIXES.includes(firstToken)) {
46
+ // Also allow absolute paths ending with known commands (e.g. /usr/bin/python3)
47
+ const commandName = firstToken.includes(path.sep)
48
+ ? path.basename(firstToken)
49
+ : firstToken;
50
+ if (!DEFAULT_ALLOW_PREFIXES.includes(commandName)) {
38
51
  throw new Error(`Shell command not allowed by policy. Command prefix: ${firstToken}`);
39
52
  }
40
53
  }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureSkillsDir } from "./paths.js";
4
+ export async function discoverSkillDirs() {
5
+ const skillsDir = await ensureSkillsDir();
6
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
7
+ const result = [];
8
+ for (const entry of entries) {
9
+ if (!entry.isDirectory())
10
+ continue;
11
+ const dir = path.join(skillsDir, entry.name);
12
+ const skillFile = path.join(dir, "SKILL.md");
13
+ try {
14
+ const stat = await fs.stat(skillFile);
15
+ if (stat.isFile()) {
16
+ result.push({ dir, skillFile });
17
+ }
18
+ }
19
+ catch {
20
+ // ignore dirs without SKILL.md
21
+ }
22
+ }
23
+ return result;
24
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types.js";
2
+ export * from "./paths.js";
3
+ export * from "./parse.js";
4
+ export * from "./discover.js";
5
+ export * from "./registry.js";
6
+ export * from "./match.js";
7
+ export * from "./prompt.js";
@@ -0,0 +1,59 @@
1
+ function tokenize(input) {
2
+ return input
3
+ .toLowerCase()
4
+ .split(/[\s,,。.!?!?::;;"'`“”‘’()(){}\[\]<>\/\\|+-]+/g)
5
+ .map((x) => x.trim())
6
+ .filter(Boolean);
7
+ }
8
+ function includesAny(text, keywords) {
9
+ const lower = text.toLowerCase();
10
+ return keywords.some((k) => lower.includes(k.toLowerCase()));
11
+ }
12
+ export function matchSkills(input, skills) {
13
+ const q = input.trim().toLowerCase();
14
+ if (!q)
15
+ return { matched: [] };
16
+ const tokens = tokenize(q);
17
+ const scored = [];
18
+ for (const skill of skills) {
19
+ let score = 0;
20
+ const name = skill.name.toLowerCase();
21
+ const desc = skill.description.toLowerCase();
22
+ const tags = skill.tags.map((t) => t.toLowerCase());
23
+ if (q.includes(name))
24
+ score += 10;
25
+ if (tokens.includes(name))
26
+ score += 8;
27
+ for (const tag of tags) {
28
+ if (q.includes(tag))
29
+ score += 6;
30
+ if (tokens.includes(tag))
31
+ score += 4;
32
+ }
33
+ const descKeywords = desc
34
+ .split(/[\s,,。.!?!?::;;"'`“”‘’()(){}\[\]<>\/\\|+-]+/g)
35
+ .map((x) => x.trim())
36
+ .filter((x) => x.length >= 3);
37
+ let descHitCount = 0;
38
+ for (const token of tokens) {
39
+ if (descKeywords.includes(token)) {
40
+ descHitCount += 1;
41
+ }
42
+ }
43
+ score += Math.min(descHitCount, 3);
44
+ // 特判:pdf skill
45
+ if (name === "pdf") {
46
+ if (includesAny(q, [".pdf", "pdf", "ocr", "表单", "合并pdf", "拆分pdf", "提取pdf"])) {
47
+ score += 8;
48
+ }
49
+ }
50
+ if (score > 0) {
51
+ scored.push({ skill, score });
52
+ }
53
+ }
54
+ scored.sort((a, b) => b.score - a.score);
55
+ return {
56
+ matched: scored.slice(0, 3).map((x) => x.skill),
57
+ reason: scored.length ? `matched ${scored.length} skill(s)` : undefined,
58
+ };
59
+ }
@@ -0,0 +1,8 @@
1
+ import matter from "gray-matter";
2
+ export function parseSkillFile(content) {
3
+ const parsed = matter(content);
4
+ return {
5
+ meta: (parsed.data ?? {}),
6
+ body: parsed.content.trim(),
7
+ };
8
+ }
@@ -0,0 +1,14 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ export function getYuanClawHomeDir() {
5
+ return path.join(os.homedir(), ".yuan-claw");
6
+ }
7
+ export function getSkillsDir() {
8
+ return path.join(getYuanClawHomeDir(), "skills");
9
+ }
10
+ export async function ensureSkillsDir() {
11
+ const dir = getSkillsDir();
12
+ await fs.mkdir(dir, { recursive: true });
13
+ return dir;
14
+ }
@@ -0,0 +1,26 @@
1
+ export function buildSkillsPrompt(skills) {
2
+ if (!skills.length)
3
+ return "";
4
+ const parts = skills.map((skill) => {
5
+ const lines = [];
6
+ lines.push(`Skill Name: ${skill.name}`);
7
+ if (skill.description)
8
+ lines.push(`Description: ${skill.description}`);
9
+ if (skill.tags.length)
10
+ lines.push(`Tags: ${skill.tags.join(", ")}`);
11
+ if (skill.license)
12
+ lines.push(`License: ${skill.license}`);
13
+ lines.push("");
14
+ lines.push("[SKILL CONTENT BEGIN]");
15
+ lines.push(skill.body);
16
+ lines.push("[SKILL CONTENT END]");
17
+ return lines.join("\n");
18
+ });
19
+ return [
20
+ "You have access to the following local skills.",
21
+ "Use them when they are relevant to the user's request.",
22
+ "If a skill contains procedures, best practices, or tool suggestions, follow them.",
23
+ "",
24
+ ...parts,
25
+ ].join("\n");
26
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { discoverSkillDirs } from "./discover.js";
4
+ import { parseSkillFile } from "./parse.js";
5
+ function normalizeTags(input) {
6
+ if (!Array.isArray(input))
7
+ return [];
8
+ return input
9
+ .filter((x) => typeof x === "string")
10
+ .map((x) => x.trim())
11
+ .filter(Boolean);
12
+ }
13
+ function fallbackNameFromDir(dir) {
14
+ return path.basename(dir);
15
+ }
16
+ export async function loadSkills() {
17
+ const discovered = await discoverSkillDirs();
18
+ const skills = [];
19
+ for (const item of discovered) {
20
+ try {
21
+ const raw = await fs.readFile(item.skillFile, "utf8");
22
+ const { meta, body } = parseSkillFile(raw);
23
+ const name = (meta.name?.trim() || fallbackNameFromDir(item.dir)).trim();
24
+ const description = (meta.description?.trim() || "").trim();
25
+ skills.push({
26
+ name,
27
+ description,
28
+ license: meta.license?.trim(),
29
+ version: meta.version?.trim(),
30
+ tags: normalizeTags(meta.tags),
31
+ dir: item.dir,
32
+ skillFile: item.skillFile,
33
+ body,
34
+ raw,
35
+ });
36
+ }
37
+ catch {
38
+ // ignore invalid skill files
39
+ }
40
+ }
41
+ skills.sort((a, b) => a.name.localeCompare(b.name));
42
+ return skills;
43
+ }
@@ -0,0 +1,19 @@
1
+ import { loadSkills } from "./registry.js";
2
+ import { matchSkills } from "./match.js";
3
+ import { buildSkillsPrompt } from "./prompt.js";
4
+ export class SkillsRuntime {
5
+ skills = [];
6
+ async reload() {
7
+ this.skills = await loadSkills();
8
+ }
9
+ list() {
10
+ return this.skills;
11
+ }
12
+ match(input) {
13
+ return matchSkills(input, this.skills).matched;
14
+ }
15
+ buildPromptForInput(input) {
16
+ const matched = this.match(input);
17
+ return buildSkillsPrompt(matched);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiangyuan1209/yuan-claw",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A local CLI agent with tools, search, and interactive approval.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "@clack/prompts": "^1.3.0",
28
28
  "@mozilla/readability": "^0.6.0",
29
29
  "dotenv": "^17.4.2",
30
+ "gray-matter": "^4.0.3",
30
31
  "jsdom": "^29.0.2",
31
32
  "openai": "^4.56.0",
32
33
  "zod": "^3.23.8"
@@ -37,4 +38,4 @@
37
38
  "tsx": "^4.19.1",
38
39
  "typescript": "^5.6.2"
39
40
  }
40
- }
41
+ }