@askmesh/mcp 0.10.5 → 0.11.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.
@@ -0,0 +1,10 @@
1
+ export interface IgnoreMatcher {
2
+ patterns: string[];
3
+ shouldIgnore: (filePath: string) => boolean;
4
+ describe: () => string;
5
+ }
6
+ /**
7
+ * Load .askmeshignore from the current working directory and merge with defaults.
8
+ * Returns a matcher with shouldIgnore() and a describe() helper for prompts.
9
+ */
10
+ export declare function loadIgnore(cwd?: string): IgnoreMatcher;
@@ -0,0 +1,97 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ // Default exclusions — applied even when no .askmeshignore file exists.
4
+ // These are patterns that almost no project should ever share via the mesh.
5
+ const DEFAULT_PATTERNS = [
6
+ // Environment files
7
+ '.env',
8
+ '.env.*',
9
+ '*.env',
10
+ // Secrets folders
11
+ 'secrets/',
12
+ 'secret/',
13
+ 'private/',
14
+ '.secrets/',
15
+ // Keys and certificates
16
+ '*.key',
17
+ '*.pem',
18
+ '*.p12',
19
+ '*.pfx',
20
+ '*.crt',
21
+ '*.csr',
22
+ '*.der',
23
+ 'id_rsa',
24
+ 'id_rsa.*',
25
+ 'id_dsa',
26
+ 'id_ecdsa',
27
+ 'id_ed25519',
28
+ // Credentials
29
+ 'credentials',
30
+ 'credentials.*',
31
+ '.npmrc',
32
+ '.pypirc',
33
+ '.netrc',
34
+ '.aws/',
35
+ '.ssh/',
36
+ '.gnupg/',
37
+ // Cloud
38
+ 'service-account.json',
39
+ 'service-account-*.json',
40
+ 'gcp-key.json',
41
+ 'firebase-adminsdk-*.json',
42
+ // Common build/dep dirs
43
+ 'node_modules/',
44
+ '.git/',
45
+ ];
46
+ /**
47
+ * Load .askmeshignore from the current working directory and merge with defaults.
48
+ * Returns a matcher with shouldIgnore() and a describe() helper for prompts.
49
+ */
50
+ export function loadIgnore(cwd = process.cwd()) {
51
+ const patterns = [...DEFAULT_PATTERNS];
52
+ const file = join(cwd, '.askmeshignore');
53
+ if (existsSync(file)) {
54
+ try {
55
+ const content = readFileSync(file, 'utf-8');
56
+ for (const rawLine of content.split('\n')) {
57
+ const line = rawLine.trim();
58
+ if (!line || line.startsWith('#'))
59
+ continue;
60
+ patterns.push(line);
61
+ }
62
+ }
63
+ catch { }
64
+ }
65
+ const compiled = patterns.map(compile);
66
+ const shouldIgnore = (filePath) => {
67
+ const name = basename(filePath);
68
+ return compiled.some((re) => re.test(filePath) || re.test(name));
69
+ };
70
+ const describe = () => {
71
+ return patterns.join(', ');
72
+ };
73
+ return { patterns, shouldIgnore, describe };
74
+ }
75
+ /**
76
+ * Convert a glob-like pattern to a RegExp.
77
+ * Supports: *, **, trailing / (folder), leading * (suffix match)
78
+ */
79
+ function compile(pattern) {
80
+ let p = pattern.trim();
81
+ // Folder pattern (trailing /)
82
+ const isFolder = p.endsWith('/');
83
+ if (isFolder)
84
+ p = p.slice(0, -1);
85
+ // Escape regex special chars except * and ?
86
+ let re = p.replace(/[.+^${}()|[\]\\]/g, '\\$&');
87
+ // ** matches anything (including /)
88
+ re = re.replace(/\*\*/g, '§§');
89
+ // * matches anything except /
90
+ re = re.replace(/\*/g, '[^/]*');
91
+ re = re.replace(/§§/g, '.*');
92
+ re = re.replace(/\?/g, '.');
93
+ if (isFolder) {
94
+ return new RegExp(`(^|/)${re}(/|$)`);
95
+ }
96
+ return new RegExp(`(^|/)${re}$`);
97
+ }
@@ -6,6 +6,12 @@ export declare class AutoResponder {
6
6
  private mcpServer;
7
7
  constructor(client: AskMeshClient);
8
8
  setServer(server: Server): void;
9
+ /**
10
+ * Send a reply through the AskMesh client with a pre-send redaction scan.
11
+ * If secrets are detected, the reply is blocked and a desktop notification +
12
+ * status line update inform the owner. Returns true if the reply was sent.
13
+ */
14
+ private safeReply;
9
15
  handleRequest(request: IncomingRequest): Promise<void>;
10
16
  private callAnthropicAPI;
11
17
  }
