@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
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ resolveClaudeCodeExecutablePath,
6
+ type ResolveClaudeCodeExecutablePathDependencies,
7
+ } from '@/shared/claude-cli-path.js';
8
+
9
+ test('resolveClaudeCodeExecutablePath resolves the npm Claude wrapper to its native exe on Windows', () => {
10
+ const wrapperDir = 'C:\\nvm4w\\nodejs';
11
+ const nativePath = `${wrapperDir}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`;
12
+ const execFileSync =
13
+ (() => `${wrapperDir}\\claude\r\n${wrapperDir}\\claude.cmd\r\n`) as unknown as ResolveClaudeCodeExecutablePathDependencies['execFileSync'];
14
+ const readFileSync = (() => '') as unknown as ResolveClaudeCodeExecutablePathDependencies['readFileSync'];
15
+
16
+ const resolved = resolveClaudeCodeExecutablePath('claude', {
17
+ platform: 'win32',
18
+ execFileSync,
19
+ existsSync: (candidate) => candidate === nativePath,
20
+ readFileSync,
21
+ });
22
+
23
+ assert.equal(resolved, nativePath);
24
+ });
25
+
26
+ test('resolveClaudeCodeExecutablePath keeps an explicit JavaScript launcher path unchanged', () => {
27
+ const scriptPath = 'C:\\tools\\claude.js';
28
+
29
+ const resolved = resolveClaudeCodeExecutablePath(scriptPath, {
30
+ platform: 'win32',
31
+ });
32
+
33
+ assert.equal(resolved, scriptPath);
34
+ });
35
+
36
+ test('resolveClaudeCodeExecutablePath can parse a wrapper file path containing letters r and n before claude.exe', () => {
37
+ const wrapperPath = 'C:\\tools\\claude';
38
+ const nativePath = 'C:\\tools\\custom\\bin\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe';
39
+ const readFileSync = (() => `exec "$basedir/custom/bin/node_modules/@anthropic-ai/claude-code/bin/claude.exe" "$@"`) as unknown as ResolveClaudeCodeExecutablePathDependencies['readFileSync'];
40
+
41
+ const resolved = resolveClaudeCodeExecutablePath(wrapperPath, {
42
+ platform: 'win32',
43
+ existsSync: (candidate) => candidate === nativePath,
44
+ readFileSync,
45
+ });
46
+
47
+ assert.equal(resolved, nativePath);
48
+ });
49
+
50
+ test('resolveClaudeCodeExecutablePath falls back to the configured command when PATH lookup fails', () => {
51
+ const execFileSync = (() => {
52
+ throw new Error('not found');
53
+ }) as unknown as ResolveClaudeCodeExecutablePathDependencies['execFileSync'];
54
+
55
+ const resolved = resolveClaudeCodeExecutablePath('claude', {
56
+ platform: 'win32',
57
+ execFileSync,
58
+ });
59
+
60
+ assert.equal(resolved, 'claude');
61
+ });
@@ -0,0 +1,139 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_CLAUDE_COMMAND = 'claude';
6
+ const CLAUDE_SCRIPT_EXTENSIONS = new Set(['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx']);
7
+ const CLAUDE_WRAPPER_SEGMENTS = ['node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'] as const;
8
+
9
+ export type ResolveClaudeCodeExecutablePathDependencies = {
10
+ execFileSync?: typeof execFileSync;
11
+ existsSync?: typeof fs.existsSync;
12
+ platform?: NodeJS.Platform;
13
+ readFileSync?: typeof fs.readFileSync;
14
+ };
15
+
16
+ function getPathApi(platform: NodeJS.Platform) {
17
+ return platform === 'win32' ? path.win32 : path;
18
+ }
19
+
20
+ function stripWrappingQuotes(value: string): string {
21
+ const trimmed = value.trim();
22
+ if (
23
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
24
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
25
+ ) {
26
+ return trimmed.slice(1, -1);
27
+ }
28
+ return trimmed;
29
+ }
30
+
31
+ function isPathLike(value: string): boolean {
32
+ return value.includes('/') || value.includes('\\');
33
+ }
34
+
35
+ function resolveClaudeWrapperBinary(
36
+ wrapperPath: string,
37
+ deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
38
+ ): string | null {
39
+ const pathApi = getPathApi(deps.platform);
40
+ const directCandidate = pathApi.resolve(pathApi.dirname(wrapperPath), ...CLAUDE_WRAPPER_SEGMENTS);
41
+
42
+ if (deps.existsSync(directCandidate)) {
43
+ return directCandidate;
44
+ }
45
+
46
+ let content: string;
47
+ try {
48
+ content = deps.readFileSync(wrapperPath, 'utf8');
49
+ } catch {
50
+ return null;
51
+ }
52
+
53
+ const matches = content.matchAll(/["']([^"'\\\r\n]*claude\.exe)["']/gi);
54
+ for (const match of matches) {
55
+ const rawTarget = match[1]
56
+ .replace(/^\$basedir[\\/]/i, '')
57
+ .replace(/^%dp0%[\\/]/i, '')
58
+ .replace(/^%~dp0[\\/]/i, '');
59
+ const normalizedTarget = rawTarget.replace(/[\\/]/g, pathApi.sep);
60
+ const candidate = pathApi.isAbsolute(normalizedTarget)
61
+ ? normalizedTarget
62
+ : pathApi.resolve(pathApi.dirname(wrapperPath), normalizedTarget);
63
+
64
+ if (deps.existsSync(candidate)) {
65
+ return candidate;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function resolveWindowsClaudeExecutablePath(
73
+ configuredPath: string,
74
+ deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
75
+ ): string {
76
+ const pathApi = getPathApi(deps.platform);
77
+ const extension = pathApi.extname(configuredPath).toLowerCase();
78
+ const explicitPath = isPathLike(configuredPath) || pathApi.isAbsolute(configuredPath);
79
+
80
+ if (CLAUDE_SCRIPT_EXTENSIONS.has(extension)) {
81
+ return configuredPath;
82
+ }
83
+
84
+ if (explicitPath && extension === '.exe') {
85
+ return configuredPath;
86
+ }
87
+
88
+ if (explicitPath) {
89
+ return resolveClaudeWrapperBinary(configuredPath, deps) ?? configuredPath;
90
+ }
91
+
92
+ try {
93
+ const stdout = deps.execFileSync('where.exe', [configuredPath], {
94
+ encoding: 'utf8',
95
+ stdio: ['ignore', 'pipe', 'ignore'],
96
+ windowsHide: true,
97
+ });
98
+ const candidates = stdout
99
+ .split(/\r?\n/)
100
+ .map((entry) => entry.trim())
101
+ .filter(Boolean);
102
+
103
+ for (const candidate of candidates) {
104
+ if (pathApi.extname(candidate).toLowerCase() === '.exe') {
105
+ return candidate;
106
+ }
107
+ }
108
+
109
+ for (const candidate of candidates) {
110
+ const resolved = resolveClaudeWrapperBinary(candidate, deps);
111
+ if (resolved) {
112
+ return resolved;
113
+ }
114
+ }
115
+ } catch {
116
+ return configuredPath;
117
+ }
118
+
119
+ return configuredPath;
120
+ }
121
+
122
+ export function resolveClaudeCodeExecutablePath(
123
+ configuredPath: string | undefined = process.env.CLAUDE_CLI_PATH,
124
+ dependencies: ResolveClaudeCodeExecutablePathDependencies = {},
125
+ ): string {
126
+ const deps: Required<ResolveClaudeCodeExecutablePathDependencies> = {
127
+ execFileSync: dependencies.execFileSync ?? execFileSync,
128
+ existsSync: dependencies.existsSync ?? fs.existsSync,
129
+ platform: dependencies.platform ?? process.platform,
130
+ readFileSync: dependencies.readFileSync ?? fs.readFileSync,
131
+ };
132
+
133
+ const normalizedPath = stripWrappingQuotes(configuredPath || DEFAULT_CLAUDE_COMMAND);
134
+ if (deps.platform !== 'win32') {
135
+ return normalizedPath;
136
+ }
137
+
138
+ return resolveWindowsClaudeExecutablePath(normalizedPath, deps);
139
+ }
@@ -9,10 +9,10 @@ const frontmatterOptions = {
9
9
  engines: {
10
10
  js: disabledFrontmatterEngine,
11
11
  javascript: disabledFrontmatterEngine,
12
- json: disabledFrontmatterEngine
13
- }
12
+ json: disabledFrontmatterEngine,
13
+ },
14
14
  };
15
15
 
16
- export function parseFrontmatter(content) {
16
+ export function parseFrontMatter(content: string) {
17
17
  return matter(content, frontmatterOptions);
18
18
  }
@@ -4,6 +4,8 @@ import type {
4
4
  LLMProvider,
5
5
  McpScope,
6
6
  NormalizedMessage,
7
+ ProviderSkill,
8
+ ProviderSkillListOptions,
7
9
  ProviderAuthStatus,
8
10
  ProviderMcpServer,
9
11
  UpsertProviderMcpServerInput,
@@ -20,6 +22,7 @@ export interface IProvider {
20
22
  readonly id: LLMProvider;
21
23
  readonly mcp: IProviderMcp;
22
24
  readonly auth: IProviderAuth;
25
+ readonly skills: IProviderSkills;
23
26
  readonly sessions: IProviderSessions;
24
27
  readonly sessionSynchronizer: IProviderSessionSynchronizer;
25
28
  }
@@ -39,6 +42,22 @@ export interface IProviderAuth {
39
42
  getStatus(): Promise<ProviderAuthStatus>;
40
43
  }
41
44
 
45
+ // ---------------------------
46
+ //----------------- PROVIDER SKILLS INTERFACE ------------
47
+ /**
48
+ * Skills contract for one provider.
49
+ *
50
+ * Implementations discover provider-native skill markdown locations and return
51
+ * normalized skill records with the exact command syntax expected by that
52
+ * provider. Each skill is read from a `SKILL.md` file under its skill directory.
53
+ */
54
+ export interface IProviderSkills {
55
+ /**
56
+ * Lists all skills visible to this provider for the optional workspace.
57
+ */
58
+ listSkills(options?: ProviderSkillListOptions): Promise<ProviderSkill[]>;
59
+ }
60
+
42
61
  // ---------------------------
43
62
  //----------------- PROVIDER MCP INTERFACE ------------
44
63
  /**
@@ -102,6 +102,21 @@ export type NormalizedMessage = {
102
102
  kind: MessageKind;
103
103
  role?: 'user' | 'assistant';
104
104
  content?: string;
105
+ /**
106
+ * Optional display-oriented metadata used by providers that need to expose
107
+ * richer transcript artifacts without introducing a brand-new message kind.
108
+ *
109
+ * Current Claude usage:
110
+ * - local slash commands expose parsed command fields
111
+ * - compact summaries are flagged so the UI can treat them differently later
112
+ */
113
+ displayText?: string;
114
+ commandName?: string;
115
+ commandMessage?: string;
116
+ commandArgs?: string;
117
+ isLocalCommand?: boolean;
118
+ isLocalCommandStdout?: boolean;
119
+ isCompactSummary?: boolean;
105
120
  images?: unknown;
106
121
  toolName?: string;
107
122
  toolInput?: unknown;
@@ -156,6 +171,69 @@ export type FetchHistoryResult = {
156
171
  tokenUsage?: unknown;
157
172
  };
158
173
 
174
+ // ---------------------------
175
+ //----------------- PROVIDER SKILL TYPES ------------
176
+ /**
177
+ * Scope where a provider skill definition was discovered.
178
+ *
179
+ * Provider skill adapters should use this to describe the origin of each
180
+ * skill markdown file without leaking provider-specific folder names into route
181
+ * contracts. `repo` is used for Codex repository lookup locations, while
182
+ * `project` is used for providers that treat workspace-local skills as project
183
+ * scoped.
184
+ */
185
+ export type ProviderSkillScope = 'user' | 'project' | 'plugin' | 'repo' | 'admin' | 'system';
186
+
187
+ /**
188
+ * Shared input accepted by provider skill listing operations.
189
+ *
190
+ * Routes pass `workspacePath` when a caller wants project/repository skills for
191
+ * a specific folder. Providers should fall back to the backend process cwd when
192
+ * this option is omitted.
193
+ */
194
+ export type ProviderSkillListOptions = {
195
+ workspacePath?: string;
196
+ };
197
+
198
+ /**
199
+ * Normalized skill record returned by provider skill adapters.
200
+ *
201
+ * The `command` value is the exact invocation text the selected provider expects
202
+ * for this skill. Claude plugin skills use a namespaced command such as
203
+ * `/plugin-name:skill-name`, while Codex skills use the `$skill-name` form.
204
+ * `sourcePath` points to the skill markdown file that produced the record so
205
+ * callers can distinguish duplicate skill names across scopes.
206
+ */
207
+ export type ProviderSkill = {
208
+ provider: LLMProvider;
209
+ name: string;
210
+ description: string;
211
+ command: string;
212
+ scope: ProviderSkillScope;
213
+ sourcePath: string;
214
+ pluginName?: string;
215
+ pluginId?: string;
216
+ };
217
+
218
+ /**
219
+ * Internal source descriptor consumed by shared provider skill discovery logic.
220
+ *
221
+ * Concrete provider adapters build these records from their native lookup rules.
222
+ * The shared skills provider then scans `rootDir` for child skill markdown files
223
+ * and uses `commandForSkill` or `commandPrefix` to produce the provider-specific
224
+ * invocation command. Set `recursive` only when a provider stores skills under
225
+ * arbitrary nested folders below the source root.
226
+ */
227
+ export type ProviderSkillSource = {
228
+ scope: ProviderSkillScope;
229
+ rootDir: string;
230
+ recursive?: boolean;
231
+ commandPrefix?: '/' | '$';
232
+ commandForSkill?: (skillName: string) => string;
233
+ pluginName?: string;
234
+ pluginId?: string;
235
+ };
236
+
159
237
  // ---------------------------
160
238
  //----------------- SHARED ERROR TYPES ------------
161
239
  /**
@@ -17,6 +17,7 @@ import readline from 'node:readline';
17
17
 
18
18
  import type { NextFunction, Request, RequestHandler, Response } from 'express';
19
19
 
20
+ import { parseFrontMatter } from '@/shared/frontmatter.js';
20
21
  import type {
21
22
  AnyRecord,
22
23
  ApiSuccessShape,
@@ -503,6 +504,99 @@ export const writeJsonConfig = async (filePath: string, data: Record<string, unk
503
504
  await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
504
505
  };
505
506
 
507
+ // ---------------------------
508
+ //----------------- PROVIDER SKILL FILE UTILITIES ------------
509
+ /**
510
+ * Finds direct child skill markdown files under a provider skill root.
511
+ *
512
+ * Skill systems usually store one skill per child directory, so direct mode
513
+ * scans only `<root>/<skill-name>/SKILL.md`. Recursive mode is reserved for
514
+ * provider sources that can nest skills arbitrarily, and it returns every
515
+ * descendant `SKILL.md`. Missing or unreadable roots return an empty list
516
+ * because users may not have every provider installed or configured.
517
+ */
518
+ export async function findProviderSkillMarkdownFiles(
519
+ rootDir: string,
520
+ options: { recursive?: boolean } = {},
521
+ ): Promise<string[]> {
522
+ const skillFiles: string[] = [];
523
+
524
+ const collectRecursive = async (dirPath: string): Promise<void> => {
525
+ let entries;
526
+ try {
527
+ entries = await readdir(dirPath, { withFileTypes: true });
528
+ } catch {
529
+ return;
530
+ }
531
+
532
+ try {
533
+ const skillPath = path.join(dirPath, 'SKILL.md');
534
+ const skillStats = await stat(skillPath);
535
+ if (skillStats.isFile()) {
536
+ skillFiles.push(skillPath);
537
+ }
538
+ } catch {
539
+ // Directories without SKILL.md are expected while walking plugin trees.
540
+ }
541
+
542
+ for (const entry of entries) {
543
+ if (entry.isDirectory()) {
544
+ await collectRecursive(path.join(dirPath, entry.name));
545
+ }
546
+ }
547
+ };
548
+
549
+ if (options.recursive) {
550
+ await collectRecursive(rootDir);
551
+ return skillFiles.sort((left, right) => left.localeCompare(right));
552
+ }
553
+
554
+ try {
555
+ const entries = await readdir(rootDir, { withFileTypes: true });
556
+
557
+ for (const entry of entries) {
558
+ if (!entry.isDirectory()) {
559
+ continue;
560
+ }
561
+
562
+ const skillPath = path.join(rootDir, entry.name, 'SKILL.md');
563
+ try {
564
+ const skillStats = await stat(skillPath);
565
+ if (skillStats.isFile()) {
566
+ skillFiles.push(skillPath);
567
+ }
568
+ } catch {
569
+ // A partial skill directory should not block discovery of sibling skills.
570
+ }
571
+ }
572
+
573
+ return skillFiles.sort((left, right) => left.localeCompare(right));
574
+ } catch {
575
+ return [];
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Reads the `name` and `description` fields from a provider skill markdown file.
581
+ *
582
+ * The metadata is expected in markdown front matter. If a skill omits `name`, the
583
+ * parent directory name is used as a stable fallback so providers can still
584
+ * expose the skill. Missing descriptions are normalized to an empty string.
585
+ */
586
+ export async function readProviderSkillMarkdownDefinition(
587
+ skillPath: string,
588
+ ): Promise<{ name: string; description: string }> {
589
+ const content = await readFile(skillPath, 'utf8');
590
+ const parsed = parseFrontMatter(content);
591
+ const data = readObjectRecord(parsed.data) ?? {};
592
+ const fallbackName = path.basename(path.dirname(skillPath));
593
+
594
+ return {
595
+ name: readOptionalString(data.name) ?? fallbackName,
596
+ description: readOptionalString(data.description) ?? '',
597
+ };
598
+ }
599
+
506
600
  // ---------------------------
507
601
  //----------------- SESSION SYNCHRONIZER TITLE HELPERS ------------
508
602
  /**
@@ -1,9 +1,11 @@
1
+ import { execFile } from 'child_process';
1
2
  import { promises as fs } from 'fs';
2
3
  import path from 'path';
3
- import { execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
+
5
6
  import { parse as parseShellCommand } from 'shell-quote';
6
- import { parseFrontmatter } from './frontmatter.js';
7
+
8
+ import { parseFrontMatter } from '../shared/frontmatter.js';
7
9
 
8
10
  const execFileAsync = promisify(execFile);
9
11
 
@@ -32,7 +34,7 @@ const BASH_COMMAND_ALLOWLIST = [
32
34
  */
33
35
  export function parseCommand(content) {
34
36
  try {
35
- const parsed = parseFrontmatter(content);
37
+ const parsed = parseFrontMatter(content);
36
38
  return {
37
39
  data: parsed.data || {},
38
40
  content: parsed.content || '',
@@ -84,6 +84,7 @@ export const GEMINI_MODELS = {
84
84
  { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
85
85
  { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
86
86
  { value: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
87
+ { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
87
88
  { value: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
88
89
  { value: "gemini-2.0-pro-exp", label: "Gemini 2.0 Pro Experimental" },
89
90
  {