@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.
- package/dist/app/application.d.ts +5 -0
- package/dist/app/application.js +55 -4
- package/dist/app/lifecycle.js +12 -0
- package/dist/app/workspace-access.d.ts +17 -4
- package/dist/core/operations/types.d.ts +1 -1
- package/dist/core/operations/types.js +1 -1
- package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
- package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
- package/dist/infrastructure/database/app-database.js +3 -3
- package/dist/infrastructure/database/migrate.d.ts +2 -0
- package/dist/infrastructure/database/migrate.js +68 -0
- package/dist/infrastructure/database/schema.d.ts +3 -1
- package/dist/infrastructure/database/schema.js +35 -6
- package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
- package/dist/infrastructure/database/workspace-repository.js +126 -0
- package/dist/mcp/custom/manager.d.ts +3 -1
- package/dist/mcp/custom/manager.js +37 -2
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
- package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
- package/dist/mcp/server/tool-control.d.ts +6 -1
- package/dist/mcp/server/tool-control.js +11 -2
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
- package/dist/plugins/agent/custom-mcp-store.js +38 -0
- package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
- package/dist/plugins/agent/mcp-gateway.js +110 -0
- package/dist/plugins/agent/mcp.js +3 -2
- package/dist/plugins/agent/plugin.js +1 -1
- package/dist/plugins/agent/provider.d.ts +1 -0
- package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
- package/dist/plugins/agent/providers/codex/launch.js +24 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
- package/dist/plugins/agent/providers/codex/provider.js +8 -2
- package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
- package/dist/plugins/agent/providers/codex/rpc.js +28 -3
- package/dist/plugins/agent/providers/index.d.ts +2 -1
- package/dist/plugins/agent/providers/index.js +5 -2
- package/dist/plugins/agent/repository.js +10 -7
- package/dist/plugins/agent/runtime-files.d.ts +4 -4
- package/dist/plugins/agent/runtime-files.js +18 -14
- package/dist/plugins/agent/runtime.d.ts +2 -0
- package/dist/plugins/agent/runtime.js +52 -13
- package/dist/plugins/agent/types.d.ts +3 -2
- package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
- package/dist/plugins/plugin-manager.d.ts +6 -1
- package/dist/plugins/plugin-manager.js +9 -2
- package/dist/plugins/types.d.ts +4 -0
- package/dist/plugins/web/http/agent-api-router.js +15 -1
- package/dist/plugins/web/http/git-api-router.js +13 -13
- package/dist/plugins/web/http/rtc-api-router.js +3 -0
- package/dist/plugins/web/http/runtime-api-router.js +22 -0
- package/dist/plugins/web/http/workspace-api-router.js +63 -12
- package/dist/plugins/web/plugin.js +1 -1
- package/dist/plugins/web/runtime.d.ts +3 -1
- package/dist/plugins/web/runtime.js +5 -1
- package/dist/plugins/workspace/mcp.js +20 -15
- package/dist/plugins/workspace/plugin.js +1 -1
- package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
- package/dist/plugins/workspace/workspace-root-policy.js +25 -19
- package/dist/plugins/workspace/workspace-service.d.ts +20 -8
- package/dist/plugins/workspace/workspace-service.js +270 -64
- package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
- package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
- package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
- 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
- package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
- package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
- package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
- package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
- package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
- package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
- package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
- package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
- package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
- package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
- package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
- package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
- package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
- package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
- package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
- package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
- package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
- package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
- package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
- package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
- package/dist/web/assets/index-9DLDvF8M.js +19 -0
- package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
- package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/transport-sw.js +43 -15
- package/package.json +1 -1
- package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
- package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
- package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
- package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
- package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
- package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
- package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
- package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
- package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
- package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
- package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
- package/dist/web/assets/index-CacDJSDn.js +0 -19
- 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.
|
|
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.
|
|
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.
|
|
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
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
37
|
-
inputSchema: z.object({
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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",
|
|
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"
|
|
10
|
+
static create(allowedRoots: string[], git: Pick<GitAccess, "projectFiles">, database: AppDatabase): Promise<WorkspaceService>;
|
|
9
11
|
roots(): string[];
|
|
10
12
|
list(): Promise<WorkspaceEntry[]>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
16
|
-
projectFilesResult(
|
|
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(
|
|
21
|
-
fs(
|
|
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
|
}
|