@cloudcli-ai/cloudcli 1.31.5 → 1.32.0

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 (147) hide show
  1. package/dist/assets/index-AaSE7-3t.js +1336 -0
  2. package/dist/assets/index-CKaku72N.css +32 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/claude-sdk.js +10 -5
  5. package/dist-server/server/claude-sdk.js.map +1 -1
  6. package/dist-server/server/cursor-cli.js +0 -6
  7. package/dist-server/server/cursor-cli.js.map +1 -1
  8. package/dist-server/server/gemini-cli.js +160 -34
  9. package/dist-server/server/gemini-cli.js.map +1 -1
  10. package/dist-server/server/modules/database/migrations.js +11 -0
  11. package/dist-server/server/modules/database/migrations.js.map +1 -1
  12. package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
  13. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
  14. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
  15. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
  16. package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
  17. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  18. package/dist-server/server/modules/database/schema.js +1 -0
  19. package/dist-server/server/modules/database/schema.js.map +1 -1
  20. package/dist-server/server/modules/projects/projects.routes.js +12 -3
  21. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  22. package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
  23. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  24. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +43 -0
  25. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  26. package/dist-server/server/modules/providers/index.js +1 -0
  27. package/dist-server/server/modules/providers/index.js.map +1 -1
  28. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +2 -1
  29. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  30. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
  31. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  32. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
  33. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  34. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
  35. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  36. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  37. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
  39. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
  41. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +82 -0
  43. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  44. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  45. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
  47. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +189 -13
  49. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
  51. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  52. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  53. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
  55. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
  57. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +7 -1
  59. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
  61. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
  62. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  63. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  64. package/dist-server/server/modules/providers/provider.routes.js +23 -2
  65. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
  67. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -5
  69. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
  71. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  73. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  74. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  75. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
  76. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  77. package/dist-server/server/modules/providers/tests/skills.test.js +226 -0
  78. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  79. package/dist-server/server/openai-codex.js +56 -26
  80. package/dist-server/server/openai-codex.js.map +1 -1
  81. package/dist-server/server/routes/commands.js +5 -5
  82. package/dist-server/server/routes/commands.js.map +1 -1
  83. package/dist-server/server/shared/claude-cli-path.js +103 -0
  84. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  85. package/dist-server/server/shared/claude-cli-path.test.js +45 -0
  86. package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
  87. package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
  88. package/dist-server/server/shared/frontmatter.js.map +1 -0
  89. package/dist-server/server/shared/utils.js +82 -0
  90. package/dist-server/server/shared/utils.js.map +1 -1
  91. package/dist-server/server/utils/commandParser.js +3 -3
  92. package/dist-server/server/utils/commandParser.js.map +1 -1
  93. package/dist-server/shared/modelConstants.js +1 -0
  94. package/dist-server/shared/modelConstants.js.map +1 -1
  95. package/package.json +1 -1
  96. package/server/claude-sdk.js +10 -5
  97. package/server/cursor-cli.js +0 -8
  98. package/server/gemini-cli.js +190 -42
  99. package/server/modules/database/migrations.ts +12 -0
  100. package/server/modules/database/repositories/projects.db.ts +13 -0
  101. package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
  102. package/server/modules/database/repositories/sessions.db.ts +60 -9
  103. package/server/modules/database/schema.ts +1 -0
  104. package/server/modules/projects/projects.routes.ts +20 -3
  105. package/server/modules/projects/services/project-delete.service.ts +16 -1
  106. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +64 -0
  107. package/server/modules/providers/README.md +346 -0
  108. package/server/modules/providers/index.ts +2 -1
  109. package/server/modules/providers/list/claude/claude-auth.provider.ts +8 -7
  110. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
  111. package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
  112. package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
  113. package/server/modules/providers/list/claude/claude.provider.ts +8 -1
  114. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
  115. package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
  116. package/server/modules/providers/list/codex/codex-skills.provider.ts +100 -0
  117. package/server/modules/providers/list/codex/codex.provider.ts +8 -1
  118. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
  119. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -13
  120. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
  121. package/server/modules/providers/list/cursor/cursor.provider.ts +8 -1
  122. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
  123. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
  124. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +7 -1
  125. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
  126. package/server/modules/providers/list/gemini/gemini.provider.ts +8 -1
  127. package/server/modules/providers/provider.routes.ts +35 -2
  128. package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
  129. package/server/modules/providers/services/sessions-watcher.service.ts +7 -5
  130. package/server/modules/providers/services/sessions.service.ts +112 -9
  131. package/server/modules/providers/services/skills.service.ts +15 -0
  132. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  133. package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
  134. package/server/modules/providers/tests/skills.test.ts +446 -0
  135. package/server/openai-codex.js +59 -27
  136. package/server/routes/commands.js +7 -5
  137. package/server/shared/claude-cli-path.test.ts +61 -0
  138. package/server/shared/claude-cli-path.ts +139 -0
  139. package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
  140. package/server/shared/interfaces.ts +19 -0
  141. package/server/shared/types.ts +78 -0
  142. package/server/shared/utils.ts +94 -0
  143. package/server/utils/commandParser.js +5 -3
  144. package/shared/modelConstants.js +1 -0
  145. package/dist/assets/index-6rYG_mRo.css +0 -32
  146. package/dist/assets/index-DqMVUeZS.js +0 -1331
  147. package/dist-server/server/utils/frontmatter.js.map +0 -1
