@zhanguym/chatroom 1.2.0 → 1.3.2

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 (110) hide show
  1. package/dist/app/application.d.ts +5 -0
  2. package/dist/app/application.js +55 -4
  3. package/dist/app/lifecycle.js +12 -0
  4. package/dist/app/workspace-access.d.ts +17 -4
  5. package/dist/core/operations/types.d.ts +1 -1
  6. package/dist/core/operations/types.js +1 -1
  7. package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
  8. package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
  9. package/dist/infrastructure/database/app-database.js +3 -3
  10. package/dist/infrastructure/database/migrate.d.ts +2 -0
  11. package/dist/infrastructure/database/migrate.js +68 -0
  12. package/dist/infrastructure/database/schema.d.ts +3 -1
  13. package/dist/infrastructure/database/schema.js +35 -6
  14. package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
  15. package/dist/infrastructure/database/workspace-repository.js +126 -0
  16. package/dist/mcp/custom/manager.d.ts +3 -1
  17. package/dist/mcp/custom/manager.js +37 -2
  18. package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
  19. package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
  20. package/dist/mcp/server/tool-control.d.ts +6 -1
  21. package/dist/mcp/server/tool-control.js +11 -2
  22. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  23. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  24. package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
  25. package/dist/plugins/agent/custom-mcp-store.js +38 -0
  26. package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
  27. package/dist/plugins/agent/mcp-gateway.js +110 -0
  28. package/dist/plugins/agent/mcp.js +3 -2
  29. package/dist/plugins/agent/plugin.js +1 -1
  30. package/dist/plugins/agent/provider.d.ts +1 -0
  31. package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
  32. package/dist/plugins/agent/providers/codex/launch.js +24 -0
  33. package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
  34. package/dist/plugins/agent/providers/codex/provider.js +8 -2
  35. package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
  36. package/dist/plugins/agent/providers/codex/rpc.js +28 -3
  37. package/dist/plugins/agent/providers/index.d.ts +2 -1
  38. package/dist/plugins/agent/providers/index.js +5 -2
  39. package/dist/plugins/agent/repository.js +10 -7
  40. package/dist/plugins/agent/runtime-files.d.ts +4 -4
  41. package/dist/plugins/agent/runtime-files.js +18 -14
  42. package/dist/plugins/agent/runtime.d.ts +2 -0
  43. package/dist/plugins/agent/runtime.js +52 -13
  44. package/dist/plugins/agent/types.d.ts +3 -2
  45. package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
  46. package/dist/plugins/plugin-manager.d.ts +6 -1
  47. package/dist/plugins/plugin-manager.js +9 -2
  48. package/dist/plugins/types.d.ts +4 -0
  49. package/dist/plugins/web/http/agent-api-router.js +15 -1
  50. package/dist/plugins/web/http/git-api-router.js +13 -13
  51. package/dist/plugins/web/http/rtc-api-router.js +3 -0
  52. package/dist/plugins/web/http/runtime-api-router.js +22 -0
  53. package/dist/plugins/web/http/workspace-api-router.js +63 -12
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +5 -1
  57. package/dist/plugins/workspace/mcp.js +20 -15
  58. package/dist/plugins/workspace/plugin.js +1 -1
  59. package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
  60. package/dist/plugins/workspace/workspace-root-policy.js +25 -19
  61. package/dist/plugins/workspace/workspace-service.d.ts +20 -8
  62. package/dist/plugins/workspace/workspace-service.js +270 -64
  63. package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
  64. package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
  65. package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
  66. package/dist/web/assets/{CodeViewer.vue_vue_type_script_setup_true_lang-DK6xGH2G.js → CodeViewer.vue_vue_type_script_setup_true_lang-BtA5aWrS.js} +1 -1
  67. package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
  68. package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
  69. package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
  70. package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
  71. package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
  72. package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
  73. package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
  74. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
  75. package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
  76. package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
  77. package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
  78. package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
  79. package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
  80. package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
  81. package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
  82. package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
  83. package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
  84. package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
  85. package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
  86. package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
  87. package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
  88. package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
  89. package/dist/web/assets/index-9DLDvF8M.js +19 -0
  90. package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
  91. package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
  92. package/dist/web/index.html +1 -1
  93. package/dist/web/transport-sw.js +43 -15
  94. package/package.json +1 -1
  95. package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
  96. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
  97. package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
  98. package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
  99. package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
  100. package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
  101. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
  102. package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
  103. package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
  104. package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
  105. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
  106. package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
  107. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
  108. package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
  109. package/dist/web/assets/index-CacDJSDn.js +0 -19
  110. package/dist/web/assets/useOperationLabels-BNiE-bRq.js +0 -1
