flavor-code 1.2.4 → 1.2.5
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 +369 -367
- package/README.zh-CN.md +369 -0
- package/dist/{app-N3655H2E.js → app-3DXEGSS7.js} +10 -3
- package/dist/{chunk-MRISY6KK.js → chunk-3TYRDK34.js} +33 -28
- package/dist/{chunk-L5A2NDMT.js → chunk-Z3BW52MK.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/desktop/main.js +33 -28
- package/dist/sdk/index.js +2 -2
- package/package.json +2 -1
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
<p align="center"><a href="./README.md">English</a> | <b><a href="./README.zh-CN.md">简体中文</a></b></p>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="./assets/icon-transparent-512.png" alt="Flavor Code Logo" width="168" />
|
|
5
|
+
<h1>Flavor Code</h1>
|
|
6
|
+
<p><strong>本地优先、可审计、可恢复的 AI 编程助手</strong></p>
|
|
7
|
+
<p>在终端、Electron 桌面端和 VS Code 中读代码、改文件、运行命令并完成复杂任务。</p>
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://www.npmjs.com/package/flavor-code"><img alt="npm version" src="https://img.shields.io/npm/v/flavor-code?color=cb3837&logo=npm" /></a>
|
|
11
|
+
<a href="https://github.com/YachuanWzh/flavor-code/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/YachuanWzh/flavor-code/actions/workflows/ci.yml/badge.svg?branch=main" /></a>
|
|
12
|
+
<img alt="Node.js 20+" src="https://img.shields.io/badge/Node.js-20%2B-339933?logo=nodedotjs&logoColor=white" />
|
|
13
|
+
<a href="./LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue.svg" /></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p>
|
|
17
|
+
<a href="#快速开始">快速开始</a> ·
|
|
18
|
+
<a href="#核心能力">核心能力</a> ·
|
|
19
|
+
<a href="#使用入口">使用入口</a> ·
|
|
20
|
+
<a href="#权限与沙箱">安全</a> ·
|
|
21
|
+
<a href="#开发">参与开发</a>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Flavor Code 接入 OpenAI、Anthropic 或兼容服务,在受控工作区内使用文件、搜索、Shell、MCP 和自定义工具。复杂任务可以拆成计划和并行子任务;会话、Diff、工具调用、checkpoint 与审计记录全部保存在本地,便于恢复、复查和继续工作。
|
|
28
|
+
|
|
29
|
+
## 核心能力
|
|
30
|
+
|
|
31
|
+
| | 能力 | 你得到什么 |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| 🖥️ | **一个运行时,三个入口** | CLI、Electron 与 VS Code 共享模型配置、会话和工具能力 |
|
|
34
|
+
| 🧭 | **复杂任务可控推进** | 任务计划、子 Agent、steering、follow-up、`/loop` 和 `/goal` |
|
|
35
|
+
| ⏪ | **结果可追溯、可恢复** | 完整时间线、checkpoint、rewind、trace、Diff 和失败审计 |
|
|
36
|
+
| 🧠 | **本地长期上下文** | 记忆、Skill、插件和项目指南均保存在本机 |
|
|
37
|
+
| 🛡️ | **明确的权限边界** | 分别控制读、写、Shell、网络和破坏性操作,也可使用 Docker |
|
|
38
|
+
|
|
39
|
+
## 快速开始
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> CLI 需要 Node.js 20 或更高版本。Windows 桌面端也可以直接从 [Releases](https://github.com/YachuanWzh/flavor-code/releases) 下载。
|
|
43
|
+
|
|
44
|
+
**1. 安装**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g flavor-code
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. 在项目中启动**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd your-project
|
|
54
|
+
flavor
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**3. 初始化项目上下文**
|
|
58
|
+
|
|
59
|
+
首次进入项目后运行 `/init`。Flavor 会分析语言、包管理器、源码目录和验证命令,并生成 `FLAVOR.md` 项目指南。
|
|
60
|
+
|
|
61
|
+
也可以直接执行一次性任务:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
flavor --print "分析这个项目并列出最值得修复的三个问题"
|
|
65
|
+
flavor --resume
|
|
66
|
+
flavor --resume -p "继续完成剩余工作"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
非交互模式会拒绝需要人工审批的操作,不会悬挂等待输入。
|
|
70
|
+
|
|
71
|
+
## 配置模型
|
|
72
|
+
|
|
73
|
+
最快的方式是设置环境变量:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# macOS / Linux
|
|
77
|
+
export OPENAI_API_KEY="sk-..."
|
|
78
|
+
|
|
79
|
+
# Windows PowerShell
|
|
80
|
+
$env:OPENAI_API_KEY = "sk-..."
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
也可以把密钥放在项目根目录的 `.env`。
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary><strong>使用 <code>.flavor/flavor.json</code> 配置多个 Provider</strong></summary>
|
|
87
|
+
|
|
88
|
+
项目配置示例:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"providers": {
|
|
93
|
+
"openai": {
|
|
94
|
+
"type": "openai",
|
|
95
|
+
"apiKey": "${OPENAI_API_KEY}",
|
|
96
|
+
"defaultModel": "gpt-5",
|
|
97
|
+
"cheapModel": "gpt-5-mini"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"agents": {
|
|
101
|
+
"main": { "model": "openai:gpt-5" },
|
|
102
|
+
"subagent": { "model": "openai:gpt-5-mini" }
|
|
103
|
+
},
|
|
104
|
+
"permissionMode": "default",
|
|
105
|
+
"maxSubagents": 3,
|
|
106
|
+
"language": "zh-CN"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
配置按以下顺序合并,后者优先:
|
|
111
|
+
|
|
112
|
+
1. 全局 `~/.flavor-code/flavor.json`
|
|
113
|
+
2. 项目 `.flavor/flavor.json`
|
|
114
|
+
3. `.env`
|
|
115
|
+
4. 进程环境变量
|
|
116
|
+
|
|
117
|
+
支持的常用 Provider 类型:
|
|
118
|
+
|
|
119
|
+
- `openai`:OpenAI 官方接口
|
|
120
|
+
- `anthropic`:Anthropic 官方接口
|
|
121
|
+
- `openai-compatible`:兼容 OpenAI 协议的服务
|
|
122
|
+
|
|
123
|
+
</details>
|
|
124
|
+
|
|
125
|
+
OAuth PKCE 的运行时行为与配置约定见 [PKCE 规范](./docs/specs/pkce-runtime-config.md)。完整配置字段以 [配置 Schema](./src/config/schema.ts) 为准。
|
|
126
|
+
|
|
127
|
+
## 使用入口
|
|
128
|
+
|
|
129
|
+
| 入口 | 适合场景 | 启动方式 |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| **CLI** | 日常开发、远程环境、脚本与 CI | `flavor` |
|
|
132
|
+
| **Electron** | 可视化会话、Diff、权限和资源管理 | `npm run desktop:start` |
|
|
133
|
+
| **VS Code / Qoder** | 编辑器上下文、诊断修复和任务控制面 | `npm run ide:install` |
|
|
134
|
+
|
|
135
|
+
### CLI
|
|
136
|
+
|
|
137
|
+
直接运行 `flavor` 后输入自然语言即可。输入 `/` 会显示内置命令、插件命令和 Skill。
|
|
138
|
+
|
|
139
|
+
常用命令:
|
|
140
|
+
|
|
141
|
+
| 命令 | 作用 |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `/init` | 生成或更新 `FLAVOR.md` |
|
|
144
|
+
| `/model` | 查看或切换主/子 Agent 模型 |
|
|
145
|
+
| `/permissions` | 切换权限模式 |
|
|
146
|
+
| `/tasks` | 查看任务计划和子 Agent 状态 |
|
|
147
|
+
| `/compact` | 手动压缩长会话上下文 |
|
|
148
|
+
| `/checkpoint`、`/tree` | 保存现场、查看会话树 |
|
|
149
|
+
| `/rewind`、`/unrevert`、`/fork` | 恢复或分叉会话 |
|
|
150
|
+
| `/memory`、`/remember`、`/forget` | 管理长期记忆 |
|
|
151
|
+
| `/mcp` | 查看和管理 MCP 服务 |
|
|
152
|
+
| `/loop <goal>` | 运行带验证的自治循环 |
|
|
153
|
+
| `/goal <objective>` | 运行规划、执行、对抗审查流程 |
|
|
154
|
+
| `/audit` | 查看工具失败审计 |
|
|
155
|
+
|
|
156
|
+
运行中可以提交 steering 或排队 follow-up;当前模型响应结束后,任务会在安全边界处接收新指令。
|
|
157
|
+
|
|
158
|
+
### Electron 桌面端
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run desktop:dev # 开发模式
|
|
162
|
+
npm run desktop:start # 构建并启动
|
|
163
|
+
npm run desktop:pack # Windows 免安装目录
|
|
164
|
+
npm run desktop:dist # Windows NSIS 安装包
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
桌面端提供项目和会话切换、流式 Markdown、工具与 Diff 展示、权限确认、任务状态,以及 Skill、MCP、记忆和模型管理。
|
|
168
|
+
|
|
169
|
+
### VS Code / Qoder
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm run vscode:install # 安装到 VS Code
|
|
173
|
+
npm run qoder:install # 安装到 Qoder
|
|
174
|
+
npm run ide:install # 自动选择已安装的 IDE
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
扩展包含 `@flavor` Chat Participant、Mission Control、Changes & Health、Time Machine、诊断修复、CodeLens、checkpoint 和 rewind。若 `flavor` 不在 `PATH`,请设置 `flavorCode.executable`。
|
|
178
|
+
|
|
179
|
+
## MCP、Skill 与插件
|
|
180
|
+
|
|
181
|
+
Flavor 可以连接 stdio 或 Streamable HTTP MCP 服务。项目配置示例:
|
|
182
|
+
|
|
183
|
+
<details>
|
|
184
|
+
<summary><strong>MCP 配置与 CLI 示例</strong></summary>
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"docs": {
|
|
190
|
+
"url": "https://example.com/mcp",
|
|
191
|
+
"headers": {
|
|
192
|
+
"Authorization": "Bearer ${MCP_TOKEN}"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
MCP 配置也可以通过 CLI 管理:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
flavor mcp list
|
|
203
|
+
flavor mcp add docs --url https://example.com/mcp
|
|
204
|
+
flavor mcp disable docs
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
</details>
|
|
208
|
+
|
|
209
|
+
Skill 是带有 YAML 头信息的 `SKILL.md`,放在 `.flavor/skills/<name>/` 或 `~/.flavor-code/skills/<name>/`。Flavor 会按任务渐进加载,也支持通过 `/<skill-name>` 显式调用。
|
|
210
|
+
|
|
211
|
+
插件放在 `.flavor/plugins/`,可以注册命令、工具、Hook、Skill 根目录和模型适配器。
|
|
212
|
+
|
|
213
|
+
> [!WARNING]
|
|
214
|
+
> 插件和 Agent 自注册工具是进程内执行的 JavaScript,不是安全沙箱。只安装、启用和批准你信任的代码。
|
|
215
|
+
|
|
216
|
+
## 会话、记忆与执行记录
|
|
217
|
+
|
|
218
|
+
项目运行数据集中在 `.flavor/`:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
.flavor/
|
|
222
|
+
├── flavor.json # 项目配置
|
|
223
|
+
├── sessions/ # 会话时间线
|
|
224
|
+
├── session-assets/ # 图片附件
|
|
225
|
+
├── session-trees/ # 会话分支
|
|
226
|
+
├── checkpoints/ # 工作区快照
|
|
227
|
+
├── memory/ # 长期记忆
|
|
228
|
+
├── traces/ # 可选执行 trace
|
|
229
|
+
├── audit.jsonl # 工具失败审计
|
|
230
|
+
├── skills/ # 项目 Skill
|
|
231
|
+
└── plugins/ # 项目插件
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
长期记忆会区分用户偏好、行为反馈、项目约定和外部引用。自动提取只保存高置信候选,并提供确认、忽略和删除入口;密钥、Token、原始工具输出和模型猜测会被拒绝。
|
|
235
|
+
|
|
236
|
+
图片提示支持 PNG、JPEG 和 WebP,单图最大 5 MiB、每次最多 5 张。桌面端支持选择或拖放;CLI 剪贴板图片目前支持 Windows 和 macOS。
|
|
237
|
+
|
|
238
|
+
## 权限与沙箱
|
|
239
|
+
|
|
240
|
+
| 模式 | 行为 |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `default` | 读操作自动放行,写、Shell、网络和破坏性操作按需确认 |
|
|
243
|
+
| `acceptEdits` | 工作区写入和例行验证自动放行 |
|
|
244
|
+
| `plan` | 只读规划,不允许修改和执行 |
|
|
245
|
+
| `bypassPermissions` | 主 Agent 在硬安全检查后尽量自动执行 |
|
|
246
|
+
| `auto` | 使用分类器判断,无法确定时回到人工确认 |
|
|
247
|
+
| `bubble` | 将不确定操作冒泡给主会话审批 |
|
|
248
|
+
|
|
249
|
+
> [!CAUTION]
|
|
250
|
+
> 本地 Shell 仍然以当前用户身份运行。处理不可信项目时建议启用 Docker。
|
|
251
|
+
|
|
252
|
+
<details>
|
|
253
|
+
<summary><strong>Docker 执行环境示例</strong></summary>
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"execution": {
|
|
258
|
+
"mode": "docker",
|
|
259
|
+
"image": "node:24-bookworm-slim",
|
|
260
|
+
"network": false,
|
|
261
|
+
"memory": "2g",
|
|
262
|
+
"cpus": 2
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Docker 不可用时任务会失败,不会静默回退到本机。配置文件中的敏感字段与 OAuth Token 使用本机配置密钥进行 AES-256-GCM 认证加密。
|
|
268
|
+
|
|
269
|
+
</details>
|
|
270
|
+
|
|
271
|
+
## SDK、RPC 与评测
|
|
272
|
+
|
|
273
|
+
<details>
|
|
274
|
+
<summary><strong>Node.js SDK 示例</strong></summary>
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { createFlavorRuntime } from "flavor-code/sdk";
|
|
278
|
+
|
|
279
|
+
const runtime = await createFlavorRuntime({
|
|
280
|
+
workspace: process.cwd(),
|
|
281
|
+
approvalPolicy: "deny",
|
|
282
|
+
output: console.log,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await runtime.session.start();
|
|
286
|
+
await runtime.session.submit("修复失败的测试");
|
|
287
|
+
await runtime.dispose();
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
</details>
|
|
291
|
+
|
|
292
|
+
其他 IDE 或语言可以通过 JSONL RPC 接入:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
flavor --mode rpc --workspace . --trace .flavor/traces/run.jsonl
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
评测运行:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
flavor eval eval.json --output report.json
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
RPC、trace、replay、eval、会话树与 Docker 的设计约束见 [控制面规范](./docs/specs/2026-07-29-control-plane-sandbox-vscode.md)。
|
|
305
|
+
|
|
306
|
+
## 开发
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm ci
|
|
310
|
+
npm test
|
|
311
|
+
npm run typecheck
|
|
312
|
+
npm run vscode:typecheck
|
|
313
|
+
npm run build
|
|
314
|
+
npm run smoke:install
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- TypeScript strict,目标 ES2022,Node.js 20+
|
|
318
|
+
- Vitest 单元与集成测试
|
|
319
|
+
- tsup 构建 CLI、SDK、Electron 主进程和 VS Code 扩展
|
|
320
|
+
- Vite 构建 Electron renderer
|
|
321
|
+
- CI 覆盖 Windows/macOS 与 Node 20/24
|
|
322
|
+
|
|
323
|
+
发布构建默认不生成或打包 source map。需要本地调试构建时显式开启:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
# macOS / Linux
|
|
327
|
+
FLAVOR_SOURCEMAP=1 npm run build
|
|
328
|
+
|
|
329
|
+
# Windows PowerShell
|
|
330
|
+
$env:FLAVOR_SOURCEMAP = "1"
|
|
331
|
+
npm run build
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## 文档
|
|
335
|
+
|
|
336
|
+
- [技术方案报告](./技术方案报告.md):整体架构、Agent 循环、上下文、权限、插件和安全模型
|
|
337
|
+
- [运行时可靠性规范](./docs/specs/2026-07-26-runtime-reliability.md)
|
|
338
|
+
- [控制面、沙箱与 VS Code 规范](./docs/specs/2026-07-29-control-plane-sandbox-vscode.md)
|
|
339
|
+
- [多模态图片规范](./docs/specs/2026-07-30-multimodal-image-attachments.md)
|
|
340
|
+
- [VS Code 后续规划](./docs/specs/2026-08-01-flavor-code-vscode-next.md)
|
|
341
|
+
|
|
342
|
+
## 安全提示
|
|
343
|
+
|
|
344
|
+
- 审查模型生成的代码和命令,尤其是依赖安装、脚本和删除操作。
|
|
345
|
+
- 不要把 `.flavor/sessions/`、trace 或长期记忆当作秘密仓库。
|
|
346
|
+
- 使用最小权限 API Key,不要提交 `.env`。
|
|
347
|
+
- Skill 内容可能影响模型行为;插件和自注册工具还拥有进程内 Node.js 权限。
|
|
348
|
+
- 建议在版本控制下工作,并在高风险任务前创建 checkpoint。
|
|
349
|
+
|
|
350
|
+
## 参与贡献
|
|
351
|
+
|
|
352
|
+
欢迎提交 Issue 和 Pull Request。提交前请至少运行:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
npm test
|
|
356
|
+
npm run typecheck
|
|
357
|
+
npm run vscode:typecheck
|
|
358
|
+
npm run build
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
架构改动建议先阅读 [技术方案报告](./技术方案报告.md) 和相关 [设计规范](./docs/specs/)。
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
[MIT](./LICENSE)
|
|
366
|
+
|
|
367
|
+
<p align="center">
|
|
368
|
+
Made with 🌶️ by Flavor Code contributors.
|
|
369
|
+
</p>
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
isDestructiveTool,
|
|
13
13
|
message,
|
|
14
14
|
modelContentTranscriptText,
|
|
15
|
+
packageVersion,
|
|
15
16
|
redactErrorText,
|
|
16
17
|
transcriptReducer
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-3TYRDK34.js";
|
|
18
19
|
import "./chunk-HFR2WS6T.js";
|
|
19
20
|
import {
|
|
20
21
|
Box_default,
|
|
@@ -1463,7 +1464,11 @@ function WelcomeCard({ model, serviceName, workspaceName, columns }) {
|
|
|
1463
1464
|
/* @__PURE__ */ jsx4(Text, { color: FLAVOR_ACCENT, children: FLAVOR_WORDMARK }),
|
|
1464
1465
|
serviceName === void 0 ? null : /* @__PURE__ */ jsx4(Text, { color: "cyan", wrap: "truncate-end", children: serviceName }),
|
|
1465
1466
|
/* @__PURE__ */ jsx4(Text, { dimColor: true, wrap: "truncate-end", children: model }),
|
|
1466
|
-
/* @__PURE__ */ jsx4(Text, { dimColor: true, wrap: "truncate-end", children: workspaceName })
|
|
1467
|
+
/* @__PURE__ */ jsx4(Text, { dimColor: true, wrap: "truncate-end", children: workspaceName }),
|
|
1468
|
+
/* @__PURE__ */ jsxs3(Text, { dimColor: true, wrap: "truncate-end", children: [
|
|
1469
|
+
"v",
|
|
1470
|
+
packageVersion()
|
|
1471
|
+
] })
|
|
1467
1472
|
]
|
|
1468
1473
|
}
|
|
1469
1474
|
),
|
|
@@ -1495,7 +1500,9 @@ function WelcomeCard({ model, serviceName, workspaceName, columns }) {
|
|
|
1495
1500
|
/* @__PURE__ */ jsxs3(Text, { dimColor: true, wrap: "truncate-end", children: [
|
|
1496
1501
|
model,
|
|
1497
1502
|
" \xB7 ",
|
|
1498
|
-
workspaceName
|
|
1503
|
+
workspaceName,
|
|
1504
|
+
" \xB7 v",
|
|
1505
|
+
packageVersion()
|
|
1499
1506
|
] }),
|
|
1500
1507
|
/* @__PURE__ */ jsxs3(Text, { children: [
|
|
1501
1508
|
/* @__PURE__ */ jsx4(Text, { color: "cyan", children: "/init" }),
|
|
@@ -104,10 +104,12 @@ function isDestructiveTool(name) {
|
|
|
104
104
|
return DESTRUCTIVE_TOOLS.has(name);
|
|
105
105
|
}
|
|
106
106
|
var PermissionEngine = class {
|
|
107
|
+
#lexicalWorkspace;
|
|
107
108
|
#workspace;
|
|
108
109
|
#mode;
|
|
109
110
|
constructor(options) {
|
|
110
111
|
const root = resolve(options.workspace);
|
|
112
|
+
this.#lexicalWorkspace = root;
|
|
111
113
|
this.#workspace = existsSync(root) ? realpathSync.native(root) : root;
|
|
112
114
|
this.#mode = canonicalMode(options.mode ?? "default");
|
|
113
115
|
}
|
|
@@ -128,13 +130,13 @@ var PermissionEngine = class {
|
|
|
128
130
|
return { decision: "deny", reason: `${request.tool} requires source and destination paths` };
|
|
129
131
|
}
|
|
130
132
|
for (const path of paths) {
|
|
131
|
-
const classification = classifyPath(this.#workspace, path);
|
|
133
|
+
const classification = classifyPath(this.#lexicalWorkspace, this.#workspace, path);
|
|
132
134
|
if (classification.escape) return { decision: "deny", reason: classification.reason ?? "Path escapes the workspace" };
|
|
133
135
|
if (request.agent === "subagent" && !classification.inside) {
|
|
134
136
|
return { decision: "deny", reason: "Subagents are restricted to the workspace" };
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
|
-
const inside = paths.every((path) => classifyPath(this.#workspace, path).inside);
|
|
139
|
+
const inside = paths.every((path) => classifyPath(this.#lexicalWorkspace, this.#workspace, path).inside);
|
|
138
140
|
if (this.#mode === "plan") {
|
|
139
141
|
return READ_TOOLS.has(request.tool) ? { decision: "allow" } : { decision: "deny", reason: "Plan mode is read-only" };
|
|
140
142
|
}
|
|
@@ -161,11 +163,11 @@ var PermissionEngine = class {
|
|
|
161
163
|
const analysis = request.args === void 0 ? analyzeCommand(request.command ?? "") : analyzeArgumentCommand(request.command ?? "", request.args);
|
|
162
164
|
if (request.agent === "subagent") {
|
|
163
165
|
if (request.cwd === void 0) return { decision: "ask", reason: "Subagent shell commands require an explicit workspace cwd" };
|
|
164
|
-
const cwd = classifyPath(this.#workspace, request.cwd);
|
|
166
|
+
const cwd = classifyPath(this.#lexicalWorkspace, this.#workspace, request.cwd);
|
|
165
167
|
if (cwd.escape || !cwd.inside) return { decision: "deny", reason: "Subagent shell cwd must remain in the workspace" };
|
|
166
168
|
if (analysis.destructive) return { decision: "deny", reason: "Destructive commands are forbidden for subagents" };
|
|
167
169
|
if (analysis.wrapped || analysis.opaque || !isRoutineCommand(analysis.command)) return { decision: "ask", reason: "Subagent shell command requires main-Agent approval" };
|
|
168
|
-
const argumentDecision = assessRoutineArguments(analysis.command, request.cwd, this.#workspace);
|
|
170
|
+
const argumentDecision = assessRoutineArguments(analysis.command, request.cwd, this.#lexicalWorkspace, this.#workspace);
|
|
169
171
|
if (argumentDecision !== "allow") {
|
|
170
172
|
return argumentDecision === "deny" ? { decision: "deny", reason: "Subagent command arguments escape the workspace" } : { decision: "ask", reason: "Ambiguous subagent command arguments require main-Agent approval" };
|
|
171
173
|
}
|
|
@@ -177,12 +179,12 @@ var PermissionEngine = class {
|
|
|
177
179
|
if (analysis.wrapped || analysis.opaque) return ask(this.#mode, "Shell wrapper requires approval");
|
|
178
180
|
if (this.#mode === "bypassPermissions") return { decision: "allow" };
|
|
179
181
|
if ((this.#mode === "acceptEdits" || this.#mode === "auto") && isRoutineCommand(analysis.command)) {
|
|
180
|
-
const cwd = request.cwd ?? this.#
|
|
181
|
-
const cwdClassification = classifyPath(this.#workspace, cwd);
|
|
182
|
+
const cwd = request.cwd ?? this.#lexicalWorkspace;
|
|
183
|
+
const cwdClassification = classifyPath(this.#lexicalWorkspace, this.#workspace, cwd);
|
|
182
184
|
if (cwdClassification.escape || !cwdClassification.inside) {
|
|
183
185
|
return { decision: "deny", reason: "Routine command cwd must remain in the workspace" };
|
|
184
186
|
}
|
|
185
|
-
const argumentDecision = assessRoutineArguments(analysis.command, cwd, this.#workspace);
|
|
187
|
+
const argumentDecision = assessRoutineArguments(analysis.command, cwd, this.#lexicalWorkspace, this.#workspace);
|
|
186
188
|
if (argumentDecision === "deny") return { decision: "deny", reason: "Routine command arguments escape the workspace" };
|
|
187
189
|
if (argumentDecision === "ask") return ask(this.#mode, "Ambiguous routine command arguments require approval");
|
|
188
190
|
return { decision: "allow" };
|
|
@@ -219,16 +221,16 @@ function analyzeArgumentCommand(executable, args) {
|
|
|
219
221
|
}
|
|
220
222
|
return { command, destructive: isForbiddenCommand(command), opaque: false, wrapped: false };
|
|
221
223
|
}
|
|
222
|
-
function classifyPath(
|
|
223
|
-
const candidate = resolve(
|
|
224
|
-
const lexicalInside = isWithin(
|
|
224
|
+
function classifyPath(lexicalWorkspace, physicalWorkspace, input) {
|
|
225
|
+
const candidate = resolve(lexicalWorkspace, input);
|
|
226
|
+
const lexicalInside = isWithin(lexicalWorkspace, candidate);
|
|
225
227
|
const traversal = input.split(/[\\/]+/).includes("..");
|
|
226
|
-
const startsInWorkspace = pathStartsWith(
|
|
228
|
+
const startsInWorkspace = pathStartsWith(lexicalWorkspace, input);
|
|
227
229
|
if ((!isAbsolute(input) || startsInWorkspace) && traversal && !lexicalInside) {
|
|
228
230
|
return { inside: false, escape: true, reason: "Path traversal escapes the workspace" };
|
|
229
231
|
}
|
|
230
232
|
const physical = resolvePhysical(candidate);
|
|
231
|
-
const physicalInside = isWithin(
|
|
233
|
+
const physicalInside = isWithin(physicalWorkspace, physical);
|
|
232
234
|
if (lexicalInside && !physicalInside) return { inside: false, escape: true, reason: "Symlink escapes the workspace" };
|
|
233
235
|
return { inside: physicalInside, escape: false };
|
|
234
236
|
}
|
|
@@ -318,7 +320,7 @@ function isRoutineCommand(command) {
|
|
|
318
320
|
if (command.executable === "go") return first === "test";
|
|
319
321
|
return ["pytest", "vitest", "jest", "eslint", "tsc"].includes(command.executable);
|
|
320
322
|
}
|
|
321
|
-
function assessRoutineArguments(command, cwd,
|
|
323
|
+
function assessRoutineArguments(command, cwd, lexicalWorkspace, physicalWorkspace) {
|
|
322
324
|
const args = routineArguments(command);
|
|
323
325
|
if (args === void 0) return "ask";
|
|
324
326
|
const pathOptions = /* @__PURE__ */ new Set([
|
|
@@ -343,7 +345,7 @@ function assessRoutineArguments(command, cwd, workspace) {
|
|
|
343
345
|
const option2 = equals[1]?.toLowerCase() ?? "";
|
|
344
346
|
const value = equals[2] ?? "";
|
|
345
347
|
if (!pathOptions.has(option2)) return "ask";
|
|
346
|
-
const decision = assessArgumentPath(value, cwd,
|
|
348
|
+
const decision = assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace);
|
|
347
349
|
if (decision !== "allow") return decision;
|
|
348
350
|
continue;
|
|
349
351
|
}
|
|
@@ -351,14 +353,14 @@ function assessRoutineArguments(command, cwd, workspace) {
|
|
|
351
353
|
if (option !== void 0 && pathOptions.has(option)) {
|
|
352
354
|
const value = args[index + 1];
|
|
353
355
|
if (value === void 0 || value.startsWith("-")) return "ask";
|
|
354
|
-
const decision = assessArgumentPath(value, cwd,
|
|
356
|
+
const decision = assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace);
|
|
355
357
|
if (decision !== "allow") return decision;
|
|
356
358
|
index += 1;
|
|
357
359
|
continue;
|
|
358
360
|
}
|
|
359
361
|
if (arg.startsWith("-")) continue;
|
|
360
362
|
if (looksLikePath(arg)) {
|
|
361
|
-
const decision = assessArgumentPath(arg, cwd,
|
|
363
|
+
const decision = assessArgumentPath(arg, cwd, lexicalWorkspace, physicalWorkspace);
|
|
362
364
|
if (decision !== "allow") return decision;
|
|
363
365
|
continue;
|
|
364
366
|
}
|
|
@@ -376,10 +378,10 @@ function routineArguments(command) {
|
|
|
376
378
|
if (["pytest", "vitest", "jest", "eslint", "tsc"].includes(command.executable)) return command.args;
|
|
377
379
|
return void 0;
|
|
378
380
|
}
|
|
379
|
-
function assessArgumentPath(value, cwd,
|
|
381
|
+
function assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace) {
|
|
380
382
|
if (value.length === 0) return "ask";
|
|
381
383
|
const resolved = resolve(cwd, value);
|
|
382
|
-
const classification = classifyPath(
|
|
384
|
+
const classification = classifyPath(lexicalWorkspace, physicalWorkspace, resolved);
|
|
383
385
|
return classification.escape || !classification.inside ? "deny" : "allow";
|
|
384
386
|
}
|
|
385
387
|
function looksLikePath(value) {
|
|
@@ -5042,7 +5044,8 @@ var ToolRuntime = class {
|
|
|
5042
5044
|
} catch {
|
|
5043
5045
|
}
|
|
5044
5046
|
}
|
|
5045
|
-
|
|
5047
|
+
const sessionAllowed = this.#alwaysAllowed.has(getToolCategory(tool.name));
|
|
5048
|
+
if (!sessionAllowed && (pre.decision === "ask" || permission.decision === "ask")) {
|
|
5046
5049
|
const reason = [pre.reason, permission.reason].filter((value) => value !== void 0).join("\n") || "Approval required";
|
|
5047
5050
|
const requestDecision = await this.#hooks.emit({
|
|
5048
5051
|
version: 1,
|
|
@@ -5067,8 +5070,7 @@ var ToolRuntime = class {
|
|
|
5067
5070
|
throw signal.reason;
|
|
5068
5071
|
} else {
|
|
5069
5072
|
const category = getToolCategory(tool.name);
|
|
5070
|
-
if (this.#
|
|
5071
|
-
} else if (this.#approve === void 0) {
|
|
5073
|
+
if (this.#approve === void 0) {
|
|
5072
5074
|
return this.#fail(tool.name, input, context.agent, "permission_denied", reason);
|
|
5073
5075
|
} else {
|
|
5074
5076
|
const decision = await this.#approve({ ...request, reason }, signal);
|
|
@@ -9220,9 +9222,11 @@ var SkillRegistry = class {
|
|
|
9220
9222
|
for (const configuredRoot of roots) {
|
|
9221
9223
|
const root = resolve13(configuredRoot);
|
|
9222
9224
|
let entries;
|
|
9225
|
+
let physicalRoot = root;
|
|
9223
9226
|
try {
|
|
9224
9227
|
const rootInfo = await lstat6(root);
|
|
9225
9228
|
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("Skill root must be a real directory");
|
|
9229
|
+
physicalRoot = await realpath4(root);
|
|
9226
9230
|
entries = await readdir5(root, { withFileTypes: true });
|
|
9227
9231
|
} catch (error) {
|
|
9228
9232
|
if (isMissing2(error)) continue;
|
|
@@ -9235,12 +9239,12 @@ var SkillRegistry = class {
|
|
|
9235
9239
|
try {
|
|
9236
9240
|
if (entry2.isSymbolicLink()) throw new Error("Symlinked skill directories are not allowed");
|
|
9237
9241
|
if (!entry2.isDirectory()) continue;
|
|
9238
|
-
const
|
|
9239
|
-
if (!isWithin3(
|
|
9242
|
+
const physicalSkillRoot = await realpath4(skillRoot);
|
|
9243
|
+
if (!isWithin3(physicalRoot, physicalSkillRoot)) throw new Error("Skill directory escapes its registry root");
|
|
9240
9244
|
const skillFile = resolve13(skillRoot, "SKILL.md");
|
|
9241
9245
|
const parsed = await readFrontmatterFile(
|
|
9242
9246
|
skillFile,
|
|
9243
|
-
|
|
9247
|
+
physicalSkillRoot,
|
|
9244
9248
|
this.#options.maxMetadataBytes,
|
|
9245
9249
|
this.#options.openFile
|
|
9246
9250
|
);
|
|
@@ -9251,7 +9255,7 @@ var SkillRegistry = class {
|
|
|
9251
9255
|
name: parsed.name,
|
|
9252
9256
|
description: parsed.description,
|
|
9253
9257
|
source,
|
|
9254
|
-
root:
|
|
9258
|
+
root: physicalSkillRoot,
|
|
9255
9259
|
disableModelInvocation: parsed.disableModelInvocation
|
|
9256
9260
|
}),
|
|
9257
9261
|
skillFile
|
|
@@ -10747,6 +10751,7 @@ function createEditTool(workspace, options = {}) {
|
|
|
10747
10751
|
paths: (input) => [guard.lexical(input.path)],
|
|
10748
10752
|
execute: async (input, signal) => {
|
|
10749
10753
|
abortIfNeeded(signal);
|
|
10754
|
+
const displayPath = guard.lexical(input.path);
|
|
10750
10755
|
const path = await guard.existing(input.path);
|
|
10751
10756
|
const contents = await readText(path);
|
|
10752
10757
|
const hasCRLF = contents.includes("\r\n");
|
|
@@ -10762,11 +10767,11 @@ function createEditTool(workspace, options = {}) {
|
|
|
10762
10767
|
}
|
|
10763
10768
|
const updatedLF = contentsLF.slice(0, first) + newTextLF + contentsLF.slice(first + oldTextLF.length);
|
|
10764
10769
|
const updated = hasCRLF ? updatedLF.replace(/\n/g, "\r\n") : updatedLF;
|
|
10765
|
-
await options.beforeCommit?.({ path, before: contents, after: updated, kind: "update" }, signal);
|
|
10770
|
+
await options.beforeCommit?.({ path: displayPath, before: contents, after: updated, kind: "update" }, signal);
|
|
10766
10771
|
await atomicWrite(path, updated, signal);
|
|
10767
10772
|
return withToolPresentation(
|
|
10768
|
-
{ path, replacements: 1 },
|
|
10769
|
-
buildFileChangePresentation(
|
|
10773
|
+
{ path: displayPath, replacements: 1 },
|
|
10774
|
+
buildFileChangePresentation(displayPath, contents, updated, "update")
|
|
10770
10775
|
);
|
|
10771
10776
|
}
|
|
10772
10777
|
};
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module"; const require = createRequire(impor
|
|
|
3
3
|
import {
|
|
4
4
|
FlavorRpcServer,
|
|
5
5
|
TraceRecorder
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-Z3BW52MK.js";
|
|
7
7
|
import {
|
|
8
8
|
staticTaskLines
|
|
9
9
|
} from "./chunk-KKIBYDJI.js";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
message,
|
|
21
21
|
packageVersion,
|
|
22
22
|
redactErrorText
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-3TYRDK34.js";
|
|
24
24
|
import {
|
|
25
25
|
McpServerConfigSchema,
|
|
26
26
|
McpServerNameSchema,
|
|
@@ -771,7 +771,7 @@ function createProgram() {
|
|
|
771
771
|
const [{ render, AlternateScreen }, { createElement }, { App }] = await Promise.all([
|
|
772
772
|
import("./claude-ink-WPWPEL5A.js"),
|
|
773
773
|
import("react"),
|
|
774
|
-
import("./app-
|
|
774
|
+
import("./app-3DXEGSS7.js")
|
|
775
775
|
]);
|
|
776
776
|
const instance = await render(createElement(
|
|
777
777
|
AlternateScreen,
|