@askmesh/mcp 0.10.3 → 0.10.5

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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ const TOKEN = process.env.ASKMESH_TOKEN;
11
11
  const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
12
12
  const server = new McpServer({
13
13
  name: 'askmesh',
14
- version: '0.10.3',
14
+ version: '0.10.5',
15
15
  });
16
16
  if (!TOKEN) {
17
17
  // No token — start in setup-only mode
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { mkdirSync, writeFileSync, readFileSync, appendFileSync, existsSync, unlinkSync, copyFileSync, chmodSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { createHash } from 'crypto';
5
6
  import { fileURLToPath } from 'url';
6
7
  import * as statusCache from '../statusline/cache.js';
7
8
  export function registerAskMesh(server, client) {
@@ -33,8 +34,9 @@ Actions disponibles :
33
34
  - "progress" : marquer un thread comme "en cours de traitement"
34
35
  - "broadcast" : envoyer un message à toute ton équipe (notification Telegram/Slack, sans créer de thread)
35
36
  - "context" : partager ton contexte projet avec ton équipe
36
- - "setup" : installer les slash commands (/inbox, /broadcast, etc.) et la status line dans le projet courant`, {
37
- action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup']).describe('Action à effectuer'),
37
+ - "setup" : installer les slash commands (/ask-inbox, /ask-broadcast, etc.) et la status line dans le projet courant
38
+ - "update" : vérifier les mises à jour du MCP, mettre à jour les skills et la status line`, {
39
+ action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup', 'update']).describe('Action à effectuer'),
38
40
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
39
41
  question: z.string().optional().describe('Question à poser (pour ask)'),
40
42
  requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
@@ -106,23 +108,39 @@ Actions disponibles :
106
108
  const { requests } = await client.getSentRequests();
107
109
  if (requests.length === 0)
108
110
  return text('Aucune question envoyée.');
109
- const lines = requests.map((r) => {
110
- const icon = r.status === 'closed' ? '✅' : r.status === 'active' ? '💬' : r.status === 'answered' ? '✅' : '⏳';
111
- let line = `${icon} #${r.id} @${r.toUsername}: "${r.question}" [${r.status}]`;
112
- if (r.answer)
113
- line += `\n Réponse: ${r.answer}`;
114
- return line;
115
- });
116
- return text(`Questions envoyées:\n\n${lines.join('\n\n')}`);
111
+ // Separate threads with unread replies from the rest
112
+ const withReply = requests.filter((r) => r.status === 'active' || r.status === 'answered');
113
+ const pending = requests.filter((r) => r.status === 'pending' || r.status === 'in_progress');
114
+ const closed = requests.filter((r) => r.status === 'closed');
115
+ const sections = [];
116
+ if (withReply.length > 0) {
117
+ const lines = withReply.map((r) => {
118
+ let line = `🔔 #${r.id} → @${r.toUsername}: "${r.question}"`;
119
+ if (r.answer)
120
+ line += `\n Réponse: ${r.answer}`;
121
+ line += `\n → Utilise action "thread" requestId ${r.id} pour voir le fil complet, ou "close" pour clôturer.`;
122
+ return line;
123
+ });
124
+ sections.push(`Réponses à lire (${withReply.length}):\n\n${lines.join('\n\n')}`);
125
+ }
126
+ if (pending.length > 0) {
127
+ const lines = pending.map((r) => `⏳ #${r.id} → @${r.toUsername}: "${r.question}" [${r.status}]`);
128
+ sections.push(`En attente de réponse (${pending.length}):\n${lines.join('\n')}`);
129
+ }
130
+ if (closed.length > 0) {
131
+ const lines = closed.map((r) => `✅ #${r.id} → @${r.toUsername}: "${r.question}"`);
132
+ sections.push(`Terminés (${closed.length}):\n${lines.join('\n')}`);
133
+ }
134
+ if (sections.length === 0)
135
+ return text('Rien à traiter, tout est à jour.');
136
+ return text(sections.join('\n\n---\n\n'));
117
137
  }
118
138
  case 'answer': {
119
139
  if (!requestId || !message) {
120
140
  return text("Paramètres requis : requestId et message");
121
141
  }
122
142
  const result = await client.answerRequest(requestId, message);
123
- // Auto-close thread after answering
124
- await client.closeThread(requestId).catch(() => { });
125
- return text(`Réponse envoyée et thread #${result.id} clôturé.`);
143
+ return text(`Réponse envoyée au thread #${result.id}. Le demandeur peut consulter la réponse via /ask-inbox.`);
126
144
  }
127
145
  case 'reply': {
128
146
  if (!requestId || !message) {
@@ -212,8 +230,11 @@ Actions disponibles :
212
230
  case 'setup': {
213
231
  return setupSkillsAndStatusLine(token);
214
232
  }
233
+ case 'update': {
234
+ return await checkUpdateAndSetup();
235
+ }
215
236
  default:
216
- return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup');
237
+ return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup, update');
217
238
  }
218
239
  });
219
240
  }
@@ -243,9 +264,46 @@ Si l'utilisateur n'a pas encore de compte, dirige-le vers https://askmesh.dev po
243
264
  return text('Action inconnue.');
244
265
  });
