@jiangyuan1209/yuan-claw 0.1.2 → 0.1.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.
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,82 +31,221 @@
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
55
103
  ```
56
104
 
57
- 最小可用配置通常只需要:
105
+ 在 macOS / Linux 上通常类似:
58
106
 
59
- ```env
60
- MODEL_API_KEY=your_api_key
61
- MODEL_BASE_URL=https://api.openai.com/v1
62
- MODEL_NAME=gpt-4o-mini
107
+ ```bash
108
+ /Users/你的用户名/.yuan-claw/settings.json
63
109
  ```
64
110
 
111
+ 如果你使用的是 Windows,则通常位于用户目录下对应的 `.yuan-claw` 文件夹中。
112
+
113
+ > 注意:第一次运行通常只是创建配置文件。
114
+ > 你需要手动填写配置后,再次执行 `yuan-claw` 才会真正生效。
115
+
65
116
  ---
66
117
 
67
- ### 2. 开发模式运行
118
+ ### 2. 编辑 `~/.yuan-claw/settings.json`
68
119
 
69
- #### 单次命令
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
+ "BAIDU_API_KEY": "your_baidu_search_api_key",
128
+ "HTTP_PROXY": "http://127.0.0.1:33210",
129
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
130
+ }
131
+ ```
132
+
133
+ 最小可用配置通常只需要:
134
+
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
+ "BAIDU_API_KEY": "your_baidu_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
+ 保存后,重新执行:
70
161
 
71
162
  ```bash
72
- npm run dev -- "帮我搜索 OpenAI 最新消息"
163
+ yuan-claw
73
164
  ```
74
165
 
166
+ ---
167
+
168
+ ### 3. 开始使用
169
+
75
170
  #### 交互式 REPL
76
171
 
77
172
  ```bash
78
- npm run dev
173
+ yuan-claw
79
174
  ```
80
175
 
81
- 启动后你会看到:
82
-
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].
176
+ #### 单次命令
87
177
 
88
- yuan-claw[ask]>
178
+ ```bash
179
+ yuan-claw "帮我搜索 OpenAI 最新消息"
89
180
  ```
90
181
 
91
182
  ---
92
183
 
93
- ### 3. 编译后运行
184
+ ## Configuration
185
+
186
+ 项目优先从用户目录中的配置文件读取配置:
94
187
 
95
188
  ```bash
96
- npm run build
97
- npm run start
189
+ ~/.yuan-claw/settings.json
98
190
  ```
99
191
 
100
- 单次命令模式:
192
+ ### 支持的配置项
101
193
 
102
- ```bash
103
- npm run start -- "帮我总结这个项目的功能"
194
+ - `MODEL_API_KEY`
195
+ - `MODEL_BASE_URL`
196
+ - `MODEL_NAME`
197
+ - `OPENAI_API_KEY`
198
+ - `OPENAI_BASE_URL`
199
+ - `OPENAI_MODEL`
200
+ - `BAIDU_API_KEY`
201
+ - `HTTP_PROXY`
202
+ - `HTTPS_PROXY`
203
+ - `http_proxy`
204
+ - `https_proxy`
205
+
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
+ "BAIDU_API_KEY": "your_baidu_search_api_key",
226
+ "HTTP_PROXY": "http://127.0.0.1:33210",
227
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
228
+ }
104
229
  ```
105
230
 
106
231
  ---
107
232
 
233
+ ## Environment Variables(已废弃)
234
+
235
+ 项目已不再通过 `.env` 加载配置,所有配置均通过 `~/.yuan-claw/settings.json` 管理。
236
+
237
+ 如果你仍习惯使用 `.env`,可以自行在项目中通过 `dotenv` 加载,但不再推荐。
238
+
239
+ > 普通 CLI 用户更推荐使用:
240
+ >
241
+ > ```bash
242
+ > ~/.yuan-claw/settings.json
243
+ > ```
244
+ >
245
+ > `.env` 更适合源码开发或本地调试。
246
+
247
+ ---
248
+
108
249
  ## REPL Usage
109
250
 
110
251
  不传入 prompt 时,程序会进入 REPL 模式。你可以连续输入多轮指令,例如:
@@ -163,86 +304,37 @@ yuan-claw[always]>
163
304
 
164
305
  ---
165
306
 
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
307
  ## CLI Usage
195
308
 
196
- 项目在 `package.json` 中定义了可执行命令:
197
-
198
- ```json
199
- "bin": {
200
- "yuan-claw": "./dist/cli/main.js"
201
- }
202
- ```
203
-
204
- 构建并安装后,可以直接使用:
309
+ 全局安装后可直接运行:
205
310
 
206
311
  ```bash
