oceancode 0.1.0 → 0.1.2

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