@askmesh/mcp 0.9.0 → 0.10.1

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/README.md CHANGED
@@ -25,22 +25,21 @@ npx -y @askmesh/mcp
25
25
  }
26
26
  ```
27
27
 
28
- **Step 2** — Add your token in `.env` (never committed):
28
+ **Step 2** — Restart Claude Code, then type: `/ask-setup`
29
29
 
30
- ```
31
- ASKMESH_TOKEN=your_token_here
32
- ASKMESH_URL=https://api.askmesh.dev
33
- ```
34
-
35
- **Step 3** — Restart Claude Code, then type: `setup askmesh`
30
+ That's it. The setup wizard will:
31
+ - Ask for your token (from [askmesh.dev](https://askmesh.dev) → Settings)
32
+ - Save it in `.env` (auto-added to `.gitignore`)
33
+ - Install slash commands (`/ask-inbox`, `/ask-broadcast`, etc.)
34
+ - Configure the status line
36
35
 
37
- This installs slash commands (`/inbox`, `/broadcast`, etc.) and configures the status line.
36
+ No env vars needed in `.mcp.json` everything lives in your `.env`.
38
37
 
39
38
  ## Get your token
40
39
 
41
40
  1. Sign up at [askmesh.dev](https://askmesh.dev)
42
41
  2. Go to **Settings** → create an agent → copy the API token
43
- 3. Paste it in your `.env`
42
+ 3. Run `/ask-setup` and paste it when prompted
44
43
 
45
44
  ## Usage
46
45
 
@@ -76,16 +75,17 @@ One single tool `askmesh` — Claude understands natural language:
76
75
 
77
76
  ### Slash commands (skills)
78
77
 
79
- Add these as `.claude/commands/*.md` in your project for quick access:
78
+ Installed automatically by `/ask-setup`:
80
79
 
81
80
  | Command | Description |
82
81
  |---|---|
83
- | `/inbox` | Check pending + sent messages |
84
- | `/broadcast <msg>` | Broadcast to your team |
85
- | `/reply <id> <msg>` | Reply to a thread |
86
- | `/board` | View team kanban board |
87
- | `/threads` | List active conversations |
88
- | `/mesh-status` | See who's online |
82
+ | `/ask-inbox` | Check pending + sent messages |
83
+ | `/ask-broadcast <msg>` | Broadcast to your team |
84
+ | `/ask-reply <id> <msg>` | Reply to a thread |
85
+ | `/ask-board` | View team kanban board |
86
+ | `/ask-threads` | List active conversations |
87
+ | `/ask-status` | See who's online |
88
+ | `/ask-setup` | Install or update AskMesh config |
89
89
 
90
90
  ## Status line
91
91
 
@@ -101,18 +101,7 @@ mesh 3↓ 1~ 2>
101
101
 
102
102
  ### Setup
103
103
 
104
- Add to your Claude Code settings (`~/.claude/settings.json`):
105
-
106
- ```json
107
- {
108
- "statusLine": {
109
- "type": "command",
110
- "command": "/path/to/node_modules/@askmesh/mcp/statusline.sh"
111
- }
112
- }
113
- ```
114
-
115
- The MCP server updates a local cache file in real-time via SSE events. The status line script reads it — no polling, no API calls.
104
+ Configured automatically by `/ask-setup`. The MCP server updates a local cache file in real-time via SSE events. The status line script reads it — no polling, no API calls.
116
105
 
117
106
  ## Auto-responder
118
107
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import 'dotenv/config';
package/dist/index.js CHANGED
@@ -1,70 +1,73 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
5
  import { AskMeshClient } from './client/askmesh_client.js';
5
6
  import { SseListener } from './sse/sse_listener.js';
6
7
  import { AutoResponder } from './agent/auto_responder.js';
7
- import { registerAskMesh } from './tools/askmesh.js';
8
+ import { registerAskMesh, registerSetupOnly } from './tools/askmesh.js';
8
9
  import * as statusCache from './statusline/cache.js';
9
10
  const TOKEN = process.env.ASKMESH_TOKEN;
10
11
  const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
11
- if (!TOKEN) {
12
- console.error('[AskMesh] ASKMESH_TOKEN is required');
13
- process.exit(1);
14
- }
15
- const client = new AskMeshClient(URL, TOKEN);
16
- const autoResponder = new AutoResponder(client);
17
12
  const server = new McpServer({
18
13
  name: 'askmesh',
19
- version: '0.9.0',
14
+ version: '0.10.1',
20
15
  });
21
- // Single unified tool
22
- registerAskMesh(server, client);
23
- // Give the auto-responder access to the underlying MCP server for sampling
24
- autoResponder.setServer(server.server);
25
- // Start SSE listener — auto-respond to incoming questions + receive answers
26
- const sse = new SseListener();
27
- sse.start(URL, TOKEN, async (request) => {
28
- statusCache.onRequestIncoming();
29
- await autoResponder.handleRequest(request);
30
- }, (answer) => {
31
- console.error(`[AskMesh] Answer received for request #${answer.id}: "${answer.answer.slice(0, 100)}${answer.answer.length > 100 ? '...' : ''}"`);
32
- }, (_reply) => {
33
- statusCache.onReplyAdded();
34
- }, (_closed) => {
35
- statusCache.onThreadClosed();
36
- });
37
- // Polling fallback — fetch pending requests periodically
38
- // Useful for server agents that may miss SSE events
39
- const POLL_INTERVAL = Number(process.env.ASKMESH_POLL_INTERVAL || 0); // seconds, 0 = disabled
40
- if (POLL_INTERVAL > 0) {
41
- console.error(`[AskMesh] Polling enabled every ${POLL_INTERVAL}s`);
42
- setInterval(async () => {
43
- try {
44
- const { requests } = await client.getPendingRequests();
45
- for (const req of requests) {
46
- console.error(`[AskMesh] Polled pending #${req.id}: "${req.question}"`);
47
- await autoResponder.handleRequest({
48
- id: req.id,
49
- fromAgentId: req.fromAgentId,
50
- fromUsername: `agent#${req.fromAgentId}`,
51
- question: req.question,
52
- context: req.context,
53
- });
16
+ if (!TOKEN) {
17
+ // No token — start in setup-only mode
18
+ console.error('[AskMesh] No ASKMESH_TOKEN found. Starting in setup mode.');
19
+ registerSetupOnly(server);
20
+ }
21
+ else {
22
+ // Full mode
23
+ statusCache.init(TOKEN);
24
+ const client = new AskMeshClient(URL, TOKEN);
25
+ const autoResponder = new AutoResponder(client);
26
+ registerAskMesh(server, client);
27
+ autoResponder.setServer(server.server);
28
+ // Start SSE listener
29
+ const sse = new SseListener();
30
+ sse.start(URL, TOKEN, async (request) => {
31
+ statusCache.onRequestIncoming();
32
+ await autoResponder.handleRequest(request);
33
+ }, (answer) => {
34
+ console.error(`[AskMesh] Answer received for request #${answer.id}: "${answer.answer.slice(0, 100)}${answer.answer.length > 100 ? '...' : ''}"`);
35
+ }, (_reply) => {
36
+ statusCache.onReplyAdded();
37
+ }, (_closed) => {
38
+ statusCache.onThreadClosed();
39
+ });
40
+ // Polling fallback
41
+ const POLL_INTERVAL = Number(process.env.ASKMESH_POLL_INTERVAL || 0);
42
+ if (POLL_INTERVAL > 0) {
43
+ console.error(`[AskMesh] Polling enabled every ${POLL_INTERVAL}s`);
44
+ setInterval(async () => {
45
+ try {
46
+ const { requests } = await client.getPendingRequests();
47
+ for (const req of requests) {
48
+ console.error(`[AskMesh] Polled pending #${req.id}: "${req.question}"`);
49
+ await autoResponder.handleRequest({
50
+ id: req.id,
51
+ fromAgentId: req.fromAgentId,
52
+ fromUsername: `agent#${req.fromAgentId}`,
53
+ question: req.question,
54
+ context: req.context,
55
+ });
56
+ }
54
57
  }
55
- }
56
- catch { }
57
- }, POLL_INTERVAL * 1000);
58
+ catch { }
59
+ }, POLL_INTERVAL * 1000);
60
+ }
61
+ // Cleanup on exit
62
+ process.on('SIGINT', () => {
63
+ sse.stop();
64
+ process.exit(0);
65
+ });
66
+ process.on('SIGTERM', () => {
67
+ sse.stop();
68
+ process.exit(0);
69
+ });
58
70
  }
59
- // Cleanup on exit
60
- process.on('SIGINT', () => {
61
- sse.stop();
62
- process.exit(0);
63
- });
64
- process.on('SIGTERM', () => {
65
- sse.stop();
66
- process.exit(0);
67
- });
68
71
  // Start MCP server via stdio
69
72
  const transport = new StdioServerTransport();
70
73
  await server.connect(transport);
@@ -4,6 +4,9 @@ interface StatusCache {
4
4
  unread_replies: number;
5
5
  last_update: string;
6
6
  }
7
+ /** Call once at startup with the agent token to isolate the cache file per agent */
8
+ export declare function init(token: string): void;
9
+ export declare function getCacheFile(): string;
7
10
  export declare function load(): StatusCache;
