@jamesmurdza/opencode-daytona 0.1.14 → 0.1.16
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.d.ts +15 -0
- package/.opencode/plugin/daytona/core/logger.d.ts.map +1 -0
- package/.opencode/plugin/daytona/core/logger.js +43 -0
- package/.opencode/plugin/daytona/core/logger.js.map +1 -0
- package/.opencode/plugin/daytona/core/project-data-storage.d.ts +38 -0
- package/.opencode/plugin/daytona/core/project-data-storage.d.ts.map +1 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js +130 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
- package/.opencode/plugin/daytona/core/session-manager.d.ts +42 -0
- package/.opencode/plugin/daytona/core/session-manager.d.ts.map +1 -0
- package/.opencode/plugin/daytona/core/session-manager.js +151 -0
- package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
- package/.opencode/plugin/daytona/core/types.d.ts +51 -0
- package/.opencode/plugin/daytona/core/types.d.ts.map +1 -0
- package/.opencode/plugin/daytona/core/types.js +11 -0
- package/.opencode/plugin/daytona/core/types.js.map +1 -0
- package/.opencode/plugin/daytona/git/host-git-manager.d.ts +13 -0
- package/.opencode/plugin/daytona/git/host-git-manager.d.ts.map +1 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js +97 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/index.d.ts +2 -0
- package/.opencode/plugin/daytona/git/index.d.ts.map +1 -0
- package/.opencode/plugin/daytona/git/index.js +2 -0
- package/.opencode/plugin/daytona/git/index.js.map +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +10 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts.map +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js +49 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/session-git-manager.d.ts +25 -0
- package/.opencode/plugin/daytona/git/session-git-manager.d.ts.map +1 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js +52 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/index.d.ts +26 -0
- package/.opencode/plugin/daytona/index.d.ts.map +1 -0
- package/.opencode/plugin/daytona/index.js +37 -0
- package/.opencode/plugin/daytona/index.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +8 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.d.ts.map +1 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js +17 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/index.d.ts +8 -0
- package/.opencode/plugin/daytona/plugins/index.d.ts.map +1 -0
- package/.opencode/plugin/daytona/plugins/{index.ts → index.js} +5 -5
- package/.opencode/plugin/daytona/plugins/index.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-cleanup.d.ts +8 -0
- package/.opencode/plugin/daytona/plugins/session-cleanup.d.ts.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-cleanup.js +19 -0
- package/.opencode/plugin/daytona/plugins/session-cleanup.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.d.ts +7 -0
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.d.ts.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.js +28 -0
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/system-transform.d.ts +7 -0
- package/.opencode/plugin/daytona/plugins/system-transform.d.ts.map +1 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js +20 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
- package/.opencode/plugin/daytona/tools/bash.d.ts +15 -0
- package/.opencode/plugin/daytona/tools/bash.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/bash.js +31 -0
- package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
- package/.opencode/plugin/daytona/tools/edit.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/edit.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/edit.js +19 -0
- package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +13 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js +13 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
- package/.opencode/plugin/daytona/tools/glob.d.ts +13 -0
- package/.opencode/plugin/daytona/tools/glob.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/glob.js +17 -0
- package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
- package/.opencode/plugin/daytona/tools/grep.d.ts +13 -0
- package/.opencode/plugin/daytona/tools/grep.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/grep.js +17 -0
- package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
- package/.opencode/plugin/daytona/tools/ls.d.ts +13 -0
- package/.opencode/plugin/daytona/tools/ls.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/ls.js +18 -0
- package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
- package/.opencode/plugin/daytona/tools/lsp.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/lsp.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/lsp.js +13 -0
- package/.opencode/plugin/daytona/tools/lsp.js.map +1 -0
- package/.opencode/plugin/daytona/tools/multiedit.d.ts +21 -0
- package/.opencode/plugin/daytona/tools/multiedit.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/multiedit.js +23 -0
- package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/patch.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/patch.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/patch.js +19 -0
- package/.opencode/plugin/daytona/tools/patch.js.map +1 -0
- package/.opencode/plugin/daytona/tools/read.d.ts +13 -0
- package/.opencode/plugin/daytona/tools/read.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/read.js +14 -0
- package/.opencode/plugin/daytona/tools/read.js.map +1 -0
- package/.opencode/plugin/daytona/tools/write.d.ts +15 -0
- package/.opencode/plugin/daytona/tools/write.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools/write.js +14 -0
- package/.opencode/plugin/daytona/tools/write.js.map +1 -0
- package/.opencode/plugin/daytona/tools.d.ts +130 -0
- package/.opencode/plugin/daytona/tools.d.ts.map +1 -0
- package/.opencode/plugin/daytona/tools.js +30 -0
- package/.opencode/plugin/daytona/tools.js.map +1 -0
- package/.opencode/plugin/index.d.ts +6 -0
- package/.opencode/plugin/index.d.ts.map +1 -0
- package/.opencode/plugin/{index.ts → index.js} +2 -1
- package/.opencode/plugin/index.js.map +1 -0
- package/package.json +6 -1
- package/.opencode/plugin/daytona/core/logger.ts +0 -50
- package/.opencode/plugin/daytona/core/project-data-storage.ts +0 -154
- package/.opencode/plugin/daytona/core/session-manager.ts +0 -170
- package/.opencode/plugin/daytona/core/types.ts +0 -68
- package/.opencode/plugin/daytona/git/host-git-manager.ts +0 -94
- package/.opencode/plugin/daytona/git/index.ts +0 -1
- package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +0 -51
- package/.opencode/plugin/daytona/git/session-git-manager.ts +0 -65
- package/.opencode/plugin/daytona/index.ts +0 -50
- package/.opencode/plugin/daytona/plugins/custom-tools.ts +0 -20
- package/.opencode/plugin/daytona/plugins/session-cleanup.ts +0 -22
- package/.opencode/plugin/daytona/plugins/session-idle-auto-commit.ts +0 -30
- package/.opencode/plugin/daytona/plugins/system-transform.ts +0 -28
- package/.opencode/plugin/daytona/tools/bash.ts +0 -32
- package/.opencode/plugin/daytona/tools/edit.ts +0 -21
- package/.opencode/plugin/daytona/tools/get-preview-url.ts +0 -15
- package/.opencode/plugin/daytona/tools/glob.ts +0 -16
- package/.opencode/plugin/daytona/tools/grep.ts +0 -16
- package/.opencode/plugin/daytona/tools/ls.ts +0 -18
- package/.opencode/plugin/daytona/tools/lsp.ts +0 -15
- package/.opencode/plugin/daytona/tools/multiedit.ts +0 -32
- package/.opencode/plugin/daytona/tools/patch.ts +0 -21
- package/.opencode/plugin/daytona/tools/read.ts +0 -16
- package/.opencode/plugin/daytona/tools/write.ts +0 -16
- package/.opencode/plugin/daytona/tools.ts +0 -33
|
@@ -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'
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { logger } from '../core/logger'
|
|
2
|
-
import { exec, execSync } from 'child_process'
|
|
3
|
-
|
|
4
|
-
function execSyncSilent(cmd: string, options: any = {}) {
|
|
5
|
-
return execSync(cmd, { stdio: 'ignore', ...options })
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class HostGitManager {
|
|
9
|
-
// No constructor needed; use global logger
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Pushes local changes to the sandbox remote, following the same steps as the standalone function.
|
|
13
|
-
* @param sshUrl The SSH URL of the sandbox remote.
|
|
14
|
-
* @param branch The branch to push to.
|
|
15
|
-
*/
|
|
16
|
-
pushLocalToSandboxRemote(sshUrl: string, branch: string): void {
|
|
17
|
-
try {
|
|
18
|
-
logger.info(`Init in current directory, pushing to ${sshUrl} on branch ${branch}`)
|
|
19
|
-
try {
|
|
20
|
-
execSyncSilent('git rev-parse --is-inside-work-tree')
|
|
21
|
-
} catch {
|
|
22
|
-
execSyncSilent('git init')
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
execSyncSilent('git remote get-url sandbox')
|
|
26
|
-
execSyncSilent(`git remote set-url sandbox ${sshUrl}`)
|
|
27
|
-
} catch (e) {
|
|
28
|
-
execSyncSilent(`git remote add sandbox ${sshUrl}`)
|
|
29
|
-
}
|
|
30
|
-
execSyncSilent('git add .')
|
|
31
|
-
execSyncSilent('git commit -m "Sync local changes before agent start" || echo "No changes to commit"', {
|
|
32
|
-
shell: '/bin/bash',
|
|
33
|
-
})
|
|
34
|
-
execSyncSilent(`git push sandbox HEAD:${branch}`)
|
|
35
|
-
logger.info('✓ Pushed local changes to sandbox')
|
|
36
|
-
} catch (e) {
|
|
37
|
-
logger.error(`Error pushing to sandbox: ${e}`)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
ensureRepo(): void {
|
|
42
|
-
try {
|
|
43
|
-
execSyncSilent('git rev-parse --is-inside-work-tree')
|
|
44
|
-
logger.info('Git repo already exists in local worktree.')
|
|
45
|
-
} catch {
|
|
46
|
-
execSyncSilent('git init')
|
|
47
|
-
logger.info('Initialized new git repo in local worktree.')
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setRemote(remoteName: string, sshUrl: string): void {
|
|
52
|
-
try {
|
|
53
|
-
// remove existing remote if it exists
|
|
54
|
-
execSyncSilent(`git remote remove ${remoteName} || true`)
|
|
55
|
-
execSyncSilent(`git remote add ${remoteName} ${sshUrl}`)
|
|
56
|
-
} catch (e) {
|
|
57
|
-
logger.warn(`Could not set sandbox remote: ${e}`)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
pull(remoteName: string, branch: string, localBranch?: string): void {
|
|
62
|
-
let attempts = 0
|
|
63
|
-
// The first pull attempt sometimes fails. I'm not sure what the cause is.
|
|
64
|
-
while (attempts < 3) {
|
|
65
|
-
try {
|
|
66
|
-
if (localBranch) {
|
|
67
|
-
// Fetch the remote branch into the specified local branch
|
|
68
|
-
execSyncSilent(`git fetch ${remoteName} ${branch}:${localBranch}`)
|
|
69
|
-
logger.info(`✓ Fetched latest changes from sandbox into ${localBranch}`)
|
|
70
|
-
} else {
|
|
71
|
-
execSyncSilent(`git pull ${remoteName} ${branch}`)
|
|
72
|
-
logger.info('✓ Pulled latest changes from sandbox')
|
|
73
|
-
}
|
|
74
|
-
return
|
|
75
|
-
} catch (e) {
|
|
76
|
-
attempts++
|
|
77
|
-
if (attempts >= 3) {
|
|
78
|
-
logger.error(`Error pulling from sandbox after 3 attempts: ${e}`)
|
|
79
|
-
} else {
|
|
80
|
-
logger.warn(`Pull attempt ${attempts} failed, retrying...`)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
push(remoteName: string, branch: string): void {
|
|
87
|
-
try {
|
|
88
|
-
execSyncSilent(`git push ${remoteName} HEAD:${branch}`)
|
|
89
|
-
logger.info('✓ Pushed changes to sandbox')
|
|
90
|
-
} catch (e) {
|
|
91
|
-
logger.error(`Error pushing to sandbox: ${e}`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SessionGitManager } from './session-git-manager'
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { Sandbox } from '@daytonaio/sdk'
|
|
2
|
-
import { logger } from '../core/logger'
|
|
3
|
-
|
|
4
|
-
export class DaytonaSandboxGitManager {
|
|
5
|
-
constructor(
|
|
6
|
-
private readonly sandbox: Sandbox,
|
|
7
|
-
private readonly repoPath: string,
|
|
8
|
-
) {}
|
|
9
|
-
|
|
10
|
-
async ensureRepo(): Promise<void> {
|
|
11
|
-
await this.sandbox.process.executeCommand(`mkdir -p ${this.repoPath}`)
|
|
12
|
-
const isGit = await this.sandbox.process.executeCommand('git rev-parse --is-inside-work-tree', this.repoPath)
|
|
13
|
-
if (!isGit || isGit.result.trim() !== 'true') {
|
|
14
|
-
await this.sandbox.process.executeCommand('git init', this.repoPath)
|
|
15
|
-
await this.sandbox.process.executeCommand('git config user.email "sandbox@example.com"', this.repoPath)
|
|
16
|
-
await this.sandbox.process.executeCommand('git config user.name "Daytona Sandbox"', this.repoPath)
|
|
17
|
-
logger.info(`Initialized git repo in sandbox at ${this.repoPath}`)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async autoCommit(): Promise<void> {
|
|
22
|
-
try {
|
|
23
|
-
await this.sandbox.process.executeCommand('git add .', this.repoPath)
|
|
24
|
-
await this.sandbox.process.executeCommand(
|
|
25
|
-
'git commit -am "Auto-commit from Daytona plugin" || true',
|
|
26
|
-
this.repoPath,
|
|
27
|
-
)
|
|
28
|
-
logger.info(`Auto-committed changes in sandbox at ${this.repoPath}`)
|
|
29
|
-
} catch (err) {
|
|
30
|
-
logger.error(`Failed to auto-commit in sandbox at ${this.repoPath}: ${err}`)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async resetToRemote(branch: string): Promise<void> {
|
|
35
|
-
try {
|
|
36
|
-
const result = await this.sandbox.process.executeCommand(`git checkout -B ${branch}`, this.repoPath)
|
|
37
|
-
logger.info(`Checked out branch '${branch}': ${result.result}`)
|
|
38
|
-
await this.sandbox.process.executeCommand('git reset --hard', this.repoPath)
|
|
39
|
-
await this.sandbox.process.executeCommand('git clean -fd', this.repoPath)
|
|
40
|
-
logger.info('Reset sandbox worktree to pushed state.')
|
|
41
|
-
const statusResult = await this.sandbox.process.executeCommand('git status --porcelain', this.repoPath)
|
|
42
|
-
if (statusResult.result.trim()) {
|
|
43
|
-
logger.warn(`Sandbox has uncommitted changes after reset:\n${statusResult.result}`)
|
|
44
|
-
} else {
|
|
45
|
-
logger.info('No uncommitted changes in sandbox after reset.')
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
logger.error(`Failed to reset sandbox worktree: ${err}`)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { Sandbox } from '@daytonaio/sdk'
|
|
2
|
-
import { logger } from '../core/logger'
|
|
3
|
-
import { DaytonaSandboxGitManager } from './sandbox-git-manager'
|
|
4
|
-
import { HostGitManager } from './host-git-manager'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* SessionGitManager: Combines DaytonaSandboxGitManager and HostGitManager for session lifecycle git operations.
|
|
8
|
-
*/
|
|
9
|
-
export class SessionGitManager {
|
|
10
|
-
private readonly sandboxGit: DaytonaSandboxGitManager
|
|
11
|
-
private readonly hostGit: HostGitManager
|
|
12
|
-
private readonly sandbox: Sandbox
|
|
13
|
-
private readonly repoPath: string
|
|
14
|
-
private readonly branch: string
|
|
15
|
-
private readonly localBranch: string
|
|
16
|
-
private readonly branchNumber: number
|
|
17
|
-
|
|
18
|
-
constructor(sandbox: Sandbox, repoPath: string, branchNumber: number) {
|
|
19
|
-
this.sandbox = sandbox
|
|
20
|
-
this.repoPath = repoPath
|
|
21
|
-
this.branch = 'opencode'
|
|
22
|
-
this.branchNumber = branchNumber
|
|
23
|
-
this.localBranch = `opencode/${branchNumber}`
|
|
24
|
-
this.sandboxGit = new DaytonaSandboxGitManager(sandbox, repoPath)
|
|
25
|
-
this.hostGit = new HostGitManager()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private async getSshUrl(): Promise<string> {
|
|
29
|
-
const sshAccess = await this.sandbox.createSshAccess(10)
|
|
30
|
-
logger.info(`Created SSH access token ${sshAccess.token}@ssh.app.daytona.io`)
|
|
31
|
-
return `ssh://${sshAccess.token}@ssh.app.daytona.io${this.repoPath}`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Initialize git in the sandbox and sync with host
|
|
36
|
-
* Used when a new sandbox is created for a session
|
|
37
|
-
*/
|
|
38
|
-
async initializeAndSync() {
|
|
39
|
-
logger.info(
|
|
40
|
-
`Using branch number ${this.branchNumber} (local ${this.localBranch}) for sandbox git init/sync`,
|
|
41
|
-
)
|
|
42
|
-
await this.sandboxGit.ensureRepo()
|
|
43
|
-
this.hostGit.ensureRepo()
|
|
44
|
-
const sshUrl = await this.getSshUrl()
|
|
45
|
-
this.hostGit.setRemote('sandbox', sshUrl)
|
|
46
|
-
this.hostGit.pushLocalToSandboxRemote(sshUrl, this.branch)
|
|
47
|
-
await this.sandboxGit.resetToRemote(this.branch)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Auto-commit in the sandbox and pull latest from host
|
|
52
|
-
* Used on session idle
|
|
53
|
-
*/
|
|
54
|
-
async autoCommitAndPull() {
|
|
55
|
-
logger.info(
|
|
56
|
-
`Using branch number ${this.branchNumber} (local ${this.localBranch}) for sandbox auto-commit/pull`,
|
|
57
|
-
)
|
|
58
|
-
await this.sandboxGit.ensureRepo()
|
|
59
|
-
await this.sandboxGit.autoCommit()
|
|
60
|
-
this.hostGit.ensureRepo()
|
|
61
|
-
const sshUrl = await this.getSshUrl()
|
|
62
|
-
this.hostGit.setRemote('sandbox', sshUrl)
|
|
63
|
-
this.hostGit.pull('sandbox', this.branch, this.localBranch)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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)
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|