@marcopeg/hal 1.0.18 → 1.0.22

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 (163) hide show
  1. package/README.md +90 -743
  2. package/dist/agent/index.d.ts +3 -3
  3. package/dist/agent/index.d.ts.map +1 -1
  4. package/dist/agent/index.js +5 -5
  5. package/dist/agent/index.js.map +1 -1
  6. package/dist/bot/commands/git/callback.d.ts +8 -0
  7. package/dist/bot/commands/git/callback.d.ts.map +1 -0
  8. package/dist/bot/commands/git/callback.js +72 -0
  9. package/dist/bot/commands/git/callback.js.map +1 -0
  10. package/dist/bot/commands/git/clean.d.ts +4 -0
  11. package/dist/bot/commands/git/clean.d.ts.map +1 -0
  12. package/dist/bot/commands/git/clean.js +50 -0
  13. package/dist/bot/commands/git/clean.js.map +1 -0
  14. package/dist/bot/commands/git/commit.d.ts +4 -0
  15. package/dist/bot/commands/git/commit.d.ts.map +1 -0
  16. package/dist/bot/commands/git/commit.js +72 -0
  17. package/dist/bot/commands/git/commit.js.map +1 -0
  18. package/dist/bot/commands/git/exec.d.ts +6 -0
  19. package/dist/bot/commands/git/exec.d.ts.map +1 -0
  20. package/dist/bot/commands/git/exec.js +16 -0
  21. package/dist/bot/commands/git/exec.js.map +1 -0
  22. package/dist/bot/commands/git/index.d.ts +6 -0
  23. package/dist/bot/commands/git/index.d.ts.map +1 -0
  24. package/dist/bot/commands/git/index.js +6 -0
  25. package/dist/bot/commands/git/index.js.map +1 -0
  26. package/dist/bot/commands/git/init.d.ts +4 -0
  27. package/dist/bot/commands/git/init.d.ts.map +1 -0
  28. package/dist/bot/commands/git/init.js +22 -0
  29. package/dist/bot/commands/git/init.js.map +1 -0
  30. package/dist/bot/commands/git/status.d.ts +4 -0
  31. package/dist/bot/commands/git/status.d.ts.map +1 -0
  32. package/dist/bot/commands/git/status.js +18 -0
  33. package/dist/bot/commands/git/status.js.map +1 -0
  34. package/dist/bot/commands/help.js +1 -1
  35. package/dist/bot/commands/help.js.map +1 -1
  36. package/dist/bot/commands/loader.d.ts +14 -6
  37. package/dist/bot/commands/loader.d.ts.map +1 -1
  38. package/dist/bot/commands/loader.js +126 -34
  39. package/dist/bot/commands/loader.js.map +1 -1
  40. package/dist/bot/commands/message.d.ts.map +1 -1
  41. package/dist/bot/commands/message.js +51 -25
  42. package/dist/bot/commands/message.js.map +1 -1
  43. package/dist/bot/commands/model-callback.d.ts +4 -0
  44. package/dist/bot/commands/model-callback.d.ts.map +1 -0
  45. package/dist/bot/commands/model-callback.js +34 -0
  46. package/dist/bot/commands/model-callback.js.map +1 -0
  47. package/dist/bot/commands/model.d.ts +4 -0
  48. package/dist/bot/commands/model.d.ts.map +1 -0
  49. package/dist/bot/commands/model.js +83 -0
  50. package/dist/bot/commands/model.js.map +1 -0
  51. package/dist/bot/commands/reset.d.ts +3 -2
  52. package/dist/bot/commands/reset.d.ts.map +1 -1
  53. package/dist/bot/commands/reset.js +73 -10
  54. package/dist/bot/commands/reset.js.map +1 -1
  55. package/dist/bot/commands/resetPrompt.d.ts +20 -0
  56. package/dist/bot/commands/resetPrompt.d.ts.map +1 -0
  57. package/dist/bot/commands/resetPrompt.js +58 -0
  58. package/dist/bot/commands/resetPrompt.js.map +1 -0
  59. package/dist/bot/commands/session.d.ts +2 -2
  60. package/dist/bot/commands/session.d.ts.map +1 -1
  61. package/dist/bot/commands/session.js +7 -4
  62. package/dist/bot/commands/session.js.map +1 -1
  63. package/dist/bot/commands/start.js +2 -2
  64. package/dist/bot/commands/start.js.map +1 -1
  65. package/dist/bot/commands/watcher.d.ts +2 -1
  66. package/dist/bot/commands/watcher.d.ts.map +1 -1
  67. package/dist/bot/commands/watcher.js +11 -5
  68. package/dist/bot/commands/watcher.js.map +1 -1
  69. package/dist/bot/handlers/text.d.ts.map +1 -1
  70. package/dist/bot/handlers/text.js +11 -6
  71. package/dist/bot/handlers/text.js.map +1 -1
  72. package/dist/bot/middleware/auth.d.ts +0 -3
  73. package/dist/bot/middleware/auth.d.ts.map +1 -1
  74. package/dist/bot/middleware/auth.js +13 -12
  75. package/dist/bot/middleware/auth.js.map +1 -1
  76. package/dist/bot.d.ts.map +1 -1
  77. package/dist/bot.js +43 -11
  78. package/dist/bot.js.map +1 -1
  79. package/dist/cli.js +52 -29
  80. package/dist/cli.js.map +1 -1
  81. package/dist/config-watcher.d.ts +10 -0
  82. package/dist/config-watcher.d.ts.map +1 -0
  83. package/dist/config-watcher.js +55 -0
  84. package/dist/config-watcher.js.map +1 -0
  85. package/dist/config-writer.d.ts +8 -0
  86. package/dist/config-writer.d.ts.map +1 -0
  87. package/dist/config-writer.js +57 -0
  88. package/dist/config-writer.js.map +1 -0
  89. package/dist/config.d.ts +338 -53
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +337 -92
  92. package/dist/config.js.map +1 -1
  93. package/dist/context/resolver.d.ts +4 -0
  94. package/dist/context/resolver.d.ts.map +1 -1
  95. package/dist/context/resolver.js +8 -2
  96. package/dist/context/resolver.js.map +1 -1
  97. package/dist/default-models.d.ts +3 -0
  98. package/dist/default-models.d.ts.map +1 -0
  99. package/dist/default-models.js +16 -0
  100. package/dist/default-models.js.map +1 -0
  101. package/dist/engine/adapters/antigravity.d.ts +13 -0
  102. package/dist/engine/adapters/antigravity.d.ts.map +1 -0
  103. package/dist/engine/adapters/antigravity.js +230 -0
  104. package/dist/engine/adapters/antigravity.js.map +1 -0
  105. package/dist/engine/adapters/claude.js +2 -2
  106. package/dist/engine/adapters/claude.js.map +1 -1
  107. package/dist/engine/adapters/codex.d.ts.map +1 -1
  108. package/dist/engine/adapters/codex.js +27 -8
  109. package/dist/engine/adapters/codex.js.map +1 -1
  110. package/dist/engine/adapters/copilot.d.ts.map +1 -1
  111. package/dist/engine/adapters/copilot.js +6 -3
  112. package/dist/engine/adapters/copilot.js.map +1 -1
  113. package/dist/engine/adapters/cursor.d.ts +3 -0
  114. package/dist/engine/adapters/cursor.d.ts.map +1 -0
  115. package/dist/engine/adapters/cursor.js +106 -0
  116. package/dist/engine/adapters/cursor.js.map +1 -0
  117. package/dist/engine/adapters/opencode.d.ts +2 -2
  118. package/dist/engine/adapters/opencode.d.ts.map +1 -1
  119. package/dist/engine/adapters/opencode.js +33 -13
  120. package/dist/engine/adapters/opencode.js.map +1 -1
  121. package/dist/engine/detect.d.ts +26 -0
  122. package/dist/engine/detect.d.ts.map +1 -0
  123. package/dist/engine/detect.js +129 -0
  124. package/dist/engine/detect.js.map +1 -0
  125. package/dist/engine/model-cache.d.ts +25 -0
  126. package/dist/engine/model-cache.d.ts.map +1 -0
  127. package/dist/engine/model-cache.js +162 -0
  128. package/dist/engine/model-cache.js.map +1 -0
  129. package/dist/engine/model-validation.d.ts +9 -0
  130. package/dist/engine/model-validation.d.ts.map +1 -0
  131. package/dist/engine/model-validation.js +21 -0
  132. package/dist/engine/model-validation.js.map +1 -0
  133. package/dist/engine/models-data.generated.d.ts +4 -0
  134. package/dist/engine/models-data.generated.d.ts.map +1 -0
  135. package/dist/engine/models-data.generated.js +196 -0
  136. package/dist/engine/models-data.generated.js.map +1 -0
  137. package/dist/engine/models-fetch.d.ts +5 -0
  138. package/dist/engine/models-fetch.d.ts.map +1 -0
  139. package/dist/engine/models-fetch.js +54 -0
  140. package/dist/engine/models-fetch.js.map +1 -0
  141. package/dist/engine/probe-utils.d.ts +6 -0
  142. package/dist/engine/probe-utils.d.ts.map +1 -0
  143. package/dist/engine/probe-utils.js +42 -0
  144. package/dist/engine/probe-utils.js.map +1 -0
  145. package/dist/engine/prompt.d.ts.map +1 -1
  146. package/dist/engine/prompt.js +8 -0
  147. package/dist/engine/prompt.js.map +1 -1
  148. package/dist/engine/registry.d.ts.map +1 -1
  149. package/dist/engine/registry.js +4 -0
  150. package/dist/engine/registry.js.map +1 -1
  151. package/dist/engine/types.d.ts +3 -3
  152. package/dist/engine/types.d.ts.map +1 -1
  153. package/dist/engine/types.js +2 -0
  154. package/dist/engine/types.js.map +1 -1
  155. package/dist/prompts.d.ts +19 -0
  156. package/dist/prompts.d.ts.map +1 -0
  157. package/dist/prompts.js +73 -0
  158. package/dist/prompts.js.map +1 -0
  159. package/dist/setup-wizard.d.ts +4 -0
  160. package/dist/setup-wizard.d.ts.map +1 -0
  161. package/dist/setup-wizard.js +258 -0
  162. package/dist/setup-wizard.js.map +1 -0
  163. package/package.json +6 -2
