@cloudcli-ai/cloudcli 1.31.5 → 1.33.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 (252) hide show
  1. package/README.de.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.tr.md +1 -1
  6. package/README.zh-CN.md +1 -1
  7. package/dist/api-docs.html +6 -7
  8. package/dist/assets/index-MOIvpy53.css +32 -0
  9. package/dist/assets/{index-DqMVUeZS.js → index-skXSjBje.js} +290 -270
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +841 -0
  12. package/dist-server/server/claude-sdk.js +67 -39
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -9
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +163 -35
  17. package/dist-server/server/gemini-cli.js.map +1 -1
  18. package/dist-server/server/gemini-response-handler.js +34 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -1
  20. package/dist-server/server/index.js +131 -19
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/database/index.js +1 -0
  23. package/dist-server/server/modules/database/index.js.map +1 -1
  24. package/dist-server/server/modules/database/migrations.js +11 -0
  25. package/dist-server/server/modules/database/migrations.js.map +1 -1
  26. package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
  28. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
  29. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
  30. package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
  31. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  32. package/dist-server/server/modules/database/schema.js +1 -0
  33. package/dist-server/server/modules/database/schema.js.map +1 -1
  34. package/dist-server/server/modules/projects/projects.routes.js +12 -3
  35. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
  37. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  38. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  39. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  40. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +47 -0
  41. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  42. package/dist-server/server/modules/providers/index.js +1 -0
  43. package/dist-server/server/modules/providers/index.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +27 -7
  45. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
  47. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  48. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
  49. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
  51. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
  53. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/claude/claude.provider.js +4 -0
  55. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  57. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
  59. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
  61. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +50 -0
  63. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/list/codex/codex.provider.js +4 -0
  65. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  66. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  67. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
  69. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +187 -24
  71. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
  73. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  74. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +4 -0
  75. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
  77. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  79. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  80. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
  81. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +10 -10
  83. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
  85. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +4 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  89. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  90. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  91. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  92. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  93. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  94. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  95. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  96. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  97. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  98. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  99. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  101. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  102. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  103. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  104. package/dist-server/server/modules/providers/provider.routes.js +65 -3
  105. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  107. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  109. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  110. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
  111. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  112. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  113. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  114. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +13 -4
  115. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  116. package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
  117. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  118. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  119. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  120. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
  122. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  123. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  124. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  125. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  126. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  127. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
  128. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  129. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  130. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  131. package/dist-server/server/modules/providers/tests/skills.test.js +259 -0
  132. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  133. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
  134. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  135. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
  136. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
  137. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  138. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  139. package/dist-server/server/openai-codex.js +87 -29
  140. package/dist-server/server/openai-codex.js.map +1 -1
  141. package/dist-server/server/opencode-cli.js +287 -0
  142. package/dist-server/server/opencode-cli.js.map +1 -0
  143. package/dist-server/server/opencode-cli.test.js +84 -0
  144. package/dist-server/server/opencode-cli.test.js.map +1 -0
  145. package/dist-server/server/routes/agent.js +21 -8
  146. package/dist-server/server/routes/agent.js.map +1 -1
  147. package/dist-server/server/routes/commands.js +204 -211
  148. package/dist-server/server/routes/commands.js.map +1 -1
  149. package/dist-server/server/routes/cursor.js +2 -2
  150. package/dist-server/server/routes/cursor.js.map +1 -1
  151. package/dist-server/server/routes/settings.js +0 -10
  152. package/dist-server/server/routes/settings.js.map +1 -1
  153. package/dist-server/server/routes/tests/commands.test.js +76 -0
  154. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  155. package/dist-server/server/shared/claude-cli-path.js +103 -0
  156. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  157. package/dist-server/server/shared/claude-cli-path.test.js +45 -0
  158. package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
  159. package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
  160. package/dist-server/server/shared/frontmatter.js.map +1 -0
  161. package/dist-server/server/shared/utils.js +368 -0
  162. package/dist-server/server/shared/utils.js.map +1 -1
  163. package/dist-server/server/utils/commandParser.js +3 -3
  164. package/dist-server/server/utils/commandParser.js.map +1 -1
  165. package/package.json +3 -1
  166. package/public/api-docs.html +878 -0
  167. package/public/modelConstants.js +841 -0
  168. package/server/claude-sdk.js +74 -40
  169. package/server/cursor-cli.js +6 -11
  170. package/server/gemini-cli.js +197 -43
  171. package/server/gemini-response-handler.js +38 -0
  172. package/server/index.js +144 -19
  173. package/server/modules/database/index.ts +1 -0
  174. package/server/modules/database/migrations.ts +12 -0
  175. package/server/modules/database/repositories/projects.db.ts +13 -0
  176. package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
  177. package/server/modules/database/repositories/sessions.db.ts +60 -9
  178. package/server/modules/database/schema.ts +1 -0
  179. package/server/modules/projects/projects.routes.ts +20 -3
  180. package/server/modules/projects/services/project-delete.service.ts +16 -1
  181. package/server/modules/projects/services/project-management.service.ts +2 -0
  182. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
  183. package/server/modules/providers/README.md +354 -0
  184. package/server/modules/providers/index.ts +2 -1
  185. package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
  186. package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
  187. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
  188. package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
  189. package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
  190. package/server/modules/providers/list/claude/claude.provider.ts +11 -1
  191. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  192. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
  193. package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
  194. package/server/modules/providers/list/codex/codex-skills.provider.ts +60 -0
  195. package/server/modules/providers/list/codex/codex.provider.ts +11 -1
  196. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  197. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
  198. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
  199. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
  200. package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
  201. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
  202. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  203. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
  204. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
  205. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
  206. package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
  207. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  208. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  209. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  210. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  211. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  212. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  213. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  214. package/server/modules/providers/provider.registry.ts +2 -0
  215. package/server/modules/providers/provider.routes.ts +97 -4
  216. package/server/modules/providers/services/mcp.service.ts +1 -12
  217. package/server/modules/providers/services/provider-models.service.ts +325 -0
  218. package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
  219. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  220. package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
  221. package/server/modules/providers/services/sessions.service.ts +112 -9
  222. package/server/modules/providers/services/skills.service.ts +15 -0
  223. package/server/modules/providers/shared/base/abstract.provider.ts +4 -0
  224. package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
  225. package/server/modules/providers/tests/mcp.test.ts +93 -6
  226. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  227. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  228. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  229. package/server/modules/providers/tests/skills.test.ts +512 -0
  230. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  231. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  232. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  233. package/server/openai-codex.js +99 -30
  234. package/server/opencode-cli.js +336 -0
  235. package/server/opencode-cli.test.js +95 -0
  236. package/server/routes/agent.js +22 -8
  237. package/server/routes/commands.js +256 -233
  238. package/server/routes/cursor.js +2 -2
  239. package/server/routes/settings.js +1 -10
  240. package/server/routes/tests/commands.test.js +82 -0
  241. package/server/shared/claude-cli-path.test.ts +61 -0
  242. package/server/shared/claude-cli-path.ts +139 -0
  243. package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
  244. package/server/shared/interfaces.ts +64 -0
  245. package/server/shared/types.ts +166 -1
  246. package/server/shared/utils.ts +478 -0
  247. package/server/utils/commandParser.js +5 -3
  248. package/dist/assets/index-6rYG_mRo.css +0 -32
  249. package/dist-server/server/utils/frontmatter.js.map +0 -1
  250. package/dist-server/shared/modelConstants.js +0 -98
  251. package/dist-server/shared/modelConstants.js.map +0 -1
  252. package/shared/modelConstants.js +0 -106
