@hilbras/omninode 2.0.0-alpha.12 → 2.0.0-alpha.14

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,60 +23,7 @@ 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.12";
10
-
11
- // src/errors/index.ts
12
- var OmniNodeError = class extends Error {
13
- code;
14
- details;
15
- constructor(code, message, options = {}) {
16
- super(message, { cause: options.cause });
17
- this.name = new.target.name;
18
- this.code = code;
19
- this.details = options.details;
20
- }
21
- };
22
- var ConfigError = class extends OmniNodeError {
23
- constructor(code, message, options = {}) {
24
- super(code, message, options);
25
- }
26
- };
27
- var ProviderError = class extends OmniNodeError {
28
- constructor(code, message, options = {}) {
29
- super(code, message, options);
30
- }
31
- };
32
- 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
- }
51
- };
52
- var MemoryError = class extends OmniNodeError {
53
- constructor(message, options = {}) {
54
- super("MEMORY_UNAVAILABLE", message, options);
55
- }
56
- };
57
- function isOmniNodeError(value) {
58
- return value instanceof OmniNodeError;
59
- }
60
-
61
- // src/cli/commands/init.ts
62
- import { existsSync as existsSync2, writeFileSync } from "fs";
26
+ var OMNINODE_VERSION = "2.0.0-alpha.14";
63
27
 
64
28
  // src/config/schema.ts
65
29
  import { z } from "zod";
@@ -142,9 +106,45 @@ var projectConfigSchema = z.object({
142
106
  }).strict();
143
107
  var appConfigSchema = z.object({
144
108
  project: projectConfigSchema,
145
- logging: z.object({ level: z.enum(["debug", "info", "warn", "error"]) }).strict().optional()
109
+ logging: z.object({
110
+ level: z.enum(["debug", "info", "warn", "error"]),
111
+ format: z.enum(["text", "json"]).optional()
112
+ }).strict().optional()
146
113
  }).strict();
147
114
 
115
+ // src/config/secrets.ts
116
+ var SAFE_KEY_SUFFIXES = ["_env", "_env_var", "_file", "_path", "_ref", "_id", "_url", "_command"];
117
+ var SECRET_KEY = /^\s*([A-Za-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|credential)[A-Za-z0-9_-]*)\s*:\s*(.*)$/i;
118
+ var SAFE_VALUE = /^\s*(?:\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*|<[^>]*>|""|''|null|~)\s*$/i;
119
+ function scanForInlineSecrets(source) {
120
+ const findings = [];
121
+ source.split("\n").forEach((line, index) => {
122
+ const trimmed = line.trim();
123
+ if (trimmed.startsWith("#")) return;
124
+ const match = SECRET_KEY.exec(line);
125
+ if (!match) return;
126
+ const key = match[1] ?? "";
127
+ const rawValue = (match[2] ?? "").trim().replace(/,\s*$/, "");
128
+ const keyIsReference = SAFE_KEY_SUFFIXES.some((suffix) => key.toLowerCase().endsWith(suffix));
129
+ if (keyIsReference) return;
130
+ const value = rawValue.replace(/^["']|["']$/g, "");
131
+ if (value.length === 0 || SAFE_VALUE.test(rawValue)) return;
132
+ if (value.length < 8) return;
133
+ findings.push({
134
+ line: index + 1,
135
+ key,
136
+ excerpt: `${key}: ${value.slice(0, 3)}\u2026 (${value.length} chars, redacted)`
137
+ });
138
+ });
139
+ return findings;
140
+ }
141
+ function describeSecretFindings(file, findings) {
142
+ const lines = findings.map((f) => ` line ${f.line}: ${f.excerpt}`).join("\n");
143
+ return `${file} appears to contain ${findings.length} inline secret(s):
144
+ ${lines}
145
+ OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
146
+ }
147
+
148
148
  // src/config/loader.ts
149
149
  import { existsSync, readFileSync } from "fs";
150
150
  import { parse as parseYaml } from "yaml";
@@ -181,6 +181,10 @@ function loadConfig(options = {}) {
181
181
  );
182
182
  }
183
183
  const raw = readFileSync(path, "utf8");
184
+ const secretFindings = scanForInlineSecrets(raw);
185
+ if (secretFindings.length > 0) {
186
+ throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
187
+ }
184
188
  const expanded = expandEnvRefs(raw, env, path);
185
189
  let data;
186
190
  try {
@@ -327,6 +331,7 @@ function defaultProjectConfigYaml(projectName = "My Project") {
327
331
  }
328
332
 
329
333
  // src/cli/commands/init.ts
334
+ import { existsSync as existsSync2, writeFileSync } from "fs";
330
335
  async function runInit(options) {
331
336
  const target = `${process.cwd()}/omninode.yaml`;
332
337
  if (!options.force && existsSync2(target)) {
@@ -352,578 +357,6 @@ function registerProjectCommands(program) {
352
357
  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));
353
358
  }
354
359
 
355
- // src/providers/auth.ts
356
- function resolveApiKey(config, env = process.env) {
357
- if (!config.apiKeyEnvVar) return void 0;
358
- const value = env[config.apiKeyEnvVar];
359
- if (!value) {
360
- throw new ProviderError(
361
- "PROVIDER_AUTH_FAILED",
362
- `Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
363
- );
364
- }
365
- return value;
366
- }
367
- function authHeaders(config, env = process.env) {
368
- const key = resolveApiKey(config, env);
369
- const base = { ...config.headers ?? {} };
370
- return key ? { ...base, authorization: `Bearer ${key}` } : base;
371
- }
372
-
373
- // src/providers/http.ts
374
- var HttpResponseError = class extends Error {
375
- constructor(message, status, cause) {
376
- super(message, { cause });
377
- this.status = status;
378
- this.name = "HttpResponseError";
379
- }
380
- status;
381
- };
382
- var DEFAULT_TIMEOUT_MS = 3e4;
383
- async function requestJson(url, options = {}) {
384
- const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
385
- const response = await fetch(url, {
386
- method,
387
- headers: {
388
- accept: "application/json",
389
- ...body !== void 0 ? { "content-type": "application/json" } : {},
390
- ...headers
391
- },
392
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
393
- signal: AbortSignal.timeout(timeoutMs)
394
- });
395
- let parsed;
396
- try {
397
- parsed = await response.json();
398
- } catch (error) {
399
- throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
400
- }
401
- return { status: response.status, body: parsed };
402
- }
403
-
404
- // src/logger/index.ts
405
- var LEVEL_ORDER = {
406
- debug: 10,
407
- info: 20,
408
- warn: 30,
409
- error: 40
410
- };
411
- var ConsoleLogSink = class {
412
- constructor(stream = process.stderr) {
413
- this.stream = stream;
414
- }
415
- stream;
416
- write(_level, line) {
417
- this.stream.write(line + "\n");
418
- }
419
- };
420
- var Logger = class _Logger {
421
- level;
422
- sink;
423
- context;
424
- constructor(options = {}) {
425
- this.level = options.level ?? "info";
426
- this.sink = options.sink ?? new ConsoleLogSink();
427
- this.context = options.context ?? {};
428
- }
429
- child(context) {
430
- return new _Logger({
431
- level: this.level,
432
- sink: this.sink,
433
- context: { ...this.context, ...context }
434
- });
435
- }
436
- debug(message, data) {
437
- this.log("debug", message, data);
438
- }
439
- info(message, data) {
440
- this.log("info", message, data);
441
- }
442
- warn(message, data) {
443
- this.log("warn", message, data);
444
- }
445
- error(message, data) {
446
- this.log("error", message, data);
447
- }
448
- log(level, message, data) {
449
- if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
450
- const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
451
- const merged = { ...this.context, ...data };
452
- if (Object.keys(merged).length > 0) {
453
- parts.push(JSON.stringify(merged));
454
- }
455
- this.sink.write(level, parts.join(" "));
456
- }
457
- };
458
- var logger = new Logger();
459
-
460
- // src/providers/errors.ts
461
- function kindToCode(kind) {
462
- switch (kind) {
463
- case "AUTHENTICATION_ERROR":
464
- return "PROVIDER_AUTH_FAILED";
465
- case "MODEL_NOT_FOUND":
466
- return "MODEL_NOT_FOUND";
467
- default:
468
- return "PROVIDER_UNAVAILABLE";
469
- }
470
- }
471
- function isRetryable(kind) {
472
- return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
473
- }
474
- function kindFromStatus(status) {
475
- if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
476
- if (status === 429) return "RATE_LIMIT_ERROR";
477
- if (status === 404) return "MODEL_NOT_FOUND";
478
- if (status === 408 || status === 504) return "TIMEOUT";
479
- if (status >= 500) return "SERVER_ERROR";
480
- if (status >= 400) return "INVALID_REQUEST";
481
- return "UNKNOWN_ERROR";
482
- }
483
- function extractMessage(body) {
484
- if (body === null || typeof body !== "object") return void 0;
485
- const root = body;
486
- const error = root.error ?? root;
487
- const message = error.message ?? error.detail ?? root.message;
488
- return typeof message === "string" ? message : void 0;
489
- }
490
- function parseRetryAfter(value, now = Date.now()) {
491
- if (!value) return void 0;
492
- const seconds = Number(value);
493
- if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
494
- const date = Date.parse(value);
495
- return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
496
- }
497
- function isTimeoutError(error) {
498
- return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
499
- }
500
- function fallbackMessage(kind, context, status) {
501
- const { provider, operation } = context;
502
- switch (kind) {
503
- case "AUTHENTICATION_ERROR":
504
- return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
505
- case "RATE_LIMIT_ERROR":
506
- return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
507
- case "MODEL_NOT_FOUND":
508
- return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
509
- case "TIMEOUT":
510
- return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
511
- case "SERVER_ERROR":
512
- return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
513
- case "INVALID_REQUEST":
514
- return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
515
- default:
516
- return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
517
- }
518
- }
519
- function providerHttpError(context) {
520
- const status = context.status ?? 0;
521
- const kind = kindFromStatus(status);
522
- const detail = extractMessage(context.body);
523
- const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
524
- return new ProviderError(
525
- context.code ?? kindToCode(kind),
526
- detail ?? fallbackMessage(kind, context, status),
527
- {
528
- cause: context.cause,
529
- details: {
530
- kind,
531
- provider: context.provider,
532
- operation: context.operation,
533
- status,
534
- retryable: isRetryable(kind),
535
- ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
536
- ...detail ? { providerMessage: detail } : {}
537
- }
538
- }
539
- );
540
- }
541
- function providerTransportError(context) {
542
- const cause = context.cause;
543
- const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
544
- const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
545
- return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
546
- cause: context.cause,
547
- details: {
548
- kind,
549
- provider: context.provider,
550
- operation: context.operation,
551
- retryable: isRetryable(kind)
552
- }
553
- });
554
- }
555
-
556
- // src/providers/openai-compatible/index.ts
557
- var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
558
- var OpenAICompatibleProvider = class {
559
- config;
560
- providerId;
561
- authentication;
562
- log;
563
- /** Discovered models, so getModel() answers without another round-trip. */
564
- discovered;
565
- env;
566
- constructor(config, log = logger, env = process.env) {
567
- this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
568
- this.env = env;
569
- this.providerId = config.providerId ?? config.name;
570
- this.authentication = {
571
- method: config.apiKeyEnvVar ? "bearer" : "none",
572
- ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
573
- configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
574
- };
575
- this.log = log.child({ provider: config.name });
576
- }
577
- /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
578
- get requestTimeoutMs() {
579
- return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
580
- }
581
- /** Authenticate + discover (roadmap §10, Model Discovery). */
582
- async connect() {
583
- authHeaders(this.config, this.env);
584
- const models = await this.listModels();
585
- this.discovered = models;
586
- return models;
587
- }
588
- async getModel(modelId) {
589
- const models = this.discovered ?? await this.connect();
590
- return models.find((m) => m.id === modelId);
591
- }
592
- /** Raw /models body (protected so adapters can capture gateway-level metadata). */
593
- async rawModelsResponse() {
594
- const response = await this.modelsRequest();
595
- return response.body;
596
- }
597
- async modelsRequest() {
598
- let response;
599
- try {
600
- response = await requestJson(`${this.config.baseUrl}/models`, {
601
- headers: authHeaders(this.config, this.env),
602
- timeoutMs: this.requestTimeoutMs
603
- });
604
- } catch (error) {
605
- if (error instanceof ProviderError) throw error;
606
- throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
607
- }
608
- const { status, body } = response;
609
- if (status !== 200) {
610
- throw providerHttpError({
611
- provider: this.config.name,
612
- operation: "listModels",
613
- status,
614
- body
615
- });
616
- }
617
- return response;
618
- }
619
- async listModels() {
620
- const body = await this.rawModelsResponse();
621
- const entries = extractModelEntries(body, this.config);
622
- const models = [];
623
- let skipped = 0;
624
- for (const entry2 of entries) {
625
- if (typeof entry2.id !== "string" || entry2.id.length === 0) {
626
- skipped += 1;
627
- continue;
628
- }
629
- models.push(this.toModelInfo(entry2));
630
- }
631
- if (skipped > 0) {
632
- this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
633
- }
634
- return models;
635
- }
636
- /** Maps a raw model entry onto the normalized metadata shape (§10). */
637
- toModelInfo(entry2) {
638
- const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
639
- const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
640
- const inputTypes = normalizeModalities(entry2.input_modalities);
641
- const outputTypes = normalizeModalities(entry2.output_modalities);
642
- return {
643
- provider: this.config.name,
644
- id: entry2.id,
645
- displayName: entry2.id,
646
- ...typeof entry2.name === "string" ? { name: entry2.name } : {},
647
- status: "available",
648
- ...contextWindow !== void 0 ? { contextWindow } : {},
649
- ...inputTypes !== void 0 ? { inputTypes } : {},
650
- ...outputTypes !== void 0 ? { outputTypes } : {},
651
- ...parameters !== void 0 ? {
652
- supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
653
- supportsStructuredOutput: parameters.includes("response_format"),
654
- supportsStreaming: parameters.includes("stream")
655
- } : {},
656
- metadata: {
657
- ...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
658
- ...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
659
- ...parameters !== void 0 ? { supportedParameters: parameters } : {}
660
- }
661
- };
662
- }
663
- /** Streaming where the gateway supports it (§11). */
664
- stream(request, onChunk) {
665
- return streamChatCompletion(this.config, request, onChunk, this.env);
666
- }
667
- async healthCheck() {
668
- const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
669
- try {
670
- const started = Date.now();
671
- const models = await this.listModels();
672
- return {
673
- name: this.config.name,
674
- type: this.config.type,
675
- baseUrl: this.config.baseUrl,
676
- health: "healthy",
677
- connected: true,
678
- modelCount: models.length,
679
- lastCheckedAt,
680
- message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
681
- };
682
- } catch (error) {
683
- return {
684
- name: this.config.name,
685
- type: this.config.type,
686
- baseUrl: this.config.baseUrl,
687
- health: inferHealth(error),
688
- connected: false,
689
- modelCount: 0,
690
- lastCheckedAt,
691
- message: error instanceof Error ? error.message : String(error)
692
- };
693
- }
694
- }
695
- async chat(request) {
696
- let response;
697
- try {
698
- response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
699
- method: "POST",
700
- headers: authHeaders(this.config, this.env),
701
- timeoutMs: this.requestTimeoutMs,
702
- body: {
703
- model: request.model,
704
- messages: request.messages,
705
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
706
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
707
- ...request.stop !== void 0 ? { stop: request.stop } : {}
708
- }
709
- });
710
- } catch (error) {
711
- if (error instanceof ProviderError) throw error;
712
- throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
713
- }
714
- const { status, body } = response;
715
- if (status !== 200) {
716
- throw providerHttpError({
717
- provider: this.config.name,
718
- operation: "chat",
719
- status,
720
- body,
721
- ...status === 404 ? {
722
- code: "MODEL_NOT_FOUND"
723
- } : {}
724
- });
725
- }
726
- const payload = body;
727
- const choices = Array.isArray(payload.choices) ? payload.choices : [];
728
- const first = choices[0];
729
- if (!first || typeof first.message?.content !== "string") {
730
- throw unavailable(this.config, "Chat completion response had no message content.", status);
731
- }
732
- const usage = payload.usage ?? {};
733
- return {
734
- model: typeof payload.model === "string" ? payload.model : request.model,
735
- content: first.message.content,
736
- finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
737
- usage: {
738
- promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
739
- completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
740
- totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
741
- },
742
- raw: body
743
- };
744
- }
745
- };
746
- async function streamChatCompletion(config, request, onChunk, env = process.env) {
747
- const baseUrl = normalizeBaseUrl(config.baseUrl);
748
- const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
749
- let response;
750
- try {
751
- response = await fetch(`${baseUrl}/chat/completions`, {
752
- method: "POST",
753
- headers: {
754
- accept: "text/event-stream",
755
- "content-type": "application/json",
756
- ...authHeaders(config, env)
757
- },
758
- body: JSON.stringify({
759
- model: request.model,
760
- messages: request.messages,
761
- stream: true,
762
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
763
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
764
- }),
765
- signal: AbortSignal.timeout(timeoutMs)
766
- });
767
- } catch (error) {
768
- throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
769
- }
770
- if (!response.ok) {
771
- const body = await response.json().catch(() => void 0);
772
- throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
773
- }
774
- if (!response.body) {
775
- throw providerHttpError({
776
- provider: config.name,
777
- operation: "stream",
778
- status: 500,
779
- body: { error: { message: "streaming is not supported by this endpoint" } }
780
- });
781
- }
782
- let content = "";
783
- let model = request.model;
784
- let finishReason;
785
- const decoder = new TextDecoder();
786
- let buffer = "";
787
- for await (const piece of response.body) {
788
- buffer += decoder.decode(piece, { stream: true });
789
- let newline = buffer.indexOf("\n");
790
- while (newline >= 0) {
791
- const line = buffer.slice(0, newline).trim();
792
- buffer = buffer.slice(newline + 1);
793
- newline = buffer.indexOf("\n");
794
- if (line.length === 0 || line.startsWith(":")) continue;
795
- const data = line.startsWith("data:") ? line.slice(5).trim() : line;
796
- if (data === "[DONE]") continue;
797
- let event;
798
- try {
799
- event = JSON.parse(data);
800
- } catch {
801
- continue;
802
- }
803
- if (typeof event.model === "string") model = event.model;
804
- const choices = Array.isArray(event.choices) ? event.choices : [];
805
- const first = choices[0];
806
- const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
807
- if (delta.length > 0) content += delta;
808
- if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
809
- onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
810
- }
811
- }
812
- return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
813
- }
814
- function normalizeModalities(value) {
815
- const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
816
- if (!list) return void 0;
817
- const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
818
- const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
819
- return modalities.length > 0 ? modalities : void 0;
820
- }
821
- function normalizeBaseUrl(baseUrl) {
822
- return baseUrl.replace(/\/+$/, "");
823
- }
824
- function extractModelEntries(body, config) {
825
- if (Array.isArray(body)) return body;
826
- if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
827
- return body.data;
828
- }
829
- throw new ProviderError(
830
- "PROVIDER_UNAVAILABLE",
831
- `Provider "${config.name}" returned an unrecognized model list shape.`,
832
- { details: { endpoint: "models" } }
833
- );
834
- }
835
- function unavailable(config, message, status) {
836
- return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
837
- details: { status }
838
- });
839
- }
840
- function inferHealth(error) {
841
- const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
842
- return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
843
- }
844
-
845
- // src/providers/omnihilbras/index.ts
846
- var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
847
- gateway;
848
- async listModels() {
849
- const raw = await this.rawModelsResponse();
850
- await this.extractGatewayMetadata(raw);
851
- const models = await super.listModels();
852
- return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
853
- }
854
- /**
855
- * Run the full connect flow. Throws on authentication or transport failure
856
- * (use healthCheck() for a non-throwing probe).
857
- */
858
- /**
859
- * Gateway metadata reported alongside the model list (gateway version, tier,
860
- * region…), kept separate from per-model metadata (§11 — provider metadata).
861
- */
862
- gatewayMetadata() {
863
- return this.gateway;
864
- }
865
- async extractGatewayMetadata(body) {
866
- if (body !== null && typeof body === "object") {
867
- const gateway = body.gateway;
868
- if (gateway !== null && typeof gateway === "object") {
869
- this.gateway = gateway;
870
- }
871
- }
872
- }
873
- async connectAndRegister(registry) {
874
- const apiKey = resolveApiKey(this.config);
875
- const discovered = await this.listModels();
876
- const seen = /* @__PURE__ */ new Set();
877
- const models = [];
878
- const duplicates = [];
879
- for (const model of discovered) {
880
- if (seen.has(model.id)) {
881
- duplicates.push(model.id);
882
- continue;
883
- }
884
- seen.add(model.id);
885
- models.push({
886
- ...model,
887
- provider: this.config.name,
888
- status: "available",
889
- metadata: { ...model.metadata, gateway: "omnihilbras" }
890
- });
891
- }
892
- if (registry) {
893
- for (const model of models) registry.register(model);
894
- }
895
- 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)` : "") + ".";
896
- const status = {
897
- name: this.config.name,
898
- type: this.config.type,
899
- baseUrl: this.config.baseUrl,
900
- health: "healthy",
901
- connected: true,
902
- modelCount: models.length,
903
- lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
904
- message
905
- };
906
- return { status, models, duplicates, registered: registry ? models.length : 0 };
907
- }
908
- };
909
-
910
- // src/providers/factory.ts
911
- var OPENAI_COMPATIBLE_TYPES = [
912
- "openai-compatible",
913
- "openrouter",
914
- "local",
915
- "custom"
916
- ];
917
- function createProvider(config) {
918
- if (config.type === "omnihilbras") {
919
- return new OmniHilbrasProvider(config);
920
- }
921
- if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
922
- return new OpenAICompatibleProvider(config);
923
- }
924
- throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
925
- }
926
-
927
360
  // src/registry/index.ts
928
361
  function registryKey(provider, id) {
929
362
  return `${provider}:${id}`;
@@ -1008,183 +441,24 @@ function appendToConfigList(listKey, item) {
1008
441
  return configPath;
1009
442
  }
1010
443
 
1011
- // src/cli/commands/providers.ts
1012
- function registerProviderCommands(program) {
1013
- const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
1014
- provider.command("list").description("List configured providers.").action(async () => {
1015
- const config = loadConfig();
1016
- const providers = config.project.providers;
1017
- if (providers.length === 0) {
1018
- console.log(
1019
- "No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
1020
- );
1021
- return;
1022
- }
1023
- for (const provider2 of providers) {
1024
- const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
1025
- const enabled = provider2.enabled === false ? " (disabled)" : "";
1026
- console.log(
1027
- `${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
1028
- );
1029
- }
1030
- });
1031
- provider.command("add").description(
1032
- "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."
1033
- ).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(
1034
- "--type <type>",
1035
- "Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
1036
- "openai-compatible"
1037
- ).option(
1038
- "--api-key-env-var <envVar>",
1039
- "Environment variable holding the API key. Keys are never written to the config file."
1040
- ).action(async (options) => addProvider(options));
1041
- provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
1042
- "--connect",
1043
- "Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
1044
- ).action(async (name, options) => testProvider(name, options));
1045
- provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
1046
- throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
1047
- });
444
+ // src/agents/process/index.ts
445
+ import { spawn } from "child_process";
446
+
447
+ // src/agent-protocol/version.ts
448
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
449
+ var SUPPORTED_PROTOCOL_VERSIONS = [
450
+ "omninode-agent-protocol/1",
451
+ PROTOCOL_V2
452
+ ];
453
+ var MAX_MESSAGE_BYTES = 1048576;
454
+ function isSupportedProtocol(protocol) {
455
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
1048
456
  }
