@build-astron-co/nimbus 0.4.1 → 0.4.3

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 (435) hide show
  1. package/CHANGELOG.md +268 -89
  2. package/README.md +26 -567
  3. package/dist/src/agent/compaction-agent.js +24 -12
  4. package/dist/src/agent/context-manager.js +2 -1
  5. package/dist/src/agent/expand-files.js +2 -1
  6. package/dist/src/agent/loop.js +71 -33
  7. package/dist/src/agent/permissions.js +4 -2
  8. package/dist/src/agent/system-prompt.js +34 -17
  9. package/dist/src/app.js +1 -1
  10. package/dist/src/auth/keychain.js +8 -4
  11. package/dist/src/auth/store.js +70 -107
  12. package/dist/src/cli/init.js +35 -19
  13. package/dist/src/cli/run.js +18 -10
  14. package/dist/src/cli/serve.js +4 -2
  15. package/dist/src/cli.js +52 -11
  16. package/dist/src/commands/alias.js +5 -3
  17. package/dist/src/commands/audit/index.js +2 -1
  18. package/dist/src/commands/aws-terraform.js +36 -18
  19. package/dist/src/commands/completions.js +1 -1
  20. package/dist/src/commands/config.js +3 -2
  21. package/dist/src/commands/connect-github.js +92 -0
  22. package/dist/src/commands/cost/index.js +3 -2
  23. package/dist/src/commands/deploy.js +15 -10
  24. package/dist/src/commands/doctor.js +9 -6
  25. package/dist/src/commands/drift/index.js +2 -1
  26. package/dist/src/commands/export.js +5 -3
  27. package/dist/src/commands/generate-terraform.js +110 -2
  28. package/dist/src/commands/import.js +3 -3
  29. package/dist/src/commands/incident.js +10 -5
  30. package/dist/src/commands/login.js +8 -93
  31. package/dist/src/commands/logs.js +16 -8
  32. package/dist/src/commands/onboarding.js +6 -4
  33. package/dist/src/commands/pipeline.js +6 -3
  34. package/dist/src/commands/plugin.js +3 -2
  35. package/dist/src/commands/profile.js +27 -14
  36. package/dist/src/commands/questionnaire.js +1 -1
  37. package/dist/src/commands/rollback.js +3 -2
  38. package/dist/src/commands/rollout.js +5 -3
  39. package/dist/src/commands/runbook.js +17 -10
  40. package/dist/src/commands/schedule.js +10 -5
  41. package/dist/src/commands/status.js +2 -1
  42. package/dist/src/commands/team-context.js +12 -7
  43. package/dist/src/commands/template.js +1 -1
  44. package/dist/src/commands/tf/index.js +6 -3
  45. package/dist/src/commands/upgrade.js +5 -3
  46. package/dist/src/commands/version.js +6 -3
  47. package/dist/src/commands/watch.js +6 -3
  48. package/dist/src/compat/sqlite.js +5 -3
  49. package/dist/src/config/mode-store.js +2 -1
  50. package/dist/src/config/profiles.js +4 -2
  51. package/dist/src/config/types.js +2 -1
  52. package/dist/src/engine/executor.js +8 -4
  53. package/dist/src/engine/planner.js +9 -5
  54. package/dist/src/llm/providers/anthropic.js +6 -3
  55. package/dist/src/llm/providers/ollama.js +1 -1
  56. package/dist/src/llm/router.js +22 -7
  57. package/dist/src/nimbus.js +1 -0
  58. package/dist/src/sessions/manager.js +6 -3
  59. package/dist/src/sharing/viewer.js +2 -1
  60. package/dist/src/tools/file-ops.js +1 -2
  61. package/dist/src/tools/schemas/devops.js +197 -108
  62. package/dist/src/tools/schemas/standard.js +1 -1
  63. package/dist/src/ui/App.js +25 -13
  64. package/dist/src/ui/FileDiffModal.js +22 -11
  65. package/dist/src/ui/HelpModal.js +2 -1
  66. package/dist/src/ui/InputBox.js +6 -3
  67. package/dist/src/ui/MessageList.js +40 -20
  68. package/dist/src/ui/TerminalPane.js +2 -1
  69. package/dist/src/ui/ToolCallDisplay.js +12 -6
  70. package/dist/src/ui/TreePane.js +2 -1
  71. package/dist/src/ui/ink/index.js +37 -21
  72. package/dist/src/version.js +1 -1
  73. package/dist/src/watcher/index.js +8 -4
  74. package/package.json +3 -5
  75. package/src/__tests__/alias.test.ts +0 -133
  76. package/src/__tests__/app.test.ts +0 -76
  77. package/src/__tests__/audit.test.ts +0 -877
  78. package/src/__tests__/circuit-breaker.test.ts +0 -116
  79. package/src/__tests__/cli-run.test.ts +0 -351
  80. package/src/__tests__/compat-sqlite.test.ts +0 -68
  81. package/src/__tests__/context-manager.test.ts +0 -632
  82. package/src/__tests__/context.test.ts +0 -242
  83. package/src/__tests__/devops-terminal-gaps.test.ts +0 -718
  84. package/src/__tests__/doctor.test.ts +0 -48
  85. package/src/__tests__/enterprise.test.ts +0 -401
  86. package/src/__tests__/export.test.ts +0 -236
  87. package/src/__tests__/gap-11-18-20.test.ts +0 -958
  88. package/src/__tests__/generator.test.ts +0 -433
  89. package/src/__tests__/helm-streaming.test.ts +0 -127
  90. package/src/__tests__/hooks.test.ts +0 -582
  91. package/src/__tests__/incident.test.ts +0 -179
  92. package/src/__tests__/init.test.ts +0 -487
  93. package/src/__tests__/intent-parser.test.ts +0 -229
  94. package/src/__tests__/llm-router.test.ts +0 -209
  95. package/src/__tests__/logs.test.ts +0 -107
  96. package/src/__tests__/loop-errors.test.ts +0 -244
  97. package/src/__tests__/lsp.test.ts +0 -293
  98. package/src/__tests__/modes.test.ts +0 -336
  99. package/src/__tests__/perf-optimizations.test.ts +0 -847
  100. package/src/__tests__/permissions.test.ts +0 -338
  101. package/src/__tests__/pipeline.test.ts +0 -50
  102. package/src/__tests__/polish-phase3.test.ts +0 -340
  103. package/src/__tests__/profile.test.ts +0 -237
  104. package/src/__tests__/rollback.test.ts +0 -83
  105. package/src/__tests__/runbook.test.ts +0 -219
  106. package/src/__tests__/schedule.test.ts +0 -206
  107. package/src/__tests__/serve.test.ts +0 -275
  108. package/src/__tests__/sessions.test.ts +0 -322
  109. package/src/__tests__/sharing.test.ts +0 -340
  110. package/src/__tests__/snapshots.test.ts +0 -581
  111. package/src/__tests__/standalone-migration.test.ts +0 -199
  112. package/src/__tests__/state-db.test.ts +0 -334
  113. package/src/__tests__/status.test.ts +0 -158
  114. package/src/__tests__/stream-with-tools.test.ts +0 -778
  115. package/src/__tests__/subagents.test.ts +0 -176
  116. package/src/__tests__/system-prompt.test.ts +0 -248
  117. package/src/__tests__/terminal-gap-v2.test.ts +0 -395
  118. package/src/__tests__/terminal-parity.test.ts +0 -393
  119. package/src/__tests__/tf-apply.test.ts +0 -187
  120. package/src/__tests__/tool-converter.test.ts +0 -256
  121. package/src/__tests__/tool-schemas.test.ts +0 -602
  122. package/src/__tests__/tools.test.ts +0 -144
  123. package/src/__tests__/version-json.test.ts +0 -184
  124. package/src/__tests__/version.test.ts +0 -49
  125. package/src/__tests__/watch.test.ts +0 -129
  126. package/src/agent/compaction-agent.ts +0 -266
  127. package/src/agent/context-manager.ts +0 -499
  128. package/src/agent/context.ts +0 -427
  129. package/src/agent/deploy-preview.ts +0 -487
  130. package/src/agent/expand-files.ts +0 -108
  131. package/src/agent/index.ts +0 -68
  132. package/src/agent/loop.ts +0 -1998
  133. package/src/agent/modes.ts +0 -429
  134. package/src/agent/permissions.ts +0 -513
  135. package/src/agent/subagents/base.ts +0 -116
  136. package/src/agent/subagents/cost.ts +0 -51
  137. package/src/agent/subagents/explore.ts +0 -42
  138. package/src/agent/subagents/general.ts +0 -54
  139. package/src/agent/subagents/index.ts +0 -102
  140. package/src/agent/subagents/infra.ts +0 -59
  141. package/src/agent/subagents/security.ts +0 -69
  142. package/src/agent/system-prompt.ts +0 -990
  143. package/src/app.ts +0 -180
  144. package/src/audit/activity-log.ts +0 -290
  145. package/src/audit/compliance-checker.ts +0 -540
  146. package/src/audit/cost-tracker.ts +0 -318
  147. package/src/audit/index.ts +0 -23
  148. package/src/audit/security-scanner.ts +0 -641
  149. package/src/auth/guard.ts +0 -75
  150. package/src/auth/index.ts +0 -56
  151. package/src/auth/keychain.ts +0 -82
  152. package/src/auth/oauth.ts +0 -465
  153. package/src/auth/providers.ts +0 -470
  154. package/src/auth/sso.ts +0 -113
  155. package/src/auth/store.ts +0 -505
  156. package/src/auth/types.ts +0 -187
  157. package/src/build.ts +0 -141
  158. package/src/cli/index.ts +0 -16
  159. package/src/cli/init.ts +0 -1227
  160. package/src/cli/openapi-spec.ts +0 -356
  161. package/src/cli/run.ts +0 -628
  162. package/src/cli/serve-auth.ts +0 -80
  163. package/src/cli/serve.ts +0 -539
  164. package/src/cli/web.ts +0 -71
  165. package/src/cli.ts +0 -1728
  166. package/src/clients/core-engine-client.ts +0 -227
  167. package/src/clients/enterprise-client.ts +0 -334
  168. package/src/clients/generator-client.ts +0 -351
  169. package/src/clients/git-client.ts +0 -627
  170. package/src/clients/github-client.ts +0 -410
  171. package/src/clients/helm-client.ts +0 -504
  172. package/src/clients/index.ts +0 -80
  173. package/src/clients/k8s-client.ts +0 -497
  174. package/src/clients/llm-client.ts +0 -161
  175. package/src/clients/rest-client.ts +0 -130
  176. package/src/clients/service-discovery.ts +0 -38
  177. package/src/clients/terraform-client.ts +0 -482
  178. package/src/clients/tools-client.ts +0 -1843
  179. package/src/clients/ws-client.ts +0 -115
  180. package/src/commands/alias.ts +0 -100
  181. package/src/commands/analyze/index.ts +0 -352
  182. package/src/commands/apply/helm.ts +0 -473
  183. package/src/commands/apply/index.ts +0 -213
  184. package/src/commands/apply/k8s.ts +0 -454
  185. package/src/commands/apply/terraform.ts +0 -582
  186. package/src/commands/ask.ts +0 -167
  187. package/src/commands/audit/index.ts +0 -357
  188. package/src/commands/auth-cloud.ts +0 -407
  189. package/src/commands/auth-list.ts +0 -134
  190. package/src/commands/auth-profile.ts +0 -121
  191. package/src/commands/auth-refresh.ts +0 -187
  192. package/src/commands/auth-status.ts +0 -141
  193. package/src/commands/aws/ec2.ts +0 -501
  194. package/src/commands/aws/iam.ts +0 -397
  195. package/src/commands/aws/index.ts +0 -133
  196. package/src/commands/aws/lambda.ts +0 -396
  197. package/src/commands/aws/rds.ts +0 -439
  198. package/src/commands/aws/s3.ts +0 -439
  199. package/src/commands/aws/vpc.ts +0 -393
  200. package/src/commands/aws-discover.ts +0 -542
  201. package/src/commands/aws-terraform.ts +0 -755
  202. package/src/commands/azure/aks.ts +0 -376
  203. package/src/commands/azure/functions.ts +0 -253
  204. package/src/commands/azure/index.ts +0 -116
  205. package/src/commands/azure/storage.ts +0 -478
  206. package/src/commands/azure/vm.ts +0 -355
  207. package/src/commands/billing/index.ts +0 -256
  208. package/src/commands/chat.ts +0 -320
  209. package/src/commands/completions.ts +0 -268
  210. package/src/commands/config.ts +0 -372
  211. package/src/commands/cost/cloud-cost-estimator.ts +0 -266
  212. package/src/commands/cost/estimator.ts +0 -79
  213. package/src/commands/cost/index.ts +0 -810
  214. package/src/commands/cost/parsers/terraform.ts +0 -273
  215. package/src/commands/cost/parsers/types.ts +0 -25
  216. package/src/commands/cost/pricing/aws.ts +0 -544
  217. package/src/commands/cost/pricing/azure.ts +0 -499
  218. package/src/commands/cost/pricing/gcp.ts +0 -396
  219. package/src/commands/cost/pricing/index.ts +0 -40
  220. package/src/commands/demo.ts +0 -250
  221. package/src/commands/deploy.ts +0 -260
  222. package/src/commands/doctor.ts +0 -1386
  223. package/src/commands/drift/index.ts +0 -787
  224. package/src/commands/explain.ts +0 -277
  225. package/src/commands/export.ts +0 -146
  226. package/src/commands/feedback.ts +0 -389
  227. package/src/commands/fix.ts +0 -324
  228. package/src/commands/fs/index.ts +0 -402
  229. package/src/commands/gcp/compute.ts +0 -325
  230. package/src/commands/gcp/functions.ts +0 -271
  231. package/src/commands/gcp/gke.ts +0 -438
  232. package/src/commands/gcp/iam.ts +0 -344
  233. package/src/commands/gcp/index.ts +0 -129
  234. package/src/commands/gcp/storage.ts +0 -284
  235. package/src/commands/generate-helm.ts +0 -1249
  236. package/src/commands/generate-k8s.ts +0 -1508
  237. package/src/commands/generate-terraform.ts +0 -1202
  238. package/src/commands/gh/index.ts +0 -863
  239. package/src/commands/git/index.ts +0 -1343
  240. package/src/commands/helm/index.ts +0 -1126
  241. package/src/commands/help.ts +0 -715
  242. package/src/commands/history.ts +0 -149
  243. package/src/commands/import.ts +0 -868
  244. package/src/commands/incident.ts +0 -166
  245. package/src/commands/index.ts +0 -367
  246. package/src/commands/init.ts +0 -1051
  247. package/src/commands/k8s/index.ts +0 -1137
  248. package/src/commands/login.ts +0 -716
  249. package/src/commands/logout.ts +0 -83
  250. package/src/commands/logs.ts +0 -167
  251. package/src/commands/onboarding.ts +0 -405
  252. package/src/commands/pipeline.ts +0 -186
  253. package/src/commands/plan/display.ts +0 -279
  254. package/src/commands/plan/index.ts +0 -599
  255. package/src/commands/plugin.ts +0 -398
  256. package/src/commands/preview.ts +0 -452
  257. package/src/commands/profile.ts +0 -342
  258. package/src/commands/questionnaire.ts +0 -1172
  259. package/src/commands/resume.ts +0 -47
  260. package/src/commands/rollback.ts +0 -315
  261. package/src/commands/rollout.ts +0 -88
  262. package/src/commands/runbook.ts +0 -346
  263. package/src/commands/schedule.ts +0 -236
  264. package/src/commands/status.ts +0 -252
  265. package/src/commands/team/index.ts +0 -346
  266. package/src/commands/team-context.ts +0 -220
  267. package/src/commands/template.ts +0 -233
  268. package/src/commands/tf/index.ts +0 -1093
  269. package/src/commands/upgrade.ts +0 -607
  270. package/src/commands/usage/index.ts +0 -134
  271. package/src/commands/version.ts +0 -174
  272. package/src/commands/watch.ts +0 -153
  273. package/src/compat/index.ts +0 -2
  274. package/src/compat/runtime.ts +0 -12
  275. package/src/compat/sqlite.ts +0 -177
  276. package/src/config/index.ts +0 -17
  277. package/src/config/manager.ts +0 -530
  278. package/src/config/mode-store.ts +0 -62
  279. package/src/config/profiles.ts +0 -84
  280. package/src/config/safety-policy.ts +0 -358
  281. package/src/config/schema.ts +0 -125
  282. package/src/config/types.ts +0 -609
  283. package/src/config/workspace-state.ts +0 -53
  284. package/src/context/context-db.ts +0 -199
  285. package/src/demo/index.ts +0 -349
  286. package/src/demo/scenarios/full-journey.ts +0 -229
  287. package/src/demo/scenarios/getting-started.ts +0 -127
  288. package/src/demo/scenarios/helm-release.ts +0 -341
  289. package/src/demo/scenarios/k8s-deployment.ts +0 -194
  290. package/src/demo/scenarios/terraform-vpc.ts +0 -170
  291. package/src/demo/types.ts +0 -92
  292. package/src/engine/cost-estimator.ts +0 -480
  293. package/src/engine/diagram-generator.ts +0 -256
  294. package/src/engine/drift-detector.ts +0 -902
  295. package/src/engine/executor.ts +0 -1066
  296. package/src/engine/index.ts +0 -76
  297. package/src/engine/orchestrator.ts +0 -636
  298. package/src/engine/planner.ts +0 -787
  299. package/src/engine/safety.ts +0 -743
  300. package/src/engine/verifier.ts +0 -770
  301. package/src/enterprise/audit.ts +0 -348
  302. package/src/enterprise/auth.ts +0 -270
  303. package/src/enterprise/billing.ts +0 -822
  304. package/src/enterprise/index.ts +0 -17
  305. package/src/enterprise/teams.ts +0 -443
  306. package/src/generator/best-practices.ts +0 -1608
  307. package/src/generator/helm.ts +0 -630
  308. package/src/generator/index.ts +0 -37
  309. package/src/generator/intent-parser.ts +0 -514
  310. package/src/generator/kubernetes.ts +0 -976
  311. package/src/generator/terraform.ts +0 -1875
  312. package/src/history/index.ts +0 -8
  313. package/src/history/manager.ts +0 -250
  314. package/src/history/types.ts +0 -34
  315. package/src/hooks/config.ts +0 -432
  316. package/src/hooks/engine.ts +0 -392
  317. package/src/hooks/index.ts +0 -4
  318. package/src/llm/auth-bridge.ts +0 -198
  319. package/src/llm/circuit-breaker.ts +0 -140
  320. package/src/llm/config-loader.ts +0 -201
  321. package/src/llm/cost-calculator.ts +0 -171
  322. package/src/llm/index.ts +0 -8
  323. package/src/llm/model-aliases.ts +0 -115
  324. package/src/llm/provider-registry.ts +0 -63
  325. package/src/llm/providers/anthropic.ts +0 -462
  326. package/src/llm/providers/bedrock.ts +0 -477
  327. package/src/llm/providers/google.ts +0 -405
  328. package/src/llm/providers/ollama.ts +0 -767
  329. package/src/llm/providers/openai-compatible.ts +0 -340
  330. package/src/llm/providers/openai.ts +0 -328
  331. package/src/llm/providers/openrouter.ts +0 -338
  332. package/src/llm/router.ts +0 -1104
  333. package/src/llm/types.ts +0 -232
  334. package/src/lsp/client.ts +0 -298
  335. package/src/lsp/languages.ts +0 -119
  336. package/src/lsp/manager.ts +0 -294
  337. package/src/mcp/client.ts +0 -402
  338. package/src/mcp/index.ts +0 -5
  339. package/src/mcp/manager.ts +0 -133
  340. package/src/nimbus.ts +0 -233
  341. package/src/plugins/index.ts +0 -27
  342. package/src/plugins/loader.ts +0 -334
  343. package/src/plugins/manager.ts +0 -376
  344. package/src/plugins/types.ts +0 -284
  345. package/src/scanners/cicd-scanner.ts +0 -258
  346. package/src/scanners/cloud-scanner.ts +0 -466
  347. package/src/scanners/framework-scanner.ts +0 -469
  348. package/src/scanners/iac-scanner.ts +0 -388
  349. package/src/scanners/index.ts +0 -539
  350. package/src/scanners/language-scanner.ts +0 -276
  351. package/src/scanners/package-manager-scanner.ts +0 -277
  352. package/src/scanners/types.ts +0 -172
  353. package/src/sessions/manager.ts +0 -472
  354. package/src/sessions/types.ts +0 -44
  355. package/src/sharing/sync.ts +0 -300
  356. package/src/sharing/viewer.ts +0 -163
  357. package/src/snapshots/index.ts +0 -2
  358. package/src/snapshots/manager.ts +0 -530
  359. package/src/state/artifacts.ts +0 -147
  360. package/src/state/audit.ts +0 -137
  361. package/src/state/billing.ts +0 -240
  362. package/src/state/checkpoints.ts +0 -117
  363. package/src/state/config.ts +0 -67
  364. package/src/state/conversations.ts +0 -14
  365. package/src/state/credentials.ts +0 -154
  366. package/src/state/db.ts +0 -58
  367. package/src/state/index.ts +0 -26
  368. package/src/state/messages.ts +0 -115
  369. package/src/state/projects.ts +0 -123
  370. package/src/state/schema.ts +0 -236
  371. package/src/state/sessions.ts +0 -147
  372. package/src/state/teams.ts +0 -200
  373. package/src/telemetry.ts +0 -108
  374. package/src/tools/aws-ops.ts +0 -952
  375. package/src/tools/azure-ops.ts +0 -579
  376. package/src/tools/file-ops.ts +0 -615
  377. package/src/tools/gcp-ops.ts +0 -625
  378. package/src/tools/git-ops.ts +0 -773
  379. package/src/tools/github-ops.ts +0 -799
  380. package/src/tools/helm-ops.ts +0 -943
  381. package/src/tools/index.ts +0 -17
  382. package/src/tools/k8s-ops.ts +0 -819
  383. package/src/tools/schemas/converter.ts +0 -184
  384. package/src/tools/schemas/devops.ts +0 -3502
  385. package/src/tools/schemas/index.ts +0 -73
  386. package/src/tools/schemas/standard.ts +0 -1148
  387. package/src/tools/schemas/types.ts +0 -735
  388. package/src/tools/spawn-exec.ts +0 -148
  389. package/src/tools/terraform-ops.ts +0 -862
  390. package/src/types/ambient.d.ts +0 -193
  391. package/src/types/config.ts +0 -83
  392. package/src/types/drift.ts +0 -116
  393. package/src/types/enterprise.ts +0 -335
  394. package/src/types/index.ts +0 -20
  395. package/src/types/plan.ts +0 -44
  396. package/src/types/request.ts +0 -65
  397. package/src/types/response.ts +0 -54
  398. package/src/types/service.ts +0 -51
  399. package/src/ui/App.tsx +0 -2114
  400. package/src/ui/DeployPreview.tsx +0 -174
  401. package/src/ui/FileDiffModal.tsx +0 -162
  402. package/src/ui/Header.tsx +0 -131
  403. package/src/ui/HelpModal.tsx +0 -57
  404. package/src/ui/InputBox.tsx +0 -503
  405. package/src/ui/MessageList.tsx +0 -1032
  406. package/src/ui/PermissionPrompt.tsx +0 -163
  407. package/src/ui/StatusBar.tsx +0 -277
  408. package/src/ui/TerminalPane.tsx +0 -84
  409. package/src/ui/ToolCallDisplay.tsx +0 -643
  410. package/src/ui/TreePane.tsx +0 -132
  411. package/src/ui/chat-ui.ts +0 -850
  412. package/src/ui/index.ts +0 -33
  413. package/src/ui/ink/index.ts +0 -1444
  414. package/src/ui/streaming.ts +0 -176
  415. package/src/ui/theme.ts +0 -104
  416. package/src/ui/types.ts +0 -75
  417. package/src/utils/analytics.ts +0 -72
  418. package/src/utils/cost-warning.ts +0 -27
  419. package/src/utils/env.ts +0 -46
  420. package/src/utils/errors.ts +0 -69
  421. package/src/utils/event-bus.ts +0 -38
  422. package/src/utils/index.ts +0 -24
  423. package/src/utils/logger.ts +0 -171
  424. package/src/utils/rate-limiter.ts +0 -121
  425. package/src/utils/service-auth.ts +0 -49
  426. package/src/utils/validation.ts +0 -53
  427. package/src/version.ts +0 -4
  428. package/src/watcher/index.ts +0 -214
  429. package/src/wizard/approval.ts +0 -383
  430. package/src/wizard/index.ts +0 -25
  431. package/src/wizard/prompts.ts +0 -338
  432. package/src/wizard/types.ts +0 -172
  433. package/src/wizard/ui.ts +0 -556
  434. package/src/wizard/wizard.ts +0 -304
  435. package/tsconfig.json +0 -24
