adhdev 0.9.76-rc.9 → 0.9.76
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 +6968 -2361
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +6746 -2189
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/vendor/mcp-server/index.js +1991 -1304
- 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,1515 @@ 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
|
+
const response = {
|
|
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
|
+
};
|
|
618
|
+
if (ctx.transport instanceof IpcTransport) {
|
|
619
|
+
try {
|
|
620
|
+
const eventsResult = await ctx.transport.command("get_pending_mesh_events", {});
|
|
621
|
+
const pendingEvents = Array.isArray(eventsResult?.events) ? eventsResult.events : [];
|
|
622
|
+
if (pendingEvents.length > 0) {
|
|
623
|
+
response.pendingCoordinatorEvents = pendingEvents;
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return JSON.stringify(response, null, 2);
|
|
629
|
+
}
|
|
630
|
+
async function meshListNodes(ctx) {
|
|
631
|
+
await refreshMeshFromDaemon(ctx);
|
|
632
|
+
const { mesh } = ctx;
|
|
633
|
+
return JSON.stringify({
|
|
634
|
+
meshId: mesh.id,
|
|
635
|
+
meshName: mesh.name,
|
|
636
|
+
nodes: mesh.nodes.map((n) => ({
|
|
637
|
+
nodeId: n.id,
|
|
638
|
+
workspace: n.workspace,
|
|
639
|
+
repoRoot: n.repoRoot,
|
|
640
|
+
isLocalWorktree: n.isLocalWorktree,
|
|
641
|
+
policy: n.policy,
|
|
642
|
+
relatedRepos: readRelatedRepos(n),
|
|
643
|
+
...getNodeLaunchReadiness(n),
|
|
644
|
+
userOverrides: n.userOverrides
|
|
645
|
+
}))
|
|
646
|
+
}, null, 2);
|
|
647
|
+
}
|
|
648
|
+
async function meshSendTask(ctx, args) {
|
|
649
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
650
|
+
if (node.policy?.readOnly) {
|
|
651
|
+
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
652
|
+
}
|
|
653
|
+
if (isLocalTransport(ctx.transport)) {
|
|
654
|
+
const result = await commandForNode(ctx, node, "send_chat", {
|
|
655
|
+
message: args.message,
|
|
656
|
+
sessionId: args.session_id,
|
|
657
|
+
targetSessionId: args.session_id
|
|
723
658
|
});
|
|
724
|
-
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
659
|
+
const payload = unwrapCommandPayload(result);
|
|
660
|
+
if (payload?.success === false) {
|
|
661
|
+
return JSON.stringify({
|
|
662
|
+
success: false,
|
|
663
|
+
nodeId: args.node_id,
|
|
664
|
+
sessionId: args.session_id,
|
|
665
|
+
error: payload.error || "send_chat failed"
|
|
728
666
|
});
|
|
729
|
-
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
730
667
|
}
|
|
668
|
+
return JSON.stringify({ success: true, nodeId: args.node_id, sessionId: args.session_id });
|
|
731
669
|
} 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;
|
|
670
|
+
return JSON.stringify({ error: "Cloud mesh send_task not yet implemented" });
|
|
744
671
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
672
|
+
}
|
|
673
|
+
async function meshReadChat(ctx, args) {
|
|
674
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
675
|
+
if (isLocalTransport(ctx.transport)) {
|
|
676
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
677
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
678
|
+
const result = await commandForNode(ctx, node, "read_chat", {
|
|
679
|
+
sessionId: args.session_id,
|
|
680
|
+
targetSessionId: args.session_id,
|
|
681
|
+
workspace: node.workspace,
|
|
682
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
683
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
684
|
+
tailLimit: args.tail ?? 10
|
|
685
|
+
});
|
|
686
|
+
const payload = annotateRapidReadChatAdvisory(unwrapCommandPayload(result), {
|
|
687
|
+
key: `mesh:${args.node_id}:${args.session_id}`,
|
|
688
|
+
toolName: "mesh_read_chat",
|
|
689
|
+
completionCallbackExpected: true
|
|
690
|
+
});
|
|
691
|
+
if (args.compact) {
|
|
692
|
+
const compactPayload = compactChatPayload(payload, {
|
|
693
|
+
nodeId: args.node_id,
|
|
694
|
+
sessionId: args.session_id,
|
|
695
|
+
limit: args.tail ?? 10
|
|
696
|
+
});
|
|
697
|
+
return JSON.stringify(
|
|
698
|
+
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
699
|
+
null,
|
|
700
|
+
2
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return JSON.stringify(payload, null, 2);
|
|
704
|
+
} else {
|
|
705
|
+
return JSON.stringify({ error: "Cloud mesh read_chat not yet implemented" });
|
|
749
706
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
707
|
+
}
|
|
708
|
+
async function meshReadDebug(ctx, args) {
|
|
709
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
710
|
+
if (isLocalTransport(ctx.transport)) {
|
|
711
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
712
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
713
|
+
const delivery = args.delivery === "inline" ? void 0 : "daemon_file";
|
|
714
|
+
const result = await commandForNode(ctx, node, "get_chat_debug_bundle", {
|
|
715
|
+
sessionId: args.session_id,
|
|
716
|
+
targetSessionId: args.session_id,
|
|
717
|
+
workspace: node.workspace,
|
|
718
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
719
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
720
|
+
tailLimit: args.tail ?? 40,
|
|
721
|
+
...delivery ? { delivery } : {}
|
|
722
|
+
});
|
|
723
|
+
const payload = unwrapCommandPayload(result);
|
|
724
|
+
return JSON.stringify(payload, null, 2);
|
|
753
725
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
726
|
+
return JSON.stringify({ error: "Cloud mesh read_debug not yet implemented" });
|
|
727
|
+
}
|
|
728
|
+
async function meshLaunchSession(ctx, args) {
|
|
729
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
730
|
+
if (isLocalTransport(ctx.transport)) {
|
|
731
|
+
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
732
|
+
if (!resolvedProviderType) {
|
|
733
|
+
const providerPriority = readProviderPriority(node.policy);
|
|
734
|
+
if (!providerPriority.length) {
|
|
735
|
+
return JSON.stringify({ success: false, error: missingProviderPriorityMessage(args.node_id) });
|
|
736
|
+
}
|
|
737
|
+
const failed = [];
|
|
738
|
+
for (const providerType of providerPriority) {
|
|
739
|
+
const detectedResult = await commandForNode(ctx, node, "detect_provider", { providerType });
|
|
740
|
+
const detectedPayload = unwrapCommandPayload(detectedResult);
|
|
741
|
+
if (detectedPayload?.success && detectedPayload?.detected) {
|
|
742
|
+
resolvedProviderType = providerType;
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
failed.push(`${providerType}: ${detectedPayload?.error || "not detected"}`);
|
|
746
|
+
}
|
|
747
|
+
if (!resolvedProviderType) {
|
|
748
|
+
return JSON.stringify({ success: false, error: `No usable provider detected for node '${args.node_id}' from providerPriority: ${failed.join("; ")}` });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
752
|
+
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
753
|
+
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
754
|
+
const result = await commandForNode(ctx, node, "launch_cli", {
|
|
755
|
+
cliType: resolvedProviderType,
|
|
756
|
+
dir: node.workspace,
|
|
757
|
+
settings: {
|
|
758
|
+
meshNodeFor: ctx.mesh.id,
|
|
759
|
+
meshNodeId: args.node_id,
|
|
760
|
+
spawnedSessionVisibility,
|
|
761
|
+
...coordinatorDaemonId ? { meshCoordinatorDaemonId: coordinatorDaemonId } : {},
|
|
762
|
+
...coordinatorNode?.id ? { meshCoordinatorNodeId: coordinatorNode.id } : {},
|
|
763
|
+
launchedByCoordinator: true
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
const launchPayload = extractLaunchPayload(result);
|
|
767
|
+
const runtimeSessionId = typeof launchPayload?.sessionId === "string" ? launchPayload.sessionId : typeof launchPayload?.id === "string" ? launchPayload.id : typeof launchPayload?.runtimeSessionId === "string" ? launchPayload.runtimeSessionId : "";
|
|
768
|
+
const providerSessionId = typeof launchPayload?.providerSessionId === "string" && launchPayload.providerSessionId.trim() ? launchPayload.providerSessionId.trim() : void 0;
|
|
769
|
+
if (runtimeSessionId) {
|
|
770
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, runtimeSessionId), {
|
|
771
|
+
providerType: resolvedProviderType,
|
|
772
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return JSON.stringify({
|
|
776
|
+
...launchPayload,
|
|
777
|
+
resolvedProviderType,
|
|
778
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
778
779
|
}, null, 2);
|
|
780
|
+
} else {
|
|
781
|
+
return JSON.stringify({ error: "Cloud mesh launch_session not yet implemented" });
|
|
779
782
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
783
|
+
}
|
|
784
|
+
async function meshGitStatus(ctx, args) {
|
|
785
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
786
|
+
if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
787
|
+
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
|
|
788
|
+
return JSON.stringify({
|
|
789
|
+
nodeId: args.node_id,
|
|
790
|
+
workspace: node.workspace,
|
|
791
|
+
status: extractGitStatus(result),
|
|
792
|
+
diff: extractGitDiff(result),
|
|
793
|
+
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
794
|
+
}, null, 2);
|
|
795
|
+
} else if (isLocalTransport(ctx.transport)) {
|
|
796
|
+
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
797
|
+
workspace: node.workspace
|
|
798
|
+
});
|
|
799
|
+
const diffResult = await commandForNode(ctx, node, "git_diff_summary", {
|
|
800
|
+
workspace: node.workspace
|
|
801
|
+
});
|
|
802
|
+
return JSON.stringify({
|
|
803
|
+
nodeId: args.node_id,
|
|
804
|
+
workspace: node.workspace,
|
|
805
|
+
status: extractGitStatus(statusResult),
|
|
806
|
+
diff: extractGitDiff(diffResult),
|
|
807
|
+
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
808
|
+
}, null, 2);
|
|
809
|
+
} else {
|
|
810
|
+
return JSON.stringify({ error: "No daemonId available for cloud git_status probe" });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function meshCheckpoint(ctx, args) {
|
|
814
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
815
|
+
if (node.policy?.readOnly) {
|
|
816
|
+
return JSON.stringify({ error: `Node '${args.node_id}' is read-only \u2014 cannot checkpoint` });
|
|
817
|
+
}
|
|
818
|
+
if (isLocalTransport(ctx.transport)) {
|
|
819
|
+
const result = await commandForNode(ctx, node, "git_checkpoint", {
|
|
820
|
+
workspace: node.workspace,
|
|
821
|
+
message: args.message,
|
|
822
|
+
includeUntracked: true
|
|
823
|
+
});
|
|
824
|
+
return JSON.stringify(result, null, 2);
|
|
825
|
+
} else {
|
|
826
|
+
return JSON.stringify({ error: "Cloud mesh checkpoint not yet implemented" });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async function meshApprove(ctx, args) {
|
|
830
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
831
|
+
if (isLocalTransport(ctx.transport)) {
|
|
832
|
+
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
833
|
+
const providerSessionId = cached?.providerSessionId;
|
|
834
|
+
const result = await commandForNode(ctx, node, "resolve_action", {
|
|
835
|
+
sessionId: args.session_id,
|
|
836
|
+
targetSessionId: args.session_id,
|
|
837
|
+
workspace: node.workspace,
|
|
838
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
839
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
840
|
+
action: args.action === "reject" ? "reject" : "approve"
|
|
841
|
+
});
|
|
842
|
+
return JSON.stringify(result, null, 2);
|
|
843
|
+
} else {
|
|
844
|
+
return JSON.stringify({ error: "Cloud mesh approve not yet implemented" });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function meshCloneNode(ctx, args) {
|
|
848
|
+
const sourceNode = await findNodeWithRefresh(ctx, args.source_node_id);
|
|
849
|
+
if (isLocalTransport(ctx.transport)) {
|
|
850
|
+
const result = await commandForNode(ctx, sourceNode, "clone_mesh_node", {
|
|
851
|
+
meshId: ctx.mesh.id,
|
|
852
|
+
sourceNodeId: args.source_node_id,
|
|
853
|
+
branch: args.branch,
|
|
854
|
+
baseBranch: args.base_branch,
|
|
855
|
+
inlineMesh: ctx.mesh
|
|
856
|
+
});
|
|
857
|
+
const clonePayload = extractCloneNodePayload(result);
|
|
858
|
+
if (clonePayload?.success && clonePayload.node?.id) {
|
|
859
|
+
const existingIndex = ctx.mesh.nodes.findIndex((n) => n.id === clonePayload.node.id);
|
|
860
|
+
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
861
|
+
else ctx.mesh.nodes.push(clonePayload.node);
|
|
862
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
863
|
+
}
|
|
864
|
+
return JSON.stringify(result, null, 2);
|
|
865
|
+
} else {
|
|
866
|
+
return JSON.stringify({ error: "Cloud mesh clone_node not yet implemented" });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function meshCleanupSessions(ctx, args) {
|
|
870
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
871
|
+
if (isLocalTransport(ctx.transport)) {
|
|
872
|
+
const result = await commandForNode(ctx, node, "cleanup_mesh_sessions", {
|
|
873
|
+
meshId: ctx.mesh.id,
|
|
874
|
+
nodeId: args.node_id,
|
|
875
|
+
mode: args.mode,
|
|
876
|
+
sessionIds: args.session_ids,
|
|
877
|
+
dryRun: args.dry_run === true,
|
|
878
|
+
inlineMesh: ctx.mesh
|
|
879
|
+
});
|
|
880
|
+
return JSON.stringify(result, null, 2);
|
|
881
|
+
} else {
|
|
882
|
+
return JSON.stringify({ error: "Cloud mesh cleanup_sessions not yet implemented" });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function meshRemoveNode(ctx, args) {
|
|
886
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
887
|
+
if (isLocalTransport(ctx.transport)) {
|
|
888
|
+
const result = await commandForNode(ctx, node, "remove_mesh_node", {
|
|
889
|
+
meshId: ctx.mesh.id,
|
|
890
|
+
nodeId: args.node_id,
|
|
891
|
+
...args.session_cleanup_mode ? { sessionCleanupMode: args.session_cleanup_mode } : {},
|
|
892
|
+
inlineMesh: ctx.mesh
|
|
893
|
+
});
|
|
894
|
+
if (result?.success && result.removed !== false) {
|
|
895
|
+
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
896
|
+
if (idx >= 0) {
|
|
897
|
+
ctx.mesh.nodes.splice(idx, 1);
|
|
898
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return JSON.stringify(result, null, 2);
|
|
902
|
+
} else {
|
|
903
|
+
return JSON.stringify({ error: "Cloud mesh remove_node not yet implemented" });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/help.ts
|
|
908
|
+
var STANDARD_TOOLS = [
|
|
909
|
+
"list_daemons",
|
|
910
|
+
"list_sessions",
|
|
911
|
+
"launch_session",
|
|
912
|
+
"stop_session",
|
|
913
|
+
"check_pending",
|
|
914
|
+
"read_chat",
|
|
915
|
+
"read_chat_debug",
|
|
916
|
+
"send_chat",
|
|
917
|
+
"approve",
|
|
918
|
+
"git_status",
|
|
919
|
+
"git_log",
|
|
920
|
+
"git_diff",
|
|
921
|
+
"git_checkpoint",
|
|
922
|
+
"git_push",
|
|
923
|
+
"screenshot"
|
|
924
|
+
];
|
|
925
|
+
function buildMcpHelpText() {
|
|
926
|
+
const meshTools = ALL_MESH_TOOLS.map((tool) => tool.name);
|
|
927
|
+
return `
|
|
928
|
+
adhdev-mcp \u2014 ADHDev MCP Server
|
|
929
|
+
|
|
930
|
+
Usage:
|
|
931
|
+
adhdev-mcp Local mode (requires standalone daemon)
|
|
932
|
+
adhdev-mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
933
|
+
adhdev-mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
934
|
+
adhdev-mcp --repo-mesh <mesh_id> Mesh mode (coordinator-scoped tools)
|
|
935
|
+
|
|
936
|
+
Options:
|
|
937
|
+
--mode <mode> Transport: local, cloud, or ipc
|
|
938
|
+
--port <n> Standalone or IPC daemon port (defaults: local 3847, ipc 19222)
|
|
939
|
+
--password <pass> Standalone daemon password (if set)
|
|
940
|
+
--api-key <key> ADHDev cloud API key (switches to cloud mode)
|
|
941
|
+
--base-url <url> Override cloud API base URL
|
|
942
|
+
--repo-mesh <mesh_id> Enable mesh mode \u2014 exposes only mesh-scoped coordinator tools
|
|
943
|
+
--help Show this help
|
|
944
|
+
|
|
945
|
+
Environment variables:
|
|
946
|
+
ADHDEV_API_KEY API key (cloud mode)
|
|
947
|
+
ADHDEV_PASSWORD Daemon password (local mode)
|
|
948
|
+
ADHDEV_MESH_ID Mesh ID (mesh mode)
|
|
949
|
+
ADHDEV_MCP_TRANSPORT Transport: local, cloud, or ipc
|
|
950
|
+
|
|
951
|
+
Standard tools: ${STANDARD_TOOLS.join(", ")}
|
|
952
|
+
Mesh tools: ${meshTools.join(", ")}
|
|
953
|
+
`.trim();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/server.ts
|
|
957
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
958
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
959
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
960
|
+
|
|
961
|
+
// src/transports/local.ts
|
|
962
|
+
var DEFAULT_PORT = 3847;
|
|
963
|
+
var LocalTransport = class {
|
|
964
|
+
baseUrl;
|
|
965
|
+
authHeader;
|
|
966
|
+
constructor(opts = {}) {
|
|
967
|
+
this.baseUrl = `http://localhost:${opts.port ?? DEFAULT_PORT}`;
|
|
968
|
+
this.authHeader = opts.password ? `Bearer ${opts.password}` : null;
|
|
969
|
+
}
|
|
970
|
+
headers() {
|
|
971
|
+
const h = { "Content-Type": "application/json" };
|
|
972
|
+
if (this.authHeader) h["Authorization"] = this.authHeader;
|
|
973
|
+
return h;
|
|
974
|
+
}
|
|
975
|
+
async getStatus() {
|
|
976
|
+
const res = await fetch(`${this.baseUrl}/api/v1/status`, { headers: this.headers() });
|
|
977
|
+
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
978
|
+
return res.json();
|
|
979
|
+
}
|
|
980
|
+
async command(type, args = {}) {
|
|
981
|
+
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
982
|
+
method: "POST",
|
|
983
|
+
headers: this.headers(),
|
|
984
|
+
body: JSON.stringify({ type, ...args })
|
|
985
|
+
});
|
|
986
|
+
if (!res.ok) {
|
|
987
|
+
const text = await res.text().catch(() => res.statusText);
|
|
988
|
+
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
989
|
+
}
|
|
990
|
+
return res.json();
|
|
991
|
+
}
|
|
992
|
+
async ping() {
|
|
993
|
+
try {
|
|
994
|
+
await this.getStatus();
|
|
995
|
+
return true;
|
|
996
|
+
} catch {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/transports/cloud.ts
|
|
1003
|
+
var DEFAULT_BASE_URL = "https://api.adhf.dev";
|
|
1004
|
+
var CloudTransport = class {
|
|
1005
|
+
baseUrl;
|
|
1006
|
+
apiKey;
|
|
1007
|
+
constructor(opts) {
|
|
1008
|
+
this.apiKey = opts.apiKey;
|
|
1009
|
+
this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
1010
|
+
}
|
|
1011
|
+
headers() {
|
|
1012
|
+
return {
|
|
1013
|
+
"Content-Type": "application/json",
|
|
1014
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
async listDaemons() {
|
|
1018
|
+
const res = await fetch(`${this.baseUrl}/api/v1/daemons`, { headers: this.headers() });
|
|
1019
|
+
if (!res.ok) throw new Error(`List daemons failed: ${res.status}`);
|
|
1020
|
+
return res.json();
|
|
1021
|
+
}
|
|
1022
|
+
async getStatus(targetId) {
|
|
1023
|
+
const res = await fetch(
|
|
1024
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/status`,
|
|
1025
|
+
{ headers: this.headers() }
|
|
1026
|
+
);
|
|
1027
|
+
if (!res.ok) throw new Error(`Status failed: ${res.status}`);
|
|
1028
|
+
return res.json();
|
|
1029
|
+
}
|
|
1030
|
+
/** Get all sessions for a daemon (returns CompactSessionEntry[]). */
|
|
1031
|
+
async getDaemonStatus(daemonId) {
|
|
1032
|
+
const res = await fetch(
|
|
1033
|
+
`${this.baseUrl}/api/v1/daemons/${encodeURIComponent(daemonId)}/status`,
|
|
1034
|
+
{ headers: this.headers() }
|
|
1035
|
+
);
|
|
1036
|
+
if (!res.ok) throw new Error(`Daemon status failed: ${res.status}`);
|
|
1037
|
+
return res.json();
|
|
1038
|
+
}
|
|
1039
|
+
async readChat(targetId, opts = {}) {
|
|
1040
|
+
const params = new URLSearchParams();
|
|
1041
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
1042
|
+
if (opts.sessionId) params.set("sessionId", opts.sessionId);
|
|
1043
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
1044
|
+
const res = await fetch(
|
|
1045
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat${qs}`,
|
|
1046
|
+
{ headers: this.headers() }
|
|
1047
|
+
);
|
|
1048
|
+
if (!res.ok) throw new Error(`Read chat failed: ${res.status}`);
|
|
1049
|
+
return res.json();
|
|
1050
|
+
}
|
|
1051
|
+
async getChatDebugBundle(targetId, opts = {}) {
|
|
1052
|
+
const res = await fetch(
|
|
1053
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat/debug`,
|
|
1054
|
+
{
|
|
1055
|
+
method: "POST",
|
|
1056
|
+
headers: this.headers(),
|
|
1057
|
+
body: JSON.stringify({
|
|
1058
|
+
...opts.agentType ? { agentType: opts.agentType } : {},
|
|
1059
|
+
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
1060
|
+
...opts.tailLimit ? { tailLimit: opts.tailLimit } : {},
|
|
1061
|
+
...opts.delivery ? { delivery: opts.delivery } : {}
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
if (!res.ok) throw new Error(`Chat debug bundle failed: ${res.status}`);
|
|
1066
|
+
return res.json();
|
|
1067
|
+
}
|
|
1068
|
+
async sendChat(targetId, message, opts = {}) {
|
|
1069
|
+
const res = await fetch(
|
|
1070
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat`,
|
|
1071
|
+
{
|
|
1072
|
+
method: "POST",
|
|
1073
|
+
headers: this.headers(),
|
|
1074
|
+
body: JSON.stringify({ message, ...opts })
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
if (!res.ok) throw new Error(`Send chat failed: ${res.status}`);
|
|
1078
|
+
return res.json();
|
|
1079
|
+
}
|
|
1080
|
+
async approve(targetId, action, agentType) {
|
|
1081
|
+
const res = await fetch(
|
|
1082
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/approve`,
|
|
1083
|
+
{
|
|
1084
|
+
method: "POST",
|
|
1085
|
+
headers: this.headers(),
|
|
1086
|
+
body: JSON.stringify({ action, ...agentType ? { agentType } : {} })
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
1090
|
+
return res.json();
|
|
1091
|
+
}
|
|
1092
|
+
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
1093
|
+
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
1094
|
+
const res = await fetch(
|
|
1095
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
1096
|
+
{ headers: this.headers() }
|
|
1097
|
+
);
|
|
1098
|
+
if (!res.ok) throw new Error(`Git status failed: ${res.status}`);
|
|
1099
|
+
return res.json();
|
|
1100
|
+
}
|
|
1101
|
+
async stop(daemonId, opts) {
|
|
1102
|
+
const res = await fetch(
|
|
1103
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/stop`,
|
|
1104
|
+
{
|
|
1105
|
+
method: "POST",
|
|
1106
|
+
headers: this.headers(),
|
|
1107
|
+
body: JSON.stringify(opts)
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
if (!res.ok) throw new Error(`Stop failed: ${res.status}`);
|
|
1111
|
+
return res.json();
|
|
1112
|
+
}
|
|
1113
|
+
async launch(daemonId, opts) {
|
|
1114
|
+
const res = await fetch(
|
|
1115
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/launch`,
|
|
1116
|
+
{
|
|
1117
|
+
method: "POST",
|
|
1118
|
+
headers: this.headers(),
|
|
1119
|
+
body: JSON.stringify(opts)
|
|
1120
|
+
}
|
|
1121
|
+
);
|
|
1122
|
+
if (!res.ok) throw new Error(`Launch failed: ${res.status}`);
|
|
1123
|
+
return res.json();
|
|
1124
|
+
}
|
|
1125
|
+
async gitLog(daemonId, workspace, opts = {}) {
|
|
1126
|
+
const params = new URLSearchParams({ workspace });
|
|
1127
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
1128
|
+
if (opts.file) params.set("file", opts.file);
|
|
1129
|
+
if (opts.since) params.set("since", opts.since);
|
|
1130
|
+
if (opts.until) params.set("until", opts.until);
|
|
1131
|
+
const res = await fetch(
|
|
1132
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-log?${params}`,
|
|
1133
|
+
{ headers: this.headers() }
|
|
1134
|
+
);
|
|
1135
|
+
if (!res.ok) throw new Error(`Git log failed: ${res.status}`);
|
|
1136
|
+
return res.json();
|
|
1137
|
+
}
|
|
1138
|
+
async gitDiff(daemonId, workspace, opts = {}) {
|
|
1139
|
+
const params = new URLSearchParams({ workspace });
|
|
1140
|
+
if (opts.file) params.set("file", opts.file);
|
|
1141
|
+
if (opts.maxLines) params.set("maxLines", String(opts.maxLines));
|
|
1142
|
+
if (opts.staged) params.set("staged", "true");
|
|
1143
|
+
const res = await fetch(
|
|
1144
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-diff?${params}`,
|
|
1145
|
+
{ headers: this.headers() }
|
|
1146
|
+
);
|
|
1147
|
+
if (!res.ok) throw new Error(`Git diff failed: ${res.status}`);
|
|
1148
|
+
return res.json();
|
|
1149
|
+
}
|
|
1150
|
+
async gitPush(daemonId, opts) {
|
|
1151
|
+
const res = await fetch(
|
|
1152
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-push`,
|
|
1153
|
+
{
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
headers: this.headers(),
|
|
1156
|
+
body: JSON.stringify(opts)
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
if (!res.ok) throw new Error(`Git push failed: ${res.status}`);
|
|
1160
|
+
return res.json();
|
|
1161
|
+
}
|
|
1162
|
+
async gitCheckpoint(daemonId, opts) {
|
|
1163
|
+
const res = await fetch(
|
|
1164
|
+
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-checkpoint`,
|
|
1165
|
+
{
|
|
1166
|
+
method: "POST",
|
|
1167
|
+
headers: this.headers(),
|
|
1168
|
+
body: JSON.stringify(opts)
|
|
1169
|
+
}
|
|
1170
|
+
);
|
|
1171
|
+
if (!res.ok) throw new Error(`Git checkpoint failed: ${res.status}`);
|
|
1172
|
+
return res.json();
|
|
1173
|
+
}
|
|
1174
|
+
async ping() {
|
|
1175
|
+
try {
|
|
1176
|
+
await this.listDaemons();
|
|
1177
|
+
return true;
|
|
1178
|
+
} catch {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// src/tools/list-sessions.ts
|
|
1185
|
+
var FORMAT_PROP = {
|
|
1186
|
+
format: {
|
|
1187
|
+
type: "string",
|
|
1188
|
+
enum: ["text", "json"],
|
|
1189
|
+
description: "Output format: 'text' (default, human-readable) or 'json' (structured, for programmatic use)."
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var LIST_SESSIONS_TOOL = {
|
|
1193
|
+
name: "list_sessions",
|
|
1194
|
+
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.",
|
|
1195
|
+
inputSchema: {
|
|
1196
|
+
type: "object",
|
|
1197
|
+
properties: {
|
|
1198
|
+
daemon_id: {
|
|
1199
|
+
type: "string",
|
|
1200
|
+
description: "Daemon ID (cloud mode only). Omit to list sessions across all daemons."
|
|
1201
|
+
},
|
|
1202
|
+
...FORMAT_PROP
|
|
1203
|
+
},
|
|
1204
|
+
required: []
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
async function listSessions(transport, args = {}) {
|
|
1208
|
+
const asJson = args.format === "json";
|
|
1209
|
+
if (isLocalTransport(transport)) {
|
|
1210
|
+
const status = await transport.getStatus();
|
|
1211
|
+
const sessions = status?.sessions ?? [];
|
|
1212
|
+
if (asJson) {
|
|
1213
|
+
return JSON.stringify({
|
|
1214
|
+
sessions: sessions.map((s) => ({
|
|
1215
|
+
id: s.id,
|
|
1216
|
+
type: s.providerType ?? s.type ?? "unknown",
|
|
1217
|
+
label: s.label ?? null,
|
|
1218
|
+
status: s.status ?? s.agentStatus ?? null,
|
|
1219
|
+
workspace: s.workspace ?? null
|
|
1220
|
+
}))
|
|
1221
|
+
}, null, 2);
|
|
1222
|
+
}
|
|
1223
|
+
if (sessions.length === 0) return "No active sessions.";
|
|
1224
|
+
const lines = sessions.map((s) => {
|
|
1225
|
+
const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
|
|
1226
|
+
if (s.label) parts.push(`label: ${s.label}`);
|
|
1227
|
+
if (s.status ?? s.agentStatus) parts.push(`status: ${s.status ?? s.agentStatus}`);
|
|
1228
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
1229
|
+
return parts.join(", ");
|
|
1230
|
+
});
|
|
1231
|
+
return `Sessions (${sessions.length}):
|
|
1232
|
+
${lines.join("\n")}`;
|
|
784
1233
|
}
|
|
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}` : ""}`);
|
|
1234
|
+
return listSessionsCloud(transport, args.daemon_id, asJson);
|
|
1235
|
+
}
|
|
1236
|
+
async function listSessionsCloud(transport, daemonId, asJson) {
|
|
1237
|
+
const collected = [];
|
|
1238
|
+
if (daemonId) {
|
|
1239
|
+
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
1240
|
+
for (const s of daemonStatus?.sessions ?? []) {
|
|
1241
|
+
collected.push({ daemonId, session: s });
|
|
799
1242
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1243
|
+
} else {
|
|
1244
|
+
const data = await transport.listDaemons();
|
|
1245
|
+
const daemons = data?.daemons ?? [];
|
|
1246
|
+
for (let i = 0; i < daemons.length; i += 5) {
|
|
1247
|
+
await Promise.allSettled(
|
|
1248
|
+
daemons.slice(i, i + 5).map(async (d) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
1251
|
+
for (const s of daemonStatus?.sessions ?? []) {
|
|
1252
|
+
collected.push({ daemonId: d.id, session: s });
|
|
1253
|
+
}
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
803
1258
|
}
|
|
804
1259
|
}
|
|
805
|
-
|
|
1260
|
+
if (asJson) {
|
|
1261
|
+
return JSON.stringify({
|
|
1262
|
+
sessions: collected.map(({ daemonId: dId, session: s }) => ({
|
|
1263
|
+
daemon_id: dId,
|
|
1264
|
+
id: s.id,
|
|
1265
|
+
type: s.providerType ?? "unknown",
|
|
1266
|
+
status: s.status ?? null,
|
|
1267
|
+
workspace: s.workspace ?? null
|
|
1268
|
+
}))
|
|
1269
|
+
}, null, 2);
|
|
1270
|
+
}
|
|
1271
|
+
if (collected.length === 0) return "No active sessions.";
|
|
1272
|
+
const lines = collected.map(({ daemonId: dId, session: s }) => {
|
|
1273
|
+
const parts = [
|
|
1274
|
+
`daemon: ${dId}`,
|
|
1275
|
+
`session: ${s.id}`,
|
|
1276
|
+
`type: ${s.providerType ?? "unknown"}`
|
|
1277
|
+
];
|
|
1278
|
+
if (s.status) parts.push(`status: ${s.status}`);
|
|
1279
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
1280
|
+
return parts.join(", ");
|
|
1281
|
+
});
|
|
1282
|
+
return `Sessions (${collected.length}):
|
|
1283
|
+
${lines.join("\n")}`;
|
|
806
1284
|
}
|
|
807
1285
|
|
|
808
|
-
// src/tools/
|
|
809
|
-
var
|
|
810
|
-
name: "
|
|
811
|
-
description: "
|
|
1286
|
+
// src/tools/list-daemons.ts
|
|
1287
|
+
var LIST_DAEMONS_TOOL = {
|
|
1288
|
+
name: "list_daemons",
|
|
1289
|
+
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
1290
|
inputSchema: {
|
|
813
1291
|
type: "object",
|
|
814
1292
|
properties: {
|
|
815
|
-
|
|
1293
|
+
...FORMAT_PROP
|
|
1294
|
+
},
|
|
1295
|
+
required: []
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
async function listDaemons(transport, args = {}) {
|
|
1299
|
+
const asJson = args.format === "json";
|
|
1300
|
+
if (isLocalTransport(transport)) {
|
|
1301
|
+
const status = await transport.getStatus();
|
|
1302
|
+
const daemon = {
|
|
1303
|
+
id: status?.id ?? status?.instanceId ?? "standalone",
|
|
1304
|
+
hostname: status?.hostname ?? status?.machine?.hostname ?? "localhost",
|
|
1305
|
+
platform: status?.platform ?? status?.machine?.platform ?? "unknown",
|
|
1306
|
+
version: status?.version ?? null,
|
|
1307
|
+
sessions: (status?.sessions ?? []).length
|
|
1308
|
+
};
|
|
1309
|
+
if (asJson) return JSON.stringify({ daemons: [daemon] }, null, 2);
|
|
1310
|
+
return `Daemons (1):
|
|
1311
|
+
id: ${daemon.id}, hostname: ${daemon.hostname}, platform: ${daemon.platform}${daemon.version ? `, version: ${daemon.version}` : ""}, sessions: ${daemon.sessions}`;
|
|
1312
|
+
}
|
|
1313
|
+
const data = await transport.listDaemons();
|
|
1314
|
+
const daemons = data?.daemons ?? [];
|
|
1315
|
+
if (asJson) {
|
|
1316
|
+
return JSON.stringify({
|
|
1317
|
+
daemons: daemons.map((d) => ({
|
|
1318
|
+
id: d.id,
|
|
1319
|
+
hostname: d.hostname ?? null,
|
|
1320
|
+
platform: d.platform ?? null,
|
|
1321
|
+
nickname: d.nickname ?? null,
|
|
1322
|
+
version: d.version ?? null,
|
|
1323
|
+
p2p_available: d.p2p?.available ?? null,
|
|
1324
|
+
cdp_connected: d.cdpConnected ?? null
|
|
1325
|
+
}))
|
|
1326
|
+
}, null, 2);
|
|
1327
|
+
}
|
|
1328
|
+
if (daemons.length === 0) return "No connected daemons.";
|
|
1329
|
+
const lines = daemons.map((d) => {
|
|
1330
|
+
const parts = [`id: ${d.id}`];
|
|
1331
|
+
if (d.nickname) parts.push(`nickname: ${d.nickname}`);
|
|
1332
|
+
if (d.hostname) parts.push(`hostname: ${d.hostname}`);
|
|
1333
|
+
if (d.platform) parts.push(`platform: ${d.platform}`);
|
|
1334
|
+
if (d.version) parts.push(`version: ${d.version}`);
|
|
1335
|
+
if (d.p2p?.available != null) parts.push(`p2p: ${d.p2p.available ? "yes" : "no"}`);
|
|
1336
|
+
return parts.join(", ");
|
|
1337
|
+
});
|
|
1338
|
+
return `Daemons (${daemons.length}):
|
|
1339
|
+
${lines.join("\n")}`;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/tools/read-chat.ts
|
|
1343
|
+
var READ_CHAT_TOOL = {
|
|
1344
|
+
name: "read_chat",
|
|
1345
|
+
description: "Read the current chat conversation from an IDE agent session. Returns recent messages.",
|
|
1346
|
+
inputSchema: {
|
|
1347
|
+
type: "object",
|
|
1348
|
+
properties: {
|
|
1349
|
+
session_id: {
|
|
816
1350
|
type: "string",
|
|
817
|
-
description: "
|
|
1351
|
+
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
1352
|
},
|
|
819
1353
|
limit: {
|
|
820
1354
|
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)."
|
|
1355
|
+
description: "Max messages to return (default: 50)."
|
|
834
1356
|
},
|
|
835
1357
|
daemon_id: {
|
|
836
1358
|
type: "string",
|
|
837
|
-
description: "Daemon ID (cloud mode only
|
|
1359
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
1360
|
+
},
|
|
1361
|
+
compact: {
|
|
1362
|
+
type: "boolean",
|
|
1363
|
+
description: "Opt-in compact mode: filters tool/terminal/system/internal/control/debug/status chatter and returns user-visible messages plus lightweight summary metadata."
|
|
838
1364
|
},
|
|
839
1365
|
...FORMAT_PROP
|
|
840
1366
|
},
|
|
841
|
-
required: [
|
|
1367
|
+
required: []
|
|
842
1368
|
}
|
|
843
1369
|
};
|
|
844
|
-
async function
|
|
845
|
-
const limit =
|
|
846
|
-
let raw;
|
|
1370
|
+
async function readChat(transport, args) {
|
|
1371
|
+
const limit = args.limit ?? 50;
|
|
847
1372
|
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 } : {}
|
|
1373
|
+
const result2 = await transport.command("read_chat", {
|
|
1374
|
+
...args.session_id ? { targetSessionId: args.session_id } : {},
|
|
1375
|
+
tailLimit: limit
|
|
854
1376
|
});
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
limit,
|
|
860
|
-
file: args.file,
|
|
861
|
-
since: args.since,
|
|
862
|
-
until: args.until
|
|
1377
|
+
const annotated2 = annotateRapidReadChatAdvisory(result2, {
|
|
1378
|
+
key: `local:${args.session_id ?? "__active__"}`,
|
|
1379
|
+
toolName: "read_chat",
|
|
1380
|
+
completionCallbackExpected: false
|
|
863
1381
|
});
|
|
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}`;
|
|
1382
|
+
return formatChatResult(annotated2, args.session_id, args.format, limit, args.compact);
|
|
870
1383
|
}
|
|
871
|
-
if (!
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1384
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1385
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1386
|
+
const result = await transport.readChat(targetId, { limit, sessionId: args.session_id });
|
|
1387
|
+
const annotated = annotateRapidReadChatAdvisory(result, {
|
|
1388
|
+
key: `cloud:${args.daemon_id}:${args.session_id ?? "__active__"}`,
|
|
1389
|
+
toolName: "read_chat",
|
|
1390
|
+
completionCallbackExpected: false
|
|
1391
|
+
});
|
|
1392
|
+
return formatChatResult(annotated, args.session_id, args.format, limit, args.compact);
|
|
1393
|
+
}
|
|
1394
|
+
function formatChatResult(result, sessionId, format, limit = 50, compact = false) {
|
|
1395
|
+
if (!result?.success && result?.error) {
|
|
1396
|
+
if (format === "json") return JSON.stringify({ error: result.error, messages: [] }, null, 2);
|
|
1397
|
+
return `Error: ${result.error}`;
|
|
875
1398
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
1399
|
+
const messages = result?.messages ?? result?.data?.messages ?? [];
|
|
1400
|
+
const source = { ...result, messages };
|
|
1401
|
+
const compactPayload = compact ? compactChatPayload(source, { sessionId: sessionId ?? null, limit }) : null;
|
|
1402
|
+
const outputMessages = compact ? compactPayload.messages : messages;
|
|
1403
|
+
if (format === "json") {
|
|
1404
|
+
if (compact && compactPayload) {
|
|
1405
|
+
return JSON.stringify({
|
|
1406
|
+
session_id: sessionId ?? null,
|
|
1407
|
+
...compactPayload,
|
|
1408
|
+
...result?.pollingAdvisory ? { pollingAdvisory: result.pollingAdvisory } : {},
|
|
1409
|
+
messages: compactPayload.messages.map((m) => ({
|
|
1410
|
+
role: m.role,
|
|
1411
|
+
kind: m.kind ?? null,
|
|
1412
|
+
content: messageContent(m),
|
|
1413
|
+
timestamp: m.timestamp ?? null
|
|
1414
|
+
}))
|
|
1415
|
+
}, null, 2);
|
|
1416
|
+
}
|
|
878
1417
|
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
|
|
1418
|
+
session_id: sessionId ?? null,
|
|
1419
|
+
...result?.pollingAdvisory ? { pollingAdvisory: result.pollingAdvisory } : {},
|
|
1420
|
+
messages: outputMessages.slice(-limit).map((m) => ({
|
|
1421
|
+
role: m.role,
|
|
1422
|
+
kind: m.kind ?? null,
|
|
1423
|
+
content: messageContent(m),
|
|
1424
|
+
timestamp: m.timestamp ?? null
|
|
1425
|
+
}))
|
|
891
1426
|
}, null, 2);
|
|
892
1427
|
}
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1428
|
+
if (outputMessages.length === 0) {
|
|
1429
|
+
return result?.pollingAdvisory ? `No messages in chat.
|
|
1430
|
+
|
|
1431
|
+
Advisory: ${result.pollingAdvisory.message}` : "No messages in chat.";
|
|
1432
|
+
}
|
|
1433
|
+
const lines = outputMessages.slice(-limit).map((m) => {
|
|
1434
|
+
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
1435
|
+
const content = messageContent(m);
|
|
1436
|
+
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
1437
|
+
return `[${role}] ${truncated}`;
|
|
899
1438
|
});
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1439
|
+
if (result?.pollingAdvisory) {
|
|
1440
|
+
lines.push(`Advisory: ${result.pollingAdvisory.message}`);
|
|
1441
|
+
}
|
|
1442
|
+
return lines.join("\n\n");
|
|
903
1443
|
}
|
|
904
1444
|
|
|
905
|
-
// src/tools/
|
|
906
|
-
var
|
|
907
|
-
name: "
|
|
908
|
-
description: "
|
|
1445
|
+
// src/tools/read-chat-debug.ts
|
|
1446
|
+
var READ_CHAT_DEBUG_TOOL = {
|
|
1447
|
+
name: "read_chat_debug",
|
|
1448
|
+
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
1449
|
inputSchema: {
|
|
910
1450
|
type: "object",
|
|
911
1451
|
properties: {
|
|
912
|
-
|
|
1452
|
+
session_id: {
|
|
1453
|
+
type: "string",
|
|
1454
|
+
description: "Target session ID (from list_sessions). Required for reliable routing."
|
|
1455
|
+
},
|
|
1456
|
+
daemon_id: {
|
|
913
1457
|
type: "string",
|
|
914
|
-
description: "
|
|
1458
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
915
1459
|
},
|
|
916
|
-
|
|
1460
|
+
agent_type: {
|
|
917
1461
|
type: "string",
|
|
918
|
-
description: "
|
|
1462
|
+
description: "Optional provider/agent type hint, e.g. hermes-cli, claude-cli, codex-cli."
|
|
919
1463
|
},
|
|
920
|
-
|
|
1464
|
+
limit: {
|
|
921
1465
|
type: "number",
|
|
922
|
-
description: "Max
|
|
923
|
-
},
|
|
924
|
-
staged: {
|
|
925
|
-
type: "boolean",
|
|
926
|
-
description: "Show staged changes instead of unstaged (default: false)."
|
|
1466
|
+
description: "Max read_chat tail messages embedded in the bundle (default: 40)."
|
|
927
1467
|
},
|
|
928
|
-
|
|
1468
|
+
delivery: {
|
|
929
1469
|
type: "string",
|
|
930
|
-
|
|
1470
|
+
enum: ["daemon_file", "inline"],
|
|
1471
|
+
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
1472
|
},
|
|
932
1473
|
...FORMAT_PROP
|
|
933
1474
|
},
|
|
934
|
-
required: ["
|
|
1475
|
+
required: ["session_id"]
|
|
935
1476
|
}
|
|
936
1477
|
};
|
|
937
|
-
async function
|
|
938
|
-
const
|
|
939
|
-
|
|
1478
|
+
async function readChatDebug(transport, args) {
|
|
1479
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
1480
|
+
if (!sessionId) throw new Error("session_id is required");
|
|
1481
|
+
const tailLimit = args.limit ?? 40;
|
|
1482
|
+
const delivery = args.delivery === "inline" ? "inline" : "daemon_file";
|
|
1483
|
+
const commandArgs = {
|
|
1484
|
+
targetSessionId: sessionId,
|
|
1485
|
+
tailLimit,
|
|
1486
|
+
...args.agent_type ? { agentType: args.agent_type, providerType: args.agent_type } : {},
|
|
1487
|
+
...delivery === "daemon_file" ? { delivery: "daemon_file" } : {}
|
|
1488
|
+
};
|
|
1489
|
+
let result;
|
|
940
1490
|
if (isLocalTransport(transport)) {
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return `Git diff error: ${result.error}`;
|
|
1491
|
+
result = await transport.command("get_chat_debug_bundle", commandArgs);
|
|
1492
|
+
} else {
|
|
1493
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1494
|
+
const targetId = `${args.daemon_id}:session:${sessionId}`;
|
|
1495
|
+
result = await transport.getChatDebugBundle(targetId, {
|
|
1496
|
+
sessionId,
|
|
1497
|
+
agentType: args.agent_type,
|
|
1498
|
+
tailLimit,
|
|
1499
|
+
delivery
|
|
1500
|
+
});
|
|
952
1501
|
}
|
|
953
|
-
return
|
|
1502
|
+
return formatChatDebugResult(result, { sessionId, delivery, format: args.format });
|
|
954
1503
|
}
|
|
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;
|
|
1504
|
+
function formatChatDebugResult(result, options) {
|
|
1505
|
+
if (!result?.success && result?.error) {
|
|
1506
|
+
if (options.format === "json") return JSON.stringify({ success: false, error: result.error }, null, 2);
|
|
1507
|
+
return `Error: ${result.error}`;
|
|
990
1508
|
}
|
|
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.";
|
|
1509
|
+
if (options.format === "json") {
|
|
1510
|
+
return JSON.stringify(result, null, 2);
|
|
995
1511
|
}
|
|
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);
|
|
1512
|
+
if (result?.delivery === "daemon_file") {
|
|
1513
|
+
const summary = result.summary && typeof result.summary === "object" ? result.summary : {};
|
|
1514
|
+
return [
|
|
1515
|
+
"ADHDev chat debug bundle saved on daemon.",
|
|
1516
|
+
`session_id: ${options.sessionId}`,
|
|
1517
|
+
`bundle_id: ${String(result.bundleId || "")}`,
|
|
1518
|
+
`saved_path: ${String(result.savedPath || "")}`,
|
|
1519
|
+
`size_bytes: ${String(result.sizeBytes || "")}`,
|
|
1520
|
+
`created_at: ${String(result.createdAt || "")}`,
|
|
1521
|
+
`read_chat_status: ${String(summary.readChatStatus || "")}`,
|
|
1522
|
+
`read_chat_total_messages: ${String(summary.readChatTotalMessages ?? "")}`,
|
|
1523
|
+
`cli_status: ${String(summary.cliStatus || "")}`,
|
|
1524
|
+
`cli_message_count: ${String(summary.cliMessageCount ?? "")}`
|
|
1525
|
+
].join("\n");
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof result?.text === "string") return result.text;
|
|
1528
|
+
if (result?.bundle) return JSON.stringify(result.bundle, null, 2);
|
|
1529
|
+
return JSON.stringify(result, null, 2);
|
|
1023
1530
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1531
|
+
|
|
1532
|
+
// src/tools/send-chat.ts
|
|
1533
|
+
var SEND_CHAT_TOOL = {
|
|
1534
|
+
name: "send_chat",
|
|
1535
|
+
description: "Send a message to an IDE agent session.",
|
|
1536
|
+
inputSchema: {
|
|
1537
|
+
type: "object",
|
|
1538
|
+
properties: {
|
|
1539
|
+
message: {
|
|
1540
|
+
type: "string",
|
|
1541
|
+
description: "The message to send to the agent."
|
|
1542
|
+
},
|
|
1543
|
+
session_id: {
|
|
1544
|
+
type: "string",
|
|
1545
|
+
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
1546
|
+
},
|
|
1547
|
+
daemon_id: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
required: ["message"]
|
|
1034
1553
|
}
|
|
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
|
-
}
|
|
1554
|
+
};
|
|
1555
|
+
async function sendChat(transport, args) {
|
|
1556
|
+
if (!args.message?.trim()) throw new Error("message is required");
|
|
1557
|
+
if (isLocalTransport(transport)) {
|
|
1558
|
+
const result2 = await transport.command("send_chat", {
|
|
1559
|
+
message: args.message,
|
|
1560
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1561
|
+
});
|
|
1562
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "send_chat failed"}`;
|
|
1563
|
+
return "Message sent.";
|
|
1053
1564
|
}
|
|
1054
|
-
|
|
1565
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1566
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1567
|
+
const result = await transport.sendChat(targetId, args.message, {
|
|
1568
|
+
...args.session_id ? { sessionId: args.session_id } : {}
|
|
1569
|
+
});
|
|
1570
|
+
if (result?.success === false) return `Error: ${result.error ?? "send_chat failed"}`;
|
|
1571
|
+
return "Message sent.";
|
|
1055
1572
|
}
|
|
1056
1573
|
|
|
1057
|
-
// src/tools/
|
|
1058
|
-
var
|
|
1059
|
-
name: "
|
|
1060
|
-
description: "
|
|
1574
|
+
// src/tools/approve.ts
|
|
1575
|
+
var APPROVE_TOOL = {
|
|
1576
|
+
name: "approve",
|
|
1577
|
+
description: "Approve or reject a pending agent action (e.g. file write, command execution).",
|
|
1061
1578
|
inputSchema: {
|
|
1062
1579
|
type: "object",
|
|
1063
1580
|
properties: {
|
|
1064
|
-
|
|
1581
|
+
action: {
|
|
1065
1582
|
type: "string",
|
|
1066
|
-
|
|
1583
|
+
enum: ["approve", "reject"],
|
|
1584
|
+
description: "Whether to approve or reject the pending action."
|
|
1067
1585
|
},
|
|
1068
|
-
|
|
1586
|
+
session_id: {
|
|
1069
1587
|
type: "string",
|
|
1070
|
-
description:
|
|
1071
|
-
},
|
|
1072
|
-
include_untracked: {
|
|
1073
|
-
type: "boolean",
|
|
1074
|
-
description: "Also stage and commit untracked files (default: false)."
|
|
1588
|
+
description: "Target session ID. Omit to use the active session."
|
|
1075
1589
|
},
|
|
1076
1590
|
daemon_id: {
|
|
1077
1591
|
type: "string",
|
|
1078
|
-
description: "Daemon ID (cloud mode only
|
|
1592
|
+
description: "Daemon ID (cloud mode only)."
|
|
1079
1593
|
}
|
|
1080
1594
|
},
|
|
1081
|
-
required: ["
|
|
1595
|
+
required: ["action"]
|
|
1082
1596
|
}
|
|
1083
1597
|
};
|
|
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;
|
|
1598
|
+
async function approve(transport, args) {
|
|
1599
|
+
const action = args.action === "reject" ? "reject" : "approve";
|
|
1089
1600
|
if (isLocalTransport(transport)) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
includeUntracked: args.include_untracked ?? false
|
|
1601
|
+
const result2 = await transport.command("resolve_action", {
|
|
1602
|
+
action,
|
|
1603
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1094
1604
|
});
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1605
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "resolve_action failed"}`;
|
|
1606
|
+
return `Action ${action}d.`;
|
|
1607
|
+
}
|
|
1608
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1609
|
+
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
1610
|
+
const result = await transport.approve(targetId, action);
|
|
1611
|
+
if (result?.success === false) return `Error: ${result.error ?? "approve failed"}`;
|
|
1612
|
+
return `Action ${action}d.`;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/tools/screenshot.ts
|
|
1616
|
+
var SCREENSHOT_TOOL = {
|
|
1617
|
+
name: "screenshot",
|
|
1618
|
+
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.",
|
|
1619
|
+
inputSchema: {
|
|
1620
|
+
type: "object",
|
|
1621
|
+
properties: {
|
|
1622
|
+
session_id: {
|
|
1623
|
+
type: "string",
|
|
1624
|
+
description: "Target session ID. Omit to use the active session."
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
required: []
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
async function screenshot(transport, args) {
|
|
1631
|
+
let result;
|
|
1632
|
+
if (isLocalTransport(transport)) {
|
|
1633
|
+
result = await transport.command("screenshot", {
|
|
1634
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
1102
1635
|
});
|
|
1103
|
-
|
|
1636
|
+
} else {
|
|
1637
|
+
return { type: "text", text: "Screenshots are not available in cloud mode. Run adhdev mcp in local mode (requires standalone daemon)." };
|
|
1104
1638
|
}
|
|
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}`;
|
|
1639
|
+
if (result?.success === false) {
|
|
1640
|
+
return { type: "text", text: `Error: ${result.error ?? "screenshot failed"}` };
|
|
1111
1641
|
}
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1642
|
+
const b64 = result?.base64 ?? result?.screenshot ?? result?.result;
|
|
1643
|
+
if (!b64) {
|
|
1644
|
+
return { type: "text", text: "Screenshot captured but no image data returned." };
|
|
1645
|
+
}
|
|
1646
|
+
const mimeType = result?.format === "png" ? "image/png" : "image/webp";
|
|
1647
|
+
return { type: "image", data: b64, mimeType };
|
|
1115
1648
|
}
|
|
1116
1649
|
|
|
1117
|
-
// src/tools/git-
|
|
1118
|
-
var
|
|
1119
|
-
name: "
|
|
1120
|
-
description: "
|
|
1650
|
+
// src/tools/git-status.ts
|
|
1651
|
+
var GIT_STATUS_TOOL = {
|
|
1652
|
+
name: "git_status",
|
|
1653
|
+
description: "Get git repository status for a workspace on the daemon machine.",
|
|
1121
1654
|
inputSchema: {
|
|
1122
1655
|
type: "object",
|
|
1123
1656
|
properties: {
|
|
@@ -1125,529 +1658,688 @@ var GIT_PUSH_TOOL = {
|
|
|
1125
1658
|
type: "string",
|
|
1126
1659
|
description: "Absolute path to the workspace/repository directory."
|
|
1127
1660
|
},
|
|
1128
|
-
|
|
1129
|
-
type: "
|
|
1130
|
-
description:
|
|
1131
|
-
},
|
|
1132
|
-
branch: {
|
|
1133
|
-
type: "string",
|
|
1134
|
-
description: "Branch to push (default: current branch)."
|
|
1661
|
+
include_diff: {
|
|
1662
|
+
type: "boolean",
|
|
1663
|
+
description: "Include changed file list (default: true)."
|
|
1135
1664
|
},
|
|
1136
1665
|
daemon_id: {
|
|
1137
1666
|
type: "string",
|
|
1138
|
-
description: "Daemon ID (cloud mode only
|
|
1139
|
-
}
|
|
1667
|
+
description: "Daemon ID (cloud mode only)."
|
|
1668
|
+
},
|
|
1669
|
+
...FORMAT_PROP
|
|
1140
1670
|
},
|
|
1141
1671
|
required: ["workspace"]
|
|
1142
1672
|
}
|
|
1143
1673
|
};
|
|
1144
|
-
async function
|
|
1145
|
-
let
|
|
1674
|
+
async function gitStatus(transport, args) {
|
|
1675
|
+
let status;
|
|
1676
|
+
let diffSummary;
|
|
1146
1677
|
if (isLocalTransport(transport)) {
|
|
1147
|
-
|
|
1148
|
-
workspace: args.workspace
|
|
1149
|
-
remote: args.remote ?? "origin",
|
|
1150
|
-
...args.branch ? { branch: args.branch } : {}
|
|
1678
|
+
const statusResult = await transport.command("git_status", {
|
|
1679
|
+
workspace: args.workspace
|
|
1151
1680
|
});
|
|
1152
|
-
|
|
1681
|
+
status = statusResult?.status ?? statusResult;
|
|
1682
|
+
if (args.include_diff !== false) {
|
|
1683
|
+
const diffResult = await transport.command("git_diff_summary", {
|
|
1684
|
+
workspace: args.workspace
|
|
1685
|
+
});
|
|
1686
|
+
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
1687
|
+
}
|
|
1153
1688
|
} else {
|
|
1154
1689
|
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1155
|
-
const result = await transport.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1690
|
+
const result = await transport.gitStatus(
|
|
1691
|
+
args.daemon_id,
|
|
1692
|
+
args.workspace,
|
|
1693
|
+
args.include_diff !== false
|
|
1694
|
+
);
|
|
1695
|
+
if (result?.error) {
|
|
1696
|
+
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
1697
|
+
return `Error: ${result.error}`;
|
|
1698
|
+
}
|
|
1699
|
+
status = result?.status;
|
|
1700
|
+
diffSummary = result?.diff;
|
|
1161
1701
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
const msg =
|
|
1164
|
-
return
|
|
1702
|
+
if (status?.success === false || status?.reason) {
|
|
1703
|
+
const msg = status?.error ?? status?.reason ?? "unknown";
|
|
1704
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1705
|
+
return `Git error: ${msg}`;
|
|
1165
1706
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1707
|
+
if (!status?.isGitRepo) {
|
|
1708
|
+
if (args.format === "json") return JSON.stringify({ error: `Not a git repository: ${args.workspace}` }, null, 2);
|
|
1709
|
+
return `Not a git repository: ${args.workspace}`;
|
|
1710
|
+
}
|
|
1711
|
+
if (args.format === "json") {
|
|
1712
|
+
const files = diffSummary?.files?.map((f) => ({
|
|
1713
|
+
path: f.path,
|
|
1714
|
+
old_path: f.oldPath ?? null,
|
|
1715
|
+
status: f.status ?? "M",
|
|
1716
|
+
insertions: f.insertions ?? 0,
|
|
1717
|
+
deletions: f.deletions ?? 0
|
|
1718
|
+
})) ?? [];
|
|
1719
|
+
return JSON.stringify({
|
|
1720
|
+
branch: status.branch ?? null,
|
|
1721
|
+
head_commit: status.headCommit ?? null,
|
|
1722
|
+
head_message: status.headMessage ?? null,
|
|
1723
|
+
ahead: status.ahead ?? 0,
|
|
1724
|
+
behind: status.behind ?? 0,
|
|
1725
|
+
staged: status.staged ?? 0,
|
|
1726
|
+
modified: status.modified ?? 0,
|
|
1727
|
+
untracked: status.untracked ?? 0,
|
|
1728
|
+
deleted: status.deleted ?? 0,
|
|
1729
|
+
stash_count: status.stashCount ?? 0,
|
|
1730
|
+
has_conflicts: status.hasConflicts ?? false,
|
|
1731
|
+
dirty: status.dirty ?? false,
|
|
1732
|
+
changed_files: files,
|
|
1733
|
+
total_insertions: diffSummary?.totalInsertions ?? 0,
|
|
1734
|
+
total_deletions: diffSummary?.totalDeletions ?? 0
|
|
1735
|
+
}, null, 2);
|
|
1736
|
+
}
|
|
1737
|
+
const lines = [];
|
|
1738
|
+
if (status.branch) lines.push(`Branch: ${status.branch}`);
|
|
1739
|
+
if (status.headCommit) {
|
|
1740
|
+
lines.push(`HEAD: ${status.headCommit.slice(0, 7)}${status.headMessage ? ` \u2014 ${status.headMessage.slice(0, 80)}` : ""}`);
|
|
1741
|
+
}
|
|
1742
|
+
if (status.ahead > 0) lines.push(`Ahead: ${status.ahead}`);
|
|
1743
|
+
if (status.behind > 0) lines.push(`Behind: ${status.behind}`);
|
|
1744
|
+
if (status.staged > 0) lines.push(`Staged: ${status.staged}`);
|
|
1745
|
+
if (status.modified > 0) lines.push(`Modified: ${status.modified}`);
|
|
1746
|
+
if (status.untracked > 0) lines.push(`Untracked: ${status.untracked}`);
|
|
1747
|
+
if (status.deleted > 0) lines.push(`Deleted: ${status.deleted}`);
|
|
1748
|
+
if (status.stashCount > 0) lines.push(`Stashes: ${status.stashCount}`);
|
|
1749
|
+
if (status.hasConflicts) lines.push("Conflicts: YES");
|
|
1750
|
+
if (!status.dirty) lines.push("Working tree: clean");
|
|
1751
|
+
if (diffSummary?.files?.length > 0) {
|
|
1752
|
+
lines.push("");
|
|
1753
|
+
lines.push(`Changed files (${diffSummary.files.length}):`);
|
|
1754
|
+
for (const f of diffSummary.files.slice(0, 20)) {
|
|
1755
|
+
lines.push(` ${f.status ?? "M"} ${f.path}${f.oldPath ? ` (was ${f.oldPath})` : ""}${f.insertions || f.deletions ? ` +${f.insertions ?? 0}/-${f.deletions ?? 0}` : ""}`);
|
|
1756
|
+
}
|
|
1757
|
+
if (diffSummary.files.length > 20) lines.push(` \u2026 and ${diffSummary.files.length - 20} more`);
|
|
1758
|
+
if (diffSummary.totalInsertions || diffSummary.totalDeletions) {
|
|
1759
|
+
lines.push(`Total: +${diffSummary.totalInsertions ?? 0}/-${diffSummary.totalDeletions ?? 0}`);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return lines.join("\n");
|
|
1172
1763
|
}
|
|
1173
1764
|
|
|
1174
|
-
// src/tools/
|
|
1175
|
-
var
|
|
1176
|
-
name: "
|
|
1177
|
-
description: "
|
|
1765
|
+
// src/tools/git-log.ts
|
|
1766
|
+
var GIT_LOG_TOOL = {
|
|
1767
|
+
name: "git_log",
|
|
1768
|
+
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
1769
|
inputSchema: {
|
|
1179
1770
|
type: "object",
|
|
1180
1771
|
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
1772
|
workspace: {
|
|
1186
1773
|
type: "string",
|
|
1187
|
-
description: "
|
|
1774
|
+
description: "Absolute path to the workspace/repository directory."
|
|
1188
1775
|
},
|
|
1189
|
-
|
|
1776
|
+
limit: {
|
|
1777
|
+
type: "number",
|
|
1778
|
+
description: "Max commits to return (default: 20, max: 100)."
|
|
1779
|
+
},
|
|
1780
|
+
file: {
|
|
1190
1781
|
type: "string",
|
|
1191
|
-
description: "
|
|
1782
|
+
description: "Filter history to commits that touched this repo-relative file path (optional)."
|
|
1192
1783
|
},
|
|
1193
|
-
|
|
1784
|
+
since: {
|
|
1194
1785
|
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: {
|
|
1786
|
+
description: "Only commits after this date (ISO 8601 or git date string, optional)."
|
|
1787
|
+
},
|
|
1788
|
+
until: {
|
|
1230
1789
|
type: "string",
|
|
1231
|
-
description: "
|
|
1790
|
+
description: "Only commits before this date (ISO 8601 or git date string, optional)."
|
|
1232
1791
|
},
|
|
1233
1792
|
daemon_id: {
|
|
1234
1793
|
type: "string",
|
|
1235
1794
|
description: "Daemon ID (cloud mode only, required)."
|
|
1236
1795
|
},
|
|
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
|
-
}
|
|
1796
|
+
...FORMAT_PROP
|
|
1241
1797
|
},
|
|
1242
|
-
required: ["
|
|
1798
|
+
required: ["workspace"]
|
|
1243
1799
|
}
|
|
1244
1800
|
};
|
|
1245
|
-
async function
|
|
1801
|
+
async function gitLog(transport, args) {
|
|
1802
|
+
const limit = Math.max(1, Math.min(100, args.limit ?? 20));
|
|
1803
|
+
let raw;
|
|
1246
1804
|
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
|
|
1805
|
+
raw = await transport.command("git_log", {
|
|
1806
|
+
workspace: args.workspace,
|
|
1807
|
+
limit,
|
|
1808
|
+
...args.file ? { path: args.file } : {},
|
|
1809
|
+
...args.since ? { since: args.since } : {},
|
|
1810
|
+
...args.until ? { until: args.until } : {}
|
|
1260
1811
|
});
|
|
1261
|
-
|
|
1262
|
-
|
|
1812
|
+
raw = raw?.log ?? raw;
|
|
1813
|
+
} else {
|
|
1814
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1815
|
+
const result = await transport.gitLog(args.daemon_id, args.workspace, {
|
|
1816
|
+
limit,
|
|
1817
|
+
file: args.file,
|
|
1818
|
+
since: args.since,
|
|
1819
|
+
until: args.until
|
|
1820
|
+
});
|
|
1821
|
+
raw = result?.log ?? result;
|
|
1822
|
+
}
|
|
1823
|
+
if (raw?.success === false || raw?.reason) {
|
|
1824
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
1825
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1826
|
+
return `Git log error: ${msg}`;
|
|
1827
|
+
}
|
|
1828
|
+
if (!raw?.isGitRepo) {
|
|
1829
|
+
const msg = `Not a git repository: ${args.workspace}`;
|
|
1830
|
+
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1831
|
+
return msg;
|
|
1832
|
+
}
|
|
1833
|
+
const entries = raw?.entries ?? [];
|
|
1834
|
+
if (args.format === "json") {
|
|
1835
|
+
return JSON.stringify({
|
|
1836
|
+
workspace: raw.workspace,
|
|
1837
|
+
branch: raw.branch ?? null,
|
|
1838
|
+
entries: entries.map((e) => ({
|
|
1839
|
+
commit: e.commit,
|
|
1840
|
+
short: e.commit?.slice(0, 7),
|
|
1841
|
+
message: e.message,
|
|
1842
|
+
author: e.authorName ?? null,
|
|
1843
|
+
author_email: e.authorEmail ?? null,
|
|
1844
|
+
authored_at: e.authoredAt ? new Date(e.authoredAt).toISOString() : null
|
|
1845
|
+
})),
|
|
1846
|
+
total: entries.length,
|
|
1847
|
+
truncated: raw.truncated ?? false
|
|
1848
|
+
}, null, 2);
|
|
1263
1849
|
}
|
|
1264
|
-
if (
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1850
|
+
if (entries.length === 0) return "No commits found.";
|
|
1851
|
+
const lines = entries.map((e) => {
|
|
1852
|
+
const hash = e.commit?.slice(0, 7) ?? "???????";
|
|
1853
|
+
const date = e.authoredAt ? new Date(e.authoredAt).toISOString().slice(0, 10) : "";
|
|
1854
|
+
const author = e.authorName ? ` (${e.authorName})` : "";
|
|
1855
|
+
return `${hash} ${date}${author} ${e.message}`;
|
|
1268
1856
|
});
|
|
1269
|
-
|
|
1270
|
-
return
|
|
1857
|
+
const header = `Commits (${entries.length}${raw.truncated ? ", truncated" : ""}):`;
|
|
1858
|
+
return `${header}
|
|
1859
|
+
${lines.join("\n")}`;
|
|
1271
1860
|
}
|
|
1272
1861
|
|
|
1273
|
-
// src/tools/
|
|
1274
|
-
var
|
|
1275
|
-
name: "
|
|
1276
|
-
description: "
|
|
1862
|
+
// src/tools/git-diff.ts
|
|
1863
|
+
var GIT_DIFF_TOOL = {
|
|
1864
|
+
name: "git_diff",
|
|
1865
|
+
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
1866
|
inputSchema: {
|
|
1278
1867
|
type: "object",
|
|
1279
1868
|
properties: {
|
|
1869
|
+
workspace: {
|
|
1870
|
+
type: "string",
|
|
1871
|
+
description: "Absolute path to the workspace/repository directory."
|
|
1872
|
+
},
|
|
1873
|
+
file: {
|
|
1874
|
+
type: "string",
|
|
1875
|
+
description: "Specific repo-relative file path to diff (optional \u2014 if omitted, returns top 5 changed files)."
|
|
1876
|
+
},
|
|
1877
|
+
max_lines: {
|
|
1878
|
+
type: "number",
|
|
1879
|
+
description: "Max diff lines per file before truncating (default: 300)."
|
|
1880
|
+
},
|
|
1881
|
+
staged: {
|
|
1882
|
+
type: "boolean",
|
|
1883
|
+
description: "Show staged changes instead of unstaged (default: false)."
|
|
1884
|
+
},
|
|
1280
1885
|
daemon_id: {
|
|
1281
1886
|
type: "string",
|
|
1282
|
-
description: "Daemon ID
|
|
1887
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
1283
1888
|
},
|
|
1284
1889
|
...FORMAT_PROP
|
|
1285
1890
|
},
|
|
1286
|
-
required: []
|
|
1891
|
+
required: ["workspace"]
|
|
1287
1892
|
}
|
|
1288
1893
|
};
|
|
1289
|
-
async function
|
|
1894
|
+
async function gitDiff(transport, args) {
|
|
1895
|
+
const maxLines = Math.max(10, Math.min(2e3, args.max_lines ?? 300));
|
|
1896
|
+
const staged = args.staged ?? false;
|
|
1290
1897
|
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);
|
|
1898
|
+
return localGitDiff(transport, args.workspace, args.file, maxLines, staged, args.format);
|
|
1311
1899
|
}
|
|
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 ");
|
|
1900
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
1901
|
+
const result = await transport.gitDiff(args.daemon_id, args.workspace, {
|
|
1902
|
+
file: args.file,
|
|
1903
|
+
maxLines,
|
|
1904
|
+
staged
|
|
1321
1905
|
});
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
${
|
|
1906
|
+
if (result?.error) {
|
|
1907
|
+
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
1908
|
+
return `Git diff error: ${result.error}`;
|
|
1909
|
+
}
|
|
1910
|
+
return formatDiffResult(result, args.format);
|
|
1325
1911
|
}
|
|
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
|
-
);
|
|
1912
|
+
async function localGitDiff(transport, workspace, file, maxLines, staged, format) {
|
|
1913
|
+
if (file) {
|
|
1914
|
+
const raw = await transport.command("git_diff_file", { workspace, path: file, staged });
|
|
1915
|
+
const d = raw?.diff ?? raw;
|
|
1916
|
+
if (d?.success === false || d?.reason) {
|
|
1917
|
+
const msg = d?.error ?? d?.reason ?? "unknown";
|
|
1918
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1919
|
+
return `Git diff error: ${msg}`;
|
|
1350
1920
|
}
|
|
1921
|
+
const lines = (d?.diff ?? "").split("\n");
|
|
1922
|
+
const truncated = lines.length > maxLines;
|
|
1923
|
+
const result = {
|
|
1924
|
+
files: [{
|
|
1925
|
+
path: file,
|
|
1926
|
+
diff: truncated ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
|
|
1927
|
+
truncated,
|
|
1928
|
+
binary: d?.binary ?? false
|
|
1929
|
+
}],
|
|
1930
|
+
total_files: 1,
|
|
1931
|
+
shown_files: 1,
|
|
1932
|
+
truncated
|
|
1933
|
+
};
|
|
1934
|
+
return formatDiffResult(result, format);
|
|
1351
1935
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
type: s.providerType ?? null,
|
|
1359
|
-
modal_message: null,
|
|
1360
|
-
buttons: []
|
|
1361
|
-
}))
|
|
1362
|
-
}, null, 2);
|
|
1936
|
+
const summaryRaw = await transport.command("git_diff_summary", { workspace, staged });
|
|
1937
|
+
const summary = summaryRaw?.diffSummary ?? summaryRaw;
|
|
1938
|
+
if (summary?.success === false || summary?.reason) {
|
|
1939
|
+
const msg = summary?.error ?? summary?.reason ?? "unknown";
|
|
1940
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1941
|
+
return `Git diff error: ${msg}`;
|
|
1363
1942
|
}
|
|
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);
|
|
1943
|
+
if (!summary?.isGitRepo) {
|
|
1944
|
+
const msg = `Not a git repository: ${workspace}`;
|
|
1945
|
+
if (format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
1946
|
+
return msg;
|
|
1386
1947
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1948
|
+
const files = summary?.files ?? [];
|
|
1949
|
+
if (files.length === 0) {
|
|
1950
|
+
if (format === "json") return JSON.stringify({ files: [], total_files: 0, shown_files: 0, truncated: false }, null, 2);
|
|
1951
|
+
return "No changed files.";
|
|
1389
1952
|
}
|
|
1390
|
-
|
|
1953
|
+
const topFiles = files.slice(0, 5);
|
|
1954
|
+
const fileDiffs = await Promise.all(
|
|
1955
|
+
topFiles.map(async (f) => {
|
|
1956
|
+
try {
|
|
1957
|
+
const raw = await transport.command("git_diff_file", { workspace, path: f.path, staged });
|
|
1958
|
+
const d = raw?.diff ?? raw;
|
|
1959
|
+
const lines = (d?.diff ?? "").split("\n");
|
|
1960
|
+
const trunc = lines.length > maxLines;
|
|
1961
|
+
return {
|
|
1962
|
+
path: f.path,
|
|
1963
|
+
old_path: f.oldPath ?? null,
|
|
1964
|
+
status: f.status ?? "M",
|
|
1965
|
+
diff: trunc ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
|
|
1966
|
+
truncated: trunc,
|
|
1967
|
+
binary: d?.binary ?? false
|
|
1968
|
+
};
|
|
1969
|
+
} catch {
|
|
1970
|
+
return { path: f.path, diff: "", truncated: false, binary: false, error: "fetch failed" };
|
|
1971
|
+
}
|
|
1972
|
+
})
|
|
1973
|
+
);
|
|
1974
|
+
return formatDiffResult({
|
|
1975
|
+
files: fileDiffs,
|
|
1976
|
+
total_files: files.length,
|
|
1977
|
+
shown_files: topFiles.length,
|
|
1978
|
+
truncated: files.length > 5
|
|
1979
|
+
}, format);
|
|
1391
1980
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1981
|
+
function formatDiffResult(result, format) {
|
|
1982
|
+
if (format === "json") return JSON.stringify(result, null, 2);
|
|
1983
|
+
const files = result?.files ?? [];
|
|
1984
|
+
if (files.length === 0) return "No changed files.";
|
|
1985
|
+
const parts = [];
|
|
1986
|
+
const totalShown = result?.shown_files ?? files.length;
|
|
1987
|
+
const totalAll = result?.total_files ?? files.length;
|
|
1988
|
+
if (totalAll > totalShown) {
|
|
1989
|
+
parts.push(`Showing ${totalShown} of ${totalAll} changed files:
|
|
1990
|
+
`);
|
|
1398
1991
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1992
|
+
for (const f of files) {
|
|
1993
|
+
const header = `--- ${f.path}${f.old_path ? ` (was ${f.old_path})` : ""} ---`;
|
|
1994
|
+
if (f.error) {
|
|
1995
|
+
parts.push(`${header}
|
|
1996
|
+
(error: ${f.error})
|
|
1997
|
+
`);
|
|
1998
|
+
} else if (f.binary) {
|
|
1999
|
+
parts.push(`${header}
|
|
2000
|
+
(binary file)
|
|
2001
|
+
`);
|
|
2002
|
+
} else if (!f.diff) {
|
|
2003
|
+
parts.push(`${header}
|
|
2004
|
+
(no diff)
|
|
2005
|
+
`);
|
|
2006
|
+
} else {
|
|
2007
|
+
parts.push(`${header}
|
|
2008
|
+
${f.diff}${f.truncated ? "" : "\n"}`);
|
|
2009
|
+
}
|
|
1406
2010
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
2011
|
+
return parts.join("\n");
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/tools/git-checkpoint.ts
|
|
2015
|
+
var GIT_CHECKPOINT_TOOL = {
|
|
2016
|
+
name: "git_checkpoint",
|
|
2017
|
+
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
2018
|
inputSchema: {
|
|
1412
2019
|
type: "object",
|
|
1413
2020
|
properties: {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
2021
|
+
workspace: {
|
|
2022
|
+
type: "string",
|
|
2023
|
+
description: "Absolute path to the workspace/repository directory."
|
|
2024
|
+
},
|
|
2025
|
+
message: {
|
|
2026
|
+
type: "string",
|
|
2027
|
+
description: 'Checkpoint message (max 200 chars). Will be prefixed with "adhdev: checkpoint ".'
|
|
2028
|
+
},
|
|
2029
|
+
include_untracked: {
|
|
2030
|
+
type: "boolean",
|
|
2031
|
+
description: "Also stage and commit untracked files (default: false)."
|
|
2032
|
+
},
|
|
2033
|
+
daemon_id: {
|
|
2034
|
+
type: "string",
|
|
2035
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2036
|
+
}
|
|
1417
2037
|
},
|
|
1418
|
-
required: ["
|
|
2038
|
+
required: ["workspace", "message"]
|
|
1419
2039
|
}
|
|
1420
2040
|
};
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
2041
|
+
async function gitCheckpoint(transport, args) {
|
|
2042
|
+
const message = args.message?.trim();
|
|
2043
|
+
if (!message) return "Error: message is required";
|
|
2044
|
+
if (message.length > 200) return "Error: message must be 200 characters or fewer";
|
|
2045
|
+
let raw;
|
|
2046
|
+
if (isLocalTransport(transport)) {
|
|
2047
|
+
raw = await transport.command("git_checkpoint", {
|
|
2048
|
+
workspace: args.workspace,
|
|
2049
|
+
message,
|
|
2050
|
+
includeUntracked: args.include_untracked ?? false
|
|
2051
|
+
});
|
|
2052
|
+
raw = raw?.checkpoint ?? raw;
|
|
2053
|
+
} else {
|
|
2054
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2055
|
+
const result = await transport.gitCheckpoint(args.daemon_id, {
|
|
2056
|
+
workspace: args.workspace,
|
|
2057
|
+
message,
|
|
2058
|
+
includeUntracked: args.include_untracked ?? false
|
|
2059
|
+
});
|
|
2060
|
+
raw = result?.checkpoint ?? result;
|
|
1432
2061
|
}
|
|
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"]
|
|
2062
|
+
if (raw?.success === false || raw?.reason) {
|
|
2063
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
2064
|
+
if (msg.includes("Nothing to commit") || msg.includes("nothing to commit")) {
|
|
2065
|
+
return "Nothing to commit \u2014 working tree is clean.";
|
|
2066
|
+
}
|
|
2067
|
+
return `Git checkpoint error: ${msg}`;
|
|
1444
2068
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2069
|
+
const commit = raw?.commit?.slice(0, 7) ?? "???????";
|
|
2070
|
+
const fullMsg = raw?.message ?? `adhdev: checkpoint ${message}`;
|
|
2071
|
+
return `Checkpoint created: ${commit} \u2014 ${fullMsg}`;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/tools/git-push.ts
|
|
2075
|
+
var GIT_PUSH_TOOL = {
|
|
2076
|
+
name: "git_push",
|
|
2077
|
+
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
2078
|
inputSchema: {
|
|
1450
2079
|
type: "object",
|
|
1451
2080
|
properties: {
|
|
1452
|
-
|
|
2081
|
+
workspace: {
|
|
2082
|
+
type: "string",
|
|
2083
|
+
description: "Absolute path to the workspace/repository directory."
|
|
2084
|
+
},
|
|
2085
|
+
remote: {
|
|
2086
|
+
type: "string",
|
|
2087
|
+
description: 'Remote name (default: "origin").'
|
|
2088
|
+
},
|
|
2089
|
+
branch: {
|
|
2090
|
+
type: "string",
|
|
2091
|
+
description: "Branch to push (default: current branch)."
|
|
2092
|
+
},
|
|
2093
|
+
daemon_id: {
|
|
2094
|
+
type: "string",
|
|
2095
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2096
|
+
}
|
|
1453
2097
|
},
|
|
1454
|
-
required: ["
|
|
2098
|
+
required: ["workspace"]
|
|
1455
2099
|
}
|
|
1456
2100
|
};
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2101
|
+
async function gitPush(transport, args) {
|
|
2102
|
+
let raw;
|
|
2103
|
+
if (isLocalTransport(transport)) {
|
|
2104
|
+
raw = await transport.command("git_push", {
|
|
2105
|
+
workspace: args.workspace,
|
|
2106
|
+
remote: args.remote ?? "origin",
|
|
2107
|
+
...args.branch ? { branch: args.branch } : {}
|
|
2108
|
+
});
|
|
2109
|
+
raw = raw?.push ?? raw;
|
|
2110
|
+
} else {
|
|
2111
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2112
|
+
const result = await transport.gitPush(args.daemon_id, {
|
|
2113
|
+
workspace: args.workspace,
|
|
2114
|
+
remote: args.remote,
|
|
2115
|
+
branch: args.branch
|
|
2116
|
+
});
|
|
2117
|
+
raw = result?.push ?? result;
|
|
1467
2118
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
2119
|
+
if (raw?.success === false || raw?.reason) {
|
|
2120
|
+
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
2121
|
+
return `Git push error: ${msg}`;
|
|
2122
|
+
}
|
|
2123
|
+
const branch = raw?.branch ?? args.branch ?? "(current)";
|
|
2124
|
+
const remote = raw?.remote ?? args.remote ?? "origin";
|
|
2125
|
+
const newBranch = raw?.newBranch ? " [new branch]" : "";
|
|
2126
|
+
const output = raw?.output ? `
|
|
2127
|
+
${raw.output}` : "";
|
|
2128
|
+
return `Pushed ${branch} \u2192 ${remote}${newBranch}${output}`;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// src/tools/launch-session.ts
|
|
2132
|
+
var LAUNCH_SESSION_TOOL = {
|
|
2133
|
+
name: "launch_session",
|
|
2134
|
+
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
2135
|
inputSchema: {
|
|
1473
2136
|
type: "object",
|
|
1474
2137
|
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";
|
|
2138
|
+
type: {
|
|
2139
|
+
type: "string",
|
|
2140
|
+
description: "Provider type to launch. CLI examples: hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode."
|
|
2141
|
+
},
|
|
2142
|
+
workspace: {
|
|
2143
|
+
type: "string",
|
|
2144
|
+
description: "Working directory for the session. Defaults to the daemon default workspace."
|
|
2145
|
+
},
|
|
2146
|
+
model: {
|
|
2147
|
+
type: "string",
|
|
2148
|
+
description: "Model override for ACP agents (e.g. claude-opus-4-7)."
|
|
2149
|
+
},
|
|
2150
|
+
daemon_id: {
|
|
2151
|
+
type: "string",
|
|
2152
|
+
description: "Daemon ID (cloud mode only). Required in cloud mode."
|
|
1518
2153
|
}
|
|
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" });
|
|
2154
|
+
},
|
|
2155
|
+
required: ["type"]
|
|
1563
2156
|
}
|
|
2157
|
+
};
|
|
2158
|
+
async function launchSession(transport, args) {
|
|
2159
|
+
if (isLocalTransport(transport)) {
|
|
2160
|
+
const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
|
|
2161
|
+
const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
|
|
2162
|
+
const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
|
|
2163
|
+
const result2 = await transport.command(commandType, payload);
|
|
2164
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "launch failed"}`;
|
|
2165
|
+
const id2 = result2?.id ?? result2?.sessionId;
|
|
2166
|
+
return id2 ? `Session launched. id: ${id2}, type: ${args.type}` : `Launched: ${JSON.stringify(result2)}`;
|
|
2167
|
+
}
|
|
2168
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2169
|
+
const result = await transport.launch(args.daemon_id, {
|
|
2170
|
+
type: args.type,
|
|
2171
|
+
dir: args.workspace,
|
|
2172
|
+
model: args.model
|
|
2173
|
+
});
|
|
2174
|
+
if (result?.success === false || result?.error) return `Error: ${result.error ?? "launch failed"}`;
|
|
2175
|
+
const id = result?.id ?? result?.sessionId;
|
|
2176
|
+
return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
|
|
1564
2177
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2178
|
+
|
|
2179
|
+
// src/tools/stop-session.ts
|
|
2180
|
+
var STOP_SESSION_TOOL = {
|
|
2181
|
+
name: "stop_session",
|
|
2182
|
+
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.",
|
|
2183
|
+
inputSchema: {
|
|
2184
|
+
type: "object",
|
|
2185
|
+
properties: {
|
|
2186
|
+
session_id: {
|
|
2187
|
+
type: "string",
|
|
2188
|
+
description: "Session ID to stop (from list_sessions)."
|
|
2189
|
+
},
|
|
2190
|
+
daemon_id: {
|
|
2191
|
+
type: "string",
|
|
2192
|
+
description: "Daemon ID (cloud mode only, required)."
|
|
2193
|
+
},
|
|
2194
|
+
type: {
|
|
2195
|
+
type: "string",
|
|
2196
|
+
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."
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
required: ["session_id"]
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
async function stopSession(transport, args) {
|
|
2203
|
+
if (isLocalTransport(transport)) {
|
|
2204
|
+
const local = transport;
|
|
2205
|
+
let resolvedType = args.type;
|
|
2206
|
+
if (!resolvedType) {
|
|
2207
|
+
const status = await local.getStatus();
|
|
2208
|
+
const session = (status?.sessions ?? []).find((s) => s.id === args.session_id);
|
|
2209
|
+
resolvedType = session?.providerType ?? session?.type;
|
|
2210
|
+
}
|
|
2211
|
+
if (!resolvedType) {
|
|
2212
|
+
return `Error: could not resolve session type for ${args.session_id}. Pass type= explicitly.`;
|
|
2213
|
+
}
|
|
2214
|
+
const result2 = await local.command("stop_cli", {
|
|
1570
2215
|
targetSessionId: args.session_id,
|
|
1571
|
-
|
|
2216
|
+
cliType: resolvedType
|
|
1572
2217
|
});
|
|
1573
|
-
return
|
|
1574
|
-
|
|
1575
|
-
return JSON.stringify({ error: "Cloud mesh read_chat not yet implemented" });
|
|
2218
|
+
if (result2?.success === false) return `Error: ${result2.error ?? "stop failed"}`;
|
|
2219
|
+
return `Session ${args.session_id} stopped.`;
|
|
1576
2220
|
}
|
|
2221
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
2222
|
+
const result = await transport.stop(args.daemon_id, {
|
|
2223
|
+
id: args.session_id,
|
|
2224
|
+
...args.type ? { type: args.type } : {}
|
|
2225
|
+
});
|
|
2226
|
+
if (result?.success === false || result?.error) return `Error: ${result.error ?? "stop failed"}`;
|
|
2227
|
+
return `Session ${args.session_id} stopped.`;
|
|
1577
2228
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2229
|
+
|
|
2230
|
+
// src/tools/check-pending.ts
|
|
2231
|
+
var CHECK_PENDING_TOOL = {
|
|
2232
|
+
name: "check_pending",
|
|
2233
|
+
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.",
|
|
2234
|
+
inputSchema: {
|
|
2235
|
+
type: "object",
|
|
2236
|
+
properties: {
|
|
2237
|
+
daemon_id: {
|
|
2238
|
+
type: "string",
|
|
2239
|
+
description: "Daemon ID to check (cloud mode). Omit to check all daemons."
|
|
2240
|
+
},
|
|
2241
|
+
...FORMAT_PROP
|
|
2242
|
+
},
|
|
2243
|
+
required: []
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
async function checkPending(transport, args) {
|
|
2247
|
+
if (isLocalTransport(transport)) {
|
|
2248
|
+
return checkPendingLocal(transport, args.format);
|
|
1592
2249
|
}
|
|
2250
|
+
return checkPendingCloud(transport, args.daemon_id, args.format);
|
|
1593
2251
|
}
|
|
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
|
-
});
|
|
2252
|
+
async function checkPendingLocal(transport, format) {
|
|
2253
|
+
const status = await transport.getStatus();
|
|
2254
|
+
const sessions = status?.sessions ?? [];
|
|
2255
|
+
const pending = sessions.filter(
|
|
2256
|
+
(s) => s.status === "waiting_approval" || s.agentStatus === "waiting_approval"
|
|
2257
|
+
);
|
|
2258
|
+
if (format === "json") {
|
|
1611
2259
|
return JSON.stringify({
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2260
|
+
pending: pending.map((s) => ({
|
|
2261
|
+
session_id: s.id,
|
|
2262
|
+
workspace: s.workspace ?? null,
|
|
2263
|
+
type: s.providerType ?? null,
|
|
2264
|
+
modal_message: s.activeChat?.activeModal?.message ?? null,
|
|
2265
|
+
buttons: s.activeChat?.activeModal?.buttons ?? []
|
|
2266
|
+
}))
|
|
1616
2267
|
}, null, 2);
|
|
1617
|
-
} else {
|
|
1618
|
-
return JSON.stringify({ error: "No daemonId available for cloud git_status probe" });
|
|
1619
2268
|
}
|
|
2269
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
2270
|
+
const lines = pending.map((s) => {
|
|
2271
|
+
const modal = s.activeChat?.activeModal;
|
|
2272
|
+
const parts = [`session_id: ${s.id}`];
|
|
2273
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
2274
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
2275
|
+
if (modal?.message) parts.push(`prompt: ${modal.message}`);
|
|
2276
|
+
if (modal?.buttons?.length) parts.push(`buttons: ${modal.buttons.join(", ")}`);
|
|
2277
|
+
return parts.join("\n ");
|
|
2278
|
+
});
|
|
2279
|
+
return `Pending approvals (${pending.length}):
|
|
2280
|
+
|
|
2281
|
+
${lines.join("\n\n")}`;
|
|
1620
2282
|
}
|
|
1621
|
-
async function
|
|
1622
|
-
const
|
|
1623
|
-
if (
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
message: args.message
|
|
1630
|
-
});
|
|
1631
|
-
return JSON.stringify(result, null, 2);
|
|
2283
|
+
async function checkPendingCloud(transport, daemonId, format) {
|
|
2284
|
+
const pending = [];
|
|
2285
|
+
if (daemonId) {
|
|
2286
|
+
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
2287
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
2288
|
+
for (const s of sessions) {
|
|
2289
|
+
if (s.status === "waiting_approval") pending.push({ daemonId, session: s });
|
|
2290
|
+
}
|
|
1632
2291
|
} else {
|
|
1633
|
-
|
|
2292
|
+
const data = await transport.listDaemons();
|
|
2293
|
+
const daemons = data?.daemons ?? [];
|
|
2294
|
+
for (let i = 0; i < daemons.length; i += 5) {
|
|
2295
|
+
await Promise.allSettled(
|
|
2296
|
+
daemons.slice(i, i + 5).map(async (d) => {
|
|
2297
|
+
try {
|
|
2298
|
+
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
2299
|
+
const sessions = daemonStatus?.sessions ?? [];
|
|
2300
|
+
for (const s of sessions) {
|
|
2301
|
+
if (s.status === "waiting_approval") pending.push({ daemonId: d.id, session: s });
|
|
2302
|
+
}
|
|
2303
|
+
} catch {
|
|
2304
|
+
}
|
|
2305
|
+
})
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
1634
2308
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
return JSON.stringify({ error: "Cloud mesh approve not yet implemented" });
|
|
2309
|
+
if (format === "json") {
|
|
2310
|
+
return JSON.stringify({
|
|
2311
|
+
pending: pending.map(({ daemonId: dId, session: s }) => ({
|
|
2312
|
+
daemon_id: dId,
|
|
2313
|
+
session_id: s.id,
|
|
2314
|
+
workspace: s.workspace ?? null,
|
|
2315
|
+
type: s.providerType ?? null,
|
|
2316
|
+
modal_message: null,
|
|
2317
|
+
buttons: []
|
|
2318
|
+
}))
|
|
2319
|
+
}, null, 2);
|
|
1647
2320
|
}
|
|
2321
|
+
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
2322
|
+
const lines = pending.map(({ daemonId: dId, session: s }) => {
|
|
2323
|
+
const parts = [`daemon_id: ${dId}`, `session_id: ${s.id}`];
|
|
2324
|
+
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
2325
|
+
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
2326
|
+
parts.push("(use read_chat to see the approval prompt)");
|
|
2327
|
+
return parts.join("\n ");
|
|
2328
|
+
});
|
|
2329
|
+
return `Pending approvals (${pending.length}):
|
|
2330
|
+
|
|
2331
|
+
${lines.join("\n\n")}`;
|
|
1648
2332
|
}
|
|
1649
2333
|
|
|
1650
2334
|
// src/server.ts
|
|
2335
|
+
async function buildMeshModeCoordinatorPrompt(mesh) {
|
|
2336
|
+
try {
|
|
2337
|
+
const { buildCoordinatorSystemPrompt } = await import("@adhdev/daemon-core");
|
|
2338
|
+
return buildCoordinatorSystemPrompt({ mesh });
|
|
2339
|
+
} catch (e) {
|
|
2340
|
+
throw new Error(`Failed to build Repo Mesh coordinator prompt: ${e?.message ?? String(e)}`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1651
2343
|
async function startMcpServer(opts) {
|
|
1652
2344
|
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
2345
|
const alive = await transport.ping();
|
|
@@ -1703,6 +2395,7 @@ async function startMcpServer(opts) {
|
|
|
1703
2395
|
requireApprovalForDestructiveGit: true,
|
|
1704
2396
|
dirtyWorkspaceBehavior: "warn",
|
|
1705
2397
|
maxParallelTasks: 2,
|
|
2398
|
+
spawnedSessionVisibility: "visible",
|
|
1706
2399
|
...policy
|
|
1707
2400
|
},
|
|
1708
2401
|
coordinator,
|
|
@@ -1753,16 +2446,19 @@ async function startMcpServer(opts) {
|
|
|
1753
2446
|
`);
|
|
1754
2447
|
process.exit(1);
|
|
1755
2448
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2449
|
+
let localDaemonId;
|
|
2450
|
+
if (transport instanceof IpcTransport) {
|
|
2451
|
+
try {
|
|
2452
|
+
const statusResult = await transport.getStatus();
|
|
2453
|
+
const instanceId = typeof statusResult?.status?.instanceId === "string" ? statusResult.status.instanceId.trim() : "";
|
|
2454
|
+
if (instanceId) localDaemonId = instanceId;
|
|
2455
|
+
} catch {
|
|
2456
|
+
}
|
|
1763
2457
|
}
|
|
2458
|
+
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {} };
|
|
2459
|
+
const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
|
|
1764
2460
|
const server2 = new import_server.Server(
|
|
1765
|
-
{ name: "adhdev-mcp-server", version: "0.9.
|
|
2461
|
+
{ name: "adhdev-mcp-server", version: "0.9.76" },
|
|
1766
2462
|
{ capabilities: { tools: {}, resources: {} } }
|
|
1767
2463
|
);
|
|
1768
2464
|
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
@@ -1799,6 +2495,9 @@ async function startMcpServer(opts) {
|
|
|
1799
2495
|
case "mesh_read_chat":
|
|
1800
2496
|
text = await meshReadChat(meshCtx, a);
|
|
1801
2497
|
break;
|
|
2498
|
+
case "mesh_read_debug":
|
|
2499
|
+
text = await meshReadDebug(meshCtx, a);
|
|
2500
|
+
break;
|
|
1802
2501
|
case "mesh_launch_session":
|
|
1803
2502
|
text = await meshLaunchSession(meshCtx, a);
|
|
1804
2503
|
break;
|
|
@@ -1811,6 +2510,15 @@ async function startMcpServer(opts) {
|
|
|
1811
2510
|
case "mesh_approve":
|
|
1812
2511
|
text = await meshApprove(meshCtx, a);
|
|
1813
2512
|
break;
|
|
2513
|
+
case "mesh_clone_node":
|
|
2514
|
+
text = await meshCloneNode(meshCtx, a);
|
|
2515
|
+
break;
|
|
2516
|
+
case "mesh_remove_node":
|
|
2517
|
+
text = await meshRemoveNode(meshCtx, a);
|
|
2518
|
+
break;
|
|
2519
|
+
case "mesh_cleanup_sessions":
|
|
2520
|
+
text = await meshCleanupSessions(meshCtx, a);
|
|
2521
|
+
break;
|
|
1814
2522
|
default:
|
|
1815
2523
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1816
2524
|
}
|
|
@@ -1832,6 +2540,7 @@ async function startMcpServer(opts) {
|
|
|
1832
2540
|
STOP_SESSION_TOOL,
|
|
1833
2541
|
CHECK_PENDING_TOOL,
|
|
1834
2542
|
READ_CHAT_TOOL,
|
|
2543
|
+
READ_CHAT_DEBUG_TOOL,
|
|
1835
2544
|
SEND_CHAT_TOOL,
|
|
1836
2545
|
APPROVE_TOOL,
|
|
1837
2546
|
GIT_STATUS_TOOL,
|
|
@@ -1863,6 +2572,10 @@ async function startMcpServer(opts) {
|
|
|
1863
2572
|
const text = await readChat(transport, a);
|
|
1864
2573
|
return { content: [{ type: "text", text }] };
|
|
1865
2574
|
}
|
|
2575
|
+
case "read_chat_debug": {
|
|
2576
|
+
const text = await readChatDebug(transport, a);
|
|
2577
|
+
return { content: [{ type: "text", text }] };
|
|
2578
|
+
}
|
|
1866
2579
|
case "send_chat": {
|
|
1867
2580
|
const text = await sendChat(transport, { message: a.message, session_id: a.session_id, daemon_id: a.daemon_id });
|
|
1868
2581
|
return { content: [{ type: "text", text }] };
|
|
@@ -1987,33 +2700,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
1987
2700
|
return { mode, port, password, apiKey, baseUrl, meshId };
|
|
1988
2701
|
}
|
|
1989
2702
|
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());
|
|
2703
|
+
console.error(buildMcpHelpText());
|
|
2017
2704
|
}
|
|
2018
2705
|
startMcpServer(parseArgs(process.argv)).catch((err) => {
|
|
2019
2706
|
process.stderr.write(`[adhdev-mcp] Fatal: ${err?.message ?? err}
|