@chatroomcp/chatroom 0.1.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 (159) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +50 -0
  3. package/assets/chatroom.svg +15 -0
  4. package/chatroom.config.example.json +24 -0
  5. package/dist/app/application.d.ts +18 -0
  6. package/dist/app/application.js +63 -0
  7. package/dist/app/event-bus.d.ts +23 -0
  8. package/dist/app/event-bus.js +12 -0
  9. package/dist/app/external-access-registry.d.ts +18 -0
  10. package/dist/app/external-access-registry.js +48 -0
  11. package/dist/app/lifecycle.d.ts +12 -0
  12. package/dist/app/lifecycle.js +41 -0
  13. package/dist/auth/auth-service.d.ts +88 -0
  14. package/dist/auth/auth-service.js +222 -0
  15. package/dist/auth/ingress-policy.d.ts +21 -0
  16. package/dist/auth/ingress-policy.js +57 -0
  17. package/dist/auth/passkey-service.d.ts +39 -0
  18. package/dist/auth/passkey-service.js +145 -0
  19. package/dist/cli/index.d.ts +2 -0
  20. package/dist/cli/index.js +10 -0
  21. package/dist/cli/run-cli.d.ts +1 -0
  22. package/dist/cli/run-cli.js +42 -0
  23. package/dist/config/load-config.d.ts +7 -0
  24. package/dist/config/load-config.js +182 -0
  25. package/dist/config/platform-paths.d.ts +6 -0
  26. package/dist/config/platform-paths.js +34 -0
  27. package/dist/config/types.d.ts +25 -0
  28. package/dist/config/types.js +1 -0
  29. package/dist/core/auth/passkey-repository.d.ts +18 -0
  30. package/dist/core/auth/passkey-repository.js +1 -0
  31. package/dist/core/auth/repository.d.ts +59 -0
  32. package/dist/core/auth/repository.js +7 -0
  33. package/dist/core/auth/session-repository.d.ts +6 -0
  34. package/dist/core/auth/session-repository.js +1 -0
  35. package/dist/core/errors/chatroom-error.d.ts +23 -0
  36. package/dist/core/errors/chatroom-error.js +46 -0
  37. package/dist/core/operations/bounded-value.d.ts +6 -0
  38. package/dist/core/operations/bounded-value.js +41 -0
  39. package/dist/core/operations/redactor.d.ts +4 -0
  40. package/dist/core/operations/redactor.js +65 -0
  41. package/dist/core/operations/repository.d.ts +19 -0
  42. package/dist/core/operations/repository.js +1 -0
  43. package/dist/core/operations/types.d.ts +31 -0
  44. package/dist/core/operations/types.js +9 -0
  45. package/dist/core/runtime/child-environment.d.ts +1 -0
  46. package/dist/core/runtime/child-environment.js +39 -0
  47. package/dist/core/runtime/command-runner.d.ts +15 -0
  48. package/dist/core/runtime/command-runner.js +39 -0
  49. package/dist/core/runtime/identity.d.ts +2 -0
  50. package/dist/core/runtime/identity.js +9 -0
  51. package/dist/infrastructure/database/app-database.d.ts +6 -0
  52. package/dist/infrastructure/database/app-database.js +104 -0
  53. package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
  54. package/dist/infrastructure/database/oauth-repository.js +140 -0
  55. package/dist/infrastructure/database/operation-repository.d.ts +16 -0
  56. package/dist/infrastructure/database/operation-repository.js +129 -0
  57. package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
  58. package/dist/infrastructure/database/passkey-repository.js +57 -0
  59. package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
  60. package/dist/infrastructure/database/web-session-repository.js +26 -0
  61. package/dist/infrastructure/http/http-server.d.ts +26 -0
  62. package/dist/infrastructure/http/http-server.js +166 -0
  63. package/dist/mcp/server/create-mcp-server.d.ts +3 -0
  64. package/dist/mcp/server/create-mcp-server.js +16 -0
  65. package/dist/mcp/server/tool-support.d.ts +105 -0
  66. package/dist/mcp/server/tool-support.js +124 -0
  67. package/dist/operations/operation-log.d.ts +23 -0
  68. package/dist/operations/operation-log.js +103 -0
  69. package/dist/plugins/cloud/api-client.d.ts +56 -0
  70. package/dist/plugins/cloud/api-client.js +122 -0
  71. package/dist/plugins/cloud/controller.d.ts +48 -0
  72. package/dist/plugins/cloud/controller.js +282 -0
  73. package/dist/plugins/cloud/device-proof.d.ts +9 -0
  74. package/dist/plugins/cloud/device-proof.js +17 -0
  75. package/dist/plugins/cloud/plugin.d.ts +4 -0
  76. package/dist/plugins/cloud/plugin.js +17 -0
  77. package/dist/plugins/cloud/state-store.d.ts +7 -0
  78. package/dist/plugins/cloud/state-store.js +74 -0
  79. package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
  80. package/dist/plugins/cloud/tunnel-client.js +293 -0
  81. package/dist/plugins/cloud/types.d.ts +70 -0
  82. package/dist/plugins/cloud/types.js +32 -0
  83. package/dist/plugins/plugin-manager.d.ts +11 -0
  84. package/dist/plugins/plugin-manager.js +24 -0
  85. package/dist/plugins/process/backend.d.ts +19 -0
  86. package/dist/plugins/process/backend.js +1 -0
  87. package/dist/plugins/process/head-tail-buffer.d.ts +18 -0
  88. package/dist/plugins/process/head-tail-buffer.js +49 -0
  89. package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
  90. package/dist/plugins/process/infrastructure/pipe-process-backend.js +61 -0
  91. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
  92. package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
  93. package/dist/plugins/process/mcp.d.ts +4 -0
  94. package/dist/plugins/process/mcp.js +69 -0
  95. package/dist/plugins/process/plugin.d.ts +4 -0
  96. package/dist/plugins/process/plugin.js +28 -0
  97. package/dist/plugins/process/process-supervisor.d.ts +36 -0
  98. package/dist/plugins/process/process-supervisor.js +232 -0
  99. package/dist/plugins/process/types.d.ts +34 -0
  100. package/dist/plugins/process/types.js +1 -0
  101. package/dist/plugins/types.d.ts +32 -0
  102. package/dist/plugins/types.js +17 -0
  103. package/dist/plugins/web/api-types.d.ts +10 -0
  104. package/dist/plugins/web/api-types.js +1 -0
  105. package/dist/plugins/web/http/api-router.d.ts +13 -0
  106. package/dist/plugins/web/http/api-router.js +146 -0
  107. package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
  108. package/dist/plugins/web/http/cloud-api-router.js +41 -0
  109. package/dist/plugins/web/http/process-api-router.d.ts +3 -0
  110. package/dist/plugins/web/http/process-api-router.js +16 -0
  111. package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
  112. package/dist/plugins/web/http/workspace-api-router.js +82 -0
  113. package/dist/plugins/web/plugin.d.ts +8 -0
  114. package/dist/plugins/web/plugin.js +17 -0
  115. package/dist/plugins/web/runtime.d.ts +36 -0
  116. package/dist/plugins/web/runtime.js +66 -0
  117. package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
  118. package/dist/plugins/workspace/domain/filesystem.js +1 -0
  119. package/dist/plugins/workspace/domain/git.d.ts +7 -0
  120. package/dist/plugins/workspace/domain/git.js +1 -0
  121. package/dist/plugins/workspace/domain/repository.d.ts +16 -0
  122. package/dist/plugins/workspace/domain/repository.js +1 -0
  123. package/dist/plugins/workspace/domain/review.d.ts +28 -0
  124. package/dist/plugins/workspace/domain/review.js +1 -0
  125. package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
  126. package/dist/plugins/workspace/domain/workspace.js +1 -0
  127. package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
  128. package/dist/plugins/workspace/git/git-command-runner.js +15 -0
  129. package/dist/plugins/workspace/git/git-service.d.ts +14 -0
  130. package/dist/plugins/workspace/git/git-service.js +107 -0
  131. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
  132. package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
  133. package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
  134. package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
  135. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
  136. package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
  137. package/dist/plugins/workspace/mcp.d.ts +4 -0
  138. package/dist/plugins/workspace/mcp.js +168 -0
  139. package/dist/plugins/workspace/plugin.d.ts +10 -0
  140. package/dist/plugins/workspace/plugin.js +37 -0
  141. package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
  142. package/dist/plugins/workspace/workspace-helpers.js +67 -0
  143. package/dist/plugins/workspace/workspace-service.d.ts +46 -0
  144. package/dist/plugins/workspace/workspace-service.js +339 -0
  145. package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
  146. package/dist/plugins/workspace/worktree-review-service.js +51 -0
  147. package/dist/presentation/http/http-utils.d.ts +7 -0
  148. package/dist/presentation/http/http-utils.js +47 -0
  149. package/dist/presentation/http/oauth-router.d.ts +5 -0
  150. package/dist/presentation/http/oauth-router.js +163 -0
  151. package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
  152. package/dist/web/assets/index-BF_mWl1D.js +26 -0
  153. package/dist/web/assets/index-JPZut9tc.css +1 -0
  154. package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  155. package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  156. package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  157. package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  158. package/dist/web/index.html +19 -0
  159. package/package.json +77 -0
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { closedRead, destructiveLocalMutation, mcpTool, openWorldMutation, processSnapshotSchema, } from "../../mcp/server/tool-support.js";
3
+ export function registerProcessTools(server, processes, operations) {
4
+ server.registerTool("process_start", {
5
+ title: "Start process",
6
+ description: "Start a supervised pipe or PTY process. Returns a ProcessId immediately.",
7
+ inputSchema: z.object({
8
+ command: z.string().min(1),
9
+ args: z.array(z.string()).default([]),
10
+ cwd: z.string().default("."),
11
+ env: z.record(z.string(), z.string()).optional(),
12
+ pty: z.boolean().default(false),
13
+ timeoutMs: z.number().int().positive().optional(),
14
+ }),
15
+ outputSchema: processSnapshotSchema,
16
+ annotations: openWorldMutation,
17
+ }, mcpTool((input) => processes.start({
18
+ cwd: input.cwd,
19
+ command: input.command,
20
+ args: input.args,
21
+ ...(input.env ? { env: input.env } : {}),
22
+ pty: input.pty,
23
+ ...(input.timeoutMs === undefined
24
+ ? {}
25
+ : { timeoutMs: input.timeoutMs }),
26
+ }, { source: "mcp" })));
27
+ server.registerTool("process_read", {
28
+ title: "Read process",
29
+ description: "Read bounded stdout/stderr and current process state.",
30
+ inputSchema: z.object({ processId: z.string() }),
31
+ outputSchema: processSnapshotSchema,
32
+ annotations: closedRead,
33
+ }, mcpTool((input) => operations.run({
34
+ pluginId: "process",
35
+ source: "mcp",
36
+ action: "read",
37
+ processId: input.processId,
38
+ input,
39
+ }, () => Promise.resolve(processes.read(input.processId)))));
40
+ server.registerTool("process_write", {
41
+ title: "Write process stdin",
42
+ description: "Write to stdin of a running process.",
43
+ inputSchema: z.object({ processId: z.string(), data: z.string() }),
44
+ outputSchema: processSnapshotSchema,
45
+ annotations: openWorldMutation,
46
+ }, mcpTool((input) => operations.run({
47
+ pluginId: "process",
48
+ source: "mcp",
49
+ action: "write",
50
+ processId: input.processId,
51
+ input,
52
+ }, () => Promise.resolve(processes.write(input.processId, input.data)))));
53
+ server.registerTool("process_kill", {
54
+ title: "Stop process",
55
+ description: "Terminate or force-kill a supervised process.",
56
+ inputSchema: z.object({
57
+ processId: z.string(),
58
+ force: z.boolean().default(false),
59
+ }),
60
+ outputSchema: processSnapshotSchema,
61
+ annotations: destructiveLocalMutation,
62
+ }, mcpTool((input) => operations.run({
63
+ pluginId: "process",
64
+ source: "mcp",
65
+ action: input.force ? "kill" : "terminate",
66
+ processId: input.processId,
67
+ input,
68
+ }, () => Promise.resolve(processes.kill(input.processId, input.force)))));
69
+ }
@@ -0,0 +1,4 @@
1
+ import type { InternalPlugin } from "../types.js";
2
+ import { ProcessSupervisor } from "./process-supervisor.js";
3
+ export declare const ProcessService: import("../types.js").ServiceToken<ProcessSupervisor>;
4
+ export declare function createProcessPlugin(): InternalPlugin;
@@ -0,0 +1,28 @@
1
+ import { createServiceToken } from "../types.js";
2
+ import { PipeProcessBackend } from "./infrastructure/pipe-process-backend.js";
3
+ import { PtyProcessBackend } from "./infrastructure/pty-process-backend.js";
4
+ import { registerProcessTools } from "./mcp.js";
5
+ import { ProcessSupervisor } from "./process-supervisor.js";
6
+ export const ProcessService = createServiceToken("process");
7
+ export function createProcessPlugin() {
8
+ let service = null;
9
+ let operations = null;
10
+ return {
11
+ id: "process",
12
+ activate(context) {
13
+ service = new ProcessSupervisor({ pipe: new PipeProcessBackend(), pty: new PtyProcessBackend() }, context.operations, context.events, context.config.process.maxOutputBytes, context.config.process.defaultTimeoutMs, context.config.process.maxCompletedProcesses);
14
+ operations = context.operations;
15
+ context.services.provide(ProcessService, service);
16
+ },
17
+ registerMcp(server) {
18
+ if (!service || !operations)
19
+ throw new Error("Process plugin is not active");
20
+ registerProcessTools(server, service, operations);
21
+ },
22
+ async deactivate() {
23
+ await service?.shutdown();
24
+ service = null;
25
+ operations = null;
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,36 @@
1
+ import type { ProcessId, ProcessSnapshot, ProcessStartRequest } from "./types.js";
2
+ import type { RuntimeEventBus } from "../../app/event-bus.js";
3
+ import type { OperationLog } from "../../operations/operation-log.js";
4
+ import type { OperationSource } from "../../core/operations/types.js";
5
+ import type { ProcessBackend } from "./backend.js";
6
+ interface ProcessOperationContext {
7
+ source: OperationSource;
8
+ action?: string;
9
+ }
10
+ export declare class ProcessSupervisor {
11
+ private readonly backends;
12
+ private readonly operations;
13
+ private readonly eventBus;
14
+ private readonly maxOutputBytes;
15
+ private readonly defaultTimeoutMs;
16
+ private readonly maxCompletedProcesses;
17
+ private readonly processes;
18
+ private shuttingDown;
19
+ constructor(backends: {
20
+ pipe: ProcessBackend;
21
+ pty: ProcessBackend;
22
+ }, operations: OperationLog, eventBus: RuntimeEventBus, maxOutputBytes: number, defaultTimeoutMs: number, maxCompletedProcesses: number);
23
+ start(request: ProcessStartRequest, operationContext: ProcessOperationContext): Promise<ProcessSnapshot>;
24
+ read(processId: ProcessId): ProcessSnapshot;
25
+ list(): ProcessSnapshot[];
26
+ write(processId: ProcessId, data: string): ProcessSnapshot;
27
+ kill(processId: ProcessId, force?: boolean): ProcessSnapshot;
28
+ shutdown(): Promise<void>;
29
+ private onOutput;
30
+ private onExit;
31
+ private trimCompletedProcesses;
32
+ private require;
33
+ private requireRunning;
34
+ private snapshotOf;
35
+ }
36
+ export {};
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Owns all ChatRoom-managed process lifecycle, timeout handling, output buffering, stdin, and process operation events.
3
+ * Platform-specific creation details are delegated to pipe/PTY backends.
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import { ChatRoomError } from "../../core/errors/chatroom-error.js";
7
+ import { childEnvironment } from "../../core/runtime/child-environment.js";
8
+ import { HeadTailBuffer } from "./head-tail-buffer.js";
9
+ export class ProcessSupervisor {
10
+ backends;
11
+ operations;
12
+ eventBus;
13
+ maxOutputBytes;
14
+ defaultTimeoutMs;
15
+ maxCompletedProcesses;
16
+ processes = new Map();
17
+ shuttingDown = false;
18
+ constructor(backends, operations, eventBus, maxOutputBytes, defaultTimeoutMs, maxCompletedProcesses) {
19
+ this.backends = backends;
20
+ this.operations = operations;
21
+ this.eventBus = eventBus;
22
+ this.maxOutputBytes = maxOutputBytes;
23
+ this.defaultTimeoutMs = defaultTimeoutMs;
24
+ this.maxCompletedProcesses = maxCompletedProcesses;
25
+ }
26
+ async start(request, operationContext) {
27
+ if (this.shuttingDown)
28
+ throw new ChatRoomError("CONFLICT", "Process supervisor is shutting down");
29
+ const processId = `proc_${randomUUID()}`;
30
+ const args = request.args ?? [];
31
+ const operation = this.operations.start({
32
+ pluginId: "process",
33
+ source: operationContext.source,
34
+ action: operationContext.action ?? "start",
35
+ processId,
36
+ input: {
37
+ command: request.command,
38
+ args,
39
+ cwd: request.cwd,
40
+ pty: request.pty ?? false,
41
+ timeoutMs: request.timeoutMs ?? this.defaultTimeoutMs,
42
+ env: request.env ?? {},
43
+ },
44
+ });
45
+ let backend;
46
+ try {
47
+ // Child environments inherit only the runtime allowlist, then apply explicit request overrides.
48
+ backend = await (request.pty ? this.backends.pty : this.backends.pipe).start(request.command, args, {
49
+ cwd: request.cwd,
50
+ env: childEnvironment(request.env),
51
+ });
52
+ }
53
+ catch (error) {
54
+ this.operations.finish(operation.operationId, "error", null, error);
55
+ throw new ChatRoomError("PROCESS_FAILED", `Failed to start process: ${request.command}`, undefined, { cause: error });
56
+ }
57
+ let resolveSettled;
58
+ const settled = new Promise((resolve) => {
59
+ resolveSettled = resolve;
60
+ });
61
+ const managed = {
62
+ request,
63
+ backend,
64
+ stdout: new HeadTailBuffer(this.maxOutputBytes),
65
+ stderr: new HeadTailBuffer(this.maxOutputBytes),
66
+ state: "running",
67
+ startedAt: new Date(),
68
+ finishedAt: null,
69
+ exitCode: null,
70
+ signal: null,
71
+ timeout: null,
72
+ forceTimeout: null,
73
+ timedOut: false,
74
+ operationId: operation.operationId,
75
+ settled,
76
+ resolveSettled,
77
+ };
78
+ this.processes.set(processId, managed);
79
+ backend.onStdout((chunk) => this.onOutput(processId, "stdout", chunk));
80
+ backend.onStderr((chunk) => this.onOutput(processId, "stderr", chunk));
81
+ backend.onExit((exit) => this.onExit(processId, exit.exitCode, exit.signal));
82
+ const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
83
+ if (timeoutMs > 0)
84
+ managed.timeout = setTimeout(() => {
85
+ if (managed.state !== "running")
86
+ return;
87
+ managed.timedOut = true;
88
+ // Timeouts first request graceful termination and escalate to SIGKILL only if the backend remains alive.
89
+ managed.backend.kill("SIGTERM");
90
+ managed.forceTimeout = setTimeout(() => {
91
+ if (managed.state === "running")
92
+ managed.backend.kill("SIGKILL");
93
+ }, 2000).unref();
94
+ }, timeoutMs).unref();
95
+ const snapshot = this.snapshotOf(processId, managed);
96
+ this.eventBus.emit({ type: "process", process: snapshot });
97
+ return snapshot;
98
+ }
99
+ read(processId) {
100
+ const item = this.require(processId);
101
+ return this.snapshotOf(processId, item);
102
+ }
103
+ list() {
104
+ return [...this.processes.entries()]
105
+ .map(([id, item]) => this.snapshotOf(id, item))
106
+ .sort((a, b) => b.startedAt.localeCompare(a.startedAt));
107
+ }
108
+ write(processId, data) {
109
+ const item = this.requireRunning(processId);
110
+ item.backend.write(data);
111
+ return this.snapshotOf(processId, item);
112
+ }
113
+ kill(processId, force = false) {
114
+ const item = this.requireRunning(processId);
115
+ item.backend.kill(force ? "SIGKILL" : "SIGTERM");
116
+ return this.snapshotOf(processId, item);
117
+ }
118
+ async shutdown() {
119
+ this.shuttingDown = true;
120
+ const running = [...this.processes.entries()].filter(([, item]) => item.state === "running");
121
+ for (const [, item] of running)
122
+ item.backend.kill("SIGTERM");
123
+ await Promise.race([
124
+ Promise.allSettled(running.map(([, item]) => item.settled)),
125
+ new Promise((resolve) => setTimeout(resolve, 1500)),
126
+ ]);
127
+ const survivors = running.filter(([, item]) => item.state === "running");
128
+ for (const [, item] of survivors)
129
+ item.backend.kill("SIGKILL");
130
+ if (survivors.length) {
131
+ await Promise.race([
132
+ Promise.allSettled(survivors.map(([, item]) => item.settled)),
133
+ new Promise((resolve) => setTimeout(resolve, 1500)),
134
+ ]);
135
+ // A backend that does not report close after SIGKILL must still be settled before SQLite closes.
136
+ for (const [processId, item] of survivors)
137
+ if (item.state === "running")
138
+ this.onExit(processId, null, "SIGKILL");
139
+ }
140
+ }
141
+ onOutput(processId, stream, chunk) {
142
+ const item = this.processes.get(processId);
143
+ if (!item || item.state !== "running")
144
+ return;
145
+ // Store only bounded head/tail output while forwarding live chunks to SSE.
146
+ (stream === "stdout" ? item.stdout : item.stderr).append(chunk);
147
+ const text = chunk.toString("utf8");
148
+ this.eventBus.emit({
149
+ type: "process-output",
150
+ processId,
151
+ stream,
152
+ chunk: text,
153
+ });
154
+ const snapshot = this.snapshotOf(processId, item);
155
+ this.operations.updateRunning(item.operationId, {
156
+ stdout: snapshot.stdout,
157
+ stderr: snapshot.stderr,
158
+ outputTruncated: snapshot.outputTruncated,
159
+ pid: snapshot.pid,
160
+ });
161
+ }
162
+ onExit(processId, exitCode, signal) {
163
+ const item = this.processes.get(processId);
164
+ if (!item || item.state !== "running")
165
+ return;
166
+ if (item.timeout)
167
+ clearTimeout(item.timeout);
168
+ if (item.forceTimeout)
169
+ clearTimeout(item.forceTimeout);
170
+ // A signal is represented as killed/cancelled; otherwise zero means exited and non-zero means failed.
171
+ item.finishedAt = new Date();
172
+ item.exitCode = exitCode;
173
+ item.signal = signal;
174
+ item.state = signal ? "killed" : exitCode === 0 ? "exited" : "failed";
175
+ const snapshot = this.snapshotOf(processId, item);
176
+ this.operations.finish(item.operationId, exitCode === 0 && !signal ? "success" : signal ? "cancelled" : "error", {
177
+ stdout: snapshot.stdout,
178
+ stderr: snapshot.stderr,
179
+ exitCode,
180
+ signal,
181
+ durationMs: snapshot.durationMs,
182
+ outputTruncated: snapshot.outputTruncated,
183
+ timedOut: snapshot.timedOut,
184
+ }, exitCode === 0 && !signal
185
+ ? undefined
186
+ : { exitCode, signal, timedOut: snapshot.timedOut });
187
+ this.eventBus.emit({ type: "process", process: snapshot });
188
+ item.resolveSettled(snapshot);
189
+ this.trimCompletedProcesses();
190
+ }
191
+ trimCompletedProcesses() {
192
+ const completed = [...this.processes.entries()]
193
+ .filter(([, item]) => item.state !== "running")
194
+ .sort(([, a], [, b]) => b.startedAt.getTime() - a.startedAt.getTime());
195
+ for (const [processId] of completed.slice(this.maxCompletedProcesses)) {
196
+ this.processes.delete(processId);
197
+ }
198
+ }
199
+ require(processId) {
200
+ const item = this.processes.get(processId);
201
+ if (!item)
202
+ throw new ChatRoomError("NOT_FOUND", `Unknown ProcessId: ${processId}`);
203
+ return item;
204
+ }
205
+ requireRunning(processId) {
206
+ const item = this.require(processId);
207
+ if (item.state !== "running")
208
+ throw new ChatRoomError("CONFLICT", `Process is not running: ${processId}`);
209
+ return item;
210
+ }
211
+ snapshotOf(id, item) {
212
+ const stdout = item.stdout.snapshot(), stderr = item.stderr.snapshot(), finish = item.finishedAt?.getTime() ?? Date.now();
213
+ return {
214
+ processId: id,
215
+ command: item.request.command,
216
+ args: item.request.args ?? [],
217
+ cwd: item.request.cwd,
218
+ pid: item.backend.pid,
219
+ state: item.state,
220
+ startedAt: item.startedAt.toISOString(),
221
+ finishedAt: item.finishedAt?.toISOString() ?? null,
222
+ durationMs: Math.max(0, finish - item.startedAt.getTime()),
223
+ exitCode: item.exitCode,
224
+ signal: item.signal,
225
+ stdout: stdout.text,
226
+ stderr: stderr.text,
227
+ outputTruncated: stdout.outputTruncated || stderr.outputTruncated,
228
+ timedOut: item.timedOut,
229
+ operationId: item.operationId,
230
+ };
231
+ }
232
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Process lifecycle and request types shared above the platform-specific backend layer.
3
+ * ProcessId is independent from the OS PID and transport/session identity.
4
+ */
5
+ type ProcessState = "running" | "exited" | "killed" | "failed";
6
+ export type ProcessId = string;
7
+ export interface ProcessStartRequest {
8
+ cwd: string;
9
+ command: string;
10
+ args?: string[];
11
+ env?: Record<string, string>;
12
+ pty?: boolean;
13
+ timeoutMs?: number;
14
+ }
15
+ export interface ProcessSnapshot {
16
+ processId: ProcessId;
17
+ command: string;
18
+ args: string[];
19
+ cwd: string;
20
+ pid: number | null;
21
+ state: ProcessState;
22
+ startedAt: string;
23
+ finishedAt: string | null;
24
+ durationMs: number;
25
+ exitCode: number | null;
26
+ signal: string | null;
27
+ /** Bounded retained output rather than an unbounded transcript. */
28
+ stdout: string;
29
+ stderr: string;
30
+ outputTruncated: boolean;
31
+ timedOut: boolean;
32
+ operationId: string;
33
+ }
34
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { ChatRoomConfig } from "../config/types.js";
3
+ import type { AppDatabase } from "../infrastructure/database/app-database.js";
4
+ import type { OperationLog } from "../operations/operation-log.js";
5
+ import type { RuntimeEventBus } from "../app/event-bus.js";
6
+ import type { ExternalAccessRegistry } from "../app/external-access-registry.js";
7
+ export interface ServiceToken<T> {
8
+ readonly key: symbol;
9
+ readonly name: string;
10
+ /** Compile-time marker only; it is never set at runtime. */
11
+ readonly __type?: T;
12
+ }
13
+ export declare function createServiceToken<T>(name: string): ServiceToken<T>;
14
+ export declare class ServiceRegistry {
15
+ private readonly values;
16
+ provide<T>(token: ServiceToken<T>, value: T): void;
17
+ require<T>(token: ServiceToken<T>): T;
18
+ }
19
+ export interface PluginContext {
20
+ config: ChatRoomConfig;
21
+ database: AppDatabase;
22
+ operations: OperationLog;
23
+ events: RuntimeEventBus;
24
+ externalAccess: ExternalAccessRegistry;
25
+ services: ServiceRegistry;
26
+ }
27
+ export interface InternalPlugin {
28
+ id: string;
29
+ activate(context: PluginContext): Promise<void> | void;
30
+ registerMcp?(server: McpServer): void;
31
+ deactivate?(): Promise<void> | void;
32
+ }
@@ -0,0 +1,17 @@
1
+ export function createServiceToken(name) {
2
+ return { key: Symbol(name), name };
3
+ }
4
+ export class ServiceRegistry {
5
+ values = new Map();
6
+ provide(token, value) {
7
+ if (this.values.has(token.key))
8
+ throw new Error(`Service already provided: ${token.name}`);
9
+ this.values.set(token.key, value);
10
+ }
11
+ require(token) {
12
+ const value = this.values.get(token.key);
13
+ if (value === undefined)
14
+ throw new Error(`Required service unavailable: ${token.name}`);
15
+ return value;
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { Operation as DomainOperation } from "../../core/operations/types.js";
2
+ import type { GitInfo } from "../workspace/domain/git.js";
3
+ import type { ProcessSnapshot } from "../process/types.js";
4
+ import type { Workspace } from "../workspace/domain/workspace.js";
5
+ import type { WorktreeApplyPreview, WorktreeFileDiff } from "../workspace/domain/review.js";
6
+ export type { ProcessSnapshot, WorktreeApplyPreview, WorktreeFileDiff };
7
+ export type Operation = DomainOperation;
8
+ export interface WorkspaceView extends Workspace {
9
+ git?: GitInfo | null;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /** HTTP/JSON presentation adapter for runtime status, workspaces, operations, processes, and management actions. */
2
+ import { Router } from "express";
3
+ import type { WebRuntime } from "../runtime.js";
4
+ import type { RuntimeEventBus } from "../../../app/event-bus.js";
5
+ import type { AuthService } from "../../../auth/auth-service.js";
6
+ import type { PasskeyService } from "../../../auth/passkey-service.js";
7
+ import type { IngressPolicy } from "../../../auth/ingress-policy.js";
8
+ import type { CloudController } from "../../cloud/controller.js";
9
+ export declare function createApiRouter(application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, ingress: IngressPolicy, cloud: CloudController, runtimeStatus: () => {
10
+ version: string;
11
+ mcpRequests: number;
12
+ uptimeMinutes: number;
13
+ }): Router;
@@ -0,0 +1,146 @@
1
+ /** HTTP/JSON presentation adapter for runtime status, workspaces, operations, processes, and management actions. */
2
+ import { Router } from "express";
3
+ import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
4
+ import { asyncRoute, parseCookie, requireString, } from "../../../presentation/http/http-utils.js";
5
+ import { createProcessApiRouter } from "./process-api-router.js";
6
+ import { createWorkspaceApiRouter } from "./workspace-api-router.js";
7
+ import { createCloudApiRouter } from "./cloud-api-router.js";
8
+ const SESSION_COOKIE = "chatroom_session";
9
+ export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, runtimeStatus) {
10
+ const router = Router();
11
+ router.get("/auth/status", (req, res) => {
12
+ const token = parseCookie(req.headers.cookie, SESSION_COOKIE);
13
+ const enabled = ingress.requiresWebAuth(req);
14
+ const passkeyAvailable = Boolean(ingress.webAuthnOrigin(req));
15
+ res.json({
16
+ enabled,
17
+ authenticated: !enabled || Boolean(token && auth.verifyWebSession(token)),
18
+ passkeyAvailable,
19
+ passkeyRegistered: passkeyAvailable && passkeys.list().length > 0,
20
+ });
21
+ });
22
+ router.post("/auth/login", asyncRoute(async (req, res) => {
23
+ if (!ingress.requiresWebAuth(req)) {
24
+ res.json({ authenticated: true });
25
+ return;
26
+ }
27
+ const ownerToken = requireString(req.body?.ownerToken, "ownerToken");
28
+ const remember = req.body?.remember !== false;
29
+ const session = auth.createWebSession(ownerToken, remember);
30
+ setSessionCookie(res, session.token, session.maxAgeSeconds, ingress.secureWebCookie(req));
31
+ res.json({ authenticated: true, expiresAt: session.expiresAt });
32
+ }));
33
+ router.post("/auth/passkey/options", asyncRoute(async (req, res) => {
34
+ res.json(await passkeys.authenticationOptions(ingress.webAuthnOrigin(req)));
35
+ }));
36
+ router.post("/auth/passkey/verify", asyncRoute(async (req, res) => {
37
+ const body = req.body;
38
+ await passkeys.verifyAuthentication({
39
+ challengeId: requireString(body.challengeId, "challengeId"),
40
+ response: body.response,
41
+ });
42
+ const remember = body.remember !== false;
43
+ const session = auth.createPasskeyWebSession(remember);
44
+ setSessionCookie(res, session.token, session.maxAgeSeconds, ingress.secureWebCookie(req));
45
+ res.json({ authenticated: true, expiresAt: session.expiresAt });
46
+ }));
47
+ router.post("/auth/logout", (req, res) => {
48
+ const token = parseCookie(req.headers.cookie, SESSION_COOKIE);
49
+ if (token)
50
+ auth.revokeWebSession(token);
51
+ const secure = ingress.secureWebCookie(req) ? "; Secure" : "";
52
+ res.setHeader("Set-Cookie", `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`);
53
+ res.json({ authenticated: false });
54
+ });
55
+ router.use(apiAuthentication(auth, ingress));
56
+ router.get("/runtime", (_req, res) => res.json(runtimeStatus()));
57
+ router.get("/auth/passkeys", (_req, res) => res.json(passkeys.list()));
58
+ router.post("/auth/passkeys/register/options", asyncRoute(async (req, res) => {
59
+ res.json(await passkeys.registrationOptions(ingress.webAuthnOrigin(req)));
60
+ }));
61
+ router.post("/auth/passkeys/register/verify", asyncRoute(async (req, res) => {
62
+ const body = req.body;
63
+ res.status(201).json(await passkeys.verifyRegistration({
64
+ challengeId: requireString(body.challengeId, "challengeId"),
65
+ response: body.response,
66
+ ...(typeof body.name === "string" ? { name: body.name } : {}),
67
+ }));
68
+ }));
69
+ router.delete("/auth/passkeys/:credentialId", (req, res) => {
70
+ passkeys.remove(requireString(req.params.credentialId, "credentialId"));
71
+ res.status(204).end();
72
+ });
73
+ router.get("/operations", (req, res) => {
74
+ res.json(application.operations.list({
75
+ limit: numberQuery(req.query.limit, 100, 500),
76
+ offset: numberQuery(req.query.offset, 0, 100000),
77
+ ...(typeof req.query.pluginId === "string"
78
+ ? { pluginId: req.query.pluginId }
79
+ : {}),
80
+ ...(typeof req.query.status === "string"
81
+ ? { status: req.query.status }
82
+ : {}),
83
+ ...(typeof req.query.workspaceId === "string"
84
+ ? { workspaceId: req.query.workspaceId }
85
+ : {}),
86
+ }));
87
+ });
88
+ router.delete("/operations", (_req, res) => {
89
+ res.json(application.operations.clearHistory());
90
+ });
91
+ router.get("/operations/:operationId", (req, res) => {
92
+ const operationId = requireString(req.params.operationId, "operationId");
93
+ const event = application.operations.get(operationId);
94
+ if (!event)
95
+ throw new ChatRoomError("NOT_FOUND", "Operation not found");
96
+ res.json(event);
97
+ });
98
+ router.use(createWorkspaceApiRouter(application));
99
+ router.use(createProcessApiRouter(application));
100
+ router.use(createCloudApiRouter(cloud, application.operations));
101
+ router.get("/events", (req, res) => {
102
+ res.status(200);
103
+ res.setHeader("Content-Type", "text/event-stream");
104
+ res.setHeader("Cache-Control", "no-cache, no-transform");
105
+ res.setHeader("Connection", "keep-alive");
106
+ res.flushHeaders();
107
+ res.write(`event: ready\ndata: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
108
+ const unsubscribe = eventBus.subscribe((event) => {
109
+ res.write(`event: runtime\ndata: ${JSON.stringify(event)}\n\n`);
110
+ });
111
+ const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
112
+ keepalive.unref();
113
+ req.on("close", () => {
114
+ clearInterval(keepalive);
115
+ unsubscribe();
116
+ });
117
+ });
118
+ return router;
119
+ }
120
+ function setSessionCookie(res, token, maxAgeSeconds, secureCookie) {
121
+ const secure = secureCookie ? "; Secure" : "";
122
+ const maxAge = maxAgeSeconds === null ? "" : `; Max-Age=${maxAgeSeconds}`;
123
+ res.setHeader("Set-Cookie", `${SESSION_COOKIE}=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/${maxAge}${secure}`);
124
+ }
125
+ function apiAuthentication(auth, ingress) {
126
+ return (req, res, next) => {
127
+ if (!ingress.requiresWebAuth(req)) {
128
+ next();
129
+ return;
130
+ }
131
+ const token = parseCookie(req.headers.cookie, SESSION_COOKIE);
132
+ if (token && auth.verifyWebSession(token)) {
133
+ next();
134
+ return;
135
+ }
136
+ res.status(401).json({
137
+ error: { code: "FORBIDDEN", message: "Authentication required" },
138
+ });
139
+ };
140
+ }
141
+ function numberQuery(value, fallback, max) {
142
+ const parsed = Number(value);
143
+ return Number.isInteger(parsed) && parsed >= 0
144
+ ? Math.min(parsed, max)
145
+ : fallback;
146
+ }
@@ -0,0 +1,4 @@
1
+ import { Router } from "express";
2
+ import type { OperationLog } from "../../../operations/operation-log.js";
3
+ import type { CloudController } from "../../cloud/controller.js";
4
+ export declare function createCloudApiRouter(controller: CloudController, operations: OperationLog): Router;