ada-agent 0.1.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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/bench/README.md +88 -0
  4. package/bench/swebench.mjs +242 -0
  5. package/bin/ada-server.mjs +6 -0
  6. package/bin/ada.mjs +7 -0
  7. package/docs/agent-loop.svg +66 -0
  8. package/docs/architecture.md +139 -0
  9. package/docs/architecture.svg +73 -0
  10. package/docs/connectors.md +48 -0
  11. package/docs/integrations.md +59 -0
  12. package/docs/login-flow.svg +56 -0
  13. package/docs/orchestration.md +45 -0
  14. package/package.json +64 -0
  15. package/skills/accessibility/SKILL.md +23 -0
  16. package/skills/add-logging/SKILL.md +23 -0
  17. package/skills/add-metrics/SKILL.md +23 -0
  18. package/skills/adr/SKILL.md +24 -0
  19. package/skills/aesthetic-direction/SKILL.md +24 -0
  20. package/skills/agent-loop/SKILL.md +23 -0
  21. package/skills/alerting/SKILL.md +23 -0
  22. package/skills/alpha-compositing/SKILL.md +23 -0
  23. package/skills/android-compose/SKILL.md +23 -0
  24. package/skills/angular-module/SKILL.md +23 -0
  25. package/skills/ansible-playbook/SKILL.md +24 -0
  26. package/skills/api-docs/SKILL.md +24 -0
  27. package/skills/app-store-prep/SKILL.md +23 -0
  28. package/skills/architecture-diagram/SKILL.md +21 -0
  29. package/skills/architecture-doc/SKILL.md +24 -0
  30. package/skills/audit-log/SKILL.md +23 -0
  31. package/skills/authz-review/SKILL.md +23 -0
  32. package/skills/aws-lambda/SKILL.md +24 -0
  33. package/skills/bash-script/SKILL.md +23 -0
  34. package/skills/batch/SKILL.md +23 -0
  35. package/skills/bisect/SKILL.md +23 -0
  36. package/skills/bounding-box/SKILL.md +24 -0
  37. package/skills/branch-cleanup/SKILL.md +23 -0
  38. package/skills/bundle-analyze/SKILL.md +23 -0
  39. package/skills/cache/SKILL.md +23 -0
  40. package/skills/call-graph/SKILL.md +23 -0
  41. package/skills/canvas-debug/SKILL.md +23 -0
  42. package/skills/cdn-setup/SKILL.md +23 -0
  43. package/skills/changelog/SKILL.md +24 -0
  44. package/skills/cherry-pick/SKILL.md +23 -0
  45. package/skills/ci-setup/SKILL.md +23 -0
  46. package/skills/cleanup/SKILL.md +23 -0
  47. package/skills/cli-tool/SKILL.md +23 -0
  48. package/skills/cloudformation/SKILL.md +23 -0
  49. package/skills/code-examples/SKILL.md +24 -0
  50. package/skills/code-review/SKILL.md +23 -0
  51. package/skills/color-palette/SKILL.md +24 -0
  52. package/skills/color-space/SKILL.md +24 -0
  53. package/skills/comment-why/SKILL.md +23 -0
  54. package/skills/commit/SKILL.md +26 -0
  55. package/skills/complexity-audit/SKILL.md +23 -0
  56. package/skills/component/SKILL.md +23 -0
  57. package/skills/component-library/SKILL.md +23 -0
  58. package/skills/connect-github/SKILL.md +20 -0
  59. package/skills/connect-mcp/SKILL.md +21 -0
  60. package/skills/connect-postgres/SKILL.md +20 -0
  61. package/skills/connect-remote/SKILL.md +23 -0
  62. package/skills/connect-slack/SKILL.md +20 -0
  63. package/skills/contract-audit/SKILL.md +25 -0
  64. package/skills/contributing/SKILL.md +23 -0
  65. package/skills/cpp-raii/SKILL.md +23 -0
  66. package/skills/cron-job/SKILL.md +23 -0
  67. package/skills/cv-preprocess/SKILL.md +24 -0
  68. package/skills/dark-mode/SKILL.md +24 -0
  69. package/skills/dashboard/SKILL.md +23 -0
  70. package/skills/dashboard-ui/SKILL.md +23 -0
  71. package/skills/data-export/SKILL.md +23 -0
  72. package/skills/data-validation/SKILL.md +23 -0
  73. package/skills/dataframe/SKILL.md +23 -0
  74. package/skills/db-index/SKILL.md +24 -0
  75. package/skills/dead-code/SKILL.md +23 -0
  76. package/skills/debug/SKILL.md +24 -0
  77. package/skills/deck-review/SKILL.md +24 -0
  78. package/skills/dedupe/SKILL.md +23 -0
  79. package/skills/dedupe-deps/SKILL.md +23 -0
  80. package/skills/dependency-audit/SKILL.md +23 -0
  81. package/skills/dependency-update/SKILL.md +23 -0
  82. package/skills/deploy/SKILL.md +23 -0
  83. package/skills/design-system/SKILL.md +24 -0
  84. package/skills/design-tokens/SKILL.md +24 -0
  85. package/skills/diagram-as-code/SKILL.md +24 -0
  86. package/skills/diff-explain/SKILL.md +23 -0
  87. package/skills/django-view/SKILL.md +23 -0
  88. package/skills/doc-lint/SKILL.md +24 -0
  89. package/skills/docker-compose/SKILL.md +23 -0
  90. package/skills/dockerize/SKILL.md +23 -0
  91. package/skills/docstrings/SKILL.md +23 -0
  92. package/skills/dotfiles/SKILL.md +23 -0
  93. package/skills/dpi-scaling/SKILL.md +23 -0
  94. package/skills/e2e-test/SKILL.md +23 -0
  95. package/skills/embeddings/SKILL.md +23 -0
  96. package/skills/empty-states/SKILL.md +23 -0
  97. package/skills/env-setup/SKILL.md +23 -0
  98. package/skills/erc20/SKILL.md +24 -0
  99. package/skills/error-tracking/SKILL.md +23 -0
  100. package/skills/estimate/SKILL.md +23 -0
  101. package/skills/etl-pipeline/SKILL.md +24 -0
  102. package/skills/eval-harness/SKILL.md +23 -0
  103. package/skills/exif-orientation/SKILL.md +23 -0
  104. package/skills/explain-code/SKILL.md +23 -0
  105. package/skills/express-middleware/SKILL.md +23 -0
  106. package/skills/extract-function/SKILL.md +23 -0
  107. package/skills/faq/SKILL.md +24 -0
  108. package/skills/fastapi-endpoint/SKILL.md +23 -0
  109. package/skills/favicon/SKILL.md +23 -0
  110. package/skills/feature-engineering/SKILL.md +23 -0
  111. package/skills/few-shot/SKILL.md +23 -0
  112. package/skills/find-owner/SKILL.md +23 -0
  113. package/skills/firmware-driver/SKILL.md +23 -0
  114. package/skills/fix-flaky-tests/SKILL.md +23 -0
  115. package/skills/flutter-widget/SKILL.md +23 -0
  116. package/skills/font-rendering/SKILL.md +23 -0
  117. package/skills/form-validation/SKILL.md +23 -0
  118. package/skills/format/SKILL.md +23 -0
  119. package/skills/game-loop/SKILL.md +23 -0
  120. package/skills/gas-optimize/SKILL.md +25 -0
  121. package/skills/gdpr-review/SKILL.md +24 -0
  122. package/skills/github-actions/SKILL.md +23 -0
  123. package/skills/glossary/SKILL.md +24 -0
  124. package/skills/go-idioms/SKILL.md +23 -0
  125. package/skills/gpu-profile/SKILL.md +23 -0
  126. package/skills/graphify/SKILL.md +21 -0
  127. package/skills/graphql-resolver/SKILL.md +23 -0
  128. package/skills/grpc-service/SKILL.md +23 -0
  129. package/skills/guardrails/SKILL.md +23 -0
  130. package/skills/healthcheck/SKILL.md +23 -0
  131. package/skills/heisenbug/SKILL.md +23 -0
  132. package/skills/helm-chart/SKILL.md +24 -0
  133. package/skills/hero-section/SKILL.md +23 -0
  134. package/skills/html-email/SKILL.md +24 -0
  135. package/skills/html-form/SKILL.md +23 -0
  136. package/skills/html-sanitize/SKILL.md +23 -0
  137. package/skills/html-table/SKILL.md +23 -0
  138. package/skills/html-to-pdf/SKILL.md +23 -0
  139. package/skills/http-client/SKILL.md +23 -0
  140. package/skills/i18n/SKILL.md +23 -0
  141. package/skills/i2c-spi/SKILL.md +23 -0
  142. package/skills/image-decode/SKILL.md +24 -0
  143. package/skills/image-memory/SKILL.md +24 -0
  144. package/skills/image-perf/SKILL.md +24 -0
  145. package/skills/image-pipeline/SKILL.md +24 -0
  146. package/skills/image-upload/SKILL.md +24 -0
  147. package/skills/infra-cost/SKILL.md +24 -0
  148. package/skills/input-validation/SKILL.md +23 -0
  149. package/skills/issue-template/SKILL.md +23 -0
  150. package/skills/java-streams/SKILL.md +23 -0
  151. package/skills/k8s-manifest/SKILL.md +23 -0
  152. package/skills/kotlin-coroutines/SKILL.md +23 -0
  153. package/skills/landing-page/SKILL.md +24 -0
  154. package/skills/laravel-controller/SKILL.md +23 -0
  155. package/skills/lazy-load/SKILL.md +23 -0
  156. package/skills/license-check/SKILL.md +23 -0
  157. package/skills/license-header/SKILL.md +23 -0
  158. package/skills/lint-fix/SKILL.md +23 -0
  159. package/skills/llm-cost/SKILL.md +23 -0
  160. package/skills/lockfile-fix/SKILL.md +23 -0
  161. package/skills/low-power/SKILL.md +23 -0
  162. package/skills/makefile/SKILL.md +23 -0
  163. package/skills/man-page/SKILL.md +24 -0
  164. package/skills/mcp-server/SKILL.md +23 -0
  165. package/skills/memory-leak/SKILL.md +23 -0
  166. package/skills/mermaid-diagram/SKILL.md +23 -0
  167. package/skills/meta-tags/SKILL.md +23 -0
  168. package/skills/micro-interactions/SKILL.md +23 -0
  169. package/skills/migration/SKILL.md +23 -0
  170. package/skills/migration-guide/SKILL.md +24 -0
  171. package/skills/mkdocs-setup/SKILL.md +24 -0
  172. package/skills/mobile-permissions/SKILL.md +23 -0
  173. package/skills/mock-api/SKILL.md +23 -0
  174. package/skills/modernize/SKILL.md +23 -0
  175. package/skills/monorepo-setup/SKILL.md +23 -0
  176. package/skills/motion-design/SKILL.md +23 -0
  177. package/skills/n-plus-one/SKILL.md +23 -0
  178. package/skills/naming-review/SKILL.md +23 -0
  179. package/skills/nextjs-route/SKILL.md +23 -0
  180. package/skills/nginx-config/SKILL.md +23 -0
  181. package/skills/ocr-debug/SKILL.md +24 -0
  182. package/skills/onboard/SKILL.md +23 -0
  183. package/skills/onboarding-map/SKILL.md +23 -0
  184. package/skills/open-pr/SKILL.md +24 -0
  185. package/skills/openapi/SKILL.md +23 -0
  186. package/skills/opencv-debug/SKILL.md +24 -0
  187. package/skills/orm-model/SKILL.md +23 -0
  188. package/skills/owasp-check/SKILL.md +24 -0
  189. package/skills/page-transitions/SKILL.md +23 -0
  190. package/skills/pagination/SKILL.md +23 -0
  191. package/skills/perf-optimize/SKILL.md +23 -0
  192. package/skills/perf-profile/SKILL.md +23 -0
  193. package/skills/physics/SKILL.md +23 -0
  194. package/skills/pitch-deck/SKILL.md +24 -0
  195. package/skills/pixel-diff/SKILL.md +23 -0
  196. package/skills/ponytail/SKILL.md +46 -0
  197. package/skills/postmortem/SKILL.md +24 -0
  198. package/skills/pptx-deck/SKILL.md +23 -0
  199. package/skills/pptx-export/SKILL.md +23 -0
  200. package/skills/pptx-from-markdown/SKILL.md +23 -0
  201. package/skills/pptx-template/SKILL.md +24 -0
  202. package/skills/pr-review/SKILL.md +24 -0
  203. package/skills/precommit/SKILL.md +23 -0
  204. package/skills/pricing-page/SKILL.md +23 -0
  205. package/skills/project-overview/SKILL.md +22 -0
  206. package/skills/prompt-template/SKILL.md +23 -0
  207. package/skills/property-test/SKILL.md +23 -0
  208. package/skills/protobuf/SKILL.md +23 -0
  209. package/skills/py-async/SKILL.md +23 -0
  210. package/skills/py-typing/SKILL.md +23 -0
  211. package/skills/query-optimize/SKILL.md +23 -0
  212. package/skills/rag-pipeline/SKILL.md +23 -0
  213. package/skills/rails-resource/SKILL.md +23 -0
  214. package/skills/rate-limit/SKILL.md +23 -0
  215. package/skills/react-hooks/SKILL.md +23 -0
  216. package/skills/react-native-screen/SKILL.md +23 -0
  217. package/skills/react-perf/SKILL.md +23 -0
  218. package/skills/readme/SKILL.md +24 -0
  219. package/skills/rebase/SKILL.md +24 -0
  220. package/skills/refactor/SKILL.md +23 -0
  221. package/skills/regression-test/SKILL.md +23 -0
  222. package/skills/release-notes/SKILL.md +24 -0
  223. package/skills/rename-symbol/SKILL.md +23 -0
  224. package/skills/repro/SKILL.md +23 -0
  225. package/skills/resolve-conflicts/SKILL.md +23 -0
  226. package/skills/responsive/SKILL.md +23 -0
  227. package/skills/rest-endpoint/SKILL.md +23 -0
  228. package/skills/retro/SKILL.md +23 -0
  229. package/skills/rtos-task/SKILL.md +23 -0
  230. package/skills/runbook/SKILL.md +25 -0
  231. package/skills/rust-borrow/SKILL.md +23 -0
  232. package/skills/rust-unsafe-audit/SKILL.md +23 -0
  233. package/skills/sanitize/SKILL.md +23 -0
  234. package/skills/schema-design/SKILL.md +23 -0
  235. package/skills/screenshot-debug/SKILL.md +22 -0
  236. package/skills/scroll-animation/SKILL.md +23 -0
  237. package/skills/secret-scan/SKILL.md +23 -0
  238. package/skills/security-audit/SKILL.md +23 -0
  239. package/skills/security-review/SKILL.md +23 -0
  240. package/skills/seed-data/SKILL.md +23 -0
  241. package/skills/self-review/SKILL.md +23 -0
  242. package/skills/semantic-html/SKILL.md +23 -0
  243. package/skills/semver-bump/SKILL.md +24 -0
  244. package/skills/shader/SKILL.md +23 -0
  245. package/skills/shader-debug/SKILL.md +23 -0
  246. package/skills/simplify-conditionals/SKILL.md +23 -0
  247. package/skills/sitemap/SKILL.md +23 -0
  248. package/skills/skeleton-loader/SKILL.md +23 -0
  249. package/skills/slide-charts/SKILL.md +24 -0
  250. package/skills/slide-outline/SKILL.md +23 -0
  251. package/skills/snapshot-update/SKILL.md +23 -0
  252. package/skills/solidity-contract/SKILL.md +25 -0
  253. package/skills/speaker-notes/SKILL.md +23 -0
  254. package/skills/spike/SKILL.md +23 -0
  255. package/skills/split-file/SKILL.md +23 -0
  256. package/skills/spring-controller/SKILL.md +23 -0
  257. package/skills/sprite-anim/SKILL.md +23 -0
  258. package/skills/sql-report/SKILL.md +23 -0
  259. package/skills/squash/SKILL.md +24 -0
  260. package/skills/ssl-setup/SKILL.md +23 -0
  261. package/skills/stacktrace/SKILL.md +23 -0
  262. package/skills/static-site/SKILL.md +24 -0
  263. package/skills/structured-logging/SKILL.md +23 -0
  264. package/skills/svelte-store/SKILL.md +23 -0
  265. package/skills/swiftui-view/SKILL.md +23 -0
  266. package/skills/tailwind-theme/SKILL.md +24 -0
  267. package/skills/tcp-server/SKILL.md +23 -0
  268. package/skills/tdd/SKILL.md +23 -0
  269. package/skills/terraform-module/SKILL.md +24 -0
  270. package/skills/test-coverage/SKILL.md +23 -0
  271. package/skills/texture-debug/SKILL.md +23 -0
  272. package/skills/threat-model/SKILL.md +23 -0
  273. package/skills/thumbnail/SKILL.md +24 -0
  274. package/skills/todo-scan/SKILL.md +23 -0
  275. package/skills/tool-definition/SKILL.md +23 -0
  276. package/skills/trace-flow/SKILL.md +23 -0
  277. package/skills/tracing/SKILL.md +23 -0
  278. package/skills/train-model/SKILL.md +24 -0
  279. package/skills/tree-shake/SKILL.md +23 -0
  280. package/skills/ts-generics/SKILL.md +23 -0
  281. package/skills/ts-strict/SKILL.md +23 -0
  282. package/skills/tui-app/SKILL.md +23 -0
  283. package/skills/tutorial/SKILL.md +24 -0
  284. package/skills/type-tighten/SKILL.md +23 -0
  285. package/skills/typography/SKILL.md +24 -0
  286. package/skills/ui-bug-repro/SKILL.md +23 -0
  287. package/skills/ui-polish/SKILL.md +24 -0
  288. package/skills/ui-review/SKILL.md +24 -0
  289. package/skills/vendor/SKILL.md +23 -0
  290. package/skills/visual-diff-ci/SKILL.md +24 -0
  291. package/skills/visual-regression/SKILL.md +23 -0
  292. package/skills/vue-composition/SKILL.md +23 -0
  293. package/skills/web-component/SKILL.md +23 -0
  294. package/skills/web-fonts/SKILL.md +24 -0
  295. package/skills/web3-frontend/SKILL.md +25 -0
  296. package/skills/webgl-debug/SKILL.md +23 -0
  297. package/skills/webhook/SKILL.md +23 -0
  298. package/skills/websocket/SKILL.md +23 -0
  299. package/skills/write-tests/SKILL.md +19 -0
  300. package/src/client/agent.ts +803 -0
  301. package/src/client/background.ts +39 -0
  302. package/src/client/checkpoint.ts +48 -0
  303. package/src/client/cli.ts +1253 -0
  304. package/src/client/compaction.ts +86 -0
  305. package/src/client/extensions.ts +83 -0
  306. package/src/client/hooks.ts +40 -0
  307. package/src/client/image.ts +26 -0
  308. package/src/client/lsp.ts +0 -0
  309. package/src/client/mcp.ts +276 -0
  310. package/src/client/models-dev.ts +52 -0
  311. package/src/client/pkg.ts +41 -0
  312. package/src/client/platform.ts +94 -0
  313. package/src/client/prompts.ts +47 -0
  314. package/src/client/render.ts +138 -0
  315. package/src/client/session.ts +107 -0
  316. package/src/client/settings.ts +86 -0
  317. package/src/client/skill-router.ts +79 -0
  318. package/src/client/skills.ts +199 -0
  319. package/src/client/snapshot.ts +56 -0
  320. package/src/client/telemetry.ts +24 -0
  321. package/src/client/todos.ts +23 -0
  322. package/src/client/tools.ts +756 -0
  323. package/src/client/tui-mode.ts +41 -0
  324. package/src/client/tui.ts +224 -0
  325. package/src/sdk/index.ts +36 -0
  326. package/src/selfcheck.ts +364 -0
  327. package/src/server/config.ts +58 -0
  328. package/src/server/credentials.ts +89 -0
  329. package/src/server/identity.ts +58 -0
  330. package/src/server/index.ts +113 -0
  331. package/src/server/oauth.ts +93 -0
  332. package/src/server/providers/adapter.ts +25 -0
  333. package/src/server/providers/anthropic.ts +189 -0
  334. package/src/server/providers/openai-compat.ts +76 -0
  335. package/src/server/providers/registry.ts +31 -0
  336. package/src/server/router.ts +29 -0
  337. package/src/server/sse.ts +20 -0
  338. package/src/shared/types.ts +20 -0
  339. package/tsconfig.json +15 -0
