@build-astron-co/nimbus 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Hooks Configuration
3
+ *
4
+ * Parses and validates `.nimbus/hooks.yaml` configuration files.
5
+ * Provides types and utilities for the Nimbus hooks system that allows
6
+ * users to run custom scripts before/after tool invocations.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Events that can trigger hook execution */
17
+ export type HookEvent = 'PreToolUse' | 'PostToolUse' | 'PermissionRequest';
18
+
19
+ /** All valid hook event names for validation */
20
+ const VALID_HOOK_EVENTS: readonly HookEvent[] = [
21
+ 'PreToolUse',
22
+ 'PostToolUse',
23
+ 'PermissionRequest',
24
+ ] as const;
25
+
26
+ /**
27
+ * A single hook definition specifying when and what to run.
28
+ *
29
+ * @example
30
+ * ```yaml
31
+ * - match: "edit_file|write_file"
32
+ * command: ".nimbus/hooks/pre-edit.sh"
33
+ * timeout: 30000
34
+ * ```
35
+ */
36
+ export interface HookDefinition {
37
+ /** Regex pattern to match tool names (e.g. "edit_file|write_file") */
38
+ match: string;
39
+ /** Shell command or path to script to execute */
40
+ command: string;
41
+ /** Timeout in milliseconds before the hook is killed (default: 30000) */
42
+ timeout?: number;
43
+ }
44
+
45
+ /**
46
+ * Top-level hooks configuration parsed from `.nimbus/hooks.yaml`.
47
+ *
48
+ * @example
49
+ * ```yaml
50
+ * hooks:
51
+ * PreToolUse:
52
+ * - match: "edit_file|write_file"
53
+ * command: ".nimbus/hooks/pre-edit.sh"
54
+ * PostToolUse:
55
+ * - match: "edit_file|write_file"
56
+ * command: ".nimbus/hooks/auto-format.sh"
57
+ * PermissionRequest:
58
+ * - match: "*"
59
+ * command: ".nimbus/hooks/audit-permission.sh"
60
+ * ```
61
+ */
62
+ export interface HooksConfig {
63
+ hooks: Record<HookEvent, HookDefinition[]>;
64
+ }
65
+
66
+ /** Default timeout in milliseconds for hook execution */
67
+ export const DEFAULT_HOOK_TIMEOUT = 30_000;
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Minimal YAML Parser
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Represents a single line of parsed YAML content with its indentation level.
75
+ */
76
+ interface YamlLine {
77
+ indent: number;
78
+ content: string;
79
+ }
80
+
81
+ /**
82
+ * Parse raw YAML text into an array of meaningful lines with indentation info.
83
+ * Strips comments and blank lines.
84
+ *
85
+ * @param text - Raw YAML content
86
+ * @returns Array of parsed lines with indentation levels
87
+ */
88
+ function tokenizeYaml(text: string): YamlLine[] {
89
+ const lines: YamlLine[] = [];
90
+ for (const raw of text.split('\n')) {
91
+ // Strip inline comments (but not inside quoted strings)
92
+ const withoutComment = raw.replace(/#.*$/, '');
93
+ const trimmed = withoutComment.trimEnd();
94
+ if (trimmed.length === 0) {
95
+ continue;
96
+ }
97
+ const indent = trimmed.search(/\S/);
98
+ if (indent === -1) {
99
+ continue;
100
+ }
101
+ lines.push({ indent, content: trimmed.trim() });
102
+ }
103
+ return lines;
104
+ }
105
+
106
+ /**
107
+ * Remove surrounding quotes (single or double) from a string value.
108
+ *
109
+ * @param value - Potentially quoted string
110
+ * @returns Unquoted string
111
+ */
112
+ function unquote(value: string): string {
113
+ if (
114
+ (value.startsWith('"') && value.endsWith('"')) ||
115
+ (value.startsWith("'") && value.endsWith("'"))
116
+ ) {
117
+ return value.slice(1, -1);
118
+ }
119
+ return value;
120
+ }
121
+
122
+ /**
123
+ * Coerce a raw YAML string value to the appropriate JS primitive.
124
+ *
125
+ * @param raw - Raw string value from YAML
126
+ * @returns Coerced value (string, number, boolean, or null)
127
+ */
128
+ function coerceValue(raw: string): string | number | boolean | null {
129
+ if (raw === 'true') {
130
+ return true;
131
+ }
132
+ if (raw === 'false') {
133
+ return false;
134
+ }
135
+ if (raw === 'null' || raw === '~') {
136
+ return null;
137
+ }
138
+
139
+ const unquoted = unquote(raw);
140
+ if (unquoted !== raw) {
141
+ // Was quoted -- keep as string
142
+ return unquoted;
143
+ }
144
+
145
+ // Try number coercion
146
+ if (raw !== '' && !isNaN(Number(raw))) {
147
+ return Number(raw);
148
+ }
149
+ return raw;
150
+ }
151
+
152
+ /**
153
+ * Minimal recursive-descent YAML parser.
154
+ *
155
+ * Handles the subset of YAML needed for hooks configuration:
156
+ * - Top-level maps
157
+ * - Arrays of objects (using `- key: value` syntax)
158
+ * - Scalar values (string, number, boolean)
159
+ * - Nested maps
160
+ *
161
+ * This is intentionally NOT a full YAML parser. It covers the structure
162
+ * required by `.nimbus/hooks.yaml` without requiring an external dependency.
163
+ *
164
+ * @param text - Raw YAML content
165
+ * @returns Parsed object
166
+ */
167
+ function parseYaml(text: string): Record<string, unknown> {
168
+ const lines = tokenizeYaml(text);
169
+ let pos = 0;
170
+
171
+ /**
172
+ * Parse a mapping (object) at the given indentation level.
173
+ */
174
+ function parseMapping(minIndent: number): Record<string, unknown> {
175
+ const result: Record<string, unknown> = {};
176
+
177
+ while (pos < lines.length) {
178
+ const line = lines[pos];
179
+
180
+ // If the line is at a lower indent, we've left this mapping
181
+ if (line.indent < minIndent) {
182
+ break;
183
+ }
184
+
185
+ // Skip lines that are deeper than expected (shouldn't happen in
186
+ // well-formed input, but be defensive)
187
+ if (line.indent > minIndent && !line.content.startsWith('- ')) {
188
+ pos++;
189
+ continue;
190
+ }
191
+
192
+ // Array items are handled by the caller (parseArray)
193
+ if (line.content.startsWith('- ')) {
194
+ break;
195
+ }
196
+
197
+ const colonIdx = line.content.indexOf(':');
198
+ if (colonIdx === -1) {
199
+ pos++;
200
+ continue;
201
+ }
202
+
203
+ const key = line.content.slice(0, colonIdx).trim();
204
+ const rest = line.content.slice(colonIdx + 1).trim();
205
+
206
+ pos++;
207
+
208
+ if (rest.length > 0) {
209
+ // Inline scalar value: `key: value`
210
+ result[key] = coerceValue(rest);
211
+ } else {
212
+ // Value is on subsequent indented lines -- either a nested map or array
213
+ if (pos < lines.length && lines[pos].indent > minIndent) {
214
+ const childIndent = lines[pos].indent;
215
+ if (lines[pos].content.startsWith('- ')) {
216
+ result[key] = parseArray(childIndent);
217
+ } else {
218
+ result[key] = parseMapping(childIndent);
219
+ }
220
+ } else {
221
+ // Empty value
222
+ result[key] = null;
223
+ }
224
+ }
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * Parse an array at the given indentation level.
232
+ * Each array element starts with `- ` and can contain inline key-value
233
+ * pairs or a nested block mapping.
234
+ */
235
+ function parseArray(minIndent: number): unknown[] {
236
+ const result: unknown[] = [];
237
+
238
+ while (pos < lines.length) {
239
+ const line = lines[pos];
240
+
241
+ if (line.indent < minIndent) {
242
+ break;
243
+ }
244
+
245
+ if (!line.content.startsWith('- ')) {
246
+ break;
247
+ }
248
+
249
+ // Strip the leading `- `
250
+ const afterDash = line.content.slice(2).trim();
251
+ pos++;
252
+
253
+ if (afterDash.includes(':')) {
254
+ // Inline object start: `- key: value`
255
+ const obj: Record<string, unknown> = {};
256
+ const colonIdx = afterDash.indexOf(':');
257
+ const key = afterDash.slice(0, colonIdx).trim();
258
+ const val = afterDash.slice(colonIdx + 1).trim();
259
+ obj[key] = val.length > 0 ? coerceValue(val) : null;
260
+
261
+ // Collect subsequent indented key-value pairs belonging to the same item
262
+ while (pos < lines.length) {
263
+ const next = lines[pos];
264
+ // Must be indented deeper than the `- ` marker and NOT be another array item
265
+ if (next.indent <= minIndent || next.content.startsWith('- ')) {
266
+ break;
267
+ }
268
+ const nextColon = next.content.indexOf(':');
269
+ if (nextColon === -1) {
270
+ pos++;
271
+ continue;
272
+ }
273
+ const nk = next.content.slice(0, nextColon).trim();
274
+ const nv = next.content.slice(nextColon + 1).trim();
275
+ obj[nk] = nv.length > 0 ? coerceValue(nv) : null;
276
+ pos++;
277
+ }
278
+
279
+ result.push(obj);
280
+ } else if (afterDash.length > 0) {
281
+ // Scalar array element: `- value`
282
+ result.push(coerceValue(afterDash));
283
+ } else {
284
+ // Block-style object under `- `
285
+ if (pos < lines.length && lines[pos].indent > minIndent) {
286
+ const childIndent = lines[pos].indent;
287
+ result.push(parseMapping(childIndent));
288
+ }
289
+ }
290
+ }
291
+
292
+ return result;
293
+ }
294
+
295
+ return parseMapping(0);
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Validation
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /**
303
+ * Validate a single hook definition and return any errors found.
304
+ *
305
+ * Checks:
306
+ * - `match` is a non-empty string that compiles as a valid RegExp
307
+ * - `command` is a non-empty string
308
+ * - `timeout`, if provided, is a positive number
309
+ *
310
+ * @param hook - The hook definition to validate
311
+ * @returns Array of human-readable error strings (empty if valid)
312
+ */
313
+ export function validateHookDefinition(hook: HookDefinition): string[] {
314
+ const errors: string[] = [];
315
+
316
+ // match
317
+ if (typeof hook.match !== 'string' || hook.match.length === 0) {
318
+ errors.push('hook "match" must be a non-empty string');
319
+ } else {
320
+ try {
321
+ new RegExp(hook.match);
322
+ } catch {
323
+ errors.push(`hook "match" is not a valid regex: "${hook.match}"`);
324
+ }
325
+ }
326
+
327
+ // command
328
+ if (typeof hook.command !== 'string' || hook.command.length === 0) {
329
+ errors.push('hook "command" must be a non-empty string');
330
+ }
331
+
332
+ // timeout (optional)
333
+ if (hook.timeout !== undefined) {
334
+ if (typeof hook.timeout !== 'number' || hook.timeout <= 0) {
335
+ errors.push('hook "timeout" must be a positive number');
336
+ }
337
+ }
338
+
339
+ return errors;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Loader
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /**
347
+ * Load and validate a hooks configuration from `<projectDir>/.nimbus/hooks.yaml`.
348
+ *
349
+ * @param projectDir - Absolute or relative path to the project root directory
350
+ * @returns Parsed and validated `HooksConfig`, or `null` if the file does not exist
351
+ * @throws Error if the file exists but contains invalid configuration
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * const config = loadHooksConfig('/path/to/project');
356
+ * if (config) {
357
+ * console.log(config.hooks.PreToolUse);
358
+ * }
359
+ * ```
360
+ */
361
+ export function loadHooksConfig(projectDir: string): HooksConfig | null {
362
+ const configPath = path.join(projectDir, '.nimbus', 'hooks.yaml');
363
+
364
+ if (!fs.existsSync(configPath)) {
365
+ return null;
366
+ }
367
+
368
+ const raw = fs.readFileSync(configPath, 'utf-8');
369
+ const parsed = parseYaml(raw) as Record<string, unknown>;
370
+
371
+ // Validate top-level structure
372
+ if (!parsed.hooks || typeof parsed.hooks !== 'object') {
373
+ throw new Error(`Invalid hooks config at ${configPath}: missing top-level "hooks" key`);
374
+ }
375
+
376
+ const hooksRaw = parsed.hooks as Record<string, unknown>;
377
+
378
+ // Build the validated config, ensuring all three event types are present
379
+ const config: HooksConfig = {
380
+ hooks: {
381
+ PreToolUse: [],
382
+ PostToolUse: [],
383
+ PermissionRequest: [],
384
+ },
385
+ };
386
+
387
+ for (const [eventName, definitions] of Object.entries(hooksRaw)) {
388
+ // Validate event name
389
+ if (!VALID_HOOK_EVENTS.includes(eventName as HookEvent)) {
390
+ throw new Error(
391
+ `Invalid hooks config at ${configPath}: unknown hook event "${eventName}". ` +
392
+ `Valid events: ${VALID_HOOK_EVENTS.join(', ')}`
393
+ );
394
+ }
395
+
396
+ const event = eventName as HookEvent;
397
+
398
+ if (!Array.isArray(definitions)) {
399
+ throw new Error(
400
+ `Invalid hooks config at ${configPath}: "${eventName}" must be an array of hook definitions`
401
+ );
402
+ }
403
+
404
+ for (let i = 0; i < definitions.length; i++) {
405
+ const def = definitions[i] as Record<string, unknown>;
406
+
407
+ if (typeof def !== 'object' || def === null) {
408
+ throw new Error(
409
+ `Invalid hooks config at ${configPath}: ${eventName}[${i}] must be an object`
410
+ );
411
+ }
412
+
413
+ const hookDef: HookDefinition = {
414
+ match: String(def.match ?? ''),
415
+ command: String(def.command ?? ''),
416
+ timeout:
417
+ def.timeout !== undefined && def.timeout !== null ? Number(def.timeout) : undefined,
418
+ };
419
+
420
+ const validationErrors = validateHookDefinition(hookDef);
421
+ if (validationErrors.length > 0) {
422
+ throw new Error(
423
+ `Invalid hooks config at ${configPath}: ${eventName}[${i}]: ${validationErrors.join('; ')}`
424
+ );
425
+ }
426
+
427
+ config.hooks[event].push(hookDef);
428
+ }
429
+ }
430
+
431
+ return config;
432
+ }