8
11
  export declare function onRequestIncoming(): void;
9
12
  export declare function onReplyAdded(): void;
@@ -1,8 +1,14 @@
1
1
  import { writeFileSync, readFileSync } from 'fs';
2
+ import { createHash } from 'crypto';
2
3
  import { tmpdir } from 'os';
3
4
  import { join } from 'path';
4
- const CACHE_FILE = join(tmpdir(), 'askmesh_status.json');
5
+ let CACHE_FILE = join(tmpdir(), 'askmesh_status.json');
5
6
  let cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
7
+ /** Call once at startup with the agent token to isolate the cache file per agent */
8
+ export function init(token) {
9
+ const hash = createHash('md5').update(token).digest('hex').slice(0, 8);
10
+ CACHE_FILE = join(tmpdir(), `askmesh_status_${hash}.json`);
11
+ }
6
12
  function flush() {
7
13
  cache.last_update = new Date().toISOString();
8
14
  try {
@@ -10,6 +16,9 @@ function flush() {
10
16
  }
11
17
  catch { }
12
18
  }
19
+ export function getCacheFile() {
20
+ return CACHE_FILE;
21
+ }
13
22
  export function load() {
14
23
  try {
15
24
  const raw = readFileSync(CACHE_FILE, 'utf-8');
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { AskMeshClient } from '../client/askmesh_client.js';
3
3
  export declare function registerAskMesh(server: McpServer, client: AskMeshClient): void;
4
+ export declare function registerSetupOnly(server: McpServer): void;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { mkdirSync, writeFileSync, readFileSync, appendFileSync, existsSync } from 'fs';
2
+ import { mkdirSync, writeFileSync, readFileSync, appendFileSync, existsSync, unlinkSync, copyFileSync, chmodSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
+ import { homedir } from 'os';
4
5
  import { fileURLToPath } from 'url';
5
6
  import * as statusCache from '../statusline/cache.js';
6
7
  export function registerAskMesh(server, client) {
@@ -40,7 +41,8 @@ Actions disponibles :
40
41
  message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/reply/context)'),
41
42
  parentThreadId: z.number().optional().describe('ID du thread parent (pour ask, lie les threads entre eux)'),
42
43
  teamId: z.number().optional().describe('ID de la team (pour board)'),
43
- }, async ({ action, username, question, requestId, message, parentThreadId, teamId }) => {
44
+ token: z.string().optional().describe('API token AskMesh (pour setup depuis askmesh.dev > Settings)'),
45
+ }, async ({ action, username, question, requestId, message, parentThreadId, teamId, token }) => {
44
46
  switch (action) {
45
47
  case 'ask': {
46
48
  if (!username || !question) {
@@ -208,56 +210,109 @@ Actions disponibles :
208
210
  return text(`Contexte mis à jour (${message.length} caractères).`);
209
211
  }
210
212
  case 'setup': {
211
- return setupSkillsAndStatusLine();
213
+ return setupSkillsAndStatusLine(token);
212
214
  }
213
215
  default:
214
216
  return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup');
215
217
  }
216
218
  });
217
219
  }
220
+ export function registerSetupOnly(server) {
221
+ server.tool('askmesh', `AskMesh — première configuration requise.
222
+ Aucun token trouvé. Utilise cet outil pour configurer AskMesh dans ce projet.
223
+
224
+ Actions disponibles :
225
+ - "setup" : configurer le projet (crée le .env, installe les slash commands, configure la status line)
226
+
227
+ Pour commencer :
228
+ 1. Inscris-toi sur https://askmesh.dev
229
+ 2. Copie ton API token depuis Settings
230
+ 3. Lance l'action "setup" avec ton token pour tout installer`, {
231
+ action: z.enum(['setup']).describe('Action à effectuer'),
232
+ token: z.string().optional().describe('Ton API token AskMesh (depuis askmesh.dev > Settings)'),
233
+ }, async ({ action, token }) => {
234
+ if (action === 'setup') {
235
+ return setupSkillsAndStatusLine(token);
236
+ }
237
+ return text('Action inconnue.');
238
+ });
239
+ }
218
240
  function text(t) {
219
241
  return { content: [{ type: 'text', text: t }] };
220
242
  }
221
- function setupSkillsAndStatusLine() {
243
+ function setupSkillsAndStatusLine(token) {
222
244
  const cwd = process.cwd();
223
245
  const commandsDir = join(cwd, '.claude', 'commands');
224
246
  const installed = [];
225
247
  const skipped = [];
226
248
  // Skills definitions
227
249
  const skills = {
228
- 'inbox.md': `Utilise l'outil MCP askmesh avec l'action "pending" pour vérifier les messages reçus en attente, puis avec l'action "inbox" pour voir les réponses à mes messages envoyés.\n\nAffiche un résumé clair :\n1. D'abord les messages reçus à traiter (pending)\n2. Ensuite les réponses reçues (inbox)\n\nSi tout est vide, dis simplement "Aucun message en attente."`,
229
- 'broadcast.md': `Utilise l'outil MCP askmesh avec l'action "broadcast" pour envoyer un message à toute l'équipe.\n\nLe message à diffuser : $ARGUMENTS\n\nSi aucun message n'est fourni, demande à l'utilisateur quel message il souhaite broadcaster.`,
230
- 'reply.md': `Utilise l'outil MCP askmesh pour répondre à un thread.\n\nArguments attendus : $ARGUMENTS (format: <requestId> <message>)\n\nExemple : /reply 42 Voici ma réponse détaillée...\n\nSi le requestId n'est pas fourni, utilise d'abord l'action "pending" pour lister les threads en attente et demande à l'utilisateur lequel traiter.\n\nUtilise l'action "reply" avec le requestId et le message.`,
231
- 'board.md': `Utilise l'outil MCP askmesh avec l'action "board" pour afficher le tableau kanban de l'équipe.\n\nSi un teamId est fourni dans les arguments ($ARGUMENTS), utilise-le directement.\nSinon, utilise d'abord l'action "list" pour identifier les teams disponibles, puis affiche le board de la première team.\n\nPrésente le résultat de manière claire avec les colonnes : Pending, In Progress, Active, Done.`,
232
- '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.`,
233
- 'mesh-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.`,
250
+ 'ask-inbox.md': `Utilise l'outil MCP askmesh avec l'action "pending" pour vérifier les messages reçus en attente, puis avec l'action "inbox" pour voir les réponses à mes messages envoyés.\n\nAffiche un résumé clair :\n1. D'abord les messages reçus à traiter (pending)\n2. Ensuite les réponses reçues (inbox)\n\nSi tout est vide, dis simplement "Aucun message en attente."`,
251
+ 'ask-broadcast.md': `Utilise l'outil MCP askmesh avec l'action "broadcast" pour envoyer un message à toute l'équipe.\n\nLe message à diffuser : $ARGUMENTS\n\nSi aucun message n'est fourni, demande à l'utilisateur quel message il souhaite broadcaster.`,
252
+ 'ask-reply.md': `Utilise l'outil MCP askmesh pour répondre à un thread.\n\nArguments attendus : $ARGUMENTS (format: <requestId> <message>)\n\nExemple : /ask-reply 42 Voici ma réponse détaillée...\n\nSi le requestId n'est pas fourni, utilise d'abord l'action "pending" pour lister les threads en attente et demande à l'utilisateur lequel traiter.\n\nUtilise l'action "reply" avec le requestId et le message.`,
253
+ 'ask-board.md': `Utilise l'outil MCP askmesh avec l'action "board" pour afficher le tableau kanban de l'équipe.\n\nSi un teamId est fourni dans les arguments ($ARGUMENTS), utilise-le directement.\nSinon, utilise d'abord l'action "list" pour identifier les teams disponibles, puis affiche le board de la première team.\n\nPrésente le résultat de manière claire avec les colonnes : Pending, In Progress, Active, Done.`,
254
+ '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.`,
255
+ '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.`,
256
+ '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".`,
234
257
  };
235
258
  // Create commands directory
236
259
  try {
237
260
  mkdirSync(commandsDir, { recursive: true });
238
261
  }
239
262
  catch { }
240
- // Write skills
263
+ // Write skills (always overwrite to keep them up to date)
241
264
  for (const [filename, content] of Object.entries(skills)) {
242
265
  const filepath = join(commandsDir, filename);
243
- if (existsSync(filepath)) {
266
+ const existed = existsSync(filepath);
267
+ writeFileSync(filepath, content);
268
+ if (existed) {
244
269
  skipped.push(filename);
245
270
  }
246
271
  else {
247
- writeFileSync(filepath, content);
248
272
  installed.push(filename);
249
273
  }
250
274
  }
251
- // Check .env for token
275
+ // Remove old skill names (migration from pre-0.10)
276
+ const oldSkills = ['inbox.md', 'broadcast.md', 'reply.md', 'board.md', 'threads.md', 'mesh-status.md'];
277
+ const removed = [];
278
+ for (const old of oldSkills) {
279
+ const oldPath = join(commandsDir, old);
280
+ if (existsSync(oldPath)) {
281
+ try {
282
+ unlinkSync(oldPath);
283
+ removed.push(old);
284
+ }
285
+ catch { }
286
+ }
287
+ }
288
+ // Handle .env token
252
289
  const envPath = join(cwd, '.env');
253
290
  let envStatus = '';
254
- if (existsSync(envPath)) {
291
+ if (token) {
292
+ if (existsSync(envPath)) {
293
+ const content = readFileSync(envPath, 'utf-8');
294
+ if (content.includes('ASKMESH_TOKEN')) {
295
+ const updated = content.replace(/ASKMESH_TOKEN=.*/, `ASKMESH_TOKEN=${token}`);
296
+ writeFileSync(envPath, updated);
297
+ envStatus = '.env : ASKMESH_TOKEN mis à jour';
298
+ }
299
+ else {
300
+ appendFileSync(envPath, `\nASKMESH_TOKEN=${token}\nASKMESH_URL=https://api.askmesh.dev\n`);
301
+ envStatus = '.env : ASKMESH_TOKEN ajouté';
302
+ }
303
+ }
304
+ else {
305
+ writeFileSync(envPath, `ASKMESH_TOKEN=${token}\nASKMESH_URL=https://api.askmesh.dev\n`);
306
+ envStatus = '.env créé avec le token';
307
+ }
308
+ }
309
+ else if (existsSync(envPath)) {
255
310
  const envContent = readFileSync(envPath, 'utf-8');
256
- if (envContent.includes('ASKMESH_TOKEN')) {
257
- envStatus = '.env : ASKMESH_TOKEN déjà présent';
311
+ if (envContent.includes('ASKMESH_TOKEN') && !envContent.includes('your_token_here')) {
312
+ envStatus = '.env : ASKMESH_TOKEN présent';
258
313
  }
259
314
  else {
260
- envStatus = '.env : ASKMESH_TOKEN absent — ajoute-le manuellement';
315
+ envStatus = '.env : ASKMESH_TOKEN absent — ajoute-le ou relance /ask-setup avec ton token';
261
316
  }
262
317
  }
263
318
  else {
@@ -274,29 +329,61 @@ function setupSkillsAndStatusLine() {
274
329
  gitignoreStatus = '.gitignore : .env ajouté';
275
330
  }
276
331
  }
277
- // Status line info
332
+ // Auto-configure status line
333
+ // Copy statusline.sh to a stable location (~/.claude/) so it survives npx cache clears
278
334
  const mcpDir = dirname(fileURLToPath(import.meta.url));
279
- const statusLineScript = join(mcpDir, '..', 'statusline.sh');
280
- const statusLineExists = existsSync(statusLineScript);
335
+ const sourceScript = join(mcpDir, '..', 'statusline.sh');
336
+ const claudeSettingsDir = join(homedir(), '.claude');
337
+ const stableScript = join(claudeSettingsDir, 'askmesh-statusline.sh');
338
+ const claudeSettingsPath = join(claudeSettingsDir, 'settings.json');
339
+ let statusLineStatus = '';
340
+ try {
341
+ mkdirSync(claudeSettingsDir, { recursive: true });
342
+ // Copy script to stable location
343
+ if (existsSync(sourceScript)) {
344
+ copyFileSync(sourceScript, stableScript);
345
+ chmodSync(stableScript, 0o755);
346
+ }
347
+ if (existsSync(stableScript)) {
348
+ let settings = {};
349
+ if (existsSync(claudeSettingsPath)) {
350
+ settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
351
+ }
352
+ if (settings.statusLine?.command === stableScript) {
353
+ statusLineStatus = 'Status line : déjà configurée';
354
+ }
355
+ else {
356
+ settings.statusLine = { type: 'command', command: stableScript };
357
+ writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
358
+ statusLineStatus = 'Status line : configurée automatiquement';
359
+ }
360
+ }
361
+ else {
362
+ statusLineStatus = 'Status line : script source non trouvé';
363
+ }
364
+ }
365
+ catch (e) {
366
+ statusLineStatus = `Status line : erreur — ${e}`;
367
+ }
368
+ // Build report
281
369
  const lines = ['Setup AskMesh terminé !\n'];
282
370
  if (installed.length > 0) {
283
371
  lines.push(`Slash commands installées : ${installed.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
284
372
  }
285
373
  if (skipped.length > 0) {
286
- lines.push(`Déjà présentes (non écrasées) : ${skipped.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
374
+ lines.push(`Mises à jour : ${skipped.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
375
+ }
376
+ if (removed.length > 0) {
377
+ lines.push(`Anciennes skills supprimées : ${removed.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
287
378
  }
288
379
  lines.push('');
289
380
  lines.push(envStatus);
290
381
  if (gitignoreStatus)
291
382
  lines.push(gitignoreStatus);
292
- lines.push('');
293
- if (statusLineExists) {
294
- lines.push(`Status line disponible : ${statusLineScript}`);
295
- lines.push(`Pour l'activer, ajoute dans ~/.claude/settings.json :`);
296
- lines.push(` "statusLine": { "type": "command", "command": "${statusLineScript}" }`);
297
- }
298
- else {
299
- lines.push('Status line script non trouvé (sera disponible après npm install)');
383
+ lines.push(statusLineStatus);
384
+ if (token) {
385
+ lines.push('');
386
+ lines.push('Redémarre Claude Code pour activer AskMesh complètement.');
300
387
  }
301
388
  return text(lines.join('\n'));
302
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "AskMesh MCP server — connect your AI coding agent to your team's mesh network",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.0.0",
37
+ "dotenv": "^17.4.0",
37
38
  "eventsource": "^2.0.2",
38
39
  "zod": "^4.3.6"
39
40
  },
package/statusline.sh CHANGED
@@ -1,29 +1,36 @@
1
1
  #!/bin/bash
2
2
  # AskMesh status line for Claude Code
3
- # Reads cached notification counts from the MCP server
4
- # Install: add to settings.json statusLine.command
3
+ # Reads cached notification counts from MCP server(s)
4
+ # Aggregates across all active agent caches
5
5
 
6
6
  input=$(cat)
7
- model=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
8
7
 
9
- cache_file="${TMPDIR:-/tmp}/askmesh_status.json"
8
+ tmpdir="${TMPDIR:-/tmp}"
9
+ pending=0
10
+ active=0
11
+ replies=0
10
12
 
11
- parts=()
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
12
23
 
13
- if [ -f "$cache_file" ]; then
14
- pending=$(jq -r '.pending // 0' "$cache_file" 2>/dev/null)
15
- active=$(jq -r '.active // 0' "$cache_file" 2>/dev/null)
16
- replies=$(jq -r '.unread_replies // 0' "$cache_file" 2>/dev/null)
24
+ parts=()
17
25
 
18
- if [ "$pending" -gt 0 ] 2>/dev/null; then
19
- parts+=("\033[33m${pending}↓\033[0m")
20
- fi
21
- if [ "$active" -gt 0 ] 2>/dev/null; then
22
- parts+=("\033[32m${active}~\033[0m")
23
- fi
24
- if [ "$replies" -gt 0 ] 2>/dev/null; then
25
- parts+=("\033[36m${replies}>\033[0m")
26
- fi
26
+ if [ "$pending" -gt 0 ] 2>/dev/null; then
27
+ parts+=("\033[33m${pending}↓\033[0m")
28
+ fi
29
+ if [ "$active" -gt 0 ] 2>/dev/null; then
30
+ parts+=("\033[32m${active}~\033[0m")
31
+ fi
32
+ if [ "$replies" -gt 0 ] 2>/dev/null; then
33
+ parts+=("\033[36m${replies}>\033[0m")
27
34
  fi
28
35
 
29
36
  if [ ${#parts[@]} -gt 0 ]; then