@@ -1,12 +1,24 @@
1
1
  import { readLocalContext } from './context_reader.js';
2
2
  import { sendDesktopNotification } from './notifier.js';
3
+ import { scanForSecrets, formatBlockedMessage } from './redaction.js';
3
4
  const SYSTEM_PROMPT = `You are an AI coding agent responding on behalf of a developer through AskMesh.
4
5
  You have access to the developer's project context below.
5
6
  Use this context to answer accurately and concisely.
6
7
  Reference specific files, conventions, or decisions when relevant.
7
8
  If the context doesn't contain enough info, say so honestly.
8
9
  Answer in the same language as the question.
9
- Keep responses under 500 words unless more detail is needed.`;
10
+ Keep responses under 500 words unless more detail is needed.
11
+
12
+ SECURITY RULES:
13
+ - READ-ONLY: Never suggest or execute destructive operations (DELETE, DROP, rm, reset --hard, etc.)
14
+ - NO SECRETS: Never include passwords, API keys, tokens, or credentials in your responses
15
+ - NO SENSITIVE DATA: Never expose personal data, database connection strings, or internal URLs
16
+ - HONOR .askmeshignore: Never read or share files listed by the project's privacy policy
17
+ - If asked for sensitive information, respond: "I can't share this information via AskMesh for security reasons."
18
+ - You may share: code patterns, architecture decisions, file structures, conventions, public configs
19
+
20
+ A pre-send filter will block your response if it contains detectable secrets (API keys, JWTs, private keys, DB URLs, etc.).
21
+ Avoid quoting raw config values — describe them instead.`;
10
22
  export class AutoResponder {
11
23
  client;
12
24
  mcpServer = null;
@@ -16,6 +28,37 @@ export class AutoResponder {
16
28
  setServer(server) {
17
29
  this.mcpServer = server;
18
30
  }
31
+ /**
32
+ * Send a reply through the AskMesh client with a pre-send redaction scan.
33
+ * If secrets are detected, the reply is blocked and a desktop notification +
34
+ * status line update inform the owner. Returns true if the reply was sent.
35
+ */
36
+ async safeReply(requestId, answer, source) {
37
+ const scan = scanForSecrets(answer);
38
+ if (!scan.safe) {
39
+ const samples = scan.hits.map((h) => `${h.pattern} (${h.sample})`).join(', ');
40
+ console.error(`[AskMesh] 🚫 BLOCKED reply #${requestId} (${source}) — secrets detected: ${samples}`);
41
+ // Notify the owner via desktop and via the MCP logging channel
42
+ sendDesktopNotification('AskMesh — réponse bloquée', `Secrets détectés: ${scan.hits.map((h) => h.pattern).join(', ')}`);
43
+ if (this.mcpServer) {
44
+ try {
45
+ await this.mcpServer.sendLoggingMessage({
46
+ level: 'warning',
47
+ data: formatBlockedMessage(scan),
48
+ });
49
+ }
50
+ catch { }
51
+ }
52
+ // Revert thread status to pending so the human can take over
53
+ try {
54
+ await this.client.updateThreadStatus(requestId, 'pending');
55
+ }
56
+ catch { }
57
+ return false;
58
+ }
59
+ await this.client.replyToThread(requestId, answer);
60
+ return true;
61
+ }
19
62
  async handleRequest(request) {
20
63
  console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
21
64
  // Desktop notification
@@ -60,8 +103,9 @@ export class AutoResponder {
60
103
  }, {}));
