@zhanguym/chatroom 1.2.0 → 1.3.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 (110) hide show
  1. package/dist/app/application.d.ts +5 -0
  2. package/dist/app/application.js +55 -4
  3. package/dist/app/lifecycle.js +12 -0
  4. package/dist/app/workspace-access.d.ts +17 -4
  5. package/dist/core/operations/types.d.ts +1 -1
  6. package/dist/core/operations/types.js +1 -1
  7. package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
  8. package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
  9. package/dist/infrastructure/database/app-database.js +3 -3
  10. package/dist/infrastructure/database/migrate.d.ts +2 -0
  11. package/dist/infrastructure/database/migrate.js +83 -0
  12. package/dist/infrastructure/database/schema.d.ts +3 -1
  13. package/dist/infrastructure/database/schema.js +35 -6
  14. package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
  15. package/dist/infrastructure/database/workspace-repository.js +126 -0
  16. package/dist/mcp/custom/manager.d.ts +3 -1
  17. package/dist/mcp/custom/manager.js +37 -2
  18. package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
  19. package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
  20. package/dist/mcp/server/tool-control.d.ts +6 -1
  21. package/dist/mcp/server/tool-control.js +11 -2
  22. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  23. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  24. package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
  25. package/dist/plugins/agent/custom-mcp-store.js +38 -0
  26. package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
  27. package/dist/plugins/agent/mcp-gateway.js +110 -0
  28. package/dist/plugins/agent/mcp.js +3 -2
  29. package/dist/plugins/agent/plugin.js +1 -1
  30. package/dist/plugins/agent/provider.d.ts +1 -0
  31. package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
  32. package/dist/plugins/agent/providers/codex/launch.js +24 -0
  33. package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
  34. package/dist/plugins/agent/providers/codex/provider.js +8 -2
  35. package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
  36. package/dist/plugins/agent/providers/codex/rpc.js +28 -3
  37. package/dist/plugins/agent/providers/index.d.ts +2 -1
  38. package/dist/plugins/agent/providers/index.js +5 -2
  39. package/dist/plugins/agent/repository.js +10 -7
  40. package/dist/plugins/agent/runtime-files.d.ts +4 -4
  41. package/dist/plugins/agent/runtime-files.js +18 -14
  42. package/dist/plugins/agent/runtime.d.ts +2 -0
  43. package/dist/plugins/agent/runtime.js +52 -13
  44. package/dist/plugins/agent/types.d.ts +3 -2
  45. package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
  46. package/dist/plugins/plugin-manager.d.ts +6 -1
  47. package/dist/plugins/plugin-manager.js +9 -2
  48. package/dist/plugins/types.d.ts +4 -0
  49. package/dist/plugins/web/http/agent-api-router.js +15 -1
  50. package/dist/plugins/web/http/git-api-router.js +13 -13
  51. package/dist/plugins/web/http/rtc-api-router.js +3 -0
  52. package/dist/plugins/web/http/runtime-api-router.js +22 -0
  53. package/dist/plugins/web/http/workspace-api-router.js +63 -12
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +5 -1
  57. package/dist/plugins/workspace/mcp.js +20 -15
  58. package/dist/plugins/workspace/plugin.js +1 -1
  59. package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
  60. package/dist/plugins/workspace/workspace-root-policy.js +25 -19
  61. package/dist/plugins/workspace/workspace-service.d.ts +20 -8
  62. package/dist/plugins/workspace/workspace-service.js +270 -64
  63. package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
  64. package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
  65. package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
  66. package/dist/web/assets/{CodeViewer.vue_vue_type_script_setup_true_lang-DK6xGH2G.js → CodeViewer.vue_vue_type_script_setup_true_lang-BtA5aWrS.js} +1 -1
  67. package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
  68. package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
  69. package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
  70. package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
  71. package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
  72. package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
  73. package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
  74. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
  75. package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
  76. package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
  77. package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
  78. package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
  79. package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
  80. package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
  81. package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
  82. package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
  83. package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
  84. package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
  85. package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
  86. package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
  87. package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
  88. package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
  89. package/dist/web/assets/index-9DLDvF8M.js +19 -0
  90. package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
  91. package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
  92. package/dist/web/index.html +1 -1
  93. package/dist/web/transport-sw.js +43 -15
  94. package/package.json +1 -1
  95. package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
  96. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
  97. package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
  98. package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
  99. package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
  100. package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
  101. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
  102. package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
  103. package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
  104. package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
  105. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
  106. package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
  107. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
  108. package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
  109. package/dist/web/assets/index-CacDJSDn.js +0 -19
  110. package/dist/web/assets/useOperationLabels-BNiE-bRq.js +0 -1
