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.
@@ -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-MRISY6KK.js";
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.#workspace;
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(workspace, input) {
223
- const candidate = resolve(workspace, input);
224
- const lexicalInside = isWithin(workspace, candidate);
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(workspace, input);
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(workspace, physical);
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, workspace) {
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, workspace);
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, workspace);
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, workspace);
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, workspace) {
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(workspace, resolved);
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
- if (pre.decision === "ask" || permission.decision === "ask") {
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.#alwaysAllowed.has(category)) {
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 physicalRoot = await realpath4(skillRoot);
9239
- if (!isWithin3(root, physicalRoot)) throw new Error("Skill directory escapes its registry root");
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
- physicalRoot,
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: physicalRoot,
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(path, contents, updated, "update")
10773
+ { path: displayPath, replacements: 1 },
10774
+ buildFileChangePresentation(displayPath, contents, updated, "update")
10770
10775
  );
10771
10776
  }
10772
10777
  };
@@ -4,7 +4,7 @@ import {
4
4
  message,
5
5
  redactErrorText,
6
6
  redactSecrets
7
- } from "./chunk-MRISY6KK.js";
7
+ } from "./chunk-3TYRDK34.js";
8
8
 
9
9
  // src/rpc/server.ts
10
10
  import { createInterface } from "readline";
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-L5A2NDMT.js";
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-MRISY6KK.js";
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-N3655H2E.js")
774
+ import("./app-3DXEGSS7.js")
775
775
  ]);
776
776
  const instance = await render(createElement(
777
777
  AlternateScreen,