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 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 fs from "fs";
31
- import path from "path";
1234
+ import fs2 from "fs";
1235
+ import path2 from "path";
32
1236
  function initializeAgentsDoc(workspaceDir = process.cwd()) {
33
- const agentsPath = path.join(workspaceDir, "AGENTS.md");
34
- const alreadyExists = fs.existsSync(agentsPath);
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 = path.join(workspaceDir, "package.json");
41
- const tsConfigPath = path.join(workspaceDir, "tsconfig.json");
42
- const pyProjectPath = path.join(workspaceDir, "pyproject.toml");
43
- const reqTxtPath = path.join(workspaceDir, "requirements.txt");
44
- const cargoPath = path.join(workspaceDir, "Cargo.toml");
45
- const goModPath = path.join(workspaceDir, "go.mod");
46
- if (fs.existsSync(pkgJsonPath)) {
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(fs.readFileSync(pkgJsonPath, "utf-8"));
49
- projectType = `Node.js / JavaScript${fs.existsSync(tsConfigPath) ? " / TypeScript" : ""}`;
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 (fs.existsSync(pyProjectPath) || fs.existsSync(reqTxtPath)) {
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 (fs.existsSync(cargoPath)) {
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 (fs.existsSync(goModPath)) {
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 = fs.readdirSync(workspaceDir, { withFileTypes: true });
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 markdown tables, clear 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.
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
- fs.writeFileSync(agentsPath, agentsContent, "utf-8");
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 path2 from "path";
134
- import os from "os";
135
- import fs2 from "fs";
1488
+ import path4 from "path";
1489
+ import os3 from "os";
1490
+ import fs4 from "fs";
136
1491
  function getOpencodeConfigPath() {
137
- return path2.join(os.homedir(), ".config", "oceancode", "opencode", "opencode.json");
1492
+ return path4.join(os3.homedir(), ".config", "oceancode", "opencode", "opencode.json");
138
1493
  }
139
1494
  function getConfiguredMcpServers() {
140
1495
  const configPath = getOpencodeConfigPath();
141
- if (!fs2.existsSync(configPath)) return {};
1496
+ if (!fs4.existsSync(configPath)) return {};
142
1497
  try {
143
- const raw = fs2.readFileSync(configPath, "utf-8");
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 = path2.dirname(configPath);
153
- fs2.mkdirSync(dir, { recursive: true });
1507
+ const dir = path4.dirname(configPath);
1508
+ fs4.mkdirSync(dir, { recursive: true });
154
1509
  let fullConfig = {};
155
- if (fs2.existsSync(configPath)) {
1510
+ if (fs4.existsSync(configPath)) {
156
1511
  try {
157
- fullConfig = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
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
- fs2.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), "utf-8");
1518
+ fs4.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), "utf-8");
164
1519
  try {
165
- const projectConfig = path2.join(process.cwd(), "opencode.json");
1520
+ const projectConfig = path4.join(process.cwd(), "opencode.json");
166
1521
  let proj = {};
167
- if (fs2.existsSync(projectConfig)) {
168
- proj = JSON.parse(fs2.readFileSync(projectConfig, "utf-8"));
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
- fs2.writeFileSync(projectConfig, JSON.stringify(proj, null, 2), "utf-8");
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
- chalk.hex("#f59e0b").bold(" \u224B Thinking "),
206
- chalk.hex("#fbbf24").bold(" \u224B Thinking. "),
207
- chalk.hex("#fde68a").bold(" \u224B Thinking.. "),
208
- chalk.hex("#fef08a").bold(" \u224B Thinking..."),
209
- chalk.hex("#fde68a").bold(" \u224B Thinking.. "),
210
- chalk.hex("#fbbf24").bold(" \u224B Thinking. "),
211
- chalk.hex("#f59e0b").bold(" \u224B Thinking "),
212
- chalk.dim(" \u224B Thinking ")
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) : "opencode/muse-spark-1.3-contributor-free";
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
- const { runCommand: runCommand2 } = await import("./run-WPMSAWQ3.js");
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(`\x1B[${contentRow};1H`);
1630
+ process.stdout.write("\x1B8");
259
1631
  const formatted = text.endsWith("\n") ? text : text + "\n";
260
1632
  process.stdout.write(formatted);
261
- const lineCount = (formatted.match(/\n/g) || []).length;
262
- contentRow = Math.min(currentScrollBottom || 20, contentRow + lineCount);
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
- chalk.yellow(
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 = chalk.hex(OCEAN_BLUE)("\u2502");
1714
+ const bar = chalk4.hex(OCEAN_BLUE)("\u2502");
342
1715
  const width = Math.min(cols - 4, 80);
343
- const peachBg = chalk.bgHex("#fba978").black;
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)) + chalk.dim("esc");
347
- lines.push(chalk.bold.white(header));
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}` : chalk.dim("Search models (e.g. 1m, reasoning, gpt-4o, claude)")
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) || m.tag.toLowerCase().includes(searchLower) || m.description.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(chalk.dim(" No matching models found."));
1732
+ lines.push(chalk4.dim(" No matching models found."));
360
1733
  } else {
361
- const MAX_MODELS = 6;
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 - 2, filtered.length - MAX_MODELS));
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(chalk.dim(` Models (${modelIndex + 1}/${filtered.length}) \xB7 \u2191/\u2193 to navigate`));
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 tag = m.tag || (m.isFree ? "Free" : "BYOK");
376
- const spaces = Math.max(2, width - left.length - tag.length);
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(chalk.white(row));
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)) + chalk.dim("esc");
390
- lines.push(chalk.bold.white(header));
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(chalk.dim("Select provider with \u2191/\u2193 and press Enter (or 'd' to disconnect):"));
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 ? chalk.green("\u2714 Connected") : chalk.dim("Not linked");
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) : chalk.white(row));
1771
+ lines.push(idx === connectProviderIndex ? peachBg(row) : chalk4.white(row));
403
1772
  });
404
1773
  lines.push("");
405
- lines.push(chalk.dim("Enter: Link API Key \u2022 d: Disconnect \u2022 Esc: Close"));
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(chalk.bold.hex("#38bdf8")(`Provider: ${prov.name}`));
409
- lines.push(chalk.dim(`Placeholder format: ${prov.placeholder}`));
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 : chalk.dim("Paste or type API key here");
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(chalk.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
1785
+ lines.push(chalk4.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
417
1786
  }
418
1787
  lines.push("");
419
- lines.push(chalk.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
1788
+ lines.push(chalk4.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
420
1789
  } else if (connectStep === "validating") {
421
- lines.push(chalk.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
1790
+ lines.push(chalk4.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
422
1791
  lines.push("");
423
- lines.push(chalk.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
1792
+ lines.push(chalk4.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
424
1793
  lines.push("");
425
- lines.push(chalk.dim("Please wait while we verify your key with the provider."));
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
- chalk.dim(` Commands (${menuIndex + 1}/${matching.length}) \xB7 \u2191/\u2193 navigate \xB7 Tab/Enter select \xB7 Esc cancel`)
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(12) + c.desc;
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) : chalk.white(row));
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(`${bar} ${thinkingText}`);
1825
+ lines.push(`${bar2} ${chalk4.dim(thinkingText)}`);
455
1826
  } else {
456
- lines.push(`${bar} ${input}\u2588`);
1827
+ lines.push(`${bar2} ${input}`);
457
1828
  }
458
- lines.push(`${bar}`);
1829
+ lines.push(bar2);
459
1830
  lines.push(
460
- ` ${chalk.hex(OCEAN_BLUE).bold(friendlyAgent)} ${chalk.dim("\xB7")} ${chalk.white(friendlyModel)} ${chalk.dim("\xB7 OceanCode")}`
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 inputLineIdx = Math.max(0, lines.length - 3);
491
- const targetRow = barStartRow + inputLineIdx;
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 inputObj = part.state?.input || {};
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(chalk.red(`\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}`));
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
- ${chalk.cyan("\u{1F44B} Goodbye!")}
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
- console.log(chalk.yellow(`
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
- console.log(
661
- chalk.green(
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(chalk.green(`\u2713 Active model changed to: ${selectedFriendly} (${selectedId})`));
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 (matching.length > 0) {
749
- if (key.name === "up") {
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
- if (key.name === "down") {
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
- if (key.name === "tab") {
760
- if (matching[menuIndex]) {
761
- input = matching[menuIndex].name;
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
- ${chalk.cyan("\u{1F44B} Goodbye!")}
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
- contentRow = 8;
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(`${chalk.green(`\u2714 ${res.summary}`)}
803
- ${chalk.dim(` Location: ${res.filePath}`)}`);
2214
+ printToContent(`${chalk4.green(`\u2714 ${res.summary}`)}
2215
+ ${chalk4.dim(` Location: ${res.filePath}`)}`);
804
2216
  } catch (err) {
805
- printToContent(chalk.red(`\u26A0\uFE0F Initialization failed: ${err.message}`));
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(chalk.green("\u2714 Started a new coding session."));
2225
+ printToContent(chalk4.green("\u2714 Started a new coding session."));
814
2226
  } catch (err) {
815
- printToContent(chalk.red(`Failed to create new session: ${err.message}`));
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(chalk.green("\u2714 Reverted the last AI action and restored previous file states."));
2237
+ printToContent(chalk4.green("\u2714 Reverted the last AI action and restored previous file states."));
826
2238
  } else {
827
- printToContent(chalk.yellow("Could not revert the last action."));
2239
+ printToContent(chalk4.yellow("Could not revert the last action."));
828
2240
  }
829
2241
  } catch (err) {
830
- printToContent(chalk.red(`Revert failed: ${err.message}`));
2242
+ printToContent(chalk4.red(`Revert failed: ${err.message}`));
831
2243
  }
832
2244
  } else {
833
- printToContent(chalk.yellow("No previous action found to undo."));
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(chalk.yellow("No previous action to redo."));
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
- chalk.yellow(
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(chalk.cyan(`Validating API key with ${provArg.toUpperCase()}...`));
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
- chalk.green(
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(chalk.red(`\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}`));
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
- chalk.yellow(
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
- chalk.green(`\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.`)
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
- chalk.dim(
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 ? chalk.green("\u25CF Connected") : chalk.cyan("\u25CF Configured");
2347
+ const badge = isLive ? chalk4.green("\u25CF Connected") : chalk4.cyan("\u25CF Configured");
936
2348
  return ` \u2022 ${n} (${type}): ${badge}
937
- ${chalk.dim(target)}`;
2349
+ ${chalk4.dim(target)}`;
938
2350
  }).join("\n");
939
2351
  printToContent(
940
- chalk.bold("Model Context Protocol (MCP) Servers:\n") + lines + chalk.dim("\n\nAdd new servers with: /mcp add <name> <command>")
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(chalk.cyan(`\u2713 Switched mode to: ${currentAgent.toUpperCase()}`));
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(chalk.dim("No file modifications recorded in the current session."));
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(chalk.bold(`Modified Files (${diffs.length}):
2382
+ printToContent(chalk4.bold(`Modified Files (${diffs.length}):
971
2383
  `) + diffSummary);
972
2384
  }
973
2385
  } catch (err) {
974
- printToContent(chalk.red(`Failed to retrieve diff: ${err.message}`));
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(chalk.green("\u2713 Session context memory compacted and summarized successfully."));
2394
+ printToContent(chalk4.green("\u2713 Session context memory compacted and summarized successfully."));
983
2395
  } catch (err) {
984
- printToContent(chalk.red(`Compact failed: ${err.message}`));
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(chalk.green(`\u2713 Switched model to: ${friendly} (${resolvedId})`));
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
- chalk.bold("Available Commands:"),
1010
- ...COMMANDS.map((c) => ` ${chalk.hex(OCEAN_BLUE).bold(c.name.padEnd(12))} ${chalk.dim(c.desc)}`)
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(`\x1B[${contentRow};1H`);
2796
+ process.stdout.write("\x1B8");
1029
2797
  renderUserMessageCard(submitted, friendlyName);
1030
- const promptLines = submitted.split("\n").length;
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
- client.promptSession(session.id, submitted, {
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(chalk.red(`\u26A0\uFE0F Prompt Error: ${err.message}`));
2824
+ printToContent(chalk4.red(`\u26A0\uFE0F Prompt Error: ${err.message}`));
1047
2825
  paintBar();
1048
2826
  });
1049
2827
  return;
1050
2828
  }
1051
- if (key.name === "backspace") {
1052
- input = input.slice(0, -1);
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 (str && str.length === 1 && !key.ctrl && !key.meta) {
1058
- input += str;
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
- contentRow = 8;
2892
+ process.stdout.write("\x1B7");
1072
2893
  paintBar();
1073
2894
  }
1074
2895
  }
1075
2896
 
1076
2897
  // src/commands/models.ts
1077
- import chalk2 from "chalk";
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 creds = getStoredCredentials();
1084
- const available = getAvailableOceanModels(creds);
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}${chalk2.hex("#00d2ff").bold("Ocean AI Models")}`);
1087
- console.log(`${indent}${chalk2.dim("Switch anytime in chat with /models")}
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}${chalk2.dim("-".repeat(54))}`);
1090
- const freeModels = available.filter((m) => m.isFree);
1091
- const byokModels = available.filter((m) => !m.isFree);
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}${chalk2.dim("Tip: Type /models in interactive chat to switch models with arrow keys.")}
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 chalk3 from "chalk";
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}${chalk3.hex("#00d2ff").bold("Recent Sessions")}`);
1125
- console.log(`${indent}${chalk3.dim("-".repeat(54))}`);
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}${chalk3.dim("No previous sessions found.")}
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}${chalk3.cyan(s.id.slice(0, 16))} ${chalk3.bold.white(s.title || "Untitled")}`);
1137
- console.log(`${indent} ${chalk3.dim(date)} \xB7 ${chalk3.dim(`${tokens} tokens`)} \xB7 ${chalk3.dim(s.directory)}`);
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}${chalk3.red(`Failed to list sessions: ${err.message}`)}`);
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 chalk4 from "chalk";
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}${chalk4.hex("#00d2ff").bold("Usage & Analytics")}`);
1154
- console.log(`${indent}${chalk4.dim("-".repeat(54))}`);
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}${chalk4.dim("Total Sessions:")} ${chalk4.bold.white(sessions.length)}`);
1168
- console.log(`${indent}${chalk4.dim("Total Tokens:")} ${chalk4.bold.cyan(totalTokens.toLocaleString())}`);
1169
- console.log(`${indent}${chalk4.dim("Input Tokens:")} ${chalk4.white(totalInput.toLocaleString())}`);
1170
- console.log(`${indent}${chalk4.dim("Output Tokens:")} ${chalk4.white(totalOutput.toLocaleString())}`);
1171
- console.log(`${indent}${chalk4.dim("Total Cost:")} ${chalk4.bold.green("$" + totalCost.toFixed(4))} ${chalk4.bgHex("#00b4d8").black.bold(" FREE ")}`);
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}${chalk4.red(`Failed to load stats: ${err.message}`)}`);
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 chalk5 from "chalk";
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("Ocean CLI: Next-generation AI coding assistant built for large codebases").version("0.1.0").addHelpText("before", `
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: opencode/muse-spark-1.3-contributor-free [1M Context])").action(async (options) => {
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(chalk5.red(`
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: opencode/muse-spark-1.3-contributor-free)").option("-a, --agent <agent>", "Agent mode (default: build)").option("--auto", "Auto-approve tool permissions").action(async (messages, options, cmd) => {
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(chalk5.green(`\u2714 ${res.summary}`));
1209
- console.log(chalk5.dim(` Path: ${res.filePath}`));
3027
+ console.log(chalk8.green(`\u2714 ${res.summary}`));
3028
+ console.log(chalk8.dim(` Path: ${res.filePath}`));
1210
3029
  } catch (err) {
1211
- console.error(chalk5.red(`Error initializing AGENTS.md: ${err.message}`));
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(chalk5.cyan(`Validating API key with ${provider.toUpperCase()}...`));
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(chalk5.green(`\u2714 ${val.message}`));
3040
+ console.log(chalk8.green(`\u2714 ${val.message}`));
1222
3041
  } else {
1223
- console.error(chalk5.red(`\u2716 Validation failed: ${val.message}`));
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(chalk5.bold("\nBring Your Own Key (BYOK) Status:"));
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 ? chalk5.green("\u2714 Connected") : chalk5.dim("Not linked");
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(chalk5.dim("\nTo link a key, run:"));
1236
- console.log(chalk5.cyan(" oceancode connect <provider> <api-key>"));
1237
- console.log(chalk5.dim("Examples:"));
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(chalk5.red("Usage: oceancode mcp add <name> <command-or-url>"));
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(chalk5.green(`\u2714 Added MCP server "${name}" (${config.type}) to opencode.json`));
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(chalk5.bold("\nModel Context Protocol (MCP) Servers:"));
3082
+ console.log(chalk8.bold("\nModel Context Protocol (MCP) Servers:"));
1264
3083
  if (allNames.length === 0) {
1265
- console.log(chalk5.dim(" No MCP servers configured."));
1266
- console.log(chalk5.dim("\nTo add an MCP server, run:"));
1267
- console.log(chalk5.cyan(" oceancode mcp add <name> <command-or-url>"));
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 ? chalk5.green("\u25CF Connected") : chalk5.cyan("\u25CF Configured");
1275
- console.log(` \u2022 ${chalk5.bold(n)} (${type}) ${status}
1276
- ${chalk5.dim(target)}`);
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(chalk5.green(`\u2714 Ocean backend is running at: ${url}`));
3115
+ console.log(chalk8.green(`\u2714 Ocean backend is running at: ${url}`));
1297
3116
  });
1298
3117
  program.parse(process.argv);