@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/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.13";
26
+ var OMNINODE_VERSION = "2.0.0-alpha.15";
10
27
 
11
- // src/errors/index.ts
12
- var OmniNodeError = class extends Error {
13
- code;
14
- details;
15
- constructor(code, message, options = {}) {
16
- super(message, { cause: options.cause });
17
- this.name = new.target.name;
18
- this.code = code;
19
- this.details = options.details;
20
- }
21
- };
22
- var ConfigError = class extends OmniNodeError {
23
- constructor(code, message, options = {}) {
24
- super(code, message, options);
25
- }
26
- };
27
- var ProviderError = class extends OmniNodeError {
28
- constructor(code, message, options = {}) {
29
- super(code, message, options);
30
- }
31
- };
32
- 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
- var MemoryError = class extends OmniNodeError {
53
- constructor(message, options = {}) {
54
- super("MEMORY_UNAVAILABLE", message, options);
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({ level: z.enum(["debug", "info", "warn", "error"]) }).strict().optional()
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/auth.ts
393
- function resolveApiKey(config, env = process.env) {
394
- if (!config.apiKeyEnvVar) return void 0;
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/cli/commands/providers.ts
1049
- function registerProviderCommands(program) {
1050
- const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
1051
- provider.command("list").description("List configured providers.").action(async () => {
1052
- const config = loadConfig();
1053
- const providers = config.project.providers;
1054
- if (providers.length === 0) {
1055
- console.log(
1056
- "No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
1057
- );
1058
- return;
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
- async function addProvider(options) {
1087
- const configPath = findConfigFile();
1088
- if (!configPath) {
1089
- throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
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 DEFAULT_TIMEOUT_MS2 = 6e5;
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 ?? DEFAULT_TIMEOUT_MS2;
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/cli/commands/agents.ts
2039
- var collectArgs = (value, previous) => [...previous, value];
2040
- function registerAgentCommands(program) {
2041
- const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2042
- agent.command("list").description("List configured agents.").action(async () => {
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
- const newAgent = candidate.data;
2089
- const existing = loadConfig();
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
- const configPath = appendToConfigList("agents", newAgent);
2097
- console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
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
- const agent = createAgent(agentConfig);
2111
- console.log(
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
- if (result.error) console.log(` error: ${result.error}`);
2125
- if (result.status !== "completed") process.exitCode = 1;
2126
- }
2127
-
2128
- // src/cli/commands/roles.ts
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
- const handle = await open(tmp, "w");
2155
- try {
2156
- await handle.writeFile(contents, "utf8");
2157
- await handle.sync();
2158
- } finally {
2159
- await handle.close();
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
- try {
2163
- const dir = await open(dirname(target), "r");
1356
+ if (durability === "fsync") {
2164
1357
  try {
2165
- await dir.sync();
2166
- } finally {
2167
- await dir.close();
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/tasks/store.ts
2283
- var FileTaskStore = class extends JsonFileStore {
2284
- constructor(directory) {
2285
- super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
2286
- }
2287
- save(task) {
2288
- return this.mutate((tasks) => {
2289
- const index = tasks.findIndex((t) => t.id === task.id);
2290
- if (index >= 0) tasks[index] = task;
2291
- else tasks.push(task);
2292
- return [tasks, void 0];
2293
- });
2294
- }
2295
- async get(id) {
2296
- const tasks = await this.readAll();
2297
- return tasks.find((t) => t.id === id);
2298
- }
2299
- async list(filter = {}) {
2300
- const tasks = await this.readAll();
2301
- 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));
2302
- }
2303
- };
2304
-
2305
- // src/tasks/engine.ts
2306
- import { randomBytes } from "crypto";
2307
- var ALLOWED_TRANSITIONS = {
2308
- created: ["queued", "running", "cancelled"],
2309
- queued: ["running", "cancelled"],
2310
- running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
2311
- waiting: ["running", "cancelled"],
2312
- completed: [],
2313
- // failed → running is the only re-entry: bounded automatic retry (§6.4).
2314
- failed: ["running"],
2315
- timed_out: [],
2316
- cancelled: [],
2317
- unknown: [],
2318
- partially_completed: []
2319
- };
2320
- var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
2321
- var EXECUTION_HISTORY_LIMIT = 10;
2322
- var TaskEngine = class {
2323
- constructor(options) {
2324
- this.options = options;
2325
- this.log = (options.log ?? logger).child({ component: "tasks" });
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
- options;
2328
- log;
2329
- get store() {
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
- async create(input) {
2333
- if (!input.objective || input.objective.trim().length === 0) {
2334
- throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
2335
- }
2336
- if (input.role && !this.options.roles.get(input.role)) {
2337
- throw new TaskError(
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
- async queue(id) {
2367
- const task = await this.mustGet(id);
2368
- return this.transition(task, "queued");
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
- /** Runs the task through its assigned agent and records the result.
2371
- * `maxAttempts` enables bounded automatic retries of *retryable* errors
2372
- * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
2373
- async run(id, options = {}) {
2374
- const task = await this.mustGet(id);
2375
- if (task.status !== "created" && task.status !== "queued") {
2376
- throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
2377
- }
2378
- if (!task.agent) {
2379
- throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
2380
- }
2381
- const agent = this.options.agents.get(task.agent);
2382
- if (!agent) {
2383
- throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
2384
- }
2385
- const role = task.role ? this.options.roles.get(task.role) : void 0;
2386
- this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
2387
- let background = task.context?.background;
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 maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2399
- const backoffMs = options.retryBackoffMs ?? 0;
2400
- let current = task;
2401
- for (let i = 0; i < maxAttempts; i += 1) {
2402
- const attempt = current.attempt + 1;
2403
- const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2404
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2405
- current = await this.transition(current, "running", { attempt });
2406
- let output;
2407
- let thrown;
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
- const finalStatus = output.status === "completed" ? "completed" : "failed";
2457
- current = await this.transition(current, finalStatus, {
2458
- attempt,
2459
- result,
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 cancel(id) {
2471
- const task = await this.mustGet(id);
2472
- if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
2473
- throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
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
- if (task.agent) {
2476
- const agent = this.options.agents.get(task.agent);
2477
- await agent?.cancel?.(task.id);
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.transition(task, "cancelled");
1661
+ return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
2480
1662
  }
2481
- /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
2482
- async retry(id) {
2483
- const task = await this.mustGet(id);
2484
- const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
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 get(id) {
2503
- return this.options.store.get(id);
2504
- }
2505
- async list(filter) {
2506
- return this.options.store.list(filter);
2507
- }
2508
- async mustGet(id) {
2509
- const task = await this.options.store.get(id);
2510
- if (!task) {
2511
- throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
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
- async transition(task, to, extra = {}) {
2516
- const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
2517
- if (!allowed.includes(to)) {
2518
- throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
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 executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
2521
- const updated = {
2522
- ...task,
2523
- status: to,
2524
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2525
- ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2526
- ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2527
- ...executions !== void 0 ? { executions } : {},
2528
- ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
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
- function generateTaskId() {
2546
- return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2547
- }
2548
- function errorCode(error) {
2549
- return error?.code ?? "";
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
- return "failed";
2557
- }
2558
- function sleep(ms) {
2559
- return new Promise((resolve2) => setTimeout(resolve2, ms));
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/audit/index.ts
2563
- import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2564
- import { dirname as dirname2 } from "path";
2565
- var FileAuditLog = class {
2566
- filePath;
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
- constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
2569
- this.filePath = `${directory}/audit.jsonl`;
2570
- this.log = log.child({ component: "audit" });
1740
+ required;
1741
+ maxContextChars;
1742
+ get providerRef() {
1743
+ return this.provider;
2571
1744
  }
2572
- async record(event) {
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 mkdir2(dirname2(this.filePath), { recursive: true });
2575
- const { open: open2 } = await import("fs/promises");
2576
- const handle = await open2(this.filePath, "a");
2577
- try {
2578
- await handle.appendFile(`${JSON.stringify(event)}
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.log.warn(
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
- /** Recent events, oldest first; used by the CLI. */
2591
- async recent(limit = 20) {
2592
- let raw;
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
- raw = await readFile2(this.filePath, "utf8");
2595
- } catch {
2596
- return [];
2597
- }
2598
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
2599
- try {
2600
- return JSON.parse(line);
2601
- } catch {
2602
- return void 0;
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
- }).filter((event) => event !== void 0);
2605
- return events.slice(-limit);
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/memory/base.ts
2610
- function defaultSearch(provider, text, options = {}) {
2611
- return provider.retrieve({
2612
- text,
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
- let tagMatches = 0;
2678
- for (const tag of entry2.tags ?? []) {
2679
- if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
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
- return overlap * 2 + tagMatches * 1.5;
2682
- }
2683
- var LocalMemoryProvider = class extends JsonFileStore {
2684
- name = "local";
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: "memory.json" });
1861
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
2687
1862
  }
2688
- metadata() {
2689
- return defaultMetadata(this.name, {
2690
- capabilities: ["retrieve", "store", "search", "categories", "tags"],
2691
- relevanceRanking: true,
2692
- backend: { kind: "json-file", deterministicScoring: true }
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
- search(text, options = {}) {
2696
- return defaultSearch(this, text, options);
1871
+ async get(id) {
1872
+ const tasks = await this.readAll();
1873
+ return tasks.find((t) => t.id === id);
2697
1874
  }
2698
- /** @deprecated v1 alias of {@link retrieve}. */
2699
- query(query) {
2700
- return this.retrieve(query);
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
- /** @deprecated v1 alias of {@link store}. */
2703
- write(entry2) {
2704
- return this.store(entry2);
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
- async retrieve(query) {
2707
- const entries = await this.readAll();
2708
- const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
2709
- (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
2710
- );
2711
- if (!query.text || query.text.trim().length === 0) {
2712
- return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
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
- const queryKeywords = keywords(query.text);
2715
- 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);
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
- store(entry2) {
2718
- return this.mutate((entries) => {
2719
- const index = entries.findIndex((e) => e.key === entry2.key);
2720
- const stamped = {
2721
- ...entry2,
2722
- createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2723
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2724
- };
2725
- if (index >= 0) entries[index] = stamped;
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
- async count() {
2731
- return (await this.readAll()).length;
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
- // src/memory/remembera.ts
2736
- var RememberaMemoryProvider = class {
2737
- name = "remembera";
2738
- baseUrl;
2739
- apiKeyEnvVar;
2740
- env;
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
- get authenticationConfigured() {
2749
- return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
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
- headers() {
2752
- const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
2753
- return key ? { authorization: `Bearer ${key}` } : {};
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
- /** Search delegates to the gateway's relevance query. */
2756
- search(text, options = {}) {
2757
- return defaultSearch(
2758
- this,
2759
- text,
2760
- {
2761
- ...options.scope !== void 0 ? { scope: options.scope } : {},
2762
- ...options.limit !== void 0 ? { limit: options.limit } : {}
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
- metadata() {
2767
- return defaultMetadata(this.name, {
2768
- capabilities: ["retrieve", "store", "search", "categories", "tags"],
2769
- relevanceRanking: true,
2770
- backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
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
- /** @deprecated v1 alias of {@link retrieve}. */
2774
- query(query) {
2775
- return this.retrieve(query);
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
- /** @deprecated v1 alias of {@link store}. */
2778
- write(entry2) {
2779
- return this.store(entry2);
2170
+ options;
2171
+ log;
2172
+ get store() {
2173
+ return this.options.store;
2780
2174
  }
2781
- async retrieve(query) {
2782
- const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
2783
- if (status !== 200) {
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
- const entries = body.entries;
2787
- if (!Array.isArray(entries)) {
2788
- throw new MemoryError("Remembera query response had no entries array.");
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
- return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
2791
- }
2792
- async store(entry2) {
2793
- const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
2794
- if (status !== 200 && status !== 201) {
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 request(url, body) {
2799
- const headers = this.headers();
2800
- try {
2801
- return await requestJson(url, {
2802
- method: "POST",
2803
- headers,
2804
- body
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
- } catch (error) {
2807
- const message = error instanceof Error ? error.message : String(error);
2808
- throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
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
- toEntry(raw) {
2812
- if (raw === null || typeof raw !== "object") {
2813
- this.log.warn("Skipping malformed Remembera memory entry.");
2814
- return void 0;
2815
- }
2816
- const entry2 = raw;
2817
- if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
2818
- this.log.warn("Skipping Remembera memory entry without key/content.");
2819
- return void 0;
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 scope = entry2.scope;
2822
- return {
2823
- key: entry2.key,
2824
- content: entry2.content,
2825
- scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
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
- if (config.provider === "remembera") {
2840
- if (!config.baseUrl) {
2841
- throw new ConfigError(
2842
- "CONFIG_INVALID",
2843
- `Memory provider "remembera" needs a base_url in omninode.yaml.`
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 new RememberaMemoryProvider({
2847
- baseUrl: config.baseUrl,
2848
- apiKeyEnvVar: config.apiKeyEnvVar
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
- throw new ConfigError(
2852
- "CONFIG_INVALID",
2853
- `Unknown memory provider "${config.provider}". Available: local, remembera.`
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
- // src/memory/service.ts
2858
- var DEFAULT_ENTRY_LIMIT = 8;
2859
- var MAX_CONTEXT_CHARS = 4e3;
2860
- var MemoryService = class {
2861
- constructor(provider, log = logger, options = {}) {
2862
- this.provider = provider;
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
- provider;
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
- required;
2870
- maxContextChars;
2871
- get providerRef() {
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
- /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2881
- async contextFor(query) {
2882
- let entries;
2443
+ async record(event) {
2883
2444
  try {
2884
- entries = await this.provider.retrieve({
2885
- text: query.objective,
2886
- ...query.role ? { tags: [query.role] } : {},
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
- return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
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
- * Failure policy (§12): continue unless memory is explicitly required.
2952
- */
2953
- handleFailure(operation, error, fallback) {
2954
- const message = error instanceof Error ? error.message : String(error);
2955
- if (this.required) {
2956
- throw new MemoryError(
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
- this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2962
- return fallback ?? { text: "", entries: [], truncated: false };
2963
- }
2964
- };
2965
-
2966
- // src/roles/index.ts
2967
- var RoleRegistry = class {
2968
- roles = /* @__PURE__ */ new Map();
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
- if (finished.status !== "completed") process.exitCode = 1;
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
- if (finished.status !== "completed") process.exitCode = 1;
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
- detail: { generatedBy: plan.generatedBy, steps: plan.steps.length }
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
- if (finished.status !== "completed") process.exitCode = 1;
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(1);
4918
+ process.exit(code);
5111
4919
  }
5112
4920
  console.error(error instanceof Error ? error.message : String(error));
5113
- process.exit(1);
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) {