@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.
Files changed (2) hide show
  1. package/build/index.js +145 -23
  2. 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
- async function askOpencode(prompt, systemPrompt) {
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: "Advisor Query",
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 async`);
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 lastTextLength = 0;
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 < maxWaitMs) {
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 parts
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
- if (part && typeof part === "object" && part.type === "text" && "text" in part) {
254
- textParts.push(String(part.text));
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] Response completed (${currentText.length} chars)`);
265
- return currentText;
293
+ console.error(`[advisor] Completed (${currentText.length} chars)`);
294
+ const toolSummary = extractToolSummary(messages);
295
+ return currentText + toolSummary;
266
296
  }
267
297
  }
268
- // Fallback: text stopped growing for 3 cycles
269
- if (currentText.length > 0) {
270
- if (currentText.length === lastTextLength) {
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 >= 3) {
273
- console.error(`[advisor] Response stable (${currentText.length} chars)`);
274
- return currentText;
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
- lastTextLength = currentText.length;
323
+ lastContentLength = totalContentLength;
280
324
  }
281
325
  }
282
326
  await sleep(pollIntervalMs);
283
327
  }
284
- throw new Error("Timed out waiting for advisor response");
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": "1.0.4",
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 a second opinion via OpenCode SDK",
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"