@wavexzore/sandbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +14 -0
- package/LICENSE +661 -0
- package/NOTICE +3 -0
- package/README.md +153 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/sandbox/cli/install.d.ts +5 -0
- package/dist/sandbox/cli/install.js +335 -0
- package/dist/sandbox/cli/local-store.d.ts +87 -0
- package/dist/sandbox/cli/local-store.js +604 -0
- package/dist/sandbox/cli/opencode-config.d.ts +25 -0
- package/dist/sandbox/cli/opencode-config.js +240 -0
- package/dist/sandbox/cli/path.d.ts +64 -0
- package/dist/sandbox/cli/path.js +127 -0
- package/dist/sandbox/cli/types.d.ts +145 -0
- package/dist/sandbox/cli/types.js +6 -0
- package/dist/sandbox/cli/wavexzore-sandbox.d.ts +65 -0
- package/dist/sandbox/cli/wavexzore-sandbox.js +577 -0
- package/dist/sandbox/core/cli-helper.d.ts +19 -0
- package/dist/sandbox/core/cli-helper.js +64 -0
- package/dist/sandbox/core/docker-archive-utils.d.ts +3 -0
- package/dist/sandbox/core/docker-archive-utils.js +50 -0
- package/dist/sandbox/core/docker-sandbox.d.ts +51 -0
- package/dist/sandbox/core/docker-sandbox.js +675 -0
- package/dist/sandbox/core/edit/filediff.d.ts +16 -0
- package/dist/sandbox/core/edit/filediff.js +21 -0
- package/dist/sandbox/core/edit/index.d.ts +5 -0
- package/dist/sandbox/core/edit/index.js +5 -0
- package/dist/sandbox/core/edit/line-endings.d.ts +4 -0
- package/dist/sandbox/core/edit/line-endings.js +10 -0
- package/dist/sandbox/core/edit/lock.d.ts +1 -0
- package/dist/sandbox/core/edit/lock.js +18 -0
- package/dist/sandbox/core/edit/replace.d.ts +10 -0
- package/dist/sandbox/core/edit/replace.js +14 -0
- package/dist/sandbox/core/edit/replacers.d.ts +15 -0
- package/dist/sandbox/core/edit/replacers.js +241 -0
- package/dist/sandbox/core/logger.d.ts +15 -0
- package/dist/sandbox/core/logger.js +59 -0
- package/dist/sandbox/core/lsp/client.d.ts +63 -0
- package/dist/sandbox/core/lsp/client.js +533 -0
- package/dist/sandbox/core/lsp/config.d.ts +13 -0
- package/dist/sandbox/core/lsp/config.js +36 -0
- package/dist/sandbox/core/lsp/diagnostics.d.ts +12 -0
- package/dist/sandbox/core/lsp/diagnostics.js +65 -0
- package/dist/sandbox/core/lsp/index.d.ts +4 -0
- package/dist/sandbox/core/lsp/index.js +4 -0
- package/dist/sandbox/core/lsp/language.d.ts +24 -0
- package/dist/sandbox/core/lsp/language.js +249 -0
- package/dist/sandbox/core/lsp/manager.d.ts +77 -0
- package/dist/sandbox/core/lsp/manager.js +237 -0
- package/dist/sandbox/core/lsp/tooling.d.ts +14 -0
- package/dist/sandbox/core/lsp/tooling.js +78 -0
- package/dist/sandbox/core/patch-parser.d.ts +23 -0
- package/dist/sandbox/core/patch-parser.js +248 -0
- package/dist/sandbox/core/path-map.d.ts +9 -0
- package/dist/sandbox/core/path-map.js +73 -0
- package/dist/sandbox/core/project-data-storage.d.ts +42 -0
- package/dist/sandbox/core/project-data-storage.js +167 -0
- package/dist/sandbox/core/read/binary.d.ts +4 -0
- package/dist/sandbox/core/read/binary.js +80 -0
- package/dist/sandbox/core/read/format.d.ts +38 -0
- package/dist/sandbox/core/read/format.js +85 -0
- package/dist/sandbox/core/read/index.d.ts +3 -0
- package/dist/sandbox/core/read/index.js +3 -0
- package/dist/sandbox/core/read/permissions.d.ts +7 -0
- package/dist/sandbox/core/read/permissions.js +13 -0
- package/dist/sandbox/core/session-manager.d.ts +29 -0
- package/dist/sandbox/core/session-manager.js +338 -0
- package/dist/sandbox/core/shell/config.d.ts +7 -0
- package/dist/sandbox/core/shell/config.js +82 -0
- package/dist/sandbox/core/shell/output.d.ts +35 -0
- package/dist/sandbox/core/shell/output.js +80 -0
- package/dist/sandbox/core/shell/parser.d.ts +7 -0
- package/dist/sandbox/core/shell/parser.js +122 -0
- package/dist/sandbox/core/shell/permissions.d.ts +13 -0
- package/dist/sandbox/core/shell/permissions.js +33 -0
- package/dist/sandbox/core/shell/workdir.d.ts +4 -0
- package/dist/sandbox/core/shell/workdir.js +19 -0
- package/dist/sandbox/core/stream-utils.d.ts +23 -0
- package/dist/sandbox/core/stream-utils.js +97 -0
- package/dist/sandbox/core/toast.d.ts +47 -0
- package/dist/sandbox/core/toast.js +73 -0
- package/dist/sandbox/core/types.d.ts +159 -0
- package/dist/sandbox/core/types.js +11 -0
- package/dist/sandbox/core/write/bom.d.ts +8 -0
- package/dist/sandbox/core/write/bom.js +15 -0
- package/dist/sandbox/core/write/config.d.ts +14 -0
- package/dist/sandbox/core/write/config.js +188 -0
- package/dist/sandbox/core/write/diagnostics.d.ts +19 -0
- package/dist/sandbox/core/write/diagnostics.js +120 -0
- package/dist/sandbox/core/write/diff.d.ts +7 -0
- package/dist/sandbox/core/write/diff.js +21 -0
- package/dist/sandbox/core/write/formatter.d.ts +16 -0
- package/dist/sandbox/core/write/formatter.js +51 -0
- package/dist/sandbox/core/write/index.d.ts +6 -0
- package/dist/sandbox/core/write/index.js +5 -0
- package/dist/sandbox/core/write/permissions.d.ts +13 -0
- package/dist/sandbox/core/write/permissions.js +19 -0
- package/dist/sandbox/core/write/pipeline.d.ts +48 -0
- package/dist/sandbox/core/write/pipeline.js +229 -0
- package/dist/sandbox/core/write/read-tracker.d.ts +13 -0
- package/dist/sandbox/core/write/read-tracker.js +30 -0
- package/dist/sandbox/git/host-git-manager.d.ts +40 -0
- package/dist/sandbox/git/host-git-manager.js +278 -0
- package/dist/sandbox/git/index.d.ts +5 -0
- package/dist/sandbox/git/index.js +5 -0
- package/dist/sandbox/git/sandbox-git-manager.d.ts +14 -0
- package/dist/sandbox/git/sandbox-git-manager.js +54 -0
- package/dist/sandbox/git/session-git-manager.d.ts +18 -0
- package/dist/sandbox/git/session-git-manager.js +85 -0
- package/dist/sandbox/index.d.ts +205 -0
- package/dist/sandbox/index.js +70 -0
- package/dist/sandbox/plugins/custom-tools.d.ts +203 -0
- package/dist/sandbox/plugins/custom-tools.js +15 -0
- package/dist/sandbox/plugins/session-events.d.ts +10 -0
- package/dist/sandbox/plugins/session-events.js +56 -0
- package/dist/sandbox/plugins/system-transform.d.ts +10 -0
- package/dist/sandbox/plugins/system-transform.js +23 -0
- package/dist/sandbox/tools/bash-output.d.ts +17 -0
- package/dist/sandbox/tools/bash-output.js +35 -0
- package/dist/sandbox/tools/bash-status.d.ts +13 -0
- package/dist/sandbox/tools/bash-status.js +29 -0
- package/dist/sandbox/tools/bash-stop.d.ts +13 -0
- package/dist/sandbox/tools/bash-stop.js +28 -0
- package/dist/sandbox/tools/bash.d.ts +26 -0
- package/dist/sandbox/tools/bash.js +120 -0
- package/dist/sandbox/tools/edit.d.ts +20 -0
- package/dist/sandbox/tools/edit.js +87 -0
- package/dist/sandbox/tools/get-preview-url.d.ts +17 -0
- package/dist/sandbox/tools/get-preview-url.js +16 -0
- package/dist/sandbox/tools/glob.d.ts +17 -0
- package/dist/sandbox/tools/glob.js +23 -0
- package/dist/sandbox/tools/grep.d.ts +17 -0
- package/dist/sandbox/tools/grep.js +23 -0
- package/dist/sandbox/tools/ls.d.ts +17 -0
- package/dist/sandbox/tools/ls.js +21 -0
- package/dist/sandbox/tools/lsp.d.ts +41 -0
- package/dist/sandbox/tools/lsp.js +198 -0
- package/dist/sandbox/tools/multiedit.d.ts +24 -0
- package/dist/sandbox/tools/multiedit.js +83 -0
- package/dist/sandbox/tools/patch.d.ts +14 -0
- package/dist/sandbox/tools/patch.js +260 -0
- package/dist/sandbox/tools/read.d.ts +22 -0
- package/dist/sandbox/tools/read.js +105 -0
- package/dist/sandbox/tools/write.d.ts +16 -0
- package/dist/sandbox/tools/write.js +27 -0
- package/dist/sandbox/tools.d.ts +200 -0
- package/dist/sandbox/tools.js +43 -0
- package/package.json +55 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { hostLspManager, formatLspDiagnosticOutput, formatLspUnavailable } from '../core/lsp/index.js';
|
|
5
|
+
const operations = [
|
|
6
|
+
'diagnostics',
|
|
7
|
+
'hover',
|
|
8
|
+
'documentSymbol',
|
|
9
|
+
'workspaceSymbol',
|
|
10
|
+
'references',
|
|
11
|
+
'findReferences',
|
|
12
|
+
'definition',
|
|
13
|
+
'goToDefinition',
|
|
14
|
+
'implementation',
|
|
15
|
+
'goToImplementation',
|
|
16
|
+
'prepareCallHierarchy',
|
|
17
|
+
'incomingCalls',
|
|
18
|
+
'outgoingCalls',
|
|
19
|
+
'status',
|
|
20
|
+
];
|
|
21
|
+
const IMPORTANT_WORKSPACE_SYMBOL_KINDS = new Set([5, 6, 10, 11, 12, 13, 14, 23]);
|
|
22
|
+
function requireFilePath(args) {
|
|
23
|
+
if (!args.filePath)
|
|
24
|
+
throw new Error(`filePath is required for ${args.operation}`);
|
|
25
|
+
return args.filePath;
|
|
26
|
+
}
|
|
27
|
+
function requirePosition(args) {
|
|
28
|
+
if (args.line == null || args.character == null) {
|
|
29
|
+
throw new Error(`line and character are required for ${args.operation}`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
line: Math.max(0, args.line - 1),
|
|
33
|
+
character: Math.max(0, args.character - 1),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function parseFileUri(value) {
|
|
37
|
+
if (!value.startsWith('file://'))
|
|
38
|
+
return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return fileURLToPath(value);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function serializeLspResult(value, paths) {
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map((item) => serializeLspResult(item, paths));
|
|
49
|
+
if (!value || typeof value !== 'object')
|
|
50
|
+
return value;
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
53
|
+
if ((key === 'uri' || key === 'targetUri') && typeof raw === 'string') {
|
|
54
|
+
const hostPath = parseFileUri(raw);
|
|
55
|
+
result[key] = raw;
|
|
56
|
+
if (hostPath)
|
|
57
|
+
result[key === 'uri' ? 'path' : 'targetPath'] = paths.toRelative(hostPath);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
result[key] = serializeLspResult(raw, paths);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function json(value) {
|
|
65
|
+
return JSON.stringify(value, null, 2);
|
|
66
|
+
}
|
|
67
|
+
async function requestLspPermission(input) {
|
|
68
|
+
if (typeof input.ctx.ask !== 'function')
|
|
69
|
+
return;
|
|
70
|
+
await input.ctx.ask({
|
|
71
|
+
permission: 'lsp',
|
|
72
|
+
patterns: [input.relativePath ?? '*'],
|
|
73
|
+
always: ['*'],
|
|
74
|
+
metadata: {
|
|
75
|
+
operation: input.operation,
|
|
76
|
+
filepath: input.hostPath,
|
|
77
|
+
containerPath: input.containerPath,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function limitWorkspaceSymbols(result) {
|
|
82
|
+
const important = result.filter((item) => {
|
|
83
|
+
if (!item || typeof item !== 'object')
|
|
84
|
+
return false;
|
|
85
|
+
const kind = item.kind;
|
|
86
|
+
return typeof kind === 'number' && IMPORTANT_WORKSPACE_SYMBOL_KINDS.has(kind);
|
|
87
|
+
});
|
|
88
|
+
return (important.length ? important : result).slice(0, 10);
|
|
89
|
+
}
|
|
90
|
+
export const lspTool = (sessionManager, projectId, worktree, pluginCtx, mapPath) => ({
|
|
91
|
+
description: 'Real host Language Server Protocol operations for mapped Docker sandbox project files. Uses persistent host LSP servers (typescript-language-server, gopls, pylsp) and never falls back to lint commands.',
|
|
92
|
+
args: {
|
|
93
|
+
operation: z.enum(operations).describe('The LSP operation to perform'),
|
|
94
|
+
filePath: z.string().optional().describe('The absolute, relative, or container path to the file'),
|
|
95
|
+
line: z.number().optional().describe('The line number (1-based)'),
|
|
96
|
+
character: z.number().optional().describe('The character offset (1-based)'),
|
|
97
|
+
query: z.string().optional().describe('Query for workspaceSymbol'),
|
|
98
|
+
},
|
|
99
|
+
async execute(args, ctx) {
|
|
100
|
+
await sessionManager.getSandbox(ctx.sessionID, projectId, worktree, pluginCtx);
|
|
101
|
+
try {
|
|
102
|
+
if (args.operation === 'status')
|
|
103
|
+
return json(await hostLspManager.status({ sessionID: ctx.sessionID }));
|
|
104
|
+
if (args.operation === 'workspaceSymbol') {
|
|
105
|
+
let hostPath;
|
|
106
|
+
let relativePath;
|
|
107
|
+
let containerPath;
|
|
108
|
+
if (args.filePath) {
|
|
109
|
+
containerPath = mapPath.toContainer(args.filePath);
|
|
110
|
+
hostPath = mapPath.toHost(containerPath);
|
|
111
|
+
relativePath = mapPath.toRelative(hostPath);
|
|
112
|
+
if (!mapPath.isMapped(hostPath))
|
|
113
|
+
return `Host LSP only supports files under ${mapPath.worktree}`;
|
|
114
|
+
if (!existsSync(hostPath))
|
|
115
|
+
return `Host file not found for LSP operation: ${relativePath}`;
|
|
116
|
+
await requestLspPermission({ ctx, operation: args.operation, relativePath, hostPath, containerPath });
|
|
117
|
+
await hostLspManager.touchFile({ sessionID: ctx.sessionID, worktree, filePath: hostPath, diagnostics: 'document' });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
await requestLspPermission({ ctx, operation: args.operation });
|
|
121
|
+
}
|
|
122
|
+
const result = limitWorkspaceSymbols(await hostLspManager.workspaceSymbol({
|
|
123
|
+
sessionID: ctx.sessionID,
|
|
124
|
+
worktree,
|
|
125
|
+
query: args.query ?? '',
|
|
126
|
+
}));
|
|
127
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : 'No workspace symbols found from active host LSP clients';
|
|
128
|
+
}
|
|
129
|
+
const filePath = requireFilePath(args);
|
|
130
|
+
const containerPath = mapPath.toContainer(filePath);
|
|
131
|
+
const hostPath = mapPath.toHost(containerPath);
|
|
132
|
+
const relativePath = mapPath.toRelative(hostPath);
|
|
133
|
+
if (!mapPath.isMapped(hostPath))
|
|
134
|
+
return `Host LSP only supports files under ${mapPath.worktree}`;
|
|
135
|
+
if (!existsSync(hostPath))
|
|
136
|
+
return `Host file not found for LSP operation: ${relativePath}`;
|
|
137
|
+
await requestLspPermission({ ctx, operation: args.operation, relativePath, hostPath, containerPath });
|
|
138
|
+
if (!(await hostLspManager.hasClients({ sessionID: ctx.sessionID, worktree, filePath: hostPath }))) {
|
|
139
|
+
return `No host LSP server configured for ${relativePath}`;
|
|
140
|
+
}
|
|
141
|
+
if (args.operation === 'diagnostics') {
|
|
142
|
+
const touched = await hostLspManager.touchFile({
|
|
143
|
+
sessionID: ctx.sessionID,
|
|
144
|
+
worktree,
|
|
145
|
+
filePath: hostPath,
|
|
146
|
+
diagnostics: 'document',
|
|
147
|
+
});
|
|
148
|
+
if (touched === 0)
|
|
149
|
+
return `No host LSP server configured for ${relativePath}`;
|
|
150
|
+
const diagnostics = hostLspManager.diagnosticsForFile({ sessionID: ctx.sessionID, worktree, filePath: hostPath });
|
|
151
|
+
return diagnostics.length ? formatLspDiagnosticOutput(relativePath, diagnostics) : `No diagnostics found for ${relativePath}`;
|
|
152
|
+
}
|
|
153
|
+
if (args.operation === 'documentSymbol') {
|
|
154
|
+
const result = await hostLspManager.documentSymbol({ sessionID: ctx.sessionID, worktree, filePath: hostPath });
|
|
155
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No document symbols found for ${relativePath}`;
|
|
156
|
+
}
|
|
157
|
+
const position = requirePosition(args);
|
|
158
|
+
switch (args.operation) {
|
|
159
|
+
case 'hover': {
|
|
160
|
+
const result = await hostLspManager.hover({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
161
|
+
return result ? json(serializeLspResult(result, mapPath)) : `No hover result for ${relativePath}:${args.line}:${args.character}`;
|
|
162
|
+
}
|
|
163
|
+
case 'definition':
|
|
164
|
+
case 'goToDefinition': {
|
|
165
|
+
const result = await hostLspManager.definition({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
166
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No definition found for ${relativePath}:${args.line}:${args.character}`;
|
|
167
|
+
}
|
|
168
|
+
case 'references':
|
|
169
|
+
case 'findReferences': {
|
|
170
|
+
const result = await hostLspManager.references({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
171
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No references found for ${relativePath}:${args.line}:${args.character}`;
|
|
172
|
+
}
|
|
173
|
+
case 'implementation':
|
|
174
|
+
case 'goToImplementation': {
|
|
175
|
+
const result = await hostLspManager.implementation({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
176
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No implementation found for ${relativePath}:${args.line}:${args.character}`;
|
|
177
|
+
}
|
|
178
|
+
case 'prepareCallHierarchy': {
|
|
179
|
+
const result = await hostLspManager.prepareCallHierarchy({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
180
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No call hierarchy item found for ${relativePath}:${args.line}:${args.character}`;
|
|
181
|
+
}
|
|
182
|
+
case 'incomingCalls': {
|
|
183
|
+
const result = await hostLspManager.incomingCalls({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
184
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No incoming calls found for ${relativePath}:${args.line}:${args.character}`;
|
|
185
|
+
}
|
|
186
|
+
case 'outgoingCalls': {
|
|
187
|
+
const result = await hostLspManager.outgoingCalls({ sessionID: ctx.sessionID, worktree, filePath: hostPath, position });
|
|
188
|
+
return result.length ? json(serializeLspResult(result, mapPath)) : `No outgoing calls found for ${relativePath}:${args.line}:${args.character}`;
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return `Unknown operation: ${args.operation}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return formatLspUnavailable(error);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
3
|
+
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
4
|
+
import type { SandboxSessionManager } from '../core/session-manager.js';
|
|
5
|
+
import type { PathMapper } from '../core/path-map.js';
|
|
6
|
+
export declare const multieditTool: (sessionManager: SandboxSessionManager, projectId: string, worktree: string, pluginCtx: PluginInput, mapPath: PathMapper) => {
|
|
7
|
+
description: string;
|
|
8
|
+
args: {
|
|
9
|
+
filePath: z.ZodString;
|
|
10
|
+
edits: z.ZodArray<z.ZodObject<{
|
|
11
|
+
oldString: z.ZodString;
|
|
12
|
+
newString: z.ZodString;
|
|
13
|
+
replaceAll: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
}, z.core.$strip>>;
|
|
15
|
+
};
|
|
16
|
+
execute(args: {
|
|
17
|
+
filePath: string;
|
|
18
|
+
edits: Array<{
|
|
19
|
+
oldString: string;
|
|
20
|
+
newString: string;
|
|
21
|
+
replaceAll?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
}, ctx: ToolContext): Promise<string>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { decodeUtf8, sandboxWriteFile, splitBom } from '../core/write/index.js';
|
|
3
|
+
import { applyOpenCodeEdit, createFileDiff, withFileLock } from '../core/edit/index.js';
|
|
4
|
+
function metadataDiagnostics(diagnostics) {
|
|
5
|
+
return {
|
|
6
|
+
current: diagnostics.current,
|
|
7
|
+
other: diagnostics.other,
|
|
8
|
+
unavailable: diagnostics.unavailable,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export const multieditTool = (sessionManager, projectId, worktree, pluginCtx, mapPath) => ({
|
|
12
|
+
description: 'Applies multiple OpenCode-style edits to a file in Docker sandbox atomically with one permission prompt, formatter, and host LSP diagnostics',
|
|
13
|
+
args: {
|
|
14
|
+
filePath: z.string(),
|
|
15
|
+
edits: z.array(z.object({
|
|
16
|
+
oldString: z.string(),
|
|
17
|
+
newString: z.string(),
|
|
18
|
+
replaceAll: z.boolean().optional().describe('Replace all occurrences of oldString for this edit'),
|
|
19
|
+
})),
|
|
20
|
+
},
|
|
21
|
+
async execute(args, ctx) {
|
|
22
|
+
if (args.edits.length === 0)
|
|
23
|
+
throw new Error('At least one edit is required');
|
|
24
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree, pluginCtx);
|
|
25
|
+
const containerPath = mapPath.toContainer(args.filePath);
|
|
26
|
+
const hostPath = mapPath.toHost(containerPath);
|
|
27
|
+
return withFileLock(hostPath, async () => {
|
|
28
|
+
const buffer = await sandbox.fs.downloadFile(containerPath).catch(() => undefined);
|
|
29
|
+
if (!buffer)
|
|
30
|
+
throw new Error(`File ${args.filePath} not found`);
|
|
31
|
+
const oldContent = splitBom(decodeUtf8(buffer)).text;
|
|
32
|
+
let content = oldContent;
|
|
33
|
+
for (const [index, edit] of args.edits.entries()) {
|
|
34
|
+
try {
|
|
35
|
+
content = applyOpenCodeEdit({
|
|
36
|
+
content,
|
|
37
|
+
oldString: edit.oldString,
|
|
38
|
+
newString: edit.newString,
|
|
39
|
+
replaceAll: edit.replaceAll,
|
|
40
|
+
}).content;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`Edit ${index + 1} failed: ${error?.message || error}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const result = await sandboxWriteFile({
|
|
47
|
+
ctx,
|
|
48
|
+
sandbox,
|
|
49
|
+
sessionID: ctx.sessionID,
|
|
50
|
+
worktree,
|
|
51
|
+
paths: mapPath,
|
|
52
|
+
filePath: args.filePath,
|
|
53
|
+
content,
|
|
54
|
+
operation: 'multiedit',
|
|
55
|
+
oldContentBuffer: buffer,
|
|
56
|
+
oldExists: true,
|
|
57
|
+
setMetadata: false,
|
|
58
|
+
});
|
|
59
|
+
const finalText = splitBom(result.finalContent).text;
|
|
60
|
+
const filediff = createFileDiff({ file: result.hostPath, oldContent, newContent: finalText });
|
|
61
|
+
if (typeof ctx.metadata === 'function') {
|
|
62
|
+
ctx.metadata({
|
|
63
|
+
title: result.relativePath,
|
|
64
|
+
metadata: {
|
|
65
|
+
operation: 'multiedit',
|
|
66
|
+
diagnostics: metadataDiagnostics(result.diagnostics),
|
|
67
|
+
filepath: result.hostPath,
|
|
68
|
+
containerPath: result.containerPath,
|
|
69
|
+
exists: result.exists,
|
|
70
|
+
diff: filediff.patch,
|
|
71
|
+
filediff,
|
|
72
|
+
formatted: result.formatted,
|
|
73
|
+
edits: args.edits.length,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
let output = `Applied ${args.edits.length} edits successfully.`;
|
|
78
|
+
if (result.diagnostics.output)
|
|
79
|
+
output += '\n\n' + result.diagnostics.output;
|
|
80
|
+
return output;
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
3
|
+
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
4
|
+
import type { SandboxSessionManager } from '../core/session-manager.js';
|
|
5
|
+
import type { PathMapper } from '../core/path-map.js';
|
|
6
|
+
export declare const patchTool: (sessionManager: SandboxSessionManager, projectId: string, worktree: string, pluginCtx: PluginInput, mapPath: PathMapper) => {
|
|
7
|
+
description: string;
|
|
8
|
+
args: {
|
|
9
|
+
patchText: z.ZodString;
|
|
10
|
+
};
|
|
11
|
+
execute(args: {
|
|
12
|
+
patchText: string;
|
|
13
|
+
}, ctx: ToolContext): Promise<string>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { deriveNewContents, parsePatch } from '../core/patch-parser.js';
|
|
3
|
+
import { decodeUtf8, sandboxDeleteFile, sandboxWriteFile, splitBom } from '../core/write/index.js';
|
|
4
|
+
import { createWriteDiff } from '../core/write/diff.js';
|
|
5
|
+
import { createFileDiff } from '../core/edit/index.js';
|
|
6
|
+
const APPLY_PATCH_DESCRIPTION = `Use this tool to apply a patch to files in the Docker sandbox. The patch format:
|
|
7
|
+
|
|
8
|
+
*** Begin Patch
|
|
9
|
+
[ one or more file sections ]
|
|
10
|
+
*** End Patch
|
|
11
|
+
|
|
12
|
+
Each section starts with one of:
|
|
13
|
+
*** Add File: <path> - create a new file (+ lines for content)
|
|
14
|
+
*** Delete File: <path> - remove a file
|
|
15
|
+
*** Update File: <path> - patch an existing file (optionally with *** Move to: <new_path>)
|
|
16
|
+
|
|
17
|
+
Update sections use @@ context markers and +/-/ space prefixed lines.
|
|
18
|
+
You must prefix new lines with + even when creating a new file.`;
|
|
19
|
+
function metadataDiagnostics(diagnostics) {
|
|
20
|
+
return {
|
|
21
|
+
current: diagnostics.current,
|
|
22
|
+
other: diagnostics.other,
|
|
23
|
+
unavailable: diagnostics.unavailable,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function downloadOptional(sandbox, containerPath) {
|
|
27
|
+
try {
|
|
28
|
+
const buffer = await sandbox.fs.downloadFile(containerPath);
|
|
29
|
+
return { exists: true, buffer, text: splitBom(decodeUtf8(buffer)).text };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { exists: false, buffer: undefined, text: '' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function ensureTrailingNewline(content) {
|
|
36
|
+
return content.length === 0 || content.endsWith('\n') ? content : `${content}\n`;
|
|
37
|
+
}
|
|
38
|
+
async function requestPatchPermission(ctx, changes) {
|
|
39
|
+
if (typeof ctx.ask !== 'function')
|
|
40
|
+
return;
|
|
41
|
+
const patterns = [
|
|
42
|
+
...new Set(changes.flatMap((change) => [change.sourceRelativePath, change.targetRelativePath]).filter(Boolean)),
|
|
43
|
+
];
|
|
44
|
+
const diff = changes.map((change) => change.diff).join('\n');
|
|
45
|
+
await ctx.ask({
|
|
46
|
+
permission: 'edit',
|
|
47
|
+
patterns,
|
|
48
|
+
always: ['*'],
|
|
49
|
+
metadata: {
|
|
50
|
+
filepath: changes.length === 1 ? changes[0].targetHostPath : undefined,
|
|
51
|
+
diff,
|
|
52
|
+
files: changes.map((change) => ({
|
|
53
|
+
operation: change.operation,
|
|
54
|
+
filepath: change.targetHostPath,
|
|
55
|
+
containerPath: change.targetContainerPath,
|
|
56
|
+
sourcePath: change.sourceHostPath,
|
|
57
|
+
sourceContainerPath: change.sourceContainerPath,
|
|
58
|
+
diff: change.diff,
|
|
59
|
+
filediff: change.filediff,
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export const patchTool = (sessionManager, projectId, worktree, pluginCtx, mapPath) => ({
|
|
65
|
+
description: APPLY_PATCH_DESCRIPTION,
|
|
66
|
+
args: {
|
|
67
|
+
patchText: z.string().describe('The full patch text that describes all changes to be made'),
|
|
68
|
+
},
|
|
69
|
+
async execute(args, ctx) {
|
|
70
|
+
if (!args.patchText)
|
|
71
|
+
return 'Error: patchText is required';
|
|
72
|
+
let hunks;
|
|
73
|
+
try {
|
|
74
|
+
const parseResult = parsePatch(args.patchText);
|
|
75
|
+
hunks = parseResult.hunks;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return `apply_patch verification failed: ${error.message || error}`;
|
|
79
|
+
}
|
|
80
|
+
if (hunks.length === 0) {
|
|
81
|
+
const normalized = args.patchText.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
82
|
+
if (normalized === '*** Begin Patch\n*** End Patch')
|
|
83
|
+
return 'Error: patch rejected: empty patch';
|
|
84
|
+
return 'apply_patch verification failed: no hunks found';
|
|
85
|
+
}
|
|
86
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree, pluginCtx);
|
|
87
|
+
const changes = [];
|
|
88
|
+
for (const hunk of hunks) {
|
|
89
|
+
const sourceContainerPath = mapPath.toContainer(hunk.path);
|
|
90
|
+
const sourceHostPath = mapPath.toHost(sourceContainerPath);
|
|
91
|
+
const sourceRelativePath = mapPath.toRelative(sourceHostPath);
|
|
92
|
+
if (hunk.type === 'add') {
|
|
93
|
+
const existing = await downloadOptional(sandbox, sourceContainerPath);
|
|
94
|
+
if (existing.exists)
|
|
95
|
+
return `apply_patch verification failed: File already exists: ${hunk.path}`;
|
|
96
|
+
const newContent = ensureTrailingNewline(hunk.contents);
|
|
97
|
+
const diff = createWriteDiff({ oldPath: sourceRelativePath, newPath: sourceRelativePath, oldContent: '', newContent });
|
|
98
|
+
changes.push({
|
|
99
|
+
operation: 'add',
|
|
100
|
+
sourcePath: hunk.path,
|
|
101
|
+
targetPath: hunk.path,
|
|
102
|
+
sourceContainerPath,
|
|
103
|
+
targetContainerPath: sourceContainerPath,
|
|
104
|
+
sourceHostPath,
|
|
105
|
+
targetHostPath: sourceHostPath,
|
|
106
|
+
sourceRelativePath,
|
|
107
|
+
targetRelativePath: sourceRelativePath,
|
|
108
|
+
oldContent: '',
|
|
109
|
+
newContent,
|
|
110
|
+
diff,
|
|
111
|
+
filediff: createFileDiff({ file: sourceHostPath, oldContent: '', newContent }),
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const existing = await downloadOptional(sandbox, sourceContainerPath);
|
|
116
|
+
if (!existing.exists) {
|
|
117
|
+
return `apply_patch verification failed: Failed to read file to ${hunk.type}: ${hunk.path}`;
|
|
118
|
+
}
|
|
119
|
+
if (hunk.type === 'delete') {
|
|
120
|
+
const diff = createWriteDiff({ oldPath: sourceRelativePath, newPath: sourceRelativePath, oldContent: existing.text, newContent: '' });
|
|
121
|
+
changes.push({
|
|
122
|
+
operation: 'delete',
|
|
123
|
+
sourcePath: hunk.path,
|
|
124
|
+
targetPath: hunk.path,
|
|
125
|
+
sourceContainerPath,
|
|
126
|
+
targetContainerPath: sourceContainerPath,
|
|
127
|
+
sourceHostPath,
|
|
128
|
+
targetHostPath: sourceHostPath,
|
|
129
|
+
sourceRelativePath,
|
|
130
|
+
targetRelativePath: sourceRelativePath,
|
|
131
|
+
oldBuffer: existing.buffer,
|
|
132
|
+
oldContent: existing.text,
|
|
133
|
+
newContent: '',
|
|
134
|
+
diff,
|
|
135
|
+
filediff: createFileDiff({ file: sourceHostPath, oldContent: existing.text, newContent: '' }),
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let newContent;
|
|
140
|
+
try {
|
|
141
|
+
newContent = deriveNewContents(hunk.path, existing.text, hunk.chunks);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return `apply_patch verification failed: ${error.message || error}`;
|
|
145
|
+
}
|
|
146
|
+
const targetPath = hunk.move_path ?? hunk.path;
|
|
147
|
+
const targetContainerPath = mapPath.toContainer(targetPath);
|
|
148
|
+
const targetHostPath = mapPath.toHost(targetContainerPath);
|
|
149
|
+
const targetRelativePath = mapPath.toRelative(targetHostPath);
|
|
150
|
+
if (hunk.move_path) {
|
|
151
|
+
const targetExisting = await downloadOptional(sandbox, targetContainerPath);
|
|
152
|
+
if (targetExisting.exists)
|
|
153
|
+
return `apply_patch verification failed: Move target already exists: ${hunk.move_path}`;
|
|
154
|
+
}
|
|
155
|
+
const diff = createWriteDiff({
|
|
156
|
+
oldPath: sourceRelativePath,
|
|
157
|
+
newPath: targetRelativePath,
|
|
158
|
+
oldContent: existing.text,
|
|
159
|
+
newContent,
|
|
160
|
+
});
|
|
161
|
+
changes.push({
|
|
162
|
+
operation: hunk.move_path ? 'move' : 'update',
|
|
163
|
+
sourcePath: hunk.path,
|
|
164
|
+
targetPath,
|
|
165
|
+
sourceContainerPath,
|
|
166
|
+
targetContainerPath,
|
|
167
|
+
sourceHostPath,
|
|
168
|
+
targetHostPath,
|
|
169
|
+
sourceRelativePath,
|
|
170
|
+
targetRelativePath,
|
|
171
|
+
oldBuffer: existing.buffer,
|
|
172
|
+
oldContent: existing.text,
|
|
173
|
+
newContent,
|
|
174
|
+
diff,
|
|
175
|
+
filediff: { ...createFileDiff({ file: targetHostPath, oldContent: existing.text, newContent }), patch: diff },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
await requestPatchPermission(ctx, changes);
|
|
179
|
+
const summaryLines = [];
|
|
180
|
+
const diagnosticsLines = [];
|
|
181
|
+
const metadataFiles = [];
|
|
182
|
+
for (const change of changes) {
|
|
183
|
+
if (change.operation === 'delete') {
|
|
184
|
+
const result = await sandboxDeleteFile({
|
|
185
|
+
ctx,
|
|
186
|
+
sandbox,
|
|
187
|
+
sessionID: ctx.sessionID,
|
|
188
|
+
worktree,
|
|
189
|
+
paths: mapPath,
|
|
190
|
+
filePath: change.sourcePath,
|
|
191
|
+
skipPermission: true,
|
|
192
|
+
setMetadata: false,
|
|
193
|
+
});
|
|
194
|
+
metadataFiles.push({ operation: 'delete', ...result, diff: change.diff, filediff: change.filediff });
|
|
195
|
+
summaryLines.push(`D ${change.sourcePath}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const result = await sandboxWriteFile({
|
|
199
|
+
ctx,
|
|
200
|
+
sandbox,
|
|
201
|
+
sessionID: ctx.sessionID,
|
|
202
|
+
worktree,
|
|
203
|
+
paths: mapPath,
|
|
204
|
+
filePath: change.targetPath,
|
|
205
|
+
content: change.newContent,
|
|
206
|
+
operation: 'patch',
|
|
207
|
+
oldContentBuffer: change.oldBuffer,
|
|
208
|
+
oldExists: change.operation !== 'add',
|
|
209
|
+
diffOldPath: change.sourcePath,
|
|
210
|
+
skipPermission: true,
|
|
211
|
+
setMetadata: false,
|
|
212
|
+
});
|
|
213
|
+
if (result.diagnostics.output)
|
|
214
|
+
diagnosticsLines.push(result.diagnostics.output);
|
|
215
|
+
if (change.operation === 'move' && change.sourceHostPath !== change.targetHostPath) {
|
|
216
|
+
await sandboxDeleteFile({
|
|
217
|
+
ctx,
|
|
218
|
+
sandbox,
|
|
219
|
+
sessionID: ctx.sessionID,
|
|
220
|
+
worktree,
|
|
221
|
+
paths: mapPath,
|
|
222
|
+
filePath: change.sourcePath,
|
|
223
|
+
skipPermission: true,
|
|
224
|
+
setMetadata: false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
metadataFiles.push({
|
|
228
|
+
operation: change.operation,
|
|
229
|
+
filepath: result.hostPath,
|
|
230
|
+
containerPath: result.containerPath,
|
|
231
|
+
sourcePath: change.sourceHostPath,
|
|
232
|
+
sourceContainerPath: change.sourceContainerPath,
|
|
233
|
+
exists: result.exists,
|
|
234
|
+
diff: change.diff,
|
|
235
|
+
filediff: change.filediff,
|
|
236
|
+
diagnostics: metadataDiagnostics(result.diagnostics),
|
|
237
|
+
formatted: result.formatted,
|
|
238
|
+
});
|
|
239
|
+
summaryLines.push(change.operation === 'add'
|
|
240
|
+
? `A ${change.targetPath}`
|
|
241
|
+
: change.operation === 'move'
|
|
242
|
+
? `M ${change.sourcePath} → ${change.targetPath}`
|
|
243
|
+
: `M ${change.targetPath}`);
|
|
244
|
+
}
|
|
245
|
+
if (typeof ctx.metadata === 'function') {
|
|
246
|
+
ctx.metadata({
|
|
247
|
+
title: `apply_patch (${summaryLines.length} files)`,
|
|
248
|
+
metadata: {
|
|
249
|
+
diff: changes.map((change) => change.diff).join('\n'),
|
|
250
|
+
files: metadataFiles,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
let output = `Success. Updated the following files:\n${summaryLines.join('\n')}`;
|
|
255
|
+
const uniqueDiagnostics = [...new Set(diagnosticsLines.filter(Boolean))];
|
|
256
|
+
if (uniqueDiagnostics.length > 0)
|
|
257
|
+
output += '\n\n' + uniqueDiagnostics.join('\n\n');
|
|
258
|
+
return output;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
7
|
+
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
8
|
+
import type { SandboxSessionManager } from '../core/session-manager.js';
|
|
9
|
+
import type { PathMapper } from '../core/path-map.js';
|
|
10
|
+
export declare const readTool: (sessionManager: SandboxSessionManager, projectId: string, worktree: string, pluginCtx: PluginInput, mapPath: PathMapper) => {
|
|
11
|
+
description: string;
|
|
12
|
+
args: {
|
|
13
|
+
filePath: z.ZodString;
|
|
14
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
};
|
|
17
|
+
execute(args: {
|
|
18
|
+
filePath: string;
|
|
19
|
+
offset?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}, ctx: ToolContext): Promise<string>;
|
|
22
|
+
};
|