@aipanel/provider-opencode 1.2.0-beta.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * OpenCode Provider 专属常量与默认值
3
+ * 与 OpenCode Web 绑定的常量自包含于此,核心层不感知。
4
+ */
5
+ import type { OpenCodeProviderOptions } from "./types";
6
+ /** ==================== OpenCode localStorage 键 ==================== */
7
+ /** OpenCode localStorage 配置键 */
8
+ export declare const OPENCODE_STORAGE_KEYS: {
9
+ /** 设置键 (settings.v3) */
10
+ readonly SETTINGS: "settings.v3";
11
+ /** 配色方案键 */
12
+ readonly COLOR_SCHEME: "opencode-color-scheme";
13
+ /** 主题 ID 键 */
14
+ readonly THEME_ID: "opencode-theme-id";
15
+ };
16
+ /** ==================== OpenCode 默认设置 ==================== */
17
+ /** OpenCode 默认设置(与 OpenCode Web localStorage settings.v3 对应) */
18
+ export declare const DEFAULT_OPENCODE_SETTINGS: {
19
+ general: {
20
+ showReasoningSummaries: boolean;
21
+ newLayoutDesigns: boolean;
22
+ showFileTree: boolean;
23
+ editToolPartsExpanded: boolean;
24
+ shellToolPartsExpanded: boolean;
25
+ };
26
+ };
27
+ /** ==================== 运行环境 ==================== */
28
+ /** OpenCode 缓存目录(相对于项目根目录,存放 opencode.json 等运行状态) */
29
+ export declare const OPENCODE_CACHE_DIR = "node_modules/.cache/opencode";
30
+ /** ==================== Provider 专属配置默认值 ==================== */
31
+ /** OpenCode Provider 专属配置默认值(插件组装 config 时使用) */
32
+ export declare const DEFAULT_OPENCODE_PROVIDER_OPTIONS: OpenCodeProviderOptions;
@@ -0,0 +1,29 @@
1
+ const OPENCODE_STORAGE_KEYS = {
2
+ /** 设置键 (settings.v3) */
3
+ SETTINGS: "settings.v3",
4
+ /** 配色方案键 */
5
+ COLOR_SCHEME: "opencode-color-scheme",
6
+ /** 主题 ID 键 */
7
+ THEME_ID: "opencode-theme-id"
8
+ };
9
+ const DEFAULT_OPENCODE_SETTINGS = {
10
+ general: {
11
+ showReasoningSummaries: true,
12
+ newLayoutDesigns: true,
13
+ showFileTree: false,
14
+ editToolPartsExpanded: true,
15
+ shellToolPartsExpanded: true
16
+ }
17
+ };
18
+ const OPENCODE_CACHE_DIR = "node_modules/.cache/opencode";
19
+ const DEFAULT_OPENCODE_PROVIDER_OPTIONS = {
20
+ enableLsp: true,
21
+ enableBlockOnError: false,
22
+ enablePrettier: true
23
+ };
24
+ export {
25
+ DEFAULT_OPENCODE_PROVIDER_OPTIONS,
26
+ DEFAULT_OPENCODE_SETTINGS,
27
+ OPENCODE_CACHE_DIR,
28
+ OPENCODE_STORAGE_KEYS
29
+ };
package/es/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * OpenCode Web Provider
3
+ * 实现 WebProvider 契约:进程管理、REST 会话 API、桥接脚本、CLI 环境检查。
4
+ * 所有 OpenCode 专属类型与常量自包含于此包。
5
+ */
6
+ import type { ProviderInitContext, WebProvider } from "@aipanel/core";
7
+ /** 约定工厂:核心层动态加载本包后调用,初始化动作完全由 Provider 定义 */
8
+ export declare function createProvider(ctx: ProviderInitContext): WebProvider;
9
+ export { OpenCodeAPI } from "./api";
10
+ export type { DefaultWebProviderConfig, DefaultWebProviderDeps } from "./provider";
11
+ export { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web";
12
+ export { generateBridgeScript, type BridgeScriptOptions } from "./bridge-script";
13
+ export { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system";
14
+ export { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants";
15
+ export type { OpenCodeProviderOptions, OpenCodeLanguage, OpenCodeSettings, SessionInfo, WebOptions, } from "./types";
package/es/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import { DefaultWebProvider } from "./provider.js";
2
+ function createProvider(ctx) {
3
+ return new DefaultWebProvider(
4
+ { hostname: ctx.hostname, chromeDevtoolsPort: ctx.chromeDevtoolsPort },
5
+ { getWebPort: ctx.getWebPort, getProxyPort: ctx.getProxyPort },
6
+ ctx.options
7
+ );
8
+ }
9
+ import { OpenCodeAPI } from "./api.js";
10
+ import { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web.js";
11
+ import { generateBridgeScript } from "./bridge-script.js";
12
+ import { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system.js";
13
+ import { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants.js";
14
+ export {
15
+ DEFAULT_OPENCODE_PROVIDER_OPTIONS,
16
+ OpenCodeAPI,
17
+ checkOpenCodeInstalled,
18
+ createProvider,
19
+ generateBridgeScript,
20
+ getOpenCodeVersion,
21
+ killOrphanOpenCodeProcesses,
22
+ prepareOpenCodeRuntime,
23
+ startOpenCodeWeb
24
+ };
@@ -0,0 +1,4 @@
1
+ import type { ResultPromise } from "execa";
2
+ import type { WebOptions } from "./types";
3
+ export declare function prepareOpenCodeRuntime(cwd: string, vitePort: number, enableLsp?: boolean, enablePrettier?: boolean): string;
4
+ export declare function startOpenCodeWeb(options: WebOptions): ResultPromise;
@@ -0,0 +1,276 @@
1
+ import { execa } from "execa";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { OPENCODE_CACHE_DIR } from "./constants.js";
6
+ import {
7
+ MCP_API_PATH,
8
+ VSCODE_EXTENSION_PORT,
9
+ ENV_VSCODE_PORT,
10
+ createLogger,
11
+ getProcessLogBuffer,
12
+ createPackageRequire,
13
+ resolvePackageDir
14
+ } from "@aipanel/core/node";
15
+ const require2 = createPackageRequire();
16
+ const packageDir = resolvePackageDir("@aipanel/opencode-plugins");
17
+ const log = createLogger("OpenCodeWeb");
18
+ function prepareOpenCodeRuntime(cwd, vitePort, enableLsp, enablePrettier) {
19
+ const cacheDir = path.join(cwd, OPENCODE_CACHE_DIR);
20
+ log.debug("Setting up OpenCode runtime", { cacheDir, enableLsp });
21
+ if (!fs.existsSync(cacheDir)) {
22
+ fs.mkdirSync(cacheDir, { recursive: true });
23
+ }
24
+ const sourcePluginsDir = resolveSourcePluginsDir();
25
+ const plugins = resolvePluginEntries(sourcePluginsDir);
26
+ const formatterConfig = buildFormatterConfig(enablePrettier);
27
+ const opencodeConfigPath = path.join(cacheDir, "opencode.json");
28
+ const config = {
29
+ plugin: plugins,
30
+ formatter: formatterConfig,
31
+ mcp: {
32
+ "chrome-devtools": {
33
+ type: "remote",
34
+ url: `http://localhost:${vitePort}${MCP_API_PATH}`
35
+ }
36
+ }
37
+ };
38
+ fs.writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2));
39
+ log.debug("OpenCode runtime ready", {
40
+ cacheDir,
41
+ opencodeConfigPath,
42
+ pluginCount: plugins.length
43
+ });
44
+ return cacheDir;
45
+ }
46
+ function startOpenCodeWeb(options) {
47
+ const {
48
+ port,
49
+ hostname,
50
+ cwd,
51
+ configDir,
52
+ corsOrigins,
53
+ contextApiUrl,
54
+ logsApiUrl,
55
+ logFilesJson,
56
+ enableBlockOnError,
57
+ verbose,
58
+ enableLsp,
59
+ vueDevtoolsApiUrl
60
+ } = options;
61
+ const stateDir = createStateDirectory(cwd);
62
+ log.debug("Building process environment", {
63
+ stateDir,
64
+ configDir,
65
+ contextApiUrl,
66
+ logsApiUrl,
67
+ logFilesJson,
68
+ enableBlockOnError,
69
+ verbose,
70
+ enableLsp
71
+ });
72
+ const env = buildProcessEnv(
73
+ stateDir,
74
+ configDir,
75
+ contextApiUrl,
76
+ logsApiUrl,
77
+ logFilesJson,
78
+ enableBlockOnError,
79
+ verbose,
80
+ enableLsp,
81
+ vueDevtoolsApiUrl,
82
+ cwd
83
+ );
84
+ const args = ["serve", "--port", String(port), "--hostname", hostname];
85
+ if (corsOrigins && corsOrigins.length > 0) {
86
+ corsOrigins.forEach((origin) => {
87
+ args.push("--cors", origin);
88
+ });
89
+ log.debug("CORS origins added", { origins: corsOrigins });
90
+ }
91
+ log.debug("Spawning OpenCode process", {
92
+ command: "opencode",
93
+ args: args.join(" "),
94
+ cwd
95
+ });
96
+ const proc = execa("opencode", args, {
97
+ cwd,
98
+ env,
99
+ reject: false,
100
+ cleanup: true,
101
+ shell: true
102
+ });
103
+ proc.stdout?.on("data", (data) => {
104
+ const output = data.toString().trim();
105
+ if (output) {
106
+ log.debug("[OpenCode stdout]", { output });
107
+ getProcessLogBuffer().addProviderStdout(output);
108
+ }
109
+ });
110
+ proc.stderr?.on("data", (data) => {
111
+ const output = data.toString().trim();
112
+ if (output) {
113
+ if (output.includes("MaxListenersExceededWarning")) return;
114
+ log.warn("[OpenCode stderr]", { output });
115
+ getProcessLogBuffer().addProviderStderr(output);
116
+ }
117
+ });
118
+ return proc;
119
+ }
120
+ function createStateDirectory(cwd) {
121
+ const stateDir = path.join(cwd, OPENCODE_CACHE_DIR);
122
+ if (!fs.existsSync(stateDir)) {
123
+ fs.mkdirSync(stateDir, { recursive: true });
124
+ log.debug("Created state directory", { stateDir });
125
+ }
126
+ return stateDir;
127
+ }
128
+ function buildFormatterConfig(enablePrettier) {
129
+ if (enablePrettier === false) {
130
+ log.debug("enablePrettier is false, formatter disabled");
131
+ return false;
132
+ }
133
+ const bridgePath = resolveFormatBridgePath();
134
+ if (!bridgePath) {
135
+ log.debug("format-bridge not found, using built-in formatters");
136
+ return true;
137
+ }
138
+ log.debug("Format bridge configured");
139
+ if (!isFormatServiceRunning()) {
140
+ log.debug("VS Code format service not running, using built-in formatters only");
141
+ return true;
142
+ }
143
+ log.debug("VS Code format service detected, enabling bridge");
144
+ log.info("\u5DF2\u8FDE\u63A5 VS Code \u683C\u5F0F\u5316\u670D\u52A1");
145
+ const extensions = [
146
+ ".ts",
147
+ ".tsx",
148
+ ".mts",
149
+ ".cts",
150
+ ".js",
151
+ ".jsx",
152
+ ".mjs",
153
+ ".cjs",
154
+ ".vue",
155
+ ".svelte",
156
+ ".astro",
157
+ ".css",
158
+ ".scss",
159
+ ".sass",
160
+ ".less",
161
+ ".pcss",
162
+ ".html",
163
+ ".htm",
164
+ ".xml",
165
+ ".svg",
166
+ ".json",
167
+ ".jsonc",
168
+ ".yaml",
169
+ ".yml",
170
+ ".toml",
171
+ ".md",
172
+ ".mdx",
173
+ ".graphql",
174
+ ".gql"
175
+ ];
176
+ return {
177
+ format_bridge: {
178
+ command: ["node", bridgePath, "$FILE"],
179
+ extensions
180
+ }
181
+ };
182
+ }
183
+ let _formatServiceRunning;
184
+ function isFormatServiceRunning() {
185
+ if (_formatServiceRunning !== void 0) return _formatServiceRunning;
186
+ try {
187
+ require2("child_process").execSync(
188
+ `node -e "const h=require('http');h.get('http://127.0.0.1:${VSCODE_EXTENSION_PORT}/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"`,
189
+ { timeout: 500, stdio: "ignore" }
190
+ );
191
+ _formatServiceRunning = true;
192
+ } catch {
193
+ _formatServiceRunning = false;
194
+ }
195
+ return _formatServiceRunning;
196
+ }
197
+ function resolveFormatBridgePath() {
198
+ const viteEntry = require2.resolve("vite-plugin-aipanel");
199
+ const bridgePath = path.resolve(path.dirname(viteEntry), "utils", "format-bridge.cjs");
200
+ if (fs.existsSync(bridgePath)) return bridgePath;
201
+ return void 0;
202
+ }
203
+ function resolveSourcePluginsDir() {
204
+ const candidatePaths = [path.join(packageDir, "es", "plugins")];
205
+ for (const candidatePath of candidatePaths) {
206
+ if (fs.existsSync(candidatePath)) {
207
+ return candidatePath;
208
+ }
209
+ }
210
+ return candidatePaths[0];
211
+ }
212
+ const MIGRATED_TO_MCP_PLUGINS = /* @__PURE__ */ new Set(["vue-devtools.js", "vite-logs.js", "service-logs.js"]);
213
+ function resolvePluginEntries(sourceDir) {
214
+ const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith(".js") && !MIGRATED_TO_MCP_PLUGINS.has(f));
215
+ const entries = files.map((file) => {
216
+ const absolutePath = path.join(sourceDir, file);
217
+ return pathToFileURL(absolutePath).href;
218
+ });
219
+ log.debug("Resolved plugin entries", { count: entries.length, entries });
220
+ return entries;
221
+ }
222
+ function buildProcessEnv(stateDir, configDir, contextApiUrl, logsApiUrl, logFilesJson, enableBlockOnError, verbose, enableLsp, vueDevtoolsApiUrl, workspace) {
223
+ const env = {
224
+ ...Object.fromEntries(
225
+ Object.entries(process.env).filter(([, v]) => v !== void 0)
226
+ ),
227
+ XDG_STATE_HOME: stateDir,
228
+ // 指向缓存目录,OpenCode 通过 opencode.json 中 plugins 字段加载插件
229
+ OPENCODE_CONFIG_DIR: stateDir
230
+ };
231
+ if (configDir) {
232
+ env.OPENCODE_CONFIG_DIR = configDir;
233
+ log.debug("Set OPENCODE_CONFIG_DIR", { configDir });
234
+ }
235
+ if (contextApiUrl) {
236
+ env.OPENCODE_CONTEXT_API_URL = contextApiUrl;
237
+ log.debug("Set OPENCODE_CONTEXT_API_URL", { contextApiUrl });
238
+ }
239
+ if (logsApiUrl) {
240
+ env.OPENCODE_VITE_LOGS_API_URL = logsApiUrl;
241
+ log.debug("Set OPENCODE_VITE_LOGS_API_URL", { logsApiUrl });
242
+ }
243
+ if (logFilesJson) {
244
+ env.OPENCODE_LOG_FILES_JSON = logFilesJson;
245
+ log.debug("Set OPENCODE_LOG_FILES_JSON", { logFilesJson });
246
+ }
247
+ if (enableBlockOnError) {
248
+ env.OPENCODE_BLOCK_ON_ERROR = "1";
249
+ log.debug("Set OPENCODE_BLOCK_ON_ERROR=1");
250
+ }
251
+ if (verbose) {
252
+ env.OPENCODE_VERBOSE = "1";
253
+ log.debug("Set OPENCODE_VERBOSE=1");
254
+ }
255
+ if (enableLsp) {
256
+ env.OPENCODE_ENABLE_LINT = "1";
257
+ log.debug("Set OPENCODE_ENABLE_LINT=1");
258
+ }
259
+ if (vueDevtoolsApiUrl) {
260
+ env.OPENCODE_VUE_DEVTOOLS_API_URL = vueDevtoolsApiUrl;
261
+ log.debug("Set OPENCODE_VUE_DEVTOOLS_API_URL", { vueDevtoolsApiUrl });
262
+ }
263
+ if (workspace) {
264
+ env.OPENCODE_WORKSPACE = workspace;
265
+ log.debug("Set OPENCODE_WORKSPACE", { workspace });
266
+ }
267
+ if (isFormatServiceRunning()) {
268
+ env[ENV_VSCODE_PORT] = String(VSCODE_EXTENSION_PORT);
269
+ log.debug("Set OPENCODE_VSCODE_PORT");
270
+ }
271
+ return env;
272
+ }
273
+ export {
274
+ prepareOpenCodeRuntime,
275
+ startOpenCodeWeb
276
+ };
@@ -0,0 +1,49 @@
1
+ import type { ChatSession, ProviderConfig, ProviderEnvironmentInfo, ProviderEvent, ProviderStartOptions, ProviderStartResult, WebProvider } from "@aipanel/core";
2
+ /** DefaultWebProvider 构造配置(核心层传入的运行时参数) */
3
+ export interface DefaultWebProviderConfig {
4
+ /** 服务主机名 */
5
+ hostname: string;
6
+ /** Chrome DevTools Protocol 端口 */
7
+ chromeDevtoolsPort: number;
8
+ }
9
+ /** DefaultWebProvider 构造依赖(端口等运行时状态由编排层提供) */
10
+ export interface DefaultWebProviderDeps {
11
+ /** 实际 Web 端口读取器 */
12
+ getWebPort: () => number;
13
+ /** 实际代理端口读取器 */
14
+ getProxyPort: () => number;
15
+ }
16
+ /**
17
+ * 默认 Web Provider
18
+ * 组合 CLI 进程管理、REST API、桥接脚本,向核心层暴露 WebProvider 契约。
19
+ */
20
+ export declare class DefaultWebProvider implements WebProvider {
21
+ private config;
22
+ readonly id = "opencode";
23
+ readonly displayName = "OpenCode Web";
24
+ /** 支持会话 URL 深链;支持代码审查面板(右上角 </> 按钮,由 bridge 渲染) */
25
+ readonly capabilities: {
26
+ readonly deepLink: true;
27
+ readonly reviewPanel: true;
28
+ };
29
+ /** REST 会话 API(Provider 内部使用) */
30
+ private readonly api;
31
+ private deps;
32
+ private process;
33
+ private bridgeOptions;
34
+ private readonly opts;
35
+ constructor(config: DefaultWebProviderConfig, deps: DefaultWebProviderDeps, options?: Record<string, unknown>);
36
+ /** 代理注入到 HTML 的桥接脚本(Provider 资产) */
37
+ get bridgeScript(): string | undefined;
38
+ /** 初始化桥接配置(主题/语言/设置) */
39
+ applyConfig(config: ProviderConfig): void;
40
+ checkEnvironment(): Promise<ProviderEnvironmentInfo>;
41
+ start(options: ProviderStartOptions): Promise<ProviderStartResult>;
42
+ stop(): Promise<void>;
43
+ killOrphans(): Promise<number>;
44
+ listSessions(projectDir: string): Promise<ChatSession[]>;
45
+ createSession(projectDir: string, title?: string): Promise<ChatSession>;
46
+ deleteSession(sessionId: string): Promise<void>;
47
+ buildSessionUrl(projectDir: string, sessionId: string): string;
48
+ subscribeEvents(handler: (e: ProviderEvent) => void): () => void;
49
+ }
package/es/provider.js ADDED
@@ -0,0 +1,257 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import http from "http";
5
+ import { RETRY_DELAY } from "@aipanel/core";
6
+ import { createLogger } from "@aipanel/core/node";
7
+ import { DEFAULT_OPENCODE_PROVIDER_OPTIONS } from "./constants.js";
8
+ import { OpenCodeAPI } from "./api.js";
9
+ import { generateBridgeScript } from "./bridge-script.js";
10
+ import { prepareOpenCodeRuntime, startOpenCodeWeb } from "./opencode-web.js";
11
+ import { checkOpenCodeInstalled, getOpenCodeVersion, killOrphanOpenCodeProcesses } from "./system.js";
12
+ const log = createLogger("DefaultWebProvider");
13
+ class DefaultWebProvider {
14
+ constructor(config, deps, options) {
15
+ __publicField(this, "config", config);
16
+ __publicField(this, "id", "opencode");
17
+ __publicField(this, "displayName", "OpenCode Web");
18
+ /** 支持会话 URL 深链;支持代码审查面板(右上角 </> 按钮,由 bridge 渲染) */
19
+ __publicField(this, "capabilities", { deepLink: true, reviewPanel: true });
20
+ /** REST 会话 API(Provider 内部使用) */
21
+ __publicField(this, "api");
22
+ __publicField(this, "deps");
23
+ __publicField(this, "process", null);
24
+ __publicField(this, "bridgeOptions", {});
25
+ __publicField(this, "opts");
26
+ this.deps = deps;
27
+ this.opts = resolveOpenCodeOptions(options);
28
+ this.api = new OpenCodeAPI(
29
+ config.hostname,
30
+ deps.getWebPort,
31
+ deps.getProxyPort,
32
+ config.chromeDevtoolsPort
33
+ );
34
+ }
35
+ /** 代理注入到 HTML 的桥接脚本(Provider 资产) */
36
+ get bridgeScript() {
37
+ return generateBridgeScript(this.bridgeOptions);
38
+ }
39
+ /** 初始化桥接配置(主题/语言/设置) */
40
+ applyConfig(config) {
41
+ this.bridgeOptions = {
42
+ theme: config.theme,
43
+ language: this.opts.language,
44
+ settings: this.opts.settings
45
+ };
46
+ }
47
+ async checkEnvironment() {
48
+ if (!await checkOpenCodeInstalled()) {
49
+ return {
50
+ ok: false,
51
+ message: `OpenCode is not installed!
52
+
53
+ Please install OpenCode first:
54
+
55
+ # YOLO
56
+ curl -fsSL https://opencode.ai/install | bash
57
+
58
+ # Package managers
59
+ npm i -g opencode-ai@latest # or bun/pnpm/yarn
60
+ scoop install opencode # Windows
61
+ choco install opencode # Windows
62
+ brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
63
+ brew install opencode # macOS and Linux (official brew formula, updated less)
64
+ sudo pacman -S opencode # Arch Linux (Stable)
65
+ paru -S opencode-bin # Arch Linux (Latest from AUR)
66
+ mise use -g opencode # Any OS
67
+ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
68
+ `
69
+ };
70
+ }
71
+ const version = await getOpenCodeVersion();
72
+ return { ok: true, version: version ?? void 0 };
73
+ }
74
+ async start(options) {
75
+ log.debug("Preparing OpenCode runtime", { cwd: options.cwd, vitePort: options.vitePort });
76
+ const configDir = prepareOpenCodeRuntime(
77
+ options.cwd,
78
+ options.vitePort,
79
+ this.opts.enableLsp,
80
+ this.opts.enablePrettier
81
+ );
82
+ log.debug("Starting OpenCode Web process", {
83
+ port: options.port,
84
+ hostname: options.hostname,
85
+ configDir
86
+ });
87
+ const proc = startOpenCodeWeb({
88
+ port: options.port,
89
+ hostname: options.hostname,
90
+ serverUrl: "",
91
+ cwd: options.cwd,
92
+ configDir,
93
+ corsOrigins: options.corsOrigins,
94
+ contextApiUrl: options.contextApiUrl,
95
+ logsApiUrl: options.logsApiUrl,
96
+ logFilesJson: this.opts.logFiles ? JSON.stringify(this.opts.logFiles) : void 0,
97
+ enableBlockOnError: this.opts.enableBlockOnError,
98
+ verbose: options.verbose,
99
+ enableLsp: this.opts.enableLsp,
100
+ enablePrettier: this.opts.enablePrettier,
101
+ vueDevtoolsApiUrl: options.vueDevtoolsApiUrl
102
+ });
103
+ this.process = proc;
104
+ return { url: `http://${options.hostname}:${options.port}`, processHandle: proc };
105
+ }
106
+ async stop() {
107
+ if (this.process) {
108
+ log.debug("Killing web process", { pid: this.process.pid });
109
+ this.process.kill("SIGTERM");
110
+ this.process = null;
111
+ }
112
+ }
113
+ async killOrphans() {
114
+ return killOrphanOpenCodeProcesses();
115
+ }
116
+ async listSessions(projectDir) {
117
+ const sessions = await this.api.getSessions(projectDir);
118
+ return sessions.filter((s) => {
119
+ if (s.title === "__chrome_mcp_warmup__") return false;
120
+ if (s.parentID) return false;
121
+ if (s.time?.archived) return false;
122
+ return true;
123
+ }).map((s) => toChatSession(s));
124
+ }
125
+ async createSession(projectDir, title) {
126
+ const session = await this.api.createSession(projectDir, void 0, title);
127
+ return toChatSession(session);
128
+ }
129
+ async deleteSession(sessionId) {
130
+ await this.api.deleteSession(sessionId);
131
+ }
132
+ buildSessionUrl(projectDir, sessionId) {
133
+ return this.api.buildSessionProxyUrl(projectDir, sessionId);
134
+ }
135
+ subscribeEvents(handler) {
136
+ const port = this.deps.getWebPort();
137
+ const url = `http://${this.config.hostname}:${port}/global/event`;
138
+ log.debug("Subscribing to provider event stream", { url });
139
+ let aborted = false;
140
+ let currentReq = null;
141
+ let retryTimer = null;
142
+ const cleanup = () => {
143
+ if (retryTimer) {
144
+ clearTimeout(retryTimer);
145
+ retryTimer = null;
146
+ }
147
+ currentReq?.destroy();
148
+ currentReq = null;
149
+ };
150
+ const scheduleReconnect = () => {
151
+ if (aborted) return;
152
+ cleanup();
153
+ retryTimer = setTimeout(connect, RETRY_DELAY);
154
+ };
155
+ const connect = () => {
156
+ if (aborted) return;
157
+ const req = http.get(url, (res) => {
158
+ res.setEncoding("utf-8");
159
+ let buffer = "";
160
+ res.on("data", (chunk) => {
161
+ buffer += chunk;
162
+ const lines = buffer.split("\n");
163
+ buffer = lines.pop() ?? "";
164
+ for (const line of lines) {
165
+ const trimmed = line.trim();
166
+ if (!trimmed.startsWith("data:")) continue;
167
+ const data = trimmed.slice(5).trim();
168
+ if (!data) continue;
169
+ try {
170
+ const message = JSON.parse(data);
171
+ const event = mapEvent(message.payload);
172
+ if (event) handler(event);
173
+ } catch {
174
+ }
175
+ }
176
+ });
177
+ res.on("end", scheduleReconnect);
178
+ res.on("error", scheduleReconnect);
179
+ });
180
+ req.on("error", scheduleReconnect);
181
+ currentReq = req;
182
+ };
183
+ connect();
184
+ return () => {
185
+ aborted = true;
186
+ cleanup();
187
+ };
188
+ }
189
+ }
190
+ function mapEvent(payload) {
191
+ if (!payload || typeof payload !== "object") return null;
192
+ const msg = payload;
193
+ const props = msg.properties;
194
+ if (!props) return null;
195
+ switch (msg.type) {
196
+ case "session.updated": {
197
+ const info = props.info;
198
+ if (!info?.id) return null;
199
+ return {
200
+ type: "session.updated",
201
+ session: {
202
+ id: info.id,
203
+ title: info.title ?? "",
204
+ createdAt: info.time?.created,
205
+ updatedAt: info.time?.updated,
206
+ archived: info.time?.archived !== void 0
207
+ }
208
+ };
209
+ }
210
+ case "session.status": {
211
+ const sessionId = props.sessionID;
212
+ if (!sessionId) return null;
213
+ const status = props.status?.type ?? "idle";
214
+ return { type: "session.status", sessionId, status };
215
+ }
216
+ case "message.updated": {
217
+ const info = props.info;
218
+ if (info?.role !== "assistant" || !info.sessionID) return null;
219
+ const thinking = typeof info.time?.completed !== "number";
220
+ return { type: "thinking", sessionId: info.sessionID, thinking };
221
+ }
222
+ case "message.part.delta": {
223
+ const sessionId = props.sessionID;
224
+ if (!sessionId) return null;
225
+ return { type: "thinking", sessionId, thinking: true };
226
+ }
227
+ default:
228
+ return null;
229
+ }
230
+ }
231
+ function toChatSession(s) {
232
+ return {
233
+ id: s.id,
234
+ title: s.title,
235
+ createdAt: s.time?.created,
236
+ updatedAt: s.time?.updated,
237
+ archived: s.time?.archived !== void 0,
238
+ parentId: s.parentID,
239
+ url: s.url
240
+ };
241
+ }
242
+ function resolveOpenCodeOptions(options) {
243
+ if (!options) return { ...DEFAULT_OPENCODE_PROVIDER_OPTIONS };
244
+ const po = options.providerOptions ?? {};
245
+ return {
246
+ ...DEFAULT_OPENCODE_PROVIDER_OPTIONS,
247
+ language: po.language ?? options.language,
248
+ settings: po.settings ?? options.settings,
249
+ logFiles: options.logFiles,
250
+ enableLsp: po.enableLsp ?? options.enableLsp,
251
+ enableBlockOnError: po.enableBlockOnError ?? options.enableBlockOnError,
252
+ enablePrettier: po.enablePrettier ?? options.enablePrettier
253
+ };
254
+ }
255
+ export {
256
+ DefaultWebProvider
257
+ };
package/es/system.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function checkOpenCodeInstalled(): Promise<boolean>;
2
+ export declare function getOpenCodeVersion(): Promise<string | null>;
3
+ export declare function killOrphanOpenCodeProcesses(): Promise<number>;