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/gitlog.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
_SKIP_DIRS = {"node_modules", ".venv"}
|
|
10
|
+
_UNIT_SEP = "\x1f"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Repo:
|
|
15
|
+
path: Path
|
|
16
|
+
name: str
|
|
17
|
+
branch: str
|
|
18
|
+
dirty: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Commit:
|
|
23
|
+
sha: str
|
|
24
|
+
short_sha: str
|
|
25
|
+
author: str
|
|
26
|
+
age: str
|
|
27
|
+
subject: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Change:
|
|
32
|
+
path: str
|
|
33
|
+
status: Literal["M", "A", "D", "R", "?"]
|
|
34
|
+
added: int
|
|
35
|
+
removed: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run(repo_path: Path, args: list[str]) -> str | None:
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["git", "-c", "core.quotepath=off", *args],
|
|
42
|
+
cwd=repo_path,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=10,
|
|
46
|
+
)
|
|
47
|
+
except (OSError, subprocess.SubprocessError):
|
|
48
|
+
return None
|
|
49
|
+
if result.returncode != 0:
|
|
50
|
+
return None
|
|
51
|
+
return result.stdout
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_repo(d: Path) -> bool:
|
|
55
|
+
return (d / ".git").exists()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _branch(repo_path: Path) -> str:
|
|
59
|
+
out = _run(repo_path, ["symbolic-ref", "--short", "-q", "HEAD"])
|
|
60
|
+
if out is not None and out.strip():
|
|
61
|
+
return out.strip()
|
|
62
|
+
out = _run(repo_path, ["rev-parse", "--short", "HEAD"])
|
|
63
|
+
if out is not None and out.strip():
|
|
64
|
+
return out.strip()
|
|
65
|
+
return "?"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dirty(repo_path: Path) -> bool:
|
|
69
|
+
out = _run(repo_path, ["status", "--porcelain", "--untracked-files=normal"])
|
|
70
|
+
return bool(out and out.strip())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_repo(d: Path) -> Repo:
|
|
74
|
+
if shutil.which("git") is None:
|
|
75
|
+
return Repo(path=d, name=d.name, branch="?", dirty=False)
|
|
76
|
+
return Repo(path=d, name=d.name, branch=_branch(d), dirty=_dirty(d))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def discover_repos(root: Path, max_depth: int = 2) -> list[Repo]:
|
|
80
|
+
if shutil.which("git") is None:
|
|
81
|
+
return []
|
|
82
|
+
root = root.resolve()
|
|
83
|
+
if _is_repo(root):
|
|
84
|
+
return [_make_repo(root)]
|
|
85
|
+
|
|
86
|
+
found: list[Repo] = []
|
|
87
|
+
|
|
88
|
+
def walk(d: Path, depth: int) -> None:
|
|
89
|
+
if depth > max_depth:
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
children = sorted(p for p in d.iterdir() if p.is_dir())
|
|
93
|
+
except OSError:
|
|
94
|
+
return
|
|
95
|
+
for child in children:
|
|
96
|
+
if child.name.startswith(".") or child.name in _SKIP_DIRS:
|
|
97
|
+
continue
|
|
98
|
+
if _is_repo(child):
|
|
99
|
+
found.append(_make_repo(child))
|
|
100
|
+
# Never descend into a discovered repo: nested .git dirs
|
|
101
|
+
# (submodules, vendored checkouts) aren't separate projects.
|
|
102
|
+
continue
|
|
103
|
+
walk(child, depth + 1)
|
|
104
|
+
|
|
105
|
+
walk(root, 1)
|
|
106
|
+
return sorted(found, key=lambda r: r.name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _relative_age(seconds: float) -> str:
|
|
110
|
+
seconds = max(0.0, seconds)
|
|
111
|
+
if seconds < 60:
|
|
112
|
+
return f"{int(seconds)}s"
|
|
113
|
+
minutes = seconds / 60
|
|
114
|
+
if minutes < 60:
|
|
115
|
+
return f"{int(minutes)}m"
|
|
116
|
+
hours = minutes / 60
|
|
117
|
+
if hours < 24:
|
|
118
|
+
return f"{int(hours)}h"
|
|
119
|
+
days = hours / 24
|
|
120
|
+
if days < 7:
|
|
121
|
+
return f"{int(days)}d"
|
|
122
|
+
weeks = days / 7
|
|
123
|
+
if weeks < 52:
|
|
124
|
+
return f"{int(weeks)}w"
|
|
125
|
+
years = days / 365
|
|
126
|
+
return f"{int(years)}y"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def recent_commits(repo: Repo, limit: int = 20) -> list[Commit]:
|
|
130
|
+
if shutil.which("git") is None:
|
|
131
|
+
return []
|
|
132
|
+
out = _run(repo.path, ["log", f"--format=%H{_UNIT_SEP}%an{_UNIT_SEP}%ct{_UNIT_SEP}%s", "-n", str(limit)])
|
|
133
|
+
if not out:
|
|
134
|
+
return []
|
|
135
|
+
now = time.time()
|
|
136
|
+
commits: list[Commit] = []
|
|
137
|
+
for line in out.splitlines():
|
|
138
|
+
parts = line.split(_UNIT_SEP)
|
|
139
|
+
if len(parts) != 4:
|
|
140
|
+
continue
|
|
141
|
+
sha, author, ct, subject = parts
|
|
142
|
+
commits.append(Commit(
|
|
143
|
+
sha=sha,
|
|
144
|
+
short_sha=sha[:7],
|
|
145
|
+
author=author,
|
|
146
|
+
age=_relative_age(now - float(ct)),
|
|
147
|
+
subject=subject,
|
|
148
|
+
))
|
|
149
|
+
return commits
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _numstat_counts(repo_path: Path) -> dict[str, tuple[int, int]]:
|
|
153
|
+
out = _run(repo_path, ["diff", "--numstat", "HEAD"])
|
|
154
|
+
if not out:
|
|
155
|
+
return {}
|
|
156
|
+
counts: dict[str, tuple[int, int]] = {}
|
|
157
|
+
for line in out.splitlines():
|
|
158
|
+
fields = line.split("\t")
|
|
159
|
+
if len(fields) != 3:
|
|
160
|
+
continue
|
|
161
|
+
added_s, removed_s, path = fields
|
|
162
|
+
try:
|
|
163
|
+
counts[path] = (int(added_s), int(removed_s))
|
|
164
|
+
except ValueError:
|
|
165
|
+
continue
|
|
166
|
+
return counts
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def working_tree(repo: Repo) -> list[Change]:
|
|
170
|
+
if shutil.which("git") is None:
|
|
171
|
+
return []
|
|
172
|
+
out = _run(repo.path, ["status", "--porcelain", "--untracked-files=normal"])
|
|
173
|
+
if not out:
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
numstat = _numstat_counts(repo.path)
|
|
177
|
+
changes: list[Change] = []
|
|
178
|
+
for line in out.splitlines():
|
|
179
|
+
if not line:
|
|
180
|
+
continue
|
|
181
|
+
xy = line[:2]
|
|
182
|
+
path = line[3:]
|
|
183
|
+
if xy == "??":
|
|
184
|
+
changes.append(Change(path=path, status="?", added=0, removed=0))
|
|
185
|
+
elif "R" in xy:
|
|
186
|
+
changes.append(Change(path=path.split(" -> ", 1)[-1], status="R", added=0, removed=0))
|
|
187
|
+
elif "A" in xy:
|
|
188
|
+
added, removed = numstat.get(path, (0, 0))
|
|
189
|
+
changes.append(Change(path=path, status="A", added=added, removed=removed))
|
|
190
|
+
elif "D" in xy:
|
|
191
|
+
changes.append(Change(path=path, status="D", added=0, removed=0))
|
|
192
|
+
elif "M" in xy:
|
|
193
|
+
added, removed = numstat.get(path, (0, 0))
|
|
194
|
+
changes.append(Change(path=path, status="M", added=added, removed=removed))
|
|
195
|
+
|
|
196
|
+
return sorted(changes, key=lambda c: c.path)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def diff(repo: Repo, path: str) -> str:
|
|
200
|
+
out = _run(repo.path, ["diff", "HEAD", "--", path])
|
|
201
|
+
if out:
|
|
202
|
+
return out
|
|
203
|
+
try:
|
|
204
|
+
result = subprocess.run(
|
|
205
|
+
["git", "-c", "core.quotepath=off", "diff", "--no-index", "--", "/dev/null", path],
|
|
206
|
+
cwd=repo.path,
|
|
207
|
+
capture_output=True,
|
|
208
|
+
text=True,
|
|
209
|
+
timeout=10,
|
|
210
|
+
)
|
|
211
|
+
except (OSError, subprocess.SubprocessError):
|
|
212
|
+
return ""
|
|
213
|
+
return result.stdout or ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def discover_repos_async(root: Path, max_depth: int = 2) -> list[Repo]:
|
|
217
|
+
return await asyncio.to_thread(discover_repos, root, max_depth)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def recent_commits_async(repo: Repo, limit: int = 20) -> list[Commit]:
|
|
221
|
+
return await asyncio.to_thread(recent_commits, repo, limit)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def working_tree_async(repo: Repo) -> list[Change]:
|
|
225
|
+
return await asyncio.to_thread(working_tree, repo)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def diff_async(repo: Repo, path: str) -> str:
|
|
229
|
+
return await asyncio.to_thread(diff, repo, path)
|
omega/hooks.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shell hooks around tool dispatch -- see config.HookRule and tools.run().
|
|
2
|
+
|
|
3
|
+
A pre-hook can block a tool call (non-zero exit); its stdout becomes the
|
|
4
|
+
denial reason. A post-hook's stdout is appended to the tool result. Hook
|
|
5
|
+
failures (bad command, timeout, non-zero post-hook exit) never crash the
|
|
6
|
+
turn -- a broken hook just contributes nothing.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
from .config import HookRule
|
|
13
|
+
|
|
14
|
+
TIMEOUT = 60
|
|
15
|
+
MAX_POST_APPEND = 2000
|
|
16
|
+
MAX_RESULT_ENV = 8000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _applicable(rules: list[HookRule], tool_name: str) -> list[HookRule]:
|
|
20
|
+
return [r for r in rules if tool_name in r.tools]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _hook_name(command: str) -> str:
|
|
24
|
+
parts = command.split(None, 1)
|
|
25
|
+
return parts[0] if parts else command
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_pre(rules: list[HookRule], tool_name: str, args: dict[str, object],
|
|
29
|
+
cwd: str) -> tuple[bool, str]:
|
|
30
|
+
"""Returns (blocked, message). The first hook that exits non-zero blocks
|
|
31
|
+
the call; its stdout (or stderr as a fallback) is the message."""
|
|
32
|
+
for rule in _applicable(rules, tool_name):
|
|
33
|
+
env = {**os.environ, "OMEGA_TOOL": tool_name,
|
|
34
|
+
"OMEGA_ARGS_JSON": json.dumps(args, default=str), "OMEGA_CWD": cwd}
|
|
35
|
+
try:
|
|
36
|
+
proc = subprocess.run(rule.command, shell=True, cwd=cwd, env=env,
|
|
37
|
+
capture_output=True, text=True, timeout=TIMEOUT)
|
|
38
|
+
except (OSError, subprocess.SubprocessError):
|
|
39
|
+
continue
|
|
40
|
+
if proc.returncode != 0:
|
|
41
|
+
return True, (proc.stdout or proc.stderr or f"hook {_hook_name(rule.command)!r} "
|
|
42
|
+
f"exited {proc.returncode}").strip()
|
|
43
|
+
return False, ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_post(rules: list[HookRule], tool_name: str, args: dict[str, object], cwd: str,
|
|
47
|
+
result: str) -> str:
|
|
48
|
+
"""Returns text to append to the tool result (empty when no post-hook
|
|
49
|
+
produced output)."""
|
|
50
|
+
appended: list[str] = []
|
|
51
|
+
for rule in _applicable(rules, tool_name):
|
|
52
|
+
env = {**os.environ, "OMEGA_TOOL": tool_name,
|
|
53
|
+
"OMEGA_ARGS_JSON": json.dumps(args, default=str), "OMEGA_CWD": cwd,
|
|
54
|
+
"OMEGA_RESULT": result[:MAX_RESULT_ENV]}
|
|
55
|
+
try:
|
|
56
|
+
proc = subprocess.run(rule.command, shell=True, cwd=cwd, env=env,
|
|
57
|
+
capture_output=True, text=True, timeout=TIMEOUT)
|
|
58
|
+
except (OSError, subprocess.SubprocessError):
|
|
59
|
+
continue
|
|
60
|
+
out = (proc.stdout or "").strip()
|
|
61
|
+
if out:
|
|
62
|
+
appended.append(f"[hook {_hook_name(rule.command)}] {out}")
|
|
63
|
+
return "\n".join(appended)[:MAX_POST_APPEND]
|
omega/instructions.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Project and global instruction files for the STABLE half of the system
|
|
2
|
+
prompt -- the omega analogue of CLAUDE.md, read once at startup rather than
|
|
3
|
+
injected per-turn like `memory`.
|
|
4
|
+
|
|
5
|
+
Load order (least to most specific, so a subdirectory's file adds to the
|
|
6
|
+
root's rather than replacing it):
|
|
7
|
+
|
|
8
|
+
1. ``~/.omega/OMEGA.md`` -- global, applies to every project
|
|
9
|
+
2. ``OMEGA.md`` at each directory from the git root down to ``cwd``
|
|
10
|
+
3. ``.omega/instructions.md`` in ``cwd``, if present
|
|
11
|
+
|
|
12
|
+
At each of the locations in (2) and (3), ``CLAUDE.md`` is read instead when
|
|
13
|
+
no ``OMEGA.md`` exists there -- many projects already have one.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
MAX_CHARS = 12_000
|
|
19
|
+
GLOBAL_PATH = Path.home() / ".omega" / "OMEGA.md"
|
|
20
|
+
|
|
21
|
+
_PRIMARY = "OMEGA.md"
|
|
22
|
+
_FALLBACK = "CLAUDE.md"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _git_root(start: Path) -> Path | None:
|
|
26
|
+
for candidate in (start, *start.parents):
|
|
27
|
+
if (candidate / ".git").exists():
|
|
28
|
+
return candidate
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _project_dirs(cwd: Path, root: Path) -> list[Path]:
|
|
33
|
+
"""root, then each path component down to cwd -- root first, so its file
|
|
34
|
+
is read before (and therefore superseded in emphasis by) a subdir's."""
|
|
35
|
+
if cwd == root:
|
|
36
|
+
return [root]
|
|
37
|
+
dirs = [root]
|
|
38
|
+
current = root
|
|
39
|
+
for part in cwd.relative_to(root).parts:
|
|
40
|
+
current = current / part
|
|
41
|
+
dirs.append(current)
|
|
42
|
+
return dirs
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find(d: Path) -> tuple[Path, str] | None:
|
|
46
|
+
for fname in (_PRIMARY, _FALLBACK):
|
|
47
|
+
p = d / fname
|
|
48
|
+
if p.is_file():
|
|
49
|
+
return p, fname
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _block(path: Path, fname: str, relpath: str) -> str | None:
|
|
54
|
+
try:
|
|
55
|
+
text = path.read_text(errors="replace").strip()
|
|
56
|
+
except OSError:
|
|
57
|
+
return None
|
|
58
|
+
if not text:
|
|
59
|
+
return None
|
|
60
|
+
return f"# Instructions ({fname} at {relpath})\n{text}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cap(text: str, limit: int | None = None) -> str:
|
|
64
|
+
limit = MAX_CHARS if limit is None else limit
|
|
65
|
+
if len(text) <= limit:
|
|
66
|
+
return text
|
|
67
|
+
dropped = len(text) - limit
|
|
68
|
+
return (f"{text[:limit]}\n\n...[truncated {dropped} chars; "
|
|
69
|
+
f"read the source file directly with `read` for the rest]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load(cwd: str | Path | None = None) -> str:
|
|
73
|
+
cwd_path = Path(cwd).resolve() if cwd else Path(os.getcwd())
|
|
74
|
+
blocks: list[str] = []
|
|
75
|
+
|
|
76
|
+
if GLOBAL_PATH.is_file():
|
|
77
|
+
block = _block(GLOBAL_PATH, _PRIMARY, "~/.omega")
|
|
78
|
+
if block:
|
|
79
|
+
blocks.append(block)
|
|
80
|
+
|
|
81
|
+
root = _git_root(cwd_path) or cwd_path
|
|
82
|
+
for d in _project_dirs(cwd_path, root):
|
|
83
|
+
found = _find(d)
|
|
84
|
+
if found is None:
|
|
85
|
+
continue
|
|
86
|
+
path, fname = found
|
|
87
|
+
relpath = "." if d == root else str(d.relative_to(root))
|
|
88
|
+
block = _block(path, fname, relpath)
|
|
89
|
+
if block:
|
|
90
|
+
blocks.append(block)
|
|
91
|
+
|
|
92
|
+
local = cwd_path / ".omega" / "instructions.md"
|
|
93
|
+
block = _block(local, "instructions.md", ".omega/instructions.md")
|
|
94
|
+
if block:
|
|
95
|
+
blocks.append(block)
|
|
96
|
+
|
|
97
|
+
return _cap("\n\n".join(blocks))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def system_block(cwd: str | Path | None = None) -> str:
|
|
101
|
+
"""Empty string when nothing is found, so a caller can splice it in
|
|
102
|
+
unconditionally without an extra blank section."""
|
|
103
|
+
return load(cwd)
|