@askmesh/mcp 0.5.0 → 0.7.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 +40 -5
- package/dist/agent/auto_responder.js +7 -2
- package/dist/client/askmesh_client.d.ts +53 -1
- package/dist/client/askmesh_client.js +55 -2
- package/dist/sse/sse_listener.d.ts +18 -1
- package/dist/sse/sse_listener.js +21 -1
- package/dist/tools/askmesh.js +95 -8
- package/package.json +1 -1
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
|
|
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,
|
|
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. **
|
|
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)
|
|
@@ -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) {
|
|
@@ -52,7 +57,7 @@ export class AutoResponder {
|
|
|
52
57
|
}, {}));
|
|
53
58
|
const answer = result?.content?.text || result?.content?.[0]?.text;
|
|
54
59
|
if (answer) {
|
|
55
|
-
await this.client.
|
|
60
|
+
await this.client.replyToThread(request.id, answer);
|
|
56
61
|
console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
@@ -68,7 +73,7 @@ export class AutoResponder {
|
|
|
68
73
|
const context = readLocalContext();
|
|
69
74
|
const answer = await this.callAnthropicAPI(apiKey, request, context);
|
|
70
75
|
if (answer) {
|
|
71
|
-
await this.client.
|
|
76
|
+
await this.client.replyToThread(request.id, answer);
|
|
72
77
|
console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
@@ -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
|
}>;
|
|
@@ -57,4 +57,56 @@ 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
|
+
}>;
|
|
87
|
+
updateThreadStatus(requestId: number, status: '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
|
+
}>;
|
|
60
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()}`);
|
|
@@ -81,4 +81,57 @@ 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
|
+
}
|
|
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
|
+
}
|
|
84
137
|
}
|
|
@@ -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;
|
package/dist/sse/sse_listener.js
CHANGED
|
@@ -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
|
});
|
package/dist/tools/askmesh.js
CHANGED
|
@@ -9,33 +9,50 @@ 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
|
|
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"
|
|
12
18
|
- "context" : partager ton contexte projet avec ta team`, {
|
|
13
|
-
action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', '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'),
|
|
14
20
|
username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
|
|
15
21
|
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)'),
|
|
18
|
-
|
|
22
|
+
requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
|
|
23
|
+
message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/reply/context)'),
|
|
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 }) => {
|
|
19
27
|
switch (action) {
|
|
20
28
|
case 'ask': {
|
|
21
29
|
if (!username || !question) {
|
|
22
30
|
return text("Paramètres requis : username et question");
|
|
23
31
|
}
|
|
24
32
|
const target = username.replace(/^@/, '');
|
|
25
|
-
const result = await client.askAgent(target, question);
|
|
33
|
+
const result = await client.askAgent(target, question, undefined, parentThreadId);
|
|
26
34
|
const reqId = result.requestId;
|
|
27
35
|
// Poll for answer (max 60s)
|
|
28
36
|
for (let i = 0; i < 20; i++) {
|
|
29
37
|
await new Promise((r) => setTimeout(r, 3000));
|
|
30
38
|
try {
|
|
31
39
|
const req = await client.getRequest(reqId);
|
|
40
|
+
if (req.status === 'active' || req.status === 'closed') {
|
|
41
|
+
// Fetch thread to get the first reply
|
|
42
|
+
const thread = await client.getThread(reqId);
|
|
43
|
+
if (thread.replies.length > 0) {
|
|
44
|
+
const firstReply = thread.replies[0];
|
|
45
|
+
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)` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Backward compat: check old answer field
|
|
32
49
|
if (req.status === 'answered' && req.answer) {
|
|
33
50
|
return text(`@${target} a répondu :\n\n${req.answer}`);
|
|
34
51
|
}
|
|
35
52
|
}
|
|
36
53
|
catch { }
|
|
37
54
|
}
|
|
38
|
-
return text(`Question envoyée à @${target} (#${reqId}). Pas de réponse dans les 60s — utilise "
|
|
55
|
+
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
56
|
}
|
|
40
57
|
case 'list': {
|
|
41
58
|
const { agents } = await client.listAgents();
|
|
@@ -67,8 +84,8 @@ Actions:
|
|
|
67
84
|
if (requests.length === 0)
|
|
68
85
|
return text('Aucune question envoyée.');
|
|
69
86
|
const lines = requests.map((r) => {
|
|
70
|
-
const
|
|
71
|
-
let line = `${
|
|
87
|
+
const icon = r.status === 'closed' ? '✅' : r.status === 'active' ? '💬' : r.status === 'answered' ? '✅' : '⏳';
|
|
88
|
+
let line = `${icon} #${r.id} → @${r.toUsername}: "${r.question}" [${r.status}]`;
|
|
72
89
|
if (r.answer)
|
|
73
90
|
line += `\n Réponse: ${r.answer}`;
|
|
74
91
|
return line;
|
|
@@ -82,6 +99,76 @@ Actions:
|
|
|
82
99
|
const result = await client.answerRequest(requestId, message);
|
|
83
100
|
return text(`Réponse envoyée pour la requête #${result.id}.`);
|
|
84
101
|
}
|
|
102
|
+
case 'reply': {
|
|
103
|
+
if (!requestId || !message) {
|
|
104
|
+
return text("Paramètres requis : requestId et message");
|
|
105
|
+
}
|
|
106
|
+
const result = await client.replyToThread(requestId, message);
|
|
107
|
+
return text(`Reply ajouté au thread #${result.requestId}.`);
|
|
108
|
+
}
|
|
109
|
+
case 'thread': {
|
|
110
|
+
if (!requestId) {
|
|
111
|
+
return text("Paramètre requis : requestId");
|
|
112
|
+
}
|
|
113
|
+
const thread = await client.getThread(requestId);
|
|
114
|
+
const lines = [
|
|
115
|
+
`Thread #${thread.id} [${thread.status}]`,
|
|
116
|
+
`@${thread.fromUsername} → @${thread.toUsername}: "${thread.question}"`,
|
|
117
|
+
'',
|
|
118
|
+
...thread.replies.map((r) => ` @${r.agentUsername}: ${r.message} (${r.createdAt})`),
|
|
119
|
+
];
|
|
120
|
+
if (thread.replies.length === 0)
|
|
121
|
+
lines.push(' (aucune réponse)');
|
|
122
|
+
return text(lines.join('\n'));
|
|
123
|
+
}
|
|
124
|
+
case 'close': {
|
|
125
|
+
if (!requestId) {
|
|
126
|
+
return text("Paramètre requis : requestId");
|
|
127
|
+
}
|
|
128
|
+
const result = await client.closeThread(requestId);
|
|
129
|
+
return text(`Thread #${result.id} clôturé.`);
|
|
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
|
+
}
|
|
85
172
|
case 'context': {
|
|
86
173
|
if (!message)
|
|
87
174
|
return text("Paramètre requis : message (le contenu du contexte)");
|