oceancode 0.1.1 → 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 +2063 -244
- package/package.json +2 -1
- package/dist/chunk-U5PZQWEB.js +0 -1161
- package/dist/run-WPMSAWQ3.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-U5PZQWEB.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,9 +1617,9 @@ 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();
|
|
249
1622
|
let currentScrollBottom = 0;
|
|
250
|
-
let contentRow = 8;
|
|
251
1623
|
let lastBarStartRow = 0;
|
|
252
1624
|
let lastBarLinesCount = 0;
|
|
253
1625
|
const printToContent = (text) => {
|
|
@@ -255,11 +1627,11 @@ async function interactiveChatCommand(options) {
|
|
|
255
1627
|
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
256
1628
|
return;
|
|
257
1629
|
}
|
|
258
|
-
process.stdout.write(
|
|
1630
|
+
process.stdout.write("\x1B8");
|
|
259
1631
|
const formatted = text.endsWith("\n") ? text : text + "\n";
|
|
260
1632
|
process.stdout.write(formatted);
|
|
261
|
-
|
|
262
|
-
|
|
1633
|
+
process.stdout.write("\x1B7");
|
|
1634
|
+
paintBar();
|
|
263
1635
|
};
|
|
264
1636
|
const getBarMetrics = (linesCount) => {
|
|
265
1637
|
const rows = process.stdout.rows || 24;
|
|
@@ -292,6 +1664,7 @@ async function interactiveChatCommand(options) {
|
|
|
292
1664
|
process.off("SIGTERM", onSignal);
|
|
293
1665
|
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
294
1666
|
if (process.stdout.isTTY) {
|
|
1667
|
+
process.stdout.write("\x1B[?1000l\x1B[?1002l\x1B[?1006l");
|
|
295
1668
|
process.stdout.off("resize", onResize);
|
|
296
1669
|
resetScrollRegion();
|
|
297
1670
|
const rows = process.stdout.rows || 24;
|
|
@@ -326,7 +1699,7 @@ async function interactiveChatCommand(options) {
|
|
|
326
1699
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
327
1700
|
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
328
1701
|
printToContent(
|
|
329
|
-
|
|
1702
|
+
chalk4.yellow(
|
|
330
1703
|
`\u26A0\uFE0F Operation timed out waiting for ${friendlyName}. You can retry or switch models with /models.`
|
|
331
1704
|
)
|
|
332
1705
|
);
|
|
@@ -338,91 +1711,87 @@ async function interactiveChatCommand(options) {
|
|
|
338
1711
|
const buildBarLines = () => {
|
|
339
1712
|
const cols = process.stdout.columns || 80;
|
|
340
1713
|
const friendlyModel = getFriendlyModelName(currentBackendModel);
|
|
341
|
-
const bar =
|
|
1714
|
+
const bar = chalk4.hex(OCEAN_BLUE)("\u2502");
|
|
342
1715
|
const width = Math.min(cols - 4, 80);
|
|
343
|
-
const peachBg =
|
|
1716
|
+
const peachBg = chalk4.bgHex("#fba978").black;
|
|
344
1717
|
const lines = [];
|
|
345
1718
|
if (mode === "model_selector") {
|
|
346
|
-
const header = "Select Model (\u2191/\u2193 to navigate, Enter to select, Esc to close)" + " ".repeat(Math.max(2, width - 60)) +
|
|
347
|
-
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));
|
|
348
1721
|
lines.push(
|
|
349
|
-
modelSearch ? `Search: ${modelSearch}` :
|
|
1722
|
+
modelSearch ? `Search: ${modelSearch}` : chalk4.dim("Search models (e.g. qwen, step, flash)")
|
|
350
1723
|
);
|
|
351
1724
|
lines.push("");
|
|
352
1725
|
const availableModels = getAvailableOceanModels();
|
|
353
1726
|
const searchLower = modelSearch.toLowerCase();
|
|
354
1727
|
const filtered = availableModels.filter(
|
|
355
|
-
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower)
|
|
1728
|
+
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower)
|
|
356
1729
|
);
|
|
357
1730
|
if (modelIndex >= filtered.length) modelIndex = Math.max(0, filtered.length - 1);
|
|
358
1731
|
if (filtered.length === 0) {
|
|
359
|
-
lines.push(
|
|
1732
|
+
lines.push(chalk4.dim(" No matching models found."));
|
|
360
1733
|
} else {
|
|
361
|
-
const MAX_MODELS =
|
|
1734
|
+
const MAX_MODELS = 8;
|
|
362
1735
|
let startIdx = 0;
|
|
363
1736
|
if (filtered.length > MAX_MODELS) {
|
|
364
|
-
startIdx = Math.max(0, Math.min(modelIndex -
|
|
1737
|
+
startIdx = Math.max(0, Math.min(modelIndex - 3, filtered.length - MAX_MODELS));
|
|
365
1738
|
}
|
|
366
1739
|
const visibleModels = filtered.slice(startIdx, startIdx + MAX_MODELS);
|
|
367
1740
|
if (filtered.length > MAX_MODELS) {
|
|
368
|
-
lines.push(
|
|
1741
|
+
lines.push(chalk4.dim(` Models (${modelIndex + 1}/${filtered.length}) \xB7 \u2191/\u2193 to navigate`));
|
|
369
1742
|
}
|
|
370
1743
|
visibleModels.forEach((m, relIdx) => {
|
|
371
1744
|
const idx = startIdx + relIdx;
|
|
372
1745
|
const isCurrent = m.id === currentBackendModel;
|
|
373
1746
|
const prefix = isCurrent ? "\u25CF " : " ";
|
|
374
1747
|
const left = prefix + m.name;
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const row = left + " ".repeat(spaces) + tag;
|
|
1748
|
+
const spaces = Math.max(0, width - left.length);
|
|
1749
|
+
const row = left + " ".repeat(spaces);
|
|
378
1750
|
if (idx === modelIndex) {
|
|
379
1751
|
lines.push(peachBg(row));
|
|
380
|
-
if (m.description) lines.push(chalk.dim(` ${m.description}`));
|
|
381
1752
|
} else {
|
|
382
|
-
lines.push(
|
|
1753
|
+
lines.push(chalk4.white(row));
|
|
383
1754
|
}
|
|
384
1755
|
});
|
|
385
1756
|
}
|
|
386
|
-
lines.push("");
|
|
387
|
-
lines.push(chalk.dim("Type /connect to link API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq)."));
|
|
388
1757
|
} else if (mode === "connect_modal") {
|
|
389
|
-
const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) +
|
|
390
|
-
lines.push(
|
|
1758
|
+
const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) + chalk4.dim("esc");
|
|
1759
|
+
lines.push(chalk4.bold.white(header));
|
|
391
1760
|
if (connectStep === "provider_list") {
|
|
392
|
-
lines.push(
|
|
1761
|
+
lines.push(chalk4.dim("Select provider with \u2191/\u2193 and press Enter (or 'd' to disconnect):"));
|
|
393
1762
|
lines.push("");
|
|
394
1763
|
const creds = getStoredCredentials();
|
|
395
1764
|
SUPPORTED_BYOK_PROVIDERS.forEach((p, idx) => {
|
|
396
1765
|
const isConnected = Boolean(creds[p.id]?.key);
|
|
397
1766
|
const prefix = isConnected ? "\u25CF " : "\u25CB ";
|
|
398
1767
|
const left = ` ${prefix}${p.name.padEnd(16)} (${p.id})`;
|
|
399
|
-
const tag = isConnected ?
|
|
1768
|
+
const tag = isConnected ? chalk4.green("\u2714 Connected") : chalk4.dim("Not linked");
|
|
400
1769
|
const spaces = Math.max(2, width - left.length - 12);
|
|
401
1770
|
const row = left + " ".repeat(spaces) + tag;
|
|
402
|
-
lines.push(idx === connectProviderIndex ? peachBg(row) :
|
|
1771
|
+
lines.push(idx === connectProviderIndex ? peachBg(row) : chalk4.white(row));
|
|
403
1772
|
});
|
|
404
1773
|
lines.push("");
|
|
405
|
-
lines.push(
|
|
1774
|
+
lines.push(chalk4.dim("Enter: Link API Key \u2022 d: Disconnect \u2022 Esc: Close"));
|
|
406
1775
|
} else if (connectStep === "enter_key") {
|
|
407
1776
|
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
408
|
-
lines.push(
|
|
409
|
-
lines.push(
|
|
1777
|
+
lines.push(chalk4.bold.hex("#38bdf8")(`Provider: ${prov.name}`));
|
|
1778
|
+
lines.push(chalk4.dim(`Placeholder format: ${prov.placeholder}`));
|
|
410
1779
|
lines.push("");
|
|
411
1780
|
const masked = connectKeyInput.length > 8 ? connectKeyInput.slice(0, 4) + "\u2022".repeat(connectKeyInput.length - 8) + connectKeyInput.slice(-4) : "\u2022".repeat(connectKeyInput.length);
|
|
412
|
-
const displayKey = connectKeyInput ? masked :
|
|
1781
|
+
const displayKey = connectKeyInput ? masked : chalk4.dim("Paste or type API key here");
|
|
413
1782
|
lines.push(`${bar} Key: ${displayKey}\u2588`);
|
|
414
1783
|
if (connectErrorMsg) {
|
|
415
1784
|
lines.push("");
|
|
416
|
-
lines.push(
|
|
1785
|
+
lines.push(chalk4.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
|
|
417
1786
|
}
|
|
418
1787
|
lines.push("");
|
|
419
|
-
lines.push(
|
|
1788
|
+
lines.push(chalk4.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
|
|
420
1789
|
} else if (connectStep === "validating") {
|
|
421
|
-
lines.push(
|
|
1790
|
+
lines.push(chalk4.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
|
|
422
1791
|
lines.push("");
|
|
423
|
-
lines.push(
|
|
1792
|
+
lines.push(chalk4.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
|
|
424
1793
|
lines.push("");
|
|
425
|
-
lines.push(
|
|
1794
|
+
lines.push(chalk4.dim("Please wait while we verify your key with the provider."));
|
|
426
1795
|
}
|
|
427
1796
|
} else {
|
|
428
1797
|
if (!isProcessing && input.startsWith("/")) {
|
|
@@ -436,35 +1805,51 @@ async function interactiveChatCommand(options) {
|
|
|
436
1805
|
}
|
|
437
1806
|
const visible = matching.slice(startIdx, startIdx + MAX_VISIBLE);
|
|
438
1807
|
lines.push(
|
|
439
|
-
|
|
1808
|
+
chalk4.dim(` Commands (${menuIndex + 1}/${matching.length}) \xB7 \u2191/\u2193 navigate \xB7 Tab/Enter select \xB7 Esc cancel`)
|
|
440
1809
|
);
|
|
441
1810
|
visible.forEach((c, relIdx) => {
|
|
442
1811
|
const absIdx = startIdx + relIdx;
|
|
443
1812
|
const isSelected = absIdx === menuIndex;
|
|
444
|
-
const row = " " + c.name.padEnd(
|
|
1813
|
+
const row = " " + c.name.padEnd(15) + c.desc;
|
|
445
1814
|
const padded = row + " ".repeat(Math.max(2, width - row.length));
|
|
446
|
-
lines.push(isSelected ? peachBg(padded) :
|
|
1815
|
+
lines.push(isSelected ? peachBg(padded) : chalk4.white(row));
|
|
447
1816
|
});
|
|
448
1817
|
}
|
|
449
1818
|
}
|
|
450
1819
|
const friendlyAgent = currentAgent === "build" ? "Build" : "Plan";
|
|
1820
|
+
const bar2 = chalk4.hex(OCEAN_BLUE)("\u2502");
|
|
1821
|
+
lines.push(bar2);
|
|
451
1822
|
if (isProcessing) {
|
|
452
1823
|
const frameIdx = renderer.getThinkingFrame() % THINKING_FRAMES.length;
|
|
453
1824
|
const thinkingText = THINKING_FRAMES[frameIdx].trim();
|
|
454
|
-
lines.push(`${
|
|
1825
|
+
lines.push(`${bar2} ${chalk4.dim(thinkingText)}`);
|
|
455
1826
|
} else {
|
|
456
|
-
lines.push(`${
|
|
1827
|
+
lines.push(`${bar2} ${input}`);
|
|
457
1828
|
}
|
|
458
|
-
lines.push(
|
|
1829
|
+
lines.push(bar2);
|
|
459
1830
|
lines.push(
|
|
460
|
-
|
|
1831
|
+
`${bar2} ${chalk4.hex(OCEAN_BLUE).bold(friendlyAgent)} ${chalk4.dim("\xB7")} ${chalk4.white(friendlyModel)} ${chalk4.dim("OceanCode")}`
|
|
461
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}`);
|
|
462
1846
|
}
|
|
463
1847
|
return lines;
|
|
464
1848
|
};
|
|
465
1849
|
const paintBar = () => {
|
|
466
1850
|
try {
|
|
467
1851
|
if (!process.stdout.isTTY) return;
|
|
1852
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
468
1853
|
const lines = buildBarLines();
|
|
469
1854
|
const { rows, barStartRow, scrollBottom } = getBarMetrics(lines.length);
|
|
470
1855
|
applyScrollRegion(scrollBottom);
|
|
@@ -487,17 +1872,15 @@ async function interactiveChatCommand(options) {
|
|
|
487
1872
|
}
|
|
488
1873
|
if (!isProcessing && mode === "normal") {
|
|
489
1874
|
process.stdout.write("\x1B[?25h");
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const col = Math.min(process.stdout.columns || 80, 3 + input.length);
|
|
1875
|
+
const targetRow = barStartRow + 1;
|
|
1876
|
+
const col = Math.min(process.stdout.columns || 80, 3 + cursorIndex);
|
|
493
1877
|
process.stdout.write(`\x1B[${targetRow};${col}H`);
|
|
494
1878
|
} else if (!isProcessing && mode === "model_selector") {
|
|
495
1879
|
process.stdout.write("\x1B[?25h");
|
|
496
1880
|
const col = Math.min(process.stdout.columns || 80, 9 + modelSearch.length);
|
|
497
1881
|
process.stdout.write(`\x1B[${barStartRow + 1};${col}H`);
|
|
498
1882
|
} else {
|
|
499
|
-
process.stdout.write("\x1B[?25l");
|
|
500
|
-
process.stdout.write(`\x1B[${contentRow};1H`);
|
|
1883
|
+
process.stdout.write("\x1B[?25l\x1B8");
|
|
501
1884
|
}
|
|
502
1885
|
} catch {
|
|
503
1886
|
}
|
|
@@ -535,8 +1918,7 @@ async function interactiveChatCommand(options) {
|
|
|
535
1918
|
}
|
|
536
1919
|
} else if (part.type === "tool") {
|
|
537
1920
|
const toolName = part.tool || "tool";
|
|
538
|
-
const
|
|
539
|
-
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);
|
|
540
1922
|
const status = part.state?.status || part.status || "";
|
|
541
1923
|
renderer.handleTool(toolName, detail, part.id, status);
|
|
542
1924
|
} else if (part.type === "text" && typeof part.text === "string") {
|
|
@@ -568,7 +1950,7 @@ async function interactiveChatCommand(options) {
|
|
|
568
1950
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
569
1951
|
const errObj = event.properties?.error;
|
|
570
1952
|
const errMsg = errObj?.message || errObj?.data?.message || (typeof errObj === "string" ? errObj : "Model request failed or timed out.");
|
|
571
|
-
printToContent(
|
|
1953
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}`));
|
|
572
1954
|
render();
|
|
573
1955
|
}
|
|
574
1956
|
}, void 0, session.directory);
|
|
@@ -582,7 +1964,7 @@ async function interactiveChatCommand(options) {
|
|
|
582
1964
|
cleanup();
|
|
583
1965
|
unsubscribe();
|
|
584
1966
|
console.log(`
|
|
585
|
-
${
|
|
1967
|
+
${chalk4.cyan("\u{1F44B} Goodbye!")}
|
|
586
1968
|
`);
|
|
587
1969
|
process.exit(0);
|
|
588
1970
|
}
|
|
@@ -624,7 +2006,7 @@ async function interactiveChatCommand(options) {
|
|
|
624
2006
|
if (str === "d" || str === "D") {
|
|
625
2007
|
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
626
2008
|
removeProviderKey(prov.id);
|
|
627
|
-
|
|
2009
|
+
printToContent(chalk4.yellow(`
|
|
628
2010
|
\u2713 Disconnected API key for ${prov.name}.
|
|
629
2011
|
`));
|
|
630
2012
|
render();
|
|
@@ -657,8 +2039,8 @@ async function interactiveChatCommand(options) {
|
|
|
657
2039
|
connectStep = "provider_list";
|
|
658
2040
|
connectKeyInput = "";
|
|
659
2041
|
connectErrorMsg = "";
|
|
660
|
-
|
|
661
|
-
|
|
2042
|
+
printToContent(
|
|
2043
|
+
chalk4.green(
|
|
662
2044
|
`
|
|
663
2045
|
\u2713 Successfully verified and connected ${prov.name}! Its models are now unlocked in /models.
|
|
664
2046
|
`
|
|
@@ -685,7 +2067,7 @@ async function interactiveChatCommand(options) {
|
|
|
685
2067
|
const availableModels = getAvailableOceanModels();
|
|
686
2068
|
const searchLower = modelSearch.toLowerCase();
|
|
687
2069
|
const filtered = availableModels.filter(
|
|
688
|
-
(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)
|
|
689
2071
|
);
|
|
690
2072
|
if (key.name === "escape") {
|
|
691
2073
|
mode = "normal";
|
|
@@ -719,7 +2101,7 @@ async function interactiveChatCommand(options) {
|
|
|
719
2101
|
input = "";
|
|
720
2102
|
modelSearch = "";
|
|
721
2103
|
if (selectedFriendly) {
|
|
722
|
-
printToContent(
|
|
2104
|
+
printToContent(chalk4.green(`\u2713 Active model changed to: ${selectedFriendly} (${selectedId})`));
|
|
723
2105
|
}
|
|
724
2106
|
render();
|
|
725
2107
|
return;
|
|
@@ -745,41 +2127,71 @@ async function interactiveChatCommand(options) {
|
|
|
745
2127
|
render();
|
|
746
2128
|
return;
|
|
747
2129
|
}
|
|
748
|
-
if (
|
|
749
|
-
if (
|
|
2130
|
+
if (key.name === "up") {
|
|
2131
|
+
if (matching.length > 0) {
|
|
750
2132
|
menuIndex = (menuIndex - 1 + matching.length) % matching.length;
|
|
751
2133
|
render();
|
|
752
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;
|
|
753
2146
|
}
|
|
754
|
-
|
|
2147
|
+
}
|
|
2148
|
+
if (key.name === "down") {
|
|
2149
|
+
if (matching.length > 0) {
|
|
755
2150
|
menuIndex = (menuIndex + 1) % matching.length;
|
|
756
2151
|
render();
|
|
757
2152
|
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
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;
|
|
762
2160
|
}
|
|
2161
|
+
cursorIndex = input.length;
|
|
763
2162
|
render();
|
|
764
2163
|
return;
|
|
765
2164
|
}
|
|
766
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
|
+
}
|
|
767
2174
|
if (key.name === "return") {
|
|
768
2175
|
let submitted = input.trim();
|
|
769
2176
|
if (matching.length > 0 && matching[menuIndex]) {
|
|
770
2177
|
submitted = matching[menuIndex].name;
|
|
771
2178
|
}
|
|
772
2179
|
input = "";
|
|
2180
|
+
cursorIndex = 0;
|
|
2181
|
+
historyIndex = -1;
|
|
773
2182
|
menuIndex = 0;
|
|
774
2183
|
if (!submitted) {
|
|
775
2184
|
paintBar();
|
|
776
2185
|
return;
|
|
777
2186
|
}
|
|
2187
|
+
if (promptHistory[promptHistory.length - 1] !== submitted) {
|
|
2188
|
+
promptHistory.push(submitted);
|
|
2189
|
+
}
|
|
778
2190
|
if (submitted === "/exit" || submitted === "/quit") {
|
|
779
2191
|
cleanup();
|
|
780
2192
|
unsubscribe();
|
|
781
2193
|
console.log(`
|
|
782
|
-
${
|
|
2194
|
+
${chalk4.cyan("\u{1F44B} Goodbye!")}
|
|
783
2195
|
`);
|
|
784
2196
|
process.exit(0);
|
|
785
2197
|
}
|
|
@@ -791,7 +2203,7 @@ async function interactiveChatCommand(options) {
|
|
|
791
2203
|
process.stdout.write("\x1B[1;1H");
|
|
792
2204
|
}
|
|
793
2205
|
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
794
|
-
|
|
2206
|
+
process.stdout.write("\x1B7");
|
|
795
2207
|
lastBarStartRow = 0;
|
|
796
2208
|
paintBar();
|
|
797
2209
|
return;
|
|
@@ -799,10 +2211,10 @@ async function interactiveChatCommand(options) {
|
|
|
799
2211
|
if (submitted === "/init") {
|
|
800
2212
|
try {
|
|
801
2213
|
const res = initializeAgentsDoc(process.cwd());
|
|
802
|
-
printToContent(`${
|
|
803
|
-
${
|
|
2214
|
+
printToContent(`${chalk4.green(`\u2714 ${res.summary}`)}
|
|
2215
|
+
${chalk4.dim(` Location: ${res.filePath}`)}`);
|
|
804
2216
|
} catch (err) {
|
|
805
|
-
printToContent(
|
|
2217
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Initialization failed: ${err.message}`));
|
|
806
2218
|
}
|
|
807
2219
|
render();
|
|
808
2220
|
return;
|
|
@@ -810,9 +2222,9 @@ ${chalk.dim(` Location: ${res.filePath}`)}`);
|
|
|
810
2222
|
if (submitted === "/new") {
|
|
811
2223
|
try {
|
|
812
2224
|
session = await client.createSession("Ocean Session", process.cwd());
|
|
813
|
-
printToContent(
|
|
2225
|
+
printToContent(chalk4.green("\u2714 Started a new coding session."));
|
|
814
2226
|
} catch (err) {
|
|
815
|
-
printToContent(
|
|
2227
|
+
printToContent(chalk4.red(`Failed to create new session: ${err.message}`));
|
|
816
2228
|
}
|
|
817
2229
|
render();
|
|
818
2230
|
return;
|
|
@@ -822,22 +2234,22 @@ ${chalk.dim(` Location: ${res.filePath}`)}`);
|
|
|
822
2234
|
try {
|
|
823
2235
|
const success = await client.revertSession(session.id, lastUserMsgId);
|
|
824
2236
|
if (success) {
|
|
825
|
-
printToContent(
|
|
2237
|
+
printToContent(chalk4.green("\u2714 Reverted the last AI action and restored previous file states."));
|
|
826
2238
|
} else {
|
|
827
|
-
printToContent(
|
|
2239
|
+
printToContent(chalk4.yellow("Could not revert the last action."));
|
|
828
2240
|
}
|
|
829
2241
|
} catch (err) {
|
|
830
|
-
printToContent(
|
|
2242
|
+
printToContent(chalk4.red(`Revert failed: ${err.message}`));
|
|
831
2243
|
}
|
|
832
2244
|
} else {
|
|
833
|
-
printToContent(
|
|
2245
|
+
printToContent(chalk4.yellow("No previous action found to undo."));
|
|
834
2246
|
}
|
|
835
2247
|
render();
|
|
836
2248
|
return;
|
|
837
2249
|
}
|
|
838
2250
|
if (submitted === "/redo") {
|
|
839
2251
|
if (!lastSubmittedPrompt) {
|
|
840
|
-
printToContent(
|
|
2252
|
+
printToContent(chalk4.yellow("No previous action to redo."));
|
|
841
2253
|
render();
|
|
842
2254
|
return;
|
|
843
2255
|
}
|
|
@@ -847,7 +2259,7 @@ ${chalk.dim(` Location: ${res.filePath}`)}`);
|
|
|
847
2259
|
const goalDesc = submitted.replace(/^\/goal\s*/, "").trim();
|
|
848
2260
|
if (!goalDesc) {
|
|
849
2261
|
printToContent(
|
|
850
|
-
|
|
2262
|
+
chalk4.yellow(
|
|
851
2263
|
"Please provide a goal description.\nUsage: /goal <your objective or feature description>\nExample: /goal Add user authentication with JWT and refresh tokens"
|
|
852
2264
|
)
|
|
853
2265
|
);
|
|
@@ -869,17 +2281,17 @@ Please accomplish this goal systematically:
|
|
|
869
2281
|
const provArg = parts[1]?.toLowerCase();
|
|
870
2282
|
const keyArg = parts[2];
|
|
871
2283
|
if (provArg && keyArg) {
|
|
872
|
-
printToContent(
|
|
2284
|
+
printToContent(chalk4.cyan(`Validating API key with ${provArg.toUpperCase()}...`));
|
|
873
2285
|
const val = await validateProviderKey(provArg, keyArg);
|
|
874
2286
|
if (val.valid) {
|
|
875
2287
|
saveProviderKey(provArg, keyArg);
|
|
876
2288
|
printToContent(
|
|
877
|
-
|
|
2289
|
+
chalk4.green(
|
|
878
2290
|
`\u2714 Successfully verified and connected ${provArg.toUpperCase()}! Its models are now unlocked in /models.`
|
|
879
2291
|
)
|
|
880
2292
|
);
|
|
881
2293
|
} else {
|
|
882
|
-
printToContent(
|
|
2294
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}`));
|
|
883
2295
|
}
|
|
884
2296
|
render();
|
|
885
2297
|
return;
|
|
@@ -901,7 +2313,7 @@ Please accomplish this goal systematically:
|
|
|
901
2313
|
const serverCmdOrUrl = parts.slice(3).join(" ");
|
|
902
2314
|
if (!serverName || !serverCmdOrUrl) {
|
|
903
2315
|
printToContent(
|
|
904
|
-
|
|
2316
|
+
chalk4.yellow(
|
|
905
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"
|
|
906
2318
|
)
|
|
907
2319
|
);
|
|
@@ -912,7 +2324,7 @@ Please accomplish this goal systematically:
|
|
|
912
2324
|
const mcpConfig = isUrl ? { type: "remote", url: serverCmdOrUrl, enabled: true } : { type: "local", command: serverCmdOrUrl.split(" "), enabled: true };
|
|
913
2325
|
saveMcpServer(serverName, mcpConfig);
|
|
914
2326
|
printToContent(
|
|
915
|
-
|
|
2327
|
+
chalk4.green(`\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.`)
|
|
916
2328
|
);
|
|
917
2329
|
render();
|
|
918
2330
|
return;
|
|
@@ -922,7 +2334,7 @@ Please accomplish this goal systematically:
|
|
|
922
2334
|
const names = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
923
2335
|
if (names.length === 0) {
|
|
924
2336
|
printToContent(
|
|
925
|
-
|
|
2337
|
+
chalk4.dim(
|
|
926
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 ."
|
|
927
2339
|
)
|
|
928
2340
|
);
|
|
@@ -932,12 +2344,12 @@ Please accomplish this goal systematically:
|
|
|
932
2344
|
const isLive = live[n] !== void 0;
|
|
933
2345
|
const type = conf.type || (conf.url ? "remote" : "local");
|
|
934
2346
|
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
935
|
-
const badge = isLive ?
|
|
2347
|
+
const badge = isLive ? chalk4.green("\u25CF Connected") : chalk4.cyan("\u25CF Configured");
|
|
936
2348
|
return ` \u2022 ${n} (${type}): ${badge}
|
|
937
|
-
${
|
|
2349
|
+
${chalk4.dim(target)}`;
|
|
938
2350
|
}).join("\n");
|
|
939
2351
|
printToContent(
|
|
940
|
-
|
|
2352
|
+
chalk4.bold("Model Context Protocol (MCP) Servers:\n") + lines + chalk4.dim("\n\nAdd new servers with: /mcp add <name> <command>")
|
|
941
2353
|
);
|
|
942
2354
|
}
|
|
943
2355
|
render();
|
|
@@ -954,7 +2366,7 @@ Please accomplish this goal systematically:
|
|
|
954
2366
|
} else {
|
|
955
2367
|
currentAgent = currentAgent === "build" ? "plan" : "build";
|
|
956
2368
|
}
|
|
957
|
-
printToContent(
|
|
2369
|
+
printToContent(chalk4.cyan(`\u2713 Switched mode to: ${currentAgent.toUpperCase()}`));
|
|
958
2370
|
render();
|
|
959
2371
|
return;
|
|
960
2372
|
}
|
|
@@ -962,16 +2374,16 @@ Please accomplish this goal systematically:
|
|
|
962
2374
|
try {
|
|
963
2375
|
const diffs = await client.getSessionDiff(session.id);
|
|
964
2376
|
if (!diffs || diffs.length === 0) {
|
|
965
|
-
printToContent(
|
|
2377
|
+
printToContent(chalk4.dim("No file modifications recorded in the current session."));
|
|
966
2378
|
} else {
|
|
967
2379
|
const diffSummary = diffs.map(
|
|
968
2380
|
(d) => ` \u2022 ${d.path || d.file || "file"} (+${d.additions || 0} -${d.deletions || 0})`
|
|
969
2381
|
).join("\n");
|
|
970
|
-
printToContent(
|
|
2382
|
+
printToContent(chalk4.bold(`Modified Files (${diffs.length}):
|
|
971
2383
|
`) + diffSummary);
|
|
972
2384
|
}
|
|
973
2385
|
} catch (err) {
|
|
974
|
-
printToContent(
|
|
2386
|
+
printToContent(chalk4.red(`Failed to retrieve diff: ${err.message}`));
|
|
975
2387
|
}
|
|
976
2388
|
render();
|
|
977
2389
|
return;
|
|
@@ -979,9 +2391,9 @@ Please accomplish this goal systematically:
|
|
|
979
2391
|
if (submitted === "/compact") {
|
|
980
2392
|
try {
|
|
981
2393
|
await client.summarizeSession(session.id);
|
|
982
|
-
printToContent(
|
|
2394
|
+
printToContent(chalk4.green("\u2713 Session context memory compacted and summarized successfully."));
|
|
983
2395
|
} catch (err) {
|
|
984
|
-
printToContent(
|
|
2396
|
+
printToContent(chalk4.red(`Compact failed: ${err.message}`));
|
|
985
2397
|
}
|
|
986
2398
|
render();
|
|
987
2399
|
return;
|
|
@@ -993,7 +2405,7 @@ Please accomplish this goal systematically:
|
|
|
993
2405
|
const resolvedId = getBackendModelId(targetArg);
|
|
994
2406
|
currentBackendModel = resolvedId;
|
|
995
2407
|
const friendly = getFriendlyModelName(resolvedId);
|
|
996
|
-
printToContent(
|
|
2408
|
+
printToContent(chalk4.green(`\u2713 Switched model to: ${friendly} (${resolvedId})`));
|
|
997
2409
|
render();
|
|
998
2410
|
return;
|
|
999
2411
|
} else {
|
|
@@ -1004,10 +2416,366 @@ Please accomplish this goal systematically:
|
|
|
1004
2416
|
return;
|
|
1005
2417
|
}
|
|
1006
2418
|
}
|
|
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}`));
|
|
2771
|
+
}
|
|
2772
|
+
render();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
1007
2775
|
if (submitted === "/help") {
|
|
1008
2776
|
const helpLines = [
|
|
1009
|
-
|
|
1010
|
-
...COMMANDS.map((c) => ` ${
|
|
2777
|
+
chalk4.bold("Available Commands:"),
|
|
2778
|
+
...COMMANDS.map((c) => ` ${chalk4.hex(OCEAN_BLUE).bold(c.name.padEnd(15))} ${chalk4.dim(c.desc)}`)
|
|
1011
2779
|
].join("\n");
|
|
1012
2780
|
printToContent(helpLines);
|
|
1013
2781
|
render();
|
|
@@ -1025,17 +2793,27 @@ Please accomplish this goal systematically:
|
|
|
1025
2793
|
return;
|
|
1026
2794
|
}
|
|
1027
2795
|
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
1028
|
-
process.stdout.write(
|
|
2796
|
+
process.stdout.write("\x1B8");
|
|
1029
2797
|
renderUserMessageCard(submitted, friendlyName);
|
|
1030
|
-
|
|
1031
|
-
contentRow = Math.min(currentScrollBottom || 20, contentRow + promptLines);
|
|
2798
|
+
process.stdout.write("\x1B7");
|
|
1032
2799
|
isProcessing = true;
|
|
1033
2800
|
userMsgId = null;
|
|
1034
2801
|
lastSubmittedPrompt = submitted;
|
|
1035
2802
|
paintBar();
|
|
1036
2803
|
resetActivityTimeout();
|
|
1037
2804
|
renderer.start(friendlyName);
|
|
1038
|
-
|
|
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, {
|
|
1039
2817
|
model: currentBackendModel,
|
|
1040
2818
|
agent: currentAgent,
|
|
1041
2819
|
directory: session.directory
|
|
@@ -1043,115 +2821,151 @@ Please accomplish this goal systematically:
|
|
|
1043
2821
|
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
1044
2822
|
isProcessing = false;
|
|
1045
2823
|
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
1046
|
-
printToContent(
|
|
2824
|
+
printToContent(chalk4.red(`\u26A0\uFE0F Prompt Error: ${err.message}`));
|
|
1047
2825
|
paintBar();
|
|
1048
2826
|
});
|
|
1049
2827
|
return;
|
|
1050
2828
|
}
|
|
1051
|
-
if (key.name === "
|
|
1052
|
-
|
|
1053
|
-
menuIndex = 0;
|
|
2829
|
+
if (key.name === "left") {
|
|
2830
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
1054
2831
|
render();
|
|
1055
2832
|
return;
|
|
1056
2833
|
}
|
|
1057
|
-
if (
|
|
1058
|
-
input
|
|
1059
|
-
menuIndex = 0;
|
|
2834
|
+
if (key.name === "right") {
|
|
2835
|
+
cursorIndex = Math.min(input.length, cursorIndex + 1);
|
|
1060
2836
|
render();
|
|
1061
2837
|
return;
|
|
1062
2838
|
}
|
|
2839
|
+
if (key.name === "home") {
|
|
2840
|
+
cursorIndex = 0;
|
|
2841
|
+
render();
|
|
2842
|
+
return;
|
|
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
|
+
}
|
|
1063
2880
|
};
|
|
1064
2881
|
process.stdin.on("keypress", onKeypress);
|
|
2882
|
+
process.title = "oceancode";
|
|
1065
2883
|
if (process.stdout.isTTY) {
|
|
2884
|
+
process.stdout.write("\x1B]0;oceancode\x07");
|
|
2885
|
+
process.stdout.write("\x1B]2;oceancode\x07");
|
|
1066
2886
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2887
|
+
process.stdout.write("\x1B[?1000l\x1B[?1002l\x1B[?1006l");
|
|
1067
2888
|
const { scrollBottom } = getBarMetrics(3);
|
|
1068
2889
|
applyScrollRegion(scrollBottom);
|
|
1069
2890
|
process.stdout.write("\x1B[1;1H");
|
|
1070
2891
|
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
1071
|
-
|
|
2892
|
+
process.stdout.write("\x1B7");
|
|
1072
2893
|
paintBar();
|
|
1073
2894
|
}
|
|
1074
2895
|
}
|
|
1075
2896
|
|
|
1076
2897
|
// src/commands/models.ts
|
|
1077
|
-
import
|
|
2898
|
+
import chalk5 from "chalk";
|
|
1078
2899
|
async function listModelsCommand(options) {
|
|
1079
|
-
const baseUrl = await ensureServer();
|
|
1080
|
-
const client = new OceanClient(baseUrl);
|
|
1081
2900
|
const { leftMargin } = getLayoutDimensions();
|
|
1082
2901
|
const indent = " ".repeat(leftMargin);
|
|
1083
|
-
const
|
|
1084
|
-
|
|
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
|
+
}
|
|
1085
2917
|
console.log("");
|
|
1086
|
-
console.log(`${indent}${
|
|
1087
|
-
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")}
|
|
1088
2920
|
`);
|
|
1089
|
-
console.log(`${indent}${
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
console.log(`${indent}${chalk2.bold.hex("#00f2fe")("Free Built-in Models:")}`);
|
|
1093
|
-
for (const m of freeModels) {
|
|
1094
|
-
const badge = chalk2.bgHex("#00b4d8").black.bold(` ${m.tag} `);
|
|
1095
|
-
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1096
|
-
}
|
|
1097
|
-
if (byokModels.length > 0) {
|
|
1098
|
-
console.log(`
|
|
1099
|
-
${indent}${chalk2.bold.hex("#3a7bd5")("Connected BYOK Models:")}`);
|
|
1100
|
-
for (const m of byokModels) {
|
|
1101
|
-
const badge = chalk2.bgHex("#10b981").black.bold(` ${m.tag} `);
|
|
1102
|
-
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
const unconnected = SUPPORTED_BYOK_PROVIDERS.filter((p) => !creds[p.id]?.key);
|
|
1106
|
-
if (unconnected.length > 0) {
|
|
1107
|
-
console.log(`
|
|
1108
|
-
${indent}${chalk2.dim("Connect more models via 'oceancode connect <provider> <key>' or /connect in chat:")}`);
|
|
1109
|
-
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)}`);
|
|
1110
2924
|
}
|
|
1111
2925
|
console.log(`
|
|
1112
|
-
${indent}${
|
|
2926
|
+
${indent}${chalk5.dim("Tip: Type /models in interactive chat to switch models with arrow keys.")}
|
|
1113
2927
|
`);
|
|
1114
2928
|
}
|
|
1115
2929
|
|
|
1116
2930
|
// src/commands/session.ts
|
|
1117
|
-
import
|
|
2931
|
+
import chalk6 from "chalk";
|
|
1118
2932
|
async function sessionCommand() {
|
|
1119
2933
|
const baseUrl = await ensureServer();
|
|
1120
2934
|
const client = new OceanClient(baseUrl);
|
|
1121
2935
|
const { leftMargin } = getLayoutDimensions();
|
|
1122
2936
|
const indent = " ".repeat(leftMargin);
|
|
1123
2937
|
console.log("");
|
|
1124
|
-
console.log(`${indent}${
|
|
1125
|
-
console.log(`${indent}${
|
|
2938
|
+
console.log(`${indent}${chalk6.hex("#00d2ff").bold("Recent Sessions")}`);
|
|
2939
|
+
console.log(`${indent}${chalk6.dim("-".repeat(54))}`);
|
|
1126
2940
|
try {
|
|
1127
2941
|
const sessions = await client.listSessions(10);
|
|
1128
2942
|
if (!sessions || sessions.length === 0) {
|
|
1129
|
-
console.log(`${indent}${
|
|
2943
|
+
console.log(`${indent}${chalk6.dim("No previous sessions found.")}
|
|
1130
2944
|
`);
|
|
1131
2945
|
return;
|
|
1132
2946
|
}
|
|
1133
2947
|
for (const s of sessions) {
|
|
1134
2948
|
const date = new Date(s.time?.created || Date.now()).toLocaleDateString();
|
|
1135
2949
|
const tokens = (s.tokens?.input || 0) + (s.tokens?.output || 0);
|
|
1136
|
-
console.log(`${indent}${
|
|
1137
|
-
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)}`);
|
|
1138
2952
|
console.log("");
|
|
1139
2953
|
}
|
|
1140
2954
|
} catch (err) {
|
|
1141
|
-
console.error(`${indent}${
|
|
2955
|
+
console.error(`${indent}${chalk6.red(`Failed to list sessions: ${err.message}`)}`);
|
|
1142
2956
|
}
|
|
1143
2957
|
}
|
|
1144
2958
|
|
|
1145
2959
|
// src/commands/stats.ts
|
|
1146
|
-
import
|
|
2960
|
+
import chalk7 from "chalk";
|
|
1147
2961
|
async function statsCommand() {
|
|
1148
2962
|
const baseUrl = await ensureServer();
|
|
1149
2963
|
const client = new OceanClient(baseUrl);
|
|
1150
2964
|
const { leftMargin } = getLayoutDimensions();
|
|
1151
2965
|
const indent = " ".repeat(leftMargin);
|
|
1152
2966
|
console.log("");
|
|
1153
|
-
console.log(`${indent}${
|
|
1154
|
-
console.log(`${indent}${
|
|
2967
|
+
console.log(`${indent}${chalk7.hex("#00d2ff").bold("Usage & Analytics")}`);
|
|
2968
|
+
console.log(`${indent}${chalk7.dim("-".repeat(54))}`);
|
|
1155
2969
|
try {
|
|
1156
2970
|
const sessions = await client.listSessions(100);
|
|
1157
2971
|
let totalTokens = 0;
|
|
@@ -1164,25 +2978,30 @@ async function statsCommand() {
|
|
|
1164
2978
|
totalCost += s.cost || 0;
|
|
1165
2979
|
}
|
|
1166
2980
|
totalTokens = totalInput + totalOutput;
|
|
1167
|
-
console.log(`${indent}${
|
|
1168
|
-
console.log(`${indent}${
|
|
1169
|
-
console.log(`${indent}${
|
|
1170
|
-
console.log(`${indent}${
|
|
1171
|
-
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 ")}`);
|
|
1172
2986
|
console.log("");
|
|
1173
2987
|
} catch (err) {
|
|
1174
|
-
console.error(`${indent}${
|
|
2988
|
+
console.error(`${indent}${chalk7.red(`Failed to load stats: ${err.message}`)}`);
|
|
1175
2989
|
}
|
|
1176
2990
|
}
|
|
1177
2991
|
|
|
1178
2992
|
// src/index.ts
|
|
1179
|
-
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
|
+
}
|
|
1180
2999
|
var program = new Command();
|
|
1181
3000
|
program.enablePositionalOptions();
|
|
1182
|
-
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", `
|
|
1183
3002
|
${getBigLogo()}
|
|
1184
3003
|
`);
|
|
1185
|
-
program.option("-m, --model <model>", "Model to use (default:
|
|
3004
|
+
program.option("-m, --model <model>", "Model to use (default: ocean/Qwen3.6-35B-A3B)").action(async (options) => {
|
|
1186
3005
|
try {
|
|
1187
3006
|
await interactiveChatCommand({ model: options.model });
|
|
1188
3007
|
} catch (err) {
|
|
@@ -1191,13 +3010,13 @@ program.option("-m, --model <model>", "Model to use (default: opencode/muse-spar
|
|
|
1191
3010
|
process.stdout.write(`\x1B[1;${rows}r\x1B[?25h
|
|
1192
3011
|
`);
|
|
1193
3012
|
}
|
|
1194
|
-
console.error(
|
|
3013
|
+
console.error(chalk8.red(`
|
|
1195
3014
|
An error occurred: ${err?.message || err}
|
|
1196
3015
|
`));
|
|
1197
3016
|
process.exit(1);
|
|
1198
3017
|
}
|
|
1199
3018
|
});
|
|
1200
|
-
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) => {
|
|
1201
3020
|
const opts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : options;
|
|
1202
3021
|
await runCommand(messages, opts);
|
|
1203
3022
|
process.exit(0);
|
|
@@ -1205,36 +3024,36 @@ program.command("run [message...]").description("Run a prompt or instruction dir
|
|
|
1205
3024
|
program.command("init").description("Initialize codebase context and generate or update AGENTS.md").action(async () => {
|
|
1206
3025
|
try {
|
|
1207
3026
|
const res = initializeAgentsDoc(process.cwd());
|
|
1208
|
-
console.log(
|
|
1209
|
-
console.log(
|
|
3027
|
+
console.log(chalk8.green(`\u2714 ${res.summary}`));
|
|
3028
|
+
console.log(chalk8.dim(` Path: ${res.filePath}`));
|
|
1210
3029
|
} catch (err) {
|
|
1211
|
-
console.error(
|
|
3030
|
+
console.error(chalk8.red(`Error initializing AGENTS.md: ${err.message}`));
|
|
1212
3031
|
}
|
|
1213
3032
|
process.exit(0);
|
|
1214
3033
|
});
|
|
1215
3034
|
program.command("connect [provider] [key]").description("Link provider API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq, OpenRouter)").action(async (provider, key) => {
|
|
1216
3035
|
if (provider && key) {
|
|
1217
|
-
console.log(
|
|
3036
|
+
console.log(chalk8.cyan(`Validating API key with ${provider.toUpperCase()}...`));
|
|
1218
3037
|
const val = await validateProviderKey(provider, key);
|
|
1219
3038
|
if (val.valid) {
|
|
1220
3039
|
saveProviderKey(provider, key);
|
|
1221
|
-
console.log(
|
|
3040
|
+
console.log(chalk8.green(`\u2714 ${val.message}`));
|
|
1222
3041
|
} else {
|
|
1223
|
-
console.error(
|
|
3042
|
+
console.error(chalk8.red(`\u2716 Validation failed: ${val.message}`));
|
|
1224
3043
|
process.exit(1);
|
|
1225
3044
|
}
|
|
1226
3045
|
process.exit(0);
|
|
1227
3046
|
}
|
|
1228
3047
|
const creds = getStoredCredentials();
|
|
1229
|
-
console.log(
|
|
3048
|
+
console.log(chalk8.bold("\nBring Your Own Key (BYOK) Status:"));
|
|
1230
3049
|
for (const p of SUPPORTED_BYOK_PROVIDERS) {
|
|
1231
|
-
const isSet = Boolean(creds[p.id]);
|
|
1232
|
-
const badge = isSet ?
|
|
3050
|
+
const isSet = Boolean(creds[p.id]?.key);
|
|
3051
|
+
const badge = isSet ? chalk8.green("\u2714 Connected") : chalk8.dim("Not linked");
|
|
1233
3052
|
console.log(` \u2022 ${p.name.padEnd(16)} (${p.id}): ${badge}`);
|
|
1234
3053
|
}
|
|
1235
|
-
console.log(
|
|
1236
|
-
console.log(
|
|
1237
|
-
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:"));
|
|
1238
3057
|
console.log(" oceancode connect openai sk-proj-...");
|
|
1239
3058
|
console.log(" oceancode connect anthropic sk-ant-...");
|
|
1240
3059
|
console.log(" oceancode connect google AIzaSy...");
|
|
@@ -1245,7 +3064,7 @@ program.command("mcp").description("List or configure Model Context Protocol (MC
|
|
|
1245
3064
|
const act = (action || "list").toLowerCase();
|
|
1246
3065
|
if (act === "add") {
|
|
1247
3066
|
if (!name || !commandArgs || commandArgs.length === 0) {
|
|
1248
|
-
console.error(
|
|
3067
|
+
console.error(chalk8.red("Usage: oceancode mcp add <name> <command-or-url>"));
|
|
1249
3068
|
console.log("Example: oceancode mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .");
|
|
1250
3069
|
process.exit(1);
|
|
1251
3070
|
}
|
|
@@ -1253,27 +3072,27 @@ program.command("mcp").description("List or configure Model Context Protocol (MC
|
|
|
1253
3072
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
1254
3073
|
const config = isUrl ? { type: "remote", url: target, enabled: true } : { type: "local", command: commandArgs, enabled: true };
|
|
1255
3074
|
saveMcpServer(name, config);
|
|
1256
|
-
console.log(
|
|
3075
|
+
console.log(chalk8.green(`\u2714 Added MCP server "${name}" (${config.type}) to opencode.json`));
|
|
1257
3076
|
process.exit(0);
|
|
1258
3077
|
}
|
|
1259
3078
|
const baseUrl = await ensureServer();
|
|
1260
3079
|
const configured = getConfiguredMcpServers();
|
|
1261
3080
|
const live = await fetchLiveMcpStatus(baseUrl);
|
|
1262
3081
|
const allNames = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
1263
|
-
console.log(
|
|
3082
|
+
console.log(chalk8.bold("\nModel Context Protocol (MCP) Servers:"));
|
|
1264
3083
|
if (allNames.length === 0) {
|
|
1265
|
-
console.log(
|
|
1266
|
-
console.log(
|
|
1267
|
-
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>"));
|
|
1268
3087
|
} else {
|
|
1269
3088
|
for (const n of allNames) {
|
|
1270
3089
|
const conf = configured[n] || {};
|
|
1271
3090
|
const isLive = live[n] !== void 0;
|
|
1272
3091
|
const type = conf.type || (conf.url ? "remote" : "local");
|
|
1273
3092
|
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
1274
|
-
const status = isLive ?
|
|
1275
|
-
console.log(` \u2022 ${
|
|
1276
|
-
${
|
|
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)}`);
|
|
1277
3096
|
}
|
|
1278
3097
|
}
|
|
1279
3098
|
console.log();
|
|
@@ -1293,6 +3112,6 @@ program.command("stats").description("View token consumption and session statist
|
|
|
1293
3112
|
});
|
|
1294
3113
|
program.command("serve").description("Ensure or start the Ocean server backend").action(async () => {
|
|
1295
3114
|
const url = await ensureServer();
|
|
1296
|
-
console.log(
|
|
3115
|
+
console.log(chalk8.green(`\u2714 Ocean backend is running at: ${url}`));
|
|
1297
3116
|
});
|
|
1298
3117
|
program.parse(process.argv);
|