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/index.js
CHANGED
|
@@ -1,52 +1,1256 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
OCEAN_BLUE,
|
|
4
|
-
OceanClient,
|
|
5
|
-
SUPPORTED_BYOK_PROVIDERS,
|
|
6
|
-
StreamRenderer,
|
|
7
|
-
ensureServer,
|
|
8
|
-
getAvailableOceanModels,
|
|
9
|
-
getBackendModelId,
|
|
10
|
-
getBigLogo,
|
|
11
|
-
getFriendlyModelName,
|
|
12
|
-
getLayoutDimensions,
|
|
13
|
-
getStoredCredentials,
|
|
14
|
-
removeProviderKey,
|
|
15
|
-
renderTopBar,
|
|
16
|
-
renderUserMessageCard,
|
|
17
|
-
runCommand,
|
|
18
|
-
saveProviderKey,
|
|
19
|
-
validateProviderKey
|
|
20
|
-
} from "./chunk-EB736JHH.js";
|
|
21
2
|
|
|
22
3
|
// src/index.ts
|
|
23
4
|
import { Command } from "commander";
|
|
24
5
|
|
|
25
6
|
// src/commands/interactive.ts
|
|
26
7
|
import readline from "readline";
|
|
8
|
+
import chalk4 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/server/manager.ts
|
|
11
|
+
import { spawn, spawnSync } from "child_process";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
17
|
+
var DEFAULT_PORT = 4196;
|
|
18
|
+
var managedProcess = null;
|
|
19
|
+
function getBaseUrl(config) {
|
|
20
|
+
const host = config?.host || process.env.OCEAN_SERVER_HOST || DEFAULT_HOST;
|
|
21
|
+
const port = config?.port || (process.env.OCEAN_SERVER_PORT ? parseInt(process.env.OCEAN_SERVER_PORT) : DEFAULT_PORT);
|
|
22
|
+
return `http://${host}:${port}`;
|
|
23
|
+
}
|
|
24
|
+
async function isServerHealthy(baseUrl) {
|
|
25
|
+
try {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeout = setTimeout(() => controller.abort(), 1e3);
|
|
28
|
+
const res = await fetch(`${baseUrl}/global/health`, {
|
|
29
|
+
signal: controller.signal
|
|
30
|
+
});
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
if (!res.ok) return false;
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
return Boolean(data.healthy);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
### CRITICAL PRINCIPLES FOR LARGE CODEBASES:
|
|
42
|
+
1. EXPLORE BEFORE ASSUMING:
|
|
43
|
+
- Always use \`glob\` or \`grep\` to locate files, symbols, types, and imports before modifying or referencing them.
|
|
44
|
+
- For broad codebase investigation, use the \`task\` tool with \`subagent_type: "explore"\`.
|
|
45
|
+
- Never assume an import, function, or class exists without verifying where it is defined.
|
|
46
|
+
|
|
47
|
+
2. PRECISE FILE READING:
|
|
48
|
+
- For large files (>200 lines), use the \`read\` tool with \`offset\` and \`limit\` to inspect the exact sections you need.
|
|
49
|
+
- Never skim or guess file contents.
|
|
50
|
+
|
|
51
|
+
3. CLEAN, SURGICAL FILE MODIFICATION:
|
|
52
|
+
- NEVER overwrite a large existing file with \`write\` when making targeted changes.
|
|
53
|
+
- ALWAYS read the file first, then use \`edit\` with the exact unique \`oldString\` and updated \`newString\`.
|
|
54
|
+
- Ensure \`oldString\` contains enough surrounding context lines (3-5 lines) to be completely unique in the file.
|
|
55
|
+
|
|
56
|
+
4. COMPLETE, PRODUCTION-READY WRITING (ZERO HALLUCINATIONS):
|
|
57
|
+
- When using \`write\` to create new files or rewrite small files, ALWAYS output complete, functional, working code.
|
|
58
|
+
- NEVER output placeholders, ellipsis comments, or abbreviations like:
|
|
59
|
+
// ... rest of code unchanged ...
|
|
60
|
+
// TODO: implement later
|
|
61
|
+
/* existing code goes here */
|
|
62
|
+
- Every file written MUST be syntactically valid and completely filled in.
|
|
63
|
+
|
|
64
|
+
5. VERIFICATION & RECOVERY:
|
|
65
|
+
- After making changes, use \`bash\` to run typechecks (\`tsc --noEmit\`), build steps (\`npm run build\`), or tests (\`npm test\`).
|
|
66
|
+
6. READABLE, STRUCTURED & ENGAGING RESPONSES (NO WALLS OF TEXT, NO TABLES):
|
|
67
|
+
- NEVER output dense, unbroken walls of text or rambling paragraphs.
|
|
68
|
+
- NEVER USE TABLES: Do not output markdown tables under any circumstances. Tables look un-neat and broken in the terminal. Always format structured data with clean, aligned bullet lists instead.
|
|
69
|
+
- ALWAYS format your responses with high visual structure:
|
|
70
|
+
\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.
|
|
71
|
+
\u2022 \u{1F522} Numbered Steps: Use numbered lists (1., 2., 3.) for chronological execution steps, install steps, or procedures.
|
|
72
|
+
\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.
|
|
73
|
+
\u2022 \u{1F4BB} Fenced Code Blocks: Always specify syntax language for code snippets (\`\`\`typescript, \`\`\`bash, etc.).
|
|
74
|
+
\u2022 \u2702\uFE0F Crisp Executive Summaries: Start with a brief, punchy overview, present the structured content (bullets), and finish with clear next steps.
|
|
75
|
+
7. CLEAN, TO THE POINT & DECORATIVE GREETINGS / RESPONSES:
|
|
76
|
+
- When introducing yourself, greeting the user, or providing project overviews:
|
|
77
|
+
\u2022 Keep it concise, punchy, decorative, and directly to the point \u2014 avoid rambling filler.
|
|
78
|
+
\u2022 Open with a stylish, clean greeting header (e.g. \u{1F30A} **OceanCode** \xB7 *<Project Name> Engineer*).
|
|
79
|
+
\u2022 Highlight capabilities with crisp emoji bullets (\u{1F680}, \u26A1, \u{1F527}, \u{1F50D}).
|
|
80
|
+
\u2022 Present project tech stack in a clean, compact bullet list (never a table).
|
|
81
|
+
\u2022 Finish with a friendly, direct prompt asking what the user wants to tackle.`;
|
|
82
|
+
function getEffectiveSystemPrompt(cwd = process.cwd()) {
|
|
83
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
84
|
+
let promptText = "";
|
|
85
|
+
if (fs.existsSync(agentsPath)) {
|
|
86
|
+
try {
|
|
87
|
+
promptText = fs.readFileSync(agentsPath, "utf-8").trim();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!promptText) {
|
|
92
|
+
promptText = AGENT_SYSTEM_PROMPT;
|
|
93
|
+
}
|
|
94
|
+
const formattingRules = `
|
|
95
|
+
|
|
96
|
+
# Critical Output Formatting Guidelines:
|
|
97
|
+
- NEVER USE TABLES: Do not output markdown tables under any circumstances. Tables look un-neat and broken in the terminal. Always format structured data with clean bullet lists instead.
|
|
98
|
+
- Bold formatting: Bold important keywords with **text** so they are rendered bold in the terminal.
|
|
99
|
+
- Provide clear, thorough, and complete explanations and working code without artificial line limits. Do not give overly brief or truncated one-word answers.
|
|
100
|
+
- Avoid unnecessary fluff or preambles, but answer questions completely and helpfully.`;
|
|
101
|
+
if (!promptText.includes("NEVER USE TABLES")) {
|
|
102
|
+
promptText += formattingRules;
|
|
103
|
+
}
|
|
104
|
+
return promptText;
|
|
105
|
+
}
|
|
106
|
+
var OCEAN_CUSTOM_MODELS = {
|
|
107
|
+
"Qwen3.6-35B-A3B": {
|
|
108
|
+
name: "Qwen 3.6 (35B)",
|
|
109
|
+
tool_call: true,
|
|
110
|
+
limit: { context: 131072, output: 8192 }
|
|
111
|
+
},
|
|
112
|
+
"step-3.7-flash": {
|
|
113
|
+
name: "Step 3.7 Flash",
|
|
114
|
+
tool_call: true,
|
|
115
|
+
limit: { context: 131072, output: 8192 }
|
|
116
|
+
},
|
|
117
|
+
"deepseek-v4-flash-vision-exp": {
|
|
118
|
+
name: "DeepSeek V4 Flash Vision",
|
|
119
|
+
tool_call: true,
|
|
120
|
+
limit: { context: 131072, output: 8192 }
|
|
121
|
+
},
|
|
122
|
+
"DeepSeek-V4-Flash": {
|
|
123
|
+
name: "DeepSeek V4 Flash",
|
|
124
|
+
tool_call: true,
|
|
125
|
+
limit: { context: 131072, output: 8192 }
|
|
126
|
+
},
|
|
127
|
+
"step-router-v1": {
|
|
128
|
+
name: "Step Router V1",
|
|
129
|
+
tool_call: true,
|
|
130
|
+
limit: { context: 131072, output: 8192 }
|
|
131
|
+
},
|
|
132
|
+
"spark-x2.5": {
|
|
133
|
+
name: "Spark X2.5",
|
|
134
|
+
tool_call: true,
|
|
135
|
+
limit: { context: 131072, output: 8192 }
|
|
136
|
+
},
|
|
137
|
+
"Qwen3.8-Flash-Next": {
|
|
138
|
+
name: "Qwen 3.8 Flash Next",
|
|
139
|
+
tool_call: true,
|
|
140
|
+
limit: { context: 131072, output: 8192 }
|
|
141
|
+
},
|
|
142
|
+
"glm-5.3-flash": {
|
|
143
|
+
name: "GLM 5.3 Flash",
|
|
144
|
+
tool_call: true,
|
|
145
|
+
limit: { context: 131072, output: 8192 }
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function setupServerConfig(oceanConfigDir) {
|
|
149
|
+
const opencodeConfigDir = path.join(oceanConfigDir, "opencode");
|
|
150
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
151
|
+
const configPath = path.join(opencodeConfigDir, "opencode.json");
|
|
152
|
+
let existing = {};
|
|
153
|
+
if (fs.existsSync(configPath)) {
|
|
154
|
+
try {
|
|
155
|
+
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const permissions = {
|
|
160
|
+
"*": "allow",
|
|
161
|
+
read: "allow",
|
|
162
|
+
write: "allow",
|
|
163
|
+
edit: "allow",
|
|
164
|
+
glob: "allow",
|
|
165
|
+
grep: "allow",
|
|
166
|
+
list: "allow",
|
|
167
|
+
bash: "allow",
|
|
168
|
+
task: "allow",
|
|
169
|
+
external_directory: "allow",
|
|
170
|
+
todowrite: "allow",
|
|
171
|
+
doom_loop: "allow",
|
|
172
|
+
skill: "allow",
|
|
173
|
+
webfetch: "allow",
|
|
174
|
+
websearch: "allow",
|
|
175
|
+
question: "allow"
|
|
176
|
+
};
|
|
177
|
+
const effectivePrompt = getEffectiveSystemPrompt();
|
|
178
|
+
const oceanApiKey = process.env.OCEAN_API_KEY || "sk-eO7z73CJ1SnBRPfDE0gTRCi2UzLqTqPdgG8WK5XOzfkHsGGf";
|
|
179
|
+
const oceanBaseUrl = process.env.OCEAN_BASE_URL || "https://api.hcnsec.cn/v1";
|
|
180
|
+
const oceanProvider = {
|
|
181
|
+
name: "Ocean",
|
|
182
|
+
npm: "@ai-sdk/openai-compatible",
|
|
183
|
+
api: "openai",
|
|
184
|
+
options: {
|
|
185
|
+
baseURL: oceanBaseUrl,
|
|
186
|
+
apiKey: oceanApiKey
|
|
187
|
+
},
|
|
188
|
+
models: OCEAN_CUSTOM_MODELS
|
|
189
|
+
};
|
|
190
|
+
const merged = {
|
|
191
|
+
$schema: "https://opencode.ai/config.json",
|
|
192
|
+
...existing,
|
|
193
|
+
disabled_providers: ["opencode"],
|
|
194
|
+
model: "ocean/Qwen3.6-35B-A3B",
|
|
195
|
+
provider: {
|
|
196
|
+
...existing.provider || {},
|
|
197
|
+
ocean: oceanProvider
|
|
198
|
+
},
|
|
199
|
+
permission: {
|
|
200
|
+
...permissions,
|
|
201
|
+
...existing.permission || {}
|
|
202
|
+
},
|
|
203
|
+
agent: {
|
|
204
|
+
...existing.agent || {},
|
|
205
|
+
build: {
|
|
206
|
+
...existing.agent?.build || {},
|
|
207
|
+
prompt: effectivePrompt,
|
|
208
|
+
permission: permissions
|
|
209
|
+
},
|
|
210
|
+
plan: {
|
|
211
|
+
...existing.agent?.plan || {},
|
|
212
|
+
prompt: effectivePrompt,
|
|
213
|
+
permission: {
|
|
214
|
+
...permissions,
|
|
215
|
+
edit: "deny",
|
|
216
|
+
write: "deny"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
222
|
+
const projectConfigPath = path.join(process.cwd(), "opencode.json");
|
|
223
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
224
|
+
try {
|
|
225
|
+
const projExisting = JSON.parse(fs.readFileSync(projectConfigPath, "utf-8"));
|
|
226
|
+
const projMerged = {
|
|
227
|
+
...projExisting,
|
|
228
|
+
disabled_providers: ["opencode"],
|
|
229
|
+
model: "ocean/Qwen3.6-35B-A3B",
|
|
230
|
+
provider: {
|
|
231
|
+
...projExisting.provider || {},
|
|
232
|
+
ocean: oceanProvider
|
|
233
|
+
},
|
|
234
|
+
agent: {
|
|
235
|
+
...projExisting.agent || {},
|
|
236
|
+
build: {
|
|
237
|
+
...projExisting.agent?.build || {},
|
|
238
|
+
prompt: effectivePrompt,
|
|
239
|
+
permission: permissions
|
|
240
|
+
},
|
|
241
|
+
plan: {
|
|
242
|
+
...projExisting.agent?.plan || {},
|
|
243
|
+
prompt: effectivePrompt,
|
|
244
|
+
permission: {
|
|
245
|
+
...permissions,
|
|
246
|
+
edit: "deny",
|
|
247
|
+
write: "deny"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
fs.writeFileSync(projectConfigPath, JSON.stringify(projMerged, null, 2), "utf-8");
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function findOpencodeBinary() {
|
|
258
|
+
const isWin = process.platform === "win32";
|
|
259
|
+
const exeName = isWin ? "opencode.exe" : "opencode";
|
|
260
|
+
try {
|
|
261
|
+
let currentDir = "";
|
|
262
|
+
if (typeof import.meta.dirname === "string") {
|
|
263
|
+
currentDir = import.meta.dirname;
|
|
264
|
+
} else if (import.meta.url) {
|
|
265
|
+
currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
266
|
+
}
|
|
267
|
+
if (currentDir) {
|
|
268
|
+
const pkgRoot = path.basename(currentDir) === "dist" ? path.resolve(currentDir, "..") : currentDir;
|
|
269
|
+
const pkgExe = path.join(pkgRoot, "node_modules", "opencode-ai", "bin", exeName);
|
|
270
|
+
if (fs.existsSync(pkgExe)) return pkgExe;
|
|
271
|
+
const hoistedExe = path.resolve(pkgRoot, "..", "opencode-ai", "bin", exeName);
|
|
272
|
+
if (fs.existsSync(hoistedExe)) return hoistedExe;
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
if (isWin && process.env.APPDATA) {
|
|
277
|
+
const globalCandidates = [
|
|
278
|
+
path.join(process.env.APPDATA, "npm", "node_modules", "oceancode", "node_modules", "opencode-ai", "bin", "opencode.exe"),
|
|
279
|
+
path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "bin", "opencode.exe")
|
|
280
|
+
];
|
|
281
|
+
for (const cand of globalCandidates) {
|
|
282
|
+
if (fs.existsSync(cand)) return cand;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const cwdExe = path.join(process.cwd(), "node_modules", "opencode-ai", "bin", exeName);
|
|
286
|
+
if (fs.existsSync(cwdExe)) return cwdExe;
|
|
287
|
+
try {
|
|
288
|
+
const lookupCmd = isWin ? "where.exe" : "which";
|
|
289
|
+
const res = spawnSync(lookupCmd, [exeName], { encoding: "utf-8", shell: false });
|
|
290
|
+
if (res.status === 0 && res.stdout) {
|
|
291
|
+
const paths = res.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
292
|
+
for (const p of paths) {
|
|
293
|
+
if (p.toLowerCase().endsWith(exeName.toLowerCase()) && fs.existsSync(p)) {
|
|
294
|
+
return p;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
async function ensureServer(config) {
|
|
303
|
+
const oceanConfigDir = path.join(os.homedir(), ".config", "oceancode");
|
|
304
|
+
setupServerConfig(oceanConfigDir);
|
|
305
|
+
const baseUrl = getBaseUrl(config);
|
|
306
|
+
const healthy = await isServerHealthy(baseUrl);
|
|
307
|
+
if (healthy) {
|
|
308
|
+
return baseUrl;
|
|
309
|
+
}
|
|
310
|
+
const port = config?.port || (process.env.OCEAN_SERVER_PORT ? parseInt(process.env.OCEAN_SERVER_PORT) : DEFAULT_PORT);
|
|
311
|
+
const host = config?.host || process.env.OCEAN_SERVER_HOST || DEFAULT_HOST;
|
|
312
|
+
const oceanDataDir = path.join(os.homedir(), ".local", "share", "oceancode");
|
|
313
|
+
const oceanStateDir = path.join(os.homedir(), ".local", "state", "oceancode");
|
|
314
|
+
fs.mkdirSync(oceanDataDir, { recursive: true });
|
|
315
|
+
fs.mkdirSync(oceanConfigDir, { recursive: true });
|
|
316
|
+
fs.mkdirSync(oceanStateDir, { recursive: true });
|
|
317
|
+
const serverLogPath = path.join(oceanStateDir, "server.log");
|
|
318
|
+
const logFd = fs.openSync(serverLogPath, "a");
|
|
319
|
+
const localBin = path.join(process.cwd(), "node_modules", ".bin");
|
|
320
|
+
const pkgBin = path.join(path.dirname(path.dirname(import.meta.dirname || "")), "node_modules", ".bin");
|
|
321
|
+
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
322
|
+
const updatedPath = [localBin, pkgBin, process.env.PATH || ""].filter(Boolean).join(pathSeparator);
|
|
323
|
+
const env = {
|
|
324
|
+
...process.env,
|
|
325
|
+
PATH: updatedPath,
|
|
326
|
+
XDG_DATA_HOME: oceanDataDir,
|
|
327
|
+
XDG_CONFIG_HOME: oceanConfigDir,
|
|
328
|
+
XDG_STATE_HOME: oceanStateDir
|
|
329
|
+
};
|
|
330
|
+
const binary = findOpencodeBinary();
|
|
331
|
+
if (!binary || !fs.existsSync(binary)) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"Could not locate the opencode backend binary (opencode.exe). Please ensure opencode-ai is installed in Oceancode."
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const args = ["serve", "--port", String(port), "--hostname", host];
|
|
337
|
+
try {
|
|
338
|
+
const child = spawn(binary, args, {
|
|
339
|
+
windowsHide: true,
|
|
340
|
+
shell: false,
|
|
341
|
+
stdio: ["ignore", logFd, logFd],
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
env,
|
|
344
|
+
detached: false
|
|
345
|
+
});
|
|
346
|
+
child.unref();
|
|
347
|
+
managedProcess = child;
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const MAX_WAIT_MS = 15e3;
|
|
350
|
+
while (Date.now() - startTime < MAX_WAIT_MS) {
|
|
351
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
352
|
+
if (await isServerHealthy(baseUrl)) {
|
|
353
|
+
return baseUrl;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let logSnippet = "";
|
|
357
|
+
try {
|
|
358
|
+
if (fs.existsSync(serverLogPath)) {
|
|
359
|
+
const fullLog = fs.readFileSync(serverLogPath, "utf-8");
|
|
360
|
+
logSnippet = "\n" + fullLog.slice(-500);
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`Ocean backend failed to respond on ${baseUrl}.${logSnippet ? ` Backend log:${logSnippet}` : ""}`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/server/client.ts
|
|
371
|
+
import http from "http";
|
|
372
|
+
var OceanClient = class {
|
|
373
|
+
baseUrl;
|
|
374
|
+
constructor(baseUrl) {
|
|
375
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
376
|
+
}
|
|
377
|
+
async getHealth() {
|
|
378
|
+
const res = await fetch(`${this.baseUrl}/global/health`);
|
|
379
|
+
return await res.json();
|
|
380
|
+
}
|
|
381
|
+
async getProviders() {
|
|
382
|
+
const res = await fetch(`${this.baseUrl}/provider`);
|
|
383
|
+
if (!res.ok) throw new Error(`Failed to fetch providers: ${res.statusText}`);
|
|
384
|
+
const data = await res.json();
|
|
385
|
+
return data.all || [];
|
|
386
|
+
}
|
|
387
|
+
async getModels() {
|
|
388
|
+
const providers = await this.getProviders();
|
|
389
|
+
const list = [];
|
|
390
|
+
for (const p of providers) {
|
|
391
|
+
if (!p.models) continue;
|
|
392
|
+
for (const [modelKey, modelData] of Object.entries(p.models)) {
|
|
393
|
+
const isFree = p.id === "opencode" || modelKey.includes("free") || modelData.cost?.input === 0;
|
|
394
|
+
list.push({
|
|
395
|
+
id: modelData.id || modelKey,
|
|
396
|
+
providerID: p.id,
|
|
397
|
+
name: modelData.name || modelKey,
|
|
398
|
+
isFree
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return list;
|
|
403
|
+
}
|
|
404
|
+
async createSession(title = "Ocean Session", directory = process.cwd()) {
|
|
405
|
+
const url = new URL(`${this.baseUrl}/session`);
|
|
406
|
+
url.searchParams.set("directory", directory);
|
|
407
|
+
const res = await fetch(url.toString(), {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"Content-Type": "application/json",
|
|
411
|
+
"x-opencode-directory": directory
|
|
412
|
+
},
|
|
413
|
+
body: JSON.stringify({ title, directory })
|
|
414
|
+
});
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
throw new Error(`Failed to create session: ${res.statusText}`);
|
|
417
|
+
}
|
|
418
|
+
return await res.json();
|
|
419
|
+
}
|
|
420
|
+
async getSessionDiff(sessionID) {
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/diff`);
|
|
423
|
+
if (!res.ok) return [];
|
|
424
|
+
return await res.json();
|
|
425
|
+
} catch {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async revertSession(sessionID, messageID) {
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/revert`, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: { "Content-Type": "application/json" },
|
|
434
|
+
body: JSON.stringify({ messageID })
|
|
435
|
+
});
|
|
436
|
+
return res.ok;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async summarizeSession(sessionID) {
|
|
442
|
+
try {
|
|
443
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/summarize`, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "Content-Type": "application/json" }
|
|
446
|
+
});
|
|
447
|
+
return res.ok;
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async listSessions(limit = 20) {
|
|
453
|
+
const res = await fetch(`${this.baseUrl}/session?limit=${limit}`);
|
|
454
|
+
if (!res.ok) throw new Error(`Failed to list sessions: ${res.statusText}`);
|
|
455
|
+
return await res.json();
|
|
456
|
+
}
|
|
457
|
+
async getSessionMessages(sessionID) {
|
|
458
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/message`);
|
|
459
|
+
if (!res.ok) throw new Error(`Failed to get session messages: ${res.statusText}`);
|
|
460
|
+
return await res.json();
|
|
461
|
+
}
|
|
462
|
+
async promptSession(sessionID, text, options) {
|
|
463
|
+
let providerID = "ocean";
|
|
464
|
+
let modelID = "Qwen3.6-35B-A3B";
|
|
465
|
+
if (options?.model) {
|
|
466
|
+
if (options.model.includes("/")) {
|
|
467
|
+
const parts = options.model.split("/");
|
|
468
|
+
providerID = parts[0];
|
|
469
|
+
modelID = parts.slice(1).join("/");
|
|
470
|
+
} else {
|
|
471
|
+
modelID = options.model;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (providerID === "opencode") {
|
|
475
|
+
providerID = "ocean";
|
|
476
|
+
modelID = "Qwen3.6-35B-A3B";
|
|
477
|
+
}
|
|
478
|
+
const payload = {
|
|
479
|
+
parts: [{ type: "text", text }],
|
|
480
|
+
model: {
|
|
481
|
+
providerID,
|
|
482
|
+
modelID
|
|
483
|
+
},
|
|
484
|
+
agent: options?.agent || "build"
|
|
485
|
+
};
|
|
486
|
+
const dir = options?.directory || process.cwd();
|
|
487
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/prompt_async`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: {
|
|
490
|
+
"Content-Type": "application/json",
|
|
491
|
+
"x-opencode-directory": dir
|
|
492
|
+
},
|
|
493
|
+
body: JSON.stringify(payload)
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
const errText = await res.text();
|
|
497
|
+
throw new Error(`Prompt error (${res.status}): ${errText}`);
|
|
498
|
+
}
|
|
499
|
+
const resText = await res.text();
|
|
500
|
+
return resText ? JSON.parse(resText) : { success: true };
|
|
501
|
+
}
|
|
502
|
+
async replyPermission(requestID, reply = "always") {
|
|
503
|
+
const res = await fetch(`${this.baseUrl}/permission/${requestID}/reply`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: { "Content-Type": "application/json" },
|
|
506
|
+
body: JSON.stringify({ reply })
|
|
507
|
+
});
|
|
508
|
+
return res.ok;
|
|
509
|
+
}
|
|
510
|
+
async replyQuestion(requestID, answers) {
|
|
511
|
+
const res = await fetch(`${this.baseUrl}/question/${requestID}/reply`, {
|
|
512
|
+
method: "POST",
|
|
513
|
+
headers: { "Content-Type": "application/json" },
|
|
514
|
+
body: JSON.stringify({ answers })
|
|
515
|
+
});
|
|
516
|
+
return res.ok;
|
|
517
|
+
}
|
|
518
|
+
async abortSession(sessionID) {
|
|
519
|
+
const res = await fetch(`${this.baseUrl}/session/${sessionID}/abort`, {
|
|
520
|
+
method: "POST"
|
|
521
|
+
});
|
|
522
|
+
return res.ok;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Subscribe to server SSE stream. Returns an unsubscribe function.
|
|
526
|
+
*/
|
|
527
|
+
subscribeEvents(onEvent, onError, directory = process.cwd()) {
|
|
528
|
+
const url = new URL(`${this.baseUrl}/event`);
|
|
529
|
+
if (directory) {
|
|
530
|
+
url.searchParams.set("directory", directory);
|
|
531
|
+
}
|
|
532
|
+
let isCancelled = false;
|
|
533
|
+
let currentRes = null;
|
|
534
|
+
const req = http.request(
|
|
535
|
+
{
|
|
536
|
+
hostname: url.hostname,
|
|
537
|
+
port: url.port,
|
|
538
|
+
path: url.pathname + url.search,
|
|
539
|
+
headers: { Accept: "text/event-stream" }
|
|
540
|
+
},
|
|
541
|
+
(res) => {
|
|
542
|
+
currentRes = res;
|
|
543
|
+
let buffer = "";
|
|
544
|
+
res.on("data", (chunk) => {
|
|
545
|
+
if (isCancelled) return;
|
|
546
|
+
buffer += chunk.toString("utf-8");
|
|
547
|
+
const lines = buffer.split("\n");
|
|
548
|
+
buffer = lines.pop() || "";
|
|
549
|
+
for (const line of lines) {
|
|
550
|
+
const trimmed = line.trim();
|
|
551
|
+
if (trimmed.startsWith("data: ")) {
|
|
552
|
+
const dataStr = trimmed.slice(6);
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(dataStr);
|
|
555
|
+
onEvent(parsed);
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
res.on("error", (err) => {
|
|
562
|
+
if (!isCancelled && onError) onError(err);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
req.on("error", (err) => {
|
|
567
|
+
if (!isCancelled && onError) onError(err);
|
|
568
|
+
});
|
|
569
|
+
req.end();
|
|
570
|
+
return () => {
|
|
571
|
+
isCancelled = true;
|
|
572
|
+
if (currentRes) {
|
|
573
|
+
currentRes.destroy();
|
|
574
|
+
currentRes = null;
|
|
575
|
+
}
|
|
576
|
+
req.destroy();
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/ui/layout.ts
|
|
27
582
|
import chalk from "chalk";
|
|
583
|
+
var INDENT = " ";
|
|
584
|
+
var OCEAN_BLUE = "#0088ff";
|
|
585
|
+
var OCEAN_CYAN = "#38bdf8";
|
|
586
|
+
function getLayoutDimensions() {
|
|
587
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
588
|
+
return { terminalWidth, contentWidth: 80, leftMargin: 2 };
|
|
589
|
+
}
|
|
590
|
+
var BIG_HEADING_LINES = [
|
|
591
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
592
|
+
"\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
|
|
593
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
594
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D ",
|
|
595
|
+
"\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
596
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
597
|
+
];
|
|
598
|
+
var COMPACT_HEADING_LINES = [
|
|
599
|
+
" \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588\u2580\u2588 \u2588\u2580\u2584 \u2588\u2580\u2580",
|
|
600
|
+
" \u2588\u2584\u2588 \u2588\u2584\u2584 \u2588\u2588\u2584 \u2588\u2580\u2588 \u2588 \u2580\u2588 \u2588\u2584\u2584 \u2588\u2584\u2588 \u2588\u2584\u2580 \u2588\u2588\u2584"
|
|
601
|
+
];
|
|
602
|
+
var ZINC_GRAYS = ["#f4f4f5", "#d4d4d8", "#a1a1aa", "#71717a", "#52525b", "#3f3f46"];
|
|
603
|
+
function getBigLogo() {
|
|
604
|
+
return BIG_HEADING_LINES.map((l, i) => chalk.hex(ZINC_GRAYS[i]).bold(l)).join("\n");
|
|
605
|
+
}
|
|
606
|
+
function renderBigLogo(indent = INDENT) {
|
|
607
|
+
const cols = process.stdout.columns || 80;
|
|
608
|
+
console.log("");
|
|
609
|
+
if (cols < 80) {
|
|
610
|
+
COMPACT_HEADING_LINES.forEach(
|
|
611
|
+
(l, i) => console.log(indent + chalk.hex(i === 0 ? "#e4e4e7" : "#71717a").bold(l))
|
|
612
|
+
);
|
|
613
|
+
} else {
|
|
614
|
+
BIG_HEADING_LINES.forEach(
|
|
615
|
+
(l, i) => console.log(indent + chalk.hex(ZINC_GRAYS[i]).bold(l))
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function renderTopBar(modelName, cwd = process.cwd()) {
|
|
620
|
+
if (process.stdout.isTTY) {
|
|
621
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
622
|
+
process.stdout.write("\x1B]2;oceancode\x07");
|
|
623
|
+
}
|
|
624
|
+
const normCwd = cwd.replace(/\\/g, "/");
|
|
625
|
+
const cols = process.stdout.columns || 80;
|
|
626
|
+
renderBigLogo(INDENT);
|
|
627
|
+
console.log("");
|
|
628
|
+
console.log(
|
|
629
|
+
`${INDENT}${chalk.bold.white("OceanCode CLI 0.1.2")}${chalk.dim(" \xB7 ")}${chalk.hex("#94a3b8")(modelName)}${chalk.dim(" \xB7 ")}${chalk.dim(normCwd)}`
|
|
630
|
+
);
|
|
631
|
+
console.log(`${INDENT}${chalk.dim("\u2500".repeat(Math.max(40, cols - 4)))}`);
|
|
632
|
+
}
|
|
633
|
+
function renderUserMessageCard(text, _modelName) {
|
|
634
|
+
const bar = chalk.hex(OCEAN_BLUE)("\u2502");
|
|
635
|
+
process.stdout.write(`
|
|
636
|
+
${bar}
|
|
637
|
+
${bar} ${chalk.bold.white(text)}
|
|
638
|
+
${bar}
|
|
639
|
+
|
|
640
|
+
`);
|
|
641
|
+
}
|
|
642
|
+
function renderFooter(modelName, durationSec, agent = "Build") {
|
|
643
|
+
const icon = chalk.hex(OCEAN_CYAN)("\u25A3");
|
|
644
|
+
const agentStr = chalk.bold.white(agent);
|
|
645
|
+
const modelStr = chalk.dim(modelName);
|
|
646
|
+
const timeStr = chalk.dim(`${durationSec.toFixed(1)}s`);
|
|
647
|
+
console.log(`
|
|
648
|
+
${icon} ${agentStr} \xB7 ${modelStr} \xB7 ${timeStr}
|
|
649
|
+
`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/ui/renderer.ts
|
|
653
|
+
import chalk2 from "chalk";
|
|
654
|
+
var TerminalMarkdownFormatter = class {
|
|
655
|
+
isBold = false;
|
|
656
|
+
isCode = false;
|
|
657
|
+
isItalic = false;
|
|
658
|
+
pendingStars = "";
|
|
659
|
+
lineBuffer = "";
|
|
660
|
+
format(chunk) {
|
|
661
|
+
this.lineBuffer += chunk;
|
|
662
|
+
let output = "";
|
|
663
|
+
if (this.lineBuffer.includes("\n")) {
|
|
664
|
+
const lines = this.lineBuffer.split("\n");
|
|
665
|
+
this.lineBuffer = lines.pop() || "";
|
|
666
|
+
for (const line of lines) {
|
|
667
|
+
const processed = this.processLine(line);
|
|
668
|
+
if (processed !== null) {
|
|
669
|
+
output += processed + "\n";
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (this.lineBuffer.startsWith("|")) {
|
|
674
|
+
return output;
|
|
675
|
+
}
|
|
676
|
+
if (this.lineBuffer) {
|
|
677
|
+
const flushed = this.processInline(this.lineBuffer);
|
|
678
|
+
this.lineBuffer = "";
|
|
679
|
+
output += flushed;
|
|
680
|
+
}
|
|
681
|
+
return output;
|
|
682
|
+
}
|
|
683
|
+
processLine(line) {
|
|
684
|
+
const trimmed = line.trim();
|
|
685
|
+
if (/^\|?(\s*:?-+:?\s*\|?)+$/.test(trimmed) && trimmed.includes("-")) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
689
|
+
const cells = trimmed.slice(1, -1).split("|").map((c) => c.trim()).filter(Boolean);
|
|
690
|
+
if (cells.length === 0) return null;
|
|
691
|
+
const lower0 = cells[0].toLowerCase().replace(/[*_`]/g, "");
|
|
692
|
+
const lower1 = (cells[1] || "").toLowerCase().replace(/[*_`]/g, "");
|
|
693
|
+
if (lower0 === "field" && lower1 === "value" || lower0 === "key" && lower1 === "value" || lower0 === "property" && lower1 === "value" || lower0 === "option" && lower1 === "description" || lower0 === "name" && lower1 === "description" || lower0 === "parameter" && lower1 === "description") {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
if (cells.length === 1) {
|
|
697
|
+
return ` \u2022 ${this.processInline(cells[0])}`;
|
|
698
|
+
} else {
|
|
699
|
+
const rawTitle = cells[0].replace(/^[*_]+|[*_]+$/g, "");
|
|
700
|
+
const title = this.processInline(rawTitle);
|
|
701
|
+
const desc = cells.slice(1).map((c) => this.processInline(c)).join(" \u2014 ");
|
|
702
|
+
return ` \u2022 \x1B[1m${title}\x1B[22m: ${desc}`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return this.processInline(line);
|
|
706
|
+
}
|
|
707
|
+
processInline(text) {
|
|
708
|
+
let result = "";
|
|
709
|
+
const combined = this.pendingStars + text;
|
|
710
|
+
this.pendingStars = "";
|
|
711
|
+
let i = 0;
|
|
712
|
+
while (i < combined.length) {
|
|
713
|
+
const ch = combined[i];
|
|
714
|
+
if (ch === "*" || ch === "_") {
|
|
715
|
+
if (i + 1 < combined.length && combined[i + 1] === ch) {
|
|
716
|
+
this.isBold = !this.isBold;
|
|
717
|
+
result += this.isBold ? "\x1B[1m" : "\x1B[22m";
|
|
718
|
+
i += 2;
|
|
719
|
+
continue;
|
|
720
|
+
} else if (i + 1 === combined.length) {
|
|
721
|
+
this.pendingStars = ch;
|
|
722
|
+
break;
|
|
723
|
+
} else {
|
|
724
|
+
this.isItalic = !this.isItalic;
|
|
725
|
+
result += this.isItalic ? "\x1B[3m" : "\x1B[23m";
|
|
726
|
+
i++;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (ch === "`") {
|
|
731
|
+
if (combined.slice(i, i + 3) === "```") {
|
|
732
|
+
result += "```";
|
|
733
|
+
i += 3;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
this.isCode = !this.isCode;
|
|
737
|
+
result += this.isCode ? "\x1B[36m" : "\x1B[39m";
|
|
738
|
+
i++;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
result += ch;
|
|
742
|
+
i++;
|
|
743
|
+
}
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
746
|
+
finish() {
|
|
747
|
+
let output = "";
|
|
748
|
+
if (this.lineBuffer) {
|
|
749
|
+
const processed = this.processLine(this.lineBuffer);
|
|
750
|
+
if (processed !== null) {
|
|
751
|
+
output += processed;
|
|
752
|
+
}
|
|
753
|
+
this.lineBuffer = "";
|
|
754
|
+
}
|
|
755
|
+
if (this.pendingStars) {
|
|
756
|
+
output += this.pendingStars;
|
|
757
|
+
this.pendingStars = "";
|
|
758
|
+
}
|
|
759
|
+
if (this.isBold) {
|
|
760
|
+
output += "\x1B[22m";
|
|
761
|
+
this.isBold = false;
|
|
762
|
+
}
|
|
763
|
+
if (this.isCode) {
|
|
764
|
+
output += "\x1B[39m";
|
|
765
|
+
this.isCode = false;
|
|
766
|
+
}
|
|
767
|
+
if (this.isItalic) {
|
|
768
|
+
output += "\x1B[23m";
|
|
769
|
+
this.isItalic = false;
|
|
770
|
+
}
|
|
771
|
+
return output;
|
|
772
|
+
}
|
|
773
|
+
reset() {
|
|
774
|
+
this.isBold = false;
|
|
775
|
+
this.isCode = false;
|
|
776
|
+
this.isItalic = false;
|
|
777
|
+
this.pendingStars = "";
|
|
778
|
+
this.lineBuffer = "";
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
var StreamRenderer = class {
|
|
782
|
+
reasoningStarted = false;
|
|
783
|
+
textStarted = false;
|
|
784
|
+
currentReasoningLength = 0;
|
|
785
|
+
currentTextLength = 0;
|
|
786
|
+
startTime = 0;
|
|
787
|
+
modelName = "Big Pickle";
|
|
788
|
+
toolsUsed = [];
|
|
789
|
+
thinkingTimer = null;
|
|
790
|
+
thinkingFrame = 0;
|
|
791
|
+
eraseBarFn = null;
|
|
792
|
+
pinBarFn = null;
|
|
793
|
+
/** Dedupe tracker: OpenCode emits pending → running → completed updates per tool part */
|
|
794
|
+
seenTools = /* @__PURE__ */ new Map();
|
|
795
|
+
/** Per-part streamed text length — prevents duplicate/lost output across parts */
|
|
796
|
+
textLens = /* @__PURE__ */ new Map();
|
|
797
|
+
/** Track parts streamed via deltas so handleText doesn't duplicate them */
|
|
798
|
+
deltaParts = /* @__PURE__ */ new Set();
|
|
799
|
+
formatter = new TerminalMarkdownFormatter();
|
|
800
|
+
/** Wire the interactive bar renderer and eraser */
|
|
801
|
+
setPinBar(pinFn, eraseFn) {
|
|
802
|
+
this.pinBarFn = pinFn;
|
|
803
|
+
this.eraseBarFn = eraseFn;
|
|
804
|
+
}
|
|
805
|
+
getThinkingFrame() {
|
|
806
|
+
return this.thinkingFrame;
|
|
807
|
+
}
|
|
808
|
+
eraseBar() {
|
|
809
|
+
this.eraseBarFn?.();
|
|
810
|
+
}
|
|
811
|
+
repinBar() {
|
|
812
|
+
this.pinBarFn?.();
|
|
813
|
+
}
|
|
814
|
+
lastOutputWasTool = false;
|
|
815
|
+
shortenPath(text) {
|
|
816
|
+
const cwd = process.cwd().replace(/\\/g, "/");
|
|
817
|
+
const cwdLower = cwd.toLowerCase();
|
|
818
|
+
let res = text.replace(/\\/g, "/");
|
|
819
|
+
let idx = res.toLowerCase().indexOf(cwdLower + "/");
|
|
820
|
+
while (idx !== -1) {
|
|
821
|
+
res = res.slice(0, idx) + res.slice(idx + cwd.length + 1);
|
|
822
|
+
idx = res.toLowerCase().indexOf(cwdLower + "/");
|
|
823
|
+
}
|
|
824
|
+
if (res.toLowerCase() === cwdLower) {
|
|
825
|
+
res = ".";
|
|
826
|
+
}
|
|
827
|
+
return res;
|
|
828
|
+
}
|
|
829
|
+
/** Collapse a tool detail to a single short line so long commands don't scroll */
|
|
830
|
+
formatDetail(detail) {
|
|
831
|
+
if (detail === void 0 || detail === null) return "";
|
|
832
|
+
const raw = Array.isArray(detail) ? detail.join(" ") : String(detail);
|
|
833
|
+
const shortened = this.shortenPath(raw);
|
|
834
|
+
const oneLine = shortened.replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
835
|
+
const MAX = 60;
|
|
836
|
+
return oneLine.length > MAX ? oneLine.slice(0, 25) + "\u2026" + oneLine.slice(-30) : oneLine;
|
|
837
|
+
}
|
|
838
|
+
// ── Thinking animation ───────────────────────────────────────────────────────
|
|
839
|
+
start(modelName = "Big Pickle") {
|
|
840
|
+
this.stopThinkingAnimation();
|
|
841
|
+
this.modelName = modelName;
|
|
842
|
+
this.startTime = Date.now();
|
|
843
|
+
this.reasoningStarted = false;
|
|
844
|
+
this.textStarted = false;
|
|
845
|
+
this.currentReasoningLength = 0;
|
|
846
|
+
this.currentTextLength = 0;
|
|
847
|
+
this.toolsUsed = [];
|
|
848
|
+
this.seenTools.clear();
|
|
849
|
+
this.textLens.clear();
|
|
850
|
+
this.deltaParts.clear();
|
|
851
|
+
this.thinkingFrame = 0;
|
|
852
|
+
this.lastOutputWasTool = false;
|
|
853
|
+
this.formatter.reset();
|
|
854
|
+
this.repinBar();
|
|
855
|
+
this.startThinkingAnimation();
|
|
856
|
+
}
|
|
857
|
+
startThinkingAnimation() {
|
|
858
|
+
if (this.thinkingTimer) return;
|
|
859
|
+
this.thinkingTimer = setInterval(() => {
|
|
860
|
+
this.thinkingFrame++;
|
|
861
|
+
this.repinBar();
|
|
862
|
+
}, 200);
|
|
863
|
+
}
|
|
864
|
+
stopThinkingAnimation() {
|
|
865
|
+
if (!this.thinkingTimer) return;
|
|
866
|
+
clearInterval(this.thinkingTimer);
|
|
867
|
+
this.thinkingTimer = null;
|
|
868
|
+
}
|
|
869
|
+
// ── Public event handlers ────────────────────────────────────────────────────
|
|
870
|
+
handleReasoning(fullText) {
|
|
871
|
+
this.reasoningStarted = true;
|
|
872
|
+
this.currentReasoningLength = fullText.length;
|
|
873
|
+
}
|
|
874
|
+
toPascalCaseTool(name) {
|
|
875
|
+
const map = {
|
|
876
|
+
list: "ListDir",
|
|
877
|
+
list_dir: "ListDir",
|
|
878
|
+
listdir: "ListDir",
|
|
879
|
+
read: "Read",
|
|
880
|
+
read_file: "Read",
|
|
881
|
+
readfile: "Read",
|
|
882
|
+
write: "Write",
|
|
883
|
+
write_to_file: "Write",
|
|
884
|
+
writefile: "Write",
|
|
885
|
+
edit: "Edit",
|
|
886
|
+
replace_file_content: "Edit",
|
|
887
|
+
replace: "Edit",
|
|
888
|
+
multi_replace_file_content: "Edit",
|
|
889
|
+
bash: "Bash",
|
|
890
|
+
run_command: "Bash",
|
|
891
|
+
grep: "Grep",
|
|
892
|
+
grep_search: "Grep",
|
|
893
|
+
glob: "Glob",
|
|
894
|
+
task: "Task",
|
|
895
|
+
webfetch: "WebFetch",
|
|
896
|
+
websearch: "WebSearch",
|
|
897
|
+
filesystemdirectorytree: "DirectoryTree",
|
|
898
|
+
filesystem_directory_tree: "DirectoryTree",
|
|
899
|
+
filesystem_read_file: "Read",
|
|
900
|
+
filesystemreadfile: "Read",
|
|
901
|
+
filesystem_write_file: "Write",
|
|
902
|
+
filesystemwritefile: "Write"
|
|
903
|
+
};
|
|
904
|
+
const lower = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
905
|
+
if (map[lower]) return map[lower];
|
|
906
|
+
return name.split(/[_-]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
907
|
+
}
|
|
908
|
+
handleTool(toolName, detail, partId, status) {
|
|
909
|
+
const key = partId ?? toolName;
|
|
910
|
+
const prev = this.seenTools.get(key);
|
|
911
|
+
if (prev && prev !== "pending_empty") {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const clean = this.formatDetail(detail).replace(/\(ctrl\+o\s+to\s+expand\)/gi, "").replace(/\\/g, "/").trim();
|
|
915
|
+
if (!clean && status === "pending") {
|
|
916
|
+
this.seenTools.set(key, "pending_empty");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
this.seenTools.set(key, status ?? "running");
|
|
920
|
+
this.stopThinkingAnimation();
|
|
921
|
+
if (!this.toolsUsed.includes(toolName)) this.toolsUsed.push(toolName);
|
|
922
|
+
this.eraseBar();
|
|
923
|
+
const arrow = chalk2.hex(OCEAN_CYAN)("\u2192");
|
|
924
|
+
const nameStr = chalk2.white(this.toPascalCaseTool(toolName));
|
|
925
|
+
const argsStr = clean ? ` ${chalk2.dim(clean)}` : "";
|
|
926
|
+
const prefix = this.lastOutputWasTool ? "" : "\n";
|
|
927
|
+
process.stdout.write(`${prefix} ${arrow} ${nameStr}${argsStr}
|
|
928
|
+
`);
|
|
929
|
+
process.stdout.write("\x1B7");
|
|
930
|
+
this.lastOutputWasTool = true;
|
|
931
|
+
this.repinBar();
|
|
932
|
+
this.startThinkingAnimation();
|
|
933
|
+
}
|
|
934
|
+
handleDelta(delta, partId = "default") {
|
|
935
|
+
if (!delta) return;
|
|
936
|
+
this.deltaParts.add(partId);
|
|
937
|
+
this.stopThinkingAnimation();
|
|
938
|
+
if (!this.textStarted) {
|
|
939
|
+
this.textStarted = true;
|
|
940
|
+
this.eraseBar();
|
|
941
|
+
if (this.lastOutputWasTool) {
|
|
942
|
+
process.stdout.write("\n");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
this.lastOutputWasTool = false;
|
|
946
|
+
this.currentTextLength += delta.length;
|
|
947
|
+
this.textLens.set(partId, (this.textLens.get(partId) ?? 0) + delta.length);
|
|
948
|
+
const formatted = this.formatter.format(delta);
|
|
949
|
+
if (formatted) {
|
|
950
|
+
process.stdout.write(formatted);
|
|
951
|
+
process.stdout.write("\x1B7");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
handleText(fullText, partId = "default") {
|
|
955
|
+
if (!fullText) return;
|
|
956
|
+
if (this.deltaParts.has(partId)) return;
|
|
957
|
+
this.stopThinkingAnimation();
|
|
958
|
+
if (!this.textStarted) {
|
|
959
|
+
this.textStarted = true;
|
|
960
|
+
this.eraseBar();
|
|
961
|
+
if (this.lastOutputWasTool) {
|
|
962
|
+
process.stdout.write("\n");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
this.lastOutputWasTool = false;
|
|
966
|
+
const written = this.textLens.get(partId) ?? 0;
|
|
967
|
+
if (fullText.length <= written) return;
|
|
968
|
+
const slice = fullText.slice(written);
|
|
969
|
+
this.textLens.set(partId, fullText.length);
|
|
970
|
+
this.currentTextLength += slice.length;
|
|
971
|
+
const formatted = this.formatter.format(slice);
|
|
972
|
+
if (formatted) {
|
|
973
|
+
process.stdout.write(formatted);
|
|
974
|
+
process.stdout.write("\x1B7");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
finish(agent = "Build") {
|
|
978
|
+
this.stopThinkingAnimation();
|
|
979
|
+
const flushed = this.formatter.finish();
|
|
980
|
+
if (flushed) {
|
|
981
|
+
process.stdout.write(flushed);
|
|
982
|
+
}
|
|
983
|
+
const totalSec = Math.max(0.1, (Date.now() - this.startTime) / 1e3);
|
|
984
|
+
process.stdout.write("\n");
|
|
985
|
+
renderFooter(this.modelName, totalSec, agent);
|
|
986
|
+
process.stdout.write("\x1B7");
|
|
987
|
+
this.reasoningStarted = false;
|
|
988
|
+
this.textStarted = false;
|
|
989
|
+
this.currentReasoningLength = 0;
|
|
990
|
+
this.currentTextLength = 0;
|
|
991
|
+
this.toolsUsed = [];
|
|
992
|
+
this.seenTools.clear();
|
|
993
|
+
this.textLens.clear();
|
|
994
|
+
this.deltaParts.clear();
|
|
995
|
+
this.repinBar();
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// src/commands/run.ts
|
|
1000
|
+
import chalk3 from "chalk";
|
|
1001
|
+
|
|
1002
|
+
// src/models/registry.ts
|
|
1003
|
+
var DEFAULT_MODEL_ID = "ocean/Qwen3.6-35B-A3B";
|
|
1004
|
+
var OCEAN_MODELS = [
|
|
1005
|
+
{
|
|
1006
|
+
id: "ocean/Qwen3.6-35B-A3B",
|
|
1007
|
+
name: "Qwen 3.6 (35B)",
|
|
1008
|
+
isFree: true,
|
|
1009
|
+
provider: "ocean"
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
id: "ocean/step-3.7-flash",
|
|
1013
|
+
name: "Step 3.7 Flash",
|
|
1014
|
+
isFree: true,
|
|
1015
|
+
provider: "ocean"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
id: "ocean/deepseek-v4-flash-vision-exp",
|
|
1019
|
+
name: "DeepSeek V4 Flash Vision",
|
|
1020
|
+
isFree: true,
|
|
1021
|
+
provider: "ocean"
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "ocean/DeepSeek-V4-Flash",
|
|
1025
|
+
name: "DeepSeek V4 Flash",
|
|
1026
|
+
isFree: true,
|
|
1027
|
+
provider: "ocean"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "ocean/step-router-v1",
|
|
1031
|
+
name: "Step Router V1",
|
|
1032
|
+
isFree: true,
|
|
1033
|
+
provider: "ocean"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
id: "ocean/spark-x2.5",
|
|
1037
|
+
name: "Spark X2.5",
|
|
1038
|
+
isFree: true,
|
|
1039
|
+
provider: "ocean"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
id: "ocean/Qwen3.8-Flash-Next",
|
|
1043
|
+
name: "Qwen 3.8 Flash Next",
|
|
1044
|
+
isFree: true,
|
|
1045
|
+
provider: "ocean"
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
id: "ocean/glm-5.3-flash",
|
|
1049
|
+
name: "GLM 5.3 Flash",
|
|
1050
|
+
isFree: true,
|
|
1051
|
+
provider: "ocean"
|
|
1052
|
+
}
|
|
1053
|
+
];
|
|
1054
|
+
function getAvailableOceanModels() {
|
|
1055
|
+
return OCEAN_MODELS;
|
|
1056
|
+
}
|
|
1057
|
+
function getFriendlyModelName(backendId) {
|
|
1058
|
+
const match = OCEAN_MODELS.find((m) => m.id === backendId);
|
|
1059
|
+
if (match) return match.name;
|
|
1060
|
+
return backendId.replace(/^ocean\//, "").replace(/^opencode\//, "").replace(/-free$/, "");
|
|
1061
|
+
}
|
|
1062
|
+
function getBackendModelId(friendlyOrPartial) {
|
|
1063
|
+
const lower = friendlyOrPartial.toLowerCase().trim();
|
|
1064
|
+
const num = parseInt(lower);
|
|
1065
|
+
if (!isNaN(num) && num >= 1 && num <= OCEAN_MODELS.length) {
|
|
1066
|
+
return OCEAN_MODELS[num - 1].id;
|
|
1067
|
+
}
|
|
1068
|
+
const match = OCEAN_MODELS.find(
|
|
1069
|
+
(m) => m.name.toLowerCase() === lower || m.id.toLowerCase() === lower || m.name.toLowerCase().includes(lower) || m.id.toLowerCase().includes(lower)
|
|
1070
|
+
);
|
|
1071
|
+
if (match) return match.id;
|
|
1072
|
+
if (lower.includes("qwen3.6") || lower.includes("35b") || lower.includes("qwen")) return "ocean/Qwen3.6-35B-A3B";
|
|
1073
|
+
if (lower.includes("step-3.7") || lower.includes("3.7") || lower.includes("step")) return "ocean/step-3.7-flash";
|
|
1074
|
+
if (lower.includes("vision") || lower.includes("flash-vision")) return "ocean/deepseek-v4-flash-vision-exp";
|
|
1075
|
+
if (lower.includes("deepseek-v4") || lower.includes("v4-flash") || lower.includes("deepseek")) return "ocean/DeepSeek-V4-Flash";
|
|
1076
|
+
if (lower.includes("router")) return "ocean/step-router-v1";
|
|
1077
|
+
if (lower.includes("spark")) return "ocean/spark-x2.5";
|
|
1078
|
+
if (lower.includes("3.8") || lower.includes("next")) return "ocean/Qwen3.8-Flash-Next";
|
|
1079
|
+
if (lower.includes("glm")) return "ocean/glm-5.3-flash";
|
|
1080
|
+
if (lower.includes("/")) return lower;
|
|
1081
|
+
return `ocean/${lower}`;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/ui/toolLabels.ts
|
|
1085
|
+
function extractToolDetail(part) {
|
|
1086
|
+
let input = part?.state?.input ?? part?.input ?? part?.args ?? part?.call?.input ?? part?.call?.args;
|
|
1087
|
+
if (typeof input === "string" && input.trim().startsWith("{")) {
|
|
1088
|
+
try {
|
|
1089
|
+
input = JSON.parse(input);
|
|
1090
|
+
} catch {
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof input === "string" && input.trim()) {
|
|
1094
|
+
return input.trim();
|
|
1095
|
+
}
|
|
1096
|
+
if (input && typeof input === "object") {
|
|
1097
|
+
const candidate = input.filePath || input.path || input.file || input.command || input.pattern || input.query || input.url || input.name || input.directory || input.dir || input.description || (Object.values(input)[0] !== void 0 ? String(Object.values(input)[0]) : "");
|
|
1098
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1099
|
+
}
|
|
1100
|
+
if (typeof part?.state?.title === "string" && part.state.title.trim()) return part.state.title.trim();
|
|
1101
|
+
if (typeof part?.call?.path === "string" && part.call.path.trim()) return part.call.path.trim();
|
|
1102
|
+
if (typeof part?.call?.command === "string" && part.call.command.trim()) return part.call.command.trim();
|
|
1103
|
+
return "";
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/commands/run.ts
|
|
1107
|
+
async function runCommand(messages, options) {
|
|
1108
|
+
const promptText = messages.join(" ").trim();
|
|
1109
|
+
if (!promptText) {
|
|
1110
|
+
console.error(chalk3.red("Error: No prompt provided. Usage: oceancode run <message>"));
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
const baseUrl = await ensureServer();
|
|
1114
|
+
const client = new OceanClient(baseUrl);
|
|
1115
|
+
const renderer = new StreamRenderer();
|
|
1116
|
+
const backendModel = options.model ? getBackendModelId(options.model) : DEFAULT_MODEL_ID;
|
|
1117
|
+
const friendlyName = getFriendlyModelName(backendModel);
|
|
1118
|
+
const session = await client.createSession(
|
|
1119
|
+
promptText.length > 30 ? promptText.slice(0, 30) + "..." : promptText,
|
|
1120
|
+
process.cwd()
|
|
1121
|
+
);
|
|
1122
|
+
renderUserMessageCard(promptText, friendlyName);
|
|
1123
|
+
renderer.start(friendlyName);
|
|
1124
|
+
await new Promise((resolve, reject) => {
|
|
1125
|
+
let resolved = false;
|
|
1126
|
+
let userMsgId = null;
|
|
1127
|
+
let activityTimer = null;
|
|
1128
|
+
const reasoningPartIds = /* @__PURE__ */ new Set();
|
|
1129
|
+
const textPartIds = /* @__PURE__ */ new Set();
|
|
1130
|
+
const resetTimeout = () => {
|
|
1131
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
1132
|
+
activityTimer = setTimeout(() => {
|
|
1133
|
+
if (!resolved) {
|
|
1134
|
+
resolved = true;
|
|
1135
|
+
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1136
|
+
unsubscribe();
|
|
1137
|
+
console.log(chalk3.yellow(`
|
|
1138
|
+
\u26A0\uFE0F Execution timed out after 3 minutes of inactivity.`));
|
|
1139
|
+
resolve();
|
|
1140
|
+
}
|
|
1141
|
+
}, 18e4);
|
|
1142
|
+
activityTimer.unref();
|
|
1143
|
+
};
|
|
1144
|
+
resetTimeout();
|
|
1145
|
+
const unsubscribe = client.subscribeEvents(async (event) => {
|
|
1146
|
+
if (event.type && typeof event.type === "string" && event.type.startsWith("permission.")) {
|
|
1147
|
+
const reqId = event.properties?.requestID || event.properties?.id;
|
|
1148
|
+
if (reqId) client.replyPermission(reqId, "always");
|
|
1149
|
+
}
|
|
1150
|
+
const sessId = event.properties?.sessionID;
|
|
1151
|
+
if (sessId && sessId !== session.id) return;
|
|
1152
|
+
if (event.type === "message.updated") {
|
|
1153
|
+
if (event.properties?.info?.role === "user") {
|
|
1154
|
+
userMsgId = event.properties.info.id;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (event.type === "message.part.updated") {
|
|
1158
|
+
const part = event.properties?.part;
|
|
1159
|
+
if (!part) return;
|
|
1160
|
+
if (userMsgId && part.messageID === userMsgId) return;
|
|
1161
|
+
resetTimeout();
|
|
1162
|
+
if (part.type === "reasoning") {
|
|
1163
|
+
reasoningPartIds.add(part.id);
|
|
1164
|
+
if (typeof part.text === "string") {
|
|
1165
|
+
renderer.handleReasoning(part.text);
|
|
1166
|
+
}
|
|
1167
|
+
} else if (part.type === "text") {
|
|
1168
|
+
textPartIds.add(part.id);
|
|
1169
|
+
if (typeof part.text === "string") {
|
|
1170
|
+
renderer.handleText(part.text, part.id);
|
|
1171
|
+
}
|
|
1172
|
+
} else if (part.type === "tool") {
|
|
1173
|
+
const toolName = part.tool || "tool";
|
|
1174
|
+
const detail = extractToolDetail(part);
|
|
1175
|
+
const status = part.state?.status || part.status || "";
|
|
1176
|
+
renderer.handleTool(toolName, detail, part.id, status);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (event.type === "message.part.delta") {
|
|
1180
|
+
const partId = event.properties?.partID || event.properties?.partId || event.properties?.id || "default";
|
|
1181
|
+
if (reasoningPartIds.has(partId)) return;
|
|
1182
|
+
const delta = event.properties?.delta;
|
|
1183
|
+
const field = event.properties?.field;
|
|
1184
|
+
if (field === "text" && typeof delta === "string") {
|
|
1185
|
+
resetTimeout();
|
|
1186
|
+
renderer.handleDelta(delta, partId);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (event.type === "session.idle" || event.type === "session.status" && event.properties?.status?.type === "idle") {
|
|
1190
|
+
if (!resolved) {
|
|
1191
|
+
resolved = true;
|
|
1192
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
1193
|
+
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1194
|
+
unsubscribe();
|
|
1195
|
+
resolve();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (event.type === "session.error") {
|
|
1199
|
+
if (!resolved) {
|
|
1200
|
+
resolved = true;
|
|
1201
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
1202
|
+
renderer.finish(options.agent === "plan" ? "Plan" : "Build");
|
|
1203
|
+
unsubscribe();
|
|
1204
|
+
const errDetail = event.properties?.error || "Model execution error";
|
|
1205
|
+
console.error(chalk3.red(`
|
|
1206
|
+
Error: ${errDetail}`));
|
|
1207
|
+
resolve();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}, (err) => {
|
|
1211
|
+
if (!resolved) {
|
|
1212
|
+
resolved = true;
|
|
1213
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
1214
|
+
unsubscribe();
|
|
1215
|
+
reject(err);
|
|
1216
|
+
}
|
|
1217
|
+
}, session.directory);
|
|
1218
|
+
client.promptSession(session.id, promptText, {
|
|
1219
|
+
model: backendModel,
|
|
1220
|
+
agent: options.agent || "build",
|
|
1221
|
+
directory: session.directory
|
|
1222
|
+
}).catch((err) => {
|
|
1223
|
+
if (!resolved) {
|
|
1224
|
+
resolved = true;
|
|
1225
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
1226
|
+
unsubscribe();
|
|
1227
|
+
reject(err);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
28
1232
|
|
|
29
1233
|
// src/utils/init.ts
|
|
30
|
-
import
|
|
31
|
-
import
|
|
1234
|
+
import fs2 from "fs";
|
|
1235
|
+
import path2 from "path";
|
|
32
1236
|
function initializeAgentsDoc(workspaceDir = process.cwd()) {
|
|
33
|
-
const agentsPath =
|
|
34
|
-
const alreadyExists =
|
|
1237
|
+
const agentsPath = path2.join(workspaceDir, "AGENTS.md");
|
|
1238
|
+
const alreadyExists = fs2.existsSync(agentsPath);
|
|
35
1239
|
let projectType = "General Codebase";
|
|
36
1240
|
let techDetails = [];
|
|
37
1241
|
let buildCommands = [];
|
|
38
1242
|
let testCommands = [];
|
|
39
1243
|
let conventions = [];
|
|
40
|
-
const pkgJsonPath =
|
|
41
|
-
const tsConfigPath =
|
|
42
|
-
const pyProjectPath =
|
|
43
|
-
const reqTxtPath =
|
|
44
|
-
const cargoPath =
|
|
45
|
-
const goModPath =
|
|
46
|
-
if (
|
|
1244
|
+
const pkgJsonPath = path2.join(workspaceDir, "package.json");
|
|
1245
|
+
const tsConfigPath = path2.join(workspaceDir, "tsconfig.json");
|
|
1246
|
+
const pyProjectPath = path2.join(workspaceDir, "pyproject.toml");
|
|
1247
|
+
const reqTxtPath = path2.join(workspaceDir, "requirements.txt");
|
|
1248
|
+
const cargoPath = path2.join(workspaceDir, "Cargo.toml");
|
|
1249
|
+
const goModPath = path2.join(workspaceDir, "go.mod");
|
|
1250
|
+
if (fs2.existsSync(pkgJsonPath)) {
|
|
47
1251
|
try {
|
|
48
|
-
const pkg = JSON.parse(
|
|
49
|
-
projectType = `Node.js / JavaScript${
|
|
1252
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
|
|
1253
|
+
projectType = `Node.js / JavaScript${fs2.existsSync(tsConfigPath) ? " / TypeScript" : ""}`;
|
|
50
1254
|
if (pkg.name) techDetails.push(`- **Project Name**: ${pkg.name}`);
|
|
51
1255
|
if (pkg.version) techDetails.push(`- **Version**: ${pkg.version}`);
|
|
52
1256
|
if (pkg.description) techDetails.push(`- **Description**: ${pkg.description}`);
|
|
@@ -66,22 +1270,22 @@ function initializeAgentsDoc(workspaceDir = process.cwd()) {
|
|
|
66
1270
|
}
|
|
67
1271
|
} catch {
|
|
68
1272
|
}
|
|
69
|
-
} else if (
|
|
1273
|
+
} else if (fs2.existsSync(pyProjectPath) || fs2.existsSync(reqTxtPath)) {
|
|
70
1274
|
projectType = "Python Project";
|
|
71
1275
|
testCommands.push("- Test: `pytest`");
|
|
72
1276
|
buildCommands.push("- Run: `python main.py`");
|
|
73
|
-
} else if (
|
|
1277
|
+
} else if (fs2.existsSync(cargoPath)) {
|
|
74
1278
|
projectType = "Rust Project";
|
|
75
1279
|
buildCommands.push("- Build: `cargo build`");
|
|
76
1280
|
testCommands.push("- Test: `cargo test`");
|
|
77
|
-
} else if (
|
|
1281
|
+
} else if (fs2.existsSync(goModPath)) {
|
|
78
1282
|
projectType = "Go Project";
|
|
79
1283
|
buildCommands.push("- Build: `go build ./...`");
|
|
80
1284
|
testCommands.push("- Test: `go test ./...`");
|
|
81
1285
|
}
|
|
82
1286
|
const dirs = [];
|
|
83
1287
|
try {
|
|
84
|
-
const entries =
|
|
1288
|
+
const entries = fs2.readdirSync(workspaceDir, { withFileTypes: true });
|
|
85
1289
|
for (const e of entries) {
|
|
86
1290
|
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist") {
|
|
87
1291
|
dirs.push(e.name);
|
|
@@ -116,11 +1320,11 @@ ${testCommands.length > 0 ? testCommands.join("\n") : "- Standard unit tests"}
|
|
|
116
1320
|
- When updating existing files, always use \`edit\` with exact matching lines rather than overwriting the entire file with \`write\`.
|
|
117
1321
|
4. **Verification**:
|
|
118
1322
|
- After completing edits, run available verification scripts or tests via \`bash\` to ensure syntax, compilation, and tests succeed.
|
|
119
|
-
5. **Readable, Structured Formatting (No Walls of Text)**:
|
|
1323
|
+
5. **Readable, Structured Formatting (No Walls of Text, No Tables)**:
|
|
120
1324
|
- Never output dense, unbroken walls of text.
|
|
121
|
-
- Always format explanations with
|
|
1325
|
+
- Never output markdown tables. Always format explanations with clean, readable bullet points with contextual emojis (\u{1F4C1}, \u{1F527}, \u{1F4A1}, \u26A1, \u{1F680}, \u26A0\uFE0F, \u2705, \u{1F4CC}, \u{1F3AF}), numbered steps, bold keywords, and fenced code blocks.
|
|
122
1326
|
`;
|
|
123
|
-
|
|
1327
|
+
fs2.writeFileSync(agentsPath, agentsContent, "utf-8");
|
|
124
1328
|
return {
|
|
125
1329
|
filePath: agentsPath,
|
|
126
1330
|
created: !alreadyExists,
|
|
@@ -129,18 +1333,169 @@ ${testCommands.length > 0 ? testCommands.join("\n") : "- Standard unit tests"}
|
|
|
129
1333
|
};
|
|
130
1334
|
}
|
|
131
1335
|
|
|
1336
|
+
// src/server/auth.ts
|
|
1337
|
+
import path3 from "path";
|
|
1338
|
+
import os2 from "os";
|
|
1339
|
+
import fs3 from "fs";
|
|
1340
|
+
var SUPPORTED_BYOK_PROVIDERS = [
|
|
1341
|
+
{ id: "openai", name: "OpenAI", envVar: "OPENAI_API_KEY", placeholder: "sk-proj-..." },
|
|
1342
|
+
{ id: "anthropic", name: "Anthropic", envVar: "ANTHROPIC_API_KEY", placeholder: "sk-ant-..." },
|
|
1343
|
+
{ id: "google", name: "Google Gemini", envVar: "GEMINI_API_KEY", placeholder: "AIzaSy..." },
|
|
1344
|
+
{ id: "deepseek", name: "DeepSeek", envVar: "DEEPSEEK_API_KEY", placeholder: "sk-..." },
|
|
1345
|
+
{ id: "groq", name: "Groq", envVar: "GROQ_API_KEY", placeholder: "gsk_..." },
|
|
1346
|
+
{ id: "openrouter", name: "OpenRouter", envVar: "OPENROUTER_API_KEY", placeholder: "sk-or-..." }
|
|
1347
|
+
];
|
|
1348
|
+
async function validateProviderKey(providerId, key) {
|
|
1349
|
+
const cleanKey = key.trim();
|
|
1350
|
+
if (!cleanKey) {
|
|
1351
|
+
return { valid: false, message: "API key cannot be empty." };
|
|
1352
|
+
}
|
|
1353
|
+
const normalized = providerId.toLowerCase().trim();
|
|
1354
|
+
const controller = new AbortController();
|
|
1355
|
+
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
1356
|
+
try {
|
|
1357
|
+
let res;
|
|
1358
|
+
if (normalized === "openai") {
|
|
1359
|
+
res = await fetch("https://api.openai.com/v1/models", {
|
|
1360
|
+
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
1361
|
+
signal: controller.signal
|
|
1362
|
+
});
|
|
1363
|
+
} else if (normalized === "anthropic") {
|
|
1364
|
+
res = await fetch("https://api.anthropic.com/v1/models", {
|
|
1365
|
+
headers: {
|
|
1366
|
+
"x-api-key": cleanKey,
|
|
1367
|
+
"anthropic-version": "2023-06-01"
|
|
1368
|
+
},
|
|
1369
|
+
signal: controller.signal
|
|
1370
|
+
});
|
|
1371
|
+
} else if (normalized === "google" || normalized === "gemini") {
|
|
1372
|
+
res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(cleanKey)}`, {
|
|
1373
|
+
signal: controller.signal
|
|
1374
|
+
});
|
|
1375
|
+
} else if (normalized === "deepseek") {
|
|
1376
|
+
res = await fetch("https://api.deepseek.com/models", {
|
|
1377
|
+
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
1378
|
+
signal: controller.signal
|
|
1379
|
+
});
|
|
1380
|
+
} else if (normalized === "groq") {
|
|
1381
|
+
res = await fetch("https://api.groq.com/openai/v1/models", {
|
|
1382
|
+
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
1383
|
+
signal: controller.signal
|
|
1384
|
+
});
|
|
1385
|
+
} else if (normalized === "openrouter") {
|
|
1386
|
+
res = await fetch("https://openrouter.ai/api/v1/auth/key", {
|
|
1387
|
+
headers: { Authorization: `Bearer ${cleanKey}` },
|
|
1388
|
+
signal: controller.signal
|
|
1389
|
+
});
|
|
1390
|
+
} else {
|
|
1391
|
+
clearTimeout(timeout);
|
|
1392
|
+
return { valid: false, message: `Unknown provider: ${providerId}` };
|
|
1393
|
+
}
|
|
1394
|
+
clearTimeout(timeout);
|
|
1395
|
+
if (res.ok) {
|
|
1396
|
+
return { valid: true, message: "API key is valid and connected." };
|
|
1397
|
+
}
|
|
1398
|
+
let detail = "";
|
|
1399
|
+
try {
|
|
1400
|
+
const data = await res.json();
|
|
1401
|
+
detail = data.error?.message || data.message || `HTTP ${res.status}`;
|
|
1402
|
+
} catch {
|
|
1403
|
+
detail = `HTTP status ${res.status}`;
|
|
1404
|
+
}
|
|
1405
|
+
return { valid: false, message: `Rejected by ${providerId}: ${detail}` };
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
clearTimeout(timeout);
|
|
1408
|
+
if (err.name === "AbortError") {
|
|
1409
|
+
return { valid: false, message: "Validation timed out after 8 seconds." };
|
|
1410
|
+
}
|
|
1411
|
+
return { valid: false, message: `Network error during validation: ${err.message}` };
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function getAuthPaths() {
|
|
1415
|
+
const oceanAuth = path3.join(os2.homedir(), ".local", "share", "oceancode", "auth.json");
|
|
1416
|
+
const opencodeAuth = path3.join(os2.homedir(), ".local", "share", "opencode", "auth.json");
|
|
1417
|
+
return [oceanAuth, opencodeAuth];
|
|
1418
|
+
}
|
|
1419
|
+
function getStoredCredentials() {
|
|
1420
|
+
const paths = getAuthPaths();
|
|
1421
|
+
const merged = {};
|
|
1422
|
+
for (const p of paths) {
|
|
1423
|
+
if (fs3.existsSync(p)) {
|
|
1424
|
+
try {
|
|
1425
|
+
const raw = fs3.readFileSync(p, "utf-8");
|
|
1426
|
+
const parsed = JSON.parse(raw);
|
|
1427
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
1428
|
+
Object.assign(merged, parsed);
|
|
1429
|
+
}
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
for (const prov of SUPPORTED_BYOK_PROVIDERS) {
|
|
1435
|
+
const envVal = process.env[prov.envVar];
|
|
1436
|
+
if (envVal && !merged[prov.id]) {
|
|
1437
|
+
merged[prov.id] = { type: "api", key: envVal };
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return merged;
|
|
1441
|
+
}
|
|
1442
|
+
function saveProviderKey(providerId, key) {
|
|
1443
|
+
const normalizedId = providerId.toLowerCase().trim();
|
|
1444
|
+
const paths = getAuthPaths();
|
|
1445
|
+
for (const p of paths) {
|
|
1446
|
+
try {
|
|
1447
|
+
const dir = path3.dirname(p);
|
|
1448
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1449
|
+
let current = {};
|
|
1450
|
+
if (fs3.existsSync(p)) {
|
|
1451
|
+
try {
|
|
1452
|
+
current = JSON.parse(fs3.readFileSync(p, "utf-8"));
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
current[normalizedId] = { type: "api", key: key.trim() };
|
|
1457
|
+
fs3.writeFileSync(p, JSON.stringify(current, null, 2), "utf-8");
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const matched = SUPPORTED_BYOK_PROVIDERS.find((p) => p.id === normalizedId);
|
|
1462
|
+
if (matched) {
|
|
1463
|
+
process.env[matched.envVar] = key.trim();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
function removeProviderKey(providerId) {
|
|
1467
|
+
const normalizedId = providerId.toLowerCase().trim();
|
|
1468
|
+
const paths = getAuthPaths();
|
|
1469
|
+
for (const p of paths) {
|
|
1470
|
+
try {
|
|
1471
|
+
if (fs3.existsSync(p)) {
|
|
1472
|
+
const current = JSON.parse(fs3.readFileSync(p, "utf-8"));
|
|
1473
|
+
if (current[normalizedId]) {
|
|
1474
|
+
delete current[normalizedId];
|
|
1475
|
+
fs3.writeFileSync(p, JSON.stringify(current, null, 2), "utf-8");
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const matched = SUPPORTED_BYOK_PROVIDERS.find((p) => p.id === normalizedId);
|
|
1482
|
+
if (matched) {
|
|
1483
|
+
delete process.env[matched.envVar];
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
132
1487
|
// src/server/mcp.ts
|
|
133
|
-
import
|
|
134
|
-
import
|
|
135
|
-
import
|
|
1488
|
+
import path4 from "path";
|
|
1489
|
+
import os3 from "os";
|
|
1490
|
+
import fs4 from "fs";
|
|
136
1491
|
function getOpencodeConfigPath() {
|
|
137
|
-
return
|
|
1492
|
+
return path4.join(os3.homedir(), ".config", "oceancode", "opencode", "opencode.json");
|
|
138
1493
|
}
|
|
139
1494
|
function getConfiguredMcpServers() {
|
|
140
1495
|
const configPath = getOpencodeConfigPath();
|
|
141
|
-
if (!
|
|
1496
|
+
if (!fs4.existsSync(configPath)) return {};
|
|
142
1497
|
try {
|
|
143
|
-
const raw =
|
|
1498
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
144
1499
|
const parsed = JSON.parse(raw);
|
|
145
1500
|
return parsed.mcp || {};
|
|
146
1501
|
} catch {
|
|
@@ -149,27 +1504,27 @@ function getConfiguredMcpServers() {
|
|
|
149
1504
|
}
|
|
150
1505
|
function saveMcpServer(name, config) {
|
|
151
1506
|
const configPath = getOpencodeConfigPath();
|
|
152
|
-
const dir =
|
|
153
|
-
|
|
1507
|
+
const dir = path4.dirname(configPath);
|
|
1508
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
154
1509
|
let fullConfig = {};
|
|
155
|
-
if (
|
|
1510
|
+
if (fs4.existsSync(configPath)) {
|
|
156
1511
|
try {
|
|
157
|
-
fullConfig = JSON.parse(
|
|
1512
|
+
fullConfig = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
158
1513
|
} catch {
|
|
159
1514
|
}
|
|
160
1515
|
}
|
|
161
1516
|
if (!fullConfig.mcp) fullConfig.mcp = {};
|
|
162
1517
|
fullConfig.mcp[name] = config;
|
|
163
|
-
|
|
1518
|
+
fs4.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), "utf-8");
|
|
164
1519
|
try {
|
|
165
|
-
const projectConfig =
|
|
1520
|
+
const projectConfig = path4.join(process.cwd(), "opencode.json");
|
|
166
1521
|
let proj = {};
|
|
167
|
-
if (
|
|
168
|
-
proj = JSON.parse(
|
|
1522
|
+
if (fs4.existsSync(projectConfig)) {
|
|
1523
|
+
proj = JSON.parse(fs4.readFileSync(projectConfig, "utf-8"));
|
|
169
1524
|
}
|
|
170
1525
|
if (!proj.mcp) proj.mcp = {};
|
|
171
1526
|
proj.mcp[name] = config;
|
|
172
|
-
|
|
1527
|
+
fs4.writeFileSync(projectConfig, JSON.stringify(proj, null, 2), "utf-8");
|
|
173
1528
|
} catch {
|
|
174
1529
|
}
|
|
175
1530
|
}
|
|
@@ -184,6 +1539,9 @@ async function fetchLiveMcpStatus(baseUrl) {
|
|
|
184
1539
|
}
|
|
185
1540
|
|
|
186
1541
|
// src/commands/interactive.ts
|
|
1542
|
+
import fs5 from "fs";
|
|
1543
|
+
import path5 from "path";
|
|
1544
|
+
import { execSync } from "child_process";
|
|
187
1545
|
var COMMANDS = [
|
|
188
1546
|
{ name: "/init", desc: "Initializes context / updates AGENTS.md" },
|
|
189
1547
|
{ name: "/goal", desc: "Sets/manages autonomous completion loops" },
|
|
@@ -194,6 +1552,17 @@ var COMMANDS = [
|
|
|
194
1552
|
{ name: "/connect", desc: "Launches popup setup to link & verify provider API keys" },
|
|
195
1553
|
{ name: "/mcp", desc: "Lists & configures Model Context Protocol servers" },
|
|
196
1554
|
{ name: "/diff", desc: "Inspect files modified in session" },
|
|
1555
|
+
{ name: "/diff-file", desc: "Inspect git diff for a specific file" },
|
|
1556
|
+
{ name: "/commit", desc: "Generate conventional commit or commit staged changes" },
|
|
1557
|
+
{ name: "/review", desc: "Review current git diff or target branch" },
|
|
1558
|
+
{ name: "/checkpoint", desc: "Snapshot workspace changes to git stash" },
|
|
1559
|
+
{ name: "/exec", desc: "Execute shell command directly (or use !<cmd>)" },
|
|
1560
|
+
{ name: "/open", desc: "Open a file in your default code editor" },
|
|
1561
|
+
{ name: "/add", desc: "Pin file(s) into model context memory" },
|
|
1562
|
+
{ name: "/context", desc: "Inspect pinned files and loaded memory" },
|
|
1563
|
+
{ name: "/drop", desc: "Remove pinned file from context (or /drop all)" },
|
|
1564
|
+
{ name: "/web", desc: "Fetch web page content and inject into context" },
|
|
1565
|
+
{ name: "/export", desc: "Export conversation transcript to Markdown" },
|
|
197
1566
|
{ name: "/agent", desc: "Switch mode (build / plan)" },
|
|
198
1567
|
{ name: "/compact", desc: "Compact & summarize session memory" },
|
|
199
1568
|
{ name: "/clear", desc: "Clear conversation canvas" },
|
|
@@ -202,17 +1571,17 @@ var COMMANDS = [
|
|
|
202
1571
|
{ name: "/exit", desc: "Exit the app" }
|
|
203
1572
|
];
|
|
204
1573
|
var THINKING_FRAMES = [
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
1574
|
+
chalk4.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
1575
|
+
chalk4.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
1576
|
+
chalk4.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
1577
|
+
chalk4.hex("#fef08a").bold(" \u224B Thinking..."),
|
|
1578
|
+
chalk4.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
1579
|
+
chalk4.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
1580
|
+
chalk4.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
1581
|
+
chalk4.dim(" \u224B Thinking ")
|
|
213
1582
|
];
|
|
214
1583
|
async function interactiveChatCommand(options) {
|
|
215
|
-
let currentBackendModel = options?.model ? getBackendModelId(options.model) :
|
|
1584
|
+
let currentBackendModel = options?.model ? getBackendModelId(options.model) : DEFAULT_MODEL_ID;
|
|
216
1585
|
if (!process.stdin.isTTY) {
|
|
217
1586
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
218
1587
|
const lines = [];
|
|
@@ -220,8 +1589,7 @@ async function interactiveChatCommand(options) {
|
|
|
220
1589
|
lines.push(line);
|
|
221
1590
|
}
|
|
222
1591
|
if (lines.length > 0) {
|
|
223
|
-
|
|
224
|
-
await runCommand2(lines, { model: currentBackendModel });
|
|
1592
|
+
await runCommand(lines, { model: currentBackendModel });
|
|
225
1593
|
}
|
|
226
1594
|
return;
|
|
227
1595
|
}
|
|
@@ -230,6 +1598,10 @@ async function interactiveChatCommand(options) {
|
|
|
230
1598
|
const conversationHistory = [];
|
|
231
1599
|
let session = await client.createSession("Ocean Session", process.cwd());
|
|
232
1600
|
let input = "";
|
|
1601
|
+
let cursorIndex = 0;
|
|
1602
|
+
const promptHistory = [];
|
|
1603
|
+
let historyIndex = -1;
|
|
1604
|
+
let tempSavedInput = "";
|
|
233
1605
|
let mode = "normal";
|
|
234
1606
|
let menuIndex = 0;
|
|
235
1607
|
let modelIndex = 0;
|
|
@@ -245,18 +1617,80 @@ async function interactiveChatCommand(options) {
|
|
|
245
1617
|
let userMsgId = null;
|
|
246
1618
|
let lastUserMsgId = null;
|
|
247
1619
|
let lastSubmittedPrompt = "";
|
|
1620
|
+
const pinnedContext = /* @__PURE__ */ new Map();
|
|
248
1621
|
const renderer = new StreamRenderer();
|
|
1622
|
+
let currentScrollBottom = 0;
|
|
1623
|
+
let lastBarStartRow = 0;
|
|
1624
|
+
let lastBarLinesCount = 0;
|
|
1625
|
+
const printToContent = (text) => {
|
|
1626
|
+
if (!process.stdout.isTTY) {
|
|
1627
|
+
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
process.stdout.write("\x1B8");
|
|
1631
|
+
const formatted = text.endsWith("\n") ? text : text + "\n";
|
|
1632
|
+
process.stdout.write(formatted);
|
|
1633
|
+
process.stdout.write("\x1B7");
|
|
1634
|
+
paintBar();
|
|
1635
|
+
};
|
|
1636
|
+
const getBarMetrics = (linesCount) => {
|
|
1637
|
+
const rows = process.stdout.rows || 24;
|
|
1638
|
+
const barRows = linesCount;
|
|
1639
|
+
const barStartRow = Math.max(1, rows - barRows + 1);
|
|
1640
|
+
const scrollBottom = Math.max(1, barStartRow - 1);
|
|
1641
|
+
return { rows, barRows, barStartRow, scrollBottom };
|
|
1642
|
+
};
|
|
1643
|
+
const applyScrollRegion = (scrollBottom) => {
|
|
1644
|
+
if (!process.stdout.isTTY) return;
|
|
1645
|
+
if (currentScrollBottom !== scrollBottom) {
|
|
1646
|
+
currentScrollBottom = scrollBottom;
|
|
1647
|
+
process.stdout.write(`\x1B[1;${scrollBottom}r`);
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
const resetScrollRegion = () => {
|
|
1651
|
+
if (!process.stdout.isTTY) return;
|
|
1652
|
+
const rows = process.stdout.rows || 24;
|
|
1653
|
+
process.stdout.write(`\x1B[1;${rows}r`);
|
|
1654
|
+
currentScrollBottom = 0;
|
|
1655
|
+
};
|
|
1656
|
+
const onSignal = () => {
|
|
1657
|
+
cleanup();
|
|
1658
|
+
process.exit(0);
|
|
1659
|
+
};
|
|
1660
|
+
process.on("SIGINT", onSignal);
|
|
1661
|
+
process.on("SIGTERM", onSignal);
|
|
249
1662
|
const cleanup = () => {
|
|
1663
|
+
process.off("SIGINT", onSignal);
|
|
1664
|
+
process.off("SIGTERM", onSignal);
|
|
250
1665
|
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
251
1666
|
if (process.stdout.isTTY) {
|
|
252
|
-
process.stdout.write("\x1B[
|
|
253
|
-
process.stdout.
|
|
1667
|
+
process.stdout.write("\x1B[?1000l\x1B[?1002l\x1B[?1006l");
|
|
1668
|
+
process.stdout.off("resize", onResize);
|
|
1669
|
+
resetScrollRegion();
|
|
1670
|
+
const rows = process.stdout.rows || 24;
|
|
1671
|
+
if (lastBarStartRow > 0) {
|
|
1672
|
+
for (let r = lastBarStartRow; r < lastBarStartRow + lastBarLinesCount; r++) {
|
|
1673
|
+
process.stdout.write(`\x1B[${r};1H\x1B[2K`);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
process.stdout.write(`\x1B[${rows};1H
|
|
1677
|
+
\x1B[?25h`);
|
|
254
1678
|
try {
|
|
255
1679
|
process.stdin.setRawMode(false);
|
|
256
1680
|
} catch {
|
|
257
1681
|
}
|
|
258
1682
|
}
|
|
259
1683
|
};
|
|
1684
|
+
const onResize = () => {
|
|
1685
|
+
if (!process.stdout.isTTY) return;
|
|
1686
|
+
const lines = buildBarLines();
|
|
1687
|
+
const { scrollBottom } = getBarMetrics(lines.length);
|
|
1688
|
+
applyScrollRegion(scrollBottom);
|
|
1689
|
+
paintBar();
|
|
1690
|
+
};
|
|
1691
|
+
if (process.stdout.isTTY) {
|
|
1692
|
+
process.stdout.on("resize", onResize);
|
|
1693
|
+
}
|
|
260
1694
|
const resetActivityTimeout = () => {
|
|
261
1695
|
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
262
1696
|
activityTimeoutTimer = setTimeout(() => {
|
|
@@ -264,11 +1698,9 @@ async function interactiveChatCommand(options) {
|
|
|
264
1698
|
isProcessing = false;
|
|
265
1699
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
266
1700
|
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
\u26A0\uFE0F Operation timed out waiting for ${friendlyName}. You can retry or switch models with /models.
|
|
271
|
-
`
|
|
1701
|
+
printToContent(
|
|
1702
|
+
chalk4.yellow(
|
|
1703
|
+
`\u26A0\uFE0F Operation timed out waiting for ${friendlyName}. You can retry or switch models with /models.`
|
|
272
1704
|
)
|
|
273
1705
|
);
|
|
274
1706
|
render();
|
|
@@ -276,146 +1708,188 @@ async function interactiveChatCommand(options) {
|
|
|
276
1708
|
}, 18e4);
|
|
277
1709
|
activityTimeoutTimer.unref();
|
|
278
1710
|
};
|
|
279
|
-
const BAR_ROWS = 3;
|
|
280
|
-
let currentBarRows = BAR_ROWS;
|
|
281
|
-
const applyScrollRegion = (rows) => {
|
|
282
|
-
const scrollBottom = Math.max(1, rows - currentBarRows);
|
|
283
|
-
process.stdout.write(`\x1B[1;${scrollBottom}r`);
|
|
284
|
-
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
285
|
-
};
|
|
286
|
-
const paintBar = (lines) => {
|
|
287
|
-
const rows = process.stdout.rows || 24;
|
|
288
|
-
const barStartRow = rows - lines.length + 1;
|
|
289
|
-
for (let i = 0; i < lines.length; i++) {
|
|
290
|
-
process.stdout.write(`\x1B[${barStartRow + i};1H\x1B[2K`);
|
|
291
|
-
process.stdout.write(lines[i]);
|
|
292
|
-
}
|
|
293
|
-
const scrollBottom = Math.max(1, rows - lines.length);
|
|
294
|
-
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
295
|
-
};
|
|
296
1711
|
const buildBarLines = () => {
|
|
297
1712
|
const cols = process.stdout.columns || 80;
|
|
298
1713
|
const friendlyModel = getFriendlyModelName(currentBackendModel);
|
|
299
|
-
const bar =
|
|
1714
|
+
const bar = chalk4.hex(OCEAN_BLUE)("\u2502");
|
|
300
1715
|
const width = Math.min(cols - 4, 80);
|
|
301
|
-
const peachBg =
|
|
1716
|
+
const peachBg = chalk4.bgHex("#fba978").black;
|
|
302
1717
|
const lines = [];
|
|
303
1718
|
if (mode === "model_selector") {
|
|
304
|
-
const header = "Select Model (\u2191/\u2193 to navigate, Enter to select, Esc to close)" + " ".repeat(Math.max(2, width - 60)) +
|
|
305
|
-
lines.push(
|
|
1719
|
+
const header = "Select Model (\u2191/\u2193 to navigate, Enter to select, Esc to close)" + " ".repeat(Math.max(2, width - 60)) + chalk4.dim("esc");
|
|
1720
|
+
lines.push(chalk4.bold.white(header));
|
|
306
1721
|
lines.push(
|
|
307
|
-
modelSearch ? `Search: ${modelSearch}` :
|
|
1722
|
+
modelSearch ? `Search: ${modelSearch}` : chalk4.dim("Search models (e.g. qwen, step, flash)")
|
|
308
1723
|
);
|
|
309
1724
|
lines.push("");
|
|
310
1725
|
const availableModels = getAvailableOceanModels();
|
|
311
1726
|
const searchLower = modelSearch.toLowerCase();
|
|
312
1727
|
const filtered = availableModels.filter(
|
|
313
|
-
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower)
|
|
1728
|
+
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower)
|
|
314
1729
|
);
|
|
315
1730
|
if (modelIndex >= filtered.length) modelIndex = Math.max(0, filtered.length - 1);
|
|
316
1731
|
if (filtered.length === 0) {
|
|
317
|
-
lines.push(
|
|
1732
|
+
lines.push(chalk4.dim(" No matching models found."));
|
|
318
1733
|
} else {
|
|
319
|
-
|
|
1734
|
+
const MAX_MODELS = 8;
|
|
1735
|
+
let startIdx = 0;
|
|
1736
|
+
if (filtered.length > MAX_MODELS) {
|
|
1737
|
+
startIdx = Math.max(0, Math.min(modelIndex - 3, filtered.length - MAX_MODELS));
|
|
1738
|
+
}
|
|
1739
|
+
const visibleModels = filtered.slice(startIdx, startIdx + MAX_MODELS);
|
|
1740
|
+
if (filtered.length > MAX_MODELS) {
|
|
1741
|
+
lines.push(chalk4.dim(` Models (${modelIndex + 1}/${filtered.length}) \xB7 \u2191/\u2193 to navigate`));
|
|
1742
|
+
}
|
|
1743
|
+
visibleModels.forEach((m, relIdx) => {
|
|
1744
|
+
const idx = startIdx + relIdx;
|
|
320
1745
|
const isCurrent = m.id === currentBackendModel;
|
|
321
1746
|
const prefix = isCurrent ? "\u25CF " : " ";
|
|
322
1747
|
const left = prefix + m.name;
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const row = left + " ".repeat(spaces) + tag;
|
|
1748
|
+
const spaces = Math.max(0, width - left.length);
|
|
1749
|
+
const row = left + " ".repeat(spaces);
|
|
326
1750
|
if (idx === modelIndex) {
|
|
327
1751
|
lines.push(peachBg(row));
|
|
328
|
-
if (m.description) lines.push(chalk.dim(` ${m.description}`));
|
|
329
1752
|
} else {
|
|
330
|
-
lines.push(
|
|
1753
|
+
lines.push(chalk4.white(row));
|
|
331
1754
|
}
|
|
332
1755
|
});
|
|
333
1756
|
}
|
|
334
|
-
lines.push("");
|
|
335
|
-
lines.push(chalk.dim("Type /connect to link API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq)."));
|
|
336
1757
|
} else if (mode === "connect_modal") {
|
|
337
|
-
const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) +
|
|
338
|
-
lines.push(
|
|
1758
|
+
const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) + chalk4.dim("esc");
|
|
1759
|
+
lines.push(chalk4.bold.white(header));
|
|
339
1760
|
if (connectStep === "provider_list") {
|
|
340
|
-
lines.push(
|
|
1761
|
+
lines.push(chalk4.dim("Select provider with \u2191/\u2193 and press Enter (or 'd' to disconnect):"));
|
|
341
1762
|
lines.push("");
|
|
342
1763
|
const creds = getStoredCredentials();
|
|
343
1764
|
SUPPORTED_BYOK_PROVIDERS.forEach((p, idx) => {
|
|
344
1765
|
const isConnected = Boolean(creds[p.id]?.key);
|
|
345
1766
|
const prefix = isConnected ? "\u25CF " : "\u25CB ";
|
|
346
1767
|
const left = ` ${prefix}${p.name.padEnd(16)} (${p.id})`;
|
|
347
|
-
const tag = isConnected ?
|
|
1768
|
+
const tag = isConnected ? chalk4.green("\u2714 Connected") : chalk4.dim("Not linked");
|
|
348
1769
|
const spaces = Math.max(2, width - left.length - 12);
|
|
349
1770
|
const row = left + " ".repeat(spaces) + tag;
|
|
350
|
-
lines.push(idx === connectProviderIndex ? peachBg(row) :
|
|
1771
|
+
lines.push(idx === connectProviderIndex ? peachBg(row) : chalk4.white(row));
|
|
351
1772
|
});
|
|
352
1773
|
lines.push("");
|
|
353
|
-
lines.push(
|
|
1774
|
+
lines.push(chalk4.dim("Enter: Link API Key \u2022 d: Disconnect \u2022 Esc: Close"));
|
|
354
1775
|
} else if (connectStep === "enter_key") {
|
|
355
1776
|
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
356
|
-
lines.push(
|
|
357
|
-
lines.push(
|
|
1777
|
+
lines.push(chalk4.bold.hex("#38bdf8")(`Provider: ${prov.name}`));
|
|
1778
|
+
lines.push(chalk4.dim(`Placeholder format: ${prov.placeholder}`));
|
|
358
1779
|
lines.push("");
|
|
359
1780
|
const masked = connectKeyInput.length > 8 ? connectKeyInput.slice(0, 4) + "\u2022".repeat(connectKeyInput.length - 8) + connectKeyInput.slice(-4) : "\u2022".repeat(connectKeyInput.length);
|
|
360
|
-
const displayKey = connectKeyInput ? masked :
|
|
1781
|
+
const displayKey = connectKeyInput ? masked : chalk4.dim("Paste or type API key here");
|
|
361
1782
|
lines.push(`${bar} Key: ${displayKey}\u2588`);
|
|
362
1783
|
if (connectErrorMsg) {
|
|
363
1784
|
lines.push("");
|
|
364
|
-
lines.push(
|
|
1785
|
+
lines.push(chalk4.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
|
|
365
1786
|
}
|
|
366
1787
|
lines.push("");
|
|
367
|
-
lines.push(
|
|
1788
|
+
lines.push(chalk4.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
|
|
368
1789
|
} else if (connectStep === "validating") {
|
|
369
|
-
lines.push(
|
|
1790
|
+
lines.push(chalk4.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
|
|
370
1791
|
lines.push("");
|
|
371
|
-
lines.push(
|
|
1792
|
+
lines.push(chalk4.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
|
|
372
1793
|
lines.push("");
|
|
373
|
-
lines.push(
|
|
1794
|
+
lines.push(chalk4.dim("Please wait while we verify your key with the provider."));
|
|
374
1795
|
}
|
|
375
1796
|
} else {
|
|
376
1797
|
if (!isProcessing && input.startsWith("/")) {
|
|
377
1798
|
const matching = COMMANDS.filter((c) => c.name.startsWith(input));
|
|
378
1799
|
if (menuIndex >= matching.length) menuIndex = Math.max(0, matching.length - 1);
|
|
379
1800
|
if (matching.length > 0) {
|
|
380
|
-
|
|
381
|
-
|
|
1801
|
+
const MAX_VISIBLE = 6;
|
|
1802
|
+
let startIdx = 0;
|
|
1803
|
+
if (matching.length > MAX_VISIBLE) {
|
|
1804
|
+
startIdx = Math.max(0, Math.min(menuIndex - 2, matching.length - MAX_VISIBLE));
|
|
1805
|
+
}
|
|
1806
|
+
const visible = matching.slice(startIdx, startIdx + MAX_VISIBLE);
|
|
1807
|
+
lines.push(
|
|
1808
|
+
chalk4.dim(` Commands (${menuIndex + 1}/${matching.length}) \xB7 \u2191/\u2193 navigate \xB7 Tab/Enter select \xB7 Esc cancel`)
|
|
1809
|
+
);
|
|
1810
|
+
visible.forEach((c, relIdx) => {
|
|
1811
|
+
const absIdx = startIdx + relIdx;
|
|
1812
|
+
const isSelected = absIdx === menuIndex;
|
|
1813
|
+
const row = " " + c.name.padEnd(15) + c.desc;
|
|
382
1814
|
const padded = row + " ".repeat(Math.max(2, width - row.length));
|
|
383
|
-
lines.push(
|
|
1815
|
+
lines.push(isSelected ? peachBg(padded) : chalk4.white(row));
|
|
384
1816
|
});
|
|
385
1817
|
}
|
|
386
1818
|
}
|
|
387
1819
|
const friendlyAgent = currentAgent === "build" ? "Build" : "Plan";
|
|
388
|
-
|
|
389
|
-
lines.push(
|
|
1820
|
+
const bar2 = chalk4.hex(OCEAN_BLUE)("\u2502");
|
|
1821
|
+
lines.push(bar2);
|
|
1822
|
+
if (isProcessing) {
|
|
1823
|
+
const frameIdx = renderer.getThinkingFrame() % THINKING_FRAMES.length;
|
|
1824
|
+
const thinkingText = THINKING_FRAMES[frameIdx].trim();
|
|
1825
|
+
lines.push(`${bar2} ${chalk4.dim(thinkingText)}`);
|
|
1826
|
+
} else {
|
|
1827
|
+
lines.push(`${bar2} ${input}`);
|
|
1828
|
+
}
|
|
1829
|
+
lines.push(bar2);
|
|
390
1830
|
lines.push(
|
|
391
|
-
|
|
1831
|
+
`${bar2} ${chalk4.hex(OCEAN_BLUE).bold(friendlyAgent)} ${chalk4.dim("\xB7")} ${chalk4.white(friendlyModel)} ${chalk4.dim("OceanCode")}`
|
|
392
1832
|
);
|
|
1833
|
+
const normCwd = process.cwd().replace(/\\/g, "/");
|
|
1834
|
+
const leftStatus = chalk4.dim(normCwd);
|
|
1835
|
+
const midStatus = `${chalk4.bold.white("ctrl+p")} ${chalk4.dim("commands")}`;
|
|
1836
|
+
const rightStatus = `${chalk4.green("\u25CF")} ${chalk4.cyan("OceanCode 0.1.2")}`;
|
|
1837
|
+
const termWidth = process.stdout.columns || 80;
|
|
1838
|
+
const plainLeft = normCwd;
|
|
1839
|
+
const plainMid = "ctrl+p commands";
|
|
1840
|
+
const plainRight = "\u25CF OceanCode 0.1.2";
|
|
1841
|
+
const totalLen = plainLeft.length + plainMid.length + plainRight.length;
|
|
1842
|
+
const availableSpace = Math.max(2, termWidth - totalLen - 2);
|
|
1843
|
+
const padLeft = " ".repeat(Math.max(2, Math.floor(availableSpace / 2)));
|
|
1844
|
+
const padRight = " ".repeat(Math.max(2, availableSpace - padLeft.length));
|
|
1845
|
+
lines.push(`${leftStatus}${padLeft}${midStatus}${padRight}${rightStatus}`);
|
|
393
1846
|
}
|
|
394
1847
|
return lines;
|
|
395
1848
|
};
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
applyScrollRegion(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1849
|
+
const paintBar = () => {
|
|
1850
|
+
try {
|
|
1851
|
+
if (!process.stdout.isTTY) return;
|
|
1852
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
1853
|
+
const lines = buildBarLines();
|
|
1854
|
+
const { rows, barStartRow, scrollBottom } = getBarMetrics(lines.length);
|
|
1855
|
+
applyScrollRegion(scrollBottom);
|
|
1856
|
+
if (lastBarStartRow > 0) {
|
|
1857
|
+
if (lastBarStartRow !== barStartRow) {
|
|
1858
|
+
for (let r = lastBarStartRow; r < lastBarStartRow + lastBarLinesCount; r++) {
|
|
1859
|
+
process.stdout.write(`\x1B[${r};1H\x1B[2K`);
|
|
1860
|
+
}
|
|
1861
|
+
} else if (lastBarLinesCount > lines.length) {
|
|
1862
|
+
for (let r = barStartRow + lines.length; r < barStartRow + lastBarLinesCount; r++) {
|
|
1863
|
+
process.stdout.write(`\x1B[${r};1H\x1B[2K`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
lastBarStartRow = barStartRow;
|
|
1868
|
+
lastBarLinesCount = lines.length;
|
|
1869
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1870
|
+
const r = barStartRow + i;
|
|
1871
|
+
process.stdout.write(`\x1B[${r};1H\x1B[2K${lines[i]}`);
|
|
1872
|
+
}
|
|
1873
|
+
if (!isProcessing && mode === "normal") {
|
|
1874
|
+
process.stdout.write("\x1B[?25h");
|
|
1875
|
+
const targetRow = barStartRow + 1;
|
|
1876
|
+
const col = Math.min(process.stdout.columns || 80, 3 + cursorIndex);
|
|
1877
|
+
process.stdout.write(`\x1B[${targetRow};${col}H`);
|
|
1878
|
+
} else if (!isProcessing && mode === "model_selector") {
|
|
1879
|
+
process.stdout.write("\x1B[?25h");
|
|
1880
|
+
const col = Math.min(process.stdout.columns || 80, 9 + modelSearch.length);
|
|
1881
|
+
process.stdout.write(`\x1B[${barStartRow + 1};${col}H`);
|
|
1882
|
+
} else {
|
|
1883
|
+
process.stdout.write("\x1B[?25l\x1B8");
|
|
1884
|
+
}
|
|
1885
|
+
} catch {
|
|
412
1886
|
}
|
|
413
|
-
const scrollBottom = Math.max(1, rows - currentBarRows);
|
|
414
|
-
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
415
1887
|
};
|
|
1888
|
+
const render = () => paintBar();
|
|
416
1889
|
renderer.setPinBar(
|
|
417
|
-
() => paintBar(
|
|
418
|
-
() =>
|
|
1890
|
+
() => paintBar(),
|
|
1891
|
+
() => {
|
|
1892
|
+
}
|
|
419
1893
|
);
|
|
420
1894
|
const reasoningPartIds = /* @__PURE__ */ new Set();
|
|
421
1895
|
const textPartIds = /* @__PURE__ */ new Set();
|
|
@@ -444,8 +1918,7 @@ async function interactiveChatCommand(options) {
|
|
|
444
1918
|
}
|
|
445
1919
|
} else if (part.type === "tool") {
|
|
446
1920
|
const toolName = part.tool || "tool";
|
|
447
|
-
const
|
|
448
|
-
const detail = inputObj.filePath || inputObj.command || inputObj.pattern || inputObj.query || inputObj.url || inputObj.name || part.state?.title || part.call?.command || part.call?.path || part.call?.description || "";
|
|
1921
|
+
const detail = extractToolDetail(part);
|
|
449
1922
|
const status = part.state?.status || part.status || "";
|
|
450
1923
|
renderer.handleTool(toolName, detail, part.id, status);
|
|
451
1924
|
} else if (part.type === "text" && typeof part.text === "string") {
|
|
@@ -454,8 +1927,8 @@ async function interactiveChatCommand(options) {
|
|
|
454
1927
|
}
|
|
455
1928
|
}
|
|
456
1929
|
if (event.type === "message.part.delta") {
|
|
457
|
-
const partId = event.properties?.partID;
|
|
458
|
-
if (
|
|
1930
|
+
const partId = event.properties?.partID || event.properties?.partId || event.properties?.id || "default";
|
|
1931
|
+
if (reasoningPartIds.has(partId)) return;
|
|
459
1932
|
const delta = event.properties?.delta;
|
|
460
1933
|
const field = event.properties?.field;
|
|
461
1934
|
if (field === "text" && typeof delta === "string") {
|
|
@@ -477,9 +1950,7 @@ async function interactiveChatCommand(options) {
|
|
|
477
1950
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
478
1951
|
const errObj = event.properties?.error;
|
|
479
1952
|
const errMsg = errObj?.message || errObj?.data?.message || (typeof errObj === "string" ? errObj : "Model request failed or timed out.");
|
|
480
|
-
|
|
481
|
-
\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}
|
|
482
|
-
`));
|
|
1953
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}`));
|
|
483
1954
|
render();
|
|
484
1955
|
}
|
|
485
1956
|
}, void 0, session.directory);
|
|
@@ -493,7 +1964,7 @@ async function interactiveChatCommand(options) {
|
|
|
493
1964
|
cleanup();
|
|
494
1965
|
unsubscribe();
|
|
495
1966
|
console.log(`
|
|
496
|
-
${
|
|
1967
|
+
${chalk4.cyan("\u{1F44B} Goodbye!")}
|
|
497
1968
|
`);
|
|
498
1969
|
process.exit(0);
|
|
499
1970
|
}
|
|
@@ -535,8 +2006,7 @@ async function interactiveChatCommand(options) {
|
|
|
535
2006
|
if (str === "d" || str === "D") {
|
|
536
2007
|
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
537
2008
|
removeProviderKey(prov.id);
|
|
538
|
-
|
|
539
|
-
console.log(chalk.yellow(`
|
|
2009
|
+
printToContent(chalk4.yellow(`
|
|
540
2010
|
\u2713 Disconnected API key for ${prov.name}.
|
|
541
2011
|
`));
|
|
542
2012
|
render();
|
|
@@ -569,9 +2039,8 @@ async function interactiveChatCommand(options) {
|
|
|
569
2039
|
connectStep = "provider_list";
|
|
570
2040
|
connectKeyInput = "";
|
|
571
2041
|
connectErrorMsg = "";
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
chalk.green(
|
|
2042
|
+
printToContent(
|
|
2043
|
+
chalk4.green(
|
|
575
2044
|
`
|
|
576
2045
|
\u2713 Successfully verified and connected ${prov.name}! Its models are now unlocked in /models.
|
|
577
2046
|
`
|
|
@@ -598,7 +2067,7 @@ async function interactiveChatCommand(options) {
|
|
|
598
2067
|
const availableModels = getAvailableOceanModels();
|
|
599
2068
|
const searchLower = modelSearch.toLowerCase();
|
|
600
2069
|
const filtered = availableModels.filter(
|
|
601
|
-
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower) || m.tag.toLowerCase().includes(searchLower) || m.description.toLowerCase().includes(searchLower)
|
|
2070
|
+
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower) || (m.tag ? m.tag.toLowerCase().includes(searchLower) : false) || (m.description ? m.description.toLowerCase().includes(searchLower) : false)
|
|
602
2071
|
);
|
|
603
2072
|
if (key.name === "escape") {
|
|
604
2073
|
mode = "normal";
|
|
@@ -631,11 +2100,8 @@ async function interactiveChatCommand(options) {
|
|
|
631
2100
|
mode = "normal";
|
|
632
2101
|
input = "";
|
|
633
2102
|
modelSearch = "";
|
|
634
|
-
clearPromptArea();
|
|
635
2103
|
if (selectedFriendly) {
|
|
636
|
-
|
|
637
|
-
\u2713 Active model changed to: ${selectedFriendly} (${selectedId})
|
|
638
|
-
`));
|
|
2104
|
+
printToContent(chalk4.green(`\u2713 Active model changed to: ${selectedFriendly} (${selectedId})`));
|
|
639
2105
|
}
|
|
640
2106
|
render();
|
|
641
2107
|
return;
|
|
@@ -661,107 +2127,129 @@ async function interactiveChatCommand(options) {
|
|
|
661
2127
|
render();
|
|
662
2128
|
return;
|
|
663
2129
|
}
|
|
664
|
-
if (
|
|
665
|
-
if (
|
|
2130
|
+
if (key.name === "up") {
|
|
2131
|
+
if (matching.length > 0) {
|
|
666
2132
|
menuIndex = (menuIndex - 1 + matching.length) % matching.length;
|
|
667
2133
|
render();
|
|
668
2134
|
return;
|
|
2135
|
+
} else if (promptHistory.length > 0) {
|
|
2136
|
+
if (historyIndex === -1) {
|
|
2137
|
+
tempSavedInput = input;
|
|
2138
|
+
historyIndex = promptHistory.length - 1;
|
|
2139
|
+
} else if (historyIndex > 0) {
|
|
2140
|
+
historyIndex--;
|
|
2141
|
+
}
|
|
2142
|
+
input = promptHistory[historyIndex] || "";
|
|
2143
|
+
cursorIndex = input.length;
|
|
2144
|
+
render();
|
|
2145
|
+
return;
|
|
669
2146
|
}
|
|
670
|
-
|
|
2147
|
+
}
|
|
2148
|
+
if (key.name === "down") {
|
|
2149
|
+
if (matching.length > 0) {
|
|
671
2150
|
menuIndex = (menuIndex + 1) % matching.length;
|
|
672
2151
|
render();
|
|
673
2152
|
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
input =
|
|
2153
|
+
} else if (historyIndex !== -1) {
|
|
2154
|
+
if (historyIndex < promptHistory.length - 1) {
|
|
2155
|
+
historyIndex++;
|
|
2156
|
+
input = promptHistory[historyIndex] || "";
|
|
2157
|
+
} else {
|
|
2158
|
+
historyIndex = -1;
|
|
2159
|
+
input = tempSavedInput;
|
|
678
2160
|
}
|
|
2161
|
+
cursorIndex = input.length;
|
|
679
2162
|
render();
|
|
680
2163
|
return;
|
|
681
2164
|
}
|
|
682
2165
|
}
|
|
2166
|
+
if (key.name === "tab") {
|
|
2167
|
+
if (matching.length > 0 && matching[menuIndex]) {
|
|
2168
|
+
input = matching[menuIndex].name;
|
|
2169
|
+
cursorIndex = input.length;
|
|
2170
|
+
}
|
|
2171
|
+
render();
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
683
2174
|
if (key.name === "return") {
|
|
684
2175
|
let submitted = input.trim();
|
|
685
2176
|
if (matching.length > 0 && matching[menuIndex]) {
|
|
686
2177
|
submitted = matching[menuIndex].name;
|
|
687
2178
|
}
|
|
688
2179
|
input = "";
|
|
2180
|
+
cursorIndex = 0;
|
|
2181
|
+
historyIndex = -1;
|
|
689
2182
|
menuIndex = 0;
|
|
690
2183
|
if (!submitted) {
|
|
691
|
-
|
|
2184
|
+
paintBar();
|
|
692
2185
|
return;
|
|
693
2186
|
}
|
|
2187
|
+
if (promptHistory[promptHistory.length - 1] !== submitted) {
|
|
2188
|
+
promptHistory.push(submitted);
|
|
2189
|
+
}
|
|
694
2190
|
if (submitted === "/exit" || submitted === "/quit") {
|
|
695
2191
|
cleanup();
|
|
696
2192
|
unsubscribe();
|
|
697
2193
|
console.log(`
|
|
698
|
-
${
|
|
2194
|
+
${chalk4.cyan("\u{1F44B} Goodbye!")}
|
|
699
2195
|
`);
|
|
700
2196
|
process.exit(0);
|
|
701
2197
|
}
|
|
702
2198
|
if (submitted === "/clear") {
|
|
703
|
-
clearPromptArea();
|
|
704
2199
|
if (process.stdout.isTTY) {
|
|
705
2200
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2201
|
+
const { scrollBottom } = getBarMetrics(3);
|
|
2202
|
+
applyScrollRegion(scrollBottom);
|
|
2203
|
+
process.stdout.write("\x1B[1;1H");
|
|
706
2204
|
}
|
|
707
2205
|
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
708
|
-
|
|
2206
|
+
process.stdout.write("\x1B7");
|
|
2207
|
+
lastBarStartRow = 0;
|
|
2208
|
+
paintBar();
|
|
709
2209
|
return;
|
|
710
2210
|
}
|
|
711
2211
|
if (submitted === "/init") {
|
|
712
|
-
clearPromptArea();
|
|
713
2212
|
try {
|
|
714
2213
|
const res = initializeAgentsDoc(process.cwd());
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
console.log(chalk.dim(` Location: ${res.filePath}
|
|
718
|
-
`));
|
|
2214
|
+
printToContent(`${chalk4.green(`\u2714 ${res.summary}`)}
|
|
2215
|
+
${chalk4.dim(` Location: ${res.filePath}`)}`);
|
|
719
2216
|
} catch (err) {
|
|
720
|
-
|
|
721
|
-
\u26A0\uFE0F Initialization failed: ${err.message}
|
|
722
|
-
`));
|
|
2217
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Initialization failed: ${err.message}`));
|
|
723
2218
|
}
|
|
724
2219
|
render();
|
|
725
2220
|
return;
|
|
726
2221
|
}
|
|
727
2222
|
if (submitted === "/new") {
|
|
728
|
-
clearPromptArea();
|
|
729
2223
|
try {
|
|
730
2224
|
session = await client.createSession("Ocean Session", process.cwd());
|
|
731
|
-
|
|
2225
|
+
printToContent(chalk4.green("\u2714 Started a new coding session."));
|
|
732
2226
|
} catch (err) {
|
|
733
|
-
|
|
734
|
-
Failed to create new session: ${err.message}
|
|
735
|
-
`));
|
|
2227
|
+
printToContent(chalk4.red(`Failed to create new session: ${err.message}`));
|
|
736
2228
|
}
|
|
737
2229
|
render();
|
|
738
2230
|
return;
|
|
739
2231
|
}
|
|
740
2232
|
if (submitted === "/undo") {
|
|
741
|
-
clearPromptArea();
|
|
742
2233
|
if (lastUserMsgId) {
|
|
743
2234
|
try {
|
|
744
2235
|
const success = await client.revertSession(session.id, lastUserMsgId);
|
|
745
2236
|
if (success) {
|
|
746
|
-
|
|
2237
|
+
printToContent(chalk4.green("\u2714 Reverted the last AI action and restored previous file states."));
|
|
747
2238
|
} else {
|
|
748
|
-
|
|
2239
|
+
printToContent(chalk4.yellow("Could not revert the last action."));
|
|
749
2240
|
}
|
|
750
2241
|
} catch (err) {
|
|
751
|
-
|
|
752
|
-
Revert failed: ${err.message}
|
|
753
|
-
`));
|
|
2242
|
+
printToContent(chalk4.red(`Revert failed: ${err.message}`));
|
|
754
2243
|
}
|
|
755
2244
|
} else {
|
|
756
|
-
|
|
2245
|
+
printToContent(chalk4.yellow("No previous action found to undo."));
|
|
757
2246
|
}
|
|
758
2247
|
render();
|
|
759
2248
|
return;
|
|
760
2249
|
}
|
|
761
2250
|
if (submitted === "/redo") {
|
|
762
2251
|
if (!lastSubmittedPrompt) {
|
|
763
|
-
|
|
764
|
-
console.log(chalk.yellow("\nNo previous action to redo.\n"));
|
|
2252
|
+
printToContent(chalk4.yellow("No previous action to redo."));
|
|
765
2253
|
render();
|
|
766
2254
|
return;
|
|
767
2255
|
}
|
|
@@ -770,10 +2258,9 @@ Revert failed: ${err.message}
|
|
|
770
2258
|
if (submitted.startsWith("/goal")) {
|
|
771
2259
|
const goalDesc = submitted.replace(/^\/goal\s*/, "").trim();
|
|
772
2260
|
if (!goalDesc) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
"\nPlease provide a goal description.\nUsage: /goal <your objective or feature description>\nExample: /goal Add user authentication with JWT and refresh tokens\n"
|
|
2261
|
+
printToContent(
|
|
2262
|
+
chalk4.yellow(
|
|
2263
|
+
"Please provide a goal description.\nUsage: /goal <your objective or feature description>\nExample: /goal Add user authentication with JWT and refresh tokens"
|
|
777
2264
|
)
|
|
778
2265
|
);
|
|
779
2266
|
render();
|
|
@@ -794,22 +2281,17 @@ Please accomplish this goal systematically:
|
|
|
794
2281
|
const provArg = parts[1]?.toLowerCase();
|
|
795
2282
|
const keyArg = parts[2];
|
|
796
2283
|
if (provArg && keyArg) {
|
|
797
|
-
|
|
798
|
-
console.log(chalk.cyan(`
|
|
799
|
-
Validating API key with ${provArg.toUpperCase()}...`));
|
|
2284
|
+
printToContent(chalk4.cyan(`Validating API key with ${provArg.toUpperCase()}...`));
|
|
800
2285
|
const val = await validateProviderKey(provArg, keyArg);
|
|
801
2286
|
if (val.valid) {
|
|
802
2287
|
saveProviderKey(provArg, keyArg);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
`\u2714 Successfully verified and connected ${provArg.toUpperCase()}! Its models are now unlocked in /models
|
|
806
|
-
`
|
|
2288
|
+
printToContent(
|
|
2289
|
+
chalk4.green(
|
|
2290
|
+
`\u2714 Successfully verified and connected ${provArg.toUpperCase()}! Its models are now unlocked in /models.`
|
|
807
2291
|
)
|
|
808
2292
|
);
|
|
809
2293
|
} else {
|
|
810
|
-
|
|
811
|
-
\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}
|
|
812
|
-
`));
|
|
2294
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}`));
|
|
813
2295
|
}
|
|
814
2296
|
render();
|
|
815
2297
|
return;
|
|
@@ -824,16 +2306,15 @@ Validating API key with ${provArg.toUpperCase()}...`));
|
|
|
824
2306
|
}
|
|
825
2307
|
}
|
|
826
2308
|
if (submitted.startsWith("/mcp")) {
|
|
827
|
-
clearPromptArea();
|
|
828
2309
|
const parts = submitted.split(" ").filter(Boolean);
|
|
829
2310
|
const sub = parts[1]?.toLowerCase();
|
|
830
2311
|
if (sub === "add") {
|
|
831
2312
|
const serverName = parts[2];
|
|
832
2313
|
const serverCmdOrUrl = parts.slice(3).join(" ");
|
|
833
2314
|
if (!serverName || !serverCmdOrUrl) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
"
|
|
2315
|
+
printToContent(
|
|
2316
|
+
chalk4.yellow(
|
|
2317
|
+
"Usage: /mcp add <server-name> <command-or-url>\nExamples:\n /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .\n /mcp add remote https://example.com/mcp"
|
|
837
2318
|
)
|
|
838
2319
|
);
|
|
839
2320
|
render();
|
|
@@ -842,10 +2323,8 @@ Validating API key with ${provArg.toUpperCase()}...`));
|
|
|
842
2323
|
const isUrl = serverCmdOrUrl.startsWith("http://") || serverCmdOrUrl.startsWith("https://");
|
|
843
2324
|
const mcpConfig = isUrl ? { type: "remote", url: serverCmdOrUrl, enabled: true } : { type: "local", command: serverCmdOrUrl.split(" "), enabled: true };
|
|
844
2325
|
saveMcpServer(serverName, mcpConfig);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.
|
|
848
|
-
`)
|
|
2326
|
+
printToContent(
|
|
2327
|
+
chalk4.green(`\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.`)
|
|
849
2328
|
);
|
|
850
2329
|
render();
|
|
851
2330
|
return;
|
|
@@ -854,9 +2333,9 @@ Validating API key with ${provArg.toUpperCase()}...`));
|
|
|
854
2333
|
const live = await fetchLiveMcpStatus(baseUrl);
|
|
855
2334
|
const names = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
856
2335
|
if (names.length === 0) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
"
|
|
2336
|
+
printToContent(
|
|
2337
|
+
chalk4.dim(
|
|
2338
|
+
"No Model Context Protocol (MCP) servers configured.\n\nTo add an MCP server, run:\n /mcp add <name> <command-or-url>\nExample:\n /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem ."
|
|
860
2339
|
)
|
|
861
2340
|
);
|
|
862
2341
|
} else {
|
|
@@ -865,19 +2344,18 @@ Validating API key with ${provArg.toUpperCase()}...`));
|
|
|
865
2344
|
const isLive = live[n] !== void 0;
|
|
866
2345
|
const type = conf.type || (conf.url ? "remote" : "local");
|
|
867
2346
|
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
868
|
-
const badge = isLive ?
|
|
2347
|
+
const badge = isLive ? chalk4.green("\u25CF Connected") : chalk4.cyan("\u25CF Configured");
|
|
869
2348
|
return ` \u2022 ${n} (${type}): ${badge}
|
|
870
|
-
${
|
|
2349
|
+
${chalk4.dim(target)}`;
|
|
871
2350
|
}).join("\n");
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
2351
|
+
printToContent(
|
|
2352
|
+
chalk4.bold("Model Context Protocol (MCP) Servers:\n") + lines + chalk4.dim("\n\nAdd new servers with: /mcp add <name> <command>")
|
|
2353
|
+
);
|
|
875
2354
|
}
|
|
876
2355
|
render();
|
|
877
2356
|
return;
|
|
878
2357
|
}
|
|
879
2358
|
if (submitted === "/agent" || submitted.startsWith("/agent ") || submitted === "/mode" || submitted.startsWith("/mode ")) {
|
|
880
|
-
clearPromptArea();
|
|
881
2359
|
if (submitted.includes(" ")) {
|
|
882
2360
|
const target = submitted.split(" ")[1].toLowerCase().trim();
|
|
883
2361
|
if (target === "plan" || target === "build") {
|
|
@@ -888,43 +2366,34 @@ Validating API key with ${provArg.toUpperCase()}...`));
|
|
|
888
2366
|
} else {
|
|
889
2367
|
currentAgent = currentAgent === "build" ? "plan" : "build";
|
|
890
2368
|
}
|
|
891
|
-
|
|
892
|
-
\u2713 Switched mode to: ${currentAgent.toUpperCase()}
|
|
893
|
-
`));
|
|
2369
|
+
printToContent(chalk4.cyan(`\u2713 Switched mode to: ${currentAgent.toUpperCase()}`));
|
|
894
2370
|
render();
|
|
895
2371
|
return;
|
|
896
2372
|
}
|
|
897
2373
|
if (submitted === "/diff") {
|
|
898
|
-
clearPromptArea();
|
|
899
2374
|
try {
|
|
900
2375
|
const diffs = await client.getSessionDiff(session.id);
|
|
901
2376
|
if (!diffs || diffs.length === 0) {
|
|
902
|
-
|
|
2377
|
+
printToContent(chalk4.dim("No file modifications recorded in the current session."));
|
|
903
2378
|
} else {
|
|
904
2379
|
const diffSummary = diffs.map(
|
|
905
2380
|
(d) => ` \u2022 ${d.path || d.file || "file"} (+${d.additions || 0} -${d.deletions || 0})`
|
|
906
2381
|
).join("\n");
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
console.log(diffSummary + "\n");
|
|
2382
|
+
printToContent(chalk4.bold(`Modified Files (${diffs.length}):
|
|
2383
|
+
`) + diffSummary);
|
|
910
2384
|
}
|
|
911
2385
|
} catch (err) {
|
|
912
|
-
|
|
913
|
-
Failed to retrieve diff: ${err.message}
|
|
914
|
-
`));
|
|
2386
|
+
printToContent(chalk4.red(`Failed to retrieve diff: ${err.message}`));
|
|
915
2387
|
}
|
|
916
2388
|
render();
|
|
917
2389
|
return;
|
|
918
2390
|
}
|
|
919
2391
|
if (submitted === "/compact") {
|
|
920
|
-
clearPromptArea();
|
|
921
2392
|
try {
|
|
922
2393
|
await client.summarizeSession(session.id);
|
|
923
|
-
|
|
2394
|
+
printToContent(chalk4.green("\u2713 Session context memory compacted and summarized successfully."));
|
|
924
2395
|
} catch (err) {
|
|
925
|
-
|
|
926
|
-
Compact failed: ${err.message}
|
|
927
|
-
`));
|
|
2396
|
+
printToContent(chalk4.red(`Compact failed: ${err.message}`));
|
|
928
2397
|
}
|
|
929
2398
|
render();
|
|
930
2399
|
return;
|
|
@@ -933,13 +2402,10 @@ Compact failed: ${err.message}
|
|
|
933
2402
|
const parts = submitted.split(" ");
|
|
934
2403
|
const targetArg = parts.slice(1).join(" ").trim();
|
|
935
2404
|
if (targetArg) {
|
|
936
|
-
clearPromptArea();
|
|
937
2405
|
const resolvedId = getBackendModelId(targetArg);
|
|
938
2406
|
currentBackendModel = resolvedId;
|
|
939
2407
|
const friendly = getFriendlyModelName(resolvedId);
|
|
940
|
-
|
|
941
|
-
\u2713 Switched model to: ${friendly} (${resolvedId})
|
|
942
|
-
`));
|
|
2408
|
+
printToContent(chalk4.green(`\u2713 Switched model to: ${friendly} (${resolvedId})`));
|
|
943
2409
|
render();
|
|
944
2410
|
return;
|
|
945
2411
|
} else {
|
|
@@ -950,39 +2416,404 @@ Compact failed: ${err.message}
|
|
|
950
2416
|
return;
|
|
951
2417
|
}
|
|
952
2418
|
}
|
|
953
|
-
if (submitted
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
2419
|
+
if (submitted.startsWith("!") || submitted.startsWith("/exec")) {
|
|
2420
|
+
const cmdToRun = submitted.startsWith("!") ? submitted.slice(1).trim() : submitted.replace(/^\/exec\s*/, "").trim();
|
|
2421
|
+
if (!cmdToRun) {
|
|
2422
|
+
printToContent(chalk4.yellow("Usage: !<command> or /exec <command>\nExample: !git status"));
|
|
2423
|
+
render();
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
printToContent(chalk4.cyan(`$ ${cmdToRun}`));
|
|
2427
|
+
try {
|
|
2428
|
+
const res = execSync(cmdToRun, {
|
|
2429
|
+
cwd: session.directory,
|
|
2430
|
+
encoding: "utf-8",
|
|
2431
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2432
|
+
timeout: 6e4,
|
|
2433
|
+
maxBuffer: 10 * 1024 * 1024
|
|
2434
|
+
});
|
|
2435
|
+
if (res.trim()) {
|
|
2436
|
+
printToContent(res.trimEnd());
|
|
2437
|
+
} else {
|
|
2438
|
+
printToContent(chalk4.dim("(Command completed with no output)"));
|
|
2439
|
+
}
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
const out = err.stdout ? String(err.stdout).trim() : "";
|
|
2442
|
+
const errOut = err.stderr ? String(err.stderr).trim() : "";
|
|
2443
|
+
if (out) printToContent(out);
|
|
2444
|
+
if (errOut) printToContent(chalk4.red(errOut));
|
|
2445
|
+
if (!out && !errOut) printToContent(chalk4.red(`Command failed: ${err.message}`));
|
|
2446
|
+
}
|
|
2447
|
+
render();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (submitted === "/checkpoint") {
|
|
2451
|
+
try {
|
|
2452
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2453
|
+
const stashMsg = `oceancode-checkpoint-${timestamp}`;
|
|
2454
|
+
const res = execSync(`git stash push -m "${stashMsg}" --include-untracked`, {
|
|
2455
|
+
cwd: session.directory,
|
|
2456
|
+
encoding: "utf-8",
|
|
2457
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2458
|
+
});
|
|
2459
|
+
if (res.includes("No local changes to save")) {
|
|
2460
|
+
printToContent(chalk4.yellow("No unstaged or modified changes to checkpoint."));
|
|
2461
|
+
} else {
|
|
2462
|
+
try {
|
|
2463
|
+
execSync("git stash apply stash@{0}", {
|
|
2464
|
+
cwd: session.directory,
|
|
2465
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2466
|
+
});
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
printToContent(
|
|
2470
|
+
`${chalk4.green(`\u2714 Checkpoint created: ${stashMsg}`)}
|
|
2471
|
+
` + chalk4.dim(" Current changes safely recorded in git stash.\n Revert anytime with: git stash apply")
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
printToContent(chalk4.red(`Checkpoint failed: ${err.message}`));
|
|
2476
|
+
}
|
|
2477
|
+
render();
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
if (submitted.startsWith("/diff-file")) {
|
|
2481
|
+
const targetFile = submitted.replace(/^\/diff-file\s*/, "").trim();
|
|
2482
|
+
if (!targetFile) {
|
|
2483
|
+
printToContent(chalk4.yellow("Usage: /diff-file <filepath>\nExample: /diff-file package.json"));
|
|
2484
|
+
render();
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
try {
|
|
2488
|
+
const res = execSync(`git diff HEAD -- "${targetFile}"`, {
|
|
2489
|
+
cwd: session.directory,
|
|
2490
|
+
encoding: "utf-8",
|
|
2491
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2492
|
+
});
|
|
2493
|
+
if (!res.trim()) {
|
|
2494
|
+
printToContent(chalk4.dim(`No differences found for "${targetFile}" compared to HEAD.`));
|
|
2495
|
+
} else {
|
|
2496
|
+
const colored = res.split("\n").map((line) => {
|
|
2497
|
+
if (line.startsWith("+") && !line.startsWith("+++")) return chalk4.green(line);
|
|
2498
|
+
if (line.startsWith("-") && !line.startsWith("---")) return chalk4.red(line);
|
|
2499
|
+
if (line.startsWith("@@")) return chalk4.cyan(line);
|
|
2500
|
+
return chalk4.dim(line);
|
|
2501
|
+
}).join("\n");
|
|
2502
|
+
printToContent(`${chalk4.bold(`Diff: ${targetFile}`)}
|
|
2503
|
+
${colored}`);
|
|
2504
|
+
}
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
printToContent(chalk4.red(`Failed to get diff for ${targetFile}: ${err.message}`));
|
|
2507
|
+
}
|
|
2508
|
+
render();
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
if (submitted === "/commit" || submitted.startsWith("/commit ")) {
|
|
2512
|
+
const manualMsg = submitted.replace(/^\/commit\s*/, "").trim();
|
|
2513
|
+
if (manualMsg) {
|
|
2514
|
+
try {
|
|
2515
|
+
execSync(`git commit -m "${manualMsg.replace(/"/g, '\\"')}"`, {
|
|
2516
|
+
cwd: session.directory,
|
|
2517
|
+
encoding: "utf-8",
|
|
2518
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2519
|
+
});
|
|
2520
|
+
printToContent(chalk4.green(`\u2714 Committed successfully with message: "${manualMsg}"`));
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
printToContent(chalk4.red(`Git commit failed: ${err.message}`));
|
|
2523
|
+
}
|
|
2524
|
+
render();
|
|
2525
|
+
return;
|
|
2526
|
+
} else {
|
|
2527
|
+
try {
|
|
2528
|
+
const staged = execSync("git diff --staged", {
|
|
2529
|
+
cwd: session.directory,
|
|
2530
|
+
encoding: "utf-8",
|
|
2531
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2532
|
+
});
|
|
2533
|
+
const unstaged = execSync("git diff", {
|
|
2534
|
+
cwd: session.directory,
|
|
2535
|
+
encoding: "utf-8",
|
|
2536
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2537
|
+
});
|
|
2538
|
+
const status = execSync("git status --short", {
|
|
2539
|
+
cwd: session.directory,
|
|
2540
|
+
encoding: "utf-8",
|
|
2541
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2542
|
+
});
|
|
2543
|
+
if (!status.trim()) {
|
|
2544
|
+
printToContent(chalk4.yellow("No changes detected to commit. Working tree is clean."));
|
|
2545
|
+
render();
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const diffToUse = staged.trim() || unstaged.trim() || status;
|
|
2549
|
+
submitted = `[GENERATE CONVENTIONAL COMMIT MESSAGE]
|
|
2550
|
+
Based on the following git changes, generate a concise, conventional commit message (e.g. feat:, fix:, refactor:, chore:) with a 1-sentence explanation:
|
|
2551
|
+
|
|
2552
|
+
\`\`\`diff
|
|
2553
|
+
${diffToUse.slice(0, 4e3)}
|
|
2554
|
+
\`\`\`
|
|
2555
|
+
|
|
2556
|
+
Please format the commit message clearly so it can be copied or committed.`;
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
printToContent(chalk4.red(`Git inspection failed: ${err.message}`));
|
|
2559
|
+
render();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (submitted === "/review" || submitted.startsWith("/review ")) {
|
|
2565
|
+
const target = submitted.replace(/^\/review\s*/, "").trim() || "HEAD";
|
|
2566
|
+
try {
|
|
2567
|
+
let diff = "";
|
|
2568
|
+
try {
|
|
2569
|
+
diff = execSync(`git diff ${target}`, {
|
|
2570
|
+
cwd: session.directory,
|
|
2571
|
+
encoding: "utf-8",
|
|
2572
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2573
|
+
});
|
|
2574
|
+
} catch {
|
|
2575
|
+
diff = execSync("git diff", {
|
|
2576
|
+
cwd: session.directory,
|
|
2577
|
+
encoding: "utf-8",
|
|
2578
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
if (!diff.trim()) {
|
|
2582
|
+
printToContent(chalk4.yellow("No git changes found to review. Working directory matches target."));
|
|
2583
|
+
render();
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
submitted = `[CODE REVIEW REQUEST]
|
|
2587
|
+
Please review the following diff carefully.
|
|
2588
|
+
Identify: 1) Potential bugs or regressions, 2) Security issues, 3) Edge case handling, 4) Code style or architectural improvements:
|
|
2589
|
+
|
|
2590
|
+
\`\`\`diff
|
|
2591
|
+
${diff.slice(0, 6e3)}
|
|
2592
|
+
\`\`\``;
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
printToContent(chalk4.red(`Review failed: ${err.message}`));
|
|
2595
|
+
render();
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
if (submitted.startsWith("/open")) {
|
|
2600
|
+
const fileToOpen = submitted.replace(/^\/open\s*/, "").trim();
|
|
2601
|
+
if (!fileToOpen) {
|
|
2602
|
+
printToContent(chalk4.yellow("Usage: /open <filepath>\nExample: /open src/index.ts"));
|
|
2603
|
+
render();
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
const resolved = path5.resolve(session.directory, fileToOpen);
|
|
2607
|
+
if (!fs5.existsSync(resolved)) {
|
|
2608
|
+
printToContent(chalk4.red(`File not found: ${fileToOpen}`));
|
|
2609
|
+
render();
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const openCmd = process.platform === "win32" ? `start "" "${resolved}"` : process.platform === "darwin" ? `open "${resolved}"` : `xdg-open "${resolved}"`;
|
|
2613
|
+
try {
|
|
2614
|
+
execSync(openCmd, {
|
|
2615
|
+
shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh",
|
|
2616
|
+
stdio: "ignore"
|
|
2617
|
+
});
|
|
2618
|
+
printToContent(chalk4.green(`\u2714 Opened ${fileToOpen}`));
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
printToContent(chalk4.red(`Failed to open file: ${err.message}`));
|
|
2621
|
+
}
|
|
2622
|
+
render();
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (submitted.startsWith("/add")) {
|
|
2626
|
+
const targetPath = submitted.replace(/^\/add\s*/, "").trim();
|
|
2627
|
+
if (!targetPath) {
|
|
2628
|
+
printToContent(chalk4.yellow("Usage: /add <filepath>\nExample: /add src/server/client.ts"));
|
|
2629
|
+
render();
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const resolved = path5.resolve(session.directory, targetPath);
|
|
2633
|
+
if (!fs5.existsSync(resolved)) {
|
|
2634
|
+
printToContent(chalk4.red(`File does not exist: ${targetPath}`));
|
|
2635
|
+
render();
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
try {
|
|
2639
|
+
const stats = fs5.statSync(resolved);
|
|
2640
|
+
if (stats.isDirectory()) {
|
|
2641
|
+
printToContent(chalk4.yellow(`Cannot add directory directly. Specify a file path: /add ${targetPath}/<filename>`));
|
|
2642
|
+
render();
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const content = fs5.readFileSync(resolved, "utf-8");
|
|
2646
|
+
pinnedContext.set(targetPath, content);
|
|
2647
|
+
printToContent(chalk4.green(`\u2714 Added "${targetPath}" (${content.length} chars) to active context.`));
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
printToContent(chalk4.red(`Failed to read file: ${err.message}`));
|
|
2650
|
+
}
|
|
2651
|
+
render();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (submitted.startsWith("/drop")) {
|
|
2655
|
+
const targetPath = submitted.replace(/^\/drop\s*/, "").trim();
|
|
2656
|
+
if (!targetPath) {
|
|
2657
|
+
printToContent(chalk4.yellow("Usage: /drop <filepath> or /drop all"));
|
|
2658
|
+
render();
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (targetPath.toLowerCase() === "all") {
|
|
2662
|
+
const count = pinnedContext.size;
|
|
2663
|
+
pinnedContext.clear();
|
|
2664
|
+
printToContent(chalk4.green(`\u2714 Dropped all ${count} pinned item(s) from session context.`));
|
|
2665
|
+
} else if (pinnedContext.has(targetPath)) {
|
|
2666
|
+
pinnedContext.delete(targetPath);
|
|
2667
|
+
printToContent(chalk4.green(`\u2714 Dropped "${targetPath}" from session context.`));
|
|
2668
|
+
} else {
|
|
2669
|
+
let matched = false;
|
|
2670
|
+
for (const key2 of pinnedContext.keys()) {
|
|
2671
|
+
if (key2.includes(targetPath)) {
|
|
2672
|
+
pinnedContext.delete(key2);
|
|
2673
|
+
printToContent(chalk4.green(`\u2714 Dropped "${key2}" from session context.`));
|
|
2674
|
+
matched = true;
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (!matched) {
|
|
2679
|
+
printToContent(chalk4.yellow(`"${targetPath}" was not found in pinned context.`));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
render();
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (submitted === "/context") {
|
|
2686
|
+
const lines = [];
|
|
2687
|
+
lines.push(chalk4.bold("Active Session Context:"));
|
|
2688
|
+
lines.push(` \u2022 Model: ${chalk4.cyan(getFriendlyModelName(currentBackendModel))}`);
|
|
2689
|
+
lines.push(` \u2022 Agent Mode: ${chalk4.cyan(currentAgent.toUpperCase())}`);
|
|
2690
|
+
lines.push(` \u2022 Working Directory: ${chalk4.dim(session.directory)}`);
|
|
2691
|
+
if (pinnedContext.size === 0) {
|
|
2692
|
+
lines.push(chalk4.dim(" \u2022 Pinned Files: None (use /add <file> to attach context)"));
|
|
2693
|
+
} else {
|
|
2694
|
+
lines.push(chalk4.bold(` \u2022 Pinned Files (${pinnedContext.size}):`));
|
|
2695
|
+
for (const [p, content] of pinnedContext.entries()) {
|
|
2696
|
+
lines.push(` - ${chalk4.cyan(p)} ${chalk4.dim(`(${content.length} chars)`)}`);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
printToContent(lines.join("\n"));
|
|
2700
|
+
render();
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (submitted.startsWith("/web")) {
|
|
2704
|
+
const targetUrl = submitted.replace(/^\/web\s*/, "").trim();
|
|
2705
|
+
if (!targetUrl || !targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
|
|
2706
|
+
printToContent(chalk4.yellow("Usage: /web <url>\nExample: /web https://docs.github.com/en/rest"));
|
|
2707
|
+
render();
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
printToContent(chalk4.cyan(`Fetching ${targetUrl}...`));
|
|
2711
|
+
try {
|
|
2712
|
+
const res = await fetch(targetUrl, { headers: { "User-Agent": "OceanCode-CLI" } });
|
|
2713
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
2714
|
+
const html = await res.text();
|
|
2715
|
+
const textOnly = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2716
|
+
const snippet = textOnly.slice(0, 1e4);
|
|
2717
|
+
pinnedContext.set(targetUrl, snippet);
|
|
2718
|
+
printToContent(chalk4.green(`\u2714 Fetched and pinned text from ${targetUrl} (${snippet.length} chars).`));
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
printToContent(chalk4.red(`Failed to fetch URL: ${err.message}`));
|
|
2721
|
+
}
|
|
2722
|
+
render();
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
if (submitted === "/export" || submitted.startsWith("/export ")) {
|
|
2726
|
+
const filenameArg = submitted.replace(/^\/export\s*/, "").trim();
|
|
2727
|
+
const exportPath = filenameArg || `oceancode-session-${session.id.slice(0, 8)}.md`;
|
|
2728
|
+
const resolvedExport = path5.resolve(session.directory, exportPath);
|
|
2729
|
+
try {
|
|
2730
|
+
printToContent(chalk4.cyan("Exporting session history..."));
|
|
2731
|
+
const rawMsgs = await client.getSessionMessages(session.id);
|
|
2732
|
+
const mdLines = [
|
|
2733
|
+
"# OceanCode Session Export",
|
|
2734
|
+
`**Session ID:** \`${session.id}\``,
|
|
2735
|
+
`**Date:** ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2736
|
+
`**Model:** ${getFriendlyModelName(currentBackendModel)}`,
|
|
2737
|
+
`**Directory:** \`${session.directory}\``,
|
|
2738
|
+
"\n---\n"
|
|
2739
|
+
];
|
|
2740
|
+
if (Array.isArray(rawMsgs) && rawMsgs.length > 0) {
|
|
2741
|
+
for (const msg of rawMsgs) {
|
|
2742
|
+
const role = msg.role || msg.type || "message";
|
|
2743
|
+
let content = "";
|
|
2744
|
+
if (typeof msg.content === "string") {
|
|
2745
|
+
content = msg.content;
|
|
2746
|
+
} else if (Array.isArray(msg.parts)) {
|
|
2747
|
+
content = msg.parts.map((p) => typeof p === "string" ? p : p.text || JSON.stringify(p)).join("\n");
|
|
2748
|
+
} else {
|
|
2749
|
+
content = JSON.stringify(msg);
|
|
2750
|
+
}
|
|
2751
|
+
mdLines.push(`### ${role.toUpperCase()}
|
|
2752
|
+
|
|
2753
|
+
${content}
|
|
2754
|
+
`);
|
|
2755
|
+
}
|
|
2756
|
+
} else if (conversationHistory.length > 0) {
|
|
2757
|
+
for (const turn of conversationHistory) {
|
|
2758
|
+
mdLines.push(`### ${turn.role.toUpperCase()}
|
|
2759
|
+
|
|
2760
|
+
${turn.text}
|
|
2761
|
+
`);
|
|
2762
|
+
}
|
|
2763
|
+
} else {
|
|
2764
|
+
mdLines.push("_No session messages recorded yet._");
|
|
2765
|
+
}
|
|
2766
|
+
fs5.writeFileSync(resolvedExport, mdLines.join("\n"), "utf-8");
|
|
2767
|
+
printToContent(`${chalk4.green(`\u2714 Session successfully exported to:`)}
|
|
2768
|
+
${resolvedExport}`);
|
|
2769
|
+
} catch (err) {
|
|
2770
|
+
printToContent(chalk4.red(`Failed to export session: ${err.message}`));
|
|
958
2771
|
}
|
|
959
|
-
|
|
2772
|
+
render();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (submitted === "/help") {
|
|
2776
|
+
const helpLines = [
|
|
2777
|
+
chalk4.bold("Available Commands:"),
|
|
2778
|
+
...COMMANDS.map((c) => ` ${chalk4.hex(OCEAN_BLUE).bold(c.name.padEnd(15))} ${chalk4.dim(c.desc)}`)
|
|
2779
|
+
].join("\n");
|
|
2780
|
+
printToContent(helpLines);
|
|
960
2781
|
render();
|
|
961
2782
|
return;
|
|
962
2783
|
}
|
|
963
2784
|
if (submitted === "/info") {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
`
|
|
967
|
-
Session Details:
|
|
2785
|
+
printToContent(
|
|
2786
|
+
`Session Details:
|
|
968
2787
|
Model: ${getFriendlyModelName(currentBackendModel)}
|
|
969
2788
|
Agent: ${currentAgent.toUpperCase()}
|
|
970
2789
|
Directory: ${session.directory}
|
|
971
|
-
Session: ${session.id}
|
|
972
|
-
`
|
|
2790
|
+
Session: ${session.id}`
|
|
973
2791
|
);
|
|
974
2792
|
render();
|
|
975
2793
|
return;
|
|
976
2794
|
}
|
|
977
2795
|
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
978
|
-
|
|
2796
|
+
process.stdout.write("\x1B8");
|
|
979
2797
|
renderUserMessageCard(submitted, friendlyName);
|
|
2798
|
+
process.stdout.write("\x1B7");
|
|
980
2799
|
isProcessing = true;
|
|
981
2800
|
userMsgId = null;
|
|
982
2801
|
lastSubmittedPrompt = submitted;
|
|
2802
|
+
paintBar();
|
|
983
2803
|
resetActivityTimeout();
|
|
984
2804
|
renderer.start(friendlyName);
|
|
985
|
-
|
|
2805
|
+
let promptToSend = submitted;
|
|
2806
|
+
if (pinnedContext.size > 0) {
|
|
2807
|
+
const parts = ["--- PINNED CONTEXT FILES ---"];
|
|
2808
|
+
for (const [k, v] of pinnedContext.entries()) {
|
|
2809
|
+
parts.push(`
|
|
2810
|
+
[Reference: ${k}]
|
|
2811
|
+
${v}`);
|
|
2812
|
+
}
|
|
2813
|
+
parts.push("--- END PINNED CONTEXT ---\n");
|
|
2814
|
+
promptToSend = parts.join("\n") + "\n" + submitted;
|
|
2815
|
+
}
|
|
2816
|
+
client.promptSession(session.id, promptToSend, {
|
|
986
2817
|
model: currentBackendModel,
|
|
987
2818
|
agent: currentAgent,
|
|
988
2819
|
directory: session.directory
|
|
@@ -990,124 +2821,151 @@ Session Details:
|
|
|
990
2821
|
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
991
2822
|
isProcessing = false;
|
|
992
2823
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
`));
|
|
996
|
-
render();
|
|
2824
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Prompt Error: ${err.message}`));
|
|
2825
|
+
paintBar();
|
|
997
2826
|
});
|
|
998
2827
|
return;
|
|
999
2828
|
}
|
|
1000
|
-
if (key.name === "
|
|
1001
|
-
|
|
1002
|
-
menuIndex = 0;
|
|
2829
|
+
if (key.name === "left") {
|
|
2830
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
1003
2831
|
render();
|
|
1004
2832
|
return;
|
|
1005
2833
|
}
|
|
1006
|
-
if (
|
|
1007
|
-
input
|
|
1008
|
-
|
|
2834
|
+
if (key.name === "right") {
|
|
2835
|
+
cursorIndex = Math.min(input.length, cursorIndex + 1);
|
|
2836
|
+
render();
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (key.name === "home") {
|
|
2840
|
+
cursorIndex = 0;
|
|
1009
2841
|
render();
|
|
1010
2842
|
return;
|
|
1011
2843
|
}
|
|
2844
|
+
if (key.name === "end") {
|
|
2845
|
+
cursorIndex = input.length;
|
|
2846
|
+
render();
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (key.name === "delete") {
|
|
2850
|
+
if (cursorIndex < input.length) {
|
|
2851
|
+
input = input.slice(0, cursorIndex) + input.slice(cursorIndex + 1);
|
|
2852
|
+
menuIndex = 0;
|
|
2853
|
+
render();
|
|
2854
|
+
}
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
if (key.name === "backspace") {
|
|
2858
|
+
if (cursorIndex > 0) {
|
|
2859
|
+
input = input.slice(0, cursorIndex - 1) + input.slice(cursorIndex);
|
|
2860
|
+
cursorIndex--;
|
|
2861
|
+
menuIndex = 0;
|
|
2862
|
+
render();
|
|
2863
|
+
}
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
if (str && !key.ctrl && !key.meta) {
|
|
2867
|
+
if (str.startsWith("\x1B") || /^<[0-9;]+[Mm]$/.test(str)) {
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const clean = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\r\n]+/g, " ");
|
|
2871
|
+
if (clean) {
|
|
2872
|
+
input = input.slice(0, cursorIndex) + clean + input.slice(cursorIndex);
|
|
2873
|
+
cursorIndex += clean.length;
|
|
2874
|
+
menuIndex = 0;
|
|
2875
|
+
historyIndex = -1;
|
|
2876
|
+
render();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
1012
2880
|
};
|
|
1013
2881
|
process.stdin.on("keypress", onKeypress);
|
|
2882
|
+
process.title = "oceancode";
|
|
1014
2883
|
if (process.stdout.isTTY) {
|
|
1015
|
-
process.stdout.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const rows = process.stdout.rows || 24;
|
|
1023
|
-
process.stdout.write("\x1B[2J");
|
|
1024
|
-
process.stdout.write("\x1B[H");
|
|
2884
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
2885
|
+
process.stdout.write("\x1B]2;oceancode\x07");
|
|
2886
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2887
|
+
process.stdout.write("\x1B[?1000l\x1B[?1002l\x1B[?1006l");
|
|
2888
|
+
const { scrollBottom } = getBarMetrics(3);
|
|
2889
|
+
applyScrollRegion(scrollBottom);
|
|
2890
|
+
process.stdout.write("\x1B[1;1H");
|
|
1025
2891
|
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
paintBar(buildBarLines());
|
|
2892
|
+
process.stdout.write("\x1B7");
|
|
2893
|
+
paintBar();
|
|
1029
2894
|
}
|
|
1030
2895
|
}
|
|
1031
2896
|
|
|
1032
2897
|
// src/commands/models.ts
|
|
1033
|
-
import
|
|
2898
|
+
import chalk5 from "chalk";
|
|
1034
2899
|
async function listModelsCommand(options) {
|
|
1035
|
-
const baseUrl = await ensureServer();
|
|
1036
|
-
const client = new OceanClient(baseUrl);
|
|
1037
2900
|
const { leftMargin } = getLayoutDimensions();
|
|
1038
2901
|
const indent = " ".repeat(leftMargin);
|
|
1039
|
-
const
|
|
1040
|
-
|
|
2902
|
+
const available = getAvailableOceanModels();
|
|
2903
|
+
if (options?.all) {
|
|
2904
|
+
try {
|
|
2905
|
+
const baseUrl = await ensureServer();
|
|
2906
|
+
const client = new OceanClient(baseUrl);
|
|
2907
|
+
const serverModels = await client.getModels();
|
|
2908
|
+
console.log(`
|
|
2909
|
+
${indent}${chalk5.bold.hex("#00f2fe")(`All Models from Server (${serverModels.length}):`)}`);
|
|
2910
|
+
for (const m of serverModels) {
|
|
2911
|
+
console.log(`${indent} \u2022 ${chalk5.white.bold(m.id.padEnd(35))} ${chalk5.dim(m.providerID)}`);
|
|
2912
|
+
}
|
|
2913
|
+
return;
|
|
2914
|
+
} catch {
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
1041
2917
|
console.log("");
|
|
1042
|
-
console.log(`${indent}${
|
|
1043
|
-
console.log(`${indent}${
|
|
2918
|
+
console.log(`${indent}${chalk5.hex("#00d2ff").bold("Ocean AI Models")}`);
|
|
2919
|
+
console.log(`${indent}${chalk5.dim("Switch anytime in chat with /models")}
|
|
1044
2920
|
`);
|
|
1045
|
-
console.log(`${indent}${
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
console.log(`${indent}${chalk2.bold.hex("#00f2fe")("Free Built-in Models:")}`);
|
|
1049
|
-
for (const m of freeModels) {
|
|
1050
|
-
const badge = chalk2.bgHex("#00b4d8").black.bold(` ${m.tag} `);
|
|
1051
|
-
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1052
|
-
}
|
|
1053
|
-
if (byokModels.length > 0) {
|
|
1054
|
-
console.log(`
|
|
1055
|
-
${indent}${chalk2.bold.hex("#3a7bd5")("Connected BYOK Models:")}`);
|
|
1056
|
-
for (const m of byokModels) {
|
|
1057
|
-
const badge = chalk2.bgHex("#10b981").black.bold(` ${m.tag} `);
|
|
1058
|
-
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
const unconnected = SUPPORTED_BYOK_PROVIDERS.filter((p) => !creds[p.id]?.key);
|
|
1062
|
-
if (unconnected.length > 0) {
|
|
1063
|
-
console.log(`
|
|
1064
|
-
${indent}${chalk2.dim("Connect more models via 'oceancode connect <provider> <key>' or /connect in chat:")}`);
|
|
1065
|
-
console.log(`${indent}${chalk2.dim(`Unlinked: ${unconnected.map((p) => p.name).join(", ")}`)}`);
|
|
2921
|
+
console.log(`${indent}${chalk5.dim("-".repeat(40))}`);
|
|
2922
|
+
for (const m of available) {
|
|
2923
|
+
console.log(`${indent} \u2022 ${chalk5.white.bold(m.name)}`);
|
|
1066
2924
|
}
|
|
1067
2925
|
console.log(`
|
|
1068
|
-
${indent}${
|
|
2926
|
+
${indent}${chalk5.dim("Tip: Type /models in interactive chat to switch models with arrow keys.")}
|
|
1069
2927
|
`);
|
|
1070
2928
|
}
|
|
1071
2929
|
|
|
1072
2930
|
// src/commands/session.ts
|
|
1073
|
-
import
|
|
2931
|
+
import chalk6 from "chalk";
|
|
1074
2932
|
async function sessionCommand() {
|
|
1075
2933
|
const baseUrl = await ensureServer();
|
|
1076
2934
|
const client = new OceanClient(baseUrl);
|
|
1077
2935
|
const { leftMargin } = getLayoutDimensions();
|
|
1078
2936
|
const indent = " ".repeat(leftMargin);
|
|
1079
2937
|
console.log("");
|
|
1080
|
-
console.log(`${indent}${
|
|
1081
|
-
console.log(`${indent}${
|
|
2938
|
+
console.log(`${indent}${chalk6.hex("#00d2ff").bold("Recent Sessions")}`);
|
|
2939
|
+
console.log(`${indent}${chalk6.dim("-".repeat(54))}`);
|
|
1082
2940
|
try {
|
|
1083
2941
|
const sessions = await client.listSessions(10);
|
|
1084
2942
|
if (!sessions || sessions.length === 0) {
|
|
1085
|
-
console.log(`${indent}${
|
|
2943
|
+
console.log(`${indent}${chalk6.dim("No previous sessions found.")}
|
|
1086
2944
|
`);
|
|
1087
2945
|
return;
|
|
1088
2946
|
}
|
|
1089
2947
|
for (const s of sessions) {
|
|
1090
2948
|
const date = new Date(s.time?.created || Date.now()).toLocaleDateString();
|
|
1091
2949
|
const tokens = (s.tokens?.input || 0) + (s.tokens?.output || 0);
|
|
1092
|
-
console.log(`${indent}${
|
|
1093
|
-
console.log(`${indent} ${
|
|
2950
|
+
console.log(`${indent}${chalk6.cyan(s.id.slice(0, 16))} ${chalk6.bold.white(s.title || "Untitled")}`);
|
|
2951
|
+
console.log(`${indent} ${chalk6.dim(date)} \xB7 ${chalk6.dim(`${tokens} tokens`)} \xB7 ${chalk6.dim(s.directory)}`);
|
|
1094
2952
|
console.log("");
|
|
1095
2953
|
}
|
|
1096
2954
|
} catch (err) {
|
|
1097
|
-
console.error(`${indent}${
|
|
2955
|
+
console.error(`${indent}${chalk6.red(`Failed to list sessions: ${err.message}`)}`);
|
|
1098
2956
|
}
|
|
1099
2957
|
}
|
|
1100
2958
|
|
|
1101
2959
|
// src/commands/stats.ts
|
|
1102
|
-
import
|
|
2960
|
+
import chalk7 from "chalk";
|
|
1103
2961
|
async function statsCommand() {
|
|
1104
2962
|
const baseUrl = await ensureServer();
|
|
1105
2963
|
const client = new OceanClient(baseUrl);
|
|
1106
2964
|
const { leftMargin } = getLayoutDimensions();
|
|
1107
2965
|
const indent = " ".repeat(leftMargin);
|
|
1108
2966
|
console.log("");
|
|
1109
|
-
console.log(`${indent}${
|
|
1110
|
-
console.log(`${indent}${
|
|
2967
|
+
console.log(`${indent}${chalk7.hex("#00d2ff").bold("Usage & Analytics")}`);
|
|
2968
|
+
console.log(`${indent}${chalk7.dim("-".repeat(54))}`);
|
|
1111
2969
|
try {
|
|
1112
2970
|
const sessions = await client.listSessions(100);
|
|
1113
2971
|
let totalTokens = 0;
|
|
@@ -1120,28 +2978,45 @@ async function statsCommand() {
|
|
|
1120
2978
|
totalCost += s.cost || 0;
|
|
1121
2979
|
}
|
|
1122
2980
|
totalTokens = totalInput + totalOutput;
|
|
1123
|
-
console.log(`${indent}${
|
|
1124
|
-
console.log(`${indent}${
|
|
1125
|
-
console.log(`${indent}${
|
|
1126
|
-
console.log(`${indent}${
|
|
1127
|
-
console.log(`${indent}${
|
|
2981
|
+
console.log(`${indent}${chalk7.dim("Total Sessions:")} ${chalk7.bold.white(sessions.length)}`);
|
|
2982
|
+
console.log(`${indent}${chalk7.dim("Total Tokens:")} ${chalk7.bold.cyan(totalTokens.toLocaleString())}`);
|
|
2983
|
+
console.log(`${indent}${chalk7.dim("Input Tokens:")} ${chalk7.white(totalInput.toLocaleString())}`);
|
|
2984
|
+
console.log(`${indent}${chalk7.dim("Output Tokens:")} ${chalk7.white(totalOutput.toLocaleString())}`);
|
|
2985
|
+
console.log(`${indent}${chalk7.dim("Total Cost:")} ${chalk7.bold.green("$" + totalCost.toFixed(4))} ${chalk7.bgHex("#00b4d8").black.bold(" FREE ")}`);
|
|
1128
2986
|
console.log("");
|
|
1129
2987
|
} catch (err) {
|
|
1130
|
-
console.error(`${indent}${
|
|
2988
|
+
console.error(`${indent}${chalk7.red(`Failed to load stats: ${err.message}`)}`);
|
|
1131
2989
|
}
|
|
1132
2990
|
}
|
|
1133
2991
|
|
|
1134
2992
|
// src/index.ts
|
|
1135
|
-
import
|
|
2993
|
+
import chalk8 from "chalk";
|
|
2994
|
+
process.title = "oceancode";
|
|
2995
|
+
if (process.stdout.isTTY) {
|
|
2996
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
2997
|
+
process.stdout.write("\x1B]2;oceancode\x07");
|
|
2998
|
+
}
|
|
1136
2999
|
var program = new Command();
|
|
1137
3000
|
program.enablePositionalOptions();
|
|
1138
|
-
program.name("oceancode").description("
|
|
3001
|
+
program.name("oceancode").description("OceanCode CLI: Next-generation AI coding assistant built for large codebases").version("0.1.2").addHelpText("before", `
|
|
1139
3002
|
${getBigLogo()}
|
|
1140
3003
|
`);
|
|
1141
|
-
program.option("-m, --model <model>", "Model to use (default:
|
|
1142
|
-
|
|
3004
|
+
program.option("-m, --model <model>", "Model to use (default: ocean/Qwen3.6-35B-A3B)").action(async (options) => {
|
|
3005
|
+
try {
|
|
3006
|
+
await interactiveChatCommand({ model: options.model });
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
if (process.stdout.isTTY) {
|
|
3009
|
+
const rows = process.stdout.rows || 24;
|
|
3010
|
+
process.stdout.write(`\x1B[1;${rows}r\x1B[?25h
|
|
3011
|
+
`);
|
|
3012
|
+
}
|
|
3013
|
+
console.error(chalk8.red(`
|
|
3014
|
+
An error occurred: ${err?.message || err}
|
|
3015
|
+
`));
|
|
3016
|
+
process.exit(1);
|
|
3017
|
+
}
|
|
1143
3018
|
});
|
|
1144
|
-
program.command("run [message...]").description("Run a prompt or instruction directly").option("-m, --model <model>", "Model to use (default:
|
|
3019
|
+
program.command("run [message...]").description("Run a prompt or instruction directly").option("-m, --model <model>", "Model to use (default: ocean/Qwen3.6-35B-A3B)").option("-a, --agent <agent>", "Agent mode (default: build)").option("--auto", "Auto-approve tool permissions").action(async (messages, options, cmd) => {
|
|
1145
3020
|
const opts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : options;
|
|
1146
3021
|
await runCommand(messages, opts);
|
|
1147
3022
|
process.exit(0);
|
|
@@ -1149,36 +3024,36 @@ program.command("run [message...]").description("Run a prompt or instruction dir
|
|
|
1149
3024
|
program.command("init").description("Initialize codebase context and generate or update AGENTS.md").action(async () => {
|
|
1150
3025
|
try {
|
|
1151
3026
|
const res = initializeAgentsDoc(process.cwd());
|
|
1152
|
-
console.log(
|
|
1153
|
-
console.log(
|
|
3027
|
+
console.log(chalk8.green(`\u2714 ${res.summary}`));
|
|
3028
|
+
console.log(chalk8.dim(` Path: ${res.filePath}`));
|
|
1154
3029
|
} catch (err) {
|
|
1155
|
-
console.error(
|
|
3030
|
+
console.error(chalk8.red(`Error initializing AGENTS.md: ${err.message}`));
|
|
1156
3031
|
}
|
|
1157
3032
|
process.exit(0);
|
|
1158
3033
|
});
|
|
1159
3034
|
program.command("connect [provider] [key]").description("Link provider API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq, OpenRouter)").action(async (provider, key) => {
|
|
1160
3035
|
if (provider && key) {
|
|
1161
|
-
console.log(
|
|
3036
|
+
console.log(chalk8.cyan(`Validating API key with ${provider.toUpperCase()}...`));
|
|
1162
3037
|
const val = await validateProviderKey(provider, key);
|
|
1163
3038
|
if (val.valid) {
|
|
1164
3039
|
saveProviderKey(provider, key);
|
|
1165
|
-
console.log(
|
|
3040
|
+
console.log(chalk8.green(`\u2714 ${val.message}`));
|
|
1166
3041
|
} else {
|
|
1167
|
-
console.error(
|
|
3042
|
+
console.error(chalk8.red(`\u2716 Validation failed: ${val.message}`));
|
|
1168
3043
|
process.exit(1);
|
|
1169
3044
|
}
|
|
1170
3045
|
process.exit(0);
|
|
1171
3046
|
}
|
|
1172
3047
|
const creds = getStoredCredentials();
|
|
1173
|
-
console.log(
|
|
3048
|
+
console.log(chalk8.bold("\nBring Your Own Key (BYOK) Status:"));
|
|
1174
3049
|
for (const p of SUPPORTED_BYOK_PROVIDERS) {
|
|
1175
|
-
const isSet = Boolean(creds[p.id]);
|
|
1176
|
-
const badge = isSet ?
|
|
3050
|
+
const isSet = Boolean(creds[p.id]?.key);
|
|
3051
|
+
const badge = isSet ? chalk8.green("\u2714 Connected") : chalk8.dim("Not linked");
|
|
1177
3052
|
console.log(` \u2022 ${p.name.padEnd(16)} (${p.id}): ${badge}`);
|
|
1178
3053
|
}
|
|
1179
|
-
console.log(
|
|
1180
|
-
console.log(
|
|
1181
|
-
console.log(
|
|
3054
|
+
console.log(chalk8.dim("\nTo link a key, run:"));
|
|
3055
|
+
console.log(chalk8.cyan(" oceancode connect <provider> <api-key>"));
|
|
3056
|
+
console.log(chalk8.dim("Examples:"));
|
|
1182
3057
|
console.log(" oceancode connect openai sk-proj-...");
|
|
1183
3058
|
console.log(" oceancode connect anthropic sk-ant-...");
|
|
1184
3059
|
console.log(" oceancode connect google AIzaSy...");
|
|
@@ -1189,7 +3064,7 @@ program.command("mcp").description("List or configure Model Context Protocol (MC
|
|
|
1189
3064
|
const act = (action || "list").toLowerCase();
|
|
1190
3065
|
if (act === "add") {
|
|
1191
3066
|
if (!name || !commandArgs || commandArgs.length === 0) {
|
|
1192
|
-
console.error(
|
|
3067
|
+
console.error(chalk8.red("Usage: oceancode mcp add <name> <command-or-url>"));
|
|
1193
3068
|
console.log("Example: oceancode mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .");
|
|
1194
3069
|
process.exit(1);
|
|
1195
3070
|
}
|
|
@@ -1197,27 +3072,27 @@ program.command("mcp").description("List or configure Model Context Protocol (MC
|
|
|
1197
3072
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
1198
3073
|
const config = isUrl ? { type: "remote", url: target, enabled: true } : { type: "local", command: commandArgs, enabled: true };
|
|
1199
3074
|
saveMcpServer(name, config);
|
|
1200
|
-
console.log(
|
|
3075
|
+
console.log(chalk8.green(`\u2714 Added MCP server "${name}" (${config.type}) to opencode.json`));
|
|
1201
3076
|
process.exit(0);
|
|
1202
3077
|
}
|
|
1203
3078
|
const baseUrl = await ensureServer();
|
|
1204
3079
|
const configured = getConfiguredMcpServers();
|
|
1205
3080
|
const live = await fetchLiveMcpStatus(baseUrl);
|
|
1206
3081
|
const allNames = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
1207
|
-
console.log(
|
|
3082
|
+
console.log(chalk8.bold("\nModel Context Protocol (MCP) Servers:"));
|
|
1208
3083
|
if (allNames.length === 0) {
|
|
1209
|
-
console.log(
|
|
1210
|
-
console.log(
|
|
1211
|
-
console.log(
|
|
3084
|
+
console.log(chalk8.dim(" No MCP servers configured."));
|
|
3085
|
+
console.log(chalk8.dim("\nTo add an MCP server, run:"));
|
|
3086
|
+
console.log(chalk8.cyan(" oceancode mcp add <name> <command-or-url>"));
|
|
1212
3087
|
} else {
|
|
1213
3088
|
for (const n of allNames) {
|
|
1214
3089
|
const conf = configured[n] || {};
|
|
1215
3090
|
const isLive = live[n] !== void 0;
|
|
1216
3091
|
const type = conf.type || (conf.url ? "remote" : "local");
|
|
1217
3092
|
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
1218
|
-
const status = isLive ?
|
|
1219
|
-
console.log(` \u2022 ${
|
|
1220
|
-
${
|
|
3093
|
+
const status = isLive ? chalk8.green("\u25CF Connected") : chalk8.cyan("\u25CF Configured");
|
|
3094
|
+
console.log(` \u2022 ${chalk8.bold(n)} (${type}) ${status}
|
|
3095
|
+
${chalk8.dim(target)}`);
|
|
1221
3096
|
}
|
|
1222
3097
|
}
|
|
1223
3098
|
console.log();
|
|
@@ -1237,6 +3112,6 @@ program.command("stats").description("View token consumption and session statist
|
|
|
1237
3112
|
});
|
|
1238
3113
|
program.command("serve").description("Ensure or start the Ocean server backend").action(async () => {
|
|
1239
3114
|
const url = await ensureServer();
|
|
1240
|
-
console.log(
|
|
3115
|
+
console.log(chalk8.green(`\u2714 Ocean backend is running at: ${url}`));
|
|
1241
3116
|
});
|
|
1242
3117
|
program.parse(process.argv);
|