@hilbras/omninode 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "0.2.0";
9
+ var OMNINODE_VERSION = "0.4.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -29,6 +29,16 @@ var ProviderError = class extends OmniNodeError {
29
29
  super(code, message, options);
30
30
  }
31
31
  };
32
+ var AgentError = class extends OmniNodeError {
33
+ constructor(code, message, options = {}) {
34
+ super(code, message, options);
35
+ }
36
+ };
37
+ var ProtocolError = class extends OmniNodeError {
38
+ constructor(message, options = {}) {
39
+ super("PROTOCOL_VIOLATION", message, options);
40
+ }
41
+ };
32
42
  function isOmniNodeError(value) {
33
43
  return value instanceof OmniNodeError;
34
44
  }
@@ -55,6 +65,8 @@ var agentConfigSchema = z.object({
55
65
  args: z.array(z.string()).optional(),
56
66
  cwd: z.string().optional(),
57
67
  env: z.record(z.string()).optional(),
68
+ input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
69
+ timeout_ms: z.number().int().positive().optional(),
58
70
  metadata: z.record(z.unknown()).optional()
59
71
  }).strict();
60
72
  var roleConfigSchema = z.object({
@@ -166,6 +178,8 @@ function toAgentConfig(raw) {
166
178
  args: raw.args,
167
179
  cwd: raw.cwd,
168
180
  env: raw.env,
181
+ inputMode: raw.input_mode,
182
+ timeoutMs: raw.timeout_ms,
169
183
  metadata: raw.metadata
170
184
  };
171
185
  }
@@ -243,10 +257,6 @@ function registerProjectCommands(program) {
243
257
  project.command("init").description("Alias of `omninode init`.").option("-f, --force", "Overwrite an existing omninode.yaml.").option("-n, --name <name>", "Project name to write into the configuration.", "My Project").action(async (options) => runInit(options));
244
258
  }
245
259
 
246
- // src/cli/commands/providers.ts
247
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
248
- import { parseDocument } from "yaml";
249
-
250
260
  // src/providers/auth.ts
251
261
  function resolveApiKey(config, env = process.env) {
252
262
  if (!config.apiKeyEnvVar) return void 0;
@@ -500,6 +510,49 @@ function inferHealth(error) {
500
510
  return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
501
511
  }
502
512
 
513
+ // src/providers/omnihilbras/index.ts
514
+ var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
515
+ /**
516
+ * Run the full connect flow. Throws on authentication or transport failure
517
+ * (use healthCheck() for a non-throwing probe).
518
+ */
519
+ async connect(registry) {
520
+ const apiKey = resolveApiKey(this.config);
521
+ const discovered = await this.listModels();
522
+ const seen = /* @__PURE__ */ new Set();
523
+ const models = [];
524
+ const duplicates = [];
525
+ for (const model of discovered) {
526
+ if (seen.has(model.id)) {
527
+ duplicates.push(model.id);
528
+ continue;
529
+ }
530
+ seen.add(model.id);
531
+ models.push({
532
+ ...model,
533
+ provider: this.config.name,
534
+ status: "available",
535
+ metadata: { ...model.metadata, gateway: "omnihilbras" }
536
+ });
537
+ }
538
+ if (registry) {
539
+ for (const model of models) registry.register(model);
540
+ }
541
+ 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)` : "") + ".";
542
+ const status = {
543
+ name: this.config.name,
544
+ type: this.config.type,
545
+ baseUrl: this.config.baseUrl,
546
+ health: "healthy",
547
+ connected: true,
548
+ modelCount: models.length,
549
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
550
+ message
551
+ };
552
+ return { status, models, duplicates, registered: registry ? models.length : 0 };
553
+ }
554
+ };
555
+
503
556
  // src/providers/factory.ts
504
557
  var OPENAI_COMPATIBLE_TYPES = [
505
558
  "openai-compatible",
@@ -508,25 +561,95 @@ var OPENAI_COMPATIBLE_TYPES = [
508
561
  "custom"
509
562
  ];
510
563
  function createProvider(config) {
564
+ if (config.type === "omnihilbras") {
565
+ return new OmniHilbrasProvider(config);
566
+ }
511
567
  if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
512
568
  return new OpenAICompatibleProvider(config);
513
569
  }
514
- if (config.type === "omnihilbras") {
515
- throw new OmniNodeError(
516
- "NOT_IMPLEMENTED",
517
- `Provider type "omnihilbras" is planned for Phase 2 (OmniHilbras Integration). If your OmniHilbras endpoint is OpenAI-compatible, use type "openai-compatible" for now.`
518
- );
519
- }
520
570
  throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
521
571
  }
522
572
 
573
+ // src/registry/index.ts
574
+ function registryKey(provider, id) {
575
+ return `${provider}:${id}`;
576
+ }
577
+ var ModelRegistry = class {
578
+ models = /* @__PURE__ */ new Map();
579
+ register(model) {
580
+ this.models.set(registryKey(model.provider, model.id), model);
581
+ }
582
+ get(provider, id) {
583
+ return this.models.get(registryKey(provider, id));
584
+ }
585
+ list(filter = {}) {
586
+ return [...this.models.values()].filter((model) => filter.provider ? model.provider === filter.provider : true).filter(
587
+ (model) => filter.capability ? model.capabilities?.[filter.capability] === true : true
588
+ ).sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
589
+ }
590
+ /** Availability validation (§27 Phase 1): present in the registry and not retired. */
591
+ isAvailable(provider, id) {
592
+ const model = this.get(provider, id);
593
+ return model !== void 0 && (model.status ?? "available") === "available";
594
+ }
595
+ /** Discover models from one provider and register them. */
596
+ async discoverFrom(provider) {
597
+ const discovered = await provider.listModels();
598
+ for (const model of discovered) this.register(model);
599
+ return discovered;
600
+ }
601
+ /** Discover from many providers; a failing provider does not abort the rest. */
602
+ async discoverAll(providers) {
603
+ return Promise.all(
604
+ providers.map(async (provider) => {
605
+ try {
606
+ return {
607
+ provider: provider.config.name,
608
+ models: await this.discoverFrom(provider)
609
+ };
610
+ } catch (error) {
611
+ return {
612
+ provider: provider.config.name,
613
+ models: [],
614
+ error: error instanceof Error ? error : new Error(String(error))
615
+ };
616
+ }
617
+ })
618
+ );
619
+ }
620
+ };
621
+
523
622
  // src/cli/commands/shared.ts
623
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
624
+ import { parseDocument } from "yaml";
524
625
  function notImplemented(command, when) {
525
626
  return new OmniNodeError(
526
627
  "NOT_IMPLEMENTED",
527
628
  `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
528
629
  );
529
630
  }