package/dist/config.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { isAbsolute, join, resolve } from "node:path";
2
+ import { basename, isAbsolute, join, resolve } from "node:path";
3
3
  import { parse as parseEnv } from "dotenv";
4
+ import stripJsonComments from "strip-json-comments";
5
+ import { parse as parseYaml } from "yaml";
4
6
  import { z } from "zod";
5
7
  // ─── Zod helpers ──────────────────────────────────────────────────────────────
6
8
  const TranscriptionModelSchema = z.enum([
@@ -18,7 +20,29 @@ const TranscriptionModelSchema = z.enum([
18
20
  ]);
19
21
  const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
20
22
  // ─── Globals schema (all fields optional) ─────────────────────────────────────
21
- const EngineNameSchema = z.enum(["claude", "copilot", "codex", "opencode"]);
23
+ const EngineNameSchema = z.enum([
24
+ "claude",
25
+ "copilot",
26
+ "codex",
27
+ "opencode",
28
+ "cursor",
29
+ "antigravity",
30
+ ]);
31
+ const CodexEngineConfigSchema = z
32
+ .object({
33
+ networkAccess: z.boolean(),
34
+ fullDiskAccess: z.boolean(),
35
+ dangerouslyEnableYolo: z.boolean(),
36
+ })
37
+ .partial()
38
+ .optional();
39
+ const AntigravityEngineConfigSchema = z
40
+ .object({
41
+ approvalMode: z.enum(["default", "auto_edit", "yolo"]),
42
+ sandbox: z.boolean(),
43
+ })
44
+ .partial()
45
+ .optional();
22
46
  const EngineConfigSchema = z
23
47
  .object({
24
48
  name: EngineNameSchema,
@@ -26,6 +50,8 @@ const EngineConfigSchema = z
26
50
  model: z.string(),
27
51
  session: z.boolean(),
28
52
  sessionMsg: z.string(),
53
+ codex: CodexEngineConfigSchema,
54
+ antigravity: AntigravityEngineConfigSchema,
29
55
  })
30
56
  .partial()
31
57
  .optional();
@@ -39,30 +65,72 @@ const CommandMessageSchema = z
39
65
  });
