@hilbras/omninode 2.0.0-alpha.13 → 2.0.0-alpha.15
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/README.md +16 -2
- package/dist/cli/chunk-6Z6O63SS.js +708 -0
- package/dist/cli/chunk-6Z6O63SS.js.map +1 -0
- package/dist/cli/index.js +1397 -1589
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/providers-OYXBAQTX.js +36 -0
- package/dist/cli/providers-OYXBAQTX.js.map +1 -0
- package/dist/index.d.ts +80 -31
- package/dist/index.js +149 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentError,
|
|
4
|
+
ConfigError,
|
|
5
|
+
MemoryError,
|
|
6
|
+
OmniHilbrasProvider,
|
|
7
|
+
OmniNodeError,
|
|
8
|
+
PipelineError,
|
|
9
|
+
PlannerError,
|
|
10
|
+
ProviderError,
|
|
11
|
+
TaskError,
|
|
12
|
+
configureLogger,
|
|
13
|
+
createProvider,
|
|
14
|
+
isOmniNodeError,
|
|
15
|
+
logger,
|
|
16
|
+
requestJson,
|
|
17
|
+
resolveApiKey
|
|
18
|
+
} from "./chunk-6Z6O63SS.js";
|
|
2
19
|
|
|
3
20
|
// src/cli/index.ts
|
|
4
21
|
import { pathToFileURL } from "url";
|
|
@@ -6,61 +23,50 @@ import { realpathSync } from "fs";
|
|
|
6
23
|
import { Command } from "commander";
|
|
7
24
|
|
|
8
25
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
26
|
+
var OMNINODE_VERSION = "2.0.0-alpha.15";
|
|
10
27
|
|
|
11
|
-
// src/
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
var AgentError = class extends OmniNodeError {
|
|
33
|
-
constructor(code, message, options = {}) {
|
|
34
|
-
super(code, message, options);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
var TaskError = class extends OmniNodeError {
|
|
38
|
-
constructor(code, message, options = {}) {
|
|
39
|
-
super(code, message, options);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var PipelineError = class extends OmniNodeError {
|
|
43
|
-
constructor(code, message, options = {}) {
|
|
44
|
-
super(code, message, options);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
var PlannerError = class extends OmniNodeError {
|
|
48
|
-
constructor(code, message, options = {}) {
|
|
49
|
-
super(code, message, options);
|
|
50
|
-
}
|
|
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
|
|
51
40
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
67
|
}
|
|
56
|
-
};
|
|
57
|
-
function isOmniNodeError(value) {
|
|
58
|
-
return value instanceof OmniNodeError;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
// src/cli/commands/init.ts
|
|
62
|
-
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
63
|
-
|
|
64
70
|
// src/config/schema.ts
|
|
65
71
|
import { z } from "zod";
|
|
66
72
|
var providerConfigSchema = z.object({
|
|
@@ -142,7 +148,10 @@ var projectConfigSchema = z.object({
|
|
|
142
148
|
}).strict();
|
|
143
149
|
var appConfigSchema = z.object({
|
|
144
150
|
project: projectConfigSchema,
|
|
145
|
-
logging: z.object({
|
|
151
|
+
logging: z.object({
|
|
152
|
+
level: z.enum(["debug", "info", "warn", "error"]),
|
|
153
|
+
format: z.enum(["text", "json"]).optional()
|
|
154
|
+
}).strict().optional()
|
|
146
155
|
}).strict();
|
|
147
156
|
|
|
148
157
|
// src/config/secrets.ts
|
|
@@ -364,6 +373,7 @@ function defaultProjectConfigYaml(projectName = "My Project") {
|
|
|
364
373
|
}
|
|
365
374
|
|
|
366
375
|
// src/cli/commands/init.ts
|
|
376
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
367
377
|
async function runInit(options) {
|
|
368
378
|
const target = `${process.cwd()}/omninode.yaml`;
|
|
369
379
|
if (!options.force && existsSync2(target)) {
|
|
@@ -389,577 +399,9 @@ function registerProjectCommands(program) {
|
|
|
389
399
|
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));
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
// src/providers
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const value = env[config.apiKeyEnvVar];
|
|
396
|
-
if (!value) {
|
|
397
|
-
throw new ProviderError(
|
|
398
|
-
"PROVIDER_AUTH_FAILED",
|
|
399
|
-
`Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return value;
|
|
403
|
-
}
|
|
404
|
-
function authHeaders(config, env = process.env) {
|
|
405
|
-
const key = resolveApiKey(config, env);
|
|
406
|
-
const base = { ...config.headers ?? {} };
|
|
407
|
-
return key ? { ...base, authorization: `Bearer ${key}` } : base;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/providers/http.ts
|
|
411
|
-
var HttpResponseError = class extends Error {
|
|
412
|
-
constructor(message, status, cause) {
|
|
413
|
-
super(message, { cause });
|
|
414
|
-
this.status = status;
|
|
415
|
-
this.name = "HttpResponseError";
|
|
416
|
-
}
|
|
417
|
-
status;
|
|
418
|
-
};
|
|
419
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
420
|
-
async function requestJson(url, options = {}) {
|
|
421
|
-
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
422
|
-
const response = await fetch(url, {
|
|
423
|
-
method,
|
|
424
|
-
headers: {
|
|
425
|
-
accept: "application/json",
|
|
426
|
-
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
427
|
-
...headers
|
|
428
|
-
},
|
|
429
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
430
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
431
|
-
});
|
|
432
|
-
let parsed;
|
|
433
|
-
try {
|
|
434
|
-
parsed = await response.json();
|
|
435
|
-
} catch (error) {
|
|
436
|
-
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
437
|
-
}
|
|
438
|
-
return { status: response.status, body: parsed };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// src/logger/index.ts
|
|
442
|
-
var LEVEL_ORDER = {
|
|
443
|
-
debug: 10,
|
|
444
|
-
info: 20,
|
|
445
|
-
warn: 30,
|
|
446
|
-
error: 40
|
|
447
|
-
};
|
|
448
|
-
var ConsoleLogSink = class {
|
|
449
|
-
constructor(stream = process.stderr) {
|
|
450
|
-
this.stream = stream;
|
|
451
|
-
}
|
|
452
|
-
stream;
|
|
453
|
-
write(_level, line) {
|
|
454
|
-
this.stream.write(line + "\n");
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
var Logger = class _Logger {
|
|
458
|
-
level;
|
|
459
|
-
sink;
|
|
460
|
-
context;
|
|
461
|
-
constructor(options = {}) {
|
|
462
|
-
this.level = options.level ?? "info";
|
|
463
|
-
this.sink = options.sink ?? new ConsoleLogSink();
|
|
464
|
-
this.context = options.context ?? {};
|
|
465
|
-
}
|
|
466
|
-
child(context) {
|
|
467
|
-
return new _Logger({
|
|
468
|
-
level: this.level,
|
|
469
|
-
sink: this.sink,
|
|
470
|
-
context: { ...this.context, ...context }
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
debug(message, data) {
|
|
474
|
-
this.log("debug", message, data);
|
|
475
|
-
}
|
|
476
|
-
info(message, data) {
|
|
477
|
-
this.log("info", message, data);
|
|
478
|
-
}
|
|
479
|
-
warn(message, data) {
|
|
480
|
-
this.log("warn", message, data);
|
|
481
|
-
}
|
|
482
|
-
error(message, data) {
|
|
483
|
-
this.log("error", message, data);
|
|
484
|
-
}
|
|
485
|
-
log(level, message, data) {
|
|
486
|
-
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
487
|
-
const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
|
|
488
|
-
const merged = { ...this.context, ...data };
|
|
489
|
-
if (Object.keys(merged).length > 0) {
|
|
490
|
-
parts.push(JSON.stringify(merged));
|
|
491
|
-
}
|
|
492
|
-
this.sink.write(level, parts.join(" "));
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
var logger = new Logger();
|
|
496
|
-
|
|
497
|
-
// src/providers/errors.ts
|
|
498
|
-
function kindToCode(kind) {
|
|
499
|
-
switch (kind) {
|
|
500
|
-
case "AUTHENTICATION_ERROR":
|
|
501
|
-
return "PROVIDER_AUTH_FAILED";
|
|
502
|
-
case "MODEL_NOT_FOUND":
|
|
503
|
-
return "MODEL_NOT_FOUND";
|
|
504
|
-
default:
|
|
505
|
-
return "PROVIDER_UNAVAILABLE";
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
function isRetryable(kind) {
|
|
509
|
-
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
510
|
-
}
|
|
511
|
-
function kindFromStatus(status) {
|
|
512
|
-
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
513
|
-
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
514
|
-
if (status === 404) return "MODEL_NOT_FOUND";
|
|
515
|
-
if (status === 408 || status === 504) return "TIMEOUT";
|
|
516
|
-
if (status >= 500) return "SERVER_ERROR";
|
|
517
|
-
if (status >= 400) return "INVALID_REQUEST";
|
|
518
|
-
return "UNKNOWN_ERROR";
|
|
519
|
-
}
|
|
520
|
-
function extractMessage(body) {
|
|
521
|
-
if (body === null || typeof body !== "object") return void 0;
|
|
522
|
-
const root = body;
|
|
523
|
-
const error = root.error ?? root;
|
|
524
|
-
const message = error.message ?? error.detail ?? root.message;
|
|
525
|
-
return typeof message === "string" ? message : void 0;
|
|
526
|
-
}
|
|
527
|
-
function parseRetryAfter(value, now = Date.now()) {
|
|
528
|
-
if (!value) return void 0;
|
|
529
|
-
const seconds = Number(value);
|
|
530
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
531
|
-
const date = Date.parse(value);
|
|
532
|
-
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
533
|
-
}
|
|
534
|
-
function isTimeoutError(error) {
|
|
535
|
-
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
536
|
-
}
|
|
537
|
-
function fallbackMessage(kind, context, status) {
|
|
538
|
-
const { provider, operation } = context;
|
|
539
|
-
switch (kind) {
|
|
540
|
-
case "AUTHENTICATION_ERROR":
|
|
541
|
-
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
542
|
-
case "RATE_LIMIT_ERROR":
|
|
543
|
-
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
544
|
-
case "MODEL_NOT_FOUND":
|
|
545
|
-
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
546
|
-
case "TIMEOUT":
|
|
547
|
-
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
548
|
-
case "SERVER_ERROR":
|
|
549
|
-
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
550
|
-
case "INVALID_REQUEST":
|
|
551
|
-
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
552
|
-
default:
|
|
553
|
-
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function providerHttpError(context) {
|
|
557
|
-
const status = context.status ?? 0;
|
|
558
|
-
const kind = kindFromStatus(status);
|
|
559
|
-
const detail = extractMessage(context.body);
|
|
560
|
-
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
561
|
-
return new ProviderError(
|
|
562
|
-
context.code ?? kindToCode(kind),
|
|
563
|
-
detail ?? fallbackMessage(kind, context, status),
|
|
564
|
-
{
|
|
565
|
-
cause: context.cause,
|
|
566
|
-
details: {
|
|
567
|
-
kind,
|
|
568
|
-
provider: context.provider,
|
|
569
|
-
operation: context.operation,
|
|
570
|
-
status,
|
|
571
|
-
retryable: isRetryable(kind),
|
|
572
|
-
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
573
|
-
...detail ? { providerMessage: detail } : {}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
function providerTransportError(context) {
|
|
579
|
-
const cause = context.cause;
|
|
580
|
-
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
581
|
-
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
582
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
583
|
-
cause: context.cause,
|
|
584
|
-
details: {
|
|
585
|
-
kind,
|
|
586
|
-
provider: context.provider,
|
|
587
|
-
operation: context.operation,
|
|
588
|
-
retryable: isRetryable(kind)
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// src/providers/openai-compatible/index.ts
|
|
594
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
595
|
-
var OpenAICompatibleProvider = class {
|
|
596
|
-
config;
|
|
597
|
-
providerId;
|
|
598
|
-
authentication;
|
|
599
|
-
log;
|
|
600
|
-
/** Discovered models, so getModel() answers without another round-trip. */
|
|
601
|
-
discovered;
|
|
602
|
-
env;
|
|
603
|
-
constructor(config, log = logger, env = process.env) {
|
|
604
|
-
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
605
|
-
this.env = env;
|
|
606
|
-
this.providerId = config.providerId ?? config.name;
|
|
607
|
-
this.authentication = {
|
|
608
|
-
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
609
|
-
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
610
|
-
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
611
|
-
};
|
|
612
|
-
this.log = log.child({ provider: config.name });
|
|
613
|
-
}
|
|
614
|
-
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
615
|
-
get requestTimeoutMs() {
|
|
616
|
-
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
617
|
-
}
|
|
618
|
-
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
619
|
-
async connect() {
|
|
620
|
-
authHeaders(this.config, this.env);
|
|
621
|
-
const models = await this.listModels();
|
|
622
|
-
this.discovered = models;
|
|
623
|
-
return models;
|
|
624
|
-
}
|
|
625
|
-
async getModel(modelId) {
|
|
626
|
-
const models = this.discovered ?? await this.connect();
|
|
627
|
-
return models.find((m) => m.id === modelId);
|
|
628
|
-
}
|
|
629
|
-
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
630
|
-
async rawModelsResponse() {
|
|
631
|
-
const response = await this.modelsRequest();
|
|
632
|
-
return response.body;
|
|
633
|
-
}
|
|
634
|
-
async modelsRequest() {
|
|
635
|
-
let response;
|
|
636
|
-
try {
|
|
637
|
-
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
638
|
-
headers: authHeaders(this.config, this.env),
|
|
639
|
-
timeoutMs: this.requestTimeoutMs
|
|
640
|
-
});
|
|
641
|
-
} catch (error) {
|
|
642
|
-
if (error instanceof ProviderError) throw error;
|
|
643
|
-
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
644
|
-
}
|
|
645
|
-
const { status, body } = response;
|
|
646
|
-
if (status !== 200) {
|
|
647
|
-
throw providerHttpError({
|
|
648
|
-
provider: this.config.name,
|
|
649
|
-
operation: "listModels",
|
|
650
|
-
status,
|
|
651
|
-
body
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
return response;
|
|
655
|
-
}
|
|
656
|
-
async listModels() {
|
|
657
|
-
const body = await this.rawModelsResponse();
|
|
658
|
-
const entries = extractModelEntries(body, this.config);
|
|
659
|
-
const models = [];
|
|
660
|
-
let skipped = 0;
|
|
661
|
-
for (const entry2 of entries) {
|
|
662
|
-
if (typeof entry2.id !== "string" || entry2.id.length === 0) {
|
|
663
|
-
skipped += 1;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
models.push(this.toModelInfo(entry2));
|
|
667
|
-
}
|
|
668
|
-
if (skipped > 0) {
|
|
669
|
-
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
670
|
-
}
|
|
671
|
-
return models;
|
|
672
|
-
}
|
|
673
|
-
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
674
|
-
toModelInfo(entry2) {
|
|
675
|
-
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
676
|
-
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
677
|
-
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
678
|
-
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
679
|
-
return {
|
|
680
|
-
provider: this.config.name,
|
|
681
|
-
id: entry2.id,
|
|
682
|
-
displayName: entry2.id,
|
|
683
|
-
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
684
|
-
status: "available",
|
|
685
|
-
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
686
|
-
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
687
|
-
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
688
|
-
...parameters !== void 0 ? {
|
|
689
|
-
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
690
|
-
supportsStructuredOutput: parameters.includes("response_format"),
|
|
691
|
-
supportsStreaming: parameters.includes("stream")
|
|
692
|
-
} : {},
|
|
693
|
-
metadata: {
|
|
694
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
695
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
696
|
-
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
/** Streaming where the gateway supports it (§11). */
|
|
701
|
-
stream(request, onChunk) {
|
|
702
|
-
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
703
|
-
}
|
|
704
|
-
async healthCheck() {
|
|
705
|
-
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
706
|
-
try {
|
|
707
|
-
const started = Date.now();
|
|
708
|
-
const models = await this.listModels();
|
|
709
|
-
return {
|
|
710
|
-
name: this.config.name,
|
|
711
|
-
type: this.config.type,
|
|
712
|
-
baseUrl: this.config.baseUrl,
|
|
713
|
-
health: "healthy",
|
|
714
|
-
connected: true,
|
|
715
|
-
modelCount: models.length,
|
|
716
|
-
lastCheckedAt,
|
|
717
|
-
message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
|
|
718
|
-
};
|
|
719
|
-
} catch (error) {
|
|
720
|
-
return {
|
|
721
|
-
name: this.config.name,
|
|
722
|
-
type: this.config.type,
|
|
723
|
-
baseUrl: this.config.baseUrl,
|
|
724
|
-
health: inferHealth(error),
|
|
725
|
-
connected: false,
|
|
726
|
-
modelCount: 0,
|
|
727
|
-
lastCheckedAt,
|
|
728
|
-
message: error instanceof Error ? error.message : String(error)
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
async chat(request) {
|
|
733
|
-
let response;
|
|
734
|
-
try {
|
|
735
|
-
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
736
|
-
method: "POST",
|
|
737
|
-
headers: authHeaders(this.config, this.env),
|
|
738
|
-
timeoutMs: this.requestTimeoutMs,
|
|
739
|
-
body: {
|
|
740
|
-
model: request.model,
|
|
741
|
-
messages: request.messages,
|
|
742
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
743
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
744
|
-
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
} catch (error) {
|
|
748
|
-
if (error instanceof ProviderError) throw error;
|
|
749
|
-
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
750
|
-
}
|
|
751
|
-
const { status, body } = response;
|
|
752
|
-
if (status !== 200) {
|
|
753
|
-
throw providerHttpError({
|
|
754
|
-
provider: this.config.name,
|
|
755
|
-
operation: "chat",
|
|
756
|
-
status,
|
|
757
|
-
body,
|
|
758
|
-
...status === 404 ? {
|
|
759
|
-
code: "MODEL_NOT_FOUND"
|
|
760
|
-
} : {}
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
const payload = body;
|
|
764
|
-
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
765
|
-
const first = choices[0];
|
|
766
|
-
if (!first || typeof first.message?.content !== "string") {
|
|
767
|
-
throw unavailable(this.config, "Chat completion response had no message content.", status);
|
|
768
|
-
}
|
|
769
|
-
const usage = payload.usage ?? {};
|
|
770
|
-
return {
|
|
771
|
-
model: typeof payload.model === "string" ? payload.model : request.model,
|
|
772
|
-
content: first.message.content,
|
|
773
|
-
finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
|
|
774
|
-
usage: {
|
|
775
|
-
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
|
|
776
|
-
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
|
|
777
|
-
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
|
|
778
|
-
},
|
|
779
|
-
raw: body
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
784
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
785
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
786
|
-
let response;
|
|
787
|
-
try {
|
|
788
|
-
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
789
|
-
method: "POST",
|
|
790
|
-
headers: {
|
|
791
|
-
accept: "text/event-stream",
|
|
792
|
-
"content-type": "application/json",
|
|
793
|
-
...authHeaders(config, env)
|
|
794
|
-
},
|
|
795
|
-
body: JSON.stringify({
|
|
796
|
-
model: request.model,
|
|
797
|
-
messages: request.messages,
|
|
798
|
-
stream: true,
|
|
799
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
800
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
801
|
-
}),
|
|
802
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
803
|
-
});
|
|
804
|
-
} catch (error) {
|
|
805
|
-
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
806
|
-
}
|
|
807
|
-
if (!response.ok) {
|
|
808
|
-
const body = await response.json().catch(() => void 0);
|
|
809
|
-
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
810
|
-
}
|
|
811
|
-
if (!response.body) {
|
|
812
|
-
throw providerHttpError({
|
|
813
|
-
provider: config.name,
|
|
814
|
-
operation: "stream",
|
|
815
|
-
status: 500,
|
|
816
|
-
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
let content = "";
|
|
820
|
-
let model = request.model;
|
|
821
|
-
let finishReason;
|
|
822
|
-
const decoder = new TextDecoder();
|
|
823
|
-
let buffer = "";
|
|
824
|
-
for await (const piece of response.body) {
|
|
825
|
-
buffer += decoder.decode(piece, { stream: true });
|
|
826
|
-
let newline = buffer.indexOf("\n");
|
|
827
|
-
while (newline >= 0) {
|
|
828
|
-
const line = buffer.slice(0, newline).trim();
|
|
829
|
-
buffer = buffer.slice(newline + 1);
|
|
830
|
-
newline = buffer.indexOf("\n");
|
|
831
|
-
if (line.length === 0 || line.startsWith(":")) continue;
|
|
832
|
-
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
833
|
-
if (data === "[DONE]") continue;
|
|
834
|
-
let event;
|
|
835
|
-
try {
|
|
836
|
-
event = JSON.parse(data);
|
|
837
|
-
} catch {
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
if (typeof event.model === "string") model = event.model;
|
|
841
|
-
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
842
|
-
const first = choices[0];
|
|
843
|
-
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
844
|
-
if (delta.length > 0) content += delta;
|
|
845
|
-
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
846
|
-
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
850
|
-
}
|
|
851
|
-
function normalizeModalities(value) {
|
|
852
|
-
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
853
|
-
if (!list) return void 0;
|
|
854
|
-
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
855
|
-
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
856
|
-
return modalities.length > 0 ? modalities : void 0;
|
|
857
|
-
}
|
|
858
|
-
function normalizeBaseUrl(baseUrl) {
|
|
859
|
-
return baseUrl.replace(/\/+$/, "");
|
|
860
|
-
}
|
|
861
|
-
function extractModelEntries(body, config) {
|
|
862
|
-
if (Array.isArray(body)) return body;
|
|
863
|
-
if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
|
|
864
|
-
return body.data;
|
|
865
|
-
}
|
|
866
|
-
throw new ProviderError(
|
|
867
|
-
"PROVIDER_UNAVAILABLE",
|
|
868
|
-
`Provider "${config.name}" returned an unrecognized model list shape.`,
|
|
869
|
-
{ details: { endpoint: "models" } }
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
function unavailable(config, message, status) {
|
|
873
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
874
|
-
details: { status }
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
function inferHealth(error) {
|
|
878
|
-
const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
|
|
879
|
-
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// src/providers/omnihilbras/index.ts
|
|
883
|
-
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
884
|
-
gateway;
|
|
885
|
-
async listModels() {
|
|
886
|
-
const raw = await this.rawModelsResponse();
|
|
887
|
-
await this.extractGatewayMetadata(raw);
|
|
888
|
-
const models = await super.listModels();
|
|
889
|
-
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Run the full connect flow. Throws on authentication or transport failure
|
|
893
|
-
* (use healthCheck() for a non-throwing probe).
|
|
894
|
-
*/
|
|
895
|
-
/**
|
|
896
|
-
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
897
|
-
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
898
|
-
*/
|
|
899
|
-
gatewayMetadata() {
|
|
900
|
-
return this.gateway;
|
|
901
|
-
}
|
|
902
|
-
async extractGatewayMetadata(body) {
|
|
903
|
-
if (body !== null && typeof body === "object") {
|
|
904
|
-
const gateway = body.gateway;
|
|
905
|
-
if (gateway !== null && typeof gateway === "object") {
|
|
906
|
-
this.gateway = gateway;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
async connectAndRegister(registry) {
|
|
911
|
-
const apiKey = resolveApiKey(this.config);
|
|
912
|
-
const discovered = await this.listModels();
|
|
913
|
-
const seen = /* @__PURE__ */ new Set();
|
|
914
|
-
const models = [];
|
|
915
|
-
const duplicates = [];
|
|
916
|
-
for (const model of discovered) {
|
|
917
|
-
if (seen.has(model.id)) {
|
|
918
|
-
duplicates.push(model.id);
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
seen.add(model.id);
|
|
922
|
-
models.push({
|
|
923
|
-
...model,
|
|
924
|
-
provider: this.config.name,
|
|
925
|
-
status: "available",
|
|
926
|
-
metadata: { ...model.metadata, gateway: "omnihilbras" }
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
if (registry) {
|
|
930
|
-
for (const model of models) registry.register(model);
|
|
931
|
-
}
|
|
932
|
-
const message = `Authenticated${apiKey ? " with API key" : " (no API key configured)"}; validated ${models.length} unique model(s)` + (duplicates.length > 0 ? `, skipped ${duplicates.length} duplicate(s)` : "") + ".";
|
|
933
|
-
const status = {
|
|
934
|
-
name: this.config.name,
|
|
935
|
-
type: this.config.type,
|
|
936
|
-
baseUrl: this.config.baseUrl,
|
|
937
|
-
health: "healthy",
|
|
938
|
-
connected: true,
|
|
939
|
-
modelCount: models.length,
|
|
940
|
-
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
941
|
-
message
|
|
942
|
-
};
|
|
943
|
-
return { status, models, duplicates, registered: registry ? models.length : 0 };
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
// src/providers/factory.ts
|
|
948
|
-
var OPENAI_COMPATIBLE_TYPES = [
|
|
949
|
-
"openai-compatible",
|
|
950
|
-
"openrouter",
|
|
951
|
-
"local",
|
|
952
|
-
"custom"
|
|
953
|
-
];
|
|
954
|
-
function createProvider(config) {
|
|
955
|
-
if (config.type === "omnihilbras") {
|
|
956
|
-
return new OmniHilbrasProvider(config);
|
|
957
|
-
}
|
|
958
|
-
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
959
|
-
return new OpenAICompatibleProvider(config);
|
|
960
|
-
}
|
|
961
|
-
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
962
|
-
}
|
|
402
|
+
// src/cli/commands/providers.ts
|
|
403
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
404
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
963
405
|
|
|
964
406
|
// src/registry/index.ts
|
|
965
407
|
function registryKey(provider, id) {
|
|
@@ -1013,12 +455,6 @@ var ModelRegistry = class {
|
|
|
1013
455
|
// src/cli/commands/shared.ts
|
|
1014
456
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1015
457
|
import { parseDocument } from "yaml";
|
|
1016
|
-
function notImplemented(command, when) {
|
|
1017
|
-
return new OmniNodeError(
|
|
1018
|
-
"NOT_IMPLEMENTED",
|
|
1019
|
-
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
458
|
function truncate(text, max) {
|
|
1023
459
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1024
460
|
}
|
|
@@ -1045,183 +481,24 @@ function appendToConfigList(listKey, item) {
|
|
|
1045
481
|
return configPath;
|
|
1046
482
|
}
|
|
1047
483
|
|
|
1048
|
-
// src/
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
for (const provider2 of providers) {
|
|
1061
|
-
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
1062
|
-
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
1063
|
-
console.log(
|
|
1064
|
-
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
provider.command("add").description(
|
|
1069
|
-
"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."
|
|
1070
|
-
).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(
|
|
1071
|
-
"--type <type>",
|
|
1072
|
-
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
1073
|
-
"openai-compatible"
|
|
1074
|
-
).option(
|
|
1075
|
-
"--api-key-env-var <envVar>",
|
|
1076
|
-
"Environment variable holding the API key. Keys are never written to the config file."
|
|
1077
|
-
).action(async (options) => addProvider(options));
|
|
1078
|
-
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
1079
|
-
"--connect",
|
|
1080
|
-
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
1081
|
-
).action(async (name, options) => testProvider(name, options));
|
|
1082
|
-
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
1083
|
-
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
1084
|
-
});
|
|
484
|
+
// src/agents/process/index.ts
|
|
485
|
+
import { spawn } from "child_process";
|
|
486
|
+
|
|
487
|
+
// src/agent-protocol/version.ts
|
|
488
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
489
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
490
|
+
"omninode-agent-protocol/1",
|
|
491
|
+
PROTOCOL_V2
|
|
492
|
+
];
|
|
493
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
494
|
+
function isSupportedProtocol(protocol) {
|
|
495
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1085
496
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
const candidate = providerConfigSchema.safeParse({
|
|
1092
|
-
name: options.name,
|
|
1093
|
-
type: options.type,
|
|
1094
|
-
base_url: options.baseUrl,
|
|
1095
|
-
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
1096
|
-
});
|
|
1097
|
-
if (!candidate.success) {
|
|
1098
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1099
|
-
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
1100
|
-
}
|
|
1101
|
-
const newProvider = candidate.data;
|
|
1102
|
-
const existing = loadConfig();
|
|
1103
|
-
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
1104
|
-
throw new OmniNodeError(
|
|
1105
|
-
"CLI_USAGE",
|
|
1106
|
-
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
const writtenPath = appendToConfigList("providers", newProvider);
|
|
1110
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
1111
|
-
if (newProvider.api_key_env_var) {
|
|
1112
|
-
console.log(
|
|
1113
|
-
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
1114
|
-
);
|
|
1115
|
-
} else {
|
|
1116
|
-
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
async function testProvider(name, options = {}) {
|
|
1120
|
-
const config = loadConfig();
|
|
1121
|
-
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
1122
|
-
if (!providerConfig) {
|
|
1123
|
-
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
1124
|
-
throw new ProviderError(
|
|
1125
|
-
"PROVIDER_NOT_FOUND",
|
|
1126
|
-
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
1127
|
-
);
|
|
1128
|
-
}
|
|
1129
|
-
const provider = createProvider(providerConfig);
|
|
1130
|
-
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
1131
|
-
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
1132
|
-
const registry = new ModelRegistry();
|
|
1133
|
-
try {
|
|
1134
|
-
const result = await provider.connectAndRegister(registry);
|
|
1135
|
-
printProviderStatus(result.status);
|
|
1136
|
-
console.log(` registered: ${result.registered}`);
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1139
|
-
process.exitCode = 1;
|
|
1140
|
-
}
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
const health = await provider.healthCheck();
|
|
1144
|
-
printProviderStatus(health);
|
|
1145
|
-
if (!health.connected) process.exitCode = 1;
|
|
1146
|
-
}
|
|
1147
|
-
function printProviderStatus(status) {
|
|
1148
|
-
console.log(` health: ${status.health}`);
|
|
1149
|
-
console.log(` connected: ${status.connected}`);
|
|
1150
|
-
console.log(` models: ${status.modelCount}`);
|
|
1151
|
-
if (status.message) console.log(` detail: ${status.message}`);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// src/cli/commands/models.ts
|
|
1155
|
-
var CAPABILITIES = [
|
|
1156
|
-
"chat",
|
|
1157
|
-
"coding",
|
|
1158
|
-
"tools",
|
|
1159
|
-
"vision",
|
|
1160
|
-
"embeddings"
|
|
1161
|
-
];
|
|
1162
|
-
function registerModelsCommand(program) {
|
|
1163
|
-
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) => {
|
|
1164
|
-
const capability = options.capability;
|
|
1165
|
-
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
1166
|
-
throw new OmniNodeError(
|
|
1167
|
-
"CLI_USAGE",
|
|
1168
|
-
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
const config = loadConfig();
|
|
1172
|
-
const selected = config.project.providers.filter(
|
|
1173
|
-
(p) => providerName ? p.name === providerName : true
|
|
1174
|
-
);
|
|
1175
|
-
if (selected.length === 0) {
|
|
1176
|
-
throw new OmniNodeError(
|
|
1177
|
-
"PROVIDER_NOT_FOUND",
|
|
1178
|
-
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
const registry = new ModelRegistry();
|
|
1182
|
-
for (const providerConfig of selected) {
|
|
1183
|
-
try {
|
|
1184
|
-
const provider = createProvider(providerConfig);
|
|
1185
|
-
const discovered = await registry.discoverFrom(provider);
|
|
1186
|
-
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
1187
|
-
} catch (error) {
|
|
1188
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1189
|
-
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
const models = registry.list({
|
|
1193
|
-
...providerName !== void 0 ? { provider: providerName } : {},
|
|
1194
|
-
...capability !== void 0 ? { capability } : {}
|
|
1195
|
-
});
|
|
1196
|
-
if (models.length === 0) {
|
|
1197
|
-
console.log("No models found.");
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
console.log("");
|
|
1201
|
-
for (const model of models) {
|
|
1202
|
-
console.log(`${model.provider}:${model.id}`);
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// src/agents/process/index.ts
|
|
1208
|
-
import { spawn } from "child_process";
|
|
1209
|
-
|
|
1210
|
-
// src/agent-protocol/version.ts
|
|
1211
|
-
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1212
|
-
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1213
|
-
"omninode-agent-protocol/1",
|
|
1214
|
-
PROTOCOL_V2
|
|
1215
|
-
];
|
|
1216
|
-
var MAX_MESSAGE_BYTES = 1048576;
|
|
1217
|
-
function isSupportedProtocol(protocol) {
|
|
1218
|
-
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// src/agent-protocol/types.ts
|
|
1222
|
-
function newMessageId(prefix = "msg") {
|
|
1223
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1224
|
-
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
497
|
+
|
|
498
|
+
// src/agent-protocol/types.ts
|
|
499
|
+
function newMessageId(prefix = "msg") {
|
|
500
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
501
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1225
502
|
}
|
|
1226
503
|
function envelope(type, payload, ids = {}) {
|
|
1227
504
|
return {
|
|
@@ -1613,7 +890,7 @@ function buildChildEnv(config) {
|
|
|
1613
890
|
}
|
|
1614
891
|
|
|
1615
892
|
// src/agents/process/index.ts
|
|
1616
|
-
var
|
|
893
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1617
894
|
var KILL_GRACE_MS = 5e3;
|
|
1618
895
|
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1619
896
|
function composeTaskText(input) {
|
|
@@ -1676,7 +953,7 @@ var ProcessAgent = class {
|
|
|
1676
953
|
throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
|
|
1677
954
|
}
|
|
1678
955
|
const mode = this.config.inputMode ?? "stdin";
|
|
1679
|
-
const timeoutMs = this.config.timeoutMs ??
|
|
956
|
+
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1680
957
|
return new Promise((resolve2, reject) => {
|
|
1681
958
|
let cwd;
|
|
1682
959
|
try {
|
|
@@ -2035,138 +1312,57 @@ function buildAgentRegistry(config, log = logger) {
|
|
|
2035
1312
|
return registry;
|
|
2036
1313
|
}
|
|
2037
1314
|
|
|
2038
|
-
// src/
|
|
2039
|
-
var
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const config = loadConfig();
|
|
2044
|
-
const agents = config.project.agents;
|
|
2045
|
-
if (agents.length === 0) {
|
|
2046
|
-
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
for (const agent2 of agents) {
|
|
2050
|
-
const command = agent2.command ?? "(no command)";
|
|
2051
|
-
console.log(
|
|
2052
|
-
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
2053
|
-
);
|
|
2054
|
-
}
|
|
2055
|
-
});
|
|
2056
|
-
agent.command("add").description(
|
|
2057
|
-
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
2058
|
-
).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(
|
|
2059
|
-
"--input-mode <mode>",
|
|
2060
|
-
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
2061
|
-
"stdin"
|
|
2062
|
-
).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(
|
|
2063
|
-
"--timeout-ms <ms>",
|
|
2064
|
-
"Per-task timeout in milliseconds.",
|
|
2065
|
-
(value) => Number(value)
|
|
2066
|
-
).action(async (options) => addAgent(options));
|
|
2067
|
-
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
2068
|
-
}
|
|
2069
|
-
async function addAgent(options) {
|
|
2070
|
-
const candidate = agentConfigSchema.safeParse({
|
|
2071
|
-
name: options.name,
|
|
2072
|
-
type: "cli",
|
|
2073
|
-
command: options.command,
|
|
2074
|
-
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
2075
|
-
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2076
|
-
input_mode: options.inputMode,
|
|
2077
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2078
|
-
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2079
|
-
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2080
|
-
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2081
|
-
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2082
|
-
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
2083
|
-
});
|
|
2084
|
-
if (!candidate.success) {
|
|
2085
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2086
|
-
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
1315
|
+
// src/roles/index.ts
|
|
1316
|
+
var RoleRegistry = class {
|
|
1317
|
+
roles = /* @__PURE__ */ new Map();
|
|
1318
|
+
register(role) {
|
|
1319
|
+
this.roles.set(role.id, role);
|
|
2087
1320
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
2091
|
-
throw new OmniNodeError(
|
|
2092
|
-
"CLI_USAGE",
|
|
2093
|
-
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
2094
|
-
);
|
|
1321
|
+
registerAll(roles) {
|
|
1322
|
+
for (const role of roles) this.register(role);
|
|
2095
1323
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
2099
|
-
}
|
|
2100
|
-
async function testAgent(name) {
|
|
2101
|
-
const config = loadConfig();
|
|
2102
|
-
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2103
|
-
if (!agentConfig) {
|
|
2104
|
-
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
2105
|
-
throw new AgentError(
|
|
2106
|
-
"AGENT_NOT_FOUND",
|
|
2107
|
-
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
2108
|
-
);
|
|
1324
|
+
get(id) {
|
|
1325
|
+
return this.roles.get(id);
|
|
2109
1326
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
2113
|
-
);
|
|
2114
|
-
const result = await agent.run({
|
|
2115
|
-
taskId: `test-${Date.now()}`,
|
|
2116
|
-
objective: "Reply with the single word: OK"
|
|
2117
|
-
});
|
|
2118
|
-
console.log(` status: ${result.status}`);
|
|
2119
|
-
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
2120
|
-
if (result.summary) console.log(` output: ${result.summary}`);
|
|
2121
|
-
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
2122
|
-
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
1327
|
+
list() {
|
|
1328
|
+
return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2123
1329
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
function registerRoleCommands(program) {
|
|
2130
|
-
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
2131
|
-
role.command("list").description("List configured roles.").action(async () => {
|
|
2132
|
-
const config = loadConfig();
|
|
2133
|
-
const roles = config.project.roles;
|
|
2134
|
-
if (roles.length === 0) {
|
|
2135
|
-
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
for (const role2 of roles) {
|
|
2139
|
-
console.log(`${role2.id} ${role2.name}`);
|
|
2140
|
-
for (const responsibility of role2.responsibilities) {
|
|
2141
|
-
console.log(` - ${responsibility}`);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
});
|
|
1330
|
+
};
|
|
1331
|
+
function buildRoleRegistry(config) {
|
|
1332
|
+
const registry = new RoleRegistry();
|
|
1333
|
+
registry.registerAll(config.project.roles);
|
|
1334
|
+
return registry;
|
|
2145
1335
|
}
|
|
2146
1336
|
|
|
2147
1337
|
// src/persistence/json-file-store.ts
|
|
2148
|
-
import { open, mkdir, readFile, rename, stat } from "fs/promises";
|
|
1338
|
+
import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
|
|
2149
1339
|
import { dirname } from "path";
|
|
2150
1340
|
var SCHEMA_VERSION = 2;
|
|
2151
|
-
async function writeFileAtomic(target, contents) {
|
|
1341
|
+
async function writeFileAtomic(target, contents, durability = "rename") {
|
|
2152
1342
|
const tmp = `${target}.tmp`;
|
|
2153
1343
|
await mkdir(dirname(target), { recursive: true });
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
1344
|
+
if (durability === "fsync") {
|
|
1345
|
+
const handle = await open(tmp, "w");
|
|
1346
|
+
try {
|
|
1347
|
+
await handle.writeFile(contents, "utf8");
|
|
1348
|
+
await handle.sync();
|
|
1349
|
+
} finally {
|
|
1350
|
+
await handle.close();
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
await writeFile(tmp, contents, "utf8");
|
|
2160
1354
|
}
|
|
2161
1355
|
await rename(tmp, target);
|
|
2162
|
-
|
|
2163
|
-
const dir = await open(dirname(target), "r");
|
|
1356
|
+
if (durability === "fsync") {
|
|
2164
1357
|
try {
|
|
2165
|
-
await
|
|
2166
|
-
|
|
2167
|
-
|
|
1358
|
+
const dir = await open(dirname(target), "r");
|
|
1359
|
+
try {
|
|
1360
|
+
await dir.sync();
|
|
1361
|
+
} finally {
|
|
1362
|
+
await dir.close();
|
|
1363
|
+
}
|
|
1364
|
+
} catch {
|
|
2168
1365
|
}
|
|
2169
|
-
} catch {
|
|
2170
1366
|
}
|
|
2171
1367
|
}
|
|
2172
1368
|
async function inspectDocument(path) {
|
|
@@ -2236,6 +1432,7 @@ async function readDocument(path, onCorrupt, quarantine = true) {
|
|
|
2236
1432
|
var JsonFileStore = class {
|
|
2237
1433
|
filePath;
|
|
2238
1434
|
schemaVersion;
|
|
1435
|
+
durability;
|
|
2239
1436
|
onCorrupt;
|
|
2240
1437
|
quarantine;
|
|
2241
1438
|
log;
|
|
@@ -2243,6 +1440,7 @@ var JsonFileStore = class {
|
|
|
2243
1440
|
constructor(options) {
|
|
2244
1441
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2245
1442
|
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
1443
|
+
this.durability = options.durability ?? "rename";
|
|
2246
1444
|
this.onCorrupt = options.onCorrupt;
|
|
2247
1445
|
this.quarantine = options.quarantine ?? true;
|
|
2248
1446
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
@@ -2257,7 +1455,7 @@ var JsonFileStore = class {
|
|
|
2257
1455
|
async writeAll(items) {
|
|
2258
1456
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
2259
1457
|
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2260
|
-
|
|
1458
|
+
`, this.durability);
|
|
2261
1459
|
}
|
|
2262
1460
|
/** Serializes an async operation against this file. */
|
|
2263
1461
|
synchronized(operation) {
|
|
@@ -2279,711 +1477,998 @@ var JsonFileStore = class {
|
|
|
2279
1477
|
}
|
|
2280
1478
|
};
|
|
2281
1479
|
|
|
2282
|
-
// src/
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
1480
|
+
// src/memory/base.ts
|
|
1481
|
+
function defaultSearch(provider, text, options = {}) {
|
|
1482
|
+
return provider.retrieve({
|
|
1483
|
+
text,
|
|
1484
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
1485
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function defaultMetadata(name, options = {}) {
|
|
1489
|
+
return {
|
|
1490
|
+
name,
|
|
1491
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
1492
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
1493
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/memory/local.ts
|
|
1498
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1499
|
+
"the",
|
|
1500
|
+
"and",
|
|
1501
|
+
"for",
|
|
1502
|
+
"with",
|
|
1503
|
+
"this",
|
|
1504
|
+
"that",
|
|
1505
|
+
"from",
|
|
1506
|
+
"into",
|
|
1507
|
+
"was",
|
|
1508
|
+
"were",
|
|
1509
|
+
"have",
|
|
1510
|
+
"has",
|
|
1511
|
+
"had",
|
|
1512
|
+
"are",
|
|
1513
|
+
"but",
|
|
1514
|
+
"not",
|
|
1515
|
+
"you",
|
|
1516
|
+
"his",
|
|
1517
|
+
"her",
|
|
1518
|
+
"its",
|
|
1519
|
+
"their",
|
|
1520
|
+
"them",
|
|
1521
|
+
"then",
|
|
1522
|
+
"than",
|
|
1523
|
+
"when",
|
|
1524
|
+
"what",
|
|
1525
|
+
"which",
|
|
1526
|
+
"will",
|
|
1527
|
+
"would",
|
|
1528
|
+
"been",
|
|
1529
|
+
"were",
|
|
1530
|
+
"also",
|
|
1531
|
+
"because",
|
|
1532
|
+
"about",
|
|
1533
|
+
"after",
|
|
1534
|
+
"before"
|
|
1535
|
+
]);
|
|
1536
|
+
function keywords(text) {
|
|
1537
|
+
return new Set(
|
|
1538
|
+
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
function relevanceScore(queryKeywords, entry2) {
|
|
1542
|
+
if (queryKeywords.size === 0) return 0;
|
|
1543
|
+
const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
|
|
1544
|
+
let overlap = 0;
|
|
1545
|
+
for (const keyword of queryKeywords) {
|
|
1546
|
+
if (entryKeywords.has(keyword)) overlap += 1;
|
|
2326
1547
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
return this.options.store;
|
|
1548
|
+
let tagMatches = 0;
|
|
1549
|
+
for (const tag of entry2.tags ?? []) {
|
|
1550
|
+
if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
|
|
2331
1551
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
"TASK_INVALID",
|
|
2339
|
-
`Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
|
|
2340
|
-
);
|
|
2341
|
-
}
|
|
2342
|
-
if (input.agent && !this.options.agents.get(input.agent)) {
|
|
2343
|
-
throw new TaskError(
|
|
2344
|
-
"TASK_INVALID",
|
|
2345
|
-
`Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2349
|
-
const task = {
|
|
2350
|
-
id: generateTaskId(),
|
|
2351
|
-
objective: input.objective.trim(),
|
|
2352
|
-
status: "created",
|
|
2353
|
-
attempt: 0,
|
|
2354
|
-
createdAt: now,
|
|
2355
|
-
updatedAt: now,
|
|
2356
|
-
...input.project !== void 0 ? { project: input.project } : {},
|
|
2357
|
-
...input.role !== void 0 ? { role: input.role } : {},
|
|
2358
|
-
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
2359
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
2360
|
-
};
|
|
2361
|
-
await this.options.store.save(task);
|
|
2362
|
-
this.log.info(`Created task ${task.id}.`);
|
|
2363
|
-
await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
|
|
2364
|
-
return task;
|
|
1552
|
+
return overlap * 2 + tagMatches * 1.5;
|
|
1553
|
+
}
|
|
1554
|
+
var LocalMemoryProvider = class extends JsonFileStore {
|
|
1555
|
+
name = "local";
|
|
1556
|
+
constructor(directory) {
|
|
1557
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
2365
1558
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
1559
|
+
metadata() {
|
|
1560
|
+
return defaultMetadata(this.name, {
|
|
1561
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
1562
|
+
relevanceRanking: true,
|
|
1563
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
1564
|
+
});
|
|
2369
1565
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (this.options.memory) {
|
|
2389
|
-
const memoryContext = await this.options.memory.gatherContext({
|
|
2390
|
-
objective: task.objective,
|
|
2391
|
-
...task.role ? { role: task.role } : {},
|
|
2392
|
-
...task.project ? { project: task.project } : {}
|
|
2393
|
-
});
|
|
2394
|
-
if (memoryContext.length > 0) {
|
|
2395
|
-
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
2396
|
-
}
|
|
1566
|
+
search(text, options = {}) {
|
|
1567
|
+
return defaultSearch(this, text, options);
|
|
1568
|
+
}
|
|
1569
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
1570
|
+
query(query) {
|
|
1571
|
+
return this.retrieve(query);
|
|
1572
|
+
}
|
|
1573
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
1574
|
+
write(entry2) {
|
|
1575
|
+
return this.store(entry2);
|
|
1576
|
+
}
|
|
1577
|
+
async retrieve(query) {
|
|
1578
|
+
const entries = await this.readAll();
|
|
1579
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
1580
|
+
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1581
|
+
);
|
|
1582
|
+
if (!query.text || query.text.trim().length === 0) {
|
|
1583
|
+
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
2397
1584
|
}
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
const
|
|
2404
|
-
const
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
try {
|
|
2409
|
-
output = await agent.run({
|
|
2410
|
-
taskId: current.id,
|
|
2411
|
-
objective: current.objective,
|
|
2412
|
-
...role ? { role } : {},
|
|
2413
|
-
...background !== void 0 ? { context: background } : {}
|
|
2414
|
-
});
|
|
2415
|
-
} catch (error) {
|
|
2416
|
-
thrown = error;
|
|
2417
|
-
}
|
|
2418
|
-
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2419
|
-
if (thrown !== void 0 || output === void 0) {
|
|
2420
|
-
const outcome = classifyThrown(thrown);
|
|
2421
|
-
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2422
|
-
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2423
|
-
current = await this.transition(current, outcome, {
|
|
2424
|
-
attempt,
|
|
2425
|
-
lastError: message,
|
|
2426
|
-
execution: {
|
|
2427
|
-
executionId,
|
|
2428
|
-
attempt,
|
|
2429
|
-
startedAt,
|
|
2430
|
-
finishedAt,
|
|
2431
|
-
outcome,
|
|
2432
|
-
error: message
|
|
2433
|
-
}
|
|
2434
|
-
});
|
|
2435
|
-
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2436
|
-
if (retryable) {
|
|
2437
|
-
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2441
|
-
return current;
|
|
2442
|
-
}
|
|
2443
|
-
const currentState = await this.options.store.get(current.id);
|
|
2444
|
-
if (currentState?.status === "cancelled") {
|
|
2445
|
-
current = currentState;
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
const result = {
|
|
2449
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2450
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2451
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2452
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2453
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
2454
|
-
finishedAt
|
|
1585
|
+
const queryKeywords = keywords(query.text);
|
|
1586
|
+
return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
|
|
1587
|
+
}
|
|
1588
|
+
store(entry2) {
|
|
1589
|
+
return this.mutate((entries) => {
|
|
1590
|
+
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1591
|
+
const stamped = {
|
|
1592
|
+
...entry2,
|
|
1593
|
+
createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1594
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2455
1595
|
};
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
2461
|
-
});
|
|
2462
|
-
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2463
|
-
break;
|
|
2464
|
-
}
|
|
2465
|
-
if (this.options.memory) {
|
|
2466
|
-
await this.options.memory.recordTaskOutcome(current);
|
|
2467
|
-
}
|
|
2468
|
-
return current;
|
|
1596
|
+
if (index >= 0) entries[index] = stamped;
|
|
1597
|
+
else entries.push(stamped);
|
|
1598
|
+
return [entries, void 0];
|
|
1599
|
+
});
|
|
2469
1600
|
}
|
|
2470
|
-
async
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
1601
|
+
async count() {
|
|
1602
|
+
return (await this.readAll()).length;
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
// src/memory/remembera.ts
|
|
1607
|
+
var RememberaMemoryProvider = class {
|
|
1608
|
+
name = "remembera";
|
|
1609
|
+
baseUrl;
|
|
1610
|
+
apiKeyEnvVar;
|
|
1611
|
+
env;
|
|
1612
|
+
log;
|
|
1613
|
+
constructor(config, log = logger) {
|
|
1614
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1615
|
+
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
1616
|
+
this.env = config.env ?? process.env;
|
|
1617
|
+
this.log = log.child({ component: "remembera" });
|
|
1618
|
+
}
|
|
1619
|
+
get authenticationConfigured() {
|
|
1620
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
1621
|
+
}
|
|
1622
|
+
headers() {
|
|
1623
|
+
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1624
|
+
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1625
|
+
}
|
|
1626
|
+
/** Search delegates to the gateway's relevance query. */
|
|
1627
|
+
search(text, options = {}) {
|
|
1628
|
+
return defaultSearch(
|
|
1629
|
+
this,
|
|
1630
|
+
text,
|
|
1631
|
+
{
|
|
1632
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
1633
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
1634
|
+
}
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
metadata() {
|
|
1638
|
+
return defaultMetadata(this.name, {
|
|
1639
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
1640
|
+
relevanceRanking: true,
|
|
1641
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
1645
|
+
query(query) {
|
|
1646
|
+
return this.retrieve(query);
|
|
1647
|
+
}
|
|
1648
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
1649
|
+
write(entry2) {
|
|
1650
|
+
return this.store(entry2);
|
|
1651
|
+
}
|
|
1652
|
+
async retrieve(query) {
|
|
1653
|
+
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1654
|
+
if (status !== 200) {
|
|
1655
|
+
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
2474
1656
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
1657
|
+
const entries = body.entries;
|
|
1658
|
+
if (!Array.isArray(entries)) {
|
|
1659
|
+
throw new MemoryError("Remembera query response had no entries array.");
|
|
2478
1660
|
}
|
|
2479
|
-
return this.
|
|
1661
|
+
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
2480
1662
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
if (!retryableStates.includes(task.status)) {
|
|
2486
|
-
throw new TaskError(
|
|
2487
|
-
"TASK_INVALID",
|
|
2488
|
-
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2489
|
-
);
|
|
1663
|
+
async store(entry2) {
|
|
1664
|
+
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1665
|
+
if (status !== 200 && status !== 201) {
|
|
1666
|
+
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
2490
1667
|
}
|
|
2491
|
-
const { result: _discarded, ...rest } = task;
|
|
2492
|
-
const reset = {
|
|
2493
|
-
...rest,
|
|
2494
|
-
status: "created",
|
|
2495
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2496
|
-
};
|
|
2497
|
-
await this.options.store.save(reset);
|
|
2498
|
-
await this.audit("task.retry", id, { from: task.status });
|
|
2499
|
-
this.log.info(`Task ${id} reset for retry.`);
|
|
2500
|
-
return reset;
|
|
2501
1668
|
}
|
|
2502
|
-
async
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
1669
|
+
async request(url, body) {
|
|
1670
|
+
const headers = this.headers();
|
|
1671
|
+
try {
|
|
1672
|
+
return await requestJson(url, {
|
|
1673
|
+
method: "POST",
|
|
1674
|
+
headers,
|
|
1675
|
+
body
|
|
1676
|
+
});
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1679
|
+
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
2512
1680
|
}
|
|
2513
|
-
return task;
|
|
2514
1681
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
1682
|
+
toEntry(raw) {
|
|
1683
|
+
if (raw === null || typeof raw !== "object") {
|
|
1684
|
+
this.log.warn("Skipping malformed Remembera memory entry.");
|
|
1685
|
+
return void 0;
|
|
2519
1686
|
}
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
1687
|
+
const entry2 = raw;
|
|
1688
|
+
if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
|
|
1689
|
+
this.log.warn("Skipping Remembera memory entry without key/content.");
|
|
1690
|
+
return void 0;
|
|
1691
|
+
}
|
|
1692
|
+
const scope = entry2.scope;
|
|
1693
|
+
return {
|
|
1694
|
+
key: entry2.key,
|
|
1695
|
+
content: entry2.content,
|
|
1696
|
+
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1697
|
+
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
1698
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
1699
|
+
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1700
|
+
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
2529
1701
|
};
|
|
2530
|
-
await this.options.store.save(updated);
|
|
2531
|
-
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
|
|
2532
|
-
from: task.status
|
|
2533
|
-
});
|
|
2534
|
-
return updated;
|
|
2535
|
-
}
|
|
2536
|
-
async audit(action, id, detail) {
|
|
2537
|
-
await this.options.audit?.record({
|
|
2538
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2539
|
-
action,
|
|
2540
|
-
id,
|
|
2541
|
-
...detail !== void 0 ? { detail } : {}
|
|
2542
|
-
});
|
|
2543
1702
|
}
|
|
2544
1703
|
};
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
}
|
|
2551
|
-
function classifyThrown(error) {
|
|
2552
|
-
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2553
|
-
const dispatched = error.details?.dispatched;
|
|
2554
|
-
return dispatched === false ? "timed_out" : "unknown";
|
|
1704
|
+
|
|
1705
|
+
// src/memory/factory.ts
|
|
1706
|
+
function createMemoryProvider(config) {
|
|
1707
|
+
if (!config || config.provider === "local") {
|
|
1708
|
+
return new LocalMemoryProvider();
|
|
2555
1709
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
1710
|
+
if (config.provider === "remembera") {
|
|
1711
|
+
if (!config.baseUrl) {
|
|
1712
|
+
throw new ConfigError(
|
|
1713
|
+
"CONFIG_INVALID",
|
|
1714
|
+
`Memory provider "remembera" needs a base_url in omninode.yaml.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
return new RememberaMemoryProvider({
|
|
1718
|
+
baseUrl: config.baseUrl,
|
|
1719
|
+
apiKeyEnvVar: config.apiKeyEnvVar
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
throw new ConfigError(
|
|
1723
|
+
"CONFIG_INVALID",
|
|
1724
|
+
`Unknown memory provider "${config.provider}". Available: local, remembera.`
|
|
1725
|
+
);
|
|
2560
1726
|
}
|
|
2561
1727
|
|
|
2562
|
-
// src/
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
var
|
|
2566
|
-
|
|
1728
|
+
// src/memory/service.ts
|
|
1729
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1730
|
+
var MAX_CONTEXT_CHARS = 4e3;
|
|
1731
|
+
var MemoryService = class {
|
|
1732
|
+
constructor(provider, log = logger, options = {}) {
|
|
1733
|
+
this.provider = provider;
|
|
1734
|
+
this.log = log.child({ component: "memory", provider: provider.name });
|
|
1735
|
+
this.required = options.required ?? false;
|
|
1736
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1737
|
+
}
|
|
1738
|
+
provider;
|
|
2567
1739
|
log;
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
1740
|
+
required;
|
|
1741
|
+
maxContextChars;
|
|
1742
|
+
get providerRef() {
|
|
1743
|
+
return this.provider;
|
|
2571
1744
|
}
|
|
2572
|
-
|
|
1745
|
+
get providerName() {
|
|
1746
|
+
return this.provider.name;
|
|
1747
|
+
}
|
|
1748
|
+
get isRequired() {
|
|
1749
|
+
return this.required;
|
|
1750
|
+
}
|
|
1751
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
1752
|
+
async contextFor(query) {
|
|
1753
|
+
let entries;
|
|
2573
1754
|
try {
|
|
2574
|
-
await
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
`, "utf8");
|
|
2580
|
-
await handle.sync();
|
|
2581
|
-
} finally {
|
|
2582
|
-
await handle.close();
|
|
2583
|
-
}
|
|
1755
|
+
entries = await this.provider.retrieve({
|
|
1756
|
+
text: query.objective,
|
|
1757
|
+
...query.role ? { tags: [query.role] } : {},
|
|
1758
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1759
|
+
});
|
|
2584
1760
|
} catch (error) {
|
|
2585
|
-
this.
|
|
2586
|
-
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2587
|
-
);
|
|
1761
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
2588
1762
|
}
|
|
1763
|
+
return this.format(entries);
|
|
2589
1764
|
}
|
|
2590
|
-
/**
|
|
2591
|
-
async
|
|
2592
|
-
|
|
1765
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
1766
|
+
async gatherContext(query) {
|
|
1767
|
+
return (await this.contextFor(query)).text;
|
|
1768
|
+
}
|
|
1769
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1770
|
+
async recordTaskOutcome(task) {
|
|
1771
|
+
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
2593
1772
|
try {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
1773
|
+
await this.provider.store({
|
|
1774
|
+
key: `task:${task.id}`,
|
|
1775
|
+
scope: "task",
|
|
1776
|
+
category: "tasks",
|
|
1777
|
+
content: `Objective: ${task.objective}
|
|
1778
|
+
Status: ${task.status}
|
|
1779
|
+
Outcome: ${outcome}`,
|
|
1780
|
+
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
1781
|
+
createdAt: task.createdAt
|
|
1782
|
+
});
|
|
1783
|
+
const significant = (task.result?.reports ?? []).flatMap(
|
|
1784
|
+
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1785
|
+
);
|
|
1786
|
+
for (const { report, finding } of significant) {
|
|
1787
|
+
await this.provider.store({
|
|
1788
|
+
key: `finding:${report.id}:${finding.id}`,
|
|
1789
|
+
scope: "project",
|
|
1790
|
+
category: "problems",
|
|
1791
|
+
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1792
|
+
tags: ["known-problem", report.agent]
|
|
1793
|
+
});
|
|
2603
1794
|
}
|
|
2604
|
-
}
|
|
2605
|
-
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
this.handleFailure("store", error, void 0);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
format(entries) {
|
|
1800
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
1801
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1802
|
+
for (const entry2 of entries) {
|
|
1803
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
1804
|
+
}
|
|
1805
|
+
const parts = [];
|
|
1806
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1807
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
1808
|
+
if (scopeEntries.length === 0) continue;
|
|
1809
|
+
const lines = scopeEntries.map((entry2) => {
|
|
1810
|
+
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
1811
|
+
return `- ${category}${entry2.content}`;
|
|
1812
|
+
}).join("\n");
|
|
1813
|
+
parts.push(`### Relevant memory (${scope})
|
|
1814
|
+
${lines}`);
|
|
1815
|
+
}
|
|
1816
|
+
let text = parts.join("\n\n");
|
|
1817
|
+
const truncated = text.length > this.maxContextChars;
|
|
1818
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
1819
|
+
return { text, entries, truncated };
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
1823
|
+
*/
|
|
1824
|
+
handleFailure(operation, error, fallback) {
|
|
1825
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1826
|
+
if (this.required) {
|
|
1827
|
+
throw new MemoryError(
|
|
1828
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
1829
|
+
{ cause: error }
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
1833
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
2606
1834
|
}
|
|
2607
1835
|
};
|
|
2608
1836
|
|
|
2609
|
-
// src/
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2614
|
-
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2615
|
-
});
|
|
2616
|
-
}
|
|
2617
|
-
function defaultMetadata(name, options = {}) {
|
|
2618
|
-
return {
|
|
2619
|
-
name,
|
|
2620
|
-
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2621
|
-
relevanceRanking: options.relevanceRanking ?? false,
|
|
2622
|
-
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
// src/memory/local.ts
|
|
2627
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2628
|
-
"the",
|
|
2629
|
-
"and",
|
|
2630
|
-
"for",
|
|
2631
|
-
"with",
|
|
2632
|
-
"this",
|
|
2633
|
-
"that",
|
|
2634
|
-
"from",
|
|
2635
|
-
"into",
|
|
2636
|
-
"was",
|
|
2637
|
-
"were",
|
|
2638
|
-
"have",
|
|
2639
|
-
"has",
|
|
2640
|
-
"had",
|
|
2641
|
-
"are",
|
|
2642
|
-
"but",
|
|
2643
|
-
"not",
|
|
2644
|
-
"you",
|
|
2645
|
-
"his",
|
|
2646
|
-
"her",
|
|
2647
|
-
"its",
|
|
2648
|
-
"their",
|
|
2649
|
-
"them",
|
|
2650
|
-
"then",
|
|
2651
|
-
"than",
|
|
2652
|
-
"when",
|
|
2653
|
-
"what",
|
|
2654
|
-
"which",
|
|
2655
|
-
"will",
|
|
2656
|
-
"would",
|
|
2657
|
-
"been",
|
|
2658
|
-
"were",
|
|
2659
|
-
"also",
|
|
2660
|
-
"because",
|
|
2661
|
-
"about",
|
|
2662
|
-
"after",
|
|
2663
|
-
"before"
|
|
2664
|
-
]);
|
|
2665
|
-
function keywords(text) {
|
|
2666
|
-
return new Set(
|
|
2667
|
-
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
2668
|
-
);
|
|
2669
|
-
}
|
|
2670
|
-
function relevanceScore(queryKeywords, entry2) {
|
|
2671
|
-
if (queryKeywords.size === 0) return 0;
|
|
2672
|
-
const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
|
|
2673
|
-
let overlap = 0;
|
|
2674
|
-
for (const keyword of queryKeywords) {
|
|
2675
|
-
if (entryKeywords.has(keyword)) overlap += 1;
|
|
1837
|
+
// src/pipelines/store.ts
|
|
1838
|
+
var FilePipelineRunStore = class extends JsonFileStore {
|
|
1839
|
+
constructor(directory) {
|
|
1840
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
2676
1841
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
1842
|
+
save(run2) {
|
|
1843
|
+
return this.mutate((runs) => {
|
|
1844
|
+
const index = runs.findIndex((r) => r.id === run2.id);
|
|
1845
|
+
if (index >= 0) runs[index] = run2;
|
|
1846
|
+
else runs.push(run2);
|
|
1847
|
+
return [runs, void 0];
|
|
1848
|
+
});
|
|
2680
1849
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
1850
|
+
async get(id) {
|
|
1851
|
+
return (await this.readAll()).find((r) => r.id === id);
|
|
1852
|
+
}
|
|
1853
|
+
async list(filter = {}) {
|
|
1854
|
+
return (await this.readAll()).filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
// src/tasks/store.ts
|
|
1859
|
+
var FileTaskStore = class extends JsonFileStore {
|
|
2685
1860
|
constructor(directory) {
|
|
2686
|
-
super({ ...directory !== void 0 ? { directory } : {}, fileName: "
|
|
1861
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
|
|
2687
1862
|
}
|
|
2688
|
-
|
|
2689
|
-
return
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
1863
|
+
save(task) {
|
|
1864
|
+
return this.mutate((tasks) => {
|
|
1865
|
+
const index = tasks.findIndex((t) => t.id === task.id);
|
|
1866
|
+
if (index >= 0) tasks[index] = task;
|
|
1867
|
+
else tasks.push(task);
|
|
1868
|
+
return [tasks, void 0];
|
|
2693
1869
|
});
|
|
2694
1870
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
1871
|
+
async get(id) {
|
|
1872
|
+
const tasks = await this.readAll();
|
|
1873
|
+
return tasks.find((t) => t.id === id);
|
|
2697
1874
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
return
|
|
1875
|
+
async list(filter = {}) {
|
|
1876
|
+
const tasks = await this.readAll();
|
|
1877
|
+
return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2701
1878
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
// src/cli/commands/inspect.ts
|
|
1882
|
+
function emit(json, payload, render) {
|
|
1883
|
+
if (json) {
|
|
1884
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1885
|
+
return;
|
|
2705
1886
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
1887
|
+
render();
|
|
1888
|
+
}
|
|
1889
|
+
function jsonOption(command) {
|
|
1890
|
+
return command.option("--json", "Emit the raw record as JSON.");
|
|
1891
|
+
}
|
|
1892
|
+
function registerTaskInspect(taskGroup) {
|
|
1893
|
+
jsonOption(
|
|
1894
|
+
taskGroup.command("inspect <id>").description("Show a task with its execution history, result and report references.").action(async (id, options) => {
|
|
1895
|
+
const tasks = await new FileTaskStore().list();
|
|
1896
|
+
const task = tasks.find((t) => t.id === id);
|
|
1897
|
+
if (!task) throw new OmniNodeError("TASK_NOT_FOUND", `No task "${id}" in this project's store.`);
|
|
1898
|
+
emit(options.json ?? false, task, () => {
|
|
1899
|
+
console.log(`Task ${task.id}`);
|
|
1900
|
+
console.log(` status: ${task.status}${task.attempt > 0 ? ` (attempt ${task.attempt})` : ""}`);
|
|
1901
|
+
if (task.project) console.log(` project: ${task.project}`);
|
|
1902
|
+
if (task.pipelineId) console.log(` pipeline: ${task.pipelineId}`);
|
|
1903
|
+
console.log(` objective: ${task.objective}`);
|
|
1904
|
+
if (task.role) console.log(` role: ${task.role}`);
|
|
1905
|
+
if (task.agent) console.log(` agent: ${task.agent}`);
|
|
1906
|
+
console.log(` created: ${task.createdAt}`);
|
|
1907
|
+
console.log(` updated: ${task.updatedAt}`);
|
|
1908
|
+
if (task.lastError) console.log(` lastError: ${task.lastError}`);
|
|
1909
|
+
if (task.executions && task.executions.length > 0) {
|
|
1910
|
+
console.log(" executions:");
|
|
1911
|
+
for (const execution of task.executions) {
|
|
1912
|
+
console.log(
|
|
1913
|
+
` #${execution.attempt} ${execution.outcome} (${execution.executionId})${execution.error ? ` \u2014 ${execution.error}` : ""}`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
if (task.result?.summary) console.log(` summary: ${task.result.summary}`);
|
|
1918
|
+
if (task.result?.reports && task.result.reports.length > 0) {
|
|
1919
|
+
console.log(` reports: ${task.result.reports.length}`);
|
|
1920
|
+
for (const report of task.result.reports) {
|
|
1921
|
+
console.log(` - [${report.agent}] ${report.summary}`);
|
|
1922
|
+
for (const finding of report.findings) {
|
|
1923
|
+
console.log(` \xB7 [${finding.severity ?? "info"}] ${finding.title}`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
})
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
function registerPipelineInspect(pipelineGroup) {
|
|
1932
|
+
jsonOption(
|
|
1933
|
+
pipelineGroup.command("inspect <run-id>").description("Show a pipeline run with per-step status and artifact references.").action(async (runId, options) => {
|
|
1934
|
+
const run2 = await new FilePipelineRunStore().get(runId);
|
|
1935
|
+
if (!run2) throw new OmniNodeError("PIPELINE_NOT_FOUND", `No pipeline run "${runId}" in this project's store.`);
|
|
1936
|
+
emit(options.json ?? false, run2, () => {
|
|
1937
|
+
console.log(`Pipeline run ${run2.id} (${run2.pipelineId})`);
|
|
1938
|
+
console.log(` status: ${run2.status}${run2.attempt > 1 ? ` (attempt ${run2.attempt})` : ""}`);
|
|
1939
|
+
if (run2.objective) console.log(` objective: ${run2.objective}`);
|
|
1940
|
+
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
1941
|
+
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
1942
|
+
if (run2.planId) console.log(` plan: ${run2.planId}`);
|
|
1943
|
+
if (run2.combinedReportId) console.log(` combined: ${run2.combinedReportId}`);
|
|
1944
|
+
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
1945
|
+
console.log(" steps:");
|
|
1946
|
+
for (const step of run2.stepRuns) {
|
|
1947
|
+
const stepTaskIds = step.taskIds ?? [];
|
|
1948
|
+
const tasks = stepTaskIds.length > 0 ? ` tasks=${stepTaskIds.join(",")}` : "";
|
|
1949
|
+
console.log(` ${step.stepId.padEnd(14)} ${step.status}${tasks}${step.error ? ` \u2014 ${step.error}` : ""}`);
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
})
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
function registerAgentInspect(agentGroup) {
|
|
1956
|
+
jsonOption(
|
|
1957
|
+
agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
|
|
1958
|
+
const config = loadConfig();
|
|
1959
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
1960
|
+
if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
|
|
1961
|
+
const registry = buildAgentRegistry(config);
|
|
1962
|
+
const agent = registry.get(name);
|
|
1963
|
+
const recent = (await new FileTaskStore().list()).filter((task) => task.agent === name).slice(-5).map((task) => ({ id: task.id, status: task.status, updatedAt: task.updatedAt }));
|
|
1964
|
+
const payload = {
|
|
1965
|
+
config: agentConfig,
|
|
1966
|
+
effective: agent?.info,
|
|
1967
|
+
envPolicy: resolveEnvPolicy(agentConfig),
|
|
1968
|
+
recentTasks: recent
|
|
1969
|
+
};
|
|
1970
|
+
emit(options.json ?? false, payload, () => {
|
|
1971
|
+
console.log(`Agent ${name}`);
|
|
1972
|
+
console.log(` integration: ${agentConfig.integration}`);
|
|
1973
|
+
console.log(` command: ${agentConfig.command ?? "(none)"}`);
|
|
1974
|
+
console.log(` input mode: ${agentConfig.inputMode ?? "stdin"}`);
|
|
1975
|
+
console.log(` env policy: ${resolveEnvPolicy(agentConfig)}`);
|
|
1976
|
+
if (agentConfig.cwd) console.log(` working dir: ${agentConfig.cwd}`);
|
|
1977
|
+
if (agentConfig.timeoutMs) console.log(` timeout: ${agentConfig.timeoutMs}ms`);
|
|
1978
|
+
if (agentConfig.maxOutputBytes) console.log(` max output: ${agentConfig.maxOutputBytes} bytes/stream`);
|
|
1979
|
+
if (agentConfig.allowExternalCwd) console.log(` external cwd allowed`);
|
|
1980
|
+
console.log(` recent tasks: ${recent.map((t) => `${t.id}:${t.status}`).join(", ") || "(none)"}`);
|
|
1981
|
+
});
|
|
1982
|
+
})
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
function registerProviderInspect(providerGroup) {
|
|
1986
|
+
jsonOption(
|
|
1987
|
+
providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
|
|
1988
|
+
const config = loadConfig();
|
|
1989
|
+
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
1990
|
+
if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
|
|
1991
|
+
const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
|
|
1992
|
+
const provider = createProvider2(providerConfig);
|
|
1993
|
+
const payload = {
|
|
1994
|
+
config: providerConfig,
|
|
1995
|
+
providerId: provider.providerId,
|
|
1996
|
+
authentication: provider.authentication,
|
|
1997
|
+
capabilities: providerConfig.capabilities ?? [],
|
|
1998
|
+
memoryConfigured: config.project.memory ? { provider: config.project.memory.provider, required: config.project.memory.required ?? false } : void 0,
|
|
1999
|
+
memoryEntries: config.project.memory ? await new MemoryService(createMemoryProvider(config.project.memory)).providerRef.query({ limit: 1e4 }).then((e) => e.length) : void 0,
|
|
2000
|
+
roles: buildRoleRegistry(config).list().length
|
|
2001
|
+
};
|
|
2002
|
+
emit(options.json ?? false, payload, () => {
|
|
2003
|
+
console.log(`Provider ${name}`);
|
|
2004
|
+
console.log(` providerId: ${provider.providerId}`);
|
|
2005
|
+
console.log(` type: ${providerConfig.type}`);
|
|
2006
|
+
console.log(` base URL: ${providerConfig.baseUrl}`);
|
|
2007
|
+
console.log(` auth: ${provider.authentication.method}${provider.authentication.envVar ? ` ($${provider.authentication.envVar})` : ""}`);
|
|
2008
|
+
console.log(` credentials: ${provider.authentication.configured ? "configured" : "MISSING"}`);
|
|
2009
|
+
if (providerConfig.capabilities?.length) {
|
|
2010
|
+
console.log(` capabilities: ${providerConfig.capabilities.join(", ")}`);
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
})
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/cli/commands/providers.ts
|
|
2018
|
+
function registerProviderCommands(program) {
|
|
2019
|
+
const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
|
|
2020
|
+
provider.command("list").description("List configured providers.").option("--json", "Emit providers as JSON.").action(async (options) => {
|
|
2021
|
+
const config = loadConfig();
|
|
2022
|
+
const providers = config.project.providers;
|
|
2023
|
+
if (options.json) {
|
|
2024
|
+
console.log(JSON.stringify(providers, null, 2));
|
|
2025
|
+
return;
|
|
2713
2026
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2027
|
+
if (providers.length === 0) {
|
|
2028
|
+
console.log(
|
|
2029
|
+
"No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
|
|
2030
|
+
);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
for (const provider2 of providers) {
|
|
2034
|
+
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
2035
|
+
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
2036
|
+
console.log(
|
|
2037
|
+
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
provider.command("add").description(
|
|
2042
|
+
"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."
|
|
2043
|
+
).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(
|
|
2044
|
+
"--type <type>",
|
|
2045
|
+
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
2046
|
+
"openai-compatible"
|
|
2047
|
+
).option(
|
|
2048
|
+
"--api-key-env-var <envVar>",
|
|
2049
|
+
"Environment variable holding the API key. Keys are never written to the config file."
|
|
2050
|
+
).action(async (options) => addProvider(options));
|
|
2051
|
+
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
2052
|
+
"--connect",
|
|
2053
|
+
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
2054
|
+
).action(async (name, options) => testProvider(name, options));
|
|
2055
|
+
provider.command("remove <name>").description("Remove a provider from omninode.yaml.").action((name) => {
|
|
2056
|
+
removeProvider(name);
|
|
2057
|
+
});
|
|
2058
|
+
registerProviderInspect(provider);
|
|
2059
|
+
}
|
|
2060
|
+
async function addProvider(options) {
|
|
2061
|
+
const configPath = findConfigFile();
|
|
2062
|
+
if (!configPath) {
|
|
2063
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
2716
2064
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
else entries.push(stamped);
|
|
2727
|
-
return [entries, void 0];
|
|
2728
|
-
});
|
|
2065
|
+
const candidate = providerConfigSchema.safeParse({
|
|
2066
|
+
name: options.name,
|
|
2067
|
+
type: options.type,
|
|
2068
|
+
base_url: options.baseUrl,
|
|
2069
|
+
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
2070
|
+
});
|
|
2071
|
+
if (!candidate.success) {
|
|
2072
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2073
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
2729
2074
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2075
|
+
const newProvider = candidate.data;
|
|
2076
|
+
const existing = loadConfig();
|
|
2077
|
+
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
2078
|
+
throw new OmniNodeError(
|
|
2079
|
+
"CLI_USAGE",
|
|
2080
|
+
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
2081
|
+
);
|
|
2732
2082
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
log;
|
|
2742
|
-
constructor(config, log = logger) {
|
|
2743
|
-
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2744
|
-
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
2745
|
-
this.env = config.env ?? process.env;
|
|
2746
|
-
this.log = log.child({ component: "remembera" });
|
|
2083
|
+
const writtenPath = appendToConfigList("providers", newProvider);
|
|
2084
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
2085
|
+
if (newProvider.api_key_env_var) {
|
|
2086
|
+
console.log(
|
|
2087
|
+
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
2088
|
+
);
|
|
2089
|
+
} else {
|
|
2090
|
+
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
2747
2091
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2092
|
+
}
|
|
2093
|
+
function removeProvider(name) {
|
|
2094
|
+
const configPath = findConfigFile();
|
|
2095
|
+
if (!configPath) {
|
|
2096
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
|
|
2750
2097
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2098
|
+
const config = loadConfig();
|
|
2099
|
+
if (!config.project.providers.some((p) => p.name === name)) {
|
|
2100
|
+
throw new ProviderError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
|
|
2754
2101
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2102
|
+
const doc = parseDocument2(readFileSync3(configPath, "utf8"));
|
|
2103
|
+
const current = doc.getIn(["project", "providers"]);
|
|
2104
|
+
const remaining = (current === void 0 || current === null ? [] : doc.get("project")?.providers ?? []).filter((entry2) => entry2?.name !== name);
|
|
2105
|
+
doc.setIn(["project", "providers"], remaining);
|
|
2106
|
+
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
2107
|
+
if (!validated.success) {
|
|
2108
|
+
throw new OmniNodeError("CONFIG_INVALID", "Removing this provider would produce an invalid configuration.");
|
|
2109
|
+
}
|
|
2110
|
+
writeFileSync3(configPath, doc.toString(), "utf8");
|
|
2111
|
+
console.log(`Removed provider "${name}" from ${configPath}.`);
|
|
2112
|
+
}
|
|
2113
|
+
async function testProvider(name, options = {}) {
|
|
2114
|
+
const config = loadConfig();
|
|
2115
|
+
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
2116
|
+
if (!providerConfig) {
|
|
2117
|
+
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
2118
|
+
throw new ProviderError(
|
|
2119
|
+
"PROVIDER_NOT_FOUND",
|
|
2120
|
+
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
2764
2121
|
);
|
|
2765
2122
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2123
|
+
const provider = createProvider(providerConfig);
|
|
2124
|
+
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
2125
|
+
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
2126
|
+
const registry = new ModelRegistry();
|
|
2127
|
+
try {
|
|
2128
|
+
const result = await provider.connectAndRegister(registry);
|
|
2129
|
+
printProviderStatus(result.status);
|
|
2130
|
+
console.log(` registered: ${result.registered}`);
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2133
|
+
process.exitCode = 1;
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2772
2136
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2137
|
+
const health = await provider.healthCheck();
|
|
2138
|
+
printProviderStatus(health);
|
|
2139
|
+
if (!health.connected) process.exitCode = 1;
|
|
2140
|
+
}
|
|
2141
|
+
function printProviderStatus(status) {
|
|
2142
|
+
console.log(` health: ${status.health}`);
|
|
2143
|
+
console.log(` connected: ${status.connected}`);
|
|
2144
|
+
console.log(` models: ${status.modelCount}`);
|
|
2145
|
+
if (status.message) console.log(` detail: ${status.message}`);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/tasks/engine.ts
|
|
2149
|
+
import { randomBytes } from "crypto";
|
|
2150
|
+
var ALLOWED_TRANSITIONS = {
|
|
2151
|
+
created: ["queued", "running", "cancelled"],
|
|
2152
|
+
queued: ["running", "cancelled"],
|
|
2153
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
2154
|
+
waiting: ["running", "cancelled"],
|
|
2155
|
+
completed: [],
|
|
2156
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
2157
|
+
failed: ["running"],
|
|
2158
|
+
timed_out: [],
|
|
2159
|
+
cancelled: [],
|
|
2160
|
+
unknown: [],
|
|
2161
|
+
partially_completed: []
|
|
2162
|
+
};
|
|
2163
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
2164
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
2165
|
+
var TaskEngine = class {
|
|
2166
|
+
constructor(options) {
|
|
2167
|
+
this.options = options;
|
|
2168
|
+
this.log = (options.log ?? logger).child({ component: "tasks" });
|
|
2776
2169
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2170
|
+
options;
|
|
2171
|
+
log;
|
|
2172
|
+
get store() {
|
|
2173
|
+
return this.options.store;
|
|
2780
2174
|
}
|
|
2781
|
-
async
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
2175
|
+
async create(input) {
|
|
2176
|
+
if (!input.objective || input.objective.trim().length === 0) {
|
|
2177
|
+
throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
|
|
2785
2178
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2179
|
+
if (input.role && !this.options.roles.get(input.role)) {
|
|
2180
|
+
throw new TaskError(
|
|
2181
|
+
"TASK_INVALID",
|
|
2182
|
+
`Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
|
|
2183
|
+
);
|
|
2789
2184
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
2185
|
+
if (input.agent && !this.options.agents.get(input.agent)) {
|
|
2186
|
+
throw new TaskError(
|
|
2187
|
+
"TASK_INVALID",
|
|
2188
|
+
`Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
|
|
2189
|
+
);
|
|
2796
2190
|
}
|
|
2191
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2192
|
+
const task = {
|
|
2193
|
+
id: generateTaskId(),
|
|
2194
|
+
objective: input.objective.trim(),
|
|
2195
|
+
status: "created",
|
|
2196
|
+
attempt: 0,
|
|
2197
|
+
createdAt: now,
|
|
2198
|
+
updatedAt: now,
|
|
2199
|
+
...input.project !== void 0 ? { project: input.project } : {},
|
|
2200
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
2201
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
2202
|
+
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
2203
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
2204
|
+
};
|
|
2205
|
+
await this.options.store.save(task);
|
|
2206
|
+
this.log.info(`Created task ${task.id}.`);
|
|
2207
|
+
await this.audit("task.created", task, { objective: task.objective });
|
|
2208
|
+
return task;
|
|
2797
2209
|
}
|
|
2798
|
-
async
|
|
2799
|
-
const
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2210
|
+
async queue(id) {
|
|
2211
|
+
const task = await this.mustGet(id);
|
|
2212
|
+
return this.transition(task, "queued");
|
|
2213
|
+
}
|
|
2214
|
+
/** Runs the task through its assigned agent and records the result.
|
|
2215
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
2216
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
2217
|
+
async run(id, options = {}) {
|
|
2218
|
+
const task = await this.mustGet(id);
|
|
2219
|
+
if (task.status !== "created" && task.status !== "queued") {
|
|
2220
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
2221
|
+
}
|
|
2222
|
+
if (!task.agent) {
|
|
2223
|
+
throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
|
|
2224
|
+
}
|
|
2225
|
+
const agent = this.options.agents.get(task.agent);
|
|
2226
|
+
if (!agent) {
|
|
2227
|
+
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
2228
|
+
}
|
|
2229
|
+
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
2230
|
+
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
2231
|
+
let background = task.context?.background;
|
|
2232
|
+
if (this.options.memory) {
|
|
2233
|
+
const memoryContext = await this.options.memory.gatherContext({
|
|
2234
|
+
objective: task.objective,
|
|
2235
|
+
...task.role ? { role: task.role } : {},
|
|
2236
|
+
...task.project ? { project: task.project } : {}
|
|
2805
2237
|
});
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2238
|
+
if (memoryContext.length > 0) {
|
|
2239
|
+
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
2243
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
2244
|
+
let current = task;
|
|
2245
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2246
|
+
const attempt = current.attempt + 1;
|
|
2247
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2248
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2249
|
+
current = await this.transition(current, "running", { attempt });
|
|
2250
|
+
await this.options.audit?.record({
|
|
2251
|
+
at: startedAt,
|
|
2252
|
+
action: "agent.started",
|
|
2253
|
+
id: current.id,
|
|
2254
|
+
taskId: current.id,
|
|
2255
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2256
|
+
agentId: task.agent,
|
|
2257
|
+
executionId,
|
|
2258
|
+
detail: { attempt }
|
|
2259
|
+
});
|
|
2260
|
+
let output;
|
|
2261
|
+
let thrown;
|
|
2262
|
+
try {
|
|
2263
|
+
output = await agent.run({
|
|
2264
|
+
taskId: current.id,
|
|
2265
|
+
objective: current.objective,
|
|
2266
|
+
executionId,
|
|
2267
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2268
|
+
...role ? { role } : {},
|
|
2269
|
+
...background !== void 0 ? { context: background } : {}
|
|
2270
|
+
});
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
thrown = error;
|
|
2273
|
+
}
|
|
2274
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2275
|
+
await this.options.audit?.record({
|
|
2276
|
+
at: finishedAt,
|
|
2277
|
+
action: "agent.completed",
|
|
2278
|
+
id: current.id,
|
|
2279
|
+
taskId: current.id,
|
|
2280
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2281
|
+
agentId: task.agent,
|
|
2282
|
+
executionId,
|
|
2283
|
+
detail: { attempt, outcome: thrown !== void 0 || output === void 0 ? "threw" : "returned" }
|
|
2284
|
+
});
|
|
2285
|
+
if (thrown !== void 0 || output === void 0) {
|
|
2286
|
+
const outcome = classifyThrown(thrown);
|
|
2287
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2288
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2289
|
+
current = await this.transition(current, outcome, {
|
|
2290
|
+
attempt,
|
|
2291
|
+
lastError: message,
|
|
2292
|
+
execution: {
|
|
2293
|
+
executionId,
|
|
2294
|
+
attempt,
|
|
2295
|
+
startedAt,
|
|
2296
|
+
finishedAt,
|
|
2297
|
+
outcome,
|
|
2298
|
+
error: message
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2302
|
+
if (retryable) {
|
|
2303
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2307
|
+
return current;
|
|
2308
|
+
}
|
|
2309
|
+
const currentState = await this.options.store.get(current.id);
|
|
2310
|
+
if (currentState?.status === "cancelled") {
|
|
2311
|
+
current = currentState;
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
const result = {
|
|
2315
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2316
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2317
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2318
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2319
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
2320
|
+
finishedAt
|
|
2321
|
+
};
|
|
2322
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2323
|
+
current = await this.transition(current, finalStatus, {
|
|
2324
|
+
attempt,
|
|
2325
|
+
result,
|
|
2326
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
2327
|
+
});
|
|
2328
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
if (this.options.memory) {
|
|
2332
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
2333
|
+
}
|
|
2334
|
+
return current;
|
|
2335
|
+
}
|
|
2336
|
+
async cancel(id) {
|
|
2337
|
+
const task = await this.mustGet(id);
|
|
2338
|
+
if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
|
|
2339
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
|
|
2340
|
+
}
|
|
2341
|
+
if (task.agent) {
|
|
2342
|
+
const agent = this.options.agents.get(task.agent);
|
|
2343
|
+
await agent?.cancel?.(task.id);
|
|
2809
2344
|
}
|
|
2345
|
+
return this.transition(task, "cancelled");
|
|
2810
2346
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2347
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
2348
|
+
async retry(id) {
|
|
2349
|
+
const task = await this.mustGet(id);
|
|
2350
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2351
|
+
if (!retryableStates.includes(task.status)) {
|
|
2352
|
+
throw new TaskError(
|
|
2353
|
+
"TASK_INVALID",
|
|
2354
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2355
|
+
);
|
|
2820
2356
|
}
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
2827
|
-
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
2828
|
-
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
2829
|
-
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
2357
|
+
const { result: _discarded, ...rest } = task;
|
|
2358
|
+
const reset = {
|
|
2359
|
+
...rest,
|
|
2360
|
+
status: "created",
|
|
2361
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2830
2362
|
};
|
|
2363
|
+
await this.options.store.save(reset);
|
|
2364
|
+
await this.audit("task.retry", reset, { from: task.status });
|
|
2365
|
+
this.log.info(`Task ${id} reset for retry.`);
|
|
2366
|
+
return reset;
|
|
2831
2367
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
// src/memory/factory.ts
|
|
2835
|
-
function createMemoryProvider(config) {
|
|
2836
|
-
if (!config || config.provider === "local") {
|
|
2837
|
-
return new LocalMemoryProvider();
|
|
2368
|
+
async get(id) {
|
|
2369
|
+
return this.options.store.get(id);
|
|
2838
2370
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2371
|
+
async list(filter) {
|
|
2372
|
+
return this.options.store.list(filter);
|
|
2373
|
+
}
|
|
2374
|
+
async mustGet(id) {
|
|
2375
|
+
const task = await this.options.store.get(id);
|
|
2376
|
+
if (!task) {
|
|
2377
|
+
throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
|
|
2845
2378
|
}
|
|
2846
|
-
return
|
|
2847
|
-
|
|
2848
|
-
|
|
2379
|
+
return task;
|
|
2380
|
+
}
|
|
2381
|
+
async transition(task, to, extra = {}) {
|
|
2382
|
+
const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
|
|
2383
|
+
if (!allowed.includes(to)) {
|
|
2384
|
+
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
2385
|
+
}
|
|
2386
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
2387
|
+
const updated = {
|
|
2388
|
+
...task,
|
|
2389
|
+
status: to,
|
|
2390
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2391
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2392
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2393
|
+
...executions !== void 0 ? { executions } : {},
|
|
2394
|
+
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
2395
|
+
};
|
|
2396
|
+
await this.options.store.save(updated);
|
|
2397
|
+
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated, {
|
|
2398
|
+
from: task.status
|
|
2849
2399
|
});
|
|
2400
|
+
return updated;
|
|
2850
2401
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2402
|
+
async audit(action, task, detail, executionId) {
|
|
2403
|
+
await this.options.audit?.record({
|
|
2404
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2405
|
+
action,
|
|
2406
|
+
id: task.id,
|
|
2407
|
+
taskId: task.id,
|
|
2408
|
+
...task.project !== void 0 ? { project: task.project } : {},
|
|
2409
|
+
...task.pipelineId !== void 0 ? { pipelineId: task.pipelineId } : {},
|
|
2410
|
+
...task.agent !== void 0 ? { agentId: task.agent } : {},
|
|
2411
|
+
...executionId !== void 0 ? { executionId } : {},
|
|
2412
|
+
...detail !== void 0 ? { detail } : {}
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
function generateTaskId() {
|
|
2417
|
+
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2855
2418
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2864
|
-
this.required = options.required ?? false;
|
|
2865
|
-
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
2419
|
+
function errorCode(error) {
|
|
2420
|
+
return error?.code ?? "";
|
|
2421
|
+
}
|
|
2422
|
+
function classifyThrown(error) {
|
|
2423
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2424
|
+
const dispatched = error.details?.dispatched;
|
|
2425
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
2866
2426
|
}
|
|
2867
|
-
|
|
2427
|
+
return "failed";
|
|
2428
|
+
}
|
|
2429
|
+
function sleep(ms) {
|
|
2430
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/audit/index.ts
|
|
2434
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2435
|
+
import { dirname as dirname2 } from "path";
|
|
2436
|
+
var FileAuditLog = class {
|
|
2437
|
+
filePath;
|
|
2868
2438
|
log;
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
return this.provider;
|
|
2873
|
-
}
|
|
2874
|
-
get providerName() {
|
|
2875
|
-
return this.provider.name;
|
|
2876
|
-
}
|
|
2877
|
-
get isRequired() {
|
|
2878
|
-
return this.required;
|
|
2439
|
+
constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
|
|
2440
|
+
this.filePath = `${directory}/audit.jsonl`;
|
|
2441
|
+
this.log = log.child({ component: "audit" });
|
|
2879
2442
|
}
|
|
2880
|
-
|
|
2881
|
-
async contextFor(query) {
|
|
2882
|
-
let entries;
|
|
2443
|
+
async record(event) {
|
|
2883
2444
|
try {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
2888
|
-
});
|
|
2445
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2446
|
+
await appendFile(this.filePath, `${JSON.stringify(event)}
|
|
2447
|
+
`, "utf8");
|
|
2889
2448
|
} catch (error) {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
return this.format(entries);
|
|
2893
|
-
}
|
|
2894
|
-
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2895
|
-
async gatherContext(query) {
|
|
2896
|
-
return (await this.contextFor(query)).text;
|
|
2897
|
-
}
|
|
2898
|
-
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
2899
|
-
async recordTaskOutcome(task) {
|
|
2900
|
-
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
2901
|
-
try {
|
|
2902
|
-
await this.provider.store({
|
|
2903
|
-
key: `task:${task.id}`,
|
|
2904
|
-
scope: "task",
|
|
2905
|
-
category: "tasks",
|
|
2906
|
-
content: `Objective: ${task.objective}
|
|
2907
|
-
Status: ${task.status}
|
|
2908
|
-
Outcome: ${outcome}`,
|
|
2909
|
-
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
2910
|
-
createdAt: task.createdAt
|
|
2911
|
-
});
|
|
2912
|
-
const significant = (task.result?.reports ?? []).flatMap(
|
|
2913
|
-
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
2449
|
+
this.log.warn(
|
|
2450
|
+
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2914
2451
|
);
|
|
2915
|
-
for (const { report, finding } of significant) {
|
|
2916
|
-
await this.provider.store({
|
|
2917
|
-
key: `finding:${report.id}:${finding.id}`,
|
|
2918
|
-
scope: "project",
|
|
2919
|
-
category: "problems",
|
|
2920
|
-
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
2921
|
-
tags: ["known-problem", report.agent]
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
2924
|
-
} catch (error) {
|
|
2925
|
-
this.handleFailure("store", error, void 0);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
format(entries) {
|
|
2929
|
-
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2930
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
2931
|
-
for (const entry2 of entries) {
|
|
2932
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2933
|
-
}
|
|
2934
|
-
const parts = [];
|
|
2935
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2936
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
2937
|
-
if (scopeEntries.length === 0) continue;
|
|
2938
|
-
const lines = scopeEntries.map((entry2) => {
|
|
2939
|
-
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2940
|
-
return `- ${category}${entry2.content}`;
|
|
2941
|
-
}).join("\n");
|
|
2942
|
-
parts.push(`### Relevant memory (${scope})
|
|
2943
|
-
${lines}`);
|
|
2944
2452
|
}
|
|
2945
|
-
let text = parts.join("\n\n");
|
|
2946
|
-
const truncated = text.length > this.maxContextChars;
|
|
2947
|
-
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2948
|
-
return { text, entries, truncated };
|
|
2949
2453
|
}
|
|
2950
|
-
/**
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2958
|
-
{ cause: error }
|
|
2959
|
-
);
|
|
2454
|
+
/** Recent events, oldest first; used by the CLI. */
|
|
2455
|
+
async recent(limit = 20) {
|
|
2456
|
+
let raw;
|
|
2457
|
+
try {
|
|
2458
|
+
raw = await readFile2(this.filePath, "utf8");
|
|
2459
|
+
} catch {
|
|
2460
|
+
return [];
|
|
2960
2461
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
register(role) {
|
|
2970
|
-
this.roles.set(role.id, role);
|
|
2971
|
-
}
|
|
2972
|
-
registerAll(roles) {
|
|
2973
|
-
for (const role of roles) this.register(role);
|
|
2974
|
-
}
|
|
2975
|
-
get(id) {
|
|
2976
|
-
return this.roles.get(id);
|
|
2977
|
-
}
|
|
2978
|
-
list() {
|
|
2979
|
-
return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2462
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
2463
|
+
try {
|
|
2464
|
+
return JSON.parse(line);
|
|
2465
|
+
} catch {
|
|
2466
|
+
return void 0;
|
|
2467
|
+
}
|
|
2468
|
+
}).filter((event) => event !== void 0);
|
|
2469
|
+
return events.slice(-limit);
|
|
2980
2470
|
}
|
|
2981
|
-
};
|
|
2982
|
-
function buildRoleRegistry(config) {
|
|
2983
|
-
const registry = new RoleRegistry();
|
|
2984
|
-
registry.registerAll(config.project.roles);
|
|
2985
|
-
return registry;
|
|
2986
|
-
}
|
|
2471
|
+
};
|
|
2987
2472
|
|
|
2988
2473
|
// src/tasks/engine-factory.ts
|
|
2989
2474
|
function createTaskEngine(config, log = logger, directory) {
|
|
@@ -3003,6 +2488,139 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
3003
2488
|
});
|
|
3004
2489
|
}
|
|
3005
2490
|
|
|
2491
|
+
// src/cli/commands/agents.ts
|
|
2492
|
+
var collectArgs = (value, previous) => [...previous, value];
|
|
2493
|
+
function registerAgentCommands(program) {
|
|
2494
|
+
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
2495
|
+
agent.command("list").description("List configured agents.").option("--json", "Emit agents as JSON.").action(async (options) => {
|
|
2496
|
+
const config = loadConfig();
|
|
2497
|
+
const agents = config.project.agents;
|
|
2498
|
+
if (options.json) {
|
|
2499
|
+
console.log(JSON.stringify(config.project.agents, null, 2));
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (agents.length === 0) {
|
|
2503
|
+
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
for (const agent2 of agents) {
|
|
2507
|
+
const command = agent2.command ?? "(no command)";
|
|
2508
|
+
console.log(
|
|
2509
|
+
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
agent.command("add").description(
|
|
2514
|
+
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
2515
|
+
).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(
|
|
2516
|
+
"--input-mode <mode>",
|
|
2517
|
+
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
2518
|
+
"stdin"
|
|
2519
|
+
).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(
|
|
2520
|
+
"--timeout-ms <ms>",
|
|
2521
|
+
"Per-task timeout in milliseconds.",
|
|
2522
|
+
(value) => Number(value)
|
|
2523
|
+
).action(async (options) => addAgent(options));
|
|
2524
|
+
agent.command("run <name> <objective>").description("Run a single task through this agent immediately.").action(async (name, objective) => {
|
|
2525
|
+
const config = loadConfig();
|
|
2526
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2527
|
+
if (!agentConfig) {
|
|
2528
|
+
throw new AgentError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
|
|
2529
|
+
}
|
|
2530
|
+
const engine = createTaskEngine(config);
|
|
2531
|
+
const task = await engine.create({ objective, agent: name, project: config.project.name });
|
|
2532
|
+
const finished = await engine.run(task.id);
|
|
2533
|
+
console.log(` id: ${finished.id}`);
|
|
2534
|
+
console.log(` status: ${finished.status}`);
|
|
2535
|
+
if (finished.result?.summary) console.log(` summary: ${finished.result.summary}`);
|
|
2536
|
+
if (finished.result?.error) console.log(` error: ${finished.result.error}`);
|
|
2537
|
+
process.exitCode = exitCodeForStatus(finished.status);
|
|
2538
|
+
});
|
|
2539
|
+
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
2540
|
+
registerAgentInspect(agent);
|
|
2541
|
+
}
|
|
2542
|
+
async function addAgent(options) {
|
|
2543
|
+
const candidate = agentConfigSchema.safeParse({
|
|
2544
|
+
name: options.name,
|
|
2545
|
+
type: "cli",
|
|
2546
|
+
command: options.command,
|
|
2547
|
+
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
2548
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2549
|
+
input_mode: options.inputMode,
|
|
2550
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2551
|
+
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2552
|
+
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2553
|
+
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2554
|
+
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2555
|
+
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
2556
|
+
});
|
|
2557
|
+
if (!candidate.success) {
|
|
2558
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2559
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
2560
|
+
}
|
|
2561
|
+
const newAgent = candidate.data;
|
|
2562
|
+
const existing = loadConfig();
|
|
2563
|
+
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
2564
|
+
throw new OmniNodeError(
|
|
2565
|
+
"CLI_USAGE",
|
|
2566
|
+
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
const configPath = appendToConfigList("agents", newAgent);
|
|
2570
|
+
console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
|
|
2571
|
+
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
2572
|
+
}
|
|
2573
|
+
async function testAgent(name) {
|
|
2574
|
+
const config = loadConfig();
|
|
2575
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2576
|
+
if (!agentConfig) {
|
|
2577
|
+
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
2578
|
+
throw new AgentError(
|
|
2579
|
+
"AGENT_NOT_FOUND",
|
|
2580
|
+
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
const agent = createAgent(agentConfig);
|
|
2584
|
+
console.log(
|
|
2585
|
+
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
2586
|
+
);
|
|
2587
|
+
const result = await agent.run({
|
|
2588
|
+
taskId: `test-${Date.now()}`,
|
|
2589
|
+
objective: "Reply with the single word: OK"
|
|
2590
|
+
});
|
|
2591
|
+
console.log(` status: ${result.status}`);
|
|
2592
|
+
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
2593
|
+
if (result.summary) console.log(` output: ${result.summary}`);
|
|
2594
|
+
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
2595
|
+
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
2596
|
+
}
|
|
2597
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
2598
|
+
if (result.status !== "completed") process.exitCode = 1;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/cli/commands/roles.ts
|
|
2602
|
+
function registerRoleCommands(program) {
|
|
2603
|
+
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
2604
|
+
role.command("list").description("List configured roles.").option("--json", "Emit roles as JSON.").action(async (options) => {
|
|
2605
|
+
const config = loadConfig();
|
|
2606
|
+
const roles = config.project.roles;
|
|
2607
|
+
if (options.json) {
|
|
2608
|
+
console.log(JSON.stringify(config.project.roles, null, 2));
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (roles.length === 0) {
|
|
2612
|
+
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
for (const role2 of roles) {
|
|
2616
|
+
console.log(`${role2.id} ${role2.name}`);
|
|
2617
|
+
for (const responsibility of role2.responsibilities) {
|
|
2618
|
+
console.log(` - ${responsibility}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
|
|
3006
2624
|
// src/cli/commands/tasks.ts
|
|
3007
2625
|
var collectFiles = (value, previous) => [...previous, value];
|
|
3008
2626
|
function registerTaskCommands(program) {
|
|
@@ -3030,7 +2648,7 @@ function registerTaskCommands(program) {
|
|
|
3030
2648
|
console.log(`Running task ${taskId}...`);
|
|
3031
2649
|
const finished = await engine.run(taskId);
|
|
3032
2650
|
printOutcome(finished);
|
|
3033
|
-
|
|
2651
|
+
process.exitCode = exitCodeForStatus(finished.status);
|
|
3034
2652
|
});
|
|
3035
2653
|
task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
|
|
3036
2654
|
const config = loadConfig();
|
|
@@ -3044,12 +2662,16 @@ function registerTaskCommands(program) {
|
|
|
3044
2662
|
}
|
|
3045
2663
|
printOutcome(found, { verbose: true });
|
|
3046
2664
|
});
|
|
3047
|
-
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) => {
|
|
2665
|
+
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) => {
|
|
3048
2666
|
const config = loadConfig();
|
|
3049
2667
|
const engine = createTaskEngine(config);
|
|
3050
2668
|
const tasks = await engine.list(
|
|
3051
2669
|
options.status !== void 0 ? { status: options.status } : {}
|
|
3052
2670
|
);
|
|
2671
|
+
if (options.json) {
|
|
2672
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
3053
2675
|
if (tasks.length === 0) {
|
|
3054
2676
|
console.log("No tasks found. Create one with `omninode task create`.");
|
|
3055
2677
|
return;
|
|
@@ -3073,8 +2695,9 @@ function registerTaskCommands(program) {
|
|
|
3073
2695
|
console.log(`Running task ${reset.id}...`);
|
|
3074
2696
|
const finished = await engine.run(reset.id);
|
|
3075
2697
|
printOutcome(finished);
|
|
3076
|
-
|
|
2698
|
+
process.exitCode = exitCodeForStatus(finished.status);
|
|
3077
2699
|
});
|
|
2700
|
+
registerTaskInspect(task);
|
|
3078
2701
|
}
|
|
3079
2702
|
function printOutcome(task, options = {}) {
|
|
3080
2703
|
console.log(` id: ${task.id}`);
|
|
@@ -3543,12 +3166,16 @@ function service() {
|
|
|
3543
3166
|
}
|
|
3544
3167
|
function registerReportCommands(program) {
|
|
3545
3168
|
const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
|
|
3546
|
-
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) => {
|
|
3169
|
+
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) => {
|
|
3547
3170
|
const svc = service();
|
|
3548
3171
|
const reports = await svc.storeRef.listReports({
|
|
3549
3172
|
...options.task !== void 0 ? { taskId: options.task } : {},
|
|
3550
3173
|
...options.agent !== void 0 ? { agent: options.agent } : {}
|
|
3551
3174
|
});
|
|
3175
|
+
if (options.json) {
|
|
3176
|
+
console.log(JSON.stringify(reports, null, 2));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3552
3179
|
if (reports.length === 0) {
|
|
3553
3180
|
console.log("No reports stored yet. Run a pipeline to collect some.");
|
|
3554
3181
|
return;
|
|
@@ -3557,11 +3184,15 @@ function registerReportCommands(program) {
|
|
|
3557
3184
|
console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
|
|
3558
3185
|
}
|
|
3559
3186
|
});
|
|
3560
|
-
report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
|
|
3187
|
+
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) => {
|
|
3561
3188
|
const svc = service();
|
|
3562
3189
|
const combined = await svc.storeRef.listCombined({
|
|
3563
3190
|
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
3564
3191
|
});
|
|
3192
|
+
if (options.json) {
|
|
3193
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3565
3196
|
if (combined.length === 0) {
|
|
3566
3197
|
console.log("No combined reports stored yet. Run a pipeline to generate one.");
|
|
3567
3198
|
return;
|
|
@@ -3641,6 +3272,10 @@ function registerReportCommands(program) {
|
|
|
3641
3272
|
});
|
|
3642
3273
|
}
|
|
3643
3274
|
|
|
3275
|
+
// src/cli/commands/pipelines.ts
|
|
3276
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3277
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
3278
|
+
|
|
3644
3279
|
// src/pipelines/engine.ts
|
|
3645
3280
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
3646
3281
|
|
|
@@ -4130,6 +3765,7 @@ var PipelineEngine = class {
|
|
|
4130
3765
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4131
3766
|
action: "pipeline.run.started",
|
|
4132
3767
|
id: run2.id,
|
|
3768
|
+
pipelineId: def.id,
|
|
4133
3769
|
detail: { pipeline: def.id, objective }
|
|
4134
3770
|
});
|
|
4135
3771
|
const deps = this.effectiveDependencies(def);
|
|
@@ -4170,7 +3806,9 @@ var PipelineEngine = class {
|
|
|
4170
3806
|
combinedContext: contextParts.join("\n\n"),
|
|
4171
3807
|
reports: [...allReports],
|
|
4172
3808
|
pipelineRunId: run2.id,
|
|
4173
|
-
runId: run2.id
|
|
3809
|
+
runId: run2.id,
|
|
3810
|
+
pipelineDefId: def.id,
|
|
3811
|
+
taskId: run2.id
|
|
4174
3812
|
};
|
|
4175
3813
|
return this.executeStep(step, ctx);
|
|
4176
3814
|
})
|
|
@@ -4247,8 +3885,9 @@ ${contribution}`);
|
|
|
4247
3885
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
4248
3886
|
await this.options.audit?.record({
|
|
4249
3887
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4250
|
-
action: "pipeline.run.finished",
|
|
3888
|
+
action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
|
|
4251
3889
|
id: run2.id,
|
|
3890
|
+
pipelineId: def.id,
|
|
4252
3891
|
detail: { pipeline: def.id, status: finalStatus }
|
|
4253
3892
|
});
|
|
4254
3893
|
return finished;
|
|
@@ -4329,6 +3968,7 @@ ${contribution}`);
|
|
|
4329
3968
|
agentNames.map(
|
|
4330
3969
|
(name) => this.options.tasks.create({
|
|
4331
3970
|
objective: ctx.objective,
|
|
3971
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4332
3972
|
agent: name,
|
|
4333
3973
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4334
3974
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4371,6 +4011,7 @@ ${contribution}`);
|
|
|
4371
4011
|
async runSingle(step, ctx, agentName) {
|
|
4372
4012
|
const task = await this.options.tasks.create({
|
|
4373
4013
|
objective: ctx.objective,
|
|
4014
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4374
4015
|
agent: agentName,
|
|
4375
4016
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4376
4017
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4441,7 +4082,13 @@ ${contribution}`);
|
|
|
4441
4082
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4442
4083
|
action: "plan.generated",
|
|
4443
4084
|
id: plan.id,
|
|
4444
|
-
|
|
4085
|
+
...ctx.pipelineDefId !== void 0 ? { pipelineId: ctx.pipelineDefId } : {},
|
|
4086
|
+
detail: {
|
|
4087
|
+
generatedBy: plan.generatedBy,
|
|
4088
|
+
steps: plan.steps.length,
|
|
4089
|
+
run: ctx.pipelineRunId,
|
|
4090
|
+
taskId: ctx.taskId
|
|
4091
|
+
}
|
|
4445
4092
|
});
|
|
4446
4093
|
return {
|
|
4447
4094
|
status: "completed",
|
|
@@ -4475,7 +4122,7 @@ function formatPlan(plan) {
|
|
|
4475
4122
|
}
|
|
4476
4123
|
return lines.join("\n");
|
|
4477
4124
|
}
|
|
4478
|
-
function createDefaultChatFn(providers) {
|
|
4125
|
+
function createDefaultChatFn(providers, options = {}) {
|
|
4479
4126
|
return async (modelRef, messages) => {
|
|
4480
4127
|
const separator = modelRef.indexOf(":");
|
|
4481
4128
|
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
@@ -4493,33 +4140,15 @@ function createDefaultChatFn(providers) {
|
|
|
4493
4140
|
`Provider "${providerName}" (model "${modelRef}") is not configured.`
|
|
4494
4141
|
);
|
|
4495
4142
|
}
|
|
4496
|
-
const provider = createProvider(providerConfig
|
|
4143
|
+
const provider = createProvider(providerConfig, {
|
|
4144
|
+
...options.audit !== void 0 ? { audit: options.audit } : {},
|
|
4145
|
+
...options.env !== void 0 ? { env: options.env } : {}
|
|
4146
|
+
});
|
|
4497
4147
|
const response = await provider.chat({ model: modelId, messages });
|
|
4498
4148
|
return response.content;
|
|
4499
4149
|
};
|
|
4500
4150
|
}
|
|
4501
4151
|
|
|
4502
|
-
// src/pipelines/store.ts
|
|
4503
|
-
var FilePipelineRunStore = class extends JsonFileStore {
|
|
4504
|
-
constructor(directory) {
|
|
4505
|
-
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
4506
|
-
}
|
|
4507
|
-
save(run2) {
|
|
4508
|
-
return this.mutate((runs) => {
|
|
4509
|
-
const index = runs.findIndex((r) => r.id === run2.id);
|
|
4510
|
-
if (index >= 0) runs[index] = run2;
|
|
4511
|
-
else runs.push(run2);
|
|
4512
|
-
return [runs, void 0];
|
|
4513
|
-
});
|
|
4514
|
-
}
|
|
4515
|
-
async get(id) {
|
|
4516
|
-
return (await this.readAll()).find((r) => r.id === id);
|
|
4517
|
-
}
|
|
4518
|
-
async list(filter = {}) {
|
|
4519
|
-
return (await this.readAll()).filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
|
|
4520
|
-
}
|
|
4521
|
-
};
|
|
4522
|
-
|
|
4523
4152
|
// src/planner/store.ts
|
|
4524
4153
|
var FilePlanStore = class extends JsonFileStore {
|
|
4525
4154
|
constructor(directory) {
|
|
@@ -4564,9 +4193,10 @@ function buildPlanner(plannerConfig, chat, log = logger) {
|
|
|
4564
4193
|
|
|
4565
4194
|
// src/pipelines/engine-factory.ts
|
|
4566
4195
|
function buildPipelineEngine(config, log = logger) {
|
|
4196
|
+
const auditLog = new FileAuditLog();
|
|
4567
4197
|
const agents = buildAgentRegistry(config, log);
|
|
4568
4198
|
const roles = buildRoleRegistry(config);
|
|
4569
|
-
const chat = createDefaultChatFn(config.project.providers);
|
|
4199
|
+
const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
|
|
4570
4200
|
const tasks = new TaskEngine({
|
|
4571
4201
|
agents,
|
|
4572
4202
|
roles,
|
|
@@ -4597,9 +4227,13 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
4597
4227
|
// src/cli/commands/pipelines.ts
|
|
4598
4228
|
function registerPipelineCommands(program) {
|
|
4599
4229
|
const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
|
|
4600
|
-
pipeline.command("list").description("List configured pipelines.").action(async () => {
|
|
4230
|
+
pipeline.command("list").description("List configured pipelines.").option("--json", "Emit pipelines as JSON.").action(async (options) => {
|
|
4601
4231
|
const config = loadConfig();
|
|
4602
4232
|
const pipelines = config.project.pipelines;
|
|
4233
|
+
if (options.json) {
|
|
4234
|
+
console.log(JSON.stringify(config.project.pipelines, null, 2));
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4603
4237
|
if (pipelines.length === 0) {
|
|
4604
4238
|
console.log("No pipelines configured. Add one under `project.pipelines` in omninode.yaml.");
|
|
4605
4239
|
return;
|
|
@@ -4640,6 +4274,9 @@ function registerPipelineCommands(program) {
|
|
|
4640
4274
|
console.log(`Cancellation requested for run ${runId}.`);
|
|
4641
4275
|
console.log("A run executing in another process stops at its next check point.");
|
|
4642
4276
|
});
|
|
4277
|
+
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) => {
|
|
4278
|
+
await createPipelineFromFile(id, options.from);
|
|
4279
|
+
});
|
|
4643
4280
|
pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
|
|
4644
4281
|
const config = loadConfig();
|
|
4645
4282
|
const def = findPipeline(config, id);
|
|
@@ -4694,6 +4331,7 @@ function registerPipelineCommands(program) {
|
|
|
4694
4331
|
console.log(` reports: ${run2.reports.length}`);
|
|
4695
4332
|
}
|
|
4696
4333
|
});
|
|
4334
|
+
registerPipelineInspect(pipeline);
|
|
4697
4335
|
}
|
|
4698
4336
|
async function runPipelineAction(id, objective) {
|
|
4699
4337
|
const config = loadConfig();
|
|
@@ -4722,11 +4360,45 @@ async function runPipelineAction(id, objective) {
|
|
|
4722
4360
|
console.log(` result: ${finished.resultSummary}`);
|
|
4723
4361
|
}
|
|
4724
4362
|
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
4725
|
-
|
|
4363
|
+
process.exitCode = exitCodeForStatus(finished.status);
|
|
4726
4364
|
}
|
|
4727
4365
|
function registerRunAlias(program) {
|
|
4728
4366
|
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));
|
|
4729
4367
|
}
|
|
4368
|
+
async function createPipelineFromFile(id, file) {
|
|
4369
|
+
const configPath = findConfigFile();
|
|
4370
|
+
if (!configPath) {
|
|
4371
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
4372
|
+
}
|
|
4373
|
+
let definition;
|
|
4374
|
+
try {
|
|
4375
|
+
definition = parseDocument3(readFileSync4(file, "utf8")).toJS();
|
|
4376
|
+
} catch (error) {
|
|
4377
|
+
throw new PipelineError("PIPELINE_INVALID", `Could not read pipeline definition from ${file}.`, {
|
|
4378
|
+
cause: error
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
const candidate = pipelineConfigSchema.safeParse({ id, ...definition });
|
|
4382
|
+
if (!candidate.success) {
|
|
4383
|
+
const issues = candidate.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
4384
|
+
throw new PipelineError("PIPELINE_INVALID", `Invalid pipeline definition: ${issues}`);
|
|
4385
|
+
}
|
|
4386
|
+
const engine = buildPipelineEngine(loadConfig());
|
|
4387
|
+
engine.validate({ id, ...candidate.data });
|
|
4388
|
+
const doc = parseDocument3(readFileSync4(configPath, "utf8"));
|
|
4389
|
+
if (doc.getIn(["project", "pipelines"]) === void 0) {
|
|
4390
|
+
doc.setIn(["project", "pipelines"], [candidate.data]);
|
|
4391
|
+
} else {
|
|
4392
|
+
doc.addIn(["project", "pipelines"], candidate.data);
|
|
4393
|
+
}
|
|
4394
|
+
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
4395
|
+
if (!validated.success) {
|
|
4396
|
+
throw new PipelineError("PIPELINE_INVALID", "Adding this pipeline would produce an invalid configuration.");
|
|
4397
|
+
}
|
|
4398
|
+
writeFileSync4(configPath, doc.toString(), "utf8");
|
|
4399
|
+
console.log(`Created pipeline "${id}" (${candidate.data.steps.length} step(s)) in ${configPath}.`);
|
|
4400
|
+
console.log(`Run it with: omninode pipeline run ${id} "<objective>"`);
|
|
4401
|
+
}
|
|
4730
4402
|
function isInterrupted(run2) {
|
|
4731
4403
|
return run2.status === "running" && run2.finishedAt === void 0;
|
|
4732
4404
|
}
|
|
@@ -4760,7 +4432,7 @@ function registerMemoryCommands(program) {
|
|
|
4760
4432
|
console.log(` ${scope}: ${count}`);
|
|
4761
4433
|
}
|
|
4762
4434
|
});
|
|
4763
|
-
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) => {
|
|
4435
|
+
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) => {
|
|
4764
4436
|
const config = loadConfig();
|
|
4765
4437
|
const svc = new MemoryService(createMemoryProvider(config.project.memory));
|
|
4766
4438
|
const entries = await svc.providerRef.query({
|
|
@@ -4768,6 +4440,10 @@ function registerMemoryCommands(program) {
|
|
|
4768
4440
|
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
4769
4441
|
limit: options.limit
|
|
4770
4442
|
});
|
|
4443
|
+
if (options.json) {
|
|
4444
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4771
4447
|
if (entries.length === 0) {
|
|
4772
4448
|
console.log("No relevant memory found.");
|
|
4773
4449
|
return;
|
|
@@ -4798,11 +4474,15 @@ function registerMemoryCommands(program) {
|
|
|
4798
4474
|
// src/cli/commands/plan.ts
|
|
4799
4475
|
function registerPlanCommands(program) {
|
|
4800
4476
|
const plan = program.command("plan").description("Inspect implementation plans produced by the planner layer (\xA718).");
|
|
4801
|
-
plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").action(async (options) => {
|
|
4477
|
+
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) => {
|
|
4802
4478
|
const store = new FilePlanStore();
|
|
4803
4479
|
const plans = await store.list({
|
|
4804
4480
|
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
4805
4481
|
});
|
|
4482
|
+
if (options.json) {
|
|
4483
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4806
4486
|
if (plans.length === 0) {
|
|
4807
4487
|
console.log("No plans stored yet. Run a pipeline with a plan step to generate one.");
|
|
4808
4488
|
return;
|
|
@@ -5069,6 +4749,122 @@ Trust model: agents execute with the permissions of the OS user running OmniNode
|
|
|
5069
4749
|
});
|
|
5070
4750
|
}
|
|
5071
4751
|
|
|
4752
|
+
// src/cli/commands/config.ts
|
|
4753
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4754
|
+
function registerConfigCommands(program) {
|
|
4755
|
+
const config = program.command("config").description("Inspect the project configuration (omninode.yaml).");
|
|
4756
|
+
config.command("path").description("Print the path of the configuration file in use.").action(() => {
|
|
4757
|
+
const path = findConfigFile();
|
|
4758
|
+
console.log(path ?? "no omninode.yaml found");
|
|
4759
|
+
});
|
|
4760
|
+
config.command("show").description("Print the effective configuration (credential references only, never values).").option("--json", "Emit the configuration as JSON.").action((options) => {
|
|
4761
|
+
const resolved = loadConfig();
|
|
4762
|
+
const payload = {
|
|
4763
|
+
project: resolved.project.name,
|
|
4764
|
+
providers: resolved.project.providers.map((p) => ({
|
|
4765
|
+
name: p.name,
|
|
4766
|
+
type: p.type,
|
|
4767
|
+
baseUrl: p.baseUrl,
|
|
4768
|
+
apiKeyEnvVar: p.apiKeyEnvVar ?? null
|
|
4769
|
+
})),
|
|
4770
|
+
agents: resolved.project.agents.map((a) => ({ name: a.name, command: a.command ?? null, inputMode: a.inputMode })),
|
|
4771
|
+
roles: resolved.project.roles.map((r) => r.id),
|
|
4772
|
+
pipelines: resolved.project.pipelines.map((p) => p.id),
|
|
4773
|
+
memory: resolved.project.memory ?? null,
|
|
4774
|
+
logging: resolved.logging ?? null
|
|
4775
|
+
};
|
|
4776
|
+
if (options.json) {
|
|
4777
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
console.log(`project: ${payload.project}`);
|
|
4781
|
+
console.log(`providers: ${payload.providers.map((p) => `${p.name} (${p.type}, key=${p.apiKeyEnvVar ? `$${p.apiKeyEnvVar}` : "none"})`).join(", ") || "none"}`);
|
|
4782
|
+
console.log(`agents: ${payload.agents.map((a) => a.name).join(", ") || "none"}`);
|
|
4783
|
+
console.log(`roles: ${payload.roles.join(", ") || "none"}`);
|
|
4784
|
+
console.log(`pipelines: ${payload.pipelines.join(", ") || "none"}`);
|
|
4785
|
+
console.log(`memory: ${payload.memory ? `${payload.memory.provider}${payload.memory.required ? " (required)" : ""}` : "not configured"}`);
|
|
4786
|
+
});
|
|
4787
|
+
config.command("validate").description("Validate the configuration file, including the inline-secret scan.").action(() => {
|
|
4788
|
+
const path = findConfigFile();
|
|
4789
|
+
if (!path) throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found.");
|
|
4790
|
+
const findings = scanForInlineSecrets(readFileSync5(path, "utf8"));
|
|
4791
|
+
if (findings.length > 0) {
|
|
4792
|
+
throw new OmniNodeError(
|
|
4793
|
+
"CONFIG_INVALID",
|
|
4794
|
+
`${path} contains ${findings.length} inline secret(s) (line ${findings.map((f) => f.line).join(", ")}). Use environment-variable references.`
|
|
4795
|
+
);
|
|
4796
|
+
}
|
|
4797
|
+
const resolved = loadConfig();
|
|
4798
|
+
console.log(`${path} is valid \u2014 project "${resolved.project.name}" with ${resolved.project.providers.length} provider(s), ${resolved.project.agents.length} agent(s), ${resolved.project.pipelines.length} pipeline(s).`);
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
// src/cli/commands/models.ts
|
|
4803
|
+
var CAPABILITIES = [
|
|
4804
|
+
"chat",
|
|
4805
|
+
"coding",
|
|
4806
|
+
"tools",
|
|
4807
|
+
"vision",
|
|
4808
|
+
"embeddings"
|
|
4809
|
+
];
|
|
4810
|
+
function registerModelsCommand(program) {
|
|
4811
|
+
for (const name of ["models", "model"]) {
|
|
4812
|
+
registerModelGroup(program, name);
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
function registerModelGroup(program, name) {
|
|
4816
|
+
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) => {
|
|
4817
|
+
const capability = options.capability;
|
|
4818
|
+
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
4819
|
+
throw new OmniNodeError(
|
|
4820
|
+
"CLI_USAGE",
|
|
4821
|
+
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
const config = loadConfig();
|
|
4825
|
+
const selected = config.project.providers.filter(
|
|
4826
|
+
(p) => providerName ? p.name === providerName : true
|
|
4827
|
+
);
|
|
4828
|
+
if (selected.length === 0) {
|
|
4829
|
+
if (options.json) {
|
|
4830
|
+
console.log("[]");
|
|
4831
|
+
return;
|
|
4832
|
+
}
|
|
4833
|
+
throw new OmniNodeError(
|
|
4834
|
+
"PROVIDER_NOT_FOUND",
|
|
4835
|
+
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
4836
|
+
);
|
|
4837
|
+
}
|
|
4838
|
+
const registry = new ModelRegistry();
|
|
4839
|
+
for (const providerConfig of selected) {
|
|
4840
|
+
try {
|
|
4841
|
+
const provider = createProvider(providerConfig);
|
|
4842
|
+
const discovered = await registry.discoverFrom(provider);
|
|
4843
|
+
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
4844
|
+
} catch (error) {
|
|
4845
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4846
|
+
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
const models = registry.list({
|
|
4850
|
+
...providerName !== void 0 ? { provider: providerName } : {},
|
|
4851
|
+
...capability !== void 0 ? { capability } : {}
|
|
4852
|
+
});
|
|
4853
|
+
if (options.json) {
|
|
4854
|
+
console.log(JSON.stringify(models, null, 2));
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
if (models.length === 0) {
|
|
4858
|
+
console.log("No models found.");
|
|
4859
|
+
return;
|
|
4860
|
+
}
|
|
4861
|
+
console.log("");
|
|
4862
|
+
for (const model of models) {
|
|
4863
|
+
console.log(`${model.provider}:${model.id}`);
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4866
|
+
}
|
|
4867
|
+
|
|
5072
4868
|
// src/cli/index.ts
|
|
5073
4869
|
function createProgram() {
|
|
5074
4870
|
const program = new Command();
|
|
@@ -5090,10 +4886,21 @@ function createProgram() {
|
|
|
5090
4886
|
registerAuditCommand(program);
|
|
5091
4887
|
registerMigrateCommand(program);
|
|
5092
4888
|
registerSecurityCommand(program);
|
|
4889
|
+
registerConfigCommands(program);
|
|
5093
4890
|
registerRunAlias(program);
|
|
5094
4891
|
return program;
|
|
5095
4892
|
}
|
|
4893
|
+
function applyLoggingConfig() {
|
|
4894
|
+
try {
|
|
4895
|
+
if (findConfigFile()) {
|
|
4896
|
+
const logging = loadConfig().logging;
|
|
4897
|
+
if (logging) configureLogger(logging);
|
|
4898
|
+
}
|
|
4899
|
+
} catch {
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
5096
4902
|
async function run(argv) {
|
|
4903
|
+
applyLoggingConfig();
|
|
5097
4904
|
const program = createProgram();
|
|
5098
4905
|
try {
|
|
5099
4906
|
await program.parseAsync(argv);
|
|
@@ -5102,15 +4909,16 @@ async function run(argv) {
|
|
|
5102
4909
|
}
|
|
5103
4910
|
}
|
|
5104
4911
|
function handleError(error) {
|
|
4912
|
+
const code = exitCodeFor(error);
|
|
5105
4913
|
if (isOmniNodeError(error)) {
|
|
5106
4914
|
console.error(`error: [${error.code}] ${error.message}`);
|
|
5107
4915
|
if (error.details !== void 0) {
|
|
5108
4916
|
console.error(JSON.stringify(error.details, null, 2));
|
|
5109
4917
|
}
|
|
5110
|
-
process.exit(
|
|
4918
|
+
process.exit(code);
|
|
5111
4919
|
}
|
|
5112
4920
|
console.error(error instanceof Error ? error.message : String(error));
|
|
5113
|
-
process.exit(
|
|
4921
|
+
process.exit(code);
|
|
5114
4922
|
}
|
|
5115
4923
|
var entry = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : "";
|
|
5116
4924
|
if (import.meta.url === entry) {
|