@zhanguym/chatroom 1.0.0 → 1.1.0-beta.1

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 (165) hide show
  1. package/dist/app/application.d.ts +0 -1
  2. package/dist/app/application.js +8 -7
  3. package/dist/app/event-bus.d.ts +13 -10
  4. package/dist/app/external-access-registry.d.ts +1 -0
  5. package/dist/app/external-access-registry.js +3 -0
  6. package/dist/app/git-access.d.ts +50 -0
  7. package/dist/app/git-access.js +2 -0
  8. package/dist/app/service-registry.d.ts +11 -0
  9. package/dist/app/service-registry.js +17 -0
  10. package/dist/app/workspace-access.d.ts +24 -0
  11. package/dist/app/workspace-access.js +2 -0
  12. package/dist/auth/ingress-policy.d.ts +2 -0
  13. package/dist/auth/ingress-policy.js +14 -2
  14. package/dist/auth/passkey-service.js +1 -1
  15. package/dist/cli/run-cli.js +1 -1
  16. package/dist/core/fs/safe-directory.d.ts +9 -0
  17. package/dist/core/fs/safe-directory.js +129 -0
  18. package/dist/core/operations/repository.d.ts +2 -0
  19. package/dist/core/runtime/line-process.d.ts +19 -0
  20. package/dist/core/runtime/line-process.js +51 -0
  21. package/dist/infrastructure/database/operation-repository.js +8 -2
  22. package/dist/infrastructure/database/schema.d.ts +1 -1
  23. package/dist/infrastructure/database/schema.js +62 -0
  24. package/dist/infrastructure/http/http-server.js +1 -1
  25. package/dist/infrastructure/logging/system-log.js +2 -1
  26. package/dist/native/linux/chatroom-computer-helper +0 -0
  27. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  28. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  29. package/dist/operations/operation-log.js +16 -4
  30. package/dist/plugins/agent/async-queue.d.ts +10 -0
  31. package/dist/plugins/agent/async-queue.js +49 -0
  32. package/dist/plugins/agent/mcp.d.ts +3 -0
  33. package/dist/plugins/agent/mcp.js +502 -0
  34. package/dist/plugins/agent/plugin.d.ts +4 -0
  35. package/dist/plugins/agent/plugin.js +35 -0
  36. package/dist/plugins/agent/provider.d.ts +132 -0
  37. package/dist/plugins/agent/providers/codex/mapping.d.ts +56 -0
  38. package/dist/plugins/agent/providers/codex/mapping.js +427 -0
  39. package/dist/plugins/agent/providers/codex/provider.d.ts +84 -0
  40. package/dist/plugins/agent/providers/codex/provider.js +828 -0
  41. package/dist/plugins/agent/providers/codex/rpc.d.ts +40 -0
  42. package/dist/plugins/agent/providers/codex/rpc.js +227 -0
  43. package/dist/plugins/agent/providers/common.d.ts +11 -0
  44. package/dist/plugins/agent/providers/common.js +75 -0
  45. package/dist/plugins/agent/providers/index.d.ts +3 -0
  46. package/dist/plugins/agent/providers/index.js +4 -0
  47. package/dist/plugins/agent/repository.d.ts +21 -0
  48. package/dist/plugins/agent/repository.js +226 -0
  49. package/dist/plugins/agent/runtime-config.d.ts +5 -0
  50. package/dist/plugins/agent/runtime-config.js +60 -0
  51. package/dist/plugins/agent/runtime-files.d.ts +8 -0
  52. package/dist/plugins/agent/runtime-files.js +170 -0
  53. package/dist/plugins/agent/runtime-state.d.ts +17 -0
  54. package/dist/plugins/agent/runtime-state.js +133 -0
  55. package/dist/plugins/agent/runtime.d.ts +60 -0
  56. package/dist/plugins/agent/runtime.js +928 -0
  57. package/dist/plugins/agent/store.d.ts +17 -0
  58. package/dist/plugins/agent/store.js +1 -0
  59. package/dist/plugins/agent/types.d.ts +320 -0
  60. package/dist/plugins/agent/types.js +13 -0
  61. package/dist/plugins/cloud/plugin.d.ts +1 -1
  62. package/dist/plugins/cloud/plugin.js +3 -3
  63. package/dist/plugins/cloud/tunnel-client.js +6 -7
  64. package/dist/plugins/computer/computer-native-backend.js +9 -1
  65. package/dist/plugins/computer/computer-native-host.js +21 -5
  66. package/dist/plugins/computer/computer-schemas.d.ts +7 -7
  67. package/dist/plugins/computer/computer-service.d.ts +3 -0
  68. package/dist/plugins/computer/computer-service.js +33 -1
  69. package/dist/plugins/computer/plugin.d.ts +1 -1
  70. package/dist/plugins/computer/plugin.js +1 -1
  71. package/dist/plugins/computer/types.d.ts +1 -0
  72. package/dist/plugins/git/git-service.d.ts +3 -2
  73. package/dist/plugins/git/git-service.js +37 -1
  74. package/dist/plugins/git/plugin.d.ts +0 -2
  75. package/dist/plugins/git/plugin.js +3 -3
  76. package/dist/plugins/process/plugin.d.ts +1 -1
  77. package/dist/plugins/process/plugin.js +3 -3
  78. package/dist/plugins/process/process-supervisor.d.ts +1 -0
  79. package/dist/plugins/process/process-supervisor.js +15 -4
  80. package/dist/plugins/types.d.ts +1 -11
  81. package/dist/plugins/types.js +1 -17
  82. package/dist/plugins/web/api-types.d.ts +20 -5
  83. package/dist/plugins/web/http/agent-api-router.d.ts +3 -0
  84. package/dist/plugins/web/http/agent-api-router.js +254 -0
  85. package/dist/plugins/web/http/api-router.js +9 -0
  86. package/dist/plugins/web/http/computer-api-router.js +21 -6
  87. package/dist/plugins/web/http/process-api-router.js +3 -0
  88. package/dist/plugins/web/http/workspace-api-router.js +198 -8
  89. package/dist/plugins/web/plugin.d.ts +1 -1
  90. package/dist/plugins/web/plugin.js +5 -4
  91. package/dist/plugins/web/runtime.d.ts +6 -3
  92. package/dist/plugins/web/runtime.js +6 -1
  93. package/dist/plugins/workspace/mcp.d.ts +1 -1
  94. package/dist/plugins/workspace/mcp.js +125 -1
  95. package/dist/plugins/workspace/metadata.d.ts +1 -1
  96. package/dist/plugins/workspace/plugin.d.ts +1 -1
  97. package/dist/plugins/workspace/plugin.js +9 -3
  98. package/dist/plugins/workspace/types.d.ts +1 -18
  99. package/dist/plugins/workspace/workspace-fs.d.ts +19 -3
  100. package/dist/plugins/workspace/workspace-fs.js +112 -120
  101. package/dist/plugins/workspace/workspace-root-policy.d.ts +9 -0
  102. package/dist/plugins/workspace/workspace-root-policy.js +56 -0
  103. package/dist/plugins/workspace/workspace-service.d.ts +13 -6
  104. package/dist/plugins/workspace/workspace-service.js +57 -49
  105. package/dist/web/assets/AgentView-BGLIBoxF.css +1 -0
  106. package/dist/web/assets/AgentView-BwL3S7sb.js +4 -0
  107. package/dist/web/assets/CloudView-DH4sOXJr.js +1 -0
  108. package/dist/web/assets/{CloudView-CHjVbp-5.css → CloudView-DVVvwuWW.css} +1 -1
  109. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-BaC1eGzG.js +3 -0
  110. package/dist/web/assets/ComputerView-BJSjCxFr.js +1 -0
  111. package/dist/web/assets/ComputerView-DWzfSYJ7.css +1 -0
  112. package/dist/web/assets/MarkdownContent-AdAmmDn5.js +16 -0
  113. package/dist/web/assets/MarkdownContent-B_5zTG8c.css +1 -0
  114. package/dist/web/assets/McpToolsView-CPaWUG9h.js +1 -0
  115. package/dist/web/assets/McpToolsView-DjxPbTHj.css +1 -0
  116. package/dist/web/assets/OperationsView-BjTY2lIh.css +1 -0
  117. package/dist/web/assets/OperationsView-D2FtLkHn.js +4 -0
  118. package/dist/web/assets/ProcessesView-BHOsaM2H.js +4 -0
  119. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +1 -0
  120. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-iW1SYlX_.js +1 -0
  121. package/dist/web/assets/SystemLogsView-By5ijObd.js +1 -0
  122. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +1 -0
  123. package/dist/web/assets/VSelect-CLnwhyWJ.js +1 -0
  124. package/dist/web/assets/VSpacer-dDRRKhUn.js +1 -0
  125. package/dist/web/assets/VSwitch-CGzxwZuj.js +1 -0
  126. package/dist/web/assets/VTabs-7aSwjrKs.js +1 -0
  127. package/dist/web/assets/VTextarea-D1_2FwMe.js +1 -0
  128. package/dist/web/assets/VTextarea-xiMOHEu-.css +1 -0
  129. package/dist/web/assets/WorkspaceDownloadPreview-BjuqUdB9.js +1 -0
  130. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +1 -0
  131. package/dist/web/assets/WorkspaceMediaPreview-D-igEnka.css +1 -0
  132. package/dist/web/assets/WorkspaceMediaPreview-LwXeVOY2.js +3 -0
  133. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +1 -0
  134. package/dist/web/assets/WorkspacesView-DHl28eWf.js +2 -0
  135. package/dist/web/assets/entity-refresh-queue-wQii8Dl5.js +1 -0
  136. package/dist/web/assets/index-BzRlodie.js +19 -0
  137. package/dist/web/assets/index-CZ-4tJlF.css +1 -0
  138. package/dist/web/assets/{syntax-highlight-B5ArnyrC.js → syntax-highlight-C_fuMcwC.js} +1 -1
  139. package/dist/web/assets/useMasterDetailFocus-BQAutDO9.js +1 -0
  140. package/dist/web/assets/useOperationLabels-C-zgM_ET.js +1 -0
  141. package/dist/web/index.html +2 -2
  142. package/package.json +4 -2
  143. package/dist/plugins/git/types.d.ts +0 -32
  144. package/dist/web/assets/CloudView-CvX0dkgu.js +0 -1
  145. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +0 -3
  146. package/dist/web/assets/ComputerView-91Z9Cefw.css +0 -1
  147. package/dist/web/assets/ComputerView-OOyEqWml.js +0 -1
  148. package/dist/web/assets/McpToolsView-BTnolyS6.css +0 -1
  149. package/dist/web/assets/McpToolsView-DjXWTKrZ.js +0 -1
  150. package/dist/web/assets/OperationsView-DqTY51Cr.js +0 -4
  151. package/dist/web/assets/ProcessesView-C13BykIa.js +0 -4
  152. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +0 -1
  153. package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +0 -1
  154. package/dist/web/assets/VSelect-DrRpKyYI.js +0 -1
  155. package/dist/web/assets/VSpacer-C3wAY3F0.js +0 -1
  156. package/dist/web/assets/VSwitch-CxusR4iO.js +0 -1
  157. package/dist/web/assets/VTabs-qBcuuXh3.js +0 -1
  158. package/dist/web/assets/WorkspacesView-BzQ0aH74.css +0 -1
  159. package/dist/web/assets/WorkspacesView-CijIvfsp.js +0 -2
  160. package/dist/web/assets/index-BegKQ3Yd.js +0 -19
  161. package/dist/web/assets/index-CkjqXRSj.css +0 -1
  162. package/dist/web/assets/useOperationLabels-BDjzGDkT.js +0 -1
  163. /package/dist/{plugins/process → core/runtime}/head-tail-buffer.d.ts +0 -0
  164. /package/dist/{plugins/process → core/runtime}/head-tail-buffer.js +0 -0
  165. /package/dist/plugins/{git/types.js → agent/provider.js} +0 -0
