@chatroomcp/chatroom 0.2.0 → 0.2.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/README.md +1 -1
- package/dist/app/application.d.ts +0 -1
- package/dist/app/application.js +2 -0
- package/dist/app/event-bus.d.ts +0 -2
- package/dist/app/event-bus.js +0 -1
- package/dist/app/external-access-registry.d.ts +0 -1
- package/dist/app/external-access-registry.js +0 -1
- package/dist/app/lifecycle.d.ts +0 -1
- package/dist/auth/auth-service.js +0 -4
- package/dist/auth/ingress-policy.d.ts +0 -1
- package/dist/auth/ingress-policy.js +0 -1
- package/dist/cli/run-cli.js +0 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/core/auth/repository.d.ts +0 -23
- package/dist/core/auth/repository.js +0 -6
- package/dist/core/errors/chatroom-error.d.ts +0 -15
- package/dist/core/errors/chatroom-error.js +0 -15
- package/dist/core/operations/bounded-value.d.ts +0 -1
- package/dist/core/operations/bounded-value.js +26 -10
- package/dist/core/operations/redactor.js +0 -1
- package/dist/core/operations/repository.d.ts +0 -1
- package/dist/core/operations/types.d.ts +0 -4
- package/dist/core/operations/types.js +0 -2
- package/dist/infrastructure/database/app-database.js +4 -97
- package/dist/infrastructure/database/migrations.d.ts +2 -0
- package/dist/infrastructure/database/migrations.js +44 -0
- package/dist/infrastructure/database/operation-repository.js +3 -9
- package/dist/infrastructure/database/schema.d.ts +2 -0
- package/dist/infrastructure/database/schema.js +85 -0
- package/dist/infrastructure/http/http-server.js +0 -1
- package/dist/mcp/server/create-mcp-server.js +3 -3
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +0 -2
- package/dist/mcp/server/plugin-mcp-registrar.js +0 -4
- package/dist/mcp/server/tool-support.d.ts +0 -61
- package/dist/mcp/server/tool-support.js +0 -54
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +1 -1
- 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/operations/operation-log.d.ts +0 -3
- package/dist/operations/operation-log.js +0 -15
- package/dist/plugins/cloud/api-client.js +1 -0
- package/dist/plugins/cloud/controller.d.ts +2 -0
- package/dist/plugins/cloud/controller.js +28 -5
- package/dist/plugins/cloud/tunnel-client.js +0 -4
- package/dist/plugins/computer/computer-native-host.d.ts +2 -1
- package/dist/plugins/computer/computer-native-host.js +107 -17
- package/dist/plugins/computer/computer-schemas.d.ts +1 -0
- package/dist/plugins/computer/computer-schemas.js +1 -1
- package/dist/plugins/computer/types.d.ts +1 -1
- package/dist/plugins/git/git-service.d.ts +33 -0
- package/dist/plugins/git/git-service.js +414 -0
- package/dist/plugins/git/plugin.d.ts +4 -0
- package/dist/plugins/git/plugin.js +12 -0
- package/dist/plugins/git/types.d.ts +32 -0
- package/dist/plugins/process/backend.d.ts +0 -1
- package/dist/plugins/process/head-tail-buffer.d.ts +0 -1
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +0 -1
- package/dist/plugins/process/infrastructure/pty-process-backend.js +1 -4
- package/dist/plugins/process/process-supervisor.js +10 -23
- package/dist/plugins/process/types.d.ts +0 -5
- package/dist/plugins/types.d.ts +0 -1
- package/dist/plugins/web/api-types.d.ts +3 -7
- package/dist/plugins/web/http/api-router.d.ts +0 -1
- package/dist/plugins/web/http/api-router.js +2 -4
- package/dist/plugins/web/http/git-api-router.d.ts +3 -0
- package/dist/plugins/web/http/git-api-router.js +98 -0
- package/dist/plugins/web/http/workspace-api-router.js +15 -37
- package/dist/plugins/web/plugin.js +4 -6
- package/dist/plugins/web/runtime.d.ts +5 -27
- package/dist/plugins/web/runtime.js +3 -42
- package/dist/plugins/workspace/mcp.js +17 -135
- package/dist/plugins/workspace/metadata.d.ts +4 -0
- package/dist/plugins/workspace/metadata.js +105 -0
- package/dist/plugins/workspace/plugin.d.ts +2 -8
- package/dist/plugins/workspace/plugin.js +8 -20
- package/dist/plugins/workspace/types.d.ts +18 -0
- package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +2 -15
- package/dist/plugins/workspace/workspace-fs.js +111 -0
- package/dist/plugins/workspace/workspace-service.d.ts +7 -42
- package/dist/plugins/workspace/workspace-service.js +61 -315
- package/dist/presentation/http/http-utils.d.ts +0 -1
- package/dist/presentation/http/oauth-router.d.ts +0 -1
- package/dist/presentation/http/oauth-router.js +0 -1
- package/dist/web/assets/{index-BXlxEk8f.css → index-DFKEvyzR.css} +1 -1
- package/dist/web/assets/index-vzPPspuL.js +26 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -3
- package/dist/plugins/workspace/domain/filesystem.d.ts +0 -25
- package/dist/plugins/workspace/domain/git.d.ts +0 -7
- package/dist/plugins/workspace/domain/repository.d.ts +0 -16
- package/dist/plugins/workspace/domain/repository.js +0 -1
- package/dist/plugins/workspace/domain/review.d.ts +0 -28
- package/dist/plugins/workspace/domain/review.js +0 -1
- package/dist/plugins/workspace/domain/workspace.d.ts +0 -23
- package/dist/plugins/workspace/domain/workspace.js +0 -1
- package/dist/plugins/workspace/git/git-command-runner.d.ts +0 -6
- package/dist/plugins/workspace/git/git-command-runner.js +0 -15
- package/dist/plugins/workspace/git/git-service.d.ts +0 -14
- package/dist/plugins/workspace/git/git-service.js +0 -107
- package/dist/plugins/workspace/git/git-snapshot-service.d.ts +0 -33
- package/dist/plugins/workspace/git/git-snapshot-service.js +0 -352
- package/dist/plugins/workspace/infrastructure/workspace-fs.js +0 -340
- package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +0 -12
- package/dist/plugins/workspace/infrastructure/workspace-repository.js +0 -51
- package/dist/plugins/workspace/workspace-helpers.d.ts +0 -9
- package/dist/plugins/workspace/workspace-helpers.js +0 -67
- package/dist/plugins/workspace/worktree-review-service.d.ts +0 -16
- package/dist/plugins/workspace/worktree-review-service.js +0 -51
- package/dist/web/assets/index-DUM6zVHS.js +0 -26
- /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
- /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
|
|
3
|
+
import { asyncRoute, requireString, } from "../../../presentation/http/http-utils.js";
|
|
4
|
+
export function createGitApiRouter(application) {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.get("/git/status", asyncRoute(async (req, res) => {
|
|
7
|
+
const root = await resolveRoot(application, req.query.root);
|
|
8
|
+
res.json(await application.git.status(root));
|
|
9
|
+
}));
|
|
10
|
+
router.get("/git/diff", asyncRoute(async (req, res) => {
|
|
11
|
+
const root = await resolveRoot(application, req.query.root);
|
|
12
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : undefined;
|
|
13
|
+
res.json(await application.git.diff(root, filePath));
|
|
14
|
+
}));
|
|
15
|
+
router.get("/git/branches", asyncRoute(async (req, res) => {
|
|
16
|
+
const root = await resolveRoot(application, req.query.root);
|
|
17
|
+
res.json(await application.git.branches(root));
|
|
18
|
+
}));
|
|
19
|
+
router.get("/git/log", asyncRoute(async (req, res) => {
|
|
20
|
+
const root = await resolveRoot(application, req.query.root);
|
|
21
|
+
const requested = Number(req.query.limit ?? 30);
|
|
22
|
+
const limit = Number.isFinite(requested) ? requested : 30;
|
|
23
|
+
res.json(await application.git.log(root, limit));
|
|
24
|
+
}));
|
|
25
|
+
router.post("/git/stage", asyncRoute(async (req, res) => {
|
|
26
|
+
const body = bodyRecord(req.body);
|
|
27
|
+
const root = await resolveRoot(application, body.root);
|
|
28
|
+
const paths = requireStringArray(body.paths, "paths");
|
|
29
|
+
res.json(await mutate(application, "git.stage", root, { paths }, () => application.git.stage(root, paths)));
|
|
30
|
+
}));
|
|
31
|
+
router.post("/git/unstage", asyncRoute(async (req, res) => {
|
|
32
|
+
const body = bodyRecord(req.body);
|
|
33
|
+
const root = await resolveRoot(application, body.root);
|
|
34
|
+
const paths = requireStringArray(body.paths, "paths");
|
|
35
|
+
res.json(await mutate(application, "git.unstage", root, { paths }, () => application.git.unstage(root, paths)));
|
|
36
|
+
}));
|
|
37
|
+
router.post("/git/restore", asyncRoute(async (req, res) => {
|
|
38
|
+
const body = bodyRecord(req.body);
|
|
39
|
+
const root = await resolveRoot(application, body.root);
|
|
40
|
+
const filePath = requireString(body.path, "path");
|
|
41
|
+
res.json(await mutate(application, "git.restore", root, { path: filePath }, () => application.git.restore(root, filePath)));
|
|
42
|
+
}));
|
|
43
|
+
router.post("/git/commit", asyncRoute(async (req, res) => {
|
|
44
|
+
const body = bodyRecord(req.body);
|
|
45
|
+
const root = await resolveRoot(application, body.root);
|
|
46
|
+
const message = requireString(body.message, "message");
|
|
47
|
+
res.json(await mutate(application, "git.commit", root, { message }, () => application.git.commit(root, message)));
|
|
48
|
+
}));
|
|
49
|
+
router.post("/git/branches", asyncRoute(async (req, res) => {
|
|
50
|
+
const body = bodyRecord(req.body);
|
|
51
|
+
const root = await resolveRoot(application, body.root);
|
|
52
|
+
const name = requireString(body.name, "name");
|
|
53
|
+
res.json(await mutate(application, "git.branch.create", root, { name }, () => application.git.createBranch(root, name)));
|
|
54
|
+
}));
|
|
55
|
+
router.post("/git/switch", asyncRoute(async (req, res) => {
|
|
56
|
+
const body = bodyRecord(req.body);
|
|
57
|
+
const root = await resolveRoot(application, body.root);
|
|
58
|
+
const name = requireString(body.name, "name");
|
|
59
|
+
res.json(await mutate(application, "git.branch.switch", root, { name }, () => application.git.switchBranch(root, name)));
|
|
60
|
+
}));
|
|
61
|
+
router.delete("/git/branches", asyncRoute(async (req, res) => {
|
|
62
|
+
const body = bodyRecord(req.body);
|
|
63
|
+
const root = await resolveRoot(application, body.root);
|
|
64
|
+
const name = requireString(body.name, "name");
|
|
65
|
+
res.json(await mutate(application, "git.branch.delete", root, { name }, () => application.git.deleteBranch(root, name)));
|
|
66
|
+
}));
|
|
67
|
+
for (const action of ["fetch", "pull", "push"]) {
|
|
68
|
+
router.post(`/git/${action}`, asyncRoute(async (req, res) => {
|
|
69
|
+
const body = bodyRecord(req.body);
|
|
70
|
+
const root = await resolveRoot(application, body.root);
|
|
71
|
+
res.json(await mutate(application, `git.${action}`, root, {}, () => application.git[action](root)));
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return router;
|
|
75
|
+
}
|
|
76
|
+
async function resolveRoot(application, value) {
|
|
77
|
+
return application.workspaces.resolve(requireString(value, "root"));
|
|
78
|
+
}
|
|
79
|
+
function bodyRecord(value) {
|
|
80
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
81
|
+
? value
|
|
82
|
+
: {};
|
|
83
|
+
}
|
|
84
|
+
function requireStringArray(value, name) {
|
|
85
|
+
if (!Array.isArray(value) ||
|
|
86
|
+
value.length === 0 ||
|
|
87
|
+
value.some((item) => typeof item !== "string" || !item))
|
|
88
|
+
throw new ChatRoomError("INVALID_INPUT", `${name} must be a non-empty array of strings`);
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function mutate(application, action, root, input, operation) {
|
|
92
|
+
return application.operations.run({
|
|
93
|
+
pluginId: "git",
|
|
94
|
+
source: "gui",
|
|
95
|
+
action,
|
|
96
|
+
input: { root, ...input },
|
|
97
|
+
}, operation);
|
|
98
|
+
}
|
|
@@ -4,34 +4,29 @@ import { asyncRoute, requireString, } from "../../../presentation/http/http-util
|
|
|
4
4
|
export function createWorkspaceApiRouter(application) {
|
|
5
5
|
const router = Router();
|
|
6
6
|
router.get("/workspaces", asyncRoute(async (_req, res) => {
|
|
7
|
-
res.json(await application.
|
|
7
|
+
res.json(await application.workspaces.list());
|
|
8
8
|
}));
|
|
9
|
-
router.get("/
|
|
10
|
-
|
|
11
|
-
const workspace = application.getWorkspace(workspaceId);
|
|
12
|
-
const git = workspace.capabilities.git
|
|
13
|
-
? await application.gitInfoQuery(workspace.id)
|
|
14
|
-
: null;
|
|
15
|
-
res.json({ ...workspace, git });
|
|
9
|
+
router.get("/workspace", asyncRoute(async (req, res) => {
|
|
10
|
+
res.json(await application.workspaces.info(requireString(req.query.root, "root")));
|
|
16
11
|
}));
|
|
17
|
-
router.get("/
|
|
18
|
-
const
|
|
12
|
+
router.get("/workspace/files", asyncRoute(async (req, res) => {
|
|
13
|
+
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
19
14
|
const filePath = typeof req.query.path === "string" ? req.query.path : ".";
|
|
20
|
-
|
|
21
|
-
res.json(await application.listWorkspaceFiles(workspaceId, filePath, recursive));
|
|
15
|
+
res.json(await fs.list(filePath, { recursive: req.query.recursive === "1" }));
|
|
22
16
|
}));
|
|
23
|
-
router.get("/
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
router.get("/workspace/file", asyncRoute(async (req, res) => {
|
|
18
|
+
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
19
|
+
res.json(await fs.read(requireString(req.query.path, "path"), {
|
|
20
|
+
maxBytes: 2 * 1024 * 1024,
|
|
21
|
+
}));
|
|
27
22
|
}));
|
|
28
|
-
router.get("/
|
|
29
|
-
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
23
|
+
router.get("/workspace/file/image", asyncRoute(async (req, res) => {
|
|
30
24
|
const filePath = requireString(req.query.path, "path");
|
|
31
25
|
const mime = imageMime(filePath);
|
|
32
26
|
if (!mime)
|
|
33
27
|
throw new ChatRoomError("INVALID_INPUT", "File type is not supported for image preview");
|
|
34
|
-
const
|
|
28
|
+
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
29
|
+
const file = await fs.readBytes(filePath, { maxBytes: 10 * 1024 * 1024 });
|
|
35
30
|
if (file.truncated)
|
|
36
31
|
throw new ChatRoomError("INVALID_INPUT", "Image is too large to preview");
|
|
37
32
|
res.setHeader("Content-Type", mime);
|
|
@@ -40,27 +35,10 @@ export function createWorkspaceApiRouter(application) {
|
|
|
40
35
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
41
36
|
res.send(file.data);
|
|
42
37
|
}));
|
|
43
|
-
router.get("/workspaces/:workspaceId/worktree/diff", asyncRoute(async (req, res) => {
|
|
44
|
-
res.json(await application.previewWorktreeApply(requireString(req.params.workspaceId, "workspaceId")));
|
|
45
|
-
}));
|
|
46
|
-
router.get("/workspaces/:workspaceId/worktree/diff/file", asyncRoute(async (req, res) => {
|
|
47
|
-
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
48
|
-
res.json(await application.previewWorktreeFileDiff(workspaceId, requireString(req.query.path, "path")));
|
|
49
|
-
}));
|
|
50
|
-
router.post("/workspaces/:workspaceId/worktree/apply", asyncRoute(async (req, res) => {
|
|
51
|
-
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
52
|
-
const body = (req.body ?? {});
|
|
53
|
-
if (body.paths !== undefined &&
|
|
54
|
-
(!Array.isArray(body.paths) ||
|
|
55
|
-
body.paths.some((value) => typeof value !== "string")))
|
|
56
|
-
throw new ChatRoomError("INVALID_INPUT", "paths must be an array of file paths");
|
|
57
|
-
res.json(await application.applyWorktree(workspaceId, body.paths));
|
|
58
|
-
}));
|
|
59
38
|
return router;
|
|
60
39
|
}
|
|
61
40
|
function imageMime(filePath) {
|
|
62
|
-
|
|
63
|
-
switch (extension) {
|
|
41
|
+
switch (filePath.split(".").pop()?.toLowerCase()) {
|
|
64
42
|
case "png":
|
|
65
43
|
return "image/png";
|
|
66
44
|
case "jpg":
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { createServiceToken } from "../types.js";
|
|
2
|
-
import { WorkspaceService } from "../workspace/plugin.js";
|
|
3
|
-
import { ProcessService } from "../process/plugin.js";
|
|
4
2
|
import { ComputerServiceToken } from "../computer/plugin.js";
|
|
3
|
+
import { GitServiceToken } from "../git/plugin.js";
|
|
4
|
+
import { ProcessService } from "../process/plugin.js";
|
|
5
|
+
import { WorkspaceServiceToken } from "../workspace/plugin.js";
|
|
5
6
|
import { WebRuntime } from "./runtime.js";
|
|
6
7
|
export const WebServiceToken = createServiceToken("web");
|
|
7
8
|
export function createWebPlugin() {
|
|
8
9
|
return {
|
|
9
10
|
id: "web",
|
|
10
11
|
activate(context) {
|
|
11
|
-
const workspace = context.services.require(WorkspaceService);
|
|
12
|
-
const processes = context.services.require(ProcessService);
|
|
13
|
-
const computer = context.services.require(ComputerServiceToken);
|
|
14
12
|
context.services.provide(WebServiceToken, {
|
|
15
|
-
application: new WebRuntime(
|
|
13
|
+
application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitServiceToken), context.operations, context.services.require(ProcessService), context.services.require(ComputerServiceToken)),
|
|
16
14
|
});
|
|
17
15
|
},
|
|
18
16
|
};
|
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
import type { OperationLog } from "../../operations/operation-log.js";
|
|
2
|
-
import type { WorkspaceService } from "../workspace/workspace-service.js";
|
|
3
|
-
import type { ProcessSupervisor } from "../process/process-supervisor.js";
|
|
4
|
-
import type { GitService } from "../workspace/git/git-service.js";
|
|
5
2
|
import type { ComputerService } from "../computer/computer-service.js";
|
|
3
|
+
import type { GitService } from "../git/git-service.js";
|
|
4
|
+
import type { ProcessSupervisor } from "../process/process-supervisor.js";
|
|
5
|
+
import type { WorkspaceService } from "../workspace/workspace-service.js";
|
|
6
6
|
export declare class WebRuntime {
|
|
7
7
|
readonly workspaces: WorkspaceService;
|
|
8
|
+
readonly git: GitService;
|
|
8
9
|
readonly operations: OperationLog;
|
|
9
10
|
readonly processes: ProcessSupervisor;
|
|
10
|
-
readonly git: GitService;
|
|
11
11
|
readonly computer: ComputerService;
|
|
12
|
-
constructor(workspaces: WorkspaceService, operations: OperationLog, processes: ProcessSupervisor,
|
|
13
|
-
previewWorktreeApply(workspaceId: string): Promise<import("./api-types.js").WorktreeApplyPreview>;
|
|
14
|
-
previewWorktreeFileDiff(workspaceId: string, filePath: string): Promise<import("./api-types.js").WorktreeFileDiff>;
|
|
15
|
-
applyWorktree(workspaceId: string, paths?: string[]): Promise<{
|
|
16
|
-
workspaceId: string;
|
|
17
|
-
bytes: number;
|
|
18
|
-
paths: string[];
|
|
19
|
-
conflicts: string[];
|
|
20
|
-
}>;
|
|
12
|
+
constructor(workspaces: WorkspaceService, git: GitService, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService);
|
|
21
13
|
processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
|
|
22
14
|
listProcesses(): import("./api-types.js").ProcessSnapshot[];
|
|
23
15
|
getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
|
|
24
|
-
discoverWorkspaces(): Promise<import("../workspace/domain/workspace.js").Workspace[]>;
|
|
25
|
-
getWorkspace(workspaceId: string): import("../workspace/domain/workspace.js").Workspace;
|
|
26
|
-
readWorkspaceFile(workspaceId: string, filePath: string, maxBytes?: number): Promise<{
|
|
27
|
-
content: string;
|
|
28
|
-
bytes: number;
|
|
29
|
-
truncated: boolean;
|
|
30
|
-
}>;
|
|
31
|
-
readWorkspaceFileBytes(workspaceId: string, filePath: string, maxBytes: number): Promise<{
|
|
32
|
-
data: Buffer;
|
|
33
|
-
bytes: number;
|
|
34
|
-
truncated: boolean;
|
|
35
|
-
}>;
|
|
36
|
-
listWorkspaceFiles(workspaceId: string, filePath?: string, recursive?: boolean): Promise<import("../workspace/domain/filesystem.js").FileInfo[]>;
|
|
37
|
-
gitInfoQuery(workspaceId: string): Promise<import("../workspace/domain/git.js").GitInfo>;
|
|
38
16
|
}
|
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
export class WebRuntime {
|
|
2
2
|
workspaces;
|
|
3
|
+
git;
|
|
3
4
|
operations;
|
|
4
5
|
processes;
|
|
5
|
-
git;
|
|
6
6
|
computer;
|
|
7
|
-
constructor(workspaces, operations, processes,
|
|
7
|
+
constructor(workspaces, git, operations, processes, computer) {
|
|
8
8
|
this.workspaces = workspaces;
|
|
9
|
+
this.git = git;
|
|
9
10
|
this.operations = operations;
|
|
10
11
|
this.processes = processes;
|
|
11
|
-
this.git = git;
|
|
12
12
|
this.computer = computer;
|
|
13
13
|
}
|
|
14
|
-
previewWorktreeApply(workspaceId) {
|
|
15
|
-
return this.workspaces.previewWorktreeApply(workspaceId);
|
|
16
|
-
}
|
|
17
|
-
previewWorktreeFileDiff(workspaceId, filePath) {
|
|
18
|
-
return this.workspaces.previewWorktreeFileDiff(workspaceId, filePath);
|
|
19
|
-
}
|
|
20
|
-
applyWorktree(workspaceId, paths) {
|
|
21
|
-
return this.operations.run({
|
|
22
|
-
pluginId: "workspace",
|
|
23
|
-
source: "gui",
|
|
24
|
-
action: "worktree.apply",
|
|
25
|
-
workspaceId,
|
|
26
|
-
input: { workspaceId, paths: paths ?? null },
|
|
27
|
-
}, () => this.workspaces.applyWorktree(workspaceId, paths));
|
|
28
|
-
}
|
|
29
14
|
processKill(processId, force = false) {
|
|
30
15
|
return this.operations.run({
|
|
31
16
|
pluginId: "process",
|
|
@@ -41,28 +26,4 @@ export class WebRuntime {
|
|
|
41
26
|
getProcess(processId) {
|
|
42
27
|
return this.processes.read(processId);
|
|
43
28
|
}
|
|
44
|
-
discoverWorkspaces() {
|
|
45
|
-
return this.workspaces.discoverGitWorkspaces();
|
|
46
|
-
}
|
|
47
|
-
getWorkspace(workspaceId) {
|
|
48
|
-
return this.workspaces.get(workspaceId);
|
|
49
|
-
}
|
|
50
|
-
async readWorkspaceFile(workspaceId, filePath, maxBytes) {
|
|
51
|
-
const fs = await this.workspaces.fs(workspaceId);
|
|
52
|
-
return fs.read(filePath, {
|
|
53
|
-
...(maxBytes === undefined ? {} : { maxBytes }),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
async readWorkspaceFileBytes(workspaceId, filePath, maxBytes) {
|
|
57
|
-
const fs = await this.workspaces.fs(workspaceId);
|
|
58
|
-
return fs.readBytes(filePath, { maxBytes });
|
|
59
|
-
}
|
|
60
|
-
async listWorkspaceFiles(workspaceId, filePath = ".", recursive = false) {
|
|
61
|
-
const fs = await this.workspaces.fs(workspaceId);
|
|
62
|
-
return fs.list(filePath, { recursive });
|
|
63
|
-
}
|
|
64
|
-
gitInfoQuery(workspaceId) {
|
|
65
|
-
const workspace = this.workspaces.markUsed(workspaceId);
|
|
66
|
-
return this.git.info(workspace.root);
|
|
67
|
-
}
|
|
68
29
|
}
|
|
@@ -1,139 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { closedRead } from "../../mcp/server/tool-support.js";
|
|
3
|
+
const workspaceInfoSchema = z.object({
|
|
4
|
+
root: z.string(),
|
|
5
|
+
instructions: z.string().nullable(),
|
|
6
|
+
skills: z.array(z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
description: z.string(),
|
|
9
|
+
path: z.string(),
|
|
10
|
+
})),
|
|
11
|
+
});
|
|
3
12
|
export function registerWorkspaceTools(mcp, workspaces) {
|
|
4
|
-
mcp.registerTool("
|
|
5
|
-
title: "
|
|
6
|
-
description: "
|
|
7
|
-
inputSchema: z.object({
|
|
8
|
-
|
|
9
|
-
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
10
|
-
}),
|
|
11
|
-
outputSchema: workspaceOutputSchema,
|
|
12
|
-
annotations: localMutation,
|
|
13
|
-
action: "open",
|
|
14
|
-
}, (input) => workspaces.open({
|
|
15
|
-
path: input.path,
|
|
16
|
-
...(input.mode ? { mode: input.mode } : {}),
|
|
17
|
-
}));
|
|
18
|
-
mcp.registerTool("remove_workspace", {
|
|
19
|
-
title: "Remove workspace",
|
|
20
|
-
description: "Unregister a checkout workspace or remove a managed worktree. Dirty worktrees require force=true.",
|
|
21
|
-
inputSchema: z.object({
|
|
22
|
-
workspaceId: z.string(),
|
|
23
|
-
force: z.boolean().optional(),
|
|
24
|
-
}),
|
|
25
|
-
outputSchema: z.object({
|
|
26
|
-
removed: z.boolean(),
|
|
27
|
-
workspaceId: z.string(),
|
|
28
|
-
mode: z.enum(["checkout", "worktree"]),
|
|
29
|
-
}),
|
|
30
|
-
annotations: destructiveLocalMutation,
|
|
31
|
-
action: "remove",
|
|
32
|
-
}, async (input) => {
|
|
33
|
-
const removed = await workspaces.remove(input.workspaceId, input.force ?? false);
|
|
34
|
-
return { removed: true, workspaceId: removed.id, mode: removed.mode };
|
|
35
|
-
});
|
|
36
|
-
mcp.registerTool("fs_read", {
|
|
37
|
-
title: "Read file",
|
|
38
|
-
description: "Read a workspace-relative file through the ChatRoom filesystem boundary.",
|
|
39
|
-
inputSchema: z.object({
|
|
40
|
-
workspaceId: z.string(),
|
|
41
|
-
path: z.string(),
|
|
42
|
-
maxBytes: z
|
|
43
|
-
.number()
|
|
44
|
-
.int()
|
|
45
|
-
.positive()
|
|
46
|
-
.max(16 * 1024 * 1024)
|
|
47
|
-
.optional(),
|
|
48
|
-
}),
|
|
49
|
-
outputSchema: fileReadOutputSchema,
|
|
13
|
+
mcp.registerTool("workspace_info", {
|
|
14
|
+
title: "Workspace info",
|
|
15
|
+
description: "Read project instructions and skill metadata for a directory within the configured allowed roots.",
|
|
16
|
+
inputSchema: z.object({ root: z.string() }),
|
|
17
|
+
outputSchema: workspaceInfoSchema,
|
|
50
18
|
annotations: closedRead,
|
|
51
|
-
action: "
|
|
52
|
-
},
|
|
53
|
-
const fs = await workspaces.fs(input.workspaceId);
|
|
54
|
-
return fs.read(input.path, input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes });
|
|
55
|
-
});
|
|
56
|
-
mcp.registerTool("fs_write", {
|
|
57
|
-
title: "Write file",
|
|
58
|
-
description: "Atomically write a workspace-relative file.",
|
|
59
|
-
inputSchema: z.object({
|
|
60
|
-
workspaceId: z.string(),
|
|
61
|
-
path: z.string(),
|
|
62
|
-
content: z.string(),
|
|
63
|
-
}),
|
|
64
|
-
outputSchema: fileWriteOutputSchema,
|
|
65
|
-
annotations: localWrite,
|
|
66
|
-
action: "fs.write",
|
|
67
|
-
}, async (input) => {
|
|
68
|
-
const fs = await workspaces.fs(input.workspaceId);
|
|
69
|
-
return fs.write(input.path, input.content);
|
|
70
|
-
});
|
|
71
|
-
mcp.registerTool("fs_list", {
|
|
72
|
-
title: "List files",
|
|
73
|
-
description: "List files under a workspace-relative directory.",
|
|
74
|
-
inputSchema: z.object({
|
|
75
|
-
workspaceId: z.string(),
|
|
76
|
-
path: z.string().default("."),
|
|
77
|
-
recursive: z.boolean().default(false),
|
|
78
|
-
}),
|
|
79
|
-
outputSchema: z.object({ files: z.array(fileInfoSchema) }),
|
|
80
|
-
annotations: closedRead,
|
|
81
|
-
action: "fs.list",
|
|
82
|
-
}, async (input) => {
|
|
83
|
-
const fs = await workspaces.fs(input.workspaceId);
|
|
84
|
-
return {
|
|
85
|
-
files: await fs.list(input.path, { recursive: input.recursive }),
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
mcp.registerTool("fs_search", {
|
|
89
|
-
title: "Search files",
|
|
90
|
-
description: "Search text under a workspace.",
|
|
91
|
-
inputSchema: z.object({
|
|
92
|
-
workspaceId: z.string(),
|
|
93
|
-
query: z.string().min(1),
|
|
94
|
-
path: z.string().default("."),
|
|
95
|
-
maxResults: z.number().int().positive().max(2000).default(200),
|
|
96
|
-
}),
|
|
97
|
-
outputSchema: z.object({ matches: z.array(searchMatchSchema) }),
|
|
98
|
-
annotations: closedRead,
|
|
99
|
-
action: "fs.search",
|
|
100
|
-
}, async (input) => {
|
|
101
|
-
const fs = await workspaces.fs(input.workspaceId);
|
|
102
|
-
return {
|
|
103
|
-
matches: await fs.search(input.query, {
|
|
104
|
-
path: input.path,
|
|
105
|
-
maxResults: input.maxResults,
|
|
106
|
-
}),
|
|
107
|
-
};
|
|
108
|
-
});
|
|
109
|
-
mcp.registerTool("fs_patch", {
|
|
110
|
-
title: "Transactional patch",
|
|
111
|
-
description: "Validate all replacements before committing a multi-file text patch.",
|
|
112
|
-
inputSchema: z.object({
|
|
113
|
-
workspaceId: z.string(),
|
|
114
|
-
replacements: z
|
|
115
|
-
.array(z.object({
|
|
116
|
-
path: z.string(),
|
|
117
|
-
oldText: z.string().min(1),
|
|
118
|
-
newText: z.string(),
|
|
119
|
-
occurrence: z
|
|
120
|
-
.union([z.number().int().positive(), z.literal("all")])
|
|
121
|
-
.optional(),
|
|
122
|
-
}))
|
|
123
|
-
.min(1),
|
|
124
|
-
}),
|
|
125
|
-
outputSchema: changeSetSchema,
|
|
126
|
-
annotations: destructiveLocalMutation,
|
|
127
|
-
action: "fs.patch",
|
|
128
|
-
}, async (input) => {
|
|
129
|
-
const fs = await workspaces.fs(input.workspaceId);
|
|
130
|
-
return fs.patch(input.replacements.map((item) => ({
|
|
131
|
-
path: item.path,
|
|
132
|
-
oldText: item.oldText,
|
|
133
|
-
newText: item.newText,
|
|
134
|
-
...(item.occurrence === undefined
|
|
135
|
-
? {}
|
|
136
|
-
: { occurrence: item.occurrence }),
|
|
137
|
-
})));
|
|
138
|
-
});
|
|
19
|
+
action: "info",
|
|
20
|
+
}, (input) => workspaces.info(input.root));
|
|
139
21
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ChatRoomError } from "../../core/errors/chatroom-error.js";
|
|
3
|
+
const SKILL_ROOTS = [".agents/skills", ".claude/skills", ".chatroom/skills"];
|
|
4
|
+
const MAX_METADATA_BYTES = 64 * 1024;
|
|
5
|
+
export async function readInstructions(fs) {
|
|
6
|
+
try {
|
|
7
|
+
return (await fs.read("AGENTS.md", { maxBytes: MAX_METADATA_BYTES }))
|
|
8
|
+
.content;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof ChatRoomError && error.code === "NOT_FOUND")
|
|
12
|
+
return null;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function readSkills(fs) {
|
|
17
|
+
const paths = [];
|
|
18
|
+
for (const root of SKILL_ROOTS) {
|
|
19
|
+
try {
|
|
20
|
+
const entries = await fs.list(root, {
|
|
21
|
+
recursive: true,
|
|
22
|
+
maxEntries: 2000,
|
|
23
|
+
});
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.type === "file" && entry.path.endsWith("/SKILL.md"))
|
|
26
|
+
paths.push(entry.path);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!(error instanceof ChatRoomError) || error.code !== "NOT_FOUND")
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Promise.all([...new Set(paths)].sort().map(async (skillPath) => {
|
|
35
|
+
const fallbackName = path.posix.basename(path.posix.dirname(skillPath));
|
|
36
|
+
try {
|
|
37
|
+
const file = await fs.read(skillPath, { maxBytes: MAX_METADATA_BYTES });
|
|
38
|
+
const metadata = parseSkillFrontmatter(file.content);
|
|
39
|
+
return {
|
|
40
|
+
name: metadata.name || fallbackName,
|
|
41
|
+
description: metadata.description,
|
|
42
|
+
path: skillPath,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!(error instanceof ChatRoomError) || error.code !== "NOT_FOUND")
|
|
47
|
+
throw error;
|
|
48
|
+
return { name: fallbackName, description: "", path: skillPath };
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
function parseSkillFrontmatter(content) {
|
|
53
|
+
const lines = content
|
|
54
|
+
.replace(/^\uFEFF/, "")
|
|
55
|
+
.replaceAll("\r\n", "\n")
|
|
56
|
+
.split("\n");
|
|
57
|
+
if (lines[0]?.trim() !== "---")
|
|
58
|
+
return { name: "", description: "" };
|
|
59
|
+
const end = lines.findIndex((line, index) => index > 0 && ["---", "..."].includes(line.trim()));
|
|
60
|
+
if (end < 0)
|
|
61
|
+
return { name: "", description: "" };
|
|
62
|
+
const values = new Map();
|
|
63
|
+
for (let index = 1; index < end; index++) {
|
|
64
|
+
const match = /^([A-Za-z0-9_-]+):(?:\s*(.*))?$/.exec(lines[index] ?? "");
|
|
65
|
+
if (!match)
|
|
66
|
+
continue;
|
|
67
|
+
const key = match[1] ?? "";
|
|
68
|
+
if (key !== "name" && key !== "description")
|
|
69
|
+
continue;
|
|
70
|
+
const raw = match[2] ?? "";
|
|
71
|
+
if (/^[>|][+-]?\d?$/.test(raw.trim())) {
|
|
72
|
+
const block = [];
|
|
73
|
+
while (index + 1 < end) {
|
|
74
|
+
const next = lines[index + 1] ?? "";
|
|
75
|
+
if (next && !/^\s/.test(next))
|
|
76
|
+
break;
|
|
77
|
+
index++;
|
|
78
|
+
block.push(next.replace(/^\s+/, ""));
|
|
79
|
+
}
|
|
80
|
+
values.set(key, raw.trim().startsWith(">")
|
|
81
|
+
? block.join(" ").replace(/\s+/g, " ").trim()
|
|
82
|
+
: block.join("\n").trim());
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
values.set(key, parseYamlScalar(raw));
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
name: values.get("name")?.trim() ?? "",
|
|
89
|
+
description: values.get("description")?.trim() ?? "",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function parseYamlScalar(value) {
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(trimmed);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return trimmed.slice(1, -1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
103
|
+
return trimmed.slice(1, -1).replaceAll("''", "'");
|
|
104
|
+
return trimmed;
|
|
105
|
+
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { InternalPlugin } from "../types.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
interface WorkspacePluginService {
|
|
5
|
-
workspaces: WorkspaceRuntime;
|
|
6
|
-
git: GitService;
|
|
7
|
-
}
|
|
8
|
-
export declare const WorkspaceService: import("../types.js").ServiceToken<WorkspacePluginService>;
|
|
2
|
+
import { WorkspaceService } from "./workspace-service.js";
|
|
3
|
+
export declare const WorkspaceServiceToken: import("../types.js").ServiceToken<WorkspaceService>;
|
|
9
4
|
export declare function createWorkspacePlugin(): InternalPlugin;
|
|
10
|
-
export {};
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import { CommandRunner } from "../../core/runtime/command-runner.js";
|
|
2
1
|
import { createServiceToken } from "../types.js";
|
|
3
|
-
import { GitCommandRunner } from "./git/git-command-runner.js";
|
|
4
|
-
import { GitService } from "./git/git-service.js";
|
|
5
|
-
import { GitSnapshotService } from "./git/git-snapshot-service.js";
|
|
6
|
-
import { WorkspaceRepository } from "./infrastructure/workspace-repository.js";
|
|
7
2
|
import { registerWorkspaceTools } from "./mcp.js";
|
|
8
|
-
import { WorkspaceService
|
|
9
|
-
|
|
10
|
-
export const WorkspaceService = createServiceToken("workspace");
|
|
3
|
+
import { WorkspaceService } from "./workspace-service.js";
|
|
4
|
+
export const WorkspaceServiceToken = createServiceToken("workspace");
|
|
11
5
|
export function createWorkspacePlugin() {
|
|
12
|
-
let
|
|
6
|
+
let service = null;
|
|
13
7
|
return {
|
|
14
8
|
id: "workspace",
|
|
15
9
|
async activate(context) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const snapshots = new GitSnapshotService(gitRunner, git);
|
|
19
|
-
const review = new WorktreeReviewService(git, snapshots);
|
|
20
|
-
const workspaces = await WorkspaceRuntime.create(new WorkspaceRepository(context.database), git, review, context.config.allowedRoots, context.config.dataDir);
|
|
21
|
-
await workspaces.discoverGitWorkspaces();
|
|
22
|
-
value = { workspaces, git };
|
|
23
|
-
context.services.provide(WorkspaceService, value);
|
|
10
|
+
service = await WorkspaceService.create(context.config.allowedRoots);
|
|
11
|
+
context.services.provide(WorkspaceServiceToken, service);
|
|
24
12
|
},
|
|
25
13
|
registerMcp(mcp) {
|
|
26
|
-
if (!
|
|
14
|
+
if (!service)
|
|
27
15
|
throw new Error("Workspace plugin is not active");
|
|
28
|
-
registerWorkspaceTools(mcp,
|
|
16
|
+
registerWorkspaceTools(mcp, service);
|
|
29
17
|
},
|
|
30
18
|
deactivate() {
|
|
31
|
-
|
|
19
|
+
service = null;
|
|
32
20
|
},
|
|
33
21
|
};
|
|
34
22
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface WorkspaceEntry {
|
|
2
|
+
root: string;
|
|
3
|
+
}
|
|
4
|
+
export interface WorkspaceSkill {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkspaceInfo {
|
|
10
|
+
root: string;
|
|
11
|
+
instructions: string | null;
|
|
12
|
+
skills: WorkspaceSkill[];
|
|
13
|
+
}
|
|
14
|
+
export interface WorkspaceFile {
|
|
15
|
+
path: string;
|
|
16
|
+
type: "file" | "directory" | "symlink";
|
|
17
|
+
size: number;
|
|
18
|
+
}
|