adhdev 0.9.58 → 0.9.60
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/cli/index.js +541 -468
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +75 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/vendor/mcp-server/index.js +835 -0
- package/vendor/mcp-server/index.js.map +1 -0
- package/vendor/mcp-server/package.json +7 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
6
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
8
|
+
|
|
9
|
+
// src/transports/local.ts
|
|
10
|
+
var DEFAULT_PORT = 3847;
|
|
11
|
+
var LocalTransport = class {
|
|
12
|
+
baseUrl;
|
|
13
|
+
authHeader;
|
|
14
|
+
constructor(opts = {}) {
|
|
15
|
+
this.baseUrl = `http://localhost:${opts.port ?? DEFAULT_PORT}`;
|
|
16
|
+
this.authHeader = opts.password ? `Bearer ${opts.password}` : null;
|
|
17
|
+
}
|
|
18
|
+
headers() {
|
|
19
|
+
const h = { "Content-Type": "application/json" };
|
|
20
|
+
if (this.authHeader) h["Authorization"] = this.authHeader;
|
|
21
|
+
return h;
|
|
22
|
+
}
|
|
23
|
+
async getStatus() {
|
|
24
|
+
const res = await fetch(`${this.baseUrl}/api/v1/status`, { headers: this.headers() });
|
|
25
|
+
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
async command(type, args = {}) {
|
|
29
|
+
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: this.headers(),
|
|
32
|
+
body: JSON.stringify({ type, ...args })
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => res.statusText);
|
|
36
|
+
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
37
|
+
}
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
async ping() {
|
|
41
|
+
try {
|
|
42
|
+
await this.getStatus();
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/transports/cloud.ts
|
|
51
|
+
var DEFAULT_BASE_URL = "https://api.adhf.dev";
|
|
52
|
+
var CloudTransport = class {
|
|
53
|
+
baseUrl;
|
|
54
|
+
apiKey;
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
this.apiKey = opts.apiKey;
|
|
57
|
+
this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
58
|
+
}
|
|
59
|
+
headers() {
|
|
60
|
+
return {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async listDaemons() {
|
|
66
|
+
const res = await fetch(`${this.baseUrl}/api/v1/daemons`, { headers: this.headers() });
|
|
67
|
+
if (!res.ok) throw new Error(`List daemons failed: ${res.status}`);
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
async getStatus(targetId) {
|
|
71
|
+
const res = await fetch(
|
|
72
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/status`,
|
|
73
|
+
{ headers: this.headers() }
|
|
74
|
+
);
|
|
75
|
+
if (!res.ok) throw new Error(`Status failed: ${res.status}`);
|
|
76
|
+
return res.json();
|
|
77
|
+
}
|
|
78
|
+
/** Get all sessions for a daemon (returns CompactSessionEntry[]). */
|
|
79
|
+
async getDaemonStatus(daemonId) {
|
|
80
|
+
const res = await fetch(
|
|
81
|
+
`${this.baseUrl}/api/v1/daemons/${encodeURIComponent(daemonId)}/status`,
|
|
82
|
+
{ headers: this.headers() }
|
|
83
|
+
);
|
|
84
|
+
if (!res.ok) throw new Error(`Daemon status failed: ${res.status}`);
|
|
85
|
+
return res.json();
|
|
86
|
+
}
|
|
87
|
+
async readChat(targetId, opts = {}) {
|
|
88
|
+
const params = new URLSearchParams();
|
|
89
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
90
|
+
if (opts.sessionId) params.set("sessionId", opts.sessionId);
|
|
91
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
92
|
+
const res = await fetch(
|
|
93
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat${qs}`,
|
|
94
|
+
{ headers: this.headers() }
|
|
95
|
+
);
|
|
96
|
+
if (!res.ok) throw new Error(`Read chat failed: ${res.status}`);
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
async sendChat(targetId, message, opts = {}) {
|
|
100
|
+
const res = await fetch(
|
|
101
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat`,
|
|
102
|
+
{
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: this.headers(),
|
|
105
|
+
body: JSON.stringify({ message, ...opts })
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
if (!res.ok) throw new Error(`Send chat failed: ${res.status}`);
|
|
109
|
+
return res.json();
|
|
110
|
+
}
|
|
111
|
+
async approve(targetId, action, agentType) {
|
|
112
|
+
const res = await fetch(
|
|
113
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/approve`,
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: this.headers(),
|
|
117
|
+
body: JSON.stringify({ action, ...agentType ? { agentType } : {} })
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
121
|
+
return res.json();
|
|
122
|
+
}
|
|
123
|
+
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
124
|
+
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
125
|
+
const res = await fetch(
|
|
126
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
127
|
+
{ headers: this.headers() }
|
|
128
|
+
);
|
|
129
|
+
if (!res.ok) throw new Error(`Git status failed: ${res.status}`);
|
|
130
|
+
return res.json();
|
|
131
|
+
}
|
|
132
|
+
async launch(daemonId, opts) {
|
|
133
|
+
const res = await fetch(
|
|
134
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/launch`,
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: this.headers(),
|
|
138
|
+
body: JSON.stringify(opts)
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
if (!res.ok) throw new Error(`Launch failed: ${res.status}`);
|
|
142
|
+
return res.json();
|
|
143
|
+
}
|
|
144
|
+
async ping() {
|
|
145
|
+
try {
|
|
146
|
+
await this.listDaemons();
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/tools/list-sessions.ts
|
|
155
|
+
var FORMAT_PROP = {
|
|
156
|
+
format: {
|
|
157
|
+
type: "string",
|
|
158
|
+
enum: ["text", "json"],
|
|
159
|
+
description: "Output format: 'text' (default, human-readable) or 'json' (structured, for programmatic use)."
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var LIST_SESSIONS_TOOL = {
|
|
163
|
+
name: "list_sessions",
|
|
164
|
+
description: "List all currently connected IDE and CLI agent sessions on the local machine.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
...FORMAT_PROP
|
|
169
|
+
},
|
|
170
|
+
required: []
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function listSessions(transport, args = {}) {
|
|
174
|
+
const asJson = args.format === "json";
|
|
175
|
+
if ("getStatus" in transport) {
|
|
176
|
+
const status = await transport.getStatus();
|
|
177
|
+
const sessions = status?.sessions ?? [];
|
|
178
|
+
if (asJson) {
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
sessions: sessions.map((s) => ({
|
|
181
|
+
id: s.id,
|
|
182
|
+
type: s.providerType ?? s.type ?? "unknown",
|
|
183
|
+
label: s.label ?? null,
|
|
184
|
+
status: s.status ?? s.agentStatus ?? null,
|
|
185
|
+
workspace: s.workspace ?? null
|
|
186
|
+
}))
|
|
187
|
+
}, null, 2);
|
|
188
|
+
}
|
|
189
|
+
if (sessions.length === 0) return "No active sessions.";
|
|
190
|
+
const lines2 = sessions.map((s) => {
|
|
191
|
+
const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
|
|
192
|
+
if (s.label) parts.push(`label: ${s.label}`);
|
|
193
|
+
if (s.agentStatus) parts.push(`status: ${s.agentStatus}`);
|
|
194
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
195
|
+
return parts.join(", ");
|
|
196
|
+
});
|
|
197
|
+
return `Sessions (${sessions.length}):
|
|
198
|
+
${lines2.join("\n")}`;
|
|
199
|
+
}
|
|
200
|
+
const data = await transport.listDaemons();
|
|
201
|
+
const daemons = data?.daemons ?? data ?? [];
|
|
202
|
+
if (asJson) {
|
|
203
|
+
const sessions = [];
|
|
204
|
+
for (const d of daemons) {
|
|
205
|
+
for (const s of d.sessions ?? []) {
|
|
206
|
+
sessions.push({
|
|
207
|
+
daemon_id: d.id,
|
|
208
|
+
id: s.id,
|
|
209
|
+
type: s.providerType ?? "unknown",
|
|
210
|
+
status: s.status ?? s.agentStatus ?? null,
|
|
211
|
+
workspace: s.workspace ?? null
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return JSON.stringify({ sessions }, null, 2);
|
|
216
|
+
}
|
|
217
|
+
if (daemons.length === 0) return "No connected daemons.";
|
|
218
|
+
const lines = [];
|
|
219
|
+
for (const d of daemons) {
|
|
220
|
+
const sessions = d.sessions ?? [];
|
|
221
|
+
for (const s of sessions) {
|
|
222
|
+
lines.push(
|
|
223
|
+
`daemon: ${d.id}, session: ${s.id}, type: ${s.providerType ?? "unknown"}${s.agentStatus ? `, status: ${s.agentStatus}` : ""}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (sessions.length === 0) lines.push(`daemon: ${d.id} (no sessions)`);
|
|
227
|
+
}
|
|
228
|
+
return lines.length > 0 ? `Sessions:
|
|
229
|
+
${lines.join("\n")}` : "No active sessions.";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/tools/read-chat.ts
|
|
233
|
+
var READ_CHAT_TOOL = {
|
|
234
|
+
name: "read_chat",
|
|
235
|
+
description: "Read the current chat conversation from an IDE agent session. Returns recent messages.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
session_id: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
242
|
+
},
|
|
243
|
+
limit: {
|
|
244
|
+
type: "number",
|
|
245
|
+
description: "Max messages to return (default: 50)."
|
|
246
|
+
},
|
|
247
|
+
daemon_id: {
|
|
248
|
+
type: "string",
|
|
249
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
250
|
+
},
|
|
251
|
+
...FORMAT_PROP
|
|
252
|
+
},
|
|
253
|
+
required: []
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
async function readChat(transport, args) {
|
|
257
|
+
const limit = args.limit ?? 50;
|
|
258
|
+
if ("command" in transport) {
|
|
259
|
+
const result2 = await transport.command("read_chat", {
|
|
260
|
+
...args.session_id ? { targetSessionId: args.session_id } : {},
|
|
261
|
+
limit
|
|
262
|
+
});
|
|
263
|
+
return formatChatResult(result2, args.session_id, args.format);
|
|
264
|
+
}
|
|
265
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
266
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
267
|
+
const result = await transport.readChat(targetId, { limit, sessionId: args.session_id });
|
|
268
|
+
return formatChatResult(result, args.session_id, args.format);
|
|
269
|
+
}
|
|
270
|
+
function formatChatResult(result, sessionId, format) {
|
|
271
|
+
if (!result?.success && result?.error) {
|
|
272
|
+
if (format === "json") return JSON.stringify({ error: result.error, messages: [] }, null, 2);
|
|
273
|
+
return `Error: ${result.error}`;
|
|
274
|
+
}
|
|
275
|
+
const messages = result?.messages ?? result?.data?.messages ?? [];
|
|
276
|
+
if (format === "json") {
|
|
277
|
+
return JSON.stringify({
|
|
278
|
+
session_id: sessionId ?? null,
|
|
279
|
+
messages: messages.slice(-50).map((m) => ({
|
|
280
|
+
role: m.role,
|
|
281
|
+
kind: m.kind ?? null,
|
|
282
|
+
content: typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => typeof p === "string" ? p : p?.text ?? "").join("") : "",
|
|
283
|
+
timestamp: m.timestamp ?? null
|
|
284
|
+
}))
|
|
285
|
+
}, null, 2);
|
|
286
|
+
}
|
|
287
|
+
if (messages.length === 0) return "No messages in chat.";
|
|
288
|
+
const lines = messages.slice(-50).map((m) => {
|
|
289
|
+
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
290
|
+
const content = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => typeof p === "string" ? p : p?.text ?? "").join("") : "";
|
|
291
|
+
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
292
|
+
return `[${role}] ${truncated}`;
|
|
293
|
+
});
|
|
294
|
+
return lines.join("\n\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/tools/send-chat.ts
|
|
298
|
+
var SEND_CHAT_TOOL = {
|
|
299
|
+
name: "send_chat",
|
|
300
|
+
description: "Send a message to an IDE agent session.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
message: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "The message to send to the agent."
|
|
307
|
+
},
|
|
308
|
+
session_id: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
311
|
+
},
|
|
312
|
+
daemon_id: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
required: ["message"]
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
async function sendChat(transport, args) {
|
|
321
|
+
if (!args.message?.trim()) throw new Error("message is required");
|
|
322
|
+
if ("command" in transport) {
|
|
323
|
+
const result2 = await transport.command("send_chat", {
|
|
324
|
+
message: args.message,
|
|
325
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
326
|
+
});
|
|
327
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "send_chat failed"}`;
|
|
328
|
+
return "Message sent.";
|
|
329
|
+
}
|
|
330
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
331
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
332
|
+
const result = await transport.sendChat(targetId, args.message, {
|
|
333
|
+
...args.session_id ? { sessionId: args.session_id } : {}
|
|
334
|
+
});
|
|
335
|
+
if (result?.success === false) return `Error: ${result.error ?? "send_chat failed"}`;
|
|
336
|
+
return "Message sent.";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/tools/approve.ts
|
|
340
|
+
var APPROVE_TOOL = {
|
|
341
|
+
name: "approve",
|
|
342
|
+
description: "Approve or reject a pending agent action (e.g. file write, command execution).",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {
|
|
346
|
+
action: {
|
|
347
|
+
type: "string",
|
|
348
|
+
enum: ["approve", "reject"],
|
|
349
|
+
description: "Whether to approve or reject the pending action."
|
|
350
|
+
},
|
|
351
|
+
session_id: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Target session ID. Omit to use the active session."
|
|
354
|
+
},
|
|
355
|
+
daemon_id: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Daemon ID (cloud mode only)."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
required: ["action"]
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
async function approve(transport, args) {
|
|
364
|
+
const action = args.action === "reject" ? "reject" : "approve";
|
|
365
|
+
if ("command" in transport) {
|
|
366
|
+
const result2 = await transport.command("resolve_action", {
|
|
367
|
+
action,
|
|
368
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
369
|
+
});
|
|
370
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "resolve_action failed"}`;
|
|
371
|
+
return `Action ${action}d.`;
|
|
372
|
+
}
|
|
373
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
374
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
375
|
+
const result = await transport.approve(targetId, action);
|
|
376
|
+
if (result?.success === false) return `Error: ${result.error ?? "approve failed"}`;
|
|
377
|
+
return `Action ${action}d.`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/tools/screenshot.ts
|
|
381
|
+
var SCREENSHOT_TOOL = {
|
|
382
|
+
name: "screenshot",
|
|
383
|
+
description: "Capture a screenshot of the current IDE window. Returns the image.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
session_id: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Target session ID. Omit to use the active session."
|
|
390
|
+
},
|
|
391
|
+
daemon_id: {
|
|
392
|
+
type: "string",
|
|
393
|
+
description: "Daemon ID (cloud mode only)."
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
required: []
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
async function screenshot(transport, args) {
|
|
400
|
+
let result;
|
|
401
|
+
if ("command" in transport) {
|
|
402
|
+
result = await transport.command("screenshot", {
|
|
403
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
return { type: "text", text: "Screenshots are not available in cloud mode. Run adhdev mcp in local mode (requires standalone daemon)." };
|
|
407
|
+
}
|
|
408
|
+
if (result?.success === false) {
|
|
409
|
+
return { type: "text", text: `Error: ${result.error ?? "screenshot failed"}` };
|
|
410
|
+
}
|
|
411
|
+
const b64 = result?.base64 ?? result?.screenshot ?? result?.result;
|
|
412
|
+
if (!b64) {
|
|
413
|
+
return { type: "text", text: "Screenshot captured but no image data returned." };
|
|
414
|
+
}
|
|
415
|
+
const mimeType = result?.format === "png" ? "image/png" : "image/webp";
|
|
416
|
+
return { type: "image", data: b64, mimeType };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/tools/git-status.ts
|
|
420
|
+
var GIT_STATUS_TOOL = {
|
|
421
|
+
name: "git_status",
|
|
422
|
+
description: "Get git repository status for a workspace on the daemon machine.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: "object",
|
|
425
|
+
properties: {
|
|
426
|
+
workspace: {
|
|
427
|
+
type: "string",
|
|
428
|
+
description: "Absolute path to the workspace/repository directory."
|
|
429
|
+
},
|
|
430
|
+
include_diff: {
|
|
431
|
+
type: "boolean",
|
|
432
|
+
description: "Include changed file list (default: true)."
|
|
433
|
+
},
|
|
434
|
+
daemon_id: {
|
|
435
|
+
type: "string",
|
|
436
|
+
description: "Daemon ID (cloud mode only)."
|
|
437
|
+
},
|
|
438
|
+
...FORMAT_PROP
|
|
439
|
+
},
|
|
440
|
+
required: ["workspace"]
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
async function gitStatus(transport, args) {
|
|
444
|
+
let status;
|
|
445
|
+
let diffSummary;
|
|
446
|
+
if ("command" in transport) {
|
|
447
|
+
const statusResult = await transport.command("git_status", {
|
|
448
|
+
workspace: args.workspace
|
|
449
|
+
});
|
|
450
|
+
status = statusResult?.status ?? statusResult;
|
|
451
|
+
if (args.include_diff !== false) {
|
|
452
|
+
const diffResult = await transport.command("git_diff_summary", {
|
|
453
|
+
workspace: args.workspace
|
|
454
|
+
});
|
|
455
|
+
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
459
|
+
const result = await transport.gitStatus(
|
|
460
|
+
args.daemon_id,
|
|
461
|
+
args.workspace,
|
|
462
|
+
args.include_diff !== false
|
|
463
|
+
);
|
|
464
|
+
if (result?.error) {
|
|
465
|
+
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
466
|
+
return `Error: ${result.error}`;
|
|
467
|
+
}
|
|
468
|
+
status = result?.status;
|
|
469
|
+
diffSummary = result?.diff;
|
|
470
|
+
}
|
|
471
|
+
if (status?.success === false || status?.reason) {
|
|
472
|
+
const msg = status?.error ?? status?.reason ?? "unknown";
|
|
473
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
474
|
+
return `Git error: ${msg}`;
|
|
475
|
+
}
|
|
476
|
+
if (!status?.isGitRepo) {
|
|
477
|
+
if (args.format === "json") return JSON.stringify({ error: `Not a git repository: ${args.workspace}` }, null, 2);
|
|
478
|
+
return `Not a git repository: ${args.workspace}`;
|
|
479
|
+
}
|
|
480
|
+
if (args.format === "json") {
|
|
481
|
+
const files = diffSummary?.files?.map((f) => ({
|
|
482
|
+
path: f.path,
|
|
483
|
+
old_path: f.oldPath ?? null,
|
|
484
|
+
status: f.status ?? "M",
|
|
485
|
+
insertions: f.insertions ?? 0,
|
|
486
|
+
deletions: f.deletions ?? 0
|
|
487
|
+
})) ?? [];
|
|
488
|
+
return JSON.stringify({
|
|
489
|
+
branch: status.branch ?? null,
|
|
490
|
+
head_commit: status.headCommit ?? null,
|
|
491
|
+
head_message: status.headMessage ?? null,
|
|
492
|
+
ahead: status.ahead ?? 0,
|
|
493
|
+
behind: status.behind ?? 0,
|
|
494
|
+
staged: status.staged ?? 0,
|
|
495
|
+
modified: status.modified ?? 0,
|
|
496
|
+
untracked: status.untracked ?? 0,
|
|
497
|
+
deleted: status.deleted ?? 0,
|
|
498
|
+
stash_count: status.stashCount ?? 0,
|
|
499
|
+
has_conflicts: status.hasConflicts ?? false,
|
|
500
|
+
dirty: status.dirty ?? false,
|
|
501
|
+
changed_files: files,
|
|
502
|
+
total_insertions: diffSummary?.totalInsertions ?? 0,
|
|
503
|
+
total_deletions: diffSummary?.totalDeletions ?? 0
|
|
504
|
+
}, null, 2);
|
|
505
|
+
}
|
|
506
|
+
const lines = [];
|
|
507
|
+
if (status.branch) lines.push(`Branch: ${status.branch}`);
|
|
508
|
+
if (status.headCommit) {
|
|
509
|
+
lines.push(`HEAD: ${status.headCommit.slice(0, 7)}${status.headMessage ? ` \u2014 ${status.headMessage.slice(0, 80)}` : ""}`);
|
|
510
|
+
}
|
|
511
|
+
if (status.ahead > 0) lines.push(`Ahead: ${status.ahead}`);
|
|
512
|
+
if (status.behind > 0) lines.push(`Behind: ${status.behind}`);
|
|
513
|
+
if (status.staged > 0) lines.push(`Staged: ${status.staged}`);
|
|
514
|
+
if (status.modified > 0) lines.push(`Modified: ${status.modified}`);
|
|
515
|
+
if (status.untracked > 0) lines.push(`Untracked: ${status.untracked}`);
|
|
516
|
+
if (status.deleted > 0) lines.push(`Deleted: ${status.deleted}`);
|
|
517
|
+
if (status.stashCount > 0) lines.push(`Stashes: ${status.stashCount}`);
|
|
518
|
+
if (status.hasConflicts) lines.push("Conflicts: YES");
|
|
519
|
+
if (!status.dirty) lines.push("Working tree: clean");
|
|
520
|
+
if (diffSummary?.files?.length > 0) {
|
|
521
|
+
lines.push("");
|
|
522
|
+
lines.push(`Changed files (${diffSummary.files.length}):`);
|
|
523
|
+
for (const f of diffSummary.files.slice(0, 20)) {
|
|
524
|
+
lines.push(` ${f.status ?? "M"} ${f.path}${f.oldPath ? ` (was ${f.oldPath})` : ""}${f.insertions || f.deletions ? ` +${f.insertions ?? 0}/-${f.deletions ?? 0}` : ""}`);
|
|
525
|
+
}
|
|
526
|
+
if (diffSummary.files.length > 20) lines.push(` \u2026 and ${diffSummary.files.length - 20} more`);
|
|
527
|
+
if (diffSummary.totalInsertions || diffSummary.totalDeletions) {
|
|
528
|
+
lines.push(`Total: +${diffSummary.totalInsertions ?? 0}/-${diffSummary.totalDeletions ?? 0}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return lines.join("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/tools/launch-session.ts
|
|
535
|
+
var LAUNCH_SESSION_TOOL = {
|
|
536
|
+
name: "launch_session",
|
|
537
|
+
description: "Launch a new agent session on the daemon. Supports CLI agents (e.g. hermes-cli, claude-cli, gemini-cli), ACP agents (e.g. claude-acp), and IDEs (e.g. cursor, vscode).",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
type: {
|
|
542
|
+
type: "string",
|
|
543
|
+
description: "Provider type to launch. CLI examples: hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode."
|
|
544
|
+
},
|
|
545
|
+
workspace: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: "Working directory for the session. Defaults to the daemon default workspace."
|
|
548
|
+
},
|
|
549
|
+
model: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Model override for ACP agents (e.g. claude-opus-4-7)."
|
|
552
|
+
},
|
|
553
|
+
daemon_id: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Daemon ID (cloud mode only). Required in cloud mode."
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
required: ["type"]
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
async function launchSession(transport, args) {
|
|
562
|
+
if ("command" in transport) {
|
|
563
|
+
const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
|
|
564
|
+
const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
|
|
565
|
+
const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
|
|
566
|
+
const result2 = await transport.command(commandType, payload);
|
|
567
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "launch failed"}`;
|
|
568
|
+
const id2 = result2?.id ?? result2?.sessionId;
|
|
569
|
+
return id2 ? `Session launched. id: ${id2}, type: ${args.type}` : `Launched: ${JSON.stringify(result2)}`;
|
|
570
|
+
}
|
|
571
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
572
|
+
const result = await transport.launch(args.daemon_id, {
|
|
573
|
+
type: args.type,
|
|
574
|
+
dir: args.workspace,
|
|
575
|
+
model: args.model
|
|
576
|
+
});
|
|
577
|
+
if (result?.success === false || result?.error) return `Error: ${result.error ?? "launch failed"}`;
|
|
578
|
+
const id = result?.id ?? result?.sessionId;
|
|
579
|
+
return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/tools/check-pending.ts
|
|
583
|
+
var CHECK_PENDING_TOOL = {
|
|
584
|
+
name: "check_pending",
|
|
585
|
+
description: "List all agent sessions currently waiting for user approval (tool-use confirmation). Returns session ID, daemon ID, workspace, and the approval prompt message when available. Use approve() with the session_id to approve or reject.",
|
|
586
|
+
inputSchema: {
|
|
587
|
+
type: "object",
|
|
588
|
+
properties: {
|
|
589
|
+
daemon_id: {
|
|
590
|
+
type: "string",
|
|
591
|
+
description: "Daemon ID to check (cloud mode). Omit to check all daemons."
|
|
592
|
+
},
|
|
593
|
+
...FORMAT_PROP
|
|
594
|
+
},
|
|
595
|
+
required: []
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
async function checkPending(transport, args) {
|
|
599
|
+
if ("getStatus" in transport) {
|
|
600
|
+
return checkPendingLocal(transport, args.format);
|
|
601
|
+
}
|
|
602
|
+
return checkPendingCloud(transport, args.daemon_id, args.format);
|
|
603
|
+
}
|
|
604
|
+
async function checkPendingLocal(transport, format) {
|
|
605
|
+
const status = await transport.getStatus();
|
|
606
|
+
const sessions = status?.sessions ?? [];
|
|
607
|
+
const pending = sessions.filter(
|
|
608
|
+
(s) => s.status === "waiting_approval" || s.agentStatus === "waiting_approval"
|
|
609
|
+
);
|
|
610
|
+
if (format === "json") {
|
|
611
|
+
return JSON.stringify({
|
|
612
|
+
pending: pending.map((s) => ({
|
|
613
|
+
session_id: s.id,
|
|
614
|
+
workspace: s.workspace ?? null,
|
|
615
|
+
type: s.providerType ?? null,
|
|
616
|
+
modal_message: s.activeChat?.activeModal?.message ?? null,
|
|
617
|
+
buttons: s.activeChat?.activeModal?.buttons ?? []
|
|
618
|
+
}))
|
|
619
|
+
}, null, 2);
|
|
620
|
+
}
|
|
621
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
622
|
+
const lines = pending.map((s) => {
|
|
623
|
+
const modal = s.activeChat?.activeModal;
|
|
624
|
+
const parts = [`session_id: ${s.id}`];
|
|
625
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
626
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
627
|
+
if (modal?.message) parts.push(`prompt: ${modal.message}`);
|
|
628
|
+
if (modal?.buttons?.length) parts.push(`buttons: ${modal.buttons.join(", ")}`);
|
|
629
|
+
return parts.join("\n ");
|
|
630
|
+
});
|
|
631
|
+
return `Pending approvals (${pending.length}):
|
|
632
|
+
|
|
633
|
+
${lines.join("\n\n")}`;
|
|
634
|
+
}
|
|
635
|
+
async function checkPendingCloud(transport, daemonId, format) {
|
|
636
|
+
const pending = [];
|
|
637
|
+
if (daemonId) {
|
|
638
|
+
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
639
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
640
|
+
for (const s of sessions) {
|
|
641
|
+
if (s.status === "waiting_approval") pending.push({ daemonId, session: s });
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
const data = await transport.listDaemons();
|
|
645
|
+
const daemons = data?.daemons ?? [];
|
|
646
|
+
for (let i = 0; i < daemons.length; i += 5) {
|
|
647
|
+
await Promise.allSettled(
|
|
648
|
+
daemons.slice(i, i + 5).map(async (d) => {
|
|
649
|
+
try {
|
|
650
|
+
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
651
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
652
|
+
for (const s of sessions) {
|
|
653
|
+
if (s.status === "waiting_approval") pending.push({ daemonId: d.id, session: s });
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (format === "json") {
|
|
662
|
+
return JSON.stringify({
|
|
663
|
+
pending: pending.map(({ daemonId: dId, session: s }) => ({
|
|
664
|
+
daemon_id: dId,
|
|
665
|
+
session_id: s.id,
|
|
666
|
+
workspace: s.workspace ?? null,
|
|
667
|
+
type: s.providerType ?? null,
|
|
668
|
+
modal_message: null,
|
|
669
|
+
buttons: []
|
|
670
|
+
}))
|
|
671
|
+
}, null, 2);
|
|
672
|
+
}
|
|
673
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
674
|
+
const lines = pending.map(({ daemonId: dId, session: s }) => {
|
|
675
|
+
const parts = [`daemon_id: ${dId}`, `session_id: ${s.id}`];
|
|
676
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
677
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
678
|
+
parts.push("(use read_chat to see the approval prompt)");
|
|
679
|
+
return parts.join("\n ");
|
|
680
|
+
});
|
|
681
|
+
return `Pending approvals (${pending.length}):
|
|
682
|
+
|
|
683
|
+
${lines.join("\n\n")}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/server.ts
|
|
687
|
+
async function startMcpServer(opts) {
|
|
688
|
+
const transport = opts.mode === "cloud" ? new CloudTransport({ apiKey: opts.apiKey, baseUrl: opts.baseUrl }) : new LocalTransport({ port: opts.port, password: opts.password });
|
|
689
|
+
const alive = await transport.ping();
|
|
690
|
+
if (!alive) {
|
|
691
|
+
const hint = opts.mode === "local" ? `Make sure the standalone daemon is running (adhdev standalone or npx @adhdev/daemon-standalone).` : `Check your API key and network connectivity.`;
|
|
692
|
+
process.stderr.write(`[adhdev-mcp] Cannot reach ${opts.mode} daemon. ${hint}
|
|
693
|
+
`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
const isLocal = opts.mode === "local";
|
|
697
|
+
const allTools = [
|
|
698
|
+
LIST_SESSIONS_TOOL,
|
|
699
|
+
LAUNCH_SESSION_TOOL,
|
|
700
|
+
CHECK_PENDING_TOOL,
|
|
701
|
+
READ_CHAT_TOOL,
|
|
702
|
+
SEND_CHAT_TOOL,
|
|
703
|
+
APPROVE_TOOL,
|
|
704
|
+
GIT_STATUS_TOOL,
|
|
705
|
+
...isLocal ? [SCREENSHOT_TOOL] : []
|
|
706
|
+
];
|
|
707
|
+
const server = new import_server.Server(
|
|
708
|
+
{ name: "adhdev-mcp-server", version: "0.9.60" },
|
|
709
|
+
{ capabilities: { tools: {} } }
|
|
710
|
+
);
|
|
711
|
+
server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({ tools: allTools }));
|
|
712
|
+
server.setRequestHandler(import_types.CallToolRequestSchema, async (req) => {
|
|
713
|
+
const { name, arguments: args } = req.params;
|
|
714
|
+
const a = args ?? {};
|
|
715
|
+
try {
|
|
716
|
+
switch (name) {
|
|
717
|
+
case "list_sessions": {
|
|
718
|
+
const text = await listSessions(transport, { format: a.format });
|
|
719
|
+
return { content: [{ type: "text", text }] };
|
|
720
|
+
}
|
|
721
|
+
case "read_chat": {
|
|
722
|
+
const text = await readChat(transport, a);
|
|
723
|
+
return { content: [{ type: "text", text }] };
|
|
724
|
+
}
|
|
725
|
+
case "send_chat": {
|
|
726
|
+
const text = await sendChat(transport, { message: a.message, session_id: a.session_id, daemon_id: a.daemon_id });
|
|
727
|
+
return { content: [{ type: "text", text }] };
|
|
728
|
+
}
|
|
729
|
+
case "approve": {
|
|
730
|
+
const action = a.action === "reject" ? "reject" : "approve";
|
|
731
|
+
const text = await approve(transport, { action, session_id: a.session_id, daemon_id: a.daemon_id });
|
|
732
|
+
return { content: [{ type: "text", text }] };
|
|
733
|
+
}
|
|
734
|
+
case "screenshot": {
|
|
735
|
+
const result = await screenshot(transport, { session_id: a.session_id, daemon_id: a.daemon_id });
|
|
736
|
+
if (result.type === "image") {
|
|
737
|
+
return {
|
|
738
|
+
content: [{ type: "image", data: result.data, mimeType: result.mimeType }]
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return { content: [{ type: "text", text: result.text }] };
|
|
742
|
+
}
|
|
743
|
+
case "git_status": {
|
|
744
|
+
const text = await gitStatus(transport, { workspace: a.workspace, include_diff: a.include_diff, daemon_id: a.daemon_id, format: a.format });
|
|
745
|
+
return { content: [{ type: "text", text }] };
|
|
746
|
+
}
|
|
747
|
+
case "launch_session": {
|
|
748
|
+
const text = await launchSession(transport, {
|
|
749
|
+
type: a.type,
|
|
750
|
+
workspace: a.workspace,
|
|
751
|
+
model: a.model,
|
|
752
|
+
daemon_id: a.daemon_id
|
|
753
|
+
});
|
|
754
|
+
return { content: [{ type: "text", text }] };
|
|
755
|
+
}
|
|
756
|
+
case "check_pending": {
|
|
757
|
+
const text = await checkPending(transport, { daemon_id: a.daemon_id, format: a.format });
|
|
758
|
+
return { content: [{ type: "text", text }] };
|
|
759
|
+
}
|
|
760
|
+
default:
|
|
761
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
762
|
+
}
|
|
763
|
+
} catch (err) {
|
|
764
|
+
return {
|
|
765
|
+
content: [{ type: "text", text: `Error: ${err?.message ?? String(err)}` }],
|
|
766
|
+
isError: true
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
const stdioTransport = new import_stdio.StdioServerTransport();
|
|
771
|
+
await server.connect(stdioTransport);
|
|
772
|
+
process.stderr.write(`[adhdev-mcp] Server running in ${opts.mode} mode.
|
|
773
|
+
`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/index.ts
|
|
777
|
+
function parseArgs(argv) {
|
|
778
|
+
const args = argv.slice(2);
|
|
779
|
+
let apiKey;
|
|
780
|
+
let baseUrl;
|
|
781
|
+
let port;
|
|
782
|
+
let password;
|
|
783
|
+
for (let i = 0; i < args.length; i++) {
|
|
784
|
+
const arg = args[i];
|
|
785
|
+
if ((arg === "--api-key" || arg === "-k") && args[i + 1]) {
|
|
786
|
+
apiKey = args[++i];
|
|
787
|
+
} else if (arg?.startsWith("--api-key=")) {
|
|
788
|
+
apiKey = arg.slice("--api-key=".length);
|
|
789
|
+
} else if (arg === "--base-url" && args[i + 1]) {
|
|
790
|
+
baseUrl = args[++i];
|
|
791
|
+
} else if (arg === "--port" && args[i + 1]) {
|
|
792
|
+
port = Number(args[++i]);
|
|
793
|
+
} else if (arg?.startsWith("--port=")) {
|
|
794
|
+
port = Number(arg.slice("--port=".length));
|
|
795
|
+
} else if (arg === "--password" && args[i + 1]) {
|
|
796
|
+
password = args[++i];
|
|
797
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
798
|
+
printHelp();
|
|
799
|
+
process.exit(0);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (!apiKey && process.env.ADHDEV_API_KEY) apiKey = process.env.ADHDEV_API_KEY;
|
|
803
|
+
if (!password && process.env.ADHDEV_PASSWORD) password = process.env.ADHDEV_PASSWORD;
|
|
804
|
+
const mode = apiKey ? "cloud" : "local";
|
|
805
|
+
return { mode, port, password, apiKey, baseUrl };
|
|
806
|
+
}
|
|
807
|
+
function printHelp() {
|
|
808
|
+
console.error(`
|
|
809
|
+
adhdev-mcp \u2014 ADHDev MCP Server
|
|
810
|
+
|
|
811
|
+
Usage:
|
|
812
|
+
adhdev-mcp Local mode (requires standalone daemon)
|
|
813
|
+
adhdev-mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
814
|
+
|
|
815
|
+
Options:
|
|
816
|
+
--port <n> Standalone daemon port (default: 3847)
|
|
817
|
+
--password <pass> Standalone daemon password (if set)
|
|
818
|
+
--api-key <key> ADHDev cloud API key (switches to cloud mode)
|
|
819
|
+
--base-url <url> Override cloud API base URL
|
|
820
|
+
--help Show this help
|
|
821
|
+
|
|
822
|
+
Environment variables:
|
|
823
|
+
ADHDEV_API_KEY API key (cloud mode)
|
|
824
|
+
ADHDEV_PASSWORD Daemon password (local mode)
|
|
825
|
+
|
|
826
|
+
Local mode tools: list_sessions, launch_session, check_pending, read_chat, send_chat, approve, git_status, screenshot
|
|
827
|
+
Cloud mode tools: list_sessions, launch_session, check_pending, read_chat, send_chat, approve, git_status
|
|
828
|
+
`.trim());
|
|
829
|
+
}
|
|
830
|
+
startMcpServer(parseArgs(process.argv)).catch((err) => {
|
|
831
|
+
process.stderr.write(`[adhdev-mcp] Fatal: ${err?.message ?? err}
|
|
832
|
+
`);
|
|
833
|
+
process.exit(1);
|
|
834
|
+
});
|
|
835
|
+
//# sourceMappingURL=index.js.map
|