@hilbras/omninode 0.3.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/README.md +20 -3
- package/dist/cli/index.js +374 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +96 -34
- package/dist/index.js +288 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v0.
|
|
21
|
+
**v0.4.0 — Phase 3: CLI Agent System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
|
|
22
22
|
for the full plan and roadmap).
|
|
23
23
|
|
|
24
24
|
What exists today:
|
|
@@ -35,6 +35,15 @@ What exists today:
|
|
|
35
35
|
- Provider factory mapping config to adapters; `omnihilbras` gets a
|
|
36
36
|
dedicated adapter implementing the plan's connect flow —
|
|
37
37
|
Connect → Authenticate → Fetch Models → Validate Models → Register
|
|
38
|
+
- **Agent system** (`src/agents/`):
|
|
39
|
+
- Process adapter: spawns CLI agents as child processes with lifecycle
|
|
40
|
+
management — environment, working directory, exit codes, stderr logs,
|
|
41
|
+
per-task timeouts (SIGTERM → SIGKILL) and cancellation
|
|
42
|
+
- Three input modes: `stdin` (composed task text on stdin), `arg` (prompt as
|
|
43
|
+
last argument) and `protocol` (the §21 JSON-lines task protocol —
|
|
44
|
+
`TASK`/`ROLE`/`CONTEXT`/`INSTRUCTION` envelopes in, `REPORT`/`COMPLETION`/
|
|
45
|
+
`ERROR` messages out, parsed into structured reports)
|
|
46
|
+
- Agent factory and registry (`createAgent`, `AgentRegistry`)
|
|
38
47
|
- Typed error hierarchy (`src/errors/`)
|
|
39
48
|
- Leveled logger with pluggable sink (`src/logger/`)
|
|
40
49
|
- Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
|
|
@@ -76,6 +85,14 @@ omninode agent list
|
|
|
76
85
|
omninode role list
|
|
77
86
|
```
|
|
78
87
|
|
|
88
|
+
Register and verify a CLI agent (anything executable works — `opencode`,
|
|
89
|
+
`kimi`, a custom script):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
omninode agent add --name opencode --command opencode --input-mode arg
|
|
93
|
+
omninode agent test opencode # sends a trivial task over the real adapter
|
|
94
|
+
```
|
|
95
|
+
|
|
79
96
|
See [omninode.yaml.example](omninode.yaml.example) for a full configuration.
|
|
80
97
|
|
|
81
98
|
### Configuration
|
|
@@ -107,8 +124,8 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
|
|
|
107
124
|
| --- | --- |
|
|
108
125
|
| v0.1.0 | Foundation |
|
|
109
126
|
| v0.2.0 | Provider System |
|
|
110
|
-
|
|
|
111
|
-
| v0.4.0 | Agent System |
|
|
127
|
+
| v0.3.0 | OmniHilbras Integration (optional, like every provider) |
|
|
128
|
+
| **v0.4.0** | **CLI Agent System (current)** |
|
|
112
129
|
| v0.5.0 | Roles & Tasks |
|
|
113
130
|
| v0.6.0 | Pipeline Engine |
|
|
114
131
|
| v0.7.0 | Multi-AI Reports |
|
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.
|
|
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;
|
|
@@ -610,12 +620,36 @@ var ModelRegistry = class {
|
|
|
610
620
|
};
|
|
611
621
|
|
|
612
622
|
// src/cli/commands/shared.ts
|
|
623
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
624
|
+
import { parseDocument } from "yaml";
|
|
613
625
|
function notImplemented(command, when) {
|
|
614
626
|
return new OmniNodeError(
|
|
615
627
|
"NOT_IMPLEMENTED",
|
|
616
628
|
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
617
629
|
);
|
|
618
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
|
+
}
|
|
619
653
|
|
|
620
654
|
// src/cli/commands/providers.ts
|
|
621
655
|
function registerProviderCommands(program) {
|
|
@@ -675,25 +709,11 @@ async function addProvider(options) {
|
|
|
675
709
|
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
676
710
|
throw new OmniNodeError(
|
|
677
711
|
"CLI_USAGE",
|
|
678
|
-
`Provider "${newProvider.name}" already exists in ${
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
const doc = parseDocument(readFileSync2(configPath, "utf8"));
|
|
682
|
-
if (doc.getIn(["project", "providers"]) === void 0) {
|
|
683
|
-
doc.setIn(["project", "providers"], [newProvider]);
|
|
684
|
-
} else {
|
|
685
|
-
doc.addIn(["project", "providers"], newProvider);
|
|
686
|
-
}
|
|
687
|
-
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
688
|
-
if (!validated.success) {
|
|
689
|
-
const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
690
|
-
throw new ConfigError(
|
|
691
|
-
"CONFIG_INVALID",
|
|
692
|
-
`Editing ${configPath} would produce an invalid configuration: ${issues}`
|
|
712
|
+
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
693
713
|
);
|
|
694
714
|
}
|
|
695
|
-
|
|
696
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${
|
|
715
|
+
const writtenPath = appendToConfigList("providers", newProvider);
|
|
716
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
697
717
|
if (newProvider.api_key_env_var) {
|
|
698
718
|
console.log(
|
|
699
719
|
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
@@ -790,26 +810,354 @@ function registerModelsCommand(program) {
|
|
|
790
810
|
});
|
|
791
811
|
}
|
|
792
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
|
+
|
|
793
1078
|
// src/cli/commands/agents.ts
|
|
1079
|
+
var collectArgs = (value, previous) => [...previous, value];
|
|
794
1080
|
function registerAgentCommands(program) {
|
|
795
1081
|
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
796
1082
|
agent.command("list").description("List configured agents.").action(async () => {
|
|
797
1083
|
const config = loadConfig();
|
|
798
1084
|
const agents = config.project.agents;
|
|
799
1085
|
if (agents.length === 0) {
|
|
800
|
-
console.log("No agents configured. Add one
|
|
1086
|
+
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
801
1087
|
return;
|
|
802
1088
|
}
|
|
803
1089
|
for (const agent2 of agents) {
|
|
804
1090
|
const command = agent2.command ?? "(no command)";
|
|
805
1091
|
console.log(
|
|
806
|
-
`${agent2.name} integration=${agent2.integration} command=${command}`
|
|
1092
|
+
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
807
1093
|
);
|
|
808
1094
|
}
|
|
809
1095
|
});
|
|
810
|
-
agent.command("add").description(
|
|
811
|
-
|
|
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 } : {}
|
|
812
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;
|
|
813
1161
|
}
|
|
814
1162
|
|
|
815
1163
|
// src/cli/commands/roles.ts
|