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.
Files changed (73) hide show
  1. omega/__init__.py +0 -0
  2. omega/__main__.py +589 -0
  3. omega/artifacts.py +151 -0
  4. omega/checkpoint.py +246 -0
  5. omega/compact.py +106 -0
  6. omega/config.py +285 -0
  7. omega/eval/__init__.py +3 -0
  8. omega/eval/cli.py +127 -0
  9. omega/eval/examples/plan-version-flag.yaml +11 -0
  10. omega/eval/examples/relative-age-negative-delta.yaml +14 -0
  11. omega/eval/examples/version-flag.yaml +10 -0
  12. omega/eval/manifest.py +129 -0
  13. omega/eval/prices.py +29 -0
  14. omega/eval/report.py +135 -0
  15. omega/eval/runner.py +199 -0
  16. omega/eval/tasks.py +97 -0
  17. omega/events.py +145 -0
  18. omega/export.py +80 -0
  19. omega/gitlog.py +229 -0
  20. omega/hooks.py +63 -0
  21. omega/instructions.py +103 -0
  22. omega/integrations.py +284 -0
  23. omega/keys.py +173 -0
  24. omega/llm.py +442 -0
  25. omega/loop.py +510 -0
  26. omega/mcp.py +490 -0
  27. omega/memory/__init__.py +5 -0
  28. omega/memory/consolidate.py +103 -0
  29. omega/memory/curate.py +69 -0
  30. omega/memory/store.py +321 -0
  31. omega/memory/tools.py +175 -0
  32. omega/migrate.py +40 -0
  33. omega/onboarding.py +242 -0
  34. omega/permissions.py +137 -0
  35. omega/secrets.py +173 -0
  36. omega/server/__init__.py +7 -0
  37. omega/server/__main__.py +18 -0
  38. omega/server/app.py +71 -0
  39. omega/server/auth.py +73 -0
  40. omega/server/manager.py +287 -0
  41. omega/server/models.py +123 -0
  42. omega/server/tasks_api.py +311 -0
  43. omega/server/terminals.py +245 -0
  44. omega/server/worker.py +186 -0
  45. omega/session.py +209 -0
  46. omega/setup.html +281 -0
  47. omega/setup_server.py +452 -0
  48. omega/skills.py +158 -0
  49. omega/subagent.py +98 -0
  50. omega/tasks.py +195 -0
  51. omega/tools.py +590 -0
  52. omega/trace.py +156 -0
  53. omega/trajectory.py +146 -0
  54. omega/ui/__init__.py +0 -0
  55. omega/ui/composer.py +140 -0
  56. omega/ui/format.py +708 -0
  57. omega/ui/plain.py +141 -0
  58. omega/ui/tui/__init__.py +9 -0
  59. omega/ui/tui/app.py +958 -0
  60. omega/ui/tui/history.py +50 -0
  61. omega/ui/tui/modals.py +292 -0
  62. omega/ui/tui/onboarding.py +367 -0
  63. omega/ui/tui/prefs.py +25 -0
  64. omega/ui/tui/sidebar.py +510 -0
  65. omega/ui/tui/status.py +115 -0
  66. omega/ui/tui/theme.py +91 -0
  67. omega/ui/tui/transcript.py +783 -0
  68. omega/verify.py +133 -0
  69. omega_code-0.4.0.dist-info/METADATA +479 -0
  70. omega_code-0.4.0.dist-info/RECORD +73 -0
  71. omega_code-0.4.0.dist-info/WHEEL +4 -0
  72. omega_code-0.4.0.dist-info/entry_points.txt +2 -0
  73. 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)