@foxden-app/foxclaw 0.5.52 → 0.5.54

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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.54 - 2026-06-20
6
+
7
+ ### 中文
8
+ - 修复升级到 Codex CLI `0.141.0` 后 Telegram 看不到工具执行明细的问题;FoxClaw 现在支持现代 app-server 的 `item/started` / `item/completed` 工具生命周期。
9
+ - 命令执行、文件修改、网页搜索、MCP/动态工具、图片查看和生成、协作 Agent 操作会重新实时显示;阶段结束后仍压缩为可展开明细,最终过程汇报继续独立折叠。
10
+ - 保留旧版 `codex/event/exec_command_begin/end` 兼容,避免旧节点或旧 Codex CLI 失去过程状态。
11
+
12
+ ### English
13
+ - Fixed Telegram tool activity details disappearing after upgrading to Codex CLI `0.141.0`; FoxClaw now supports the modern app-server `item/started` and `item/completed` tool lifecycle.
14
+ - Command execution, file changes, web search, MCP/dynamic tools, image activity, and collaborative agent operations are visible in real time again, then archived into expandable stage details.
15
+ - Kept compatibility with legacy `codex/event/exec_command_begin/end` notifications for older Codex CLI installations.
16
+
17
+ ## 0.5.53 - 2026-06-20
18
+
19
+ ### 中文
20
+ - 修复单 bot 默认 runtime 的 `telegram-voice-delivery` 无法找到私聊的问题:当 `CODEX_HOME` 不含 bot ID 时,`send-voice` 现在会从唯一配置的 Telegram token 安全补全 bot ID,再读取已经记录的私聊目标。
21
+ - 将内置 `telegram-voice-delivery` Skill 的触发说明、工作流和 UI 元数据改为中文,并明确只有确实没有私聊记录时才要求用户发送 `/status`。
22
+
23
+ ### English
24
+ - Fixed `telegram-voice-delivery` failing to find the private chat in a single-bot default runtime. When `CODEX_HOME` has no bot ID, `send-voice` now safely derives it from the only configured Telegram token before reading the remembered chat target.
25
+ - Localized the bundled `telegram-voice-delivery` skill instructions and UI metadata into Chinese, and clarified that users should only be asked to send `/status` when no private chat is actually remembered.
26
+
5
27
  ## 0.5.52 - 2026-06-20
6
28
 
7
29
  ### 中文
@@ -52,6 +52,10 @@ function normalizeStartedEvent(params) {
52
52
  state: 'thinking',
53
53
  };
54
54
  }
55
+ const toolEvent = normalizeThreadItemToolEvent(params, 'tool_started');
56
+ if (toolEvent) {
57
+ return toolEvent;
58
+ }
55
59
  if (!isRelayableTextItemType(itemType)) {
56
60
  return null;
57
61
  }
@@ -113,6 +117,10 @@ function normalizeCompletedEvent(params) {
113
117
  state: 'thinking',
114
118
  };
115
119
  }
120
+ const toolEvent = normalizeThreadItemToolEvent(params, 'tool_completed');
121
+ if (toolEvent) {
122
+ return toolEvent;
123
+ }
116
124
  if (!isRelayableTextItemType(itemType)) {
117
125
  return null;
118
126
  }
@@ -161,6 +169,86 @@ function normalizeToolEvent(params, kind) {
161
169
  state: inferToolActivityState(exec),
162
170
  };
163
171
  }
