@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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -1,15 +1,40 @@
1
- import { existsSync, rmSync } from 'node:fs';
2
- import * as net from 'node:net';
3
- import { join } from 'node:path';
4
-
5
- import { getConfig, invalidateConfigCache,loadRawConfig, saveRawConfig } from '../../config/loader.js';
6
- import { resolveSkillStates } from '../../config/skill-state.js';
7
- import { ensureSkillIcon,loadSkillBySelector, loadSkillCatalog, type SkillSummary } from '../../config/skills.js';
8
- import { createTimeout,extractText, getConfiguredProvider, userMessage } from '../../providers/provider-send-message.js';
9
- import { clawhubCheckUpdates, clawhubInspect, clawhubInstall, clawhubSearch, type ClawhubSearchResultItem,clawhubUpdate } from '../../skills/clawhub.js';
10
- import { createManagedSkill,deleteManagedSkill, removeSkillsIndexEntry, validateManagedSkillId } from '../../skills/managed-store.js';
11
- import { checkVellumSkill,installFromVellumCatalog, listCatalogEntries } from '../../tools/skills/vellum-catalog.js';
12
- import { getWorkspaceSkillsDir } from '../../util/platform.js';
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 '../ipc-protocol.js';
27
- import { CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, ensureSkillEntry, type HandlerContext,log } from './shared.js';
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: 'first-party' | 'third-party' | 'local';
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 = 'https://skills.sh';
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 === 'bundled') {
43
- return { kind: 'first-party', provider: 'Vellum' };
73
+ if (summary.source === "bundled") {
74
+ return { kind: "first-party", provider: "Vellum" };
44
75
  }
45
76
 
46
- // Managed skills could be either first-party (installed from Vellum catalog)
47
- // or third-party (installed from clawhub). The homepage field serves as a
48
- // heuristic: Vellum catalog skills don't typically have a clawhub homepage.
49
- if (summary.source === 'managed') {
50
- if (summary.homepage?.includes('skills.sh') || summary.homepage?.includes('clawhub')) {
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: 'third-party',
53
- provider: 'skills.sh',
85
+ kind: "third-party",
86
+ provider: "skills.sh",
54
87
  originId: summary.id,
55
- sourceUrl: summary.homepage ?? `${CLAWHUB_BASE_URL}/skills/${encodeURIComponent(summary.id)}`,
88
+ sourceUrl:
89
+ summary.homepage ??
90
+ `${CLAWHUB_BASE_URL}/skills/${encodeURIComponent(summary.id)}`,
56
91
  };
57
92
  }
58
- // No positive evidence of origin -- could be user-authored or from Vellum catalog.
59
- // Default to "local" to avoid mislabeling user-created skills as first-party.
60
- return { kind: 'local' };
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 === 'workspace' || summary.source === 'extra') {
65
- return { kind: 'local' };
99
+ if (summary.source === "workspace" || summary.source === "extra") {
100
+ return { kind: "local" };
66
101
  }
67
102
 
68
- return { kind: 'local' };
103
+ return { kind: "local" };
69
104
  }
70
105
 
71
- export function handleSkillsList(socket: net.Socket, ctx: HandlerContext): void {
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 === 'degraded' ? 'enabled' : r.state) as 'enabled' | 'disabled' | 'available',
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: 'skills_list_response', skills });
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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: 'skills_operation_response',
118
- operation: 'enable',
164
+ type: "skills_operation_response",
165
+ operation: "enable",
119
166
  success: true,
120
167
  });
121
168
  ctx.broadcast({
122
- type: 'skills_state_changed',
169
+ type: "skills_state_changed",
123
170
  name: msg.name,
124
- state: 'enabled',
171
+ state: "enabled",
125
172
  });
