@vellumai/assistant 0.4.11 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
|
@@ -1,58 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
16
|
+
mock.module("../util/platform.js", () => ({
|
|
10
17
|
getRootDir: () => TEST_DIR,
|
|
11
18
|
getDataDir: () => TEST_DIR,
|
|
12
19
|
ensureDataDir: () => {},
|
|
13
|
-
getSocketPath: () => join(TEST_DIR,
|
|
14
|
-
getPidPath: () => join(TEST_DIR,
|
|
15
|
-
getDbPath: () => join(TEST_DIR,
|
|
16
|
-
getLogPath: () => join(TEST_DIR,
|
|
17
|
-
isMacOS: () => process.platform ===
|
|
18
|
-
isLinux: () => process.platform ===
|
|
19
|
-
isWindows: () => process.platform ===
|
|
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,
|
|
22
|
-
getWorkspaceSkillsDir: () => join(TEST_DIR,
|
|
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(
|
|
30
|
-
getLogger: () =>
|
|
31
|
-
|
|
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 } =
|
|
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(
|
|
45
|
-
|
|
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,
|
|
62
|
+
join(skillDir, "SKILL.md"),
|
|
49
63
|
`---\nname: "${name}"\ndescription: "${description}"\n---\n\n${body}\n`,
|
|
50
64
|
);
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
describe(
|
|
67
|
+
describe("skills catalog loading", () => {
|
|
54
68
|
beforeEach(() => {
|
|
55
|
-
mkdirSync(join(TEST_DIR,
|
|
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(
|
|
65
|
-
writeSkill(
|
|
66
|
-
writeSkill(
|
|
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,
|
|
69
|
-
|
|
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([
|
|
87
|
+
expect(catalog.map((skill) => skill.id)).toEqual(["alpha", "beta"]);
|
|
74
88
|
});
|
|
75
89
|
|
|
76
|
-
test(
|
|
77
|
-
writeSkill(
|
|
78
|
-
writeSkill(
|
|
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,
|
|
81
|
-
|
|
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([
|
|
99
|
+
expect(catalog.map((skill) => skill.id)).toEqual(["lint", "test"]);
|
|
86
100
|
});
|
|
87
101
|
|
|
88
|
-
test(
|
|
89
|
-
writeSkill(
|
|
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,
|
|
92
|
-
|
|
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([
|
|
110
|
+
expect(catalog.map((skill) => skill.id)).toEqual(["safe"]);
|
|
97
111
|
});
|
|
98
112
|
|
|
99
|
-
test(
|
|
100
|
-
const externalSkillDir = join(TEST_DIR,
|
|
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,
|
|
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,
|
|
108
|
-
writeFileSync(join(TEST_DIR,
|
|
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(
|
|
115
|
-
const linkedSkillDir = join(TEST_DIR,
|
|
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,
|
|
132
|
+
const outsideDir = join(TEST_DIR, "outside");
|
|
119
133
|
mkdirSync(outsideDir, { recursive: true });
|
|
120
|
-
const externalSkillFile = join(outsideDir,
|
|
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,
|
|
127
|
-
writeFileSync(
|
|
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(
|
|
134
|
-
writeSkill(
|
|
135
|
-
writeSkill(
|
|
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([
|
|
156
|
+
expect(catalog.map((skill) => skill.id)).toEqual(["second", "first"]);
|
|
143
157
|
});
|
|
144
158
|
|
|
145
|
-
test(
|
|
146
|
-
writeSkill(
|
|
147
|
-
writeSkill(
|
|
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([
|
|
164
|
+
expect(catalog.map((skill) => skill.id)).toEqual(["alpha", "zeta"]);
|
|
151
165
|
});
|
|
152
166
|
|
|
153
|
-
test(
|
|
154
|
-
writeSkill(
|
|
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(
|
|
166
|
-
const WORKSPACE_DIR = join(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
193
|
-
writeWorkspaceSkill(
|
|
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 ===
|
|
215
|
+
const wsSkills = catalog.filter((s) => s.source === "workspace");
|
|
197
216
|
expect(wsSkills).toHaveLength(1);
|
|
198
|
-
expect(wsSkills[0].id).toBe(
|
|
217
|
+
expect(wsSkills[0].id).toBe("ws-skill");
|
|
199
218
|
});
|
|
200
219
|
|
|
201
|
-
test(
|
|
202
|
-
writeWorkspaceSkill(
|
|
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(
|
|
227
|
+
const result = resolveSkillSelector("ws-resolve", workspaceSkillsDir);
|
|
205
228
|
expect(result.skill).toBeDefined();
|
|
206
|
-
expect(result.skill!.id).toBe(
|
|
207
|
-
expect(result.skill!.source).toBe(
|
|
229
|
+
expect(result.skill!.id).toBe("ws-resolve");
|
|
230
|
+
expect(result.skill!.source).toBe("workspace");
|
|
208
231
|
});
|
|
209
232
|
|
|
210
|
-
test(
|
|
211
|
-
writeWorkspaceSkill(
|
|
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(
|
|
236
|
+
const result = resolveSkillSelector("ws-hidden");
|
|
214
237
|
expect(result.skill).toBeUndefined();
|
|
215
238
|
expect(result.error).toBeDefined();
|
|
216
239
|
});
|
|
217
240
|
|
|
218
|
-
test(
|
|
219
|
-
writeWorkspaceSkill(
|
|
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(
|
|
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(
|
|
225
|
-
expect(result.skill!.body).toBe(
|
|
226
|
-
expect(result.skill!.source).toBe(
|
|
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(
|
|
258
|
+
describe("tool manifest detection", () => {
|
|
231
259
|
beforeEach(() => {
|
|
232
|
-
mkdirSync(join(TEST_DIR,
|
|
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(
|
|
242
|
-
writeSkill(
|
|
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:
|
|
248
|
-
description:
|
|
249
|
-
category:
|
|
250
|
-
risk:
|
|
251
|
-
input_schema: { type:
|
|
252
|
-
executor:
|
|
253
|
-
execution_target:
|
|
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:
|
|
257
|
-
description:
|
|
258
|
-
category:
|
|
259
|
-
risk:
|
|
260
|
-
input_schema: { type:
|
|
261
|
-
executor:
|
|
262
|
-
execution_target:
|
|
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,
|
|
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 ===
|
|
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: [
|
|
306
|
+
toolNames: ["run-lint", "run-test"],
|
|
279
307
|
versionHash: expect.stringMatching(/^v1:[0-9a-f]{64}$/),
|
|
280
308
|
});
|
|
281
309
|
});
|
|
282
310
|
|
|
283
|
-
test(
|
|
284
|
-
writeSkill(
|
|
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,
|
|
287
|
-
|
|
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 ===
|
|
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(
|
|
303
|
-
writeSkill(
|
|
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,
|
|
307
|
-
JSON.stringify({ version: 1, tools: [{ name:
|
|
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 ===
|
|
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(
|
|
323
|
-
writeSkill(
|
|
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 ===
|
|
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(
|
|
332
|
-
writeSkill(
|
|
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:
|
|
338
|
-
description:
|
|
339
|
-
category:
|
|
340
|
-
risk:
|
|
341
|
-
input_schema: { type:
|
|
342
|
-
executor:
|
|
343
|
-
execution_target:
|
|
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,
|
|
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 ===
|
|
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(
|
|
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(
|
|
361
|
-
writeSkill(
|
|
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,
|
|
364
|
-
|
|
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 ===
|
|
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(
|
|
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(
|
|
378
|
-
writeSkill(
|
|
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 ===
|
|
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(
|
|
431
|
+
describe("includes frontmatter parsing", () => {
|
|
388
432
|
beforeEach(() => {
|
|
389
|
-
mkdirSync(join(TEST_DIR,
|
|
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,
|
|
443
|
+
const skillDir = join(TEST_DIR, "skills", skillId);
|
|
400
444
|
mkdirSync(skillDir, { recursive: true });
|
|
401
445
|
writeFileSync(
|
|
402
|
-
join(skillDir,
|
|
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(
|
|
408
|
-
writeSkillWithIncludes(
|
|
409
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
455
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
412
456
|
expect(skill).toBeDefined();
|
|
413
|
-
expect(skill!.includes).toEqual([
|
|
457
|
+
expect(skill!.includes).toEqual(["child-a", "child-b"]);
|
|
414
458
|
});
|
|
415
459
|
|
|
416
|
-
test(
|
|
417
|
-
writeSkillWithIncludes(
|
|
418
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
421
|
-
expect(skill!.includes).toEqual([
|
|
464
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
465
|
+
expect(skill!.includes).toEqual(["child-a", "child-b"]);
|
|
422
466
|
});
|
|
423
467
|
|
|
424
|
-
test(
|
|
425
|
-
writeSkillWithIncludes(
|
|
426
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
429
|
-
expect(skill!.includes).toEqual([
|
|
472
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
473
|
+
expect(skill!.includes).toEqual(["child-a", "child-b"]);
|
|
430
474
|
});
|
|
431
475
|
|
|
432
|
-
test(
|
|
433
|
-
writeSkillWithIncludes(
|
|
434
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
437
|
-
expect(skill!.includes).toEqual([
|
|
480
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
481
|
+
expect(skill!.includes).toEqual(["child-a", "child-b"]);
|
|
438
482
|
});
|
|
439
483
|
|
|
440
|
-
test(
|
|
441
|
-
writeSkillWithIncludes(
|
|
442
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
488
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
445
489
|
expect(skill!.includes).toBeUndefined();
|
|
446
490
|
});
|
|
447
491
|
|
|
448
|
-
test(
|
|
449
|
-
writeSkillWithIncludes(
|
|
450
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
496
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
453
497
|
expect(skill!.includes).toBeUndefined();
|
|
454
498
|
});
|
|
455
499
|
|
|
456
|
-
test(
|
|
457
|
-
writeSkillWithIncludes(
|
|
458
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
504
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
461
505
|
expect(skill!.includes).toBeUndefined();
|
|
462
506
|
});
|
|
463
507
|
|
|
464
|
-
test(
|
|
465
|
-
writeSkillWithIncludes(
|
|
466
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
512
|
+
const skill = catalog.find((s) => s.id === "parent");
|
|
469
513
|
expect(skill!.includes).toBeUndefined();
|
|
470
514
|
});
|
|
471
515
|
|
|
472
|
-
test(
|
|
473
|
-
writeSkill(
|
|
474
|
-
writeFileSync(join(TEST_DIR,
|
|
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 ===
|
|
520
|
+
const skill = catalog.find((s) => s.id === "no-includes");
|
|
477
521
|
expect(skill!.includes).toBeUndefined();
|
|
478
522
|
});
|
|
479
523
|
});
|
|
480
524
|
|
|
481
|
-
describe(
|
|
525
|
+
describe("bundled browser skill", () => {
|
|
482
526
|
beforeEach(() => {
|
|
483
|
-
mkdirSync(join(TEST_DIR,
|
|
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(
|
|
536
|
+
test("browser skill appears in full catalog (including bundled)", () => {
|
|
493
537
|
const catalog = loadSkillCatalog();
|
|
494
|
-
const browserSkill = catalog.find((s) => s.id ===
|
|
538
|
+
const browserSkill = catalog.find((s) => s.id === "browser");
|
|
495
539
|
expect(browserSkill).toBeDefined();
|
|
496
|
-
expect(browserSkill!.name).toBe(
|
|
540
|
+
expect(browserSkill!.name).toBe("Browser");
|
|
497
541
|
expect(browserSkill!.bundled).toBe(true);
|
|
498
542
|
});
|
|
499
543
|
|
|
500
|
-
test(
|
|
544
|
+
test("browser skill has correct metadata", () => {
|
|
501
545
|
const catalog = loadSkillCatalog();
|
|
502
|
-
const browserSkill = catalog.find((s) => s.id ===
|
|
546
|
+
const browserSkill = catalog.find((s) => s.id === "browser");
|
|
503
547
|
expect(browserSkill).toBeDefined();
|
|
504
|
-
expect(browserSkill!.description).toBe(
|
|
548
|
+
expect(browserSkill!.description).toBe(
|
|
549
|
+
"Navigate and interact with web pages using a headless browser",
|
|
550
|
+
);
|
|
505
551
|
});
|
|
506
552
|
|
|
507
|
-
test(
|
|
553
|
+
test("browser skill is user-invocable", () => {
|
|
508
554
|
const catalog = loadSkillCatalog();
|
|
509
|
-
const browserSkill = catalog.find((s) => s.id ===
|
|
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(
|
|
560
|
+
test("browser skill has model invocation enabled", () => {
|
|
515
561
|
const catalog = loadSkillCatalog();
|
|
516
|
-
const browserSkill = catalog.find((s) => s.id ===
|
|
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(
|
|
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 ===
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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(
|
|
594
|
+
describe("bundled public-ingress skill", () => {
|
|
549
595
|
beforeEach(() => {
|
|
550
|
-
mkdirSync(join(TEST_DIR,
|
|
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(
|
|
605
|
+
test("public-ingress skill appears in full catalog (including bundled)", () => {
|
|
560
606
|
const catalog = loadSkillCatalog();
|
|
561
|
-
const skill = catalog.find((s) => s.id ===
|
|
607
|
+
const skill = catalog.find((s) => s.id === "public-ingress");
|
|
562
608
|
expect(skill).toBeDefined();
|
|
563
|
-
expect(skill!.name).toBe(
|
|
609
|
+
expect(skill!.name).toBe("Public Ingress");
|
|
564
610
|
expect(skill!.bundled).toBe(true);
|
|
565
611
|
});
|
|
566
612
|
|
|
567
|
-
test(
|
|
613
|
+
test("public-ingress skill has correct description", () => {
|
|
568
614
|
const catalog = loadSkillCatalog();
|
|
569
|
-
const skill = catalog.find((s) => s.id ===
|
|
615
|
+
const skill = catalog.find((s) => s.id === "public-ingress");
|
|
570
616
|
expect(skill).toBeDefined();
|
|
571
|
-
expect(skill!.description).toContain(
|
|
572
|
-
expect(skill!.description).toContain(
|
|
617
|
+
expect(skill!.description).toContain("ngrok");
|
|
618
|
+
expect(skill!.description).toContain("ingress.publicBaseUrl");
|
|
573
619
|
});
|
|
574
620
|
|
|
575
|
-
test(
|
|
621
|
+
test("public-ingress skill is user-invocable", () => {
|
|
576
622
|
const catalog = loadSkillCatalog();
|
|
577
|
-
const skill = catalog.find((s) => s.id ===
|
|
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(
|
|
628
|
+
test("public-ingress skill has no tool manifest (instructions-only)", () => {
|
|
583
629
|
const catalog = loadSkillCatalog();
|
|
584
|
-
const skill = catalog.find((s) => s.id ===
|
|
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(
|
|
636
|
+
describe("ingress-dependent setup skills declare public-ingress", () => {
|
|
591
637
|
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
592
|
-
const
|
|
593
|
-
|
|
638
|
+
const BUNDLED_SKILLS_DIR = join(
|
|
639
|
+
import.meta.dir,
|
|
640
|
+
"..",
|
|
641
|
+
"config",
|
|
642
|
+
"bundled-skills",
|
|
643
|
+
);
|
|
594
644
|
|
|
595
|
-
function readSkillIncludes(
|
|
596
|
-
|
|
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 !==
|
|
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 {
|
|
661
|
+
} catch {
|
|
662
|
+
/* ignore */
|
|
663
|
+
}
|
|
609
664
|
}
|
|
610
665
|
return undefined;
|
|
611
666
|
}
|
|
612
667
|
|
|
613
|
-
test(
|
|
614
|
-
const includes = readSkillIncludes(
|
|
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(
|
|
671
|
+
expect(includes).toContain("public-ingress");
|
|
617
672
|
});
|
|
618
673
|
|
|
619
|
-
test(
|
|
620
|
-
const includes = readSkillIncludes(
|
|
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(
|
|
680
|
+
expect(includes).toContain("public-ingress");
|
|
623
681
|
});
|
|
624
682
|
|
|
625
|
-
test(
|
|
626
|
-
const includes = readSkillIncludes(
|
|
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(
|
|
686
|
+
expect(includes).toContain("browser");
|
|
629
687
|
});
|
|
630
688
|
});
|
|
631
689
|
|
|
632
|
-
describe(
|
|
690
|
+
describe("bundled computer-use skill", () => {
|
|
633
691
|
beforeEach(() => {
|
|
634
|
-
mkdirSync(join(TEST_DIR,
|
|
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(
|
|
701
|
+
test("computer-use skill appears in full catalog (including bundled)", () => {
|
|
644
702
|
const catalog = loadSkillCatalog();
|
|
645
|
-
const cuSkill = catalog.find((s) => s.id ===
|
|
703
|
+
const cuSkill = catalog.find((s) => s.id === "computer-use");
|
|
646
704
|
expect(cuSkill).toBeDefined();
|
|
647
|
-
expect(cuSkill!.name).toBe(
|
|
705
|
+
expect(cuSkill!.name).toBe("Computer Use");
|
|
648
706
|
expect(cuSkill!.bundled).toBe(true);
|
|
649
707
|
});
|
|
650
708
|
|
|
651
|
-
test(
|
|
709
|
+
test("computer-use skill is not user-invocable", () => {
|
|
652
710
|
const catalog = loadSkillCatalog();
|
|
653
|
-
const cuSkill = catalog.find((s) => s.id ===
|
|
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(
|
|
716
|
+
test("computer-use skill has model invocation disabled", () => {
|
|
659
717
|
const catalog = loadSkillCatalog();
|
|
660
|
-
const cuSkill = catalog.find((s) => s.id ===
|
|
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(
|
|
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 ===
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
});
|