@hilbras/omninode 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/cli/index.js +842 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1036 -0
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -4
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/version.ts
|
|
9
|
+
var OMNINODE_VERSION = "0.2.0";
|
|
10
|
+
|
|
11
|
+
// src/errors/index.ts
|
|
12
|
+
var OmniNodeError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
details;
|
|
15
|
+
constructor(code, message, options = {}) {
|
|
16
|
+
super(message, { cause: options.cause });
|
|
17
|
+
this.name = new.target.name;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.details = options.details;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var ConfigError = class extends OmniNodeError {
|
|
23
|
+
constructor(code, message, options = {}) {
|
|
24
|
+
super(code, message, options);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var ProviderError = class extends OmniNodeError {
|
|
28
|
+
constructor(code, message, options = {}) {
|
|
29
|
+
super(code, message, options);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function isOmniNodeError(value) {
|
|
33
|
+
return value instanceof OmniNodeError;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/cli/commands/init.ts
|
|
37
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
38
|
+
|
|
39
|
+
// src/config/schema.ts
|
|
40
|
+
import { z } from "zod";
|
|
41
|
+
var providerConfigSchema = z.object({
|
|
42
|
+
name: z.string().min(1),
|
|
43
|
+
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
44
|
+
base_url: z.string().url(),
|
|
45
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
46
|
+
headers: z.record(z.string()).optional(),
|
|
47
|
+
enabled: z.boolean().optional(),
|
|
48
|
+
metadata: z.record(z.unknown()).optional()
|
|
49
|
+
}).strict();
|
|
50
|
+
var agentConfigSchema = z.object({
|
|
51
|
+
name: z.string().min(1),
|
|
52
|
+
/** "cli" agents communicate over stdin/stdout (process adapter). */
|
|
53
|
+
type: z.enum(["cli", "native", "process"]).default("cli"),
|
|
54
|
+
command: z.string().min(1).optional(),
|
|
55
|
+
args: z.array(z.string()).optional(),
|
|
56
|
+
cwd: z.string().optional(),
|
|
57
|
+
env: z.record(z.string()).optional(),
|
|
58
|
+
metadata: z.record(z.unknown()).optional()
|
|
59
|
+
}).strict();
|
|
60
|
+
var roleConfigSchema = z.object({
|
|
61
|
+
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "role id must be kebab-case"),
|
|
62
|
+
name: z.string().min(1),
|
|
63
|
+
description: z.string().optional(),
|
|
64
|
+
responsibilities: z.array(z.string()).default([]),
|
|
65
|
+
expected_outputs: z.array(z.string()).default([]),
|
|
66
|
+
preferred_agents: z.array(z.string()).optional(),
|
|
67
|
+
system_prompt: z.string().optional(),
|
|
68
|
+
metadata: z.record(z.unknown()).optional()
|
|
69
|
+
}).strict();
|
|
70
|
+
var projectConfigSchema = z.object({
|
|
71
|
+
name: z.string().min(1),
|
|
72
|
+
providers: z.array(providerConfigSchema).default([]),
|
|
73
|
+
agents: z.array(agentConfigSchema).default([]),
|
|
74
|
+
roles: z.array(roleConfigSchema).default([]),
|
|
75
|
+
memory: z.object({ provider: z.string().min(1) }).strict().optional()
|
|
76
|
+
}).strict();
|
|
77
|
+
var appConfigSchema = z.object({
|
|
78
|
+
project: projectConfigSchema,
|
|
79
|
+
logging: z.object({ level: z.enum(["debug", "info", "warn", "error"]) }).strict().optional()
|
|
80
|
+
}).strict();
|
|
81
|
+
|
|
82
|
+
// src/config/loader.ts
|
|
83
|
+
import { existsSync, readFileSync } from "fs";
|
|
84
|
+
import { parse as parseYaml } from "yaml";
|
|
85
|
+
var CONFIG_FILENAMES = ["omninode.yaml", "omninode.yml", "omninode.json"];
|
|
86
|
+
function findConfigFile(directory = process.cwd()) {
|
|
87
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
88
|
+
const candidate = `${directory}/${filename}`;
|
|
89
|
+
if (existsSync(candidate)) return candidate;
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
function expandEnvRefs(text, env, source = "config") {
|
|
94
|
+
return text.replace(
|
|
95
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
|
|
96
|
+
(_match, name, fallback) => {
|
|
97
|
+
const value = env[name] ?? fallback;
|
|
98
|
+
if (value === void 0) {
|
|
99
|
+
throw new ConfigError(
|
|
100
|
+
"CONFIG_INVALID",
|
|
101
|
+
`Environment variable "${name}" is referenced in ${source} but is not set and has no default.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function loadConfig(options = {}) {
|
|
109
|
+
const env = options.env ?? process.env;
|
|
110
|
+
const path = options.path ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
|
|
111
|
+
if (!path) {
|
|
112
|
+
throw new ConfigError(
|
|
113
|
+
"CONFIG_NOT_FOUND",
|
|
114
|
+
"No omninode.yaml found. Run `omninode init` to create one, or pass an explicit path."
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const raw = readFileSync(path, "utf8");
|
|
118
|
+
const expanded = expandEnvRefs(raw, env, path);
|
|
119
|
+
let data;
|
|
120
|
+
try {
|
|
121
|
+
data = parseYaml(expanded);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path}.`, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
const parsed = appConfigSchema.safeParse(data);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
const issues = parsed.error.issues.map((issue) => {
|
|
128
|
+
const location = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
129
|
+
return `${location}: ${issue.message}`;
|
|
130
|
+
});
|
|
131
|
+
throw new ConfigError("CONFIG_INVALID", `Invalid configuration in ${path}.`, {
|
|
132
|
+
details: { issues }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return toAppConfig(parsed.data);
|
|
136
|
+
}
|
|
137
|
+
function toAppConfig(yaml) {
|
|
138
|
+
return {
|
|
139
|
+
project: {
|
|
140
|
+
name: yaml.project.name,
|
|
141
|
+
providers: yaml.project.providers.map(toProviderConfig),
|
|
142
|
+
agents: yaml.project.agents.map(toAgentConfig),
|
|
143
|
+
roles: yaml.project.roles.map(toRoleDefinition),
|
|
144
|
+
...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
|
|
145
|
+
},
|
|
146
|
+
...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function toProviderConfig(raw) {
|
|
150
|
+
return {
|
|
151
|
+
name: raw.name,
|
|
152
|
+
type: raw.type,
|
|
153
|
+
baseUrl: raw.base_url,
|
|
154
|
+
apiKeyEnvVar: raw.api_key_env_var,
|
|
155
|
+
headers: raw.headers,
|
|
156
|
+
enabled: raw.enabled,
|
|
157
|
+
metadata: raw.metadata
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function toAgentConfig(raw) {
|
|
161
|
+
const integration = raw.type === "native" ? "native" : "process";
|
|
162
|
+
return {
|
|
163
|
+
name: raw.name,
|
|
164
|
+
integration,
|
|
165
|
+
command: raw.command,
|
|
166
|
+
args: raw.args,
|
|
167
|
+
cwd: raw.cwd,
|
|
168
|
+
env: raw.env,
|
|
169
|
+
metadata: raw.metadata
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function toRoleDefinition(raw) {
|
|
173
|
+
return {
|
|
174
|
+
id: raw.id,
|
|
175
|
+
name: raw.name,
|
|
176
|
+
description: raw.description,
|
|
177
|
+
responsibilities: raw.responsibilities,
|
|
178
|
+
expectedOutputs: raw.expected_outputs,
|
|
179
|
+
preferredAgents: raw.preferred_agents,
|
|
180
|
+
systemPrompt: raw.system_prompt,
|
|
181
|
+
metadata: raw.metadata
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/config/default.ts
|
|
186
|
+
function defaultProjectConfigYaml(projectName = "My Project") {
|
|
187
|
+
return `project:
|
|
188
|
+
name: ${projectName}
|
|
189
|
+
|
|
190
|
+
# Providers connect OmniNode to AI model gateways (Phase 1+).
|
|
191
|
+
# Credentials are referenced by environment variable, never stored in this file.
|
|
192
|
+
providers: []
|
|
193
|
+
# - name: my-gateway
|
|
194
|
+
# type: openai-compatible
|
|
195
|
+
# base_url: https://example.com/v1
|
|
196
|
+
# api_key_env_var: MY_API_KEY
|
|
197
|
+
|
|
198
|
+
# CLI agents registered with OmniNode (Phase 3 wires them up).
|
|
199
|
+
agents: []
|
|
200
|
+
# - name: opencode
|
|
201
|
+
# type: cli
|
|
202
|
+
# command: opencode
|
|
203
|
+
|
|
204
|
+
# Roles describe what an AI is supposed to do (Phase 4).
|
|
205
|
+
roles: []
|
|
206
|
+
# - id: architecture-reviewer
|
|
207
|
+
# name: Architecture Reviewer
|
|
208
|
+
# responsibilities:
|
|
209
|
+
# - architecture analysis
|
|
210
|
+
# expected_outputs:
|
|
211
|
+
# - findings
|
|
212
|
+
# - recommendations
|
|
213
|
+
|
|
214
|
+
# Memory is optional; Remembera is the preferred integration (Phase 7).
|
|
215
|
+
# memory:
|
|
216
|
+
# provider: remembera
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/cli/commands/init.ts
|
|
221
|
+
async function runInit(options) {
|
|
222
|
+
const target = `${process.cwd()}/omninode.yaml`;
|
|
223
|
+
if (!options.force && existsSync2(target)) {
|
|
224
|
+
throw new OmniNodeError(
|
|
225
|
+
"CLI_USAGE",
|
|
226
|
+
`${target} already exists. Use --force to overwrite it.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
writeFileSync(target, defaultProjectConfigYaml(options.name), "utf8");
|
|
230
|
+
console.log(`Created ${target}`);
|
|
231
|
+
console.log("\nNext steps:");
|
|
232
|
+
console.log(" 1. Add a provider under `project.providers` (see omninode.yaml.example).");
|
|
233
|
+
console.log(" 2. Register CLI agents under `project.agents`.");
|
|
234
|
+
console.log(" 3. Run `omninode provider list` to validate the configuration.");
|
|
235
|
+
}
|
|
236
|
+
function registerInitCommand(program) {
|
|
237
|
+
program.command("init").description("Create an omninode.yaml project configuration in the current directory.").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));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/cli/commands/project.ts
|
|
241
|
+
function registerProjectCommands(program) {
|
|
242
|
+
const project = program.command("project").description("Project-level operations.");
|
|
243
|
+
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));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/cli/commands/providers.ts
|
|
247
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
248
|
+
import { parseDocument } from "yaml";
|
|
249
|
+
|
|
250
|
+
// src/providers/auth.ts
|
|
251
|
+
function resolveApiKey(config, env = process.env) {
|
|
252
|
+
if (!config.apiKeyEnvVar) return void 0;
|
|
253
|
+
const value = env[config.apiKeyEnvVar];
|
|
254
|
+
if (!value) {
|
|
255
|
+
throw new ProviderError(
|
|
256
|
+
"PROVIDER_AUTH_FAILED",
|
|
257
|
+
`Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
function authHeaders(config, env = process.env) {
|
|
263
|
+
const key = resolveApiKey(config, env);
|
|
264
|
+
const base = { ...config.headers ?? {} };
|
|
265
|
+
return key ? { ...base, authorization: `Bearer ${key}` } : base;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/providers/http.ts
|
|
269
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
270
|
+
async function requestJson(url, options = {}) {
|
|
271
|
+
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
272
|
+
let response;
|
|
273
|
+
try {
|
|
274
|
+
response = await fetch(url, {
|
|
275
|
+
method,
|
|
276
|
+
headers: {
|
|
277
|
+
accept: "application/json",
|
|
278
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
279
|
+
...headers
|
|
280
|
+
},
|
|
281
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
282
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
286
|
+
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
287
|
+
cause: error
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
let parsed;
|
|
291
|
+
try {
|
|
292
|
+
parsed = await response.json();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
|
|
295
|
+
cause: error,
|
|
296
|
+
details: { status: response.status }
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return { status: response.status, body: parsed };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/logger/index.ts
|
|
303
|
+
var LEVEL_ORDER = {
|
|
304
|
+
debug: 10,
|
|
305
|
+
info: 20,
|
|
306
|
+
warn: 30,
|
|
307
|
+
error: 40
|
|
308
|
+
};
|
|
309
|
+
var ConsoleLogSink = class {
|
|
310
|
+
constructor(stream = process.stderr) {
|
|
311
|
+
this.stream = stream;
|
|
312
|
+
}
|
|
313
|
+
stream;
|
|
314
|
+
write(_level, line) {
|
|
315
|
+
this.stream.write(line + "\n");
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var Logger = class _Logger {
|
|
319
|
+
level;
|
|
320
|
+
sink;
|
|
321
|
+
context;
|
|
322
|
+
constructor(options = {}) {
|
|
323
|
+
this.level = options.level ?? "info";
|
|
324
|
+
this.sink = options.sink ?? new ConsoleLogSink();
|
|
325
|
+
this.context = options.context ?? {};
|
|
326
|
+
}
|
|
327
|
+
child(context) {
|
|
328
|
+
return new _Logger({
|
|
329
|
+
level: this.level,
|
|
330
|
+
sink: this.sink,
|
|
331
|
+
context: { ...this.context, ...context }
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
debug(message, data) {
|
|
335
|
+
this.log("debug", message, data);
|
|
336
|
+
}
|
|
337
|
+
info(message, data) {
|
|
338
|
+
this.log("info", message, data);
|
|
339
|
+
}
|
|
340
|
+
warn(message, data) {
|
|
341
|
+
this.log("warn", message, data);
|
|
342
|
+
}
|
|
343
|
+
error(message, data) {
|
|
344
|
+
this.log("error", message, data);
|
|
345
|
+
}
|
|
346
|
+
log(level, message, data) {
|
|
347
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
348
|
+
const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
|
|
349
|
+
const merged = { ...this.context, ...data };
|
|
350
|
+
if (Object.keys(merged).length > 0) {
|
|
351
|
+
parts.push(JSON.stringify(merged));
|
|
352
|
+
}
|
|
353
|
+
this.sink.write(level, parts.join(" "));
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var logger = new Logger();
|
|
357
|
+
|
|
358
|
+
// src/providers/openai-compatible/index.ts
|
|
359
|
+
var OpenAICompatibleProvider = class {
|
|
360
|
+
config;
|
|
361
|
+
log;
|
|
362
|
+
constructor(config, log = logger) {
|
|
363
|
+
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
364
|
+
this.log = log.child({ provider: config.name });
|
|
365
|
+
}
|
|
366
|
+
async listModels() {
|
|
367
|
+
const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
|
|
368
|
+
headers: authHeaders(this.config)
|
|
369
|
+
});
|
|
370
|
+
if (status === 401 || status === 403) {
|
|
371
|
+
throw authFailed(this.config, status);
|
|
372
|
+
}
|
|
373
|
+
if (status !== 200) {
|
|
374
|
+
throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
|
|
375
|
+
}
|
|
376
|
+
const entries = extractModelEntries(body, this.config);
|
|
377
|
+
const models = [];
|
|
378
|
+
let skipped = 0;
|
|
379
|
+
for (const entry2 of entries) {
|
|
380
|
+
if (typeof entry2.id !== "string" || entry2.id.length === 0) {
|
|
381
|
+
skipped += 1;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
models.push({
|
|
385
|
+
provider: this.config.name,
|
|
386
|
+
id: entry2.id,
|
|
387
|
+
displayName: entry2.id,
|
|
388
|
+
status: "available",
|
|
389
|
+
metadata: {
|
|
390
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
391
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (skipped > 0) {
|
|
396
|
+
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
397
|
+
}
|
|
398
|
+
return models;
|
|
399
|
+
}
|
|
400
|
+
async healthCheck() {
|
|
401
|
+
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
402
|
+
try {
|
|
403
|
+
const started = Date.now();
|
|
404
|
+
const models = await this.listModels();
|
|
405
|
+
return {
|
|
406
|
+
name: this.config.name,
|
|
407
|
+
type: this.config.type,
|
|
408
|
+
baseUrl: this.config.baseUrl,
|
|
409
|
+
health: "healthy",
|
|
410
|
+
connected: true,
|
|
411
|
+
modelCount: models.length,
|
|
412
|
+
lastCheckedAt,
|
|
413
|
+
message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
name: this.config.name,
|
|
418
|
+
type: this.config.type,
|
|
419
|
+
baseUrl: this.config.baseUrl,
|
|
420
|
+
health: inferHealth(error),
|
|
421
|
+
connected: false,
|
|
422
|
+
modelCount: 0,
|
|
423
|
+
lastCheckedAt,
|
|
424
|
+
message: error instanceof Error ? error.message : String(error)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async chat(request) {
|
|
429
|
+
const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: authHeaders(this.config),
|
|
432
|
+
body: {
|
|
433
|
+
model: request.model,
|
|
434
|
+
messages: request.messages,
|
|
435
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
436
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
437
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
if (status === 401 || status === 403) throw authFailed(this.config, status);
|
|
441
|
+
if (status === 404) {
|
|
442
|
+
throw new ProviderError(
|
|
443
|
+
"MODEL_NOT_FOUND",
|
|
444
|
+
`Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
|
|
445
|
+
{ details: { status } }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (status !== 200) {
|
|
449
|
+
throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
|
|
450
|
+
}
|
|
451
|
+
const payload = body;
|
|
452
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
453
|
+
const first = choices[0];
|
|
454
|
+
if (!first || typeof first.message?.content !== "string") {
|
|
455
|
+
throw unavailable(this.config, "Chat completion response had no message content.", status);
|
|
456
|
+
}
|
|
457
|
+
const usage = payload.usage ?? {};
|
|
458
|
+
return {
|
|
459
|
+
model: typeof payload.model === "string" ? payload.model : request.model,
|
|
460
|
+
content: first.message.content,
|
|
461
|
+
finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
|
|
462
|
+
usage: {
|
|
463
|
+
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
|
|
464
|
+
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
|
|
465
|
+
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
|
|
466
|
+
},
|
|
467
|
+
raw: body
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
function normalizeBaseUrl(baseUrl) {
|
|
472
|
+
return baseUrl.replace(/\/+$/, "");
|
|
473
|
+
}
|
|
474
|
+
function extractModelEntries(body, config) {
|
|
475
|
+
if (Array.isArray(body)) return body;
|
|
476
|
+
if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
|
|
477
|
+
return body.data;
|
|
478
|
+
}
|
|
479
|
+
throw new ProviderError(
|
|
480
|
+
"PROVIDER_UNAVAILABLE",
|
|
481
|
+
`Provider "${config.name}" returned an unrecognized model list shape.`,
|
|
482
|
+
{ details: { endpoint: "models" } }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function authFailed(config, status) {
|
|
486
|
+
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
487
|
+
return new ProviderError(
|
|
488
|
+
"PROVIDER_AUTH_FAILED",
|
|
489
|
+
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
490
|
+
{ details: { status } }
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function unavailable(config, message, status) {
|
|
494
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
495
|
+
details: { status }
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function inferHealth(error) {
|
|
499
|
+
const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
|
|
500
|
+
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/providers/factory.ts
|
|
504
|
+
var OPENAI_COMPATIBLE_TYPES = [
|
|
505
|
+
"openai-compatible",
|
|
506
|
+
"openrouter",
|
|
507
|
+
"local",
|
|
508
|
+
"custom"
|
|
509
|
+
];
|
|
510
|
+
function createProvider(config) {
|
|
511
|
+
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
512
|
+
return new OpenAICompatibleProvider(config);
|
|
513
|
+
}
|
|
514
|
+
if (config.type === "omnihilbras") {
|
|
515
|
+
throw new OmniNodeError(
|
|
516
|
+
"NOT_IMPLEMENTED",
|
|
517
|
+
`Provider type "omnihilbras" is planned for Phase 2 (OmniHilbras Integration). If your OmniHilbras endpoint is OpenAI-compatible, use type "openai-compatible" for now.`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/cli/commands/shared.ts
|
|
524
|
+
function notImplemented(command, when) {
|
|
525
|
+
return new OmniNodeError(
|
|
526
|
+
"NOT_IMPLEMENTED",
|
|
527
|
+
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/cli/commands/providers.ts
|
|
532
|
+
function registerProviderCommands(program) {
|
|
533
|
+
const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
|
|
534
|
+
provider.command("list").description("List configured providers.").action(async () => {
|
|
535
|
+
const config = loadConfig();
|
|
536
|
+
const providers = config.project.providers;
|
|
537
|
+
if (providers.length === 0) {
|
|
538
|
+
console.log(
|
|
539
|
+
"No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
|
|
540
|
+
);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
for (const provider2 of providers) {
|
|
544
|
+
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
545
|
+
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
546
|
+
console.log(
|
|
547
|
+
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
provider.command("add").description(
|
|
552
|
+
"Add a provider to omninode.yaml. OpenAI-compatible gateways (including OpenRouter and local runtimes) are supported today; OmniHilbras gets its own adapter in Phase 2."
|
|
553
|
+
).requiredOption("--name <name>", "Unique provider name used in configuration and the model registry.").requiredOption("--base-url <url>", "Provider base URL, e.g. https://example.com/v1").option(
|
|
554
|
+
"--type <type>",
|
|
555
|
+
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
556
|
+
"openai-compatible"
|
|
557
|
+
).option(
|
|
558
|
+
"--api-key-env-var <envVar>",
|
|
559
|
+
"Environment variable holding the API key. Keys are never written to the config file."
|
|
560
|
+
).action(async (options) => addProvider(options));
|
|
561
|
+
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").action(async (name) => testProvider(name));
|
|
562
|
+
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
563
|
+
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async function addProvider(options) {
|
|
567
|
+
const configPath = findConfigFile();
|
|
568
|
+
if (!configPath) {
|
|
569
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
570
|
+
}
|
|
571
|
+
const candidate = providerConfigSchema.safeParse({
|
|
572
|
+
name: options.name,
|
|
573
|
+
type: options.type,
|
|
574
|
+
base_url: options.baseUrl,
|
|
575
|
+
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
576
|
+
});
|
|
577
|
+
if (!candidate.success) {
|
|
578
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
579
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
580
|
+
}
|
|
581
|
+
const newProvider = candidate.data;
|
|
582
|
+
const existing = loadConfig();
|
|
583
|
+
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
584
|
+
throw new OmniNodeError(
|
|
585
|
+
"CLI_USAGE",
|
|
586
|
+
`Provider "${newProvider.name}" already exists in ${configPath}.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const doc = parseDocument(readFileSync2(configPath, "utf8"));
|
|
590
|
+
if (doc.getIn(["project", "providers"]) === void 0) {
|
|
591
|
+
doc.setIn(["project", "providers"], [newProvider]);
|
|
592
|
+
} else {
|
|
593
|
+
doc.addIn(["project", "providers"], newProvider);
|
|
594
|
+
}
|
|
595
|
+
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
596
|
+
if (!validated.success) {
|
|
597
|
+
const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
598
|
+
throw new ConfigError(
|
|
599
|
+
"CONFIG_INVALID",
|
|
600
|
+
`Editing ${configPath} would produce an invalid configuration: ${issues}`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
writeFileSync2(configPath, doc.toString(), "utf8");
|
|
604
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
|
|
605
|
+
if (newProvider.api_key_env_var) {
|
|
606
|
+
console.log(
|
|
607
|
+
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
608
|
+
);
|
|
609
|
+
} else {
|
|
610
|
+
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function testProvider(name) {
|
|
614
|
+
const config = loadConfig();
|
|
615
|
+
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
616
|
+
if (!providerConfig) {
|
|
617
|
+
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
618
|
+
throw new ProviderError(
|
|
619
|
+
"PROVIDER_NOT_FOUND",
|
|
620
|
+
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const provider = createProvider(providerConfig);
|
|
624
|
+
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
625
|
+
const health = await provider.healthCheck();
|
|
626
|
+
console.log(` health: ${health.health}`);
|
|
627
|
+
console.log(` connected: ${health.connected}`);
|
|
628
|
+
console.log(` models: ${health.modelCount}`);
|
|
629
|
+
if (health.message) console.log(` detail: ${health.message}`);
|
|
630
|
+
if (!health.connected) process.exitCode = 1;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/registry/index.ts
|
|
634
|
+
function registryKey(provider, id) {
|
|
635
|
+
return `${provider}:${id}`;
|
|
636
|
+
}
|
|
637
|
+
var ModelRegistry = class {
|
|
638
|
+
models = /* @__PURE__ */ new Map();
|
|
639
|
+
register(model) {
|
|
640
|
+
this.models.set(registryKey(model.provider, model.id), model);
|
|
641
|
+
}
|
|
642
|
+
get(provider, id) {
|
|
643
|
+
return this.models.get(registryKey(provider, id));
|
|
644
|
+
}
|
|
645
|
+
list(filter = {}) {
|
|
646
|
+
return [...this.models.values()].filter((model) => filter.provider ? model.provider === filter.provider : true).filter(
|
|
647
|
+
(model) => filter.capability ? model.capabilities?.[filter.capability] === true : true
|
|
648
|
+
).sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
|
|
649
|
+
}
|
|
650
|
+
/** Availability validation (§27 Phase 1): present in the registry and not retired. */
|
|
651
|
+
isAvailable(provider, id) {
|
|
652
|
+
const model = this.get(provider, id);
|
|
653
|
+
return model !== void 0 && (model.status ?? "available") === "available";
|
|
654
|
+
}
|
|
655
|
+
/** Discover models from one provider and register them. */
|
|
656
|
+
async discoverFrom(provider) {
|
|
657
|
+
const discovered = await provider.listModels();
|
|
658
|
+
for (const model of discovered) this.register(model);
|
|
659
|
+
return discovered;
|
|
660
|
+
}
|
|
661
|
+
/** Discover from many providers; a failing provider does not abort the rest. */
|
|
662
|
+
async discoverAll(providers) {
|
|
663
|
+
return Promise.all(
|
|
664
|
+
providers.map(async (provider) => {
|
|
665
|
+
try {
|
|
666
|
+
return {
|
|
667
|
+
provider: provider.config.name,
|
|
668
|
+
models: await this.discoverFrom(provider)
|
|
669
|
+
};
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return {
|
|
672
|
+
provider: provider.config.name,
|
|
673
|
+
models: [],
|
|
674
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/cli/commands/models.ts
|
|
683
|
+
var CAPABILITIES = [
|
|
684
|
+
"chat",
|
|
685
|
+
"coding",
|
|
686
|
+
"tools",
|
|
687
|
+
"vision",
|
|
688
|
+
"embeddings"
|
|
689
|
+
];
|
|
690
|
+
function registerModelsCommand(program) {
|
|
691
|
+
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) => {
|
|
692
|
+
const capability = options.capability;
|
|
693
|
+
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
694
|
+
throw new OmniNodeError(
|
|
695
|
+
"CLI_USAGE",
|
|
696
|
+
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const config = loadConfig();
|
|
700
|
+
const selected = config.project.providers.filter(
|
|
701
|
+
(p) => providerName ? p.name === providerName : true
|
|
702
|
+
);
|
|
703
|
+
if (selected.length === 0) {
|
|
704
|
+
throw new OmniNodeError(
|
|
705
|
+
"PROVIDER_NOT_FOUND",
|
|
706
|
+
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const registry = new ModelRegistry();
|
|
710
|
+
for (const providerConfig of selected) {
|
|
711
|
+
try {
|
|
712
|
+
const provider = createProvider(providerConfig);
|
|
713
|
+
const discovered = await registry.discoverFrom(provider);
|
|
714
|
+
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
717
|
+
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const models = registry.list({
|
|
721
|
+
...providerName !== void 0 ? { provider: providerName } : {},
|
|
722
|
+
...capability !== void 0 ? { capability } : {}
|
|
723
|
+
});
|
|
724
|
+
if (models.length === 0) {
|
|
725
|
+
console.log("No models found.");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
console.log("");
|
|
729
|
+
for (const model of models) {
|
|
730
|
+
console.log(`${model.provider}:${model.id}`);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/cli/commands/agents.ts
|
|
736
|
+
function registerAgentCommands(program) {
|
|
737
|
+
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
738
|
+
agent.command("list").description("List configured agents.").action(async () => {
|
|
739
|
+
const config = loadConfig();
|
|
740
|
+
const agents = config.project.agents;
|
|
741
|
+
if (agents.length === 0) {
|
|
742
|
+
console.log("No agents configured. Add one under `project.agents` in omninode.yaml.");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
for (const agent2 of agents) {
|
|
746
|
+
const command = agent2.command ?? "(no command)";
|
|
747
|
+
console.log(
|
|
748
|
+
`${agent2.name} integration=${agent2.integration} command=${command}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
agent.command("add").description("Register an agent (planned for Phase 3 \u2014 CLI Agent System).").action(() => {
|
|
753
|
+
throw notImplemented("omninode agent add", "Phase 3 (CLI Agent System)");
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/cli/commands/roles.ts
|
|
758
|
+
function registerRoleCommands(program) {
|
|
759
|
+
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
760
|
+
role.command("list").description("List configured roles.").action(async () => {
|
|
761
|
+
const config = loadConfig();
|
|
762
|
+
const roles = config.project.roles;
|
|
763
|
+
if (roles.length === 0) {
|
|
764
|
+
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
for (const role2 of roles) {
|
|
768
|
+
console.log(`${role2.id} ${role2.name}`);
|
|
769
|
+
for (const responsibility of role2.responsibilities) {
|
|
770
|
+
console.log(` - ${responsibility}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/cli/commands/tasks.ts
|
|
777
|
+
function registerTaskCommands(program) {
|
|
778
|
+
const task = program.command("task").description("Create, run and inspect tasks (Phase 4+).");
|
|
779
|
+
task.command("create").description("Create a task (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
|
|
780
|
+
throw notImplemented("omninode task create", "Phase 4 (Roles & Tasks)");
|
|
781
|
+
});
|
|
782
|
+
task.command("run <objective>").description("Run a task or pipeline (planned for Phase 5 \u2014 Pipeline Engine).").action(() => {
|
|
783
|
+
throw notImplemented("omninode task run", "Phase 5 (Pipeline Engine)");
|
|
784
|
+
});
|
|
785
|
+
task.command("status <task-id>").description("Show task status (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
|
|
786
|
+
throw notImplemented("omninode task status", "Phase 4 (Roles & Tasks)");
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/cli/commands/reports.ts
|
|
791
|
+
function registerReportCommands(program) {
|
|
792
|
+
const report = program.command("report").description("Inspect AI reports (Phase 6+).");
|
|
793
|
+
report.command("list").description("List collected reports (planned for Phase 6 \u2014 Multi-AI Report System).").action(() => {
|
|
794
|
+
throw notImplemented("omninode report list", "Phase 6 (Multi-AI Report System)");
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/cli/index.ts
|
|
799
|
+
function createProgram() {
|
|
800
|
+
const program = new Command();
|
|
801
|
+
program.name("omninode").description(
|
|
802
|
+
"Provider-agnostic multi-AI orchestration platform: coordinate CLI agents, AI providers, memory, roles, reports and planning in one execution system."
|
|
803
|
+
).version(OMNINODE_VERSION, "-v, --version", "Print the OmniNode version.");
|
|
804
|
+
registerInitCommand(program);
|
|
805
|
+
registerProjectCommands(program);
|
|
806
|
+
registerProviderCommands(program);
|
|
807
|
+
registerAgentCommands(program);
|
|
808
|
+
registerRoleCommands(program);
|
|
809
|
+
registerTaskCommands(program);
|
|
810
|
+
registerReportCommands(program);
|
|
811
|
+
registerModelsCommand(program);
|
|
812
|
+
return program;
|
|
813
|
+
}
|
|
814
|
+
async function run(argv) {
|
|
815
|
+
const program = createProgram();
|
|
816
|
+
try {
|
|
817
|
+
await program.parseAsync(argv);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
handleError(error);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function handleError(error) {
|
|
823
|
+
if (isOmniNodeError(error)) {
|
|
824
|
+
console.error(`error: [${error.code}] ${error.message}`);
|
|
825
|
+
if (error.details !== void 0) {
|
|
826
|
+
console.error(JSON.stringify(error.details, null, 2));
|
|
827
|
+
}
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
var entry = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : "";
|
|
834
|
+
if (import.meta.url === entry) {
|
|
835
|
+
run(process.argv).catch((error) => handleError(error));
|
|
836
|
+
}
|
|
837
|
+
export {
|
|
838
|
+
createProgram,
|
|
839
|
+
handleError,
|
|
840
|
+
run
|
|
841
|
+
};
|
|
842
|
+
//# sourceMappingURL=index.js.map
|