@@ -1,7 +1,10 @@
1
- import { Router } from "express";
1
+ import { randomUUID } from "node:crypto";
2
+ import express, { Router } from "express";
2
3
  import { ChatRoomError } from "#core/errors/chatroom-error";
3
- import { asyncRoute, bodyRecord, requireString, } from "#presentation/http/http-utils";
4
+ import { asyncRoute, bodyRecord, boundedIntegerQuery, requireString, } from "#presentation/http/http-utils";
4
5
  const MAX_WRITE_BYTES = 1024 * 1024;
6
+ const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
7
+ const MAX_IMAGE_PREVIEW_BYTES = 32 * 1024 * 1024;
5
8
  export function createWorkspaceApiRouter(application) {
6
9
  const router = Router();
7
10
  router.get("/workspaces", asyncRoute(async (_req, res) => {
@@ -27,10 +30,24 @@ export function createWorkspaceApiRouter(application) {
27
30
  router.get("/workspace/files", asyncRoute(async (req, res) => {
28
31
  const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
29
32
  const filePath = typeof req.query.path === "string" ? req.query.path : ".";
30
- res.json(await fs.list(filePath, { recursive: req.query.recursive === "1" }));
33
+ const limit = boundedIntegerQuery(req.query.limit, 300, 1, 500);
34
+ const offset = boundedIntegerQuery(req.query.offset, 0, 0, 100000);
35
+ const page = await fs.list(filePath, {
36
+ recursive: req.query.recursive === "1",
37
+ offset,
38
+ maxEntries: limit + 1,
39
+ });
40
+ const hasMore = page.length > limit;
41
+ res.json({
42
+ items: page.slice(0, limit),
43
+ nextOffset: hasMore ? offset + limit : null,
44
+ });
45
+ }));
46
+ router.get("/workspace/project-files", asyncRoute(async (req, res) => {
47
+ res.json(await application.workspaces.projectFilesResult(requireString(req.query.root, "root")));
31
48
  }));
32
49
  router.get("/workspace/file", asyncRoute(async (req, res) => {
33
- const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
50
+ const fs = await requestWorkspaceFs(req, application);
34
51
  res.json(await fs.read(requireString(req.query.path, "path"), {
35
52
  maxBytes: 2 * 1024 * 1024,
36
53
  }));
@@ -51,23 +68,94 @@ export function createWorkspaceApiRouter(application) {
51
68
  input: { root: fs.root, path: filePath, bytes },
52
69
  }, () => fs.write(filePath, body.content)));
53
70
  }));
71
+ router.post("/workspace/attachment", express.raw({
72
+ type: "application/octet-stream",
73
+ limit: MAX_ATTACHMENT_BYTES,
74
+ }), asyncRoute(async (req, res) => {
75
+ const root = requireString(req.query.root, "root");
76
+ const sessionId = requireString(req.query.sessionId, "sessionId");
77
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId))
78
+ throw new ChatRoomError("INVALID_INPUT", "Invalid attachment session id");
79
+ const originalName = requireString(req.query.name, "name");
80
+ if (!Buffer.isBuffer(req.body))
81
+ throw new ChatRoomError("INVALID_INPUT", "Attachment body must be binary");
82
+ const fs = await application.workspaces.fs(root);
83
+ const session = application.agents.getSession(sessionId);
84
+ if (session.workspaceRoot !== fs.root)
85
+ throw new ChatRoomError("FORBIDDEN", "Agent session belongs to another workspace");
86
+ const filePath = ".chatroom/agent-attachments/" +
87
+ sessionId +
88
+ "/" +
89
+ randomUUID() +
90
+ "-" +
91
+ attachmentFileName(originalName);
92
+ const file = await application.operations.run({
93
+ pluginId: "workspace",
94
+ source: "gui",
95
+ action: "attachment.upload",
96
+ input: { root: fs.root, path: filePath, bytes: req.body.byteLength },
97
+ }, () => fs.writeBytes(filePath, req.body));
98
+ res.json({
99
+ ...file,
100
+ name: originalName,
101
+ mimeType: typeof req.headers["x-chatroom-mime-type"] === "string"
102
+ ? req.headers["x-chatroom-mime-type"]
103
+ : null,
104
+ });
105
+ }));
106
+ router.get("/workspace/file/download", asyncRoute(async (req, res) => {
107
+ const filePath = requireString(req.query.path, "path");
108
+ const fs = await requestWorkspaceFs(req, application);
109
+ await streamWorkspaceFile(req, res, fs, filePath, "application/octet-stream", true);
110
+ }));
111
+ router.get("/workspace/file/preview", asyncRoute(async (req, res) => {
112
+ const filePath = requireString(req.query.path, "path");
113
+ const mime = previewMime(filePath);
114
+ if (!mime)
115
+ throw new ChatRoomError("INVALID_INPUT", "Unsupported file preview type");
116
+ const fs = await requestWorkspaceFs(req, application);
117
+ await streamWorkspaceFile(req, res, fs, filePath, mime, false);
118
+ }));
54
119
  router.get("/workspace/file/image", asyncRoute(async (req, res) => {
55
120
  const filePath = requireString(req.query.path, "path");
56
121
  const mime = imageMime(filePath);
57
122
  if (!mime)
58
123
  throw new ChatRoomError("INVALID_INPUT", "File type is not supported for image preview");
59
- const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
60
- const file = await fs.readBytes(filePath, { maxBytes: 10 * 1024 * 1024 });
124
+ const fs = await requestWorkspaceFs(req, application);
125
+ const file = await fs.readBytes(filePath, {
126
+ maxBytes: MAX_IMAGE_PREVIEW_BYTES,
127
+ });
61
128
  if (file.truncated)
62
129
  throw new ChatRoomError("INVALID_INPUT", "Image is too large to preview");
130
+ const etag = 'W/"' +
131
+ file.bytes.toString(16) +
132
+ "-" +
133
+ Date.parse(file.modifiedAt).toString(16) +
134
+ '"';
135
+ const requestedVersion = typeof req.query.v === "string" ? req.query.v : null;
63
136
  res.setHeader("Content-Type", mime);
64
- res.setHeader("Content-Length", String(file.data.byteLength));
65
- res.setHeader("Cache-Control", "no-store");
137
+ if (mime === "image/svg+xml")
138
+ res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'");
139
+ res.setHeader("ETag", etag);
140
+ res.setHeader("Last-Modified", file.modifiedAt);
141
+ res.setHeader("Cache-Control", requestedVersion === file.modifiedAt
142
+ ? "private, max-age=31536000, immutable"
143
+ : "private, no-cache");
66
144
  res.setHeader("X-Content-Type-Options", "nosniff");
145
+ if (req.headers["if-none-match"] === etag) {
146
+ res.status(304).end();
147
+ return;
148
+ }
149
+ res.setHeader("Content-Length", String(file.data.byteLength));
67
150
  res.send(file.data);
68
151
  }));
69
152
  return router;
70
153
  }
