circuit-mcp 1.0.7 → 1.0.10
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/package.json +1 -1
- package/src/auth.js +49 -19
- package/src/index.js +158 -16
- package/src/server.js +74 -127
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -148,39 +148,69 @@ function getSuccessPage() {
|
|
|
148
148
|
justify-content: center;
|
|
149
149
|
color: #1C1A18;
|
|
150
150
|
}
|
|
151
|
-
.
|
|
151
|
+
.card {
|
|
152
|
+
background: white;
|
|
153
|
+
border-radius: 16px;
|
|
154
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
|
155
|
+
padding: 48px 64px;
|
|
152
156
|
text-align: center;
|
|
153
|
-
|
|
157
|
+
max-width: 420px;
|
|
154
158
|
}
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
.logo {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
gap: 8px;
|
|
164
|
+
margin-bottom: 32px;
|
|
165
|
+
}
|
|
166
|
+
.logo-icon {
|
|
167
|
+
width: 28px;
|
|
168
|
+
height: 28px;
|
|
169
|
+
background: #1C1A18;
|
|
170
|
+
border-radius: 50%;
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
}
|
|
175
|
+
.logo-icon::after {
|
|
176
|
+
content: '';
|
|
177
|
+
width: 12px;
|
|
178
|
+
height: 12px;
|
|
179
|
+
border: 2px solid white;
|
|
180
|
+
border-radius: 50%;
|
|
181
|
+
}
|
|
182
|
+
.logo-text {
|
|
183
|
+
font-size: 16px;
|
|
184
|
+
font-weight: 500;
|
|
185
|
+
}
|
|
186
|
+
.woo {
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
color: rgba(28,26,24,0.5);
|
|
189
|
+
margin-bottom: 8px;
|
|
158
190
|
}
|
|
159
191
|
h1 {
|
|
160
|
-
font-size:
|
|
192
|
+
font-size: 28px;
|
|
161
193
|
font-weight: 600;
|
|
162
|
-
margin-bottom:
|
|
194
|
+
margin-bottom: 12px;
|
|
163
195
|
}
|
|
164
196
|
p {
|
|
165
|
-
font-size:
|
|
197
|
+
font-size: 15px;
|
|
166
198
|
color: rgba(28,26,24,0.6);
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
.hint {
|
|
170
|
-
margin-top: 24px;
|
|
171
|
-
font-size: 14px;
|
|
172
|
-
color: rgba(28,26,24,0.4);
|
|
199
|
+
line-height: 1.5;
|
|
173
200
|
}
|
|
174
201
|
</style>
|
|
175
202
|
</head>
|
|
176
203
|
<body>
|
|
177
|
-
<div class="
|
|
178
|
-
<div class="
|
|
204
|
+
<div class="card">
|
|
205
|
+
<div class="logo">
|
|
206
|
+
<div class="logo-icon"></div>
|
|
207
|
+
<span class="logo-text">Circuit</span>
|
|
208
|
+
</div>
|
|
209
|
+
<p class="woo">Woo hoo!</p>
|
|
179
210
|
<h1>Connected to Circuit</h1>
|
|
180
|
-
<p>You can close this window.</p>
|
|
181
|
-
<p class="hint">Return to your terminal to continue.</p>
|
|
211
|
+
<p>You can close this window to continue Circuiting.</p>
|
|
182
212
|
</div>
|
|
183
|
-
<script>setTimeout(() => window.close(),
|
|
213
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
184
214
|
</body>
|
|
185
215
|
</html>`;
|
|
186
216
|
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
2
7
|
import { authenticate, getStoredToken } from './auth.js';
|
|
3
8
|
import { startMcpServer } from './server.js';
|
|
4
9
|
import { showBanner, showSuccess, showError, showSpinner } from './ui.js';
|
|
@@ -47,7 +52,7 @@ function showHelp() {
|
|
|
47
52
|
|
|
48
53
|
console.log(chalk.white.bold(' Commands:\n'));
|
|
49
54
|
console.log(` ${chalk.cyan('npx circuit-mcp')} Start MCP server`);
|
|
50
|
-
console.log(` ${chalk.cyan('npx circuit-mcp setup')}
|
|
55
|
+
console.log(` ${chalk.cyan('npx circuit-mcp setup')} Configure Cursor or Claude Code`);
|
|
51
56
|
console.log(` ${chalk.cyan('npx circuit-mcp auth')} Re-authenticate`);
|
|
52
57
|
console.log(` ${chalk.cyan('npx circuit-mcp logout')} Clear stored token`);
|
|
53
58
|
console.log(` ${chalk.cyan('npx circuit-mcp fix')} Troubleshooting guide`);
|
|
@@ -110,34 +115,170 @@ function showTroubleshoot() {
|
|
|
110
115
|
console.log(chalk.dim(' Or visit: https://withcircuit.com/help\n'));
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
// Helper to prompt user for input
|
|
119
|
+
function prompt(question) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const rl = readline.createInterface({
|
|
122
|
+
input: process.stdin,
|
|
123
|
+
output: process.stdout
|
|
124
|
+
});
|
|
125
|
+
rl.question(question, (answer) => {
|
|
126
|
+
rl.close();
|
|
127
|
+
resolve(answer.trim());
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function showUsageGuide() {
|
|
133
|
+
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
134
|
+
console.log(chalk.white.bold(' How to use Circuit\n'));
|
|
135
|
+
console.log(chalk.dim(' Open Claude Code or Cursor and ask:\n'));
|
|
136
|
+
console.log(chalk.white(' "What are my top priorities?"'));
|
|
137
|
+
console.log(chalk.white(' "Show me the brief for priority #1"'));
|
|
138
|
+
console.log(chalk.white(' "What feedback mentions dark mode?"'));
|
|
139
|
+
console.log(chalk.white(' "What are my most urgent issues?"'));
|
|
140
|
+
console.log(chalk.white(' "Show priorities by revenue impact"'));
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.dim(' Circuit will fetch your real customer data.'));
|
|
143
|
+
console.log(chalk.dim(' Just ask naturally and your AI will handle the rest.\n'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Configure Cursor by writing to ~/.cursor/mcp.json
|
|
147
|
+
async function configureCursor() {
|
|
148
|
+
const configPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
149
|
+
const circuitConfig = {
|
|
150
|
+
command: 'npx',
|
|
151
|
+
args: ['circuit-mcp']
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Ensure .cursor directory exists
|
|
156
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
157
|
+
|
|
158
|
+
let config = { mcpServers: {} };
|
|
159
|
+
|
|
160
|
+
// Try to read existing config
|
|
161
|
+
try {
|
|
162
|
+
const existing = await fs.readFile(configPath, 'utf-8');
|
|
163
|
+
config = JSON.parse(existing);
|
|
164
|
+
if (!config.mcpServers) {
|
|
165
|
+
config.mcpServers = {};
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// File doesn't exist, use default
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add Circuit
|
|
172
|
+
config.mcpServers.circuit = circuitConfig;
|
|
173
|
+
|
|
174
|
+
// Write config
|
|
175
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
176
|
+
return true;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Configure Claude Code by running the CLI command
|
|
183
|
+
function configureClaudeCode() {
|
|
184
|
+
try {
|
|
185
|
+
execSync('claude mcp add circuit -- npx circuit-mcp', {
|
|
186
|
+
stdio: 'pipe',
|
|
187
|
+
timeout: 10000
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Command might fail if claude CLI not installed
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
113
196
|
async function runSetup() {
|
|
114
197
|
showBanner();
|
|
115
198
|
|
|
116
199
|
console.log(chalk.white.bold(' Welcome to Circuit!\n'));
|
|
117
|
-
console.log(chalk.dim('
|
|
200
|
+
console.log(chalk.dim(' Let\'s connect Circuit to your AI coding tool.\n'));
|
|
118
201
|
|
|
119
202
|
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
120
203
|
|
|
121
|
-
console.log(chalk.
|
|
122
|
-
console.log(chalk.
|
|
123
|
-
console.log(chalk.white('
|
|
124
|
-
console.log(chalk.dim('
|
|
125
|
-
|
|
204
|
+
console.log(chalk.white(' Which tool are you using?\n'));
|
|
205
|
+
console.log(chalk.white(' 1. Cursor'));
|
|
206
|
+
console.log(chalk.white(' 2. Claude Code'));
|
|
207
|
+
console.log(chalk.dim(' 3. Both\n'));
|
|
208
|
+
|
|
209
|
+
const choice = await prompt(chalk.cyan(' Enter 1, 2, or 3: '));
|
|
210
|
+
|
|
211
|
+
console.log();
|
|
212
|
+
|
|
213
|
+
if (choice === '1' || choice === '3') {
|
|
214
|
+
console.log(chalk.dim(' Setting up Cursor...\n'));
|
|
215
|
+
|
|
216
|
+
const success = await configureCursor();
|
|
217
|
+
|
|
218
|
+
if (success) {
|
|
219
|
+
showSuccess('Added Circuit to ~/.cursor/mcp.json');
|
|
220
|
+
console.log(chalk.dim(' Restart Cursor to activate.\n'));
|
|
221
|
+
} else {
|
|
222
|
+
showError('Could not update Cursor config automatically.');
|
|
223
|
+
console.log(chalk.dim(' Add this to ~/.cursor/mcp.json manually:\n'));
|
|
224
|
+
console.log(chalk.white(` {
|
|
126
225
|
"mcpServers": {
|
|
127
|
-
"circuit": {
|
|
128
|
-
"command": "npx",
|
|
129
|
-
"args": ["circuit-mcp"]
|
|
130
|
-
}
|
|
226
|
+
"circuit": { "command": "npx", "args": ["circuit-mcp"] }
|
|
131
227
|
}
|
|
132
228
|
}\n`));
|
|
133
|
-
|
|
229
|
+
}
|
|
230
|
+
}
|
|
134
231
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
232
|
+
if (choice === '2' || choice === '3') {
|
|
233
|
+
console.log(chalk.dim(' Setting up Claude Code...\n'));
|
|
234
|
+
|
|
235
|
+
const success = configureClaudeCode();
|
|
236
|
+
|
|
237
|
+
if (success) {
|
|
238
|
+
showSuccess('Added Circuit to Claude Code');
|
|
239
|
+
console.log(chalk.dim(' Restart Claude Code to activate.\n'));
|
|
240
|
+
} else {
|
|
241
|
+
showError('Could not run claude CLI automatically.');
|
|
242
|
+
console.log(chalk.dim(' Run this command manually:\n'));
|
|
243
|
+
console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!['1', '2', '3'].includes(choice)) {
|
|
248
|
+
console.log(chalk.dim(' No worries! Here are the manual steps:\n'));
|
|
249
|
+
console.log(chalk.cyan.bold(' Cursor'));
|
|
250
|
+
console.log(chalk.dim(' Add to ~/.cursor/mcp.json:'));
|
|
251
|
+
console.log(chalk.white(' { "mcpServers": { "circuit": { "command": "npx", "args": ["circuit-mcp"] } } }\n'));
|
|
252
|
+
console.log(chalk.cyan.bold(' Claude Code'));
|
|
253
|
+
console.log(chalk.dim(' Run:'));
|
|
254
|
+
console.log(chalk.white(' claude mcp add circuit -- npx circuit-mcp\n'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
259
|
+
console.log(chalk.white.bold(' Almost done!\n'));
|
|
260
|
+
console.log(chalk.dim(' Now let\'s connect your Circuit account.\n'));
|
|
261
|
+
|
|
262
|
+
const authChoice = await prompt(chalk.cyan(' Sign in now? (Y/n): '));
|
|
263
|
+
|
|
264
|
+
if (authChoice.toLowerCase() !== 'n') {
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(chalk.dim(' Opening browser to sign in...\n'));
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const token = await authenticate();
|
|
270
|
+
showSuccess('Signed in to Circuit');
|
|
271
|
+
console.log();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
showError('Could not sign in automatically.');
|
|
274
|
+
console.log(chalk.dim(' Run this later: npx circuit-mcp auth\n'));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
138
277
|
|
|
139
278
|
console.log(chalk.dim(' ─────────────────────────────────────────\n'));
|
|
140
|
-
console.log(chalk.
|
|
279
|
+
console.log(chalk.white.bold(' You\'re all set!\n'));
|
|
280
|
+
console.log(chalk.dim(' Restart your editor and start using Circuit.\n'));
|
|
281
|
+
showUsageGuide();
|
|
141
282
|
}
|
|
142
283
|
|
|
143
284
|
async function runAuth() {
|
|
@@ -150,6 +291,7 @@ async function runAuth() {
|
|
|
150
291
|
spinner.stop();
|
|
151
292
|
showSuccess('Authenticated successfully!');
|
|
152
293
|
console.log(chalk.dim('\n Your token is stored locally.\n'));
|
|
294
|
+
showUsageGuide();
|
|
153
295
|
} catch (err) {
|
|
154
296
|
spinner.stop();
|
|
155
297
|
showError(`Authentication failed: ${err.message}`);
|
package/src/server.js
CHANGED
|
@@ -35,6 +35,30 @@ export async function startMcpServer(token) {
|
|
|
35
35
|
process.stdin.resume();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Call Circuit MCP backend API
|
|
40
|
+
*/
|
|
41
|
+
async function callMcpApi(token, toolName, args = {}) {
|
|
42
|
+
const response = await fetch(`${CIRCUIT_API}/mcp/call`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Authorization': `Bearer ${token}`,
|
|
46
|
+
'Content-Type': 'application/json'
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
tool: toolName,
|
|
50
|
+
arguments: args
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return response.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
/**
|
|
39
63
|
* Handle incoming MCP message
|
|
40
64
|
*/
|
|
@@ -71,47 +95,83 @@ async function handleMessage(message, token) {
|
|
|
71
95
|
tools: [
|
|
72
96
|
{
|
|
73
97
|
name: 'get_priorities',
|
|
74
|
-
description: 'Get
|
|
98
|
+
description: 'Get your top priorities from Circuit, ranked by customer volume. These are the features/fixes that matter most to your users.',
|
|
75
99
|
inputSchema: {
|
|
76
100
|
type: 'object',
|
|
77
101
|
properties: {
|
|
78
102
|
limit: {
|
|
79
103
|
type: 'number',
|
|
80
|
-
description: '
|
|
104
|
+
description: 'Number of priorities to return (default: 5, max: 20)'
|
|
81
105
|
},
|
|
82
|
-
|
|
106
|
+
sort_by: {
|
|
83
107
|
type: 'string',
|
|
84
|
-
description: '
|
|
85
|
-
enum: ['volume', '
|
|
108
|
+
description: 'Sort criterion: volume, urgency, revenue, negative, positive',
|
|
109
|
+
enum: ['volume', 'urgency', 'revenue', 'negative', 'positive']
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
},
|
|
90
114
|
{
|
|
91
115
|
name: 'get_brief',
|
|
92
|
-
description: 'Get the engineering brief for a specific priority',
|
|
116
|
+
description: 'Get the full engineering brief for a specific priority. Includes what to build, why it matters, customer quotes, files to touch, and done criteria.',
|
|
93
117
|
inputSchema: {
|
|
94
118
|
type: 'object',
|
|
95
119
|
properties: {
|
|
96
120
|
priority_id: {
|
|
97
121
|
type: 'string',
|
|
98
|
-
description: 'The
|
|
122
|
+
description: 'The priority ID (from get_priorities)'
|
|
123
|
+
},
|
|
124
|
+
build_id: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'The build ID directly (alternative to priority_id)'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'start_building',
|
|
133
|
+
description: "Mark a brief as 'building' - you're actively working on this feature.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
build_id: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'The build ID to mark as building'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
required: ['build_id']
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'mark_done',
|
|
147
|
+
description: "Mark a brief as 'done' - the feature has shipped! This closes the feedback loop.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
build_id: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'The build ID to mark as done'
|
|
99
154
|
}
|
|
100
155
|
},
|
|
101
|
-
required: ['
|
|
156
|
+
required: ['build_id']
|
|
102
157
|
}
|
|
103
158
|
},
|
|
104
159
|
{
|
|
105
|
-
name: '
|
|
106
|
-
description: '
|
|
160
|
+
name: 'search_feedback',
|
|
161
|
+
description: 'Search customer feedback by keyword. Useful for understanding pain points around specific features.',
|
|
107
162
|
inputSchema: {
|
|
108
163
|
type: 'object',
|
|
109
164
|
properties: {
|
|
165
|
+
query: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'Search query (keywords or phrase)'
|
|
168
|
+
},
|
|
110
169
|
limit: {
|
|
111
170
|
type: 'number',
|
|
112
|
-
description: '
|
|
171
|
+
description: 'Max results (default: 10)'
|
|
113
172
|
}
|
|
114
|
-
}
|
|
173
|
+
},
|
|
174
|
+
required: ['query']
|
|
115
175
|
}
|
|
116
176
|
}
|
|
117
177
|
]
|
|
@@ -151,28 +211,8 @@ async function handleToolCall(id, params, token) {
|
|
|
151
211
|
const { name, arguments: args } = params;
|
|
152
212
|
|
|
153
213
|
try {
|
|
154
|
-
|
|
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
|
-
}
|
|
214
|
+
// Call the Circuit MCP backend
|
|
215
|
+
const result = await callMcpApi(token, name, args || {});
|
|
176
216
|
|
|
177
217
|
return {
|
|
178
218
|
jsonrpc: '2.0',
|
|
@@ -191,96 +231,3 @@ async function handleToolCall(id, params, token) {
|
|
|
191
231
|
}
|
|
192
232
|
}
|
|
193
233
|
|
|
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
|
-
}
|