@hilbras/omninode 2.0.0-alpha.14 → 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,49 @@ 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.14";
26
+ var OMNINODE_VERSION = "2.0.0-alpha.16";
27
+
28
+ // src/cli/exit-codes.ts
29
+ var EXIT = {
30
+ success: 0,
31
+ general: 1,
32
+ invalidInput: 2,
33
+ config: 3,
34
+ provider: 4,
35
+ agent: 5,
36
+ timeout: 6,
37
+ cancelled: 7,
38
+ notFound: 8,
39
+ notImplemented: 9
40
+ };
41
+ function exitCodeFor(error) {
42
+ if (!isOmniNodeError(error)) return EXIT.general;
43
+ const code = error.code;
44
+ if (code === "NOT_IMPLEMENTED") return EXIT.notImplemented;
45
+ if (code === "AGENT_TIMEOUT") return EXIT.timeout;
46
+ if (code.startsWith("CONFIG_")) return EXIT.config;
47
+ if (code.startsWith("PROVIDER_") || code === "MODEL_NOT_FOUND") return EXIT.provider;
48
+ if (code.startsWith("AGENT_")) return EXIT.agent;
49
+ if (code.endsWith("_NOT_FOUND")) return EXIT.notFound;
50
+ if (code.endsWith("_INVALID") || code === "CLI_USAGE" || code === "PROTOCOL_VIOLATION") {
51
+ return EXIT.invalidInput;
52
+ }
53
+ return EXIT.general;
54
+ }
55
+ function exitCodeForStatus(status) {
56
+ switch (status) {
57
+ case "completed":
58
+ return EXIT.success;
59
+ case "cancelled":
60
+ return EXIT.cancelled;
61
+ case "timed_out":
62
+ return EXIT.timeout;
63
+ case "failed":
64
+ return EXIT.general;
65
+ default:
66
+ return EXIT.success;
67
+ }
68
+ }
27
69
 
28
70
  // src/config/schema.ts
29
71
  import { z } from "zod";
@@ -105,6 +147,10 @@ var projectConfigSchema = z.object({
105
147
  }).strict().optional()
106
148
  }).strict();
107
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(),
108
154
  project: projectConfigSchema,
109
155
  logging: z.object({
110
156
  level: z.enum(["debug", "info", "warn", "error"]),
@@ -145,6 +191,85 @@ ${lines}
145
191
  OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
146
192
  }
147
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
+
148
273
  // src/config/loader.ts
149
274
  import { existsSync, readFileSync } from "fs";
150
275
  import { parse as parseYaml } from "yaml";
@@ -171,38 +296,108 @@ function expandEnvRefs(text, env, source = "config") {
171
296
  }
172
297
  );
173
298
  }
174
- function loadConfig(options = {}) {
299
+ function loadConfigDetailed(options = {}) {
175
300
  const env = options.env ?? process.env;
176
- const path = options.path ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
177
- 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) {
178
318
  throw new ConfigError(
179
319
  "CONFIG_NOT_FOUND",
180
320
  "No omninode.yaml found. Run `omninode init` to create one, or pass an explicit path."
181
321
  );
182
322
  }
183
- 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");
184
362
  const secretFindings = scanForInlineSecrets(raw);
185
363
  if (secretFindings.length > 0) {
186
- throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
364
+ throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path2, secretFindings));
187
365
  }
188
- const expanded = expandEnvRefs(raw, env, path);
189
- let data;
366
+ const expanded = expandEnvRefs(raw, optionsEnv, path2);
190
367
  try {
191
- data = parseYaml(expanded);
368
+ return parseYaml(expanded);
192
369
  } catch (error) {
193
- throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path}.`, { cause: error });
370
+ throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path2}.`, { cause: error });
194
371
  }
195
- const parsed = appConfigSchema.safeParse(data);
196
- if (!parsed.success) {
197
- const issues = parsed.error.issues.map((issue) => {
198
- const location = issue.path.length > 0 ? issue.path.join(".") : "(root)";
199
- return `${location}: ${issue.message}`;
200
- });
201
- throw new ConfigError("CONFIG_INVALID", `Invalid configuration in ${path}.`, {
202
- details: { issues }
203
- });
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
+ }
204
381
  }
205
- return toAppConfig(parsed.data);
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
+ }
389
+ }
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;
206
401
  }
207
402
  function toAppConfig(yaml) {
208
403
  return {
@@ -330,6 +525,18 @@ function defaultProjectConfigYaml(projectName = "My Project") {
330
525
  `;
331
526
  }
332
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
+
333
540
  // src/cli/commands/init.ts
334
541
  import { existsSync as existsSync2, writeFileSync } from "fs";
335
542
  async function runInit(options) {
@@ -357,6 +564,10 @@ function registerProjectCommands(program) {
357
564
  project.command("init").description("Alias of `omninode init`.").option("-f, --force", "Overwrite an existing omninode.yaml.").option("-n, --name <name>", "Project name to write into the configuration.", "My Project").action(async (options) => runInit(options));
358
565
  }
359
566
 
567
+ // src/cli/commands/providers.ts
568
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
569
+ import { parseDocument as parseDocument2 } from "yaml";
570
+
360
571
  // src/registry/index.ts
361
572
  function registryKey(provider, id) {
362
573
  return `${provider}:${id}`;
@@ -409,17 +620,11 @@ var ModelRegistry = class {
409
620
  // src/cli/commands/shared.ts
410
621
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
411
622
  import { parseDocument } from "yaml";
412
- function notImplemented(command, when) {
413
- return new OmniNodeError(
414
- "NOT_IMPLEMENTED",
415
- `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
416
- );
417
- }
418
623
  function truncate(text, max) {
419
624
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
420
625
  }
421
626
  function appendToConfigList(listKey, item) {
422
- const configPath = findConfigFile();
627
+ const configPath = activeConfigPath();
423
628
  if (!configPath) {
424
629
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
425
630
  }
@@ -1325,15 +1530,15 @@ async function writeFileAtomic(target, contents, durability = "rename") {
1325
1530
  }
1326
1531
  }
1327
1532
  }
