@myagentroam/node 0.1.3 → 0.1.6

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 (195) hide show
  1. package/README.md +9 -1
  2. package/dist/capabilities.d.ts +2 -0
  3. package/dist/capabilities.js +15 -4
  4. package/dist/claude-agent-sdk.d.ts +4 -16
  5. package/dist/claude-agent-sdk.js +18 -79
  6. package/dist/claude-channel.js +0 -1
  7. package/dist/codex-app-server.d.ts +5 -0
  8. package/dist/codex-app-server.js +26 -5
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +18 -2
  11. package/dist/connector/node-connector-options.d.ts +26 -0
  12. package/dist/connector/node-connector-options.js +1 -0
  13. package/dist/connector/node-operation-router.d.ts +9 -0
  14. package/dist/connector/node-operation-router.js +22 -0
  15. package/dist/connector.d.ts +62 -356
  16. package/dist/connector.js +714 -6176
  17. package/dist/control-outbound-scheduler.d.ts +26 -0
  18. package/dist/control-outbound-scheduler.js +121 -0
  19. package/dist/database.d.ts +4 -0
  20. package/dist/database.js +15 -10
  21. package/dist/event-buffer.js +0 -1
  22. package/dist/fake-runner.js +0 -1
  23. package/dist/health.js +0 -1
  24. package/dist/main.js +6 -1
  25. package/dist/migrations/v001.js +0 -1
  26. package/dist/migrations/v002.js +0 -1
  27. package/dist/migrations/v003.d.ts +4 -0
  28. package/dist/migrations/v003.js +44 -0
  29. package/dist/native-jsonl-reader.d.ts +14 -0
  30. package/dist/native-jsonl-reader.js +97 -0
  31. package/dist/native-session-history.d.ts +41 -4
  32. package/dist/native-session-history.js +340 -159
  33. package/dist/opencode-runtime.d.ts +33 -0
  34. package/dist/opencode-runtime.js +80 -0
  35. package/dist/opencode-server.d.ts +47 -0
  36. package/dist/opencode-server.js +250 -0
  37. package/dist/operational.js +10 -3
  38. package/dist/process-tree.js +0 -1
  39. package/dist/runner/abstract-runner.d.ts +153 -0
  40. package/dist/runner/abstract-runner.js +154 -0
  41. package/dist/runner/claude/managed-run-controller.d.ts +52 -0
  42. package/dist/runner/claude/managed-run-controller.js +326 -0
  43. package/dist/runner/claude-code-runner.d.ts +70 -0
  44. package/dist/runner/claude-code-runner.js +286 -0
  45. package/dist/runner/codex/conversation-parser.d.ts +59 -0
  46. package/dist/runner/codex/conversation-parser.js +816 -0
  47. package/dist/runner/codex/managed-run-controller.d.ts +80 -0
  48. package/dist/runner/codex/managed-run-controller.js +388 -0
  49. package/dist/runner/codex-runner.d.ts +85 -0
  50. package/dist/runner/codex-runner.js +365 -0
  51. package/dist/runner/fake-test-runner.d.ts +26 -0
  52. package/dist/runner/fake-test-runner.js +47 -0
  53. package/dist/runner/opencode/conversation-parser.d.ts +27 -0
  54. package/dist/runner/opencode/conversation-parser.js +217 -0
  55. package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
  56. package/dist/runner/opencode/managed-run-controller.js +451 -0
  57. package/dist/runner/opencode-runner.d.ts +60 -0
  58. package/dist/runner/opencode-runner.js +330 -0
  59. package/dist/runner/runner-registry.d.ts +19 -0
  60. package/dist/runner/runner-registry.js +75 -0
  61. package/dist/runner-command-engine.js +0 -1
  62. package/dist/runner-profiles.d.ts +4 -0
  63. package/dist/runner-profiles.js +76 -3
  64. package/dist/runner-usage.d.ts +2 -2
  65. package/dist/runner-usage.js +3 -6
  66. package/dist/runtime-state.js +26 -14
  67. package/dist/service/attachment-domain-state.d.ts +5 -0
  68. package/dist/service/attachment-domain-state.js +5 -0
  69. package/dist/service/conversation-history-service.d.ts +34 -0
  70. package/dist/service/conversation-history-service.js +158 -0
  71. package/dist/service/external-session-resume-service.d.ts +32 -0
  72. package/dist/service/external-session-resume-service.js +98 -0
  73. package/dist/service/fake-managed-run-service.d.ts +29 -0
  74. package/dist/service/fake-managed-run-service.js +62 -0
  75. package/dist/service/managed-runner-session-service.d.ts +12 -0
  76. package/dist/service/managed-runner-session-service.js +35 -0
  77. package/dist/service/native-session-projection-service.d.ts +51 -0
  78. package/dist/service/native-session-projection-service.js +271 -0
  79. package/dist/service/native-session-watch-service.d.ts +37 -0
  80. package/dist/service/native-session-watch-service.js +150 -0
  81. package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
  82. package/dist/service/node-connection-lifecycle-service.js +85 -0
  83. package/dist/service/node-control-channel.d.ts +32 -0
  84. package/dist/service/node-control-channel.js +85 -0
  85. package/dist/service/node-control-message-service.d.ts +28 -0
  86. package/dist/service/node-control-message-service.js +104 -0
  87. package/dist/service/node-request-service.d.ts +14 -0
  88. package/dist/service/node-request-service.js +50 -0
  89. package/dist/service/node-run-event-bridge.d.ts +40 -0
  90. package/dist/service/node-run-event-bridge.js +83 -0
  91. package/dist/service/node-terminal-channel.d.ts +29 -0
  92. package/dist/service/node-terminal-channel.js +182 -0
  93. package/dist/service/node-upgrade-coordinator.d.ts +29 -0
  94. package/dist/service/node-upgrade-coordinator.js +137 -0
  95. package/dist/service/node-workspace-coordinator.d.ts +32 -0
  96. package/dist/service/node-workspace-coordinator.js +80 -0
  97. package/dist/service/run-attachment-service.d.ts +37 -0
  98. package/dist/service/run-attachment-service.js +160 -0
  99. package/dist/service/run-domain-state.d.ts +4 -0
  100. package/dist/service/run-domain-state.js +4 -0
  101. package/dist/service/run-event-service.d.ts +31 -0
  102. package/dist/service/run-event-service.js +102 -0
  103. package/dist/service/run-workbench-service.d.ts +27 -0
  104. package/dist/service/run-workbench-service.js +113 -0
  105. package/dist/service/runner-channel-message-service.d.ts +13 -0
  106. package/dist/service/runner-channel-message-service.js +20 -0
  107. package/dist/service/runner-interaction-service.d.ts +40 -0
  108. package/dist/service/runner-interaction-service.js +246 -0
  109. package/dist/service/runner-service.d.ts +44 -0
  110. package/dist/service/runner-service.js +68 -0
  111. package/dist/service/session-catalog-service.d.ts +45 -0
  112. package/dist/service/session-catalog-service.js +224 -0
  113. package/dist/service/session-command-service.d.ts +23 -0
  114. package/dist/service/session-command-service.js +69 -0
  115. package/dist/service/session-context-service.d.ts +34 -0
  116. package/dist/service/session-context-service.js +87 -0
  117. package/dist/service/session-domain-state.d.ts +8 -0
  118. package/dist/service/session-domain-state.js +8 -0
  119. package/dist/service/session-identity-service.d.ts +24 -0
  120. package/dist/service/session-identity-service.js +86 -0
  121. package/dist/service/session-lifecycle-service.d.ts +31 -0
  122. package/dist/service/session-lifecycle-service.js +242 -0
  123. package/dist/service/session-message-service.d.ts +33 -0
  124. package/dist/service/session-message-service.js +157 -0
  125. package/dist/service/session-presentation-service.d.ts +33 -0
  126. package/dist/service/session-presentation-service.js +36 -0
  127. package/dist/service/session-query-service.d.ts +31 -0
  128. package/dist/service/session-query-service.js +77 -0
  129. package/dist/service/terminal-relay-state.d.ts +6 -0
  130. package/dist/service/terminal-relay-state.js +3 -0
  131. package/dist/service/terminal-service.d.ts +12 -0
  132. package/dist/service/terminal-service.js +52 -0
  133. package/dist/service/workbench-event-service.d.ts +21 -0
  134. package/dist/service/workbench-event-service.js +53 -0
  135. package/dist/service/workbench-manifest-service.d.ts +89 -0
  136. package/dist/service/workbench-manifest-service.js +70 -0
  137. package/dist/service/workspace-change-service.d.ts +9 -0
  138. package/dist/service/workspace-change-service.js +86 -0
  139. package/dist/service/workspace-content-search-service.d.ts +14 -0
  140. package/dist/service/workspace-content-search-service.js +477 -0
  141. package/dist/service/workspace-domain-state.d.ts +75 -0
  142. package/dist/service/workspace-domain-state.js +40 -0
  143. package/dist/service/workspace-file-service.d.ts +17 -0
  144. package/dist/service/workspace-file-service.js +109 -0
  145. package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
  146. package/dist/service/workspace-queue-workbench-service.js +226 -0
  147. package/dist/service/workspace-service.d.ts +15 -0
  148. package/dist/service/workspace-service.js +67 -0
  149. package/dist/service/workspace-session-service.d.ts +23 -0
  150. package/dist/service/workspace-session-service.js +139 -0
  151. package/dist/service/workspace-upload-service.d.ts +37 -0
  152. package/dist/service/workspace-upload-service.js +240 -0
  153. package/dist/service/workspace-watch-service.d.ts +18 -0
  154. package/dist/service/workspace-watch-service.js +120 -0
  155. package/dist/service/workspace-workbench-service.d.ts +47 -0
  156. package/dist/service/workspace-workbench-service.js +223 -0
  157. package/dist/service.js +2 -3
  158. package/dist/storage.js +0 -1
  159. package/dist/supervisor.d.ts +1 -0
  160. package/dist/supervisor.js +154 -0
  161. package/dist/terminal.d.ts +1 -0
  162. package/dist/terminal.js +1 -2
  163. package/dist/util/node-operation-parsers.d.ts +92 -0
  164. package/dist/util/node-operation-parsers.js +382 -0
  165. package/dist/util/runner-native-session-parsers.d.ts +72 -0
  166. package/dist/util/runner-native-session-parsers.js +381 -0
  167. package/dist/util/secret-environment.d.ts +1 -0
  168. package/dist/util/secret-environment.js +25 -0
  169. package/dist/workspace.d.ts +62 -2
  170. package/dist/workspace.js +539 -57
  171. package/package.json +3 -2
  172. package/dist/capabilities.js.map +0 -1
  173. package/dist/claude-agent-sdk.js.map +0 -1
  174. package/dist/claude-channel.js.map +0 -1
  175. package/dist/codex-app-server.js.map +0 -1
  176. package/dist/config.js.map +0 -1
  177. package/dist/connector.js.map +0 -1
  178. package/dist/database.js.map +0 -1
  179. package/dist/event-buffer.js.map +0 -1
  180. package/dist/fake-runner.js.map +0 -1
  181. package/dist/health.js.map +0 -1
  182. package/dist/main.js.map +0 -1
  183. package/dist/migrations/v001.js.map +0 -1
  184. package/dist/migrations/v002.js.map +0 -1
  185. package/dist/native-session-history.js.map +0 -1
  186. package/dist/operational.js.map +0 -1
  187. package/dist/process-tree.js.map +0 -1
  188. package/dist/runner-command-engine.js.map +0 -1
  189. package/dist/runner-profiles.js.map +0 -1
  190. package/dist/runner-usage.js.map +0 -1
  191. package/dist/runtime-state.js.map +0 -1
  192. package/dist/service.js.map +0 -1
  193. package/dist/storage.js.map +0 -1
  194. package/dist/terminal.js.map +0 -1
  195. package/dist/workspace.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { parseComposerAttachmentUpload, validComposerAttachmentName } from '../util/node-operation-parsers.js';
