@chatroomcp/chatroom 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +1 -1
  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/computer/computer-native-host.d.ts +2 -1
  47. package/dist/plugins/computer/computer-native-host.js +107 -17
  48. package/dist/plugins/computer/computer-schemas.d.ts +1 -0
  49. package/dist/plugins/computer/computer-schemas.js +1 -1
  50. package/dist/plugins/computer/types.d.ts +1 -1
  51. package/dist/plugins/git/git-service.d.ts +33 -0
  52. package/dist/plugins/git/git-service.js +414 -0
  53. package/dist/plugins/git/plugin.d.ts +4 -0
  54. package/dist/plugins/git/plugin.js +12 -0
  55. package/dist/plugins/git/types.d.ts +32 -0
  56. package/dist/plugins/process/backend.d.ts +0 -1
  57. package/dist/plugins/process/head-tail-buffer.d.ts +0 -1
  58. package/dist/plugins/process/infrastructure/pipe-process-backend.js +0 -1
  59. package/dist/plugins/process/infrastructure/pty-process-backend.js +1 -4
  60. package/dist/plugins/process/process-supervisor.js +10 -23
  61. package/dist/plugins/process/types.d.ts +0 -5
  62. package/dist/plugins/types.d.ts +0 -1
  63. package/dist/plugins/web/api-types.d.ts +3 -7
  64. package/dist/plugins/web/http/api-router.d.ts +0 -1
  65. package/dist/plugins/web/http/api-router.js +2 -4
  66. package/dist/plugins/web/http/git-api-router.d.ts +3 -0
  67. package/dist/plugins/web/http/git-api-router.js +98 -0
  68. package/dist/plugins/web/http/workspace-api-router.js +15 -37
  69. package/dist/plugins/web/plugin.js +4 -6
  70. package/dist/plugins/web/runtime.d.ts +5 -27
  71. package/dist/plugins/web/runtime.js +3 -42
  72. package/dist/plugins/workspace/mcp.js +17 -135
  73. package/dist/plugins/workspace/metadata.d.ts +4 -0
  74. package/dist/plugins/workspace/metadata.js +105 -0
  75. package/dist/plugins/workspace/plugin.d.ts +2 -8
  76. package/dist/plugins/workspace/plugin.js +8 -20
  77. package/dist/plugins/workspace/types.d.ts +18 -0
  78. package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +2 -15
  79. package/dist/plugins/workspace/workspace-fs.js +111 -0
  80. package/dist/plugins/workspace/workspace-service.d.ts +7 -42
  81. package/dist/plugins/workspace/workspace-service.js +61 -315
  82. package/dist/presentation/http/http-utils.d.ts +0 -1
  83. package/dist/presentation/http/oauth-router.d.ts +0 -1
  84. package/dist/presentation/http/oauth-router.js +0 -1
  85. package/dist/web/assets/{index-BXlxEk8f.css → index-DFKEvyzR.css} +1 -1
  86. package/dist/web/assets/index-vzPPspuL.js +26 -0
  87. package/dist/web/index.html +2 -2
  88. package/package.json +4 -3
  89. package/dist/plugins/workspace/domain/filesystem.d.ts +0 -25
  90. package/dist/plugins/workspace/domain/git.d.ts +0 -7
  91. package/dist/plugins/workspace/domain/repository.d.ts +0 -16
  92. package/dist/plugins/workspace/domain/repository.js +0 -1
  93. package/dist/plugins/workspace/domain/review.d.ts +0 -28
  94. package/dist/plugins/workspace/domain/review.js +0 -1
  95. package/dist/plugins/workspace/domain/workspace.d.ts +0 -23
  96. package/dist/plugins/workspace/domain/workspace.js +0 -1
  97. package/dist/plugins/workspace/git/git-command-runner.d.ts +0 -6
  98. package/dist/plugins/workspace/git/git-command-runner.js +0 -15
  99. package/dist/plugins/workspace/git/git-service.d.ts +0 -14
  100. package/dist/plugins/workspace/git/git-service.js +0 -107
  101. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +0 -33
  102. package/dist/plugins/workspace/git/git-snapshot-service.js +0 -352
  103. package/dist/plugins/workspace/infrastructure/workspace-fs.js +0 -340
  104. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +0 -12
  105. package/dist/plugins/workspace/infrastructure/workspace-repository.js +0 -51
  106. package/dist/plugins/workspace/workspace-helpers.d.ts +0 -9
  107. package/dist/plugins/workspace/workspace-helpers.js +0 -67
  108. package/dist/plugins/workspace/worktree-review-service.d.ts +0 -16
  109. package/dist/plugins/workspace/worktree-review-service.js +0 -51
  110. package/dist/web/assets/index-DUM6zVHS.js +0 -26
  111. /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
  112. /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