631
+ function appendToConfigList(listKey, item) {
632
+ const configPath = findConfigFile();
633
+ if (!configPath) {
634
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
635
+ }
636
+ const doc = parseDocument(readFileSync2(configPath, "utf8"));
637
+ if (doc.getIn(["project", listKey]) === void 0) {
638
+ doc.setIn(["project", listKey], [item]);
639
+ } else {
640
+ doc.addIn(["project", listKey], item);
641
+ }
642
+ const validated = appConfigSchema.safeParse(doc.toJS());
643
+ if (!validated.success) {
644
+ const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
645
+ throw new ConfigError(
646
+ "CONFIG_INVALID",
647
+ `Editing ${configPath} would produce an invalid configuration: ${issues}`
648
+ );
649
+ }
650
+ writeFileSync2(configPath, doc.toString(), "utf8");
651
+ return configPath;
652
+ }
530
653
 
531
654
  // src/cli/commands/providers.ts
532
655
  function registerProviderCommands(program) {
@@ -558,7 +681,10 @@ function registerProviderCommands(program) {
558
681
  "--api-key-env-var <envVar>",
559
682
  "Environment variable holding the API key. Keys are never written to the config file."
560
683
  ).action(async (options) => addProvider(options));
561
- provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").action(async (name) => testProvider(name));
684
+ provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
685
+ "--connect",
686
+ "Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
687
+ ).action(async (name, options) => testProvider(name, options));
562
688
  provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
563
689
  throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
564
690
  });
