omega-code 0.4.0__py3-none-any.whl
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.
- omega/__init__.py +0 -0
- omega/__main__.py +589 -0
- omega/artifacts.py +151 -0
- omega/checkpoint.py +246 -0
- omega/compact.py +106 -0
- omega/config.py +285 -0
- omega/eval/__init__.py +3 -0
- omega/eval/cli.py +127 -0
- omega/eval/examples/plan-version-flag.yaml +11 -0
- omega/eval/examples/relative-age-negative-delta.yaml +14 -0
- omega/eval/examples/version-flag.yaml +10 -0
- omega/eval/manifest.py +129 -0
- omega/eval/prices.py +29 -0
- omega/eval/report.py +135 -0
- omega/eval/runner.py +199 -0
- omega/eval/tasks.py +97 -0
- omega/events.py +145 -0
- omega/export.py +80 -0
- omega/gitlog.py +229 -0
- omega/hooks.py +63 -0
- omega/instructions.py +103 -0
- omega/integrations.py +284 -0
- omega/keys.py +173 -0
- omega/llm.py +442 -0
- omega/loop.py +510 -0
- omega/mcp.py +490 -0
- omega/memory/__init__.py +5 -0
- omega/memory/consolidate.py +103 -0
- omega/memory/curate.py +69 -0
- omega/memory/store.py +321 -0
- omega/memory/tools.py +175 -0
- omega/migrate.py +40 -0
- omega/onboarding.py +242 -0
- omega/permissions.py +137 -0
- omega/secrets.py +173 -0
- omega/server/__init__.py +7 -0
- omega/server/__main__.py +18 -0
- omega/server/app.py +71 -0
- omega/server/auth.py +73 -0
- omega/server/manager.py +287 -0
- omega/server/models.py +123 -0
- omega/server/tasks_api.py +311 -0
- omega/server/terminals.py +245 -0
- omega/server/worker.py +186 -0
- omega/session.py +209 -0
- omega/setup.html +281 -0
- omega/setup_server.py +452 -0
- omega/skills.py +158 -0
- omega/subagent.py +98 -0
- omega/tasks.py +195 -0
- omega/tools.py +590 -0
- omega/trace.py +156 -0
- omega/trajectory.py +146 -0
- omega/ui/__init__.py +0 -0
- omega/ui/composer.py +140 -0
- omega/ui/format.py +708 -0
- omega/ui/plain.py +141 -0
- omega/ui/tui/__init__.py +9 -0
- omega/ui/tui/app.py +958 -0
- omega/ui/tui/history.py +50 -0
- omega/ui/tui/modals.py +292 -0
- omega/ui/tui/onboarding.py +367 -0
- omega/ui/tui/prefs.py +25 -0
- omega/ui/tui/sidebar.py +510 -0
- omega/ui/tui/status.py +115 -0
- omega/ui/tui/theme.py +91 -0
- omega/ui/tui/transcript.py +783 -0
- omega/verify.py +133 -0
- omega_code-0.4.0.dist-info/METADATA +479 -0
- omega_code-0.4.0.dist-info/RECORD +73 -0
- omega_code-0.4.0.dist-info/WHEEL +4 -0
- omega_code-0.4.0.dist-info/entry_points.txt +2 -0
- omega_code-0.4.0.dist-info/licenses/LICENSE +21 -0
omega/server/worker.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""The child process spawned per running task (see manager.py). One process =
|
|
2
|
+
one task, so the process-wide globals `tools.CONFIRM`/`ASK_USER`/`SESSION_ID`
|
|
3
|
+
and `os.getcwd()` are safe to set here without racing any other task.
|
|
4
|
+
|
|
5
|
+
Wire protocol, newline-delimited JSON both ways:
|
|
6
|
+
|
|
7
|
+
Parent -> child (stdin):
|
|
8
|
+
{"cmd": "prompt", "text": str}
|
|
9
|
+
{"cmd": "answer", "request_id": str, "answer": str}
|
|
10
|
+
{"cmd": "confirm", "request_id": str, "allow": bool}
|
|
11
|
+
{"cmd": "cancel"}
|
|
12
|
+
{"cmd": "set_model", "model": str | None}
|
|
13
|
+
{"cmd": "set_mode", "mode": str}
|
|
14
|
+
{"cmd": "shutdown"}
|
|
15
|
+
|
|
16
|
+
Child -> parent (stdout), one JSON object per line:
|
|
17
|
+
{"type": "worker_ready"}
|
|
18
|
+
{"type": <events.Event dataclass name>, ...fields, "t": epoch, "turn": n}
|
|
19
|
+
{"type": "ask_user_request", "request_id", "question", "options", "multi_select"}
|
|
20
|
+
{"type": "confirm_request", "request_id", "tool", "args_preview", "why"}
|
|
21
|
+
{"type": "fatal", "message": str} -- unrecoverable startup failure
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import dataclasses
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import secrets
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from .. import config, events, loop, session, tasks, tools
|
|
35
|
+
from ..session import Message
|
|
36
|
+
from ..ui import format
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Worker:
|
|
40
|
+
def __init__(self, task_id: str) -> None:
|
|
41
|
+
self.task_id = task_id
|
|
42
|
+
self.turn = 0
|
|
43
|
+
self.mode = "build"
|
|
44
|
+
self.model: str | None = None
|
|
45
|
+
self.history: list[Message] = []
|
|
46
|
+
self.cfg: config.Config | None = None
|
|
47
|
+
self.sess: session.Session | None = None
|
|
48
|
+
self._pending: dict[str, asyncio.Future[Any]] = {}
|
|
49
|
+
self._turn_task: asyncio.Task[None] | None = None
|
|
50
|
+
|
|
51
|
+
def _emit_line(self, obj: dict[str, Any]) -> None:
|
|
52
|
+
sys.stdout.write(json.dumps(obj) + "\n")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
55
|
+
def emit(self, ev: events.Event) -> None:
|
|
56
|
+
self._emit_line({"type": type(ev).__name__, **dataclasses.asdict(ev),
|
|
57
|
+
"t": time.time(), "turn": self.turn})
|
|
58
|
+
|
|
59
|
+
async def confirm(self, name: str, args: dict[str, Any], why: str) -> bool:
|
|
60
|
+
request_id = secrets.token_hex(8)
|
|
61
|
+
fut: asyncio.Future[bool] = asyncio.get_event_loop().create_future()
|
|
62
|
+
self._pending[request_id] = fut
|
|
63
|
+
try:
|
|
64
|
+
preview = format.describe_call(name, args)
|
|
65
|
+
except Exception:
|
|
66
|
+
preview = str(args)
|
|
67
|
+
self._emit_line({"type": "confirm_request", "request_id": request_id,
|
|
68
|
+
"tool": name, "args_preview": preview, "why": why})
|
|
69
|
+
return await fut
|
|
70
|
+
|
|
71
|
+
async def ask_user(self, question: str, options: list[events.Option],
|
|
72
|
+
multi_select: bool) -> str:
|
|
73
|
+
request_id = secrets.token_hex(8)
|
|
74
|
+
fut: asyncio.Future[str] = asyncio.get_event_loop().create_future()
|
|
75
|
+
self._pending[request_id] = fut
|
|
76
|
+
self._emit_line({"type": "ask_user_request", "request_id": request_id,
|
|
77
|
+
"question": question, "options": list(options),
|
|
78
|
+
"multi_select": multi_select})
|
|
79
|
+
return await fut
|
|
80
|
+
|
|
81
|
+
def _resolve(self, request_id: object, value: Any) -> None:
|
|
82
|
+
if not isinstance(request_id, str):
|
|
83
|
+
return
|
|
84
|
+
fut = self._pending.pop(request_id, None)
|
|
85
|
+
if fut is not None and not fut.done():
|
|
86
|
+
fut.set_result(value)
|
|
87
|
+
|
|
88
|
+
async def _run_turn(self, text: str) -> None:
|
|
89
|
+
assert self.cfg is not None and self.sess is not None
|
|
90
|
+
self.turn += 1
|
|
91
|
+
self.history.append({"role": "user", "content": text})
|
|
92
|
+
interrupted = False
|
|
93
|
+
try:
|
|
94
|
+
await loop.run_turn(self.cfg, self.history, mode=self.mode,
|
|
95
|
+
emit=self.emit, model=self.model)
|
|
96
|
+
except asyncio.CancelledError:
|
|
97
|
+
interrupted = True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.emit(events.Error(message=f"{type(e).__name__}: {e}"))
|
|
100
|
+
finally:
|
|
101
|
+
try:
|
|
102
|
+
self.sess.close_turn(self.history, self.mode, interrupted)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self._emit_line({"type": "fatal", "message": f"could not save session: {e}"})
|
|
105
|
+
self._turn_task = None
|
|
106
|
+
|
|
107
|
+
async def _dispatch(self, cmd: dict[str, Any]) -> bool:
|
|
108
|
+
"""Returns False when the worker should exit."""
|
|
109
|
+
kind = cmd.get("cmd")
|
|
110
|
+
if kind == "prompt":
|
|
111
|
+
if self._turn_task is not None and not self._turn_task.done():
|
|
112
|
+
self._emit_line({"type": "error", "message": "a turn is already running"})
|
|
113
|
+
else:
|
|
114
|
+
self._turn_task = asyncio.create_task(self._run_turn(str(cmd.get("text", ""))))
|
|
115
|
+
elif kind == "answer":
|
|
116
|
+
self._resolve(cmd.get("request_id"), str(cmd.get("answer", "")))
|
|
117
|
+
elif kind == "confirm":
|
|
118
|
+
self._resolve(cmd.get("request_id"), bool(cmd.get("allow", False)))
|
|
119
|
+
elif kind == "cancel":
|
|
120
|
+
if self._turn_task is not None:
|
|
121
|
+
self._turn_task.cancel()
|
|
122
|
+
elif kind == "set_mode":
|
|
123
|
+
self.mode = str(cmd.get("mode", self.mode))
|
|
124
|
+
elif kind == "set_model":
|
|
125
|
+
self.model = cmd.get("model") or None
|
|
126
|
+
elif kind == "jobs":
|
|
127
|
+
self._emit_line({"type": "jobs", "request_id": cmd.get("request_id"),
|
|
128
|
+
"jobs": tools.list_jobs()})
|
|
129
|
+
elif kind == "shutdown":
|
|
130
|
+
if self._turn_task is not None:
|
|
131
|
+
self._turn_task.cancel()
|
|
132
|
+
await asyncio.gather(self._turn_task, return_exceptions=True)
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
async def run(self) -> None:
|
|
137
|
+
task = tasks.get(self.task_id)
|
|
138
|
+
if task is None:
|
|
139
|
+
self._emit_line({"type": "fatal", "message": f"no such task {self.task_id!r}"})
|
|
140
|
+
return
|
|
141
|
+
os.chdir(task.cwd)
|
|
142
|
+
self.cfg = config.load()
|
|
143
|
+
self.mode = task.mode
|
|
144
|
+
self.model = task.model
|
|
145
|
+
self.sess = session.load(self.task_id)
|
|
146
|
+
self.history = self.sess.history
|
|
147
|
+
tools.SESSION_ID = self.sess.id
|
|
148
|
+
tools.CONFIRM = self.confirm
|
|
149
|
+
tools.ASK_USER = self.ask_user
|
|
150
|
+
self.turn = self.sess.turns
|
|
151
|
+
|
|
152
|
+
reader = await _stdin_reader()
|
|
153
|
+
self._emit_line({"type": "worker_ready"})
|
|
154
|
+
while True:
|
|
155
|
+
raw = await reader.readline()
|
|
156
|
+
if not raw:
|
|
157
|
+
break
|
|
158
|
+
try:
|
|
159
|
+
cmd = json.loads(raw)
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
continue
|
|
162
|
+
if not await self._dispatch(cmd):
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def _stdin_reader() -> asyncio.StreamReader:
|
|
167
|
+
reader = asyncio.StreamReader()
|
|
168
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
169
|
+
loop_ = asyncio.get_event_loop()
|
|
170
|
+
await loop_.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
171
|
+
return reader
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def _amain(task_id: str) -> None:
|
|
175
|
+
await Worker(task_id).run()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main() -> None:
|
|
179
|
+
if len(sys.argv) < 2:
|
|
180
|
+
print("usage: python -m omega.server.worker <task_id>", file=sys.stderr)
|
|
181
|
+
raise SystemExit(2)
|
|
182
|
+
asyncio.run(_amain(sys.argv[1]))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
omega/session.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
Message = dict[str, Any]
|
|
10
|
+
|
|
11
|
+
DIR = Path.home() / ".omega" / "sessions"
|
|
12
|
+
|
|
13
|
+
# Shared with __main__'s resumed-session marker so UIs can identify and
|
|
14
|
+
# exclude it from turn/message counts shown to the user.
|
|
15
|
+
RESUME_PREFIX = "[session resumed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Session:
|
|
20
|
+
id: str = ""
|
|
21
|
+
cwd: str = ""
|
|
22
|
+
mode: str = "build"
|
|
23
|
+
created: float = 0.0
|
|
24
|
+
updated: float = 0.0
|
|
25
|
+
history: list[Message] = field(default_factory=list)
|
|
26
|
+
compactions: int = 0
|
|
27
|
+
model_override: str | None = None
|
|
28
|
+
# Set by load() when the turn log (.jsonl) held more messages than the
|
|
29
|
+
# last saved .json -- i.e. the process crashed mid-turn. Never persisted:
|
|
30
|
+
# it describes this one load, not the session itself.
|
|
31
|
+
recovered: int = 0
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def new(cls, cwd: str | None = None, mode: str = "build") -> "Session":
|
|
35
|
+
now = time.time()
|
|
36
|
+
sid = time.strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:4]
|
|
37
|
+
return cls(id=sid, cwd=cwd or os.getcwd(), mode=mode, created=now, updated=now)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def path(self) -> Path:
|
|
41
|
+
return DIR / f"{self.id}.json"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def jsonl_path(self) -> Path:
|
|
45
|
+
return DIR / f"{self.id}.jsonl"
|
|
46
|
+
|
|
47
|
+
def append(self, msg: Message) -> None:
|
|
48
|
+
"""Add one message to history AND durably log it immediately -- unlike
|
|
49
|
+
`save()` (a full rewrite that only happens at turn boundaries), this
|
|
50
|
+
makes a crash mid-turn resumable: see `load()`'s jsonl-vs-json check."""
|
|
51
|
+
self.history.append(msg)
|
|
52
|
+
log_message(self.id, msg)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def turns(self) -> int:
|
|
56
|
+
return sum(1 for m in self.history if m.get("role") == "user")
|
|
57
|
+
|
|
58
|
+
def first_prompt(self, width: int = 70) -> str:
|
|
59
|
+
for m in self.history:
|
|
60
|
+
if m.get("role") == "user":
|
|
61
|
+
text = " ".join((m.get("content") or "").split())
|
|
62
|
+
return text[:width - 1] + "…" if len(text) > width else text
|
|
63
|
+
return "(empty)"
|
|
64
|
+
|
|
65
|
+
def save(self) -> None:
|
|
66
|
+
DIR.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
self.updated = time.time()
|
|
68
|
+
data = asdict(self)
|
|
69
|
+
data.pop("recovered", None) # a fact about this load, not the session
|
|
70
|
+
tmp = self.path.with_suffix(".tmp")
|
|
71
|
+
tmp.write_text(json.dumps(data, indent=1))
|
|
72
|
+
tmp.replace(self.path)
|
|
73
|
+
|
|
74
|
+
def close_turn(self, history: list[dict[str, Any]], mode: str, interrupted: bool) -> None:
|
|
75
|
+
"""Shared by every UI: whatever ran the turn, the session must land in
|
|
76
|
+
the same state afterwards."""
|
|
77
|
+
self.mode = mode
|
|
78
|
+
repair(history)
|
|
79
|
+
if interrupted:
|
|
80
|
+
history.append({"role": "user",
|
|
81
|
+
"content": "[previous turn interrupted by user]"})
|
|
82
|
+
self._sync_jsonl(history)
|
|
83
|
+
self.save()
|
|
84
|
+
|
|
85
|
+
def _sync_jsonl(self, history: list[Message]) -> None:
|
|
86
|
+
"""Nothing in this codebase yet calls `append()` per-message during a
|
|
87
|
+
turn, so bring the turn log up to date at the turn boundary instead --
|
|
88
|
+
the log is then complete as of every close_turn(), even before a
|
|
89
|
+
caller is wired to append() live. A no-op once the log has caught up."""
|
|
90
|
+
path = self.jsonl_path
|
|
91
|
+
existing = len(_read_jsonl(path)) if path.exists() else 0
|
|
92
|
+
if existing >= len(history):
|
|
93
|
+
return
|
|
94
|
+
DIR.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
tmp = path.with_suffix(".jsonl.tmp")
|
|
96
|
+
with tmp.open("w") as f:
|
|
97
|
+
for msg in history:
|
|
98
|
+
f.write(json.dumps(msg) + "\n")
|
|
99
|
+
tmp.replace(path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def log_message(session_id: str, msg: Message) -> None:
|
|
103
|
+
"""Append one message to `<session_id>.jsonl` without needing a `Session`
|
|
104
|
+
object loaded -- the primitive `Session.append` builds on."""
|
|
105
|
+
DIR.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
with (DIR / f"{session_id}.jsonl").open("a") as f:
|
|
107
|
+
f.write(json.dumps(msg) + "\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_jsonl(path: Path) -> list[Message]:
|
|
111
|
+
"""Tolerates a truncated trailing line -- the shape a hard crash mid-write
|
|
112
|
+
leaves, since appends are not fsynced. Anything after the first bad line
|
|
113
|
+
is dropped rather than raising, since the log is append-only sequential."""
|
|
114
|
+
messages: list[Message] = []
|
|
115
|
+
for line in path.read_text().splitlines():
|
|
116
|
+
line = line.strip()
|
|
117
|
+
if not line:
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
messages.append(json.loads(line))
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
break
|
|
123
|
+
return messages
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load(sid: str) -> Session:
|
|
127
|
+
p = DIR / f"{sid}.json"
|
|
128
|
+
if not p.exists():
|
|
129
|
+
matches = sorted(DIR.glob(f"{sid}*.json")) if DIR.exists() else []
|
|
130
|
+
if not matches:
|
|
131
|
+
raise SystemExit(f"omega: no session {sid!r}")
|
|
132
|
+
p = matches[-1]
|
|
133
|
+
raw = json.loads(p.read_text())
|
|
134
|
+
known = set(Session.__dataclass_fields__)
|
|
135
|
+
sess = Session(**{k: v for k, v in raw.items() if k in known})
|
|
136
|
+
sess.recovered = 0
|
|
137
|
+
|
|
138
|
+
jsonl = DIR / f"{sess.id}.jsonl"
|
|
139
|
+
if jsonl.exists():
|
|
140
|
+
logged = _read_jsonl(jsonl)
|
|
141
|
+
if len(logged) > len(sess.history):
|
|
142
|
+
sess.recovered = len(logged) - len(sess.history)
|
|
143
|
+
sess.history = logged
|
|
144
|
+
|
|
145
|
+
repair(sess.history)
|
|
146
|
+
return sess
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def repair(history: list[Message]) -> list[Message]:
|
|
150
|
+
"""Drop a trailing assistant message whose tool_calls were never answered.
|
|
151
|
+
Providers hard-400 on that shape, which would brick the session forever."""
|
|
152
|
+
while history:
|
|
153
|
+
last = history[-1]
|
|
154
|
+
if last.get("role") == "assistant" and last.get("tool_calls"):
|
|
155
|
+
answered = {m.get("tool_call_id") for m in history if m.get("role") == "tool"}
|
|
156
|
+
if any(c["id"] not in answered for c in last["tool_calls"]):
|
|
157
|
+
history.pop()
|
|
158
|
+
continue
|
|
159
|
+
break
|
|
160
|
+
return history
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def all_sessions() -> list[Session]:
|
|
164
|
+
if not DIR.exists():
|
|
165
|
+
return []
|
|
166
|
+
out = []
|
|
167
|
+
for p in DIR.glob("*.json"):
|
|
168
|
+
try:
|
|
169
|
+
out.append(Session(**json.loads(p.read_text())))
|
|
170
|
+
except Exception:
|
|
171
|
+
continue
|
|
172
|
+
return sorted(out, key=lambda s: s.updated, reverse=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def latest(cwd: str | None = None) -> Session | None:
|
|
176
|
+
cwd = cwd or os.getcwd()
|
|
177
|
+
# Never fall back to another directory's session: bash and relative paths
|
|
178
|
+
# resolve against the real cwd, so the model would edit the wrong project.
|
|
179
|
+
here = [s for s in all_sessions() if s.cwd == cwd]
|
|
180
|
+
return here[0] if here else None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def render_list(limit: int = 20, cwd: str | None = None) -> str:
|
|
184
|
+
"""One session per line: wrapped rows make the list unparseable. `cwd`
|
|
185
|
+
filters to that directory's sessions only (used by `omega resume` with
|
|
186
|
+
no id, and `sessions --resume`'s hint)."""
|
|
187
|
+
rows = all_sessions()
|
|
188
|
+
if cwd is not None:
|
|
189
|
+
rows = [s for s in rows if s.cwd == cwd]
|
|
190
|
+
rows = rows[:limit]
|
|
191
|
+
if not rows:
|
|
192
|
+
return "no sessions yet"
|
|
193
|
+
now = time.time()
|
|
194
|
+
import shutil
|
|
195
|
+
cols = shutil.get_terminal_size((80, 24)).columns
|
|
196
|
+
lines = [f"{'ID':<22}{'AGE':>6}{'TURNS':>6} {'CWD':<24}PROMPT"]
|
|
197
|
+
for s in rows:
|
|
198
|
+
age = now - s.updated
|
|
199
|
+
ago = (f"{age/86400:.0f}d" if age > 86400 else
|
|
200
|
+
f"{age/3600:.0f}h" if age > 3600 else f"{age/60:.0f}m")
|
|
201
|
+
cwd = s.cwd.replace(str(Path.home()), "~")
|
|
202
|
+
if len(cwd) > 23:
|
|
203
|
+
cwd = "…" + cwd[-22:]
|
|
204
|
+
lines.append(f"{s.id:<22}{ago:>6}{s.turns:>6} {cwd:<24}"
|
|
205
|
+
f"{s.first_prompt(200)}")
|
|
206
|
+
# hard-truncate: a wrapped row makes the list unreadable and unparseable
|
|
207
|
+
# Stay strictly under the terminal width: a line of exactly `cols` chars
|
|
208
|
+
# still wraps in some terminals.
|
|
209
|
+
return "\n".join(l[:cols - 2] + "…" if len(l) >= cols - 1 else l for l in lines)
|
omega/setup.html
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8"><title>omega setup</title>
|
|
3
|
+
<style>
|
|
4
|
+
:root{--bg:#fbfbfa;--fg:#1a1a19;--dim:#6b6b68;--line:#e4e4e1;--card:#fff;--soft:#f4f4f2;
|
|
5
|
+
--ok:#1a7f4b;--err:#b3261e;--accent:#2b6cb0}
|
|
6
|
+
@media(prefers-color-scheme:dark){:root{--bg:#191918;--fg:#e8e8e5;--dim:#96968f;--line:#33332f;
|
|
7
|
+
--card:#212120;--soft:#262625;--ok:#4ade80;--err:#f87171;--accent:#7cb3ea}}
|
|
8
|
+
*{box-sizing:border-box}
|
|
9
|
+
body{margin:0;padding:0;background:var(--bg);color:var(--fg);
|
|
10
|
+
font:14px/1.55 ui-sans-serif,-apple-system,system-ui,sans-serif}
|
|
11
|
+
main{max-width:680px;margin:0 auto;padding:48px 24px 80px}
|
|
12
|
+
h1{font-size:26px;margin:0 0 6px;letter-spacing:-.02em}
|
|
13
|
+
h2{font-size:19px;margin:0 0 6px;letter-spacing:-.01em}
|
|
14
|
+
.sub{color:var(--dim);margin:0 0 26px}
|
|
15
|
+
.steps{display:flex;gap:6px;margin-bottom:34px}
|
|
16
|
+
.steps div{height:3px;flex:1;background:var(--line);border-radius:2px;transition:background .25s}
|
|
17
|
+
.steps div.done{background:var(--ok)}.steps div.now{background:var(--accent)}
|
|
18
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:26px}
|
|
19
|
+
label{display:block;font-size:12px;color:var(--dim);margin:14px 0 5px}
|
|
20
|
+
input,select{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:7px;
|
|
21
|
+
background:var(--bg);color:var(--fg);font:13px ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
22
|
+
input:focus,select:focus{outline:2px solid var(--accent);outline-offset:-1px}
|
|
23
|
+
button{padding:9px 16px;border:1px solid var(--line);border-radius:7px;background:var(--bg);
|
|
24
|
+
color:var(--fg);font-size:13px;cursor:pointer}
|
|
25
|
+
button:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
|
|
26
|
+
button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
27
|
+
button.primary:hover:not(:disabled){opacity:.9;color:#fff}
|
|
28
|
+
button:disabled{opacity:.45;cursor:default}
|
|
29
|
+
.nav{display:flex;justify-content:space-between;margin-top:26px}
|
|
30
|
+
.ok{color:var(--ok)}.err{color:var(--err)}.dim{color:var(--dim)}
|
|
31
|
+
.mono{font:12px ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
32
|
+
.msg{margin-top:12px;font-size:13px;min-height:20px}
|
|
33
|
+
table{width:100%;border-collapse:collapse;font-size:13px;margin-top:6px}
|
|
34
|
+
td,th{padding:7px 4px;border-bottom:1px solid var(--line);text-align:left}
|
|
35
|
+
th{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);font-weight:600}
|
|
36
|
+
tr.best td{background:var(--soft)}
|
|
37
|
+
.role{display:grid;grid-template-columns:120px 1fr;gap:10px;align-items:center;margin-bottom:9px}
|
|
38
|
+
.role .n{font-size:12px}.role .n b{display:block;font:12px ui-monospace,monospace}
|
|
39
|
+
pre{background:var(--soft);border:1px solid var(--line);border-radius:7px;padding:13px;
|
|
40
|
+
overflow-x:auto;font:12px ui-monospace,monospace;margin:12px 0 0}
|
|
41
|
+
.pill{display:inline-block;padding:2px 8px;border-radius:20px;font-size:11px;
|
|
42
|
+
background:var(--soft);border:1px solid var(--line);margin:2px 4px 2px 0}
|
|
43
|
+
h3.sec{margin:20px 0 6px;font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:var(--dim)}
|
|
44
|
+
.cathead{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);
|
|
45
|
+
margin:16px 0 4px;font-weight:600}
|
|
46
|
+
.introw{display:flex;align-items:center;gap:10px;padding:8px 2px;border-bottom:1px solid var(--line)}
|
|
47
|
+
.introw .meta{flex:1;min-width:0}
|
|
48
|
+
.introw .meta .b{font-size:12px;color:var(--dim);margin-top:1px}
|
|
49
|
+
#catSearch{margin:8px 0}
|
|
50
|
+
button.small{padding:5px 10px;font-size:12px}
|
|
51
|
+
</style>
|
|
52
|
+
<main>
|
|
53
|
+
<h1>omega</h1><p class="sub" id="tagline">Let's get you running.</p>
|
|
54
|
+
<div class="steps" id="steps"></div>
|
|
55
|
+
<div class="card" id="card"></div>
|
|
56
|
+
<div class="nav"><button id="back" onclick="go(-1)">Back</button>
|
|
57
|
+
<button id="next" class="primary" onclick="go(1)">Continue</button></div>
|
|
58
|
+
</main>
|
|
59
|
+
<script>
|
|
60
|
+
const T = new URLSearchParams(location.search).get("t");
|
|
61
|
+
// /api/* is POST-only: GET dispatch was a CSRF-reachable RCE.
|
|
62
|
+
const api=(p,b)=>fetch(p+"?t="+T,{method:"POST",headers:{"Content-Type":"application/json",
|
|
63
|
+
"X-Omega-Token":T},body:JSON.stringify(b||{})}).then(r=>r.json());
|
|
64
|
+
const $=s=>document.querySelector(s);
|
|
65
|
+
let cfg=null, models=[], bench=[], step=0, keyOk=false, ranTest=false;
|
|
66
|
+
|
|
67
|
+
const ROLES={main:["Main","drives your session — pick the smartest"],
|
|
68
|
+
plan:["Planning","read-only research and plans"],
|
|
69
|
+
subagent_fast:["Fast subagent","bounded lookups — pick the quickest"],
|
|
70
|
+
subagent_mid:["Mid subagent","reasoning across files"],
|
|
71
|
+
compact:["Compaction","summarises old context"]};
|
|
72
|
+
|
|
73
|
+
const STEPS=[
|
|
74
|
+
{t:"Provider",r:()=>`<h2>Connect a provider</h2>
|
|
75
|
+
<p class="dim">omega talks to Anthropic natively, or any OpenAI-compatible endpoint.</p>
|
|
76
|
+
<label>Provider type</label>
|
|
77
|
+
<select id="providerType" onchange="onProviderTypeChange()">
|
|
78
|
+
<option value="openai"${Object.values(cfg.providers)[0].type!=='anthropic'?' selected':''}>OpenAI-compatible</option>
|
|
79
|
+
<option value="anthropic"${Object.values(cfg.providers)[0].type==='anthropic'?' selected':''}>Anthropic (native)</option>
|
|
80
|
+
</select>
|
|
81
|
+
<div id="baseUrlRow" style="${Object.values(cfg.providers)[0].type==='anthropic'?'display:none':''}">
|
|
82
|
+
<label>Base URL</label><input id="baseUrl" value="${Object.values(cfg.providers)[0].baseUrl||''}"></div>
|
|
83
|
+
<label>API key ${Object.values(cfg.providers)[0].hasKey?'<span class="ok">· already stored</span>':''}</label>
|
|
84
|
+
<input id="apiKey" type="password" placeholder="${Object.values(cfg.providers)[0].hasKey?'•••••••••••• (stored)':'sk-…'}">
|
|
85
|
+
<div class="msg" id="m"></div>`,
|
|
86
|
+
enter(){ if(Object.values(cfg.providers)[0].hasKey) verify(); },
|
|
87
|
+
next:()=>keyOk||verify()},
|
|
88
|
+
|
|
89
|
+
{t:"Models",r:()=>`<h2>Pick your models</h2>
|
|
90
|
+
<p class="dim">${models.length} available. Measure them, then assign roles.</p>
|
|
91
|
+
<button onclick="runBench()" id="bb">Measure latency</button>
|
|
92
|
+
<span class="mono dim" id="bs"></span>
|
|
93
|
+
<div id="bt"></div><div id="rs" style="margin-top:20px"></div>`,
|
|
94
|
+
enter(){ renderRoles(); }},
|
|
95
|
+
|
|
96
|
+
{t:"Tools",r:()=>`<h2>Connect tools</h2>
|
|
97
|
+
<p class="dim">Connect from the catalog, import what Claude Code already has, or manage
|
|
98
|
+
what omega is already using. Tools stay out of the prompt until the model searches for them.</p>
|
|
99
|
+
<div id="mc" class="dim">loading…</div>`,
|
|
100
|
+
enter(){ loadConnections(); }},
|
|
101
|
+
|
|
102
|
+
{t:"Test",r:()=>`<h2>Take it for a spin</h2>
|
|
103
|
+
<p class="dim">Runs one real turn against your config — proof it works end to end.</p>
|
|
104
|
+
<button onclick="drive()" id="db" class="primary">Run test</button>
|
|
105
|
+
<div id="dr"></div>`},
|
|
106
|
+
|
|
107
|
+
{t:"Done",r:()=>`<h2>You're set</h2>
|
|
108
|
+
<p class="dim">Config written to <span class="mono">~/.omega/config.json</span>.</p>
|
|
109
|
+
<pre>omega # interactive
|
|
110
|
+
omega "fix the failing test"
|
|
111
|
+
omega --plan "add caching" # read-only planning
|
|
112
|
+
omega --mcp "list my Linear issues"</pre>
|
|
113
|
+
<p class="dim" style="margin-top:16px">Re-run this anytime with
|
|
114
|
+
<span class="mono">omega setup</span>.</p>`}];
|
|
115
|
+
|
|
116
|
+
function render(){
|
|
117
|
+
$("#steps").innerHTML = STEPS.map((_,i)=>
|
|
118
|
+
`<div class="${i<step?'done':i===step?'now':''}"></div>`).join("");
|
|
119
|
+
$("#card").innerHTML = STEPS[step].r();
|
|
120
|
+
$("#back").style.visibility = step? "visible":"hidden";
|
|
121
|
+
$("#next").textContent = step===STEPS.length-1? "Finish":"Continue";
|
|
122
|
+
$("#tagline").textContent = STEPS[step].t;
|
|
123
|
+
STEPS[step].enter?.();
|
|
124
|
+
}
|
|
125
|
+
async function go(d){
|
|
126
|
+
if(d>0 && STEPS[step].next && !(await STEPS[step].next())) return;
|
|
127
|
+
if(d>0 && step===STEPS.length-1){ await save(); $("#next").textContent="Saved"; return; }
|
|
128
|
+
if(d>0) await save();
|
|
129
|
+
step=Math.max(0,Math.min(STEPS.length-1,step+d)); render();
|
|
130
|
+
}
|
|
131
|
+
function onProviderTypeChange(){
|
|
132
|
+
const p=Object.values(cfg.providers)[0], isAnthropic=$("#providerType").value==="anthropic";
|
|
133
|
+
p.type=$("#providerType").value;
|
|
134
|
+
$("#baseUrlRow").style.display = isAnthropic ? "none" : "";
|
|
135
|
+
}
|
|
136
|
+
async function verify(){
|
|
137
|
+
const m=$("#m"); m.textContent="checking…"; m.className="msg dim";
|
|
138
|
+
const p=Object.values(cfg.providers)[0];
|
|
139
|
+
p.type=$("#providerType").value;
|
|
140
|
+
const body={}; const k=$("#apiKey")?.value; if(k) body.apiKey=k;
|
|
141
|
+
if(p.type!=='anthropic' && $("#baseUrl")) p.baseUrl=$("#baseUrl").value;
|
|
142
|
+
if(k) p.apiKey=k;
|
|
143
|
+
else delete p.apiKey; // never write back a redacted key
|
|
144
|
+
const r=await api("/api/models",body);
|
|
145
|
+
if(r.error){ m.textContent="✗ "+r.error.slice(0,90); m.className="msg err"; return keyOk=false; }
|
|
146
|
+
models=r.models; keyOk=true;
|
|
147
|
+
m.innerHTML=`<span class="ok">✓ connected — ${models.length} models available</span>`;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
async function runBench(){
|
|
151
|
+
const b=$("#bb"); b.disabled=true; $("#bs").textContent=" measuring one at a time…";
|
|
152
|
+
const r=await api("/api/benchmark",{});
|
|
153
|
+
bench=r.results||[]; b.disabled=false; $("#bs").textContent="";
|
|
154
|
+
$("#bt").innerHTML=`<table><tr><th>Model</th><th>Round trip</th></tr>`+
|
|
155
|
+
bench.map((x,i)=>`<tr class="${i===0&&x.ok?'best':''}"><td class="mono">${x.model}</td>
|
|
156
|
+
<td class="${x.ok?'ok':'err'} mono">${x.ok?x.ms+"ms":x.error}</td></tr>`).join("")+`</table>`;
|
|
157
|
+
renderRoles();
|
|
158
|
+
}
|
|
159
|
+
function renderRoles(){
|
|
160
|
+
const fast=bench.filter(b=>b.ok)[0]?.model;
|
|
161
|
+
$("#rs").innerHTML=Object.entries(ROLES).map(([k,[n,d]])=>`<div class="role">
|
|
162
|
+
<span class="n">${n}<b>${k}</b></span>
|
|
163
|
+
<select data-role="${k}">${(models.length?models:[cfg.roles[k].model])
|
|
164
|
+
.map(m=>`<option${m===cfg.roles[k].model?" selected":""}>${m}</option>`).join("")}</select>
|
|
165
|
+
</div><p class="dim" style="margin:-4px 0 12px;font-size:12px">${d}${
|
|
166
|
+
fast&&k.includes("fast")?` — fastest measured: <b>${fast}</b>`:""}</p>`).join("");
|
|
167
|
+
}
|
|
168
|
+
let connData=null, catFilter="";
|
|
169
|
+
|
|
170
|
+
async function loadConnections(){
|
|
171
|
+
connData=await api("/api/connections/list");
|
|
172
|
+
renderConnections();
|
|
173
|
+
}
|
|
174
|
+
function connStateBadge(r){
|
|
175
|
+
if(r.state==="connected") return `<span class="ok">connected</span> <span class="dim">(${r.tools})</span>`;
|
|
176
|
+
if(r.state==="needs_auth") return `<span class="err">needs auth</span>`;
|
|
177
|
+
if(r.state==="error") return `<span class="err">error</span>`;
|
|
178
|
+
if(r.state==="disabled") return `<span class="dim">disabled</span>`;
|
|
179
|
+
return `<span class="dim">configured</span>`;
|
|
180
|
+
}
|
|
181
|
+
function renderConnections(){
|
|
182
|
+
let html="";
|
|
183
|
+
if(connData.configured.length){
|
|
184
|
+
html+=`<h3 class="sec">Connected</h3><table><tr><th>Name</th><th>State</th><th></th></tr>`+
|
|
185
|
+
connData.configured.map(r=>`<tr><td>${r.name}</td><td>${connStateBadge(r)}</td>
|
|
186
|
+
<td style="width:260px">
|
|
187
|
+
<button class="small" onclick="connectServer('${r.name}')">connect</button>
|
|
188
|
+
<button class="small" onclick="testServer('${r.name}')">test</button>
|
|
189
|
+
<button class="small" onclick="toggleServer('${r.name}',${r.enabled?"false":"true"})">${r.enabled?"disable":"enable"}</button>
|
|
190
|
+
<button class="small" onclick="removeServer('${r.name}')">remove</button>
|
|
191
|
+
</td></tr>`).join("")+`</table>`;
|
|
192
|
+
}
|
|
193
|
+
if(connData.claudeCode.length){
|
|
194
|
+
html+=`<h3 class="sec">Found in Claude Code</h3><table><tr><th>Name</th><th></th><th></th></tr>`+
|
|
195
|
+
connData.claudeCode.map(c=>`<tr><td>${c.name}</td>
|
|
196
|
+
<td class="mono dim">${c.transport==="stdio"?c.command:c.url}</td>
|
|
197
|
+
<td style="width:100px"><button class="small" onclick="importClaude('${c.name}')">import</button></td>
|
|
198
|
+
</tr>`).join("")+`</table>`;
|
|
199
|
+
}
|
|
200
|
+
html+=`<h3 class="sec">Catalog</h3>
|
|
201
|
+
<input id="catSearch" placeholder="Search integrations…" value="${catFilter}"
|
|
202
|
+
oninput="catFilter=this.value;renderCatalog()">
|
|
203
|
+
<div id="catList"></div>
|
|
204
|
+
<div class="msg" id="connMsg"></div>`;
|
|
205
|
+
$("#mc").innerHTML=html;
|
|
206
|
+
renderCatalog();
|
|
207
|
+
}
|
|
208
|
+
function renderCatalog(){
|
|
209
|
+
const q=catFilter.trim().toLowerCase();
|
|
210
|
+
const groups={};
|
|
211
|
+
connData.catalog.forEach(i=>{
|
|
212
|
+
if(q && !(i.name.toLowerCase().includes(q) || i.blurb.toLowerCase().includes(q) || i.key.includes(q))) return;
|
|
213
|
+
(groups[i.category]=groups[i.category]||[]).push(i);
|
|
214
|
+
});
|
|
215
|
+
const cats=Object.keys(groups).sort();
|
|
216
|
+
$("#catList").innerHTML=cats.length ? cats.map(cat=>`
|
|
217
|
+
<div class="cathead">${cat}</div>
|
|
218
|
+
${groups[cat].map(i=>`<div class="introw">
|
|
219
|
+
<div class="meta"><b>${i.name}</b>${i.verified?"":' <span class="dim">unverified</span>'}
|
|
220
|
+
<div class="b">${i.blurb}</div></div>
|
|
221
|
+
<span class="pill">${i.auth}</span>
|
|
222
|
+
<button class="small" onclick="addFromCatalog('${i.key}')">connect</button>
|
|
223
|
+
</div>`).join("")}`).join("")
|
|
224
|
+
: `<p class="dim">no matches</p>`;
|
|
225
|
+
}
|
|
226
|
+
function connMsg(text,ok){
|
|
227
|
+
const m=$("#connMsg"); if(!m) return;
|
|
228
|
+
m.textContent=text; m.className="msg "+(ok?"ok":"err");
|
|
229
|
+
}
|
|
230
|
+
async function addFromCatalog(key){
|
|
231
|
+
const entry=connData.catalog.find(i=>i.key===key);
|
|
232
|
+
const env={};
|
|
233
|
+
for(const k of (entry?.env||[])){
|
|
234
|
+
const v=window.prompt(`${entry.name} needs ${k}:`);
|
|
235
|
+
if(v===null) return; // cancelled
|
|
236
|
+
if(v) env[k]=v;
|
|
237
|
+
}
|
|
238
|
+
const r=await api("/api/connections/add",{name:key,key,env});
|
|
239
|
+
if(r.error){ connMsg(`${key}: ${r.error}`,false); return; }
|
|
240
|
+
await connectServer(key);
|
|
241
|
+
}
|
|
242
|
+
async function importClaude(name){
|
|
243
|
+
const r=await api("/api/connections/add",{name,source:"claude-code"});
|
|
244
|
+
if(r.error){ connMsg(`${name}: ${r.error}`,false); return; }
|
|
245
|
+
connData=await api("/api/connections/list"); renderConnections();
|
|
246
|
+
}
|
|
247
|
+
async function connectServer(name){
|
|
248
|
+
const st=await api("/api/connections/connect",{name});
|
|
249
|
+
if(st.state==="needs_auth") connMsg(`${name}: open ${st.error} to authorise, then click connect again`,false);
|
|
250
|
+
else if(st.state==="connected") connMsg(`${name}: connected (${st.tools} tools)`,true);
|
|
251
|
+
else connMsg(`${name}: ${st.state}${st.error?" — "+st.error:""}`,false);
|
|
252
|
+
connData=await api("/api/connections/list"); renderConnections();
|
|
253
|
+
}
|
|
254
|
+
async function testServer(name){
|
|
255
|
+
const st=await api("/api/connections/test",{name});
|
|
256
|
+
connMsg(`${name}: ${st.state==="connected"?`ok (${st.tools} tools)`:(st.error||st.state)}`,st.state==="connected");
|
|
257
|
+
connData=await api("/api/connections/list"); renderConnections();
|
|
258
|
+
}
|
|
259
|
+
async function toggleServer(name,val){
|
|
260
|
+
await api("/api/connections/toggle",{name,enabled:val});
|
|
261
|
+
connData=await api("/api/connections/list"); renderConnections();
|
|
262
|
+
}
|
|
263
|
+
async function removeServer(name){
|
|
264
|
+
await api("/api/connections/remove",{name});
|
|
265
|
+
connData=await api("/api/connections/list"); renderConnections();
|
|
266
|
+
}
|
|
267
|
+
async function drive(){
|
|
268
|
+
const b=$("#db"); b.disabled=true; b.textContent="running…";
|
|
269
|
+
const r=await api("/api/agent",{prompt:"Run 'pwd' and 'date' with bash, then say in one short sentence where you are and what day it is."});
|
|
270
|
+
b.disabled=false; b.textContent="Run again"; ranTest=r.ok;
|
|
271
|
+
$("#dr").innerHTML=r.ok
|
|
272
|
+
?`<p style="margin-top:14px">${r.tools.map(t=>`<span class="pill">⏺ ${t}</span>`).join("")}</p>
|
|
273
|
+
<pre>${r.text.replace(/[<>]/g,"")}</pre><p class="ok" style="margin-top:10px">✓ working</p>`
|
|
274
|
+
:`<pre class="err">${(r.error||"failed").replace(/[<>]/g,"")}</pre>`;
|
|
275
|
+
}
|
|
276
|
+
async function save(){
|
|
277
|
+
document.querySelectorAll("select[data-role]").forEach(s=>cfg.roles[s.dataset.role].model=s.value);
|
|
278
|
+
await api("/api/save",cfg);
|
|
279
|
+
}
|
|
280
|
+
api("/api/config").then(c=>{cfg=c; render();});
|
|
281
|
+
</script>
|