40
66
  const StartConfigSchema = z
41
67
  .object({
68
+ enabled: z.boolean().optional(),
42
69
  session: z.object({ reset: z.boolean() }).partial().optional(),
43
- message: CommandMessageSchema,
70
+ message: CommandMessageSchema.optional(),
44
71
  })
45
72
  .optional();
46
73
  const SimpleCommandConfigSchema = z
47
74
  .object({
48
- message: CommandMessageSchema,
75
+ enabled: z.boolean().optional(),
76
+ message: CommandMessageSchema.optional(),
77
+ })
78
+ .optional();
79
+ const ResetCommandConfigSchema = z
80
+ .object({
81
+ enabled: z.boolean().optional(),
82
+ session: z.object({ reset: z.boolean() }).partial().optional(),
83
+ message: z
84
+ .object({
85
+ confirm: z.string().optional(),
86
+ done: z.string().optional(),
87
+ })
88
+ .optional(),
89
+ timeout: z.number().positive().optional(),
90
+ })
91
+ .optional();
92
+ const GitConfigSchema = z
93
+ .object({
94
+ enabled: z.boolean().optional(),
49
95
  })
50
96
  .optional();
51
97
  const CommandsConfigSchema = z
52
98
  .object({
53
99
  start: StartConfigSchema,
54
100
  help: SimpleCommandConfigSchema,
55
- reset: SimpleCommandConfigSchema,
101
+ reset: ResetCommandConfigSchema,
56
102
  clean: SimpleCommandConfigSchema,
103
+ git: GitConfigSchema,
104
+ model: GitConfigSchema,
57
105
  })
58
106
  .optional();
107
+ const ProviderModelSchema = z.object({
108
+ name: z.string().min(1),
109
+ description: z.string().optional(),
110
+ });
111
+ const ProvidersConfigSchema = z
112
+ .object({
113
+ claude: z.array(ProviderModelSchema).optional(),
114
+ copilot: z.array(ProviderModelSchema).optional(),
115
+ codex: z.array(ProviderModelSchema).optional(),
116
+ opencode: z.array(ProviderModelSchema).optional(),
117
+ cursor: z.array(ProviderModelSchema).optional(),
118
+ antigravity: z.array(ProviderModelSchema).optional(),
119
+ })
120
+ .optional();
121
+ const AllowedUserIdSchema = z.union([z.number(), z.string()]);
122
+ const AccessSchema = z
123
+ .object({
124
+ allowedUserIds: z.array(AllowedUserIdSchema),
125
+ dangerouslyAllowUnrestrictedAccess: z.boolean(),
126
+ })
127
+ .partial()
128
+ .optional();
59
129
  const GlobalsFileSchema = z
