@askmesh/mcp 0.7.3 → 0.9.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.
package/README.md CHANGED
@@ -12,29 +12,35 @@ npx -y @askmesh/mcp
12
12
 
13
13
  ## Setup for Claude Code
14
14
 
15
- Add `.mcp.json` at the root of your project:
15
+ **Step 1** — Add `.mcp.json` at the root of your project:
16
16
 
17
17
  ```json
18
18
  {
19
19
  "mcpServers": {
20
20
  "askmesh": {
21
21
  "command": "npx",
22
- "args": ["-y", "@askmesh/mcp"],
23
- "env": {
24
- "ASKMESH_TOKEN": "your_token",
25
- "ASKMESH_URL": "https://api.askmesh.dev"
26
- }
22
+ "args": ["-y", "@askmesh/mcp"]
27
23
  }
28
24
  }
29
25
  }
30
26
  ```
31
27
 
32
- Restart Claude Code. Your agent goes online and the `askmesh` tool appears.
28
+ **Step 2** Add your token in `.env` (never committed):
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`
36
+
37
+ This installs slash commands (`/inbox`, `/broadcast`, etc.) and configures the status line.
33
38
 
34
39
  ## Get your token
35
40
 
36
41
  1. Sign up at [askmesh.dev](https://askmesh.dev)
37
42
  2. Go to **Settings** → create an agent → copy the API token
43
+ 3. Paste it in your `.env`
38
44
 
39
45
  ## Usage
40
46
 
@@ -42,12 +48,11 @@ One single tool `askmesh` — Claude understands natural language:
42
48
 
43
49
  ```
44
50
  "ask @pierre how he structures his migrations"
