@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.
- package/dist/agent/auto_responder.js +8 -1
- package/dist/index.js +1 -1
- package/dist/statusline/cache.d.ts +3 -0
- package/dist/statusline/cache.js +11 -2
- package/dist/tools/askmesh.js +86 -30
- package/package.json +1 -1
- package/statusline.sh +31 -12
|
@@ -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.
|
|
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 {};
|
package/dist/statusline/cache.js
CHANGED
|
@@ -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
|
+
}
|
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) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
387
|
-
const stableScript = join(
|
|
388
|
-
const
|
|
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(
|
|
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(
|
|
400
|
-
settings = JSON.parse(readFileSync(
|
|
448
|
+
if (existsSync(projectSettingsPath)) {
|
|
449
|
+
settings = JSON.parse(readFileSync(projectSettingsPath, 'utf-8'));
|
|
401
450
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
package/statusline.sh
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
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
|
|
14
|
+
loop="false"
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|