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.
Files changed (23) hide show
  1. package/dist/cli/index.js +6968 -2361
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.js +6746 -2189
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/vendor/mcp-server/index.js +1991 -1304
  7. package/vendor/mcp-server/index.js.map +1 -1
  8. package/vendor/session-host-daemon/index.js +21 -0
  9. package/vendor/session-host-daemon/index.js.map +1 -1
  10. package/vendor/session-host-daemon/index.mjs +22 -0
  11. package/vendor/session-host-daemon/index.mjs.map +1 -1
  12. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.mts +15 -1
  13. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.d.ts +15 -1
  14. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js +25 -0
  15. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.js.map +1 -1
  16. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs +24 -0
  17. package/vendor/session-host-daemon/node_modules/@adhdev/session-host-core/index.mjs.map +1 -1
  18. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.mts +15 -1
  19. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.d.ts +15 -1
  20. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js +25 -0
  21. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.js.map +1 -1
  22. package/vendor/terminal-mux-cli/node_modules/@adhdev/session-host-core/index.mjs +24 -0
  23. 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 15s`)));
301
- }, 15e3);
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/list-sessions.ts
353
- var FORMAT_PROP = {
354
- format: {
355
- type: "string",
356
- enum: ["text", "json"],
357
- description: "Output format: 'text' (default, human-readable) or 'json' (structured, for programmatic use)."
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
- var LIST_SESSIONS_TOOL = {
361
- name: "list_sessions",
362
- 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.",
363
- inputSchema: {
364
- type: "object",
365
- properties: {
366
- daemon_id: {
367
- type: "string",
368
- description: "Daemon ID (cloud mode only). Omit to list sessions across all daemons."
369
- },
370
- ...FORMAT_PROP
371
- },
372
- required: []
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
- async function listSessions(transport, args = {}) {
376
- const asJson = args.format === "json";
377
- if (isLocalTransport(transport)) {
378
- const status = await transport.getStatus();
379
- const sessions = status?.sessions ?? [];
380
- if (asJson) {
381
- return JSON.stringify({
382
- sessions: sessions.map((s) => ({
383
- id: s.id,
384
- type: s.providerType ?? s.type ?? "unknown",
385
- label: s.label ?? null,
386
- status: s.status ?? s.agentStatus ?? null,
387
- workspace: s.workspace ?? null
388
- }))
389
- }, null, 2);
390
- }
391
- if (sessions.length === 0) return "No active sessions.";
392
- const lines = sessions.map((s) => {
393
- const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
394
- if (s.label) parts.push(`label: ${s.label}`);
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
- return listSessionsCloud(transport, args.daemon_id, asJson);
300
+ if (ctx.localDaemonId) {
301
+ return ctx.mesh.nodes.find((n) => n.daemonId === ctx.localDaemonId);
302
+ }
303
+ return void 0;
403
304
  }
404
- async function listSessionsCloud(transport, daemonId, asJson) {
405
- const collected = [];
406
- if (daemonId) {
407
- const daemonStatus = await transport.getDaemonStatus(daemonId);
408
- for (const s of daemonStatus?.sessions ?? []) {
409
- collected.push({ daemonId, session: s });
410
- }
411
- } else {
412
- const data = await transport.listDaemons();
413
- const daemons = data?.daemons ?? [];
414
- for (let i = 0; i < daemons.length; i += 5) {
415
- await Promise.allSettled(
416
- daemons.slice(i, i + 5).map(async (d) => {
417
- try {
418
- const daemonStatus = await transport.getDaemonStatus(d.id);
419
- for (const s of daemonStatus?.sessions ?? []) {
420
- collected.push({ daemonId: d.id, session: s });
421
- }
422
- } catch {
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
- if (asJson) {
429
- return JSON.stringify({
430
- sessions: collected.map(({ daemonId: dId, session: s }) => ({
431
- daemon_id: dId,
432
- id: s.id,
433
- type: s.providerType ?? "unknown",
434
- status: s.status ?? null,
435
- workspace: s.workspace ?? null
436
- }))
437
- }, null, 2);
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
- if (collected.length === 0) return "No active sessions.";
440
- const lines = collected.map(({ daemonId: dId, session: s }) => {
441
- const parts = [
442
- `daemon: ${dId}`,
443
- `session: ${s.id}`,
444
- `type: ${s.providerType ?? "unknown"}`
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
- // src/tools/list-daemons.ts
455
- var LIST_DAEMONS_TOOL = {
456
- name: "list_daemons",
457
- 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.",
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
- async function listDaemons(transport, args = {}) {
467
- const asJson = args.format === "json";
468
- if (isLocalTransport(transport)) {
469
- const status = await transport.getStatus();
470
- const daemon = {
471
- id: status?.id ?? status?.instanceId ?? "standalone",
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
- if (daemons.length === 0) return "No connected daemons.";
497
- const lines = daemons.map((d) => {
498
- const parts = [`id: ${d.id}`];
499
- if (d.nickname) parts.push(`nickname: ${d.nickname}`);
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
- session_id: {
518
- type: "string",
519
- 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."
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
- async function readChat(transport, args) {
535
- const limit = args.limit ?? 50;
536
- if (isLocalTransport(transport)) {
537
- const result2 = await transport.command("read_chat", {
538
- ...args.session_id ? { targetSessionId: args.session_id } : {},
539
- tailLimit: limit
540
- });
541
- return formatChatResult(result2, args.session_id, args.format, limit);
542
- }
543
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
544
- const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
545
- const result = await transport.readChat(targetId, { limit, sessionId: args.session_id });
546
- return formatChatResult(result, args.session_id, args.format, limit);
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
- const messages = result?.messages ?? result?.data?.messages ?? [];
554
- if (format === "json") {
555
- return JSON.stringify({
556
- session_id: sessionId ?? null,
557
- messages: messages.slice(-limit).map((m) => ({
558
- role: m.role,
559
- kind: m.kind ?? null,
560
- content: typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => typeof p === "string" ? p : p?.text ?? "").join("") : "",
561
- timestamp: m.timestamp ?? null
562
- }))
563
- }, null, 2);
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
- if (messages.length === 0) return "No messages in chat.";
566
- const lines = messages.slice(-limit).map((m) => {
567
- const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
568
- const content = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => typeof p === "string" ? p : p?.text ?? "").join("") : "";
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
- message: {
583
- type: "string",
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: ["message"]
465
+ required: ["node_id"]
596
466
  }
597
467
  };
598
- async function sendChat(transport, args) {
599
- if (!args.message?.trim()) throw new Error("message is required");
600
- if (isLocalTransport(transport)) {
601
- const result2 = await transport.command("send_chat", {
602
- message: args.message,
603
- ...args.session_id ? { targetSessionId: args.session_id } : {}
604
- });
605
- if (result2?.success === false) return `Error: ${result2.error ?? "send_chat failed"}`;
606
- return "Message sent.";
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
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
609
- const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
610
- const result = await transport.sendChat(targetId, args.message, {
611
- ...args.session_id ? { sessionId: args.session_id } : {}
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
- action: {
625
- type: "string",
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: ["action"]
488
+ required: ["node_id", "message"]
639
489
  }
640
490
  };
641
- async function approve(transport, args) {
642
- const action = args.action === "reject" ? "reject" : "approve";
643
- if (isLocalTransport(transport)) {
644
- const result2 = await transport.command("resolve_action", {
645
- action,
646
- ...args.session_id ? { targetSessionId: args.session_id } : {}
647
- });
648
- if (result2?.success === false) return `Error: ${result2.error ?? "resolve_action failed"}`;
649
- return `Action ${action}d.`;
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
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
652
- const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
653
- const result = await transport.approve(targetId, action);
654
- if (result?.success === false) return `Error: ${result.error ?? "approve failed"}`;
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
- session_id: {
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
- description: "Target session ID. Omit to use the active session."
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
- async function screenshot(transport, args) {
674
- let result;
675
- if (isLocalTransport(transport)) {
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
- workspace: {
539
+ node_id: { type: "string", description: "Node ID whose delegated sessions should be considered for cleanup." },
540
+ mode: {
701
541
  type: "string",
702
- description: "Absolute path to the workspace/repository directory."
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
- include_diff: {
705
- type: "boolean",
706
- description: "Include changed file list (default: true)."
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
- ...FORMAT_PROP
550
+ dry_run: { type: "boolean", description: "Preview matched/stopped/deleted/skipped session IDs without mutating session-host state." }
713
551
  },
714
- required: ["workspace"]
552
+ required: ["node_id", "mode"]
715
553
  }
716
554
  };
717
- async function gitStatus(transport, args) {
718
- let status;
719
- let diffSummary;
720
- if (isLocalTransport(transport)) {
721
- const statusResult = await transport.command("git_status", {
722
- workspace: args.workspace
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
- status = statusResult?.status ?? statusResult;
725
- if (args.include_diff !== false) {
726
- const diffResult = await transport.command("git_diff_summary", {
727
- workspace: args.workspace
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
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
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
- if (status?.success === false || status?.reason) {
746
- const msg = status?.error ?? status?.reason ?? "unknown";
747
- if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
748
- return `Git error: ${msg}`;
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
- if (!status?.isGitRepo) {
751
- if (args.format === "json") return JSON.stringify({ error: `Not a git repository: ${args.workspace}` }, null, 2);
752
- return `Not a git repository: ${args.workspace}`;
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
- if (args.format === "json") {
755
- const files = diffSummary?.files?.map((f) => ({
756
- path: f.path,
757
- old_path: f.oldPath ?? null,
758
- status: f.status ?? "M",
759
- insertions: f.insertions ?? 0,
760
- deletions: f.deletions ?? 0
761
- })) ?? [];
762
- return JSON.stringify({
763
- branch: status.branch ?? null,
764
- head_commit: status.headCommit ?? null,
765
- head_message: status.headMessage ?? null,
766
- ahead: status.ahead ?? 0,
767
- behind: status.behind ?? 0,
768
- staged: status.staged ?? 0,
769
- modified: status.modified ?? 0,
770
- untracked: status.untracked ?? 0,
771
- deleted: status.deleted ?? 0,
772
- stash_count: status.stashCount ?? 0,
773
- has_conflicts: status.hasConflicts ?? false,
774
- dirty: status.dirty ?? false,
775
- changed_files: files,
776
- total_insertions: diffSummary?.totalInsertions ?? 0,
777
- total_deletions: diffSummary?.totalDeletions ?? 0
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
- const lines = [];
781
- if (status.branch) lines.push(`Branch: ${status.branch}`);
782
- if (status.headCommit) {
783
- lines.push(`HEAD: ${status.headCommit.slice(0, 7)}${status.headMessage ? ` \u2014 ${status.headMessage.slice(0, 80)}` : ""}`);
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
- if (status.ahead > 0) lines.push(`Ahead: ${status.ahead}`);
786
- if (status.behind > 0) lines.push(`Behind: ${status.behind}`);
787
- if (status.staged > 0) lines.push(`Staged: ${status.staged}`);
788
- if (status.modified > 0) lines.push(`Modified: ${status.modified}`);
789
- if (status.untracked > 0) lines.push(`Untracked: ${status.untracked}`);
790
- if (status.deleted > 0) lines.push(`Deleted: ${status.deleted}`);
791
- if (status.stashCount > 0) lines.push(`Stashes: ${status.stashCount}`);
792
- if (status.hasConflicts) lines.push("Conflicts: YES");
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
- if (diffSummary.files.length > 20) lines.push(` \u2026 and ${diffSummary.files.length - 20} more`);
801
- if (diffSummary.totalInsertions || diffSummary.totalDeletions) {
802
- lines.push(`Total: +${diffSummary.totalInsertions ?? 0}/-${diffSummary.totalDeletions ?? 0}`);
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
- return lines.join("\n");
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/git-log.ts
809
- var GIT_LOG_TOOL = {
810
- name: "git_log",
811
- 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.",
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
- workspace: {
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: "Absolute path to the workspace/repository directory."
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 commits to return (default: 20, max: 100)."
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, required)."
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: ["workspace"]
1367
+ required: []
842
1368
  }
843
1369
  };
844
- async function gitLog(transport, args) {
845
- const limit = Math.max(1, Math.min(100, args.limit ?? 20));
846
- let raw;
1370
+ async function readChat(transport, args) {
1371
+ const limit = args.limit ?? 50;
847
1372
  if (isLocalTransport(transport)) {
848
- raw = await transport.command("git_log", {
849
- workspace: args.workspace,
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
- raw = raw?.log ?? raw;
856
- } else {
857
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
858
- const result = await transport.gitLog(args.daemon_id, args.workspace, {
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
- raw = result?.log ?? result;
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 (!raw?.isGitRepo) {
872
- const msg = `Not a git repository: ${args.workspace}`;
873
- if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
874
- return msg;
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 entries = raw?.entries ?? [];
877
- if (args.format === "json") {
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
- workspace: raw.workspace,
880
- branch: raw.branch ?? null,
881
- entries: entries.map((e) => ({
882
- commit: e.commit,
883
- short: e.commit?.slice(0, 7),
884
- message: e.message,
885
- author: e.authorName ?? null,
886
- author_email: e.authorEmail ?? null,
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 (entries.length === 0) return "No commits found.";
894
- const lines = entries.map((e) => {
895
- const hash = e.commit?.slice(0, 7) ?? "???????";
896
- const date = e.authoredAt ? new Date(e.authoredAt).toISOString().slice(0, 10) : "";
897
- const author = e.authorName ? ` (${e.authorName})` : "";
898
- return `${hash} ${date}${author} ${e.message}`;
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
- const header = `Commits (${entries.length}${raw.truncated ? ", truncated" : ""}):`;
901
- return `${header}
902
- ${lines.join("\n")}`;
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/git-diff.ts
906
- var GIT_DIFF_TOOL = {
907
- name: "git_diff",
908
- 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.",
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
- workspace: {
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: "Absolute path to the workspace/repository directory."
1458
+ description: "Daemon ID (cloud mode only). Omit for local mode."
915
1459
  },
916
- file: {
1460
+ agent_type: {
917
1461
  type: "string",
918
- description: "Specific repo-relative file path to diff (optional \u2014 if omitted, returns top 5 changed files)."
1462
+ description: "Optional provider/agent type hint, e.g. hermes-cli, claude-cli, codex-cli."
919
1463
  },
920
- max_lines: {
1464
+ limit: {
921
1465
  type: "number",
922
- description: "Max diff lines per file before truncating (default: 300)."
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
- daemon_id: {
1468
+ delivery: {
929
1469
  type: "string",
930
- description: "Daemon ID (cloud mode only, required)."
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: ["workspace"]
1475
+ required: ["session_id"]
935
1476
  }
936
1477
  };
937
- async function gitDiff(transport, args) {
938
- const maxLines = Math.max(10, Math.min(2e3, args.max_lines ?? 300));
939
- const staged = args.staged ?? false;
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
- return localGitDiff(transport, args.workspace, args.file, maxLines, staged, args.format);
942
- }
943
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
944
- const result = await transport.gitDiff(args.daemon_id, args.workspace, {
945
- file: args.file,
946
- maxLines,
947
- staged
948
- });
949
- if (result?.error) {
950
- if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
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 formatDiffResult(result, args.format);
1502
+ return formatChatDebugResult(result, { sessionId, delivery, format: args.format });
954
1503
  }
955
- async function localGitDiff(transport, workspace, file, maxLines, staged, format) {
956
- if (file) {
957
- const raw = await transport.command("git_diff_file", { workspace, path: file, staged });
958
- const d = raw?.diff ?? raw;
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
- const files = summary?.files ?? [];
992
- if (files.length === 0) {
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
- const topFiles = files.slice(0, 5);
997
- const fileDiffs = await Promise.all(
998
- topFiles.map(async (f) => {
999
- try {
1000
- const raw = await transport.command("git_diff_file", { workspace, path: f.path, staged });
1001
- const d = raw?.diff ?? raw;
1002
- const lines = (d?.diff ?? "").split("\n");
1003
- const trunc = lines.length > maxLines;
1004
- return {
1005
- path: f.path,
1006
- old_path: f.oldPath ?? null,
1007
- status: f.status ?? "M",
1008
- diff: trunc ? lines.slice(0, maxLines).join("\n") + "\n... (truncated)" : d?.diff ?? "",
1009
- truncated: trunc,
1010
- binary: d?.binary ?? false
1011
- };
1012
- } catch {
1013
- return { path: f.path, diff: "", truncated: false, binary: false, error: "fetch failed" };
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
- function formatDiffResult(result, format) {
1025
- if (format === "json") return JSON.stringify(result, null, 2);
1026
- const files = result?.files ?? [];
1027
- if (files.length === 0) return "No changed files.";
1028
- const parts = [];
1029
- const totalShown = result?.shown_files ?? files.length;
1030
- const totalAll = result?.total_files ?? files.length;
1031
- if (totalAll > totalShown) {
1032
- parts.push(`Showing ${totalShown} of ${totalAll} changed files:
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
- for (const f of files) {
1036
- const header = `--- ${f.path}${f.old_path ? ` (was ${f.old_path})` : ""} ---`;
1037
- if (f.error) {
1038
- parts.push(`${header}
1039
- (error: ${f.error})
1040
- `);
1041
- } else if (f.binary) {
1042
- parts.push(`${header}
1043
- (binary file)
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
- return parts.join("\n");
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/git-checkpoint.ts
1058
- var GIT_CHECKPOINT_TOOL = {
1059
- name: "git_checkpoint",
1060
- 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.",
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
- workspace: {
1581
+ action: {
1065
1582
  type: "string",
1066
- description: "Absolute path to the workspace/repository directory."
1583
+ enum: ["approve", "reject"],
1584
+ description: "Whether to approve or reject the pending action."
1067
1585
  },
1068
- message: {
1586
+ session_id: {
1069
1587
  type: "string",
1070
- description: 'Checkpoint message (max 200 chars). Will be prefixed with "adhdev: checkpoint ".'
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, required)."
1592
+ description: "Daemon ID (cloud mode only)."
1079
1593
  }
1080
1594
  },
1081
- required: ["workspace", "message"]
1595
+ required: ["action"]
1082
1596
  }
1083
1597
  };
1084
- async function gitCheckpoint(transport, args) {
1085
- const message = args.message?.trim();
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
- raw = await transport.command("git_checkpoint", {
1091
- workspace: args.workspace,
1092
- message,
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
- raw = raw?.checkpoint ?? raw;
1096
- } else {
1097
- if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
1098
- const result = await transport.gitCheckpoint(args.daemon_id, {
1099
- workspace: args.workspace,
1100
- message,
1101
- includeUntracked: args.include_untracked ?? false
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
- raw = result?.checkpoint ?? result;
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 (raw?.success === false || raw?.reason) {
1106
- const msg = raw?.error ?? raw?.reason ?? "unknown";
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 commit = raw?.commit?.slice(0, 7) ?? "???????";
1113
- const fullMsg = raw?.message ?? `adhdev: checkpoint ${message}`;
1114
- return `Checkpoint created: ${commit} \u2014 ${fullMsg}`;
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-push.ts
1118
- var GIT_PUSH_TOOL = {
1119
- name: "git_push",
1120
- 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.",
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
- remote: {
1129
- type: "string",
1130
- description: 'Remote name (default: "origin").'
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, required)."
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 gitPush(transport, args) {
1145
- let raw;
1674
+ async function gitStatus(transport, args) {
1675
+ let status;
1676
+ let diffSummary;
1146
1677
  if (isLocalTransport(transport)) {
1147
- raw = await transport.command("git_push", {
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
- raw = raw?.push ?? raw;
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.gitPush(args.daemon_id, {
1156
- workspace: args.workspace,
1157
- remote: args.remote,
1158
- branch: args.branch
1159
- });
1160
- raw = result?.push ?? result;
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 (raw?.success === false || raw?.reason) {
1163
- const msg = raw?.error ?? raw?.reason ?? "unknown";
1164
- return `Git push error: ${msg}`;
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
- const branch = raw?.branch ?? args.branch ?? "(current)";
1167
- const remote = raw?.remote ?? args.remote ?? "origin";
1168
- const newBranch = raw?.newBranch ? " [new branch]" : "";
1169
- const output = raw?.output ? `
1170
- ${raw.output}` : "";
1171
- return `Pushed ${branch} \u2192 ${remote}${newBranch}${output}`;
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/launch-session.ts
1175
- var LAUNCH_SESSION_TOOL = {
1176
- name: "launch_session",
1177
- 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).",
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: "Working directory for the session. Defaults to the daemon default workspace."
1774
+ description: "Absolute path to the workspace/repository directory."
1188
1775
  },
1189
- model: {
1776
+ limit: {
1777
+ type: "number",
1778
+ description: "Max commits to return (default: 20, max: 100)."
1779
+ },
1780
+ file: {
1190
1781
  type: "string",
1191
- description: "Model override for ACP agents (e.g. claude-opus-4-7)."
1782
+ description: "Filter history to commits that touched this repo-relative file path (optional)."
1192
1783
  },
1193
- daemon_id: {
1784
+ since: {
1194
1785
  type: "string",
1195
- description: "Daemon ID (cloud mode only). Required in cloud mode."
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: "Session ID to stop (from list_sessions)."
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
- type: {
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: ["session_id"]
1798
+ required: ["workspace"]
1243
1799
  }
1244
1800
  };
1245
- async function stopSession(transport, args) {
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
- const local = transport;
1248
- let resolvedType = args.type;
1249
- if (!resolvedType) {
1250
- const status = await local.getStatus();
1251
- const session = (status?.sessions ?? []).find((s) => s.id === args.session_id);
1252
- resolvedType = session?.providerType ?? session?.type;
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
- if (result2?.success === false) return `Error: ${result2.error ?? "stop failed"}`;
1262
- return `Session ${args.session_id} stopped.`;
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 (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
1265
- const result = await transport.stop(args.daemon_id, {
1266
- id: args.session_id,
1267
- ...args.type ? { type: args.type } : {}
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
- if (result?.success === false || result?.error) return `Error: ${result.error ?? "stop failed"}`;
1270
- return `Session ${args.session_id} stopped.`;
1857
+ const header = `Commits (${entries.length}${raw.truncated ? ", truncated" : ""}):`;
1858
+ return `${header}
1859
+ ${lines.join("\n")}`;
1271
1860
  }
1272
1861
 
1273
- // src/tools/check-pending.ts
1274
- var CHECK_PENDING_TOOL = {
1275
- name: "check_pending",
1276
- 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.",
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 to check (cloud mode). Omit to check all daemons."
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 checkPending(transport, args) {
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 checkPendingLocal(transport, args.format);
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 (pending.length === 0) return "No sessions waiting for approval.";
1313
- const lines = pending.map((s) => {
1314
- const modal = s.activeChat?.activeModal;
1315
- const parts = [`session_id: ${s.id}`];
1316
- if (s.workspace) parts.push(`workspace: ${s.workspace}`);
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
- return `Pending approvals (${pending.length}):
1323
-
1324
- ${lines.join("\n\n")}`;
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 checkPendingCloud(transport, daemonId, format) {
1327
- const pending = [];
1328
- if (daemonId) {
1329
- const daemonStatus = await transport.getDaemonStatus(daemonId);
1330
- const sessions = daemonStatus?.sessions ?? [];
1331
- for (const s of sessions) {
1332
- if (s.status === "waiting_approval") pending.push({ daemonId, session: s });
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
- if (format === "json") {
1353
- return JSON.stringify({
1354
- pending: pending.map(({ daemonId: dId, session: s }) => ({
1355
- daemon_id: dId,
1356
- session_id: s.id,
1357
- workspace: s.workspace ?? null,
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 (pending.length === 0) return "No sessions waiting for approval.";
1365
- const lines = pending.map(({ daemonId: dId, session: s }) => {
1366
- const parts = [`daemon_id: ${dId}`, `session_id: ${s.id}`];
1367
- if (s.workspace) parts.push(`workspace: ${s.workspace}`);
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
- if (isLocalTransport(ctx.transport)) {
1388
- return ctx.transport.command(command, args);
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
- throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}'`);
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
- var MESH_STATUS_TOOL = {
1393
- name: "mesh_status",
1394
- 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.",
1395
- inputSchema: {
1396
- type: "object",
1397
- properties: {}
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
- var MESH_LIST_NODES_TOOL = {
1401
- name: "mesh_list_nodes",
1402
- description: "List all nodes in the mesh with their capabilities, platform, and workspace paths.",
1403
- inputSchema: {
1404
- type: "object",
1405
- properties: {}
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
- var MESH_SEND_TASK_TOOL = {
1409
- name: "mesh_send_task",
1410
- description: "Send a natural-language task to an agent session on a mesh node. The agent will execute the task autonomously.",
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
- node_id: { type: "string", description: "Target node ID (from mesh_list_nodes)." },
1415
- session_id: { type: "string", description: "Agent session ID on the target node." },
1416
- message: { type: "string", description: "Natural-language task to send to the agent." }
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: ["node_id", "session_id", "message"]
2038
+ required: ["workspace", "message"]
1419
2039
  }
1420
2040
  };
1421
- var MESH_READ_CHAT_TOOL = {
1422
- name: "mesh_read_chat",
1423
- description: "Read recent chat messages from a delegated agent session on a mesh node. Use this to check progress.",
1424
- inputSchema: {
1425
- type: "object",
1426
- properties: {
1427
- node_id: { type: "string", description: "Target node ID." },
1428
- session_id: { type: "string", description: "Agent session ID to read from." },
1429
- tail: { type: "number", description: "Number of recent messages to return (default: 10)." }
1430
- },
1431
- required: ["node_id", "session_id"]
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
- var MESH_LAUNCH_SESSION_TOOL = {
1435
- name: "mesh_launch_session",
1436
- description: "Launch a new agent session on a mesh node. Returns the session ID for subsequent send_task/read_chat calls.",
1437
- inputSchema: {
1438
- type: "object",
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
- var MESH_GIT_STATUS_TOOL = {
1447
- name: "mesh_git_status",
1448
- description: "Get git status for a mesh node workspace \u2014 branch, dirty state, changed files.",
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
- node_id: { type: "string", description: "Target node ID." }
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: ["node_id"]
2098
+ required: ["workspace"]
1455
2099
  }
1456
2100
  };
1457
- var MESH_CHECKPOINT_TOOL = {
1458
- name: "mesh_checkpoint",
1459
- description: "Create a git checkpoint (commit) on a mesh node workspace.",
1460
- inputSchema: {
1461
- type: "object",
1462
- properties: {
1463
- node_id: { type: "string", description: "Target node ID." },
1464
- message: { type: "string", description: "Checkpoint commit message." }
1465
- },
1466
- required: ["node_id", "message"]
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
- var MESH_APPROVE_TOOL = {
1470
- name: "mesh_approve",
1471
- description: "Approve or reject a pending action on a delegated agent session.",
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
- node_id: { type: "string", description: "Target node ID." },
1476
- session_id: { type: "string", description: "Agent session ID with pending approval." },
1477
- action: { type: "string", enum: ["approve", "reject"], description: "Action to take." }
1478
- },
1479
- required: ["node_id", "session_id", "action"]
1480
- }
1481
- };
1482
- var ALL_MESH_TOOLS = [
1483
- MESH_STATUS_TOOL,
1484
- MESH_LIST_NODES_TOOL,
1485
- MESH_SEND_TASK_TOOL,
1486
- MESH_READ_CHAT_TOOL,
1487
- MESH_LAUNCH_SESSION_TOOL,
1488
- MESH_GIT_STATUS_TOOL,
1489
- MESH_CHECKPOINT_TOOL,
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
- } catch (e) {
1520
- entry.health = "degraded";
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
- async function meshReadChat(ctx, args) {
1566
- const node = findNode(ctx.mesh, args.node_id);
1567
- if (isLocalTransport(ctx.transport)) {
1568
- const result = await commandForNode(ctx, node, "read_chat", {
1569
- sessionId: args.session_id,
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
- tailLimit: args.tail ?? 10
2216
+ cliType: resolvedType
1572
2217
  });
1573
- return JSON.stringify(result, null, 2);
1574
- } else {
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
- async function meshLaunchSession(ctx, args) {
1579
- const node = findNode(ctx.mesh, args.node_id);
1580
- if (isLocalTransport(ctx.transport)) {
1581
- const result = await commandForNode(ctx, node, "launch_cli", {
1582
- cliType: args.type,
1583
- dir: node.workspace,
1584
- settings: {
1585
- meshNodeFor: ctx.mesh.id,
1586
- launchedByCoordinator: true
1587
- }
1588
- });
1589
- return JSON.stringify(result, null, 2);
1590
- } else {
1591
- return JSON.stringify({ error: "Cloud mesh launch_session not yet implemented" });
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 meshGitStatus(ctx, args) {
1595
- const node = findNode(ctx.mesh, args.node_id);
1596
- if (!isLocalTransport(ctx.transport) && node.daemonId) {
1597
- const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
1598
- return JSON.stringify({
1599
- nodeId: args.node_id,
1600
- workspace: node.workspace,
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
- nodeId: args.node_id,
1613
- workspace: node.workspace,
1614
- status: statusResult?.status ?? statusResult,
1615
- diff: diffResult?.diffSummary ?? diffResult
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 meshCheckpoint(ctx, args) {
1622
- const node = findNode(ctx.mesh, args.node_id);
1623
- if (node.policy?.readOnly) {
1624
- return JSON.stringify({ error: `Node '${args.node_id}' is read-only \u2014 cannot checkpoint` });
1625
- }
1626
- if (isLocalTransport(ctx.transport)) {
1627
- const result = await commandForNode(ctx, node, "git_checkpoint", {
1628
- workspace: node.workspace,
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
- return JSON.stringify({ error: "Cloud mesh checkpoint not yet implemented" });
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
- async function meshApprove(ctx, args) {
1637
- const node = findNode(ctx.mesh, args.node_id);
1638
- if (isLocalTransport(ctx.transport)) {
1639
- const result = await commandForNode(ctx, node, "resolve_action", {
1640
- sessionId: args.session_id,
1641
- targetSessionId: args.session_id,
1642
- action: args.action === "reject" ? "reject" : "approve"
1643
- });
1644
- return JSON.stringify(result, null, 2);
1645
- } else {
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
- const meshCtx = { mesh, transport };
1757
- let coordinatorPrompt = "";
1758
- try {
1759
- const { buildCoordinatorSystemPrompt } = await import("@adhdev/daemon-core");
1760
- coordinatorPrompt = buildCoordinatorSystemPrompt({ mesh });
1761
- } catch {
1762
- coordinatorPrompt = `You are a Repo Mesh Coordinator for "${mesh.name}" (${mesh.repoIdentity}). Use mesh_* tools to orchestrate work.`;
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.75" },
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}