@cloudcli-ai/cloudcli 1.34.0 → 1.35.1
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/README.de.md +9 -1
- package/README.ja.md +8 -0
- package/README.ko.md +9 -1
- package/README.md +23 -5
- package/README.ru.md +9 -1
- package/README.tr.md +8 -1
- package/README.zh-CN.md +9 -1
- package/README.zh-TW.md +9 -1
- package/dist/api-docs.html +36 -18
- package/dist/assets/index-BvX30NW5.css +32 -0
- package/dist/assets/index-D1IdCTT6.js +1418 -0
- package/dist/index.html +12 -4
- package/dist-server/server/browser-use-mcp.js +350 -0
- package/dist-server/server/browser-use-mcp.js.map +1 -0
- package/dist-server/server/claude-sdk.js +25 -5
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +14 -6
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/cursor-cli.js +27 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +17 -2
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +136 -40
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/load-env.js +1 -1
- package/dist-server/server/load-env.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
- package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
- package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
- package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +23 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
- package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +26 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
- package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/notifications/index.js +4 -0
- package/dist-server/server/modules/notifications/index.js.map +1 -0
- package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
- package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
- package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
- package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
- package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
- package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +1 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +123 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/skills.service.js +11 -0
- package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +1 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +5 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
- package/dist-server/server/openai-codex.js +24 -14
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +21 -8
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +19 -1
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +1 -176
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
- package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
- package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
- package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +73 -1
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/voice-proxy.js +226 -0
- package/dist-server/server/voice-proxy.js.map +1 -0
- package/electron/assets/logo-macos.icns +0 -0
- package/electron/assets/logo-macos.png +0 -0
- package/electron/assets/logo-windows.ico +0 -0
- package/electron/cloud.js +260 -0
- package/electron/desktopNotifications.js +378 -0
- package/electron/desktopWindow.js +766 -0
- package/electron/launcher/index.html +14 -0
- package/electron/launcher/launcher.css +801 -0
- package/electron/launcher/launcher.js +687 -0
- package/electron/localServer.js +550 -0
- package/electron/main.js +944 -0
- package/electron/preload.cjs +60 -0
- package/electron/scripts/generate-macos-icon.js +62 -0
- package/electron/serverInstaller.js +277 -0
- package/electron/tabs.js +87 -0
- package/electron/viewHost.js +331 -0
- package/package.json +79 -3
- package/public/api-docs.html +36 -18
- package/scripts/release/build-server-bundle.js +176 -0
- package/scripts/release/prepare-desktop-app.js +152 -0
- package/server/browser-use-mcp.ts +384 -0
- package/server/claude-sdk.js +27 -5
- package/server/cli.js +15 -6
- package/server/cursor-cli.js +27 -11
- package/server/gemini-cli.js +17 -2
- package/server/index.js +137 -47
- package/server/load-env.js +1 -1
- package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
- package/server/modules/browser-use/browser-use.routes.ts +96 -0
- package/server/modules/browser-use/browser-use.service.ts +836 -0
- package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
- package/server/modules/database/index.ts +1 -0
- package/server/modules/database/migrations.ts +25 -0
- package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
- package/server/modules/database/repositories/notification-preferences.ts +12 -1
- package/server/modules/database/repositories/sessions.db.ts +227 -23
- package/server/modules/database/schema.ts +27 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
- package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
- package/server/modules/notifications/index.ts +13 -0
- package/server/modules/notifications/notifications.routes.ts +127 -0
- package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
- package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
- package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
- package/server/modules/projects/services/project-management.service.ts +0 -8
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
- package/server/modules/providers/README.md +1 -1
- package/server/modules/providers/index.ts +1 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
- package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
- package/server/modules/providers/provider.routes.ts +167 -1
- package/server/modules/providers/services/mcp.service.ts +26 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
- package/server/modules/providers/services/provider-models.service.ts +1 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
- package/server/modules/providers/services/sessions.service.ts +79 -4
- package/server/modules/providers/services/skills.service.ts +25 -1
- package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
- package/server/modules/providers/tests/skills.test.ts +212 -0
- package/server/modules/websocket/README.md +33 -27
- package/server/modules/websocket/index.ts +1 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
- package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
- package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
- package/server/modules/websocket/services/websocket-server.service.ts +6 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
- package/server/openai-codex.js +24 -14
- package/server/opencode-cli.js +21 -8
- package/server/opencode-cli.test.js +20 -1
- package/server/routes/settings.js +6 -1
- package/server/services/notification-orchestrator.js +3 -224
- package/server/services/tests/notification-orchestrator.test.js +80 -0
- package/server/shared/interfaces.ts +15 -0
- package/server/shared/tests/slice-tail-page.test.ts +42 -0
- package/server/shared/types.ts +84 -1
- package/server/shared/utils.ts +95 -1
- package/server/voice-proxy.js +224 -0
- package/dist/assets/index-73Xw3i9v.css +0 -32
- package/dist/assets/index-BIHaviaA.js +0 -1361
- package/dist/modelConstants.js +0 -854
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
- package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
- package/public/modelConstants.js +0 -854
- /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
- /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
- /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
|
@@ -69,6 +69,23 @@ CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
|
69
69
|
);
|
|
70
70
|
`;
|
|
71
71
|
|
|
72
|
+
export const NOTIFICATION_CHANNEL_ENDPOINTS_TABLE_SCHEMA_SQL = `
|
|
73
|
+
CREATE TABLE IF NOT EXISTS notification_channel_endpoints (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
user_id INTEGER NOT NULL,
|
|
76
|
+
channel TEXT NOT NULL,
|
|
77
|
+
endpoint_id TEXT NOT NULL,
|
|
78
|
+
label TEXT,
|
|
79
|
+
metadata_json TEXT,
|
|
80
|
+
enabled BOOLEAN DEFAULT 1,
|
|
81
|
+
last_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
82
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
83
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
84
|
+
UNIQUE(user_id, channel, endpoint_id),
|
|
85
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
86
|
+
);
|
|
87
|
+
`;
|
|
88
|
+
|
|
72
89
|
export const PROJECTS_TABLE_SCHEMA_SQL = `
|
|
73
90
|
CREATE TABLE IF NOT EXISTS projects (
|
|
74
91
|
project_id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -83,6 +100,12 @@ export const SESSIONS_TABLE_SCHEMA_SQL = `
|
|
|
83
100
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
84
101
|
session_id TEXT NOT NULL,
|
|
85
102
|
provider TEXT NOT NULL DEFAULT 'claude',
|
|
103
|
+
-- The session id used by the provider CLI/SDK on disk (JSONL file name,
|
|
104
|
+
-- store.db folder, sqlite row id, ...). \`session_id\` is the stable
|
|
105
|
+
-- app-facing id that the frontend uses for the whole session lifetime;
|
|
106
|
+
-- \`provider_session_id\` is filled in once the provider announces its own
|
|
107
|
+
-- id mid-run, or equals \`session_id\` for sessions discovered on disk.
|
|
108
|
+
provider_session_id TEXT,
|
|
86
109
|
custom_name TEXT,
|
|
87
110
|
project_path TEXT,
|
|
88
111
|
jsonl_path TEXT,
|
|
@@ -138,6 +161,10 @@ ${VAPID_KEYS_TABLE_SCHEMA_SQL}
|
|
|
138
161
|
${PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL}
|
|
139
162
|
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id);
|
|
140
163
|
|
|
164
|
+
${NOTIFICATION_CHANNEL_ENDPOINTS_TABLE_SCHEMA_SQL}
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_notification_channel_endpoints_user_channel ON notification_channel_endpoints(user_id, channel);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_notification_channel_endpoints_enabled ON notification_channel_endpoints(enabled);
|
|
167
|
+
|
|
141
168
|
${PROJECTS_TABLE_SCHEMA_SQL}
|
|
142
169
|
-- NOTE: These indexes are created in migrations after legacy table-shape repairs.
|
|
143
170
|
-- Creating them here can fail on upgraded installs where projects lacks those columns.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { closeConnection } from '@/modules/database/connection.js';
|
|
8
|
+
import { initializeDatabase } from '@/modules/database/init-db.js';
|
|
9
|
+
import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
|
|
10
|
+
|
|
11
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
12
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
13
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'sessions-mapping-'));
|
|
14
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
15
|
+
|
|
16
|
+
closeConnection();
|
|
17
|
+
process.env.DATABASE_PATH = databasePath;
|
|
18
|
+
await initializeDatabase();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await runTest();
|
|
22
|
+
} finally {
|
|
23
|
+
closeConnection();
|
|
24
|
+
if (previousDatabasePath === undefined) {
|
|
25
|
+
delete process.env.DATABASE_PATH;
|
|
26
|
+
} else {
|
|
27
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
28
|
+
}
|
|
29
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('disk-discovered sessions are keyed by the provider id for both columns', async () => {
|
|
34
|
+
await withIsolatedDatabase(() => {
|
|
35
|
+
sessionsDb.createSession('provider-abc', 'claude', '/workspace/demo', 'From Disk');
|
|
36
|
+
|
|
37
|
+
const row = sessionsDb.getSessionById('provider-abc');
|
|
38
|
+
assert.equal(row?.session_id, 'provider-abc');
|
|
39
|
+
assert.equal(row?.provider_session_id, 'provider-abc');
|
|
40
|
+
|
|
41
|
+
const byProviderId = sessionsDb.getSessionByProviderSessionId('provider-abc');
|
|
42
|
+
assert.equal(byProviderId?.session_id, 'provider-abc');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('app sessions get the provider id assigned without creating a duplicate row', async () => {
|
|
47
|
+
await withIsolatedDatabase(() => {
|
|
48
|
+
sessionsDb.createAppSession('app-id-1', 'claude', '/workspace/demo');
|
|
49
|
+
sessionsDb.assignProviderSessionId('app-id-1', 'provider-xyz');
|
|
50
|
+
|
|
51
|
+
// A later synchronizer pass that discovers the transcript on disk must
|
|
52
|
+
// update the app row in place instead of inserting a provider-keyed row.
|
|
53
|
+
const returnedId = sessionsDb.createSession(
|
|
54
|
+
'provider-xyz',
|
|
55
|
+
'claude',
|
|
56
|
+
'/workspace/demo',
|
|
57
|
+
'Synced Name',
|
|
58
|
+
undefined,
|
|
59
|
+
undefined,
|
|
60
|
+
'/fake/path/provider-xyz.jsonl',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.equal(returnedId, 'app-id-1');
|
|
64
|
+
assert.equal(sessionsDb.getAllSessions().length, 1);
|
|
65
|
+
|
|
66
|
+
const row = sessionsDb.getSessionById('app-id-1');
|
|
67
|
+
assert.equal(row?.provider_session_id, 'provider-xyz');
|
|
68
|
+
assert.equal(row?.jsonl_path, '/fake/path/provider-xyz.jsonl');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('assignProviderSessionId merges a watcher-created duplicate into the app row', async () => {
|
|
73
|
+
await withIsolatedDatabase(() => {
|
|
74
|
+
sessionsDb.createAppSession('app-id-2', 'codex', '/workspace/demo');
|
|
75
|
+
|
|
76
|
+
// Simulate the race: the filesystem watcher indexed the provider
|
|
77
|
+
// transcript before the runtime announced its session id to the gateway.
|
|
78
|
+
sessionsDb.createSession(
|
|
79
|
+
'provider-race',
|
|
80
|
+
'codex',
|
|
81
|
+
'/workspace/demo',
|
|
82
|
+
'Watcher Name',
|
|
83
|
+
undefined,
|
|
84
|
+
undefined,
|
|
85
|
+
'/fake/provider-race.jsonl',
|
|
86
|
+
);
|
|
87
|
+
assert.equal(sessionsDb.getAllSessions().length, 2);
|
|
88
|
+
|
|
89
|
+
sessionsDb.assignProviderSessionId('app-id-2', 'provider-race');
|
|
90
|
+
|
|
91
|
+
const rows = sessionsDb.getAllSessions();
|
|
92
|
+
assert.equal(rows.length, 1);
|
|
93
|
+
assert.equal(rows[0]?.session_id, 'app-id-2');
|
|
94
|
+
assert.equal(rows[0]?.provider_session_id, 'provider-race');
|
|
95
|
+
// Transcript path and name from the duplicate are adopted.
|
|
96
|
+
assert.equal(rows[0]?.jsonl_path, '/fake/provider-race.jsonl');
|
|
97
|
+
assert.equal(rows[0]?.custom_name, 'Watcher Name');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('legacy provider-keyed rows stay resolvable through both lookups', async () => {
|
|
102
|
+
await withIsolatedDatabase(() => {
|
|
103
|
+
sessionsDb.createSession('legacy-1', 'gemini', '/workspace/demo');
|
|
104
|
+
|
|
105
|
+
assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'gemini');
|
|
106
|
+
assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -70,3 +70,15 @@ test('createSession reactivates archived rows when the session becomes active ag
|
|
|
70
70
|
assert.equal(restoredSession?.isArchived, 0);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
test('repository reads normalize SQLite UTC timestamps to ISO strings', async () => {
|
|
75
|
+
await withIsolatedDatabase(() => {
|
|
76
|
+
sessionsDb.createAppSession('session-timezone', 'claude', '/workspace/demo-project');
|
|
77
|
+
|
|
78
|
+
const row = sessionsDb.getSessionById('session-timezone');
|
|
79
|
+
assert.ok(row?.created_at.endsWith('Z'));
|
|
80
|
+
assert.ok(row?.updated_at.endsWith('Z'));
|
|
81
|
+
assert.match(row?.created_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
82
|
+
assert.match(row?.updated_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildNotificationPayload,
|
|
3
|
+
createNotificationEvent,
|
|
4
|
+
notifyUserIfEnabled,
|
|
5
|
+
notifyRunFailed,
|
|
6
|
+
notifyRunStopped,
|
|
7
|
+
} from '@/modules/notifications/services/notification-orchestrator.service.js';
|
|
8
|
+
export {
|
|
9
|
+
registerDesktopNotificationClient,
|
|
10
|
+
sendDesktopNotification,
|
|
11
|
+
unregisterDesktopNotificationClient,
|
|
12
|
+
} from '@/modules/notifications/services/desktop-notification-clients.service.js';
|
|
13
|
+
export { handleDesktopNotificationsConnection } from '@/modules/notifications/websocket/desktop-notifications-websocket.service.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import { notificationChannelEndpointsDb, notificationPreferencesDb } from '@/modules/database/index.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
function readText(value: unknown): string {
|
|
8
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeEndpoint(endpoint: any) {
|
|
12
|
+
return {
|
|
13
|
+
id: endpoint.id,
|
|
14
|
+
channel: endpoint.channel,
|
|
15
|
+
endpointId: endpoint.endpoint_id,
|
|
16
|
+
label: endpoint.label,
|
|
17
|
+
metadata: notificationChannelEndpointsDb.parseMetadata(endpoint.metadata_json),
|
|
18
|
+
enabled: Boolean(endpoint.enabled),
|
|
19
|
+
lastSeenAt: endpoint.last_seen_at,
|
|
20
|
+
createdAt: endpoint.created_at,
|
|
21
|
+
updatedAt: endpoint.updated_at,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readUserId(req: express.Request): number {
|
|
26
|
+
const userId = Number((req as any).user?.id);
|
|
27
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
28
|
+
throw new Error('Authenticated user is missing');
|
|
29
|
+
}
|
|
30
|
+
return userId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateChannelPreference(userId: number, channel: string): unknown {
|
|
34
|
+
const currentPrefs = notificationPreferencesDb.getPreferences(userId);
|
|
35
|
+
const hasEnabledEndpoint = notificationChannelEndpointsDb.getEnabledEndpoints(userId, channel).length > 0;
|
|
36
|
+
return notificationPreferencesDb.updatePreferences(userId, {
|
|
37
|
+
...currentPrefs,
|
|
38
|
+
channels: { ...currentPrefs.channels, [channel]: hasEnabledEndpoint },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
router.get('/endpoints', (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const channel = readText(req.query.channel);
|
|
45
|
+
if (!channel) {
|
|
46
|
+
return res.status(400).json({ error: 'channel is required' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userId = readUserId(req);
|
|
50
|
+
const endpoints = notificationChannelEndpointsDb
|
|
51
|
+
.getEndpoints(userId, channel)
|
|
52
|
+
.map(sanitizeEndpoint);
|
|
53
|
+
return res.json({ success: true, endpoints });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error fetching notification endpoints:', error);
|
|
56
|
+
return res.status(500).json({ error: 'Failed to fetch notification endpoints' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.post('/endpoints/current', (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { channel, endpointId, label, metadata = {}, enabled = true } = req.body || {};
|
|
63
|
+
const normalizedChannel = readText(channel);
|
|
64
|
+
const normalizedEndpointId = readText(endpointId);
|
|
65
|
+
if (!normalizedChannel || !normalizedEndpointId) {
|
|
66
|
+
return res.status(400).json({ error: 'channel and endpointId are required' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const userId = readUserId(req);
|
|
70
|
+
const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
|
|
71
|
+
userId,
|
|
72
|
+
channel: normalizedChannel,
|
|
73
|
+
endpointId: normalizedEndpointId,
|
|
74
|
+
label,
|
|
75
|
+
metadata: metadata && typeof metadata === 'object' ? metadata : {},
|
|
76
|
+
enabled: enabled !== false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const preferences = updateChannelPreference(userId, normalizedChannel);
|
|
80
|
+
return res.json({ success: true, endpoint: sanitizeEndpoint(endpoint), preferences });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error registering notification endpoint:', error);
|
|
83
|
+
return res.status(500).json({ error: 'Failed to register notification endpoint' });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
router.patch('/endpoints/:channel/:endpointId', (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { channel, endpointId } = req.params;
|
|
90
|
+
const { enabled } = req.body || {};
|
|
91
|
+
if (typeof enabled !== 'boolean') {
|
|
92
|
+
return res.status(400).json({ error: 'enabled must be a boolean' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const userId = readUserId(req);
|
|
96
|
+
const updated = notificationChannelEndpointsDb.setEndpointEnabled(userId, channel, endpointId, enabled);
|
|
97
|
+
if (!updated) {
|
|
98
|
+
return res.status(404).json({ error: 'Notification endpoint not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const endpoint = notificationChannelEndpointsDb.getEndpoint(userId, channel, endpointId);
|
|
102
|
+
const preferences = updateChannelPreference(userId, channel);
|
|
103
|
+
return res.json({ success: true, endpoint: endpoint ? sanitizeEndpoint(endpoint) : null, preferences });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error updating notification endpoint:', error);
|
|
106
|
+
return res.status(500).json({ error: 'Failed to update notification endpoint' });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
router.delete('/endpoints/:channel/:endpointId', (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const { channel, endpointId } = req.params;
|
|
113
|
+
const userId = readUserId(req);
|
|
114
|
+
const removed = notificationChannelEndpointsDb.removeEndpoint(userId, channel, endpointId);
|
|
115
|
+
if (!removed) {
|
|
116
|
+
return res.status(404).json({ error: 'Notification endpoint not found' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const preferences = updateChannelPreference(userId, channel);
|
|
120
|
+
return res.json({ success: true, preferences });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error removing notification endpoint:', error);
|
|
123
|
+
return res.status(500).json({ error: 'Failed to remove notification endpoint' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export default router;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { WebSocket } from 'ws';
|
|
2
|
+
|
|
3
|
+
import { notificationChannelEndpointsDb } from '@/modules/database/index.js';
|
|
4
|
+
|
|
5
|
+
const DESKTOP_CHANNEL = 'desktop';
|
|
6
|
+
|
|
7
|
+
const clientsByUserId = new Map<number, Map<string, WebSocket>>();
|
|
8
|
+
const clientBySocket = new WeakMap<WebSocket, { userId: number; endpointId: string }>();
|
|
9
|
+
|
|
10
|
+
function normalizeUserId(userId: unknown): number | null {
|
|
11
|
+
const numeric = Number(userId);
|
|
12
|
+
return Number.isInteger(numeric) && numeric > 0 ? numeric : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeEndpointId(endpointId: unknown): string {
|
|
16
|
+
if (typeof endpointId !== 'string') return '';
|
|
17
|
+
return endpointId.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getUserClients(userId: unknown, create = false): Map<string, WebSocket> | null {
|
|
21
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
22
|
+
if (!normalizedUserId) return null;
|
|
23
|
+
let clients = clientsByUserId.get(normalizedUserId);
|
|
24
|
+
if (!clients && create) {
|
|
25
|
+
clients = new Map();
|
|
26
|
+
clientsByUserId.set(normalizedUserId, clients);
|
|
27
|
+
}
|
|
28
|
+
return clients || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerDesktopNotificationClient({
|
|
32
|
+
userId,
|
|
33
|
+
deviceId,
|
|
34
|
+
label = null,
|
|
35
|
+
platform = null,
|
|
36
|
+
appVersion = null,
|
|
37
|
+
ws,
|
|
38
|
+
}: {
|
|
39
|
+
userId: number;
|
|
40
|
+
deviceId: string;
|
|
41
|
+
label?: string | null;
|
|
42
|
+
platform?: string | null;
|
|
43
|
+
appVersion?: string | null;
|
|
44
|
+
ws: WebSocket;
|
|
45
|
+
}) {
|
|
46
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
47
|
+
const endpointId = normalizeEndpointId(deviceId);
|
|
48
|
+
if (!normalizedUserId || !endpointId) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
|
|
53
|
+
userId: normalizedUserId,
|
|
54
|
+
channel: DESKTOP_CHANNEL,
|
|
55
|
+
endpointId,
|
|
56
|
+
label,
|
|
57
|
+
metadata: { platform, appVersion },
|
|
58
|
+
enabled: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const clients = getUserClients(normalizedUserId, true)!;
|
|
62
|
+
const previous = clients.get(endpointId);
|
|
63
|
+
if (previous && previous !== ws && previous.readyState === previous.OPEN) {
|
|
64
|
+
previous.close(4000, 'Device reconnected');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clients.set(endpointId, ws);
|
|
68
|
+
clientBySocket.set(ws, { userId: normalizedUserId, endpointId });
|
|
69
|
+
return endpoint;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function unregisterDesktopNotificationClient(ws: WebSocket): void {
|
|
73
|
+
const registration = clientBySocket.get(ws);
|
|
74
|
+
if (!registration) return;
|
|
75
|
+
|
|
76
|
+
const clients = getUserClients(registration.userId);
|
|
77
|
+
if (clients?.get(registration.endpointId) === ws) {
|
|
78
|
+
clients.delete(registration.endpointId);
|
|
79
|
+
if (clients.size === 0) {
|
|
80
|
+
clientsByUserId.delete(registration.userId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
clientBySocket.delete(ws);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function sendDesktopNotification(userId: unknown, payload: unknown): { attempted: number; sent: number } {
|
|
87
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
88
|
+
if (!normalizedUserId) return { attempted: 0, sent: 0 };
|
|
89
|
+
|
|
90
|
+
const clients = getUserClients(normalizedUserId);
|
|
91
|
+
if (!clients?.size) return { attempted: 0, sent: 0 };
|
|
92
|
+
|
|
93
|
+
const enabledEndpointIds = new Set(
|
|
94
|
+
notificationChannelEndpointsDb
|
|
95
|
+
.getEnabledEndpoints(normalizedUserId, DESKTOP_CHANNEL)
|
|
96
|
+
.map((endpoint) => endpoint.endpoint_id)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const message = JSON.stringify({
|
|
100
|
+
type: 'notification',
|
|
101
|
+
id: typeof (payload as any)?.data?.tag === 'string' ? (payload as any).data.tag : `${Date.now()}`,
|
|
102
|
+
payload,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let attempted = 0;
|
|
106
|
+
let sent = 0;
|
|
107
|
+
for (const [endpointId, ws] of clients.entries()) {
|
|
108
|
+
if (!enabledEndpointIds.has(endpointId)) continue;
|
|
109
|
+
attempted += 1;
|
|
110
|
+
if (ws.readyState !== ws.OPEN) {
|
|
111
|
+
unregisterDesktopNotificationClient(ws);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
ws.send(message);
|
|
116
|
+
notificationChannelEndpointsDb.touchEndpoint(normalizedUserId, DESKTOP_CHANNEL, endpointId);
|
|
117
|
+
sent += 1;
|
|
118
|
+
} catch {
|
|
119
|
+
unregisterDesktopNotificationClient(ws);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { attempted, sent };
|
|
124
|
+
}
|