circuit-mcp 1.0.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/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Circuit MCP
2
+
3
+ Connect [Circuit](https://withcircuit.com) to Cursor and Claude Code via MCP (Model Context Protocol).
4
+
5
+ ## Quick Start
6
+
7
+ ### Cursor
8
+
9
+ Add to your Cursor settings (`Cmd+Shift+P` → "Cursor Settings: Open User Settings"):
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "circuit": {
15
+ "command": "npx",
16
+ "args": ["circuit-mcp"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ### Claude Code
23
+
24
+ Run this command:
25
+
26
+ ```bash
27
+ claude mcp add circuit -- npx circuit-mcp
28
+ ```
29
+
30
+ ## First Run
31
+
32
+ On first use, Circuit will open your browser to authenticate. After signing in, your token is cached locally at `~/.circuit/token.json`.
33
+
34
+ ```
35
+ ╭──────────────────────────────────╮
36
+ │ ⚡ Circuit MCP │
37
+ ╰──────────────────────────────────╯
38
+
39
+ First time setup - let's connect your account.
40
+
41
+ Opening browser to authenticate...
42
+
43
+ ✓ Connected!
44
+ ```
45
+
46
+ ## Commands
47
+
48
+ ```bash
49
+ # Start MCP server (used by Cursor/Claude)
50
+ npx circuit-mcp
51
+
52
+ # Show setup instructions
53
+ npx circuit-mcp setup
54
+
55
+ # Re-authenticate
56
+ npx circuit-mcp auth
57
+
58
+ # Log out (clear stored token)
59
+ npx circuit-mcp logout
60
+ ```
61
+
62
+ ## Available Tools
63
+
64
+ Once connected, your AI coding assistant can use these tools:
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `get_priorities` | Get top customer feedback priorities |
69
+ | `get_brief` | Get the engineering brief for a priority |
70
+ | `get_feedback` | Get raw customer feedback items |
71
+
72
+ ### Example Usage in Cursor/Claude
73
+
74
+ > "What are the top 5 customer priorities?"
75
+
76
+ > "Get the brief for priority #1 and help me implement it"
77
+
78
+ > "Show me recent feedback about authentication issues"
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/index.js';
4
+
5
+ main().catch(err => {
6
+ console.error('Fatal error:', err.message);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "circuit-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Circuit MCP server for Cursor and Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "circuit-mcp": "./bin/circuit-mcp.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "scripts": {
11
+ "start": "node bin/circuit-mcp.js"
12
+ },
13
+ "keywords": [
14
+ "circuit",
15
+ "mcp",
16
+ "cursor",
17
+ "claude",
18
+ "ai",
19
+ "feedback"
20
+ ],
21
+ "author": "Circuit",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "open": "^10.1.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }
package/src/auth.js ADDED
@@ -0,0 +1,217 @@
1
+ import http from 'http';
2
+ import { URL } from 'url';
3
+ import { randomBytes } from 'crypto';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import open from 'open';
8
+ import chalk from 'chalk';
9
+ import { showSpinner, showInfo, showPrompt } from './ui.js';
10
+
11
+ const CIRCUIT_URL = 'https://app.withcircuit.com';
12
+ const TOKEN_FILE = path.join(os.homedir(), '.circuit', 'token.json');
13
+
14
+ /**
15
+ * Get stored token from disk
16
+ */
17
+ export async function getStoredToken() {
18
+ try {
19
+ const data = await fs.readFile(TOKEN_FILE, 'utf-8');
20
+ const { token, expiresAt } = JSON.parse(data);
21
+
22
+ // Check if expired
23
+ if (expiresAt && new Date(expiresAt) < new Date()) {
24
+ await clearToken();
25
+ return null;
26
+ }
27
+
28
+ return token;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Store token to disk
36
+ */
37
+ async function storeToken(token, expiresIn = 86400 * 30) {
38
+ const dir = path.dirname(TOKEN_FILE);
39
+ await fs.mkdir(dir, { recursive: true });
40
+
41
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
42
+ await fs.writeFile(TOKEN_FILE, JSON.stringify({ token, expiresAt }, null, 2));
43
+ }
44
+
45
+ /**
46
+ * Clear stored token
47
+ */
48
+ export async function clearToken() {
49
+ try {
50
+ await fs.unlink(TOKEN_FILE);
51
+ } catch {
52
+ // Ignore if file doesn't exist
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Authenticate with Circuit via OAuth
58
+ */
59
+ export async function authenticate() {
60
+ return new Promise((resolve, reject) => {
61
+ // Find available port
62
+ const server = http.createServer();
63
+
64
+ server.listen(0, '127.0.0.1', async () => {
65
+ const { port } = server.address();
66
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
67
+ const state = randomBytes(16).toString('hex');
68
+
69
+ // Build auth URL
70
+ const authUrl = new URL(`${CIRCUIT_URL}/mcp/auth`);
71
+ authUrl.searchParams.set('redirect_uri', callbackUrl);
72
+ authUrl.searchParams.set('state', state);
73
+ authUrl.searchParams.set('response_type', 'code');
74
+
75
+ // Handle callback
76
+ server.on('request', async (req, res) => {
77
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
78
+
79
+ if (url.pathname === '/callback') {
80
+ const token = url.searchParams.get('access_token');
81
+ const error = url.searchParams.get('error');
82
+ const returnedState = url.searchParams.get('state');
83
+
84
+ if (error) {
85
+ res.writeHead(400, { 'Content-Type': 'text/html' });
86
+ res.end(getErrorPage(error));
87
+ server.close();
88
+ reject(new Error(error));
89
+ return;
90
+ }
91
+
92
+ if (!token) {
93
+ res.writeHead(400, { 'Content-Type': 'text/html' });
94
+ res.end(getErrorPage('No token received'));
95
+ server.close();
96
+ reject(new Error('No token received'));
97
+ return;
98
+ }
99
+
100
+ // Store token
101
+ await storeToken(token);
102
+
103
+ // Send success page
104
+ res.writeHead(200, { 'Content-Type': 'text/html' });
105
+ res.end(getSuccessPage());
106
+
107
+ server.close();
108
+ resolve(token);
109
+ }
110
+ });
111
+
112
+ // Open browser
113
+ console.log(chalk.dim(' Opening browser to authenticate...\n'));
114
+ showInfo(`If browser doesn't open, visit:\n ${chalk.cyan(authUrl.toString())}\n`);
115
+
116
+ try {
117
+ await open(authUrl.toString());
118
+ } catch {
119
+ // Browser failed to open, user will need to click link
120
+ }
121
+
122
+ // Timeout after 5 minutes
123
+ setTimeout(() => {
124
+ server.close();
125
+ reject(new Error('Authentication timed out'));
126
+ }, 5 * 60 * 1000);
127
+ });
128
+
129
+ server.on('error', reject);
130
+ });
131
+ }
132
+
133
+ function getSuccessPage() {
134
+ return `<!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <title>Circuit - Connected!</title>
138
+ <style>
139
+ * { margin: 0; padding: 0; box-sizing: border-box; }
140
+ body {
141
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
142
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
143
+ min-height: 100vh;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ color: white;
148
+ }
149
+ .container {
150
+ text-align: center;
151
+ padding: 48px;
152
+ }
153
+ .icon {
154
+ font-size: 64px;
155
+ margin-bottom: 24px;
156
+ }
157
+ h1 {
158
+ font-size: 32px;
159
+ font-weight: 600;
160
+ margin-bottom: 16px;
161
+ color: #6366F1;
162
+ }
163
+ p {
164
+ font-size: 18px;
165
+ color: rgba(255,255,255,0.7);
166
+ margin-bottom: 8px;
167
+ }
168
+ .hint {
169
+ margin-top: 32px;
170
+ font-size: 14px;
171
+ color: rgba(255,255,255,0.5);
172
+ }
173
+ </style>
174
+ </head>
175
+ <body>
176
+ <div class="container">
177
+ <div class="icon">⚡</div>
178
+ <h1>Connected to Circuit!</h1>
179
+ <p>You can close this window and return to your terminal.</p>
180
+ <p class="hint">Circuit is now ready to use in Cursor or Claude Code.</p>
181
+ </div>
182
+ <script>setTimeout(() => window.close(), 3000);</script>
183
+ </body>
184
+ </html>`;
185
+ }
186
+
187
+ function getErrorPage(error) {
188
+ return `<!DOCTYPE html>
189
+ <html>
190
+ <head>
191
+ <title>Circuit - Error</title>
192
+ <style>
193
+ * { margin: 0; padding: 0; box-sizing: border-box; }
194
+ body {
195
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
196
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
197
+ min-height: 100vh;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ color: white;
202
+ }
203
+ .container { text-align: center; padding: 48px; }
204
+ .icon { font-size: 64px; margin-bottom: 24px; }
205
+ h1 { font-size: 32px; color: #ef4444; margin-bottom: 16px; }
206
+ p { font-size: 18px; color: rgba(255,255,255,0.7); }
207
+ </style>
208
+ </head>
209
+ <body>
210
+ <div class="container">
211
+ <div class="icon">⚠️</div>
212
+ <h1>Authentication Failed</h1>
213
+ <p>${error}</p>
214
+ </div>
215
+ </body>
216
+ </html>`;
217
+ }
package/src/index.js ADDED
@@ -0,0 +1,108 @@
1
+ import chalk from 'chalk';
2
+ import { authenticate, getStoredToken } from './auth.js';
3
+ import { startMcpServer } from './server.js';
4
+ import { showBanner, showSuccess, showError, showSpinner } from './ui.js';
5
+
6
+ const CIRCUIT_URL = 'https://app.withcircuit.com';
7
+
8
+ export async function main() {
9
+ const args = process.argv.slice(2);
10
+
11
+ // Handle setup command
12
+ if (args[0] === 'setup') {
13
+ await runSetup();
14
+ return;
15
+ }
16
+
17
+ // Handle auth command
18
+ if (args[0] === 'auth') {
19
+ await runAuth();
20
+ return;
21
+ }
22
+
23
+ // Handle logout command
24
+ if (args[0] === 'logout') {
25
+ await runLogout();
26
+ return;
27
+ }
28
+
29
+ // Default: start MCP server
30
+ await runServer();
31
+ }
32
+
33
+ async function runSetup() {
34
+ showBanner();
35
+
36
+ console.log(chalk.dim(' Choose your AI coding tool:\n'));
37
+ console.log(` ${chalk.bold('1.')} Cursor`);
38
+ console.log(` ${chalk.bold('2.')} Claude Code`);
39
+ console.log(` ${chalk.bold('3.')} Both\n`);
40
+
41
+ // For now, show instructions for both
42
+ console.log(chalk.cyan.bold(' Cursor Setup\n'));
43
+ console.log(chalk.dim(' Add to your Cursor settings (Cmd+Shift+P → "Cursor Settings: Open User Settings"):\n'));
44
+ console.log(chalk.white(` {
45
+ "mcpServers": {
46
+ "circuit": {
47
+ "command": "npx",
48
+ "args": ["circuit-mcp"]
49
+ }
50
+ }
51
+ }\n`));
52
+
53
+ console.log(chalk.cyan.bold(' Claude Code Setup\n'));
54
+ console.log(chalk.dim(' Run this command in your terminal:\n'));
55
+ console.log(chalk.white(` claude mcp add circuit -- npx circuit-mcp\n`));
56
+
57
+ console.log(chalk.dim(' ─────────────────────────────────────────\n'));
58
+ console.log(chalk.dim(' After setup, Circuit will prompt you to'));
59
+ console.log(chalk.dim(' authenticate on first use.\n'));
60
+ }
61
+
62
+ async function runAuth() {
63
+ showBanner();
64
+
65
+ const spinner = showSpinner('Authenticating...');
66
+
67
+ try {
68
+ const token = await authenticate();
69
+ spinner.stop();
70
+ showSuccess('Authenticated successfully!');
71
+ console.log(chalk.dim('\n Your token is stored locally.\n'));
72
+ } catch (err) {
73
+ spinner.stop();
74
+ showError(`Authentication failed: ${err.message}`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ async function runLogout() {
80
+ showBanner();
81
+
82
+ const { clearToken } = await import('./auth.js');
83
+ await clearToken();
84
+ showSuccess('Logged out successfully.');
85
+ }
86
+
87
+ async function runServer() {
88
+ // Check for existing token
89
+ let token = await getStoredToken();
90
+
91
+ if (!token) {
92
+ // First run - show banner and auth
93
+ showBanner();
94
+ console.log(chalk.dim(' First time setup - let\'s connect your account.\n'));
95
+
96
+ try {
97
+ token = await authenticate();
98
+ showSuccess('Connected!');
99
+ console.log();
100
+ } catch (err) {
101
+ showError(`Authentication failed: ${err.message}`);
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ // Start MCP server (stdio mode for Cursor/Claude)
107
+ await startMcpServer(token);
108
+ }
package/src/server.js ADDED
@@ -0,0 +1,286 @@
1
+ import { createInterface } from 'readline';
2
+
3
+ const CIRCUIT_API = 'https://app.withcircuit.com';
4
+
5
+ /**
6
+ * Start MCP server in stdio mode
7
+ * Communicates with Cursor/Claude via JSON-RPC over stdin/stdout
8
+ */
9
+ export async function startMcpServer(token) {
10
+ const rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ terminal: false
14
+ });
15
+
16
+ // Handle incoming JSON-RPC messages
17
+ rl.on('line', async (line) => {
18
+ try {
19
+ const message = JSON.parse(line);
20
+ const response = await handleMessage(message, token);
21
+ if (response) {
22
+ console.log(JSON.stringify(response));
23
+ }
24
+ } catch (err) {
25
+ // Send error response
26
+ console.log(JSON.stringify({
27
+ jsonrpc: '2.0',
28
+ error: { code: -32700, message: 'Parse error' },
29
+ id: null
30
+ }));
31
+ }
32
+ });
33
+
34
+ // Keep process alive
35
+ process.stdin.resume();
36
+ }
37
+
38
+ /**
39
+ * Handle incoming MCP message
40
+ */
41
+ async function handleMessage(message, token) {
42
+ const { id, method, params } = message;
43
+
44
+ switch (method) {
45
+ case 'initialize':
46
+ return {
47
+ jsonrpc: '2.0',
48
+ id,
49
+ result: {
50
+ protocolVersion: '2024-11-05',
51
+ serverInfo: {
52
+ name: 'circuit-mcp',
53
+ version: '1.0.0'
54
+ },
55
+ capabilities: {
56
+ tools: {},
57
+ resources: {}
58
+ }
59
+ }
60
+ };
61
+
62
+ case 'initialized':
63
+ // No response needed for notification
64
+ return null;
65
+
66
+ case 'tools/list':
67
+ return {
68
+ jsonrpc: '2.0',
69
+ id,
70
+ result: {
71
+ tools: [
72
+ {
73
+ name: 'get_priorities',
74
+ description: 'Get the top customer feedback priorities from Circuit',
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ limit: {
79
+ type: 'number',
80
+ description: 'Maximum number of priorities to return (default: 10)'
81
+ },
82
+ focus: {
83
+ type: 'string',
84
+ description: 'Focus type: volume, revenue, urgency, negative, positive, feature',
85
+ enum: ['volume', 'revenue', 'urgency', 'negative', 'positive', 'feature']
86
+ }
87
+ }
88
+ }
89
+ },
90
+ {
91
+ name: 'get_brief',
92
+ description: 'Get the engineering brief for a specific priority',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ priority_id: {
97
+ type: 'string',
98
+ description: 'The ID of the priority to get the brief for'
99
+ }
100
+ },
101
+ required: ['priority_id']
102
+ }
103
+ },
104
+ {
105
+ name: 'get_feedback',
106
+ description: 'Get raw customer feedback items',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ limit: {
111
+ type: 'number',
112
+ description: 'Maximum number of feedback items to return (default: 20)'
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ]
118
+ }
119
+ };
120
+
121
+ case 'tools/call':
122
+ return await handleToolCall(id, params, token);
123
+
124
+ case 'resources/list':
125
+ return {
126
+ jsonrpc: '2.0',
127
+ id,
128
+ result: { resources: [] }
129
+ };
130
+
131
+ case 'ping':
132
+ return {
133
+ jsonrpc: '2.0',
134
+ id,
135
+ result: {}
136
+ };
137
+
138
+ default:
139
+ return {
140
+ jsonrpc: '2.0',
141
+ id,
142
+ error: { code: -32601, message: `Method not found: ${method}` }
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Handle tool calls
149
+ */
150
+ async function handleToolCall(id, params, token) {
151
+ const { name, arguments: args } = params;
152
+
153
+ try {
154
+ let result;
155
+
156
+ switch (name) {
157
+ case 'get_priorities':
158
+ result = await fetchPriorities(token, args);
159
+ break;
160
+
161
+ case 'get_brief':
162
+ result = await fetchBrief(token, args);
163
+ break;
164
+
165
+ case 'get_feedback':
166
+ result = await fetchFeedback(token, args);
167
+ break;
168
+
169
+ default:
170
+ return {
171
+ jsonrpc: '2.0',
172
+ id,
173
+ error: { code: -32602, message: `Unknown tool: ${name}` }
174
+ };
175
+ }
176
+
177
+ return {
178
+ jsonrpc: '2.0',
179
+ id,
180
+ result: {
181
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
182
+ }
183
+ };
184
+
185
+ } catch (err) {
186
+ return {
187
+ jsonrpc: '2.0',
188
+ id,
189
+ error: { code: -32000, message: err.message }
190
+ };
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Fetch priorities from Circuit API
196
+ */
197
+ async function fetchPriorities(token, args = {}) {
198
+ const limit = args.limit || 10;
199
+ const focus = args.focus || 'volume';
200
+
201
+ const url = `${CIRCUIT_API}/api/focus?limit=${limit}&focus_type=${focus}`;
202
+
203
+ const response = await fetch(url, {
204
+ headers: { 'Authorization': `Bearer ${token}` }
205
+ });
206
+
207
+ if (!response.ok) {
208
+ throw new Error(`API error: ${response.status}`);
209
+ }
210
+
211
+ const data = await response.json();
212
+
213
+ // Format for readability
214
+ return (data.priorities || data.clusters || []).map((p, i) => ({
215
+ rank: i + 1,
216
+ id: p.cluster_id || p.id,
217
+ title: p.summary || p.theme,
218
+ category: p.category,
219
+ mentions: p.volume || p.count,
220
+ urgency: p.urgency_score,
221
+ trend: p.trend
222
+ }));
223
+ }
224
+
225
+ /**
226
+ * Fetch a specific brief from Circuit API
227
+ */
228
+ async function fetchBrief(token, args) {
229
+ if (!args.priority_id) {
230
+ throw new Error('priority_id is required');
231
+ }
232
+
233
+ const url = `${CIRCUIT_API}/api/builds?cluster_id=${args.priority_id}`;
234
+
235
+ const response = await fetch(url, {
236
+ headers: { 'Authorization': `Bearer ${token}` }
237
+ });
238
+
239
+ if (!response.ok) {
240
+ throw new Error(`API error: ${response.status}`);
241
+ }
242
+
243
+ const data = await response.json();
244
+ const build = (data.builds || []).find(b =>
245
+ b.cluster_id === args.priority_id || b.clusterId === args.priority_id
246
+ );
247
+
248
+ if (!build) {
249
+ throw new Error('Brief not found for this priority');
250
+ }
251
+
252
+ return {
253
+ id: build.id,
254
+ title: build.title,
255
+ status: build.status,
256
+ content: build.build || build.spec_content
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Fetch raw feedback from Circuit API
262
+ */
263
+ async function fetchFeedback(token, args = {}) {
264
+ const limit = args.limit || 20;
265
+
266
+ const url = `${CIRCUIT_API}/api/feedback/all?limit=${limit}`;
267
+
268
+ const response = await fetch(url, {
269
+ headers: { 'Authorization': `Bearer ${token}` }
270
+ });
271
+
272
+ if (!response.ok) {
273
+ throw new Error(`API error: ${response.status}`);
274
+ }
275
+
276
+ const data = await response.json();
277
+
278
+ return (data.feedback || []).map(f => ({
279
+ id: f.id,
280
+ text: f.text,
281
+ source: f.source,
282
+ sentiment: f.analysis?.sentiment,
283
+ intent: f.analysis?.intent,
284
+ created_at: f.created_at
285
+ }));
286
+ }
package/src/ui.js ADDED
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+
3
+ const CIRCUIT_PURPLE = '#6366F1';
4
+
5
+ export function showBanner() {
6
+ console.log();
7
+ console.log(chalk.hex(CIRCUIT_PURPLE).bold(' ╭──────────────────────────────────╮'));
8
+ console.log(chalk.hex(CIRCUIT_PURPLE).bold(' │') + chalk.white.bold(' ⚡ Circuit MCP ') + chalk.hex(CIRCUIT_PURPLE).bold('│'));
9
+ console.log(chalk.hex(CIRCUIT_PURPLE).bold(' ╰──────────────────────────────────╯'));
10
+ console.log();
11
+ }
12
+
13
+ export function showSuccess(message) {
14
+ console.log(chalk.green(' ✓ ') + chalk.white(message));
15
+ }
16
+
17
+ export function showError(message) {
18
+ console.log(chalk.red(' ✗ ') + chalk.white(message));
19
+ }
20
+
21
+ export function showInfo(message) {
22
+ console.log(chalk.blue(' ℹ ') + chalk.dim(message));
23
+ }
24
+
25
+ export function showStep(step, message) {
26
+ console.log(chalk.dim(` ${step}. `) + chalk.white(message));
27
+ }
28
+
29
+ export function showSpinner(message) {
30
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
31
+ let i = 0;
32
+ let stopped = false;
33
+
34
+ const interval = setInterval(() => {
35
+ if (stopped) return;
36
+ process.stderr.write(`\r ${chalk.hex(CIRCUIT_PURPLE)(frames[i])} ${chalk.dim(message)}`);
37
+ i = (i + 1) % frames.length;
38
+ }, 80);
39
+
40
+ return {
41
+ stop: () => {
42
+ stopped = true;
43
+ clearInterval(interval);
44
+ process.stderr.write('\r' + ' '.repeat(message.length + 10) + '\r');
45
+ },
46
+ update: (newMessage) => {
47
+ message = newMessage;
48
+ }
49
+ };
50
+ }
51
+
52
+ export function showPrompt(message) {
53
+ process.stdout.write(chalk.cyan(' → ') + chalk.white(message));
54
+ }
55
+
56
+ export function clearLine() {
57
+ process.stdout.write('\r\x1b[K');
58
+ }