@askmesh/mcp 0.6.0 → 0.7.1

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.
@@ -17,6 +17,11 @@ export class AutoResponder {
17
17
  }
18
18
  async handleRequest(request) {
19
19
  console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
20
+ // Mark thread as in_progress before processing
21
+ try {
22
+ await this.client.updateThreadStatus(request.id, 'in_progress');
23
+ }
24
+ catch { }
20
25
  // Strategy 1: MCP sampling — asks the active Claude Code to respond
21
26
  // Uses the user's existing Claude subscription, no API key needed
22
27
  if (this.mcpServer) {
@@ -57,11 +62,11 @@ export class AutoResponder {
57
62
  return;
58
63
  }
59
64
  }
60
- catch {
61
- console.error('[AskMesh] MCP sampling not available');
65
+ catch (err) {
66
+ console.error('[AskMesh] MCP sampling failed:', err instanceof Error ? err.message : err);
62
67
  }
63
68
  }
64
- // Strategy 2: Anthropic API fallback (optional — for standalone/server mode)
69
+ // Strategy 2: Anthropic API fallback (optional)
65
70
  const apiKey = process.env.ANTHROPIC_API_KEY;
66
71
  if (apiKey) {
67
72
  try {
@@ -77,8 +82,23 @@ export class AutoResponder {
77
82
  console.error('[AskMesh] Anthropic API failed:', err);
78
83
  }
79
84
  }
80
- // Strategy 3: Manual question stays pending
81
- console.error(`[AskMesh] Question #${request.id} queued use askmesh(action:"pending") to respond`);
85
+ // Strategy 3: Notify Claude Code and revert to pending
86
+ // Revert status so the message can be answered manually
87
+ try {
88
+ await this.client.updateThreadStatus(request.id, 'pending');
89
+ }
90
+ catch { }
91
+ // Send logging notification to Claude Code
92
+ if (this.mcpServer) {
93
+ try {
94
+ await this.mcpServer.sendLoggingMessage({
95
+ level: 'warning',
96
+ data: `📩 AskMesh — @${request.fromUsername} asks: "${request.question.slice(0, 200)}" → Use askmesh(action:"pending") to see and reply.`,
97
+ });
98
+ }
99
+ catch { }
100
+ }
101
+ console.error(`[AskMesh] Question #${request.id} from @${request.fromUsername} — auto-response failed, use askmesh(action:"pending") to respond`);
82
102
  }
83
103
  async callAnthropicAPI(apiKey, request, context) {
84
104
  const model = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
@@ -3,7 +3,7 @@ export declare class AskMeshClient {
3
3
  private token;
4
4
  constructor(baseUrl: string, token: string);
5
5
  private headers;
6
- askAgent(toUsername: string, question: string, context?: string): Promise<{
6
+ askAgent(toUsername: string, question: string, context?: string, parentThreadId?: number): Promise<{
7
7
  requestId: number;
8
8
  status: string;
9
9
  }>;
@@ -84,4 +84,29 @@ export declare class AskMeshClient {
84
84
  createdAt: string;
85
85
  }>;
86
86
  }>;
87
+ updateThreadStatus(requestId: number, status: 'pending' | 'in_progress' | 'delegated'): Promise<{
88
+ id: number;
89
+ status: string;
90
+ }>;
91
+ getMyThreads(): Promise<{
92
+ threads: Array<{
93
+ id: number;
94
+ fromUsername: string;
95
+ toUsername: string;
96
+ question: string;
97
+ status: string;
98
+ replyCount: number;
99
+ parentThreadId: number | null;
100
+ createdAt: string;
101
+ updatedAt: string;
102
+ }>;
103
+ }>;
104
+ getTeamBoard(teamId: number): Promise<{
105
+ columns: {
106
+ pending: any[];
107
+ in_progress: any[];
108
+ active: any[];
109
+ closed: any[];
110
+ };
111
+ }>;
87
112
  }
@@ -11,11 +11,11 @@ export class AskMeshClient {
11
11
  'Content-Type': 'application/json',
12
12
  };
13
13
  }
14
- async askAgent(toUsername, question, context) {
14
+ async askAgent(toUsername, question, context, parentThreadId) {
15
15
  const res = await fetch(`${this.baseUrl}/api/v1/requests`, {
16
16
  method: 'POST',
17
17
  headers: this.headers(),
18
- body: JSON.stringify({ toUsername, question, context }),
18
+ body: JSON.stringify({ toUsername, question, context, parentThreadId }),
19
19
  });
20
20
  if (!res.ok)
21
21
  throw new Error(`askAgent failed: ${res.status} ${await res.text()}`);
@@ -108,4 +108,30 @@ export class AskMeshClient {
108
108
  throw new Error(`getThread failed: ${res.status}`);
109
109
  return res.json();
110
110
  }
111
+ async updateThreadStatus(requestId, status) {
112
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/${requestId}/status`, {
113
+ method: 'PUT',
114
+ headers: this.headers(),
115
+ body: JSON.stringify({ status }),
116
+ });
117
+ if (!res.ok)
118
+ throw new Error(`updateThreadStatus failed: ${res.status} ${await res.text()}`);
119
+ return res.json();
120
+ }
121
+ async getMyThreads() {
122
+ const res = await fetch(`${this.baseUrl}/api/v1/requests/my-threads`, {
123
+ headers: this.headers(),
124
+ });
125
+ if (!res.ok)
126
+ throw new Error(`getMyThreads failed: ${res.status}`);
127
+ return res.json();
128
+ }
129
+ async getTeamBoard(teamId) {
130
+ const res = await fetch(`${this.baseUrl}/api/v1/teams/${teamId}/board`, {
131
+ headers: this.headers(),
132
+ });
133
+ if (!res.ok)
134
+ throw new Error(`getTeamBoard failed: ${res.status}`);
135
+ return res.json();
136
+ }
111
137
  }
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ const client = new AskMeshClient(URL, TOKEN);
15
15
  const autoResponder = new AutoResponder(client);
16
16
  const server = new McpServer({
17
17
  name: 'askmesh',
18
- version: '0.3.0',
18
+ version: '0.7.0',
19
19
  });
20
20
  // Single unified tool
21
21
  registerAskMesh(server, client);
@@ -12,20 +12,25 @@ Actions:
12
12
  - "reply" : ajouter une réponse à un thread existant
13
13
  - "thread" : voir le thread complet d'une question
14
14
  - "close" : clôturer un thread
15
+ - "my-threads" : voir tous tes threads actifs
16
+ - "board" : voir le kanban board d'une team
17
+ - "progress" : marquer un thread comme "en cours de traitement"
15
18
  - "context" : partager ton contexte projet avec ta team`, {
16
- action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'context']).describe('Action à effectuer'),
19
+ action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'context']).describe('Action à effectuer'),
17
20
  username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
18
21
  question: z.string().optional().describe('Question à poser (pour ask)'),
19
- requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close)'),
22
+ requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
20
23
  message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/reply/context)'),
