@jamesmurdza/opencode-daytona 0.1.15 → 0.1.17

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.
Files changed (135) hide show
  1. package/.opencode/plugin/daytona/core/logger.d.ts +15 -0
  2. package/.opencode/plugin/daytona/core/logger.d.ts.map +1 -0
  3. package/.opencode/plugin/daytona/core/logger.js +43 -0
  4. package/.opencode/plugin/daytona/core/logger.js.map +1 -0
  5. package/.opencode/plugin/daytona/core/project-data-storage.d.ts +38 -0
  6. package/.opencode/plugin/daytona/core/project-data-storage.d.ts.map +1 -0
  7. package/.opencode/plugin/daytona/core/project-data-storage.js +130 -0
  8. package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
  9. package/.opencode/plugin/daytona/core/session-manager.d.ts +42 -0
  10. package/.opencode/plugin/daytona/core/session-manager.d.ts.map +1 -0
  11. package/.opencode/plugin/daytona/core/session-manager.js +151 -0
  12. package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
  13. package/.opencode/plugin/daytona/core/types.d.ts +51 -0
  14. package/.opencode/plugin/daytona/core/types.d.ts.map +1 -0
  15. package/.opencode/plugin/daytona/core/types.js +11 -0
  16. package/.opencode/plugin/daytona/core/types.js.map +1 -0
  17. package/.opencode/plugin/daytona/git/host-git-manager.d.ts +13 -0
  18. package/.opencode/plugin/daytona/git/host-git-manager.d.ts.map +1 -0
  19. package/.opencode/plugin/daytona/git/host-git-manager.js +97 -0
  20. package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
  21. package/.opencode/plugin/daytona/git/index.d.ts +2 -0
  22. package/.opencode/plugin/daytona/git/index.d.ts.map +1 -0
  23. package/.opencode/plugin/daytona/git/index.js +2 -0
  24. package/.opencode/plugin/daytona/git/index.js.map +1 -0
  25. package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +10 -0
  26. package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts.map +1 -0
  27. package/.opencode/plugin/daytona/git/sandbox-git-manager.js +49 -0
  28. package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
  29. package/.opencode/plugin/daytona/git/session-git-manager.d.ts +25 -0
  30. package/.opencode/plugin/daytona/git/session-git-manager.d.ts.map +1 -0
  31. package/.opencode/plugin/daytona/git/session-git-manager.js +52 -0
  32. package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
  33. package/.opencode/plugin/daytona/index.d.ts +26 -0
  34. package/.opencode/plugin/daytona/index.d.ts.map +1 -0
  35. package/.opencode/plugin/daytona/index.js +37 -0
  36. package/.opencode/plugin/daytona/index.js.map +1 -0
  37. package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +8 -0
  38. package/.opencode/plugin/daytona/plugins/custom-tools.d.ts.map +1 -0
  39. package/.opencode/plugin/daytona/plugins/custom-tools.js +17 -0
  40. package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
  41. package/.opencode/plugin/daytona/plugins/index.d.ts +8 -0
  42. package/.opencode/plugin/daytona/plugins/index.d.ts.map +1 -0
  43. package/.opencode/plugin/daytona/plugins/{index.ts → index.js} +5 -5
  44. package/.opencode/plugin/daytona/plugins/index.js.map +1 -0
  45. package/.opencode/plugin/daytona/plugins/session-cleanup.d.ts +8 -0
  46. package/.opencode/plugin/daytona/plugins/session-cleanup.d.ts.map +1 -0
  47. package/.opencode/plugin/daytona/plugins/session-cleanup.js +19 -0
  48. package/.opencode/plugin/daytona/plugins/session-cleanup.js.map +1 -0
  49. package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.d.ts +7 -0
  50. package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.d.ts.map +1 -0
  51. package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.js +28 -0
  52. package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.js.map +1 -0
  53. package/.opencode/plugin/daytona/plugins/system-transform.d.ts +7 -0
  54. package/.opencode/plugin/daytona/plugins/system-transform.d.ts.map +1 -0
  55. package/.opencode/plugin/daytona/plugins/system-transform.js +20 -0
  56. package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
  57. package/.opencode/plugin/daytona/tools/bash.d.ts +15 -0
  58. package/.opencode/plugin/daytona/tools/bash.d.ts.map +1 -0
  59. package/.opencode/plugin/daytona/tools/bash.js +31 -0
  60. package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
  61. package/.opencode/plugin/daytona/tools/edit.d.ts +17 -0
  62. package/.opencode/plugin/daytona/tools/edit.d.ts.map +1 -0
  63. package/.opencode/plugin/daytona/tools/edit.js +19 -0
  64. package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
  65. package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +13 -0
  66. package/.opencode/plugin/daytona/tools/get-preview-url.d.ts.map +1 -0
  67. package/.opencode/plugin/daytona/tools/get-preview-url.js +13 -0
  68. package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
  69. package/.opencode/plugin/daytona/tools/glob.d.ts +13 -0
  70. package/.opencode/plugin/daytona/tools/glob.d.ts.map +1 -0
  71. package/.opencode/plugin/daytona/tools/glob.js +17 -0
  72. package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
  73. package/.opencode/plugin/daytona/tools/grep.d.ts +13 -0
  74. package/.opencode/plugin/daytona/tools/grep.d.ts.map +1 -0
  75. package/.opencode/plugin/daytona/tools/grep.js +17 -0
  76. package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
  77. package/.opencode/plugin/daytona/tools/ls.d.ts +13 -0
  78. package/.opencode/plugin/daytona/tools/ls.d.ts.map +1 -0
  79. package/.opencode/plugin/daytona/tools/ls.js +18 -0
  80. package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
  81. package/.opencode/plugin/daytona/tools/lsp.d.ts +17 -0
  82. package/.opencode/plugin/daytona/tools/lsp.d.ts.map +1 -0
  83. package/.opencode/plugin/daytona/tools/lsp.js +13 -0
  84. package/.opencode/plugin/daytona/tools/lsp.js.map +1 -0
  85. package/.opencode/plugin/daytona/tools/multiedit.d.ts +21 -0
  86. package/.opencode/plugin/daytona/tools/multiedit.d.ts.map +1 -0
  87. package/.opencode/plugin/daytona/tools/multiedit.js +23 -0
  88. package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
  89. package/.opencode/plugin/daytona/tools/patch.d.ts +17 -0
  90. package/.opencode/plugin/daytona/tools/patch.d.ts.map +1 -0
  91. package/.opencode/plugin/daytona/tools/patch.js +19 -0
  92. package/.opencode/plugin/daytona/tools/patch.js.map +1 -0
  93. package/.opencode/plugin/daytona/tools/read.d.ts +13 -0
  94. package/.opencode/plugin/daytona/tools/read.d.ts.map +1 -0
  95. package/.opencode/plugin/daytona/tools/read.js +14 -0
  96. package/.opencode/plugin/daytona/tools/read.js.map +1 -0
  97. package/.opencode/plugin/daytona/tools/write.d.ts +15 -0
  98. package/.opencode/plugin/daytona/tools/write.d.ts.map +1 -0
  99. package/.opencode/plugin/daytona/tools/write.js +14 -0
  100. package/.opencode/plugin/daytona/tools/write.js.map +1 -0
  101. package/.opencode/plugin/daytona/tools.d.ts +130 -0
  102. package/.opencode/plugin/daytona/tools.d.ts.map +1 -0
  103. package/.opencode/plugin/daytona/tools.js +30 -0
  104. package/.opencode/plugin/daytona/tools.js.map +1 -0
  105. package/.opencode/plugin/index.d.ts +6 -0
  106. package/.opencode/plugin/index.d.ts.map +1 -0
  107. package/.opencode/plugin/{index.ts → index.js} +2 -1
  108. package/.opencode/plugin/index.js.map +1 -0
  109. package/README.md +192 -0
  110. package/package.json +8 -1
  111. package/.opencode/plugin/daytona/core/logger.ts +0 -50
  112. package/.opencode/plugin/daytona/core/project-data-storage.ts +0 -154
  113. package/.opencode/plugin/daytona/core/session-manager.ts +0 -170
  114. package/.opencode/plugin/daytona/core/types.ts +0 -68
  115. package/.opencode/plugin/daytona/git/host-git-manager.ts +0 -94
  116. package/.opencode/plugin/daytona/git/index.ts +0 -1
  117. package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +0 -51
  118. package/.opencode/plugin/daytona/git/session-git-manager.ts +0 -65
  119. package/.opencode/plugin/daytona/index.ts +0 -50
  120. package/.opencode/plugin/daytona/plugins/custom-tools.ts +0 -20
  121. package/.opencode/plugin/daytona/plugins/session-cleanup.ts +0 -22
  122. package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.ts +0 -30
  123. package/.opencode/plugin/daytona/plugins/system-transform.ts +0 -28
  124. package/.opencode/plugin/daytona/tools/bash.ts +0 -32
  125. package/.opencode/plugin/daytona/tools/edit.ts +0 -21
  126. package/.opencode/plugin/daytona/tools/get-preview-url.ts +0 -15
  127. package/.opencode/plugin/daytona/tools/glob.ts +0 -16
  128. package/.opencode/plugin/daytona/tools/grep.ts +0 -16
  129. package/.opencode/plugin/daytona/tools/ls.ts +0 -18
  130. package/.opencode/plugin/daytona/tools/lsp.ts +0 -15
  131. package/.opencode/plugin/daytona/tools/multiedit.ts +0 -32
  132. package/.opencode/plugin/daytona/tools/patch.ts +0 -21
  133. package/.opencode/plugin/daytona/tools/read.ts +0 -16
  134. package/.opencode/plugin/daytona/tools/write.ts +0 -16
  135. package/.opencode/plugin/daytona/tools.ts +0 -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,10 +1,17 @@