207
312
  yuan-claw
208
313
  ```
209
314
 
210
- 或:
315
+ 单次命令模式:
211
316
 
212
317
  ```bash
213
318
  yuan-claw "帮我搜索 AI 新闻"
214
319
  ```
215
320
 
216
- ---
217
-
218
- ## Environment Variables
219
-
220
- 项目使用 **OpenAI-compatible API**,按以下优先级读取配置:
321
+ 如果你是源码模式开发,则可以使用:
221
322
 
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
323
+ ```bash
324
+ npm run dev
325
+ ```
233
326
 
234
- 1. `MODEL_NAME`
235
- 2. `OPENAI_MODEL`
236
- 3. 默认值:`gpt-4o-mini`
327
+ 或:
237
328
 
238
- ### OpenAI 风格兼容变量
329
+ ```bash
330
+ npm run dev -- "帮我总结这个项目的功能"
331
+ ```
239
332
 
240
- 如果你更习惯 OpenAI 风格变量名,也可以使用:
333
+ 编译后运行:
241
334
 
242
- ```env
243
- OPENAI_API_KEY=
244
- OPENAI_BASE_URL=
245
- OPENAI_MODEL=
335
+ ```bash
336
+ npm run build
337
+ npm run start
246
338
  ```
247
339
 
248
340
  ---
@@ -251,8 +343,10 @@ OPENAI_MODEL=
251
343
 
252
344
  如果你希望启用网页搜索工具,请配置:
253
345
 
254
- ```env
255
- BRAVE_SEARCH_API_KEY=your_brave_search_api_key
346
+ ```json
347
+ {
348
+ "BAIDU_API_KEY": "your_baidu_search_api_key"
349
+ }
256
350
  ```
257
351
 
258
352
  未配置时,`web_search` 工具会被禁用。
@@ -263,12 +357,14 @@ BRAVE_SEARCH_API_KEY=your_brave_search_api_key
263
357
 
264
358
  如需通过代理访问模型服务或外部网站,可以配置:
265
359
 
266
- ```env
267
- HTTP_PROXY=http://127.0.0.1:33210
268
- HTTPS_PROXY=http://127.0.0.1:33210
360
+ ```json
361
+ {
362
+ "HTTP_PROXY": "http://127.0.0.1:33210",
363
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
364
+ }
269
365
  ```
270
366
 
271
- 程序会自动读取以下变量:
367
+ 程序会自动读取以下配置项:
272
368
 
273
369
  - `HTTP_PROXY`
274
370
  - `HTTPS_PROXY`
@@ -277,41 +373,124 @@ HTTPS_PROXY=http://127.0.0.1:33210
277
373
 
278
374
  ---
279
375
 
