@askmesh/mcp 0.10.4 → 0.10.7

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.
@@ -6,7 +6,14 @@ Use this context to answer accurately and concisely.
6
6
  Reference specific files, conventions, or decisions when relevant.
7
7
  If the context doesn't contain enough info, say so honestly.
8
8
  Answer in the same language as the question.
9
- Keep responses under 500 words unless more detail is needed.`;
9
+ Keep responses under 500 words unless more detail is needed.
10
+
11
+ SECURITY RULES:
12
+ - READ-ONLY: Never suggest or execute destructive operations (DELETE, DROP, rm, reset --hard, etc.)
13
+ - NO SECRETS: Never include passwords, API keys, tokens, or credentials in your responses
14
+ - NO SENSITIVE DATA: Never expose personal data, database connection strings, or internal URLs
15
+ - If asked for sensitive information, respond: "I can't share this information via AskMesh for security reasons."
16
+ - You may share: code patterns, architecture decisions, file structures, conventions, public configs`;
10
17
  export class AutoResponder {
11
18
  client;
12
19
  mcpServer = null;
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.4',
14
+ version: '0.10.7',
15
15
  });
16
16
  if (!TOKEN) {
17
17
  // No token — start in setup-only mode
@@ -2,6 +2,7 @@ interface StatusCache {
2
2
  pending: number;
3
3
  active: number;
4
4
  unread_replies: number;
5
+ loop_active: boolean;
5
6
  last_update: string;
6
7
  }
7
8
  /** Call once at startup with the agent token to isolate the cache file per agent */
@@ -13,4 +14,6 @@ export declare function onReplyAdded(): void;
13
14
  export declare function onThreadClosed(): void;
14
15
  export declare function onPendingFetched(count: number): void;
15
16
  export declare function syncFromApi(pending: number, active: number): void;
17
+ export declare function setLoopActive(active: boolean): void;
18
+ export declare function isLoopActive(): boolean;
16
19
  export {};
@@ -3,7 +3,7 @@ import { createHash } from 'crypto';
3
3
  import { tmpdir } from 'os';
4
4
  import { join } from 'path';
5
5
  let CACHE_FILE = join(tmpdir(), 'askmesh_status.json');
6
- let cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
6
+ let cache = { pending: 0, active: 0, unread_replies: 0, loop_active: false, last_update: '' };
7
7
  /** Call once at startup with the agent token to isolate the cache file per agent */
8
8
  export function init(token) {
9
9
  const hash = createHash('md5').update(token).digest('hex').slice(0, 8);
@@ -25,7 +25,7 @@ export function load() {
25
25
  cache = JSON.parse(raw);
26
26
  }
27
27
  catch {
28
- cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
28
+ cache = { pending: 0, active: 0, unread_replies: 0, loop_active: false, last_update: '' };
29
29
  }
30
30
  return cache;
31
31
  }
@@ -58,3 +58,12 @@ export function syncFromApi(pending, active) {
58
58
  cache.unread_replies = 0;
59
59
  flush();
60
60
  }
61
+ export function setLoopActive(active) {
62
+ load();
63
+ cache.loop_active = active;
64
+ flush();
65
+ }
66
+ export function isLoopActive() {
67
+ load();
68
+ return cache.loop_active;
69
+ }
@@ -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) {
@@ -34,8 +35,10 @@ Actions disponibles :
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
37
  - "setup" : installer les slash commands (/ask-inbox, /ask-broadcast, etc.) et la status line dans le projet courant
37
- - "update" : vérifier les mises à jour du MCP, mettre à jour les skills et la status line`, {
38
- action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup', 'update']).describe('Action à effectuer'),
38
+ - "update" : vérifier les mises à jour du MCP, mettre à jour les skills et la status line
39
+ - "loop-start" : marquer le loop comme actif (utilisé par /ask-loop)
40
+ - "loop-stop" : arrêter le loop (utilisé par /ask-loop-stop)`, {
41
+ action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup', 'update', 'loop-start', 'loop-stop']).describe('Action à effectuer'),
39
42
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
40
43
  question: z.string().optional().describe('Question à poser (pour ask)'),
41
44
  requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
@@ -107,23 +110,39 @@ Actions disponibles :
107
110
  const { requests } = await client.getSentRequests();
108
111
  if (requests.length === 0)
109
112
  return text('Aucune question envoyée.');
110
- const lines = requests.map((r) => {
111
- const icon = r.status === 'closed' ? '✅' : r.status === 'active' ? '💬' : r.status === 'answered' ? '✅' : '⏳';
112
- let line = `${icon} #${r.id} @${r.toUsername}: "${r.question}" [${r.status}]`;
113
- if (r.answer)
114
- line += `\n Réponse: ${r.answer}`;
115
- return line;
116
- });
117
- return text(`Questions envoyées:\n\n${lines.join('\n\n')}`);
113
+ // Separate threads with unread replies from the rest
114
+ const withReply = requests.filter((r) => r.status === 'active' || r.status === 'answered');
115
+ const pending = requests.filter((r) => r.status === 'pending' || r.status === 'in_progress');
116
+ const closed = requests.filter((r) => r.status === 'closed');
117
+ const sections = [];
118
+ if (withReply.length > 0) {
119
+ const lines = withReply.map((r) => {
120
+ let line = `🔔 #${r.id} → @${r.toUsername}: "${r.question}"`;
121
+ if (r.answer)
122
+ line += `\n Réponse: ${r.answer}`;
123
+ line += `\n → Utilise action "thread" requestId ${r.id} pour voir le fil complet, ou "close" pour clôturer.`;
124
+ return line;
125
+ });
126
+ sections.push(`Réponses à lire (${withReply.length}):\n\n${lines.join('\n\n')}`);
127
+ }
128
+ if (pending.length > 0) {
129
+ const lines = pending.map((r) => `⏳ #${r.id} → @${r.toUsername}: "${r.question}" [${r.status}]`);
130
+ sections.push(`En attente de réponse (${pending.length}):\n${lines.join('\n')}`);
131
+ }
132
+ if (closed.length > 0) {
133
+ const lines = closed.map((r) => `✅ #${r.id} → @${r.toUsername}: "${r.question}"`);
134
+ sections.push(`Terminés (${closed.length}):\n${lines.join('\n')}`);
135
+ }
136
+ if (sections.length === 0)
137
+ return text('Rien à traiter, tout est à jour.');
138
+ return text(sections.join('\n\n---\n\n'));
118
139
  }
119
140
  case 'answer': {
120
141
  if (!requestId || !message) {
121
142
  return text("Paramètres requis : requestId et message");
122
143
  }
123
144
  const result = await client.answerRequest(requestId, message);
124
- // Auto-close thread after answering
125
- await client.closeThread(requestId).catch(() => { });
126
- return text(`Réponse envoyée et thread #${result.id} clôturé.`);
145
+ return text(`Réponse envoyée au thread #${result.id}. Le demandeur peut consulter la réponse via /ask-inbox.`);
127
146
  }
128
147
  case 'reply': {
129
148
  if (!requestId || !message) {
@@ -216,8 +235,19 @@ Actions disponibles :
216
235
  case 'update': {
217
236
  return await checkUpdateAndSetup();
218
237
  }
238
+ case 'loop-start': {
239
+ if (statusCache.isLoopActive()) {
240
+ return text('Un loop est déjà actif. Utilise /ask-loop-stop pour l\'arrêter avant d\'en lancer un nouveau.');
241
+ }
242
+ statusCache.setLoopActive(true);
243
+ return text('Loop activé. La status line affiche maintenant l\'indicateur "loop".');
244
+ }
245
+ case 'loop-stop': {
246
+ statusCache.setLoopActive(false);
247
+ return text('Loop arrêté.');
248
+ }
219
249
  default:
220
- return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup, update');
250
+ return text('Action inconnue.');
221
251
  }
222
252
  });