@@ -0,0 +1,756 @@
1
+ // The built-in tools, in OpenAI function-tool shape. read_file is safe; the rest are gated.
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { glob as fsGlob } from "node:fs/promises";
7
+ import { dirname, extname, join, relative, resolve } from "node:path";
8
+ import type * as PtyType from "node-pty";
9
+ import { green, renderEditDiff } from "./render.ts";
10
+ import * as checkpoint from "./checkpoint.ts";
11
+ import { renderTodos, setTodos, type Todo } from "./todos.ts";
12
+ import { isTrusted, loadSettings } from "./settings.ts";
13
+ import { getDiagnostics } from "./lsp.ts";
14
+
15
+ const MAX_OUTPUT = 30_000;
16
+
17
+ export interface ToolResult {
18
+ output: string; // text returned to the model
19
+ isError?: boolean;
20
+ display?: string; // optional rich, user-facing render (e.g. a colored diff)
21
+ }
22
+
23
+ export interface Tool {
24
+ name: string;
25
+ description: string;
26
+ parameters: Record<string, unknown>; // JSON Schema
27
+ needsApproval: boolean;
28
+ run(args: Record<string, unknown>): Promise<ToolResult>;
29
+ }
30
+
31
+ function truncate(s: string): string {
32
+ return s.length > MAX_OUTPUT ? `${s.slice(0, MAX_OUTPUT)}\n… [truncated ${s.length - MAX_OUTPUT} chars]` : s;
33
+ }
34
+
35
+ // The front-end (CLI/TUI) installs an asker so the ask_user tool can prompt the user mid-task.
36
+ type Asker = (question: string, options?: string[]) => Promise<string>;
37
+ let asker: Asker | null = null;
38
+ export function setAsker(fn: Asker | null): void {
39
+ asker = fn;
40
+ }
41
+
42
+ /** Strip HTML to readable text (no dependency) — good enough for "read this page". */
43
+ export function htmlToText(html: string): string {
44
+ return html
45
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
46
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
47
+ .replace(/<!--[\s\S]*?-->/g, " ")
48
+ .replace(/<li[^>]*>/gi, "\n- ")
49
+ .replace(/<\/(?:p|div|section|article|tr|h[1-6])>/gi, "\n")
50
+ .replace(/<(?:br|h[1-6])[^>]*>/gi, "\n")
51
+ .replace(/<[^>]+>/g, " ")
52
+ .replace(/&nbsp;/gi, " ")
53
+ .replace(/&amp;/gi, "&")
54
+ .replace(/&lt;/gi, "<")
55
+ .replace(/&gt;/gi, ">")
56
+ .replace(/&quot;/gi, '"')
57
+ .replace(/&#x?39;|&#x27;/gi, "'")
58
+ .replace(/[ \t]+/g, " ")
59
+ .replace(/\n{3,}/g, "\n\n")
60
+ .trim();
61
+ }
62
+
63
+ // Auto-format a just-written file with a discovered project formatter (best-effort).
64
+ // Trust-gated (same gate as extensions/MCP) so a repo can't auto-run a trojan local formatter.
65
+ const FORMATTERS: { exts: string[]; bin: string; args: (f: string) => string[] }[] = [
66
+ { exts: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".css", ".scss", ".less", ".html", ".md", ".mdx", ".yaml", ".yml", ".vue", ".svelte", ".graphql"], bin: "prettier", args: (f) => ["--write", f] },
67
+ { exts: [".go"], bin: "gofmt", args: (f) => ["-w", f] },
68
+ { exts: [".rs"], bin: "rustfmt", args: (f) => [f] },
69
+ { exts: [".py"], bin: "ruff", args: (f) => ["format", "-q", f] },
70
+ { exts: [".sh", ".bash"], bin: "shfmt", args: (f) => ["-w", f] },
71
+ ];
72
+ const binCache = new Map<string, string | null>();
73
+ function findBin(bin: string): string | null {
74
+ const cached = binCache.get(bin);
75
+ if (cached !== undefined) return cached;
76
+ let found: string | null = null;
77
+ const local = resolve(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? `${bin}.cmd` : bin);
78
+ if (existsSync(local)) found = local;
79
+ else {
80
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf8" });
81
+ if (probe.status === 0 && (probe.stdout ?? "").trim()) found = bin;
82
+ }
83
+ binCache.set(bin, found);
84
+ return found;
85
+ }
86
+
87
+ /** Format `abs` in place with a discovered formatter. No-op (returns false) if untrusted, disabled,
88
+ * or no formatter is available for the extension. Never throws. */
89
+ export function formatFile(abs: string): boolean {
90
+ if (process.env.ADA_NO_FORMAT || !isTrusted(process.cwd())) return false;
91
+ const ext = extname(abs).toLowerCase();
92
+ const fmt = FORMATTERS.find((f) => f.exts.includes(ext) && findBin(f.bin));
93
+ if (!fmt) return false;
94
+ try {
95
+ return spawnSync(findBin(fmt.bin)!, fmt.args(abs), { timeout: 10_000, encoding: "utf8", shell: process.platform === "win32" }).status === 0;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ // node-pty gives the bash tool a real terminal. It's a required dependency; if the native build is
102
+ // ever broken on a platform, fall back to spawnSync so bash still works.
103
+ const pty: typeof PtyType | null = (() => {
104
+ try {
105
+ return createRequire(import.meta.url)("node-pty") as typeof PtyType;
106
+ } catch {
107
+ return null;
108
+ }
109
+ })();
110
+
111
+ // Built via new RegExp (string escapes) so no literal ESC/BEL bytes live in the source.
112
+ const ANSI = new RegExp("[\\u001B\\u009B][\\[\\]()#;?]*(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007|(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])", "g");
113
+ function stripAnsi(s: string): string {
114
+ return s.replace(ANSI, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
115
+ }
116
+
117
+ /** Run a command in a PTY (real terminal); resolves with combined output + exit code. */
118
+ function runPty(command: string, timeoutMs = 120_000): Promise<{ output: string; code: number | null }> {
119
+ return new Promise((res) => {
120
+ const win = process.platform === "win32";
121
+ const shell = win ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash";
122
+ const shellArgs = win ? ["/c", command] : ["-lc", command];
123
+ const p = pty!.spawn(shell, shellArgs, { name: "xterm-256color", cols: 120, rows: 30, cwd: process.cwd(), env: process.env as Record<string, string> });
124
+ let out = "";
125
+ const cap = 10 * 1024 * 1024;
126
+ p.onData((d) => {
127
+ if (out.length < cap) out += d;
128
+ });
129
+ let done = false;
130
+ const finish = (code: number | null): void => {
131
+ if (done) return;
132
+ done = true;
133
+ clearTimeout(timer);
134
+ res({ output: out, code });
135
+ };
136
+ const timer = setTimeout(() => {
137
+ try {
138
+ p.kill();
139
+ } catch {
140
+ /* already gone */
141
+ }
142
+ finish(null);
143
+ }, timeoutMs);
144
+ p.onExit(({ exitCode }) => finish(exitCode));
145
+ });
146
+ }
147
+
148
+ /** Block localhost / private / metadata hosts (basic SSRF guard for web_fetch). */
149
+ function isBlockedHost(host: string): boolean {
150
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
151
+ if (h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "0.0.0.0") return true;
152
+ const m = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
153
+ if (m) {
154
+ const a = Number(m[1]);
155
+ const b = Number(m[2]);
156
+ if (a === 0 || a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254)) return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ const IMG_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico"]);
162
+
163
+ // Serialize mutations to the same path so concurrent writes never interleave.
164
+ const fileLocks = new Map<string, Promise<unknown>>();
165
+ function withFileLock<T>(abs: string, fn: () => Promise<T>): Promise<T> {
166
+ const prev = fileLocks.get(abs) ?? Promise.resolve();
167
+ const next = prev.then(fn, fn);
168
+ fileLocks.set(abs, next.catch(() => undefined));
169
+ return next;
170
+ }
171
+
172
+ // Huge output is spilled to .ada/tmp and replaced by a head + pointer, instead of lost to truncation.
173
+ function spillIfHuge(text: string): string {
174
+ if (text.length <= MAX_OUTPUT) return text;
175
+ try {
176
+ const dir = join(process.cwd(), ".ada", "tmp");
177
+ mkdirSync(dir, { recursive: true });
178
+ const f = join(dir, `out-${Date.now()}-${Math.floor(Math.random() * 1e6)}.txt`);
179
+ writeFileSync(f, text, "utf8");
180
+ return `${text.slice(0, MAX_OUTPUT)}\n… [truncated ${text.length - MAX_OUTPUT} chars; full output: ${relative(process.cwd(), f)}]`;
181
+ } catch {
182
+ return truncate(text);
183
+ }
184
+ }
185
+
186
+ function globMatch(rel: string, pattern: string): boolean {
187
+ const p = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::").replace(/\*/g, "[^/]*").replace(/::/g, ".*");
188
+ try {
189
+ return new RegExp(`^${p}$`).test(rel);
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+
195
+ /** A write/edit target is protected if it matches (or contains) a glob in settings.protectedPaths. */
196
+ function isProtected(abs: string): boolean {
197
+ const pats = loadSettings(true).protectedPaths;
198
+ if (!pats || !pats.length) return false;
199
+ const rel = relative(process.cwd(), abs).replace(/\\/g, "/");
200
+ return pats.some((g) => rel.includes(g) || abs.includes(g) || globMatch(rel, g));
201
+ }
202
+
203
+ // Note the /dev/ rule excludes the standard sinks (>/dev/null, 2>/dev/null, /dev/stdout, …) — those
204
+ // are everyday redirects, not device-overwrites like `> /dev/sda`.
205
+ const DESTRUCTIVE = /\brm\s+-[a-z]*[rf]|\brmdir\b|\bdd\b|mkfs|>\s*\/dev\/(?!(?:null|stdout|stderr|tty|zero|u?random)(?:$|[\s>;&|])|fd\/)|:\(\)\s*\{|git\s+push\b[^\n]*--force|git\s+reset\s+--hard|\bshutdown\b|\breboot\b|\bkillall\b|chmod\s+-R|chown\s+-R/i;
206
+ /** True for shell commands dangerous enough to always confirm, even in auto-approve. */
207
+ export function isDestructive(command: string): boolean {
208
+ return DESTRUCTIVE.test(command);
209
+ }
210
+
211
+ export const tools: Tool[] = [
212
+ {
213
+ name: "read_file",
214
+ description: "Read a UTF-8 text file. Optional offset/limit (1-based line range) for large files.",
215
+ parameters: {
216
+ type: "object",
217
+ properties: {
218
+ path: { type: "string", description: "File path, relative to cwd or absolute." },
219
+ offset: { type: "number", description: "1-based first line to return." },
220
+ limit: { type: "number", description: "Maximum number of lines to return." },
221
+ },
222
+ required: ["path"],
223
+ additionalProperties: false,
224
+ },
225
+ needsApproval: false,
226
+ async run(args) {
227
+ const abs = resolve(process.cwd(), String(args.path));
228
+ if (!existsSync(abs)) return { output: `File not found: ${String(args.path)}`, isError: true };
229
+ const ext = extname(abs).toLowerCase();
230
+ if (IMG_EXT.has(ext)) {
231
+ try {
232
+ return { output: `[${ext.slice(1)} image: ${String(args.path)}, ${statSync(abs).size} bytes] — this build cannot view images` };
233
+ } catch (e) {
234
+ return { output: String(e), isError: true };
235
+ }
236
+ }
237
+ try {
238
+ let text = readFileSync(abs, "utf8");
239
+ const offset = Number(args.offset) || 0;
240
+ const limit = Number(args.limit) || 0;
241
+ if (offset > 0 || limit > 0) {
242
+ const lines = text.split("\n");
243
+ const start = offset > 0 ? offset - 1 : 0;
244
+ text = lines.slice(start, limit > 0 ? start + limit : undefined).join("\n");
245
+ }
246
+ return { output: truncate(text) };
247
+ } catch (e) {
248
+ return { output: String(e), isError: true };
249
+ }
250
+ },
251
+ },
252
+ {
253
+ name: "write_file",
254
+ description: "Create or overwrite a file with the given content. Creates parent directories.",
255
+ parameters: {
256
+ type: "object",
257
+ properties: { path: { type: "string" }, content: { type: "string" } },
258
+ required: ["path", "content"],
259
+ additionalProperties: false,
260
+ },
261
+ needsApproval: true,
262
+ async run(args) {
263
+ const abs = resolve(process.cwd(), String(args.path));
264
+ const content = String(args.content ?? "");
265
+ return withFileLock(abs, async () => {
266
+ if (isProtected(abs)) return { output: `Refused: ${String(args.path)} is a protected path.`, isError: true };
267
+ checkpoint.record(abs);
268
+ try {
269
+ mkdirSync(dirname(abs), { recursive: true });
270
+ writeFileSync(abs, content, "utf8");
271
+ const formatted = formatFile(abs);
272
+ return {
273
+ output: `Wrote ${content.length} bytes to ${String(args.path)}${formatted ? " (auto-formatted)" : ""}`,
274
+ display: green(`+ ${String(args.path)} (${content.length} bytes written)${formatted ? " · formatted" : ""}`),
275
+ };
276
+ } catch (e) {
277
+ return { output: String(e), isError: true };
278
+ }
279
+ });
280
+ },
281
+ },
282
+ {
283
+ name: "edit_file",
284
+ description:
285
+ "Replace exact, unique snippet(s) in a file. Each old_text must occur exactly once. " +
286
+ "Pass old_text/new_text for one edit, or an `edits` array of {old_text,new_text} applied in order. " +
287
+ "Matching tolerates CRLF/LF differences; the file's original line endings and BOM are preserved.",
288
+ parameters: {
289
+ type: "object",
290
+ properties: {
291
+ path: { type: "string" },
292
+ old_text: { type: "string" },
293
+ new_text: { type: "string" },
294
+ edits: {
295
+ type: "array",
296
+ description: "Multiple edits applied in sequence.",
297
+ items: {
298
+ type: "object",
299
+ properties: { old_text: { type: "string" }, new_text: { type: "string" } },
300
+ required: ["old_text", "new_text"],
301
+ additionalProperties: false,
302
+ },
303
+ },
304
+ },
305
+ required: ["path"],
306
+ additionalProperties: false,
307
+ },
308
+ needsApproval: true,
309
+ async run(args) {
310
+ const abs = resolve(process.cwd(), String(args.path));
311
+ if (!existsSync(abs)) return { output: `File not found: ${String(args.path)}`, isError: true };
312
+ if (isProtected(abs)) return { output: `Refused: ${String(args.path)} is a protected path.`, isError: true };
313
+ const norm = (s: string): string => s.replace(/\r\n/g, "\n");
314
+ const list = Array.isArray(args.edits)
315
+ ? (args.edits as Array<Record<string, unknown>>).map((e) => ({ old: norm(String(e.old_text ?? "")), neu: norm(String(e.new_text ?? "")) }))
316
+ : [{ old: norm(String(args.old_text ?? "")), neu: norm(String(args.new_text ?? "")) }];
317
+ if (!list.length || !list[0]!.old) return { output: "Provide old_text/new_text or a non-empty edits array.", isError: true };
318
+ return withFileLock(abs, async () => {
319
+ let raw: string;
320
+ try {
321
+ raw = readFileSync(abs, "utf8");
322
+ } catch (e) {
323
+ return { output: String(e), isError: true };
324
+ }
325
+ const bom = raw.charCodeAt(0) === 0xfeff;
326
+ if (bom) raw = raw.slice(1);
327
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
328
+ let content = norm(raw);
329
+ for (let i = 0; i < list.length; i++) {
330
+ const { old, neu } = list[i]!;
331
+ if (!old) return { output: `edit ${i + 1}: old_text must not be empty`, isError: true };
332
+ const count = content.split(old).length - 1;
333
+ if (count === 0) return { output: `edit ${i + 1}: old_text not found in ${String(args.path)}`, isError: true };
334
+ if (count > 1) return { output: `edit ${i + 1}: old_text appears ${count} times; add context to make it unique`, isError: true };
335
+ content = content.replace(old, neu);
336
+ }
337
+ let out = eol === "\r\n" ? content.replace(/\n/g, "\r\n") : content;
338
+ if (bom) out = String.fromCharCode(0xfeff) + out;
339
+ checkpoint.record(abs);
340
+ try {
341
+ writeFileSync(abs, out, "utf8");
342
+ } catch (e) {
343
+ return { output: String(e), isError: true };
344
+ }
345
+ const formatted = formatFile(abs);
346
+ const label = list.length > 1 ? `${list.length} changes` : "1 change";
347
+ return { output: `Edited ${String(args.path)} (${label})${formatted ? " · auto-formatted" : ""}`, display: renderEditDiff(String(args.path), list[0]!.old, list[0]!.neu) };
348
+ });
349
+ },
350
+ },
351
+ {
352
+ name: "bash",
353
+ description: "Run a shell command in the working directory through a real PTY (terminal); returns exit code + combined output.",
354
+ parameters: {
355
+ type: "object",
356
+ properties: { command: { type: "string" } },
357
+ required: ["command"],
358
+ additionalProperties: false,
359
+ },
360
+ needsApproval: true,
361
+ async run(args) {
362
+ const command = String(args.command);
363
+ if (pty) {
364
+ const { output, code } = await runPty(command);
365
+ return { output: `exit ${code ?? "null"}\n${spillIfHuge(stripAnsi(output).trim() || "(no output)")}`, isError: code !== 0 };
366
+ }
367
+ // fallback: native PTY unavailable on this platform
368
+ const res = spawnSync(command, { shell: true, encoding: "utf8", timeout: 120_000, maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
369
+ const out = [res.stdout, res.stderr].filter(Boolean).join("\n").trim() || "(no output)";
370
+ return { output: `exit ${res.status ?? "null"}\n${spillIfHuge(out)}`, isError: res.status !== 0 };
371
+ },
372
+ },
373
+ {
374
+ name: "update_todos",
375
+ description: "Maintain the task checklist for the current request. Pass the FULL list each call; mark items done as you finish them. Use for any multi-step task.",
376
+ parameters: {
377
+ type: "object",
378
+ properties: {
379
+ todos: {
380
+ type: "array",
381
+ items: {
382
+ type: "object",
383
+ properties: { text: { type: "string" }, status: { type: "string", enum: ["todo", "doing", "done"] } },
384
+ required: ["text", "status"],
385
+ additionalProperties: false,
386
+ },
387
+ },
388
+ },
389
+ required: ["todos"],
390
+ additionalProperties: false,
391
+ },
392
+ needsApproval: false,
393
+ async run(args) {
394
+ const items: Todo[] = (Array.isArray(args.todos) ? (args.todos as Array<Record<string, unknown>>) : []).map((t) => ({
395
+ text: String(t.text ?? ""),
396
+ status: (["todo", "doing", "done"].includes(String(t.status)) ? String(t.status) : "todo") as Todo["status"],
397
+ }));
398
+ setTodos(items);
399
+ return { output: `Updated ${items.length} todo(s).`, display: renderTodos() };
400
+ },
401
+ },
402
+ {
403
+ name: "ls",
404
+ description: "List entries in a directory (directories shown with a trailing slash).",
405
+ parameters: {
406
+ type: "object",
407
+ properties: { path: { type: "string", description: "Directory path; defaults to the working directory." } },
408
+ additionalProperties: false,
409
+ },
410
+ needsApproval: false,
411
+ async run(args) {
412
+ const dir = resolve(process.cwd(), String(args.path ?? "."));
413
+ if (!existsSync(dir)) return { output: `Not found: ${String(args.path ?? ".")}`, isError: true };
414
+ try {
415
+ const entries = readdirSync(dir, { withFileTypes: true })
416
+ .map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
417
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
418
+ return { output: truncate(entries.join("\n") || "(empty)") };
419
+ } catch (e) {
420
+ return { output: String(e), isError: true };
421
+ }
422
+ },
423
+ },
424
+ {
425
+ name: "grep",
426
+ description: "Search file contents for a regular expression; returns matching path:line:text. Recurses, skipping node_modules/.git/dist and binary files.",
427
+ parameters: {
428
+ type: "object",
429
+ properties: {
430
+ pattern: { type: "string", description: "A JavaScript regular expression." },
431
+ path: { type: "string", description: "File or directory to search; defaults to the working directory." },
432
+ ignore_case: { type: "boolean" },
433
+ },
434
+ required: ["pattern"],
435
+ additionalProperties: false,
436
+ },
437
+ needsApproval: false,
438
+ async run(args) {
439
+ const pattern = String(args.pattern);
440
+ const searchPath = String(args.path ?? ".");
441
+ const root = resolve(process.cwd(), searchPath);
442
+ if (!existsSync(root)) return { output: `Not found: ${searchPath}`, isError: true };
443
+ const MAX = 200;
444
+ // Fast path: ripgrep if available (much faster on big trees; respects .gitignore).
445
+ const rg = findBin("rg");
446
+ if (rg) {
447
+ const rgArgs = ["--no-heading", "--line-number", "--color", "never"];
448
+ if (args.ignore_case) rgArgs.push("-i");
449
+ rgArgs.push("-e", pattern, searchPath);
450
+ const r = spawnSync(rg, rgArgs, { cwd: process.cwd(), encoding: "utf8", maxBuffer: 10 * 1024 * 1024, timeout: 30_000 });
451
+ if (r.status === 0 || r.status === 1) {
452
+ // 0 = matches, 1 = no matches
453
+ const out = (r.stdout || "").split("\n").filter(Boolean).slice(0, MAX);
454
+ const more = out.length >= MAX ? `\n… (capped at ${MAX})` : "";
455
+ return { output: (out.join("\n") || "(no matches)") + more };
456
+ }
457
+ // rg failed (e.g. its regex dialect rejected the pattern) → fall through to the JS scan
458
+ }
459
+ let re: RegExp;
460
+ try {
461
+ re = new RegExp(pattern, args.ignore_case ? "i" : "");
462
+ } catch (e) {
463
+ return { output: `Invalid regex: ${e instanceof Error ? e.message : e}`, isError: true };
464
+ }
465
+ const SKIP = new Set(["node_modules", ".git", "dist", ".ada", ".next", "build", "coverage"]);
466
+ const results: string[] = [];
467
+ const walk = (p: string): void => {
468
+ if (results.length >= MAX) return;
469
+ let st: ReturnType<typeof statSync>;
470
+ try {
471
+ st = statSync(p);
472
+ } catch {
473
+ return;
474
+ }
475
+ if (st.isDirectory()) {
476
+ let names: string[];
477
+ try {
478
+ names = readdirSync(p);
479
+ } catch {
480
+ return;
481
+ }
482
+ for (const n of names) {
483
+ if (results.length >= MAX) break;
484
+ if (!SKIP.has(n)) walk(join(p, n));
485
+ }
486
+ } else if (st.isFile() && st.size <= 2_000_000) {
487
+ let text: string;
488
+ try {
489
+ text = readFileSync(p, "utf8");
490
+ } catch {
491
+ return;
492
+ }
493
+ if (text.includes(String.fromCharCode(0))) return; // skip binary
494
+ const rel = relative(process.cwd(), p) || p;
495
+ const lines = text.split("\n");
496
+ for (let i = 0; i < lines.length; i++) {
497
+ if (re.test(lines[i]!)) {
498
+ results.push(`${rel}:${i + 1}:${lines[i]!.slice(0, 300)}`);
499
+ if (results.length >= MAX) break;
500
+ }
501
+ }
502
+ }
503
+ };
504
+ walk(root);
505
+ const more = results.length >= MAX ? `\n… (capped at ${MAX} matches)` : "";
506
+ return { output: (results.join("\n") || "(no matches)") + more };
507
+ },
508
+ },
509
+ {
510
+ name: "glob",
511
+ description: 'Find files matching a glob pattern (e.g. "**/*.ts", "src/**/*.test.js"), relative to the working directory.',
512
+ parameters: {
513
+ type: "object",
514
+ properties: { pattern: { type: "string" } },
515
+ required: ["pattern"],
516
+ additionalProperties: false,
517
+ },
518
+ needsApproval: false,
519
+ async run(args) {
520
+ const MAX = 500;
521
+ const matches: string[] = [];
522
+ try {
523
+ for await (const m of fsGlob(String(args.pattern))) {
524
+ if (/(^|[\\/])(node_modules|\.git|dist|\.ada)([\\/]|$)/.test(m)) continue;
525
+ matches.push(m);
526
+ if (matches.length >= MAX) break;
527
+ }
528
+ } catch (e) {
529
+ return { output: String(e), isError: true };
530
+ }
531
+ matches.sort();
532
+ const more = matches.length >= MAX ? `\n… (capped at ${MAX})` : "";
533
+ return { output: (matches.join("\n") || "(no matches)") + more };
534
+ },
535
+ },
536
+ {
537
+ name: "web_fetch",
538
+ description: "Fetch an http(s) URL and return its content as readable text (HTML is stripped to text). Use to read docs, articles, changelogs, or JSON APIs.",
539
+ parameters: {
540
+ type: "object",
541
+ properties: { url: { type: "string" }, raw: { type: "boolean", description: "return the raw body instead of HTML→text" } },
542
+ required: ["url"],
543
+ additionalProperties: false,
544
+ },
545
+ needsApproval: false,
546
+ async run(args) {
547
+ let url: URL;
548
+ try {
549
+ url = new URL(String(args.url));
550
+ } catch {
551
+ return { output: `Invalid URL: ${String(args.url)}`, isError: true };
552
+ }
553
+ if (url.protocol !== "http:" && url.protocol !== "https:") return { output: "Only http/https URLs are allowed.", isError: true };
554
+ if (isBlockedHost(url.hostname)) return { output: "Refusing to fetch a localhost/private address.", isError: true };
555
+ try {
556
+ const res = await fetch(url, {
557
+ headers: { "user-agent": "ada/0.0.1 (+https://github.com/black141312/ada)", accept: "text/html,text/plain,application/json,*/*" },
558
+ redirect: "follow",
559
+ signal: AbortSignal.timeout(20_000),
560
+ });
561
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]!.trim();
562
+ const body = await res.text();
563
+ if (!res.ok) return { output: truncate(`HTTP ${res.status} ${res.statusText} (${url.href})\n\n${body}`), isError: true };
564
+ const text = args.raw || !/html/i.test(ct) ? body : htmlToText(body);
565
+ return { output: truncate(`${url.href} — ${res.status} ${ct}\n\n${text}`) };
566
+ } catch (e) {
567
+ return { output: `fetch failed: ${e instanceof Error ? e.message : e}`, isError: true };
568
+ }
569
+ },
570
+ },
571
+ {
572
+ name: "web_search",
573
+ description: "Search the web; returns the top results (title, URL, snippet). Requires a Brave Search API key (BRAVE_API_KEY). Use to find docs/answers, then web_fetch a result.",
574
+ parameters: {
575
+ type: "object",
576
+ properties: { query: { type: "string" } },
577
+ required: ["query"],
578
+ additionalProperties: false,
579
+ },
580
+ needsApproval: false,
581
+ async run(args) {
582
+ const key = process.env.BRAVE_API_KEY ?? process.env.ADA_BRAVE_API_KEY;
583
+ if (!key) return { output: "web_search needs a Brave Search API key — set BRAVE_API_KEY (free tier at brave.com/search/api). For a known page, use web_fetch instead.", isError: true };
584
+ try {
585
+ const u = new URL("https://api.search.brave.com/res/v1/web/search");
586
+ u.searchParams.set("q", String(args.query));
587
+ u.searchParams.set("count", "8");
588
+ const res = await fetch(u, { headers: { "x-subscription-token": key, accept: "application/json" }, signal: AbortSignal.timeout(20_000) });
589
+ if (!res.ok) return { output: `Brave search HTTP ${res.status} ${res.statusText}`, isError: true };
590
+ const data = (await res.json()) as { web?: { results?: { title: string; url: string; description?: string }[] } };
591
+ const results = data.web?.results ?? [];
592
+ if (!results.length) return { output: "(no results)" };
593
+ return { output: results.map((r) => `- ${r.title}\n ${r.url}\n ${htmlToText(r.description ?? "").slice(0, 200)}`).join("\n\n") };
594
+ } catch (e) {
595
+ return { output: `search failed: ${e instanceof Error ? e.message : e}`, isError: true };
596
+ }
597
+ },
598
+ },
599
+ {
600
+ name: "ask_user",
601
+ description: "Ask the user a clarifying question and wait for their answer. Use only when you're genuinely blocked or a decision is the user's to make — not for routine choices. Optionally provide a list of options.",
602
+ parameters: {
603
+ type: "object",
604
+ properties: { question: { type: "string" }, options: { type: "array", items: { type: "string" } } },
605
+ required: ["question"],
606
+ additionalProperties: false,
607
+ },
608
+ needsApproval: false,
609
+ async run(args) {
610
+ if (!asker) return { output: "(no interactive session — cannot ask the user; proceed with your best judgment)", isError: true };
611
+ const options = Array.isArray(args.options) ? (args.options as unknown[]).map(String) : undefined;
612
+ try {
613
+ const answer = (await asker(String(args.question ?? ""), options)).trim();
614
+ return { output: answer ? `User answered: ${answer}` : "(user gave no answer)" };
615
+ } catch (e) {
616
+ return { output: String(e), isError: true };
617
+ }
618
+ },
619
+ },
620
+ {
621
+ name: "apply_patch",
622
+ description: "Apply a coordinated change across multiple files in one call. Each file has an action: create (full content), update (exact-match edits), or delete. Prefer this over many edit_file calls for multi-file changes.",
623
+ parameters: {
624
+ type: "object",
625
+ properties: {
626
+ files: {
627
+ type: "array",
628
+ items: {
629
+ type: "object",
630
+ properties: {
631
+ path: { type: "string" },
632
+ action: { type: "string", enum: ["create", "update", "delete"] },
633
+ content: { type: "string", description: "create: the full file content" },
634
+ edits: {
635
+ type: "array",
636
+ description: "update: exact-match replacements applied in order",
637
+ items: { type: "object", properties: { old_text: { type: "string" }, new_text: { type: "string" } }, required: ["old_text", "new_text"], additionalProperties: false },
638
+ },
639
+ },
640
+ required: ["path", "action"],
641
+ additionalProperties: false,
642
+ },
643
+ },
644
+ },
645
+ required: ["files"],
646
+ additionalProperties: false,
647
+ },
648
+ needsApproval: true,
649
+ async run(args) {
650
+ const files = Array.isArray(args.files) ? (args.files as Array<Record<string, unknown>>) : [];
651
+ if (!files.length) return { output: "No files in patch.", isError: true };
652
+ const lines: string[] = [];
653
+ let anyErr = false;
654
+ const norm = (s: string): string => s.replace(/\r\n/g, "\n");
655
+ for (const f of files) {
656
+ const p = String(f.path ?? "");
657
+ const abs = resolve(process.cwd(), p);
658
+ const action = String(f.action);
659
+ if (isProtected(abs)) {
660
+ lines.push(`✗ ${p}: protected path`);
661
+ anyErr = true;
662
+ continue;
663
+ }
664
+ try {
665
+ if (action === "delete") {
666
+ if (!existsSync(abs)) {
667
+ lines.push(`✗ ${p}: not found`);
668
+ anyErr = true;
669
+ continue;
670
+ }
671
+ checkpoint.record(abs);
672
+ rmSync(abs);
673
+ lines.push(`− ${p} (deleted)`);
674
+ } else if (action === "create") {
675
+ checkpoint.record(abs);
676
+ mkdirSync(dirname(abs), { recursive: true });
677
+ const content = String(f.content ?? "");
678
+ writeFileSync(abs, content, "utf8");
679
+ const fmt = formatFile(abs);
680
+ lines.push(`+ ${p} (${content.length} bytes${fmt ? ", formatted" : ""})`);
681
+ } else if (action === "update") {
682
+ if (!existsSync(abs)) {
683
+ lines.push(`✗ ${p}: not found`);
684
+ anyErr = true;
685
+ continue;
686
+ }
687
+ let raw = readFileSync(abs, "utf8");
688
+ const bom = raw.charCodeAt(0) === 0xfeff;
689
+ if (bom) raw = raw.slice(1);
690
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
691
+ let content = norm(raw);
692
+ const edits = Array.isArray(f.edits) ? (f.edits as Array<Record<string, unknown>>) : [];
693
+ let ok = true;
694
+ for (const e of edits) {
695
+ const old = norm(String(e.old_text ?? ""));
696
+ const neu = norm(String(e.new_text ?? ""));
697
+ const count = old ? content.split(old).length - 1 : 0;
698
+ if (count !== 1) {
699
+ lines.push(`✗ ${p}: an edit matched ${count} times (must be exactly 1)`);
700
+ anyErr = true;
701
+ ok = false;
702
+ break;
703
+ }
704
+ content = content.replace(old, neu);
705
+ }
706
+ if (!ok) continue;
707
+ checkpoint.record(abs);
708
+ let out = eol === "\r\n" ? content.replace(/\n/g, "\r\n") : content;
709
+ if (bom) out = String.fromCharCode(0xfeff) + out;
710
+ writeFileSync(abs, out, "utf8");
711
+ const fmt = formatFile(abs);
712
+ lines.push(`~ ${p} (${edits.length} edit${edits.length === 1 ? "" : "s"}${fmt ? ", formatted" : ""})`);
713
+ } else {
714
+ lines.push(`✗ ${p}: unknown action "${action}"`);
715
+ anyErr = true;
716
+ }
717
+ } catch (e) {
718
+ lines.push(`✗ ${p}: ${e instanceof Error ? e.message : e}`);
719
+ anyErr = true;
720
+ }
721
+ }
722
+ return { output: lines.join("\n"), isError: anyErr };
723
+ },
724
+ },
725
+ {
726
+ name: "lsp_diagnostics",
727
+ description: "Get language-server diagnostics (errors/warnings) for a file — call after editing to check it compiles/type-checks. Needs the language server installed (typescript-language-server, pyright, gopls, rust-analyzer) in a trusted project.",
728
+ parameters: {
729
+ type: "object",
730
+ properties: { path: { type: "string" } },
731
+ required: ["path"],
732
+ additionalProperties: false,
733
+ },
734
+ needsApproval: false,
735
+ async run(args) {
736
+ const abs = resolve(process.cwd(), String(args.path));
737
+ if (!existsSync(abs)) return { output: `File not found: ${String(args.path)}`, isError: true };
738
+ try {
739
+ const diags = await getDiagnostics(abs);
740
+ return { output: diags.length ? diags.join("\n") : "No diagnostics (clean, or no language server available for this file)." };
741
+ } catch (e) {
742
+ return { output: String(e), isError: true };
743
+ }
744
+ },
745
+ },
746
+ ];
747
+
748
+ export const toolByName = new Map(tools.map((t) => [t.name, t]));
749
+
750
+ /** Register a dynamic tool (from an extension, skill, or MCP server). Last registration wins. */
751
+ export function registerTool(t: Tool): void {
752
+ const existing = tools.findIndex((x) => x.name === t.name);
753
+ if (existing >= 0) tools.splice(existing, 1);
754
+ tools.push(t);
755
+ toolByName.set(t.name, t);
756
+ }