adhdev 0.9.76-rc.7 → 0.9.76-rc.71
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 +6970 -2387
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +6748 -2215
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/vendor/mcp-server/index.js +1979 -1303
- package/vendor/mcp-server/index.js.map +1 -1
- package/vendor/session-host-daemon/index.js +21 -0
- package/vendor/session-host-daemon/index.js.map +1 -1
- package/vendor/session-host-daemon/index.mjs +22 -0
- package/vendor/session-host-daemon/index.mjs.map +1 -1
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.mts +15 -1
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.ts +15 -1
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js +25 -0
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js.map +1 -1
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs +24 -0
- package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs.map +1 -1
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.mts +15 -1
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.ts +15 -1
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js +25 -0
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js.map +1 -1
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs +24 -0
- package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs.map +1 -1
|
@@ -35,217 +35,6 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
|
-
// src/server.ts
|
|
39
|
-
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
40
|
-
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
41
|
-
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
42
|
-
|
|
43
|
-
// src/transports/local.ts
|
|
44
|
-
var DEFAULT_PORT = 3847;
|
|
45
|
-
var LocalTransport = class {
|
|
46
|
-
baseUrl;
|
|
47
|
-
authHeader;
|
|
48
|
-
constructor(opts = {}) {
|
|
49
|
-
this.baseUrl = `http://localhost:${opts.port ?? DEFAULT_PORT}`;
|
|
50
|
-
this.authHeader = opts.password ? `Bearer ${opts.password}` : null;
|
|
51
|
-
}
|
|
52
|
-
headers() {
|
|
53
|
-
const h = { "Content-Type": "application/json" };
|
|
54
|
-
if (this.authHeader) h["Authorization"] = this.authHeader;
|
|
55
|
-
return h;
|
|
56
|
-
}
|
|
57
|
-
async getStatus() {
|
|
58
|
-
const res = await fetch(`${this.baseUrl}/api/v1/status`, { headers: this.headers() });
|
|
59
|
-
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
60
|
-
return res.json();
|
|
61
|
-
}
|
|
62
|
-
async command(type, args = {}) {
|
|
63
|
-
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: this.headers(),
|
|
66
|
-
body: JSON.stringify({ type, ...args })
|
|
67
|
-
});
|
|
68
|
-
if (!res.ok) {
|
|
69
|
-
const text = await res.text().catch(() => res.statusText);
|
|
70
|
-
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
71
|
-
}
|
|
72
|
-
return res.json();
|
|
73
|
-
}
|
|
74
|
-
async ping() {
|
|
75
|
-
try {
|
|
76
|
-
await this.getStatus();
|
|
77
|
-
return true;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// src/transports/cloud.ts
|
|
85
|
-
var DEFAULT_BASE_URL = "https://api.adhf.dev";
|
|
86
|
-
var CloudTransport = class {
|
|
87
|
-
baseUrl;
|
|
88
|
-
apiKey;
|
|
89
|
-
constructor(opts) {
|
|
90
|
-
this.apiKey = opts.apiKey;
|
|
91
|
-
this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
92
|
-
}
|
|
93
|
-
headers() {
|
|
94
|
-
return {
|
|
95
|
-
"Content-Type": "application/json",
|
|
96
|
-
"Authorization": `Bearer ${this.apiKey}`
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
async listDaemons() {
|
|
100
|
-
const res = await fetch(`${this.baseUrl}/api/v1/daemons`, { headers: this.headers() });
|
|
101
|
-
if (!res.ok) throw new Error(`List daemons failed: ${res.status}`);
|
|
102
|
-
return res.json();
|
|
103
|
-
}
|
|
104
|
-
async getStatus(targetId) {
|
|
105
|
-
const res = await fetch(
|
|
106
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/status`,
|
|
107
|
-
{ headers: this.headers() }
|
|
108
|
-
);
|
|
109
|
-
if (!res.ok) throw new Error(`Status failed: ${res.status}`);
|
|
110
|
-
return res.json();
|
|
111
|
-
}
|
|
112
|
-
/** Get all sessions for a daemon (returns CompactSessionEntry[]). */
|
|
113
|
-
async getDaemonStatus(daemonId) {
|
|
114
|
-
const res = await fetch(
|
|
115
|
-
`${this.baseUrl}/api/v1/daemons/${encodeURIComponent(daemonId)}/status`,
|
|
116
|
-
{ headers: this.headers() }
|
|
117
|
-
);
|
|
118
|
-
if (!res.ok) throw new Error(`Daemon status failed: ${res.status}`);
|
|
119
|
-
return res.json();
|
|
120
|
-
}
|
|
121
|
-
async readChat(targetId, opts = {}) {
|
|
122
|
-
const params = new URLSearchParams();
|
|
123
|
-
if (opts.limit) params.set("limit", String(opts.limit));
|
|
124
|
-
if (opts.sessionId) params.set("sessionId", opts.sessionId);
|
|
125
|
-
const qs = params.toString() ? `?${params}` : "";
|
|
126
|
-
const res = await fetch(
|
|
127
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat${qs}`,
|
|
128
|
-
{ headers: this.headers() }
|
|
129
|
-
);
|
|
130
|
-
if (!res.ok) throw new Error(`Read chat failed: ${res.status}`);
|
|
131
|
-
return res.json();
|
|
132
|
-
}
|
|
133
|
-
async sendChat(targetId, message, opts = {}) {
|
|
134
|
-
const res = await fetch(
|
|
135
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat`,
|
|
136
|
-
{
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers: this.headers(),
|
|
139
|
-
body: JSON.stringify({ message, ...opts })
|
|
140
|
-
}
|
|
141
|
-
);
|
|
142
|
-
if (!res.ok) throw new Error(`Send chat failed: ${res.status}`);
|
|
143
|
-
return res.json();
|
|
144
|
-
}
|
|
145
|
-
async approve(targetId, action, agentType) {
|
|
146
|
-
const res = await fetch(
|
|
147
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/approve`,
|
|
148
|
-
{
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: this.headers(),
|
|
151
|
-
body: JSON.stringify({ action, ...agentType ? { agentType } : {} })
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
155
|
-
return res.json();
|
|
156
|
-
}
|
|
157
|
-
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
158
|
-
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
159
|
-
const res = await fetch(
|
|
160
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
161
|
-
{ headers: this.headers() }
|
|
162
|
-
);
|
|
163
|
-
if (!res.ok) throw new Error(`Git status failed: ${res.status}`);
|
|
164
|
-
return res.json();
|
|
165
|
-
}
|
|
166
|
-
async stop(daemonId, opts) {
|
|
167
|
-
const res = await fetch(
|
|
168
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/stop`,
|
|
169
|
-
{
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: this.headers(),
|
|
172
|
-
body: JSON.stringify(opts)
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
if (!res.ok) throw new Error(`Stop failed: ${res.status}`);
|
|
176
|
-
return res.json();
|
|
177
|
-
}
|
|
178
|
-
async launch(daemonId, opts) {
|
|
179
|
-
const res = await fetch(
|
|
180
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/launch`,
|
|
181
|
-
{
|
|
182
|
-
method: "POST",
|
|
183
|
-
headers: this.headers(),
|
|
184
|
-
body: JSON.stringify(opts)
|
|
185
|
-
}
|
|
186
|
-
);
|
|
187
|
-
if (!res.ok) throw new Error(`Launch failed: ${res.status}`);
|
|
188
|
-
return res.json();
|
|
189
|
-
}
|
|
190
|
-
async gitLog(daemonId, workspace, opts = {}) {
|
|
191
|
-
const params = new URLSearchParams({ workspace });
|
|
192
|
-
if (opts.limit) params.set("limit", String(opts.limit));
|
|
193
|
-
if (opts.file) params.set("file", opts.file);
|
|
194
|
-
if (opts.since) params.set("since", opts.since);
|
|
195
|
-
if (opts.until) params.set("until", opts.until);
|
|
196
|
-
const res = await fetch(
|
|
197
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-log?${params}`,
|
|
198
|
-
{ headers: this.headers() }
|
|
199
|
-
);
|
|
200
|
-
if (!res.ok) throw new Error(`Git log failed: ${res.status}`);
|
|
201
|
-
return res.json();
|
|
202
|
-
}
|
|
203
|
-
async gitDiff(daemonId, workspace, opts = {}) {
|
|
204
|
-
const params = new URLSearchParams({ workspace });
|
|
205
|
-
if (opts.file) params.set("file", opts.file);
|
|
206
|
-
if (opts.maxLines) params.set("maxLines", String(opts.maxLines));
|
|
207
|
-
if (opts.staged) params.set("staged", "true");
|
|
208
|
-
const res = await fetch(
|
|
209
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-diff?${params}`,
|
|
210
|
-
{ headers: this.headers() }
|
|
211
|
-
);
|
|
212
|
-
if (!res.ok) throw new Error(`Git diff failed: ${res.status}`);
|
|
213
|
-
return res.json();
|
|
214
|
-
}
|
|
215
|
-
async gitPush(daemonId, opts) {
|
|
216
|
-
const res = await fetch(
|
|
217
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-push`,
|
|
218
|
-
{
|
|
219
|
-
method: "POST",
|
|
220
|
-
headers: this.headers(),
|
|
221
|
-
body: JSON.stringify(opts)
|
|
222
|
-
}
|
|
223
|
-
);
|
|
224
|
-
if (!res.ok) throw new Error(`Git push failed: ${res.status}`);
|
|
225
|
-
return res.json();
|
|
226
|
-
}
|
|
227
|
-
async gitCheckpoint(daemonId, opts) {
|
|
228
|
-
const res = await fetch(
|
|
229
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-checkpoint`,
|
|
230
|
-
{
|
|
231
|
-
method: "POST",
|
|
232
|
-
headers: this.headers(),
|
|
233
|
-
body: JSON.stringify(opts)
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
if (!res.ok) throw new Error(`Git checkpoint failed: ${res.status}`);
|
|
237
|
-
return res.json();
|
|
238
|
-
}
|
|
239
|
-
async ping() {
|
|
240
|
-
try {
|
|
241
|
-
await this.listDaemons();
|
|
242
|
-
return true;
|
|
243
|
-
} catch {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
38
|
// src/transports/ipc.ts
|
|
250
39
|
var DEFAULT_IPC_PORT = 19222;
|
|
251
40
|
var DEFAULT_IPC_PATH = "/ipc";
|
|
@@ -296,10 +85,14 @@ var IpcTransport = class {
|
|
|
296
85
|
}
|
|
297
86
|
fn();
|
|
298
87
|
};
|
|
88
|
+
const timeoutMs = type === "mesh_relay_command" ? 6e4 : 15e3;
|
|
299
89
|
const timeout = setTimeout(() => {
|
|
300
|
-
finish(() => reject(new Error(`Daemon IPC command '${type}' timed out after
|
|
301
|
-
},
|
|
90
|
+
finish(() => reject(new Error(`Daemon IPC command '${type}' timed out after ${Math.round(timeoutMs / 1e3)}s`)));
|
|
91
|
+
}, timeoutMs);
|
|
92
|
+
let commandSent = false;
|
|
302
93
|
const send = () => {
|
|
94
|
+
if (commandSent) return;
|
|
95
|
+
commandSent = true;
|
|
303
96
|
ws.send(JSON.stringify({
|
|
304
97
|
type: "ext:command",
|
|
305
98
|
payload: { command: type, args, requestId }
|
|
@@ -349,775 +142,1504 @@ function isLocalTransport(transport) {
|
|
|
349
142
|
return typeof transport.command === "function";
|
|
350
143
|
}
|
|
351
144
|
|
|
352
|
-
// src/tools/
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
145
|
+
// src/tools/chat-compact.ts
|
|
146
|
+
function messageContent(message) {
|
|
147
|
+
const content = message?.content;
|
|
148
|
+
if (typeof content === "string") return content;
|
|
149
|
+
if (Array.isArray(content)) {
|
|
150
|
+
return content.map((part) => typeof part === "string" ? part : part?.text ?? "").join("");
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
function isCoordinatorVisibleMessage(message) {
|
|
155
|
+
if (!message || typeof message !== "object") return false;
|
|
156
|
+
const role = String(message.role ?? "").toLowerCase();
|
|
157
|
+
if (role === "tool" || role === "system" || role === "debug") return false;
|
|
158
|
+
const kind = String(message.kind ?? message.type ?? message.messageKind ?? "").toLowerCase();
|
|
159
|
+
if (["tool", "tool_call", "tool_result", "terminal", "internal", "control", "debug", "status"].includes(kind)) return false;
|
|
160
|
+
const meta = message.meta ?? message.metadata;
|
|
161
|
+
if (meta?.internal === true || meta?.debug === true || meta?.control === true || meta?.userVisible === false || meta?.user_visible === false) return false;
|
|
162
|
+
return role === "user" || role === "assistant" || role === "agent";
|
|
163
|
+
}
|
|
164
|
+
function compactChatPayload(payload, opts = {}) {
|
|
165
|
+
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
166
|
+
const visible = rawMessages.filter(isCoordinatorVisibleMessage);
|
|
167
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 10, 10));
|
|
168
|
+
const messages = visible.slice(-limit);
|
|
169
|
+
const finalAssistant = [...visible].reverse().find((message) => {
|
|
170
|
+
const role = String(message?.role ?? "").toLowerCase();
|
|
171
|
+
return (role === "assistant" || role === "agent") && messageContent(message).trim();
|
|
172
|
+
});
|
|
173
|
+
const summary = typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : messageContent(finalAssistant).trim();
|
|
174
|
+
return {
|
|
175
|
+
success: payload?.success !== false,
|
|
176
|
+
compact: true,
|
|
177
|
+
...opts.nodeId ? { nodeId: opts.nodeId } : {},
|
|
178
|
+
...opts.sessionId !== void 0 ? { sessionId: opts.sessionId } : {},
|
|
179
|
+
status: payload?.status ?? null,
|
|
180
|
+
providerSessionId: payload?.providerSessionId ?? null,
|
|
181
|
+
totalMessages: rawMessages.length,
|
|
182
|
+
visibleMessages: visible.length,
|
|
183
|
+
filteredMessages: visible.length,
|
|
184
|
+
omittedMessages: Math.max(0, rawMessages.length - visible.length),
|
|
185
|
+
summary,
|
|
186
|
+
...payload?.changedFiles !== void 0 ? { changedFiles: payload.changedFiles } : {},
|
|
187
|
+
...payload?.testsRun !== void 0 ? { testsRun: payload.testsRun } : {},
|
|
188
|
+
messages
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/tools/read-chat-polling-advisory.ts
|
|
193
|
+
var RAPID_READ_CHAT_ADVISORY_WINDOW_MS = 5e3;
|
|
194
|
+
var ACTIVE_READ_STATUSES = /* @__PURE__ */ new Set([
|
|
195
|
+
"generating",
|
|
196
|
+
"running",
|
|
197
|
+
"streaming",
|
|
198
|
+
"starting",
|
|
199
|
+
"busy"
|
|
200
|
+
]);
|
|
201
|
+
var recentReads = /* @__PURE__ */ new Map();
|
|
202
|
+
function isActiveReadChatStatus(status) {
|
|
203
|
+
return typeof status === "string" && ACTIVE_READ_STATUSES.has(status.toLowerCase());
|
|
204
|
+
}
|
|
205
|
+
function annotateRapidReadChatAdvisory(payload, options) {
|
|
206
|
+
const now = options.now ?? Date.now();
|
|
207
|
+
const status = options.status ?? payload?.status ?? payload?.data?.status ?? payload?.result?.status;
|
|
208
|
+
const active = isActiveReadChatStatus(status);
|
|
209
|
+
const previous = recentReads.get(options.key);
|
|
210
|
+
if (!active) {
|
|
211
|
+
recentReads.set(options.key, { at: now, status: typeof status === "string" ? status : void 0 });
|
|
212
|
+
return payload;
|
|
213
|
+
}
|
|
214
|
+
recentReads.set(options.key, { at: now, status: typeof status === "string" ? status : void 0 });
|
|
215
|
+
if (!previous || !isActiveReadChatStatus(previous.status)) return payload;
|
|
216
|
+
const elapsedMs = now - previous.at;
|
|
217
|
+
if (elapsedMs < 0 || elapsedMs >= RAPID_READ_CHAT_ADVISORY_WINDOW_MS) return payload;
|
|
218
|
+
return {
|
|
219
|
+
...payload,
|
|
220
|
+
pollingAdvisory: {
|
|
221
|
+
type: "rapid_read_chat_polling",
|
|
222
|
+
toolName: options.toolName,
|
|
223
|
+
windowMs: RAPID_READ_CHAT_ADVISORY_WINDOW_MS,
|
|
224
|
+
elapsedMs,
|
|
225
|
+
nextSuggestedReadAt: previous.at + RAPID_READ_CHAT_ADVISORY_WINDOW_MS,
|
|
226
|
+
completionCallbackExpected: Boolean(options.completionCallbackExpected),
|
|
227
|
+
message: `This session is still ${String(status)}. Avoid repeated ${options.toolName} polling for the same generating session; wait for the completion callback/status event or retry after the suggested time if you are debugging a real stall.`
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/tools/mesh-tools.ts
|
|
233
|
+
var meshSessionProviderMetadata = /* @__PURE__ */ new Map();
|
|
234
|
+
async function refreshMeshFromDaemon(ctx) {
|
|
235
|
+
if (!(ctx.transport instanceof IpcTransport)) return;
|
|
236
|
+
try {
|
|
237
|
+
const result = await ctx.transport.command("get_mesh", { meshId: ctx.mesh.id });
|
|
238
|
+
if (!result?.success || !Array.isArray(result.mesh?.nodes)) return;
|
|
239
|
+
const refreshedNodes = result.mesh.nodes.filter((n) => n?.id).map((n) => n);
|
|
240
|
+
if (!refreshedNodes.length) return;
|
|
241
|
+
ctx.mesh.nodes.splice(0, ctx.mesh.nodes.length, ...refreshedNodes);
|
|
242
|
+
ctx.mesh.updatedAt = result.mesh.updatedAt ?? ctx.mesh.updatedAt;
|
|
243
|
+
} catch {
|
|
358
244
|
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
245
|
+
}
|
|
246
|
+
async function findNodeWithRefresh(ctx, nodeId) {
|
|
247
|
+
const hit = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
248
|
+
if (hit) return hit;
|
|
249
|
+
await refreshMeshFromDaemon(ctx);
|
|
250
|
+
const refreshed = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
251
|
+
if (!refreshed) throw new Error(`Node '${nodeId}' is not a member of mesh '${ctx.mesh.name}'`);
|
|
252
|
+
return refreshed;
|
|
253
|
+
}
|
|
254
|
+
function unwrapCommandPayload(value) {
|
|
255
|
+
let current = value;
|
|
256
|
+
const seen = /* @__PURE__ */ new Set();
|
|
257
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
258
|
+
if (!current || typeof current !== "object" || seen.has(current)) break;
|
|
259
|
+
seen.add(current);
|
|
260
|
+
const nested = current.result ?? current.payload;
|
|
261
|
+
if (!nested || typeof nested !== "object") break;
|
|
262
|
+
current = nested;
|
|
263
|
+
}
|
|
264
|
+
return current;
|
|
265
|
+
}
|
|
266
|
+
function findNestedPayload(value, predicate) {
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const stack = [{ payload: value, depth: 0 }];
|
|
269
|
+
while (stack.length) {
|
|
270
|
+
const { payload, depth } = stack.pop();
|
|
271
|
+
if (predicate(payload)) return payload;
|
|
272
|
+
if (!payload || typeof payload !== "object" || seen.has(payload) || depth >= 8) continue;
|
|
273
|
+
seen.add(payload);
|
|
274
|
+
for (const key of ["payload", "result"]) {
|
|
275
|
+
if (key in payload) stack.push({ payload: payload[key], depth: depth + 1 });
|
|
276
|
+
}
|
|
373
277
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (s.status ?? s.agentStatus) parts.push(`status: ${s.status ?? s.agentStatus}`);
|
|
396
|
-
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
397
|
-
return parts.join(", ");
|
|
398
|
-
});
|
|
399
|
-
return `Sessions (${sessions.length}):
|
|
400
|
-
${lines.join("\n")}`;
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function extractCloneNodePayload(value) {
|
|
281
|
+
return findNestedPayload(value, (payload) => Boolean(payload?.node?.id));
|
|
282
|
+
}
|
|
283
|
+
function extractGitStatus(value) {
|
|
284
|
+
const payload = unwrapCommandPayload(value);
|
|
285
|
+
return payload?.status ?? value?.status ?? payload;
|
|
286
|
+
}
|
|
287
|
+
function extractGitDiff(value) {
|
|
288
|
+
const payload = unwrapCommandPayload(value);
|
|
289
|
+
return payload?.diffSummary ?? payload?.diff ?? value?.diffSummary ?? value?.diff ?? payload;
|
|
290
|
+
}
|
|
291
|
+
function extractLaunchPayload(value) {
|
|
292
|
+
return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
|
|
293
|
+
}
|
|
294
|
+
function resolveCoordinatorNode(ctx) {
|
|
295
|
+
const preferredNodeId = typeof ctx.mesh.coordinator?.preferredNodeId === "string" ? ctx.mesh.coordinator.preferredNodeId.trim() : "";
|
|
296
|
+
if (preferredNodeId) {
|
|
297
|
+
const preferred = ctx.mesh.nodes.find((n) => n.id === preferredNodeId && typeof n.daemonId === "string" && n.daemonId.trim());
|
|
298
|
+
if (preferred) return preferred;
|
|
401
299
|
}
|
|
402
|
-
|
|
300
|
+
if (ctx.localDaemonId) {
|
|
301
|
+
return ctx.mesh.nodes.find((n) => n.daemonId === ctx.localDaemonId);
|
|
302
|
+
}
|
|
303
|
+
return void 0;
|
|
403
304
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
305
|
+
function meshSessionCacheKey(nodeId, runtimeSessionId) {
|
|
306
|
+
return `${nodeId}:${runtimeSessionId}`;
|
|
307
|
+
}
|
|
308
|
+
function countUncommittedChanges(status) {
|
|
309
|
+
if (typeof status?.uncommittedChanges === "number") return status.uncommittedChanges;
|
|
310
|
+
const keys = ["staged", "modified", "untracked", "deleted", "renamed"];
|
|
311
|
+
const counted = keys.reduce((sum, key) => sum + (Number.isFinite(Number(status?.[key])) ? Number(status[key]) : 0), 0);
|
|
312
|
+
const conflicts = Array.isArray(status?.conflictFiles) ? status.conflictFiles.length : status?.hasConflicts ? 1 : 0;
|
|
313
|
+
return counted + conflicts;
|
|
314
|
+
}
|
|
315
|
+
function isGitStatusDirty(status) {
|
|
316
|
+
if (typeof status?.isDirty === "boolean") return status.isDirty;
|
|
317
|
+
if (typeof status?.dirty === "boolean") return status.dirty;
|
|
318
|
+
return countUncommittedChanges(status) > 0;
|
|
319
|
+
}
|
|
320
|
+
function readRelatedRepos(node) {
|
|
321
|
+
const raw = Array.isArray(node.relatedRepos) ? node.relatedRepos : Array.isArray(node.policy?.relatedRepos) ? node.policy.relatedRepos : [];
|
|
322
|
+
return raw.map((entry) => ({
|
|
323
|
+
label: typeof entry?.label === "string" ? entry.label.trim() : "",
|
|
324
|
+
workspace: typeof entry?.workspace === "string" ? entry.workspace.trim() : ""
|
|
325
|
+
})).filter((entry) => Boolean(entry.label && entry.workspace));
|
|
326
|
+
}
|
|
327
|
+
function summarizeRelatedRepoStatus(repo, status) {
|
|
328
|
+
const dirty = isGitStatusDirty(status);
|
|
329
|
+
return {
|
|
330
|
+
label: repo.label,
|
|
331
|
+
workspace: repo.workspace,
|
|
332
|
+
isGitRepo: status?.isGitRepo === true,
|
|
333
|
+
branch: status?.branch ?? null,
|
|
334
|
+
ahead: Number.isFinite(Number(status?.ahead)) ? Number(status.ahead) : 0,
|
|
335
|
+
behind: Number.isFinite(Number(status?.behind)) ? Number(status.behind) : 0,
|
|
336
|
+
dirty,
|
|
337
|
+
uncommittedChanges: countUncommittedChanges(status),
|
|
338
|
+
head: status?.headCommit ?? null,
|
|
339
|
+
lastCommitSummary: status?.headMessage ?? null,
|
|
340
|
+
...status?.reason ? { reason: status.reason } : {},
|
|
341
|
+
...status?.error ? { error: status.error } : {}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function collectRelatedRepoStatuses(ctx, node) {
|
|
345
|
+
const relatedRepos = readRelatedRepos(node);
|
|
346
|
+
if (!relatedRepos.length) return [];
|
|
347
|
+
const results = [];
|
|
348
|
+
for (const repo of relatedRepos) {
|
|
349
|
+
try {
|
|
350
|
+
const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace });
|
|
351
|
+
const status = extractGitStatus(statusResult);
|
|
352
|
+
results.push(summarizeRelatedRepoStatus(repo, status));
|
|
353
|
+
} catch (e) {
|
|
354
|
+
results.push({
|
|
355
|
+
label: repo.label,
|
|
356
|
+
workspace: repo.workspace,
|
|
357
|
+
error: e?.message || "related repo status failed"
|
|
358
|
+
});
|
|
426
359
|
}
|
|
427
360
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
361
|
+
return results;
|
|
362
|
+
}
|
|
363
|
+
function readProviderPriority(policy) {
|
|
364
|
+
const raw = policy?.providerPriority;
|
|
365
|
+
return Array.isArray(raw) ? raw.map((type) => typeof type === "string" ? type.trim() : "").filter(Boolean) : [];
|
|
366
|
+
}
|
|
367
|
+
function readSpawnedSessionVisibility(policy) {
|
|
368
|
+
return policy?.spawnedSessionVisibility === "hidden" ? "hidden" : "visible";
|
|
369
|
+
}
|
|
370
|
+
function missingProviderPriorityMessage(nodeId) {
|
|
371
|
+
return `Node '${nodeId}' has no providerPriority policy; pass type explicitly or configure node.policy.providerPriority`;
|
|
372
|
+
}
|
|
373
|
+
function getNodeLaunchReadiness(node) {
|
|
374
|
+
const providerPriority = readProviderPriority(node.policy);
|
|
375
|
+
if (providerPriority.length) {
|
|
376
|
+
return {
|
|
377
|
+
providerPriority,
|
|
378
|
+
launchReady: true
|
|
379
|
+
};
|
|
438
380
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
];
|
|
446
|
-
if (s.status) parts.push(`status: ${s.status}`);
|
|
447
|
-
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
448
|
-
return parts.join(", ");
|
|
449
|
-
});
|
|
450
|
-
return `Sessions (${collected.length}):
|
|
451
|
-
${lines.join("\n")}`;
|
|
381
|
+
return {
|
|
382
|
+
providerPriority,
|
|
383
|
+
launchReady: false,
|
|
384
|
+
launchBlockedReason: "missing_provider_priority",
|
|
385
|
+
launchBlockedMessage: missingProviderPriorityMessage(node.id)
|
|
386
|
+
};
|
|
452
387
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
388
|
+
async function commandForNode(ctx, node, command, args = {}) {
|
|
389
|
+
if (ctx.transport instanceof IpcTransport && node.daemonId && node.daemonId !== ctx.localDaemonId) {
|
|
390
|
+
return ctx.transport.meshCommand(node.daemonId, command, args);
|
|
391
|
+
}
|
|
392
|
+
if (isLocalTransport(ctx.transport)) {
|
|
393
|
+
return ctx.transport.command(command, args);
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}'`);
|
|
396
|
+
}
|
|
397
|
+
var MESH_STATUS_TOOL = {
|
|
398
|
+
name: "mesh_status",
|
|
399
|
+
description: "Get the current status of all nodes in the repo mesh \u2014 health, git state, active sessions. Use this to decide which node to send work to.",
|
|
458
400
|
inputSchema: {
|
|
459
401
|
type: "object",
|
|
460
|
-
properties: {
|
|
461
|
-
...FORMAT_PROP
|
|
462
|
-
},
|
|
463
|
-
required: []
|
|
402
|
+
properties: {}
|
|
464
403
|
}
|
|
465
404
|
};
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
hostname: status?.hostname ?? status?.machine?.hostname ?? "localhost",
|
|
473
|
-
platform: status?.platform ?? status?.machine?.platform ?? "unknown",
|
|
474
|
-
version: status?.version ?? null,
|
|
475
|
-
sessions: (status?.sessions ?? []).length
|
|
476
|
-
};
|
|
477
|
-
if (asJson) return JSON.stringify({ daemons: [daemon] }, null, 2);
|
|
478
|
-
return `Daemons (1):
|
|
479
|
-
id: ${daemon.id}, hostname: ${daemon.hostname}, platform: ${daemon.platform}${daemon.version ? `, version: ${daemon.version}` : ""}, sessions: ${daemon.sessions}`;
|
|
480
|
-
}
|
|
481
|
-
const data = await transport.listDaemons();
|
|
482
|
-
const daemons = data?.daemons ?? [];
|
|
483
|
-
if (asJson) {
|
|
484
|
-
return JSON.stringify({
|
|
485
|
-
daemons: daemons.map((d) => ({
|
|
486
|
-
id: d.id,
|
|
487
|
-
hostname: d.hostname ?? null,
|
|
488
|
-
platform: d.platform ?? null,
|
|
489
|
-
nickname: d.nickname ?? null,
|
|
490
|
-
version: d.version ?? null,
|
|
491
|
-
p2p_available: d.p2p?.available ?? null,
|
|
492
|
-
cdp_connected: d.cdpConnected ?? null
|
|
493
|
-
}))
|
|
494
|
-
}, null, 2);
|
|
405
|
+
var MESH_LIST_NODES_TOOL = {
|
|
406
|
+
name: "mesh_list_nodes",
|
|
407
|
+
description: "List all nodes in the mesh with their capabilities, platform, and workspace paths.",
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
properties: {}
|
|
495
411
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (d.hostname) parts.push(`hostname: ${d.hostname}`);
|
|
501
|
-
if (d.platform) parts.push(`platform: ${d.platform}`);
|
|
502
|
-
if (d.version) parts.push(`version: ${d.version}`);
|
|
503
|
-
if (d.p2p?.available != null) parts.push(`p2p: ${d.p2p.available ? "yes" : "no"}`);
|
|
504
|
-
return parts.join(", ");
|
|
505
|
-
});
|
|
506
|
-
return `Daemons (${daemons.length}):
|
|
507
|
-
${lines.join("\n")}`;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// src/tools/read-chat.ts
|
|
511
|
-
var READ_CHAT_TOOL = {
|
|
512
|
-
name: "read_chat",
|
|
513
|
-
description: "Read the current chat conversation from an IDE agent session. Returns recent messages.",
|
|
412
|
+
};
|
|
413
|
+
var MESH_SEND_TASK_TOOL = {
|
|
414
|
+
name: "mesh_send_task",
|
|
415
|
+
description: "Send a natural-language task to an agent session on a mesh node. The agent will execute the task autonomously.",
|
|
514
416
|
inputSchema: {
|
|
515
417
|
type: "object",
|
|
516
418
|
properties: {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
},
|
|
521
|
-
limit: {
|
|
522
|
-
type: "number",
|
|
523
|
-
description: "Max messages to return (default: 50)."
|
|
524
|
-
},
|
|
525
|
-
daemon_id: {
|
|
526
|
-
type: "string",
|
|
527
|
-
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
528
|
-
},
|
|
529
|
-
...FORMAT_PROP
|
|
419
|
+
node_id: { type: "string", description: "Target node ID (from mesh_list_nodes)." },
|
|
420
|
+
session_id: { type: "string", description: "Agent session ID on the target node." },
|
|
421
|
+
message: { type: "string", description: "Natural-language task to send to the agent." }
|
|
530
422
|
},
|
|
531
|
-
required: []
|
|
423
|
+
required: ["node_id", "session_id", "message"]
|
|
532
424
|
}
|
|
533
425
|
};
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
function formatChatResult(result, sessionId, format, limit = 50) {
|
|
549
|
-
if (!result?.success && result?.error) {
|
|
550
|
-
if (format === "json") return JSON.stringify({ error: result.error, messages: [] }, null, 2);
|
|
551
|
-
return `Error: ${result.error}`;
|
|
426
|
+
var MESH_READ_CHAT_TOOL = {
|
|
427
|
+
name: "mesh_read_chat",
|
|
428
|
+
description: "Read recent chat messages from a delegated agent session on a mesh node. Use compact=true for coordinator context-efficient review: it filters tool/internal/debug chatter and returns the final user-visible summary plus recent key messages. If the runtime session has completed, provider_session_id can explicitly target provider transcript history.",
|
|
429
|
+
inputSchema: {
|
|
430
|
+
type: "object",
|
|
431
|
+
properties: {
|
|
432
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
433
|
+
session_id: { type: "string", description: "Agent session ID to read from." },
|
|
434
|
+
provider_session_id: { type: "string", description: "Optional provider transcript/session ID for completed sessions." },
|
|
435
|
+
tail: { type: "number", description: "Number of recent messages to return (default: 10)." },
|
|
436
|
+
compact: { type: "boolean", description: "When true, return a compact coordinator summary instead of the full transcript: tool/internal/control/debug messages are excluded and only recent user-visible key messages plus the final assistant summary are included." }
|
|
437
|
+
},
|
|
438
|
+
required: ["node_id", "session_id"]
|
|
552
439
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
440
|
+
};
|
|
441
|
+
var MESH_READ_DEBUG_TOOL = {
|
|
442
|
+
name: "mesh_read_debug",
|
|
443
|
+
description: "Collect a daemon-side chat/parser debug bundle for a delegated agent session on a mesh node without opening the browser UI. Defaults to daemon_file delivery and returns a saved bundle locator.",
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: {
|
|
447
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
448
|
+
session_id: { type: "string", description: "Agent session ID to debug." },
|
|
449
|
+
provider_session_id: { type: "string", description: "Optional provider transcript/session ID for completed session history." },
|
|
450
|
+
tail: { type: "number", description: "Number of recent read_chat messages to embed (default: 40)." },
|
|
451
|
+
delivery: { type: "string", enum: ["daemon_file", "inline"], description: "daemon_file saves the full sanitized bundle on the daemon; inline returns it directly. Default: daemon_file." }
|
|
452
|
+
},
|
|
453
|
+
required: ["node_id", "session_id"]
|
|
564
454
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
570
|
-
return `[${role}] ${truncated}`;
|
|
571
|
-
});
|
|
572
|
-
return lines.join("\n\n");
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// src/tools/send-chat.ts
|
|
576
|
-
var SEND_CHAT_TOOL = {
|
|
577
|
-
name: "send_chat",
|
|
578
|
-
description: "Send a message to an IDE agent session.",
|
|
455
|
+
};
|
|
456
|
+
var MESH_LAUNCH_SESSION_TOOL = {
|
|
457
|
+
name: "mesh_launch_session",
|
|
458
|
+
description: "Launch a new agent session on a mesh node. Returns the session ID for subsequent send_task/read_chat calls. If the user names a provider, preserve it exactly: Hermes = hermes-cli, Claude Code/Claude = claude-cli, Codex = codex-cli, Gemini = gemini-cli. If type is omitted, resolve strictly from the node policy providerPriority and provider detection; fail closed when no configured provider is usable. Do not default to claude-cli.",
|
|
579
459
|
inputSchema: {
|
|
580
460
|
type: "object",
|
|
581
461
|
properties: {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
description: "The message to send to the agent."
|
|
585
|
-
},
|
|
586
|
-
session_id: {
|
|
587
|
-
type: "string",
|
|
588
|
-
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
589
|
-
},
|
|
590
|
-
daemon_id: {
|
|
591
|
-
type: "string",
|
|
592
|
-
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
593
|
-
}
|
|
462
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
463
|
+
type: { type: "string", description: "Optional provider type to launch. Use hermes-cli for Hermes, claude-cli for Claude Code, codex-cli for Codex, gemini-cli for Gemini. When omitted, node.policy.providerPriority is probed in order." }
|
|
594
464
|
},
|
|
595
|
-
required: ["
|
|
465
|
+
required: ["node_id"]
|
|
596
466
|
}
|
|
597
467
|
};
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
468
|
+
var MESH_GIT_STATUS_TOOL = {
|
|
469
|
+
name: "mesh_git_status",
|
|
470
|
+
description: "Get git status for a mesh node workspace \u2014 branch, dirty state, changed files.",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
type: "object",
|
|
473
|
+
properties: {
|
|
474
|
+
node_id: { type: "string", description: "Target node ID." }
|
|
475
|
+
},
|
|
476
|
+
required: ["node_id"]
|
|
607
477
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
});
|
|
613
|
-
if (result?.success === false) return `Error: ${result.error ?? "send_chat failed"}`;
|
|
614
|
-
return "Message sent.";
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// src/tools/approve.ts
|
|
618
|
-
var APPROVE_TOOL = {
|
|
619
|
-
name: "approve",
|
|
620
|
-
description: "Approve or reject a pending agent action (e.g. file write, command execution).",
|
|
478
|
+
};
|
|
479
|
+
var MESH_CHECKPOINT_TOOL = {
|
|
480
|
+
name: "mesh_checkpoint",
|
|
481
|
+
description: "Create a git checkpoint (commit) on a mesh node workspace.",
|
|
621
482
|
inputSchema: {
|
|
622
483
|
type: "object",
|
|
623
484
|
properties: {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
enum: ["approve", "reject"],
|
|
627
|
-
description: "Whether to approve or reject the pending action."
|
|
628
|
-
},
|
|
629
|
-
session_id: {
|
|
630
|
-
type: "string",
|
|
631
|
-
description: "Target session ID. Omit to use the active session."
|
|
632
|
-
},
|
|
633
|
-
daemon_id: {
|
|
634
|
-
type: "string",
|
|
635
|
-
description: "Daemon ID (cloud mode only)."
|
|
636
|
-
}
|
|
485
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
486
|
+
message: { type: "string", description: "Checkpoint commit message." }
|
|
637
487
|
},
|
|
638
|
-
required: ["
|
|
488
|
+
required: ["node_id", "message"]
|
|
639
489
|
}
|
|
640
490
|
};
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
491
|
+
var MESH_APPROVE_TOOL = {
|
|
492
|
+
name: "mesh_approve",
|
|
493
|
+
description: "Approve or reject a pending action on a delegated agent session.",
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: {
|
|
497
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
498
|
+
session_id: { type: "string", description: "Agent session ID with pending approval." },
|
|
499
|
+
action: { type: "string", enum: ["approve", "reject"], description: "Action to take." }
|
|
500
|
+
},
|
|
501
|
+
required: ["node_id", "session_id", "action"]
|
|
650
502
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return `Action ${action}d.`;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// src/tools/screenshot.ts
|
|
659
|
-
var SCREENSHOT_TOOL = {
|
|
660
|
-
name: "screenshot",
|
|
661
|
-
description: "Capture a screenshot of the current IDE window. Returns the image. Local mode only \u2014 screenshots require direct P2P access to the daemon and are not available in cloud mode.",
|
|
503
|
+
};
|
|
504
|
+
var MESH_CLONE_NODE_TOOL = {
|
|
505
|
+
name: "mesh_clone_node",
|
|
506
|
+
description: "Create a new worktree-based node from an existing node for isolated parallel work. Creates a git worktree on a new branch so multiple tasks can run on separate branches simultaneously.",
|
|
662
507
|
inputSchema: {
|
|
663
508
|
type: "object",
|
|
664
509
|
properties: {
|
|
665
|
-
|
|
510
|
+
source_node_id: { type: "string", description: "Node ID to clone from (from mesh_list_nodes)." },
|
|
511
|
+
branch: { type: "string", description: 'Branch name for the new worktree (e.g. "feat/auth-refactor").' },
|
|
512
|
+
base_branch: { type: "string", description: "Starting point for the branch (default: current HEAD)." }
|
|
513
|
+
},
|
|
514
|
+
required: ["source_node_id", "branch"]
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var MESH_REMOVE_NODE_TOOL = {
|
|
518
|
+
name: "mesh_remove_node",
|
|
519
|
+
description: "Remove a node from the mesh. If the node is a worktree, also cleans up the git worktree and directory. Session cleanup is controlled by mesh policy sessionCleanupOnNodeRemove unless session_cleanup_mode overrides it for this call.",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
node_id: { type: "string", description: "Node ID to remove." },
|
|
524
|
+
session_cleanup_mode: {
|
|
666
525
|
type: "string",
|
|
667
|
-
|
|
526
|
+
enum: ["preserve", "stop", "delete_stopped", "stop_and_delete"],
|
|
527
|
+
description: "Optional override for cleanup of delegated sessions attached to this node. preserve keeps history/processes; stop stops live runtimes only; delete_stopped removes completed transcripts only; stop_and_delete stops live runtimes and deletes records."
|
|
668
528
|
}
|
|
669
529
|
},
|
|
670
|
-
required: []
|
|
530
|
+
required: ["node_id"]
|
|
671
531
|
}
|
|
672
532
|
};
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
result = await transport.command("screenshot", {
|
|
677
|
-
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
678
|
-
});
|
|
679
|
-
} else {
|
|
680
|
-
return { type: "text", text: "Screenshots are not available in cloud mode. Run adhdev mcp in local mode (requires standalone daemon)." };
|
|
681
|
-
}
|
|
682
|
-
if (result?.success === false) {
|
|
683
|
-
return { type: "text", text: `Error: ${result.error ?? "screenshot failed"}` };
|
|
684
|
-
}
|
|
685
|
-
const b64 = result?.base64 ?? result?.screenshot ?? result?.result;
|
|
686
|
-
if (!b64) {
|
|
687
|
-
return { type: "text", text: "Screenshot captured but no image data returned." };
|
|
688
|
-
}
|
|
689
|
-
const mimeType = result?.format === "png" ? "image/png" : "image/webp";
|
|
690
|
-
return { type: "image", data: b64, mimeType };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// src/tools/git-status.ts
|
|
694
|
-
var GIT_STATUS_TOOL = {
|
|
695
|
-
name: "git_status",
|
|
696
|
-
description: "Get git repository status for a workspace on the daemon machine.",
|
|
533
|
+
var MESH_CLEANUP_SESSIONS_TOOL = {
|
|
534
|
+
name: "mesh_cleanup_sessions",
|
|
535
|
+
description: "Manually clean up delegated session records for a mesh node without removing the node. Defaults should preserve reviewable history unless the caller chooses a mode explicitly.",
|
|
697
536
|
inputSchema: {
|
|
698
537
|
type: "object",
|
|
699
538
|
properties: {
|
|
700
|
-
|
|
539
|
+
node_id: { type: "string", description: "Node ID whose delegated sessions should be considered for cleanup." },
|
|
540
|
+
mode: {
|
|
701
541
|
type: "string",
|
|
702
|
-
|
|
542
|
+
enum: ["preserve", "stop", "delete_stopped", "stop_and_delete"],
|
|
543
|
+
description: "preserve = no-op; stop = release process occupancy by stopping live runtimes; delete_stopped = remove completed/stopped records while leaving live runtimes alone; stop_and_delete = stop live runtimes and delete records."
|
|
703
544
|
},
|
|
704
|
-
|
|
705
|
-
type: "
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
daemon_id: {
|
|
709
|
-
type: "string",
|
|
710
|
-
description: "Daemon ID (cloud mode only)."
|
|
545
|
+
session_ids: {
|
|
546
|
+
type: "array",
|
|
547
|
+
items: { type: "string" },
|
|
548
|
+
description: "Optional explicit session IDs to limit cleanup to. When omitted, sessions are matched by node/workspace metadata."
|
|
711
549
|
},
|
|
712
|
-
|
|
550
|
+
dry_run: { type: "boolean", description: "Preview matched/stopped/deleted/skipped session IDs without mutating session-host state." }
|
|
713
551
|
},
|
|
714
|
-
required: ["
|
|
552
|
+
required: ["node_id", "mode"]
|
|
715
553
|
}
|
|
716
554
|
};
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
555
|
+
var ALL_MESH_TOOLS = [
|
|
556
|
+
MESH_STATUS_TOOL,
|
|
557
|
+
MESH_LIST_NODES_TOOL,
|
|
558
|
+
MESH_SEND_TASK_TOOL,
|
|
559
|
+
MESH_READ_CHAT_TOOL,
|
|
560
|
+
MESH_READ_DEBUG_TOOL,
|
|
561
|
+
MESH_LAUNCH_SESSION_TOOL,
|
|
562
|
+
MESH_GIT_STATUS_TOOL,
|
|
563
|
+
MESH_CHECKPOINT_TOOL,
|
|
564
|
+
MESH_APPROVE_TOOL,
|
|
565
|
+
MESH_CLONE_NODE_TOOL,
|
|
566
|
+
MESH_REMOVE_NODE_TOOL,
|
|
567
|
+
MESH_CLEANUP_SESSIONS_TOOL
|
|
568
|
+
];
|
|
569
|
+
async function meshStatus(ctx) {
|
|
570
|
+
await refreshMeshFromDaemon(ctx);
|
|
571
|
+
const { mesh, transport } = ctx;
|
|
572
|
+
const results = [];
|
|
573
|
+
for (const node of mesh.nodes) {
|
|
574
|
+
const entry = {
|
|
575
|
+
nodeId: node.id,
|
|
576
|
+
workspace: node.workspace,
|
|
577
|
+
...getNodeLaunchReadiness(node)
|
|
578
|
+
};
|
|
579
|
+
try {
|
|
580
|
+
if (!isLocalTransport(transport) && node.daemonId) {
|
|
581
|
+
const result = await transport.gitStatus(node.daemonId, node.workspace, false);
|
|
582
|
+
const status = extractGitStatus(result);
|
|
583
|
+
const uncommittedChanges = countUncommittedChanges(status);
|
|
584
|
+
const dirty = isGitStatusDirty(status);
|
|
585
|
+
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
586
|
+
entry.branch = status?.branch;
|
|
587
|
+
entry.isDirty = dirty;
|
|
588
|
+
entry.uncommittedChanges = uncommittedChanges;
|
|
589
|
+
} else if (isLocalTransport(transport)) {
|
|
590
|
+
const statusResult = await commandForNode(ctx, node, "git_status", { workspace: node.workspace });
|
|
591
|
+
const status = extractGitStatus(statusResult);
|
|
592
|
+
const uncommittedChanges = countUncommittedChanges(status);
|
|
593
|
+
const dirty = isGitStatusDirty(status);
|
|
594
|
+
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
595
|
+
entry.branch = status?.branch;
|
|
596
|
+
entry.isDirty = dirty;
|
|
597
|
+
entry.uncommittedChanges = uncommittedChanges;
|
|
598
|
+
} else {
|
|
599
|
+
entry.health = "unknown";
|
|
600
|
+
entry.note = "No daemonId available for cloud status probe";
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
entry.health = "degraded";
|
|
604
|
+
entry.error = e.message;
|
|
605
|
+
}
|
|
606
|
+
const relatedRepos = await collectRelatedRepoStatuses(ctx, node);
|
|
607
|
+
if (relatedRepos.length) entry.relatedRepos = relatedRepos;
|
|
608
|
+
results.push(entry);
|
|
609
|
+
}
|
|
610
|
+
return JSON.stringify({
|
|
611
|
+
meshId: mesh.id,
|
|
612
|
+
meshName: mesh.name,
|
|
613
|
+
repoIdentity: mesh.repoIdentity,
|
|
614
|
+
policy: mesh.policy,
|
|
615
|
+
refreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
616
|
+
nodes: results
|
|
617
|
+
}, null, 2);
|
|
618
|
+
}
|
|
619
|
+
async function meshListNodes(ctx) {
|
|
620
|
+
await refreshMeshFromDaemon(ctx);
|
|
621
|
+
const { mesh } = ctx;
|
|
622
|
+
return JSON.stringify({
|
|
623
|
+
meshId: mesh.id,
|
|
624
|
+
meshName: mesh.name,
|
|
625
|
+
nodes: mesh.nodes.map((n) => ({
|
|
626
|
+
nodeId: n.id,
|
|
627
|
+
workspace: n.workspace,
|
|
628
|
+
repoRoot: n.repoRoot,
|
|
629
|
+
isLocalWorktree: n.isLocalWorktree,
|
|
630
|
+
policy: n.policy,
|
|
631
|
+
relatedRepos: readRelatedRepos(n),
|
|
632
|
+
...getNodeLaunchReadiness(n),
|
|
633
|
+
userOverrides: n.userOverrides
|
|
634
|
+
}))
|
|
635
|
+
}, null, 2);
|
|
636
|
+
}
|
|
637
|
+
async function meshSendTask(ctx, args) {
|
|
638
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
639
|
+
if (node.policy?.readOnly) {
|
|
640
|
+
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
641
|
+
}
|
|
642
|
+
if (isLocalTransport(ctx.transport)) {
|
|
643
|
+
const result = await commandForNode(ctx, node, "send_chat", {
|
|
644
|
+
message: args.message,
|
|
645
|
+
sessionId: args.session_id,
|
|
646
|
+
targetSessionId: args.session_id
|
|
723
647
|
});
|
|
724
|
-
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
648
|
+
const payload = unwrapCommandPayload(result);
|
|
649
|
+
if (payload?.success === false) {
|
|
650
|
+
return JSON.stringify({
|
|
651
|
+
success: false,
|
|
652
|
+
nodeId: args.node_id,
|
|
653
|
+
sessionId: args.session_id,
|
|
654
|
+
error: payload.error || "send_chat failed"
|
|
728
655
|
});
|
|
729
|
-
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
730
656
|
}
|
|
657
|
+
return JSON.stringify({ success: true, nodeId: args.node_id, sessionId: args.session_id });
|
|
731
658
|
} else {
|
|
732
|
-
|
|
733
|
-
const result = await transport.gitStatus(
|
|
734
|
-
args.daemon_id,
|
|
735
|
-
args.workspace,
|
|
736
|
-
args.include_diff !== false
|
|
737
|
-
);
|
|
738
|
-
if (result?.error) {
|
|
739
|
-
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
740
|
-
return `Error: ${result.error}`;
|
|
741
|
-
}
|
|
742
|
-
status = result?.status;
|
|
743
|
-
diffSummary = result?.diff;
|
|
659
|
+
return JSON.stringify({ error: "Cloud mesh send_task not yet implemented" });
|
|
744
660
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
661
|
+
}
|
|
662
|
+
async function meshReadChat(ctx, args) {
|
|
663
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
664
|
+
if (isLocalTransport(ctx.transport)) {
|
|
665
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
666
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
667
|
+
const result = await commandForNode(ctx, node, "read_chat", {
|
|
668
|
+
sessionId: args.session_id,
|
|
669
|
+
targetSessionId: args.session_id,
|
|
670
|
+
workspace: node.workspace,
|
|
671
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
672
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
673
|
+
tailLimit: args.tail ?? 10
|
|
674
|
+
});
|
|
675
|
+
const payload = annotateRapidReadChatAdvisory(unwrapCommandPayload(result), {
|
|
676
|
+
key: `mesh:${args.node_id}:${args.session_id}`,
|
|
677
|
+
toolName: "mesh_read_chat",
|
|
678
|
+
completionCallbackExpected: true
|
|
679
|
+
});
|
|
680
|
+
if (args.compact) {
|
|
681
|
+
const compactPayload = compactChatPayload(payload, {
|
|
682
|
+
nodeId: args.node_id,
|
|
683
|
+
sessionId: args.session_id,
|
|
684
|
+
limit: args.tail ?? 10
|
|
685
|
+
});
|
|
686
|
+
return JSON.stringify(
|
|
687
|
+
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
688
|
+
null,
|
|
689
|
+
2
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
return JSON.stringify(payload, null, 2);
|
|
693
|
+
} else {
|
|
694
|
+
return JSON.stringify({ error: "Cloud mesh read_chat not yet implemented" });
|
|
749
695
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
696
|
+
}
|
|
697
|
+
async function meshReadDebug(ctx, args) {
|
|
698
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
699
|
+
if (isLocalTransport(ctx.transport)) {
|
|
700
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
701
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
702
|
+
const delivery = args.delivery === "inline" ? void 0 : "daemon_file";
|
|
703
|
+
const result = await commandForNode(ctx, node, "get_chat_debug_bundle", {
|
|
704
|
+
sessionId: args.session_id,
|
|
705
|
+
targetSessionId: args.session_id,
|
|
706
|
+
workspace: node.workspace,
|
|
707
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
708
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
709
|
+
tailLimit: args.tail ?? 40,
|
|
710
|
+
...delivery ? { delivery } : {}
|
|
711
|
+
});
|
|
712
|
+
const payload = unwrapCommandPayload(result);
|
|
713
|
+
return JSON.stringify(payload, null, 2);
|
|
753
714
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
715
|
+
return JSON.stringify({ error: "Cloud mesh read_debug not yet implemented" });
|
|
716
|
+
}
|
|
717
|
+
async function meshLaunchSession(ctx, args) {
|
|
718
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
719
|
+
if (isLocalTransport(ctx.transport)) {
|
|
720
|
+
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
721
|
+
if (!resolvedProviderType) {
|
|
722
|
+
const providerPriority = readProviderPriority(node.policy);
|
|
723
|
+
if (!providerPriority.length) {
|
|
724
|
+
return JSON.stringify({ success: false, error: missingProviderPriorityMessage(args.node_id) });
|
|
725
|
+
}
|
|
726
|
+
const failed = [];
|
|
727
|
+
for (const providerType of providerPriority) {
|
|
728
|
+
const detectedResult = await commandForNode(ctx, node, "detect_provider", { providerType });
|
|
729
|
+
const detectedPayload = unwrapCommandPayload(detectedResult);
|
|
730
|
+
if (detectedPayload?.success && detectedPayload?.detected) {
|
|
731
|
+
resolvedProviderType = providerType;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
failed.push(`${providerType}: ${detectedPayload?.error || "not detected"}`);
|
|
735
|
+
}
|
|
736
|
+
if (!resolvedProviderType) {
|
|
737
|
+
return JSON.stringify({ success: false, error: `No usable provider detected for node '${args.node_id}' from providerPriority: ${failed.join("; ")}` });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
741
|
+
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
742
|
+
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
743
|
+
const result = await commandForNode(ctx, node, "launch_cli", {
|
|
744
|
+
cliType: resolvedProviderType,
|
|
745
|
+
dir: node.workspace,
|
|
746
|
+
settings: {
|
|
747
|
+
meshNodeFor: ctx.mesh.id,
|
|
748
|
+
meshNodeId: args.node_id,
|
|
749
|
+
spawnedSessionVisibility,
|
|
750
|
+
...coordinatorDaemonId ? { meshCoordinatorDaemonId: coordinatorDaemonId } : {},
|
|
751
|
+
...coordinatorNode?.id ? { meshCoordinatorNodeId: coordinatorNode.id } : {},
|
|
752
|
+
launchedByCoordinator: true
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
const launchPayload = extractLaunchPayload(result);
|
|
756
|
+
const runtimeSessionId = typeof launchPayload?.sessionId === "string" ? launchPayload.sessionId : typeof launchPayload?.id === "string" ? launchPayload.id : typeof launchPayload?.runtimeSessionId === "string" ? launchPayload.runtimeSessionId : "";
|
|
757
|
+
const providerSessionId = typeof launchPayload?.providerSessionId === "string" && launchPayload.providerSessionId.trim() ? launchPayload.providerSessionId.trim() : void 0;
|
|
758
|
+
if (runtimeSessionId) {
|
|
759
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, runtimeSessionId), {
|
|
760
|
+
providerType: resolvedProviderType,
|
|
761
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return JSON.stringify({
|
|
765
|
+
...launchPayload,
|
|
766
|
+
resolvedProviderType,
|
|
767
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
778
768
|
}, null, 2);
|
|
769
|
+
} else {
|
|
770
|
+
return JSON.stringify({ error: "Cloud mesh launch_session not yet implemented" });
|
|
779
771
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
772
|
+
}
|
|
773
|
+
async function meshGitStatus(ctx, args) {
|
|
774
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
775
|
+
if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
776
|
+
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
|
|
777
|
+
return JSON.stringify({
|
|
778
|
+
nodeId: args.node_id,
|
|
779
|
+
workspace: node.workspace,
|
|
780
|
+
status: extractGitStatus(result),
|
|
781
|
+
diff: extractGitDiff(result),
|
|
782
|
+
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
783
|
+
}, null, 2);
|
|
784
|
+
} else if (isLocalTransport(ctx.transport)) {
|
|
785
|
+
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
786
|
+
workspace: node.workspace
|
|
787
|
+
});
|
|
788
|
+
const diffResult = await commandForNode(ctx, node, "git_diff_summary", {
|
|
789
|
+
workspace: node.workspace
|
|
790
|
+
});
|
|
791
|
+
return JSON.stringify({
|
|
792
|
+
nodeId: args.node_id,
|
|
793
|
+
workspace: node.workspace,
|
|
794
|
+
status: extractGitStatus(statusResult),
|
|
795
|
+
diff: extractGitDiff(diffResult),
|
|
796
|
+
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
797
|
+
}, null, 2);
|
|
798
|
+
} else {
|
|
799
|
+
return JSON.stringify({ error: "No daemonId available for cloud git_status probe" });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function meshCheckpoint(ctx, args) {
|
|
803
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
804
|
+
if (node.policy?.readOnly) {
|
|
805
|
+
return JSON.stringify({ error: `Node '${args.node_id}' is read-only \u2014 cannot checkpoint` });
|
|
806
|
+
}
|
|
807
|
+
if (isLocalTransport(ctx.transport)) {
|
|
808
|
+
const result = await commandForNode(ctx, node, "git_checkpoint", {
|
|
809
|
+
workspace: node.workspace,
|
|
810
|
+
message: args.message,
|
|
811
|
+
includeUntracked: true
|
|
812
|
+
});
|
|
813
|
+
return JSON.stringify(result, null, 2);
|
|
814
|
+
} else {
|
|
815
|
+
return JSON.stringify({ error: "Cloud mesh checkpoint not yet implemented" });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async function meshApprove(ctx, args) {
|
|
819
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
820
|
+
if (isLocalTransport(ctx.transport)) {
|
|
821
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
822
|
+
const providerSessionId = cached?.providerSessionId;
|
|
823
|
+
const result = await commandForNode(ctx, node, "resolve_action", {
|
|
824
|
+
sessionId: args.session_id,
|
|
825
|
+
targetSessionId: args.session_id,
|
|
826
|
+
workspace: node.workspace,
|
|
827
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
828
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
829
|
+
action: args.action === "reject" ? "reject" : "approve"
|
|
830
|
+
});
|
|
831
|
+
return JSON.stringify(result, null, 2);
|
|
832
|
+
} else {
|
|
833
|
+
return JSON.stringify({ error: "Cloud mesh approve not yet implemented" });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function meshCloneNode(ctx, args) {
|
|
837
|
+
const sourceNode = await findNodeWithRefresh(ctx, args.source_node_id);
|
|
838
|
+
if (isLocalTransport(ctx.transport)) {
|
|
839
|
+
const result = await commandForNode(ctx, sourceNode, "clone_mesh_node", {
|
|
840
|
+
meshId: ctx.mesh.id,
|
|
841
|
+
sourceNodeId: args.source_node_id,
|
|
842
|
+
branch: args.branch,
|
|
843
|
+
baseBranch: args.base_branch,
|
|
844
|
+
inlineMesh: ctx.mesh
|
|
845
|
+
});
|
|
846
|
+
const clonePayload = extractCloneNodePayload(result);
|
|
847
|
+
if (clonePayload?.success && clonePayload.node?.id) {
|
|
848
|
+
const existingIndex = ctx.mesh.nodes.findIndex((n) => n.id === clonePayload.node.id);
|
|
849
|
+
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
850
|
+
else ctx.mesh.nodes.push(clonePayload.node);
|
|
851
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
852
|
+
}
|
|
853
|
+
return JSON.stringify(result, null, 2);
|
|
854
|
+
} else {
|
|
855
|
+
return JSON.stringify({ error: "Cloud mesh clone_node not yet implemented" });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function meshCleanupSessions(ctx, args) {
|
|
859
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
860
|
+
if (isLocalTransport(ctx.transport)) {
|
|
861
|
+
const result = await commandForNode(ctx, node, "cleanup_mesh_sessions", {
|
|
862
|
+
meshId: ctx.mesh.id,
|
|
863
|
+
nodeId: args.node_id,
|
|
864
|
+
mode: args.mode,
|
|
865
|
+
sessionIds: args.session_ids,
|
|
866
|
+
dryRun: args.dry_run === true,
|
|
867
|
+
inlineMesh: ctx.mesh
|
|
868
|
+
});
|
|
869
|
+
return JSON.stringify(result, null, 2);
|
|
870
|
+
} else {
|
|
871
|
+
return JSON.stringify({ error: "Cloud mesh cleanup_sessions not yet implemented" });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function meshRemoveNode(ctx, args) {
|
|
875
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
876
|
+
if (isLocalTransport(ctx.transport)) {
|
|
877
|
+
const result = await commandForNode(ctx, node, "remove_mesh_node", {
|
|
878
|
+
meshId: ctx.mesh.id,
|
|
879
|
+
nodeId: args.node_id,
|
|
880
|
+
...args.session_cleanup_mode ? { sessionCleanupMode: args.session_cleanup_mode } : {},
|
|
881
|
+
inlineMesh: ctx.mesh
|
|
882
|
+
});
|
|
883
|
+
if (result?.success && result.removed !== false) {
|
|
884
|
+
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
885
|
+
if (idx >= 0) {
|
|
886
|
+
ctx.mesh.nodes.splice(idx, 1);
|
|
887
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return JSON.stringify(result, null, 2);
|
|
891
|
+
} else {
|
|
892
|
+
return JSON.stringify({ error: "Cloud mesh remove_node not yet implemented" });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/help.ts
|
|
897
|
+
var STANDARD_TOOLS = [
|
|
898
|
+
"list_daemons",
|
|
899
|
+
"list_sessions",
|
|
900
|
+
"launch_session",
|
|
901
|
+
"stop_session",
|
|
902
|
+
"check_pending",
|
|
903
|
+
"read_chat",
|
|
904
|
+
"read_chat_debug",
|
|
905
|
+
"send_chat",
|
|
906
|
+
"approve",
|
|
907
|
+
"git_status",
|
|
908
|
+
"git_log",
|
|
909
|
+
"git_diff",
|
|
910
|
+
"git_checkpoint",
|
|
911
|
+
"git_push",
|
|
912
|
+
"screenshot"
|
|
913
|
+
];
|
|
914
|
+
function buildMcpHelpText() {
|
|
915
|
+
const meshTools = ALL_MESH_TOOLS.map((tool) => tool.name);
|
|
916
|
+
return `
|
|
917
|
+
adhdev-mcp \u2014 ADHDev MCP Server
|
|
918
|
+
|
|
919
|
+
Usage:
|
|
920
|
+
adhdev-mcp Local mode (requires standalone daemon)
|
|
921
|
+
adhdev-mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
922
|
+
adhdev-mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
923
|
+
adhdev-mcp --repo-mesh <mesh_id> Mesh mode (coordinator-scoped tools)
|
|
924
|
+
|
|
925
|
+
Options:
|
|
926
|
+
--mode <mode> Transport: local, cloud, or ipc
|
|
927
|
+
--port <n> Standalone or IPC daemon port (defaults: local 3847, ipc 19222)
|
|
928
|
+
--password <pass> Standalone daemon password (if set)
|
|
929
|
+
--api-key <key> ADHDev cloud API key (switches to cloud mode)
|
|
930
|
+
--base-url <url> Override cloud API base URL
|
|
931
|
+
--repo-mesh <mesh_id> Enable mesh mode \u2014 exposes only mesh-scoped coordinator tools
|
|
932
|
+
--help Show this help
|
|
933
|
+
|
|
934
|
+
Environment variables:
|
|
935
|
+
ADHDEV_API_KEY API key (cloud mode)
|
|
936
|
+
ADHDEV_PASSWORD Daemon password (local mode)
|
|
937
|
+
ADHDEV_MESH_ID Mesh ID (mesh mode)
|
|
938
|
+
ADHDEV_MCP_TRANSPORT Transport: local, cloud, or ipc
|
|
939
|
+
|
|
940
|
+
Standard tools: ${STANDARD_TOOLS.join(", ")}
|
|
941
|
+
Mesh tools: ${meshTools.join(", ")}
|
|
942
|
+
`.trim();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/server.ts
|
|
946
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
947
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
948
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
949
|
+
|
|
950
|
+
// src/transports/local.ts
|
|
951
|
+
var DEFAULT_PORT = 3847;
|
|
952
|
+
var LocalTransport = class {
|
|
953
|
+
baseUrl;
|
|
954
|
+
authHeader;
|
|
955
|
+
constructor(opts = {}) {
|
|
956
|
+
this.baseUrl = `http://localhost:${opts.port ?? DEFAULT_PORT}`;
|
|
957
|
+
this.authHeader = opts.password ? `Bearer ${opts.password}` : null;
|
|
958
|
+
}
|
|
959
|
+
headers() {
|
|
960
|
+
const h = { "Content-Type": "application/json" };
|
|
961
|
+
if (this.authHeader) h["Authorization"] = this.authHeader;
|
|
962
|
+
return h;
|
|
963
|
+
}
|
|
964
|
+
async getStatus() {
|
|
965
|
+
const res = await fetch(`${this.baseUrl}/api/v1/status`, { headers: this.headers() });
|
|
966
|
+
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
967
|
+
return res.json();
|
|
968
|
+
}
|
|
969
|
+
async command(type, args = {}) {
|
|
970
|
+
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
971
|
+
method: "POST",
|
|
972
|
+
headers: this.headers(),
|
|
973
|
+
body: JSON.stringify({ type, ...args })
|
|
974
|
+
});
|
|
975
|
+
if (!res.ok) {
|
|
976
|
+
const text = await res.text().catch(() => res.statusText);
|
|
977
|
+
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
978
|
+
}
|
|
979
|
+
return res.json();
|
|
980
|
+
}
|
|
981
|
+
async ping() {
|
|
982
|
+
try {
|
|
983
|
+
await this.getStatus();
|
|
984
|
+
return true;
|
|
985
|
+
} catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/transports/cloud.ts
|
|
992
|
+
var DEFAULT_BASE_URL = "https://api.adhf.dev";
|
|
993
|
+
var CloudTransport = class {
|
|
994
|
+
baseUrl;
|
|
995
|
+
apiKey;
|
|
996
|
+
constructor(opts) {
|
|
997
|
+
this.apiKey = opts.apiKey;
|
|
998
|
+
this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
999
|
+
}
|
|
1000
|
+
headers() {
|
|
1001
|
+
return {
|
|
1002
|
+
"Content-Type": "application/json",
|
|
1003
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
async listDaemons() {
|
|
1007
|
+
const res = await fetch(`${this.baseUrl}/api/v1/daemons`, { headers: this.headers() });
|
|
1008
|
+
if (!res.ok) throw new Error(`List daemons failed: ${res.status}`);
|
|
1009
|
+
return res.json();
|
|
1010
|
+
}
|
|
1011
|
+
async getStatus(targetId) {
|
|
1012
|
+
const res = await fetch(
|
|
1013
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/status`,
|
|
1014
|
+
{ headers: this.headers() }
|
|
1015
|
+
);
|
|
1016
|
+
if (!res.ok) throw new Error(`Status failed: ${res.status}`);
|
|
1017
|
+
return res.json();
|
|
1018
|
+
}
|
|
1019
|
+
/** Get all sessions for a daemon (returns CompactSessionEntry[]). */
|
|
1020
|
+
async getDaemonStatus(daemonId) {
|
|
1021
|
+
const res = await fetch(
|
|
1022
|
+
`${this.baseUrl}/api/v1/daemons/${encodeURIComponent(daemonId)}/status`,
|
|
1023
|
+
{ headers: this.headers() }
|
|
1024
|
+
);
|
|
1025
|
+
if (!res.ok) throw new Error(`Daemon status failed: ${res.status}`);
|
|
1026
|
+
return res.json();
|
|
1027
|
+
}
|
|
1028
|
+
async readChat(targetId, opts = {}) {
|
|
1029
|
+
const params = new URLSearchParams();
|
|
1030
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
1031
|
+
if (opts.sessionId) params.set("sessionId", opts.sessionId);
|
|
1032
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
1033
|
+
const res = await fetch(
|
|
1034
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat${qs}`,
|
|
1035
|
+
{ headers: this.headers() }
|
|
1036
|
+
);
|
|
1037
|
+
if (!res.ok) throw new Error(`Read chat failed: ${res.status}`);
|
|
1038
|
+
return res.json();
|
|
1039
|
+
}
|
|
1040
|
+
async getChatDebugBundle(targetId, opts = {}) {
|
|
1041
|
+
const res = await fetch(
|
|
1042
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat/debug`,
|
|
1043
|
+
{
|
|
1044
|
+
method: "POST",
|
|
1045
|
+
headers: this.headers(),
|
|
1046
|
+
body: JSON.stringify({
|
|
1047
|
+
...opts.agentType ? { agentType: opts.agentType } : {},
|
|
1048
|
+
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
1049
|
+
...opts.tailLimit ? { tailLimit: opts.tailLimit } : {},
|
|
1050
|
+
...opts.delivery ? { delivery: opts.delivery } : {}
|
|
1051
|
+
})
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
if (!res.ok) throw new Error(`Chat debug bundle failed: ${res.status}`);
|
|
1055
|
+
return res.json();
|
|
1056
|
+
}
|
|
1057
|
+
async sendChat(targetId, message, opts = {}) {
|
|
1058
|
+
const res = await fetch(
|
|
1059
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat`,
|
|
1060
|
+
{
|
|
1061
|
+
method: "POST",
|
|
1062
|
+
headers: this.headers(),
|
|
1063
|
+
body: JSON.stringify({ message, ...opts })
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
if (!res.ok) throw new Error(`Send chat failed: ${res.status}`);
|
|
1067
|
+
return res.json();
|
|
1068
|
+
}
|
|
1069
|
+
async approve(targetId, action, agentType) {
|
|
1070
|
+
const res = await fetch(
|
|
1071
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/approve`,
|
|
1072
|
+
{
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: this.headers(),
|
|
1075
|
+
body: JSON.stringify({ action, ...agentType ? { agentType } : {} })
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
1079
|
+
return res.json();
|
|
1080
|
+
}
|
|
1081
|
+
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
1082
|
+
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
1083
|
+
const res = await fetch(
|
|
1084
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
1085
|
+
{ headers: this.headers() }
|
|
1086
|
+
);
|
|
1087
|
+
if (!res.ok) throw new Error(`Git status failed: ${res.status}`);
|
|
1088
|
+
return res.json();
|
|
1089
|
+
}
|
|
1090
|
+
async stop(daemonId, opts) {
|
|
1091
|
+
const res = await fetch(
|
|
1092
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/stop`,
|
|
1093
|
+
{
|
|
1094
|
+
method: "POST",
|
|
1095
|
+
headers: this.headers(),
|
|
1096
|
+
body: JSON.stringify(opts)
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
if (!res.ok) throw new Error(`Stop failed: ${res.status}`);
|
|
1100
|
+
return res.json();
|
|
1101
|
+
}
|
|
1102
|
+
async launch(daemonId, opts) {
|
|
1103
|
+
const res = await fetch(
|
|
1104
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/launch`,
|
|
1105
|
+
{
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
headers: this.headers(),
|
|
1108
|
+
body: JSON.stringify(opts)
|
|
1109
|
+
}
|
|
1110
|
+
);
|
|
1111
|
+
if (!res.ok) throw new Error(`Launch failed: ${res.status}`);
|
|
1112
|
+
return res.json();
|
|
1113
|
+
}
|
|
1114
|
+
async gitLog(daemonId, workspace, opts = {}) {
|
|
1115
|
+
const params = new URLSearchParams({ workspace });
|
|
1116
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
1117
|
+
if (opts.file) params.set("file", opts.file);
|
|
1118
|
+
if (opts.since) params.set("since", opts.since);
|
|
1119
|
+
if (opts.until) params.set("until", opts.until);
|
|
1120
|
+
const res = await fetch(
|
|
1121
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-log?${params}`,
|
|
1122
|
+
{ headers: this.headers() }
|
|
1123
|
+
);
|
|
1124
|
+
if (!res.ok) throw new Error(`Git log failed: ${res.status}`);
|
|
1125
|
+
return res.json();
|
|
1126
|
+
}
|
|
1127
|
+
async gitDiff(daemonId, workspace, opts = {}) {
|
|
1128
|
+
const params = new URLSearchParams({ workspace });
|
|
1129
|
+
if (opts.file) params.set("file", opts.file);
|
|
1130
|
+
if (opts.maxLines) params.set("maxLines", String(opts.maxLines));
|
|
1131
|
+
if (opts.staged) params.set("staged", "true");
|
|
1132
|
+
const res = await fetch(
|
|
1133
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-diff?${params}`,
|
|
1134
|
+
{ headers: this.headers() }
|
|
1135
|
+
);
|
|
1136
|
+
if (!res.ok) throw new Error(`Git diff failed: ${res.status}`);
|
|
1137
|
+
return res.json();
|
|
1138
|
+
}
|
|
1139
|
+
async gitPush(daemonId, opts) {
|
|
1140
|
+
const res = await fetch(
|
|
1141
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-push`,
|
|
1142
|
+
{
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: this.headers(),
|
|
1145
|
+
body: JSON.stringify(opts)
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
if (!res.ok) throw new Error(`Git push failed: ${res.status}`);
|
|
1149
|
+
return res.json();
|
|
1150
|
+
}
|
|
1151
|
+
async gitCheckpoint(daemonId, opts) {
|
|
1152
|
+
const res = await fetch(
|
|
1153
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-checkpoint`,
|
|
1154
|
+
{
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
headers: this.headers(),
|
|
1157
|
+
body: JSON.stringify(opts)
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
if (!res.ok) throw new Error(`Git checkpoint failed: ${res.status}`);
|
|
1161
|
+
return res.json();
|
|
1162
|
+
}
|
|
1163
|
+
async ping() {
|
|
1164
|
+
try {
|
|
1165
|
+
await this.listDaemons();
|
|
1166
|
+
return true;
|
|
1167
|
+
} catch {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
// src/tools/list-sessions.ts
|
|
1174
|
+
var FORMAT_PROP = {
|
|
1175
|
+
format: {
|
|
1176
|
+
type: "string",
|
|
1177
|
+
enum: ["text", "json"],
|
|
1178
|
+
description: "Output format: 'text' (default, human-readable) or 'json' (structured, for programmatic use)."
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
var LIST_SESSIONS_TOOL = {
|
|
1182
|
+
name: "list_sessions",
|
|
1183
|
+
description: "List all connected agent sessions. In cloud mode, fetches session state from each daemon (data is sourced from daemon WS status reports, up to 30s stale). Pass daemon_id to scope to a single daemon.",
|
|
1184
|
+
inputSchema: {
|
|
1185
|
+
type: "object",
|
|
1186
|
+
properties: {
|
|
1187
|
+
daemon_id: {
|
|
1188
|
+
type: "string",
|
|
1189
|
+
description: "Daemon ID (cloud mode only). Omit to list sessions across all daemons."
|
|
1190
|
+
},
|
|
1191
|
+
...FORMAT_PROP
|
|
1192
|
+
},
|
|
1193
|
+
required: []
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
async function listSessions(transport, args = {}) {
|
|
1197
|
+
const asJson = args.format === "json";
|
|
1198
|
+
if (isLocalTransport(transport)) {
|
|
1199
|
+
const status = await transport.getStatus();
|
|
1200
|
+
const sessions = status?.sessions ?? [];
|
|
1201
|
+
if (asJson) {
|
|
1202
|
+
return JSON.stringify({
|
|
1203
|
+
sessions: sessions.map((s) => ({
|
|
1204
|
+
id: s.id,
|
|
1205
|
+
type: s.providerType ?? s.type ?? "unknown",
|
|
1206
|
+
label: s.label ?? null,
|
|
1207
|
+
status: s.status ?? s.agentStatus ?? null,
|
|
1208
|
+
workspace: s.workspace ?? null
|
|
1209
|
+
}))
|
|
1210
|
+
}, null, 2);
|
|
1211
|
+
}
|
|
1212
|
+
if (sessions.length === 0) return "No active sessions.";
|
|
1213
|
+
const lines = sessions.map((s) => {
|
|
1214
|
+
const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
|
|
1215
|
+
if (s.label) parts.push(`label: ${s.label}`);
|
|
1216
|
+
if (s.status ?? s.agentStatus) parts.push(`status: ${s.status ?? s.agentStatus}`);
|
|
1217
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
1218
|
+
return parts.join(", ");
|
|
1219
|
+
});
|
|
1220
|
+
return `Sessions (${sessions.length}):
|
|
1221
|
+
${lines.join("\n")}`;
|
|
784
1222
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (!status.dirty) lines.push("Working tree: clean");
|
|
794
|
-
if (diffSummary?.files?.length > 0) {
|
|
795
|
-
lines.push("");
|
|
796
|
-
lines.push(`Changed files (${diffSummary.files.length}):`);
|
|
797
|
-
for (const f of diffSummary.files.slice(0, 20)) {
|
|
798
|
-
lines.push(` ${f.status ?? "M"} ${f.path}${f.oldPath ? ` (was ${f.oldPath})` : ""}${f.insertions || f.deletions ? ` +${f.insertions ?? 0}/-${f.deletions ?? 0}` : ""}`);
|
|
1223
|
+
return listSessionsCloud(transport, args.daemon_id, asJson);
|
|
1224
|
+
}
|
|
1225
|
+
async function listSessionsCloud(transport, daemonId, asJson) {
|
|
1226
|
+
const collected = [];
|
|
1227
|
+
if (daemonId) {
|
|
1228
|
+
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
1229
|
+
for (const s of daemonStatus?.sessions ?? []) {
|
|
1230
|
+
collected.push({ daemonId, session: s });
|
|
799
1231
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1232
|
+
} else {
|
|
1233
|
+
const data = await transport.listDaemons();
|
|
1234
|
+
const daemons = data?.daemons ?? [];
|
|
1235
|
+
for (let i = 0; i < daemons.length; i += 5) {
|
|
1236
|
+
await Promise.allSettled(
|
|
1237
|
+
daemons.slice(i, i + 5).map(async (d) => {
|
|
1238
|
+
try {
|
|
1239
|
+
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
1240
|
+
for (const s of daemonStatus?.sessions ?? []) {
|
|
1241
|
+
collected.push({ daemonId: d.id, session: s });
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
})
|
|
1246
|
+
);
|
|
803
1247
|
}
|
|
804
1248
|
}
|
|
805
|
-
|
|
1249
|
+
if (asJson) {
|
|
1250
|
+
return JSON.stringify({
|
|
1251
|
+
sessions: collected.map(({ daemonId: dId, session: s }) => ({
|
|
1252
|
+
daemon_id: dId,
|
|
1253
|
+
id: s.id,
|
|
1254
|
+
type: s.providerType ?? "unknown",
|
|
1255
|
+
status: s.status ?? null,
|
|
1256
|
+
workspace: s.workspace ?? null
|
|
1257
|
+
}))
|
|
1258
|
+
}, null, 2);
|
|
1259
|
+
}
|
|
1260
|
+
if (collected.length === 0) return "No active sessions.";
|
|
1261
|
+
const lines = collected.map(({ daemonId: dId, session: s }) => {
|
|
1262
|
+
const parts = [
|
|
1263
|
+
`daemon: ${dId}`,
|
|
1264
|
+
`session: ${s.id}`,
|
|
1265
|
+
`type: ${s.providerType ?? "unknown"}`
|
|
1266
|
+
];
|
|
1267
|
+
if (s.status) parts.push(`status: ${s.status}`);
|
|
1268
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
1269
|
+
return parts.join(", ");
|
|
1270
|
+
});
|
|
1271
|
+
return `Sessions (${collected.length}):
|
|
1272
|
+
${lines.join("\n")}`;
|
|
806
1273
|
}
|
|
807
1274
|
|
|
808
|
-
// src/tools/
|
|
809
|
-
var
|
|
810
|
-
name: "
|
|
811
|
-
description: "
|
|
1275
|
+
// src/tools/list-daemons.ts
|
|
1276
|
+
var LIST_DAEMONS_TOOL = {
|
|
1277
|
+
name: "list_daemons",
|
|
1278
|
+
description: "List all connected daemons (machines running the ADHDev agent). Use this to discover daemon IDs before calling launch_session, git_status, or other tools that require daemon_id. In local mode returns the single standalone daemon info.",
|
|
812
1279
|
inputSchema: {
|
|
813
1280
|
type: "object",
|
|
814
1281
|
properties: {
|
|
815
|
-
|
|
1282
|
+
...FORMAT_PROP
|
|
1283
|
+
},
|
|
1284
|
+
required: []
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
async function listDaemons(transport, args = {}) {
|
|
1288
|
+
const asJson = args.format === "json";
|
|
1289
|
+
if (isLocalTransport(transport)) {
|
|
1290
|
+
const status = await transport.getStatus();
|
|
1291
|
+
const daemon = {
|
|
1292
|
+
id: status?.id ?? status?.instanceId ?? "standalone",
|
|
1293
|
+
hostname: status?.hostname ?? status?.machine?.hostname ?? "localhost",
|
|
1294
|
+
platform: status?.platform ?? status?.machine?.platform ?? "unknown",
|
|
1295
|
+
version: status?.version ?? null,
|
|
1296
|
+
sessions: (status?.sessions ?? []).length
|
|
1297
|
+
};
|
|
1298
|
+
if (asJson) return JSON.stringify({ daemons: [daemon] }, null, 2);
|
|
1299
|
+
return `Daemons (1):
|
|
1300
|
+
id: ${daemon.id}, hostname: ${daemon.hostname}, platform: ${daemon.platform}${daemon.version ? `, version: ${daemon.version}` : ""}, sessions: ${daemon.sessions}`;
|
|
1301
|
+
}
|
|
1302
|
+
const data = await transport.listDaemons();
|
|
1303
|
+
const daemons = data?.daemons ?? [];
|
|
1304
|
+
if (asJson) {
|
|
1305
|
+
return JSON.stringify({
|
|
1306
|
+
daemons: daemons.map((d) => ({
|
|
1307
|
+
id: d.id,
|
|
1308
|
+
hostname: d.hostname ?? null,
|
|
1309
|
+
platform: d.platform ?? null,
|
|
1310
|
+
nickname: d.nickname ?? null,
|
|
1311
|
+
version: d.version ?? null,
|
|
1312
|
+
p2p_available: d.p2p?.available ?? null,
|
|
1313
|
+
cdp_connected: d.cdpConnected ?? null
|
|
1314
|
+
}))
|
|
1315
|
+
}, null, 2);
|
|
1316
|
+
}
|
|
1317
|
+
if (daemons.length === 0) return "No connected daemons.";
|
|
1318
|
+
const lines = daemons.map((d) => {
|
|
1319
|
+
const parts = [`id: ${d.id}`];
|
|
1320
|
+
if (d.nickname) parts.push(`nickname: ${d.nickname}`);
|
|
1321
|
+
if (d.hostname) parts.push(`hostname: ${d.hostname}`);
|
|
1322
|
+
if (d.platform) parts.push(`platform: ${d.platform}`);
|
|
1323
|
+
if (d.version) parts.push(`version: ${d.version}`);
|
|
1324
|
+
if (d.p2p?.available != null) parts.push(`p2p: ${d.p2p.available ? "yes" : "no"}`);
|
|
1325
|
+
return parts.join(", ");
|
|
1326
|
+
});
|
|
1327
|
+
return `Daemons (${daemons.length}):
|
|
1328
|
+
${lines.join("\n")}`;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/tools/read-chat.ts
|
|
1332
|
+
var READ_CHAT_TOOL = {
|
|
1333
|
+
name: "read_chat",
|
|
1334
|
+
description: "Read the current chat conversation from an IDE agent session. Returns recent messages.",
|
|
1335
|
+
inputSchema: {
|
|
1336
|
+
type: "object",
|
|
1337
|
+
properties: {
|
|
1338
|
+
session_id: {
|
|
816
1339
|
type: "string",
|
|
817
|
-
description: "
|
|
1340
|
+
description: "Target session ID (from list_sessions). Pass explicitly in local mode when more than one session exists; omitting requires an active target and may fail."
|
|
818
1341
|
},
|
|
819
1342
|
limit: {
|
|
820
1343
|
type: "number",
|
|
821
|
-
description: "Max
|
|
822
|
-
},
|
|
823
|
-
file: {
|
|
824
|
-
type: "string",
|
|
825
|
-
description: "Filter history to commits that touched this repo-relative file path (optional)."
|
|
826
|
-
},
|
|
827
|
-
since: {
|
|
828
|
-
type: "string",
|
|
829
|
-
description: "Only commits after this date (ISO 8601 or git date string, optional)."
|
|
830
|
-
},
|
|
831
|
-
until: {
|
|
832
|
-
type: "string",
|
|
833
|
-
description: "Only commits before this date (ISO 8601 or git date string, optional)."
|
|
1344
|
+
description: "Max messages to return (default: 50)."
|
|
834
1345
|
},
|
|
835
1346
|
daemon_id: {
|
|
836
1347
|
type: "string",
|
|
837
|
-
description: "Daemon ID (cloud mode only
|
|
1348
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
1349
|
+
},
|
|
1350
|
+
compact: {
|
|
1351
|
+
type: "boolean",
|
|
1352
|
+
description: "Opt-in compact mode: filters tool/terminal/system/internal/control/debug/status chatter and returns user-visible messages plus lightweight summary metadata."
|
|
838
1353
|
},
|
|
839
1354
|
...FORMAT_PROP
|
|
840
1355
|
},
|
|
841
|
-
required: [
|
|
1356
|
+
required: []
|
|
842
1357
|
}
|
|
843
1358
|
};
|
|
844
|
-
async function
|
|
845
|
-
const limit =
|
|
846
|
-
let raw;
|
|
1359
|
+
async function readChat(transport, args) {
|
|
1360
|
+
const limit = args.limit ?? 50;
|
|
847
1361
|
if (isLocalTransport(transport)) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
limit
|
|
851
|
-
...args.file ? { path: args.file } : {},
|
|
852
|
-
...args.since ? { since: args.since } : {},
|
|
853
|
-
...args.until ? { until: args.until } : {}
|
|
1362
|
+
const result2 = await transport.command("read_chat", {
|
|
1363
|
+
...args.session_id ? { targetSessionId: args.session_id } : {},
|
|
1364
|
+
tailLimit: limit
|
|
854
1365
|
});
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
limit,
|
|
860
|
-
file: args.file,
|
|
861
|
-
since: args.since,
|
|
862
|
-
until: args.until
|
|
1366
|
+
const annotated2 = annotateRapidReadChatAdvisory(result2, {
|
|
1367
|
+
key: `local:${args.session_id ?? "__active__"}`,
|
|
1368
|
+
toolName: "read_chat",
|
|
1369
|
+
completionCallbackExpected: false
|
|
863
1370
|
});
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
if (raw?.success === false || raw?.reason) {
|
|
867
|
-
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
868
|
-
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
869
|
-
return `Git log error: ${msg}`;
|
|
1371
|
+
return formatChatResult(annotated2, args.session_id, args.format, limit, args.compact);
|
|
870
1372
|
}
|
|
871
|
-
if (!
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1373
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1374
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1375
|
+
const result = await transport.readChat(targetId, { limit, sessionId: args.session_id });
|
|
1376
|
+
const annotated = annotateRapidReadChatAdvisory(result, {
|
|
1377
|
+
key: `cloud:${args.daemon_id}:${args.session_id ?? "__active__"}`,
|
|
1378
|
+
toolName: "read_chat",
|
|
1379
|
+
completionCallbackExpected: false
|
|
1380
|
+
});
|
|
1381
|
+
return formatChatResult(annotated, args.session_id, args.format, limit, args.compact);
|
|
1382
|
+
}
|
|
1383
|
+
function formatChatResult(result, sessionId, format, limit = 50, compact = false) {
|
|
1384
|
+
if (!result?.success && result?.error) {
|
|
1385
|
+
if (format === "json") return JSON.stringify({ error: result.error, messages: [] }, null, 2);
|
|
1386
|
+
return `Error: ${result.error}`;
|
|
875
1387
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
1388
|
+
const messages = result?.messages ?? result?.data?.messages ?? [];
|
|
1389
|
+
const source = { ...result, messages };
|
|
1390
|
+
const compactPayload = compact ? compactChatPayload(source, { sessionId: sessionId ?? null, limit }) : null;
|
|
1391
|
+
const outputMessages = compact ? compactPayload.messages : messages;
|
|
1392
|
+
if (format === "json") {
|
|
1393
|
+
if (compact && compactPayload) {
|
|
1394
|
+
return JSON.stringify({
|
|
1395
|
+
session_id: sessionId ?? null,
|
|
1396
|
+
...compactPayload,
|
|
1397
|
+
...result?.pollingAdvisory ? { pollingAdvisory: result.pollingAdvisory } : {},
|
|
1398
|
+
messages: compactPayload.messages.map((m) => ({
|
|
1399
|
+
role: m.role,
|
|
1400
|
+
kind: m.kind ?? null,
|
|
1401
|
+
content: messageContent(m),
|
|
1402
|
+
timestamp: m.timestamp ?? null
|
|
1403
|
+
}))
|
|
1404
|
+
}, null, 2);
|
|
1405
|
+
}
|
|
878
1406
|
return JSON.stringify({
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
authored_at: e.authoredAt ? new Date(e.authoredAt).toISOString() : null
|
|
888
|
-
})),
|
|
889
|
-
total: entries.length,
|
|
890
|
-
truncated: raw.truncated ?? false
|
|
1407
|
+
session_id: sessionId ?? null,
|
|
1408
|
+
...result?.pollingAdvisory ? { pollingAdvisory: result.pollingAdvisory } : {},
|
|
1409
|
+
messages: outputMessages.slice(-limit).map((m) => ({
|
|
1410
|
+
role: m.role,
|
|
1411
|
+
kind: m.kind ?? null,
|
|
1412
|
+
content: messageContent(m),
|
|
1413
|
+
timestamp: m.timestamp ?? null
|
|
1414
|
+
}))
|
|
891
1415
|
}, null, 2);
|
|
892
1416
|
}
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1417
|
+
if (outputMessages.length === 0) {
|
|
1418
|
+
return result?.pollingAdvisory ? `No messages in chat.
|
|
1419
|
+
|
|
1420
|
+
Advisory: ${result.pollingAdvisory.message}` : "No messages in chat.";
|
|
1421
|
+
}
|
|
1422
|
+
const lines = outputMessages.slice(-limit).map((m) => {
|
|
1423
|
+
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
1424
|
+
const content = messageContent(m);
|
|
1425
|
+
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
1426
|
+
return `[${role}] ${truncated}`;
|
|
899
1427
|
});
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1428
|
+
if (result?.pollingAdvisory) {
|
|
1429
|
+
lines.push(`Advisory: ${result.pollingAdvisory.message}`);
|
|
1430
|
+
}
|
|
1431
|
+
return lines.join("\n\n");
|
|
903
1432
|
}
|
|
904
1433
|
|
|
905
|
-
// src/tools/
|
|
906
|
-
var
|
|
907
|
-
name: "
|
|
908
|
-
description: "
|
|
1434
|
+
// src/tools/read-chat-debug.ts
|
|
1435
|
+
var READ_CHAT_DEBUG_TOOL = {
|
|
1436
|
+
name: "read_chat_debug",
|
|
1437
|
+
description: "Collect a daemon-side chat/parser debug bundle for an agent session without opening the browser UI. Prefer this when terminal/chat diverge or long CLI transcripts parse incorrectly. Defaults to daemon_file delivery and returns a saved bundle locator.",
|
|
909
1438
|
inputSchema: {
|
|
910
1439
|
type: "object",
|
|
911
1440
|
properties: {
|
|
912
|
-
|
|
1441
|
+
session_id: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
description: "Target session ID (from list_sessions). Required for reliable routing."
|
|
1444
|
+
},
|
|
1445
|
+
daemon_id: {
|
|
913
1446
|
type: "string",
|
|
914
|
-
description: "
|
|
1447
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
915
1448
|
},
|
|
916
|
-
|
|
1449
|
+
agent_type: {
|
|
917
1450
|
type: "string",
|
|
918
|
-
description: "
|
|
1451
|
+
description: "Optional provider/agent type hint, e.g. hermes-cli, claude-cli, codex-cli."
|
|
919
1452
|
},
|
|
920
|
-
|
|
1453
|
+
limit: {
|
|
921
1454
|
type: "number",
|
|
922
|
-
description: "Max
|
|
923
|
-
},
|
|
924
|
-
staged: {
|
|
925
|
-
type: "boolean",
|
|
926
|
-
description: "Show staged changes instead of unstaged (default: false)."
|
|
1455
|
+
description: "Max read_chat tail messages embedded in the bundle (default: 40)."
|
|
927
1456
|
},
|
|
928
|
-
|
|
1457
|
+
delivery: {
|
|
929
1458
|
type: "string",
|
|
930
|
-
|
|
1459
|
+
enum: ["daemon_file", "inline"],
|
|
1460
|
+
description: "daemon_file saves the full sanitized bundle on the daemon and returns a locator; inline returns the sanitized bundle in the MCP response. Default: daemon_file."
|
|
931
1461
|
},
|
|
932
1462
|
...FORMAT_PROP
|
|
933
1463
|
},
|
|
934
|
-
required: ["
|
|
1464
|
+
required: ["session_id"]
|
|
935
1465
|
}
|
|
936
1466
|
};
|
|
937
|
-
async function
|
|
938
|
-
const
|
|
939
|
-
|
|
1467
|
+
async function readChatDebug(transport, args) {
|
|
1468
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
1469
|
+
if (!sessionId) throw new Error("session_id is required");
|
|
1470
|
+
const tailLimit = args.limit ?? 40;
|
|
1471
|
+
const delivery = args.delivery === "inline" ? "inline" : "daemon_file";
|
|
1472
|
+
const commandArgs = {
|
|
1473
|
+
targetSessionId: sessionId,
|
|
1474
|
+
tailLimit,
|
|
1475
|
+
...args.agent_type ? { agentType: args.agent_type, providerType: args.agent_type } : {},
|
|
1476
|
+
...delivery === "daemon_file" ? { delivery: "daemon_file" } : {}
|
|
1477
|
+
};
|
|
1478
|
+
let result;
|
|
940
1479
|
if (isLocalTransport(transport)) {
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return `Git diff error: ${result.error}`;
|
|
1480
|
+
result = await transport.command("get_chat_debug_bundle", commandArgs);
|
|
1481
|
+
} else {
|
|
1482
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1483
|
+
const targetId = `${args.daemon_id}:session:${sessionId}`;
|
|
1484
|
+
result = await transport.getChatDebugBundle(targetId, {
|
|
1485
|
+
sessionId,
|
|
1486
|
+
agentType: args.agent_type,
|
|
1487
|
+
tailLimit,
|
|
1488
|
+
delivery
|
|
1489
|
+
});
|
|
952
1490
|
}
|
|
953
|
-
return
|
|
1491
|
+
return formatChatDebugResult(result, { sessionId, delivery, format: args.format });
|
|
954
1492
|
}
|
|
955
|
-
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (d?.success === false || d?.reason) {
|
|
960
|
-
const msg = d?.error ?? d?.reason ?? "unknown";
|
|
961
|
-
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
962
|
-
return `Git diff error: ${msg}`;
|
|
963
|
-
}
|
|
964
|
-
const lines = (d?.diff ?? "").split("\n");
|
|
965
|
-
const truncated = lines.length > maxLines;
|
|
966
|
-
const result = {
|
|
967
|
-
files: [{
|
|
968
|
-
path: file,
|
|
969
|
-
diff: truncated ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
|
|
970
|
-
truncated,
|
|
971
|
-
binary: d?.binary ?? false
|
|
972
|
-
}],
|
|
973
|
-
total_files: 1,
|
|
974
|
-
shown_files: 1,
|
|
975
|
-
truncated
|
|
976
|
-
};
|
|
977
|
-
return formatDiffResult(result, format);
|
|
978
|
-
}
|
|
979
|
-
const summaryRaw = await transport.command("git_diff_summary", { workspace, staged });
|
|
980
|
-
const summary = summaryRaw?.diffSummary ?? summaryRaw;
|
|
981
|
-
if (summary?.success === false || summary?.reason) {
|
|
982
|
-
const msg = summary?.error ?? summary?.reason ?? "unknown";
|
|
983
|
-
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
984
|
-
return `Git diff error: ${msg}`;
|
|
985
|
-
}
|
|
986
|
-
if (!summary?.isGitRepo) {
|
|
987
|
-
const msg = `Not a git repository: ${workspace}`;
|
|
988
|
-
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
989
|
-
return msg;
|
|
1493
|
+
function formatChatDebugResult(result, options) {
|
|
1494
|
+
if (!result?.success && result?.error) {
|
|
1495
|
+
if (options.format === "json") return JSON.stringify({ success: false, error: result.error }, null, 2);
|
|
1496
|
+
return `Error: ${result.error}`;
|
|
990
1497
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (format === "json") return JSON.stringify({ files: [], total_files: 0, shown_files: 0, truncated: false }, null, 2);
|
|
994
|
-
return "No changed files.";
|
|
1498
|
+
if (options.format === "json") {
|
|
1499
|
+
return JSON.stringify(result, null, 2);
|
|
995
1500
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1015
|
-
})
|
|
1016
|
-
);
|
|
1017
|
-
return formatDiffResult({
|
|
1018
|
-
files: fileDiffs,
|
|
1019
|
-
total_files: files.length,
|
|
1020
|
-
shown_files: topFiles.length,
|
|
1021
|
-
truncated: files.length > 5
|
|
1022
|
-
}, format);
|
|
1501
|
+
if (result?.delivery === "daemon_file") {
|
|
1502
|
+
const summary = result.summary && typeof result.summary === "object" ? result.summary : {};
|
|
1503
|
+
return [
|
|
1504
|
+
"ADHDev chat debug bundle saved on daemon.",
|
|
1505
|
+
`session_id: ${options.sessionId}`,
|
|
1506
|
+
`bundle_id: ${String(result.bundleId || "")}`,
|
|
1507
|
+
`saved_path: ${String(result.savedPath || "")}`,
|
|
1508
|
+
`size_bytes: ${String(result.sizeBytes || "")}`,
|
|
1509
|
+
`created_at: ${String(result.createdAt || "")}`,
|
|
1510
|
+
`read_chat_status: ${String(summary.readChatStatus || "")}`,
|
|
1511
|
+
`read_chat_total_messages: ${String(summary.readChatTotalMessages ?? "")}`,
|
|
1512
|
+
`cli_status: ${String(summary.cliStatus || "")}`,
|
|
1513
|
+
`cli_message_count: ${String(summary.cliMessageCount ?? "")}`
|
|
1514
|
+
].join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof result?.text === "string") return result.text;
|
|
1517
|
+
if (result?.bundle) return JSON.stringify(result.bundle, null, 2);
|
|
1518
|
+
return JSON.stringify(result, null, 2);
|
|
1023
1519
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1520
|
+
|
|
1521
|
+
// src/tools/send-chat.ts
|
|
1522
|
+
var SEND_CHAT_TOOL = {
|
|
1523
|
+
name: "send_chat",
|
|
1524
|
+
description: "Send a message to an IDE agent session.",
|
|
1525
|
+
inputSchema: {
|
|
1526
|
+
type: "object",
|
|
1527
|
+
properties: {
|
|
1528
|
+
message: {
|
|
1529
|
+
type: "string",
|
|
1530
|
+
description: "The message to send to the agent."
|
|
1531
|
+
},
|
|
1532
|
+
session_id: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
1535
|
+
},
|
|
1536
|
+
daemon_id: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
required: ["message"]
|
|
1034
1542
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
(
|
|
1044
|
-
|
|
1045
|
-
} else if (!f.diff) {
|
|
1046
|
-
parts.push(`${header}
|
|
1047
|
-
(no diff)
|
|
1048
|
-
`);
|
|
1049
|
-
} else {
|
|
1050
|
-
parts.push(`${header}
|
|
1051
|
-
${f.diff}${f.truncated ? "" : "\n"}`);
|
|
1052
|
-
}
|
|
1543
|
+
};
|
|
1544
|
+
async function sendChat(transport, args) {
|
|
1545
|
+
if (!args.message?.trim()) throw new Error("message is required");
|
|
1546
|
+
if (isLocalTransport(transport)) {
|
|
1547
|
+
const result2 = await transport.command("send_chat", {
|
|
1548
|
+
message: args.message,
|
|
1549
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1550
|
+
});
|
|
1551
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "send_chat failed"}`;
|
|
1552
|
+
return "Message sent.";
|
|
1053
1553
|
}
|
|
1054
|
-
|
|
1554
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1555
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1556
|
+
const result = await transport.sendChat(targetId, args.message, {
|
|
1557
|
+
...args.session_id ? { sessionId: args.session_id } : {}
|
|
1558
|
+
});
|
|
1559
|
+
if (result?.success === false) return `Error: ${result.error ?? "send_chat failed"}`;
|
|
1560
|
+
return "Message sent.";
|
|
1055
1561
|
}
|
|
1056
1562
|
|
|
1057
|
-
// src/tools/
|
|
1058
|
-
var
|
|
1059
|
-
name: "
|
|
1060
|
-
description: "
|
|
1563
|
+
// src/tools/approve.ts
|
|
1564
|
+
var APPROVE_TOOL = {
|
|
1565
|
+
name: "approve",
|
|
1566
|
+
description: "Approve or reject a pending agent action (e.g. file write, command execution).",
|
|
1061
1567
|
inputSchema: {
|
|
1062
1568
|
type: "object",
|
|
1063
1569
|
properties: {
|
|
1064
|
-
|
|
1570
|
+
action: {
|
|
1065
1571
|
type: "string",
|
|
1066
|
-
|
|
1572
|
+
enum: ["approve", "reject"],
|
|
1573
|
+
description: "Whether to approve or reject the pending action."
|
|
1067
1574
|
},
|
|
1068
|
-
|
|
1575
|
+
session_id: {
|
|
1069
1576
|
type: "string",
|
|
1070
|
-
description:
|
|
1071
|
-
},
|
|
1072
|
-
include_untracked: {
|
|
1073
|
-
type: "boolean",
|
|
1074
|
-
description: "Also stage and commit untracked files (default: false)."
|
|
1577
|
+
description: "Target session ID. Omit to use the active session."
|
|
1075
1578
|
},
|
|
1076
1579
|
daemon_id: {
|
|
1077
1580
|
type: "string",
|
|
1078
|
-
description: "Daemon ID (cloud mode only
|
|
1581
|
+
description: "Daemon ID (cloud mode only)."
|
|
1079
1582
|
}
|
|
1080
1583
|
},
|
|
1081
|
-
required: ["
|
|
1584
|
+
required: ["action"]
|
|
1082
1585
|
}
|
|
1083
1586
|
};
|
|
1084
|
-
async function
|
|
1085
|
-
const
|
|
1086
|
-
if (!message) return "Error: message is required";
|
|
1087
|
-
if (message.length > 200) return "Error: message must be 200 characters or fewer";
|
|
1088
|
-
let raw;
|
|
1587
|
+
async function approve(transport, args) {
|
|
1588
|
+
const action = args.action === "reject" ? "reject" : "approve";
|
|
1089
1589
|
if (isLocalTransport(transport)) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
includeUntracked: args.include_untracked ?? false
|
|
1590
|
+
const result2 = await transport.command("resolve_action", {
|
|
1591
|
+
action,
|
|
1592
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1094
1593
|
});
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1594
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "resolve_action failed"}`;
|
|
1595
|
+
return `Action ${action}d.`;
|
|
1596
|
+
}
|
|
1597
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1598
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1599
|
+
const result = await transport.approve(targetId, action);
|
|
1600
|
+
if (result?.success === false) return `Error: ${result.error ?? "approve failed"}`;
|
|
1601
|
+
return `Action ${action}d.`;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/tools/screenshot.ts
|
|
1605
|
+
var SCREENSHOT_TOOL = {
|
|
1606
|
+
name: "screenshot",
|
|
1607
|
+
description: "Capture a screenshot of the current IDE window. Returns the image. Local mode only \u2014 screenshots require direct P2P access to the daemon and are not available in cloud mode.",
|
|
1608
|
+
inputSchema: {
|
|
1609
|
+
type: "object",
|
|
1610
|
+
properties: {
|
|
1611
|
+
session_id: {
|
|
1612
|
+
type: "string",
|
|
1613
|
+
description: "Target session ID. Omit to use the active session."
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
required: []
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
async function screenshot(transport, args) {
|
|
1620
|
+
let result;
|
|
1621
|
+
if (isLocalTransport(transport)) {
|
|
1622
|
+
result = await transport.command("screenshot", {
|
|
1623
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1102
1624
|
});
|
|
1103
|
-
|
|
1625
|
+
} else {
|
|
1626
|
+
return { type: "text", text: "Screenshots are not available in cloud mode. Run adhdev mcp in local mode (requires standalone daemon)." };
|
|
1104
1627
|
}
|
|
1105
|
-
if (
|
|
1106
|
-
|
|
1107
|
-
if (msg.includes("Nothing to commit") || msg.includes("nothing to commit")) {
|
|
1108
|
-
return "Nothing to commit \u2014 working tree is clean.";
|
|
1109
|
-
}
|
|
1110
|
-
return `Git checkpoint error: ${msg}`;
|
|
1628
|
+
if (result?.success === false) {
|
|
1629
|
+
return { type: "text", text: `Error: ${result.error ?? "screenshot failed"}` };
|
|
1111
1630
|
}
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1631
|
+
const b64 = result?.base64 ?? result?.screenshot ?? result?.result;
|
|
1632
|
+
if (!b64) {
|
|
1633
|
+
return { type: "text", text: "Screenshot captured but no image data returned." };
|
|
1634
|
+
}
|
|
1635
|
+
const mimeType = result?.format === "png" ? "image/png" : "image/webp";
|
|
1636
|
+
return { type: "image", data: b64, mimeType };
|
|
1115
1637
|
}
|
|
1116
1638
|
|
|
1117
|
-
// src/tools/git-
|
|
1118
|
-
var
|
|
1119
|
-
name: "
|
|
1120
|
-
description: "
|
|
1639
|
+
// src/tools/git-status.ts
|
|
1640
|
+
var GIT_STATUS_TOOL = {
|
|
1641
|
+
name: "git_status",
|
|
1642
|
+
description: "Get git repository status for a workspace on the daemon machine.",
|
|
1121
1643
|
inputSchema: {
|
|
1122
1644
|
type: "object",
|
|
1123
1645
|
properties: {
|
|
@@ -1125,529 +1647,688 @@ var GIT_PUSH_TOOL = {
|
|
|
1125
1647
|
type: "string",
|
|
1126
1648
|
description: "Absolute path to the workspace/repository directory."
|
|
1127
1649
|
},
|
|
1128
|
-
|
|
1129
|
-
type: "
|
|
1130
|
-
description:
|
|
1131
|
-
},
|
|
1132
|
-
branch: {
|
|
1133
|
-
type: "string",
|
|
1134
|
-
description: "Branch to push (default: current branch)."
|
|
1650
|
+
include_diff: {
|
|
1651
|
+
type: "boolean",
|
|
1652
|
+
description: "Include changed file list (default: true)."
|
|
1135
1653
|
},
|
|
1136
1654
|
daemon_id: {
|
|
1137
1655
|
type: "string",
|
|
1138
|
-
description: "Daemon ID (cloud mode only
|
|
1139
|
-
}
|
|
1656
|
+
description: "Daemon ID (cloud mode only)."
|
|
1657
|
+
},
|
|
1658
|
+
...FORMAT_PROP
|
|
1140
1659
|
},
|
|
1141
1660
|
required: ["workspace"]
|
|
1142
1661
|
}
|
|
1143
1662
|
};
|
|
1144
|
-
async function
|
|
1145
|
-
let
|
|
1663
|
+
async function gitStatus(transport, args) {
|
|
1664
|
+
let status;
|
|
1665
|
+
let diffSummary;
|
|
1146
1666
|
if (isLocalTransport(transport)) {
|
|
1147
|
-
|
|
1148
|
-
workspace: args.workspace
|
|
1149
|
-
remote: args.remote ?? "origin",
|
|
1150
|
-
...args.branch ? { branch: args.branch } : {}
|
|
1667
|
+
const statusResult = await transport.command("git_status", {
|
|
1668
|
+
workspace: args.workspace
|
|
1151
1669
|
});
|
|
1152
|
-
|
|
1670
|
+
status = statusResult?.status ?? statusResult;
|
|
1671
|
+
if (args.include_diff !== false) {
|
|
1672
|
+
const diffResult = await transport.command("git_diff_summary", {
|
|
1673
|
+
workspace: args.workspace
|
|
1674
|
+
});
|
|
1675
|
+
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
1676
|
+
}
|
|
1153
1677
|
} else {
|
|
1154
1678
|
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1155
|
-
const result = await transport.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1679
|
+
const result = await transport.gitStatus(
|
|
1680
|
+
args.daemon_id,
|
|
1681
|
+
args.workspace,
|
|
1682
|
+
args.include_diff !== false
|
|
1683
|
+
);
|
|
1684
|
+
if (result?.error) {
|
|
1685
|
+
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
1686
|
+
return `Error: ${result.error}`;
|
|
1687
|
+
}
|
|
1688
|
+
status = result?.status;
|
|
1689
|
+
diffSummary = result?.diff;
|
|
1161
1690
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
const msg =
|
|
1164
|
-
return
|
|
1691
|
+
if (status?.success === false || status?.reason) {
|
|
1692
|
+
const msg = status?.error ?? status?.reason ?? "unknown";
|
|
1693
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1694
|
+
return `Git error: ${msg}`;
|
|
1165
1695
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1696
|
+
if (!status?.isGitRepo) {
|
|
1697
|
+
if (args.format === "json") return JSON.stringify({ error: `Not a git repository: ${args.workspace}` }, null, 2);
|
|
1698
|
+
return `Not a git repository: ${args.workspace}`;
|
|
1699
|
+
}
|
|
1700
|
+
if (args.format === "json") {
|
|
1701
|
+
const files = diffSummary?.files?.map((f) => ({
|
|
1702
|
+
path: f.path,
|
|
1703
|
+
old_path: f.oldPath ?? null,
|
|
1704
|
+
status: f.status ?? "M",
|
|
1705
|
+
insertions: f.insertions ?? 0,
|
|
1706
|
+
deletions: f.deletions ?? 0
|
|
1707
|
+
})) ?? [];
|
|
1708
|
+
return JSON.stringify({
|
|
1709
|
+
branch: status.branch ?? null,
|
|
1710
|
+
head_commit: status.headCommit ?? null,
|
|
1711
|
+
head_message: status.headMessage ?? null,
|
|
1712
|
+
ahead: status.ahead ?? 0,
|
|
1713
|
+
behind: status.behind ?? 0,
|
|
1714
|
+
staged: status.staged ?? 0,
|
|
1715
|
+
modified: status.modified ?? 0,
|
|
1716
|
+
untracked: status.untracked ?? 0,
|
|
1717
|
+
deleted: status.deleted ?? 0,
|
|
1718
|
+
stash_count: status.stashCount ?? 0,
|
|
1719
|
+
has_conflicts: status.hasConflicts ?? false,
|
|
1720
|
+
dirty: status.dirty ?? false,
|
|
1721
|
+
changed_files: files,
|
|
1722
|
+
total_insertions: diffSummary?.totalInsertions ?? 0,
|
|
1723
|
+
total_deletions: diffSummary?.totalDeletions ?? 0
|
|
1724
|
+
}, null, 2);
|
|
1725
|
+
}
|
|
1726
|
+
const lines = [];
|
|
1727
|
+
if (status.branch) lines.push(`Branch: ${status.branch}`);
|
|
1728
|
+
if (status.headCommit) {
|
|
1729
|
+
lines.push(`HEAD: ${status.headCommit.slice(0, 7)}${status.headMessage ? ` \u2014 ${status.headMessage.slice(0, 80)}` : ""}`);
|
|
1730
|
+
}
|
|
1731
|
+
if (status.ahead > 0) lines.push(`Ahead: ${status.ahead}`);
|
|
1732
|
+
if (status.behind > 0) lines.push(`Behind: ${status.behind}`);
|
|
1733
|
+
if (status.staged > 0) lines.push(`Staged: ${status.staged}`);
|
|
1734
|
+
if (status.modified > 0) lines.push(`Modified: ${status.modified}`);
|
|
1735
|
+
if (status.untracked > 0) lines.push(`Untracked: ${status.untracked}`);
|
|
1736
|
+
if (status.deleted > 0) lines.push(`Deleted: ${status.deleted}`);
|
|
1737
|
+
if (status.stashCount > 0) lines.push(`Stashes: ${status.stashCount}`);
|
|
1738
|
+
if (status.hasConflicts) lines.push("Conflicts: YES");
|
|
1739
|
+
if (!status.dirty) lines.push("Working tree: clean");
|
|
1740
|
+
if (diffSummary?.files?.length > 0) {
|
|
1741
|
+
lines.push("");
|
|
1742
|
+
lines.push(`Changed files (${diffSummary.files.length}):`);
|
|
1743
|
+
for (const f of diffSummary.files.slice(0, 20)) {
|
|
1744
|
+
lines.push(` ${f.status ?? "M"} ${f.path}${f.oldPath ? ` (was ${f.oldPath})` : ""}${f.insertions || f.deletions ? ` +${f.insertions ?? 0}/-${f.deletions ?? 0}` : ""}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (diffSummary.files.length > 20) lines.push(` \u2026 and ${diffSummary.files.length - 20} more`);
|
|
1747
|
+
if (diffSummary.totalInsertions || diffSummary.totalDeletions) {
|
|
1748
|
+
lines.push(`Total: +${diffSummary.totalInsertions ?? 0}/-${diffSummary.totalDeletions ?? 0}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return lines.join("\n");
|
|
1172
1752
|
}
|
|
1173
1753
|
|
|
1174
|
-
// src/tools/
|
|
1175
|
-
var
|
|
1176
|
-
name: "
|
|
1177
|
-
description: "
|
|
1754
|
+
// src/tools/git-log.ts
|
|
1755
|
+
var GIT_LOG_TOOL = {
|
|
1756
|
+
name: "git_log",
|
|
1757
|
+
description: "Get commit history for a workspace. Shows hash, message, author, and date for recent commits. Use this to track what changes an agent has made, verify checkpoint commits, or understand project history.",
|
|
1178
1758
|
inputSchema: {
|
|
1179
1759
|
type: "object",
|
|
1180
1760
|
properties: {
|
|
1181
|
-
type: {
|
|
1182
|
-
type: "string",
|
|
1183
|
-
description: "Provider type to launch. CLI examples: hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode."
|
|
1184
|
-
},
|
|
1185
1761
|
workspace: {
|
|
1186
1762
|
type: "string",
|
|
1187
|
-
description: "
|
|
1763
|
+
description: "Absolute path to the workspace/repository directory."
|
|
1188
1764
|
},
|
|
1189
|
-
|
|
1765
|
+
limit: {
|
|
1766
|
+
type: "number",
|
|
1767
|
+
description: "Max commits to return (default: 20, max: 100)."
|
|
1768
|
+
},
|
|
1769
|
+
file: {
|
|
1190
1770
|
type: "string",
|
|
1191
|
-
description: "
|
|
1771
|
+
description: "Filter history to commits that touched this repo-relative file path (optional)."
|
|
1192
1772
|
},
|
|
1193
|
-
|
|
1773
|
+
since: {
|
|
1194
1774
|
type: "string",
|
|
1195
|
-
description: "
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
required: ["type"]
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
async function launchSession(transport, args) {
|
|
1202
|
-
if (isLocalTransport(transport)) {
|
|
1203
|
-
const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
|
|
1204
|
-
const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
|
|
1205
|
-
const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
|
|
1206
|
-
const result2 = await transport.command(commandType, payload);
|
|
1207
|
-
if (result2?.success === false) return `Error: ${result2.error ?? "launch failed"}`;
|
|
1208
|
-
const id2 = result2?.id ?? result2?.sessionId;
|
|
1209
|
-
return id2 ? `Session launched. id: ${id2}, type: ${args.type}` : `Launched: ${JSON.stringify(result2)}`;
|
|
1210
|
-
}
|
|
1211
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1212
|
-
const result = await transport.launch(args.daemon_id, {
|
|
1213
|
-
type: args.type,
|
|
1214
|
-
dir: args.workspace,
|
|
1215
|
-
model: args.model
|
|
1216
|
-
});
|
|
1217
|
-
if (result?.success === false || result?.error) return `Error: ${result.error ?? "launch failed"}`;
|
|
1218
|
-
const id = result?.id ?? result?.sessionId;
|
|
1219
|
-
return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// src/tools/stop-session.ts
|
|
1223
|
-
var STOP_SESSION_TOOL = {
|
|
1224
|
-
name: "stop_session",
|
|
1225
|
-
description: "Stop a running agent session. For CLI agents (hermes-cli, claude-cli, etc.) this sends a graceful stop signal. Use list_sessions to find the session_id.",
|
|
1226
|
-
inputSchema: {
|
|
1227
|
-
type: "object",
|
|
1228
|
-
properties: {
|
|
1229
|
-
session_id: {
|
|
1775
|
+
description: "Only commits after this date (ISO 8601 or git date string, optional)."
|
|
1776
|
+
},
|
|
1777
|
+
until: {
|
|
1230
1778
|
type: "string",
|
|
1231
|
-
description: "
|
|
1779
|
+
description: "Only commits before this date (ISO 8601 or git date string, optional)."
|
|
1232
1780
|
},
|
|
1233
1781
|
daemon_id: {
|
|
1234
1782
|
type: "string",
|
|
1235
1783
|
description: "Daemon ID (cloud mode only, required)."
|
|
1236
1784
|
},
|
|
1237
|
-
|
|
1238
|
-
type: "string",
|
|
1239
|
-
description: "Provider type (e.g. hermes-cli, claude-cli). Local mode auto-resolves from session_id if omitted; cloud mode forwards the session_id and omits type unless explicitly provided."
|
|
1240
|
-
}
|
|
1785
|
+
...FORMAT_PROP
|
|
1241
1786
|
},
|
|
1242
|
-
required: ["
|
|
1787
|
+
required: ["workspace"]
|
|
1243
1788
|
}
|
|
1244
1789
|
};
|
|
1245
|
-
async function
|
|
1790
|
+
async function gitLog(transport, args) {
|
|
1791
|
+
const limit = Math.max(1, Math.min(100, args.limit ?? 20));
|
|
1792
|
+
let raw;
|
|
1246
1793
|
if (isLocalTransport(transport)) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
if (!resolvedType) {
|
|
1255
|
-
return `Error: could not resolve session type for ${args.session_id}. Pass type= explicitly.`;
|
|
1256
|
-
}
|
|
1257
|
-
const result2 = await local.command("stop_cli", {
|
|
1258
|
-
targetSessionId: args.session_id,
|
|
1259
|
-
cliType: resolvedType
|
|
1794
|
+
raw = await transport.command("git_log", {
|
|
1795
|
+
workspace: args.workspace,
|
|
1796
|
+
limit,
|
|
1797
|
+
...args.file ? { path: args.file } : {},
|
|
1798
|
+
...args.since ? { since: args.since } : {},
|
|
1799
|
+
...args.until ? { until: args.until } : {}
|
|
1260
1800
|
});
|
|
1261
|
-
|
|
1262
|
-
|
|
1801
|
+
raw = raw?.log ?? raw;
|
|
1802
|
+
} else {
|
|
1803
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1804
|
+
const result = await transport.gitLog(args.daemon_id, args.workspace, {
|
|
1805
|
+
limit,
|
|
1806
|
+
file: args.file,
|
|
1807
|
+
since: args.since,
|
|
1808
|
+
until: args.until
|
|
1809
|
+
});
|
|
1810
|
+
raw = result?.log ?? result;
|
|
1811
|
+
}
|
|
1812
|
+
if (raw?.success === false || raw?.reason) {
|
|
1813
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
1814
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1815
|
+
return `Git log error: ${msg}`;
|
|
1816
|
+
}
|
|
1817
|
+
if (!raw?.isGitRepo) {
|
|
1818
|
+
const msg = `Not a git repository: ${args.workspace}`;
|
|
1819
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1820
|
+
return msg;
|
|
1821
|
+
}
|
|
1822
|
+
const entries = raw?.entries ?? [];
|
|
1823
|
+
if (args.format === "json") {
|
|
1824
|
+
return JSON.stringify({
|
|
1825
|
+
workspace: raw.workspace,
|
|
1826
|
+
branch: raw.branch ?? null,
|
|
1827
|
+
entries: entries.map((e) => ({
|
|
1828
|
+
commit: e.commit,
|
|
1829
|
+
short: e.commit?.slice(0, 7),
|
|
1830
|
+
message: e.message,
|
|
1831
|
+
author: e.authorName ?? null,
|
|
1832
|
+
author_email: e.authorEmail ?? null,
|
|
1833
|
+
authored_at: e.authoredAt ? new Date(e.authoredAt).toISOString() : null
|
|
1834
|
+
})),
|
|
1835
|
+
total: entries.length,
|
|
1836
|
+
truncated: raw.truncated ?? false
|
|
1837
|
+
}, null, 2);
|
|
1263
1838
|
}
|
|
1264
|
-
if (
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1839
|
+
if (entries.length === 0) return "No commits found.";
|
|
1840
|
+
const lines = entries.map((e) => {
|
|
1841
|
+
const hash = e.commit?.slice(0, 7) ?? "???????";
|
|
1842
|
+
const date = e.authoredAt ? new Date(e.authoredAt).toISOString().slice(0, 10) : "";
|
|
1843
|
+
const author = e.authorName ? ` (${e.authorName})` : "";
|
|
1844
|
+
return `${hash} ${date}${author} ${e.message}`;
|
|
1268
1845
|
});
|
|
1269
|
-
|
|
1270
|
-
return
|
|
1846
|
+
const header = `Commits (${entries.length}${raw.truncated ? ", truncated" : ""}):`;
|
|
1847
|
+
return `${header}
|
|
1848
|
+
${lines.join("\n")}`;
|
|
1271
1849
|
}
|
|
1272
1850
|
|
|
1273
|
-
// src/tools/
|
|
1274
|
-
var
|
|
1275
|
-
name: "
|
|
1276
|
-
description: "
|
|
1851
|
+
// src/tools/git-diff.ts
|
|
1852
|
+
var GIT_DIFF_TOOL = {
|
|
1853
|
+
name: "git_diff",
|
|
1854
|
+
description: "Get the actual diff content for changed files in a workspace. Without a specific file, returns diffs for up to 5 changed files. Use this to review what an agent actually changed \u2014 file names alone (from git_status) are not enough for code review.",
|
|
1277
1855
|
inputSchema: {
|
|
1278
1856
|
type: "object",
|
|
1279
1857
|
properties: {
|
|
1858
|
+
workspace: {
|
|
1859
|
+
type: "string",
|
|
1860
|
+
description: "Absolute path to the workspace/repository directory."
|
|
1861
|
+
},
|
|
1862
|
+
file: {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
description: "Specific repo-relative file path to diff (optional \u2014 if omitted, returns top 5 changed files)."
|
|
1865
|
+
},
|
|
1866
|
+
max_lines: {
|
|
1867
|
+
type: "number",
|
|
1868
|
+
description: "Max diff lines per file before truncating (default: 300)."
|
|
1869
|
+
},
|
|
1870
|
+
staged: {
|
|
1871
|
+
type: "boolean",
|
|
1872
|
+
description: "Show staged changes instead of unstaged (default: false)."
|
|
1873
|
+
},
|
|
1280
1874
|
daemon_id: {
|
|
1281
1875
|
type: "string",
|
|
1282
|
-
description: "Daemon ID
|
|
1876
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
1283
1877
|
},
|
|
1284
1878
|
...FORMAT_PROP
|
|
1285
1879
|
},
|
|
1286
|
-
required: []
|
|
1880
|
+
required: ["workspace"]
|
|
1287
1881
|
}
|
|
1288
1882
|
};
|
|
1289
|
-
async function
|
|
1883
|
+
async function gitDiff(transport, args) {
|
|
1884
|
+
const maxLines = Math.max(10, Math.min(2e3, args.max_lines ?? 300));
|
|
1885
|
+
const staged = args.staged ?? false;
|
|
1290
1886
|
if (isLocalTransport(transport)) {
|
|
1291
|
-
return
|
|
1292
|
-
}
|
|
1293
|
-
return checkPendingCloud(transport, args.daemon_id, args.format);
|
|
1294
|
-
}
|
|
1295
|
-
async function checkPendingLocal(transport, format) {
|
|
1296
|
-
const status = await transport.getStatus();
|
|
1297
|
-
const sessions = status?.sessions ?? [];
|
|
1298
|
-
const pending = sessions.filter(
|
|
1299
|
-
(s) => s.status === "waiting_approval" || s.agentStatus === "waiting_approval"
|
|
1300
|
-
);
|
|
1301
|
-
if (format === "json") {
|
|
1302
|
-
return JSON.stringify({
|
|
1303
|
-
pending: pending.map((s) => ({
|
|
1304
|
-
session_id: s.id,
|
|
1305
|
-
workspace: s.workspace ?? null,
|
|
1306
|
-
type: s.providerType ?? null,
|
|
1307
|
-
modal_message: s.activeChat?.activeModal?.message ?? null,
|
|
1308
|
-
buttons: s.activeChat?.activeModal?.buttons ?? []
|
|
1309
|
-
}))
|
|
1310
|
-
}, null, 2);
|
|
1887
|
+
return localGitDiff(transport, args.workspace, args.file, maxLines, staged, args.format);
|
|
1311
1888
|
}
|
|
1312
|
-
if (
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
1318
|
-
if (modal?.message) parts.push(`prompt: ${modal.message}`);
|
|
1319
|
-
if (modal?.buttons?.length) parts.push(`buttons: ${modal.buttons.join(", ")}`);
|
|
1320
|
-
return parts.join("\n ");
|
|
1889
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1890
|
+
const result = await transport.gitDiff(args.daemon_id, args.workspace, {
|
|
1891
|
+
file: args.file,
|
|
1892
|
+
maxLines,
|
|
1893
|
+
staged
|
|
1321
1894
|
});
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
${
|
|
1895
|
+
if (result?.error) {
|
|
1896
|
+
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
1897
|
+
return `Git diff error: ${result.error}`;
|
|
1898
|
+
}
|
|
1899
|
+
return formatDiffResult(result, args.format);
|
|
1325
1900
|
}
|
|
1326
|
-
async function
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
} else {
|
|
1335
|
-
const data = await transport.listDaemons();
|
|
1336
|
-
const daemons = data?.daemons ?? [];
|
|
1337
|
-
for (let i = 0; i < daemons.length; i += 5) {
|
|
1338
|
-
await Promise.allSettled(
|
|
1339
|
-
daemons.slice(i, i + 5).map(async (d) => {
|
|
1340
|
-
try {
|
|
1341
|
-
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
1342
|
-
const sessions = daemonStatus?.sessions ?? [];
|
|
1343
|
-
for (const s of sessions) {
|
|
1344
|
-
if (s.status === "waiting_approval") pending.push({ daemonId: d.id, session: s });
|
|
1345
|
-
}
|
|
1346
|
-
} catch {
|
|
1347
|
-
}
|
|
1348
|
-
})
|
|
1349
|
-
);
|
|
1901
|
+
async function localGitDiff(transport, workspace, file, maxLines, staged, format) {
|
|
1902
|
+
if (file) {
|
|
1903
|
+
const raw = await transport.command("git_diff_file", { workspace, path: file, staged });
|
|
1904
|
+
const d = raw?.diff ?? raw;
|
|
1905
|
+
if (d?.success === false || d?.reason) {
|
|
1906
|
+
const msg = d?.error ?? d?.reason ?? "unknown";
|
|
1907
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1908
|
+
return `Git diff error: ${msg}`;
|
|
1350
1909
|
}
|
|
1910
|
+
const lines = (d?.diff ?? "").split("\n");
|
|
1911
|
+
const truncated = lines.length > maxLines;
|
|
1912
|
+
const result = {
|
|
1913
|
+
files: [{
|
|
1914
|
+
path: file,
|
|
1915
|
+
diff: truncated ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
|
|
1916
|
+
truncated,
|
|
1917
|
+
binary: d?.binary ?? false
|
|
1918
|
+
}],
|
|
1919
|
+
total_files: 1,
|
|
1920
|
+
shown_files: 1,
|
|
1921
|
+
truncated
|
|
1922
|
+
};
|
|
1923
|
+
return formatDiffResult(result, format);
|
|
1351
1924
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
type: s.providerType ?? null,
|
|
1359
|
-
modal_message: null,
|
|
1360
|
-
buttons: []
|
|
1361
|
-
}))
|
|
1362
|
-
}, null, 2);
|
|
1925
|
+
const summaryRaw = await transport.command("git_diff_summary", { workspace, staged });
|
|
1926
|
+
const summary = summaryRaw?.diffSummary ?? summaryRaw;
|
|
1927
|
+
if (summary?.success === false || summary?.reason) {
|
|
1928
|
+
const msg = summary?.error ?? summary?.reason ?? "unknown";
|
|
1929
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1930
|
+
return `Git diff error: ${msg}`;
|
|
1363
1931
|
}
|
|
1364
|
-
if (
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
1369
|
-
parts.push("(use read_chat to see the approval prompt)");
|
|
1370
|
-
return parts.join("\n ");
|
|
1371
|
-
});
|
|
1372
|
-
return `Pending approvals (${pending.length}):
|
|
1373
|
-
|
|
1374
|
-
${lines.join("\n\n")}`;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// src/tools/mesh-tools.ts
|
|
1378
|
-
function findNode(mesh, nodeId) {
|
|
1379
|
-
const node = mesh.nodes.find((n) => n.id === nodeId);
|
|
1380
|
-
if (!node) throw new Error(`Node '${nodeId}' is not a member of mesh '${mesh.name}'`);
|
|
1381
|
-
return node;
|
|
1382
|
-
}
|
|
1383
|
-
async function commandForNode(ctx, node, command, args = {}) {
|
|
1384
|
-
if (ctx.transport instanceof IpcTransport && node.daemonId) {
|
|
1385
|
-
return ctx.transport.meshCommand(node.daemonId, command, args);
|
|
1932
|
+
if (!summary?.isGitRepo) {
|
|
1933
|
+
const msg = `Not a git repository: ${workspace}`;
|
|
1934
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1935
|
+
return msg;
|
|
1386
1936
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1937
|
+
const files = summary?.files ?? [];
|
|
1938
|
+
if (files.length === 0) {
|
|
1939
|
+
if (format === "json") return JSON.stringify({ files: [], total_files: 0, shown_files: 0, truncated: false }, null, 2);
|
|
1940
|
+
return "No changed files.";
|
|
1389
1941
|
}
|
|
1390
|
-
|
|
1942
|
+
const topFiles = files.slice(0, 5);
|
|
1943
|
+
const fileDiffs = await Promise.all(
|
|
1944
|
+
topFiles.map(async (f) => {
|
|
1945
|
+
try {
|
|
1946
|
+
const raw = await transport.command("git_diff_file", { workspace, path: f.path, staged });
|
|
1947
|
+
const d = raw?.diff ?? raw;
|
|
1948
|
+
const lines = (d?.diff ?? "").split("\n");
|
|
1949
|
+
const trunc = lines.length > maxLines;
|
|
1950
|
+
return {
|
|
1951
|
+
path: f.path,
|
|
1952
|
+
old_path: f.oldPath ?? null,
|
|
1953
|
+
status: f.status ?? "M",
|
|
1954
|
+
diff: trunc ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
|
|
1955
|
+
truncated: trunc,
|
|
1956
|
+
binary: d?.binary ?? false
|
|
1957
|
+
};
|
|
1958
|
+
} catch {
|
|
1959
|
+
return { path: f.path, diff: "", truncated: false, binary: false, error: "fetch failed" };
|
|
1960
|
+
}
|
|
1961
|
+
})
|
|
1962
|
+
);
|
|
1963
|
+
return formatDiffResult({
|
|
1964
|
+
files: fileDiffs,
|
|
1965
|
+
total_files: files.length,
|
|
1966
|
+
shown_files: topFiles.length,
|
|
1967
|
+
truncated: files.length > 5
|
|
1968
|
+
}, format);
|
|
1391
1969
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1970
|
+
function formatDiffResult(result, format) {
|
|
1971
|
+
if (format === "json") return JSON.stringify(result, null, 2);
|
|
1972
|
+
const files = result?.files ?? [];
|
|
1973
|
+
if (files.length === 0) return "No changed files.";
|
|
1974
|
+
const parts = [];
|
|
1975
|
+
const totalShown = result?.shown_files ?? files.length;
|
|
1976
|
+
const totalAll = result?.total_files ?? files.length;
|
|
1977
|
+
if (totalAll > totalShown) {
|
|
1978
|
+
parts.push(`Showing ${totalShown} of ${totalAll} changed files:
|
|
1979
|
+
`);
|
|
1398
1980
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1981
|
+
for (const f of files) {
|
|
1982
|
+
const header = `--- ${f.path}${f.old_path ? ` (was ${f.old_path})` : ""} ---`;
|
|
1983
|
+
if (f.error) {
|
|
1984
|
+
parts.push(`${header}
|
|
1985
|
+
(error: ${f.error})
|
|
1986
|
+
`);
|
|
1987
|
+
} else if (f.binary) {
|
|
1988
|
+
parts.push(`${header}
|
|
1989
|
+
(binary file)
|
|
1990
|
+
`);
|
|
1991
|
+
} else if (!f.diff) {
|
|
1992
|
+
parts.push(`${header}
|
|
1993
|
+
(no diff)
|
|
1994
|
+
`);
|
|
1995
|
+
} else {
|
|
1996
|
+
parts.push(`${header}
|
|
1997
|
+
${f.diff}${f.truncated ? "" : "\n"}`);
|
|
1998
|
+
}
|
|
1406
1999
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
2000
|
+
return parts.join("\n");
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/tools/git-checkpoint.ts
|
|
2004
|
+
var GIT_CHECKPOINT_TOOL = {
|
|
2005
|
+
name: "git_checkpoint",
|
|
2006
|
+
description: "Create a checkpoint commit in a workspace. Stages all tracked changes (or all files including untracked) and commits with a prefixed message. Use this to save progress before a risky operation, or to create a restore point the orchestrator can reference.",
|
|
1411
2007
|
inputSchema: {
|
|
1412
2008
|
type: "object",
|
|
1413
2009
|
properties: {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
2010
|
+
workspace: {
|
|
2011
|
+
type: "string",
|
|
2012
|
+
description: "Absolute path to the workspace/repository directory."
|
|
2013
|
+
},
|
|
2014
|
+
message: {
|
|
2015
|
+
type: "string",
|
|
2016
|
+
description: 'Checkpoint message (max 200 chars). Will be prefixed with "adhdev: checkpoint ".'
|
|
2017
|
+
},
|
|
2018
|
+
include_untracked: {
|
|
2019
|
+
type: "boolean",
|
|
2020
|
+
description: "Also stage and commit untracked files (default: false)."
|
|
2021
|
+
},
|
|
2022
|
+
daemon_id: {
|
|
2023
|
+
type: "string",
|
|
2024
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2025
|
+
}
|
|
1417
2026
|
},
|
|
1418
|
-
required: ["
|
|
2027
|
+
required: ["workspace", "message"]
|
|
1419
2028
|
}
|
|
1420
2029
|
};
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
2030
|
+
async function gitCheckpoint(transport, args) {
|
|
2031
|
+
const message = args.message?.trim();
|
|
2032
|
+
if (!message) return "Error: message is required";
|
|
2033
|
+
if (message.length > 200) return "Error: message must be 200 characters or fewer";
|
|
2034
|
+
let raw;
|
|
2035
|
+
if (isLocalTransport(transport)) {
|
|
2036
|
+
raw = await transport.command("git_checkpoint", {
|
|
2037
|
+
workspace: args.workspace,
|
|
2038
|
+
message,
|
|
2039
|
+
includeUntracked: args.include_untracked ?? false
|
|
2040
|
+
});
|
|
2041
|
+
raw = raw?.checkpoint ?? raw;
|
|
2042
|
+
} else {
|
|
2043
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2044
|
+
const result = await transport.gitCheckpoint(args.daemon_id, {
|
|
2045
|
+
workspace: args.workspace,
|
|
2046
|
+
message,
|
|
2047
|
+
includeUntracked: args.include_untracked ?? false
|
|
2048
|
+
});
|
|
2049
|
+
raw = result?.checkpoint ?? result;
|
|
1432
2050
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
properties: {
|
|
1440
|
-
node_id: { type: "string", description: "Target node ID." },
|
|
1441
|
-
type: { type: "string", description: 'Provider type (e.g. "claude-cli", "gemini-cli", "cursor").' }
|
|
1442
|
-
},
|
|
1443
|
-
required: ["node_id", "type"]
|
|
2051
|
+
if (raw?.success === false || raw?.reason) {
|
|
2052
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
2053
|
+
if (msg.includes("Nothing to commit") || msg.includes("nothing to commit")) {
|
|
2054
|
+
return "Nothing to commit \u2014 working tree is clean.";
|
|
2055
|
+
}
|
|
2056
|
+
return `Git checkpoint error: ${msg}`;
|
|
1444
2057
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2058
|
+
const commit = raw?.commit?.slice(0, 7) ?? "???????";
|
|
2059
|
+
const fullMsg = raw?.message ?? `adhdev: checkpoint ${message}`;
|
|
2060
|
+
return `Checkpoint created: ${commit} \u2014 ${fullMsg}`;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/tools/git-push.ts
|
|
2064
|
+
var GIT_PUSH_TOOL = {
|
|
2065
|
+
name: "git_push",
|
|
2066
|
+
description: "Push a branch to a remote repository on the daemon machine. If the branch has no upstream configured, sets it automatically. Key for parallel multi-machine workflows: after git_checkpoint, push each machine's branch to origin so changes are available for PR/review.",
|
|
1449
2067
|
inputSchema: {
|
|
1450
2068
|
type: "object",
|
|
1451
2069
|
properties: {
|
|
1452
|
-
|
|
2070
|
+
workspace: {
|
|
2071
|
+
type: "string",
|
|
2072
|
+
description: "Absolute path to the workspace/repository directory."
|
|
2073
|
+
},
|
|
2074
|
+
remote: {
|
|
2075
|
+
type: "string",
|
|
2076
|
+
description: 'Remote name (default: "origin").'
|
|
2077
|
+
},
|
|
2078
|
+
branch: {
|
|
2079
|
+
type: "string",
|
|
2080
|
+
description: "Branch to push (default: current branch)."
|
|
2081
|
+
},
|
|
2082
|
+
daemon_id: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2085
|
+
}
|
|
1453
2086
|
},
|
|
1454
|
-
required: ["
|
|
2087
|
+
required: ["workspace"]
|
|
1455
2088
|
}
|
|
1456
2089
|
};
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2090
|
+
async function gitPush(transport, args) {
|
|
2091
|
+
let raw;
|
|
2092
|
+
if (isLocalTransport(transport)) {
|
|
2093
|
+
raw = await transport.command("git_push", {
|
|
2094
|
+
workspace: args.workspace,
|
|
2095
|
+
remote: args.remote ?? "origin",
|
|
2096
|
+
...args.branch ? { branch: args.branch } : {}
|
|
2097
|
+
});
|
|
2098
|
+
raw = raw?.push ?? raw;
|
|
2099
|
+
} else {
|
|
2100
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2101
|
+
const result = await transport.gitPush(args.daemon_id, {
|
|
2102
|
+
workspace: args.workspace,
|
|
2103
|
+
remote: args.remote,
|
|
2104
|
+
branch: args.branch
|
|
2105
|
+
});
|
|
2106
|
+
raw = result?.push ?? result;
|
|
1467
2107
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
2108
|
+
if (raw?.success === false || raw?.reason) {
|
|
2109
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
2110
|
+
return `Git push error: ${msg}`;
|
|
2111
|
+
}
|
|
2112
|
+
const branch = raw?.branch ?? args.branch ?? "(current)";
|
|
2113
|
+
const remote = raw?.remote ?? args.remote ?? "origin";
|
|
2114
|
+
const newBranch = raw?.newBranch ? " [new branch]" : "";
|
|
2115
|
+
const output = raw?.output ? `
|
|
2116
|
+
${raw.output}` : "";
|
|
2117
|
+
return `Pushed ${branch} \u2192 ${remote}${newBranch}${output}`;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/tools/launch-session.ts
|
|
2121
|
+
var LAUNCH_SESSION_TOOL = {
|
|
2122
|
+
name: "launch_session",
|
|
2123
|
+
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).",
|
|
1472
2124
|
inputSchema: {
|
|
1473
2125
|
type: "object",
|
|
1474
2126
|
properties: {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
MESH_APPROVE_TOOL
|
|
1491
|
-
];
|
|
1492
|
-
async function meshStatus(ctx) {
|
|
1493
|
-
const { mesh, transport } = ctx;
|
|
1494
|
-
const results = [];
|
|
1495
|
-
for (const node of mesh.nodes) {
|
|
1496
|
-
const entry = {
|
|
1497
|
-
nodeId: node.id,
|
|
1498
|
-
workspace: node.workspace
|
|
1499
|
-
};
|
|
1500
|
-
try {
|
|
1501
|
-
if (!isLocalTransport(transport) && node.daemonId) {
|
|
1502
|
-
const result = await transport.gitStatus(node.daemonId, node.workspace, false);
|
|
1503
|
-
const status = result?.status ?? result;
|
|
1504
|
-
entry.health = status?.isGitRepo ? status?.isDirty ? "dirty" : "online" : "degraded";
|
|
1505
|
-
entry.branch = status?.branch;
|
|
1506
|
-
entry.isDirty = status?.isDirty;
|
|
1507
|
-
entry.uncommittedChanges = status?.uncommittedChanges ?? 0;
|
|
1508
|
-
} else if (isLocalTransport(transport)) {
|
|
1509
|
-
const statusResult = await commandForNode(ctx, node, "git_status", { workspace: node.workspace });
|
|
1510
|
-
const status = statusResult?.status ?? statusResult;
|
|
1511
|
-
entry.health = status?.isGitRepo ? status?.isDirty ? "dirty" : "online" : "degraded";
|
|
1512
|
-
entry.branch = status?.branch;
|
|
1513
|
-
entry.isDirty = status?.isDirty;
|
|
1514
|
-
entry.uncommittedChanges = status?.uncommittedChanges ?? 0;
|
|
1515
|
-
} else {
|
|
1516
|
-
entry.health = "unknown";
|
|
1517
|
-
entry.note = "No daemonId available for cloud status probe";
|
|
2127
|
+
type: {
|
|
2128
|
+
type: "string",
|
|
2129
|
+
description: "Provider type to launch. CLI examples: hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode."
|
|
2130
|
+
},
|
|
2131
|
+
workspace: {
|
|
2132
|
+
type: "string",
|
|
2133
|
+
description: "Working directory for the session. Defaults to the daemon default workspace."
|
|
2134
|
+
},
|
|
2135
|
+
model: {
|
|
2136
|
+
type: "string",
|
|
2137
|
+
description: "Model override for ACP agents (e.g. claude-opus-4-7)."
|
|
2138
|
+
},
|
|
2139
|
+
daemon_id: {
|
|
2140
|
+
type: "string",
|
|
2141
|
+
description: "Daemon ID (cloud mode only). Required in cloud mode."
|
|
1518
2142
|
}
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
entry.error = e.message;
|
|
1522
|
-
}
|
|
1523
|
-
results.push(entry);
|
|
1524
|
-
}
|
|
1525
|
-
return JSON.stringify({
|
|
1526
|
-
meshId: mesh.id,
|
|
1527
|
-
meshName: mesh.name,
|
|
1528
|
-
repoIdentity: mesh.repoIdentity,
|
|
1529
|
-
policy: mesh.policy,
|
|
1530
|
-
refreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1531
|
-
nodes: results
|
|
1532
|
-
}, null, 2);
|
|
1533
|
-
}
|
|
1534
|
-
async function meshListNodes(ctx) {
|
|
1535
|
-
const { mesh } = ctx;
|
|
1536
|
-
return JSON.stringify({
|
|
1537
|
-
meshId: mesh.id,
|
|
1538
|
-
meshName: mesh.name,
|
|
1539
|
-
nodes: mesh.nodes.map((n) => ({
|
|
1540
|
-
nodeId: n.id,
|
|
1541
|
-
workspace: n.workspace,
|
|
1542
|
-
repoRoot: n.repoRoot,
|
|
1543
|
-
isLocalWorktree: n.isLocalWorktree,
|
|
1544
|
-
policy: n.policy,
|
|
1545
|
-
userOverrides: n.userOverrides
|
|
1546
|
-
}))
|
|
1547
|
-
}, null, 2);
|
|
1548
|
-
}
|
|
1549
|
-
async function meshSendTask(ctx, args) {
|
|
1550
|
-
const node = findNode(ctx.mesh, args.node_id);
|
|
1551
|
-
if (node.policy?.readOnly) {
|
|
1552
|
-
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
1553
|
-
}
|
|
1554
|
-
if (isLocalTransport(ctx.transport)) {
|
|
1555
|
-
await commandForNode(ctx, node, "send_chat", {
|
|
1556
|
-
message: args.message,
|
|
1557
|
-
sessionId: args.session_id,
|
|
1558
|
-
targetSessionId: args.session_id
|
|
1559
|
-
});
|
|
1560
|
-
return JSON.stringify({ success: true, nodeId: args.node_id, sessionId: args.session_id });
|
|
1561
|
-
} else {
|
|
1562
|
-
return JSON.stringify({ error: "Cloud mesh send_task not yet implemented" });
|
|
2143
|
+
},
|
|
2144
|
+
required: ["type"]
|
|
1563
2145
|
}
|
|
2146
|
+
};
|
|
2147
|
+
async function launchSession(transport, args) {
|
|
2148
|
+
if (isLocalTransport(transport)) {
|
|
2149
|
+
const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
|
|
2150
|
+
const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
|
|
2151
|
+
const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
|
|
2152
|
+
const result2 = await transport.command(commandType, payload);
|
|
2153
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "launch failed"}`;
|
|
2154
|
+
const id2 = result2?.id ?? result2?.sessionId;
|
|
2155
|
+
return id2 ? `Session launched. id: ${id2}, type: ${args.type}` : `Launched: ${JSON.stringify(result2)}`;
|
|
2156
|
+
}
|
|
2157
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2158
|
+
const result = await transport.launch(args.daemon_id, {
|
|
2159
|
+
type: args.type,
|
|
2160
|
+
dir: args.workspace,
|
|
2161
|
+
model: args.model
|
|
2162
|
+
});
|
|
2163
|
+
if (result?.success === false || result?.error) return `Error: ${result.error ?? "launch failed"}`;
|
|
2164
|
+
const id = result?.id ?? result?.sessionId;
|
|
2165
|
+
return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
|
|
1564
2166
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2167
|
+
|
|
2168
|
+
// src/tools/stop-session.ts
|
|
2169
|
+
var STOP_SESSION_TOOL = {
|
|
2170
|
+
name: "stop_session",
|
|
2171
|
+
description: "Stop a running agent session. For CLI agents (hermes-cli, claude-cli, etc.) this sends a graceful stop signal. Use list_sessions to find the session_id.",
|
|
2172
|
+
inputSchema: {
|
|
2173
|
+
type: "object",
|
|
2174
|
+
properties: {
|
|
2175
|
+
session_id: {
|
|
2176
|
+
type: "string",
|
|
2177
|
+
description: "Session ID to stop (from list_sessions)."
|
|
2178
|
+
},
|
|
2179
|
+
daemon_id: {
|
|
2180
|
+
type: "string",
|
|
2181
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2182
|
+
},
|
|
2183
|
+
type: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "Provider type (e.g. hermes-cli, claude-cli). Local mode auto-resolves from session_id if omitted; cloud mode forwards the session_id and omits type unless explicitly provided."
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
required: ["session_id"]
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
async function stopSession(transport, args) {
|
|
2192
|
+
if (isLocalTransport(transport)) {
|
|
2193
|
+
const local = transport;
|
|
2194
|
+
let resolvedType = args.type;
|
|
2195
|
+
if (!resolvedType) {
|
|
2196
|
+
const status = await local.getStatus();
|
|
2197
|
+
const session = (status?.sessions ?? []).find((s) => s.id === args.session_id);
|
|
2198
|
+
resolvedType = session?.providerType ?? session?.type;
|
|
2199
|
+
}
|
|
2200
|
+
if (!resolvedType) {
|
|
2201
|
+
return `Error: could not resolve session type for ${args.session_id}. Pass type= explicitly.`;
|
|
2202
|
+
}
|
|
2203
|
+
const result2 = await local.command("stop_cli", {
|
|
1570
2204
|
targetSessionId: args.session_id,
|
|
1571
|
-
|
|
2205
|
+
cliType: resolvedType
|
|
1572
2206
|
});
|
|
1573
|
-
return
|
|
1574
|
-
|
|
1575
|
-
return JSON.stringify({ error: "Cloud mesh read_chat not yet implemented" });
|
|
2207
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "stop failed"}`;
|
|
2208
|
+
return `Session ${args.session_id} stopped.`;
|
|
1576
2209
|
}
|
|
2210
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2211
|
+
const result = await transport.stop(args.daemon_id, {
|
|
2212
|
+
id: args.session_id,
|
|
2213
|
+
...args.type ? { type: args.type } : {}
|
|
2214
|
+
});
|
|
2215
|
+
if (result?.success === false || result?.error) return `Error: ${result.error ?? "stop failed"}`;
|
|
2216
|
+
return `Session ${args.session_id} stopped.`;
|
|
1577
2217
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2218
|
+
|
|
2219
|
+
// src/tools/check-pending.ts
|
|
2220
|
+
var CHECK_PENDING_TOOL = {
|
|
2221
|
+
name: "check_pending",
|
|
2222
|
+
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.",
|
|
2223
|
+
inputSchema: {
|
|
2224
|
+
type: "object",
|
|
2225
|
+
properties: {
|
|
2226
|
+
daemon_id: {
|
|
2227
|
+
type: "string",
|
|
2228
|
+
description: "Daemon ID to check (cloud mode). Omit to check all daemons."
|
|
2229
|
+
},
|
|
2230
|
+
...FORMAT_PROP
|
|
2231
|
+
},
|
|
2232
|
+
required: []
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
async function checkPending(transport, args) {
|
|
2236
|
+
if (isLocalTransport(transport)) {
|
|
2237
|
+
return checkPendingLocal(transport, args.format);
|
|
1592
2238
|
}
|
|
2239
|
+
return checkPendingCloud(transport, args.daemon_id, args.format);
|
|
1593
2240
|
}
|
|
1594
|
-
async function
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
status: result?.status ?? result,
|
|
1602
|
-
diff: result?.diff ?? null
|
|
1603
|
-
}, null, 2);
|
|
1604
|
-
} else if (isLocalTransport(ctx.transport)) {
|
|
1605
|
-
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
1606
|
-
workspace: node.workspace
|
|
1607
|
-
});
|
|
1608
|
-
const diffResult = await commandForNode(ctx, node, "git_diff_summary", {
|
|
1609
|
-
workspace: node.workspace
|
|
1610
|
-
});
|
|
2241
|
+
async function checkPendingLocal(transport, format) {
|
|
2242
|
+
const status = await transport.getStatus();
|
|
2243
|
+
const sessions = status?.sessions ?? [];
|
|
2244
|
+
const pending = sessions.filter(
|
|
2245
|
+
(s) => s.status === "waiting_approval" || s.agentStatus === "waiting_approval"
|
|
2246
|
+
);
|
|
2247
|
+
if (format === "json") {
|
|
1611
2248
|
return JSON.stringify({
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2249
|
+
pending: pending.map((s) => ({
|
|
2250
|
+
session_id: s.id,
|
|
2251
|
+
workspace: s.workspace ?? null,
|
|
2252
|
+
type: s.providerType ?? null,
|
|
2253
|
+
modal_message: s.activeChat?.activeModal?.message ?? null,
|
|
2254
|
+
buttons: s.activeChat?.activeModal?.buttons ?? []
|
|
2255
|
+
}))
|
|
1616
2256
|
}, null, 2);
|
|
1617
|
-
} else {
|
|
1618
|
-
return JSON.stringify({ error: "No daemonId available for cloud git_status probe" });
|
|
1619
2257
|
}
|
|
2258
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
2259
|
+
const lines = pending.map((s) => {
|
|
2260
|
+
const modal = s.activeChat?.activeModal;
|
|
2261
|
+
const parts = [`session_id: ${s.id}`];
|
|
2262
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
2263
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
2264
|
+
if (modal?.message) parts.push(`prompt: ${modal.message}`);
|
|
2265
|
+
if (modal?.buttons?.length) parts.push(`buttons: ${modal.buttons.join(", ")}`);
|
|
2266
|
+
return parts.join("\n ");
|
|
2267
|
+
});
|
|
2268
|
+
return `Pending approvals (${pending.length}):
|
|
2269
|
+
|
|
2270
|
+
${lines.join("\n\n")}`;
|
|
1620
2271
|
}
|
|
1621
|
-
async function
|
|
1622
|
-
const
|
|
1623
|
-
if (
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
message: args.message
|
|
1630
|
-
});
|
|
1631
|
-
return JSON.stringify(result, null, 2);
|
|
2272
|
+
async function checkPendingCloud(transport, daemonId, format) {
|
|
2273
|
+
const pending = [];
|
|
2274
|
+
if (daemonId) {
|
|
2275
|
+
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
2276
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
2277
|
+
for (const s of sessions) {
|
|
2278
|
+
if (s.status === "waiting_approval") pending.push({ daemonId, session: s });
|
|
2279
|
+
}
|
|
1632
2280
|
} else {
|
|
1633
|
-
|
|
2281
|
+
const data = await transport.listDaemons();
|
|
2282
|
+
const daemons = data?.daemons ?? [];
|
|
2283
|
+
for (let i = 0; i < daemons.length; i += 5) {
|
|
2284
|
+
await Promise.allSettled(
|
|
2285
|
+
daemons.slice(i, i + 5).map(async (d) => {
|
|
2286
|
+
try {
|
|
2287
|
+
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
2288
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
2289
|
+
for (const s of sessions) {
|
|
2290
|
+
if (s.status === "waiting_approval") pending.push({ daemonId: d.id, session: s });
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
})
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
1634
2297
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
return JSON.stringify({ error: "Cloud mesh approve not yet implemented" });
|
|
2298
|
+
if (format === "json") {
|
|
2299
|
+
return JSON.stringify({
|
|
2300
|
+
pending: pending.map(({ daemonId: dId, session: s }) => ({
|
|
2301
|
+
daemon_id: dId,
|
|
2302
|
+
session_id: s.id,
|
|
2303
|
+
workspace: s.workspace ?? null,
|
|
2304
|
+
type: s.providerType ?? null,
|
|
2305
|
+
modal_message: null,
|
|
2306
|
+
buttons: []
|
|
2307
|
+
}))
|
|
2308
|
+
}, null, 2);
|
|
1647
2309
|
}
|
|
2310
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
2311
|
+
const lines = pending.map(({ daemonId: dId, session: s }) => {
|
|
2312
|
+
const parts = [`daemon_id: ${dId}`, `session_id: ${s.id}`];
|
|
2313
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
2314
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
2315
|
+
parts.push("(use read_chat to see the approval prompt)");
|
|
2316
|
+
return parts.join("\n ");
|
|
2317
|
+
});
|
|
2318
|
+
return `Pending approvals (${pending.length}):
|
|
2319
|
+
|
|
2320
|
+
${lines.join("\n\n")}`;
|
|
1648
2321
|
}
|
|
1649
2322
|
|
|
1650
2323
|
// src/server.ts
|
|
2324
|
+
async function buildMeshModeCoordinatorPrompt(mesh) {
|
|
2325
|
+
try {
|
|
2326
|
+
const { buildCoordinatorSystemPrompt } = await import("@adhdev/daemon-core");
|
|
2327
|
+
return buildCoordinatorSystemPrompt({ mesh });
|
|
2328
|
+
} catch (e) {
|
|
2329
|
+
throw new Error(`Failed to build Repo Mesh coordinator prompt: ${e?.message ?? String(e)}`);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
1651
2332
|
async function startMcpServer(opts) {
|
|
1652
2333
|
const transport = opts.mode === "cloud" ? new CloudTransport({ apiKey: opts.apiKey, baseUrl: opts.baseUrl }) : opts.mode === "ipc" ? new IpcTransport({ port: opts.port }) : new LocalTransport({ port: opts.port, password: opts.password });
|
|
1653
2334
|
const alive = await transport.ping();
|
|
@@ -1703,6 +2384,7 @@ async function startMcpServer(opts) {
|
|
|
1703
2384
|
requireApprovalForDestructiveGit: true,
|
|
1704
2385
|
dirtyWorkspaceBehavior: "warn",
|
|
1705
2386
|
maxParallelTasks: 2,
|
|
2387
|
+
spawnedSessionVisibility: "visible",
|
|
1706
2388
|
...policy
|
|
1707
2389
|
},
|
|
1708
2390
|
coordinator,
|
|
@@ -1753,14 +2435,17 @@ async function startMcpServer(opts) {
|
|
|
1753
2435
|
`);
|
|
1754
2436
|
process.exit(1);
|
|
1755
2437
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2438
|
+
let localDaemonId;
|
|
2439
|
+
if (transport instanceof IpcTransport) {
|
|
2440
|
+
try {
|
|
2441
|
+
const statusResult = await transport.getStatus();
|
|
2442
|
+
const instanceId = typeof statusResult?.status?.instanceId === "string" ? statusResult.status.instanceId.trim() : "";
|
|
2443
|
+
if (instanceId) localDaemonId = instanceId;
|
|
2444
|
+
} catch {
|
|
2445
|
+
}
|
|
1763
2446
|
}
|
|
2447
|
+
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {} };
|
|
2448
|
+
const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
|
|
1764
2449
|
const server2 = new import_server.Server(
|
|
1765
2450
|
{ name: "adhdev-mcp-server", version: "0.9.75" },
|
|
1766
2451
|
{ capabilities: { tools: {}, resources: {} } }
|
|
@@ -1799,6 +2484,9 @@ async function startMcpServer(opts) {
|
|
|
1799
2484
|
case "mesh_read_chat":
|
|
1800
2485
|
text = await meshReadChat(meshCtx, a);
|
|
1801
2486
|
break;
|
|
2487
|
+
case "mesh_read_debug":
|
|
2488
|
+
text = await meshReadDebug(meshCtx, a);
|
|
2489
|
+
break;
|
|
1802
2490
|
case "mesh_launch_session":
|
|
1803
2491
|
text = await meshLaunchSession(meshCtx, a);
|
|
1804
2492
|
break;
|
|
@@ -1811,6 +2499,15 @@ async function startMcpServer(opts) {
|
|
|
1811
2499
|
case "mesh_approve":
|
|
1812
2500
|
text = await meshApprove(meshCtx, a);
|
|
1813
2501
|
break;
|
|
2502
|
+
case "mesh_clone_node":
|
|
2503
|
+
text = await meshCloneNode(meshCtx, a);
|
|
2504
|
+
break;
|
|
2505
|
+
case "mesh_remove_node":
|
|
2506
|
+
text = await meshRemoveNode(meshCtx, a);
|
|
2507
|
+
break;
|
|
2508
|
+
case "mesh_cleanup_sessions":
|
|
2509
|
+
text = await meshCleanupSessions(meshCtx, a);
|
|
2510
|
+
break;
|
|
1814
2511
|
default:
|
|
1815
2512
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1816
2513
|
}
|
|
@@ -1832,6 +2529,7 @@ async function startMcpServer(opts) {
|
|
|
1832
2529
|
STOP_SESSION_TOOL,
|
|
1833
2530
|
CHECK_PENDING_TOOL,
|
|
1834
2531
|
READ_CHAT_TOOL,
|
|
2532
|
+
READ_CHAT_DEBUG_TOOL,
|
|
1835
2533
|
SEND_CHAT_TOOL,
|
|
1836
2534
|
APPROVE_TOOL,
|
|
1837
2535
|
GIT_STATUS_TOOL,
|
|
@@ -1863,6 +2561,10 @@ async function startMcpServer(opts) {
|
|
|
1863
2561
|
const text = await readChat(transport, a);
|
|
1864
2562
|
return { content: [{ type: "text", text }] };
|
|
1865
2563
|
}
|
|
2564
|
+
case "read_chat_debug": {
|
|
2565
|
+
const text = await readChatDebug(transport, a);
|
|
2566
|
+
return { content: [{ type: "text", text }] };
|
|
2567
|
+
}
|
|
1866
2568
|
case "send_chat": {
|
|
1867
2569
|
const text = await sendChat(transport, { message: a.message, session_id: a.session_id, daemon_id: a.daemon_id });
|
|
1868
2570
|
return { content: [{ type: "text", text }] };
|
|
@@ -1987,33 +2689,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
1987
2689
|
return { mode, port, password, apiKey, baseUrl, meshId };
|
|
1988
2690
|
}
|
|
1989
2691
|
function printHelp() {
|
|
1990
|
-
console.error(
|
|
1991
|
-
adhdev-mcp \u2014 ADHDev MCP Server
|
|
1992
|
-
|
|
1993
|
-
Usage:
|
|
1994
|
-
adhdev-mcp Local mode (requires standalone daemon)
|
|
1995
|
-
adhdev-mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
1996
|
-
adhdev-mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
1997
|
-
adhdev-mcp --repo-mesh <mesh_id> Mesh mode (coordinator-scoped tools)
|
|
1998
|
-
|
|
1999
|
-
Options:
|
|
2000
|
-
--mode <mode> Transport: local, cloud, or ipc
|
|
2001
|
-
--port <n> Standalone or IPC daemon port (defaults: local 3847, ipc 19222)
|
|
2002
|
-
--password <pass> Standalone daemon password (if set)
|
|
2003
|
-
--api-key <key> ADHDev cloud API key (switches to cloud mode)
|
|
2004
|
-
--base-url <url> Override cloud API base URL
|
|
2005
|
-
--repo-mesh <mesh_id> Enable mesh mode \u2014 exposes only mesh-scoped coordinator tools
|
|
2006
|
-
--help Show this help
|
|
2007
|
-
|
|
2008
|
-
Environment variables:
|
|
2009
|
-
ADHDEV_API_KEY API key (cloud mode)
|
|
2010
|
-
ADHDEV_PASSWORD Daemon password (local mode)
|
|
2011
|
-
ADHDEV_MESH_ID Mesh ID (mesh mode)
|
|
2012
|
-
ADHDEV_MCP_TRANSPORT Transport: local, cloud, or ipc
|
|
2013
|
-
|
|
2014
|
-
Standard tools: list_daemons, list_sessions, launch_session, stop_session, check_pending, read_chat, send_chat, approve, git_status, git_log, git_diff, git_checkpoint, git_push, screenshot
|
|
2015
|
-
Mesh tools: mesh_status, mesh_list_nodes, mesh_send_task, mesh_read_chat, mesh_launch_session, mesh_git_status, mesh_checkpoint, mesh_approve
|
|
2016
|
-
`.trim());
|
|
2692
|
+
console.error(buildMcpHelpText());
|
|
2017
2693
|
}
|
|
2018
2694
|
startMcpServer(parseArgs(process.argv)).catch((err) => {
|
|
2019
2695
|
process.stderr.write(`[adhdev-mcp] Fatal: ${err?.message ?? err}
|