@veris-ai/daytona-opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.opencode/plugin/daytona/core/logger.d.ts +15 -0
  2. package/.opencode/plugin/daytona/core/logger.js +65 -0
  3. package/.opencode/plugin/daytona/core/logger.js.map +1 -0
  4. package/.opencode/plugin/daytona/core/logger.ts +71 -0
  5. package/.opencode/plugin/daytona/core/project-data-storage.d.ts +63 -0
  6. package/.opencode/plugin/daytona/core/project-data-storage.js +213 -0
  7. package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
  8. package/.opencode/plugin/daytona/core/project-data-storage.ts +234 -0
  9. package/.opencode/plugin/daytona/core/session-manager.d.ts +74 -0
  10. package/.opencode/plugin/daytona/core/session-manager.js +441 -0
  11. package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
  12. package/.opencode/plugin/daytona/core/session-manager.ts +471 -0
  13. package/.opencode/plugin/daytona/core/toast.d.ts +47 -0
  14. package/.opencode/plugin/daytona/core/toast.js +70 -0
  15. package/.opencode/plugin/daytona/core/toast.js.map +1 -0
  16. package/.opencode/plugin/daytona/core/toast.ts +96 -0
  17. package/.opencode/plugin/daytona/core/types.d.ts +53 -0
  18. package/.opencode/plugin/daytona/core/types.js +12 -0
  19. package/.opencode/plugin/daytona/core/types.js.map +1 -0
  20. package/.opencode/plugin/daytona/core/types.ts +72 -0
  21. package/.opencode/plugin/daytona/git/host-git-manager.d.ts +58 -0
  22. package/.opencode/plugin/daytona/git/host-git-manager.js +342 -0
  23. package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
  24. package/.opencode/plugin/daytona/git/host-git-manager.ts +372 -0
  25. package/.opencode/plugin/daytona/git/index.d.ts +5 -0
  26. package/.opencode/plugin/daytona/git/index.js +6 -0
  27. package/.opencode/plugin/daytona/git/index.js.map +1 -0
  28. package/.opencode/plugin/daytona/git/index.ts +6 -0
  29. package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +22 -0
  30. package/.opencode/plugin/daytona/git/sandbox-git-manager.js +80 -0
  31. package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
  32. package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +92 -0
  33. package/.opencode/plugin/daytona/git/session-git-manager.d.ts +54 -0
  34. package/.opencode/plugin/daytona/git/session-git-manager.js +184 -0
  35. package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
  36. package/.opencode/plugin/daytona/git/session-git-manager.ts +203 -0
  37. package/.opencode/plugin/daytona/index.d.ts +125 -0
  38. package/.opencode/plugin/daytona/index.js +60 -0
  39. package/.opencode/plugin/daytona/index.js.map +1 -0
  40. package/.opencode/plugin/daytona/index.ts +73 -0
  41. package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +122 -0
  42. package/.opencode/plugin/daytona/plugins/custom-tools.js +18 -0
  43. package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
  44. package/.opencode/plugin/daytona/plugins/custom-tools.ts +21 -0
  45. package/.opencode/plugin/daytona/plugins/session-events.d.ts +10 -0
  46. package/.opencode/plugin/daytona/plugins/session-events.js +63 -0
  47. package/.opencode/plugin/daytona/plugins/session-events.js.map +1 -0
  48. package/.opencode/plugin/daytona/plugins/session-events.ts +61 -0
  49. package/.opencode/plugin/daytona/plugins/system-transform.d.ts +10 -0
  50. package/.opencode/plugin/daytona/plugins/system-transform.js +38 -0
  51. package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
  52. package/.opencode/plugin/daytona/plugins/system-transform.ts +43 -0
  53. package/.opencode/plugin/daytona/tools/bash.d.ts +19 -0
  54. package/.opencode/plugin/daytona/tools/bash.js +42 -0
  55. package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
  56. package/.opencode/plugin/daytona/tools/bash.ts +51 -0
  57. package/.opencode/plugin/daytona/tools/edit.d.ts +21 -0
  58. package/.opencode/plugin/daytona/tools/edit.js +33 -0
  59. package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
  60. package/.opencode/plugin/daytona/tools/edit.ts +44 -0
  61. package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +17 -0
  62. package/.opencode/plugin/daytona/tools/get-preview-url.js +17 -0
  63. package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
  64. package/.opencode/plugin/daytona/tools/get-preview-url.ts +26 -0
  65. package/.opencode/plugin/daytona/tools/git-sync.d.ts +12 -0
  66. package/.opencode/plugin/daytona/tools/git-sync.js +26 -0
  67. package/.opencode/plugin/daytona/tools/git-sync.js.map +1 -0
  68. package/.opencode/plugin/daytona/tools/git-sync.ts +36 -0
  69. package/.opencode/plugin/daytona/tools/glob.d.ts +17 -0
  70. package/.opencode/plugin/daytona/tools/glob.js +21 -0
  71. package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
  72. package/.opencode/plugin/daytona/tools/glob.ts +30 -0
  73. package/.opencode/plugin/daytona/tools/grep.d.ts +17 -0
  74. package/.opencode/plugin/daytona/tools/grep.js +29 -0
  75. package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
  76. package/.opencode/plugin/daytona/tools/grep.ts +39 -0
  77. package/.opencode/plugin/daytona/tools/ls.d.ts +17 -0
  78. package/.opencode/plugin/daytona/tools/ls.js +22 -0
  79. package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
  80. package/.opencode/plugin/daytona/tools/ls.ts +32 -0
  81. package/.opencode/plugin/daytona/tools/multiedit.d.ts +25 -0
  82. package/.opencode/plugin/daytona/tools/multiedit.js +37 -0
  83. package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
  84. package/.opencode/plugin/daytona/tools/multiedit.ts +54 -0
  85. package/.opencode/plugin/daytona/tools/read.d.ts +17 -0
  86. package/.opencode/plugin/daytona/tools/read.js +18 -0
  87. package/.opencode/plugin/daytona/tools/read.js.map +1 -0
  88. package/.opencode/plugin/daytona/tools/read.ts +27 -0
  89. package/.opencode/plugin/daytona/tools/veris-receipt.d.ts +26 -0
  90. package/.opencode/plugin/daytona/tools/veris-receipt.js +76 -0
  91. package/.opencode/plugin/daytona/tools/veris-receipt.js.map +1 -0
  92. package/.opencode/plugin/daytona/tools/veris-receipt.ts +103 -0
  93. package/.opencode/plugin/daytona/tools/write.d.ts +19 -0
  94. package/.opencode/plugin/daytona/tools/write.js +18 -0
  95. package/.opencode/plugin/daytona/tools/write.js.map +1 -0
  96. package/.opencode/plugin/daytona/tools/write.ts +27 -0
  97. package/.opencode/plugin/daytona/tools.d.ts +119 -0
  98. package/.opencode/plugin/daytona/tools.js +35 -0
  99. package/.opencode/plugin/daytona/tools.js.map +1 -0
  100. package/.opencode/plugin/daytona/tools.ts +45 -0
  101. package/.opencode/plugin/index.d.ts +9 -0
  102. package/.opencode/plugin/index.js +10 -0
  103. package/.opencode/plugin/index.js.map +1 -0
  104. package/.opencode/plugin/index.ts +11 -0
  105. package/README.md +87 -0
  106. package/package.json +52 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { logger } from '../core/logger';
