@cosmicstack/mercury-agent 1.1.0 → 1.1.1

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
@@ -20,6 +20,10 @@
20
20
  <a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/@cosmicstack/mercury-agent" alt="node"></a>
21
21
  </p>
22
22
 
23
+ <p align="center">
24
+ English | <a href="README.zh-CN.md">简体中文</a>
25
+ </p>
26
+
23
27
  ---
24
28
 
25
29
  ## Quick Start
@@ -0,0 +1,240 @@
1
+ # Mercury Agent 中文文档
2
+
3
+ > 一个以“灵魂”为中心的 AI Agent,内置权限加固工具、Token 预算、多渠道访问和 SQLite 支持的 Second Brain 记忆。
4
+
5
+ [English](README.md) | 简体中文
6
+
7
+ Mercury 会记住重要信息,在执行有风险的操作前先请求确认,并且可以通过 CLI 或 Telegram 以 24/7 后台进程运行。它适合需要本地文件操作、命令执行、长期记忆、定时任务和多模型兜底能力的个人 AI 助手场景。
8
+
9
+ ## 快速开始
10
+
11
+ 直接运行:
12
+
13
+ ```bash
14
+ npx @cosmicstack/mercury-agent
15
+ ```
16
+
17
+ 或全局安装:
18
+
19
+ ```bash
20
+ npm i -g @cosmicstack/mercury-agent
21
+ mercury
22
+ ```
23
+
24
+ 首次运行会启动配置向导。你需要输入姓名、模型 API Key,并可选择配置 Telegram Bot Token。之后如需重新配置:
25
+
26
+ ```bash
27
+ mercury doctor
28
+ ```
29
+
30
+ ## 为什么选择 Mercury
31
+
32
+ - **权限优先**:Shell 命令有阻止列表,文件读写受目录作用域限制,危险操作会进入待审批流程。
33
+ - **Second Brain 记忆**:基于 SQLite 和 FTS5 的结构化持久记忆,支持自动提取、相关召回、冲突处理和自动整理。
34
+ - **灵魂驱动**:人格由你拥有的 Markdown 文件定义,包括 `soul.md`、`persona.md`、`taste.md` 和 `heartbeat.md`。
35
+ - **Token 感知**:内置每日 Token 预算,超过阈值后自动简洁回复,并支持 `/budget` 查看、重置或临时覆盖。
36
+ - **实时流式输出**:CLI 支持实时 Token 流和 Markdown 重渲染,Telegram 支持可编辑状态消息。
37
+ - **持续运行**:可作为后台守护进程运行,崩溃后自动重启,并支持开机自启、定时任务和主动通知。
38
+ - **可扩展**:支持安装社区 Skill、调度 Skill 定时运行,并兼容 [Agent Skills](https://agentskills.io) 规范。
39
+
40
+ ## 守护进程模式
41
+
42
+ 推荐使用:
43
+
44
+ ```bash
45
+ mercury up
46
+ ```
47
+
48
+ 该命令会安装系统服务、启动后台守护进程,并确保 Mercury 正在运行。如果 Mercury 已经运行,它只会确认状态并显示 PID。
49
+
50
+ 常用命令:
51
+
52
+ ```bash
53
+ mercury restart # 重启后台进程
54
+ mercury stop # 停止后台进程
55
+ mercury start -d # 后台启动,不安装系统服务
56
+ mercury logs # 查看近期守护进程日志
57
+ mercury status # 查看运行状态
58
+ ```
59
+
60
+ 系统服务支持:
61
+
62
+ | 平台 | 方式 | 是否需要管理员权限 |
63
+ |------|------|--------------------|
64
+ | macOS | LaunchAgent (`~/Library/LaunchAgents/`) | 否 |
65
+ | Linux | systemd user unit (`~/.config/systemd/user/`) | 否,开机启动可能需要 linger |
66
+ | Windows | Task Scheduler (`schtasks`) | 否 |
67
+
68
+ ## CLI 命令
69
+
70
+ | 命令 | 说明 |
71
+ |------|------|
72
+ | `mercury up` | 推荐命令:安装服务、启动守护进程并确保运行 |
73
+ | `mercury` | 启动 Agent,等同于 `mercury start` |
74
+ | `mercury start` | 前台启动 |
75
+ | `mercury start -d` | 后台启动 |
76
+ | `mercury restart` | 重启后台进程 |
77
+ | `mercury stop` | 停止后台进程 |
78
+ | `mercury logs` | 查看近期日志 |
79
+ | `mercury doctor` | 重新配置,回车保留现有值 |
80
+ | `mercury setup` | 重新运行配置向导 |
81
+ | `mercury status` | 查看配置和守护进程状态 |
82
+ | `mercury help` | 查看完整手册 |
83
+ | `mercury upgrade` | 升级到最新版本 |
84
+ | `mercury telegram list` | 查看已批准和待处理的 Telegram 用户 |
85
+ | `mercury telegram approve <code\|id>` | 批准配对码或待处理请求 |
86
+ | `mercury telegram reject <id>` | 拒绝 Telegram 访问请求 |
87
+ | `mercury telegram remove <id>` | 移除已批准用户 |
88
+ | `mercury telegram promote <id>` | 将 Telegram 成员提升为管理员 |
89
+ | `mercury telegram demote <id>` | 将 Telegram 管理员降级为成员 |
90
+ | `mercury telegram reset` | 清空 Telegram 访问状态并重新开始 |
91
+ | `mercury service install` | 安装开机自启系统服务 |
92
+ | `mercury service uninstall` | 卸载系统服务 |
93
+ | `mercury service status` | 查看系统服务状态 |
94
+ | `mercury --verbose` | 使用调试日志启动 |
95
+
96
+ ## 对话内命令
97
+
98
+ 这些命令可在 CLI 或 Telegram 对话中输入,不消耗 API Token。
99
+
100
+ | 命令 | 说明 |
101
+ |------|------|
102
+ | `/help` | 查看完整手册 |
103
+ | `/status` | 查看 Agent 配置、预算和用量 |
104
+ | `/tools` | 列出已加载工具 |
105
+ | `/skills` | 列出已安装 Skill |
106
+ | `/stream` | 切换 Telegram 文本流式输出 |
107
+ | `/stream off` | 关闭流式输出,改为单条消息 |
108
+ | `/budget` | 查看 Token 预算状态 |
109
+ | `/budget override` | 为单次请求临时覆盖预算 |
110
+ | `/budget reset` | 将用量重置为零 |
111
+ | `/budget set <n>` | 修改每日 Token 预算 |
112
+ | `/permissions` | 修改权限模式 |
113
+ | `/tasks` | 列出定时任务 |
114
+ | `/memory` | 查看和管理 Second Brain 记忆 |
115
+ | `/unpair` | Telegram:重置所有访问 |
116
+
117
+ ## 内置工具
118
+
119
+ | 分类 | 工具 |
120
+ |------|------|
121
+ | 文件系统 | `read_file`, `write_file`, `create_file`, `edit_file`, `list_dir`, `delete_file`, `send_file`, `approve_scope` |
122
+ | Shell | `run_command`, `cd`, `approve_command` |
123
+ | 消息 | `send_message` |
124
+ | Git | `git_status`, `git_diff`, `git_log`, `git_add`, `git_commit`, `git_push` |
125
+ | Web | `fetch_url` |
126
+ | Skills | `install_skill`, `list_skills`, `use_skill` |
127
+ | 调度 | `schedule_task`, `list_scheduled_tasks`, `cancel_scheduled_task` |
128
+ | 系统 | `budget_status` |
129
+
130
+ ## 渠道
131
+
132
+ | 渠道 | 能力 |
133
+ |------|------|
134
+ | CLI | Readline 提示符、方向键命令菜单、实时文本流、Markdown 重渲染、权限模式选择 |
135
+ | Telegram | HTML 格式化、可编辑流式消息、文件上传、输入状态、多用户访问和管理员/成员角色 |
136
+
137
+ ### Telegram 访问模型
138
+
139
+ Mercury 使用组织式访问模型,包含管理员和成员。
140
+
141
+ - 首次设置:向你的 Bot 发送 `/start`,获取配对码,然后在 CLI 中执行 `mercury telegram approve <code>`。你会成为首位管理员。
142
+ - 新用户:发送 `/start` 请求访问,由管理员在 CLI 中批准或拒绝。
143
+ - 角色:管理员可以批准、拒绝、提升、降级和重置访问;成员可以与 Mercury 对话。
144
+ - 重置:管理员可在 Telegram 发送 `/unpair`,或在 CLI 中执行 `mercury telegram reset`。
145
+ - 仅支持私聊,群聊消息会被忽略。
146
+
147
+ ## 调度器
148
+
149
+ - **周期任务**:使用 cron 表达式,例如 `0 9 * * *` 表示每天 9 点。
150
+ - **一次性任务**:使用 `delay_seconds`,例如 15 秒后执行。
151
+ - 任务会持久化到 `~/.mercury/schedules.yaml`,重启后自动恢复。
152
+ - 执行结果会返回到创建任务时所在的渠道。
153
+
154
+ ## Second Brain
155
+
156
+ Mercury 默认启用结构化持久记忆,并会在对话后自动提取、存储和召回与你有关的重要事实。
157
+
158
+ - 10 种记忆类型:identity、preference、goal、project、habit、decision、constraint、relationship、episode、reflection。
159
+ - 自动提取:每轮对话后提取 0 到 3 条事实,并记录置信度、重要性和持久性。
160
+ - 相关召回:每次消息前注入最相关的前 5 条记忆,默认预算 900 字符。
161
+ - 自动整理:每 60 分钟生成个人资料摘要、活跃状态摘要和反思。
162
+ - 冲突处理:按置信度和时间新旧处理相互冲突的记忆。
163
+ - 自动修剪:活跃作用域记忆 21 天后过期,推断记忆会衰减,低置信持久记忆 120 天后撤销。
164
+ - 用户控制:通过 `/memory` 查看、搜索、暂停、恢复和清空。
165
+ - 禁用方式:设置 `SECOND_BRAIN_ENABLED=false`,或在配置中设置 `memory.secondBrain.enabled: false`。
166
+
167
+ 所有数据都保存在本机 `~/.mercury/memory/second-brain/second-brain.db`,不会上传到云端。
168
+
169
+ ## 配置位置
170
+
171
+ 运行时数据保存在 `~/.mercury/`,不会写入你的项目目录。
172
+
173
+ | 路径 | 用途 |
174
+ |------|------|
175
+ | `~/.mercury/mercury.yaml` | 主配置,包括提供商、渠道和预算 |
176
+ | `~/.mercury/.env` | API Key 和 Token |
177
+ | `~/.mercury/soul/*.md` | Agent 人格文件 |
178
+ | `~/.mercury/permissions.yaml` | 能力和审批规则 |
179
+ | `~/.mercury/skills/` | 已安装 Skill |
180
+ | `~/.mercury/schedules.yaml` | 定时任务 |
181
+ | `~/.mercury/token-usage.json` | 每日 Token 用量 |
182
+ | `~/.mercury/memory/short-term/` | 每段对话的短期记忆 JSON 文件 |
183
+ | `~/.mercury/memory/long-term/` | 自动提取事实,JSONL 格式 |
184
+ | `~/.mercury/memory/episodic/` | 带时间戳的事件日志,JSONL 格式 |
185
+ | `~/.mercury/memory/second-brain/` | 结构化记忆数据库 |
186
+ | `~/.mercury/daemon.pid` | 后台进程 PID |
187
+ | `~/.mercury/daemon.log` | 守护进程日志 |
188
+
189
+ ## 模型提供商兜底
190
+
191
+ Mercury 可以配置多个 LLM 提供商,并按顺序自动尝试。如果某个提供商失败,会切换到下一个。
192
+
193
+ | 提供商 | 默认模型 | API Key | 说明 |
194
+ |--------|----------|---------|------|
195
+ | DeepSeek | `deepseek-chat` | `DEEPSEEK_API_KEY` | 默认、成本较低 |
196
+ | OpenAI | `gpt-4o-mini` | `OPENAI_API_KEY` | 支持 GPT-4o、o3 等 |
197
+ | Anthropic | `claude-sonnet-4` | `ANTHROPIC_API_KEY` | Claude Sonnet、Haiku、Opus |
198
+ | Grok (xAI) | `grok-4` | `GROK_API_KEY` | OpenAI 兼容接口 |
199
+ | Ollama Cloud | `gpt-oss:120b` | `OLLAMA_CLOUD_API_KEY` | 远程 Ollama API |
200
+ | Ollama Local | `gpt-oss:20b` | 无需 Key | 本地 Ollama 实例 |
201
+
202
+ ## 架构
203
+
204
+ - TypeScript + Node.js 20+
205
+ - Vercel AI SDK v4,支持 `generateText`、`streamText` 和多步 Agent 循环
206
+ - grammY Telegram Bot
207
+ - SQLite + FTS5 Second Brain
208
+ - JSONL 短期、长期和情景记忆
209
+ - 后台守护进程、PID 文件和崩溃恢复
210
+ - macOS、Linux、Windows 系统服务
211
+
212
+ ## 参与贡献
213
+
214
+ 欢迎贡献修复、工具、记忆能力、渠道能力或文档改进。请保持 PR 聚焦,并在提交前运行:
215
+
216
+ ```bash
217
+ npm install
218
+ npm run build
219
+ ```
220
+
221
+ 贡献 Mercury 时请特别注意:
222
+
223
+ - 工具必须走权限系统,不能绕过审批。
224
+ - 面向 Agent 循环设计,尽量保持幂等。
225
+ - 避免冗长输出和过度日志,Token 预算是核心约束。
226
+ - CLI 和 Telegram 行为应尽量一致。
227
+ - 新增依赖、破坏性变更和 soul/persona 系统调整应先讨论。
228
+
229
+ ## 许可证
230
+
231
+ MIT © [Cosmic Stack](https://github.com/cosmicstack-labs)
232
+
233
+ ## 社区
234
+
235
+ - Discord:[加入 Mercury Agent Discord](https://discord.gg/5emMpMJy5J)
236
+ - 邮箱:[mercury@cosmicstack.org](mailto:mercury@cosmicstack.org)
237
+
238
+ ## 免责声明
239
+
240
+ 这是 AI 软件,可能出现错误。请自行评估风险后使用。
package/dist/index.js CHANGED
@@ -4581,6 +4581,22 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
4581
4581
  this.shutdown();
4582
4582
  return true;
4583
4583
  }
4584
+ if (cmd === "/permissions") {
4585
+ if (channelType === "cli" && channel instanceof CLIChannel) {
4586
+ const mode = await channel.askPermissionMode?.();
4587
+ if (mode === "allow-all") {
4588
+ this.capabilities.permissions.setAutoApproveAll(true);
4589
+ this.capabilities.permissions.addTempScope("/", true, true);
4590
+ await channel.send("Allow All mode active for this session. All scopes, commands, and loops auto-approved. Resets on restart.", channelId);
4591
+ } else {
4592
+ this.capabilities.permissions.setAutoApproveAll(false);
4593
+ await channel.send("Ask Me mode active. Risky actions will prompt for confirmation.", channelId);
4594
+ }
4595
+ return true;
4596
+ }
4597
+ await channel.send("Use /permissions in CLI to switch permission mode. On Telegram, use the /permissions button or command.", channelId);
4598
+ return true;
4599
+ }
4584
4600
  if (cmd === "/status") {
4585
4601
  const config = ctx.config();
4586
4602
  const budget = ctx.tokenBudget();
@@ -4833,9 +4849,11 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
4833
4849
  await channel.withMenu(async (select) => {
4834
4850
  while (true) {
4835
4851
  const streamLabel = this.telegramStreaming ? "Disable Telegram Streaming" : "Enable Telegram Streaming";
4852
+ const permLabel = this.capabilities.permissions.isAutoApproveAll() ? "Switch to Ask Me" : "Switch to Allow All";
4836
4853
  const action = await select("Mercury Commands", [
4837
4854
  { value: "status", label: "Status" },
4838
4855
  { value: "memory", label: "Memory" },
4856
+ { value: "permissions", label: permLabel },
4839
4857
  { value: "telegram", label: "Telegram" },
4840
4858
  { value: "tools", label: "Tools" },
4841
4859
  { value: "skills", label: "Skills" },
@@ -4858,6 +4876,10 @@ Or use /budget override, /budget reset, /budget set <number> anytime.`,
4858
4876
  }
4859
4877
  continue;
4860
4878
  }
4879
+ if (action === "permissions") {
4880
+ await this.handleChatCommand("/permissions", "cli", channelId);
4881
+ continue;
4882
+ }
4861
4883
  if (action === "telegram") {
4862
4884
  await this.openCliTelegramMenu(channel, channelId, select);
4863
4885
  continue;
@@ -5365,6 +5387,11 @@ var ChannelRegistry = class {
5365
5387
  import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
5366
5388
  import { join as join7 } from "path";
5367
5389
  var TOKEN_FILE = "token-usage.json";
5390
+ function safeNumber(value) {
5391
+ if (value === null || value === void 0) return 0;
5392
+ const n = Number(value);
5393
+ return isNaN(n) ? 0 : n;
5394
+ }
5368
5395
  var TokenBudget = class {
5369
5396
  constructor(config) {
5370
5397
  this.config = config;
@@ -5380,7 +5407,7 @@ var TokenBudget = class {
5380
5407
  forceNext = false;
5381
5408
  canAfford(estimatedTokens) {
5382
5409
  this.resetIfNewDay();
5383
- return this.dailyUsed + estimatedTokens <= this.dailyBudget;
5410
+ return safeNumber(this.dailyUsed) + estimatedTokens <= safeNumber(this.dailyBudget);
5384
5411
  }
5385
5412
  isOverBudget() {
5386
5413
  this.resetIfNewDay();
@@ -5388,7 +5415,7 @@ var TokenBudget = class {
5388
5415
  this.forceNext = false;
5389
5416
  return false;
5390
5417
  }
5391
- return this.dailyUsed >= this.dailyBudget;
5418
+ return safeNumber(this.dailyUsed) >= safeNumber(this.dailyBudget);
5392
5419
  }
5393
5420
  forceAllowNext() {
5394
5421
  this.forceNext = true;
@@ -5416,18 +5443,24 @@ var TokenBudget = class {
5416
5443
  }
5417
5444
  recordUsage(entry) {
5418
5445
  this.resetIfNewDay();
5419
- const logEntry = { ...entry, timestamp: Date.now() };
5420
- this.dailyUsed += entry.totalTokens;
5446
+ const inputTokens = safeNumber(entry.inputTokens);
5447
+ const outputTokens = safeNumber(entry.outputTokens);
5448
+ const totalTokens = safeNumber(entry.totalTokens) || inputTokens + outputTokens;
5449
+ const safeEntry = { ...entry, inputTokens, outputTokens, totalTokens };
5450
+ const logEntry = { ...safeEntry, timestamp: Date.now() };
5451
+ this.dailyUsed += totalTokens;
5421
5452
  this.requestLog.push(logEntry);
5422
5453
  this.persist();
5423
5454
  }
5424
5455
  getRemaining() {
5425
5456
  this.resetIfNewDay();
5426
- return Math.max(0, this.dailyBudget - this.dailyUsed);
5457
+ return Math.max(0, safeNumber(this.dailyBudget) - safeNumber(this.dailyUsed));
5427
5458
  }
5428
5459
  getUsagePercentage() {
5429
5460
  this.resetIfNewDay();
5430
- return this.dailyBudget > 0 ? this.dailyUsed / this.dailyBudget * 100 : 0;
5461
+ const budget = safeNumber(this.dailyBudget);
5462
+ const used = safeNumber(this.dailyUsed);
5463
+ return budget > 0 ? used / budget * 100 : 0;
5431
5464
  }
5432
5465
  getStatusText() {
5433
5466
  const pct = Math.round(this.getUsagePercentage());
@@ -5466,8 +5499,16 @@ var TokenBudget = class {
5466
5499
  const data = JSON.parse(raw);
5467
5500
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5468
5501
  if (data.lastResetDate === today) {
5469
- this.dailyUsed = data.dailyUsed ?? 0;
5470
- this.requestLog = data.requestLog ?? [];
5502
+ const restored = safeNumber(data.dailyUsed);
5503
+ if (!isNaN(restored)) {
5504
+ this.dailyUsed = restored;
5505
+ }
5506
+ this.requestLog = (data.requestLog ?? []).map((entry) => ({
5507
+ ...entry,
5508
+ inputTokens: safeNumber(entry.inputTokens),
5509
+ outputTokens: safeNumber(entry.outputTokens),
5510
+ totalTokens: safeNumber(entry.totalTokens)
5511
+ }));
5471
5512
  }
5472
5513
  this.lastResetDate = data.lastResetDate ?? today;
5473
5514
  } catch (err) {
@@ -5592,7 +5633,6 @@ var PermissionManager = class {
5592
5633
  askHandler;
5593
5634
  autoApproveAll = false;
5594
5635
  elevatedCommands = /* @__PURE__ */ new Set();
5595
- pendingApprovals = /* @__PURE__ */ new Set();
5596
5636
  currentChannelType = "cli";
5597
5637
  tempScopes = [];
5598
5638
  constructor() {
@@ -5635,12 +5675,6 @@ var PermissionManager = class {
5635
5675
  isShellElevated() {
5636
5676
  return this.elevatedCommands.has("run_command");
5637
5677
  }
5638
- addPendingApproval(baseCommand) {
5639
- this.pendingApprovals.add(baseCommand);
5640
- }
5641
- clearPendingApprovals() {
5642
- this.pendingApprovals.clear();
5643
- }
5644
5678
  load() {
5645
5679
  if (existsSync7(PERMISSIONS_FILE)) {
5646
5680
  try {
@@ -5699,6 +5733,9 @@ var PermissionManager = class {
5699
5733
  if (mode === "write" && tempScope.write) return { allowed: true };
5700
5734
  return { allowed: false, reason: `Permission denied: ${mode} access to ${path3}` };
5701
5735
  }
5736
+ if (!this.autoApproveAll && this.askHandler && this.currentChannelType !== "internal") {
5737
+ return this.requestScopeExternal(path3, mode);
5738
+ }
5702
5739
  return { allowed: false, reason: `Permission denied for ${mode} access to ${path3}` };
5703
5740
  }
5704
5741
  async checkShellCommand(command) {
@@ -5716,10 +5753,6 @@ var PermissionManager = class {
5716
5753
  }
5717
5754
  const trimmed = command.trim();
5718
5755
  const baseCmd = trimmed.split(/\s+/)[0];
5719
- if (this.pendingApprovals.has(baseCmd)) {
5720
- logger.info({ cmd: trimmed }, "Shell command auto-approved (pending approval)");
5721
- return { allowed: true, needsApproval: false };
5722
- }
5723
5756
  for (const pattern of shell.blocked) {
5724
5757
  if (this.matchPattern(trimmed, pattern)) {
5725
5758
  return { allowed: false, reason: `Blocked command: matches "${pattern}"`, needsApproval: false };
@@ -5742,7 +5775,7 @@ var PermissionManager = class {
5742
5775
  }
5743
5776
  for (const pattern of shell.needsApproval) {
5744
5777
  if (this.matchPattern(trimmed, pattern)) {
5745
- if (this.currentChannelType === "telegram" && this.askHandler) {
5778
+ if (this.askHandler && this.currentChannelType !== "internal") {
5746
5779
  const result = await this.askHandler(`Run command: ${trimmed}`);
5747
5780
  if (result === "yes") {
5748
5781
  return { allowed: true, needsApproval: false };
@@ -5756,7 +5789,7 @@ var PermissionManager = class {
5756
5789
  return { allowed: false, reason: `Command requires approval: matches "${pattern}"`, needsApproval: true };
5757
5790
  }
5758
5791
  }
5759
- if (this.currentChannelType === "telegram" && this.askHandler) {
5792
+ if (this.askHandler && this.currentChannelType !== "internal") {
5760
5793
  const result = await this.askHandler(`Run command: ${trimmed}`);
5761
5794
  if (result === "yes") {
5762
5795
  return { allowed: true, needsApproval: false };
@@ -5862,6 +5895,11 @@ Allow access?`;
5862
5895
  return null;
5863
5896
  }
5864
5897
  mergeDefaults(parsed) {
5898
+ const mergeArray = (existing, defaults) => {
5899
+ if (!existing) return [...defaults];
5900
+ const combined = /* @__PURE__ */ new Set([...defaults, ...existing]);
5901
+ return [...combined];
5902
+ };
5865
5903
  return {
5866
5904
  capabilities: {
5867
5905
  filesystem: {
@@ -5870,9 +5908,9 @@ Allow access?`;
5870
5908
  },
5871
5909
  shell: {
5872
5910
  enabled: parsed.capabilities?.shell?.enabled ?? DEFAULT_MANIFEST.capabilities.shell.enabled,
5873
- blocked: parsed.capabilities?.shell?.blocked ?? DEFAULT_MANIFEST.capabilities.shell.blocked,
5874
- autoApproved: parsed.capabilities?.shell?.autoApproved ?? DEFAULT_MANIFEST.capabilities.shell.autoApproved,
5875
- needsApproval: parsed.capabilities?.shell?.needsApproval ?? DEFAULT_MANIFEST.capabilities.shell.needsApproval,
5911
+ blocked: mergeArray(parsed.capabilities?.shell?.blocked, DEFAULT_MANIFEST.capabilities.shell.blocked),
5912
+ autoApproved: mergeArray(parsed.capabilities?.shell?.autoApproved, DEFAULT_MANIFEST.capabilities.shell.autoApproved),
5913
+ needsApproval: mergeArray(parsed.capabilities?.shell?.needsApproval, DEFAULT_MANIFEST.capabilities.shell.needsApproval),
5876
5914
  cwdOnly: parsed.capabilities?.shell?.cwdOnly ?? DEFAULT_MANIFEST.capabilities.shell.cwdOnly
5877
5915
  },
5878
5916
  git: {
@@ -6222,20 +6260,13 @@ function createRunCommandTool(permissions, getCwd, setCwd) {
6222
6260
  description: `Run a shell command in the current working directory. Use the cd tool to change directories first \u2014 cd commands within this tool only affect chained commands (e.g., "cd /path && ls"), not subsequent calls.
6223
6261
  Blocked commands (sudo, rm -rf /, etc.) are never executed.
6224
6262
  Auto-approved commands (ls, cat, git status, curl, etc.) run without asking.
6225
- Other commands require user approval.`,
6263
+ Other commands prompt the user for approval before execution.`,
6226
6264
  inputSchema: zodSchema10(z10.object({
6227
6265
  command: z10.string().describe("The shell command to execute")
6228
6266
  })),
6229
6267
  execute: async ({ command }) => {
6230
6268
  const check = await permissions.checkShellCommand(command);
6231
6269
  if (!check.allowed) {
6232
- if (check.needsApproval) {
6233
- const baseCmd = command.trim().split(/\s+/)[0];
6234
- permissions.addPendingApproval(baseCmd);
6235
- return `\u26A0 Command requires approval: ${command}
6236
-
6237
- Tell the user what this command does and ask for permission. If they approve, try running it again. If they say "always", use the approve_command tool to permanently approve this command type.`;
6238
- }
6239
6270
  return `Error: ${check.reason}`;
6240
6271
  }
6241
6272
  const cwd = getCwd();
@@ -6711,12 +6742,6 @@ function createGitPushTool(permissions, getCwd) {
6711
6742
  execute: async ({ remote, branch }) => {
6712
6743
  const cmd = `git push ${remote || "origin"} ${branch || ""}`.trim();
6713
6744
  const check = await permissions.checkShellCommand(cmd);
6714
- if (!check.allowed && check.needsApproval) {
6715
- const baseCmd = "git";
6716
- permissions.addPendingApproval(baseCmd);
6717
- return `\u26A0 This command pushes to a remote: ${cmd}
6718
- Ask the user for permission. If they approve, try again. If they say "always", use the approve_command tool.`;
6719
- }
6720
6745
  if (!check.allowed) {
6721
6746
  return `Error: ${check.reason}`;
6722
6747
  }