@phuetz/code-buddy 1.0.0 → 1.2.0

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.
Files changed (312) hide show
  1. package/README.md +160 -164
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +101 -13
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/lesson-auto-proposer.js +10 -0
  20. package/dist/agent/middleware/index.d.ts +1 -0
  21. package/dist/agent/middleware/index.js +1 -0
  22. package/dist/agent/middleware/session-duration.d.ts +36 -0
  23. package/dist/agent/middleware/session-duration.js +78 -0
  24. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  25. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  26. package/dist/agent/model-benchmark.d.ts +77 -0
  27. package/dist/agent/model-benchmark.js +309 -0
  28. package/dist/agent/model-tier.d.ts +14 -0
  29. package/dist/agent/model-tier.js +71 -0
  30. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  31. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  32. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  33. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  34. package/dist/agent/multi-agent/session-tools.js +22 -5
  35. package/dist/agent/reasoning/mcts.js +20 -6
  36. package/dist/agent/session-end-flush.d.ts +71 -0
  37. package/dist/agent/session-end-flush.js +233 -0
  38. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  39. package/dist/agent/streaming/streaming-handler.js +8 -1
  40. package/dist/agent/tool-executor.js +1 -0
  41. package/dist/agent/tool-handler.d.ts +5 -0
  42. package/dist/agent/tool-handler.js +130 -4
  43. package/dist/browser-automation/browser-operator-executor.js +15 -0
  44. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  45. package/dist/browser-automation/browser-use-runner.js +492 -0
  46. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  47. package/dist/browser-automation/camofox-runner.js +287 -0
  48. package/dist/channels/dingtalk/index.js +3 -0
  49. package/dist/channels/discord/client.d.ts +9 -0
  50. package/dist/channels/discord/client.js +12 -0
  51. package/dist/channels/feishu/index.d.ts +167 -1
  52. package/dist/channels/feishu/index.js +432 -7
  53. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  54. package/dist/channels/gateway-lifecycle.js +219 -0
  55. package/dist/channels/google-chat/index.d.ts +5 -1
  56. package/dist/channels/google-chat/index.js +5 -1
  57. package/dist/channels/imessage/index.d.ts +14 -0
  58. package/dist/channels/imessage/index.js +64 -17
  59. package/dist/channels/index.d.ts +4 -0
  60. package/dist/channels/index.js +4 -0
  61. package/dist/channels/irc/index.d.ts +73 -3
  62. package/dist/channels/irc/index.js +446 -11
  63. package/dist/channels/line/index.js +3 -0
  64. package/dist/channels/mattermost/index.d.ts +51 -4
  65. package/dist/channels/mattermost/index.js +303 -20
  66. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  67. package/dist/channels/nextcloud-talk/index.js +367 -16
  68. package/dist/channels/nostr/index.d.ts +121 -1
  69. package/dist/channels/nostr/index.js +396 -8
  70. package/dist/channels/ntfy/index.js +3 -0
  71. package/dist/channels/qq/index.js +3 -0
  72. package/dist/channels/slash-parity.d.ts +117 -0
  73. package/dist/channels/slash-parity.js +185 -0
  74. package/dist/channels/synology-chat/index.js +3 -0
  75. package/dist/channels/teams/index.d.ts +3 -0
  76. package/dist/channels/teams/index.js +3 -0
  77. package/dist/channels/telegram/client.d.ts +9 -0
  78. package/dist/channels/telegram/client.js +12 -0
  79. package/dist/channels/twilio-voice/index.js +3 -0
  80. package/dist/channels/webchat/index.d.ts +7 -1
  81. package/dist/channels/webchat/index.js +7 -1
  82. package/dist/channels/wecom/index.js +3 -0
  83. package/dist/channels/weixin/index.js +3 -0
  84. package/dist/channels/zalo/index.js +3 -0
  85. package/dist/cli/config-loader.js +1 -1
  86. package/dist/codebuddy/client.d.ts +30 -1
  87. package/dist/codebuddy/client.js +167 -20
  88. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  90. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  91. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  92. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  93. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  94. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  95. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  96. package/dist/codebuddy/tool-definitions/index.js +2 -1
  97. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  99. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  100. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  101. package/dist/codebuddy/tools.d.ts +1 -1
  102. package/dist/codebuddy/tools.js +27 -24
  103. package/dist/commands/cli/hermes-commands.js +108 -0
  104. package/dist/commands/cli/native-engine-commands.js +107 -3
  105. package/dist/commands/cli/utility-commands.js +45 -0
  106. package/dist/commands/enhanced-command-handler.js +5 -0
  107. package/dist/commands/goal-cli.d.ts +71 -0
  108. package/dist/commands/goal-cli.js +280 -0
  109. package/dist/commands/handlers/goal-handler.d.ts +34 -0
  110. package/dist/commands/handlers/goal-handler.js +155 -0
  111. package/dist/commands/handlers/index.d.ts +1 -0
  112. package/dist/commands/handlers/index.js +2 -0
  113. package/dist/commands/handlers/infra-handlers.js +36 -0
  114. package/dist/commands/handlers/memory-handlers.js +96 -1
  115. package/dist/commands/headless-slash.d.ts +2 -0
  116. package/dist/commands/headless-slash.js +12 -0
  117. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  118. package/dist/commands/llm-provider-resolution.js +87 -33
  119. package/dist/commands/ollama.d.ts +25 -0
  120. package/dist/commands/ollama.js +100 -0
  121. package/dist/commands/provider.d.ts +5 -0
  122. package/dist/commands/provider.js +106 -55
  123. package/dist/commands/slash/builtin-commands.js +20 -0
  124. package/dist/commands/spec-next.js +2 -1
  125. package/dist/commands/spec-plan.js +5 -16
  126. package/dist/commands/tunnel.d.ts +2 -0
  127. package/dist/commands/tunnel.js +48 -0
  128. package/dist/config/config-resolver.d.ts +2 -1
  129. package/dist/config/config-resolver.js +54 -41
  130. package/dist/config/constants.d.ts +28 -0
  131. package/dist/config/constants.js +7 -0
  132. package/dist/config/env-schema.js +770 -0
  133. package/dist/config/feature-flags.js +7 -0
  134. package/dist/config/model-tools.js +4 -4
  135. package/dist/config/toml-config.d.ts +16 -0
  136. package/dist/config/toml-config.js +3 -0
  137. package/dist/context/context-manager-v2.d.ts +39 -0
  138. package/dist/context/context-manager-v2.js +91 -0
  139. package/dist/daemon/agent-task-executor.js +12 -1
  140. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  141. package/dist/daemon/autonomous-daemon.js +5 -3
  142. package/dist/daemon/autonomous-loop.d.ts +21 -1
  143. package/dist/daemon/autonomous-loop.js +69 -0
  144. package/dist/daemon/colab-goal.d.ts +38 -0
  145. package/dist/daemon/colab-goal.js +81 -0
  146. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  147. package/dist/daemon/cron-agent-bridge.js +25 -9
  148. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  149. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  150. package/dist/desktop/engine-adapter.d.ts +14 -0
  151. package/dist/desktop-automation/automation-manager.js +16 -0
  152. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  153. package/dist/desktop-automation/omniparser-runner.js +115 -0
  154. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  155. package/dist/desktop-automation/smart-snapshot.js +72 -5
  156. package/dist/doctor/index.js +40 -23
  157. package/dist/events/types.d.ts +11 -0
  158. package/dist/fleet/colab-store.d.ts +21 -0
  159. package/dist/fleet/colab-store.js +28 -0
  160. package/dist/fleet/peer-session-bridge.d.ts +1 -1
  161. package/dist/fleet/peer-session-bridge.js +243 -2
  162. package/dist/fleet/peer-session-store.d.ts +3 -0
  163. package/dist/fleet/peer-tool-bridge.js +14 -0
  164. package/dist/fleet/privacy-lint.d.ts +8 -0
  165. package/dist/fleet/privacy-lint.js +22 -0
  166. package/dist/goals/goal-decomposer.d.ts +27 -0
  167. package/dist/goals/goal-decomposer.js +293 -0
  168. package/dist/goals/goal-judge-client.d.ts +15 -0
  169. package/dist/goals/goal-judge-client.js +45 -0
  170. package/dist/goals/goal-judge.d.ts +38 -0
  171. package/dist/goals/goal-judge.js +158 -0
  172. package/dist/goals/goal-loop.d.ts +37 -0
  173. package/dist/goals/goal-loop.js +101 -0
  174. package/dist/goals/goal-manager.d.ts +77 -0
  175. package/dist/goals/goal-manager.js +289 -0
  176. package/dist/goals/goal-state.d.ts +99 -0
  177. package/dist/goals/goal-state.js +315 -0
  178. package/dist/goals/goal-store.d.ts +28 -0
  179. package/dist/goals/goal-store.js +87 -0
  180. package/dist/goals/index.d.ts +5 -0
  181. package/dist/goals/index.js +6 -0
  182. package/dist/hooks/use-input-handler.js +36 -1
  183. package/dist/hooks/user-hooks.js +17 -3
  184. package/dist/index.js +506 -25
  185. package/dist/input/text-to-speech.d.ts +2 -6
  186. package/dist/input/text-to-speech.js +2 -27
  187. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  188. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  189. package/dist/integrations/tailscale.d.ts +13 -0
  190. package/dist/integrations/tailscale.js +87 -34
  191. package/dist/knowledge/workspace-indexer.js +53 -9
  192. package/dist/mcp/client.js +1 -0
  193. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  194. package/dist/mcp/mcp-desktop-tools.js +158 -0
  195. package/dist/mcp/mcp-server.js +2 -0
  196. package/dist/memory/enhanced-memory.d.ts +10 -0
  197. package/dist/memory/enhanced-memory.js +33 -2
  198. package/dist/memory/index.d.ts +3 -1
  199. package/dist/memory/index.js +4 -1
  200. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  201. package/dist/memory/memory-auto-proposer.js +308 -0
  202. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  203. package/dist/memory/memory-candidate-queue.js +261 -0
  204. package/dist/memory/persistent-memory.d.ts +45 -2
  205. package/dist/memory/persistent-memory.js +236 -40
  206. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  207. package/dist/ml/bayesian-qualifier.js +35 -2
  208. package/dist/observability/run-store.d.ts +1 -1
  209. package/dist/openclaw/gateway-bridge.js +5 -0
  210. package/dist/plugins/marketplace.d.ts +1 -0
  211. package/dist/plugins/marketplace.js +7 -0
  212. package/dist/prompts/execution-discipline.d.ts +14 -0
  213. package/dist/prompts/execution-discipline.js +29 -0
  214. package/dist/prompts/variation-injector.js +9 -2
  215. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  216. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  217. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  218. package/dist/protocols/acp/acp-session-store.js +77 -0
  219. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  220. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  221. package/dist/providers/active-llm-registry.d.ts +37 -0
  222. package/dist/providers/active-llm-registry.js +186 -0
  223. package/dist/providers/auxiliary-provider.d.ts +25 -0
  224. package/dist/providers/auxiliary-provider.js +192 -0
  225. package/dist/providers/codex-oauth.d.ts +1 -1
  226. package/dist/providers/codex-oauth.js +27 -4
  227. package/dist/providers/index.d.ts +3 -0
  228. package/dist/providers/index.js +4 -0
  229. package/dist/providers/provider-catalog.d.ts +70 -0
  230. package/dist/providers/provider-catalog.js +738 -0
  231. package/dist/providers/provider-fallback.d.ts +35 -0
  232. package/dist/providers/provider-fallback.js +171 -0
  233. package/dist/providers/xai-oauth.d.ts +128 -0
  234. package/dist/providers/xai-oauth.js +735 -0
  235. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  236. package/dist/scheduler/cron-scheduler.js +53 -7
  237. package/dist/scheduler/script-runner.d.ts +2 -0
  238. package/dist/scheduler/script-runner.js +19 -4
  239. package/dist/scheduler/watchdog-handlers.js +7 -20
  240. package/dist/search/usearch-index.js +7 -2
  241. package/dist/security/tool-policy/tool-groups.js +2 -0
  242. package/dist/server/index.js +17 -2
  243. package/dist/server/routes/mobile.d.ts +12 -4
  244. package/dist/server/routes/mobile.js +116 -1
  245. package/dist/server/tls-config.d.ts +35 -0
  246. package/dist/server/tls-config.js +142 -0
  247. package/dist/server/tunnel-manager.d.ts +20 -0
  248. package/dist/server/tunnel-manager.js +58 -0
  249. package/dist/server/websocket/fleet-bridge.d.ts +13 -1
  250. package/dist/server/websocket/fleet-bridge.js +16 -0
  251. package/dist/services/prompt-builder.d.ts +1 -0
  252. package/dist/services/prompt-builder.js +49 -8
  253. package/dist/shared/engine-types.d.ts +15 -1
  254. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  255. package/dist/sidecar/sidecar-bridge.js +21 -10
  256. package/dist/spec/spec-store.js +8 -1
  257. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  258. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  259. package/dist/templates/project-scaffolding.js +3 -2
  260. package/dist/themes/theme-schema.d.ts +10 -10
  261. package/dist/tools/application-profiles.js +38 -0
  262. package/dist/tools/bash/bash-tool.d.ts +1 -0
  263. package/dist/tools/bash/bash-tool.js +16 -2
  264. package/dist/tools/bash/command-validator.js +3 -0
  265. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  266. package/dist/tools/bash/rtk-rewrite.js +100 -0
  267. package/dist/tools/bash/streaming-executor.js +14 -1
  268. package/dist/tools/computer-control-tool.d.ts +16 -1
  269. package/dist/tools/computer-control-tool.js +317 -5
  270. package/dist/tools/document-generator.d.ts +14 -0
  271. package/dist/tools/document-generator.js +79 -0
  272. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  273. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  274. package/dist/tools/metadata.js +28 -0
  275. package/dist/tools/office-macro-tool.d.ts +10 -0
  276. package/dist/tools/office-macro-tool.js +93 -0
  277. package/dist/tools/registry/index.d.ts +2 -1
  278. package/dist/tools/registry/index.js +2 -1
  279. package/dist/tools/registry/memory-tools.d.ts +20 -0
  280. package/dist/tools/registry/memory-tools.js +225 -3
  281. package/dist/tools/registry/multimodal-tools.js +2 -2
  282. package/dist/tools/registry/vision-tools.d.ts +33 -0
  283. package/dist/tools/registry/vision-tools.js +308 -6
  284. package/dist/tools/registry/windows-tools.d.ts +2 -0
  285. package/dist/tools/registry/windows-tools.js +47 -0
  286. package/dist/tools/route-peer-tool.js +14 -0
  287. package/dist/tools/screenshot-tool.js +14 -2
  288. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  289. package/dist/tools/text-to-speech-tool.js +2 -12
  290. package/dist/tools/tool-selector.js +3 -59
  291. package/dist/utils/config-validation/schema.d.ts +40 -7
  292. package/dist/utils/config-validation/schema.js +130 -1
  293. package/dist/utils/cost-tracker.js +21 -1
  294. package/dist/utils/disk-guard.d.ts +180 -0
  295. package/dist/utils/disk-guard.js +404 -0
  296. package/dist/utils/interactive-setup.js +35 -2
  297. package/dist/utils/model-utils.js +11 -2
  298. package/dist/utils/output-sanitizer.js +11 -0
  299. package/dist/utils/provider-detector.d.ts +8 -7
  300. package/dist/utils/provider-detector.js +19 -77
  301. package/dist/utils/settings-manager.d.ts +7 -0
  302. package/dist/utils/token-display.d.ts +3 -3
  303. package/dist/utils/token-display.js +22 -4
  304. package/dist/wizard/onboarding.d.ts +39 -0
  305. package/dist/wizard/onboarding.js +433 -21
  306. package/dist/wizard/provider-onboarding.d.ts +8 -23
  307. package/dist/wizard/provider-onboarding.js +39 -158
  308. package/package.json +4 -2
  309. package/dist/plugins/provider-onboarding.d.ts +0 -23
  310. package/dist/plugins/provider-onboarding.js +0 -116
  311. package/dist/utils/kokoro-tts.d.ts +0 -8
  312. package/dist/utils/kokoro-tts.js +0 -67