@@ -583,25 +709,11 @@ async function addProvider(options) {
583
709
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
584
710
  throw new OmniNodeError(
585
711
  "CLI_USAGE",
586
- `Provider "${newProvider.name}" already exists in ${configPath}.`
712
+ `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
587
713
  );
588
714
  }
589
- const doc = parseDocument(readFileSync2(configPath, "utf8"));
590
- if (doc.getIn(["project", "providers"]) === void 0) {
591
- doc.setIn(["project", "providers"], [newProvider]);
592
- } else {
593
- doc.addIn(["project", "providers"], newProvider);
594
- }
595
- const validated = appConfigSchema.safeParse(doc.toJS());
596
- if (!validated.success) {
597
- const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
598
- throw new ConfigError(
599
- "CONFIG_INVALID",
600
- `Editing ${configPath} would produce an invalid configuration: ${issues}`
601
- );
602
- }
603
- writeFileSync2(configPath, doc.toString(), "utf8");
604
- console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
715
+ const writtenPath = appendToConfigList("providers", newProvider);
716
+ console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
605
717
  if (newProvider.api_key_env_var) {
606
718
  console.log(
607
719
  `Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
@@ -610,7 +722,7 @@ async function addProvider(options) {
610
722
  console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
611
723
  }
612
724
  }
613
- async function testProvider(name) {
725
+ async function testProvider(name, options = {}) {
614
726
  const config = loadConfig();
615
727
  const providerConfig = config.project.providers.find((p) => p.name === name);
616
728
  if (!providerConfig) {
@@ -622,62 +734,28 @@ async function testProvider(name) {
622
734
  }
623
735
  const provider = createProvider(providerConfig);
624
736
  console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
737
+ if (options.connect && provider instanceof OmniHilbrasProvider) {
738
+ const registry = new ModelRegistry();
739
+ try {
740
+ const result = await provider.connect(registry);
741
+ printProviderStatus(result.status);
742
+ console.log(` registered: ${result.registered}`);
743
+ } catch (error) {
744
+ console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
745
+ process.exitCode = 1;
746
+ }
747
+ return;
748
+ }
625
749
  const health = await provider.healthCheck();
626
- console.log(` health: ${health.health}`);
627
- console.log(` connected: ${health.connected}`);
628
- console.log(` models: ${health.modelCount}`);
629
- if (health.message) console.log(` detail: ${health.message}`);
750
+ printProviderStatus(health);
630
751
  if (!health.connected) process.exitCode = 1;
631
752
  }
632
-
633
- // src/registry/index.ts
634
- function registryKey(provider, id) {
635
- return `${provider}:${id}`;
753
+ function printProviderStatus(status) {
754
+ console.log(` health: ${status.health}`);
755
+ console.log(` connected: ${status.connected}`);
756
+ console.log(` models: ${status.modelCount}`);
757
+ if (status.message) console.log(` detail: ${status.message}`);
636
758
  }
637
- var ModelRegistry = class {
638
- models = /* @__PURE__ */ new Map();
639
- register(model) {
640
- this.models.set(registryKey(model.provider, model.id), model);
641
- }
642
- get(provider, id) {
643
- return this.models.get(registryKey(provider, id));
644
- }
645
- list(filter = {}) {
646
- return [...this.models.values()].filter((model) => filter.provider ? model.provider === filter.provider : true).filter(
647
- (model) => filter.capability ? model.capabilities?.[filter.capability] === true : true
648
- ).sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
649
- }
650
- /** Availability validation (§27 Phase 1): present in the registry and not retired. */
651
- isAvailable(provider, id) {
652
- const model = this.get(provider, id);
653
- return model !== void 0 && (model.status ?? "available") === "available";
654
- }
655
- /** Discover models from one provider and register them. */
656
- async discoverFrom(provider) {
657
- const discovered = await provider.listModels();
658
- for (const model of discovered) this.register(model);
659
- return discovered;
660
- }
661
- /** Discover from many providers; a failing provider does not abort the rest. */
662
- async discoverAll(providers) {
663
- return Promise.all(
664
- providers.map(async (provider) => {
665
- try {
666
- return {
667
- provider: provider.config.name,
668
- models: await this.discoverFrom(provider)
669
- };
670
- } catch (error) {
671
- return {
672
- provider: provider.config.name,
673
- models: [],
674
- error: error instanceof Error ? error : new Error(String(error))
675
- };
676
- }
677
- })
678
- );
679
- }
680
- };
681
759
 
682
760
  // src/cli/commands/models.ts
683
761
  var CAPABILITIES = [
@@ -732,26 +810,354 @@ function registerModelsCommand(program) {
732
810
  });
733
811
  }
734
812
 
813
+ // src/agents/process/index.ts
814
+ import { spawn } from "child_process";
815
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
816
+ var KILL_GRACE_MS = 5e3;
817
+ function composeTaskText(input) {
818
+ const sections = [`# Objective
819
+ ${input.objective}`];
820
+ if (input.role) {
821
+ const lines = ["# Role", `${input.role.name} (${input.role.id})`];
822
+ if (input.role.responsibilities.length > 0) {
823
+ lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
824
+ }
825
+ if (input.role.expectedOutputs.length > 0) {
826
+ lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
827
+ }
828
+ sections.push(lines.join("\n"));
829
+ }
830
+ if (input.context) sections.push(`# Context
831
+ ${input.context}`);
832
+ if (input.instruction) sections.push(`# Instruction
833
+ ${input.instruction}`);
834
+ return sections.join("\n\n");
835
+ }
836
+ function buildProtocolMessages(input, agentName) {
837
+ const meta = {
838
+ taskId: input.taskId,
839
+ agent: agentName,
840
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
841
+ };
842
+ const messages = [
843
+ { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
844
+ ];
845
+ if (input.role) messages.push({ type: "ROLE", payload: input.role });
846
+ if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
847
+ if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
848
+ return messages.map((message) => JSON.stringify({ meta, message }));
849
+ }
850
+ var ProcessAgent = class {
851
+ config;
852
+ info;
853
+ log;
854
+ running = /* @__PURE__ */ new Map();
855
+ constructor(config, log = logger) {
856
+ if (!config.command) {
857
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
858
+ }
859
+ this.config = config;
860
+ this.log = log.child({ agent: config.name });
861
+ this.info = {
862
+ name: config.name,
863
+ integration: "process",
864
+ command: config.command,
865
+ cwd: config.cwd,
866
+ status: "ready",
867
+ capabilities: [config.inputMode ?? "stdin"]
868
+ };
869
+ }
870
+ async run(input) {
871
+ const command = this.config.command;
872
+ if (!command) {
873
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
874
+ }
875
+ const mode = this.config.inputMode ?? "stdin";
876
+ const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
877
+ return new Promise((resolve, reject) => {
878
+ const child = spawn(
879
+ command,
880
+ [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
881
+ {
882
+ cwd: this.config.cwd,
883
+ env: { ...process.env, ...this.config.env },
884
+ stdio: ["pipe", "pipe", "pipe"]
885
+ }
886
+ );
887
+ this.running.set(input.taskId, child);
888
+ let stdout = "";
889
+ let stderr = "";
890
+ let timedOut = false;
891
+ let settled = false;
892
+ const timer = setTimeout(() => {
893
+ timedOut = true;
894
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
895
+ child.kill("SIGTERM");
896
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
897
+ }, timeoutMs);
898
+ const settle = (finish) => {
899
+ if (settled) return;
900
+ settled = true;
901
+ clearTimeout(timer);
902
+ this.running.delete(input.taskId);
903
+ finish();
904
+ };
905
+ child.stdout?.on("data", (chunk) => {
906
+ stdout += chunk.toString();
907
+ });
908
+ child.stderr?.on("data", (chunk) => {
909
+ stderr += chunk.toString();
910
+ });
911
+ child.stdin?.on("error", () => {
912
+ });
913
+ if (mode !== "arg") {
914
+ const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
915
+ child.stdin?.write(payload);
916
+ child.stdin?.end();
917
+ }
918
+ child.on("error", (error) => {
919
+ settle(() => {
920
+ if (error.code === "ENOENT") {
921
+ reject(
922
+ new AgentError(
923
+ "AGENT_NOT_FOUND",
924
+ `Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
925
+ { cause: error }
926
+ )
927
+ );
928
+ return;
929
+ }
930
+ reject(
931
+ new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
932
+ cause: error
933
+ })
934
+ );
935
+ });
936
+ });
937
+ child.on("close", (code, signal) => {
938
+ settle(() => {
939
+ if (timedOut) {
940
+ reject(
941
+ new AgentError(
942
+ "AGENT_TIMEOUT",
943
+ `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
944
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
945
+ )
946
+ );
947
+ return;
948
+ }
949
+ const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
950
+ if (mode === "protocol") {
951
+ try {
952
+ resolve(
953
+ interpretProtocolRun({
954
+ input,
955
+ agentName: this.config.name,
956
+ stdout,
957
+ exitCode: code,
958
+ logs
959
+ })
960
+ );
961
+ } catch (error) {
962
+ reject(error);
963
+ }
964
+ return;
965
+ }
966
+ const trimmed = stdout.trim();
967
+ resolve({
968
+ taskId: input.taskId,
969
+ status: code === 0 ? "completed" : "failed",
970
+ summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
971
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
972
+ exitCode: code ?? void 0,
973
+ logs,
974
+ error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
975
+ });
976
+ });
977
+ });
978
+ });
979
+ }
980
+ /** Ask the agent to stop current work (SIGTERM). */
981
+ async cancel(taskId) {
982
+ const child = this.running.get(taskId);
983
+ if (child) child.kill("SIGTERM");
984
+ }
985
+ runningTaskIds() {
986
+ return [...this.running.keys()];
987
+ }
988
+ };
989
+ function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
990
+ const reports = [];
991
+ let summary;
992
+ let error;
993
+ let sawProtocolMessage = false;
994
+ for (const line of stdout.split("\n")) {
995
+ const trimmed2 = line.trim();
996
+ if (trimmed2.length === 0) continue;
997
+ const message = parseAgentMessage(trimmed2);
998
+ if (!message) continue;
999
+ sawProtocolMessage = true;
1000
+ if (message.type === "REPORT") {
1001
+ const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1002
+ if (report) reports.push(report);
1003
+ } else if (message.type === "COMPLETION") {
1004
+ const payload = message.payload;
1005
+ if (typeof payload?.summary === "string") summary = payload.summary;
1006
+ } else if (message.type === "ERROR") {
1007
+ const payload = message.payload;
1008
+ const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1009
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1010
+ error = `Agent error${code}: ${detail}`;
1011
+ }
1012
+ }
1013
+ if (!sawProtocolMessage && exitCode === 0) {
1014
+ throw new ProtocolError(
1015
+ `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1016
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1017
+ );
1018
+ }
1019
+ const trimmed = stdout.trim();
1020
+ return {
1021
+ taskId: input.taskId,
1022
+ status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1023
+ summary: summary ?? reports[0]?.summary,
1024
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1025
+ exitCode: exitCode ?? void 0,
1026
+ logs,
1027
+ reports: reports.length > 0 ? reports : void 0,
1028
+ error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1029
+ };
1030
+ }
1031
+ function parseAgentMessage(line) {
1032
+ try {
1033
+ const parsed = JSON.parse(line);
1034
+ if (typeof parsed.type === "string") {
1035
+ return { type: parsed.type, payload: parsed.payload };
1036
+ }
1037
+ const inner = parsed.message;
1038
+ if (inner && typeof inner.type === "string") {
1039
+ return { type: inner.type, payload: inner.payload };
1040
+ }
1041
+ } catch {
1042
+ }
1043
+ return void 0;
1044
+ }
1045
+ function normalizeReport(payload, taskId, agentName, index) {
1046
+ if (payload === null || typeof payload !== "object") return void 0;
1047
+ const raw = payload;
1048
+ if (typeof raw.summary !== "string") return void 0;
1049
+ return {
1050
+ id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
1051
+ taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
1052
+ agent: typeof raw.agent === "string" ? raw.agent : agentName,
1053
+ summary: raw.summary,
1054
+ findings: Array.isArray(raw.findings) ? raw.findings : [],
1055
+ recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
1056
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
1057
+ };
1058
+ }
1059
+ function firstLine(text, maxLength = 200) {
1060
+ const line = text.split("\n", 1)[0] ?? text;
1061
+ return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1062
+ }
1063
+
1064
+ // src/agents/factory.ts
1065
+ function createAgent(config) {
1066
+ if (config.integration === "process") {
1067
+ return new ProcessAgent(config);
1068
+ }
1069
+ if (config.integration === "native") {
1070
+ throw new OmniNodeError(
1071
+ "NOT_IMPLEMENTED",
1072
+ `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1073
+ );
1074
+ }
1075
+ throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
1076
+ }
1077
+
735
1078
  // src/cli/commands/agents.ts
1079
+ var collectArgs = (value, previous) => [...previous, value];
736
1080
  function registerAgentCommands(program) {
737
1081
  const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
738
1082
  agent.command("list").description("List configured agents.").action(async () => {
739
1083
  const config = loadConfig();
740
1084
  const agents = config.project.agents;
741
1085
  if (agents.length === 0) {
742
- console.log("No agents configured. Add one under `project.agents` in omninode.yaml.");
1086
+ console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
743
1087
  return;
744
1088
  }
745
1089
  for (const agent2 of agents) {
746
1090
  const command = agent2.command ?? "(no command)";
747
1091
  console.log(
748
- `${agent2.name} integration=${agent2.integration} command=${command}`
1092
+ `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
749
1093
  );
750
1094
  }
751
1095
  });
752
- agent.command("add").description("Register an agent (planned for Phase 3 \u2014 CLI Agent System).").action(() => {
753
- throw notImplemented("omninode agent add", "Phase 3 (CLI Agent System)");
1096
+ agent.command("add").description(
1097
+ "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
1098
+ ).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(
1099
+ "--input-mode <mode>",
1100
+ "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
1101
+ "stdin"
1102
+ ).option(
1103
+ "--timeout-ms <ms>",
1104
+ "Per-task timeout in milliseconds.",
1105
+ (value) => Number(value)
1106
+ ).action(async (options) => addAgent(options));
1107
+ agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
1108
+ }
1109
+ async function addAgent(options) {
1110
+ const candidate = agentConfigSchema.safeParse({
1111
+ name: options.name,
1112
+ type: "cli",
1113
+ command: options.command,
1114
+ ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
1115
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1116
+ input_mode: options.inputMode,
1117
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
754
1118
  });
1119
+ if (!candidate.success) {
1120
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
1121
+ throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
1122
+ }
1123
+ const newAgent = candidate.data;
1124
+ const existing = loadConfig();
1125
+ if (existing.project.agents.some((a) => a.name === newAgent.name)) {
1126
+ throw new OmniNodeError(
1127
+ "CLI_USAGE",
1128
+ `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
1129
+ );
1130
+ }
1131
+ const configPath = appendToConfigList("agents", newAgent);
1132
+ console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
1133
+ console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
1134
+ }
1135
+ async function testAgent(name) {
1136
+ const config = loadConfig();
1137
+ const agentConfig = config.project.agents.find((a) => a.name === name);
1138
+ if (!agentConfig) {
1139
+ const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
1140
+ throw new AgentError(
1141
+ "AGENT_NOT_FOUND",
1142
+ `Agent "${name}" is not configured. Configured agents: ${available}.`
1143
+ );
1144
+ }
1145
+ const agent = createAgent(agentConfig);
1146
+ console.log(
1147
+ `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
1148
+ );
1149
+ const result = await agent.run({
1150
+ taskId: `test-${Date.now()}`,
1151
+ objective: "Reply with the single word: OK"
1152
+ });
1153
+ console.log(` status: ${result.status}`);
1154
+ if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
1155
+ if (result.summary) console.log(` output: ${result.summary}`);
1156
+ if (result.rawOutput && result.rawOutput !== result.summary) {
1157
+ console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
1158
+ }
1159
+ if (result.error) console.log(` error: ${result.error}`);
1160
+ if (result.status !== "completed") process.exitCode = 1;
755
1161
  }
756
1162
 
757
1163
  // src/cli/commands/roles.ts