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/integrations.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""The catalog of well-known public MCP servers `omega connections` can offer,
|
|
2
|
+
plus a read-only view of whatever Claude Code already has configured.
|
|
3
|
+
|
|
4
|
+
`verified=True` means the url/command was confirmed against the vendor's own
|
|
5
|
+
current docs (or the official modelcontextprotocol/servers repo). Where that
|
|
6
|
+
couldn't be confirmed, the entry keeps its best-known value with
|
|
7
|
+
`verified=False` and a `docs` link so the user can check before connecting --
|
|
8
|
+
never a guessed endpoint standing in for a real one."""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from . import config, mcp
|
|
14
|
+
|
|
15
|
+
Transport = Literal["remote", "stdio"]
|
|
16
|
+
Auth = Literal["oauth", "api_key", "none"]
|
|
17
|
+
Category = Literal["search & web", "code & dev", "cloud & infra", "data",
|
|
18
|
+
"observability", "work & pm", "payments & commerce", "utilities", "design"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Integration:
|
|
23
|
+
key: str
|
|
24
|
+
name: str
|
|
25
|
+
category: Category
|
|
26
|
+
blurb: str
|
|
27
|
+
transport: Transport
|
|
28
|
+
url: str | None
|
|
29
|
+
command: list[str] | None
|
|
30
|
+
auth: Auth
|
|
31
|
+
verified: bool
|
|
32
|
+
env: tuple[str, ...] = field(default_factory=tuple)
|
|
33
|
+
docs: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _remote(key: str, name: str, category: Category, blurb: str, url: str | None, auth: Auth,
|
|
37
|
+
verified: bool, env: tuple[str, ...] = (), docs: str = "") -> Integration:
|
|
38
|
+
return Integration(key, name, category, blurb, "remote", url, None, auth, verified, env, docs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _stdio(key: str, name: str, category: Category, blurb: str, command: list[str], auth: Auth,
|
|
42
|
+
verified: bool, env: tuple[str, ...] = (), docs: str = "") -> Integration:
|
|
43
|
+
return Integration(key, name, category, blurb, "stdio", None, command, auth, verified, env, docs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
CATALOG: dict[str, Integration] = {
|
|
47
|
+
i.key: i for i in [
|
|
48
|
+
# -- search & web --------------------------------------------------
|
|
49
|
+
_remote("perplexity", "Perplexity", "search & web", "Web-grounded answers and research.",
|
|
50
|
+
"https://api.perplexity.ai/mcp", "api_key", True,
|
|
51
|
+
env=("PERPLEXITY_API_KEY",),
|
|
52
|
+
docs="https://docs.perplexity.ai/docs/getting-started/integrations/mcp-server"),
|
|
53
|
+
_stdio("exa", "Exa", "search & web", "AI-native web search.",
|
|
54
|
+
["npx", "-y", "exa-mcp-server"], "api_key", True,
|
|
55
|
+
env=("EXA_API_KEY",),
|
|
56
|
+
docs="https://docs.exa.ai/reference/exa-mcp"),
|
|
57
|
+
_stdio("brave-search", "Brave Search", "search & web", "Web, local, image, video and news search.",
|
|
58
|
+
["npx", "-y", "@brave/brave-search-mcp-server"], "api_key", True,
|
|
59
|
+
env=("BRAVE_API_KEY",),
|
|
60
|
+
docs="https://www.npmjs.com/package/@brave/brave-search-mcp-server"),
|
|
61
|
+
_stdio("tavily", "Tavily", "search & web", "Search and extraction tuned for agents.",
|
|
62
|
+
["npx", "-y", "tavily-mcp"], "api_key", True,
|
|
63
|
+
env=("TAVILY_API_KEY",),
|
|
64
|
+
docs="https://docs.tavily.com/documentation/mcp"),
|
|
65
|
+
_stdio("firecrawl", "Firecrawl", "search & web", "Scrape, crawl and extract structured web data.",
|
|
66
|
+
["npx", "-y", "firecrawl-mcp"], "api_key", True,
|
|
67
|
+
env=("FIRECRAWL_API_KEY",),
|
|
68
|
+
docs="https://docs.firecrawl.dev/mcp-server/local"),
|
|
69
|
+
_stdio("fetch", "Fetch", "search & web", "Fetch a URL and convert it to markdown.",
|
|
70
|
+
["uvx", "mcp-server-fetch"], "none", True,
|
|
71
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/fetch"),
|
|
72
|
+
|
|
73
|
+
# -- code & dev -------------------------------------------------------
|
|
74
|
+
_remote("github", "GitHub", "code & dev", "Repos, issues, PRs and code search.",
|
|
75
|
+
"https://api.githubcopilot.com/mcp/", "oauth", True,
|
|
76
|
+
docs="https://github.com/github/github-mcp-server"),
|
|
77
|
+
_remote("gitlab", "GitLab", "code & dev", "Projects, issues and merge requests (needs Duo enabled).",
|
|
78
|
+
"https://gitlab.com/api/v4/mcp", "oauth", True,
|
|
79
|
+
docs="https://docs.gitlab.com/user/model_context_protocol/mcp_server/"),
|
|
80
|
+
_remote("context7", "Context7", "code & dev", "Up-to-date library and framework docs.",
|
|
81
|
+
"https://mcp.context7.com/mcp", "api_key", True,
|
|
82
|
+
env=("CONTEXT7_API_KEY",),
|
|
83
|
+
docs="https://github.com/upstash/context7"),
|
|
84
|
+
_stdio("chrome-devtools", "Chrome DevTools", "code & dev", "Drive and inspect a live Chrome instance.",
|
|
85
|
+
["npx", "-y", "chrome-devtools-mcp@latest"], "none", True,
|
|
86
|
+
docs="https://github.com/ChromeDevTools/chrome-devtools-mcp"),
|
|
87
|
+
_stdio("playwright", "Playwright", "code & dev", "Drive a real browser: navigate, click, screenshot.",
|
|
88
|
+
["npx", "-y", "@playwright/mcp@latest"], "none", True,
|
|
89
|
+
docs="https://github.com/microsoft/playwright-mcp"),
|
|
90
|
+
_stdio("docker", "Docker MCP Gateway", "code & dev", "Run containerized MCP servers from Docker's catalog.",
|
|
91
|
+
["docker", "mcp", "gateway", "run"], "none", True,
|
|
92
|
+
docs="https://docs.docker.com/ai/mcp-catalog-and-toolkit/mcp-gateway/"),
|
|
93
|
+
_remote("hugging-face", "Hugging Face", "code & dev", "Search models, datasets, Spaces and papers.",
|
|
94
|
+
"https://huggingface.co/mcp", "oauth", True,
|
|
95
|
+
env=("HF_TOKEN",),
|
|
96
|
+
docs="https://huggingface.co/docs/hub/hf-mcp-server"),
|
|
97
|
+
|
|
98
|
+
# -- cloud & infra ----------------------------------------------------
|
|
99
|
+
_remote("cloudflare-docs", "Cloudflare Docs", "cloud & infra", "Search Cloudflare's documentation.",
|
|
100
|
+
"https://docs.mcp.cloudflare.com/mcp", "none", True,
|
|
101
|
+
docs="https://developers.cloudflare.com/agents/model-context-protocol/"),
|
|
102
|
+
_remote("cloudflare-bindings", "Cloudflare Bindings", "cloud & infra",
|
|
103
|
+
"Build Workers with storage, AI and compute bindings.",
|
|
104
|
+
"https://bindings.mcp.cloudflare.com/mcp", "oauth", True,
|
|
105
|
+
docs="https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/"),
|
|
106
|
+
_remote("cloudflare-observability", "Cloudflare Observability", "cloud & infra",
|
|
107
|
+
"Workers logs, traffic trends and analytics.",
|
|
108
|
+
"https://observability.mcp.cloudflare.com/sse", "oauth", True,
|
|
109
|
+
docs="https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/"),
|
|
110
|
+
_remote("vercel", "Vercel", "cloud & infra", "Deployments, projects and domains.",
|
|
111
|
+
"https://mcp.vercel.com", "oauth", True,
|
|
112
|
+
docs="https://vercel.com/docs/mcp/vercel-mcp"),
|
|
113
|
+
_remote("supabase", "Supabase", "cloud & infra", "Postgres, auth and storage projects.",
|
|
114
|
+
"https://mcp.supabase.com/mcp", "oauth", True,
|
|
115
|
+
docs="https://supabase.com/docs/guides/getting-started/mcp"),
|
|
116
|
+
_stdio("railway", "Railway", "cloud & infra", "Manage Railway projects, services and deploys.",
|
|
117
|
+
["railway", "mcp"], "none", True,
|
|
118
|
+
docs="https://docs.railway.com/cli/mcp"),
|
|
119
|
+
_remote("neon", "Neon", "cloud & infra", "Serverless Postgres: branches, SQL and docs.",
|
|
120
|
+
"https://mcp.neon.tech/mcp", "oauth", True,
|
|
121
|
+
docs="https://neon.com/docs/ai/neon-mcp-server"),
|
|
122
|
+
_stdio("aws-docs", "AWS Documentation", "cloud & infra", "Search and read AWS documentation.",
|
|
123
|
+
["uvx", "awslabs.aws-documentation-mcp-server@latest"], "none", True,
|
|
124
|
+
docs="https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server"),
|
|
125
|
+
|
|
126
|
+
# -- data ---------------------------------------------------------
|
|
127
|
+
_stdio("postgres", "Postgres", "data", "Query and inspect a Postgres database.",
|
|
128
|
+
["postgres-mcp"], "api_key", True,
|
|
129
|
+
env=("DATABASE_URL",),
|
|
130
|
+
docs="https://github.com/crystaldba/postgres-mcp"),
|
|
131
|
+
_stdio("redis", "Redis", "data", "Manage and search data in Redis.",
|
|
132
|
+
["uvx", "--from", "redis-mcp-server@latest", "redis-mcp-server"], "api_key", True,
|
|
133
|
+
env=("REDIS_URL",),
|
|
134
|
+
docs="https://redis.io/docs/latest/integrate/redis-mcp/install/"),
|
|
135
|
+
_stdio("bigquery", "BigQuery", "data",
|
|
136
|
+
"Inspect schemas and run queries (needs the toolbox binary + gcloud ADC).",
|
|
137
|
+
["toolbox", "--prebuilt", "bigquery"], "none", True,
|
|
138
|
+
docs="https://github.com/googleapis/mcp-toolbox"),
|
|
139
|
+
_stdio("airtable", "Airtable", "data", "Read and write Airtable bases, tables and records.",
|
|
140
|
+
["npx", "-y", "airtable-mcp-server"], "api_key", False,
|
|
141
|
+
env=("AIRTABLE_API_KEY",),
|
|
142
|
+
docs="https://www.npmjs.com/package/airtable-mcp-server"),
|
|
143
|
+
|
|
144
|
+
# -- observability --------------------------------------------------
|
|
145
|
+
_remote("sentry", "Sentry", "observability", "Errors, issues and traces.",
|
|
146
|
+
"https://mcp.sentry.dev/mcp", "oauth", True,
|
|
147
|
+
docs="https://docs.sentry.io/product/sentry-mcp/"),
|
|
148
|
+
_remote("posthog", "PostHog", "observability", "Product analytics, insights and feature flags.",
|
|
149
|
+
"https://mcp.posthog.com/mcp", "api_key", True,
|
|
150
|
+
env=("POSTHOG_API_KEY",),
|
|
151
|
+
docs="https://posthog.com/docs/model-context-protocol"),
|
|
152
|
+
_remote("datadog", "Datadog", "observability", "APM, logs, metrics, monitors and dashboards.",
|
|
153
|
+
None, "oauth", False,
|
|
154
|
+
docs="https://docs.datadoghq.com/mcp_server/setup/"),
|
|
155
|
+
_stdio("grafana", "Grafana", "observability", "Dashboards, alerts and datasources (via Docker).",
|
|
156
|
+
["docker", "run", "--rm", "-i", "-e", "GRAFANA_URL", "-e", "GRAFANA_SERVICE_ACCOUNT_TOKEN",
|
|
157
|
+
"grafana/mcp-grafana", "-t", "stdio"], "api_key", True,
|
|
158
|
+
env=("GRAFANA_URL", "GRAFANA_SERVICE_ACCOUNT_TOKEN"),
|
|
159
|
+
docs="https://grafana.com/docs/grafana/latest/developer-resources/mcp/set-up/"),
|
|
160
|
+
|
|
161
|
+
# -- work & pm --------------------------------------------------------
|
|
162
|
+
_remote("linear", "Linear", "work & pm", "Issues, projects and cycles.",
|
|
163
|
+
"https://mcp.linear.app/mcp", "oauth", True,
|
|
164
|
+
docs="https://linear.app/developers/mcp"),
|
|
165
|
+
_remote("notion", "Notion", "work & pm", "Pages, databases and search.",
|
|
166
|
+
"https://mcp.notion.com/mcp", "oauth", True,
|
|
167
|
+
docs="https://developers.notion.com/docs/get-started-with-mcp"),
|
|
168
|
+
_stdio("slack", "Slack", "work & pm", "Read and post to channels and threads.",
|
|
169
|
+
["npx", "-y", "@modelcontextprotocol/server-slack"], "api_key", True,
|
|
170
|
+
env=("SLACK_BOT_TOKEN", "SLACK_TEAM_ID"),
|
|
171
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/slack"),
|
|
172
|
+
_remote("atlassian", "Atlassian", "work & pm", "Jira, Confluence, Bitbucket and Compass.",
|
|
173
|
+
"https://mcp.atlassian.com/v1/sse", "oauth", True,
|
|
174
|
+
docs="https://support.atlassian.com/atlassian-rovo-mcp-server/docs/use-atlassian-rovo-mcp-server/"),
|
|
175
|
+
_remote("asana", "Asana", "work & pm", "Tasks, projects and workload across the Work Graph.",
|
|
176
|
+
"https://mcp.asana.com/v2/mcp", "oauth", True,
|
|
177
|
+
docs="https://developers.asana.com/docs/using-asanas-mcp-server"),
|
|
178
|
+
_remote("zapier", "Zapier", "work & pm", "Trigger actions across 8,000+ connected apps.",
|
|
179
|
+
None, "api_key", False,
|
|
180
|
+
docs="https://docs.zapier.com/mcp"),
|
|
181
|
+
_stdio("google-workspace", "Google Workspace", "work & pm", "Gmail, Calendar, Drive, Docs and Sheets.",
|
|
182
|
+
["uvx", "workspace-mcp"], "oauth", False,
|
|
183
|
+
env=("GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET"),
|
|
184
|
+
docs="https://github.com/taylorwilsdon/google_workspace_mcp"),
|
|
185
|
+
_remote("intercom", "Intercom", "work & pm", "Conversations, contacts and Help Center articles.",
|
|
186
|
+
"https://mcp.intercom.com/mcp", "oauth", True,
|
|
187
|
+
docs="https://developers.intercom.com/docs/guides/mcp"),
|
|
188
|
+
_remote("hubspot", "HubSpot", "work & pm", "CRM contacts, deals and marketing objects.",
|
|
189
|
+
"https://mcp.hubspot.com", "oauth", True,
|
|
190
|
+
docs="https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server"),
|
|
191
|
+
|
|
192
|
+
# -- payments & commerce ----------------------------------------------
|
|
193
|
+
_remote("stripe", "Stripe", "payments & commerce", "Payments, customers and subscriptions.",
|
|
194
|
+
"https://mcp.stripe.com", "api_key", True,
|
|
195
|
+
env=("STRIPE_API_KEY",),
|
|
196
|
+
docs="https://docs.stripe.com/mcp"),
|
|
197
|
+
_remote("paypal", "PayPal", "payments & commerce", "Invoices, payments, refunds and disputes.",
|
|
198
|
+
"https://mcp.paypal.com/mcp", "oauth", True,
|
|
199
|
+
docs="https://developer.paypal.com/ai-tools/mcp-server"),
|
|
200
|
+
_stdio("shopify", "Shopify Dev", "payments & commerce", "API docs, GraphQL schemas and code validation.",
|
|
201
|
+
["npx", "-y", "@shopify/dev-mcp@latest"], "none", True,
|
|
202
|
+
docs="https://shopify.dev/changelog/mcp-server-for-the-shopify-dev-assistant"),
|
|
203
|
+
|
|
204
|
+
# -- design -----------------------------------------------------------
|
|
205
|
+
_remote("figma", "Figma", "design", "Design files, components and dev-mode inspection.",
|
|
206
|
+
"https://mcp.figma.com/mcp", "oauth", False,
|
|
207
|
+
docs="https://www.figma.com/developers/mcp"),
|
|
208
|
+
|
|
209
|
+
# -- utilities --------------------------------------------------------
|
|
210
|
+
_stdio("filesystem", "Filesystem", "utilities", "Read and write files outside the working directory.",
|
|
211
|
+
["npx", "-y", "@modelcontextprotocol/server-filesystem", "<cwd>"], "none", True,
|
|
212
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem"),
|
|
213
|
+
_stdio("memory", "Memory", "utilities", "A knowledge-graph scratchpad for a session.",
|
|
214
|
+
["npx", "-y", "@modelcontextprotocol/server-memory"], "none", True,
|
|
215
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/memory"),
|
|
216
|
+
_stdio("sequential-thinking", "Sequential Thinking", "utilities", "Step-by-step reflective problem solving.",
|
|
217
|
+
["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"], "none", True,
|
|
218
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking"),
|
|
219
|
+
_stdio("time", "Time", "utilities", "Timezone lookups and conversions.",
|
|
220
|
+
["uvx", "mcp-server-time"], "none", True,
|
|
221
|
+
docs="https://github.com/modelcontextprotocol/servers/tree/main/src/time"),
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _sanitized(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
227
|
+
"""Never surface secret values -- only that an env var or header is set."""
|
|
228
|
+
out: dict[str, Any] = {}
|
|
229
|
+
if "command" in cfg:
|
|
230
|
+
out["transport"] = "stdio"
|
|
231
|
+
out["command"] = cfg["command"]
|
|
232
|
+
# A manually-configured remote proxy stores its token inline
|
|
233
|
+
# (`--header Authorization:Bearer ...`), not in `env` -- redact it too.
|
|
234
|
+
args = list(cfg.get("args", []))
|
|
235
|
+
for idx, a in enumerate(args):
|
|
236
|
+
if a in ("--header", "-H") and idx + 1 < len(args):
|
|
237
|
+
args[idx + 1] = "***"
|
|
238
|
+
out["args"] = args
|
|
239
|
+
else:
|
|
240
|
+
out["transport"] = "remote"
|
|
241
|
+
out["url"] = cfg.get("url") or cfg.get("serverUrl") or ""
|
|
242
|
+
if cfg.get("env"):
|
|
243
|
+
out["env"] = sorted(cfg["env"])
|
|
244
|
+
if cfg.get("headers"):
|
|
245
|
+
out["headers"] = sorted(cfg["headers"])
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def imported_from_claude_code() -> dict[str, dict[str, Any]]:
|
|
250
|
+
"""Servers omega's own mcp.discover() picked up from ~/.claude.json and
|
|
251
|
+
installed plugins, formatted for display -- names and shapes only, no
|
|
252
|
+
tokens or header values."""
|
|
253
|
+
return {name: _sanitized(cfg) for name, cfg in mcp.discover(include_omega=False).items()}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def overview() -> list[dict[str, Any]]:
|
|
257
|
+
"""One row per server: what omega has configured, what the catalog offers
|
|
258
|
+
that isn't configured yet, and what Claude Code has that isn't either --
|
|
259
|
+
the shared data behind `omega connections` and the setup page's Tools step."""
|
|
260
|
+
configured = mcp.status()
|
|
261
|
+
raw = config.mcp_config()
|
|
262
|
+
rows: list[dict[str, Any]] = []
|
|
263
|
+
for name, st in sorted(configured.items()):
|
|
264
|
+
cat = CATALOG.get(raw.get(name, {}).get("catalog", ""))
|
|
265
|
+
rows.append({
|
|
266
|
+
"name": name, "state": st.state, "tools": st.tools,
|
|
267
|
+
"auth": cat.auth if cat else None, "source": "config",
|
|
268
|
+
"last_used": st.last_used, "category": cat.category if cat else None,
|
|
269
|
+
"verified": cat.verified if cat else None, "error": st.error,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
known = set(configured)
|
|
273
|
+
claude_only = {n for n in imported_from_claude_code() if n not in known}
|
|
274
|
+
for key, i in sorted(CATALOG.items()):
|
|
275
|
+
if key in known or key in claude_only:
|
|
276
|
+
continue
|
|
277
|
+
rows.append({"name": key, "state": "available", "tools": 0, "auth": i.auth,
|
|
278
|
+
"source": "catalog", "last_used": None, "category": i.category,
|
|
279
|
+
"verified": i.verified, "error": None})
|
|
280
|
+
for name in sorted(claude_only):
|
|
281
|
+
rows.append({"name": name, "state": "found", "tools": 0, "auth": None,
|
|
282
|
+
"source": "claude-code", "last_used": None, "category": None,
|
|
283
|
+
"verified": None, "error": None})
|
|
284
|
+
return rows
|
omega/keys.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""`omega keys` -- inspect and move provider API keys.
|
|
2
|
+
|
|
3
|
+
The interesting command is `migrate`: it lifts every plaintext `apiKey` out
|
|
4
|
+
of config.json into the OS keychain and rewrites the file without them, so a
|
|
5
|
+
config that has accumulated keys over months can stop being a liability in
|
|
6
|
+
one step instead of by hand-editing JSON around live credentials.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import getpass
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from . import config, secrets
|
|
18
|
+
|
|
19
|
+
USAGE = """usage:
|
|
20
|
+
omega keys where each provider's key comes from
|
|
21
|
+
omega keys set <provider> store a key in the OS keychain (prompted, not echoed)
|
|
22
|
+
omega keys rm <provider> delete a key from the OS keychain
|
|
23
|
+
omega keys migrate [--dry-run] move plaintext keys out of config.json into the keychain
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _providers() -> dict[str, dict[str, Any]]:
|
|
28
|
+
raw = config._json_or_default()
|
|
29
|
+
providers = raw.get("providers")
|
|
30
|
+
return dict(providers) if isinstance(providers, dict) else {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_providers(providers: dict[str, dict[str, Any]]) -> None:
|
|
34
|
+
"""Same merge-and-rewrite pattern as `mcp._write_mcp`: touch the
|
|
35
|
+
"providers" key and leave every other setting exactly as it was."""
|
|
36
|
+
raw = config._json_or_default()
|
|
37
|
+
raw["providers"] = providers
|
|
38
|
+
config.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
config.CONFIG_PATH.write_text(json.dumps(raw, indent=2) + "\n")
|
|
40
|
+
config.CONFIG_PATH.chmod(0o600)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _list() -> int:
|
|
44
|
+
providers = _providers()
|
|
45
|
+
if not providers:
|
|
46
|
+
print(f"no providers configured in {config.CONFIG_PATH}")
|
|
47
|
+
return 0
|
|
48
|
+
width = max(len(n) for n in providers)
|
|
49
|
+
exposed = []
|
|
50
|
+
for name, p in sorted(providers.items()):
|
|
51
|
+
source = secrets.describe_source(p.get("apiKeyEnv", ""), p.get("apiKeyCmd", ""),
|
|
52
|
+
p.get("apiKey", ""), name)
|
|
53
|
+
print(f" {name:<{width}} {source}")
|
|
54
|
+
if p.get("apiKey"):
|
|
55
|
+
exposed.append(name)
|
|
56
|
+
if exposed:
|
|
57
|
+
print(f"\n{len(exposed)} key(s) stored in plaintext in {config.CONFIG_PATH}.")
|
|
58
|
+
print(" run `omega keys migrate` to move them into the keychain")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _set(name: str) -> int:
|
|
63
|
+
if not secrets.keychain_available():
|
|
64
|
+
print("omega: no OS keychain on this platform -- use apiKeyEnv or "
|
|
65
|
+
"apiKeyCmd instead", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
secret = getpass.getpass(f"API key for {name!r} (not echoed): ").strip()
|
|
68
|
+
if not secret:
|
|
69
|
+
print("omega: nothing entered", file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
if not secrets.looks_like_key(secret):
|
|
72
|
+
# A warning, not a refusal: self-hosted and enterprise gateways issue
|
|
73
|
+
# keys in shapes this has never seen.
|
|
74
|
+
print("note: that doesn't look like a usual provider key -- storing it anyway")
|
|
75
|
+
secrets.keychain_set(name, secret)
|
|
76
|
+
print(f"stored {name!r} in the {secrets.SERVICE!r} keychain service")
|
|
77
|
+
|
|
78
|
+
providers = _providers()
|
|
79
|
+
if providers.get(name, {}).get("apiKey"):
|
|
80
|
+
print(f"warning: {config.CONFIG_PATH} still holds a plaintext apiKey for "
|
|
81
|
+
f"{name!r}, which takes precedence.\n"
|
|
82
|
+
f" run `omega keys migrate` to remove it")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _rm(name: str) -> int:
|
|
87
|
+
if secrets.keychain_delete(name):
|
|
88
|
+
print(f"deleted {name!r} from the keychain")
|
|
89
|
+
return 0
|
|
90
|
+
print(f"omega: no keychain entry for {name!r}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _backup_path() -> Path:
|
|
95
|
+
return config.CONFIG_PATH.with_suffix(config.CONFIG_PATH.suffix + ".pre-migrate")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _migrate(dry_run: bool) -> int:
|
|
99
|
+
if not secrets.keychain_available():
|
|
100
|
+
print("omega: no OS keychain on this platform -- move keys to apiKeyEnv "
|
|
101
|
+
"or apiKeyCmd by hand", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
providers = _providers()
|
|
104
|
+
plaintext = {n: p["apiKey"] for n, p in providers.items() if p.get("apiKey")}
|
|
105
|
+
if not plaintext:
|
|
106
|
+
print(f"no plaintext keys in {config.CONFIG_PATH} -- nothing to do")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
print(f"{len(plaintext)} plaintext key(s) in {config.CONFIG_PATH}:")
|
|
110
|
+
for name in sorted(plaintext):
|
|
111
|
+
print(f" {name} -> keychain ({secrets.SERVICE}/{name})")
|
|
112
|
+
if dry_run:
|
|
113
|
+
print("\n--dry-run: nothing written")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
for name, secret in plaintext.items():
|
|
117
|
+
secrets.keychain_set(name, secret)
|
|
118
|
+
# Verify every key reads back BEFORE the file loses them -- a keychain
|
|
119
|
+
# write that silently failed would otherwise take the only copy with it.
|
|
120
|
+
unreadable = [n for n, s in plaintext.items() if secrets.keychain_get(n) != s]
|
|
121
|
+
if unreadable:
|
|
122
|
+
print(f"omega: keychain did not return {unreadable} as written; "
|
|
123
|
+
f"config.json left untouched", file=sys.stderr)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
backup = _backup_path()
|
|
127
|
+
shutil.copy2(config.CONFIG_PATH, backup)
|
|
128
|
+
backup.chmod(0o600)
|
|
129
|
+
for name in plaintext:
|
|
130
|
+
providers[name].pop("apiKey", None)
|
|
131
|
+
_write_providers(providers)
|
|
132
|
+
|
|
133
|
+
print(f"\nmoved {len(plaintext)} key(s) into the keychain and removed them "
|
|
134
|
+
f"from {config.CONFIG_PATH}")
|
|
135
|
+
print(f"a copy WITH the keys is at {backup} -- delete it once you have "
|
|
136
|
+
f"confirmed omega still runs:")
|
|
137
|
+
print(f" rm {backup}")
|
|
138
|
+
_warn_about_stale_copies()
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _warn_about_stale_copies() -> None:
|
|
143
|
+
"""Point at the other files that may still hold the old keys. Migrating
|
|
144
|
+
the live config is only half the job if a `.bak` beside it still has
|
|
145
|
+
them, which is exactly the file nobody thinks to check."""
|
|
146
|
+
stale = [p for p in config.CONFIG_PATH.parent.glob(f"{config.CONFIG_PATH.name}.*")
|
|
147
|
+
if p != _backup_path() and "apiKey" in _safe_read(p)]
|
|
148
|
+
if stale:
|
|
149
|
+
print("\nthese files still contain plaintext keys:")
|
|
150
|
+
for p in stale:
|
|
151
|
+
print(f" {p}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _safe_read(path: Path) -> str:
|
|
155
|
+
try:
|
|
156
|
+
return path.read_text()
|
|
157
|
+
except OSError:
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main(argv: list[str]) -> int:
|
|
162
|
+
if not argv:
|
|
163
|
+
return _list()
|
|
164
|
+
cmd, rest = argv[0], argv[1:]
|
|
165
|
+
if cmd == "migrate":
|
|
166
|
+
return _migrate(dry_run="--dry-run" in rest)
|
|
167
|
+
if cmd in ("set", "rm"):
|
|
168
|
+
if not rest:
|
|
169
|
+
print(f"omega: `keys {cmd}` needs a provider name\n\n{USAGE}", file=sys.stderr)
|
|
170
|
+
return 1
|
|
171
|
+
return _set(rest[0]) if cmd == "set" else _rm(rest[0])
|
|
172
|
+
print(USAGE, file=sys.stderr)
|
|
173
|
+
return 1
|