circuit-mcp 2.0.1 → 2.3.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 +107 -117
- package/package.json +1 -1
- package/preview-error.html +57 -0
- package/preview-success.html +57 -0
- package/src/auth.js +74 -63
- package/src/server.js +348 -263
package/src/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
2
|
|
|
3
|
-
const CIRCUIT_API = 'https://
|
|
3
|
+
const CIRCUIT_API = process.env.CIRCUIT_API_URL || 'https://api.withcircuit.com';
|
|
4
|
+
const API_TIMEOUT_MS = 30_000;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Start MCP server in stdio mode
|
|
@@ -13,7 +14,6 @@ export async function startMcpServer(token) {
|
|
|
13
14
|
terminal: false
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
// Handle incoming JSON-RPC messages
|
|
17
17
|
rl.on('line', async (line) => {
|
|
18
18
|
try {
|
|
19
19
|
const message = JSON.parse(line);
|
|
@@ -22,7 +22,6 @@ export async function startMcpServer(token) {
|
|
|
22
22
|
console.log(JSON.stringify(response));
|
|
23
23
|
}
|
|
24
24
|
} catch (err) {
|
|
25
|
-
// Send error response
|
|
26
25
|
console.log(JSON.stringify({
|
|
27
26
|
jsonrpc: '2.0',
|
|
28
27
|
error: { code: -32700, message: 'Parse error' },
|
|
@@ -31,32 +30,45 @@ export async function startMcpServer(token) {
|
|
|
31
30
|
}
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
// Keep process alive
|
|
35
33
|
process.stdin.resume();
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* Call Circuit
|
|
37
|
+
* Call Circuit backend API with timeout
|
|
40
38
|
*/
|
|
41
39
|
async function callMcpApi(token, toolName, args = {}) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
headers: {
|
|
45
|
-
'Authorization': `Bearer ${token}`,
|
|
46
|
-
'Content-Type': 'application/json'
|
|
47
|
-
},
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
tool: toolName,
|
|
50
|
-
arguments: args
|
|
51
|
-
})
|
|
52
|
-
});
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(`${CIRCUIT_API}/mcp/call`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Authorization': `Bearer ${token}`,
|
|
48
|
+
'Content-Type': 'application/json'
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ tool: toolName, arguments: args }),
|
|
51
|
+
signal: controller.signal
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response.status === 401) {
|
|
55
|
+
throw new Error('Token expired. Run: npx circuit-mcp auth');
|
|
56
|
+
}
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
throw new Error(`API error ${response.status}: ${text}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response.json();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.name === 'AbortError') {
|
|
66
|
+
throw new Error('Request timed out. Check your connection and try again.');
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/**
|
|
@@ -72,14 +84,8 @@ async function handleMessage(message, token) {
|
|
|
72
84
|
id,
|
|
73
85
|
result: {
|
|
74
86
|
protocolVersion: '2024-11-05',
|
|
75
|
-
serverInfo: {
|
|
76
|
-
|
|
77
|
-
version: '2.0.0'
|
|
78
|
-
},
|
|
79
|
-
capabilities: {
|
|
80
|
-
tools: {},
|
|
81
|
-
resources: {}
|
|
82
|
-
}
|
|
87
|
+
serverInfo: { name: 'circuit-mcp', version: '2.2.0' },
|
|
88
|
+
capabilities: { tools: {}, resources: {} }
|
|
83
89
|
}
|
|
84
90
|
};
|
|
85
91
|
|
|
@@ -87,142 +93,16 @@ async function handleMessage(message, token) {
|
|
|
87
93
|
return null;
|
|
88
94
|
|
|
89
95
|
case 'tools/list':
|
|
90
|
-
return {
|
|
91
|
-
jsonrpc: '2.0',
|
|
92
|
-
id,
|
|
93
|
-
result: {
|
|
94
|
-
tools: [
|
|
95
|
-
{
|
|
96
|
-
name: 'circuit.priorities',
|
|
97
|
-
description: 'What should I work on? Get ranked priorities with confidence indicators, trends, and memory context.',
|
|
98
|
-
inputSchema: {
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
lens: {
|
|
102
|
-
type: 'string',
|
|
103
|
-
description: "Focus lens: 'volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'",
|
|
104
|
-
enum: ['volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'],
|
|
105
|
-
default: 'volume'
|
|
106
|
-
},
|
|
107
|
-
segment: {
|
|
108
|
-
type: 'string',
|
|
109
|
-
description: "Filter by customer segment: 'enterprise', 'smb', 'all'",
|
|
110
|
-
default: 'all'
|
|
111
|
-
},
|
|
112
|
-
limit: {
|
|
113
|
-
type: 'number',
|
|
114
|
-
description: 'Number of priorities (default: 5, max: 20)',
|
|
115
|
-
default: 5
|
|
116
|
-
},
|
|
117
|
-
category: {
|
|
118
|
-
type: 'string',
|
|
119
|
-
description: "Filter by category: 'Bug', 'Feature', 'Friction', 'Complaint', 'Praise'",
|
|
120
|
-
enum: ['Bug', 'Feature', 'Friction', 'Complaint', 'Praise']
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'circuit.brief',
|
|
127
|
-
description: 'Get the full engineering spec for a priority. Includes brief content, customer context, version history, and related memory (previous ships, outcomes).',
|
|
128
|
-
inputSchema: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: {
|
|
131
|
-
priority_id: {
|
|
132
|
-
type: 'string',
|
|
133
|
-
description: 'The priority ID'
|
|
134
|
-
},
|
|
135
|
-
build_id: {
|
|
136
|
-
type: 'string',
|
|
137
|
-
description: 'The build ID directly (alternative to priority_id)'
|
|
138
|
-
},
|
|
139
|
-
include_history: {
|
|
140
|
-
type: 'boolean',
|
|
141
|
-
description: 'Include version history and related memory',
|
|
142
|
-
default: true
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: 'circuit.act',
|
|
149
|
-
description: 'Take an action in Circuit. Ship a brief, start building, correct a classification, or submit new feedback.',
|
|
150
|
-
inputSchema: {
|
|
151
|
-
type: 'object',
|
|
152
|
-
properties: {
|
|
153
|
-
action: {
|
|
154
|
-
type: 'string',
|
|
155
|
-
description: "Action to take: 'build' (start building), 'ship' (mark shipped), 'correct' (fix classification), 'submit' (add feedback)",
|
|
156
|
-
enum: ['build', 'ship', 'correct', 'submit']
|
|
157
|
-
},
|
|
158
|
-
brief_id: {
|
|
159
|
-
type: 'string',
|
|
160
|
-
description: "Brief ID (for 'build' and 'ship' actions)"
|
|
161
|
-
},
|
|
162
|
-
priority_id: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
description: "Priority ID (for 'correct' action)"
|
|
165
|
-
},
|
|
166
|
-
correction_type: {
|
|
167
|
-
type: 'string',
|
|
168
|
-
description: "What to correct (for 'correct' action): 'category'",
|
|
169
|
-
enum: ['category']
|
|
170
|
-
},
|
|
171
|
-
original: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
description: 'Original value being corrected'
|
|
174
|
-
},
|
|
175
|
-
corrected: {
|
|
176
|
-
type: 'string',
|
|
177
|
-
description: 'New corrected value'
|
|
178
|
-
},
|
|
179
|
-
feedback: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: "Feedback text (for 'submit' action)"
|
|
182
|
-
},
|
|
183
|
-
source: {
|
|
184
|
-
type: 'string',
|
|
185
|
-
description: "Feedback source (for 'submit' action)",
|
|
186
|
-
default: 'mcp'
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
required: ['action']
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: 'circuit.ask',
|
|
194
|
-
description: 'Ask anything about your feedback data. Searches across feedback, priorities, briefs, and help articles using semantic search.',
|
|
195
|
-
inputSchema: {
|
|
196
|
-
type: 'object',
|
|
197
|
-
properties: {
|
|
198
|
-
question: {
|
|
199
|
-
type: 'string',
|
|
200
|
-
description: "Natural language question (e.g., 'What are enterprise customers complaining about?', 'How do briefs work?')"
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
required: ['question']
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
};
|
|
96
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
209
97
|
|
|
210
98
|
case 'tools/call':
|
|
211
99
|
return await handleToolCall(id, params, token);
|
|
212
100
|
|
|
213
101
|
case 'resources/list':
|
|
214
|
-
return {
|
|
215
|
-
jsonrpc: '2.0',
|
|
216
|
-
id,
|
|
217
|
-
result: { resources: [] }
|
|
218
|
-
};
|
|
102
|
+
return { jsonrpc: '2.0', id, result: { resources: [] } };
|
|
219
103
|
|
|
220
104
|
case 'ping':
|
|
221
|
-
return {
|
|
222
|
-
jsonrpc: '2.0',
|
|
223
|
-
id,
|
|
224
|
-
result: {}
|
|
225
|
-
};
|
|
105
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
226
106
|
|
|
227
107
|
default:
|
|
228
108
|
return {
|
|
@@ -233,96 +113,264 @@ async function handleMessage(message, token) {
|
|
|
233
113
|
}
|
|
234
114
|
}
|
|
235
115
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
116
|
+
// ─────────────────────────────────────────────────────────────
|
|
117
|
+
// Tool Definitions
|
|
118
|
+
// ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
const TOOLS = [
|
|
121
|
+
{
|
|
122
|
+
name: 'circuit.priorities',
|
|
123
|
+
description: 'What should I work on? Ranked customer priorities with trend data, confidence indicators, and pattern matching. Set weekly: true to get the weekly digest instead.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
weekly: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Set to true to get the weekly digest: priority movements, new entries, dropped items, and volume spikes compared to last week'
|
|
130
|
+
},
|
|
131
|
+
lens: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: "How to rank: 'volume' (most users), 'urgency' (bugs & quality), 'revenue' (revenue impact), 'retention' (negative sentiment), 'delight' (positive sentiment), 'feature' (new feature requests)",
|
|
134
|
+
enum: ['volume', 'urgency', 'revenue', 'retention', 'delight', 'feature'],
|
|
135
|
+
default: 'volume'
|
|
136
|
+
},
|
|
137
|
+
segment: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: "Customer segment filter: 'enterprise', 'smb', 'all'",
|
|
140
|
+
default: 'all'
|
|
141
|
+
},
|
|
142
|
+
limit: {
|
|
143
|
+
type: 'number',
|
|
144
|
+
description: 'Number of priorities to return (default: 5, max: 20)',
|
|
145
|
+
default: 5
|
|
146
|
+
},
|
|
147
|
+
category: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: "Filter by feedback category",
|
|
150
|
+
enum: ['Bug', 'Feature', 'Improvement', 'Praise']
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'circuit.spec',
|
|
157
|
+
description: 'Full engineering spec for a priority. 5 sections: What to Build, Why It Matters, Customer Voice, Files to Touch, Done When. Set batch: true to export multiple specs as markdown.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
priority_id: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'The priority ID (from circuit.priorities)'
|
|
164
|
+
},
|
|
165
|
+
spec_id: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The spec ID directly (alternative to priority_id)'
|
|
168
|
+
},
|
|
169
|
+
include_history: {
|
|
170
|
+
type: 'boolean',
|
|
171
|
+
description: 'Include version history and related ship memory',
|
|
172
|
+
default: true
|
|
173
|
+
},
|
|
174
|
+
batch: {
|
|
175
|
+
type: 'boolean',
|
|
176
|
+
description: 'Set to true to export multiple specs as markdown for sprint planning'
|
|
177
|
+
},
|
|
178
|
+
spec_ids: {
|
|
179
|
+
type: 'array',
|
|
180
|
+
items: { type: 'string' },
|
|
181
|
+
description: 'Specific spec IDs to export (batch mode). If empty, exports recent specs.'
|
|
182
|
+
},
|
|
183
|
+
status: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: "Filter by status (batch mode): 'ready', 'building', 'shipped'",
|
|
186
|
+
enum: ['ready', 'building', 'shipped', 'done']
|
|
187
|
+
},
|
|
188
|
+
limit: {
|
|
189
|
+
type: 'number',
|
|
190
|
+
description: 'Number of specs to export in batch mode (default: 10, max: 50)',
|
|
191
|
+
default: 10
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'circuit.act',
|
|
198
|
+
description: "Take action. Start building, ship it, share back with customers, assign, correct a classification, submit feedback, or submit a transcript.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
action: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
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)",
|
|
205
|
+
enum: ['build', 'ship', 'share', 'assign', 'correct', 'submit', 'transcript']
|
|
206
|
+
},
|
|
207
|
+
spec_id: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: "Spec ID (for build, ship, share, assign)"
|
|
210
|
+
},
|
|
211
|
+
channel: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: "Notification channel for share: 'email', 'widget', 'both', 'skip'",
|
|
214
|
+
enum: ['email', 'widget', 'both', 'skip']
|
|
215
|
+
},
|
|
216
|
+
message: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: "Custom message to include in share notifications (optional)"
|
|
219
|
+
},
|
|
220
|
+
assigned_to: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: "Team member email or user ID (for assign). Empty to unassign."
|
|
223
|
+
},
|
|
224
|
+
priority_id: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: "Priority ID (for correct)"
|
|
227
|
+
},
|
|
228
|
+
correction_type: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: "What to correct: 'category'",
|
|
231
|
+
enum: ['category']
|
|
232
|
+
},
|
|
233
|
+
original: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Original value being corrected'
|
|
236
|
+
},
|
|
237
|
+
corrected: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'New corrected value'
|
|
240
|
+
},
|
|
241
|
+
feedback: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
description: "Feedback text (for submit)"
|
|
244
|
+
},
|
|
245
|
+
source: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: "Feedback source (for submit)",
|
|
248
|
+
default: 'mcp'
|
|
249
|
+
},
|
|
250
|
+
text: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Full transcript text, min 50 chars (for transcript)'
|
|
253
|
+
},
|
|
254
|
+
title: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: "Transcript title, e.g. 'Sales call with Acme Corp' (for transcript)"
|
|
257
|
+
},
|
|
258
|
+
type: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: "Transcript type (for transcript)",
|
|
261
|
+
enum: ['interview', 'sales_call', 'support', 'other']
|
|
262
|
+
},
|
|
263
|
+
customer_name: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Customer name (for transcript, optional)'
|
|
266
|
+
},
|
|
267
|
+
customer_email: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'Customer email (for transcript, optional)'
|
|
270
|
+
},
|
|
271
|
+
revenue_band: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: "Customer segment (for transcript): 'enterprise', 'paid', 'free'",
|
|
274
|
+
enum: ['enterprise', 'paid', 'free']
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
required: ['action']
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'circuit.ask',
|
|
282
|
+
description: 'Search across all your feedback data. Semantic search over feedback, priorities, briefs, and help articles. Also returns your shipping patterns and behavioral insights.',
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
question: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: "Natural language question, e.g. 'What are enterprise customers complaining about?'"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
required: ['question']
|
|
292
|
+
}
|
|
242
293
|
}
|
|
294
|
+
];
|
|
243
295
|
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
// ─────────────────────────────────────────────────────────────
|
|
297
|
+
// Response Formatting
|
|
298
|
+
// ─────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
function formatPriorities(data) {
|
|
301
|
+
// Weekly digest mode — detected by presence of movements/new_entries/headline
|
|
302
|
+
if (data.headline !== undefined || data.movements !== undefined) {
|
|
303
|
+
return formatWeeklyDigest(data);
|
|
246
304
|
}
|
|
247
305
|
|
|
306
|
+
if (data.message) return data.message;
|
|
307
|
+
if (!data.priorities?.length) return 'No priorities found. Upload feedback to get started.';
|
|
308
|
+
|
|
248
309
|
const lines = [];
|
|
249
310
|
|
|
250
311
|
if (data.memory_applied) {
|
|
251
|
-
lines.push(
|
|
312
|
+
lines.push(`*${data.ships_count} ships tracked · segment: ${data.segment_affinity || 'mixed'}*\n`);
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
for (const p of data.priorities) {
|
|
255
|
-
let trendText = '';
|
|
256
|
-
if (p.trend === 'up') {
|
|
257
|
-
trendText = ` ↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
|
|
258
|
-
} else if (p.trend === 'down') {
|
|
259
|
-
trendText = ` ↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
316
|
lines.push(`**#${p.rank}. ${p.theme}**`);
|
|
263
317
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (trendText) badges.push(trendText.trim());
|
|
318
|
+
const meta = [p.category || 'Other', `${p.volume} users`];
|
|
319
|
+
if (p.trend === 'up') meta.push(`↑${p.trend_percent ? ` ${p.trend_percent}%` : ''}`);
|
|
320
|
+
if (p.trend === 'down') meta.push(`↓${p.trend_percent ? ` ${p.trend_percent}%` : ''}`);
|
|
268
321
|
if (p.brief_status && p.brief_status !== 'no_brief') {
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
}
|
|
272
|
-
if (p.matches_pattern) badges.push('matches pattern');
|
|
273
|
-
if (p.version) badges.push(p.version);
|
|
274
|
-
|
|
275
|
-
lines.push(badges.join(' · '));
|
|
276
|
-
|
|
277
|
-
if (p.key_quote) {
|
|
278
|
-
lines.push(`> "${p.key_quote}"`);
|
|
322
|
+
const label = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[p.brief_status];
|
|
323
|
+
if (label) meta.push(label);
|
|
279
324
|
}
|
|
325
|
+
if (p.matches_pattern) meta.push('matches pattern');
|
|
326
|
+
if (p.version) meta.push(p.version);
|
|
327
|
+
lines.push(meta.join(' · '));
|
|
280
328
|
|
|
281
|
-
lines.push(
|
|
329
|
+
if (p.key_quote) lines.push(`> "${p.key_quote}"`);
|
|
330
|
+
lines.push(`priority_id: \`${p.priority_id}\`${p.build_id ? ` · spec_id: \`${p.build_id}\`` : ''}`);
|
|
282
331
|
lines.push('');
|
|
283
332
|
}
|
|
284
333
|
|
|
285
334
|
lines.push('---');
|
|
286
|
-
lines.push('Use `circuit.
|
|
335
|
+
lines.push('Use `circuit.spec` with a priority_id to get the full spec.');
|
|
287
336
|
|
|
288
337
|
return lines.join('\n');
|
|
289
338
|
}
|
|
290
339
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
340
|
+
function formatSpec(data) {
|
|
341
|
+
// Batch export mode — detected by presence of markdown/count
|
|
342
|
+
if (data.markdown !== undefined) {
|
|
343
|
+
return formatBatchExport(data);
|
|
344
|
+
}
|
|
345
|
+
|
|
295
346
|
if (data.error) {
|
|
296
347
|
if (data.error === 'no_brief' && data.priority) {
|
|
297
348
|
const p = data.priority;
|
|
298
|
-
|
|
299
|
-
output += `${p.category || 'Other'} · ${p.volume || 0} users\n\n`;
|
|
300
|
-
output += `**No brief generated yet.**\n\n`;
|
|
301
|
-
if (data.suggestion) {
|
|
302
|
-
output += `${data.suggestion}\n`;
|
|
303
|
-
}
|
|
304
|
-
return output;
|
|
349
|
+
return `# ${p.theme || 'Priority'}\n\n${p.category || 'Other'} · ${p.volume || 0} users\n\n**No brief generated yet.**\n\n${data.suggestion || ''}`;
|
|
305
350
|
}
|
|
306
351
|
return `Error: ${data.message || data.error}`;
|
|
307
352
|
}
|
|
308
353
|
|
|
309
354
|
const spec = data.spec_content || '';
|
|
310
|
-
let output = '';
|
|
311
|
-
|
|
312
355
|
const title = data.title || 'Engineering Brief';
|
|
313
|
-
|
|
356
|
+
const status = { ready: 'Ready', building: 'Building', shipped: 'Shipped' }[data.status] || data.status;
|
|
357
|
+
const version = data.version_badge ? ` · ${data.version_badge}` : '';
|
|
314
358
|
|
|
315
|
-
|
|
316
|
-
const versionBadge = data.version_badge ? ` · ${data.version_badge}` : '';
|
|
317
|
-
output += `Status: ${statusText}${versionBadge}\n`;
|
|
359
|
+
let output = `# ${title}\n\nStatus: ${status}${version}\n`;
|
|
318
360
|
|
|
319
361
|
if (data.customer_context) {
|
|
320
362
|
const ctx = data.customer_context;
|
|
321
363
|
output += `${ctx.category} · ${ctx.volume} users · ${ctx.paying_percent}% paying\n`;
|
|
322
364
|
}
|
|
365
|
+
|
|
366
|
+
if (data.effort) {
|
|
367
|
+
const e = data.effort;
|
|
368
|
+
output += `Effort: ${e.files} file${e.files !== 1 ? 's' : ''} · ${e.label}\n`;
|
|
369
|
+
}
|
|
370
|
+
|
|
323
371
|
output += '\n';
|
|
324
372
|
|
|
325
|
-
|
|
373
|
+
output += spec
|
|
326
374
|
.replace(/<what_to_build>/gi, '## WHAT TO BUILD\n')
|
|
327
375
|
.replace(/<\/what_to_build>/gi, '\n')
|
|
328
376
|
.replace(/<why_it_matters>/gi, '## WHY IT MATTERS\n')
|
|
@@ -334,39 +382,31 @@ function formatBrief(data) {
|
|
|
334
382
|
.replace(/<done_when>/gi, '## DONE WHEN\n')
|
|
335
383
|
.replace(/<\/done_when>/gi, '\n');
|
|
336
384
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (data.related_memory && data.related_memory.length > 0) {
|
|
385
|
+
if (data.related_memory?.length > 0) {
|
|
340
386
|
output += '\n## WHAT CIRCUIT REMEMBERS\n\n';
|
|
341
387
|
for (const mem of data.related_memory) {
|
|
342
|
-
|
|
343
|
-
output += `- Shipped: ${mem.theme || mem.summary || 'Related feature'}\n`;
|
|
344
|
-
} else if (mem.type === 'correction') {
|
|
345
|
-
output += `- Correction: ${mem.summary || 'Classification adjusted'}\n`;
|
|
346
|
-
} else {
|
|
347
|
-
output += `- ${mem.summary || JSON.stringify(mem)}\n`;
|
|
348
|
-
}
|
|
388
|
+
output += `- ${mem.summary || mem.theme || JSON.stringify(mem)}\n`;
|
|
349
389
|
}
|
|
350
390
|
}
|
|
351
391
|
|
|
352
|
-
output += `\n---\
|
|
353
|
-
output += `build_id: \`${data.build_id}\``;
|
|
392
|
+
output += `\n---\nspec_id: \`${data.build_id}\``;
|
|
354
393
|
|
|
355
394
|
return output;
|
|
356
395
|
}
|
|
357
396
|
|
|
358
|
-
/**
|
|
359
|
-
* Format act response
|
|
360
|
-
*/
|
|
361
397
|
function formatAct(data) {
|
|
362
|
-
if (data.error) {
|
|
363
|
-
return `Error: ${data.error}`;
|
|
364
|
-
}
|
|
398
|
+
if (data.error) return `Error: ${data.error}`;
|
|
365
399
|
|
|
366
400
|
if (data.success) {
|
|
367
401
|
let output = data.message;
|
|
368
402
|
if (data.memory_created) {
|
|
369
|
-
output += '\nShip memory recorded
|
|
403
|
+
output += '\nShip memory recorded. Circuit will remember this for future specs.';
|
|
404
|
+
}
|
|
405
|
+
if (data.transcript_id) {
|
|
406
|
+
output += `\n\ntranscript_id: \`${data.transcript_id}\``;
|
|
407
|
+
}
|
|
408
|
+
if (data.spec_id && data.action === 'share') {
|
|
409
|
+
output += `\n\nspec_id: \`${data.spec_id}\``;
|
|
370
410
|
}
|
|
371
411
|
return output;
|
|
372
412
|
}
|
|
@@ -374,17 +414,12 @@ function formatAct(data) {
|
|
|
374
414
|
return JSON.stringify(data, null, 2);
|
|
375
415
|
}
|
|
376
416
|
|
|
377
|
-
/**
|
|
378
|
-
* Format ask response
|
|
379
|
-
*/
|
|
380
417
|
function formatAsk(data) {
|
|
381
|
-
if (data.message)
|
|
382
|
-
return data.message;
|
|
383
|
-
}
|
|
418
|
+
if (data.message) return data.message;
|
|
384
419
|
|
|
385
420
|
const lines = [];
|
|
386
421
|
|
|
387
|
-
if (data.help_articles
|
|
422
|
+
if (data.help_articles?.length > 0) {
|
|
388
423
|
lines.push('**Help Articles:**');
|
|
389
424
|
for (const a of data.help_articles) {
|
|
390
425
|
lines.push(`- **${a.title}**: ${a.content}`);
|
|
@@ -392,15 +427,15 @@ function formatAsk(data) {
|
|
|
392
427
|
lines.push('');
|
|
393
428
|
}
|
|
394
429
|
|
|
395
|
-
if (data.priorities
|
|
430
|
+
if (data.priorities?.length > 0) {
|
|
396
431
|
lines.push('**Related Priorities:**');
|
|
397
432
|
for (const p of data.priorities) {
|
|
398
|
-
lines.push(`- ${p.theme} (${p.category}, ${p.volume} users) —
|
|
433
|
+
lines.push(`- ${p.theme} (${p.category}, ${p.volume} users) — \`${p.id}\``);
|
|
399
434
|
}
|
|
400
435
|
lines.push('');
|
|
401
436
|
}
|
|
402
437
|
|
|
403
|
-
if (data.feedback
|
|
438
|
+
if (data.feedback?.length > 0) {
|
|
404
439
|
lines.push('**Related Feedback:**');
|
|
405
440
|
for (const f of data.feedback) {
|
|
406
441
|
lines.push(`- [${f.source}] "${f.text}"`);
|
|
@@ -415,7 +450,7 @@ function formatAsk(data) {
|
|
|
415
450
|
lines.push(`- Segment: ${pat.segment_affinity}`);
|
|
416
451
|
if (pat.top_categories) {
|
|
417
452
|
const cats = Object.entries(pat.top_categories).map(([k, v]) => `${k} (${Math.round(v * 100)}%)`).join(', ');
|
|
418
|
-
lines.push(`-
|
|
453
|
+
lines.push(`- Categories: ${cats}`);
|
|
419
454
|
}
|
|
420
455
|
lines.push('');
|
|
421
456
|
}
|
|
@@ -424,45 +459,95 @@ function formatAsk(data) {
|
|
|
424
459
|
return `No results found for "${data.question}". Try rephrasing.`;
|
|
425
460
|
}
|
|
426
461
|
|
|
427
|
-
lines.push(`---\n${data.total} results
|
|
462
|
+
lines.push(`---\n${data.total} results`);
|
|
463
|
+
return lines.join('\n');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Internal helper: format weekly digest data (called from formatPriorities when weekly: true)
|
|
467
|
+
function formatWeeklyDigest(data) {
|
|
468
|
+
if (data.error) return `Error: ${data.error}`;
|
|
469
|
+
|
|
470
|
+
const lines = [];
|
|
471
|
+
|
|
472
|
+
lines.push(`**${data.headline || 'Weekly Digest'}**\n`);
|
|
473
|
+
|
|
474
|
+
if (data.movements?.length > 0) {
|
|
475
|
+
lines.push('**Movements:**');
|
|
476
|
+
for (const m of data.movements) {
|
|
477
|
+
const arrow = m.direction === 'up' ? '↑' : '↓';
|
|
478
|
+
lines.push(`- ${arrow} ${m.title} (#${m.from_rank} → #${m.to_rank})`);
|
|
479
|
+
}
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (data.new_entries?.length > 0) {
|
|
484
|
+
lines.push('**New This Week:**');
|
|
485
|
+
for (const n of data.new_entries) {
|
|
486
|
+
lines.push(`- ${n.title} (rank #${n.rank}, ${n.mentions} mentions)`);
|
|
487
|
+
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (data.volume_spikes?.length > 0) {
|
|
492
|
+
lines.push('**Volume Spikes:**');
|
|
493
|
+
for (const v of data.volume_spikes) {
|
|
494
|
+
lines.push(`- ${v.title} (+${v.percent_increase}%)`);
|
|
495
|
+
}
|
|
496
|
+
lines.push('');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (data.dropped?.length > 0) {
|
|
500
|
+
lines.push('**Dropped Off:**');
|
|
501
|
+
for (const d of data.dropped) {
|
|
502
|
+
lines.push(`- ${d.title}${d.reason ? ` (${d.reason})` : ''}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push('');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!data.has_previous) {
|
|
508
|
+
lines.push('*First week — baseline captured. Changes will appear next week.*');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (lines.length <= 1) {
|
|
512
|
+
return data.headline || 'No changes this week.';
|
|
513
|
+
}
|
|
428
514
|
|
|
429
515
|
return lines.join('\n');
|
|
430
516
|
}
|
|
431
517
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
518
|
+
// Internal helper: format batch export data (called from formatSpec when batch: true)
|
|
519
|
+
function formatBatchExport(data) {
|
|
520
|
+
if (data.error) return `Error: ${data.error}${data.suggestion ? `\n${data.suggestion}` : ''}`;
|
|
521
|
+
|
|
522
|
+
let output = `**${data.count} brief${data.count !== 1 ? 's' : ''} exported**\n\n`;
|
|
523
|
+
output += data.markdown;
|
|
524
|
+
return output;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─────────────────────────────────────────────────────────────
|
|
528
|
+
// Tool Call Handler
|
|
529
|
+
// ─────────────────────────────────────────────────────────────
|
|
530
|
+
|
|
435
531
|
async function handleToolCall(id, params, token) {
|
|
436
532
|
const { name, arguments: args } = params;
|
|
437
533
|
|
|
438
534
|
try {
|
|
439
535
|
const result = await callMcpApi(token, name, args || {});
|
|
440
536
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
case 'circuit.act':
|
|
451
|
-
formattedText = formatAct(result);
|
|
452
|
-
break;
|
|
453
|
-
case 'circuit.ask':
|
|
454
|
-
formattedText = formatAsk(result);
|
|
455
|
-
break;
|
|
456
|
-
default:
|
|
457
|
-
formattedText = JSON.stringify(result, null, 2);
|
|
458
|
-
}
|
|
537
|
+
const formatters = {
|
|
538
|
+
'circuit.priorities': formatPriorities,
|
|
539
|
+
'circuit.spec': formatSpec,
|
|
540
|
+
'circuit.act': formatAct,
|
|
541
|
+
'circuit.ask': formatAsk,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const formatter = formatters[name] || ((d) => JSON.stringify(d, null, 2));
|
|
545
|
+
const text = formatter(result);
|
|
459
546
|
|
|
460
547
|
return {
|
|
461
548
|
jsonrpc: '2.0',
|
|
462
549
|
id,
|
|
463
|
-
result: {
|
|
464
|
-
content: [{ type: 'text', text: formattedText }]
|
|
465
|
-
}
|
|
550
|
+
result: { content: [{ type: 'text', text }] }
|
|
466
551
|
};
|
|
467
552
|
|
|
468
553
|
} catch (err) {
|