@@ -18,6 +18,7 @@ import { listApplicationProfiles, resolveApplicationProfile, } from './applicati
18
18
  import { getActiveRunStore } from '../observability/run-store.js';
19
19
  import { logger } from '../utils/logger.js';
20
20
  import { getDesktopAutomation, getPermissionManager, getSystemControl, getSmartSnapshotManager, getScreenRecorder, } from '../desktop-automation/index.js';
21
+ import { OmniParserRunner } from "../desktop-automation/omniparser-runner.js";
21
22
  const execFileAsync = promisify(execFile);
22
23
  let visionGroundingProvider = null;
23
24
  export function setVisionGroundingProvider(p) {
@@ -55,6 +56,7 @@ export class ComputerControlTool {
55
56
  systemControl = getSystemControl();
56
57
  snapshotManager = getSmartSnapshotManager();
57
58
  screenRecorder = getScreenRecorder();
59
+ omniParser = new OmniParserRunner();
58
60
  lastTargetFocusProof = null;
59
61
  lastVisualContextProof = null;
60
62
  /**
@@ -161,6 +163,20 @@ export class ComputerControlTool {
161
163
  return run(() => this.excelGetCell(enrichedInput));
162
164
  case 'excel_save_workbook':
163
165
  return run(() => this.excelSaveWorkbook(enrichedInput));
166
+ case 'powerpoint_open_presentation':
167
+ return run(() => this.powerpointOpenPresentation(enrichedInput));
168
+ case 'powerpoint_add_slide':
169
+ return run(() => this.powerpointAddSlide(enrichedInput));
170
+ case 'powerpoint_set_text':
171
+ return run(() => this.powerpointSetText(enrichedInput));
172
+ case 'powerpoint_save_presentation':
173
+ return run(() => this.powerpointSavePresentation(enrichedInput));
174
+ case 'word_open_document':
175
+ return run(() => this.wordOpenDocument(enrichedInput));
176
+ case 'word_type_text':
177
+ return run(() => this.wordTypeText(enrichedInput));
178
+ case 'word_save_document':
179
+ return run(() => this.wordSaveDocument(enrichedInput));
164
180
  case 'use_app_workflow':
165
181
  case 'macro':
166
182
  return run(() => this.executeMacro(enrichedInput));
@@ -417,7 +433,7 @@ export class ComputerControlTool {
417
433
  const snapshot = await this.snapshotManager.takeSnapshot({
418
434
  interactiveOnly: input.interactiveOnly ?? true,
419
435
  });
420
- const textRepresentation = this.snapshotManager.toTextRepresentation(snapshot);
436
+ let textRepresentation = this.snapshotManager.toTextRepresentation(snapshot);
421
437
  // Capture and normalize screenshot for LLM
422
438
  let screenshotData = {};
423
439
  try {
@@ -447,6 +463,26 @@ export class ComputerControlTool {
447
463
  catch (err) {
448
464
  logger.debug('Screenshot capture failed in snapshot_with_screenshot', { error: err });
449
465
  }
466
+ // Apply OmniParser if requested and screenshot was successful
467
+ if (input.useOmniParser && screenshotData.base64) {
468
+ const omniResult = await this.omniParser.parseScreen(screenshotData.base64, {
469
+ width: screenshotData.width,
470
+ height: screenshotData.height,
471
+ });
472
+ if (omniResult.elements.length > 0) {
473
+ // Override screenshot with the annotated (Set-of-Marks) image whose numbers match the ids below.
474
+ screenshotData.base64 = omniResult.annotatedImageBase64;
475
+ const unit = omniResult.elements[0]?.normalized ? 'ratio 0-1' : 'px';
476
+ const parsedText = omniResult.elements
477
+ .map((e) => `[${e.id}] ${e.type}${e.interactable ? ' (interactive)' : ''} "${e.content}" center=(${e.center[0]},${e.center[1]})`)
478
+ .join('\n');
479
+ textRepresentation += `\n\n[OmniParser Elements] (coords in ${unit}; use 'click' at an element center to act):\n${parsedText}`;
480
+ }
481
+ else {
482
+ // No elements => server down/misconfigured or empty parse. Keep the original snapshot, don't add noise.
483
+ logger.debug('OmniParser returned no elements; keeping original screenshot and snapshot text');
484
+ }
485
+ }
450
486
  return {
451
487
  success: true,
452
488
  output: textRepresentation,
@@ -1512,7 +1548,7 @@ if ($clickButtonName) {
1512
1548
  withDerivedAppWindowTarget(input, profile) {
1513
1549
  const next = { ...input };
1514
1550
  if (profile.id === 'notepad' && input.filePath && !next.windowTitle && !next.windowTitleRegex) {
1515
- next.windowTitle = path.basename(input.filePath);
1551
+ next.windowTitle = this.basenameForWindowTitle(input.filePath);
1516
1552
  next.windowTitleMatch = 'contains';
1517
1553
  }
1518
1554
  return next;
@@ -1603,11 +1639,14 @@ if ($clickButtonName) {
1603
1639
  }
1604
1640
  }
1605
1641
  if (profile?.id === 'notepad' && input.filePath && !next.windowTitle && !next.windowTitleRegex) {
1606
- next.windowTitle = path.basename(input.filePath);
1642
+ next.windowTitle = this.basenameForWindowTitle(input.filePath);
1607
1643
  next.windowTitleMatch = 'contains';
1608
1644
  }
1609
1645
  return next;
1610
1646
  }
1647
+ basenameForWindowTitle(filePath) {
1648
+ return filePath.includes('\\') ? path.win32.basename(filePath) : path.basename(filePath);
1649
+ }
1611
1650
  async excelOpenWorkbook(input) {
1612
1651
  const result = await this.runExcelAutomation({
1613
1652
  operation: 'open',
@@ -1666,6 +1705,97 @@ if ($clickButtonName) {
1666
1705
  data: result,
1667
1706
  };
1668
1707
  }
1708
+ async powerpointOpenPresentation(input) {
1709
+ const result = await this.runPowerpointAutomation({
1710
+ operation: 'open',
1711
+ filePath: input.filePath,
1712
+ });
1713
+ return {
1714
+ success: true,
1715
+ output: `PowerPoint presentation ready: ${result.presentationName ?? result.filePath ?? 'new presentation'}`,
1716
+ data: result,
1717
+ };
1718
+ }
1719
+ async powerpointAddSlide(input) {
1720
+ const result = await this.runPowerpointAutomation({
1721
+ operation: 'addSlide',
1722
+ filePath: input.filePath,
1723
+ layoutIndex: input.layoutIndex,
1724
+ });
1725
+ return {
1726
+ success: true,
1727
+ output: `PowerPoint slide added (index ${result.slideIndex})`,
1728
+ data: result,
1729
+ };
1730
+ }
1731
+ async powerpointSetText(input) {
1732
+ if (input.slideIndex === undefined || input.shapeIndex === undefined) {
1733
+ return { success: false, error: 'slideIndex and shapeIndex are required' };
1734
+ }
1735
+ const result = await this.runPowerpointAutomation({
1736
+ operation: 'setText',
1737
+ filePath: input.filePath,
1738
+ slideIndex: input.slideIndex,
1739
+ shapeIndex: input.shapeIndex,
1740
+ value: input.value ?? input.text ?? '',
1741
+ });
1742
+ return {
1743
+ success: true,
1744
+ output: `PowerPoint slide ${input.slideIndex} shape ${input.shapeIndex} text set.`,
1745
+ data: result,
1746
+ };
1747
+ }
1748
+ async powerpointSavePresentation(input) {
1749
+ const result = await this.runPowerpointAutomation({
1750
+ operation: 'save',
1751
+ filePath: input.filePath,
1752
+ saveAsPath: input.saveAsPath,
1753
+ });
1754
+ return {
1755
+ success: true,
1756
+ output: `PowerPoint presentation saved${result.filePath ? `: ${result.filePath}` : ''}`,
1757
+ data: result,
1758
+ };
1759
+ }
1760
+ async wordOpenDocument(input) {
1761
+ const result = await this.runWordAutomation({
1762
+ operation: 'open',
1763
+ filePath: input.filePath,
1764
+ });
1765
+ return {
1766
+ success: true,
1767
+ output: `Word document ready: ${result.documentName ?? result.filePath ?? 'new document'}`,
1768
+ data: result,
1769
+ };
1770
+ }
1771
+ async wordTypeText(input) {
1772
+ const value = input.value ?? input.text;
1773
+ if (value === undefined || value === null) {
1774
+ return { success: false, error: 'value (or text) is required for word_type_text' };
1775
+ }
1776
+ const result = await this.runWordAutomation({
1777
+ operation: 'typeText',
1778
+ filePath: input.filePath,
1779
+ value,
1780
+ });
1781
+ return {
1782
+ success: true,
1783
+ output: `Word document text appended.`,
1784
+ data: result,
1785
+ };
1786
+ }
1787
+ async wordSaveDocument(input) {
1788
+ const result = await this.runWordAutomation({
1789
+ operation: 'save',
1790
+ filePath: input.filePath,
1791
+ saveAsPath: input.saveAsPath,
1792
+ });
1793
+ return {
1794
+ success: true,
1795
+ output: `Word document saved${result.filePath ? `: ${result.filePath}` : ''}`,
1796
+ data: result,
1797
+ };
1798
+ }
1669
1799
  // ============================================================================
1670
1800
  // Mouse Actions
1671
1801
  // ============================================================================
@@ -2697,6 +2827,186 @@ switch ($operation) {
2697
2827
  value = $value
2698
2828
  visible = $excel.Visible
2699
2829
  } | ConvertTo-Json -Compress
2830
+ `;
2831
+ const stdout = await this.runPowerShellEncoded(script, 30000);
2832
+ return JSON.parse(stdout || '{}');
2833
+ }
2834
+ async runPowerpointAutomation(payload) {
2835
+ if (process.platform !== 'win32') {
2836
+ throw new Error('PowerPoint application profile currently requires Windows COM automation.');
2837
+ }
2838
+ const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
2839
+ const script = `
2840
+ $ErrorActionPreference = 'Stop'
2841
+ $payload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPayload}')) | ConvertFrom-Json
2842
+
2843
+ function Get-PowerPointApplication {
2844
+ try {
2845
+ return [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
2846
+ } catch {
2847
+ return New-Object -ComObject PowerPoint.Application
2848
+ }
2849
+ }
2850
+
2851
+ function Resolve-Presentation($ppt, $filePath) {
2852
+ if ($filePath) {
2853
+ $fullPath = [System.IO.Path]::GetFullPath([string]$filePath)
2854
+ foreach ($pres in @($ppt.Presentations)) {
2855
+ try {
2856
+ if ($pres.FullName -eq $fullPath) { return $pres }
2857
+ } catch {}
2858
+ }
2859
+ if (Test-Path -LiteralPath $fullPath) {
2860
+ return $ppt.Presentations.Open($fullPath)
2861
+ }
2862
+ $pres = $ppt.Presentations.Add()
2863
+ $pres.SaveAs($fullPath)
2864
+ return $pres
2865
+ }
2866
+
2867
+ if ($ppt.Presentations.Count -gt 0) {
2868
+ return $ppt.ActivePresentation
2869
+ }
2870
+ return $ppt.Presentations.Add()
2871
+ }
2872
+
2873
+ $ppt = Get-PowerPointApplication
2874
+ $ppt.Visible = 1
2875
+ $presentation = Resolve-Presentation $ppt $payload.filePath
2876
+ $operation = [string]$payload.operation
2877
+ $slideIndex = $payload.slideIndex
2878
+ $shapeIndex = $payload.shapeIndex
2879
+ $value = $null
2880
+
2881
+ switch ($operation) {
2882
+ 'open' {
2883
+ $value = $null
2884
+ }
2885
+ 'addSlide' {
2886
+ $layoutIndex = 1
2887
+ if ($null -ne $payload.layoutIndex) { $layoutIndex = $payload.layoutIndex }
2888
+ # ppLayoutText = 2, ppLayoutTitle = 1
2889
+ $slideCount = $presentation.Slides.Count
2890
+ $slide = $presentation.Slides.Add($slideCount + 1, $layoutIndex)
2891
+ $slideIndex = $slide.SlideIndex
2892
+ }
2893
+ 'setText' {
2894
+ if ($null -eq $slideIndex -or $null -eq $shapeIndex) { throw 'slideIndex and shapeIndex are required' }
2895
+ $slide = $presentation.Slides.Item([int]$slideIndex)
2896
+ $shape = $slide.Shapes.Item([int]$shapeIndex)
2897
+ if ($shape.HasTextFrame) {
2898
+ $shape.TextFrame.TextRange.Text = $payload.value
2899
+ } else {
2900
+ throw 'Shape does not have a text frame'
2901
+ }
2902
+ $value = $payload.value
2903
+ }
2904
+ 'save' {
2905
+ if ($payload.saveAsPath) {
2906
+ $savePath = [System.IO.Path]::GetFullPath([string]$payload.saveAsPath)
2907
+ $presentation.SaveAs($savePath)
2908
+ } elseif ($payload.filePath) {
2909
+ $presentation.Save()
2910
+ } elseif ($presentation.Path) {
2911
+ $presentation.Save()
2912
+ } else {
2913
+ throw 'saveAsPath is required for an unsaved presentation.'
2914
+ }
2915
+ $value = $null
2916
+ }
2917
+ default {
2918
+ throw "Unknown PowerPoint operation '$operation'"
2919
+ }
2920
+ }
2921
+
2922
+ [pscustomobject]@{
2923
+ operation = $operation
2924
+ filePath = $presentation.FullName
2925
+ presentationName = $presentation.Name
2926
+ slideIndex = $slideIndex
2927
+ value = $value
2928
+ } | ConvertTo-Json -Compress
2929
+ `;
2930
+ const stdout = await this.runPowerShellEncoded(script, 30000);
2931
+ return JSON.parse(stdout || '{}');
2932
+ }
2933
+ async runWordAutomation(payload) {
2934
+ if (process.platform !== 'win32') {
2935
+ throw new Error('Word application profile currently requires Windows COM automation.');
2936
+ }
2937
+ const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
2938
+ const script = `
2939
+ $ErrorActionPreference = 'Stop'
2940
+ $payload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPayload}')) | ConvertFrom-Json
2941
+
2942
+ function Get-WordApplication {
2943
+ try {
2944
+ return [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
2945
+ } catch {
2946
+ return New-Object -ComObject Word.Application
2947
+ }
2948
+ }
2949
+
2950
+ function Resolve-Document($word, $filePath) {
2951
+ if ($filePath) {
2952
+ $fullPath = [System.IO.Path]::GetFullPath([string]$filePath)
2953
+ foreach ($doc in @($word.Documents)) {
2954
+ try {
2955
+ if ($doc.FullName -eq $fullPath) { return $doc }
2956
+ } catch {}
2957
+ }
2958
+ if (Test-Path -LiteralPath $fullPath) {
2959
+ return $word.Documents.Open($fullPath)
2960
+ }
2961
+ $doc = $word.Documents.Add()
2962
+ $doc.SaveAs([string]$fullPath)
2963
+ return $doc
2964
+ }
2965
+
2966
+ if ($word.Documents.Count -gt 0) {
2967
+ return $word.ActiveDocument
2968
+ }
2969
+ return $word.Documents.Add()
2970
+ }
2971
+
2972
+ $word = Get-WordApplication
2973
+ $word.Visible = 1
2974
+ $document = Resolve-Document $word $payload.filePath
2975
+ $operation = [string]$payload.operation
2976
+ $value = $null
2977
+
2978
+ switch ($operation) {
2979
+ 'open' {
2980
+ $value = $null
2981
+ }
2982
+ 'typeText' {
2983
+ $document.Content.InsertAfter([string]$payload.value)
2984
+ $value = $payload.value
2985
+ }
2986
+ 'save' {
2987
+ if ($payload.saveAsPath) {
2988
+ $savePath = [System.IO.Path]::GetFullPath([string]$payload.saveAsPath)
2989
+ $document.SaveAs([string]$savePath)
2990
+ } elseif ($payload.filePath) {
2991
+ $document.Save()
2992
+ } elseif ($document.Path) {
2993
+ $document.Save()
2994
+ } else {
2995
+ throw 'saveAsPath is required for an unsaved document.'
2996
+ }
2997
+ $value = $null
2998
+ }
2999
+ default {
3000
+ throw "Unknown Word operation '$operation'"
3001
+ }
3002
+ }
3003
+
3004
+ [pscustomobject]@{
3005
+ operation = $operation
3006
+ filePath = $document.FullName
3007
+ documentName = $document.Name
3008
+ value = $value
3009
+ } | ConvertTo-Json -Compress
2700
3010
  `;
2701
3011
  const stdout = await this.runPowerShellEncoded(script, 30000);
2702
3012
  return JSON.parse(stdout || '{}');
@@ -5202,8 +5512,8 @@ $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
5202
5512
  requiresApplicationConfirmation(input) {
5203
5513
  if (input.simulateOnly)
5204
5514
  return false;
5205
- if (input.action.startsWith('excel_')) {
5206
- return ['excel_open_workbook', 'excel_set_cell', 'excel_save_workbook'].includes(input.action);
5515
+ if (input.action.startsWith('excel_') || input.action.startsWith('powerpoint_') || input.action.startsWith('word_')) {
5516
+ return ['excel_open_workbook', 'excel_set_cell', 'excel_save_workbook', 'powerpoint_open_presentation', 'powerpoint_add_slide', 'powerpoint_set_text', 'powerpoint_save_presentation', 'word_open_document', 'word_type_text', 'word_save_document'].includes(input.action);
5207
5517
  }
5208
5518
  if (input.action === 'save_app_document') {
5209
5519
  return true;
@@ -5240,6 +5550,8 @@ $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
5240
5550
  'set_slider_value', 'select_tree_item', 'expand_tree_item', 'collapse_tree_item',
5241
5551
  'click_dialog_button', 'handle_dialog',
5242
5552
  'open_app', 'focus_app', 'save_app_document', 'excel_open_workbook', 'excel_set_cell', 'excel_save_workbook',
5553
+ 'powerpoint_open_presentation', 'powerpoint_add_slide', 'powerpoint_set_text', 'powerpoint_save_presentation',
5554
+ 'word_open_document', 'word_type_text', 'word_save_document',
5243
5555
  'click', 'left_click', 'middle_click', 'double_click', 'right_click', 'move_mouse', 'drag', 'scroll',
5244
5556
  'type', 'key', 'key_down', 'key_up', 'hotkey',
5245
5557
  'focus_window', 'close_window', 'minimize_window', 'maximize_window', 'restore_window',
@@ -52,3 +52,17 @@ export declare function executeGenerateDocument(args: {
52
52
  outputPath: string;
53
53
  theme?: string;
54
54
  }): Promise<ToolResult>;
55
+ /**
56
+ * Streaming variant of {@link executeGenerateDocument}: identical output, but
57
+ * yields the build phases (prepare → parse → build → write → verify → done) as
58
+ * they happen so a host UI (e.g. the Cowork trace panel) can show the steps
59
+ * unfold one by one instead of a single opaque "Created X" at the end. Used by
60
+ * the tool-handler streaming path (`STREAMING_TOOLS`).
61
+ */
62
+ export declare function executeGenerateDocumentStreaming(args: {
63
+ type: string;
64
+ title: string;
65
+ content: string;
66
+ outputPath: string;
67
+ theme?: string;
68
+ }): AsyncGenerator<string, ToolResult, undefined>;
@@ -649,4 +649,83 @@ export async function executeGenerateDocument(args) {
649
649
  },
650
650
  };
651
651
  }