1
1
  {
2
2
  "name": "@jamesmurdza/opencode-daytona",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",
5
7
  "files": [
6
8
  ".opencode"
7
9
  ],
10
+ "scripts": {
11
+ "build": "rm -rf dist && tsc --project tsconfig.build.json && cp package.json README.md dist/ && cp .npmignore dist/ 2>/dev/null || true",
12
+ "pack": "npm run build && cd dist && npm pack --dry-run",
13
+ "prepack": "npm run build"
14
+ },
8
15
  "keywords": [
9
16
  "daytona",
10
17
  "opencode",
@@ -1,50 +0,0 @@
1
- /**
2
- * Logger class for handling plugin logging
3
- */
4
-
5
- import { appendFileSync, statSync, truncateSync } from 'fs'
6
- import type { LogLevel } from './types'
7
- import { LOG_LEVEL_INFO, LOG_LEVEL_ERROR, LOG_LEVEL_WARN } from './types'
8
-
9
- let logFilePath: string | undefined
10
-
11
- export function setLogFilePath(path: string) {
12
- logFilePath = path
13
- }
14
-
15
- class Logger {
16
- private get logFile() {
17
- if (!logFilePath) throw new Error('Logger file path not set. Call setLogFilePath(path) before use.')
18
- return logFilePath
19
- }
20
-
21
- log(message: string, level: LogLevel = LOG_LEVEL_INFO): void {
22
- // Truncate log file if it exceeds 5MB
23
- try {
24
- const stats = statSync(this.logFile)
25
- const maxSize = 5 * 1024 * 1024 // 5MB
26
- if (stats.size > maxSize) {
27
- truncateSync(this.logFile, 0)
28
- }
29
- } catch (err) {
30
- // File may not exist yet, ignore
31
- }
32
- const timestamp = new Date().toISOString()
33
- const logEntry = `[${timestamp}] [${level}] ${message}\n`
34
- appendFileSync(this.logFile, logEntry)
35
- }
36
-
37
- info(message: string): void {
38
- this.log(message, LOG_LEVEL_INFO)
39
- }
40
-
41
- error(message: string): void {
42
- this.log(message, LOG_LEVEL_ERROR)
43
- }
44
-
45
- warn(message: string): void {
46
- this.log(message, LOG_LEVEL_WARN)
47
- }
48
- }
49
-
50
- export const logger = new Logger()
@@ -1,154 +0,0 @@
1
- /**
2
- * Handles file storage operations for project session data
3
- * Stores data per-project in ~/.local/share/opencode/storage/daytona/{projectId}.json
4
- */
5
-
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
7
- import { join } from 'path'
8
- import { logger } from './logger'
9
- import type { ProjectSessionData, SessionInfo } from './types'
10
-
11
- export class ProjectDataStorage {
12
- private readonly storageDir: string
13
-
14
- constructor(storageDir: string) {
15
- this.storageDir = storageDir
16
-
17
- // Ensure storage directory exists
18
- if (!existsSync(this.storageDir)) {
19
- mkdirSync(this.storageDir, { recursive: true })
20
- }
21
- }
22
-
23
- /**
24
- * Get the file path for a project's session data
25
- */
26
- private getProjectFilePath(projectId: string): string {
27
- return join(this.storageDir, `${projectId}.json`)
28
- }
29
-
30
- /**
31
- * Load project session data from disk
32
- */
33
- load(projectId: string): ProjectSessionData | null {
34
- const filePath = this.getProjectFilePath(projectId)
35
- try {
36
- if (existsSync(filePath)) {
37
- return JSON.parse(readFileSync(filePath, 'utf-8')) as ProjectSessionData
38
- }
39
- } catch (err) {
40
- logger.error(`Failed to load project data for ${projectId}: ${err}`)
41
- }
42
- return null
43
- }
44
-
45
- /**
46
- * Save project session data to disk
47
- */
48
- save(
49
- projectId: string,
50
- worktree: string,
51
- sessions: Record<string, SessionInfo>,
52
- lastBranchNumber?: number,
53
- ): void {
54
- const filePath = this.getProjectFilePath(projectId)
55
- const projectData: ProjectSessionData = {
56
- projectId,
57
- worktree,
58
- lastBranchNumber,
59
- sessions,
60
- }
61
-
62
- try {
63
- writeFileSync(filePath, JSON.stringify(projectData, null, 2))
64
- logger.info(`Saved project data for ${projectId}`)
65
- } catch (err) {
66
- logger.error(`Failed to save project data for ${projectId}: ${err}`)
67
- }
68
- }
69
-
70
- /**
71
- * Get the next available branch number for a project
72
- */
73
- getNextBranchNumber(projectId: string): number {
74
- const projectData = this.load(projectId)
75
- if (!projectData) {
76
- return 1
77
- }
78
-
79
- const branchNumbers = Object.values(projectData.sessions)
80
- .map(s => s.branchNumber)
81
- .filter((n): n is number => n !== undefined)
82
-
83
- // Use a persisted monotonic pointer so we never reuse deleted numbers.
84
- const pointer = projectData.lastBranchNumber ?? 0
85
- const maxInSessions = branchNumbers.length > 0 ? Math.max(...branchNumbers) : 0
86
- return Math.max(pointer, maxInSessions) + 1
87
- }
88
-
89
- /**
90
- * Get branch number for a sandbox
91
- */
92
- getBranchNumberForSandbox(projectId: string, sandboxId: string): number | undefined {
93
- const projectData = this.load(projectId)
94
- if (!projectData) {
95
- return undefined
96
- }
97
- const session = Object.values(projectData.sessions).find(s => s.sandboxId === sandboxId)
98
- return session?.branchNumber
99
- }
100
-
101
- /**
102
- * Update a single session in the project file
103
- */
104
- updateSession(projectId: string, worktree: string, sessionId: string, sandboxId: string, branchNumber?: number): void {
105
- const projectData = this.load(projectId) || {
106
- projectId,
107
- worktree,
108
- lastBranchNumber: 0,
109
- sessions: {},
110
- }
111
-
112
- const now = Date.now()
113
- if (!projectData.sessions[sessionId]) {
114
- // Assign branch number if not provided
115
- const assignedBranchNumber = branchNumber ?? this.getNextBranchNumber(projectId)
116
- logger.info(
117
- `Allocated branch number ${assignedBranchNumber} for new session ${sessionId} (project ${projectId})`,
118
- )
119
- projectData.sessions[sessionId] = {
120
- sandboxId,
121
- branchNumber: assignedBranchNumber,
122
- created: now,
123
- lastAccessed: now,
124
- }
125
- projectData.lastBranchNumber = Math.max(projectData.lastBranchNumber ?? 0, assignedBranchNumber)
126
- } else {
127
- projectData.sessions[sessionId].sandboxId = sandboxId
128
- projectData.sessions[sessionId].lastAccessed = now
129
- // Only update branch number if it wasn't set before
130
- if (projectData.sessions[sessionId].branchNumber === undefined) {
131
- const assignedBranchNumber = branchNumber ?? this.getNextBranchNumber(projectId)
132
- logger.info(
133
- `Allocated branch number ${assignedBranchNumber} for existing session ${sessionId} (project ${projectId})`,
134
- )
135
- projectData.sessions[sessionId].branchNumber = assignedBranchNumber
136
- projectData.lastBranchNumber = Math.max(projectData.lastBranchNumber ?? 0, assignedBranchNumber)
137
- }
138
- }
139
-
140
- this.save(projectId, worktree, projectData.sessions, projectData.lastBranchNumber)
141
- }
142
-
143
- /**
144
- * Remove a session from the project file
145
- */
146
- removeSession(projectId: string, worktree: string, sessionId: string): void {
147
- const projectData = this.load(projectId)
148
- if (projectData && projectData.sessions[sessionId]) {
149
- delete projectData.sessions[sessionId]
150
- // Intentionally keep lastBranchNumber so branch numbering remains monotonic
151
- this.save(projectId, worktree, projectData.sessions, projectData.lastBranchNumber)
152
- }
153
- }
154
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * Manages Daytona sandbox sessions and persists session-sandbox mappings
3
- * Stores data per-project in ~/.local/share/opencode/storage/daytona/{projectId}.json
4
- */
5
-
6
- import { Daytona, type Sandbox } from '@daytonaio/sdk'
7
- import { logger } from './logger'
8
- import type { SessionSandboxMap, SandboxInfo } from './types'
9
- import { SessionGitManager } from '../git/session-git-manager'
10
- import { ProjectDataStorage } from './project-data-storage'
11
-
12
- export class DaytonaSessionManager {
13
- private readonly apiKey: string
14
- private readonly dataStorage: ProjectDataStorage
15
- private sessionSandboxes: SessionSandboxMap
16
- private currentProjectId?: string
17
- private readonly repoPath: string
18
-
19
- constructor(apiKey: string, storageDir: string, repoPath: string) {
20
- this.apiKey = apiKey
21
- this.dataStorage = new ProjectDataStorage(storageDir)
22
- this.repoPath = repoPath
23
- this.sessionSandboxes = new Map()
24
- }
25
-
26
- /**
27
- * Check if a sandbox is fully initialized (has process property)
28
- */
29
- private isFullyInitialized(sandbox: Sandbox | SandboxInfo | undefined): sandbox is Sandbox {
30
- return sandbox !== undefined && 'process' in sandbox
31
- }
32
-
33
- /**
34
- * Check if a sandbox is partially initialized (has id but not process)
35
- */
36
- private isPartiallyInitialized(sandbox: Sandbox | SandboxInfo | undefined): sandbox is SandboxInfo {
37
- return sandbox !== undefined && 'id' in sandbox && !('process' in sandbox)
38
- }
39
-
40
- /**
41
- * Load sessions for a specific project into memory
42
- */
43
- private loadProjectSessions(projectId: string): void {
44
- const projectData = this.dataStorage.load(projectId)
45
- if (projectData) {
46
- for (const [sessionId, sessionInfo] of Object.entries(projectData.sessions)) {
47
- this.sessionSandboxes.set(sessionId, { id: sessionInfo.sandboxId })
48
- }
49
- logger.info(`Loaded ${Object.keys(projectData.sessions).length} sessions for project ${projectId}`)
50
- }
51
- }
52
-
53
- /**
54
- * Set the current project context
55
- */
56
- setProjectContext(projectId: string): void {
57
- if (this.currentProjectId !== projectId) {
58
- this.currentProjectId = projectId
59
- this.loadProjectSessions(projectId)
60
- }
61
- }
62
-
63
- /**
64
- * Get branch number for a sandbox
65
- */
66
- getBranchNumberForSandbox(projectId: string, sandboxId: string): number | undefined {
67
- return this.dataStorage.getBranchNumberForSandbox(projectId, sandboxId)
68
- }
69
-
70
- /**
71
- * Get or create a sandbox for the given session ID
72
- */
73
- async getSandbox(sessionId: string, projectId: string, worktree: string): Promise<Sandbox> {
74
- if (!this.apiKey) {
75
- logger.error('DAYTONA_API_KEY is not set. Cannot create or retrieve sandbox.')
76
- throw new Error('DAYTONA_API_KEY is not set. Please set the environment variable to use Daytona sandboxes.')
77
- }
78
-
79
- // Load project sessions if needed
80
- this.setProjectContext(projectId)
81
-
82
- const existing = this.sessionSandboxes.get(sessionId)
83
-
84
- // If we have a fully initialized sandbox, reuse it
85
- if (this.isFullyInitialized(existing)) {
86
- logger.info(`Reusing existing sandbox for session: ${sessionId}`)
87
- this.dataStorage.updateSession(projectId, worktree, sessionId, existing.id)
88
- return existing
89
- }
90
-
91
- // If we have a sandboxId but not a full sandbox object, reconnect to it
92
- if (this.isPartiallyInitialized(existing)) {
93
- logger.info(`Reconnecting to existing sandbox: ${existing.id}`)
94
- const daytona = new Daytona({ apiKey: this.apiKey })
95
- const sandbox = await daytona.get(existing.id)
96
- await sandbox.start()
97
- this.sessionSandboxes.set(sessionId, sandbox)
98
- // Preserve branch number if it exists for this sandbox
99
- let branchNumber = this.dataStorage.getBranchNumberForSandbox(projectId, sandbox.id)
100
- if (!branchNumber) {
101
- branchNumber = this.dataStorage.getNextBranchNumber(projectId)
102
- }
103
- this.dataStorage.updateSession(projectId, worktree, sessionId, sandbox.id, branchNumber)
104
- return sandbox
105
- }
106
-
107
- // Otherwise, create a new sandbox
108
- logger.info(`Creating new sandbox for session: ${sessionId} in project: ${projectId}`)
109
- const daytona = new Daytona({ apiKey: this.apiKey })
110
- const sandbox = await daytona.create()
111
- this.sessionSandboxes.set(sessionId, sandbox)
112
-
113
- // Get or assign branch number for this sandbox
114
- let branchNumber = this.dataStorage.getBranchNumberForSandbox(projectId, sandbox.id)
115
- if (!branchNumber) {
116
- branchNumber = this.dataStorage.getNextBranchNumber(projectId)
117
- }
118
- this.dataStorage.updateSession(projectId, worktree, sessionId, sandbox.id, branchNumber)
119
- logger.info(`Sandbox created successfully: ${sandbox.id} with branch number ${branchNumber}`)
120
-
121
- // Initialize git repo in the sandbox and sync with host
122
- try {
123
- const sessionGit = new SessionGitManager(sandbox, this.repoPath, branchNumber)
124
- await sessionGit.initializeAndSync()
125
- } catch (err) {
126
- logger.error(`Failed to initialize git repo or push local changes in sandbox: ${err}`)
127
- }
128
-
129
- return sandbox
130
- }
131
-
132
- /**
133
- * Delete the sandbox associated with the given session ID
134
- */
135
- async deleteSandbox(sessionId: string, projectId: string): Promise<void> {
136
- let sandbox = this.sessionSandboxes.get(sessionId)
137
-
138
- // If not in cache, try to load from storage and reconnect
139
- if (!sandbox || this.isPartiallyInitialized(sandbox)) {
140
- const projectData = this.dataStorage.load(projectId)
141
- const sessionInfo = projectData?.sessions?.[sessionId]
142
- if (sessionInfo?.sandboxId) {
143
- const daytona = new Daytona({ apiKey: this.apiKey })
144
- try {
145
- sandbox = await daytona.get(sessionInfo.sandboxId)
146
- this.sessionSandboxes.set(sessionId, sandbox)
147
- } catch (err) {
148
- logger.error(`Failed to reconnect to sandbox ${sessionInfo.sandboxId}: ${err}`)
149
- }
150
- }
151
- }
152
-
153
- // Delete the sandbox if we have a fully initialized one
154
- if (this.isFullyInitialized(sandbox)) {
155
- logger.info(`Removing sandbox for session: ${sessionId}`)
156
- await sandbox.delete()
157
- this.sessionSandboxes.delete(sessionId)
158
-
159
- // Remove from storage
160
- const projectData = this.dataStorage.load(projectId)
161
- if (projectData) {
162
- this.dataStorage.removeSession(projectId, projectData.worktree, sessionId)
163
- }
164
-
165
- logger.info(`Sandbox deleted successfully.`)
166
- } else {
167
- logger.warn(`No sandbox found for session: ${sessionId}`)
168
- }
169
- }
170
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Type definitions and constants for the Daytona OpenCode plugin
3
- */
4
-
5
- import type { Sandbox } from '@daytonaio/sdk'
6
-
7
- // OpenCode Types
8
-
9
- export type EventSessionDeleted = {
10
- type: 'session.deleted'
11
- properties: {
12
- info: { id: string }
13
- }
14
- }
15
-
16
- export type EventSessionIdle = {
17
- type: 'session.idle'
18
- properties: {
19
- sessionID: string
20
- }
21
- }
22
-
23
- export type ExperimentalChatSystemTransformInput = {
24
- sessionID: string;
25
- };
26
-
27
- export type ExperimentalChatSystemTransformOutput = {
28
- system: string[];
29
- };
30
-
31
- // OpenCode constants
32
-
33
- export const EVENT_TYPE_SESSION_DELETED = 'session.deleted'
34
- export const EVENT_TYPE_SESSION_IDLE = 'session.idle'
35
-
36
- // Daytona plugin types
37
-
38
- export type LogLevel = 'INFO' | 'ERROR' | 'WARN'
39
-
40
- export type SandboxInfo = {
41
- id: string
42
- }
43
-
44
- export type SessionInfo = {
45
- sandboxId: string
46
- branchNumber: number
47
- created: number
48
- lastAccessed: number
49
- }
50
-
51
- export type ProjectSessionData = {
52
- projectId: string
53
- worktree: string
54
- /**
55
- * Monotonically increasing pointer for branch numbering.
56
- * We persist this so we don't reuse branch numbers after sessions are deleted.
57
- */
58
- lastBranchNumber?: number
59
- sessions: Record<string, SessionInfo>
60
- }
61
-
62
- export type SessionSandboxMap = Map<string, Sandbox | SandboxInfo>
63
-
64
- // Daytona plugin constants
65
-
66
- export const LOG_LEVEL_INFO: LogLevel = 'INFO'
67
- export const LOG_LEVEL_ERROR: LogLevel = 'ERROR'
68
- export const LOG_LEVEL_WARN: LogLevel = 'WARN'