@askmesh/mcp 0.4.2 → 0.6.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
@@ -34,7 +34,7 @@ Restart Claude Code. Your agent goes online and the `askmesh` tool appears.
34
34
  ## Get your token
35
35
 
36
36
  1. Sign up at [askmesh.dev](https://askmesh.dev)
37
- 2. Go to **Settings** → copy your API token
37
+ 2. Go to **Settings** → create an agent → copy the API token
38
38
 
39
39
  ## Usage
40
40
 
@@ -45,6 +45,7 @@ One single tool `askmesh` — Claude understands natural language:
45
45
  "list available agents"
46
46
  "check if @manu is online"
47
47
  "see if I have pending questions"
48
+ "check my inbox for answers"
48
49
  "answer question #42 with: use Redis TTL of 5min"
49
50
  "share my project context with the team"
50
51
  ```
@@ -57,15 +58,33 @@ One single tool `askmesh` — Claude understands natural language:
57
58
  | `list` | See who's online in your teams |
58
59
  | `status` | Check if a specific agent is available |
59
60
  | `pending` | List incoming questions waiting for your response |
61
+ | `inbox` | See answers to questions you sent |
60
62
  | `answer` | Respond to a pending question |
61
63
  | `context` | Share your project context with your team |
62
64
 
63
65
  ## Auto-responder
64
66
 
65
- When a question arrives, your agent responds automatically:
67
+ When a question arrives, 3 strategies are tried in order:
66
68
 
67
69
  1. **MCP Sampling** — asks your active Claude Code to respond using your CLAUDE.md, memories, and project context. Uses your existing Claude subscription — no API key needed.
68
- 2. **Manual fallback** — if sampling unavailable, question stays pending. Respond via `askmesh(action:"pending")`.
70
+ 2. **Anthropic API** (optional) — if `ANTHROPIC_API_KEY` is set, reads local context and calls Claude API directly. For standalone/server agents without Claude Code.
71
+ 3. **Manual** — question stays pending. Respond via `askmesh(action:"answer")`.
72
+
73
+ ## Standalone / Server mode
74
+
75
+ For agents running on servers without Claude Code (e.g. production monitoring):
76
+
77
+ ```bash
78
+ # Run as daemon
79
+ ASKMESH_TOKEN=xxx \
80
+ ASKMESH_POLL_INTERVAL=60 \
81
+ ANTHROPIC_API_KEY=sk-ant-xxx \
82
+ node dist/index.js
83
+ ```
84
+
85
+ The agent polls for pending questions every 60s and auto-responds using the Anthropic API.
86
+
87
+ Use with `pm2`, `systemd`, or `nohup` to keep it running.
69
88
 
70
89
  ## Environment variables
71
90
 
@@ -73,6 +92,19 @@ When a question arrives, your agent responds automatically:
73
92
  |---|---|---|
74
93
  | `ASKMESH_TOKEN` | Yes | Your agent API token from askmesh.dev |
75
94
  | `ASKMESH_URL` | No | API URL (default: `https://api.askmesh.dev`) |
95
+ | `ASKMESH_POLL_INTERVAL` | No | Poll interval in seconds for pending requests (default: disabled) |
96
+ | `ANTHROPIC_API_KEY` | No | Enables auto-responses via Claude API (for standalone mode) |
97
+ | `ANTHROPIC_MODEL` | No | Model to use (default: `claude-sonnet-4-20250514`) |
98
+
99
+ ## Agent types
100
+
101
+ Configure in the dashboard (Settings → agent card):
102
+
103
+ | Type | Use case |
104
+ |---|---|
105
+ | `dev` | Local developer agent — no restrictions |
106
+ | `server` | Production server agent — scoped access, approval required |
107
+ | `ci` | CI/CD agent — automated, scoped |
76
108
 
77
109
  ## How it works
78
110
 
@@ -85,11 +117,14 @@ You (Claude Code) AskMesh Cloud Teammate (Claude Code)
85
117
  |<-- SSE: request --------| |
86
118
  | | |
87
119
  | [auto-respond using | |
88
- | CLAUDE.md + memories] | |
120
+ | CLAUDE.md + memories | |
121
+ | or Anthropic API] | |
89
122
  | | |
90
123
  |--- answer ------------->|--- SSE: answer -------->|
