@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,58 +1,72 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- import { afterEach, beforeEach, describe, expect, mock,test } from 'bun:test';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ symlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
6
13
 
7
14
  const TEST_DIR = join(tmpdir(), `vellum-skills-test-${crypto.randomUUID()}`);
8
15
 
9
- mock.module('../util/platform.js', () => ({
16
+ mock.module("../util/platform.js", () => ({
10
17
  getRootDir: () => TEST_DIR,
11
18
  getDataDir: () => TEST_DIR,
12
19
  ensureDataDir: () => {},
13
- getSocketPath: () => join(TEST_DIR, 'vellum.sock'),
14
- getPidPath: () => join(TEST_DIR, 'vellum.pid'),
15
- getDbPath: () => join(TEST_DIR, 'data', 'assistant.db'),
16
- getLogPath: () => join(TEST_DIR, 'logs', 'vellum.log'),
17
- isMacOS: () => process.platform === 'darwin',
18
- isLinux: () => process.platform === 'linux',
19
- isWindows: () => process.platform === 'win32',
20
+ getSocketPath: () => join(TEST_DIR, "vellum.sock"),
21
+ getPidPath: () => join(TEST_DIR, "vellum.pid"),
22
+ getDbPath: () => join(TEST_DIR, "data", "assistant.db"),
23
+ getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
24
+ isMacOS: () => process.platform === "darwin",
25
+ isLinux: () => process.platform === "linux",
26
+ isWindows: () => process.platform === "win32",
20
27
  getPlatformName: () => process.platform,
21
- getWorkspaceConfigPath: () => join(TEST_DIR, 'config.json'),
22
- getWorkspaceSkillsDir: () => join(TEST_DIR, 'skills'),
28
+ getWorkspaceConfigPath: () => join(TEST_DIR, "config.json"),
29
+ getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
23
30
  getWorkspaceDir: () => TEST_DIR,
24
31
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
25
32
  migrateToDataLayout: () => {},
26
33
  migrateToWorkspaceLayout: () => {},
27
34
  }));
28
35
 
29
- mock.module('../util/logger.js', () => ({
30
- getLogger: () => new Proxy({} as Record<string, unknown>, {
31
- get: () => () => {},
32
- }),
36
+ mock.module("../util/logger.js", () => ({
37
+ getLogger: () =>
38
+ new Proxy({} as Record<string, unknown>, {
39
+ get: () => () => {},
40
+ }),
33
41
  isDebug: () => false,
34
42
  truncateForLog: (v: string) => v,
35
43
  }));
36
44
 
37
- const { loadSkillCatalog, loadSkillBySelector, resolveSkillSelector } = await import('../config/skills.js');
45
+ const { loadSkillCatalog, loadSkillBySelector, resolveSkillSelector } =
46
+ await import("../config/skills.js");
38
47
 
39
48
  /** Return only user-installed skills (filters out bundled skills that ship with the source tree). */
40
49
  function loadUserSkillCatalog() {
41
50
  return loadSkillCatalog().filter((s) => !s.bundled);
42
51
  }
43
52
 
44
- function writeSkill(skillId: string, name: string, description: string, body: string = 'Skill body'): void {
45
- const skillDir = join(TEST_DIR, 'skills', skillId);
53
+ function writeSkill(
54
+ skillId: string,
55
+ name: string,
56
+ description: string,
57
+ body: string = "Skill body",
58
+ ): void {
59
+ const skillDir = join(TEST_DIR, "skills", skillId);
46
60
  mkdirSync(skillDir, { recursive: true });
47
61
  writeFileSync(
48
- join(skillDir, 'SKILL.md'),
62
+ join(skillDir, "SKILL.md"),
49
63
  `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${body}\n`,
50
64
  );
51
65
  }
52
66
 
53
- describe('skills catalog loading', () => {
67
+ describe("skills catalog loading", () => {
54
68
  beforeEach(() => {
55
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
69
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
56
70
  });
57
71
 
58
72
  afterEach(() => {
@@ -61,122 +75,127 @@ describe('skills catalog loading', () => {
61
75
  }
62
76
  });
63
77
 
64
- test('parses markdown list path entries from SKILLS.md', () => {
65
- writeSkill('alpha', 'Alpha Skill', 'First skill');
66
- writeSkill('beta', 'Beta Skill', 'Second skill');
78
+ test("parses markdown list path entries from SKILLS.md", () => {
79
+ writeSkill("alpha", "Alpha Skill", "First skill");
80
+ writeSkill("beta", "Beta Skill", "Second skill");
67
81
  writeFileSync(
68
- join(TEST_DIR, 'skills', 'SKILLS.md'),
69
- '- alpha\n- beta/SKILL.md\n',
82
+ join(TEST_DIR, "skills", "SKILLS.md"),
83
+ "- alpha\n- beta/SKILL.md\n",
70
84
  );
71
85
 
72
86
  const catalog = loadUserSkillCatalog();
73
- expect(catalog.map((skill) => skill.id)).toEqual(['alpha', 'beta']);
87
+ expect(catalog.map((skill) => skill.id)).toEqual(["alpha", "beta"]);
74
88
  });
75
89
 
76
- test('resolves markdown links from SKILLS.md', () => {
77
- writeSkill('lint', 'Lint Skill', 'Runs lint checks');
78
- writeSkill('test', 'Test Skill', 'Runs test checks');
90
+ test("resolves markdown links from SKILLS.md", () => {
91
+ writeSkill("lint", "Lint Skill", "Runs lint checks");
92
+ writeSkill("test", "Test Skill", "Runs test checks");
79
93
  writeFileSync(
80
- join(TEST_DIR, 'skills', 'SKILLS.md'),
81
- '- [Lint](lint)\n- [Tests](test)\n',
94
+ join(TEST_DIR, "skills", "SKILLS.md"),
95
+ "- [Lint](lint)\n- [Tests](test)\n",
82
96
  );
83
97
 
84
98
  const catalog = loadUserSkillCatalog();
85
- expect(catalog.map((skill) => skill.id)).toEqual(['lint', 'test']);
99
+ expect(catalog.map((skill) => skill.id)).toEqual(["lint", "test"]);
86
100
  });
87
101
 
88
- test('rejects SKILLS.md entries that resolve outside ~/.vellum/workspace/skills', () => {
89
- writeSkill('safe', 'Safe Skill', 'Safe skill');
102
+ test("rejects SKILLS.md entries that resolve outside ~/.vellum/workspace/skills", () => {
103
+ writeSkill("safe", "Safe Skill", "Safe skill");
90
104
  writeFileSync(
91
- join(TEST_DIR, 'skills', 'SKILLS.md'),
92
- '- ../escape\n- /tmp/absolute\n- safe\n',
105
+ join(TEST_DIR, "skills", "SKILLS.md"),
106
+ "- ../escape\n- /tmp/absolute\n- safe\n",
93
107
  );
94
108
 
95
109
  const catalog = loadUserSkillCatalog();
96
- expect(catalog.map((skill) => skill.id)).toEqual(['safe']);
110
+ expect(catalog.map((skill) => skill.id)).toEqual(["safe"]);
97
111
  });
98
112
 
99
- test('rejects symlinked SKILLS.md entries that point outside ~/.vellum/workspace/skills', () => {
100
- const externalSkillDir = join(TEST_DIR, 'outside', 'external-skill');
113
+ test("rejects symlinked SKILLS.md entries that point outside ~/.vellum/workspace/skills", () => {
114
+ const externalSkillDir = join(TEST_DIR, "outside", "external-skill");
101
115
  mkdirSync(externalSkillDir, { recursive: true });
102
116
  writeFileSync(
103
- join(externalSkillDir, 'SKILL.md'),
117
+ join(externalSkillDir, "SKILL.md"),
104
118
  '---\nname: "External Skill"\ndescription: "Outside skills root."\n---\n\nDo not load.\n',
105
119
  );
106
120
 
107
- symlinkSync(externalSkillDir, join(TEST_DIR, 'skills', 'linked-skill'));
108
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- linked-skill\n');
121
+ symlinkSync(externalSkillDir, join(TEST_DIR, "skills", "linked-skill"));
122
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- linked-skill\n");
109
123
 
110
124
  const catalog = loadUserSkillCatalog();
111
125
  expect(catalog).toHaveLength(0);
112
126
  });
113
127
 
114
- test('rejects symlinked SKILL.md files that point outside ~/.vellum/workspace/skills', () => {
115
- const linkedSkillDir = join(TEST_DIR, 'skills', 'linked-file-skill');
128
+ test("rejects symlinked SKILL.md files that point outside ~/.vellum/workspace/skills", () => {
129
+ const linkedSkillDir = join(TEST_DIR, "skills", "linked-file-skill");
116
130
  mkdirSync(linkedSkillDir, { recursive: true });
117
131
 
118
- const outsideDir = join(TEST_DIR, 'outside');
132
+ const outsideDir = join(TEST_DIR, "outside");
119
133
  mkdirSync(outsideDir, { recursive: true });
120
- const externalSkillFile = join(outsideDir, 'external-skill.md');
134
+ const externalSkillFile = join(outsideDir, "external-skill.md");
121
135
  writeFileSync(
122
136
  externalSkillFile,
123
137
  '---\nname: "External File Skill"\ndescription: "Outside skills root."\n---\n\nDo not load.\n',
124
138
  );
125
139
 
126
- symlinkSync(externalSkillFile, join(linkedSkillDir, 'SKILL.md'));
127
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- linked-file-skill\n');
140
+ symlinkSync(externalSkillFile, join(linkedSkillDir, "SKILL.md"));
141
+ writeFileSync(
142
+ join(TEST_DIR, "skills", "SKILLS.md"),
143
+ "- linked-file-skill\n",
144
+ );
128
145
 
129
146
  const catalog = loadUserSkillCatalog();
130
147
  expect(catalog).toHaveLength(0);
131
148
  });
132
149
 
133
- test('uses SKILLS.md ordering when index exists', () => {
134
- writeSkill('first', 'First Skill', 'First');
135
- writeSkill('second', 'Second Skill', 'Second');
136
- writeFileSync(
137
- join(TEST_DIR, 'skills', 'SKILLS.md'),
138
- '- second\n- first\n',
139
- );
150
+ test("uses SKILLS.md ordering when index exists", () => {
151
+ writeSkill("first", "First Skill", "First");
152
+ writeSkill("second", "Second Skill", "Second");
153
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- second\n- first\n");
140
154
 
141
155
  const catalog = loadUserSkillCatalog();
142
- expect(catalog.map((skill) => skill.id)).toEqual(['second', 'first']);
156
+ expect(catalog.map((skill) => skill.id)).toEqual(["second", "first"]);
143
157
  });
144
158
 
145
- test('falls back to auto-discovery when SKILLS.md is missing', () => {
146
- writeSkill('zeta', 'Zeta Skill', 'Zeta');
147
- writeSkill('alpha', 'Alpha Skill', 'Alpha');
159
+ test("falls back to auto-discovery when SKILLS.md is missing", () => {
160
+ writeSkill("zeta", "Zeta Skill", "Zeta");
161
+ writeSkill("alpha", "Alpha Skill", "Alpha");
148
162
 
149
163
  const catalog = loadUserSkillCatalog();
150
- expect(catalog.map((skill) => skill.id)).toEqual(['alpha', 'zeta']);
164
+ expect(catalog.map((skill) => skill.id)).toEqual(["alpha", "zeta"]);
151
165
  });
152
166
 
153
- test('treats SKILLS.md as authoritative when present', () => {
154
- writeSkill('available', 'Available Skill', 'Present on disk');
155
- writeFileSync(
156
- join(TEST_DIR, 'skills', 'SKILLS.md'),
157
- '- ../invalid-only\n',
158
- );
167
+ test("treats SKILLS.md as authoritative when present", () => {
168
+ writeSkill("available", "Available Skill", "Present on disk");
169
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- ../invalid-only\n");
159
170
 
160
171
  const catalog = loadUserSkillCatalog();
161
172
  expect(catalog).toHaveLength(0);
162
173
  });
163
174
  });
164
175
 
165
- describe('workspace skills', () => {
166
- const WORKSPACE_DIR = join(tmpdir(), `vellum-workspace-test-${crypto.randomUUID()}`);
167
- const workspaceSkillsDir = join(WORKSPACE_DIR, '.vellum', 'skills');
168
-
169
- function writeWorkspaceSkill(skillId: string, name: string, description: string, body: string = 'Workspace skill body'): void {
176
+ describe("workspace skills", () => {
177
+ const WORKSPACE_DIR = join(
178
+ tmpdir(),
179
+ `vellum-workspace-test-${crypto.randomUUID()}`,
180
+ );
181
+ const workspaceSkillsDir = join(WORKSPACE_DIR, ".vellum", "skills");
182
+
183
+ function writeWorkspaceSkill(
184
+ skillId: string,
185
+ name: string,
186
+ description: string,
187
+ body: string = "Workspace skill body",
188
+ ): void {
170
189
  const skillDir = join(workspaceSkillsDir, skillId);
171
190
  mkdirSync(skillDir, { recursive: true });
172
191
  writeFileSync(
173
- join(skillDir, 'SKILL.md'),
192
+ join(skillDir, "SKILL.md"),
174
193
  `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${body}\n`,
175
194
  );
176
195
  }
177
196
 
178
197
  beforeEach(() => {
179
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
198
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
180
199
  mkdirSync(workspaceSkillsDir, { recursive: true });
181
200
  });
182
201
 
@@ -189,47 +208,56 @@ describe('workspace skills', () => {
189
208
  }
190
209
  });
191
210
 
192
- test('workspace skills appear in catalog when workspaceSkillsDir is provided', () => {
193
- writeWorkspaceSkill('ws-skill', 'Workspace Skill', 'A workspace skill');
211
+ test("workspace skills appear in catalog when workspaceSkillsDir is provided", () => {
212
+ writeWorkspaceSkill("ws-skill", "Workspace Skill", "A workspace skill");
194
213
 
195
214
  const catalog = loadSkillCatalog(workspaceSkillsDir);
196
- const wsSkills = catalog.filter((s) => s.source === 'workspace');
215
+ const wsSkills = catalog.filter((s) => s.source === "workspace");
197
216
  expect(wsSkills).toHaveLength(1);
198
- expect(wsSkills[0].id).toBe('ws-skill');
217
+ expect(wsSkills[0].id).toBe("ws-skill");
199
218
  });
200
219
 
201
- test('resolveSkillSelector finds workspace skills when workspaceSkillsDir is provided', () => {
202
- writeWorkspaceSkill('ws-resolve', 'Workspace Resolve', 'Resolvable workspace skill');
220
+ test("resolveSkillSelector finds workspace skills when workspaceSkillsDir is provided", () => {
221
+ writeWorkspaceSkill(
222
+ "ws-resolve",
223
+ "Workspace Resolve",
224
+ "Resolvable workspace skill",
225
+ );
203
226
 
204
- const result = resolveSkillSelector('ws-resolve', workspaceSkillsDir);
227
+ const result = resolveSkillSelector("ws-resolve", workspaceSkillsDir);
205
228
  expect(result.skill).toBeDefined();
206
- expect(result.skill!.id).toBe('ws-resolve');
207
- expect(result.skill!.source).toBe('workspace');
229
+ expect(result.skill!.id).toBe("ws-resolve");
230
+ expect(result.skill!.source).toBe("workspace");
208
231
  });
209
232
 
210
- test('resolveSkillSelector does not find workspace skills without workspaceSkillsDir', () => {
211
- writeWorkspaceSkill('ws-hidden', 'Hidden Workspace', 'Should not be found');
233
+ test("resolveSkillSelector does not find workspace skills without workspaceSkillsDir", () => {
234
+ writeWorkspaceSkill("ws-hidden", "Hidden Workspace", "Should not be found");
212
235
 
213
- const result = resolveSkillSelector('ws-hidden');
236
+ const result = resolveSkillSelector("ws-hidden");
214
237
  expect(result.skill).toBeUndefined();
215
238
  expect(result.error).toBeDefined();
216
239
  });
217
240
 
218
- test('loadSkillBySelector loads workspace skill body without isOutsideSkillsRoot rejection', () => {
219
- writeWorkspaceSkill('ws-load', 'Loadable Workspace', 'Can be loaded', 'Full workspace body here');
241
+ test("loadSkillBySelector loads workspace skill body without isOutsideSkillsRoot rejection", () => {
242
+ writeWorkspaceSkill(
243
+ "ws-load",
244
+ "Loadable Workspace",
245
+ "Can be loaded",
246
+ "Full workspace body here",
247
+ );
220
248
 
221
- const result = loadSkillBySelector('ws-load', workspaceSkillsDir);
249
+ const result = loadSkillBySelector("ws-load", workspaceSkillsDir);
222
250
  expect(result.error).toBeUndefined();
223
251
  expect(result.skill).toBeDefined();
224
- expect(result.skill!.id).toBe('ws-load');
225
- expect(result.skill!.body).toBe('Full workspace body here');
226
- expect(result.skill!.source).toBe('workspace');
252
+ expect(result.skill!.id).toBe("ws-load");
253
+ expect(result.skill!.body).toBe("Full workspace body here");
254
+ expect(result.skill!.source).toBe("workspace");
227
255
  });
228
256
  });
229
257
 
230
- describe('tool manifest detection', () => {
258
+ describe("tool manifest detection", () => {
231
259
  beforeEach(() => {
232
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
260
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
233
261
  });
234
262
 
235
263
  afterEach(() => {
@@ -238,57 +266,57 @@ describe('tool manifest detection', () => {
238
266
  }
239
267
  });
240
268
 
241
- test('attaches toolManifest metadata when valid TOOLS.json is present', () => {
242
- writeSkill('with-tools', 'Tool Skill', 'Skill with tools');
269
+ test("attaches toolManifest metadata when valid TOOLS.json is present", () => {
270
+ writeSkill("with-tools", "Tool Skill", "Skill with tools");
243
271
  const toolsJson = {
244
272
  version: 1,
245
273
  tools: [
246
274
  {
247
- name: 'run-lint',
248
- description: 'Runs linting',
249
- category: 'quality',
250
- risk: 'low',
251
- input_schema: { type: 'object', properties: {} },
252
- executor: 'lint.sh',
253
- execution_target: 'host',
275
+ name: "run-lint",
276
+ description: "Runs linting",
277
+ category: "quality",
278
+ risk: "low",
279
+ input_schema: { type: "object", properties: {} },
280
+ executor: "lint.sh",
281
+ execution_target: "host",
254
282
  },
255
283
  {
256
- name: 'run-test',
257
- description: 'Runs tests',
258
- category: 'quality',
259
- risk: 'medium',
260
- input_schema: { type: 'object', properties: {} },
261
- executor: 'test.sh',
262
- execution_target: 'sandbox',
284
+ name: "run-test",
285
+ description: "Runs tests",
286
+ category: "quality",
287
+ risk: "medium",
288
+ input_schema: { type: "object", properties: {} },
289
+ executor: "test.sh",
290
+ execution_target: "sandbox",
263
291
  },
264
292
  ],
265
293
  };
266
294
  writeFileSync(
267
- join(TEST_DIR, 'skills', 'with-tools', 'TOOLS.json'),
295
+ join(TEST_DIR, "skills", "with-tools", "TOOLS.json"),
268
296
  JSON.stringify(toolsJson),
269
297
  );
270
298
 
271
299
  const catalog = loadUserSkillCatalog();
272
- const skill = catalog.find((s) => s.id === 'with-tools');
300
+ const skill = catalog.find((s) => s.id === "with-tools");
273
301
  expect(skill).toBeDefined();
274
302
  expect(skill!.toolManifest).toEqual({
275
303
  present: true,
276
304
  valid: true,
277
305
  toolCount: 2,
278
- toolNames: ['run-lint', 'run-test'],
306
+ toolNames: ["run-lint", "run-test"],
279
307
  versionHash: expect.stringMatching(/^v1:[0-9a-f]{64}$/),
280
308
  });
281
309
  });
282
310
 
283
- test('marks toolManifest as invalid when TOOLS.json fails to parse', () => {
284
- writeSkill('bad-tools', 'Bad Tool Skill', 'Skill with invalid tools');
311
+ test("marks toolManifest as invalid when TOOLS.json fails to parse", () => {
312
+ writeSkill("bad-tools", "Bad Tool Skill", "Skill with invalid tools");
285
313
  writeFileSync(
286
- join(TEST_DIR, 'skills', 'bad-tools', 'TOOLS.json'),
287
- '{ not valid json !!!',
314
+ join(TEST_DIR, "skills", "bad-tools", "TOOLS.json"),
315
+ "{ not valid json !!!",
288
316
  );
289
317
 
290
318
  const catalog = loadUserSkillCatalog();
291
- const skill = catalog.find((s) => s.id === 'bad-tools');
319
+ const skill = catalog.find((s) => s.id === "bad-tools");
292
320
  expect(skill).toBeDefined();
293
321
  expect(skill!.toolManifest).toEqual({
294
322
  present: true,
@@ -299,16 +327,20 @@ describe('tool manifest detection', () => {
299
327
  });
300
328
  });
301
329
 
302
- test('marks toolManifest as invalid when TOOLS.json has schema errors', () => {
303
- writeSkill('schema-error', 'Schema Error Skill', 'Skill with schema errors');
330
+ test("marks toolManifest as invalid when TOOLS.json has schema errors", () => {
331
+ writeSkill(
332
+ "schema-error",
333
+ "Schema Error Skill",
334
+ "Skill with schema errors",
335
+ );
304
336
  // Valid JSON but missing required fields
305
337
  writeFileSync(
306
- join(TEST_DIR, 'skills', 'schema-error', 'TOOLS.json'),
307
- JSON.stringify({ version: 1, tools: [{ name: 'incomplete' }] }),
338
+ join(TEST_DIR, "skills", "schema-error", "TOOLS.json"),
339
+ JSON.stringify({ version: 1, tools: [{ name: "incomplete" }] }),
308
340
  );
309
341
 
310
342
  const catalog = loadUserSkillCatalog();
311
- const skill = catalog.find((s) => s.id === 'schema-error');
343
+ const skill = catalog.find((s) => s.id === "schema-error");
312
344
  expect(skill).toBeDefined();
313
345
  expect(skill!.toolManifest).toEqual({
314
346
  present: true,
@@ -319,74 +351,86 @@ describe('tool manifest detection', () => {
319
351
  });
320
352
  });
321
353
 
322
- test('does not set toolManifest when TOOLS.json is absent', () => {
323
- writeSkill('no-tools', 'No Tool Skill', 'Skill without tools');
354
+ test("does not set toolManifest when TOOLS.json is absent", () => {
355
+ writeSkill("no-tools", "No Tool Skill", "Skill without tools");
324
356
 
325
357
  const catalog = loadUserSkillCatalog();
326
- const skill = catalog.find((s) => s.id === 'no-tools');
358
+ const skill = catalog.find((s) => s.id === "no-tools");
327
359
  expect(skill).toBeDefined();
328
360
  expect(skill!.toolManifest).toBeUndefined();
329
361
  });
330
362
 
331
- test('versionHash is a v1: prefixed string when TOOLS.json is valid', () => {
332
- writeSkill('hash-valid', 'Hash Valid Skill', 'Skill with valid tools for hash check');
363
+ test("versionHash is a v1: prefixed string when TOOLS.json is valid", () => {
364
+ writeSkill(
365
+ "hash-valid",
366
+ "Hash Valid Skill",
367
+ "Skill with valid tools for hash check",
368
+ );
333
369
  const toolsJson = {
334
370
  version: 1,
335
371
  tools: [
336
372
  {
337
- name: 'hash-tool',
338
- description: 'A tool for hash testing',
339
- category: 'test',
340
- risk: 'low',
341
- input_schema: { type: 'object', properties: {} },
342
- executor: 'run.sh',
343
- execution_target: 'host',
373
+ name: "hash-tool",
374
+ description: "A tool for hash testing",
375
+ category: "test",
376
+ risk: "low",
377
+ input_schema: { type: "object", properties: {} },
378
+ executor: "run.sh",
379
+ execution_target: "host",
344
380
  },
345
381
  ],
346
382
  };
347
383
  writeFileSync(
348
- join(TEST_DIR, 'skills', 'hash-valid', 'TOOLS.json'),
384
+ join(TEST_DIR, "skills", "hash-valid", "TOOLS.json"),
349
385
  JSON.stringify(toolsJson),
350
386
  );
351
387
 
352
388
  const catalog = loadUserSkillCatalog();
353
- const skill = catalog.find((s) => s.id === 'hash-valid');
389
+ const skill = catalog.find((s) => s.id === "hash-valid");
354
390
  expect(skill).toBeDefined();
355
391
  expect(skill!.toolManifest).toBeDefined();
356
- expect(typeof skill!.toolManifest!.versionHash).toBe('string');
392
+ expect(typeof skill!.toolManifest!.versionHash).toBe("string");
357
393
  expect(skill!.toolManifest!.versionHash).toMatch(/^v1:[0-9a-f]{64}$/);
358
394
  });
359
395
 
360
- test('versionHash is computed even when TOOLS.json is invalid', () => {
361
- writeSkill('hash-invalid', 'Hash Invalid Skill', 'Skill with invalid tools for hash check');
396
+ test("versionHash is computed even when TOOLS.json is invalid", () => {
397
+ writeSkill(
398
+ "hash-invalid",
399
+ "Hash Invalid Skill",
400
+ "Skill with invalid tools for hash check",
401
+ );
362
402
  writeFileSync(
363
- join(TEST_DIR, 'skills', 'hash-invalid', 'TOOLS.json'),
364
- '{ broken json',
403
+ join(TEST_DIR, "skills", "hash-invalid", "TOOLS.json"),
404
+ "{ broken json",
365
405
  );
366
406
 
367
407
  const catalog = loadUserSkillCatalog();
368
- const skill = catalog.find((s) => s.id === 'hash-invalid');
408
+ const skill = catalog.find((s) => s.id === "hash-invalid");
369
409
  expect(skill).toBeDefined();
370
410
  expect(skill!.toolManifest).toBeDefined();
371
411
  expect(skill!.toolManifest!.valid).toBe(false);
372
412
  // Hash is based on the directory content, not manifest validity
373
- expect(typeof skill!.toolManifest!.versionHash).toBe('string');
413
+ expect(typeof skill!.toolManifest!.versionHash).toBe("string");
374
414
  expect(skill!.toolManifest!.versionHash).toMatch(/^v1:[0-9a-f]{64}$/);
375
415
  });
376
416
 
377
- test('toolManifest is undefined when TOOLS.json is absent (no hash computed)', () => {
378
- writeSkill('hash-absent', 'Hash Absent Skill', 'Skill without tools for hash check');
417
+ test("toolManifest is undefined when TOOLS.json is absent (no hash computed)", () => {
418
+ writeSkill(
419
+ "hash-absent",
420
+ "Hash Absent Skill",
421
+ "Skill without tools for hash check",
422
+ );
379
423
 
380
424
  const catalog = loadUserSkillCatalog();
381
- const skill = catalog.find((s) => s.id === 'hash-absent');
425
+ const skill = catalog.find((s) => s.id === "hash-absent");
382
426
  expect(skill).toBeDefined();
383
427
  expect(skill!.toolManifest).toBeUndefined();
384
428
  });
385
429
  });
386
430
 
387
- describe('includes frontmatter parsing', () => {
431
+ describe("includes frontmatter parsing", () => {
388
432
  beforeEach(() => {
389
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
433
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
390
434
  });
391
435
 
392
436
  afterEach(() => {
@@ -396,91 +440,91 @@ describe('includes frontmatter parsing', () => {
396
440
  });
397
441
 
398
442
  function writeSkillWithIncludes(skillId: string, includes: string): void {
399
- const skillDir = join(TEST_DIR, 'skills', skillId);
443
+ const skillDir = join(TEST_DIR, "skills", skillId);
400
444
  mkdirSync(skillDir, { recursive: true });
401
445
  writeFileSync(
402
- join(skillDir, 'SKILL.md'),
446
+ join(skillDir, "SKILL.md"),
403
447
  `---\nname: "${skillId}"\ndescription: "test"\nincludes: ${includes}\n---\n\nBody.\n`,
404
448
  );
405
449
  }
406
450
 
407
- test('parses valid includes array', () => {
408
- writeSkillWithIncludes('parent', '["child-a", "child-b"]');
409
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
451
+ test("parses valid includes array", () => {
452
+ writeSkillWithIncludes("parent", '["child-a", "child-b"]');
453
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
410
454
  const catalog = loadUserSkillCatalog();
411
- const skill = catalog.find(s => s.id === 'parent');
455
+ const skill = catalog.find((s) => s.id === "parent");
412
456
  expect(skill).toBeDefined();
413
- expect(skill!.includes).toEqual(['child-a', 'child-b']);
457
+ expect(skill!.includes).toEqual(["child-a", "child-b"]);
414
458
  });
415
459
 
416
- test('trims whitespace in includes entries', () => {
417
- writeSkillWithIncludes('parent', '[" child-a ", " child-b "]');
418
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
460
+ test("trims whitespace in includes entries", () => {
461
+ writeSkillWithIncludes("parent", '[" child-a ", " child-b "]');
462
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
419
463
  const catalog = loadUserSkillCatalog();
420
- const skill = catalog.find(s => s.id === 'parent');
421
- expect(skill!.includes).toEqual(['child-a', 'child-b']);
464
+ const skill = catalog.find((s) => s.id === "parent");
465
+ expect(skill!.includes).toEqual(["child-a", "child-b"]);
422
466
  });
423
467
 
424
- test('removes empty strings from includes', () => {
425
- writeSkillWithIncludes('parent', '["child-a", "", " ", "child-b"]');
426
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
468
+ test("removes empty strings from includes", () => {
469
+ writeSkillWithIncludes("parent", '["child-a", "", " ", "child-b"]');
470
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
427
471
  const catalog = loadUserSkillCatalog();
428
- const skill = catalog.find(s => s.id === 'parent');
429
- expect(skill!.includes).toEqual(['child-a', 'child-b']);
472
+ const skill = catalog.find((s) => s.id === "parent");
473
+ expect(skill!.includes).toEqual(["child-a", "child-b"]);
430
474
  });
431
475
 
432
- test('deduplicates includes preserving first-seen order', () => {
433
- writeSkillWithIncludes('parent', '["child-a", "child-b", "child-a"]');
434
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
476
+ test("deduplicates includes preserving first-seen order", () => {
477
+ writeSkillWithIncludes("parent", '["child-a", "child-b", "child-a"]');
478
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
435
479
  const catalog = loadUserSkillCatalog();
436
- const skill = catalog.find(s => s.id === 'parent');
437
- expect(skill!.includes).toEqual(['child-a', 'child-b']);
480
+ const skill = catalog.find((s) => s.id === "parent");
481
+ expect(skill!.includes).toEqual(["child-a", "child-b"]);
438
482
  });
439
483
 
440
- test('returns undefined for invalid JSON', () => {
441
- writeSkillWithIncludes('parent', 'not-json');
442
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
484
+ test("returns undefined for invalid JSON", () => {
485
+ writeSkillWithIncludes("parent", "not-json");
486
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
443
487
  const catalog = loadUserSkillCatalog();
444
- const skill = catalog.find(s => s.id === 'parent');
488
+ const skill = catalog.find((s) => s.id === "parent");
445
489
  expect(skill!.includes).toBeUndefined();
446
490
  });
447
491
 
448
- test('returns undefined for non-array JSON', () => {
449
- writeSkillWithIncludes('parent', '"just-a-string"');
450
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
492
+ test("returns undefined for non-array JSON", () => {
493
+ writeSkillWithIncludes("parent", '"just-a-string"');
494
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
451
495
  const catalog = loadUserSkillCatalog();
452
- const skill = catalog.find(s => s.id === 'parent');
496
+ const skill = catalog.find((s) => s.id === "parent");
453
497
  expect(skill!.includes).toBeUndefined();
454
498
  });
455
499
 
456
- test('returns undefined for array with non-string elements', () => {
457
- writeSkillWithIncludes('parent', '[123, true]');
458
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
500
+ test("returns undefined for array with non-string elements", () => {
501
+ writeSkillWithIncludes("parent", "[123, true]");
502
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
459
503
  const catalog = loadUserSkillCatalog();
460
- const skill = catalog.find(s => s.id === 'parent');
504
+ const skill = catalog.find((s) => s.id === "parent");
461
505
  expect(skill!.includes).toBeUndefined();
462
506
  });
463
507
 
464
- test('returns undefined for empty array', () => {
465
- writeSkillWithIncludes('parent', '[]');
466
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- parent\n');
508
+ test("returns undefined for empty array", () => {
509
+ writeSkillWithIncludes("parent", "[]");
510
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- parent\n");
467
511
  const catalog = loadUserSkillCatalog();
468
- const skill = catalog.find(s => s.id === 'parent');
512
+ const skill = catalog.find((s) => s.id === "parent");
469
513
  expect(skill!.includes).toBeUndefined();
470
514
  });
471
515
 
472
- test('skill without includes has undefined includes', () => {
473
- writeSkill('no-includes', 'No Includes', 'Test');
474
- writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), '- no-includes\n');
516
+ test("skill without includes has undefined includes", () => {
517
+ writeSkill("no-includes", "No Includes", "Test");
518
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- no-includes\n");
475
519
  const catalog = loadUserSkillCatalog();
476
- const skill = catalog.find(s => s.id === 'no-includes');
520
+ const skill = catalog.find((s) => s.id === "no-includes");
477
521
  expect(skill!.includes).toBeUndefined();
478
522
  });
479
523
  });
480
524
 
481
- describe('bundled browser skill', () => {
525
+ describe("bundled browser skill", () => {
482
526
  beforeEach(() => {
483
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
527
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
484
528
  });
485
529
 
486
530
  afterEach(() => {
@@ -489,65 +533,67 @@ describe('bundled browser skill', () => {
489
533
  }
490
534
  });
491
535
 
492
- test('browser skill appears in full catalog (including bundled)', () => {
536
+ test("browser skill appears in full catalog (including bundled)", () => {
493
537
  const catalog = loadSkillCatalog();
494
- const browserSkill = catalog.find((s) => s.id === 'browser');
538
+ const browserSkill = catalog.find((s) => s.id === "browser");
495
539
  expect(browserSkill).toBeDefined();
496
- expect(browserSkill!.name).toBe('Browser');
540
+ expect(browserSkill!.name).toBe("Browser");
497
541
  expect(browserSkill!.bundled).toBe(true);
498
542
  });
499
543
 
500
- test('browser skill has correct metadata', () => {
544
+ test("browser skill has correct metadata", () => {
501
545
  const catalog = loadSkillCatalog();
502
- const browserSkill = catalog.find((s) => s.id === 'browser');
546
+ const browserSkill = catalog.find((s) => s.id === "browser");
503
547
  expect(browserSkill).toBeDefined();
504
- expect(browserSkill!.description).toBe('Navigate and interact with web pages using a headless browser');
548
+ expect(browserSkill!.description).toBe(
549
+ "Navigate and interact with web pages using a headless browser",
550
+ );
505
551
  });
506
552
 
507
- test('browser skill is user-invocable', () => {
553
+ test("browser skill is user-invocable", () => {
508
554
  const catalog = loadSkillCatalog();
509
- const browserSkill = catalog.find((s) => s.id === 'browser');
555
+ const browserSkill = catalog.find((s) => s.id === "browser");
510
556
  expect(browserSkill).toBeDefined();
511
557
  expect(browserSkill!.userInvocable).toBe(true);
512
558
  });
513
559
 
514
- test('browser skill has model invocation enabled', () => {
560
+ test("browser skill has model invocation enabled", () => {
515
561
  const catalog = loadSkillCatalog();
516
- const browserSkill = catalog.find((s) => s.id === 'browser');
562
+ const browserSkill = catalog.find((s) => s.id === "browser");
517
563
  expect(browserSkill).toBeDefined();
518
564
  expect(browserSkill!.disableModelInvocation).toBe(false);
519
565
  });
520
566
 
521
- test('browser skill has a valid tool manifest with 14 tools', () => {
567
+ test("browser skill has a valid tool manifest with 14 tools", () => {
522
568
  const catalog = loadSkillCatalog();
523
- const browserSkill = catalog.find((s) => s.id === 'browser');
569
+ const browserSkill = catalog.find((s) => s.id === "browser");
524
570
  expect(browserSkill).toBeDefined();
525
571
  expect(browserSkill!.toolManifest).toBeDefined();
526
572
  expect(browserSkill!.toolManifest!.present).toBe(true);
527
573
  expect(browserSkill!.toolManifest!.valid).toBe(true);
528
574
  expect(browserSkill!.toolManifest!.toolCount).toBe(14);
529
575
  expect(browserSkill!.toolManifest!.toolNames).toEqual([
530
- 'browser_navigate',
531
- 'browser_snapshot',
532
- 'browser_screenshot',
533
- 'browser_close',
534
- 'browser_click',
535
- 'browser_type',
536
- 'browser_press_key',
537
- 'browser_scroll',
538
- 'browser_select_option',
539
- 'browser_hover',
540
- 'browser_wait_for',
541
- 'browser_extract',
542
- 'browser_wait_for_download',
543
- 'browser_fill_credential',
576
+ "browser_navigate",
577
+ "browser_snapshot",
578
+ "browser_screenshot",
579
+ "browser_close",
580
+ "browser_click",
581
+ "browser_type",
582
+ "browser_press_key",
583
+ "browser_scroll",
584
+ "browser_select_option",
585
+ "browser_hover",
586
+ "browser_wait_for",
587
+ "browser_extract",
588
+ "browser_wait_for_download",
589
+ "browser_fill_credential",
544
590
  ]);
545
591
  });
546
592
  });
547
593
 
548
- describe('bundled public-ingress skill', () => {
594
+ describe("bundled public-ingress skill", () => {
549
595
  beforeEach(() => {
550
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
596
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
551
597
  });
552
598
 
553
599
  afterEach(() => {
@@ -556,82 +602,94 @@ describe('bundled public-ingress skill', () => {
556
602
  }
557
603
  });
558
604
 
559
- test('public-ingress skill appears in full catalog (including bundled)', () => {
605
+ test("public-ingress skill appears in full catalog (including bundled)", () => {
560
606
  const catalog = loadSkillCatalog();
561
- const skill = catalog.find((s) => s.id === 'public-ingress');
607
+ const skill = catalog.find((s) => s.id === "public-ingress");
562
608
  expect(skill).toBeDefined();
563
- expect(skill!.name).toBe('Public Ingress');
609
+ expect(skill!.name).toBe("Public Ingress");
564
610
  expect(skill!.bundled).toBe(true);
565
611
  });
566
612
 
567
- test('public-ingress skill has correct description', () => {
613
+ test("public-ingress skill has correct description", () => {
568
614
  const catalog = loadSkillCatalog();
569
- const skill = catalog.find((s) => s.id === 'public-ingress');
615
+ const skill = catalog.find((s) => s.id === "public-ingress");
570
616
  expect(skill).toBeDefined();
571
- expect(skill!.description).toContain('ngrok');
572
- expect(skill!.description).toContain('ingress.publicBaseUrl');
617
+ expect(skill!.description).toContain("ngrok");
618
+ expect(skill!.description).toContain("ingress.publicBaseUrl");
573
619
  });
574
620
 
575
- test('public-ingress skill is user-invocable', () => {
621
+ test("public-ingress skill is user-invocable", () => {
576
622
  const catalog = loadSkillCatalog();
577
- const skill = catalog.find((s) => s.id === 'public-ingress');
623
+ const skill = catalog.find((s) => s.id === "public-ingress");
578
624
  expect(skill).toBeDefined();
579
625
  expect(skill!.userInvocable).toBe(true);
580
626
  });
581
627
 
582
- test('public-ingress skill has no tool manifest (instructions-only)', () => {
628
+ test("public-ingress skill has no tool manifest (instructions-only)", () => {
583
629
  const catalog = loadSkillCatalog();
584
- const skill = catalog.find((s) => s.id === 'public-ingress');
630
+ const skill = catalog.find((s) => s.id === "public-ingress");
585
631
  expect(skill).toBeDefined();
586
632
  expect(skill!.toolManifest).toBeUndefined();
587
633
  });
588
634
  });
589
635
 
590
- describe('ingress-dependent setup skills declare public-ingress', () => {
636
+ describe("ingress-dependent setup skills declare public-ingress", () => {
591
637
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
592
- const VELLUM_SKILLS_DIR = join(import.meta.dir, '..', 'config', 'vellum-skills');
593
- const BUNDLED_SKILLS_DIR = join(import.meta.dir, '..', 'config', 'bundled-skills');
638
+ const BUNDLED_SKILLS_DIR = join(
639
+ import.meta.dir,
640
+ "..",
641
+ "config",
642
+ "bundled-skills",
643
+ );
594
644
 
595
- function readSkillIncludes(dir: string, skillId: string): string[] | undefined {
596
- const content = readFileSync(join(dir, skillId, 'SKILL.md'), 'utf-8');
645
+ function readSkillIncludes(
646
+ dir: string,
647
+ skillId: string,
648
+ ): string[] | undefined {
649
+ const content = readFileSync(join(dir, skillId, "SKILL.md"), "utf-8");
597
650
  const match = content.match(FRONTMATTER_REGEX);
598
651
  if (!match) return undefined;
599
652
  for (const line of match[1].split(/\r?\n/)) {
600
- const sep = line.indexOf(':');
653
+ const sep = line.indexOf(":");
601
654
  if (sep === -1) continue;
602
655
  const key = line.slice(0, sep).trim();
603
- if (key !== 'includes') continue;
656
+ if (key !== "includes") continue;
604
657
  const val = line.slice(sep + 1).trim();
605
658
  try {
606
659
  const parsed = JSON.parse(val);
607
660
  if (Array.isArray(parsed)) return parsed as string[];
608
- } catch { /* ignore */ }
661
+ } catch {
662
+ /* ignore */
663
+ }
609
664
  }
610
665
  return undefined;
611
666
  }
612
667
 
613
- test('telegram-setup includes public-ingress', () => {
614
- const includes = readSkillIncludes(VELLUM_SKILLS_DIR, 'telegram-setup');
668
+ test("telegram-setup includes public-ingress", () => {
669
+ const includes = readSkillIncludes(BUNDLED_SKILLS_DIR, "telegram-setup");
615
670
  expect(includes).toBeDefined();
616
- expect(includes).toContain('public-ingress');
671
+ expect(includes).toContain("public-ingress");
617
672
  });
618
673
 
619
- test('google-oauth-setup includes public-ingress', () => {
620
- const includes = readSkillIncludes(BUNDLED_SKILLS_DIR, 'google-oauth-setup');
674
+ test("google-oauth-setup includes public-ingress", () => {
675
+ const includes = readSkillIncludes(
676
+ BUNDLED_SKILLS_DIR,
677
+ "google-oauth-setup",
678
+ );
621
679
  expect(includes).toBeDefined();
622
- expect(includes).toContain('public-ingress');
680
+ expect(includes).toContain("public-ingress");
623
681
  });
624
682
 
625
- test('slack-oauth-setup includes browser', () => {
626
- const includes = readSkillIncludes(VELLUM_SKILLS_DIR, 'slack-oauth-setup');
683
+ test("slack-oauth-setup includes browser", () => {
684
+ const includes = readSkillIncludes(BUNDLED_SKILLS_DIR, "slack-oauth-setup");
627
685
  expect(includes).toBeDefined();
628
- expect(includes).toContain('browser');
686
+ expect(includes).toContain("browser");
629
687
  });
630
688
  });
631
689
 
632
- describe('bundled computer-use skill', () => {
690
+ describe("bundled computer-use skill", () => {
633
691
  beforeEach(() => {
634
- mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
692
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
635
693
  });
636
694
 
637
695
  afterEach(() => {
@@ -640,49 +698,49 @@ describe('bundled computer-use skill', () => {
640
698
  }
641
699
  });
642
700
 
643
- test('computer-use skill appears in full catalog (including bundled)', () => {
701
+ test("computer-use skill appears in full catalog (including bundled)", () => {
644
702
  const catalog = loadSkillCatalog();
645
- const cuSkill = catalog.find((s) => s.id === 'computer-use');
703
+ const cuSkill = catalog.find((s) => s.id === "computer-use");
646
704
  expect(cuSkill).toBeDefined();
647
- expect(cuSkill!.name).toBe('Computer Use');
705
+ expect(cuSkill!.name).toBe("Computer Use");
648
706
  expect(cuSkill!.bundled).toBe(true);
649
707
  });
650
708
 
651
- test('computer-use skill is not user-invocable', () => {
709
+ test("computer-use skill is not user-invocable", () => {
652
710
  const catalog = loadSkillCatalog();
653
- const cuSkill = catalog.find((s) => s.id === 'computer-use');
711
+ const cuSkill = catalog.find((s) => s.id === "computer-use");
654
712
  expect(cuSkill).toBeDefined();
655
713
  expect(cuSkill!.userInvocable).toBe(false);
656
714
  });
657
715
 
658
- test('computer-use skill has model invocation disabled', () => {
716
+ test("computer-use skill has model invocation disabled", () => {
659
717
  const catalog = loadSkillCatalog();
660
- const cuSkill = catalog.find((s) => s.id === 'computer-use');
718
+ const cuSkill = catalog.find((s) => s.id === "computer-use");
661
719
  expect(cuSkill).toBeDefined();
662
720
  expect(cuSkill!.disableModelInvocation).toBe(true);
663
721
  });
664
722
 
665
- test('computer-use skill has a valid tool manifest with 12 tools', () => {
723
+ test("computer-use skill has a valid tool manifest with 12 tools", () => {
666
724
  const catalog = loadSkillCatalog();
667
- const cuSkill = catalog.find((s) => s.id === 'computer-use');
725
+ const cuSkill = catalog.find((s) => s.id === "computer-use");
668
726
  expect(cuSkill).toBeDefined();
669
727
  expect(cuSkill!.toolManifest).toBeDefined();
670
728
  expect(cuSkill!.toolManifest!.present).toBe(true);
671
729
  expect(cuSkill!.toolManifest!.valid).toBe(true);
672
730
  expect(cuSkill!.toolManifest!.toolCount).toBe(12);
673
731
  expect(cuSkill!.toolManifest!.toolNames).toEqual([
674
- 'computer_use_click',
675
- 'computer_use_double_click',
676
- 'computer_use_right_click',
677
- 'computer_use_type_text',
678
- 'computer_use_key',
679
- 'computer_use_scroll',
680
- 'computer_use_drag',
681
- 'computer_use_wait',
682
- 'computer_use_open_app',
683
- 'computer_use_run_applescript',
684
- 'computer_use_done',
685
- 'computer_use_respond',
732
+ "computer_use_click",
733
+ "computer_use_double_click",
734
+ "computer_use_right_click",
735
+ "computer_use_type_text",
736
+ "computer_use_key",
737
+ "computer_use_scroll",
738
+ "computer_use_drag",
739
+ "computer_use_wait",
740
+ "computer_use_open_app",
741
+ "computer_use_run_applescript",
742
+ "computer_use_done",
743
+ "computer_use_respond",
686
744
  ]);
687
745
  });
688
746
  });