245
266
  }
267
+ const CURRENT_VERSION = '0.10.5';
268
+ async function checkUpdateAndSetup() {
269
+ const lines = [];
270
+ lines.push(`Version actuelle : ${CURRENT_VERSION}`);
271
+ // Check latest version on npm
272
+ try {
273
+ const res = await fetch('https://registry.npmjs.org/@askmesh/mcp/latest');
274
+ const data = await res.json();
275
+ const latest = data.version;
276
+ lines.push(`Dernière version npm : ${latest}`);
277
+ if (latest !== CURRENT_VERSION) {
278
+ lines.push(`\n⚡ Mise à jour disponible ! ${CURRENT_VERSION} → ${latest}`);
279
+ lines.push(`Pour mettre à jour, relance Claude Code — npx avec @latest récupérera la nouvelle version.`);
280
+ lines.push(`Ou lance : npx clear-npx-cache && npx -y @askmesh/mcp@latest`);
281
+ }
282
+ else {
283
+ lines.push(`\n✓ Tu es à jour.`);
284
+ }
285
+ }
286
+ catch {
287
+ lines.push(`Impossible de vérifier la version npm.`);
288
+ }
289
+ // Re-run setup to update skills and status line
290
+ const setupResult = setupSkillsAndStatusLine();
291
+ const setupText = setupResult.content[0].text;
292
+ lines.push('\n---\n');
293
+ lines.push(setupText);
294
+ return text(lines.join('\n'));
295
+ }
246
296
  function text(t) {
247
297
  return { content: [{ type: 'text', text: t }] };
248
298
  }
299
+ function readEnvToken(envPath) {
300
+ if (!existsSync(envPath))
301
+ return undefined;
302
+ const content = readFileSync(envPath, 'utf-8');
303
+ const match = content.match(/ASKMESH_TOKEN=(.+)/);
304
+ const val = match?.[1]?.trim();
305
+ return val && val !== 'your_token_here' ? val : undefined;
306
+ }
249
307
  function setupSkillsAndStatusLine(token, pollInterval, url) {
250
308
  const cwd = process.cwd();
251
309
  const commandsDir = join(cwd, '.claude', 'commands');
@@ -260,6 +318,7 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
260
318
  'ask-threads.md': `Utilise l'outil MCP askmesh avec l'action "my-threads" pour afficher toutes mes conversations actives.\n\nPrésente les threads regroupés par statut :\n- En attente (pending)\n- En cours (in_progress)\n- Actifs (active)\n\nIgnore les threads clos. Pour chaque thread, montre : l'ID, qui a posé la question, le sujet, et le nombre de réponses.`,
261
319
  'ask-status.md': `Utilise l'outil MCP askmesh avec l'action "list" pour voir qui est connecté sur le réseau.\n\nAffiche la liste des membres de l'équipe avec leur statut (online/offline) de manière claire et concise.`,
262
320
  'ask-setup.md': `Utilise l'outil MCP askmesh avec l'action "setup" pour installer ou mettre à jour la configuration AskMesh dans ce projet.\n\nCela va :\n- Créer ou vérifier le .env avec le token AskMesh\n- Installer les slash commands (/ask-inbox, /ask-broadcast, etc.)\n- Configurer la status line\n- Ajouter .env au .gitignore\n\nSi l'utilisateur fournit un token dans les arguments ($ARGUMENTS), passe-le en paramètre "token".`,
321
+ 'ask-update.md': `Utilise l'outil MCP askmesh avec l'action "update" pour vérifier les mises à jour et mettre à jour les skills et la status line.\n\nCela va :\n- Vérifier si une nouvelle version du MCP est disponible sur npm\n- Mettre à jour les slash commands\n- Mettre à jour le script de status line\n\nSi une mise à jour est disponible, indique à l'utilisateur comment l'installer (relancer Claude Code ou npx clear-npx-cache).`,
263
322
  };