6
+ import { toast } from '../core/toast';
7
+ import { DaytonaSandboxGitManager } from './sandbox-git-manager';
8
+ import { HostGitManager } from './host-git-manager';
9
+ /**
10
+ * SessionGitManager: Combines DaytonaSandboxGitManager and HostGitManager for session lifecycle git operations.
11
+ */
12
+ export class SessionGitManager {
13
+ sandboxGit;
14
+ hostGit;
15
+ sandbox;
16
+ repoPath;
17
+ worktree;
18
+ branch;
19
+ localBranch;
20
+ /** Numbered remote (sandbox-2) matches localBranch (opencode/2) */
21
+ remoteName;
22
+ constructor(sandbox, repoPath, worktree, branchNumber) {
23
+ this.sandbox = sandbox;
24
+ this.repoPath = repoPath;
25
+ this.worktree = worktree;
26
+ this.branch = 'opencode';
27
+ this.localBranch = `opencode/${branchNumber}`;
28
+ this.remoteName = `sandbox-${branchNumber}`;
29
+ this.sandboxGit = new DaytonaSandboxGitManager(sandbox, repoPath);
30
+ this.hostGit = new HostGitManager();
31
+ }
32
+ /**
33
+ * Allocate and reserve the next opencode/N number in the local repo at `worktree`.
34
+ * This keeps all host-git concerns inside the git manager layer.
35
+ */
36
+ static allocateAndReserveBranchNumber(worktree, prefix = 'opencode') {
37
+ return new HostGitManager().allocateAndReserveBranchNumber(worktree, prefix);
38
+ }
39
+ // In-flight syncs per session. OpenCode dispatches the `event` hook without awaiting it,
40
+ // so syncs started on session.idle are invisible to callers; tracking them here lets the
41
+ // delete path and plugin shutdown wait instead of destroying a sandbox mid-sync.
42
+ static pendingSyncs = new Map();
43
+ /**
44
+ * Run `fn` after any in-flight sync for this session and track it until it settles.
45
+ * The caller of this invocation sees failures; waiters only observe completion.
46
+ */
47
+ static enqueueSessionSync(sessionId, fn) {
48
+ const prev = SessionGitManager.pendingSyncs.get(sessionId) ?? Promise.resolve();
49
+ const operation = prev.then(fn);
50
+ const stored = operation.then(() => undefined, () => undefined);
51
+ SessionGitManager.pendingSyncs.set(sessionId, stored);
52
+ stored.then(() => {
53
+ if (SessionGitManager.pendingSyncs.get(sessionId) === stored) {
54
+ SessionGitManager.pendingSyncs.delete(sessionId);
55
+ }
56
+ });
57
+ return operation;
58
+ }
59
+ /** Resolves when the session has no in-flight sync. Never rejects. */
60
+ static async waitForPendingSync(sessionId) {
61
+ let pending = SessionGitManager.pendingSyncs.get(sessionId);
62
+ while (pending) {
63
+ await pending;
64
+ const next = SessionGitManager.pendingSyncs.get(sessionId);
65
+ pending = next === pending ? undefined : next;
66
+ }
67
+ }
68
+ /**
69
+ * Resolves when no session has an in-flight sync, or when `timeoutMs` elapses first
70
+ * (returns false in that case). Never rejects. The bound exists for shutdown, where a
71
+ * sync stalled on an unreachable sandbox must not wedge process exit; the delete path
72
+ * intentionally waits unbounded instead, because deleting mid-sync loses data.
73
+ */
74
+ static async waitForAllPendingSyncs(timeoutMs) {
75
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
76
+ while (SessionGitManager.pendingSyncs.size > 0) {
77
+ const waits = [Promise.all([...SessionGitManager.pendingSyncs.values()])];
78
+ if (deadline !== undefined) {
79
+ const remaining = deadline - Date.now();
80
+ if (remaining <= 0)
81
+ return false;
82
+ waits.push(new Promise((resolve) => {
83
+ setTimeout(resolve, remaining).unref();
84
+ }));
85
+ }
86
+ await Promise.race(waits);
87
+ if (deadline !== undefined && Date.now() >= deadline && SessionGitManager.pendingSyncs.size > 0)
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ async getSshUrl() {
93
+ const sshAccess = await this.sandbox.createSshAccess(10);
94
+ return `ssh://${sshAccess.token}@ssh.app.daytona.io${this.repoPath}`;
95
+ }
96
+ hasLocalRepo() {
97
+ return this.hostGit.hasRepo(this.worktree);
98
+ }
99
+ /**
100
+ * Initialize git in the sandbox and sync with host
101
+ * Used when a new sandbox is created for a session
102
+ */
103
+ async initializeAndSync(pluginCtx) {
104
+ if (pluginCtx?.client?.tui) {
105
+ toast.initialize(pluginCtx.client.tui);
106
+ }
107
+ try {
108
+ // Check if local git repo exists before initializing sandbox repo
109
+ if (!this.hostGit.hasRepo(this.worktree)) {
110
+ // Always ensure the directory exists, even if git syncing is disabled
111
+ await this.sandboxGit.ensureDirectory();
112
+ logger.warn('No local git repository found. Git syncing is disabled.');
113
+ toast.show({
114
+ title: 'Git syncing disabled',
115
+ message: 'No local git repository found. Git syncing is disabled for this session.',
116
+ variant: 'warning',
117
+ });
118
+ return;
119
+ }
120
+ await this.sandboxGit.ensureRepo();
121
+ const sshUrl = await this.getSshUrl();
122
+ const pushed = await this.hostGit.pushLocalToSandboxRemote(this.remoteName, sshUrl, this.branch, this.worktree);
123
+ if (pushed) {
124
+ await this.sandboxGit.resetToRemote(this.branch);
125
+ }
126
+ }
127
+ catch (err) {
128
+ toast.show({
129
+ title: 'Git sync error',
130
+ message: err?.message || 'Failed to sync git repo.',
131
+ variant: 'error',
132
+ });
133
+ throw err;
134
+ }
135
+ }
136
+ /**
137
+ * Auto-commit in the sandbox and pull latest from host
138
+ * Used on session idle
139
+ * Returns true if changes were synced, false if no changes or no local repo
140
+ */
141
+ async autoCommitAndPull(pluginCtx) {
142
+ if (pluginCtx?.client?.tui) {
143
+ toast.initialize(pluginCtx.client.tui);
144
+ }
145
+ try {
146
+ // Check if local git repo exists before attempting any git operations
147
+ if (!this.hostGit.hasRepo(this.worktree)) {
148
+ logger.warn('No local git repository found. Git syncing is disabled.');
149
+ return false;
150
+ }
151
+ await this.sandboxGit.ensureRepo();
152
+ await this.sandboxGit.autoCommit();
153
+ // Pull whenever the sandbox tip differs from the local opencode/N ref, not only
154
+ // when this call created a commit: a previous sync may have committed in the
155
+ // sandbox and then failed to pull, and a status-only check would skip those
156
+ // stranded commits forever (and let the delete path destroy them).
157
+ const sandboxHead = await this.sandboxGit.getHeadOid();
158
+ if (!sandboxHead || sandboxHead === this.hostGit.getRefOid(this.worktree, `refs/heads/${this.localBranch}`)) {
159
+ return false;
160
+ }
161
+ const sshUrl = await this.getSshUrl();
162
+ // Pull the branch the sandbox actually committed to, which may differ from the
163
+ // initial 'opencode' branch, so commits are never left unsynced.
164
+ const sandboxBranch = await this.sandboxGit.getCurrentBranch();
165
+ await this.hostGit.pull(this.remoteName, sshUrl, sandboxBranch, this.worktree, this.localBranch);
166
+ toast.show({
167
+ title: 'Changes synced',
168
+ message: `Changes have been synced to ${this.localBranch} in your local repository`,
169
+ variant: 'success',
170
+ });
171
+ return true;
172
+ }
173
+ catch (err) {
174
+ toast.show({
175
+ title: 'Sync failed',
176
+ message: err?.message || 'Failed to auto-commit and pull.',
177
+ variant: 'error',
178
+ });
179
+ logger.error(`[idle/git] error sandboxId=${this.sandbox.id}: ${err}`);
180
+ throw err;
181
+ }
182
+ }
183
+ }
184
+ //# sourceMappingURL=session-git-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-git-manager.js","sourceRoot":"","sources":["session-git-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import type { Sandbox } from '@daytona/sdk'
7
+ import { logger } from '../core/logger'
8
+ import { toast } from '../core/toast'
9
+ import { DaytonaSandboxGitManager } from './sandbox-git-manager'
10
+ import { HostGitManager } from './host-git-manager'
11
+ import type { PluginInput } from '@opencode-ai/plugin'
12
+
13
+ /**
14
+ * SessionGitManager: Combines DaytonaSandboxGitManager and HostGitManager for session lifecycle git operations.
15
+ */
16
+ export class SessionGitManager {
17
+ private readonly sandboxGit: DaytonaSandboxGitManager
18
+ private readonly hostGit: HostGitManager
19
+ private readonly sandbox: Sandbox
20
+ private readonly repoPath: string
21
+ private readonly worktree: string
22
+ private readonly branch: string
23
+ private readonly localBranch: string
24
+ /** Numbered remote (sandbox-2) matches localBranch (opencode/2) */
25
+ private readonly remoteName: string
26
+
27
+ constructor(sandbox: Sandbox, repoPath: string, worktree: string, branchNumber: number) {
28
+ this.sandbox = sandbox
29
+ this.repoPath = repoPath
30
+ this.worktree = worktree
31
+ this.branch = 'opencode'
32
+ this.localBranch = `opencode/${branchNumber}`
33
+ this.remoteName = `sandbox-${branchNumber}`
34
+ this.sandboxGit = new DaytonaSandboxGitManager(sandbox, repoPath)
35
+ this.hostGit = new HostGitManager()
36
+ }
37
+
38
+ /**
39
+ * Allocate and reserve the next opencode/N number in the local repo at `worktree`.
40
+ * This keeps all host-git concerns inside the git manager layer.
41
+ */
42
+ static allocateAndReserveBranchNumber(worktree: string, prefix = 'opencode'): number {
43
+ return new HostGitManager().allocateAndReserveBranchNumber(worktree, prefix)
44
+ }
45
+
46
+ // In-flight syncs per session. OpenCode dispatches the `event` hook without awaiting it,
47
+ // so syncs started on session.idle are invisible to callers; tracking them here lets the
48
+ // delete path and plugin shutdown wait instead of destroying a sandbox mid-sync.
49
+ private static pendingSyncs = new Map<string, Promise<void>>()
50
+
51
+ /**
52
+ * Run `fn` after any in-flight sync for this session and track it until it settles.
53
+ * The caller of this invocation sees failures; waiters only observe completion.
54
+ */
55
+ static enqueueSessionSync<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {
56
+ const prev = SessionGitManager.pendingSyncs.get(sessionId) ?? Promise.resolve()
57
+ const operation = prev.then(fn)
58
+ const stored: Promise<void> = operation.then(
59
+ () => undefined,
60
+ () => undefined,
61
+ )
62
+ SessionGitManager.pendingSyncs.set(sessionId, stored)
63
+ stored.then(() => {
64
+ if (SessionGitManager.pendingSyncs.get(sessionId) === stored) {
65
+ SessionGitManager.pendingSyncs.delete(sessionId)
66
+ }
67
+ })
68
+ return operation
69
+ }
70
+
71
+ /** Resolves when the session has no in-flight sync. Never rejects. */
72
+ static async waitForPendingSync(sessionId: string): Promise<void> {
73
+ let pending = SessionGitManager.pendingSyncs.get(sessionId)
74
+ while (pending) {
75
+ await pending
76
+ const next = SessionGitManager.pendingSyncs.get(sessionId)
77
+ pending = next === pending ? undefined : next
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Resolves when no session has an in-flight sync, or when `timeoutMs` elapses first
83
+ * (returns false in that case). Never rejects. The bound exists for shutdown, where a
84
+ * sync stalled on an unreachable sandbox must not wedge process exit; the delete path
85
+ * intentionally waits unbounded instead, because deleting mid-sync loses data.
86
+ */
87
+ static async waitForAllPendingSyncs(timeoutMs?: number): Promise<boolean> {
88
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs
89
+ while (SessionGitManager.pendingSyncs.size > 0) {
90
+ const waits: Promise<unknown>[] = [Promise.all([...SessionGitManager.pendingSyncs.values()])]
91
+ if (deadline !== undefined) {
92
+ const remaining = deadline - Date.now()
93
+ if (remaining <= 0) return false
94
+ waits.push(
95
+ new Promise<void>((resolve) => {
96
+ setTimeout(resolve, remaining).unref()
97
+ }),
98
+ )
99
+ }
100
+ await Promise.race(waits)
101
+ if (deadline !== undefined && Date.now() >= deadline && SessionGitManager.pendingSyncs.size > 0) return false
102
+ }
103
+ return true
104
+ }
105
+
106
+ private async getSshUrl(): Promise<string> {
107
+ const sshAccess = await this.sandbox.createSshAccess(10)
108
+ return `ssh://${sshAccess.token}@ssh.app.daytona.io${this.repoPath}`
109
+ }
110
+
111
+ hasLocalRepo(): boolean {
112
+ return this.hostGit.hasRepo(this.worktree)
113
+ }
114
+
115
+ /**
116
+ * Initialize git in the sandbox and sync with host
117
+ * Used when a new sandbox is created for a session
118
+ */
119
+ async initializeAndSync(pluginCtx?: PluginInput) {
120
+ if (pluginCtx?.client?.tui) {
121
+ toast.initialize(pluginCtx.client.tui)
122
+ }
123
+ try {
124
+ // Check if local git repo exists before initializing sandbox repo
125
+ if (!this.hostGit.hasRepo(this.worktree)) {
126
+ // Always ensure the directory exists, even if git syncing is disabled
127
+ await this.sandboxGit.ensureDirectory()
128
+ logger.warn('No local git repository found. Git syncing is disabled.')
129
+ toast.show({
130
+ title: 'Git syncing disabled',
131
+ message: 'No local git repository found. Git syncing is disabled for this session.',
132
+ variant: 'warning',
133
+ })
134
+ return
135
+ }
136
+
137
+ await this.sandboxGit.ensureRepo()
138
+ const sshUrl = await this.getSshUrl()
139
+ const pushed = await this.hostGit.pushLocalToSandboxRemote(this.remoteName, sshUrl, this.branch, this.worktree)
140
+ if (pushed) {
141
+ await this.sandboxGit.resetToRemote(this.branch)
142
+ }
143
+ } catch (err: any) {
144
+ toast.show({
145
+ title: 'Git sync error',
146
+ message: err?.message || 'Failed to sync git repo.',
147
+ variant: 'error',
148
+ })
149
+ throw err
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Auto-commit in the sandbox and pull latest from host
155
+ * Used on session idle
156
+ * Returns true if changes were synced, false if no changes or no local repo
157
+ */
158
+ async autoCommitAndPull(pluginCtx?: PluginInput): Promise<boolean> {
159
+ if (pluginCtx?.client?.tui) {
160
+ toast.initialize(pluginCtx.client.tui)
161
+ }
162
+ try {
163
+ // Check if local git repo exists before attempting any git operations
164
+ if (!this.hostGit.hasRepo(this.worktree)) {
165
+ logger.warn('No local git repository found. Git syncing is disabled.')
166
+ return false
167
+ }
168
+
169
+ await this.sandboxGit.ensureRepo()
170
+ await this.sandboxGit.autoCommit()
171
+
172
+ // Pull whenever the sandbox tip differs from the local opencode/N ref, not only
173
+ // when this call created a commit: a previous sync may have committed in the
174
+ // sandbox and then failed to pull, and a status-only check would skip those
175
+ // stranded commits forever (and let the delete path destroy them).
176
+ const sandboxHead = await this.sandboxGit.getHeadOid()
177
+ if (!sandboxHead || sandboxHead === this.hostGit.getRefOid(this.worktree, `refs/heads/${this.localBranch}`)) {
178
+ return false
179
+ }
180
+
181
+ const sshUrl = await this.getSshUrl()
182
+
183
+ // Pull the branch the sandbox actually committed to, which may differ from the
184
+ // initial 'opencode' branch, so commits are never left unsynced.
185
+ const sandboxBranch = await this.sandboxGit.getCurrentBranch()
186
+ await this.hostGit.pull(this.remoteName, sshUrl, sandboxBranch, this.worktree, this.localBranch)
187
+ toast.show({
188
+ title: 'Changes synced',
189
+ message: `Changes have been synced to ${this.localBranch} in your local repository`,
190
+ variant: 'success',
191
+ })
192
+ return true
193
+ } catch (err: any) {
194
+ toast.show({
195
+ title: 'Sync failed',
196
+ message: err?.message || 'Failed to auto-commit and pull.',
197
+ variant: 'error',
198
+ })
199
+ logger.error(`[idle/git] error sandboxId=${this.sandbox.id}: ${err}`)
200
+ throw err
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { PluginInput } from '@opencode-ai/plugin';
6
+ export type { EventSessionDeleted, LogLevel, SandboxInfo, SessionInfo, ProjectSessionData } from './core/types';
7
+ declare function daytonaPlugin(ctx: PluginInput): Promise<{
8
+ tool: {
9
+ bash: {
10
+ description: string;
11
+ args: {
12
+ command: import("zod").ZodString;
13
+ background: import("zod").ZodOptional<import("zod").ZodBoolean>;
14
+ };
15
+ execute(args: {
16
+ command: string;
17
+ background?: boolean;
18
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
19
+ };
20
+ read: {
21
+ description: string;
22
+ args: {
23
+ filePath: import("zod").ZodString;
24
+ };
25
+ execute(args: {
26
+ filePath: string;
27
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
28
+ };
29
+ write: {
30
+ description: string;
31
+ args: {
32
+ filePath: import("zod").ZodString;
33
+ content: import("zod").ZodString;
34
+ };
35
+ execute(args: {
36
+ filePath: string;
37
+ content: string;
38
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
39
+ };
40
+ edit: {
41
+ description: string;
42
+ args: {
43
+ filePath: import("zod").ZodString;
44
+ oldString: import("zod").ZodString;
45
+ newString: import("zod").ZodString;
46
+ };
47
+ execute(args: {
48
+ filePath: string;
49
+ oldString: string;
50
+ newString: string;
51
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
52
+ };
53
+ multiedit: {
54
+ description: string;
55
+ args: {
56
+ filePath: import("zod").ZodString;
57
+ edits: import("zod").ZodArray<import("zod").ZodObject<{
58
+ oldString: import("zod").ZodString;
59
+ newString: import("zod").ZodString;
60
+ }, import("zod/v4/core").$strip>>;
61
+ };
62
+ execute(args: {
63
+ filePath: string;
64
+ edits: Array<{
65
+ oldString: string;
66
+ newString: string;
67
+ }>;
68
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
69
+ };
70
+ ls: {
71
+ description: string;
72
+ args: {
73
+ dirPath: import("zod").ZodOptional<import("zod").ZodString>;
74
+ };
75
+ execute(args: {
76
+ dirPath?: string;
77
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
78
+ };
79
+ glob: {
80
+ description: string;
81
+ args: {
82
+ pattern: import("zod").ZodString;
83
+ };
84
+ execute(args: {
85
+ pattern: string;
86
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
87
+ };
88
+ grep: {
89
+ description: string;
90
+ args: {
91
+ pattern: import("zod").ZodString;
92
+ };
93
+ execute(args: {
94
+ pattern: string;
95
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
96
+ };
97
+ getPreviewURL: {
98
+ description: string;
99
+ args: {
100
+ port: import("zod").ZodNumber;
101
+ };
102
+ execute(args: {
103
+ port: number;
104
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
105
+ };
106
+ gitSync: {
107
+ description: string;
108
+ args: {};
109
+ execute(_args: {}, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
110
+ };
111
+ verisReceipt: {
112
+ description: string;
113
+ args: {
114
+ service: import("zod").ZodOptional<import("zod").ZodString>;
115
+ };
116
+ execute(args: {
117
+ service?: string;
118
+ }, ctx: import("@opencode-ai/plugin").ToolContext): Promise<string>;
119
+ };
120
+ };
121
+ event: (args: any) => Promise<void>;
122
+ 'experimental.chat.system.transform': (input: import("./core/types").ExperimentalChatSystemTransformInput, output: import("./core/types").ExperimentalChatSystemTransformOutput) => Promise<void>;
123
+ dispose: () => Promise<void>;
124
+ }>;
125
+ export default daytonaPlugin;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /**
6
+ * OpenCode Plugin: Daytona Sandbox Integration
7
+ *
8
+ * OpenCode plugins extend the AI coding assistant by adding custom tools, handling events,
9
+ * and modifying behavior. Plugins are TypeScript/JavaScript modules that export functions
10
+ * which return hooks for various lifecycle events.
11
+ *
12
+ * This plugin integrates Daytona sandboxes with OpenCode, providing isolated development
13
+ * environments for each session. It adds custom tools for file operations, command execution,
14
+ * and search within sandboxes, and automatically cleans up resources when sessions end.
15
+ *
16
+ * Learn more: https://opencode.ai/docs/plugins/
17
+ *
18
+ * Daytona Sandbox Integration Tools
19
+ *
20
+ * Requires:
21
+ * - npm install @daytona/sdk
22
+ * - Environment: DAYTONA_API_KEY
23
+ * - Environment (optional): DAYTONA_SNAPSHOT - snapshot to create sandboxes from
24
+ */
25
+ import { join } from 'path';
26
+ import { homedir } from 'os';
27
+ import { xdgData } from 'xdg-basedir';
28
+ import { logger, setLogFilePath } from './core/logger';
29
+ import { DaytonaSessionManager } from './core/session-manager';
30
+ import { SessionGitManager } from './git/session-git-manager';
31
+ import { toast } from './core/toast';
32
+ import { customTools } from './plugins/custom-tools';
33
+ import { eventHandlers } from './plugins/session-events';
34
+ import { systemPromptTransform } from './plugins/system-transform';
35
+ const xdgDataDir = xdgData ?? join(homedir(), '.local', 'share');
36
+ const LOG_FILE = join(xdgDataDir, 'opencode', 'log', 'daytona.log');
37
+ const STORAGE_DIR = join(xdgDataDir, 'opencode', 'storage', 'daytona');
38
+ const REPO_PATH = '/home/daytona/project';
39
+ setLogFilePath(LOG_FILE);
40
+ const sessionManager = new DaytonaSessionManager(process.env.DAYTONA_API_KEY || '', STORAGE_DIR, REPO_PATH, process.env.DAYTONA_SNAPSHOT);
41
+ async function daytonaPlugin(ctx) {
42
+ toast.initialize(ctx.client?.tui);
43
+ return {
44
+ tool: await customTools(ctx, sessionManager),
45
+ event: await eventHandlers(ctx, sessionManager, REPO_PATH),
46
+ 'experimental.chat.system.transform': await systemPromptTransform(ctx, REPO_PATH),
47
+ // Awaited by OpenCode when the plugin scope closes (newer than the published Hooks
48
+ // type, ignored by older versions). Draining here keeps a graceful shutdown from
49
+ // abandoning a git sync that the unawaited `event` hook started on session.idle.
50
+ // Bounded so a sync stalled on an unreachable sandbox cannot wedge process exit.
51
+ dispose: async () => {
52
+ const drained = await SessionGitManager.waitForAllPendingSyncs(60_000);
53
+ if (!drained) {
54
+ logger.warn('[dispose] exiting with git syncs still pending after 60s; a sync may be stalled');
55
+ }
56
+ },
57
+ };
58
+ }
59
+ export default daytonaPlugin;
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAErC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAIlE,MAAM,UAAU,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;AAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,CAAC,CAAA;AACnE,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;AACtE,MAAM,SAAS,GAAG,uBAAuB,CAAA;AAEzC,cAAc,CAAC,QAAQ,CAAC,CAAA;AACxB,MAAM,cAAc,GAAG,IAAI,qBAAqB,CAC9C,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,EACjC,WAAW,EACX,SAAS,EACT,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAC7B,CAAA;AAED,KAAK,UAAU,aAAa,CAAC,GAAgB;IAC3C,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO;QACL,IAAI,EAAE,MAAM,WAAW,CAAC,GAAG,EAAE,cAAc,CAAC;QAC5C,KAAK,EAAE,MAAM,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,SAAS,CAAC;QAC1D,oCAAoC,EAAE,MAAM,qBAAqB,CAAC,GAAG,EAAE,SAAS,CAAC;QACjF,mFAAmF;QACnF,iFAAiF;QACjF,iFAAiF;QACjF,iFAAiF;QACjF,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAA;YACtE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC,iFAAiF,CAAC,CAAA;YAChG,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,eAAe,aAAa,CAAA"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * OpenCode Plugin: Daytona Sandbox Integration
8
+ *
9
+ * OpenCode plugins extend the AI coding assistant by adding custom tools, handling events,
10
+ * and modifying behavior. Plugins are TypeScript/JavaScript modules that export functions
11
+ * which return hooks for various lifecycle events.
12
+ *
13
+ * This plugin integrates Daytona sandboxes with OpenCode, providing isolated development
14
+ * environments for each session. It adds custom tools for file operations, command execution,
15
+ * and search within sandboxes, and automatically cleans up resources when sessions end.
16
+ *
17
+ * Learn more: https://opencode.ai/docs/plugins/
18
+ *
19
+ * Daytona Sandbox Integration Tools
20
+ *
21
+ * Requires:
22
+ * - npm install @daytona/sdk
23
+ * - Environment: DAYTONA_API_KEY
24
+ * - Environment (optional): DAYTONA_SNAPSHOT - snapshot to create sandboxes from
25
+ */
26
+
27
+ import { join } from 'path'
28
+ import { homedir } from 'os'
29
+ import { xdgData } from 'xdg-basedir'
30
+ import type { PluginInput } from '@opencode-ai/plugin'
31
+ import { logger, setLogFilePath } from './core/logger'
32
+ import { DaytonaSessionManager } from './core/session-manager'
33
+ import { SessionGitManager } from './git/session-git-manager'
34
+ import { toast } from './core/toast'
35
+ import { customTools } from './plugins/custom-tools'
36
+ import { eventHandlers } from './plugins/session-events'
37
+ import { systemPromptTransform } from './plugins/system-transform'
38
+
39
+ export type { EventSessionDeleted, LogLevel, SandboxInfo, SessionInfo, ProjectSessionData } from './core/types'
40
+
41
+ const xdgDataDir = xdgData ?? join(homedir(), '.local', 'share')
42
+ const LOG_FILE = join(xdgDataDir, 'opencode', 'log', 'daytona.log')
43
+ const STORAGE_DIR = join(xdgDataDir, 'opencode', 'storage', 'daytona')
44
+ const REPO_PATH = '/home/daytona/project'
45
+
46
+ setLogFilePath(LOG_FILE)
47
+ const sessionManager = new DaytonaSessionManager(
48
+ process.env.DAYTONA_API_KEY || '',
49
+ STORAGE_DIR,
50
+ REPO_PATH,
51
+ process.env.DAYTONA_SNAPSHOT,
52
+ )
53
+
54
+ async function daytonaPlugin(ctx: PluginInput) {
55
+ toast.initialize(ctx.client?.tui)
56
+ return {
57
+ tool: await customTools(ctx, sessionManager),
58
+ event: await eventHandlers(ctx, sessionManager, REPO_PATH),
59
+ 'experimental.chat.system.transform': await systemPromptTransform(ctx, REPO_PATH),
60
+ // Awaited by OpenCode when the plugin scope closes (newer than the published Hooks
61
+ // type, ignored by older versions). Draining here keeps a graceful shutdown from
62
+ // abandoning a git sync that the unawaited `event` hook started on session.idle.
63
+ // Bounded so a sync stalled on an unreachable sandbox cannot wedge process exit.
64
+ dispose: async () => {
65
+ const drained = await SessionGitManager.waitForAllPendingSyncs(60_000)
66
+ if (!drained) {
67
+ logger.warn('[dispose] exiting with git syncs still pending after 60s; a sync may be stalled')
68
+ }
69
+ },
70
+ }
71
+ }
72
+
73
+ export default daytonaPlugin