@vellumai/assistant 0.4.11 → 0.4.13
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/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
import { existsSync, rmSync } from
|
|
2
|
-
import * as net from
|
|
3
|
-
import { join } from
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getConfig,
|
|
7
|
+
invalidateConfigCache,
|
|
8
|
+
loadRawConfig,
|
|
9
|
+
saveRawConfig,
|
|
10
|
+
} from "../../config/loader.js";
|
|
11
|
+
import { resolveSkillStates } from "../../config/skill-state.js";
|
|
12
|
+
import {
|
|
13
|
+
ensureSkillIcon,
|
|
14
|
+
loadSkillBySelector,
|
|
15
|
+
loadSkillCatalog,
|
|
16
|
+
type SkillSummary,
|
|
17
|
+
} from "../../config/skills.js";
|
|
18
|
+
import {
|
|
19
|
+
createTimeout,
|
|
20
|
+
extractText,
|
|
21
|
+
getConfiguredProvider,
|
|
22
|
+
userMessage,
|
|
23
|
+
} from "../../providers/provider-send-message.js";
|
|
24
|
+
import {
|
|
25
|
+
clawhubCheckUpdates,
|
|
26
|
+
clawhubInspect,
|
|
27
|
+
clawhubInstall,
|
|
28
|
+
clawhubSearch,
|
|
29
|
+
clawhubUpdate,
|
|
30
|
+
} from "../../skills/clawhub.js";
|
|
31
|
+
import {
|
|
32
|
+
createManagedSkill,
|
|
33
|
+
deleteManagedSkill,
|
|
34
|
+
removeSkillsIndexEntry,
|
|
35
|
+
validateManagedSkillId,
|
|
36
|
+
} from "../../skills/managed-store.js";
|
|
37
|
+
import { getWorkspaceSkillsDir } from "../../util/platform.js";
|
|
13
38
|
import type {
|
|
14
39
|
SkillDetailRequest,
|
|
15
40
|
SkillsCheckUpdatesRequest,
|
|
@@ -23,52 +48,65 @@ import type {
|
|
|
23
48
|
SkillsSearchRequest,
|
|
24
49
|
SkillsUninstallRequest,
|
|
25
50
|
SkillsUpdateRequest,
|
|
26
|
-
} from
|
|
27
|
-
import {
|
|
51
|
+
} from "../ipc-protocol.js";
|
|
52
|
+
import {
|
|
53
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
54
|
+
defineHandlers,
|
|
55
|
+
ensureSkillEntry,
|
|
56
|
+
type HandlerContext,
|
|
57
|
+
log,
|
|
58
|
+
} from "./shared.js";
|
|
28
59
|
|
|
29
60
|
// ─── Provenance resolution ──────────────────────────────────────────────────
|
|
30
61
|
|
|
31
62
|
interface SkillProvenance {
|
|
32
|
-
kind:
|
|
63
|
+
kind: "first-party" | "third-party" | "local";
|
|
33
64
|
provider?: string;
|
|
34
65
|
originId?: string;
|
|
35
66
|
sourceUrl?: string;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
|
-
const CLAWHUB_BASE_URL =
|
|
69
|
+
const CLAWHUB_BASE_URL = "https://skills.sh";
|
|
39
70
|
|
|
40
71
|
function resolveProvenance(summary: SkillSummary): SkillProvenance {
|
|
41
72
|
// Bundled skills are always first-party (shipped with Vellum)
|
|
42
|
-
if (summary.source ===
|
|
43
|
-
return { kind:
|
|
73
|
+
if (summary.source === "bundled") {
|
|
74
|
+
return { kind: "first-party", provider: "Vellum" };
|
|
44
75
|
}
|
|
45
76
|
|
|
46
|
-
// Managed skills
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
// Managed skills are third-party (installed from clawhub). The homepage field
|
|
78
|
+
// confirms provenance.
|
|
79
|
+
if (summary.source === "managed") {
|
|
80
|
+
if (
|
|
81
|
+
summary.homepage?.includes("skills.sh") ||
|
|
82
|
+
summary.homepage?.includes("clawhub")
|
|
83
|
+
) {
|
|
51
84
|
return {
|
|
52
|
-
kind:
|
|
53
|
-
provider:
|
|
85
|
+
kind: "third-party",
|
|
86
|
+
provider: "skills.sh",
|
|
54
87
|
originId: summary.id,
|
|
55
|
-
sourceUrl:
|
|
88
|
+
sourceUrl:
|
|
89
|
+
summary.homepage ??
|
|
90
|
+
`${CLAWHUB_BASE_URL}/skills/${encodeURIComponent(summary.id)}`,
|
|
56
91
|
};
|
|
57
92
|
}
|
|
58
|
-
// No positive evidence of origin --
|
|
59
|
-
// Default to "local" to avoid mislabeling
|
|
60
|
-
return { kind:
|
|
93
|
+
// No positive evidence of clawhub origin -- likely user-authored.
|
|
94
|
+
// Default to "local" to avoid mislabeling.
|
|
95
|
+
return { kind: "local" };
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
// Workspace and extra skills are user-provided
|
|
64
|
-
if (summary.source ===
|
|
65
|
-
return { kind:
|
|
99
|
+
if (summary.source === "workspace" || summary.source === "extra") {
|
|
100
|
+
return { kind: "local" };
|
|
66
101
|
}
|
|
67
102
|
|
|
68
|
-
return { kind:
|
|
103
|
+
return { kind: "local" };
|
|
69
104
|
}
|
|
70
105
|
|
|
71
|
-
export function handleSkillsList(
|
|
106
|
+
export function handleSkillsList(
|
|
107
|
+
socket: net.Socket,
|
|
108
|
+
ctx: HandlerContext,
|
|
109
|
+
): void {
|
|
72
110
|
const config = getConfig();
|
|
73
111
|
const catalog = loadSkillCatalog();
|
|
74
112
|
const resolved = resolveSkillStates(catalog, config);
|
|
@@ -80,7 +118,10 @@ export function handleSkillsList(socket: net.Socket, ctx: HandlerContext): void
|
|
|
80
118
|
emoji: r.summary.emoji,
|
|
81
119
|
homepage: r.summary.homepage,
|
|
82
120
|
source: r.summary.source,
|
|
83
|
-
state: (r.state ===
|
|
121
|
+
state: (r.state === "degraded" ? "enabled" : r.state) as
|
|
122
|
+
| "enabled"
|
|
123
|
+
| "disabled"
|
|
124
|
+
| "available",
|
|
84
125
|
degraded: r.degraded,
|
|
85
126
|
missingRequirements: r.missingRequirements,
|
|
86
127
|
updateAvailable: false,
|
|
@@ -88,7 +129,7 @@ export function handleSkillsList(socket: net.Socket, ctx: HandlerContext): void
|
|
|
88
129
|
provenance: resolveProvenance(r.summary),
|
|
89
130
|
}));
|
|
90
131
|
|
|
91
|
-
ctx.send(socket, { type:
|
|
132
|
+
ctx.send(socket, { type: "skills_list_response", skills });
|
|
92
133
|
}
|
|
93
134
|
|
|
94
135
|
export function handleSkillsEnable(
|
|
@@ -109,26 +150,32 @@ export function handleSkillsEnable(
|
|
|
109
150
|
}
|
|
110
151
|
invalidateConfigCache();
|
|
111
152
|
|
|
112
|
-
ctx.debounceTimers.schedule(
|
|
153
|
+
ctx.debounceTimers.schedule(
|
|
154
|
+
"__suppress_reset__",
|
|
155
|
+
() => {
|
|
156
|
+
ctx.setSuppressConfigReload(false);
|
|
157
|
+
},
|
|
158
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
159
|
+
);
|
|
113
160
|
|
|
114
161
|
ctx.updateConfigFingerprint();
|
|
115
162
|
|
|
116
163
|
ctx.send(socket, {
|
|
117
|
-
type:
|
|
118
|
-
operation:
|
|
164
|
+
type: "skills_operation_response",
|
|
165
|
+
operation: "enable",
|
|
119
166
|
success: true,
|
|
120
167
|
});
|
|
121
168
|
ctx.broadcast({
|
|
122
|
-
type:
|
|
169
|
+
type: "skills_state_changed",
|
|
123
170
|
name: msg.name,
|
|
124
|
-
state:
|
|
171
|
+
state: "enabled",
|
|
125
172
|
});
|
|
126
173
|
} catch (err) {
|
|
127
174
|
const message = err instanceof Error ? err.message : String(err);
|
|
128
|
-
log.error({ err },
|
|
175
|
+
log.error({ err }, "Failed to enable skill");
|
|
129
176
|
ctx.send(socket, {
|
|
130
|
-
type:
|
|
131
|
-
operation:
|
|
177
|
+
type: "skills_operation_response",
|
|
178
|
+
operation: "enable",
|
|
132
179
|
success: false,
|
|
133
180
|
error: message,
|
|
134
181
|
});
|
|
@@ -153,26 +200,32 @@ export function handleSkillsDisable(
|
|
|
153
200
|
}
|
|
154
201
|
invalidateConfigCache();
|
|
155
202
|
|
|
156
|
-
ctx.debounceTimers.schedule(
|
|
203
|
+
ctx.debounceTimers.schedule(
|
|
204
|
+
"__suppress_reset__",
|
|
205
|
+
() => {
|
|
206
|
+
ctx.setSuppressConfigReload(false);
|
|
207
|
+
},
|
|
208
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
209
|
+
);
|
|
157
210
|
|
|
158
211
|
ctx.updateConfigFingerprint();
|
|
159
212
|
|
|
160
213
|
ctx.send(socket, {
|
|
161
|
-
type:
|
|
162
|
-
operation:
|
|
214
|
+
type: "skills_operation_response",
|
|
215
|
+
operation: "disable",
|
|
163
216
|
success: true,
|
|
164
217
|
});
|
|
165
218
|
ctx.broadcast({
|
|
166
|
-
type:
|
|
219
|
+
type: "skills_state_changed",
|
|
167
220
|
name: msg.name,
|
|
168
|
-
state:
|
|
221
|
+
state: "disabled",
|
|
169
222
|
});
|
|
170
223
|
} catch (err) {
|
|
171
224
|
const message = err instanceof Error ? err.message : String(err);
|
|
172
|
-
log.error({ err },
|
|
225
|
+
log.error({ err }, "Failed to disable skill");
|
|
173
226
|
ctx.send(socket, {
|
|
174
|
-
type:
|
|
175
|
-
operation:
|
|
227
|
+
type: "skills_operation_response",
|
|
228
|
+
operation: "disable",
|
|
176
229
|
success: false,
|
|
177
230
|
error: message,
|
|
178
231
|
});
|
|
@@ -207,21 +260,27 @@ export function handleSkillsConfigure(
|
|
|
207
260
|
}
|
|
208
261
|
invalidateConfigCache();
|
|
209
262
|
|
|
210
|
-
ctx.debounceTimers.schedule(
|
|
263
|
+
ctx.debounceTimers.schedule(
|
|
264
|
+
"__suppress_reset__",
|
|
265
|
+
() => {
|
|
266
|
+
ctx.setSuppressConfigReload(false);
|
|
267
|
+
},
|
|
268
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
269
|
+
);
|
|
211
270
|
|
|
212
271
|
ctx.updateConfigFingerprint();
|
|
213
272
|
|
|
214
273
|
ctx.send(socket, {
|
|
215
|
-
type:
|
|
216
|
-
operation:
|
|
274
|
+
type: "skills_operation_response",
|
|
275
|
+
operation: "configure",
|
|
217
276
|
success: true,
|
|
218
277
|
});
|
|
219
278
|
} catch (err) {
|
|
220
279
|
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
-
log.error({ err },
|
|
280
|
+
log.error({ err }, "Failed to configure skill");
|
|
222
281
|
ctx.send(socket, {
|
|
223
|
-
type:
|
|
224
|
-
operation:
|
|
282
|
+
type: "skills_operation_response",
|
|
283
|
+
operation: "configure",
|
|
225
284
|
success: false,
|
|
226
285
|
error: message,
|
|
227
286
|
});
|
|
@@ -234,39 +293,33 @@ export async function handleSkillsInstall(
|
|
|
234
293
|
ctx: HandlerContext,
|
|
235
294
|
): Promise<void> {
|
|
236
295
|
try {
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
296
|
+
// Bundled skills are already available — no install needed
|
|
297
|
+
const catalog = loadSkillCatalog();
|
|
298
|
+
const bundled = catalog.find(
|
|
299
|
+
(s) => s.id === msg.slug && s.source === "bundled",
|
|
300
|
+
);
|
|
301
|
+
if (bundled) {
|
|
302
|
+
ctx.send(socket, {
|
|
303
|
+
type: "skills_operation_response",
|
|
304
|
+
operation: "install",
|
|
305
|
+
success: true,
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
241
309
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
skillId = result.skillName ?? msg.slug;
|
|
255
|
-
} else {
|
|
256
|
-
// Install from clawhub (community)
|
|
257
|
-
const result = await clawhubInstall(msg.slug, { version: msg.version });
|
|
258
|
-
if (!result.success) {
|
|
259
|
-
ctx.send(socket, {
|
|
260
|
-
type: 'skills_operation_response',
|
|
261
|
-
operation: 'install',
|
|
262
|
-
success: false,
|
|
263
|
-
error: result.error ?? 'Unknown error',
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const rawId = result.skillName ?? msg.slug;
|
|
268
|
-
skillId = rawId.includes('/') ? rawId.split('/').pop()! : rawId;
|
|
310
|
+
// Install from clawhub (community)
|
|
311
|
+
const result = await clawhubInstall(msg.slug, { version: msg.version });
|
|
312
|
+
if (!result.success) {
|
|
313
|
+
ctx.send(socket, {
|
|
314
|
+
type: "skills_operation_response",
|
|
315
|
+
operation: "install",
|
|
316
|
+
success: false,
|
|
317
|
+
error: result.error ?? "Unknown error",
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
269
320
|
}
|
|
321
|
+
const rawId = result.skillName ?? msg.slug;
|
|
322
|
+
const skillId = rawId.includes("/") ? rawId.split("/").pop()! : rawId;
|
|
270
323
|
|
|
271
324
|
// Reload skill catalog so the newly installed skill is picked up
|
|
272
325
|
loadSkillCatalog();
|
|
@@ -283,28 +336,34 @@ export async function handleSkillsInstall(
|
|
|
283
336
|
throw err;
|
|
284
337
|
}
|
|
285
338
|
invalidateConfigCache();
|
|
286
|
-
ctx.debounceTimers.schedule(
|
|
339
|
+
ctx.debounceTimers.schedule(
|
|
340
|
+
"__suppress_reset__",
|
|
341
|
+
() => {
|
|
342
|
+
ctx.setSuppressConfigReload(false);
|
|
343
|
+
},
|
|
344
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
345
|
+
);
|
|
287
346
|
ctx.updateConfigFingerprint();
|
|
288
347
|
} catch (err) {
|
|
289
|
-
log.warn({ err, skillId },
|
|
348
|
+
log.warn({ err, skillId }, "Failed to auto-enable installed skill");
|
|
290
349
|
}
|
|
291
350
|
|
|
292
351
|
ctx.send(socket, {
|
|
293
|
-
type:
|
|
294
|
-
operation:
|
|
352
|
+
type: "skills_operation_response",
|
|
353
|
+
operation: "install",
|
|
295
354
|
success: true,
|
|
296
355
|
});
|
|
297
356
|
ctx.broadcast({
|
|
298
|
-
type:
|
|
357
|
+
type: "skills_state_changed",
|
|
299
358
|
name: skillId,
|
|
300
|
-
state:
|
|
359
|
+
state: "enabled",
|
|
301
360
|
});
|
|
302
361
|
} catch (err) {
|
|
303
362
|
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
-
log.error({ err },
|
|
363
|
+
log.error({ err }, "Failed to install skill");
|
|
305
364
|
ctx.send(socket, {
|
|
306
|
-
type:
|
|
307
|
-
operation:
|
|
365
|
+
type: "skills_operation_response",
|
|
366
|
+
operation: "install",
|
|
308
367
|
success: false,
|
|
309
368
|
error: message,
|
|
310
369
|
});
|
|
@@ -317,14 +376,19 @@ export async function handleSkillsUninstall(
|
|
|
317
376
|
ctx: HandlerContext,
|
|
318
377
|
): Promise<void> {
|
|
319
378
|
// Validate skill name to prevent path traversal while allowing namespaced slugs (org/name)
|
|
320
|
-
const validNamespacedSlug =
|
|
379
|
+
const validNamespacedSlug =
|
|
380
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
321
381
|
const validSimpleName = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
322
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
msg.name.includes("..") ||
|
|
384
|
+
msg.name.includes("\\") ||
|
|
385
|
+
!(validSimpleName.test(msg.name) || validNamespacedSlug.test(msg.name))
|
|
386
|
+
) {
|
|
323
387
|
ctx.send(socket, {
|
|
324
|
-
type:
|
|
325
|
-
operation:
|
|
388
|
+
type: "skills_operation_response",
|
|
389
|
+
operation: "uninstall",
|
|
326
390
|
success: false,
|
|
327
|
-
error:
|
|
391
|
+
error: "Invalid skill name",
|
|
328
392
|
});
|
|
329
393
|
return;
|
|
330
394
|
}
|
|
@@ -336,10 +400,10 @@ export async function handleSkillsUninstall(
|
|
|
336
400
|
const result = deleteManagedSkill(msg.name);
|
|
337
401
|
if (!result.deleted) {
|
|
338
402
|
ctx.send(socket, {
|
|
339
|
-
type:
|
|
340
|
-
operation:
|
|
403
|
+
type: "skills_operation_response",
|
|
404
|
+
operation: "uninstall",
|
|
341
405
|
success: false,
|
|
342
|
-
error: result.error ??
|
|
406
|
+
error: result.error ?? "Failed to delete managed skill",
|
|
343
407
|
});
|
|
344
408
|
return;
|
|
345
409
|
}
|
|
@@ -348,15 +412,19 @@ export async function handleSkillsUninstall(
|
|
|
348
412
|
const skillDir = join(getWorkspaceSkillsDir(), msg.name);
|
|
349
413
|
if (!existsSync(skillDir)) {
|
|
350
414
|
ctx.send(socket, {
|
|
351
|
-
type:
|
|
352
|
-
operation:
|
|
415
|
+
type: "skills_operation_response",
|
|
416
|
+
operation: "uninstall",
|
|
353
417
|
success: false,
|
|
354
|
-
error:
|
|
418
|
+
error: "Skill not found",
|
|
355
419
|
});
|
|
356
420
|
return;
|
|
357
421
|
}
|
|
358
422
|
rmSync(skillDir, { recursive: true });
|
|
359
|
-
try {
|
|
423
|
+
try {
|
|
424
|
+
removeSkillsIndexEntry(msg.name);
|
|
425
|
+
} catch {
|
|
426
|
+
/* best effort */
|
|
427
|
+
}
|
|
360
428
|
}
|
|
361
429
|
|
|
362
430
|
// Clean config entry
|
|
@@ -375,27 +443,33 @@ export async function handleSkillsUninstall(
|
|
|
375
443
|
}
|
|
376
444
|
invalidateConfigCache();
|
|
377
445
|
|
|
378
|
-
ctx.debounceTimers.schedule(
|
|
446
|
+
ctx.debounceTimers.schedule(
|
|
447
|
+
"__suppress_reset__",
|
|
448
|
+
() => {
|
|
449
|
+
ctx.setSuppressConfigReload(false);
|
|
450
|
+
},
|
|
451
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
452
|
+
);
|
|
379
453
|
|
|
380
454
|
ctx.updateConfigFingerprint();
|
|
381
455
|
}
|
|
382
456
|
|
|
383
457
|
ctx.send(socket, {
|
|
384
|
-
type:
|
|
385
|
-
operation:
|
|
458
|
+
type: "skills_operation_response",
|
|
459
|
+
operation: "uninstall",
|
|
386
460
|
success: true,
|
|
387
461
|
});
|
|
388
462
|
ctx.broadcast({
|
|
389
|
-
type:
|
|
463
|
+
type: "skills_state_changed",
|
|
390
464
|
name: msg.name,
|
|
391
|
-
state:
|
|
465
|
+
state: "uninstalled",
|
|
392
466
|
});
|
|
393
467
|
} catch (err) {
|
|
394
468
|
const message = err instanceof Error ? err.message : String(err);
|
|
395
|
-
log.error({ err },
|
|
469
|
+
log.error({ err }, "Failed to uninstall skill");
|
|
396
470
|
ctx.send(socket, {
|
|
397
|
-
type:
|
|
398
|
-
operation:
|
|
471
|
+
type: "skills_operation_response",
|
|
472
|
+
operation: "uninstall",
|
|
399
473
|
success: false,
|
|
400
474
|
error: message,
|
|
401
475
|
});
|
|
@@ -411,10 +485,10 @@ export async function handleSkillsUpdate(
|
|
|
411
485
|
const result = await clawhubUpdate(msg.name);
|
|
412
486
|
if (!result.success) {
|
|
413
487
|
ctx.send(socket, {
|
|
414
|
-
type:
|
|
415
|
-
operation:
|
|
488
|
+
type: "skills_operation_response",
|
|
489
|
+
operation: "update",
|
|
416
490
|
success: false,
|
|
417
|
-
error: result.error ??
|
|
491
|
+
error: result.error ?? "Unknown error",
|
|
418
492
|
});
|
|
419
493
|
return;
|
|
420
494
|
}
|
|
@@ -423,16 +497,16 @@ export async function handleSkillsUpdate(
|
|
|
423
497
|
loadSkillCatalog();
|
|
424
498
|
|
|
425
499
|
ctx.send(socket, {
|
|
426
|
-
type:
|
|
427
|
-
operation:
|
|
500
|
+
type: "skills_operation_response",
|
|
501
|
+
operation: "update",
|
|
428
502
|
success: true,
|
|
429
503
|
});
|
|
430
504
|
} catch (err) {
|
|
431
505
|
const message = err instanceof Error ? err.message : String(err);
|
|
432
|
-
log.error({ err },
|
|
506
|
+
log.error({ err }, "Failed to update skill");
|
|
433
507
|
ctx.send(socket, {
|
|
434
|
-
type:
|
|
435
|
-
operation:
|
|
508
|
+
type: "skills_operation_response",
|
|
509
|
+
operation: "update",
|
|
436
510
|
success: false,
|
|
437
511
|
error: message,
|
|
438
512
|
});
|
|
@@ -447,17 +521,17 @@ export async function handleSkillsCheckUpdates(
|
|
|
447
521
|
try {
|
|
448
522
|
const updates = await clawhubCheckUpdates();
|
|
449
523
|
ctx.send(socket, {
|
|
450
|
-
type:
|
|
451
|
-
operation:
|
|
524
|
+
type: "skills_operation_response",
|
|
525
|
+
operation: "check_updates",
|
|
452
526
|
success: true,
|
|
453
527
|
data: updates,
|
|
454
528
|
});
|
|
455
529
|
} catch (err) {
|
|
456
530
|
const message = err instanceof Error ? err.message : String(err);
|
|
457
|
-
log.error({ err },
|
|
531
|
+
log.error({ err }, "Failed to check for skill updates");
|
|
458
532
|
ctx.send(socket, {
|
|
459
|
-
type:
|
|
460
|
-
operation:
|
|
533
|
+
type: "skills_operation_response",
|
|
534
|
+
operation: "check_updates",
|
|
461
535
|
success: false,
|
|
462
536
|
error: message,
|
|
463
537
|
});
|
|
@@ -470,43 +544,20 @@ export async function handleSkillsSearch(
|
|
|
470
544
|
ctx: HandlerContext,
|
|
471
545
|
): Promise<void> {
|
|
472
546
|
try {
|
|
473
|
-
|
|
474
|
-
const catalogEntries = await listCatalogEntries();
|
|
475
|
-
const query = (msg.query ?? '').toLowerCase();
|
|
476
|
-
const matchingCatalog = catalogEntries.filter((e) => {
|
|
477
|
-
if (!query) return true;
|
|
478
|
-
return e.name.toLowerCase().includes(query) || e.description.toLowerCase().includes(query) || e.id.toLowerCase().includes(query);
|
|
479
|
-
});
|
|
480
|
-
const vellumSkills: ClawhubSearchResultItem[] = matchingCatalog.map((e) => ({
|
|
481
|
-
name: e.name,
|
|
482
|
-
slug: e.id,
|
|
483
|
-
description: e.description,
|
|
484
|
-
author: 'Vellum',
|
|
485
|
-
stars: 0,
|
|
486
|
-
installs: 0,
|
|
487
|
-
version: '',
|
|
488
|
-
createdAt: 0,
|
|
489
|
-
source: 'vellum' as const,
|
|
490
|
-
}));
|
|
491
|
-
|
|
492
|
-
// Search clawhub concurrently
|
|
493
|
-
const clawhubResult = await clawhubSearch(msg.query);
|
|
494
|
-
|
|
495
|
-
// Merge: vellum first, then clawhub
|
|
496
|
-
const merged = { skills: [...vellumSkills, ...clawhubResult.skills] };
|
|
547
|
+
const result = await clawhubSearch(msg.query);
|
|
497
548
|
|
|
498
549
|
ctx.send(socket, {
|
|
499
|
-
type:
|
|
500
|
-
operation:
|
|
550
|
+
type: "skills_operation_response",
|
|
551
|
+
operation: "search",
|
|
501
552
|
success: true,
|
|
502
|
-
data:
|
|
553
|
+
data: result,
|
|
503
554
|
});
|
|
504
555
|
} catch (err) {
|
|
505
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
506
|
-
log.error({ err },
|
|
557
|
+
log.error({ err }, "Failed to search skills");
|
|
507
558
|
ctx.send(socket, {
|
|
508
|
-
type:
|
|
509
|
-
operation:
|
|
559
|
+
type: "skills_operation_response",
|
|
560
|
+
operation: "search",
|
|
510
561
|
success: false,
|
|
511
562
|
error: message,
|
|
512
563
|
});
|
|
@@ -521,16 +572,16 @@ export async function handleSkillsInspect(
|
|
|
521
572
|
try {
|
|
522
573
|
const result = await clawhubInspect(msg.slug);
|
|
523
574
|
ctx.send(socket, {
|
|
524
|
-
type:
|
|
575
|
+
type: "skills_inspect_response",
|
|
525
576
|
slug: msg.slug,
|
|
526
577
|
...(result.data ? { data: result.data } : {}),
|
|
527
578
|
...(result.error ? { error: result.error } : {}),
|
|
528
579
|
});
|
|
529
580
|
} catch (err) {
|
|
530
581
|
const message = err instanceof Error ? err.message : String(err);
|
|
531
|
-
log.error({ err },
|
|
582
|
+
log.error({ err }, "Failed to inspect skill");
|
|
532
583
|
ctx.send(socket, {
|
|
533
|
-
type:
|
|
584
|
+
type: "skills_inspect_response",
|
|
534
585
|
slug: msg.slug,
|
|
535
586
|
error: message,
|
|
536
587
|
});
|
|
@@ -544,19 +595,23 @@ export async function handleSkillDetail(
|
|
|
544
595
|
): Promise<void> {
|
|
545
596
|
const result = loadSkillBySelector(msg.skillId);
|
|
546
597
|
if (result.skill) {
|
|
547
|
-
const icon = await ensureSkillIcon(
|
|
598
|
+
const icon = await ensureSkillIcon(
|
|
599
|
+
result.skill.directoryPath,
|
|
600
|
+
result.skill.name,
|
|
601
|
+
result.skill.description,
|
|
602
|
+
);
|
|
548
603
|
ctx.send(socket, {
|
|
549
|
-
type:
|
|
604
|
+
type: "skill_detail_response",
|
|
550
605
|
skillId: result.skill.id,
|
|
551
606
|
body: result.skill.body,
|
|
552
607
|
...(icon ? { icon } : {}),
|
|
553
608
|
});
|
|
554
609
|
} else {
|
|
555
610
|
ctx.send(socket, {
|
|
556
|
-
type:
|
|
611
|
+
type: "skill_detail_response",
|
|
557
612
|
skillId: msg.skillId,
|
|
558
|
-
body:
|
|
559
|
-
error: result.error ??
|
|
613
|
+
body: "",
|
|
614
|
+
error: result.error ?? "Skill not found",
|
|
560
615
|
});
|
|
561
616
|
}
|
|
562
617
|
}
|
|
@@ -578,7 +633,7 @@ function parseFrontmatter(sourceText: string): ParsedFrontmatter {
|
|
|
578
633
|
if (!match) return { body: sourceText };
|
|
579
634
|
|
|
580
635
|
const yamlBlock = match[1];
|
|
581
|
-
const body = match[2].replace(/\r\n/g,
|
|
636
|
+
const body = match[2].replace(/\r\n/g, "\n");
|
|
582
637
|
|
|
583
638
|
const result: ParsedFrontmatter = { body };
|
|
584
639
|
|
|
@@ -589,22 +644,25 @@ function parseFrontmatter(sourceText: string): ParsedFrontmatter {
|
|
|
589
644
|
const key = kvMatch[1];
|
|
590
645
|
// Strip surrounding quotes
|
|
591
646
|
let value = kvMatch[2].trim();
|
|
592
|
-
if (
|
|
647
|
+
if (
|
|
648
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
649
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
650
|
+
) {
|
|
593
651
|
value = value.slice(1, -1);
|
|
594
652
|
}
|
|
595
653
|
switch (key) {
|
|
596
|
-
case
|
|
597
|
-
case
|
|
598
|
-
case
|
|
654
|
+
case "skill-id":
|
|
655
|
+
case "skillId":
|
|
656
|
+
case "id":
|
|
599
657
|
result.skillId = value;
|
|
600
658
|
break;
|
|
601
|
-
case
|
|
659
|
+
case "name":
|
|
602
660
|
result.name = value;
|
|
603
661
|
break;
|
|
604
|
-
case
|
|
662
|
+
case "description":
|
|
605
663
|
result.description = value;
|
|
606
664
|
break;
|
|
607
|
-
case
|
|
665
|
+
case "emoji":
|
|
608
666
|
result.emoji = value;
|
|
609
667
|
break;
|
|
610
668
|
}
|
|
@@ -618,22 +676,28 @@ function parseFrontmatter(sourceText: string): ParsedFrontmatter {
|
|
|
618
676
|
function toSkillSlug(raw: string): string {
|
|
619
677
|
return raw
|
|
620
678
|
.toLowerCase()
|
|
621
|
-
.replace(/[^a-z0-9._-]+/g,
|
|
622
|
-
.replace(/^[^a-z0-9]+/,
|
|
623
|
-
.replace(/-+/g,
|
|
679
|
+
.replace(/[^a-z0-9._-]+/g, "-") // replace non-valid chars with hyphens
|
|
680
|
+
.replace(/^[^a-z0-9]+/, "") // must start with alphanumeric
|
|
681
|
+
.replace(/-+/g, "-") // collapse multiple hyphens
|
|
624
682
|
.slice(0, 50)
|
|
625
|
-
.replace(/-$/,
|
|
683
|
+
.replace(/-$/, ""); // no trailing hyphen (after truncation)
|
|
626
684
|
}
|
|
627
685
|
|
|
628
686
|
// ─── Deterministic heuristic draft ───────────────────────────────────────────
|
|
629
687
|
|
|
630
|
-
function heuristicDraft(body: string): {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
688
|
+
function heuristicDraft(body: string): {
|
|
689
|
+
skillId: string;
|
|
690
|
+
name: string;
|
|
691
|
+
description: string;
|
|
692
|
+
emoji: string;
|
|
693
|
+
} {
|
|
694
|
+
const lines = body.split("\n").filter((l) => l.trim());
|
|
695
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
696
|
+
const name =
|
|
697
|
+
firstLine.replace(/^#+\s*/, "").slice(0, 100) || "Untitled Skill";
|
|
698
|
+
const skillId = toSkillSlug(name) || "untitled-skill";
|
|
699
|
+
const description = body.trim().slice(0, 200) || "No description provided";
|
|
700
|
+
return { skillId, name, description, emoji: "\u{1F4DD}" };
|
|
637
701
|
}
|
|
638
702
|
|
|
639
703
|
// ─── Draft handler ───────────────────────────────────────────────────────────
|
|
@@ -654,10 +718,10 @@ export async function handleSkillsDraft(
|
|
|
654
718
|
|
|
655
719
|
// Determine which fields still need filling
|
|
656
720
|
const missing: string[] = [];
|
|
657
|
-
if (!skillId) missing.push(
|
|
658
|
-
if (!name) missing.push(
|
|
659
|
-
if (!description) missing.push(
|
|
660
|
-
if (!emoji) missing.push(
|
|
721
|
+
if (!skillId) missing.push("skillId");
|
|
722
|
+
if (!name) missing.push("name");
|
|
723
|
+
if (!description) missing.push("description");
|
|
724
|
+
if (!emoji) missing.push("emoji");
|
|
661
725
|
|
|
662
726
|
// Attempt LLM generation for missing fields
|
|
663
727
|
if (missing.length > 0) {
|
|
@@ -668,23 +732,26 @@ export async function handleSkillsDraft(
|
|
|
668
732
|
const { signal, cleanup } = createTimeout(LLM_DRAFT_TIMEOUT_MS);
|
|
669
733
|
try {
|
|
670
734
|
const prompt = [
|
|
671
|
-
|
|
672
|
-
`Return ONLY valid JSON with these fields: ${missing.join(
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
735
|
+
"Given the following skill body text, generate metadata for a managed skill.",
|
|
736
|
+
`Return ONLY valid JSON with these fields: ${missing.join(", ")}.`,
|
|
737
|
+
"Field descriptions:",
|
|
738
|
+
"- skillId: a short kebab-case identifier (lowercase, alphanumeric + hyphens/dots/underscores, max 50 chars, must start with a letter or digit)",
|
|
739
|
+
"- name: a human-readable name (max 100 chars)",
|
|
740
|
+
"- description: a brief one-line description (max 200 chars)",
|
|
741
|
+
"- emoji: a single emoji character representing the skill",
|
|
742
|
+
"",
|
|
743
|
+
"Skill body:",
|
|
680
744
|
body.slice(0, 2000),
|
|
681
|
-
].join(
|
|
745
|
+
].join("\n");
|
|
682
746
|
|
|
683
747
|
const response = await provider.sendMessage(
|
|
684
748
|
[userMessage(prompt)],
|
|
685
749
|
[],
|
|
686
750
|
undefined,
|
|
687
|
-
{
|
|
751
|
+
{
|
|
752
|
+
config: { modelIntent: "latency-optimized", max_tokens: 256 },
|
|
753
|
+
signal,
|
|
754
|
+
},
|
|
688
755
|
);
|
|
689
756
|
cleanup();
|
|
690
757
|
|
|
@@ -693,41 +760,62 @@ export async function handleSkillsDraft(
|
|
|
693
760
|
const jsonMatch = /\{[\s\S]*?\}/.exec(responseText);
|
|
694
761
|
if (jsonMatch) {
|
|
695
762
|
const generated = JSON.parse(jsonMatch[0]);
|
|
696
|
-
if (typeof generated ===
|
|
697
|
-
if (!skillId && typeof generated.skillId ===
|
|
698
|
-
|
|
699
|
-
if (!
|
|
700
|
-
|
|
763
|
+
if (typeof generated === "object" && generated) {
|
|
764
|
+
if (!skillId && typeof generated.skillId === "string")
|
|
765
|
+
skillId = generated.skillId;
|
|
766
|
+
if (!name && typeof generated.name === "string")
|
|
767
|
+
name = generated.name;
|
|
768
|
+
if (!description && typeof generated.description === "string")
|
|
769
|
+
description = generated.description;
|
|
770
|
+
if (!emoji && typeof generated.emoji === "string")
|
|
771
|
+
emoji = generated.emoji;
|
|
701
772
|
llmGenerated = true;
|
|
702
773
|
}
|
|
703
774
|
}
|
|
704
775
|
} catch (err) {
|
|
705
776
|
cleanup();
|
|
706
|
-
log.warn(
|
|
707
|
-
|
|
777
|
+
log.warn(
|
|
778
|
+
{ err },
|
|
779
|
+
"LLM draft generation failed, falling back to heuristic",
|
|
780
|
+
);
|
|
781
|
+
warnings.push(
|
|
782
|
+
"LLM draft generation failed, used heuristic fallback",
|
|
783
|
+
);
|
|
708
784
|
}
|
|
709
785
|
} else {
|
|
710
|
-
warnings.push(
|
|
786
|
+
warnings.push("No LLM provider available, used heuristic fallback");
|
|
711
787
|
}
|
|
712
788
|
} catch (err) {
|
|
713
|
-
log.warn({ err },
|
|
714
|
-
warnings.push(
|
|
789
|
+
log.warn({ err }, "Provider resolution failed for draft generation");
|
|
790
|
+
warnings.push("Provider resolution failed, used heuristic fallback");
|
|
715
791
|
}
|
|
716
792
|
|
|
717
793
|
// Fall back to heuristic for any fields still missing
|
|
718
794
|
if (!skillId || !name || !description || !emoji) {
|
|
719
795
|
const heuristic = heuristicDraft(body);
|
|
720
|
-
if (!skillId) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
796
|
+
if (!skillId) {
|
|
797
|
+
skillId = heuristic.skillId;
|
|
798
|
+
if (!llmGenerated) warnings.push("skillId derived from heuristic");
|
|
799
|
+
}
|
|
800
|
+
if (!name) {
|
|
801
|
+
name = heuristic.name;
|
|
802
|
+
if (!llmGenerated) warnings.push("name derived from heuristic");
|
|
803
|
+
}
|
|
804
|
+
if (!description) {
|
|
805
|
+
description = heuristic.description;
|
|
806
|
+
if (!llmGenerated)
|
|
807
|
+
warnings.push("description derived from heuristic");
|
|
808
|
+
}
|
|
809
|
+
if (!emoji) {
|
|
810
|
+
emoji = heuristic.emoji;
|
|
811
|
+
}
|
|
724
812
|
}
|
|
725
813
|
}
|
|
726
814
|
|
|
727
815
|
// Normalize skillId to valid managed-skill slug format
|
|
728
816
|
const originalId = skillId!;
|
|
729
817
|
skillId = toSkillSlug(originalId);
|
|
730
|
-
if (!skillId) skillId =
|
|
818
|
+
if (!skillId) skillId = "untitled-skill";
|
|
731
819
|
if (skillId !== originalId) {
|
|
732
820
|
warnings.push(`skillId normalized from "${originalId}" to "${skillId}"`);
|
|
733
821
|
}
|
|
@@ -735,12 +823,15 @@ export async function handleSkillsDraft(
|
|
|
735
823
|
// Final validation pass
|
|
736
824
|
const validationError = validateManagedSkillId(skillId);
|
|
737
825
|
if (validationError) {
|
|
738
|
-
skillId =
|
|
739
|
-
|
|
826
|
+
skillId =
|
|
827
|
+
toSkillSlug(skillId.replace(/[^a-z0-9]/g, "-")) || "untitled-skill";
|
|
828
|
+
warnings.push(
|
|
829
|
+
`skillId re-normalized due to validation: ${validationError}`,
|
|
830
|
+
);
|
|
740
831
|
}
|
|
741
832
|
|
|
742
833
|
ctx.send(socket, {
|
|
743
|
-
type:
|
|
834
|
+
type: "skills_draft_response",
|
|
744
835
|
success: true,
|
|
745
836
|
draft: {
|
|
746
837
|
skillId: skillId!,
|
|
@@ -753,9 +844,9 @@ export async function handleSkillsDraft(
|
|
|
753
844
|
});
|
|
754
845
|
} catch (err) {
|
|
755
846
|
const message = err instanceof Error ? err.message : String(err);
|
|
756
|
-
log.error({ err },
|
|
847
|
+
log.error({ err }, "Failed to generate skill draft");
|
|
757
848
|
ctx.send(socket, {
|
|
758
|
-
type:
|
|
849
|
+
type: "skills_draft_response",
|
|
759
850
|
success: false,
|
|
760
851
|
error: message,
|
|
761
852
|
});
|
|
@@ -783,10 +874,10 @@ export async function handleSkillsCreate(
|
|
|
783
874
|
|
|
784
875
|
if (!result.created) {
|
|
785
876
|
ctx.send(socket, {
|
|
786
|
-
type:
|
|
787
|
-
operation:
|
|
877
|
+
type: "skills_operation_response",
|
|
878
|
+
operation: "create",
|
|
788
879
|
success: false,
|
|
789
|
-
error: result.error ??
|
|
880
|
+
error: result.error ?? "Failed to create managed skill",
|
|
790
881
|
});
|
|
791
882
|
return;
|
|
792
883
|
}
|
|
@@ -804,31 +895,40 @@ export async function handleSkillsCreate(
|
|
|
804
895
|
throw err;
|
|
805
896
|
}
|
|
806
897
|
invalidateConfigCache();
|
|
807
|
-
ctx.debounceTimers.schedule(
|
|
898
|
+
ctx.debounceTimers.schedule(
|
|
899
|
+
"__suppress_reset__",
|
|
900
|
+
() => {
|
|
901
|
+
ctx.setSuppressConfigReload(false);
|
|
902
|
+
},
|
|
903
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
904
|
+
);
|
|
808
905
|
ctx.updateConfigFingerprint();
|
|
809
906
|
autoEnabled = true;
|
|
810
907
|
} catch (err) {
|
|
811
|
-
log.warn(
|
|
908
|
+
log.warn(
|
|
909
|
+
{ err, skillId: msg.skillId },
|
|
910
|
+
"Failed to auto-enable created skill",
|
|
911
|
+
);
|
|
812
912
|
}
|
|
813
913
|
|
|
814
914
|
ctx.send(socket, {
|
|
815
|
-
type:
|
|
816
|
-
operation:
|
|
915
|
+
type: "skills_operation_response",
|
|
916
|
+
operation: "create",
|
|
817
917
|
success: true,
|
|
818
918
|
});
|
|
819
919
|
if (autoEnabled) {
|
|
820
920
|
ctx.broadcast({
|
|
821
|
-
type:
|
|
921
|
+
type: "skills_state_changed",
|
|
822
922
|
name: msg.skillId,
|
|
823
|
-
state:
|
|
923
|
+
state: "enabled",
|
|
824
924
|
});
|
|
825
925
|
}
|
|
826
926
|
} catch (err) {
|
|
827
927
|
const message = err instanceof Error ? err.message : String(err);
|
|
828
|
-
log.error({ err },
|
|
928
|
+
log.error({ err }, "Failed to create skill");
|
|
829
929
|
ctx.send(socket, {
|
|
830
|
-
type:
|
|
831
|
-
operation:
|
|
930
|
+
type: "skills_operation_response",
|
|
931
|
+
operation: "create",
|
|
832
932
|
success: false,
|
|
833
933
|
error: message,
|
|
834
934
|
});
|