1049
- async function addProvider(options) {
1050
- const configPath = findConfigFile();
1051
- if (!configPath) {
1052
- throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
1053
- }
1054
- const candidate = providerConfigSchema.safeParse({
1055
- name: options.name,
1056
- type: options.type,
1057
- base_url: options.baseUrl,
1058
- ...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
1059
- });
1060
- if (!candidate.success) {
1061
- const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
1062
- throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
1063
- }
1064
- const newProvider = candidate.data;
1065
- const existing = loadConfig();
1066
- if (existing.project.providers.some((p) => p.name === newProvider.name)) {
1067
- throw new OmniNodeError(
1068
- "CLI_USAGE",
1069
- `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
1070
- );
1071
- }
1072
- const writtenPath = appendToConfigList("providers", newProvider);
1073
- console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
1074
- if (newProvider.api_key_env_var) {
1075
- console.log(
1076
- `Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
1077
- );
1078
- } else {
1079
- console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
1080
- }
1081
- }
1082
- async function testProvider(name, options = {}) {
1083
- const config = loadConfig();
1084
- const providerConfig = config.project.providers.find((p) => p.name === name);
1085
- if (!providerConfig) {
1086
- const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
1087
- throw new ProviderError(
1088
- "PROVIDER_NOT_FOUND",
1089
- `Provider "${name}" is not configured. Configured providers: ${available}.`
1090
- );
1091
- }
1092
- const provider = createProvider(providerConfig);
1093
- console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
1094
- if (options.connect && provider instanceof OmniHilbrasProvider) {
1095
- const registry = new ModelRegistry();
1096
- try {
1097
- const result = await provider.connectAndRegister(registry);
1098
- printProviderStatus(result.status);
1099
- console.log(` registered: ${result.registered}`);
1100
- } catch (error) {
1101
- console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
1102
- process.exitCode = 1;
1103
- }
1104
- return;
1105
- }
1106
- const health = await provider.healthCheck();
1107
- printProviderStatus(health);
1108
- if (!health.connected) process.exitCode = 1;
1109
- }
1110
- function printProviderStatus(status) {
1111
- console.log(` health: ${status.health}`);
1112
- console.log(` connected: ${status.connected}`);
1113
- console.log(` models: ${status.modelCount}`);
1114
- if (status.message) console.log(` detail: ${status.message}`);
1115
- }
1116
-
1117
- // src/cli/commands/models.ts
1118
- var CAPABILITIES = [
1119
- "chat",
1120
- "coding",
1121
- "tools",
1122
- "vision",
1123
- "embeddings"
1124
- ];
1125
- function registerModelsCommand(program) {
1126
- 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) => {
1127
- const capability = options.capability;
1128
- if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
1129
- throw new OmniNodeError(
1130
- "CLI_USAGE",
1131
- `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
1132
- );
1133
- }
1134
- const config = loadConfig();
1135
- const selected = config.project.providers.filter(
1136
- (p) => providerName ? p.name === providerName : true
1137
- );
1138
- if (selected.length === 0) {
1139
- throw new OmniNodeError(
1140
- "PROVIDER_NOT_FOUND",
1141
- providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
1142
- );
1143
- }
1144
- const registry = new ModelRegistry();
1145
- for (const providerConfig of selected) {
1146
- try {
1147
- const provider = createProvider(providerConfig);
1148
- const discovered = await registry.discoverFrom(provider);
1149
- console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
1150
- } catch (error) {
1151
- const message = error instanceof Error ? error.message : String(error);
1152
- console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
1153
- }
1154
- }
1155
- const models = registry.list({
1156
- ...providerName !== void 0 ? { provider: providerName } : {},
1157
- ...capability !== void 0 ? { capability } : {}
1158
- });
1159
- if (models.length === 0) {
1160
- console.log("No models found.");
1161
- return;
1162
- }
1163
- console.log("");
1164
- for (const model of models) {
1165
- console.log(`${model.provider}:${model.id}`);
1166
- }
1167
- });
1168
- }
1169
-
1170
- // src/agents/process/index.ts
1171
- import { spawn } from "child_process";
1172
-
1173
- // src/agent-protocol/version.ts
1174
- var PROTOCOL_V2 = "omninode-agent-protocol/2";
1175
- var SUPPORTED_PROTOCOL_VERSIONS = [
1176
- "omninode-agent-protocol/1",
1177
- PROTOCOL_V2
1178
- ];
1179
- var MAX_MESSAGE_BYTES = 1048576;
1180
- function isSupportedProtocol(protocol) {
1181
- return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
1182
- }
1183
-
1184
- // src/agent-protocol/types.ts
1185
- function newMessageId(prefix = "msg") {
1186
- const random = Math.random().toString(36).slice(2, 10);
1187
- return `${prefix}-${Date.now().toString(36)}-${random}`;
457
+
458
+ // src/agent-protocol/types.ts
459
+ function newMessageId(prefix = "msg") {
460
+ const random = Math.random().toString(36).slice(2, 10);
461
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1188
462
  }
1189
463
  function envelope(type, payload, ids = {}) {
1190
464
  return {
@@ -1576,7 +850,7 @@ function buildChildEnv(config) {
1576
850
  }
1577
851
 
1578
852
  // src/agents/process/index.ts
1579
- var DEFAULT_TIMEOUT_MS2 = 6e5;
853
+ var DEFAULT_TIMEOUT_MS = 6e5;
1580
854
  var KILL_GRACE_MS = 5e3;
1581
855
  var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
1582
856
  function composeTaskText(input) {
@@ -1639,7 +913,7 @@ var ProcessAgent = class {
1639
913
  throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
1640
914
  }
1641
915
  const mode = this.config.inputMode ?? "stdin";
1642
- const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
916
+ const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1643
917
  return new Promise((resolve2, reject) => {
1644
918
  let cwd;
1645
919
  try {
@@ -1998,138 +1272,57 @@ function buildAgentRegistry(config, log = logger) {
1998
1272
  return registry;
1999
1273
  }
2000
1274
 
2001
- // src/cli/commands/agents.ts
2002
- var collectArgs = (value, previous) => [...previous, value];
2003
- function registerAgentCommands(program) {
2004
- const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2005
- agent.command("list").description("List configured agents.").action(async () => {
2006
- const config = loadConfig();
2007
- const agents = config.project.agents;
2008
- if (agents.length === 0) {
2009
- console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
2010
- return;
2011
- }
2012
- for (const agent2 of agents) {
2013
- const command = agent2.command ?? "(no command)";
2014
- console.log(
2015
- `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
2016
- );
2017
- }
2018
- });
2019
- agent.command("add").description(
2020
- "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
2021
- ).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(
2022
- "--input-mode <mode>",
2023
- "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
2024
- "stdin"
2025
- ).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(
2026
- "--timeout-ms <ms>",
2027
- "Per-task timeout in milliseconds.",
2028
- (value) => Number(value)
2029
- ).action(async (options) => addAgent(options));
2030
- agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
2031
- }
2032
- async function addAgent(options) {
2033
- const candidate = agentConfigSchema.safeParse({
2034
- name: options.name,
2035
- type: "cli",
2036
- command: options.command,
2037
- ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
2038
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2039
- input_mode: options.inputMode,
2040
- ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
2041
- ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
2042
- ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
2043
- ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
2044
- ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
2045
- ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
2046
- });
2047
- if (!candidate.success) {
2048
- const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
2049
- throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
1275
+ // src/roles/index.ts
1276
+ var RoleRegistry = class {
1277
+ roles = /* @__PURE__ */ new Map();
1278
+ register(role) {
1279
+ this.roles.set(role.id, role);
2050
1280
  }
2051
- const newAgent = candidate.data;
2052
- const existing = loadConfig();
2053
- if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2054
- throw new OmniNodeError(
2055
- "CLI_USAGE",
2056
- `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
2057
- );
1281
+ registerAll(roles) {
1282
+ for (const role of roles) this.register(role);
2058
1283
  }
2059
- const configPath = appendToConfigList("agents", newAgent);
2060
- console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
2061
- console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2062
- }
2063
- async function testAgent(name) {
2064
- const config = loadConfig();
2065
- const agentConfig = config.project.agents.find((a) => a.name === name);
2066
- if (!agentConfig) {
2067
- const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
2068
- throw new AgentError(
2069
- "AGENT_NOT_FOUND",
2070
- `Agent "${name}" is not configured. Configured agents: ${available}.`
2071
- );
1284
+ get(id) {
1285
+ return this.roles.get(id);
2072
1286
  }
2073
- const agent = createAgent(agentConfig);
2074
- console.log(
2075
- `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
2076
- );
2077
- const result = await agent.run({
2078
- taskId: `test-${Date.now()}`,
2079
- objective: "Reply with the single word: OK"
2080
- });
2081
- console.log(` status: ${result.status}`);
2082
- if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
2083
- if (result.summary) console.log(` output: ${result.summary}`);
2084
- if (result.rawOutput && result.rawOutput !== result.summary) {
2085
- console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
1287
+ list() {
1288
+ return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
2086
1289
  }
2087
- if (result.error) console.log(` error: ${result.error}`);
2088
- if (result.status !== "completed") process.exitCode = 1;
2089
- }
2090
-
2091
- // src/cli/commands/roles.ts
2092
- function registerRoleCommands(program) {
2093
- const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2094
- role.command("list").description("List configured roles.").action(async () => {
2095
- const config = loadConfig();
2096
- const roles = config.project.roles;
2097
- if (roles.length === 0) {
2098
- console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
2099
- return;
2100
- }
2101
- for (const role2 of roles) {
2102
- console.log(`${role2.id} ${role2.name}`);
2103
- for (const responsibility of role2.responsibilities) {
2104
- console.log(` - ${responsibility}`);
2105
- }
2106
- }
2107
- });
1290
+ };
1291
+ function buildRoleRegistry(config) {
1292
+ const registry = new RoleRegistry();
1293
+ registry.registerAll(config.project.roles);
1294
+ return registry;
2108
1295
  }
2109
1296
 
2110
1297
  // src/persistence/json-file-store.ts
2111
- import { open, mkdir, readFile, rename, stat } from "fs/promises";
1298
+ import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
2112
1299
  import { dirname } from "path";
2113
1300
  var SCHEMA_VERSION = 2;
2114
- async function writeFileAtomic(target, contents) {
1301
+ async function writeFileAtomic(target, contents, durability = "rename") {
2115
1302
  const tmp = `${target}.tmp`;
2116
1303
  await mkdir(dirname(target), { recursive: true });
2117
- const handle = await open(tmp, "w");
2118
- try {
2119
- await handle.writeFile(contents, "utf8");
2120
- await handle.sync();
2121
- } finally {
2122
- await handle.close();
1304
+ if (durability === "fsync") {
1305
+ const handle = await open(tmp, "w");
1306
+ try {
1307
+ await handle.writeFile(contents, "utf8");
1308
+ await handle.sync();
1309
+ } finally {
1310
+ await handle.close();
1311
+ }
1312
+ } else {
1313
+ await writeFile(tmp, contents, "utf8");
2123
1314
  }
2124
1315
  await rename(tmp, target);
2125
- try {
2126
- const dir = await open(dirname(target), "r");
1316
+ if (durability === "fsync") {
2127
1317
  try {
2128
- await dir.sync();
2129
- } finally {
2130
- await dir.close();
1318
+ const dir = await open(dirname(target), "r");
1319
+ try {
1320
+ await dir.sync();
1321
+ } finally {
1322
+ await dir.close();
1323
+ }
1324
+ } catch {
2131
1325
  }
2132
- } catch {
2133
1326
  }
2134
1327
  }
2135
1328
  async function inspectDocument(path) {
@@ -2199,6 +1392,7 @@ async function readDocument(path, onCorrupt, quarantine = true) {
2199
1392
  var JsonFileStore = class {
2200
1393
  filePath;
2201
1394
  schemaVersion;
1395
+ durability;
2202
1396
  onCorrupt;
2203
1397
  quarantine;
2204
1398
  log;
@@ -2206,6 +1400,7 @@ var JsonFileStore = class {
2206
1400
  constructor(options) {
2207
1401
  this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
2208
1402
  this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
1403
+ this.durability = options.durability ?? "rename";
2209
1404
  this.onCorrupt = options.onCorrupt;
2210
1405
  this.quarantine = options.quarantine ?? true;
2211
1406
  this.log = logger.child({ component: "persistence", file: options.fileName });
@@ -2220,7 +1415,7 @@ var JsonFileStore = class {
2220
1415
  async writeAll(items) {
2221
1416
  const document = { schemaVersion: this.schemaVersion, items };
2222
1417
  await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
2223
- `);
1418
+ `, this.durability);
2224
1419
  }
2225
1420
  /** Serializes an async operation against this file. */
2226
1421
  synchronized(operation) {
@@ -2242,711 +1437,1137 @@ var JsonFileStore = class {
2242
1437
  }
2243
1438
  };
2244
1439
 
2245
- // src/tasks/store.ts
2246
- var FileTaskStore = class extends JsonFileStore {
2247
- constructor(directory) {
2248
- super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
2249
- }
2250
- save(task) {
2251
- return this.mutate((tasks) => {
2252
- const index = tasks.findIndex((t) => t.id === task.id);
2253
- if (index >= 0) tasks[index] = task;
2254
- else tasks.push(task);
2255
- return [tasks, void 0];
2256
- });
2257
- }
2258
- async get(id) {
2259
- const tasks = await this.readAll();
2260
- return tasks.find((t) => t.id === id);
2261
- }
2262
- async list(filter = {}) {
2263
- const tasks = await this.readAll();
2264
- 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));
1440
+ // src/memory/base.ts
1441
+ function defaultSearch(provider, text, options = {}) {
1442
+ return provider.retrieve({
1443
+ text,
1444
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
1445
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
1446
+ });
1447
+ }
1448
+ function defaultMetadata(name, options = {}) {
1449
+ return {
1450
+ name,
1451
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
1452
+ relevanceRanking: options.relevanceRanking ?? false,
1453
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
1454
+ };
1455
+ }
1456
+
1457
+ // src/memory/local.ts
1458
+ var STOPWORDS = /* @__PURE__ */ new Set([
1459
+ "the",
1460
+ "and",
1461
+ "for",
1462
+ "with",
1463
+ "this",
1464
+ "that",
1465
+ "from",
1466
+ "into",
1467
+ "was",
1468
+ "were",
1469
+ "have",
1470
+ "has",
1471
+ "had",
1472
+ "are",
1473
+ "but",
1474
+ "not",
1475
+ "you",
1476
+ "his",
1477
+ "her",
1478
+ "its",
1479
+ "their",
1480
+ "them",
1481
+ "then",
1482
+ "than",
1483
+ "when",
1484
+ "what",
1485
+ "which",
1486
+ "will",
1487
+ "would",
1488
+ "been",
1489
+ "were",
1490
+ "also",
1491
+ "because",
1492
+ "about",
1493
+ "after",
1494
+ "before"
1495
+ ]);
1496
+ function keywords(text) {
1497
+ return new Set(
1498
+ (text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
1499
+ );
1500
+ }
1501
+ function relevanceScore(queryKeywords, entry2) {
1502
+ if (queryKeywords.size === 0) return 0;
1503
+ const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
1504
+ let overlap = 0;
1505
+ for (const keyword of queryKeywords) {
1506
+ if (entryKeywords.has(keyword)) overlap += 1;
1507
+ }
1508
+ let tagMatches = 0;
1509
+ for (const tag of entry2.tags ?? []) {
1510
+ if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
1511
+ }
1512
+ return overlap * 2 + tagMatches * 1.5;
1513
+ }
1514
+ var LocalMemoryProvider = class extends JsonFileStore {
1515
+ name = "local";
1516
+ constructor(directory) {
1517
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1518
+ }
1519
+ metadata() {
1520
+ return defaultMetadata(this.name, {
1521
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
1522
+ relevanceRanking: true,
1523
+ backend: { kind: "json-file", deterministicScoring: true }
1524
+ });
1525
+ }
1526
+ search(text, options = {}) {
1527
+ return defaultSearch(this, text, options);
1528
+ }
1529
+ /** @deprecated v1 alias of {@link retrieve}. */
1530
+ query(query) {
1531
+ return this.retrieve(query);
1532
+ }
1533
+ /** @deprecated v1 alias of {@link store}. */
1534
+ write(entry2) {
1535
+ return this.store(entry2);
1536
+ }
1537
+ async retrieve(query) {
1538
+ const entries = await this.readAll();
1539
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
1540
+ (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1541
+ );
1542
+ if (!query.text || query.text.trim().length === 0) {
1543
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1544
+ }
1545
+ const queryKeywords = keywords(query.text);
1546
+ 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);
1547
+ }
1548
+ store(entry2) {
1549
+ return this.mutate((entries) => {
1550
+ const index = entries.findIndex((e) => e.key === entry2.key);
1551
+ const stamped = {
1552
+ ...entry2,
1553
+ createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1554
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1555
+ };
1556
+ if (index >= 0) entries[index] = stamped;
1557
+ else entries.push(stamped);
1558
+ return [entries, void 0];
1559
+ });
1560
+ }
1561
+ async count() {
1562
+ return (await this.readAll()).length;
2265
1563
  }
2266
1564
  };
2267
1565
 
2268
- // src/tasks/engine.ts
2269
- import { randomBytes } from "crypto";
2270
- var ALLOWED_TRANSITIONS = {
2271
- created: ["queued", "running", "cancelled"],
2272
- queued: ["running", "cancelled"],
2273
- running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
2274
- waiting: ["running", "cancelled"],
2275
- completed: [],
2276
- // failed → running is the only re-entry: bounded automatic retry (§6.4).
2277
- failed: ["running"],
2278
- timed_out: [],
2279
- cancelled: [],
2280
- unknown: [],
2281
- partially_completed: []
1566
+ // src/memory/remembera.ts
1567
+ var RememberaMemoryProvider = class {
1568
+ name = "remembera";
1569
+ baseUrl;
1570
+ apiKeyEnvVar;
1571
+ env;
1572
+ log;
1573
+ constructor(config, log = logger) {
1574
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
1575
+ this.apiKeyEnvVar = config.apiKeyEnvVar;
1576
+ this.env = config.env ?? process.env;
1577
+ this.log = log.child({ component: "remembera" });
1578
+ }
1579
+ get authenticationConfigured() {
1580
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
1581
+ }
1582
+ headers() {
1583
+ const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1584
+ return key ? { authorization: `Bearer ${key}` } : {};
1585
+ }
1586
+ /** Search delegates to the gateway's relevance query. */
1587
+ search(text, options = {}) {
1588
+ return defaultSearch(
1589
+ this,
1590
+ text,
1591
+ {
1592
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
1593
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
1594
+ }
1595
+ );
1596
+ }
1597
+ metadata() {
1598
+ return defaultMetadata(this.name, {
1599
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
1600
+ relevanceRanking: true,
1601
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
1602
+ });
1603
+ }
1604
+ /** @deprecated v1 alias of {@link retrieve}. */
1605
+ query(query) {
1606
+ return this.retrieve(query);
1607
+ }
1608
+ /** @deprecated v1 alias of {@link store}. */
1609
+ write(entry2) {
1610
+ return this.store(entry2);
1611
+ }
1612
+ async retrieve(query) {
1613
+ const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1614
+ if (status !== 200) {
1615
+ throw new MemoryError(`Remembera query returned HTTP ${status}.`);
1616
+ }
1617
+ const entries = body.entries;
1618
+ if (!Array.isArray(entries)) {
1619
+ throw new MemoryError("Remembera query response had no entries array.");
1620
+ }
1621
+ return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
1622
+ }
1623
+ async store(entry2) {
1624
+ const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
1625
+ if (status !== 200 && status !== 201) {
1626
+ throw new MemoryError(`Remembera write returned HTTP ${status}.`);
1627
+ }
1628
+ }
1629
+ async request(url, body) {
1630
+ const headers = this.headers();
1631
+ try {
1632
+ return await requestJson(url, {
1633
+ method: "POST",
1634
+ headers,
1635
+ body
1636
+ });
1637
+ } catch (error) {
1638
+ const message = error instanceof Error ? error.message : String(error);
1639
+ throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
1640
+ }
1641
+ }
1642
+ toEntry(raw) {
1643
+ if (raw === null || typeof raw !== "object") {
1644
+ this.log.warn("Skipping malformed Remembera memory entry.");
1645
+ return void 0;
1646
+ }
1647
+ const entry2 = raw;
1648
+ if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
1649
+ this.log.warn("Skipping Remembera memory entry without key/content.");
1650
+ return void 0;
1651
+ }
1652
+ const scope = entry2.scope;
1653
+ return {
1654
+ key: entry2.key,
1655
+ content: entry2.content,
1656
+ scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1657
+ ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
1658
+ ...typeof entry2.category === "string" ? { category: entry2.category } : {},
1659
+ ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
1660
+ ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
1661
+ };
1662
+ }
2282
1663
  };
2283
- var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
2284
- var EXECUTION_HISTORY_LIMIT = 10;
2285
- var TaskEngine = class {
2286
- constructor(options) {
2287
- this.options = options;
2288
- this.log = (options.log ?? logger).child({ component: "tasks" });
1664
+
1665
+ // src/memory/factory.ts
1666
+ function createMemoryProvider(config) {
1667
+ if (!config || config.provider === "local") {
1668
+ return new LocalMemoryProvider();
1669
+ }
1670
+ if (config.provider === "remembera") {
1671
+ if (!config.baseUrl) {
1672
+ throw new ConfigError(
1673
+ "CONFIG_INVALID",
1674
+ `Memory provider "remembera" needs a base_url in omninode.yaml.`
1675
+ );
1676
+ }
1677
+ return new RememberaMemoryProvider({
1678
+ baseUrl: config.baseUrl,
1679
+ apiKeyEnvVar: config.apiKeyEnvVar
1680
+ });
1681
+ }
1682
+ throw new ConfigError(
1683
+ "CONFIG_INVALID",
1684
+ `Unknown memory provider "${config.provider}". Available: local, remembera.`
1685
+ );
1686
+ }
1687
+
1688
+ // src/memory/service.ts
1689
+ var DEFAULT_ENTRY_LIMIT = 8;
1690
+ var MAX_CONTEXT_CHARS = 4e3;
1691
+ var MemoryService = class {
1692
+ constructor(provider, log = logger, options = {}) {
1693
+ this.provider = provider;
1694
+ this.log = log.child({ component: "memory", provider: provider.name });
1695
+ this.required = options.required ?? false;
1696
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
1697
+ }
1698
+ provider;
1699
+ log;
1700
+ required;
1701
+ maxContextChars;
1702
+ get providerRef() {
1703
+ return this.provider;
2289
1704
  }
2290
- options;
2291
- log;
2292
- get store() {
2293
- return this.options.store;
1705
+ get providerName() {
1706
+ return this.provider.name;
2294
1707
  }
2295
- async create(input) {
2296
- if (!input.objective || input.objective.trim().length === 0) {
2297
- throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
2298
- }
2299
- if (input.role && !this.options.roles.get(input.role)) {
2300
- throw new TaskError(
2301
- "TASK_INVALID",
2302
- `Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
2303
- );
2304
- }
2305
- if (input.agent && !this.options.agents.get(input.agent)) {
2306
- throw new TaskError(
2307
- "TASK_INVALID",
2308
- `Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
2309
- );
1708
+ get isRequired() {
1709
+ return this.required;
1710
+ }
1711
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
1712
+ async contextFor(query) {
1713
+ let entries;
1714
+ try {
1715
+ entries = await this.provider.retrieve({
1716
+ text: query.objective,
1717
+ ...query.role ? { tags: [query.role] } : {},
1718
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
1719
+ });
1720
+ } catch (error) {
1721
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
2310
1722
  }
2311
- const now = (/* @__PURE__ */ new Date()).toISOString();
2312
- const task = {
2313
- id: generateTaskId(),
2314
- objective: input.objective.trim(),
2315
- status: "created",
2316
- attempt: 0,
2317
- createdAt: now,
2318
- updatedAt: now,
2319
- ...input.project !== void 0 ? { project: input.project } : {},
2320
- ...input.role !== void 0 ? { role: input.role } : {},
2321
- ...input.agent !== void 0 ? { agent: input.agent } : {},
2322
- ...input.context !== void 0 ? { context: input.context } : {}
2323
- };
2324
- await this.options.store.save(task);
2325
- this.log.info(`Created task ${task.id}.`);
2326
- await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
2327
- return task;
1723
+ return this.format(entries);
2328
1724
  }
2329
- async queue(id) {
2330
- const task = await this.mustGet(id);
2331
- return this.transition(task, "queued");
1725
+ /** Prompt-ready context text (v1 convenience wrapper). */
1726
+ async gatherContext(query) {
1727
+ return (await this.contextFor(query)).text;
2332
1728
  }
2333
- /** Runs the task through its assigned agent and records the result.
2334
- * `maxAttempts` enables bounded automatic retries of *retryable* errors
2335
- * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
2336
- async run(id, options = {}) {
2337
- const task = await this.mustGet(id);
2338
- if (task.status !== "created" && task.status !== "queued") {
2339
- throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
2340
- }
2341
- if (!task.agent) {
2342
- throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
2343
- }
2344
- const agent = this.options.agents.get(task.agent);
2345
- if (!agent) {
2346
- throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
2347
- }
2348
- const role = task.role ? this.options.roles.get(task.role) : void 0;
2349
- this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
2350
- let background = task.context?.background;
2351
- if (this.options.memory) {
2352
- const memoryContext = await this.options.memory.gatherContext({
2353
- objective: task.objective,
2354
- ...task.role ? { role: task.role } : {},
2355
- ...task.project ? { project: task.project } : {}
1729
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
1730
+ async recordTaskOutcome(task) {
1731
+ const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1732
+ try {
1733
+ await this.provider.store({
1734
+ key: `task:${task.id}`,
1735
+ scope: "task",
1736
+ category: "tasks",
1737
+ content: `Objective: ${task.objective}
1738
+ Status: ${task.status}
1739
+ Outcome: ${outcome}`,
1740
+ tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
1741
+ createdAt: task.createdAt
2356
1742
  });
2357
- if (memoryContext.length > 0) {
2358
- background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
2359
- }
2360
- }
2361
- const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2362
- const backoffMs = options.retryBackoffMs ?? 0;
2363
- let current = task;
2364
- for (let i = 0; i < maxAttempts; i += 1) {
2365
- const attempt = current.attempt + 1;
2366
- const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2367
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2368
- current = await this.transition(current, "running", { attempt });
2369
- let output;
2370
- let thrown;
2371
- try {
2372
- output = await agent.run({
2373
- taskId: current.id,
2374
- objective: current.objective,
2375
- ...role ? { role } : {},
2376
- ...background !== void 0 ? { context: background } : {}
2377
- });
2378
- } catch (error) {
2379
- thrown = error;
2380
- }
2381
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2382
- if (thrown !== void 0 || output === void 0) {
2383
- const outcome = classifyThrown(thrown);
2384
- const message = thrown instanceof Error ? thrown.message : String(thrown);
2385
- const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
2386
- current = await this.transition(current, outcome, {
2387
- attempt,
2388
- lastError: message,
2389
- execution: {
2390
- executionId,
2391
- attempt,
2392
- startedAt,
2393
- finishedAt,
2394
- outcome,
2395
- error: message
2396
- }
1743
+ const significant = (task.result?.reports ?? []).flatMap(
1744
+ (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1745
+ );
1746
+ for (const { report, finding } of significant) {
1747
+ await this.provider.store({
1748
+ key: `finding:${report.id}:${finding.id}`,
1749
+ scope: "project",
1750
+ category: "problems",
1751
+ content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1752
+ tags: ["known-problem", report.agent]
2397
1753
  });
2398
- this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
2399
- if (retryable) {
2400
- if (backoffMs > 0) await sleep(backoffMs * attempt);
2401
- continue;
2402
- }
2403
- if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2404
- return current;
2405
- }
2406
- const currentState = await this.options.store.get(current.id);
2407
- if (currentState?.status === "cancelled") {
2408
- current = currentState;
2409
- break;
2410
1754
  }
2411
- const result = {
2412
- ...output.summary !== void 0 ? { summary: output.summary } : {},
2413
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2414
- // Keep the verbatim output (bounded) for report extraction and audit.
2415
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2416
- ...output.error !== void 0 ? { error: output.error } : {},
2417
- finishedAt
2418
- };
2419
- const finalStatus = output.status === "completed" ? "completed" : "failed";
2420
- current = await this.transition(current, finalStatus, {
2421
- attempt,
2422
- result,
2423
- execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
2424
- });
2425
- this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
2426
- break;
2427
- }
2428
- if (this.options.memory) {
2429
- await this.options.memory.recordTaskOutcome(current);
1755
+ } catch (error) {
1756
+ this.handleFailure("store", error, void 0);
2430
1757
  }
2431
- return current;
2432
1758
  }
2433
- async cancel(id) {
2434
- const task = await this.mustGet(id);
2435
- if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
2436
- throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
1759
+ format(entries) {
1760
+ if (entries.length === 0) return { text: "", entries, truncated: false };
1761
+ const byScope = /* @__PURE__ */ new Map();
1762
+ for (const entry2 of entries) {
1763
+ byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2437
1764
  }
2438
- if (task.agent) {
2439
- const agent = this.options.agents.get(task.agent);
2440
- await agent?.cancel?.(task.id);
1765
+ const parts = [];
1766
+ for (const scope of ["project", "task", "role", "knowledge"]) {
1767
+ const scopeEntries = byScope.get(scope) ?? [];
1768
+ if (scopeEntries.length === 0) continue;
1769
+ const lines = scopeEntries.map((entry2) => {
1770
+ const category = entry2.category ? `[${entry2.category}] ` : "";
1771
+ return `- ${category}${entry2.content}`;
1772
+ }).join("\n");
1773
+ parts.push(`### Relevant memory (${scope})
1774
+ ${lines}`);
2441
1775
  }
2442
- return this.transition(task, "cancelled");
1776
+ let text = parts.join("\n\n");
1777
+ const truncated = text.length > this.maxContextChars;
1778
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
1779
+ return { text, entries, truncated };
2443
1780
  }
2444
- /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
2445
- async retry(id) {
2446
- const task = await this.mustGet(id);
2447
- const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
2448
- if (!retryableStates.includes(task.status)) {
2449
- throw new TaskError(
2450
- "TASK_INVALID",
2451
- `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1781
+ /**
1782
+ * Failure policy (§12): continue unless memory is explicitly required.
1783
+ */
1784
+ handleFailure(operation, error, fallback) {
1785
+ const message = error instanceof Error ? error.message : String(error);
1786
+ if (this.required) {
1787
+ throw new MemoryError(
1788
+ `Memory is marked required, but ${operation} failed: ${message}`,
1789
+ { cause: error }
2452
1790
  );
2453
1791
  }
2454
- const { result: _discarded, ...rest } = task;
2455
- const reset = {
2456
- ...rest,
2457
- status: "created",
2458
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2459
- };
2460
- await this.options.store.save(reset);
2461
- await this.audit("task.retry", id, { from: task.status });
2462
- this.log.info(`Task ${id} reset for retry.`);
2463
- return reset;
1792
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
1793
+ return fallback ?? { text: "", entries: [], truncated: false };
1794
+ }
1795
+ };
1796
+
1797
+ // src/pipelines/store.ts
1798
+ var FilePipelineRunStore = class extends JsonFileStore {
1799
+ constructor(directory) {
1800
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
1801
+ }
1802
+ save(run2) {
1803
+ return this.mutate((runs) => {
1804
+ const index = runs.findIndex((r) => r.id === run2.id);
1805
+ if (index >= 0) runs[index] = run2;
1806
+ else runs.push(run2);
1807
+ return [runs, void 0];
1808
+ });
2464
1809
  }
2465
1810
  async get(id) {
2466
- return this.options.store.get(id);
1811
+ return (await this.readAll()).find((r) => r.id === id);
2467
1812
  }
2468
- async list(filter) {
2469
- return this.options.store.list(filter);
1813
+ async list(filter = {}) {
1814
+ 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 ?? ""));
2470
1815
  }
2471
- async mustGet(id) {
2472
- const task = await this.options.store.get(id);
2473
- if (!task) {
2474
- throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
2475
- }
2476
- return task;
1816
+ };
1817
+
1818
+ // src/tasks/store.ts
1819
+ var FileTaskStore = class extends JsonFileStore {
1820
+ constructor(directory) {
1821
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
2477
1822
  }
2478
- async transition(task, to, extra = {}) {
2479
- const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
2480
- if (!allowed.includes(to)) {
2481
- throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
2482
- }
2483
- const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
2484
- const updated = {
2485
- ...task,
2486
- status: to,
2487
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2488
- ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2489
- ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2490
- ...executions !== void 0 ? { executions } : {},
2491
- ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
2492
- };
2493
- await this.options.store.save(updated);
2494
- await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
2495
- from: task.status
1823
+ save(task) {
1824
+ return this.mutate((tasks) => {
1825
+ const index = tasks.findIndex((t) => t.id === task.id);
1826
+ if (index >= 0) tasks[index] = task;
1827
+ else tasks.push(task);
1828
+ return [tasks, void 0];
2496
1829
  });
2497
- return updated;
2498
1830
  }
2499
- async audit(action, id, detail) {
2500
- await this.options.audit?.record({
2501
- at: (/* @__PURE__ */ new Date()).toISOString(),
2502
- action,
2503
- id,
2504
- ...detail !== void 0 ? { detail } : {}
2505
- });
1831
+ async get(id) {
1832
+ const tasks = await this.readAll();
1833
+ return tasks.find((t) => t.id === id);
1834
+ }
1835
+ async list(filter = {}) {
1836
+ const tasks = await this.readAll();
1837
+ 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));
2506
1838
  }
2507
1839
  };
2508
- function generateTaskId() {
2509
- return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1840
+
1841
+ // src/cli/commands/inspect.ts
1842
+ function emit(json, payload, render) {
1843
+ if (json) {
1844
+ console.log(JSON.stringify(payload, null, 2));
1845
+ return;
1846
+ }
1847
+ render();
1848
+ }
1849
+ function jsonOption(command) {
1850
+ return command.option("--json", "Emit the raw record as JSON.");
1851
+ }
1852
+ function registerTaskInspect(taskGroup) {
1853
+ jsonOption(
1854
+ taskGroup.command("inspect <id>").description("Show a task with its execution history, result and report references.").action(async (id, options) => {
1855
+ const tasks = await new FileTaskStore().list();
1856
+ const task = tasks.find((t) => t.id === id);
1857
+ if (!task) throw new OmniNodeError("TASK_NOT_FOUND", `No task "${id}" in this project's store.`);
1858
+ emit(options.json ?? false, task, () => {
1859
+ console.log(`Task ${task.id}`);
1860
+ console.log(` status: ${task.status}${task.attempt > 0 ? ` (attempt ${task.attempt})` : ""}`);
1861
+ if (task.project) console.log(` project: ${task.project}`);
1862
+ if (task.pipelineId) console.log(` pipeline: ${task.pipelineId}`);
1863
+ console.log(` objective: ${task.objective}`);
1864
+ if (task.role) console.log(` role: ${task.role}`);
1865
+ if (task.agent) console.log(` agent: ${task.agent}`);
1866
+ console.log(` created: ${task.createdAt}`);
1867
+ console.log(` updated: ${task.updatedAt}`);
1868
+ if (task.lastError) console.log(` lastError: ${task.lastError}`);
1869
+ if (task.executions && task.executions.length > 0) {
1870
+ console.log(" executions:");
1871
+ for (const execution of task.executions) {
1872
+ console.log(
1873
+ ` #${execution.attempt} ${execution.outcome} (${execution.executionId})${execution.error ? ` \u2014 ${execution.error}` : ""}`
1874
+ );
1875
+ }
1876
+ }
1877
+ if (task.result?.summary) console.log(` summary: ${task.result.summary}`);
1878
+ if (task.result?.reports && task.result.reports.length > 0) {
1879
+ console.log(` reports: ${task.result.reports.length}`);
1880
+ for (const report of task.result.reports) {
1881
+ console.log(` - [${report.agent}] ${report.summary}`);
1882
+ for (const finding of report.findings) {
1883
+ console.log(` \xB7 [${finding.severity ?? "info"}] ${finding.title}`);
1884
+ }
1885
+ }
1886
+ }
1887
+ });
1888
+ })
1889
+ );
2510
1890
  }
2511
- function errorCode(error) {
2512
- return error?.code ?? "";
1891
+ function registerPipelineInspect(pipelineGroup) {
1892
+ jsonOption(
1893
+ pipelineGroup.command("inspect <run-id>").description("Show a pipeline run with per-step status and artifact references.").action(async (runId, options) => {
1894
+ const run2 = await new FilePipelineRunStore().get(runId);
1895
+ if (!run2) throw new OmniNodeError("PIPELINE_NOT_FOUND", `No pipeline run "${runId}" in this project's store.`);
1896
+ emit(options.json ?? false, run2, () => {
1897
+ console.log(`Pipeline run ${run2.id} (${run2.pipelineId})`);
1898
+ console.log(` status: ${run2.status}${run2.attempt > 1 ? ` (attempt ${run2.attempt})` : ""}`);
1899
+ if (run2.objective) console.log(` objective: ${run2.objective}`);
1900
+ console.log(` started: ${run2.startedAt ?? "?"}`);
1901
+ if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
1902
+ if (run2.planId) console.log(` plan: ${run2.planId}`);
1903
+ if (run2.combinedReportId) console.log(` combined: ${run2.combinedReportId}`);
1904
+ if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
1905
+ console.log(" steps:");
1906
+ for (const step of run2.stepRuns) {
1907
+ const stepTaskIds = step.taskIds ?? [];
1908
+ const tasks = stepTaskIds.length > 0 ? ` tasks=${stepTaskIds.join(",")}` : "";
1909
+ console.log(` ${step.stepId.padEnd(14)} ${step.status}${tasks}${step.error ? ` \u2014 ${step.error}` : ""}`);
1910
+ }
1911
+ });
1912
+ })
1913
+ );
2513
1914
  }
2514
- function classifyThrown(error) {
2515
- if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
2516
- const dispatched = error.details?.dispatched;
2517
- return dispatched === false ? "timed_out" : "unknown";
2518
- }
2519
- return "failed";
1915
+ function registerAgentInspect(agentGroup) {
1916
+ jsonOption(
1917
+ agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
1918
+ const config = loadConfig();
1919
+ const agentConfig = config.project.agents.find((a) => a.name === name);
1920
+ if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
1921
+ const registry = buildAgentRegistry(config);
1922
+ const agent = registry.get(name);
1923
+ const recent = (await new FileTaskStore().list()).filter((task) => task.agent === name).slice(-5).map((task) => ({ id: task.id, status: task.status, updatedAt: task.updatedAt }));
1924
+ const payload = {
1925
+ config: agentConfig,
1926
+ effective: agent?.info,
1927
+ envPolicy: resolveEnvPolicy(agentConfig),
1928
+ recentTasks: recent
1929
+ };
1930
+ emit(options.json ?? false, payload, () => {
1931
+ console.log(`Agent ${name}`);
1932
+ console.log(` integration: ${agentConfig.integration}`);
1933
+ console.log(` command: ${agentConfig.command ?? "(none)"}`);
1934
+ console.log(` input mode: ${agentConfig.inputMode ?? "stdin"}`);
1935
+ console.log(` env policy: ${resolveEnvPolicy(agentConfig)}`);
1936
+ if (agentConfig.cwd) console.log(` working dir: ${agentConfig.cwd}`);
1937
+ if (agentConfig.timeoutMs) console.log(` timeout: ${agentConfig.timeoutMs}ms`);
1938
+ if (agentConfig.maxOutputBytes) console.log(` max output: ${agentConfig.maxOutputBytes} bytes/stream`);
1939
+ if (agentConfig.allowExternalCwd) console.log(` external cwd allowed`);
1940
+ console.log(` recent tasks: ${recent.map((t) => `${t.id}:${t.status}`).join(", ") || "(none)"}`);
1941
+ });
1942
+ })
1943
+ );
2520
1944
  }
2521
- function sleep(ms) {
2522
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1945
+ function registerProviderInspect(providerGroup) {
1946
+ jsonOption(
1947
+ providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
1948
+ const config = loadConfig();
1949
+ const providerConfig = config.project.providers.find((p) => p.name === name);
1950
+ if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
1951
+ const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
1952
+ const provider = createProvider2(providerConfig);
1953
+ const payload = {
1954
+ config: providerConfig,
1955
+ providerId: provider.providerId,
1956
+ authentication: provider.authentication,
1957
+ capabilities: providerConfig.capabilities ?? [],
1958
+ memoryConfigured: config.project.memory ? { provider: config.project.memory.provider, required: config.project.memory.required ?? false } : void 0,
1959
+ memoryEntries: config.project.memory ? await new MemoryService(createMemoryProvider(config.project.memory)).providerRef.query({ limit: 1e4 }).then((e) => e.length) : void 0,
1960
+ roles: buildRoleRegistry(config).list().length
1961
+ };
1962
+ emit(options.json ?? false, payload, () => {
1963
+ console.log(`Provider ${name}`);
1964
+ console.log(` providerId: ${provider.providerId}`);
1965
+ console.log(` type: ${providerConfig.type}`);
1966
+ console.log(` base URL: ${providerConfig.baseUrl}`);
1967
+ console.log(` auth: ${provider.authentication.method}${provider.authentication.envVar ? ` ($${provider.authentication.envVar})` : ""}`);
1968
+ console.log(` credentials: ${provider.authentication.configured ? "configured" : "MISSING"}`);
1969
+ if (providerConfig.capabilities?.length) {
1970
+ console.log(` capabilities: ${providerConfig.capabilities.join(", ")}`);
1971
+ }
1972
+ });
1973
+ })
1974
+ );
2523
1975
  }
2524
1976
 
2525
- // src/audit/index.ts
2526
- import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2527
- import { dirname as dirname2 } from "path";
2528
- var FileAuditLog = class {
2529
- filePath;
2530
- log;
2531
- constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
2532
- this.filePath = `${directory}/audit.jsonl`;
2533
- this.log = log.child({ component: "audit" });
2534
- }
2535
- async record(event) {
2536
- try {
2537
- await mkdir2(dirname2(this.filePath), { recursive: true });
2538
- const { open: open2 } = await import("fs/promises");
2539
- const handle = await open2(this.filePath, "a");
2540
- try {
2541
- await handle.appendFile(`${JSON.stringify(event)}
2542
- `, "utf8");
2543
- await handle.sync();
2544
- } finally {
2545
- await handle.close();
2546
- }
2547
- } catch (error) {
2548
- this.log.warn(
2549
- `Audit write failed: ${error instanceof Error ? error.message : String(error)}`
1977
+ // src/cli/commands/providers.ts
1978
+ function registerProviderCommands(program) {
1979
+ const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
1980
+ provider.command("list").description("List configured providers.").action(async () => {
1981
+ const config = loadConfig();
1982
+ const providers = config.project.providers;
1983
+ if (providers.length === 0) {
1984
+ console.log(
1985
+ "No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
2550
1986
  );
1987
+ return;
2551
1988
  }
2552
- }
2553
- /** Recent events, oldest first; used by the CLI. */
2554
- async recent(limit = 20) {
2555
- let raw;
2556
- try {
2557
- raw = await readFile2(this.filePath, "utf8");
2558
- } catch {
2559
- return [];
1989
+ for (const provider2 of providers) {
1990
+ const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
1991
+ const enabled = provider2.enabled === false ? " (disabled)" : "";
1992
+ console.log(
1993
+ `${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
1994
+ );
2560
1995
  }
2561
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
2562
- try {
2563
- return JSON.parse(line);
2564
- } catch {
2565
- return void 0;
2566
- }
2567
- }).filter((event) => event !== void 0);
2568
- return events.slice(-limit);
2569
- }
2570
- };
2571
-
2572
- // src/memory/base.ts
2573
- function defaultSearch(provider, text, options = {}) {
2574
- return provider.retrieve({
2575
- text,
2576
- ...options.scope !== void 0 ? { scope: options.scope } : {},
2577
- ...options.limit !== void 0 ? { limit: options.limit } : {}
2578
1996
  });
1997
+ provider.command("add").description(
1998
+ "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."
1999
+ ).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(
2000
+ "--type <type>",
2001
+ "Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
2002
+ "openai-compatible"
2003
+ ).option(
2004
+ "--api-key-env-var <envVar>",
2005
+ "Environment variable holding the API key. Keys are never written to the config file."
2006
+ ).action(async (options) => addProvider(options));
2007
+ provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
2008
+ "--connect",
2009
+ "Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
2010
+ ).action(async (name, options) => testProvider(name, options));
2011
+ provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
2012
+ throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
2013
+ });
2014
+ registerProviderInspect(provider);
2579
2015
  }
2580
- function defaultMetadata(name, options = {}) {
2581
- return {
2582
- name,
2583
- capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2584
- relevanceRanking: options.relevanceRanking ?? false,
2585
- ...options.backend !== void 0 ? { backend: options.backend } : {}
2586
- };
2587
- }
2588
-
2589
- // src/memory/local.ts
2590
- var STOPWORDS = /* @__PURE__ */ new Set([
2591
- "the",
2592
- "and",
2593
- "for",
2594
- "with",
2595
- "this",
2596
- "that",
2597
- "from",
2598
- "into",
2599
- "was",
2600
- "were",
2601
- "have",
2602
- "has",
2603
- "had",
2604
- "are",
2605
- "but",
2606
- "not",
2607
- "you",
2608
- "his",
2609
- "her",
2610
- "its",
2611
- "their",
2612
- "them",
2613
- "then",
2614
- "than",
2615
- "when",
2616
- "what",
2617
- "which",
2618
- "will",
2619
- "would",
2620
- "been",
2621
- "were",
2622
- "also",
2623
- "because",
2624
- "about",
2625
- "after",
2626
- "before"
2627
- ]);
2628
- function keywords(text) {
2629
- return new Set(
2630
- (text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
2631
- );
2632
- }
2633
- function relevanceScore(queryKeywords, entry2) {
2634
- if (queryKeywords.size === 0) return 0;
2635
- const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
2636
- let overlap = 0;
2637
- for (const keyword of queryKeywords) {
2638
- if (entryKeywords.has(keyword)) overlap += 1;
2639
- }
2640
- let tagMatches = 0;
2641
- for (const tag of entry2.tags ?? []) {
2642
- if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
2643
- }
2644
- return overlap * 2 + tagMatches * 1.5;
2645
- }
2646
- var LocalMemoryProvider = class extends JsonFileStore {
2647
- name = "local";
2648
- constructor(directory) {
2649
- super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
2650
- }
2651
- metadata() {
2652
- return defaultMetadata(this.name, {
2653
- capabilities: ["retrieve", "store", "search", "categories", "tags"],
2654
- relevanceRanking: true,
2655
- backend: { kind: "json-file", deterministicScoring: true }
2656
- });
2657
- }
2658
- search(text, options = {}) {
2659
- return defaultSearch(this, text, options);
2016
+ async function addProvider(options) {
2017
+ const configPath = findConfigFile();
2018
+ if (!configPath) {
2019
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
2660
2020
  }
2661
- /** @deprecated v1 alias of {@link retrieve}. */
2662
- query(query) {
2663
- return this.retrieve(query);
2021
+ const candidate = providerConfigSchema.safeParse({
2022
+ name: options.name,
2023
+ type: options.type,
2024
+ base_url: options.baseUrl,
2025
+ ...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
2026
+ });
2027
+ if (!candidate.success) {
2028
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
2029
+ throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
2664
2030
  }
2665
- /** @deprecated v1 alias of {@link store}. */
2666
- write(entry2) {
2667
- return this.store(entry2);
2031
+ const newProvider = candidate.data;
2032
+ const existing = loadConfig();
2033
+ if (existing.project.providers.some((p) => p.name === newProvider.name)) {
2034
+ throw new OmniNodeError(
2035
+ "CLI_USAGE",
2036
+ `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
2037
+ );
2668
2038
  }
2669
- async retrieve(query) {
2670
- const entries = await this.readAll();
2671
- const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
2672
- (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
2039
+ const writtenPath = appendToConfigList("providers", newProvider);
2040
+ console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
2041
+ if (newProvider.api_key_env_var) {
2042
+ console.log(
2043
+ `Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
2673
2044
  );
2674
- if (!query.text || query.text.trim().length === 0) {
2675
- return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
2676
- }
2677
- const queryKeywords = keywords(query.text);
2678
- 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);
2045
+ } else {
2046
+ console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
2679
2047
  }
2680
- store(entry2) {
2681
- return this.mutate((entries) => {
2682
- const index = entries.findIndex((e) => e.key === entry2.key);
2683
- const stamped = {
2684
- ...entry2,
2685
- createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2686
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2687
- };
2688
- if (index >= 0) entries[index] = stamped;
2689
- else entries.push(stamped);
2690
- return [entries, void 0];
2691
- });
2048
+ }
2049
+ async function testProvider(name, options = {}) {
2050
+ const config = loadConfig();
2051
+ const providerConfig = config.project.providers.find((p) => p.name === name);
2052
+ if (!providerConfig) {
2053
+ const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
2054
+ throw new ProviderError(
2055
+ "PROVIDER_NOT_FOUND",
2056
+ `Provider "${name}" is not configured. Configured providers: ${available}.`
2057
+ );
2692
2058
  }
2693
- async count() {
2694
- return (await this.readAll()).length;
2059
+ const provider = createProvider(providerConfig);
2060
+ console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
2061
+ if (options.connect && provider instanceof OmniHilbrasProvider) {
2062
+ const registry = new ModelRegistry();
2063
+ try {
2064
+ const result = await provider.connectAndRegister(registry);
2065
+ printProviderStatus(result.status);
2066
+ console.log(` registered: ${result.registered}`);
2067
+ } catch (error) {
2068
+ console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
2069
+ process.exitCode = 1;
2070
+ }
2071
+ return;
2695
2072
  }
2696
- };
2073
+ const health = await provider.healthCheck();
2074
+ printProviderStatus(health);
2075
+ if (!health.connected) process.exitCode = 1;
2076
+ }
2077
+ function printProviderStatus(status) {
2078
+ console.log(` health: ${status.health}`);
2079
+ console.log(` connected: ${status.connected}`);
2080
+ console.log(` models: ${status.modelCount}`);
2081
+ if (status.message) console.log(` detail: ${status.message}`);
2082
+ }
2697
2083
 
2698
- // src/memory/remembera.ts
2699
- var RememberaMemoryProvider = class {
2700
- name = "remembera";
2701
- baseUrl;
2702
- apiKeyEnvVar;
2703
- env;
2704
- log;
2705
- constructor(config, log = logger) {
2706
- this.baseUrl = config.baseUrl.replace(/\/+$/, "");
2707
- this.apiKeyEnvVar = config.apiKeyEnvVar;
2708
- this.env = config.env ?? process.env;
2709
- this.log = log.child({ component: "remembera" });
2710
- }
2711
- get authenticationConfigured() {
2712
- return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2713
- }
2714
- headers() {
2715
- const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
2716
- return key ? { authorization: `Bearer ${key}` } : {};
2717
- }
2718
- /** Search delegates to the gateway's relevance query. */
2719
- search(text, options = {}) {
2720
- return defaultSearch(
2721
- this,
2722
- text,
2723
- {
2724
- ...options.scope !== void 0 ? { scope: options.scope } : {},
2725
- ...options.limit !== void 0 ? { limit: options.limit } : {}
2726
- }
2084
+ // src/cli/commands/models.ts
2085
+ var CAPABILITIES = [
2086
+ "chat",
2087
+ "coding",
2088
+ "tools",
2089
+ "vision",
2090
+ "embeddings"
2091
+ ];
2092
+ function registerModelsCommand(program) {
2093
+ program.command("models").description("Discover and list models from configured providers (model registry, \xA78).").argument("[provider]", "Only discover from this provider.").option("-c, --capability <capability>", `Filter by capability: ${CAPABILITIES.join(", ")}.`).action(async (providerName, options) => {
2094
+ const capability = options.capability;
2095
+ if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
2096
+ throw new OmniNodeError(
2097
+ "CLI_USAGE",
2098
+ `Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
2099
+ );
2100
+ }
2101
+ const config = loadConfig();
2102
+ const selected = config.project.providers.filter(
2103
+ (p) => providerName ? p.name === providerName : true
2727
2104
  );
2728
- }
2729
- metadata() {
2730
- return defaultMetadata(this.name, {
2731
- capabilities: ["retrieve", "store", "search", "categories", "tags"],
2732
- relevanceRanking: true,
2733
- backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2734
- });
2735
- }
2736
- /** @deprecated v1 alias of {@link retrieve}. */
2737
- query(query) {
2738
- return this.retrieve(query);
2739
- }
2740
- /** @deprecated v1 alias of {@link store}. */
2741
- write(entry2) {
2742
- return this.store(entry2);
2743
- }
2744
- async retrieve(query) {
2745
- const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
2746
- if (status !== 200) {
2747
- throw new MemoryError(`Remembera query returned HTTP ${status}.`);
2105
+ if (selected.length === 0) {
2106
+ throw new OmniNodeError(
2107
+ "PROVIDER_NOT_FOUND",
2108
+ providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
2109
+ );
2748
2110
  }
2749
- const entries = body.entries;
2750
- if (!Array.isArray(entries)) {
2751
- throw new MemoryError("Remembera query response had no entries array.");
2111
+ const registry = new ModelRegistry();
2112
+ for (const providerConfig of selected) {
2113
+ try {
2114
+ const provider = createProvider(providerConfig);
2115
+ const discovered = await registry.discoverFrom(provider);
2116
+ console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
2117
+ } catch (error) {
2118
+ const message = error instanceof Error ? error.message : String(error);
2119
+ console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
2120
+ }
2752
2121
  }
2753
- return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
2754
- }
2755
- async store(entry2) {
2756
- const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
2757
- if (status !== 200 && status !== 201) {
2758
- throw new MemoryError(`Remembera write returned HTTP ${status}.`);
2122
+ const models = registry.list({
2123
+ ...providerName !== void 0 ? { provider: providerName } : {},
2124
+ ...capability !== void 0 ? { capability } : {}
2125
+ });
2126
+ if (models.length === 0) {
2127
+ console.log("No models found.");
2128
+ return;
2759
2129
  }
2760
- }
2761
- async request(url, body) {
2762
- const headers = this.headers();
2763
- try {
2764
- return await requestJson(url, {
2765
- method: "POST",
2766
- headers,
2767
- body
2768
- });
2769
- } catch (error) {
2770
- const message = error instanceof Error ? error.message : String(error);
2771
- throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
2130
+ console.log("");
2131
+ for (const model of models) {
2132
+ console.log(`${model.provider}:${model.id}`);
2772
2133
  }
2773
- }
2774
- toEntry(raw) {
2775
- if (raw === null || typeof raw !== "object") {
2776
- this.log.warn("Skipping malformed Remembera memory entry.");
2777
- return void 0;
2134
+ });
2135
+ }
2136
+
2137
+ // src/cli/commands/agents.ts
2138
+ var collectArgs = (value, previous) => [...previous, value];
2139
+ function registerAgentCommands(program) {
2140
+ const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
2141
+ agent.command("list").description("List configured agents.").action(async () => {
2142
+ const config = loadConfig();
2143
+ const agents = config.project.agents;
2144
+ if (agents.length === 0) {
2145
+ console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
2146
+ return;
2778
2147
  }
2779
- const entry2 = raw;
2780
- if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
2781
- this.log.warn("Skipping Remembera memory entry without key/content.");
2782
- return void 0;
2148
+ for (const agent2 of agents) {
2149
+ const command = agent2.command ?? "(no command)";
2150
+ console.log(
2151
+ `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
2152
+ );
2783
2153
  }
2784
- const scope = entry2.scope;
2785
- return {
2786
- key: entry2.key,
2787
- content: entry2.content,
2788
- scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
2789
- ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
2790
- ...typeof entry2.category === "string" ? { category: entry2.category } : {},
2791
- ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
2792
- ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
2793
- };
2154
+ });
2155
+ agent.command("add").description(
2156
+ "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
2157
+ ).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
2158
+ "--input-mode <mode>",
2159
+ "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
2160
+ "stdin"
2161
+ ).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
2162
+ "--timeout-ms <ms>",
2163
+ "Per-task timeout in milliseconds.",
2164
+ (value) => Number(value)
2165
+ ).action(async (options) => addAgent(options));
2166
+ agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
2167
+ registerAgentInspect(agent);
2168
+ }
2169
+ async function addAgent(options) {
2170
+ const candidate = agentConfigSchema.safeParse({
2171
+ name: options.name,
2172
+ type: "cli",
2173
+ command: options.command,
2174
+ ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
2175
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2176
+ input_mode: options.inputMode,
2177
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
2178
+ ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
2179
+ ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
2180
+ ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
2181
+ ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
2182
+ ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
2183
+ });
2184
+ if (!candidate.success) {
2185
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
2186
+ throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
2794
2187
  }
2795
- };
2796
-
2797
- // src/memory/factory.ts
2798
- function createMemoryProvider(config) {
2799
- if (!config || config.provider === "local") {
2800
- return new LocalMemoryProvider();
2188
+ const newAgent = candidate.data;
2189
+ const existing = loadConfig();
2190
+ if (existing.project.agents.some((a) => a.name === newAgent.name)) {
2191
+ throw new OmniNodeError(
2192
+ "CLI_USAGE",
2193
+ `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
2194
+ );
2801
2195
  }
2802
- if (config.provider === "remembera") {
2803
- if (!config.baseUrl) {
2804
- throw new ConfigError(
2805
- "CONFIG_INVALID",
2806
- `Memory provider "remembera" needs a base_url in omninode.yaml.`
2807
- );
2808
- }
2809
- return new RememberaMemoryProvider({
2810
- baseUrl: config.baseUrl,
2811
- apiKeyEnvVar: config.apiKeyEnvVar
2812
- });
2196
+ const configPath = appendToConfigList("agents", newAgent);
2197
+ console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
2198
+ console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
2199
+ }
2200
+ async function testAgent(name) {
2201
+ const config = loadConfig();
2202
+ const agentConfig = config.project.agents.find((a) => a.name === name);
2203
+ if (!agentConfig) {
2204
+ const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
2205
+ throw new AgentError(
2206
+ "AGENT_NOT_FOUND",
2207
+ `Agent "${name}" is not configured. Configured agents: ${available}.`
2208
+ );
2813
2209
  }
2814
- throw new ConfigError(
2815
- "CONFIG_INVALID",
2816
- `Unknown memory provider "${config.provider}". Available: local, remembera.`
2210
+ const agent = createAgent(agentConfig);
2211
+ console.log(
2212
+ `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
2817
2213
  );
2214
+ const result = await agent.run({
2215
+ taskId: `test-${Date.now()}`,
2216
+ objective: "Reply with the single word: OK"
2217
+ });
2218
+ console.log(` status: ${result.status}`);
2219
+ if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
2220
+ if (result.summary) console.log(` output: ${result.summary}`);
2221
+ if (result.rawOutput && result.rawOutput !== result.summary) {
2222
+ console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
2223
+ }
2224
+ if (result.error) console.log(` error: ${result.error}`);
2225
+ if (result.status !== "completed") process.exitCode = 1;
2818
2226
  }
2819
2227
 
2820
- // src/memory/service.ts
2821
- var DEFAULT_ENTRY_LIMIT = 8;
2822
- var MAX_CONTEXT_CHARS = 4e3;
2823
- var MemoryService = class {
2824
- constructor(provider, log = logger, options = {}) {
2825
- this.provider = provider;
2826
- this.log = log.child({ component: "memory", provider: provider.name });
2827
- this.required = options.required ?? false;
2828
- this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
2228
+ // src/cli/commands/roles.ts
2229
+ function registerRoleCommands(program) {
2230
+ const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
2231
+ role.command("list").description("List configured roles.").action(async () => {
2232
+ const config = loadConfig();
2233
+ const roles = config.project.roles;
2234
+ if (roles.length === 0) {
2235
+ console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
2236
+ return;
2237
+ }
2238
+ for (const role2 of roles) {
2239
+ console.log(`${role2.id} ${role2.name}`);
2240
+ for (const responsibility of role2.responsibilities) {
2241
+ console.log(` - ${responsibility}`);
2242
+ }
2243
+ }
2244
+ });
2245
+ }
2246
+
2247
+ // src/tasks/engine.ts
2248
+ import { randomBytes } from "crypto";
2249
+ var ALLOWED_TRANSITIONS = {
2250
+ created: ["queued", "running", "cancelled"],
2251
+ queued: ["running", "cancelled"],
2252
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
2253
+ waiting: ["running", "cancelled"],
2254
+ completed: [],
2255
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
2256
+ failed: ["running"],
2257
+ timed_out: [],
2258
+ cancelled: [],
2259
+ unknown: [],
2260
+ partially_completed: []
2261
+ };
2262
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
2263
+ var EXECUTION_HISTORY_LIMIT = 10;
2264
+ var TaskEngine = class {
2265
+ constructor(options) {
2266
+ this.options = options;
2267
+ this.log = (options.log ?? logger).child({ component: "tasks" });
2829
2268
  }
2830
- provider;
2269
+ options;
2831
2270
  log;
2832
- required;
2833
- maxContextChars;
2834
- get providerRef() {
2835
- return this.provider;
2271
+ get store() {
2272
+ return this.options.store;
2836
2273
  }
2837
- get providerName() {
2838
- return this.provider.name;
2274
+ async create(input) {
2275
+ if (!input.objective || input.objective.trim().length === 0) {
2276
+ throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
2277
+ }
2278
+ if (input.role && !this.options.roles.get(input.role)) {
2279
+ throw new TaskError(
2280
+ "TASK_INVALID",
2281
+ `Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
2282
+ );
2283
+ }
2284
+ if (input.agent && !this.options.agents.get(input.agent)) {
2285
+ throw new TaskError(
2286
+ "TASK_INVALID",
2287
+ `Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
2288
+ );
2289
+ }
2290
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2291
+ const task = {
2292
+ id: generateTaskId(),
2293
+ objective: input.objective.trim(),
2294
+ status: "created",
2295
+ attempt: 0,
2296
+ createdAt: now,
2297
+ updatedAt: now,
2298
+ ...input.project !== void 0 ? { project: input.project } : {},
2299
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
2300
+ ...input.role !== void 0 ? { role: input.role } : {},
2301
+ ...input.agent !== void 0 ? { agent: input.agent } : {},
2302
+ ...input.context !== void 0 ? { context: input.context } : {}
2303
+ };
2304
+ await this.options.store.save(task);
2305
+ this.log.info(`Created task ${task.id}.`);
2306
+ await this.audit("task.created", task, { objective: task.objective });
2307
+ return task;
2839
2308
  }
2840
- get isRequired() {
2841
- return this.required;
2309
+ async queue(id) {
2310
+ const task = await this.mustGet(id);
2311
+ return this.transition(task, "queued");
2842
2312
  }
2843
- /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2844
- async contextFor(query) {
2845
- let entries;
2846
- try {
2847
- entries = await this.provider.retrieve({
2848
- text: query.objective,
2849
- ...query.role ? { tags: [query.role] } : {},
2850
- limit: query.limit ?? DEFAULT_ENTRY_LIMIT
2313
+ /** Runs the task through its assigned agent and records the result.
2314
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
2315
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
2316
+ async run(id, options = {}) {
2317
+ const task = await this.mustGet(id);
2318
+ if (task.status !== "created" && task.status !== "queued") {
2319
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
2320
+ }
2321
+ if (!task.agent) {
2322
+ throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
2323
+ }
2324
+ const agent = this.options.agents.get(task.agent);
2325
+ if (!agent) {
2326
+ throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
2327
+ }
2328
+ const role = task.role ? this.options.roles.get(task.role) : void 0;
2329
+ this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
2330
+ let background = task.context?.background;
2331
+ if (this.options.memory) {
2332
+ const memoryContext = await this.options.memory.gatherContext({
2333
+ objective: task.objective,
2334
+ ...task.role ? { role: task.role } : {},
2335
+ ...task.project ? { project: task.project } : {}
2851
2336
  });
2852
- } catch (error) {
2853
- return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
2337
+ if (memoryContext.length > 0) {
2338
+ background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
2339
+ }
2854
2340
  }
2855
- return this.format(entries);
2856
- }
2857
- /** Prompt-ready context text (v1 convenience wrapper). */
2858
- async gatherContext(query) {
2859
- return (await this.contextFor(query)).text;
2860
- }
2861
- /** Records a finished task's outcome — task history plus "known problems" (§12). */
2862
- async recordTaskOutcome(task) {
2863
- const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
2864
- try {
2865
- await this.provider.store({
2866
- key: `task:${task.id}`,
2867
- scope: "task",
2868
- category: "tasks",
2869
- content: `Objective: ${task.objective}
2870
- Status: ${task.status}
2871
- Outcome: ${outcome}`,
2872
- tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
2873
- createdAt: task.createdAt
2341
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2342
+ const backoffMs = options.retryBackoffMs ?? 0;
2343
+ let current = task;
2344
+ for (let i = 0; i < maxAttempts; i += 1) {
2345
+ const attempt = current.attempt + 1;
2346
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2347
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2348
+ current = await this.transition(current, "running", { attempt });
2349
+ await this.options.audit?.record({
2350
+ at: startedAt,
2351
+ action: "agent.started",
2352
+ id: current.id,
2353
+ taskId: current.id,
2354
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
2355
+ agentId: task.agent,
2356
+ executionId,
2357
+ detail: { attempt }
2874
2358
  });
2875
- const significant = (task.result?.reports ?? []).flatMap(
2876
- (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
2877
- );
2878
- for (const { report, finding } of significant) {
2879
- await this.provider.store({
2880
- key: `finding:${report.id}:${finding.id}`,
2881
- scope: "project",
2882
- category: "problems",
2883
- content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
2884
- tags: ["known-problem", report.agent]
2359
+ let output;
2360
+ let thrown;
2361
+ try {
2362
+ output = await agent.run({
2363
+ taskId: current.id,
2364
+ objective: current.objective,
2365
+ executionId,
2366
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
2367
+ ...role ? { role } : {},
2368
+ ...background !== void 0 ? { context: background } : {}
2369
+ });
2370
+ } catch (error) {
2371
+ thrown = error;
2372
+ }
2373
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2374
+ await this.options.audit?.record({
2375
+ at: finishedAt,
2376
+ action: "agent.completed",
2377
+ id: current.id,
2378
+ taskId: current.id,
2379
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
2380
+ agentId: task.agent,
2381
+ executionId,
2382
+ detail: { attempt, outcome: thrown !== void 0 || output === void 0 ? "threw" : "returned" }
2383
+ });
2384
+ if (thrown !== void 0 || output === void 0) {
2385
+ const outcome = classifyThrown(thrown);
2386
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
2387
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
2388
+ current = await this.transition(current, outcome, {
2389
+ attempt,
2390
+ lastError: message,
2391
+ execution: {
2392
+ executionId,
2393
+ attempt,
2394
+ startedAt,
2395
+ finishedAt,
2396
+ outcome,
2397
+ error: message
2398
+ }
2885
2399
  });
2400
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
2401
+ if (retryable) {
2402
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
2403
+ continue;
2404
+ }
2405
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2406
+ return current;
2886
2407
  }
2887
- } catch (error) {
2888
- this.handleFailure("store", error, void 0);
2408
+ const currentState = await this.options.store.get(current.id);
2409
+ if (currentState?.status === "cancelled") {
2410
+ current = currentState;
2411
+ break;
2412
+ }
2413
+ const result = {
2414
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
2415
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2416
+ // Keep the verbatim output (bounded) for report extraction and audit.
2417
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2418
+ ...output.error !== void 0 ? { error: output.error } : {},
2419
+ finishedAt
2420
+ };
2421
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
2422
+ current = await this.transition(current, finalStatus, {
2423
+ attempt,
2424
+ result,
2425
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
2426
+ });
2427
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
2428
+ break;
2429
+ }
2430
+ if (this.options.memory) {
2431
+ await this.options.memory.recordTaskOutcome(current);
2889
2432
  }
2433
+ return current;
2890
2434
  }
2891
- format(entries) {
2892
- if (entries.length === 0) return { text: "", entries, truncated: false };
2893
- const byScope = /* @__PURE__ */ new Map();
2894
- for (const entry2 of entries) {
2895
- byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2435
+ async cancel(id) {
2436
+ const task = await this.mustGet(id);
2437
+ if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
2438
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
2896
2439
  }
2897
- const parts = [];
2898
- for (const scope of ["project", "task", "role", "knowledge"]) {
2899
- const scopeEntries = byScope.get(scope) ?? [];
2900
- if (scopeEntries.length === 0) continue;
2901
- const lines = scopeEntries.map((entry2) => {
2902
- const category = entry2.category ? `[${entry2.category}] ` : "";
2903
- return `- ${category}${entry2.content}`;
2904
- }).join("\n");
2905
- parts.push(`### Relevant memory (${scope})
2906
- ${lines}`);
2440
+ if (task.agent) {
2441
+ const agent = this.options.agents.get(task.agent);
2442
+ await agent?.cancel?.(task.id);
2907
2443
  }
2908
- let text = parts.join("\n\n");
2909
- const truncated = text.length > this.maxContextChars;
2910
- if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2911
- return { text, entries, truncated };
2444
+ return this.transition(task, "cancelled");
2912
2445
  }
2913
- /**
2914
- * Failure policy (§12): continue unless memory is explicitly required.
2915
- */
2916
- handleFailure(operation, error, fallback) {
2917
- const message = error instanceof Error ? error.message : String(error);
2918
- if (this.required) {
2919
- throw new MemoryError(
2920
- `Memory is marked required, but ${operation} failed: ${message}`,
2921
- { cause: error }
2446
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
2447
+ async retry(id) {
2448
+ const task = await this.mustGet(id);
2449
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
2450
+ if (!retryableStates.includes(task.status)) {
2451
+ throw new TaskError(
2452
+ "TASK_INVALID",
2453
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
2922
2454
  );
2923
2455
  }
2924
- this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2925
- return fallback ?? { text: "", entries: [], truncated: false };
2456
+ const { result: _discarded, ...rest } = task;
2457
+ const reset = {
2458
+ ...rest,
2459
+ status: "created",
2460
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2461
+ };
2462
+ await this.options.store.save(reset);
2463
+ await this.audit("task.retry", reset, { from: task.status });
2464
+ this.log.info(`Task ${id} reset for retry.`);
2465
+ return reset;
2926
2466
  }
2927
- };
2928
-
2929
- // src/roles/index.ts
2930
- var RoleRegistry = class {
2931
- roles = /* @__PURE__ */ new Map();
2932
- register(role) {
2933
- this.roles.set(role.id, role);
2467
+ async get(id) {
2468
+ return this.options.store.get(id);
2934
2469
  }
2935
- registerAll(roles) {
2936
- for (const role of roles) this.register(role);
2470
+ async list(filter) {
2471
+ return this.options.store.list(filter);
2937
2472
  }
2938
- get(id) {
2939
- return this.roles.get(id);
2473
+ async mustGet(id) {
2474
+ const task = await this.options.store.get(id);
2475
+ if (!task) {
2476
+ throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
2477
+ }
2478
+ return task;
2940
2479
  }
2941
- list() {
2942
- return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
2480
+ async transition(task, to, extra = {}) {
2481
+ const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
2482
+ if (!allowed.includes(to)) {
2483
+ throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
2484
+ }
2485
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
2486
+ const updated = {
2487
+ ...task,
2488
+ status: to,
2489
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2490
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2491
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2492
+ ...executions !== void 0 ? { executions } : {},
2493
+ ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
2494
+ };
2495
+ await this.options.store.save(updated);
2496
+ await this.audit(`task.${to === "queued" ? "queued" : to}`, updated, {
2497
+ from: task.status
2498
+ });
2499
+ return updated;
2500
+ }
2501
+ async audit(action, task, detail, executionId) {
2502
+ await this.options.audit?.record({
2503
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2504
+ action,
2505
+ id: task.id,
2506
+ taskId: task.id,
2507
+ ...task.project !== void 0 ? { project: task.project } : {},
2508
+ ...task.pipelineId !== void 0 ? { pipelineId: task.pipelineId } : {},
2509
+ ...task.agent !== void 0 ? { agentId: task.agent } : {},
2510
+ ...executionId !== void 0 ? { executionId } : {},
2511
+ ...detail !== void 0 ? { detail } : {}
2512
+ });
2943
2513
  }
2944
2514
  };
2945
- function buildRoleRegistry(config) {
2946
- const registry = new RoleRegistry();
2947
- registry.registerAll(config.project.roles);
2948
- return registry;
2515
+ function generateTaskId() {
2516
+ return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2517
+ }
2518
+ function errorCode(error) {
2519
+ return error?.code ?? "";
2520
+ }
2521
+ function classifyThrown(error) {
2522
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
2523
+ const dispatched = error.details?.dispatched;
2524
+ return dispatched === false ? "timed_out" : "unknown";
2525
+ }
2526
+ return "failed";
2949
2527
  }
2528
+ function sleep(ms) {
2529
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2530
+ }
2531
+
2532
+ // src/audit/index.ts
2533
+ import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2534
+ import { dirname as dirname2 } from "path";
2535
+ var FileAuditLog = class {
2536
+ filePath;
2537
+ log;
2538
+ constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
2539
+ this.filePath = `${directory}/audit.jsonl`;
2540
+ this.log = log.child({ component: "audit" });
2541
+ }
2542
+ async record(event) {
2543
+ try {
2544
+ await mkdir2(dirname2(this.filePath), { recursive: true });
2545
+ await appendFile(this.filePath, `${JSON.stringify(event)}
2546
+ `, "utf8");
2547
+ } catch (error) {
2548
+ this.log.warn(
2549
+ `Audit write failed: ${error instanceof Error ? error.message : String(error)}`
2550
+ );
2551
+ }
2552
+ }
2553
+ /** Recent events, oldest first; used by the CLI. */
2554
+ async recent(limit = 20) {
2555
+ let raw;
2556
+ try {
2557
+ raw = await readFile2(this.filePath, "utf8");
2558
+ } catch {
2559
+ return [];
2560
+ }
2561
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
2562
+ try {
2563
+ return JSON.parse(line);
2564
+ } catch {
2565
+ return void 0;
2566
+ }
2567
+ }).filter((event) => event !== void 0);
2568
+ return events.slice(-limit);
2569
+ }
2570
+ };
2950
2571
 
2951
2572
  // src/tasks/engine-factory.ts
2952
2573
  function createTaskEngine(config, log = logger, directory) {
@@ -3038,6 +2659,7 @@ function registerTaskCommands(program) {
3038
2659
  printOutcome(finished);
3039
2660
  if (finished.status !== "completed") process.exitCode = 1;
3040
2661
  });
2662
+ registerTaskInspect(task);
3041
2663
  }
3042
2664
  function printOutcome(task, options = {}) {
3043
2665
  console.log(` id: ${task.id}`);
@@ -3384,6 +3006,7 @@ function truncate4(text, max) {
3384
3006
  }
3385
3007
 
3386
3008
  // src/reports/service.ts
3009
+ var MAX_REPORT_BYTES = 256 * 1024;
3387
3010
  var ReportService = class {
3388
3011
  constructor(store, log = logger) {
3389
3012
  this.store = store;
@@ -3418,6 +3041,16 @@ var ReportService = class {
3418
3041
  const reports = [];
3419
3042
  const rejected = [];
3420
3043
  for (const candidate of candidates) {
3044
+ const size = Buffer.byteLength(JSON.stringify(candidate), "utf8");
3045
+ if (size > MAX_REPORT_BYTES) {
3046
+ rejected.push({
3047
+ key: candidate?.id ?? "(unknown)",
3048
+ reason: `report exceeds the ${MAX_REPORT_BYTES} byte limit (${size} bytes)`,
3049
+ issues: []
3050
+ });
3051
+ this.log.warn(`Rejected oversized report (${size} bytes).`);
3052
+ continue;
3053
+ }
3421
3054
  const result = validateReport(candidate);
3422
3055
  if (result.valid) {
3423
3056
  reports.push(result.report);
@@ -4082,6 +3715,7 @@ var PipelineEngine = class {
4082
3715
  at: (/* @__PURE__ */ new Date()).toISOString(),
4083
3716
  action: "pipeline.run.started",
4084
3717
  id: run2.id,
3718
+ pipelineId: def.id,
4085
3719
  detail: { pipeline: def.id, objective }
4086
3720
  });
4087
3721
  const deps = this.effectiveDependencies(def);
@@ -4122,7 +3756,9 @@ var PipelineEngine = class {
4122
3756
  combinedContext: contextParts.join("\n\n"),
4123
3757
  reports: [...allReports],
4124
3758
  pipelineRunId: run2.id,
4125
- runId: run2.id
3759
+ runId: run2.id,
3760
+ pipelineDefId: def.id,
3761
+ taskId: run2.id
4126
3762
  };
4127
3763
  return this.executeStep(step, ctx);
4128
3764
  })
@@ -4199,8 +3835,9 @@ ${contribution}`);
4199
3835
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
4200
3836
  await this.options.audit?.record({
4201
3837
  at: (/* @__PURE__ */ new Date()).toISOString(),
4202
- action: "pipeline.run.finished",
3838
+ action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
4203
3839
  id: run2.id,
3840
+ pipelineId: def.id,
4204
3841
  detail: { pipeline: def.id, status: finalStatus }
4205
3842
  });
4206
3843
  return finished;
@@ -4281,6 +3918,7 @@ ${contribution}`);
4281
3918
  agentNames.map(
4282
3919
  (name) => this.options.tasks.create({
4283
3920
  objective: ctx.objective,
3921
+ ...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
4284
3922
  agent: name,
4285
3923
  ...ctx.role ? { role: ctx.role.id } : {},
4286
3924
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
@@ -4323,6 +3961,7 @@ ${contribution}`);
4323
3961
  async runSingle(step, ctx, agentName) {
4324
3962
  const task = await this.options.tasks.create({
4325
3963
  objective: ctx.objective,
3964
+ ...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
4326
3965
  agent: agentName,
4327
3966
  ...ctx.role ? { role: ctx.role.id } : {},
4328
3967
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
@@ -4393,7 +4032,13 @@ ${contribution}`);
4393
4032
  at: (/* @__PURE__ */ new Date()).toISOString(),
4394
4033
  action: "plan.generated",
4395
4034
  id: plan.id,
4396
- detail: { generatedBy: plan.generatedBy, steps: plan.steps.length }
4035
+ ...ctx.pipelineDefId !== void 0 ? { pipelineId: ctx.pipelineDefId } : {},
4036
+ detail: {
4037
+ generatedBy: plan.generatedBy,
4038
+ steps: plan.steps.length,
4039
+ run: ctx.pipelineRunId,
4040
+ taskId: ctx.taskId
4041
+ }
4397
4042
  });
4398
4043
  return {
4399
4044
  status: "completed",
@@ -4427,7 +4072,7 @@ function formatPlan(plan) {
4427
4072
  }
4428
4073
  return lines.join("\n");
4429
4074
  }
4430
- function createDefaultChatFn(providers) {
4075
+ function createDefaultChatFn(providers, options = {}) {
4431
4076
  return async (modelRef, messages) => {
4432
4077
  const separator = modelRef.indexOf(":");
4433
4078
  if (separator <= 0 || separator === modelRef.length - 1) {
@@ -4445,33 +4090,15 @@ function createDefaultChatFn(providers) {
4445
4090
  `Provider "${providerName}" (model "${modelRef}") is not configured.`
4446
4091
  );
4447
4092
  }
4448
- const provider = createProvider(providerConfig);
4093
+ const provider = createProvider(providerConfig, {
4094
+ ...options.audit !== void 0 ? { audit: options.audit } : {},
4095
+ ...options.env !== void 0 ? { env: options.env } : {}
4096
+ });
4449
4097
  const response = await provider.chat({ model: modelId, messages });
4450
4098
  return response.content;
4451
4099
  };
4452
4100
  }
4453
4101
 
4454
- // src/pipelines/store.ts
4455
- var FilePipelineRunStore = class extends JsonFileStore {
4456
- constructor(directory) {
4457
- super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
4458
- }
4459
- save(run2) {
4460
- return this.mutate((runs) => {
4461
- const index = runs.findIndex((r) => r.id === run2.id);
4462
- if (index >= 0) runs[index] = run2;
4463
- else runs.push(run2);
4464
- return [runs, void 0];
4465
- });
4466
- }
4467
- async get(id) {
4468
- return (await this.readAll()).find((r) => r.id === id);
4469
- }
4470
- async list(filter = {}) {
4471
- 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 ?? ""));
4472
- }
4473
- };
4474
-
4475
4102
  // src/planner/store.ts
4476
4103
  var FilePlanStore = class extends JsonFileStore {
4477
4104
  constructor(directory) {
@@ -4516,9 +4143,10 @@ function buildPlanner(plannerConfig, chat, log = logger) {
4516
4143
 
4517
4144
  // src/pipelines/engine-factory.ts
4518
4145
  function buildPipelineEngine(config, log = logger) {
4146
+ const auditLog = new FileAuditLog();
4519
4147
  const agents = buildAgentRegistry(config, log);
4520
4148
  const roles = buildRoleRegistry(config);
4521
- const chat = createDefaultChatFn(config.project.providers);
4149
+ const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
4522
4150
  const tasks = new TaskEngine({
4523
4151
  agents,
4524
4152
  roles,
@@ -4646,6 +4274,7 @@ function registerPipelineCommands(program) {
4646
4274
  console.log(` reports: ${run2.reports.length}`);
4647
4275
  }
4648
4276
  });
4277
+ registerPipelineInspect(pipeline);
4649
4278
  }
4650
4279
  async function runPipelineAction(id, objective) {
4651
4280
  const config = loadConfig();
@@ -4955,6 +4584,72 @@ function registerMigrateCommand(program) {
4955
4584
  });
4956
4585
  }
4957
4586
 
4587
+ // src/cli/commands/security.ts
4588
+ function registerSecurityCommand(program) {
4589
+ program.command("security").description("Inspect a project's security posture (advisory).").argument("[action]", "Action to perform.", "audit").action(async (action) => {
4590
+ if (action !== "audit") {
4591
+ console.log(`Unknown security action "${action}". Available: audit.`);
4592
+ return;
4593
+ }
4594
+ const config = loadConfig();
4595
+ const findings = [];
4596
+ for (const agent of config.project.agents) {
4597
+ const policy = resolveEnvPolicy(agent);
4598
+ if (policy === "inherit") {
4599
+ findings.push({
4600
+ severity: "warn",
4601
+ subject: `agent "${agent.name}"`,
4602
+ message: "inherits the full environment \u2014 provider keys in OmniNode's environment are visible to it. Consider env_policy: allowlist|denylist|explicit."
4603
+ });
4604
+ }
4605
+ if (agent.allowExternalCwd) {
4606
+ findings.push({
4607
+ severity: "info",
4608
+ subject: `agent "${agent.name}"`,
4609
+ message: "may run outside the project root (allow_external_cwd)."
4610
+ });
4611
+ }
4612
+ if (agent.env && Object.keys(agent.env).length > 0) {
4613
+ findings.push({
4614
+ severity: "info",
4615
+ subject: `agent "${agent.name}"`,
4616
+ message: `receives ${Object.keys(agent.env).length} explicit environment variable(s).`
4617
+ });
4618
+ }
4619
+ }
4620
+ for (const provider of config.project.providers) {
4621
+ if (!provider.apiKeyEnvVar) {
4622
+ findings.push({
4623
+ severity: "info",
4624
+ subject: `provider "${provider.name}"`,
4625
+ message: "no api_key_env_var \u2014 fine for keyless local endpoints, otherwise credentials cannot be resolved."
4626
+ });
4627
+ }
4628
+ }
4629
+ if (config.project.memory?.provider === "remembera" && !config.project.memory.apiKeyEnvVar) {
4630
+ findings.push({
4631
+ severity: "warn",
4632
+ subject: "memory",
4633
+ message: "Remembera is configured without api_key_env_var; requests will be unauthenticated."
4634
+ });
4635
+ }
4636
+ console.log(`OmniNode security audit \u2014 project "${config.project.name}"`);
4637
+ if (findings.length === 0) {
4638
+ console.log(" no findings.");
4639
+ }
4640
+ for (const finding of findings) {
4641
+ console.log(` [${finding.severity.toUpperCase()}] ${finding.subject}: ${finding.message}`);
4642
+ }
4643
+ console.log(
4644
+ `
4645
+ Trust model: agents execute with the permissions of the OS user running OmniNode.`
4646
+ );
4647
+ console.log(
4648
+ `OmniNode does not sandbox agents \u2014 run untrusted agents in a container or as a dedicated user.`
4649
+ );
4650
+ });
4651
+ }
4652
+
4958
4653
  // src/cli/index.ts
4959
4654
  function createProgram() {
4960
4655
  const program = new Command();
@@ -4975,10 +4670,21 @@ function createProgram() {
4975
4670
  registerStatusCommand(program);
4976
4671
  registerAuditCommand(program);
4977
4672
  registerMigrateCommand(program);
4673
+ registerSecurityCommand(program);
4978
4674
  registerRunAlias(program);
4979
4675
  return program;
4980
4676
  }
4677
+ function applyLoggingConfig() {
4678
+ try {
4679
+ if (findConfigFile()) {
4680
+ const logging = loadConfig().logging;
4681
+ if (logging) configureLogger(logging);
4682
+ }
4683
+ } catch {
4684
+ }
4685
+ }
4981
4686
  async function run(argv) {
4687
+ applyLoggingConfig();
4982
4688
  const program = createProgram();
4983
4689
  try {
4984
4690
  await program.parseAsync(argv);