@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.
- package/dist/app/application.d.ts +5 -0
- package/dist/app/application.js +55 -4
- package/dist/app/lifecycle.js +12 -0
- package/dist/app/workspace-access.d.ts +17 -4
- package/dist/core/operations/types.d.ts +1 -1
- package/dist/core/operations/types.js +1 -1
- package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
- package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
- package/dist/infrastructure/database/app-database.js +3 -3
- package/dist/infrastructure/database/migrate.d.ts +2 -0
- package/dist/infrastructure/database/migrate.js +83 -0
- package/dist/infrastructure/database/schema.d.ts +3 -1
- package/dist/infrastructure/database/schema.js +35 -6
- package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
- package/dist/infrastructure/database/workspace-repository.js +126 -0
- package/dist/mcp/custom/manager.d.ts +3 -1
- package/dist/mcp/custom/manager.js +37 -2
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
- package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
- package/dist/mcp/server/tool-control.d.ts +6 -1
- package/dist/mcp/server/tool-control.js +11 -2
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
- package/dist/plugins/agent/custom-mcp-store.js +38 -0
- package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
- package/dist/plugins/agent/mcp-gateway.js +110 -0
- package/dist/plugins/agent/mcp.js +3 -2
- package/dist/plugins/agent/plugin.js +1 -1
- package/dist/plugins/agent/provider.d.ts +1 -0
- package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
- package/dist/plugins/agent/providers/codex/launch.js +24 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
- package/dist/plugins/agent/providers/codex/provider.js +8 -2
- package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
- package/dist/plugins/agent/providers/codex/rpc.js +28 -3
- package/dist/plugins/agent/providers/index.d.ts +2 -1
- package/dist/plugins/agent/providers/index.js +5 -2
- package/dist/plugins/agent/repository.js +10 -7
- package/dist/plugins/agent/runtime-files.d.ts +4 -4
- package/dist/plugins/agent/runtime-files.js +18 -14
- package/dist/plugins/agent/runtime.d.ts +2 -0
- package/dist/plugins/agent/runtime.js +52 -13
- package/dist/plugins/agent/types.d.ts +3 -2
- package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
- package/dist/plugins/plugin-manager.d.ts +6 -1
- package/dist/plugins/plugin-manager.js +9 -2
- package/dist/plugins/types.d.ts +4 -0
- package/dist/plugins/web/http/agent-api-router.js +15 -1
- package/dist/plugins/web/http/git-api-router.js +13 -13
- package/dist/plugins/web/http/rtc-api-router.js +3 -0
- package/dist/plugins/web/http/runtime-api-router.js +22 -0
- package/dist/plugins/web/http/workspace-api-router.js +63 -12
- package/dist/plugins/web/plugin.js +1 -1
- package/dist/plugins/web/runtime.d.ts +3 -1
- package/dist/plugins/web/runtime.js +5 -1
- package/dist/plugins/workspace/mcp.js +20 -15
- package/dist/plugins/workspace/plugin.js +1 -1
- package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
- package/dist/plugins/workspace/workspace-root-policy.js +25 -19
- package/dist/plugins/workspace/workspace-service.d.ts +20 -8
- package/dist/plugins/workspace/workspace-service.js +270 -64
- package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
- package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
- package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
- 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
- package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
- package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
- package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
- package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
- package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
- package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
- package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
- package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
- package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
- package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
- package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
- package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
- package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
- package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
- package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
- package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
- package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
- package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
- package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
- package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
- package/dist/web/assets/index-9DLDvF8M.js +19 -0
- package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
- package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/transport-sw.js +43 -15
- package/package.json +1 -1
- package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
- package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
- package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
- package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
- package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
- package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
- package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
- package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
- package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
- package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
- package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
- package/dist/web/assets/index-CacDJSDn.js +0 -19
- 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;
|
package/dist/app/application.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/dist/app/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
+
listAll(): Promise<WorkspaceEntry[]>;
|
|
27
|
+
resolve(workspaceId: string): Promise<string>;
|
|
20
28
|
createProject(parentInput: string, nameInput: string): Promise<WorkspaceEntry>;
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
@@ -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 {
|
|
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;
|
|
12
|
-
this.raw
|
|
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,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
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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>;
|