@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 +376 -153
- package/dist/agent/build-system-prompt.js +5 -1
- package/dist/agent/run-local-agent-loop.js +6 -1
- package/dist/cli/main.js +0 -1
- package/dist/config/init-user-config.js +1 -2
- package/dist/config/load-config.js +2 -18
- package/dist/model/providers/openai-compatible.js +3 -14
- package/dist/security/path-guards.js +5 -0
- package/dist/security/shell-policy.js +14 -1
- package/dist/skills/discover.js +24 -0
- package/dist/skills/index.js +7 -0
- package/dist/skills/match.js +59 -0
- package/dist/skills/parse.js +8 -0
- package/dist/skills/paths.js +14 -0
- package/dist/skills/prompt.js +26 -0
- package/dist/skills/registry.js +43 -0
- package/dist/skills/runtime.js +19 -0
- package/dist/skills/types.js +1 -0
- package/dist/tools/registry.js +7 -10
- package/dist/tools/web/index.js +1 -1
- package/dist/tools/web/search-providers/baidu.js +36 -0
- package/package.json +3 -2
- package/.env.example +0 -28
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
|
-
|
|
93
|
+
安装完成后,先执行一次:
|
|
45
94
|
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
MODEL_NAME=
|
|
95
|
+
```bash
|
|
96
|
+
yuan-claw
|
|
97
|
+
```
|
|
50
98
|
|
|
51
|
-
|
|
99
|
+
程序会在你的用户目录下自动初始化配置文件:
|
|
52
100
|
|
|
53
|
-
|
|
54
|
-
|
|
101
|
+
```bash
|
|
102
|
+
~/.yuan-claw/settings.json
|
|
55
103
|
```
|
|
56
104
|
|
|
57
|
-
|
|
105
|
+
在 macOS / Linux 上通常类似:
|
|
58
106
|
|
|
59
|
-
```
|
|
60
|
-
|
|
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
|
-
|
|
163
|
+
yuan-claw
|
|
73
164
|
```
|
|
74
165
|
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### 3. 开始使用
|
|
169
|
+
|
|
75
170
|
#### 交互式 REPL
|
|
76
171
|
|
|
77
172
|
```bash
|
|
78
|
-
|
|
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
|
-
|
|
178
|
+
```bash
|
|
179
|
+
yuan-claw "帮我搜索 OpenAI 最新消息"
|
|
89
180
|
```
|
|
90
181
|
|
|
91
182
|
---
|
|
92
183
|
|
|
93
|
-
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
项目优先从用户目录中的配置文件读取配置:
|
|
94
187
|
|
|
95
188
|
```bash
|
|
96
|
-
|
|
97
|
-
npm run start
|
|
189
|
+
~/.yuan-claw/settings.json
|
|
98
190
|
```
|
|
99
191
|
|
|
100
|
-
|
|
192
|
+
### 支持的配置项
|
|
101
193
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
235
|
-
2. `OPENAI_MODEL`
|
|
236
|
-
3. 默认值:`gpt-4o-mini`
|
|
327
|
+
或:
|
|
237
328
|
|
|
238
|
-
|
|
329
|
+
```bash
|
|
330
|
+
npm run dev -- "帮我总结这个项目的功能"
|
|
331
|
+
```
|
|
239
332
|
|
|
240
|
-
|
|
333
|
+
编译后运行:
|
|
241
334
|
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
```
|
|
255
|
-
|
|
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
|
-
```
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
432
|
+
Agent 会根据用户输入自动匹配最相关的 Skill(最多匹配 3 个),匹配依据包括:
|
|
283
433
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
# =========================
|
|
434
|
+
- 用户输入中是否包含 Skill 名称
|
|
435
|
+
- 用户输入中是否包含 Skill 的标签
|
|
436
|
+
- 用户输入分词后与 Skill 描述的关键词重合度
|
|
288
437
|
|
|
289
|
-
|
|
290
|
-
MODEL_BASE_URL=
|
|
291
|
-
MODEL_NAME=
|
|
438
|
+
匹配到的 Skill 内容会被组装到 system prompt 中,指导 Agent 使用相应的知识和流程。
|
|
292
439
|
|
|
293
|
-
|
|
294
|
-
# OpenAI 风格兼容变量(可选)
|
|
295
|
-
# =========================
|
|
440
|
+
### 添加自定义 Skill
|
|
296
441
|
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
501
|
+
yuan-claw "帮我总结这个项目的功能"
|
|
323
502
|
```
|
|
324
503
|
|
|
325
504
|
### 搜索最新消息
|
|
326
505
|
|
|
327
506
|
```bash
|
|
328
|
-
|
|
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
|
-
###
|
|
533
|
+
### 第一次运行后为什么没有立即生效?
|
|
349
534
|
|
|
350
|
-
|
|
535
|
+
因为第一次执行:
|
|
351
536
|
|
|
352
|
-
```
|
|
353
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
|
573
|
+
### `web_search disabled: set BAIDU_API_KEY`
|
|
365
574
|
|
|
366
|
-
|
|
575
|
+
说明未配置百度搜索 API Key。请添加:
|
|
367
576
|
|
|
368
|
-
```
|
|
369
|
-
|
|
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
|
-
```
|
|
379
|
-
|
|
380
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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,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,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/dist/tools/registry.js
CHANGED
|
@@ -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,
|
|
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
|
|
23
|
-
|
|
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:
|
|
29
|
-
apiKey:
|
|
25
|
+
provider: createBaiduSearchProvider({
|
|
26
|
+
apiKey: baiduApiKey,
|
|
30
27
|
}),
|
|
31
28
|
}));
|
|
32
|
-
console.warn("[tools] web_search enabled via
|
|
29
|
+
console.warn("[tools] web_search enabled via Baidu Search API");
|
|
33
30
|
}
|
|
34
31
|
else {
|
|
35
|
-
console.warn("[tools] web_search disabled: set
|
|
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
|
}
|
package/dist/tools/web/index.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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=
|