264
323
  // Create commands directory
265
324
  try {
@@ -345,33 +404,49 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
345
404
  gitignoreStatus = '.gitignore : .env ajouté';
346
405
  }
347
406
  }
348
- // Auto-configure status line
349
- // Copy statusline.sh to a stable location (~/.claude/) so it survives npx cache clears
407
+ // Auto-configure status line — per-project, not global
408
+ // Copy statusline.sh to ~/.claude/ (stable location), configure per-project with agent hash
350
409
  const mcpDir = dirname(fileURLToPath(import.meta.url));
351
410
  const sourceScript = join(mcpDir, '..', '..', 'statusline.sh');
352
- const claudeSettingsDir = join(homedir(), '.claude');
353
- const stableScript = join(claudeSettingsDir, 'askmesh-statusline.sh');
354
- const claudeSettingsPath = join(claudeSettingsDir, 'settings.json');
411
+ const globalClaudeDir = join(homedir(), '.claude');
412
+ const stableScript = join(globalClaudeDir, 'askmesh-statusline.sh');
413
+ const projectClaudeDir = join(cwd, '.claude');
414
+ const projectSettingsPath = join(projectClaudeDir, 'settings.local.json');
355
415
  let statusLineStatus = '';
416
+ // Compute token hash for this agent
417
+ const envToken = token || readEnvToken(envPath);
418
+ const tokenHash = envToken ? createHash('md5').update(envToken).digest('hex').slice(0, 8) : '';
419
+ const statusLineCommand = tokenHash
420
+ ? `${stableScript} ${tokenHash}`
421
+ : stableScript;
356
422
  try {
357
- mkdirSync(claudeSettingsDir, { recursive: true });
423
+ mkdirSync(globalClaudeDir, { recursive: true });
424
+ mkdirSync(projectClaudeDir, { recursive: true });
358
425
  // Copy script to stable location
359
426
  if (existsSync(sourceScript)) {
360
427
  copyFileSync(sourceScript, stableScript);
361
428
  chmodSync(stableScript, 0o755);
362
429
  }
363
430
  if (existsSync(stableScript)) {
431
+ // Write to project-level settings (not global)
364
432
  let settings = {};
365
- if (existsSync(claudeSettingsPath)) {
366
- settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
367
- }
368
- if (settings.statusLine?.command === stableScript) {
369
- statusLineStatus = 'Status line : déjà configurée';
433
+ if (existsSync(projectSettingsPath)) {
434
+ settings = JSON.parse(readFileSync(projectSettingsPath, 'utf-8'));
370
435
  }
371
- else {
372
- settings.statusLine = { type: 'command', command: stableScript };
373
- writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
374
- statusLineStatus = 'Status line : configurée automatiquement';
436
+ settings.statusLine = { type: 'command', command: statusLineCommand };
437
+ writeFileSync(projectSettingsPath, JSON.stringify(settings, null, 2) + '\n');
438
+ statusLineStatus = `Status line : configurée pour ce projet${tokenHash ? ` (agent ${tokenHash})` : ''}`;
439
+ // Clean up global settings if it had the old config
440
+ const globalSettingsPath = join(globalClaudeDir, 'settings.json');
441
+ if (existsSync(globalSettingsPath)) {
442
+ try {
443
+ const globalSettings = JSON.parse(readFileSync(globalSettingsPath, 'utf-8'));
444
+ if (globalSettings.statusLine?.command?.includes('askmesh')) {
445
+ delete globalSettings.statusLine;
446
+ writeFileSync(globalSettingsPath, JSON.stringify(globalSettings, null, 2) + '\n');
447
+ }
448
+ }
449
+ catch { }
375
450
  }
376
451
  }
377
452
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "AskMesh MCP server — connect your AI coding agent to your team's mesh network",
5
5
  "type": "module",
6
6
  "bin": {
package/statusline.sh CHANGED
@@ -1,41 +1,53 @@
1
1
  #!/bin/bash
2
2
  # AskMesh status line for Claude Code
3
- # Reads cached notification counts from MCP server(s)
4
- # Aggregates across all active agent caches
3
+ # Usage: statusline.sh [token_hash]
4
+ # If token_hash provided, reads only that agent's cache
5
+ # Otherwise reads the single cache file
5
6
 
6
7
  input=$(cat)
7
8
 
8
9
  tmpdir="${TMPDIR:-/tmp}"
10
+ hash="$1"
9
11
  pending=0
10
12
  active=0
11
13
  replies=0
12
14
 
13
- # Aggregate all agent cache files
14
- for f in "$tmpdir"/askmesh_status_*.json; do
15
- [ -f "$f" ] || continue
16
- p=$(jq -r '.pending // 0' "$f" 2>/dev/null)
17
- a=$(jq -r '.active // 0' "$f" 2>/dev/null)
18
- r=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
19
- pending=$((pending + p))
20
- active=$((active + a))
21
- replies=$((replies + r))
22
- done
15
+ if [ -n "$hash" ]; then
16
+ # Read specific agent cache
17
+ f="$tmpdir/askmesh_status_${hash}.json"
18
+ if [ -f "$f" ]; then
19
+ pending=$(jq -r '.pending // 0' "$f" 2>/dev/null)
20
+ active=$(jq -r '.active // 0' "$f" 2>/dev/null)
21
+ replies=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
22
+ fi
23
+ else
24
+ # Fallback: aggregate all agent caches
25
+ for f in "$tmpdir"/askmesh_status_*.json; do
26
+ [ -f "$f" ] || continue
27
+ p=$(jq -r '.pending // 0' "$f" 2>/dev/null)
28
+ a=$(jq -r '.active // 0' "$f" 2>/dev/null)
29
+ r=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
30
+ pending=$((pending + p))
31
+ active=$((active + a))
32
+ replies=$((replies + r))
33
+ done
34
+ fi
23
35
 
24
36
  parts=()
25
37
 
26
38
  if [ "$pending" -gt 0 ] 2>/dev/null; then
27
- parts+=("\033[33m${pending}↓\033[0m")
39
+ parts+=("\033[33m${pending} pending\033[0m")
28
40
  fi
29
41
  if [ "$active" -gt 0 ] 2>/dev/null; then
30
- parts+=("\033[32m${active}~\033[0m")
42
+ parts+=("\033[32m${active} active\033[0m")
31
43
  fi
32
44
  if [ "$replies" -gt 0 ] 2>/dev/null; then
33
- parts+=("\033[36m${replies}>\033[0m")
45
+ parts+=("\033[36m${replies} replies\033[0m")
34
46
  fi
35
47
 
36
48
  if [ ${#parts[@]} -gt 0 ]; then
37
- joined=$(IFS=' '; echo "${parts[*]}")
38
- echo -e "mesh $joined"
49
+ joined=$(IFS=' · '; echo "${parts[*]}")
50
+ echo -e "mesh > $joined"
39
51
  else
40
52
  echo "mesh"
41
53
  fi