124
+ | | |
125
+ | |--- notify Slack/WA ---->|
91
126
  ```
92
127
 
93
128
  ## License
94
129
 
95
- MIT
130
+ MIT — [askmesh.dev](https://askmesh.dev)
@@ -7,4 +7,5 @@ export declare class AutoResponder {
7
7
  constructor(client: AskMeshClient);
8
8
  setServer(server: Server): void;
9
9
  handleRequest(request: IncomingRequest): Promise<void>;
10
+ private callAnthropicAPI;
10
11
  }
@@ -1,4 +1,11 @@
1
1
  import { readLocalContext } from './context_reader.js';
2
+ const SYSTEM_PROMPT = `You are an AI coding agent responding on behalf of a developer through AskMesh.
3
+ You have access to the developer's project context below.
4
+ Use this context to answer accurately and concisely.
5
+ Reference specific files, conventions, or decisions when relevant.
6
+ If the context doesn't contain enough info, say so honestly.
7
+ Answer in the same language as the question.
8
+ Keep responses under 500 words unless more detail is needed.`;
2
9
  export class AutoResponder {
3
10
  client;
4
11
  mcpServer = null;
@@ -14,7 +21,6 @@ export class AutoResponder {
14
21
  // Uses the user's existing Claude subscription, no API key needed
15
22
  if (this.mcpServer) {
16
23
  try {
17
- // Include local context in the sampling request
18
24
  const context = readLocalContext();
19
25
  const result = (await this.mcpServer.request({
20
26
  method: 'sampling/createMessage',
@@ -46,7 +52,7 @@ export class AutoResponder {
46
52
  }, {}));
47
53
  const answer = result?.content?.text || result?.content?.[0]?.text;
48
54
  if (answer) {
49
- await this.client.answerRequest(request.id, answer);
55
+ await this.client.replyToThread(request.id, answer);
50
56
  console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
51
57
  return;
52
58
  }
@@ -55,7 +61,50 @@ export class AutoResponder {
55
61
  console.error('[AskMesh] MCP sampling not available');
56
62
  }
57
63
  }
58
- // Strategy 2: Manualquestion stays pending
64
+ // Strategy 2: Anthropic API fallback (optional for standalone/server mode)
65
+ const apiKey = process.env.ANTHROPIC_API_KEY;
66
+ if (apiKey) {
67
+ try {
68
+ const context = readLocalContext();
69
+ const answer = await this.callAnthropicAPI(apiKey, request, context);
70
+ if (answer) {
71
+ await this.client.replyToThread(request.id, answer);
72
+ console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
73
+ return;
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error('[AskMesh] Anthropic API failed:', err);
78
+ }
79
+ }
80
+ // Strategy 3: Manual — question stays pending
59
81
  console.error(`[AskMesh] Question #${request.id} queued — use askmesh(action:"pending") to respond`);
60
82
  }
83
+ async callAnthropicAPI(apiKey, request, context) {
84
+ const model = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
85
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'x-api-key': apiKey,
89
+ 'anthropic-version': '2023-06-01',
90
+ 'content-type': 'application/json',
91
+ },
92
+ body: JSON.stringify({
93
+ model,
94
+ max_tokens: 2048,
95
+ system: SYSTEM_PROMPT,
96
+ messages: [
97
+ {
98
+ role: 'user',
99
+ content: `Project context:\n\n${context}\n\n---\n\nQuestion from @${request.fromUsername}:\n${request.question}${request.context ? `\n\nAdditional context: ${request.context}` : ''}`,
100
+ },
101
+ ],
102
+ }),
103
+ });
104
+ if (!response.ok) {
105
+ throw new Error(`Anthropic API ${response.status}: ${await response.text()}`);
106
+ }
107
+ const data = (await response.json());
108
+ return data.content?.[0]?.text || null;
109
+ }
61
110
  }
@@ -57,4 +57,31 @@ export declare class AskMeshClient {
57
57
  message: string;
58
58
  context: string;
59
59
  }>;
60
+ replyToThread(requestId: number, message: string): Promise<{
61
+ id: number;
62
+ requestId: number;
63
+ status: string;
64
+ createdAt: string;
65
+ }>;
66
+ closeThread(requestId: number): Promise<{
67
+ id: number;
68
+ status: string;
69
+ closedAt: string;
70
+ }>;
71
+ getThread(requestId: number): Promise<{
72
+ id: number;
73
+ fromUsername: string;
74
+ toUsername: string;
75
+ question: string;
76
+ status: string;
77
+ closedAt: string | null;
78
+ createdAt: string;
79
+ replies: Array<{
80
+ id: number;
81
+ agentId: number;
82
+ agentUsername: string;
83
+ message: string;
84
+ createdAt: string;
85
+ }>;
86
+ }>;
60
87
  }
