amicus 1.0.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 +46 -0
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/bin/amicus.js +382 -0
- package/electron/assets/icon.png +0 -0
- package/electron/assets/icon.svg +5 -0
- package/electron/fold.js +163 -0
- package/electron/ipc-setup.js +176 -0
- package/electron/load-failsafe.js +85 -0
- package/electron/main.js +468 -0
- package/electron/preload-setup.js +38 -0
- package/electron/preload.js +33 -0
- package/electron/setup-ui-alias-script.js +218 -0
- package/electron/setup-ui-aliases.js +85 -0
- package/electron/setup-ui-keys-script.js +115 -0
- package/electron/setup-ui-keys.js +97 -0
- package/electron/setup-ui-model.js +138 -0
- package/electron/setup-ui-styles.js +327 -0
- package/electron/setup-ui.js +465 -0
- package/electron/summary.js +118 -0
- package/electron/toolbar.js +229 -0
- package/electron/window-position.js +35 -0
- package/package.json +98 -0
- package/scripts/postinstall.js +193 -0
- package/scripts/setup-hooks.js +42 -0
- package/skill/SKILL.md +976 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
- package/skills/second-opinion/MODEL-NOTES.md +104 -0
- package/skills/second-opinion/SKILL.md +389 -0
- package/src/cli-handlers.js +188 -0
- package/src/cli.js +400 -0
- package/src/conflict.js +144 -0
- package/src/context-compression.js +102 -0
- package/src/context.js +199 -0
- package/src/drift.js +144 -0
- package/src/environment.js +157 -0
- package/src/headless.js +742 -0
- package/src/index.js +106 -0
- package/src/jsonl-parser.js +180 -0
- package/src/mcp-server.js +625 -0
- package/src/mcp-tools.js +407 -0
- package/src/opencode-client.js +615 -0
- package/src/prompt-builder.js +355 -0
- package/src/prompts/cowork-agent-prompt.js +118 -0
- package/src/session-manager.js +414 -0
- package/src/session.js +180 -0
- package/src/sidecar/context-builder.js +297 -0
- package/src/sidecar/continue.js +212 -0
- package/src/sidecar/crash-handler.js +56 -0
- package/src/sidecar/fanout-leg.js +107 -0
- package/src/sidecar/fanout-output.js +46 -0
- package/src/sidecar/fanout.js +236 -0
- package/src/sidecar/interactive.js +217 -0
- package/src/sidecar/models.js +135 -0
- package/src/sidecar/progress.js +218 -0
- package/src/sidecar/read.js +183 -0
- package/src/sidecar/resume.js +221 -0
- package/src/sidecar/session-utils.js +288 -0
- package/src/sidecar/setup-window.js +79 -0
- package/src/sidecar/setup.js +280 -0
- package/src/sidecar/start.js +251 -0
- package/src/utils/agent-mapping.js +138 -0
- package/src/utils/alias-audit.js +98 -0
- package/src/utils/alias-resolver.js +77 -0
- package/src/utils/api-key-store.js +259 -0
- package/src/utils/api-key-validation.js +97 -0
- package/src/utils/auth-json.js +109 -0
- package/src/utils/config.js +291 -0
- package/src/utils/curated-models.js +82 -0
- package/src/utils/env-compat.js +38 -0
- package/src/utils/env-loader.js +54 -0
- package/src/utils/idle-watchdog.js +225 -0
- package/src/utils/input-validators.js +127 -0
- package/src/utils/lifecycle.js +43 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/mcp-discovery.js +194 -0
- package/src/utils/mcp-validators.js +78 -0
- package/src/utils/model-catalog.js +103 -0
- package/src/utils/model-fetcher.js +179 -0
- package/src/utils/model-validator.js +207 -0
- package/src/utils/path-setup.js +41 -0
- package/src/utils/port-pid.js +39 -0
- package/src/utils/prompt-source.js +53 -0
- package/src/utils/result-schema.js +261 -0
- package/src/utils/server-setup.js +93 -0
- package/src/utils/session-abort.js +53 -0
- package/src/utils/session-lock.js +95 -0
- package/src/utils/shared-server.js +216 -0
- package/src/utils/start-helpers.js +76 -0
- package/src/utils/thinking-validators.js +92 -0
- package/src/utils/update-notifier-loader.js +18 -0
- package/src/utils/updater.js +157 -0
- package/src/utils/validators.js +300 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode SDK Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides a clean interface for interacting with the @opencode-ai/sdk.
|
|
5
|
+
* Handles model string parsing and provides simplified API methods.
|
|
6
|
+
* Now uses SDK server creation instead of spawning CLI.
|
|
7
|
+
*
|
|
8
|
+
* Spec Reference: SDK Migration Plan
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Lazy-load SDK (ESM module) - all imports must be dynamic
|
|
12
|
+
let _sdk = null;
|
|
13
|
+
async function getSDK() {
|
|
14
|
+
if (!_sdk) {
|
|
15
|
+
_sdk = await import('@opencode-ai/sdk');
|
|
16
|
+
}
|
|
17
|
+
return _sdk;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getCreateOpencodeClient() {
|
|
21
|
+
const sdk = await getSDK();
|
|
22
|
+
return sdk.createOpencodeClient;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getCreateOpencodeServer() {
|
|
26
|
+
const sdk = await getSDK();
|
|
27
|
+
return sdk.createOpencodeServer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a model string into SDK format
|
|
32
|
+
*
|
|
33
|
+
* Converts from sidecar format (e.g., 'openrouter/google/gemini-2.5-flash')
|
|
34
|
+
* to SDK format ({ providerID: 'openrouter', modelID: 'google/gemini-2.5-flash' })
|
|
35
|
+
*
|
|
36
|
+
* @param {string|object} modelString - Model identifier or already-parsed object
|
|
37
|
+
* @returns {{providerID: string, modelID: string}} SDK model specification
|
|
38
|
+
*/
|
|
39
|
+
function parseModelString(modelString) {
|
|
40
|
+
// If already an object, return as-is
|
|
41
|
+
if (typeof modelString === 'object' && modelString !== null) {
|
|
42
|
+
return modelString;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle empty string
|
|
46
|
+
if (!modelString) {
|
|
47
|
+
return { providerID: 'openrouter', modelID: '' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parts = modelString.split('/');
|
|
51
|
+
|
|
52
|
+
// Single part (just model name) - default to openrouter
|
|
53
|
+
if (parts.length === 1) {
|
|
54
|
+
return { providerID: 'openrouter', modelID: modelString };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Two or more parts - first is provider, rest is modelID
|
|
58
|
+
return {
|
|
59
|
+
providerID: parts[0],
|
|
60
|
+
modelID: parts.slice(1).join('/')
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an OpenCode SDK client
|
|
66
|
+
*
|
|
67
|
+
* @param {string} [baseUrl] - Base URL for the OpenCode server
|
|
68
|
+
* @returns {Promise<import('@opencode-ai/sdk').OpencodeClient>} SDK client instance
|
|
69
|
+
*/
|
|
70
|
+
async function createClient(baseUrl) {
|
|
71
|
+
const createOpencodeClient = await getCreateOpencodeClient();
|
|
72
|
+
const config = baseUrl ? { baseUrl } : {};
|
|
73
|
+
return createOpencodeClient(config);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new session
|
|
78
|
+
*
|
|
79
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
80
|
+
* @returns {Promise<string>} Session ID
|
|
81
|
+
* @throws {Error} If session creation fails
|
|
82
|
+
*/
|
|
83
|
+
async function createSession(client) {
|
|
84
|
+
const result = await client.session.create({});
|
|
85
|
+
|
|
86
|
+
if (result.error) {
|
|
87
|
+
throw new Error(result.error.message || 'Failed to create session');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle both direct ID and nested session.id
|
|
91
|
+
const sessionId = result.data?.id || result.data?.session?.id;
|
|
92
|
+
|
|
93
|
+
if (!sessionId) {
|
|
94
|
+
throw new Error('No session ID returned');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return sessionId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Send a prompt to a session
|
|
102
|
+
*
|
|
103
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
104
|
+
* @param {string} sessionId - Session ID
|
|
105
|
+
* @param {object} options - Prompt options
|
|
106
|
+
* @param {string|object} options.model - Model identifier or SDK format object
|
|
107
|
+
* @param {string} [options.system] - System prompt
|
|
108
|
+
* @param {Array} options.parts - Message parts
|
|
109
|
+
* @param {string} [options.agent] - Agent to use (e.g., 'build', 'explore')
|
|
110
|
+
* @param {object} [options.tools] - Tool configuration
|
|
111
|
+
* @param {object} [options.reasoning] - Reasoning/thinking configuration
|
|
112
|
+
* @param {string} [options.reasoning.effort] - Effort level: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'none'
|
|
113
|
+
* @param {object} [options.watchdog] - IdleWatchdog instance to signal busy/idle around the API call
|
|
114
|
+
* @returns {Promise<object>} API response
|
|
115
|
+
*/
|
|
116
|
+
async function sendPrompt(client, sessionId, options) {
|
|
117
|
+
const { model, system, parts, agent, tools, reasoning, watchdog } = options;
|
|
118
|
+
|
|
119
|
+
// Parse model string to SDK format
|
|
120
|
+
const modelSpec = parseModelString(model);
|
|
121
|
+
|
|
122
|
+
// Build request body
|
|
123
|
+
const body = {
|
|
124
|
+
model: modelSpec,
|
|
125
|
+
parts
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Add optional fields
|
|
129
|
+
if (system) {
|
|
130
|
+
body.system = system;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (agent) {
|
|
134
|
+
// OpenCode API expects lowercase agent names
|
|
135
|
+
body.agent = agent.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (tools) {
|
|
139
|
+
body.tools = tools;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (reasoning) {
|
|
143
|
+
body.reasoning = reasoning;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (watchdog) {
|
|
147
|
+
watchdog.markBusy();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let result;
|
|
151
|
+
try {
|
|
152
|
+
result = await client.session.promptAsync({
|
|
153
|
+
path: { id: sessionId },
|
|
154
|
+
body
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
if (watchdog) {
|
|
158
|
+
watchdog.markIdle();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Log but don't throw on promptAsync errors.
|
|
163
|
+
// promptAsync is fire-and-forget: the server queues the prompt for async
|
|
164
|
+
// processing. Errors here may be informational (e.g., model config warnings)
|
|
165
|
+
// rather than fatal. The polling loop will detect real failures via timeout.
|
|
166
|
+
if (result.error) {
|
|
167
|
+
const { logger } = require('./utils/logger');
|
|
168
|
+
logger.error('promptAsync returned error (continuing to poll)', {
|
|
169
|
+
error: result.error.message || JSON.stringify(result.error),
|
|
170
|
+
sessionId
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get messages for a session
|
|
179
|
+
*
|
|
180
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
181
|
+
* @param {string} sessionId - Session ID
|
|
182
|
+
* @returns {Promise<Array>} Array of messages
|
|
183
|
+
*/
|
|
184
|
+
async function getMessages(client, sessionId) {
|
|
185
|
+
const result = await client.session.messages({
|
|
186
|
+
path: { id: sessionId }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return result.data || [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if the OpenCode server is healthy
|
|
194
|
+
*
|
|
195
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
196
|
+
* @returns {Promise<boolean>} True if server is healthy
|
|
197
|
+
*/
|
|
198
|
+
async function checkHealth(client) {
|
|
199
|
+
try {
|
|
200
|
+
await client.config.get({});
|
|
201
|
+
return true;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a child session with a parent ID
|
|
209
|
+
*
|
|
210
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
211
|
+
* @param {string} parentId - Parent session ID
|
|
212
|
+
* @returns {Promise<string>} Child session ID
|
|
213
|
+
* @throws {Error} If child session creation fails
|
|
214
|
+
*/
|
|
215
|
+
async function createChildSession(client, parentId) {
|
|
216
|
+
const result = await client.session.create({
|
|
217
|
+
body: { parentID: parentId }
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (result.error) {
|
|
221
|
+
throw new Error(result.error.message || 'Failed to create child session');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle both direct ID and nested session.id
|
|
225
|
+
const sessionId = result.data?.id || result.data?.session?.id;
|
|
226
|
+
|
|
227
|
+
if (!sessionId) {
|
|
228
|
+
throw new Error('No session ID returned');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return sessionId;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get child sessions for a parent session
|
|
236
|
+
*
|
|
237
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
238
|
+
* @param {string} parentId - Parent session ID
|
|
239
|
+
* @returns {Promise<Array>} Array of child sessions
|
|
240
|
+
*/
|
|
241
|
+
async function getChildren(client, parentId) {
|
|
242
|
+
const result = await client.session.children({
|
|
243
|
+
path: { id: parentId }
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return result.data || [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* List all sessions from the OpenCode server
|
|
251
|
+
*
|
|
252
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
253
|
+
* @returns {Promise<Array>} Array of sessions
|
|
254
|
+
*/
|
|
255
|
+
async function listSessions(client) {
|
|
256
|
+
try {
|
|
257
|
+
const result = await client.session.list();
|
|
258
|
+
return result.data || [];
|
|
259
|
+
} catch (_error) {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Abort a running session
|
|
266
|
+
*
|
|
267
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
268
|
+
* @param {string} sessionId - Session ID to abort
|
|
269
|
+
* @returns {Promise<void>}
|
|
270
|
+
*/
|
|
271
|
+
async function abortSession(client, sessionId) {
|
|
272
|
+
await client.session.abort({ path: { id: sessionId } });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get session status
|
|
277
|
+
*
|
|
278
|
+
* @param {import('@opencode-ai/sdk').OpencodeClient} client - SDK client
|
|
279
|
+
* @param {string} sessionId - Session ID
|
|
280
|
+
* @returns {Promise<Object>} Session status
|
|
281
|
+
*/
|
|
282
|
+
async function getSessionStatus(client, sessionId) {
|
|
283
|
+
const result = await client.session.status({
|
|
284
|
+
path: { id: sessionId }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return result.data || {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build the server options object for createOpencodeServer.
|
|
292
|
+
* Extracted for testability (no SDK dependency).
|
|
293
|
+
*
|
|
294
|
+
* @param {object} [options] - Server options
|
|
295
|
+
* @param {number} [options.port] - Port to run on
|
|
296
|
+
* @param {string} [options.hostname='127.0.0.1'] - Hostname to bind to
|
|
297
|
+
* @param {AbortSignal} [options.signal] - Abort signal to stop server
|
|
298
|
+
* @param {object} [options.mcp] - MCP server configurations
|
|
299
|
+
* @param {string} [options.model] - Default model
|
|
300
|
+
* @param {string} [options.client] - Client type ('cowork', 'code-local', etc.)
|
|
301
|
+
* @param {string} [options.systemPrompt] - System prompt to set on agent config (hidden from UI)
|
|
302
|
+
* @param {string} [options.agentName] - Agent to set systemPrompt on (default: 'chat')
|
|
303
|
+
* @returns {object} Server options ready for createOpencodeServer
|
|
304
|
+
*/
|
|
305
|
+
function buildServerOptions(options = {}) {
|
|
306
|
+
// Build config object for SDK
|
|
307
|
+
const config = {};
|
|
308
|
+
if (options.mcp) {
|
|
309
|
+
// Normalize MCP configs for OpenCode's discriminated union format.
|
|
310
|
+
//
|
|
311
|
+
// OpenCode accepts exactly two type values (ConfigInvalidError otherwise):
|
|
312
|
+
// { type: "local", enabled: true, command: ["cmd", ...args] }
|
|
313
|
+
// { type: "remote", enabled: true, url: "https://..." }
|
|
314
|
+
//
|
|
315
|
+
// Input formats we handle:
|
|
316
|
+
// Claude Code internal : { type: "stdio", command: "cmd", args: [...] }
|
|
317
|
+
// Claude Desktop : { command: "cmd", args: [...] } (no type field)
|
|
318
|
+
// Claude Code remote : { type: "http", url: "..." }
|
|
319
|
+
// { type: "sse", url: "..." }
|
|
320
|
+
// Already normalized : { type: "local"|"remote", ... } → pass through
|
|
321
|
+
//
|
|
322
|
+
// Note: OpenCode's "local" schema does NOT support an `env` field.
|
|
323
|
+
// Environment variables from the source config are intentionally dropped;
|
|
324
|
+
// MCP servers inherit the parent process environment which is sufficient.
|
|
325
|
+
const normalized = {};
|
|
326
|
+
for (const [name, serverConfig] of Object.entries(options.mcp)) {
|
|
327
|
+
const t = serverConfig.type;
|
|
328
|
+
if (t === 'stdio' || (!t && serverConfig.command)) {
|
|
329
|
+
// stdio process (Claude Code or Claude Desktop format) → local
|
|
330
|
+
if (!serverConfig.command) {
|
|
331
|
+
const { logger } = require('./utils/logger');
|
|
332
|
+
logger.warn(`MCP server "${name}": type "stdio" requires a command — skipping`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const cmd = typeof serverConfig.command === 'string' ? serverConfig.command : String(serverConfig.command);
|
|
336
|
+
const args = Array.isArray(serverConfig.args) ? serverConfig.args : [];
|
|
337
|
+
normalized[name] = {
|
|
338
|
+
type: 'local',
|
|
339
|
+
enabled: true,
|
|
340
|
+
command: [cmd, ...args]
|
|
341
|
+
};
|
|
342
|
+
} else if (t === 'http' || t === 'sse') {
|
|
343
|
+
// HTTP/SSE remote server → remote
|
|
344
|
+
if (!serverConfig.url) {
|
|
345
|
+
const { logger } = require('./utils/logger');
|
|
346
|
+
logger.warn(`MCP server "${name}": type "${t}" requires a url — skipping`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Preserve extra remote options (headers, oauth, timeout, etc.)
|
|
350
|
+
// eslint-disable-next-line no-unused-vars
|
|
351
|
+
const { type: _t, args: _a, command: _c, ...rest } = serverConfig;
|
|
352
|
+
normalized[name] = {
|
|
353
|
+
...rest,
|
|
354
|
+
type: 'remote',
|
|
355
|
+
enabled: rest.enabled !== undefined ? rest.enabled : true
|
|
356
|
+
};
|
|
357
|
+
} else {
|
|
358
|
+
// Already in OpenCode format (type: "local"|"remote") or unknown — pass through
|
|
359
|
+
if (t && t !== 'local' && t !== 'remote') {
|
|
360
|
+
const { logger } = require('./utils/logger');
|
|
361
|
+
logger.warn(`MCP server "${name}": unrecognized type "${t}" — passing through unchanged`);
|
|
362
|
+
}
|
|
363
|
+
normalized[name] = serverConfig;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
config.mcp = normalized;
|
|
367
|
+
}
|
|
368
|
+
if (options.model) {
|
|
369
|
+
config.model = options.model;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Sync sidecar aliases into OpenCode's provider.models so the UI
|
|
373
|
+
// model picker shows all configured models (single source of truth).
|
|
374
|
+
const { buildProviderModels } = require('./utils/config');
|
|
375
|
+
config.provider = buildProviderModels();
|
|
376
|
+
|
|
377
|
+
// Register custom 'chat' agent: reads auto-approved, writes/bash require permission
|
|
378
|
+
const chatAgent = {
|
|
379
|
+
description: 'Conversational agent — reads are auto-approved, writes and commands require permission',
|
|
380
|
+
mode: 'primary',
|
|
381
|
+
permission: {
|
|
382
|
+
edit: 'ask',
|
|
383
|
+
bash: 'ask',
|
|
384
|
+
webfetch: 'allow'
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// When launched from Cowork, replace the SE-focused base prompt with a general-purpose one
|
|
389
|
+
if (options.client === 'cowork') {
|
|
390
|
+
const { buildCoworkAgentPrompt } = require('./prompts/cowork-agent-prompt');
|
|
391
|
+
chatAgent.prompt = buildCoworkAgentPrompt();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
config.agent = {
|
|
395
|
+
...(config.agent || {}),
|
|
396
|
+
chat: chatAgent
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Set system prompt on the target agent's config (hidden from UI).
|
|
400
|
+
// The promptAsync `system` field is rendered as a visible chat message,
|
|
401
|
+
// but agent.prompt is injected as the system instruction invisibly.
|
|
402
|
+
if (options.systemPrompt) {
|
|
403
|
+
const targetName = (options.agentName || 'chat').toLowerCase();
|
|
404
|
+
if (targetName === 'chat') {
|
|
405
|
+
// Chat is our custom agent — append to existing prompt (e.g., cowork)
|
|
406
|
+
chatAgent.prompt = chatAgent.prompt
|
|
407
|
+
? `${chatAgent.prompt}\n\n${options.systemPrompt}`
|
|
408
|
+
: options.systemPrompt;
|
|
409
|
+
} else {
|
|
410
|
+
// Built-in agents (build, plan, etc.) — register with system prompt
|
|
411
|
+
config.agent[targetName] = {
|
|
412
|
+
...(config.agent[targetName] || {}),
|
|
413
|
+
prompt: options.systemPrompt
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const serverOptions = {
|
|
419
|
+
hostname: options.hostname || '127.0.0.1',
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Only include port/signal when explicitly set — passing undefined
|
|
423
|
+
// overrides the SDK's defaults via Object.assign, causing --port=undefined
|
|
424
|
+
if (options.port !== undefined) {
|
|
425
|
+
serverOptions.port = options.port;
|
|
426
|
+
}
|
|
427
|
+
if (options.signal !== undefined) {
|
|
428
|
+
serverOptions.signal = options.signal;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Only add config if we have settings
|
|
432
|
+
if (Object.keys(config).length > 0) {
|
|
433
|
+
serverOptions.config = config;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return serverOptions;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const { findListenerPid } = require('./utils/port-pid');
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build the { url, goPid, close } server handle around a raw SDK server.
|
|
443
|
+
* Extracted + dependency-injected so the goPid capture and cross-platform
|
|
444
|
+
* force-kill (F3 #15) can be unit-tested without the SDK's dynamic import.
|
|
445
|
+
* @param {{url:string, close:Function, pid?:number, process?:{pid:number}}} sdkServer
|
|
446
|
+
* @param {{findListenerPid?:Function, kill?:Function, logger?:object}} [deps]
|
|
447
|
+
* @returns {{url:string, goPid:number|null, close:Function}}
|
|
448
|
+
*/
|
|
449
|
+
function buildServerHandle(sdkServer, deps = {}) {
|
|
450
|
+
const findPid = deps.findListenerPid || findListenerPid;
|
|
451
|
+
const kill = deps.kill || ((pid, sig) => process.kill(pid, sig));
|
|
452
|
+
const log = deps.logger || require('./utils/logger').logger;
|
|
453
|
+
const serverPort = parseInt(new URL(sdkServer.url).port, 10);
|
|
454
|
+
const goPid = sdkServer.pid || (sdkServer.process && sdkServer.process.pid) || findPid(serverPort);
|
|
455
|
+
const server = {
|
|
456
|
+
url: sdkServer.url,
|
|
457
|
+
goPid,
|
|
458
|
+
close() {
|
|
459
|
+
sdkServer.close();
|
|
460
|
+
const fallback = setTimeout(() => {
|
|
461
|
+
const pid = server.goPid;
|
|
462
|
+
if (pid && pid !== process.pid) {
|
|
463
|
+
try { kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
464
|
+
log.debug('Force-killed OpenCode server', { port: serverPort, pid });
|
|
465
|
+
}
|
|
466
|
+
}, 2000);
|
|
467
|
+
if (fallback.unref) { fallback.unref(); }
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
return server;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Start an OpenCode server and return client + server handle
|
|
475
|
+
*
|
|
476
|
+
* @param {object} [options] - Server options
|
|
477
|
+
* @param {number} [options.port] - Port to run on
|
|
478
|
+
* @param {string} [options.hostname='127.0.0.1'] - Hostname to bind to
|
|
479
|
+
* @param {AbortSignal} [options.signal] - Abort signal to stop server
|
|
480
|
+
* @param {object} [options.mcp] - MCP server configurations
|
|
481
|
+
* @param {string} [options.model] - Default model
|
|
482
|
+
* @param {string} [options.client] - Client type ('cowork', 'code-local', etc.)
|
|
483
|
+
* @returns {Promise<{client: object, server: {url: string, close: Function}}>}
|
|
484
|
+
*/
|
|
485
|
+
async function startServer(options = {}) {
|
|
486
|
+
const createOpencodeServer = await getCreateOpencodeServer();
|
|
487
|
+
const serverOptions = buildServerOptions(options);
|
|
488
|
+
|
|
489
|
+
const sdkServer = await createOpencodeServer(serverOptions);
|
|
490
|
+
const client = await createClient(sdkServer.url);
|
|
491
|
+
|
|
492
|
+
// Capture the Go server PID once so close() can force-kill it cross-platform
|
|
493
|
+
// (F3 #15). Prefer a PID the SDK exposes; fall back to the port listener.
|
|
494
|
+
// findListenerPid may return null if the port isn't bound yet (startup race);
|
|
495
|
+
// in that case close() degrades to SIGTERM-only, which is acceptable best-effort.
|
|
496
|
+
const server = buildServerHandle(sdkServer);
|
|
497
|
+
|
|
498
|
+
return { client, server };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Load MCP configuration from user's opencode.json
|
|
503
|
+
*
|
|
504
|
+
* @param {string} [configPath] - Optional path to config file
|
|
505
|
+
* @returns {object|null} MCP configuration or null if not found
|
|
506
|
+
*/
|
|
507
|
+
function loadMcpConfig(configPath) {
|
|
508
|
+
const fs = require('fs');
|
|
509
|
+
const path = require('path');
|
|
510
|
+
const os = require('os');
|
|
511
|
+
|
|
512
|
+
// Check paths in order of precedence
|
|
513
|
+
const paths = [];
|
|
514
|
+
|
|
515
|
+
if (configPath) {
|
|
516
|
+
paths.push(configPath);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Global config location
|
|
520
|
+
paths.push(path.join(os.homedir(), '.config', 'opencode', 'opencode.json'));
|
|
521
|
+
|
|
522
|
+
// Project-level config (cwd)
|
|
523
|
+
paths.push(path.join(process.cwd(), 'opencode.json'));
|
|
524
|
+
|
|
525
|
+
for (const configFile of paths) {
|
|
526
|
+
try {
|
|
527
|
+
if (fs.existsSync(configFile)) {
|
|
528
|
+
const content = fs.readFileSync(configFile, 'utf-8');
|
|
529
|
+
const config = JSON.parse(content);
|
|
530
|
+
if (config.mcp && Object.keys(config.mcp).length > 0) {
|
|
531
|
+
return config.mcp;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// Ignore parse errors, try next file
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Parse MCP server specification from CLI format
|
|
544
|
+
*
|
|
545
|
+
* Supports formats:
|
|
546
|
+
* - name=url (remote server)
|
|
547
|
+
* - name=command (local server with simple command)
|
|
548
|
+
* - JSON string (full config)
|
|
549
|
+
*
|
|
550
|
+
* @param {string} spec - MCP server specification
|
|
551
|
+
* @returns {{name: string, config: object}|null} Parsed MCP config or null
|
|
552
|
+
*/
|
|
553
|
+
function parseMcpSpec(spec) {
|
|
554
|
+
// Try JSON first
|
|
555
|
+
if (spec.startsWith('{')) {
|
|
556
|
+
try {
|
|
557
|
+
const parsed = JSON.parse(spec);
|
|
558
|
+
const name = Object.keys(parsed)[0];
|
|
559
|
+
return { name, config: parsed[name] };
|
|
560
|
+
} catch (e) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Try name=value format
|
|
566
|
+
const eqIndex = spec.indexOf('=');
|
|
567
|
+
if (eqIndex > 0) {
|
|
568
|
+
const name = spec.slice(0, eqIndex);
|
|
569
|
+
const value = spec.slice(eqIndex + 1);
|
|
570
|
+
|
|
571
|
+
// If value looks like a URL, treat as remote
|
|
572
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
573
|
+
return {
|
|
574
|
+
name,
|
|
575
|
+
config: {
|
|
576
|
+
type: 'remote',
|
|
577
|
+
url: value,
|
|
578
|
+
enabled: true
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Otherwise treat as local command
|
|
584
|
+
return {
|
|
585
|
+
name,
|
|
586
|
+
config: {
|
|
587
|
+
type: 'local',
|
|
588
|
+
command: value.split(' '),
|
|
589
|
+
enabled: true
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
module.exports = {
|
|
598
|
+
parseModelString,
|
|
599
|
+
createClient,
|
|
600
|
+
createSession,
|
|
601
|
+
createChildSession,
|
|
602
|
+
sendPrompt,
|
|
603
|
+
sendPromptAsync: sendPrompt, // Alias: sendPrompt already uses promptAsync internally
|
|
604
|
+
getMessages,
|
|
605
|
+
getChildren,
|
|
606
|
+
listSessions,
|
|
607
|
+
getSessionStatus,
|
|
608
|
+
abortSession,
|
|
609
|
+
checkHealth,
|
|
610
|
+
buildServerOptions,
|
|
611
|
+
buildServerHandle,
|
|
612
|
+
startServer,
|
|
613
|
+
loadMcpConfig,
|
|
614
|
+
parseMcpSpec
|
|
615
|
+
};
|