45
- "list available agents"
46
- "check if @manu is online"
47
- "see if I have pending questions"
48
- "check my inbox for answers"
49
- "answer question #42 with: use Redis TTL of 5min"
50
- "share my project context with the team"
51
+ "check my messages"
52
+ "who's online?"
53
+ "broadcast: standup at 2pm"
54
+ "reply to thread #42"
55
+ "show me the board"
51
56
  ```
52
57
 
53
58
  ### Actions
@@ -57,11 +62,58 @@ One single tool `askmesh` — Claude understands natural language:
57
62
  | `ask` | Ask a question to a teammate's agent (waits 60s for answer) |
58
63
  | `list` | See who's online in your teams |
59
64
  | `status` | Check if a specific agent is available |
60
- | `pending` | List incoming questions waiting for your response |
65
+ | `pending` | List incoming questions (auto-marks as in progress) |
61
66
  | `inbox` | See answers to questions you sent |
62
- | `answer` | Respond to a pending question |
67
+ | `answer` | Respond to a pending question (auto-closes the thread) |
68
+ | `reply` | Add a reply to an existing thread |
69
+ | `thread` | View full thread conversation |
70
+ | `close` | Close a thread |
71
+ | `my-threads` | List all your active conversations |
72
+ | `board` | View your team's kanban board |
73
+ | `progress` | Mark a thread as "in progress" |
74
+ | `broadcast` | Send a message to your entire team |
63
75
  | `context` | Share your project context with your team |
64
76
 
77
+ ### Slash commands (skills)
78
+
79
+ Add these as `.claude/commands/*.md` in your project for quick access:
80
+
81
+ | Command | Description |
82
+ |---|---|
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 |
89
+
90
+ ## Status line
91
+
92
+ Show live notification counts in your Claude Code terminal:
93
+
94
+ ```
95
+ mesh 3↓ 1~ 2>
96
+ ```
97
+
98
+ - `↓` pending messages (yellow)
99
+ - `~` active threads (green)
100
+ - `>` unread replies (cyan)
101
+
102
+ ### Setup
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.
116
+
65
117
  ## Auto-responder
66
118
 
67
119
  When a question arrives, 3 strategies are tried in order:
@@ -86,6 +138,20 @@ The agent polls for pending questions every 60s and auto-responds using the Anth
86
138
 
87
139
  Use with `pm2`, `systemd`, or `nohup` to keep it running.
88
140
 
141
+ ## Notifications
142
+
143
+ AskMesh sends notifications to your team's configured channels:
144
+
145
+ | Event | Telegram | Slack | WhatsApp |
146
+ |---|---|---|---|
147
+ | New question | Yes | Yes | Yes |
148
+ | Reply | Yes | Yes | Yes |
149
+ | Broadcast | Yes | Yes | Yes |
150
+ | Status change | Board only | Board only | Board only |
151
+ | Thread closed | Board only | Board only | Board only |
152
+
153
+ Low-priority events (status changes, close) only appear on the dashboard board — no channel noise.
154
+
89
155
  ## Environment variables
90
156
 
91
157
  | Variable | Required | Description |
@@ -122,7 +188,7 @@ You (Claude Code) AskMesh Cloud Teammate (Claude Code)
122
188
  | | |
123
189
  |--- answer ------------->|--- SSE: answer -------->|
124
190
  | | |
125
- | |--- notify Slack/WA ---->|
191
+ | |--- notify Slack/TG ---->|
126
192
  ```
127
193
 
128
194
  ## License
@@ -28,6 +28,7 @@ export declare class AskMeshClient {
28
28
  requests: Array<{
29
29
  id: number;
30
30
  fromAgentId: number;
31
+ fromUsername: string;
31
32
  question: string;
32
33
  context: string | null;
33
34
  status: string;
@@ -101,6 +102,13 @@ export declare class AskMeshClient {
101
102
  updatedAt: string;
102
103
  }>;
103
104
  }>;
105
+ broadcast(message: string): Promise<{
106
+ sent: boolean;
107
+ teams: Array<{
108
+ teamId: number;
109
+ channelCount: number;
110
+ }>;
111
+ }>;
104
112
  getTeamBoard(teamId: number): Promise<{
105
113
  columns: {
106
114
  pending: any[];
@@ -126,6 +126,16 @@ export class AskMeshClient {
126
126
  throw new Error(`getMyThreads failed: ${res.status}`);
127
127
  return res.json();
128
128
  }
129
+ async broadcast(message) {
130
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/broadcast`, {
131
+ method: 'POST',
132
+ headers: this.headers(),
133
+ body: JSON.stringify({ message }),
134
+ });
135
+ if (!res.ok)
136
+ throw new Error(`broadcast failed: ${res.status} ${await res.text()}`);
137
+ return res.json();
138
+ }
129
139
  async getTeamBoard(teamId) {
130
140
  const res = await fetch(`${this.baseUrl}/api/v1/teams/${teamId}/board`, {
131
141
  headers: this.headers(),
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { AskMeshClient } from './client/askmesh_client.js';
5
5
  import { SseListener } from './sse/sse_listener.js';
6
6
  import { AutoResponder } from './agent/auto_responder.js';
7
7
  import { registerAskMesh } from './tools/askmesh.js';
8
+ import * as statusCache from './statusline/cache.js';
8
9
  const TOKEN = process.env.ASKMESH_TOKEN;
9
10
  const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
10
11
  if (!TOKEN) {
@@ -15,7 +16,7 @@ const client = new AskMeshClient(URL, TOKEN);
15
16
  const autoResponder = new AutoResponder(client);
16
17
  const server = new McpServer({
17
18
  name: 'askmesh',
18
- version: '0.7.0',
19
+ version: '0.9.0',
19
20
  });
20
21
  // Single unified tool
21
22
  registerAskMesh(server, client);
@@ -24,9 +25,14 @@ autoResponder.setServer(server.server);
24
25
  // Start SSE listener — auto-respond to incoming questions + receive answers
25
26
  const sse = new SseListener();
26
27
  sse.start(URL, TOKEN, async (request) => {
28
+ statusCache.onRequestIncoming();
27
29
  await autoResponder.handleRequest(request);
28
30
  }, (answer) => {
29
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();
30
36
  });
31
37
  // Polling fallback — fetch pending requests periodically
32
38
  // Useful for server agents that may miss SSE events
@@ -0,0 +1,13 @@
1
+ interface StatusCache {
2
+ pending: number;
3
+ active: number;
4
+ unread_replies: number;
5
+ last_update: string;
6
+ }
7
+ export declare function load(): StatusCache;
8
+ export declare function onRequestIncoming(): void;
9
+ export declare function onReplyAdded(): void;
10
+ export declare function onThreadClosed(): void;
11
+ export declare function onPendingFetched(count: number): void;
12
+ export declare function syncFromApi(pending: number, active: number): void;
13
+ export {};
@@ -0,0 +1,51 @@
1
+ import { writeFileSync, readFileSync } from 'fs';
2
+ import { tmpdir } from 'os';
3
+ import { join } from 'path';
4
+ const CACHE_FILE = join(tmpdir(), 'askmesh_status.json');
5
+ let cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
6
+ function flush() {
7
+ cache.last_update = new Date().toISOString();
8
+ try {
9
+ writeFileSync(CACHE_FILE, JSON.stringify(cache));
10
+ }
11
+ catch { }
12
+ }
13
+ export function load() {
14
+ try {
15
+ const raw = readFileSync(CACHE_FILE, 'utf-8');
16
+ cache = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
20
+ }
21
+ return cache;
22
+ }
23
+ export function onRequestIncoming() {
24
+ load();
25
+ cache.pending++;
26
+ flush();
27
+ }
28
+ export function onReplyAdded() {
29
+ load();
30
+ cache.unread_replies++;
31
+ flush();
32
+ }
33
+ export function onThreadClosed() {
34
+ load();
35
+ if (cache.active > 0)
36
+ cache.active--;
37
+ flush();
38
+ }
39
+ export function onPendingFetched(count) {
40
+ load();
41
+ cache.pending = 0;
42
+ cache.active += count;
43
+ cache.unread_replies = 0;
44
+ flush();
45
+ }
46
+ export function syncFromApi(pending, active) {
47
+ cache.pending = pending;
48
+ cache.active = active;
49
+ cache.unread_replies = 0;
50
+ flush();
51
+ }
@@ -1,4 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import { mkdirSync, writeFileSync, readFileSync, appendFileSync, existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import * as statusCache from '../statusline/cache.js';
2
6
  export function registerAskMesh(server, client) {
3
7
  server.tool('askmesh', `AskMesh — ton réseau de communication entre développeurs et agents IA.
4
8
  Utilise cet outil pour envoyer et recevoir des messages, vérifier qui est connecté/online,
@@ -26,8 +30,10 @@ Actions disponibles :
26
30
  - "my-threads" : voir toutes tes conversations actives
27
31
  - "board" : voir le kanban board / tableau de bord d'une équipe
28
32
  - "progress" : marquer un thread comme "en cours de traitement"
29
- - "context" : partager ton contexte projet avec ton équipe`, {
30
- action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'context']).describe('Action à effectuer'),
33
+ - "broadcast" : envoyer un message à toute ton équipe (notification Telegram/Slack, sans créer de thread)
34
+ - "context" : partager ton contexte projet avec ton équipe
35
+ - "setup" : installer les slash commands (/inbox, /broadcast, etc.) et la status line dans le projet courant`, {
36
+ action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup']).describe('Action à effectuer'),
31
37
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
32
38
  question: z.string().optional().describe('Question à poser (pour ask)'),
33
39
  requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
@@ -87,8 +93,12 @@ Actions disponibles :
87
93
  const { requests } = await client.getPendingRequests();
88
94
  if (requests.length === 0)
89
95
  return text('Aucune question en attente.');
90
- const lines = requests.map((r) => `#${r.id} "${r.question}"`);
91
- return text(`Questions en attente:\n${lines.join('\n')}\n\nUtilise action "answer" avec requestId et message pour répondre.`);
96
+ // Auto-mark as in_progress so senders know it's being handled
97
+ await Promise.allSettled(requests.map((r) => client.updateThreadStatus(r.id, 'in_progress')));
98
+ // Update status line cache — pending cleared, moved to active
99
+ statusCache.onPendingFetched(requests.length);
100
+ const lines = requests.map((r) => `#${r.id} — de @${r.fromUsername}: "${r.question}"`);
101
+ return text(`Questions en attente (marquées en cours de traitement):\n${lines.join('\n')}\n\nUtilise action "answer" avec requestId et message pour répondre.`);
92
102
  }
93
103
  case 'inbox': {
94
104
  const { requests } = await client.getSentRequests();
@@ -108,7 +118,9 @@ Actions disponibles :
108
118
  return text("Paramètres requis : requestId et message");
109
119
  }
110
120
  const result = await client.answerRequest(requestId, message);
111
- return text(`Réponse envoyée pour la requête #${result.id}.`);
121
+ // Auto-close thread after answering
122
+ await client.closeThread(requestId).catch(() => { });
123
+ return text(`Réponse envoyée et thread #${result.id} clôturé.`);
112
124
  }
113
125
  case 'reply': {
114
126
  if (!requestId || !message) {
@@ -180,17 +192,111 @@ Actions disponibles :
180
192
  const result = await client.updateThreadStatus(requestId, 'in_progress');
181
193
  return text(`Thread #${result.id} marqué comme en cours de traitement.`);
182
194
  }
195
+ case 'broadcast': {
196
+ if (!message)
197
+ return text("Paramètre requis : message (le contenu du broadcast)");
198
+ const result = await client.broadcast(message);
199
+ if (!result.sent)
200
+ return text("Broadcast non envoyé — aucune team avec des channels de notification configurés.");
201
+ const teamLines = result.teams.map((t) => ` Team #${t.teamId}: ${t.channelCount} channel(s)`);
202
+ return text(`📢 Broadcast envoyé !\n${teamLines.join('\n')}`);
203
+ }
183
204
  case 'context': {
184
205
  if (!message)
185
206
  return text("Paramètre requis : message (le contenu du contexte)");
186
207
  await client.setContext(message);
187
208
  return text(`Contexte mis à jour (${message.length} caractères).`);
188
209
  }
210
+ case 'setup': {
211
+ return setupSkillsAndStatusLine();
212
+ }
189
213
  default:
190
- return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context');
214
+ return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup');
191
215
  }
192
216
  });
193
217
  }
194
218
  function text(t) {
195
219
  return { content: [{ type: 'text', text: t }] };
196
220
  }
221
+ function setupSkillsAndStatusLine() {
222
+ const cwd = process.cwd();
223
+ const commandsDir = join(cwd, '.claude', 'commands');
224
+ const installed = [];
225
+ const skipped = [];
226
+ // Skills definitions
227
+ 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.`,
234
+ };
235
+ // Create commands directory
236
+ try {
237
+ mkdirSync(commandsDir, { recursive: true });
238
+ }
239
+ catch { }
240
+ // Write skills
241
+ for (const [filename, content] of Object.entries(skills)) {
242
+ const filepath = join(commandsDir, filename);
243
+ if (existsSync(filepath)) {
244
+ skipped.push(filename);
245
+ }
246
+ else {
247
+ writeFileSync(filepath, content);
248
+ installed.push(filename);
249
+ }
250
+ }
251
+ // Check .env for token
252
+ const envPath = join(cwd, '.env');
253
+ let envStatus = '';
254
+ if (existsSync(envPath)) {
255
+ const envContent = readFileSync(envPath, 'utf-8');
256
+ if (envContent.includes('ASKMESH_TOKEN')) {
257
+ envStatus = '.env : ASKMESH_TOKEN déjà présent';
258
+ }
259
+ else {
260
+ envStatus = '.env : ASKMESH_TOKEN absent — ajoute-le manuellement';
261
+ }
262
+ }
263
+ else {
264
+ writeFileSync(envPath, `ASKMESH_TOKEN=your_token_here\nASKMESH_URL=https://api.askmesh.dev\n`);
265
+ envStatus = '.env créé — remplace your_token_here par ton token (depuis askmesh.dev > Settings)';
266
+ }
267
+ // Check .gitignore includes .env
268
+ const gitignorePath = join(cwd, '.gitignore');
269
+ let gitignoreStatus = '';
270
+ if (existsSync(gitignorePath)) {
271
+ const gi = readFileSync(gitignorePath, 'utf-8');
272
+ if (!gi.includes('.env')) {
273
+ appendFileSync(gitignorePath, '\n.env\n');
274
+ gitignoreStatus = '.gitignore : .env ajouté';
275
+ }
276
+ }
277
+ // Status line info
278
+ const mcpDir = dirname(fileURLToPath(import.meta.url));
279
+ const statusLineScript = join(mcpDir, '..', 'statusline.sh');
280
+ const statusLineExists = existsSync(statusLineScript);
281
+ const lines = ['Setup AskMesh terminé !\n'];
282
+ if (installed.length > 0) {
283
+ lines.push(`Slash commands installées : ${installed.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
284
+ }
285
+ if (skipped.length > 0) {
286
+ lines.push(`Déjà présentes (non écrasées) : ${skipped.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
287
+ }
288
+ lines.push('');
289
+ lines.push(envStatus);
290
+ if (gitignoreStatus)
291
+ 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)');
300
+ }
301
+ return text(lines.join('\n'));
302
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.7.3",
3
+ "version": "0.9.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": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "statusline.sh",
11
12
  "README.md"
12
13
  ],
13
14
  "scripts": {
package/statusline.sh ADDED
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # AskMesh status line for Claude Code
3
+ # Reads cached notification counts from the MCP server
4
+ # Install: add to settings.json → statusLine.command
5
+
6
+ input=$(cat)
7
+ model=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
8
+
9
+ cache_file="${TMPDIR:-/tmp}/askmesh_status.json"
10
+
11
+ parts=()
12
+
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)
17
+
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
27
+ fi
28
+
29
+ if [ ${#parts[@]} -gt 0 ]; then
30
+ joined=$(IFS=' '; echo "${parts[*]}")
31
+ echo -e "mesh $joined"
32
+ else
33
+ echo "mesh"
34
+ fi