@askmesh/mcp 0.8.0 → 0.10.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 +82 -16
- package/dist/client/askmesh_client.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +57 -48
- package/dist/statusline/cache.d.ts +16 -0
- package/dist/statusline/cache.js +60 -0
- package/dist/tools/askmesh.d.ts +1 -0
- package/dist/tools/askmesh.js +185 -7
- package/package.json +3 -1
- package/statusline.sh +41 -0
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
|
-
|
|
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
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
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
|
|
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/
|
|
191
|
+
| |--- notify Slack/TG ---->|
|
|
126
192
|
```
|
|
127
193
|
|
|
128
194
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import 'dotenv/config';
|
package/dist/index.js
CHANGED
|
@@ -1,64 +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';
|
|
9
|
+
import * as statusCache from './statusline/cache.js';
|
|
8
10
|
const TOKEN = process.env.ASKMESH_TOKEN;
|
|
9
11
|
const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
|
|
10
|
-
if (!TOKEN) {
|
|
11
|
-
console.error('[AskMesh] ASKMESH_TOKEN is required');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
const client = new AskMeshClient(URL, TOKEN);
|
|
15
|
-
const autoResponder = new AutoResponder(client);
|
|
16
12
|
const server = new McpServer({
|
|
17
13
|
name: 'askmesh',
|
|
18
|
-
version: '0.
|
|
14
|
+
version: '0.10.0',
|
|
19
15
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
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
|
+
});
|
|
52
70
|
}
|
|
53
|
-
// Cleanup on exit
|
|
54
|
-
process.on('SIGINT', () => {
|
|
55
|
-
sse.stop();
|
|
56
|
-
process.exit(0);
|
|
57
|
-
});
|
|
58
|
-
process.on('SIGTERM', () => {
|
|
59
|
-
sse.stop();
|
|
60
|
-
process.exit(0);
|
|
61
|
-
});
|
|
62
71
|
// Start MCP server via stdio
|
|
63
72
|
const transport = new StdioServerTransport();
|
|
64
73
|
await server.connect(transport);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface StatusCache {
|
|
2
|
+
pending: number;
|
|
3
|
+
active: number;
|
|
4
|
+
unread_replies: number;
|
|
5
|
+
last_update: string;
|
|
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;
|
|
10
|
+
export declare function load(): StatusCache;
|
|
11
|
+
export declare function onRequestIncoming(): void;
|
|
12
|
+
export declare function onReplyAdded(): void;
|
|
13
|
+
export declare function onThreadClosed(): void;
|
|
14
|
+
export declare function onPendingFetched(count: number): void;
|
|
15
|
+
export declare function syncFromApi(pending: number, active: number): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
let CACHE_FILE = join(tmpdir(), 'askmesh_status.json');
|
|
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
|
+
}
|
|
12
|
+
function flush() {
|
|
13
|
+
cache.last_update = new Date().toISOString();
|
|
14
|
+
try {
|
|
15
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache));
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
}
|
|
19
|
+
export function getCacheFile() {
|
|
20
|
+
return CACHE_FILE;
|
|
21
|
+
}
|
|
22
|
+
export function load() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(CACHE_FILE, 'utf-8');
|
|
25
|
+
cache = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
cache = { pending: 0, active: 0, unread_replies: 0, last_update: '' };
|
|
29
|
+
}
|
|
30
|
+
return cache;
|
|
31
|
+
}
|
|
32
|
+
export function onRequestIncoming() {
|
|
33
|
+
load();
|
|
34
|
+
cache.pending++;
|
|
35
|
+
flush();
|
|
36
|
+
}
|
|
37
|
+
export function onReplyAdded() {
|
|
38
|
+
load();
|
|
39
|
+
cache.unread_replies++;
|
|
40
|
+
flush();
|
|
41
|
+
}
|
|
42
|
+
export function onThreadClosed() {
|
|
43
|
+
load();
|
|
44
|
+
if (cache.active > 0)
|
|
45
|
+
cache.active--;
|
|
46
|
+
flush();
|
|
47
|
+
}
|
|
48
|
+
export function onPendingFetched(count) {
|
|
49
|
+
load();
|
|
50
|
+
cache.pending = 0;
|
|
51
|
+
cache.active += count;
|
|
52
|
+
cache.unread_replies = 0;
|
|
53
|
+
flush();
|
|
54
|
+
}
|
|
55
|
+
export function syncFromApi(pending, active) {
|
|
56
|
+
cache.pending = pending;
|
|
57
|
+
cache.active = active;
|
|
58
|
+
cache.unread_replies = 0;
|
|
59
|
+
flush();
|
|
60
|
+
}
|
package/dist/tools/askmesh.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/askmesh.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, appendFileSync, existsSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import * as statusCache from '../statusline/cache.js';
|
|
2
7
|
export function registerAskMesh(server, client) {
|
|
3
8
|
server.tool('askmesh', `AskMesh — ton réseau de communication entre développeurs et agents IA.
|
|
4
9
|
Utilise cet outil pour envoyer et recevoir des messages, vérifier qui est connecté/online,
|
|
@@ -27,15 +32,17 @@ Actions disponibles :
|
|
|
27
32
|
- "board" : voir le kanban board / tableau de bord d'une équipe
|
|
28
33
|
- "progress" : marquer un thread comme "en cours de traitement"
|
|
29
34
|
- "broadcast" : envoyer un message à toute ton équipe (notification Telegram/Slack, sans créer de thread)
|
|
30
|
-
- "context" : partager ton contexte projet avec ton équipe
|
|
31
|
-
|
|
35
|
+
- "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
|
+
action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup']).describe('Action à effectuer'),
|
|
32
38
|
username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
|
|
33
39
|
question: z.string().optional().describe('Question à poser (pour ask)'),
|
|
34
40
|
requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
|
|
35
41
|
message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/reply/context)'),
|
|
36
42
|
parentThreadId: z.number().optional().describe('ID du thread parent (pour ask, lie les threads entre eux)'),
|
|
37
43
|
teamId: z.number().optional().describe('ID de la team (pour board)'),
|
|
38
|
-
|
|
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 }) => {
|
|
39
46
|
switch (action) {
|
|
40
47
|
case 'ask': {
|
|
41
48
|
if (!username || !question) {
|
|
@@ -88,8 +95,12 @@ Actions disponibles :
|
|
|
88
95
|
const { requests } = await client.getPendingRequests();
|
|
89
96
|
if (requests.length === 0)
|
|
90
97
|
return text('Aucune question en attente.');
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
// Auto-mark as in_progress so senders know it's being handled
|
|
99
|
+
await Promise.allSettled(requests.map((r) => client.updateThreadStatus(r.id, 'in_progress')));
|
|
100
|
+
// Update status line cache — pending cleared, moved to active
|
|
101
|
+
statusCache.onPendingFetched(requests.length);
|
|
102
|
+
const lines = requests.map((r) => `#${r.id} — de @${r.fromUsername}: "${r.question}"`);
|
|
103
|
+
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.`);
|
|
93
104
|
}
|
|
94
105
|
case 'inbox': {
|
|
95
106
|
const { requests } = await client.getSentRequests();
|
|
@@ -109,7 +120,9 @@ Actions disponibles :
|
|
|
109
120
|
return text("Paramètres requis : requestId et message");
|
|
110
121
|
}
|
|
111
122
|
const result = await client.answerRequest(requestId, message);
|
|
112
|
-
|
|
123
|
+
// Auto-close thread after answering
|
|
124
|
+
await client.closeThread(requestId).catch(() => { });
|
|
125
|
+
return text(`Réponse envoyée et thread #${result.id} clôturé.`);
|
|
113
126
|
}
|
|
114
127
|
case 'reply': {
|
|
115
128
|
if (!requestId || !message) {
|
|
@@ -196,11 +209,176 @@ Actions disponibles :
|
|
|
196
209
|
await client.setContext(message);
|
|
197
210
|
return text(`Contexte mis à jour (${message.length} caractères).`);
|
|
198
211
|
}
|
|
212
|
+
case 'setup': {
|
|
213
|
+
return setupSkillsAndStatusLine(token);
|
|
214
|
+
}
|
|
199
215
|
default:
|
|
200
|
-
return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context');
|
|
216
|
+
return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context, setup');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
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);
|
|
201
236
|
}
|
|
237
|
+
return text('Action inconnue.');
|
|
202
238
|
});
|
|
203
239
|
}
|
|
204
240
|
function text(t) {
|
|
205
241
|
return { content: [{ type: 'text', text: t }] };
|
|
206
242
|
}
|
|
243
|
+
function setupSkillsAndStatusLine(token) {
|
|
244
|
+
const cwd = process.cwd();
|
|
245
|
+
const commandsDir = join(cwd, '.claude', 'commands');
|
|
246
|
+
const installed = [];
|
|
247
|
+
const skipped = [];
|
|
248
|
+
// Skills definitions
|
|
249
|
+
const skills = {
|
|
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".`,
|
|
257
|
+
};
|
|
258
|
+
// Create commands directory
|
|
259
|
+
try {
|
|
260
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
261
|
+
}
|
|
262
|
+
catch { }
|
|
263
|
+
// Write skills (always overwrite to keep them up to date)
|
|
264
|
+
for (const [filename, content] of Object.entries(skills)) {
|
|
265
|
+
const filepath = join(commandsDir, filename);
|
|
266
|
+
const existed = existsSync(filepath);
|
|
267
|
+
writeFileSync(filepath, content);
|
|
268
|
+
if (existed) {
|
|
269
|
+
skipped.push(filename);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
installed.push(filename);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
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
|
|
289
|
+
const envPath = join(cwd, '.env');
|
|
290
|
+
let envStatus = '';
|
|
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)) {
|
|
310
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
311
|
+
if (envContent.includes('ASKMESH_TOKEN') && !envContent.includes('your_token_here')) {
|
|
312
|
+
envStatus = '.env : ASKMESH_TOKEN présent';
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
envStatus = '.env : ASKMESH_TOKEN absent — ajoute-le ou relance /ask-setup avec ton token';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
writeFileSync(envPath, `ASKMESH_TOKEN=your_token_here\nASKMESH_URL=https://api.askmesh.dev\n`);
|
|
320
|
+
envStatus = '.env créé — remplace your_token_here par ton token (depuis askmesh.dev > Settings)';
|
|
321
|
+
}
|
|
322
|
+
// Check .gitignore includes .env
|
|
323
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
324
|
+
let gitignoreStatus = '';
|
|
325
|
+
if (existsSync(gitignorePath)) {
|
|
326
|
+
const gi = readFileSync(gitignorePath, 'utf-8');
|
|
327
|
+
if (!gi.includes('.env')) {
|
|
328
|
+
appendFileSync(gitignorePath, '\n.env\n');
|
|
329
|
+
gitignoreStatus = '.gitignore : .env ajouté';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Auto-configure status line in ~/.claude/settings.json
|
|
333
|
+
const mcpDir = dirname(fileURLToPath(import.meta.url));
|
|
334
|
+
const statusLineScript = join(mcpDir, '..', 'statusline.sh');
|
|
335
|
+
const statusLineExists = existsSync(statusLineScript);
|
|
336
|
+
let statusLineStatus = '';
|
|
337
|
+
if (statusLineExists) {
|
|
338
|
+
const claudeSettingsDir = join(homedir(), '.claude');
|
|
339
|
+
const claudeSettingsPath = join(claudeSettingsDir, 'settings.json');
|
|
340
|
+
try {
|
|
341
|
+
mkdirSync(claudeSettingsDir, { recursive: true });
|
|
342
|
+
let settings = {};
|
|
343
|
+
if (existsSync(claudeSettingsPath)) {
|
|
344
|
+
settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
|
|
345
|
+
}
|
|
346
|
+
const currentCommand = settings.statusLine?.command;
|
|
347
|
+
if (currentCommand === statusLineScript) {
|
|
348
|
+
statusLineStatus = 'Status line : déjà configurée';
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
settings.statusLine = { type: 'command', command: statusLineScript };
|
|
352
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
353
|
+
statusLineStatus = 'Status line : configurée automatiquement';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
statusLineStatus = `Status line : erreur de configuration — ${e}`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
statusLineStatus = 'Status line : script non trouvé (sera disponible après npm install)';
|
|
362
|
+
}
|
|
363
|
+
// Build report
|
|
364
|
+
const lines = ['Setup AskMesh terminé !\n'];
|
|
365
|
+
if (installed.length > 0) {
|
|
366
|
+
lines.push(`Slash commands installées : ${installed.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
|
|
367
|
+
}
|
|
368
|
+
if (skipped.length > 0) {
|
|
369
|
+
lines.push(`Mises à jour : ${skipped.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
|
|
370
|
+
}
|
|
371
|
+
if (removed.length > 0) {
|
|
372
|
+
lines.push(`Anciennes skills supprimées : ${removed.map((f) => '/' + f.replace('.md', '')).join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
lines.push('');
|
|
375
|
+
lines.push(envStatus);
|
|
376
|
+
if (gitignoreStatus)
|
|
377
|
+
lines.push(gitignoreStatus);
|
|
378
|
+
lines.push(statusLineStatus);
|
|
379
|
+
if (token) {
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push('Redémarre Claude Code pour activer AskMesh complètement.');
|
|
382
|
+
}
|
|
383
|
+
return text(lines.join('\n'));
|
|
384
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askmesh/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"license": "MIT",
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
37
|
+
"dotenv": "^17.4.0",
|
|
36
38
|
"eventsource": "^2.0.2",
|
|
37
39
|
"zod": "^4.3.6"
|
|
38
40
|
},
|
package/statusline.sh
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AskMesh status line for Claude Code
|
|
3
|
+
# Reads cached notification counts from MCP server(s)
|
|
4
|
+
# Aggregates across all active agent caches
|
|
5
|
+
|
|
6
|
+
input=$(cat)
|
|
7
|
+
|
|
8
|
+
tmpdir="${TMPDIR:-/tmp}"
|
|
9
|
+
pending=0
|
|
10
|
+
active=0
|
|
11
|
+
replies=0
|
|
12
|
+
|
|
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
|
|
23
|
+
|
|
24
|
+
parts=()
|
|
25
|
+
|
|
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")
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [ ${#parts[@]} -gt 0 ]; then
|
|
37
|
+
joined=$(IFS=' '; echo "${parts[*]}")
|
|
38
|
+
echo -e "mesh $joined"
|
|
39
|
+
else
|
|
40
|
+
echo "mesh"
|
|
41
|
+
fi
|