280
- ## Recommended `.env.example`
376
+ ## Skills / 本地插件扩展
377
+
378
+ yuan-claw 支持通过本地 Skill(技能)文件扩展 Agent 的能力。Skill 是存放在固定目录下的 Markdown 文件,Agent 会在运行时根据用户输入自动匹配并加载相关 Skill 的提示词,从而获得领域知识或操作指引。
379
+
380
+ ### Skill 目录结构
381
+
382
+ 所有 Skill 文件存放在:
383
+
384
+ ```bash
385
+ ~/.yuan-claw/skills/
386
+ ```
387
+
388
+ 每个 Skill 是一个子目录,其中必须包含一个 `SKILL.md` 文件:
389
+
390
+ ```
391
+ ~/.yuan-claw/skills/
392
+ ├── pdf/
393
+ │ └── SKILL.md
394
+ ├── frontend-design/
395
+ │ └── SKILL.md
396
+ └── my-skill/
397
+ └── SKILL.md
398
+ ```
399
+
400
+ ### SKILL.md 文件格式
401
+
402
+ `SKILL.md` 使用 YAML frontmatter + Markdown body 的格式:
403
+
404
+ ```markdown
405
+ ---
406
+ name: pdf
407
+ description: PDF 文件处理技能,支持提取文本、表格、OCR 等
408
+ tags: [pdf, document, ocr]
409
+ license: MIT
410
+ version: 1.0.0
411
+ ---
412
+
413
+ ## 使用指南
414
+
415
+ 当用户需要处理 PDF 文件时:
416
+ 1. 使用 pdfplumber 提取文本...
417
+ 2. 对于扫描件,使用 OCR...
418
+ ```
419
+
420
+ Frontmatter 字段说明:
421
+
422
+ - `name`(可选):Skill 名称,用于匹配和展示。未填写时使用目录名
423
+ - `description`(可选):简短描述,用于匹配和展示
424
+ - `tags`(可选):标签数组,用于关键词匹配
425
+ - `license`(可选):许可证
426
+ - `version`(可选):版本号
427
+
428
+ Markdown body 是 Skill 的实际提示词内容,会被注入到 system prompt 中指导 Agent 行为。
429
+
430
+ ### 匹配机制
281
431
 
282
- 建议在仓库中提供如下 `.env.example`:
432
+ Agent 会根据用户输入自动匹配最相关的 Skill(最多匹配 3 个),匹配依据包括:
283
433
 
284
- ```env
285
- # =========================
286
- # 模型服务配置(推荐)
287
- # =========================
434
+ - 用户输入中是否包含 Skill 名称
435
+ - 用户输入中是否包含 Skill 的标签
436
+ - 用户输入分词后与 Skill 描述的关键词重合度
288
437
 
289
- MODEL_API_KEY=
290
- MODEL_BASE_URL=
291
- MODEL_NAME=
438
+ 匹配到的 Skill 内容会被组装到 system prompt 中,指导 Agent 使用相应的知识和流程。
292
439
 
293
- # =========================
294
- # OpenAI 风格兼容变量(可选)
295
- # =========================
440
+ ### 添加自定义 Skill
296
441
 
297
- OPENAI_API_KEY=
298
- OPENAI_BASE_URL=
299
- OPENAI_MODEL=
442
+ 1. 在 `~/.yuan-claw/skills/` 下创建新目录:
300
443
 
301
- # =========================
302
- # 网页搜索配置
303
- # =========================
444
+ ```bash
445
+ mkdir -p ~/.yuan-claw/skills/my-skill
446
+ ```
304
447
 
305
- BRAVE_SEARCH_API_KEY=
448
+ 2. 创建 `SKILL.md` 文件:
306
449
 
307
- # =========================
308
- # 代理配置(可选)
309
- # =========================
450
+ ```bash
451
+ cat > ~/.yuan-claw/skills/my-skill/SKILL.md << 'EOF'
452
+ ---
453
+ name: my-skill
454
+ description: 我的自定义技能
455
+ tags: [custom]
456
+ ---
310
457
 