652
+ /**
653
+ * Streaming variant of {@link executeGenerateDocument}: identical output, but
654
+ * yields the build phases (prepare → parse → build → write → verify → done) as
655
+ * they happen so a host UI (e.g. the Cowork trace panel) can show the steps
656
+ * unfold one by one instead of a single opaque "Created X" at the end. Used by
657
+ * the tool-handler streaming path (`STREAMING_TOOLS`).
658
+ */
659
+ export async function* executeGenerateDocumentStreaming(args) {
660
+ const type = args.type;
661
+ const { title, content, outputPath, theme } = args;
662
+ const file = path.basename(outputPath);
663
+ const label = type.toUpperCase();
664
+ try {
665
+ const expectedExt = expectedExtensionForType(type);
666
+ if (path.extname(outputPath).toLowerCase() !== expectedExt) {
667
+ return { success: false, error: `outputPath must end with ${expectedExt} for ${label} documents` };
668
+ }
669
+ yield `📄 Preparing ${label} "${title}"…\n`;
670
+ const outDir = path.dirname(path.resolve(outputPath));
671
+ if (!fs.existsSync(outDir))
672
+ fs.mkdirSync(outDir, { recursive: true });
673
+ yield `📑 Parsing content…\n`;
674
+ const sections = parseMarkdownSections(content);
675
+ yield ` parsed ${sections.length} section(s)\n`;
676
+ const buildLabel = type === 'pptx' ? `Building ${sections.length + 1} slide(s)`
677
+ : type === 'xlsx' ? 'Building spreadsheet'
678
+ : type === 'docx' ? 'Building document'
679
+ : 'Rendering pages';
680
+ yield `🧩 ${buildLabel}…\n`;
681
+ let embeddedImages;
682
+ let rowCount = 0;
683
+ switch (type) {
684
+ case 'pptx':
685
+ await generatePptx(title, sections, outputPath, theme);
686
+ break;
687
+ case 'docx':
688
+ embeddedImages = await generateDocx(title, sections, outputPath);
689
+ break;
690
+ case 'xlsx':
691
+ rowCount = await generateXlsx(title, content, outputPath);
692
+ break;
693
+ case 'pdf':
694
+ await generatePdf(title, sections, outputPath);
695
+ break;
696
+ default: return { success: false, error: `Unknown document type: ${type}` };
697
+ }
698
+ yield `💾 Wrote ${file}\n`;
699
+ yield `✅ Verifying…\n`;
700
+ const docxValidation = type === 'docx' ? await validateDocxPackage(outputPath) : undefined;
701
+ const sizeKb = fs.existsSync(outputPath)
702
+ ? Math.max(1, Math.round(fs.statSync(outputPath).size / 1024))
703
+ : 0;
704
+ logger.info(`Document generated (streaming): ${outputPath} (${type})`);
705
+ yield `✓ Created ${file} (${sizeKb} KB)\n`;
706
+ const embImgs = embeddedImages ?? [];
707
+ const embeddedImageLines = embImgs.length > 0
708
+ ? ['Embedded images:', ...embImgs.map((i) => `- ${i.path}${i.caption ? ` (${i.caption})` : ''} [${i.width}x${i.height}]`)]
709
+ : [];
710
+ const validationLines = docxValidation
711
+ ? [
712
+ 'DOCX validation:',
713
+ `- relationships: ${docxValidation.relationshipCount}`,
714
+ `- embedded image relationships: ${docxValidation.embeddedRelationshipCount}`,
715
+ `- media files: ${docxValidation.mediaFileCount}`,
716
+ ]
717
+ : [];
718
+ const xlsxLine = type === 'xlsx' ? [`Rows: ${rowCount}`] : [];
719
+ return {
720
+ success: true,
721
+ output: [`Created ${label}: ${outputPath}`, ...xlsxLine, ...embeddedImageLines, ...validationLines].join('\n'),
722
+ data: { outputPath, embeddedImages: embImgs, ...(docxValidation ? { docxValidation } : {}) },
723
+ };
724
+ }
725
+ catch (err) {
726
+ const msg = err instanceof Error ? err.message : String(err);
727
+ logger.error('Document generation (streaming) failed', { error: msg });
728
+ return { success: false, error: `Document generation failed: ${msg}` };
729
+ }
730
+ }
652
731
  //# sourceMappingURL=document-generator.js.map
