@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.
- package/.opencode/plugin/daytona/core/logger.d.ts +15 -0
- package/.opencode/plugin/daytona/core/logger.js +65 -0
- package/.opencode/plugin/daytona/core/logger.js.map +1 -0
- package/.opencode/plugin/daytona/core/logger.ts +71 -0
- package/.opencode/plugin/daytona/core/project-data-storage.d.ts +63 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js +213 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
- package/.opencode/plugin/daytona/core/project-data-storage.ts +234 -0
- package/.opencode/plugin/daytona/core/session-manager.d.ts +74 -0
- package/.opencode/plugin/daytona/core/session-manager.js +441 -0
- package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
- package/.opencode/plugin/daytona/core/session-manager.ts +471 -0
- package/.opencode/plugin/daytona/core/toast.d.ts +47 -0
- package/.opencode/plugin/daytona/core/toast.js +70 -0
- package/.opencode/plugin/daytona/core/toast.js.map +1 -0
- package/.opencode/plugin/daytona/core/toast.ts +96 -0
- package/.opencode/plugin/daytona/core/types.d.ts +53 -0
- package/.opencode/plugin/daytona/core/types.js +12 -0
- package/.opencode/plugin/daytona/core/types.js.map +1 -0
- package/.opencode/plugin/daytona/core/types.ts +72 -0
- package/.opencode/plugin/daytona/git/host-git-manager.d.ts +58 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js +342 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/host-git-manager.ts +372 -0
- package/.opencode/plugin/daytona/git/index.d.ts +5 -0
- package/.opencode/plugin/daytona/git/index.js +6 -0
- package/.opencode/plugin/daytona/git/index.js.map +1 -0
- package/.opencode/plugin/daytona/git/index.ts +6 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +22 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js +80 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +92 -0
- package/.opencode/plugin/daytona/git/session-git-manager.d.ts +54 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js +184 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/session-git-manager.ts +203 -0
- package/.opencode/plugin/daytona/index.d.ts +125 -0
- package/.opencode/plugin/daytona/index.js +60 -0
- package/.opencode/plugin/daytona/index.js.map +1 -0
- package/.opencode/plugin/daytona/index.ts +73 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +122 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js +18 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.ts +21 -0
- package/.opencode/plugin/daytona/plugins/session-events.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/session-events.js +63 -0
- package/.opencode/plugin/daytona/plugins/session-events.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-events.ts +61 -0
- package/.opencode/plugin/daytona/plugins/system-transform.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js +38 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/system-transform.ts +43 -0
- package/.opencode/plugin/daytona/tools/bash.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/bash.js +42 -0
- package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
- package/.opencode/plugin/daytona/tools/bash.ts +51 -0
- package/.opencode/plugin/daytona/tools/edit.d.ts +21 -0
- package/.opencode/plugin/daytona/tools/edit.js +33 -0
- package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/edit.ts +44 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.ts +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.d.ts +12 -0
- package/.opencode/plugin/daytona/tools/git-sync.js +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.js.map +1 -0
- package/.opencode/plugin/daytona/tools/git-sync.ts +36 -0
- package/.opencode/plugin/daytona/tools/glob.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/glob.js +21 -0
- package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
- package/.opencode/plugin/daytona/tools/glob.ts +30 -0
- package/.opencode/plugin/daytona/tools/grep.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/grep.js +29 -0
- package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
- package/.opencode/plugin/daytona/tools/grep.ts +39 -0
- package/.opencode/plugin/daytona/tools/ls.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/ls.js +22 -0
- package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
- package/.opencode/plugin/daytona/tools/ls.ts +32 -0
- package/.opencode/plugin/daytona/tools/multiedit.d.ts +25 -0
- package/.opencode/plugin/daytona/tools/multiedit.js +37 -0
- package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/multiedit.ts +54 -0
- package/.opencode/plugin/daytona/tools/read.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/read.js +18 -0
- package/.opencode/plugin/daytona/tools/read.js.map +1 -0
- package/.opencode/plugin/daytona/tools/read.ts +27 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.d.ts +26 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js +76 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js.map +1 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.ts +103 -0
- package/.opencode/plugin/daytona/tools/write.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/write.js +18 -0
- package/.opencode/plugin/daytona/tools/write.js.map +1 -0
- package/.opencode/plugin/daytona/tools/write.ts +27 -0
- package/.opencode/plugin/daytona/tools.d.ts +119 -0
- package/.opencode/plugin/daytona/tools.js +35 -0
- package/.opencode/plugin/daytona/tools.js.map +1 -0
- package/.opencode/plugin/daytona/tools.ts +45 -0
- package/.opencode/plugin/index.d.ts +9 -0
- package/.opencode/plugin/index.js +10 -0
- package/.opencode/plugin/index.js.map +1 -0
- package/.opencode/plugin/index.ts +11 -0
- package/README.md +87 -0
- 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
|