Replace OpenCode with SmartAgent runtime

This commit is contained in:
2026-05-14 13:15:48 +00:00
parent 08ed394737
commit 73cccc1fc2
28 changed files with 1636 additions and 1840 deletions
+3 -3
View File
@@ -10,12 +10,12 @@
"package": "pnpm run build && electron-builder --config electron-builder.yml"
},
"dependencies": {
"@git.zone/ide-opencode-bridge": "workspace:*",
"@git.zone/ide-protocol": "workspace:*",
"@git.zone/ide-server-installer": "workspace:*",
"@git.zone/ide-ssh": "workspace:*",
"electron": "^42.0.1",
"opencode-ai": "1.14.48"
"@push.rocks/smartagent": "^3.2.0",
"@push.rocks/smartai": "^2.3.0",
"electron": "^42.0.1"
},
"devDependencies": {
"electron-builder": "^26.8.1"
+11 -12
View File
@@ -6,20 +6,19 @@ contextBridge.exposeInMainWorld('gitZoneIde', {
connect: (input) => ipcRenderer.invoke('gitzone:connect', input),
addProject: (input) => ipcRenderer.invoke('gitzone:add-project', input),
openProject: (input) => ipcRenderer.invoke('gitzone:open-project', input),
openCode: {
health: (input) => ipcRenderer.invoke('gitzone:opencode-health', input),
sessions: (input) => ipcRenderer.invoke('gitzone:opencode-sessions', input),
createSession: (input) => ipcRenderer.invoke('gitzone:opencode-create-session', input),
messages: (input) => ipcRenderer.invoke('gitzone:opencode-messages', input),
prompt: (input) => ipcRenderer.invoke('gitzone:opencode-prompt', input),
abort: (input) => ipcRenderer.invoke('gitzone:opencode-abort', input),
respondToPermission: (input) => ipcRenderer.invoke('gitzone:opencode-respond-permission', input),
providers: (input) => ipcRenderer.invoke('gitzone:opencode-providers', input),
agents: (input) => ipcRenderer.invoke('gitzone:opencode-agents', input),
agent: {
sessions: (input) => ipcRenderer.invoke('gitzone:agent:sessions', input),
createSession: (input) => ipcRenderer.invoke('gitzone:agent:create-session', input),
messages: (input) => ipcRenderer.invoke('gitzone:agent:messages', input),
sessionStatus: (input) => ipcRenderer.invoke('gitzone:agent:session-status', input),
children: (input) => ipcRenderer.invoke('gitzone:agent:children', input),
prompt: (input) => ipcRenderer.invoke('gitzone:agent:prompt', input),
abort: (input) => ipcRenderer.invoke('gitzone:agent:abort', input),
respondToPermission: (input) => ipcRenderer.invoke('gitzone:agent:respond-permission', input),
onEvent: (callback) => {
const listener = (_event, payload) => callback(payload);
ipcRenderer.on('gitzone:opencode-event', listener);
return () => ipcRenderer.removeListener('gitzone:opencode-event', listener);
ipcRenderer.on('gitzone:agent:event', listener);
return () => ipcRenderer.removeListener('gitzone:agent:event', listener);
},
},
onConnectProgress: (callback) => {
@@ -0,0 +1,811 @@
import type { IIdeSshTarget } from '@git.zone/ide-protocol';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import * as plugins from './plugins.js';
export interface IAgentProjectContext {
instanceId: string;
title: string;
path: string;
runtimeRoot: string;
target: IIdeSshTarget;
batchMode: boolean;
}
export interface IAgentEventEnvelope {
instanceId: string;
event: IAgentEvent;
}
type TAgentStatus =
| { type: 'idle' }
| { type: 'busy'; message?: string }
| { type: 'error'; message: string };
interface IAgentEvent {
type: string;
data?: Record<string, unknown>;
}
interface IAgentMessage {
id: string;
role: 'user' | 'assistant' | 'tool';
text: string;
createdAt: string;
updatedAt?: string;
}
interface IAgentSessionDescriptor {
id: string;
title: string;
createdAt: string;
updatedAt: string;
}
interface IPendingPermission {
id: string;
key: string;
resolve: () => void;
reject: (error: Error) => void;
}
interface IAgentSession extends IAgentSessionDescriptor {
instanceId: string;
messages: IAgentMessage[];
status: TAgentStatus;
abortController?: AbortController;
pendingPermissions: Map<string, IPendingPermission>;
rememberedPermissions: Set<string>;
}
interface IPersistedAgentSession {
id: string;
title: string;
createdAt: string;
updatedAt: string;
messages: IAgentMessage[];
rememberedPermissions: string[];
}
interface IPersistedAgentSessionsFile {
version: 1;
project: {
title: string;
path: string;
target: {
hostAlias: string;
hostName?: string;
user?: string;
port?: number;
};
};
sessions: IPersistedAgentSession[];
}
interface IAgentSessionInput {
instanceId?: string;
sessionId?: string;
}
interface ICreateAgentSessionInput {
instanceId?: string;
title?: string;
}
interface IAgentPromptInput extends IAgentSessionInput {
text?: string;
}
interface IAgentPermissionResponseInput extends IAgentSessionInput {
permissionId?: string;
response?: 'once' | 'always' | 'reject';
remember?: boolean;
}
type TEmitAgentEvent = (payload: IAgentEventEnvelope) => void;
type TResolveProjectContext = (instanceId: string) => IAgentProjectContext | undefined;
interface IOpenAiMaxIdTokenInfo {
chatgptAccountId?: string;
expiresAt?: string;
rawJwt: string;
}
interface IOpenAiMaxTokenData {
accessToken: string;
refreshToken: string;
idToken: string;
idTokenInfo: IOpenAiMaxIdTokenInfo;
accountId?: string;
}
interface ILooseSmartAiModule {
getModel: (options: { provider: 'openai'; model: string; apiKey?: string; openAiMaxAuth?: IOpenAiMaxTokenData }) => unknown;
parseOpenAiMaxIdToken: (idToken: string) => IOpenAiMaxIdTokenInfo;
refreshOpenAiMaxTokenData: (tokenData: IOpenAiMaxTokenData) => Promise<IOpenAiMaxTokenData>;
}
interface ILooseZodBuilder {
default: (value: unknown) => ILooseZodBuilder;
describe: (description: string) => ILooseZodBuilder;
optional: () => ILooseZodBuilder;
}
interface ILooseSmartAgentModule {
runAgent: (options: {
model: unknown;
prompt: string;
system?: string;
messages?: Array<{ role: 'user' | 'assistant'; content: string }>;
tools?: Record<string, unknown>;
sessionId?: string;
maxSteps?: number;
cache?: 'auto' | false;
abort?: AbortSignal;
onToken?: (delta: string) => void;
onToolCall?: (toolName: string, toolInput: unknown) => void;
onToolResult?: (toolName: string, toolResult: unknown) => void;
}) => Promise<{ text?: string }>;
tool: <TInput extends object>(options: {
description: string;
inputSchema: unknown;
execute: (input: TInput) => Promise<string> | string;
}) => unknown;
truncateOutput: (text: string, options?: { maxLines?: number; maxBytes?: number }) => { content: string; truncated: boolean; notice?: string };
z: {
object: (shape: Record<string, unknown>) => unknown;
number: () => ILooseZodBuilder;
string: () => ILooseZodBuilder;
};
}
const smartAgentPackageName: string = '@push.rocks/smartagent';
const smartAiPackageName: string = '@push.rocks/smartai';
let smartAgentModulePromise: Promise<ILooseSmartAgentModule> | undefined;
let smartAiModulePromise: Promise<ILooseSmartAiModule> | undefined;
const loadSmartAgent = (): Promise<ILooseSmartAgentModule> => {
smartAgentModulePromise ??= import(smartAgentPackageName).then((module: unknown) => module as ILooseSmartAgentModule);
return smartAgentModulePromise;
};
const loadSmartAi = (): Promise<ILooseSmartAiModule> => {
smartAiModulePromise ??= import(smartAiPackageName).then((module: unknown) => module as ILooseSmartAiModule);
return smartAiModulePromise;
};
export class GitZoneAgentRuntime {
private readonly sessionsByInstance = new Map<string, Map<string, IAgentSession>>();
private readonly sessionLoadPromises = new Map<string, Promise<Map<string, IAgentSession>>>();
constructor(
private readonly emitEvent: TEmitAgentEvent,
private readonly resolveProjectContext: TResolveProjectContext,
private readonly persistenceRoot = path.join(os.homedir(), '.git.zone', 'ide', 'agent-sessions'),
) {}
async listSessions(input: IAgentSessionInput): Promise<IAgentSessionDescriptor[]> {
const instanceId = requireString(input.instanceId, 'Agent instance id');
return [...(await this.ensureInstanceSessions(instanceId)).values()].map(toSessionDescriptor);
}
async createSession(input: ICreateAgentSessionInput): Promise<IAgentSessionDescriptor> {
const instanceId = requireString(input.instanceId, 'Agent instance id');
await this.ensureInstanceSessions(instanceId);
const now = new Date().toISOString();
const session: IAgentSession = {
id: randomId('agent'),
instanceId,
title: trimOptional(input.title) ?? 'Git.Zone IDE Session',
createdAt: now,
updatedAt: now,
messages: [],
status: { type: 'idle' },
pendingPermissions: new Map(),
rememberedPermissions: new Set(),
};
this.sessionsByInstance.get(instanceId)!.set(session.id, session);
await this.persistInstanceSessions(instanceId);
this.emit(instanceId, 'session.created', { session: toSessionDescriptor(session), sessionId: session.id });
return toSessionDescriptor(session);
}
async getMessages(input: IAgentSessionInput & { limit?: number }): Promise<IAgentMessage[]> {
const session = await this.requireSession(input);
const limit = input.limit && Number.isInteger(input.limit) && input.limit > 0 ? input.limit : session.messages.length;
return session.messages.slice(-limit);
}
async getSessionStatus(input: IAgentSessionInput): Promise<Record<string, TAgentStatus>> {
const instanceId = requireString(input.instanceId, 'Agent instance id');
const result: Record<string, TAgentStatus> = {};
for (const session of (await this.ensureInstanceSessions(instanceId)).values()) {
result[session.id] = session.status;
}
return result;
}
getChildren(_input: IAgentSessionInput): unknown[] {
return [];
}
async prompt(input: IAgentPromptInput): Promise<IAgentMessage> {
const session = await this.requireSession(input);
const context = this.requireProjectContext(session.instanceId);
const text = requireString(input.text, 'Prompt text');
if (session.abortController) {
throw new Error('Agent session already has a running prompt.');
}
const userMessage = this.addMessage(session, 'user', text);
this.emit(session.instanceId, 'message.created', { sessionId: session.id, message: userMessage });
const assistantMessage = this.addMessage(session, 'assistant', '');
this.emit(session.instanceId, 'message.created', { sessionId: session.id, message: assistantMessage });
const abortController = new AbortController();
session.abortController = abortController;
this.setStatus(session, { type: 'busy', message: 'Running SmartAgent' });
try {
const smartagent = await loadSmartAgent();
const model = await this.createModel();
const result = await smartagent.runAgent({
model,
prompt: text,
system: createSystemPrompt(context),
messages: toModelMessages(session.messages.filter((message) => message.id !== userMessage.id && message.id !== assistantMessage.id)),
tools: this.createRemoteTools(smartagent, context, session),
sessionId: session.id,
maxSteps: 20,
cache: 'auto',
abort: abortController.signal,
onToken: (delta: string) => {
assistantMessage.text += delta;
assistantMessage.updatedAt = new Date().toISOString();
this.emit(session.instanceId, 'message.updated', { sessionId: session.id, message: assistantMessage });
},
onToolCall: (toolName: string, toolInput: unknown) => {
const toolMessage = this.addMessage(session, 'tool', `Tool: ${toolName}\n${formatJson(toolInput)}`);
this.emit(session.instanceId, 'tool.started', { sessionId: session.id, message: toolMessage, toolName, input: toolInput });
},
onToolResult: (toolName: string, toolResult: unknown) => {
const toolMessage = this.addMessage(session, 'tool', `Tool result: ${toolName}\n${formatJson(toolResult)}`);
this.emit(session.instanceId, 'tool.finished', { sessionId: session.id, message: toolMessage, toolName, output: toolResult });
},
});
assistantMessage.text = result.text || assistantMessage.text;
assistantMessage.updatedAt = new Date().toISOString();
session.updatedAt = assistantMessage.updatedAt;
this.emit(session.instanceId, 'message.updated', { sessionId: session.id, message: assistantMessage });
this.setStatus(session, { type: 'idle' });
return assistantMessage;
} catch (error) {
if (abortController.signal.aborted) {
assistantMessage.text = `${assistantMessage.text}\n\nStopped.`.trim();
this.setStatus(session, { type: 'idle' });
} else {
const message = error instanceof Error ? error.message : String(error);
assistantMessage.text = `${assistantMessage.text}\n\nAgent error: ${message}`.trim();
this.setStatus(session, { type: 'error', message });
}
assistantMessage.updatedAt = new Date().toISOString();
this.emit(session.instanceId, 'message.updated', { sessionId: session.id, message: assistantMessage });
throw error;
} finally {
session.abortController = undefined;
await this.persistInstanceSessions(session.instanceId);
}
}
async abort(input: IAgentSessionInput): Promise<{ aborted: boolean }> {
const session = await this.requireSession(input);
if (!session.abortController) {
return { aborted: false };
}
session.abortController.abort();
this.rejectPendingPermissions(session, new Error('Agent run was stopped.'));
return { aborted: true };
}
async respondToPermission(input: IAgentPermissionResponseInput): Promise<{ accepted: boolean }> {
const session = await this.requireSession(input);
const permissionId = requireString(input.permissionId, 'Permission id');
const pending = session.pendingPermissions.get(permissionId);
if (!pending) {
return { accepted: false };
}
session.pendingPermissions.delete(permissionId);
const response = input.response ?? 'reject';
if (response === 'reject') {
pending.reject(new Error('Permission rejected.'));
this.emit(session.instanceId, 'permission.replied', { sessionId: session.id, requestID: permissionId, response });
return { accepted: true };
}
if (response === 'always' || input.remember) {
session.rememberedPermissions.add(pending.key);
await this.persistInstanceSessions(session.instanceId);
}
pending.resolve();
this.emit(session.instanceId, 'permission.replied', { sessionId: session.id, requestID: permissionId, response });
return { accepted: true };
}
dispose(): void {
for (const instanceSessions of this.sessionsByInstance.values()) {
for (const session of instanceSessions.values()) {
session.abortController?.abort();
this.rejectPendingPermissions(session, new Error('Agent runtime is shutting down.'));
}
}
}
private async ensureInstanceSessions(instanceId: string): Promise<Map<string, IAgentSession>> {
const existing = this.sessionsByInstance.get(instanceId);
if (existing) return existing;
const existingLoad = this.sessionLoadPromises.get(instanceId);
if (existingLoad) return existingLoad;
const loadPromise = this.loadInstanceSessions(instanceId);
this.sessionLoadPromises.set(instanceId, loadPromise);
try {
const sessions = await loadPromise;
this.sessionsByInstance.set(instanceId, sessions);
return sessions;
} finally {
this.sessionLoadPromises.delete(instanceId);
}
}
private async requireSession(input: IAgentSessionInput): Promise<IAgentSession> {
const instanceId = requireString(input.instanceId, 'Agent instance id');
const sessionId = requireString(input.sessionId, 'Agent session id');
const session = (await this.ensureInstanceSessions(instanceId)).get(sessionId);
if (!session) {
throw new Error(`Agent session not found: ${sessionId}`);
}
return session;
}
private requireProjectContext(instanceId: string): IAgentProjectContext {
const context = this.resolveProjectContext(instanceId);
if (!context) {
throw new Error(`Project instance not found for agent runtime: ${instanceId}`);
}
return context;
}
private setStatus(session: IAgentSession, status: TAgentStatus): void {
session.status = status;
session.updatedAt = new Date().toISOString();
this.emit(session.instanceId, 'session.updated', { sessionId: session.id, status });
}
private addMessage(session: IAgentSession, role: IAgentMessage['role'], text: string): IAgentMessage {
const now = new Date().toISOString();
const message: IAgentMessage = {
id: randomId('message'),
role,
text,
createdAt: now,
updatedAt: now,
};
session.messages.push(message);
session.updatedAt = now;
return message;
}
private emit(instanceId: string, type: string, data?: Record<string, unknown>): void {
this.emitEvent({ instanceId, event: { type, data } });
}
private async loadInstanceSessions(instanceId: string): Promise<Map<string, IAgentSession>> {
const context = this.requireProjectContext(instanceId);
const sessions = new Map<string, IAgentSession>();
let parsed: IPersistedAgentSessionsFile;
try {
parsed = JSON.parse(await fs.readFile(this.persistenceFilePath(context), 'utf8')) as IPersistedAgentSessionsFile;
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') {
return sessions;
}
throw error;
}
if (!Array.isArray(parsed.sessions)) {
return sessions;
}
for (const persisted of parsed.sessions) {
const session = normalizePersistedSession(instanceId, persisted);
if (session) {
sessions.set(session.id, session);
}
}
return sessions;
}
private async persistInstanceSessions(instanceId: string): Promise<void> {
const context = this.requireProjectContext(instanceId);
const sessions = this.sessionsByInstance.get(instanceId);
if (!sessions) return;
const filePath = this.persistenceFilePath(context);
const payload: IPersistedAgentSessionsFile = {
version: 1,
project: {
title: context.title,
path: context.path,
target: {
hostAlias: context.target.hostAlias,
hostName: context.target.hostName,
user: context.target.user,
port: context.target.port,
},
},
sessions: [...sessions.values()].map(toPersistedSession),
};
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
await fs.writeFile(tempPath, `${JSON.stringify(payload, undefined, 2)}\n`, { mode: 0o600 });
await fs.rename(tempPath, filePath);
}
private persistenceFilePath(context: IAgentProjectContext): string {
return path.join(this.persistenceRoot, `${persistenceKeyForContext(context)}.json`);
}
private async createModel(): Promise<unknown> {
const smartai = await loadSmartAi();
const model = process.env.GITZONE_IDE_AGENT_MODEL || 'gpt-5.5';
const apiKey = process.env.OPENAI_API_KEY || process.env.OPENAI_TOKEN;
if (apiKey) {
return smartai.getModel({ provider: 'openai', model, apiKey });
}
const openAiMaxAuth = await loadOpenAiMaxAuth();
if (openAiMaxAuth) {
return smartai.getModel({ provider: 'openai', model, openAiMaxAuth });
}
throw new Error('No OpenAI credentials available. Set OPENAI_API_KEY/OPENAI_TOKEN or sign in with ChatGPT using Codex device auth.');
}
private createRemoteTools(smartagent: ILooseSmartAgentModule, context: IAgentProjectContext, session: IAgentSession): Record<string, unknown> {
return {
run_command: smartagent.tool({
description: 'Run a shell command on the remote SSH host inside the active project directory. Requires user permission.',
inputSchema: smartagent.z.object({
command: smartagent.z.string().describe('Shell command to run'),
timeoutMs: smartagent.z.number().optional().describe('Timeout in milliseconds'),
}),
execute: async ({ command, timeoutMs }: { command: string; timeoutMs?: number }) => {
await this.requirePermission(session, context, 'shell', 'Run remote command', { command });
const result = await runRemoteShellCommand(context, command, timeoutMs ?? 120000);
return truncateRemoteResult(result, smartagent);
},
}),
list_directory: smartagent.tool({
description: 'List files in a directory under the active remote project. Paths may be relative to the project root.',
inputSchema: smartagent.z.object({
path: smartagent.z.string().default('.').describe('Directory path'),
}),
execute: async ({ path: directoryPath }: { path: string }) => {
const result = await runRemoteNodeTool(context, listDirectoryScript, {
GITZONE_AGENT_PATH: directoryPath,
});
return truncateRemoteResult(result, smartagent);
},
}),
read_file: smartagent.tool({
description: 'Read a UTF-8 file under the active remote project. Paths may be relative to the project root.',
inputSchema: smartagent.z.object({
path: smartagent.z.string().describe('File path'),
startLine: smartagent.z.number().optional().describe('First line, 1-indexed'),
endLine: smartagent.z.number().optional().describe('Last line, 1-indexed'),
}),
execute: async ({ path: filePath, startLine, endLine }: { path: string; startLine?: number; endLine?: number }) => {
const result = await runRemoteNodeTool(context, readFileScript, {
GITZONE_AGENT_PATH: filePath,
GITZONE_AGENT_START_LINE: startLine ? String(startLine) : '',
GITZONE_AGENT_END_LINE: endLine ? String(endLine) : '',
});
return truncateRemoteResult(result, smartagent);
},
}),
write_file: smartagent.tool({
description: 'Write a UTF-8 file under the active remote project. Requires user permission.',
inputSchema: smartagent.z.object({
path: smartagent.z.string().describe('File path'),
content: smartagent.z.string().describe('Complete file content to write'),
}),
execute: async ({ path: filePath, content }: { path: string; content: string }) => {
await this.requirePermission(session, context, 'write', 'Write remote file', { path: filePath, bytes: Buffer.byteLength(content) });
const result = await runRemoteNodeTool(context, writeFileScript, {
GITZONE_AGENT_PATH: filePath,
GITZONE_AGENT_CONTENT_B64: Buffer.from(content, 'utf8').toString('base64'),
});
return truncateRemoteResult(result, smartagent);
},
}),
};
}
private async requirePermission(
session: IAgentSession,
_context: IAgentProjectContext,
type: string,
title: string,
metadata: Record<string, unknown>,
): Promise<void> {
const key = `${type}:${JSON.stringify(metadata)}`;
if (session.rememberedPermissions.has(key)) {
return;
}
const id = randomId('permission');
await new Promise<void>((resolve, reject) => {
session.pendingPermissions.set(id, { id, key, resolve, reject });
this.emit(session.instanceId, 'permission.asked', {
requestID: id,
sessionId: session.id,
title,
type,
metadata,
});
});
}
private rejectPendingPermissions(session: IAgentSession, error: Error): void {
for (const pending of session.pendingPermissions.values()) {
pending.reject(error);
this.emit(session.instanceId, 'permission.replied', { sessionId: session.id, requestID: pending.id, response: 'reject' });
}
session.pendingPermissions.clear();
}
}
const toSessionDescriptor = (session: IAgentSession): IAgentSessionDescriptor => ({
id: session.id,
title: session.title,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
});
const toPersistedSession = (session: IAgentSession): IPersistedAgentSession => ({
id: session.id,
title: session.title,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
messages: session.messages,
rememberedPermissions: [...session.rememberedPermissions],
});
const normalizePersistedSession = (instanceId: string, persisted: IPersistedAgentSession): IAgentSession | undefined => {
if (!persisted || typeof persisted !== 'object') return undefined;
const id = stringValue(persisted.id);
if (!id) return undefined;
const now = new Date().toISOString();
return {
id,
instanceId,
title: stringValue(persisted.title) ?? 'Git.Zone IDE Session',
createdAt: stringValue(persisted.createdAt) ?? now,
updatedAt: stringValue(persisted.updatedAt) ?? now,
messages: Array.isArray(persisted.messages)
? persisted.messages.map(normalizePersistedMessage).filter((message): message is IAgentMessage => !!message)
: [],
status: { type: 'idle' },
pendingPermissions: new Map(),
rememberedPermissions: new Set(Array.isArray(persisted.rememberedPermissions) ? persisted.rememberedPermissions.filter((item) => typeof item === 'string') : []),
};
};
const normalizePersistedMessage = (message: IAgentMessage): IAgentMessage | undefined => {
if (!message || typeof message !== 'object') return undefined;
const id = stringValue(message.id);
const role = message.role;
const text = typeof message.text === 'string' ? message.text : undefined;
const createdAt = stringValue(message.createdAt);
if (!id || !isAgentMessageRole(role) || text === undefined || !createdAt) return undefined;
return {
id,
role,
text,
createdAt,
updatedAt: stringValue(message.updatedAt),
};
};
const isAgentMessageRole = (role: unknown): role is IAgentMessage['role'] => {
return role === 'user' || role === 'assistant' || role === 'tool';
};
const persistenceKeyForContext = (context: IAgentProjectContext): string => {
return plugins.crypto
.createHash('sha256')
.update(JSON.stringify({
hostAlias: context.target.hostAlias,
hostName: context.target.hostName,
path: context.path,
port: context.target.port,
user: context.target.user,
}))
.digest('hex');
};
const toModelMessages = (messages: IAgentMessage[]): Array<{ role: 'user' | 'assistant'; content: string }> => {
return messages
.filter((message) => (message.role === 'user' || message.role === 'assistant') && message.text.trim())
.map((message) => ({ role: message.role as 'user' | 'assistant', content: message.text }));
};
const createSystemPrompt = (context: IAgentProjectContext) => [
'You are the Git.Zone IDE SmartAgent for a remote SSH workspace.',
`Project title: ${context.title}`,
`Remote project root: ${context.path}`,
'Use the provided remote tools for project inspection and changes. Do not assume local filesystem access is the remote workspace.',
'Prefer small, focused changes. Ask for clarification if the user request is ambiguous or risky.',
].join('\n');
const runRemoteShellCommand = async (context: IAgentProjectContext, command: string, timeoutMs: number) => {
const remoteCommand = [
'set -euo pipefail',
`cd ${plugins.ideServerInstaller.quoteRemotePath(context.path)}`,
command,
].join('\n');
return plugins.ideSsh.runSshCommand(context.target, remoteCommand, {
batchMode: context.batchMode,
timeoutMs,
});
};
const runRemoteNodeTool = async (
context: IAgentProjectContext,
script: string,
env: Record<string, string>,
timeoutMs = 60000,
) => {
const nodePath = plugins.ideServerInstaller.joinRemotePath(context.runtimeRoot, 'node/bin/node');
const nodeLibPath = plugins.ideServerInstaller.joinRemotePath(context.runtimeRoot, 'node/lib');
const remoteCommand = [
'set -euo pipefail',
`export LD_LIBRARY_PATH=${plugins.ideServerInstaller.quoteRemotePath(nodeLibPath)}:\${LD_LIBRARY_PATH:-}`,
`export GITZONE_AGENT_ROOT=${plugins.ideServerInstaller.quoteRemotePath(context.path)}`,
...Object.entries(env).map(([key, value]) => `export ${key}=${plugins.ideServerInstaller.quoteShellArg(value)}`),
`${plugins.ideServerInstaller.quoteRemotePath(nodePath)} <<'GITZONE_IDE_AGENT_NODE'`,
script,
'GITZONE_IDE_AGENT_NODE',
].join('\n');
return plugins.ideSsh.runSshCommand(context.target, remoteCommand, {
batchMode: context.batchMode,
timeoutMs,
});
};
const truncateRemoteResult = (result: plugins.ideSsh.ISshRunResult, smartagent: ILooseSmartAgentModule): string => {
const output = result.exitCode === 0
? result.stdout
: `Exit code: ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`;
return smartagent.truncateOutput(output).content;
};
const remotePathPrelude = `
const fs = require('fs');
const path = require('path');
const root = path.resolve(process.env.GITZONE_AGENT_ROOT || '.');
const requested = process.env.GITZONE_AGENT_PATH || '.';
const target = path.isAbsolute(requested) ? path.resolve(requested) : path.resolve(root, requested);
if (target !== root && !target.startsWith(root + path.sep)) {
throw new Error('Path is outside the active project root: ' + requested);
}
`;
const listDirectoryScript = `${remotePathPrelude}
const entries = fs.readdirSync(target, { withFileTypes: true })
.map((entry) => entry.name + (entry.isDirectory() ? '/' : ''))
.sort();
console.log(entries.join('\n'));
`;
const readFileScript = `${remotePathPrelude}
const text = fs.readFileSync(target, 'utf8');
const startLine = Number.parseInt(process.env.GITZONE_AGENT_START_LINE || '', 10);
const endLine = Number.parseInt(process.env.GITZONE_AGENT_END_LINE || '', 10);
if (Number.isFinite(startLine) || Number.isFinite(endLine)) {
const lines = text.split('\n');
const start = Number.isFinite(startLine) && startLine > 0 ? startLine - 1 : 0;
const end = Number.isFinite(endLine) && endLine > 0 ? endLine : lines.length;
console.log(lines.slice(start, end).join('\n'));
} else {
console.log(text);
}
`;
const writeFileScript = `${remotePathPrelude}
const content = Buffer.from(process.env.GITZONE_AGENT_CONTENT_B64 || '', 'base64').toString('utf8');
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content, 'utf8');
console.log('Written ' + Buffer.byteLength(content, 'utf8') + ' bytes to ' + path.relative(root, target));
`;
const loadOpenAiMaxAuth = async (): Promise<IOpenAiMaxTokenData | undefined> => {
const smartai = await loadSmartAi();
const localAuthPath = path.join(os.homedir(), '.git.zone', 'ide', 'openai-max-auth.json');
const codexAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
return await loadOpenAiMaxAuthFromPath(smartai, localAuthPath, true) ?? await loadOpenAiMaxAuthFromPath(smartai, codexAuthPath, false);
};
const loadOpenAiMaxAuthFromPath = async (smartai: ILooseSmartAiModule, filePath: string, allowRefreshWrite: boolean): Promise<IOpenAiMaxTokenData | undefined> => {
try {
const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as Record<string, unknown>;
const tokenData = normalizeOpenAiMaxAuth(smartai, parsed);
if (!tokenData) return undefined;
if (allowRefreshWrite && shouldRefreshToken(tokenData)) {
const refreshed = await smartai.refreshOpenAiMaxTokenData(tokenData);
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
await fs.writeFile(filePath, `${JSON.stringify(refreshed, undefined, 2)}\n`, { mode: 0o600 });
return refreshed;
}
return tokenData;
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') return undefined;
return undefined;
}
};
const normalizeOpenAiMaxAuth = (smartai: ILooseSmartAiModule, input: Record<string, unknown>): IOpenAiMaxTokenData | undefined => {
const tokens = (input.tokens && typeof input.tokens === 'object' ? input.tokens : input) as Record<string, unknown>;
const accessToken = stringValue(tokens.accessToken) ?? stringValue(tokens.access_token);
const refreshToken = stringValue(tokens.refreshToken) ?? stringValue(tokens.refresh_token);
const idToken = stringValue(tokens.idToken) ?? stringValue(tokens.id_token) ?? stringValue((tokens.idTokenInfo as Record<string, unknown> | undefined)?.rawJwt);
if (!accessToken || !refreshToken || !idToken) return undefined;
const idTokenInfo = smartai.parseOpenAiMaxIdToken(idToken);
return {
accessToken,
refreshToken,
idToken,
idTokenInfo,
accountId: stringValue(tokens.accountId) ?? stringValue(tokens.account_id) ?? idTokenInfo.chatgptAccountId,
};
};
const shouldRefreshToken = (tokenData: IOpenAiMaxTokenData): boolean => {
if (!tokenData.idTokenInfo.expiresAt) return false;
return Date.parse(tokenData.idTokenInfo.expiresAt) - Date.now() < 5 * 60 * 1000;
};
const requireString = (value: string | undefined, label: string): string => {
const trimmed = trimOptional(value);
if (!trimmed) {
throw new Error(`${label} is required.`);
}
return trimmed;
};
const trimOptional = (value: string | undefined): string | undefined => {
const trimmed = value?.trim();
return trimmed || undefined;
};
const stringValue = (value: unknown): string | undefined => {
return typeof value === 'string' && value ? value : undefined;
};
const randomId = (prefix: string): string => `${prefix}-${plugins.crypto.randomBytes(12).toString('hex')}`;
const formatJson = (value: unknown): string => {
if (typeof value === 'string') return value;
try {
return JSON.stringify(value, undefined, 2);
} catch {
return String(value);
}
};
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -1,8 +1,6 @@
import * as crypto from 'node:crypto';
import * as http from 'node:http';
import * as electron from 'electron';
import * as ideOpenCodeBridge from '@git.zone/ide-opencode-bridge';
import * as ideServerInstaller from '@git.zone/ide-server-installer';
import * as ideSsh from '@git.zone/ide-ssh';
export { crypto, electron, http, ideOpenCodeBridge, ideServerInstaller, ideSsh };
export { crypto, electron, ideServerInstaller, ideSsh };
+2 -8
View File
@@ -4,18 +4,12 @@ Git.Zone IDE is split into a local Electron shell and a remote Theia workspace s
## Local Electron Shell
The Electron app is intentionally small. It owns SSH host selection, remote bootstrap execution, local SSH tunnel lifecycle, and workspace windows. It does not implement remote filesystem, terminal, Git, or OpenCode behavior itself.
The Electron app is intentionally small. It owns SSH host selection, remote bootstrap execution, local SSH tunnel lifecycle, and workspace windows. It does not implement remote filesystem, terminal, or Git behavior itself.
## Remote Theia Server
The remote server is a Theia browser application installed under `~/.git.zone/ide-server/<version>`. It runs on the SSH host, bound to `127.0.0.1`, and is accessed only through an SSH local port forward.
## OpenCode Integration
The Electron shell starts a local `opencode serve` runtime for each opened project. The native shell owns OpenCode sessions, messages, permissions, and chat UI. Git.Zone writes OpenCode tool overrides into that local runtime so shell, file, search, and patch operations are forwarded over SSH and execute in the selected remote workspace.
Theia does not host OpenCode chat or OpenCode sessions. Any future Theia-facing tool should be an intellisense/context provider for editor and language-server state, not another OpenCode transport.
## Execution Boundary
Files, terminal commands, Git, language servers, builds, tests, Git.Zone commands, and bridged OpenCode code tools all run on the remote SSH host. The local machine displays UI, runs the local OpenCode provider process, and maintains SSH transport.
Files, terminal commands, Git, language servers, builds, tests, and Git.Zone commands all run on the remote SSH host. The local machine displays UI and maintains SSH transport.
+2 -2
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Remote-first Git.Zone IDE based on Theia, Electron, SSH, and OpenCode server.",
"description": "Remote-first Git.Zone IDE based on Theia, Electron, and SSH.",
"scripts": {
"build": "pnpm -r --filter '@git.zone/ide-protocol' --filter '@git.zone/ide-ssh' --filter '@git.zone/ide-server-installer' --filter '@git.zone/ide-opencode-bridge' --filter '@git.zone/ide-extension-*' --filter '@git.zone/ide-electron-shell' run build",
"build": "pnpm -r --filter '@git.zone/ide-protocol' --filter '@git.zone/ide-ssh' --filter '@git.zone/ide-server-installer' --filter '@git.zone/ide-extension-*' --filter '@git.zone/ide-electron-shell' run build",
"build:theia": "pnpm --filter '@git.zone/ide-remote-theia' run build",
"start:electron": "pnpm --filter '@git.zone/ide-electron-shell' run start",
"start:remote": "pnpm --filter '@git.zone/ide-remote-theia' run start",
-15
View File
@@ -1,15 +0,0 @@
{
"name": "@git.zone/ide-opencode-bridge",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"files": [
"ts/**/*",
"dist_ts/**/*"
]
}
-512
View File
@@ -1,512 +0,0 @@
import * as plugins from './plugins.js';
export interface IOpenCodeClientOptions {
baseUrl: string;
username?: string;
password?: string;
fetch?: typeof fetch;
}
export interface IOpenCodeMessagePart {
type: string;
[key: string]: unknown;
}
export interface IOpenCodePromptBody {
messageID?: string;
model?: {
providerID: string;
modelID: string;
};
agent?: string;
noReply?: boolean;
system?: string;
tools?: Record<string, boolean>;
parts: IOpenCodeMessagePart[];
}
export interface IOpenCodeCommandBody {
messageID?: string;
agent?: string;
model?: {
providerID: string;
modelID: string;
};
command: string;
arguments?: string;
}
export interface IOpenCodeShellBody {
agent: string;
model?: {
providerID: string;
modelID: string;
};
command: string;
}
export interface IOpenCodeEvent {
type: string;
id?: string;
retry?: number;
data?: unknown;
raw: string;
}
export interface IRendererOpenCodeEvent {
type: string;
id?: string;
retry?: number;
data?: unknown;
}
export class OpenCodeHttpError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly body: string,
) {
super(message);
}
}
export class OpenCodeServerClient {
private readonly baseUrl: string;
private readonly fetchImpl: typeof fetch;
private readonly authorizationHeader?: string;
constructor(options: IOpenCodeClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/+$/g, '');
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
if (options.username || options.password) {
const username = options.username ?? 'opencode';
const password = options.password ?? '';
this.authorizationHeader = `Basic ${plugins.Buffer.from(`${username}:${password}`).toString('base64')}`;
}
}
async health<T = unknown>() {
return this.request<T>('/global/health');
}
async projects<T = unknown>() {
return this.request<T>('/project');
}
async currentProject<T = unknown>() {
return this.request<T>('/project/current');
}
async path<T = unknown>() {
return this.request<T>('/path');
}
async vcs<T = unknown>() {
return this.request<T>('/vcs');
}
async config<T = unknown>() {
return this.request<T>('/config');
}
async providers<T = unknown>() {
return this.request<T>('/provider');
}
async agents<T = unknown>() {
return this.request<T>('/agent');
}
async commands<T = unknown>() {
return this.request<T>('/command');
}
async sessions<T = unknown>() {
return this.request<T>('/session');
}
async createSession<T = unknown>(body: { parentID?: string; title?: string } = {}) {
return this.request<T>('/session', { method: 'POST', body });
}
async session<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}`);
}
async updateSession<T = unknown>(id: string, body: { title?: string }) {
return this.request<T>(`/session/${encodeURIComponent(id)}`, { method: 'PATCH', body });
}
async deleteSession<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}`, { method: 'DELETE' });
}
async sessionStatus<T = unknown>() {
return this.request<T>('/session/status');
}
async children<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}/children`);
}
async todo<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}/todo`);
}
async messages<T = unknown>(id: string, limit?: number) {
const query = limit ? `?limit=${encodeURIComponent(`${limit}`)}` : '';
return this.request<T>(`/session/${encodeURIComponent(id)}/message${query}`);
}
async message<T = unknown>(id: string, messageID: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}/message/${encodeURIComponent(messageID)}`);
}
async prompt<T = unknown>(id: string, body: IOpenCodePromptBody) {
return this.request<T>(`/session/${encodeURIComponent(id)}/message`, { method: 'POST', body });
}
async promptAsync(id: string, body: IOpenCodePromptBody) {
return this.request<void>(`/session/${encodeURIComponent(id)}/prompt_async`, { method: 'POST', body });
}
async command<T = unknown>(id: string, body: IOpenCodeCommandBody) {
return this.request<T>(`/session/${encodeURIComponent(id)}/command`, { method: 'POST', body });
}
async shell<T = unknown>(id: string, body: IOpenCodeShellBody) {
return this.request<T>(`/session/${encodeURIComponent(id)}/shell`, { method: 'POST', body });
}
async abort<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}/abort`, { method: 'POST' });
}
async diff<T = unknown>(id: string, messageID?: string) {
const query = messageID ? `?messageID=${encodeURIComponent(messageID)}` : '';
return this.request<T>(`/session/${encodeURIComponent(id)}/diff${query}`);
}
async revert<T = unknown>(id: string, body: { messageID: string; partID?: string }) {
return this.request<T>(`/session/${encodeURIComponent(id)}/revert`, { method: 'POST', body });
}
async unrevert<T = unknown>(id: string) {
return this.request<T>(`/session/${encodeURIComponent(id)}/unrevert`, { method: 'POST' });
}
async respondToPermission<T = unknown>(
sessionID: string,
permissionID: string,
body: { response: string; remember?: boolean },
) {
return this.request<T>(
`/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(permissionID)}`,
{ method: 'POST', body },
);
}
async findFiles<T = unknown>(query: { query: string; type?: 'file' | 'directory'; directory?: string; limit?: number }) {
return this.request<T>(`/find/file?${new URLSearchParams(stringifyQuery(query))}`);
}
async findText<T = unknown>(query: { pattern: string }) {
return this.request<T>(`/find?${new URLSearchParams(stringifyQuery(query))}`);
}
async fileContent<T = unknown>(path: string) {
return this.request<T>(`/file/content?${new URLSearchParams({ path })}`);
}
async fileStatus<T = unknown>() {
return this.request<T>('/file/status');
}
async *events(signal?: AbortSignal): AsyncGenerator<IOpenCodeEvent> {
const response = await this.fetchImpl(`${this.baseUrl}/event`, {
headers: this.createHeaders(),
signal,
});
if (!response.ok) {
throw await this.createHttpError(response, '/event');
}
if (!response.body) {
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split(/\n\n/);
buffer = parts.pop() ?? '';
for (const part of parts) {
const event = parseServerSentEvent(part);
if (event) {
yield event;
}
}
}
const finalEvent = parseServerSentEvent(buffer);
if (finalEvent) {
yield finalEvent;
}
}
private async request<T>(path: string, options: { method?: string; body?: unknown } = {}) {
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
method: options.method ?? 'GET',
headers: this.createHeaders(options.body !== undefined),
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
if (!response.ok) {
throw await this.createHttpError(response, path);
}
if (response.status === 204) {
return undefined as T;
}
const text = await response.text();
if (!text) {
return undefined as T;
}
return JSON.parse(text) as T;
}
private createHeaders(hasJsonBody = false) {
const headers: Record<string, string> = {};
if (hasJsonBody) {
headers['content-type'] = 'application/json';
}
if (this.authorizationHeader) {
headers.authorization = this.authorizationHeader;
}
return headers;
}
private async createHttpError(response: Response, path: string) {
const body = await response.text();
return new OpenCodeHttpError(`OpenCode request failed: ${response.status} ${path}`, response.status, body);
}
}
export const openCodeBridgeToolNames = [
'bash',
'read',
'write',
'edit',
'grep',
'glob',
'apply_patch',
] as const;
export type TOpenCodeBridgeToolName = typeof openCodeBridgeToolNames[number];
export interface IOpenCodeBridgeConfigRenderOptions {
bridgeUrlEnvName?: string;
bridgeTokenEnvName?: string;
}
export const renderOpenCodeBridgeConfigContent = () => `${JSON.stringify({
$schema: 'https://opencode.ai/config.json',
autoupdate: false,
snapshot: false,
permission: {
lsp: 'deny',
skill: 'deny',
},
}, undefined, 2)}\n`;
export const renderOpenCodeBridgeToolFiles = (options: IOpenCodeBridgeConfigRenderOptions = {}) => {
const files: Record<string, string> = {};
for (const toolName of openCodeBridgeToolNames) {
files[`tools/${toolName}.js`] = renderOpenCodeBridgeToolFile(toolName, options);
}
return files;
};
export const renderOpenCodeBridgeToolFile = (
toolName: TOpenCodeBridgeToolName,
options: IOpenCodeBridgeConfigRenderOptions = {},
) => {
const bridgeUrlEnvName = options.bridgeUrlEnvName ?? 'GITZONE_IDE_TOOL_BRIDGE_URL';
const bridgeTokenEnvName = options.bridgeTokenEnvName ?? 'GITZONE_IDE_TOOL_BRIDGE_TOKEN';
const definition = openCodeBridgeToolDefinitions[toolName];
return `import { tool } from "@opencode-ai/plugin";
const bridgeUrlEnvName = ${JSON.stringify(bridgeUrlEnvName)};
const bridgeTokenEnvName = ${JSON.stringify(bridgeTokenEnvName)};
const forwardTool = async (toolName, args, context) => {
const bridgeUrl = process.env[bridgeUrlEnvName];
const bridgeToken = process.env[bridgeTokenEnvName];
if (!bridgeUrl || !bridgeToken) {
throw new Error("Git.Zone OpenCode tool bridge is not configured.");
}
const response = await fetch(new URL("/tool/" + encodeURIComponent(toolName), bridgeUrl), {
method: "POST",
headers: {
"authorization": "Bearer " + bridgeToken,
"content-type": "application/json",
},
body: JSON.stringify({
args,
context: {
sessionID: context.sessionID,
messageID: context.messageID,
agent: context.agent,
directory: context.directory,
worktree: context.worktree,
},
}),
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(responseText || ("Git.Zone OpenCode tool bridge failed with HTTP " + response.status));
}
return responseText ? JSON.parse(responseText) : "";
};
export default tool({
description: ${JSON.stringify(definition.description)},
args: {
${definition.args.map((arg) => ` ${arg.name}: ${arg.schema},`).join('\n')}
},
async execute(args, context) {
return forwardTool(${JSON.stringify(toolName)}, args, context);
},
});
`;
};
const openCodeBridgeToolDefinitions: Record<TOpenCodeBridgeToolName, {
description: string;
args: Array<{ name: string; schema: string }>;
}> = {
bash: {
description: 'Execute a shell command on the selected remote Git.Zone workspace over SSH. The command runs on the remote host, not on the local proxy workspace. Use workdir for a remote working directory when needed.',
args: [
{ name: 'command', schema: 'tool.schema.string().describe("The command to execute on the remote host")' },
{ name: 'timeout', schema: 'tool.schema.number().optional().describe("Optional timeout in milliseconds")' },
{ name: 'workdir', schema: 'tool.schema.string().optional().describe("Remote working directory. Defaults to the selected remote project path.")' },
{ name: 'description', schema: 'tool.schema.string().describe("Clear, concise description of what this command does in 5-10 words")' },
],
},
read: {
description: 'Read a file or directory from the selected remote Git.Zone workspace. Paths may be remote absolute paths or paths relative to the selected remote project.',
args: [
{ name: 'filePath', schema: 'tool.schema.string().describe("Remote file or directory path to read")' },
{ name: 'offset', schema: 'tool.schema.number().optional().describe("The line number to start reading from (1-indexed)")' },
{ name: 'limit', schema: 'tool.schema.number().optional().describe("The maximum number of lines to read")' },
],
},
write: {
description: 'Create or overwrite a file in the selected remote Git.Zone workspace.',
args: [
{ name: 'filePath', schema: 'tool.schema.string().describe("Remote file path to write")' },
{ name: 'content', schema: 'tool.schema.string().describe("The content to write to the remote file")' },
],
},
edit: {
description: 'Modify an existing remote file using exact string replacement.',
args: [
{ name: 'filePath', schema: 'tool.schema.string().describe("Remote file path to modify")' },
{ name: 'oldString', schema: 'tool.schema.string().describe("The text to replace")' },
{ name: 'newString', schema: 'tool.schema.string().describe("The replacement text")' },
{ name: 'replaceAll', schema: 'tool.schema.boolean().optional().describe("Replace all occurrences of oldString")' },
],
},
grep: {
description: 'Search remote file contents in the selected Git.Zone workspace using ripgrep on the remote host.',
args: [
{ name: 'pattern', schema: 'tool.schema.string().describe("The regex pattern to search for in file contents")' },
{ name: 'path', schema: 'tool.schema.string().optional().describe("Remote directory or file to search. Defaults to the selected remote project path.")' },
{ name: 'include', schema: 'tool.schema.string().optional().describe("File glob to include, for example *.ts or *.{ts,tsx}")' },
],
},
glob: {
description: 'Find remote files by glob pattern in the selected Git.Zone workspace using ripgrep on the remote host.',
args: [
{ name: 'pattern', schema: 'tool.schema.string().describe("The glob pattern to match remote files against")' },
{ name: 'path', schema: 'tool.schema.string().optional().describe("Remote directory to search. Defaults to the selected remote project path.")' },
],
},
apply_patch: {
description: 'Apply a stripped-down file patch to the selected remote Git.Zone workspace. Patch paths are relative to the selected remote project unless absolute remote paths are supplied.',
args: [
{ name: 'patchText', schema: 'tool.schema.string().describe("The full patch text that describes all remote file changes to make")' },
],
},
};
export const parseServerSentEvent = (raw: string): IOpenCodeEvent | undefined => {
const trimmed = raw.trim();
if (!trimmed) {
return undefined;
}
const dataLines: string[] = [];
let type = 'message';
let id: string | undefined;
let retry: number | undefined;
for (const line of trimmed.split(/\r?\n/)) {
if (line.startsWith(':')) {
continue;
}
const separator = line.indexOf(':');
const field = separator === -1 ? line : line.slice(0, separator);
const value = separator === -1 ? '' : line.slice(separator + 1).replace(/^ /, '');
if (field === 'event') {
type = value;
} else if (field === 'data') {
dataLines.push(value);
} else if (field === 'id') {
id = value;
} else if (field === 'retry') {
const retryNumber = Number(value);
if (Number.isFinite(retryNumber)) {
retry = retryNumber;
}
}
}
const dataText = dataLines.join('\n');
const data = parseJsonIfPossible(dataText);
if (type === 'message' && data && typeof data === 'object' && 'type' in data) {
type = String((data as { type: unknown }).type);
}
return { type, id, retry, data, raw };
};
export const sanitizeOpenCodeEventForRenderer = (event: IOpenCodeEvent): IRendererOpenCodeEvent => ({
type: event.type,
id: event.id,
retry: event.retry,
data: event.data,
});
const parseJsonIfPossible = (value: string) => {
if (!value) {
return undefined;
}
try {
return JSON.parse(value) as unknown;
} catch {
return value;
}
};
const stringifyQuery = (query: Record<string, string | number | boolean | undefined>) => {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(query)) {
if (value !== undefined) {
result[key] = String(value);
}
}
return result;
};
-3
View File
@@ -1,3 +0,0 @@
import { Buffer } from 'node:buffer';
export { Buffer };
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist_ts",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["ts/**/*.ts"]
}
-10
View File
@@ -30,14 +30,6 @@ export interface IRemoteServerPaths {
manifestPath: string;
}
export interface IRemoteOpenCodeDescriptor {
baseUrl: string;
port: number;
username: string;
password: string;
status: TRemoteProcessStatus;
}
export interface IRemoteTheiaDescriptor {
baseUrl: string;
localPort: number;
@@ -52,7 +44,6 @@ export interface IRemoteSessionDescriptor {
serverVersion: string;
createdAt: string;
theia: IRemoteTheiaDescriptor;
opencode?: IRemoteOpenCodeDescriptor;
}
export interface IRemoteProbeResult {
@@ -65,7 +56,6 @@ export interface IRemoteProbeResult {
nodeVersion?: string;
pnpmVersion?: string;
gitVersion?: string;
opencodeVersion?: string;
errors: string[];
}
-227
View File
@@ -25,9 +25,6 @@ export interface IRemoteServerBootstrapOptions {
serverVersion: string;
workspacePath: string;
theiaPort: number;
opencodePort: number;
opencodeUsername: string;
opencodePassword: string;
installRoot?: string;
nodeEnv?: string;
theiaColorTheme?: string;
@@ -78,14 +75,6 @@ export interface IRemoteProjectUpsertOptions extends IRemoteProjectRegistryOptio
title?: string;
}
export interface IRemoteOpenCodeToolCommandOptions {
runtimeRoot: string;
workspacePath: string;
toolName: string;
nodePath?: string;
rgPath?: string;
}
export const defaultIdeDataRoot = '~/.git.zone/ide';
export const defaultInstallRoot = '~/.git.zone/ide/server';
export const remoteEphemeralRuntimeMarkerFileName = '.gitzone-runtime-sha256';
@@ -147,10 +136,6 @@ export const createRemoteBootstrapCommand = (options: IRemoteServerBootstrapOpti
const logFile = joinRemotePath(plan.paths.logsDir, `theia-${options.theiaPort}.log`);
const env = {
GITZONE_IDE_WORKSPACE: options.workspacePath,
GITZONE_IDE_OPENCODE_PORT: `${options.opencodePort}`,
GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART: '1',
OPENCODE_SERVER_USERNAME: options.opencodeUsername,
OPENCODE_SERVER_PASSWORD: options.opencodePassword,
NODE_ENV: options.nodeEnv ?? 'production',
} satisfies Record<string, string>;
@@ -167,7 +152,6 @@ export const createRemoteBootstrapCommand = (options: IRemoteServerBootstrapOpti
}),
`nohup pnpm --dir ${quoteRemotePath(appDir)} start --hostname 127.0.0.1 --port ${options.theiaPort} ${quoteRemotePath(options.workspacePath)} > ${quoteRemotePath(logFile)} 2>&1 < /dev/null &`,
`printf 'theiaPort=%s\\n' ${options.theiaPort}`,
`printf 'opencodePort=%s\\n' ${options.opencodePort}`,
].join('\n');
};
@@ -196,10 +180,6 @@ export const createRemoteEphemeralBootstrapCommand = (options: IRemoteEphemeralB
].join('\n');
const env = {
GITZONE_IDE_WORKSPACE: options.workspacePath,
GITZONE_IDE_OPENCODE_PORT: `${options.opencodePort}`,
GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART: '1',
OPENCODE_SERVER_USERNAME: options.opencodeUsername,
OPENCODE_SERVER_PASSWORD: options.opencodePassword,
NODE_ENV: options.nodeEnv ?? 'production',
} satisfies Record<string, string>;
@@ -222,7 +202,6 @@ export const createRemoteEphemeralBootstrapCommand = (options: IRemoteEphemeralB
`nohup ${quoteRemotePath(nodePath)} ${quoteRemotePath(joinRemotePath(appDir, 'lib/backend/main.js'))} --hostname 127.0.0.1 --port ${options.theiaPort} ${quoteRemotePath(options.workspacePath)} > ${quoteRemotePath(logFile)} 2>&1 < /dev/null &`,
`printf 'runtimeRoot=%s\n' ${quoteShellArg(options.runtimeRoot)}`,
`printf 'theiaPort=%s\n' ${options.theiaPort}`,
`printf 'opencodePort=%s\n' ${options.opencodePort}`,
].join('\n');
};
@@ -313,20 +292,6 @@ export const createRemoteEphemeralPortAllocationCommand = (options: IRemoteEphem
].join('\n');
};
export const createRemoteOpenCodeToolCommand = (options: IRemoteOpenCodeToolCommandOptions) => {
const nodePath = options.nodePath ?? joinRemotePath(options.runtimeRoot, 'node/bin/node');
const rgPath = options.rgPath ?? joinRemotePath(options.runtimeRoot, 'applications/remote-theia/lib/backend/native/rg');
return [
'set -euo pipefail',
`export LD_LIBRARY_PATH=${quoteRemotePath(joinRemotePath(options.runtimeRoot, 'node/lib'))}:\${LD_LIBRARY_PATH:-}`,
`export GITZONE_IDE_WORKSPACE=${quoteRemotePath(options.workspacePath)}`,
`export GITZONE_IDE_TOOL_NAME=${quoteShellArg(options.toolName)}`,
`export GITZONE_IDE_RG_PATH=${quoteRemotePath(rgPath)}`,
`${quoteRemotePath(nodePath)} -e ${quoteShellArg(remoteOpenCodeToolScript)}`,
].join('\n');
};
export const createRemoteProjectListCommand = (options: IRemoteProjectRegistryOptions) => {
const projectsFile = joinRemotePath(options.ideDataRoot ?? defaultIdeDataRoot, remoteProjectsFileName);
@@ -396,198 +361,6 @@ export const createRemoteHealthCommand = (serverVersion: string, installRoot = d
].join('\n');
};
export const remoteOpenCodeToolScript = [
"const fs = require('fs');",
"const path = require('path');",
"const childProcess = require('child_process');",
"const toolName = process.env.GITZONE_IDE_TOOL_NAME;",
"const workspacePath = expandHome(process.env.GITZONE_IDE_WORKSPACE || process.cwd());",
"const rgPath = process.env.GITZONE_IDE_RG_PATH || 'rg';",
"const inputText = fs.readFileSync(0, 'utf8');",
"const request = inputText.trim() ? JSON.parse(inputText) : {};",
"const args = request.args || {};",
"const MAX_LINES = 2000;",
"const MAX_LINE_LENGTH = 2000;",
"const MAX_BYTES = 50 * 1024;",
"function expandHome(value) {",
" const text = String(value || '');",
" if (text === '~' || text === '$HOME') return process.env.HOME || text;",
" if (text.startsWith('~/')) return path.join(process.env.HOME || '', text.slice(2));",
" if (text.startsWith('$HOME/')) return path.join(process.env.HOME || '', text.slice(6));",
" return text;",
"}",
"function normalizeRemotePath(value, fallback) {",
" if (!value) return fallback;",
" const expanded = expandHome(value);",
" return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(fallback, expanded);",
"}",
"function limitLine(line) {",
" return line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + '... (line truncated to 2000 chars)' : line;",
"}",
"function writeJson(result) { process.stdout.write(JSON.stringify(result)); }",
"function ensureParent(filePath) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); }",
"function toLines(text) {",
" if (!text) return [];",
" const lines = text.split(/\\r?\\n/);",
" if (text.endsWith('\\n')) lines.pop();",
" return lines;",
"}",
"function readTool() {",
" const filePath = normalizeRemotePath(args.filePath, workspacePath);",
" const stat = fs.statSync(filePath);",
" const offset = Math.max(1, Number(args.offset || 1));",
" const limit = Math.max(0, Number(args.limit || MAX_LINES));",
" if (stat.isDirectory()) {",
" const entries = fs.readdirSync(filePath, { withFileTypes: true }).map((entry) => entry.name + (entry.isDirectory() ? '/' : '')).sort((a, b) => a.localeCompare(b));",
" const sliced = entries.slice(offset - 1, offset - 1 + limit);",
" const truncated = offset - 1 + sliced.length < entries.length;",
" return { output: ['<path>' + filePath + '</path>', '<type>directory</type>', '<entries>', sliced.join('\\n'), truncated ? '\\n(Showing ' + sliced.length + ' of ' + entries.length + ' entries. Use offset=' + (offset + sliced.length) + ' to continue.)' : '\\n(' + entries.length + ' entries)', '</entries>'].join('\\n'), metadata: { preview: sliced.slice(0, 20).join('\\n'), truncated } };",
" }",
" const buffer = fs.readFileSync(filePath);",
" if (buffer.includes(0)) throw new Error('Cannot read binary file: ' + filePath);",
" const lines = toLines(buffer.toString('utf8'));",
" if (offset > lines.length && !(lines.length === 0 && offset === 1)) throw new Error('Offset ' + offset + ' is out of range for this file (' + lines.length + ' lines)');",
" const raw = [];",
" let bytes = 0;",
" let cut = false;",
" for (const line of lines.slice(offset - 1)) {",
" if (raw.length >= limit) break;",
" const next = limitLine(line);",
" const size = Buffer.byteLength(next, 'utf8') + (raw.length > 0 ? 1 : 0);",
" if (bytes + size > MAX_BYTES) { cut = true; break; }",
" raw.push(next);",
" bytes += size;",
" }",
" const last = offset + raw.length - 1;",
" const more = cut || last < lines.length;",
" let output = '<path>' + filePath + '</path>\\n<type>file</type>\\n<content>\\n';",
" output += raw.map((line, index) => (index + offset) + ': ' + line).join('\\n');",
" output += more ? '\\n\\n(Showing lines ' + offset + '-' + last + ' of ' + lines.length + '. Use offset=' + (last + 1) + ' to continue.)' : '\\n\\n(End of file - total ' + lines.length + ' lines)';",
" output += '\\n</content>';",
" return { output, metadata: { preview: raw.slice(0, 20).join('\\n'), truncated: more } };",
"}",
"function writeTool() {",
" const filePath = normalizeRemotePath(args.filePath, workspacePath);",
" const existed = fs.existsSync(filePath);",
" ensureParent(filePath);",
" fs.writeFileSync(filePath, String(args.content || ''));",
" return { output: 'Wrote file successfully.', metadata: { filepath: filePath, exists: existed } };",
"}",
"function editTool() {",
" const filePath = normalizeRemotePath(args.filePath, workspacePath);",
" if (args.oldString === args.newString) throw new Error('No changes to apply: oldString and newString are identical.');",
" let content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';",
" let next;",
" if (args.oldString === '') {",
" next = String(args.newString || '');",
" } else {",
" const oldString = String(args.oldString || '');",
" const newString = String(args.newString || '');",
" const matches = content.split(oldString).length - 1;",
" if (matches === 0) throw new Error('Could not find oldString in the file. It must match exactly.');",
" if (!args.replaceAll && matches > 1) throw new Error('Found multiple matches for oldString. Provide more surrounding context or set replaceAll.');",
" next = args.replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);",
" }",
" ensureParent(filePath);",
" fs.writeFileSync(filePath, next);",
" return { output: 'Edit applied successfully.', metadata: { filepath: filePath } };",
"}",
"function bashTool() {",
" const cwd = normalizeRemotePath(args.workdir, workspacePath);",
" const timeout = Number(args.timeout || 120000);",
" const shell = process.env.SHELL || '/bin/sh';",
" const result = childProcess.spawnSync(shell, ['-lc', String(args.command || '')], { cwd, encoding: 'utf8', timeout, maxBuffer: 10 * 1024 * 1024 });",
" const outputText = [result.stdout || '', result.stderr || ''].filter(Boolean).join('');",
" const metadata = [];",
" if (result.error && result.error.code === 'ETIMEDOUT') metadata.push('remote shell tool terminated command after exceeding timeout ' + timeout + ' ms.');",
" metadata.push('exit=' + (result.status === null || result.status === undefined ? 1 : result.status));",
" const output = (outputText.trim() ? outputText.replace(/\\s+$/g, '') : '(no output)') + '\\n\\n<shell_metadata>\\n' + metadata.join('\\n') + '\\n</shell_metadata>';",
" return { output, metadata: { exit: result.status, cwd } };",
"}",
"function grepTool() {",
" if (!args.pattern) throw new Error('pattern is required');",
" const search = normalizeRemotePath(args.path, workspacePath);",
" const executable = fs.existsSync(rgPath) ? rgPath : 'rg';",
" const rgArgs = ['--line-number', '--with-filename', '--color', 'never', '--no-heading'];",
" if (args.include) rgArgs.push('--glob', String(args.include));",
" rgArgs.push('--', String(args.pattern), search);",
" const result = childProcess.spawnSync(executable, rgArgs, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });",
" if (result.status !== 0 && result.status !== 1) throw new Error(result.stderr || ('ripgrep failed with exit ' + result.status));",
" const rows = (result.stdout || '').split(/\\r?\\n/).filter(Boolean).map((line) => { const match = line.match(/^(.*?):(\\d+):(.*)$/); if (!match) return undefined; const filePath = path.resolve(match[1]); let mtime = 0; try { mtime = fs.statSync(filePath).mtimeMs; } catch {} return { path: filePath, line: Number(match[2]), text: match[3], mtime }; }).filter(Boolean);",
" rows.sort((left, right) => right.mtime - left.mtime);",
" const limit = 100;",
" const truncated = rows.length > limit;",
" const finalRows = truncated ? rows.slice(0, limit) : rows;",
" if (finalRows.length === 0) return { output: 'No files found', metadata: { matches: 0, truncated: false } };",
" const output = ['Found ' + rows.length + ' matches' + (truncated ? ' (showing first ' + limit + ')' : '')];",
" let current = '';",
" for (const row of finalRows) { if (current !== row.path) { if (current) output.push(''); current = row.path; output.push(row.path + ':'); } output.push(' Line ' + row.line + ': ' + limitLine(row.text)); }",
" if (truncated) output.push('', '(Results truncated: showing ' + limit + ' of ' + rows.length + ' matches.)');",
" return { output: output.join('\\n'), metadata: { matches: rows.length, truncated } };",
"}",
"function globTool() {",
" if (!args.pattern) throw new Error('pattern is required');",
" const search = normalizeRemotePath(args.path, workspacePath);",
" const executable = fs.existsSync(rgPath) ? rgPath : 'rg';",
" const result = childProcess.spawnSync(executable, ['--files', '--color', 'never', '--glob', String(args.pattern), '--', search], { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });",
" if (result.status !== 0 && result.status !== 1) throw new Error(result.stderr || ('ripgrep failed with exit ' + result.status));",
" const files = (result.stdout || '').split(/\\r?\\n/).filter(Boolean).map((entry) => path.isAbsolute(entry) ? entry : path.resolve(search, entry)).map((filePath) => { let mtime = 0; try { mtime = fs.statSync(filePath).mtimeMs; } catch {} return { path: filePath, mtime }; }).sort((left, right) => right.mtime - left.mtime);",
" const limit = 100;",
" const truncated = files.length > limit;",
" const finalFiles = truncated ? files.slice(0, limit) : files;",
" const output = finalFiles.length ? finalFiles.map((file) => file.path) : ['No files found'];",
" if (truncated) output.push('', '(Results are truncated: showing first ' + limit + ' results.)');",
" return { output: output.join('\\n'), metadata: { count: finalFiles.length, truncated } };",
"}",
"function parsePatch(text) {",
" const lines = String(text || '').replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n').split('\\n');",
" if (lines[0] !== '*** Begin Patch') throw new Error('apply_patch verification failed: missing begin marker');",
" const ops = [];",
" let index = 1;",
" while (index < lines.length) {",
" const line = lines[index];",
" if (line === '*** End Patch') break;",
" if (line.startsWith('*** Add File: ')) { const file = line.slice(14); const body = []; index++; while (index < lines.length && !lines[index].startsWith('*** ')) { if (!lines[index].startsWith('+')) throw new Error('add file lines must start with +'); body.push(lines[index].slice(1)); index++; } ops.push({ type: 'add', file, body }); continue; }",
" if (line.startsWith('*** Delete File: ')) { ops.push({ type: 'delete', file: line.slice(17) }); index++; continue; }",
" if (line.startsWith('*** Update File: ')) { const file = line.slice(17); const body = []; let moveTo; index++; if (lines[index] && lines[index].startsWith('*** Move to: ')) { moveTo = lines[index].slice(13); index++; } while (index < lines.length && !lines[index].startsWith('*** ')) { body.push(lines[index]); index++; } ops.push({ type: 'update', file, moveTo, body }); continue; }",
" throw new Error('apply_patch verification failed: unknown patch line ' + line);",
" }",
" if (ops.length === 0) throw new Error('patch rejected: empty patch');",
" return ops;",
"}",
"function replaceHunk(content, oldLines, newLines) {",
" const oldText = oldLines.join('\\n');",
" const newText = newLines.join('\\n');",
" const candidates = oldLines.length === 0 ? [['', newText]] : [[oldText + '\\n', newText + '\\n'], [oldText, newText]];",
" for (const pair of candidates) { const from = pair[0]; const to = pair[1]; const position = from === '' ? content.length : content.indexOf(from); if (position !== -1) return content.slice(0, position) + to + content.slice(position + from.length); }",
" throw new Error('apply_patch verification failed: hunk context not found');",
"}",
"function applyPatchTool() {",
" const ops = parsePatch(args.patchText);",
" const summary = [];",
" for (const op of ops) {",
" const filePath = normalizeRemotePath(op.file, workspacePath);",
" if (op.type === 'add') { ensureParent(filePath); fs.writeFileSync(filePath, op.body.join('\\n') + (op.body.length ? '\\n' : '')); summary.push('A ' + path.relative(workspacePath, filePath)); continue; }",
" if (op.type === 'delete') { fs.rmSync(filePath, { force: true }); summary.push('D ' + path.relative(workspacePath, filePath)); continue; }",
" let content = fs.readFileSync(filePath, 'utf8');",
" const groups = []; let current = [];",
" for (const line of op.body) { if (line.startsWith('@@')) { if (current.length) groups.push(current); current = []; continue; } current.push(line); }",
" if (current.length) groups.push(current);",
" for (const group of groups) { const oldLines = []; const newLines = []; for (const line of group) { if (!line) continue; const marker = line[0]; const value = line.slice(1); if (marker === ' ') { oldLines.push(value); newLines.push(value); } else if (marker === '-') { oldLines.push(value); } else if (marker === '+') { newLines.push(value); } else if (line.startsWith('\\\\ No newline')) { } else { throw new Error('apply_patch verification failed: invalid hunk line ' + line); } } content = replaceHunk(content, oldLines, newLines); }",
" const targetPath = op.moveTo ? normalizeRemotePath(op.moveTo, workspacePath) : filePath;",
" ensureParent(targetPath); fs.writeFileSync(targetPath, content); if (op.moveTo) fs.rmSync(filePath, { force: true }); summary.push('M ' + path.relative(workspacePath, targetPath));",
" }",
" return { output: 'Success. Updated the following files:\\n' + summary.join('\\n'), metadata: { files: summary } };",
"}",
"try {",
" const handlers = { bash: bashTool, read: readTool, write: writeTool, edit: editTool, grep: grepTool, glob: globTool, apply_patch: applyPatchTool };",
" if (!handlers[toolName]) throw new Error('Unsupported Git.Zone OpenCode tool: ' + toolName);",
" writeJson(handlers[toolName]());",
"} catch (error) {",
" console.error(error && error.stack ? error.stack : String(error));",
" process.exit(1);",
"}",
].join('\n');
export const quoteShellArg = (value: string | number | boolean) => {
const stringValue = String(value);
-2
View File
@@ -440,7 +440,6 @@ export const probeRemoteHost = async (
`printf 'nodeVersion=%s\\n' "$(node --version 2>/dev/null || true)"`,
`printf 'pnpmVersion=%s\\n' "$(pnpm --version 2>/dev/null || true)"`,
`printf 'gitVersion=%s\\n' "$(git --version 2>/dev/null || true)"`,
`printf 'opencodeVersion=%s\\n' "$(opencode --version 2>/dev/null || true)"`,
].join('; ');
const result = await runSshCommand(target, command, options);
@@ -460,7 +459,6 @@ export const probeRemoteHost = async (
nodeVersion: fields.nodeVersion,
pnpmVersion: fields.pnpmVersion,
gitVersion: fields.gitVersion,
opencodeVersion: fields.opencodeVersion,
errors,
};
};
+307 -136
View File
@@ -32,9 +32,6 @@ importers:
applications/electron-shell:
dependencies:
'@git.zone/ide-opencode-bridge':
specifier: workspace:*
version: link:../../packages/opencode-bridge
'@git.zone/ide-protocol':
specifier: workspace:*
version: link:../../packages/protocol
@@ -44,12 +41,15 @@ importers:
'@git.zone/ide-ssh':
specifier: workspace:*
version: link:../../packages/ssh
'@push.rocks/smartagent':
specifier: ^3.2.0
version: 3.2.0(typescript@5.6.3)(ws@8.20.0)
'@push.rocks/smartai':
specifier: ^2.3.0
version: 2.3.0(typescript@5.6.3)(ws@8.20.0)(zod@4.4.3)
electron:
specifier: ^42.0.1
version: 42.0.1
opencode-ai:
specifier: 1.14.48
version: 1.14.48
devDependencies:
electron-builder:
specifier: ^26.8.1
@@ -92,10 +92,10 @@ importers:
version: 1.71.0(typescript@6.0.3)
'@theia/plugin-ext':
specifier: 1.71.0
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/plugin-ext-vscode':
specifier: 1.71.0
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/preferences':
specifier: 1.71.0
version: 1.71.0(typescript@6.0.3)
@@ -116,7 +116,7 @@ importers:
version: 1.71.0(typescript@6.0.3)
'@theia/vsx-registry':
specifier: 1.71.0
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
version: 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/workspace':
specifier: 1.71.0
version: 1.71.0(typescript@6.0.3)
@@ -131,8 +131,6 @@ importers:
specifier: ^3.3.0
version: 3.3.0(webpack@5.106.2)
packages/opencode-bridge: {}
packages/protocol: {}
packages/server-installer:
@@ -167,6 +165,79 @@ packages:
7zip-bin@5.2.0:
resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==}
'@ai-sdk/anthropic@3.0.77':
resolution: {integrity: sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.114':
resolution: {integrity: sha512-MqkZ5sd+qiq6RgIxELkoFQXg2/JwK+WCMaot7U+rtrZpWJl3fSyYvc28SC03b256o4F7OXjQtdjTqs81B2w+dA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/google@3.0.73':
resolution: {integrity: sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/groq@3.0.39':
resolution: {integrity: sha512-BZAr6DjCbzWQ0Qn1/TSsHo/bmCt4JaAMb4A7HCSUZBQCAcOjne/03D0sVjHnQhUC3TpwcmYiv7tHAviK7BluRw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/mistral@3.0.36':
resolution: {integrity: sha512-FLIb2QdLraOgQP3puUybuFYWbtsB02YWQBTOJOk8heiEsdFW3YE0dfuzwtsvoF4FXlBnbYYMWu5jgOesthcmWg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai-compatible@2.0.47':
resolution: {integrity: sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@3.0.63':
resolution: {integrity: sha512-4yY/m8a57MNNVoJCsXuNblKf6BO4yuAuLKRX4tzSNffBEBSp1FlcWdPE0Z4FkqUeS0AJhYSSqp0GIiA/cIcDNA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/perplexity@3.0.33':
resolution: {integrity: sha512-aNt6pTAzq+akadDXVdg2SjN2dODtaVlkKbw8/35c+sekr+Tx0sJwVqMR1udxrjLzhQvz8qtfsWRuz+hB9pmOnQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.27':
resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider@3.0.10':
resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==}
engines: {node: '>=18'}
'@ai-sdk/xai@3.0.89':
resolution: {integrity: sha512-ecFE4iQnWePrxPYuSUYCh8lpoKZ52J3jao5whDVC3+Z9Cu/XeyOe2oUGzsYSUPgbJNi/ZmD/KN69bHbUcAHFvw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@anthropic-ai/sdk@0.95.2':
resolution: {integrity: sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==}
hasBin: true
peerDependencies:
zod: ^3.25.0 || ^4.0.0
peerDependenciesMeta:
zod:
optional: true
'@api.global/typedrequest-interfaces@2.0.2':
resolution: {integrity: sha512-D+mkr4IiUZ/eUgrdp5jXjBKOW/iuMcl0z2ZLQsLLypKX/psFGD3viZJ58FNRa+/1OSM38JS5wFyoWl8oPEFLrw==}
@@ -1742,6 +1813,10 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@opentelemetry/api@1.9.1':
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
'@oxc-project/types@0.129.0':
resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==}
@@ -1923,6 +1998,12 @@ packages:
'@push.rocks/qenv@6.1.4':
resolution: {integrity: sha512-NlDwrb3KJVBCeEXIWaYRZXZLOvHhDoo+n2X5akcGCDjn5HyP0C9/opn2RDpCnSt+hoValKpp89wcX4BEB+gWjA==}
'@push.rocks/smartagent@3.2.0':
resolution: {integrity: sha512-npWpKMtl2Fa250IVC4L6ZMaBBhl5vCl/icbVVhUb7CueySoH0D0sO1/+oUtj2vQ4i4W+QTMOq/uN0rQ4eGisfQ==}
'@push.rocks/smartai@2.3.0':
resolution: {integrity: sha512-i2Oz322qzU0ao/QJvpFNmqN8fkGbctImYZ6iDs9MYwR6KKbwoLDp1tZg1rM/nf1LuHOqjdojGcDNf0ycrTfHTw==}
'@push.rocks/smartarchive@5.2.2':
resolution: {integrity: sha512-EEh3X5f5EAERx6qYmqPFsAAWYSlodmEYxFTKsa4jUK4AFb5Dn/vK5Jsx2A46PKriu8mQJIMEfGWrkLU4kTi5tw==}
@@ -2459,6 +2540,12 @@ packages:
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@stablelib/base64@1.0.1':
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@stroncium/procfs@1.2.1':
resolution: {integrity: sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA==}
engines: {node: '>=8'}
@@ -2897,6 +2984,10 @@ packages:
'@ungap/structured-clone@1.3.1':
resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
'@vercel/oidc@3.2.0':
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
engines: {node: '>= 20'}
'@virtuoso.dev/react-urx@0.2.13':
resolution: {integrity: sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==}
engines: {node: '>=10'}
@@ -3053,6 +3144,12 @@ packages:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
ai@6.0.182:
resolution: {integrity: sha512-ooJdziFjYrYRcsCx107roqA8gDTI3P82nUfroNWIhVvwrkYzEN3W1l50YK+XNqkUew8AiimaW0/SLBewRXMuHQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@@ -4210,6 +4307,9 @@ packages:
fast-plist@0.1.3:
resolution: {integrity: sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A==}
fast-sha256@1.3.0:
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
@@ -4899,6 +4999,10 @@ packages:
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-to-ts@3.1.1:
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
engines: {node: '>=16'}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -4908,6 +5012,9 @@ packages:
json-schema-typed@8.0.2:
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
json-stable-stringify@1.3.0:
resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==}
engines: {node: '>= 0.4'}
@@ -5612,69 +5719,17 @@ packages:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
opencode-ai@1.14.48:
resolution: {integrity: sha512-65ogCJao8ujS4gFP64QuStv/h+qII30xNiARGsu9ai8Uoj+a/m6gz8yhNC9oz1dZm7PF6n15iIpB/LzCXBrhZQ==}
openai@6.37.0:
resolution: {integrity: sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==}
hasBin: true
opencode-darwin-arm64@1.14.48:
resolution: {integrity: sha512-QF05WtuPVnXCkvBHdDSuIhHVgYb/f10HO2mRlUbbBWStbPedLWwOKC4YKUhpsrLAVBEFLcL/xh7RtPM70+0TZQ==}
cpu: [arm64]
os: [darwin]
opencode-darwin-x64-baseline@1.14.48:
resolution: {integrity: sha512-F3p1GRuPR+HKDVIGn/uS9N0685BQ1msOOZsYHiAc4Qe22ZhZGrnMveg4CZwDseAQxoFL5n29i9J7iw/QORmO/g==}
cpu: [x64]
os: [darwin]
opencode-darwin-x64@1.14.48:
resolution: {integrity: sha512-Yx0/opXz7cdne1Xi77TNTIwWpYSwv+T32PdEcyqrcgEx0NR5f4S4kEsydmKsV7lzFRSkqRXniwvLj+8BPJEwNA==}
cpu: [x64]
os: [darwin]
opencode-linux-arm64-musl@1.14.48:
resolution: {integrity: sha512-w+wY4ROlq6lpL+SPYLYHBA6k+twVU9kurBGt+6irf4ZA5BCkhva7mgRET4NBxheUKEPSEQknq3Umi2Ltab4G+w==}
cpu: [arm64]
os: [linux]
opencode-linux-arm64@1.14.48:
resolution: {integrity: sha512-DPZUi4IErlM/oXbNQKOiAqlmIij322sGUewNkvn+UptZtoqMVjtM8UQc6RkfoSQtAhk9zfdXTRjmajXpSSPJTA==}
cpu: [arm64]
os: [linux]
opencode-linux-x64-baseline-musl@1.14.48:
resolution: {integrity: sha512-92icmlOdDHLm0ityztGHGlgv6OiqQTdXYZRsPUs17IcHlkxLUT2b5O8HUpHk/xtMwyJEleYpV3E5EQz4vP5HVw==}
cpu: [x64]
os: [linux]
opencode-linux-x64-baseline@1.14.48:
resolution: {integrity: sha512-I8KoLTSpCYkChdosh2iFfXO0zwPuAp7ZbPZyzPN9R0jTW95EstO5qYPTLs8F+sZJiM+oUdxOEpgIVkGF6JMzAg==}
cpu: [x64]
os: [linux]
opencode-linux-x64-musl@1.14.48:
resolution: {integrity: sha512-h2QPam8t++TEphKm8zroxfuDZk8oAF40aKFxH753XrINfCAyRikOb/hNfLJd0J3oAcolxMbFHjr6c35zyJbzHQ==}
cpu: [x64]
os: [linux]
opencode-linux-x64@1.14.48:
resolution: {integrity: sha512-WLAABtQD2Zr1+7zfGRcNYdAKYtr2EuMY6RwjoNRAqHzQslOlItZhDpwdqg03YFe0zrQhg0IlK6iGuplF1ertdw==}
cpu: [x64]
os: [linux]
opencode-windows-arm64@1.14.48:
resolution: {integrity: sha512-WqcJHoxI3e06So6g83tKuyAGYXHp++/urLtojw78HDhV0RlO9Sthbv19YRu9gqp/GLYNqNChU5JLjVYFmoegkg==}
cpu: [arm64]
os: [win32]
opencode-windows-x64-baseline@1.14.48:
resolution: {integrity: sha512-3RC6Pq/9yJn4jnWjOdDbJPJgIoMXRmWr4AytNeBwi7NifTBGmGnqxU+mFRWYJxJmCIvyWxHw/iVfEUtoej7/JQ==}
cpu: [x64]
os: [win32]
opencode-windows-x64@1.14.48:
resolution: {integrity: sha512-dq5glnTtVjd5sJJcQWpcSHTHj4x9hKsBTFSvGV7tV+FoBTQFsnPtCFqt6Wmo/s0D0VYQsMz/syY1YGtrA+O3pA==}
cpu: [x64]
os: [win32]
peerDependencies:
ws: ^8.18.0
zod: ^3.25 || ^4.0
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
@@ -6587,6 +6642,9 @@ packages:
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
standardwebhooks@1.0.0:
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
stat-mode@1.0.0:
resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
engines: {node: '>= 6'}
@@ -6866,6 +6924,9 @@ packages:
truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
ts-algebra@2.0.0:
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
ts-md5@1.3.1:
resolution: {integrity: sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg==}
engines: {node: '>=12'}
@@ -7438,6 +7499,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -7445,6 +7509,80 @@ snapshots:
7zip-bin@5.2.0: {}
'@ai-sdk/anthropic@3.0.77(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/gateway@3.0.114(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
'@vercel/oidc': 3.2.0
zod: 4.4.3
'@ai-sdk/google@3.0.73(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/groq@3.0.39(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/mistral@3.0.36(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/openai-compatible@2.0.47(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/openai@3.0.63(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/perplexity@3.0.33(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@ai-sdk/provider-utils@4.0.27(zod@4.4.3)':
dependencies:
'@ai-sdk/provider': 3.0.10
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.8
zod: 4.4.3
'@ai-sdk/provider@3.0.10':
dependencies:
json-schema: 0.4.0
'@ai-sdk/xai@3.0.89(zod@4.4.3)':
dependencies:
'@ai-sdk/openai-compatible': 2.0.47(zod@4.4.3)
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
zod: 4.4.3
'@anthropic-ai/sdk@0.95.2(zod@4.4.3)':
dependencies:
json-schema-to-ts: 3.1.1
standardwebhooks: 1.0.0
optionalDependencies:
zod: 4.4.3
'@api.global/typedrequest-interfaces@2.0.2': {}
'@api.global/typedrequest-interfaces@3.0.19': {}
@@ -9604,7 +9742,7 @@ snapshots:
'@mixmark-io/domino@2.2.0': {}
'@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)':
'@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)':
dependencies:
'@hono/node-server': 1.19.14(hono@4.12.18)
ajv: 8.20.0
@@ -9621,8 +9759,8 @@ snapshots:
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
zod: 4.4.3
zod-to-json-schema: 3.25.2(zod@4.4.3)
optionalDependencies:
'@cfworker/json-schema': 4.1.1
transitivePeerDependencies:
@@ -9671,6 +9809,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@opentelemetry/api@1.9.1': {}
'@oxc-project/types@0.129.0': {}
'@parcel/watcher-android-arm64@2.5.6':
@@ -9939,6 +10079,51 @@ snapshots:
'@push.rocks/smartpath': 6.0.0
yaml: 2.8.4
'@push.rocks/smartagent@3.2.0(typescript@5.6.3)(ws@8.20.0)':
dependencies:
'@push.rocks/smartai': 2.3.0(typescript@5.6.3)(ws@8.20.0)(zod@4.4.3)
'@push.rocks/smartfs': 1.5.1
'@push.rocks/smartrequest': 5.0.3
'@push.rocks/smartshell': 3.4.0
ai: 6.0.182(zod@4.4.3)
zod: 4.4.3
transitivePeerDependencies:
- aws-crt
- bare-abort-controller
- bare-buffer
- bufferutil
- react-native-b4a
- supports-color
- typescript
- utf-8-validate
- ws
'@push.rocks/smartai@2.3.0(typescript@5.6.3)(ws@8.20.0)(zod@4.4.3)':
dependencies:
'@ai-sdk/anthropic': 3.0.77(zod@4.4.3)
'@ai-sdk/google': 3.0.73(zod@4.4.3)
'@ai-sdk/groq': 3.0.39(zod@4.4.3)
'@ai-sdk/mistral': 3.0.36(zod@4.4.3)
'@ai-sdk/openai': 3.0.63(zod@4.4.3)
'@ai-sdk/perplexity': 3.0.33(zod@4.4.3)
'@ai-sdk/provider': 3.0.10
'@ai-sdk/xai': 3.0.89(zod@4.4.3)
'@anthropic-ai/sdk': 0.95.2(zod@4.4.3)
'@push.rocks/smartpdf': 4.2.2(typescript@5.6.3)
ai: 6.0.182(zod@4.4.3)
openai: 6.37.0(ws@8.20.0)(zod@4.4.3)
transitivePeerDependencies:
- aws-crt
- bare-abort-controller
- bare-buffer
- bufferutil
- react-native-b4a
- supports-color
- typescript
- utf-8-validate
- ws
- zod
'@push.rocks/smartarchive@5.2.2':
dependencies:
'@push.rocks/smartdelay': 3.1.0
@@ -10835,6 +11020,10 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
'@stablelib/base64@1.0.1': {}
'@standard-schema/spec@1.1.0': {}
'@stroncium/procfs@1.2.1': {}
'@szmarczak/http-timer@4.0.6':
@@ -10871,9 +11060,9 @@ snapshots:
- typescript
- utf-8-validate
'@theia/ai-mcp@1.71.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3)(zod@3.25.76)':
'@theia/ai-mcp@1.71.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)
'@theia/ai-core': 1.71.0(typescript@6.0.3)
'@theia/core': 1.71.0
'@theia/workspace': 1.71.0(typescript@6.0.3)
@@ -11445,7 +11634,7 @@ snapshots:
semver: 7.8.0
tslib: 2.8.1
'@theia/plugin-ext-vscode@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)':
'@theia/plugin-ext-vscode@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@theia/callhierarchy': 1.71.0
'@theia/core': 1.71.0
@@ -11456,7 +11645,7 @@ snapshots:
'@theia/navigator': 1.71.0(typescript@6.0.3)
'@theia/outline-view': 1.71.0
'@theia/plugin': 1.71.0
'@theia/plugin-ext': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
'@theia/plugin-ext': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/scm': 1.71.0(@types/react@18.3.28)(react@18.3.1)(typescript@6.0.3)
'@theia/terminal': 1.71.0(typescript@6.0.3)
'@theia/typehierarchy': 1.71.0
@@ -11481,9 +11670,9 @@ snapshots:
- utf-8-validate
- zod
'@theia/plugin-ext@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)':
'@theia/plugin-ext@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@theia/ai-mcp': 1.71.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3)(zod@3.25.76)
'@theia/ai-mcp': 1.71.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/bulk-edit': 1.71.0(typescript@6.0.3)
'@theia/callhierarchy': 1.71.0
'@theia/console': 1.71.0(typescript@6.0.3)
@@ -11785,14 +11974,14 @@ snapshots:
- supports-color
- utf-8-validate
'@theia/vsx-registry@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)':
'@theia/vsx-registry@1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@theia/core': 1.71.0
'@theia/filesystem': 1.71.0(typescript@6.0.3)
'@theia/navigator': 1.71.0(typescript@6.0.3)
'@theia/ovsx-client': 1.71.0
'@theia/plugin-ext': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
'@theia/plugin-ext-vscode': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@3.25.76)
'@theia/plugin-ext': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/plugin-ext-vscode': 1.71.0(@cfworker/json-schema@4.1.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)(zod@4.4.3)
'@theia/preferences': 1.71.0(typescript@6.0.3)
'@theia/workspace': 1.71.0(typescript@6.0.3)
limiter: 2.1.0
@@ -12151,6 +12340,8 @@ snapshots:
'@ungap/structured-clone@1.3.1': {}
'@vercel/oidc@3.2.0': {}
'@virtuoso.dev/react-urx@0.2.13(react@18.3.1)':
dependencies:
'@virtuoso.dev/urx': 0.2.13
@@ -12338,6 +12529,14 @@ snapshots:
clean-stack: 2.2.0
indent-string: 4.0.0
ai@6.0.182(zod@4.4.3):
dependencies:
'@ai-sdk/gateway': 3.0.114(zod@4.4.3)
'@ai-sdk/provider': 3.0.10
'@ai-sdk/provider-utils': 4.0.27(zod@4.4.3)
'@opentelemetry/api': 1.9.1
zod: 4.4.3
ajv-formats@2.1.1(ajv@8.20.0):
optionalDependencies:
ajv: 8.20.0
@@ -13701,6 +13900,8 @@ snapshots:
fast-plist@0.1.3: {}
fast-sha256@1.3.0: {}
fast-uri@3.1.2: {}
fast-xml-builder@1.2.0:
@@ -14474,12 +14675,19 @@ snapshots:
json-parse-even-better-errors@2.3.1: {}
json-schema-to-ts@3.1.1:
dependencies:
'@babel/runtime': 7.29.2
ts-algebra: 2.0.0
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-schema-typed@8.0.2: {}
json-schema@0.4.0: {}
json-stable-stringify@1.3.0:
dependencies:
call-bind: 1.0.9
@@ -15370,56 +15578,10 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
opencode-ai@1.14.48:
openai@6.37.0(ws@8.20.0)(zod@4.4.3):
optionalDependencies:
opencode-darwin-arm64: 1.14.48
opencode-darwin-x64: 1.14.48
opencode-darwin-x64-baseline: 1.14.48
opencode-linux-arm64: 1.14.48
opencode-linux-arm64-musl: 1.14.48
opencode-linux-x64: 1.14.48
opencode-linux-x64-baseline: 1.14.48
opencode-linux-x64-baseline-musl: 1.14.48
opencode-linux-x64-musl: 1.14.48
opencode-windows-arm64: 1.14.48
opencode-windows-x64: 1.14.48
opencode-windows-x64-baseline: 1.14.48
opencode-darwin-arm64@1.14.48:
optional: true
opencode-darwin-x64-baseline@1.14.48:
optional: true
opencode-darwin-x64@1.14.48:
optional: true
opencode-linux-arm64-musl@1.14.48:
optional: true
opencode-linux-arm64@1.14.48:
optional: true
opencode-linux-x64-baseline-musl@1.14.48:
optional: true
opencode-linux-x64-baseline@1.14.48:
optional: true
opencode-linux-x64-musl@1.14.48:
optional: true
opencode-linux-x64@1.14.48:
optional: true
opencode-windows-arm64@1.14.48:
optional: true
opencode-windows-x64-baseline@1.14.48:
optional: true
opencode-windows-x64@1.14.48:
optional: true
ws: 8.20.0
zod: 4.4.3
opener@1.5.2: {}
@@ -16533,6 +16695,11 @@ snapshots:
sprintf-js@1.1.3:
optional: true
standardwebhooks@1.0.0:
dependencies:
'@stablelib/base64': 1.0.1
fast-sha256: 1.3.0
stat-mode@1.0.0: {}
statuses@2.0.2: {}
@@ -16832,6 +16999,8 @@ snapshots:
dependencies:
utf8-byte-length: 1.0.5
ts-algebra@2.0.0: {}
ts-md5@1.3.1: {}
tslib@1.14.1: {}
@@ -17373,12 +17542,14 @@ snapshots:
yoctocolors-cjs@2.1.3: {}
zod-to-json-schema@3.25.2(zod@3.25.76):
zod-to-json-schema@3.25.2(zod@4.4.3):
dependencies:
zod: 3.25.76
zod: 4.4.3
zod@3.23.8: {}
zod@3.25.76: {}
zod@4.4.3: {}
zwitch@2.0.4: {}
+3 -4
View File
@@ -1,8 +1,8 @@
# Git.Zone IDE
Git.Zone IDE is a remote-first desktop IDE based on Eclipse Theia, Electron, SSH, and OpenCode.
Git.Zone IDE is a remote-first desktop IDE based on Eclipse Theia, Electron, and SSH.
The local Electron shell manages SSH sessions, tunnels, and the native OpenCode chat runtime. The remote host runs the Theia backend inside the selected workspace; OpenCode code tools are overridden so file, shell, search, and patch actions execute remotely over SSH where the code lives.
The local Electron shell manages SSH sessions and tunnels. The remote host runs the Theia backend inside the selected workspace.
## Development
@@ -25,6 +25,5 @@ pnpm run start:remote
- `applications/remote-theia`: remote Theia application served through an SSH tunnel.
- `packages/ssh`: OpenSSH config parsing, command execution, probes, and tunnel lifecycle.
- `packages/server-installer`: remote server manifest, install paths, bootstrap script generation, and command planning.
- `packages/opencode-bridge`: typed OpenCode server client wrapper and event normalization.
- `packages/protocol`: shared session and server protocol types.
- `theia-extensions/gitzone-*`: Theia product, remote, and OpenCode integration extensions.
- `theia-extensions/gitzone-*`: Theia product and remote integration extensions.
+3 -4
View File
@@ -2,7 +2,6 @@
1. Build a local Electron shell that reads SSH targets, starts SSH tunnels, and opens the remote Theia frontend.
2. Install a versioned Theia server bundle into `~/.git.zone/ide-server/<version>` on remote SSH hosts.
3. Run the Theia backend and OpenCode server on the remote host, bound to `127.0.0.1`.
4. Run OpenCode in the native Electron shell with tool overrides that execute against the selected remote project over SSH.
5. Render OpenCode sessions, messages, permissions, diffs, todos, and Git.Zone commands in the native shell UI.
6. Keep Theia integration scoped to editor context and future intellisense data for OpenCode tools.
3. Run the Theia backend on the remote host, bound to `127.0.0.1`.
4. Keep native shell concerns scoped to SSH/session orchestration and project window management.
5. Keep Theia integration scoped to editor context and future intellisense data.
+55
View File
@@ -0,0 +1,55 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { GitZoneAgentRuntime, type IAgentEventEnvelope, type IAgentProjectContext } from '../applications/electron-shell/ts/agent-runtime.js';
const createProjectContext = (instanceId: string): IAgentProjectContext => ({
instanceId,
title: 'Persisted Project',
path: '/srv/work/persisted-project',
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
target: {
id: 'dev-box',
hostAlias: 'dev-box',
hostName: 'dev.example.com',
user: 'deploy',
port: 2222,
},
batchMode: true,
});
tap.test('should persist agent sessions by remote project context', async () => {
const persistenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-agent-runtime-'));
try {
const events: IAgentEventEnvelope[] = [];
const firstRuntime = new GitZoneAgentRuntime(
(payload) => events.push(payload),
(instanceId) => createProjectContext(instanceId),
persistenceRoot,
);
const createdSession = await firstRuntime.createSession({
instanceId: 'first-instance',
title: 'Persisted Chat',
});
firstRuntime.dispose();
const secondRuntime = new GitZoneAgentRuntime(
() => undefined,
(instanceId) => createProjectContext(instanceId),
persistenceRoot,
);
const sessions = await secondRuntime.listSessions({ instanceId: 'second-instance' });
secondRuntime.dispose();
expect(createdSession.title).toEqual('Persisted Chat');
expect(events.some((payload) => payload.event.type === 'session.created')).toEqual(true);
expect(sessions).toHaveLength(1);
expect(sessions[0]!.id).toEqual(createdSession.id);
expect(sessions[0]!.title).toEqual('Persisted Chat');
} finally {
await fs.rm(persistenceRoot, { recursive: true, force: true });
}
});
export default tap.start();
-72
View File
@@ -1,18 +1,12 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
createRemoteEphemeralBootstrapCommand,
createRemoteEphemeralReadinessCommand,
createRemoteEphemeralPortAllocationCommand,
createRemoteEphemeralRuntimeCacheCheckCommand,
createRemoteEphemeralRuntimeMarkCommand,
createRemoteOpenCodeToolCommand,
createRemoteProjectListCommand,
createRemoteProjectUpsertCommand,
remoteOpenCodeToolScript,
createRemoteBootstrapCommand,
createRemoteInstallCommand,
createRemoteServerInstallPlan,
@@ -52,15 +46,10 @@ tap.test('should render install and bootstrap commands', async () => {
serverVersion: '0.1.0',
workspacePath: '/srv/work/project',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(installCommand).toInclude('GITZONE_IDE_MANIFEST');
expect(installCommand).toInclude('"$HOME"/\'.git.zone/ide/server/0.1.0\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_OPENCODE_PORT');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART');
expect(bootstrapCommand).toInclude('pnpm --dir');
});
@@ -69,9 +58,6 @@ tap.test('should render remote home paths as expandable shell paths', async () =
serverVersion: '0.1.0',
workspacePath: '$HOME',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(bootstrapCommand).toInclude('test -d "$HOME"');
@@ -85,9 +71,6 @@ tap.test('should render ephemeral runtime bootstrap without remote pnpm', async
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
workspacePath: '$HOME',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(bootstrapCommand).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
@@ -95,7 +78,6 @@ tap.test('should render ephemeral runtime bootstrap without remote pnpm', async
expect(bootstrapCommand).not.toInclude('pnpm');
expect(bootstrapCommand).toInclude('LD_LIBRARY_PATH');
expect(bootstrapCommand).toInclude('THEIA_CONFIG_DIR="$HOME"/\'.git.zone/ide/theia\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART=\'1\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_THEIA_COLOR_THEME=\'dark\'');
expect(bootstrapCommand).toInclude("settings['workbench.colorTheme'] = colorTheme");
expect(bootstrapCommand).toInclude('"$HOME"/\'.git.zone/ide/logs\'');
@@ -150,43 +132,6 @@ tap.test('should render remote port allocation command', async () => {
expect(portCommand).toInclude('LD_LIBRARY_PATH');
});
tap.test('should render remote OpenCode tool bridge command', async () => {
const command = createRemoteOpenCodeToolCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
workspacePath: '$HOME/project',
toolName: 'read',
});
expect(command).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(command).toInclude('GITZONE_IDE_TOOL_NAME=\'read\'');
expect(command).toInclude('GITZONE_IDE_WORKSPACE="$HOME"/\'project\'');
expect(command).toInclude('GITZONE_IDE_RG_PATH');
expect(command).toInclude('fs.readFileSync(0');
});
tap.test('should execute remote OpenCode tool script with stdin payloads', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-opencode-tool-'));
const filePath = path.join(tempDir, 'sample.txt');
await fs.writeFile(filePath, 'hello\nworld\n');
const readResult = runRemoteOpenCodeToolScript('read', tempDir, { filePath: 'sample.txt' });
expect(readResult.output).toInclude('1: hello');
const editResult = runRemoteOpenCodeToolScript('edit', tempDir, {
filePath: 'sample.txt',
oldString: 'world',
newString: 'remote',
});
expect(editResult.output).toInclude('Edit applied successfully');
expect(await fs.readFile(filePath, 'utf8')).toEqual('hello\nremote\n');
const patchResult = runRemoteOpenCodeToolScript('apply_patch', tempDir, {
patchText: '*** Begin Patch\n*** Add File: nested/new.txt\n+created remotely\n*** End Patch',
});
expect(patchResult.output).toInclude('A nested/new.txt');
expect(await fs.readFile(path.join(tempDir, 'nested', 'new.txt'), 'utf8')).toEqual('created remotely\n');
});
tap.test('should render remote project registry commands', async () => {
const listCommand = createRemoteProjectListCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
@@ -204,21 +149,4 @@ tap.test('should render remote project registry commands', async () => {
expect(upsertCommand).toInclude('crypto.createHash');
});
const runRemoteOpenCodeToolScript = (toolName: string, workspacePath: string, args: Record<string, unknown>) => {
const result = childProcess.spawnSync(process.execPath, ['-e', remoteOpenCodeToolScript], {
input: JSON.stringify({ args }),
encoding: 'utf8',
env: {
...process.env,
GITZONE_IDE_TOOL_NAME: toolName,
GITZONE_IDE_WORKSPACE: workspacePath,
GITZONE_IDE_RG_PATH: '/not-found/rg',
},
});
if (result.status !== 0) {
throw new Error(result.stderr || `tool script failed with ${result.status}`);
}
return JSON.parse(result.stdout) as { output: string; metadata?: Record<string, unknown> };
};
export default tap.start();
-39
View File
@@ -1,39 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import {
openCodeBridgeToolNames,
renderOpenCodeBridgeConfigContent,
renderOpenCodeBridgeToolFile,
renderOpenCodeBridgeToolFiles,
} from '../packages/opencode-bridge/ts/index.js';
tap.test('should render managed OpenCode bridge config', async () => {
const config = JSON.parse(renderOpenCodeBridgeConfigContent());
expect(config.snapshot).toEqual(false);
expect(config.autoupdate).toEqual(false);
expect(config.permission.lsp).toEqual('deny');
expect(config.permission.skill).toEqual('deny');
});
tap.test('should render tool overrides for remote bridge', async () => {
const files = renderOpenCodeBridgeToolFiles();
for (const toolName of openCodeBridgeToolNames) {
expect(files[`tools/${toolName}.js`]).toInclude(`forwardTool(${JSON.stringify(toolName)}`);
}
expect(files['tools/bash.js']).toInclude('GITZONE_IDE_TOOL_BRIDGE_URL');
expect(files['tools/apply_patch.js']).toInclude('patchText');
});
tap.test('should allow custom bridge environment names', async () => {
const toolFile = renderOpenCodeBridgeToolFile('read', {
bridgeUrlEnvName: 'CUSTOM_BRIDGE_URL',
bridgeTokenEnvName: 'CUSTOM_BRIDGE_TOKEN',
});
expect(toolFile).toInclude('CUSTOM_BRIDGE_URL');
expect(toolFile).toInclude('CUSTOM_BRIDGE_TOKEN');
expect(toolFile).toInclude('filePath');
});
export default tap.start();
-40
View File
@@ -1,40 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'node:fs/promises';
import { parseServerSentEvent, sanitizeOpenCodeEventForRenderer } from '../packages/opencode-bridge/ts/index.js';
tap.test('should parse named opencode sse events', async () => {
const event = parseServerSentEvent('id: 1\nevent: server.connected\ndata: {"type":"server.connected"}\n');
expect(event!.id).toEqual('1');
expect(event!.type).toEqual('server.connected');
expect(event!.data).toEqual({ type: 'server.connected' });
});
tap.test('should infer opencode event type from json data', async () => {
const event = parseServerSentEvent('data: {"type":"session.updated","properties":{"id":"abc"}}\n');
expect(event!.type).toEqual('session.updated');
expect(event!.data).toEqual({ type: 'session.updated', properties: { id: 'abc' } });
});
tap.test('should sanitize opencode events for renderer delivery', async () => {
const event = parseServerSentEvent('id: 2\nretry: 1000\nevent: permission.asked\ndata: {"permissionID":"perm-1"}\n')!;
const sanitized = sanitizeOpenCodeEventForRenderer(event);
expect(sanitized).toEqual({
type: 'permission.asked',
id: '2',
retry: 1000,
data: { permissionID: 'perm-1' },
});
expect(Object.prototype.hasOwnProperty.call(sanitized, 'raw')).toEqual(false);
});
tap.test('should keep electron shell opencode resolution IDE-local', async () => {
const source = await fs.readFile(new URL('../applications/electron-shell/ts/main.ts', import.meta.url), 'utf8');
expect(source.includes('process.env.OPENCODE_BINARY')).toEqual(false);
expect(source.includes("'.opencode', 'bin', 'opencode'")).toEqual(false);
expect(source.includes('/usr/local/bin/opencode')).toEqual(false);
expect(source.includes('/usr/bin/opencode')).toEqual(false);
});
export default tap.start();
@@ -3,7 +3,6 @@ export declare const GitZoneRemoteServer: unique symbol;
export interface IGitZoneRemoteEnvironment {
workspacePath: string;
processId: number;
opencodePort?: number;
theiaPort?: number;
serverVersion?: string;
}
@@ -1 +1 @@
{"version":3,"file":"gitzone-remote-protocol.d.ts","sourceRoot":"","sources":["../../src/common/gitzone-remote-protocol.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,IAAI,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACtD"}
{"version":3,"file":"gitzone-remote-protocol.d.ts","sourceRoot":"","sources":["../../src/common/gitzone-remote-protocol.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,IAAI,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACtD"}
@@ -1 +1 @@
{"version":3,"file":"gitzone-remote-node-service.d.ts","sourceRoot":"","sources":["../../src/node/gitzone-remote-node-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAE5G,qBACa,wBAAyB,YAAW,oBAAoB;IAC7D,cAAc,IAAI,OAAO,CAAC,yBAAyB,CAAC;CAS3D"}
{"version":3,"file":"gitzone-remote-node-service.d.ts","sourceRoot":"","sources":["../../src/node/gitzone-remote-node-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAE5G,qBACa,wBAAyB,YAAW,oBAAoB;IAC7D,cAAc,IAAI,OAAO,CAAC,yBAAyB,CAAC;CAQ3D"}
@@ -13,7 +13,6 @@ let GitZoneRemoteNodeService = class GitZoneRemoteNodeService {
return {
workspacePath: process.env.GITZONE_IDE_WORKSPACE || process.cwd(),
processId: process.pid,
opencodePort: parseOptionalPort(process.env.GITZONE_IDE_OPENCODE_PORT),
theiaPort: parseOptionalPort(process.env.THEIA_PORT),
serverVersion: process.env.GITZONE_IDE_SERVER_VERSION,
};
@@ -1 +1 @@
{"version":3,"file":"gitzone-remote-node-service.js","sourceRoot":"","sources":["../../src/node/gitzone-remote-node-service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,oEAAmE;AAI5D,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IACnC,KAAK,CAAC,cAAc;QAClB,OAAO;YACL,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,GAAG,EAAE;YACjE,SAAS,EAAE,OAAO,CAAC,GAAG;YACtB,YAAY,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;YACtE,SAAS,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YACpD,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B;SACtD,CAAC;IACJ,CAAC;CACF,CAAA;AAVY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,qBAAU,GAAE;GACA,wBAAwB,CAUpC;AAED,MAAM,iBAAiB,GAAG,CAAC,KAAyB,EAAE,EAAE;IACtD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC,CAAC"}
{"version":3,"file":"gitzone-remote-node-service.js","sourceRoot":"","sources":["../../src/node/gitzone-remote-node-service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,oEAAmE;AAI5D,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IACnC,KAAK,CAAC,cAAc;QAClB,OAAO;YACL,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,GAAG,EAAE;YACjE,SAAS,EAAE,OAAO,CAAC,GAAG;YACtB,SAAS,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YACpD,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B;SACtD,CAAC;IACJ,CAAC;CACF,CAAA;AATY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,qBAAU,GAAE;GACA,wBAAwB,CASpC;AAED,MAAM,iBAAiB,GAAG,CAAC,KAAyB,EAAE,EAAE;IACtD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC,CAAC"}
@@ -5,7 +5,6 @@ export const GitZoneRemoteServer = Symbol('GitZoneRemoteServer');
export interface IGitZoneRemoteEnvironment {
workspacePath: string;
processId: number;
opencodePort?: number;
theiaPort?: number;
serverVersion?: string;
}
@@ -7,7 +7,6 @@ export class GitZoneRemoteNodeService implements IGitZoneRemoteServer {
return {
workspacePath: process.env.GITZONE_IDE_WORKSPACE || process.cwd(),
processId: process.pid,
opencodePort: parseOptionalPort(process.env.GITZONE_IDE_OPENCODE_PORT),
theiaPort: parseOptionalPort(process.env.THEIA_PORT),
serverVersion: process.env.GITZONE_IDE_SERVER_VERSION,
};