@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.
Files changed (247) hide show
  1. package/README.de.md +9 -1
  2. package/README.ja.md +8 -0
  3. package/README.ko.md +9 -1
  4. package/README.md +23 -5
  5. package/README.ru.md +9 -1
  6. package/README.tr.md +8 -1
  7. package/README.zh-CN.md +9 -1
  8. package/README.zh-TW.md +9 -1
  9. package/dist/api-docs.html +36 -18
  10. package/dist/assets/index-BvX30NW5.css +32 -0
  11. package/dist/assets/index-D1IdCTT6.js +1418 -0
  12. package/dist/index.html +12 -4
  13. package/dist-server/server/browser-use-mcp.js +350 -0
  14. package/dist-server/server/browser-use-mcp.js.map +1 -0
  15. package/dist-server/server/claude-sdk.js +25 -5
  16. package/dist-server/server/claude-sdk.js.map +1 -1
  17. package/dist-server/server/cli.js +14 -6
  18. package/dist-server/server/cli.js.map +1 -1
  19. package/dist-server/server/cursor-cli.js +27 -11
  20. package/dist-server/server/cursor-cli.js.map +1 -1
  21. package/dist-server/server/gemini-cli.js +17 -2
  22. package/dist-server/server/gemini-cli.js.map +1 -1
  23. package/dist-server/server/index.js +136 -40
  24. package/dist-server/server/index.js.map +1 -1
  25. package/dist-server/server/load-env.js +1 -1
  26. package/dist-server/server/load-env.js.map +1 -1
  27. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  28. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  29. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  30. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  31. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  32. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  34. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  35. package/dist-server/server/modules/database/index.js +1 -0
  36. package/dist-server/server/modules/database/index.js.map +1 -1
  37. package/dist-server/server/modules/database/migrations.js +23 -1
  38. package/dist-server/server/modules/database/migrations.js.map +1 -1
  39. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  40. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  41. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  42. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  43. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  44. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  45. package/dist-server/server/modules/database/schema.js +26 -0
  46. package/dist-server/server/modules/database/schema.js.map +1 -1
  47. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  48. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  49. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  50. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  51. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  52. package/dist-server/server/modules/notifications/index.js +4 -0
  53. package/dist-server/server/modules/notifications/index.js.map +1 -0
  54. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  55. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  56. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  57. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  58. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  59. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  60. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  61. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  62. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  63. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  64. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  65. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/index.js +1 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -1
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  73. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  79. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  81. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  85. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  91. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  92. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  93. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  94. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  95. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  96. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  97. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  98. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  99. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  100. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  101. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  102. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  103. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  104. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  105. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  107. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  109. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  111. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  112. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  113. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  114. package/dist-server/server/modules/websocket/index.js +1 -0
  115. package/dist-server/server/modules/websocket/index.js.map +1 -1
  116. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  117. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  118. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  119. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  120. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  121. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  122. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  123. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  124. package/dist-server/server/modules/websocket/services/websocket-server.service.js +5 -0
  125. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  126. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  127. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  128. package/dist-server/server/openai-codex.js +24 -14
  129. package/dist-server/server/openai-codex.js.map +1 -1
  130. package/dist-server/server/opencode-cli.js +21 -8
  131. package/dist-server/server/opencode-cli.js.map +1 -1
  132. package/dist-server/server/opencode-cli.test.js +19 -1
  133. package/dist-server/server/opencode-cli.test.js.map +1 -1
  134. package/dist-server/server/routes/settings.js +1 -1
  135. package/dist-server/server/routes/settings.js.map +1 -1
  136. package/dist-server/server/services/notification-orchestrator.js +1 -176
  137. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  138. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  139. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  140. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  141. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  142. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  143. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  144. package/dist-server/server/shared/utils.js +73 -1
  145. package/dist-server/server/shared/utils.js.map +1 -1
  146. package/dist-server/server/voice-proxy.js +226 -0
  147. package/dist-server/server/voice-proxy.js.map +1 -0
  148. package/electron/assets/logo-macos.icns +0 -0
  149. package/electron/assets/logo-macos.png +0 -0
  150. package/electron/assets/logo-windows.ico +0 -0
  151. package/electron/cloud.js +260 -0
  152. package/electron/desktopNotifications.js +378 -0
  153. package/electron/desktopWindow.js +766 -0
  154. package/electron/launcher/index.html +14 -0
  155. package/electron/launcher/launcher.css +801 -0
  156. package/electron/launcher/launcher.js +687 -0
  157. package/electron/localServer.js +550 -0
  158. package/electron/main.js +944 -0
  159. package/electron/preload.cjs +60 -0
  160. package/electron/scripts/generate-macos-icon.js +62 -0
  161. package/electron/serverInstaller.js +277 -0
  162. package/electron/tabs.js +87 -0
  163. package/electron/viewHost.js +331 -0
  164. package/package.json +79 -3
  165. package/public/api-docs.html +36 -18
  166. package/scripts/release/build-server-bundle.js +176 -0
  167. package/scripts/release/prepare-desktop-app.js +152 -0
  168. package/server/browser-use-mcp.ts +384 -0
  169. package/server/claude-sdk.js +27 -5
  170. package/server/cli.js +15 -6
  171. package/server/cursor-cli.js +27 -11
  172. package/server/gemini-cli.js +17 -2
  173. package/server/index.js +137 -47
  174. package/server/load-env.js +1 -1
  175. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  176. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  177. package/server/modules/browser-use/browser-use.service.ts +836 -0
  178. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  179. package/server/modules/database/index.ts +1 -0
  180. package/server/modules/database/migrations.ts +25 -0
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  182. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  183. package/server/modules/database/repositories/sessions.db.ts +227 -23
  184. package/server/modules/database/schema.ts +27 -0
  185. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  186. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  187. package/server/modules/notifications/index.ts +13 -0
  188. package/server/modules/notifications/notifications.routes.ts +127 -0
  189. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  190. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  191. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  192. package/server/modules/projects/services/project-management.service.ts +0 -8
  193. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  194. package/server/modules/providers/README.md +1 -1
  195. package/server/modules/providers/index.ts +1 -0
  196. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  197. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  198. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  199. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  200. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  201. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  202. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  203. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  204. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  205. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  206. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  207. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  208. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  209. package/server/modules/providers/provider.routes.ts +167 -1
  210. package/server/modules/providers/services/mcp.service.ts +26 -0
  211. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  212. package/server/modules/providers/services/provider-models.service.ts +1 -1
  213. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  214. package/server/modules/providers/services/sessions.service.ts +79 -4
  215. package/server/modules/providers/services/skills.service.ts +25 -1
  216. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  217. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  218. package/server/modules/providers/tests/skills.test.ts +212 -0
  219. package/server/modules/websocket/README.md +33 -27
  220. package/server/modules/websocket/index.ts +1 -0
  221. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  222. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  223. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  224. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  225. package/server/modules/websocket/services/websocket-server.service.ts +6 -0
  226. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  227. package/server/openai-codex.js +24 -14
  228. package/server/opencode-cli.js +21 -8
  229. package/server/opencode-cli.test.js +20 -1
  230. package/server/routes/settings.js +6 -1
  231. package/server/services/notification-orchestrator.js +3 -224
  232. package/server/services/tests/notification-orchestrator.test.js +80 -0
  233. package/server/shared/interfaces.ts +15 -0
  234. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  235. package/server/shared/types.ts +84 -1
  236. package/server/shared/utils.ts +95 -1
  237. package/server/voice-proxy.js +224 -0
  238. package/dist/assets/index-73Xw3i9v.css +0 -32
  239. package/dist/assets/index-BIHaviaA.js +0 -1361
  240. package/dist/modelConstants.js +0 -854
  241. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  242. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  243. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  244. package/public/modelConstants.js +0 -854
  245. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  246. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  247. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -5,7 +5,7 @@ import readline from 'node:readline';
