@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.
Files changed (2) hide show
  1. package/dist/cli.js +225 -8
  2. 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 API = process.env.HARNESS_API_URL || "http://localhost:4000";
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(`${API}/projects`);
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(`${API}/projects`);
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(`${API}/projects`, {
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(API);
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 Command4 } from "commander";
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 Command4("skill").description("Manage skill artifacts");
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.6",
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"