311
- HTTP_PROXY=
312
- HTTPS_PROXY=
458
+ 当用户问到 XXX 时,请按以下步骤操作:
459
+ 1. ...
460
+ 2. ...
461
+ EOF
462
+ ```
463
+
464
+ 3. 下次运行 `yuan-claw` 时,该 Skill 会被自动发现和加载。
465
+
466
+ ---
467
+
468
+ ## Scripts
469
+
470
+ ```json
471
+ {
472
+ "scripts": {
473
+ "dev": "tsx src/cli/main.ts",
474
+ "build": "tsc -p tsconfig.json",
475
+ "start": "node dist/cli/main.js",
476
+ "check": "tsc --noEmit"
477
+ }
478
+ }
313
479
  ```
314
480
 
481
+ ### 脚本说明
482
+
483
+ - `npm run dev`:开发模式运行源码
484
+ - `npm run build`:编译到 `dist/`
485
+ - `npm run start`:运行编译后的 CLI
486
+ - `npm run check`:执行 TypeScript 类型检查
487
+
488
+ 说明:
489
+
490
+ - `npm run dev` / `npm run start`
491
+ - 不传参数:进入 REPL
492
+ - 传入参数:执行单次命令
493
+
315
494
  ---
316
495
 
317
496
  ## Examples
@@ -319,17 +498,23 @@ HTTPS_PROXY=
319
498
  ### 普通问答
320
499
 
321
500
  ```bash
322
- npm run dev -- "帮我总结这个项目的功能"
501
+ yuan-claw "帮我总结这个项目的功能"
323
502
  ```
324
503
 
325
504
  ### 搜索最新消息
326
505
 
327
506
  ```bash
328
- npm run dev -- "帮我搜索 OpenAI 最新消息"
507
+ yuan-claw "帮我搜索 OpenAI 最新消息"
329
508
  ```
330
509
 
331
510
  ### 进入 REPL
332
511
 
512
+ ```bash
513
+ yuan-claw
514
+ ```
515
+
516
+ ### 源码开发模式
517
+
333
518
  ```bash
334
519
  npm run dev
335
520
  ```
@@ -345,39 +530,67 @@ npm run start -- "帮我搜索 AI 新闻"
345
530
 
346
531
  ## Troubleshooting
347
532
 
348
- ### `Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables.`
533
+ ### 第一次运行后为什么没有立即生效?
349
534
 
350
- 说明模型 API Key 未配置。请至少设置其一:
535
+ 因为第一次执行:
351
536
 
352
- ```env
353
- MODEL_API_KEY=your_api_key
537
+ ```bash
538
+ yuan-claw
354
539
  ```
355
540
 
356
- 或:
541
+ 通常只是为了初始化配置文件:
542
+
543
+ ```bash
544
+ ~/.yuan-claw/settings.json
545
+ ```
546
+
547
+ 你需要手动填写配置项并保存,然后再次执行:
548
+
549
+ ```bash
550
+ yuan-claw
551
+ ```
552
+
553
+ ---
554
+
555
+ ### `Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables.`
357
556
 
358
- ```env
359
- OPENAI_API_KEY=your_api_key
557
+ 说明模型 API Key 尚未正确配置。请至少在以下任一位置填写:
558
+
559
+ - `~/.yuan-claw/settings.json`
560
+ - 系统环境变量
561
+ - 项目根目录 `.env`
562
+
563
+ 例如:
564
+
565
+ ```json
566
+ {
567
+ "MODEL_API_KEY": "your_api_key"
568
+ }
360
569
  ```
361
570
 
362
571
  ---
363
572
 
364
- ### `web_search disabled: set BRAVE_SEARCH_API_KEY`
573
+ ### `web_search disabled: set BAIDU_API_KEY`
365
574
 
366
- 说明未配置 Brave Search API Key。请添加:
575
+ 说明未配置百度搜索 API Key。请添加:
367
576
 
368
- ```env
369
- BRAVE_SEARCH_API_KEY=your_key
577
+ ```json
578
+ {
579
+ "BAIDU_API_KEY": "your_baidu_search_api_key"
580
+ }
370
581
  ```
371
582
 
372
583
  ---
373
584
 
374
585
  ### 无法访问外部服务 / 请求超时
375
586
 
376
- 请检查是否需要代理:
587
+ 请检查是否需要代理,例如:
377
588
 
