@chatroomcp/chatroom 0.2.1 → 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 (107) 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/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 +15 -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 +17 -135
  68. package/dist/plugins/workspace/metadata.d.ts +4 -0
  69. package/dist/plugins/workspace/metadata.js +105 -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 +18 -0
  73. package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +2 -15
  74. package/dist/plugins/workspace/workspace-fs.js +111 -0
  75. package/dist/plugins/workspace/workspace-service.d.ts +7 -42
  76. package/dist/plugins/workspace/workspace-service.js +61 -315
  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-B4vO7wVS.css → index-DFKEvyzR.css} +1 -1
  81. package/dist/web/assets/index-vzPPspuL.js +26 -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-BU7LCgKB.js +0 -26
  106. /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
  107. /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  English | <a href="./docs/README.zh-CN.md">简体中文</a> | <a href="https://doc.chatroomcp.com/">Documentation</a>
9
9
  </p>
10
10
 
11
- ChatRoom is a local MCP runtime that lets ChatGPT work with files, Git worktrees, and processes on your device. Its WebUI provides workspace browsing, process monitoring, ChatRoom Cloud controls, and plugin-attributed operation logs.
11
+ ChatRoom is a local MCP runtime that lets ChatGPT work with projects and processes on your device. Its WebUI provides workspace browsing, Git repository management, process monitoring, ChatRoom Cloud controls, and plugin-attributed operation logs.
12
12
 
13
13
  ## Install
14
14
 
@@ -1,4 +1,3 @@
1
- /** Composition root for the single-package ChatRoom runtime. */
2
1
  import type { ChatRoomConfig } from "../config/types.js";
3
2
  import { AppDatabase } from "../infrastructure/database/app-database.js";
4
3
  import { RuntimeEventBus } from "./event-bus.js";
@@ -11,6 +11,7 @@ import { ExternalAccessRegistry } from "./external-access-registry.js";
11
11
  import { ServiceRegistry } from "../plugins/types.js";
12
12
  import { PluginManager } from "../plugins/plugin-manager.js";
13
13
  import { createWorkspacePlugin } from "../plugins/workspace/plugin.js";
14
+ import { createGitPlugin } from "../plugins/git/plugin.js";
14
15
  import { createProcessPlugin, ProcessService, } from "../plugins/process/plugin.js";
15
16
  import { createCloudPlugin, CloudService } from "../plugins/cloud/plugin.js";
16
17
  import { createComputerPlugin, ComputerServiceToken, } from "../plugins/computer/plugin.js";