223
253
  }
@@ -247,7 +277,7 @@ Si l'utilisateur n'a pas encore de compte, dirige-le vers https://askmesh.dev po
247
277
  return text('Action inconnue.');
248
278
  });
249
279
  }
250
- const CURRENT_VERSION = '0.10.4';
280
+ const CURRENT_VERSION = '0.10.7';
251
281
  async function checkUpdateAndSetup() {
252
282
  const lines = [];
253
283
  lines.push(`Version actuelle : ${CURRENT_VERSION}`);
@@ -279,6 +309,14 @@ async function checkUpdateAndSetup() {
279
309
  function text(t) {
280
310
  return { content: [{ type: 'text', text: t }] };
281
311
  }
312
+ function readEnvToken(envPath) {
313
+ if (!existsSync(envPath))
314
+ return undefined;
315
+ const content = readFileSync(envPath, 'utf-8');
316
+ const match = content.match(/ASKMESH_TOKEN=(.+)/);
317
+ const val = match?.[1]?.trim();
318
+ return val && val !== 'your_token_here' ? val : undefined;
319
+ }
282
320
  function setupSkillsAndStatusLine(token, pollInterval, url) {
283
321
  const cwd = process.cwd();
284
322
  const commandsDir = join(cwd, '.claude', 'commands');
@@ -294,6 +332,8 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
294
332
  '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.`,
295
333
  '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".`,
296
334
  '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).`,
335
+ 'ask-loop.md': `Lance une boucle de vérification périodique des messages AskMesh.\n\nÉtapes :\n1. D'abord, utilise l'outil MCP askmesh avec l'action "loop-start" pour vérifier qu'aucun loop n'est déjà actif et marquer le loop comme actif\n2. Si le loop-start réussit, lance : /loop <intervalle ou 2m> /ask-inbox\n\nArguments optionnels : $ARGUMENTS (intervalle, ex: "2m", "5m", "30s")\nPar défaut : 2m\n\nSi un loop est déjà actif, préviens l'utilisateur et propose /ask-loop-stop.`,
336
+ 'ask-loop-stop.md': `Arrête la boucle de vérification périodique des messages AskMesh.\n\nÉtapes :\n1. Utilise l'outil MCP askmesh avec l'action "loop-stop" pour marquer le loop comme inactif\n2. Indique à l'utilisateur que le loop est arrêté\n\nNote : cela met à jour la status line pour retirer l'indicateur "loop".`,
297
337
  };
298
338
  // Create commands directory
299
339
  try {
@@ -379,33 +419,49 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
379
419
  gitignoreStatus = '.gitignore : .env ajouté';
380
420
  }
381
421
  }
382
- // Auto-configure status line
383
- // Copy statusline.sh to a stable location (~/.claude/) so it survives npx cache clears
422
+ // Auto-configure status line — per-project, not global
423
+ // Copy statusline.sh to ~/.claude/ (stable location), configure per-project with agent hash
384
424
  const mcpDir = dirname(fileURLToPath(import.meta.url));
385
425
  const sourceScript = join(mcpDir, '..', '..', 'statusline.sh');
386
- const claudeSettingsDir = join(homedir(), '.claude');
387
- const stableScript = join(claudeSettingsDir, 'askmesh-statusline.sh');
388
- const claudeSettingsPath = join(claudeSettingsDir, 'settings.json');
426
+ const globalClaudeDir = join(homedir(), '.claude');
427
+ const stableScript = join(globalClaudeDir, 'askmesh-statusline.sh');
428
+ const projectClaudeDir = join(cwd, '.claude');
429
+ const projectSettingsPath = join(projectClaudeDir, 'settings.local.json');
389
430
  let statusLineStatus = '';
431
+ // Compute token hash for this agent
432
+ const envToken = token || readEnvToken(envPath);
433
+ const tokenHash = envToken ? createHash('md5').update(envToken).digest('hex').slice(0, 8) : '';
434
+ const statusLineCommand = tokenHash
435
+ ? `${stableScript} ${tokenHash}`
436
+ : stableScript;
390
437
  try {
391
- mkdirSync(claudeSettingsDir, { recursive: true });
438
+ mkdirSync(globalClaudeDir, { recursive: true });
439
+ mkdirSync(projectClaudeDir, { recursive: true });
392
440
  // Copy script to stable location
393
441
  if (existsSync(sourceScript)) {
394
442
  copyFileSync(sourceScript, stableScript);
395
443
  chmodSync(stableScript, 0o755);
396
444
  }
397
445
  if (existsSync(stableScript)) {
446
+ // Write to project-level settings (not global)
398
447
  let settings = {};
399
- if (existsSync(claudeSettingsPath)) {
400
- settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
448
+ if (existsSync(projectSettingsPath)) {
449
+ settings = JSON.parse(readFileSync(projectSettingsPath, 'utf-8'));
401
450
  }
402
- if (settings.statusLine?.command === stableScript) {
403
- statusLineStatus = 'Status line : déjà configurée';
404
- }
405
- else {
406
- settings.statusLine = { type: 'command', command: stableScript };
407
- writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
408
- statusLineStatus = 'Status line : configurée automatiquement';
451
+ settings.statusLine = { type: 'command', command: statusLineCommand };
452
+ writeFileSync(projectSettingsPath, JSON.stringify(settings, null, 2) + '\n');
453
+ statusLineStatus = `Status line : configurée pour ce projet${tokenHash ? ` (agent ${tokenHash})` : ''}`;
454
+ // Clean up global settings if it had the old config
455
+ const globalSettingsPath = join(globalClaudeDir, 'settings.json');
456
+ if (existsSync(globalSettingsPath)) {
457
+ try {
458
+ const globalSettings = JSON.parse(readFileSync(globalSettingsPath, 'utf-8'));
459
+ if (globalSettings.statusLine?.command?.includes('askmesh')) {
460
+ delete globalSettings.statusLine;
461
+ writeFileSync(globalSettingsPath, JSON.stringify(globalSettings, null, 2) + '\n');
462
+ }
463
+ }
464
+ catch { }
409
465
  }
410
466
  }
411
467
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.10.4",
3
+ "version": "0.10.7",
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,28 +1,47 @@
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
14
+ loop="false"
12
15
 
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
16
+ if [ -n "$hash" ]; then
17
+ # Read specific agent cache
18
+ f="$tmpdir/askmesh_status_${hash}.json"
19
+ if [ -f "$f" ]; then
20
+ pending=$(jq -r '.pending // 0' "$f" 2>/dev/null)
21
+ active=$(jq -r '.active // 0' "$f" 2>/dev/null)
22
+ replies=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
23
+ loop=$(jq -r '.loop_active // false' "$f" 2>/dev/null)
24
+ fi
25
+ else
26
+ # Fallback: aggregate all agent caches
27
+ for f in "$tmpdir"/askmesh_status_*.json; do
28
+ [ -f "$f" ] || continue
29
+ p=$(jq -r '.pending // 0' "$f" 2>/dev/null)
30
+ a=$(jq -r '.active // 0' "$f" 2>/dev/null)
31
+ r=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
32
+ l=$(jq -r '.loop_active // false' "$f" 2>/dev/null)
33
+ pending=$((pending + p))
34
+ active=$((active + a))
35
+ replies=$((replies + r))
36
+ [ "$l" = "true" ] && loop="true"
37
+ done
38
+ fi
23
39
 
24
40
  parts=()
25
41
 
42
+ if [ "$loop" = "true" ]; then
43
+ parts+=("\033[35mloop\033[0m")
44
+ fi
26
45
  if [ "$pending" -gt 0 ] 2>/dev/null; then
27
46
  parts+=("\033[33m${pending} pending\033[0m")
28
47
  fi