154
+ function requestWorkspaceFs(req, application) {
155
+ if (req.query.workspace !== undefined)
156
+ return application.workspaces.fsByAlias(requireString(req.query.workspace, "workspace"));
157
+ return application.workspaces.fs(requireString(req.query.root, "root"));
158
+ }
71
159
  function imageMime(filePath) {
72
160
  switch (filePath.split(".").pop()?.toLowerCase()) {
73
161
  case "png":
@@ -85,7 +173,109 @@ function imageMime(filePath) {
85
173
  return "image/bmp";
86
174
  case "ico":
87
175
  return "image/x-icon";
176
+ case "svg":
177
+ return "image/svg+xml";
88
178
  default:
89
179
  return null;
90
180
  }
91
181
  }
182
+ function attachmentFileName(value) {
183
+ const name = value.split(/[\\/]/).pop()?.trim() ?? "";
184
+ const safe = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
185
+ return (safe || "attachment").slice(0, 160);
186
+ }
187
+ function previewMime(filePath) {
188
+ switch (filePath.split(".").pop()?.toLowerCase()) {
189
+ case "pdf":
190
+ return "application/pdf";
191
+ case "mp4":
192
+ case "m4v":
193
+ return "video/mp4";
194
+ case "webm":
195
+ return "video/webm";
196
+ case "ogv":
197
+ return "video/ogg";
198
+ case "mov":
199
+ return "video/quicktime";
200
+ case "mp3":
201
+ return "audio/mpeg";
202
+ case "m4a":
203
+ return "audio/mp4";
204
+ case "aac":
205
+ return "audio/aac";
206
+ case "wav":
207
+ return "audio/wav";
208
+ case "flac":
209
+ return "audio/flac";
210
+ case "oga":
211
+ case "opus":
212
+ case "ogg":
213
+ return "audio/ogg";
214
+ default:
215
+ return null;
216
+ }
217
+ }
218
+ function parseByteRange(header, size) {
219
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
220
+ if (!match || size === 0 || (!match[1] && !match[2]))
221
+ return null;
222
+ let start;
223
+ let end;
224
+ if (!match[1]) {
225
+ const suffix = Number(match[2]);
226
+ if (!Number.isSafeInteger(suffix) || suffix <= 0)
227
+ return null;
228
+ start = Math.max(0, size - suffix);
229
+ end = size - 1;
230
+ }
231
+ else {
232
+ start = Number(match[1]);
233
+ end = match[2] ? Number(match[2]) : size - 1;
234
+ if (!Number.isSafeInteger(start) ||
235
+ !Number.isSafeInteger(end) ||
236
+ start >= size ||
237
+ end < start)
238
+ return null;
239
+ end = Math.min(end, size - 1);
240
+ }
241
+ return { start, end };
242
+ }
243
+ async function streamWorkspaceFile(req, res, fs, relativePath, mime, download) {
244
+ const { handle, bytes } = await fs.openReadable(relativePath);
245
+ const isRange = typeof req.headers.range === "string";
246
+ const range = isRange ? parseByteRange(req.headers.range, bytes) : null;
247
+ res.setHeader("Content-Type", mime);
248
+ res.setHeader("Cache-Control", "private, no-store, no-transform");
249
+ res.setHeader("X-Content-Type-Options", "nosniff");
250
+ res.setHeader("Accept-Ranges", "bytes");
251
+ if (download) {
252
+ const name = relativePath.replaceAll("\\", "/").split("/").pop() || "download";
253
+ const fallback = name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
254
+ res.setHeader("Content-Disposition", `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(name)}`);
255
+ }
256
+ else {
257
+ res.setHeader("Content-Disposition", "inline");
258
+ }
259
+ if (isRange && !range) {
260
+ res.setHeader("Content-Range", `bytes */${bytes}`);
261
+ await handle.close();
262
+ res.status(416).end();
263
+ return;
264
+ }
265
+ const start = range?.start ?? 0;
266
+ const end = range?.end ?? bytes - 1;
267
+ if (range) {
268
+ res.status(206);
269
+ res.setHeader("Content-Range", `bytes ${start}-${end}/${bytes}`);
270
+ }
271
+ res.setHeader("Content-Length", String(Math.max(0, end - start + 1)));
272
+ if (req.method === "HEAD" || bytes === 0) {
273
+ await handle.close();
274
+ res.end();
275
+ return;
276
+ }
277
+ const stream = handle.createReadStream({ start, end, autoClose: true });
278
+ res.on("close", () => stream.destroy());
279
+ stream.on("error", (error) => res.destroy(error));
280
+ stream.pipe(res);
281
+ }
@@ -3,6 +3,6 @@ import { WebRuntime } from "./runtime.js";
3
3
  interface WebPluginService {
4
4
  application: WebRuntime;
5
5
  }
6
- export declare const WebServiceToken: import("#plugins/types").ServiceToken<WebPluginService>;
6
+ export declare const WebServiceToken: import("#app/service-registry").ServiceToken<WebPluginService>;
7
7
  export declare function createWebPlugin(): InternalPlugin;
8
8
  export {};
@@ -1,8 +1,9 @@
1
- import { createServiceToken } from "#plugins/types";
1
+ import { createServiceToken } from "#app/service-registry";
2
2
  import { ComputerServiceToken } from "#plugins/computer/plugin";
3
- import { GitServiceToken } from "#plugins/git/plugin";
4
- import { ProcessService } from "#plugins/process/plugin";
3
+ import { GitAccessToken } from "#app/git-access";
4
+ import { ProcessServiceToken } from "#plugins/process/plugin";
5
5
  import { WorkspaceServiceToken } from "#plugins/workspace/plugin";
6
+ import { AgentServiceToken } from "#plugins/agent/plugin";
6
7
  import { WebRuntime } from "./runtime.js";
7
8
  export const WebServiceToken = createServiceToken("web");
8
9
  export function createWebPlugin() {
@@ -10,7 +11,7 @@ export function createWebPlugin() {
10
11
  id: "web",
11
12
  activate(context) {
12
13
  context.services.provide(WebServiceToken, {
13
- application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitServiceToken), context.operations, context.services.require(ProcessService), context.services.require(ComputerServiceToken), context.mcpTools),
14
+ application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitAccessToken), context.operations, context.services.require(ProcessServiceToken), context.services.require(ComputerServiceToken), context.services.require(AgentServiceToken), context.mcpTools),
14
15
  });