1328
- async function inspectDocument(path) {
1533
+ async function inspectDocument(path2) {
1329
1534
  let raw;
1330
1535
  try {
1331
- await stat(path);
1536
+ await stat(path2);
1332
1537
  } catch {
1333
1538
  return { status: "missing" };
1334
1539
  }
1335
1540
  try {
1336
- raw = await readFile(path, "utf8");
1541
+ raw = await readFile(path2, "utf8");
1337
1542
  } catch (error) {
1338
1543
  return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
1339
1544
  }
@@ -1353,21 +1558,21 @@ async function inspectDocument(path) {
1353
1558
  if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
1354
1559
  return { status: "current", schemaVersion: version, count: doc.items.length };
1355
1560
  }
1356
- async function readDocument(path, onCorrupt, quarantine = true) {
1357
- const state = await inspectDocument(path);
1561
+ async function readDocument(path2, onCorrupt, quarantine = true) {
1562
+ const state = await inspectDocument(path2);
1358
1563
  if (state.status === "missing" || state.status === "empty") return [];
1359
1564
  if (state.status === "future") {
1360
1565
  const error = new Error(
1361
- `${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})`
1362
1567
  );
1363
- onCorrupt?.(path, error);
1568
+ onCorrupt?.(path2, error);
1364
1569
  return [];
1365
1570
  }
1366
1571
  let raw;
1367
1572
  try {
1368
- raw = await readFile(path, "utf8");
1573
+ raw = await readFile(path2, "utf8");
1369
1574
  } catch (error) {
1370
- onCorrupt?.(path, error);
1575
+ onCorrupt?.(path2, error);
1371
1576
  return [];
1372
1577
  }
1373
1578
  try {
@@ -1376,16 +1581,16 @@ async function readDocument(path, onCorrupt, quarantine = true) {
1376
1581
  return parsed.items ?? [];
1377
1582
  } catch (error) {
1378
1583
  if (quarantine) {
1379
- const quarantinePath = `${path}.corrupt-${Date.now()}`;
1584
+ const quarantinePath = `${path2}.corrupt-${Date.now()}`;
1380
1585
  try {
1381
- await rename(path, quarantinePath);
1586
+ await rename(path2, quarantinePath);
1382
1587
  logger.child({ component: "persistence" }).warn(
1383
- `Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
1588
+ `Quarantined unreadable store file ${path2} \u2192 ${quarantinePath}`
1384
1589
  );
1385
1590
  } catch {
1386
1591
  }
1387
1592
  }
1388
- onCorrupt?.(path, error);
1593
+ onCorrupt?.(path2, error);
1389
1594
  return [];
1390
1595
  }
1391
1596
  }
@@ -1915,7 +2120,7 @@ function registerPipelineInspect(pipelineGroup) {
1915
2120
  function registerAgentInspect(agentGroup) {
1916
2121
  jsonOption(
1917
2122
  agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
1918
- const config = loadConfig();
2123
+ const config = loadProjectConfig();
1919
2124
  const agentConfig = config.project.agents.find((a) => a.name === name);
1920
2125
  if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
1921
2126
  const registry = buildAgentRegistry(config);
@@ -1945,7 +2150,7 @@ function registerAgentInspect(agentGroup) {
1945
2150
  function registerProviderInspect(providerGroup) {
1946
2151
  jsonOption(
1947
2152
  providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
1948
- const config = loadConfig();
2153
+ const config = loadProjectConfig();
1949
2154
  const providerConfig = config.project.providers.find((p) => p.name === name);
1950
2155
  if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
1951
2156
  const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
@@ -1977,9 +2182,13 @@ function registerProviderInspect(providerGroup) {
1977
2182
  // src/cli/commands/providers.ts
1978
2183
  function registerProviderCommands(program) {
1979
2184
  const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
1980
- provider.command("list").description("List configured providers.").action(async () => {
1981
- const config = loadConfig();
2185
+ provider.command("list").description("List configured providers.").option("--json", "Emit providers as JSON.").action(async (options) => {
2186
+ const config = loadProjectConfig();
1982
2187
  const providers = config.project.providers;
2188
+ if (options.json) {
2189
+ console.log(JSON.stringify(providers, null, 2));
2190
+ return;
2191
+ }
1983
2192
  if (providers.length === 0) {
1984
2193
  console.log(
1985
2194
  "No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
@@ -2008,13 +2217,13 @@ function registerProviderCommands(program) {
2008
2217
  "--connect",
2009
2218
  "Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
2010
2219
  ).action(async (name, options) => testProvider(name, options));
2011
- provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
2012
- throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
2220
+ provider.command("remove <name>").description("Remove a provider from omninode.yaml.").action((name) => {
2221
+ removeProvider(name);
2013
2222
  });
2014
2223
  registerProviderInspect(provider);
2015
2224
  }
2016
2225
  async function addProvider(options) {
2017
- const configPath = findConfigFile();
2226
+ const configPath = activeConfigPath();
2018
2227
  if (!configPath) {
2019
2228
  throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
2020
2229
  }
@@ -2029,11 +2238,11 @@ async function addProvider(options) {
2029
2238
  throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
2030
2239
  }
2031
2240
  const newProvider = candidate.data;
2032
- const existing = loadConfig();
2241
+ const existing = loadProjectConfig();
2033
2242
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
2034
2243
  throw new OmniNodeError(
2035
2244
  "CLI_USAGE",
2036
- `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
2245
+ `Provider "${newProvider.name}" already exists in ${activeConfigPath() ?? "omninode.yaml"}.`
2037
2246
  );
2038
2247
  }
2039
2248
  const writtenPath = appendToConfigList("providers", newProvider);
@@ -2046,8 +2255,28 @@ async function addProvider(options) {
2046
2255
  console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
2047
2256
  }
2048
2257
  }
2258
+ function removeProvider(name) {
2259
+ const configPath = activeConfigPath();
2260
+ if (!configPath) {
2261
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
2262
+ }
2263
+ const config = loadProjectConfig();
2264
+ if (!config.project.providers.some((p) => p.name === name)) {
2265
+ throw new ProviderError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
2266
+ }
2267
+ const doc = parseDocument2(readFileSync3(configPath, "utf8"));
2268
+ const current = doc.getIn(["project", "providers"]);
2269
+ const remaining = (current === void 0 || current === null ? [] : doc.get("project")?.providers ?? []).filter((entry2) => entry2?.name !== name);
2270
+ doc.setIn(["project", "providers"], remaining);
2271
+ const validated = appConfigSchema.safeParse(doc.toJS());
2272
+ if (!validated.success) {
2273
+ throw new OmniNodeError("CONFIG_INVALID", "Removing this provider would produce an invalid configuration.");
2274
+ }
2275
+ writeFileSync3(configPath, doc.toString(), "utf8");
2276
+ console.log(`Removed provider "${name}" from ${configPath}.`);
2277
+ }
2049
2278
  async function testProvider(name, options = {}) {
2050
- const config = loadConfig();
2279
+ const config = loadProjectConfig();
2051
2280
  const providerConfig = config.project.providers.find((p) => p.name === name);
2052
2281
  if (!providerConfig) {
2053
2282
  const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
@@ -2081,169 +2310,6 @@ function printProviderStatus(status) {
2081
2310
  if (status.message) console.log(` detail: ${status.message}`);
2082
2311
  }
2083
2312
 
2084
- // src/cli/commands/models.ts
2085
- var CAPABILITIES = [
2086
- "chat",
2087
- "coding",
2088
- "tools",
2089
- "vision",
2090
- "embeddings"
2091
- ];
2092
- function registerModelsCommand(program) {
2093
- program.command("models").description("Discover and list models from configured providers (model registry, \xA78).").argument("[provider]", "Only discover from this provider.").option("-c, --capability <capability>", `Filter by capability: ${CAPABILITIES.join(", ")}.`).action(async (providerName, options) => {
2094
- const capability = options.capability;
2095
- if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
2096
- throw new OmniNodeError(
2097
- "CLI_USAGE",
2098
- `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
2099
- );
2100
- }
2101
- const config = loadConfig();
2102
- const selected = config.project.providers.filter(
2103
- (p) => providerName ? p.name === providerName : true
2104
- );
2105
- if (selected.length === 0) {
2106
- throw new OmniNodeError(
2107
- "PROVIDER_NOT_FOUND",
2108
- providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
2109
- );
2110
- }
2111
- const registry = new ModelRegistry();
2112
- for (const providerConfig of selected) {
2113
- try {
2114
- const provider = createProvider(providerConfig);
2115
- const discovered = await registry.discoverFrom(provider);
2116
- console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
2117
- } catch (error) {
2118
- const message = error instanceof Error ? error.message : String(error);
2119
- console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
2120
- }
2121
- }
2122
- const models = registry.list({
2123
- ...providerName !== void 0 ? { provider: providerName } : {},
2124
- ...capability !== void 0 ? { capability } : {}
2125
- });
2126
- if (models.length === 0) {
2127
- console.log("No models found.");
2128
- return;
2129
- }
2130
- console.log("");
2131
- for (const model of models) {
2132
- console.log(`${model.provider}:${model.id}`);
2133
- }
2134
- });
2135
- }
2136
-
2137
- // src/cli/commands/agents.ts
2138
- var collectArgs = (value, previous) => [...previous, value];
2139
- function registerAgentCommands(program) {
2140
- const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2141
- agent.command("list").description("List configured agents.").action(async () => {
2142
- const config = loadConfig();
2143
- const agents = config.project.agents;
2144
- if (agents.length === 0) {
2145
- console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
2146
- return;
2147
- }
2148
- for (const agent2 of agents) {
2149
- const command = agent2.command ?? "(no command)";
2150
- console.log(
2151
- `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
2152
- );
2153
- }
2154
- });
2155
- agent.command("add").description(
2156
- "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
2157
- ).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
2158
- "--input-mode <mode>",
2159
- "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
2160
- "stdin"
2161
- ).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
2162
- "--timeout-ms <ms>",
2163
- "Per-task timeout in milliseconds.",
2164
- (value) => Number(value)
2165
- ).action(async (options) => addAgent(options));
2166
- agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
2167
- registerAgentInspect(agent);
2168
- }
2169
- async function addAgent(options) {
2170
- const candidate = agentConfigSchema.safeParse({
2171
- name: options.name,
2172
- type: "cli",
2173
- command: options.command,
2174
- ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
2175
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2176
- input_mode: options.inputMode,
2177
- ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
2178
- ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
2179
- ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
2180
- ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
2181
- ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
2182
- ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
2183
- });
2184
- if (!candidate.success) {
2185
- const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
2186
- throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
2187
- }
2188
- const newAgent = candidate.data;
2189
- const existing = loadConfig();
2190
- if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2191
- throw new OmniNodeError(
2192
- "CLI_USAGE",
2193
- `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
2194
- );
2195
- }
2196
- const configPath = appendToConfigList("agents", newAgent);
2197
- console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
2198
- console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2199
- }
2200
- async function testAgent(name) {
2201
- const config = loadConfig();
2202
- const agentConfig = config.project.agents.find((a) => a.name === name);
2203
- if (!agentConfig) {
2204
- const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
2205
- throw new AgentError(
2206
- "AGENT_NOT_FOUND",
2207
- `Agent "${name}" is not configured. Configured agents: ${available}.`
2208
- );
2209
- }
2210
- const agent = createAgent(agentConfig);
2211
- console.log(
2212
- `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
2213
- );
2214
- const result = await agent.run({
2215
- taskId: `test-${Date.now()}`,
2216
- objective: "Reply with the single word: OK"
2217
- });
2218
- console.log(` status: ${result.status}`);
2219
- if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
2220
- if (result.summary) console.log(` output: ${result.summary}`);
2221
- if (result.rawOutput && result.rawOutput !== result.summary) {
2222
- console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
2223
- }
2224
- if (result.error) console.log(` error: ${result.error}`);
2225
- if (result.status !== "completed") process.exitCode = 1;
2226
- }
2227
-
2228
- // src/cli/commands/roles.ts
2229
- function registerRoleCommands(program) {
2230
- const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2231
- role.command("list").description("List configured roles.").action(async () => {
2232
- const config = loadConfig();
2233
- const roles = config.project.roles;
2234
- if (roles.length === 0) {
2235
- console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
2236
- return;
2237
- }
2238
- for (const role2 of roles) {
2239
- console.log(`${role2.id} ${role2.name}`);
2240
- for (const responsibility of role2.responsibilities) {
2241
- console.log(` - ${responsibility}`);
2242
- }
2243
- }
2244
- });
2245
- }
2246
-
2247
2313
  // src/tasks/engine.ts
2248
2314
  import { randomBytes } from "crypto";
2249
2315
  var ALLOWED_TRANSITIONS = {
@@ -2587,12 +2653,145 @@ function createTaskEngine(config, log = logger, directory) {
2587
2653
  });
2588
2654
  }
2589
2655
 
2656
+ // src/cli/commands/agents.ts
2657
+ var collectArgs = (value, previous) => [...previous, value];
2658
+ function registerAgentCommands(program) {
2659
+ const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2660
+ agent.command("list").description("List configured agents.").option("--json", "Emit agents as JSON.").action(async (options) => {
2661
+ const config = loadProjectConfig();
2662
+ const agents = config.project.agents;
2663
+ if (options.json) {
2664
+ console.log(JSON.stringify(config.project.agents, null, 2));
2665
+ return;
2666
+ }
2667
+ if (agents.length === 0) {
2668
+ console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
2669
+ return;
2670
+ }
2671
+ for (const agent2 of agents) {
2672
+ const command = agent2.command ?? "(no command)";
2673
+ console.log(
2674
+ `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
2675
+ );
2676
+ }
2677
+ });
2678
+ agent.command("add").description(
2679
+ "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
2680
+ ).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
2681
+ "--input-mode <mode>",
2682
+ "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
2683
+ "stdin"
2684
+ ).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
2685
+ "--timeout-ms <ms>",
2686
+ "Per-task timeout in milliseconds.",
2687
+ (value) => Number(value)
2688
+ ).action(async (options) => addAgent(options));
2689
+ agent.command("run <name> <objective>").description("Run a single task through this agent immediately.").action(async (name, objective) => {
2690
+ const config = loadProjectConfig();
2691
+ const agentConfig = config.project.agents.find((a) => a.name === name);
2692
+ if (!agentConfig) {
2693
+ throw new AgentError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
2694
+ }
2695
+ const engine = createTaskEngine(config);
2696
+ const task = await engine.create({ objective, agent: name, project: config.project.name });
2697
+ const finished = await engine.run(task.id);
2698
+ console.log(` id: ${finished.id}`);
2699
+ console.log(` status: ${finished.status}`);
2700
+ if (finished.result?.summary) console.log(` summary: ${finished.result.summary}`);
2701
+ if (finished.result?.error) console.log(` error: ${finished.result.error}`);
2702
+ process.exitCode = exitCodeForStatus(finished.status);
2703
+ });
2704
+ agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
2705
+ registerAgentInspect(agent);
2706
+ }
2707
+ async function addAgent(options) {
2708
+ const candidate = agentConfigSchema.safeParse({
2709
+ name: options.name,
2710
+ type: "cli",
2711
+ command: options.command,
2712
+ ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
2713
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2714
+ input_mode: options.inputMode,
2715
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
2716
+ ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
2717
+ ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
2718
+ ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
2719
+ ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
2720
+ ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
2721
+ });
2722
+ if (!candidate.success) {
2723
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
2724
+ throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
2725
+ }
2726
+ const newAgent = candidate.data;
2727
+ const existing = loadProjectConfig();
2728
+ if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2729
+ throw new OmniNodeError(
2730
+ "CLI_USAGE",
2731
+ `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
2732
+ );
2733
+ }
2734
+ const configPath = appendToConfigList("agents", newAgent);
2735
+ console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
2736
+ console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2737
+ }
2738
+ async function testAgent(name) {
2739
+ const config = loadProjectConfig();
2740
+ const agentConfig = config.project.agents.find((a) => a.name === name);
2741
+ if (!agentConfig) {
2742
+ const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
2743
+ throw new AgentError(
2744
+ "AGENT_NOT_FOUND",
2745
+ `Agent "${name}" is not configured. Configured agents: ${available}.`
2746
+ );
2747
+ }
2748
+ const agent = createAgent(agentConfig);
2749
+ console.log(
2750
+ `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
2751
+ );
2752
+ const result = await agent.run({
2753
+ taskId: `test-${Date.now()}`,
2754
+ objective: "Reply with the single word: OK"
2755
+ });
2756
+ console.log(` status: ${result.status}`);
2757
+ if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
2758
+ if (result.summary) console.log(` output: ${result.summary}`);
2759
+ if (result.rawOutput && result.rawOutput !== result.summary) {
2760
+ console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
2761
+ }
2762
+ if (result.error) console.log(` error: ${result.error}`);
2763
+ if (result.status !== "completed") process.exitCode = 1;
2764
+ }
2765
+
2766
+ // src/cli/commands/roles.ts
2767
+ function registerRoleCommands(program) {
2768
+ const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2769
+ role.command("list").description("List configured roles.").option("--json", "Emit roles as JSON.").action(async (options) => {
2770
+ const config = loadProjectConfig();
2771
+ const roles = config.project.roles;
2772
+ if (options.json) {
2773
+ console.log(JSON.stringify(config.project.roles, null, 2));
2774
+ return;
2775
+ }
2776
+ if (roles.length === 0) {
2777
+ console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
2778
+ return;
2779
+ }
2780
+ for (const role2 of roles) {
2781
+ console.log(`${role2.id} ${role2.name}`);
2782
+ for (const responsibility of role2.responsibilities) {
2783
+ console.log(` - ${responsibility}`);
2784
+ }
2785
+ }
2786
+ });
2787
+ }
2788
+
2590
2789
  // src/cli/commands/tasks.ts
2591
2790
  var collectFiles = (value, previous) => [...previous, value];
2592
2791
  function registerTaskCommands(program) {
2593
2792
  const task = program.command("task").description("Create, run and inspect tasks (\xA713).");
2594
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) => {
2595
- const config = loadConfig();
2794
+ const config = loadProjectConfig();
2596
2795
  const engine = createTaskEngine(config);
2597
2796
  const created = await engine.create({
2598
2797
  objective,
@@ -2609,15 +2808,15 @@ function registerTaskCommands(program) {
2609
2808
  console.log(`Run \`omninode task run ${created.id}\` to execute it.`);
2610
2809
  });
2611
2810
  task.command("run <task-id>").description("Run a task through its assigned agent.").action(async (taskId) => {
2612
- const config = loadConfig();
2811
+ const config = loadProjectConfig();
2613
2812
  const engine = createTaskEngine(config);
2614
2813
  console.log(`Running task ${taskId}...`);
2615
2814
  const finished = await engine.run(taskId);
2616
2815
  printOutcome(finished);
2617
- if (finished.status !== "completed") process.exitCode = 1;
2816
+ process.exitCode = exitCodeForStatus(finished.status);
2618
2817
  });
2619
2818
  task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
2620
- const config = loadConfig();
2819
+ const config = loadProjectConfig();
2621
2820
  const engine = createTaskEngine(config);
2622
2821
  const found = await engine.get(taskId);
2623
2822
  if (!found) {
@@ -2628,12 +2827,16 @@ function registerTaskCommands(program) {
2628
2827
  }
2629
2828
  printOutcome(found, { verbose: true });
2630
2829
  });
2631
- 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).").action(async (options) => {
2632
- const config = loadConfig();
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) => {
2831
+ const config = loadProjectConfig();
2633
2832
  const engine = createTaskEngine(config);
2634
2833
  const tasks = await engine.list(
2635
2834
  options.status !== void 0 ? { status: options.status } : {}
2636
2835
  );
2836
+ if (options.json) {
2837
+ console.log(JSON.stringify(tasks, null, 2));
2838
+ return;
2839
+ }
2637
2840
  if (tasks.length === 0) {
2638
2841
  console.log("No tasks found. Create one with `omninode task create`.");
2639
2842
  return;
@@ -2643,13 +2846,13 @@ function registerTaskCommands(program) {
2643
2846
  }
2644
2847
  });
2645
2848
  task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
2646
- const config = loadConfig();
2849
+ const config = loadProjectConfig();
2647
2850
  const engine = createTaskEngine(config);
2648
2851
  const cancelled = await engine.cancel(taskId);
2649
2852
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
2650
2853
  });