378
- ```env
379
- HTTP_PROXY=http://127.0.0.1:33210
380
- HTTPS_PROXY=http://127.0.0.1:33210
589
+ ```json
590
+ {
591
+ "HTTP_PROXY": "http://127.0.0.1:33210",
592
+ "HTTPS_PROXY": "http://127.0.0.1:33210"
593
+ }
381
594
  ```
382
595
 
383
596
  ---
@@ -405,13 +618,33 @@ dist/cli/main.js
405
618
 
406
619
  ---
407
620
 
621
+ ### 全局命令 `yuan-claw` 不存在
622
+
623
+ 如果你是通过 npm 全局安装,请确认已成功安装:
624
+
625
+ ```bash
626
+ npm install -g @jiangyuan1209/yuan-claw
627
+ ```
628
+
629
+ 如仍有问题,可尝试重新安装:
630
+
631
+ ```bash
632
+ npm uninstall -g @jiangyuan1209/yuan-claw
633
+ npm install -g @jiangyuan1209/yuan-claw@latest
634
+ ```
635
+
636
+ ---
637
+
408
638
  ## Development
409
639
 
410
640
  ```bash
641
+ npm install
411
642
  npm run check
412
643
  npm run dev
413
644
  ```
414
645
 
646
+ 如果你使用源码开发方式,也可以在项目根目录创建 `.env` 文件辅助调试。
647
+
415
648
  ---
416
649
 
417
650
  ## Roadmap
@@ -428,14 +661,4 @@ npm run dev
428
661
 
429
662
  ## License
430
663
 
431
- MIT
432
- ```
433
-
434
-
435
- ## `.gitignore`
436
-
437
- ```gitignore
438
- node_modules
439
- dist
440
- .env
441
- ```
664
+ 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";
@@ -7,8 +7,7 @@ const DEFAULT_SETTINGS = {
7
7
  OPENAI_API_KEY: "",
8
8
  OPENAI_BASE_URL: "",
9
9
  OPENAI_MODEL: "",
10
- BRAVE_SEARCH_API_KEY: "",
11
- BRAVE_API_KEY: "",
10
+ BAIDU_API_KEY: "",
12
11
  HTTP_PROXY: "",
13
12
  HTTPS_PROXY: "",
14
13
  };
@@ -8,8 +8,7 @@ const userSettingsSchema = z.object({
8
8
  OPENAI_API_KEY: z.string().optional(),
9
9
  OPENAI_BASE_URL: z.string().optional(),
10
10
  OPENAI_MODEL: z.string().optional(),
11
- BRAVE_SEARCH_API_KEY: z.string().optional(),
12
- BRAVE_API_KEY: z.string().optional(),
11
+ BAIDU_API_KEY: z.string().optional(),
13
12
  HTTP_PROXY: z.string().optional(),
14
13
  HTTPS_PROXY: z.string().optional(),
15
14
  http_proxy: z.string().optional(),
@@ -27,20 +26,5 @@ async function loadSettingsFile() {
27
26
  }
28
27
  }
29
28
  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
- };
29
+ return await loadSettingsFile();
46
30
  }
@@ -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 {};
@@ -5,7 +5,7 @@ import { createGrepTextTool } from "./file/grep-text.js";
5
5
  import { createShellExecTool } from "./shell/shell-exec.js";
6
6
  import { createGitStatusTool } from "./git/git-status.js";
7
7
  import { createGitDiffTool } from "./git/git-diff.js";