21
- }, async ({ action, username, question, requestId, message }) => {
24
+ parentThreadId: z.number().optional().describe('ID du thread parent (pour ask, lie les threads entre eux)'),
25
+ teamId: z.number().optional().describe('ID de la team (pour board)'),
26
+ }, async ({ action, username, question, requestId, message, parentThreadId, teamId }) => {
22
27
  switch (action) {
23
28
  case 'ask': {
24
29
  if (!username || !question) {
25
30
  return text("Paramètres requis : username et question");
26
31
  }
27
32
  const target = username.replace(/^@/, '');
28
- const result = await client.askAgent(target, question);
33
+ const result = await client.askAgent(target, question, undefined, parentThreadId);
29
34
  const reqId = result.requestId;
30
35
  // Poll for answer (max 60s)
31
36
  for (let i = 0; i < 20; i++) {
@@ -123,6 +128,47 @@ Actions:
123
128
  const result = await client.closeThread(requestId);
124
129
  return text(`Thread #${result.id} clôturé.`);
125
130
  }
131
+ case 'my-threads': {
132
+ const { threads } = await client.getMyThreads();
133
+ if (threads.length === 0)
134
+ return text('Aucun thread actif.');
135
+ const statusIcons = {
136
+ pending: '⏳', in_progress: '🔧', active: '💬', delegated: '📤', closed: '✅',
137
+ };
138
+ const lines = threads.map((t) => {
139
+ const icon = statusIcons[t.status] || '❓';
140
+ let line = `${icon} #${t.id} [${t.status}] @${t.fromUsername} → @${t.toUsername}: "${t.question}"`;
141
+ if (t.replyCount > 0)
142
+ line += ` (${t.replyCount} replies)`;
143
+ if (t.parentThreadId)
144
+ line += ` [lié au thread #${t.parentThreadId}]`;
145
+ return line;
146
+ });
147
+ return text(`Mes threads:\n\n${lines.join('\n')}`);
148
+ }
149
+ case 'board': {
150
+ if (!teamId)
151
+ return text("Paramètre requis : teamId");
152
+ const board = await client.getTeamBoard(teamId);
153
+ const formatCol = (name, items) => {
154
+ if (items.length === 0)
155
+ return `**${name}**: (vide)`;
156
+ return `**${name}** (${items.length}):\n${items.map((t) => ` #${t.id} @${t.fromUsername}→@${t.toUsername}: "${t.question}" [${t.replyCount} replies]`).join('\n')}`;
157
+ };
158
+ const lines = [
159
+ formatCol('Pending', board.columns.pending),
160
+ formatCol('In Progress', board.columns.in_progress),
161
+ formatCol('Active', board.columns.active),
162
+ formatCol('Closed (24h)', board.columns.closed),
163
+ ];
164
+ return text(`Team Board:\n\n${lines.join('\n\n')}`);
165
+ }
166
+ case 'progress': {
167
+ if (!requestId)
168
+ return text("Paramètre requis : requestId");
169
+ const result = await client.updateThreadStatus(requestId, 'in_progress');
170
+ return text(`Thread #${result.id} marqué comme en cours de traitement.`);
171
+ }
126
172
  case 'context': {
127
173
  if (!message)
128
174
  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.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "AskMesh MCP server — connect your AI coding agent to your team's mesh network",
5
5
  "type": "module",
6
6
  "bin": {