@@ -0,0 +1,512 @@
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 OpenCode skill lookup across cwd-to-git-root project folders
382
+ * plus the global OpenCode/Claude/Agents compatibility locations.
383
+ */
384
+ test('providerSkillsService lists opencode project and user compatibility skills', { concurrency: false }, async () => {
385
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-opencode-'));
386
+ const repoRoot = path.join(tempRoot, 'repo');
387
+ const workspacePath = path.join(repoRoot, 'packages', 'app');
388
+ await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true });
389
+ await fs.mkdir(workspacePath, { recursive: true });
390
+
391
+ const restoreHomeDir = patchHomeDir(tempRoot);
392
+ try {
393
+ await writeSkill(
394
+ path.join(workspacePath, '.opencode', 'skills'),
395
+ 'opencode-cwd-dir',
396
+ 'opencode-cwd',
397
+ 'OpenCode cwd skill',
398
+ );
399
+ await writeSkill(
400
+ path.join(repoRoot, 'packages', '.claude', 'skills'),
401
+ 'opencode-claude-parent-dir',
402
+ 'opencode-claude-parent',
403
+ 'OpenCode Claude parent skill',
404
+ );
405
+ await writeSkill(
406
+ path.join(repoRoot, '.agents', 'skills'),
407
+ 'opencode-agents-root-dir',
408
+ 'opencode-agents-root',
409
+ 'OpenCode Agents root skill',
410
+ );
411
+ await writeSkill(
412
+ path.join(tempRoot, '.config', 'opencode', 'skills'),
413
+ 'opencode-user-dir',
414
+ 'opencode-user',
415
+ 'OpenCode user skill',
416
+ );
417
+ await writeSkill(
418
+ path.join(tempRoot, '.claude', 'skills'),
419
+ 'opencode-claude-user-dir',
420
+ 'opencode-claude-user',
421
+ 'OpenCode Claude user skill',
422
+ );
423
+ await writeSkill(
424
+ path.join(tempRoot, '.agents', 'skills'),
425
+ 'opencode-agents-user-dir',
426
+ 'opencode-agents-user',
427
+ 'OpenCode Agents user skill',
428
+ );
429
+
430
+ const skills = await providerSkillsService.listProviderSkills('opencode', { workspacePath });
431
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
432
+
433
+ assert.equal(byName.get('opencode-cwd')?.scope, 'project');
434
+ assert.equal(byName.get('opencode-claude-parent')?.scope, 'project');
435
+ assert.equal(byName.get('opencode-agents-root')?.scope, 'project');
436
+ assert.equal(byName.get('opencode-user')?.scope, 'user');
437
+ assert.equal(byName.get('opencode-claude-user')?.scope, 'user');
438
+ assert.equal(byName.get('opencode-agents-user')?.scope, 'user');
439
+ assert.equal(byName.get('opencode-cwd')?.command, '/opencode-cwd');
440
+ } finally {
441
+ restoreHomeDir();
442
+ await fs.rm(tempRoot, { recursive: true, force: true });
443
+ }
444
+ });
445
+
446
+ /**
447
+ * This test covers Gemini and Cursor skill directory rules, including shared
448
+ * `.agents/skills` project support.
449
+ */
450
+ test('providerSkillsService lists gemini and cursor skills from their configured directories', { concurrency: false }, async () => {
451
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gc-'));
452
+ const workspacePath = path.join(tempRoot, 'workspace');
453
+ await fs.mkdir(workspacePath, { recursive: true });
454
+
455
+ const restoreHomeDir = patchHomeDir(tempRoot);
456
+ try {
457
+ await writeSkill(
458
+ path.join(tempRoot, '.gemini', 'skills'),
459
+ 'gemini-user-dir',
460
+ 'gemini-user',
461
+ 'Gemini user skill',
462
+ );
463
+ await writeSkill(
464
+ path.join(tempRoot, '.agents', 'skills'),
465
+ 'agents-user-dir',
466
+ 'agents-user',
467
+ 'Agents user skill',
468
+ );
469
+ await writeSkill(
470
+ path.join(workspacePath, '.gemini', 'skills'),
471
+ 'gemini-project-dir',
472
+ 'gemini-project',
473
+ 'Gemini project skill',
474
+ );
475
+ await writeSkill(
476
+ path.join(workspacePath, '.agents', 'skills'),
477
+ 'agents-project-dir',
478
+ 'agents-project',
479
+ 'Agents project skill',
480
+ );
481
+ await writeSkill(
482
+ path.join(workspacePath, '.cursor', 'skills'),
483
+ 'cursor-project-dir',
484
+ 'cursor-project',
485
+ 'Cursor project skill',
486
+ );
487
+ await writeSkill(
488
+ path.join(tempRoot, '.cursor', 'skills'),
489
+ 'cursor-user-dir',
490
+ 'cursor-user',
491
+ 'Cursor user skill',
492
+ );
493
+
494
+ const geminiSkills = await providerSkillsService.listProviderSkills('gemini', { workspacePath });
495
+ const geminiByName = new Map(geminiSkills.map((skill) => [skill.name, skill]));
496
+ assert.equal(geminiByName.get('gemini-user')?.scope, 'user');
497
+ assert.equal(geminiByName.get('agents-user')?.scope, 'user');
498
+ assert.equal(geminiByName.get('gemini-project')?.scope, 'project');
499
+ assert.equal(geminiByName.get('agents-project')?.scope, 'project');
500
+ assert.equal(geminiByName.get('gemini-project')?.command, '/gemini-project');
501
+
502
+ const cursorSkills = await providerSkillsService.listProviderSkills('cursor', { workspacePath });
503
+ const cursorByName = new Map(cursorSkills.map((skill) => [skill.name, skill]));
504
+ assert.equal(cursorByName.get('agents-project')?.scope, 'project');
505
+ assert.equal(cursorByName.get('cursor-project')?.scope, 'project');
506
+ assert.equal(cursorByName.get('cursor-user')?.scope, 'user');
507
+ assert.equal(cursorByName.get('cursor-user')?.command, '/cursor-user');
508
+ } finally {
509
+ restoreHomeDir();
510
+ await fs.rm(tempRoot, { recursive: true, force: true });
511
+ }
512
+ });
@@ -29,10 +29,12 @@ type ChatWebSocketDependencies = {
29
29
  spawnCursor: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
30
30
  queryCodex: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
31
31
  spawnGemini: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
32
+ spawnOpenCode: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
32
33
  abortClaudeSDKSession: (sessionId: string) => Promise<boolean>;
33
34
  abortCursorSession: (sessionId: string) => boolean;
34
35
  abortCodexSession: (sessionId: string) => boolean;
35
36
  abortGeminiSession: (sessionId: string) => boolean;
37
+ abortOpenCodeSession: (sessionId: string) => boolean;
36
38
  resolveToolApproval: (
37
39
  requestId: string,
38
40
  payload: {
@@ -46,19 +48,21 @@ type ChatWebSocketDependencies = {
46
48
  isCursorSessionActive: (sessionId: string) => boolean;
47
49
  isCodexSessionActive: (sessionId: string) => boolean;
48
50
  isGeminiSessionActive: (sessionId: string) => boolean;
51
+ isOpenCodeSessionActive: (sessionId: string) => boolean;
49
52
  reconnectSessionWriter: (sessionId: string, ws: WebSocket) => boolean;
50
53
  getPendingApprovalsForSession: (sessionId: string) => unknown[];
51
54
  getActiveClaudeSDKSessions: () => unknown;
52
55
  getActiveCursorSessions: () => unknown;
53
56
  getActiveCodexSessions: () => unknown;
54
57
  getActiveGeminiSessions: () => unknown;
58
+ getActiveOpenCodeSessions: () => unknown;
55
59
  };
56
60
 
57
61
  /**
58
62
  * Normalizes potentially invalid provider names coming from websocket payloads.
59
63
  */
60
64
  function readProvider(value: unknown): LLMProvider {
61
- if (value === 'claude' || value === 'cursor' || value === 'codex' || value === 'gemini') {
65
+ if (value === 'claude' || value === 'cursor' || value === 'codex' || value === 'gemini' || value === 'opencode') {
62
66
  return value;
63
67
  }
64
68
 
@@ -134,6 +138,11 @@ export function handleChatConnection(
134
138
  return;
135
139
  }
136
140
 
141
+ if (messageType === 'opencode-command') {
142
+ await dependencies.spawnOpenCode(data.command ?? '', data.options, writer);
143
+ return;
144
+ }
145
+
137
146
  if (messageType === 'cursor-resume') {
138
147
  await dependencies.spawnCursor(
139
148
  '',
@@ -158,6 +167,8 @@ export function handleChatConnection(
158
167
  success = dependencies.abortCodexSession(sessionId);
159
168
  } else if (provider === 'gemini') {
160
169
  success = dependencies.abortGeminiSession(sessionId);
170
+ } else if (provider === 'opencode') {
171
+ success = dependencies.abortOpenCodeSession(sessionId);
161
172
  } else {
162
173
  success = await dependencies.abortClaudeSDKSession(sessionId);
163
174
  }
@@ -214,6 +225,8 @@ export function handleChatConnection(
214
225
  isActive = dependencies.isCodexSessionActive(sessionId);
215
226
  } else if (provider === 'gemini') {
216
227
  isActive = dependencies.isGeminiSessionActive(sessionId);
228
+ } else if (provider === 'opencode') {
229
+ isActive = dependencies.isOpenCodeSessionActive(sessionId);
217
230
  } else {
218
231
  isActive = dependencies.isClaudeSDKSessionActive(sessionId);
219
232
  if (isActive) {
@@ -251,6 +264,7 @@ export function handleChatConnection(
251
264
  cursor: dependencies.getActiveCursorSessions(),
252
265
  codex: dependencies.getActiveCodexSessions(),
253
266
  gemini: dependencies.getActiveGeminiSessions(),
267
+ opencode: dependencies.getActiveOpenCodeSessions(),
254
268
  },
255
269
  });
256
270
  }
@@ -26,15 +26,15 @@ export function handlePluginWsProxy(
26
26
  console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
27
27
  });
28
28
 
29
- upstream.on('message', (data) => {
29
+ upstream.on('message', (data, isBinary) => {
30
30
  if (clientWs.readyState === WebSocket.OPEN) {
31
- clientWs.send(data);
31
+ clientWs.send(data, { binary: isBinary });
32
32
  }
33
33
  });
34
34
 
35
- clientWs.on('message', (data) => {
35
+ clientWs.on('message', (data, isBinary) => {
36
36
  if (upstream.readyState === WebSocket.OPEN) {
37
- upstream.send(data);
37
+ upstream.send(data, { binary: isBinary });
38
38
  }
39
39
  });
40
40
 
@@ -136,6 +136,13 @@ function buildShellCommand(
136
136
  return command;
137
137
  }
138
138
 
139
+ if (provider === 'opencode') {
140
+ if (hasSession && sessionId) {
141
+ return `opencode --session "${sessionId}"`;
142
+ }
143
+ return initialCommand || 'opencode';
144
+ }
145
+
139
146
  const command = initialCommand || 'claude';
140
147
  if (hasSession && sessionId) {
141
148
  if (os.platform() === 'win32') {
@@ -389,6 +396,8 @@ export function handleShellConnection(
389
396
  ? 'Codex'
390
397
  : provider === 'gemini'
391
398
  ? 'Gemini'
399
+ : provider === 'opencode'
400
+ ? 'OpenCode'
392
401
  : 'Claude';
393
402
  welcomeMsg = hasSession
394
403
  ? `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n`