@@ -3,7 +3,9 @@ import { AppDatabase } from "#infrastructure/database/app-database";
3
3
  import { RuntimeEventBus } from "./event-bus.js";
4
4
  import { OperationLog } from "#operations/operation-log";
5
5
  import { PluginManager } from "#plugins/plugin-manager";
6
+ import { McpToolControl } from "#mcp/server/tool-control";
6
7
  import { CustomMcpManager } from "#mcp/custom/manager";
8
+ import { AgentMcpGateway } from "#plugins/agent/mcp-gateway";
7
9
  import { HttpServer } from "#infrastructure/http/http-server";
8
10
  import type { LogService } from "#core/logging/types";
9
11
  export interface ApplicationComponents {
@@ -12,6 +14,9 @@ export interface ApplicationComponents {
12
14
  operations: OperationLog;
13
15
  plugins: PluginManager;
14
16
  customMcp: CustomMcpManager;
17
+ agentCustomMcp: CustomMcpManager;
18
+ agentMcpGateway: AgentMcpGateway;
19
+ agentMcpTools: McpToolControl;
15
20
  processes: import("#plugins/process/process-supervisor").ProcessSupervisor;
16
21
  cloud: import("#plugins/cloud/controller").CloudController;
17
22
  computer: import("#plugins/computer/computer-service").ComputerService;
@@ -1,6 +1,7 @@
1
1
  import { AppDatabase } from "#infrastructure/database/app-database";
2
2
  import { OperationRepository } from "#infrastructure/database/operation-repository";
3
3
  import { McpToolSettingsRepository } from "#infrastructure/database/mcp-tool-settings-repository";
4
+ import { AgentMcpSettingsRepository } from "#infrastructure/database/agent-mcp-settings-repository";
4
5
  import { OAuthRepository } from "#infrastructure/database/oauth-repository";
5
6
  import { PasskeyRepository } from "#infrastructure/database/passkey-repository";
6
7
  import { WebSessionRepository } from "#infrastructure/database/web-session-repository";
@@ -22,25 +23,48 @@ import { createChatRoomMcpHandler } from "#mcp/server/create-mcp-server";
22
23
  import { McpToolControl } from "#mcp/server/tool-control";
23
24
  import { CustomMcpRepository } from "#infrastructure/database/custom-mcp-repository";
24
25
  import { CustomMcpManager } from "#mcp/custom/manager";
26
+ import { AgentCustomMcpStore } from "#plugins/agent/custom-mcp-store";
27
+ import { AgentMcpGateway, createAgentMcpServer, } from "#plugins/agent/mcp-gateway";
25
28
  import { HttpServer } from "#infrastructure/http/http-server";
26
29
  export async function createApplication(config, logs) {
27
30
  const database = new AppDatabase(config.databasePath);
31
+ let gatewayForCleanup = null;
32
+ let agentCustomMcpForCleanup = null;
33
+ let customMcpForCleanup = null;
34
+ let pluginsForCleanup = null;
28
35
  try {
29
36
  const eventBus = new RuntimeEventBus();
30
37
  const operations = new OperationLog(new OperationRepository(database), eventBus, config.operations.maxPayloadBytes);
31
38
  operations.reconcileInterrupted();
32
39
  const mcpTools = new McpToolControl(new McpToolSettingsRepository(database));
33
40
  const customMcp = new CustomMcpManager(new CustomMcpRepository(database), mcpTools, operations, config.dataDir);
41
+ customMcpForCleanup = customMcp;
42
+ const agentMcpSettings = new AgentMcpSettingsRepository(database);
43
+ const agentMcpTools = new McpToolControl(agentMcpSettings, false);
44
+ const agentCustomMcp = new CustomMcpManager(new AgentCustomMcpStore(new CustomMcpRepository(database), agentMcpSettings), agentMcpTools, operations, config.dataDir, "agent_mcp");
45
+ agentCustomMcpForCleanup = agentCustomMcp;
46
+ let plugins = null;
47
+ const agentMcpGateway = new AgentMcpGateway(() => createAgentMcpServer((server) => {
48
+ if (!plugins)
49
+ throw new Error("Agent MCP gateway is not initialized");
50
+ plugins.registerMcp(server, agentMcpTools, { agentMode: true });
51
+ agentCustomMcp.registerMcp(server);
52
+ }));
53
+ await agentMcpGateway.start();
54
+ gatewayForCleanup = agentMcpGateway;
34
55
  const externalAccess = new ExternalAccessRegistry(config.auth);
35
56
  const auth = new AuthService(new OAuthRepository(database), new WebSessionRepository(database), config.auth);
36
57
  const passkeys = new PasskeyService(new PasskeyRepository(database));
37
58
  const services = new ServiceRegistry();
38
- const plugins = new PluginManager({
59
+ plugins = new PluginManager({
39
60
  config,
40
61
  database,
41
62
  operations,
42
63
  mcpTools,
43
64
  customMcp,
65
+ agentMcp: agentMcpGateway.connection(),
66
+ agentMcpTools,
67
+ agentCustomMcp,
44
68
  events: eventBus,
45
69
  externalAccess,
46
70
  services,
@@ -54,20 +78,27 @@ export async function createApplication(config, logs) {
54
78
  createAgentPlugin(),
55
79
  createWebPlugin(),
56
80
  ]);
81
+ pluginsForCleanup = plugins;
57
82
  await plugins.start();
83
+ plugins.defineMcp(agentMcpTools, { agentMode: true });
58
84
  customMcp.start();
85
+ agentCustomMcp.start();
86
+ const activePlugins = plugins;
59
87
  const web = services.require(WebServiceToken);
60
88
  const processes = services.require(ProcessServiceToken);
61
89
  const cloud = services.require(CloudServiceToken);
62
90
  const computer = services.require(ComputerServiceToken);
63
- const mcp = createChatRoomMcpHandler(plugins, customMcp);
91
+ const mcp = createChatRoomMcpHandler(activePlugins, customMcp);
64
92
  const http = new HttpServer(config, web.application, eventBus, auth, passkeys, mcp, externalAccess, cloud, logs);
65
93
  return {
66
94
  database,
67
95
  eventBus,
68
96
  operations,
69
- plugins,
97
+ plugins: activePlugins,
70
98
  customMcp,
99
+ agentCustomMcp,
100
+ agentMcpGateway,
101
+ agentMcpTools,
71
102
  processes,
72
103
  cloud,
73
104
  computer,
@@ -76,7 +107,27 @@ export async function createApplication(config, logs) {
76
107
  };
77
108
  }
78
109
  catch (error) {
79
- database.close();
110
+ // Startup can fail after the loopback gateway has bound its ephemeral port.
111
+ // Tear down resources in reverse construction order before closing the DB.
112
+ for (const cleanup of [
113
+ () => pluginsForCleanup?.stop(),
114
+ () => gatewayForCleanup?.close(),
115
+ () => agentCustomMcpForCleanup?.close(),
116
+ () => customMcpForCleanup?.close(),
117
+ ]) {
118
+ try {
119
+ await cleanup();
120
+ }
121
+ catch {
122
+ /* preserve the startup error */
123
+ }
124
+ }
125
+ try {
126
+ database.close();
127
+ }
128
+ catch {
129
+ /* preserve the startup error */
130
+ }
80
131
  throw error;
81
132
  }
82
133
  }
@@ -111,6 +111,18 @@ async function cleanupComponents(components) {
111
111
  catch (error) {
112
112
  errors.push(error);
113
113
  }
114
+ try {
115
+ await components.agentMcpGateway.close();
116
+ }
117
+ catch (error) {
118
+ errors.push(error);
119
+ }
120
+ try {
121
+ await components.agentCustomMcp.close();
122
+ }
123
+ catch (error) {
124
+ errors.push(error);
125
+ }
114
126
  try {
115
127
  await components.customMcp.close();
116
128
  }
@@ -1,7 +1,14 @@
1
+ export type WorkspaceStatus = "available" | "missing" | "restore_pending" | "unregistered";
2
+ export type WorkspaceSource = "configured" | "manual" | "both" | "history";
1
3
  export interface WorkspaceEntry {
2
- root: string;
4
+ id: string;
5
+ path: string;
3
6
  name: string;
4
7
  summary: string | null;
8
+ status: WorkspaceStatus;
9
+ source: WorkspaceSource;
10
+ manuallyRegistered: boolean;
11
+ discovered: boolean;
5
12
  }
6
13
  export interface WorkspaceSkill {
7
14
  name: string;
@@ -16,9 +23,15 @@ export interface WorkspaceInfo extends WorkspaceEntry {
16
23
  export interface WorkspaceAccess {
17
24
  roots(): string[];
18
25
  list(): Promise<WorkspaceEntry[]>;
19
- resolve(input: string): Promise<string>;
26
+ listAll(): Promise<WorkspaceEntry[]>;
27
+ resolve(workspaceId: string): Promise<string>;
20
28
  createProject(parentInput: string, nameInput: string): Promise<WorkspaceEntry>;
21
- projectFiles(input: string, maxFiles?: number): Promise<string[]>;
22
- info(input: string): Promise<WorkspaceInfo>;
29
+ registerProject(pathInput: string, nameInput?: string | null): Promise<WorkspaceEntry>;
30
+ renameProject(workspaceId: string, nameInput: string): Promise<WorkspaceEntry>;
31
+ removeProject(workspaceId: string): Promise<void>;
32
+ cleanupProject(workspaceId: string): Promise<void>;
33
+ restoreProject(workspaceId: string): Promise<WorkspaceEntry>;
34
+ projectFiles(workspaceId: string, maxFiles?: number): Promise<string[]>;
35
+ info(workspaceId: string): Promise<WorkspaceInfo>;
23
36
  }
24
37
  export declare const WorkspaceAccessToken: import("#app/service-registry").ServiceToken<WorkspaceAccess>;
@@ -1,6 +1,6 @@
1
1
  declare const OPERATION_STATUSES: readonly ["running", "success", "error", "cancelled"];
2
2
  export type OperationStatus = (typeof OPERATION_STATUSES)[number];
3
- declare const OPERATION_SOURCES: readonly ["mcp", "gui", "system", "cli"];
3
+ declare const OPERATION_SOURCES: readonly ["mcp", "agent_mcp", "gui", "system", "cli"];
4
4
  export type OperationSource = (typeof OPERATION_SOURCES)[number];
5
5
  export interface Operation {
6
6
  operationId: string;
@@ -4,5 +4,5 @@ const OPERATION_STATUSES = [
4
4
  "error",
5
5
  "cancelled",
6
6
  ];
7
- const OPERATION_SOURCES = ["mcp", "gui", "system", "cli"];
7
+ const OPERATION_SOURCES = ["mcp", "agent_mcp", "gui", "system", "cli"];
8
8
  export {};
@@ -0,0 +1,12 @@
1
+ import type { AppDatabase } from "./app-database.js";
2
+ import type { McpToolSettingStore } from "#mcp/server/tool-control";
3
+ export declare class AgentMcpSettingsRepository implements McpToolSettingStore {
4
+ private readonly database;
5
+ constructor(database: AppDatabase);
6
+ disabledTools(): string[];
7
+ configuredTools(): string[];
8
+ setEnabled(name: string, enabled: boolean): void;
9
+ serverEnabled(serverId: string): boolean;
10
+ setServerEnabled(serverId: string, enabled: boolean): void;
11
+ deleteServerSetting(serverId: string): void;
12
+ }
@@ -0,0 +1,55 @@
1
+ export class AgentMcpSettingsRepository {
2
+ database;
3
+ constructor(database) {
4
+ this.database = database;
5
+ }
6
+ disabledTools() {
7
+ return this.database.raw
8
+ .prepare("SELECT tool_name FROM agent_mcp_tool_settings WHERE enabled=0")
9
+ .all().map((row) => row.tool_name);
10
+ }
11
+ configuredTools() {
12
+ return this.database.raw
13
+ .prepare("SELECT tool_name FROM agent_mcp_tool_settings")
14
+ .all().map((row) => row.tool_name);
15
+ }
16
+ setEnabled(name, enabled) {
17
+ this.database.raw
18
+ .prepare(`
19
+ INSERT INTO agent_mcp_tool_settings(tool_name, enabled, updated_at)
20
+ VALUES(?,?,?)
21
+ ON CONFLICT(tool_name) DO UPDATE SET
22
+ enabled=excluded.enabled,
23
+ updated_at=excluded.updated_at
24
+ `)
25
+ .run(name, enabled ? 1 : 0, new Date().toISOString());
26
+ }
27
+ serverEnabled(serverId) {
28
+ const row = this.database.raw
29
+ .prepare("SELECT enabled FROM agent_mcp_server_settings WHERE server_id=?")
30
+ .get(serverId);
31
+ return row ? Boolean(row.enabled) : false;
32
+ }
33
+ setServerEnabled(serverId, enabled) {
34
+ this.database.raw
35
+ .prepare(`
36
+ INSERT INTO agent_mcp_server_settings(server_id, enabled, updated_at)
37
+ VALUES(?,?,?)
38
+ ON CONFLICT(server_id) DO UPDATE SET
39
+ enabled=excluded.enabled,
40
+ updated_at=excluded.updated_at
41
+ `)
42
+ .run(serverId, enabled ? 1 : 0, new Date().toISOString());
43
+ }
44
+ deleteServerSetting(serverId) {
45
+ this.database.raw
46
+ .prepare("DELETE FROM agent_mcp_server_settings WHERE server_id=?")
47
+ .run(serverId);
48
+ if (serverId.startsWith("custom:")) {
49
+ const id = serverId.slice("custom:".length);
50
+ this.database.raw
51
+ .prepare("DELETE FROM agent_mcp_tool_settings WHERE tool_name GLOB ?")
52
+ .run("custom_" + id + "__*");
53
+ }
54
+ }
55
+ }
@@ -1,15 +1,15 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
- import { DATABASE_SCHEMA } from "./schema.js";
4
+ import { initializeDatabase } from "./migrate.js";
5
5
  export class AppDatabase {
6
6
  raw;
7
7
  constructor(databasePath) {
8
8
  mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
9
9
  this.raw = new DatabaseSync(databasePath);
10
10
  try {
11
- this.raw.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;");
12
- this.raw.exec(DATABASE_SCHEMA);
11
+ this.raw.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
12
+ initializeDatabase(this.raw);
13
13
  }
14
14
  catch (error) {
15
15
  this.raw.close();
@@ -0,0 +1,2 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ export declare function initializeDatabase(database: DatabaseSync): void;
@@ -0,0 +1,83 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { agentSessionsSchema, DATABASE_SCHEMA, WORKSPACES_SCHEMA, } from "./schema.js";
3
+ const SCHEMA_VERSION = 1;
4
+ const LEGACY_SESSION_COLUMNS = [
5
+ "id",
6
+ "workspace_root",
7
+ "provider",
8
+ "status",
9
+ "created_at",
10
+ "updated_at",
11
+ ];
12
+ export function initializeDatabase(database) {
13
+ const version = database.prepare("PRAGMA user_version").get().user_version;
14
+ const columns = database
15
+ .prepare("PRAGMA table_info(agent_sessions)")
16
+ .all();
17
+ const legacySessions = columns.some((column) => column.name === "workspace_root");
18
+ const recognizedLegacySessions = columns.length === LEGACY_SESSION_COLUMNS.length &&
19
+ columns.every((column, index) => column.name === LEGACY_SESSION_COLUMNS[index]);
20
+ // Older installations can have user_version=2 while still retaining the
21
+ // original 1.2.0 agent_sessions layout. Accept that specific layout only;
22
+ // never downgrade an unknown newer database.
23
+ if (version > SCHEMA_VERSION && !(version === 2 && recognizedLegacySessions))
24
+ throw new Error(`Unsupported newer ChatRoom database schema: ${version}`);
25
+ if (legacySessions && !recognizedLegacySessions)
26
+ throw new Error("Unrecognized ChatRoom agent_sessions schema");
27
+ if (columns.length > 0 &&
28
+ !legacySessions &&
29
+ !columns.some((column) => column.name === "workspace_id"))
30
+ throw new Error("Unrecognized ChatRoom agent_sessions schema");
31
+ // Rebuilding a referenced table requires foreign keys to be disabled before
32
+ // entering the transaction. The transaction keeps the old database intact
33
+ // if any migration or schema initialization step fails.
34
+ database.exec(`PRAGMA foreign_keys = ${legacySessions ? "OFF" : "ON"};`);
35
+ try {
36
+ database.exec("BEGIN IMMEDIATE");
37
+ try {
38
+ if (legacySessions)
39
+ migrateLegacySessions(database);
40
+ database.exec(DATABASE_SCHEMA);
41
+ if (legacySessions) {
42
+ const invalidReferences = database
43
+ .prepare("PRAGMA foreign_key_check")
44
+ .all();
45
+ if (invalidReferences.length)
46
+ throw new Error("Database migration failed foreign-key validation");
47
+ }
48
+ database.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
49
+ database.exec("COMMIT");
50
+ }
51
+ catch (error) {
52
+ database.exec("ROLLBACK");
53
+ throw error;
54
+ }
55
+ }
56
+ finally {
57
+ database.exec("PRAGMA foreign_keys = ON;");
58
+ }
59
+ }
60
+ function migrateLegacySessions(database) {
61
+ database.exec(WORKSPACES_SCHEMA);
62
+ const roots = database
63
+ .prepare("SELECT DISTINCT workspace_root FROM agent_sessions")
64
+ .all();
65
+ const insertWorkspace = database.prepare("INSERT INTO workspaces(id, path, manually_registered, state) VALUES(?, ?, 1, 'available') ON CONFLICT(path) DO NOTHING");
66
+ for (const { workspace_root } of roots)
67
+ insertWorkspace.run(`ws_${randomUUID()}`, workspace_root);
68
+ database.exec(agentSessionsSchema("agent_sessions_next"));
69
+ database.exec(`
70
+ INSERT INTO agent_sessions_next(id, workspace_id, provider, status, created_at, updated_at)
71
+ SELECT s.id, w.id, s.provider, s.status, s.created_at, s.updated_at
72
+ FROM agent_sessions s
73
+ JOIN workspaces w ON w.path = s.workspace_root;
74
+ `);
75
+ const oldCount = database.prepare("SELECT COUNT(*) AS count FROM agent_sessions").get().count;
76
+ const newCount = database
77
+ .prepare("SELECT COUNT(*) AS count FROM agent_sessions_next")
78
+ .get().count;
79
+ if (oldCount !== newCount)
80
+ throw new Error("Database migration would lose Agent sessions");
81
+ database.exec("DROP TABLE agent_sessions;");
82
+ database.exec("ALTER TABLE agent_sessions_next RENAME TO agent_sessions;");
83
+ }
@@ -1 +1,3 @@
1
- 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 CREATE TABLE IF NOT EXISTS mcp_tool_settings (\n tool_name TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS custom_mcp_servers (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n config_json TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_sessions (\n id TEXT PRIMARY KEY,\n workspace_root TEXT NOT NULL,\n provider TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('idle','running','waiting_input','error')),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS agent_sessions_updated_idx ON agent_sessions(updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_turns (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,\n provider_turn_id TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n reasoning_effort TEXT,\n reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),\n service_tier TEXT,\n approval_policy_json TEXT,\n approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),\n permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),\n usage_json TEXT,\n origin INTEGER NOT NULL CHECK(origin IN (0,1)),\n status TEXT NOT NULL CHECK(status IN ('running','completed','interrupted','failed')),\n error_json TEXT NOT NULL,\n started_at TEXT NOT NULL,\n completed_at TEXT\n );\n CREATE INDEX IF NOT EXISTS agent_turns_session_idx ON agent_turns(session_id, started_at ASC);\n\n CREATE TABLE IF NOT EXISTS agent_items (\n id TEXT PRIMARY KEY,\n turn_id TEXT NOT NULL REFERENCES agent_turns(id) ON DELETE CASCADE,\n provider_item_id TEXT,\n type TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('running','completed','waiting','interrupted','failed')),\n payload_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS agent_items_turn_idx ON agent_items(turn_id, sequence ASC);\n\n CREATE TABLE IF NOT EXISTS agent_session_providers (\n session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n provider_session_id TEXT NOT NULL,\n model TEXT,\n reasoning_effort TEXT,\n reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),\n service_tier TEXT,\n approval_policy_json TEXT,\n approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),\n permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),\n token_usage_json TEXT,\n continuity_context TEXT NOT NULL DEFAULT '',\n synced_through_turn_id TEXT REFERENCES agent_turns(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY(session_id, provider)\n );\n";
1
+ export declare const WORKSPACES_SCHEMA = "\n CREATE TABLE IF NOT EXISTS workspaces (\n id TEXT PRIMARY KEY,\n path TEXT NOT NULL UNIQUE,\n name_override TEXT,\n manually_registered INTEGER NOT NULL DEFAULT 0 CHECK(manually_registered IN (0, 1)),\n discovered_from TEXT,\n state TEXT NOT NULL DEFAULT 'available' CHECK(state IN ('available', 'missing', 'restore_pending'))\n );\n";
2
+ export declare function agentSessionsSchema(table: "agent_sessions" | "agent_sessions_next"): string;
3
+ export declare const DATABASE_SCHEMA: string;
@@ -1,3 +1,25 @@
1
+ export const WORKSPACES_SCHEMA = `
2
+ CREATE TABLE IF NOT EXISTS workspaces (
3
+ id TEXT PRIMARY KEY,
4
+ path TEXT NOT NULL UNIQUE,
5
+ name_override TEXT,
6
+ manually_registered INTEGER NOT NULL DEFAULT 0 CHECK(manually_registered IN (0, 1)),
7
+ discovered_from TEXT,
8
+ state TEXT NOT NULL DEFAULT 'available' CHECK(state IN ('available', 'missing', 'restore_pending'))
9
+ );
10
+ `;
11
+ export function agentSessionsSchema(table) {
12
+ return `
13
+ CREATE TABLE IF NOT EXISTS ${table} (
14
+ id TEXT PRIMARY KEY,
15
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
16
+ provider TEXT NOT NULL,
17
+ status TEXT NOT NULL CHECK(status IN ('idle','running','waiting_input','error')),
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL
20
+ );
21
+ `;
22
+ }
1
23
  export const DATABASE_SCHEMA = `
2
24
  CREATE TABLE IF NOT EXISTS operations (
3
25
  operation_id TEXT PRIMARY KEY,
@@ -96,15 +118,22 @@ export const DATABASE_SCHEMA = `
96
118
  updated_at TEXT NOT NULL
97
119
  );
98
120
 
99
- CREATE TABLE IF NOT EXISTS agent_sessions (
100
- id TEXT PRIMARY KEY,
101
- workspace_root TEXT NOT NULL,
102
- provider TEXT NOT NULL,
103
- status TEXT NOT NULL CHECK(status IN ('idle','running','waiting_input','error')),
104
- created_at TEXT NOT NULL,
121
+ CREATE TABLE IF NOT EXISTS agent_mcp_tool_settings (
122
+ tool_name TEXT PRIMARY KEY,
123
+ enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),
105
124
  updated_at TEXT NOT NULL
106
125
  );
126
+
127
+ CREATE TABLE IF NOT EXISTS agent_mcp_server_settings (
128
+ server_id TEXT PRIMARY KEY,
129
+ enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),
130
+ updated_at TEXT NOT NULL
131
+ );
132
+
133
+ ${WORKSPACES_SCHEMA}
134
+ ${agentSessionsSchema("agent_sessions")}
107
135
  CREATE INDEX IF NOT EXISTS agent_sessions_updated_idx ON agent_sessions(updated_at DESC);
136
+ CREATE INDEX IF NOT EXISTS agent_sessions_workspace_idx ON agent_sessions(workspace_id);
108
137
 
109
138
  CREATE TABLE IF NOT EXISTS agent_turns (
110
139
  id TEXT PRIMARY KEY,
@@ -0,0 +1,28 @@
1
+ import type { AppDatabase } from "./app-database.js";
2
+ export type WorkspaceState = "available" | "missing" | "restore_pending";
3
+ export interface WorkspaceRecord {
4
+ id: string;
5
+ path: string;
6
+ name_override: string | null;
7
+ manually_registered: number;
8
+ discovered_from: string | null;
9
+ state: WorkspaceState;
10
+ }
11
+ export declare class WorkspaceRepository {
12
+ private readonly database;
13
+ constructor(database: AppDatabase);
14
+ list(): WorkspaceRecord[];
15
+ get(id: string): WorkspaceRecord | null;
16
+ getByPath(workspacePath: string): WorkspaceRecord | null;
17
+ syncDiscoveredRoot(root: string, discoveredPaths: readonly string[]): void;
18
+ registerDiscoveredPath(workspacePath: string, root: string, requireRestoreConfirmation: boolean): WorkspaceRecord;
19
+ clearRemovedDiscoveryRoots(roots: readonly string[]): void;
20
+ registerManual(workspacePath: string, name: string | null): WorkspaceRecord;
21
+ rename(id: string, name: string): WorkspaceRecord | null;
22
+ removeManual(id: string): WorkspaceRecord | null;
23
+ hasActiveSessions(id: string): boolean;
24
+ cleanup(id: string): void;
25
+ markMissing(id: string): void;
26
+ markRestorePending(id: string): void;
27
+ confirmRestore(id: string): WorkspaceRecord | null;
28
+ }
@@ -0,0 +1,126 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ export class WorkspaceRepository {
4
+ database;
5
+ constructor(database) {
6
+ this.database = database;
7
+ }
8
+ list() {
9
+ const rows = this.database.raw
10
+ .prepare("SELECT * FROM workspaces")
11
+ .all();
12
+ return rows.sort((a, b) => displayName(a).localeCompare(displayName(b), undefined, {
13
+ sensitivity: "base",
14
+ }) || a.path.localeCompare(b.path));
15
+ }
16
+ get(id) {
17
+ return (this.database.raw
18
+ .prepare("SELECT * FROM workspaces WHERE id=?")
19
+ .get(id) ?? null);
20
+ }
21
+ getByPath(workspacePath) {
22
+ return (this.database.raw
23
+ .prepare("SELECT * FROM workspaces WHERE path=?")
24
+ .get(workspacePath) ?? null);
25
+ }
26
+ syncDiscoveredRoot(root, discoveredPaths) {
27
+ const paths = new Set(discoveredPaths);
28
+ const updateMissing = this.database.raw.prepare("UPDATE workspaces SET discovered_from=CASE WHEN manually_registered=1 THEN NULL ELSE discovered_from END, state=CASE WHEN manually_registered=1 THEN state ELSE 'missing' END WHERE id=?");
29
+ for (const row of this.list()) {
30
+ if (row.discovered_from === root && !paths.has(row.path))
31
+ updateMissing.run(row.id);
32
+ }
33
+ for (const workspacePath of paths)
34
+ this.registerDiscoveredPath(workspacePath, root, true);
35
+ }
36
+ registerDiscoveredPath(workspacePath, root, requireRestoreConfirmation) {
37
+ const existing = this.getByPath(workspacePath);
38
+ if (!existing) {
39
+ const id = "ws_" + randomUUID();
40
+ this.database.raw
41
+ .prepare("INSERT INTO workspaces(id, path, manually_registered, discovered_from, state) VALUES(?,?,0,?,'available')")
42
+ .run(id, workspacePath, root);
43
+ return this.get(id);
44
+ }
45
+ const wasUnavailable = existing.state !== "available";
46
+ const state = requireRestoreConfirmation && wasUnavailable
47
+ ? "restore_pending"
48
+ : "available";
49
+ this.database.raw
50
+ .prepare("UPDATE workspaces SET discovered_from=?, state=? WHERE id=?")
51
+ .run(root, state, existing.id);
52
+ return this.get(existing.id);
53
+ }
54
+ clearRemovedDiscoveryRoots(roots) {
55
+ const configured = new Set(roots);
56
+ const update = this.database.raw.prepare("UPDATE workspaces SET discovered_from=NULL, state=CASE WHEN state='restore_pending' THEN 'missing' ELSE state END WHERE id=?");
57
+ for (const row of this.list()) {
58
+ if (row.discovered_from && !configured.has(row.discovered_from))
59
+ update.run(row.id);
60
+ }
61
+ }
62
+ registerManual(workspacePath, name) {
63
+ const existing = this.getByPath(workspacePath);
64
+ if (existing) {
65
+ this.database.raw
66
+ .prepare("UPDATE workspaces SET name_override=COALESCE(?, name_override), manually_registered=1, state='available' WHERE id=?")
67
+ .run(name, existing.id);
68
+ return this.get(existing.id);
69
+ }
70
+ const id = "ws_" + randomUUID();
71
+ this.database.raw
72
+ .prepare("INSERT INTO workspaces(id, path, name_override, manually_registered, state) VALUES(?,?,?,?, 'available')")
73
+ .run(id, workspacePath, name, 1);
74
+ return this.get(id);
75
+ }
76
+ rename(id, name) {
77
+ this.database.raw
78
+ .prepare("UPDATE workspaces SET name_override=? WHERE id=?")
79
+ .run(name, id);
80
+ return this.get(id);
81
+ }
82
+ removeManual(id) {
83
+ this.database.raw
84
+ .prepare("UPDATE workspaces SET manually_registered=0, state=CASE WHEN state='restore_pending' THEN 'missing' ELSE state END WHERE id=?")
85
+ .run(id);
86
+ return this.get(id);
87
+ }
88
+ hasActiveSessions(id) {
89
+ return Boolean(this.database.raw
90
+ .prepare("SELECT 1 FROM agent_sessions WHERE workspace_id=? AND status IN ('running','waiting_input') LIMIT 1")
91
+ .get(id));
92
+ }
93
+ cleanup(id) {
94
+ this.database.raw.exec("BEGIN IMMEDIATE");
95
+ try {
96
+ this.database.raw
97
+ .prepare("DELETE FROM agent_sessions WHERE workspace_id=?")
98
+ .run(id);
99
+ this.database.raw.prepare("DELETE FROM workspaces WHERE id=?").run(id);
100
+ this.database.raw.exec("COMMIT");
101
+ }
102
+ catch (error) {
103
+ this.database.raw.exec("ROLLBACK");
104
+ throw error;
105
+ }
106
+ }
107
+ markMissing(id) {
108
+ this.database.raw
109
+ .prepare("UPDATE workspaces SET state='missing' WHERE id=?")
110
+ .run(id);
111
+ }
112
+ markRestorePending(id) {
113
+ this.database.raw
114
+ .prepare("UPDATE workspaces SET state='restore_pending' WHERE id=? AND state='missing'")
115
+ .run(id);
116
+ }
117
+ confirmRestore(id) {
118
+ this.database.raw
119
+ .prepare("UPDATE workspaces SET state='available' WHERE id=? AND (manually_registered=1 OR discovered_from IS NOT NULL)")
120
+ .run(id);
121
+ return this.get(id);
122
+ }
123
+ }
124
+ function displayName(record) {
125
+ return record.name_override ?? (path.basename(record.path) || record.path);
126
+ }
@@ -6,11 +6,12 @@ export declare class CustomMcpManager {
6
6
  private readonly repository;
7
7
  private readonly control;
8
8
  private readonly operations;
9
+ private readonly operationSource;
9
10
  private readonly oauth;
10
11
  private readonly authorizationFlows;
11
12
  private readonly entries;
12
13
  private closed;
13
- constructor(repository: CustomMcpStore, control: McpToolControl, operations: OperationLog, dataDir?: string);
14
+ constructor(repository: CustomMcpStore, control: McpToolControl, operations: OperationLog, dataDir?: string, operationSource?: "mcp" | "agent_mcp");
14
15
  start(): void;
15
16
  list(): CustomMcpServerSummary[];
16
17
  create(input: Omit<CustomMcpConfig, "id">): Promise<CustomMcpServerSummary>;
@@ -48,6 +49,7 @@ export declare class CustomMcpManager {
48
49
  private forgetTools;
49
50
  private disconnect;
50
51
  private dispose;
52
+ private syncRepository;
51
53
  private require;
52
54
  private summary;
53
55
  close(): Promise<void>;