@@ -36,6 +37,7 @@ export async function createApplication(config) {
36
37
  services,
37
38
  }, [
38
39
  createWorkspacePlugin(),
40
+ createGitPlugin(),
39
41
  createProcessPlugin(),
40
42
  createComputerPlugin(),
41
43
  createCloudPlugin(),
@@ -17,8 +17,6 @@ export type RuntimeEvent = {
17
17
  } | {
18
18
  type: "process-output";
19
19
  processId: string;
20
- stream: "stdout" | "stderr";
21
- chunk: string;
22
20
  };
23
21
  export declare class RuntimeEventBus {
24
22
  private readonly emitter;
@@ -1,4 +1,3 @@
1
- /** Lightweight in-process event bus used to fan runtime changes out to SSE/WebUI observers. */
2
1
  import { EventEmitter } from "node:events";
3
2
  export class RuntimeEventBus {
4
3
  emitter = new EventEmitter();
@@ -4,7 +4,6 @@ export interface ExternalAccessState {
4
4
  mcpBaseUrl: string | null;
5
5
  webBaseUrl: string | null;
6
6
  }
7
- /** Combines configured self-hosted URLs with the current ChatRoom Cloud URLs. */
8
7
  export declare class ExternalAccessRegistry {
9
8
  private readonly config;
10
9
  private cloud;
@@ -1,4 +1,3 @@
1
- /** Combines configured self-hosted URLs with the current ChatRoom Cloud URLs. */
2
1
  export class ExternalAccessRegistry {
3
2
  config;
4
3
  cloud = { mcpBaseUrl: null, webBaseUrl: null };
@@ -1,4 +1,3 @@
1
- /** Coordinates startup and shutdown ordering for runtime resources and internal plugins. */
2
1
  import type { ChatRoomConfig } from "../config/types.js";
3
2
  import { type ApplicationComponents } from "./application.js";
4
3
  export declare class ApplicationLifecycle {
@@ -1,7 +1,3 @@
1
- /**
2
- * Implements owner authentication and the OAuth Authorization Code + PKCE flow used for remote MCP access.
3
- * Raw authorization codes and access tokens are never persisted; repositories receive only their hashes.
4
- */
5
1
  import { createHash, randomBytes, randomUUID, timingSafeEqual, } from "node:crypto";
6
2
  import { ChatRoomError } from "../core/errors/chatroom-error.js";
7
3
  export class AuthService {
@@ -5,7 +5,6 @@ export interface WebAuthnOrigin {
5
5
  origin: string;
6
6
  rpId: string;
7
7
  }
8
- /** Classifies ingress exclusively from configured/registered public hosts. */
9
8
  export declare class IngressPolicy {
10
9
  private readonly config;
11
10
  private readonly externalAccess;
@@ -1,4 +1,3 @@
1
- /** Classifies ingress exclusively from configured/registered public hosts. */
2
1
  export class IngressPolicy {
3
2
  config;
4
3
  externalAccess;
@@ -1,4 +1,3 @@
1
- /** ChatRoom CLI host. */
2
1
  import { initializeConfig, loadConfig } from "../config/load-config.js";
3
2
  import { ApplicationLifecycle } from "../app/lifecycle.js";
4
3
  export async function runChatRoomCli(argv = process.argv.slice(2)) {
@@ -1,4 +1,3 @@
1
- /** Runtime configuration after defaults and path normalization have been resolved. */
2
1
  export interface ChatRoomConfig {
3
2
  allowedRoots: string[];
4
3
  dataDir: string;
@@ -1,59 +1,36 @@
1
- /**
2
- * Persistence contracts for OAuth clients, authorization codes, and access tokens.
3
- *
4
- * Raw authorization codes and bearer tokens must never cross this boundary. AuthService hashes
5
- * those credentials first and repositories store only the resulting digest values.
6
- */
7
- /** Dynamically registered OAuth public client. */
8
1
  export interface OAuthClientRecord {
9
2
  clientId: string;
10
3
  name: string;
11
- /** Exact redirect URI allowlist registered for the client. */
12
4
  redirectUris: string[];
13
5
  createdAt: string;
14
6
  }
15
- /** Hashed Authorization Code + PKCE state. */
16
7
  export interface OAuthCodeRecord {
17
- /** SHA-256 digest of the raw one-time authorization code. */
18
8
  codeHash: string;
19
9
  clientId: string;
20
10
  redirectUri: string;
21
- /** S256 challenge supplied during authorization. */
22
11
  codeChallenge: string;
23
- /** Granted scopes bound to this authorization code. */
24
12
  scope: string;
25
13
  expiresAt: string;
26
- /** Consumption timestamp, or null before the code is atomically used. */
27
14
  usedAt: string | null;
28
15
  }
29
- /** Minimal active-token metadata returned after a hashed token lookup. */
30
16
  export interface OAuthTokenRecord {
31
17
  clientId: string;
32
18
  scope: string;
33
19
  expiresAt: string;
34
20
  }
35
- /** Storage port used by AuthService for the complete OAuth state machine. */
36
21
  export interface OAuthStateRepository {
37
22
  createClient(client: OAuthClientRecord): void;
38
23
  getClient(clientId: string): OAuthClientRecord | null;
39
24
  createCode(code: OAuthCodeRecord): void;
40
- /** Read an unconsumed candidate without mutating it so PKCE can be validated first. */
41
25
  getCode(codeHash: string): OAuthCodeRecord | null;
42
- /** Atomically consume a valid code; null means it was missing, expired by policy, or already used. */
43
26
  consumeCode(codeHash: string, now: string): OAuthCodeRecord | null;
44
- /** Persist only a token hash together with scope and expiry metadata. */
45
27
  createToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
46
- /** Persist a hashed refresh token separately from access tokens. */
47
28
  createRefreshToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
48
- /** Atomically rotate an active refresh token. */
49
29
  consumeRefreshToken(tokenHash: string, clientId?: string): {
50
30
  clientId: string;
51
31
  scope: string;
52
32
  } | null;
53
- /** Resolve active, non-revoked token metadata from a token hash. */
54
33
  tokenInfo(tokenHash: string): OAuthTokenRecord | null;
55
- /** Mark a token hash revoked without requiring the raw bearer token to be stored. */
56
34
  revokeToken(tokenHash: string): void;
57
- /** Remove expired and terminal OAuth state that no longer participates in the protocol. */
58
35
  prune(now: string): number;
59
36
  }
@@ -1,7 +1 @@
1
- /**
2
- * Persistence contracts for OAuth clients, authorization codes, and access tokens.
3
- *
4
- * Raw authorization codes and bearer tokens must never cross this boundary. AuthService hashes
5
- * those credentials first and repositories store only the resulting digest values.
6
- */
7
1
  export {};
@@ -1,23 +1,8 @@
1
- /**
2
- * Typed error vocabulary exposed across ChatRoom application boundaries.
3
- *
4
- * Presentation adapters translate these stable codes into protocol responses. Domain/application
5
- * code should prefer ChatRoomError over parsing message text or leaking implementation-specific
6
- * filesystem, SQLite, process, or SDK errors to external callers.
7
- */
8
1
  export declare const ERROR_CODES: readonly ["INVALID_INPUT", "NOT_FOUND", "FORBIDDEN", "CONFLICT", "UNSUPPORTED", "PROCESS_FAILED", "INTERNAL"];
9
2
  export type ErrorCode = (typeof ERROR_CODES)[number];
10
- /** Error carrying a stable machine-readable code plus optional structured diagnostic details. */
11
3
  export declare class ChatRoomError extends Error {
12
4
  readonly code: ErrorCode;
13
5
  readonly details?: unknown;
14
6
  constructor(code: ErrorCode, message: string, details?: unknown, options?: ErrorOptions);
15
7
  }
16
- /**
17
- * Normalize an arbitrary thrown value at an application or protocol boundary.
18
- *
19
- * Existing ChatRoomError instances retain their public code. Native Error values become INTERNAL
20
- * while preserving the original error as `cause`; non-Error throws are converted as well so every
21
- * external path observes the same error vocabulary.
22
- */
23
8
  export declare function asChatRoomError(error: unknown): ChatRoomError;
@@ -1,10 +1,3 @@
1
- /**
2
- * Typed error vocabulary exposed across ChatRoom application boundaries.
3
- *
4
- * Presentation adapters translate these stable codes into protocol responses. Domain/application
5
- * code should prefer ChatRoomError over parsing message text or leaking implementation-specific
6
- * filesystem, SQLite, process, or SDK errors to external callers.
7
- */
8
1
  export const ERROR_CODES = [
9
2
  "INVALID_INPUT",
10
3
  "NOT_FOUND",
@@ -14,7 +7,6 @@ export const ERROR_CODES = [
14
7
  "PROCESS_FAILED",
15
8
  "INTERNAL",
16
9
  ];
17
- /** Error carrying a stable machine-readable code plus optional structured diagnostic details. */
18
10
  export class ChatRoomError extends Error {
19
11
  code;
20
12
  details;
@@ -25,13 +17,6 @@ export class ChatRoomError extends Error {
25
17
  this.details = details;
26
18
  }
27
19
  }
28
- /**
29
- * Normalize an arbitrary thrown value at an application or protocol boundary.
30
- *
31
- * Existing ChatRoomError instances retain their public code. Native Error values become INTERNAL
32
- * while preserving the original error as `cause`; non-Error throws are converted as well so every
33
- * external path observes the same error vocabulary.
34
- */
35
20
  export function asChatRoomError(error) {
36
21
  if (error instanceof ChatRoomError)
37
22
  return error;
@@ -1,4 +1,3 @@
1
- /** Utilities for bounding JSON-like operation payloads before persistence. */
2
1
  export interface BoundedValue<T = unknown> {
3
2
  value: T;
4
3
  truncated: boolean;
@@ -1,22 +1,38 @@
1
1
  export function boundJson(value, maxBytes) {
2
- // Preserve both the beginning and end of oversized strings because failures commonly appear at the tail.
3
2
  if (value === undefined)
4
3
  return { value: null, truncated: false };
5
4
  const json = safeStringify(value);
6
5
  const size = Buffer.byteLength(json);
7
6
  if (size <= maxBytes)
8
7
  return { value: JSON.parse(json), truncated: false };
9
- const marker = "\n…[truncated]…\n";
10
- const budget = Math.max(0, maxBytes - Buffer.byteLength(marker));
11
- const headBudget = Math.floor(budget / 2);
12
- const tailBudget = budget - headBudget;
13
8
  const encoded = Buffer.from(json);
14
- const head = encoded.subarray(0, headBudget).toString("utf8");
15
- const tail = encoded.subarray(encoded.length - tailBudget).toString("utf8");
16
- const text = `${head}${marker}${tail}`;
9
+ const marker = "\n…[truncated]…\n";
10
+ let low = 0;
11
+ let high = encoded.length;
12
+ let best = truncatedValue(encoded, marker, size, 0);
13
+ while (low <= high) {
14
+ const retainedBytes = Math.floor((low + high) / 2);
15
+ const candidate = truncatedValue(encoded, marker, size, retainedBytes);
16
+ if (Buffer.byteLength(JSON.stringify(candidate)) <= maxBytes) {
17
+ best = candidate;
18
+ low = retainedBytes + 1;
19
+ }
20
+ else {
21
+ high = retainedBytes - 1;
22
+ }
23
+ }
24
+ return { value: best, truncated: true };
25
+ }
26
+ function truncatedValue(encoded, marker, originalBytes, retainedBytes) {
27
+ const headBytes = Math.floor(retainedBytes / 2);
28
+ const tailBytes = retainedBytes - headBytes;
29
+ const head = encoded.subarray(0, headBytes).toString("utf8");
30
+ const tail = encoded
31
+ .subarray(Math.max(0, encoded.length - tailBytes))
32
+ .toString("utf8");
17
33
  return {
18
- value: { truncatedJson: text, originalBytes: size },
19
- truncated: true,
34
+ truncatedJson: `${head}${marker}${tail}`,
35
+ originalBytes,
20
36
  };
21
37
  }
22
38
  function safeStringify(value) {
@@ -1,4 +1,3 @@
1
- /** Redacts common credential shapes before values cross the durable operation-log boundary. */
2
1
  const SECRET_KEY = new RegExp("(^|[_-])" +
3
2
  "(authorization|cookie|password|passwd|token|access[_-]?token|" +
4
3
  "refresh[_-]?token|api[_-]?key|apikey|secret|private[_-]?key|" +
@@ -4,7 +4,6 @@ export interface OperationQuery {
4
4
  offset?: number;
5
5
  pluginId?: string;
6
6
  status?: OperationStatus | string;
7
- workspaceId?: string;
8
7
  }
9
8
  export interface OperationRepository {
10
9
  insert(operation: Operation): void;
@@ -1,7 +1,5 @@
1
- /** Persistent plugin operation lifecycle recorded by ChatRoom. */
2
1
  export declare const OPERATION_STATUSES: readonly ["running", "success", "error", "cancelled"];
3
2
  export type OperationStatus = (typeof OPERATION_STATUSES)[number];
4
- /** Where an operation was initiated; pluginId identifies which plugin owns the operation. */
5
3
  export declare const OPERATION_SOURCES: readonly ["mcp", "gui", "system", "cli"];
6
4
  export type OperationSource = (typeof OPERATION_SOURCES)[number];
7
5
  export interface Operation {
@@ -10,7 +8,6 @@ export interface Operation {
10
8
  source: OperationSource;
11
9
  action: string;
12
10
  status: OperationStatus;
13
- workspaceId: string | null;
14
11
  processId: string | null;
15
12
  input: unknown;
16
13
  output: unknown;
@@ -26,6 +23,5 @@ export interface OperationStart {
26
23
  source: OperationSource;
27
24
  action: string;
28
25
  input?: unknown;
29
- workspaceId?: string | null;
30
26
  processId?: string | null;
31
27
  }
@@ -1,9 +1,7 @@
1
- /** Persistent plugin operation lifecycle recorded by ChatRoom. */
2
1
  export const OPERATION_STATUSES = [
3
2
  "running",
4
3
  "success",
5
4
  "error",
6
5
  "cancelled",
7
6
  ];
8
- /** Where an operation was initiated; pluginId identifies which plugin owns the operation. */
9
7
  export const OPERATION_SOURCES = ["mcp", "gui", "system", "cli"];
@@ -1,109 +1,16 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
- const SCHEMA = `
5
- CREATE TABLE IF NOT EXISTS workspaces (
6
- id TEXT PRIMARY KEY,
7
- root TEXT NOT NULL UNIQUE,
8
- source_root TEXT NOT NULL,
9
- mode TEXT NOT NULL CHECK(mode IN ('checkout','worktree')),
10
- created_at TEXT NOT NULL,
11
- last_used_at TEXT NOT NULL
12
- );
13
- CREATE INDEX IF NOT EXISTS workspaces_last_used_idx ON workspaces(last_used_at DESC);
14
-
15
- CREATE TABLE IF NOT EXISTS operations (
16
- operation_id TEXT PRIMARY KEY,
17
- plugin_id TEXT NOT NULL,
18
- source TEXT NOT NULL,
19
- action TEXT NOT NULL,
20
- status TEXT NOT NULL,
21
- workspace_id TEXT,
22
- process_id TEXT,
23
- input_json TEXT NOT NULL,
24
- output_json TEXT NOT NULL,
25
- error_json TEXT NOT NULL,
26
- input_truncated INTEGER NOT NULL DEFAULT 0,
27
- output_truncated INTEGER NOT NULL DEFAULT 0,
28
- started_at TEXT NOT NULL,
29
- finished_at TEXT,
30
- duration_ms INTEGER
31
- );
32
- CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);
33
- CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);
34
- CREATE INDEX IF NOT EXISTS operations_workspace_idx ON operations(workspace_id, started_at DESC);
35
- CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);
36
-
37
- CREATE TABLE IF NOT EXISTS oauth_clients (
38
- client_id TEXT PRIMARY KEY,
39
- name TEXT NOT NULL,
40
- redirect_uris_json TEXT NOT NULL,
41
- created_at TEXT NOT NULL
42
- );
43
- CREATE TABLE IF NOT EXISTS oauth_codes (
44
- code_hash TEXT PRIMARY KEY,
45
- client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
46
- redirect_uri TEXT NOT NULL,
47
- code_challenge TEXT NOT NULL,
48
- scope TEXT NOT NULL,
49
- expires_at TEXT NOT NULL,
50
- used_at TEXT
51
- );
52
- CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);
53
- CREATE TABLE IF NOT EXISTS oauth_tokens (
54
- token_hash TEXT PRIMARY KEY,
55
- client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
56
- scope TEXT NOT NULL,
57
- expires_at TEXT NOT NULL,
58
- revoked_at TEXT,
59
- created_at TEXT NOT NULL
60
- );
61
- CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);
62
- CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
63
- token_hash TEXT PRIMARY KEY,
64
- client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
65
- scope TEXT NOT NULL,
66
- expires_at TEXT NOT NULL,
67
- revoked_at TEXT,
68
- created_at TEXT NOT NULL
69
- );
70
- CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);
71
-
72
- CREATE TABLE IF NOT EXISTS web_sessions (
73
- token_hash TEXT PRIMARY KEY,
74
- expires_at TEXT NOT NULL,
75
- created_at TEXT NOT NULL
76
- );
77
- CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);
78
-
79
- CREATE TABLE IF NOT EXISTS passkeys (
80
- credential_id TEXT PRIMARY KEY,
81
- public_key BLOB NOT NULL,
82
- counter INTEGER NOT NULL,
83
- transports_json TEXT NOT NULL,
84
- device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),
85
- backed_up INTEGER NOT NULL,
86
- name TEXT NOT NULL,
87
- created_at TEXT NOT NULL,
88
- last_used_at TEXT NOT NULL
89
- );
90
- CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);
91
-
92
- CREATE TABLE IF NOT EXISTS computer_settings (
93
- id INTEGER PRIMARY KEY CHECK(id = 1),
94
- enabled INTEGER NOT NULL DEFAULT 0,
95
- remote_access INTEGER NOT NULL DEFAULT 1,
96
- updated_at TEXT NOT NULL
97
- );
98
-
99
- `;
4
+ import { migrateDatabase } from "./migrations.js";
5
+ import { DATABASE_SCHEMA } from "./schema.js";
100
6
  export class AppDatabase {
101
7
  raw;
102
8
  constructor(databasePath) {
103
9
  mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
104
10
  this.raw = new DatabaseSync(databasePath);
105
11
  this.raw.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;");
106
- this.raw.exec(SCHEMA);
12
+ migrateDatabase(this.raw);
13
+ this.raw.exec(DATABASE_SCHEMA);
107
14
  }
108
15
  close() {
109
16
  this.raw.close();
@@ -0,0 +1,2 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ export declare function migrateDatabase(db: DatabaseSync): void;
@@ -0,0 +1,44 @@
1
+ import { DATABASE_SCHEMA_VERSION } from "./schema.js";
2
+ export function migrateDatabase(db) {
3
+ let version = userVersion(db);
4
+ if (version > DATABASE_SCHEMA_VERSION)
5
+ throw new Error(`Database schema ${version} is newer than this ChatRoom build (${DATABASE_SCHEMA_VERSION})`);
6
+ while (version < DATABASE_SCHEMA_VERSION) {
7
+ switch (version) {
8
+ case 0:
9
+ migrate0To1(db);
10
+ version = 1;
11
+ break;
12
+ default:
13
+ throw new Error(`Unsupported ChatRoom database schema: ${version}`);
14
+ }
15
+ }
16
+ }
17
+ function migrate0To1(db) {
18
+ db.exec("BEGIN IMMEDIATE;");
19
+ try {
20
+ db.exec(`
21
+ DROP TABLE IF EXISTS workspaces;
22
+ DROP TABLE IF EXISTS audit_events;
23
+ DROP TABLE IF EXISTS agent_runs;
24
+ DROP INDEX IF EXISTS operations_workspace_idx;
25
+ `);
26
+ const operationColumns = db
27
+ .prepare("PRAGMA table_info(operations)")
28
+ .all();
29
+ if (operationColumns.some((column) => column.name === "workspace_id"))
30
+ db.exec("ALTER TABLE operations DROP COLUMN workspace_id;");
31
+ db.exec("PRAGMA user_version = 1; COMMIT;");
32
+ }
33
+ catch (error) {
34
+ db.exec("ROLLBACK;");
35
+ throw error;
36
+ }
37
+ }
38
+ function userVersion(db) {
39
+ const row = db.prepare("PRAGMA user_version").get();
40
+ const version = Number(row?.user_version ?? 0);
41
+ if (!Number.isInteger(version) || version < 0)
42
+ throw new Error(`Invalid ChatRoom database schema version: ${version}`);
43
+ return version;
44
+ }
@@ -7,10 +7,10 @@ export class OperationRepository {
7
7
  this.database.raw
8
8
  .prepare(`
9
9
  INSERT INTO operations(
10
- operation_id, plugin_id, source, action, status, workspace_id, process_id,
10
+ operation_id, plugin_id, source, action, status, process_id,
11
11
  input_json, output_json, error_json, input_truncated, output_truncated,
12
12
  started_at, finished_at, duration_ms
13
- ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
13
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
14
14
  `)
15
15
  .run(...toRowValues(operation));
16
16
  }
@@ -18,7 +18,7 @@ export class OperationRepository {
18
18
  this.database.raw
19
19
  .prepare(`
20
20
  UPDATE operations SET
21
- plugin_id=?, source=?, action=?, status=?, workspace_id=?, process_id=?,
21
+ plugin_id=?, source=?, action=?, status=?, process_id=?,
22
22
  input_json=?, output_json=?, error_json=?, input_truncated=?, output_truncated=?,
23
23
  started_at=?, finished_at=?, duration_ms=?
24
24
  WHERE operation_id=?
@@ -42,10 +42,6 @@ export class OperationRepository {
42
42
  where.push("status=?");
43
43
  args.push(query.status);
44
44
  }
45
- if (query.workspaceId) {
46
- where.push("workspace_id=?");
47
- args.push(query.workspaceId);
48
- }
49
45
  const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
50
46
  const offset = Math.max(query.offset ?? 0, 0);
51
47
  const filter = where.length ? `WHERE ${where.join(" AND ")}` : "";
@@ -88,7 +84,6 @@ function toRowValues(operation) {
88
84
  operation.source,
89
85
  operation.action,
90
86
  operation.status,
91
- operation.workspaceId,
92
87
  operation.processId,
93
88
  JSON.stringify(operation.input),
94
89
  JSON.stringify(operation.output),
@@ -107,7 +102,6 @@ function fromRow(row) {
107
102
  source: row.source,
108
103
  action: row.action,
109
104
  status: row.status,
110
- workspaceId: row.workspace_id,
111
105
  processId: row.process_id,
112
106
  input: parse(row.input_json),
113
107
  output: parse(row.output_json),
@@ -0,0 +1,2 @@
1
+ export declare const DATABASE_SCHEMA_VERSION = 1;
2
+ export declare const DATABASE_SCHEMA = "\n CREATE TABLE IF NOT EXISTS operations (\n operation_id TEXT PRIMARY KEY,\n plugin_id TEXT NOT NULL,\n source TEXT NOT NULL,\n action TEXT NOT NULL,\n status TEXT NOT NULL,\n process_id TEXT,\n input_json TEXT NOT NULL,\n output_json TEXT NOT NULL,\n error_json TEXT NOT NULL,\n input_truncated INTEGER NOT NULL DEFAULT 0,\n output_truncated INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n finished_at TEXT,\n duration_ms INTEGER\n );\n CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS oauth_clients (\n client_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n redirect_uris_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS oauth_codes (\n code_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n redirect_uri TEXT NOT NULL,\n code_challenge TEXT NOT NULL,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n used_at TEXT\n );\n CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);\n\n CREATE TABLE IF NOT EXISTS web_sessions (\n token_hash TEXT PRIMARY KEY,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);\n\n CREATE TABLE IF NOT EXISTS passkeys (\n credential_id TEXT PRIMARY KEY,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL,\n transports_json TEXT NOT NULL,\n device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),\n backed_up INTEGER NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);\n\n CREATE TABLE IF NOT EXISTS computer_settings (\n id INTEGER PRIMARY KEY CHECK(id = 1),\n enabled INTEGER NOT NULL DEFAULT 0,\n remote_access INTEGER NOT NULL DEFAULT 1,\n updated_at TEXT NOT NULL\n );\n\n";
@@ -0,0 +1,85 @@
1
+ export const DATABASE_SCHEMA_VERSION = 1;
2
+ export const DATABASE_SCHEMA = `
3
+ CREATE TABLE IF NOT EXISTS operations (
4
+ operation_id TEXT PRIMARY KEY,
5
+ plugin_id TEXT NOT NULL,
6
+ source TEXT NOT NULL,
7
+ action TEXT NOT NULL,
8
+ status TEXT NOT NULL,
9
+ process_id TEXT,
10
+ input_json TEXT NOT NULL,
11
+ output_json TEXT NOT NULL,
12
+ error_json TEXT NOT NULL,
13
+ input_truncated INTEGER NOT NULL DEFAULT 0,
14
+ output_truncated INTEGER NOT NULL DEFAULT 0,
15
+ started_at TEXT NOT NULL,
16
+ finished_at TEXT,
17
+ duration_ms INTEGER
18
+ );
19
+ CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);
20
+ CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);
21
+ CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);
22
+
23
+ CREATE TABLE IF NOT EXISTS oauth_clients (
24
+ client_id TEXT PRIMARY KEY,
25
+ name TEXT NOT NULL,
26
+ redirect_uris_json TEXT NOT NULL,
27
+ created_at TEXT NOT NULL
28
+ );
29
+ CREATE TABLE IF NOT EXISTS oauth_codes (
30
+ code_hash TEXT PRIMARY KEY,
31
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
32
+ redirect_uri TEXT NOT NULL,
33
+ code_challenge TEXT NOT NULL,
34
+ scope TEXT NOT NULL,
35
+ expires_at TEXT NOT NULL,
36
+ used_at TEXT
37
+ );
38
+ CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);
39
+ CREATE TABLE IF NOT EXISTS oauth_tokens (
40
+ token_hash TEXT PRIMARY KEY,
41
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
42
+ scope TEXT NOT NULL,
43
+ expires_at TEXT NOT NULL,
44
+ revoked_at TEXT,
45
+ created_at TEXT NOT NULL
46
+ );
47
+ CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);
48
+ CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
49
+ token_hash TEXT PRIMARY KEY,
50
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
51
+ scope TEXT NOT NULL,
52
+ expires_at TEXT NOT NULL,
53
+ revoked_at TEXT,
54
+ created_at TEXT NOT NULL
55
+ );
56
+ CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);
57
+
58
+ CREATE TABLE IF NOT EXISTS web_sessions (
59
+ token_hash TEXT PRIMARY KEY,
60
+ expires_at TEXT NOT NULL,
61
+ created_at TEXT NOT NULL
62
+ );
63
+ CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);
64
+
65
+ CREATE TABLE IF NOT EXISTS passkeys (
66
+ credential_id TEXT PRIMARY KEY,
67
+ public_key BLOB NOT NULL,
68
+ counter INTEGER NOT NULL,
69
+ transports_json TEXT NOT NULL,
70
+ device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),
71
+ backed_up INTEGER NOT NULL,
72
+ name TEXT NOT NULL,
73
+ created_at TEXT NOT NULL,
74
+ last_used_at TEXT NOT NULL
75
+ );
76
+ CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);
77
+
78
+ CREATE TABLE IF NOT EXISTS computer_settings (
79
+ id INTEGER PRIMARY KEY CHECK(id = 1),
80
+ enabled INTEGER NOT NULL DEFAULT 0,
81
+ remote_access INTEGER NOT NULL DEFAULT 1,
82
+ updated_at TEXT NOT NULL
83
+ );
84
+
85
+ `;
@@ -1,4 +1,3 @@
1
- /** Express/HTTP transport host for REST, OAuth, MCP, plugins, and the built WebUI. */
2
1
  import { createServer } from "node:http";
3
2
  import path from "node:path";
4
3
  import { existsSync, readFileSync } from "node:fs";