@wavexzore/sandbox 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 (149) hide show
  1. package/Dockerfile +14 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +3 -0
  4. package/README.md +153 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.js +9 -0
  7. package/dist/sandbox/cli/install.d.ts +5 -0
  8. package/dist/sandbox/cli/install.js +335 -0
  9. package/dist/sandbox/cli/local-store.d.ts +87 -0
  10. package/dist/sandbox/cli/local-store.js +604 -0
  11. package/dist/sandbox/cli/opencode-config.d.ts +25 -0
  12. package/dist/sandbox/cli/opencode-config.js +240 -0
  13. package/dist/sandbox/cli/path.d.ts +64 -0
  14. package/dist/sandbox/cli/path.js +127 -0
  15. package/dist/sandbox/cli/types.d.ts +145 -0
  16. package/dist/sandbox/cli/types.js +6 -0
  17. package/dist/sandbox/cli/wavexzore-sandbox.d.ts +65 -0
  18. package/dist/sandbox/cli/wavexzore-sandbox.js +577 -0
  19. package/dist/sandbox/core/cli-helper.d.ts +19 -0
  20. package/dist/sandbox/core/cli-helper.js +64 -0
  21. package/dist/sandbox/core/docker-archive-utils.d.ts +3 -0
  22. package/dist/sandbox/core/docker-archive-utils.js +50 -0
  23. package/dist/sandbox/core/docker-sandbox.d.ts +51 -0
  24. package/dist/sandbox/core/docker-sandbox.js +675 -0
  25. package/dist/sandbox/core/edit/filediff.d.ts +16 -0
  26. package/dist/sandbox/core/edit/filediff.js +21 -0
  27. package/dist/sandbox/core/edit/index.d.ts +5 -0
  28. package/dist/sandbox/core/edit/index.js +5 -0
  29. package/dist/sandbox/core/edit/line-endings.d.ts +4 -0
  30. package/dist/sandbox/core/edit/line-endings.js +10 -0
  31. package/dist/sandbox/core/edit/lock.d.ts +1 -0
  32. package/dist/sandbox/core/edit/lock.js +18 -0
  33. package/dist/sandbox/core/edit/replace.d.ts +10 -0
  34. package/dist/sandbox/core/edit/replace.js +14 -0
  35. package/dist/sandbox/core/edit/replacers.d.ts +15 -0
  36. package/dist/sandbox/core/edit/replacers.js +241 -0
  37. package/dist/sandbox/core/logger.d.ts +15 -0
  38. package/dist/sandbox/core/logger.js +59 -0
  39. package/dist/sandbox/core/lsp/client.d.ts +63 -0
  40. package/dist/sandbox/core/lsp/client.js +533 -0
  41. package/dist/sandbox/core/lsp/config.d.ts +13 -0
  42. package/dist/sandbox/core/lsp/config.js +36 -0
  43. package/dist/sandbox/core/lsp/diagnostics.d.ts +12 -0
  44. package/dist/sandbox/core/lsp/diagnostics.js +65 -0
  45. package/dist/sandbox/core/lsp/index.d.ts +4 -0
  46. package/dist/sandbox/core/lsp/index.js +4 -0
  47. package/dist/sandbox/core/lsp/language.d.ts +24 -0
  48. package/dist/sandbox/core/lsp/language.js +249 -0
  49. package/dist/sandbox/core/lsp/manager.d.ts +77 -0
  50. package/dist/sandbox/core/lsp/manager.js +237 -0
  51. package/dist/sandbox/core/lsp/tooling.d.ts +14 -0
  52. package/dist/sandbox/core/lsp/tooling.js +78 -0
  53. package/dist/sandbox/core/patch-parser.d.ts +23 -0
  54. package/dist/sandbox/core/patch-parser.js +248 -0
  55. package/dist/sandbox/core/path-map.d.ts +9 -0
  56. package/dist/sandbox/core/path-map.js +73 -0
  57. package/dist/sandbox/core/project-data-storage.d.ts +42 -0
  58. package/dist/sandbox/core/project-data-storage.js +167 -0
  59. package/dist/sandbox/core/read/binary.d.ts +4 -0
  60. package/dist/sandbox/core/read/binary.js +80 -0
  61. package/dist/sandbox/core/read/format.d.ts +38 -0
  62. package/dist/sandbox/core/read/format.js +85 -0
  63. package/dist/sandbox/core/read/index.d.ts +3 -0
  64. package/dist/sandbox/core/read/index.js +3 -0
  65. package/dist/sandbox/core/read/permissions.d.ts +7 -0
  66. package/dist/sandbox/core/read/permissions.js +13 -0
  67. package/dist/sandbox/core/session-manager.d.ts +29 -0
  68. package/dist/sandbox/core/session-manager.js +338 -0
  69. package/dist/sandbox/core/shell/config.d.ts +7 -0
  70. package/dist/sandbox/core/shell/config.js +82 -0
  71. package/dist/sandbox/core/shell/output.d.ts +35 -0
  72. package/dist/sandbox/core/shell/output.js +80 -0
  73. package/dist/sandbox/core/shell/parser.d.ts +7 -0
  74. package/dist/sandbox/core/shell/parser.js +122 -0
  75. package/dist/sandbox/core/shell/permissions.d.ts +13 -0
  76. package/dist/sandbox/core/shell/permissions.js +33 -0
  77. package/dist/sandbox/core/shell/workdir.d.ts +4 -0
  78. package/dist/sandbox/core/shell/workdir.js +19 -0
  79. package/dist/sandbox/core/stream-utils.d.ts +23 -0
  80. package/dist/sandbox/core/stream-utils.js +97 -0
  81. package/dist/sandbox/core/toast.d.ts +47 -0
  82. package/dist/sandbox/core/toast.js +73 -0
  83. package/dist/sandbox/core/types.d.ts +159 -0
  84. package/dist/sandbox/core/types.js +11 -0
  85. package/dist/sandbox/core/write/bom.d.ts +8 -0
  86. package/dist/sandbox/core/write/bom.js +15 -0
  87. package/dist/sandbox/core/write/config.d.ts +14 -0
  88. package/dist/sandbox/core/write/config.js +188 -0
  89. package/dist/sandbox/core/write/diagnostics.d.ts +19 -0
  90. package/dist/sandbox/core/write/diagnostics.js +120 -0
  91. package/dist/sandbox/core/write/diff.d.ts +7 -0
  92. package/dist/sandbox/core/write/diff.js +21 -0
  93. package/dist/sandbox/core/write/formatter.d.ts +16 -0
  94. package/dist/sandbox/core/write/formatter.js +51 -0
  95. package/dist/sandbox/core/write/index.d.ts +6 -0
  96. package/dist/sandbox/core/write/index.js +5 -0
  97. package/dist/sandbox/core/write/permissions.d.ts +13 -0
  98. package/dist/sandbox/core/write/permissions.js +19 -0
  99. package/dist/sandbox/core/write/pipeline.d.ts +48 -0
  100. package/dist/sandbox/core/write/pipeline.js +229 -0
  101. package/dist/sandbox/core/write/read-tracker.d.ts +13 -0
  102. package/dist/sandbox/core/write/read-tracker.js +30 -0
  103. package/dist/sandbox/git/host-git-manager.d.ts +40 -0
  104. package/dist/sandbox/git/host-git-manager.js +278 -0
  105. package/dist/sandbox/git/index.d.ts +5 -0
  106. package/dist/sandbox/git/index.js +5 -0
  107. package/dist/sandbox/git/sandbox-git-manager.d.ts +14 -0
  108. package/dist/sandbox/git/sandbox-git-manager.js +54 -0
  109. package/dist/sandbox/git/session-git-manager.d.ts +18 -0
  110. package/dist/sandbox/git/session-git-manager.js +85 -0
  111. package/dist/sandbox/index.d.ts +205 -0
  112. package/dist/sandbox/index.js +70 -0
  113. package/dist/sandbox/plugins/custom-tools.d.ts +203 -0
  114. package/dist/sandbox/plugins/custom-tools.js +15 -0
  115. package/dist/sandbox/plugins/session-events.d.ts +10 -0
  116. package/dist/sandbox/plugins/session-events.js +56 -0
  117. package/dist/sandbox/plugins/system-transform.d.ts +10 -0
  118. package/dist/sandbox/plugins/system-transform.js +23 -0
  119. package/dist/sandbox/tools/bash-output.d.ts +17 -0
  120. package/dist/sandbox/tools/bash-output.js +35 -0
  121. package/dist/sandbox/tools/bash-status.d.ts +13 -0
  122. package/dist/sandbox/tools/bash-status.js +29 -0
  123. package/dist/sandbox/tools/bash-stop.d.ts +13 -0
  124. package/dist/sandbox/tools/bash-stop.js +28 -0
  125. package/dist/sandbox/tools/bash.d.ts +26 -0
  126. package/dist/sandbox/tools/bash.js +120 -0
  127. package/dist/sandbox/tools/edit.d.ts +20 -0
  128. package/dist/sandbox/tools/edit.js +87 -0
  129. package/dist/sandbox/tools/get-preview-url.d.ts +17 -0
  130. package/dist/sandbox/tools/get-preview-url.js +16 -0
  131. package/dist/sandbox/tools/glob.d.ts +17 -0
  132. package/dist/sandbox/tools/glob.js +23 -0
  133. package/dist/sandbox/tools/grep.d.ts +17 -0
  134. package/dist/sandbox/tools/grep.js +23 -0
  135. package/dist/sandbox/tools/ls.d.ts +17 -0
  136. package/dist/sandbox/tools/ls.js +21 -0
  137. package/dist/sandbox/tools/lsp.d.ts +41 -0
  138. package/dist/sandbox/tools/lsp.js +198 -0
  139. package/dist/sandbox/tools/multiedit.d.ts +24 -0
  140. package/dist/sandbox/tools/multiedit.js +83 -0
  141. package/dist/sandbox/tools/patch.d.ts +14 -0
  142. package/dist/sandbox/tools/patch.js +260 -0
  143. package/dist/sandbox/tools/read.d.ts +22 -0
  144. package/dist/sandbox/tools/read.js +105 -0
  145. package/dist/sandbox/tools/write.d.ts +16 -0
  146. package/dist/sandbox/tools/write.js +27 -0
  147. package/dist/sandbox/tools.d.ts +200 -0
  148. package/dist/sandbox/tools.js +43 -0
  149. package/package.json +55 -0
