@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 +1 -1
- package/dist/tools/askmesh.js +104 -29
- package/package.json +1 -1
- package/statusline.sh +29 -17
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.
|
|
14
|
+
version: '0.10.5',
|
|
15
15
|
});
|
|
16
16
|
if (!TOKEN) {
|
|
17
17
|
// No token — start in setup-only mode
|
package/dist/tools/askmesh.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
353
|
-
const stableScript = join(
|
|
354
|
-
const
|
|
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(
|
|
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(
|
|
366
|
-
settings = JSON.parse(readFileSync(
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
package/statusline.sh
CHANGED
|
@@ -1,41 +1,53 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# AskMesh status line for Claude Code
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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
|