oceancode 0.1.0
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/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/chunk-EB736JHH.js +1191 -0
- package/dist/index.js +1242 -0
- package/dist/run-R7NU5EID.js +6 -0
- package/package.json +50 -0
- package/scripts/postinstall.mjs +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
OCEAN_BLUE,
|
|
4
|
+
OceanClient,
|
|
5
|
+
SUPPORTED_BYOK_PROVIDERS,
|
|
6
|
+
StreamRenderer,
|
|
7
|
+
ensureServer,
|
|
8
|
+
getAvailableOceanModels,
|
|
9
|
+
getBackendModelId,
|
|
10
|
+
getBigLogo,
|
|
11
|
+
getFriendlyModelName,
|
|
12
|
+
getLayoutDimensions,
|
|
13
|
+
getStoredCredentials,
|
|
14
|
+
removeProviderKey,
|
|
15
|
+
renderTopBar,
|
|
16
|
+
renderUserMessageCard,
|
|
17
|
+
runCommand,
|
|
18
|
+
saveProviderKey,
|
|
19
|
+
validateProviderKey
|
|
20
|
+
} from "./chunk-EB736JHH.js";
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
import { Command } from "commander";
|
|
24
|
+
|
|
25
|
+
// src/commands/interactive.ts
|
|
26
|
+
import readline from "readline";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
|
|
29
|
+
// src/utils/init.ts
|
|
30
|
+
import fs from "fs";
|
|
31
|
+
import path from "path";
|
|
32
|
+
function initializeAgentsDoc(workspaceDir = process.cwd()) {
|
|
33
|
+
const agentsPath = path.join(workspaceDir, "AGENTS.md");
|
|
34
|
+
const alreadyExists = fs.existsSync(agentsPath);
|
|
35
|
+
let projectType = "General Codebase";
|
|
36
|
+
let techDetails = [];
|
|
37
|
+
let buildCommands = [];
|
|
38
|
+
let testCommands = [];
|
|
39
|
+
let conventions = [];
|
|
40
|
+
const pkgJsonPath = path.join(workspaceDir, "package.json");
|
|
41
|
+
const tsConfigPath = path.join(workspaceDir, "tsconfig.json");
|
|
42
|
+
const pyProjectPath = path.join(workspaceDir, "pyproject.toml");
|
|
43
|
+
const reqTxtPath = path.join(workspaceDir, "requirements.txt");
|
|
44
|
+
const cargoPath = path.join(workspaceDir, "Cargo.toml");
|
|
45
|
+
const goModPath = path.join(workspaceDir, "go.mod");
|
|
46
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
49
|
+
projectType = `Node.js / JavaScript${fs.existsSync(tsConfigPath) ? " / TypeScript" : ""}`;
|
|
50
|
+
if (pkg.name) techDetails.push(`- **Project Name**: ${pkg.name}`);
|
|
51
|
+
if (pkg.version) techDetails.push(`- **Version**: ${pkg.version}`);
|
|
52
|
+
if (pkg.description) techDetails.push(`- **Description**: ${pkg.description}`);
|
|
53
|
+
if (pkg.scripts) {
|
|
54
|
+
if (pkg.scripts.build) buildCommands.push(`- Build: \`npm run build\` (${pkg.scripts.build})`);
|
|
55
|
+
if (pkg.scripts.test) testCommands.push(`- Test: \`npm test\` (${pkg.scripts.test})`);
|
|
56
|
+
if (pkg.scripts.dev) buildCommands.push(`- Dev: \`npm run dev\` (${pkg.scripts.dev})`);
|
|
57
|
+
if (pkg.scripts.lint) buildCommands.push(`- Lint: \`npm run lint\` (${pkg.scripts.lint})`);
|
|
58
|
+
}
|
|
59
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
60
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
61
|
+
if (deps.length > 0) {
|
|
62
|
+
techDetails.push(`- **Core Dependencies**: ${deps.slice(0, 15).join(", ")}${deps.length > 15 ? ` (+${deps.length - 15} more)` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
if (devDeps.length > 0) {
|
|
65
|
+
techDetails.push(`- **Dev Dependencies**: ${devDeps.slice(0, 10).join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
} else if (fs.existsSync(pyProjectPath) || fs.existsSync(reqTxtPath)) {
|
|
70
|
+
projectType = "Python Project";
|
|
71
|
+
testCommands.push("- Test: `pytest`");
|
|
72
|
+
buildCommands.push("- Run: `python main.py`");
|
|
73
|
+
} else if (fs.existsSync(cargoPath)) {
|
|
74
|
+
projectType = "Rust Project";
|
|
75
|
+
buildCommands.push("- Build: `cargo build`");
|
|
76
|
+
testCommands.push("- Test: `cargo test`");
|
|
77
|
+
} else if (fs.existsSync(goModPath)) {
|
|
78
|
+
projectType = "Go Project";
|
|
79
|
+
buildCommands.push("- Build: `go build ./...`");
|
|
80
|
+
testCommands.push("- Test: `go test ./...`");
|
|
81
|
+
}
|
|
82
|
+
const dirs = [];
|
|
83
|
+
try {
|
|
84
|
+
const entries = fs.readdirSync(workspaceDir, { withFileTypes: true });
|
|
85
|
+
for (const e of entries) {
|
|
86
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist") {
|
|
87
|
+
dirs.push(e.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
const agentsContent = `# AGENTS.md - Repository Guidelines & AI Context
|
|
93
|
+
|
|
94
|
+
This document guides AI agents (like OceanCode) working in this repository to ensure accurate, non-hallucinatory, and maintainable software engineering.
|
|
95
|
+
|
|
96
|
+
## Project Overview
|
|
97
|
+
- **Project Type**: ${projectType}
|
|
98
|
+
- **Root Directory**: \`${workspaceDir}\`
|
|
99
|
+
${techDetails.join("\n")}
|
|
100
|
+
|
|
101
|
+
## Key Directories
|
|
102
|
+
${dirs.length > 0 ? dirs.map((d) => `- \`${d}/\``).join("\n") : "- Single root project directory"}
|
|
103
|
+
|
|
104
|
+
## Build & Test Workflow
|
|
105
|
+
${buildCommands.length > 0 ? buildCommands.join("\n") : "- Standard build scripts"}
|
|
106
|
+
${testCommands.length > 0 ? testCommands.join("\n") : "- Standard unit tests"}
|
|
107
|
+
|
|
108
|
+
## Coding Rules for AI Agents
|
|
109
|
+
1. **Never Hallucinate or Truncate**:
|
|
110
|
+
- Always produce full, working code when writing or updating files.
|
|
111
|
+
- Never insert placeholder comments like \`// ... existing code ...\` or \`// TODO\`.
|
|
112
|
+
2. **Search & Read First**:
|
|
113
|
+
- Use \`glob\` and \`grep\` to verify types, exports, and imports across the codebase before modifying or adding code.
|
|
114
|
+
- For files over 200 lines, use chunked reading (\`offset\` and \`limit\`) to inspect specific functions or classes.
|
|
115
|
+
3. **Surgical Modifications**:
|
|
116
|
+
- When updating existing files, always use \`edit\` with exact matching lines rather than overwriting the entire file with \`write\`.
|
|
117
|
+
4. **Verification**:
|
|
118
|
+
- After completing edits, run available verification scripts or tests via \`bash\` to ensure syntax, compilation, and tests succeed.
|
|
119
|
+
5. **Readable, Structured Formatting (No Walls of Text)**:
|
|
120
|
+
- Never output dense, unbroken walls of text.
|
|
121
|
+
- Always format explanations with markdown tables, clear bullet points with contextual emojis (\u{1F4C1}, \u{1F527}, \u{1F4A1}, \u26A1, \u{1F680}, \u26A0\uFE0F, \u2705, \u{1F4CC}, \u{1F3AF}), numbered steps, bold keywords, and fenced code blocks.
|
|
122
|
+
`;
|
|
123
|
+
fs.writeFileSync(agentsPath, agentsContent, "utf-8");
|
|
124
|
+
return {
|
|
125
|
+
filePath: agentsPath,
|
|
126
|
+
created: !alreadyExists,
|
|
127
|
+
projectType,
|
|
128
|
+
summary: `Configured context for ${projectType}. Generated repository guidelines in AGENTS.md.`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/server/mcp.ts
|
|
133
|
+
import path2 from "path";
|
|
134
|
+
import os from "os";
|
|
135
|
+
import fs2 from "fs";
|
|
136
|
+
function getOpencodeConfigPath() {
|
|
137
|
+
return path2.join(os.homedir(), ".config", "oceancode", "opencode", "opencode.json");
|
|
138
|
+
}
|
|
139
|
+
function getConfiguredMcpServers() {
|
|
140
|
+
const configPath = getOpencodeConfigPath();
|
|
141
|
+
if (!fs2.existsSync(configPath)) return {};
|
|
142
|
+
try {
|
|
143
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
144
|
+
const parsed = JSON.parse(raw);
|
|
145
|
+
return parsed.mcp || {};
|
|
146
|
+
} catch {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function saveMcpServer(name, config) {
|
|
151
|
+
const configPath = getOpencodeConfigPath();
|
|
152
|
+
const dir = path2.dirname(configPath);
|
|
153
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
154
|
+
let fullConfig = {};
|
|
155
|
+
if (fs2.existsSync(configPath)) {
|
|
156
|
+
try {
|
|
157
|
+
fullConfig = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!fullConfig.mcp) fullConfig.mcp = {};
|
|
162
|
+
fullConfig.mcp[name] = config;
|
|
163
|
+
fs2.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2), "utf-8");
|
|
164
|
+
try {
|
|
165
|
+
const projectConfig = path2.join(process.cwd(), "opencode.json");
|
|
166
|
+
let proj = {};
|
|
167
|
+
if (fs2.existsSync(projectConfig)) {
|
|
168
|
+
proj = JSON.parse(fs2.readFileSync(projectConfig, "utf-8"));
|
|
169
|
+
}
|
|
170
|
+
if (!proj.mcp) proj.mcp = {};
|
|
171
|
+
proj.mcp[name] = config;
|
|
172
|
+
fs2.writeFileSync(projectConfig, JSON.stringify(proj, null, 2), "utf-8");
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function fetchLiveMcpStatus(baseUrl) {
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(`${baseUrl}/mcp`);
|
|
179
|
+
if (!res.ok) return {};
|
|
180
|
+
return await res.json();
|
|
181
|
+
} catch {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/commands/interactive.ts
|
|
187
|
+
var COMMANDS = [
|
|
188
|
+
{ name: "/init", desc: "Initializes context / updates AGENTS.md" },
|
|
189
|
+
{ name: "/goal", desc: "Sets/manages autonomous completion loops" },
|
|
190
|
+
{ name: "/new", desc: "Clears conversation buffer for a fresh session" },
|
|
191
|
+
{ name: "/undo", desc: "Rolls back the last prompt and file changes" },
|
|
192
|
+
{ name: "/redo", desc: "Re-applies or regenerates the last action" },
|
|
193
|
+
{ name: "/models", desc: "Opens model picker UI to switch active LLMs" },
|
|
194
|
+
{ name: "/connect", desc: "Launches popup setup to link & verify provider API keys" },
|
|
195
|
+
{ name: "/mcp", desc: "Lists & configures Model Context Protocol servers" },
|
|
196
|
+
{ name: "/diff", desc: "Inspect files modified in session" },
|
|
197
|
+
{ name: "/agent", desc: "Switch mode (build / plan)" },
|
|
198
|
+
{ name: "/compact", desc: "Compact & summarize session memory" },
|
|
199
|
+
{ name: "/clear", desc: "Clear conversation canvas" },
|
|
200
|
+
{ name: "/info", desc: "Session details & status" },
|
|
201
|
+
{ name: "/help", desc: "Help & commands" },
|
|
202
|
+
{ name: "/exit", desc: "Exit the app" }
|
|
203
|
+
];
|
|
204
|
+
var THINKING_FRAMES = [
|
|
205
|
+
chalk.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
206
|
+
chalk.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
207
|
+
chalk.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
208
|
+
chalk.hex("#fef08a").bold(" \u224B Thinking..."),
|
|
209
|
+
chalk.hex("#fde68a").bold(" \u224B Thinking.. "),
|
|
210
|
+
chalk.hex("#fbbf24").bold(" \u224B Thinking. "),
|
|
211
|
+
chalk.hex("#f59e0b").bold(" \u224B Thinking "),
|
|
212
|
+
chalk.dim(" \u224B Thinking ")
|
|
213
|
+
];
|
|
214
|
+
async function interactiveChatCommand(options) {
|
|
215
|
+
let currentBackendModel = options?.model ? getBackendModelId(options.model) : "opencode/muse-spark-1.3-contributor-free";
|
|
216
|
+
if (!process.stdin.isTTY) {
|
|
217
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
218
|
+
const lines = [];
|
|
219
|
+
for await (const line of rl) {
|
|
220
|
+
lines.push(line);
|
|
221
|
+
}
|
|
222
|
+
if (lines.length > 0) {
|
|
223
|
+
const { runCommand: runCommand2 } = await import("./run-R7NU5EID.js");
|
|
224
|
+
await runCommand2(lines, { model: currentBackendModel });
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const baseUrl = await ensureServer();
|
|
229
|
+
const client = new OceanClient(baseUrl);
|
|
230
|
+
const conversationHistory = [];
|
|
231
|
+
let session = await client.createSession("Ocean Session", process.cwd());
|
|
232
|
+
let input = "";
|
|
233
|
+
let mode = "normal";
|
|
234
|
+
let menuIndex = 0;
|
|
235
|
+
let modelIndex = 0;
|
|
236
|
+
let modelSearch = "";
|
|
237
|
+
let connectStep = "provider_list";
|
|
238
|
+
let connectProviderIndex = 0;
|
|
239
|
+
let connectKeyInput = "";
|
|
240
|
+
let connectErrorMsg = "";
|
|
241
|
+
let connectValidatingProv = "";
|
|
242
|
+
let currentAgent = "build";
|
|
243
|
+
let isProcessing = false;
|
|
244
|
+
let activityTimeoutTimer = null;
|
|
245
|
+
let userMsgId = null;
|
|
246
|
+
let lastUserMsgId = null;
|
|
247
|
+
let lastSubmittedPrompt = "";
|
|
248
|
+
const renderer = new StreamRenderer();
|
|
249
|
+
const cleanup = () => {
|
|
250
|
+
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
251
|
+
if (process.stdout.isTTY) {
|
|
252
|
+
process.stdout.write("\x1B[r");
|
|
253
|
+
process.stdout.write("\x1B[?25h");
|
|
254
|
+
try {
|
|
255
|
+
process.stdin.setRawMode(false);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const resetActivityTimeout = () => {
|
|
261
|
+
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
262
|
+
activityTimeoutTimer = setTimeout(() => {
|
|
263
|
+
if (isProcessing) {
|
|
264
|
+
isProcessing = false;
|
|
265
|
+
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
266
|
+
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
267
|
+
process.stdout.write(
|
|
268
|
+
chalk.yellow(
|
|
269
|
+
`
|
|
270
|
+
\u26A0\uFE0F Operation timed out waiting for ${friendlyName}. You can retry or switch models with /models.
|
|
271
|
+
`
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
render();
|
|
275
|
+
}
|
|
276
|
+
}, 18e4);
|
|
277
|
+
activityTimeoutTimer.unref();
|
|
278
|
+
};
|
|
279
|
+
const BAR_ROWS = 3;
|
|
280
|
+
let currentBarRows = BAR_ROWS;
|
|
281
|
+
const applyScrollRegion = (rows) => {
|
|
282
|
+
const scrollBottom = Math.max(1, rows - currentBarRows);
|
|
283
|
+
process.stdout.write(`\x1B[1;${scrollBottom}r`);
|
|
284
|
+
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
285
|
+
};
|
|
286
|
+
const paintBar = (lines) => {
|
|
287
|
+
const rows = process.stdout.rows || 24;
|
|
288
|
+
const barStartRow = rows - lines.length + 1;
|
|
289
|
+
for (let i = 0; i < lines.length; i++) {
|
|
290
|
+
process.stdout.write(`\x1B[${barStartRow + i};1H\x1B[2K`);
|
|
291
|
+
process.stdout.write(lines[i]);
|
|
292
|
+
}
|
|
293
|
+
const scrollBottom = Math.max(1, rows - lines.length);
|
|
294
|
+
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
295
|
+
};
|
|
296
|
+
const buildBarLines = () => {
|
|
297
|
+
const cols = process.stdout.columns || 80;
|
|
298
|
+
const friendlyModel = getFriendlyModelName(currentBackendModel);
|
|
299
|
+
const bar = chalk.hex(OCEAN_BLUE)("\u2502");
|
|
300
|
+
const width = Math.min(cols - 4, 80);
|
|
301
|
+
const peachBg = chalk.bgHex("#fba978").black;
|
|
302
|
+
const lines = [];
|
|
303
|
+
if (mode === "model_selector") {
|
|
304
|
+
const header = "Select Model (\u2191/\u2193 to navigate, Enter to select, Esc to close)" + " ".repeat(Math.max(2, width - 60)) + chalk.dim("esc");
|
|
305
|
+
lines.push(chalk.bold.white(header));
|
|
306
|
+
lines.push(
|
|
307
|
+
modelSearch ? `Search: ${modelSearch}` : chalk.dim("Search models (e.g. 1m, reasoning, gpt-4o, claude)")
|
|
308
|
+
);
|
|
309
|
+
lines.push("");
|
|
310
|
+
const availableModels = getAvailableOceanModels();
|
|
311
|
+
const searchLower = modelSearch.toLowerCase();
|
|
312
|
+
const filtered = availableModels.filter(
|
|
313
|
+
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower) || m.tag.toLowerCase().includes(searchLower) || m.description.toLowerCase().includes(searchLower)
|
|
314
|
+
);
|
|
315
|
+
if (modelIndex >= filtered.length) modelIndex = Math.max(0, filtered.length - 1);
|
|
316
|
+
if (filtered.length === 0) {
|
|
317
|
+
lines.push(chalk.dim(" No matching models found."));
|
|
318
|
+
} else {
|
|
319
|
+
filtered.forEach((m, idx) => {
|
|
320
|
+
const isCurrent = m.id === currentBackendModel;
|
|
321
|
+
const prefix = isCurrent ? "\u25CF " : " ";
|
|
322
|
+
const left = prefix + m.name;
|
|
323
|
+
const tag = m.tag || (m.isFree ? "Free" : "BYOK");
|
|
324
|
+
const spaces = Math.max(2, width - left.length - tag.length);
|
|
325
|
+
const row = left + " ".repeat(spaces) + tag;
|
|
326
|
+
if (idx === modelIndex) {
|
|
327
|
+
lines.push(peachBg(row));
|
|
328
|
+
if (m.description) lines.push(chalk.dim(` ${m.description}`));
|
|
329
|
+
} else {
|
|
330
|
+
lines.push(chalk.white(row));
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
lines.push("");
|
|
335
|
+
lines.push(chalk.dim("Type /connect to link API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq)."));
|
|
336
|
+
} else if (mode === "connect_modal") {
|
|
337
|
+
const header = "Connect Provider (BYOK)" + " ".repeat(Math.max(2, width - 24)) + chalk.dim("esc");
|
|
338
|
+
lines.push(chalk.bold.white(header));
|
|
339
|
+
if (connectStep === "provider_list") {
|
|
340
|
+
lines.push(chalk.dim("Select provider with \u2191/\u2193 and press Enter (or 'd' to disconnect):"));
|
|
341
|
+
lines.push("");
|
|
342
|
+
const creds = getStoredCredentials();
|
|
343
|
+
SUPPORTED_BYOK_PROVIDERS.forEach((p, idx) => {
|
|
344
|
+
const isConnected = Boolean(creds[p.id]?.key);
|
|
345
|
+
const prefix = isConnected ? "\u25CF " : "\u25CB ";
|
|
346
|
+
const left = ` ${prefix}${p.name.padEnd(16)} (${p.id})`;
|
|
347
|
+
const tag = isConnected ? chalk.green("\u2714 Connected") : chalk.dim("Not linked");
|
|
348
|
+
const spaces = Math.max(2, width - left.length - 12);
|
|
349
|
+
const row = left + " ".repeat(spaces) + tag;
|
|
350
|
+
lines.push(idx === connectProviderIndex ? peachBg(row) : chalk.white(row));
|
|
351
|
+
});
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push(chalk.dim("Enter: Link API Key \u2022 d: Disconnect \u2022 Esc: Close"));
|
|
354
|
+
} else if (connectStep === "enter_key") {
|
|
355
|
+
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
356
|
+
lines.push(chalk.bold.hex("#38bdf8")(`Provider: ${prov.name}`));
|
|
357
|
+
lines.push(chalk.dim(`Placeholder format: ${prov.placeholder}`));
|
|
358
|
+
lines.push("");
|
|
359
|
+
const masked = connectKeyInput.length > 8 ? connectKeyInput.slice(0, 4) + "\u2022".repeat(connectKeyInput.length - 8) + connectKeyInput.slice(-4) : "\u2022".repeat(connectKeyInput.length);
|
|
360
|
+
const displayKey = connectKeyInput ? masked : chalk.dim("Paste or type API key here");
|
|
361
|
+
lines.push(`${bar} Key: ${displayKey}\u2588`);
|
|
362
|
+
if (connectErrorMsg) {
|
|
363
|
+
lines.push("");
|
|
364
|
+
lines.push(chalk.hex("#ef4444").bold(` \u26A0\uFE0F ${connectErrorMsg}`));
|
|
365
|
+
}
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push(chalk.dim("Enter: Validate & Save \u2022 Esc: Back to providers"));
|
|
368
|
+
} else if (connectStep === "validating") {
|
|
369
|
+
lines.push(chalk.bold.hex("#f59e0b")(`Verifying ${connectValidatingProv} API Key...`));
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push(chalk.hex("#fbbf24").bold(" \u224B Testing authentication with provider API..."));
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push(chalk.dim("Please wait while we verify your key with the provider."));
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
if (!isProcessing && input.startsWith("/")) {
|
|
377
|
+
const matching = COMMANDS.filter((c) => c.name.startsWith(input));
|
|
378
|
+
if (menuIndex >= matching.length) menuIndex = Math.max(0, matching.length - 1);
|
|
379
|
+
if (matching.length > 0) {
|
|
380
|
+
matching.forEach((c, idx) => {
|
|
381
|
+
const row = " " + c.name.padEnd(12) + c.desc;
|
|
382
|
+
const padded = row + " ".repeat(Math.max(2, width - row.length));
|
|
383
|
+
lines.push(idx === menuIndex ? peachBg(padded) : chalk.white(row));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const friendlyAgent = currentAgent === "build" ? "Build" : "Plan";
|
|
388
|
+
lines.push(`${bar} ${input}\u2588`);
|
|
389
|
+
lines.push(`${bar}`);
|
|
390
|
+
lines.push(
|
|
391
|
+
` ${chalk.hex(OCEAN_BLUE).bold(friendlyAgent)} ${chalk.dim("\xB7")} ${chalk.white(friendlyModel)} ${chalk.dim("\xB7 OceanCode")}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return lines;
|
|
395
|
+
};
|
|
396
|
+
const render = () => {
|
|
397
|
+
if (!process.stdout.isTTY) return;
|
|
398
|
+
const lines = buildBarLines();
|
|
399
|
+
const rows = process.stdout.rows || 24;
|
|
400
|
+
if (lines.length !== currentBarRows) {
|
|
401
|
+
currentBarRows = lines.length;
|
|
402
|
+
applyScrollRegion(rows);
|
|
403
|
+
}
|
|
404
|
+
paintBar(lines);
|
|
405
|
+
};
|
|
406
|
+
const clearPromptArea = () => {
|
|
407
|
+
if (!process.stdout.isTTY) return;
|
|
408
|
+
const rows = process.stdout.rows || 24;
|
|
409
|
+
const barStartRow = rows - currentBarRows + 1;
|
|
410
|
+
for (let i = 0; i < currentBarRows; i++) {
|
|
411
|
+
process.stdout.write(`\x1B[${barStartRow + i};1H\x1B[2K`);
|
|
412
|
+
}
|
|
413
|
+
const scrollBottom = Math.max(1, rows - currentBarRows);
|
|
414
|
+
process.stdout.write(`\x1B[${scrollBottom};1H`);
|
|
415
|
+
};
|
|
416
|
+
renderer.setPinBar(
|
|
417
|
+
() => paintBar(buildBarLines()),
|
|
418
|
+
() => currentBarRows
|
|
419
|
+
);
|
|
420
|
+
const reasoningPartIds = /* @__PURE__ */ new Set();
|
|
421
|
+
const textPartIds = /* @__PURE__ */ new Set();
|
|
422
|
+
const unsubscribe = client.subscribeEvents((event) => {
|
|
423
|
+
if (event.type && typeof event.type === "string" && event.type.startsWith("permission.")) {
|
|
424
|
+
const reqId = event.properties?.requestID || event.properties?.id;
|
|
425
|
+
if (reqId) client.replyPermission(reqId, "always");
|
|
426
|
+
}
|
|
427
|
+
if (event.properties?.sessionID !== session.id) return;
|
|
428
|
+
if (event.type === "message.updated") {
|
|
429
|
+
if (event.properties?.info?.role === "user") {
|
|
430
|
+
userMsgId = event.properties.info.id;
|
|
431
|
+
lastUserMsgId = userMsgId;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (event.type === "message.part.updated") {
|
|
435
|
+
const part = event.properties?.part;
|
|
436
|
+
if (!part) return;
|
|
437
|
+
if (userMsgId && part.messageID === userMsgId) return;
|
|
438
|
+
isProcessing = true;
|
|
439
|
+
resetActivityTimeout();
|
|
440
|
+
if (part.type === "reasoning") {
|
|
441
|
+
reasoningPartIds.add(part.id);
|
|
442
|
+
if (typeof part.text === "string") {
|
|
443
|
+
renderer.handleReasoning(part.text);
|
|
444
|
+
}
|
|
445
|
+
} else if (part.type === "tool") {
|
|
446
|
+
const toolName = part.tool || "tool";
|
|
447
|
+
const inputObj = part.state?.input || {};
|
|
448
|
+
const detail = inputObj.filePath || inputObj.command || inputObj.pattern || inputObj.query || inputObj.url || inputObj.name || part.state?.title || part.call?.command || part.call?.path || part.call?.description || "";
|
|
449
|
+
const status = part.state?.status || part.status || "";
|
|
450
|
+
renderer.handleTool(toolName, detail, part.id, status);
|
|
451
|
+
} else if (part.type === "text" && typeof part.text === "string") {
|
|
452
|
+
textPartIds.add(part.id);
|
|
453
|
+
renderer.handleText(part.text, part.id);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (event.type === "message.part.delta") {
|
|
457
|
+
const partId = event.properties?.partID;
|
|
458
|
+
if (partId && reasoningPartIds.has(partId)) return;
|
|
459
|
+
const delta = event.properties?.delta;
|
|
460
|
+
const field = event.properties?.field;
|
|
461
|
+
if (field === "text" && typeof delta === "string") {
|
|
462
|
+
resetActivityTimeout();
|
|
463
|
+
renderer.handleDelta(delta, partId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (event.type === "session.idle" || event.type === "session.status" && event.properties?.status?.type === "idle") {
|
|
467
|
+
if (isProcessing) {
|
|
468
|
+
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
469
|
+
isProcessing = false;
|
|
470
|
+
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
471
|
+
render();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (event.type === "session.error") {
|
|
475
|
+
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
476
|
+
isProcessing = false;
|
|
477
|
+
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
478
|
+
const errObj = event.properties?.error;
|
|
479
|
+
const errMsg = errObj?.message || errObj?.data?.message || (typeof errObj === "string" ? errObj : "Model request failed or timed out.");
|
|
480
|
+
console.error(chalk.red(`
|
|
481
|
+
\u26A0\uFE0F Model Error (${getFriendlyModelName(currentBackendModel)}): ${errMsg}
|
|
482
|
+
`));
|
|
483
|
+
render();
|
|
484
|
+
}
|
|
485
|
+
}, void 0, session.directory);
|
|
486
|
+
readline.emitKeypressEvents(process.stdin);
|
|
487
|
+
if (process.stdin.isTTY) {
|
|
488
|
+
process.stdin.setRawMode(true);
|
|
489
|
+
process.stdin.resume();
|
|
490
|
+
}
|
|
491
|
+
const onKeypress = async (str, key) => {
|
|
492
|
+
if (key && key.ctrl && key.name === "c") {
|
|
493
|
+
cleanup();
|
|
494
|
+
unsubscribe();
|
|
495
|
+
console.log(`
|
|
496
|
+
${chalk.cyan("\u{1F44B} Goodbye!")}
|
|
497
|
+
`);
|
|
498
|
+
process.exit(0);
|
|
499
|
+
}
|
|
500
|
+
if (isProcessing) return;
|
|
501
|
+
if (mode === "connect_modal") {
|
|
502
|
+
if (connectStep === "validating") return;
|
|
503
|
+
if (key.name === "escape") {
|
|
504
|
+
if (connectStep === "enter_key") {
|
|
505
|
+
connectStep = "provider_list";
|
|
506
|
+
connectKeyInput = "";
|
|
507
|
+
connectErrorMsg = "";
|
|
508
|
+
} else {
|
|
509
|
+
mode = "normal";
|
|
510
|
+
connectStep = "provider_list";
|
|
511
|
+
connectKeyInput = "";
|
|
512
|
+
connectErrorMsg = "";
|
|
513
|
+
}
|
|
514
|
+
render();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (connectStep === "provider_list") {
|
|
518
|
+
if (key.name === "up") {
|
|
519
|
+
connectProviderIndex = (connectProviderIndex - 1 + SUPPORTED_BYOK_PROVIDERS.length) % SUPPORTED_BYOK_PROVIDERS.length;
|
|
520
|
+
render();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (key.name === "down") {
|
|
524
|
+
connectProviderIndex = (connectProviderIndex + 1) % SUPPORTED_BYOK_PROVIDERS.length;
|
|
525
|
+
render();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (key.name === "return") {
|
|
529
|
+
connectStep = "enter_key";
|
|
530
|
+
connectKeyInput = "";
|
|
531
|
+
connectErrorMsg = "";
|
|
532
|
+
render();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (str === "d" || str === "D") {
|
|
536
|
+
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
537
|
+
removeProviderKey(prov.id);
|
|
538
|
+
clearPromptArea();
|
|
539
|
+
console.log(chalk.yellow(`
|
|
540
|
+
\u2713 Disconnected API key for ${prov.name}.
|
|
541
|
+
`));
|
|
542
|
+
render();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (connectStep === "enter_key") {
|
|
548
|
+
if (key.name === "backspace") {
|
|
549
|
+
connectKeyInput = connectKeyInput.slice(0, -1);
|
|
550
|
+
connectErrorMsg = "";
|
|
551
|
+
render();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (key.name === "return") {
|
|
555
|
+
if (!connectKeyInput.trim()) {
|
|
556
|
+
connectErrorMsg = "Please enter an API key.";
|
|
557
|
+
render();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const prov = SUPPORTED_BYOK_PROVIDERS[connectProviderIndex];
|
|
561
|
+
connectStep = "validating";
|
|
562
|
+
connectValidatingProv = prov.name;
|
|
563
|
+
connectErrorMsg = "";
|
|
564
|
+
render();
|
|
565
|
+
const result = await validateProviderKey(prov.id, connectKeyInput);
|
|
566
|
+
if (result.valid) {
|
|
567
|
+
saveProviderKey(prov.id, connectKeyInput);
|
|
568
|
+
mode = "normal";
|
|
569
|
+
connectStep = "provider_list";
|
|
570
|
+
connectKeyInput = "";
|
|
571
|
+
connectErrorMsg = "";
|
|
572
|
+
clearPromptArea();
|
|
573
|
+
console.log(
|
|
574
|
+
chalk.green(
|
|
575
|
+
`
|
|
576
|
+
\u2713 Successfully verified and connected ${prov.name}! Its models are now unlocked in /models.
|
|
577
|
+
`
|
|
578
|
+
)
|
|
579
|
+
);
|
|
580
|
+
render();
|
|
581
|
+
} else {
|
|
582
|
+
connectStep = "enter_key";
|
|
583
|
+
connectErrorMsg = result.message;
|
|
584
|
+
render();
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (str && !key.ctrl && !key.meta) {
|
|
589
|
+
connectKeyInput += str;
|
|
590
|
+
connectErrorMsg = "";
|
|
591
|
+
render();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (mode === "model_selector") {
|
|
598
|
+
const availableModels = getAvailableOceanModels();
|
|
599
|
+
const searchLower = modelSearch.toLowerCase();
|
|
600
|
+
const filtered = availableModels.filter(
|
|
601
|
+
(m) => m.name.toLowerCase().includes(searchLower) || m.id.toLowerCase().includes(searchLower) || m.tag.toLowerCase().includes(searchLower) || m.description.toLowerCase().includes(searchLower)
|
|
602
|
+
);
|
|
603
|
+
if (key.name === "escape") {
|
|
604
|
+
mode = "normal";
|
|
605
|
+
modelSearch = "";
|
|
606
|
+
render();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key.name === "up") {
|
|
610
|
+
if (filtered.length > 0) {
|
|
611
|
+
modelIndex = (modelIndex - 1 + filtered.length) % filtered.length;
|
|
612
|
+
}
|
|
613
|
+
render();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (key.name === "down") {
|
|
617
|
+
if (filtered.length > 0) {
|
|
618
|
+
modelIndex = (modelIndex + 1) % filtered.length;
|
|
619
|
+
}
|
|
620
|
+
render();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (key.name === "return") {
|
|
624
|
+
let selectedFriendly = "";
|
|
625
|
+
let selectedId = "";
|
|
626
|
+
if (filtered[modelIndex]) {
|
|
627
|
+
currentBackendModel = filtered[modelIndex].id;
|
|
628
|
+
selectedId = currentBackendModel;
|
|
629
|
+
selectedFriendly = getFriendlyModelName(currentBackendModel);
|
|
630
|
+
}
|
|
631
|
+
mode = "normal";
|
|
632
|
+
input = "";
|
|
633
|
+
modelSearch = "";
|
|
634
|
+
clearPromptArea();
|
|
635
|
+
if (selectedFriendly) {
|
|
636
|
+
console.log(chalk.green(`
|
|
637
|
+
\u2713 Active model changed to: ${selectedFriendly} (${selectedId})
|
|
638
|
+
`));
|
|
639
|
+
}
|
|
640
|
+
render();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (key.name === "backspace") {
|
|
644
|
+
modelSearch = modelSearch.slice(0, -1);
|
|
645
|
+
modelIndex = 0;
|
|
646
|
+
render();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
650
|
+
modelSearch += str;
|
|
651
|
+
modelIndex = 0;
|
|
652
|
+
render();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const matching = input.startsWith("/") ? COMMANDS.filter((c) => c.name.startsWith(input)) : [];
|
|
658
|
+
if (key.name === "escape") {
|
|
659
|
+
input = "";
|
|
660
|
+
menuIndex = 0;
|
|
661
|
+
render();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (matching.length > 0) {
|
|
665
|
+
if (key.name === "up") {
|
|
666
|
+
menuIndex = (menuIndex - 1 + matching.length) % matching.length;
|
|
667
|
+
render();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (key.name === "down") {
|
|
671
|
+
menuIndex = (menuIndex + 1) % matching.length;
|
|
672
|
+
render();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (key.name === "tab") {
|
|
676
|
+
if (matching[menuIndex]) {
|
|
677
|
+
input = matching[menuIndex].name;
|
|
678
|
+
}
|
|
679
|
+
render();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (key.name === "return") {
|
|
684
|
+
let submitted = input.trim();
|
|
685
|
+
if (matching.length > 0 && matching[menuIndex]) {
|
|
686
|
+
submitted = matching[menuIndex].name;
|
|
687
|
+
}
|
|
688
|
+
input = "";
|
|
689
|
+
menuIndex = 0;
|
|
690
|
+
if (!submitted) {
|
|
691
|
+
render();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (submitted === "/exit" || submitted === "/quit") {
|
|
695
|
+
cleanup();
|
|
696
|
+
unsubscribe();
|
|
697
|
+
console.log(`
|
|
698
|
+
${chalk.cyan("\u{1F44B} Goodbye!")}
|
|
699
|
+
`);
|
|
700
|
+
process.exit(0);
|
|
701
|
+
}
|
|
702
|
+
if (submitted === "/clear") {
|
|
703
|
+
clearPromptArea();
|
|
704
|
+
if (process.stdout.isTTY) {
|
|
705
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
706
|
+
}
|
|
707
|
+
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
708
|
+
render();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (submitted === "/init") {
|
|
712
|
+
clearPromptArea();
|
|
713
|
+
try {
|
|
714
|
+
const res = initializeAgentsDoc(process.cwd());
|
|
715
|
+
console.log(chalk.green(`
|
|
716
|
+
\u2714 ${res.summary}`));
|
|
717
|
+
console.log(chalk.dim(` Location: ${res.filePath}
|
|
718
|
+
`));
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.error(chalk.red(`
|
|
721
|
+
\u26A0\uFE0F Initialization failed: ${err.message}
|
|
722
|
+
`));
|
|
723
|
+
}
|
|
724
|
+
render();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (submitted === "/new") {
|
|
728
|
+
clearPromptArea();
|
|
729
|
+
try {
|
|
730
|
+
session = await client.createSession("Ocean Session", process.cwd());
|
|
731
|
+
console.log(chalk.green("\n\u2714 Started a new coding session.\n"));
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.error(chalk.red(`
|
|
734
|
+
Failed to create new session: ${err.message}
|
|
735
|
+
`));
|
|
736
|
+
}
|
|
737
|
+
render();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (submitted === "/undo") {
|
|
741
|
+
clearPromptArea();
|
|
742
|
+
if (lastUserMsgId) {
|
|
743
|
+
try {
|
|
744
|
+
const success = await client.revertSession(session.id, lastUserMsgId);
|
|
745
|
+
if (success) {
|
|
746
|
+
console.log(chalk.green("\n\u2714 Reverted the last AI action and restored previous file states.\n"));
|
|
747
|
+
} else {
|
|
748
|
+
console.log(chalk.yellow("\nCould not revert the last action.\n"));
|
|
749
|
+
}
|
|
750
|
+
} catch (err) {
|
|
751
|
+
console.error(chalk.red(`
|
|
752
|
+
Revert failed: ${err.message}
|
|
753
|
+
`));
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
console.log(chalk.yellow("\nNo previous action found to undo.\n"));
|
|
757
|
+
}
|
|
758
|
+
render();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (submitted === "/redo") {
|
|
762
|
+
if (!lastSubmittedPrompt) {
|
|
763
|
+
clearPromptArea();
|
|
764
|
+
console.log(chalk.yellow("\nNo previous action to redo.\n"));
|
|
765
|
+
render();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
submitted = lastSubmittedPrompt;
|
|
769
|
+
}
|
|
770
|
+
if (submitted.startsWith("/goal")) {
|
|
771
|
+
const goalDesc = submitted.replace(/^\/goal\s*/, "").trim();
|
|
772
|
+
if (!goalDesc) {
|
|
773
|
+
clearPromptArea();
|
|
774
|
+
console.log(
|
|
775
|
+
chalk.yellow(
|
|
776
|
+
"\nPlease provide a goal description.\nUsage: /goal <your objective or feature description>\nExample: /goal Add user authentication with JWT and refresh tokens\n"
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
render();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
submitted = `[AUTONOMOUS GOAL EXECUTION LOOP]
|
|
783
|
+
Goal: ${goalDesc}
|
|
784
|
+
|
|
785
|
+
Please accomplish this goal systematically:
|
|
786
|
+
1. Search and explore relevant files using glob and grep.
|
|
787
|
+
2. Formulate an execution plan and track with todowrite.
|
|
788
|
+
3. Implement code changes cleanly without truncations or placeholders.
|
|
789
|
+
4. Verify code with compiler or tests.
|
|
790
|
+
5. Present a clear final summary of what was accomplished.`;
|
|
791
|
+
}
|
|
792
|
+
if (submitted === "/connect" || submitted.startsWith("/connect")) {
|
|
793
|
+
const parts = submitted.split(" ").filter(Boolean);
|
|
794
|
+
const provArg = parts[1]?.toLowerCase();
|
|
795
|
+
const keyArg = parts[2];
|
|
796
|
+
if (provArg && keyArg) {
|
|
797
|
+
clearPromptArea();
|
|
798
|
+
console.log(chalk.cyan(`
|
|
799
|
+
Validating API key with ${provArg.toUpperCase()}...`));
|
|
800
|
+
const val = await validateProviderKey(provArg, keyArg);
|
|
801
|
+
if (val.valid) {
|
|
802
|
+
saveProviderKey(provArg, keyArg);
|
|
803
|
+
console.log(
|
|
804
|
+
chalk.green(
|
|
805
|
+
`\u2714 Successfully verified and connected ${provArg.toUpperCase()}! Its models are now unlocked in /models.
|
|
806
|
+
`
|
|
807
|
+
)
|
|
808
|
+
);
|
|
809
|
+
} else {
|
|
810
|
+
console.error(chalk.red(`
|
|
811
|
+
\u26A0\uFE0F Validation failed for ${provArg.toUpperCase()}: ${val.message}
|
|
812
|
+
`));
|
|
813
|
+
}
|
|
814
|
+
render();
|
|
815
|
+
return;
|
|
816
|
+
} else {
|
|
817
|
+
mode = "connect_modal";
|
|
818
|
+
connectStep = "provider_list";
|
|
819
|
+
connectProviderIndex = 0;
|
|
820
|
+
connectKeyInput = "";
|
|
821
|
+
connectErrorMsg = "";
|
|
822
|
+
render();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (submitted.startsWith("/mcp")) {
|
|
827
|
+
clearPromptArea();
|
|
828
|
+
const parts = submitted.split(" ").filter(Boolean);
|
|
829
|
+
const sub = parts[1]?.toLowerCase();
|
|
830
|
+
if (sub === "add") {
|
|
831
|
+
const serverName = parts[2];
|
|
832
|
+
const serverCmdOrUrl = parts.slice(3).join(" ");
|
|
833
|
+
if (!serverName || !serverCmdOrUrl) {
|
|
834
|
+
console.log(
|
|
835
|
+
chalk.yellow(
|
|
836
|
+
"\nUsage: /mcp add <server-name> <command-or-url>\nExamples:\n /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .\n /mcp add remote https://example.com/mcp\n"
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
render();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const isUrl = serverCmdOrUrl.startsWith("http://") || serverCmdOrUrl.startsWith("https://");
|
|
843
|
+
const mcpConfig = isUrl ? { type: "remote", url: serverCmdOrUrl, enabled: true } : { type: "local", command: serverCmdOrUrl.split(" "), enabled: true };
|
|
844
|
+
saveMcpServer(serverName, mcpConfig);
|
|
845
|
+
console.log(
|
|
846
|
+
chalk.green(`
|
|
847
|
+
\u2713 Added MCP server "${serverName}" (${mcpConfig.type}). Configured in opencode.json.
|
|
848
|
+
`)
|
|
849
|
+
);
|
|
850
|
+
render();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const configured = getConfiguredMcpServers();
|
|
854
|
+
const live = await fetchLiveMcpStatus(baseUrl);
|
|
855
|
+
const names = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
856
|
+
if (names.length === 0) {
|
|
857
|
+
console.log(
|
|
858
|
+
chalk.dim(
|
|
859
|
+
"\nNo Model Context Protocol (MCP) servers configured.\n\nTo add an MCP server, run:\n /mcp add <name> <command-or-url>\nExample:\n /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .\n"
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
const lines = names.map((n) => {
|
|
864
|
+
const conf = configured[n] || {};
|
|
865
|
+
const isLive = live[n] !== void 0;
|
|
866
|
+
const type = conf.type || (conf.url ? "remote" : "local");
|
|
867
|
+
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
868
|
+
const badge = isLive ? chalk.green("\u25CF Connected") : chalk.cyan("\u25CF Configured");
|
|
869
|
+
return ` \u2022 ${n} (${type}): ${badge}
|
|
870
|
+
${chalk.dim(target)}`;
|
|
871
|
+
}).join("\n");
|
|
872
|
+
console.log(chalk.bold("\nModel Context Protocol (MCP) Servers:"));
|
|
873
|
+
console.log(lines);
|
|
874
|
+
console.log(chalk.dim("\nAdd new servers with: /mcp add <name> <command>\n"));
|
|
875
|
+
}
|
|
876
|
+
render();
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (submitted === "/agent" || submitted.startsWith("/agent ") || submitted === "/mode" || submitted.startsWith("/mode ")) {
|
|
880
|
+
clearPromptArea();
|
|
881
|
+
if (submitted.includes(" ")) {
|
|
882
|
+
const target = submitted.split(" ")[1].toLowerCase().trim();
|
|
883
|
+
if (target === "plan" || target === "build") {
|
|
884
|
+
currentAgent = target;
|
|
885
|
+
} else {
|
|
886
|
+
currentAgent = currentAgent === "build" ? "plan" : "build";
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
currentAgent = currentAgent === "build" ? "plan" : "build";
|
|
890
|
+
}
|
|
891
|
+
console.log(chalk.cyan(`
|
|
892
|
+
\u2713 Switched mode to: ${currentAgent.toUpperCase()}
|
|
893
|
+
`));
|
|
894
|
+
render();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (submitted === "/diff") {
|
|
898
|
+
clearPromptArea();
|
|
899
|
+
try {
|
|
900
|
+
const diffs = await client.getSessionDiff(session.id);
|
|
901
|
+
if (!diffs || diffs.length === 0) {
|
|
902
|
+
console.log(chalk.dim("\nNo file modifications recorded in the current session.\n"));
|
|
903
|
+
} else {
|
|
904
|
+
const diffSummary = diffs.map(
|
|
905
|
+
(d) => ` \u2022 ${d.path || d.file || "file"} (+${d.additions || 0} -${d.deletions || 0})`
|
|
906
|
+
).join("\n");
|
|
907
|
+
console.log(chalk.bold(`
|
|
908
|
+
Modified Files (${diffs.length}):`));
|
|
909
|
+
console.log(diffSummary + "\n");
|
|
910
|
+
}
|
|
911
|
+
} catch (err) {
|
|
912
|
+
console.error(chalk.red(`
|
|
913
|
+
Failed to retrieve diff: ${err.message}
|
|
914
|
+
`));
|
|
915
|
+
}
|
|
916
|
+
render();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (submitted === "/compact") {
|
|
920
|
+
clearPromptArea();
|
|
921
|
+
try {
|
|
922
|
+
await client.summarizeSession(session.id);
|
|
923
|
+
console.log(chalk.green("\n\u2713 Session context memory compacted and summarized successfully.\n"));
|
|
924
|
+
} catch (err) {
|
|
925
|
+
console.error(chalk.red(`
|
|
926
|
+
Compact failed: ${err.message}
|
|
927
|
+
`));
|
|
928
|
+
}
|
|
929
|
+
render();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (submitted.startsWith("/model") || submitted.startsWith("/models")) {
|
|
933
|
+
const parts = submitted.split(" ");
|
|
934
|
+
const targetArg = parts.slice(1).join(" ").trim();
|
|
935
|
+
if (targetArg) {
|
|
936
|
+
clearPromptArea();
|
|
937
|
+
const resolvedId = getBackendModelId(targetArg);
|
|
938
|
+
currentBackendModel = resolvedId;
|
|
939
|
+
const friendly = getFriendlyModelName(resolvedId);
|
|
940
|
+
console.log(chalk.green(`
|
|
941
|
+
\u2713 Switched model to: ${friendly} (${resolvedId})
|
|
942
|
+
`));
|
|
943
|
+
render();
|
|
944
|
+
return;
|
|
945
|
+
} else {
|
|
946
|
+
mode = "model_selector";
|
|
947
|
+
modelSearch = "";
|
|
948
|
+
modelIndex = 0;
|
|
949
|
+
render();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (submitted === "/help") {
|
|
954
|
+
clearPromptArea();
|
|
955
|
+
console.log(chalk.bold("\nAvailable Commands:"));
|
|
956
|
+
for (const c of COMMANDS) {
|
|
957
|
+
console.log(` ${chalk.hex(OCEAN_BLUE).bold(c.name.padEnd(12))} ${chalk.dim(c.desc)}`);
|
|
958
|
+
}
|
|
959
|
+
console.log("");
|
|
960
|
+
render();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (submitted === "/info") {
|
|
964
|
+
clearPromptArea();
|
|
965
|
+
console.log(
|
|
966
|
+
`
|
|
967
|
+
Session Details:
|
|
968
|
+
Model: ${getFriendlyModelName(currentBackendModel)}
|
|
969
|
+
Agent: ${currentAgent.toUpperCase()}
|
|
970
|
+
Directory: ${session.directory}
|
|
971
|
+
Session: ${session.id}
|
|
972
|
+
`
|
|
973
|
+
);
|
|
974
|
+
render();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const friendlyName = getFriendlyModelName(currentBackendModel);
|
|
978
|
+
clearPromptArea();
|
|
979
|
+
renderUserMessageCard(submitted, friendlyName);
|
|
980
|
+
isProcessing = true;
|
|
981
|
+
userMsgId = null;
|
|
982
|
+
lastSubmittedPrompt = submitted;
|
|
983
|
+
resetActivityTimeout();
|
|
984
|
+
renderer.start(friendlyName);
|
|
985
|
+
client.promptSession(session.id, submitted, {
|
|
986
|
+
model: currentBackendModel,
|
|
987
|
+
agent: currentAgent,
|
|
988
|
+
directory: session.directory
|
|
989
|
+
}).catch((err) => {
|
|
990
|
+
if (activityTimeoutTimer) clearTimeout(activityTimeoutTimer);
|
|
991
|
+
isProcessing = false;
|
|
992
|
+
renderer.finish(currentAgent === "build" ? "Build" : "Plan");
|
|
993
|
+
console.error(chalk.red(`
|
|
994
|
+
\u26A0\uFE0F Prompt Error: ${err.message}
|
|
995
|
+
`));
|
|
996
|
+
render();
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (key.name === "backspace") {
|
|
1001
|
+
input = input.slice(0, -1);
|
|
1002
|
+
menuIndex = 0;
|
|
1003
|
+
render();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
1007
|
+
input += str;
|
|
1008
|
+
menuIndex = 0;
|
|
1009
|
+
render();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
process.stdin.on("keypress", onKeypress);
|
|
1014
|
+
if (process.stdout.isTTY) {
|
|
1015
|
+
process.stdout.on("resize", () => {
|
|
1016
|
+
const rows = process.stdout.rows || 24;
|
|
1017
|
+
applyScrollRegion(rows);
|
|
1018
|
+
render();
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (process.stdout.isTTY) {
|
|
1022
|
+
const rows = process.stdout.rows || 24;
|
|
1023
|
+
process.stdout.write("\x1B[2J");
|
|
1024
|
+
process.stdout.write("\x1B[H");
|
|
1025
|
+
renderTopBar(getFriendlyModelName(currentBackendModel));
|
|
1026
|
+
currentBarRows = BAR_ROWS;
|
|
1027
|
+
applyScrollRegion(rows);
|
|
1028
|
+
paintBar(buildBarLines());
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/commands/models.ts
|
|
1033
|
+
import chalk2 from "chalk";
|
|
1034
|
+
async function listModelsCommand(options) {
|
|
1035
|
+
const baseUrl = await ensureServer();
|
|
1036
|
+
const client = new OceanClient(baseUrl);
|
|
1037
|
+
const { leftMargin } = getLayoutDimensions();
|
|
1038
|
+
const indent = " ".repeat(leftMargin);
|
|
1039
|
+
const creds = getStoredCredentials();
|
|
1040
|
+
const available = getAvailableOceanModels(creds);
|
|
1041
|
+
console.log("");
|
|
1042
|
+
console.log(`${indent}${chalk2.hex("#00d2ff").bold("Ocean AI Models")}`);
|
|
1043
|
+
console.log(`${indent}${chalk2.dim("Switch anytime in chat with /models")}
|
|
1044
|
+
`);
|
|
1045
|
+
console.log(`${indent}${chalk2.dim("-".repeat(54))}`);
|
|
1046
|
+
const freeModels = available.filter((m) => m.isFree);
|
|
1047
|
+
const byokModels = available.filter((m) => !m.isFree);
|
|
1048
|
+
console.log(`${indent}${chalk2.bold.hex("#00f2fe")("Free Built-in Models:")}`);
|
|
1049
|
+
for (const m of freeModels) {
|
|
1050
|
+
const badge = chalk2.bgHex("#00b4d8").black.bold(` ${m.tag} `);
|
|
1051
|
+
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1052
|
+
}
|
|
1053
|
+
if (byokModels.length > 0) {
|
|
1054
|
+
console.log(`
|
|
1055
|
+
${indent}${chalk2.bold.hex("#3a7bd5")("Connected BYOK Models:")}`);
|
|
1056
|
+
for (const m of byokModels) {
|
|
1057
|
+
const badge = chalk2.bgHex("#10b981").black.bold(` ${m.tag} `);
|
|
1058
|
+
console.log(`${indent} ${badge} ${chalk2.white.bold(m.name.padEnd(30))} ${chalk2.dim(m.description)}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const unconnected = SUPPORTED_BYOK_PROVIDERS.filter((p) => !creds[p.id]?.key);
|
|
1062
|
+
if (unconnected.length > 0) {
|
|
1063
|
+
console.log(`
|
|
1064
|
+
${indent}${chalk2.dim("Connect more models via 'oceancode connect <provider> <key>' or /connect in chat:")}`);
|
|
1065
|
+
console.log(`${indent}${chalk2.dim(`Unlinked: ${unconnected.map((p) => p.name).join(", ")}`)}`);
|
|
1066
|
+
}
|
|
1067
|
+
console.log(`
|
|
1068
|
+
${indent}${chalk2.dim("Tip: Type /models in interactive chat to switch models with arrow keys.")}
|
|
1069
|
+
`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/commands/session.ts
|
|
1073
|
+
import chalk3 from "chalk";
|
|
1074
|
+
async function sessionCommand() {
|
|
1075
|
+
const baseUrl = await ensureServer();
|
|
1076
|
+
const client = new OceanClient(baseUrl);
|
|
1077
|
+
const { leftMargin } = getLayoutDimensions();
|
|
1078
|
+
const indent = " ".repeat(leftMargin);
|
|
1079
|
+
console.log("");
|
|
1080
|
+
console.log(`${indent}${chalk3.hex("#00d2ff").bold("Recent Sessions")}`);
|
|
1081
|
+
console.log(`${indent}${chalk3.dim("-".repeat(54))}`);
|
|
1082
|
+
try {
|
|
1083
|
+
const sessions = await client.listSessions(10);
|
|
1084
|
+
if (!sessions || sessions.length === 0) {
|
|
1085
|
+
console.log(`${indent}${chalk3.dim("No previous sessions found.")}
|
|
1086
|
+
`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
for (const s of sessions) {
|
|
1090
|
+
const date = new Date(s.time?.created || Date.now()).toLocaleDateString();
|
|
1091
|
+
const tokens = (s.tokens?.input || 0) + (s.tokens?.output || 0);
|
|
1092
|
+
console.log(`${indent}${chalk3.cyan(s.id.slice(0, 16))} ${chalk3.bold.white(s.title || "Untitled")}`);
|
|
1093
|
+
console.log(`${indent} ${chalk3.dim(date)} \uFFFD ${chalk3.dim(`${tokens} tokens`)} \uFFFD ${chalk3.dim(s.directory)}`);
|
|
1094
|
+
console.log("");
|
|
1095
|
+
}
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
console.error(`${indent}${chalk3.red(`Failed to list sessions: ${err.message}`)}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/commands/stats.ts
|
|
1102
|
+
import chalk4 from "chalk";
|
|
1103
|
+
async function statsCommand() {
|
|
1104
|
+
const baseUrl = await ensureServer();
|
|
1105
|
+
const client = new OceanClient(baseUrl);
|
|
1106
|
+
const { leftMargin } = getLayoutDimensions();
|
|
1107
|
+
const indent = " ".repeat(leftMargin);
|
|
1108
|
+
console.log("");
|
|
1109
|
+
console.log(`${indent}${chalk4.hex("#00d2ff").bold("Usage & Analytics")}`);
|
|
1110
|
+
console.log(`${indent}${chalk4.dim("-".repeat(54))}`);
|
|
1111
|
+
try {
|
|
1112
|
+
const sessions = await client.listSessions(100);
|
|
1113
|
+
let totalTokens = 0;
|
|
1114
|
+
let totalInput = 0;
|
|
1115
|
+
let totalOutput = 0;
|
|
1116
|
+
let totalCost = 0;
|
|
1117
|
+
for (const s of sessions) {
|
|
1118
|
+
totalInput += s.tokens?.input || 0;
|
|
1119
|
+
totalOutput += s.tokens?.output || 0;
|
|
1120
|
+
totalCost += s.cost || 0;
|
|
1121
|
+
}
|
|
1122
|
+
totalTokens = totalInput + totalOutput;
|
|
1123
|
+
console.log(`${indent}${chalk4.dim("Total Sessions:")} ${chalk4.bold.white(sessions.length)}`);
|
|
1124
|
+
console.log(`${indent}${chalk4.dim("Total Tokens:")} ${chalk4.bold.cyan(totalTokens.toLocaleString())}`);
|
|
1125
|
+
console.log(`${indent}${chalk4.dim("Input Tokens:")} ${chalk4.white(totalInput.toLocaleString())}`);
|
|
1126
|
+
console.log(`${indent}${chalk4.dim("Output Tokens:")} ${chalk4.white(totalOutput.toLocaleString())}`);
|
|
1127
|
+
console.log(`${indent}${chalk4.dim("Total Cost:")} ${chalk4.bold.green("$" + totalCost.toFixed(4))} ${chalk4.bgHex("#00b4d8").black.bold(" FREE ")}`);
|
|
1128
|
+
console.log("");
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.error(`${indent}${chalk4.red(`Failed to load stats: ${err.message}`)}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/index.ts
|
|
1135
|
+
import chalk5 from "chalk";
|
|
1136
|
+
var program = new Command();
|
|
1137
|
+
program.enablePositionalOptions();
|
|
1138
|
+
program.name("oceancode").description("Ocean CLI: Next-generation AI coding assistant built for large codebases").version("0.1.0").addHelpText("before", `
|
|
1139
|
+
${getBigLogo()}
|
|
1140
|
+
`);
|
|
1141
|
+
program.option("-m, --model <model>", "Model to use (default: opencode/muse-spark-1.3-contributor-free [1M Context])").action(async (options) => {
|
|
1142
|
+
await interactiveChatCommand({ model: options.model });
|
|
1143
|
+
});
|
|
1144
|
+
program.command("run [message...]").description("Run a prompt or instruction directly").option("-m, --model <model>", "Model to use (default: opencode/muse-spark-1.3-contributor-free)").option("-a, --agent <agent>", "Agent mode (default: build)").option("--auto", "Auto-approve tool permissions").action(async (messages, options, cmd) => {
|
|
1145
|
+
const opts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : options;
|
|
1146
|
+
await runCommand(messages, opts);
|
|
1147
|
+
process.exit(0);
|
|
1148
|
+
});
|
|
1149
|
+
program.command("init").description("Initialize codebase context and generate or update AGENTS.md").action(async () => {
|
|
1150
|
+
try {
|
|
1151
|
+
const res = initializeAgentsDoc(process.cwd());
|
|
1152
|
+
console.log(chalk5.green(`\u2714 ${res.summary}`));
|
|
1153
|
+
console.log(chalk5.dim(` Path: ${res.filePath}`));
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
console.error(chalk5.red(`Error initializing AGENTS.md: ${err.message}`));
|
|
1156
|
+
}
|
|
1157
|
+
process.exit(0);
|
|
1158
|
+
});
|
|
1159
|
+
program.command("connect [provider] [key]").description("Link provider API keys (OpenAI, Anthropic, Gemini, DeepSeek, Groq, OpenRouter)").action(async (provider, key) => {
|
|
1160
|
+
if (provider && key) {
|
|
1161
|
+
console.log(chalk5.cyan(`Validating API key with ${provider.toUpperCase()}...`));
|
|
1162
|
+
const val = await validateProviderKey(provider, key);
|
|
1163
|
+
if (val.valid) {
|
|
1164
|
+
saveProviderKey(provider, key);
|
|
1165
|
+
console.log(chalk5.green(`\u2714 ${val.message}`));
|
|
1166
|
+
} else {
|
|
1167
|
+
console.error(chalk5.red(`\u2716 Validation failed: ${val.message}`));
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
process.exit(0);
|
|
1171
|
+
}
|
|
1172
|
+
const creds = getStoredCredentials();
|
|
1173
|
+
console.log(chalk5.bold("\nBring Your Own Key (BYOK) Status:"));
|
|
1174
|
+
for (const p of SUPPORTED_BYOK_PROVIDERS) {
|
|
1175
|
+
const isSet = Boolean(creds[p.id]);
|
|
1176
|
+
const badge = isSet ? chalk5.green("\u2714 Connected") : chalk5.dim("Not linked");
|
|
1177
|
+
console.log(` \u2022 ${p.name.padEnd(16)} (${p.id}): ${badge}`);
|
|
1178
|
+
}
|
|
1179
|
+
console.log(chalk5.dim("\nTo link a key, run:"));
|
|
1180
|
+
console.log(chalk5.cyan(" oceancode connect <provider> <api-key>"));
|
|
1181
|
+
console.log(chalk5.dim("Examples:"));
|
|
1182
|
+
console.log(" oceancode connect openai sk-proj-...");
|
|
1183
|
+
console.log(" oceancode connect anthropic sk-ant-...");
|
|
1184
|
+
console.log(" oceancode connect google AIzaSy...");
|
|
1185
|
+
console.log(" oceancode connect deepseek sk-...\n");
|
|
1186
|
+
process.exit(0);
|
|
1187
|
+
});
|
|
1188
|
+
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) => {
|
|
1189
|
+
const act = (action || "list").toLowerCase();
|
|
1190
|
+
if (act === "add") {
|
|
1191
|
+
if (!name || !commandArgs || commandArgs.length === 0) {
|
|
1192
|
+
console.error(chalk5.red("Usage: oceancode mcp add <name> <command-or-url>"));
|
|
1193
|
+
console.log("Example: oceancode mcp add filesystem npx -y @modelcontextprotocol/server-filesystem .");
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
}
|
|
1196
|
+
const target = commandArgs.join(" ");
|
|
1197
|
+
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
1198
|
+
const config = isUrl ? { type: "remote", url: target, enabled: true } : { type: "local", command: commandArgs, enabled: true };
|
|
1199
|
+
saveMcpServer(name, config);
|
|
1200
|
+
console.log(chalk5.green(`\u2714 Added MCP server "${name}" (${config.type}) to opencode.json`));
|
|
1201
|
+
process.exit(0);
|
|
1202
|
+
}
|
|
1203
|
+
const baseUrl = await ensureServer();
|
|
1204
|
+
const configured = getConfiguredMcpServers();
|
|
1205
|
+
const live = await fetchLiveMcpStatus(baseUrl);
|
|
1206
|
+
const allNames = Array.from(/* @__PURE__ */ new Set([...Object.keys(configured), ...Object.keys(live)]));
|
|
1207
|
+
console.log(chalk5.bold("\nModel Context Protocol (MCP) Servers:"));
|
|
1208
|
+
if (allNames.length === 0) {
|
|
1209
|
+
console.log(chalk5.dim(" No MCP servers configured."));
|
|
1210
|
+
console.log(chalk5.dim("\nTo add an MCP server, run:"));
|
|
1211
|
+
console.log(chalk5.cyan(" oceancode mcp add <name> <command-or-url>"));
|
|
1212
|
+
} else {
|
|
1213
|
+
for (const n of allNames) {
|
|
1214
|
+
const conf = configured[n] || {};
|
|
1215
|
+
const isLive = live[n] !== void 0;
|
|
1216
|
+
const type = conf.type || (conf.url ? "remote" : "local");
|
|
1217
|
+
const target = conf.url || (conf.command ? conf.command.join(" ") : "active");
|
|
1218
|
+
const status = isLive ? chalk5.green("\u25CF Connected") : chalk5.cyan("\u25CF Configured");
|
|
1219
|
+
console.log(` \u2022 ${chalk5.bold(n)} (${type}) ${status}
|
|
1220
|
+
${chalk5.dim(target)}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
console.log();
|
|
1224
|
+
process.exit(0);
|
|
1225
|
+
});
|
|
1226
|
+
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) => {
|
|
1227
|
+
await listModelsCommand(options);
|
|
1228
|
+
process.exit(0);
|
|
1229
|
+
});
|
|
1230
|
+
program.command("session").description("Manage and inspect coding sessions").action(async () => {
|
|
1231
|
+
await sessionCommand();
|
|
1232
|
+
process.exit(0);
|
|
1233
|
+
});
|
|
1234
|
+
program.command("stats").description("View token consumption and session statistics").action(async () => {
|
|
1235
|
+
await statsCommand();
|
|
1236
|
+
process.exit(0);
|
|
1237
|
+
});
|
|
1238
|
+
program.command("serve").description("Ensure or start the Ocean server backend").action(async () => {
|
|
1239
|
+
const url = await ensureServer();
|
|
1240
|
+
console.log(chalk5.green(`\u2714 Ocean backend is running at: ${url}`));
|
|
1241
|
+
});
|
|
1242
|
+
program.parse(process.argv);
|