@weirgate/cli 0.0.0-stage → 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astraen Labs LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
- # Temporary Holding Version
1
+ # `@weirgate/cli`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Weirgate CLI and local stdio MCP server. Both are clients of the public Weirgate
4
+ management API (`https://api.weirgate.com`, `Weirgate-Api-Version: 2026-07-18`), with
5
+ the same scoped, proposal-first workflows as the hosted MCP server at
6
+ `https://mcp.weirgate.com/mcp`. Guide: https://weirgate.com/guides/cli/
7
+
8
+ ```sh
9
+ npm install -g @weirgate/cli
10
+ weirgate auth login # browser consent; stores the key in the macOS Keychain
11
+ weirgate auth status --output json
12
+ weirgate init --app example-app --output json
13
+ ```
14
+
15
+ `weirgate auth login --stdin` accepts an already-minted `wgk_` key. On any platform,
16
+ `WEIRGATE_API_KEY` overrides the keychain. `WEIRGATE_API_URL` (or `--api-url`) must be an
17
+ HTTPS URL.
18
+
19
+ The local MCP server runs over stdio and reads `WEIRGATE_API_KEY`:
20
+
21
+ ```sh
22
+ npx -y --package @weirgate/cli weirgate-mcp
23
+ ```
24
+
25
+ `weirgate mcp install --client codex --transport stdio` writes that command into the
26
+ Codex config; `--transport hosted` (the default) points Codex at the hosted server
27
+ instead.
28
+
29
+ Provider-key values are entered only through `weirgate provider-keys put|rotate` (a
30
+ local browser page by default, or hidden terminal input with `--stdin`) and never appear
31
+ in output. Exit codes are listed in the guide.
32
+
33
+ MIT licensed. Copyright 2026 Astraen Labs LLC.
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { createServer } from "node:http";
4
+ import { API_VERSION, WeirgateApiError } from "../shared/api-client.js";
5
+ const SUCCESS_PATH = "/cli/access-granted";
6
+ const FAILURE_PATH = "/cli/access-failed";
7
+ export async function browserSessionGrant(input, dependencies = {}) {
8
+ const { apiUrl, dashboardUrl } = requireHttpsUrls(input);
9
+ const fetchImpl = dependencies.fetchImpl ?? fetch;
10
+ const openExternal = dependencies.openExternal ?? openInBrowser;
11
+ const notify = dependencies.notify ?? ((message) => process.stderr.write(message));
12
+ const state = (dependencies.stateFactory ?? randomUUID)();
13
+ return new Promise((resolveGrant, rejectGrant) => {
14
+ let callbackInProgress = false;
15
+ let settled = false;
16
+ const server = createServer((request, response) => {
17
+ const callback = new URL(request.url ?? "/", "http://127.0.0.1");
18
+ const code = callback.searchParams.get("code");
19
+ const receivedState = callback.searchParams.get("state");
20
+ if (request.method !== "GET" || callback.pathname !== "/callback" || !code || receivedState !== state) {
21
+ respondText(response, 400, "Invalid Weirgate authorization callback.");
22
+ return;
23
+ }
24
+ if (callbackInProgress) {
25
+ respondText(response, 409, "Weirgate authorization is already being completed.");
26
+ return;
27
+ }
28
+ callbackInProgress = true;
29
+ void exchangeSessionGrantCode(apiUrl, code, fetchImpl).then((grant) => {
30
+ redirect(response, new URL(SUCCESS_PATH, dashboardUrl));
31
+ finish(() => resolveGrant(grant));
32
+ }).catch((error) => {
33
+ redirect(response, new URL(FAILURE_PATH, dashboardUrl));
34
+ finish(() => rejectGrant(error));
35
+ });
36
+ });
37
+ const finish = (settle) => {
38
+ if (settled)
39
+ return;
40
+ settled = true;
41
+ server.close();
42
+ settle();
43
+ };
44
+ server.once("error", (error) => finish(() => rejectGrant(error)));
45
+ server.listen(0, "127.0.0.1", () => {
46
+ const address = server.address();
47
+ if (!address || typeof address === "string") {
48
+ finish(() => rejectGrant(invalidInvocation("Could not start the local authorization callback.")));
49
+ return;
50
+ }
51
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
52
+ const consent = new URL("/consent", dashboardUrl);
53
+ consent.searchParams.set("client", "cli");
54
+ consent.searchParams.set("redirect_uri", redirectUri);
55
+ consent.searchParams.set("state", state);
56
+ consent.searchParams.set("scope", input.scope);
57
+ try {
58
+ openExternal(consent.toString());
59
+ notify("Opened Weirgate consent in your browser.\n");
60
+ }
61
+ catch (error) {
62
+ finish(() => rejectGrant(error));
63
+ }
64
+ });
65
+ });
66
+ }
67
+ async function exchangeSessionGrantCode(apiUrl, code, fetchImpl) {
68
+ const response = await fetchImpl(new URL("/v1/session-grants/exchange", apiUrl), {
69
+ method: "POST",
70
+ headers: { Accept: "application/json", "Content-Type": "application/json", "Weirgate-Api-Version": API_VERSION },
71
+ body: JSON.stringify({ code }),
72
+ });
73
+ const requestId = response.headers.get("X-Weirgate-Request-Id") ?? "unknown";
74
+ const apiVersion = response.headers.get("Weirgate-Api-Version") ?? "unknown";
75
+ const payload = await response.json().catch(() => ({}));
76
+ if (apiVersion !== API_VERSION) {
77
+ throw new WeirgateApiError({ message: `API version mismatch: expected ${API_VERSION}, received ${apiVersion}`, errorType: "api_version_mismatch", status: 502, requestId, apiVersion, detail: { expected: API_VERSION, received: apiVersion } });
78
+ }
79
+ if (!response.ok || typeof payload.value !== "string") {
80
+ throw new WeirgateApiError({ message: payload.error?.message ?? `Weirgate API returned HTTP ${response.status}`, errorType: response.headers.get("X-Weirgate-Error-Type") ?? payload.error?.type ?? "remote_error", status: response.status, requestId, apiVersion, ...(payload.error?.detail ? { detail: payload.error.detail } : {}) });
81
+ }
82
+ return payload.value;
83
+ }
84
+ function requireHttpsUrls(input) {
85
+ try {
86
+ const apiUrl = new URL(input.apiUrl);
87
+ const dashboardUrl = new URL(input.dashboardUrl);
88
+ if (apiUrl.protocol === "https:" && dashboardUrl.protocol === "https:")
89
+ return { apiUrl, dashboardUrl };
90
+ }
91
+ catch {
92
+ // Use the same stable invocation error for malformed and non-HTTPS URLs.
93
+ }
94
+ throw invalidInvocation("Browser grants require HTTPS API and dashboard URLs.");
95
+ }
96
+ function redirect(response, destination) {
97
+ response.writeHead(303, {
98
+ "cache-control": "no-store",
99
+ location: destination.toString(),
100
+ "referrer-policy": "no-referrer",
101
+ });
102
+ response.end();
103
+ }
104
+ function respondText(response, status, message) {
105
+ response.writeHead(status, {
106
+ "cache-control": "no-store",
107
+ "content-type": "text/plain; charset=utf-8",
108
+ "referrer-policy": "no-referrer",
109
+ });
110
+ response.end(message);
111
+ }
112
+ function openInBrowser(url) {
113
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
114
+ const args = process.platform === "win32" ? ["/c", "start", url] : [url];
115
+ spawn(opener, args, { detached: true, stdio: "ignore" }).unref();
116
+ }
117
+ function invalidInvocation(message) {
118
+ return new WeirgateApiError({ message, errorType: "invalid_invocation", status: 400, requestId: "local", apiVersion: API_VERSION });
119
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Stable CLI exit codes, so scripts can retry, ask a human, or stop. Explicit error types
3
+ * win; the HTTP status is the fallback for types this table doesn't name.
4
+ *
5
+ * 0 success · 2 invalid invocation · 3 invalid request or not found · 4 authentication or
6
+ * scope · 5 conflict or stale proposal · 6 blocked by policy or limits · 7 provider or
7
+ * upstream failure · 8 anything else
8
+ */
9
+ const EXIT_CODES_BY_TYPE = {
10
+ invalid_invocation: 2,
11
+ invalid_request: 3,
12
+ resource_not_found: 3,
13
+ feature_not_found: 3,
14
+ telemetry_request_unavailable: 3,
15
+ invalid_token: 4,
16
+ insufficient_scope: 4,
17
+ invalid_api_key: 4,
18
+ platform_key_rejected: 4,
19
+ resource_conflict: 5,
20
+ proposal_stale: 5,
21
+ provider_policy_blocked: 6,
22
+ abuse_blocked: 6,
23
+ feature_disabled: 6,
24
+ rate_limited: 6,
25
+ funding_rail_unavailable: 6,
26
+ provider_unavailable: 7,
27
+ output_contract_violation: 7,
28
+ };
29
+ export function exitCodeFor(errorType, status) {
30
+ const explicit = EXIT_CODES_BY_TYPE[errorType];
31
+ if (explicit !== undefined)
32
+ return explicit;
33
+ if (status === 400 || status === 404 || status === 422)
34
+ return 3;
35
+ if (status === 401 || status === 403)
36
+ return 4;
37
+ if (status === 409)
38
+ return 5;
39
+ if (status === 429)
40
+ return 6;
41
+ if (status >= 502)
42
+ return 7;
43
+ return 8;
44
+ }
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "node:http";
3
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, resolve } from "node:path";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { stdin, stdout } from "node:process";
8
+ import YAML from "yaml";
9
+ import { AgentApiClient, API_VERSION, DEFAULT_API_URL, pathPart, WeirgateApiError } from "../shared/api-client.js";
10
+ import { invokeTool } from "../shared/tools.js";
11
+ import { browserSessionGrant } from "./browser-session-grant.js";
12
+ import { exitCodeFor } from "./exit-codes.js";
13
+ let deferringRequiredFlags = false;
14
+ const parsed = parse(process.argv.slice(2));
15
+ try {
16
+ const result = await run(parsed);
17
+ if (result)
18
+ printSuccess(result, parsed.flags);
19
+ }
20
+ catch (error) {
21
+ const exitCode = printError(error, parsed.flags);
22
+ process.exitCode = exitCode;
23
+ }
24
+ function parse(argv) {
25
+ const words = [];
26
+ const flags = {};
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const value = argv[index];
29
+ if (!value.startsWith("--")) {
30
+ words.push(value);
31
+ continue;
32
+ }
33
+ const [rawKey, inline] = value.slice(2).split("=", 2);
34
+ const key = rawKey.replace(/^no-/, "");
35
+ if (rawKey.startsWith("no-")) {
36
+ flags[key] = false;
37
+ continue;
38
+ }
39
+ if (inline !== undefined) {
40
+ flags[key] = inline;
41
+ continue;
42
+ }
43
+ const next = argv[index + 1];
44
+ if (next && !next.startsWith("--")) {
45
+ flags[key] = next;
46
+ index += 1;
47
+ }
48
+ else
49
+ flags[key] = true;
50
+ }
51
+ return { words, flags };
52
+ }
53
+ async function run({ words, flags }) {
54
+ if (flags.help || words.length === 0) {
55
+ stdout.write(help());
56
+ return undefined;
57
+ }
58
+ const [noun, verb] = words;
59
+ if (noun === "completion") {
60
+ stdout.write(completion(verb ?? "bash"));
61
+ return undefined;
62
+ }
63
+ if (noun === "mcp" && verb === "install")
64
+ return installMcp(flags);
65
+ if (noun === "auth" && ["login", "status", "logout"].includes(verb ?? ""))
66
+ return authCommand(verb, flags);
67
+ const client = await clientFromEnvironment(flags);
68
+ if (noun === "init")
69
+ return init(client, flags);
70
+ if (noun === "provider-keys" && ["put", "rotate", "revoke"].includes(verb ?? ""))
71
+ return providerMutation(client, verb, flags);
72
+ if (noun === "config" && verb === "validate")
73
+ return validateConfig(flags);
74
+ const call = await commandToTool(client, noun, verb, flags);
75
+ return call;
76
+ }
77
+ async function clientFromEnvironment(flags) {
78
+ const key = process.env.WEIRGATE_API_KEY ?? keychainRead();
79
+ if (!key)
80
+ throw invocation("No key reference resolved. Set WEIRGATE_API_KEY or run `weirgate auth login`.");
81
+ return new AgentApiClient({ apiKey: key, baseUrl: stringFlag(flags, "api-url") ?? process.env.WEIRGATE_API_URL ?? DEFAULT_API_URL });
82
+ }
83
+ async function authCommand(verb, flags) {
84
+ if (verb === "logout") {
85
+ spawnSync("security", ["delete-generic-password", "-s", "weirgate-cli"], { stdio: "ignore" });
86
+ return localResult({ logged_in: false });
87
+ }
88
+ if (verb === "login") {
89
+ const apiUrl = stringFlag(flags, "api-url") ?? process.env.WEIRGATE_API_URL ?? DEFAULT_API_URL;
90
+ if (!process.env.WEIRGATE_API_KEY && !flags.stdin && flags.browser === false) {
91
+ throw invocation("Choose --browser or --stdin.");
92
+ }
93
+ const key = process.env.WEIRGATE_API_KEY
94
+ ?? (flags.stdin ? await readSecret(flags) : await browserSessionGrant({
95
+ apiUrl,
96
+ dashboardUrl: stringFlag(flags, "dashboard-url") ?? "https://dashboard.weirgate.com",
97
+ scope: stringFlag(flags, "scope") ?? "propose",
98
+ }));
99
+ const client = new AgentApiClient({ apiKey: key, baseUrl: apiUrl });
100
+ const identity = await client.whoami();
101
+ if (identity.data.actor.type === "platform_super_admin")
102
+ throw invocation("Platform super-admin credentials are rejected.");
103
+ const stored = spawnSync("security", ["add-generic-password", "-U", "-s", "weirgate-cli", "-a", "default", "-w", key], { stdio: "ignore" });
104
+ if (stored.status !== 0)
105
+ throw invocation("Unable to store key in macOS Keychain; use WEIRGATE_API_KEY instead.");
106
+ return identity;
107
+ }
108
+ const client = await clientFromEnvironment(flags);
109
+ return client.whoami();
110
+ }
111
+ function keychainRead() {
112
+ const result = spawnSync("security", ["find-generic-password", "-s", "weirgate-cli", "-a", "default", "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
113
+ return result.status === 0 ? result.stdout.trim() : undefined;
114
+ }
115
+ async function commandToTool(client, noun, verb, flags) {
116
+ const app_id = requiredContext(flags, "app");
117
+ const data = await dataFlag(flags);
118
+ const common = { ...data, app_id, idempotency_key: stringFlag(flags, "idempotency-key") };
119
+ deferringRequiredFlags = true;
120
+ const map = {
121
+ "apps:list": ["weirgate_list_apps", { limit: numberFlag(flags, "limit"), cursor: stringFlag(flags, "cursor") }],
122
+ "apps:get": ["weirgate_get_app", common],
123
+ "apps:propose": ["weirgate_propose_app", { app: data }],
124
+ "apps:auth": ["weirgate_propose_auth", { ...common, auth: data }],
125
+ "config:export": ["weirgate_export_config", {}],
126
+ "config:propose": ["__config_propose", data],
127
+ "config:apply": ["weirgate_apply_proposal", { proposal_id: requiredFlag(flags, "proposal"), expected_base_revision: numberFlag(flags, "expected-base-revision"), idempotency_key: requiredFlag(flags, "idempotency-key") }],
128
+ "auth-presets:list": ["__auth_presets", {}],
129
+ "features:list": ["weirgate_list_features", common],
130
+ "features:get": ["weirgate_get_feature", { ...common, feature_id: requiredFlag(flags, "feature") }],
131
+ "features:propose": ["weirgate_propose_feature", { ...common, feature_id: requiredFlag(flags, "feature"), feature: data }],
132
+ "tiers:list": ["__tiers_list", common],
133
+ "tiers:get": ["__tiers_get", { ...common, tier_id: requiredFlag(flags, "tier") }],
134
+ "tiers:propose": ["weirgate_propose_tier", { ...common, tier_id: requiredFlag(flags, "tier"), tier: data }],
135
+ "rules:get": ["weirgate_get_rules", common],
136
+ "rules:preview": ["weirgate_preview_rules", { ...common, ruleset: data, since: requiredFlag(flags, "since"), until: requiredFlag(flags, "until") }],
137
+ "rules:propose": ["weirgate_propose_rules", { ...common, ruleset: data, preview_id: requiredFlag(flags, "preview") }],
138
+ "overrides:propose": ["weirgate_propose_user_override", { ...common, external_id: requiredFlag(flags, "user"), override: data, since: requiredFlag(flags, "since"), until: requiredFlag(flags, "until") }],
139
+ "proposals:list": ["weirgate_list_proposals", { ...common, kind: stringFlag(flags, "kind"), status: stringFlag(flags, "status"), limit: numberFlag(flags, "limit") }],
140
+ "proposals:get": ["weirgate_get_proposal", { proposal_id: requiredFlag(flags, "proposal") }],
141
+ "proposals:cancel": ["weirgate_cancel_proposal", { proposal_id: requiredFlag(flags, "proposal") }],
142
+ "proposals:apply": ["weirgate_apply_proposal", { proposal_id: requiredFlag(flags, "proposal"), expected_base_revision: numberFlag(flags, "expected-base-revision"), idempotency_key: requiredFlag(flags, "idempotency-key") }],
143
+ "usage:query": ["weirgate_query_usage", { ...common, since: requiredFlag(flags, "since"), until: requiredFlag(flags, "until"), group_by: stringFlag(flags, "group-by"), limit: numberFlag(flags, "limit") }],
144
+ "pricing-insights:get": ["weirgate_get_pricing_insights", { ...common, since: requiredFlag(flags, "since"), until: requiredFlag(flags, "until") }],
145
+ "users:timeline": ["weirgate_get_user_timeline", { ...common, external_id: requiredFlag(flags, "user") }],
146
+ "events:list": ["weirgate_list_events", common],
147
+ "audit:list": ["weirgate_list_audit_events", { ...common, since: stringFlag(flags, "since"), until: stringFlag(flags, "until"), action: stringFlag(flags, "action") }],
148
+ "provider-keys:list": ["weirgate_list_provider_key_slots", common],
149
+ "provider-keys:test": ["weirgate_test_provider_key", { ...common, provider: requiredFlag(flags, "provider") }],
150
+ "grants:create": ["weirgate_grant_allowance", { ...common, external_id: requiredFlag(flags, "user"), units: numberRequired(flags, "units"), source: stringFlag(flags, "source") }],
151
+ "grants:reverse": ["weirgate_reverse_grant", { ...common, grant_id: requiredFlag(flags, "grant") }],
152
+ "user-tiers:assign": ["weirgate_assign_user_tier", { ...common, external_id: requiredFlag(flags, "user"), tier: requiredFlag(flags, "tier"), top_up_now: booleanFlag(flags, "top-up-now") }],
153
+ "user-tiers:revert": ["weirgate_revert_user_tier", { ...common, external_id: requiredFlag(flags, "user"), top_up_now: booleanFlag(flags, "top-up-now") }],
154
+ "doctor:all": ["weirgate_run_config_doctor", { ...common, checks: ["auth", "provider", "webhook", "payments"] }],
155
+ "doctor:auth": ["weirgate_run_config_doctor", { ...common, checks: ["auth"] }],
156
+ "doctor:provider": ["weirgate_run_config_doctor", { ...common, checks: ["provider"] }],
157
+ "doctor:webhook": ["weirgate_run_config_doctor", { ...common, checks: ["webhook"] }],
158
+ "doctor:payments": ["weirgate_run_config_doctor", { ...common, checks: ["payments"] }],
159
+ "sandbox:call": ["weirgate_run_mock_call", { ...common, fixture: stringFlag(flags, "fixture") ?? "success", session_name: stringFlag(flags, "session-name") }],
160
+ "docs:search": ["weirgate_search_docs", { query: requiredFlag(flags, "query"), section: stringFlag(flags, "section"), limit: numberFlag(flags, "limit") }],
161
+ "docs:get": ["weirgate_get_doc", { page_id: requiredFlag(flags, "page") }],
162
+ };
163
+ deferringRequiredFlags = false;
164
+ const entry = map[`${noun}:${verb}`];
165
+ if (!entry)
166
+ throw invocation(`Unknown command: ${noun} ${verb}`);
167
+ assertRequiredFlags(entry[1]);
168
+ if (flags["dry-run"] && ["proposals:apply", "config:apply", "grants:create", "grants:reverse", "user-tiers:assign", "user-tiers:revert"].includes(`${noun}:${verb}`)) {
169
+ if (noun === "proposals" || noun === "config")
170
+ return invokeTool(client, "weirgate_get_proposal", { proposal_id: entry[1].proposal_id });
171
+ return localResult({ dry_run: true, operation: `${noun}:${verb}`, input: redact(entry[1]) });
172
+ }
173
+ if (entry[0] === "__auth_presets")
174
+ return client.request("/v1/admin/auth-presets");
175
+ if (entry[0] === "__config_propose")
176
+ return client.request("/v1/admin/config/proposals", { method: "POST", body: entry[1] });
177
+ if (entry[0].startsWith("__tiers")) {
178
+ const result = await invokeTool(client, "weirgate_get_app", common);
179
+ const appData = result.data.app;
180
+ const tiers = appData?.tiers;
181
+ return { ...result, data: entry[0] === "__tiers_get" ? tiers?.[String(entry[1].tier_id)] : { tiers } };
182
+ }
183
+ return invokeTool(client, entry[0], entry[1]);
184
+ }
185
+ async function providerMutation(client, verb, flags) {
186
+ const provider = pathPart(requiredFlag(flags, "provider"));
187
+ if (verb === "revoke") {
188
+ if (flags["dry-run"])
189
+ return localResult({ dry_run: true, operation: "provider-key revoke", provider });
190
+ return client.request(`/v1/admin/provider-keys/${provider}`, { method: "DELETE", idempotencyKey: requiredFlag(flags, "idempotency-key") });
191
+ }
192
+ if (flags["dry-run"])
193
+ return localResult({ dry_run: true, operation: `provider-key ${verb}`, provider, key_value: "[omitted]" });
194
+ // Check every flag before asking the human for the secret, so a missing flag never wastes an entry.
195
+ const idempotencyKey = requiredFlag(flags, "idempotency-key");
196
+ const key = await readSecret(flags);
197
+ const path = `/v1/admin/provider-keys/${provider}${verb === "rotate" ? "/rotate" : ""}`;
198
+ const result = await client.request(path, { method: verb === "put" ? "PUT" : "POST", body: { value: key }, idempotencyKey });
199
+ return { ...result, data: redact(result.data) };
200
+ }
201
+ async function readSecret(flags) {
202
+ if (flags.stdin) {
203
+ if (!stdin.isTTY && !flags["allow-pipe"])
204
+ throw invocation("Refusing piped secret input without --allow-pipe.");
205
+ return readAllStdin();
206
+ }
207
+ if (flags.browser === false)
208
+ throw invocation("Choose --browser or --stdin. Secret values are never positional arguments.");
209
+ return browserSecretCeremony();
210
+ }
211
+ async function browserSecretCeremony() {
212
+ return new Promise((resolveSecret, reject) => {
213
+ const server = createServer((request, response) => {
214
+ if (request.method === "GET") {
215
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
216
+ response.end("<!doctype html><title>Weirgate local secret entry</title><form method=post><label>Scoped key or provider key <input name=key type=password autofocus required></label><button>Continue</button></form><p>The value is sent only to this localhost process.</p>");
217
+ return;
218
+ }
219
+ let body = "";
220
+ request.on("data", (chunk) => { body += String(chunk); });
221
+ request.on("end", () => {
222
+ const key = new URLSearchParams(body).get("key") ?? "";
223
+ response.writeHead(key ? 200 : 400, { "content-type": "text/plain", "cache-control": "no-store" });
224
+ response.end(key ? "Captured locally. You may close this tab." : "Missing value.");
225
+ server.close();
226
+ key ? resolveSecret(key) : reject(invocation("No secret entered."));
227
+ });
228
+ });
229
+ server.listen(0, "127.0.0.1", () => {
230
+ const address = server.address();
231
+ if (!address || typeof address === "string")
232
+ return reject(invocation("Could not start local secret entry."));
233
+ const url = `http://127.0.0.1:${address.port}`;
234
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
235
+ const args = process.platform === "win32" ? ["/c", "start", url] : [url];
236
+ spawn(opener, args, { detached: true, stdio: "ignore" }).unref();
237
+ process.stderr.write(`Opened one-time localhost secret entry: ${url}\n`);
238
+ });
239
+ });
240
+ }
241
+ async function installMcp(flags) {
242
+ const target = stringFlag(flags, "client") ?? "codex";
243
+ const transport = stringFlag(flags, "transport") ?? "hosted";
244
+ if (target !== "codex")
245
+ throw invocation("This build currently installs Codex config; use --client codex.");
246
+ const configPath = resolve(stringFlag(flags, "config") ?? `${homedir()}/.codex/config.toml`);
247
+ const config = await readFile(configPath, "utf8").catch(() => "");
248
+ if (/\[mcp_servers\.weirgate\]/.test(config))
249
+ return localResult({ installed: false, reason: "already_configured", path: configPath });
250
+ const block = transport === "stdio"
251
+ ? '\n[mcp_servers.weirgate]\ncommand = "npx"\nargs = ["-y", "--package", "@weirgate/cli", "weirgate-mcp"]\nenv_vars = ["WEIRGATE_API_KEY"]\n'
252
+ : '\n[mcp_servers.weirgate]\nurl = "https://mcp.weirgate.com/mcp"\nbearer_token_env_var = "WEIRGATE_API_KEY"\n';
253
+ await mkdir(dirname(configPath), { recursive: true });
254
+ await writeFile(configPath, `${config.replace(/\s*$/, "")}\n${block}`, { mode: 0o600 });
255
+ return localResult({ installed: true, path: configPath, transport, credential: { kind: "environment_reference", name: "WEIRGATE_API_KEY" } });
256
+ }
257
+ async function init(client, flags) {
258
+ const app = requiredFlag(flags, "app");
259
+ const identity = await client.whoami();
260
+ try {
261
+ const readiness = await invokeTool(client, "weirgate_get_onboarding_state", { app_id: app });
262
+ return { ...readiness, data: { app_id: app, readiness: readiness.data, next: "Resolve the first missing readiness item, enter provider material locally when requested, then run doctor and sandbox proof." } };
263
+ }
264
+ catch (error) {
265
+ if (!(error instanceof WeirgateApiError) || error.status !== 404)
266
+ throw error;
267
+ const candidate = await dataFlag(flags);
268
+ if (Object.keys(candidate).length === 0) {
269
+ return { ...identity, data: { app_id: app, readiness: { state: "app_not_proposed" }, next: "Re-run init with --file <app.json> to create a durable app proposal." } };
270
+ }
271
+ const appDocument = "app_id" in candidate ? candidate : candidate.app;
272
+ if (!appDocument || appDocument.app_id !== app)
273
+ throw invocation("The init document must describe the --app identifier.");
274
+ const proposal = await invokeTool(client, "weirgate_propose_app", { app: appDocument });
275
+ return { ...proposal, data: { app_id: app, proposal: proposal.data, next: "Review and apply this proposal with an apply-scoped test key, then re-run init." } };
276
+ }
277
+ }
278
+ async function validateConfig(flags) {
279
+ const data = await dataFlag(flags);
280
+ const valid = typeof data.tenant_id === "string" && Array.isArray(data.apps);
281
+ if (!valid)
282
+ throw new WeirgateApiError({ message: "Config must contain tenant_id and apps[]", errorType: "validation_error", status: 400, requestId: "local", apiVersion: API_VERSION });
283
+ return localResult({ valid: true });
284
+ }
285
+ async function dataFlag(flags) {
286
+ const file = stringFlag(flags, "file");
287
+ const raw = file ? await readFile(resolve(file), "utf8") : stringFlag(flags, "data");
288
+ if (!raw)
289
+ return {};
290
+ const value = file?.endsWith(".yaml") || file?.endsWith(".yml") ? YAML.parse(raw) : JSON.parse(raw);
291
+ if (!value || typeof value !== "object" || Array.isArray(value))
292
+ throw invocation("--data/--file must contain an object.");
293
+ return value;
294
+ }
295
+ function printSuccess(result, flags) {
296
+ const output = stringFlag(flags, "output") ?? (stdout.isTTY && !flags.json ? "table" : "json");
297
+ const envelope = { ok: true, data: result.data, request_id: result.request_id, api_version: result.api_version };
298
+ if (output === "yaml")
299
+ stdout.write(`${YAML.stringify(envelope)}`);
300
+ else if (output === "table")
301
+ stdout.write(`${human(result.data)}${flags["request-id"] ? `\nrequest_id: ${result.request_id}` : ""}\n`);
302
+ else
303
+ stdout.write(`${JSON.stringify(envelope)}\n`);
304
+ }
305
+ function printError(error, flags) {
306
+ const typed = error instanceof WeirgateApiError ? error : new WeirgateApiError({ message: error instanceof Error ? error.message : String(error), errorType: "internal", status: 500, requestId: "local", apiVersion: API_VERSION });
307
+ const envelope = { ok: false, error: { type: typed.errorType, message: typed.message, request_id: typed.requestId, ...(typed.detail ? { detail: typed.detail } : {}) }, api_version: typed.apiVersion };
308
+ if (flags.json || !process.stderr.isTTY)
309
+ process.stderr.write(`${JSON.stringify(envelope)}\n`);
310
+ else
311
+ process.stderr.write(`weirgate: ${typed.errorType}: ${typed.message}\n`);
312
+ return exitCodeFor(typed.errorType, typed.status);
313
+ }
314
+ function localResult(data) { return { data, request_id: "local", api_version: API_VERSION }; }
315
+ function invocation(message) { return new WeirgateApiError({ message, errorType: "invalid_invocation", status: 400, requestId: "local", apiVersion: API_VERSION }); }
316
+ function stringFlag(flags, name) { const value = flags[name]; return typeof value === "string" ? value : undefined; }
317
+ function requiredFlag(flags, name) { const value = stringFlag(flags, name); if (!value) {
318
+ if (deferringRequiredFlags)
319
+ return `\u0000required:${name}`;
320
+ throw invocation(`--${name} is required`);
321
+ } return value; }
322
+ function requiredContext(flags, name) { return stringFlag(flags, name); }
323
+ function numberFlag(flags, name) { const value = stringFlag(flags, name); return value === undefined ? undefined : Number(value); }
324
+ function numberRequired(flags, name) { const raw = stringFlag(flags, name); if (raw === undefined && deferringRequiredFlags)
325
+ return requiredFlag(flags, name); const value = raw === undefined ? undefined : Number(raw); if (!Number.isFinite(value))
326
+ throw invocation(`--${name} is required and must be a number`); return value; }
327
+ function booleanFlag(flags, name) {
328
+ const value = flags[name];
329
+ if (value === undefined || value === false || value === "false")
330
+ return false;
331
+ if (value === true || value === "true")
332
+ return true;
333
+ throw invocation(`--${name} must be true or false`);
334
+ }
335
+ function assertRequiredFlags(value) { if (typeof value === "string" && value.startsWith("\u0000required:"))
336
+ throw invocation(`--${value.slice(10)} is required`); if (Array.isArray(value))
337
+ value.forEach(assertRequiredFlags);
338
+ else if (value && typeof value === "object")
339
+ Object.values(value).forEach(assertRequiredFlags); }
340
+ function readAllStdin() { return new Promise((resolveInput, reject) => { let value = ""; stdin.setEncoding("utf8"); stdin.on("data", (chunk) => { value += chunk; }); stdin.on("end", () => resolveInput(value.trim())); stdin.on("error", reject); }); }
341
+ function human(data) { return typeof data === "string" ? data : YAML.stringify(data).trimEnd(); }
342
+ function redact(value) { if (Array.isArray(value))
343
+ return value.map(redact); if (!value || typeof value !== "object")
344
+ return value; return Object.fromEntries(Object.entries(value).map(([key, item]) => [/^(key|secret|token|password|credential|authorization)$/i.test(key) ? key : key, /^(key|secret|token|password|credential|authorization)$/i.test(key) ? "[redacted]" : redact(item)])); }
345
+ function help() {
346
+ return `weirgate <noun> <verb> [flags]\n\nAuth: auth login|status|logout, mcp install, init\nApps: apps list|get|propose, config export|validate|propose|apply, auth-presets list\nConfig: features/tiers/rules/overrides/proposals, doctor, sandbox\nOps: usage, pricing-insights, users, user-tiers, events, audit, provider-keys, grants, docs\nOutput: --output json|table|yaml; --json; mutations use --idempotency-key\nAuth login: browser consent by default; --stdin accepts a pre-minted key\nSecrets: provider-keys put|rotate defaults to --browser; --stdin rejects pipes without --allow-pipe\n`;
347
+ }
348
+ function completion(shell) {
349
+ const commands = "auth mcp init apps config auth-presets features tiers rules overrides proposals usage pricing-insights users user-tiers events audit provider-keys grants doctor sandbox docs completion";
350
+ if (shell === "zsh")
351
+ return `#compdef weirgate\n_arguments '1:command:(${commands})'\n`;
352
+ if (shell === "fish")
353
+ return commands.split(" ").map((c) => `complete -c weirgate -f -a '${c}'`).join("\n") + "\n";
354
+ return `complete -W '${commands}' weirgate\n`;
355
+ }
@@ -0,0 +1,86 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { AgentApiClient, API_VERSION, WeirgateApiError } from "../shared/api-client.js";
4
+ import { allowedTools, invokeTool, toolGroupAllowed, TOOL_DEFINITIONS } from "../shared/tools.js";
5
+ export async function authenticateAgent(apiKey, baseUrl) {
6
+ const client = new AgentApiClient({ apiKey, baseUrl });
7
+ const result = await client.whoami();
8
+ if (result.data.actor.type === "platform_super_admin") {
9
+ throw new WeirgateApiError({
10
+ message: "The platform super-admin credential is never accepted by agent surfaces",
11
+ errorType: "platform_key_rejected",
12
+ status: 403,
13
+ requestId: result.request_id,
14
+ apiVersion: result.api_version,
15
+ });
16
+ }
17
+ return { client, identity: result.data, identityRequestId: result.request_id };
18
+ }
19
+ const rank = { read: 0, propose: 1, apply: 2 };
20
+ export function createMcpServer(auth) {
21
+ const server = new Server({ name: "weirgate", version: API_VERSION }, {
22
+ capabilities: { tools: { listChanged: false } },
23
+ instructions: "Operate Weirgate only through the frozen 2026-07-18 public API. Propose before apply. Provider keys are entered outside the model-facing MCP surface.",
24
+ });
25
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
26
+ tools: allowedTools({ scope: auth.identity.scope, tool_groups: auth.identity.tool_groups }).map((tool) => ({
27
+ name: tool.name,
28
+ title: tool.title,
29
+ description: tool.description,
30
+ inputSchema: tool.inputSchema,
31
+ annotations: {
32
+ readOnlyHint: tool.scope === "read",
33
+ destructiveHint: false,
34
+ idempotentHint: tool.scope === "read" || [
35
+ "weirgate_apply_proposal",
36
+ "weirgate_grant_allowance",
37
+ "weirgate_reverse_grant",
38
+ "weirgate_assign_user_tier",
39
+ "weirgate_revert_user_tier",
40
+ ].includes(tool.name),
41
+ openWorldHint: false,
42
+ },
43
+ })),
44
+ }));
45
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
46
+ const definition = TOOL_DEFINITIONS.find((tool) => tool.name === request.params.name);
47
+ if (!definition)
48
+ return errorResult("invalid_request", `Unknown tool: ${request.params.name}`, auth.identityRequestId);
49
+ const groups = auth.identity.tool_groups ?? ["*"];
50
+ if (rank[auth.identity.scope] < rank[definition.scope] || !toolGroupAllowed(definition, groups)) {
51
+ return errorResult("insufficient_scope", `${definition.name} requires ${definition.scope} scope and ${definition.group} tool access`, auth.identityRequestId);
52
+ }
53
+ try {
54
+ const result = await invokeTool(auth.client, definition.name, request.params.arguments);
55
+ const safeResult = removeSecretValues(result);
56
+ return {
57
+ content: [{ type: "text", text: JSON.stringify(safeResult) }],
58
+ structuredContent: safeResult,
59
+ };
60
+ }
61
+ catch (error) {
62
+ if (error instanceof WeirgateApiError)
63
+ return errorResult(error.errorType, error.message, error.requestId, error.detail, error.apiVersion);
64
+ return errorResult("invalid_request", error instanceof Error ? error.message : String(error), "local");
65
+ }
66
+ });
67
+ return server;
68
+ }
69
+ function removeSecretValues(value) {
70
+ if (Array.isArray(value))
71
+ return value.map(removeSecretValues);
72
+ if (!value || typeof value !== "object")
73
+ return value;
74
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => /^(signing_secret|secret|token|password|credential|authorization|provider_key|key_value)$/i.test(key)
75
+ ? []
76
+ : [[key, removeSecretValues(item)]]));
77
+ }
78
+ /** Mirrors the HTTP error envelope (`error.type`, `message`, `request_id`, `detail`), plus `api_version`. */
79
+ function errorResult(type, message, requestId, detail, apiVersion = API_VERSION) {
80
+ const body = { error: { type, message, request_id: requestId, ...(detail === undefined ? {} : { detail }) }, api_version: apiVersion };
81
+ return {
82
+ isError: true,
83
+ content: [{ type: "text", text: JSON.stringify(body) }],
84
+ structuredContent: body,
85
+ };
86
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { authenticateAgent, createMcpServer } from "./server.js";
4
+ const apiKey = process.env.WEIRGATE_API_KEY;
5
+ if (!apiKey)
6
+ throw new Error("WEIRGATE_API_KEY must reference a minted scoped wgk_ key");
7
+ const auth = await authenticateAgent(apiKey, process.env.WEIRGATE_API_URL);
8
+ const server = createMcpServer(auth);
9
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,104 @@
1
+ export const API_VERSION = "2026-07-18";
2
+ export const DEFAULT_API_URL = "https://api.weirgate.com";
3
+ export class WeirgateApiError extends Error {
4
+ errorType;
5
+ status;
6
+ requestId;
7
+ apiVersion;
8
+ /** The API error's `detail` (for example `detail.reason`), as the HTTP envelope carries it. */
9
+ detail;
10
+ constructor(input) {
11
+ super(input.message);
12
+ this.name = "WeirgateApiError";
13
+ this.errorType = input.errorType;
14
+ this.status = input.status;
15
+ this.requestId = input.requestId;
16
+ this.apiVersion = input.apiVersion;
17
+ this.detail = input.detail;
18
+ }
19
+ }
20
+ export class AgentApiClient {
21
+ baseUrl;
22
+ apiKey;
23
+ constructor(input) {
24
+ if (!/^wgk_[a-f0-9]{64}$/.test(input.apiKey)) {
25
+ throw new WeirgateApiError({
26
+ message: "A minted wgk_ management key is required",
27
+ errorType: "invalid_api_key",
28
+ status: 401,
29
+ requestId: "local",
30
+ apiVersion: API_VERSION,
31
+ });
32
+ }
33
+ this.apiKey = input.apiKey;
34
+ this.baseUrl = (input.baseUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
35
+ if (new URL(this.baseUrl).protocol !== "https:") {
36
+ throw new WeirgateApiError({
37
+ message: "Agent surfaces require the public API over HTTPS",
38
+ errorType: "invalid_request",
39
+ status: 400,
40
+ requestId: "local",
41
+ apiVersion: API_VERSION,
42
+ });
43
+ }
44
+ }
45
+ async request(path, options = {}) {
46
+ const url = new URL(path, `${this.baseUrl}/`);
47
+ for (const [key, value] of Object.entries(options.query ?? {})) {
48
+ if (value !== undefined)
49
+ url.searchParams.set(key, String(value));
50
+ }
51
+ const headers = new Headers({
52
+ Accept: "application/json",
53
+ "X-Admin-Key": this.apiKey,
54
+ "Weirgate-Api-Version": API_VERSION,
55
+ });
56
+ if (options.body !== undefined)
57
+ headers.set("Content-Type", "application/json");
58
+ if (options.idempotencyKey)
59
+ headers.set("X-Idempotency-Key", options.idempotencyKey);
60
+ const response = await fetch(url, {
61
+ method: options.method ?? "GET",
62
+ headers,
63
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
64
+ });
65
+ const requestId = response.headers.get("X-Weirgate-Request-Id") ?? "unknown";
66
+ const apiVersion = response.headers.get("Weirgate-Api-Version") ?? "unknown";
67
+ let payload;
68
+ const contentType = response.headers.get("content-type") ?? "";
69
+ payload = contentType.includes("json") ? await response.json() : await response.text();
70
+ if (apiVersion !== API_VERSION) {
71
+ throw new WeirgateApiError({
72
+ message: `API version mismatch: expected ${API_VERSION}, received ${apiVersion}`,
73
+ errorType: "api_version_mismatch",
74
+ status: 502,
75
+ requestId,
76
+ apiVersion,
77
+ detail: { expected: API_VERSION, received: apiVersion },
78
+ });
79
+ }
80
+ if (!response.ok) {
81
+ const body = payload;
82
+ throw new WeirgateApiError({
83
+ message: body?.error?.message ?? `Weirgate API returned HTTP ${response.status}`,
84
+ errorType: response.headers.get("X-Weirgate-Error-Type") ?? body?.error?.type ?? "remote_error",
85
+ status: response.status,
86
+ requestId,
87
+ apiVersion,
88
+ ...(body?.error?.detail ? { detail: body.error.detail } : {}),
89
+ });
90
+ }
91
+ return { data: payload, request_id: requestId, api_version: API_VERSION };
92
+ }
93
+ whoami() {
94
+ return this.request("/v1/admin/whoami");
95
+ }
96
+ }
97
+ export function pathPart(value) {
98
+ if (typeof value !== "string" || value.length === 0)
99
+ throw new Error("missing required identifier");
100
+ return encodeURIComponent(value);
101
+ }
102
+ export function record(value) {
103
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
104
+ }
@@ -0,0 +1,194 @@
1
+ import { API_VERSION, pathPart, record, WeirgateApiError } from "./api-client.js";
2
+ const objectSchema = (properties = {}, required = []) => ({
3
+ type: "object",
4
+ additionalProperties: false,
5
+ properties,
6
+ ...(required.length ? { required } : {}),
7
+ });
8
+ const str = (description) => ({ type: "string", ...(description ? { description } : {}) });
9
+ const integer = () => ({ type: "integer" });
10
+ const document = () => ({ type: "object", additionalProperties: true, description: "Public API document; secret-valued fields are not accepted." });
11
+ const app = { app_id: str("Application identifier") };
12
+ const appRequired = ["app_id"];
13
+ export const TOOL_DEFINITIONS = [
14
+ { name: "weirgate_whoami", title: "Who am I", description: "Inspect the current tenant grant, scope, environment, and tool groups.", scope: "read", group: "identity", inputSchema: objectSchema() },
15
+ { name: "weirgate_list_apps", title: "List apps", description: "List apps visible to the current scoped key.", scope: "read", group: "setup", inputSchema: objectSchema({ cursor: str(), limit: integer() }) },
16
+ { name: "weirgate_get_app", title: "Get app", description: "Get one visible app configuration.", scope: "read", group: "setup", inputSchema: objectSchema(app, appRequired) },
17
+ { name: "weirgate_list_features", title: "List features", description: "List configured features for an app.", scope: "read", group: "setup", inputSchema: objectSchema(app, appRequired) },
18
+ { name: "weirgate_get_feature", title: "Get feature", description: "Get a configured feature.", scope: "read", group: "setup", inputSchema: objectSchema({ ...app, feature_id: str() }, ["app_id", "feature_id"]) },
19
+ { name: "weirgate_search_docs", title: "Search docs", description: "Search the authenticated canonical Markdown corpus.", scope: "read", group: "docs", inputSchema: objectSchema({ query: str(), section: str(), limit: integer() }, ["query"]) },
20
+ { name: "weirgate_get_doc", title: "Get doc", description: "Read one canonical Markdown document.", scope: "read", group: "docs", inputSchema: objectSchema({ page_id: str() }, ["page_id"]) },
21
+ { name: "weirgate_get_onboarding_state", title: "Get onboarding state", description: "Read durable onboarding readiness evidence.", scope: "read", group: "setup", inputSchema: objectSchema(app, appRequired) },
22
+ { name: "weirgate_run_config_doctor", title: "Run config doctor", description: "Run non-billable configuration checks.", scope: "read", group: "setup", inputSchema: objectSchema({ ...app, checks: { type: "array", items: { enum: ["auth", "provider", "webhook", "payments"] } } }, appRequired) },
23
+ { name: "weirgate_list_provider_key_slots", title: "List provider key slots", description: "List write-only provider slot metadata. No key values are returned.", scope: "read", group: "provider_keys", inputSchema: objectSchema(app) },
24
+ { name: "weirgate_test_provider_key", title: "Test provider key", description: "Run a non-billable metadata probe for an existing provider slot.", scope: "read", group: "provider_keys", inputSchema: objectSchema({ ...app, provider: str() }, ["app_id", "provider"]) },
25
+ { name: "weirgate_get_integration_snippet", title: "Get integration snippet", description: "Generate an HTTP/curl integration snippet.", scope: "read", group: "setup", inputSchema: objectSchema({ ...app, mode: { enum: ["http", "curl"] }, feature_id: str() }, appRequired) },
26
+ { name: "weirgate_run_mock_call", title: "Run mock call", description: "Run a deterministic sandbox fixture; never invokes live inference.", scope: "apply", group: "sandbox", inputSchema: objectSchema({ ...app, fixture: str(), session_name: str(), idempotency_key: str() }, ["app_id", "fixture"]) },
27
+ { name: "weirgate_query_usage", title: "Query usage", description: "Query windowed usage rollups.", scope: "read", group: "usage", inputSchema: objectSchema({ ...app, since: str(), until: str(), group_by: { enum: ["feature", "user"] }, limit: integer() }, ["app_id", "since", "until"]) },
28
+ { name: "weirgate_get_pricing_insights", title: "Get pricing insights", description: "Read closed-window pricing evidence.", scope: "read", group: "usage", inputSchema: objectSchema({ ...app, since: str(), until: str() }, ["app_id", "since", "until"]) },
29
+ { name: "weirgate_get_user_timeline", title: "Get user timeline", description: "Read one user's tenant-scoped operational timeline.", scope: "read", group: "usage", inputSchema: objectSchema({ ...app, external_id: str() }, ["app_id", "external_id"]) },
30
+ { name: "weirgate_list_events", title: "List events", description: "List webhook delivery events for an app.", scope: "read", group: "webhooks", inputSchema: objectSchema({ ...app, status: str(), cursor: str(), limit: integer() }, appRequired) },
31
+ { name: "weirgate_get_provider_health", title: "Get provider health", description: "Get configured provider health from non-secret slot metadata.", scope: "read", group: "provider_keys", inputSchema: objectSchema(app, appRequired) },
32
+ { name: "weirgate_list_audit_events", title: "List audit events", description: "List tenant-scoped management audit events.", scope: "read", group: "audit", inputSchema: objectSchema({ ...app, action: str(), since: str(), until: str(), cursor: str(), limit: integer() }) },
33
+ { name: "weirgate_export_config", title: "Export config", description: "Export the visible tenant configuration.", scope: "read", group: "setup", inputSchema: objectSchema() },
34
+ { name: "weirgate_propose_app", title: "Propose app", description: "Create a durable proposal to add or replace an app.", scope: "propose", group: "setup", inputSchema: objectSchema({ app: document() }, ["app"]) },
35
+ { name: "weirgate_propose_auth", title: "Propose auth", description: "Create a durable proposal to change app authentication.", scope: "propose", group: "setup", inputSchema: objectSchema({ ...app, auth: document() }, ["app_id", "auth"]) },
36
+ { name: "weirgate_propose_feature", title: "Propose feature", description: "Create a durable proposal to add or replace an app feature.", scope: "propose", group: "setup", inputSchema: objectSchema({ ...app, feature_id: str(), feature: document() }, ["app_id", "feature_id", "feature"]) },
37
+ { name: "weirgate_propose_tier", title: "Propose tier", description: "Create a durable proposal to add or replace an app tier.", scope: "propose", group: "setup", inputSchema: objectSchema({ ...app, tier_id: str(), tier: document() }, ["app_id", "tier_id", "tier"]) },
38
+ { name: "weirgate_propose_webhook", title: "Propose webhook", description: "Propose updating or deleting an existing webhook endpoint. Proposals cannot create endpoints: a new endpoint's signing secret is shown only once, so the developer creates it in the dashboard.", scope: "propose", group: "webhooks", inputSchema: objectSchema({ ...app, operation: { enum: ["update", "delete"] }, endpoint_id: str(), url: str(), event_types: { type: "array", items: str() }, enabled: { type: "boolean" }, base_revision: integer() }, ["app_id", "operation", "endpoint_id", "base_revision"]) },
39
+ { name: "weirgate_get_proposal", title: "Get proposal", description: "Read a config, rules, or webhook proposal.", scope: "read", group: "setup", inputSchema: objectSchema({ proposal_id: str() }, ["proposal_id"]) },
40
+ { name: "weirgate_list_proposals", title: "List proposals", description: "List visible durable proposals.", scope: "read", group: "setup", inputSchema: objectSchema({ ...app, kind: str(), status: str(), cursor: str(), limit: integer() }) },
41
+ { name: "weirgate_cancel_proposal", title: "Cancel proposal", description: "Cancel a pending proposal without applying it.", scope: "propose", group: "setup", inputSchema: objectSchema({ proposal_id: str() }, ["proposal_id"]) },
42
+ { name: "weirgate_apply_proposal", title: "Apply proposal", description: "Apply a reviewed proposal using base-revision CAS.", scope: "apply", group: "setup", inputSchema: objectSchema({ proposal_id: str(), expected_base_revision: integer(), idempotency_key: str() }, ["proposal_id", "idempotency_key"]) },
43
+ { name: "weirgate_get_rules", title: "Get rules", description: "Read the current revisioned app ruleset.", scope: "read", group: "rules", inputSchema: objectSchema(app, appRequired) },
44
+ { name: "weirgate_preview_rules", title: "Preview rules", description: "Preview rules against recent traffic and bind a content hash.", scope: "propose", group: "rules", inputSchema: objectSchema({ ...app, ruleset: document(), since: str(), until: str() }, ["app_id", "ruleset", "since", "until"]) },
45
+ { name: "weirgate_propose_rules", title: "Propose rules", description: "Create a preview-hash-bound rules proposal.", scope: "propose", group: "rules", inputSchema: objectSchema({ ...app, ruleset: document(), preview_id: str() }, ["app_id", "ruleset", "preview_id"]) },
46
+ { name: "weirgate_propose_user_override", title: "Propose user override", description: "Preview and propose a per-user velocity override.", scope: "propose", group: "rules", inputSchema: objectSchema({ ...app, external_id: str(), override: document(), since: str(), until: str() }, ["app_id", "external_id", "override"]) },
47
+ { name: "weirgate_grant_allowance", title: "Grant allowance", description: "Grant units idempotently to a user.", scope: "apply", group: "credits", inputSchema: objectSchema({ ...app, external_id: str(), units: { type: "number", exclusiveMinimum: 0 }, source: str(), idempotency_key: str() }, ["app_id", "external_id", "units", "idempotency_key"]) },
48
+ { name: "weirgate_reverse_grant", title: "Reverse grant", description: "Reverse an allowance grant idempotently.", scope: "apply", group: "credits", inputSchema: objectSchema({ ...app, grant_id: str(), idempotency_key: str() }, ["app_id", "grant_id", "idempotency_key"]) },
49
+ { name: "weirgate_assign_user_tier", title: "Assign user tier", description: "Schedule a configured user tier for the next monthly grant, with an optional current-period delta top-up.", scope: "apply", group: "plans", inputSchema: objectSchema({ ...app, external_id: str(), tier: str(), top_up_now: { type: "boolean" }, idempotency_key: str() }, ["app_id", "external_id", "tier", "idempotency_key"]) },
50
+ { name: "weirgate_revert_user_tier", title: "Revert user tier", description: "Schedule a user to return to the app default tier, with an optional current-period delta top-up.", scope: "apply", group: "plans", inputSchema: objectSchema({ ...app, external_id: str(), top_up_now: { type: "boolean" }, idempotency_key: str() }, ["app_id", "external_id", "idempotency_key"]) },
51
+ ];
52
+ const scopeRank = { read: 0, propose: 1, apply: 2 };
53
+ export function allowedTools(identity) {
54
+ const groups = identity.tool_groups ?? ["*"];
55
+ return TOOL_DEFINITIONS.filter((tool) => scopeRank[identity.scope] >= scopeRank[tool.scope] && toolGroupAllowed(tool, groups));
56
+ }
57
+ export function toolGroupAllowed(tool, groups) {
58
+ if (tool.group === "identity" || groups.includes("*") || groups.includes(tool.group))
59
+ return true;
60
+ return ["weirgate_get_proposal", "weirgate_list_proposals", "weirgate_cancel_proposal", "weirgate_apply_proposal"].includes(tool.name)
61
+ && groups.some((group) => ["setup", "rules", "webhooks"].includes(group));
62
+ }
63
+ function configTenant(data) {
64
+ const value = record(data);
65
+ return "tenant_id" in value ? value : record(value.tenants?.[0]);
66
+ }
67
+ function apps(data) {
68
+ return configTenant(data).apps ?? [];
69
+ }
70
+ function findApp(data, appId) {
71
+ const value = apps(data).find((item) => item.app_id === appId);
72
+ if (!value)
73
+ throw new Error(`App ${appId} is not visible`);
74
+ return value;
75
+ }
76
+ async function mutateConfig(client, change) {
77
+ const current = await client.request("/v1/admin/config");
78
+ const tenant = structuredClone(configTenant(current.data));
79
+ change(tenant);
80
+ return client.request("/v1/admin/config/proposals", { method: "POST", body: tenant });
81
+ }
82
+ const q = (input, keys) => Object.fromEntries(keys.map((key) => [key, input[key]]));
83
+ export async function invokeTool(client, name, rawInput) {
84
+ const input = record(rawInput);
85
+ const a = () => pathPart(input.app_id);
86
+ switch (name) {
87
+ case "weirgate_whoami": return client.whoami();
88
+ case "weirgate_list_apps": {
89
+ const r = await client.request("/v1/admin/config");
90
+ const limit = Math.max(1, Math.min(Number(input.limit ?? 100), 100));
91
+ const all = apps(r.data);
92
+ const start = input.cursor ? Math.max(0, all.findIndex((item) => item.app_id === input.cursor) + 1) : 0;
93
+ const page = all.slice(start, start + limit);
94
+ return { ...r, data: { data: page, pagination: { limit, returned: page.length, truncated: start + limit < all.length, next_cursor: start + limit < all.length ? page.at(-1)?.app_id ?? null : null } } };
95
+ }
96
+ case "weirgate_get_app": {
97
+ const r = await client.request("/v1/admin/config");
98
+ const readiness = await client.request(`/v1/admin/apps/${a()}/readiness`);
99
+ return { ...r, data: { app: findApp(r.data, String(input.app_id)), readiness: readiness.data } };
100
+ }
101
+ case "weirgate_list_features": {
102
+ const r = await client.request("/v1/admin/config");
103
+ return { ...r, data: { features: record(findApp(r.data, String(input.app_id)).features) } };
104
+ }
105
+ case "weirgate_get_feature": {
106
+ const r = await client.request("/v1/admin/config");
107
+ return { ...r, data: record(findApp(r.data, String(input.app_id)).features)[String(input.feature_id)] };
108
+ }
109
+ case "weirgate_search_docs": return client.request("/v1/admin/docs/search", { query: q(input, ["query", "section", "limit"]) });
110
+ case "weirgate_get_doc": return client.request(`/v1/admin/docs/${pathPart(input.page_id)}`);
111
+ case "weirgate_get_onboarding_state": return client.request(`/v1/admin/apps/${a()}/readiness`);
112
+ case "weirgate_run_config_doctor": return client.request(`/v1/admin/apps/${a()}/doctor`, { method: "POST", body: { checks: input.checks ?? ["auth", "provider", "webhook", "payments"] } });
113
+ case "weirgate_list_provider_key_slots": return client.request("/v1/admin/provider-keys", { query: q(input, ["app_id"]) });
114
+ case "weirgate_test_provider_key": return client.request(`/v1/admin/provider-keys/${pathPart(input.provider)}/health`, { method: "POST", query: { app_id: String(input.app_id) } });
115
+ case "weirgate_get_integration_snippet": return client.request(`/v1/admin/apps/${a()}/integration-snippet`, { query: q(input, ["mode", "feature_id"]) });
116
+ case "weirgate_run_mock_call": {
117
+ const session = await client.request(`/v1/admin/apps/${a()}/sandbox/sessions`, { method: "POST", body: { name: input.session_name ?? "agent mock call" }, idempotencyKey: input.idempotency_key });
118
+ return client.request(`/v1/admin/apps/${a()}/sandbox/sessions/${pathPart(session.data.id)}/runs`, { method: "POST", body: { fixture: input.fixture } });
119
+ }
120
+ case "weirgate_query_usage": return client.request(`/v1/admin/apps/${a()}/usage`, { query: q(input, ["since", "until", "group_by", "limit"]) });
121
+ case "weirgate_get_pricing_insights": return client.request(`/v1/admin/apps/${a()}/pricing-insights`, { query: q(input, ["since", "until"]) });
122
+ case "weirgate_get_user_timeline": return client.request(`/v1/admin/apps/${a()}/users/${pathPart(input.external_id)}`);
123
+ case "weirgate_list_events": return client.request(`/v1/admin/apps/${a()}/webhooks/deliveries`, { query: q(input, ["status", "cursor", "limit"]) });
124
+ case "weirgate_get_provider_health": {
125
+ const config = await client.request("/v1/admin/config");
126
+ const slots = await client.request("/v1/admin/provider-keys", { query: { app_id: String(input.app_id) } });
127
+ const target = findApp(config.data, String(input.app_id));
128
+ return { ...config, data: { providers: slots.data, effective_feature_policy: Object.fromEntries(Object.entries(record(target.features)).map(([id, feature]) => [id, record(feature).provider_policy])) } };
129
+ }
130
+ case "weirgate_list_audit_events": return client.request("/v1/admin/audit", { query: q(input, ["app_id", "action", "since", "until", "cursor", "limit"]) });
131
+ case "weirgate_export_config": return client.request("/v1/admin/config");
132
+ case "weirgate_propose_app": return mutateConfig(client, (tenant) => { const list = tenant.apps ?? []; const next = record(input.app); tenant.apps = [...list.filter((x) => x.app_id !== next.app_id), next]; });
133
+ case "weirgate_propose_auth": return mutateConfig(client, (tenant) => { findApp(tenant, String(input.app_id)).auth = input.auth; });
134
+ case "weirgate_propose_feature": return mutateConfig(client, (tenant) => { const target = findApp(tenant, String(input.app_id)); target.features = { ...record(target.features), [String(input.feature_id)]: input.feature }; });
135
+ case "weirgate_propose_tier": return mutateConfig(client, (tenant) => { const target = findApp(tenant, String(input.app_id)); target.tiers = { ...record(target.tiers), [String(input.tier_id)]: input.tier }; });
136
+ case "weirgate_propose_webhook": return client.request(`/v1/admin/apps/${a()}/webhook-proposals`, { method: "POST", body: { operation: input.operation, endpoint_id: input.endpoint_id, url: input.url, event_types: input.event_types, enabled: input.enabled, base_revision: input.base_revision } });
137
+ case "weirgate_get_proposal": return getProposal(client, pathPart(input.proposal_id));
138
+ case "weirgate_list_proposals": {
139
+ const config = await optionalScopeRequest(client, "/v1/admin/config/proposals", q(input, ["app_id", "kind", "status", "cursor", "limit"]));
140
+ const webhooks = await optionalScopeRequest(client, "/v1/admin/webhook-proposals", q(input, ["app_id", "status", "cursor", "limit"]));
141
+ return { ...config, data: { config: config.data, webhooks: webhooks.data } };
142
+ }
143
+ case "weirgate_cancel_proposal": return proposalAction(client, pathPart(input.proposal_id), "cancel", input);
144
+ case "weirgate_apply_proposal": return proposalAction(client, pathPart(input.proposal_id), "apply", input);
145
+ case "weirgate_get_rules": return client.request(`/v1/admin/apps/${a()}/rules`);
146
+ case "weirgate_preview_rules": return client.request(`/v1/admin/apps/${a()}/rules:preview`, { method: "POST", body: { ruleset: input.ruleset, since: input.since, until: input.until } });
147
+ case "weirgate_propose_rules": return client.request(`/v1/admin/apps/${a()}/rules/proposals`, { method: "POST", body: { ruleset: input.ruleset, preview_id: input.preview_id } });
148
+ case "weirgate_propose_user_override": {
149
+ const current = await client.request(`/v1/admin/apps/${a()}/rules`);
150
+ const ruleset = structuredClone(record(record(current.data).ruleset ?? current.data));
151
+ const velocity = record(ruleset.velocity);
152
+ ruleset.velocity = velocity;
153
+ velocity.user_overrides = { ...record(velocity.user_overrides), [String(input.external_id)]: input.override };
154
+ const preview = await client.request(`/v1/admin/apps/${a()}/rules:preview`, { method: "POST", body: { ruleset, since: input.since, until: input.until } });
155
+ return client.request(`/v1/admin/apps/${a()}/rules/proposals`, { method: "POST", body: { ruleset, preview_id: preview.data.preview_id } });
156
+ }
157
+ case "weirgate_grant_allowance": return client.request(`/v1/admin/apps/${a()}/users/${pathPart(input.external_id)}/grants`, { method: "POST", idempotencyKey: String(input.idempotency_key), body: { units: input.units, ...(input.source === undefined ? {} : { source: input.source }) } });
158
+ case "weirgate_reverse_grant": return client.request(`/v1/admin/apps/${a()}/grants/${pathPart(input.grant_id)}/reverse`, { method: "POST", idempotencyKey: String(input.idempotency_key) });
159
+ case "weirgate_assign_user_tier": return client.request(`/v1/admin/apps/${a()}/users/${pathPart(input.external_id)}/tier`, { method: "PUT", idempotencyKey: String(input.idempotency_key), body: { tier: input.tier, top_up_now: input.top_up_now ?? false } });
160
+ case "weirgate_revert_user_tier": return client.request(`/v1/admin/apps/${a()}/users/${pathPart(input.external_id)}/tier`, { method: "DELETE", idempotencyKey: String(input.idempotency_key), body: { top_up_now: input.top_up_now ?? false } });
161
+ default: throw new WeirgateApiError({ message: `Unknown tool: ${name}`, errorType: "invalid_request", status: 400, requestId: "local", apiVersion: API_VERSION });
162
+ }
163
+ }
164
+ async function optionalScopeRequest(client, path, query) {
165
+ try {
166
+ return await client.request(path, { query });
167
+ }
168
+ catch (error) {
169
+ if (!(error instanceof WeirgateApiError) || error.errorType !== "insufficient_scope")
170
+ throw error;
171
+ return { data: { data: [], pagination: { limit: 0, returned: 0, truncated: false, next_cursor: null } }, request_id: error.requestId, api_version: API_VERSION };
172
+ }
173
+ }
174
+ async function getProposal(client, proposalId) {
175
+ try {
176
+ return await client.request(`/v1/admin/config/proposals/${proposalId}`);
177
+ }
178
+ catch (error) {
179
+ if (!(error instanceof WeirgateApiError) || error.status !== 404)
180
+ throw error;
181
+ return client.request(`/v1/admin/webhook-proposals/${proposalId}`);
182
+ }
183
+ }
184
+ async function proposalAction(client, proposalId, action, input) {
185
+ const options = { method: "POST", body: action === "apply" ? { expected_base_revision: input.expected_base_revision } : undefined, idempotencyKey: input.idempotency_key };
186
+ try {
187
+ return await client.request(`/v1/admin/config/proposals/${proposalId}/${action}`, options);
188
+ }
189
+ catch (error) {
190
+ if (!(error instanceof WeirgateApiError) || error.status !== 404)
191
+ throw error;
192
+ return client.request(`/v1/admin/webhook-proposals/${proposalId}/${action}`, options);
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@weirgate/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Weirgate CLI and local stdio MCP server: scoped, proposal-first management of Weirgate apps.",
5
+ "license": "MIT",
6
+ "author": "Astraen Labs LLC",
7
+ "type": "module",
8
+ "homepage": "https://weirgate.com/guides/cli/",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "bin": {
13
+ "weirgate": "dist/cli/index.js",
14
+ "weirgate-mcp": "dist/mcp/stdio.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "yaml": "^2.9.0"
27
+ }
28
+ }