@hilbras/omninode 0.3.0 → 0.5.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 +38 -3
- package/dist/cli/index.js +699 -33
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +172 -36
- package/dist/index.js +521 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,21 @@ 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 TaskError = class extends OmniNodeError {
|
|
38
|
+
constructor(code, message, options = {}) {
|
|
39
|
+
super(code, message, options);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var ProtocolError = class extends OmniNodeError {
|
|
43
|
+
constructor(message, options = {}) {
|
|
44
|
+
super("PROTOCOL_VIOLATION", message, options);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
32
47
|
function isOmniNodeError(value) {
|
|
33
48
|
return value instanceof OmniNodeError;
|
|
34
49
|
}
|
|
@@ -55,6 +70,8 @@ var agentConfigSchema = z.object({
|
|
|
55
70
|
args: z.array(z.string()).optional(),
|
|
56
71
|
cwd: z.string().optional(),
|
|
57
72
|
env: z.record(z.string()).optional(),
|
|
73
|
+
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
74
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
58
75
|
metadata: z.record(z.unknown()).optional()
|
|
59
76
|
}).strict();
|
|
60
77
|
var roleConfigSchema = z.object({
|
|
@@ -166,6 +183,8 @@ function toAgentConfig(raw) {
|
|
|
166
183
|
args: raw.args,
|
|
167
184
|
cwd: raw.cwd,
|
|
168
185
|
env: raw.env,
|
|
186
|
+
inputMode: raw.input_mode,
|
|
187
|
+
timeoutMs: raw.timeout_ms,
|
|
169
188
|
metadata: raw.metadata
|
|
170
189
|
};
|
|
171
190
|
}
|
|
@@ -243,10 +262,6 @@ function registerProjectCommands(program) {
|
|
|
243
262
|
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
263
|
}
|
|
245
264
|
|
|
246
|
-
// src/cli/commands/providers.ts
|
|
247
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
248
|
-
import { parseDocument } from "yaml";
|
|
249
|
-
|
|
250
265
|
// src/providers/auth.ts
|
|
251
266
|
function resolveApiKey(config, env = process.env) {
|
|
252
267
|
if (!config.apiKeyEnvVar) return void 0;
|
|
@@ -610,12 +625,36 @@ var ModelRegistry = class {
|
|
|
610
625
|
};
|
|
611
626
|
|
|
612
627
|
// src/cli/commands/shared.ts
|
|
628
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
629
|
+
import { parseDocument } from "yaml";
|
|
613
630
|
function notImplemented(command, when) {
|
|
614
631
|
return new OmniNodeError(
|
|
615
632
|
"NOT_IMPLEMENTED",
|
|
616
633
|
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
617
634
|
);
|
|
618
635
|
}
|
|
636
|
+
function appendToConfigList(listKey, item) {
|
|
637
|
+
const configPath = findConfigFile();
|
|
638
|
+
if (!configPath) {
|
|
639
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
640
|
+
}
|
|
641
|
+
const doc = parseDocument(readFileSync2(configPath, "utf8"));
|
|
642
|
+
if (doc.getIn(["project", listKey]) === void 0) {
|
|
643
|
+
doc.setIn(["project", listKey], [item]);
|
|
644
|
+
} else {
|
|
645
|
+
doc.addIn(["project", listKey], item);
|
|
646
|
+
}
|
|
647
|
+
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
648
|
+
if (!validated.success) {
|
|
649
|
+
const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
650
|
+
throw new ConfigError(
|
|
651
|
+
"CONFIG_INVALID",
|
|
652
|
+
`Editing ${configPath} would produce an invalid configuration: ${issues}`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
writeFileSync2(configPath, doc.toString(), "utf8");
|
|
656
|
+
return configPath;
|
|
657
|
+
}
|
|
619
658
|
|
|
620
659
|
// src/cli/commands/providers.ts
|
|
621
660
|
function registerProviderCommands(program) {
|
|
@@ -675,25 +714,11 @@ async function addProvider(options) {
|
|
|
675
714
|
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
676
715
|
throw new OmniNodeError(
|
|
677
716
|
"CLI_USAGE",
|
|
678
|
-
`Provider "${newProvider.name}" already exists in ${
|
|
717
|
+
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
679
718
|
);
|
|
680
719
|
}
|
|
681
|
-
const
|
|
682
|
-
|
|
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}`
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
writeFileSync2(configPath, doc.toString(), "utf8");
|
|
696
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
|
|
720
|
+
const writtenPath = appendToConfigList("providers", newProvider);
|
|
721
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
697
722
|
if (newProvider.api_key_env_var) {
|
|
698
723
|
console.log(
|
|
699
724
|
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
@@ -790,26 +815,381 @@ function registerModelsCommand(program) {
|
|
|
790
815
|
});
|
|
791
816
|
}
|
|
792
817
|
|
|
818
|
+
// src/agents/process/index.ts
|
|
819
|
+
import { spawn } from "child_process";
|
|
820
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
821
|
+
var KILL_GRACE_MS = 5e3;
|
|
822
|
+
function composeTaskText(input) {
|
|
823
|
+
const sections = [`# Objective
|
|
824
|
+
${input.objective}`];
|
|
825
|
+
if (input.role) {
|
|
826
|
+
const lines = ["# Role", `${input.role.name} (${input.role.id})`];
|
|
827
|
+
if (input.role.responsibilities.length > 0) {
|
|
828
|
+
lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
|
|
829
|
+
}
|
|
830
|
+
if (input.role.expectedOutputs.length > 0) {
|
|
831
|
+
lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
|
|
832
|
+
}
|
|
833
|
+
sections.push(lines.join("\n"));
|
|
834
|
+
}
|
|
835
|
+
if (input.context) sections.push(`# Context
|
|
836
|
+
${input.context}`);
|
|
837
|
+
if (input.instruction) sections.push(`# Instruction
|
|
838
|
+
${input.instruction}`);
|
|
839
|
+
return sections.join("\n\n");
|
|
840
|
+
}
|
|
841
|
+
function buildProtocolMessages(input, agentName) {
|
|
842
|
+
const meta = {
|
|
843
|
+
taskId: input.taskId,
|
|
844
|
+
agent: agentName,
|
|
845
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
846
|
+
};
|
|
847
|
+
const messages = [
|
|
848
|
+
{ type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
|
|
849
|
+
];
|
|
850
|
+
if (input.role) messages.push({ type: "ROLE", payload: input.role });
|
|
851
|
+
if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
|
|
852
|
+
if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
|
|
853
|
+
return messages.map((message) => JSON.stringify({ meta, message }));
|
|
854
|
+
}
|
|
855
|
+
var ProcessAgent = class {
|
|
856
|
+
config;
|
|
857
|
+
info;
|
|
858
|
+
log;
|
|
859
|
+
running = /* @__PURE__ */ new Map();
|
|
860
|
+
constructor(config, log = logger) {
|
|
861
|
+
if (!config.command) {
|
|
862
|
+
throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
|
|
863
|
+
}
|
|
864
|
+
this.config = config;
|
|
865
|
+
this.log = log.child({ agent: config.name });
|
|
866
|
+
this.info = {
|
|
867
|
+
name: config.name,
|
|
868
|
+
integration: "process",
|
|
869
|
+
command: config.command,
|
|
870
|
+
cwd: config.cwd,
|
|
871
|
+
status: "ready",
|
|
872
|
+
capabilities: [config.inputMode ?? "stdin"]
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async run(input) {
|
|
876
|
+
const command = this.config.command;
|
|
877
|
+
if (!command) {
|
|
878
|
+
throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
|
|
879
|
+
}
|
|
880
|
+
const mode = this.config.inputMode ?? "stdin";
|
|
881
|
+
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
883
|
+
const child = spawn(
|
|
884
|
+
command,
|
|
885
|
+
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
886
|
+
{
|
|
887
|
+
cwd: this.config.cwd,
|
|
888
|
+
env: { ...process.env, ...this.config.env },
|
|
889
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
this.running.set(input.taskId, child);
|
|
893
|
+
let stdout = "";
|
|
894
|
+
let stderr = "";
|
|
895
|
+
let timedOut = false;
|
|
896
|
+
let settled = false;
|
|
897
|
+
const timer = setTimeout(() => {
|
|
898
|
+
timedOut = true;
|
|
899
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
900
|
+
child.kill("SIGTERM");
|
|
901
|
+
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
|
|
902
|
+
}, timeoutMs);
|
|
903
|
+
const settle = (finish) => {
|
|
904
|
+
if (settled) return;
|
|
905
|
+
settled = true;
|
|
906
|
+
clearTimeout(timer);
|
|
907
|
+
this.running.delete(input.taskId);
|
|
908
|
+
finish();
|
|
909
|
+
};
|
|
910
|
+
child.stdout?.on("data", (chunk) => {
|
|
911
|
+
stdout += chunk.toString();
|
|
912
|
+
});
|
|
913
|
+
child.stderr?.on("data", (chunk) => {
|
|
914
|
+
stderr += chunk.toString();
|
|
915
|
+
});
|
|
916
|
+
child.stdin?.on("error", () => {
|
|
917
|
+
});
|
|
918
|
+
if (mode !== "arg") {
|
|
919
|
+
const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
|
|
920
|
+
child.stdin?.write(payload);
|
|
921
|
+
child.stdin?.end();
|
|
922
|
+
}
|
|
923
|
+
child.on("error", (error) => {
|
|
924
|
+
settle(() => {
|
|
925
|
+
if (error.code === "ENOENT") {
|
|
926
|
+
reject(
|
|
927
|
+
new AgentError(
|
|
928
|
+
"AGENT_NOT_FOUND",
|
|
929
|
+
`Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
|
|
930
|
+
{ cause: error }
|
|
931
|
+
)
|
|
932
|
+
);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
reject(
|
|
936
|
+
new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
|
|
937
|
+
cause: error
|
|
938
|
+
})
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
child.on("close", (code, signal) => {
|
|
943
|
+
settle(() => {
|
|
944
|
+
if (timedOut) {
|
|
945
|
+
reject(
|
|
946
|
+
new AgentError(
|
|
947
|
+
"AGENT_TIMEOUT",
|
|
948
|
+
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
949
|
+
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
955
|
+
if (mode === "protocol") {
|
|
956
|
+
try {
|
|
957
|
+
resolve(
|
|
958
|
+
interpretProtocolRun({
|
|
959
|
+
input,
|
|
960
|
+
agentName: this.config.name,
|
|
961
|
+
stdout,
|
|
962
|
+
exitCode: code,
|
|
963
|
+
logs
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
reject(error);
|
|
968
|
+
}
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const trimmed = stdout.trim();
|
|
972
|
+
resolve({
|
|
973
|
+
taskId: input.taskId,
|
|
974
|
+
status: code === 0 ? "completed" : "failed",
|
|
975
|
+
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
976
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
977
|
+
exitCode: code ?? void 0,
|
|
978
|
+
logs,
|
|
979
|
+
error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/** Ask the agent to stop current work (SIGTERM). */
|
|
986
|
+
async cancel(taskId) {
|
|
987
|
+
const child = this.running.get(taskId);
|
|
988
|
+
if (child) child.kill("SIGTERM");
|
|
989
|
+
}
|
|
990
|
+
runningTaskIds() {
|
|
991
|
+
return [...this.running.keys()];
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
|
|
995
|
+
const reports = [];
|
|
996
|
+
let summary;
|
|
997
|
+
let error;
|
|
998
|
+
let sawProtocolMessage = false;
|
|
999
|
+
for (const line of stdout.split("\n")) {
|
|
1000
|
+
const trimmed2 = line.trim();
|
|
1001
|
+
if (trimmed2.length === 0) continue;
|
|
1002
|
+
const message = parseAgentMessage(trimmed2);
|
|
1003
|
+
if (!message) continue;
|
|
1004
|
+
sawProtocolMessage = true;
|
|
1005
|
+
if (message.type === "REPORT") {
|
|
1006
|
+
const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
|
|
1007
|
+
if (report) reports.push(report);
|
|
1008
|
+
} else if (message.type === "COMPLETION") {
|
|
1009
|
+
const payload = message.payload;
|
|
1010
|
+
if (typeof payload?.summary === "string") summary = payload.summary;
|
|
1011
|
+
} else if (message.type === "ERROR") {
|
|
1012
|
+
const payload = message.payload;
|
|
1013
|
+
const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
|
|
1014
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1015
|
+
error = `Agent error${code}: ${detail}`;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (!sawProtocolMessage && exitCode === 0) {
|
|
1019
|
+
throw new ProtocolError(
|
|
1020
|
+
`Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
|
|
1021
|
+
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
const trimmed = stdout.trim();
|
|
1025
|
+
return {
|
|
1026
|
+
taskId: input.taskId,
|
|
1027
|
+
status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
|
|
1028
|
+
summary: summary ?? reports[0]?.summary,
|
|
1029
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1030
|
+
exitCode: exitCode ?? void 0,
|
|
1031
|
+
logs,
|
|
1032
|
+
reports: reports.length > 0 ? reports : void 0,
|
|
1033
|
+
error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function parseAgentMessage(line) {
|
|
1037
|
+
try {
|
|
1038
|
+
const parsed = JSON.parse(line);
|
|
1039
|
+
if (typeof parsed.type === "string") {
|
|
1040
|
+
return { type: parsed.type, payload: parsed.payload };
|
|
1041
|
+
}
|
|
1042
|
+
const inner = parsed.message;
|
|
1043
|
+
if (inner && typeof inner.type === "string") {
|
|
1044
|
+
return { type: inner.type, payload: inner.payload };
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
return void 0;
|
|
1049
|
+
}
|
|
1050
|
+
function normalizeReport(payload, taskId, agentName, index) {
|
|
1051
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
1052
|
+
const raw = payload;
|
|
1053
|
+
if (typeof raw.summary !== "string") return void 0;
|
|
1054
|
+
return {
|
|
1055
|
+
id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
|
|
1056
|
+
taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
|
|
1057
|
+
agent: typeof raw.agent === "string" ? raw.agent : agentName,
|
|
1058
|
+
summary: raw.summary,
|
|
1059
|
+
findings: Array.isArray(raw.findings) ? raw.findings : [],
|
|
1060
|
+
recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
|
|
1061
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function firstLine(text, maxLength = 200) {
|
|
1065
|
+
const line = text.split("\n", 1)[0] ?? text;
|
|
1066
|
+
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/agents/registry.ts
|
|
1070
|
+
var AgentRegistry = class {
|
|
1071
|
+
agents = /* @__PURE__ */ new Map();
|
|
1072
|
+
register(agent) {
|
|
1073
|
+
this.agents.set(agent.info.name, agent);
|
|
1074
|
+
}
|
|
1075
|
+
get(name) {
|
|
1076
|
+
return this.agents.get(name);
|
|
1077
|
+
}
|
|
1078
|
+
list() {
|
|
1079
|
+
return [...this.agents.values()].sort((a, b) => a.info.name.localeCompare(b.info.name));
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/agents/factory.ts
|
|
1084
|
+
function createAgent(config) {
|
|
1085
|
+
if (config.integration === "process") {
|
|
1086
|
+
return new ProcessAgent(config);
|
|
1087
|
+
}
|
|
1088
|
+
if (config.integration === "native") {
|
|
1089
|
+
throw new OmniNodeError(
|
|
1090
|
+
"NOT_IMPLEMENTED",
|
|
1091
|
+
`Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
1095
|
+
}
|
|
1096
|
+
function buildAgentRegistry(config, log = logger) {
|
|
1097
|
+
const registry = new AgentRegistry();
|
|
1098
|
+
for (const agentConfig of config.project.agents) {
|
|
1099
|
+
try {
|
|
1100
|
+
registry.register(createAgent(agentConfig));
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
log.warn(
|
|
1103
|
+
`Skipping agent "${agentConfig.name}": ${error instanceof Error ? error.message : String(error)}`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return registry;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
793
1110
|
// src/cli/commands/agents.ts
|
|
1111
|
+
var collectArgs = (value, previous) => [...previous, value];
|
|
794
1112
|
function registerAgentCommands(program) {
|
|
795
1113
|
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
796
1114
|
agent.command("list").description("List configured agents.").action(async () => {
|
|
797
1115
|
const config = loadConfig();
|
|
798
1116
|
const agents = config.project.agents;
|
|
799
1117
|
if (agents.length === 0) {
|
|
800
|
-
console.log("No agents configured. Add one
|
|
1118
|
+
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
801
1119
|
return;
|
|
802
1120
|
}
|
|
803
1121
|
for (const agent2 of agents) {
|
|
804
1122
|
const command = agent2.command ?? "(no command)";
|
|
805
1123
|
console.log(
|
|
806
|
-
`${agent2.name} integration=${agent2.integration} command=${command}`
|
|
1124
|
+
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
807
1125
|
);
|
|
808
1126
|
}
|
|
809
1127
|
});
|
|
810
|
-
agent.command("add").description(
|
|
811
|
-
|
|
1128
|
+
agent.command("add").description(
|
|
1129
|
+
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
1130
|
+
).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(
|
|
1131
|
+
"--input-mode <mode>",
|
|
1132
|
+
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
1133
|
+
"stdin"
|
|
1134
|
+
).option(
|
|
1135
|
+
"--timeout-ms <ms>",
|
|
1136
|
+
"Per-task timeout in milliseconds.",
|
|
1137
|
+
(value) => Number(value)
|
|
1138
|
+
).action(async (options) => addAgent(options));
|
|
1139
|
+
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
1140
|
+
}
|
|
1141
|
+
async function addAgent(options) {
|
|
1142
|
+
const candidate = agentConfigSchema.safeParse({
|
|
1143
|
+
name: options.name,
|
|
1144
|
+
type: "cli",
|
|
1145
|
+
command: options.command,
|
|
1146
|
+
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
1147
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1148
|
+
input_mode: options.inputMode,
|
|
1149
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
|
|
1150
|
+
});
|
|
1151
|
+
if (!candidate.success) {
|
|
1152
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1153
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
1154
|
+
}
|
|
1155
|
+
const newAgent = candidate.data;
|
|
1156
|
+
const existing = loadConfig();
|
|
1157
|
+
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
1158
|
+
throw new OmniNodeError(
|
|
1159
|
+
"CLI_USAGE",
|
|
1160
|
+
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const configPath = appendToConfigList("agents", newAgent);
|
|
1164
|
+
console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
|
|
1165
|
+
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
1166
|
+
}
|
|
1167
|
+
async function testAgent(name) {
|
|
1168
|
+
const config = loadConfig();
|
|
1169
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
1170
|
+
if (!agentConfig) {
|
|
1171
|
+
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
1172
|
+
throw new AgentError(
|
|
1173
|
+
"AGENT_NOT_FOUND",
|
|
1174
|
+
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
const agent = createAgent(agentConfig);
|
|
1178
|
+
console.log(
|
|
1179
|
+
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
1180
|
+
);
|
|
1181
|
+
const result = await agent.run({
|
|
1182
|
+
taskId: `test-${Date.now()}`,
|
|
1183
|
+
objective: "Reply with the single word: OK"
|
|
812
1184
|
});
|
|
1185
|
+
console.log(` status: ${result.status}`);
|
|
1186
|
+
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
1187
|
+
if (result.summary) console.log(` output: ${result.summary}`);
|
|
1188
|
+
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
1189
|
+
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
1190
|
+
}
|
|
1191
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
1192
|
+
if (result.status !== "completed") process.exitCode = 1;
|
|
813
1193
|
}
|
|
814
1194
|
|
|
815
1195
|
// src/cli/commands/roles.ts
|
|
@@ -831,19 +1211,305 @@ function registerRoleCommands(program) {
|
|
|
831
1211
|
});
|
|
832
1212
|
}
|
|
833
1213
|
|
|
1214
|
+
// src/tasks/store.ts
|
|
1215
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
1216
|
+
import { dirname } from "path";
|
|
1217
|
+
var FileTaskStore = class {
|
|
1218
|
+
filePath;
|
|
1219
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1220
|
+
this.filePath = `${directory}/tasks.json`;
|
|
1221
|
+
}
|
|
1222
|
+
async save(task) {
|
|
1223
|
+
const tasks = await this.load();
|
|
1224
|
+
const index = tasks.findIndex((t) => t.id === task.id);
|
|
1225
|
+
if (index >= 0) tasks[index] = task;
|
|
1226
|
+
else tasks.push(task);
|
|
1227
|
+
await this.write(tasks);
|
|
1228
|
+
}
|
|
1229
|
+
async get(id) {
|
|
1230
|
+
const tasks = await this.load();
|
|
1231
|
+
return tasks.find((t) => t.id === id);
|
|
1232
|
+
}
|
|
1233
|
+
async list(filter = {}) {
|
|
1234
|
+
const tasks = await this.load();
|
|
1235
|
+
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));
|
|
1236
|
+
}
|
|
1237
|
+
async load() {
|
|
1238
|
+
try {
|
|
1239
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
1240
|
+
const parsed = JSON.parse(raw);
|
|
1241
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1242
|
+
} catch {
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async write(tasks) {
|
|
1247
|
+
const tmp = `${this.filePath}.tmp`;
|
|
1248
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
1249
|
+
await writeFile(tmp, `${JSON.stringify(tasks, null, 2)}
|
|
1250
|
+
`, "utf8");
|
|
1251
|
+
await rename(tmp, this.filePath);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// src/tasks/engine.ts
|
|
1256
|
+
import { randomBytes } from "crypto";
|
|
1257
|
+
var ALLOWED_TRANSITIONS = {
|
|
1258
|
+
created: ["queued", "running", "cancelled"],
|
|
1259
|
+
queued: ["running", "cancelled"],
|
|
1260
|
+
running: ["completed", "failed", "cancelled"],
|
|
1261
|
+
waiting: ["running", "cancelled"],
|
|
1262
|
+
completed: [],
|
|
1263
|
+
failed: [],
|
|
1264
|
+
cancelled: []
|
|
1265
|
+
};
|
|
1266
|
+
var TaskEngine = class {
|
|
1267
|
+
constructor(options) {
|
|
1268
|
+
this.options = options;
|
|
1269
|
+
this.log = (options.log ?? logger).child({ component: "tasks" });
|
|
1270
|
+
}
|
|
1271
|
+
options;
|
|
1272
|
+
log;
|
|
1273
|
+
get store() {
|
|
1274
|
+
return this.options.store;
|
|
1275
|
+
}
|
|
1276
|
+
async create(input) {
|
|
1277
|
+
if (!input.objective || input.objective.trim().length === 0) {
|
|
1278
|
+
throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
|
|
1279
|
+
}
|
|
1280
|
+
if (input.role && !this.options.roles.get(input.role)) {
|
|
1281
|
+
throw new TaskError(
|
|
1282
|
+
"TASK_INVALID",
|
|
1283
|
+
`Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
if (input.agent && !this.options.agents.get(input.agent)) {
|
|
1287
|
+
throw new TaskError(
|
|
1288
|
+
"TASK_INVALID",
|
|
1289
|
+
`Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1293
|
+
const task = {
|
|
1294
|
+
id: generateTaskId(),
|
|
1295
|
+
objective: input.objective.trim(),
|
|
1296
|
+
status: "created",
|
|
1297
|
+
createdAt: now,
|
|
1298
|
+
updatedAt: now,
|
|
1299
|
+
...input.project !== void 0 ? { project: input.project } : {},
|
|
1300
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
1301
|
+
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
1302
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1303
|
+
};
|
|
1304
|
+
await this.options.store.save(task);
|
|
1305
|
+
this.log.info(`Created task ${task.id}.`);
|
|
1306
|
+
return task;
|
|
1307
|
+
}
|
|
1308
|
+
async queue(id) {
|
|
1309
|
+
const task = await this.mustGet(id);
|
|
1310
|
+
return this.transition(task, "queued");
|
|
1311
|
+
}
|
|
1312
|
+
/** Runs the task through its assigned agent and records the result. */
|
|
1313
|
+
async run(id) {
|
|
1314
|
+
const task = await this.mustGet(id);
|
|
1315
|
+
if (task.status !== "created" && task.status !== "queued") {
|
|
1316
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
1317
|
+
}
|
|
1318
|
+
if (!task.agent) {
|
|
1319
|
+
throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
|
|
1320
|
+
}
|
|
1321
|
+
const agent = this.options.agents.get(task.agent);
|
|
1322
|
+
if (!agent) {
|
|
1323
|
+
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1324
|
+
}
|
|
1325
|
+
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1326
|
+
const running = await this.transition(task, "running");
|
|
1327
|
+
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1328
|
+
let output;
|
|
1329
|
+
try {
|
|
1330
|
+
output = await agent.run({
|
|
1331
|
+
taskId: running.id,
|
|
1332
|
+
objective: running.objective,
|
|
1333
|
+
...role ? { role } : {},
|
|
1334
|
+
...running.context?.background ? { context: running.context.background } : {}
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1338
|
+
this.log.error(`Task ${task.id} failed: ${message}`);
|
|
1339
|
+
return this.transition(running, "failed", {
|
|
1340
|
+
result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const result = {
|
|
1344
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1345
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1346
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
1347
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
|
+
};
|
|
1349
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1350
|
+
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1351
|
+
return this.transition(running, finalStatus, { result });
|
|
1352
|
+
}
|
|
1353
|
+
async cancel(id) {
|
|
1354
|
+
const task = await this.mustGet(id);
|
|
1355
|
+
if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
|
|
1356
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
|
|
1357
|
+
}
|
|
1358
|
+
if (task.agent) {
|
|
1359
|
+
const agent = this.options.agents.get(task.agent);
|
|
1360
|
+
await agent?.cancel?.(task.id);
|
|
1361
|
+
}
|
|
1362
|
+
return this.transition(task, "cancelled");
|
|
1363
|
+
}
|
|
1364
|
+
async get(id) {
|
|
1365
|
+
return this.options.store.get(id);
|
|
1366
|
+
}
|
|
1367
|
+
async list(filter) {
|
|
1368
|
+
return this.options.store.list(filter);
|
|
1369
|
+
}
|
|
1370
|
+
async mustGet(id) {
|
|
1371
|
+
const task = await this.options.store.get(id);
|
|
1372
|
+
if (!task) {
|
|
1373
|
+
throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
|
|
1374
|
+
}
|
|
1375
|
+
return task;
|
|
1376
|
+
}
|
|
1377
|
+
async transition(task, to, extra = {}) {
|
|
1378
|
+
const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
|
|
1379
|
+
if (!allowed.includes(to)) {
|
|
1380
|
+
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1381
|
+
}
|
|
1382
|
+
const updated = {
|
|
1383
|
+
...task,
|
|
1384
|
+
status: to,
|
|
1385
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1386
|
+
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1387
|
+
};
|
|
1388
|
+
await this.options.store.save(updated);
|
|
1389
|
+
return updated;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
function generateTaskId() {
|
|
1393
|
+
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/roles/index.ts
|
|
1397
|
+
var RoleRegistry = class {
|
|
1398
|
+
roles = /* @__PURE__ */ new Map();
|
|
1399
|
+
register(role) {
|
|
1400
|
+
this.roles.set(role.id, role);
|
|
1401
|
+
}
|
|
1402
|
+
registerAll(roles) {
|
|
1403
|
+
for (const role of roles) this.register(role);
|
|
1404
|
+
}
|
|
1405
|
+
get(id) {
|
|
1406
|
+
return this.roles.get(id);
|
|
1407
|
+
}
|
|
1408
|
+
list() {
|
|
1409
|
+
return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
function buildRoleRegistry(config) {
|
|
1413
|
+
const registry = new RoleRegistry();
|
|
1414
|
+
registry.registerAll(config.project.roles);
|
|
1415
|
+
return registry;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/tasks/engine-factory.ts
|
|
1419
|
+
function createTaskEngine(config, log = logger, directory) {
|
|
1420
|
+
return new TaskEngine({
|
|
1421
|
+
agents: buildAgentRegistry(config, log),
|
|
1422
|
+
roles: buildRoleRegistry(config),
|
|
1423
|
+
store: new FileTaskStore(directory),
|
|
1424
|
+
log
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
834
1428
|
// src/cli/commands/tasks.ts
|
|
1429
|
+
var collectFiles = (value, previous) => [...previous, value];
|
|
835
1430
|
function registerTaskCommands(program) {
|
|
836
|
-
const task = program.command("task").description("Create, run and inspect tasks (
|
|
837
|
-
task.command("create").description("Create a task (
|
|
838
|
-
|
|
1431
|
+
const task = program.command("task").description("Create, run and inspect tasks (\xA713).");
|
|
1432
|
+
task.command("create <objective>").description("Create a task. Assign a role and/or agent; run it with `omninode task run`.").option("-r, --role <roleId>", "Role id from project.roles (validated at creation).").option("-a, --agent <name>", "Agent name from project.agents (validated at creation).").option("-f, --file <path>", "File or glob relevant to the task (repeatable).", collectFiles).action(async (objective, options) => {
|
|
1433
|
+
const config = loadConfig();
|
|
1434
|
+
const engine = createTaskEngine(config);
|
|
1435
|
+
const created = await engine.create({
|
|
1436
|
+
objective,
|
|
1437
|
+
project: config.project.name,
|
|
1438
|
+
...options.role !== void 0 ? { role: options.role } : {},
|
|
1439
|
+
...options.agent !== void 0 ? { agent: options.agent } : {},
|
|
1440
|
+
...options.file !== void 0 && options.file.length > 0 ? { context: { files: options.file } } : {}
|
|
1441
|
+
});
|
|
1442
|
+
console.log(`Created task ${created.id} (${created.status})`);
|
|
1443
|
+
console.log(` objective: ${created.objective}`);
|
|
1444
|
+
if (created.role) console.log(` role: ${created.role}`);
|
|
1445
|
+
if (created.agent) console.log(` agent: ${created.agent}`);
|
|
1446
|
+
if (created.context?.files) console.log(` files: ${created.context.files.join(", ")}`);
|
|
1447
|
+
console.log(`Run \`omninode task run ${created.id}\` to execute it.`);
|
|
1448
|
+
});
|
|
1449
|
+
task.command("run <task-id>").description("Run a task through its assigned agent.").action(async (taskId) => {
|
|
1450
|
+
const config = loadConfig();
|
|
1451
|
+
const engine = createTaskEngine(config);
|
|
1452
|
+
console.log(`Running task ${taskId}...`);
|
|
1453
|
+
const finished = await engine.run(taskId);
|
|
1454
|
+
printOutcome(finished);
|
|
1455
|
+
if (finished.status !== "completed") process.exitCode = 1;
|
|
1456
|
+
});
|
|
1457
|
+
task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
|
|
1458
|
+
const config = loadConfig();
|
|
1459
|
+
const engine = createTaskEngine(config);
|
|
1460
|
+
const found = await engine.get(taskId);
|
|
1461
|
+
if (!found) {
|
|
1462
|
+
throw new TaskError(
|
|
1463
|
+
"TASK_NOT_FOUND",
|
|
1464
|
+
`Task "${taskId}" does not exist in this project's task store.`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
printOutcome(found, { verbose: true });
|
|
839
1468
|
});
|
|
840
|
-
task.command("
|
|
841
|
-
|
|
1469
|
+
task.command("list").description("List tasks in the project's task store.").option("-s, --status <status>", "Filter by status (created, queued, running, completed, failed, cancelled).").action(async (options) => {
|
|
1470
|
+
const config = loadConfig();
|
|
1471
|
+
const engine = createTaskEngine(config);
|
|
1472
|
+
const tasks = await engine.list(
|
|
1473
|
+
options.status !== void 0 ? { status: options.status } : {}
|
|
1474
|
+
);
|
|
1475
|
+
if (tasks.length === 0) {
|
|
1476
|
+
console.log("No tasks found. Create one with `omninode task create`.");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
for (const t of tasks) {
|
|
1480
|
+
console.log(`${t.id} ${t.status.padEnd(10)} ${truncate(t.objective, 60)}`);
|
|
1481
|
+
}
|
|
842
1482
|
});
|
|
843
|
-
task.command("
|
|
844
|
-
|
|
1483
|
+
task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
|
|
1484
|
+
const config = loadConfig();
|
|
1485
|
+
const engine = createTaskEngine(config);
|
|
1486
|
+
const cancelled = await engine.cancel(taskId);
|
|
1487
|
+
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
845
1488
|
});
|
|
846
1489
|
}
|
|
1490
|
+
function printOutcome(task, options = {}) {
|
|
1491
|
+
console.log(` id: ${task.id}`);
|
|
1492
|
+
console.log(` status: ${task.status}`);
|
|
1493
|
+
console.log(` created: ${task.createdAt}`);
|
|
1494
|
+
if (options.verbose) {
|
|
1495
|
+
console.log(` project: ${task.project ?? "(none)"}`);
|
|
1496
|
+
if (task.role) console.log(` role: ${task.role}`);
|
|
1497
|
+
if (task.agent) console.log(` agent: ${task.agent}`);
|
|
1498
|
+
if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
|
|
1499
|
+
console.log(` objective: ${task.objective}`);
|
|
1500
|
+
}
|
|
1501
|
+
const result = task.result;
|
|
1502
|
+
if (!result) return;
|
|
1503
|
+
if (result.summary) console.log(` summary: ${result.summary}`);
|
|
1504
|
+
if (result.reports && result.reports.length > 0) {
|
|
1505
|
+
console.log(` reports: ${result.reports.length} structured report(s)`);
|
|
1506
|
+
}
|
|
1507
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
1508
|
+
if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
|
|
1509
|
+
}
|
|
1510
|
+
function truncate(text, max) {
|
|
1511
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1512
|
+
}
|
|
847
1513
|
|
|
848
1514
|
// src/cli/commands/reports.ts
|
|
849
1515
|
function registerReportCommands(program) {
|