oceancode 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2230 -355
- package/package.json +28 -27
- package/dist/chunk-EB736JHH.js +0 -1191
- package/dist/run-R7NU5EID.js +0 -6
package/dist/chunk-EB736JHH.js
DELETED
|
@@ -1,1191 +0,0 @@
|
|
|
1
|
-
// src/commands/run.ts
|
|
2
|
-
import chalk3 from "chalk";
|
|
3
|
-
|
|
4
|
-
// src/server/manager.ts
|
|
5
|
-
import { spawn } from "child_process";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import os from "os";
|
|
8
|
-
import fs from "fs";
|
|
9
|
-
var DEFAULT_HOST = "127.0.0.1";
|
|
10
|
-
var DEFAULT_PORT = 4196;
|
|
11
|
-
var managedProcess = null;
|
|
12
|
-
function getBaseUrl(config) {
|
|
13
|
-
const host = config?.host || process.env.OCEAN_SERVER_HOST || DEFAULT_HOST;
|
|
14
|
-
const port = config?.port || (process.env.OCEAN_SERVER_PORT ? parseInt(process.env.OCEAN_SERVER_PORT) : DEFAULT_PORT);
|
|
15
|
-
return `http://${host}:${port}`;
|
|
16
|
-
}
|
|
17
|
-
async function isServerHealthy(baseUrl) {
|
|
18
|
-
try {
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const timeout = setTimeout(() => controller.abort(), 1e3);
|
|
21
|
-
const res = await fetch(`${baseUrl}/global/health`, {
|
|
22
|
-
signal: controller.signal
|
|
23
|
-
});
|
|
24
|
-
clearTimeout(timeout);
|
|
25
|
-
if (!res.ok) return false;
|
|
26
|
-
const data = await res.json();
|
|
27
|
-
return Boolean(data.healthy);
|
|
28
|
-
} catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
var AGENT_SYSTEM_PROMPT = `You are OceanCode, an elite, autonomous AI software engineer designed to master large, complex codebases and write production-quality code without hallucination or truncation.
|
|
33
|
-
|
|
34
|
-
### CRITICAL PRINCIPLES FOR LARGE CODEBASES:
|
|
35
|
-
1. EXPLORE BEFORE ASSUMING:
|
|
36
|
-
- Always use \`glob\` or \`grep\` to locate files, symbols, types, and imports before modifying or referencing them.
|
|
37
|
-
- For broad codebase investigation, use the \`task\` tool with \`subagent_type: "explore"\`.
|
|
38
|
-
- Never assume an import, function, or class exists without verifying where it is defined.
|
|
39
|
-
|
|
40
|
-
2. PRECISE FILE READING:
|
|
41
|
-
- For large files (>200 lines), use the \`read\` tool with \`offset\` and \`limit\` to inspect the exact sections you need.
|
|
42
|
-
- Never skim or guess file contents.
|
|
43
|
-
|
|
44
|
-
3. CLEAN, SURGICAL FILE MODIFICATION:
|
|
45
|
-
- NEVER overwrite a large existing file with \`write\` when making targeted changes.
|
|
46
|
-
- ALWAYS read the file first, then use \`edit\` with the exact unique \`oldString\` and updated \`newString\`.
|
|
47
|
-
- Ensure \`oldString\` contains enough surrounding context lines (3-5 lines) to be completely unique in the file.
|
|
48
|
-
|
|
49
|
-
4. COMPLETE, PRODUCTION-READY WRITING (ZERO HALLUCINATIONS):
|
|
50
|
-
- When using \`write\` to create new files or rewrite small files, ALWAYS output complete, functional, working code.
|
|
51
|
-
- NEVER output placeholders, ellipsis comments, or abbreviations like:
|
|
52
|
-
// ... rest of code unchanged ...
|
|
53
|
-
// TODO: implement later
|
|
54
|
-
/* existing code goes here */
|
|
55
|
-
- Every file written MUST be syntactically valid and completely filled in.
|
|
56
|
-
|
|
57
|
-
5. VERIFICATION & RECOVERY:
|
|
58
|
-
- After making changes, use \`bash\` to run typechecks (\`tsc --noEmit\`), build steps (\`npm run build\`), or tests (\`npm test\`).
|
|
59
|
-
6. READABLE, STRUCTURED & ENGAGING RESPONSES (NO WALLS OF TEXT):
|
|
60
|
-
- NEVER output dense, unbroken walls of text or rambling paragraphs.
|
|
61
|
-
- ALWAYS format your responses with high visual structure:
|
|
62
|
-
\u2022 \u{1F3AF} Bullet Points with Emojis: Use clean bullet points prefixed with contextual emojis (\u{1F4C1}, \u{1F527}, \u{1F4A1}, \u26A1, \u{1F680}, \u26A0\uFE0F, \u2705, \u{1F4CC}, \u{1F3AF}, \u{1F50D}) so key takeaways scan instantly.
|
|
63
|
-
\u2022 \u{1F522} Numbered Steps: Use numbered lists (1., 2., 3.) for chronological execution steps, install steps, or procedures.
|
|
64
|
-
\u2022 \u{1F524} Bold & Code Highlights: Bold key concepts (**keyword**, **feature**) and use inline code (\`path/file.ts\`, \`function()\`) to clearly separate technical terms from explanation.
|
|
65
|
-
\u2022 \u{1F4BB} Fenced Code Blocks: Always specify syntax language for code snippets (\`\`\`typescript, \`\`\`bash, etc.).
|
|
66
|
-
\u2022 \u2702\uFE0F Crisp Executive Summaries: Start with a brief, punchy overview, present the structured content (bullets), and finish with clear next steps.`;
|
|
67
|
-
function setupServerConfig(oceanConfigDir) {
|
|
68
|
-
const opencodeConfigDir = path.join(oceanConfigDir, "opencode");
|
|
69
|
-
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
70
|
-
const configPath = path.join(opencodeConfigDir, "opencode.json");
|
|
71
|
-
let existing = {};
|
|
72
|
-
if (fs.existsSync(configPath)) {
|
|
73
|
-
try {
|
|
74
|
-
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
75
|
-
} catch {
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const permissions = {
|
|
79
|
-
"*": "allow",
|
|
80
|
-
read: "allow",
|
|
81
|
-
write: "allow",
|
|
82
|
-
edit: "allow",
|
|
83
|
-
glob: "allow",
|
|
84
|
-
grep: "allow",
|
|
85
|
-
list: "allow",
|
|
86
|
-
bash: "allow",
|
|
87
|
-
task: "allow",
|
|
88
|
-
external_directory: "allow",
|
|
89
|
-
todowrite: "allow",
|
|
90
|
-
doom_loop: "allow",
|
|
91
|
-
skill: "allow",
|
|
92
|
-
webfetch: "allow",
|
|
93
|
-
websearch: "allow",
|
|
94
|
-
question: "allow"
|
|
95
|
-
};
|
|
96
|
-
const merged = {
|
|
97
|
-
$schema: "https://opencode.ai/config.json",
|
|
98
|
-
...existing,
|
|
99
|
-
permission: {
|
|
100
|
-
...permissions,
|
|
101
|
-
...existing.permission || {}
|
|
102
|
-
},
|
|
103
|
-
agent: {
|
|
104
|
-
...existing.agent || {},
|
|
105
|
-
build: {
|
|
106
|
-
...existing.agent?.build || {},
|
|
107
|
-
prompt: AGENT_SYSTEM_PROMPT,
|
|
108
|
-
permission: permissions
|
|
109
|
-
},
|
|
110
|
-
plan: {
|
|
111
|
-
...existing.agent?.plan || {},
|
|
112
|
-
prompt: AGENT_SYSTEM_PROMPT,
|
|
113
|
-
permission: {
|
|
114
|
-
...permissions,
|
|
115
|
-
edit: "deny",
|
|
116
|
-
write: "deny"
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
122
|
-
const projectConfigPath = path.join(process.cwd(), "opencode.json");
|
|
123
|
-
if (fs.existsSync(projectConfigPath)) {
|
|
124
|
-
try {
|
|
125
|
-
const projExisting = JSON.parse(fs.readFileSync(projectConfigPath, "utf-8"));
|
|
126
|
-
const projMerged = {
|
|
127
|
-
...projExisting,
|
|
128
|
-
agent: {
|
|
129
|
-
...projExisting.agent || {},
|
|
130
|
-
build: {
|
|
131
|
-
...projExisting.agent?.build || {},
|
|
132
|
-
prompt: AGENT_SYSTEM_PROMPT,
|
|
133
|
-
permission: permissions
|
|
134
|
-
},
|
|
135
|
-
plan: {
|
|
136
|
-
...projExisting.agent?.plan || {},
|
|
137
|
-
prompt: AGENT_SYSTEM_PROMPT,
|
|
138
|
-
permission: {
|
|
139
|
-
...permissions,
|
|
140
|
-
edit: "deny",
|
|
141
|
-
write: "deny"
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
fs.writeFileSync(projectConfigPath, JSON.stringify(projMerged, null, 2), "utf-8");
|
|
147
|
-
} catch {
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
async function ensureServer(config) {
|
|
152
|
-
const oceanConfigDir = path.join(os.homedir(), ".config", "oceancode");
|
|
153
|
-
setupServerConfig(oceanConfigDir);
|
|
154
|
-
const baseUrl = getBaseUrl(config);
|
|
155
|
-
const healthy = await isServerHealthy(baseUrl);
|
|
156
|
-
if (healthy) {
|
|
157
|
-
return baseUrl;
|
|
158
|
-
}
|
|
159
|
-
const port = config?.port || (process.env.OCEAN_SERVER_PORT ? parseInt(process.env.OCEAN_SERVER_PORT) : DEFAULT_PORT);
|
|
160
|
-
const host = config?.host || process.env.OCEAN_SERVER_HOST || DEFAULT_HOST;
|
|
161
|
-
const oceanDataDir = path.join(os.homedir(), ".local", "share", "oceancode");
|
|
162
|
-
const oceanStateDir = path.join(os.homedir(), ".local", "state", "oceancode");
|
|
163
|
-
fs.mkdirSync(oceanDataDir, { recursive: true });
|
|
164
|
-
fs.mkdirSync(oceanConfigDir, { recursive: true });
|
|
165
|
-
fs.mkdirSync(oceanStateDir, { recursive: true });
|
|
166
|
-
const env = {
|
|
167
|
-
...process.env,
|
|
168
|
-
XDG_DATA_HOME: oceanDataDir,
|
|
169
|
-
XDG_CONFIG_HOME: oceanConfigDir,
|
|
170
|
-
XDG_STATE_HOME: oceanStateDir
|
|
171
|
-
};
|
|
172
|
-
try {
|
|
173
|
-
const child = spawn(`opencode serve --port ${port} --hostname ${host}`, {
|
|
174
|
-
shell: true,
|
|
175
|
-
windowsHide: true,
|
|
176
|
-
stdio: "ignore",
|
|
177
|
-
cwd: process.cwd(),
|
|
178
|
-
env
|
|
179
|
-
});
|
|
180
|
-
managedProcess = child;
|
|
181
|
-
process.on("exit", () => {
|
|
182
|
-
try {
|
|
183
|
-
if (managedProcess && !managedProcess.killed) {
|
|
184
|
-
managedProcess.kill();
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
const startTime = Date.now();
|
|
190
|
-
const MAX_WAIT_MS = 15e3;
|
|
191
|
-
while (Date.now() - startTime < MAX_WAIT_MS) {
|
|
192
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
193
|
-
if (await isServerHealthy(baseUrl)) {
|
|
194
|
-
return baseUrl;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
throw new Error(`Ocean backend failed to respond on ${baseUrl}.`);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// src/server/client.ts
|
|
204
|
-
import http from "http";
|
|
205
|
-
var OceanClient = class {
|
|
206
|
-
baseUrl;
|
|
207
|
-
constructor(baseUrl) {
|
|
208
|
-
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
209
|
-
}
|
|
210
|
-
async getHealth() {
|
|
211
|
-
const res = await fetch(`${this.baseUrl}/global/health`);
|
|
212
|
-
return await res.json();
|
|
213
|
-
}
|
|
214
|
-
async getProviders() {
|
|
215
|
-
const res = await fetch(`${this.baseUrl}/provider`);
|
|
216
|
-
if (!res.ok) throw new Error(`Failed to fetch providers: ${res.statusText}`);
|
|
217
|
-
const data = await res.json();
|
|
218
|
-
return data.all || [];
|
|
219
|
-
}
|
|
220
|
-
async getModels() {
|
|
221
|
-
const providers = await this.getProviders();
|
|
222
|
-
const list = [];
|
|
223
|
-
for (const p of providers) {
|
|
224
|
-
if (!p.models) continue;
|
|
225
|
-
for (const [modelKey, modelData] of Object.entries(p.models)) {
|
|
226
|
-
const isFree = p.id === "opencode" || modelKey.includes("free") || modelData.cost?.input === 0;
|
|
227
|
-
list.push({
|
|
228
|
-
id: modelData.id || modelKey,
|
|
229
|
-
providerID: p.id,
|
|
230
|
-
name: modelData.name || modelKey,
|
|
231
|
-
isFree
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return list;
|
|
236
|
-
}
|
|
237
|
-
async createSession(title = "Ocean Session", directory = process.cwd()) {
|
|
238
|
-
const url = new URL(`${this.baseUrl}/session`);
|
|
239
|
-
url.searchParams.set("directory", directory);
|
|
240
|
-
const res = await fetch(url.toString(), {
|
|
241
|
-
method: "POST",
|
|
242
|
-
headers: {
|
|
243
|
-
"Content-Type": "application/json",
|
|
244
|
-
"x-opencode-directory": directory
|
|
245
|
-
},
|
|
246
|
-
body: JSON.stringify({ title, directory })
|
|
247
|
-
});
|
|
248
|
-
if (!res.ok) {
|
|
249
|
-
throw new Error(`Failed to create session: ${res.statusText}`);
|
|
250
|
-
}
|
|
251
|
-
return await res.json();
|
|
252
|
-
}
|
|
253
|
-
async getSessionDiff(sessionID) {
|
|
254
|
-
try {
|
|
255
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/diff`);
|
|
256
|
-
if (!res.ok) return [];
|
|
257
|
-
return await res.json();
|
|
258
|
-
} catch {
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
async revertSession(sessionID, messageID) {
|
|
263
|
-
try {
|
|
264
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/revert`, {
|
|
265
|
-
method: "POST",
|
|
266
|
-
headers: { "Content-Type": "application/json" },
|
|
267
|
-
body: JSON.stringify({ messageID })
|
|
268
|
-
});
|
|
269
|
-
return res.ok;
|
|
270
|
-
} catch {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async summarizeSession(sessionID) {
|
|
275
|
-
try {
|
|
276
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/summarize`, {
|
|
277
|
-
method: "POST",
|
|
278
|
-
headers: { "Content-Type": "application/json" }
|
|
279
|
-
});
|
|
280
|
-
return res.ok;
|
|
281
|
-
} catch {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async listSessions(limit = 20) {
|
|
286
|
-
const res = await fetch(`${this.baseUrl}/session?limit=${limit}`);
|
|
287
|
-
if (!res.ok) throw new Error(`Failed to list sessions: ${res.statusText}`);
|
|
288
|
-
return await res.json();
|
|
289
|
-
}
|
|
290
|
-
async getSessionMessages(sessionID) {
|
|
291
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/message`);
|
|
292
|
-
if (!res.ok) throw new Error(`Failed to get session messages: ${res.statusText}`);
|
|
293
|
-
return await res.json();
|
|
294
|
-
}
|
|
295
|
-
async promptSession(sessionID, text, options) {
|
|
296
|
-
let providerID = "opencode";
|
|
297
|
-
let modelID = "big-pickle";
|
|
298
|
-
if (options?.model) {
|
|
299
|
-
if (options.model.includes("/")) {
|
|
300
|
-
const parts = options.model.split("/");
|
|
301
|
-
providerID = parts[0];
|
|
302
|
-
modelID = parts.slice(1).join("/");
|
|
303
|
-
} else {
|
|
304
|
-
modelID = options.model;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const payload = {
|
|
308
|
-
parts: [{ type: "text", text }],
|
|
309
|
-
model: {
|
|
310
|
-
providerID,
|
|
311
|
-
modelID
|
|
312
|
-
},
|
|
313
|
-
agent: options?.agent || "build"
|
|
314
|
-
};
|
|
315
|
-
const dir = options?.directory || process.cwd();
|
|
316
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/prompt_async`, {
|
|
317
|
-
method: "POST",
|
|
318
|
-
headers: {
|
|
319
|
-
"Content-Type": "application/json",
|
|
320
|
-
"x-opencode-directory": dir
|
|
321
|
-
},
|
|
322
|
-
body: JSON.stringify(payload)
|
|
323
|
-
});
|
|
324
|
-
if (!res.ok) {
|
|
325
|
-
const errText = await res.text();
|
|
326
|
-
throw new Error(`Prompt error (${res.status}): ${errText}`);
|
|
327
|
-
}
|
|
328
|
-
const resText = await res.text();
|
|
329
|
-
return resText ? JSON.parse(resText) : { success: true };
|
|
330
|
-
}
|
|
331
|
-
async replyPermission(requestID, reply = "always") {
|
|
332
|
-
const res = await fetch(`${this.baseUrl}/permission/${requestID}/reply`, {
|
|
333
|
-
method: "POST",
|
|
334
|
-
headers: { "Content-Type": "application/json" },
|
|
335
|
-
body: JSON.stringify({ reply })
|
|
336
|
-
});
|
|
337
|
-
return res.ok;
|
|
338
|
-
}
|
|
339
|
-
async replyQuestion(requestID, answers) {
|
|
340
|
-
const res = await fetch(`${this.baseUrl}/question/${requestID}/reply`, {
|
|
341
|
-
method: "POST",
|
|
342
|
-
headers: { "Content-Type": "application/json" },
|
|
343
|
-
body: JSON.stringify({ answers })
|
|
344
|
-
});
|
|
345
|
-
return res.ok;
|
|
346
|
-
}
|
|
347
|
-
async abortSession(sessionID) {
|
|
348
|
-
const res = await fetch(`${this.baseUrl}/session/${sessionID}/abort`, {
|
|
349
|
-
method: "POST"
|
|
350
|
-
});
|
|
351
|
-
return res.ok;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Subscribe to server SSE stream. Returns an unsubscribe function.
|
|
355
|
-
*/
|
|
356
|
-
subscribeEvents(onEvent, onError, directory = process.cwd()) {
|
|
357
|
-
const url = new URL(`${this.baseUrl}/event`);
|
|
358
|
-
if (directory) {
|
|
359
|
-
url.searchParams.set("directory", directory);
|
|
360
|
-
}
|
|
361
|
-
let isCancelled = false;
|
|
362
|
-
let currentRes = null;
|
|
363
|
-
const req = http.request(
|
|
364
|
-
{
|
|
365
|
-
hostname: url.hostname,
|
|
366
|
-
port: url.port,
|
|
367
|
-
path: url.pathname + url.search,
|
|
368
|
-
headers: { Accept: "text/event-stream" }
|
|
369
|
-
},
|
|
370
|
-
(res) => {
|
|
371
|
-
currentRes = res;
|
|
372
|
-
let buffer = "";
|
|
373
|
-
res.on("data", (chunk) => {
|
|
374
|
-
if (isCancelled) return;
|
|
375
|
-
buffer += chunk.toString("utf-8");
|
|
376
|
-
const lines = buffer.split("\n");
|
|
377
|
-
buffer = lines.pop() || "";
|
|
378
|
-
for (const line of lines) {
|
|
379
|
-
const trimmed = line.trim();
|
|
380
|
-
if (trimmed.startsWith("data: ")) {
|
|
381
|
-
const dataStr = trimmed.slice(6);
|
|
382
|
-
try {
|
|
383
|
-
const parsed = JSON.parse(dataStr);
|
|
384
|
-
onEvent(parsed);
|
|
385
|
-
} catch {
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
res.on("error", (err) => {
|
|
391
|
-
if (!isCancelled && onError) onError(err);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
);
|
|
395
|
-
req.on("error", (err) => {
|
|
396
|
-
if (!isCancelled && onError) onError(err);
|
|
397
|
-
});
|
|
398
|
-
req.end();
|
|
399
|
-
return () => {
|
|
400
|
-
isCancelled = true;
|
|
401
|
-
if (currentRes) {
|
|
402
|
-
currentRes.destroy();
|
|
403
|
-
currentRes = null;
|
|
404
|
-
}
|
|
405
|
-
req.destroy();
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
// src/ui/renderer.ts
|
|
411
|
-
import chalk2 from "chalk";
|
|
412
|
-
|
|
413
|
-
// src/ui/layout.ts
|
|
414
|
-
import chalk from "chalk";
|
|
415
|
-
var INDENT = " ";
|
|
416
|
-
var OCEAN_BLUE = "#0088ff";
|
|
417
|
-
var OCEAN_CYAN = "#38bdf8";
|
|
418
|
-
function getLayoutDimensions() {
|
|
419
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
420
|
-
return { terminalWidth, contentWidth: 80, leftMargin: 2 };
|
|
421
|
-
}
|
|
422
|
-
function getBigLogo() {
|
|
423
|
-
const line0 = "\u2800" + " ".repeat(37) + "\u2584";
|
|
424
|
-
const line1 = "\u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2580 \u2588\u2580\u2580\u2588 \u2584\u2580\u2580\u2588 \u2588\u2580\u2580\u2584 \u2588\u2580\u2580\u2580 \u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2588";
|
|
425
|
-
const line2 = "\u2588 \u2588 \u2588 \u2588\u2580\u2580\u2580 \u2588\u2584\u2584\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2580\u2580";
|
|
426
|
-
const line3 = "\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580 \u2580 \u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580";
|
|
427
|
-
return [
|
|
428
|
-
chalk.hex("#60a5fa")(line0),
|
|
429
|
-
chalk.hex(OCEAN_CYAN).bold(line1),
|
|
430
|
-
chalk.hex(OCEAN_BLUE).bold(line2),
|
|
431
|
-
chalk.hex("#0055b3")(line3)
|
|
432
|
-
].join("\n");
|
|
433
|
-
}
|
|
434
|
-
function renderBigLogo(indent = INDENT) {
|
|
435
|
-
const line0 = "\u2800" + " ".repeat(37) + "\u2584";
|
|
436
|
-
const line1 = "\u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2580 \u2588\u2580\u2580\u2588 \u2584\u2580\u2580\u2588 \u2588\u2580\u2580\u2584 \u2588\u2580\u2580\u2580 \u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2588 \u2588\u2580\u2580\u2588";
|
|
437
|
-
const line2 = "\u2588 \u2588 \u2588 \u2588\u2580\u2580\u2580 \u2588\u2584\u2584\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2580\u2580";
|
|
438
|
-
const line3 = "\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580 \u2580 \u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580";
|
|
439
|
-
console.log("");
|
|
440
|
-
console.log(indent + chalk.hex("#60a5fa")(line0));
|
|
441
|
-
console.log(indent + chalk.hex(OCEAN_CYAN).bold(line1));
|
|
442
|
-
console.log(indent + chalk.hex(OCEAN_BLUE).bold(line2));
|
|
443
|
-
console.log(indent + chalk.hex("#0055b3")(line3));
|
|
444
|
-
}
|
|
445
|
-
function renderTopBar(modelName, cwd = process.cwd()) {
|
|
446
|
-
const width = Math.max(60, process.stdout.columns || 80);
|
|
447
|
-
const folderStr = cwd.length > 35 ? "..." + cwd.slice(-32) : cwd;
|
|
448
|
-
renderBigLogo(INDENT);
|
|
449
|
-
console.log("");
|
|
450
|
-
const leftText = folderStr;
|
|
451
|
-
const rightText = modelName + " /help";
|
|
452
|
-
const spaces = Math.max(2, width - (leftText.length + rightText.length + 6));
|
|
453
|
-
console.log(
|
|
454
|
-
INDENT + chalk.dim(leftText) + " ".repeat(spaces) + chalk.hex(OCEAN_BLUE).bold(modelName) + " " + chalk.dim("/help")
|
|
455
|
-
);
|
|
456
|
-
console.log(INDENT + chalk.dim("\u2500".repeat(Math.max(40, width - 4))));
|
|
457
|
-
console.log("");
|
|
458
|
-
}
|
|
459
|
-
function renderUserMessageCard(text, _modelName) {
|
|
460
|
-
const width = Math.min(80, (process.stdout.columns || 80) - 4);
|
|
461
|
-
const bar = chalk.hex(OCEAN_BLUE)("\u258C");
|
|
462
|
-
const bg = chalk.bgHex("#161b22");
|
|
463
|
-
const pad = Math.max(2, width - text.length - 3);
|
|
464
|
-
console.log(bar + bg(" " + text + " ".repeat(pad)));
|
|
465
|
-
}
|
|
466
|
-
function renderFooter(modelName, durationSec, agent = "Build") {
|
|
467
|
-
const icon = chalk.hex(OCEAN_BLUE)("\u25A3");
|
|
468
|
-
const agentLabel = chalk.bold.white(agent);
|
|
469
|
-
const dot = chalk.dim(" \xB7 ");
|
|
470
|
-
const modelLabel = chalk.hex("#94a3b8")(modelName);
|
|
471
|
-
const timeLabel = chalk.dim(`${durationSec.toFixed(1)}s`);
|
|
472
|
-
console.log(`
|
|
473
|
-
|
|
474
|
-
${INDENT}${icon} ${agentLabel}${dot}${modelLabel}${dot}${timeLabel}
|
|
475
|
-
`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// src/ui/toolLabels.ts
|
|
479
|
-
var TOOL_LABEL_MAP = {
|
|
480
|
-
// ── Filesystem ─────────────────────────────────────────────────────────────
|
|
481
|
-
filesystem_read_text_file: { icon: "\u{1F4D6}", label: "Reading file" },
|
|
482
|
-
filesystem_read_file: { icon: "\u{1F4D6}", label: "Reading file" },
|
|
483
|
-
read_file: { icon: "\u{1F4D6}", label: "Reading file" },
|
|
484
|
-
read: { icon: "\u{1F4D6}", label: "Reading file" },
|
|
485
|
-
filesystem_write_file: { icon: "\u270F\uFE0F ", label: "Writing file" },
|
|
486
|
-
filesystem_write_text_file: { icon: "\u270F\uFE0F ", label: "Writing file" },
|
|
487
|
-
write_file: { icon: "\u270F\uFE0F ", label: "Writing file" },
|
|
488
|
-
write: { icon: "\u270F\uFE0F ", label: "Writing file" },
|
|
489
|
-
filesystem_edit_file: { icon: "\u{1F527}", label: "Editing file" },
|
|
490
|
-
edit_file: { icon: "\u{1F527}", label: "Editing file" },
|
|
491
|
-
edit: { icon: "\u{1F527}", label: "Editing file" },
|
|
492
|
-
patch: { icon: "\u{1F527}", label: "Patching file" },
|
|
493
|
-
filesystem_list_directory: { icon: "\u{1F4C1}", label: "Listing directory" },
|
|
494
|
-
filesystem_list_allowed_directories: { icon: "\u{1F4C1}", label: "Listing allowed directories" },
|
|
495
|
-
list_directory: { icon: "\u{1F4C1}", label: "Listing directory" },
|
|
496
|
-
ls: { icon: "\u{1F4C1}", label: "Listing directory" },
|
|
497
|
-
filesystem_create_directory: { icon: "\u{1F4C2}", label: "Creating directory" },
|
|
498
|
-
create_directory: { icon: "\u{1F4C2}", label: "Creating directory" },
|
|
499
|
-
filesystem_move_file: { icon: "\u{1F4E6}", label: "Moving file" },
|
|
500
|
-
filesystem_copy_file: { icon: "\u{1F4CB}", label: "Copying file" },
|
|
501
|
-
filesystem_delete_file: { icon: "\u{1F5D1}\uFE0F ", label: "Deleting file" },
|
|
502
|
-
filesystem_rename_file: { icon: "\u{1F3F7}\uFE0F ", label: "Renaming file" },
|
|
503
|
-
filesystem_search_files: { icon: "\u{1F50D}", label: "Searching files" },
|
|
504
|
-
filesystem_find_files: { icon: "\u{1F50D}", label: "Finding files" },
|
|
505
|
-
filesystem_get_file_info: { icon: "\u2139\uFE0F ", label: "Getting file info" },
|
|
506
|
-
filesystem_stat: { icon: "\u2139\uFE0F ", label: "Getting file info" },
|
|
507
|
-
// ── Shell / Bash ───────────────────────────────────────────────────────────
|
|
508
|
-
bash: { icon: "\u26A1", label: "Running command" },
|
|
509
|
-
shell: { icon: "\u26A1", label: "Running command" },
|
|
510
|
-
run_command: { icon: "\u26A1", label: "Running command" },
|
|
511
|
-
exec: { icon: "\u26A1", label: "Running command" },
|
|
512
|
-
execute: { icon: "\u26A1", label: "Running command" },
|
|
513
|
-
run_terminal_cmd: { icon: "\u26A1", label: "Running command" },
|
|
514
|
-
// ── Search / Grep ──────────────────────────────────────────────────────────
|
|
515
|
-
grep: { icon: "\u{1F50E}", label: "Searching code" },
|
|
516
|
-
grep_search: { icon: "\u{1F50E}", label: "Searching code" },
|
|
517
|
-
ripgrep: { icon: "\u{1F50E}", label: "Searching code" },
|
|
518
|
-
glob: { icon: "\u{1F50D}", label: "Globbing files" },
|
|
519
|
-
find: { icon: "\u{1F50D}", label: "Finding files" },
|
|
520
|
-
// ── Web / HTTP ─────────────────────────────────────────────────────────────
|
|
521
|
-
web_search: { icon: "\u{1F310}", label: "Searching web" },
|
|
522
|
-
search_web: { icon: "\u{1F310}", label: "Searching web" },
|
|
523
|
-
browser: { icon: "\u{1F310}", label: "Opening browser" },
|
|
524
|
-
fetch: { icon: "\u{1F310}", label: "Fetching URL" },
|
|
525
|
-
http_request: { icon: "\u{1F310}", label: "HTTP request" },
|
|
526
|
-
curl: { icon: "\u{1F310}", label: "Fetching URL" },
|
|
527
|
-
// ── Todo / Task management ─────────────────────────────────────────────────
|
|
528
|
-
todowrite: { icon: "\u{1F4DD}", label: "Updating todo list" },
|
|
529
|
-
todo_write: { icon: "\u{1F4DD}", label: "Updating todo list" },
|
|
530
|
-
todoread: { icon: "\u{1F4CB}", label: "Reading todo list" },
|
|
531
|
-
todo_read: { icon: "\u{1F4CB}", label: "Reading todo list" },
|
|
532
|
-
// ── Subagent / Task ────────────────────────────────────────────────────────
|
|
533
|
-
task: { icon: "\u{1F916}", label: "Spawning subagent" },
|
|
534
|
-
spawn_agent: { icon: "\u{1F916}", label: "Spawning subagent" },
|
|
535
|
-
agent: { icon: "\u{1F916}", label: "Running agent" },
|
|
536
|
-
// ── Git ────────────────────────────────────────────────────────────────────
|
|
537
|
-
git_status: { icon: "\u{1F500}", label: "Git status" },
|
|
538
|
-
git_diff: { icon: "\u{1F500}", label: "Git diff" },
|
|
539
|
-
git_commit: { icon: "\u{1F500}", label: "Git commit" },
|
|
540
|
-
git_log: { icon: "\u{1F500}", label: "Git log" },
|
|
541
|
-
git: { icon: "\u{1F500}", label: "Git" },
|
|
542
|
-
// ── AI / Model ─────────────────────────────────────────────────────────────
|
|
543
|
-
openai: { icon: "\u{1F9E0}", label: "Calling model" },
|
|
544
|
-
anthropic: { icon: "\u{1F9E0}", label: "Calling model" },
|
|
545
|
-
gemini: { icon: "\u{1F9E0}", label: "Calling model" }
|
|
546
|
-
};
|
|
547
|
-
function getFriendlyToolLabel(rawName) {
|
|
548
|
-
const exact = TOOL_LABEL_MAP[rawName];
|
|
549
|
-
if (exact) return exact;
|
|
550
|
-
for (const [key, val] of Object.entries(TOOL_LABEL_MAP)) {
|
|
551
|
-
if (rawName.startsWith(key) || key.startsWith(rawName)) return val;
|
|
552
|
-
}
|
|
553
|
-
const clean = rawName.replace(/^(filesystem_|mcp_)/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
554
|
-
return { icon: "\u{1F527}", label: clean };
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// src/ui/renderer.ts
|
|
558
|
-
var BOTTOM_BAR_ROWS = 3;
|
|
559
|
-
var StreamRenderer = class {
|
|
560
|
-
reasoningStarted = false;
|
|
561
|
-
textStarted = false;
|
|
562
|
-
currentReasoningLength = 0;
|
|
563
|
-
currentTextLength = 0;
|
|
564
|
-
startTime = 0;
|
|
565
|
-
modelName = "Big Pickle";
|
|
566
|
-
toolsUsed = [];
|
|
567
|
-
thinkingTimer = null;
|
|
568
|
-
thinkingFrame = 0;
|
|
569
|
-
/** Injected by interactive.ts — repaints the full bottom bar in place */
|
|
570
|
-
pinBarFn = null;
|
|
571
|
-
/** How many rows the bar currently occupies (updated by interactive.ts) */
|
|
572
|
-
barRows = BOTTOM_BAR_ROWS;
|
|
573
|
-
/** Dedupe tracker: OpenCode emits pending → running → completed updates per tool part */
|
|
574
|
-
seenTools = /* @__PURE__ */ new Map();
|
|
575
|
-
/** Per-part streamed text length — prevents duplicate/lost output across parts */
|
|
576
|
-
textLens = /* @__PURE__ */ new Map();
|
|
577
|
-
/** Throttle for bottom-bar repaints during streaming */
|
|
578
|
-
lastRepinAt = 0;
|
|
579
|
-
/** Call this from interactive.ts right after constructing StreamRenderer */
|
|
580
|
-
setPinBar(fn, rows) {
|
|
581
|
-
this.pinBarFn = fn;
|
|
582
|
-
const origFn = fn;
|
|
583
|
-
this.pinBarFn = () => {
|
|
584
|
-
this.barRows = rows();
|
|
585
|
-
origFn();
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
// ── With scroll regions, bar rows are scroll-protected — no erasing needed ──
|
|
589
|
-
// Output always goes inside the scroll region; bar rows are untouched.
|
|
590
|
-
eraseBar() {
|
|
591
|
-
}
|
|
592
|
-
/** Repaint the bar after a chunk of output (forced — for tool/finish boundaries) */
|
|
593
|
-
repinBar() {
|
|
594
|
-
if (!this.pinBarFn) return;
|
|
595
|
-
this.lastRepinAt = Date.now();
|
|
596
|
-
this.pinBarFn();
|
|
597
|
-
}
|
|
598
|
-
/** Throttled repaint for hot paths (per-token streaming). Max ~4 repaints/sec. */
|
|
599
|
-
repinBarThrottled() {
|
|
600
|
-
if (!this.pinBarFn) return;
|
|
601
|
-
const now = Date.now();
|
|
602
|
-
if (now - this.lastRepinAt < 250) return;
|
|
603
|
-
this.lastRepinAt = now;
|
|
604
|
-
this.pinBarFn();
|
|
605
|
-
}
|
|
606
|
-
/** Collapse a tool detail to a single short line so long commands don't scroll */
|
|
607
|
-
formatDetail(detail) {
|
|
608
|
-
if (detail === void 0 || detail === null) return "";
|
|
609
|
-
const raw = Array.isArray(detail) ? detail.join(" ") : String(detail);
|
|
610
|
-
const oneLine = raw.replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
611
|
-
const MAX = 100;
|
|
612
|
-
return oneLine.length > MAX ? oneLine.slice(0, MAX - 1) + "\u2026" : oneLine;
|
|
613
|
-
}
|
|
614
|
-
// ── Thinking animation ───────────────────────────────────────────────────────
|
|
615
|
-
start(modelName = "Big Pickle") {
|
|
616
|
-
this.stopThinkingAnimation();
|
|
617
|
-
this.modelName = modelName;
|
|
618
|
-
this.startTime = Date.now();
|
|
619
|
-
this.reasoningStarted = false;
|
|
620
|
-
this.textStarted = false;
|
|
621
|
-
this.currentReasoningLength = 0;
|
|
622
|
-
this.currentTextLength = 0;
|
|
623
|
-
this.toolsUsed = [];
|
|
624
|
-
this.seenTools.clear();
|
|
625
|
-
this.textLens.clear();
|
|
626
|
-
this.lastRepinAt = 0;
|
|
627
|
-
this.startThinkingAnimation();
|
|
628
|
-
}
|
|
629
|
-
startThinkingAnimation() {
|
|
630
|
-
if (this.thinkingTimer) return;
|
|
631
|
-
const frames = [
|
|
632
|
-
chalk2.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
633
|
-
chalk2.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
634
|
-
chalk2.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
635
|
-
chalk2.hex("#fef08a").bold(" \u224B Thinking..."),
|
|
636
|
-
chalk2.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
637
|
-
chalk2.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
638
|
-
chalk2.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
639
|
-
chalk2.dim(" \u224B Thinking ")
|
|
640
|
-
];
|
|
641
|
-
const writeFrame = (frame) => {
|
|
642
|
-
process.stdout.write(`\r${frame}\x1B[K`);
|
|
643
|
-
};
|
|
644
|
-
writeFrame(frames[0]);
|
|
645
|
-
this.thinkingTimer = setInterval(() => {
|
|
646
|
-
this.thinkingFrame++;
|
|
647
|
-
writeFrame(frames[this.thinkingFrame % frames.length]);
|
|
648
|
-
}, 180);
|
|
649
|
-
}
|
|
650
|
-
stopThinkingAnimation() {
|
|
651
|
-
if (!this.thinkingTimer) return;
|
|
652
|
-
clearInterval(this.thinkingTimer);
|
|
653
|
-
this.thinkingTimer = null;
|
|
654
|
-
process.stdout.write("\r\x1B[K");
|
|
655
|
-
}
|
|
656
|
-
// ── Public event handlers ────────────────────────────────────────────────────
|
|
657
|
-
handleReasoning(fullText) {
|
|
658
|
-
this.reasoningStarted = true;
|
|
659
|
-
this.currentReasoningLength = fullText.length;
|
|
660
|
-
}
|
|
661
|
-
handleTool(toolName, detail, partId, status) {
|
|
662
|
-
const key = partId ?? `${toolName}::${typeof detail === "string" ? detail : ""}`;
|
|
663
|
-
const prev = this.seenTools.get(key);
|
|
664
|
-
if (prev) {
|
|
665
|
-
if (status && (status === "completed" || status === "error")) this.seenTools.set(key, status);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
this.seenTools.set(key, status ?? "running");
|
|
669
|
-
this.stopThinkingAnimation();
|
|
670
|
-
if (!this.toolsUsed.includes(toolName)) this.toolsUsed.push(toolName);
|
|
671
|
-
this.eraseBar();
|
|
672
|
-
const { icon, label } = getFriendlyToolLabel(toolName);
|
|
673
|
-
const badge = chalk2.bgHex("#3b82f6").black.bold(" TOOL ");
|
|
674
|
-
const action = chalk2.bold.hex("#60a5fa")(`${icon} ${label}`);
|
|
675
|
-
const clean = this.formatDetail(detail);
|
|
676
|
-
const info = clean ? chalk2.dim(` ${clean}`) : "";
|
|
677
|
-
process.stdout.write(`
|
|
678
|
-
${INDENT}${badge} ${action}${info}
|
|
679
|
-
`);
|
|
680
|
-
this.repinBar();
|
|
681
|
-
this.startThinkingAnimation();
|
|
682
|
-
}
|
|
683
|
-
handleDelta(delta, partId = "default") {
|
|
684
|
-
if (!delta) return;
|
|
685
|
-
if (this.thinkingTimer) this.stopThinkingAnimation();
|
|
686
|
-
if (!this.textStarted) {
|
|
687
|
-
this.textStarted = true;
|
|
688
|
-
this.eraseBar();
|
|
689
|
-
if (this.toolsUsed.length > 0) {
|
|
690
|
-
process.stdout.write(
|
|
691
|
-
`${INDENT}${chalk2.green("\u2713")} ${chalk2.dim(`Used tools: ${this.toolsUsed.join(", ")}`)}
|
|
692
|
-
|
|
693
|
-
`
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
this.currentTextLength += delta.length;
|
|
698
|
-
this.textLens.set(partId, (this.textLens.get(partId) ?? 0) + delta.length);
|
|
699
|
-
process.stdout.write(delta);
|
|
700
|
-
this.repinBarThrottled();
|
|
701
|
-
}
|
|
702
|
-
handleText(fullText, partId = "default") {
|
|
703
|
-
if (!this.textStarted) {
|
|
704
|
-
this.textStarted = true;
|
|
705
|
-
this.eraseBar();
|
|
706
|
-
if (this.toolsUsed.length > 0) {
|
|
707
|
-
process.stdout.write(
|
|
708
|
-
`${INDENT}${chalk2.green("\u2713")} ${chalk2.dim(`Used tools: ${this.toolsUsed.join(", ")}`)}
|
|
709
|
-
|
|
710
|
-
`
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const written = this.textLens.get(partId) ?? 0;
|
|
715
|
-
if (fullText.length <= written) return;
|
|
716
|
-
const slice = fullText.slice(written);
|
|
717
|
-
this.textLens.set(partId, fullText.length);
|
|
718
|
-
this.currentTextLength += slice.length;
|
|
719
|
-
process.stdout.write(slice);
|
|
720
|
-
this.repinBarThrottled();
|
|
721
|
-
}
|
|
722
|
-
finish(agent = "Build") {
|
|
723
|
-
this.stopThinkingAnimation();
|
|
724
|
-
const totalSec = Math.max(0.1, (Date.now() - this.startTime) / 1e3);
|
|
725
|
-
process.stdout.write("\n");
|
|
726
|
-
renderFooter(this.modelName, totalSec, agent);
|
|
727
|
-
this.reasoningStarted = false;
|
|
728
|
-
this.textStarted = false;
|
|
729
|
-
this.currentReasoningLength = 0;
|
|
730
|
-
this.currentTextLength = 0;
|
|
731
|
-
this.toolsUsed = [];
|
|
732
|
-
this.seenTools.clear();
|
|
733
|
-
this.textLens.clear();
|
|
734
|
-
this.repinBar();
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
// src/server/auth.ts
|
|
739
|
-
import path2 from "path";
|
|
740
|
-
import os2 from "os";
|
|
741
|
-
import fs2 from "fs";
|
|
742
|
-
var SUPPORTED_BYOK_PROVIDERS = [
|
|
743
|
-
{ id: "openai", name: "OpenAI", envVar: "OPENAI_API_KEY", placeholder: "sk-proj-..." },
|
|
744
|
-
{ id: "anthropic", name: "Anthropic", envVar: "ANTHROPIC_API_KEY", placeholder: "sk-ant-..." },
|
|
745
|
-
{ id: "google", name: "Google Gemini", envVar: "GEMINI_API_KEY", placeholder: "AIzaSy..." },
|
|
746
|
-
{ id: "deepseek", name: "DeepSeek", envVar: "DEEPSEEK_API_KEY", placeholder: "sk-..." },
|
|
747
|
-
{ id: "groq", name: "Groq", envVar: "GROQ_API_KEY", placeholder: "gsk_..." },
|
|
748
|
-
{ id: "openrouter", name: "OpenRouter", envVar: "OPENROUTER_API_KEY", placeholder: "sk-or-..." }
|
|
749
|
-
];
|
|
750
|
-
async function validateProviderKey(providerId, key) {
|
|
751
|
-
const cleanKey = key.trim();
|
|
752
|
-
if (!cleanKey) {
|
|
753
|
-
return { valid: false, message: "API key cannot be empty." };
|
|
754
|
-
}
|
|
755
|
-
const normalized = providerId.toLowerCase().trim();
|
|
756
|
-
const controller = new AbortController();
|
|
757
|
-
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
758
|
-
try {
|
|
759
|
-
let res;
|
|
760
|
-
if (normalized === "openai") {
|
|
761
|
-
res = await fetch("https://api.openai.com/v1/models", {
|
|
762
|
-
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
763
|
-
signal: controller.signal
|
|
764
|
-
});
|
|
765
|
-
} else if (normalized === "anthropic") {
|
|
766
|
-
res = await fetch("https://api.anthropic.com/v1/models", {
|
|
767
|
-
headers: {
|
|
768
|
-
"x-api-key": cleanKey,
|
|
769
|
-
"anthropic-version": "2023-06-01"
|
|
770
|
-
},
|
|
771
|
-
signal: controller.signal
|
|
772
|
-
});
|
|
773
|
-
} else if (normalized === "google" || normalized === "gemini") {
|
|
774
|
-
res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(cleanKey)}`, {
|
|
775
|
-
signal: controller.signal
|
|
776
|
-
});
|
|
777
|
-
} else if (normalized === "deepseek") {
|
|
778
|
-
res = await fetch("https://api.deepseek.com/models", {
|
|
779
|
-
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
780
|
-
signal: controller.signal
|
|
781
|
-
});
|
|
782
|
-
} else if (normalized === "groq") {
|
|
783
|
-
res = await fetch("https://api.groq.com/openai/v1/models", {
|
|
784
|
-
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
785
|
-
signal: controller.signal
|
|
786
|
-
});
|
|
787
|
-
} else if (normalized === "openrouter") {
|
|
788
|
-
res = await fetch("https://openrouter.ai/api/v1/auth/key", {
|
|
789
|
-
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
790
|
-
signal: controller.signal
|
|
791
|
-
});
|
|
792
|
-
} else {
|
|
793
|
-
clearTimeout(timeout);
|
|
794
|
-
return { valid: false, message: `Unknown provider: ${providerId}` };
|
|
795
|
-
}
|
|
796
|
-
clearTimeout(timeout);
|
|
797
|
-
if (res.ok) {
|
|
798
|
-
return { valid: true, message: "API key is valid and connected." };
|
|
799
|
-
}
|
|
800
|
-
let detail = "";
|
|
801
|
-
try {
|
|
802
|
-
const data = await res.json();
|
|
803
|
-
detail = data.error?.message || data.message || `HTTP ${res.status}`;
|
|
804
|
-
} catch {
|
|
805
|
-
detail = `HTTP status ${res.status}`;
|
|
806
|
-
}
|
|
807
|
-
return { valid: false, message: `Rejected by ${providerId}: ${detail}` };
|
|
808
|
-
} catch (err) {
|
|
809
|
-
clearTimeout(timeout);
|
|
810
|
-
if (err.name === "AbortError") {
|
|
811
|
-
return { valid: false, message: "Validation timed out after 8 seconds." };
|
|
812
|
-
}
|
|
813
|
-
return { valid: false, message: `Network error during validation: ${err.message}` };
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function getAuthPaths() {
|
|
817
|
-
const oceanAuth = path2.join(os2.homedir(), ".local", "share", "oceancode", "auth.json");
|
|
818
|
-
const opencodeAuth = path2.join(os2.homedir(), ".local", "share", "opencode", "auth.json");
|
|
819
|
-
return [oceanAuth, opencodeAuth];
|
|
820
|
-
}
|
|
821
|
-
function getStoredCredentials() {
|
|
822
|
-
const paths = getAuthPaths();
|
|
823
|
-
const merged = {};
|
|
824
|
-
for (const p of paths) {
|
|
825
|
-
if (fs2.existsSync(p)) {
|
|
826
|
-
try {
|
|
827
|
-
const raw = fs2.readFileSync(p, "utf-8");
|
|
828
|
-
const parsed = JSON.parse(raw);
|
|
829
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
830
|
-
Object.assign(merged, parsed);
|
|
831
|
-
}
|
|
832
|
-
} catch {
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
for (const prov of SUPPORTED_BYOK_PROVIDERS) {
|
|
837
|
-
const envVal = process.env[prov.envVar];
|
|
838
|
-
if (envVal && !merged[prov.id]) {
|
|
839
|
-
merged[prov.id] = { type: "api", key: envVal };
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return merged;
|
|
843
|
-
}
|
|
844
|
-
function saveProviderKey(providerId, key) {
|
|
845
|
-
const normalizedId = providerId.toLowerCase().trim();
|
|
846
|
-
const paths = getAuthPaths();
|
|
847
|
-
for (const p of paths) {
|
|
848
|
-
try {
|
|
849
|
-
const dir = path2.dirname(p);
|
|
850
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
851
|
-
let current = {};
|
|
852
|
-
if (fs2.existsSync(p)) {
|
|
853
|
-
try {
|
|
854
|
-
current = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
855
|
-
} catch {
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
current[normalizedId] = { type: "api", key: key.trim() };
|
|
859
|
-
fs2.writeFileSync(p, JSON.stringify(current, null, 2), "utf-8");
|
|
860
|
-
} catch {
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
const matched = SUPPORTED_BYOK_PROVIDERS.find((p) => p.id === normalizedId);
|
|
864
|
-
if (matched) {
|
|
865
|
-
process.env[matched.envVar] = key.trim();
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
function removeProviderKey(providerId) {
|
|
869
|
-
const normalizedId = providerId.toLowerCase().trim();
|
|
870
|
-
const paths = getAuthPaths();
|
|
871
|
-
for (const p of paths) {
|
|
872
|
-
try {
|
|
873
|
-
if (fs2.existsSync(p)) {
|
|
874
|
-
const current = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
875
|
-
if (current[normalizedId]) {
|
|
876
|
-
delete current[normalizedId];
|
|
877
|
-
fs2.writeFileSync(p, JSON.stringify(current, null, 2), "utf-8");
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
} catch {
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
const matched = SUPPORTED_BYOK_PROVIDERS.find((p) => p.id === normalizedId);
|
|
884
|
-
if (matched) {
|
|
885
|
-
delete process.env[matched.envVar];
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/models/registry.ts
|
|
890
|
-
var OCEAN_MODELS = [
|
|
891
|
-
{
|
|
892
|
-
id: "opencode/muse-spark-1.3-contributor-free",
|
|
893
|
-
name: "Muse Spark 1.3 (1M Context)",
|
|
894
|
-
description: "1,048,576 tokens \xB7 Best for big codebases & large files",
|
|
895
|
-
tag: "1M Context",
|
|
896
|
-
isFree: true,
|
|
897
|
-
provider: "opencode"
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
id: "opencode/nemotron-3-ultra-free",
|
|
901
|
-
name: "Nemotron 3 Ultra",
|
|
902
|
-
description: "Deep reasoning & architecture analysis",
|
|
903
|
-
tag: "Reasoning",
|
|
904
|
-
isFree: true,
|
|
905
|
-
provider: "opencode"
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
id: "opencode/nemotron-3.5-lightning-free",
|
|
909
|
-
name: "Nemotron 3.5 Lightning",
|
|
910
|
-
description: "Ultra-fast completions \xB7 262k context",
|
|
911
|
-
tag: "Speed",
|
|
912
|
-
isFree: true,
|
|
913
|
-
provider: "opencode"
|
|
914
|
-
},
|
|
915
|
-
{
|
|
916
|
-
id: "opencode/mimo-v2.5-free",
|
|
917
|
-
name: "MiMo 2.5",
|
|
918
|
-
description: "High accuracy code refactoring & design",
|
|
919
|
-
tag: "Pro",
|
|
920
|
-
isFree: true,
|
|
921
|
-
provider: "opencode"
|
|
922
|
-
},
|
|
923
|
-
{
|
|
924
|
-
id: "opencode/big-pickle",
|
|
925
|
-
name: "Big Pickle",
|
|
926
|
-
description: "General purpose & code generation",
|
|
927
|
-
tag: "Default",
|
|
928
|
-
isFree: true,
|
|
929
|
-
provider: "opencode"
|
|
930
|
-
},
|
|
931
|
-
{
|
|
932
|
-
id: "opencode/ling-3.0-flash-fin-free",
|
|
933
|
-
name: "Ling 3.0 Flash",
|
|
934
|
-
description: "Instant responses & quick answers",
|
|
935
|
-
tag: "Instant",
|
|
936
|
-
isFree: true,
|
|
937
|
-
provider: "opencode"
|
|
938
|
-
},
|
|
939
|
-
{
|
|
940
|
-
id: "opencode/muse-spark-1.2-contributor-free",
|
|
941
|
-
name: "Muse Spark 1.2",
|
|
942
|
-
description: "Lightweight efficient companion \xB7 1M context",
|
|
943
|
-
tag: "Light",
|
|
944
|
-
isFree: true,
|
|
945
|
-
provider: "opencode"
|
|
946
|
-
},
|
|
947
|
-
// BYOK Models (Only visible when the respective provider API key is connected)
|
|
948
|
-
{
|
|
949
|
-
id: "openai/gpt-4o",
|
|
950
|
-
name: "OpenAI GPT-4o",
|
|
951
|
-
description: "Flagship multimodal intelligence",
|
|
952
|
-
tag: "OpenAI",
|
|
953
|
-
isFree: false,
|
|
954
|
-
provider: "openai"
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
id: "openai/o3-mini",
|
|
958
|
-
name: "OpenAI o3-mini",
|
|
959
|
-
description: "High-speed reasoning for coding",
|
|
960
|
-
tag: "OpenAI",
|
|
961
|
-
isFree: false,
|
|
962
|
-
provider: "openai"
|
|
963
|
-
},
|
|
964
|
-
{
|
|
965
|
-
id: "anthropic/claude-3-5-sonnet-latest",
|
|
966
|
-
name: "Claude 3.5 Sonnet",
|
|
967
|
-
description: "State-of-the-art coding & refactoring",
|
|
968
|
-
tag: "Anthropic",
|
|
969
|
-
isFree: false,
|
|
970
|
-
provider: "anthropic"
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
id: "google/gemini-2.5-pro",
|
|
974
|
-
name: "Google Gemini 2.5 Pro",
|
|
975
|
-
description: "Long-context reasoning & coding",
|
|
976
|
-
tag: "Google",
|
|
977
|
-
isFree: false,
|
|
978
|
-
provider: "google"
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
id: "deepseek/deepseek-chat",
|
|
982
|
-
name: "DeepSeek V3",
|
|
983
|
-
description: "Powerful open-weight architecture",
|
|
984
|
-
tag: "DeepSeek",
|
|
985
|
-
isFree: false,
|
|
986
|
-
provider: "deepseek"
|
|
987
|
-
},
|
|
988
|
-
{
|
|
989
|
-
id: "deepseek/deepseek-reasoner",
|
|
990
|
-
name: "DeepSeek R1",
|
|
991
|
-
description: "Pure reasoning & mathematical logic",
|
|
992
|
-
tag: "DeepSeek",
|
|
993
|
-
isFree: false,
|
|
994
|
-
provider: "deepseek"
|
|
995
|
-
},
|
|
996
|
-
{
|
|
997
|
-
id: "groq/llama-3.3-70b-versatile",
|
|
998
|
-
name: "Groq Llama 3.3 70B",
|
|
999
|
-
description: "Ultra-low latency inference",
|
|
1000
|
-
tag: "Groq",
|
|
1001
|
-
isFree: false,
|
|
1002
|
-
provider: "groq"
|
|
1003
|
-
}
|
|
1004
|
-
];
|
|
1005
|
-
function getAvailableOceanModels(credentials) {
|
|
1006
|
-
const creds = credentials || getStoredCredentials();
|
|
1007
|
-
return OCEAN_MODELS.filter((m) => {
|
|
1008
|
-
if (m.isFree || m.provider === "opencode") return true;
|
|
1009
|
-
return Boolean(creds[m.provider]?.key);
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
function getFriendlyModelName(backendId) {
|
|
1013
|
-
const match = OCEAN_MODELS.find((m) => m.id === backendId);
|
|
1014
|
-
if (match) return match.name;
|
|
1015
|
-
return backendId.replace(/^opencode\//, "").replace(/-free$/, "");
|
|
1016
|
-
}
|
|
1017
|
-
function getBackendModelId(friendlyOrPartial) {
|
|
1018
|
-
const lower = friendlyOrPartial.toLowerCase().trim();
|
|
1019
|
-
const num = parseInt(lower);
|
|
1020
|
-
if (!isNaN(num) && num >= 1 && num <= OCEAN_MODELS.length) {
|
|
1021
|
-
return OCEAN_MODELS[num - 1].id;
|
|
1022
|
-
}
|
|
1023
|
-
const match = OCEAN_MODELS.find(
|
|
1024
|
-
(m) => m.name.toLowerCase() === lower || m.id.toLowerCase() === lower || m.name.toLowerCase().includes(lower) || m.id.toLowerCase().includes(lower)
|
|
1025
|
-
);
|
|
1026
|
-
if (match) return match.id;
|
|
1027
|
-
if (lower.includes("spark-1.3") || lower.includes("spark 1.3") || lower.includes("1m")) return "opencode/muse-spark-1.3-contributor-free";
|
|
1028
|
-
if (lower.includes("gpt-4o") || lower.includes("gpt4o") || lower.includes("openai")) return "openai/gpt-4o";
|
|
1029
|
-
if (lower.includes("o3") || lower.includes("o3-mini")) return "openai/o3-mini";
|
|
1030
|
-
if (lower.includes("claude") || lower.includes("sonnet") || lower.includes("anthropic")) return "anthropic/claude-3-5-sonnet-latest";
|
|
1031
|
-
if (lower.includes("gemini")) return "google/gemini-2.5-pro";
|
|
1032
|
-
if (lower.includes("r1") || lower.includes("reasoner")) return "deepseek/deepseek-reasoner";
|
|
1033
|
-
if (lower.includes("deepseek") || lower.includes("v3")) return "deepseek/deepseek-chat";
|
|
1034
|
-
if (lower.includes("groq")) return "groq/llama-3.3-70b-versatile";
|
|
1035
|
-
if (lower.includes("mimo")) return "opencode/mimo-v2.5-free";
|
|
1036
|
-
if (lower.includes("ultra")) return "opencode/nemotron-3-ultra-free";
|
|
1037
|
-
if (lower.includes("nemotron") || lower.includes("lightning")) return "opencode/nemotron-3.5-lightning-free";
|
|
1038
|
-
if (lower.includes("ling") || lower.includes("flash")) return "opencode/ling-3.0-flash-fin-free";
|
|
1039
|
-
if (lower.includes("muse") || lower.includes("spark")) return "opencode/muse-spark-1.3-contributor-free";
|
|
1040
|
-
if (lower.includes("pickle")) return "opencode/big-pickle";
|
|
1041
|
-
if (lower.includes("/")) return lower;
|
|
1042
|
-
return `opencode/${lower}`;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// src/commands/run.ts
|
|
1046
|
-
async function runCommand(messages, options) {
|
|
1047
|
-
const promptText = messages.join(" ").trim();
|
|
1048
|
-
if (!promptText) {
|
|
1049
|
-
console.error(chalk3.red("Error: No prompt provided. Usage: oceancode run <message>"));
|
|
1050
|
-
process.exit(1);
|
|
1051
|
-
}
|
|
1052
|
-
const baseUrl = await ensureServer();
|
|
1053
|
-
const client = new OceanClient(baseUrl);
|
|
1054
|
-
const renderer = new StreamRenderer();
|
|
1055
|
-
const backendModel = options.model ? getBackendModelId(options.model) : "opencode/muse-spark-1.3-contributor-free";
|
|
1056
|
-
const friendlyName = getFriendlyModelName(backendModel);
|
|
1057
|
-
const session = await client.createSession(
|
|
1058
|
-
promptText.length > 30 ? promptText.slice(0, 30) + "..." : promptText,
|
|
1059
|
-
process.cwd()
|
|
1060
|
-
);
|
|
1061
|
-
renderUserMessageCard(promptText, friendlyName);
|
|
1062
|
-
renderer.start(friendlyName);
|
|
1063
|
-
await new Promise((resolve, reject) => {
|
|
1064
|
-
let resolved = false;
|
|
1065
|
-
let userMsgId = null;
|
|
1066
|
-
let activityTimer = null;
|
|
1067
|
-
const reasoningPartIds = /* @__PURE__ */ new Set();
|
|
1068
|
-
const textPartIds = /* @__PURE__ */ new Set();
|
|
1069
|
-
const resetTimeout = () => {
|
|
1070
|
-
if (activityTimer) clearTimeout(activityTimer);
|
|
1071
|
-
activityTimer = setTimeout(() => {
|
|
1072
|
-
if (!resolved) {
|
|
1073
|
-
resolved = true;
|
|
1074
|
-
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1075
|
-
unsubscribe();
|
|
1076
|
-
console.log(chalk3.yellow(`
|
|
1077
|
-
\u26A0\uFE0F Execution timed out after 3 minutes of inactivity.`));
|
|
1078
|
-
resolve();
|
|
1079
|
-
}
|
|
1080
|
-
}, 18e4);
|
|
1081
|
-
activityTimer.unref();
|
|
1082
|
-
};
|
|
1083
|
-
resetTimeout();
|
|
1084
|
-
const unsubscribe = client.subscribeEvents(async (event) => {
|
|
1085
|
-
if (event.type && typeof event.type === "string" && event.type.startsWith("permission.")) {
|
|
1086
|
-
const reqId = event.properties?.requestID || event.properties?.id;
|
|
1087
|
-
if (reqId) client.replyPermission(reqId, "always");
|
|
1088
|
-
}
|
|
1089
|
-
const sessId = event.properties?.sessionID;
|
|
1090
|
-
if (sessId && sessId !== session.id) return;
|
|
1091
|
-
if (event.type === "message.updated") {
|
|
1092
|
-
if (event.properties?.info?.role === "user") {
|
|
1093
|
-
userMsgId = event.properties.info.id;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
if (event.type === "message.part.updated") {
|
|
1097
|
-
const part = event.properties?.part;
|
|
1098
|
-
if (!part) return;
|
|
1099
|
-
if (userMsgId && part.messageID === userMsgId) return;
|
|
1100
|
-
resetTimeout();
|
|
1101
|
-
if (part.type === "reasoning") {
|
|
1102
|
-
reasoningPartIds.add(part.id);
|
|
1103
|
-
if (typeof part.text === "string") {
|
|
1104
|
-
renderer.handleReasoning(part.text);
|
|
1105
|
-
}
|
|
1106
|
-
} else if (part.type === "text") {
|
|
1107
|
-
textPartIds.add(part.id);
|
|
1108
|
-
if (typeof part.text === "string") {
|
|
1109
|
-
renderer.handleText(part.text, part.id);
|
|
1110
|
-
}
|
|
1111
|
-
} else if (part.type === "tool") {
|
|
1112
|
-
const toolName = part.tool || "tool";
|
|
1113
|
-
const inputObj = part.state?.input || {};
|
|
1114
|
-
const detail = inputObj.filePath || inputObj.command || inputObj.pattern || inputObj.query || inputObj.url || inputObj.name || part.state?.title || part.call?.command || part.call?.path || "";
|
|
1115
|
-
const status = part.state?.status || part.status || "";
|
|
1116
|
-
renderer.handleTool(toolName, detail, part.id, status);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
if (event.type === "message.part.delta") {
|
|
1120
|
-
const partId = event.properties?.partID;
|
|
1121
|
-
if (partId && reasoningPartIds.has(partId)) return;
|
|
1122
|
-
const delta = event.properties?.delta;
|
|
1123
|
-
const field = event.properties?.field;
|
|
1124
|
-
if (field === "text" && typeof delta === "string") {
|
|
1125
|
-
resetTimeout();
|
|
1126
|
-
renderer.handleDelta(delta, partId);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
if (event.type === "session.idle" || event.type === "session.status" && event.properties?.status?.type === "idle") {
|
|
1130
|
-
if (!resolved) {
|
|
1131
|
-
resolved = true;
|
|
1132
|
-
if (activityTimer) clearTimeout(activityTimer);
|
|
1133
|
-
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1134
|
-
unsubscribe();
|
|
1135
|
-
resolve();
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (event.type === "session.error") {
|
|
1139
|
-
if (!resolved) {
|
|
1140
|
-
resolved = true;
|
|
1141
|
-
if (activityTimer) clearTimeout(activityTimer);
|
|
1142
|
-
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1143
|
-
unsubscribe();
|
|
1144
|
-
const errDetail = event.properties?.error || "Model execution error";
|
|
1145
|
-
console.error(chalk3.red(`
|
|
1146
|
-
Error: ${errDetail}`));
|
|
1147
|
-
resolve();
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}, (err) => {
|
|
1151
|
-
if (!resolved) {
|
|
1152
|
-
resolved = true;
|
|
1153
|
-
if (activityTimer) clearTimeout(activityTimer);
|
|
1154
|
-
unsubscribe();
|
|
1155
|
-
reject(err);
|
|
1156
|
-
}
|
|
1157
|
-
}, session.directory);
|
|
1158
|
-
client.promptSession(session.id, promptText, {
|
|
1159
|
-
model: backendModel,
|
|
1160
|
-
agent: options.agent || "build",
|
|
1161
|
-
directory: session.directory
|
|
1162
|
-
}).catch((err) => {
|
|
1163
|
-
if (!resolved) {
|
|
1164
|
-
resolved = true;
|
|
1165
|
-
if (activityTimer) clearTimeout(activityTimer);
|
|
1166
|
-
unsubscribe();
|
|
1167
|
-
reject(err);
|
|
1168
|
-
}
|
|
1169
|
-
});
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
export {
|
|
1174
|
-
ensureServer,
|
|
1175
|
-
OceanClient,
|
|
1176
|
-
OCEAN_BLUE,
|
|
1177
|
-
getLayoutDimensions,
|
|
1178
|
-
getBigLogo,
|
|
1179
|
-
renderTopBar,
|
|
1180
|
-
renderUserMessageCard,
|
|
1181
|
-
StreamRenderer,
|
|
1182
|
-
SUPPORTED_BYOK_PROVIDERS,
|
|
1183
|
-
validateProviderKey,
|
|
1184
|
-
getStoredCredentials,
|
|
1185
|
-
saveProviderKey,
|
|
1186
|
-
removeProviderKey,
|
|
1187
|
-
getAvailableOceanModels,
|
|
1188
|
-
getFriendlyModelName,
|
|
1189
|
-
getBackendModelId,
|
|
1190
|
-
runCommand
|
|
1191
|
-
};
|