@castlemilk/omega 0.1.6 → 0.1.8
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.js +225 -8
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -80,9 +80,201 @@ import { existsSync } from "fs";
|
|
|
80
80
|
import path from "path";
|
|
81
81
|
import { fileURLToPath } from "url";
|
|
82
82
|
import open from "open";
|
|
83
|
+
import { render } from "ink";
|
|
84
|
+
import React2 from "react";
|
|
85
|
+
|
|
86
|
+
// ../../apps/cli/src/commands/tui.tsx
|
|
87
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
88
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
89
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
90
|
+
var API = process.env.HARNESS_API_URL || "http://localhost:4000";
|
|
91
|
+
function formatTime(d) {
|
|
92
|
+
return d.toLocaleTimeString("en-US", { hour12: false });
|
|
93
|
+
}
|
|
94
|
+
function statusColor(status) {
|
|
95
|
+
switch (status) {
|
|
96
|
+
case "done":
|
|
97
|
+
return "green";
|
|
98
|
+
case "failed":
|
|
99
|
+
return "red";
|
|
100
|
+
case "in_progress":
|
|
101
|
+
return "yellow";
|
|
102
|
+
default:
|
|
103
|
+
return "gray";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function statusSymbol(status) {
|
|
107
|
+
switch (status) {
|
|
108
|
+
case "done":
|
|
109
|
+
return "\u2713";
|
|
110
|
+
case "failed":
|
|
111
|
+
return "\u2717";
|
|
112
|
+
case "in_progress":
|
|
113
|
+
return "\u25D0";
|
|
114
|
+
default:
|
|
115
|
+
return "\u25CB";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function useTasks(pollMs = 1e3) {
|
|
119
|
+
const [tasks, setTasks] = useState([]);
|
|
120
|
+
const [logs, setLogs] = useState([]);
|
|
121
|
+
const [connected, setConnected] = useState(false);
|
|
122
|
+
const previousTasks = useRef({});
|
|
123
|
+
const addLog = (message, level = "info") => {
|
|
124
|
+
setLogs((prev) => {
|
|
125
|
+
const next = [...prev, { time: formatTime(/* @__PURE__ */ new Date()), message, level }];
|
|
126
|
+
return next.slice(-50);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
let cancelled = false;
|
|
131
|
+
const tick = async () => {
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(`${API}/tasks`);
|
|
134
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
setConnected(true);
|
|
137
|
+
const current = {};
|
|
138
|
+
for (const task of data) {
|
|
139
|
+
current[task.id] = task;
|
|
140
|
+
const prev = previousTasks.current[task.id];
|
|
141
|
+
if (!prev) {
|
|
142
|
+
addLog(`New task queued: "${task.title}"`, "info");
|
|
143
|
+
} else if (prev.status !== task.status) {
|
|
144
|
+
if (task.status === "in_progress") {
|
|
145
|
+
const provider = task.provider || "unknown";
|
|
146
|
+
const model = task.model || "unknown";
|
|
147
|
+
addLog(`LLM picked up "${task.title}" \u2192 ${provider}/${model}`, "info");
|
|
148
|
+
} else if (task.status === "done") {
|
|
149
|
+
addLog(`Completed: "${task.title}"`, "success");
|
|
150
|
+
} else if (task.status === "failed") {
|
|
151
|
+
addLog(`Failed: "${task.title}"${task.error ? ` \u2014 ${task.error}` : ""}`, "error");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const id of Object.keys(previousTasks.current)) {
|
|
156
|
+
if (!current[id]) {
|
|
157
|
+
addLog(`Task removed: "${previousTasks.current[id].title}"`, "warning");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
previousTasks.current = current;
|
|
161
|
+
setTasks(data);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
setConnected(false);
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
if (message !== previousTasks.current["__last_error"]?.title) {
|
|
166
|
+
addLog(`Connection lost: ${message}`, "error");
|
|
167
|
+
previousTasks.current["__last_error"] = { title: message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!cancelled) {
|
|
171
|
+
setTimeout(tick, pollMs);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
tick();
|
|
175
|
+
return () => {
|
|
176
|
+
cancelled = true;
|
|
177
|
+
};
|
|
178
|
+
}, [pollMs]);
|
|
179
|
+
return { tasks, logs, connected };
|
|
180
|
+
}
|
|
181
|
+
function Header({ connected }) {
|
|
182
|
+
return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", paddingX: 1, children: [
|
|
183
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Omega Harness Console" }),
|
|
184
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
185
|
+
/* @__PURE__ */ jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25CF connected" : "\u25CF disconnected" }),
|
|
186
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
187
|
+
" ",
|
|
188
|
+
API
|
|
189
|
+
] })
|
|
190
|
+
] });
|
|
191
|
+
}
|
|
192
|
+
function TaskList({ tasks }) {
|
|
193
|
+
const sorted = useMemo(
|
|
194
|
+
() => [...tasks].sort(
|
|
195
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
196
|
+
),
|
|
197
|
+
[tasks]
|
|
198
|
+
);
|
|
199
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, minHeight: 10, children: [
|
|
200
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, underline: true, children: [
|
|
201
|
+
"Tasks (",
|
|
202
|
+
tasks.length,
|
|
203
|
+
")"
|
|
204
|
+
] }),
|
|
205
|
+
sorted.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No tasks yet. Create one in the web UI." }),
|
|
206
|
+
sorted.map((task) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
207
|
+
/* @__PURE__ */ jsx(Text, { color: statusColor(task.status), children: statusSymbol(task.status) }),
|
|
208
|
+
/* @__PURE__ */ jsx(Box, { width: 20, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: formatTime(new Date(task.updatedAt)) }) }),
|
|
209
|
+
/* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: statusColor(task.status), bold: true, children: task.status.toUpperCase() }) }),
|
|
210
|
+
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: task.provider ? `${task.provider}/${task.model}` : "-" }) }),
|
|
211
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { children: task.title }) })
|
|
212
|
+
] }, task.id))
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
function ConsoleLog({ logs }) {
|
|
216
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, flexGrow: 1, children: [
|
|
217
|
+
/* @__PURE__ */ jsx(Text, { bold: true, underline: true, children: "Console" }),
|
|
218
|
+
logs.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Waiting for activity..." }),
|
|
219
|
+
logs.map((log, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
220
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: log.time }),
|
|
221
|
+
/* @__PURE__ */ jsx(Text, { color: log.level, children: log.message })
|
|
222
|
+
] }, i))
|
|
223
|
+
] });
|
|
224
|
+
}
|
|
225
|
+
function Stats({ tasks }) {
|
|
226
|
+
const counts = useMemo(() => {
|
|
227
|
+
const total = tasks.length;
|
|
228
|
+
const todo = tasks.filter((t) => t.status === "todo").length;
|
|
229
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
230
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
231
|
+
const failed = tasks.filter((t) => t.status === "failed").length;
|
|
232
|
+
return { total, todo, inProgress, done, failed };
|
|
233
|
+
}, [tasks]);
|
|
234
|
+
return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", paddingX: 1, gap: 2, children: [
|
|
235
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
236
|
+
"total: ",
|
|
237
|
+
counts.total
|
|
238
|
+
] }),
|
|
239
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
240
|
+
"todo: ",
|
|
241
|
+
counts.todo
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
244
|
+
"in progress: ",
|
|
245
|
+
counts.inProgress
|
|
246
|
+
] }),
|
|
247
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
248
|
+
"done: ",
|
|
249
|
+
counts.done
|
|
250
|
+
] }),
|
|
251
|
+
/* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
252
|
+
"failed: ",
|
|
253
|
+
counts.failed
|
|
254
|
+
] })
|
|
255
|
+
] });
|
|
256
|
+
}
|
|
257
|
+
function TuiApp() {
|
|
258
|
+
const { exit } = useApp();
|
|
259
|
+
const { tasks, logs, connected } = useTasks();
|
|
260
|
+
useInput((input, key) => {
|
|
261
|
+
if (input === "q" || key.escape) {
|
|
262
|
+
exit();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: "100%", gap: 1, padding: 1, children: [
|
|
266
|
+
/* @__PURE__ */ jsx(Header, { connected }),
|
|
267
|
+
/* @__PURE__ */ jsx(Stats, { tasks }),
|
|
268
|
+
/* @__PURE__ */ jsx(TaskList, { tasks }),
|
|
269
|
+
/* @__PURE__ */ jsx(ConsoleLog, { logs }),
|
|
270
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press q or Esc to quit" })
|
|
271
|
+
] });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ../../apps/cli/src/commands/ui.ts
|
|
83
275
|
var __filename = fileURLToPath(import.meta.url);
|
|
84
276
|
var __dirname = path.dirname(__filename);
|
|
85
|
-
var
|
|
277
|
+
var API2 = process.env.HARNESS_API_URL || "http://localhost:4000";
|
|
86
278
|
function startBundledServer() {
|
|
87
279
|
const serverPath = path.resolve(__dirname, "server.js");
|
|
88
280
|
if (existsSync(serverPath)) {
|
|
@@ -92,7 +284,7 @@ function startBundledServer() {
|
|
|
92
284
|
}
|
|
93
285
|
async function isApiReady() {
|
|
94
286
|
try {
|
|
95
|
-
const res = await fetch(`${
|
|
287
|
+
const res = await fetch(`${API2}/projects`);
|
|
96
288
|
return res.ok;
|
|
97
289
|
} catch {
|
|
98
290
|
return false;
|
|
@@ -110,14 +302,14 @@ async function ensureProject() {
|
|
|
110
302
|
const cwd = process.cwd();
|
|
111
303
|
const name = path.basename(cwd);
|
|
112
304
|
try {
|
|
113
|
-
const listRes = await fetch(`${
|
|
305
|
+
const listRes = await fetch(`${API2}/projects`);
|
|
114
306
|
const projects = await listRes.json();
|
|
115
307
|
const existing = projects.find((p) => p.path === cwd);
|
|
116
308
|
if (existing) {
|
|
117
309
|
console.log(`Using existing project: ${name}`);
|
|
118
310
|
return;
|
|
119
311
|
}
|
|
120
|
-
const createRes = await fetch(`${
|
|
312
|
+
const createRes = await fetch(`${API2}/projects`, {
|
|
121
313
|
method: "POST",
|
|
122
314
|
headers: { "Content-Type": "application/json" },
|
|
123
315
|
body: JSON.stringify({ name, path: cwd })
|
|
@@ -131,9 +323,10 @@ async function ensureProject() {
|
|
|
131
323
|
console.warn("Could not auto-detect project:", err);
|
|
132
324
|
}
|
|
133
325
|
}
|
|
134
|
-
var uiCmd = new Command3("ui").description("Open the harness web UI").action(async () => {
|
|
326
|
+
var uiCmd = new Command3("ui").description("Open the harness web UI").option("--no-tui", "Do not open the terminal console alongside the web UI").action(async (options) => {
|
|
135
327
|
let server = void 0;
|
|
136
328
|
const alreadyRunning = await isApiReady();
|
|
329
|
+
const useTui = options.tui !== false;
|
|
137
330
|
if (!alreadyRunning) {
|
|
138
331
|
server = startBundledServer() ?? spawn("pnpm", ["--filter", "@omega/server", "dev"], {
|
|
139
332
|
stdio: "inherit",
|
|
@@ -148,16 +341,39 @@ var uiCmd = new Command3("ui").description("Open the harness web UI").action(asy
|
|
|
148
341
|
try {
|
|
149
342
|
await waitForApi();
|
|
150
343
|
await ensureProject();
|
|
151
|
-
await open(
|
|
344
|
+
await open(API2);
|
|
152
345
|
} catch (err) {
|
|
153
346
|
console.error(err);
|
|
154
347
|
server?.kill();
|
|
155
348
|
process.exit(1);
|
|
156
349
|
}
|
|
350
|
+
if (useTui) {
|
|
351
|
+
if (!process.stdin.isTTY) {
|
|
352
|
+
console.log("Terminal is not interactive; skipping TUI. Open the web UI above.");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const { waitUntilExit } = render(React2.createElement(TuiApp));
|
|
356
|
+
await waitUntilExit();
|
|
357
|
+
if (server && !alreadyRunning) {
|
|
358
|
+
server.kill();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ../../apps/cli/src/commands/console.ts
|
|
364
|
+
import { Command as Command4 } from "commander";
|
|
365
|
+
import { render as render2 } from "ink";
|
|
366
|
+
import React3 from "react";
|
|
367
|
+
var consoleCmd = new Command4("console").description("Open the harness TUI console").action(async () => {
|
|
368
|
+
if (!process.stdin.isTTY) {
|
|
369
|
+
console.error("The harness console requires an interactive terminal (TTY).");
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
render2(React3.createElement(TuiApp));
|
|
157
373
|
});
|
|
158
374
|
|
|
159
375
|
// ../../apps/cli/src/commands/skill.ts
|
|
160
|
-
import { Command as
|
|
376
|
+
import { Command as Command5 } from "commander";
|
|
161
377
|
import path2 from "path";
|
|
162
378
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
163
379
|
|
|
@@ -270,7 +486,7 @@ function getOutputDir() {
|
|
|
270
486
|
return path2.join(process.cwd(), "harness-skills");
|
|
271
487
|
}
|
|
272
488
|
}
|
|
273
|
-
var skillCmd = new
|
|
489
|
+
var skillCmd = new Command5("skill").description("Manage skill artifacts");
|
|
274
490
|
skillCmd.command("generate").description("Generate a harness adapter from a SKILL.md file").argument("<source>", "path to SKILL.md").action(async (source) => {
|
|
275
491
|
const outputDir = getOutputDir();
|
|
276
492
|
const manifest = await registerSkill(path2.resolve(source), outputDir);
|
|
@@ -283,5 +499,6 @@ program2.name("harness").description("Omega harness CLI").version("0.1.0").optio
|
|
|
283
499
|
program2.addCommand(projectCmd);
|
|
284
500
|
program2.addCommand(taskCmd);
|
|
285
501
|
program2.addCommand(uiCmd);
|
|
502
|
+
program2.addCommand(consoleCmd);
|
|
286
503
|
program2.addCommand(skillCmd);
|
|
287
504
|
program2.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@castlemilk/omega",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Omega Harness CLI - installable via npx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"cors": "^2.8.5",
|
|
17
17
|
"express": "^4.19.2",
|
|
18
18
|
"gray-matter": "^4.0.3",
|
|
19
|
+
"ink": "^7.1.0",
|
|
19
20
|
"open": "^10.1.0",
|
|
21
|
+
"react": "^19.2.7",
|
|
20
22
|
"yaml": "^2.4.0",
|
|
21
23
|
"zod": "^3.23.8"
|
|
22
24
|
},
|
|
@@ -32,7 +34,7 @@
|
|
|
32
34
|
"url": "git+https://github.com/castlemilk/harness.git"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
|
-
"build:cli": "esbuild ../../apps/cli/src/index.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --external:commander --external:open --external:gray-matter --external:yaml --alias:@omega/core=../../packages/core/src/index.ts --alias:@omega/skills=../../packages/skills/src/index.ts",
|
|
37
|
+
"build:cli": "esbuild ../../apps/cli/src/index.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --external:commander --external:open --external:gray-matter --external:yaml --external:ink --external:react --alias:@omega/core=../../packages/core/src/index.ts --alias:@omega/skills=../../packages/skills/src/index.ts",
|
|
36
38
|
"build:server": "esbuild src/server.ts --bundle --platform=node --format=esm --outfile=dist/server.js --external:express --external:cors --external:zod --alias:@omega/core=../../packages/core/src/index.ts --alias:@omega/providers=../../packages/providers/src/index.ts --alias:@omega/router=../../packages/router/src/index.ts",
|
|
37
39
|
"copy:web": "cp -R ../../apps/web/dist dist/web",
|
|
38
40
|
"build": "pnpm build:cli && pnpm build:server && pnpm copy:web"
|