172
+ function normalizeThreadItemToolEvent(params, kind) {
173
+ const turnId = extractTurnId(params);
174
+ const item = params?.item;
175
+ const itemId = extractItemId(item);
176
+ const itemType = normalizeEventItemType(item);
177
+ if (!turnId || !itemId || !itemType) {
178
+ return null;
179
+ }
180
+ let command = [];
181
+ let cwd = null;
182
+ let parsedCmd = [];
183
+ switch (itemType) {
184
+ case 'commandexecution':
185
+ command = typeof item.command === 'string' ? [item.command] : [];
186
+ cwd = typeof item.cwd === 'string' ? item.cwd : null;
187
+ parsedCmd = normalizeCommandActions(item.commandActions);
188
+ break;
189
+ case 'filechange':
190
+ parsedCmd = normalizeFileChanges(item.changes);
191
+ break;
192
+ case 'websearch':
193
+ parsedCmd = [{
194
+ type: 'search',
195
+ query: typeof item.query === 'string' ? item.query : '',
196
+ path: null,
197
+ }];
198
+ break;
199
+ case 'imageview':
200
+ parsedCmd = [{ type: 'read', path: typeof item.path === 'string' ? item.path : null }];
201
+ break;
202
+ case 'mcptoolcall':
203
+ command = [`MCP ${String(item.server ?? 'server')}/${String(item.tool ?? 'tool')}`];
204
+ break;
205
+ case 'dynamictoolcall':
206
+ command = [`Tool ${item.namespace ? `${String(item.namespace)}/` : ''}${String(item.tool ?? 'tool')}`];
207
+ break;
208
+ case 'collabagenttoolcall':
209
+ command = [`Agent ${String(item.tool ?? 'operation')}`];
210
+ break;
211
+ case 'imagegeneration':
212
+ command = ['Image generation'];
213
+ break;
214
+ default:
215
+ return null;
216
+ }
217
+ const exec = {
218
+ callId: itemId,
219
+ turnId,
220
+ command,
221
+ cwd,
222
+ parsedCmd,
223
+ };
224
+ return {
225
+ kind,
226
+ turnId,
227
+ exec,
228
+ state: inferToolActivityState(exec),
229
+ };
230
+ }
231
+ function normalizeCommandActions(value) {
232
+ if (!Array.isArray(value)) {
233
+ return [];
234
+ }
235
+ return value.map((entry) => {
236
+ const type = typeof entry?.type === 'string' ? entry.type : '';
237
+ if (type === 'listFiles') {
238
+ return { ...entry, type: 'list_files' };
239
+ }
240
+ return entry;
241
+ });
242
+ }
243
+ function normalizeFileChanges(value) {
244
+ if (!Array.isArray(value)) {
245
+ return [];
246
+ }
247
+ return value.map((entry) => ({
248
+ type: 'apply_patch',
249
+ path: typeof entry?.path === 'string' ? entry.path : null,
250
+ }));
251
+ }
164
252
  export function classifyAgentOutput(phase, completed) {
165
253
  if (!phase) {
166
254
  return completed ? 'final_answer' : 'commentary';
package/dist/main.js CHANGED
@@ -16,6 +16,7 @@ import { buildFoxclawLaunchdPlistText, extractNodePathFromLaunchdPlist, } from '
16
16
  import { buildFoxclawSystemdUnitText, buildSystemdRestartHelperArgs, cgroupContainsSystemdUnit, refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns, } from './systemd.js';
17
17
  import { clearPendingClusterUpdateBroadcast, createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readPendingClusterUpdateBroadcast, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
18
18
  import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from './voice/files.js';
19
+ import { inferTelegramBotId, resolveTelegramVoiceTarget } from './voice/target.js';
19
20
  const rawCommand = process.argv[2];
20
21
  const command = rawCommand || 'serve';
21
22
  loadEnv();
@@ -297,8 +298,8 @@ async function runSendVoiceCli() {
297
298
  if (stat.size > TELEGRAM_VOICE_MAX_BYTES) {
298
299
  throw new Error('Telegram voice files must be 50MB or smaller.');
299
300
  }
300
- const botId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
301
- const botToken = resolveTelegramBotToken(config.tgBotTokens, botId);
301
+ const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
302
+ const { botId, botToken } = resolveTelegramVoiceTarget(config.tgBotTokens, inferredBotId);
302
303
  const { BridgeStore } = await import('./store/database.js');
303
304
  const store = new BridgeStore(config.storePath);
304
305
  let chatId = parsed.chatId;
@@ -355,24 +356,6 @@ function parseSendVoiceCliArgs(args) {
355
356
  chatId,
356
357
  };
357
358
  }
358
- function inferTelegramBotId(codexHome) {
359
- if (!codexHome)
360
- return null;
361
- const match = codexHome.match(/(?:^|[\\/])(bot\d+)(?:[\\/]|$)/i);
362
- return match?.[1]?.toLowerCase() ?? null;
363
- }
364
- function resolveTelegramBotToken(tokens, botId) {
365
- if (botId) {
366
- const numericId = botId.replace(/^bot/i, '');
367
- const matched = tokens.find(token => token.startsWith(`${numericId}:`));
368
- if (matched)
369
- return matched;
370
- throw new Error(`No configured Telegram token matches ${botId}. Pass --bot-id for a configured bot.`);
371
- }
372
- if (tokens.length === 1)
373
- return tokens[0];
374
- throw new Error('Cannot infer the Telegram bot from this Codex session. Pass --bot-id <bot-id>.');
375
- }
376
359
  function formatRuntimeStatusSummary(status) {
377
360
  const lines = [];
378
361
  const age = formatAge(status.updatedAt);
@@ -0,0 +1,6 @@
1
+ export type TelegramVoiceTarget = {
2
+ botId: string;
3
+ botToken: string;
4
+ };
5
+ export declare function inferTelegramBotId(codexHome: string | null | undefined): string | null;
6
+ export declare function resolveTelegramVoiceTarget(tokens: string[], requestedBotId: string | null): TelegramVoiceTarget;
@@ -0,0 +1,23 @@
1
+ export function inferTelegramBotId(codexHome) {
2
+ if (!codexHome)
3
+ return null;
4
+ const match = codexHome.match(/(?:^|[\\/])(bot\d+)(?:[\\/]|$)/i);
5
+ return match?.[1]?.toLowerCase() ?? null;
6
+ }
7
+ export function resolveTelegramVoiceTarget(tokens, requestedBotId) {
8
+ if (requestedBotId) {
9
+ const numericId = requestedBotId.replace(/^bot/i, '');
10
+ const matched = tokens.find(token => token.startsWith(`${numericId}:`));
11
+ if (matched)
12
+ return { botId: `bot${numericId}`, botToken: matched };
13
+ throw new Error(`No configured Telegram token matches ${requestedBotId}. Pass --bot-id for a configured bot.`);
14
+ }
15
+ if (tokens.length === 1) {
16
+ const botToken = tokens[0];
17
+ const numericId = botToken.slice(0, botToken.indexOf(':'));
18
+ if (/^\d+$/.test(numericId)) {
19
+ return { botId: `bot${numericId}`, botToken };
20
+ }
21
+ }
22
+ throw new Error('Cannot infer the Telegram bot from this Codex session. Pass --bot-id <bot-id>.');
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.52",
3
+ "version": "0.5.54",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -1,25 +1,25 @@
1
1
  ---
2
2
  name: telegram-voice-delivery
3
- description: Deliver an audio file from Codex to the current Telegram conversation through FoxClaw. Use when Codex has generated or found speech, narration, a podcast, or another audio file that the user should receive in Telegram without manually running a FoxClaw command.
3
+ description: 通过 FoxClaw Codex 已生成或找到的语音、解说、播客等音频文件直接发送到当前 Telegram 会话。需要把音频交付给用户且不应要求用户手动执行 FoxClaw 命令时使用。
4
4
  ---
5
5
 
6
- # Telegram Voice Delivery
6
+ # Telegram 语音投递
7
7
 
8
- Send completed audio artifacts back to the active FoxClaw Telegram conversation without asking the user to operate `/voice`.
8
+ 把已经完成的音频直接发送到当前 FoxClaw Telegram 私聊,不要让用户操作 `/voice`。
9
9
 
10
- ## Workflow
10
+ ## 操作流程
11
11
 
12
- 1. Finish generating the audio file before attempting delivery.
13
- 2. Prefer `.ogg` or `.opus` for Telegram voice playback. `.oga`, `.mp3`, and `.m4a` are also accepted.
14
- 3. Keep the file at or below 50MB.
15
- 4. Run:
12
+ 1. 确认音频文件已经完整生成。
13
+ 2. 优先使用适合 Telegram 语音播放的 `.ogg` `.opus`;也支持 `.oga`、`.mp3` `.m4a`。
14
+ 3. 文件不得超过 50MB
15
+ 4. 执行:
16
16
 
17
17
  ```bash
18
- foxclaw send-voice "/absolute/path/to/audio.ogg" "Short caption"
18
+ foxclaw send-voice "/音频的绝对路径/audio.ogg" "简短说明"
19
19
  ```
20
20
 
21
- 5. Treat a successful command as delivery confirmation and tell the user the audio was sent.
21
+ 5. 命令成功即表示 Telegram 已接收该语音;随后简短告知用户已经发送。
22
22
 
23
- FoxClaw infers the current Telegram bot from `CODEX_HOME`, reads that bot's remembered private chat from its local store, and calls Telegram `sendVoice`. Do not read, print, or expose Telegram bot tokens.
23
+ FoxClaw 会从 `CODEX_HOME` 推断当前 Telegram bot;单 bot 默认 runtime 会从唯一配置的 token 补全 bot ID。随后从本地数据库读取该 bot 最近记录的私聊,并调用 Telegram `sendVoice`。不要读取、打印或泄露 Telegram bot token。
24
24
 
25
- If an audio file already exists, send it directly instead of regenerating it. If FoxClaw reports that no private chat is remembered, ask the user to send `/status` to that bot once; use `--bot-id <bot-id>` or `--chat-id <chat-id>` only when automatic session inference is unavailable.
25
+ 音频已经存在时直接发送,不要重新生成。只有 FoxClaw 明确报告没有记录私聊时,才请用户对当前 bot 发送一次 `/status`;仅在自动会话推断确实不可用时使用 `--bot-id <bot-id>` `--chat-id <chat-id>`,不要凭猜测指定其他 bot。
@@ -1,4 +1,4 @@
1
1
  interface:
2
- display_name: "Telegram Voice Delivery"
3
- short_description: "Send generated audio back through FoxClaw"
4
- default_prompt: "Use $telegram-voice-delivery to send a completed audio file to the current Telegram conversation through FoxClaw."
2
+ display_name: "Telegram 语音投递"
3
+ short_description: " Codex 已生成的音频直接发送到当前 Telegram 会话"
4
+ default_prompt: "使用 $telegram-voice-delivery,把已完成的音频文件发送到当前 Telegram 会话。"