60
130
  .object({
61
- access: z
62
- .object({ allowedUserIds: z.array(z.number()) })
63
- .partial()
64
- .optional(),
131
+ access: AccessSchema,
65
132
  engine: EngineConfigSchema,
133
+ providers: ProvidersConfigSchema,
66
134
  logging: z
67
135
  .object({
68
136
  level: LogLevelSchema,
@@ -94,11 +162,9 @@ const ProjectFileSchema = z.object({
94
162
  telegram: z.object({
95
163
  botToken: z.string().min(1, "project.telegram.botToken is required"),
96
164
  }),
97
- access: z
98
- .object({ allowedUserIds: z.array(z.number()) })
99
- .partial()
100
- .optional(),
165
+ access: AccessSchema,
101
166
  engine: EngineConfigSchema,
167
+ providers: ProvidersConfigSchema,
102
168
  logging: z
103
169
  .object({
104
170
  level: LogLevelSchema,
@@ -142,6 +208,52 @@ const LocalConfigFileSchema = z
142
208
  projects: z.array(LocalProjectSchema).optional(),
143
209
  })
144
210
  .optional();
211
+ // ─── Config load result & errors ───────────────────────────────────────────────
212
+ export class ConfigLoadError extends Error {
213
+ constructor(message) {
214
+ super(message);
215
+ this.name = "ConfigLoadError";
216
+ }
217
+ }
218
+ // Telegram user ID range (Bot API): 1 to 0xFFFFFFFFF inclusive
219
+ const TELEGRAM_USER_ID_MAX = 0xfffffffff;
220
+ function parseTelegramUserId(value, path) {
221
+ const str = typeof value === "string" ? value : String(value);
222
+ const num = Number(str);
223
+ if (!Number.isFinite(num) || !Number.isInteger(num)) {
224
+ throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: "${str}" is not a valid integer`);
225
+ }
226
+ if (typeof value === "string" && String(num) !== str) {
227
+ throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: "${str}" (expected exact integer form, no spaces/decimals/leading zeros)`);
228
+ }
229
+ if (num < 1 || num > TELEGRAM_USER_ID_MAX) {
230
+ throw new ConfigLoadError(`Configuration error: invalid allowedUserIds entry at ${path}: ${num} is outside Telegram user ID range (1–${TELEGRAM_USER_ID_MAX})`);
231
+ }
232
+ return num;
233
+ }
234
+ function normalizeAllowedUserIdsInConfig(config) {
235
+ const globalsAccess = config.globals?.access;
236
+ if (globalsAccess?.allowedUserIds != null) {
237
+ const raw = globalsAccess.allowedUserIds;
238
+ const normalized = [];
239
+ for (let i = 0; i < raw.length; i++) {
240
+ normalized.push(parseTelegramUserId(raw[i], `globals.access.allowedUserIds[${i}]`));
241
+ }
242
+ globalsAccess.allowedUserIds = normalized;
243
+ }
244
+ for (let j = 0; j < config.projects.length; j++) {
245
+ const project = config.projects[j];
246
+ const access = project.access;
247
+ if (access?.allowedUserIds == null)
248
+ continue;
249
+ const raw = access.allowedUserIds;
250
+ const normalized = [];
251
+ for (let i = 0; i < raw.length; i++) {
252
+ normalized.push(parseTelegramUserId(raw[i], `projects[${j}].access.allowedUserIds[${i}]`));
253
+ }
254
+ access.allowedUserIds = normalized;
255
+ }
256
+ }
145
257
  // ─── Slug derivation ──────────────────────────────────────────────────────────
146
258
  export function deriveSlug(name, cwd) {
147
259
  if (name)
@@ -181,39 +293,78 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
181
293
  if (msg.from) {
182
294
  const filePath = resolve(resolvedCwd, msg.from);
183
295
  if (!existsSync(filePath)) {
184
- console.error(`Configuration error: ${label}.message.from file not found: ${filePath}`);
185
- process.exit(1);
296
+ throw new ConfigLoadError(`Configuration error: ${label}.message.from file not found: ${filePath}`);
186
297
  }
187
298
  try {
188
299
  return readFileSync(filePath, "utf-8");
189
300
  }
190
301
  catch (err) {
191
- console.error(`Configuration error: cannot read ${label}.message.from file: ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
192
- process.exit(1);
302
+ throw new ConfigLoadError(`Configuration error: cannot read ${label}.message.from file: ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
193
303
  }
194
304
  }
195
305
  return msg.text;
196
306
  }
307
+ // Resolve command enabled flags (project > globals > default)
197
308
  const rawStart = project.commands?.start ?? globals.commands?.start;
198
- let resolvedStart;
199
- if (rawStart) {
200
- resolvedStart = {
201
- sessionReset: rawStart.session?.reset ?? false,
202
- message: resolveMessageTemplate(rawStart.message, "commands.start"),
203
- };
204
- }
205
309
  const rawHelp = project.commands?.help ?? globals.commands?.help;
206
- const resolvedHelp = rawHelp
207
- ? { message: resolveMessageTemplate(rawHelp.message, "commands.help") }
208
- : undefined;
209
310
  const rawReset = project.commands?.reset ?? globals.commands?.reset;
210
- const resolvedReset = rawReset
211
- ? { message: resolveMessageTemplate(rawReset.message, "commands.reset") }
212
- : undefined;
213
311
  const rawClean = project.commands?.clean ?? globals.commands?.clean;
214
- const resolvedClean = rawClean
215
- ? { message: resolveMessageTemplate(rawClean.message, "commands.clean") }
216
- : undefined;
312
+ const resolvedCommands = {
313
+ start: {
314
+ enabled: project.commands?.start?.enabled ??
315
+ globals.commands?.start?.enabled ??
316
+ true,
317
+ sessionReset: rawStart?.session?.reset ?? false,
318
+ message: rawStart?.message
319
+ ? resolveMessageTemplate(rawStart.message, "commands.start")
320
+ : undefined,
321
+ },
322
+ help: {
323
+ enabled: project.commands?.help?.enabled ??
324
+ globals.commands?.help?.enabled ??
325
+ true,
326
+ message: rawHelp?.message
327
+ ? resolveMessageTemplate(rawHelp.message, "commands.help")
328
+ : undefined,
329
+ },
330
+ reset: {
331
+ enabled: project.commands?.reset?.enabled ??
332
+ globals.commands?.reset?.enabled ??
333
+ true,
334
+ sessionReset: rawReset?.session?.reset ?? false,
335
+ message: {
336
+ confirm: rawReset?.message?.confirm,
337
+ done: rawReset?.message?.done,
338
+ },
339
+ timeout: rawReset?.timeout ?? 60,
340
+ },
341
+ clean: {
342
+ enabled: project.commands?.clean?.enabled ??
343
+ globals.commands?.clean?.enabled ??
344
+ true,
345
+ message: rawClean?.message
346
+ ? resolveMessageTemplate(rawClean.message, "commands.clean")
347
+ : undefined,
348
+ },
349
+ git: {
350
+ enabled: project.commands?.git?.enabled ??
351
+ globals.commands?.git?.enabled ??
352
+ false,
353
+ },
354
+ model: {
355
+ enabled: project.commands?.model?.enabled ??
356
+ globals.commands?.model?.enabled ??
357
+ true,
358
+ },
359
+ };
360
+ const engineName = (project.engine?.name ??
361
+ globals.engine?.name ??
362
+ "claude");
363
+ const rawProviderModels = project.providers?.[engineName] ?? globals.providers?.[engineName] ?? [];
364
+ const providerModels = rawProviderModels.map((m) => ({
365
+ name: m.name,
366
+ description: m.description,
367
+ }));
217
368
  return {
218
369
  slug,
219
370
  name: project.name,
@@ -223,19 +374,37 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
223
374
  logDir,
224
375
  telegram: { botToken: project.telegram.botToken },
225
376
  access: {
226
- allowedUserIds: project.access?.allowedUserIds ?? globals.access?.allowedUserIds ?? [],
377
+ allowedUserIds: ((project.access !== undefined
378
+ ? project.access.allowedUserIds
379
+ : globals.access?.allowedUserIds) ?? []),
380
+ dangerouslyAllowUnrestrictedAccess: (project.access !== undefined
381
+ ? project.access.dangerouslyAllowUnrestrictedAccess
382
+ : globals.access?.dangerouslyAllowUnrestrictedAccess) ?? false,
227
383
  },
228
- engine: (project.engine?.name ??
229
- globals.engine?.name ??
230
- "claude"),
231
- engineCommand: project.engine?.command ??
232
- globals.engine?.command ??
233
- project.engine?.name ??
234
- globals.engine?.name ??
235
- "claude",
384
+ engine: engineName,
385
+ engineCommand: project.engine?.command ?? globals.engine?.command,
236
386
  engineModel: project.engine?.model ?? globals.engine?.model,
237
387
  engineSession: project.engine?.session ?? globals.engine?.session ?? true,
238
388
  engineSessionMsg: project.engine?.sessionMsg ?? globals.engine?.sessionMsg ?? "hi!",
389
+ codex: {
390
+ networkAccess: project.engine?.codex?.networkAccess ??
391
+ globals.engine?.codex?.networkAccess ??
392
+ false,
393
+ fullDiskAccess: project.engine?.codex?.fullDiskAccess ??
394
+ globals.engine?.codex?.fullDiskAccess ??
395
+ false,
396
+ dangerouslyEnableYolo: project.engine?.codex?.dangerouslyEnableYolo ??
397
+ globals.engine?.codex?.dangerouslyEnableYolo ??
398
+ false,
399
+ },
400
+ antigravity: {
401
+ approvalMode: project.engine?.antigravity?.approvalMode ??
402
+ globals.engine?.antigravity?.approvalMode ??
403
+ "yolo",
404
+ sandbox: project.engine?.antigravity?.sandbox ??
405
+ globals.engine?.antigravity?.sandbox ??
406
+ false,
407
+ },
239
408
  logging: {
240
409
  level: project.logging?.level ?? globals.logging?.level ?? "info",
241
410
  flow: project.logging?.flow ?? globals.logging?.flow ?? true,
@@ -255,13 +424,9 @@ export function resolveProjectConfig(project, globals, configDir, rootContext) {
255
424
  true,
256
425
  }
257
426
  : undefined,
427
+ providerModels,
258
428
  context: hasContext ? { ...rootContext, ...project.context } : undefined,
259
- commands: {
260
- start: resolvedStart,
261
- help: resolvedHelp,
262
- reset: resolvedReset,
263
- clean: resolvedClean,
264
- },
429
+ commands: resolvedCommands,
265
430
  };
266
431
  }
267
432
  // ─── Boot-time uniqueness validation ──────────────────────────────────────────
@@ -271,24 +436,37 @@ export function validateProjects(projects) {
271
436
  const names = new Set();
272
437
  for (const project of projects) {
273
438
  if (cwds.has(project.cwd)) {
274
- console.error(`Configuration error: duplicate project cwd "${project.cwd}". Each project must have a unique cwd.`);
275
- process.exit(1);
439
+ throw new ConfigLoadError(`Configuration error: duplicate project cwd "${project.cwd}". Each project must have a unique cwd.`);
276
440
  }
277
441
  cwds.add(project.cwd);
278
442
  if (tokens.has(project.telegram.botToken)) {
279
- console.error(`Configuration error: duplicate botToken in project "${project.slug}". Each project must use a unique Telegram bot token.`);
280
- process.exit(1);
443
+ throw new ConfigLoadError(`Configuration error: duplicate botToken in project "${project.slug}". Each project must use a unique Telegram bot token.`);
281
444
  }
282
445
  tokens.add(project.telegram.botToken);
283
446
  if (project.name) {
284
447
  if (names.has(project.name)) {
285
- console.error(`Configuration error: duplicate project name "${project.name}". Each named project must have a unique name.`);
286
- process.exit(1);
448
+ throw new ConfigLoadError(`Configuration error: duplicate project name "${project.name}". Each named project must have a unique name.`);
287
449
  }
288
450
  names.add(project.name);
289
451
  }
290
452
  }
291
453
  }
454
+ // ─── Boot-time access policy validation ───────────────────────────────────────
455
+ export function validateAccessPolicies(projects) {
456
+ const errors = [];
457
+ for (const project of projects) {
458
+ const { allowedUserIds, dangerouslyAllowUnrestrictedAccess } = project.access;
459
+ const hasUsers = allowedUserIds.length > 0;
460
+ const hasUnsafe = dangerouslyAllowUnrestrictedAccess === true;
461
+ if (!hasUsers && !hasUnsafe) {
462
+ errors.push(`Project "${project.slug}": no access policy configured. ` +
463
+ "Set access.allowedUserIds or access.dangerouslyAllowUnrestrictedAccess.");
464
+ }
465
+ }
466
+ if (errors.length > 0) {
467
+ throw new ConfigLoadError(`Configuration error: invalid access policy\n${errors.map((e) => ` - ${e}`).join("\n")}`);
468
+ }
469
+ }
292
470
  function loadEnvFiles(configDir, projectCwds) {
293
471
  const loadedFiles = [];
294
472
  const vars = {};
@@ -323,9 +501,8 @@ function substituteEnvVars(obj, env, path = "") {
323
501
  return obj.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
324
502
  const value = env[varName] ?? process.env[varName];
325
503
  if (value === undefined) {
326
- console.error(`Configuration error: environment variable "${varName}" is not defined\n` +
504
+ throw new ConfigLoadError(`Configuration error: environment variable "${varName}" is not defined\n` +
327
505
  ` (referenced in field: ${path || "<root>"})`);
328
- process.exit(1);
329
506
  }
330
507
  return value;
331
508
  });
@@ -369,32 +546,76 @@ function deepMerge(base, override) {
369
546
  }
370
547
  return result;
371
548
  }
372
- // ─── Phase 3: Local config loading ───────────────────────────────────────────
549
+ const CONFIG_EXTENSIONS = [
550
+ { ext: ".json", format: "json" },
551
+ { ext: ".jsonc", format: "jsonc" },
552
+ { ext: ".yaml", format: "yaml" },
553
+ { ext: ".yml", format: "yaml" },
554
+ ];
555
+ /**
556
+ * Scan configDir for a config file matching the given slot basename
557
+ * (e.g. "hal.config" or "hal.config.local") in any supported format.
558
+ * Returns null when no file is found.
559
+ * Throws ConfigLoadError when multiple formats exist for the same slot.
560
+ */
561
+ export function resolveConfigFile(configDir, slotBasename) {
562
+ const found = [];
563
+ for (const { ext, format } of CONFIG_EXTENSIONS) {
564
+ const filePath = join(configDir, `${slotBasename}${ext}`);
565
+ if (existsSync(filePath)) {
566
+ found.push({ path: filePath, format });
567
+ }
568
+ }
569
+ if (found.length === 0)
570
+ return null;
571
+ if (found.length === 1)
572
+ return found[0];
573
+ const names = found.map((f) => basename(f.path)).join(", ");
574
+ throw new ConfigLoadError(`Configuration error: multiple config files found for "${slotBasename}": ${names}\n` +
575
+ " Only one format per config file is allowed. Remove the extras.");
576
+ }
577
+ /**
578
+ * Parse raw config file content using the appropriate parser for the format.
579
+ */
580
+ export function parseConfigContent(content, format, filePath) {
581
+ try {
582
+ if (format === "yaml") {
583
+ return parseYaml(content);
584
+ }
585
+ if (format === "jsonc") {
586
+ return JSON.parse(stripJsonComments(content, { trailingCommas: true }));
587
+ }
588
+ return JSON.parse(content);
589
+ }
590
+ catch (err) {
591
+ throw new ConfigLoadError(`Configuration error: failed to parse ${basename(filePath)} — ${err instanceof Error ? err.message : String(err)}`);
592
+ }
593
+ }
373
594
  function loadLocalConfig(configDir) {
374
- const localPath = join(configDir, "hal.config.local.json");
375
- if (!existsSync(localPath))
595
+ const resolved = resolveConfigFile(configDir, "hal.config.local");
596
+ if (!resolved)
376
597
  return null;
377
598
  let raw;
378
599
  try {
379
- const content = readFileSync(localPath, "utf-8");
380
- raw = JSON.parse(content);
600
+ const content = readFileSync(resolved.path, "utf-8");
601
+ raw = parseConfigContent(content, resolved.format, resolved.path);
381
602
  }
382
603
  catch (err) {
383
- console.error(`Configuration error: failed to read hal.config.local.json — ${err instanceof Error ? err.message : String(err)}`);
384
- process.exit(1);
604
+ if (err instanceof ConfigLoadError)
605
+ throw err;
606
+ throw new ConfigLoadError(`Configuration error: failed to read ${basename(resolved.path)} — ${err instanceof Error ? err.message : String(err)}`);
385
607
  }
386
608
  const result = LocalConfigFileSchema.safeParse(raw);
387
609
  if (!result.success) {
388
610
  const issues = result.error.issues
389
611
  .map((i) => ` - ${i.path.join(".")}: ${i.message}`)
390
612
  .join("\n");
391
- console.error(`Configuration error in hal.config.local.json:\n${issues}`);
392
- process.exit(1);
613
+ throw new ConfigLoadError(`Configuration error in ${basename(resolved.path)}:\n${issues}`);
393
614
  }
394
- return result.data ?? null;
615
+ return result.data ? { config: result.data, path: resolved.path } : null;
395
616
  }
396
617
  // ─── Phase 3: Merge local into base ──────────────────────────────────────────
397
- function mergeLocalIntoBase(base, local) {
618
+ function mergeLocalIntoBase(base, local, baseFileName, localFileName) {
398
619
  const mergedGlobals = local.globals !== undefined
399
620
  ? deepMerge(base.globals ?? {}, local.globals)
400
621
  : base.globals;
@@ -417,9 +638,8 @@ function mergeLocalIntoBase(base, local) {
417
638
  return false;
418
639
  });
419
640
  if (idx === -1) {
420
- console.error(`Configuration error: local project "${matchKey}" not found in hal.config.json.\n` +
421
- ` Every entry in hal.config.local.json projects must match a base project by name or cwd.`);
422
- process.exit(1);
641
+ throw new ConfigLoadError(`Configuration error: local project "${matchKey}" not found in ${baseFileName}.\n` +
642
+ ` Every entry in ${localFileName} projects must match a base project by name or cwd.`);
423
643
  }
424
644
  mergedProjects[idx] = deepMerge(mergedProjects[idx], localProject);
425
645
  }
@@ -429,42 +649,44 @@ function mergeLocalIntoBase(base, local) {
429
649
  projects: mergedProjects,
430
650
  };
431
651
  }
432
- // ─── Phase 4: Config file loading (public API) ────────────────────────────────
433
- export function loadMultiConfig(configDir) {
434
- const configPath = join(configDir, "hal.config.json");
435
- const localPath = join(configDir, "hal.config.local.json");
652
+ // ─── Phase 4: Config file loading (internal: throws on error) ──────────────────
653
+ function loadMultiConfigInternal(configDir) {
436
654
  const loadedFiles = [];
437
- // 1. Load base config
438
- if (!existsSync(configPath)) {
439
- console.error(`Configuration error: hal.config.json not found in ${configDir}\n` +
440
- `Run "npx @marcopeg/hal init" to create one.`);
441
- process.exit(1);
655
+ // 1. Detect and load base config
656
+ const baseResolved = resolveConfigFile(configDir, "hal.config");
657
+ const supportedExts = CONFIG_EXTENSIONS.map((e) => e.ext).join(", ");
658
+ if (!baseResolved) {
659
+ throw new ConfigLoadError(`Configuration error: no config file found in ${configDir}\n` +
660
+ ` Looked for: hal.config{${supportedExts}}\n` +
661
+ ` Run "npx @marcopeg/hal init" to create one.`);
442
662
  }
663
+ const baseFileName = basename(baseResolved.path);
443
664
  let rawBase;
444
665
  try {
445
- const content = readFileSync(configPath, "utf-8");
446
- rawBase = JSON.parse(content);
666
+ const content = readFileSync(baseResolved.path, "utf-8");
667
+ rawBase = parseConfigContent(content, baseResolved.format, baseResolved.path);
447
668
  }
448
669
  catch (err) {
449
- console.error(`Configuration error: failed to read hal.config.json — ${err instanceof Error ? err.message : String(err)}`);
450
- process.exit(1);
670
+ if (err instanceof ConfigLoadError)
671
+ throw err;
672
+ throw new ConfigLoadError(`Configuration error: failed to read ${baseFileName} — ${err instanceof Error ? err.message : String(err)}`);
451
673
  }
452
- loadedFiles.push(configPath);
674
+ loadedFiles.push(baseResolved.path);
453
675
  // 2. Validate base config schema
454
676
  const baseResult = MultiConfigFileSchema.safeParse(rawBase);
455
677
  if (!baseResult.success) {
456
678
  const issues = baseResult.error.issues
457
679
  .map((i) => ` - ${i.path.join(".")}: ${i.message}`)
458
680
  .join("\n");
459
- console.error(`Configuration error in hal.config.json:\n${issues}`);
460
- process.exit(1);
681
+ throw new ConfigLoadError(`Configuration error in ${baseFileName}:\n${issues}`);
461
682
  }
462
683
  let merged = baseResult.data;
463
684
  // 3. Load and merge local config
464
- const localConfig = loadLocalConfig(configDir);
465
- if (localConfig !== null) {
466
- loadedFiles.push(localPath);
467
- merged = mergeLocalIntoBase(merged, localConfig);
685
+ const localResult = loadLocalConfig(configDir);
686
+ if (localResult !== null) {
687
+ const localFileName = basename(localResult.path);
688
+ loadedFiles.push(localResult.path);
689
+ merged = mergeLocalIntoBase(merged, localResult.config, baseFileName, localFileName);
468
690
  }
469
691
  // 4. Load .env files (using raw cwds from merged config for path resolution)
470
692
  const rawCwds = merged.projects.map((p) => isAbsolute(p.cwd) ? p.cwd : resolve(configDir, p.cwd));
@@ -477,12 +699,35 @@ export function loadMultiConfig(configDir) {
477
699
  const issues = finalResult.error.issues
478
700
  .map((i) => ` - ${i.path.join(".")}: ${i.message}`)
479
701
  .join("\n");
480
- console.error(`Configuration error after environment variable substitution:\n${issues}`);
481
- process.exit(1);
702
+ throw new ConfigLoadError(`Configuration error after environment variable substitution:\n${issues}`);
482
703
  }
704
+ // 7. Normalize allowedUserIds (string | number)[] → number[] with validation
705
+ normalizeAllowedUserIdsInConfig(finalResult.data);
483
706
  return {
484
707
  config: finalResult.data,
485
708
  loadedFiles: [...loadedFiles, ...envSources.loadedFiles],
486
709
  };
487
710
  }
711
+ // ─── Public API ───────────────────────────────────────────────────────────────
712
+ /**
713
+ * Load multi-project config. On any error, logs and exits the process.
714
+ * Use for initial startup.
715
+ */
716
+ export function loadMultiConfig(configDir) {
717
+ try {
718
+ return loadMultiConfigInternal(configDir);
719
+ }
720
+ catch (err) {
721
+ const message = err instanceof Error ? err.message : String(err);
722
+ console.error(message);
723
+ process.exit(1);
724
+ }
725
+ }
726
+ /**
727
+ * Load multi-project config without exiting. Throws on error.
728
+ * Use for hot-reload so callers can log and retry on next file change.
729
+ */
730
+ export function tryLoadMultiConfig(configDir) {
731
+ return loadMultiConfigInternal(configDir);
732
+ }
488
733
  //# sourceMappingURL=config.js.map