@@ -17,9 +17,17 @@
17
17
  * override via env `CODEBUDDY_EXECUTE_CODE_TOOL_RPC_ALLOWLIST` (csv).
18
18
  * 3. **fleetSafe** — the tool's registry metadata must declare
19
19
  * `fleetSafe: true` (read-only by definition).
20
+ * 4. **Per-tool opt-in** — `CODEBUDDY_EXECUTE_CODE_RPC_TOOLS` (csv)
21
+ * adds specific executor-backed tools to the allowlist. These tools
22
+ * bypass the `fleetSafe` gate since they are explicitly opted in by the
23
+ * operator. Each tool name is validated against both the registry and
24
+ * this module's executor table; unknown or unsupported names are logged
25
+ * and skipped.
20
26
  *
21
- * No write/exec tool is reachable; the executors below physically only
22
- * read. This mirrors the audited `peer-tool-bridge.ts` pattern.
27
+ * No write/exec tool is reachable by default; the default executors
28
+ * below physically only read. Tools added via RPC_TOOLS opt-in are the
29
+ * operator's responsibility. This mirrors the audited
30
+ * `peer-tool-bridge.ts` pattern.
23
31
  */
24
32
  export interface ExecuteCodeRpcInvokeRequest {
25
33
  tool: string;
@@ -35,11 +43,27 @@ export type ExecuteCodeRpcInvoker = (request: ExecuteCodeRpcInvokeRequest) => Pr
35
43
  /** Truthy-flag parse — the single master gate. Read once, in the parent. */
36
44
  export declare function isExecuteCodeToolRpcEnabled(raw?: string | undefined): boolean;
37
45
  export declare function getExecuteCodeToolRpcAllowlist(raw?: string | undefined): Set<string>;
46
+ /**
47
+ * Parse `CODEBUDDY_EXECUTE_CODE_RPC_TOOLS` — a csv of additional tool names
48
+ * to add to the RPC allowlist. Each name is validated against the registry
49
+ * and this module's executor table; unknown or unsupported names are logged
50
+ * as warnings and silently skipped so a typo never opens an unintended tool.
51
+ *
52
+ * Tools listed here bypass the `fleetSafe` gate because the operator
53
+ * explicitly opted them in — requiring `fleetSafe: true` would defeat
54
+ * the purpose of this escape hatch.
55
+ */
56
+ export declare function getExecuteCodeRpcExtraTools(raw?: string | undefined, isRegistered?: (name: string) => boolean, hasExecutor?: (name: string) => boolean): Set<string>;
38
57
  interface InvokerOptions {
39
58
  /** Root the read-only executors are confined to. Defaults to cwd. */
40
59
  workspaceRoot: string;
41
60
  allowlist?: Set<string>;
42
61
  isFleetSafe?: (name: string) => boolean;
62
+ /**
63
+ * Additional tools to allow through RPC, bypassing the fleetSafe gate.
64
+ * Parsed from `CODEBUDDY_EXECUTE_CODE_RPC_TOOLS` by default.
65
+ */
66
+ extraTools?: Set<string>;
43
67
  }
44
68
  /**
45
69
  * Build the policy-gated invoker. The master flag is intentionally NOT
@@ -17,9 +17,17 @@
17
17
  * override via env `CODEBUDDY_EXECUTE_CODE_TOOL_RPC_ALLOWLIST` (csv).
18
18
  * 3. **fleetSafe** — the tool's registry metadata must declare
19
19
  * `fleetSafe: true` (read-only by definition).
20
+ * 4. **Per-tool opt-in** — `CODEBUDDY_EXECUTE_CODE_RPC_TOOLS` (csv)
21
+ * adds specific executor-backed tools to the allowlist. These tools
22
+ * bypass the `fleetSafe` gate since they are explicitly opted in by the
23
+ * operator. Each tool name is validated against both the registry and
24
+ * this module's executor table; unknown or unsupported names are logged
25
+ * and skipped.
20
26
  *
21
- * No write/exec tool is reachable; the executors below physically only
22
- * read. This mirrors the audited `peer-tool-bridge.ts` pattern.
27
+ * No write/exec tool is reachable by default; the default executors
28
+ * below physically only read. Tools added via RPC_TOOLS opt-in are the
29
+ * operator's responsibility. This mirrors the audited
30
+ * `peer-tool-bridge.ts` pattern.
23
31
  */
24
32
  import { spawn } from 'child_process';
25
33
  import { rgPath } from '@vscode/ripgrep';
@@ -28,6 +36,7 @@ import * as path from 'path';
28
36
  import { logger } from '../utils/logger.js';
29
37
  import { getToolRegistry } from './registry.js';
30
38
  const DEFAULT_ALLOWLIST = ['view_file', 'list_directory', 'search'];
39
+ const EXECUTOR_TOOL_NAMES = new Set(DEFAULT_ALLOWLIST);
31
40
  const READ_TRUNCATE_BYTES = 256 * 1024;
32
41
  const LIST_DIRECTORY_MAX_ENTRIES = 256;
33
42
  const SEARCH_TIMEOUT_MS = 30_000;
@@ -45,25 +54,57 @@ export function getExecuteCodeToolRpcAllowlist(raw = process.env.CODEBUDDY_EXECU
45
54
  const items = raw.split(',').map((item) => item.trim()).filter(Boolean);
46
55
  return items.length > 0 ? new Set(items) : new Set(DEFAULT_ALLOWLIST);
47
56
  }
57
+ /**
58
+ * Parse `CODEBUDDY_EXECUTE_CODE_RPC_TOOLS` — a csv of additional tool names
59
+ * to add to the RPC allowlist. Each name is validated against the registry
60
+ * and this module's executor table; unknown or unsupported names are logged
61
+ * as warnings and silently skipped so a typo never opens an unintended tool.
62
+ *
63
+ * Tools listed here bypass the `fleetSafe` gate because the operator
64
+ * explicitly opted them in — requiring `fleetSafe: true` would defeat
65
+ * the purpose of this escape hatch.
66
+ */
67
+ export function getExecuteCodeRpcExtraTools(raw = process.env.CODEBUDDY_EXECUTE_CODE_RPC_TOOLS, isRegistered = (name) => !!getToolRegistry().getTool(name), hasExecutor = (name) => EXECUTOR_TOOL_NAMES.has(name)) {
68
+ if (!raw)
69
+ return new Set();
70
+ const items = raw.split(',').map((item) => item.trim()).filter(Boolean);
71
+ const validated = new Set();
72
+ for (const name of items) {
73
+ if (!isRegistered(name)) {
74
+ logger.warn('[execute-code-rpc] CODEBUDDY_EXECUTE_CODE_RPC_TOOLS: unknown tool, skipping', { tool: name });
75
+ continue;
76
+ }
77
+ if (!hasExecutor(name)) {
78
+ logger.warn('[execute-code-rpc] CODEBUDDY_EXECUTE_CODE_RPC_TOOLS: tool has no execute_code RPC executor, skipping', { tool: name });
79
+ continue;
80
+ }
81
+ validated.add(name);
82
+ }
83
+ return validated;
84
+ }
48
85
  /**
49
86
  * Build the policy-gated invoker. The master flag is intentionally NOT
50
87
  * read here — the runner only constructs an invoker once it has already
51
88
  * decided RPC is enabled, keeping the flag boundary in the parent.
52
89
  */
53
90
  export function createExecuteCodeRpcInvoker(options) {
54
- const allowlist = options.allowlist ?? getExecuteCodeToolRpcAllowlist();
91
+ const baseAllowlist = options.allowlist ?? getExecuteCodeToolRpcAllowlist();
92
+ const extraTools = options.extraTools ?? getExecuteCodeRpcExtraTools();
93
+ const combinedAllowlist = new Set([...baseAllowlist, ...extraTools]);
55
94
  const isFleetSafe = options.isFleetSafe ?? ((name) => getToolRegistry().isFleetSafe(name));
56
95
  const workspaceRoot = path.resolve(options.workspaceRoot);
57
96
  return async (request) => {
58
97
  const tool = request.tool;
59
98
  try {
60
- if (!allowlist.has(tool)) {
99
+ if (!combinedAllowlist.has(tool)) {
61
100
  return {
62
101
  ok: false,
63
102
  error: `TOOL_NOT_ALLOWED_FOR_EXECUTE_CODE_RPC: tool "${tool}" is not in the execute_code RPC allowlist`,
64
103
  };
65
104
  }
66
- if (!isFleetSafe(tool)) {
105
+ // Tools explicitly opted in via CODEBUDDY_EXECUTE_CODE_RPC_TOOLS
106
+ // bypass the fleetSafe gate — the operator accepted responsibility.
107
+ if (!extraTools.has(tool) && !isFleetSafe(tool)) {
67
108
  return {
68
109
  ok: false,
69
110
  error: `TOOL_NOT_FLEET_SAFE: tool "${tool}" lacks fleetSafe metadata`,