126
173
  } catch (err) {
127
174
  const message = err instanceof Error ? err.message : String(err);
128
- log.error({ err }, 'Failed to enable skill');
175
+ log.error({ err }, "Failed to enable skill");
129
176
  ctx.send(socket, {
130
- type: 'skills_operation_response',
131
- operation: 'enable',
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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: 'skills_operation_response',
162
- operation: 'disable',
214
+ type: "skills_operation_response",
215
+ operation: "disable",
163
216
  success: true,
164
217
  });
165
218
  ctx.broadcast({
166
- type: 'skills_state_changed',
219
+ type: "skills_state_changed",
167
220
  name: msg.name,
168
- state: 'disabled',
221
+ state: "disabled",
169
222
  });
170
223
  } catch (err) {
171
224
  const message = err instanceof Error ? err.message : String(err);
172
- log.error({ err }, 'Failed to disable skill');
225
+ log.error({ err }, "Failed to disable skill");
173
226
  ctx.send(socket, {
174
- type: 'skills_operation_response',
175
- operation: 'disable',
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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: 'skills_operation_response',
216
- operation: 'configure',
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 }, 'Failed to configure skill');
280
+ log.error({ err }, "Failed to configure skill");
222
281
  ctx.send(socket, {
223
- type: 'skills_operation_response',
224
- operation: 'configure',
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
- // Check if the slug matches a vellum-skills catalog entry first
238
- const isVellumSkill = await checkVellumSkill(msg.slug);
239
-
240
- let skillId: string;
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
- if (isVellumSkill) {
243
- // Install from vellum-skills catalog (remote with bundled fallback)
244
- const result = await installFromVellumCatalog(msg.slug);
245
- if (!result.success) {
246
- ctx.send(socket, {
247
- type: 'skills_operation_response',
248
- operation: 'install',
249
- success: false,
250
- error: result.error ?? 'Unknown error',
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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 }, 'Failed to auto-enable installed skill');
348
+ log.warn({ err, skillId }, "Failed to auto-enable installed skill");
290
349
  }
291
350
 
292
351
  ctx.send(socket, {
293
- type: 'skills_operation_response',
294
- operation: 'install',
352
+ type: "skills_operation_response",
353
+ operation: "install",
295
354
  success: true,
296
355
  });
297
356
  ctx.broadcast({
298
- type: 'skills_state_changed',
357
+ type: "skills_state_changed",
299
358
  name: skillId,
300
- state: 'enabled',
359
+ state: "enabled",
301
360
  });
302
361
  } catch (err) {
303
362
  const message = err instanceof Error ? err.message : String(err);
304
- log.error({ err }, 'Failed to install skill');
363
+ log.error({ err }, "Failed to install skill");
305
364
  ctx.send(socket, {
306
- type: 'skills_operation_response',
307
- operation: 'install',
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 = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
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 (msg.name.includes('..') || msg.name.includes('\\') || !(validSimpleName.test(msg.name) || validNamespacedSlug.test(msg.name))) {
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: 'skills_operation_response',
325
- operation: 'uninstall',
388
+ type: "skills_operation_response",
389
+ operation: "uninstall",
326
390
  success: false,
327
- error: 'Invalid skill name',
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: 'skills_operation_response',
340
- operation: 'uninstall',
403
+ type: "skills_operation_response",
404
+ operation: "uninstall",
341
405
  success: false,
342
- error: result.error ?? 'Failed to delete managed skill',
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: 'skills_operation_response',
352
- operation: 'uninstall',
415
+ type: "skills_operation_response",
416
+ operation: "uninstall",
353
417
  success: false,
354
- error: 'Skill not found',
418
+ error: "Skill not found",
355
419
  });
356
420
  return;
357
421
  }
358
422
  rmSync(skillDir, { recursive: true });
359
- try { removeSkillsIndexEntry(msg.name); } catch { /* best effort */ }
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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: 'skills_operation_response',
385
- operation: 'uninstall',
458
+ type: "skills_operation_response",
459
+ operation: "uninstall",
386
460
  success: true,
387
461
  });
388
462
  ctx.broadcast({
389
- type: 'skills_state_changed',
463
+ type: "skills_state_changed",
390
464
  name: msg.name,
391
- state: 'uninstalled',
465
+ state: "uninstalled",
392
466
  });
393
467
  } catch (err) {
394
468
  const message = err instanceof Error ? err.message : String(err);
395
- log.error({ err }, 'Failed to uninstall skill');
469
+ log.error({ err }, "Failed to uninstall skill");
396
470
  ctx.send(socket, {
397
- type: 'skills_operation_response',
398
- operation: 'uninstall',
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: 'skills_operation_response',
415
- operation: 'update',
488
+ type: "skills_operation_response",
489
+ operation: "update",
416
490
  success: false,
417
- error: result.error ?? 'Unknown 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: 'skills_operation_response',
427
- operation: 'update',
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 }, 'Failed to update skill');
506
+ log.error({ err }, "Failed to update skill");
433
507
  ctx.send(socket, {
434
- type: 'skills_operation_response',
435
- operation: 'update',
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: 'skills_operation_response',
451
- operation: 'check_updates',
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 }, 'Failed to check for skill updates');
531
+ log.error({ err }, "Failed to check for skill updates");
458
532
  ctx.send(socket, {
459
- type: 'skills_operation_response',
460
- operation: 'check_updates',
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
- // Search vellum-skills catalog (platform API with bundled fallback)
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: 'skills_operation_response',
500
- operation: 'search',
550
+ type: "skills_operation_response",
551
+ operation: "search",
501
552
  success: true,
502
- data: merged,
553
+ data: result,
503
554
  });
504
555
  } catch (err) {
505
556
  const message = err instanceof Error ? err.message : String(err);
506
- log.error({ err }, 'Failed to search skills');
557
+ log.error({ err }, "Failed to search skills");
507
558
  ctx.send(socket, {
508
- type: 'skills_operation_response',
509
- operation: 'search',
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: 'skills_inspect_response',
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 }, 'Failed to inspect skill');
582
+ log.error({ err }, "Failed to inspect skill");
532
583
  ctx.send(socket, {
533
- type: 'skills_inspect_response',
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(result.skill.directoryPath, result.skill.name, result.skill.description);
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: 'skill_detail_response',
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: 'skill_detail_response',
611
+ type: "skill_detail_response",
557
612
  skillId: msg.skillId,
558
- body: '',
559
- error: result.error ?? 'Skill not found',
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, '\n');
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 ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
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 'skill-id':
597
- case 'skillId':
598
- case 'id':
654
+ case "skill-id":
655
+ case "skillId":
656
+ case "id":
599
657
  result.skillId = value;
600
658
  break;
601
- case 'name':
659
+ case "name":
602
660
  result.name = value;
603
661
  break;
604
- case 'description':
662
+ case "description":
605
663
  result.description = value;
606
664
  break;
607
- case 'emoji':
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, '-') // replace non-valid chars with hyphens
622
- .replace(/^[^a-z0-9]+/, '') // must start with alphanumeric
623
- .replace(/-+/g, '-') // collapse multiple hyphens
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(/-$/, ''); // no trailing hyphen (after truncation)
683
+ .replace(/-$/, ""); // no trailing hyphen (after truncation)
626
684
  }
627
685
 
628
686
  // ─── Deterministic heuristic draft ───────────────────────────────────────────
629
687
 
630
- function heuristicDraft(body: string): { skillId: string; name: string; description: string; emoji: string } {
631
- const lines = body.split('\n').filter((l) => l.trim());
632
- const firstLine = lines[0]?.trim() ?? '';
633
- const name = firstLine.replace(/^#+\s*/, '').slice(0, 100) || 'Untitled Skill';
634
- const skillId = toSkillSlug(name) || 'untitled-skill';
635
- const description = body.trim().slice(0, 200) || 'No description provided';
636
- return { skillId, name, description, emoji: '\u{1F4DD}' };
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('skillId');
658
- if (!name) missing.push('name');
659
- if (!description) missing.push('description');
660
- if (!emoji) missing.push('emoji');
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
- 'Given the following skill body text, generate metadata for a managed skill.',
672
- `Return ONLY valid JSON with these fields: ${missing.join(', ')}.`,
673
- 'Field descriptions:',
674
- '- skillId: a short kebab-case identifier (lowercase, alphanumeric + hyphens/dots/underscores, max 50 chars, must start with a letter or digit)',
675
- '- name: a human-readable name (max 100 chars)',
676
- '- description: a brief one-line description (max 200 chars)',
677
- '- emoji: a single emoji character representing the skill',
678
- '',
679
- 'Skill body:',
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('\n');
745
+ ].join("\n");
682
746
 
683
747
  const response = await provider.sendMessage(
684
748
  [userMessage(prompt)],
685
749
  [],
686
750
  undefined,
687
- { config: { modelIntent: 'latency-optimized', max_tokens: 256 }, signal },
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 === 'object' && generated) {
697
- if (!skillId && typeof generated.skillId === 'string') skillId = generated.skillId;
698
- if (!name && typeof generated.name === 'string') name = generated.name;
699
- if (!description && typeof generated.description === 'string') description = generated.description;
700
- if (!emoji && typeof generated.emoji === 'string') emoji = generated.emoji;
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({ err }, 'LLM draft generation failed, falling back to heuristic');
707
- warnings.push('LLM draft generation failed, used heuristic fallback');
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('No LLM provider available, used heuristic fallback');
786
+ warnings.push("No LLM provider available, used heuristic fallback");
711
787
  }
712
788
  } catch (err) {
713
- log.warn({ err }, 'Provider resolution failed for draft generation');
714
- warnings.push('Provider resolution failed, used heuristic fallback');
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) { skillId = heuristic.skillId; if (!llmGenerated) warnings.push('skillId derived from heuristic'); }
721
- if (!name) { name = heuristic.name; if (!llmGenerated) warnings.push('name derived from heuristic'); }
722
- if (!description) { description = heuristic.description; if (!llmGenerated) warnings.push('description derived from heuristic'); }
723
- if (!emoji) { emoji = heuristic.emoji; }
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 = 'untitled-skill';
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 = toSkillSlug(skillId.replace(/[^a-z0-9]/g, '-')) || 'untitled-skill';
739
- warnings.push(`skillId re-normalized due to validation: ${validationError}`);
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: 'skills_draft_response',
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 }, 'Failed to generate skill draft');
847
+ log.error({ err }, "Failed to generate skill draft");
757
848
  ctx.send(socket, {
758
- type: 'skills_draft_response',
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: 'skills_operation_response',
787
- operation: 'create',
877
+ type: "skills_operation_response",
878
+ operation: "create",
788
879
  success: false,
789
- error: result.error ?? 'Failed to create managed skill',
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('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
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({ err, skillId: msg.skillId }, 'Failed to auto-enable created skill');
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: 'skills_operation_response',
816
- operation: 'create',
915
+ type: "skills_operation_response",
916
+ operation: "create",
817
917
  success: true,
818
918
  });
819
919
  if (autoEnabled) {
820
920
  ctx.broadcast({
821
- type: 'skills_state_changed',
921
+ type: "skills_state_changed",
822
922
  name: msg.skillId,
823
- state: 'enabled',
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 }, 'Failed to create skill');
928
+ log.error({ err }, "Failed to create skill");
829
929
  ctx.send(socket, {
830
- type: 'skills_operation_response',
831
- operation: 'create',
930
+ type: "skills_operation_response",
931
+ operation: "create",
832
932
  success: false,
833
933
  error: message,
834
934
  });