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

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.16";
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,108 @@ 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
+ return {
350
+ config: toAppConfig(parsed.data),
351
+ sources: {
352
+ ...projectFile !== void 0 ? { projectFile } : {},
353
+ ...userFile !== void 0 ? { userFile } : {},
354
+ profile,
355
+ envOverrides: withEnv.applied,
356
+ defaults: withDefaults.applied
357
+ }
358
+ };
359
+ }
360
+ function readConfigDocument(path2, optionsEnv) {
361
+ const raw = readFileSync(path2, "utf8");
226
362
  const secretFindings = scanForInlineSecrets(raw);
227
363
  if (secretFindings.length > 0) {
228
- throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
364
+ throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path2, secretFindings));
229
365
  }
230
- const expanded = expandEnvRefs(raw, env, path);
231
- let data;
366
+ const expanded = expandEnvRefs(raw, optionsEnv, path2);
232
367
  try {
233
- data = parseYaml(expanded);
368
+ return parseYaml(expanded);
234
369
  } catch (error) {
235
- throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path}.`, { cause: error });
370
+ throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path2}.`, { cause: error });
236
371
  }
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
- });
372
+ }
373
+ function configDiagnostics(config) {
374
+ const notes = [];
375
+ for (const provider of config.project.providers) {
376
+ if (!provider.apiKeyEnvVar && provider.baseUrl.startsWith("https://")) {
377
+ notes.push(
378
+ `Provider "${provider.name}": API key reference is missing \u2014 add api_key_env_var: <ENV_VAR> unless this endpoint is intentionally keyless.`
379
+ );
380
+ }
381
+ }
382
+ for (const agent of config.project.agents) {
383
+ if (!agent.command && agent.integration !== "custom") {
384
+ notes.push(`Agent "${agent.name}": no command configured \u2014 the process adapter needs one.`);
385
+ }
386
+ if (agent.allowExternalCwd) {
387
+ notes.push(`Agent "${agent.name}": may run outside the project root (allow_external_cwd).`);
388
+ }
246
389
  }
247
- return toAppConfig(parsed.data);
390
+ for (const pipeline of config.project.pipelines) {
391
+ const ids = new Set(pipeline.steps.map((s) => s.id));
392
+ for (const step of pipeline.steps) {
393
+ for (const dep of step.dependsOn ?? []) {
394
+ if (!ids.has(dep)) {
395
+ notes.push(`Pipeline "${pipeline.id}": step "${step.id}" depends on unknown step "${dep}".`);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ return notes;
248
401
  }
249
402
  function toAppConfig(yaml) {
250
403
  return {
@@ -372,6 +525,18 @@ function defaultProjectConfigYaml(projectName = "My Project") {
372
525
  `;
373
526
  }
374
527
 
528
+ // src/cli/options.ts
529
+ var cliConfigOptions = {};
530
+ function loadProjectConfig() {
531
+ return loadConfigDetailed({
532
+ ...cliConfigOptions.path !== void 0 ? { path: cliConfigOptions.path } : {},
533
+ ...cliConfigOptions.profile !== void 0 ? { profile: cliConfigOptions.profile } : {}
534
+ }).config;
535
+ }
536
+ function activeConfigPath() {
537
+ return cliConfigOptions.path ?? findConfigFile();
538
+ }
539
+
375
540
  // src/cli/commands/init.ts
376
541
  import { existsSync as existsSync2, writeFileSync } from "fs";
377
542
  async function runInit(options) {
@@ -459,7 +624,7 @@ function truncate(text, max) {
459
624
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
460
625
  }
461
626
  function appendToConfigList(listKey, item) {
462
- const configPath = findConfigFile();
627
+ const configPath = activeConfigPath();
463
628
  if (!configPath) {
464
629
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
465
630
  }
@@ -1365,15 +1530,15 @@ async function writeFileAtomic(target, contents, durability = "rename") {
1365
1530
  }
1366
1531
  }
1367
1532
  }
1368
- async function inspectDocument(path) {
1533
+ async function inspectDocument(path2) {
1369
1534
  let raw;
1370
1535
  try {
1371
- await stat(path);
1536
+ await stat(path2);
1372
1537
  } catch {
1373
1538
  return { status: "missing" };
1374
1539
  }
1375
1540
  try {
1376
- raw = await readFile(path, "utf8");
1541
+ raw = await readFile(path2, "utf8");
1377
1542
  } catch (error) {
1378
1543
  return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
1379
1544
  }
@@ -1393,21 +1558,21 @@ async function inspectDocument(path) {
1393
1558
  if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
1394
1559
  return { status: "current", schemaVersion: version, count: doc.items.length };
1395
1560
  }
1396
- async function readDocument(path, onCorrupt, quarantine = true) {
1397
- const state = await inspectDocument(path);
1561
+ async function readDocument(path2, onCorrupt, quarantine = true) {
1562
+ const state = await inspectDocument(path2);
1398
1563
  if (state.status === "missing" || state.status === "empty") return [];
1399
1564
  if (state.status === "future") {
1400
1565
  const error = new Error(
1401
- `${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
1566
+ `${path2} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
1402
1567
  );
1403
- onCorrupt?.(path, error);
1568
+ onCorrupt?.(path2, error);
1404
1569
  return [];
1405
1570
  }
1406
1571
  let raw;
1407
1572
  try {
1408
- raw = await readFile(path, "utf8");
1573
+ raw = await readFile(path2, "utf8");
1409
1574
  } catch (error) {
1410
- onCorrupt?.(path, error);
1575
+ onCorrupt?.(path2, error);
1411
1576
  return [];
1412
1577
  }
1413
1578
  try {
@@ -1416,16 +1581,16 @@ async function readDocument(path, onCorrupt, quarantine = true) {
1416
1581
  return parsed.items ?? [];
1417
1582
  } catch (error) {
1418
1583
  if (quarantine) {
1419
- const quarantinePath = `${path}.corrupt-${Date.now()}`;
1584
+ const quarantinePath = `${path2}.corrupt-${Date.now()}`;
1420
1585
  try {
1421
- await rename(path, quarantinePath);
1586
+ await rename(path2, quarantinePath);
1422
1587
  logger.child({ component: "persistence" }).warn(
1423
- `Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
1588
+ `Quarantined unreadable store file ${path2} \u2192 ${quarantinePath}`
1424
1589
  );
1425
1590
  } catch {
1426
1591
  }
1427
1592
  }
1428
- onCorrupt?.(path, error);
1593
+ onCorrupt?.(path2, error);
1429
1594
  return [];
1430
1595
  }
1431
1596
  }
@@ -1955,7 +2120,7 @@ function registerPipelineInspect(pipelineGroup) {
1955
2120
  function registerAgentInspect(agentGroup) {
1956
2121
  jsonOption(
1957
2122
  agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
1958
- const config = loadConfig();
2123
+ const config = loadProjectConfig();
1959
2124
  const agentConfig = config.project.agents.find((a) => a.name === name);
1960
2125
  if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
1961
2126
  const registry = buildAgentRegistry(config);
@@ -1985,7 +2150,7 @@ function registerAgentInspect(agentGroup) {
1985
2150
  function registerProviderInspect(providerGroup) {
1986
2151
  jsonOption(
1987
2152
  providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
1988
- const config = loadConfig();
2153
+ const config = loadProjectConfig();
1989
2154
  const providerConfig = config.project.providers.find((p) => p.name === name);
1990
2155
  if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
1991
2156
  const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
@@ -2018,7 +2183,7 @@ function registerProviderInspect(providerGroup) {
2018
2183
  function registerProviderCommands(program) {
2019
2184
  const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
2020
2185
  provider.command("list").description("List configured providers.").option("--json", "Emit providers as JSON.").action(async (options) => {
2021
- const config = loadConfig();
2186
+ const config = loadProjectConfig();
2022
2187
  const providers = config.project.providers;
2023
2188
  if (options.json) {
2024
2189
  console.log(JSON.stringify(providers, null, 2));
@@ -2058,7 +2223,7 @@ function registerProviderCommands(program) {
2058
2223
  registerProviderInspect(provider);
2059
2224
  }
2060
2225
  async function addProvider(options) {
2061
- const configPath = findConfigFile();
2226
+ const configPath = activeConfigPath();
2062
2227
  if (!configPath) {
2063
2228
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
2064
2229
  }
@@ -2073,11 +2238,11 @@ async function addProvider(options) {
2073
2238
  throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
2074
2239
  }
2075
2240
  const newProvider = candidate.data;
2076
- const existing = loadConfig();
2241
+ const existing = loadProjectConfig();
2077
2242
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
2078
2243
  throw new OmniNodeError(
2079
2244
  "CLI_USAGE",
2080
- `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
2245
+ `Provider "${newProvider.name}" already exists in ${activeConfigPath() ?? "omninode.yaml"}.`
2081
2246
  );
2082
2247
  }
2083
2248
  const writtenPath = appendToConfigList("providers", newProvider);
@@ -2091,11 +2256,11 @@ async function addProvider(options) {
2091
2256
  }
2092
2257
  }
2093
2258
  function removeProvider(name) {
2094
- const configPath = findConfigFile();
2259
+ const configPath = activeConfigPath();
2095
2260
  if (!configPath) {
2096
2261
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
2097
2262
  }
2098
- const config = loadConfig();
2263
+ const config = loadProjectConfig();
2099
2264
  if (!config.project.providers.some((p) => p.name === name)) {
2100
2265
  throw new ProviderError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
2101
2266
  }
@@ -2111,7 +2276,7 @@ function removeProvider(name) {
2111
2276
  console.log(`Removed provider "${name}" from ${configPath}.`);
2112
2277
  }
2113
2278
  async function testProvider(name, options = {}) {
2114
- const config = loadConfig();
2279
+ const config = loadProjectConfig();
2115
2280
  const providerConfig = config.project.providers.find((p) => p.name === name);
2116
2281
  if (!providerConfig) {
2117
2282
  const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
@@ -2493,7 +2658,7 @@ var collectArgs = (value, previous) => [...previous, value];
2493
2658
  function registerAgentCommands(program) {
2494
2659
  const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2495
2660
  agent.command("list").description("List configured agents.").option("--json", "Emit agents as JSON.").action(async (options) => {
2496
- const config = loadConfig();
2661
+ const config = loadProjectConfig();
2497
2662
  const agents = config.project.agents;
2498
2663
  if (options.json) {
2499
2664
  console.log(JSON.stringify(config.project.agents, null, 2));
@@ -2522,7 +2687,7 @@ function registerAgentCommands(program) {
2522
2687
  (value) => Number(value)
2523
2688
  ).action(async (options) => addAgent(options));
2524
2689
  agent.command("run <name> <objective>").description("Run a single task through this agent immediately.").action(async (name, objective) => {
2525
- const config = loadConfig();
2690
+ const config = loadProjectConfig();
2526
2691
  const agentConfig = config.project.agents.find((a) => a.name === name);
2527
2692
  if (!agentConfig) {
2528
2693
  throw new AgentError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
@@ -2559,7 +2724,7 @@ async function addAgent(options) {
2559
2724
  throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
2560
2725
  }
2561
2726
  const newAgent = candidate.data;
2562
- const existing = loadConfig();
2727
+ const existing = loadProjectConfig();
2563
2728
  if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2564
2729
  throw new OmniNodeError(
2565
2730
  "CLI_USAGE",
@@ -2571,7 +2736,7 @@ async function addAgent(options) {
2571
2736
  console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2572
2737
  }
2573
2738
  async function testAgent(name) {
2574
- const config = loadConfig();
2739
+ const config = loadProjectConfig();
2575
2740
  const agentConfig = config.project.agents.find((a) => a.name === name);
2576
2741
  if (!agentConfig) {
2577
2742
  const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
@@ -2602,7 +2767,7 @@ async function testAgent(name) {
2602
2767
  function registerRoleCommands(program) {
2603
2768
  const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2604
2769
  role.command("list").description("List configured roles.").option("--json", "Emit roles as JSON.").action(async (options) => {
2605
- const config = loadConfig();
2770
+ const config = loadProjectConfig();
2606
2771
  const roles = config.project.roles;
2607
2772
  if (options.json) {
2608
2773
  console.log(JSON.stringify(config.project.roles, null, 2));
@@ -2626,7 +2791,7 @@ var collectFiles = (value, previous) => [...previous, value];
2626
2791
  function registerTaskCommands(program) {
2627
2792
  const task = program.command("task").description("Create, run and inspect tasks (\xA713).");
2628
2793
  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();
2794
+ const config = loadProjectConfig();
2630
2795
  const engine = createTaskEngine(config);
2631
2796
  const created = await engine.create({
2632
2797
  objective,
@@ -2643,7 +2808,7 @@ function registerTaskCommands(program) {
2643
2808
  console.log(`Run \`omninode task run ${created.id}\` to execute it.`);
2644
2809
  });
2645
2810
  task.command("run <task-id>").description("Run a task through its assigned agent.").action(async (taskId) => {
2646
- const config = loadConfig();
2811
+ const config = loadProjectConfig();
2647
2812
  const engine = createTaskEngine(config);
2648
2813
  console.log(`Running task ${taskId}...`);
2649
2814
  const finished = await engine.run(taskId);
@@ -2651,7 +2816,7 @@ function registerTaskCommands(program) {
2651
2816
  process.exitCode = exitCodeForStatus(finished.status);
2652
2817
  });
2653
2818
  task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
2654
- const config = loadConfig();
2819
+ const config = loadProjectConfig();
2655
2820
  const engine = createTaskEngine(config);
2656
2821
  const found = await engine.get(taskId);
2657
2822
  if (!found) {
@@ -2663,7 +2828,7 @@ function registerTaskCommands(program) {
2663
2828
  printOutcome(found, { verbose: true });
2664
2829
  });
2665
2830
  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();
2831
+ const config = loadProjectConfig();
2667
2832
  const engine = createTaskEngine(config);
2668
2833
  const tasks = await engine.list(
2669
2834
  options.status !== void 0 ? { status: options.status } : {}
@@ -2681,13 +2846,13 @@ function registerTaskCommands(program) {
2681
2846
  }
2682
2847
  });
2683
2848
  task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
2684
- const config = loadConfig();
2849
+ const config = loadProjectConfig();
2685
2850
  const engine = createTaskEngine(config);
2686
2851
  const cancelled = await engine.cancel(taskId);
2687
2852
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
2688
2853
  });
2689
2854
  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();
2855
+ const config = loadProjectConfig();
2691
2856
  const engine = createTaskEngine(config);
2692
2857
  const reset = await engine.retry(taskId);
2693
2858
  console.log(`Task ${reset.id} reset to "${reset.status}".`);
@@ -4228,7 +4393,7 @@ function buildPipelineEngine(config, log = logger) {
4228
4393
  function registerPipelineCommands(program) {
4229
4394
  const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
4230
4395
  pipeline.command("list").description("List configured pipelines.").option("--json", "Emit pipelines as JSON.").action(async (options) => {
4231
- const config = loadConfig();
4396
+ const config = loadProjectConfig();
4232
4397
  const pipelines = config.project.pipelines;
4233
4398
  if (options.json) {
4234
4399
  console.log(JSON.stringify(config.project.pipelines, null, 2));
@@ -4249,7 +4414,7 @@ function registerPipelineCommands(program) {
4249
4414
  pipeline.command("retry <run-id> [objective]").description(
4250
4415
  "Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
4251
4416
  ).action(async (runId, objective) => {
4252
- const config = loadConfig();
4417
+ const config = loadProjectConfig();
4253
4418
  const engine = buildPipelineEngine(config);
4254
4419
  const previous = await engine.get(runId);
4255
4420
  if (!previous) {
@@ -4268,7 +4433,7 @@ function registerPipelineCommands(program) {
4268
4433
  await runPipelineAction(def.id, objective ?? previous.objective);
4269
4434
  });
4270
4435
  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();
4436
+ const config = loadProjectConfig();
4272
4437
  const engine = buildPipelineEngine(config);
4273
4438
  await engine.cancel(runId);
4274
4439
  console.log(`Cancellation requested for run ${runId}.`);
@@ -4278,7 +4443,7 @@ function registerPipelineCommands(program) {
4278
4443
  await createPipelineFromFile(id, options.from);
4279
4444
  });
4280
4445
  pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
4281
- const config = loadConfig();
4446
+ const config = loadProjectConfig();
4282
4447
  const def = findPipeline(config, id);
4283
4448
  const engine = buildPipelineEngine(config);
4284
4449
  engine.validate(def);
@@ -4289,7 +4454,7 @@ function registerPipelineCommands(program) {
4289
4454
  }
4290
4455
  });
4291
4456
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
4292
- const config = loadConfig();
4457
+ const config = loadProjectConfig();
4293
4458
  const engine = buildPipelineEngine(config);
4294
4459
  const runs = await engine.listRuns({ pipelineId: id });
4295
4460
  if (runs.length === 0) {
@@ -4302,7 +4467,7 @@ function registerPipelineCommands(program) {
4302
4467
  }
4303
4468
  });
4304
4469
  pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
4305
- const config = loadConfig();
4470
+ const config = loadProjectConfig();
4306
4471
  const engine = buildPipelineEngine(config);
4307
4472
  const run2 = await engine.get(runId);
4308
4473
  if (!run2) {
@@ -4334,7 +4499,7 @@ function registerPipelineCommands(program) {
4334
4499
  registerPipelineInspect(pipeline);
4335
4500
  }
4336
4501
  async function runPipelineAction(id, objective) {
4337
- const config = loadConfig();
4502
+ const config = loadProjectConfig();
4338
4503
  const def = findPipeline(config, id);
4339
4504
  const engine = buildPipelineEngine(config);
4340
4505
  console.log(
@@ -4366,7 +4531,7 @@ function registerRunAlias(program) {
4366
4531
  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
4532
  }
4368
4533
  async function createPipelineFromFile(id, file) {
4369
- const configPath = findConfigFile();
4534
+ const configPath = activeConfigPath();
4370
4535
  if (!configPath) {
4371
4536
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
4372
4537
  }
@@ -4383,7 +4548,7 @@ async function createPipelineFromFile(id, file) {
4383
4548
  const issues = candidate.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
4384
4549
  throw new PipelineError("PIPELINE_INVALID", `Invalid pipeline definition: ${issues}`);
4385
4550
  }
4386
- const engine = buildPipelineEngine(loadConfig());
4551
+ const engine = buildPipelineEngine(loadProjectConfig());
4387
4552
  engine.validate({ id, ...candidate.data });
4388
4553
  const doc = parseDocument3(readFileSync4(configPath, "utf8"));
4389
4554
  if (doc.getIn(["project", "pipelines"]) === void 0) {
@@ -4418,7 +4583,7 @@ function findPipeline(config, id) {
4418
4583
  function registerMemoryCommands(program) {
4419
4584
  const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
4420
4585
  memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
4421
- const config = loadConfig();
4586
+ const config = loadProjectConfig();
4422
4587
  const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
4423
4588
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4424
4589
  const entries = await svc.providerRef.query({ limit: 1e4 });
@@ -4433,7 +4598,7 @@ function registerMemoryCommands(program) {
4433
4598
  }
4434
4599
  });
4435
4600
  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();
4601
+ const config = loadProjectConfig();
4437
4602
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4438
4603
  const entries = await svc.providerRef.query({
4439
4604
  text,
@@ -4458,7 +4623,7 @@ function registerMemoryCommands(program) {
4458
4623
  "Comma-separated tags.",
4459
4624
  (value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
4460
4625
  ).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
4461
- const config = loadConfig();
4626
+ const config = loadProjectConfig();
4462
4627
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4463
4628
  const key = options.key ?? `note:${Date.now().toString(36)}`;
4464
4629
  await svc.providerRef.write({
@@ -4541,7 +4706,7 @@ function countBy(items, key) {
4541
4706
  }
4542
4707
  function registerStatusCommand(program) {
4543
4708
  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();
4709
+ const config = loadProjectConfig();
4545
4710
  const tasks = await new FileTaskStore().list();
4546
4711
  const runs = await new FilePipelineRunStore().list();
4547
4712
  const reportStore = new FileReportStore();
@@ -4622,8 +4787,8 @@ var STORE_FILES = [
4622
4787
  async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4623
4788
  const reports = [];
4624
4789
  for (const file of STORE_FILES) {
4625
- const path = `${directory}/${file}`;
4626
- const state = await inspectDocument(path);
4790
+ const path2 = `${directory}/${file}`;
4791
+ const state = await inspectDocument(path2);
4627
4792
  if (state.status === "missing") {
4628
4793
  reports.push({ file, state: state.status, migrated: false });
4629
4794
  continue;
@@ -4645,9 +4810,9 @@ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, op
4645
4810
  reports.push({ file, state: state.status, migrated: false, note: state.detail });
4646
4811
  continue;
4647
4812
  }
4648
- const items = await readDocument(path, void 0, false);
4813
+ const items = await readDocument(path2, void 0, false);
4649
4814
  if (!options.checkOnly) {
4650
- await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4815
+ await writeFileAtomic(path2, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4651
4816
  `);
4652
4817
  }
4653
4818
  const beforeCount = items.length;
@@ -4690,7 +4855,7 @@ function registerSecurityCommand(program) {
4690
4855
  console.log(`Unknown security action "${action}". Available: audit.`);
4691
4856
  return;
4692
4857
  }
4693
- const config = loadConfig();
4858
+ const config = loadProjectConfig();
4694
4859
  const findings = [];
4695
4860
  for (const agent of config.project.agents) {
4696
4861
  const policy = resolveEnvPolicy(agent);
@@ -4751,14 +4916,15 @@ Trust model: agents execute with the permissions of the OS user running OmniNode
4751
4916
 
4752
4917
  // src/cli/commands/config.ts
4753
4918
  import { readFileSync as readFileSync5 } from "fs";
4919
+ import { parse as parseYaml2 } from "yaml";
4754
4920
  function registerConfigCommands(program) {
4755
4921
  const config = program.command("config").description("Inspect the project configuration (omninode.yaml).");
4756
4922
  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");
4923
+ const path2 = activeConfigPath();
4924
+ console.log(path2 ?? "no omninode.yaml found");
4759
4925
  });
4760
4926
  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();
4927
+ const resolved = loadConfigDetailed().config;
4762
4928
  const payload = {
4763
4929
  project: resolved.project.name,
4764
4930
  providers: resolved.project.providers.map((p) => ({
@@ -4784,18 +4950,41 @@ function registerConfigCommands(program) {
4784
4950
  console.log(`pipelines: ${payload.pipelines.join(", ") || "none"}`);
4785
4951
  console.log(`memory: ${payload.memory ? `${payload.memory.provider}${payload.memory.required ? " (required)" : ""}` : "not configured"}`);
4786
4952
  });
4953
+ config.command("profiles").description("List the configuration profiles defined in this project.").action(() => {
4954
+ const path2 = activeConfigPath();
4955
+ const raw = path2 ? parseYaml2(readFileSync5(path2, "utf8")) : {};
4956
+ const names = Object.keys(raw.profiles ?? {});
4957
+ const active = loadConfigDetailed().sources.profile;
4958
+ console.log(`active: ${active}`);
4959
+ console.log(`defined: ${names.length > 0 ? names.join(", ") : "none (add a `profiles:` section)"}`);
4960
+ });
4787
4961
  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"));
4962
+ const path2 = activeConfigPath();
4963
+ if (!path2) throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
4964
+ const findings = scanForInlineSecrets(readFileSync5(path2, "utf8"));
4791
4965
  if (findings.length > 0) {
4792
4966
  throw new OmniNodeError(
4793
4967
  "CONFIG_INVALID",
4794
- `${path} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
4968
+ `${path2} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
4795
4969
  );
4796
4970
  }
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).`);
4971
+ const { config: resolved, sources } = loadConfigDetailed();
4972
+ console.log(
4973
+ `${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).`
4974
+ );
4975
+ console.log(` profile: ${sources.profile}`);
4976
+ if (sources.userFile) console.log(` user cfg: ${sources.userFile}`);
4977
+ if (sources.envOverrides.length > 0) {
4978
+ console.log(` env overrides: ${sources.envOverrides.join(", ")}`);
4979
+ }
4980
+ if (sources.defaults.length > 0) {
4981
+ console.log(` defaults applied: ${sources.defaults.join(", ")}`);
4982
+ }
4983
+ const diagnostics = configDiagnostics(resolved);
4984
+ if (diagnostics.length > 0) {
4985
+ console.log(" diagnostics:");
4986
+ for (const note of diagnostics) console.log(` - ${note}`);
4987
+ }
4799
4988
  });
4800
4989
  }
4801
4990
 
@@ -4821,7 +5010,7 @@ function registerModelGroup(program, name) {
4821
5010
  `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
4822
5011
  );
4823
5012
  }
4824
- const config = loadConfig();
5013
+ const config = loadProjectConfig();
4825
5014
  const selected = config.project.providers.filter(
4826
5015
  (p) => providerName ? p.name === providerName : true
4827
5016
  );
@@ -4870,7 +5059,10 @@ function createProgram() {
4870
5059
  const program = new Command();
4871
5060
  program.name("omninode").description(
4872
5061
  "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.");
5062
+ ).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) => {
5063
+ cliConfigOptions.path = thisCommand.opts().config;
5064
+ cliConfigOptions.profile = thisCommand.opts().profile;
5065
+ });
4874
5066
  registerInitCommand(program);
4875
5067
  registerProjectCommands(program);
4876
5068
  registerProviderCommands(program);
@@ -4892,10 +5084,8 @@ function createProgram() {
4892
5084
  }
4893
5085
  function applyLoggingConfig() {
4894
5086
  try {
4895
- if (findConfigFile()) {
4896
- const logging = loadConfig().logging;
4897
- if (logging) configureLogger(logging);
4898
- }
5087
+ const resolved = loadProjectConfig();
5088
+ if (resolved.logging) configureLogger(resolved.logging);
4899
5089
  } catch {
4900
5090
  }
4901
5091
  }