5
5
 
6
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
7
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
8
- import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
8
+ import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
9
9
  import { sessionsDb } from '@/modules/database/index.js';
10
10
 
11
11
  const PROVIDER = 'claude';
@@ -103,10 +103,13 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
103
103
 
104
104
  async function getSessionMessages(
105
105
  sessionId: string,
106
+ providerSessionId: string,
106
107
  limit: number | null,
107
108
  offset: number,
108
109
  ): Promise<ClaudeHistoryMessagesResult> {
109
110
  try {
111
+ // The DB row is keyed by the app-facing session id, while the JSONL rows
112
+ // on disk carry the provider-native id — both ids are needed here.
110
113
  const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
111
114
 
112
115
  if (!jsonLPath) {
@@ -133,7 +136,7 @@ async function getSessionMessages(
133
136
 
134
137
  try {
135
138
  const entry = JSON.parse(line) as AnyRecord;
136
- if (entry.sessionId === sessionId) {
139
+ if (entry.sessionId === providerSessionId) {
137
140
  messages.push(entry);
138
141
  }
139
142
  } catch {
@@ -553,12 +556,13 @@ export class ClaudeSessionsProvider implements IProviderSessions {
553
556
  options: FetchHistoryOptions = {},
554
557
  ): Promise<FetchHistoryResult> {
555
558
  const { limit = null, offset = 0 } = options;
559
+ const providerSessionId = options.providerSessionId ?? sessionId;
556
560
 
557
561
  let result: ClaudeHistoryResult;
558
562
  try {
559
563
  // Load full history first so `total` reflects frontend-normalized messages,
560
564
  // not raw JSONL records.
561
- result = await getSessionMessages(sessionId, null, 0);
565
+ result = await getSessionMessages(sessionId, providerSessionId, null, 0);
562
566
  } catch (error) {
563
567
  const message = error instanceof Error ? error.message : String(error);
564
568
  console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
@@ -606,7 +610,6 @@ export class ClaudeSessionsProvider implements IProviderSessions {
606
610
  }
607
611
  }
608
612
 
609
- const totalNormalized = normalized.length;
610
613
  let total = 0;
611
614
  for (const msg of normalized) {
612
615
  if (msg.kind !== 'tool_result') {
@@ -615,18 +618,10 @@ export class ClaudeSessionsProvider implements IProviderSessions {
615
618
  }
616
619
  const normalizedOffset = Math.max(0, offset);
617
620
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
618
- const messages = normalizedLimit === null
619
- ? normalized
620
- : normalized.slice(
621
- Math.max(0, totalNormalized - normalizedOffset - normalizedLimit),
622
- Math.max(0, totalNormalized - normalizedOffset),
623
- );
624
- const hasMore = normalizedLimit === null
625
- ? false
626
- : Math.max(0, totalNormalized - normalizedOffset - normalizedLimit) > 0;
621
+ const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
627
622
 
628
623
  return {
629
- messages,
624
+ messages: page,
630
625
  total,
631
626
  hasMore,
632
627
  offset: normalizedOffset,
@@ -99,6 +99,14 @@ export class ClaudeSkillsProvider extends SkillsProvider {
99
99
  ];
100
100
  }
101
101
 
102
+ protected async getGlobalSkillSource(): Promise<ProviderSkillSource> {
103
+ return {
104
+ scope: 'user',
105
+ rootDir: path.join(getClaudeHomePath(), 'skills'),
106
+ commandPrefix: '/',
107
+ };
108
+ }
109
+
102
110
  private async listPluginSkills(claudeHomePath: string): Promise<ProviderSkill[]> {
103
111
  const settings = await readJsonConfig(path.join(claudeHomePath, 'settings.json'));
104
112
  const enabledPlugins = readObjectRecord(settings.enabledPlugins);
@@ -43,11 +43,12 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
43
43
  continue;
44
44
  }
45
45
 
46
- const existingSession = sessionsDb.getSessionById(parsed.sessionId);
46
+ const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId)
47
+ ?? sessionsDb.getSessionById(parsed.sessionId);
47
48
  if (existingSession) {
48
49
  // If session name is untitled and we now have a name, update it
49
50
  if (existingSession.custom_name === 'Untitled Codex Session' && parsed.sessionName && parsed.sessionName !== 'Untitled Codex Session') {
50
- sessionsDb.updateSessionCustomName(parsed.sessionId, parsed.sessionName);
51
+ sessionsDb.updateSessionCustomName(existingSession.session_id, parsed.sessionName);
51
52
  }
52
53
  }
53
54
 
@@ -120,7 +121,10 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
120
121
  return null;
121
122
  }
122
123
 
123
- const existingSession = sessionsDb.getSessionById(parsed.sessionId);
124
+ // App-created sessions are keyed by an app id, so disk-discovered provider
125
+ // ids must be resolved through the provider-id mapping first.
126
+ const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId)
127
+ ?? sessionsDb.getSessionById(parsed.sessionId);
124
128
  const existingSessionName = existingSession?.custom_name;
125
129
  if (existingSessionName && existingSessionName !== 'Untitled Codex Session') {
126
130
  return {
@@ -4,7 +4,7 @@ import readline from 'node:readline';
4
4
  import { sessionsDb } from '@/modules/database/index.js';
5
5
  import type { IProviderSessions } from '@/shared/interfaces.js';
6
6
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
- import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
7
+ import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
8
8
 
9
9
  const PROVIDER = 'codex';
10
10
 
@@ -552,7 +552,6 @@ export class CodexSessionsProvider implements IProviderSessions {
552
552
  }
553
553
  }
554
554
 
555
- const totalNormalized = normalized.length;
556
555
  let total = 0;
557
556
  for (const msg of normalized) {
558
557
  if (msg.kind !== 'tool_result') {
@@ -561,18 +560,10 @@ export class CodexSessionsProvider implements IProviderSessions {
561
560
  }
562
561
  const normalizedOffset = Math.max(0, offset);
563
562
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
564
- const messages = normalizedLimit === null
565
- ? normalized
566
- : normalized.slice(
567
- Math.max(0, totalNormalized - normalizedOffset - normalizedLimit),
568
- Math.max(0, totalNormalized - normalizedOffset),
569
- );
570
- const hasMore = normalizedLimit === null
571
- ? false
572
- : Math.max(0, totalNormalized - normalizedOffset - normalizedLimit) > 0;
563
+ const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
573
564
 
574
565
  return {
575
- messages,
566
+ messages: page,
576
567
  total,
577
568
  hasMore,
578
569
  offset: normalizedOffset,
@@ -57,4 +57,12 @@ export class CodexSkillsProvider extends SkillsProvider {
57
57
 
58
58
  return sources;
59
59
  }
60
+
61
+ protected async getGlobalSkillSource(): Promise<ProviderSkillSource> {
62
+ return {
63
+ scope: 'user',
64
+ rootDir: path.join(os.homedir(), '.agents', 'skills'),
65
+ commandPrefix: '$',
66
+ };
67
+ }
60
68
  }
@@ -9,6 +9,7 @@ import {
9
9
  generateMessageId,
10
10
  readObjectRecord,
11
11
  sanitizeLeafDirectoryName,
12
+ sliceTailPage,
12
13
  } from '@/shared/utils.js';
13
14
 
14
15
  const PROVIDER = 'cursor';
@@ -363,42 +364,32 @@ export class CursorSessionsProvider implements IProviderSessions {
363
364
 
364
365
  /**
365
366
  * Fetches and paginates Cursor session history from its project-scoped store.db.
367
+ *
368
+ * Pagination follows the shared tail contract (`sliceTailPage`): offset 0 is
369
+ * the most recent page, matching every other provider.
366
370
  */
367
371
  async fetchHistory(
368
372
  sessionId: string,
369
373
  options: FetchHistoryOptions = {},
370
374
  ): Promise<FetchHistoryResult> {
371
375
  const { projectPath = '', limit = null, offset = 0 } = options;
376
+ // The store.db folder on disk is named after the provider-native id, not
377
+ // the app-facing session id this method is addressed with.
378
+ const providerSessionId = options.providerSessionId ?? sessionId;
372
379
 
373
380
  try {
374
- const blobs = await this.loadCursorBlobs(sessionId, projectPath);
381
+ const blobs = await this.loadCursorBlobs(providerSessionId, projectPath);
375
382
  const allNormalized = this.normalizeCursorBlobs(blobs, sessionId);
376
383
  const renderableMessages = allNormalized.filter((msg) => msg.kind !== 'tool_result');
377
384
  const total = renderableMessages.length;
378
-
379
- if (limit !== null) {
380
- const start = offset;
381
- const page = limit === 0
382
- ? []
383
- : renderableMessages.slice(start, start + limit);
384
- const hasMore = limit === 0
385
- ? start < total
386
- : start + limit < total;
387
- return {
388
- messages: page,
389
- total,
390
- hasMore,
391
- offset,
392
- limit,
393
- };
394
- }
385
+ const { page, hasMore } = sliceTailPage(renderableMessages, limit, offset);
395
386
 
396
387
  return {
397
- messages: renderableMessages,
388
+ messages: page,
398
389
  total,
399
- hasMore: false,
400
- offset: 0,
401
- limit: null,
390
+ hasMore,
391
+ offset,
392
+ limit,
402
393
  };
403
394
  } catch (error) {
404
395
  const message = error instanceof Error ? error.message : String(error);
@@ -28,4 +28,12 @@ export class CursorSkillsProvider extends SkillsProvider {
28
28
  },
29
29
  ];
30
30
  }
31
+
32
+ protected async getGlobalSkillSource(): Promise<ProviderSkillSource> {
33
+ return {
34
+ scope: 'user',
35
+ rootDir: path.join(os.homedir(), '.cursor', 'skills'),
36
+ commandPrefix: '/',
37
+ };
38
+ }
31
39
  }
@@ -12,17 +12,14 @@ import {
12
12
 
13
13
  export const GEMINI_FALLBACK_MODELS: ProviderModelsDefinition = {
14
14
  OPTIONS: [
15
- { value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
16
- { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
17
15
  { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
16
+ { value: 'gemini-3.1-flash-lite-preview', label: 'Gemini 3.1 Flash Lite Preview' },
18
17
  { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
19
- { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
20
- { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' },
21
- { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
22
- { value: 'gemini-2.0-pro-exp', label: 'Gemini 2.0 Pro Experimental' },
23
- { value: 'gemini-2.0-flash-thinking-exp', label: 'Gemini 2.0 Flash Thinking' },
18
+ { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
19
+ { value: 'gemma-4-31b-it', label: 'Gemma 4 31B IT' },
20
+ { value: 'gemma-4-26b-a4b-it', label: 'Gemma 4 26B A4B IT' },
24
21
  ],
25
- DEFAULT: 'gemini-3.1-pro-preview',
22
+ DEFAULT: 'gemini-3-flash-preview',
26
23
  };
27
24
 
28
25
  export class GeminiProviderModels implements IProviderModels {
@@ -5,7 +5,7 @@ import readline from 'node:readline';
5
5
  import { sessionsDb } from '@/modules/database/index.js';
6
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
7
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
8
- import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
8
+ import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
9
9
 
10
10
  const PROVIDER = 'gemini';
11
11
 
@@ -518,9 +518,9 @@ export class GeminiSessionsProvider implements IProviderSessions {
518
518
 
519
519
  const start = Math.max(0, offset);
520
520
  const pageLimit = limit === null ? null : Math.max(0, limit);
521
- const messages = pageLimit === null
522
- ? normalized.slice(start)
523
- : normalized.slice(start, start + pageLimit);
521
+ // Tail pagination via the shared contract: offset 0 returns the most
522
+ // recent page, matching every other provider.
523
+ const { page, hasMore } = sliceTailPage(normalized, pageLimit, start);
524
524
  let total = 0;
525
525
  for (const msg of normalized) {
526
526
  if (msg.kind !== 'tool_result') {
@@ -529,9 +529,9 @@ export class GeminiSessionsProvider implements IProviderSessions {
529
529
  }
530
530
 
531
531
  return {
532
- messages,
532
+ messages: page,
533
533
  total,
534
- hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
534
+ hasMore,
535
535
  offset: start,
536
536
  limit: pageLimit,
537
537
  tokenUsage: result.tokenUsage,
@@ -33,4 +33,12 @@ export class GeminiSkillsProvider extends SkillsProvider {
33
33
  },
34
34
  ];
35
35
  }
36
+
37
+ protected async getGlobalSkillSource(): Promise<ProviderSkillSource> {
38
+ return {
39
+ scope: 'user',
40
+ rootDir: path.join(os.homedir(), '.gemini', 'skills'),
41
+ commandPrefix: '/',
42
+ };
43
+ }
36
44
  }
@@ -112,7 +112,21 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
112
112
  }
113
113
 
114
114
  const fallbackTitle = 'Untitled OpenCode Session';
115
- const existingSession = sessionsDb.getSessionById(sessionId);
115
+ const pendingAppSession = sessionsDb.getSessionByProviderSessionId(sessionId)
116
+ ?? sessionsDb.getSessionById(sessionId)
117
+ ?? sessionsDb.findLatestPendingAppSession(this.provider, projectPath);
118
+ if (pendingAppSession && !pendingAppSession.provider_session_id) {
119
+ // Slow networks can let the sqlite watcher index opencode.db before the
120
+ // runtime reports its provider id back through the websocket mapping.
121
+ // Bind that id to the fresh app row first so the watcher does not create
122
+ // a temporary provider-id sidebar entry for the same session.
123
+ sessionsDb.assignProviderSessionId(pendingAppSession.session_id, sessionId);
124
+ }
125
+
126
+ // App-created sessions are keyed by an app id, so disk-discovered provider
127
+ // ids must be resolved through the provider-id mapping first.
128
+ const existingSession = sessionsDb.getSessionByProviderSessionId(sessionId)
129
+ ?? sessionsDb.getSessionById(sessionId);
116
130
  const existingName = existingSession?.custom_name;
117
131
  const nextName = existingName && existingName !== fallbackTitle
118
132
  ? existingName
@@ -120,7 +134,9 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
120
134
 
121
135
  // OpenCode stores every session in one shared sqlite database, so jsonl_path
122
136
  // must stay null to avoid deleting opencode.db when one app session is removed.
123
- sessionsDb.createSession(
137
+ // Return the canonical stored row id so watcher-triggered sidebar updates
138
+ // stay on the app session once provider_session_id has already been mapped.
139
+ return sessionsDb.createSession(
124
140
  sessionId,
125
141
  this.provider,
126
142
  projectPath,
@@ -129,8 +145,6 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
129
145
  normalizeProviderTimestamp(row.time_updated ?? row.time_created),
130
146
  null,
131
147
  );
132
-
133
- return sessionId;
134
148
  }
135
149
 
136
150
  private readFirstUserText(db: Database.Database, sessionId: string): string | undefined {
@@ -12,6 +12,7 @@ import {
12
12
  readObjectRecord,
13
13
  readJsonRecord,
14
14
  readOptionalString,
15
+ sliceTailPage,
15
16
  } from '@/shared/utils.js';
16
17
 
17
18
  const PROVIDER = 'opencode';
@@ -325,6 +326,9 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
325
326
  options: FetchHistoryOptions = {},
326
327
  ): Promise<FetchHistoryResult> {
327
328
  const { limit = null, offset = 0 } = options;
329
+ // OpenCode's shared sqlite database keys messages by the provider-native
330
+ // session id, not the app-facing id this method is addressed with.
331
+ const providerSessionId = options.providerSessionId ?? sessionId;
328
332
  const db = openOpenCodeDatabase();
329
333
  if (!db) {
330
334
  return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
@@ -349,27 +353,20 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
349
353
  m.id,
350
354
  COALESCE(p.time_created, 0),
351
355
  p.id
352
- `).all(sessionId) as OpenCodeHistoryRow[];
356
+ `).all(providerSessionId) as OpenCodeHistoryRow[];
353
357
 
354
358
  const normalized = this.normalizeHistoryRows(rows, sessionId);
355
- const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, sessionId);
359
+ const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, providerSessionId);
356
360
 
357
361
  const normalizedOffset = Math.max(0, offset);
358
362
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
359
363
  const total = normalized.length;
360
- const messages = normalizedLimit === null
361
- ? normalized
362
- : normalized.slice(
363
- Math.max(0, total - normalizedOffset - normalizedLimit),
364
- Math.max(0, total - normalizedOffset),
365
- );
364
+ const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
366
365
 
367
366
  return {
368
- messages,
367
+ messages: page,
369
368
  total,
370
- hasMore: normalizedLimit === null
371
- ? false
372
- : Math.max(0, total - normalizedOffset - normalizedLimit) > 0,
369
+ hasMore,
373
370
  offset: normalizedOffset,
374
371
  limit: normalizedLimit,
375
372
  tokenUsage,
@@ -1,6 +1,7 @@
1
1
  import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
+ import { providerCapabilitiesService } from '@/modules/providers/services/provider-capabilities.service.js';
4
5
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
6
  import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
6
7
  import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
@@ -11,6 +12,8 @@ import type {
11
12
  McpScope,
12
13
  McpTransport,
13
14
  ProviderChangeActiveModelInput,
15
+ ProviderSkillCreateFile,
16
+ ProviderSkillCreateInput,
14
17
  UpsertProviderMcpServerInput,
15
18
  } from '@/shared/types.js';
16
19
  import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
@@ -178,6 +181,104 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
178
181
  };
179
182
  };
180
183
 
184
+ const parseProviderSkillCreatePayload = (payload: unknown): ProviderSkillCreateInput => {
185
+ if (!payload || typeof payload !== 'object') {
186
+ throw new AppError('Request body must be an object.', {
187
+ code: 'INVALID_REQUEST_BODY',
188
+ statusCode: 400,
189
+ });
190
+ }
191
+
192
+ const body = payload as Record<string, unknown>;
193
+ const rawEntries = Array.isArray(body.entries)
194
+ ? body.entries
195
+ : typeof body.content === 'string'
196
+ ? [{
197
+ content: body.content,
198
+ directoryName: body.directoryName,
199
+ fileName: body.fileName,
200
+ files: body.files,
201
+ }]
202
+ : null;
203
+
204
+ if (!rawEntries || rawEntries.length === 0) {
205
+ throw new AppError('At least one skill entry is required.', {
206
+ code: 'PROVIDER_SKILLS_REQUIRED',
207
+ statusCode: 400,
208
+ });
209
+ }
210
+
211
+ const entries = rawEntries.map((entry, index) => {
212
+ if (!entry || typeof entry !== 'object') {
213
+ throw new AppError(`Skill entry ${index + 1} must be an object.`, {
214
+ code: 'INVALID_REQUEST_BODY',
215
+ statusCode: 400,
216
+ });
217
+ }
218
+
219
+ const record = entry as Record<string, unknown>;
220
+ const content = typeof record.content === 'string' ? record.content : '';
221
+ const directoryName = readOptionalQueryString(record.directoryName);
222
+ const fileName = readOptionalQueryString(record.fileName);
223
+ const rawFiles = record.files;
224
+
225
+ if (!content.trim()) {
226
+ throw new AppError(`Skill entry ${index + 1} must include markdown content.`, {
227
+ code: 'PROVIDER_SKILL_CONTENT_REQUIRED',
228
+ statusCode: 400,
229
+ });
230
+ }
231
+
232
+ if (rawFiles !== undefined && !Array.isArray(rawFiles)) {
233
+ throw new AppError(`Skill entry ${index + 1} files must be an array.`, {
234
+ code: 'INVALID_REQUEST_BODY',
235
+ statusCode: 400,
236
+ });
237
+ }
238
+
239
+ const files: ProviderSkillCreateFile[] | undefined = rawFiles?.map((file, fileIndex) => {
240
+ if (!file || typeof file !== 'object') {
241
+ throw new AppError(`Skill entry ${index + 1} file ${fileIndex + 1} must be an object.`, {
242
+ code: 'INVALID_REQUEST_BODY',
243
+ statusCode: 400,
244
+ });
245
+ }
246
+
247
+ const fileRecord = file as Record<string, unknown>;
248
+ const relativePath = readOptionalQueryString(fileRecord.relativePath);
249
+ const fileContent = typeof fileRecord.content === 'string' ? fileRecord.content : null;
250
+ const encoding = fileRecord.encoding === 'utf8' || fileRecord.encoding === 'base64'
251
+ ? fileRecord.encoding
252
+ : null;
253
+
254
+ if (!relativePath || fileContent === null || !encoding) {
255
+ throw new AppError(
256
+ `Skill entry ${index + 1} file ${fileIndex + 1} requires relativePath, content, and encoding.`,
257
+ {
258
+ code: 'INVALID_REQUEST_BODY',
259
+ statusCode: 400,
260
+ },
261
+ );
262
+ }
263
+
264
+ return {
265
+ relativePath,
266
+ content: fileContent,
267
+ encoding,
268
+ };
269
+ });
270
+
271
+ return {
272
+ content,
273
+ directoryName,
274
+ fileName,
275
+ files,
276
+ };
277
+ });
278
+
279
+ return { entries };
280
+ };
281
+
181
282
  const parseProvider = (value: unknown): LLMProvider => {
182
283
  const normalized = normalizeProviderParam(value);
183
284
  if (
@@ -319,6 +420,27 @@ router.get(
319
420
  }),
320
421
  );
321
422
 
423
+ router.post(
424
+ '/:provider/skills',
425
+ asyncHandler(async (req: Request, res: Response) => {
426
+ const provider = parseProvider(req.params.provider);
427
+ const input = parseProviderSkillCreatePayload(req.body);
428
+ const skills = await providerSkillsService.addProviderSkills(provider, input);
429
+ res.json(createApiSuccessResponse({ provider, skills }));
430
+ }),
431
+ );
432
+
433
+ router.delete(
434
+ '/:provider/skills/:directoryName',
435
+ asyncHandler(async (req: Request, res: Response) => {
436
+ const provider = parseProvider(req.params.provider);
437
+ const result = await providerSkillsService.removeProviderSkill(provider, {
438
+ directoryName: readPathParam(req.params.directoryName, 'directoryName'),
439
+ });
440
+ res.json(createApiSuccessResponse(result));
441
+ }),
442
+ );
443
+
322
444
  // ----------------- MCP routes -----------------
323
445
  router.get(
324
446
  '/:provider/mcp/servers',
@@ -382,7 +504,51 @@ router.post(
382
504
  }),
383
505
  );
384
506
 
507
+ router.get(
508
+ '/capabilities',
509
+ asyncHandler(async (_req: Request, res: Response) => {
510
+ res.json(createApiSuccessResponse({
511
+ providers: providerCapabilitiesService.listAllProviderCapabilities(),
512
+ }));
513
+ }),
514
+ );
515
+
516
+ router.get(
517
+ '/:provider/capabilities',
518
+ asyncHandler(async (req: Request, res: Response) => {
519
+ const provider = parseProvider(req.params.provider);
520
+ res.json(createApiSuccessResponse(
521
+ providerCapabilitiesService.getProviderCapabilities(provider),
522
+ ));
523
+ }),
524
+ );
525
+
385
526
  // ----------------- Session routes -----------------
527
+ /**
528
+ * Session gateway entry point: allocates the stable app-facing session id for
529
+ * a brand-new chat. The frontend must call this before the first `chat.send`
530
+ * so the session id in the URL, the store, and the websocket all agree from
531
+ * the very first message — there is no client-visible session-id handoff.
532
+ */
533
+ router.post(
534
+ '/sessions',
535
+ asyncHandler(async (req: Request, res: Response) => {
536
+ const body = (req.body ?? {}) as Record<string, unknown>;
537
+ const provider = parseProvider(body.provider);
538
+ const projectPath = typeof body.projectPath === 'string' ? body.projectPath : '';
539
+ const result = sessionsService.createAppSession(provider, projectPath);
540
+ res.status(201).json(createApiSuccessResponse(result));
541
+ }),
542
+ );
543
+
544
+ router.get(
545
+ '/sessions/running',
546
+ asyncHandler(async (_req: Request, res: Response) => {
547
+ const sessions = sessionsService.listRunningSessions();
548
+ res.json(createApiSuccessResponse({ sessions }));
549
+ }),
550
+ );
551
+
386
552
  router.get(
387
553
  '/sessions/archived',
388
554
  asyncHandler(async (_req: Request, res: Response) => {
@@ -459,7 +625,7 @@ router.get(
459
625
  limit,
460
626
  offset,
461
627
  });
462
- res.json(result);
628
+ res.json(createApiSuccessResponse(result));
463
629
  }),
464
630
  );
465
631
 
@@ -80,4 +80,30 @@ export const providerMcpService = {
80
80
 
81
81
  return results;
82
82
  },
83
+
84
+ /**
85
+ * Removes one MCP server from every provider. Mirrors `addMcpServerToAllProviders`
86
+ * by iterating the live provider registry, so callers stay in sync with which
87
+ * providers exist instead of maintaining their own provider list.
88
+ */
89
+ async removeMcpServerFromAllProviders(
90
+ input: { name: string; scope?: McpScope; workspacePath?: string },
91
+ ): Promise<Array<{ provider: LLMProvider; removed: boolean; error?: string }>> {
92
+ const results: Array<{ provider: LLMProvider; removed: boolean; error?: string }> = [];
93
+ const providers = providerRegistry.listProviders();
94
+ for (const provider of providers) {
95
+ try {
96
+ const result = await provider.mcp.removeServer(input);
97
+ results.push({ provider: provider.id, removed: result.removed });
98
+ } catch (error) {
99
+ results.push({
100
+ provider: provider.id,
101
+ removed: false,
102
+ error: error instanceof Error ? error.message : 'Unknown error',
103
+ });
104
+ }
105
+ }
106
+
107
+ return results;
108
+ },
83
109
  };