@jamesmurdza/opencode-daytona 0.1.13 → 0.1.14
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/.opencode/plugin/daytona/core/logger.ts +50 -0
- package/.opencode/plugin/daytona/core/project-data-storage.ts +154 -0
- package/.opencode/plugin/daytona/core/session-manager.ts +170 -0
- package/.opencode/plugin/daytona/core/types.ts +68 -0
- package/.opencode/plugin/daytona/git/host-git-manager.ts +94 -0
- package/.opencode/plugin/daytona/git/index.ts +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +51 -0
- package/.opencode/plugin/daytona/git/session-git-manager.ts +65 -0
- package/.opencode/plugin/daytona/index.ts +50 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.ts +20 -0
- package/.opencode/plugin/daytona/plugins/index.ts +8 -0
- package/.opencode/plugin/daytona/plugins/session-cleanup.ts +22 -0
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.ts +30 -0
- package/.opencode/plugin/daytona/plugins/system-transform.ts +28 -0
- package/.opencode/plugin/daytona/tools/bash.ts +32 -0
- package/.opencode/plugin/daytona/tools/edit.ts +21 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.ts +15 -0
- package/.opencode/plugin/daytona/tools/glob.ts +16 -0
- package/.opencode/plugin/daytona/tools/grep.ts +16 -0
- package/.opencode/plugin/daytona/tools/ls.ts +18 -0
- package/.opencode/plugin/daytona/tools/lsp.ts +15 -0
- package/.opencode/plugin/daytona/tools/multiedit.ts +32 -0
- package/.opencode/plugin/daytona/tools/patch.ts +21 -0
- package/.opencode/plugin/daytona/tools/read.ts +16 -0
- package/.opencode/plugin/daytona/tools/write.ts +16 -0
- package/.opencode/plugin/daytona/tools.ts +33 -0
- package/.opencode/plugin/{index.js → index.ts} +1 -2
- package/README.md +192 -0
- package/package.json +3 -12
- package/.opencode/plugin/daytona/index.d.ts +0 -25
- package/.opencode/plugin/daytona/index.d.ts.map +0 -1
- package/.opencode/plugin/daytona/index.js +0 -36
- package/.opencode/plugin/daytona/index.js.map +0 -1
- package/.opencode/plugin/daytona/logger.d.ts +0 -13
- package/.opencode/plugin/daytona/logger.d.ts.map +0 -1
- package/.opencode/plugin/daytona/logger.js +0 -26
- package/.opencode/plugin/daytona/logger.js.map +0 -1
- package/.opencode/plugin/daytona/plugins.d.ts +0 -22
- package/.opencode/plugin/daytona/plugins.d.ts.map +0 -1
- package/.opencode/plugin/daytona/plugins.js +0 -60
- package/.opencode/plugin/daytona/plugins.js.map +0 -1
- package/.opencode/plugin/daytona/session-manager.d.ts +0 -47
- package/.opencode/plugin/daytona/session-manager.d.ts.map +0 -1
- package/.opencode/plugin/daytona/session-manager.js +0 -165
- package/.opencode/plugin/daytona/session-manager.js.map +0 -1
- package/.opencode/plugin/daytona/tools.d.ts +0 -185
- package/.opencode/plugin/daytona/tools.d.ts.map +0 -1
- package/.opencode/plugin/daytona/tools.js +0 -232
- package/.opencode/plugin/daytona/tools.js.map +0 -1
- package/.opencode/plugin/daytona/types.d.ts +0 -32
- package/.opencode/plugin/daytona/types.d.ts.map +0 -1
- package/.opencode/plugin/daytona/types.js +0 -10
- package/.opencode/plugin/daytona/types.js.map +0 -1
- package/.opencode/plugin/index.d.ts +0 -6
- package/.opencode/plugin/index.d.ts.map +0 -1
- package/.opencode/plugin/index.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Plugin: Daytona Sandbox Integration
|
|
3
|
+
*
|
|
4
|
+
* OpenCode plugins extend the AI coding assistant by adding custom tools, handling events,
|
|
5
|
+
* and modifying behavior. Plugins are TypeScript/JavaScript modules that export functions
|
|
6
|
+
* which return hooks for various lifecycle events.
|
|
7
|
+
*
|
|
8
|
+
* This plugin integrates Daytona sandboxes with OpenCode, providing isolated development
|
|
9
|
+
* environments for each session. It adds custom tools for file operations, command execution,
|
|
10
|
+
* and search within sandboxes, and automatically cleans up resources when sessions end.
|
|
11
|
+
*
|
|
12
|
+
* Learn more: https://opencode.ai/docs/plugins/
|
|
13
|
+
*
|
|
14
|
+
* Daytona Sandbox Integration Tools
|
|
15
|
+
*
|
|
16
|
+
* Requires:
|
|
17
|
+
* - npm install @daytonaio/sdk
|
|
18
|
+
* - Environment: DAYTONA_API_KEY
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { join } from 'path'
|
|
22
|
+
import { xdgData } from 'xdg-basedir'
|
|
23
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
24
|
+
|
|
25
|
+
// Import modules
|
|
26
|
+
import { setLogFilePath } from './core/logger'
|
|
27
|
+
import { DaytonaSessionManager } from './core/session-manager'
|
|
28
|
+
import {
|
|
29
|
+
createCustomToolsPlugin,
|
|
30
|
+
createSessionCleanupPlugin,
|
|
31
|
+
createSystemTransformPlugin,
|
|
32
|
+
createSessionIdleAutoCommitPlugin,
|
|
33
|
+
} from './plugins'
|
|
34
|
+
|
|
35
|
+
// Export types for consumers
|
|
36
|
+
export type { EventSessionDeleted, LogLevel, SandboxInfo, SessionInfo, ProjectSessionData } from './core/types'
|
|
37
|
+
|
|
38
|
+
// Initialize logger and session manager using xdg-basedir (same as OpenCode)
|
|
39
|
+
const LOG_FILE = join(xdgData!, '.daytona-plugin.log')
|
|
40
|
+
const STORAGE_DIR = join(xdgData!, 'opencode', 'storage', 'daytona')
|
|
41
|
+
const REPO_PATH = '/home/daytona/project'
|
|
42
|
+
|
|
43
|
+
setLogFilePath(LOG_FILE)
|
|
44
|
+
const sessionManager = new DaytonaSessionManager(process.env.DAYTONA_API_KEY || '', STORAGE_DIR, REPO_PATH)
|
|
45
|
+
|
|
46
|
+
// Export plugin instances
|
|
47
|
+
export const CustomToolsPlugin: Plugin = createCustomToolsPlugin(sessionManager)
|
|
48
|
+
export const DaytonaSessionCleanupPlugin: Plugin = createSessionCleanupPlugin(sessionManager)
|
|
49
|
+
export const SystemTransformPlugin: Plugin = createSystemTransformPlugin(REPO_PATH)
|
|
50
|
+
export const DaytonaSessionIdleAutoCommitPlugin: Plugin = createSessionIdleAutoCommitPlugin(sessionManager, REPO_PATH)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
2
|
+
import { createDaytonaTools } from '../tools'
|
|
3
|
+
import { logger } from '../core/logger'
|
|
4
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates the custom tools plugin for Daytona sandbox integration
|
|
8
|
+
* Provides tools for file operations, command execution, and search within sandboxes
|
|
9
|
+
*/
|
|
10
|
+
export function createCustomToolsPlugin(sessionManager: DaytonaSessionManager): Plugin {
|
|
11
|
+
return async (pluginCtx: PluginInput) => {
|
|
12
|
+
logger.info('OpenCode started with Daytona plugin')
|
|
13
|
+
const projectId = pluginCtx.project.id
|
|
14
|
+
const worktree = pluginCtx.project.worktree
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
tool: createDaytonaTools(sessionManager, projectId, worktree),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode plugin implementations for Daytona sandbox integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createCustomToolsPlugin } from './custom-tools'
|
|
6
|
+
export { createSessionIdleAutoCommitPlugin } from './session-idle-auto-commit'
|
|
7
|
+
export { createSessionCleanupPlugin } from './session-cleanup'
|
|
8
|
+
export { createSystemTransformPlugin } from './system-transform'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
2
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
3
|
+
import { EVENT_TYPE_SESSION_DELETED } from '../core/types'
|
|
4
|
+
import type { EventSessionDeleted } from '../core/types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates the session cleanup plugin for Daytona
|
|
8
|
+
* Automatically cleans up sandbox resources when sessions end
|
|
9
|
+
*/
|
|
10
|
+
export function createSessionCleanupPlugin(sessionManager: DaytonaSessionManager): Plugin {
|
|
11
|
+
return async (pluginCtx: PluginInput) => {
|
|
12
|
+
const projectId = pluginCtx.project.id
|
|
13
|
+
return {
|
|
14
|
+
event: async ({ event }) => {
|
|
15
|
+
if (event.type === EVENT_TYPE_SESSION_DELETED) {
|
|
16
|
+
const sessionId = (event as EventSessionDeleted).properties.info.id
|
|
17
|
+
await sessionManager.deleteSandbox(sessionId, projectId)
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
2
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
3
|
+
import { SessionGitManager } from '../git/session-git-manager'
|
|
4
|
+
import { EVENT_TYPE_SESSION_IDLE } from '../core/types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a plugin to auto-commit in the sandbox on session idle
|
|
8
|
+
*/
|
|
9
|
+
export function createSessionIdleAutoCommitPlugin(sessionManager: DaytonaSessionManager, repoPath: string): Plugin {
|
|
10
|
+
return async (pluginCtx: PluginInput) => {
|
|
11
|
+
const projectId = pluginCtx.project.id
|
|
12
|
+
const worktree = pluginCtx.project.worktree
|
|
13
|
+
return {
|
|
14
|
+
event: async (args: any) => {
|
|
15
|
+
const event = args.event
|
|
16
|
+
if (event.type === EVENT_TYPE_SESSION_IDLE) {
|
|
17
|
+
const sessionId = event.properties.sessionID
|
|
18
|
+
// Use SessionGitManager abstraction for auto-commit and pull
|
|
19
|
+
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree)
|
|
20
|
+
const branchNumber = sessionManager.getBranchNumberForSandbox(projectId, sandbox.id)
|
|
21
|
+
if (!branchNumber) {
|
|
22
|
+
throw new Error(`No branch number found for sandbox ${sandbox.id}`)
|
|
23
|
+
}
|
|
24
|
+
const sessionGit = new SessionGitManager(sandbox, repoPath, branchNumber)
|
|
25
|
+
await sessionGit.autoCommitAndPull()
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
2
|
+
import type {
|
|
3
|
+
ExperimentalChatSystemTransformInput,
|
|
4
|
+
ExperimentalChatSystemTransformOutput,
|
|
5
|
+
} from '../core/types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates the system transform plugin for Daytona
|
|
9
|
+
* Adds Daytona-specific instructions to the system prompt
|
|
10
|
+
*/
|
|
11
|
+
export function createSystemTransformPlugin(repoPath: string): Plugin {
|
|
12
|
+
return async (pluginCtx: PluginInput) => {
|
|
13
|
+
return {
|
|
14
|
+
'experimental.chat.system.transform': async (
|
|
15
|
+
input: ExperimentalChatSystemTransformInput,
|
|
16
|
+
output: ExperimentalChatSystemTransformOutput,
|
|
17
|
+
) => {
|
|
18
|
+
output.system.push(`
|
|
19
|
+
## Daytona Sandbox Integration
|
|
20
|
+
This session is integrated with a Daytona sandbox.
|
|
21
|
+
The main project repository is located at: ${repoPath}.
|
|
22
|
+
Work in this directory. Do NOT try to use the current working directory of the host system.
|
|
23
|
+
When executing long-running commands, use the 'background' option to run them asynchronously.
|
|
24
|
+
`)
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const bashTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Executes shell commands in a Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
command: z.string(),
|
|
9
|
+
background: z.boolean().optional(),
|
|
10
|
+
},
|
|
11
|
+
async execute(args: { command: string; background?: boolean }, ctx: ToolContext) {
|
|
12
|
+
const sessionId = ctx.sessionID
|
|
13
|
+
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree)
|
|
14
|
+
|
|
15
|
+
if (args.background) {
|
|
16
|
+
const execSessionId = `exec-session-${sessionId}`
|
|
17
|
+
try {
|
|
18
|
+
await sandbox.process.getSession(execSessionId)
|
|
19
|
+
} catch {
|
|
20
|
+
await sandbox.process.createSession(execSessionId)
|
|
21
|
+
}
|
|
22
|
+
const result = await sandbox.process.executeSessionCommand(execSessionId, {
|
|
23
|
+
command: args.command,
|
|
24
|
+
runAsync: true,
|
|
25
|
+
})
|
|
26
|
+
return `Command started in background (cmdId: ${result.cmdId})`
|
|
27
|
+
} else {
|
|
28
|
+
const result = await sandbox.process.executeCommand(args.command)
|
|
29
|
+
return `Exit code: ${result.exitCode}\n${result.result}`
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const editTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Replaces text in a file in Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
filePath: z.string(),
|
|
9
|
+
oldString: z.string(),
|
|
10
|
+
newString: z.string(),
|
|
11
|
+
},
|
|
12
|
+
async execute(args: { filePath: string; oldString: string; newString: string }, ctx: ToolContext) {
|
|
13
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
14
|
+
const buffer = await sandbox.fs.downloadFile(args.filePath)
|
|
15
|
+
const decoder = new TextDecoder()
|
|
16
|
+
const content = decoder.decode(buffer)
|
|
17
|
+
const newContent = content.replace(args.oldString, args.newString)
|
|
18
|
+
await sandbox.fs.uploadFile(Buffer.from(newContent), args.filePath)
|
|
19
|
+
return `Edited ${args.filePath}`
|
|
20
|
+
},
|
|
21
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const getPreviewURLTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Gets a preview URL for the Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
port: z.number(),
|
|
9
|
+
},
|
|
10
|
+
async execute(args: { port: number }, ctx: ToolContext) {
|
|
11
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
12
|
+
const previewLink = await sandbox.getPreviewLink(args.port)
|
|
13
|
+
return `Sandbox Preview URL: ${previewLink.url}`
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const globTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Searches for files matching a pattern in Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
pattern: z.string(),
|
|
9
|
+
},
|
|
10
|
+
async execute(args: { pattern: string }, ctx: ToolContext) {
|
|
11
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
12
|
+
const workDir = await sandbox.getWorkDir()
|
|
13
|
+
const result = await sandbox.fs.searchFiles(workDir, args.pattern)
|
|
14
|
+
return result.files.join('\n')
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const grepTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Searches for text pattern in files in Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
pattern: z.string(),
|
|
9
|
+
},
|
|
10
|
+
async execute(args: { pattern: string }, ctx: ToolContext) {
|
|
11
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
12
|
+
const workDir = await sandbox.getWorkDir()
|
|
13
|
+
const matches = await sandbox.fs.findFiles(workDir, args.pattern)
|
|
14
|
+
return matches.map((m) => `${m.file}:${m.line}: ${m.content}`).join('\n')
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
import type { FileInfo } from '@daytonaio/sdk'
|
|
5
|
+
|
|
6
|
+
export const lsTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
7
|
+
description: 'Lists files in a directory in Daytona sandbox',
|
|
8
|
+
args: {
|
|
9
|
+
dirPath: z.string().optional(),
|
|
10
|
+
},
|
|
11
|
+
async execute(args: { dirPath?: string }, ctx: ToolContext) {
|
|
12
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
13
|
+
const workDir = await sandbox.getWorkDir()
|
|
14
|
+
const path = args.dirPath || workDir
|
|
15
|
+
const files = (await sandbox.fs.listFiles(path)) as FileInfo[]
|
|
16
|
+
return files.map((f) => f.name).join('\n')
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const lspTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'LSP operation in Daytona sandbox (code intelligence)',
|
|
7
|
+
args: {
|
|
8
|
+
op: z.string(),
|
|
9
|
+
filePath: z.string(),
|
|
10
|
+
line: z.number(),
|
|
11
|
+
},
|
|
12
|
+
async execute(args: { op: string; filePath: string; line: number }, ctx: ToolContext) {
|
|
13
|
+
return `LSP operations are not yet implemented in the Daytona plugin.`
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const multieditTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Applies multiple edits to a file in Daytona sandbox atomically',
|
|
7
|
+
args: {
|
|
8
|
+
filePath: z.string(),
|
|
9
|
+
edits: z.array(
|
|
10
|
+
z.object({
|
|
11
|
+
oldString: z.string(),
|
|
12
|
+
newString: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
async execute(
|
|
17
|
+
args: { filePath: string; edits: Array<{ oldString: string; newString: string }> },
|
|
18
|
+
ctx: ToolContext,
|
|
19
|
+
) {
|
|
20
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
21
|
+
const buffer = await sandbox.fs.downloadFile(args.filePath)
|
|
22
|
+
const decoder = new TextDecoder()
|
|
23
|
+
let content = decoder.decode(buffer)
|
|
24
|
+
|
|
25
|
+
for (const edit of args.edits) {
|
|
26
|
+
content = content.replace(edit.oldString, edit.newString)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await sandbox.fs.uploadFile(Buffer.from(content), args.filePath)
|
|
30
|
+
return `Applied ${args.edits.length} edits to ${args.filePath}`
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const patchTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Patches a file with a code snippet in Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
filePath: z.string(),
|
|
9
|
+
oldSnippet: z.string(),
|
|
10
|
+
newSnippet: z.string(),
|
|
11
|
+
},
|
|
12
|
+
async execute(args: { filePath: string; oldSnippet: string; newSnippet: string }, ctx: ToolContext) {
|
|
13
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
14
|
+
const buffer = await sandbox.fs.downloadFile(args.filePath)
|
|
15
|
+
const decoder = new TextDecoder()
|
|
16
|
+
const content = decoder.decode(buffer)
|
|
17
|
+
const newContent = content.replace(args.oldSnippet, args.newSnippet)
|
|
18
|
+
await sandbox.fs.uploadFile(Buffer.from(newContent), args.filePath)
|
|
19
|
+
return `Patched ${args.filePath}`
|
|
20
|
+
},
|
|
21
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const readTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Reads file from Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
filePath: z.string(),
|
|
9
|
+
},
|
|
10
|
+
async execute(args: { filePath: string }, ctx: ToolContext) {
|
|
11
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
12
|
+
const buffer = await sandbox.fs.downloadFile(args.filePath)
|
|
13
|
+
const decoder = new TextDecoder()
|
|
14
|
+
return decoder.decode(buffer)
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ToolContext } from '@opencode-ai/plugin'
|
|
3
|
+
import type { DaytonaSessionManager } from '../core/session-manager'
|
|
4
|
+
|
|
5
|
+
export const writeTool = (sessionManager: DaytonaSessionManager, projectId: string, worktree: string) => ({
|
|
6
|
+
description: 'Writes content to file in Daytona sandbox',
|
|
7
|
+
args: {
|
|
8
|
+
filePath: z.string(),
|
|
9
|
+
content: z.string(),
|
|
10
|
+
},
|
|
11
|
+
async execute(args: { filePath: string; content: string }, ctx: ToolContext) {
|
|
12
|
+
const sandbox = await sessionManager.getSandbox(ctx.sessionID, projectId, worktree)
|
|
13
|
+
await sandbox.fs.uploadFile(Buffer.from(args.content), args.filePath)
|
|
14
|
+
return `Written ${args.content.length} bytes to ${args.filePath}`
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool implementations for Daytona sandbox integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { bashTool } from './tools/bash'
|
|
6
|
+
import { readTool } from './tools/read'
|
|
7
|
+
import { writeTool } from './tools/write'
|
|
8
|
+
import { editTool } from './tools/edit'
|
|
9
|
+
import { multieditTool } from './tools/multiedit'
|
|
10
|
+
import { patchTool } from './tools/patch'
|
|
11
|
+
import { lsTool } from './tools/ls'
|
|
12
|
+
import { globTool } from './tools/glob'
|
|
13
|
+
import { grepTool } from './tools/grep'
|
|
14
|
+
import { lspTool } from './tools/lsp'
|
|
15
|
+
import { getPreviewURLTool } from './tools/get-preview-url'
|
|
16
|
+
|
|
17
|
+
import type { DaytonaSessionManager } from './core/session-manager'
|
|
18
|
+
|
|
19
|
+
export function createDaytonaTools(sessionManager: DaytonaSessionManager, projectId: string, worktree: string) {
|
|
20
|
+
return {
|
|
21
|
+
bash: bashTool(sessionManager, projectId, worktree),
|
|
22
|
+
read: readTool(sessionManager, projectId, worktree),
|
|
23
|
+
write: writeTool(sessionManager, projectId, worktree),
|
|
24
|
+
edit: editTool(sessionManager, projectId, worktree),
|
|
25
|
+
multiedit: multieditTool(sessionManager, projectId, worktree),
|
|
26
|
+
patch: patchTool(sessionManager, projectId, worktree),
|
|
27
|
+
ls: lsTool(sessionManager, projectId, worktree),
|
|
28
|
+
glob: globTool(sessionManager, projectId, worktree),
|
|
29
|
+
grep: grepTool(sessionManager, projectId, worktree),
|
|
30
|
+
lsp: lspTool(sessionManager, projectId, worktree),
|
|
31
|
+
getPreviewURL: getPreviewURLTool(sessionManager, projectId, worktree),
|
|
32
|
+
}
|
|
33
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Daytona Sandbox Plugin for OpenCode
|
|
2
|
+
|
|
3
|
+
This is an OpenCode plugin that automatically runs all your OpenCode sessions in Daytona sandboxes. This provides isolated, reproducible development environments for your AI coding sessions.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Securely isolate each OpenCode session in a sandbox environment
|
|
8
|
+
- Automatically generates live preview links to applications or servers running in the sandbox
|
|
9
|
+
- Synchronizes changes made within the sandbox to the current git repository
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
|
|
15
|
+
You can install this extension globally or within a specific project.
|
|
16
|
+
|
|
17
|
+
To install it globally, edit `~/.config/opencode/opencode.json`. To install for a specific project, add it to `opencode.json` in the project's root directory.
|
|
18
|
+
|
|
19
|
+
Adding this extension to the plugins field will install it automatically when OpenCode starts:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"$schema": "https://opencode.ai/config.json",
|
|
24
|
+
"plugins": ["@jamesmurdza/opencode-daytona"]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Environment Configuration
|
|
29
|
+
|
|
30
|
+
This extension requires a [Daytona account](https://www.daytona.io/) and [Daytona API key](https://app.daytona.io/dashboard/keys) to create sandboxes.
|
|
31
|
+
|
|
32
|
+
Set your Daytona API key and URL as environment variables:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export DAYTONA_API_KEY="your-api-key"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or create a `.env` file in your project root:
|
|
39
|
+
|
|
40
|
+
```env
|
|
41
|
+
DAYTONA_API_KEY=your-api-key
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Running OpenCode
|
|
45
|
+
|
|
46
|
+
Start OpenCode in your project using the OpenCode command:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
opencode
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
OpenCode sessions running in Daytona sandboxes are not immediately distinguishable from local sessions. To ensure that the plugin is working, type `pwd` in the chat. If the response is `/home/daytona`, then the plugin is working correctly.
|
|
53
|
+
|
|
54
|
+
See branches created by OpenCode:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
git branch
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Continue working with OpenCode's latest changes on your local system:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
git checkout opencode/1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To see live logs for debugging, run this command in a separate terminal:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
tail -f ~/.local/share/.daytona-plugin.log
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
### File Synchronization
|
|
75
|
+
|
|
76
|
+
The plugin uses git to synchronize files between the sandbox and your local system. This happens automatically and in the background, keeping your copy of the code up-to-date without exposing your system to the agent.
|
|
77
|
+
|
|
78
|
+
#### Sandbox Setup
|
|
79
|
+
|
|
80
|
+
When a new Daytona sandbox is created:
|
|
81
|
+
1. The plugin looks for a git repository in the local directory If none is found, it creates a new one.
|
|
82
|
+
2. In the sandbox, a parallel repository to the local repository is created in the sandbox. An `opencode` branch is created in the sandbox repository.
|
|
83
|
+
3. A new `sandbox` remote is added to the local repository using an SSH connection to the sandbox.
|
|
84
|
+
4. The `HEAD` of the local repository is pushed to `opencode`, and the sandbox repository is reset to match this initial state.
|
|
85
|
+
5. Each sandbox is assigned a unique incrementing branch number (1, 2, 3, etc.) that persists across sessions.
|
|
86
|
+
|
|
87
|
+
#### Synchronization
|
|
88
|
+
|
|
89
|
+
Each time the agent makes changes:
|
|
90
|
+
1. A new commit is created in the sandbox repository on the `opencode` branch.
|
|
91
|
+
2. The plugin pulls the latest commits from the sandbox remote into a unique local branch named `opencode/1`, `opencode/2`, etc. This keeps both environments in sync while isolating changes from different sandboxes in separate local branches.
|
|
92
|
+
|
|
93
|
+
The above workflow only synchronizes changes from the sandbox to your local system. To apply changes made locally, it is recommend to start with a new OpenCode session (and sandbox).
|
|
94
|
+
|
|
95
|
+
### Session-sandbox data storage
|
|
96
|
+
|
|
97
|
+
The plugin keeps track of which sandbox belongs to each OpenCode project using local state files. This data is stored in a separate JSON file for each project:
|
|
98
|
+
|
|
99
|
+
* On macOS: `~/.local/share/opencode/storage/daytona/[projectid].json`.
|
|
100
|
+
* On Windows: `%LOCALAPPDATA%\opencode\storage\daytona\[projectid].json`.
|
|
101
|
+
|
|
102
|
+
Each JSON file contains the sandbox metadata for each session in the project, including when the sandbox was created, and when it was last used.
|
|
103
|
+
|
|
104
|
+
The plugin uses [XDG Base Directory](https://specifications.freedesktop.org/basedir/latest/) specifical to resolve the path to this directory, using the convention [set by OpenCode](https://github.com/anomalyco/opencode/blob/052f887a9a7aaf79d9f1a560f9b686d59faa8348/packages/opencode/src/global/index.ts#L4).
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
This plugin is part of the Daytona monorepo.
|
|
110
|
+
|
|
111
|
+
### Setup
|
|
112
|
+
|
|
113
|
+
First, clone the Daytona monorepo:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone -b feat/opencode-plugin https://github.com/jamesmurdza/daytona.git
|
|
117
|
+
git checkout feat/opencode-plugin
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Install dependencies:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
yarn install
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Development and Testing
|
|
127
|
+
|
|
128
|
+
To modify the extension, edit the source code files in `libs/opencode-plugin/.opencode`.
|
|
129
|
+
|
|
130
|
+
To test the OpenCode extension, create a new directory to run OpenCode in:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
mkdir ~/myproject
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use a symbolic link to install a development version of the plugin:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
ln -s libs/opencode-plugin/.opencode ~/myproject
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Open OpenCode in the new directory:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
cd ~/myproject && opencode
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use the instructions from [Running OpenCode](#running-opencode) above to check that the plugin is running and view live logs for debugging.
|
|
149
|
+
|
|
150
|
+
> [!NOTE]
|
|
151
|
+
> Because OpenCode uses Bun, the TypeScript plugin is loaded directly without a build step.
|
|
152
|
+
|
|
153
|
+
### Publishing
|
|
154
|
+
|
|
155
|
+
Check for TypeScript errors before publishing:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx nx run opencode-plugin:type-check
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Log into npm:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm login
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Publish the TypeScript package to npm:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx nx publish opencode-plugin:
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This will publish to npm with public access and use the version number from `package.json`.
|
|
174
|
+
|
|
175
|
+
## Project Structure
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
libs/opencode-plugin/
|
|
179
|
+
├── .opencode/
|
|
180
|
+
│ ├── plugin/
|
|
181
|
+
│ │ ├── daytona/ # Main Daytona integration
|
|
182
|
+
│ │ │ └── ...
|
|
183
|
+
│ │ └── index.ts # Plugin entry point
|
|
184
|
+
├── package.json # Package metadata
|
|
185
|
+
├── project.json # Nx build configuration
|
|
186
|
+
├── tsconfig.json # TypeScript config
|
|
187
|
+
└── README.md
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamesmurdza/opencode-daytona",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "OpenCode plugin that automatically runs all sessions in Daytona sandboxes for isolated, reproducible development environments",
|
|
5
|
-
"main": "./.opencode/plugin/index.js",
|
|
6
|
-
"types": "./.opencode/plugin/index.d.ts",
|
|
7
5
|
"files": [
|
|
8
6
|
".opencode"
|
|
9
7
|
],
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"dev": "tsc --watch",
|
|
13
|
-
"clean": "rm -rf dist"
|
|
14
|
-
},
|
|
15
8
|
"keywords": [
|
|
16
9
|
"daytona",
|
|
17
10
|
"opencode",
|
|
@@ -27,7 +20,5 @@
|
|
|
27
20
|
"@opencode-ai/plugin": "^1.1.21",
|
|
28
21
|
"@types/node": "^25.0.9",
|
|
29
22
|
"typescript": "^5.3.3"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
"type": "module"
|
|
33
|
-
}
|
|
23
|
+
}
|
|
24
|
+
}
|