@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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import webPush from 'web-push';
|
|
2
|
+
|
|
3
|
+
import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '@/modules/database/index.js';
|
|
4
|
+
import { sendDesktopNotification as sendDesktopNotificationToClients } from '@/modules/notifications/services/desktop-notification-clients.service.js';
|
|
5
|
+
|
|
6
|
+
const KIND_TO_PREF_KEY = {
|
|
7
|
+
action_required: 'actionRequired',
|
|
8
|
+
stop: 'stop',
|
|
9
|
+
error: 'error'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PROVIDER_LABELS = {
|
|
13
|
+
claude: 'Claude',
|
|
14
|
+
cursor: 'Cursor',
|
|
15
|
+
codex: 'Codex',
|
|
16
|
+
gemini: 'Gemini',
|
|
17
|
+
system: 'System'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const recentEventKeys = new Map();
|
|
21
|
+
const DEDUPE_WINDOW_MS = 20000;
|
|
22
|
+
|
|
23
|
+
const cleanupOldEventKeys = () => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [key, timestamp] of recentEventKeys.entries()) {
|
|
26
|
+
if (now - timestamp > DEDUPE_WINDOW_MS) {
|
|
27
|
+
recentEventKeys.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function isNotificationEventEnabled(preferences, event) {
|
|
33
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
34
|
+
const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
|
|
35
|
+
|
|
36
|
+
return eventEnabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isDuplicate(event) {
|
|
40
|
+
cleanupOldEventKeys();
|
|
41
|
+
const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
|
|
42
|
+
if (recentEventKeys.has(key)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
recentEventKeys.set(key, Date.now());
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createNotificationEvent({
|
|
50
|
+
provider,
|
|
51
|
+
sessionId = null,
|
|
52
|
+
kind = 'info',
|
|
53
|
+
code = 'generic.info',
|
|
54
|
+
meta = {},
|
|
55
|
+
severity = 'info',
|
|
56
|
+
dedupeKey = null,
|
|
57
|
+
requiresUserAction = false
|
|
58
|
+
}) {
|
|
59
|
+
return {
|
|
60
|
+
provider,
|
|
61
|
+
sessionId,
|
|
62
|
+
kind,
|
|
63
|
+
code,
|
|
64
|
+
meta,
|
|
65
|
+
severity,
|
|
66
|
+
requiresUserAction,
|
|
67
|
+
dedupeKey,
|
|
68
|
+
createdAt: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeErrorMessage(error) {
|
|
73
|
+
if (typeof error === 'string') {
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (error && typeof error.message === 'string') {
|
|
78
|
+
return error.message;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error == null) {
|
|
82
|
+
return 'Unknown error';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return String(error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeSessionName(sessionName) {
|
|
89
|
+
if (typeof sessionName !== 'string') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const normalized = sessionName.replace(/\s+/g, ' ').trim();
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rowMatchesProvider(row, provider) {
|
|
102
|
+
return row && (!provider || row.provider === provider);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveSessionRow(sessionId, provider) {
|
|
106
|
+
if (!sessionId) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const appSessionRow = sessionsDb.getSessionById(sessionId);
|
|
111
|
+
if (rowMatchesProvider(appSessionRow, provider)) {
|
|
112
|
+
return appSessionRow;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const providerSessionRow = sessionsDb.getSessionByProviderSessionId(sessionId);
|
|
116
|
+
if (rowMatchesProvider(providerSessionRow, provider)) {
|
|
117
|
+
return providerSessionRow;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeNotificationSession(event) {
|
|
124
|
+
if (!event?.sessionId || !event.provider || event.provider === 'system') {
|
|
125
|
+
return event;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const row = resolveSessionRow(event.sessionId, event.provider);
|
|
129
|
+
if (!row || row.session_id === event.sessionId) {
|
|
130
|
+
return event;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...event,
|
|
135
|
+
sessionId: row.session_id
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveSessionName(event) {
|
|
140
|
+
const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
|
|
141
|
+
if (explicitSessionName) {
|
|
142
|
+
return explicitSessionName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!event.sessionId || !event.provider) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildNotificationPayload(event) {
|
|
153
|
+
const normalizedEvent = normalizeNotificationSession(event);
|
|
154
|
+
const CODE_MAP = {
|
|
155
|
+
'permission.required': normalizedEvent.meta?.toolName
|
|
156
|
+
? `Action Required: Tool "${normalizedEvent.meta.toolName}" needs approval`
|
|
157
|
+
: 'Action Required: A tool needs your approval',
|
|
158
|
+
'run.stopped': normalizedEvent.meta?.stopReason || 'Run Stopped: The run has stopped',
|
|
159
|
+
'run.failed': normalizedEvent.meta?.error ? `Run Failed: ${normalizedEvent.meta.error}` : 'Run Failed: The run encountered an error',
|
|
160
|
+
'agent.notification': normalizedEvent.meta?.message ? String(normalizedEvent.meta.message) : 'You have a new notification',
|
|
161
|
+
'push.enabled': 'Push notifications are now enabled!'
|
|
162
|
+
};
|
|
163
|
+
const providerLabel = PROVIDER_LABELS[normalizedEvent.provider] || 'Assistant';
|
|
164
|
+
const sessionName = resolveSessionName(normalizedEvent);
|
|
165
|
+
const message = CODE_MAP[normalizedEvent.code] || 'You have a new notification';
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
title: sessionName || 'CloudCLI',
|
|
169
|
+
body: `${providerLabel}: ${message}`,
|
|
170
|
+
data: {
|
|
171
|
+
sessionId: normalizedEvent.sessionId || null,
|
|
172
|
+
code: normalizedEvent.code,
|
|
173
|
+
provider: normalizedEvent.provider || null,
|
|
174
|
+
sessionName,
|
|
175
|
+
tag: `${normalizedEvent.provider || 'assistant'}:${normalizedEvent.sessionId || 'none'}:${normalizedEvent.code}`
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sendWebPushPayload(userId, payload) {
|
|
181
|
+
const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
|
|
182
|
+
if (!subscriptions.length) return Promise.resolve();
|
|
183
|
+
|
|
184
|
+
const serializedPayload = JSON.stringify(payload);
|
|
185
|
+
return Promise.allSettled(
|
|
186
|
+
subscriptions.map((sub) =>
|
|
187
|
+
webPush.sendNotification(
|
|
188
|
+
{
|
|
189
|
+
endpoint: sub.endpoint,
|
|
190
|
+
keys: {
|
|
191
|
+
p256dh: sub.keys_p256dh,
|
|
192
|
+
auth: sub.keys_auth
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
serializedPayload
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
).then((results) => {
|
|
199
|
+
results.forEach((result, index) => {
|
|
200
|
+
if (result.status === 'rejected') {
|
|
201
|
+
const statusCode = result.reason?.statusCode;
|
|
202
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
203
|
+
pushSubscriptionsDb.removeSubscription(subscriptions[index].endpoint);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const notificationChannels = [
|
|
211
|
+
{
|
|
212
|
+
id: 'webPush',
|
|
213
|
+
// TODO: Web push still uses push_subscriptions. Do not remove that table until
|
|
214
|
+
// browser push subscriptions are migrated into notification_channel_endpoints.
|
|
215
|
+
isEnabled: (preferences) => Boolean(preferences?.channels?.webPush),
|
|
216
|
+
send: ({ userId, payload }) => sendWebPushPayload(userId, payload)
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'desktop',
|
|
220
|
+
isEnabled: (preferences) => Boolean(preferences?.channels?.desktop),
|
|
221
|
+
send: ({ userId, payload }) => sendDesktopNotificationToClients(userId, payload)
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
function notifyUserIfEnabled({ userId, event }) {
|
|
226
|
+
if (!userId || !event) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalizedEvent = normalizeNotificationSession(event);
|
|
231
|
+
const preferences = notificationPreferencesDb.getPreferences(userId);
|
|
232
|
+
if (!isNotificationEventEnabled(preferences, normalizedEvent)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isDuplicate(normalizedEvent)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const payload = buildNotificationPayload(normalizedEvent);
|
|
240
|
+
for (const channel of notificationChannels) {
|
|
241
|
+
if (!channel.isEnabled(preferences)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
Promise.resolve(channel.send({ userId, event: normalizedEvent, payload })).catch((err) => {
|
|
245
|
+
console.error(`Notification channel "${channel.id}" send error:`, err);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
|
|
251
|
+
notifyUserIfEnabled({
|
|
252
|
+
userId,
|
|
253
|
+
event: createNotificationEvent({
|
|
254
|
+
provider,
|
|
255
|
+
sessionId,
|
|
256
|
+
kind: 'stop',
|
|
257
|
+
code: 'run.stopped',
|
|
258
|
+
meta: { stopReason, sessionName },
|
|
259
|
+
severity: 'info',
|
|
260
|
+
dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
|
|
266
|
+
const errorMessage = normalizeErrorMessage(error);
|
|
267
|
+
|
|
268
|
+
notifyUserIfEnabled({
|
|
269
|
+
userId,
|
|
270
|
+
event: createNotificationEvent({
|
|
271
|
+
provider,
|
|
272
|
+
sessionId,
|
|
273
|
+
kind: 'error',
|
|
274
|
+
code: 'run.failed',
|
|
275
|
+
meta: { error: errorMessage, sessionName },
|
|
276
|
+
severity: 'error',
|
|
277
|
+
dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
buildNotificationPayload,
|
|
284
|
+
createNotificationEvent,
|
|
285
|
+
notifyUserIfEnabled,
|
|
286
|
+
notifyRunStopped,
|
|
287
|
+
notifyRunFailed
|
|
288
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { WebSocket } from 'ws';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
registerDesktopNotificationClient,
|
|
5
|
+
unregisterDesktopNotificationClient,
|
|
6
|
+
} from '@/modules/notifications/services/desktop-notification-clients.service.js';
|
|
7
|
+
import type { AuthenticatedWebSocketRequest } from '@/shared/types.js';
|
|
8
|
+
import { parseIncomingJsonObject } from '@/shared/utils.js';
|
|
9
|
+
|
|
10
|
+
type DesktopNotificationRegisterMessage = {
|
|
11
|
+
type?: unknown;
|
|
12
|
+
kind?: unknown;
|
|
13
|
+
deviceId?: unknown;
|
|
14
|
+
label?: unknown;
|
|
15
|
+
platform?: unknown;
|
|
16
|
+
appVersion?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function readRequestUserId(request: AuthenticatedWebSocketRequest): number | null {
|
|
20
|
+
const user = request.user;
|
|
21
|
+
const rawUserId = typeof user?.id === 'number' || typeof user?.id === 'string'
|
|
22
|
+
? user.id
|
|
23
|
+
: typeof user?.userId === 'number' || typeof user?.userId === 'string'
|
|
24
|
+
? user.userId
|
|
25
|
+
: null;
|
|
26
|
+
const numericUserId = Number(rawUserId);
|
|
27
|
+
return Number.isInteger(numericUserId) && numericUserId > 0 ? numericUserId : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readOptionalString(value: unknown): string | null {
|
|
31
|
+
if (typeof value !== 'string') return null;
|
|
32
|
+
const normalized = value.trim();
|
|
33
|
+
return normalized || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sendJson(ws: WebSocket, payload: unknown): void {
|
|
37
|
+
if (ws.readyState === ws.OPEN) {
|
|
38
|
+
ws.send(JSON.stringify(payload));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function handleDesktopNotificationsConnection(
|
|
43
|
+
ws: WebSocket,
|
|
44
|
+
request: AuthenticatedWebSocketRequest
|
|
45
|
+
): void {
|
|
46
|
+
const userId = readRequestUserId(request);
|
|
47
|
+
if (!userId) {
|
|
48
|
+
ws.close(1008, 'Missing authenticated user');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let registered = false;
|
|
53
|
+
|
|
54
|
+
ws.on('message', (rawMessage) => {
|
|
55
|
+
const data = parseIncomingJsonObject(rawMessage) as DesktopNotificationRegisterMessage | null;
|
|
56
|
+
if (!data) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const type = typeof data.type === 'string' ? data.type : typeof data.kind === 'string' ? data.kind : '';
|
|
61
|
+
if (type === 'notification_ack') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (type !== 'register' || registered) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const deviceId = readOptionalString(data.deviceId);
|
|
70
|
+
if (!deviceId) {
|
|
71
|
+
sendJson(ws, {
|
|
72
|
+
type: 'error',
|
|
73
|
+
code: 'DEVICE_ID_REQUIRED',
|
|
74
|
+
message: 'Desktop notification registration requires deviceId.',
|
|
75
|
+
});
|
|
76
|
+
ws.close(1008, 'Missing deviceId');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const device = registerDesktopNotificationClient({
|
|
81
|
+
userId,
|
|
82
|
+
deviceId,
|
|
83
|
+
label: readOptionalString(data.label),
|
|
84
|
+
platform: readOptionalString(data.platform),
|
|
85
|
+
appVersion: readOptionalString(data.appVersion),
|
|
86
|
+
ws,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!device) {
|
|
90
|
+
ws.close(1011, 'Registration failed');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registered = true;
|
|
95
|
+
sendJson(ws, {
|
|
96
|
+
type: 'registered',
|
|
97
|
+
deviceId: device.endpoint_id,
|
|
98
|
+
enabled: Boolean(device.enabled),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
ws.on('close', () => {
|
|
103
|
+
unregisterDesktopNotificationClient(ws);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
ws.on('error', () => {
|
|
107
|
+
unregisterDesktopNotificationClient(ws);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -30,10 +30,6 @@ type ProjectApiView = {
|
|
|
30
30
|
isArchived: boolean;
|
|
31
31
|
isStarred: boolean;
|
|
32
32
|
sessions: [];
|
|
33
|
-
cursorSessions: [];
|
|
34
|
-
codexSessions: [];
|
|
35
|
-
geminiSessions: [];
|
|
36
|
-
opencodeSessions: [];
|
|
37
33
|
sessionMeta: {
|
|
38
34
|
hasMore: false;
|
|
39
35
|
total: 0;
|
|
@@ -82,10 +78,6 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
|
|
|
82
78
|
isArchived: Boolean(projectRow.isArchived),
|
|
83
79
|
isStarred: Boolean(projectRow.isStarred),
|
|
84
80
|
sessions: [],
|
|
85
|
-
cursorSessions: [],
|
|
86
|
-
codexSessions: [],
|
|
87
|
-
geminiSessions: [],
|
|
88
|
-
opencodeSessions: [],
|
|
89
81
|
sessionMeta: {
|
|
90
82
|
hasMore: false,
|
|
91
83
|
total: 0,
|
|
@@ -9,13 +9,12 @@ import { AppError } from '@/shared/utils.js';
|
|
|
9
9
|
|
|
10
10
|
type SessionSummary = {
|
|
11
11
|
id: string;
|
|
12
|
+
provider: string;
|
|
12
13
|
summary: string;
|
|
13
14
|
messageCount: number;
|
|
14
15
|
lastActivity: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode', SessionSummary[]>;
|
|
18
|
-
|
|
19
18
|
type SessionRepositoryRow = {
|
|
20
19
|
provider: string;
|
|
21
20
|
session_id: string;
|
|
@@ -31,10 +30,6 @@ export type ProjectListItem = {
|
|
|
31
30
|
fullPath: string;
|
|
32
31
|
isStarred: boolean;
|
|
33
32
|
sessions: SessionSummary[];
|
|
34
|
-
cursorSessions: SessionSummary[];
|
|
35
|
-
codexSessions: SessionSummary[];
|
|
36
|
-
geminiSessions: SessionSummary[];
|
|
37
|
-
opencodeSessions: SessionSummary[];
|
|
38
33
|
sessionMeta: {
|
|
39
34
|
hasMore: boolean;
|
|
40
35
|
total: number;
|
|
@@ -64,7 +59,7 @@ type SessionPaginationOptions = {
|
|
|
64
59
|
};
|
|
65
60
|
|
|
66
61
|
type ProjectSessionsPageResult = {
|
|
67
|
-
|
|
62
|
+
sessions: SessionSummary[];
|
|
68
63
|
total: number;
|
|
69
64
|
hasMore: boolean;
|
|
70
65
|
};
|
|
@@ -72,10 +67,6 @@ type ProjectSessionsPageResult = {
|
|
|
72
67
|
export type ProjectSessionsPageApiView = {
|
|
73
68
|
projectId: string;
|
|
74
69
|
sessions: SessionSummary[];
|
|
75
|
-
cursorSessions: SessionSummary[];
|
|
76
|
-
codexSessions: SessionSummary[];
|
|
77
|
-
geminiSessions: SessionSummary[];
|
|
78
|
-
opencodeSessions: SessionSummary[];
|
|
79
70
|
sessionMeta: {
|
|
80
71
|
hasMore: boolean;
|
|
81
72
|
total: number;
|
|
@@ -129,39 +120,18 @@ function normalizeSessionPagination(options: SessionPaginationOptions = {}): { l
|
|
|
129
120
|
function mapSessionRowToSummary(row: SessionRepositoryRow): SessionSummary {
|
|
130
121
|
return {
|
|
131
122
|
id: row.session_id,
|
|
123
|
+
provider: row.provider,
|
|
132
124
|
summary: row.custom_name || '',
|
|
133
125
|
messageCount: 0,
|
|
134
126
|
lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
|
|
135
127
|
};
|
|
136
128
|
}
|
|
137
129
|
|
|
138
|
-
function bucketSessionRowsByProvider(rows: SessionRepositoryRow[]): SessionsByProvider {
|
|
139
|
-
const byProvider: SessionsByProvider = {
|
|
140
|
-
claude: [],
|
|
141
|
-
cursor: [],
|
|
142
|
-
codex: [],
|
|
143
|
-
gemini: [],
|
|
144
|
-
opencode: [],
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
for (const row of rows) {
|
|
148
|
-
const provider = row.provider as keyof SessionsByProvider;
|
|
149
|
-
const bucket = byProvider[provider];
|
|
150
|
-
if (!bucket) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
bucket.push(mapSessionRowToSummary(row));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return byProvider;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
130
|
function readProjectSessionsIncludingArchived(projectPath: string): ProjectSessionsPageResult {
|
|
161
131
|
const rows = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath) as SessionRepositoryRow[];
|
|
162
132
|
|
|
163
133
|
return {
|
|
164
|
-
|
|
134
|
+
sessions: rows.map(mapSessionRowToSummary),
|
|
165
135
|
total: rows.length,
|
|
166
136
|
hasMore: false,
|
|
167
137
|
};
|
|
@@ -183,16 +153,17 @@ function readProjectSessionsPageByPath(
|
|
|
183
153
|
const total = sessionsDb.countSessionsByProjectPath(projectPath);
|
|
184
154
|
|
|
185
155
|
return {
|
|
186
|
-
|
|
156
|
+
sessions: rows.map(mapSessionRowToSummary),
|
|
187
157
|
total,
|
|
188
158
|
hasMore: pagination.offset + rows.length < total,
|
|
189
159
|
};
|
|
190
160
|
}
|
|
191
161
|
|
|
192
|
-
// Broadcast progress to all connected WebSocket clients
|
|
162
|
+
// Broadcast progress to all connected WebSocket clients.
|
|
163
|
+
// Uses the unified `kind` envelope like every other websocket frame.
|
|
193
164
|
function broadcastProgress(progress: ProgressUpdate) {
|
|
194
165
|
const message = JSON.stringify({
|
|
195
|
-
|
|
166
|
+
kind: 'loading_progress',
|
|
196
167
|
...progress,
|
|
197
168
|
});
|
|
198
169
|
|
|
@@ -204,7 +175,7 @@ function broadcastProgress(progress: ProgressUpdate) {
|
|
|
204
175
|
}
|
|
205
176
|
|
|
206
177
|
/**
|
|
207
|
-
* Reads all projects from DB and returns
|
|
178
|
+
* Reads all projects from DB and returns normalized session summaries.
|
|
208
179
|
*/
|
|
209
180
|
export async function getProjectsWithSessions(
|
|
210
181
|
options: GetProjectsWithSessionsOptions = {}
|
|
@@ -252,11 +223,7 @@ export async function getProjectsWithSessions(
|
|
|
252
223
|
displayName,
|
|
253
224
|
fullPath: projectPath,
|
|
254
225
|
isStarred: Boolean(row.isStarred),
|
|
255
|
-
sessions: sessionsPage.
|
|
256
|
-
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
257
|
-
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
258
|
-
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
259
|
-
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
226
|
+
sessions: sessionsPage.sessions,
|
|
260
227
|
sessionMeta: {
|
|
261
228
|
hasMore: sessionsPage.hasMore,
|
|
262
229
|
total: sessionsPage.total,
|
|
@@ -309,11 +276,7 @@ export async function getArchivedProjectsWithSessions(
|
|
|
309
276
|
fullPath: row.project_path,
|
|
310
277
|
isStarred: Boolean(row.isStarred),
|
|
311
278
|
isArchived: true,
|
|
312
|
-
sessions: sessionsPage.
|
|
313
|
-
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
314
|
-
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
315
|
-
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
316
|
-
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
279
|
+
sessions: sessionsPage.sessions,
|
|
317
280
|
sessionMeta: {
|
|
318
281
|
hasMore: sessionsPage.hasMore,
|
|
319
282
|
total: sessionsPage.total,
|
|
@@ -342,11 +305,7 @@ export async function getProjectSessionsPage(
|
|
|
342
305
|
const sessionsPage = readProjectSessionsPageByPath(projectRow.project_path, options);
|
|
343
306
|
return {
|
|
344
307
|
projectId: projectRow.project_id,
|
|
345
|
-
sessions: sessionsPage.
|
|
346
|
-
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
347
|
-
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
348
|
-
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
349
|
-
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
308
|
+
sessions: sessionsPage.sessions,
|
|
350
309
|
sessionMeta: {
|
|
351
310
|
hasMore: sessionsPage.hasMore,
|
|
352
311
|
total: sessionsPage.total,
|
|
@@ -83,7 +83,7 @@ The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
|
|
|
83
83
|
- Update `server/modules/providers/provider.routes.ts`.
|
|
84
84
|
- Update `server/routes/agent.js` if the provider is launchable from the agent runtime.
|
|
85
85
|
- Update `server/index.js` if the provider needs runtime boot or shutdown wiring.
|
|
86
|
-
- Update `public/
|
|
86
|
+
- Update the `PROVIDER_ORDER` list in `public/api-docs.html` if the provider should appear in the public API docs.
|
|
87
87
|
- Update `src/components/chat/hooks/useChatProviderState.ts` and
|
|
88
88
|
`src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx` if
|
|
89
89
|
the provider should be selectable in chat.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { sessionSynchronizerService } from './services/session-synchronizer.service.js';
|
|
2
2
|
export { providerSkillsService } from './services/skills.service.js';
|
|
3
|
+
export { providerMcpService } from './services/mcp.service.js';
|
|
3
4
|
|
|
4
5
|
export { initializeSessionsWatcher } from './services/sessions-watcher.service.js';
|
|
5
6
|
export { closeSessionsWatcher } from './services/sessions-watcher.service.js';
|
|
@@ -25,6 +25,21 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
25
25
|
private readonly provider = 'claude' as const;
|
|
26
26
|
private readonly claudeHome = path.join(os.homedir(), '.claude');
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Returns true when a JSONL file is a subagent transcript rather than a
|
|
30
|
+
* top-level session.
|
|
31
|
+
*
|
|
32
|
+
* Claude stores subagent transcripts under a `subagents/` directory, e.g.
|
|
33
|
+
* `~/.claude/projects/<encoded-cwd>/<session-id>/subagents/agent-<id>.jsonl`.
|
|
34
|
+
* Those files repeat the parent session's `sessionId`, so indexing them as
|
|
35
|
+
* standalone sessions overwrites the parent row's `jsonl_path` and corrupts
|
|
36
|
+
* the main session record. The recursive scan in `synchronize()` reaches
|
|
37
|
+
* them, so both entry points must skip them.
|
|
38
|
+
*/
|
|
39
|
+
private isSubagentTranscript(filePath: string): boolean {
|
|
40
|
+
return path.normalize(filePath).split(path.sep).includes('subagents');
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
/**
|
|
29
44
|
* Scans ~/.claude/projects and upserts discovered sessions into DB.
|
|
30
45
|
*/
|
|
@@ -38,6 +53,10 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
38
53
|
|
|
39
54
|
let processed = 0;
|
|
40
55
|
for (const filePath of files) {
|
|
56
|
+
if (this.isSubagentTranscript(filePath)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
|
42
61
|
if (!parsed) {
|
|
43
62
|
continue;
|
|
@@ -66,6 +85,9 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
66
85
|
if (!filePath.endsWith('.jsonl')) {
|
|
67
86
|
return null;
|
|
68
87
|
}
|
|
88
|
+
if (this.isSubagentTranscript(filePath)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
69
91
|
|
|
70
92
|
const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
|
|
71
93
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
|
@@ -111,7 +133,10 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
111
133
|
return null;
|
|
112
134
|
}
|
|
113
135
|
|
|
114
|
-
|
|
136
|
+
// App-created sessions are keyed by an app id, so disk-discovered provider
|
|
137
|
+
// ids must be resolved through the provider-id mapping first.
|
|
138
|
+
const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId)
|
|
139
|
+
?? sessionsDb.getSessionById(parsed.sessionId);
|
|
115
140
|
const existingSessionName = existingSession?.custom_name;
|
|
116
141
|
if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
|
|
117
142
|
return {
|