@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
|
@@ -200,17 +200,18 @@ async function getSessionMessages(
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* Claude writes
|
|
204
|
-
*
|
|
203
|
+
* Claude writes a mix of truly internal transcript rows and "UI-hidden" local
|
|
204
|
+
* command artifacts into the same JSONL stream.
|
|
205
|
+
*
|
|
206
|
+
* Important distinction:
|
|
207
|
+
* - system reminders / caveats / interruption banners should stay hidden
|
|
208
|
+
* - local command payloads (`<command-name>...`) and stdout wrappers
|
|
209
|
+
* (`<local-command-stdout>...`) should be remapped into normal chat messages
|
|
210
|
+
* instead of being discarded as internal content
|
|
205
211
|
*/
|
|
206
212
|
const INTERNAL_CONTENT_PREFIXES = [
|
|
207
|
-
'<command-name>',
|
|
208
|
-
'<command-message>',
|
|
209
|
-
'<command-args>',
|
|
210
|
-
'<local-command-stdout>',
|
|
211
213
|
'<system-reminder>',
|
|
212
214
|
'Caveat:',
|
|
213
|
-
'This session is being continued from a previous',
|
|
214
215
|
'[Request interrupted',
|
|
215
216
|
] as const;
|
|
216
217
|
|
|
@@ -218,6 +219,73 @@ function isInternalContent(content: string): boolean {
|
|
|
218
219
|
return INTERNAL_CONTENT_PREFIXES.some((prefix) => content.startsWith(prefix));
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Claude wraps local slash-command metadata in lightweight XML-like tags inside
|
|
224
|
+
* a plain string payload. We intentionally parse only the small tag surface we
|
|
225
|
+
* care about instead of introducing a generic XML parser for untrusted history.
|
|
226
|
+
*/
|
|
227
|
+
function extractTaggedContent(content: string, tagName: string): string | null {
|
|
228
|
+
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
229
|
+
const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
|
|
230
|
+
return match ? match[1] : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
type ClaudeLocalCommandPayload = {
|
|
234
|
+
commandName: string;
|
|
235
|
+
commandMessage: string;
|
|
236
|
+
commandArgs: string;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Converts Claude's hidden local command wrapper into structured metadata.
|
|
241
|
+
*
|
|
242
|
+
* The three tags often coexist in one string payload. Returning `null` lets the
|
|
243
|
+
* normal text path continue untouched for unrelated messages.
|
|
244
|
+
*/
|
|
245
|
+
function parseLocalCommandPayload(content: string): ClaudeLocalCommandPayload | null {
|
|
246
|
+
const commandName = extractTaggedContent(content, 'command-name');
|
|
247
|
+
const commandMessage = extractTaggedContent(content, 'command-message');
|
|
248
|
+
const commandArgs = extractTaggedContent(content, 'command-args');
|
|
249
|
+
|
|
250
|
+
if (commandName === null && commandMessage === null && commandArgs === null) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
commandName: commandName ?? '',
|
|
256
|
+
commandMessage: commandMessage ?? '',
|
|
257
|
+
commandArgs: commandArgs ?? '',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Produces the short user-visible command string that should appear in chat.
|
|
263
|
+
*
|
|
264
|
+
* We prefer the slash-prefixed command name because that most closely matches
|
|
265
|
+
* what the user actually typed, and only fall back to the message body when the
|
|
266
|
+
* command name is unavailable in older transcript variants.
|
|
267
|
+
*/
|
|
268
|
+
function buildLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): string {
|
|
269
|
+
const commandName = payload.commandName.trim();
|
|
270
|
+
const commandMessage = payload.commandMessage.trim();
|
|
271
|
+
const commandArgs = payload.commandArgs.trim();
|
|
272
|
+
const baseCommand = commandName || commandMessage;
|
|
273
|
+
|
|
274
|
+
if (!baseCommand) {
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Claude local-command stdout may contain ANSI styling codes because it was
|
|
283
|
+
* captured from the terminal. The web chat should receive readable plain text.
|
|
284
|
+
*/
|
|
285
|
+
function stripAnsiFormatting(text: string): string {
|
|
286
|
+
return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
|
|
287
|
+
}
|
|
288
|
+
|
|
221
289
|
export class ClaudeSessionsProvider implements IProviderSessions {
|
|
222
290
|
/**
|
|
223
291
|
* Normalizes one Claude JSONL entry or live SDK stream event into the shared
|
|
@@ -240,7 +308,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
240
308
|
const ts = raw.timestamp || new Date().toISOString();
|
|
241
309
|
const baseId = raw.uuid || generateMessageId('claude');
|
|
242
310
|
|
|
243
|
-
if (raw.message?.role === 'user' && raw.message?.content) {
|
|
311
|
+
if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
|
|
244
312
|
if (Array.isArray(raw.message.content)) {
|
|
245
313
|
for (let partIndex = 0; partIndex < raw.message.content.length; partIndex++) {
|
|
246
314
|
const part = raw.message.content[partIndex];
|
|
@@ -293,6 +361,80 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
293
361
|
}
|
|
294
362
|
} else if (typeof raw.message.content === 'string') {
|
|
295
363
|
const text = raw.message.content;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Claude stores compact summaries as synthetic "user" rows so the CLI
|
|
367
|
+
* can resume the next session turn with the summary in-context.
|
|
368
|
+
*
|
|
369
|
+
* For the web UI this is much more useful as assistant-authored summary
|
|
370
|
+
* text; otherwise it is both filtered by the generic internal-prefix
|
|
371
|
+
* check and visually mislabeled as a user message.
|
|
372
|
+
*/
|
|
373
|
+
if (raw.isCompactSummary === true && text.trim()) {
|
|
374
|
+
messages.push(createNormalizedMessage({
|
|
375
|
+
id: baseId,
|
|
376
|
+
sessionId,
|
|
377
|
+
timestamp: ts,
|
|
378
|
+
provider: PROVIDER,
|
|
379
|
+
kind: 'text',
|
|
380
|
+
role: 'assistant',
|
|
381
|
+
content: text,
|
|
382
|
+
isCompactSummary: true,
|
|
383
|
+
}));
|
|
384
|
+
return messages;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Local slash commands are serialized as tagged text even though they
|
|
389
|
+
* are semantically a user action. Expose the parsed fields to the
|
|
390
|
+
* frontend and emit a plain user-visible command string so the command
|
|
391
|
+
* no longer disappears from history.
|
|
392
|
+
*/
|
|
393
|
+
const localCommandPayload = parseLocalCommandPayload(text);
|
|
394
|
+
if (localCommandPayload) {
|
|
395
|
+
const displayText = buildLocalCommandDisplayText(localCommandPayload);
|
|
396
|
+
if (displayText) {
|
|
397
|
+
messages.push(createNormalizedMessage({
|
|
398
|
+
id: baseId,
|
|
399
|
+
sessionId,
|
|
400
|
+
timestamp: ts,
|
|
401
|
+
provider: PROVIDER,
|
|
402
|
+
kind: 'text',
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: displayText,
|
|
405
|
+
commandName: localCommandPayload.commandName,
|
|
406
|
+
commandMessage: localCommandPayload.commandMessage,
|
|
407
|
+
commandArgs: localCommandPayload.commandArgs,
|
|
408
|
+
isLocalCommand: true,
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
return messages;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Local command stdout is also written as a "user" row in Claude's
|
|
416
|
+
* transcript, but it is terminal output produced in response to the
|
|
417
|
+
* command. Re-label it as assistant text so the chat transcript matches
|
|
418
|
+
* the actual conversational flow seen by the user.
|
|
419
|
+
*/
|
|
420
|
+
const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
|
|
421
|
+
if (localCommandStdout !== null) {
|
|
422
|
+
const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
|
|
423
|
+
if (stdoutText) {
|
|
424
|
+
messages.push(createNormalizedMessage({
|
|
425
|
+
id: baseId,
|
|
426
|
+
sessionId,
|
|
427
|
+
timestamp: ts,
|
|
428
|
+
provider: PROVIDER,
|
|
429
|
+
kind: 'text',
|
|
430
|
+
role: 'assistant',
|
|
431
|
+
content: stdoutText,
|
|
432
|
+
isLocalCommandStdout: true,
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
return messages;
|
|
436
|
+
}
|
|
437
|
+
|
|
296
438
|
if (text && !isInternalContent(text)) {
|
|
297
439
|
messages.push(createNormalizedMessage({
|
|
298
440
|
id: baseId,
|
|
@@ -414,7 +556,9 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
414
556
|
|
|
415
557
|
let result: ClaudeHistoryResult;
|
|
416
558
|
try {
|
|
417
|
-
|
|
559
|
+
// Load full history first so `total` reflects frontend-normalized messages,
|
|
560
|
+
// not raw JSONL records.
|
|
561
|
+
result = await getSessionMessages(sessionId, null, 0);
|
|
418
562
|
} catch (error) {
|
|
419
563
|
const message = error instanceof Error ? error.message : String(error);
|
|
420
564
|
console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
|
|
@@ -422,8 +566,6 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
422
566
|
}
|
|
423
567
|
|
|
424
568
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
|
425
|
-
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
|
426
|
-
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
|
427
569
|
|
|
428
570
|
const toolResultMap = new Map<string, ClaudeToolResult>();
|
|
429
571
|
for (const raw of rawMessages) {
|
|
@@ -464,12 +606,31 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
|
|
464
606
|
}
|
|
465
607
|
}
|
|
466
608
|
|
|
609
|
+
const totalNormalized = normalized.length;
|
|
610
|
+
let total = 0;
|
|
611
|
+
for (const msg of normalized) {
|
|
612
|
+
if (msg.kind !== 'tool_result') {
|
|
613
|
+
total += 1;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const normalizedOffset = Math.max(0, offset);
|
|
617
|
+
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;
|
|
627
|
+
|
|
467
628
|
return {
|
|
468
|
-
messages
|
|
629
|
+
messages,
|
|
469
630
|
total,
|
|
470
631
|
hasMore,
|
|
471
|
-
offset,
|
|
472
|
-
limit,
|
|
632
|
+
offset: normalizedOffset,
|
|
633
|
+
limit: normalizedLimit,
|
|
473
634
|
};
|
|
474
635
|
}
|
|
475
636
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
|
|
6
|
+
import { parseFrontMatter } from '@/shared/frontmatter.js';
|
|
7
|
+
import type {
|
|
8
|
+
ProviderSkill,
|
|
9
|
+
ProviderSkillListOptions,
|
|
10
|
+
ProviderSkillSource,
|
|
11
|
+
} from '@/shared/types.js';
|
|
12
|
+
import {
|
|
13
|
+
findProviderSkillMarkdownFiles,
|
|
14
|
+
readJsonConfig,
|
|
15
|
+
readObjectRecord,
|
|
16
|
+
readOptionalString,
|
|
17
|
+
readProviderSkillMarkdownDefinition,
|
|
18
|
+
} from '@/shared/utils.js';
|
|
19
|
+
|
|
20
|
+
const getClaudeHomePath = (): string => path.join(os.homedir(), '.claude');
|
|
21
|
+
|
|
22
|
+
const getClaudePluginName = (pluginId: string): string | null => {
|
|
23
|
+
const normalizedPluginId = pluginId.trim();
|
|
24
|
+
if (!normalizedPluginId || normalizedPluginId === '@') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [pluginName] = normalizedPluginId.split('@');
|
|
29
|
+
return readOptionalString(pluginName) ?? null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const stripMarkdownExtension = (filename: string): string =>
|
|
33
|
+
filename.replace(/\.md$/i, '');
|
|
34
|
+
|
|
35
|
+
const pathExistsAsDirectory = async (directoryPath: string): Promise<boolean> => {
|
|
36
|
+
try {
|
|
37
|
+
const directoryStats = await stat(directoryPath);
|
|
38
|
+
return directoryStats.isDirectory();
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const listChildDirectories = async (directoryPath: string): Promise<string[]> => {
|
|
45
|
+
try {
|
|
46
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
47
|
+
return entries
|
|
48
|
+
.filter((entry) => entry.isDirectory())
|
|
49
|
+
.map((entry) => path.join(directoryPath, entry.name))
|
|
50
|
+
.sort((left, right) => left.localeCompare(right));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const readClaudePluginName = async (
|
|
57
|
+
installPath: string,
|
|
58
|
+
pluginId: string,
|
|
59
|
+
): Promise<string | null> => {
|
|
60
|
+
try {
|
|
61
|
+
const pluginConfig = await readJsonConfig(
|
|
62
|
+
path.join(installPath, '.claude-plugin', 'plugin.json'),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Older or partial plugin installs may not have plugin.json yet. Falling
|
|
66
|
+
// back keeps discovery useful without inventing a separate namespace.
|
|
67
|
+
return readOptionalString(pluginConfig.name) ?? getClaudePluginName(pluginId);
|
|
68
|
+
} catch {
|
|
69
|
+
return getClaudePluginName(pluginId);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export class ClaudeSkillsProvider extends SkillsProvider {
|
|
74
|
+
constructor() {
|
|
75
|
+
super('claude');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async listSkills(options?: ProviderSkillListOptions): Promise<ProviderSkill[]> {
|
|
79
|
+
return [
|
|
80
|
+
...(await super.listSkills(options)),
|
|
81
|
+
...(await this.listPluginSkills(getClaudeHomePath())),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected async getSkillSources(workspacePath: string): Promise<ProviderSkillSource[]> {
|
|
86
|
+
const claudeHomePath = getClaudeHomePath();
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
scope: 'user',
|
|
91
|
+
rootDir: path.join(claudeHomePath, 'skills'),
|
|
92
|
+
commandPrefix: '/',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
scope: 'project',
|
|
96
|
+
rootDir: path.join(workspacePath, '.claude', 'skills'),
|
|
97
|
+
commandPrefix: '/',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async listPluginSkills(claudeHomePath: string): Promise<ProviderSkill[]> {
|
|
103
|
+
const settings = await readJsonConfig(path.join(claudeHomePath, 'settings.json'));
|
|
104
|
+
const enabledPlugins = readObjectRecord(settings.enabledPlugins);
|
|
105
|
+
if (!enabledPlugins) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const installedConfig = await readJsonConfig(
|
|
110
|
+
path.join(claudeHomePath, 'plugins', 'installed_plugins.json'),
|
|
111
|
+
);
|
|
112
|
+
const installedPlugins = readObjectRecord(installedConfig.plugins);
|
|
113
|
+
if (!installedPlugins) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const skills: ProviderSkill[] = [];
|
|
118
|
+
const visitedPluginFolders = new Set<string>();
|
|
119
|
+
const pluginEntries = Object.entries(enabledPlugins)
|
|
120
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
121
|
+
for (const [pluginId, enabled] of pluginEntries) {
|
|
122
|
+
if (enabled !== true) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const installs = installedPlugins[pluginId];
|
|
127
|
+
if (!Array.isArray(installs)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const install of installs) {
|
|
132
|
+
const installRecord = readObjectRecord(install);
|
|
133
|
+
const installPath = readOptionalString(installRecord?.installPath);
|
|
134
|
+
if (!installPath) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Claude's installed path points at one version folder; the usable
|
|
139
|
+
// plugin payloads live in the direct child folders beside it.
|
|
140
|
+
const pluginFolders = await listChildDirectories(path.dirname(installPath));
|
|
141
|
+
for (const pluginFolder of pluginFolders) {
|
|
142
|
+
const pluginFolderKey = `${pluginId}:${path.resolve(pluginFolder)}`;
|
|
143
|
+
if (visitedPluginFolders.has(pluginFolderKey)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
visitedPluginFolders.add(pluginFolderKey);
|
|
147
|
+
|
|
148
|
+
const pluginName = await readClaudePluginName(pluginFolder, pluginId);
|
|
149
|
+
if (!pluginName) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const commandsPath = path.join(pluginFolder, 'commands');
|
|
154
|
+
if (await pathExistsAsDirectory(commandsPath)) {
|
|
155
|
+
skills.push(
|
|
156
|
+
...(await this.listPluginCommandSkills(commandsPath, pluginId, pluginName)),
|
|
157
|
+
);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const skillsPath = path.join(pluginFolder, 'skills');
|
|
162
|
+
if (!(await pathExistsAsDirectory(skillsPath))) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
skills.push(
|
|
167
|
+
...(await this.listPluginSkillMarkdowns(pluginFolder, pluginId, pluginName)),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return skills;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async listPluginCommandSkills(
|
|
177
|
+
commandsPath: string,
|
|
178
|
+
pluginId: string,
|
|
179
|
+
pluginName: string,
|
|
180
|
+
): Promise<ProviderSkill[]> {
|
|
181
|
+
const skills: ProviderSkill[] = [];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const entries = await readdir(commandsPath, { withFileTypes: true });
|
|
185
|
+
const commandFiles = entries
|
|
186
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
|
|
187
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
188
|
+
|
|
189
|
+
for (const commandFile of commandFiles) {
|
|
190
|
+
const sourcePath = path.join(commandsPath, commandFile.name);
|
|
191
|
+
try {
|
|
192
|
+
const definition = await this.readPluginCommandDefinition(sourcePath);
|
|
193
|
+
skills.push({
|
|
194
|
+
provider: this.provider,
|
|
195
|
+
name: definition.name,
|
|
196
|
+
description: definition.description,
|
|
197
|
+
command: `/${pluginName}:${definition.name}`,
|
|
198
|
+
scope: 'plugin',
|
|
199
|
+
sourcePath,
|
|
200
|
+
pluginName,
|
|
201
|
+
pluginId,
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
// Malformed command markdown should not block sibling plugin commands.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Missing or unreadable command folders are treated as empty plugin command sets.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return skills;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async readPluginCommandDefinition(
|
|
215
|
+
commandPath: string,
|
|
216
|
+
): Promise<{ name: string; description: string }> {
|
|
217
|
+
const content = await readFile(commandPath, 'utf8');
|
|
218
|
+
const parsed = parseFrontMatter(content);
|
|
219
|
+
const data = readObjectRecord(parsed.data) ?? {};
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
name: stripMarkdownExtension(path.basename(commandPath)),
|
|
223
|
+
description: readOptionalString(data.description) ?? '',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private async listPluginSkillMarkdowns(
|
|
228
|
+
installPath: string,
|
|
229
|
+
pluginId: string,
|
|
230
|
+
pluginName: string,
|
|
231
|
+
): Promise<ProviderSkill[]> {
|
|
232
|
+
const skillFiles = await findProviderSkillMarkdownFiles(path.join(installPath, 'skills'), {
|
|
233
|
+
recursive: true,
|
|
234
|
+
});
|
|
235
|
+
const skills: ProviderSkill[] = [];
|
|
236
|
+
|
|
237
|
+
for (const skillPath of skillFiles) {
|
|
238
|
+
try {
|
|
239
|
+
const definition = await readProviderSkillMarkdownDefinition(skillPath);
|
|
240
|
+
skills.push({
|
|
241
|
+
provider: this.provider,
|
|
242
|
+
name: definition.name,
|
|
243
|
+
description: definition.description,
|
|
244
|
+
command: `/${pluginName}:${definition.name}`,
|
|
245
|
+
scope: 'plugin',
|
|
246
|
+
sourcePath: skillPath,
|
|
247
|
+
pluginName,
|
|
248
|
+
pluginId,
|
|
249
|
+
});
|
|
250
|
+
} catch {
|
|
251
|
+
// A bad plugin skill file should not block other installed plugin skills.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return skills;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -3,11 +3,18 @@ import { ClaudeProviderAuth } from '@/modules/providers/list/claude/claude-auth.
|
|
|
3
3
|
import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
|
|
4
4
|
import { ClaudeSessionSynchronizer } from '@/modules/providers/list/claude/claude-session-synchronizer.provider.js';
|
|
5
5
|
import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
|
|
6
|
-
import
|
|
6
|
+
import { ClaudeSkillsProvider } from '@/modules/providers/list/claude/claude-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 ClaudeProvider extends AbstractProvider {
|
|
9
15
|
readonly mcp = new ClaudeMcpProvider();
|
|
10
16
|
readonly auth: IProviderAuth = new ClaudeProviderAuth();
|
|
17
|
+
readonly skills: IProviderSkills = new ClaudeSkillsProvider();
|
|
11
18
|
readonly sessions: IProviderSessions = new ClaudeSessionsProvider();
|
|
12
19
|
readonly sessionSynchronizer: IProviderSessionSynchronizer = new ClaudeSessionSynchronizer();
|
|
13
20
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
|
|
4
5
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
5
6
|
import {
|
|
@@ -99,7 +100,7 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
99
100
|
filePath: string,
|
|
100
101
|
nameMap: Map<string, string>
|
|
101
102
|
): Promise<ParsedSession | null> {
|
|
102
|
-
|
|
103
|
+
const parsed = await extractFirstValidJsonlData(filePath, (rawData) => {
|
|
103
104
|
const data = rawData as Record<string, unknown>;
|
|
104
105
|
const payload = data.payload as Record<string, unknown> | undefined;
|
|
105
106
|
const sessionId = typeof payload?.id === 'string' ? payload.id : undefined;
|
|
@@ -112,8 +113,67 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
|
112
113
|
return {
|
|
113
114
|
sessionId,
|
|
114
115
|
projectPath,
|
|
115
|
-
sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'),
|
|
116
116
|
};
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
if (!parsed) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const existingSession = sessionsDb.getSessionById(parsed.sessionId);
|
|
124
|
+
const existingSessionName = existingSession?.custom_name;
|
|
125
|
+
if (existingSessionName && existingSessionName !== 'Untitled Codex Session') {
|
|
126
|
+
return {
|
|
127
|
+
...parsed,
|
|
128
|
+
sessionName: normalizeSessionName(existingSessionName, 'Untitled Codex Session'),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let sessionName = nameMap.get(parsed.sessionId);
|
|
133
|
+
if (!sessionName) {
|
|
134
|
+
sessionName = await this.extractLastAgentMessageFromEnd(filePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
...parsed,
|
|
139
|
+
sessionName: normalizeSessionName(sessionName, 'Untitled Codex Session'),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async extractLastAgentMessageFromEnd(filePath: string): Promise<string | undefined> {
|
|
144
|
+
try {
|
|
145
|
+
const content = await readFile(filePath, 'utf8');
|
|
146
|
+
const lines = content.split(/\r?\n/);
|
|
147
|
+
|
|
148
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
149
|
+
const line = lines[index]?.trim();
|
|
150
|
+
if (!line) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let parsed: unknown;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(line);
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = parsed as Record<string, unknown>;
|
|
162
|
+
const eventType = typeof data.type === 'string' ? data.type : undefined;
|
|
163
|
+
const payload = data.payload as Record<string, unknown> | undefined;
|
|
164
|
+
const payloadType = typeof payload?.type === 'string' ? payload.type : undefined;
|
|
165
|
+
const lastAgentMessage = typeof payload?.last_agent_message === 'string'
|
|
166
|
+
? payload.last_agent_message
|
|
167
|
+
: undefined;
|
|
168
|
+
|
|
169
|
+
if (eventType === 'event_msg' && payloadType === 'task_complete' && lastAgentMessage?.trim()) {
|
|
170
|
+
return lastAgentMessage;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Ignore missing/unreadable files so sync can continue.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return undefined;
|
|
118
178
|
}
|
|
119
179
|
}
|
|
@@ -520,7 +520,9 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
520
520
|
|
|
521
521
|
let result: CodexHistoryResult;
|
|
522
522
|
try {
|
|
523
|
-
|
|
523
|
+
// Load full history first so `total` reflects frontend-normalized messages,
|
|
524
|
+
// not raw JSONL records.
|
|
525
|
+
result = await getCodexSessionMessages(sessionId, null, 0);
|
|
524
526
|
} catch (error) {
|
|
525
527
|
const message = error instanceof Error ? error.message : String(error);
|
|
526
528
|
console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message);
|
|
@@ -528,8 +530,6 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
528
530
|
}
|
|
529
531
|
|
|
530
532
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
|
531
|
-
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
|
532
|
-
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
|
533
533
|
const tokenUsage = Array.isArray(result) ? undefined : result.tokenUsage;
|
|
534
534
|
|
|
535
535
|
const normalized: NormalizedMessage[] = [];
|
|
@@ -552,12 +552,31 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
const totalNormalized = normalized.length;
|
|
556
|
+
let total = 0;
|
|
557
|
+
for (const msg of normalized) {
|
|
558
|
+
if (msg.kind !== 'tool_result') {
|
|
559
|
+
total += 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const normalizedOffset = Math.max(0, offset);
|
|
563
|
+
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;
|
|
573
|
+
|
|
555
574
|
return {
|
|
556
|
-
messages
|
|
575
|
+
messages,
|
|
557
576
|
total,
|
|
558
577
|
hasMore,
|
|
559
|
-
offset,
|
|
560
|
-
limit,
|
|
578
|
+
offset: normalizedOffset,
|
|
579
|
+
limit: normalizedLimit,
|
|
561
580
|
tokenUsage,
|
|
562
581
|
};
|
|
563
582
|
}
|