@hilbras/omninode 2.0.0-alpha.15 → 2.0.0-alpha.17

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.
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import { realpathSync } from "fs";
23
23
  import { Command } from "commander";
24
24
 
25
25
  // src/version.ts
26
- var OMNINODE_VERSION = "2.0.0-alpha.15";
26
+ var OMNINODE_VERSION = "2.0.0-alpha.17";
27
27
 
28
28
  // src/cli/exit-codes.ts
29
29
  var EXIT = {
@@ -147,6 +147,10 @@ var projectConfigSchema = z.object({
147
147
  }).strict().optional()
148
148
  }).strict();
149
149
  var appConfigSchema = z.object({
150
+ /** Active profile; overlays live under `profiles` (§16). */
151
+ profile: z.string().min(1).optional(),
152
+ /** Named partial overlays, e.g. development/production/testing. */
153
+ profiles: z.record(z.unknown()).optional(),
150
154
  project: projectConfigSchema,
151
155
  logging: z.object({
152
156
  level: z.enum(["debug", "info", "warn", "error"]),
@@ -187,6 +191,85 @@ ${lines}
187
191
  OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
188
192
  }
189
193
 
194
+ // src/config/profiles.ts
195
+ import { homedir } from "os";
196
+ import path from "path";
197
+ function userConfigPath(env = process.env) {
198
+ if (env.OMNINODE_USER_CONFIG) return env.OMNINODE_USER_CONFIG;
199
+ const home = env.HOME ?? homedir();
200
+ return path.join(home, ".omninode", "config.yaml");
201
+ }
202
+ function resolveProfile(input, env) {
203
+ if (env.OMNINODE_PROFILE) return env.OMNINODE_PROFILE;
204
+ if (typeof input.profile === "string" && input.profile.length > 0) return input.profile;
205
+ return "default";
206
+ }
207
+ function mergeConfig(base, overlay) {
208
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
209
+ return overlay === void 0 ? base : overlay;
210
+ }
211
+ const result = { ...base };
212
+ for (const [key, value] of Object.entries(overlay)) {
213
+ const existing = result[key];
214
+ if (Array.isArray(value)) {
215
+ result[key] = value;
216
+ } else if (isPlainObject(value) && isPlainObject(existing)) {
217
+ result[key] = mergeConfig(existing, value);
218
+ } else {
219
+ result[key] = value;
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+ function isPlainObject(value) {
225
+ return value !== null && typeof value === "object" && !Array.isArray(value);
226
+ }
227
+ function asObject(value) {
228
+ return isPlainObject(value) ? value : {};
229
+ }
230
+ function applyEnvOverrides(input, env) {
231
+ const applied = [];
232
+ const config = mergeConfig(input, {});
233
+ if (env.OMNINODE_LOG_LEVEL) {
234
+ config.logging = { ...asObject(config.logging), level: env.OMNINODE_LOG_LEVEL };
235
+ applied.push("OMNINODE_LOG_LEVEL");
236
+ }
237
+ if (env.OMNINODE_LOG_FORMAT) {
238
+ config.logging = { ...asObject(config.logging), format: env.OMNINODE_LOG_FORMAT };
239
+ applied.push("OMNINODE_LOG_FORMAT");
240
+ }
241
+ const memoryProvider = env.OMNINODE_MEMORY_PROVIDER;
242
+ const memoryRequired = env.OMNINODE_MEMORY_REQUIRED;
243
+ if (memoryProvider || memoryRequired) {
244
+ const project = asObject(config.project);
245
+ config.project = {
246
+ ...project,
247
+ memory: {
248
+ ...asObject(project.memory ?? { provider: "local" }),
249
+ ...memoryProvider ? { provider: memoryProvider } : {},
250
+ ...memoryRequired ? { required: memoryRequired !== "false" } : {}
251
+ }
252
+ };
253
+ if (memoryProvider) applied.push("OMNINODE_MEMORY_PROVIDER");
254
+ if (memoryRequired) applied.push("OMNINODE_MEMORY_REQUIRED");
255
+ }
256
+ return { config, applied };
257
+ }
258
+ function applyDefaults(input) {
259
+ const applied = [];
260
+ const config = mergeConfig(input, {});
261
+ const logging = asObject(config.logging);
262
+ if (logging.level === void 0) {
263
+ config.logging = { ...logging, level: "info" };
264
+ applied.push("logging.level");
265
+ }
266
+ if (logging.format === void 0) {
267
+ config.logging = { ...asObject(config.logging), format: "text" };
268
+ applied.push("logging.format");
269
+ }
270
+ return { config, applied };
271
+ }
272
+
190
273
  // src/config/loader.ts
191
274
  import { existsSync, readFileSync } from "fs";
192
275
  import { parse as parseYaml } from "yaml";
@@ -213,38 +296,129 @@ function expandEnvRefs(text, env, source = "config") {
213
296
  }
214
297
  );
215
298
  }
216
- function loadConfig(options = {}) {
299
+ function loadConfigDetailed(options = {}) {
217
300
  const env = options.env ?? process.env;
218
- const path = options.path ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
219
- if (!path) {
301
+ const explicitPath = options.path;
302
+ let input = {};
303
+ let projectFile;
304
+ let userFile;
305
+ if (!explicitPath) {
306
+ const candidate = userConfigPath(env);
307
+ if (existsSync(candidate)) {
308
+ input = mergeConfig(input, readConfigDocument(candidate, env));
309
+ userFile = candidate;
310
+ }
311
+ }
312
+ const resolvedProject = explicitPath ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
313
+ if (resolvedProject) {
314
+ input = mergeConfig(input, readConfigDocument(resolvedProject, env));
315
+ projectFile = resolvedProject;
316
+ }
317
+ if (!projectFile && !userFile) {
220
318
  throw new ConfigError(
221
319
  "CONFIG_NOT_FOUND",
222
320
  "No omninode.yaml found. Run `omninode init` to create one, or pass an explicit path."
223
321
  );
224
322
  }
225
- const raw = readFileSync(path, "utf8");
323
+ const profile = options.profile ?? resolveProfile(input, env);
324
+ const overlay = input.profiles?.[profile];
325
+ if (overlay !== void 0) {
326
+ const { profiles: _drop, ...base } = input;
327
+ input = mergeConfig(base, overlay);
328
+ } else if (profile !== "default" && profile !== "") {
329
+ throw new ConfigError(
330
+ "CONFIG_INVALID",
331
+ `Profile "${profile}" is not defined in this configuration (known: ${Object.keys(input.profiles ?? {}).join(", ") || "none"}).`
332
+ );
333
+ }
334
+ const withEnv = applyEnvOverrides(input, env);
335
+ const withDefaults = applyDefaults(withEnv.config);
336
+ const parsed = appConfigSchema.safeParse(withDefaults.config);
337
+ if (!parsed.success) {
338
+ const issues = parsed.error.issues.map((issue) => {
339
+ const location = issue.path.join(".") || "(root)";
340
+ return `${location}: ${issue.message}`;
341
+ });
342
+ throw new ConfigError(
343
+ "CONFIG_INVALID",
344
+ `Invalid configuration:
345
+ ${issues.join("\n ")}`,
346
+ { details: { issues } }
347
+ );
348
+ }
349
+ assertUniqueNames(parsed.data);
350
+ return {
351
+ config: toAppConfig(parsed.data),
352
+ sources: {
353
+ ...projectFile !== void 0 ? { projectFile } : {},
354
+ ...userFile !== void 0 ? { userFile } : {},
355
+ profile,
356
+ envOverrides: withEnv.applied,
357
+ defaults: withDefaults.applied
358
+ }
359
+ };
360
+ }
361
+ function assertUniqueNames(data) {
362
+ const groups = [
363
+ ["provider", data.project.providers.map((p) => p.name)],
364
+ ["agent", data.project.agents.map((a) => a.name)],
365
+ ["role", data.project.roles.map((r) => r.id)],
366
+ ["pipeline", data.project.pipelines.map((p) => p.id)]
367
+ ];
368
+ for (const [kind, names] of groups) {
369
+ const seen = /* @__PURE__ */ new Set();
370
+ for (const name of names) {
371
+ if (seen.has(name)) {
372
+ throw new ConfigError(
373
+ "CONFIG_INVALID",
374
+ `Duplicate ${kind} name "${name}" \u2014 every ${kind} must have a unique name.`
375
+ );
376
+ }
377
+ seen.add(name);
378
+ }
379
+ }
380
+ }
381
+ function readConfigDocument(path2, optionsEnv) {
382
+ const raw = readFileSync(path2, "utf8");
226
383
  const secretFindings = scanForInlineSecrets(raw);
227
384
  if (secretFindings.length > 0) {
228
- throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
385
+ throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path2, secretFindings));
229
386
  }
230
- const expanded = expandEnvRefs(raw, env, path);
231
- let data;
387
+ const expanded = expandEnvRefs(raw, optionsEnv, path2);
232
388
  try {
233
- data = parseYaml(expanded);
389
+ return parseYaml(expanded);
234
390
  } catch (error) {
235
- throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path}.`, { cause: error });
391
+ throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path2}.`, { cause: error });
236
392
  }
237
- const parsed = appConfigSchema.safeParse(data);
238
- if (!parsed.success) {
239
- const issues = parsed.error.issues.map((issue) => {
240
- const location = issue.path.length > 0 ? issue.path.join(".") : "(root)";
241
- return `${location}: ${issue.message}`;
242
- });
243
- throw new ConfigError("CONFIG_INVALID", `Invalid configuration in ${path}.`, {
244
- details: { issues }
245
- });
393
+ }
394
+ function configDiagnostics(config) {
395
+ const notes = [];
396
+ for (const provider of config.project.providers) {
397
+ if (!provider.apiKeyEnvVar && provider.baseUrl.startsWith("https://")) {
398
+ notes.push(
399
+ `Provider "${provider.name}": API key reference is missing \u2014 add api_key_env_var: <ENV_VAR> unless this endpoint is intentionally keyless.`
400
+ );
401
+ }
246
402
  }
247
- return toAppConfig(parsed.data);
403
+ for (const agent of config.project.agents) {
404
+ if (!agent.command && agent.integration !== "custom") {
405
+ notes.push(`Agent "${agent.name}": no command configured \u2014 the process adapter needs one.`);
406
+ }
407
+ if (agent.allowExternalCwd) {
408
+ notes.push(`Agent "${agent.name}": may run outside the project root (allow_external_cwd).`);
409
+ }
410
+ }
411
+ for (const pipeline of config.project.pipelines) {
412
+ const ids = new Set(pipeline.steps.map((s) => s.id));
413
+ for (const step of pipeline.steps) {
414
+ for (const dep of step.dependsOn ?? []) {
415
+ if (!ids.has(dep)) {
416
+ notes.push(`Pipeline "${pipeline.id}": step "${step.id}" depends on unknown step "${dep}".`);
417
+ }
418
+ }
419
+ }
420
+ }
421
+ return notes;
248
422
  }
249
423
  function toAppConfig(yaml) {
250
424
  return {
@@ -372,6 +546,18 @@ function defaultProjectConfigYaml(projectName = "My Project") {
372
546
  `;
373
547
  }
374
548
 
549
+ // src/cli/options.ts
550
+ var cliConfigOptions = {};
551
+ function loadProjectConfig() {
552
+ return loadConfigDetailed({
553
+ ...cliConfigOptions.path !== void 0 ? { path: cliConfigOptions.path } : {},
554
+ ...cliConfigOptions.profile !== void 0 ? { profile: cliConfigOptions.profile } : {}
555
+ }).config;
556
+ }
557
+ function activeConfigPath() {
558
+ return cliConfigOptions.path ?? findConfigFile();
559
+ }
560
+
375
561
  // src/cli/commands/init.ts
376
562
  import { existsSync as existsSync2, writeFileSync } from "fs";
377
563
  async function runInit(options) {
@@ -459,7 +645,7 @@ function truncate(text, max) {
459
645
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
460
646
  }
461
647
  function appendToConfigList(listKey, item) {
462
- const configPath = findConfigFile();
648
+ const configPath = activeConfigPath();
463
649
  if (!configPath) {
464
650
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
465
651
  }
@@ -1365,15 +1551,15 @@ async function writeFileAtomic(target, contents, durability = "rename") {
1365
1551
  }
1366
1552
  }
1367
1553
  }
1368
- async function inspectDocument(path) {
1554
+ async function inspectDocument(path2) {
1369
1555
  let raw;
1370
1556
  try {
1371
- await stat(path);
1557
+ await stat(path2);
1372
1558
  } catch {
1373
1559
  return { status: "missing" };
1374
1560
  }
1375
1561
  try {
1376
- raw = await readFile(path, "utf8");
1562
+ raw = await readFile(path2, "utf8");
1377
1563
  } catch (error) {
1378
1564
  return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
1379
1565
  }
@@ -1393,21 +1579,21 @@ async function inspectDocument(path) {
1393
1579
  if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
1394
1580
  return { status: "current", schemaVersion: version, count: doc.items.length };
1395
1581
  }
1396
- async function readDocument(path, onCorrupt, quarantine = true) {
1397
- const state = await inspectDocument(path);
1582
+ async function readDocument(path2, onCorrupt, quarantine = true) {
1583
+ const state = await inspectDocument(path2);
1398
1584
  if (state.status === "missing" || state.status === "empty") return [];
1399
1585
  if (state.status === "future") {
1400
1586
  const error = new Error(
1401
- `${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
1587
+ `${path2} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
1402
1588
  );
1403
- onCorrupt?.(path, error);
1589
+ onCorrupt?.(path2, error);
1404
1590
  return [];
1405
1591
  }
1406
1592
  let raw;
1407
1593
  try {
1408
- raw = await readFile(path, "utf8");
1594
+ raw = await readFile(path2, "utf8");
1409
1595
  } catch (error) {
1410
- onCorrupt?.(path, error);
1596
+ onCorrupt?.(path2, error);
1411
1597
  return [];
1412
1598
  }
1413
1599
  try {
@@ -1416,16 +1602,16 @@ async function readDocument(path, onCorrupt, quarantine = true) {
1416
1602
  return parsed.items ?? [];
1417
1603
  } catch (error) {
1418
1604
  if (quarantine) {
1419
- const quarantinePath = `${path}.corrupt-${Date.now()}`;
1605
+ const quarantinePath = `${path2}.corrupt-${Date.now()}`;
1420
1606
  try {
1421
- await rename(path, quarantinePath);
1607
+ await rename(path2, quarantinePath);
1422
1608
  logger.child({ component: "persistence" }).warn(
1423
- `Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
1609
+ `Quarantined unreadable store file ${path2} \u2192 ${quarantinePath}`
1424
1610
  );
1425
1611
  } catch {
1426
1612
  }
1427
1613
  }
1428
- onCorrupt?.(path, error);
1614
+ onCorrupt?.(path2, error);
1429
1615
  return [];
1430
1616
  }
1431
1617
  }
@@ -1955,7 +2141,7 @@ function registerPipelineInspect(pipelineGroup) {
1955
2141
  function registerAgentInspect(agentGroup) {
1956
2142
  jsonOption(
1957
2143
  agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
1958
- const config = loadConfig();
2144
+ const config = loadProjectConfig();
1959
2145
  const agentConfig = config.project.agents.find((a) => a.name === name);
1960
2146
  if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
1961
2147
  const registry = buildAgentRegistry(config);
@@ -1985,7 +2171,7 @@ function registerAgentInspect(agentGroup) {
1985
2171
  function registerProviderInspect(providerGroup) {
1986
2172
  jsonOption(
1987
2173
  providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
1988
- const config = loadConfig();
2174
+ const config = loadProjectConfig();
1989
2175
  const providerConfig = config.project.providers.find((p) => p.name === name);
1990
2176
  if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
1991
2177
  const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
@@ -2018,7 +2204,7 @@ function registerProviderInspect(providerGroup) {
2018
2204
  function registerProviderCommands(program) {
2019
2205
  const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
2020
2206
  provider.command("list").description("List configured providers.").option("--json", "Emit providers as JSON.").action(async (options) => {
2021
- const config = loadConfig();
2207
+ const config = loadProjectConfig();
2022
2208
  const providers = config.project.providers;
2023
2209
  if (options.json) {
2024
2210
  console.log(JSON.stringify(providers, null, 2));
@@ -2058,7 +2244,7 @@ function registerProviderCommands(program) {
2058
2244
  registerProviderInspect(provider);
2059
2245
  }
2060
2246
  async function addProvider(options) {
2061
- const configPath = findConfigFile();
2247
+ const configPath = activeConfigPath();
2062
2248
  if (!configPath) {
2063
2249
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
2064
2250
  }
@@ -2073,11 +2259,11 @@ async function addProvider(options) {
2073
2259
  throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
2074
2260
  }
2075
2261
  const newProvider = candidate.data;
2076
- const existing = loadConfig();
2262
+ const existing = loadProjectConfig();
2077
2263
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
2078
2264
  throw new OmniNodeError(
2079
2265
  "CLI_USAGE",
2080
- `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
2266
+ `Provider "${newProvider.name}" already exists in ${activeConfigPath() ?? "omninode.yaml"}.`
2081
2267
  );
2082
2268
  }
2083
2269
  const writtenPath = appendToConfigList("providers", newProvider);
@@ -2091,11 +2277,11 @@ async function addProvider(options) {
2091
2277
  }
2092
2278
  }
2093
2279
  function removeProvider(name) {
2094
- const configPath = findConfigFile();
2280
+ const configPath = activeConfigPath();
2095
2281
  if (!configPath) {
2096
2282
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
2097
2283
  }
2098
- const config = loadConfig();
2284
+ const config = loadProjectConfig();
2099
2285
  if (!config.project.providers.some((p) => p.name === name)) {
2100
2286
  throw new ProviderError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
2101
2287
  }
@@ -2111,7 +2297,7 @@ function removeProvider(name) {
2111
2297
  console.log(`Removed provider "${name}" from ${configPath}.`);
2112
2298
  }
2113
2299
  async function testProvider(name, options = {}) {
2114
- const config = loadConfig();
2300
+ const config = loadProjectConfig();
2115
2301
  const providerConfig = config.project.providers.find((p) => p.name === name);
2116
2302
  if (!providerConfig) {
2117
2303
  const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
@@ -2314,6 +2500,7 @@ var TaskEngine = class {
2314
2500
  const result = {
2315
2501
  ...output.summary !== void 0 ? { summary: output.summary } : {},
2316
2502
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2503
+ ...output.protocol !== void 0 ? { protocol: output.protocol } : {},
2317
2504
  // Keep the verbatim output (bounded) for report extraction and audit.
2318
2505
  ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2319
2506
  ...output.error !== void 0 ? { error: output.error } : {},
@@ -2493,7 +2680,7 @@ var collectArgs = (value, previous) => [...previous, value];
2493
2680
  function registerAgentCommands(program) {
2494
2681
  const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2495
2682
  agent.command("list").description("List configured agents.").option("--json", "Emit agents as JSON.").action(async (options) => {
2496
- const config = loadConfig();
2683
+ const config = loadProjectConfig();
2497
2684
  const agents = config.project.agents;
2498
2685
  if (options.json) {
2499
2686
  console.log(JSON.stringify(config.project.agents, null, 2));
@@ -2522,7 +2709,7 @@ function registerAgentCommands(program) {
2522
2709
  (value) => Number(value)
2523
2710
  ).action(async (options) => addAgent(options));
2524
2711
  agent.command("run <name> <objective>").description("Run a single task through this agent immediately.").action(async (name, objective) => {
2525
- const config = loadConfig();
2712
+ const config = loadProjectConfig();
2526
2713
  const agentConfig = config.project.agents.find((a) => a.name === name);
2527
2714
  if (!agentConfig) {
2528
2715
  throw new AgentError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
@@ -2559,7 +2746,7 @@ async function addAgent(options) {
2559
2746
  throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
2560
2747
  }
2561
2748
  const newAgent = candidate.data;
2562
- const existing = loadConfig();
2749
+ const existing = loadProjectConfig();
2563
2750
  if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2564
2751
  throw new OmniNodeError(
2565
2752
  "CLI_USAGE",
@@ -2571,7 +2758,7 @@ async function addAgent(options) {
2571
2758
  console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2572
2759
  }
2573
2760
  async function testAgent(name) {
2574
- const config = loadConfig();
2761
+ const config = loadProjectConfig();
2575
2762
  const agentConfig = config.project.agents.find((a) => a.name === name);
2576
2763
  if (!agentConfig) {
2577
2764
  const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
@@ -2602,7 +2789,7 @@ async function testAgent(name) {
2602
2789
  function registerRoleCommands(program) {
2603
2790
  const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2604
2791
  role.command("list").description("List configured roles.").option("--json", "Emit roles as JSON.").action(async (options) => {
2605
- const config = loadConfig();
2792
+ const config = loadProjectConfig();
2606
2793
  const roles = config.project.roles;
2607
2794
  if (options.json) {
2608
2795
  console.log(JSON.stringify(config.project.roles, null, 2));
@@ -2626,7 +2813,7 @@ var collectFiles = (value, previous) => [...previous, value];
2626
2813
  function registerTaskCommands(program) {
2627
2814
  const task = program.command("task").description("Create, run and inspect tasks (\xA713).");
2628
2815
  task.command("create <objective>").description("Create a task. Assign a role and/or agent; run it with `omninode task run`.").option("-r, --role <roleId>", "Role id from project.roles (validated at creation).").option("-a, --agent <name>", "Agent name from project.agents (validated at creation).").option("-f, --file <path>", "File or glob relevant to the task (repeatable).", collectFiles).action(async (objective, options) => {
2629
- const config = loadConfig();
2816
+ const config = loadProjectConfig();
2630
2817
  const engine = createTaskEngine(config);
2631
2818
  const created = await engine.create({
2632
2819
  objective,
@@ -2643,7 +2830,7 @@ function registerTaskCommands(program) {
2643
2830
  console.log(`Run \`omninode task run ${created.id}\` to execute it.`);
2644
2831
  });
2645
2832
  task.command("run <task-id>").description("Run a task through its assigned agent.").action(async (taskId) => {
2646
- const config = loadConfig();
2833
+ const config = loadProjectConfig();
2647
2834
  const engine = createTaskEngine(config);
2648
2835
  console.log(`Running task ${taskId}...`);
2649
2836
  const finished = await engine.run(taskId);
@@ -2651,7 +2838,7 @@ function registerTaskCommands(program) {
2651
2838
  process.exitCode = exitCodeForStatus(finished.status);
2652
2839
  });
2653
2840
  task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
2654
- const config = loadConfig();
2841
+ const config = loadProjectConfig();
2655
2842
  const engine = createTaskEngine(config);
2656
2843
  const found = await engine.get(taskId);
2657
2844
  if (!found) {
@@ -2663,7 +2850,7 @@ function registerTaskCommands(program) {
2663
2850
  printOutcome(found, { verbose: true });
2664
2851
  });
2665
2852
  task.command("list").description("List tasks in the project's task store.").option("-s, --status <status>", "Filter by status (created, queued, running, completed, failed, cancelled).").option("--json", "Emit tasks as JSON.").action(async (options) => {
2666
- const config = loadConfig();
2853
+ const config = loadProjectConfig();
2667
2854
  const engine = createTaskEngine(config);
2668
2855
  const tasks = await engine.list(
2669
2856
  options.status !== void 0 ? { status: options.status } : {}
@@ -2681,13 +2868,13 @@ function registerTaskCommands(program) {
2681
2868
  }
2682
2869
  });
2683
2870
  task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
2684
- const config = loadConfig();
2871
+ const config = loadProjectConfig();
2685
2872
  const engine = createTaskEngine(config);
2686
2873
  const cancelled = await engine.cancel(taskId);
2687
2874
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
2688
2875
  });
2689
2876
  task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
2690
- const config = loadConfig();
2877
+ const config = loadProjectConfig();
2691
2878
  const engine = createTaskEngine(config);
2692
2879
  const reset = await engine.retry(taskId);
2693
2880
  console.log(`Task ${reset.id} reset to "${reset.status}".`);
@@ -4228,7 +4415,7 @@ function buildPipelineEngine(config, log = logger) {
4228
4415
  function registerPipelineCommands(program) {
4229
4416
  const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
4230
4417
  pipeline.command("list").description("List configured pipelines.").option("--json", "Emit pipelines as JSON.").action(async (options) => {
4231
- const config = loadConfig();
4418
+ const config = loadProjectConfig();
4232
4419
  const pipelines = config.project.pipelines;
4233
4420
  if (options.json) {
4234
4421
  console.log(JSON.stringify(config.project.pipelines, null, 2));
@@ -4249,7 +4436,7 @@ function registerPipelineCommands(program) {
4249
4436
  pipeline.command("retry <run-id> [objective]").description(
4250
4437
  "Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
4251
4438
  ).action(async (runId, objective) => {
4252
- const config = loadConfig();
4439
+ const config = loadProjectConfig();
4253
4440
  const engine = buildPipelineEngine(config);
4254
4441
  const previous = await engine.get(runId);
4255
4442
  if (!previous) {
@@ -4268,7 +4455,7 @@ function registerPipelineCommands(program) {
4268
4455
  await runPipelineAction(def.id, objective ?? previous.objective);
4269
4456
  });
4270
4457
  pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
4271
- const config = loadConfig();
4458
+ const config = loadProjectConfig();
4272
4459
  const engine = buildPipelineEngine(config);
4273
4460
  await engine.cancel(runId);
4274
4461
  console.log(`Cancellation requested for run ${runId}.`);
@@ -4278,7 +4465,7 @@ function registerPipelineCommands(program) {
4278
4465
  await createPipelineFromFile(id, options.from);
4279
4466
  });
4280
4467
  pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
4281
- const config = loadConfig();
4468
+ const config = loadProjectConfig();
4282
4469
  const def = findPipeline(config, id);
4283
4470
  const engine = buildPipelineEngine(config);
4284
4471
  engine.validate(def);
@@ -4289,7 +4476,7 @@ function registerPipelineCommands(program) {
4289
4476
  }
4290
4477
  });
4291
4478
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
4292
- const config = loadConfig();
4479
+ const config = loadProjectConfig();
4293
4480
  const engine = buildPipelineEngine(config);
4294
4481
  const runs = await engine.listRuns({ pipelineId: id });
4295
4482
  if (runs.length === 0) {
@@ -4302,7 +4489,7 @@ function registerPipelineCommands(program) {
4302
4489
  }
4303
4490
  });
4304
4491
  pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
4305
- const config = loadConfig();
4492
+ const config = loadProjectConfig();
4306
4493
  const engine = buildPipelineEngine(config);
4307
4494
  const run2 = await engine.get(runId);
4308
4495
  if (!run2) {
@@ -4334,7 +4521,7 @@ function registerPipelineCommands(program) {
4334
4521
  registerPipelineInspect(pipeline);
4335
4522
  }
4336
4523
  async function runPipelineAction(id, objective) {
4337
- const config = loadConfig();
4524
+ const config = loadProjectConfig();
4338
4525
  const def = findPipeline(config, id);
4339
4526
  const engine = buildPipelineEngine(config);
4340
4527
  console.log(
@@ -4366,7 +4553,7 @@ function registerRunAlias(program) {
4366
4553
  program.command("run <id> [objective]").description("Alias of `omninode pipeline run` \u2014 execute a configured pipeline end to end.").action(async (id, objective) => runPipelineAction(id, objective));
4367
4554
  }
4368
4555
  async function createPipelineFromFile(id, file) {
4369
- const configPath = findConfigFile();
4556
+ const configPath = activeConfigPath();
4370
4557
  if (!configPath) {
4371
4558
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
4372
4559
  }
@@ -4383,7 +4570,7 @@ async function createPipelineFromFile(id, file) {
4383
4570
  const issues = candidate.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
4384
4571
  throw new PipelineError("PIPELINE_INVALID", `Invalid pipeline definition: ${issues}`);
4385
4572
  }
4386
- const engine = buildPipelineEngine(loadConfig());
4573
+ const engine = buildPipelineEngine(loadProjectConfig());
4387
4574
  engine.validate({ id, ...candidate.data });
4388
4575
  const doc = parseDocument3(readFileSync4(configPath, "utf8"));
4389
4576
  if (doc.getIn(["project", "pipelines"]) === void 0) {
@@ -4418,7 +4605,7 @@ function findPipeline(config, id) {
4418
4605
  function registerMemoryCommands(program) {
4419
4606
  const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
4420
4607
  memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
4421
- const config = loadConfig();
4608
+ const config = loadProjectConfig();
4422
4609
  const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
4423
4610
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4424
4611
  const entries = await svc.providerRef.query({ limit: 1e4 });
@@ -4433,7 +4620,7 @@ function registerMemoryCommands(program) {
4433
4620
  }
4434
4621
  });
4435
4622
  memory.command("query <text>").description("Search memory for entries relevant to the text.").option("-s, --scope <scope>", "Restrict to a scope: project, task, role or knowledge.").option("-n, --limit <count>", "Maximum entries to return.", (value) => Number(value), 10).option("--json", "Emit entries as JSON.").action(async (text, options) => {
4436
- const config = loadConfig();
4623
+ const config = loadProjectConfig();
4437
4624
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4438
4625
  const entries = await svc.providerRef.query({
4439
4626
  text,
@@ -4458,7 +4645,7 @@ function registerMemoryCommands(program) {
4458
4645
  "Comma-separated tags.",
4459
4646
  (value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
4460
4647
  ).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
4461
- const config = loadConfig();
4648
+ const config = loadProjectConfig();
4462
4649
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4463
4650
  const key = options.key ?? `note:${Date.now().toString(36)}`;
4464
4651
  await svc.providerRef.write({
@@ -4541,7 +4728,7 @@ function countBy(items, key) {
4541
4728
  }
4542
4729
  function registerStatusCommand(program) {
4543
4730
  program.command("status").description("Show the project's OmniNode state overview (config, tasks, runs, reports, plans, memory).").option("--json", "Emit machine-readable JSON (for scripts and CI).").action(async (options) => {
4544
- const config = loadConfig();
4731
+ const config = loadProjectConfig();
4545
4732
  const tasks = await new FileTaskStore().list();
4546
4733
  const runs = await new FilePipelineRunStore().list();
4547
4734
  const reportStore = new FileReportStore();
@@ -4622,8 +4809,8 @@ var STORE_FILES = [
4622
4809
  async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4623
4810
  const reports = [];
4624
4811
  for (const file of STORE_FILES) {
4625
- const path = `${directory}/${file}`;
4626
- const state = await inspectDocument(path);
4812
+ const path2 = `${directory}/${file}`;
4813
+ const state = await inspectDocument(path2);
4627
4814
  if (state.status === "missing") {
4628
4815
  reports.push({ file, state: state.status, migrated: false });
4629
4816
  continue;
@@ -4645,9 +4832,9 @@ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, op
4645
4832
  reports.push({ file, state: state.status, migrated: false, note: state.detail });
4646
4833
  continue;
4647
4834
  }
4648
- const items = await readDocument(path, void 0, false);
4835
+ const items = await readDocument(path2, void 0, false);
4649
4836
  if (!options.checkOnly) {
4650
- await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4837
+ await writeFileAtomic(path2, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4651
4838
  `);
4652
4839
  }
4653
4840
  const beforeCount = items.length;
@@ -4690,7 +4877,7 @@ function registerSecurityCommand(program) {
4690
4877
  console.log(`Unknown security action "${action}". Available: audit.`);
4691
4878
  return;
4692
4879
  }
4693
- const config = loadConfig();
4880
+ const config = loadProjectConfig();
4694
4881
  const findings = [];
4695
4882
  for (const agent of config.project.agents) {
4696
4883
  const policy = resolveEnvPolicy(agent);
@@ -4751,14 +4938,15 @@ Trust model: agents execute with the permissions of the OS user running OmniNode
4751
4938
 
4752
4939
  // src/cli/commands/config.ts
4753
4940
  import { readFileSync as readFileSync5 } from "fs";
4941
+ import { parse as parseYaml2 } from "yaml";
4754
4942
  function registerConfigCommands(program) {
4755
4943
  const config = program.command("config").description("Inspect the project configuration (omninode.yaml).");
4756
4944
  config.command("path").description("Print the path of the configuration file in use.").action(() => {
4757
- const path = findConfigFile();
4758
- console.log(path ?? "no omninode.yaml found");
4945
+ const path2 = activeConfigPath();
4946
+ console.log(path2 ?? "no omninode.yaml found");
4759
4947
  });
4760
4948
  config.command("show").description("Print the effective configuration (credential references only, never values).").option("--json", "Emit the configuration as JSON.").action((options) => {
4761
- const resolved = loadConfig();
4949
+ const resolved = loadConfigDetailed().config;
4762
4950
  const payload = {
4763
4951
  project: resolved.project.name,
4764
4952
  providers: resolved.project.providers.map((p) => ({
@@ -4784,18 +4972,41 @@ function registerConfigCommands(program) {
4784
4972
  console.log(`pipelines: ${payload.pipelines.join(", ") || "none"}`);
4785
4973
  console.log(`memory: ${payload.memory ? `${payload.memory.provider}${payload.memory.required ? " (required)" : ""}` : "not configured"}`);
4786
4974
  });
4975
+ config.command("profiles").description("List the configuration profiles defined in this project.").action(() => {
4976
+ const path2 = activeConfigPath();
4977
+ const raw = path2 ? parseYaml2(readFileSync5(path2, "utf8")) : {};
4978
+ const names = Object.keys(raw.profiles ?? {});
4979
+ const active = loadConfigDetailed().sources.profile;
4980
+ console.log(`active: ${active}`);
4981
+ console.log(`defined: ${names.length > 0 ? names.join(", ") : "none (add a `profiles:` section)"}`);
4982
+ });
4787
4983
  config.command("validate").description("Validate the configuration file, including the inline-secret scan.").action(() => {
4788
- const path = findConfigFile();
4789
- if (!path) throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
4790
- const findings = scanForInlineSecrets(readFileSync5(path, "utf8"));
4984
+ const path2 = activeConfigPath();
4985
+ if (!path2) throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
4986
+ const findings = scanForInlineSecrets(readFileSync5(path2, "utf8"));
4791
4987
  if (findings.length > 0) {
4792
4988
  throw new OmniNodeError(
4793
4989
  "CONFIG_INVALID",
4794
- `${path} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
4990
+ `${path2} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
4795
4991
  );
4796
4992
  }
4797
- const resolved = loadConfig();
4798
- console.log(`${path} is valid \u2014 project "${resolved.project.name}" with ${resolved.project.providers.length} provider(s), ${resolved.project.agents.length} agent(s), ${resolved.project.pipelines.length} pipeline(s).`);
4993
+ const { config: resolved, sources } = loadConfigDetailed();
4994
+ console.log(
4995
+ `${path2} is valid \u2014 project "${resolved.project.name}" with ${resolved.project.providers.length} provider(s), ${resolved.project.agents.length} agent(s), ${resolved.project.pipelines.length} pipeline(s).`
4996
+ );
4997
+ console.log(` profile: ${sources.profile}`);
4998
+ if (sources.userFile) console.log(` user cfg: ${sources.userFile}`);
4999
+ if (sources.envOverrides.length > 0) {
5000
+ console.log(` env overrides: ${sources.envOverrides.join(", ")}`);
5001
+ }
5002
+ if (sources.defaults.length > 0) {
5003
+ console.log(` defaults applied: ${sources.defaults.join(", ")}`);
5004
+ }
5005
+ const diagnostics = configDiagnostics(resolved);
5006
+ if (diagnostics.length > 0) {
5007
+ console.log(" diagnostics:");
5008
+ for (const note of diagnostics) console.log(` - ${note}`);
5009
+ }
4799
5010
  });
4800
5011
  }
4801
5012
 
@@ -4821,7 +5032,7 @@ function registerModelGroup(program, name) {
4821
5032
  `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
4822
5033
  );
4823
5034
  }
4824
- const config = loadConfig();
5035
+ const config = loadProjectConfig();
4825
5036
  const selected = config.project.providers.filter(
4826
5037
  (p) => providerName ? p.name === providerName : true
4827
5038
  );
@@ -4870,7 +5081,10 @@ function createProgram() {
4870
5081
  const program = new Command();
4871
5082
  program.name("omninode").description(
4872
5083
  "Provider-agnostic multi-AI orchestration platform: coordinate CLI agents, AI providers, memory, roles, reports and planning in one execution system."
4873
- ).version(OMNINODE_VERSION, "-v, --version", "Print the OmniNode version.");
5084
+ ).version(OMNINODE_VERSION, "-v, --version", "Print the OmniNode version.").option("-c, --config <file>", "Use a specific configuration file instead of ./omninode.yaml.").option("-p, --profile <name>", "Apply a named configuration profile (development/production/testing).").hook("preAction", (thisCommand) => {
5085
+ cliConfigOptions.path = thisCommand.opts().config;
5086
+ cliConfigOptions.profile = thisCommand.opts().profile;
5087
+ });
4874
5088
  registerInitCommand(program);
4875
5089
  registerProjectCommands(program);
4876
5090
  registerProviderCommands(program);
@@ -4892,10 +5106,8 @@ function createProgram() {
4892
5106
  }
4893
5107
  function applyLoggingConfig() {
4894
5108
  try {
4895
- if (findConfigFile()) {
4896
- const logging = loadConfig().logging;
4897
- if (logging) configureLogger(logging);
4898
- }
5109
+ const resolved = loadProjectConfig();
5110
+ if (resolved.logging) configureLogger(resolved.logging);
4899
5111
  } catch {
4900
5112
  }
4901
5113
  }