@sphyr/cli 2.0.0-beta.1

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 Sphyr
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 ADDED
@@ -0,0 +1,167 @@
1
+ <!-- generated-by: gsd-doc-writer -->
2
+
3
+ # @sphyr/cli
4
+
5
+ Sphyr platform CLI. Currently ships Agent Guard commands; additional product groups will be added as they launch.
6
+
7
+ Part of the [Sphyr Agent Guard monorepo](../../README.md).
8
+
9
+ ## Commands
10
+
11
+ ```
12
+ sphyr login Sign in to Sphyr (device flow — writes ~/.sphyr/config.json)
13
+ sphyr guard init Interactive setup wizard — writes IDE MCP configs
14
+ sphyr guard verify Connectivity check (delegates to @sphyr/sdk sphyr-mcp)
15
+ sphyr guard run STDIO↔HTTP bridge — used directly in IDE MCP config entries
16
+ ```
17
+
18
+ > **`sphyr guard verify`** delegates to the `sphyr-mcp` binary from `@sphyr/sdk` — it performs
19
+ > a real HMAC handshake and `agent_guard_up` round-trip, reports gateway reachability and
20
+ > per-library instrumentation status, and supports `--json` for CI output.
21
+
22
+ ## Installation
23
+
24
+ Designed to be used via `npx` without a permanent install:
25
+
26
+ ```bash
27
+ npx @sphyr/cli login
28
+ ```
29
+
30
+ To install globally:
31
+
32
+ ```bash
33
+ npm install -g @sphyr/cli
34
+ ```
35
+
36
+ ## Quick start — Claude Desktop setup
37
+
38
+ The recommended way to configure Claude Desktop is to run the interactive setup wizard, which writes the correct config automatically:
39
+
40
+ ```bash
41
+ npx @sphyr/cli guard init
42
+ ```
43
+
44
+ The wizard writes a config entry that routes through the `sphyr-mcp` command from `@sphyr/sdk`, which signs every outbound request with HMAC. The resulting entry looks like:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "sphyr": {
50
+ "command": "npx",
51
+ "args": ["-y", "-p", "@sphyr/sdk", "sphyr-mcp"],
52
+ "env": {
53
+ "SPHYR_CREDENTIAL": "your-credential"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ `SPHYR_DOWNSTREAM` is optional — add a JSON array of MCP servers to also wrap and guard them
61
+ (tool-poisoning + tool-argument-entropy blocking). The old standalone `sphyr-proxy` binary has been
62
+ **removed**; `sphyr-mcp` replaces it (`init` rewrites any legacy `sphyr-proxy` and `sphyr-guard` IDE entries automatically).
63
+
64
+ > **Note:** The `init` wizard configures the unified SDK binary (`sphyr-mcp` from `@sphyr/sdk`). The `sphyr guard run` command in *this* CLI package is a simpler STDIO↔HTTP bridge intended for development and advanced manual setups.
65
+
66
+ ## Sign in for SDK use (`login`)
67
+
68
+ If you are using the Python or TypeScript SDK directly (not just the IDE MCP integration), run:
69
+
70
+ ```bash
71
+ npx @sphyr/cli login
72
+ ```
73
+
74
+ This runs the device flow and writes your credential to `~/.sphyr/config.json` (owner-only `0600`).
75
+ The SDKs then auto-load it — `auto_instrument()` / `autoInstrument()` work with no arguments and no
76
+ manual key copy-paste. Credential precedence: explicit args → `SPHYR_*` env vars → `~/.sphyr/config.json`.
77
+
78
+ ## Headless environments (SSH, containers, CI)
79
+
80
+ If you are running in a terminal without a browser — SSH session, Docker container, GitHub Codespaces, or a CI environment — use the device flow:
81
+
82
+ ```bash
83
+ npx @sphyr/cli guard init
84
+ ```
85
+
86
+ The wizard will print a short authorization code (e.g. `J3K9-M7PQ`) and a URL:
87
+
88
+ ```
89
+ ◆ Authorization code: J3K9-M7PQ
90
+ → Open: https://console.sphyr.io/device
91
+ ```
92
+
93
+ Open the URL on any device, log in with GitHub if prompted, enter the code, and click **Authorize**. The CLI will detect the approval within 5 seconds and write your IDE configs automatically.
94
+
95
+ `sphyr guard init` also auto-detects headless environments: if `stdin` is not a TTY (e.g., piped input or a non-interactive shell), the device flow starts automatically.
96
+
97
+ ### Behind a corporate NAT or VPN
98
+
99
+ If many users on your network run `sphyr guard init` from a shared egress IP (corporate VPN, NAT'd office network, CI fleet, GitHub Codespaces region), the API may transiently rate-limit polling requests from that IP and respond with `HTTP 429 Too Many Requests` and a `Retry-After` header. The CLI handles this automatically: it sleeps for the server-specified duration and resumes polling without surfacing an error.
100
+
101
+ ## Environment variables
102
+
103
+ | Variable | Required | Default | Description |
104
+ | --------------------- | ------------------------ | -------------------------- | --------------------------------------------------------------------- |
105
+ | `SPHYR_CREDENTIAL` | Yes (injected by `init`) | — | Single credential passed through to the Agent Guard HTTP endpoint |
106
+ | `AGENT_GUARD_MCP_URL` | No | `https://api.sphyr.io/mcp` | Override the upstream MCP endpoint (dev/staging only; must use HTTPS) |
107
+
108
+ The bridge enforces HTTPS for `AGENT_GUARD_MCP_URL` and will throw at startup if a non-HTTPS URL is provided.
109
+
110
+ ## Source structure
111
+
112
+ ```
113
+ packages/cli/
114
+ index.js # Entry point — two-level command dispatch
115
+ src/
116
+ commands/
117
+ guard.js # runGuard() — STDIO↔HTTP bridge
118
+ init.js # runInit() — interactive setup wizard
119
+ login.js # runLogin() — device-flow sign-in → ~/.sphyr/config.json
120
+ verify.js # runVerify() — delegates to @sphyr/sdk sphyr-mcp verify
121
+ lib/
122
+ ide-clients.js # IDE_CLIENTS registry, config read/merge/write helpers
123
+ url-guard.js # HTTPS/SSRF URL validation for AGENT_GUARD_MCP_URL
124
+ ```
125
+
126
+ ## Supported IDE clients
127
+
128
+ The `init` wizard detects and can configure:
129
+
130
+ - Claude Desktop
131
+ - Cursor
132
+ - VS Code (via Cline / Roo extension)
133
+ - Antigravity
134
+ - Zed
135
+ - Windsurf
136
+
137
+ ## Development
138
+
139
+ This package has no build step — it is plain ES module JavaScript targeting Node >= 22.
140
+
141
+ ```bash
142
+ # From the monorepo root
143
+ nvm use
144
+ npm ci
145
+
146
+ # Run the bridge against the live API
147
+ AGENT_GUARD_MCP_URL=https://api.sphyr.io/mcp node packages/cli/index.js guard run
148
+
149
+ # Run the init wizard
150
+ node packages/cli/index.js guard init
151
+
152
+ # Sign in
153
+ node packages/cli/index.js login
154
+ ```
155
+
156
+ ## Tests
157
+
158
+ Tests are not yet present in this package. The `init` wizard and bridge logic are covered by integration tests in the root `tests/` directory.
159
+
160
+ ## License
161
+
162
+ MIT
163
+
164
+ ## Prerequisites
165
+
166
+ - `Node.js >= 22.0.0 < 24.0.0` (enforced by the package `engines` field). Use [nvm](https://github.com/nvm-sh/nvm) and run `nvm use` from the monorepo root to switch to the correct version automatically.
167
+ - When invoking via `npx`, ensure the Node.js version in your shell meets this requirement — `npx` inherits the active Node version and will fail with an engines mismatch error on older releases.
package/index.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ // SPDX-License-Identifier: MIT
4
+ // Copyright (c) 2026 Sphyr
5
+
6
+ /**
7
+ * Sphyr platform CLI
8
+ *
9
+ * Top-level commands:
10
+ * login — platform-wide device-flow sign-in (cross-product)
11
+ * guard — Agent Guard subcommand group
12
+ *
13
+ * Agent Guard subcommands (sphyr guard <subcommand>):
14
+ * init — interactive setup wizard (configures IDE MCP configs)
15
+ * verify — connectivity check (delegates to @sphyr/sdk sphyr-mcp)
16
+ * run — STDIO↔HTTP bridge (used in IDE MCP config entries)
17
+ *
18
+ * Reserved (products not yet available):
19
+ * box — future product placeholder
20
+ *
21
+ * Usage in IDE MCP config:
22
+ * "command": "npx",
23
+ * "args": ["-y", "-p", "@sphyr/sdk@<version>", "sphyr-mcp"]
24
+ */
25
+
26
+ import { runGuard } from './src/commands/guard.js';
27
+ import { runInit } from './src/commands/init.js';
28
+ import { runLogin } from './src/commands/login.js';
29
+ import { runVerify } from './src/commands/verify.js';
30
+
31
+ const [, , topLevel, subCommand] = process.argv;
32
+
33
+ switch (topLevel) {
34
+ case 'login':
35
+ runLogin();
36
+ break;
37
+
38
+ case 'guard':
39
+ switch (subCommand) {
40
+ case 'init':
41
+ runInit();
42
+ break;
43
+ case 'verify':
44
+ runVerify();
45
+ break;
46
+ case 'run':
47
+ runGuard();
48
+ break;
49
+ case undefined:
50
+ process.stderr.write(
51
+ 'Usage: sphyr guard <subcommand>\n\n' +
52
+ 'Subcommands:\n' +
53
+ ' init Interactive setup wizard — configures IDE MCP configs\n' +
54
+ ' verify Check connectivity to Sphyr Agent Guard\n' +
55
+ ' run Start the STDIO↔HTTP MCP bridge\n',
56
+ );
57
+ process.exit(1);
58
+ break;
59
+ default:
60
+ process.stderr.write(
61
+ `Unknown subcommand: sphyr guard ${subCommand}\n` +
62
+ 'Run "sphyr guard" for available subcommands.\n',
63
+ );
64
+ process.exit(1);
65
+ }
66
+ break;
67
+
68
+ case 'box':
69
+ process.stderr.write('sphyr box: not yet available.\n');
70
+ process.exit(1);
71
+ break;
72
+
73
+ case undefined:
74
+ process.stderr.write(
75
+ 'Usage: sphyr <command>\n\n' +
76
+ 'Commands:\n' +
77
+ ' login Sign in to Sphyr (device flow)\n' +
78
+ ' guard Agent Guard commands (init, verify, run)\n',
79
+ );
80
+ process.exit(1);
81
+ break;
82
+
83
+ default:
84
+ process.stderr.write(
85
+ `Unknown command: sphyr ${topLevel}\n` +
86
+ 'Run "sphyr" for available commands.\n',
87
+ );
88
+ process.exit(1);
89
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@sphyr/cli",
3
+ "version": "2.0.0-beta.1",
4
+ "license": "MIT",
5
+ "description": "Sphyr platform CLI",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "bin": {
9
+ "sphyr": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "start": "node index.js"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^1.2.0",
25
+ "@sphyr/sdk": "^2.0.0-beta.1",
26
+ "open": "^11.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.0.0 <24.0.0"
30
+ }
31
+ }
@@ -0,0 +1,145 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Sphyr
3
+
4
+ /**
5
+ * sphyr guard — STDIO-to-HTTP MCP bridge
6
+ *
7
+ * Proxies JSON-RPC lines from stdin to the Sphyr Agent Guard HTTP endpoint
8
+ * and writes responses to stdout. Used as the command in IDE MCP config:
9
+ *
10
+ * "command": "npx",
11
+ * "args": ["-y", "@sphyr/guard", "guard"]
12
+ */
13
+
14
+ import readline from 'node:readline';
15
+ import { URL as NURL } from 'node:url';
16
+
17
+ const AGENT_GUARD_URL = process.env.AGENT_GUARD_MCP_URL || 'https://api.sphyr.io/mcp';
18
+
19
+ function safeUrl(raw) {
20
+ try {
21
+ const u = new NURL(raw);
22
+ u.username = '';
23
+ u.password = '';
24
+ return u.toString();
25
+ } catch {
26
+ return '<invalid url>';
27
+ }
28
+ }
29
+
30
+ if (!AGENT_GUARD_URL.startsWith('https://')) {
31
+ throw new Error(
32
+ `AGENT_GUARD_MCP_URL must use HTTPS. Refusing to proxy over insecure connection: ${safeUrl(AGENT_GUARD_URL)}`,
33
+ );
34
+ }
35
+
36
+ const AGENT_GUARD_SAFE_URL = safeUrl(AGENT_GUARD_URL);
37
+
38
+ /**
39
+ * Runs the STDIO-to-HTTP MCP bridge.
40
+ * Reads JSON-RPC lines from stdin, proxies each to the MCP HTTP endpoint,
41
+ * and writes the response to stdout as a single line.
42
+ */
43
+ export function runGuard() {
44
+ const rl = readline.createInterface({
45
+ input: process.stdin,
46
+ terminal: false,
47
+ });
48
+
49
+ rl.on('close', () => {
50
+ // stdin closed (IDE shut down or pipe ended) — event loop drains naturally
51
+ process.exit(0);
52
+ });
53
+
54
+ let inFlight = 0;
55
+ const MAX_CONCURRENT = 10;
56
+
57
+ rl.on('line', async (line) => {
58
+ const content = line.trim();
59
+ if (!content) return;
60
+
61
+ if (inFlight >= MAX_CONCURRENT) {
62
+ // Drop the request and emit a JSON-RPC error to avoid unbounded concurrency.
63
+ let requestId = null;
64
+ try {
65
+ requestId = JSON.parse(content).id ?? null;
66
+ } catch {
67
+ /* ignore */
68
+ }
69
+ const errMessage = JSON.stringify({
70
+ jsonrpc: '2.0',
71
+ error: { code: -32000, message: 'Agent Guard CLI Bridge Error: too many concurrent requests' },
72
+ id: requestId,
73
+ });
74
+ process.stdout.write(errMessage + '\n');
75
+ return;
76
+ }
77
+
78
+ inFlight = inFlight + 1;
79
+
80
+ try {
81
+ // Attempt to extract the request ID for well-formed error responses
82
+ let requestId = null;
83
+ try {
84
+ const parsed = JSON.parse(content);
85
+ requestId = parsed.id ?? null;
86
+ } catch {
87
+ // Ignore parse errors here, let the upstream gateway handle it
88
+ }
89
+
90
+ try {
91
+ const response = await fetch(AGENT_GUARD_URL, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ Accept: 'application/json',
96
+ },
97
+ body: content,
98
+ });
99
+
100
+ const body = await response.text();
101
+ if (!response.ok) {
102
+ // Non-2xx response — emit a proper JSON-RPC error instead of forwarding
103
+ // a raw HTML error page that would corrupt the JSON-RPC stream.
104
+ process.stderr.write(`[guard] upstream error ${response.status}: ${body.slice(0, 200)}\n`);
105
+ const errMessage = JSON.stringify({
106
+ jsonrpc: '2.0',
107
+ error: {
108
+ code: -32000,
109
+ message: `Agent Guard CLI Bridge Error: upstream returned ${response.status}`,
110
+ },
111
+ id: requestId,
112
+ });
113
+ process.stdout.write(errMessage + '\n');
114
+ return;
115
+ }
116
+ // Ensure the response is written to stdout as a single line
117
+ process.stdout.write(body.replace(/\r?\n/g, '') + '\n');
118
+ } catch (networkError) {
119
+ // Log raw error (may contain credentials) to stderr only — never stdout
120
+ process.stderr.write(`[guard] network error: ${networkError.message}\n`);
121
+ // Ensure STDIO JSON-RPC clients get a properly formatted message on failure
122
+ const errMessage = JSON.stringify({
123
+ jsonrpc: '2.0',
124
+ error: {
125
+ code: -32000,
126
+ message: `Agent Guard CLI Bridge Error: Could not reach gateway at ${AGENT_GUARD_SAFE_URL}`,
127
+ },
128
+ id: requestId,
129
+ });
130
+ process.stdout.write(errMessage + '\n');
131
+ }
132
+ } catch (fatalError) {
133
+ // Write the real error to stderr for operator visibility; never expose it to the agent.
134
+ process.stderr.write(`[agent-guard] fatal: ${fatalError?.message ?? fatalError}\n`);
135
+ const fatalMsg = JSON.stringify({
136
+ jsonrpc: '2.0',
137
+ error: { code: -32603, message: 'Internal agent guard error. Check stderr for details.' },
138
+ id: null,
139
+ });
140
+ process.stdout.write(fatalMsg + '\n');
141
+ } finally {
142
+ inFlight = inFlight - 1;
143
+ }
144
+ });
145
+ }