oceancode 0.1.2 → 0.1.3

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 DELETED
@@ -1,3117 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/index.ts
4
- import { Command } from "commander";
5
-
6
- // src/commands/interactive.ts
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
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
- }
1232
-
1233
- // src/utils/init.ts
1234
- import fs2 from "fs";
1235
- import path2 from "path";
1236
- function initializeAgentsDoc(workspaceDir = process.cwd()) {
1237
- const agentsPath = path2.join(workspaceDir, "AGENTS.md");
1238
- const alreadyExists = fs2.existsSync(agentsPath);
1239
- let projectType = "General Codebase";
1240
- let techDetails = [];
1241
- let buildCommands = [];
1242
- let testCommands = [];
1243
- let conventions = [];
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)) {
1251
- try {
1252
- const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
1253
- projectType = `Node.js / JavaScript${fs2.existsSync(tsConfigPath) ? " / TypeScript" : ""}`;
1254
- if (pkg.name) techDetails.push(`- **Project Name**: ${pkg.name}`);
1255
- if (pkg.version) techDetails.push(`- **Version**: ${pkg.version}`);
1256
- if (pkg.description) techDetails.push(`- **Description**: ${pkg.description}`);
1257
- if (pkg.scripts) {
1258
- if (pkg.scripts.build) buildCommands.push(`- Build: \`npm run build\` (${pkg.scripts.build})`);
1259
- if (pkg.scripts.test) testCommands.push(`- Test: \`npm test\` (${pkg.scripts.test})`);
1260
- if (pkg.scripts.dev) buildCommands.push(`- Dev: \`npm run dev\` (${pkg.scripts.dev})`);
1261
- if (pkg.scripts.lint) buildCommands.push(`- Lint: \`npm run lint\` (${pkg.scripts.lint})`);
1262
- }
1263
- const deps = Object.keys(pkg.dependencies || {});
1264
- const devDeps = Object.keys(pkg.devDependencies || {});
1265
- if (deps.length > 0) {
1266
- techDetails.push(`- **Core Dependencies**: ${deps.slice(0, 15).join(", ")}${deps.length > 15 ? ` (+${deps.length - 15} more)` : ""}`);
1267
- }
1268
- if (devDeps.length > 0) {
1269
- techDetails.push(`- **Dev Dependencies**: ${devDeps.slice(0, 10).join(", ")}`);
1270
- }
1271
- } catch {
1272
- }
1273
- } else if (fs2.existsSync(pyProjectPath) || fs2.existsSync(reqTxtPath)) {
1274
- projectType = "Python Project";
1275
- testCommands.push("- Test: `pytest`");
1276
- buildCommands.push("- Run: `python main.py`");
1277
- } else if (fs2.existsSync(cargoPath)) {
1278
- projectType = "Rust Project";
1279
- buildCommands.push("- Build: `cargo build`");
1280
- testCommands.push("- Test: `cargo test`");
1281
- } else if (fs2.existsSync(goModPath)) {
1282
- projectType = "Go Project";
1283
- buildCommands.push("- Build: `go build ./...`");
1284
- testCommands.push("- Test: `go test ./...`");
1285
- }
1286
- const dirs = [];
1287
- try {
1288
- const entries = fs2.readdirSync(workspaceDir, { withFileTypes: true });
1289
- for (const e of entries) {
1290
- if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist") {
1291
- dirs.push(e.name);
1292
- }
1293
- }
1294
- } catch {
1295
- }
1296
- const agentsContent = `# AGENTS.md - Repository Guidelines & AI Context
1297
-
1298
- This document guides AI agents (like OceanCode) working in this repository to ensure accurate, non-hallucinatory, and maintainable software engineering.
1299
-
1300
- ## Project Overview
1301
- - **Project Type**: ${projectType}
1302
- - **Root Directory**: \`${workspaceDir}\`
1303
- ${techDetails.join("\n")}
1304
-
1305
- ## Key Directories
1306
- ${dirs.length > 0 ? dirs.map((d) => `- \`${d}/\``).join("\n") : "- Single root project directory"}
1307
-
1308
- ## Build & Test Workflow
1309
- ${buildCommands.length > 0 ? buildCommands.join("\n") : "- Standard build scripts"}
1310
- ${testCommands.length > 0 ? testCommands.join("\n") : "- Standard unit tests"}
1311
-
1312
- ## Coding Rules for AI Agents
1313
- 1. **Never Hallucinate or Truncate**:
1314
- - Always produce full, working code when writing or updating files.
1315
- - Never insert placeholder comments like \`// ... existing code ...\` or \`// TODO\`.
1316
- 2. **Search & Read First**:
1317
- - Use \`glob\` and \`grep\` to verify types, exports, and imports across the codebase before modifying or adding code.
1318
- - For files over 200 lines, use chunked reading (\`offset\` and \`limit\`) to inspect specific functions or classes.
1319
- 3. **Surgical Modifications**:
1320
- - When updating existing files, always use \`edit\` with exact matching lines rather than overwriting the entire file with \`write\`.
1321
- 4. **Verification**:
1322
- - After completing edits, run available verification scripts or tests via \`bash\` to ensure syntax, compilation, and tests succeed.
1323
- 5. **Readable, Structured Formatting (No Walls of Text, No Tables)**:
1324
- - Never output dense, unbroken walls of text.
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.
1326
- `;
1327
- fs2.writeFileSync(agentsPath, agentsContent, "utf-8");
1328
- return {
1329
- filePath: agentsPath,
1330
- created: !alreadyExists,
1331
- projectType,
1332
- summary: `Configured context for ${projectType}. Generated repository guidelines in AGENTS.md.`
1333
- };
1334
- }
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
-
1487
- // src/server/mcp.ts
1488
- import path4 from "path";
1489
- import os3 from "os";
1490
- import fs4 from "fs";
1491
- function getOpencodeConfigPath() {
1492
- return path4.join(os3.homedir(), ".config", "oceancode", "opencode", "opencode.json");
1493
- }
1494
- function getConfiguredMcpServers() {
1495
- const configPath = getOpencodeConfigPath();
1496
- if (!fs4.existsSync(configPath)) return {};
1497
- try {
1498
- const raw = fs4.readFileSync(configPath, "utf-8");
1499
- const parsed = JSON.parse(raw);
1500
- return parsed.mcp || {};
1501
- } catch {
1502
- return {};
1503
- }
1504
- }
1505
- function saveMcpServer(name, config) {
1506
- const configPath = getOpencodeConfigPath();
1507
- const dir = path4.dirname(configPath);
1508
- fs4.mkdirSync(dir, { recursive: true });
1509
- let fullConfig = {};
1510
- if (fs4.existsSync(configPath)) {
1511
- try {
1512
- fullConfig = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
1513
- } catch {
1514
- }
1515
- }
1516
- if (!fullConfig.mcp) fullConfig.mcp = {};
1517
- fullConfig.mcp[name] = config;
1518
- fs4.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), "utf-8");
1519
- try {
1520
- const projectConfig = path4.join(process.cwd(), "opencode.json");
1521
- let proj = {};
1522
- if (fs4.existsSync(projectConfig)) {
1523
- proj = JSON.parse(fs4.readFileSync(projectConfig, "utf-8"));
1524
- }
1525
- if (!proj.mcp) proj.mcp = {};
1526
- proj.mcp[name] = config;
1527
- fs4.writeFileSync(projectConfig, JSON.stringify(proj, null, 2), "utf-8");
1528
- } catch {
1529
- }
1530
- }
1531
- async function fetchLiveMcpStatus(baseUrl) {
1532
- try {
1533
- const res = await fetch(`${baseUrl}/mcp`);
1534
- if (!res.ok) return {};
1535
- return await res.json();
1536
- } catch {
1537
- return {};
1538
- }
1539
- }
1540
-
1541
- // src/commands/interactive.ts
1542
- import fs5 from "fs";
1543
- import path5 from "path";
1544
- import { execSync } from "child_process";
1545
- var COMMANDS = [
1546
- { name: "/init", desc: "Initializes context / updates AGENTS.md" },
1547
- { name: "/goal", desc: "Sets/manages autonomous completion loops" },
1548
- { name: "/new", desc: "Clears conversation buffer for a fresh session" },
1549
- { name: "/undo", desc: "Rolls back the last prompt and file changes" },
1550
- { name: "/redo", desc: "Re-applies or regenerates the last action" },
1551
- { name: "/models", desc: "Opens model picker UI to switch active LLMs" },
1552
- { name: "/connect", desc: "Launches popup setup to link & verify provider API keys" },
1553
- { name: "/mcp", desc: "Lists & configures Model Context Protocol servers" },
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" },
1566
- { name: "/agent", desc: "Switch mode (build / plan)" },
1567
- { name: "/compact", desc: "Compact & summarize session memory" },
1568
- { name: "/clear", desc: "Clear conversation canvas" },
1569
- { name: "/info", desc: "Session details & status" },
1570
- { name: "/help", desc: "Help & commands" },
1571
- { name: "/exit", desc: "Exit the app" }
1572
- ];
1573
- var THINKING_FRAMES = [
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 ")
1582
- ];
1583
- async function interactiveChatCommand(options) {
1584
- let currentBackendModel = options?.model ? getBackendModelId(options.model) : DEFAULT_MODEL_ID;
1585
- if (!process.stdin.isTTY) {
1586
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1587
- const lines = [];
1588
- for await (const line of rl) {
1589
- lines.push(line);
1590
- }
1591
- if (lines.length > 0) {
1592
- await runCommand(lines, { model: currentBackendModel });
1593
- }
1594
- return;
1595
- }
1596
- const baseUrl = await ensureServer();
1597
- const client = new OceanClient(baseUrl);
1598
- const conversationHistory = [];
1599
- let session = await client.createSession("Ocean Session", process.cwd());
1600
- let input = "";
1601
- let cursorIndex = 0;
1602
- const promptHistory = [];
1603
- let historyIndex = -1;
1604
- let tempSavedInput = "";
1605
- let mode = "normal";
1606
- let menuIndex = 0;
1607
- let modelIndex = 0;
1608
- let modelSearch = "";
1609
- let connectStep = "provider_list";
1610
- let connectProviderIndex = 0;
1611
- let connectKeyInput = "";
1612
- let connectErrorMsg = "";
1613
- let connectValidatingProv = "";
1614
- let currentAgent = "build";
1615
- let isProcessing = false;
1616
- let activityTimeoutTimer = null;
1617
- let userMsgId = null;
1618
- let lastUserMsgId = null;
1619
- let lastSubmittedPrompt = "";
1620
- const pinnedContext = /* @__PURE__ */ new Map();
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);
1662
- const cleanup = () => {
1663
- process.off("SIGINT", onSignal);
1664
- process.off("SIGTERM", onSignal);
1665
- if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
1666
- if (process.stdout.isTTY) {
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`);
1678
- try {
1679
- process.stdin.setRawMode(false);
1680
- } catch {
1681
- }
1682
- }
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
- }
1694
- const resetActivityTimeout = () => {
1695
- if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
1696
- activityTimeoutTimer = setTimeout(() => {
1697
- if (isProcessing) {
1698
- isProcessing = false;
1699
- renderer.finish(currentAgent === "build" ? "Build" : "Plan");
1700
- const friendlyName = getFriendlyModelName(currentBackendModel);
1701
- printToContent(
1702
- chalk4.yellow(
1703
- `\u26A0\uFE0F Operation timed out waiting for ${friendlyName}. You can retry or switch models with /models.`
1704
- )
1705
- );
1706
- render();
1707
- }
1708
- }, 18e4);
1709
- activityTimeoutTimer.unref();
1710
- };
1711
- const buildBarLines = () => {
1712
- const cols = process.stdout.columns || 80;
1713
- const friendlyModel = getFriendlyModelName(currentBackendModel);
1714
- const bar = chalk4.hex(OCEAN_BLUE)("\u2502");
1715
- const width = Math.min(cols - 4, 80);
1716
- const peachBg = chalk4.bgHex("#fba978").black;
1717
- const lines = [];
1718
- if (mode === "model_selector") {
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));
1721
- lines.push(
1722
- modelSearch ? `Search: ${modelSearch}` : chalk4.dim("Search models (e.g. qwen, step, flash)")
1723
- );
1724
- lines.push("");
1725
- const availableModels = getAvailableOceanModels();
1726
- const searchLower = modelSearch.toLowerCase();
1727
- const filtered = availableModels.filter(
1728
- (m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower)
1729
- );
1730
- if (modelIndex >= filtered.length) modelIndex = Math.max(0, filtered.length - 1);
1731
- if (filtered.length === 0) {
1732
- lines.push(chalk4.dim(" No matching models found."));
1733
- } else {
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;
1745
- const isCurrent = m.id === currentBackendModel;
1746
- const prefix = isCurrent ? "\u25CF " : " ";
1747
- const left = prefix + m.name;
1748
- const spaces = Math.max(0, width - left.length);
1749
- const row = left + " ".repeat(spaces);
1750
- if (idx === modelIndex) {
1751
- lines.push(peachBg(row));
1752
- } else {
1753
- lines.push(chalk4.white(row));
1754
- }
1755
- });
1756
- }
1757
- } else if (mode === "connect_modal") {
1758
- const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) + chalk4.dim("esc");
1759
- lines.push(chalk4.bold.white(header));
1760
- if (connectStep === "provider_list") {
1761
- lines.push(chalk4.dim("Select provider with \u2191/\u2193 and press Enter (or 'd' to disconnect):"));
1762
- lines.push("");
1763
- const creds = getStoredCredentials();
1764
- SUPPORTED_BYOK_PROVIDERS.forEach((p, idx) => {
1765
- const isConnected = Boolean(creds[p.id]?.key);
1766
- const prefix = isConnected ? "\u25CF " : "\u25CB ";
1767
- const left = ` ${prefix}${p.name.padEnd(16)} (${p.id})`;
1768
- const tag = isConnected ? chalk4.green("\u2714 Connected") : chalk4.dim("Not linked");
1769
- const spaces = Math.max(2, width - left.length - 12);
1770
- const row = left + " ".repeat(spaces) + tag;
1771
- lines.push(idx === connectProviderIndex ? peachBg(row) : chalk4.white(row));
1772
- });
1773
- lines.push("");
1774
- lines.push(chalk4.dim("Enter: Link API Key \u2022 d: Disconnect \u2022 Esc: Close"));
1775
- } else if (connectStep === "enter_key") {
1776
- const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
1777
- lines.push(chalk4.bold.hex("#38bdf8")(`Provider: ${prov.name}`));
1778
- lines.push(chalk4.dim(`Placeholder format: ${prov.placeholder}`));
1779
- lines.push("");
1780
- const masked = connectKeyInput.length > 8 ? connectKeyInput.slice(0, 4) + "\u2022".repeat(connectKeyInput.length - 8) + connectKeyInput.slice(-4) : "\u2022".repeat(connectKeyInput.length);
1781
- const displayKey = connectKeyInput ? masked : chalk4.dim("Paste or type API key here");
1782
- lines.push(`${bar} Key: ${displayKey}\u2588`);
1783
- if (connectErrorMsg) {
1784
- lines.push("");
1785
- lines.push(chalk4.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
1786
- }
1787
- lines.push("");
1788
- lines.push(chalk4.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
1789
- } else if (connectStep === "validating") {
1790
- lines.push(chalk4.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
1791
- lines.push("");
1792
- lines.push(chalk4.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
1793
- lines.push("");
1794
- lines.push(chalk4.dim("Please wait while we verify your key with the provider."));
1795
- }
1796
- } else {
1797
- if (!isProcessing && input.startsWith("/")) {
1798
- const matching = COMMANDS.filter((c) => c.name.startsWith(input));
1799
- if (menuIndex >= matching.length) menuIndex = Math.max(0, matching.length - 1);
1800
- if (matching.length > 0) {
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;
1814
- const padded = row + " ".repeat(Math.max(2, width - row.length));
1815
- lines.push(isSelected ? peachBg(padded) : chalk4.white(row));
1816
- });
1817
- }
1818
- }
1819
- const friendlyAgent = currentAgent === "build" ? "Build" : "Plan";
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);
1830
- lines.push(
1831
- `${bar2} ${chalk4.hex(OCEAN_BLUE).bold(friendlyAgent)} ${chalk4.dim("\xB7")} ${chalk4.white(friendlyModel)} ${chalk4.dim("OceanCode")}`
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}`);
1846
- }
1847
- return lines;
1848
- };
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 {
1886
- }
1887
- };
1888
- const render = () => paintBar();
1889
- renderer.setPinBar(
1890
- () => paintBar(),
1891
- () => {
1892
- }
1893
- );
1894
- const reasoningPartIds = /* @__PURE__ */ new Set();
1895
- const textPartIds = /* @__PURE__ */ new Set();
1896
- const unsubscribe = client.subscribeEvents((event) => {
1897
- if (event.type && typeof event.type === "string" && event.type.startsWith("permission.")) {
1898
- const reqId = event.properties?.requestID || event.properties?.id;
1899
- if (reqId) client.replyPermission(reqId, "always");
1900
- }
1901
- if (event.properties?.sessionID !== session.id) return;
1902
- if (event.type === "message.updated") {
1903
- if (event.properties?.info?.role === "user") {
1904
- userMsgId = event.properties.info.id;
1905
- lastUserMsgId = userMsgId;
1906
- }
1907
- }
1908
- if (event.type === "message.part.updated") {
1909
- const part = event.properties?.part;
1910
- if (!part) return;
1911
- if (userMsgId && part.messageID === userMsgId) return;
1912
- isProcessing = true;
1913
- resetActivityTimeout();
1914
- if (part.type === "reasoning") {
1915
- reasoningPartIds.add(part.id);
1916
- if (typeof part.text === "string") {
1917
- renderer.handleReasoning(part.text);
1918
- }
1919
- } else if (part.type === "tool") {
1920
- const toolName = part.tool || "tool";
1921
- const detail = extractToolDetail(part);
1922
- const status = part.state?.status || part.status || "";
1923
- renderer.handleTool(toolName, detail, part.id, status);
1924
- } else if (part.type === "text" && typeof part.text === "string") {
1925
- textPartIds.add(part.id);
1926
- renderer.handleText(part.text, part.id);
1927
- }
1928
- }
1929
- if (event.type === "message.part.delta") {
1930
- const partId = event.properties?.partID || event.properties?.partId || event.properties?.id || "default";
1931
- if (reasoningPartIds.has(partId)) return;
1932
- const delta = event.properties?.delta;
1933
- const field = event.properties?.field;
1934
- if (field === "text" && typeof delta === "string") {
1935
- resetActivityTimeout();
1936
- renderer.handleDelta(delta, partId);
1937
- }
1938
- }
1939
- if (event.type === "session.idle" || event.type === "session.status" && event.properties?.status?.type === "idle") {
1940
- if (isProcessing) {
1941
- if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
1942
- isProcessing = false;
1943
- renderer.finish(currentAgent === "build" ? "Build" : "Plan");
1944
- render();
1945
- }
1946
- }
1947
- if (event.type === "session.error") {
1948
- if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
1949
- isProcessing = false;
1950
- renderer.finish(currentAgent === "build" ? "Build" : "Plan");
1951
- const errObj = event.properties?.error;
1952
- const errMsg = errObj?.message || errObj?.data?.message || (typeof errObj === "string" ? errObj : "Model request failed or timed out.");
1953
- printToContent(chalk4.red(`\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}`));
1954
- render();
1955
- }
1956
- }, void 0, session.directory);
1957
- readline.emitKeypressEvents(process.stdin);
1958
- if (process.stdin.isTTY) {
1959
- process.stdin.setRawMode(true);
1960
- process.stdin.resume();
1961
- }
1962
- const onKeypress = async (str, key) => {
1963
- if (key && key.ctrl && key.name === "c") {
1964
- cleanup();
1965
- unsubscribe();
1966
- console.log(`
1967
- ${chalk4.cyan("\u{1F44B} Goodbye!")}
1968
- `);
1969
- process.exit(0);
1970
- }
1971
- if (isProcessing) return;
1972
- if (mode === "connect_modal") {
1973
- if (connectStep === "validating") return;
1974
- if (key.name === "escape") {
1975
- if (connectStep === "enter_key") {
1976
- connectStep = "provider_list";
1977
- connectKeyInput = "";
1978
- connectErrorMsg = "";
1979
- } else {
1980
- mode = "normal";
1981
- connectStep = "provider_list";
1982
- connectKeyInput = "";
1983
- connectErrorMsg = "";
1984
- }
1985
- render();
1986
- return;
1987
- }
1988
- if (connectStep === "provider_list") {
1989
- if (key.name === "up") {
1990
- connectProviderIndex = (connectProviderIndex - 1 + SUPPORTED_BYOK_PROVIDERS.length) % SUPPORTED_BYOK_PROVIDERS.length;
1991
- render();
1992
- return;
1993
- }
1994
- if (key.name === "down") {
1995
- connectProviderIndex = (connectProviderIndex + 1) % SUPPORTED_BYOK_PROVIDERS.length;
1996
- render();
1997
- return;
1998
- }
1999
- if (key.name === "return") {
2000
- connectStep = "enter_key";
2001
- connectKeyInput = "";
2002
- connectErrorMsg = "";
2003
- render();
2004
- return;
2005
- }
2006
- if (str === "d" || str === "D") {
2007
- const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
2008
- removeProviderKey(prov.id);
2009
- printToContent(chalk4.yellow(`
2010
- \u2713 Disconnected API key for ${prov.name}.
2011
- `));
2012
- render();
2013
- return;
2014
- }
2015
- return;
2016
- }
2017
- if (connectStep === "enter_key") {
2018
- if (key.name === "backspace") {
2019
- connectKeyInput = connectKeyInput.slice(0, -1);
2020
- connectErrorMsg = "";
2021
- render();
2022
- return;
2023
- }
2024
- if (key.name === "return") {
2025
- if (!connectKeyInput.trim()) {
2026
- connectErrorMsg = "Please enter an API key.";
2027
- render();
2028
- return;
2029
- }
2030
- const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
2031
- connectStep = "validating";
2032
- connectValidatingProv = prov.name;
2033
- connectErrorMsg = "";
2034
- render();
2035
- const result = await validateProviderKey(prov.id, connectKeyInput);
2036
- if (result.valid) {
2037
- saveProviderKey(prov.id, connectKeyInput);
2038
- mode = "normal";
2039
- connectStep = "provider_list";
2040
- connectKeyInput = "";
2041
- connectErrorMsg = "";
2042
- printToContent(
2043
- chalk4.green(
2044
- `
2045
- \u2713 Successfully verified and connected ${prov.name}! Its models are now unlocked in /models.
2046
- `
2047
- )
2048
- );
2049
- render();
2050
- } else {
2051
- connectStep = "enter_key";
2052
- connectErrorMsg = result.message;
2053
- render();
2054
- }
2055
- return;
2056
- }
2057
- if (str && !key.ctrl && !key.meta) {
2058
- connectKeyInput += str;
2059
- connectErrorMsg = "";
2060
- render();
2061
- return;
2062
- }
2063
- }
2064
- return;
2065
- }
2066
- if (mode === "model_selector") {
2067
- const availableModels = getAvailableOceanModels();
2068
- const searchLower = modelSearch.toLowerCase();
2069
- const filtered = availableModels.filter(
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)
2071
- );
2072
- if (key.name === "escape") {
2073
- mode = "normal";
2074
- modelSearch = "";
2075
- render();
2076
- return;
2077
- }
2078
- if (key.name === "up") {
2079
- if (filtered.length > 0) {
2080
- modelIndex = (modelIndex - 1 + filtered.length) % filtered.length;
2081
- }
2082
- render();
2083
- return;
2084
- }
2085
- if (key.name === "down") {
2086
- if (filtered.length > 0) {
2087
- modelIndex = (modelIndex + 1) % filtered.length;
2088
- }
2089
- render();
2090
- return;
2091
- }
2092
- if (key.name === "return") {
2093
- let selectedFriendly = "";
2094
- let selectedId = "";
2095
- if (filtered[modelIndex]) {
2096
- currentBackendModel = filtered[modelIndex].id;
2097
- selectedId = currentBackendModel;
2098
- selectedFriendly = getFriendlyModelName(currentBackendModel);
2099
- }
2100
- mode = "normal";
2101
- input = "";
2102
- modelSearch = "";
2103
- if (selectedFriendly) {
2104
- printToContent(chalk4.green(`\u2713 Active model changed to: ${selectedFriendly} (${selectedId})`));
2105
- }
2106
- render();
2107
- return;
2108
- }
2109
- if (key.name === "backspace") {
2110
- modelSearch = modelSearch.slice(0, -1);
2111
- modelIndex = 0;
2112
- render();
2113
- return;
2114
- }
2115
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
2116
- modelSearch += str;
2117
- modelIndex = 0;
2118
- render();
2119
- return;
2120
- }
2121
- return;
2122
- }
2123
- const matching = input.startsWith("/") ? COMMANDS.filter((c) => c.name.startsWith(input)) : [];
2124
- if (key.name === "escape") {
2125
- input = "";
2126
- menuIndex = 0;
2127
- render();
2128
- return;
2129
- }
2130
- if (key.name === "up") {
2131
- if (matching.length > 0) {
2132
- menuIndex = (menuIndex - 1 + matching.length) % matching.length;
2133
- render();
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;
2146
- }
2147
- }
2148
- if (key.name === "down") {
2149
- if (matching.length > 0) {
2150
- menuIndex = (menuIndex + 1) % matching.length;
2151
- render();
2152
- return;
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;
2160
- }
2161
- cursorIndex = input.length;
2162
- render();
2163
- return;
2164
- }
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
- }
2174
- if (key.name === "return") {
2175
- let submitted = input.trim();
2176
- if (matching.length > 0 && matching[menuIndex]) {
2177
- submitted = matching[menuIndex].name;
2178
- }
2179
- input = "";
2180
- cursorIndex = 0;
2181
- historyIndex = -1;
2182
- menuIndex = 0;
2183
- if (!submitted) {
2184
- paintBar();
2185
- return;
2186
- }
2187
- if (promptHistory[promptHistory.length - 1] !== submitted) {
2188
- promptHistory.push(submitted);
2189
- }
2190
- if (submitted === "/exit" || submitted === "/quit") {
2191
- cleanup();
2192
- unsubscribe();
2193
- console.log(`
2194
- ${chalk4.cyan("\u{1F44B} Goodbye!")}
2195
- `);
2196
- process.exit(0);
2197
- }
2198
- if (submitted === "/clear") {
2199
- if (process.stdout.isTTY) {
2200
- process.stdout.write("\x1B[2J\x1B[H");
2201
- const { scrollBottom } = getBarMetrics(3);
2202
- applyScrollRegion(scrollBottom);
2203
- process.stdout.write("\x1B[1;1H");
2204
- }
2205
- renderTopBar(getFriendlyModelName(currentBackendModel));
2206
- process.stdout.write("\x1B7");
2207
- lastBarStartRow = 0;
2208
- paintBar();
2209
- return;
2210
- }
2211
- if (submitted === "/init") {
2212
- try {
2213
- const res = initializeAgentsDoc(process.cwd());
2214
- printToContent(`${chalk4.green(`\u2714 ${res.summary}`)}
2215
- ${chalk4.dim(` Location: ${res.filePath}`)}`);
2216
- } catch (err) {
2217
- printToContent(chalk4.red(`\u26A0\uFE0F Initialization failed: ${err.message}`));
2218
- }
2219
- render();
2220
- return;
2221
- }
2222
- if (submitted === "/new") {
2223
- try {
2224
- session = await client.createSession("Ocean Session", process.cwd());
2225
- printToContent(chalk4.green("\u2714 Started a new coding session."));
2226
- } catch (err) {
2227
- printToContent(chalk4.red(`Failed to create new session: ${err.message}`));
2228
- }
2229
- render();
2230
- return;
2231
- }
2232
- if (submitted === "/undo") {
2233
- if (lastUserMsgId) {
2234
- try {
2235
- const success = await client.revertSession(session.id, lastUserMsgId);
2236
- if (success) {
2237
- printToContent(chalk4.green("\u2714 Reverted the last AI action and restored previous file states."));
2238
- } else {
2239
- printToContent(chalk4.yellow("Could not revert the last action."));
2240
- }
2241
- } catch (err) {
2242
- printToContent(chalk4.red(`Revert failed: ${err.message}`));
2243
- }
2244
- } else {
2245
- printToContent(chalk4.yellow("No previous action found to undo."));
2246
- }
2247
- render();
2248
- return;
2249
- }
2250
- if (submitted === "/redo") {
2251
- if (!lastSubmittedPrompt) {
2252
- printToContent(chalk4.yellow("No previous action to redo."));
2253
- render();
2254
- return;
2255
- }
2256
- submitted = lastSubmittedPrompt;
2257
- }
2258
- if (submitted.startsWith("/goal")) {
2259
- const goalDesc = submitted.replace(/^\/goal\s*/, "").trim();
2260
- if (!goalDesc) {
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"
2264
- )
2265
- );
2266
- render();
2267
- return;
2268
- }
2269
- submitted = `[AUTONOMOUS GOAL EXECUTION LOOP]
2270
- Goal: ${goalDesc}
2271
-
2272
- Please accomplish this goal systematically:
2273
- 1. Search and explore relevant files using glob and grep.
2274
- 2. Formulate an execution plan and track with todowrite.
2275
- 3. Implement code changes cleanly without truncations or placeholders.
2276
- 4. Verify code with compiler or tests.
2277
- 5. Present a clear final summary of what was accomplished.`;
2278
- }
2279
- if (submitted === "/connect" || submitted.startsWith("/connect")) {
2280
- const parts = submitted.split(" ").filter(Boolean);
2281
- const provArg = parts[1]?.toLowerCase();
2282
- const keyArg = parts[2];
2283
- if (provArg && keyArg) {
2284
- printToContent(chalk4.cyan(`Validating API key with ${provArg.toUpperCase()}...`));
2285
- const val = await validateProviderKey(provArg, keyArg);
2286
- if (val.valid) {
2287
- saveProviderKey(provArg, keyArg);
2288
- printToContent(
2289
- chalk4.green(
2290
- `\u2714 Successfully verified and connected ${provArg.toUpperCase()}! Its models are now unlocked in /models.`
2291
- )
2292
- );
2293
- } else {
2294
- printToContent(chalk4.red(`\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}`));
2295
- }
2296
- render();
2297
- return;
2298
- } else {
2299
- mode = "connect_modal";
2300
- connectStep = "provider_list";
2301
- connectProviderIndex = 0;
2302
- connectKeyInput = "";
2303
- connectErrorMsg = "";
2304
- render();
2305
- return;
2306
- }
2307
- }
2308
- if (submitted.startsWith("/mcp")) {
2309
- const parts = submitted.split(" ").filter(Boolean);
2310
- const sub = parts[1]?.toLowerCase();
2311
- if (sub === "add") {
2312
- const serverName = parts[2];
2313
- const serverCmdOrUrl = parts.slice(3).join(" ");
2314
- if (!serverName || !serverCmdOrUrl) {
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"
2318
- )
2319
- );
2320
- render();
2321
- return;
2322
- }
2323
- const isUrl = serverCmdOrUrl.startsWith("http://") || serverCmdOrUrl.startsWith("https://");
2324
- const mcpConfig = isUrl ? { type: "remote", url: serverCmdOrUrl, enabled: true } : { type: "local", command: serverCmdOrUrl.split(" "), enabled: true };
2325
- saveMcpServer(serverName, mcpConfig);
2326
- printToContent(
2327
- chalk4.green(`\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.`)
2328
- );
2329
- render();
2330
- return;
2331
- }
2332
- const configured = getConfiguredMcpServers();
2333
- const live = await fetchLiveMcpStatus(baseUrl);
2334
- const names = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
2335
- if (names.length === 0) {
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 ."
2339
- )
2340
- );
2341
- } else {
2342
- const lines = names.map((n) => {
2343
- const conf = configured[n] || {};
2344
- const isLive = live[n] !== void 0;
2345
- const type = conf.type || (conf.url ? "remote" : "local");
2346
- const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
2347
- const badge = isLive ? chalk4.green("\u25CF Connected") : chalk4.cyan("\u25CF Configured");
2348
- return ` \u2022 ${n} (${type}): ${badge}
2349
- ${chalk4.dim(target)}`;
2350
- }).join("\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
- );
2354
- }
2355
- render();
2356
- return;
2357
- }
2358
- if (submitted === "/agent" || submitted.startsWith("/agent ") || submitted === "/mode" || submitted.startsWith("/mode ")) {
2359
- if (submitted.includes(" ")) {
2360
- const target = submitted.split(" ")[1].toLowerCase().trim();
2361
- if (target === "plan" || target === "build") {
2362
- currentAgent = target;
2363
- } else {
2364
- currentAgent = currentAgent === "build" ? "plan" : "build";
2365
- }
2366
- } else {
2367
- currentAgent = currentAgent === "build" ? "plan" : "build";
2368
- }
2369
- printToContent(chalk4.cyan(`\u2713 Switched mode to: ${currentAgent.toUpperCase()}`));
2370
- render();
2371
- return;
2372
- }
2373
- if (submitted === "/diff") {
2374
- try {
2375
- const diffs = await client.getSessionDiff(session.id);
2376
- if (!diffs || diffs.length === 0) {
2377
- printToContent(chalk4.dim("No file modifications recorded in the current session."));
2378
- } else {
2379
- const diffSummary = diffs.map(
2380
- (d) => ` \u2022 ${d.path || d.file || "file"} (+${d.additions || 0} -${d.deletions || 0})`
2381
- ).join("\n");
2382
- printToContent(chalk4.bold(`Modified Files (${diffs.length}):
2383
- `) + diffSummary);
2384
- }
2385
- } catch (err) {
2386
- printToContent(chalk4.red(`Failed to retrieve diff: ${err.message}`));
2387
- }
2388
- render();
2389
- return;
2390
- }
2391
- if (submitted === "/compact") {
2392
- try {
2393
- await client.summarizeSession(session.id);
2394
- printToContent(chalk4.green("\u2713 Session context memory compacted and summarized successfully."));
2395
- } catch (err) {
2396
- printToContent(chalk4.red(`Compact failed: ${err.message}`));
2397
- }
2398
- render();
2399
- return;
2400
- }
2401
- if (submitted.startsWith("/model") || submitted.startsWith("/models")) {
2402
- const parts = submitted.split(" ");
2403
- const targetArg = parts.slice(1).join(" ").trim();
2404
- if (targetArg) {
2405
- const resolvedId = getBackendModelId(targetArg);
2406
- currentBackendModel = resolvedId;
2407
- const friendly = getFriendlyModelName(resolvedId);
2408
- printToContent(chalk4.green(`\u2713 Switched model to: ${friendly} (${resolvedId})`));
2409
- render();
2410
- return;
2411
- } else {
2412
- mode = "model_selector";
2413
- modelSearch = "";
2414
- modelIndex = 0;
2415
- render();
2416
- return;
2417
- }
2418
- }
2419
- if (submitted.startsWith("!") || submitted.startsWith("/exec")) {
2420
- const cmdToRun = submitted.startsWith("!") ? submitted.slice(1).trim() : submitted.replace(/^\/exec\s*/, "").trim();
2421
- if (!cmdToRun) {
2422
- printToContent(chalk4.yellow("Usage: !<command> or /exec <command>\nExample: !git status"));
2423
- render();
2424
- return;
2425
- }
2426
- printToContent(chalk4.cyan(`$ ${cmdToRun}`));
2427
- try {
2428
- const res = execSync(cmdToRun, {
2429
- cwd: session.directory,
2430
- encoding: "utf-8",
2431
- stdio: ["ignore", "pipe", "pipe"],
2432
- timeout: 6e4,
2433
- maxBuffer: 10 * 1024 * 1024
2434
- });
2435
- if (res.trim()) {
2436
- printToContent(res.trimEnd());
2437
- } else {
2438
- printToContent(chalk4.dim("(Command completed with no output)"));
2439
- }
2440
- } catch (err) {
2441
- const out = err.stdout ? String(err.stdout).trim() : "";
2442
- const errOut = err.stderr ? String(err.stderr).trim() : "";
2443
- if (out) printToContent(out);
2444
- if (errOut) printToContent(chalk4.red(errOut));
2445
- if (!out && !errOut) printToContent(chalk4.red(`Command failed: ${err.message}`));
2446
- }
2447
- render();
2448
- return;
2449
- }
2450
- if (submitted === "/checkpoint") {
2451
- try {
2452
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2453
- const stashMsg = `oceancode-checkpoint-${timestamp}`;
2454
- const res = execSync(`git stash push -m "${stashMsg}" --include-untracked`, {
2455
- cwd: session.directory,
2456
- encoding: "utf-8",
2457
- stdio: ["ignore", "pipe", "pipe"]
2458
- });
2459
- if (res.includes("No local changes to save")) {
2460
- printToContent(chalk4.yellow("No unstaged or modified changes to checkpoint."));
2461
- } else {
2462
- try {
2463
- execSync("git stash apply stash@{0}", {
2464
- cwd: session.directory,
2465
- stdio: ["ignore", "pipe", "pipe"]
2466
- });
2467
- } catch {
2468
- }
2469
- printToContent(
2470
- `${chalk4.green(`\u2714 Checkpoint created: ${stashMsg}`)}
2471
- ` + chalk4.dim(" Current changes safely recorded in git stash.\n Revert anytime with: git stash apply")
2472
- );
2473
- }
2474
- } catch (err) {
2475
- printToContent(chalk4.red(`Checkpoint failed: ${err.message}`));
2476
- }
2477
- render();
2478
- return;
2479
- }
2480
- if (submitted.startsWith("/diff-file")) {
2481
- const targetFile = submitted.replace(/^\/diff-file\s*/, "").trim();
2482
- if (!targetFile) {
2483
- printToContent(chalk4.yellow("Usage: /diff-file <filepath>\nExample: /diff-file package.json"));
2484
- render();
2485
- return;
2486
- }
2487
- try {
2488
- const res = execSync(`git diff HEAD -- "${targetFile}"`, {
2489
- cwd: session.directory,
2490
- encoding: "utf-8",
2491
- stdio: ["ignore", "pipe", "pipe"]
2492
- });
2493
- if (!res.trim()) {
2494
- printToContent(chalk4.dim(`No differences found for "${targetFile}" compared to HEAD.`));
2495
- } else {
2496
- const colored = res.split("\n").map((line) => {
2497
- if (line.startsWith("+") && !line.startsWith("+++")) return chalk4.green(line);
2498
- if (line.startsWith("-") && !line.startsWith("---")) return chalk4.red(line);
2499
- if (line.startsWith("@@")) return chalk4.cyan(line);
2500
- return chalk4.dim(line);
2501
- }).join("\n");
2502
- printToContent(`${chalk4.bold(`Diff: ${targetFile}`)}
2503
- ${colored}`);
2504
- }
2505
- } catch (err) {
2506
- printToContent(chalk4.red(`Failed to get diff for ${targetFile}: ${err.message}`));
2507
- }
2508
- render();
2509
- return;
2510
- }
2511
- if (submitted === "/commit" || submitted.startsWith("/commit ")) {
2512
- const manualMsg = submitted.replace(/^\/commit\s*/, "").trim();
2513
- if (manualMsg) {
2514
- try {
2515
- execSync(`git commit -m "${manualMsg.replace(/"/g, '\\"')}"`, {
2516
- cwd: session.directory,
2517
- encoding: "utf-8",
2518
- stdio: ["ignore", "pipe", "pipe"]
2519
- });
2520
- printToContent(chalk4.green(`\u2714 Committed successfully with message: "${manualMsg}"`));
2521
- } catch (err) {
2522
- printToContent(chalk4.red(`Git commit failed: ${err.message}`));
2523
- }
2524
- render();
2525
- return;
2526
- } else {
2527
- try {
2528
- const staged = execSync("git diff --staged", {
2529
- cwd: session.directory,
2530
- encoding: "utf-8",
2531
- stdio: ["ignore", "pipe", "pipe"]
2532
- });
2533
- const unstaged = execSync("git diff", {
2534
- cwd: session.directory,
2535
- encoding: "utf-8",
2536
- stdio: ["ignore", "pipe", "pipe"]
2537
- });
2538
- const status = execSync("git status --short", {
2539
- cwd: session.directory,
2540
- encoding: "utf-8",
2541
- stdio: ["ignore", "pipe", "pipe"]
2542
- });
2543
- if (!status.trim()) {
2544
- printToContent(chalk4.yellow("No changes detected to commit. Working tree is clean."));
2545
- render();
2546
- return;
2547
- }
2548
- const diffToUse = staged.trim() || unstaged.trim() || status;
2549
- submitted = `[GENERATE CONVENTIONAL COMMIT MESSAGE]
2550
- Based on the following git changes, generate a concise, conventional commit message (e.g. feat:, fix:, refactor:, chore:) with a 1-sentence explanation:
2551
-
2552
- \`\`\`diff
2553
- ${diffToUse.slice(0, 4e3)}
2554
- \`\`\`
2555
-
2556
- Please format the commit message clearly so it can be copied or committed.`;
2557
- } catch (err) {
2558
- printToContent(chalk4.red(`Git inspection failed: ${err.message}`));
2559
- render();
2560
- return;
2561
- }
2562
- }
2563
- }
2564
- if (submitted === "/review" || submitted.startsWith("/review ")) {
2565
- const target = submitted.replace(/^\/review\s*/, "").trim() || "HEAD";
2566
- try {
2567
- let diff = "";
2568
- try {
2569
- diff = execSync(`git diff ${target}`, {
2570
- cwd: session.directory,
2571
- encoding: "utf-8",
2572
- stdio: ["ignore", "pipe", "pipe"]
2573
- });
2574
- } catch {
2575
- diff = execSync("git diff", {
2576
- cwd: session.directory,
2577
- encoding: "utf-8",
2578
- stdio: ["ignore", "pipe", "pipe"]
2579
- });
2580
- }
2581
- if (!diff.trim()) {
2582
- printToContent(chalk4.yellow("No git changes found to review. Working directory matches target."));
2583
- render();
2584
- return;
2585
- }
2586
- submitted = `[CODE REVIEW REQUEST]
2587
- Please review the following diff carefully.
2588
- Identify: 1) Potential bugs or regressions, 2) Security issues, 3) Edge case handling, 4) Code style or architectural improvements:
2589
-
2590
- \`\`\`diff
2591
- ${diff.slice(0, 6e3)}
2592
- \`\`\``;
2593
- } catch (err) {
2594
- printToContent(chalk4.red(`Review failed: ${err.message}`));
2595
- render();
2596
- return;
2597
- }
2598
- }
2599
- if (submitted.startsWith("/open")) {
2600
- const fileToOpen = submitted.replace(/^\/open\s*/, "").trim();
2601
- if (!fileToOpen) {
2602
- printToContent(chalk4.yellow("Usage: /open <filepath>\nExample: /open src/index.ts"));
2603
- render();
2604
- return;
2605
- }
2606
- const resolved = path5.resolve(session.directory, fileToOpen);
2607
- if (!fs5.existsSync(resolved)) {
2608
- printToContent(chalk4.red(`File not found: ${fileToOpen}`));
2609
- render();
2610
- return;
2611
- }
2612
- const openCmd = process.platform === "win32" ? `start "" "${resolved}"` : process.platform === "darwin" ? `open "${resolved}"` : `xdg-open "${resolved}"`;
2613
- try {
2614
- execSync(openCmd, {
2615
- shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh",
2616
- stdio: "ignore"
2617
- });
2618
- printToContent(chalk4.green(`\u2714 Opened ${fileToOpen}`));
2619
- } catch (err) {
2620
- printToContent(chalk4.red(`Failed to open file: ${err.message}`));
2621
- }
2622
- render();
2623
- return;
2624
- }
2625
- if (submitted.startsWith("/add")) {
2626
- const targetPath = submitted.replace(/^\/add\s*/, "").trim();
2627
- if (!targetPath) {
2628
- printToContent(chalk4.yellow("Usage: /add <filepath>\nExample: /add src/server/client.ts"));
2629
- render();
2630
- return;
2631
- }
2632
- const resolved = path5.resolve(session.directory, targetPath);
2633
- if (!fs5.existsSync(resolved)) {
2634
- printToContent(chalk4.red(`File does not exist: ${targetPath}`));
2635
- render();
2636
- return;
2637
- }
2638
- try {
2639
- const stats = fs5.statSync(resolved);
2640
- if (stats.isDirectory()) {
2641
- printToContent(chalk4.yellow(`Cannot add directory directly. Specify a file path: /add ${targetPath}/<filename>`));
2642
- render();
2643
- return;
2644
- }
2645
- const content = fs5.readFileSync(resolved, "utf-8");
2646
- pinnedContext.set(targetPath, content);
2647
- printToContent(chalk4.green(`\u2714 Added "${targetPath}" (${content.length} chars) to active context.`));
2648
- } catch (err) {
2649
- printToContent(chalk4.red(`Failed to read file: ${err.message}`));
2650
- }
2651
- render();
2652
- return;
2653
- }
2654
- if (submitted.startsWith("/drop")) {
2655
- const targetPath = submitted.replace(/^\/drop\s*/, "").trim();
2656
- if (!targetPath) {
2657
- printToContent(chalk4.yellow("Usage: /drop <filepath> or /drop all"));
2658
- render();
2659
- return;
2660
- }
2661
- if (targetPath.toLowerCase() === "all") {
2662
- const count = pinnedContext.size;
2663
- pinnedContext.clear();
2664
- printToContent(chalk4.green(`\u2714 Dropped all ${count} pinned item(s) from session context.`));
2665
- } else if (pinnedContext.has(targetPath)) {
2666
- pinnedContext.delete(targetPath);
2667
- printToContent(chalk4.green(`\u2714 Dropped "${targetPath}" from session context.`));
2668
- } else {
2669
- let matched = false;
2670
- for (const key2 of pinnedContext.keys()) {
2671
- if (key2.includes(targetPath)) {
2672
- pinnedContext.delete(key2);
2673
- printToContent(chalk4.green(`\u2714 Dropped "${key2}" from session context.`));
2674
- matched = true;
2675
- break;
2676
- }
2677
- }
2678
- if (!matched) {
2679
- printToContent(chalk4.yellow(`"${targetPath}" was not found in pinned context.`));
2680
- }
2681
- }
2682
- render();
2683
- return;
2684
- }
2685
- if (submitted === "/context") {
2686
- const lines = [];
2687
- lines.push(chalk4.bold("Active Session Context:"));
2688
- lines.push(` \u2022 Model: ${chalk4.cyan(getFriendlyModelName(currentBackendModel))}`);
2689
- lines.push(` \u2022 Agent Mode: ${chalk4.cyan(currentAgent.toUpperCase())}`);
2690
- lines.push(` \u2022 Working Directory: ${chalk4.dim(session.directory)}`);
2691
- if (pinnedContext.size === 0) {
2692
- lines.push(chalk4.dim(" \u2022 Pinned Files: None (use /add <file> to attach context)"));
2693
- } else {
2694
- lines.push(chalk4.bold(` \u2022 Pinned Files (${pinnedContext.size}):`));
2695
- for (const [p, content] of pinnedContext.entries()) {
2696
- lines.push(` - ${chalk4.cyan(p)} ${chalk4.dim(`(${content.length} chars)`)}`);
2697
- }
2698
- }
2699
- printToContent(lines.join("\n"));
2700
- render();
2701
- return;
2702
- }
2703
- if (submitted.startsWith("/web")) {
2704
- const targetUrl = submitted.replace(/^\/web\s*/, "").trim();
2705
- if (!targetUrl || !targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
2706
- printToContent(chalk4.yellow("Usage: /web <url>\nExample: /web https://docs.github.com/en/rest"));
2707
- render();
2708
- return;
2709
- }
2710
- printToContent(chalk4.cyan(`Fetching ${targetUrl}...`));
2711
- try {
2712
- const res = await fetch(targetUrl, { headers: { "User-Agent": "OceanCode-CLI" } });
2713
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
2714
- const html = await res.text();
2715
- const textOnly = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
2716
- const snippet = textOnly.slice(0, 1e4);
2717
- pinnedContext.set(targetUrl, snippet);
2718
- printToContent(chalk4.green(`\u2714 Fetched and pinned text from ${targetUrl} (${snippet.length} chars).`));
2719
- } catch (err) {
2720
- printToContent(chalk4.red(`Failed to fetch URL: ${err.message}`));
2721
- }
2722
- render();
2723
- return;
2724
- }
2725
- if (submitted === "/export" || submitted.startsWith("/export ")) {
2726
- const filenameArg = submitted.replace(/^\/export\s*/, "").trim();
2727
- const exportPath = filenameArg || `oceancode-session-${session.id.slice(0, 8)}.md`;
2728
- const resolvedExport = path5.resolve(session.directory, exportPath);
2729
- try {
2730
- printToContent(chalk4.cyan("Exporting session history..."));
2731
- const rawMsgs = await client.getSessionMessages(session.id);
2732
- const mdLines = [
2733
- "# OceanCode Session Export",
2734
- `**Session ID:** \`${session.id}\``,
2735
- `**Date:** ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2736
- `**Model:** ${getFriendlyModelName(currentBackendModel)}`,
2737
- `**Directory:** \`${session.directory}\``,
2738
- "\n---\n"
2739
- ];
2740
- if (Array.isArray(rawMsgs) && rawMsgs.length > 0) {
2741
- for (const msg of rawMsgs) {
2742
- const role = msg.role || msg.type || "message";
2743
- let content = "";
2744
- if (typeof msg.content === "string") {
2745
- content = msg.content;
2746
- } else if (Array.isArray(msg.parts)) {
2747
- content = msg.parts.map((p) => typeof p === "string" ? p : p.text || JSON.stringify(p)).join("\n");
2748
- } else {
2749
- content = JSON.stringify(msg);
2750
- }
2751
- mdLines.push(`### ${role.toUpperCase()}
2752
-
2753
- ${content}
2754
- `);
2755
- }
2756
- } else if (conversationHistory.length > 0) {
2757
- for (const turn of conversationHistory) {
2758
- mdLines.push(`### ${turn.role.toUpperCase()}
2759
-
2760
- ${turn.text}
2761
- `);
2762
- }
2763
- } else {
2764
- mdLines.push("_No session messages recorded yet._");
2765
- }
2766
- fs5.writeFileSync(resolvedExport, mdLines.join("\n"), "utf-8");
2767
- printToContent(`${chalk4.green(`\u2714 Session successfully exported to:`)}
2768
- ${resolvedExport}`);
2769
- } catch (err) {
2770
- printToContent(chalk4.red(`Failed to export session: ${err.message}`));
2771
- }
2772
- render();
2773
- return;
2774
- }
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);
2781
- render();
2782
- return;
2783
- }
2784
- if (submitted === "/info") {
2785
- printToContent(
2786
- `Session Details:
2787
- Model: ${getFriendlyModelName(currentBackendModel)}
2788
- Agent: ${currentAgent.toUpperCase()}
2789
- Directory: ${session.directory}
2790
- Session: ${session.id}`
2791
- );
2792
- render();
2793
- return;
2794
- }
2795
- const friendlyName = getFriendlyModelName(currentBackendModel);
2796
- process.stdout.write("\x1B8");
2797
- renderUserMessageCard(submitted, friendlyName);
2798
- process.stdout.write("\x1B7");
2799
- isProcessing = true;
2800
- userMsgId = null;
2801
- lastSubmittedPrompt = submitted;
2802
- paintBar();
2803
- resetActivityTimeout();
2804
- renderer.start(friendlyName);
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, {
2817
- model: currentBackendModel,
2818
- agent: currentAgent,
2819
- directory: session.directory
2820
- }).catch((err) => {
2821
- if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
2822
- isProcessing = false;
2823
- renderer.finish(currentAgent === "build" ? "Build" : "Plan");
2824
- printToContent(chalk4.red(`\u26A0\uFE0F Prompt Error: ${err.message}`));
2825
- paintBar();
2826
- });
2827
- return;
2828
- }
2829
- if (key.name === "left") {
2830
- cursorIndex = Math.max(0, cursorIndex - 1);
2831
- render();
2832
- return;
2833
- }
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;
2841
- render();
2842
- return;
2843
- }
2844
- if (key.name === "end") {
2845
- cursorIndex = input.length;
2846
- render();
2847
- return;
2848
- }
2849
- if (key.name === "delete") {
2850
- if (cursorIndex < input.length) {
2851
- input = input.slice(0, cursorIndex) + input.slice(cursorIndex + 1);
2852
- menuIndex = 0;
2853
- render();
2854
- }
2855
- return;
2856
- }
2857
- if (key.name === "backspace") {
2858
- if (cursorIndex > 0) {
2859
- input = input.slice(0, cursorIndex - 1) + input.slice(cursorIndex);
2860
- cursorIndex--;
2861
- menuIndex = 0;
2862
- render();
2863
- }
2864
- return;
2865
- }
2866
- if (str && !key.ctrl && !key.meta) {
2867
- if (str.startsWith("\x1B") || /^<[0-9;]+[Mm]$/.test(str)) {
2868
- return;
2869
- }
2870
- const clean = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\r\n]+/g, " ");
2871
- if (clean) {
2872
- input = input.slice(0, cursorIndex) + clean + input.slice(cursorIndex);
2873
- cursorIndex += clean.length;
2874
- menuIndex = 0;
2875
- historyIndex = -1;
2876
- render();
2877
- return;
2878
- }
2879
- }
2880
- };
2881
- process.stdin.on("keypress", onKeypress);
2882
- process.title = "oceancode";
2883
- if (process.stdout.isTTY) {
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");
2891
- renderTopBar(getFriendlyModelName(currentBackendModel));
2892
- process.stdout.write("\x1B7");
2893
- paintBar();
2894
- }
2895
- }
2896
-
2897
- // src/commands/models.ts
2898
- import chalk5 from "chalk";
2899
- async function listModelsCommand(options) {
2900
- const { leftMargin } = getLayoutDimensions();
2901
- const indent = " ".repeat(leftMargin);
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
- }
2917
- console.log("");
2918
- console.log(`${indent}${chalk5.hex("#00d2ff").bold("Ocean AI Models")}`);
2919
- console.log(`${indent}${chalk5.dim("Switch anytime in chat with /models")}
2920
- `);
2921
- console.log(`${indent}${chalk5.dim("-".repeat(40))}`);
2922
- for (const m of available) {
2923
- console.log(`${indent} \u2022 ${chalk5.white.bold(m.name)}`);
2924
- }
2925
- console.log(`
2926
- ${indent}${chalk5.dim("Tip: Type /models in interactive chat to switch models with arrow keys.")}
2927
- `);
2928
- }
2929
-
2930
- // src/commands/session.ts
2931
- import chalk6 from "chalk";
2932
- async function sessionCommand() {
2933
- const baseUrl = await ensureServer();
2934
- const client = new OceanClient(baseUrl);
2935
- const { leftMargin } = getLayoutDimensions();
2936
- const indent = " ".repeat(leftMargin);
2937
- console.log("");
2938
- console.log(`${indent}${chalk6.hex("#00d2ff").bold("Recent Sessions")}`);
2939
- console.log(`${indent}${chalk6.dim("-".repeat(54))}`);
2940
- try {
2941
- const sessions = await client.listSessions(10);
2942
- if (!sessions || sessions.length === 0) {
2943
- console.log(`${indent}${chalk6.dim("No previous sessions found.")}
2944
- `);
2945
- return;
2946
- }
2947
- for (const s of sessions) {
2948
- const date = new Date(s.time?.created || Date.now()).toLocaleDateString();
2949
- const tokens = (s.tokens?.input || 0) + (s.tokens?.output || 0);
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)}`);
2952
- console.log("");
2953
- }
2954
- } catch (err) {
2955
- console.error(`${indent}${chalk6.red(`Failed to list sessions: ${err.message}`)}`);
2956
- }
2957
- }
2958
-
2959
- // src/commands/stats.ts
2960
- import chalk7 from "chalk";
2961
- async function statsCommand() {
2962
- const baseUrl = await ensureServer();
2963
- const client = new OceanClient(baseUrl);
2964
- const { leftMargin } = getLayoutDimensions();
2965
- const indent = " ".repeat(leftMargin);
2966
- console.log("");
2967
- console.log(`${indent}${chalk7.hex("#00d2ff").bold("Usage & Analytics")}`);
2968
- console.log(`${indent}${chalk7.dim("-".repeat(54))}`);
2969
- try {
2970
- const sessions = await client.listSessions(100);
2971
- let totalTokens = 0;
2972
- let totalInput = 0;
2973
- let totalOutput = 0;
2974
- let totalCost = 0;
2975
- for (const s of sessions) {
2976
- totalInput += s.tokens?.input || 0;
2977
- totalOutput += s.tokens?.output || 0;
2978
- totalCost += s.cost || 0;
2979
- }
2980
- totalTokens = totalInput + totalOutput;
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 ")}`);
2986
- console.log("");
2987
- } catch (err) {
2988
- console.error(`${indent}${chalk7.red(`Failed to load stats: ${err.message}`)}`);
2989
- }
2990
- }
2991
-
2992
- // src/index.ts
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
- }
2999
- var program = new Command();
3000
- program.enablePositionalOptions();
3001
- program.name("oceancode").description("OceanCode CLI: Next-generation AI coding assistant built for large codebases").version("0.1.2").addHelpText("before", `
3002
- ${getBigLogo()}
3003
- `);
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
- }
3018
- });
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) => {
3020
- const opts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : options;
3021
- await runCommand(messages, opts);
3022
- process.exit(0);
3023
- });
3024
- program.command("init").description("Initialize codebase context and generate or update AGENTS.md").action(async () => {
3025
- try {
3026
- const res = initializeAgentsDoc(process.cwd());
3027
- console.log(chalk8.green(`\u2714 ${res.summary}`));
3028
- console.log(chalk8.dim(` Path: ${res.filePath}`));
3029
- } catch (err) {
3030
- console.error(chalk8.red(`Error initializing AGENTS.md: ${err.message}`));
3031
- }
3032
- process.exit(0);
3033
- });
3034
- program.command("connect [provider] [key]").description("Link provider API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq, OpenRouter)").action(async (provider, key) => {
3035
- if (provider && key) {
3036
- console.log(chalk8.cyan(`Validating API key with ${provider.toUpperCase()}...`));
3037
- const val = await validateProviderKey(provider, key);
3038
- if (val.valid) {
3039
- saveProviderKey(provider, key);
3040
- console.log(chalk8.green(`\u2714 ${val.message}`));
3041
- } else {
3042
- console.error(chalk8.red(`\u2716 Validation failed: ${val.message}`));
3043
- process.exit(1);
3044
- }
3045
- process.exit(0);
3046
- }
3047
- const creds = getStoredCredentials();
3048
- console.log(chalk8.bold("\nBring Your Own Key (BYOK) Status:"));
3049
- for (const p of SUPPORTED_BYOK_PROVIDERS) {
3050
- const isSet = Boolean(creds[p.id]?.key);
3051
- const badge = isSet ? chalk8.green("\u2714 Connected") : chalk8.dim("Not linked");
3052
- console.log(` \u2022 ${p.name.padEnd(16)} (${p.id}): ${badge}`);
3053
- }
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:"));
3057
- console.log(" oceancode connect openai sk-proj-...");
3058
- console.log(" oceancode connect anthropic sk-ant-...");
3059
- console.log(" oceancode connect google AIzaSy...");
3060
- console.log(" oceancode connect deepseek sk-...\n");
3061
- process.exit(0);
3062
- });
3063
- program.command("mcp").description("List or configure Model Context Protocol (MCP) servers").allowUnknownOption().passThroughOptions().argument("[action]", "list or add").argument("[name]", "server name").argument("[command...]", "command or url").action(async (action, name, commandArgs) => {
3064
- const act = (action || "list").toLowerCase();
3065
- if (act === "add") {
3066
- if (!name || !commandArgs || commandArgs.length === 0) {
3067
- console.error(chalk8.red("Usage: oceancode mcp add <name> <command-or-url>"));
3068
- console.log("Example: oceancode mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .");
3069
- process.exit(1);
3070
- }
3071
- const target = commandArgs.join(" ");
3072
- const isUrl = target.startsWith("http://") || target.startsWith("https://");
3073
- const config = isUrl ? { type: "remote", url: target, enabled: true } : { type: "local", command: commandArgs, enabled: true };
3074
- saveMcpServer(name, config);
3075
- console.log(chalk8.green(`\u2714 Added MCP server "${name}" (${config.type}) to opencode.json`));
3076
- process.exit(0);
3077
- }
3078
- const baseUrl = await ensureServer();
3079
- const configured = getConfiguredMcpServers();
3080
- const live = await fetchLiveMcpStatus(baseUrl);
3081
- const allNames = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
3082
- console.log(chalk8.bold("\nModel Context Protocol (MCP) Servers:"));
3083
- if (allNames.length === 0) {
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>"));
3087
- } else {
3088
- for (const n of allNames) {
3089
- const conf = configured[n] || {};
3090
- const isLive = live[n] !== void 0;
3091
- const type = conf.type || (conf.url ? "remote" : "local");
3092
- const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
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)}`);
3096
- }
3097
- }
3098
- console.log();
3099
- process.exit(0);
3100
- });
3101
- program.command("models").description("List available AI models (highlighting free models)").option("-p, --provider <provider>", "Filter by provider").option("--all", "Show all models across all providers").action(async (options) => {
3102
- await listModelsCommand(options);
3103
- process.exit(0);
3104
- });
3105
- program.command("session").description("Manage and inspect coding sessions").action(async () => {
3106
- await sessionCommand();
3107
- process.exit(0);
3108
- });
3109
- program.command("stats").description("View token consumption and session statistics").action(async () => {
3110
- await statsCommand();
3111
- process.exit(0);
3112
- });
3113
- program.command("serve").description("Ensure or start the Ocean server backend").action(async () => {
3114
- const url = await ensureServer();
3115
- console.log(chalk8.green(`\u2714 Ocean backend is running at: ${url}`));
3116
- });
3117
- program.parse(process.argv);