@mekareteriker/opencode-mcp 1.10.2-mekareteriker.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +204 -0
- package/LICENSE +22 -0
- package/README.md +174 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +282 -0
- package/dist/client.js.map +1 -0
- package/dist/helpers.d.ts +150 -0
- package/dist/helpers.js +575 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +210 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +10 -0
- package/dist/resources.js +197 -0
- package/dist/resources.js.map +1 -0
- package/dist/server-manager.d.ts +72 -0
- package/dist/server-manager.js +264 -0
- package/dist/server-manager.js.map +1 -0
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/config.js +105 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/events.d.ts +6 -0
- package/dist/tools/events.js +63 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/file.d.ts +3 -0
- package/dist/tools/file.js +153 -0
- package/dist/tools/file.js.map +1 -0
- package/dist/tools/global.d.ts +3 -0
- package/dist/tools/global.js +17 -0
- package/dist/tools/global.js.map +1 -0
- package/dist/tools/message.d.ts +3 -0
- package/dist/tools/message.js +169 -0
- package/dist/tools/message.js.map +1 -0
- package/dist/tools/misc.d.ts +3 -0
- package/dist/tools/misc.js +298 -0
- package/dist/tools/misc.js.map +1 -0
- package/dist/tools/project.d.ts +3 -0
- package/dist/tools/project.js +62 -0
- package/dist/tools/project.js.map +1 -0
- package/dist/tools/provider.d.ts +3 -0
- package/dist/tools/provider.js +175 -0
- package/dist/tools/provider.js.map +1 -0
- package/dist/tools/session.d.ts +3 -0
- package/dist/tools/session.js +392 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/tui.d.ts +7 -0
- package/dist/tools/tui.js +121 -0
- package/dist/tools/tui.js.map +1 -0
- package/dist/tools/workflow.d.ts +7 -0
- package/dist/tools/workflow.js +775 -0
- package/dist/tools/workflow.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level workflow tools — composite operations that make it easy
|
|
3
|
+
* for an LLM to accomplish common tasks in a single call.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatMessageResponse, formatMessageList, analyzeMessageResponse, isProviderConfigured, redactSecrets, resolveSessionStatus, applyModelDefaults, normalizeDirectory, toolResult, toolError, directoryParam, readOnly, } from "../helpers.js";
|
|
7
|
+
export function registerWorkflowTools(server, client) {
|
|
8
|
+
// ─── Setup / onboarding ───────────────────────────────────────────
|
|
9
|
+
server.tool("opencode_setup", "Check OpenCode status, provider configuration, and optionally initialize a project directory. Use this as the first step when starting work — it tells you what is ready and what still needs configuration.", {
|
|
10
|
+
directory: directoryParam,
|
|
11
|
+
}, readOnly, async ({ directory }) => {
|
|
12
|
+
try {
|
|
13
|
+
const sections = [];
|
|
14
|
+
// 1. Health check
|
|
15
|
+
let healthy = false;
|
|
16
|
+
try {
|
|
17
|
+
const health = (await client.get("/global/health", undefined, directory));
|
|
18
|
+
healthy = true;
|
|
19
|
+
sections.push(`## Server\nStatus: healthy\nVersion: ${health.version ?? "unknown"}`);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
sections.push(`## Server\nStatus: UNREACHABLE — is \`opencode serve\` running?\nError: ${e instanceof Error ? e.message : String(e)}`);
|
|
23
|
+
}
|
|
24
|
+
if (!healthy) {
|
|
25
|
+
return toolResult(sections.join("\n\n"));
|
|
26
|
+
}
|
|
27
|
+
// 2. Providers — categorize by readiness
|
|
28
|
+
try {
|
|
29
|
+
const raw = await client.get("/provider", undefined, directory);
|
|
30
|
+
const providers = (raw && typeof raw === "object" && "all" in raw
|
|
31
|
+
? raw.all
|
|
32
|
+
: raw);
|
|
33
|
+
if (Array.isArray(providers) && providers.length > 0) {
|
|
34
|
+
// Fetch auth methods for richer guidance
|
|
35
|
+
let authMethods = null;
|
|
36
|
+
try {
|
|
37
|
+
authMethods = (await client.get("/provider/auth", undefined, directory));
|
|
38
|
+
}
|
|
39
|
+
catch { /* non-critical */ }
|
|
40
|
+
// Helper to count models
|
|
41
|
+
const countModels = (p) => {
|
|
42
|
+
const m = p.models;
|
|
43
|
+
return Array.isArray(m) ? m.length : m && typeof m === "object" ? Object.keys(m).length : 0;
|
|
44
|
+
};
|
|
45
|
+
const ready = providers.filter(isProviderConfigured);
|
|
46
|
+
const withOAuth = providers.filter((p) => !ready.includes(p) && authMethods && Array.isArray(authMethods[p.id]));
|
|
47
|
+
// Popular providers worth highlighting (have free tiers or are well-known)
|
|
48
|
+
const popularIds = new Set([
|
|
49
|
+
"anthropic", "openai", "google", "firmware", "openrouter",
|
|
50
|
+
"groq", "deepseek", "huggingface", "github-copilot", "mistral",
|
|
51
|
+
]);
|
|
52
|
+
const popular = providers.filter((p) => !ready.includes(p) && popularIds.has(p.id));
|
|
53
|
+
const otherCount = providers.length - ready.length - popular.length;
|
|
54
|
+
const providerLines = [];
|
|
55
|
+
// Section: Ready to use
|
|
56
|
+
if (ready.length > 0) {
|
|
57
|
+
providerLines.push("**Ready to use:**");
|
|
58
|
+
for (const p of ready) {
|
|
59
|
+
const id = p.id;
|
|
60
|
+
const name = p.name;
|
|
61
|
+
const mc = countModels(p);
|
|
62
|
+
const envVars = p.env?.join(", ") ?? "";
|
|
63
|
+
providerLines.push(`- ${id} (${name}): detected via ${envVars} — ${mc} models`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
providerLines.push("**No providers configured yet.** You need at least one to start using OpenCode.");
|
|
68
|
+
}
|
|
69
|
+
// Section: Quick setup options
|
|
70
|
+
providerLines.push("");
|
|
71
|
+
providerLines.push("**Quick setup options:**");
|
|
72
|
+
for (const p of popular) {
|
|
73
|
+
const id = p.id;
|
|
74
|
+
const name = p.name;
|
|
75
|
+
const mc = countModels(p);
|
|
76
|
+
const envVars = p.env ?? [];
|
|
77
|
+
const envHint = envVars.length > 0 ? `set ${envVars[0]}` : "";
|
|
78
|
+
// Check for OAuth
|
|
79
|
+
const oauthAvailable = authMethods && Array.isArray(authMethods[id]);
|
|
80
|
+
const methods = [];
|
|
81
|
+
if (oauthAvailable) {
|
|
82
|
+
const labels = authMethods[id]
|
|
83
|
+
.filter((m) => m.type === "oauth")
|
|
84
|
+
.map((m) => m.label);
|
|
85
|
+
if (labels.length > 0)
|
|
86
|
+
methods.push(`OAuth (${labels[0]})`);
|
|
87
|
+
}
|
|
88
|
+
if (envHint)
|
|
89
|
+
methods.push(`\`opencode_auth_set\` or env var \`${envVars[0]}\``);
|
|
90
|
+
// Check for free models
|
|
91
|
+
const rawModels = p.models;
|
|
92
|
+
const modelValues = rawModels && typeof rawModels === "object" && !Array.isArray(rawModels)
|
|
93
|
+
? Object.values(rawModels)
|
|
94
|
+
: Array.isArray(rawModels) ? rawModels : [];
|
|
95
|
+
const hasFree = modelValues.some((m) => {
|
|
96
|
+
const cost = m.cost;
|
|
97
|
+
return cost && cost.input === 0 && cost.output === 0;
|
|
98
|
+
});
|
|
99
|
+
const freeTag = hasFree ? " [has free models]" : "";
|
|
100
|
+
providerLines.push(`- ${id} (${name}): ${mc} models${freeTag} — ${methods.join(" or ")}`);
|
|
101
|
+
}
|
|
102
|
+
// Mention OAuth-only providers not already listed
|
|
103
|
+
const oauthOnly = withOAuth.filter((p) => !popular.includes(p) && !ready.includes(p));
|
|
104
|
+
if (oauthOnly.length > 0) {
|
|
105
|
+
const names = oauthOnly.map((p) => `${p.id}`).join(", ");
|
|
106
|
+
providerLines.push(`- Also available via OAuth: ${names}`);
|
|
107
|
+
}
|
|
108
|
+
if (otherCount > 0) {
|
|
109
|
+
providerLines.push(`\n+${otherCount} more providers available. Use \`opencode_provider_list\` to see all.`);
|
|
110
|
+
}
|
|
111
|
+
sections.push(`## Providers (${providers.length} available)\n${providerLines.join("\n")}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
sections.push("## Providers\nNo providers found. Is the server running correctly?");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
sections.push("## Providers\nCould not fetch provider list.");
|
|
119
|
+
}
|
|
120
|
+
// 3. Project info (if directory given or from default)
|
|
121
|
+
try {
|
|
122
|
+
const project = (await client.get("/project/current", undefined, directory));
|
|
123
|
+
const worktree = (project.worktree ?? "unknown");
|
|
124
|
+
// Derive a readable name: prefer project.name, then last dir component from worktree, then id
|
|
125
|
+
const name = project.name
|
|
126
|
+
?? (worktree !== "unknown" ? worktree.split("/").filter(Boolean).pop() : null)
|
|
127
|
+
?? project.id
|
|
128
|
+
?? "unknown";
|
|
129
|
+
const vcs = project.vcs ?? "none";
|
|
130
|
+
sections.push(`## Project\nName: ${name}\nPath: ${worktree}\nVCS: ${vcs}`);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
if (directory) {
|
|
134
|
+
sections.push(`## Project\nDirectory: ${directory}\nNote: Could not load project info. Make sure the directory exists and contains a git repository.`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
sections.push("## Project\nNo project context available (no directory specified and server has no default project).");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 4. Context-dependent next steps
|
|
141
|
+
const tips = [];
|
|
142
|
+
const hasReady = sections.some((s) => s.includes("**Ready to use:**"));
|
|
143
|
+
const hasProject = sections.some((s) => s.startsWith("## Project\nName:"));
|
|
144
|
+
if (!hasReady) {
|
|
145
|
+
// No providers configured — guide them to set one up
|
|
146
|
+
tips.push("**You need to configure a provider first.** Options:");
|
|
147
|
+
tips.push("1. Set an API key: `opencode_auth_set` with providerId (e.g. 'anthropic', 'openai', 'google')");
|
|
148
|
+
tips.push("2. Set an env var (e.g. `OPENROUTER_API_KEY`, `HF_TOKEN`, `ANTHROPIC_API_KEY`) and restart opencode");
|
|
149
|
+
tips.push("3. Try **firmware** — it has 23 free models, no API key needed. Use `opencode_ask` with `providerID: 'firmware'`");
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Providers ready — guide to first task
|
|
153
|
+
tips.push("**You're ready to go!** Try:");
|
|
154
|
+
tips.push("- `opencode_ask` — ask a question or give an instruction (easiest way to start)");
|
|
155
|
+
if (!hasProject) {
|
|
156
|
+
tips.push("- Pass a `directory` parameter to target a specific project");
|
|
157
|
+
}
|
|
158
|
+
tips.push("- `opencode_context` — get full project context (config, VCS, agents)");
|
|
159
|
+
tips.push("- `opencode_provider_models` — explore available models for your configured providers");
|
|
160
|
+
}
|
|
161
|
+
sections.push(`## Next Steps\n${tips.join("\n")}`);
|
|
162
|
+
return toolResult(sections.join("\n\n"));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return toolError(e);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// ─── One-shot: create session + send prompt + return answer ─────────
|
|
169
|
+
server.tool("opencode_ask", "Ask OpenCode a question in one step. Creates a new session, sends your prompt, and returns the AI response. This is the easiest way to interact with OpenCode.", {
|
|
170
|
+
prompt: z.string().describe("The question or instruction to send"),
|
|
171
|
+
title: z
|
|
172
|
+
.string()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Optional title for the session"),
|
|
175
|
+
providerID: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("Provider ID (e.g. 'anthropic')"),
|
|
179
|
+
modelID: z
|
|
180
|
+
.string()
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("Model ID (e.g. 'claude-3-5-sonnet-20241022')"),
|
|
183
|
+
variant: z.string().optional().describe("Model variant (e.g. 'fast', 'smart')"),
|
|
184
|
+
agent: z
|
|
185
|
+
.string()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("Agent to use (e.g. 'build', 'plan')"),
|
|
188
|
+
system: z
|
|
189
|
+
.string()
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("Optional system prompt override"),
|
|
192
|
+
directory: directoryParam,
|
|
193
|
+
}, async ({ prompt, title, providerID, modelID, variant, agent, system, directory }) => {
|
|
194
|
+
try {
|
|
195
|
+
// 1. Create session
|
|
196
|
+
const session = (await client.post("/session", {
|
|
197
|
+
title: title ?? prompt.slice(0, 80),
|
|
198
|
+
}, { directory }));
|
|
199
|
+
const sessionId = session.id;
|
|
200
|
+
// 2. Send prompt
|
|
201
|
+
const body = {
|
|
202
|
+
parts: [{ type: "text", text: prompt }],
|
|
203
|
+
};
|
|
204
|
+
const model = applyModelDefaults(providerID, modelID, variant);
|
|
205
|
+
if (model)
|
|
206
|
+
body.model = model;
|
|
207
|
+
if (agent)
|
|
208
|
+
body.agent = agent;
|
|
209
|
+
if (system)
|
|
210
|
+
body.system = system;
|
|
211
|
+
const response = await client.post(`/session/${sessionId}/message`, body, { directory });
|
|
212
|
+
// 3. Analyze for auth / empty response issues
|
|
213
|
+
const analysis = analyzeMessageResponse(response);
|
|
214
|
+
// 4. Format and return
|
|
215
|
+
const formatted = formatMessageResponse(response);
|
|
216
|
+
const dirLabel = directory ? `Directory: ${directory}` : "Directory: (server default)";
|
|
217
|
+
const parts = [`${dirLabel}\nSession: ${sessionId}`];
|
|
218
|
+
if (formatted)
|
|
219
|
+
parts.push(formatted);
|
|
220
|
+
if (analysis.warning) {
|
|
221
|
+
parts.push(`\n--- WARNING ---\n${analysis.warning}`);
|
|
222
|
+
}
|
|
223
|
+
return toolResult(parts.join("\n\n"), analysis.hasError);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
return toolError(e);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// ─── Continue a conversation ────────────────────────────────────────
|
|
230
|
+
server.tool("opencode_reply", "Send a follow-up message to an existing session. Use this to continue a conversation started with opencode_ask or opencode_session_create.", {
|
|
231
|
+
sessionId: z.string().describe("Session ID to reply in"),
|
|
232
|
+
prompt: z.string().describe("The follow-up message"),
|
|
233
|
+
providerID: z.string().optional().describe("Provider ID"),
|
|
234
|
+
modelID: z.string().optional().describe("Model ID"),
|
|
235
|
+
variant: z.string().optional().describe("Model variant"),
|
|
236
|
+
agent: z.string().optional().describe("Agent to use"),
|
|
237
|
+
directory: directoryParam,
|
|
238
|
+
}, async ({ sessionId, prompt, providerID, modelID, variant, agent, directory }) => {
|
|
239
|
+
try {
|
|
240
|
+
const body = {
|
|
241
|
+
parts: [{ type: "text", text: prompt }],
|
|
242
|
+
};
|
|
243
|
+
const model = applyModelDefaults(providerID, modelID, variant);
|
|
244
|
+
if (model)
|
|
245
|
+
body.model = model;
|
|
246
|
+
if (agent)
|
|
247
|
+
body.agent = agent;
|
|
248
|
+
const response = await client.post(`/session/${sessionId}/message`, body, { directory });
|
|
249
|
+
const analysis = analyzeMessageResponse(response);
|
|
250
|
+
const formatted = formatMessageResponse(response);
|
|
251
|
+
const parts = [];
|
|
252
|
+
if (formatted)
|
|
253
|
+
parts.push(formatted);
|
|
254
|
+
if (analysis.warning) {
|
|
255
|
+
parts.push(`\n--- WARNING ---\n${analysis.warning}`);
|
|
256
|
+
}
|
|
257
|
+
// Session-directory consistency note
|
|
258
|
+
if (sessionId && directory) {
|
|
259
|
+
parts.push(`\n_Note: Using session ${sessionId} in directory ${directory}. Ensure this session belongs to this project._`);
|
|
260
|
+
}
|
|
261
|
+
return toolResult(parts.join("\n\n") || "Empty response.", analysis.hasError);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
return toolError(e);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// ─── Get conversation history (formatted) ──────────────────────────
|
|
268
|
+
server.tool("opencode_conversation", "Get the full conversation history of a session, formatted for easy reading. Shows all messages with their roles and content.", {
|
|
269
|
+
sessionId: z.string().describe("Session ID"),
|
|
270
|
+
limit: z
|
|
271
|
+
.number()
|
|
272
|
+
.optional()
|
|
273
|
+
.describe("Max messages to return (default: all)"),
|
|
274
|
+
directory: directoryParam,
|
|
275
|
+
}, readOnly, async ({ sessionId, limit, directory }) => {
|
|
276
|
+
try {
|
|
277
|
+
const query = {};
|
|
278
|
+
if (limit !== undefined)
|
|
279
|
+
query.limit = String(limit);
|
|
280
|
+
const messages = await client.get(`/session/${sessionId}/message`, query, directory);
|
|
281
|
+
const formatted = formatMessageList(messages);
|
|
282
|
+
return toolResult(formatted);
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
return toolError(e);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// ─── Quick session overview ────────────────────────────────────────
|
|
289
|
+
server.tool("opencode_sessions_overview", "Get a quick overview of all sessions with their titles and status. Useful to find which session to continue working in.", {
|
|
290
|
+
directory: directoryParam,
|
|
291
|
+
}, readOnly, async ({ directory }) => {
|
|
292
|
+
try {
|
|
293
|
+
const [sessions, statuses] = await Promise.all([
|
|
294
|
+
client.get("/session", undefined, directory),
|
|
295
|
+
client.get("/session/status", undefined, directory),
|
|
296
|
+
]);
|
|
297
|
+
if (!sessions || sessions.length === 0) {
|
|
298
|
+
return toolResult("No sessions found.");
|
|
299
|
+
}
|
|
300
|
+
// Merge status and show enriched overview
|
|
301
|
+
const lines = sessions.map((s) => {
|
|
302
|
+
const id = s.id ?? "?";
|
|
303
|
+
const title = s.title ?? "(untitled)";
|
|
304
|
+
const status = resolveSessionStatus(statuses[id]);
|
|
305
|
+
const parentTag = s.parentID ? ` (child of ${s.parentID})` : "";
|
|
306
|
+
return `- [${status}] ${title} [${id}]${parentTag}`;
|
|
307
|
+
});
|
|
308
|
+
return toolResult(`## Sessions (${sessions.length})\n${lines.join("\n")}`);
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return toolError(e);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// ─── Project context ──────────────────────────────────────────────
|
|
315
|
+
server.tool("opencode_context", "Get full project context in one call: current project, path, VCS info, config, and available agents. Useful to understand the current state before starting work.", {
|
|
316
|
+
directory: directoryParam,
|
|
317
|
+
}, readOnly, async ({ directory }) => {
|
|
318
|
+
try {
|
|
319
|
+
// Validate directory early — before Promise.all with .catch(() => null)
|
|
320
|
+
// swallows the validation error.
|
|
321
|
+
directory = normalizeDirectory(directory);
|
|
322
|
+
const [project, path, vcs, config, agents] = await Promise.all([
|
|
323
|
+
client.get("/project/current", undefined, directory).catch(() => null),
|
|
324
|
+
client.get("/path", undefined, directory).catch(() => null),
|
|
325
|
+
client.get("/vcs", undefined, directory).catch(() => null),
|
|
326
|
+
client.get("/config", undefined, directory).catch(() => null),
|
|
327
|
+
client.get("/agent", undefined, directory).catch(() => null),
|
|
328
|
+
]);
|
|
329
|
+
const sections = [];
|
|
330
|
+
if (project) {
|
|
331
|
+
const p = project;
|
|
332
|
+
const worktree = (p.worktree ?? "unknown");
|
|
333
|
+
const name = p.name
|
|
334
|
+
?? (worktree !== "unknown" ? worktree.split("/").filter(Boolean).pop() : null)
|
|
335
|
+
?? p.id ?? "unknown";
|
|
336
|
+
const lines = [`Name: ${name}`, `Path: ${worktree}`];
|
|
337
|
+
if (p.vcs)
|
|
338
|
+
lines.push(`VCS: ${p.vcs}`);
|
|
339
|
+
if (p.id)
|
|
340
|
+
lines.push(`ID: ${p.id}`);
|
|
341
|
+
sections.push(`## Project\n${lines.join("\n")}`);
|
|
342
|
+
}
|
|
343
|
+
if (path) {
|
|
344
|
+
const pp = path;
|
|
345
|
+
const workDir = pp.worktree ?? pp.directory ?? pp.cwd ?? pp.path;
|
|
346
|
+
const pathLines = [];
|
|
347
|
+
if (workDir)
|
|
348
|
+
pathLines.push(`Working directory: ${workDir}`);
|
|
349
|
+
if (pp.config && pp.config !== workDir)
|
|
350
|
+
pathLines.push(`Config: ${pp.config}`);
|
|
351
|
+
if (pp.state && pp.state !== workDir)
|
|
352
|
+
pathLines.push(`State: ${pp.state}`);
|
|
353
|
+
if (pp.home && pp.home !== workDir)
|
|
354
|
+
pathLines.push(`Home: ${pp.home}`);
|
|
355
|
+
if (pathLines.length === 0)
|
|
356
|
+
pathLines.push(`Working directory: ${JSON.stringify(pp)}`);
|
|
357
|
+
sections.push(`## Path\n${pathLines.join("\n")}`);
|
|
358
|
+
}
|
|
359
|
+
if (vcs) {
|
|
360
|
+
const v = vcs;
|
|
361
|
+
const lines = [];
|
|
362
|
+
if (v.branch)
|
|
363
|
+
lines.push(`Branch: ${v.branch}`);
|
|
364
|
+
if (v.remote)
|
|
365
|
+
lines.push(`Remote: ${v.remote}`);
|
|
366
|
+
if (v.sha)
|
|
367
|
+
lines.push(`HEAD: ${v.sha}`);
|
|
368
|
+
if (v.dirty !== undefined)
|
|
369
|
+
lines.push(`Dirty: ${v.dirty}`);
|
|
370
|
+
sections.push(`## VCS (Git)\n${lines.length > 0 ? lines.join("\n") : "No VCS info available."}`);
|
|
371
|
+
}
|
|
372
|
+
if (config) {
|
|
373
|
+
// Show config summary with secrets redacted — skip overwhelming nested objects
|
|
374
|
+
const c = redactSecrets(config);
|
|
375
|
+
const topLevel = [];
|
|
376
|
+
for (const [k, v] of Object.entries(c)) {
|
|
377
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
378
|
+
const keys = Object.keys(v);
|
|
379
|
+
topLevel.push(`${k}: {${keys.length} entries}`);
|
|
380
|
+
}
|
|
381
|
+
else if (Array.isArray(v)) {
|
|
382
|
+
topLevel.push(`${k}: [${v.length} items]`);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
topLevel.push(`${k}: ${v}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
sections.push(`## Config\n${topLevel.join("\n")}`);
|
|
389
|
+
}
|
|
390
|
+
if (agents) {
|
|
391
|
+
const agentList = agents;
|
|
392
|
+
sections.push(`## Agents (${agentList.length})\n${agentList.map((a) => `- ${a.name ?? a.id}: ${a.description ?? "(no description)"} [${a.mode ?? "?"}]`).join("\n")}`);
|
|
393
|
+
}
|
|
394
|
+
return toolResult(sections.join("\n\n"));
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
return toolError(e);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// ─── Wait for async session to complete ───────────────────────────
|
|
401
|
+
server.tool("opencode_wait", "Poll a session until it finishes processing. Use after opencode_message_send_async to wait for the AI to complete its response. Sends progress notifications while waiting. If timeout is reached, returns a progress report (not an error). For long tasks, consider using opencode_session_todo to check progress instead of blocking.", {
|
|
402
|
+
sessionId: z.string().describe("Session ID to wait on"),
|
|
403
|
+
timeoutSeconds: z
|
|
404
|
+
.number()
|
|
405
|
+
.optional()
|
|
406
|
+
.describe("Max seconds to wait (default: 120). Set higher (300-600) for complex tasks."),
|
|
407
|
+
pollIntervalMs: z
|
|
408
|
+
.number()
|
|
409
|
+
.optional()
|
|
410
|
+
.describe("Polling interval in ms (default: 2000)"),
|
|
411
|
+
directory: directoryParam,
|
|
412
|
+
}, async ({ sessionId, timeoutSeconds, pollIntervalMs, directory }) => {
|
|
413
|
+
try {
|
|
414
|
+
const timeout = (timeoutSeconds ?? 120) * 1000;
|
|
415
|
+
const interval = pollIntervalMs ?? 2000;
|
|
416
|
+
const start = Date.now();
|
|
417
|
+
while (Date.now() - start < timeout) {
|
|
418
|
+
const statuses = (await client.get("/session/status", undefined, directory));
|
|
419
|
+
const status = resolveSessionStatus(statuses[sessionId]);
|
|
420
|
+
if (status === "idle" || status === "completed") {
|
|
421
|
+
// Fetch latest messages
|
|
422
|
+
const messages = await client.get(`/session/${sessionId}/message`, { limit: "1" }, directory);
|
|
423
|
+
const arr = messages;
|
|
424
|
+
if (arr.length > 0) {
|
|
425
|
+
return toolResult(`Session completed.\n\n${formatMessageResponse(arr[arr.length - 1])}`);
|
|
426
|
+
}
|
|
427
|
+
return toolResult("Session completed (no messages).");
|
|
428
|
+
}
|
|
429
|
+
if (status === "error") {
|
|
430
|
+
return toolResult(`Session ended with error status.`, true);
|
|
431
|
+
}
|
|
432
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
433
|
+
}
|
|
434
|
+
return toolResult(`Timeout: session still processing after ${timeoutSeconds ?? 120}s. ` +
|
|
435
|
+
`Use \`opencode_conversation\` to check progress, or \`opencode_session_abort\` to stop it.`, true);
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
return toolError(e);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// ─── Review changes ────────────────────────────────────────────────
|
|
442
|
+
server.tool("opencode_review_changes", "Get a formatted summary of all file changes made in a session. Shows diffs in a readable format.", {
|
|
443
|
+
sessionId: z.string().describe("Session ID"),
|
|
444
|
+
messageID: z
|
|
445
|
+
.string()
|
|
446
|
+
.optional()
|
|
447
|
+
.describe("Specific message ID to get diff for"),
|
|
448
|
+
directory: directoryParam,
|
|
449
|
+
}, readOnly, async ({ sessionId, messageID, directory }) => {
|
|
450
|
+
try {
|
|
451
|
+
const query = {};
|
|
452
|
+
if (messageID)
|
|
453
|
+
query.messageID = messageID;
|
|
454
|
+
const diffs = await client.get(`/session/${sessionId}/diff`, query, directory);
|
|
455
|
+
const { formatDiffResponse } = await import("../helpers.js");
|
|
456
|
+
return toolResult(formatDiffResponse(diffs));
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
return toolError(e);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// ─── Provider test ────────────────────────────────────────────────
|
|
463
|
+
server.tool("opencode_provider_test", "Quick-test whether a provider is working. Creates a temporary session, sends a trivial prompt, checks the response, and cleans up. Great for debugging auth issues.", {
|
|
464
|
+
providerId: z.string().describe("Provider ID to test (e.g. 'anthropic', 'openrouter')"),
|
|
465
|
+
modelID: z.string().optional().describe("Specific model ID to test. If omitted, uses provider default."),
|
|
466
|
+
variant: z.string().optional().describe("Model variant"),
|
|
467
|
+
directory: directoryParam,
|
|
468
|
+
}, async ({ providerId, modelID, variant, directory }) => {
|
|
469
|
+
let sessionId = null;
|
|
470
|
+
try {
|
|
471
|
+
// 1. Create a temporary test session
|
|
472
|
+
const session = (await client.post("/session", {
|
|
473
|
+
title: `[test] ${providerId}`,
|
|
474
|
+
}, { directory }));
|
|
475
|
+
sessionId = session.id;
|
|
476
|
+
// 2. Send a trivial prompt
|
|
477
|
+
const body = {
|
|
478
|
+
parts: [{ type: "text", text: "Say hello in one word." }],
|
|
479
|
+
};
|
|
480
|
+
// Use the specified model, or let the provider pick its default
|
|
481
|
+
if (modelID) {
|
|
482
|
+
body.model = { providerID: providerId, modelID, ...(variant ? { variant } : {}) };
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
body.providerID = providerId;
|
|
486
|
+
}
|
|
487
|
+
const response = await client.post(`/session/${sessionId}/message`, body, { directory });
|
|
488
|
+
// 3. Analyze the response
|
|
489
|
+
const analysis = analyzeMessageResponse(response);
|
|
490
|
+
const formatted = formatMessageResponse(response);
|
|
491
|
+
// 4. Cleanup — delete test session
|
|
492
|
+
try {
|
|
493
|
+
await client.delete(`/session/${sessionId}`, undefined, directory);
|
|
494
|
+
}
|
|
495
|
+
catch { /* best-effort cleanup */ }
|
|
496
|
+
if (analysis.hasError || analysis.isEmpty) {
|
|
497
|
+
const reason = analysis.warning ?? "Unknown error — no response received.";
|
|
498
|
+
return toolResult(`Provider "${providerId}" FAILED.\n\n${reason}`, true);
|
|
499
|
+
}
|
|
500
|
+
const preview = formatted.length > 200 ? formatted.slice(0, 200) + "..." : formatted;
|
|
501
|
+
return toolResult(`Provider "${providerId}" is working.\n\nResponse: ${preview}`);
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
// Cleanup on error
|
|
505
|
+
if (sessionId) {
|
|
506
|
+
try {
|
|
507
|
+
await client.delete(`/session/${sessionId}`, undefined, directory);
|
|
508
|
+
}
|
|
509
|
+
catch { /* best-effort cleanup */ }
|
|
510
|
+
}
|
|
511
|
+
return toolError(e);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
// ─── Run: create session + async send + poll until done ──────────
|
|
515
|
+
server.tool("opencode_run", "Send a task to OpenCode and wait for completion. Combines session creation, async prompt, and polling into a single tool call. Use this instead of the manual opencode_message_send_async + opencode_wait pattern.", {
|
|
516
|
+
prompt: z.string().describe("The task or instruction to send"),
|
|
517
|
+
sessionId: z
|
|
518
|
+
.string()
|
|
519
|
+
.optional()
|
|
520
|
+
.describe("Existing session ID to continue (omit to create a new session)"),
|
|
521
|
+
title: z.string().optional().describe("Session title (only for new sessions)"),
|
|
522
|
+
providerID: z.string().optional().describe("Provider ID (e.g. 'anthropic')"),
|
|
523
|
+
modelID: z.string().optional().describe("Model ID (e.g. 'claude-opus-4-6')"),
|
|
524
|
+
variant: z.string().optional().describe("Model variant"),
|
|
525
|
+
agent: z.string().optional().describe("Agent to use"),
|
|
526
|
+
maxDurationSeconds: z
|
|
527
|
+
.number()
|
|
528
|
+
.optional()
|
|
529
|
+
.describe("Max seconds to wait for completion (default: 600 = 10 minutes)"),
|
|
530
|
+
directory: directoryParam,
|
|
531
|
+
}, async ({ prompt, sessionId, title, providerID, modelID, variant, agent, maxDurationSeconds, directory }) => {
|
|
532
|
+
try {
|
|
533
|
+
// 1. Create or reuse session
|
|
534
|
+
let sid = sessionId;
|
|
535
|
+
if (!sid) {
|
|
536
|
+
const session = (await client.post("/session", {
|
|
537
|
+
title: title ?? prompt.slice(0, 80),
|
|
538
|
+
}, { directory }));
|
|
539
|
+
sid = session.id;
|
|
540
|
+
}
|
|
541
|
+
// 2. Send async (fire-and-forget)
|
|
542
|
+
const body = {
|
|
543
|
+
parts: [{ type: "text", text: prompt }],
|
|
544
|
+
noReply: false,
|
|
545
|
+
};
|
|
546
|
+
const model = applyModelDefaults(providerID, modelID, variant);
|
|
547
|
+
if (model)
|
|
548
|
+
body.model = model;
|
|
549
|
+
if (agent)
|
|
550
|
+
body.agent = agent;
|
|
551
|
+
await client.post(`/session/${sid}/message`, body, { directory });
|
|
552
|
+
// Session-directory consistency note
|
|
553
|
+
const dirNote = sessionId && directory
|
|
554
|
+
? `\n_Note: Using session ${sid} in directory ${directory}. Ensure this session belongs to this project._`
|
|
555
|
+
: "";
|
|
556
|
+
// 3. Poll until done
|
|
557
|
+
const timeout = (maxDurationSeconds ?? 600) * 1000;
|
|
558
|
+
const interval = 3000;
|
|
559
|
+
const start = Date.now();
|
|
560
|
+
const dirLabel = directory ? `Directory: ${directory}\n` : "";
|
|
561
|
+
while (Date.now() - start < timeout) {
|
|
562
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
563
|
+
const statuses = (await client.get("/session/status", undefined, directory));
|
|
564
|
+
const status = resolveSessionStatus(statuses[sid]);
|
|
565
|
+
if (status === "idle" || status === "completed") {
|
|
566
|
+
// Get final response
|
|
567
|
+
const messages = await client.get(`/session/${sid}/message`, { limit: "1" }, directory);
|
|
568
|
+
const arr = messages;
|
|
569
|
+
const lastMsg = arr.length > 0 ? formatMessageResponse(arr[arr.length - 1]) : "";
|
|
570
|
+
// Get todo summary
|
|
571
|
+
let todoSummary = "";
|
|
572
|
+
try {
|
|
573
|
+
const todos = await client.get(`/session/${sid}/todo`, undefined, directory);
|
|
574
|
+
if (Array.isArray(todos) && todos.length > 0) {
|
|
575
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
576
|
+
todoSummary = `\nTasks: ${completed}/${todos.length} completed`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch { /* non-critical */ }
|
|
580
|
+
return toolResult(`${dirLabel}Session: ${sid}\nStatus: completed${todoSummary}${dirNote}\n\n${lastMsg || "(no response text)"}`);
|
|
581
|
+
}
|
|
582
|
+
if (status === "error") {
|
|
583
|
+
return toolResult(`${dirLabel}Session: ${sid}\nStatus: error${dirNote}\n\nThe session ended with an error. Use \`opencode_conversation({sessionId: "${sid}"})\` to see what happened.`, true);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Timeout — return progress report
|
|
587
|
+
let todoProgress = "";
|
|
588
|
+
try {
|
|
589
|
+
const todos = await client.get(`/session/${sid}/todo`, undefined, directory);
|
|
590
|
+
if (Array.isArray(todos) && todos.length > 0) {
|
|
591
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
592
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
593
|
+
todoProgress = `\nTasks: ${completed}/${todos.length} completed, ${inProgress} in progress`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch { /* non-critical */ }
|
|
597
|
+
return toolResult(`${dirLabel}Session: ${sid}\nStatus: still running after ${maxDurationSeconds ?? 600}s${todoProgress}${dirNote}\n\n` +
|
|
598
|
+
`The session is still working. Options:\n` +
|
|
599
|
+
`- \`opencode_check({sessionId: "${sid}"})\` — quick progress check\n` +
|
|
600
|
+
`- \`opencode_wait({sessionId: "${sid}", timeoutSeconds: 300})\` — wait longer\n` +
|
|
601
|
+
`- \`opencode_session_abort({id: "${sid}"})\` — stop the session`, true);
|
|
602
|
+
}
|
|
603
|
+
catch (e) {
|
|
604
|
+
return toolError(e);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// ─── Fire: send task and return immediately ────────────────────────
|
|
608
|
+
server.tool("opencode_fire", "Fire-and-forget: send a task to OpenCode and return immediately. OpenCode works autonomously in the background. Use `opencode_check` to check progress anytime. Best for long-running tasks when you want to do other work in parallel.", {
|
|
609
|
+
prompt: z.string().describe("The task or instruction to send"),
|
|
610
|
+
sessionId: z
|
|
611
|
+
.string()
|
|
612
|
+
.optional()
|
|
613
|
+
.describe("Existing session ID to continue (omit to create a new session)"),
|
|
614
|
+
title: z.string().optional().describe("Session title (only for new sessions)"),
|
|
615
|
+
providerID: z.string().optional().describe("Provider ID (e.g. 'anthropic')"),
|
|
616
|
+
modelID: z.string().optional().describe("Model ID (e.g. 'claude-opus-4-6')"),
|
|
617
|
+
variant: z.string().optional().describe("Model variant"),
|
|
618
|
+
agent: z.string().optional().describe("Agent to use"),
|
|
619
|
+
directory: directoryParam,
|
|
620
|
+
}, async ({ prompt, sessionId, title, providerID, modelID, variant, agent, directory }) => {
|
|
621
|
+
try {
|
|
622
|
+
// 1. Create or reuse session
|
|
623
|
+
let sid = sessionId;
|
|
624
|
+
if (!sid) {
|
|
625
|
+
const session = (await client.post("/session", {
|
|
626
|
+
title: title ?? prompt.slice(0, 80),
|
|
627
|
+
}, { directory }));
|
|
628
|
+
sid = session.id;
|
|
629
|
+
}
|
|
630
|
+
// 2. Send async
|
|
631
|
+
const body = {
|
|
632
|
+
parts: [{ type: "text", text: prompt }],
|
|
633
|
+
noReply: false,
|
|
634
|
+
};
|
|
635
|
+
const model = applyModelDefaults(providerID, modelID, variant);
|
|
636
|
+
if (model)
|
|
637
|
+
body.model = model;
|
|
638
|
+
if (agent)
|
|
639
|
+
body.agent = agent;
|
|
640
|
+
await client.post(`/session/${sid}/message`, body, { directory });
|
|
641
|
+
const dirLabel = directory ? `Directory: ${directory}\n` : "";
|
|
642
|
+
return toolResult(`${dirLabel}Task dispatched to session: ${sid}\n\n` +
|
|
643
|
+
`OpenCode is now working autonomously. Use these tools to monitor:\n` +
|
|
644
|
+
`- \`opencode_check({sessionId: "${sid}"})\` — quick progress check\n` +
|
|
645
|
+
`- \`opencode_session_todo({id: "${sid}"})\` — see the agent's task list\n` +
|
|
646
|
+
`- \`opencode_wait({sessionId: "${sid}"})\` — block until done\n` +
|
|
647
|
+
`- \`opencode_review_changes({sessionId: "${sid}"})\` — see file changes after completion`);
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
return toolError(e);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
// ─── Check: cheap progress report for a session ────────────────────
|
|
654
|
+
server.tool("opencode_check", "Get a compact cached progress report for a session. Much cheaper than opencode_conversation or opencode_wait — returns status, todos, and file counts in a single call. Use this to monitor sessions launched with opencode_fire.", {
|
|
655
|
+
sessionId: z.string().describe("Session ID to check"),
|
|
656
|
+
detailed: z
|
|
657
|
+
.boolean()
|
|
658
|
+
.optional()
|
|
659
|
+
.describe("If true, include the last message text (default: false)"),
|
|
660
|
+
directory: directoryParam,
|
|
661
|
+
}, readOnly, async ({ sessionId, detailed, directory }) => {
|
|
662
|
+
try {
|
|
663
|
+
// Validate directory early — before .catch(() => null) swallows the error
|
|
664
|
+
directory = normalizeDirectory(directory);
|
|
665
|
+
// Parallel fetch: status, todos, session info, optionally last message
|
|
666
|
+
const promises = [
|
|
667
|
+
client.get("/session/status", undefined, directory),
|
|
668
|
+
client.get(`/session/${sessionId}/todo`, undefined, directory).catch(() => null),
|
|
669
|
+
client.get(`/session/${sessionId}`, undefined, directory).catch(() => null),
|
|
670
|
+
];
|
|
671
|
+
if (detailed) {
|
|
672
|
+
promises.push(client.get(`/session/${sessionId}/message`, { limit: "1" }, directory).catch(() => null));
|
|
673
|
+
}
|
|
674
|
+
const [statuses, todos, sessionInfo, lastMessages] = await Promise.all(promises);
|
|
675
|
+
const status = resolveSessionStatus(statuses[sessionId]);
|
|
676
|
+
const lines = [];
|
|
677
|
+
// Session title
|
|
678
|
+
const title = sessionInfo?.title ?? "(untitled)";
|
|
679
|
+
lines.push(`## ${title} [${sessionId}]`);
|
|
680
|
+
lines.push(`Status: **${status}**`);
|
|
681
|
+
// Todos
|
|
682
|
+
if (Array.isArray(todos) && todos.length > 0) {
|
|
683
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
684
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
685
|
+
const pending = todos.length - completed - inProgress;
|
|
686
|
+
lines.push(`Tasks: ${completed}/${todos.length} completed` +
|
|
687
|
+
(inProgress > 0 ? `, ${inProgress} in progress` : "") +
|
|
688
|
+
(pending > 0 ? `, ${pending} pending` : ""));
|
|
689
|
+
// Show current task
|
|
690
|
+
const current = todos.find((t) => t.status === "in_progress");
|
|
691
|
+
if (current) {
|
|
692
|
+
lines.push(`Current: ${current.content ?? current.title ?? "(unknown)"}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// File changes count (from diff endpoint)
|
|
696
|
+
try {
|
|
697
|
+
const diffs = await client.get(`/session/${sessionId}/diff`, undefined, directory);
|
|
698
|
+
if (Array.isArray(diffs) && diffs.length > 0) {
|
|
699
|
+
lines.push(`Files changed: ${diffs.length}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch { /* non-critical */ }
|
|
703
|
+
// Last message (if detailed)
|
|
704
|
+
if (detailed && Array.isArray(lastMessages) && lastMessages.length > 0) {
|
|
705
|
+
const lastMsg = formatMessageResponse(lastMessages[lastMessages.length - 1]);
|
|
706
|
+
if (lastMsg) {
|
|
707
|
+
const truncated = lastMsg.length > 500 ? lastMsg.slice(0, 497) + "..." : lastMsg;
|
|
708
|
+
lines.push(`\n### Last message\n${truncated}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Suggest next action based on status
|
|
712
|
+
if (status === "idle" || status === "completed") {
|
|
713
|
+
lines.push(`\nDone! Use \`opencode_review_changes({sessionId: "${sessionId}"})\` to see all changes.`);
|
|
714
|
+
}
|
|
715
|
+
else if (status === "error") {
|
|
716
|
+
lines.push(`\nFailed. Use \`opencode_conversation({sessionId: "${sessionId}"})\` to see what went wrong.`);
|
|
717
|
+
}
|
|
718
|
+
return toolResult(lines.join("\n"));
|
|
719
|
+
}
|
|
720
|
+
catch (e) {
|
|
721
|
+
return toolError(e);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// ─── Quick status dashboard ───────────────────────────────────────
|
|
725
|
+
server.tool("opencode_status", "Get a quick status dashboard: server health, provider count, session count, and VCS info. Lighter than opencode_setup — good for at-a-glance checks.", {
|
|
726
|
+
directory: directoryParam,
|
|
727
|
+
}, readOnly, async ({ directory }) => {
|
|
728
|
+
try {
|
|
729
|
+
// Validate directory early — before Promise.all with .catch(() => null)
|
|
730
|
+
// swallows the validation error.
|
|
731
|
+
directory = normalizeDirectory(directory);
|
|
732
|
+
const [health, providerRaw, sessions, vcs] = await Promise.all([
|
|
733
|
+
client.get("/global/health", undefined, directory).catch(() => null),
|
|
734
|
+
client.get("/provider", undefined, directory).catch(() => null),
|
|
735
|
+
client.get("/session", undefined, directory).catch(() => null),
|
|
736
|
+
client.get("/vcs", undefined, directory).catch(() => null),
|
|
737
|
+
]);
|
|
738
|
+
const lines = [];
|
|
739
|
+
// Health
|
|
740
|
+
if (health) {
|
|
741
|
+
const h = health;
|
|
742
|
+
lines.push(`Server: healthy (v${h.version ?? "?"})`);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
lines.push("Server: UNREACHABLE");
|
|
746
|
+
}
|
|
747
|
+
// Providers
|
|
748
|
+
if (providerRaw) {
|
|
749
|
+
const providers = (providerRaw && typeof providerRaw === "object" && "all" in providerRaw
|
|
750
|
+
? providerRaw.all
|
|
751
|
+
: providerRaw);
|
|
752
|
+
if (Array.isArray(providers)) {
|
|
753
|
+
const configured = providers.filter(isProviderConfigured).length;
|
|
754
|
+
lines.push(`Providers: ${configured} configured / ${providers.length} total`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Sessions
|
|
758
|
+
if (sessions && Array.isArray(sessions)) {
|
|
759
|
+
lines.push(`Sessions: ${sessions.length}`);
|
|
760
|
+
}
|
|
761
|
+
// VCS
|
|
762
|
+
if (vcs) {
|
|
763
|
+
const v = vcs;
|
|
764
|
+
const branch = v.branch ?? "unknown";
|
|
765
|
+
const dirty = v.dirty === true ? " (dirty)" : v.dirty === false ? " (clean)" : "";
|
|
766
|
+
lines.push(`Branch: ${branch}${dirty}`);
|
|
767
|
+
}
|
|
768
|
+
return toolResult(`## Status\n${lines.join("\n")}`);
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
return toolError(e);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
//# sourceMappingURL=workflow.js.map
|