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/__init__.py
ADDED
|
File without changes
|
omega/__main__.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import config, mcp, migrate, session, skills, subagent, tools, trace
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .memory import consolidate
|
|
9
|
+
from .ui import plain
|
|
10
|
+
|
|
11
|
+
console = plain.console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main() -> None:
|
|
15
|
+
argv = sys.argv[1:]
|
|
16
|
+
# `--help`/`-h`/`help` used to fall through every check below and run a
|
|
17
|
+
# real agent turn with the literal prompt "--help" -- handle them (and
|
|
18
|
+
# `--version`) before any other dispatch, config.load() included, so a
|
|
19
|
+
# broken or missing config never blocks either.
|
|
20
|
+
if argv and argv[0] in ("-h", "--help", "help"):
|
|
21
|
+
return console.print(_usage_text(), markup=False, highlight=False)
|
|
22
|
+
if argv and argv[0] == "--version":
|
|
23
|
+
return console.print(f"omega {_version()}")
|
|
24
|
+
# Dispatch subcommands BEFORE config.load(): it exits when no API key is
|
|
25
|
+
# set, which made `omega setup` -- the flow that sets the key -- unreachable.
|
|
26
|
+
if argv and argv[0] == "setup":
|
|
27
|
+
from .setup_server import serve
|
|
28
|
+
return serve()
|
|
29
|
+
if argv and argv[0] == "keys":
|
|
30
|
+
from . import keys
|
|
31
|
+
raise SystemExit(keys.main(argv[1:]))
|
|
32
|
+
if argv and argv[0] == "onboard":
|
|
33
|
+
from . import onboarding
|
|
34
|
+
wrote = await onboarding.run()
|
|
35
|
+
if not wrote:
|
|
36
|
+
return console.print("[dim]onboarding cancelled -- no config written.[/dim]")
|
|
37
|
+
return
|
|
38
|
+
if argv and argv[0] == "sessions":
|
|
39
|
+
# `sessions` lists and exits; silently swallowing further flags made
|
|
40
|
+
# `omega sessions --resume X` look like it had done something.
|
|
41
|
+
extra = [a for a in argv[1:] if a != "--"]
|
|
42
|
+
if extra:
|
|
43
|
+
console.print(f"[yellow]note:[/yellow] `sessions` only lists — "
|
|
44
|
+
f"ignoring {' '.join(extra)}")
|
|
45
|
+
if "--resume" in extra:
|
|
46
|
+
i = extra.index("--resume")
|
|
47
|
+
sid = extra[i + 1] if i + 1 < len(extra) else "<id>"
|
|
48
|
+
console.print(f" to open it: [bold]omega --resume {sid}[/bold]")
|
|
49
|
+
return console.print(session.render_list())
|
|
50
|
+
if argv and argv[0] == "memory":
|
|
51
|
+
# gc needs config.load(), unlike setup/sessions above -- keep it local
|
|
52
|
+
# to this branch instead of moving it above the flag-parsing section.
|
|
53
|
+
cfg = config.load()
|
|
54
|
+
if len(argv) > 1 and argv[1] == "gc":
|
|
55
|
+
console.print(await consolidate.run(cfg, "project", force=True))
|
|
56
|
+
console.print(await consolidate.run(cfg, "global", force=True))
|
|
57
|
+
else:
|
|
58
|
+
console.print("usage: omega memory gc")
|
|
59
|
+
return
|
|
60
|
+
if argv and argv[0] == "models":
|
|
61
|
+
return console.print(_render_models_table(config.load()))
|
|
62
|
+
if argv and argv[0] == "skills":
|
|
63
|
+
return _render_skills(argv[1:])
|
|
64
|
+
if argv and argv[0] == "connections":
|
|
65
|
+
return await _connections(argv[1:])
|
|
66
|
+
if argv and argv[0] == "eval":
|
|
67
|
+
from .eval import cli as eval_cli
|
|
68
|
+
return await eval_cli.main(argv[1:])
|
|
69
|
+
if argv and argv[0] == "trace":
|
|
70
|
+
return _trace(argv[1:])
|
|
71
|
+
if argv and argv[0] == "update":
|
|
72
|
+
return await _update()
|
|
73
|
+
if argv and argv[0] == "doctor":
|
|
74
|
+
return _render_doctor()
|
|
75
|
+
|
|
76
|
+
# `resume`/`continue` are aliases for `--resume <id>`/`--continue`: rewrite
|
|
77
|
+
# argv into the flag form and fall through to the normal parsing below so
|
|
78
|
+
# every downstream behaviour (mode, model, MCP) stays in one place. Both
|
|
79
|
+
# branches either `return` or produce a recognized `--resume`/`--continue`
|
|
80
|
+
# flag -- the token can never fall through and become literal prompt text.
|
|
81
|
+
if argv and argv[0] == "resume":
|
|
82
|
+
rewritten = await _resume_command(argv[1:])
|
|
83
|
+
if rewritten is None:
|
|
84
|
+
return
|
|
85
|
+
argv = rewritten
|
|
86
|
+
if argv and argv[0] == "continue":
|
|
87
|
+
argv = ["--continue", *argv[1:]]
|
|
88
|
+
|
|
89
|
+
# Parse every flag up front so order never matters.
|
|
90
|
+
flags = {a for a in argv if a.startswith("-")}
|
|
91
|
+
use_mcp, yolo = "--mcp" in flags, "--yolo" in flags
|
|
92
|
+
want_plan = bool(flags & {"--plan", "-p"})
|
|
93
|
+
want_continue = bool(flags & {"--continue", "-c"})
|
|
94
|
+
resume_id = None
|
|
95
|
+
if "--resume" in argv:
|
|
96
|
+
i = argv.index("--resume")
|
|
97
|
+
if i + 1 >= len(argv):
|
|
98
|
+
return console.print("[red]--resume needs a session id[/red] "
|
|
99
|
+
"(see `omega sessions`)")
|
|
100
|
+
resume_id = argv[i + 1]
|
|
101
|
+
argv = argv[:i] + argv[i + 2:]
|
|
102
|
+
model_arg = None
|
|
103
|
+
if "--model" in argv:
|
|
104
|
+
i = argv.index("--model")
|
|
105
|
+
if i + 1 >= len(argv):
|
|
106
|
+
return console.print("[red]--model needs an alias or model id[/red] "
|
|
107
|
+
"(see `omega models`)")
|
|
108
|
+
model_arg = argv[i + 1]
|
|
109
|
+
argv = argv[:i] + argv[i + 2:]
|
|
110
|
+
argv = [a for a in argv
|
|
111
|
+
if a not in ("--mcp", "--yolo", "--plan", "-p", "--continue", "-c")]
|
|
112
|
+
# Anything else that still looks like a flag at the front is a typo, not a
|
|
113
|
+
# prompt -- silently absorbing it into `" ".join(argv)` is how `--help`
|
|
114
|
+
# ended up being sent to the model as literal text.
|
|
115
|
+
if argv and argv[0].startswith("-"):
|
|
116
|
+
console.print(f"[red]omega: unknown flag {argv[0]!r}[/red]")
|
|
117
|
+
return console.print(_usage_text(), markup=False, highlight=False)
|
|
118
|
+
|
|
119
|
+
cfg = config.load()
|
|
120
|
+
# First run (no config file) or an unusable `main` role: config.load()
|
|
121
|
+
# itself no longer exits for a missing key (that check is lazy), so this
|
|
122
|
+
# is the one place that must decide between a short interactive setup and
|
|
123
|
+
# the old hard-exit -- never leave it to whatever call happens to touch
|
|
124
|
+
# the key deep inside a turn.
|
|
125
|
+
if not config.CONFIG_PATH.exists() or not cfg.role("main").provider.has_key:
|
|
126
|
+
if yolo or not sys.stdin.isatty():
|
|
127
|
+
_ = cfg.role("main").provider.api_key # raises the helpful SystemExit
|
|
128
|
+
else:
|
|
129
|
+
from . import onboarding
|
|
130
|
+
wrote = await onboarding.run()
|
|
131
|
+
if not wrote:
|
|
132
|
+
_ = cfg.role("main").provider.api_key # raises the helpful SystemExit
|
|
133
|
+
cfg = config.load()
|
|
134
|
+
subagent.CFG = cfg
|
|
135
|
+
# --model overrides both `main` and `plan` for this session; resolved
|
|
136
|
+
# against the catalog now so a typo is reported before any turn runs.
|
|
137
|
+
model_alias = cfg.resolve_alias(model_arg) if model_arg else None
|
|
138
|
+
if not yolo and sys.stdin.isatty():
|
|
139
|
+
tools.CONFIRM = plain.confirm
|
|
140
|
+
tools.ASK_USER = plain.ask_user
|
|
141
|
+
|
|
142
|
+
sess = None
|
|
143
|
+
if want_continue:
|
|
144
|
+
sess = session.latest()
|
|
145
|
+
if sess is None:
|
|
146
|
+
return console.print("[dim]no session for this directory[/dim]")
|
|
147
|
+
elif resume_id:
|
|
148
|
+
sess = session.load(resume_id)
|
|
149
|
+
# Deferred: the TUI shows this in-transcript instead of on the console
|
|
150
|
+
# (printed before app.run_async() would just scroll away under the TUI).
|
|
151
|
+
resumed_note = None
|
|
152
|
+
if sess:
|
|
153
|
+
resumed_note = (f"resumed {sess.id} — {sess.turns} turns, "
|
|
154
|
+
f"{len(sess.history)} messages · {sess.cwd}")
|
|
155
|
+
|
|
156
|
+
# An explicit --plan must win over the stored mode: silently ignoring a
|
|
157
|
+
# read-only flag is a safety bug, not a papercut.
|
|
158
|
+
mode = "plan" if want_plan else (sess.mode if sess else "build")
|
|
159
|
+
|
|
160
|
+
if use_mcp:
|
|
161
|
+
with console.status("[dim]connecting MCP servers…[/dim]"):
|
|
162
|
+
for name, status in (await mcp.load(only=set(config.mcp_names()))).items():
|
|
163
|
+
console.print(f"[dim] {name}: {status}[/dim]")
|
|
164
|
+
|
|
165
|
+
if sess is None:
|
|
166
|
+
sess = session.Session.new(mode=mode)
|
|
167
|
+
if model_alias:
|
|
168
|
+
sess.model_override = model_alias
|
|
169
|
+
tools.SESSION_ID = sess.id
|
|
170
|
+
history = sess.history
|
|
171
|
+
if history:
|
|
172
|
+
# Without this the model has no signal it is mid-conversation and can
|
|
173
|
+
# mistake a resumed session for a cold start.
|
|
174
|
+
history.append({"role": "user", "content":
|
|
175
|
+
f"{session.RESUME_PREFIX} — the {len(history)} messages "
|
|
176
|
+
f"above are our earlier conversation and are available to you]"})
|
|
177
|
+
|
|
178
|
+
prompt = " ".join(argv).strip()
|
|
179
|
+
# A one-shot prompt always uses ui/plain.py, even from a real terminal;
|
|
180
|
+
# the TUI only replaces the bare interactive REPL.
|
|
181
|
+
if not prompt and sys.stdin.isatty() and sys.stdout.isatty():
|
|
182
|
+
from .ui.tui import OmegaApp
|
|
183
|
+
app = OmegaApp(cfg, sess, mode, history, model_alias=sess.model_override)
|
|
184
|
+
if not yolo:
|
|
185
|
+
tools.CONFIRM = app.confirm
|
|
186
|
+
tools.ASK_USER = app.ask_user
|
|
187
|
+
await app.run_async()
|
|
188
|
+
await _consolidate_on_close(cfg)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if resumed_note:
|
|
192
|
+
console.print(f"[dim]{resumed_note}[/dim]")
|
|
193
|
+
|
|
194
|
+
# No prompt on argv and stdin isn't a terminal: read it from there, so
|
|
195
|
+
# `echo "fix the tests" | omega` works instead of only piping into a REPL
|
|
196
|
+
# that no longer exists for this invocation.
|
|
197
|
+
if not prompt and not sys.stdin.isatty():
|
|
198
|
+
prompt = sys.stdin.read().strip()
|
|
199
|
+
|
|
200
|
+
if prompt:
|
|
201
|
+
await plain.run_prompt(cfg, history, prompt, mode, sess, model=sess.model_override)
|
|
202
|
+
await _consolidate_on_close(cfg)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
console.print("[dim]omega: no prompt given and not an interactive terminal[/dim]")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def _resume_command(rest: list[str]) -> list[str] | None:
|
|
209
|
+
"""`omega resume [id]`. With an id (or prefix), rewrite to `--resume
|
|
210
|
+
<id> ...` and let the caller fall through to the normal flow. With none,
|
|
211
|
+
list this directory's sessions and, on a TTY, offer a numbered pick --
|
|
212
|
+
returns None once this call has fully handled the request (nothing left
|
|
213
|
+
to resume, or the user cancelled)."""
|
|
214
|
+
if rest and not rest[0].startswith("-"):
|
|
215
|
+
return ["--resume", rest[0], *rest[1:]]
|
|
216
|
+
|
|
217
|
+
cwd = os.getcwd()
|
|
218
|
+
rows = [s for s in session.all_sessions() if s.cwd == cwd][:20]
|
|
219
|
+
if not rows:
|
|
220
|
+
console.print("[dim]no sessions for this directory[/dim]")
|
|
221
|
+
return None
|
|
222
|
+
console.print(session.render_list(cwd=cwd))
|
|
223
|
+
if not sys.stdin.isatty():
|
|
224
|
+
return None
|
|
225
|
+
try:
|
|
226
|
+
answer = (await asyncio.to_thread(
|
|
227
|
+
input, "resume which? (number, blank to cancel): ")).strip()
|
|
228
|
+
except (EOFError, KeyboardInterrupt):
|
|
229
|
+
return None
|
|
230
|
+
if not answer.isdigit():
|
|
231
|
+
return None
|
|
232
|
+
idx = int(answer) - 1
|
|
233
|
+
if not (0 <= idx < len(rows)):
|
|
234
|
+
return None
|
|
235
|
+
return ["--resume", rows[idx].id, *rest]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _trace(argv: list[str]) -> None:
|
|
239
|
+
if not argv:
|
|
240
|
+
return console.print("[red]usage: omega trace <session-id> [--tools] [--json][/red]")
|
|
241
|
+
sess = session.load(argv[0])
|
|
242
|
+
text = trace.render_timeline(sess.id, tools_only="--tools" in argv[1:],
|
|
243
|
+
raw_json="--json" in argv[1:])
|
|
244
|
+
console.print(text, markup="--json" not in argv[1:], highlight=False)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _run(cmd: list[str], *, merge_stderr: bool = False) -> str:
|
|
248
|
+
proc = await asyncio.create_subprocess_exec(
|
|
249
|
+
*cmd, stdout=asyncio.subprocess.PIPE,
|
|
250
|
+
stderr=asyncio.subprocess.STDOUT if merge_stderr else asyncio.subprocess.PIPE)
|
|
251
|
+
out, _ = await proc.communicate()
|
|
252
|
+
return out.decode(errors="replace")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _installed_from_pypi(tool_list: str) -> bool:
|
|
256
|
+
"""`uv tool list --show-version-specifiers` prints e.g.
|
|
257
|
+
`omega-code v0.3.0 [required: git+https://github.com/...]` for a git
|
|
258
|
+
install, and no `[required: ...]` annotation for a PyPI one. Defaults to
|
|
259
|
+
the PyPI path when `omega-code` isn't a `uv tool` at all."""
|
|
260
|
+
for line in tool_list.splitlines():
|
|
261
|
+
if line.startswith("omega-code"):
|
|
262
|
+
return "git+" not in line
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def _update() -> None:
|
|
267
|
+
listing = await _run(["uv", "tool", "list", "--show-version-specifiers"])
|
|
268
|
+
target = "omega-code" if _installed_from_pypi(listing) \
|
|
269
|
+
else "git+https://github.com/Timothy102/omega.git@main"
|
|
270
|
+
console.print(f"[dim]uv tool install --force {target}[/dim]")
|
|
271
|
+
install_out = await _run(["uv", "tool", "install", "--force", target], merge_stderr=True)
|
|
272
|
+
console.print(install_out, markup=False, highlight=False)
|
|
273
|
+
try:
|
|
274
|
+
new_version = await _run(["omega", "--version"])
|
|
275
|
+
except FileNotFoundError:
|
|
276
|
+
new_version = f"omega {_version()}"
|
|
277
|
+
console.print(new_version.strip() or f"omega {_version()}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _doctor_checks() -> list[tuple[str, bool, str]]:
|
|
281
|
+
"""(label, ok, detail) rows for `omega doctor`."""
|
|
282
|
+
import shutil
|
|
283
|
+
import stat
|
|
284
|
+
|
|
285
|
+
rows: list[tuple[str, bool, str]] = []
|
|
286
|
+
py_ok = sys.version_info >= (3, 11)
|
|
287
|
+
rows.append(("python >= 3.11", py_ok, f"{sys.version_info.major}.{sys.version_info.minor}"))
|
|
288
|
+
|
|
289
|
+
for tool in ("rg", "git", "node", "npx", "uv"):
|
|
290
|
+
path = shutil.which(tool)
|
|
291
|
+
rows.append((tool, path is not None, path or "not found"))
|
|
292
|
+
|
|
293
|
+
cfg_ok, cfg_detail = True, "no config file yet"
|
|
294
|
+
cfg: Config | None = None
|
|
295
|
+
if config.CONFIG_PATH.exists():
|
|
296
|
+
try:
|
|
297
|
+
cfg = config.load()
|
|
298
|
+
cfg_detail = f"{len(cfg.models)} models, {len(cfg.providers)} providers"
|
|
299
|
+
except Exception as e:
|
|
300
|
+
cfg_ok, cfg_detail = False, f"{type(e).__name__}: {e}"
|
|
301
|
+
rows.append(("config valid", cfg_ok, cfg_detail))
|
|
302
|
+
|
|
303
|
+
if cfg is not None:
|
|
304
|
+
for name, provider in sorted(cfg.providers.items()):
|
|
305
|
+
rows.append((f"provider key: {name}", provider.has_key,
|
|
306
|
+
provider.key_source if provider.has_key else "missing"))
|
|
307
|
+
plaintext = [n for n, p in sorted(cfg.providers.items()) if p.api_key_literal]
|
|
308
|
+
rows.append(("keys out of config.json", not plaintext,
|
|
309
|
+
"all by reference" if not plaintext
|
|
310
|
+
else f"{', '.join(plaintext)} stored in plaintext "
|
|
311
|
+
f"-- run `omega keys migrate`"))
|
|
312
|
+
|
|
313
|
+
if config.CONFIG_PATH.exists():
|
|
314
|
+
mode = stat.S_IMODE(config.CONFIG_PATH.stat().st_mode)
|
|
315
|
+
rows.append(("config permissions (0600)", mode == 0o600, oct(mode)))
|
|
316
|
+
|
|
317
|
+
return rows
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _render_doctor() -> None:
|
|
321
|
+
from rich.table import Table
|
|
322
|
+
table = Table(box=None)
|
|
323
|
+
for col in ("CHECK", "", "DETAIL"):
|
|
324
|
+
table.add_column(col)
|
|
325
|
+
for label, ok, detail in _doctor_checks():
|
|
326
|
+
mark = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
327
|
+
table.add_row(label, mark, detail)
|
|
328
|
+
console.print(table)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _version() -> str:
|
|
332
|
+
import importlib.metadata
|
|
333
|
+
try:
|
|
334
|
+
return importlib.metadata.version("omega-code")
|
|
335
|
+
except importlib.metadata.PackageNotFoundError:
|
|
336
|
+
return "0.0.0-dev"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _usage_text() -> str:
|
|
340
|
+
return (
|
|
341
|
+
"omega -- a fast, small coding agent for your terminal\n\n"
|
|
342
|
+
"usage:\n"
|
|
343
|
+
" omega [prompt] [flags] one-shot, or bare for the interactive TUI\n"
|
|
344
|
+
" omega <subcommand> [args]\n\n"
|
|
345
|
+
"subcommands:\n"
|
|
346
|
+
" sessions list saved sessions\n"
|
|
347
|
+
" resume [id] resume a session (prefix works; no id -- pick from a list)\n"
|
|
348
|
+
" continue resume this directory's last session\n"
|
|
349
|
+
" models show the model catalog and role defaults\n"
|
|
350
|
+
" skills list available skills\n"
|
|
351
|
+
" memory gc consolidate memory now\n"
|
|
352
|
+
" connections [...] manage MCP servers\n"
|
|
353
|
+
" eval [...] run the eval harness (see `omega eval --help`)\n"
|
|
354
|
+
" trace <id> print a session's event trace (--tools, --json)\n"
|
|
355
|
+
" update update omega to the latest release\n"
|
|
356
|
+
" keys [...] show, store, or migrate provider API keys\n"
|
|
357
|
+
" doctor check your environment and config\n"
|
|
358
|
+
" setup browser-based setup wizard\n"
|
|
359
|
+
" onboard terminal setup wizard\n\n"
|
|
360
|
+
"flags:\n"
|
|
361
|
+
" --plan, -p read-only planning mode\n"
|
|
362
|
+
" --model <alias> override the model for this session\n"
|
|
363
|
+
" --continue, -c resume this directory's last session\n"
|
|
364
|
+
" --resume <id> resume a specific session (id prefix works)\n"
|
|
365
|
+
" --yolo skip permission prompts\n"
|
|
366
|
+
" --mcp connect all MCP servers eagerly at startup\n"
|
|
367
|
+
" --version print the version and exit\n"
|
|
368
|
+
" -h, --help, help show this message"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _render_models_table(cfg: Config) -> str:
|
|
373
|
+
role_defaults: dict[str, list[str]] = {}
|
|
374
|
+
for role_name, role in cfg.roles.items():
|
|
375
|
+
if role.alias:
|
|
376
|
+
role_defaults.setdefault(role.alias, []).append(role_name)
|
|
377
|
+
|
|
378
|
+
lines = [f"{'ALIAS':<12}{'MODEL':<26}{'PROVIDER':<16}{'CONTEXT':>10}"
|
|
379
|
+
f"{'EFFORT':>8} DEFAULT FOR"]
|
|
380
|
+
for alias, m in sorted(cfg.models.items()):
|
|
381
|
+
roles = ", ".join(sorted(role_defaults.get(alias, [])))
|
|
382
|
+
lines.append(f"{alias:<12}{m.model:<26}{m.provider:<16}{m.context:>10,}"
|
|
383
|
+
f"{m.effort or '-':>8} {roles}")
|
|
384
|
+
return "\n".join(lines)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _render_skills(argv: list[str]) -> None:
|
|
388
|
+
from rich.table import Table
|
|
389
|
+
|
|
390
|
+
if argv and argv[0] == "show":
|
|
391
|
+
if len(argv) < 2:
|
|
392
|
+
return console.print("[red]usage: omega skills show <name>[/red]")
|
|
393
|
+
body = skills.load_body(argv[1])
|
|
394
|
+
if body is None:
|
|
395
|
+
return console.print(f"[red]no skill named {argv[1]!r}[/red]")
|
|
396
|
+
return console.print(body)
|
|
397
|
+
|
|
398
|
+
table = Table(box=None)
|
|
399
|
+
for col in ("NAME", "SOURCE", "DESCRIPTION"):
|
|
400
|
+
table.add_column(col)
|
|
401
|
+
for s in skills.catalog():
|
|
402
|
+
table.add_row(s.name, s.source, s.description)
|
|
403
|
+
console.print(table)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _fmt_last_used(ts: float | None) -> str:
|
|
407
|
+
if ts is None:
|
|
408
|
+
return "-"
|
|
409
|
+
import datetime
|
|
410
|
+
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _connections_rows() -> list[tuple[str, str, str, str, str, str]]:
|
|
414
|
+
"""(name, state, tools, auth, source, last used), built from
|
|
415
|
+
integrations.overview() -- omega's own servers plus what's importable from
|
|
416
|
+
the catalog or Claude Code but isn't configured yet."""
|
|
417
|
+
from . import integrations
|
|
418
|
+
rows: list[tuple[str, str, str, str, str, str]] = []
|
|
419
|
+
for r in integrations.overview():
|
|
420
|
+
state = r["state"]
|
|
421
|
+
if r["source"] == "catalog" and not r["verified"]:
|
|
422
|
+
state = f"{state} (unverified)"
|
|
423
|
+
rows.append((r["name"], state, str(r["tools"]) if r["state"] == "connected" else "-",
|
|
424
|
+
r["auth"] or "-", r["source"], _fmt_last_used(r["last_used"])))
|
|
425
|
+
return rows
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def _connections(argv: list[str]) -> None:
|
|
429
|
+
from rich.table import Table
|
|
430
|
+
|
|
431
|
+
if not argv:
|
|
432
|
+
table = Table(box=None)
|
|
433
|
+
for col in ("NAME", "STATE", "TOOLS", "AUTH", "SOURCE", "LAST USED"):
|
|
434
|
+
table.add_column(col)
|
|
435
|
+
for row in _connections_rows():
|
|
436
|
+
table.add_row(*row)
|
|
437
|
+
return console.print(table)
|
|
438
|
+
|
|
439
|
+
sub, rest = argv[0], argv[1:]
|
|
440
|
+
|
|
441
|
+
if sub == "catalog":
|
|
442
|
+
from . import integrations
|
|
443
|
+
by_category: dict[str, list[Any]] = {}
|
|
444
|
+
for i in integrations.CATALOG.values():
|
|
445
|
+
by_category.setdefault(i.category, []).append(i)
|
|
446
|
+
for category in sorted(by_category):
|
|
447
|
+
console.print(f"\n[bold]{category}[/bold]")
|
|
448
|
+
for i in sorted(by_category[category], key=lambda x: x.key):
|
|
449
|
+
tag = "" if i.verified else " [dim]unverified[/dim]"
|
|
450
|
+
console.print(f" [bold]{i.key:<20}[/bold] {i.blurb}{tag}")
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if sub == "add":
|
|
454
|
+
return await _connections_add(rest)
|
|
455
|
+
|
|
456
|
+
if sub in ("connect", "test"):
|
|
457
|
+
if not rest:
|
|
458
|
+
return console.print(f"[red]usage: omega connections {sub} <name>[/red]")
|
|
459
|
+
name = rest[0]
|
|
460
|
+
st = await mcp.connect(name)
|
|
461
|
+
if sub == "test":
|
|
462
|
+
await mcp.disconnect(name)
|
|
463
|
+
if st.state == "needs_auth":
|
|
464
|
+
console.print(f"[yellow]{name}: needs auth[/yellow] -- open {st.error} to "
|
|
465
|
+
f"authorise, then run `omega connections connect {name}` again")
|
|
466
|
+
elif st.state == "connected":
|
|
467
|
+
console.print(f"[green]{name}: connected[/green] ({st.tools} tools)")
|
|
468
|
+
else:
|
|
469
|
+
console.print(f"[red]{name}: {st.state}[/red]" + (f" -- {st.error}" if st.error else ""))
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
if sub in ("enable", "disable"):
|
|
473
|
+
if not rest:
|
|
474
|
+
return console.print(f"[red]usage: omega connections {sub} <name>[/red]")
|
|
475
|
+
name = rest[0]
|
|
476
|
+
try:
|
|
477
|
+
await mcp.enable(name, sub == "enable")
|
|
478
|
+
except KeyError:
|
|
479
|
+
return console.print(f"[red]no such server {name!r}[/red] (see `omega connections`)")
|
|
480
|
+
console.print(f"[green]{name}: {'enabled' if sub == 'enable' else 'disabled'}[/green]")
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if sub == "remove":
|
|
484
|
+
if not rest:
|
|
485
|
+
return console.print("[red]usage: omega connections remove <name>[/red]")
|
|
486
|
+
await mcp.remove(rest[0])
|
|
487
|
+
console.print(f"[green]{rest[0]}: removed[/green]")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
console.print(f"[red]unknown `omega connections {sub}`[/red] -- add, connect, enable, "
|
|
491
|
+
f"disable, remove, test, catalog")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
async def _connections_add(rest: list[str]) -> None:
|
|
495
|
+
import shlex
|
|
496
|
+
|
|
497
|
+
from . import integrations
|
|
498
|
+
|
|
499
|
+
if not rest:
|
|
500
|
+
return console.print("[red]usage: omega connections add <catalog-key|name> "
|
|
501
|
+
"[--url U | --cmd \"...\"] [--env K=V ...][/red]")
|
|
502
|
+
name = rest[0]
|
|
503
|
+
url = cmd = None
|
|
504
|
+
env: dict[str, str] = {}
|
|
505
|
+
i = 1
|
|
506
|
+
while i < len(rest):
|
|
507
|
+
a = rest[i]
|
|
508
|
+
if a == "--url" and i + 1 < len(rest):
|
|
509
|
+
url, i = rest[i + 1], i + 2
|
|
510
|
+
elif a == "--cmd" and i + 1 < len(rest):
|
|
511
|
+
cmd, i = rest[i + 1], i + 2
|
|
512
|
+
elif a == "--env" and i + 1 < len(rest):
|
|
513
|
+
k, _, v = rest[i + 1].partition("=")
|
|
514
|
+
env[k] = v
|
|
515
|
+
i += 2
|
|
516
|
+
else:
|
|
517
|
+
i += 1
|
|
518
|
+
|
|
519
|
+
catalog = integrations.CATALOG.get(name)
|
|
520
|
+
spec: dict[str, Any] = {}
|
|
521
|
+
if catalog is not None:
|
|
522
|
+
spec["catalog"] = catalog.key
|
|
523
|
+
if catalog.transport == "remote" and catalog.url:
|
|
524
|
+
spec["url"] = catalog.url
|
|
525
|
+
elif catalog.command:
|
|
526
|
+
import os
|
|
527
|
+
cmdline = [c.replace("<cwd>", os.getcwd()) for c in catalog.command]
|
|
528
|
+
spec["command"], spec["args"] = cmdline[0], cmdline[1:]
|
|
529
|
+
|
|
530
|
+
if url:
|
|
531
|
+
spec["url"] = url
|
|
532
|
+
spec.pop("command", None)
|
|
533
|
+
spec.pop("args", None)
|
|
534
|
+
if cmd:
|
|
535
|
+
parts = shlex.split(cmd)
|
|
536
|
+
spec["command"], spec["args"] = parts[0], parts[1:]
|
|
537
|
+
spec.pop("url", None)
|
|
538
|
+
if env:
|
|
539
|
+
spec["env"] = env
|
|
540
|
+
|
|
541
|
+
if not spec.get("command") and not spec.get("url"):
|
|
542
|
+
return console.print("[red]give --url, --cmd, or a known catalog key[/red] "
|
|
543
|
+
"(see `omega connections catalog`)")
|
|
544
|
+
|
|
545
|
+
mcp.add(name, spec)
|
|
546
|
+
hint = ""
|
|
547
|
+
if catalog and catalog.auth == "oauth":
|
|
548
|
+
hint = f" -- run `omega connections connect {name}` to authorise"
|
|
549
|
+
elif catalog and catalog.env and not env:
|
|
550
|
+
hint = f" -- needs env: {', '.join(catalog.env)} (rerun with --env K=V)"
|
|
551
|
+
console.print(f"[green]{name}: added[/green]{hint}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def _consolidate_on_close(cfg: Config) -> None:
|
|
555
|
+
# A provider error here must never block exit -- consolidation is a
|
|
556
|
+
# courtesy, not a precondition for closing the session.
|
|
557
|
+
try:
|
|
558
|
+
for scope in ("project", "global"):
|
|
559
|
+
summary = await consolidate.run(cfg, scope, force=False)
|
|
560
|
+
if summary:
|
|
561
|
+
console.print(f"[dim]{summary}[/dim]")
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
async def _main_guarded() -> None:
|
|
567
|
+
try:
|
|
568
|
+
await main()
|
|
569
|
+
finally:
|
|
570
|
+
await mcp.shutdown()
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def cli() -> None:
|
|
574
|
+
migrate.run()
|
|
575
|
+
# uvicorn owns its own event loop, so the daemon must start before
|
|
576
|
+
# asyncio.run() below.
|
|
577
|
+
if len(sys.argv) > 1 and sys.argv[1] == "serve":
|
|
578
|
+
from .server.app import main as serve_main
|
|
579
|
+
args = sys.argv[2:]
|
|
580
|
+
port = int(args[args.index("--port") + 1]) if "--port" in args else 7777
|
|
581
|
+
return serve_main(port=port)
|
|
582
|
+
try:
|
|
583
|
+
asyncio.run(_main_guarded())
|
|
584
|
+
except KeyboardInterrupt:
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
if __name__ == "__main__":
|
|
589
|
+
cli()
|