@zhanguym/chatroom 1.0.0 → 1.1.0-beta.0
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 +0 -1
- package/dist/app/application.js +8 -7
- package/dist/app/event-bus.d.ts +13 -10
- package/dist/app/git-access.d.ts +50 -0
- package/dist/app/git-access.js +2 -0
- package/dist/app/service-registry.d.ts +11 -0
- package/dist/app/service-registry.js +17 -0
- package/dist/app/workspace-access.d.ts +24 -0
- package/dist/app/workspace-access.js +2 -0
- package/dist/auth/ingress-policy.d.ts +2 -0
- package/dist/auth/ingress-policy.js +14 -2
- package/dist/auth/passkey-service.js +1 -1
- package/dist/cli/run-cli.js +1 -1
- package/dist/core/fs/safe-directory.d.ts +9 -0
- package/dist/core/fs/safe-directory.js +129 -0
- package/dist/core/operations/repository.d.ts +2 -0
- package/dist/core/runtime/line-process.d.ts +19 -0
- package/dist/core/runtime/line-process.js +51 -0
- package/dist/infrastructure/database/operation-repository.js +8 -2
- package/dist/infrastructure/database/schema.d.ts +1 -1
- package/dist/infrastructure/database/schema.js +62 -0
- package/dist/infrastructure/http/http-server.js +1 -1
- package/dist/native/linux/chatroom-computer-helper +0 -0
- 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.js +16 -4
- package/dist/plugins/agent/async-queue.d.ts +10 -0
- package/dist/plugins/agent/async-queue.js +49 -0
- package/dist/plugins/agent/mcp.d.ts +3 -0
- package/dist/plugins/agent/mcp.js +502 -0
- package/dist/plugins/agent/plugin.d.ts +4 -0
- package/dist/plugins/agent/plugin.js +35 -0
- package/dist/plugins/agent/provider.d.ts +132 -0
- package/dist/plugins/agent/providers/codex/mapping.d.ts +56 -0
- package/dist/plugins/agent/providers/codex/mapping.js +427 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +84 -0
- package/dist/plugins/agent/providers/codex/provider.js +828 -0
- package/dist/plugins/agent/providers/codex/rpc.d.ts +40 -0
- package/dist/plugins/agent/providers/codex/rpc.js +227 -0
- package/dist/plugins/agent/providers/common.d.ts +11 -0
- package/dist/plugins/agent/providers/common.js +75 -0
- package/dist/plugins/agent/providers/index.d.ts +3 -0
- package/dist/plugins/agent/providers/index.js +4 -0
- package/dist/plugins/agent/repository.d.ts +21 -0
- package/dist/plugins/agent/repository.js +226 -0
- package/dist/plugins/agent/runtime-config.d.ts +5 -0
- package/dist/plugins/agent/runtime-config.js +60 -0
- package/dist/plugins/agent/runtime-files.d.ts +8 -0
- package/dist/plugins/agent/runtime-files.js +170 -0
- package/dist/plugins/agent/runtime-state.d.ts +17 -0
- package/dist/plugins/agent/runtime-state.js +133 -0
- package/dist/plugins/agent/runtime.d.ts +60 -0
- package/dist/plugins/agent/runtime.js +928 -0
- package/dist/plugins/agent/store.d.ts +17 -0
- package/dist/plugins/agent/store.js +1 -0
- package/dist/plugins/agent/types.d.ts +320 -0
- package/dist/plugins/agent/types.js +13 -0
- package/dist/plugins/cloud/plugin.d.ts +1 -1
- package/dist/plugins/cloud/plugin.js +3 -3
- package/dist/plugins/cloud/tunnel-client.js +6 -7
- package/dist/plugins/computer/computer-schemas.d.ts +7 -7
- package/dist/plugins/computer/computer-service.d.ts +3 -0
- package/dist/plugins/computer/computer-service.js +33 -1
- package/dist/plugins/computer/plugin.d.ts +1 -1
- package/dist/plugins/computer/plugin.js +1 -1
- package/dist/plugins/git/git-service.d.ts +3 -2
- package/dist/plugins/git/git-service.js +37 -1
- package/dist/plugins/git/plugin.d.ts +0 -2
- package/dist/plugins/git/plugin.js +3 -3
- package/dist/plugins/process/plugin.d.ts +1 -1
- package/dist/plugins/process/plugin.js +3 -3
- package/dist/plugins/process/process-supervisor.d.ts +1 -0
- package/dist/plugins/process/process-supervisor.js +15 -4
- package/dist/plugins/types.d.ts +1 -11
- package/dist/plugins/types.js +1 -17
- package/dist/plugins/web/api-types.d.ts +20 -5
- package/dist/plugins/web/http/agent-api-router.d.ts +3 -0
- package/dist/plugins/web/http/agent-api-router.js +254 -0
- package/dist/plugins/web/http/api-router.js +9 -0
- package/dist/plugins/web/http/computer-api-router.js +21 -6
- package/dist/plugins/web/http/process-api-router.js +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +79 -6
- package/dist/plugins/web/plugin.d.ts +1 -1
- package/dist/plugins/web/plugin.js +5 -4
- package/dist/plugins/web/runtime.d.ts +6 -3
- package/dist/plugins/web/runtime.js +6 -1
- package/dist/plugins/workspace/mcp.js +16 -0
- package/dist/plugins/workspace/metadata.d.ts +1 -1
- package/dist/plugins/workspace/plugin.d.ts +1 -1
- package/dist/plugins/workspace/plugin.js +5 -2
- package/dist/plugins/workspace/types.d.ts +1 -18
- package/dist/plugins/workspace/workspace-fs.d.ts +9 -3
- package/dist/plugins/workspace/workspace-fs.js +78 -125
- package/dist/plugins/workspace/workspace-root-policy.d.ts +9 -0
- package/dist/plugins/workspace/workspace-root-policy.js +56 -0
- package/dist/plugins/workspace/workspace-service.d.ts +11 -6
- package/dist/plugins/workspace/workspace-service.js +39 -49
- package/dist/web/assets/AgentView-DKuspiki.js +4 -0
- package/dist/web/assets/AgentView-DYJeMJLX.css +1 -0
- package/dist/web/assets/{CloudView-CHjVbp-5.css → CloudView-DVVvwuWW.css} +1 -1
- package/dist/web/assets/CloudView-PbREbJgz.js +1 -0
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-CiFNEpWo.js +3 -0
- package/dist/web/assets/ComputerView-BM0u3wSF.css +1 -0
- package/dist/web/assets/ComputerView-BRb2sE0Z.js +1 -0
- package/dist/web/assets/McpToolsView-B8NSFdf4.js +1 -0
- package/dist/web/assets/McpToolsView-C22ZbSuV.css +1 -0
- package/dist/web/assets/OperationsView-BjTY2lIh.css +1 -0
- package/dist/web/assets/OperationsView-Cavi-6SZ.js +4 -0
- package/dist/web/assets/ProcessesView-Ce6iVwjA.css +1 -0
- package/dist/web/assets/ProcessesView-UO48Nh2q.js +4 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-Bvc9wRyA.js +1 -0
- package/dist/web/assets/SystemLogsView-BXmOrmu5.js +1 -0
- package/dist/web/assets/SystemLogsView-CofP4Jvi.css +1 -0
- package/dist/web/assets/VSelect-BPRXx8KM.js +1 -0
- package/dist/web/assets/VSpacer-Cl-QiwaM.js +1 -0
- package/dist/web/assets/VSwitch-C0XNj1lR.js +1 -0
- package/dist/web/assets/{VTabs-qBcuuXh3.js → VTabs-CHj39Ket.js} +1 -1
- package/dist/web/assets/VTextarea-BIcUWOU6.js +16 -0
- package/dist/web/assets/VTextarea-DG1zE1as.css +1 -0
- package/dist/web/assets/WorkspacesView-DPCt6kYy.css +1 -0
- package/dist/web/assets/WorkspacesView-rY6RXeiR.js +2 -0
- package/dist/web/assets/entity-refresh-queue-wQii8Dl5.js +1 -0
- package/dist/web/assets/{index-CkjqXRSj.css → index-BPpl3-sP.css} +1 -1
- package/dist/web/assets/index-Dt-pWiW1.js +19 -0
- package/dist/web/assets/{syntax-highlight-B5ArnyrC.js → syntax-highlight-C_fuMcwC.js} +1 -1
- package/dist/web/assets/useMasterDetailFocus-D0j-KcXv.js +1 -0
- package/dist/web/assets/useOperationLabels-DR8r6Vrl.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/plugins/git/types.d.ts +0 -32
- package/dist/web/assets/CloudView-CvX0dkgu.js +0 -1
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +0 -3
- package/dist/web/assets/ComputerView-91Z9Cefw.css +0 -1
- package/dist/web/assets/ComputerView-OOyEqWml.js +0 -1
- package/dist/web/assets/McpToolsView-BTnolyS6.css +0 -1
- package/dist/web/assets/McpToolsView-DjXWTKrZ.js +0 -1
- package/dist/web/assets/OperationsView-DqTY51Cr.js +0 -4
- package/dist/web/assets/ProcessesView-C13BykIa.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +0 -1
- package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +0 -1
- package/dist/web/assets/VSelect-DrRpKyYI.js +0 -1
- package/dist/web/assets/VSpacer-C3wAY3F0.js +0 -1
- package/dist/web/assets/VSwitch-CxusR4iO.js +0 -1
- package/dist/web/assets/WorkspacesView-BzQ0aH74.css +0 -1
- package/dist/web/assets/WorkspacesView-CijIvfsp.js +0 -2
- package/dist/web/assets/index-BegKQ3Yd.js +0 -19
- package/dist/web/assets/useOperationLabels-BDjzGDkT.js +0 -1
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.d.ts +0 -0
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.js +0 -0
- /package/dist/plugins/{git/types.js → agent/provider.js} +0 -0
|
@@ -3,6 +3,6 @@ import { WebRuntime } from "./runtime.js";
|
|
|
3
3
|
interface WebPluginService {
|
|
4
4
|
application: WebRuntime;
|
|
5
5
|
}
|
|
6
|
-
export declare const WebServiceToken: import("#
|
|
6
|
+
export declare const WebServiceToken: import("#app/service-registry").ServiceToken<WebPluginService>;
|
|
7
7
|
export declare function createWebPlugin(): InternalPlugin;
|
|
8
8
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createServiceToken } from "#
|
|
1
|
+
import { createServiceToken } from "#app/service-registry";
|
|
2
2
|
import { ComputerServiceToken } from "#plugins/computer/plugin";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { GitAccessToken } from "#app/git-access";
|
|
4
|
+
import { ProcessServiceToken } from "#plugins/process/plugin";
|
|
5
5
|
import { WorkspaceServiceToken } from "#plugins/workspace/plugin";
|
|
6
|
+
import { AgentServiceToken } from "#plugins/agent/plugin";
|
|
6
7
|
import { WebRuntime } from "./runtime.js";
|
|
7
8
|
export const WebServiceToken = createServiceToken("web");
|
|
8
9
|
export function createWebPlugin() {
|
|
@@ -10,7 +11,7 @@ export function createWebPlugin() {
|
|
|
10
11
|
id: "web",
|
|
11
12
|
activate(context) {
|
|
12
13
|
context.services.provide(WebServiceToken, {
|
|
13
|
-
application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(
|
|
14
|
+
application: new WebRuntime(context.services.require(WorkspaceServiceToken), context.services.require(GitAccessToken), context.operations, context.services.require(ProcessServiceToken), context.services.require(ComputerServiceToken), context.services.require(AgentServiceToken), context.mcpTools),
|
|
14
15
|
});
|
|
15
16
|
},
|
|
16
17
|
};
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import type { OperationLog } from "#operations/operation-log";
|
|
2
2
|
import type { ComputerService } from "#plugins/computer/computer-service";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GitAccess } from "#app/git-access";
|
|
4
4
|
import type { ProcessSupervisor } from "#plugins/process/process-supervisor";
|
|
5
5
|
import type { WorkspaceService } from "#plugins/workspace/workspace-service";
|
|
6
|
+
import type { AgentRuntime } from "#plugins/agent/runtime";
|
|
6
7
|
import type { McpToolControl } from "#mcp/server/tool-control";
|
|
7
8
|
export declare class WebRuntime {
|
|
8
9
|
readonly workspaces: WorkspaceService;
|
|
9
|
-
readonly git:
|
|
10
|
+
readonly git: GitAccess;
|
|
10
11
|
readonly operations: OperationLog;
|
|
11
12
|
readonly processes: ProcessSupervisor;
|
|
12
13
|
readonly computer: ComputerService;
|
|
14
|
+
readonly agents: AgentRuntime;
|
|
13
15
|
readonly mcpTools: McpToolControl;
|
|
14
|
-
constructor(workspaces: WorkspaceService, git:
|
|
16
|
+
constructor(workspaces: WorkspaceService, git: GitAccess, operations: OperationLog, processes: ProcessSupervisor, computer: ComputerService, agents: AgentRuntime, mcpTools: McpToolControl);
|
|
15
17
|
processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
|
|
16
18
|
listProcesses(): import("./api-types.js").ProcessSummary[];
|
|
17
19
|
getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
|
|
20
|
+
getProcessSummary(processId: string): import("./api-types.js").ProcessSummary;
|
|
18
21
|
}
|
|
@@ -4,13 +4,15 @@ export class WebRuntime {
|
|
|
4
4
|
operations;
|
|
5
5
|
processes;
|
|
6
6
|
computer;
|
|
7
|
+
agents;
|
|
7
8
|
mcpTools;
|
|
8
|
-
constructor(workspaces, git, operations, processes, computer, mcpTools) {
|
|
9
|
+
constructor(workspaces, git, operations, processes, computer, agents, mcpTools) {
|
|
9
10
|
this.workspaces = workspaces;
|
|
10
11
|
this.git = git;
|
|
11
12
|
this.operations = operations;
|
|
12
13
|
this.processes = processes;
|
|
13
14
|
this.computer = computer;
|
|
15
|
+
this.agents = agents;
|
|
14
16
|
this.mcpTools = mcpTools;
|
|
15
17
|
}
|
|
16
18
|
processKill(processId, force = false) {
|
|
@@ -28,4 +30,7 @@ export class WebRuntime {
|
|
|
28
30
|
getProcess(processId) {
|
|
29
31
|
return this.processes.read(processId);
|
|
30
32
|
}
|
|
33
|
+
getProcessSummary(processId) {
|
|
34
|
+
return this.processes.summary(processId);
|
|
35
|
+
}
|
|
31
36
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { closedRead } from "#mcp/server/tool-support";
|
|
3
|
+
const workspaceEntrySchema = z.object({
|
|
4
|
+
root: z.string(),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
summary: z.string().nullable(),
|
|
7
|
+
});
|
|
8
|
+
const workspaceListSchema = z.object({
|
|
9
|
+
workspaces: z.array(workspaceEntrySchema),
|
|
10
|
+
});
|
|
3
11
|
const workspaceInfoSchema = z.object({
|
|
4
12
|
root: z.string(),
|
|
5
13
|
name: z.string(),
|
|
@@ -13,6 +21,14 @@ const workspaceInfoSchema = z.object({
|
|
|
13
21
|
})),
|
|
14
22
|
});
|
|
15
23
|
export function registerWorkspaceTools(mcp, workspaces) {
|
|
24
|
+
mcp.registerTool("workspace_list", {
|
|
25
|
+
title: "List workspaces",
|
|
26
|
+
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.",
|
|
27
|
+
inputSchema: z.object({}),
|
|
28
|
+
outputSchema: workspaceListSchema,
|
|
29
|
+
annotations: closedRead,
|
|
30
|
+
action: "list",
|
|
31
|
+
}, async () => ({ workspaces: await workspaces.list() }));
|
|
16
32
|
mcp.registerTool("workspace_info", {
|
|
17
33
|
title: "Workspace info",
|
|
18
34
|
description: "Read the complete workspace context for a project, including its summary, preset prompt, project instructions, and skill metadata.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkspaceFs } from "./workspace-fs.js";
|
|
2
|
-
import type { WorkspaceSkill } from "
|
|
2
|
+
import type { WorkspaceSkill } from "#app/workspace-access";
|
|
3
3
|
export declare function readSummary(fs: WorkspaceFs): Promise<string | null>;
|
|
4
4
|
export declare function readPresetPrompt(fs: WorkspaceFs): Promise<string | null>;
|
|
5
5
|
export declare function readInstructions(fs: WorkspaceFs): Promise<string | null>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { InternalPlugin } from "#plugins/types";
|
|
2
2
|
import { WorkspaceService } from "./workspace-service.js";
|
|
3
|
-
export declare const WorkspaceServiceToken: import("#
|
|
3
|
+
export declare const WorkspaceServiceToken: import("#app/service-registry").ServiceToken<WorkspaceService>;
|
|
4
4
|
export declare function createWorkspacePlugin(): InternalPlugin;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GitAccessToken } from "#app/git-access";
|
|
2
|
+
import { WorkspaceAccessToken } from "#app/workspace-access";
|
|
3
|
+
import { createServiceToken } from "#app/service-registry";
|
|
2
4
|
import { registerWorkspaceTools } from "./mcp.js";
|
|
3
5
|
import { WorkspaceService } from "./workspace-service.js";
|
|
4
6
|
export const WorkspaceServiceToken = createServiceToken("workspace");
|
|
@@ -7,8 +9,9 @@ export function createWorkspacePlugin() {
|
|
|
7
9
|
return {
|
|
8
10
|
id: "workspace",
|
|
9
11
|
async activate(context) {
|
|
10
|
-
service = await WorkspaceService.create(context.config.allowedRoots);
|
|
12
|
+
service = await WorkspaceService.create(context.config.allowedRoots, context.services.require(GitAccessToken));
|
|
11
13
|
context.services.provide(WorkspaceServiceToken, service);
|
|
14
|
+
context.services.provide(WorkspaceAccessToken, service);
|
|
12
15
|
},
|
|
13
16
|
registerMcp(mcp) {
|
|
14
17
|
if (!service)
|
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
export interface WorkspaceEntry {
|
|
2
|
-
root: string;
|
|
3
|
-
name: string;
|
|
4
|
-
summary: string | null;
|
|
5
|
-
}
|
|
6
|
-
export interface WorkspaceSkill {
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
path: string;
|
|
10
|
-
}
|
|
11
|
-
export interface WorkspaceInfo {
|
|
12
|
-
root: string;
|
|
13
|
-
name: string;
|
|
14
|
-
summary: string | null;
|
|
15
|
-
presetPrompt: string | null;
|
|
16
|
-
instructions: string | null;
|
|
17
|
-
skills: WorkspaceSkill[];
|
|
18
|
-
}
|
|
19
1
|
export interface WorkspaceFile {
|
|
20
2
|
path: string;
|
|
21
3
|
type: "file" | "directory" | "symlink";
|
|
22
4
|
size: number;
|
|
5
|
+
modifiedAt?: string;
|
|
23
6
|
}
|
|
@@ -3,12 +3,17 @@ export declare class WorkspaceFs {
|
|
|
3
3
|
readonly root: string;
|
|
4
4
|
private constructor();
|
|
5
5
|
static create(root: string): Promise<WorkspaceFs>;
|
|
6
|
+
fileInfo(relativePath: string): Promise<{
|
|
7
|
+
bytes: number;
|
|
8
|
+
modifiedAt: string;
|
|
9
|
+
}>;
|
|
6
10
|
read(relativePath: string, options?: {
|
|
7
11
|
maxBytes?: number;
|
|
8
12
|
}): Promise<{
|
|
9
13
|
content: string;
|
|
10
14
|
bytes: number;
|
|
11
15
|
truncated: boolean;
|
|
16
|
+
modifiedAt: string;
|
|
12
17
|
}>;
|
|
13
18
|
readBytes(relativePath: string, options?: {
|
|
14
19
|
maxBytes?: number;
|
|
@@ -16,13 +21,14 @@ export declare class WorkspaceFs {
|
|
|
16
21
|
data: Buffer;
|
|
17
22
|
bytes: number;
|
|
18
23
|
truncated: boolean;
|
|
24
|
+
modifiedAt: string;
|
|
19
25
|
}>;
|
|
20
26
|
write(relativePath: string, content: string): Promise<WorkspaceFile>;
|
|
27
|
+
writeBytes(relativePath: string, content: Uint8Array): Promise<WorkspaceFile>;
|
|
21
28
|
list(relativePath?: string, options?: {
|
|
22
29
|
recursive?: boolean;
|
|
23
30
|
maxEntries?: number;
|
|
31
|
+
excludeDirectoryNames?: readonly string[];
|
|
32
|
+
offset?: number;
|
|
24
33
|
}): Promise<WorkspaceFile[]>;
|
|
25
|
-
private lexical;
|
|
26
|
-
private resolveWritable;
|
|
27
|
-
private resolveReadable;
|
|
28
34
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lstat, mkdir, open, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
1
|
+
import { lstat, open, readdir, realpath, stat } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
4
|
+
import { normalizeRelativePath, resolveReadableWithin, resolveWritableWithin, writeBytesAtomicWithin, } from "#core/fs/safe-directory";
|
|
5
5
|
export class WorkspaceFs {
|
|
6
6
|
root;
|
|
7
7
|
constructor(root) {
|
|
@@ -16,42 +16,65 @@ export class WorkspaceFs {
|
|
|
16
16
|
throw new ChatRoomError("INVALID_INPUT", `Workspace root is not a directory: ${root}`);
|
|
17
17
|
return new WorkspaceFs(canonical);
|
|
18
18
|
}
|
|
19
|
+
async fileInfo(relativePath) {
|
|
20
|
+
const target = await resolveReadableWithin(this.root, relativePath);
|
|
21
|
+
const info = await stat(target);
|
|
22
|
+
if (!info.isFile())
|
|
23
|
+
throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
|
|
24
|
+
return { bytes: info.size, modifiedAt: info.mtime.toISOString() };
|
|
25
|
+
}
|
|
19
26
|
async read(relativePath, options = {}) {
|
|
20
27
|
const result = await this.readBytes(relativePath, options);
|
|
21
28
|
return {
|
|
22
29
|
content: result.data.toString("utf8"),
|
|
23
30
|
bytes: result.bytes,
|
|
24
31
|
truncated: result.truncated,
|
|
32
|
+
modifiedAt: result.modifiedAt,
|
|
25
33
|
};
|
|
26
34
|
}
|
|
27
35
|
async readBytes(relativePath, options = {}) {
|
|
28
|
-
const target = await this.
|
|
29
|
-
const info = await stat(target);
|
|
30
|
-
if (!info.isFile())
|
|
31
|
-
throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
|
|
36
|
+
const target = await resolveReadableWithin(this.root, relativePath);
|
|
32
37
|
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
39
|
+
const handle = await open(target, "r");
|
|
40
|
+
try {
|
|
41
|
+
const before = await handle.stat();
|
|
42
|
+
if (!before.isFile())
|
|
43
|
+
throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
|
|
44
|
+
const readSize = Math.min(before.size, maxBytes);
|
|
45
|
+
const buffer = Buffer.alloc(readSize);
|
|
46
|
+
let bytesRead = 0;
|
|
47
|
+
while (bytesRead < readSize) {
|
|
48
|
+
const result = await handle.read(buffer, bytesRead, readSize - bytesRead, bytesRead);
|
|
49
|
+
if (result.bytesRead === 0)
|
|
50
|
+
break;
|
|
51
|
+
bytesRead += result.bytesRead;
|
|
52
|
+
}
|
|
53
|
+
const after = await handle.stat();
|
|
54
|
+
const stable = before.size === after.size &&
|
|
55
|
+
before.mtimeMs === after.mtimeMs &&
|
|
56
|
+
before.ctimeMs === after.ctimeMs;
|
|
57
|
+
if (stable)
|
|
58
|
+
return {
|
|
59
|
+
data: buffer.subarray(0, bytesRead),
|
|
60
|
+
bytes: after.size,
|
|
61
|
+
truncated: after.size > maxBytes,
|
|
62
|
+
modifiedAt: after.mtime.toISOString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await handle.close();
|
|
67
|
+
}
|
|
46
68
|
}
|
|
69
|
+
throw new ChatRoomError("CONFLICT", `File changed while being read: ${relativePath}`);
|
|
47
70
|
}
|
|
48
71
|
async write(relativePath, content) {
|
|
49
72
|
if (typeof content !== "string")
|
|
50
73
|
throw new ChatRoomError("INVALID_INPUT", "File content must be a string");
|
|
51
|
-
const normalized =
|
|
74
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
52
75
|
if (normalized === ".")
|
|
53
76
|
throw new ChatRoomError("INVALID_INPUT", "File path is required");
|
|
54
|
-
const target = await this.
|
|
77
|
+
const target = await resolveWritableWithin(this.root, normalized);
|
|
55
78
|
let mode = 0o644;
|
|
56
79
|
try {
|
|
57
80
|
const info = await lstat(target);
|
|
@@ -65,34 +88,41 @@ export class WorkspaceFs {
|
|
|
65
88
|
if (error.code !== "ENOENT")
|
|
66
89
|
throw error;
|
|
67
90
|
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
await writeFile(temporary, content, {
|
|
71
|
-
encoding: "utf8",
|
|
72
|
-
flag: "wx",
|
|
73
|
-
mode,
|
|
74
|
-
});
|
|
75
|
-
await rename(temporary, target);
|
|
76
|
-
}
|
|
77
|
-
finally {
|
|
78
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
79
|
-
}
|
|
91
|
+
await writeBytesAtomicWithin(this.root, normalized, Buffer.from(content, "utf8"), mode);
|
|
80
92
|
return {
|
|
81
93
|
path: normalized,
|
|
82
94
|
type: "file",
|
|
83
95
|
size: Buffer.byteLength(content, "utf8"),
|
|
84
96
|
};
|
|
85
97
|
}
|
|
98
|
+
async writeBytes(relativePath, content) {
|
|
99
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
100
|
+
if (normalized === ".")
|
|
101
|
+
throw new ChatRoomError("INVALID_INPUT", "File path is required");
|
|
102
|
+
await writeBytesAtomicWithin(this.root, normalized, content);
|
|
103
|
+
return {
|
|
104
|
+
path: normalized,
|
|
105
|
+
type: "file",
|
|
106
|
+
size: content.byteLength,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
86
109
|
async list(relativePath = ".", options = {}) {
|
|
87
|
-
const start = await this.
|
|
110
|
+
const start = await resolveReadableWithin(this.root, relativePath);
|
|
88
111
|
if (!(await stat(start)).isDirectory())
|
|
89
112
|
throw new ChatRoomError("INVALID_INPUT", `Not a directory: ${relativePath}`);
|
|
90
113
|
const maxEntries = Math.min(options.maxEntries ?? 1000, 10000);
|
|
114
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
115
|
+
const excluded = new Set(options.excludeDirectoryNames ?? []);
|
|
91
116
|
const output = [];
|
|
117
|
+
let seen = 0;
|
|
92
118
|
const walk = async (directory) => {
|
|
93
|
-
|
|
119
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
120
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
121
|
+
for (const entry of entries) {
|
|
94
122
|
if (output.length >= maxEntries)
|
|
95
123
|
return;
|
|
124
|
+
if (entry.isDirectory() && excluded.has(entry.name))
|
|
125
|
+
continue;
|
|
96
126
|
const absolute = path.join(directory, entry.name);
|
|
97
127
|
const info = await lstat(absolute);
|
|
98
128
|
const type = info.isSymbolicLink()
|
|
@@ -100,11 +130,19 @@ export class WorkspaceFs {
|
|
|
100
130
|
: info.isDirectory()
|
|
101
131
|
? "directory"
|
|
102
132
|
: "file";
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
const relative = path
|
|
134
|
+
.relative(this.root, absolute)
|
|
135
|
+
.split(path.sep)
|
|
136
|
+
.join("/");
|
|
137
|
+
if (seen >= offset) {
|
|
138
|
+
output.push({
|
|
139
|
+
path: relative,
|
|
140
|
+
type,
|
|
141
|
+
size: info.size,
|
|
142
|
+
modifiedAt: info.mtime.toISOString(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
seen += 1;
|
|
108
146
|
if (options.recursive && entry.isDirectory() && !entry.isSymbolicLink())
|
|
109
147
|
await walk(absolute);
|
|
110
148
|
}
|
|
@@ -112,89 +150,4 @@ export class WorkspaceFs {
|
|
|
112
150
|
await walk(start);
|
|
113
151
|
return output;
|
|
114
152
|
}
|
|
115
|
-
lexical(relativePath) {
|
|
116
|
-
const normalized = normalizeRelative(relativePath);
|
|
117
|
-
const absolute = path.resolve(this.root, ...normalized.split("/"));
|
|
118
|
-
if (!inside(this.root, absolute))
|
|
119
|
-
throw new ChatRoomError("FORBIDDEN", "Path escapes workspace");
|
|
120
|
-
return absolute;
|
|
121
|
-
}
|
|
122
|
-
async resolveWritable(relativePath) {
|
|
123
|
-
const target = this.lexical(relativePath);
|
|
124
|
-
const relative = path.relative(this.root, target);
|
|
125
|
-
const parts = relative.split(path.sep).filter(Boolean);
|
|
126
|
-
let current = this.root;
|
|
127
|
-
for (let index = 0; index < parts.length - 1; index++) {
|
|
128
|
-
current = path.join(current, parts[index]);
|
|
129
|
-
let info;
|
|
130
|
-
try {
|
|
131
|
-
info = await lstat(current);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (error.code !== "ENOENT")
|
|
135
|
-
throw error;
|
|
136
|
-
try {
|
|
137
|
-
await mkdir(current, { recursive: false, mode: 0o700 });
|
|
138
|
-
}
|
|
139
|
-
catch (mkdirError) {
|
|
140
|
-
if (mkdirError.code !== "EEXIST")
|
|
141
|
-
throw mkdirError;
|
|
142
|
-
}
|
|
143
|
-
info = await lstat(current);
|
|
144
|
-
}
|
|
145
|
-
if (info.isSymbolicLink())
|
|
146
|
-
throw new ChatRoomError("FORBIDDEN", `Writes through symlinked directories are not allowed: ${relativePath}`);
|
|
147
|
-
if (!info.isDirectory())
|
|
148
|
-
throw new ChatRoomError("INVALID_INPUT", `Parent is not a directory: ${parts[index]}`);
|
|
149
|
-
const canonical = await realpath(current);
|
|
150
|
-
if (!inside(this.root, canonical))
|
|
151
|
-
throw new ChatRoomError("FORBIDDEN", "Write path escapes workspace");
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
const targetInfo = await lstat(target);
|
|
155
|
-
if (targetInfo.isSymbolicLink())
|
|
156
|
-
throw new ChatRoomError("FORBIDDEN", `Writes through symlinks are not allowed: ${relativePath}`);
|
|
157
|
-
if (!targetInfo.isFile())
|
|
158
|
-
throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
if (error instanceof ChatRoomError)
|
|
162
|
-
throw error;
|
|
163
|
-
if (error.code !== "ENOENT")
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
return target;
|
|
167
|
-
}
|
|
168
|
-
async resolveReadable(relativePath) {
|
|
169
|
-
const lexical = this.lexical(relativePath);
|
|
170
|
-
const canonical = await realpath(lexical).catch((error) => {
|
|
171
|
-
if (error.code === "ENOENT")
|
|
172
|
-
throw new ChatRoomError("NOT_FOUND", `Path does not exist: ${relativePath}`);
|
|
173
|
-
throw error;
|
|
174
|
-
});
|
|
175
|
-
if (!inside(this.root, canonical))
|
|
176
|
-
throw new ChatRoomError("FORBIDDEN", "Symlink escapes workspace");
|
|
177
|
-
return canonical;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function normalizeRelative(input) {
|
|
181
|
-
if (typeof input !== "string" || input.includes("\0"))
|
|
182
|
-
throw new ChatRoomError("INVALID_INPUT", "Path must be a valid string");
|
|
183
|
-
if (path.isAbsolute(input) ||
|
|
184
|
-
path.win32.isAbsolute(input) ||
|
|
185
|
-
/^\\\\/.test(input))
|
|
186
|
-
throw new ChatRoomError("FORBIDDEN", "Absolute paths are not allowed");
|
|
187
|
-
const parts = input
|
|
188
|
-
.split(/[\\/]+/)
|
|
189
|
-
.filter((part) => part !== "" && part !== ".");
|
|
190
|
-
if (parts.some((part) => part === ".."))
|
|
191
|
-
throw new ChatRoomError("FORBIDDEN", "Path traversal is not allowed");
|
|
192
|
-
return parts.join("/") || ".";
|
|
193
|
-
}
|
|
194
|
-
function inside(root, candidate) {
|
|
195
|
-
const relative = path.relative(root, candidate);
|
|
196
|
-
return (relative === "" ||
|
|
197
|
-
(!relative.startsWith(`..${path.sep}`) &&
|
|
198
|
-
relative !== ".." &&
|
|
199
|
-
!path.isAbsolute(relative)));
|
|
200
153
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class WorkspaceRootPolicy {
|
|
2
|
+
private readonly allowedRoots;
|
|
3
|
+
private constructor();
|
|
4
|
+
static create(allowedRoots: string[]): Promise<WorkspaceRootPolicy>;
|
|
5
|
+
roots(): string[];
|
|
6
|
+
isWorkspaceRoot(candidate: string): boolean;
|
|
7
|
+
resolveWorkspace(input: string): Promise<string>;
|
|
8
|
+
resolveAllowedRoot(input: string): Promise<string>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
5
|
+
export class WorkspaceRootPolicy {
|
|
6
|
+
allowedRoots;
|
|
7
|
+
constructor(allowedRoots) {
|
|
8
|
+
this.allowedRoots = allowedRoots;
|
|
9
|
+
}
|
|
10
|
+
static async create(allowedRoots) {
|
|
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;
|
|
18
|
+
}));
|
|
19
|
+
return new WorkspaceRootPolicy([...new Set(canonicalRoots)]);
|
|
20
|
+
}
|
|
21
|
+
roots() {
|
|
22
|
+
return [...this.allowedRoots];
|
|
23
|
+
}
|
|
24
|
+
isWorkspaceRoot(candidate) {
|
|
25
|
+
return this.allowedRoots.some((root) => path.dirname(candidate) === root);
|
|
26
|
+
}
|
|
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
|
+
async resolveAllowedRoot(input) {
|
|
40
|
+
if (typeof input !== "string" || !input.trim())
|
|
41
|
+
throw new ChatRoomError("INVALID_INPUT", "Allowed root is required");
|
|
42
|
+
const canonical = await realpath(expandHome(input)).catch(() => {
|
|
43
|
+
throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${input}`);
|
|
44
|
+
});
|
|
45
|
+
if (!this.allowedRoots.includes(canonical))
|
|
46
|
+
throw new ChatRoomError("FORBIDDEN", "Project can only be created directly under a configured allowed root");
|
|
47
|
+
return canonical;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function expandHome(input) {
|
|
51
|
+
if (input === "~")
|
|
52
|
+
return os.homedir();
|
|
53
|
+
if (input.startsWith("~/") || input.startsWith("~\\"))
|
|
54
|
+
return path.join(os.homedir(), input.slice(2));
|
|
55
|
+
return path.resolve(input);
|
|
56
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GitAccess } from "#app/git-access";
|
|
2
|
+
import type { WorkspaceAccess, WorkspaceEntry, WorkspaceInfo } from "#app/workspace-access";
|
|
2
3
|
import { WorkspaceFs } from "./workspace-fs.js";
|
|
3
|
-
export declare class WorkspaceService {
|
|
4
|
-
private readonly
|
|
4
|
+
export declare class WorkspaceService implements WorkspaceAccess {
|
|
5
|
+
private readonly rootsPolicy;
|
|
6
|
+
private readonly git;
|
|
5
7
|
private constructor();
|
|
6
|
-
static create(allowedRoots: string[]): Promise<WorkspaceService>;
|
|
8
|
+
static create(allowedRoots: string[], git: Pick<GitAccess, "projectFiles">): Promise<WorkspaceService>;
|
|
7
9
|
roots(): string[];
|
|
8
10
|
list(): Promise<WorkspaceEntry[]>;
|
|
9
11
|
resolve(input: string): Promise<string>;
|
|
10
12
|
createProject(parentInput: string, nameInput: string): Promise<WorkspaceEntry>;
|
|
13
|
+
projectFiles(input: string, maxFiles?: number): Promise<string[]>;
|
|
14
|
+
projectFilesResult(input: string, maxFiles?: number): Promise<{
|
|
15
|
+
paths: string[];
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
}>;
|
|
11
18
|
info(input: string): Promise<WorkspaceInfo>;
|
|
12
19
|
fs(input: string): Promise<WorkspaceFs>;
|
|
13
|
-
private isWorkspaceRoot;
|
|
14
|
-
private resolveAllowedRoot;
|
|
15
20
|
}
|