@@ -10,6 +10,50 @@ export function createWorkspaceApiRouter(application) {
10
10
  router.get("/workspaces", asyncRoute(async (_req, res) => {
11
11
  res.json(await application.workspaces.list());
12
12
  }));
13
+ router.get("/workspaces/manage", asyncRoute(async (_req, res) => {
14
+ res.json(await application.workspaces.listAll());
15
+ }));
16
+ router.post("/workspaces/register", asyncRoute(async (req, res) => {
17
+ const body = bodyRecord(req.body);
18
+ const projectPath = requireString(body.path, "path");
19
+ const name = body.name === undefined || body.name === null
20
+ ? null
21
+ : requireString(body.name, "name");
22
+ res.json(await application.operations.run({
23
+ pluginId: "workspace",
24
+ source: "gui",
25
+ action: "register",
26
+ input: { path: projectPath, name },
27
+ }, () => application.workspaces.registerProject(projectPath, name)));
28
+ }));
29
+ router.patch("/workspaces/:workspaceId", asyncRoute(async (req, res) => {
30
+ const body = bodyRecord(req.body);
31
+ res.json(await application.workspaces.renameProject(requireString(req.params.workspaceId, "workspaceId"), requireString(body.name, "name")));
32
+ }));
33
+ router.delete("/workspaces/:workspaceId/cleanup", asyncRoute(async (req, res) => {
34
+ const workspaceId = requireString(req.params.workspaceId, "workspaceId");
35
+ const project = (await application.workspaces.listAll()).find((item) => item.id === workspaceId);
36
+ if (!project)
37
+ throw new ChatRoomError("NOT_FOUND", "Workspace not found");
38
+ if (project.status === "available")
39
+ throw new ChatRoomError("CONFLICT", "Available workspaces cannot be cleaned up");
40
+ const sessions = application.agents
41
+ .listSessions()
42
+ .filter((session) => session.workspaceId === workspaceId);
43
+ if (sessions.some((session) => session.status === "running" || session.status === "waiting_input"))
44
+ throw new ChatRoomError("CONFLICT", "Stop active Agent sessions before cleaning up this workspace");
45
+ for (const session of sessions)
46
+ await application.agents.deleteSession(session.id);
47
+ await application.workspaces.cleanupProject(workspaceId);
48
+ res.status(204).end();
49
+ }));
50
+ router.delete("/workspaces/:workspaceId", asyncRoute(async (req, res) => {
51
+ await application.workspaces.removeProject(requireString(req.params.workspaceId, "workspaceId"));
52
+ res.status(204).end();
53
+ }));
54
+ router.post("/workspaces/:workspaceId/restore", asyncRoute(async (req, res) => {
55
+ res.json(await application.workspaces.restoreProject(requireString(req.params.workspaceId, "workspaceId")));
56
+ }));
13
57
  router.post("/workspaces", asyncRoute(async (req, res) => {
14
58
  const body = bodyRecord(req.body);
15
59
  const parent = requireString(body.parent, "parent");
@@ -25,10 +69,10 @@ export function createWorkspaceApiRouter(application) {
25
69
  res.json(application.workspaces.roots());
26
70
  });
27
71
  router.get("/workspace", asyncRoute(async (req, res) => {
28
- res.json(await application.workspaces.info(requireString(req.query.root, "root")));
72
+ res.json(await application.workspaces.info(requireString(req.query.workspaceId, "workspaceId")));
29
73
  }));
30
74
  router.get("/workspace/files", asyncRoute(async (req, res) => {
31
- const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
75
+ const fs = await application.workspaces.fs(requireString(req.query.workspaceId, "workspaceId"));
32
76
  const filePath = typeof req.query.path === "string" ? req.query.path : ".";
33
77
  const limit = boundedIntegerQuery(req.query.limit, 300, 1, 500);
34
78
  const offset = boundedIntegerQuery(req.query.offset, 0, 0, 100000);
@@ -44,7 +88,7 @@ export function createWorkspaceApiRouter(application) {
44
88
  });
45
89
  }));
46
90
  router.get("/workspace/project-files", asyncRoute(async (req, res) => {
47
- res.json(await application.workspaces.projectFilesResult(requireString(req.query.root, "root")));
91
+ res.json(await application.workspaces.projectFilesResult(requireString(req.query.workspaceId, "workspaceId")));
48
92
  }));
49
93
  router.get("/workspace/file", asyncRoute(async (req, res) => {
50
94
  const fs = await requestWorkspaceFs(req, application);
@@ -54,7 +98,8 @@ export function createWorkspaceApiRouter(application) {
54
98
  }));
55
99
  router.put("/workspace/file", asyncRoute(async (req, res) => {
56
100
  const body = bodyRecord(req.body);
57
- const fs = await application.workspaces.fs(requireString(body.root, "root"));
101
+ const workspaceId = requireString(body.workspaceId, "workspaceId");
102
+ const fs = await application.workspaces.fs(workspaceId);
58
103
  const filePath = requireString(body.path, "path");
59
104
  if (typeof body.content !== "string")
60
105
  throw new ChatRoomError("INVALID_INPUT", "content must be a string");
@@ -65,23 +110,31 @@ export function createWorkspaceApiRouter(application) {
65
110
  pluginId: "workspace",
66
111
  source: "gui",
67
112
  action: "file.write",
68
- input: { root: fs.root, path: filePath, bytes },
113
+ input: { workspaceId, path: filePath, bytes },
69
114
  }, () => fs.write(filePath, body.content)));
70
115
  }));
71
116
  router.post("/workspace/attachment", express.raw({
72
117
  type: "application/octet-stream",
73
118
  limit: MAX_ATTACHMENT_BYTES,
74
119
  }), asyncRoute(async (req, res) => {
75
- const root = requireString(req.query.root, "root");
120
+ const workspaceId = requireString(req.query.workspaceId, "workspaceId");
76
121
  const sessionId = requireString(req.query.sessionId, "sessionId");
77
122
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId))
78
123
  throw new ChatRoomError("INVALID_INPUT", "Invalid attachment session id");
79
124
  const originalName = requireString(req.query.name, "name");
80
125
  if (!Buffer.isBuffer(req.body))
81
126
  throw new ChatRoomError("INVALID_INPUT", "Attachment body must be binary");
82
- const fs = await application.workspaces.fs(root);
127
+ if (req.body.byteLength === 0)
128
+ throw new ChatRoomError("INVALID_INPUT", "Attachment is empty");
129
+ const expectedSize = req.headers["x-chatroom-upload-size"];
130
+ if (expectedSize !== undefined &&
131
+ (typeof expectedSize !== "string" ||
132
+ !/^\d+$/.test(expectedSize) ||
133
+ Number(expectedSize) !== req.body.byteLength))
134
+ throw new ChatRoomError("INVALID_INPUT", "Attachment size mismatch");
135
+ const fs = await application.workspaces.fs(workspaceId);
83
136
  const session = application.agents.getSession(sessionId);
84
- if (session.workspaceRoot !== fs.root)
137
+ if (session.workspaceId !== workspaceId)
85
138
  throw new ChatRoomError("FORBIDDEN", "Agent session belongs to another workspace");
86
139
  const filePath = ".chatroom/agent-attachments/" +
87
140
  sessionId +
@@ -93,7 +146,7 @@ export function createWorkspaceApiRouter(application) {
93
146
  pluginId: "workspace",
94
147
  source: "gui",
95
148
  action: "attachment.upload",
96
- input: { root: fs.root, path: filePath, bytes: req.body.byteLength },
149
+ input: { workspaceId, path: filePath, bytes: req.body.byteLength },
97
150
  }, () => fs.writeBytes(filePath, req.body));
98
151
  res.json({
99
152
  ...file,
@@ -152,9 +205,7 @@ export function createWorkspaceApiRouter(application) {
152
205
  return router;
153
206
  }
154
207
  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"));
208
+ return application.workspaces.fs(requireString(req.query.workspaceId, "workspaceId"));
158
209
  }
159
210
  function imageMime(filePath) {
160
211
  switch (filePath.split(".").pop()?.toLowerCase()) {
@@ -11,7 +11,7 @@ export function createWebPlugin() {
11
11
  id: "web",
12
12
  activate(context) {
13
13
  context.services.provide(WebServiceToken, {
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, context.customMcp),
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, context.customMcp, context.agentMcpTools, context.agentCustomMcp),
15
15
  });
16
16
  },
17
17
  };
@@ -15,7 +15,9 @@ export declare class WebRuntime {
15
15
  readonly agents: AgentRuntime;
16
16
  readonly mcpTools: McpToolControl;
17
17
  readonly customMcp: CustomMcpManager;
18
- constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl, customMcp: CustomMcpManager);
18
+ readonly agentMcpTools: McpToolControl;
19
+ readonly agentCustomMcp: CustomMcpManager;
20
+ constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl, customMcp: CustomMcpManager, agentMcpTools: McpToolControl, agentCustomMcp: CustomMcpManager);
19
21
  processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
20
22
  listProcesses(): import("./api-types.js").ProcessSummary[];
21
23
  getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
@@ -7,7 +7,9 @@ export class WebRuntime {
7
7
  agents;
8
8
  mcpTools;
9
9
  customMcp;
10
- constructor(workspaces, git, operations, processes, computer, agents, mcpTools, customMcp) {
10
+ agentMcpTools;
11
+ agentCustomMcp;
12
+ constructor(workspaces, git, operations, processes, computer, agents, mcpTools, customMcp, agentMcpTools, agentCustomMcp) {
11
13
  this.workspaces = workspaces;
12
14
  this.git = git;
13
15
  this.operations = operations;
@@ -16,6 +18,8 @@ export class WebRuntime {
16
18
  this.agents = agents;
17
19
  this.mcpTools = mcpTools;
18
20
  this.customMcp = customMcp;
21
+ this.agentMcpTools = agentMcpTools;
22
+ this.agentCustomMcp = agentCustomMcp;
19
23
  }
20
24
  processKill(processId, force = false) {
21
25
  return this.operations.run({
@@ -3,17 +3,19 @@ import { ChatRoomError } from "#core/errors/chatroom-error";
3
3
  import { z } from "zod";
4
4
  import { closedRead } from "#mcp/server/tool-support";
5
5
  const workspaceEntrySchema = z.object({
6
- root: z.string(),
6
+ id: z.string(),
7
+ path: z.string(),
7
8
  name: z.string(),
8
9
  summary: z.string().nullable(),
10
+ status: z.enum(["available", "missing", "restore_pending", "unregistered"]),
11
+ source: z.enum(["configured", "manual", "both", "history"]),
12
+ manuallyRegistered: z.boolean(),
13
+ discovered: z.boolean(),
9
14
  });
10
15
  const workspaceListSchema = z.object({
11
16
  workspaces: z.array(workspaceEntrySchema),
12
17
  });
13
- const workspaceInfoSchema = z.object({
14
- root: z.string(),
15
- name: z.string(),
16
- summary: z.string().nullable(),
18
+ const workspaceInfoSchema = workspaceEntrySchema.extend({
17
19
  presetPrompt: z.string().nullable(),
18
20
  instructions: z.string().nullable(),
19
21
  skills: z.array(z.object({
@@ -25,7 +27,7 @@ const workspaceInfoSchema = z.object({
25
27
  export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
26
28
  mcp.registerTool("workspace_list", {
27
29
  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.",
30
+ description: "List available projects with stable workspace IDs, display names, paths, and project summaries. Use the ID for all subsequent workspace operations; the path is informational and is not an identifier.",
29
31
  inputSchema: z.object({}),
30
32
  outputSchema: workspaceListSchema,
31
33
  annotations: closedRead,
@@ -33,27 +35,28 @@ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
33
35
  }, async () => ({ workspaces: await workspaces.list() }));
34
36
  mcp.registerTool("workspace_info", {
35
37
  title: "Workspace info",
36
- description: "Read the complete workspace context for a project, including its summary, preset prompt, project instructions, and skill metadata.",
37
- inputSchema: z.object({ root: z.string() }),
38
+ description: "Read the complete context for a project by workspace ID, including summary, preset prompt, project instructions, and skill metadata.",
39
+ inputSchema: z.object({ workspaceId: z.string().min(1) }),
38
40
  outputSchema: workspaceInfoSchema,
39
41
  annotations: closedRead,
40
42
  action: "info",
41
- }, (input) => workspaces.info(input.root));
43
+ }, (input) => workspaces.info(input.workspaceId));
42
44
  mcp.registerTool("workspace_file_get", {
43
45
  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. " +
46
+ description: "Read a file from a project identified by workspaceId in bounded chunks, or return a browser download link protected by the normal ChatRoom Web login. " +
45
47
  "Use mode=content with encoding=utf8 for text and encoding=base64 for binary files; advance offset with nextOffset for large files. " +
46
48
  "Use mode=link to give the user a download link when the Web service has a configured public address. " +
47
49
  "Links do not contain credentials and the user must log in to ChatRoom Web to download.",
48
50
  inputSchema: z.object({
49
- root: z.string(),
50
- path: z.string(),
51
+ workspaceId: z.string().min(1),
52
+ path: z.string().min(1),
51
53
  mode: z.enum(["content", "link"]).default("content"),
52
54
  encoding: z.enum(["utf8", "base64"]).default("utf8"),
53
55
  offset: z.number().int().nonnegative().default(0),
54
56
  maxBytes: z.number().int().min(1).max(262144).default(65536),
55
57
  }),
56
58
  outputSchema: z.object({
59
+ workspaceId: z.string(),
57
60
  mode: z.enum(["content", "link"]),
58
61
  path: z.string(),
59
62
  name: z.string(),
@@ -69,7 +72,7 @@ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
69
72
  action: "file.get",
70
73
  audit: {
71
74
  input: (input) => ({
72
- root: input.root,
75
+ workspaceId: input.workspaceId,
73
76
  path: input.path,
74
77
  mode: input.mode,
75
78
  offset: input.mode === "content" ? input.offset : undefined,
@@ -85,7 +88,7 @@ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
85
88
  },
86
89
  },
87
90
  }, async (input) => {
88
- const fs = await workspaces.fs(input.root);
91
+ const fs = await workspaces.fs(input.workspaceId);
89
92
  const name = path.posix.basename(input.path.replaceAll("\\", "/"));
90
93
  if (input.mode === "link") {
91
94
  const base = webBaseUrl();
@@ -93,9 +96,10 @@ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
93
96
  throw new ChatRoomError("INVALID_INPUT", "No public Web URL is configured; download link is unavailable");
94
97
  const info = await fs.fileInfo(input.path);
95
98
  const url = new URL("/download", base);
96
- url.searchParams.set("workspace", path.basename(fs.root));
99
+ url.searchParams.set("workspaceId", input.workspaceId);
97
100
  url.searchParams.set("path", input.path);
98
101
  return {
102
+ workspaceId: input.workspaceId,
99
103
  mode: "link",
100
104
  path: input.path,
101
105
  name,
@@ -134,6 +138,7 @@ export function registerWorkspaceTools(mcp, workspaces, webBaseUrl) {
134
138
  }
135
139
  const next = input.offset + consumed;
136
140
  return {
141
+ workspaceId: input.workspaceId,
137
142
  mode: "content",
138
143
  path: input.path,
139
144
  name,
@@ -11,7 +11,7 @@ export function createWorkspacePlugin() {
11
11
  id: "workspace",
12
12
  async activate(context) {
13
13
  webBaseUrl = () => context.externalAccess.publicWebBaseUrl();
14
- service = await WorkspaceService.create(context.config.allowedRoots, context.services.require(GitAccessToken));
14
+ service = await WorkspaceService.create(context.config.allowedRoots, context.services.require(GitAccessToken), context.database);
15
15
  context.services.provide(WorkspaceServiceToken, service);
16
16
  context.services.provide(WorkspaceAccessToken, service);
17
17
  },
@@ -4,6 +4,5 @@ export declare class WorkspaceRootPolicy {
4
4
  static create(allowedRoots: string[]): Promise<WorkspaceRootPolicy>;
5
5
  roots(): string[];
6
6
  isWorkspaceRoot(candidate: string): boolean;
7
- resolveWorkspace(input: string): Promise<string>;
8
7
  resolveAllowedRoot(input: string): Promise<string>;
9
8
  }
@@ -9,12 +9,28 @@ export class WorkspaceRootPolicy {
9
9
  }
10
10
  static async create(allowedRoots) {
11
11
  const canonicalRoots = await Promise.all(allowedRoots.map(async (root) => {
12
- const canonical = await realpath(expandHome(root)).catch(() => {
13
- throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${root}`);
14
- });
15
- if (!(await stat(canonical)).isDirectory())
16
- throw new ChatRoomError("INVALID_INPUT", `Allowed root is not a directory: ${root}`);
17
- return canonical;
12
+ let candidate = expandHome(root);
13
+ const missing = [];
14
+ while (true) {
15
+ let canonical;
16
+ try {
17
+ canonical = await realpath(candidate);
18
+ }
19
+ catch (error) {
20
+ const code = error.code;
21
+ if (code !== "ENOENT" && code !== "ENOTDIR")
22
+ throw new ChatRoomError("NOT_FOUND", "Allowed root cannot be resolved: " + root);
23
+ const parent = path.dirname(candidate);
24
+ if (parent === candidate)
25
+ throw new ChatRoomError("NOT_FOUND", "Allowed root cannot be resolved: " + root);
26
+ missing.push(path.basename(candidate));
27
+ candidate = parent;
28
+ continue;
29
+ }
30
+ if (!(await stat(canonical)).isDirectory())
31
+ throw new ChatRoomError("INVALID_INPUT", "Allowed root is not a directory: " + root);
32
+ return path.join(canonical, ...missing.reverse());
33
+ }
18
34
  }));
19
35
  return new WorkspaceRootPolicy([...new Set(canonicalRoots)]);
20
36
  }
@@ -24,26 +40,16 @@ export class WorkspaceRootPolicy {
24
40
  isWorkspaceRoot(candidate) {
25
41
  return this.allowedRoots.some((root) => path.dirname(candidate) === root);
26
42
  }
27
- async resolveWorkspace(input) {
28
- if (typeof input !== "string" || !input.trim())
29
- throw new ChatRoomError("INVALID_INPUT", "Workspace root is required");
30
- const canonical = await realpath(expandHome(input)).catch(() => {
31
- throw new ChatRoomError("NOT_FOUND", `Workspace root does not exist: ${input}`);
32
- });
33
- if (!(await stat(canonical)).isDirectory())
34
- throw new ChatRoomError("INVALID_INPUT", `Workspace root is not a directory: ${input}`);
35
- if (!this.isWorkspaceRoot(canonical))
36
- throw new ChatRoomError("FORBIDDEN", "Workspace must be a direct child of a configured allowed root", { root: canonical });
37
- return canonical;
38
- }
39
43
  async resolveAllowedRoot(input) {
40
44
  if (typeof input !== "string" || !input.trim())
41
45
  throw new ChatRoomError("INVALID_INPUT", "Allowed root is required");
42
46
  const canonical = await realpath(expandHome(input)).catch(() => {
43
- throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${input}`);
47
+ throw new ChatRoomError("NOT_FOUND", "Allowed root does not exist: " + input);
44
48
  });
45
49
  if (!this.allowedRoots.includes(canonical))
46
50
  throw new ChatRoomError("FORBIDDEN", "Project can only be created directly under a configured allowed root");
51
+ if (!(await stat(canonical)).isDirectory())
52
+ throw new ChatRoomError("INVALID_INPUT", "Allowed root is not a directory: " + input);
47
53
  return canonical;
48
54
  }
49
55
  }
@@ -1,22 +1,34 @@
1
1
  import type { GitAccess } from "#app/git-access";
2
2
  import type { WorkspaceAccess, WorkspaceEntry, WorkspaceInfo } from "#app/workspace-access";
3
+ import type { AppDatabase } from "#infrastructure/database/app-database";
3
4
  import { WorkspaceFs } from "./workspace-fs.js";
4
5
  export declare class WorkspaceService implements WorkspaceAccess {
5
6
  private readonly rootsPolicy;
6
7
  private readonly git;
8
+ private readonly repository;
7
9
  private constructor();
8
- static create(allowedRoots: string[], git: Pick<GitAccess, "projectFiles">): Promise<WorkspaceService>;
10
+ static create(allowedRoots: string[], git: Pick<GitAccess, "projectFiles">, database: AppDatabase): Promise<WorkspaceService>;
9
11
  roots(): string[];
10
12
  list(): Promise<WorkspaceEntry[]>;
11
- resolve(input: string): Promise<string>;
12
- resolveAlias(alias: string): Promise<string>;
13
- fsByAlias(alias: string): Promise<WorkspaceFs>;
13
+ listAll(): Promise<WorkspaceEntry[]>;
14
+ resolve(workspaceId: string): Promise<string>;
15
+ registerProject(pathInput: string, nameInput?: string | null): Promise<WorkspaceEntry>;
16
+ renameProject(workspaceId: string, nameInput: string): Promise<WorkspaceEntry>;
17
+ removeProject(workspaceId: string): Promise<void>;
18
+ cleanupProject(workspaceId: string): Promise<void>;
19
+ restoreProject(workspaceId: string): Promise<WorkspaceEntry>;
14
20
  createProject(parentInput: string, nameInput: string): Promise<WorkspaceEntry>;
15
- projectFiles(input: string, maxFiles?: number): Promise<string[]>;
16
- projectFilesResult(input: string, maxFiles?: number): Promise<{
21
+ projectFiles(workspaceId: string, maxFiles?: number): Promise<string[]>;
22
+ projectFilesResult(workspaceId: string, maxFiles?: number): Promise<{
17
23
  paths: string[];
18
24
  truncated: boolean;
19
25
  }>;
20
- info(input: string): Promise<WorkspaceInfo>;
21
- fs(input: string): Promise<WorkspaceFs>;
26
+ info(workspaceId: string): Promise<WorkspaceInfo>;
27
+ fs(workspaceId: string): Promise<WorkspaceFs>;
28
+ private refreshDiscovery;
29
+ private refreshWorkspaceStatuses;
30
+ private entries;
31
+ private toEntry;
32
+ private entry;
33
+ private requireRecord;
22
34
  }