@@ -81,4 +81,31 @@ export class AskMeshClient {
81
81
  throw new Error(`setContext failed: ${res.status}`);
82
82
  return res.json();
83
83
  }
84
+ async replyToThread(requestId, message) {
85
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/${requestId}/reply`, {
86
+ method: 'POST',
87
+ headers: this.headers(),
88
+ body: JSON.stringify({ message }),
89
+ });
90
+ if (!res.ok)
91
+ throw new Error(`replyToThread failed: ${res.status} ${await res.text()}`);
92
+ return res.json();
93
+ }
94
+ async closeThread(requestId) {
95
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/${requestId}/close`, {
96
+ method: 'POST',
97
+ headers: this.headers(),
98
+ });
99
+ if (!res.ok)
100
+ throw new Error(`closeThread failed: ${res.status} ${await res.text()}`);
101
+ return res.json();
102
+ }
103
+ async getThread(requestId) {
104
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/${requestId}/thread`, {
105
+ headers: this.headers(),
106
+ });
107
+ if (!res.ok)
108
+ throw new Error(`getThread failed: ${res.status}`);
109
+ return res.json();
110
+ }
84
111
  }
@@ -9,6 +9,21 @@ export interface IncomingAnswer {
9
9
  id: number;
10
10
  answer: string;
11
11
  }
12
+ export interface IncomingReply {
13
+ requestId: number;
14
+ reply: {
15
+ id: number;
16
+ agentId: number;
17
+ agentUsername: string;
18
+ message: string;
19
+ createdAt: string;
20
+ };
21
+ }
22
+ export interface ThreadClosed {
23
+ requestId: number;
24
+ closedByAgentId: number;
25
+ closedByUsername: string;
26
+ }
12
27
  export declare class SseListener {
13
28
  private es;
14
29
  private reconnectTimer;
@@ -16,8 +31,10 @@ export declare class SseListener {
16
31
  private token;
17
32
  private onRequest;
18
33
  private onAnswer;
34
+ private onReply;
35
+ private onThreadClosed;
19
36
  private connected;
20
- start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void, onAnswer?: (ans: IncomingAnswer) => void): void;
37
+ start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void, onAnswer?: (ans: IncomingAnswer) => void, onReply?: (data: IncomingReply) => void, onThreadClosed?: (data: ThreadClosed) => void): void;
21
38
  private connect;
22
39
  private scheduleReconnect;
23
40
  stop(): void;
@@ -6,12 +6,16 @@ export class SseListener {
6
6
  token = '';
7
7
  onRequest = null;
8
8
  onAnswer = null;
9
+ onReply = null;
10
+ onThreadClosed = null;
9
11
  connected = false;
10
- start(baseUrl, token, onRequest, onAnswer) {
12
+ start(baseUrl, token, onRequest, onAnswer, onReply, onThreadClosed) {
11
13
  this.baseUrl = baseUrl;
12
14
  this.token = token;
13
15
  this.onRequest = onRequest;
14
16
  this.onAnswer = onAnswer || null;
17
+ this.onReply = onReply || null;
18
+ this.onThreadClosed = onThreadClosed || null;
15
19
  this.connect();
16
20
  }
17
21
  connect() {
@@ -41,6 +45,22 @@ export class SseListener {
41
45
  }
42
46
  catch { }
43
47
  }));
48
+ this.es.addEventListener('reply_added', ((e) => {
49
+ try {
50
+ const payload = JSON.parse(e.data);
51
+ this.onReply?.(payload);
52
+ console.error(`[AskMesh] Reply added to thread #${payload.requestId} by @${payload.reply.agentUsername}`);
53
+ }
54
+ catch { }
55
+ }));
56
+ this.es.addEventListener('thread_closed', ((e) => {
57
+ try {
58
+ const payload = JSON.parse(e.data);
59
+ this.onThreadClosed?.(payload);
60
+ console.error(`[AskMesh] Thread #${payload.requestId} closed by @${payload.closedByUsername}`);
61
+ }
62
+ catch { }
63
+ }));
44
64
  this.es.addEventListener('ping', () => {
45
65
  // Keepalive — confirms connection is alive
46
66
  });
@@ -9,12 +9,15 @@ Actions:
9
9
  - "pending" : voir les questions qu'on t'a posées
10
10
  - "inbox" : voir les réponses aux questions que tu as envoyées
11
11
  - "answer" : répondre à une question en attente
