circuit-mcp 2.3.4 → 2.5.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 +21 -0
- package/README.md +10 -2
- package/bin/circuit-mcp.js +0 -0
- package/package.json +14 -6
- package/src/auth.js +61 -141
- package/src/index.js +47 -13
- package/src/pages.js +119 -0
- package/src/server.js +12 -7
- package/preview-error.html +0 -57
- package/preview-success.html +0 -57
- package/publish.sh +0 -30
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Circuit (withcircuit.com)
|
|
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
|
@@ -5,12 +5,20 @@ Connect Circuit to Cursor and Claude Code via the Model Context Protocol.
|
|
|
5
5
|
|
|
6
6
|
## Setup
|
|
7
7
|
|
|
8
|
+
Sign in first — this opens your browser and caches a token at `~/.circuit/token.json` (30-day expiry, auto-refresh):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx circuit-mcp auth
|
|
12
|
+
```
|
|
13
|
+
|
|
8
14
|
### Claude Code
|
|
9
15
|
|
|
10
16
|
```bash
|
|
11
|
-
claude mcp add circuit -- npx circuit-mcp
|
|
17
|
+
claude mcp add --scope user circuit -- npx circuit-mcp
|
|
12
18
|
```
|
|
13
19
|
|
|
20
|
+
`--scope user` makes Circuit available in every project, not just the directory you ran the command from.
|
|
21
|
+
|
|
14
22
|
### Cursor
|
|
15
23
|
|
|
16
24
|
Add to `~/.cursor/mcp.json`:
|
|
@@ -26,7 +34,7 @@ Add to `~/.cursor/mcp.json`:
|
|
|
26
34
|
}
|
|
27
35
|
```
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
If you add the server before signing in, it exits with a reminder to run `npx circuit-mcp auth` — sign in, then reconnect from your editor.
|
|
30
38
|
|
|
31
39
|
---
|
|
32
40
|
|
package/bin/circuit-mcp.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circuit-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Connect Circuit to Cursor and Claude Code - bring customer priorities and engineering briefs into your AI coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"circuit-mcp": "
|
|
7
|
+
"circuit-mcp": "bin/circuit-mcp.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./src/index.js",
|
|
10
|
+
"exports": "./src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"src/",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
10
17
|
"scripts": {
|
|
11
|
-
"start": "node bin/circuit-mcp.js"
|
|
18
|
+
"start": "node bin/circuit-mcp.js",
|
|
19
|
+
"test": "node --test"
|
|
12
20
|
},
|
|
13
21
|
"keywords": [
|
|
14
22
|
"circuit",
|
|
@@ -25,15 +33,15 @@
|
|
|
25
33
|
"customer-feedback"
|
|
26
34
|
],
|
|
27
35
|
"author": "Circuit <hello@withcircuit.com>",
|
|
28
|
-
"license": "
|
|
36
|
+
"license": "MIT",
|
|
29
37
|
"homepage": "https://withcircuit.com",
|
|
30
38
|
"repository": {
|
|
31
39
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "git+https://github.com/withcircuit/Circuit.git",
|
|
33
41
|
"directory": "circuit-mcp"
|
|
34
42
|
},
|
|
35
43
|
"bugs": {
|
|
36
|
-
"url": "https://github.com/
|
|
44
|
+
"url": "https://github.com/withcircuit/Circuit/issues"
|
|
37
45
|
},
|
|
38
46
|
"dependencies": {
|
|
39
47
|
"chalk": "^5.3.0",
|
package/src/auth.js
CHANGED
|
@@ -1,31 +1,65 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { URL } from 'url';
|
|
3
|
-
import { randomBytes } from 'crypto';
|
|
3
|
+
import { randomBytes, createHash } from 'crypto';
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import open from 'open';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { showSpinner, showInfo, showPrompt } from './ui.js';
|
|
10
|
+
import { getSuccessPage, getErrorPage } from './pages.js';
|
|
10
11
|
|
|
11
12
|
const CIRCUIT_URL = process.env.CIRCUIT_APP_URL || 'https://app.withcircuit.com';
|
|
12
13
|
const TOKEN_FILE = path.join(os.homedir(), '.circuit', 'token.json');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Get stored token from disk
|
|
16
|
+
* Get stored token from disk. Attempts refresh if access token is expired.
|
|
16
17
|
*/
|
|
17
18
|
export async function getStoredToken() {
|
|
18
19
|
try {
|
|
19
20
|
const data = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
20
|
-
const { token, expiresAt } = JSON.parse(data);
|
|
21
|
+
const { token, expiresAt, refreshToken, refreshExpiresAt } = JSON.parse(data);
|
|
21
22
|
|
|
22
|
-
// Check if
|
|
23
|
-
if (expiresAt && new Date(expiresAt)
|
|
24
|
-
|
|
25
|
-
return null;
|
|
23
|
+
// Check if access token is still valid
|
|
24
|
+
if (expiresAt && new Date(expiresAt) > new Date()) {
|
|
25
|
+
return token;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Access token expired — try refresh
|
|
29
|
+
if (refreshToken && refreshExpiresAt && new Date(refreshExpiresAt) > new Date()) {
|
|
30
|
+
const refreshed = await refreshAccessToken(refreshToken);
|
|
31
|
+
if (refreshed) return refreshed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Both expired
|
|
35
|
+
await clearToken();
|
|
36
|
+
return null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Refresh an expired access token using the refresh token.
|
|
44
|
+
*/
|
|
45
|
+
async function refreshAccessToken(refreshToken) {
|
|
46
|
+
const CIRCUIT_API = process.env.CIRCUIT_API_URL || 'https://api.withcircuit.com';
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`${CIRCUIT_API}/mcp/token`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
grant_type: 'refresh_token',
|
|
53
|
+
refresh_token: refreshToken,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) return null;
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
if (data.access_token) {
|
|
59
|
+
await storeToken(data.access_token, data.expires_in, data.refresh_token);
|
|
60
|
+
return data.access_token;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
29
63
|
} catch {
|
|
30
64
|
return null;
|
|
31
65
|
}
|
|
@@ -34,12 +68,18 @@ export async function getStoredToken() {
|
|
|
34
68
|
/**
|
|
35
69
|
* Store token to disk
|
|
36
70
|
*/
|
|
37
|
-
async function storeToken(token, expiresIn = 86400 * 30) {
|
|
71
|
+
async function storeToken(token, expiresIn = 86400 * 30, refreshToken = null) {
|
|
38
72
|
const dir = path.dirname(TOKEN_FILE);
|
|
39
73
|
await fs.mkdir(dir, { recursive: true });
|
|
40
74
|
|
|
41
75
|
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
42
|
-
|
|
76
|
+
const tokenData = { token, expiresAt };
|
|
77
|
+
if (refreshToken) {
|
|
78
|
+
tokenData.refreshToken = refreshToken;
|
|
79
|
+
// Refresh tokens last 90 days
|
|
80
|
+
tokenData.refreshExpiresAt = new Date(Date.now() + 90 * 86400 * 1000).toISOString();
|
|
81
|
+
}
|
|
82
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
|
|
43
83
|
}
|
|
44
84
|
|
|
45
85
|
/**
|
|
@@ -66,11 +106,18 @@ export async function authenticate() {
|
|
|
66
106
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
67
107
|
const state = randomBytes(16).toString('hex');
|
|
68
108
|
|
|
69
|
-
//
|
|
109
|
+
// PKCE (RFC 7636): generate code_verifier and code_challenge
|
|
110
|
+
const codeVerifier = randomBytes(32).toString('base64url');
|
|
111
|
+
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
112
|
+
|
|
113
|
+
// Build auth URL with PKCE (RFC 7636)
|
|
70
114
|
const authUrl = new URL(`${CIRCUIT_URL}/mcp/auth`);
|
|
71
115
|
authUrl.searchParams.set('redirect_uri', callbackUrl);
|
|
72
116
|
authUrl.searchParams.set('state', state);
|
|
73
117
|
authUrl.searchParams.set('response_type', 'code');
|
|
118
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
119
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
120
|
+
authUrl.searchParams.set('code_verifier', codeVerifier);
|
|
74
121
|
|
|
75
122
|
// Handle callback
|
|
76
123
|
server.on('request', async (req, res) => {
|
|
@@ -78,6 +125,7 @@ export async function authenticate() {
|
|
|
78
125
|
|
|
79
126
|
if (url.pathname === '/callback') {
|
|
80
127
|
const token = url.searchParams.get('access_token');
|
|
128
|
+
const refreshTokenParam = url.searchParams.get('refresh_token');
|
|
81
129
|
const error = url.searchParams.get('error');
|
|
82
130
|
const returnedState = url.searchParams.get('state');
|
|
83
131
|
|
|
@@ -97,8 +145,8 @@ export async function authenticate() {
|
|
|
97
145
|
return;
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
// Store token
|
|
101
|
-
await storeToken(token);
|
|
148
|
+
// Store token (with refresh token if available)
|
|
149
|
+
await storeToken(token, 86400 * 30, refreshTokenParam);
|
|
102
150
|
|
|
103
151
|
// Send success page and close after response is fully sent
|
|
104
152
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
@@ -131,131 +179,3 @@ export async function authenticate() {
|
|
|
131
179
|
server.on('error', reject);
|
|
132
180
|
});
|
|
133
181
|
}
|
|
134
|
-
|
|
135
|
-
function getSuccessPage() {
|
|
136
|
-
return `<!DOCTYPE html>
|
|
137
|
-
<html>
|
|
138
|
-
<head>
|
|
139
|
-
<title>Circuit - Connected</title>
|
|
140
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>">
|
|
141
|
-
<style>
|
|
142
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
143
|
-
body {
|
|
144
|
-
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
145
|
-
background: #F5F3F0;
|
|
146
|
-
min-height: 100vh;
|
|
147
|
-
display: flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
150
|
-
color: #1C1A18;
|
|
151
|
-
padding: 24px;
|
|
152
|
-
}
|
|
153
|
-
.content {
|
|
154
|
-
text-align: center;
|
|
155
|
-
max-width: 400px;
|
|
156
|
-
}
|
|
157
|
-
.icon {
|
|
158
|
-
margin-bottom: 24px;
|
|
159
|
-
}
|
|
160
|
-
.icon svg {
|
|
161
|
-
width: 48px;
|
|
162
|
-
height: 48px;
|
|
163
|
-
color: #1C1A18;
|
|
164
|
-
}
|
|
165
|
-
h1 {
|
|
166
|
-
font-size: 24px;
|
|
167
|
-
font-weight: 600;
|
|
168
|
-
color: #1C1A18;
|
|
169
|
-
margin-bottom: 12px;
|
|
170
|
-
}
|
|
171
|
-
p {
|
|
172
|
-
font-size: 14px;
|
|
173
|
-
color: rgba(28, 26, 24, 0.6);
|
|
174
|
-
line-height: 1.6;
|
|
175
|
-
}
|
|
176
|
-
.hint {
|
|
177
|
-
font-size: 12px;
|
|
178
|
-
color: rgba(28, 26, 24, 0.6);
|
|
179
|
-
margin-top: 16px;
|
|
180
|
-
}
|
|
181
|
-
</style>
|
|
182
|
-
</head>
|
|
183
|
-
<body>
|
|
184
|
-
<div class="content">
|
|
185
|
-
<div class="icon">
|
|
186
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
187
|
-
</div>
|
|
188
|
-
<h1>Connected.</h1>
|
|
189
|
-
<p>Your AI coding assistant is now connected to Circuit.</p>
|
|
190
|
-
<p class="hint">You can close this tab and return to your editor.</p>
|
|
191
|
-
</div>
|
|
192
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
193
|
-
</body>
|
|
194
|
-
</html>`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function getErrorPage(error) {
|
|
198
|
-
// Sanitize error message to prevent XSS in the static HTML page
|
|
199
|
-
const safeError = String(error).replace(/[<>&"']/g, c => ({
|
|
200
|
-
'<': '<', '>': '>', '&': '&', '"': '"', "'": '''
|
|
201
|
-
})[c]);
|
|
202
|
-
|
|
203
|
-
return `<!DOCTYPE html>
|
|
204
|
-
<html>
|
|
205
|
-
<head>
|
|
206
|
-
<title>Circuit - Connection Failed</title>
|
|
207
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>">
|
|
208
|
-
<style>
|
|
209
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
210
|
-
body {
|
|
211
|
-
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
212
|
-
background: #F5F3F0;
|
|
213
|
-
min-height: 100vh;
|
|
214
|
-
display: flex;
|
|
215
|
-
align-items: center;
|
|
216
|
-
justify-content: center;
|
|
217
|
-
color: #1C1A18;
|
|
218
|
-
padding: 24px;
|
|
219
|
-
}
|
|
220
|
-
.content {
|
|
221
|
-
text-align: center;
|
|
222
|
-
max-width: 400px;
|
|
223
|
-
}
|
|
224
|
-
.icon {
|
|
225
|
-
margin-bottom: 24px;
|
|
226
|
-
}
|
|
227
|
-
.icon svg {
|
|
228
|
-
width: 48px;
|
|
229
|
-
height: 48px;
|
|
230
|
-
color: #D64545;
|
|
231
|
-
}
|
|
232
|
-
h1 {
|
|
233
|
-
font-size: 24px;
|
|
234
|
-
font-weight: 600;
|
|
235
|
-
color: #1C1A18;
|
|
236
|
-
margin-bottom: 12px;
|
|
237
|
-
}
|
|
238
|
-
p {
|
|
239
|
-
font-size: 14px;
|
|
240
|
-
color: rgba(28, 26, 24, 0.6);
|
|
241
|
-
line-height: 1.6;
|
|
242
|
-
}
|
|
243
|
-
.hint {
|
|
244
|
-
font-size: 12px;
|
|
245
|
-
color: rgba(28, 26, 24, 0.6);
|
|
246
|
-
margin-top: 16px;
|
|
247
|
-
}
|
|
248
|
-
</style>
|
|
249
|
-
</head>
|
|
250
|
-
<body>
|
|
251
|
-
<div class="content">
|
|
252
|
-
<div class="icon">
|
|
253
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
|
254
|
-
</div>
|
|
255
|
-
<h1>Connection Failed</h1>
|
|
256
|
-
<p>${safeError}</p>
|
|
257
|
-
<p class="hint">Please try connecting again from your AI assistant.</p>
|
|
258
|
-
</div>
|
|
259
|
-
</body>
|
|
260
|
-
</html>`;
|
|
261
|
-
}
|
package/src/index.js
CHANGED
|
@@ -92,7 +92,7 @@ function showTroubleshoot() {
|
|
|
92
92
|
console.log(chalk.dim(' 1. Check if MCP is installed:'));
|
|
93
93
|
console.log(chalk.white(' claude mcp list\n'));
|
|
94
94
|
console.log(chalk.dim(' 2. If not listed, add it:'));
|
|
95
|
-
console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
|
|
95
|
+
console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
|
|
96
96
|
console.log(chalk.dim(' 3. Restart Claude Code\n'));
|
|
97
97
|
|
|
98
98
|
// Issue 4: Authentication issues
|
|
@@ -169,6 +169,7 @@ async function configureCursor() {
|
|
|
169
169
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
170
170
|
|
|
171
171
|
let config = { mcpServers: {} };
|
|
172
|
+
let wasDisabled = false;
|
|
172
173
|
|
|
173
174
|
// Try to read existing config
|
|
174
175
|
try {
|
|
@@ -177,31 +178,40 @@ async function configureCursor() {
|
|
|
177
178
|
if (!config.mcpServers) {
|
|
178
179
|
config.mcpServers = {};
|
|
179
180
|
}
|
|
181
|
+
// Check if Circuit was previously disabled in Cursor
|
|
182
|
+
wasDisabled = config.mcpServers.circuit?.disabled === true;
|
|
180
183
|
} catch {
|
|
181
184
|
// File doesn't exist, use default
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
//
|
|
187
|
+
// Write Circuit config — overwrites any previous disabled: true
|
|
185
188
|
config.mcpServers.circuit = circuitConfig;
|
|
186
189
|
|
|
187
190
|
// Write config
|
|
188
191
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
189
|
-
return true;
|
|
192
|
+
return { success: true, wasDisabled };
|
|
190
193
|
} catch (err) {
|
|
191
|
-
return false;
|
|
194
|
+
return { success: false, wasDisabled: false };
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
// Configure Claude Code by running the CLI command
|
|
196
199
|
function configureClaudeCode() {
|
|
197
200
|
try {
|
|
198
|
-
|
|
201
|
+
// --scope user makes Circuit available in every Claude Code session,
|
|
202
|
+
// not just the current project directory (default scope is "local").
|
|
203
|
+
// Remove first so re-running setup always produces a clean state.
|
|
204
|
+
try {
|
|
205
|
+
execSync('claude mcp remove circuit', { stdio: 'pipe', timeout: 5000 });
|
|
206
|
+
} catch {
|
|
207
|
+
// Not present yet — fine
|
|
208
|
+
}
|
|
209
|
+
execSync('claude mcp add --scope user circuit -- npx circuit-mcp', {
|
|
199
210
|
stdio: 'pipe',
|
|
200
211
|
timeout: 10000
|
|
201
212
|
});
|
|
202
213
|
return true;
|
|
203
214
|
} catch (err) {
|
|
204
|
-
// Command might fail if claude CLI not installed
|
|
205
215
|
return false;
|
|
206
216
|
}
|
|
207
217
|
}
|
|
@@ -226,10 +236,15 @@ async function runSetup() {
|
|
|
226
236
|
if (choice === '1' || choice === '3') {
|
|
227
237
|
console.log(chalk.dim(' Setting up Cursor...\n'));
|
|
228
238
|
|
|
229
|
-
const success = await configureCursor();
|
|
239
|
+
const { success, wasDisabled } = await configureCursor();
|
|
230
240
|
|
|
231
241
|
if (success) {
|
|
232
|
-
|
|
242
|
+
if (wasDisabled) {
|
|
243
|
+
showSuccess('Re-enabled Circuit in ~/.cursor/mcp.json');
|
|
244
|
+
console.log(chalk.dim(' (It was disabled — turned back on.)\n'));
|
|
245
|
+
} else {
|
|
246
|
+
showSuccess('Added Circuit to ~/.cursor/mcp.json');
|
|
247
|
+
}
|
|
233
248
|
console.log(chalk.dim(' Restart Cursor to activate.\n'));
|
|
234
249
|
} else {
|
|
235
250
|
showError('Could not update Cursor config automatically.');
|
|
@@ -253,7 +268,7 @@ async function runSetup() {
|
|
|
253
268
|
} else {
|
|
254
269
|
showError('Could not run claude CLI automatically.');
|
|
255
270
|
console.log(chalk.dim(' Run this command manually:\n'));
|
|
256
|
-
console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
|
|
271
|
+
console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
|
|
@@ -264,7 +279,7 @@ async function runSetup() {
|
|
|
264
279
|
console.log(chalk.white(' { "mcpServers": { "circuit": { "command": "npx", "args": ["circuit-mcp"] } } }\n'));
|
|
265
280
|
console.log(chalk.cyan.bold(' Claude Code'));
|
|
266
281
|
console.log(chalk.dim(' Run:'));
|
|
267
|
-
console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
|
|
282
|
+
console.log(chalk.white(' claude mcp add --scope user circuit -- npx circuit-mcp\n'));
|
|
268
283
|
return;
|
|
269
284
|
}
|
|
270
285
|
|
|
@@ -274,6 +289,8 @@ async function runSetup() {
|
|
|
274
289
|
|
|
275
290
|
const authChoice = await prompt(chalk.cyan(' Sign in now? (Y/n): '));
|
|
276
291
|
|
|
292
|
+
let authSucceeded = false;
|
|
293
|
+
|
|
277
294
|
if (authChoice.toLowerCase() !== 'n') {
|
|
278
295
|
console.log();
|
|
279
296
|
console.log(chalk.dim(' Opening browser to sign in...\n'));
|
|
@@ -281,6 +298,7 @@ async function runSetup() {
|
|
|
281
298
|
try {
|
|
282
299
|
const token = await authenticate();
|
|
283
300
|
showSuccess('Signed in to Circuit');
|
|
301
|
+
authSucceeded = true;
|
|
284
302
|
console.log();
|
|
285
303
|
} catch (err) {
|
|
286
304
|
showError('Could not sign in automatically.');
|
|
@@ -289,9 +307,17 @@ async function runSetup() {
|
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
|
|
311
|
+
if (authSucceeded || authChoice.toLowerCase() === 'n') {
|
|
312
|
+
console.log(chalk.white.bold(' You\'re all set!\n'));
|
|
313
|
+
console.log(chalk.dim(' Restart your editor and start using Circuit.\n'));
|
|
314
|
+
showUsageGuide();
|
|
315
|
+
} else {
|
|
316
|
+
console.log(chalk.white.bold(' Almost ready!\n'));
|
|
317
|
+
console.log(chalk.dim(' Sign in to finish connecting your Circuit account:\n'));
|
|
318
|
+
console.log(chalk.white(' npx circuit-mcp auth\n'));
|
|
319
|
+
console.log(chalk.dim(' Then restart your editor.\n'));
|
|
320
|
+
}
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
async function runAuth() {
|
|
@@ -325,6 +351,14 @@ async function runServer() {
|
|
|
325
351
|
let token = await getStoredToken();
|
|
326
352
|
|
|
327
353
|
if (!token) {
|
|
354
|
+
// Headless (spawned by Claude Code / Cursor over stdio): the browser auth
|
|
355
|
+
// flow has no one to drive it, so fail fast with instructions instead of
|
|
356
|
+
// hanging the MCP handshake.
|
|
357
|
+
if (!process.stdin.isTTY) {
|
|
358
|
+
console.error('Circuit MCP: not signed in. Run `npx circuit-mcp auth` in a terminal, then reconnect the MCP server.');
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
|
|
328
362
|
// First run - show banner and auth
|
|
329
363
|
showBanner();
|
|
330
364
|
console.log(chalk.dim(' First time setup - let\'s connect your account.\n'));
|
package/src/pages.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local auth result pages, served from 127.0.0.1 after the OAuth callback.
|
|
3
|
+
*
|
|
4
|
+
* Design: docs/superpowers/specs/2026-06-11-mcp-auth-pages-design.md
|
|
5
|
+
* Mirrors the product's /welcome onboarding cinematic system. Art and Geist
|
|
6
|
+
* load from app.withcircuit.com (CORS-open); offline the page degrades to a
|
|
7
|
+
* plain #1A1310 canvas with system fonts — values are hardcoded here because
|
|
8
|
+
* circuit-tokens.css is not available to the CLI.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ASSET_ORIGIN = 'https://app.withcircuit.com';
|
|
12
|
+
|
|
13
|
+
const FAVICON = `data:image/svg+xml,<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'><defs><mask id='m'><circle cx='16' cy='16' r='13' fill='white'/><circle cx='16' cy='16' r='6.9' fill='black'/></mask></defs><ellipse cx='16' cy='17.73' rx='13' ry='11.27' fill='rgba(168,102,122,0.2)'/><circle cx='16' cy='16' r='13' fill='%23A8667A' mask='url(%23m)'/></svg>`;
|
|
14
|
+
|
|
15
|
+
const RING_MARK = `<svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><defs><mask id="ring"><circle cx="16" cy="16" r="13" fill="white"/><circle cx="16" cy="16" r="6.9" fill="black"/></mask></defs><circle cx="16" cy="16" r="13" fill="#A8667A" mask="url(#ring)"/></svg>`;
|
|
16
|
+
|
|
17
|
+
const COPY_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
18
|
+
|
|
19
|
+
const CHECK_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
20
|
+
|
|
21
|
+
// Shared frame: backdrop layers + header + centered card.
|
|
22
|
+
// All values measured from the live /welcome flow (see spec table).
|
|
23
|
+
function renderPage({ title, eyebrow, eyebrowError, heading, body, pillHtml, hintHtml }) {
|
|
24
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
+
<title>${title}</title>
|
|
30
|
+
<link rel="icon" type="image/svg+xml" href="${FAVICON}">
|
|
31
|
+
<style>
|
|
32
|
+
@font-face { font-family: 'Geist'; src: url('${ASSET_ORIGIN}/fonts/Geist-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; }
|
|
33
|
+
@font-face { font-family: 'Geist Mono'; src: url('${ASSET_ORIGIN}/fonts/GeistMono-Variable.woff2') format('woff2'); font-weight: 100 900; font-display: swap; }
|
|
34
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
35
|
+
body { font-family: 'Geist', -apple-system, system-ui, sans-serif; background: #1a1310; min-height: 100vh; }
|
|
36
|
+
.scene { position: relative; min-height: 100vh; overflow: hidden; }
|
|
37
|
+
.scene__bg { position: absolute; inset: 0; background-image: url('${ASSET_ORIGIN}/assets/ring-hero.png'); background-size: cover; background-position: 0% 38%; }
|
|
38
|
+
.scene__overlay { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(14,9,7,0.72), rgba(14,9,7,0.6), rgba(14,9,7,0.55)); }
|
|
39
|
+
.scene__grain { position: absolute; inset: 0; background: radial-gradient(rgba(244,228,201,0.5) 1px, transparent 1px); background-size: 3px 3px; opacity: 0.1; mix-blend-mode: overlay; }
|
|
40
|
+
.header { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 16px; padding: 22px 28px; }
|
|
41
|
+
.header__wordmark { font-size: 17px; font-weight: 500; letter-spacing: -0.01em; color: #efe7da; }
|
|
42
|
+
.center { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
43
|
+
.card { background: rgba(20,15,13,0.78); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 16px; padding: 48px; width: 620px; }
|
|
44
|
+
.eyebrow { font-family: 'Geist Mono', ui-monospace, monospace; font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: #c9919e; }
|
|
45
|
+
.eyebrow--error { color: #d06a55; }
|
|
46
|
+
h1 { font-size: 42px; font-weight: 500; letter-spacing: -0.03em; line-height: 1.08; color: #efe7da; margin-top: 24px; }
|
|
47
|
+
.body { font-size: 14.5px; line-height: 1.55; color: rgba(239,231,218,0.62); margin-top: 20px; max-width: 92%; }
|
|
48
|
+
.pill { display: flex; align-items: center; gap: 12px; margin-top: 26px; background: #0e0907; border-radius: 9px; padding: 13px 16px; font-family: 'Geist Mono', ui-monospace, monospace; font-size: 13px; color: #efe7da; box-shadow: inset 0 0 0 1px rgba(239,231,218,0.12); }
|
|
49
|
+
.pill__label { color: rgba(239,231,218,0.45); }
|
|
50
|
+
.pill__copy { margin-left: auto; display: inline-flex; align-items: center; background: none; border: none; padding: 4px; cursor: pointer; color: rgba(239,231,218,0.45); transition: color 200ms cubic-bezier(0, 0, 0.2, 1); /* --duration-normal --ease-out */ }
|
|
51
|
+
.pill__copy:hover { color: #efe7da; }
|
|
52
|
+
.hint { font-size: 12.5px; color: rgba(239,231,218,0.45); margin-top: 18px; }
|
|
53
|
+
@media (max-width: 899px) { .scene__bg { background-position: 50% 38%; } .card { width: 640px; max-width: calc(100% - 48px); } h1 { font-size: 40px; } }
|
|
54
|
+
@media (max-width: 639px) { .card { padding: 28px 24px; max-width: calc(100% - 24px); } h1 { font-size: 28px; } .body { max-width: 100%; } .pill { font-size: 12px; padding: 12px 14px; } }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="scene">
|
|
59
|
+
<div class="scene__bg"></div>
|
|
60
|
+
<div class="scene__overlay"></div>
|
|
61
|
+
<div class="scene__grain"></div>
|
|
62
|
+
<div class="header">${RING_MARK}<span class="header__wordmark">Circuit</span></div>
|
|
63
|
+
<div class="center">
|
|
64
|
+
<div class="card">
|
|
65
|
+
<p class="eyebrow${eyebrowError ? ' eyebrow--error' : ''}">${eyebrow}</p>
|
|
66
|
+
<h1>${heading}</h1>
|
|
67
|
+
<p class="body">${body}</p>
|
|
68
|
+
${pillHtml}
|
|
69
|
+
${hintHtml}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<script>
|
|
74
|
+
document.querySelectorAll('[data-copy]').forEach((btn) => {
|
|
75
|
+
btn.addEventListener('click', async () => {
|
|
76
|
+
try {
|
|
77
|
+
await navigator.clipboard.writeText(btn.getAttribute('data-copy'));
|
|
78
|
+
btn.innerHTML = ${JSON.stringify(CHECK_ICON)};
|
|
79
|
+
setTimeout(() => { btn.innerHTML = ${JSON.stringify(COPY_ICON)}; }, 2000);
|
|
80
|
+
} catch {}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
</script>
|
|
84
|
+
</body>
|
|
85
|
+
</html>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getSuccessPage() {
|
|
89
|
+
return renderPage({
|
|
90
|
+
title: 'Circuit — Connected',
|
|
91
|
+
eyebrow: 'Circuit MCP',
|
|
92
|
+
eyebrowError: false,
|
|
93
|
+
heading: 'Connected.',
|
|
94
|
+
body: 'Circuit is in your editor now. Your customers are about to start writing your next sprint.',
|
|
95
|
+
pillHtml: `<div class="pill"><span class="pill__label">Try:</span><span>“What should I work on?”</span><button type="button" class="pill__copy" aria-label="Copy prompt" data-copy="What should I work on?">${COPY_ICON}</button></div>`,
|
|
96
|
+
hintHtml: `<p class="hint">You can close this tab and return to your editor.</p>`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getErrorPage(error) {
|
|
101
|
+
// Sanitize before interpolation — same contract as the old page
|
|
102
|
+
const safeError = String(error).replace(/[<>&"']/g, (c) => ({
|
|
103
|
+
'<': '<',
|
|
104
|
+
'>': '>',
|
|
105
|
+
'&': '&',
|
|
106
|
+
'"': '"',
|
|
107
|
+
"'": ''',
|
|
108
|
+
})[c]);
|
|
109
|
+
|
|
110
|
+
return renderPage({
|
|
111
|
+
title: 'Circuit — Connection Failed',
|
|
112
|
+
eyebrow: 'Connection failed',
|
|
113
|
+
eyebrowError: true,
|
|
114
|
+
heading: 'Let’s try that again.',
|
|
115
|
+
body: 'We couldn’t finish signing you in. Run this in your terminal:',
|
|
116
|
+
pillHtml: `<div class="pill"><span>npx circuit-mcp auth</span><button type="button" class="pill__copy" aria-label="Copy command" data-copy="npx circuit-mcp auth">${COPY_ICON}</button></div>`,
|
|
117
|
+
hintHtml: `<p class="hint">${safeError}</p>`,
|
|
118
|
+
});
|
|
119
|
+
}
|
package/src/server.js
CHANGED
|
@@ -83,8 +83,8 @@ async function handleMessage(message, token) {
|
|
|
83
83
|
jsonrpc: '2.0',
|
|
84
84
|
id,
|
|
85
85
|
result: {
|
|
86
|
-
protocolVersion: '
|
|
87
|
-
serverInfo: { name: 'circuit-mcp', version: '2.
|
|
86
|
+
protocolVersion: '2025-03-26',
|
|
87
|
+
serverInfo: { name: 'circuit-mcp', version: '2.4.0' },
|
|
88
88
|
capabilities: { tools: {}, resources: {} },
|
|
89
89
|
instructions: `Circuit is connected. 4 tools available:
|
|
90
90
|
• circuit.priorities — what to work on next
|
|
@@ -201,14 +201,14 @@ const TOOLS = [
|
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
name: 'circuit.act',
|
|
204
|
-
description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, submit feedback, or submit a transcript.",
|
|
204
|
+
description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, park a priority, submit feedback, or submit a transcript.",
|
|
205
205
|
inputSchema: {
|
|
206
206
|
type: 'object',
|
|
207
207
|
properties: {
|
|
208
208
|
action: {
|
|
209
209
|
type: 'string',
|
|
210
|
-
description: "'build' (start building), 'ship' (mark shipped), 'share' (notify customers via email/widget), 'assign' (assign to team member), 'correct' (fix classification), 'submit' (add feedback), 'transcript' (submit a transcript)",
|
|
211
|
-
enum: ['build', 'ship', 'share', 'assign', 'correct', 'submit', 'transcript']
|
|
210
|
+
description: "'build' (start building), 'ship' (mark shipped), 'share' (notify customers via email/widget), 'assign' (assign to team member), 'correct' (fix classification), 'park' (park priority with reason), 'submit' (add feedback), 'transcript' (submit a transcript)",
|
|
211
|
+
enum: ['build', 'ship', 'share', 'assign', 'correct', 'park', 'submit', 'transcript']
|
|
212
212
|
},
|
|
213
213
|
spec_id: {
|
|
214
214
|
type: 'string',
|
|
@@ -229,7 +229,11 @@ const TOOLS = [
|
|
|
229
229
|
},
|
|
230
230
|
priority_id: {
|
|
231
231
|
type: 'string',
|
|
232
|
-
description: "Priority ID (for correct)"
|
|
232
|
+
description: "Priority ID (for correct, park)"
|
|
233
|
+
},
|
|
234
|
+
park_reason: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: "Reason for parking this priority (for park action). E.g. 'Out of scope this quarter', 'Waiting for more signal', 'Low revenue impact'"
|
|
233
237
|
},
|
|
234
238
|
correction_type: {
|
|
235
239
|
type: 'string',
|
|
@@ -557,11 +561,12 @@ Ranked customer priorities with revenue impact and trend data.
|
|
|
557
561
|
* "Export all ready specs for sprint planning"
|
|
558
562
|
|
|
559
563
|
## circuit.act — Ship it and close the loop
|
|
560
|
-
Start building, mark as shipped, notify customers, assign
|
|
564
|
+
Start building, mark as shipped, notify customers, assign, park a priority, or submit new feedback.
|
|
561
565
|
* "Start building <spec_id>"
|
|
562
566
|
* "Mark <spec_id> as shipped"
|
|
563
567
|
* "Tell customers we shipped <spec_id>"
|
|
564
568
|
* "Assign <spec_id> to Catherine"
|
|
569
|
+
* "Park <priority_id> — waiting for more signal"
|
|
565
570
|
* "Submit feedback: users want dark mode"
|
|
566
571
|
* "Submit transcript: <transcript text>"
|
|
567
572
|
|
package/preview-error.html
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>Circuit - Connection Failed</title>
|
|
5
|
-
<style>
|
|
6
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7
|
-
body {
|
|
8
|
-
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
9
|
-
background: #F5F3F0;
|
|
10
|
-
min-height: 100vh;
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
justify-content: center;
|
|
14
|
-
color: #1C1A18;
|
|
15
|
-
padding: 24px;
|
|
16
|
-
}
|
|
17
|
-
.content {
|
|
18
|
-
text-align: center;
|
|
19
|
-
max-width: 400px;
|
|
20
|
-
}
|
|
21
|
-
.icon {
|
|
22
|
-
margin-bottom: 24px;
|
|
23
|
-
}
|
|
24
|
-
.icon svg {
|
|
25
|
-
width: 48px;
|
|
26
|
-
height: 48px;
|
|
27
|
-
color: #D64545;
|
|
28
|
-
}
|
|
29
|
-
h1 {
|
|
30
|
-
font-size: 24px;
|
|
31
|
-
font-weight: 600;
|
|
32
|
-
color: #1C1A18;
|
|
33
|
-
margin-bottom: 12px;
|
|
34
|
-
}
|
|
35
|
-
p {
|
|
36
|
-
font-size: 14px;
|
|
37
|
-
color: rgba(28, 26, 24, 0.6);
|
|
38
|
-
line-height: 1.6;
|
|
39
|
-
}
|
|
40
|
-
.hint {
|
|
41
|
-
font-size: 12px;
|
|
42
|
-
color: rgba(28, 26, 24, 0.6);
|
|
43
|
-
margin-top: 16px;
|
|
44
|
-
}
|
|
45
|
-
</style>
|
|
46
|
-
</head>
|
|
47
|
-
<body>
|
|
48
|
-
<div class="content">
|
|
49
|
-
<div class="icon">
|
|
50
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
|
51
|
-
</div>
|
|
52
|
-
<h1>Connection Failed</h1>
|
|
53
|
-
<p>Authentication timed out</p>
|
|
54
|
-
<p class="hint">Please try connecting again from your AI assistant.</p>
|
|
55
|
-
</div>
|
|
56
|
-
</body>
|
|
57
|
-
</html>
|
package/preview-success.html
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>Circuit - Connected</title>
|
|
5
|
-
<style>
|
|
6
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7
|
-
body {
|
|
8
|
-
font-family: 'Geist', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
9
|
-
background: #F5F3F0;
|
|
10
|
-
min-height: 100vh;
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
justify-content: center;
|
|
14
|
-
color: #1C1A18;
|
|
15
|
-
padding: 24px;
|
|
16
|
-
}
|
|
17
|
-
.content {
|
|
18
|
-
text-align: center;
|
|
19
|
-
max-width: 400px;
|
|
20
|
-
}
|
|
21
|
-
.icon {
|
|
22
|
-
margin-bottom: 24px;
|
|
23
|
-
}
|
|
24
|
-
.icon svg {
|
|
25
|
-
width: 48px;
|
|
26
|
-
height: 48px;
|
|
27
|
-
color: #1C1A18;
|
|
28
|
-
}
|
|
29
|
-
h1 {
|
|
30
|
-
font-size: 24px;
|
|
31
|
-
font-weight: 600;
|
|
32
|
-
color: #1C1A18;
|
|
33
|
-
margin-bottom: 12px;
|
|
34
|
-
}
|
|
35
|
-
p {
|
|
36
|
-
font-size: 14px;
|
|
37
|
-
color: rgba(28, 26, 24, 0.6);
|
|
38
|
-
line-height: 1.6;
|
|
39
|
-
}
|
|
40
|
-
.hint {
|
|
41
|
-
font-size: 12px;
|
|
42
|
-
color: rgba(28, 26, 24, 0.6);
|
|
43
|
-
margin-top: 16px;
|
|
44
|
-
}
|
|
45
|
-
</style>
|
|
46
|
-
</head>
|
|
47
|
-
<body>
|
|
48
|
-
<div class="content">
|
|
49
|
-
<div class="icon">
|
|
50
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
51
|
-
</div>
|
|
52
|
-
<h1>Connected.</h1>
|
|
53
|
-
<p>Your AI coding assistant is now connected to Circuit.</p>
|
|
54
|
-
<p class="hint">This window will close automatically...</p>
|
|
55
|
-
</div>
|
|
56
|
-
</body>
|
|
57
|
-
</html>
|
package/publish.sh
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Publish script for circuit-mcp package
|
|
4
|
-
# Uses token to bypass 2FA
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# NPM_TOKEN=your_token ./publish.sh
|
|
8
|
-
#
|
|
9
|
-
# Or set NPM_TOKEN in your environment
|
|
10
|
-
|
|
11
|
-
set -e
|
|
12
|
-
|
|
13
|
-
# Use NPM_TOKEN from environment, or prompt if not set
|
|
14
|
-
if [ -z "$NPM_TOKEN" ]; then
|
|
15
|
-
echo "Error: NPM_TOKEN environment variable is required"
|
|
16
|
-
echo "Usage: NPM_TOKEN=your_token ./publish.sh"
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
# Create temporary .npmrc with token
|
|
21
|
-
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
|
|
22
|
-
|
|
23
|
-
# Publish the package
|
|
24
|
-
npm publish --access public
|
|
25
|
-
|
|
26
|
-
# Clean up .npmrc
|
|
27
|
-
rm .npmrc
|
|
28
|
-
|
|
29
|
-
echo "✅ Published successfully!"
|
|
30
|
-
|