@chatroomcp/chatroom 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +5 -3
  2. package/dist/app/application.d.ts +0 -1
  3. package/dist/app/application.js +2 -0
  4. package/dist/app/event-bus.d.ts +0 -2
  5. package/dist/app/event-bus.js +0 -1
  6. package/dist/app/external-access-registry.d.ts +0 -1
  7. package/dist/app/external-access-registry.js +0 -1
  8. package/dist/app/lifecycle.d.ts +0 -1
  9. package/dist/auth/auth-service.js +0 -4
  10. package/dist/auth/ingress-policy.d.ts +0 -1
  11. package/dist/auth/ingress-policy.js +0 -1
  12. package/dist/cli/run-cli.js +0 -1
  13. package/dist/config/types.d.ts +0 -1
  14. package/dist/core/auth/repository.d.ts +0 -23
  15. package/dist/core/auth/repository.js +0 -6
  16. package/dist/core/errors/chatroom-error.d.ts +0 -15
  17. package/dist/core/errors/chatroom-error.js +0 -15
  18. package/dist/core/operations/bounded-value.d.ts +0 -1
  19. package/dist/core/operations/bounded-value.js +26 -10
  20. package/dist/core/operations/redactor.js +0 -1
  21. package/dist/core/operations/repository.d.ts +0 -1
  22. package/dist/core/operations/types.d.ts +0 -4
  23. package/dist/core/operations/types.js +0 -2
  24. package/dist/infrastructure/database/app-database.js +4 -97
  25. package/dist/infrastructure/database/migrations.d.ts +2 -0
  26. package/dist/infrastructure/database/migrations.js +44 -0
  27. package/dist/infrastructure/database/operation-repository.js +3 -9
  28. package/dist/infrastructure/database/schema.d.ts +2 -0
  29. package/dist/infrastructure/database/schema.js +85 -0
  30. package/dist/infrastructure/http/http-server.js +0 -1
  31. package/dist/mcp/server/create-mcp-server.js +3 -3
  32. package/dist/mcp/server/plugin-mcp-registrar.d.ts +0 -2
  33. package/dist/mcp/server/plugin-mcp-registrar.js +0 -4
  34. package/dist/mcp/server/tool-support.d.ts +0 -61
  35. package/dist/mcp/server/tool-support.js +0 -54
  36. package/dist/native/linux/chatroom-computer-helper +0 -0
  37. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +1 -1
  38. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  39. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  40. package/dist/operations/operation-log.d.ts +0 -3
  41. package/dist/operations/operation-log.js +0 -15
  42. package/dist/plugins/cloud/api-client.js +1 -0
  43. package/dist/plugins/cloud/controller.d.ts +2 -0
  44. package/dist/plugins/cloud/controller.js +28 -5
  45. package/dist/plugins/cloud/tunnel-client.js +0 -4
  46. package/dist/plugins/git/git-service.d.ts +33 -0
  47. package/dist/plugins/git/git-service.js +414 -0
  48. package/dist/plugins/git/plugin.d.ts +4 -0
  49. package/dist/plugins/git/plugin.js +12 -0
  50. package/dist/plugins/git/types.d.ts +32 -0
  51. package/dist/plugins/process/backend.d.ts +0 -1
  52. package/dist/plugins/process/head-tail-buffer.d.ts +0 -1
  53. package/dist/plugins/process/infrastructure/pipe-process-backend.js +0 -1
  54. package/dist/plugins/process/infrastructure/pty-process-backend.js +1 -4
  55. package/dist/plugins/process/process-supervisor.js +10 -23
  56. package/dist/plugins/process/types.d.ts +0 -5
  57. package/dist/plugins/types.d.ts +0 -1
  58. package/dist/plugins/web/api-types.d.ts +3 -7
  59. package/dist/plugins/web/http/api-router.d.ts +0 -1
  60. package/dist/plugins/web/http/api-router.js +2 -4
  61. package/dist/plugins/web/http/git-api-router.d.ts +3 -0
  62. package/dist/plugins/web/http/git-api-router.js +98 -0
  63. package/dist/plugins/web/http/workspace-api-router.js +51 -37
  64. package/dist/plugins/web/plugin.js +4 -6
  65. package/dist/plugins/web/runtime.d.ts +5 -27
  66. package/dist/plugins/web/runtime.js +3 -42
  67. package/dist/plugins/workspace/mcp.js +20 -135
  68. package/dist/plugins/workspace/metadata.d.ts +6 -0
  69. package/dist/plugins/workspace/metadata.js +115 -0
  70. package/dist/plugins/workspace/plugin.d.ts +2 -8
  71. package/dist/plugins/workspace/plugin.js +8 -20
  72. package/dist/plugins/workspace/types.d.ts +23 -0
  73. package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +4 -14
  74. package/dist/plugins/workspace/workspace-fs.js +200 -0
  75. package/dist/plugins/workspace/workspace-service.d.ts +11 -42
  76. package/dist/plugins/workspace/workspace-service.js +103 -302
  77. package/dist/presentation/http/http-utils.d.ts +0 -1
  78. package/dist/presentation/http/oauth-router.d.ts +0 -1
  79. package/dist/presentation/http/oauth-router.js +0 -1
  80. package/dist/web/assets/index-CEzdY5mz.js +27 -0
  81. package/dist/web/assets/index-D_mpjwCG.css +1 -0
  82. package/dist/web/index.html +2 -2
  83. package/package.json +4 -3
  84. package/dist/plugins/workspace/domain/filesystem.d.ts +0 -25
  85. package/dist/plugins/workspace/domain/git.d.ts +0 -7
  86. package/dist/plugins/workspace/domain/repository.d.ts +0 -16
  87. package/dist/plugins/workspace/domain/repository.js +0 -1
  88. package/dist/plugins/workspace/domain/review.d.ts +0 -28
  89. package/dist/plugins/workspace/domain/review.js +0 -1
  90. package/dist/plugins/workspace/domain/workspace.d.ts +0 -23
  91. package/dist/plugins/workspace/domain/workspace.js +0 -1
  92. package/dist/plugins/workspace/git/git-command-runner.d.ts +0 -6
  93. package/dist/plugins/workspace/git/git-command-runner.js +0 -15
  94. package/dist/plugins/workspace/git/git-service.d.ts +0 -14
  95. package/dist/plugins/workspace/git/git-service.js +0 -107
  96. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +0 -33
  97. package/dist/plugins/workspace/git/git-snapshot-service.js +0 -352
  98. package/dist/plugins/workspace/infrastructure/workspace-fs.js +0 -340
  99. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +0 -12
  100. package/dist/plugins/workspace/infrastructure/workspace-repository.js +0 -51
  101. package/dist/plugins/workspace/workspace-helpers.d.ts +0 -9
  102. package/dist/plugins/workspace/workspace-helpers.js +0 -67
  103. package/dist/plugins/workspace/worktree-review-service.d.ts +0 -16
  104. package/dist/plugins/workspace/worktree-review-service.js +0 -51
  105. package/dist/web/assets/index-B4vO7wVS.css +0 -1
  106. package/dist/web/assets/index-BU7LCgKB.js +0 -26
  107. /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
  108. /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