@@ -0,0 +1,23 @@
1
+ export type Hunk = {
2
+ type: 'add';
3
+ path: string;
4
+ contents: string;
5
+ } | {
6
+ type: 'delete';
7
+ path: string;
8
+ } | {
9
+ type: 'update';
10
+ path: string;
11
+ move_path?: string;
12
+ chunks: UpdateFileChunk[];
13
+ };
14
+ export interface UpdateFileChunk {
15
+ old_lines: string[];
16
+ new_lines: string[];
17
+ change_context?: string;
18
+ is_end_of_file?: boolean;
19
+ }
20
+ export declare function parsePatch(patchText: string): {
21
+ hunks: Hunk[];
22
+ };
23
+ export declare function deriveNewContents(filePath: string, originalContent: string, chunks: UpdateFileChunk[]): string;
@@ -0,0 +1,248 @@
1
+ function parsePatchHeader(lines, startIdx) {
2
+ const line = lines[startIdx];
3
+ if (line.startsWith('*** Add File:')) {
4
+ const filePath = line.slice('*** Add File:'.length).trim();
5
+ return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
6
+ }
7
+ if (line.startsWith('*** Delete File:')) {
8
+ const filePath = line.slice('*** Delete File:'.length).trim();
9
+ return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
10
+ }
11
+ if (line.startsWith('*** Update File:')) {
12
+ const filePath = line.slice('*** Update File:'.length).trim();
13
+ let movePath;
14
+ let nextIdx = startIdx + 1;
15
+ if (nextIdx < lines.length && lines[nextIdx].startsWith('*** Move to:')) {
16
+ movePath = lines[nextIdx].slice('*** Move to:'.length).trim();
17
+ nextIdx++;
18
+ }
19
+ return filePath ? { filePath, movePath, nextIdx } : null;
20
+ }
21
+ return null;
22
+ }
23
+ function parseUpdateFileChunks(lines, startIdx) {
24
+ const chunks = [];
25
+ let i = startIdx;
26
+ while (i < lines.length && !lines[i].startsWith('***')) {
27
+ if (lines[i].startsWith('@@')) {
28
+ const contextLine = lines[i].substring(2).trim();
29
+ i++;
30
+ const oldLines = [];
31
+ const newLines = [];
32
+ let isEndOfFile = false;
33
+ while (i < lines.length && !lines[i].startsWith('@@') && !lines[i].startsWith('***')) {
34
+ const changeLine = lines[i];
35
+ if (changeLine === '*** End of File') {
36
+ isEndOfFile = true;
37
+ i++;
38
+ break;
39
+ }
40
+ if (changeLine.startsWith(' ')) {
41
+ const content = changeLine.substring(1);
42
+ oldLines.push(content);
43
+ newLines.push(content);
44
+ }
45
+ else if (changeLine.startsWith('-')) {
46
+ oldLines.push(changeLine.substring(1));
47
+ }
48
+ else if (changeLine.startsWith('+')) {
49
+ newLines.push(changeLine.substring(1));
50
+ }
51
+ i++;
52
+ }
53
+ chunks.push({
54
+ old_lines: oldLines,
55
+ new_lines: newLines,
56
+ change_context: contextLine || undefined,
57
+ is_end_of_file: isEndOfFile || undefined,
58
+ });
59
+ }
60
+ else {
61
+ i++;
62
+ }
63
+ }
64
+ return { chunks, nextIdx: i };
65
+ }
66
+ function parseAddFileContent(lines, startIdx) {
67
+ let content = '';
68
+ let i = startIdx;
69
+ while (i < lines.length && !lines[i].startsWith('***')) {
70
+ if (lines[i].startsWith('+')) {
71
+ content += lines[i].substring(1) + '\n';
72
+ }
73
+ i++;
74
+ }
75
+ if (content.endsWith('\n')) {
76
+ content = content.slice(0, -1);
77
+ }
78
+ return { content, nextIdx: i };
79
+ }
80
+ function stripHeredoc(input) {
81
+ const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
82
+ if (heredocMatch) {
83
+ return heredocMatch[2];
84
+ }
85
+ return input;
86
+ }
87
+ export function parsePatch(patchText) {
88
+ const cleaned = stripHeredoc(patchText.trim());
89
+ const lines = cleaned.split('\n');
90
+ const hunks = [];
91
+ let i = 0;
92
+ const beginIdx = lines.findIndex((line) => line.trim() === '*** Begin Patch');
93
+ const endIdx = lines.findIndex((line) => line.trim() === '*** End Patch');
94
+ if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
95
+ throw new Error('Invalid patch format: missing Begin/End markers');
96
+ }
97
+ i = beginIdx + 1;
98
+ while (i < endIdx) {
99
+ const header = parsePatchHeader(lines, i);
100
+ if (!header) {
101
+ i++;
102
+ continue;
103
+ }
104
+ if (lines[i].startsWith('*** Add File:')) {
105
+ const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx);
106
+ hunks.push({
107
+ type: 'add',
108
+ path: header.filePath,
109
+ contents: content,
110
+ });
111
+ i = nextIdx;
112
+ }
113
+ else if (lines[i].startsWith('*** Delete File:')) {
114
+ hunks.push({
115
+ type: 'delete',
116
+ path: header.filePath,
117
+ });
118
+ i = header.nextIdx;
119
+ }
120
+ else if (lines[i].startsWith('*** Update File:')) {
121
+ const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx);
122
+ hunks.push({
123
+ type: 'update',
124
+ path: header.filePath,
125
+ move_path: header.movePath,
126
+ chunks,
127
+ });
128
+ i = nextIdx;
129
+ }
130
+ else {
131
+ i++;
132
+ }
133
+ }
134
+ return { hunks };
135
+ }
136
+ function normalizeUnicode(str) {
137
+ return str
138
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
139
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
140
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, '-')
141
+ .replace(/\u2026/g, '...')
142
+ .replace(/\u00A0/g, ' ');
143
+ }
144
+ function tryMatch(lines, pattern, startIndex, compare, eof) {
145
+ if (eof) {
146
+ const fromEnd = lines.length - pattern.length;
147
+ if (fromEnd >= startIndex) {
148
+ let matches = true;
149
+ for (let j = 0; j < pattern.length; j++) {
150
+ if (!compare(lines[fromEnd + j], pattern[j])) {
151
+ matches = false;
152
+ break;
153
+ }
154
+ }
155
+ if (matches)
156
+ return fromEnd;
157
+ }
158
+ }
159
+ for (let i = startIndex; i <= lines.length - pattern.length; i++) {
160
+ let matches = true;
161
+ for (let j = 0; j < pattern.length; j++) {
162
+ if (!compare(lines[i + j], pattern[j])) {
163
+ matches = false;
164
+ break;
165
+ }
166
+ }
167
+ if (matches)
168
+ return i;
169
+ }
170
+ return -1;
171
+ }
172
+ function seekSequence(lines, pattern, startIndex, eof = false) {
173
+ if (pattern.length === 0)
174
+ return -1;
175
+ const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof);
176
+ if (exact !== -1)
177
+ return exact;
178
+ const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof);
179
+ if (rstrip !== -1)
180
+ return rstrip;
181
+ const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof);
182
+ if (trim !== -1)
183
+ return trim;
184
+ const normalized = tryMatch(lines, pattern, startIndex, (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), eof);
185
+ return normalized;
186
+ }
187
+ function computeReplacements(originalLines, filePath, chunks) {
188
+ const replacements = [];
189
+ let lineIndex = 0;
190
+ for (const chunk of chunks) {
191
+ if (chunk.change_context) {
192
+ const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex);
193
+ if (contextIdx === -1) {
194
+ throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`);
195
+ }
196
+ lineIndex = contextIdx + 1;
197
+ }
198
+ if (chunk.old_lines.length === 0) {
199
+ const insertionIdx = originalLines.length > 0 && originalLines[originalLines.length - 1] === ''
200
+ ? originalLines.length - 1
201
+ : originalLines.length;
202
+ replacements.push([insertionIdx, 0, chunk.new_lines]);
203
+ continue;
204
+ }
205
+ let pattern = chunk.old_lines;
206
+ let newSlice = chunk.new_lines;
207
+ let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
208
+ if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === '') {
209
+ pattern = pattern.slice(0, -1);
210
+ if (newSlice.length > 0 && newSlice[newSlice.length - 1] === '') {
211
+ newSlice = newSlice.slice(0, -1);
212
+ }
213
+ found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
214
+ }
215
+ if (found !== -1) {
216
+ replacements.push([found, pattern.length, newSlice]);
217
+ lineIndex = found + pattern.length;
218
+ }
219
+ else {
220
+ throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join('\n')}`);
221
+ }
222
+ }
223
+ replacements.sort((a, b) => a[0] - b[0]);
224
+ return replacements;
225
+ }
226
+ function applyReplacements(lines, replacements) {
227
+ const result = [...lines];
228
+ for (let i = replacements.length - 1; i >= 0; i--) {
229
+ const [startIdx, oldLen, newSegment] = replacements[i];
230
+ result.splice(startIdx, oldLen);
231
+ for (let j = 0; j < newSegment.length; j++) {
232
+ result.splice(startIdx + j, 0, newSegment[j]);
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+ export function deriveNewContents(filePath, originalContent, chunks) {
238
+ const originalLines = originalContent.split('\n');
239
+ if (originalLines.length > 0 && originalLines[originalLines.length - 1] === '') {
240
+ originalLines.pop();
241
+ }
242
+ const replacements = computeReplacements(originalLines, filePath, chunks);
243
+ const newLines = applyReplacements(originalLines, replacements);
244
+ if (newLines.length === 0 || newLines[newLines.length - 1] !== '') {
245
+ newLines.push('');
246
+ }
247
+ return newLines.join('\n');
248
+ }
@@ -0,0 +1,9 @@
1
+ export type PathMapper = ((p: string) => string) & {
2
+ worktree: string;
3
+ repoPath: string;
4
+ toContainer(input: string): string;
5
+ toHost(input: string): string;
6
+ toRelative(input: string): string;
7
+ isMapped(input: string): boolean;
8
+ };
9
+ export declare function createPathMapper(worktree: string, repoPath: string): PathMapper;
@@ -0,0 +1,73 @@
1
+ import path from 'path';
2
+ import posix from 'path/posix';
3
+ function stripTrailingSlash(value) {
4
+ if (value === '/')
5
+ return value;
6
+ return value.replace(/\/+$/, '');
7
+ }
8
+ function isInsideOrEqual(target, root) {
9
+ const relative = path.relative(root, target);
10
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
11
+ }
12
+ function isInsideOrEqualPosix(target, root) {
13
+ const relative = posix.relative(root, target);
14
+ return relative === '' || (!relative.startsWith('..') && !posix.isAbsolute(relative));
15
+ }
16
+ function normalizeRelative(input) {
17
+ const normalized = input.replace(/\\/g, '/');
18
+ const result = posix.normalize(normalized);
19
+ return result === '.' ? '' : result.replace(/^\.\//, '');
20
+ }
21
+ export function createPathMapper(worktree, repoPath) {
22
+ const hostRoot = path.resolve(stripTrailingSlash(worktree));
23
+ const containerRoot = stripTrailingSlash(repoPath);
24
+ function toRelative(input) {
25
+ if (input.startsWith(containerRoot + '/') || input === containerRoot) {
26
+ return posix.relative(containerRoot, input) || '.';
27
+ }
28
+ if (path.isAbsolute(input)) {
29
+ const absolute = path.resolve(input);
30
+ if (isInsideOrEqual(absolute, hostRoot))
31
+ return path.relative(hostRoot, absolute) || '.';
32
+ return input;
33
+ }
34
+ return normalizeRelative(input) || '.';
35
+ }
36
+ function toContainer(input) {
37
+ if (input === containerRoot || input.startsWith(containerRoot + '/'))
38
+ return input;
39
+ if (path.isAbsolute(input)) {
40
+ const absolute = path.resolve(input);
41
+ if (isInsideOrEqual(absolute, hostRoot)) {
42
+ const relative = path.relative(hostRoot, absolute).split(path.sep).join('/');
43
+ return relative ? posix.join(containerRoot, relative) : containerRoot;
44
+ }
45
+ return input;
46
+ }
47
+ return posix.join(containerRoot, normalizeRelative(input));
48
+ }
49
+ function toHost(input) {
50
+ if (input === containerRoot || input.startsWith(containerRoot + '/')) {
51
+ const relative = posix.relative(containerRoot, input);
52
+ return relative ? path.join(hostRoot, relative) : hostRoot;
53
+ }
54
+ if (path.isAbsolute(input))
55
+ return path.resolve(input);
56
+ return path.join(hostRoot, normalizeRelative(input));
57
+ }
58
+ function isMapped(input) {
59
+ if (input === containerRoot || input.startsWith(containerRoot + '/'))
60
+ return true;
61
+ if (!path.isAbsolute(input))
62
+ return true;
63
+ return isInsideOrEqual(path.resolve(input), hostRoot) || isInsideOrEqualPosix(input, containerRoot);
64
+ }
65
+ const mapper = ((input) => toContainer(input));
66
+ mapper.worktree = hostRoot;
67
+ mapper.repoPath = containerRoot;
68
+ mapper.toContainer = toContainer;
69
+ mapper.toHost = toHost;
70
+ mapper.toRelative = toRelative;
71
+ mapper.isMapped = isMapped;
72
+ return mapper;
73
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ProjectSessionData, SessionInfo } from './types.js';
6
+ export declare class ProjectDataStorage {
7
+ private readonly storageDir;
8
+ constructor(storageDir: string);
9
+ /**
10
+ * Get the file path for a project's session data
11
+ */
12
+ private getProjectFilePath;
13
+ /**
14
+ * List known project IDs from storage.
15
+ */
16
+ listProjectIds(): string[];
17
+ /**
18
+ * Load project session data from disk
19
+ */
20
+ load(projectId: string): ProjectSessionData | null;
21
+ /**
22
+ * Get a session for a project. If not found in the requested project, search all other
23
+ * projects on disk and, if found, migrate it into the requested project.
24
+ */
25
+ getSession(projectId: string, worktree: string, sessionId: string): SessionInfo | undefined;
26
+ /**
27
+ * Save project session data to disk
28
+ */
29
+ save(projectId: string, worktree: string, sessions: Record<string, SessionInfo>): void;
30
+ /**
31
+ * Get branch number for a sandbox
32
+ */
33
+ getBranchNumberForSandbox(projectId: string, sandboxId: string): number | undefined;
34
+ /**
35
+ * Update a single session in the project file
36
+ */
37
+ updateSession(projectId: string, worktree: string, sessionId: string, sandboxId: string, branchNumber?: number): void;
38
+ /**
39
+ * Remove a session from the project file
40
+ */
41
+ removeSession(projectId: string, worktree: string, sessionId: string): void;
42
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Copyright Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /**
6
+ * Handles file storage operations for project session data
7
+ * Stores data per-project in ~/.local/share/opencode/storage/sandbox/{projectId}.json
8
+ */
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { logger } from './logger.js';
12
+ export class ProjectDataStorage {
13
+ storageDir;
14
+ constructor(storageDir) {
15
+ this.storageDir = storageDir;
16
+ // Ensure storage directory exists
17
+ if (!existsSync(this.storageDir)) {
18
+ mkdirSync(this.storageDir, { recursive: true });
19
+ }
20
+ }
21
+ /**
22
+ * Get the file path for a project's session data
23
+ */
24
+ getProjectFilePath(projectId) {
25
+ return join(this.storageDir, `${projectId}.json`);
26
+ }
27
+ /**
28
+ * List known project IDs from storage.
29
+ */
30
+ listProjectIds() {
31
+ try {
32
+ return readdirSync(this.storageDir)
33
+ .filter((name) => name.endsWith('.json'))
34
+ .map((name) => name.slice(0, -'.json'.length));
35
+ }
36
+ catch (err) {
37
+ logger.error(`Failed to list project data files: ${err}`);
38
+ return [];
39
+ }
40
+ }
41
+ /**
42
+ * Load project session data from disk
43
+ */
44
+ load(projectId) {
45
+ const filePath = this.getProjectFilePath(projectId);
46
+ try {
47
+ if (existsSync(filePath)) {
48
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
49
+ }
50
+ }
51
+ catch (err) {
52
+ logger.error(`Failed to load project data for ${projectId}: ${err}`);
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Get a session for a project. If not found in the requested project, search all other
58
+ * projects on disk and, if found, migrate it into the requested project.
59
+ */
60
+ getSession(projectId, worktree, sessionId) {
61
+ const current = this.load(projectId);
62
+ const currentSession = current?.sessions?.[sessionId];
63
+ if (currentSession) {
64
+ return currentSession;
65
+ }
66
+ // Look in other projects and migrate if found.
67
+ for (const otherProjectId of this.listProjectIds()) {
68
+ if (otherProjectId === projectId)
69
+ continue;
70
+ const otherData = this.load(otherProjectId);
71
+ const sourceData = otherData;
72
+ const found = sourceData?.sessions?.[sessionId];
73
+ if (!sourceData || !found)
74
+ continue;
75
+ const destination = current ?? {
76
+ projectId,
77
+ worktree,
78
+ sessions: {},
79
+ };
80
+ // Remove from source first (best effort).
81
+ try {
82
+ delete sourceData.sessions[sessionId];
83
+ this.save(otherProjectId, sourceData.worktree, sourceData.sessions);
84
+ }
85
+ catch (err) {
86
+ logger.warn(`Failed to remove session ${sessionId} from project ${otherProjectId}: ${err}`);
87
+ }
88
+ // Add to destination and persist.
89
+ destination.sessions[sessionId] = found;
90
+ // Prefer the worktree for the project we're actually operating on.
91
+ destination.worktree = worktree;
92
+ this.save(projectId, destination.worktree, destination.sessions);
93
+ logger.info(`Migrated session ${sessionId} from project ${otherProjectId} to project ${projectId}`);
94
+ return found;
95
+ }
96
+ return undefined;
97
+ }
98
+ /**
99
+ * Save project session data to disk
100
+ */
101
+ save(projectId, worktree, sessions) {
102
+ const filePath = this.getProjectFilePath(projectId);
103
+ const projectData = {
104
+ projectId,
105
+ worktree,
106
+ sessions,
107
+ };
108
+ try {
109
+ writeFileSync(filePath, JSON.stringify(projectData, null, 2));
110
+ logger.info(`Saved project data for ${projectId}`);
111
+ }
112
+ catch (err) {
113
+ logger.error(`Failed to save project data for ${projectId}: ${err}`);
114
+ }
115
+ }
116
+ /**
117
+ * Get branch number for a sandbox
118
+ */
119
+ getBranchNumberForSandbox(projectId, sandboxId) {
120
+ const projectData = this.load(projectId);
121
+ if (!projectData) {
122
+ return undefined;
123
+ }
124
+ const session = Object.values(projectData.sessions).find((s) => s.sandboxId === sandboxId);
125
+ return session?.branchNumber;
126
+ }
127
+ /**
128
+ * Update a single session in the project file
129
+ */
130
+ updateSession(projectId, worktree, sessionId, sandboxId, branchNumber) {
131
+ const projectData = this.load(projectId) || {
132
+ projectId,
133
+ worktree,
134
+ sessions: {},
135
+ };
136
+ const now = Date.now();
137
+ if (!projectData.sessions[sessionId]) {
138
+ projectData.sessions[sessionId] = {
139
+ sandboxId,
140
+ ...(branchNumber !== undefined ? { branchNumber } : {}),
141
+ created: now,
142
+ lastAccessed: now,
143
+ };
144
+ }
145
+ else {
146
+ projectData.sessions[sessionId].sandboxId = sandboxId;
147
+ projectData.sessions[sessionId].lastAccessed = now;
148
+ // Only update branch number if it wasn't set before
149
+ if (projectData.sessions[sessionId].branchNumber === undefined) {
150
+ if (branchNumber !== undefined) {
151
+ projectData.sessions[sessionId].branchNumber = branchNumber;
152
+ }
153
+ }
154
+ }
155
+ this.save(projectId, worktree, projectData.sessions);
156
+ }
157
+ /**
158
+ * Remove a session from the project file
159
+ */
160
+ removeSession(projectId, worktree, sessionId) {
161
+ const projectData = this.load(projectId);
162
+ if (projectData && projectData.sessions[sessionId]) {
163
+ delete projectData.sessions[sessionId];
164
+ this.save(projectId, worktree, projectData.sessions);
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,4 @@
1
+ export type AttachmentKind = 'image' | 'pdf';
2
+ export declare function detectAttachmentKind(filePath: string, buffer: Buffer): AttachmentKind | undefined;
3
+ export declare function isBinaryBuffer(buffer: Buffer, filePath?: string): boolean;
4
+ export declare function formatBytes(size: number): string;
@@ -0,0 +1,80 @@
1
+ import path from 'path';
2
+ const BINARY_EXTENSIONS = new Set([
3
+ '.7z',
4
+ '.a',
5
+ '.avi',
6
+ '.bin',
7
+ '.bmp',
8
+ '.class',
9
+ '.dll',
10
+ '.dmg',
11
+ '.doc',
12
+ '.docx',
13
+ '.exe',
14
+ '.gif',
15
+ '.gz',
16
+ '.ico',
17
+ '.jar',
18
+ '.jpeg',
19
+ '.jpg',
20
+ '.mov',
21
+ '.mp3',
22
+ '.mp4',
23
+ '.o',
24
+ '.pdf',
25
+ '.png',
26
+ '.pyc',
27
+ '.rar',
28
+ '.so',
29
+ '.tar',
30
+ '.tgz',
31
+ '.wasm',
32
+ '.webp',
33
+ '.woff',
34
+ '.woff2',
35
+ '.zip',
36
+ ]);
37
+ export function detectAttachmentKind(filePath, buffer) {
38
+ const ext = path.extname(filePath).toLowerCase();
39
+ if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico'].includes(ext))
40
+ return 'image';
41
+ if (ext === '.pdf')
42
+ return 'pdf';
43
+ const header = buffer.subarray(0, 16);
44
+ if (header.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
45
+ return 'image';
46
+ if (header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff)
47
+ return 'image';
48
+ if (header.subarray(0, 4).toString('ascii') === 'GIF8')
49
+ return 'image';
50
+ if (header.subarray(0, 4).toString('ascii') === '%PDF')
51
+ return 'pdf';
52
+ return undefined;
53
+ }
54
+ export function isBinaryBuffer(buffer, filePath = '') {
55
+ if (BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase()))
56
+ return true;
57
+ const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
58
+ if (sample.length === 0)
59
+ return false;
60
+ if (sample.includes(0))
61
+ return true;
62
+ let suspicious = 0;
63
+ for (const byte of sample) {
64
+ if (byte === 9 || byte === 10 || byte === 13)
65
+ continue;
66
+ if (byte >= 32 && byte <= 126)
67
+ continue;
68
+ if (byte >= 0xc2)
69
+ continue;
70
+ suspicious++;
71
+ }
72
+ return suspicious / sample.length > 0.3;
73
+ }
74
+ export function formatBytes(size) {
75
+ if (size < 1024)
76
+ return `${size} B`;
77
+ if (size < 1024 * 1024)
78
+ return `${(size / 1024).toFixed(1)} KB`;
79
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
80
+ }
@@ -0,0 +1,38 @@
1
+ import type { SandboxFileInfo } from '../types.js';
2
+ export declare const DEFAULT_READ_LIMIT = 2000;
3
+ export declare const MAX_READ_LINES = 2000;
4
+ export declare const MAX_LINE_LENGTH = 2000;
5
+ export declare const MAX_TEXT_BYTES: number;
6
+ export declare function formatTextRead(input: {
7
+ displayPath: string;
8
+ content: string;
9
+ offset?: number;
10
+ limit?: number;
11
+ }): {
12
+ output: string;
13
+ metadata: {
14
+ type: string;
15
+ offset: number;
16
+ limit: number;
17
+ lineCount: number;
18
+ returnedLines: number;
19
+ hasMore: boolean;
20
+ truncatedByBytes: boolean;
21
+ };
22
+ };
23
+ export declare function formatDirectoryRead(input: {
24
+ displayPath: string;
25
+ entries: SandboxFileInfo[];
26
+ offset?: number;
27
+ limit?: number;
28
+ }): {
29
+ output: string;
30
+ metadata: {
31
+ type: string;
32
+ offset: number;
33
+ limit: number;
34
+ entryCount: number;
35
+ returnedEntries: number;
36
+ hasMore: boolean;
37
+ };
38
+ };