61
104
  const answer = result?.content?.text || result?.content?.[0]?.text;
62
105
  if (answer) {
63
- await this.client.replyToThread(request.id, answer);
64
- console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
106
+ const sent = await this.safeReply(request.id, answer, 'Claude Code');
107
+ if (sent)
108
+ console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
65
109
  return;
66
110
  }
67
111
  }
@@ -76,8 +120,9 @@ export class AutoResponder {
76
120
  const context = readLocalContext();
77
121
  const answer = await this.callAnthropicAPI(apiKey, request, context);
78
122
  if (answer) {
79
- await this.client.replyToThread(request.id, answer);
80
- console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
123
+ const sent = await this.safeReply(request.id, answer, 'Anthropic API');
124
+ if (sent)
125
+ console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
81
126
  return;
82
127
  }
83
128
  }
@@ -1,18 +1,22 @@
1
1
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { loadIgnore } from './askmeshignore.js';
4
5
  export function readLocalContext() {
5
6
  const parts = [];
7
+ const ignore = loadIgnore();
6
8
  // 1. CLAUDE.md from current working directory
7
9
  const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
8
- if (existsSync(claudeMdPath)) {
10
+ if (existsSync(claudeMdPath) && !ignore.shouldIgnore('CLAUDE.md')) {
9
11
  parts.push('=== CLAUDE.md ===');
10
12
  parts.push(readSafe(claudeMdPath));
11
13
  }
12
14
  // 2. Global Claude Code memories
13
15
  const globalMemDir = join(homedir(), '.claude', 'memory');
14
16
  if (existsSync(globalMemDir)) {
15
- const files = safeReadDir(globalMemDir).filter((f) => f.endsWith('.md'));
17
+ const files = safeReadDir(globalMemDir)
18
+ .filter((f) => f.endsWith('.md'))
19
+ .filter((f) => !ignore.shouldIgnore(f));
16
20
  if (files.length > 0) {
17
21
  parts.push('=== Global memories ===');
18
22
  for (const file of files.slice(0, 10)) {
@@ -32,7 +36,9 @@ export function readLocalContext() {
32
36
  if (dir.includes(cwdKey) || cwdKey.includes(dir)) {
33
37
  const memDir = join(projectsDir, dir, 'memory');
34
38
  if (existsSync(memDir)) {
35
- const files = safeReadDir(memDir).filter((f) => f.endsWith('.md'));
39
+ const files = safeReadDir(memDir)
40
+ .filter((f) => f.endsWith('.md'))
41
+ .filter((f) => !ignore.shouldIgnore(f));
36
42
  if (files.length > 0) {
37
43
  parts.push('=== Project memories ===');
38
44
  for (const file of files.slice(0, 10)) {
@@ -45,6 +51,9 @@ export function readLocalContext() {
45
51
  }
46
52
  }
47
53
  }
54
+ // 4. Append the active ignore policy so the agent knows what NOT to read
55
+ parts.push('=== Privacy policy (.askmeshignore) ===');
56
+ parts.push(`Never read or share contents of files matching these patterns: ${ignore.describe()}`);
48
57
  return parts.join('\n\n') || 'No local context available.';
49
58
  }
50
59
  function readSafe(path) {
@@ -0,0 +1,16 @@
1
+ export interface RedactionResult {
2
+ safe: boolean;
3
+ hits: Array<{
4
+ pattern: string;
5
+ sample: string;
6
+ }>;
7
+ }
8
+ /**
9
+ * Scan a reply for known secret patterns. Returns safe=false if any are found.
10
+ * The hits include the pattern name and a truncated sample for logging/audit.
11
+ */
12
+ export declare function scanForSecrets(text: string): RedactionResult;
13
+ /**
14
+ * Format a user-facing block message when redaction triggers.
15
+ */
16
+ export declare function formatBlockedMessage(result: RedactionResult): string;
@@ -0,0 +1,101 @@
1
+ // Pre-send redaction — scans outgoing replies for common secret patterns
2
+ // and blocks them before they're sent through the mesh.
3
+ //
4
+ // This is a defense-in-depth layer. The agent is also instructed not to share
5
+ // secrets, but we don't trust the LLM to never slip up.
6
+ const PATTERNS = [
7
+ {
8
+ name: 'AWS Access Key',
9
+ detect: (t) => match(t, /\bAKIA[0-9A-Z]{16}\b/g),
10
+ },
11
+ {
12
+ name: 'AWS Secret Key (assignment)',
13
+ detect: (t) => match(t, /aws_secret_access_key\s*[=:]\s*['"][^'"]+['"]/gi),
14
+ },
15
+ {
16
+ name: 'OpenAI API Key',
17
+ detect: (t) => match(t, /\bsk-(?:proj-)?[A-Za-z0-9_\-]{32,}\b/g),
18
+ },
19
+ {
20
+ name: 'Anthropic API Key',
21
+ detect: (t) => match(t, /\bsk-ant-[A-Za-z0-9_\-]{20,}\b/g),
22
+ },
23
+ {
24
+ name: 'GitHub Token',
25
+ detect: (t) => match(t, /\bgh[poursa]_[A-Za-z0-9_]{36,}\b/g),
26
+ },
27
+ {
28
+ name: 'Slack Token',
29
+ detect: (t) => match(t, /\bxox[bpoars]-[A-Za-z0-9-]{10,}\b/g),
30
+ },
31
+ {
32
+ name: 'Stripe Key',
33
+ detect: (t) => match(t, /\b(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}\b/g),
34
+ },
35
+ {
36
+ name: 'JWT',
37
+ detect: (t) => match(t, /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g),
38
+ },
39
+ {
40
+ name: 'Private Key (PEM)',
41
+ detect: (t) => match(t, /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g),
42
+ },
43
+ {
44
+ name: 'Database URL',
45
+ detect: (t) => match(t, /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s'"]*:[^\s'"@]+@[^\s'"]+/gi),
46
+ },
47
+ {
48
+ name: 'Generic API Key Assignment',
49
+ detect: (t) => match(t, /\b(?:api[_-]?key|apikey|secret(?:[_-]?key)?|access[_-]?token|auth[_-]?token|password)\s*[:=]\s*['"][A-Za-z0-9_\-+/=]{16,}['"]/gi),
50
+ },
51
+ {
52
+ name: 'Google API Key',
53
+ detect: (t) => match(t, /\bAIza[0-9A-Za-z\-_]{35}\b/g),
54
+ },
55
+ {
56
+ name: 'Square Token',
57
+ detect: (t) => match(t, /\bsq0(?:atp|csp|idp)-[A-Za-z0-9_-]{22,}\b/g),
58
+ },
59
+ {
60
+ name: 'Twilio API Key',
61
+ detect: (t) => match(t, /\bSK[a-f0-9]{32}\b/g),
62
+ },
63
+ {
64
+ name: 'SendGrid API Key',
65
+ detect: (t) => match(t, /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g),
66
+ },
67
+ {
68
+ name: 'Mailgun API Key',
69
+ detect: (t) => match(t, /\bkey-[a-f0-9]{32}\b/g),
70
+ },
71
+ ];
72
+ function match(text, re) {
73
+ const found = text.match(re);
74
+ return found && found.length > 0 ? found : null;
75
+ }
76
+ /**
77
+ * Scan a reply for known secret patterns. Returns safe=false if any are found.
78
+ * The hits include the pattern name and a truncated sample for logging/audit.
79
+ */
80
+ export function scanForSecrets(text) {
81
+ const hits = [];
82
+ for (const p of PATTERNS) {
83
+ const found = p.detect(text);
84
+ if (found) {
85
+ for (const f of found) {
86
+ hits.push({
87
+ pattern: p.name,
88
+ sample: f.length > 24 ? `${f.slice(0, 8)}…${f.slice(-4)}` : f,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ return { safe: hits.length === 0, hits };
94
+ }
95
+ /**
96
+ * Format a user-facing block message when redaction triggers.
97
+ */
98
+ export function formatBlockedMessage(result) {
99
+ const list = result.hits.map((h) => ` - ${h.pattern}: ${h.sample}`).join('\n');
100
+ return `🚫 Réponse bloquée par le filtre AskMesh — secrets détectés :\n${list}\n\nReformule ta réponse sans les valeurs sensibles.`;
101
+ }
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.5',
14
+ version: '0.11.0',
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
+ }
@@ -35,8 +35,10 @@ Actions disponibles :
35
35
  - "broadcast" : envoyer un message à toute ton équipe (notification Telegram/Slack, sans créer de thread)
36
36
  - "context" : partager ton contexte projet avec ton équipe
37
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
+ - "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'),
40
42
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
41
43
  question: z.string().optional().describe('Question à poser (pour ask)'),
42
44
  requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
@@ -233,8 +235,19 @@ Actions disponibles :
233
235
  case 'update': {
234
236
  return await checkUpdateAndSetup();
235
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
+ }
236
249
  default:
237
- return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup, update');
250
+ return text('Action inconnue.');
238
251
  }
239
252
  });
240
253
  }
@@ -264,7 +277,7 @@ Si l'utilisateur n'a pas encore de compte, dirige-le vers https://askmesh.dev po
264
277
  return text('Action inconnue.');
265
278
  });
266
279
  }
267
- const CURRENT_VERSION = '0.10.5';
280
+ const CURRENT_VERSION = '0.11.0';
268
281
  async function checkUpdateAndSetup() {
269
282
  const lines = [];
270
283
  lines.push(`Version actuelle : ${CURRENT_VERSION}`);
@@ -319,6 +332,8 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
319
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.`,
320
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".`,
321
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".`,
322
337
  };
323
338
  // Create commands directory
324
339
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.10.5",
3
+ "version": "0.11.0",
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
@@ -11,6 +11,7 @@ hash="$1"
11
11
  pending=0
12
12
  active=0
13
13
  replies=0
14
+ loop="false"
14
15
 
15
16
  if [ -n "$hash" ]; then
16
17
  # Read specific agent cache
@@ -19,6 +20,7 @@ if [ -n "$hash" ]; then
19
20
  pending=$(jq -r '.pending // 0' "$f" 2>/dev/null)
20
21
  active=$(jq -r '.active // 0' "$f" 2>/dev/null)
21
22
  replies=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
23
+ loop=$(jq -r '.loop_active // false' "$f" 2>/dev/null)
22
24
  fi
23
25
  else
24
26
  # Fallback: aggregate all agent caches
@@ -27,14 +29,19 @@ else
27
29
  p=$(jq -r '.pending // 0' "$f" 2>/dev/null)
28
30
  a=$(jq -r '.active // 0' "$f" 2>/dev/null)
29
31
  r=$(jq -r '.unread_replies // 0' "$f" 2>/dev/null)
32
+ l=$(jq -r '.loop_active // false' "$f" 2>/dev/null)
30
33
  pending=$((pending + p))
31
34
  active=$((active + a))
32
35
  replies=$((replies + r))
36
+ [ "$l" = "true" ] && loop="true"
33
37
  done
34
38
  fi
35
39
 
36
40
  parts=()
37
41
 
42
+ if [ "$loop" = "true" ]; then
43
+ parts+=("\033[35mloop\033[0m")
44
+ fi
38
45
  if [ "$pending" -gt 0 ] 2>/dev/null; then
39
46
  parts+=("\033[33m${pending} pending\033[0m")
40
47
  fi