@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.
- package/dist/assets/index-AaSE7-3t.js +1336 -0
- package/dist/assets/index-CKaku72N.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +10 -5
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +0 -6
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +160 -34
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +11 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +1 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/projects/projects.routes.js +12 -3
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +43 -0
- 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-auth.provider.js +2 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
- 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 +153 -14
- 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 +198 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
- 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 +21 -6
- 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 +82 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +189 -13
- 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 +28 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +7 -1
- 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 +33 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +23 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -5
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
- 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 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +226 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
- package/dist-server/server/openai-codex.js +56 -26
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/routes/commands.js +5 -5
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +103 -0
- package/dist-server/server/shared/claude-cli-path.js.map +1 -0
- package/dist-server/server/shared/claude-cli-path.test.js +45 -0
- package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
- package/dist-server/server/shared/frontmatter.js.map +1 -0
- package/dist-server/server/shared/utils.js +82 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/commandParser.js +3 -3
- package/dist-server/server/utils/commandParser.js.map +1 -1
- package/dist-server/shared/modelConstants.js +1 -0
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +1 -1
- package/server/claude-sdk.js +10 -5
- package/server/cursor-cli.js +0 -8
- package/server/gemini-cli.js +190 -42
- package/server/modules/database/migrations.ts +12 -0
- package/server/modules/database/repositories/projects.db.ts +13 -0
- package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/sessions.db.ts +60 -9
- package/server/modules/database/schema.ts +1 -0
- package/server/modules/projects/projects.routes.ts +20 -3
- package/server/modules/projects/services/project-delete.service.ts +16 -1
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +64 -0
- package/server/modules/providers/README.md +346 -0
- package/server/modules/providers/index.ts +2 -1
- package/server/modules/providers/list/claude/claude-auth.provider.ts +8 -7
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
- package/server/modules/providers/list/claude/claude.provider.ts +8 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
- package/server/modules/providers/list/codex/codex-skills.provider.ts +100 -0
- package/server/modules/providers/list/codex/codex.provider.ts +8 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -13
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +8 -1
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +7 -1
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +8 -1
- package/server/modules/providers/provider.routes.ts +35 -2
- package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
- package/server/modules/providers/services/sessions-watcher.service.ts +7 -5
- package/server/modules/providers/services/sessions.service.ts +112 -9
- package/server/modules/providers/services/skills.service.ts +15 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
- package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
- package/server/modules/providers/tests/skills.test.ts +446 -0
- package/server/openai-codex.js +59 -27
- package/server/routes/commands.js +7 -5
- package/server/shared/claude-cli-path.test.ts +61 -0
- package/server/shared/claude-cli-path.ts +139 -0
- package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
- package/server/shared/interfaces.ts +19 -0
- package/server/shared/types.ts +78 -0
- package/server/shared/utils.ts +94 -0
- package/server/utils/commandParser.js +5 -3
- package/shared/modelConstants.js +1 -0
- package/dist/assets/index-6rYG_mRo.css +0 -32
- package/dist/assets/index-DqMVUeZS.js +0 -1331
- 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(
|
|
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(
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
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
|
|
319
|
-
const
|
|
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
|
-
|
|
882
|
+
const searchableMessage = extractClaudeSearchableMessage(entry);
|
|
883
|
+
if (!searchableMessage) {
|
|
737
884
|
continue;
|
|
738
885
|
}
|
|
739
886
|
|
|
740
|
-
const role =
|
|
741
|
-
if (role !== 'user' && role !== 'assistant') {
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
887
|
+
const { text, role } = searchableMessage;
|
|
744
888
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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', '
|
|
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
|
-
|
|
29
|
-
|
|
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'),
|