@@ -7,9 +7,9 @@ export function createChatRoomMcpHandler(plugins) {
7
7
  });
8
8
  }
9
9
  function createServer(plugins) {
10
- const server = new McpServer({ name: CHATROOM_NAME, version: CHATROOM_VERSION }, {
11
- instructions: "Workspace lifecycle is independent from MCP connections. " +
12
- "Use WorkspaceId only with workspace tools and ProcessId only with process tools.",
10
+ const server = new McpServer({
11
+ name: CHATROOM_NAME,
12
+ version: CHATROOM_VERSION,
13
13
  });
14
14
  plugins.registerMcp(server);
15
15
  return server;
@@ -4,7 +4,6 @@ type PluginToolInput<Schema extends StandardSchemaWithJSON> = StandardSchemaWith
4
4
  type PluginToolAction<Input> = string | ((input: Input) => string);
5
5
  export interface PluginToolExecution {
6
6
  readonly operationId: string;
7
- /** Keep the operation running after the MCP handler returns. The owner must finish it later. */
8
7
  deferCompletion(): void;
9
8
  }
10
9
  export interface PluginToolConfig<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON> {
@@ -20,7 +19,6 @@ export interface PluginToolConfig<InputSchema extends StandardSchemaWithJSON, Ou
20
19
  };
21
20
  present?: (output: unknown) => CallToolResult;
22
21
  }
23
- /** Framework-owned MCP registration boundary that guarantees every plugin tool is audited. */
24
22
  export declare class PluginMcpRegistrar {
25
23
  private readonly server;
26
24
  private readonly operations;
@@ -1,6 +1,5 @@
1
1
  import { asChatRoomError } from "../../core/errors/chatroom-error.js";
2
2
  import { mcpTool } from "./tool-support.js";
3
- /** Framework-owned MCP registration boundary that guarantees every plugin tool is audited. */
4
3
  export class PluginMcpRegistrar {
5
4
  server;
6
5
  operations;
@@ -53,9 +52,6 @@ function operationReferences(input) {
53
52
  return {};
54
53
  const record = input;
55
54
  return {
56
- ...(typeof record.workspaceId === "string"
57
- ? { workspaceId: record.workspaceId }
58
- : {}),
59
55
  ...(typeof record.processId === "string"
60
56
  ? { processId: record.processId }
61
57
  : {}),
@@ -6,18 +6,6 @@ export declare const closedRead: {
6
6
  idempotentHint: true;
7
7
  openWorldHint: false;
8
8
  };
9
- export declare const localWrite: {
10
- readOnlyHint: false;
11
- destructiveHint: true;
12
- idempotentHint: true;
13
- openWorldHint: false;
14
- };
15
- export declare const localMutation: {
16
- readOnlyHint: false;
17
- destructiveHint: false;
18
- idempotentHint: false;
19
- openWorldHint: false;
20
- };
21
9
  export declare const destructiveLocalMutation: {
22
10
  readOnlyHint: false;
23
11
  destructiveHint: true;
@@ -30,55 +18,6 @@ export declare const openWorldMutation: {
30
18
  idempotentHint: false;
31
19
  openWorldHint: true;
32
20
  };
33
- export declare const workspaceOutputSchema: z.ZodObject<{
34
- id: z.ZodString;
35
- root: z.ZodString;
36
- sourceRoot: z.ZodString;
37
- mode: z.ZodEnum<{
38
- checkout: "checkout";
39
- worktree: "worktree";
40
- }>;
41
- createdAt: z.ZodString;
42
- lastUsedAt: z.ZodString;
43
- instructions: z.ZodArray<z.ZodString>;
44
- skills: z.ZodArray<z.ZodString>;
45
- capabilities: z.ZodObject<{
46
- filesystem: z.ZodLiteral<"read-write">;
47
- git: z.ZodBoolean;
48
- skills: z.ZodBoolean;
49
- }, z.core.$strip>;
50
- }, z.core.$strip>;
51
- export declare const fileReadOutputSchema: z.ZodObject<{
52
- content: z.ZodString;
53
- bytes: z.ZodNumber;
54
- truncated: z.ZodBoolean;
55
- }, z.core.$strip>;
56
- export declare const fileWriteOutputSchema: z.ZodObject<{
57
- path: z.ZodString;
58
- bytes: z.ZodNumber;
59
- }, z.core.$strip>;
60
- export declare const fileInfoSchema: z.ZodObject<{
61
- path: z.ZodString;
62
- type: z.ZodEnum<{
63
- file: "file";
64
- directory: "directory";
65
- symlink: "symlink";
66
- }>;
67
- size: z.ZodNumber;
68
- modifiedAt: z.ZodString;
69
- }, z.core.$strip>;
70
- export declare const searchMatchSchema: z.ZodObject<{
71
- path: z.ZodString;
72
- line: z.ZodNumber;
73
- column: z.ZodNumber;
74
- preview: z.ZodString;
75
- }, z.core.$strip>;
76
- export declare const changeSetSchema: z.ZodObject<{
77
- files: z.ZodArray<z.ZodString>;
78
- replacements: z.ZodNumber;
79
- bytesBefore: z.ZodNumber;
80
- bytesAfter: z.ZodNumber;
81
- }, z.core.$strip>;
82
21
  export declare const processSnapshotSchema: z.ZodObject<{
83
22
  processId: z.ZodString;
84
23
  command: z.ZodString;
@@ -6,18 +6,6 @@ export const closedRead = {
6
6
  idempotentHint: true,
7
7
  openWorldHint: false,
8
8
  };
9
- export const localWrite = {
10
- readOnlyHint: false,
11
- destructiveHint: true,
12
- idempotentHint: true,
13
- openWorldHint: false,
14
- };
15
- export const localMutation = {
16
- readOnlyHint: false,
17
- destructiveHint: false,
18
- idempotentHint: false,
19
- openWorldHint: false,
20
- };
21
9
  export const destructiveLocalMutation = {
22
10
  readOnlyHint: false,
23
11
  destructiveHint: true,
@@ -30,48 +18,6 @@ export const openWorldMutation = {
30
18
  idempotentHint: false,
31
19
  openWorldHint: true,
32
20
  };
33
- export const workspaceOutputSchema = z.object({
34
- id: z.string(),
35
- root: z.string(),
36
- sourceRoot: z.string(),
37
- mode: z.enum(["checkout", "worktree"]),
38
- createdAt: z.string(),
39
- lastUsedAt: z.string(),
40
- instructions: z.array(z.string()),
41
- skills: z.array(z.string()),
42
- capabilities: z.object({
43
- filesystem: z.literal("read-write"),
44
- git: z.boolean(),
45
- skills: z.boolean(),
46
- }),
47
- });
48
- export const fileReadOutputSchema = z.object({
49
- content: z.string(),
50
- bytes: z.number().int().nonnegative(),
51
- truncated: z.boolean(),
52
- });
53
- export const fileWriteOutputSchema = z.object({
54
- path: z.string(),
55
- bytes: z.number().int().nonnegative(),
56
- });
57
- export const fileInfoSchema = z.object({
58
- path: z.string(),
59
- type: z.enum(["file", "directory", "symlink"]),
60
- size: z.number().int().nonnegative(),
61
- modifiedAt: z.string(),
62
- });
63
- export const searchMatchSchema = z.object({
64
- path: z.string(),
65
- line: z.number().int().positive(),
66
- column: z.number().int().positive(),
67
- preview: z.string(),
68
- });
69
- export const changeSetSchema = z.object({
70
- files: z.array(z.string()),
71
- replacements: z.number().int().nonnegative(),
72
- bytesBefore: z.number().int().nonnegative(),
73
- bytesAfter: z.number().int().nonnegative(),
74
- });
75
21
  export const processSnapshotSchema = z.object({
76
22
  processId: z.string(),
77
23
  command: z.string(),
@@ -8,7 +8,7 @@
8
8
  <key>CFBundlePackageType</key><string>APPL</string>
9
9
  <key>CFBundleVersion</key><string>1</string>
10
10
  <key>CFBundleShortVersionString</key><string>1.0</string>
11
- <key>LSMinimumSystemVersion</key><string>15.2</string>
11
+ <key>LSMinimumSystemVersion</key><string>14.0</string>
12
12
  <key>LSUIElement</key><true/>
13
13
  <key>NSHumanReadableCopyright</key><string>ChatRoom</string>
14
14
  </dict></plist>
@@ -1,7 +1,6 @@
1
1
  import type { Operation, OperationStart, OperationStatus } from "../core/operations/types.js";
2
2
  import type { OperationQuery, OperationRepository } from "../core/operations/repository.js";
3
3
  import type { RuntimeEventBus } from "../app/event-bus.js";
4
- /** Persists redacted, bounded operations produced by ChatRoom plugins. */
5
4
  export declare class OperationLog {
6
5
  private readonly repository;
7
6
  private readonly eventBus;
@@ -10,12 +9,10 @@ export declare class OperationLog {
10
9
  constructor(repository: OperationRepository, eventBus: RuntimeEventBus, maxPayloadBytes: number);
11
10
  start(request: OperationStart): Operation;
12
11
  finish(operationId: string, status: Exclude<OperationStatus, "running">, output?: unknown, error?: unknown): Operation;
13
- updateRunning(operationId: string, output: unknown): Operation;
14
12
  run<T>(request: OperationStart, action: () => Promise<T>): Promise<T>;
15
13
  list(query?: OperationQuery): Operation[];
16
14
  get(operationId: string): Operation | null;
17
15
  associate(operationId: string, references: {
18
- workspaceId?: string | null;
19
16
  processId?: string | null;
20
17
  }): Operation;
21
18
  clearHistory(): {
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
2
2
  import { boundJson } from "../core/operations/bounded-value.js";
3
3
  import { SecretRedactor } from "../core/operations/redactor.js";
4
4
  import { asChatRoomError, ChatRoomError, } from "../core/errors/chatroom-error.js";
5
- /** Persists redacted, bounded operations produced by ChatRoom plugins. */
6
5
  export class OperationLog {
7
6
  repository;
8
7
  eventBus;
@@ -21,7 +20,6 @@ export class OperationLog {
21
20
  source: request.source,
22
21
  action: request.action,
23
22
  status: "running",
24
- workspaceId: request.workspaceId ?? null,
25
23
  processId: request.processId ?? null,
26
24
  input: input.value,
27
25
  output: null,
@@ -52,17 +50,6 @@ export class OperationLog {
52
50
  this.eventBus.emit({ type: "operation", operation });
53
51
  return operation;
54
52
  }
55
- updateRunning(operationId, output) {
56
- const operation = this.require(operationId);
57
- if (operation.status !== "running")
58
- return operation;
59
- const bounded = boundJson(this.redactor.redact(output), this.maxPayloadBytes);
60
- operation.output = bounded.value;
61
- operation.outputTruncated = bounded.truncated;
62
- this.repository.update(operation);
63
- this.eventBus.emit({ type: "operation", operation });
64
- return operation;
65
- }
66
53
  async run(request, action) {
67
54
  const operation = this.start(request);
68
55
  try {
@@ -88,8 +75,6 @@ export class OperationLog {
88
75
  }
89
76
  associate(operationId, references) {
90
77
  const operation = this.require(operationId);
91
- if (references.workspaceId !== undefined)
92
- operation.workspaceId = references.workspaceId;
93
78
  if (references.processId !== undefined)
94
79
  operation.processId = references.processId;
95
80
  this.repository.update(operation);
@@ -79,6 +79,7 @@ export class CloudApiClient {
79
79
  ...payload,
80
80
  ...proof,
81
81
  }),
82
+ signal: AbortSignal.timeout(15_000),
82
83
  });
83
84
  const body = (await response.json().catch(() => null));
84
85
  if (!response.ok) {
@@ -18,6 +18,7 @@ export declare class CloudController {
18
18
  private lastError;
19
19
  private managementTimer;
20
20
  private renewalTimer;
21
+ private statusRetryTimer;
21
22
  private stopped;
22
23
  private constructor();
23
24
  static create(config: ChatRoomConfig, externalAccess?: ExternalAccessSink): Promise<CloudController>;
@@ -38,6 +39,7 @@ export declare class CloudController {
38
39
  private refreshLeaseIfNeeded;
39
40
  private connectTunnel;
40
41
  private publishExternalAccess;
42
+ private scheduleStatusRetry;
41
43
  private scheduleManagementSync;
42
44
  private scheduleRenewal;
43
45
  private validManagementSession;
@@ -13,6 +13,7 @@ export class CloudController {
13
13
  lastError = null;
14
14
  managementTimer = null;
15
15
  renewalTimer = null;
16
+ statusRetryTimer = null;
16
17
  stopped = true;
17
18
  constructor(config, cloudApi, externalAccess) {
18
19
  this.config = config;
@@ -28,6 +29,9 @@ export class CloudController {
28
29
  return controller;
29
30
  }
30
31
  status() {
32
+ const lease = this.state.lease && Date.parse(this.state.lease.expiresAt) > Date.now()
33
+ ? this.state.lease
34
+ : null;
31
35
  return {
32
36
  installationId: this.state.installationId,
33
37
  customerId: this.state.customerId,
@@ -37,10 +41,8 @@ export class CloudController {
37
41
  managementSessionActive: Boolean(this.state.managementSession &&
38
42
  Date.parse(this.state.managementSession.expiresAt) > Date.now()),
39
43
  connection: this.connection,
40
- mcpUrl: this.state.lease?.mcpBaseUrl
41
- ? `${this.state.lease.mcpBaseUrl}/mcp`
42
- : null,
43
- webUrl: this.state.lease?.webBaseUrl ?? null,
44
+ mcpUrl: lease?.mcpBaseUrl ? `${lease.mcpBaseUrl}/mcp` : null,
45
+ webUrl: lease?.webBaseUrl ?? null,
44
46
  lastError: this.lastError,
45
47
  };
46
48
  }
@@ -48,7 +50,10 @@ export class CloudController {
48
50
  if (!this.stopped)
49
51
  return;
50
52
  this.stopped = false;
51
- await this.syncStatus().catch((error) => this.setError(error));
53
+ await this.syncStatus().catch((error) => {
54
+ this.setError(error);
55
+ this.scheduleStatusRetry();
56
+ });
52
57
  this.scheduleManagementSync();
53
58
  }
54
59
  async stop() {
@@ -57,8 +62,11 @@ export class CloudController {
57
62
  clearTimeout(this.managementTimer);
58
63
  if (this.renewalTimer)
59
64
  clearTimeout(this.renewalTimer);
65
+ if (this.statusRetryTimer)
66
+ clearTimeout(this.statusRetryTimer);
60
67
  this.managementTimer = null;
61
68
  this.renewalTimer = null;
69
+ this.statusRetryTimer = null;
62
70
  this.tunnel?.stop();
63
71
  this.tunnel = null;
64
72
  this.externalAccess?.clearCloud();
@@ -106,6 +114,9 @@ export class CloudController {
106
114
  this.scheduleRenewal();
107
115
  }
108
116
  this.lastError = null;
117
+ if (this.statusRetryTimer)
118
+ clearTimeout(this.statusRetryTimer);
119
+ this.statusRetryTimer = null;
109
120
  return this.status();
110
121
  }
111
122
  async restore(recoveryKey) {
@@ -222,6 +233,18 @@ export class CloudController {
222
233
  : null,
223
234
  });
224
235
  }
236
+ scheduleStatusRetry(delayMs = 30_000) {
237
+ if (this.stopped || this.statusRetryTimer)
238
+ return;
239
+ this.statusRetryTimer = setTimeout(() => {
240
+ this.statusRetryTimer = null;
241
+ void this.syncStatus().catch((error) => {
242
+ this.setError(error);
243
+ this.scheduleStatusRetry();
244
+ });
245
+ }, delayMs);
246
+ this.statusRetryTimer.unref();
247
+ }
225
248
  scheduleManagementSync(delayMs = 3_000) {
226
249
  if (this.managementTimer)
227
250
  clearTimeout(this.managementTimer);
@@ -77,15 +77,11 @@ export class CloudTunnelClient {
77
77
  this.acceptingServices = new Set(lease.services);
78
78
  if (this.stopped)
79
79
  return;
80
- // A new signed lease on the same tunnel is applied in-band. This keeps the
81
- // current WebUI/API request alive while service permissions change.
82
80
  if (!tunnelChanged) {
83
81
  if (this.socket?.readyState === WebSocket.OPEN)
84
82
  this.send({ type: "update-lease", lease: lease.token });
85
83
  return;
86
84
  }
87
- // Changing the tunnel endpoint still requires a reconnect. If a service is
88
- // being removed, let current streams finish before moving the connection.
89
85
  if (!removesService || this.streams.size === 0) {
90
86
  this.socket?.close();
91
87
  return;
@@ -15,9 +15,10 @@ export declare class ComputerNativeHost {
15
15
  restart(reason?: string): void;
16
16
  dispose(): Promise<void>;
17
17
  private ensureStarted;
18
+ private startHelper;
18
19
  private startMacHelper;
19
20
  private attachSocket;
20
- private startWindowsHelper;
21
+ private startPipeHelper;
21
22
  private failTransport;
22
23
  private reset;
23
24
  private rejectPending;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync, } from "node:child_process";
3
3
  import { chmodSync, existsSync, rmSync } from "node:fs";
4
4
  import { createServer } from "node:net";
5
5
  import { createInterface } from "node:readline";
@@ -21,6 +21,8 @@ export class ComputerNativeHost {
21
21
  return "macos";
22
22
  if (process.platform === "win32")
23
23
  return "windows";
24
+ if (process.platform === "linux")
25
+ return "linux";
24
26
  return "unsupported";
25
27
  }
26
28
  get idle() {
@@ -59,18 +61,16 @@ export class ComputerNativeHost {
59
61
  this.reset(new Error("Computer helper stopped"));
60
62
  }
61
63
  async ensureStarted() {
62
- if (this.platform === "macos" && this.socket && !this.socket.destroyed)
64
+ const platform = this.platform;
65
+ if (platform === "macos" && this.socket && !this.socket.destroyed)
63
66
  return;
64
- if (this.platform === "windows" && this.child && !this.child.killed)
67
+ if ((platform === "windows" || platform === "linux") &&
68
+ this.child &&
69
+ !this.child.killed)
65
70
  return;
66
71
  if (this.starting)
67
72
  return this.starting;
68
- this.starting =
69
- this.platform === "macos"
70
- ? this.startMacHelper()
71
- : this.platform === "windows"
72
- ? this.startWindowsHelper()
73
- : Promise.reject(new ChatRoomError("UNSUPPORTED", "Computer Use is supported only on macOS and Windows"));
73
+ this.starting = this.startHelper(platform);
74
74
  try {
75
75
  await this.starting;
76
76
  }
@@ -78,6 +78,18 @@ export class ComputerNativeHost {
78
78
  this.starting = null;
79
79
  }
80
80
  }
81
+ startHelper(platform) {
82
+ switch (platform) {
83
+ case "macos":
84
+ return this.startMacHelper();
85
+ case "windows":
86
+ return this.startPipeHelper(windowsHelperPath(), "Windows");
87
+ case "linux":
88
+ return this.startPipeHelper(linuxHelperPath(), "Linux X11", linuxDesktopEnvironment());
89
+ case "unsupported":
90
+ return Promise.reject(new ChatRoomError("UNSUPPORTED", "Computer Use is supported only on macOS, Windows, and Linux X11"));
91
+ }
92
+ }
81
93
  async startMacHelper() {
82
94
  const app = macHelperAppPath();
83
95
  if (!app)
@@ -131,13 +143,13 @@ export class ComputerNativeHost {
131
143
  socket.once("error", (error) => this.failTransport(socket, error));
132
144
  socket.once("close", () => this.failTransport(socket, new Error("Computer helper exited")));
133
145
  }
134
- async startWindowsHelper() {
135
- const executable = windowsHelperPath();
146
+ async startPipeHelper(executable, platformName, env = process.env) {
136
147
  if (!executable)
137
- throw new ChatRoomError("UNSUPPORTED", "Windows Computer helper is missing");
148
+ throw new ChatRoomError("UNSUPPORTED", `${platformName} Computer helper is missing`);
138
149
  const child = spawn(executable, [], {
139
150
  stdio: ["pipe", "pipe", "pipe"],
140
- windowsHide: true,
151
+ windowsHide: process.platform === "win32",
152
+ env,
141
153
  });
142
154
  this.child = child;
143
155
  this.lines = createInterface({ input: child.stdout });
@@ -236,10 +248,88 @@ function projectRoot() {
236
248
  return path.resolve(path.dirname(current), "../../..");
237
249
  }
238
250
  function macHelperAppPath() {
239
- const app = path.join(projectRoot(), "dist", "native", "macos", "ChatRoomComputerHelper.app");
240
- return existsSync(app) ? app : null;
251
+ return nativeHelperPath("macos", "ChatRoomComputerHelper.app");
241
252
  }
242
253
  function windowsHelperPath() {
243
- const executable = path.join(projectRoot(), "dist", "native", "windows", "chatroom-computer-helper.exe");
244
- return existsSync(executable) ? executable : null;
254
+ return nativeHelperPath("windows", "chatroom-computer-helper.exe");
255
+ }
256
+ function linuxHelperPath() {
257
+ return nativeHelperPath("linux", "chatroom-computer-helper");
258
+ }
259
+ function nativeHelperPath(...parts) {
260
+ const target = path.join(projectRoot(), "dist", "native", ...parts);
261
+ return existsSync(target) ? target : null;
262
+ }
263
+ function linuxDesktopEnvironment() {
264
+ const env = { ...process.env };
265
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
266
+ if (!env.XDG_RUNTIME_DIR && uid !== null) {
267
+ const runtimeDir = `/run/user/${uid}`;
268
+ if (existsSync(runtimeDir))
269
+ env.XDG_RUNTIME_DIR = runtimeDir;
270
+ }
271
+ if (!env.DISPLAY) {
272
+ const display = activeX11Display(uid);
273
+ if (display) {
274
+ env.DISPLAY = display;
275
+ env.XDG_SESSION_TYPE = "x11";
276
+ }
277
+ }
278
+ if (!env.XAUTHORITY) {
279
+ const candidates = [
280
+ env.HOME ? path.join(env.HOME, ".Xauthority") : null,
281
+ env.XDG_RUNTIME_DIR
282
+ ? path.join(env.XDG_RUNTIME_DIR, "gdm", "Xauthority")
283
+ : null,
284
+ ];
285
+ const authority = candidates.find((candidate) => !!candidate && existsSync(candidate));
286
+ if (authority)
287
+ env.XAUTHORITY = authority;
288
+ }
289
+ if (!env.DBUS_SESSION_BUS_ADDRESS && env.XDG_RUNTIME_DIR) {
290
+ const bus = path.join(env.XDG_RUNTIME_DIR, "bus");
291
+ if (existsSync(bus))
292
+ env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${bus}`;
293
+ }
294
+ return env;
295
+ }
296
+ function activeX11Display(uid) {
297
+ if (process.platform !== "linux" || uid === null)
298
+ return null;
299
+ const loginctl = existsSync("/usr/bin/loginctl")
300
+ ? "/usr/bin/loginctl"
301
+ : "loginctl";
302
+ const sessions = spawnSync(loginctl, ["list-sessions", "--no-legend", "--no-pager"], {
303
+ encoding: "utf8",
304
+ timeout: 2_000,
305
+ });
306
+ if (sessions.status !== 0 || !sessions.stdout)
307
+ return null;
308
+ const userId = String(uid);
309
+ for (const line of sessions.stdout.split("\n")) {
310
+ const [sessionId, sessionUid] = line.trim().split(/\s+/, 3);
311
+ if (!sessionId || sessionUid !== userId)
312
+ continue;
313
+ const details = spawnSync(loginctl, [
314
+ "show-session",
315
+ sessionId,
316
+ "--no-pager",
317
+ "-p",
318
+ "Type",
319
+ "-p",
320
+ "Active",
321
+ "-p",
322
+ "Display",
323
+ ], { encoding: "utf8", timeout: 2_000 });
324
+ if (details.status !== 0 || !details.stdout)
325
+ continue;
326
+ const values = Object.fromEntries(details.stdout
327
+ .split("\n")
328
+ .map((item) => item.split("=", 2))
329
+ .filter((item) => item.length === 2));
330
+ if (values.Type === "x11" && values.Active === "yes" && values.Display) {
331
+ return values.Display;
332
+ }
333
+ }
334
+ return null;
245
335
  }
@@ -36,6 +36,7 @@ export declare const computerNativeStatusSchema: z.ZodObject<{
36
36
  platform: z.ZodEnum<{
37
37
  macos: "macos";
38
38
  windows: "windows";
39
+ linux: "linux";
39
40
  unsupported: "unsupported";
40
41
  }>;
41
42
  helper: z.ZodEnum<{
@@ -30,7 +30,7 @@ export const computerScreenshotSchema = z.object({
30
30
  data: z.string(),
31
31
  });
32
32
  export const computerNativeStatusSchema = z.object({
33
- platform: z.enum(["macos", "windows", "unsupported"]),
33
+ platform: z.enum(["macos", "windows", "linux", "unsupported"]),
34
34
  helper: z.enum(["running", "stopped", "unavailable"]),
35
35
  permissions: z.object({
36
36
  accessibility: computerPermissionStateSchema,
@@ -1,5 +1,5 @@
1
1
  export type ComputerAccessScope = "local" | "remote";
2
- export type ComputerPlatform = "macos" | "windows" | "unsupported";
2
+ export type ComputerPlatform = "macos" | "windows" | "linux" | "unsupported";
3
3
  export type ComputerHelperState = "running" | "stopped" | "unavailable";
4
4
  export type ComputerPermissionState = "granted" | "denied" | "unknown" | "not-required";
5
5
  export type ComputerPermission = "accessibility" | "screenRecording";
@@ -0,0 +1,33 @@
1
+ import type { CommandRunner } from "../../core/runtime/command-runner.js";
2
+ import type { GitBranch, GitCommit, GitDiff, GitStatus } from "./types.js";
3
+ export declare class GitService {
4
+ private readonly commands;
5
+ constructor(commands: CommandRunner);
6
+ status(cwd: string): Promise<GitStatus | null>;
7
+ diff(cwd: string, filePath?: string, maxPreviewBytes?: number): Promise<GitDiff>;
8
+ stage(cwd: string, paths: string[]): Promise<GitStatus>;
9
+ unstage(cwd: string, paths: string[]): Promise<GitStatus>;
10
+ restore(cwd: string, filePath: string): Promise<GitStatus>;
11
+ commit(cwd: string, message: string): Promise<GitStatus>;
12
+ branches(cwd: string): Promise<GitBranch[]>;
13
+ createBranch(cwd: string, name: string): Promise<GitStatus>;
14
+ switchBranch(cwd: string, name: string): Promise<GitStatus>;
15
+ deleteBranch(cwd: string, name: string): Promise<GitStatus>;
16
+ log(cwd: string, limit?: number): Promise<GitCommit[]>;
17
+ fetch(cwd: string): Promise<GitStatus>;
18
+ pull(cwd: string): Promise<GitStatus>;
19
+ push(cwd: string): Promise<GitStatus>;
20
+ private run;
21
+ private repositoryRoot;
22
+ private requireRepository;
23
+ private requireStatus;
24
+ private head;
25
+ private requireHead;
26
+ private branch;
27
+ private upstream;
28
+ private aheadBehind;
29
+ private relatedPaths;
30
+ private pathExistsInHead;
31
+ private validateBranchName;
32
+ private withSnapshotIndex;
33
+ }