5
+ import { preflightWorkspaceUpload, workspaceUploadTemporaryName } from '../workspace.js';
6
+ const CHUNK_BYTES = 512 * 1024;
7
+ const TTL_MS = 60 * 60_000;
8
+ const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
9
+ export class WorkspaceUploadService {
10
+ state;
11
+ onCompleted;
12
+ constructor(state, onCompleted) {
13
+ this.state = state;
14
+ this.onCompleted = onCompleted;
15
+ }
16
+ async preflight(workspace, input) {
17
+ if ((input.directory !== undefined && typeof input.directory !== 'string') ||
18
+ !Array.isArray(input.files) ||
19
+ input.files.length === 0 ||
20
+ input.files.some((file) => !plain(file) || typeof file.name !== 'string'))
21
+ throw new Error('UPLOAD_INVALID');
22
+ const seen = new Set();
23
+ return Promise.all(input.files.map(async (file) => {
24
+ const name = file.name;
25
+ if (seen.has(name))
26
+ return { name, status: 'INVALID_NAME', path: null };
27
+ seen.add(name);
28
+ const target = await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', name);
29
+ return { name, status: target.status, path: target.path || null };
30
+ }));
31
+ }
32
+ async create(workspace, input) {
33
+ const attachment = parseComposerAttachmentUpload(input);
34
+ if ((input.directory !== undefined && typeof input.directory !== 'string') ||
35
+ typeof input.name !== 'string' ||
36
+ !Number.isSafeInteger(input.size) ||
37
+ input.size < 0 ||
38
+ typeof input.overwrite !== 'boolean' ||
39
+ (attachment !== undefined && input.directory !== undefined))
40
+ throw new Error('UPLOAD_INVALID');
41
+ if (attachment !== undefined &&
42
+ (!validComposerAttachmentName(input.name) ||
43
+ input.size > MAX_ATTACHMENT_BYTES ||
44
+ this.state.composerAttachments.has(attachment.id) ||
45
+ [...this.state.uploads.values()].some((upload) => upload.composerAttachment?.id === attachment.id)))
46
+ throw new Error('COMPOSER_ATTACHMENT_INVALID');
47
+ const target = attachment === undefined
48
+ ? await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', input.name)
49
+ : await this.attachmentTarget(workspace.path, attachment.id, input.name);
50
+ if (target.status === 'INVALID_NAME')
51
+ throw new Error('UPLOAD_NAME_INVALID');
52
+ if (target.status === 'EXISTS_DIRECTORY')
53
+ throw new Error('UPLOAD_TARGET_DIRECTORY');
54
+ if (target.status === 'EXISTS_FILE' && !input.overwrite)
55
+ throw new Error('UPLOAD_TARGET_EXISTS');
56
+ if ([...this.state.uploads.values()].some((upload) => upload.workspaceId === workspace.id && upload.targetPath === target.absolutePath))
57
+ throw new Error('UPLOAD_TARGET_BUSY');
58
+ const id = randomUUID();
59
+ const temporaryPath = resolve(dirname(target.absolutePath), workspaceUploadTemporaryName(id));
60
+ const handle = await open(temporaryPath, 'wx');
61
+ const timer = setTimeout(() => {
62
+ const upload = this.state.uploads.get(id);
63
+ if (upload !== undefined)
64
+ void this.remove(upload);
65
+ }, TTL_MS);
66
+ const upload = {
67
+ id,
68
+ workspaceId: workspace.id,
69
+ path: target.path,
70
+ targetPath: target.absolutePath,
71
+ temporaryPath,
72
+ size: input.size,
73
+ overwrite: input.overwrite,
74
+ ...(attachment === undefined ? {} : { composerAttachment: attachment }),
75
+ handle,
76
+ receivedBytes: 0,
77
+ timer
78
+ };
79
+ this.state.uploads.set(id, upload);
80
+ return this.status(id);
81
+ }
82
+ async write(input) {
83
+ if (typeof input.uploadId !== 'string' ||
84
+ !Number.isSafeInteger(input.offset) ||
85
+ input.offset < 0 ||
86
+ typeof input.dataBase64 !== 'string')
87
+ throw new Error('UPLOAD_CHUNK_INVALID');
88
+ const upload = this.require(input.uploadId);
89
+ if (input.offset !== upload.receivedBytes)
90
+ throw new Error('UPLOAD_OFFSET_INVALID');
91
+ const bytes = Buffer.from(input.dataBase64, 'base64');
92
+ if (bytes.length === 0 || bytes.length > CHUNK_BYTES)
93
+ throw new Error('UPLOAD_CHUNK_INVALID');
94
+ if (upload.receivedBytes + bytes.length > upload.size)
95
+ throw new Error('UPLOAD_SIZE_INVALID');
96
+ const result = await upload.handle.write(bytes, 0, bytes.length, upload.receivedBytes);
97
+ if (result.bytesWritten !== bytes.length)
98
+ throw new Error('UPLOAD_WRITE_FAILED');
99
+ upload.receivedBytes += bytes.length;
100
+ return this.status(upload.id);
101
+ }
102
+ status(uploadId) {
103
+ const upload = this.require(uploadId);
104
+ return {
105
+ uploadId: upload.id,
106
+ path: upload.path,
107
+ receivedBytes: upload.receivedBytes,
108
+ size: upload.size
109
+ };
110
+ }
111
+ async complete(uploadId) {
112
+ const upload = this.require(uploadId);
113
+ if (upload.receivedBytes !== upload.size)
114
+ throw new Error('UPLOAD_INCOMPLETE');
115
+ await upload.handle.sync();
116
+ await upload.handle.close();
117
+ const existing = await lstat(upload.targetPath).catch((error) => error.code === 'ENOENT' ? undefined : Promise.reject(error));
118
+ if (existing?.isSymbolicLink())
119
+ throw new Error('FILE_PATH_ESCAPE');
120
+ if (existing !== undefined && (!existing.isFile() || !upload.overwrite))
121
+ throw new Error('UPLOAD_TARGET_EXISTS');
122
+ try {
123
+ await rename(upload.temporaryPath, upload.targetPath);
124
+ }
125
+ catch (error) {
126
+ if (error.code !== 'EEXIST')
127
+ throw error;
128
+ if (!upload.overwrite)
129
+ throw new Error('UPLOAD_TARGET_EXISTS');
130
+ await rm(upload.targetPath, { force: false });
131
+ await rename(upload.temporaryPath, upload.targetPath);
132
+ }
133
+ clearTimeout(upload.timer);
134
+ this.state.uploads.delete(upload.id);
135
+ this.onCompleted(upload.workspaceId);
136
+ if (upload.composerAttachment !== undefined)
137
+ this.state.composerAttachments.set(upload.composerAttachment.id, {
138
+ id: upload.composerAttachment.id,
139
+ workspaceId: upload.workspaceId,
140
+ path: upload.path,
141
+ targetPath: upload.targetPath,
142
+ name: upload.composerAttachment.name,
143
+ mime: upload.composerAttachment.mime,
144
+ size: upload.size
145
+ });
146
+ return {
147
+ uploadId: upload.id,
148
+ path: upload.path,
149
+ receivedBytes: upload.receivedBytes,
150
+ size: upload.size
151
+ };
152
+ }
153
+ async cancel(uploadId) {
154
+ if (typeof uploadId !== 'string')
155
+ throw new Error('UPLOAD_NOT_FOUND');
156
+ const active = this.state.uploads.get(uploadId);
157
+ if (active !== undefined)
158
+ return this.remove(active);
159
+ const attachment = this.state.composerAttachments.get(uploadId);
160
+ if (attachment === undefined)
161
+ throw new Error('UPLOAD_NOT_FOUND');
162
+ this.state.composerAttachments.delete(uploadId);
163
+ await rm(dirname(attachment.targetPath), { recursive: true, force: true });
164
+ }
165
+ async resolve(workspaceId, ids) {
166
+ return Promise.all(ids.map(async (id) => {
167
+ const stored = this.state.composerAttachments.get(id);
168
+ if (stored === undefined || stored.workspaceId !== workspaceId)
169
+ throw new Error('COMPOSER_ATTACHMENT_UNAVAILABLE');
170
+ const metadata = await lstat(stored.targetPath).catch(() => undefined);
171
+ if (metadata === undefined ||
172
+ metadata.isSymbolicLink() ||
173
+ !metadata.isFile() ||
174
+ metadata.size !== stored.size)
175
+ throw new Error('COMPOSER_ATTACHMENT_UNAVAILABLE');
176
+ const bytes = await readFile(stored.targetPath);
177
+ return {
178
+ attachment: {
179
+ name: stored.name,
180
+ mime: stored.mime,
181
+ dataBase64: bytes.toString('base64')
182
+ },
183
+ path: stored.path
184
+ };
185
+ }));
186
+ }
187
+ async shutdown() {
188
+ await Promise.allSettled([...this.state.uploads.values()].map((upload) => this.remove(upload)));
189
+ for (const attachment of this.state.composerAttachments.values())
190
+ await rm(dirname(attachment.targetPath), { recursive: true, force: true });
191
+ this.state.composerAttachments.clear();
192
+ }
193
+ require(id) {
194
+ if (typeof id !== 'string')
195
+ throw new Error('UPLOAD_NOT_FOUND');
196
+ const upload = this.state.uploads.get(id);
197
+ if (upload === undefined)
198
+ throw new Error('UPLOAD_NOT_FOUND');
199
+ return upload;
200
+ }
201
+ async remove(upload) {
202
+ this.state.uploads.delete(upload.id);
203
+ clearTimeout(upload.timer);
204
+ await upload.handle.close().catch(() => undefined);
205
+ await rm(upload.temporaryPath, { force: true }).catch(() => undefined);
206
+ if (upload.composerAttachment !== undefined)
207
+ await rm(dirname(upload.targetPath), { recursive: true, force: true }).catch(() => undefined);
208
+ }
209
+ async attachmentTarget(workspacePath, id, name) {
210
+ const mar = resolve(workspacePath, '.mar');
211
+ await mkdir(mar, { recursive: true, mode: 0o700 });
212
+ await safeDirectory(mar);
213
+ const temp = join(mar, 'temp-files');
214
+ await mkdir(temp, { recursive: true, mode: 0o700 });
215
+ await safeDirectory(temp);
216
+ const directory = join(temp, id);
217
+ try {
218
+ await mkdir(directory, { mode: 0o700 });
219
+ }
220
+ catch (error) {
221
+ if (error.code === 'EEXIST')
222
+ throw new Error('COMPOSER_ATTACHMENT_INVALID');
223
+ throw error;
224
+ }
225
+ await safeDirectory(directory);
226
+ return {
227
+ path: `.mar/temp-files/${id}/${name}`,
228
+ absolutePath: join(directory, name),
229
+ status: 'READY'
230
+ };
231
+ }
232
+ }
233
+ async function safeDirectory(path) {
234
+ const value = await lstat(path);
235
+ if (!value.isDirectory() || value.isSymbolicLink())
236
+ throw new Error('COMPOSER_ATTACHMENT_DIRECTORY_INVALID');
237
+ }
238
+ function plain(value) {
239
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
240
+ }
@@ -0,0 +1,18 @@
1
+ import { WorkspaceDomainState, type WorkspaceWatch } from './workspace-domain-state.js';
2
+ export declare class WorkspaceWatchService {
3
+ private readonly state;
4
+ private readonly onSessionWatchRequired;
5
+ private readonly onSessionWatchUnused;
6
+ constructor(state: WorkspaceDomainState, onSessionWatchRequired: (workspaceId: string) => void, onSessionWatchUnused: (workspaceId: string) => void);
7
+ parseTopics(input: Record<string, unknown>): WorkspaceWatch['topics'];
8
+ key(input: Record<string, unknown>): string;
9
+ sessionHash(input: Record<string, unknown>): string;
10
+ create(workspaceId: string, watchId: string, sessionHash: string, topics: WorkspaceWatch['topics']): WorkspaceWatch;
11
+ renew(workspaceId: string, key: string): WorkspaceWatch | undefined;
12
+ remove(workspaceId: string, key: string): void;
13
+ clear(workspaceId: string): void;
14
+ clearAll(): void;
15
+ nextRevision(workspaceId: string): number;
16
+ topicActive(workspaceId: string, topic: 'files' | 'changes' | 'sessions'): boolean;
17
+ private expiryTimer;
18
+ }
@@ -0,0 +1,120 @@
1
+ const WATCH_TTL_MS = 45_000;
2
+ export class WorkspaceWatchService {
3
+ state;
4
+ onSessionWatchRequired;
5
+ onSessionWatchUnused;
6
+ constructor(state, onSessionWatchRequired, onSessionWatchUnused) {
7
+ this.state = state;
8
+ this.onSessionWatchRequired = onSessionWatchRequired;
9
+ this.onSessionWatchUnused = onSessionWatchUnused;
10
+ }
11
+ parseTopics(input) {
12
+ if (!plain(input.topics))
13
+ throw new Error('WORKSPACE_WATCH_INVALID');
14
+ const sessions = input.topics.sessions === true;
15
+ const changes = input.topics.changes === true;
16
+ const rawFiles = input.topics.files;
17
+ let files;
18
+ if (rawFiles !== undefined) {
19
+ if (!plain(rawFiles) || typeof rawFiles.path !== 'string')
20
+ throw new Error('WORKSPACE_WATCH_INVALID');
21
+ if ((rawFiles.cursor !== undefined && typeof rawFiles.cursor !== 'string') ||
22
+ (rawFiles.limit !== undefined &&
23
+ (typeof rawFiles.limit !== 'number' ||
24
+ !Number.isInteger(rawFiles.limit) ||
25
+ rawFiles.limit < 1 ||
26
+ rawFiles.limit > 200)))
27
+ throw new Error('WORKSPACE_WATCH_INVALID');
28
+ files = {
29
+ path: rawFiles.path,
30
+ ...(typeof rawFiles.cursor === 'string' ? { cursor: rawFiles.cursor } : {}),
31
+ ...(typeof rawFiles.limit === 'number' ? { limit: rawFiles.limit } : {})
32
+ };
33
+ }
34
+ if (!sessions && !changes && files === undefined)
35
+ throw new Error('WORKSPACE_WATCH_INVALID');
36
+ return { sessions, changes, files };
37
+ }
38
+ key(input) {
39
+ return `${this.sessionHash(input)}:${input.watchId}`;
40
+ }
41
+ sessionHash(input) {
42
+ return typeof input.__workspaceWatchSessionHash === 'string'
43
+ ? input.__workspaceWatchSessionHash
44
+ : 'local-test';
45
+ }
46
+ create(workspaceId, watchId, sessionHash, topics) {
47
+ const key = `${sessionHash}:${watchId}`;
48
+ this.remove(workspaceId, key);
49
+ const watch = {
50
+ workspaceId,
51
+ watchId,
52
+ sessionHash,
53
+ expiresAt: Date.now() + WATCH_TTL_MS,
54
+ topics,
55
+ timer: undefined
56
+ };
57
+ watch.timer = this.expiryTimer(workspaceId, key);
58
+ const watches = this.state.watches.get(workspaceId) ?? new Map();
59
+ watches.set(key, watch);
60
+ this.state.watches.set(workspaceId, watches);
61
+ if (topics.sessions)
62
+ this.onSessionWatchRequired(workspaceId);
63
+ return watch;
64
+ }
65
+ renew(workspaceId, key) {
66
+ const watch = this.state.watches.get(workspaceId)?.get(key);
67
+ if (watch === undefined || watch.expiresAt <= Date.now()) {
68
+ this.remove(workspaceId, key);
69
+ return undefined;
70
+ }
71
+ clearTimeout(watch.timer);
72
+ watch.expiresAt = Date.now() + WATCH_TTL_MS;
73
+ watch.timer = this.expiryTimer(workspaceId, key);
74
+ return watch;
75
+ }
76
+ remove(workspaceId, key) {
77
+ const watches = this.state.watches.get(workspaceId);
78
+ const watch = watches?.get(key);
79
+ if (watch === undefined)
80
+ return;
81
+ clearTimeout(watch.timer);
82
+ watches?.delete(key);
83
+ if (watches?.size === 0)
84
+ this.state.watches.delete(workspaceId);
85
+ if (!this.topicActive(workspaceId, 'sessions'))
86
+ this.onSessionWatchUnused(workspaceId);
87
+ }
88
+ clear(workspaceId) {
89
+ const watches = this.state.watches.get(workspaceId);
90
+ if (watches !== undefined)
91
+ for (const watch of watches.values())
92
+ clearTimeout(watch.timer);
93
+ this.state.watches.delete(workspaceId);
94
+ this.state.watchRevisions.delete(workspaceId);
95
+ this.onSessionWatchUnused(workspaceId);
96
+ }
97
+ clearAll() {
98
+ for (const watches of this.state.watches.values())
99
+ for (const watch of watches.values())
100
+ clearTimeout(watch.timer);
101
+ this.state.watches.clear();
102
+ this.state.watchRevisions.clear();
103
+ }
104
+ nextRevision(workspaceId) {
105
+ const revision = (this.state.watchRevisions.get(workspaceId) ?? 0) + 1;
106
+ this.state.watchRevisions.set(workspaceId, revision);
107
+ return revision;
108
+ }
109
+ topicActive(workspaceId, topic) {
110
+ return [...(this.state.watches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files' ? watch.topics.files !== undefined : watch.topics[topic]);
111
+ }
112
+ expiryTimer(workspaceId, key) {
113
+ const timer = setTimeout(() => this.remove(workspaceId, key), WATCH_TTL_MS);
114
+ timer.unref();
115
+ return timer;
116
+ }
117
+ }
118
+ function plain(value) {
119
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
120
+ }
@@ -0,0 +1,47 @@
1
+ import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
+ import type { NodeWorkspace } from '../database.js';
3
+ import { NodeRuntimeState } from '../runtime-state.js';
4
+ import { WorkspaceChangeService } from './workspace-change-service.js';
5
+ import { WorkspaceFileService } from './workspace-file-service.js';
6
+ import { WorkspaceUploadService } from './workspace-upload-service.js';
7
+ import { WorkspaceWatchService } from './workspace-watch-service.js';
8
+ export interface WorkspaceWorkbenchServiceOptions {
9
+ readonly requireWorkspace: (workspaceId: unknown) => NodeWorkspace;
10
+ readonly runtime: NodeRuntimeState;
11
+ readonly watch: WorkspaceWatchService;
12
+ readonly files: WorkspaceFileService;
13
+ readonly uploads: WorkspaceUploadService;
14
+ readonly changes: WorkspaceChangeService;
15
+ readonly listSessions: (workspaceId: string) => Promise<unknown>;
16
+ readonly invalidate: (workspaceId: string) => void;
17
+ }
18
+ export declare class WorkspaceWorkbenchService {
19
+ private readonly options;
20
+ private readonly contentSearch;
21
+ constructor(options: WorkspaceWorkbenchServiceOptions);
22
+ operations(): Readonly<Record<string, NodeOperationHandler>>;
23
+ private activity;
24
+ private watch;
25
+ private unwatch;
26
+ private listFiles;
27
+ private readFile;
28
+ private searchFiles;
29
+ private mutateFile;
30
+ private searchContent;
31
+ private preflight;
32
+ private createUpload;
33
+ private writeUpload;
34
+ private completeUpload;
35
+ private cancelUpload;
36
+ private currentChanges;
37
+ private changeDiff;
38
+ private restoreChange;
39
+ private gitRepository;
40
+ private refs;
41
+ private gitRefs;
42
+ private gitHistory;
43
+ private gitCommitFiles;
44
+ private gitCommitFileDiff;
45
+ private diff;
46
+ private gitWorkspace;
47
+ }
@@ -0,0 +1,223 @@
1
+ import { listGitHistoryRefs, readGitHistory, readGitHistoryFileDiff, readGitHistoryFiles, readCurrentChangeDiff, readRestrictedDiff, resolveGitRepository, restoreCurrentChange } from '../workspace.js';
2
+ import { WorkspaceContentSearchService } from './workspace-content-search-service.js';
3
+ export class WorkspaceWorkbenchService {
4
+ options;
5
+ contentSearch;
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.contentSearch = new WorkspaceContentSearchService(options.files);
9
+ }
10
+ operations() {
11
+ return {
12
+ 'workspace.activity.read': (data) => this.activity(record(data)),
13
+ 'workspace.watch': (data) => this.watch(record(data)),
14
+ 'workspace.unwatch': (data) => this.unwatch(record(data)),
15
+ 'workspace.files.list': (data) => this.listFiles(record(data)),
16
+ 'workspace.file.read': (data) => this.readFile(record(data)),
17
+ 'workspace.files.search': (data) => this.searchFiles(record(data)),
18
+ 'workspace.files.mutate': (data) => this.mutateFile(record(data)),
19
+ 'workspace.content.search': (data) => this.searchContent(record(data)),
20
+ 'workspace.files.upload.preflight': (data) => this.preflight(record(data)),
21
+ 'workspace.files.upload.create': (data) => this.createUpload(record(data)),
22
+ 'workspace.files.upload.chunk': (data) => this.writeUpload(record(data)),
23
+ 'workspace.files.upload.status': (data) => ({
24
+ upload: this.options.uploads.status(record(data).uploadId)
25
+ }),
26
+ 'workspace.files.upload.complete': (data) => this.completeUpload(record(data)),
27
+ 'workspace.files.upload.cancel': (data) => this.cancelUpload(record(data)),
28
+ 'workspace.changes.current': (data) => this.currentChanges(record(data)),
29
+ 'workspace.change.diff': (data) => this.changeDiff(record(data)),
30
+ 'workspace.change.restore': (data) => this.restoreChange(record(data)),
31
+ 'workspace.git.refs.list': (data) => this.gitRefs(record(data)),
32
+ 'workspace.git.history.list': (data) => this.gitHistory(record(data)),
33
+ 'workspace.git.commit.files': (data) => this.gitCommitFiles(record(data)),
34
+ 'workspace.git.commit.file.diff': (data) => this.gitCommitFileDiff(record(data)),
35
+ 'workspace.diff': (data) => this.diff(record(data))
36
+ };
37
+ }
38
+ activity(input) {
39
+ if (typeof input.workspaceId !== 'string' ||
40
+ (input.cursor !== undefined && typeof input.cursor !== 'string') ||
41
+ (input.limit !== undefined &&
42
+ (!Number.isInteger(input.limit) ||
43
+ input.limit < 1 ||
44
+ input.limit > 500)))
45
+ throw new Error('EVENT_CURSOR_INVALID');
46
+ return this.options.runtime.listWorkspaceActivity({
47
+ workspaceId: input.workspaceId,
48
+ ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}),
49
+ ...(typeof input.limit === 'number' ? { limit: input.limit } : {})
50
+ });
51
+ }
52
+ async watch(input) {
53
+ if (typeof input.workspaceId !== 'string' ||
54
+ typeof input.watchId !== 'string' ||
55
+ !/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId) ||
56
+ (input.mode !== 'initial' && input.mode !== 'renew'))
57
+ throw new Error('WORKSPACE_WATCH_INVALID');
58
+ const workspace = this.options.requireWorkspace(input.workspaceId);
59
+ if (input.mode === 'renew') {
60
+ const watch = this.options.watch.renew(workspace.id, this.options.watch.key(input));
61
+ if (watch === undefined)
62
+ throw new Error('WORKSPACE_WATCH_NOT_FOUND');
63
+ return { renewed: true };
64
+ }
65
+ const topics = this.options.watch.parseTopics(input);
66
+ const snapshot = {};
67
+ if (topics.sessions)
68
+ snapshot.sessions = await this.options.listSessions(workspace.id);
69
+ if (topics.files !== undefined)
70
+ snapshot.files = { files: await this.options.files.list(workspace, topics.files) };
71
+ if (topics.changes)
72
+ snapshot.changes =
73
+ workspace.kind === 'GIT_WORKSPACE' ? await this.options.changes.current(workspace) : null;
74
+ if (topics.files !== undefined || topics.changes)
75
+ await this.options.files.index(workspace).observe();
76
+ const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), topics);
77
+ return { watch: { expiresAt: watch.expiresAt }, snapshot };
78
+ }
79
+ unwatch(input) {
80
+ if (typeof input.workspaceId !== 'string' ||
81
+ typeof input.watchId !== 'string' ||
82
+ !/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId))
83
+ throw new Error('WORKSPACE_WATCH_INVALID');
84
+ this.options.watch.remove(input.workspaceId, this.options.watch.key(input));
85
+ return { stopped: true };
86
+ }
87
+ async listFiles(input) {
88
+ const workspace = this.options.requireWorkspace(input.workspaceId);
89
+ return { files: await this.options.files.list(workspace, input) };
90
+ }
91
+ async readFile(input) {
92
+ const workspace = this.options.requireWorkspace(input.workspaceId);
93
+ return { file: await this.options.files.read(workspace, input) };
94
+ }
95
+ async searchFiles(input) {
96
+ const workspace = this.options.requireWorkspace(input.workspaceId);
97
+ return { files: await this.options.files.search(workspace, input) };
98
+ }
99
+ async mutateFile(input) {
100
+ const workspace = this.options.requireWorkspace(input.workspaceId);
101
+ return { file: await this.options.files.mutate(workspace, input) };
102
+ }
103
+ async searchContent(input) {
104
+ const workspace = this.options.requireWorkspace(input.workspaceId);
105
+ return { search: await this.contentSearch.search(workspace, input) };
106
+ }
107
+ async preflight(input) {
108
+ return {
109
+ uploads: await this.options.uploads.preflight(this.options.requireWorkspace(input.workspaceId), input)
110
+ };
111
+ }
112
+ async createUpload(input) {
113
+ return {
114
+ upload: await this.options.uploads.create(this.options.requireWorkspace(input.workspaceId), input)
115
+ };
116
+ }
117
+ async writeUpload(input) {
118
+ return { upload: await this.options.uploads.write(input) };
119
+ }
120
+ async completeUpload(input) {
121
+ return { upload: await this.options.uploads.complete(input.uploadId) };
122
+ }
123
+ async cancelUpload(input) {
124
+ await this.options.uploads.cancel(input.uploadId);
125
+ return { cancelled: true };
126
+ }
127
+ async currentChanges(input) {
128
+ return this.options.changes.current(this.options.requireWorkspace(input.workspaceId));
129
+ }
130
+ async changeDiff(input) {
131
+ if (typeof input.workspaceId !== 'string' ||
132
+ typeof input.path !== 'string' ||
133
+ (input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
134
+ throw new Error('GIT_DIFF_INVALID');
135
+ const workspace = this.gitWorkspace(input.workspaceId);
136
+ const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
137
+ return { diff: await readCurrentChangeDiff(repository, input.path) };
138
+ }
139
+ async restoreChange(input) {
140
+ if (typeof input.workspaceId !== 'string' ||
141
+ typeof input.path !== 'string' ||
142
+ (input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string') ||
143
+ (input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
144
+ throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
145
+ const workspace = this.gitWorkspace(input.workspaceId);
146
+ if (this.options.runtime.hasActiveWorkspaceLease(workspace.id))
147
+ throw new Error('WORKSPACE_BUSY');
148
+ const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
149
+ await restoreCurrentChange(repository, input.path, input.state);
150
+ this.options.invalidate(workspace.id);
151
+ return { restored: true };
152
+ }
153
+ async gitRepository(input) {
154
+ if (typeof input.workspaceId !== 'string' ||
155
+ (input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
156
+ throw new Error('GIT_REPOSITORY_INVALID');
157
+ const workspace = this.gitWorkspace(input.workspaceId);
158
+ return resolveGitRepository(workspace.path, input.repositoryPath ?? '');
159
+ }
160
+ refs(input) {
161
+ if (input.refs === undefined)
162
+ return undefined;
163
+ if (!Array.isArray(input.refs) ||
164
+ input.refs.length > 128 ||
165
+ input.refs.some((ref) => typeof ref !== 'string'))
166
+ throw new Error('GIT_HISTORY_REF_INVALID');
167
+ return input.refs;
168
+ }
169
+ async gitRefs(input) {
170
+ return { refs: await listGitHistoryRefs(await this.gitRepository(input)) };
171
+ }
172
+ async gitHistory(input) {
173
+ const refs = this.refs(input);
174
+ if (input.cursor !== undefined && typeof input.cursor !== 'string')
175
+ throw new Error('GIT_HISTORY_CURSOR_INVALID');
176
+ if (input.limit !== undefined &&
177
+ (typeof input.limit !== 'number' ||
178
+ !Number.isInteger(input.limit) ||
179
+ input.limit < 1 ||
180
+ input.limit > 100))
181
+ throw new Error('GIT_HISTORY_LIMIT_INVALID');
182
+ return readGitHistory(await this.gitRepository(input), {
183
+ ...(refs === undefined ? {} : { refs }),
184
+ ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}),
185
+ ...(typeof input.limit === 'number' ? { limit: input.limit } : {})
186
+ });
187
+ }
188
+ async gitCommitFiles(input) {
189
+ if (typeof input.commit !== 'string')
190
+ throw new Error('GIT_HISTORY_COMMIT_INVALID');
191
+ return {
192
+ files: await readGitHistoryFiles(await this.gitRepository(input), input.commit, this.refs(input))
193
+ };
194
+ }
195
+ async gitCommitFileDiff(input) {
196
+ if (typeof input.commit !== 'string' ||
197
+ typeof input.path !== 'string' ||
198
+ (input.previousPath !== undefined && typeof input.previousPath !== 'string'))
199
+ throw new Error('GIT_DIFF_INVALID');
200
+ return {
201
+ diff: await readGitHistoryFileDiff(await this.gitRepository(input), input.commit, input.path, this.refs(input), input.previousPath)
202
+ };
203
+ }
204
+ async diff(input) {
205
+ if (typeof input.workspaceId !== 'string' ||
206
+ typeof input.fromRef !== 'string' ||
207
+ typeof input.toRef !== 'string')
208
+ throw new Error('GIT_DIFF_INVALID');
209
+ const workspace = this.gitWorkspace(input.workspaceId);
210
+ return { diff: await readRestrictedDiff(workspace.path, input.fromRef, input.toRef) };
211
+ }
212
+ gitWorkspace(workspaceId) {
213
+ const workspace = this.options.requireWorkspace(workspaceId);
214
+ if (workspace.kind !== 'GIT_WORKSPACE')
215
+ throw new Error('WORKSPACE_NOT_GIT');
216
+ return workspace;
217
+ }
218
+ }
219
+ function record(value) {
220
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
221
+ ? value
222
+ : {};
223
+ }
package/dist/service.js CHANGED
@@ -6,13 +6,13 @@ export function systemdUnit(executable, configPath, entrypoint, writablePaths =
6
6
  ...(entrypoint === undefined ? [] : [escapeSystemd(entrypoint)])
7
7
  ].join(' ');
8
8
  const readWritePaths = writablePaths.map((path) => escapeSystemd(resolve(path))).join(' ');
9
- return `[Unit]\nDescription=MyAgentRoam Node\nAfter=network-online.target\n\n[Service]\nType=simple\nUser=mar-node\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\n${readWritePaths.length === 0 ? '' : `ReadWritePaths=${readWritePaths}\n`}ExecStart=${command} run --config ${escapeSystemd(configPath)}\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n`;
9
+ return `[Unit]\nDescription=MyAgentRoam Node\nAfter=network-online.target\n\n[Service]\nType=simple\nUser=mar-node\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\n${readWritePaths.length === 0 ? '' : `ReadWritePaths=${readWritePaths}\n`}ExecStart=${command} supervise --config ${escapeSystemd(configPath)}\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n`;
10
10
  }
11
11
  export function windowsServiceDefinition(executable, configPath, entrypoint) {
12
12
  return JSON.stringify({
13
13
  serviceName: 'MyAgentRoamNode',
14
14
  displayName: 'MyAgentRoam Node',
15
- binaryPath: `"${executable}"${entrypoint === undefined ? '' : ` "${entrypoint}"`} run --config "${configPath}"`,
15
+ binaryPath: `"${executable}"${entrypoint === undefined ? '' : ` "${entrypoint}"`} supervise --config "${configPath}"`,
16
16
  startType: 'auto',
17
17
  account: 'LocalService'
18
18
  }, undefined, 2);
@@ -36,4 +36,3 @@ export async function removeServiceDefinition(outputPath) {
36
36
  function escapeSystemd(value) {
37
37
  return JSON.stringify(value);
38
38
  }
39
- //# sourceMappingURL=service.js.map
package/dist/storage.js CHANGED
@@ -30,4 +30,3 @@ async function existingParent(path) {
30
30
  current = parent;
31
31
  }
32
32
  }
33
- //# sourceMappingURL=storage.js.map