@vibescope/mcp-server 0.4.6 → 0.4.8

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.
@@ -27,7 +27,9 @@ export declare class VibescopeApiClient {
27
27
  private retryConfig;
28
28
  constructor(config: ApiClientConfig);
29
29
  private request;
30
- validateAuth(): Promise<ApiResponse<{
30
+ validateAuth(options?: {
31
+ timeoutMs?: number;
32
+ }): Promise<ApiResponse<{
31
33
  valid: boolean;
32
34
  user_id: string;
33
35
  api_key_id: string;
@@ -47,21 +47,30 @@ export class VibescopeApiClient {
47
47
  retryStatusCodes: config.retry?.retryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES,
48
48
  };
49
49
  }
50
- async request(method, path, body) {
50
+ async request(method, path, body, options) {
51
51
  const url = `${this.baseUrl}${path}`;
52
52
  const { maxRetries, baseDelayMs, maxDelayMs, retryStatusCodes } = this.retryConfig;
53
53
  let lastError = null;
54
54
  let lastResponse = null;
55
55
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
56
+ let timeoutId;
56
57
  try {
57
- const response = await fetch(url, {
58
+ const fetchOptions = {
58
59
  method,
59
60
  headers: {
60
61
  'Content-Type': 'application/json',
61
62
  'X-API-Key': this.apiKey
62
63
  },
63
- body: body ? JSON.stringify(body) : undefined
64
- });
64
+ body: body ? JSON.stringify(body) : undefined,
65
+ };
66
+ if (options?.timeoutMs) {
67
+ const controller = new AbortController();
68
+ timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
69
+ fetchOptions.signal = controller.signal;
70
+ }
71
+ const response = await fetch(url, fetchOptions);
72
+ if (timeoutId)
73
+ clearTimeout(timeoutId);
65
74
  // Check if we should retry this status code
66
75
  if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
67
76
  lastResponse = response;
@@ -94,7 +103,15 @@ export class VibescopeApiClient {
94
103
  };
95
104
  }
96
105
  catch (err) {
97
- lastError = err instanceof Error ? err : new Error('Network error');
106
+ if (timeoutId)
107
+ clearTimeout(timeoutId);
108
+ // Detect AbortError from timeout
109
+ if (err instanceof Error && err.name === 'AbortError' && options?.timeoutMs) {
110
+ lastError = new Error(`Request timed out after ${options.timeoutMs}ms`);
111
+ }
112
+ else {
113
+ lastError = err instanceof Error ? err : new Error('Network error');
114
+ }
98
115
  // Retry on network errors (connection failures, timeouts)
99
116
  if (attempt < maxRetries) {
100
117
  const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
@@ -130,10 +147,10 @@ export class VibescopeApiClient {
130
147
  };
131
148
  }
132
149
  // Auth endpoints
133
- async validateAuth() {
150
+ async validateAuth(options) {
134
151
  return this.request('POST', '/api/mcp/auth/validate', {
135
152
  api_key: this.apiKey
136
- });
153
+ }, options);
137
154
  }
138
155
  // Session endpoints
139
156
  async startSession(params) {
package/dist/cli-init.js CHANGED
@@ -236,16 +236,17 @@ function writeJsonFile(path, data) {
236
236
  }
237
237
  function buildMcpServerConfig(apiKey) {
238
238
  const isWindows = platform() === 'win32';
239
+ // Prefer globally installed binary (instant start) with npx fallback
239
240
  if (isWindows) {
240
241
  return {
241
242
  command: 'cmd',
242
- args: ['/c', 'npx', '-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
243
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
243
244
  env: { VIBESCOPE_API_KEY: apiKey },
244
245
  };
245
246
  }
246
247
  return {
247
- command: 'npx',
248
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
248
+ command: 'bash',
249
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
249
250
  env: { VIBESCOPE_API_KEY: apiKey },
250
251
  };
251
252
  }
@@ -340,6 +340,12 @@ export const TOOL_CATEGORIES = {
340
340
  { name: 'update_mcp_server', brief: 'Self-update the MCP server' },
341
341
  ],
342
342
  },
343
+ persona_templates: {
344
+ description: 'Persona templates — behavioral archetypes that shape agent behavior',
345
+ tools: [
346
+ { name: 'get_persona_templates', brief: 'List available persona templates for a project' },
347
+ ],
348
+ },
343
349
  features: {
344
350
  description: 'Feature specs — flesh out ideas into full specs before breaking into tasks',
345
351
  tools: [
@@ -1300,6 +1300,17 @@ Read recent project chat messages to stay informed about project communication.
1300
1300
  - count: Number of messages returned
1301
1301
 
1302
1302
  **Example:** get_project_messages(project_id: "123e4567-e89b-12d3-a456-426614174000", limit: 10)`,
1303
+ post_progress: `# post_progress
1304
+ Post a structured progress update to the project chat. Use at key milestones: starting a task, creating a PR, hitting a blocker, or completing work.
1305
+
1306
+ **Parameters:**
1307
+ - project_id (required): Project UUID
1308
+ - message (required): Progress update message (supports markdown)
1309
+ - type (optional): Update type — info (general), milestone (key achievement), blocker (blocking issue), question (asking for input). Default: info
1310
+
1311
+ **Returns:** Confirmation with message_id and timestamp.
1312
+
1313
+ **Example:** post_progress(project_id: "123e4567-e89b-12d3-a456-426614174000", message: "PR #42 created for auth feature", type: "milestone")`,
1303
1314
  // Version management tools
1304
1315
  check_mcp_version: `# check_mcp_version
1305
1316
  Check for available MCP server updates and version information.
@@ -1331,6 +1342,17 @@ Self-update the MCP server to the latest available version.
1331
1342
  **Example:** update_mcp_server()
1332
1343
 
1333
1344
  **Note:** This operation may temporarily disconnect active sessions during restart.`,
1345
+ // Persona template tools
1346
+ get_persona_templates: `# get_persona_templates
1347
+ List available persona templates for a project. Returns both global defaults and project-specific templates.
1348
+
1349
+ **Parameters:**
1350
+ - project_id (required): Project UUID
1351
+
1352
+ **Returns:**
1353
+ - templates: Array of persona templates with id, name, description, focus_areas, icon, is_default, scope (global/project)
1354
+
1355
+ **Example:** get_persona_templates(project_id: "123e4567-e89b-12d3-a456-426614174000")`,
1334
1356
  add_feature: `# add_feature
1335
1357
  Create a draft feature spec. Features sit between ideas (quick thoughts) and tasks (actionable work).
1336
1358
 
@@ -7,7 +7,7 @@ import { checkVersion, getLocalVersion } from '../version.js';
7
7
  const PACKAGE_NAME = '@vibescope/mcp-server';
8
8
  export const versionHandlers = {
9
9
  check_mcp_version: async (_args, _ctx) => {
10
- const info = await checkVersion();
10
+ const info = await checkVersion({ bypassCache: true });
11
11
  if (info.error) {
12
12
  return success({
13
13
  current_version: info.current,
package/dist/index.js CHANGED
@@ -141,9 +141,9 @@ initApiClient({ apiKey: API_KEY });
141
141
  // ============================================================================
142
142
  // Authentication
143
143
  // ============================================================================
144
- async function validateApiKey() {
144
+ async function validateApiKey(timeoutMs) {
145
145
  const apiClient = getApiClient();
146
- const response = await apiClient.validateAuth();
146
+ const response = await apiClient.validateAuth(timeoutMs ? { timeoutMs } : undefined);
147
147
  if (!response.ok || !response.data?.valid) {
148
148
  return null;
149
149
  }
@@ -153,6 +153,61 @@ async function validateApiKey() {
153
153
  scope: 'personal', // API handles authorization, scope not needed locally
154
154
  };
155
155
  }
156
+ // Deferred auth: started eagerly but awaited on first tool call
157
+ let deferredAuthPromise = null;
158
+ let resolvedAuth = null;
159
+ let authError = null;
160
+ function startDeferredAuth() {
161
+ deferredAuthPromise = validateApiKey(10000)
162
+ .then((auth) => {
163
+ resolvedAuth = auth;
164
+ if (!auth)
165
+ authError = 'Invalid API key';
166
+ return auth;
167
+ })
168
+ .catch((err) => {
169
+ authError = err instanceof Error ? err.message : 'Auth validation failed';
170
+ return null;
171
+ });
172
+ }
173
+ async function getAuth() {
174
+ // If already resolved, return immediately
175
+ if (resolvedAuth)
176
+ return resolvedAuth;
177
+ // Await the deferred promise
178
+ if (deferredAuthPromise) {
179
+ const auth = await deferredAuthPromise;
180
+ if (auth)
181
+ return auth;
182
+ }
183
+ // Auth failed — return structured error
184
+ const troubleshooting = [
185
+ 'MCP auth validation failed.',
186
+ authError ? `Reason: ${authError}` : '',
187
+ 'Troubleshooting:',
188
+ '1. Check your API key is valid at https://vibescope.dev/dashboard/settings',
189
+ '2. Verify VIBESCOPE_API_KEY environment variable is set correctly',
190
+ '3. Check network connectivity to vibescope.dev',
191
+ '4. Restart Claude Code and try again',
192
+ ].filter(Boolean).join('\n');
193
+ throw new Error(troubleshooting);
194
+ }
195
+ // Deferred update warning: fire-and-forget, delivered on first tool call
196
+ let updateWarningPromise = null;
197
+ let resolvedUpdateWarning = undefined; // undefined = not yet resolved
198
+ function startDeferredUpdateCheck() {
199
+ updateWarningPromise = getUpdateWarning().catch(() => null);
200
+ updateWarningPromise.then((warning) => {
201
+ resolvedUpdateWarning = warning;
202
+ });
203
+ }
204
+ function consumeUpdateWarning() {
205
+ if (resolvedUpdateWarning === undefined)
206
+ return null; // not ready yet
207
+ const warning = resolvedUpdateWarning;
208
+ resolvedUpdateWarning = null; // deliver only once
209
+ return warning;
210
+ }
156
211
  // Tool definitions imported from tools.ts
157
212
  // ============================================================================
158
213
  // Tool Handlers
@@ -205,17 +260,10 @@ async function handleTool(auth, name, args) {
205
260
  // Server Setup
206
261
  // ============================================================================
207
262
  async function main() {
208
- // Validate API key on startup via API
209
- const auth = await validateApiKey();
210
- if (!auth) {
211
- console.error('Invalid API key');
212
- process.exit(1);
213
- }
214
- // Check for updates (non-blocking, with timeout)
215
- const updateWarning = await getUpdateWarning();
216
- const serverInstructions = updateWarning
217
- ? `${updateWarning}\n\nVibescope MCP server - AI project tracking and coordination tools.`
218
- : 'Vibescope MCP server - AI project tracking and coordination tools.';
263
+ // Start auth validation eagerly in background (10s timeout) — don't block startup
264
+ startDeferredAuth();
265
+ // Start update check in background — delivered on first tool call if available
266
+ startDeferredUpdateCheck();
219
267
  const server = new Server({
220
268
  name: 'vibescope',
221
269
  version: '0.1.0',
@@ -223,7 +271,7 @@ async function main() {
223
271
  capabilities: {
224
272
  tools: {},
225
273
  },
226
- instructions: serverInstructions,
274
+ instructions: 'Vibescope MCP server - AI project tracking and coordination tools.',
227
275
  });
228
276
  // List available tools
229
277
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -242,6 +290,22 @@ async function main() {
242
290
  // Handle tool calls
243
291
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
244
292
  const { name, arguments: args } = request.params;
293
+ // Await deferred auth on first tool call (usually already resolved)
294
+ let auth;
295
+ try {
296
+ auth = await getAuth();
297
+ }
298
+ catch (err) {
299
+ return {
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ text: err instanceof Error ? err.message : 'Auth validation failed',
304
+ },
305
+ ],
306
+ isError: true,
307
+ };
308
+ }
245
309
  // Check rate limit
246
310
  const rateCheck = rateLimiter.check(auth.apiKeyId);
247
311
  if (!rateCheck.allowed) {
@@ -267,6 +331,14 @@ async function main() {
267
331
  text: JSON.stringify(result, null, 2),
268
332
  },
269
333
  ];
334
+ // Deliver update warning on first tool call (if available)
335
+ const updateWarning = consumeUpdateWarning();
336
+ if (updateWarning) {
337
+ content.push({
338
+ type: 'text',
339
+ text: `\n--- ${updateWarning} ---`,
340
+ });
341
+ }
270
342
  // Include reminder nudge if applicable
271
343
  const reminder = getReminder(name);
272
344
  if (reminder) {
package/dist/setup.js CHANGED
@@ -176,13 +176,19 @@ export function writeConfig(configPath, config) {
176
176
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
177
177
  }
178
178
  export function generateMcpConfig(apiKey, ide) {
179
- const vibescopeServer = {
180
- command: 'npx',
181
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
182
- env: {
183
- VIBESCOPE_API_KEY: apiKey,
184
- },
185
- };
179
+ // Prefer globally installed binary (instant start) with npx fallback
180
+ const isWindows = platform() === 'win32';
181
+ const vibescopeServer = isWindows
182
+ ? {
183
+ command: 'cmd',
184
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
185
+ env: { VIBESCOPE_API_KEY: apiKey },
186
+ }
187
+ : {
188
+ command: 'bash',
189
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
190
+ env: { VIBESCOPE_API_KEY: apiKey },
191
+ };
186
192
  // Gemini CLI uses a different config format with additional options
187
193
  if (ide.configFormat === 'settings-json') {
188
194
  return {
@@ -32,6 +32,7 @@ import { connectorTools } from './connectors.js';
32
32
  import { cloudAgentTools } from './cloud-agents.js';
33
33
  import { versionTools } from './version.js';
34
34
  import { featureTools } from './features.js';
35
+ import { personaTemplateTools } from './persona-templates.js';
35
36
  /**
36
37
  * All MCP tool definitions combined
37
38
  */
@@ -71,5 +72,6 @@ export declare const toolCategories: {
71
72
  readonly cloudAgents: string[];
72
73
  readonly version: string[];
73
74
  readonly features: string[];
75
+ readonly personaTemplates: string[];
74
76
  };
75
- export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, };
77
+ export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, personaTemplateTools, };
@@ -32,6 +32,7 @@ import { cloudAgentTools } from './cloud-agents.js';
32
32
  import { versionTools } from './version.js';
33
33
  import { chatTools } from './chat.js';
34
34
  import { featureTools } from './features.js';
35
+ import { personaTemplateTools } from './persona-templates.js';
35
36
  /**
36
37
  * All MCP tool definitions combined
37
38
  */
@@ -63,6 +64,7 @@ export const tools = [
63
64
  ...versionTools,
64
65
  ...chatTools,
65
66
  ...featureTools,
67
+ ...personaTemplateTools,
66
68
  ];
67
69
  /**
68
70
  * Build the complete tool list from all domain modules
@@ -101,6 +103,7 @@ export const toolCategories = {
101
103
  cloudAgents: cloudAgentTools.map((t) => t.name),
102
104
  version: versionTools.map((t) => t.name),
103
105
  features: featureTools.map((t) => t.name),
106
+ personaTemplates: personaTemplateTools.map((t) => t.name),
104
107
  };
105
108
  // Re-export domain tools for selective imports
106
- export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, };
109
+ export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, personaTemplateTools, };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Persona Template Tool Definitions
3
+ *
4
+ * Tools for managing persona templates:
5
+ * - get_persona_templates
6
+ */
7
+ import type { Tool } from './types.js';
8
+ export declare const personaTemplateTools: Tool[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Persona Template Tool Definitions
3
+ *
4
+ * Tools for managing persona templates:
5
+ * - get_persona_templates
6
+ */
7
+ export const personaTemplateTools = [
8
+ {
9
+ name: 'get_persona_templates',
10
+ description: 'List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ project_id: {
15
+ type: 'string',
16
+ description: 'Project UUID',
17
+ },
18
+ },
19
+ required: ['project_id'],
20
+ },
21
+ },
22
+ ];
@@ -222,6 +222,10 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
222
222
  enum: ['haiku', 'sonnet', 'opus'],
223
223
  description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
224
224
  },
225
+ assigned_agent_name: {
226
+ type: 'string',
227
+ description: 'Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task.',
228
+ },
225
229
  },
226
230
  required: ['project_id', 'title'],
227
231
  },
@@ -292,6 +296,10 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
292
296
  enum: ['haiku', 'sonnet', 'opus'],
293
297
  description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
294
298
  },
299
+ assigned_agent_name: {
300
+ type: ['string', 'null'],
301
+ description: 'Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task.',
302
+ },
295
303
  skip_worktree_requirement: {
296
304
  type: 'boolean',
297
305
  description: 'Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false',
package/dist/version.d.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
9
  export interface VersionInfo {
8
10
  current: string;
@@ -17,11 +19,15 @@ export declare function getLocalVersion(): string;
17
19
  /**
18
20
  * Fetch the latest published version from npm registry
19
21
  */
20
- export declare function getLatestVersion(): Promise<string | null>;
22
+ export declare function getLatestVersion(options?: {
23
+ bypassCache?: boolean;
24
+ }): Promise<string | null>;
21
25
  /**
22
26
  * Check if an update is available
23
27
  */
24
- export declare function checkVersion(): Promise<VersionInfo>;
28
+ export declare function checkVersion(options?: {
29
+ bypassCache?: boolean;
30
+ }): Promise<VersionInfo>;
25
31
  /**
26
32
  * Get the update warning message for server instructions, or null if up to date
27
33
  */
package/dist/version.js CHANGED
@@ -2,13 +2,19 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
- import { readFileSync } from 'fs';
8
- import { resolve, dirname } from 'path';
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { resolve, dirname, join } from 'path';
9
11
  import { fileURLToPath } from 'url';
12
+ import { homedir } from 'os';
10
13
  const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@vibescope/mcp-server';
11
14
  const PACKAGE_NAME = '@vibescope/mcp-server';
15
+ const CACHE_DIR = join(homedir(), '.vibescope');
16
+ const CACHE_PATH = join(CACHE_DIR, 'version-cache.json');
17
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
12
18
  /**
13
19
  * Get the current locally installed version from package.json
14
20
  */
@@ -23,13 +29,51 @@ export function getLocalVersion() {
23
29
  return 'unknown';
24
30
  }
25
31
  }
32
+ /**
33
+ * Read version from file-based cache if still valid
34
+ */
35
+ function readVersionCache() {
36
+ try {
37
+ if (!existsSync(CACHE_PATH))
38
+ return null;
39
+ const raw = JSON.parse(readFileSync(CACHE_PATH, 'utf-8'));
40
+ if (Date.now() - raw.fetchedAt < CACHE_TTL_MS) {
41
+ return raw.latestVersion;
42
+ }
43
+ return null; // expired
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Write version to file-based cache
51
+ */
52
+ function writeVersionCache(version) {
53
+ try {
54
+ if (!existsSync(CACHE_DIR)) {
55
+ mkdirSync(CACHE_DIR, { recursive: true });
56
+ }
57
+ const cache = { latestVersion: version, fetchedAt: Date.now() };
58
+ writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2) + '\n');
59
+ }
60
+ catch {
61
+ // Non-critical — silently ignore write failures
62
+ }
63
+ }
26
64
  /**
27
65
  * Fetch the latest published version from npm registry
28
66
  */
29
- export async function getLatestVersion() {
67
+ export async function getLatestVersion(options) {
68
+ // Check cache first (unless bypassed)
69
+ if (!options?.bypassCache) {
70
+ const cached = readVersionCache();
71
+ if (cached)
72
+ return cached;
73
+ }
30
74
  try {
31
75
  const controller = new AbortController();
32
- const timeout = setTimeout(() => controller.abort(), 5000);
76
+ const timeout = setTimeout(() => controller.abort(), 3000);
33
77
  const response = await fetch(`${NPM_REGISTRY_URL}/latest`, {
34
78
  signal: controller.signal,
35
79
  headers: { 'Accept': 'application/json' },
@@ -38,7 +82,11 @@ export async function getLatestVersion() {
38
82
  if (!response.ok)
39
83
  return null;
40
84
  const data = (await response.json());
41
- return data.version ?? null;
85
+ const version = data.version ?? null;
86
+ // Write to cache on successful fetch
87
+ if (version)
88
+ writeVersionCache(version);
89
+ return version;
42
90
  }
43
91
  catch {
44
92
  return null;
@@ -62,9 +110,9 @@ function isNewer(current, latest) {
62
110
  /**
63
111
  * Check if an update is available
64
112
  */
65
- export async function checkVersion() {
113
+ export async function checkVersion(options) {
66
114
  const current = getLocalVersion();
67
- const latest = await getLatestVersion();
115
+ const latest = await getLatestVersion(options);
68
116
  if (!latest) {
69
117
  return {
70
118
  current,
package/docs/TOOLS.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > Auto-generated from tool definitions. Do not edit manually.
4
4
  >
5
- > Generated: 2026-03-01
5
+ > Generated: 2026-03-05
6
6
  >
7
- > Total tools: 167
7
+ > Total tools: 168
8
8
 
9
9
  ## Table of Contents
10
10
 
@@ -36,6 +36,7 @@
36
36
  - [cloud_agents](#cloud-agents) - Cloud agent management and cleanup (3 tools)
37
37
  - [chat](#chat) - Project-wide chat channel for agent and user communication (3 tools)
38
38
  - [version](#version) - MCP server version management and updates (2 tools)
39
+ - [persona_templates](#persona-templates) - Persona templates — behavioral archetypes that shape agent behavior (1 tools)
39
40
  - [features](#features) - Feature specs — flesh out ideas into full specs before breaking into tasks (6 tools)
40
41
 
41
42
  ## session
@@ -380,6 +381,7 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
380
381
  | `estimated_minutes` | `number` | No | Estimated time to complete in minutes (min: 1) |
381
382
  | `blocking` | `boolean` | No | When true, this task blocks all other work until complete (used for deployment finalization) |
382
383
  | `model_capability` | `"haiku" | "sonnet" | "opus"` | No | Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning) |
384
+ | `assigned_agent_name` | `string` | No | Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task. |
383
385
 
384
386
  ---
385
387
 
@@ -417,6 +419,7 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
417
419
  | `worktree_path` | `string` | No | Git worktree path for this task (e.g., "../project-task-abc123"). Store this for cleanup tracking across sessions. |
418
420
  | `worktree_hostname` | `string` | No | Machine hostname where worktree was created (os.hostname()). Required with worktree_path to enable machine-aware cleanup. |
419
421
  | `model_capability` | `"haiku" | "sonnet" | "opus"` | No | Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning) |
422
+ | `assigned_agent_name` | `string,null` | No | Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task. |
420
423
  | `skip_worktree_requirement` | `boolean` | No | Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false |
421
424
  | `session_id` | `string` | No | Session ID from start_work_session. Required for cloud agents using mcporter (session context is not preserved between calls). Links the task to your agent on the dashboard. |
422
425
 
@@ -2574,6 +2577,22 @@ Update the Vibescope MCP server to the latest version. Runs npm install to fetch
2574
2577
 
2575
2578
  ---
2576
2579
 
2580
+ ## persona_templates
2581
+
2582
+ *Persona templates — behavioral archetypes that shape agent behavior*
2583
+
2584
+ ### get_persona_templates
2585
+
2586
+ List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.
2587
+
2588
+ **Parameters:**
2589
+
2590
+ | Parameter | Type | Required | Description |
2591
+ |-----------|------|----------|-------------|
2592
+ | `project_id` | `string` | Yes | Project UUID |
2593
+
2594
+ ---
2595
+
2577
2596
  ## features
2578
2597
 
2579
2598
  *Feature specs — flesh out ideas into full specs before breaking into tasks*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescope/mcp-server",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "MCP server for Vibescope - AI project tracking tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/api-client.ts CHANGED
@@ -81,22 +81,32 @@ export class VibescopeApiClient {
81
81
  };
82
82
  }
83
83
 
84
- private async request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> {
84
+ private async request<T>(method: string, path: string, body?: unknown, options?: { timeoutMs?: number }): Promise<ApiResponse<T>> {
85
85
  const url = `${this.baseUrl}${path}`;
86
86
  const { maxRetries, baseDelayMs, maxDelayMs, retryStatusCodes } = this.retryConfig;
87
87
  let lastError: Error | null = null;
88
88
  let lastResponse: Response | null = null;
89
89
 
90
90
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
91
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
91
92
  try {
92
- const response = await fetch(url, {
93
+ const fetchOptions: RequestInit = {
93
94
  method,
94
95
  headers: {
95
96
  'Content-Type': 'application/json',
96
97
  'X-API-Key': this.apiKey
97
98
  },
98
- body: body ? JSON.stringify(body) : undefined
99
- });
99
+ body: body ? JSON.stringify(body) : undefined,
100
+ };
101
+
102
+ if (options?.timeoutMs) {
103
+ const controller = new AbortController();
104
+ timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
105
+ fetchOptions.signal = controller.signal;
106
+ }
107
+
108
+ const response = await fetch(url, fetchOptions);
109
+ if (timeoutId) clearTimeout(timeoutId);
100
110
 
101
111
  // Check if we should retry this status code
102
112
  if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
@@ -132,7 +142,13 @@ export class VibescopeApiClient {
132
142
  data
133
143
  };
134
144
  } catch (err) {
135
- lastError = err instanceof Error ? err : new Error('Network error');
145
+ if (timeoutId) clearTimeout(timeoutId);
146
+ // Detect AbortError from timeout
147
+ if (err instanceof Error && err.name === 'AbortError' && options?.timeoutMs) {
148
+ lastError = new Error(`Request timed out after ${options.timeoutMs}ms`);
149
+ } else {
150
+ lastError = err instanceof Error ? err : new Error('Network error');
151
+ }
136
152
  // Retry on network errors (connection failures, timeouts)
137
153
  if (attempt < maxRetries) {
138
154
  const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
@@ -170,7 +186,7 @@ export class VibescopeApiClient {
170
186
  }
171
187
 
172
188
  // Auth endpoints
173
- async validateAuth(): Promise<ApiResponse<{
189
+ async validateAuth(options?: { timeoutMs?: number }): Promise<ApiResponse<{
174
190
  valid: boolean;
175
191
  user_id: string;
176
192
  api_key_id: string;
@@ -178,7 +194,7 @@ export class VibescopeApiClient {
178
194
  }>> {
179
195
  return this.request('POST', '/api/mcp/auth/validate', {
180
196
  api_key: this.apiKey
181
- });
197
+ }, options);
182
198
  }
183
199
 
184
200
  // Session endpoints
package/src/cli-init.ts CHANGED
@@ -275,16 +275,17 @@ function writeJsonFile(path: string, data: Record<string, unknown>): void {
275
275
 
276
276
  function buildMcpServerConfig(apiKey: string): Record<string, unknown> {
277
277
  const isWindows = platform() === 'win32';
278
+ // Prefer globally installed binary (instant start) with npx fallback
278
279
  if (isWindows) {
279
280
  return {
280
281
  command: 'cmd',
281
- args: ['/c', 'npx', '-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
282
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
282
283
  env: { VIBESCOPE_API_KEY: apiKey },
283
284
  };
284
285
  }
285
286
  return {
286
- command: 'npx',
287
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
287
+ command: 'bash',
288
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
288
289
  env: { VIBESCOPE_API_KEY: apiKey },
289
290
  };
290
291
  }
@@ -348,6 +348,12 @@ export const TOOL_CATEGORIES: Record<string, { description: string; tools: Array
348
348
  { name: 'update_mcp_server', brief: 'Self-update the MCP server' },
349
349
  ],
350
350
  },
351
+ persona_templates: {
352
+ description: 'Persona templates — behavioral archetypes that shape agent behavior',
353
+ tools: [
354
+ { name: 'get_persona_templates', brief: 'List available persona templates for a project' },
355
+ ],
356
+ },
351
357
  features: {
352
358
  description: 'Feature specs — flesh out ideas into full specs before breaking into tasks',
353
359
  tools: [
@@ -1464,6 +1464,18 @@ Read recent project chat messages to stay informed about project communication.
1464
1464
 
1465
1465
  **Example:** get_project_messages(project_id: "123e4567-e89b-12d3-a456-426614174000", limit: 10)`,
1466
1466
 
1467
+ post_progress: `# post_progress
1468
+ Post a structured progress update to the project chat. Use at key milestones: starting a task, creating a PR, hitting a blocker, or completing work.
1469
+
1470
+ **Parameters:**
1471
+ - project_id (required): Project UUID
1472
+ - message (required): Progress update message (supports markdown)
1473
+ - type (optional): Update type — info (general), milestone (key achievement), blocker (blocking issue), question (asking for input). Default: info
1474
+
1475
+ **Returns:** Confirmation with message_id and timestamp.
1476
+
1477
+ **Example:** post_progress(project_id: "123e4567-e89b-12d3-a456-426614174000", message: "PR #42 created for auth feature", type: "milestone")`,
1478
+
1467
1479
  // Version management tools
1468
1480
  check_mcp_version: `# check_mcp_version
1469
1481
  Check for available MCP server updates and version information.
@@ -1497,6 +1509,18 @@ Self-update the MCP server to the latest available version.
1497
1509
 
1498
1510
  **Note:** This operation may temporarily disconnect active sessions during restart.`,
1499
1511
 
1512
+ // Persona template tools
1513
+ get_persona_templates: `# get_persona_templates
1514
+ List available persona templates for a project. Returns both global defaults and project-specific templates.
1515
+
1516
+ **Parameters:**
1517
+ - project_id (required): Project UUID
1518
+
1519
+ **Returns:**
1520
+ - templates: Array of persona templates with id, name, description, focus_areas, icon, is_default, scope (global/project)
1521
+
1522
+ **Example:** get_persona_templates(project_id: "123e4567-e89b-12d3-a456-426614174000")`,
1523
+
1500
1524
  add_feature: `# add_feature
1501
1525
  Create a draft feature spec. Features sit between ideas (quick thoughts) and tasks (actionable work).
1502
1526
 
@@ -11,7 +11,7 @@ const PACKAGE_NAME = '@vibescope/mcp-server';
11
11
 
12
12
  export const versionHandlers: HandlerRegistry = {
13
13
  check_mcp_version: async (_args, _ctx) => {
14
- const info = await checkVersion();
14
+ const info = await checkVersion({ bypassCache: true });
15
15
 
16
16
  if (info.error) {
17
17
  return success({
package/src/index.ts CHANGED
@@ -574,9 +574,9 @@ initApiClient({ apiKey: API_KEY });
574
574
  // Authentication
575
575
  // ============================================================================
576
576
 
577
- async function validateApiKey(): Promise<AuthContext | null> {
577
+ async function validateApiKey(timeoutMs?: number): Promise<AuthContext | null> {
578
578
  const apiClient = getApiClient();
579
- const response = await apiClient.validateAuth();
579
+ const response = await apiClient.validateAuth(timeoutMs ? { timeoutMs } : undefined);
580
580
 
581
581
  if (!response.ok || !response.data?.valid) {
582
582
  return null;
@@ -589,6 +589,66 @@ async function validateApiKey(): Promise<AuthContext | null> {
589
589
  };
590
590
  }
591
591
 
592
+ // Deferred auth: started eagerly but awaited on first tool call
593
+ let deferredAuthPromise: Promise<AuthContext | null> | null = null;
594
+ let resolvedAuth: AuthContext | null = null;
595
+ let authError: string | null = null;
596
+
597
+ function startDeferredAuth(): void {
598
+ deferredAuthPromise = validateApiKey(10000)
599
+ .then((auth) => {
600
+ resolvedAuth = auth;
601
+ if (!auth) authError = 'Invalid API key';
602
+ return auth;
603
+ })
604
+ .catch((err) => {
605
+ authError = err instanceof Error ? err.message : 'Auth validation failed';
606
+ return null;
607
+ });
608
+ }
609
+
610
+ async function getAuth(): Promise<AuthContext> {
611
+ // If already resolved, return immediately
612
+ if (resolvedAuth) return resolvedAuth;
613
+
614
+ // Await the deferred promise
615
+ if (deferredAuthPromise) {
616
+ const auth = await deferredAuthPromise;
617
+ if (auth) return auth;
618
+ }
619
+
620
+ // Auth failed — return structured error
621
+ const troubleshooting = [
622
+ 'MCP auth validation failed.',
623
+ authError ? `Reason: ${authError}` : '',
624
+ 'Troubleshooting:',
625
+ '1. Check your API key is valid at https://vibescope.dev/dashboard/settings',
626
+ '2. Verify VIBESCOPE_API_KEY environment variable is set correctly',
627
+ '3. Check network connectivity to vibescope.dev',
628
+ '4. Restart Claude Code and try again',
629
+ ].filter(Boolean).join('\n');
630
+
631
+ throw new Error(troubleshooting);
632
+ }
633
+
634
+ // Deferred update warning: fire-and-forget, delivered on first tool call
635
+ let updateWarningPromise: Promise<string | null> | null = null;
636
+ let resolvedUpdateWarning: string | null | undefined = undefined; // undefined = not yet resolved
637
+
638
+ function startDeferredUpdateCheck(): void {
639
+ updateWarningPromise = getUpdateWarning().catch(() => null);
640
+ updateWarningPromise.then((warning) => {
641
+ resolvedUpdateWarning = warning;
642
+ });
643
+ }
644
+
645
+ function consumeUpdateWarning(): string | null {
646
+ if (resolvedUpdateWarning === undefined) return null; // not ready yet
647
+ const warning = resolvedUpdateWarning;
648
+ resolvedUpdateWarning = null; // deliver only once
649
+ return warning;
650
+ }
651
+
592
652
  // Tool definitions imported from tools.ts
593
653
 
594
654
 
@@ -648,19 +708,11 @@ async function handleTool(
648
708
  // ============================================================================
649
709
 
650
710
  async function main() {
651
- // Validate API key on startup via API
652
- const auth = await validateApiKey();
653
- if (!auth) {
654
- console.error('Invalid API key');
655
- process.exit(1);
656
- }
657
-
658
- // Check for updates (non-blocking, with timeout)
659
- const updateWarning = await getUpdateWarning();
711
+ // Start auth validation eagerly in background (10s timeout) — don't block startup
712
+ startDeferredAuth();
660
713
 
661
- const serverInstructions = updateWarning
662
- ? `${updateWarning}\n\nVibescope MCP server - AI project tracking and coordination tools.`
663
- : 'Vibescope MCP server - AI project tracking and coordination tools.';
714
+ // Start update check in background — delivered on first tool call if available
715
+ startDeferredUpdateCheck();
664
716
 
665
717
  const server = new Server(
666
718
  {
@@ -671,7 +723,7 @@ async function main() {
671
723
  capabilities: {
672
724
  tools: {},
673
725
  },
674
- instructions: serverInstructions,
726
+ instructions: 'Vibescope MCP server - AI project tracking and coordination tools.',
675
727
  }
676
728
  );
677
729
 
@@ -695,6 +747,22 @@ async function main() {
695
747
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
696
748
  const { name, arguments: args } = request.params;
697
749
 
750
+ // Await deferred auth on first tool call (usually already resolved)
751
+ let auth: AuthContext;
752
+ try {
753
+ auth = await getAuth();
754
+ } catch (err) {
755
+ return {
756
+ content: [
757
+ {
758
+ type: 'text',
759
+ text: err instanceof Error ? err.message : 'Auth validation failed',
760
+ },
761
+ ],
762
+ isError: true,
763
+ };
764
+ }
765
+
698
766
  // Check rate limit
699
767
  const rateCheck = rateLimiter.check(auth.apiKeyId);
700
768
  if (!rateCheck.allowed) {
@@ -728,6 +796,15 @@ async function main() {
728
796
  },
729
797
  ];
730
798
 
799
+ // Deliver update warning on first tool call (if available)
800
+ const updateWarning = consumeUpdateWarning();
801
+ if (updateWarning) {
802
+ content.push({
803
+ type: 'text',
804
+ text: `\n--- ${updateWarning} ---`,
805
+ });
806
+ }
807
+
731
808
  // Include reminder nudge if applicable
732
809
  const reminder = getReminder(name);
733
810
  if (reminder) {
package/src/setup.test.ts CHANGED
@@ -186,8 +186,11 @@ describe('Setup module', () => {
186
186
  const mcpServers = config.mcpServers as Record<string, unknown>;
187
187
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
188
188
 
189
- expect(vibescope.command).toBe('npx');
190
- expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
189
+ // Config uses shell wrapper to prefer global binary with npx fallback
190
+ expect(['cmd', 'bash']).toContain(vibescope.command);
191
+ const argsStr = JSON.stringify(vibescope.args);
192
+ expect(argsStr).toContain('vibescope-mcp');
193
+ expect(argsStr).toContain('npx');
191
194
  expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
192
195
  // Standard MCP config should NOT have timeout/trust
193
196
  expect(vibescope.timeout).toBeUndefined();
@@ -207,14 +210,16 @@ describe('Setup module', () => {
207
210
  const mcpServers = config.mcpServers as Record<string, unknown>;
208
211
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
209
212
 
210
- expect(vibescope.command).toBe('npx');
211
- expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
213
+ expect(['cmd', 'bash']).toContain(vibescope.command);
214
+ const argsStr = JSON.stringify(vibescope.args);
215
+ expect(argsStr).toContain('vibescope-mcp');
216
+ expect(argsStr).toContain('npx');
212
217
  expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
213
218
  expect(vibescope.timeout).toBe(30000);
214
219
  expect(vibescope.trust).toBe(true);
215
220
  });
216
221
 
217
- it('should use correct npx args format', () => {
222
+ it('should use shell wrapper with global binary fallback', () => {
218
223
  const ide: IdeConfig = {
219
224
  name: 'claude-code',
220
225
  displayName: 'Claude Code (CLI)',
@@ -226,8 +231,13 @@ describe('Setup module', () => {
226
231
  const config = generateMcpConfig('my-key', ide);
227
232
  const mcpServers = config.mcpServers as Record<string, unknown>;
228
233
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
234
+ const args = vibescope.args as string[];
229
235
 
230
- expect(vibescope.args).toEqual(['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp']);
236
+ // Should check for global binary first, then fall back to npx
237
+ const shellCmd = args.join(' ');
238
+ expect(shellCmd).toContain('vibescope-mcp');
239
+ expect(shellCmd).toContain('npx');
240
+ expect(shellCmd).toContain('@vibescope/mcp-server@latest');
231
241
  });
232
242
  });
233
243
  });
package/src/setup.ts CHANGED
@@ -210,13 +210,19 @@ export function writeConfig(configPath: string, config: Record<string, unknown>)
210
210
  }
211
211
 
212
212
  export function generateMcpConfig(apiKey: string, ide: IdeConfig): Record<string, unknown> {
213
- const vibescopeServer = {
214
- command: 'npx',
215
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
216
- env: {
217
- VIBESCOPE_API_KEY: apiKey,
218
- },
219
- };
213
+ // Prefer globally installed binary (instant start) with npx fallback
214
+ const isWindows = platform() === 'win32';
215
+ const vibescopeServer = isWindows
216
+ ? {
217
+ command: 'cmd',
218
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
219
+ env: { VIBESCOPE_API_KEY: apiKey },
220
+ }
221
+ : {
222
+ command: 'bash',
223
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
224
+ env: { VIBESCOPE_API_KEY: apiKey },
225
+ };
220
226
 
221
227
  // Gemini CLI uses a different config format with additional options
222
228
  if (ide.configFormat === 'settings-json') {
@@ -36,6 +36,7 @@ import { cloudAgentTools } from './cloud-agents.js';
36
36
  import { versionTools } from './version.js';
37
37
  import { chatTools } from './chat.js';
38
38
  import { featureTools } from './features.js';
39
+ import { personaTemplateTools } from './persona-templates.js';
39
40
 
40
41
  /**
41
42
  * All MCP tool definitions combined
@@ -68,6 +69,7 @@ export const tools: Tool[] = [
68
69
  ...versionTools,
69
70
  ...chatTools,
70
71
  ...featureTools,
72
+ ...personaTemplateTools,
71
73
  ];
72
74
 
73
75
  /**
@@ -108,6 +110,7 @@ export const toolCategories = {
108
110
  cloudAgents: cloudAgentTools.map((t) => t.name),
109
111
  version: versionTools.map((t) => t.name),
110
112
  features: featureTools.map((t) => t.name),
113
+ personaTemplates: personaTemplateTools.map((t) => t.name),
111
114
  } as const;
112
115
 
113
116
  // Re-export domain tools for selective imports
@@ -138,4 +141,5 @@ export {
138
141
  cloudAgentTools,
139
142
  versionTools,
140
143
  featureTools,
144
+ personaTemplateTools,
141
145
  };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Persona Template Tool Definitions
3
+ *
4
+ * Tools for managing persona templates:
5
+ * - get_persona_templates
6
+ */
7
+
8
+ import type { Tool } from './types.js';
9
+
10
+ export const personaTemplateTools: Tool[] = [
11
+ {
12
+ name: 'get_persona_templates',
13
+ description: 'List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ project_id: {
18
+ type: 'string',
19
+ description: 'Project UUID',
20
+ },
21
+ },
22
+ required: ['project_id'],
23
+ },
24
+ },
25
+ ];
@@ -225,6 +225,10 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
225
225
  enum: ['haiku', 'sonnet', 'opus'],
226
226
  description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
227
227
  },
228
+ assigned_agent_name: {
229
+ type: 'string',
230
+ description: 'Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task.',
231
+ },
228
232
  },
229
233
  required: ['project_id', 'title'],
230
234
  },
@@ -295,6 +299,10 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
295
299
  enum: ['haiku', 'sonnet', 'opus'],
296
300
  description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
297
301
  },
302
+ assigned_agent_name: {
303
+ type: ['string', 'null'],
304
+ description: 'Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task.',
305
+ },
298
306
  skip_worktree_requirement: {
299
307
  type: 'boolean',
300
308
  description: 'Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false',
package/src/version.ts CHANGED
@@ -2,15 +2,26 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
9
 
8
- import { readFileSync } from 'fs';
9
- import { resolve, dirname } from 'path';
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
11
+ import { resolve, dirname, join } from 'path';
10
12
  import { fileURLToPath } from 'url';
13
+ import { homedir } from 'os';
11
14
 
12
15
  const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@vibescope/mcp-server';
13
16
  const PACKAGE_NAME = '@vibescope/mcp-server';
17
+ const CACHE_DIR = join(homedir(), '.vibescope');
18
+ const CACHE_PATH = join(CACHE_DIR, 'version-cache.json');
19
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
20
+
21
+ interface VersionCache {
22
+ latestVersion: string;
23
+ fetchedAt: number; // epoch ms
24
+ }
14
25
 
15
26
  export interface VersionInfo {
16
27
  current: string;
@@ -33,13 +44,50 @@ export function getLocalVersion(): string {
33
44
  }
34
45
  }
35
46
 
47
+ /**
48
+ * Read version from file-based cache if still valid
49
+ */
50
+ function readVersionCache(): string | null {
51
+ try {
52
+ if (!existsSync(CACHE_PATH)) return null;
53
+ const raw = JSON.parse(readFileSync(CACHE_PATH, 'utf-8')) as VersionCache;
54
+ if (Date.now() - raw.fetchedAt < CACHE_TTL_MS) {
55
+ return raw.latestVersion;
56
+ }
57
+ return null; // expired
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Write version to file-based cache
65
+ */
66
+ function writeVersionCache(version: string): void {
67
+ try {
68
+ if (!existsSync(CACHE_DIR)) {
69
+ mkdirSync(CACHE_DIR, { recursive: true });
70
+ }
71
+ const cache: VersionCache = { latestVersion: version, fetchedAt: Date.now() };
72
+ writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2) + '\n');
73
+ } catch {
74
+ // Non-critical — silently ignore write failures
75
+ }
76
+ }
77
+
36
78
  /**
37
79
  * Fetch the latest published version from npm registry
38
80
  */
39
- export async function getLatestVersion(): Promise<string | null> {
81
+ export async function getLatestVersion(options?: { bypassCache?: boolean }): Promise<string | null> {
82
+ // Check cache first (unless bypassed)
83
+ if (!options?.bypassCache) {
84
+ const cached = readVersionCache();
85
+ if (cached) return cached;
86
+ }
87
+
40
88
  try {
41
89
  const controller = new AbortController();
42
- const timeout = setTimeout(() => controller.abort(), 5000);
90
+ const timeout = setTimeout(() => controller.abort(), 3000);
43
91
 
44
92
  const response = await fetch(`${NPM_REGISTRY_URL}/latest`, {
45
93
  signal: controller.signal,
@@ -51,7 +99,12 @@ export async function getLatestVersion(): Promise<string | null> {
51
99
  if (!response.ok) return null;
52
100
 
53
101
  const data = (await response.json()) as { version?: string };
54
- return data.version ?? null;
102
+ const version = data.version ?? null;
103
+
104
+ // Write to cache on successful fetch
105
+ if (version) writeVersionCache(version);
106
+
107
+ return version;
55
108
  } catch {
56
109
  return null;
57
110
  }
@@ -75,9 +128,9 @@ function isNewer(current: string, latest: string): boolean {
75
128
  /**
76
129
  * Check if an update is available
77
130
  */
78
- export async function checkVersion(): Promise<VersionInfo> {
131
+ export async function checkVersion(options?: { bypassCache?: boolean }): Promise<VersionInfo> {
79
132
  const current = getLocalVersion();
80
- const latest = await getLatestVersion();
133
+ const latest = await getLatestVersion(options);
81
134
 
82
135
  if (!latest) {
83
136
  return {