@gopersonal/advisor 1.0.4 → 2.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/build/index.js +145 -23
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -170,11 +170,44 @@ function getModelOverride() {
|
|
|
170
170
|
function sleep(ms) {
|
|
171
171
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
function extractTextFromMessages(messages) {
|
|
174
|
+
const allText = [];
|
|
175
|
+
for (const m of messages) {
|
|
176
|
+
const msg = m;
|
|
177
|
+
if (msg.info && typeof msg.info === "object" && "role" in msg.info && msg.info.role === "assistant") {
|
|
178
|
+
if (Array.isArray(msg.parts)) {
|
|
179
|
+
for (const part of msg.parts) {
|
|
180
|
+
if (part && typeof part === "object" && part.type === "text" && "text" in part) {
|
|
181
|
+
allText.push(String(part.text));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return allText.join("\n");
|
|
188
|
+
}
|
|
189
|
+
function extractToolSummary(messages) {
|
|
190
|
+
const tools = [];
|
|
191
|
+
for (const m of messages) {
|
|
192
|
+
const msg = m;
|
|
193
|
+
if (Array.isArray(msg.parts)) {
|
|
194
|
+
for (const part of msg.parts) {
|
|
195
|
+
const p = part;
|
|
196
|
+
if (p && p.type === "tool-invocation" && p.toolName) {
|
|
197
|
+
const args = p.args ? JSON.stringify(p.args).slice(0, 200) : "";
|
|
198
|
+
tools.push(`- ${p.toolName}${args ? `: ${args}` : ""}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return tools.length > 0 ? `\n\n## Tools used\n${tools.join("\n")}` : "";
|
|
204
|
+
}
|
|
205
|
+
async function runOpencodeSession(prompt, systemPrompt, options = {}) {
|
|
206
|
+
const { timeoutMs = 90_000, stableThreshold = 3, sessionTitle = "Advisor Query", } = options;
|
|
174
207
|
const client = await getOpencodeClient();
|
|
175
208
|
const directory = process.env.ADVISOR_DIRECTORY;
|
|
176
209
|
const sessionResult = await client.session.create({
|
|
177
|
-
title:
|
|
210
|
+
title: sessionTitle,
|
|
178
211
|
...(directory ? { directory } : {}),
|
|
179
212
|
});
|
|
180
213
|
if (!sessionResult.data) {
|
|
@@ -183,7 +216,6 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
183
216
|
const sessionId = sessionResult.data.id;
|
|
184
217
|
const modelOverride = getModelOverride();
|
|
185
218
|
try {
|
|
186
|
-
// Send the prompt asynchronously - returns 204 immediately while session processes
|
|
187
219
|
try {
|
|
188
220
|
await client.session.promptAsync({
|
|
189
221
|
sessionID: sessionId,
|
|
@@ -192,21 +224,19 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
192
224
|
...(directory ? { directory } : {}),
|
|
193
225
|
parts: [{ type: "text", text: prompt }],
|
|
194
226
|
});
|
|
195
|
-
console.error(`[advisor] Prompt submitted
|
|
227
|
+
console.error(`[advisor] Prompt submitted (timeout: ${timeoutMs / 1000}s, stable: ${stableThreshold} cycles)`);
|
|
196
228
|
}
|
|
197
229
|
catch (err) {
|
|
198
230
|
const msg = err instanceof Error ? err.message : String(err);
|
|
199
231
|
throw new Error(`Failed to submit prompt: ${msg}`);
|
|
200
232
|
}
|
|
201
|
-
// Poll for the assistant's response and auto-answer any questions
|
|
202
|
-
const maxWaitMs = 90_000;
|
|
203
233
|
const pollIntervalMs = 2000;
|
|
204
234
|
const startTime = Date.now();
|
|
205
|
-
let
|
|
235
|
+
let lastContentLength = 0;
|
|
206
236
|
let stableCount = 0;
|
|
207
237
|
const answeredQuestions = new Set();
|
|
208
238
|
await sleep(3000);
|
|
209
|
-
while (Date.now() - startTime <
|
|
239
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
210
240
|
// Auto-answer any pending questions (opencode agent may ask permission)
|
|
211
241
|
try {
|
|
212
242
|
const questions = await client.question.list({});
|
|
@@ -215,7 +245,6 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
215
245
|
if (q.sessionID === sessionId && !answeredQuestions.has(q.id)) {
|
|
216
246
|
console.error(`[advisor] Auto-answering question: ${q.id}`);
|
|
217
247
|
const answers = q.questions.map((qi) => {
|
|
218
|
-
// Pick the first option for each question
|
|
219
248
|
const opts = qi.options;
|
|
220
249
|
return opts && opts.length > 0 ? [opts[0].label] : ["yes"];
|
|
221
250
|
});
|
|
@@ -231,7 +260,6 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
231
260
|
catch {
|
|
232
261
|
// question API may not be available, ignore
|
|
233
262
|
}
|
|
234
|
-
// Check messages for the assistant's response
|
|
235
263
|
const messagesResult = await client.session.messages({
|
|
236
264
|
sessionID: sessionId,
|
|
237
265
|
});
|
|
@@ -246,12 +274,13 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
246
274
|
await sleep(pollIntervalMs);
|
|
247
275
|
continue;
|
|
248
276
|
}
|
|
249
|
-
// Extract text
|
|
277
|
+
// Extract text from latest assistant message
|
|
250
278
|
const textParts = [];
|
|
251
279
|
if (Array.isArray(assistantMsg.parts)) {
|
|
252
280
|
for (const part of assistantMsg.parts) {
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
const p = part;
|
|
282
|
+
if (p && p.type === "text" && "text" in p) {
|
|
283
|
+
textParts.push(String(p.text));
|
|
255
284
|
}
|
|
256
285
|
}
|
|
257
286
|
}
|
|
@@ -261,27 +290,52 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
261
290
|
if (info.time && typeof info.time === "object") {
|
|
262
291
|
const time = info.time;
|
|
263
292
|
if (time.completed && currentText.length > 0) {
|
|
264
|
-
console.error(`[advisor]
|
|
265
|
-
|
|
293
|
+
console.error(`[advisor] Completed (${currentText.length} chars)`);
|
|
294
|
+
const toolSummary = extractToolSummary(messages);
|
|
295
|
+
return currentText + toolSummary;
|
|
266
296
|
}
|
|
267
297
|
}
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
298
|
+
// Stability check: use total content across ALL messages to detect when agent stops working
|
|
299
|
+
const totalContentLength = messages.reduce((acc, m) => {
|
|
300
|
+
const msg = m;
|
|
301
|
+
if (Array.isArray(msg.parts)) {
|
|
302
|
+
for (const p of msg.parts) {
|
|
303
|
+
const part = p;
|
|
304
|
+
if (part && "text" in part)
|
|
305
|
+
acc += String(part.text).length;
|
|
306
|
+
if (part && part.type === "tool-invocation")
|
|
307
|
+
acc += 100; // count tool calls as content
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return acc;
|
|
311
|
+
}, 0);
|
|
312
|
+
if (totalContentLength > 0) {
|
|
313
|
+
if (totalContentLength === lastContentLength) {
|
|
271
314
|
stableCount++;
|
|
272
|
-
if (stableCount >=
|
|
273
|
-
console.error(`[advisor]
|
|
274
|
-
|
|
315
|
+
if (stableCount >= stableThreshold) {
|
|
316
|
+
console.error(`[advisor] Stable after ${stableThreshold} cycles (${totalContentLength} total content)`);
|
|
317
|
+
const toolSummary = extractToolSummary(messages);
|
|
318
|
+
return currentText + toolSummary;
|
|
275
319
|
}
|
|
276
320
|
}
|
|
277
321
|
else {
|
|
278
322
|
stableCount = 0;
|
|
279
|
-
|
|
323
|
+
lastContentLength = totalContentLength;
|
|
280
324
|
}
|
|
281
325
|
}
|
|
282
326
|
await sleep(pollIntervalMs);
|
|
283
327
|
}
|
|
284
|
-
|
|
328
|
+
// Timeout — return partial results if available
|
|
329
|
+
const finalMessages = await client.session.messages({ sessionID: sessionId });
|
|
330
|
+
if (finalMessages.data && Array.isArray(finalMessages.data)) {
|
|
331
|
+
const text = extractTextFromMessages(finalMessages.data);
|
|
332
|
+
const toolSummary = extractToolSummary(finalMessages.data);
|
|
333
|
+
if (text) {
|
|
334
|
+
console.error(`[advisor] Timed out but returning partial result (${text.length} chars)`);
|
|
335
|
+
return text + toolSummary + "\n\n[Note: Task timed out but partial work was completed]";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw new Error("Timed out waiting for response");
|
|
285
339
|
}
|
|
286
340
|
finally {
|
|
287
341
|
try {
|
|
@@ -292,6 +346,14 @@ async function askOpencode(prompt, systemPrompt) {
|
|
|
292
346
|
}
|
|
293
347
|
}
|
|
294
348
|
}
|
|
349
|
+
// Legacy wrapper for advisory tools
|
|
350
|
+
async function askOpencode(prompt, systemPrompt) {
|
|
351
|
+
return runOpencodeSession(prompt, systemPrompt, {
|
|
352
|
+
timeoutMs: 90_000,
|
|
353
|
+
stableThreshold: 3,
|
|
354
|
+
sessionTitle: "Advisor Query",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
295
357
|
// --- MCP Server setup ---
|
|
296
358
|
const server = new McpServer({
|
|
297
359
|
name: "advisor",
|
|
@@ -401,6 +463,66 @@ server.registerTool("get_second_opinion", {
|
|
|
401
463
|
};
|
|
402
464
|
}
|
|
403
465
|
});
|
|
466
|
+
// Tool 3: execute_task
|
|
467
|
+
server.registerTool("execute_task", {
|
|
468
|
+
description: "Delegate a coding task to a separate AI agent that will actually DO the work — edit files, run commands, " +
|
|
469
|
+
"create code, fix bugs, etc. Use this when you want another agent to independently complete a task. " +
|
|
470
|
+
"The agent has full access to the project: it can read/write files, run bash commands, and make changes. " +
|
|
471
|
+
"Use this for: implementing features, fixing bugs, refactoring code, writing tests, running builds, " +
|
|
472
|
+
"setting up configurations, or any hands-on coding work you want to delegate. " +
|
|
473
|
+
"Be specific about what you want done — the agent works independently and returns results when finished.",
|
|
474
|
+
inputSchema: {
|
|
475
|
+
task: z
|
|
476
|
+
.string()
|
|
477
|
+
.describe("Clear description of what needs to be done. Be specific: include file paths, function names, " +
|
|
478
|
+
"expected behavior, and acceptance criteria. The more detail you provide, the better the result."),
|
|
479
|
+
context: z
|
|
480
|
+
.string()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe("Additional context: relevant code snippets, error messages, architectural decisions, " +
|
|
483
|
+
"or constraints the agent should know about."),
|
|
484
|
+
files_to_focus: z
|
|
485
|
+
.string()
|
|
486
|
+
.optional()
|
|
487
|
+
.describe("Comma-separated list of file paths the agent should focus on (e.g. 'src/auth.ts, src/routes/login.ts'). " +
|
|
488
|
+
"Helps the agent find relevant code faster."),
|
|
489
|
+
},
|
|
490
|
+
}, async ({ task, context, files_to_focus }) => {
|
|
491
|
+
const systemPrompt = `You are a senior software engineer executing a coding task. Your job is to COMPLETE the task, not discuss it.\n\n` +
|
|
492
|
+
`RULES:\n` +
|
|
493
|
+
`1. DO the work. Read files, write code, run commands. Don't just explain what to do.\n` +
|
|
494
|
+
`2. Start by understanding the codebase — read relevant files before making changes.\n` +
|
|
495
|
+
`3. Make all necessary changes to fully complete the task.\n` +
|
|
496
|
+
`4. After making changes, verify they work (run the code, check for errors, test if possible).\n` +
|
|
497
|
+
`5. If you encounter an error, fix it. Don't stop at the first problem.\n` +
|
|
498
|
+
`6. When done, provide a brief summary of what you changed and why.\n\n` +
|
|
499
|
+
`You have full access to the project filesystem and shell. Use your tools actively.`;
|
|
500
|
+
let userPrompt = `## Task\n${task}`;
|
|
501
|
+
if (context) {
|
|
502
|
+
userPrompt += `\n\n## Context\n${context}`;
|
|
503
|
+
}
|
|
504
|
+
if (files_to_focus) {
|
|
505
|
+
userPrompt += `\n\n## Key files to focus on\n${files_to_focus}`;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const result = await runOpencodeSession(userPrompt, systemPrompt, {
|
|
509
|
+
timeoutMs: 300_000, // 5 minutes — tasks need more time than advisory questions
|
|
510
|
+
stableThreshold: 8, // 16 seconds of no activity — tasks involve tool calls between text
|
|
511
|
+
sessionTitle: "Task Execution",
|
|
512
|
+
});
|
|
513
|
+
return { content: [{ type: "text", text: result }] };
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
517
|
+
return {
|
|
518
|
+
content: [{
|
|
519
|
+
type: "text",
|
|
520
|
+
text: `Task execution failed: ${msg}\nMake sure opencode is installed and configured with a provider.`,
|
|
521
|
+
}],
|
|
522
|
+
isError: true,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
});
|
|
404
526
|
// --- Start the server ---
|
|
405
527
|
async function main() {
|
|
406
528
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gopersonal/advisor",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "gopersonal",
|
|
26
26
|
"license": "ISC",
|
|
27
|
-
"description": "MCP server that gives AI agents
|
|
27
|
+
"description": "MCP server that gives AI agents advice and can execute coding tasks via OpenCode SDK",
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
30
|
"url": "https://github.com/gopersonal/calvincode-mcps"
|