2651
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) => {
2652
- const config = loadConfig();
2855
+ const config = loadProjectConfig();
2653
2856
  const engine = createTaskEngine(config);
2654
2857
  const reset = await engine.retry(taskId);
2655
2858
  console.log(`Task ${reset.id} reset to "${reset.status}".`);
@@ -2657,7 +2860,7 @@ function registerTaskCommands(program) {
2657
2860
  console.log(`Running task ${reset.id}...`);
2658
2861
  const finished = await engine.run(reset.id);
2659
2862
  printOutcome(finished);
2660
- if (finished.status !== "completed") process.exitCode = 1;
2863
+ process.exitCode = exitCodeForStatus(finished.status);
2661
2864
  });
2662
2865
  registerTaskInspect(task);
2663
2866
  }
@@ -3128,12 +3331,16 @@ function service() {
3128
3331
  }
3129
3332
  function registerReportCommands(program) {
3130
3333
  const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
3131
- report.command("list").description("List stored reports.").option("--task <taskId>", "Only reports for this task.").option("--agent <name>", "Only reports from this agent.").action(async (options) => {
3334
+ report.command("list").description("List stored reports.").option("--task <taskId>", "Only reports for this task.").option("--agent <name>", "Only reports from this agent.").option("--json", "Emit reports as JSON.").action(async (options) => {
3132
3335
  const svc = service();
3133
3336
  const reports = await svc.storeRef.listReports({
3134
3337
  ...options.task !== void 0 ? { taskId: options.task } : {},
3135
3338
  ...options.agent !== void 0 ? { agent: options.agent } : {}
3136
3339
  });
3340
+ if (options.json) {
3341
+ console.log(JSON.stringify(reports, null, 2));
3342
+ return;
3343
+ }
3137
3344
  if (reports.length === 0) {
3138
3345
  console.log("No reports stored yet. Run a pipeline to collect some.");
3139
3346
  return;
@@ -3142,11 +3349,15 @@ function registerReportCommands(program) {
3142
3349
  console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
3143
3350
  }
3144
3351
  });
3145
- report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
3352
+ report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").option("--json", "Emit combined reports as JSON.").action(async (options) => {
3146
3353
  const svc = service();
3147
3354
  const combined = await svc.storeRef.listCombined({
3148
3355
  ...options.run !== void 0 ? { pipelineRunId: options.run } : {}
3149
3356
  });
3357
+ if (options.json) {
3358
+ console.log(JSON.stringify(combined, null, 2));
3359
+ return;
3360
+ }
3150
3361
  if (combined.length === 0) {
3151
3362
  console.log("No combined reports stored yet. Run a pipeline to generate one.");
3152
3363
  return;
@@ -3226,6 +3437,10 @@ function registerReportCommands(program) {
3226
3437
  });
3227
3438
  }
3228
3439
 
3440
+ // src/cli/commands/pipelines.ts
3441
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
3442
+ import { parseDocument as parseDocument3 } from "yaml";
3443
+
3229
3444
  // src/pipelines/engine.ts
3230
3445
  import { randomBytes as randomBytes5 } from "crypto";
3231
3446
 
@@ -4177,9 +4392,13 @@ function buildPipelineEngine(config, log = logger) {
4177
4392
  // src/cli/commands/pipelines.ts
4178
4393
  function registerPipelineCommands(program) {
4179
4394
  const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
4180
- pipeline.command("list").description("List configured pipelines.").action(async () => {
4181
- const config = loadConfig();
4395
+ pipeline.command("list").description("List configured pipelines.").option("--json", "Emit pipelines as JSON.").action(async (options) => {
4396
+ const config = loadProjectConfig();
4182
4397
  const pipelines = config.project.pipelines;
4398
+ if (options.json) {
4399
+ console.log(JSON.stringify(config.project.pipelines, null, 2));
4400
+ return;
4401
+ }
4183
4402
  if (pipelines.length === 0) {
4184
4403
  console.log("No pipelines configured. Add one under `project.pipelines` in omninode.yaml.");
4185
4404
  return;
@@ -4195,7 +4414,7 @@ function registerPipelineCommands(program) {
4195
4414
  pipeline.command("retry <run-id> [objective]").description(
4196
4415
  "Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
4197
4416
  ).action(async (runId, objective) => {
4198
- const config = loadConfig();
4417
+ const config = loadProjectConfig();
4199
4418
  const engine = buildPipelineEngine(config);
4200
4419
  const previous = await engine.get(runId);
4201
4420
  if (!previous) {
@@ -4214,14 +4433,17 @@ function registerPipelineCommands(program) {
4214
4433
  await runPipelineAction(def.id, objective ?? previous.objective);
4215
4434
  });
4216
4435
  pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
4217
- const config = loadConfig();
4436
+ const config = loadProjectConfig();
4218
4437
  const engine = buildPipelineEngine(config);
4219
4438
  await engine.cancel(runId);
4220
4439
  console.log(`Cancellation requested for run ${runId}.`);
4221
4440
  console.log("A run executing in another process stops at its next check point.");
4222
4441
  });
4442
+ pipeline.command("create <id>").description("Create a pipeline from a YAML/JSON file and append it to omninode.yaml.").requiredOption("--from <file>", "File containing { objective?, steps: [...] } (YAML or JSON).").action(async (id, options) => {
4443
+ await createPipelineFromFile(id, options.from);
4444
+ });
4223
4445
  pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
4224
- const config = loadConfig();
4446
+ const config = loadProjectConfig();
4225
4447
  const def = findPipeline(config, id);
4226
4448
  const engine = buildPipelineEngine(config);
4227
4449
  engine.validate(def);
@@ -4232,7 +4454,7 @@ function registerPipelineCommands(program) {
4232
4454
  }
4233
4455
  });
4234
4456
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
4235
- const config = loadConfig();
4457
+ const config = loadProjectConfig();
4236
4458
  const engine = buildPipelineEngine(config);
4237
4459
  const runs = await engine.listRuns({ pipelineId: id });
4238
4460
  if (runs.length === 0) {
@@ -4245,7 +4467,7 @@ function registerPipelineCommands(program) {
4245
4467
  }
4246
4468
  });
4247
4469
  pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
4248
- const config = loadConfig();
4470
+ const config = loadProjectConfig();
4249
4471
  const engine = buildPipelineEngine(config);
4250
4472
  const run2 = await engine.get(runId);
4251
4473
  if (!run2) {
@@ -4277,7 +4499,7 @@ function registerPipelineCommands(program) {
4277
4499
  registerPipelineInspect(pipeline);
4278
4500
  }
4279
4501
  async function runPipelineAction(id, objective) {
4280
- const config = loadConfig();
4502
+ const config = loadProjectConfig();
4281
4503
  const def = findPipeline(config, id);
4282
4504
  const engine = buildPipelineEngine(config);
4283
4505
  console.log(
@@ -4303,11 +4525,45 @@ async function runPipelineAction(id, objective) {
4303
4525
  console.log(` result: ${finished.resultSummary}`);
4304
4526
  }
4305
4527
  console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
4306
- if (finished.status !== "completed") process.exitCode = 1;
4528
+ process.exitCode = exitCodeForStatus(finished.status);
4307
4529
  }
4308
4530
  function registerRunAlias(program) {
4309
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));
4310
4532
  }
4533
+ async function createPipelineFromFile(id, file) {
4534
+ const configPath = activeConfigPath();
4535
+ if (!configPath) {
4536
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
4537
+ }
4538
+ let definition;
4539
+ try {
4540
+ definition = parseDocument3(readFileSync4(file, "utf8")).toJS();
4541
+ } catch (error) {
4542
+ throw new PipelineError("PIPELINE_INVALID", `Could not read pipeline definition from ${file}.`, {
4543
+ cause: error
4544
+ });
4545
+ }
4546
+ const candidate = pipelineConfigSchema.safeParse({ id, ...definition });
4547
+ if (!candidate.success) {
4548
+ const issues = candidate.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
4549
+ throw new PipelineError("PIPELINE_INVALID", `Invalid pipeline definition: ${issues}`);
4550
+ }
4551
+ const engine = buildPipelineEngine(loadProjectConfig());
4552
+ engine.validate({ id, ...candidate.data });
4553
+ const doc = parseDocument3(readFileSync4(configPath, "utf8"));
4554
+ if (doc.getIn(["project", "pipelines"]) === void 0) {
4555
+ doc.setIn(["project", "pipelines"], [candidate.data]);
4556
+ } else {
4557
+ doc.addIn(["project", "pipelines"], candidate.data);
4558
+ }
4559
+ const validated = appConfigSchema.safeParse(doc.toJS());
4560
+ if (!validated.success) {
4561
+ throw new PipelineError("PIPELINE_INVALID", "Adding this pipeline would produce an invalid configuration.");
4562
+ }
4563
+ writeFileSync4(configPath, doc.toString(), "utf8");
4564
+ console.log(`Created pipeline "${id}" (${candidate.data.steps.length} step(s)) in ${configPath}.`);
4565
+ console.log(`Run it with: omninode pipeline run ${id} "<objective>"`);
4566
+ }
4311
4567
  function isInterrupted(run2) {
4312
4568
  return run2.status === "running" && run2.finishedAt === void 0;
4313
4569
  }
@@ -4327,7 +4583,7 @@ function findPipeline(config, id) {
4327
4583
  function registerMemoryCommands(program) {
4328
4584
  const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
4329
4585
  memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
4330
- const config = loadConfig();
4586
+ const config = loadProjectConfig();
4331
4587
  const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
4332
4588
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4333
4589
  const entries = await svc.providerRef.query({ limit: 1e4 });
@@ -4341,14 +4597,18 @@ function registerMemoryCommands(program) {
4341
4597
  console.log(` ${scope}: ${count}`);
4342
4598
  }
4343
4599
  });
4344
- 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).action(async (text, options) => {
4345
- const config = loadConfig();
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) => {
4601
+ const config = loadProjectConfig();
4346
4602
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4347
4603
  const entries = await svc.providerRef.query({
4348
4604
  text,
4349
4605
  ...options.scope !== void 0 ? { scope: options.scope } : {},
4350
4606
  limit: options.limit
4351
4607
  });
4608
+ if (options.json) {
4609
+ console.log(JSON.stringify(entries, null, 2));
4610
+ return;
4611
+ }
4352
4612
  if (entries.length === 0) {
4353
4613
  console.log("No relevant memory found.");
4354
4614
  return;
@@ -4363,7 +4623,7 @@ function registerMemoryCommands(program) {
4363
4623
  "Comma-separated tags.",
4364
4624
  (value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
4365
4625
  ).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
4366
- const config = loadConfig();
4626
+ const config = loadProjectConfig();
4367
4627
  const svc = new MemoryService(createMemoryProvider(config.project.memory));
4368
4628
  const key = options.key ?? `note:${Date.now().toString(36)}`;
4369
4629
  await svc.providerRef.write({
@@ -4379,11 +4639,15 @@ function registerMemoryCommands(program) {
4379
4639
  // src/cli/commands/plan.ts
4380
4640
  function registerPlanCommands(program) {
4381
4641
  const plan = program.command("plan").description("Inspect implementation plans produced by the planner layer (\xA718).");
4382
- plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").action(async (options) => {
4642
+ plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").option("--json", "Emit plans as JSON.").action(async (options) => {
4383
4643
  const store = new FilePlanStore();
4384
4644
  const plans = await store.list({
4385
4645
  ...options.run !== void 0 ? { pipelineRunId: options.run } : {}
4386
4646
  });
4647
+ if (options.json) {
4648
+ console.log(JSON.stringify(plans, null, 2));
4649
+ return;
4650
+ }
4387
4651
  if (plans.length === 0) {
4388
4652
  console.log("No plans stored yet. Run a pipeline with a plan step to generate one.");
4389
4653
  return;
@@ -4442,7 +4706,7 @@ function countBy(items, key) {
4442
4706
  }
4443
4707
  function registerStatusCommand(program) {
4444
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) => {
4445
- const config = loadConfig();
4709
+ const config = loadProjectConfig();
4446
4710
  const tasks = await new FileTaskStore().list();
4447
4711
  const runs = await new FilePipelineRunStore().list();
4448
4712
  const reportStore = new FileReportStore();
@@ -4523,8 +4787,8 @@ var STORE_FILES = [
4523
4787
  async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4524
4788
  const reports = [];
4525
4789
  for (const file of STORE_FILES) {
4526
- const path = `${directory}/${file}`;
4527
- const state = await inspectDocument(path);
4790
+ const path2 = `${directory}/${file}`;
4791
+ const state = await inspectDocument(path2);
4528
4792
  if (state.status === "missing") {
4529
4793
  reports.push({ file, state: state.status, migrated: false });
4530
4794
  continue;
@@ -4546,9 +4810,9 @@ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, op
4546
4810
  reports.push({ file, state: state.status, migrated: false, note: state.detail });
4547
4811
  continue;
4548
4812
  }
4549
- const items = await readDocument(path, void 0, false);
4813
+ const items = await readDocument(path2, void 0, false);
4550
4814
  if (!options.checkOnly) {
4551
- await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4815
+ await writeFileAtomic(path2, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4552
4816
  `);
4553
4817
  }
4554
4818
  const beforeCount = items.length;
@@ -4591,7 +4855,7 @@ function registerSecurityCommand(program) {
4591
4855
  console.log(`Unknown security action "${action}". Available: audit.`);
4592
4856
  return;
4593
4857
  }
4594
- const config = loadConfig();
4858
+ const config = loadProjectConfig();
4595
4859
  const findings = [];
4596
4860
  for (const agent of config.project.agents) {
4597
4861
  const policy = resolveEnvPolicy(agent);
@@ -4650,12 +4914,155 @@ Trust model: agents execute with the permissions of the OS user running OmniNode
4650
4914
  });
4651
4915
  }
4652
4916
 
4917
+ // src/cli/commands/config.ts
4918
+ import { readFileSync as readFileSync5 } from "fs";
4919
+ import { parse as parseYaml2 } from "yaml";
4920
+ function registerConfigCommands(program) {
4921
+ const config = program.command("config").description("Inspect the project configuration (omninode.yaml).");
4922
+ config.command("path").description("Print the path of the configuration file in use.").action(() => {
4923
+ const path2 = activeConfigPath();
4924
+ console.log(path2 ?? "no omninode.yaml found");
4925
+ });
4926
+ config.command("show").description("Print the effective configuration (credential references only, never values).").option("--json", "Emit the configuration as JSON.").action((options) => {
4927
+ const resolved = loadConfigDetailed().config;
4928
+ const payload = {
4929
+ project: resolved.project.name,
4930
+ providers: resolved.project.providers.map((p) => ({
4931
+ name: p.name,
4932
+ type: p.type,
4933
+ baseUrl: p.baseUrl,
4934
+ apiKeyEnvVar: p.apiKeyEnvVar ?? null
4935
+ })),
4936
+ agents: resolved.project.agents.map((a) => ({ name: a.name, command: a.command ?? null, inputMode: a.inputMode })),
4937
+ roles: resolved.project.roles.map((r) => r.id),
4938
+ pipelines: resolved.project.pipelines.map((p) => p.id),
4939
+ memory: resolved.project.memory ?? null,
4940
+ logging: resolved.logging ?? null
4941
+ };
4942
+ if (options.json) {
4943
+ console.log(JSON.stringify(payload, null, 2));
4944
+ return;
4945
+ }
4946
+ console.log(`project: ${payload.project}`);
4947
+ console.log(`providers: ${payload.providers.map((p) => `${p.name} (${p.type}, key=${p.apiKeyEnvVar ? `$${p.apiKeyEnvVar}` : "none"})`).join(", ") || "none"}`);
4948
+ console.log(`agents: ${payload.agents.map((a) => a.name).join(", ") || "none"}`);
4949
+ console.log(`roles: ${payload.roles.join(", ") || "none"}`);
4950
+ console.log(`pipelines: ${payload.pipelines.join(", ") || "none"}`);
4951
+ console.log(`memory: ${payload.memory ? `${payload.memory.provider}${payload.memory.required ? " (required)" : ""}` : "not configured"}`);
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
+ });
4961
+ config.command("validate").description("Validate the configuration file, including the inline-secret scan.").action(() => {
4962
+ const path2 = activeConfigPath();
4963
+ if (!path2) throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
4964
+ const findings = scanForInlineSecrets(readFileSync5(path2, "utf8"));
4965
+ if (findings.length > 0) {
4966
+ throw new OmniNodeError(
4967
+ "CONFIG_INVALID",
4968
+ `${path2} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
4969
+ );
4970
+ }
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
+ }
4988
+ });
4989
+ }
4990
+
4991
+ // src/cli/commands/models.ts
4992
+ var CAPABILITIES = [
4993
+ "chat",
4994
+ "coding",
4995
+ "tools",
4996
+ "vision",
4997
+ "embeddings"
4998
+ ];
4999
+ function registerModelsCommand(program) {
5000
+ for (const name of ["models", "model"]) {
5001
+ registerModelGroup(program, name);
5002
+ }
5003
+ }
5004
+ function registerModelGroup(program, name) {
5005
+ program.command(name).description("Discover and list models from configured providers (model registry, \xA78).").argument("[provider]", "Only discover from this provider.").option("-c, --capability <capability>", `Filter by capability: ${CAPABILITIES.join(", ")}.`).option("--json", "Emit the discovered models as JSON.").action(async (providerName, options) => {
5006
+ const capability = options.capability;
5007
+ if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
5008
+ throw new OmniNodeError(
5009
+ "CLI_USAGE",
5010
+ `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
5011
+ );
5012
+ }
5013
+ const config = loadProjectConfig();
5014
+ const selected = config.project.providers.filter(
5015
+ (p) => providerName ? p.name === providerName : true
5016
+ );
5017
+ if (selected.length === 0) {
5018
+ if (options.json) {
5019
+ console.log("[]");
5020
+ return;
5021
+ }
5022
+ throw new OmniNodeError(
5023
+ "PROVIDER_NOT_FOUND",
5024
+ providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
5025
+ );
5026
+ }
5027
+ const registry = new ModelRegistry();
5028
+ for (const providerConfig of selected) {
5029
+ try {
5030
+ const provider = createProvider(providerConfig);
5031
+ const discovered = await registry.discoverFrom(provider);
5032
+ console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
5033
+ } catch (error) {
5034
+ const message = error instanceof Error ? error.message : String(error);
5035
+ console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
5036
+ }
5037
+ }
5038
+ const models = registry.list({
5039
+ ...providerName !== void 0 ? { provider: providerName } : {},
5040
+ ...capability !== void 0 ? { capability } : {}
5041
+ });
5042
+ if (options.json) {
5043
+ console.log(JSON.stringify(models, null, 2));
5044
+ return;
5045
+ }
5046
+ if (models.length === 0) {
5047
+ console.log("No models found.");
5048
+ return;
5049
+ }
5050
+ console.log("");
5051
+ for (const model of models) {
5052
+ console.log(`${model.provider}:${model.id}`);
5053
+ }
5054
+ });
5055
+ }
5056
+
4653
5057
  // src/cli/index.ts
4654
5058
  function createProgram() {
4655
5059
  const program = new Command();
4656
5060
  program.name("omninode").description(
4657
5061
  "Provider-agnostic multi-AI orchestration platform: coordinate CLI agents, AI providers, memory, roles, reports and planning in one execution system."
4658
- ).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
+ });
4659
5066
  registerInitCommand(program);
4660
5067
  registerProjectCommands(program);
4661
5068
  registerProviderCommands(program);
@@ -4671,15 +5078,14 @@ function createProgram() {
4671
5078
  registerAuditCommand(program);
4672
5079
  registerMigrateCommand(program);
4673
5080
  registerSecurityCommand(program);
5081
+ registerConfigCommands(program);
4674
5082
  registerRunAlias(program);
4675
5083
  return program;
4676
5084
  }
4677
5085
  function applyLoggingConfig() {
4678
5086
  try {
4679
- if (findConfigFile()) {
4680
- const logging = loadConfig().logging;
4681
- if (logging) configureLogger(logging);
4682
- }
5087
+ const resolved = loadProjectConfig();
5088
+ if (resolved.logging) configureLogger(resolved.logging);
4683
5089
  } catch {
4684
5090
  }
4685
5091
  }
@@ -4693,15 +5099,16 @@ async function run(argv) {
4693
5099
  }
4694
5100
  }
4695
5101
  function handleError(error) {
5102
+ const code = exitCodeFor(error);
4696
5103
  if (isOmniNodeError(error)) {
4697
5104
  console.error(`error: [${error.code}] ${error.message}`);
4698
5105
  if (error.details !== void 0) {
4699
5106
  console.error(JSON.stringify(error.details, null, 2));
4700
5107
  }
4701
- process.exit(1);
5108
+ process.exit(code);
4702
5109
  }
4703
5110
  console.error(error instanceof Error ? error.message : String(error));
4704
- process.exit(1);
5111
+ process.exit(code);
4705
5112
  }
4706
5113
  var entry = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : "";
4707
5114
  if (import.meta.url === entry) {