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/setup_server.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import secrets
|
|
6
|
+
import shlex
|
|
7
|
+
import threading
|
|
8
|
+
import webbrowser
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from functools import partial
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import ParseResult, parse_qs, urlparse
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from . import config, events, integrations, mcp
|
|
19
|
+
|
|
20
|
+
Body = dict[str, Any]
|
|
21
|
+
|
|
22
|
+
UI = Path(__file__).parent / "setup.html"
|
|
23
|
+
TOKEN = secrets.token_urlsafe(16)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cfg() -> Body:
|
|
27
|
+
p = config.CONFIG_PATH
|
|
28
|
+
if p.exists():
|
|
29
|
+
return dict(json.loads(config._strip_jsonc(p.read_text())))
|
|
30
|
+
return config.DEFAULTS
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ALLOWED_TOP = {"providers", "models", "roles", "mcp"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate(data: Body) -> Body:
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
raise ValueError("config must be an object")
|
|
39
|
+
unknown = set(data) - ALLOWED_TOP
|
|
40
|
+
if unknown:
|
|
41
|
+
raise ValueError(f"unexpected keys: {sorted(unknown)}")
|
|
42
|
+
for name, prov in (data.get("providers") or {}).items():
|
|
43
|
+
if prov.get("type") == "anthropic":
|
|
44
|
+
continue # no baseUrl needed -- the SDK talks to api.anthropic.com directly
|
|
45
|
+
url = prov.get("baseUrl", "")
|
|
46
|
+
if not re.match(r"^https://[A-Za-z0-9.\-]+(:\d+)?(/[\w./\-]*)?$", url):
|
|
47
|
+
raise ValueError(f"provider {name!r}: baseUrl must be a plain https URL")
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _save(data: Body) -> Body:
|
|
52
|
+
_validate(data)
|
|
53
|
+
p = config.CONFIG_PATH
|
|
54
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
p.write_text(json.dumps(data, indent=2) + "\n")
|
|
56
|
+
p.chmod(0o600)
|
|
57
|
+
return {"ok": True, "path": str(p)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _provider(body: Body) -> tuple[str, str]:
|
|
61
|
+
cfg = _cfg()
|
|
62
|
+
name = body.get("provider") or next(iter(cfg["providers"]))
|
|
63
|
+
p = cfg["providers"][name]
|
|
64
|
+
key = body.get("apiKey") or p.get("apiKey") or ""
|
|
65
|
+
if not key and p.get("apiKeyEnv"):
|
|
66
|
+
import os
|
|
67
|
+
key = os.environ.get(p["apiKeyEnv"], "")
|
|
68
|
+
return (p.get("baseUrl") or "").rstrip("/"), key
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _provider_type(body: Body) -> str:
|
|
72
|
+
cfg = _cfg()
|
|
73
|
+
name = body.get("provider") or next(iter(cfg["providers"]))
|
|
74
|
+
return str(cfg["providers"][name].get("type", "openai"))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _anthropic_catalog() -> list[str]:
|
|
78
|
+
return sorted({m["model"] for m in config.DEFAULTS["models"].values()
|
|
79
|
+
if m["model"].startswith("claude")})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _probe_anthropic(model: str, key: str, prompt: str) -> Body:
|
|
83
|
+
"""The latency/reachability probe, routed through llm.stream instead of a
|
|
84
|
+
raw httpx POST -- an anthropic provider speaks the Messages API, not
|
|
85
|
+
OpenAI's /chat/completions."""
|
|
86
|
+
import time
|
|
87
|
+
|
|
88
|
+
from . import llm
|
|
89
|
+
from .config import Provider, Role
|
|
90
|
+
if not key:
|
|
91
|
+
return {"model": model, "ok": False, "error": "no API key set"}
|
|
92
|
+
role = Role(model=model, provider=Provider(name="setup-probe", type="anthropic", api_key_literal=key),
|
|
93
|
+
context=200000)
|
|
94
|
+
t0 = time.perf_counter()
|
|
95
|
+
try:
|
|
96
|
+
async for kind, _payload in llm.stream(role, [{"role": "user", "content": prompt}]):
|
|
97
|
+
if kind == "done":
|
|
98
|
+
break
|
|
99
|
+
return {"model": model, "ok": True, "ms": round((time.perf_counter() - t0) * 1000)}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {"model": model, "ok": False, "ms": round((time.perf_counter() - t0) * 1000),
|
|
102
|
+
"error": f"{type(e).__name__}: {e}"[:120]}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def api_models(body: Body) -> Body:
|
|
106
|
+
if _provider_type(body) == "anthropic":
|
|
107
|
+
return {"models": _anthropic_catalog()}
|
|
108
|
+
base, key = _provider(body)
|
|
109
|
+
if not key:
|
|
110
|
+
return {"error": "no API key set"}
|
|
111
|
+
r = httpx.get(f"{base}/models", headers={"Authorization": f"Bearer {key}"}, timeout=30)
|
|
112
|
+
if r.status_code != 200:
|
|
113
|
+
return {"error": f"HTTP {r.status_code}: {r.text[:200]}"}
|
|
114
|
+
return {"models": sorted(m["id"] for m in r.json().get("data", []))}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def api_test_model(body: Body) -> Body:
|
|
118
|
+
import time
|
|
119
|
+
model = body["model"]
|
|
120
|
+
if _provider_type(body) == "anthropic":
|
|
121
|
+
_base, key = _provider(body)
|
|
122
|
+
return asyncio.run(_probe_anthropic(model, key, "Reply with the single word: ok"))
|
|
123
|
+
base, key = _provider(body)
|
|
124
|
+
t0 = time.perf_counter()
|
|
125
|
+
try:
|
|
126
|
+
r = httpx.post(f"{base}/chat/completions",
|
|
127
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
128
|
+
json={"model": model, "max_tokens": 40, "messages":
|
|
129
|
+
[{"role": "user", "content": "Reply with the single word: ok"}],
|
|
130
|
+
"tools": [{"type": "function", "function": {
|
|
131
|
+
"name": "noop", "description": "no-op",
|
|
132
|
+
"parameters": {"type": "object", "properties": {}}}}]},
|
|
133
|
+
timeout=60)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return {"model": model, "ok": False, "error": f"{type(e).__name__}"}
|
|
136
|
+
dt = (time.perf_counter() - t0) * 1000
|
|
137
|
+
if r.status_code != 200:
|
|
138
|
+
return {"model": model, "ok": False, "ms": round(dt),
|
|
139
|
+
"error": f"HTTP {r.status_code}: {r.text[:120]}"}
|
|
140
|
+
return {"model": model, "ok": True, "ms": round(dt)}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def api_mcp_discover(body: Body) -> Body:
|
|
144
|
+
out = []
|
|
145
|
+
for name, cfg in mcp.discover().items():
|
|
146
|
+
out.append({"name": name, "stdio": "command" in cfg,
|
|
147
|
+
"detail": cfg.get("command", cfg.get("url", ""))})
|
|
148
|
+
return {"servers": sorted(out, key=lambda s: (not s["stdio"], s["name"]))}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def api_mcp_test(body: Body) -> Body:
|
|
152
|
+
name, spec = body["name"], body["config"]
|
|
153
|
+
async def go() -> Body:
|
|
154
|
+
s = mcp.Server(name, spec)
|
|
155
|
+
try:
|
|
156
|
+
await s.start(timeout=120)
|
|
157
|
+
return {"ok": True, "tools": [t.name for t in s.tools]}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {"ok": False, "error": str(e)[:200]}
|
|
160
|
+
finally:
|
|
161
|
+
await s.stop()
|
|
162
|
+
return asyncio.run(go())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _status_json(st: "mcp.ServerStatus") -> Body:
|
|
166
|
+
return {"name": st.name, "enabled": st.enabled, "state": st.state,
|
|
167
|
+
"tools": st.tools, "error": st.error, "lastUsed": st.last_used}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def api_connections_list(_b: Body) -> Body:
|
|
171
|
+
configured = mcp.status()
|
|
172
|
+
raw = config.mcp_config()
|
|
173
|
+
known = set(configured)
|
|
174
|
+
claude = {n: c for n, c in integrations.imported_from_claude_code().items() if n not in known}
|
|
175
|
+
|
|
176
|
+
def row(name: str, st: "mcp.ServerStatus") -> Body:
|
|
177
|
+
key = raw.get(name, {}).get("catalog")
|
|
178
|
+
cat = integrations.CATALOG.get(key) if key else None
|
|
179
|
+
out = _status_json(st)
|
|
180
|
+
out["auth"] = cat.auth if cat else None
|
|
181
|
+
out["category"] = cat.category if cat else None
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
configured_rows = [row(n, st) for n, st in sorted(configured.items())]
|
|
185
|
+
catalog_rows = [{"key": i.key, "name": i.name, "category": i.category, "blurb": i.blurb,
|
|
186
|
+
"auth": i.auth, "transport": i.transport, "verified": i.verified,
|
|
187
|
+
"env": list(i.env), "docs": i.docs}
|
|
188
|
+
for i in sorted(integrations.CATALOG.values(), key=lambda x: x.key)
|
|
189
|
+
if i.key not in known]
|
|
190
|
+
claude_rows = [{"name": n, **c} for n, c in sorted(claude.items())]
|
|
191
|
+
return {"configured": configured_rows, "catalog": catalog_rows, "claudeCode": claude_rows}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def api_connections_add(body: Body) -> Body:
|
|
195
|
+
name = body.get("name")
|
|
196
|
+
if not name:
|
|
197
|
+
return {"error": "name required"}
|
|
198
|
+
|
|
199
|
+
if body.get("source") == "claude-code":
|
|
200
|
+
# Never round-trip a real secret through the browser: read the
|
|
201
|
+
# unredacted spec server-side and write it straight into omega's config.
|
|
202
|
+
raw = mcp.discover(include_omega=False)
|
|
203
|
+
found = raw.get(name)
|
|
204
|
+
if found is None:
|
|
205
|
+
return {"error": f"{name!r} not found in Claude Code's config"}
|
|
206
|
+
mcp.add(name, dict(found))
|
|
207
|
+
return {"ok": True}
|
|
208
|
+
|
|
209
|
+
key = body.get("key")
|
|
210
|
+
catalog = integrations.CATALOG.get(key) if key else integrations.CATALOG.get(name)
|
|
211
|
+
spec: Body = {}
|
|
212
|
+
if catalog is not None:
|
|
213
|
+
spec["catalog"] = catalog.key
|
|
214
|
+
if catalog.transport == "remote" and catalog.url:
|
|
215
|
+
spec["url"] = catalog.url
|
|
216
|
+
elif catalog.command:
|
|
217
|
+
cmdline = [c.replace("<cwd>", os.getcwd()) for c in catalog.command]
|
|
218
|
+
spec["command"], spec["args"] = cmdline[0], cmdline[1:]
|
|
219
|
+
|
|
220
|
+
if body.get("url"):
|
|
221
|
+
spec["url"] = body["url"]
|
|
222
|
+
spec.pop("command", None)
|
|
223
|
+
spec.pop("args", None)
|
|
224
|
+
if body.get("command"):
|
|
225
|
+
parts = shlex.split(body["command"])
|
|
226
|
+
spec["command"], spec["args"] = parts[0], parts[1:]
|
|
227
|
+
spec.pop("url", None)
|
|
228
|
+
if body.get("env"):
|
|
229
|
+
spec["env"] = body["env"]
|
|
230
|
+
|
|
231
|
+
if not spec.get("command") and not spec.get("url"):
|
|
232
|
+
return {"error": "give a url, command, or known catalog key"}
|
|
233
|
+
mcp.add(name, spec)
|
|
234
|
+
return {"ok": True}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def api_connections_connect(body: Body) -> Body:
|
|
238
|
+
st = asyncio.run(mcp.connect(body["name"], timeout=body.get("timeout")))
|
|
239
|
+
return _status_json(st)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def api_connections_toggle(body: Body) -> Body:
|
|
243
|
+
try:
|
|
244
|
+
asyncio.run(mcp.enable(body["name"], bool(body.get("enabled"))))
|
|
245
|
+
except KeyError as e:
|
|
246
|
+
return {"error": str(e)}
|
|
247
|
+
return {"ok": True}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def api_connections_test(body: Body) -> Body:
|
|
251
|
+
async def go() -> "mcp.ServerStatus":
|
|
252
|
+
st = await mcp.connect(body["name"], timeout=body.get("timeout", 30))
|
|
253
|
+
await mcp.disconnect(body["name"])
|
|
254
|
+
return st
|
|
255
|
+
return _status_json(asyncio.run(go()))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def api_connections_remove(body: Body) -> Body:
|
|
259
|
+
asyncio.run(mcp.remove(body["name"]))
|
|
260
|
+
return {"ok": True}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
BENCH_CANDIDATES = ["glm-5.3-flash", "kimi-k3", "gemini-2.5-flash-lite",
|
|
264
|
+
"gemini-3.5-flash-lite", "claude-haiku-4-5", "gpt-4.1-mini",
|
|
265
|
+
"deepseek-v4-flash", "kimi-k3-fast"]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def api_benchmark(body: Body) -> Body:
|
|
269
|
+
"""Measure time-to-first-token and tool-calling for candidate models."""
|
|
270
|
+
import time
|
|
271
|
+
if _provider_type(body) == "anthropic":
|
|
272
|
+
_base, key = _provider(body)
|
|
273
|
+
models = body.get("models") or _anthropic_catalog()
|
|
274
|
+
|
|
275
|
+
async def go_anthropic() -> list[Body]:
|
|
276
|
+
return [await _probe_anthropic(m, key, "Say ok.") for m in models]
|
|
277
|
+
|
|
278
|
+
rows = asyncio.run(go_anthropic())
|
|
279
|
+
rows.sort(key=lambda r: (not r["ok"], r.get("ms", 9e9)))
|
|
280
|
+
return {"results": rows}
|
|
281
|
+
|
|
282
|
+
base, key = _provider(body)
|
|
283
|
+
models = body.get("models") or BENCH_CANDIDATES
|
|
284
|
+
catalog = api_models(body).get("models", [])
|
|
285
|
+
if catalog:
|
|
286
|
+
models = [m for m in models if m in catalog]
|
|
287
|
+
|
|
288
|
+
async def one(client: httpx.AsyncClient, model: str) -> Body:
|
|
289
|
+
t0 = time.perf_counter()
|
|
290
|
+
try:
|
|
291
|
+
r = await client.post(f"{base}/chat/completions",
|
|
292
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
293
|
+
json={"model": model, "max_tokens": 30, "messages":
|
|
294
|
+
[{"role": "user", "content": "Say ok."}],
|
|
295
|
+
"tools": [{"type": "function", "function": {"name": "noop",
|
|
296
|
+
"description": "no-op",
|
|
297
|
+
"parameters": {"type": "object", "properties": {}}}}]},
|
|
298
|
+
timeout=45)
|
|
299
|
+
ms = round((time.perf_counter() - t0) * 1000)
|
|
300
|
+
if r.status_code != 200:
|
|
301
|
+
return {"model": model, "ok": False, "error": f"HTTP {r.status_code}"}
|
|
302
|
+
return {"model": model, "ok": True, "ms": ms}
|
|
303
|
+
except Exception as e:
|
|
304
|
+
return {"model": model, "ok": False, "error": type(e).__name__}
|
|
305
|
+
|
|
306
|
+
async def go() -> list[Body]:
|
|
307
|
+
# Sequential on purpose: concurrent requests contend and scramble the
|
|
308
|
+
# ranking, which is the one thing this measurement exists to produce.
|
|
309
|
+
async with httpx.AsyncClient() as c:
|
|
310
|
+
return [await one(c, m) for m in models]
|
|
311
|
+
|
|
312
|
+
rows = asyncio.run(go())
|
|
313
|
+
rows.sort(key=lambda r: (not r["ok"], r.get("ms", 9e9)))
|
|
314
|
+
return {"results": rows}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
PROBE_PROMPT = ("Run 'pwd' and 'date' with bash, then say in one short "
|
|
318
|
+
"sentence where you are and what day it is.")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def api_agent(body: Body) -> Body:
|
|
322
|
+
"""Run one real omega turn so onboarding ends on proof it works."""
|
|
323
|
+
from . import loop
|
|
324
|
+
calls: list[str] = []
|
|
325
|
+
def emit(ev: events.Event) -> None:
|
|
326
|
+
if isinstance(ev, events.ToolStart):
|
|
327
|
+
calls.append(ev.name)
|
|
328
|
+
async def go() -> str:
|
|
329
|
+
cfg = config.load()
|
|
330
|
+
history: list[Body] = [{"role": "user", "content": PROBE_PROMPT}]
|
|
331
|
+
text = await loop.run_agent(cfg, "main",
|
|
332
|
+
loop.BUILD_SYSTEM, history,
|
|
333
|
+
emit=emit)
|
|
334
|
+
return text
|
|
335
|
+
try:
|
|
336
|
+
return {"ok": True, "text": asyncio.run(go()), "tools": calls}
|
|
337
|
+
except Exception as e:
|
|
338
|
+
return {"ok": False, "error": f"{type(e).__name__}: {e}"[:250]}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _redacted_cfg(_b: Body | None = None) -> Body:
|
|
342
|
+
import copy
|
|
343
|
+
c = copy.deepcopy(_cfg())
|
|
344
|
+
for prov in (c.get("providers") or {}).values():
|
|
345
|
+
if prov.get("apiKey"):
|
|
346
|
+
prov["apiKey"] = ""
|
|
347
|
+
prov["hasKey"] = True
|
|
348
|
+
prov["keyMask"] = "••••••••"
|
|
349
|
+
# A role may be written as a catalog alias ({"alias": "opus"}); the wizard
|
|
350
|
+
# only understands the inline {model, provider, context} shape, so resolve
|
|
351
|
+
# every role to that shape for the page regardless of how it's stored.
|
|
352
|
+
try:
|
|
353
|
+
resolved = config.load()
|
|
354
|
+
c["roles"] = {name: {"model": r.model, "provider": r.provider.name,
|
|
355
|
+
"context": r.context, "alias": r.alias}
|
|
356
|
+
for name, r in resolved.roles.items()}
|
|
357
|
+
except SystemExit:
|
|
358
|
+
pass
|
|
359
|
+
return c
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
ROUTES: dict[str, Callable[[Body], Body]] = {
|
|
363
|
+
"/api/config": _redacted_cfg,
|
|
364
|
+
"/api/save": _save,
|
|
365
|
+
"/api/models": api_models,
|
|
366
|
+
"/api/test-model": api_test_model,
|
|
367
|
+
"/api/mcp/discover": api_mcp_discover,
|
|
368
|
+
"/api/mcp/test": api_mcp_test,
|
|
369
|
+
"/api/connections/list": api_connections_list,
|
|
370
|
+
"/api/connections/add": api_connections_add,
|
|
371
|
+
"/api/connections/connect": api_connections_connect,
|
|
372
|
+
"/api/connections/toggle": api_connections_toggle,
|
|
373
|
+
"/api/connections/test": api_connections_test,
|
|
374
|
+
"/api/connections/remove": api_connections_remove,
|
|
375
|
+
"/api/benchmark": api_benchmark,
|
|
376
|
+
"/api/agent": api_agent,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class Handler(BaseHTTPRequestHandler):
|
|
381
|
+
def log_message(self, *a: Any) -> None:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
def _authed(self, url: ParseResult) -> bool:
|
|
385
|
+
supplied = (self.headers.get("X-Omega-Token")
|
|
386
|
+
or parse_qs(url.query).get("t", [""])[0])
|
|
387
|
+
return secrets.compare_digest(supplied, TOKEN)
|
|
388
|
+
|
|
389
|
+
def _same_origin(self) -> bool:
|
|
390
|
+
"""Blocks DNS rebinding and cross-origin CSRF. Without this, a page you
|
|
391
|
+
visit can drive these endpoints even without reading the response."""
|
|
392
|
+
host = (self.headers.get("Host") or "").split(",")[0].strip()
|
|
393
|
+
if host.rsplit(":", 1)[0] not in ("127.0.0.1", "localhost", "[::1]"):
|
|
394
|
+
return False
|
|
395
|
+
origin = self.headers.get("Origin")
|
|
396
|
+
if origin and origin not in (f"http://{host}",):
|
|
397
|
+
return False
|
|
398
|
+
if (self.headers.get("Sec-Fetch-Site") or "same-origin") not in (
|
|
399
|
+
"same-origin", "none"):
|
|
400
|
+
return False
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
def _send(self, code: int, body: bytes | Body, ctype: str = "application/json") -> None:
|
|
404
|
+
raw = body if isinstance(body, bytes) else json.dumps(body).encode()
|
|
405
|
+
self.send_response(code)
|
|
406
|
+
self.send_header("Content-Type", ctype)
|
|
407
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
408
|
+
self.end_headers()
|
|
409
|
+
self.wfile.write(raw)
|
|
410
|
+
|
|
411
|
+
def do_GET(self) -> None:
|
|
412
|
+
url = urlparse(self.path)
|
|
413
|
+
if not self._same_origin():
|
|
414
|
+
return self._send(403, {"error": "bad origin"})
|
|
415
|
+
if not self._authed(url):
|
|
416
|
+
return self._send(403, {"error": "bad token"})
|
|
417
|
+
if url.path == "/":
|
|
418
|
+
return self._send(200, UI.read_bytes(), "text/html; charset=utf-8")
|
|
419
|
+
# API is POST-only: GET dispatch let <img src="/api/agent?t=..."> run
|
|
420
|
+
# a full agent turn with bash from any page you happened to visit.
|
|
421
|
+
if url.path in ROUTES:
|
|
422
|
+
return self._send(405, {"error": "use POST"})
|
|
423
|
+
self._send(404, {"error": "not found"})
|
|
424
|
+
|
|
425
|
+
def do_POST(self) -> None:
|
|
426
|
+
url = urlparse(self.path)
|
|
427
|
+
if not self._same_origin():
|
|
428
|
+
return self._send(403, {"error": "bad origin"})
|
|
429
|
+
if not self._authed(url):
|
|
430
|
+
return self._send(403, {"error": "bad token"})
|
|
431
|
+
n = int(self.headers.get("Content-Length", 0))
|
|
432
|
+
body = json.loads(self.rfile.read(n) or b"{}")
|
|
433
|
+
fn = ROUTES.get(url.path)
|
|
434
|
+
if not fn:
|
|
435
|
+
return self._send(404, {"error": "not found"})
|
|
436
|
+
try:
|
|
437
|
+
self._send(200, fn(body))
|
|
438
|
+
except Exception as e:
|
|
439
|
+
self._send(200, {"error": f"{type(e).__name__}: {e}"[:300]})
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def serve(port: int = 0, open_browser: bool = True) -> None:
|
|
443
|
+
httpd = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
444
|
+
port = httpd.server_address[1]
|
|
445
|
+
url = f"http://127.0.0.1:{port}/?t={TOKEN}"
|
|
446
|
+
print(f"omega setup → {url}\nctrl-c to stop")
|
|
447
|
+
if open_browser:
|
|
448
|
+
threading.Timer(0.4, partial(webbrowser.open, url)).start()
|
|
449
|
+
try:
|
|
450
|
+
httpd.serve_forever()
|
|
451
|
+
except KeyboardInterrupt:
|
|
452
|
+
print()
|
omega/skills.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Skill discovery and the `skill` tool.
|
|
2
|
+
|
|
3
|
+
A skill is a markdown checklist (YAML frontmatter + body) the model loads on
|
|
4
|
+
demand and follows in the SAME conversation -- not a subagent. Spawning a
|
|
5
|
+
subagent would hide the steps from the transcript and lose the model's
|
|
6
|
+
accumulated context on this task; a skill is meant to steer the very loop
|
|
7
|
+
that's already running, the way a human would open a runbook mid-task.
|
|
8
|
+
|
|
9
|
+
Discovery mirrors Claude Code's `SKILL.md` format so a user's existing
|
|
10
|
+
``~/.claude/skills/*`` work here unchanged, plus two omega-specific
|
|
11
|
+
locations:
|
|
12
|
+
|
|
13
|
+
- ``.omega/skills/*/SKILL.md`` -- project, highest precedence
|
|
14
|
+
- ``~/.omega/skills/*/SKILL.md`` -- global
|
|
15
|
+
- ``~/.claude/skills/*/SKILL.md`` -- Claude Code's own skills, lowest precedence
|
|
16
|
+
|
|
17
|
+
Plugin subdirectories (Claude Code's ``plugin:skill`` namespacing) are not
|
|
18
|
+
walked -- only the top-level ``*/SKILL.md`` layout.
|
|
19
|
+
"""
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from . import tools
|
|
28
|
+
|
|
29
|
+
INDEX_MAX = 40
|
|
30
|
+
DESC_MAX = 90
|
|
31
|
+
BODY_MAX = 24_000
|
|
32
|
+
SKILL_TOOL = "skill"
|
|
33
|
+
|
|
34
|
+
_PROJECT_SUBDIR = ".omega/skills"
|
|
35
|
+
_GLOBAL_DIR = Path.home() / ".omega" / "skills"
|
|
36
|
+
_CLAUDE_DIR = Path.home() / ".claude" / "skills"
|
|
37
|
+
|
|
38
|
+
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?\n)---\s*\n?", re.DOTALL)
|
|
39
|
+
_MD_LINK_RE = re.compile(r"(\]\()([^)\s]+)(\))")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Skill:
|
|
44
|
+
name: str
|
|
45
|
+
description: str
|
|
46
|
+
path: Path
|
|
47
|
+
source: str # "project" | "global" | "claude"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse(path: Path, source: str) -> Skill | None:
|
|
51
|
+
try:
|
|
52
|
+
text = path.read_text(errors="replace")
|
|
53
|
+
except OSError:
|
|
54
|
+
return None
|
|
55
|
+
name, description = path.parent.name, ""
|
|
56
|
+
m = _FRONTMATTER_RE.match(text)
|
|
57
|
+
if m:
|
|
58
|
+
try:
|
|
59
|
+
meta = yaml.safe_load(m.group(1))
|
|
60
|
+
except yaml.YAMLError:
|
|
61
|
+
meta = None
|
|
62
|
+
if isinstance(meta, dict):
|
|
63
|
+
name = str(meta.get("name") or name)
|
|
64
|
+
description = str(meta.get("description") or "")
|
|
65
|
+
return Skill(name=name, description=description, path=path, source=source)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _discover(root: Path, source: str) -> dict[str, Skill]:
|
|
69
|
+
if not root.is_dir():
|
|
70
|
+
return {}
|
|
71
|
+
found: dict[str, Skill] = {}
|
|
72
|
+
for skill_md in sorted(root.glob("*/SKILL.md")):
|
|
73
|
+
parsed = _parse(skill_md, source)
|
|
74
|
+
if parsed is not None:
|
|
75
|
+
found[parsed.name] = parsed
|
|
76
|
+
return found
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def catalog(cwd: str | Path | None = None) -> list[Skill]:
|
|
80
|
+
"""Project overrides global overrides claude, keyed by skill name."""
|
|
81
|
+
cwd_path = Path(cwd) if cwd else Path(os.getcwd())
|
|
82
|
+
merged: dict[str, Skill] = {}
|
|
83
|
+
merged.update(_discover(_CLAUDE_DIR, "claude"))
|
|
84
|
+
merged.update(_discover(_GLOBAL_DIR, "global"))
|
|
85
|
+
merged.update(_discover(cwd_path / _PROJECT_SUBDIR, "project"))
|
|
86
|
+
return sorted(merged.values(), key=lambda s: s.name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _truncate_desc(description: str, limit: int = DESC_MAX) -> str:
|
|
90
|
+
description = " ".join(description.split())
|
|
91
|
+
if len(description) <= limit:
|
|
92
|
+
return description
|
|
93
|
+
return description[: limit - 1].rstrip() + "…"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render_index(cwd: str | Path | None = None) -> str:
|
|
97
|
+
entries = catalog(cwd)[:INDEX_MAX]
|
|
98
|
+
if not entries:
|
|
99
|
+
return ""
|
|
100
|
+
lines = [f"{s.name} — {_truncate_desc(s.description)}" for s in entries]
|
|
101
|
+
return ("## Skills\n" + "\n".join(lines) +
|
|
102
|
+
"\n\nCall `skill(name)` to load one; follow it as a checklist "
|
|
103
|
+
"in this same conversation.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def system_block(cwd: str | Path | None = None) -> str:
|
|
107
|
+
return render_index(cwd)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def find(name: str, cwd: str | Path | None = None) -> Skill | None:
|
|
111
|
+
for s in catalog(cwd):
|
|
112
|
+
if s.name == name:
|
|
113
|
+
return s
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _rewrite_refs(body: str, skill_dir: Path) -> str:
|
|
118
|
+
"""A relative link only resolves from the skill's own directory; the
|
|
119
|
+
`read` tool has no notion of that, so rewrite it to an absolute path
|
|
120
|
+
before the model ever sees it."""
|
|
121
|
+
def repl(m: re.Match[str]) -> str:
|
|
122
|
+
prefix, ref, suffix = m.group(1), m.group(2), m.group(3)
|
|
123
|
+
if ref.startswith(("/", "http://", "https://", "#", "mailto:")):
|
|
124
|
+
return m.group(0)
|
|
125
|
+
resolved = (skill_dir / ref).resolve()
|
|
126
|
+
return f"{prefix}{resolved}{suffix}" if resolved.exists() else m.group(0)
|
|
127
|
+
|
|
128
|
+
return _MD_LINK_RE.sub(repl, body)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_body(name: str, cwd: str | Path | None = None) -> str | None:
|
|
132
|
+
found = find(name, cwd)
|
|
133
|
+
if found is None:
|
|
134
|
+
return None
|
|
135
|
+
text = found.path.read_text(errors="replace")
|
|
136
|
+
body = _FRONTMATTER_RE.sub("", text, count=1).strip()
|
|
137
|
+
body = _rewrite_refs(body, found.path.parent)
|
|
138
|
+
if len(body) > BODY_MAX:
|
|
139
|
+
dropped = len(body) - BODY_MAX
|
|
140
|
+
body = f"{body[:BODY_MAX]}\n...[truncated {dropped} chars]"
|
|
141
|
+
return f'<skill name="{found.name}">\n{body}\n</skill>'
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
S = {"type": "string"}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@tools.tool(
|
|
148
|
+
SKILL_TOOL,
|
|
149
|
+
"Load a skill by name -- a pre-written checklist for a sub-workflow "
|
|
150
|
+
"(from the catalog in the system prompt). Returns its instructions to "
|
|
151
|
+
"follow in this same conversation; it does not spawn another agent.",
|
|
152
|
+
{"name": S}, ["name"])
|
|
153
|
+
def _skill(name: str) -> str:
|
|
154
|
+
body = load_body(name)
|
|
155
|
+
if body is not None:
|
|
156
|
+
return body
|
|
157
|
+
available = ", ".join(s.name for s in catalog()) or "(none found)"
|
|
158
|
+
return f"error: no skill named {name!r}. Available: {available}"
|