@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,446 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
8
+
9
+ const patchHomeDir = (nextHomeDir: string) => {
10
+ const original = os.homedir;
11
+ (os as any).homedir = () => nextHomeDir;
12
+ return () => {
13
+ (os as any).homedir = original;
14
+ };
15
+ };
16
+
17
+ const writeSkill = async (
18
+ skillsRoot: string,
19
+ directoryName: string,
20
+ name: string,
21
+ description: string,
22
+ ): Promise<string> => {
23
+ const skillDir = path.join(skillsRoot, directoryName);
24
+ await fs.mkdir(skillDir, { recursive: true });
25
+ const skillPath = path.join(skillDir, 'SKILL.md');
26
+ await fs.writeFile(
27
+ skillPath,
28
+ `---\nname: ${name}\ndescription: ${description}\n---\n\n`,
29
+ 'utf8',
30
+ );
31
+ return skillPath;
32
+ };
33
+
34
+ const writeClaudePluginManifest = async (
35
+ installPath: string,
36
+ name: string,
37
+ ): Promise<void> => {
38
+ const pluginConfigDir = path.join(installPath, '.claude-plugin');
39
+ await fs.mkdir(pluginConfigDir, { recursive: true });
40
+ await fs.writeFile(
41
+ path.join(pluginConfigDir, 'plugin.json'),
42
+ JSON.stringify(
43
+ {
44
+ name,
45
+ version: '0.1.0',
46
+ description: `${name} test plugin`,
47
+ },
48
+ null,
49
+ 2,
50
+ ),
51
+ 'utf8',
52
+ );
53
+ };
54
+
55
+ const writeClaudePluginCommand = async (
56
+ commandsRoot: string,
57
+ commandName: string,
58
+ description: string,
59
+ ): Promise<string> => {
60
+ await fs.mkdir(commandsRoot, { recursive: true });
61
+ const commandPath = path.join(commandsRoot, `${commandName}.md`);
62
+ await fs.writeFile(
63
+ commandPath,
64
+ `---\ndescription: ${description}\nargument-hint: 'test args'\n---\n\nCommand body.\n`,
65
+ 'utf8',
66
+ );
67
+ return commandPath;
68
+ };
69
+
70
+ /**
71
+ * This test covers Claude user/project skill folders plus plugin discovery from
72
+ * installed plugin command files and fallback plugin skill files.
73
+ */
74
+ test('providerSkillsService lists claude user, project, and enabled plugin skills', { concurrency: false }, async () => {
75
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
76
+ const workspacePath = path.join(tempRoot, 'workspace');
77
+ const commandPluginInstallPath = path.join(
78
+ tempRoot,
79
+ '.claude',
80
+ 'plugins',
81
+ 'cache',
82
+ 'notion-plugin',
83
+ 'notion',
84
+ 'abc123',
85
+ );
86
+ const skillPluginInstallPath = path.join(
87
+ tempRoot,
88
+ '.claude',
89
+ 'plugins',
90
+ 'cache',
91
+ 'anthropic-agent-skills',
92
+ 'example-skills',
93
+ 'def456',
94
+ );
95
+ const disabledPluginInstallPath = path.join(
96
+ tempRoot,
97
+ '.claude',
98
+ 'plugins',
99
+ 'cache',
100
+ 'disabled-marketplace',
101
+ 'disabled-skills',
102
+ 'ghi789',
103
+ );
104
+ const emptyIdPluginInstallPath = path.join(
105
+ tempRoot,
106
+ '.claude',
107
+ 'plugins',
108
+ 'cache',
109
+ 'invalid-empty-plugin',
110
+ 'empty',
111
+ '000',
112
+ );
113
+ const atIdPluginInstallPath = path.join(
114
+ tempRoot,
115
+ '.claude',
116
+ 'plugins',
117
+ 'cache',
118
+ 'invalid-at-plugin',
119
+ 'at',
120
+ '000',
121
+ );
122
+ const siblingSkillPluginPath = path.join(path.dirname(skillPluginInstallPath), 'legacy777');
123
+ await fs.mkdir(workspacePath, { recursive: true });
124
+
125
+ const restoreHomeDir = patchHomeDir(tempRoot);
126
+ try {
127
+ await writeSkill(
128
+ path.join(tempRoot, '.claude', 'skills'),
129
+ 'claude-user-dir',
130
+ 'claude-user',
131
+ 'Claude user skill',
132
+ );
133
+ await writeSkill(
134
+ path.join(workspacePath, '.claude', 'skills'),
135
+ 'claude-project-dir',
136
+ 'claude-project',
137
+ 'Claude project skill',
138
+ );
139
+ await writeClaudePluginManifest(commandPluginInstallPath, 'Notion');
140
+ await writeClaudePluginCommand(
141
+ path.join(commandPluginInstallPath, 'commands'),
142
+ 'insert-row',
143
+ 'Insert a Notion database row',
144
+ );
145
+ await writeSkill(
146
+ path.join(commandPluginInstallPath, 'skills'),
147
+ 'ignored-command-plugin-skill-dir',
148
+ 'ignored-command-plugin-skill',
149
+ 'Command plugin fallback skill should be ignored',
150
+ );
151
+ await writeClaudePluginManifest(skillPluginInstallPath, 'ExampleSkills');
152
+ await writeSkill(
153
+ path.join(skillPluginInstallPath, 'skills'),
154
+ 'claude-plugin-dir',
155
+ 'claude-plugin',
156
+ 'Claude plugin skill',
157
+ );
158
+ await writeSkill(
159
+ path.join(skillPluginInstallPath, 'skills'),
160
+ 'claude-plugin-second-dir',
161
+ 'claude-plugin-second',
162
+ 'Second Claude plugin skill',
163
+ );
164
+ await writeSkill(
165
+ path.join(skillPluginInstallPath, 'skills', 'nested', 'collection'),
166
+ 'claude-plugin-nested-dir',
167
+ 'claude-plugin-nested',
168
+ 'Nested Claude plugin skill',
169
+ );
170
+ await writeSkill(
171
+ path.join(siblingSkillPluginPath, 'skills'),
172
+ 'claude-plugin-sibling-dir',
173
+ 'claude-plugin-sibling',
174
+ 'Sibling Claude plugin skill',
175
+ );
176
+ await writeClaudePluginManifest(disabledPluginInstallPath, 'DisabledSkills');
177
+ await writeClaudePluginCommand(
178
+ path.join(disabledPluginInstallPath, 'commands'),
179
+ 'disabled-command',
180
+ 'Disabled plugin command',
181
+ );
182
+ await writeClaudePluginCommand(
183
+ path.join(emptyIdPluginInstallPath, 'commands'),
184
+ 'invalid-empty-command',
185
+ 'Invalid empty id command',
186
+ );
187
+ await writeClaudePluginCommand(
188
+ path.join(atIdPluginInstallPath, 'commands'),
189
+ 'invalid-at-command',
190
+ 'Invalid at id command',
191
+ );
192
+ await writeSkill(
193
+ path.join(
194
+ disabledPluginInstallPath,
195
+ 'skills',
196
+ ),
197
+ 'disabled-plugin-dir',
198
+ 'disabled-plugin',
199
+ 'Disabled plugin skill',
200
+ );
201
+
202
+ await fs.writeFile(
203
+ path.join(tempRoot, '.claude', 'settings.json'),
204
+ JSON.stringify(
205
+ {
206
+ enabledPlugins: {
207
+ '': true,
208
+ '@': true,
209
+ 'notion@notion-marketplace': true,
210
+ 'example-skills@anthropic-agent-skills': true,
211
+ 'disabled-skills@disabled-marketplace': false,
212
+ },
213
+ },
214
+ null,
215
+ 2,
216
+ ),
217
+ 'utf8',
218
+ );
219
+ await fs.writeFile(
220
+ path.join(tempRoot, '.claude', 'plugins', 'installed_plugins.json'),
221
+ JSON.stringify(
222
+ {
223
+ version: 2,
224
+ plugins: {
225
+ '': [
226
+ {
227
+ scope: 'user',
228
+ installPath: emptyIdPluginInstallPath,
229
+ version: '000',
230
+ },
231
+ ],
232
+ '@': [
233
+ {
234
+ scope: 'user',
235
+ installPath: atIdPluginInstallPath,
236
+ version: '000',
237
+ },
238
+ ],
239
+ 'notion@notion-marketplace': [
240
+ {
241
+ scope: 'user',
242
+ installPath: commandPluginInstallPath,
243
+ version: 'abc123',
244
+ },
245
+ ],
246
+ 'example-skills@anthropic-agent-skills': [
247
+ {
248
+ scope: 'user',
249
+ installPath: skillPluginInstallPath,
250
+ version: 'def456',
251
+ },
252
+ ],
253
+ 'disabled-skills@disabled-marketplace': [
254
+ {
255
+ scope: 'user',
256
+ installPath: disabledPluginInstallPath,
257
+ version: 'ghi789',
258
+ },
259
+ ],
260
+ },
261
+ },
262
+ null,
263
+ 2,
264
+ ),
265
+ 'utf8',
266
+ );
267
+
268
+ const skills = await providerSkillsService.listProviderSkills('claude', { workspacePath });
269
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
270
+
271
+ assert.equal(byName.get('claude-user')?.scope, 'user');
272
+ assert.equal(byName.get('claude-user')?.command, '/claude-user');
273
+ assert.equal(byName.get('claude-project')?.scope, 'project');
274
+ assert.equal(byName.get('claude-project')?.command, '/claude-project');
275
+
276
+ const pluginCommand = byName.get('insert-row');
277
+ assert.equal(pluginCommand?.scope, 'plugin');
278
+ assert.equal(pluginCommand?.pluginName, 'Notion');
279
+ assert.equal(pluginCommand?.pluginId, 'notion@notion-marketplace');
280
+ assert.equal(pluginCommand?.command, '/Notion:insert-row');
281
+ assert.equal(pluginCommand?.description, 'Insert a Notion database row');
282
+ assert.match(pluginCommand?.sourcePath ?? '', /commands[\\/]insert-row\.md$/);
283
+ assert.equal(byName.has('ignored-command-plugin-skill'), false);
284
+
285
+ const pluginSkill = byName.get('claude-plugin');
286
+ assert.equal(pluginSkill?.scope, 'plugin');
287
+ assert.equal(pluginSkill?.pluginName, 'ExampleSkills');
288
+ assert.equal(pluginSkill?.pluginId, 'example-skills@anthropic-agent-skills');
289
+ assert.equal(pluginSkill?.command, '/ExampleSkills:claude-plugin');
290
+ assert.equal(pluginSkill?.description, 'Claude plugin skill');
291
+ assert.match(
292
+ pluginSkill?.sourcePath ?? '',
293
+ /cache[\\/]anthropic-agent-skills[\\/]example-skills[\\/]def456[\\/]skills[\\/]/,
294
+ );
295
+
296
+ const secondPluginSkill = byName.get('claude-plugin-second');
297
+ assert.equal(secondPluginSkill?.scope, 'plugin');
298
+ assert.equal(secondPluginSkill?.command, '/ExampleSkills:claude-plugin-second');
299
+
300
+ const nestedPluginSkill = byName.get('claude-plugin-nested');
301
+ assert.equal(nestedPluginSkill?.scope, 'plugin');
302
+ assert.equal(nestedPluginSkill?.command, '/ExampleSkills:claude-plugin-nested');
303
+ assert.equal(nestedPluginSkill?.description, 'Nested Claude plugin skill');
304
+
305
+ const siblingPluginSkill = byName.get('claude-plugin-sibling');
306
+ assert.equal(siblingPluginSkill?.scope, 'plugin');
307
+ assert.equal(siblingPluginSkill?.pluginName, 'example-skills');
308
+ assert.equal(siblingPluginSkill?.command, '/example-skills:claude-plugin-sibling');
309
+ assert.equal(siblingPluginSkill?.description, 'Sibling Claude plugin skill');
310
+ assert.equal(byName.has('disabled-command'), false);
311
+ assert.equal(byName.has('disabled-plugin'), false);
312
+ assert.equal(byName.has('invalid-empty-command'), false);
313
+ assert.equal(byName.has('invalid-at-command'), false);
314
+ assert.equal(skills.some((skill) => skill.command.startsWith('/:')), false);
315
+ } finally {
316
+ restoreHomeDir();
317
+ await fs.rm(tempRoot, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ /**
322
+ * This test covers Codex repository/user/system skill folders and verifies that
323
+ * repository lookup includes cwd, parent, and git root skill locations.
324
+ */
325
+ test('providerSkillsService lists codex repository, user, and system skills', { concurrency: false }, async () => {
326
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-codex-'));
327
+ const repoRoot = path.join(tempRoot, 'repo');
328
+ const workspacePath = path.join(repoRoot, 'packages', 'app');
329
+ await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true });
330
+ await fs.mkdir(workspacePath, { recursive: true });
331
+
332
+ const restoreHomeDir = patchHomeDir(tempRoot);
333
+ try {
334
+ await writeSkill(
335
+ path.join(workspacePath, '.agents', 'skills'),
336
+ 'codex-cwd-dir',
337
+ 'codex-cwd',
338
+ 'Codex cwd skill',
339
+ );
340
+ await writeSkill(
341
+ path.join(repoRoot, 'packages', '.agents', 'skills'),
342
+ 'codex-parent-dir',
343
+ 'codex-parent',
344
+ 'Codex parent skill',
345
+ );
346
+ await writeSkill(
347
+ path.join(repoRoot, '.agents', 'skills'),
348
+ 'codex-root-dir',
349
+ 'codex-root',
350
+ 'Codex root skill',
351
+ );
352
+ await writeSkill(
353
+ path.join(tempRoot, '.agents', 'skills'),
354
+ 'codex-user-dir',
355
+ 'codex-user',
356
+ 'Codex user skill',
357
+ );
358
+ await writeSkill(
359
+ path.join(tempRoot, '.codex', 'skills', '.system'),
360
+ 'codex-system-dir',
361
+ 'codex-system',
362
+ 'Codex system skill',
363
+ );
364
+
365
+ const skills = await providerSkillsService.listProviderSkills('codex', { workspacePath });
366
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
367
+
368
+ assert.equal(byName.get('codex-cwd')?.scope, 'repo');
369
+ assert.equal(byName.get('codex-parent')?.scope, 'repo');
370
+ assert.equal(byName.get('codex-root')?.scope, 'repo');
371
+ assert.equal(byName.get('codex-user')?.scope, 'user');
372
+ assert.equal(byName.get('codex-system')?.scope, 'system');
373
+ assert.equal(byName.get('codex-root')?.command, '$codex-root');
374
+ } finally {
375
+ restoreHomeDir();
376
+ await fs.rm(tempRoot, { recursive: true, force: true });
377
+ }
378
+ });
379
+
380
+ /**
381
+ * This test covers Gemini and Cursor skill directory rules, including shared
382
+ * `.agents/skills` project support.
383
+ */
384
+ test('providerSkillsService lists gemini and cursor skills from their configured directories', { concurrency: false }, async () => {
385
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gc-'));
386
+ const workspacePath = path.join(tempRoot, 'workspace');
387
+ await fs.mkdir(workspacePath, { recursive: true });
388
+
389
+ const restoreHomeDir = patchHomeDir(tempRoot);
390
+ try {
391
+ await writeSkill(
392
+ path.join(tempRoot, '.gemini', 'skills'),
393
+ 'gemini-user-dir',
394
+ 'gemini-user',
395
+ 'Gemini user skill',
396
+ );
397
+ await writeSkill(
398
+ path.join(tempRoot, '.agents', 'skills'),
399
+ 'agents-user-dir',
400
+ 'agents-user',
401
+ 'Agents user skill',
402
+ );
403
+ await writeSkill(
404
+ path.join(workspacePath, '.gemini', 'skills'),
405
+ 'gemini-project-dir',
406
+ 'gemini-project',
407
+ 'Gemini project skill',
408
+ );
409
+ await writeSkill(
410
+ path.join(workspacePath, '.agents', 'skills'),
411
+ 'agents-project-dir',
412
+ 'agents-project',
413
+ 'Agents project skill',
414
+ );
415
+ await writeSkill(
416
+ path.join(workspacePath, '.cursor', 'skills'),
417
+ 'cursor-project-dir',
418
+ 'cursor-project',
419
+ 'Cursor project skill',
420
+ );
421
+ await writeSkill(
422
+ path.join(tempRoot, '.cursor', 'skills'),
423
+ 'cursor-user-dir',
424
+ 'cursor-user',
425
+ 'Cursor user skill',
426
+ );
427
+
428
+ const geminiSkills = await providerSkillsService.listProviderSkills('gemini', { workspacePath });
429
+ const geminiByName = new Map(geminiSkills.map((skill) => [skill.name, skill]));
430
+ assert.equal(geminiByName.get('gemini-user')?.scope, 'user');
431
+ assert.equal(geminiByName.get('agents-user')?.scope, 'user');
432
+ assert.equal(geminiByName.get('gemini-project')?.scope, 'project');
433
+ assert.equal(geminiByName.get('agents-project')?.scope, 'project');
434
+ assert.equal(geminiByName.get('gemini-project')?.command, '/gemini-project');
435
+
436
+ const cursorSkills = await providerSkillsService.listProviderSkills('cursor', { workspacePath });
437
+ const cursorByName = new Map(cursorSkills.map((skill) => [skill.name, skill]));
438
+ assert.equal(cursorByName.get('agents-project')?.scope, 'project');
439
+ assert.equal(cursorByName.get('cursor-project')?.scope, 'project');
440
+ assert.equal(cursorByName.get('cursor-user')?.scope, 'user');
441
+ assert.equal(cursorByName.get('cursor-user')?.command, '/cursor-user');
442
+ } finally {
443
+ restoreHomeDir();
444
+ await fs.rm(tempRoot, { recursive: true, force: true });
445
+ }
446
+ });
@@ -143,7 +143,7 @@ function transformCodexEvent(event) {
143
143
  case 'thread.started':
144
144
  return {
145
145
  type: 'thread_started',
146
- threadId: event.id
146
+ threadId: event.thread_id || event.id
147
147
  };
148
148
 
149
149
  case 'error':
@@ -207,7 +207,8 @@ export async function queryCodex(command, options = {}, ws) {
207
207
 
208
208
  let codex;
209
209
  let thread;
210
- let currentSessionId = sessionId;
210
+ let capturedSessionId = sessionId;
211
+ let sessionCreatedSent = false;
211
212
  let terminalFailure = null;
212
213
  const abortController = new AbortController();
213
214
 
@@ -231,20 +232,23 @@ export async function queryCodex(command, options = {}, ws) {
231
232
  thread = codex.startThread(threadOptions);
232
233
  }
233
234
 
234
- // Get the thread ID
235
- currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
236
-
237
- // Track the session
238
- activeCodexSessions.set(currentSessionId, {
239
- thread,
240
- codex,
241
- status: 'running',
242
- abortController,
243
- startedAt: new Date().toISOString()
244
- });
235
+ const registerSession = (id) => {
236
+ if (!id) {
237
+ return;
238
+ }
239
+ activeCodexSessions.set(id, {
240
+ thread,
241
+ codex,
242
+ status: 'running',
243
+ abortController,
244
+ startedAt: new Date().toISOString()
245
+ });
246
+ };
245
247
 
246
- // Send session created event
247
- sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
248
+ // Existing sessions can be tracked immediately; new sessions are tracked after thread.started.
249
+ if (capturedSessionId) {
250
+ registerSession(capturedSessionId);
251
+ }
248
252
 
249
253
  // Execute with streaming
250
254
  const streamedTurn = await thread.runStreamed(command, {
@@ -252,11 +256,34 @@ export async function queryCodex(command, options = {}, ws) {
252
256
  });
253
257
 
254
258
  for await (const event of streamedTurn.events) {
259
+ // Capture thread/session id lazily from the stream (Codex emits this asynchronously).
260
+ if (event.type === 'thread.started') {
261
+ const discoveredSessionId = event.thread_id || event.id || null;
262
+ if (discoveredSessionId && !capturedSessionId) {
263
+ capturedSessionId = discoveredSessionId;
264
+ registerSession(capturedSessionId);
265
+
266
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
267
+ ws.setSessionId(capturedSessionId);
268
+ }
269
+
270
+ if (!sessionId && !sessionCreatedSent) {
271
+ sessionCreatedSent = true;
272
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'codex' }));
273
+ }
274
+ }
275
+ }
276
+
255
277
  // Check if session was aborted
256
- const session = activeCodexSessions.get(currentSessionId);
257
- if (!session || session.status === 'aborted') {
278
+ if (abortController.signal.aborted) {
258
279
  break;
259
280
  }
281
+ if (capturedSessionId) {
282
+ const session = activeCodexSessions.get(capturedSessionId);
283
+ if (session?.status === 'aborted') {
284
+ break;
285
+ }
286
+ }
260
287
 
261
288
  if (event.type === 'item.started' || event.type === 'item.updated') {
262
289
  continue;
@@ -265,7 +292,7 @@ export async function queryCodex(command, options = {}, ws) {
265
292
  const transformed = transformCodexEvent(event);
266
293
 
267
294
  // Normalize the transformed event into NormalizedMessage(s) via adapter
268
- const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, currentSessionId);
295
+ const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, capturedSessionId || sessionId || null);
269
296
  for (const msg of normalizedMsgs) {
270
297
  sendMessage(ws, msg);
271
298
  }
@@ -275,7 +302,7 @@ export async function queryCodex(command, options = {}, ws) {
275
302
  notifyRunFailed({
276
303
  userId: ws?.userId || null,
277
304
  provider: 'codex',
278
- sessionId: currentSessionId,
305
+ sessionId: capturedSessionId || sessionId || null,
279
306
  sessionName: sessionSummary,
280
307
  error: terminalFailure
281
308
  });
@@ -284,24 +311,29 @@ export async function queryCodex(command, options = {}, ws) {
284
311
  // Extract and send token usage if available (normalized to match Claude format)
285
312
  if (event.type === 'turn.completed' && event.usage) {
286
313
  const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
287
- sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
314
+ sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
288
315
  }
289
316
  }
290
317
 
291
318
  // Send completion event
292
319
  if (!terminalFailure) {
293
- sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
320
+ sendMessage(ws, createNormalizedMessage({
321
+ kind: 'complete',
322
+ actualSessionId: capturedSessionId || thread.id || sessionId || null,
323
+ sessionId: capturedSessionId || sessionId || null,
324
+ provider: 'codex'
325
+ }));
294
326
  notifyRunStopped({
295
327
  userId: ws?.userId || null,
296
328
  provider: 'codex',
297
- sessionId: currentSessionId,
329
+ sessionId: capturedSessionId || sessionId || null,
298
330
  sessionName: sessionSummary,
299
331
  stopReason: 'completed'
300
332
  });
301
333
  }
302
334
 
303
335
  } catch (error) {
304
- const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
336
+ const session = capturedSessionId ? activeCodexSessions.get(capturedSessionId) : null;
305
337
  const wasAborted =
306
338
  session?.status === 'aborted' ||
307
339
  error?.name === 'AbortError' ||
@@ -316,12 +348,12 @@ export async function queryCodex(command, options = {}, ws) {
316
348
  ? 'Codex CLI is not configured. Please set up authentication first.'
317
349
  : error.message;
318
350
 
319
- sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
351
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
320
352
  if (!terminalFailure) {
321
353
  notifyRunFailed({
322
354
  userId: ws?.userId || null,
323
355
  provider: 'codex',
324
- sessionId: currentSessionId,
356
+ sessionId: capturedSessionId || sessionId || null,
325
357
  sessionName: sessionSummary,
326
358
  error
327
359
  });
@@ -330,8 +362,8 @@ export async function queryCodex(command, options = {}, ws) {
330
362
 
331
363
  } finally {
332
364
  // Update session status
333
- if (currentSessionId) {
334
- const session = activeCodexSessions.get(currentSessionId);
365
+ if (capturedSessionId) {
366
+ const session = activeCodexSessions.get(capturedSessionId);
335
367
  if (session) {
336
368
  session.status = session.status === 'aborted' ? 'aborted' : 'completed';
337
369
  }
@@ -1,9 +1,11 @@
1
- import express from 'express';
2
1
  import { promises as fs } from 'fs';
3
- import path from 'path';
4
2
  import os from 'os';
3
+ import path from 'path';
4
+
5
+ import express from 'express';
6
+
5
7
  import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
6
- import { parseFrontmatter } from '../utils/frontmatter.js';
8
+ import { parseFrontMatter } from '../shared/frontmatter.js';
7
9
  import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
8
10
 
9
11
  const __dirname = getModuleDir(import.meta.url);
@@ -40,7 +42,7 @@ async function scanCommandsDirectory(dir, baseDir, namespace) {
40
42
  // Parse markdown file for metadata
41
43
  try {
42
44
  const content = await fs.readFile(fullPath, 'utf8');
43
- const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
45
+ const { data: frontmatter, content: commandContent } = parseFrontMatter(content);
44
46
 
45
47
  // Calculate relative path from baseDir for command name
46
48
  const relativePath = path.relative(baseDir, fullPath);
@@ -513,7 +515,7 @@ router.post('/execute', async (req, res) => {
513
515
  }
514
516
  }
515
517
  const content = await fs.readFile(commandPath, 'utf8');
516
- const { data: metadata, content: commandContent } = parseFrontmatter(content);
518
+ const { data: metadata, content: commandContent } = parseFrontMatter(content);
517
519
  // Basic argument replacement (will be enhanced in command parser utility)
518
520
  let processedContent = commandContent;
519
521