12
+ - "reply" : ajouter une réponse à un thread existant
13
+ - "thread" : voir le thread complet d'une question
14
+ - "close" : clôturer un thread
12
15
  - "context" : partager ton contexte projet avec ta team`, {
13
- action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'context']).describe('Action à effectuer'),
16
+ action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'context']).describe('Action à effectuer'),
14
17
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
15
18
  question: z.string().optional().describe('Question à poser (pour ask)'),
16
- requestId: z.number().optional().describe('ID de la requête (pour answer)'),
17
- message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/context)'),
19
+ requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close)'),
20
+ message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/reply/context)'),
18
21
  }, async ({ action, username, question, requestId, message }) => {
19
22
  switch (action) {
20
23
  case 'ask': {
@@ -29,13 +32,22 @@ Actions:
29
32
  await new Promise((r) => setTimeout(r, 3000));
30
33
  try {
31
34
  const req = await client.getRequest(reqId);
35
+ if (req.status === 'active' || req.status === 'closed') {
36
+ // Fetch thread to get the first reply
37
+ const thread = await client.getThread(reqId);
38
+ if (thread.replies.length > 0) {
39
+ const firstReply = thread.replies[0];
40
+ return text(`@${firstReply.agentUsername} a répondu :\n\n${firstReply.message}${thread.replies.length > 1 ? `\n\n(+${thread.replies.length - 1} autres réponses — utilise action "thread" avec requestId ${reqId} pour voir le thread complet)` : ''}`);
41
+ }
42
+ }
43
+ // Backward compat: check old answer field
32
44
  if (req.status === 'answered' && req.answer) {
33
45
  return text(`@${target} a répondu :\n\n${req.answer}`);
34
46
  }
35
47
  }
36
48
  catch { }
37
49
  }
38
- return text(`Question envoyée à @${target} (#${reqId}). Pas de réponse dans les 60s — utilise "pending" pour vérifier plus tard.`);
50
+ return text(`Question envoyée à @${target} (#${reqId}). Pas de réponse dans les 60s — utilise "thread" avec requestId ${reqId} pour vérifier plus tard.`);
39
51
  }
40
52
  case 'list': {
41
53
  const { agents } = await client.listAgents();
@@ -67,8 +79,8 @@ Actions:
67
79
  if (requests.length === 0)
68
80
  return text('Aucune question envoyée.');
69
81
  const lines = requests.map((r) => {
70
- const status = r.status === 'answered' ? '✅' : '⏳';
71
- let line = `${status} #${r.id} → @${r.toUsername}: "${r.question}"`;
82
+ const icon = r.status === 'closed' ? '✅' : r.status === 'active' ? '💬' : r.status === 'answered' ? '✅' : '⏳';
83
+ let line = `${icon} #${r.id} → @${r.toUsername}: "${r.question}" [${r.status}]`;
72
84
  if (r.answer)
73
85
  line += `\n Réponse: ${r.answer}`;
74
86
  return line;
@@ -82,6 +94,35 @@ Actions:
82
94
  const result = await client.answerRequest(requestId, message);
83
95
  return text(`Réponse envoyée pour la requête #${result.id}.`);
84
96
  }
97
+ case 'reply': {
98
+ if (!requestId || !message) {
99
+ return text("Paramètres requis : requestId et message");
100
+ }
101
+ const result = await client.replyToThread(requestId, message);
102
+ return text(`Reply ajouté au thread #${result.requestId}.`);
103
+ }
104
+ case 'thread': {
105
+ if (!requestId) {
106
+ return text("Paramètre requis : requestId");
107
+ }
108
+ const thread = await client.getThread(requestId);
109
+ const lines = [
110
+ `Thread #${thread.id} [${thread.status}]`,
111
+ `@${thread.fromUsername} → @${thread.toUsername}: "${thread.question}"`,
112
+ '',
113
+ ...thread.replies.map((r) => ` @${r.agentUsername}: ${r.message} (${r.createdAt})`),
114
+ ];
115
+ if (thread.replies.length === 0)
116
+ lines.push(' (aucune réponse)');
117
+ return text(lines.join('\n'));
118
+ }
119
+ case 'close': {
120
+ if (!requestId) {
121
+ return text("Paramètre requis : requestId");
122
+ }
123
+ const result = await client.closeThread(requestId);
124
+ return text(`Thread #${result.id} clôturé.`);
125
+ }
85
126
  case 'context': {
86
127
  if (!message)
87
128
  return text("Paramètre requis : message (le contenu du contexte)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.6.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": {