adhdev 0.9.76-rc.7 → 0.9.76-rc.71

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