@openchamber/web 1.5.4 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{ToolOutputDialog-PTbkCnSK.js → ToolOutputDialog-CLg_YFoi.js} +1 -1
- package/dist/assets/{index-X4_48EOB.js → index-DCvGkY6V.js} +2 -2
- package/dist/assets/{main-E3fIOPkV.js → main-Co1JTQ7W.js} +57 -57
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/index.js +50 -60
- package/server/lib/opencode-config.js +79 -17
- package/server/lib/skills-catalog/clawdhub/install.js +17 -1
- package/server/lib/skills-catalog/install.js +19 -3
package/dist/index.html
CHANGED
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
pointer-events: none;
|
|
192
192
|
}
|
|
193
193
|
</style>
|
|
194
|
-
<script type="module" crossorigin src="/assets/index-
|
|
194
|
+
<script type="module" crossorigin src="/assets/index-DCvGkY6V.js"></script>
|
|
195
195
|
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-BrcdMJ39.js">
|
|
196
196
|
<link rel="stylesheet" crossorigin href="/assets/vendor--Jn2c0Clh.css">
|
|
197
197
|
<link rel="stylesheet" crossorigin href="/assets/index-DMDb0aQz.css">
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";self.addEventListener("install",t=>{t.waitUntil(self.skipWaiting())}),self.addEventListener("activate",t=>{t.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{const i=t.data?.json()??null;if(!i)return;const
|
|
1
|
+
(function(){"use strict";self.addEventListener("install",t=>{t.waitUntil(self.skipWaiting())}),self.addEventListener("activate",t=>{t.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{t.waitUntil((async()=>{const i=t.data?.json()??null;if(!i||(await self.clients.matchAll({type:"window",includeUncontrolled:!0})).some(a=>a.visibilityState==="visible"||a.focused))return;const e=i.title||"OpenChamber",s=i.body??"",l=i.icon??"/apple-touch-icon-180x180.png",o=i.badge??"/favicon-32.png";await self.registration.showNotification(e,{body:s,icon:l,badge:o,tag:i.tag,data:i.data})})())}),self.addEventListener("notificationclick",t=>{t.notification.close();const n=(t.notification.data??null)?.url??"/";t.waitUntil(self.clients.openWindow(n))})})();
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1009,10 +1009,7 @@ const getUiSessionTokenFromRequest = (req) => {
|
|
|
1009
1009
|
return null;
|
|
1010
1010
|
};
|
|
1011
1011
|
|
|
1012
|
-
const
|
|
1013
|
-
if (!uiSessionToken) return [];
|
|
1014
|
-
const store = await readPushSubscriptionsFromDisk();
|
|
1015
|
-
const record = store.subscriptionsBySession?.[uiSessionToken];
|
|
1012
|
+
const normalizePushSubscriptions = (record) => {
|
|
1016
1013
|
if (!Array.isArray(record)) return [];
|
|
1017
1014
|
return record
|
|
1018
1015
|
.map((entry) => {
|
|
@@ -1033,6 +1030,13 @@ const getPushSubscriptionsForUiSession = async (uiSessionToken) => {
|
|
|
1033
1030
|
.filter(Boolean);
|
|
1034
1031
|
};
|
|
1035
1032
|
|
|
1033
|
+
const getPushSubscriptionsForUiSession = async (uiSessionToken) => {
|
|
1034
|
+
if (!uiSessionToken) return [];
|
|
1035
|
+
const store = await readPushSubscriptionsFromDisk();
|
|
1036
|
+
const record = store.subscriptionsBySession?.[uiSessionToken];
|
|
1037
|
+
return normalizePushSubscriptions(record);
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1036
1040
|
const addOrUpdatePushSubscription = async (uiSessionToken, subscription, userAgent) => {
|
|
1037
1041
|
if (!uiSessionToken) {
|
|
1038
1042
|
return;
|
|
@@ -1108,71 +1112,75 @@ const buildSessionDeepLinkUrl = (sessionId) => {
|
|
|
1108
1112
|
return `/?session=${encodeURIComponent(sessionId)}`;
|
|
1109
1113
|
};
|
|
1110
1114
|
|
|
1111
|
-
const
|
|
1115
|
+
const sendPushToSubscription = async (sub, payload) => {
|
|
1112
1116
|
await ensurePushInitialized();
|
|
1113
|
-
|
|
1114
|
-
const subscriptions = await getPushSubscriptionsForUiSession(uiSessionToken);
|
|
1115
|
-
if (subscriptions.length === 0) {
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
1117
|
const body = JSON.stringify(payload);
|
|
1120
1118
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
};
|
|
1119
|
+
const pushSubscription = {
|
|
1120
|
+
endpoint: sub.endpoint,
|
|
1121
|
+
keys: {
|
|
1122
|
+
p256dh: sub.p256dh,
|
|
1123
|
+
auth: sub.auth,
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1129
1126
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
console.warn('[Push] Failed to send notification:', error);
|
|
1127
|
+
try {
|
|
1128
|
+
await webPush.sendNotification(pushSubscription, body);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
const statusCode = typeof error?.statusCode === 'number' ? error.statusCode : null;
|
|
1131
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
1132
|
+
await removePushSubscriptionFromAllSessions(sub.endpoint);
|
|
1133
|
+
return;
|
|
1139
1134
|
}
|
|
1140
|
-
|
|
1135
|
+
console.warn('[Push] Failed to send notification:', error);
|
|
1136
|
+
}
|
|
1141
1137
|
};
|
|
1142
1138
|
|
|
1143
1139
|
const sendPushToAllUiSessions = async (payload, options = {}) => {
|
|
1144
1140
|
const requireNoSse = options.requireNoSse === true;
|
|
1145
1141
|
const store = await readPushSubscriptionsFromDisk();
|
|
1146
|
-
const
|
|
1142
|
+
const sessions = store.subscriptionsBySession || {};
|
|
1143
|
+
const subscriptionsByEndpoint = new Map();
|
|
1144
|
+
|
|
1145
|
+
for (const [token, record] of Object.entries(sessions)) {
|
|
1146
|
+
const subscriptions = normalizePushSubscriptions(record);
|
|
1147
|
+
if (subscriptions.length === 0) continue;
|
|
1148
|
+
|
|
1149
|
+
for (const sub of subscriptions) {
|
|
1150
|
+
if (!subscriptionsByEndpoint.has(sub.endpoint)) {
|
|
1151
|
+
subscriptionsByEndpoint.set(sub.endpoint, sub);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1147
1155
|
|
|
1148
|
-
await Promise.all(
|
|
1149
|
-
if (requireNoSse &&
|
|
1156
|
+
await Promise.all(Array.from(subscriptionsByEndpoint.entries()).map(async ([endpoint, sub]) => {
|
|
1157
|
+
if (requireNoSse && isAnyUiVisible()) {
|
|
1150
1158
|
return;
|
|
1151
1159
|
}
|
|
1152
|
-
await
|
|
1160
|
+
await sendPushToSubscription(sub, payload);
|
|
1153
1161
|
}));
|
|
1154
1162
|
};
|
|
1155
1163
|
|
|
1156
1164
|
let pushInitialized = false;
|
|
1157
|
-
const activeUiSseConnections = new Set();
|
|
1158
1165
|
|
|
1159
1166
|
|
|
1160
1167
|
|
|
1161
|
-
const VISIBILITY_TTL_MS = 30000;
|
|
1162
1168
|
const uiVisibilityByToken = new Map();
|
|
1169
|
+
let globalVisibilityState = false;
|
|
1163
1170
|
|
|
1164
1171
|
const updateUiVisibility = (token, visible) => {
|
|
1165
1172
|
if (!token) return;
|
|
1166
|
-
|
|
1167
|
-
|
|
1173
|
+
const now = Date.now();
|
|
1174
|
+
const nextVisible = Boolean(visible);
|
|
1175
|
+
uiVisibilityByToken.set(token, { visible: nextVisible, updatedAt: now });
|
|
1176
|
+
globalVisibilityState = nextVisible;
|
|
1168
1177
|
|
|
1169
|
-
const isUiVisible = (token) => {
|
|
1170
|
-
const entry = uiVisibilityByToken.get(token);
|
|
1171
|
-
if (!entry) return false;
|
|
1172
|
-
if (Date.now() - entry.updatedAt > VISIBILITY_TTL_MS) return false;
|
|
1173
|
-
return entry.visible === true;
|
|
1174
1178
|
};
|
|
1175
1179
|
|
|
1180
|
+
const isAnyUiVisible = () => globalVisibilityState === true;
|
|
1181
|
+
|
|
1182
|
+
const isUiVisible = (token) => uiVisibilityByToken.get(token)?.visible === true;
|
|
1183
|
+
|
|
1176
1184
|
const resolveVapidSubject = async () => {
|
|
1177
1185
|
const configured = process.env.OPENCHAMBER_VAPID_SUBJECT;
|
|
1178
1186
|
if (typeof configured === 'string' && configured.trim().length > 0) {
|
|
@@ -2808,15 +2816,6 @@ async function main(options = {}) {
|
|
|
2808
2816
|
});
|
|
2809
2817
|
|
|
2810
2818
|
app.get('/api/global/event', async (req, res) => {
|
|
2811
|
-
const uiToken = getUiSessionTokenFromRequest(req);
|
|
2812
|
-
if (uiToken) {
|
|
2813
|
-
activeUiSseConnections.add(uiToken);
|
|
2814
|
-
const cleanupUiToken = () => {
|
|
2815
|
-
activeUiSseConnections.delete(uiToken);
|
|
2816
|
-
};
|
|
2817
|
-
req.on('close', cleanupUiToken);
|
|
2818
|
-
req.on('error', cleanupUiToken);
|
|
2819
|
-
}
|
|
2820
2819
|
let targetUrl;
|
|
2821
2820
|
try {
|
|
2822
2821
|
targetUrl = new URL(buildOpenCodeUrl('/global/event', ''));
|
|
@@ -2928,15 +2927,6 @@ async function main(options = {}) {
|
|
|
2928
2927
|
});
|
|
2929
2928
|
|
|
2930
2929
|
app.get('/api/event', async (req, res) => {
|
|
2931
|
-
const uiToken = getUiSessionTokenFromRequest(req);
|
|
2932
|
-
if (uiToken) {
|
|
2933
|
-
activeUiSseConnections.add(uiToken);
|
|
2934
|
-
const cleanupUiToken = () => {
|
|
2935
|
-
activeUiSseConnections.delete(uiToken);
|
|
2936
|
-
};
|
|
2937
|
-
req.on('close', cleanupUiToken);
|
|
2938
|
-
req.on('error', cleanupUiToken);
|
|
2939
|
-
}
|
|
2940
2930
|
let targetUrl;
|
|
2941
2931
|
try {
|
|
2942
2932
|
targetUrl = new URL(buildOpenCodeUrl('/event', ''));
|
|
@@ -5,9 +5,9 @@ import yaml from 'yaml';
|
|
|
5
5
|
import { parse as parseJsonc } from 'jsonc-parser';
|
|
6
6
|
|
|
7
7
|
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
8
|
-
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, '
|
|
9
|
-
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, '
|
|
10
|
-
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, '
|
|
8
|
+
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
|
9
|
+
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
|
10
|
+
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
|
11
11
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
|
12
12
|
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
|
13
13
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
|
@@ -51,10 +51,14 @@ function ensureDirs() {
|
|
|
51
51
|
* Ensure project-level agent directory exists
|
|
52
52
|
*/
|
|
53
53
|
function ensureProjectAgentDir(workingDirectory) {
|
|
54
|
-
const projectAgentDir = path.join(workingDirectory, '.opencode', '
|
|
54
|
+
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agents');
|
|
55
55
|
if (!fs.existsSync(projectAgentDir)) {
|
|
56
56
|
fs.mkdirSync(projectAgentDir, { recursive: true });
|
|
57
57
|
}
|
|
58
|
+
const legacyProjectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
|
59
|
+
if (!fs.existsSync(legacyProjectAgentDir)) {
|
|
60
|
+
fs.mkdirSync(legacyProjectAgentDir, { recursive: true });
|
|
61
|
+
}
|
|
58
62
|
return projectAgentDir;
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -62,14 +66,20 @@ function ensureProjectAgentDir(workingDirectory) {
|
|
|
62
66
|
* Get project-level agent path
|
|
63
67
|
*/
|
|
64
68
|
function getProjectAgentPath(workingDirectory, agentName) {
|
|
65
|
-
|
|
69
|
+
const pluralPath = path.join(workingDirectory, '.opencode', 'agents', `${agentName}.md`);
|
|
70
|
+
const legacyPath = path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
|
71
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
72
|
+
return pluralPath;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
76
|
* Get user-level agent path
|
|
70
77
|
*/
|
|
71
78
|
function getUserAgentPath(agentName) {
|
|
72
|
-
|
|
79
|
+
const pluralPath = path.join(AGENT_DIR, `${agentName}.md`);
|
|
80
|
+
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'agent', `${agentName}.md`);
|
|
81
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
82
|
+
return pluralPath;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
@@ -173,10 +183,14 @@ function getAgentPermissionSource(agentName, workingDirectory) {
|
|
|
173
183
|
* Ensure project-level command directory exists
|
|
174
184
|
*/
|
|
175
185
|
function ensureProjectCommandDir(workingDirectory) {
|
|
176
|
-
const projectCommandDir = path.join(workingDirectory, '.opencode', '
|
|
186
|
+
const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
|
|
177
187
|
if (!fs.existsSync(projectCommandDir)) {
|
|
178
188
|
fs.mkdirSync(projectCommandDir, { recursive: true });
|
|
179
189
|
}
|
|
190
|
+
const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
|
191
|
+
if (!fs.existsSync(legacyProjectCommandDir)) {
|
|
192
|
+
fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
|
|
193
|
+
}
|
|
180
194
|
return projectCommandDir;
|
|
181
195
|
}
|
|
182
196
|
|
|
@@ -184,14 +198,20 @@ function ensureProjectCommandDir(workingDirectory) {
|
|
|
184
198
|
* Get project-level command path
|
|
185
199
|
*/
|
|
186
200
|
function getProjectCommandPath(workingDirectory, commandName) {
|
|
187
|
-
|
|
201
|
+
const pluralPath = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
|
|
202
|
+
const legacyPath = path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
|
203
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
204
|
+
return pluralPath;
|
|
188
205
|
}
|
|
189
206
|
|
|
190
207
|
/**
|
|
191
208
|
* Get user-level command path
|
|
192
209
|
*/
|
|
193
210
|
function getUserCommandPath(commandName) {
|
|
194
|
-
|
|
211
|
+
const pluralPath = path.join(COMMAND_DIR, `${commandName}.md`);
|
|
212
|
+
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'command', `${commandName}.md`);
|
|
213
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
214
|
+
return pluralPath;
|
|
195
215
|
}
|
|
196
216
|
|
|
197
217
|
/**
|
|
@@ -245,10 +265,14 @@ function getCommandWritePath(commandName, workingDirectory, requestedScope) {
|
|
|
245
265
|
* Ensure project-level skill directory exists
|
|
246
266
|
*/
|
|
247
267
|
function ensureProjectSkillDir(workingDirectory) {
|
|
248
|
-
const projectSkillDir = path.join(workingDirectory, '.opencode', '
|
|
268
|
+
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skills');
|
|
249
269
|
if (!fs.existsSync(projectSkillDir)) {
|
|
250
270
|
fs.mkdirSync(projectSkillDir, { recursive: true });
|
|
251
271
|
}
|
|
272
|
+
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
|
273
|
+
if (!fs.existsSync(legacyProjectSkillDir)) {
|
|
274
|
+
fs.mkdirSync(legacyProjectSkillDir, { recursive: true });
|
|
275
|
+
}
|
|
252
276
|
return projectSkillDir;
|
|
253
277
|
}
|
|
254
278
|
|
|
@@ -256,28 +280,40 @@ function ensureProjectSkillDir(workingDirectory) {
|
|
|
256
280
|
* Get project-level skill directory path (.opencode/skill/{name}/)
|
|
257
281
|
*/
|
|
258
282
|
function getProjectSkillDir(workingDirectory, skillName) {
|
|
259
|
-
|
|
283
|
+
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName);
|
|
284
|
+
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName);
|
|
285
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
286
|
+
return pluralPath;
|
|
260
287
|
}
|
|
261
288
|
|
|
262
289
|
/**
|
|
263
290
|
* Get project-level skill SKILL.md path
|
|
264
291
|
*/
|
|
265
292
|
function getProjectSkillPath(workingDirectory, skillName) {
|
|
266
|
-
|
|
293
|
+
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName, 'SKILL.md');
|
|
294
|
+
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName, 'SKILL.md');
|
|
295
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
296
|
+
return pluralPath;
|
|
267
297
|
}
|
|
268
298
|
|
|
269
299
|
/**
|
|
270
300
|
* Get user-level skill directory path
|
|
271
301
|
*/
|
|
272
302
|
function getUserSkillDir(skillName) {
|
|
273
|
-
|
|
303
|
+
const pluralPath = path.join(SKILL_DIR, skillName);
|
|
304
|
+
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName);
|
|
305
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
306
|
+
return pluralPath;
|
|
274
307
|
}
|
|
275
308
|
|
|
276
309
|
/**
|
|
277
310
|
* Get user-level skill SKILL.md path
|
|
278
311
|
*/
|
|
279
312
|
function getUserSkillPath(skillName) {
|
|
280
|
-
|
|
313
|
+
const pluralPath = path.join(SKILL_DIR, skillName, 'SKILL.md');
|
|
314
|
+
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName, 'SKILL.md');
|
|
315
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
316
|
+
return pluralPath;
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
/**
|
|
@@ -1463,9 +1499,9 @@ function discoverSkills(workingDirectory) {
|
|
|
1463
1499
|
}
|
|
1464
1500
|
};
|
|
1465
1501
|
|
|
1466
|
-
// 1. Project level .opencode/
|
|
1502
|
+
// 1. Project level .opencode/skills/ (highest priority)
|
|
1467
1503
|
if (workingDirectory) {
|
|
1468
|
-
const projectSkillDir = path.join(workingDirectory, '.opencode', '
|
|
1504
|
+
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skills');
|
|
1469
1505
|
if (fs.existsSync(projectSkillDir)) {
|
|
1470
1506
|
const entries = fs.readdirSync(projectSkillDir, { withFileTypes: true });
|
|
1471
1507
|
for (const entry of entries) {
|
|
@@ -1477,6 +1513,19 @@ function discoverSkills(workingDirectory) {
|
|
|
1477
1513
|
}
|
|
1478
1514
|
}
|
|
1479
1515
|
}
|
|
1516
|
+
|
|
1517
|
+
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
|
1518
|
+
if (fs.existsSync(legacyProjectSkillDir)) {
|
|
1519
|
+
const entries = fs.readdirSync(legacyProjectSkillDir, { withFileTypes: true });
|
|
1520
|
+
for (const entry of entries) {
|
|
1521
|
+
if (entry.isDirectory()) {
|
|
1522
|
+
const skillMdPath = path.join(legacyProjectSkillDir, entry.name, 'SKILL.md');
|
|
1523
|
+
if (fs.existsSync(skillMdPath)) {
|
|
1524
|
+
addSkill(entry.name, skillMdPath, SKILL_SCOPE.PROJECT, 'opencode');
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1480
1529
|
|
|
1481
1530
|
// 2. Claude-compatible .claude/skills/
|
|
1482
1531
|
const claudeSkillDir = path.join(workingDirectory, '.claude', 'skills');
|
|
@@ -1493,7 +1542,7 @@ function discoverSkills(workingDirectory) {
|
|
|
1493
1542
|
}
|
|
1494
1543
|
}
|
|
1495
1544
|
|
|
1496
|
-
// 3. User level ~/.config/opencode/
|
|
1545
|
+
// 3. User level ~/.config/opencode/skills/
|
|
1497
1546
|
if (fs.existsSync(SKILL_DIR)) {
|
|
1498
1547
|
const entries = fs.readdirSync(SKILL_DIR, { withFileTypes: true });
|
|
1499
1548
|
for (const entry of entries) {
|
|
@@ -1505,6 +1554,19 @@ function discoverSkills(workingDirectory) {
|
|
|
1505
1554
|
}
|
|
1506
1555
|
}
|
|
1507
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
const legacyUserSkillDir = path.join(OPENCODE_CONFIG_DIR, 'skill');
|
|
1559
|
+
if (fs.existsSync(legacyUserSkillDir)) {
|
|
1560
|
+
const entries = fs.readdirSync(legacyUserSkillDir, { withFileTypes: true });
|
|
1561
|
+
for (const entry of entries) {
|
|
1562
|
+
if (entry.isDirectory()) {
|
|
1563
|
+
const skillMdPath = path.join(legacyUserSkillDir, entry.name, 'SKILL.md');
|
|
1564
|
+
if (fs.existsSync(skillMdPath)) {
|
|
1565
|
+
addSkill(entry.name, skillMdPath, SKILL_SCOPE.USER, 'opencode');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1508
1570
|
|
|
1509
1571
|
return Array.from(skills.values());
|
|
1510
1572
|
}
|
|
@@ -14,6 +14,17 @@ import { downloadClawdHubSkill, fetchClawdHubSkillInfo } from './api.js';
|
|
|
14
14
|
|
|
15
15
|
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
16
16
|
|
|
17
|
+
function normalizeUserSkillDir(userSkillDir) {
|
|
18
|
+
if (!userSkillDir) return null;
|
|
19
|
+
const legacySkillDir = path.join(os.homedir(), '.config', 'opencode', 'skill');
|
|
20
|
+
const pluralSkillDir = path.join(os.homedir(), '.config', 'opencode', 'skills');
|
|
21
|
+
if (userSkillDir === legacySkillDir) {
|
|
22
|
+
if (fs.existsSync(legacySkillDir) && !fs.existsSync(pluralSkillDir)) return legacySkillDir;
|
|
23
|
+
return pluralSkillDir;
|
|
24
|
+
}
|
|
25
|
+
return userSkillDir;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
function validateSkillName(skillName) {
|
|
18
29
|
if (typeof skillName !== 'string') return false;
|
|
19
30
|
if (skillName.length < 1 || skillName.length > 64) return false;
|
|
@@ -41,7 +52,7 @@ function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName })
|
|
|
41
52
|
throw new Error('workingDirectory is required for project installs');
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
return path.join(workingDirectory, '.opencode', '
|
|
55
|
+
return path.join(workingDirectory, '.opencode', 'skills', skillName);
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/**
|
|
@@ -71,6 +82,11 @@ export async function installSkillsFromClawdHub({
|
|
|
71
82
|
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
const normalizedUserSkillDir = normalizeUserSkillDir(userSkillDir);
|
|
86
|
+
if (normalizedUserSkillDir) {
|
|
87
|
+
userSkillDir = normalizedUserSkillDir;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
if (scope === 'project' && !workingDirectory) {
|
|
75
91
|
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
|
76
92
|
}
|
|
@@ -7,6 +7,17 @@ import { parseSkillRepoSource } from './source.js';
|
|
|
7
7
|
|
|
8
8
|
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
9
9
|
|
|
10
|
+
function normalizeUserSkillDir(userSkillDir) {
|
|
11
|
+
if (!userSkillDir) return null;
|
|
12
|
+
const legacySkillDir = path.join(os.homedir(), '.config', 'opencode', 'skill');
|
|
13
|
+
const pluralSkillDir = path.join(os.homedir(), '.config', 'opencode', 'skills');
|
|
14
|
+
if (userSkillDir === legacySkillDir) {
|
|
15
|
+
if (fs.existsSync(legacySkillDir) && !fs.existsSync(pluralSkillDir)) return legacySkillDir;
|
|
16
|
+
return pluralSkillDir;
|
|
17
|
+
}
|
|
18
|
+
return userSkillDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
function validateSkillName(skillName) {
|
|
11
22
|
if (typeof skillName !== 'string') return false;
|
|
12
23
|
if (skillName.length < 1 || skillName.length > 64) return false;
|
|
@@ -103,7 +114,7 @@ function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName })
|
|
|
103
114
|
throw new Error('workingDirectory is required for project installs');
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
return path.join(workingDirectory, '.opencode', '
|
|
117
|
+
return path.join(workingDirectory, '.opencode', 'skills', skillName);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
export async function installSkillsFromRepository({
|
|
@@ -123,14 +134,19 @@ export async function installSkillsFromRepository({
|
|
|
123
134
|
return { ok: false, error: gitCheck.error };
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const normalizedUserSkillDir = normalizeUserSkillDir(userSkillDir);
|
|
138
|
+
if (normalizedUserSkillDir) {
|
|
139
|
+
userSkillDir = normalizedUserSkillDir;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
if (!userSkillDir) {
|
|
131
143
|
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
147
|
+
return { ok: false, error: { kind: 'invalidSource', message: 'Invalid scope' } };
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
if (scope === 'project' && !workingDirectory) {
|
|
135
151
|
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
|
136
152
|
}
|