15
16
  },
16
17
  };
@@ -1,18 +1,21 @@
1
1
  import type { OperationLog } from "#operations/operation-log";
2
2
  import type { ComputerService } from "#plugins/computer/computer-service";
3
- import type { GitService } from "#plugins/git/git-service";
3
+ import type { GitAccess } from "#app/git-access";
4
4
  import type { ProcessSupervisor } from "#plugins/process/process-supervisor";
5
5
  import type { WorkspaceService } from "#plugins/workspace/workspace-service";
6
+ import type { AgentRuntime } from "#plugins/agent/runtime";
6
7
  import type { McpToolControl } from "#mcp/server/tool-control";
7
8
  export declare class WebRuntime {
8
9
  readonly workspaces: WorkspaceService;
9
- readonly git: GitService;
10
+ readonly git: GitAccess;
10
11
  readonly operations: OperationLog;
11
12
  readonly processes: ProcessSupervisor;
12
13
  readonly computer: ComputerService;
14
+ readonly agents: AgentRuntime;
13
15
  readonly mcpTools: McpToolControl;
14
- constructor(workspaces: WorkspaceService, git: GitService, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, mcpTools: McpToolControl);
16
+ constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl);
15
17
  processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
16
18
  listProcesses(): import("./api-types.js").ProcessSummary[];