@@ -1,339 +1,140 @@
1
- /**
2
- * Owns persistent Workspace identity, allowed-root validation, checkout/worktree creation, and workspace capabilities.
3
- * Workspace lifetime is deliberately independent from MCP or HTTP connection lifetime.
4
- */
5
- import { existsSync, realpathSync } from "node:fs";
6
- import { mkdir, readdir, realpath, rm } from "node:fs/promises";
1
+ import { mkdir, readdir, realpath, stat } from "node:fs/promises";
2
+ import os from "node:os";
7
3
  import path from "node:path";
8
- import { randomUUID } from "node:crypto";
9
4
  import { ChatRoomError } from "../../core/errors/chatroom-error.js";
10
- import { WorkspaceFs } from "./infrastructure/workspace-fs.js";
11
- import { discoverInstructions, discoverSkills, exists, expandHome, inside, toRecord, } from "./workspace-helpers.js";
5
+ import { readInstructions, readPresetPrompt, readSkills, readSummary, } from "./metadata.js";
6
+ import { WorkspaceFs } from "./workspace-fs.js";
12
7
  export class WorkspaceService {
13
- repository;
14
- git;
15
- review;
16
8
  allowedRoots;
17
- dataDir;
18
- pending = new Map();
19
- filesystems = new Map();
20
- workspaces = new Map();
21
- constructor(repository, git, review, allowedRoots, dataDir) {
22
- this.repository = repository;
23
- this.git = git;
24
- this.review = review;
9
+ constructor(allowedRoots) {
25
10
  this.allowedRoots = allowedRoots;
26
- this.dataDir = dataDir;
27
11
  }
28
- static async create(repository, git, review, allowedRoots, dataDir) {
29
- // Canonicalize allowed roots once at startup so later comparisons use stable real paths.
30
- const canonicalRoots = [];
31
- for (const root of allowedRoots) {
32
- const expanded = expandHome(root);
33
- const canonical = await realpath(expanded).catch(() => {
12
+ static async create(allowedRoots) {
13
+ const canonicalRoots = await Promise.all(allowedRoots.map(async (root) => {
14
+ const canonical = await realpath(expandHome(root)).catch(() => {
34
15
  throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${root}`);
35
16
  });
36
- canonicalRoots.push(canonical);
37
- }
38
- await mkdir(path.join(dataDir, "worktrees"), {
39
- recursive: true,
40
- mode: 0o700,
41
- });
42
- const service = new WorkspaceService(repository, git, review, canonicalRoots, dataDir);
43
- await service.restorePersistedWorkspaces();
44
- await service.reconcileManagedWorktrees();
45
- return service;
46
- }
47
- async open(request) {
48
- const mode = request.mode ?? "checkout";
49
- const sourceRoot = await this.validateSourceRoot(request.path);
50
- if (mode === "worktree")
51
- return this.openLocked(sourceRoot, mode);
52
- const key = `checkout:${sourceRoot}`;
53
- // Coalesce concurrent opens of the same logical workspace instead of racing worktree creation or persistence.
54
- const existingPending = this.pending.get(key);
55
- if (existingPending)
56
- return existingPending;
57
- const operation = this.openLocked(sourceRoot, mode).finally(() => this.pending.delete(key));
58
- this.pending.set(key, operation);
59
- return operation;
60
- }
61
- get(workspaceId) {
62
- const workspace = this.workspaces.get(workspaceId);
63
- if (!workspace)
64
- throw new ChatRoomError("NOT_FOUND", `Unknown WorkspaceId: ${workspaceId}`);
65
- if (!this.isRecordValid(workspace)) {
66
- this.filesystems.delete(workspace.id);
67
- this.workspaces.delete(workspace.id);
68
- this.repository.remove(workspace.id);
69
- throw new ChatRoomError("NOT_FOUND", `Workspace no longer exists: ${workspaceId}`);
70
- }
71
- return workspace;
72
- }
73
- list() {
74
- const valid = [];
75
- for (const workspace of this.workspaces.values()) {
76
- if (this.isRecordValid(workspace)) {
77
- valid.push(workspace);
78
- continue;
79
- }
80
- this.filesystems.delete(workspace.id);
81
- this.workspaces.delete(workspace.id);
82
- this.repository.remove(workspace.id);
83
- }
84
- return valid.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
85
- }
86
- async discoverGitWorkspaces() {
87
- for (const root of this.allowedRoots) {
88
- await this.discoverGitWorkspace(root);
17
+ if (!(await stat(canonical)).isDirectory())
18
+ throw new ChatRoomError("INVALID_INPUT", `Allowed root is not a directory: ${root}`);
19
+ return canonical;
20
+ }));
21
+ return new WorkspaceService([...new Set(canonicalRoots)]);
22
+ }
23
+ roots() {
24
+ return [...this.allowedRoots];
25
+ }
26
+ async list() {
27
+ const roots = new Set();
28
+ for (const allowedRoot of this.allowedRoots) {
89
29
  let entries;
90
30
  try {
91
- entries = await readdir(root, { withFileTypes: true });
31
+ entries = await readdir(allowedRoot, { withFileTypes: true });
92
32
  }
93
33
  catch {
94
34
  continue;
95
35
  }
96
36
  for (const entry of entries) {
97
- if (!entry.isDirectory() || entry.isSymbolicLink())
37
+ if (!entry.isDirectory() ||
38
+ entry.isSymbolicLink() ||
39
+ entry.name.startsWith("."))
98
40
  continue;
99
- await this.discoverGitWorkspace(path.join(root, entry.name));
41
+ const candidate = await realpath(path.join(allowedRoot, entry.name)).catch(() => null);
42
+ if (candidate && this.isWorkspaceRoot(candidate))
43
+ roots.add(candidate);
100
44
  }
101
45
  }
102
- return this.list();
103
- }
104
- async discoverGitWorkspace(candidate) {
105
- if (!(await exists(path.join(candidate, ".git"))))
106
- return;
107
- const existing = [...this.workspaces.values()].find((workspace) => workspace.root === candidate && workspace.mode === "checkout");
108
- if (existing && this.isRecordValid(existing))
109
- return;
110
- if (existing) {
111
- this.workspaces.delete(existing.id);
112
- this.repository.remove(existing.id);
113
- }
114
- await this.createWorkspace(candidate, candidate, "checkout");
115
- }
116
- markUsed(workspaceId) {
117
- const workspace = this.get(workspaceId);
118
- workspace.lastUsedAt = new Date().toISOString();
119
- this.repository.upsert(toRecord(workspace));
120
- return workspace;
121
- }
122
- async fs(workspaceId) {
123
- const workspace = this.markUsed(workspaceId);
124
- const existing = this.filesystems.get(workspaceId);
125
- if (existing)
126
- return existing;
127
- const filesystem = await WorkspaceFs.create(workspace.root);
128
- this.filesystems.set(workspaceId, filesystem);
129
- return filesystem;
130
- }
131
- async refresh(workspaceId) {
132
- const workspace = this.get(workspaceId);
133
- const fs = await this.fs(workspaceId);
134
- const gitInfo = await this.git.info(workspace.root);
135
- workspace.instructions = await discoverInstructions(fs);
136
- workspace.skills = await discoverSkills(fs);
137
- workspace.capabilities = {
138
- filesystem: "read-write",
139
- git: gitInfo.isRepository,
140
- skills: workspace.skills.length > 0,
141
- };
142
- return workspace;
143
- }
144
- async remove(workspaceId, force = false) {
145
- const workspace = this.get(workspaceId);
146
- if (workspace.mode === "checkout") {
147
- this.filesystems.delete(workspace.id);
148
- this.workspaces.delete(workspace.id);
149
- this.repository.remove(workspace.id);
150
- return workspace;
151
- }
152
- const gitInfo = await this.git.info(workspace.root);
153
- if (gitInfo.dirty && !force)
154
- throw new ChatRoomError("CONFLICT", "Worktree contains uncommitted changes");
155
- await this.git.removeWorktree(workspace.sourceRoot, workspace.root, force);
156
- await this.git.pruneWorktrees(workspace.sourceRoot).catch(() => undefined);
157
- this.filesystems.delete(workspace.id);
158
- this.workspaces.delete(workspace.id);
159
- this.repository.remove(workspace.id);
160
- await this.removeManagedContainer(workspace.root);
161
- return workspace;
162
- }
163
- async previewWorktreeApply(workspaceId) {
164
- return this.review.preview(this.get(workspaceId));
165
- }
166
- async previewWorktreeFileDiff(workspaceId, filePath) {
167
- return this.review.fileDiff(this.get(workspaceId), filePath);
168
- }
169
- async applyWorktree(workspaceId, paths) {
170
- const workspace = this.get(workspaceId);
171
- const result = await this.review.apply(workspace, paths);
172
- this.markUsed(workspace.id);
173
- return { workspaceId: workspace.id, ...result };
46
+ return Promise.all([...roots]
47
+ .sort((a, b) => a.localeCompare(b))
48
+ .map(async (root) => {
49
+ const fs = await WorkspaceFs.create(root);
50
+ return {
51
+ root,
52
+ name: path.basename(root),
53
+ summary: await readSummary(fs),
54
+ };
55
+ }));
56
+ }
57
+ async resolve(input) {
58
+ if (typeof input !== "string" || !input.trim())
59
+ throw new ChatRoomError("INVALID_INPUT", "Workspace root is required");
60
+ const canonical = await realpath(expandHome(input)).catch(() => {
61
+ throw new ChatRoomError("NOT_FOUND", `Workspace root does not exist: ${input}`);
62
+ });
63
+ if (!(await stat(canonical)).isDirectory())
64
+ throw new ChatRoomError("INVALID_INPUT", `Workspace root is not a directory: ${input}`);
65
+ if (!this.isWorkspaceRoot(canonical))
66
+ throw new ChatRoomError("FORBIDDEN", "Workspace must be a direct child of a configured allowed root", { root: canonical });
67
+ return canonical;
174
68
  }
175
- async openLocked(sourceRoot, mode) {
176
- if (mode === "checkout") {
177
- const existing = [...this.workspaces.values()].find((workspace) => workspace.root === sourceRoot && workspace.mode === "checkout");
178
- if (existing) {
179
- this.filesystems.set(existing.id, await WorkspaceFs.create(existing.root));
180
- return this.refresh(existing.id);
181
- }
182
- return this.createWorkspace(sourceRoot, sourceRoot, mode);
183
- }
184
- const uuid = randomUUID();
185
- const id = `ws_${uuid}`;
186
- const container = path.join(this.dataDir, "worktrees", uuid.replaceAll("-", "").slice(0, 8));
187
- const target = path.join(container, path.basename(sourceRoot) || "workspace");
188
- await mkdir(container, { recursive: true, mode: 0o700 });
189
- let worktreeCreated = false;
69
+ async createProject(parentInput, nameInput) {
70
+ const parent = await this.resolveAllowedRoot(parentInput);
71
+ const name = validateProjectName(nameInput);
72
+ const target = path.join(parent, name);
190
73
  try {
191
- await this.git.createWorktree(sourceRoot, target);
192
- worktreeCreated = true;
193
- return await this.createWorkspace(target, sourceRoot, mode, id);
74
+ await mkdir(target, { recursive: false });
194
75
  }
195
76
  catch (error) {
196
- this.filesystems.delete(id);
197
- this.workspaces.delete(id);
198
- this.repository.remove(id);
199
- if (worktreeCreated)
200
- await this.git
201
- .removeWorktree(sourceRoot, target, true)
202
- .catch(() => undefined);
203
- await this.git.pruneWorktrees(sourceRoot).catch(() => undefined);
204
- await rm(container, { recursive: true, force: true }).catch(() => undefined);
77
+ if (error.code === "EEXIST")
78
+ throw new ChatRoomError("CONFLICT", `Workspace already exists: ${target}`);
205
79
  throw error;
206
80
  }
81
+ await mkdir(path.join(target, ".chatroom"));
82
+ const root = await realpath(target);
83
+ return { root, name: path.basename(root), summary: null };
207
84
  }
208
- async createWorkspace(root, sourceRoot, mode, id = `ws_${randomUUID()}`) {
85
+ async info(input) {
86
+ const root = await this.resolve(input);
209
87
  const fs = await WorkspaceFs.create(root);
210
- this.filesystems.set(id, fs);
211
- const gitInfo = await this.git.info(root);
212
- const now = new Date().toISOString();
213
- const skills = await discoverSkills(fs);
214
- const workspace = {
215
- id,
88
+ const [summary, presetPrompt, instructions, skills] = await Promise.all([
89
+ readSummary(fs),
90
+ readPresetPrompt(fs),
91
+ readInstructions(fs),
92
+ readSkills(fs),
93
+ ]);
94
+ return {
216
95
  root,
217
- sourceRoot,
218
- mode,
219
- createdAt: now,
220
- lastUsedAt: now,
221
- instructions: await discoverInstructions(fs),
96
+ name: path.basename(root),
97
+ summary,
98
+ presetPrompt,
99
+ instructions,
222
100
  skills,
223
- capabilities: {
224
- filesystem: "read-write",
225
- git: gitInfo.isRepository,
226
- skills: skills.length > 0,
227
- },
228
101
  };
229
- this.workspaces.set(workspace.id, workspace);
230
- this.repository.upsert(toRecord(workspace));
231
- return workspace;
232
102
  }
233
- async restorePersistedWorkspaces() {
234
- for (const record of this.repository.list()) {
235
- if (!this.isRecordValid(record)) {
236
- this.repository.remove(record.id);
237
- if (record.mode === "worktree")
238
- await this.removeManagedContainer(record.root);
239
- continue;
240
- }
241
- try {
242
- const fs = await WorkspaceFs.create(record.root);
243
- const gitInfo = await this.git.info(record.root);
244
- const skills = await discoverSkills(fs);
245
- const workspace = {
246
- ...record,
247
- instructions: await discoverInstructions(fs),
248
- skills,
249
- capabilities: {
250
- filesystem: "read-write",
251
- git: gitInfo.isRepository,
252
- skills: skills.length > 0,
253
- },
254
- };
255
- this.filesystems.set(record.id, fs);
256
- this.workspaces.set(record.id, workspace);
257
- }
258
- catch {
259
- this.repository.remove(record.id);
260
- if (record.mode === "worktree")
261
- await this.removeManagedContainer(record.root);
262
- }
263
- }
103
+ async fs(input) {
104
+ return WorkspaceFs.create(await this.resolve(input));
264
105
  }
265
- isRecordValid(record) {
266
- if (!existsSync(record.root) || !existsSync(record.sourceRoot))
267
- return false;
268
- try {
269
- const sourceRoot = realpathSync(record.sourceRoot);
270
- if (!this.allowedRoots.some((root) => inside(root, sourceRoot)))
271
- return false;
272
- if (record.mode === "checkout")
273
- return realpathSync(record.root) === sourceRoot;
274
- const managedRoot = realpathSync(path.join(this.dataDir, "worktrees"));
275
- return inside(managedRoot, realpathSync(record.root));
276
- }
277
- catch {
278
- return false;
279
- }
106
+ isWorkspaceRoot(candidate) {
107
+ return this.allowedRoots.some((root) => path.dirname(candidate) === root);
280
108
  }
281
- async reconcileManagedWorktrees() {
282
- const pruned = new Set();
283
- for (const workspace of [...this.workspaces.values()]) {
284
- if (workspace.mode !== "worktree")
285
- continue;
286
- if (await exists(workspace.sourceRoot)) {
287
- if (!pruned.has(workspace.sourceRoot)) {
288
- await this.git
289
- .pruneWorktrees(workspace.sourceRoot)
290
- .catch(() => undefined);
291
- pruned.add(workspace.sourceRoot);
292
- }
293
- }
294
- if (await exists(workspace.root))
295
- continue;
296
- this.filesystems.delete(workspace.id);
297
- this.workspaces.delete(workspace.id);
298
- this.repository.remove(workspace.id);
299
- await this.removeManagedContainer(workspace.root);
300
- }
301
- const managedRoot = path.join(this.dataDir, "worktrees");
302
- const knownContainers = new Set([...this.workspaces.values()]
303
- .filter((workspace) => workspace.mode === "worktree")
304
- .map((workspace) => path.resolve(path.dirname(workspace.root))));
305
- for (const entry of await readdir(managedRoot, { withFileTypes: true })) {
306
- const container = path.resolve(managedRoot, entry.name);
307
- if (knownContainers.has(container))
308
- continue;
309
- if (entry.isSymbolicLink()) {
310
- await rm(container, { force: true }).catch(() => undefined);
311
- continue;
312
- }
313
- if (entry.isDirectory())
314
- await rm(container, { recursive: true, force: true });
315
- }
316
- }
317
- async removeManagedContainer(worktreeRoot) {
318
- const managedRoot = path.join(this.dataDir, "worktrees");
319
- if (!inside(managedRoot, worktreeRoot))
320
- return;
321
- const container = path.dirname(worktreeRoot);
322
- if (container === managedRoot)
323
- return;
324
- await rm(container, { recursive: true, force: true }).catch(() => undefined);
325
- }
326
- async validateSourceRoot(input) {
327
- if (!input || typeof input !== "string")
328
- throw new ChatRoomError("INVALID_INPUT", "Workspace path is required");
109
+ async resolveAllowedRoot(input) {
110
+ if (typeof input !== "string" || !input.trim())
111
+ throw new ChatRoomError("INVALID_INPUT", "Allowed root is required");
329
112
  const canonical = await realpath(expandHome(input)).catch(() => {
330
- throw new ChatRoomError("NOT_FOUND", `Workspace path does not exist: ${input}`);
113
+ throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${input}`);
331
114
  });
332
- if (!this.allowedRoots.some((root) => inside(root, canonical))) {
333
- throw new ChatRoomError("FORBIDDEN", "Workspace is outside configured allowedRoots", {
334
- path: canonical,
335
- });
336
- }
115
+ if (!this.allowedRoots.includes(canonical))
116
+ throw new ChatRoomError("FORBIDDEN", "Project can only be created directly under a configured allowed root");
337
117
  return canonical;
338
118
  }
339
119
  }
120
+ function validateProjectName(input) {
121
+ if (typeof input !== "string")
122
+ throw new ChatRoomError("INVALID_INPUT", "Project name is required");
123
+ const name = input.trim();
124
+ if (!name ||
125
+ name === "." ||
126
+ name === ".." ||
127
+ name.startsWith(".") ||
128
+ name.includes("/") ||
129
+ name.includes("\\") ||
130
+ name.includes("\0"))
131
+ throw new ChatRoomError("INVALID_INPUT", "Invalid project name");
132
+ return name;
133
+ }
134
+ function expandHome(input) {
135
+ if (input === "~")
136
+ return os.homedir();
137
+ if (input.startsWith("~/") || input.startsWith("~\\"))
138
+ return path.join(os.homedir(), input.slice(2));
139
+ return path.resolve(input);
140
+ }
@@ -1,4 +1,3 @@
1
- /** Shared Express helpers for async error propagation and typed ChatRoom error responses. */
2
1
  import type { Request, Response, NextFunction, RequestHandler } from "express";
3
2
  export declare function asyncRoute(handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): RequestHandler;
4
3
  export declare function errorMiddleware(error: unknown, _req: Request, res: Response, _next: NextFunction): void;
@@ -1,4 +1,3 @@
1
- /** OAuth and owner-session HTTP routes backed by AuthService rather than protocol-specific persistence logic. */
2
1
  import { Router } from "express";
3
2
  import type { AuthService } from "../../auth/auth-service.js";
4
3
  import type { IngressPolicy } from "../../auth/ingress-policy.js";
@@ -1,4 +1,3 @@
1
- /** OAuth and owner-session HTTP routes backed by AuthService rather than protocol-specific persistence logic. */
2
1
  import { Router } from "express";
3
2
  import { asChatRoomError, ChatRoomError, } from "../../core/errors/chatroom-error.js";
4
3
  import { escapeHtml, requireString } from "./http-utils.js";