@@ -15,7 +15,24 @@ type GeminiCredentialsStatus = {
15
15
  error?: string;
16
16
  };
17
17
 
18
+ type GeminiAuthType =
19
+ | 'oauth-personal'
20
+ | 'gemini-api-key'
21
+ | 'vertex-ai'
22
+ | 'compute-default-credentials'
23
+ | 'gateway'
24
+ | 'cloud-shell'
25
+ | null;
26
+
18
27
  export class GeminiProviderAuth implements IProviderAuth {
28
+ /**
29
+ * Gemini CLI can override its home root via GEMINI_CLI_HOME.
30
+ * Use the same resolution so status checks match runtime behavior.
31
+ */
32
+ private getGeminiCliHome(): string {
33
+ return process.env.GEMINI_CLI_HOME?.trim() || os.homedir();
34
+ }
35
+
19
36
  /**
20
37
  * Checks whether the Gemini CLI is available on this host.
21
38
  */
@@ -58,6 +75,88 @@ export class GeminiProviderAuth implements IProviderAuth {
58
75
  };
59
76
  }
60
77
 
78
+ /**
79
+ * Parses dotenv-style key/value pairs.
80
+ */
81
+ private parseEnvFile(content: string): Record<string, string> {
82
+ const parsed: Record<string, string> = {};
83
+
84
+ for (const rawLine of content.split(/\r?\n/)) {
85
+ const line = rawLine.trim();
86
+ if (!line || line.startsWith('#')) {
87
+ continue;
88
+ }
89
+
90
+ const normalizedLine = line.startsWith('export ')
91
+ ? line.slice('export '.length).trim()
92
+ : line;
93
+ const separatorIndex = normalizedLine.indexOf('=');
94
+ if (separatorIndex <= 0) {
95
+ continue;
96
+ }
97
+
98
+ const key = normalizedLine.slice(0, separatorIndex).trim();
99
+ if (!key) {
100
+ continue;
101
+ }
102
+
103
+ let value = normalizedLine.slice(separatorIndex + 1).trim();
104
+ const quoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''));
105
+ if (quoted) {
106
+ value = value.slice(1, -1);
107
+ } else {
108
+ value = value.replace(/\s+#.*$/, '').trim();
109
+ }
110
+
111
+ parsed[key] = value;
112
+ }
113
+
114
+ return parsed;
115
+ }
116
+
117
+ /**
118
+ * Loads user-level auth env in Gemini's "first file found" order.
119
+ */
120
+ private async loadUserLevelAuthEnv(): Promise<Record<string, string>> {
121
+ const geminiCliHome = this.getGeminiCliHome();
122
+ const envCandidates = [
123
+ path.join(geminiCliHome, '.gemini', '.env'),
124
+ path.join(geminiCliHome, '.env'),
125
+ ];
126
+
127
+ for (const envPath of envCandidates) {
128
+ try {
129
+ const content = await readFile(envPath, 'utf8');
130
+ return this.parseEnvFile(content);
131
+ } catch {
132
+ // Continue to the next fallback.
133
+ }
134
+ }
135
+
136
+ return {};
137
+ }
138
+
139
+ /**
140
+ * Reads Gemini's selected auth type from settings.json when available.
141
+ */
142
+ private async readSelectedAuthType(): Promise<GeminiAuthType> {
143
+ try {
144
+ const settingsPath = path.join(this.getGeminiCliHome(), '.gemini', 'settings.json');
145
+ const content = await readFile(settingsPath, 'utf8');
146
+ const settings = readObjectRecord(JSON.parse(content));
147
+ const security = readObjectRecord(settings?.security);
148
+ const auth = readObjectRecord(security?.auth);
149
+ const selectedType = readOptionalString(auth?.selectedType);
150
+ if (!selectedType) {
151
+ return null;
152
+ }
153
+
154
+ return selectedType as GeminiAuthType;
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
61
160
  /**
62
161
  * Checks Gemini credentials from API key env vars or local OAuth credential files.
63
162
  */
@@ -66,8 +165,46 @@ export class GeminiProviderAuth implements IProviderAuth {
66
165
  return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
67
166
  }
68
167
 
168
+ const userEnv = await this.loadUserLevelAuthEnv();
169
+ if (readOptionalString(userEnv.GEMINI_API_KEY)) {
170
+ return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
171
+ }
172
+
173
+ const selectedType = await this.readSelectedAuthType();
174
+ if (selectedType === 'vertex-ai') {
175
+ const hasGoogleApiKey = Boolean(
176
+ process.env.GOOGLE_API_KEY?.trim()
177
+ || readOptionalString(userEnv.GOOGLE_API_KEY)
178
+ );
179
+ const hasProject = Boolean(
180
+ process.env.GOOGLE_CLOUD_PROJECT?.trim()
181
+ || process.env.GOOGLE_CLOUD_PROJECT_ID?.trim()
182
+ || readOptionalString(userEnv.GOOGLE_CLOUD_PROJECT)
183
+ || readOptionalString(userEnv.GOOGLE_CLOUD_PROJECT_ID)
184
+ );
185
+ const hasLocation = Boolean(
186
+ process.env.GOOGLE_CLOUD_LOCATION?.trim()
187
+ || readOptionalString(userEnv.GOOGLE_CLOUD_LOCATION)
188
+ );
189
+ const hasServiceAccount = Boolean(
190
+ process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim()
191
+ || readOptionalString(userEnv.GOOGLE_APPLICATION_CREDENTIALS)
192
+ );
193
+
194
+ if (hasGoogleApiKey || hasServiceAccount || (hasProject && hasLocation)) {
195
+ return { authenticated: true, email: 'Vertex AI Auth', method: 'vertex_ai' };
196
+ }
197
+
198
+ return {
199
+ authenticated: false,
200
+ email: null,
201
+ method: 'vertex_ai',
202
+ error: 'Gemini is set to Vertex AI, but required env vars are missing',
203
+ };
204
+ }
205
+
69
206
  try {
70
- const credsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
207
+ const credsPath = path.join(this.getGeminiCliHome(), '.gemini', 'oauth_creds.json');
71
208
  const content = await readFile(credsPath, 'utf8');
72
209
  const creds = readObjectRecord(JSON.parse(content)) ?? {};
73
210
  const accessToken = readOptionalString(creds.access_token);
@@ -106,6 +243,25 @@ export class GeminiProviderAuth implements IProviderAuth {
106
243
  method: 'credentials_file',
107
244
  };
108
245
  } catch {
246
+ if (selectedType === 'gemini-api-key') {
247
+ return {
248
+ authenticated: false,
249
+ email: null,
250
+ method: 'api_key',
251
+ error: 'Gemini is set to "Use Gemini API key", but GEMINI_API_KEY is unavailable',
252
+ };
253
+ }
254
+
255
+ if (selectedType === 'oauth-personal') {
256
+ return {
257
+ authenticated: false,
258
+ email: null,
259
+ method: 'credentials_file',
260
+ error: 'Gemini is set to Google sign-in, but no cached OAuth credentials were found',
261
+ };
262
+ }
263
+
264
+ // If no explicit auth type was selected, surface the generic "not configured" error.
109
265
  return {
110
266
  authenticated: false,
111
267
  email: null,
@@ -140,7 +296,7 @@ export class GeminiProviderAuth implements IProviderAuth {
140
296
  */
141
297
  private async getActiveAccountEmail(): Promise<string | null> {
142
298
  try {
143
- const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
299
+ const accPath = path.join(this.getGeminiCliHome(), '.gemini', 'google_accounts.json');
144
300
  const accContent = await readFile(accPath, 'utf8');
145
301
  const accounts = readObjectRecord(JSON.parse(accContent));
146
302
  return readOptionalString(accounts?.active) ?? null;
@@ -39,33 +39,37 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
39
39
  async synchronize(since?: Date): Promise<number> {
40
40
  const projectHashLookup = this.buildProjectHashLookup();
41
41
 
42
- const legacySessionFiles = await findFilesRecursivelyCreatedAfter(
43
- path.join(this.geminiHome, 'sessions'),
44
- '.json',
45
- since ?? null
46
- );
47
- const legacyTempFiles = await findFilesRecursivelyCreatedAfter(
48
- path.join(this.geminiHome, 'tmp'),
49
- '.json',
50
- since ?? null
51
- );
52
- const jsonlSessionFiles = await findFilesRecursivelyCreatedAfter(
53
- path.join(this.geminiHome, 'sessions'),
54
- '.jsonl',
55
- since ?? null
56
- );
42
+ // const legacySessionFiles = await findFilesRecursivelyCreatedAfter(
43
+ // path.join(this.geminiHome, 'sessions'),
44
+ // '.json',
45
+ // since ?? null
46
+ // );
47
+ // Gemini creates overlapping artifacts across `sessions/` and `tmp/`.
48
+ // We currently index only `tmp/*/chats/*.jsonl` because those files are the
49
+ // live transcript source and avoid duplicate session rows from mirrored files.
50
+ // const legacyTempFiles = await findFilesRecursivelyCreatedAfter(
51
+ // path.join(this.geminiHome, 'tmp'),
52
+ // '.json',
53
+ // since ?? null
54
+ // );
55
+ // const jsonlSessionFiles = await findFilesRecursivelyCreatedAfter(
56
+ // path.join(this.geminiHome, 'sessions'),
57
+ // '.jsonl',
58
+ // since ?? null
59
+ // );
57
60
  const jsonlTempFiles = await findFilesRecursivelyCreatedAfter(
58
61
  path.join(this.geminiHome, 'tmp'),
59
62
  '.jsonl',
60
63
  since ?? null
61
64
  );
62
65
 
63
- // Process legacy JSON first, then JSONL. If both exist for a session id,
64
- // the JSONL artifact becomes the canonical jsonl_path via upsert.
66
+ // Current strategy: index only temp chat JSONL artifacts.
65
67
  const files = [
66
- ...legacySessionFiles,
67
- ...legacyTempFiles,
68
- ...jsonlSessionFiles,
68
+ // ...legacySessionFiles,
69
+ // Intentionally disabled to avoid duplicate indexing from mirrored
70
+ // `sessions/*.json` and `sessions/*.jsonl` artifacts.
71
+ // ...legacyTempFiles,
72
+ // ...jsonlSessionFiles,
69
73
  ...jsonlTempFiles,
70
74
  ];
71
75
 
@@ -528,10 +528,16 @@ export class GeminiSessionsProvider implements IProviderSessions {
528
528
  const messages = pageLimit === null
529
529
  ? normalized.slice(start)
530
530
  : normalized.slice(start, start + pageLimit);
531
+ let total = 0;
532
+ for (const msg of normalized) {
533
+ if (msg.kind !== 'tool_result') {
534
+ total += 1;
535
+ }
536
+ }
531
537
 
532
538
  return {
533
539
  messages,
534
- total: normalized.length,
540
+ total,
535
541
  hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
536
542
  offset: start,
537
543
  limit: pageLimit,
@@ -0,0 +1,36 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
5
+ import type { ProviderSkillSource } from '@/shared/types.js';
6
+
7
+ export class GeminiSkillsProvider extends SkillsProvider {
8
+ constructor() {
9
+ super('gemini');
10
+ }
11
+
12
+ protected async getSkillSources(workspacePath: string): Promise<ProviderSkillSource[]> {
13
+ return [
14
+ {
15
+ scope: 'user',
16
+ rootDir: path.join(os.homedir(), '.gemini', 'skills'),
17
+ commandPrefix: '/',
18
+ },
19
+ {
20
+ scope: 'user',
21
+ rootDir: path.join(os.homedir(), '.agents', 'skills'),
22
+ commandPrefix: '/',
23
+ },
24
+ {
25
+ scope: 'project',
26
+ rootDir: path.join(workspacePath, '.gemini', 'skills'),
27
+ commandPrefix: '/',
28
+ },
29
+ {
30
+ scope: 'project',
31
+ rootDir: path.join(workspacePath, '.agents', 'skills'),
32
+ commandPrefix: '/',
33
+ },
34
+ ];
35
+ }
36
+ }
@@ -3,11 +3,18 @@ import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.
3
3
  import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
4
4
  import { GeminiSessionSynchronizer } from '@/modules/providers/list/gemini/gemini-session-synchronizer.provider.js';
5
5
  import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js';
6
- import type { IProviderAuth, IProviderSessionSynchronizer, IProviderSessions } from '@/shared/interfaces.js';
6
+ import { GeminiSkillsProvider } from '@/modules/providers/list/gemini/gemini-skills.provider.js';
7
+ import type {
8
+ IProviderAuth,
9
+ IProviderSessionSynchronizer,
10
+ IProviderSkills,
11
+ IProviderSessions,
12
+ } from '@/shared/interfaces.js';
7
13
 
8
14
  export class GeminiProvider extends AbstractProvider {
9
15
  readonly mcp = new GeminiMcpProvider();
10
16
  readonly auth: IProviderAuth = new GeminiProviderAuth();
17
+ readonly skills: IProviderSkills = new GeminiSkillsProvider();
11
18
  readonly sessions: IProviderSessions = new GeminiSessionsProvider();
12
19
  readonly sessionSynchronizer: IProviderSessionSynchronizer = new GeminiSessionSynchronizer();
13
20
 
@@ -2,6 +2,7 @@ import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
4
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
+ import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
5
6
  import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
6
7
  import { sessionsService } from '@/modules/providers/services/sessions.service.js';
7
8
  import type { LLMProvider, McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/shared/types.js';
@@ -247,6 +248,17 @@ router.get(
247
248
  }),
248
249
  );
249
250
 
251
+ // ----------------- Skills routes -----------------
252
+ router.get(
253
+ '/:provider/skills',
254
+ asyncHandler(async (req: Request, res: Response) => {
255
+ const provider = parseProvider(req.params.provider);
256
+ const workspacePath = readOptionalQueryString(req.query.workspacePath);
257
+ const skills = await providerSkillsService.listProviderSkills(provider, { workspacePath });
258
+ res.json(createApiSuccessResponse({ provider, skills }));
259
+ }),
260
+ );
261
+
250
262
  // ----------------- MCP routes -----------------
251
263
  router.get(
252
264
  '/:provider/mcp/servers',
@@ -311,12 +323,33 @@ router.post(
311
323
  );
312
324
 
313
325
  // ----------------- Session routes -----------------
326
+ router.get(
327
+ '/sessions/archived',
328
+ asyncHandler(async (_req: Request, res: Response) => {
329
+ const sessions = sessionsService.listArchivedSessions();
330
+ res.json(createApiSuccessResponse({ sessions }));
331
+ }),
332
+ );
333
+
314
334
  router.delete(
315
335
  '/sessions/:sessionId',
316
336
  asyncHandler(async (req: Request, res: Response) => {
317
337
  const sessionId = parseSessionId(req.params.sessionId);
318
- const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? false;
319
- const result = await sessionsService.deleteSessionById(sessionId, deletedFromDisk);
338
+ const force = parseOptionalBooleanQuery(req.query.force, 'force') ?? false;
339
+ const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? force;
340
+ const result = await sessionsService.deleteOrArchiveSessionById(sessionId, {
341
+ force,
342
+ deletedFromDisk,
343
+ });
344
+ res.json(createApiSuccessResponse(result));
345
+ }),
346
+ );
347
+
348
+ router.post(
349
+ '/sessions/:sessionId/restore',
350
+ asyncHandler(async (req: Request, res: Response) => {
351
+ const sessionId = parseSessionId(req.params.sessionId);
352
+ const result = sessionsService.restoreSessionById(sessionId);
320
353
  res.json(createApiSuccessResponse(result));
321
354
  }),
322
355
  );
@@ -89,13 +89,8 @@ const RIPGREP_CHUNK_CONCURRENCY = 6;
89
89
  const UNKNOWN_PROJECT_KEY = '__unknown_project__';
90
90
 
91
91
  const INTERNAL_CONTENT_PREFIXES = [
92
- '<command-name>',
93
- '<command-message>',
94
- '<command-args>',
95
- '<local-command-stdout>',
96
92
  '<system-reminder>',
97
93
  'Caveat:',
98
- 'This session is being continued from a previous',
99
94
  'Invalid API key',
100
95
  '[Request interrupted',
101
96
  ] as const;
@@ -302,6 +297,135 @@ function extractClaudeText(content: unknown): string {
302
297
  .join(' ');
303
298
  }
304
299
 
300
+ function extractTaggedContent(content: string, tagName: string): string | null {
301
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
302
+ const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
303
+ return match ? match[1] : null;
304
+ }
305
+
306
+ type ClaudeLocalCommandPayload = {
307
+ commandName: string;
308
+ commandMessage: string;
309
+ commandArgs: string;
310
+ };
311
+
312
+ function parseClaudeLocalCommandPayload(content: string): ClaudeLocalCommandPayload | null {
313
+ const commandName = extractTaggedContent(content, 'command-name');
314
+ const commandMessage = extractTaggedContent(content, 'command-message');
315
+ const commandArgs = extractTaggedContent(content, 'command-args');
316
+
317
+ if (commandName === null && commandMessage === null && commandArgs === null) {
318
+ return null;
319
+ }
320
+
321
+ return {
322
+ commandName: commandName ?? '',
323
+ commandMessage: commandMessage ?? '',
324
+ commandArgs: commandArgs ?? '',
325
+ };
326
+ }
327
+
328
+ function buildClaudeLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): string {
329
+ const commandName = payload.commandName.trim();
330
+ const commandMessage = payload.commandMessage.trim();
331
+ const commandArgs = payload.commandArgs.trim();
332
+ const baseCommand = commandName || commandMessage;
333
+
334
+ if (!baseCommand) {
335
+ return '';
336
+ }
337
+
338
+ return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
339
+ }
340
+
341
+ function stripAnsiFormatting(text: string): string {
342
+ return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
343
+ }
344
+
345
+ type ClaudeSearchableMessage = {
346
+ text: string;
347
+ role: 'user' | 'assistant';
348
+ };
349
+
350
+ /**
351
+ * Claude mixes visible chat, compact summaries, and local command wrappers into
352
+ * the same transcript stream. Search should operate on the user-visible meaning
353
+ * of those rows rather than the raw wrapper syntax.
354
+ */
355
+ function extractClaudeSearchableMessage(entry: AnyRecord): ClaudeSearchableMessage | null {
356
+ if (!entry.message?.content || entry.isApiErrorMessage) {
357
+ return null;
358
+ }
359
+
360
+ const rawRole = entry.message.role;
361
+ if (rawRole !== 'user' && rawRole !== 'assistant') {
362
+ return null;
363
+ }
364
+
365
+ if (typeof entry.message.content === 'string') {
366
+ const content = String(entry.message.content);
367
+
368
+ if (entry.isCompactSummary === true && content.trim()) {
369
+ return {
370
+ text: content,
371
+ role: 'assistant',
372
+ };
373
+ }
374
+
375
+ const localCommand = parseClaudeLocalCommandPayload(content);
376
+ if (localCommand) {
377
+ const displayText = buildClaudeLocalCommandDisplayText(localCommand);
378
+ return displayText
379
+ ? {
380
+ text: displayText,
381
+ role: 'user',
382
+ }
383
+ : null;
384
+ }
385
+
386
+ const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
387
+ if (localCommandStdout !== null) {
388
+ const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
389
+ return stdoutText
390
+ ? {
391
+ text: stdoutText,
392
+ role: 'assistant',
393
+ }
394
+ : null;
395
+ }
396
+
397
+ if (!content || isInternalContent(content)) {
398
+ return null;
399
+ }
400
+
401
+ return {
402
+ text: content,
403
+ role: rawRole,
404
+ };
405
+ }
406
+
407
+ const text = extractClaudeText(entry.message.content);
408
+ if (!text) {
409
+ return null;
410
+ }
411
+
412
+ if (entry.isCompactSummary === true) {
413
+ return {
414
+ text,
415
+ role: 'assistant',
416
+ };
417
+ }
418
+
419
+ if (isInternalContent(text)) {
420
+ return null;
421
+ }
422
+
423
+ return {
424
+ text,
425
+ role: rawRole,
426
+ };
427
+ }
428
+
305
429
  function extractCodexText(content: unknown): string {
306
430
  if (typeof content === 'string') {
307
431
  return content;
@@ -348,6 +472,7 @@ function extractGeminiText(content: unknown): string {
348
472
 
349
473
  function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSessionRow[] {
350
474
  const normalizedRows: SearchableSessionRow[] = [];
475
+ const projectArchiveStateByPath = new Map<string, boolean>();
351
476
 
352
477
  for (const row of rows) {
353
478
  const provider = row.provider as SearchableProvider;
@@ -365,6 +490,27 @@ function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSe
365
490
  continue;
366
491
  }
367
492
 
493
+ /**
494
+ * Active session rows can still belong to an archived project because
495
+ * project archiving intentionally preserves the underlying session data.
496
+ * Global conversation search should follow the visible workspace model,
497
+ * which means excluding any session whose owning project is archived.
498
+ *
499
+ * Cache the archive lookup per normalized project path so one search pass
500
+ * does not re-query the same project row for every session in that folder.
501
+ */
502
+ const normalizedProjectPath = typeof row.project_path === 'string' ? row.project_path.trim() : '';
503
+ if (normalizedProjectPath) {
504
+ if (!projectArchiveStateByPath.has(normalizedProjectPath)) {
505
+ const projectRow = projectsDb.getProjectPath(normalizedProjectPath);
506
+ projectArchiveStateByPath.set(normalizedProjectPath, Boolean(projectRow?.isArchived));
507
+ }
508
+
509
+ if (projectArchiveStateByPath.get(normalizedProjectPath) === true) {
510
+ continue;
511
+ }
512
+ }
513
+
368
514
  normalizedRows.push({
369
515
  ...row,
370
516
  provider,
@@ -733,18 +879,21 @@ async function parseClaudeSessionMatches(
733
879
  }
734
880
  }
735
881
 
736
- if (!entry.message?.content || entry.isApiErrorMessage) {
882
+ const searchableMessage = extractClaudeSearchableMessage(entry);
883
+ if (!searchableMessage) {
737
884
  continue;
738
885
  }
739
886
 
740
- const role = entry.message.role;
741
- if (role !== 'user' && role !== 'assistant') {
742
- continue;
743
- }
887
+ const { text, role } = searchableMessage;
744
888
 
745
- const text = extractClaudeText(entry.message.content);
746
- if (!text || isInternalContent(text)) {
747
- continue;
889
+ /**
890
+ * Claude compact summaries are the most faithful session-summary source
891
+ * after a `/compact` because they describe the post-compaction state that
892
+ * the resumed session actually continues from. Prefer them over generic
893
+ * fallback user text when present.
894
+ */
895
+ if (entry.isCompactSummary === true) {
896
+ state.resolvedSummary = text;
748
897
  }
749
898
 
750
899
  if (role === 'user') {
@@ -18,16 +18,18 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
18
18
  },
19
19
  {
20
20
  provider: 'cursor',
21
- rootPath: path.join(os.homedir(), '.cursor', 'chats'),
21
+ rootPath: path.join(os.homedir(), '.cursor', 'projects'),
22
22
  },
23
23
  {
24
24
  provider: 'codex',
25
25
  rootPath: path.join(os.homedir(), '.codex', 'sessions'),
26
26
  },
27
- {
28
- provider: 'gemini',
29
- rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
30
- },
27
+ // {
28
+ // provider: 'gemini',
29
+ // rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
30
+ // },
31
+ // Keep `sessions/` watcher disabled: Gemini also mirrors artifacts there,
32
+ // which causes duplicate synchronization events.
31
33
  {
32
34
  provider: 'gemini',
33
35
  rootPath: path.join(os.homedir(), '.gemini', 'tmp'),