8
- import { createHttpFetchTool, createWebSearchTool, createExtractReadableTextTool, createBraveSearchProvider, } from "./web/index.js";
8
+ import { createHttpFetchTool, createWebSearchTool, createExtractReadableTextTool, createBaiduSearchProvider, } from "./web/index.js";
9
9
  export function createToolRegistry(options) {
10
10
  const tools = [
11
11
  createReadFileTool({ workspaceRoot: options.workspaceRoot }),
@@ -19,20 +19,17 @@ export function createToolRegistry(options) {
19
19
  createExtractReadableTextTool(),
20
20
  ];
21
21
  const config = options.config ?? {};
22
- const braveApiKey = config.BRAVE_SEARCH_API_KEY ??
23
- config.BRAVE_API_KEY ??
24
- process.env.BRAVE_SEARCH_API_KEY ??
25
- process.env.BRAVE_API_KEY;
26
- if (braveApiKey) {
22
+ const baiduApiKey = config.BAIDU_API_KEY;
23
+ if (baiduApiKey) {
27
24
  tools.push(createWebSearchTool({
28
- provider: createBraveSearchProvider({
29
- apiKey: braveApiKey,
25
+ provider: createBaiduSearchProvider({
26
+ apiKey: baiduApiKey,
30
27
  }),
31
28
  }));
32
- console.warn("[tools] web_search enabled via Brave Search API");
29
+ console.warn("[tools] web_search enabled via Baidu Search API");
33
30
  }
34
31
  else {
35
- console.warn("[tools] web_search disabled: set BRAVE_SEARCH_API_KEY or BRAVE_API_KEY");
32
+ console.warn("[tools] web_search disabled: set BAIDU_API_KEY in settings.json");
36
33
  }
37
34
  return new Map(tools.map((tool) => [tool.name, tool]));
38
35
  }
@@ -1,4 +1,4 @@
1
1
  export { createHttpFetchTool } from "./http-fetch.js";
2
2
  export { createWebSearchTool } from "./web-search.js";
3
3
  export { createExtractReadableTextTool } from "./extract-readable-text.js";
4
- export { createBraveSearchProvider } from "./search-providers/brave.js";
4
+ export { createBaiduSearchProvider } from "./search-providers/baidu.js";
@@ -0,0 +1,36 @@
1
+ export function createBaiduSearchProvider(options) {
2
+ const baseUrl = options.baseUrl ?? "https://qianfan.baidubce.com/v2/ai_search/web_search";
3
+ return async function baiduSearch(query, count) {
4
+ const response = await fetch(baseUrl, {
5
+ method: "POST",
6
+ headers: {
7
+ "Content-Type": "application/json",
8
+ Authorization: `Bearer ${options.apiKey}`,
9
+ "X-Appbuilder-From": "yuan-claw",
10
+ "User-Agent": "Mozilla/5.0 (compatible; XSimpleWebSearch/1.0)",
11
+ },
12
+ body: JSON.stringify({
13
+ messages: [{ content: query, role: "user" }],
14
+ search_source: "baidu_search_v2",
15
+ resource_type_filter: [{ type: "web", top_k: count }],
16
+ }),
17
+ });
18
+ if (!response.ok) {
19
+ const errorText = await response.text().catch(() => "");
20
+ throw new Error(`Baidu search failed: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
21
+ }
22
+ const data = (await response.json());
23
+ if (data.code) {
24
+ throw new Error(`Baidu search error: ${data.message ?? data.code}`);
25
+ }
26
+ return (data.references ?? [])
27
+ .slice(0, count)
28
+ .map((item) => ({
29
+ title: item.title ?? "",
30
+ url: item.url ?? "",
31
+ snippet: item.content ?? "",
32
+ source: "baidu",
33
+ }))
34
+ .filter((item) => item.url.trim().length > 0);
35
+ };
36
+ }
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.4",
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
+ }
package/.env.example DELETED
@@ -1,28 +0,0 @@
1
- # =========================
2
- # 模型服务配置(推荐)
3
- # =========================
4
-
5
- MODEL_API_KEY=
6
- MODEL_BASE_URL=
7
- MODEL_NAME=
8
-
9
- # =========================
10
- # OpenAI 风格兼容变量(可选)
11
- # =========================
12
-
13
- OPENAI_API_KEY=
14
- OPENAI_BASE_URL=
15
- OPENAI_MODEL=
16
-
17
- # =========================
18
- # 网页搜索配置
19
- # =========================
20
-
21
- BRAVE_SEARCH_API_KEY=
22
-
23
- # =========================
24
- # 代理配置(可选)
25
- # =========================
26
-
27
- HTTP_PROXY=
28
- HTTPS_PROXY=