17
19
  getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
20
+ getProcessSummary(processId: string): import("./api-types.js").ProcessSummary;
18
21
  }
@@ -4,13 +4,15 @@ export class WebRuntime {
4
4
  operations;
5
5
  processes;
6
6
  computer;
7
+ agents;
7
8
  mcpTools;
8
- constructor(workspaces, git, operations, processes, computer, mcpTools) {
9
+ constructor(workspaces, git, operations, processes, computer, agents, mcpTools) {
9
10
  this.workspaces = workspaces;
10
11
  this.git = git;
11
12
  this.operations = operations;
12
13
  this.processes = processes;
13
14
  this.computer = computer;
15
+ this.agents = agents;
14
16
  this.mcpTools = mcpTools;
15
17
  }
16
18
  processKill(processId, force = false) {
@@ -28,4 +30,7 @@ export class WebRuntime {
28
30
  getProcess(processId) {
29
31
  return this.processes.read(processId);
30
32
  }
33
+ getProcessSummary(processId) {
34
+ return this.processes.summary(processId);
35
+ }
31
36
  }
@@ -1,3 +1,3 @@
1
1
  import type { PluginMcpRegistrar } from "#mcp/server/plugin-mcp-registrar";
2
2
  import type { WorkspaceService } from "./workspace-service.js";
3
- export declare function registerWorkspaceTools(mcp: PluginMcpRegistrar, workspaces: WorkspaceService): void;
3
+ export declare function registerWorkspaceTools(mcp: PluginMcpRegistrar, workspaces: WorkspaceService, webBaseUrl: () => string | null): void;
@@ -1,5 +1,15 @@
1
+ import path from "node:path";
2
+ import { ChatRoomError } from "#core/errors/chatroom-error";
1
3
  import { z } from "zod";
2
4
  import { closedRead } from "#mcp/server/tool-support";
5
+ const workspaceEntrySchema = z.object({
6
+ root: z.string(),
7
+ name: z.string(),
8
+ summary: z.string().nullable(),
9
+ });
10
+ const workspaceListSchema = z.object({
11
+ workspaces: z.array(workspaceEntrySchema),
12
+ });
3
13
  const workspaceInfoSchema = z.object({
4
14
  root: z.string(),
5
15
  name: z.string(),
@@ -12,7 +22,15 @@ const workspaceInfoSchema = z.object({
12
22
  path: z.string(),
13
23
  })),
14
24
  });
15
- export function registerWorkspaceTools(mcp, workspaces) {
25
+ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
26
+ mcp.registerTool("workspace_list", {
27
+ title: "List workspaces",
28
+ description: "List available projects with their workspace roots, names, and project summaries from the workspace prompt metadata. Use this first to identify the relevant project before calling workspace_info for its full prompt, instructions, and skills.",
29
+ inputSchema: z.object({}),
30
+ outputSchema: workspaceListSchema,
31
+ annotations: closedRead,
32
+ action: "list",
33
+ }, async () => ({ workspaces: await workspaces.list() }));
16
34
  mcp.registerTool("workspace_info", {
17
35
  title: "Workspace info",
18
36
  description: "Read the complete workspace context for a project, including its summary, preset prompt, project instructions, and skill metadata.",
@@ -21,4 +39,110 @@ export function registerWorkspaceTools(mcp, workspaces) {
21
39
  annotations: closedRead,
22
40
  action: "info",
23
41
  }, (input) => workspaces.info(input.root));
42
+ mcp.registerTool("workspace_file_get", {
43
+ title: "Get workspace file",
44
+ description: "Read a file from a workspace in bounded chunks, or return a browser download link protected by the normal ChatRoom Web login. " +
45
+ "Use mode=content with encoding=utf8 for text and encoding=base64 for binary files; advance offset with nextOffset for large files. " +
46
+ "Use mode=link to give the user a download link when the Web service has a configured public address. " +
47
+ "Links do not contain credentials and the user must log in to ChatRoom Web to download.",
48
+ inputSchema: z.object({
49
+ root: z.string(),
50
+ path: z.string(),
51
+ mode: z.enum(["content", "link"]).default("content"),
52
+ encoding: z.enum(["utf8", "base64"]).default("utf8"),
53
+ offset: z.number().int().nonnegative().default(0),
54
+ maxBytes: z.number().int().min(1).max(262144).default(65536),
55
+ }),
56
+ outputSchema: z.object({
57
+ mode: z.enum(["content", "link"]),
58
+ path: z.string(),
59
+ name: z.string(),
60
+ bytes: z.number(),
61
+ modifiedAt: z.string(),
62
+ encoding: z.enum(["utf8", "base64"]).optional(),
63
+ content: z.string().optional(),
64
+ offset: z.number().optional(),
65
+ nextOffset: z.number().nullable().optional(),
66
+ url: z.string().optional(),
67
+ }),
68
+ annotations: closedRead,
69
+ action: "file.get",
70
+ audit: {
71
+ input: (input) => ({
72
+ root: input.root,
73
+ path: input.path,
74
+ mode: input.mode,
75
+ offset: input.mode === "content" ? input.offset : undefined,
76
+ }),
77
+ output: (output) => {
78
+ const result = output;
79
+ return {
80
+ mode: result.mode,
81
+ path: result.path,
82
+ bytes: result.bytes,
83
+ nextOffset: result.nextOffset,
84
+ };
85
+ },
86
+ },
87
+ }, async (input) => {
88
+ const fs = await workspaces.fs(input.root);
89
+ const name = path.posix.basename(input.path.replaceAll("\\", "/"));
90
+ if (input.mode === "link") {
91
+ const base = webBaseUrl();
92
+ if (!base)
93
+ throw new ChatRoomError("INVALID_INPUT", "No public Web URL is configured; download link is unavailable");
94
+ const info = await fs.fileInfo(input.path);
95
+ const url = new URL("/download", base);
96
+ url.searchParams.set("workspace", path.basename(fs.root));
97
+ url.searchParams.set("path", input.path);
98
+ return {
99
+ mode: "link",
100
+ path: input.path,
101
+ name,
102
+ ...info,
103
+ url: url.toString(),
104
+ };
105
+ }
106
+ const chunk = await fs.readChunk(input.path, input.offset, input.maxBytes);
107
+ let content;
108
+ let consumed = chunk.data.byteLength;
109
+ if (input.encoding === "utf8") {
110
+ const decoder = new TextDecoder("utf-8", { fatal: true });
111
+ let decoded = null;
112
+ for (let trim = 0; trim < 4 && trim < chunk.data.byteLength; trim += 1) {
113
+ if (trim > 0 && input.offset + chunk.data.byteLength >= chunk.bytes)
114
+ break;
115
+ try {
116
+ consumed = chunk.data.byteLength - trim;
117
+ decoded = decoder.decode(chunk.data.subarray(0, consumed));
118
+ break;
119
+ }
120
+ catch {
121
+ // Only incomplete trailing UTF-8 sequences can be repaired.
122
+ }
123
+ }
124
+ if (decoded === null) {
125
+ if (chunk.data.byteLength === 0)
126
+ decoded = "";
127
+ else
128
+ throw new ChatRoomError("INVALID_INPUT", "Invalid UTF-8 data; use base64");
129
+ }
130
+ content = decoded;
131
+ }
132
+ else {
133
+ content = chunk.data.toString("base64");
134
+ }
135
+ const next = input.offset + consumed;
136
+ return {
137
+ mode: "content",
138
+ path: input.path,
139
+ name,
140
+ bytes: chunk.bytes,
141
+ modifiedAt: chunk.modifiedAt,
142
+ encoding: input.encoding,
143
+ content,
144
+ offset: input.offset,
145
+ nextOffset: next < chunk.bytes ? next : null,
146
+ };
147
+ });
24
148
  }
@@ -1,5 +1,5 @@
1
1
  import type { WorkspaceFs } from "./workspace-fs.js";
2
- import type { WorkspaceSkill } from "./types.js";
2
+ import type { WorkspaceSkill } from "#app/workspace-access";
3
3
  export declare function readSummary(fs: WorkspaceFs): Promise<string | null>;
4
4
  export declare function readPresetPrompt(fs: WorkspaceFs): Promise<string | null>;
5
5
  export declare function readInstructions(fs: WorkspaceFs): Promise<string | null>;
@@ -1,4 +1,4 @@
1
1
  import type { InternalPlugin } from "#plugins/types";
2
2
  import { WorkspaceService } from "./workspace-service.js";
3
- export declare const WorkspaceServiceToken: import("#plugins/types").ServiceToken<WorkspaceService>;
3
+ export declare const WorkspaceServiceToken: import("#app/service-registry").ServiceToken<WorkspaceService>;
4
4
  export declare function createWorkspacePlugin(): InternalPlugin;
@@ -1,22 +1,28 @@
1
- import { createServiceToken } from "#plugins/types";
1
+ import { GitAccessToken } from "#app/git-access";
2
+ import { WorkspaceAccessToken } from "#app/workspace-access";
3
+ import { createServiceToken } from "#app/service-registry";
2
4
  import { registerWorkspaceTools } from "./mcp.js";
3
5
  import { WorkspaceService } from "./workspace-service.js";
4
6
  export const WorkspaceServiceToken = createServiceToken("workspace");
5
7
  export function createWorkspacePlugin() {
6
8
  let service = null;
9
+ let webBaseUrl = () => null;
7
10
  return {
8
11
  id: "workspace",
9
12
  async activate(context) {
10
- service = await WorkspaceService.create(context.config.allowedRoots);
13
+ webBaseUrl = () => context.externalAccess.publicWebBaseUrl();
14
+ service = await WorkspaceService.create(context.config.allowedRoots, context.services.require(GitAccessToken));
11
15
  context.services.provide(WorkspaceServiceToken, service);
16
+ context.services.provide(WorkspaceAccessToken, service);
12
17
  },
13
18
  registerMcp(mcp) {
14
19
  if (!service)
15
20
  throw new Error("Workspace plugin is not active");
16
- registerWorkspaceTools(mcp, service);
21
+ registerWorkspaceTools(mcp, service, webBaseUrl);
17
22
  },
18
23
  deactivate() {
19
24
  service = null;
25
+ webBaseUrl = () => null;
20
26
  },
21
27
  };
22
28
  }
@@ -1,23 +1,6 @@
1
- export interface WorkspaceEntry {
2
- root: string;
3
- name: string;
4
- summary: string | null;
5
- }
6
- export interface WorkspaceSkill {
7
- name: string;
8
- description: string;
9
- path: string;
10
- }
11
- export interface WorkspaceInfo {
12
- root: string;
13
- name: string;
14
- summary: string | null;
15
- presetPrompt: string | null;
16
- instructions: string | null;
17
- skills: WorkspaceSkill[];
18
- }
19
1
  export interface WorkspaceFile {
20
2
  path: string;
21
3
  type: "file" | "directory" | "symlink";
22
4
  size: number;
5
+ modifiedAt?: string;
23
6
  }
@@ -3,12 +3,17 @@ export declare class WorkspaceFs {
3
3
  readonly root: string;
4
4
  private constructor();
5
5
  static create(root: string): Promise<WorkspaceFs>;
6
+ fileInfo(relativePath: string): Promise<{
7
+ bytes: number;
8
+ modifiedAt: string;
9
+ }>;
6
10
  read(relativePath: string, options?: {
7
11
  maxBytes?: number;
8
12
  }): Promise<{
9
13
  content: string;
10
14
  bytes: number;
11
15
  truncated: boolean;
16
+ modifiedAt: string;
12
17
  }>;
13
18
  readBytes(relativePath: string, options?: {
14
19
  maxBytes?: number;
@@ -16,13 +21,24 @@ export declare class WorkspaceFs {
16
21
  data: Buffer;
17
22
  bytes: number;
18
23
  truncated: boolean;
24
+ modifiedAt: string;
25
+ }>;
26
+ openReadable(relativePath: string): Promise<{
27
+ handle: import("fs/promises").FileHandle;
28
+ bytes: number;
29
+ modifiedAt: string;
30
+ }>;
31
+ readChunk(relativePath: string, offset: number, maxBytes: number): Promise<{
32
+ data: Buffer;
33
+ bytes: number;
34
+ modifiedAt: string;
19
35
  }>;
20
36
  write(relativePath: string, content: string): Promise<WorkspaceFile>;
37
+ writeBytes(relativePath: string, content: Uint8Array): Promise<WorkspaceFile>;
21
38
  list(relativePath?: string, options?: {
22
39
  recursive?: boolean;
23
40
  maxEntries?: number;
41
+ excludeDirectoryNames?: readonly string[];
42
+ offset?: number;
24
43
  }): Promise<WorkspaceFile[]>;
25
- private lexical;
26
- private resolveWritable;
27
- private resolveReadable;
28
44
  }