@@ -1,1148 +0,0 @@
1
- /**
2
- * Standard Tool Definitions
3
- *
4
- * Defines the 11 standard coding tools available to the Nimbus agentic loop.
5
- * Each tool wraps existing filesystem operations (src/tools/file-ops.ts) or
6
- * uses child_process for shell commands.
7
- *
8
- * Tools:
9
- * read_file, edit_file, multi_edit, write_file, bash,
10
- * glob, grep, list_dir, webfetch, todo_read, todo_write
11
- */
12
-
13
- import { z } from 'zod';
14
- import * as fs from 'node:fs/promises';
15
- import * as path from 'node:path';
16
- import { spawn, exec } from 'node:child_process';
17
- import { promisify } from 'node:util';
18
- import { glob as fastGlob } from 'fast-glob';
19
- import type { ToolDefinition, ToolResult } from './types';
20
-
21
- const execAsync = promisify(exec);
22
-
23
- /**
24
- * Execute a shell command using spawn with process group management.
25
- * On timeout, kills the entire process group to avoid orphaned children.
26
- */
27
- function spawnAsync(
28
- command: string,
29
- options: { timeout?: number; cwd?: string; maxBuffer?: number; signal?: AbortSignal; onChunk?: (chunk: string) => void }
30
- ): Promise<{ stdout: string; stderr: string }> {
31
- return new Promise((resolve, reject) => {
32
- const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
33
- const child = spawn('sh', ['-c', command], {
34
- cwd: options.cwd,
35
- stdio: ['ignore', 'pipe', 'pipe'],
36
- // Create a new process group so we can kill the whole tree
37
- detached: true,
38
- });
39
-
40
- let stdout = '';
41
- let stderr = '';
42
- let killed = false;
43
- let stdoutOverflow = false;
44
- let stderrOverflow = false;
45
-
46
- // Kill process group helper
47
- const killProcessGroup = () => {
48
- killed = true;
49
- try {
50
- if (child.pid) {
51
- process.kill(-child.pid, 'SIGTERM');
52
- setTimeout(() => {
53
- try {
54
- if (child.pid) {
55
- process.kill(-child.pid, 'SIGKILL');
56
- }
57
- } catch {
58
- /* already dead */
59
- }
60
- }, 2000);
61
- }
62
- } catch {
63
- /* Process already exited */
64
- }
65
- };
66
-
67
- // Wire AbortSignal (Ctrl+C) to kill the child process group
68
- if (options.signal) {
69
- if (options.signal.aborted) {
70
- killProcessGroup();
71
- } else {
72
- options.signal.addEventListener('abort', killProcessGroup, { once: true });
73
- }
74
- }
75
-
76
- child.stdout?.on('data', (data: Buffer) => {
77
- options.onChunk?.(data.toString());
78
- if (stdout.length + data.length > maxBuffer) {
79
- stdoutOverflow = true;
80
- return;
81
- }
82
- stdout += data.toString();
83
- });
84
-
85
- child.stderr?.on('data', (data: Buffer) => {
86
- options.onChunk?.(data.toString());
87
- if (stderr.length + data.length > maxBuffer) {
88
- stderrOverflow = true;
89
- return;
90
- }
91
- stderr += data.toString();
92
- });
93
-
94
- const timer = options.timeout
95
- ? setTimeout(() => {
96
- killProcessGroup();
97
- }, options.timeout)
98
- : null;
99
-
100
- child.on('close', code => {
101
- if (timer) {
102
- clearTimeout(timer);
103
- }
104
- // Clean up abort listener
105
- if (options.signal) {
106
- options.signal.removeEventListener('abort', killProcessGroup);
107
- }
108
-
109
- if (killed) {
110
- const err = new Error(
111
- options.signal?.aborted
112
- ? 'Command aborted by user'
113
- : `Command timed out after ${options.timeout}ms`
114
- ) as Error & { stdout: string; stderr: string };
115
- err.stdout = stdout;
116
- err.stderr = stderr;
117
- reject(err);
118
- return;
119
- }
120
-
121
- if (stdoutOverflow || stderrOverflow) {
122
- stdout += '\n[Output truncated: exceeded buffer limit]';
123
- }
124
-
125
- if (code !== 0) {
126
- const err = new Error(`Command exited with code ${code}`) as Error & {
127
- stdout: string;
128
- stderr: string;
129
- code: number;
130
- };
131
- err.stdout = stdout;
132
- err.stderr = stderr;
133
- err.code = code!;
134
- reject(err);
135
- return;
136
- }
137
-
138
- resolve({ stdout, stderr });
139
- });
140
-
141
- child.on('error', error => {
142
- if (timer) {
143
- clearTimeout(timer);
144
- }
145
- reject(error);
146
- });
147
- });
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // Helpers
152
- // ---------------------------------------------------------------------------
153
-
154
- /** Build a successful ToolResult. */
155
- function ok(output: string): ToolResult {
156
- return { output, isError: false };
157
- }
158
-
159
- /** Build an error ToolResult. */
160
- function err(message: string): ToolResult {
161
- return { output: '', error: message, isError: true };
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // 1. read_file
166
- // ---------------------------------------------------------------------------
167
-
168
- const readFileSchema = z.object({
169
- path: z.string().describe('Absolute or relative path to the file to read'),
170
- offset: z.number().optional().describe('1-based line number to start reading from'),
171
- limit: z.number().optional().describe('Maximum number of lines to return'),
172
- });
173
-
174
- /** Patterns for sensitive files that should be blocked from read_file. */
175
- const SENSITIVE_FILE_PATTERNS = [
176
- /\.env($|\.)/i, // .env, .env.local, .env.production, etc.
177
- /credentials\.json$/i, // GCP credentials
178
- /\.aws\/credentials$/i, // AWS credentials
179
- /\.ssh\/(id_|known_hosts|config)/i, // SSH keys and config
180
- /\.gnupg\//i, // GPG keys
181
- /\.netrc$/i, // Network credentials
182
- /secret[s]?\.ya?ml$/i, // Kubernetes secrets
183
- /\.pem$/i, // SSL certificates
184
- /\.key$/i, // Private keys
185
- /\.p12$/i, // PKCS#12 certificates
186
- /\.keystore$/i, // Java keystores
187
- /\/\.git\/config$/i, // Git config (may contain tokens)
188
- /token[s]?\.json$/i, // OAuth tokens
189
- ];
190
-
191
- function isSensitivePath(filePath: string): boolean {
192
- const normalized = filePath.replace(/\\/g, '/');
193
- return SENSITIVE_FILE_PATTERNS.some(pattern => pattern.test(normalized));
194
- }
195
-
196
- export const readFileTool: ToolDefinition = {
197
- name: 'read_file',
198
- description: 'Read file contents. Returns the text content of a file at the given path.',
199
- inputSchema: readFileSchema,
200
- permissionTier: 'auto_allow',
201
- category: 'standard',
202
-
203
- async execute(raw: unknown): Promise<ToolResult> {
204
- try {
205
- const input = readFileSchema.parse(raw);
206
- const resolved = path.resolve(input.path);
207
-
208
- // Block reading of sensitive files (credentials, keys, secrets)
209
- if (isSensitivePath(resolved)) {
210
- return err(
211
- `Blocked: ${path.basename(resolved)} appears to be a sensitive file (credentials, secrets, or keys). ` +
212
- `Reading it could expose secrets in the conversation history.`
213
- );
214
- }
215
-
216
- // Detect image files — return base64-encoded data for multimodal LLM support
217
- const ext = path.extname(resolved).toLowerCase();
218
- const imageExts = new Set([
219
- '.png',
220
- '.jpg',
221
- '.jpeg',
222
- '.gif',
223
- '.bmp',
224
- '.webp',
225
- '.svg',
226
- '.ico',
227
- '.tiff',
228
- '.tif',
229
- ]);
230
- // Subset that LLMs can actually process as vision input
231
- const MULTIMODAL_EXTS: Record<string, string> = {
232
- '.png': 'image/png',
233
- '.jpg': 'image/jpeg',
234
- '.jpeg': 'image/jpeg',
235
- '.gif': 'image/gif',
236
- '.webp': 'image/webp',
237
- };
238
-
239
- if (imageExts.has(ext)) {
240
- try {
241
- const stats = await fs.stat(resolved);
242
- const sizeStr =
243
- stats.size < 1024
244
- ? `${stats.size} B`
245
- : stats.size < 1024 * 1024
246
- ? `${(stats.size / 1024).toFixed(1)} KB`
247
- : `${(stats.size / (1024 * 1024)).toFixed(1)} MB`;
248
-
249
- const mediaType = MULTIMODAL_EXTS[ext];
250
- // If the image is a supported multimodal format and under 20MB, include base64
251
- if (mediaType && stats.size < 20 * 1024 * 1024) {
252
- const buffer = await fs.readFile(resolved);
253
- const base64 = buffer.toString('base64');
254
- return ok(
255
- `[Image file: ${path.basename(resolved)}]\nType: ${ext.slice(1).toUpperCase()}\nSize: ${sizeStr}\nPath: ${resolved}\n\n` +
256
- `<image_data media_type="${mediaType}" encoding="base64">${base64}</image_data>`
257
- );
258
- }
259
-
260
- return ok(
261
- `[Image file: ${path.basename(resolved)}]\nType: ${ext.slice(1).toUpperCase()}\nSize: ${sizeStr}\nPath: ${resolved}\n\nNote: Image content cannot be displayed in text mode. Use an image viewer or IDE to see the contents.`
262
- );
263
- } catch {
264
- return ok(
265
- `[Image file: ${path.basename(resolved)}]\nPath: ${resolved}\n\nNote: Image content cannot be displayed in text mode.`
266
- );
267
- }
268
- }
269
-
270
- // Detect Jupyter notebooks
271
- if (ext === '.ipynb') {
272
- try {
273
- const nbRaw = await fs.readFile(resolved, 'utf-8');
274
- const notebook = JSON.parse(nbRaw) as {
275
- cells?: Array<{ cell_type: string; source: string[]; outputs?: any[] }>;
276
- };
277
-
278
- if (!notebook.cells || !Array.isArray(notebook.cells)) {
279
- return ok(nbRaw); // Malformed notebook, return raw JSON
280
- }
281
-
282
- const parts: string[] = [`# Jupyter Notebook: ${path.basename(resolved)}\n`];
283
-
284
- for (let i = 0; i < notebook.cells.length; i++) {
285
- const cell = notebook.cells[i];
286
- const source = Array.isArray(cell.source)
287
- ? cell.source.join('')
288
- : String(cell.source || '');
289
-
290
- if (cell.cell_type === 'markdown') {
291
- parts.push(`## Cell ${i + 1} [Markdown]\n${source}\n`);
292
- } else if (cell.cell_type === 'code') {
293
- parts.push(`## Cell ${i + 1} [Code]\n\`\`\`python\n${source}\n\`\`\`\n`);
294
-
295
- // Show outputs
296
- if (cell.outputs && Array.isArray(cell.outputs)) {
297
- for (const output of cell.outputs) {
298
- if (output.text) {
299
- const text = Array.isArray(output.text)
300
- ? output.text.join('')
301
- : String(output.text);
302
- parts.push(`Output:\n\`\`\`\n${text}\n\`\`\`\n`);
303
- } else if (output.data?.['text/plain']) {
304
- const text = Array.isArray(output.data['text/plain'])
305
- ? output.data['text/plain'].join('')
306
- : String(output.data['text/plain']);
307
- parts.push(`Output:\n\`\`\`\n${text}\n\`\`\`\n`);
308
- } else if (output.data?.['image/png'] || output.data?.['image/jpeg']) {
309
- parts.push(`Output: [Image output — cannot display in text mode]\n`);
310
- }
311
- }
312
- }
313
- } else if (cell.cell_type === 'raw') {
314
- parts.push(`## Cell ${i + 1} [Raw]\n${source}\n`);
315
- }
316
- }
317
-
318
- return ok(parts.join('\n'));
319
- } catch {
320
- // If notebook parsing fails, fall through to normal file read
321
- }
322
- }
323
-
324
- const content = await fs.readFile(resolved, 'utf-8');
325
-
326
- if (input.offset !== undefined || input.limit !== undefined) {
327
- const lines = content.split('\n');
328
- const start = (input.offset ?? 1) - 1; // convert to 0-based
329
- const end = input.limit !== undefined ? start + input.limit : lines.length;
330
- return ok(lines.slice(start, end).join('\n'));
331
- }
332
-
333
- return ok(content);
334
- } catch (error: unknown) {
335
- const message = error instanceof Error ? error.message : String(error);
336
- return err(`Failed to read file: ${message}`);
337
- }
338
- },
339
- };
340
-
341
- // ---------------------------------------------------------------------------
342
- // 2. edit_file
343
- // ---------------------------------------------------------------------------
344
-
345
- const editFileSchema = z.object({
346
- path: z.string().describe('Path to the file to edit'),
347
- old_string: z.string().describe('Exact text to find'),
348
- new_string: z.string().describe('Replacement text'),
349
- replace_all: z
350
- .boolean()
351
- .optional()
352
- .default(false)
353
- .describe('Replace all occurrences instead of just the first'),
354
- });
355
-
356
- export const editFileTool: ToolDefinition = {
357
- name: 'edit_file',
358
- description:
359
- 'Make a precise text replacement in a file. By default replaces the first occurrence of old_string with new_string. Set replace_all to true to replace every occurrence.',
360
- inputSchema: editFileSchema,
361
- permissionTier: 'ask_once',
362
- category: 'standard',
363
- isDestructive: true,
364
-
365
- async execute(raw: unknown): Promise<ToolResult> {
366
- try {
367
- const input = editFileSchema.parse(raw);
368
- const content = await fs.readFile(input.path, 'utf-8');
369
-
370
- if (!content.includes(input.old_string)) {
371
- return err(
372
- `old_string not found in ${input.path}. Make sure the string matches exactly, including whitespace and indentation.`
373
- );
374
- }
375
-
376
- let updated: string;
377
- if (input.replace_all) {
378
- // Count occurrences for the success message
379
- let count = 0;
380
- let searchPos = 0;
381
- for (;;) {
382
- const idx = content.indexOf(input.old_string, searchPos);
383
- if (idx === -1) {
384
- break;
385
- }
386
- count++;
387
- searchPos = idx + input.old_string.length;
388
- }
389
- updated = content.replaceAll(input.old_string, input.new_string);
390
- await fs.writeFile(input.path, updated, 'utf-8');
391
- return ok(
392
- `Successfully replaced ${count} occurrence${count !== 1 ? 's' : ''} in ${input.path}`
393
- );
394
- }
395
-
396
- // Replace only the first occurrence
397
- const idx = content.indexOf(input.old_string);
398
- updated =
399
- content.slice(0, idx) + input.new_string + content.slice(idx + input.old_string.length);
400
-
401
- await fs.writeFile(input.path, updated, 'utf-8');
402
- return ok(`Successfully edited ${input.path}`);
403
- } catch (error: unknown) {
404
- const message = error instanceof Error ? error.message : String(error);
405
- return err(`Failed to edit file: ${message}`);
406
- }
407
- },
408
- };
409
-
410
- // ---------------------------------------------------------------------------
411
- // 3. multi_edit
412
- // ---------------------------------------------------------------------------
413
-
414
- const multiEditSchema = z.object({
415
- path: z.string().describe('Path to the file to edit'),
416
- edits: z
417
- .array(
418
- z.object({
419
- old_string: z.string().describe('Exact text to find'),
420
- new_string: z.string().describe('Replacement text'),
421
- })
422
- )
423
- .describe('Array of edits to apply sequentially'),
424
- });
425
-
426
- export const multiEditTool: ToolDefinition = {
427
- name: 'multi_edit',
428
- description: 'Make multiple text replacements in a single file atomically.',
429
- inputSchema: multiEditSchema,
430
- permissionTier: 'ask_once',
431
- category: 'standard',
432
- isDestructive: true,
433
-
434
- async execute(raw: unknown): Promise<ToolResult> {
435
- try {
436
- const input = multiEditSchema.parse(raw);
437
- const content = await fs.readFile(input.path, 'utf-8');
438
-
439
- // Pre-compute all edit positions on the original content to detect
440
- // overlap issues and enable bottom-up application.
441
- const positioned: Array<{
442
- index: number;
443
- editIndex: number;
444
- old_string: string;
445
- new_string: string;
446
- }> = [];
447
- const usedPositions = new Set<number>();
448
- for (let i = 0; i < input.edits.length; i++) {
449
- const edit = input.edits[i];
450
- // Search for the next occurrence that hasn't already been claimed
451
- let searchFrom = 0;
452
- let idx = -1;
453
- for (;;) {
454
- idx = content.indexOf(edit.old_string, searchFrom);
455
- if (idx === -1) {
456
- break;
457
- }
458
- if (!usedPositions.has(idx)) {
459
- break;
460
- }
461
- searchFrom = idx + 1;
462
- }
463
- if (idx === -1) {
464
- return err(
465
- `Edit ${i + 1}: old_string not found in ${input.path}. Aborting — no changes were written.`
466
- );
467
- }
468
- usedPositions.add(idx);
469
- positioned.push({
470
- index: idx,
471
- editIndex: i,
472
- old_string: edit.old_string,
473
- new_string: edit.new_string,
474
- });
475
- }
476
-
477
- // Sort by position descending (bottom-up) so earlier edits don't
478
- // shift the positions of later ones.
479
- positioned.sort((a, b) => b.index - a.index);
480
-
481
- let result = content;
482
- for (const edit of positioned) {
483
- result =
484
- result.slice(0, edit.index) +
485
- edit.new_string +
486
- result.slice(edit.index + edit.old_string.length);
487
- }
488
-
489
- await fs.writeFile(input.path, result, 'utf-8');
490
- return ok(`Successfully applied ${input.edits.length} edit(s) to ${input.path}`);
491
- } catch (error: unknown) {
492
- const message = error instanceof Error ? error.message : String(error);
493
- return err(`Failed to apply multi-edit: ${message}`);
494
- }
495
- },
496
- };
497
-
498
- // ---------------------------------------------------------------------------
499
- // 4. write_file
500
- // ---------------------------------------------------------------------------
501
-
502
- const writeFileSchema = z.object({
503
- path: z.string().describe('Path to the file to create or overwrite'),
504
- content: z.string().describe('Full file content to write'),
505
- });
506
-
507
- export const writeFileTool: ToolDefinition = {
508
- name: 'write_file',
509
- description: 'Create or overwrite a file with the given content.',
510
- inputSchema: writeFileSchema,
511
- permissionTier: 'ask_once',
512
- category: 'standard',
513
- isDestructive: true,
514
-
515
- async execute(raw: unknown): Promise<ToolResult> {
516
- try {
517
- const input = writeFileSchema.parse(raw);
518
- await fs.mkdir(path.dirname(input.path), { recursive: true });
519
- await fs.writeFile(input.path, input.content, 'utf-8');
520
- return ok(`Successfully wrote ${input.path}`);
521
- } catch (error: unknown) {
522
- const message = error instanceof Error ? error.message : String(error);
523
- return err(`Failed to write file: ${message}`);
524
- }
525
- },
526
- };
527
-
528
- // ---------------------------------------------------------------------------
529
- // 5. bash
530
- // ---------------------------------------------------------------------------
531
-
532
- const bashSchema = z.object({
533
- command: z.string().describe('Shell command to execute'),
534
- timeout: z
535
- .number()
536
- .optional()
537
- .default(120_000)
538
- .describe('Timeout in milliseconds (default: 120000)'),
539
- workdir: z.string().optional().describe('Working directory for the command'),
540
- });
541
-
542
- export const bashTool: ToolDefinition = {
543
- name: 'bash',
544
- description:
545
- 'Execute a shell command and return its output. Use for running tests, installing packages, or other terminal operations.',
546
- inputSchema: bashSchema,
547
- permissionTier: 'ask_once',
548
- category: 'standard',
549
- isDestructive: true,
550
-
551
- async execute(raw: unknown): Promise<ToolResult> {
552
- try {
553
- const input = bashSchema.parse(raw);
554
- const signal = (raw as Record<string, unknown> | null)?._signal as AbortSignal | undefined;
555
- const onChunk = (raw as Record<string, unknown>)?._onChunk as ((chunk: string) => void) | undefined;
556
- const { stdout, stderr } = await spawnAsync(input.command, {
557
- timeout: input.timeout,
558
- cwd: input.workdir,
559
- maxBuffer: 10 * 1024 * 1024, // 10 MB
560
- signal,
561
- onChunk,
562
- });
563
- const combined = [stdout, stderr].filter(Boolean).join('\n');
564
- return ok(combined || '(no output)');
565
- } catch (error: unknown) {
566
- if (error !== null && typeof error === 'object' && 'stdout' in error) {
567
- // Process errors still carry partial output
568
- const execErr = error as {
569
- stdout?: string;
570
- stderr?: string;
571
- message?: string;
572
- };
573
- const combined = [execErr.stdout, execErr.stderr].filter(Boolean).join('\n');
574
- return err(combined || execErr.message || 'Command failed');
575
- }
576
- const message = error instanceof Error ? error.message : String(error);
577
- return err(`Command failed: ${message}`);
578
- }
579
- },
580
- };
581
-
582
- // ---------------------------------------------------------------------------
583
- // 6. glob
584
- // ---------------------------------------------------------------------------
585
-
586
- const globSchema = z.object({
587
- pattern: z.string().describe('Glob pattern to match files (e.g., "**/*.ts")'),
588
- path: z.string().optional().describe('Base directory to search in (defaults to cwd)'),
589
- });
590
-
591
- export const globTool: ToolDefinition = {
592
- name: 'glob',
593
- description: 'Find files matching a glob pattern. Returns matching file paths.',
594
- inputSchema: globSchema,
595
- permissionTier: 'auto_allow',
596
- category: 'standard',
597
-
598
- async execute(raw: unknown): Promise<ToolResult> {
599
- try {
600
- const input = globSchema.parse(raw);
601
- const matches = await fastGlob(input.pattern, {
602
- cwd: input.path ?? process.cwd(),
603
- absolute: true,
604
- dot: false,
605
- });
606
- if (matches.length === 0) {
607
- return ok('No files matched the pattern.');
608
- }
609
- return ok(matches.join('\n'));
610
- } catch (error: unknown) {
611
- const message = error instanceof Error ? error.message : String(error);
612
- return err(`Glob search failed: ${message}`);
613
- }
614
- },
615
- };
616
-
617
- // ---------------------------------------------------------------------------
618
- // 7. grep
619
- // ---------------------------------------------------------------------------
620
-
621
- const grepSchema = z.object({
622
- pattern: z.string().describe('Regex pattern to search for'),
623
- path: z.string().optional().describe('Directory or file to search in (defaults to cwd)'),
624
- include: z.string().optional().describe('Glob filter for file types (e.g., "*.ts")'),
625
- });
626
-
627
- export const grepTool: ToolDefinition = {
628
- name: 'grep',
629
- description:
630
- 'Search file contents using a regex pattern. Returns matching lines with file paths and line numbers.',
631
- inputSchema: grepSchema,
632
- permissionTier: 'auto_allow',
633
- category: 'standard',
634
-
635
- async execute(raw: unknown): Promise<ToolResult> {
636
- try {
637
- const input = grepSchema.parse(raw);
638
- const searchPath = input.path ?? process.cwd();
639
-
640
- // Try ripgrep first (faster, respects .gitignore), fall back to grep
641
- let command: string;
642
- try {
643
- await execAsync('rg --version', { timeout: 2000 });
644
- const globFlag = input.include ? ` --glob ${JSON.stringify(input.include)}` : '';
645
- command = `rg -n${globFlag} ${JSON.stringify(input.pattern)} ${JSON.stringify(searchPath)}`;
646
- } catch {
647
- const includeFlag = input.include ? ` --include=${JSON.stringify(input.include)}` : '';
648
- command = `grep -rn${includeFlag} ${JSON.stringify(input.pattern)} ${JSON.stringify(searchPath)}`;
649
- }
650
-
651
- const { stdout } = await execAsync(command, {
652
- maxBuffer: 10 * 1024 * 1024,
653
- });
654
- return ok(stdout || 'No matches found.');
655
- } catch (error: unknown) {
656
- if (
657
- error !== null &&
658
- typeof error === 'object' &&
659
- 'code' in error &&
660
- (error as { code: number }).code === 1
661
- ) {
662
- // grep/rg exit code 1 = no matches (not a real error)
663
- return ok('No matches found.');
664
- }
665
- const message = error instanceof Error ? error.message : String(error);
666
- return err(`Grep failed: ${message}`);
667
- }
668
- },
669
- };
670
-
671
- // ---------------------------------------------------------------------------
672
- // 8. list_dir
673
- // ---------------------------------------------------------------------------
674
-
675
- const listDirSchema = z.object({
676
- path: z.string().describe('Absolute or relative path to the directory'),
677
- });
678
-
679
- export const listDirTool: ToolDefinition = {
680
- name: 'list_dir',
681
- description:
682
- 'List the contents of a directory. Returns file and directory names with type indicators.',
683
- inputSchema: listDirSchema,
684
- permissionTier: 'auto_allow',
685
- category: 'standard',
686
-
687
- async execute(raw: unknown): Promise<ToolResult> {
688
- try {
689
- const input = listDirSchema.parse(raw);
690
- const entries = await fs.readdir(input.path, {
691
- withFileTypes: true,
692
- });
693
-
694
- if (entries.length === 0) {
695
- return ok('(empty directory)');
696
- }
697
-
698
- const lines = entries.map(entry => {
699
- if (entry.isDirectory()) {
700
- return `[DIR] ${entry.name}/`;
701
- }
702
- return `[FILE] ${entry.name}`;
703
- });
704
-
705
- return ok(lines.join('\n'));
706
- } catch (error: unknown) {
707
- const message = error instanceof Error ? error.message : String(error);
708
- return err(`Failed to list directory: ${message}`);
709
- }
710
- },
711
- };
712
-
713
- // ---------------------------------------------------------------------------
714
- // 9. webfetch
715
- // ---------------------------------------------------------------------------
716
-
717
- const webfetchSchema = z.object({
718
- url: z.string().url().describe('URL to fetch content from'),
719
- prompt: z.string().optional().describe('Optional prompt describing what information to extract'),
720
- });
721
-
722
- /** Maximum characters returned from a fetched page. */
723
- const WEBFETCH_MAX_CHARS = 50_000;
724
-
725
- export const webfetchTool: ToolDefinition = {
726
- name: 'webfetch',
727
- description: 'Fetch content from a URL and optionally process it with a prompt.',
728
- inputSchema: webfetchSchema,
729
- permissionTier: 'ask_once',
730
- category: 'standard',
731
-
732
- async execute(raw: unknown): Promise<ToolResult> {
733
- try {
734
- const input = webfetchSchema.parse(raw);
735
- const response = await fetch(input.url);
736
-
737
- if (!response.ok) {
738
- return err(`HTTP ${response.status} ${response.statusText} fetching ${input.url}`);
739
- }
740
-
741
- let text = await response.text();
742
-
743
- if (text.length > WEBFETCH_MAX_CHARS) {
744
- text = `${text.slice(
745
- 0,
746
- WEBFETCH_MAX_CHARS
747
- )}\n\n... (truncated, ${text.length} total characters)`;
748
- }
749
-
750
- if (input.prompt) {
751
- // Attempt to process the fetched content through a fast LLM model.
752
- // Falls back to returning raw text if the router is unavailable or
753
- // the LLM call fails for any reason.
754
- try {
755
- const { getAppContext } = await import('../../app');
756
- const ctx = getAppContext();
757
- if (ctx?.router) {
758
- const stream = ctx.router.routeStream(
759
- {
760
- messages: [
761
- {
762
- role: 'system' as const,
763
- content:
764
- 'You are a content extraction assistant. The user will provide web page content and a question or instruction. Extract the requested information concisely from the content. Do not add information that is not present in the content.',
765
- },
766
- {
767
- role: 'user' as const,
768
- content: `${input.prompt}\n\n---\n\nWeb page content from ${input.url}:\n\n${text}`,
769
- },
770
- ],
771
- model: 'haiku',
772
- maxTokens: 4096,
773
- },
774
- 'summarization'
775
- );
776
-
777
- let result = '';
778
- for await (const chunk of stream) {
779
- if (chunk.content) {
780
- result += chunk.content;
781
- }
782
- }
783
-
784
- if (result.length > 0) {
785
- return ok(result);
786
- }
787
- }
788
- } catch {
789
- // LLM processing failed — fall through to raw text response.
790
- }
791
-
792
- return ok(`[Prompt: ${input.prompt}]\n\n${text}`);
793
- }
794
-
795
- return ok(text);
796
- } catch (error: unknown) {
797
- const message = error instanceof Error ? error.message : String(error);
798
- return err(`Fetch failed: ${message}`);
799
- }
800
- },
801
- };
802
-
803
- // ---------------------------------------------------------------------------
804
- // 10. todo_read
805
- // ---------------------------------------------------------------------------
806
-
807
- const todoReadSchema = z.object({}).describe('No input required');
808
-
809
- export const todoReadTool: ToolDefinition = {
810
- name: 'todo_read',
811
- description: "Read the current session's task list.",
812
- inputSchema: todoReadSchema,
813
- permissionTier: 'auto_allow',
814
- category: 'standard',
815
-
816
- async execute(_raw: unknown): Promise<ToolResult> {
817
- try {
818
- const { getDb } = await import('../../state/db');
819
- const db = getDb();
820
- db.exec(`
821
- CREATE TABLE IF NOT EXISTS todos (
822
- id INTEGER PRIMARY KEY AUTOINCREMENT,
823
- subject TEXT NOT NULL,
824
- status TEXT NOT NULL DEFAULT 'pending',
825
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
826
- )
827
- `);
828
- const rows = db.query('SELECT id, subject, status FROM todos ORDER BY id').all() as Array<{
829
- id: number;
830
- subject: string;
831
- status: string;
832
- }>;
833
- if (rows.length === 0) {
834
- return ok('No tasks yet.');
835
- }
836
- const lines = rows.map(r => {
837
- const indicator =
838
- r.status === 'completed' ? '[x]' : r.status === 'in_progress' ? '[~]' : '[ ]';
839
- return `${r.id}. ${indicator} ${r.subject}`;
840
- });
841
- return ok(lines.join('\n'));
842
- } catch (error: unknown) {
843
- // Fallback to original placeholder behavior on DB failure.
844
- return ok('No tasks yet.');
845
- }
846
- },
847
- };
848
-
849
- // ---------------------------------------------------------------------------
850
- // 11. todo_write
851
- // ---------------------------------------------------------------------------
852
-
853
- const todoWriteSchema = z.object({
854
- tasks: z
855
- .array(
856
- z.object({
857
- subject: z.string().describe('Brief task title'),
858
- status: z
859
- .enum(['pending', 'in_progress', 'completed'])
860
- .describe('Current status of the task'),
861
- })
862
- )
863
- .describe('Array of tasks to write to the session'),
864
- });
865
-
866
- export const todoWriteTool: ToolDefinition = {
867
- name: 'todo_write',
868
- description: "Update the session's task list.",
869
- inputSchema: todoWriteSchema,
870
- permissionTier: 'auto_allow',
871
- category: 'standard',
872
-
873
- async execute(raw: unknown): Promise<ToolResult> {
874
- try {
875
- const input = todoWriteSchema.parse(raw);
876
- const { getDb } = await import('../../state/db');
877
- const db = getDb();
878
- db.exec(`
879
- CREATE TABLE IF NOT EXISTS todos (
880
- id INTEGER PRIMARY KEY AUTOINCREMENT,
881
- subject TEXT NOT NULL,
882
- status TEXT NOT NULL DEFAULT 'pending',
883
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
884
- )
885
- `);
886
- // Replace strategy: clear existing rows and insert the new set.
887
- db.exec('DELETE FROM todos');
888
- const insert = db.prepare('INSERT INTO todos (subject, status) VALUES (?, ?)');
889
- const insertAll = db.transaction((tasks: Array<{ subject: string; status: string }>) => {
890
- for (const t of tasks) {
891
- insert.run(t.subject, t.status);
892
- }
893
- });
894
- insertAll(input.tasks);
895
- const summary = input.tasks.map(t => {
896
- const indicator =
897
- t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
898
- return `${indicator} ${t.subject}`;
899
- });
900
- return ok(
901
- `Task list updated (${input.tasks.length} task${input.tasks.length === 1 ? '' : 's'}):\n${summary.join('\n')}`
902
- );
903
- } catch (error: unknown) {
904
- // Fallback to original placeholder behavior on DB failure.
905
- const message = error instanceof Error ? error.message : String(error);
906
- return err(`Failed to update tasks: ${message}`);
907
- }
908
- },
909
- };
910
-
911
- // ---------------------------------------------------------------------------
912
- // 12. web_search — Multi-engine search helpers
913
- // ---------------------------------------------------------------------------
914
-
915
- /**
916
- * Search using the Brave Search API. Returns formatted markdown results
917
- * or null if the request fails (allowing fallback to DuckDuckGo).
918
- */
919
- async function searchBrave(
920
- query: string,
921
- maxResults: number,
922
- apiKey: string
923
- ): Promise<string | null> {
924
- try {
925
- const encodedQuery = encodeURIComponent(query);
926
- const response = await fetch(
927
- `https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}&count=${maxResults}`,
928
- {
929
- headers: {
930
- Accept: 'application/json',
931
- 'Accept-Encoding': 'gzip',
932
- 'X-Subscription-Token': apiKey,
933
- },
934
- }
935
- );
936
-
937
- if (!response.ok) {
938
- return null;
939
- }
940
-
941
- const data = (await response.json()) as any;
942
- const results: string[] = [];
943
-
944
- // Featured snippet
945
- if (data.mixed?.main?.[0]?.type === 'faq') {
946
- const faq = data.mixed.main[0];
947
- if (faq.results?.[0]?.answer) {
948
- results.push(`## Featured Answer\n${faq.results[0].answer}\n`);
949
- }
950
- }
951
-
952
- // Web results
953
- if (data.web?.results) {
954
- results.push('## Results');
955
- for (const r of data.web.results.slice(0, maxResults)) {
956
- results.push(`### ${r.title}\n${r.description || ''}\nURL: ${r.url}\n`);
957
- }
958
- }
959
-
960
- // Knowledge graph
961
- if (data.infobox?.results?.[0]) {
962
- const info = data.infobox.results[0];
963
- results.push(`## ${info.title || 'Info'}\n${info.description || ''}`);
964
- }
965
-
966
- return results.length > 0 ? results.join('\n') : null;
967
- } catch {
968
- return null;
969
- }
970
- }
971
-
972
- /**
973
- * Search using DuckDuckGo Instant Answer API. Returns formatted markdown
974
- * results. This is the fallback engine that requires no API key.
975
- */
976
- async function searchDuckDuckGo(query: string, maxResults: number): Promise<string> {
977
- const encodedQuery = encodeURIComponent(query);
978
-
979
- // Try the Instant Answer API first for direct answers
980
- const iaResponse = await fetch(
981
- `https://api.duckduckgo.com/?q=${encodedQuery}&format=json&no_html=1&skip_disambig=1`
982
- );
983
-
984
- const results: string[] = [];
985
-
986
- if (iaResponse.ok) {
987
- const data = (await iaResponse.json()) as any;
988
-
989
- // Abstract (direct answer)
990
- if (data.Abstract) {
991
- results.push(`## Direct Answer\n${data.Abstract}\nSource: ${data.AbstractURL}\n`);
992
- }
993
-
994
- // Definition
995
- if (data.Definition) {
996
- results.push(`## Definition\n${data.Definition}\nSource: ${data.DefinitionURL}\n`);
997
- }
998
-
999
- // Answer (calculations, conversions, etc.)
1000
- if (data.Answer) {
1001
- results.push(`## Answer\n${data.Answer}\n`);
1002
- }
1003
-
1004
- // Related topics
1005
- if (data.RelatedTopics && Array.isArray(data.RelatedTopics)) {
1006
- const topics = data.RelatedTopics.filter((t: any) => t.Text && t.FirstURL).slice(
1007
- 0,
1008
- maxResults
1009
- );
1010
-
1011
- if (topics.length > 0) {
1012
- results.push('## Results');
1013
- for (const topic of topics) {
1014
- results.push(`- ${topic.Text}\n URL: ${topic.FirstURL}`);
1015
- }
1016
- }
1017
- }
1018
-
1019
- // Infobox
1020
- if (data.Infobox?.content?.length > 0) {
1021
- results.push('## Info');
1022
- for (const item of data.Infobox.content.slice(0, 5)) {
1023
- if (item.label && item.value) {
1024
- results.push(`- ${item.label}: ${item.value}`);
1025
- }
1026
- }
1027
- }
1028
-
1029
- // Redirect (if DDG suggests a better page)
1030
- if (data.Redirect) {
1031
- results.push(`\nSuggested page: ${data.Redirect}`);
1032
- }
1033
- }
1034
-
1035
- // If Instant Answer API returned nothing, try the HTML lite endpoint
1036
- if (results.length === 0) {
1037
- try {
1038
- const htmlResponse = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {
1039
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Nimbus-CLI/1.0)' },
1040
- });
1041
- if (htmlResponse.ok) {
1042
- const html = await htmlResponse.text();
1043
- // Parse result snippets from the lite HTML page
1044
- const resultPattern =
1045
- /<a rel="nofollow" class="result__a" href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([^<]*)<\/a>/g;
1046
- let match: RegExpExecArray | null;
1047
- let count = 0;
1048
- results.push('## Results');
1049
- while ((match = resultPattern.exec(html)) !== null && count < maxResults) {
1050
- const url = match[1].startsWith('//') ? `https:${match[1]}` : match[1];
1051
- const title = match[2]
1052
- .replace(/&#x27;/g, "'")
1053
- .replace(/&amp;/g, '&')
1054
- .replace(/&quot;/g, '"');
1055
- const snippet = match[3]
1056
- .replace(/<\/?b>/g, '**')
1057
- .replace(/&#x27;/g, "'")
1058
- .replace(/&amp;/g, '&');
1059
- results.push(`### ${title}\n${snippet}\nURL: ${url}\n`);
1060
- count++;
1061
- }
1062
- }
1063
- } catch {
1064
- // HTML fallback failed — fall through to "no results" message
1065
- }
1066
- }
1067
-
1068
- if (results.length === 0) {
1069
- return `No results found for: "${query}". Try rephrasing the query or using more specific terms.\n\nTip: Set BRAVE_SEARCH_API_KEY env var for significantly better web search results.`;
1070
- }
1071
-
1072
- return results.join('\n');
1073
- }
1074
-
1075
- // ---------------------------------------------------------------------------
1076
- // 12. web_search
1077
- // ---------------------------------------------------------------------------
1078
-
1079
- const webSearchSchema = z.object({
1080
- query: z.string().describe('The search query string'),
1081
- maxResults: z
1082
- .number()
1083
- .optional()
1084
- .default(5)
1085
- .describe('Maximum number of results to return (default: 5)'),
1086
- engine: z
1087
- .enum(['auto', 'duckduckgo', 'brave'])
1088
- .optional()
1089
- .default('auto')
1090
- .describe('Search engine to use (default: auto)'),
1091
- });
1092
-
1093
- export const webSearchTool: ToolDefinition = {
1094
- name: 'web_search',
1095
- description:
1096
- 'Search the web using a query string. Returns titles, URLs, and snippets. ' +
1097
- 'Supports Brave Search (set BRAVE_SEARCH_API_KEY) and DuckDuckGo. ' +
1098
- 'Useful for finding documentation, looking up error messages, or researching technologies.',
1099
- inputSchema: webSearchSchema,
1100
- permissionTier: 'ask_once',
1101
- category: 'standard',
1102
-
1103
- async execute(raw: unknown): Promise<ToolResult> {
1104
- try {
1105
- const input = webSearchSchema.parse(raw);
1106
-
1107
- // Try Brave Search API first if key is available, then DuckDuckGo as fallback
1108
- const braveKey = process.env.BRAVE_SEARCH_API_KEY;
1109
-
1110
- if ((input.engine === 'brave' || input.engine === 'auto') && braveKey) {
1111
- const result = await searchBrave(input.query, input.maxResults, braveKey);
1112
- if (result) {
1113
- return ok(result);
1114
- }
1115
- if (input.engine === 'brave') {
1116
- return err('Brave Search failed and no fallback allowed');
1117
- }
1118
- }
1119
-
1120
- // DuckDuckGo HTML search (better results than Instant Answer API)
1121
- const result = await searchDuckDuckGo(input.query, input.maxResults);
1122
- return ok(result);
1123
- } catch (error: unknown) {
1124
- const msg = error instanceof Error ? error.message : String(error);
1125
- return err(`Web search failed: ${msg}`);
1126
- }
1127
- },
1128
- };
1129
-
1130
- // ---------------------------------------------------------------------------
1131
- // Aggregate export
1132
- // ---------------------------------------------------------------------------
1133
-
1134
- /** All 12 standard tools as an ordered array. */
1135
- export const standardTools: ToolDefinition[] = [
1136
- readFileTool,
1137
- editFileTool,
1138
- multiEditTool,
1139
- writeFileTool,
1140
- bashTool,
1141
- globTool,
1142
- grepTool,
1143
- listDirTool,
1144
- webfetchTool,
1145
- todoReadTool,
1146
- todoWriteTool,
1147
- webSearchTool,
1148
- ];