dev3000 0.0.144 → 0.0.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +82 -8
- package/dist/cli.js.map +1 -1
- package/dist/components/SkillSelector.d.ts +2 -1
- package/dist/components/SkillSelector.d.ts.map +1 -1
- package/dist/components/SkillSelector.js +16 -5
- package/dist/components/SkillSelector.js.map +1 -1
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +94 -0
- package/dist/dev-environment.js.map +1 -1
- package/dist/skills/index.d.ts +49 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +185 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/index.test.ts +218 -0
- package/dist/skills/index.ts +219 -0
- package/dist/skills/react-performance/SKILL.md +1034 -0
- package/dist/utils/agent-browser.d.ts +120 -0
- package/dist/utils/agent-browser.d.ts.map +1 -0
- package/dist/utils/agent-browser.js +387 -0
- package/dist/utils/agent-browser.js.map +1 -0
- package/dist/utils/skill-installer.d.ts +10 -0
- package/dist/utils/skill-installer.d.ts.map +1 -1
- package/dist/utils/skill-installer.js +77 -4
- package/dist/utils/skill-installer.js.map +1 -1
- package/dist/utils/user-config.d.ts +1 -0
- package/dist/utils/user-config.d.ts.map +1 -1
- package/dist/utils/user-config.js +10 -8
- package/dist/utils/user-config.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/build-manifest.json +2 -2
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error/page.js +1 -1
- package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_global-error.html +2 -2
- package/mcp-server/.next/server/app/_global-error.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found/page.js +1 -1
- package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/api/jank/[session]/route.js +1 -1
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/orchestrator/route.js +1 -1
- package/mcp-server/.next/server/app/api/orchestrator/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/capture/route.js +2 -2
- package/mcp-server/.next/server/app/api/screenshots/capture/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/clear/route.js +1 -1
- package/mcp-server/.next/server/app/api/screenshots/clear/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/list/route.js +1 -1
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/logs/page.js +1 -1
- package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/mcp/route.js +4 -4
- package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page.js +1 -1
- package/mcp-server/.next/server/app/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/video/[session]/page.js +1 -1
- package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -1
- package/mcp-server/.next/server/chunks/[externals]__0e1fd2ca._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__130a5f58._.js +4 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__ee39655c._.js.map → [root-of-the-server]__130a5f58._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__af8a6500._.js → [root-of-the-server]__1dca9894._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__2f95edf0._.js +3 -3
- package/mcp-server/.next/server/chunks/[root-of-the-server]__2f95edf0._.js.map +1 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__444592aa._.js +3 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__69498adb._.js.map → [root-of-the-server]__444592aa._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__7ed5139a._.js → [root-of-the-server]__8f84b4cc._.js} +2 -2
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__6c914ee2._.js → [root-of-the-server]__94946101._.js} +2 -2
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__6728fd4b._.js → [root-of-the-server]__9c4c7095._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__b71c83ed._.js +4 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__f4213a2f._.js.map → [root-of-the-server]__b71c83ed._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__daca64a4._.js → [root-of-the-server]__b86e20b6._.js} +2 -2
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__95098840._.js → [root-of-the-server]__ee139f8a._.js} +2 -2
- package/mcp-server/.next/server/chunks/mcp-server_app_mcp_tools_ts_faf6d7df._.js +26 -31
- package/mcp-server/.next/server/chunks/mcp-server_app_mcp_tools_ts_faf6d7df._.js.map +1 -1
- package/mcp-server/.next/server/chunks/src_utils_agent-browser_ts_cc00e0d8._.js +3 -0
- package/mcp-server/.next/server/chunks/src_utils_agent-browser_ts_cc00e0d8._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__50eb2eba._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__4b74b898._.js → [root-of-the-server]__f66148e5._.js} +2 -2
- package/mcp-server/.next/server/pages/404.html +1 -1
- package/mcp-server/.next/server/pages/500.html +2 -2
- package/mcp-server/.next/server/server-reference-manifest.js +1 -1
- package/mcp-server/.next/server/server-reference-manifest.json +1 -1
- package/mcp-server/app/mcp/route.ts +30 -0
- package/mcp-server/app/mcp/tools.ts +204 -61
- package/mcp-server/start-production.mjs +37 -3
- package/package.json +4 -3
- package/mcp-server/.next/server/chunks/[externals]__eb460e97._.js +0 -3
- package/mcp-server/.next/server/chunks/[root-of-the-server]__69498adb._.js +0 -3
- package/mcp-server/.next/server/chunks/[root-of-the-server]__ee39655c._.js +0 -4
- package/mcp-server/.next/server/chunks/[root-of-the-server]__f4213a2f._.js +0 -4
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__163e0fe2._.js +0 -3
- /package/mcp-server/.next/server/chunks/{[externals]__eb460e97._.js.map → [externals]__0e1fd2ca._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__af8a6500._.js.map → [root-of-the-server]__1dca9894._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__7ed5139a._.js.map → [root-of-the-server]__8f84b4cc._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__6c914ee2._.js.map → [root-of-the-server]__94946101._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__6728fd4b._.js.map → [root-of-the-server]__9c4c7095._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__daca64a4._.js.map → [root-of-the-server]__b86e20b6._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__95098840._.js.map → [root-of-the-server]__ee139f8a._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__163e0fe2._.js.map → [root-of-the-server]__50eb2eba._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__4b74b898._.js.map → [root-of-the-server]__f66148e5._.js.map} +0 -0
- /package/mcp-server/.next/static/{2Xvryb3dyQkf-Of6R9qdS → f8-qsTYpMHe5da6lkWH6m}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{2Xvryb3dyQkf-Of6R9qdS → f8-qsTYpMHe5da6lkWH6m}/_clientMiddlewareManifest.json +0 -0
- /package/mcp-server/.next/static/{2Xvryb3dyQkf-Of6R9qdS → f8-qsTYpMHe5da6lkWH6m}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared skill module for d3k
|
|
3
|
+
*
|
|
4
|
+
* Skills are prompt templates stored as SKILL.md files that provide
|
|
5
|
+
* specialized instructions for specific tasks.
|
|
6
|
+
*
|
|
7
|
+
* This module is used by both:
|
|
8
|
+
* - CLI: `d3k skill <name>`
|
|
9
|
+
* - MCP: `get_skill` tool
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
/**
|
|
15
|
+
* Get the base directories where skills can be found.
|
|
16
|
+
* Searches in order of priority:
|
|
17
|
+
* 1. Project-local skills (.claude/skills/)
|
|
18
|
+
* 2. Source skills (src/skills/) - for development
|
|
19
|
+
* 3. Dist skills (dist/skills/) - for installed packages
|
|
20
|
+
*/
|
|
21
|
+
export function getSkillDirectories(cwd) {
|
|
22
|
+
const dirs = [];
|
|
23
|
+
// 1. Project-local skills (highest priority)
|
|
24
|
+
const projectDir = cwd || process.cwd();
|
|
25
|
+
dirs.push(join(projectDir, ".claude", "skills"));
|
|
26
|
+
// 2. Check if we're running from a compiled binary (bun compile)
|
|
27
|
+
// In compiled binaries, import.meta.url returns /$bunfs/... virtual path
|
|
28
|
+
// We detect this by checking if the URL starts with /$bunfs
|
|
29
|
+
const moduleUrl = import.meta.url;
|
|
30
|
+
const isCompiledBinary = moduleUrl.startsWith("file:///$bunfs") || moduleUrl.startsWith("/$bunfs");
|
|
31
|
+
if (isCompiledBinary) {
|
|
32
|
+
// For compiled binaries, process.execPath contains the actual binary path
|
|
33
|
+
const binaryPath = process.execPath;
|
|
34
|
+
if (binaryPath && existsSync(binaryPath)) {
|
|
35
|
+
const binDir = dirname(binaryPath); // bin/
|
|
36
|
+
const packageDir = dirname(binDir); // platform package root (e.g., @d3k/darwin-arm64)
|
|
37
|
+
dirs.push(join(packageDir, "skills"));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 3. Source and dist skills from the d3k package
|
|
41
|
+
// Handle both ESM (__dirname equivalent) and different execution contexts
|
|
42
|
+
try {
|
|
43
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
44
|
+
const srcDir = dirname(currentFile); // src/skills
|
|
45
|
+
const packageRoot = dirname(dirname(srcDir)); // package root
|
|
46
|
+
// Source skills (for development)
|
|
47
|
+
dirs.push(join(packageRoot, "src", "skills"));
|
|
48
|
+
// Dist skills (for installed package)
|
|
49
|
+
dirs.push(join(packageRoot, "dist", "skills"));
|
|
50
|
+
// Also check if we're in a binary distribution
|
|
51
|
+
dirs.push(join(packageRoot, "skills"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Fallback for CommonJS or other contexts
|
|
55
|
+
}
|
|
56
|
+
return dirs.filter((dir) => existsSync(dir));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find a skill by name.
|
|
60
|
+
* Returns the path to the SKILL.md file if found.
|
|
61
|
+
*/
|
|
62
|
+
export function findSkill(name, cwd) {
|
|
63
|
+
const skillDirs = getSkillDirectories(cwd);
|
|
64
|
+
for (const dir of skillDirs) {
|
|
65
|
+
const skillPath = join(dir, name, "SKILL.md");
|
|
66
|
+
if (existsSync(skillPath)) {
|
|
67
|
+
return skillPath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the content of a skill by name.
|
|
74
|
+
*/
|
|
75
|
+
export function getSkill(name, cwd) {
|
|
76
|
+
const skillPath = findSkill(name, cwd);
|
|
77
|
+
if (skillPath) {
|
|
78
|
+
try {
|
|
79
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
80
|
+
return {
|
|
81
|
+
found: true,
|
|
82
|
+
name,
|
|
83
|
+
content,
|
|
84
|
+
path: skillPath
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
found: false,
|
|
90
|
+
name,
|
|
91
|
+
error: `Failed to read skill: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
availableSkills: listAvailableSkills(cwd)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
found: false,
|
|
98
|
+
name,
|
|
99
|
+
error: `Skill "${name}" not found`,
|
|
100
|
+
availableSkills: listAvailableSkills(cwd)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* List all available skills.
|
|
105
|
+
*/
|
|
106
|
+
export function listAvailableSkills(cwd) {
|
|
107
|
+
const skills = new Set();
|
|
108
|
+
const skillDirs = getSkillDirectories(cwd);
|
|
109
|
+
for (const dir of skillDirs) {
|
|
110
|
+
try {
|
|
111
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
const skillFile = join(dir, entry.name, "SKILL.md");
|
|
115
|
+
if (existsSync(skillFile)) {
|
|
116
|
+
skills.add(entry.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Directory might not exist or be readable
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return Array.from(skills).sort();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get detailed info about all available skills.
|
|
129
|
+
*/
|
|
130
|
+
export function getSkillsInfo(cwd) {
|
|
131
|
+
const skillsMap = new Map();
|
|
132
|
+
const skillDirs = getSkillDirectories(cwd);
|
|
133
|
+
for (const dir of skillDirs) {
|
|
134
|
+
try {
|
|
135
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry.isDirectory() && !skillsMap.has(entry.name)) {
|
|
138
|
+
const skillPath = join(dir, entry.name, "SKILL.md");
|
|
139
|
+
if (existsSync(skillPath)) {
|
|
140
|
+
try {
|
|
141
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
142
|
+
const description = extractDescription(content);
|
|
143
|
+
skillsMap.set(entry.name, {
|
|
144
|
+
name: entry.name,
|
|
145
|
+
description,
|
|
146
|
+
path: skillPath
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Skip unreadable skills
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Directory might not exist or be readable
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return Array.from(skillsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract description from SKILL.md frontmatter or first paragraph.
|
|
164
|
+
*/
|
|
165
|
+
function extractDescription(content) {
|
|
166
|
+
// Try to extract from YAML frontmatter
|
|
167
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
168
|
+
if (frontmatterMatch) {
|
|
169
|
+
const frontmatter = frontmatterMatch[1];
|
|
170
|
+
const descMatch = frontmatter.match(/description:\s*(.+)/);
|
|
171
|
+
if (descMatch) {
|
|
172
|
+
return descMatch[1].trim();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Fallback: use first non-heading, non-empty line
|
|
176
|
+
const lines = content.split("\n");
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const trimmed = line.trim();
|
|
179
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
|
|
180
|
+
return trimmed.slice(0, 100) + (trimmed.length > 100 ? "..." : "");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return "No description available";
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/skills/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "fs"
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
3
|
+
import { findSkill, getSkill, getSkillDirectories, getSkillsInfo, listAvailableSkills, type SkillResult } from "./index"
|
|
4
|
+
|
|
5
|
+
// Mock fs module
|
|
6
|
+
vi.mock("fs", () => ({
|
|
7
|
+
existsSync: vi.fn(),
|
|
8
|
+
readdirSync: vi.fn(),
|
|
9
|
+
readFileSync: vi.fn()
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
describe("skills module", () => {
|
|
13
|
+
const mockExistsSync = existsSync as unknown as ReturnType<typeof vi.fn>
|
|
14
|
+
const mockReaddirSync = readdirSync as unknown as ReturnType<typeof vi.fn>
|
|
15
|
+
const mockReadFileSync = readFileSync as unknown as ReturnType<typeof vi.fn>
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("getSkillDirectories", () => {
|
|
22
|
+
it("should return directories that exist", () => {
|
|
23
|
+
// At minimum, should return project-local .claude/skills if it exists
|
|
24
|
+
mockExistsSync.mockImplementation((path: string) => {
|
|
25
|
+
return path.includes(".claude/skills") || path.includes("src/skills")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const dirs = getSkillDirectories("/test/project")
|
|
29
|
+
expect(dirs.length).toBeGreaterThan(0)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should include project-local skills directory", () => {
|
|
33
|
+
mockExistsSync.mockReturnValue(true)
|
|
34
|
+
|
|
35
|
+
const dirs = getSkillDirectories("/test/project")
|
|
36
|
+
const hasProjectLocal = dirs.some((d) => d.includes(".claude/skills"))
|
|
37
|
+
expect(hasProjectLocal).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("findSkill", () => {
|
|
42
|
+
it("should find a skill in the first matching directory", () => {
|
|
43
|
+
// Mock: directory exists AND skill file exists
|
|
44
|
+
mockExistsSync.mockImplementation((path: string) => {
|
|
45
|
+
// Return true for both the directory check and the SKILL.md file check
|
|
46
|
+
return path.includes(".claude/skills") || path.includes("src/skills") || path.includes("test-skill/SKILL.md")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const result = findSkill("test-skill", "/test/project")
|
|
50
|
+
expect(result).toBeTruthy()
|
|
51
|
+
expect(result).toContain("test-skill/SKILL.md")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("should return null if skill not found", () => {
|
|
55
|
+
mockExistsSync.mockReturnValue(false)
|
|
56
|
+
|
|
57
|
+
const result = findSkill("nonexistent-skill", "/test/project")
|
|
58
|
+
expect(result).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("getSkill", () => {
|
|
63
|
+
it("should return skill content when found", () => {
|
|
64
|
+
const skillContent = `---
|
|
65
|
+
description: Test skill description
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
# Test Skill
|
|
69
|
+
|
|
70
|
+
This is test content.`
|
|
71
|
+
|
|
72
|
+
// Mock: directory exists AND skill file exists
|
|
73
|
+
mockExistsSync.mockImplementation((path: string) => {
|
|
74
|
+
return path.includes(".claude/skills") || path.includes("src/skills") || path.includes("test-skill/SKILL.md")
|
|
75
|
+
})
|
|
76
|
+
mockReadFileSync.mockReturnValue(skillContent)
|
|
77
|
+
|
|
78
|
+
const result: SkillResult = getSkill("test-skill", "/test/project")
|
|
79
|
+
expect(result.found).toBe(true)
|
|
80
|
+
expect(result.name).toBe("test-skill")
|
|
81
|
+
expect(result.content).toBe(skillContent)
|
|
82
|
+
expect(result.path).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("should return error when skill not found", () => {
|
|
86
|
+
mockExistsSync.mockReturnValue(false)
|
|
87
|
+
mockReaddirSync.mockReturnValue([])
|
|
88
|
+
|
|
89
|
+
const result: SkillResult = getSkill("nonexistent", "/test/project")
|
|
90
|
+
expect(result.found).toBe(false)
|
|
91
|
+
expect(result.error).toContain("not found")
|
|
92
|
+
expect(result.availableSkills).toBeDefined()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should handle read errors gracefully", () => {
|
|
96
|
+
// Mock: directory and file exist but read fails
|
|
97
|
+
mockExistsSync.mockImplementation((path: string) => {
|
|
98
|
+
return path.includes(".claude/skills") || path.includes("src/skills") || path.includes("error-skill/SKILL.md")
|
|
99
|
+
})
|
|
100
|
+
mockReadFileSync.mockImplementation(() => {
|
|
101
|
+
throw new Error("Permission denied")
|
|
102
|
+
})
|
|
103
|
+
mockReaddirSync.mockReturnValue([])
|
|
104
|
+
|
|
105
|
+
const result: SkillResult = getSkill("error-skill", "/test/project")
|
|
106
|
+
expect(result.found).toBe(false)
|
|
107
|
+
expect(result.error).toContain("Failed to read skill")
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("listAvailableSkills", () => {
|
|
112
|
+
it("should list skills from all directories", () => {
|
|
113
|
+
mockExistsSync.mockReturnValue(true)
|
|
114
|
+
mockReaddirSync.mockImplementation((dir: string) => {
|
|
115
|
+
if (dir.includes(".claude/skills")) {
|
|
116
|
+
return [
|
|
117
|
+
{ name: "skill-a", isDirectory: () => true },
|
|
118
|
+
{ name: "skill-b", isDirectory: () => true }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
if (dir.includes("src/skills")) {
|
|
122
|
+
return [
|
|
123
|
+
{ name: "skill-c", isDirectory: () => true },
|
|
124
|
+
{ name: "not-a-skill.txt", isDirectory: () => false }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
return []
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const skills = listAvailableSkills("/test/project")
|
|
131
|
+
expect(skills).toContain("skill-a")
|
|
132
|
+
expect(skills).toContain("skill-b")
|
|
133
|
+
expect(skills).toContain("skill-c")
|
|
134
|
+
expect(skills).not.toContain("not-a-skill.txt")
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("should deduplicate skills across directories", () => {
|
|
138
|
+
mockExistsSync.mockReturnValue(true)
|
|
139
|
+
mockReaddirSync.mockImplementation(() => [{ name: "same-skill", isDirectory: () => true }])
|
|
140
|
+
|
|
141
|
+
const skills = listAvailableSkills("/test/project")
|
|
142
|
+
const sameSkillCount = skills.filter((s) => s === "same-skill").length
|
|
143
|
+
expect(sameSkillCount).toBe(1)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should return sorted list", () => {
|
|
147
|
+
mockExistsSync.mockReturnValue(true)
|
|
148
|
+
mockReaddirSync.mockImplementation(() => [
|
|
149
|
+
{ name: "zebra", isDirectory: () => true },
|
|
150
|
+
{ name: "alpha", isDirectory: () => true },
|
|
151
|
+
{ name: "beta", isDirectory: () => true }
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
const skills = listAvailableSkills("/test/project")
|
|
155
|
+
expect(skills).toEqual(["alpha", "beta", "zebra"])
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should handle directory read errors gracefully", () => {
|
|
159
|
+
mockExistsSync.mockReturnValue(true)
|
|
160
|
+
mockReaddirSync.mockImplementation(() => {
|
|
161
|
+
throw new Error("Permission denied")
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const skills = listAvailableSkills("/test/project")
|
|
165
|
+
expect(skills).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("getSkillsInfo", () => {
|
|
170
|
+
it("should return detailed skill info with descriptions", () => {
|
|
171
|
+
const skillContent = `---
|
|
172
|
+
description: A helpful skill for testing
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
# Test Skill`
|
|
176
|
+
|
|
177
|
+
mockExistsSync.mockReturnValue(true)
|
|
178
|
+
mockReaddirSync.mockImplementation(() => [{ name: "test-skill", isDirectory: () => true }])
|
|
179
|
+
mockReadFileSync.mockReturnValue(skillContent)
|
|
180
|
+
|
|
181
|
+
const skills = getSkillsInfo("/test/project")
|
|
182
|
+
expect(skills.length).toBeGreaterThan(0)
|
|
183
|
+
expect(skills[0].name).toBe("test-skill")
|
|
184
|
+
expect(skills[0].description).toBe("A helpful skill for testing")
|
|
185
|
+
expect(skills[0].path).toBeTruthy()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("should extract description from first paragraph if no frontmatter", () => {
|
|
189
|
+
const skillContent = `# Test Skill
|
|
190
|
+
|
|
191
|
+
This is the first paragraph that should be used as description.
|
|
192
|
+
|
|
193
|
+
More content here.`
|
|
194
|
+
|
|
195
|
+
mockExistsSync.mockReturnValue(true)
|
|
196
|
+
mockReaddirSync.mockImplementation(() => [{ name: "test-skill", isDirectory: () => true }])
|
|
197
|
+
mockReadFileSync.mockReturnValue(skillContent)
|
|
198
|
+
|
|
199
|
+
const skills = getSkillsInfo("/test/project")
|
|
200
|
+
expect(skills[0].description).toContain("This is the first paragraph")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("should truncate long descriptions", () => {
|
|
204
|
+
const longDescription = "A".repeat(150)
|
|
205
|
+
const skillContent = `---
|
|
206
|
+
description: ${longDescription}
|
|
207
|
+
---`
|
|
208
|
+
|
|
209
|
+
mockExistsSync.mockReturnValue(true)
|
|
210
|
+
mockReaddirSync.mockImplementation(() => [{ name: "test-skill", isDirectory: () => true }])
|
|
211
|
+
mockReadFileSync.mockReturnValue(skillContent)
|
|
212
|
+
|
|
213
|
+
const skills = getSkillsInfo("/test/project")
|
|
214
|
+
// The frontmatter description isn't truncated, only first-paragraph fallback is
|
|
215
|
+
expect(skills[0].description).toBe(longDescription)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared skill module for d3k
|
|
3
|
+
*
|
|
4
|
+
* Skills are prompt templates stored as SKILL.md files that provide
|
|
5
|
+
* specialized instructions for specific tasks.
|
|
6
|
+
*
|
|
7
|
+
* This module is used by both:
|
|
8
|
+
* - CLI: `d3k skill <name>`
|
|
9
|
+
* - MCP: `get_skill` tool
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from "fs"
|
|
13
|
+
import { dirname, join } from "path"
|
|
14
|
+
import { fileURLToPath } from "url"
|
|
15
|
+
|
|
16
|
+
export interface SkillInfo {
|
|
17
|
+
name: string
|
|
18
|
+
description: string
|
|
19
|
+
path: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SkillResult {
|
|
23
|
+
found: boolean
|
|
24
|
+
name: string
|
|
25
|
+
content?: string
|
|
26
|
+
path?: string
|
|
27
|
+
error?: string
|
|
28
|
+
availableSkills?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the base directories where skills can be found.
|
|
33
|
+
* Searches in order of priority:
|
|
34
|
+
* 1. Project-local skills (.claude/skills/)
|
|
35
|
+
* 2. Source skills (src/skills/) - for development
|
|
36
|
+
* 3. Dist skills (dist/skills/) - for installed packages
|
|
37
|
+
*/
|
|
38
|
+
export function getSkillDirectories(cwd?: string): string[] {
|
|
39
|
+
const dirs: string[] = []
|
|
40
|
+
|
|
41
|
+
// 1. Project-local skills (highest priority)
|
|
42
|
+
const projectDir = cwd || process.cwd()
|
|
43
|
+
dirs.push(join(projectDir, ".claude", "skills"))
|
|
44
|
+
|
|
45
|
+
// 2. Check if we're running from a compiled binary (bun compile)
|
|
46
|
+
// In compiled binaries, import.meta.url returns /$bunfs/... virtual path
|
|
47
|
+
// We detect this by checking if the URL starts with /$bunfs
|
|
48
|
+
const moduleUrl = import.meta.url
|
|
49
|
+
const isCompiledBinary = moduleUrl.startsWith("file:///$bunfs") || moduleUrl.startsWith("/$bunfs")
|
|
50
|
+
|
|
51
|
+
if (isCompiledBinary) {
|
|
52
|
+
// For compiled binaries, process.execPath contains the actual binary path
|
|
53
|
+
const binaryPath = process.execPath
|
|
54
|
+
if (binaryPath && existsSync(binaryPath)) {
|
|
55
|
+
const binDir = dirname(binaryPath) // bin/
|
|
56
|
+
const packageDir = dirname(binDir) // platform package root (e.g., @d3k/darwin-arm64)
|
|
57
|
+
dirs.push(join(packageDir, "skills"))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Source and dist skills from the d3k package
|
|
62
|
+
// Handle both ESM (__dirname equivalent) and different execution contexts
|
|
63
|
+
try {
|
|
64
|
+
const currentFile = fileURLToPath(import.meta.url)
|
|
65
|
+
const srcDir = dirname(currentFile) // src/skills
|
|
66
|
+
const packageRoot = dirname(dirname(srcDir)) // package root
|
|
67
|
+
|
|
68
|
+
// Source skills (for development)
|
|
69
|
+
dirs.push(join(packageRoot, "src", "skills"))
|
|
70
|
+
|
|
71
|
+
// Dist skills (for installed package)
|
|
72
|
+
dirs.push(join(packageRoot, "dist", "skills"))
|
|
73
|
+
|
|
74
|
+
// Also check if we're in a binary distribution
|
|
75
|
+
dirs.push(join(packageRoot, "skills"))
|
|
76
|
+
} catch {
|
|
77
|
+
// Fallback for CommonJS or other contexts
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return dirs.filter((dir) => existsSync(dir))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find a skill by name.
|
|
85
|
+
* Returns the path to the SKILL.md file if found.
|
|
86
|
+
*/
|
|
87
|
+
export function findSkill(name: string, cwd?: string): string | null {
|
|
88
|
+
const skillDirs = getSkillDirectories(cwd)
|
|
89
|
+
|
|
90
|
+
for (const dir of skillDirs) {
|
|
91
|
+
const skillPath = join(dir, name, "SKILL.md")
|
|
92
|
+
if (existsSync(skillPath)) {
|
|
93
|
+
return skillPath
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the content of a skill by name.
|
|
102
|
+
*/
|
|
103
|
+
export function getSkill(name: string, cwd?: string): SkillResult {
|
|
104
|
+
const skillPath = findSkill(name, cwd)
|
|
105
|
+
|
|
106
|
+
if (skillPath) {
|
|
107
|
+
try {
|
|
108
|
+
const content = readFileSync(skillPath, "utf-8")
|
|
109
|
+
return {
|
|
110
|
+
found: true,
|
|
111
|
+
name,
|
|
112
|
+
content,
|
|
113
|
+
path: skillPath
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
found: false,
|
|
118
|
+
name,
|
|
119
|
+
error: `Failed to read skill: ${error instanceof Error ? error.message : String(error)}`,
|
|
120
|
+
availableSkills: listAvailableSkills(cwd)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
found: false,
|
|
127
|
+
name,
|
|
128
|
+
error: `Skill "${name}" not found`,
|
|
129
|
+
availableSkills: listAvailableSkills(cwd)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List all available skills.
|
|
135
|
+
*/
|
|
136
|
+
export function listAvailableSkills(cwd?: string): string[] {
|
|
137
|
+
const skills = new Set<string>()
|
|
138
|
+
const skillDirs = getSkillDirectories(cwd)
|
|
139
|
+
|
|
140
|
+
for (const dir of skillDirs) {
|
|
141
|
+
try {
|
|
142
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
const skillFile = join(dir, entry.name, "SKILL.md")
|
|
146
|
+
if (existsSync(skillFile)) {
|
|
147
|
+
skills.add(entry.name)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// Directory might not exist or be readable
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return Array.from(skills).sort()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get detailed info about all available skills.
|
|
161
|
+
*/
|
|
162
|
+
export function getSkillsInfo(cwd?: string): SkillInfo[] {
|
|
163
|
+
const skillsMap = new Map<string, SkillInfo>()
|
|
164
|
+
const skillDirs = getSkillDirectories(cwd)
|
|
165
|
+
|
|
166
|
+
for (const dir of skillDirs) {
|
|
167
|
+
try {
|
|
168
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (entry.isDirectory() && !skillsMap.has(entry.name)) {
|
|
171
|
+
const skillPath = join(dir, entry.name, "SKILL.md")
|
|
172
|
+
if (existsSync(skillPath)) {
|
|
173
|
+
try {
|
|
174
|
+
const content = readFileSync(skillPath, "utf-8")
|
|
175
|
+
const description = extractDescription(content)
|
|
176
|
+
skillsMap.set(entry.name, {
|
|
177
|
+
name: entry.name,
|
|
178
|
+
description,
|
|
179
|
+
path: skillPath
|
|
180
|
+
})
|
|
181
|
+
} catch {
|
|
182
|
+
// Skip unreadable skills
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Directory might not exist or be readable
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Array.from(skillsMap.values()).sort((a, b) => a.name.localeCompare(b.name))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract description from SKILL.md frontmatter or first paragraph.
|
|
197
|
+
*/
|
|
198
|
+
function extractDescription(content: string): string {
|
|
199
|
+
// Try to extract from YAML frontmatter
|
|
200
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
201
|
+
if (frontmatterMatch) {
|
|
202
|
+
const frontmatter = frontmatterMatch[1]
|
|
203
|
+
const descMatch = frontmatter.match(/description:\s*(.+)/)
|
|
204
|
+
if (descMatch) {
|
|
205
|
+
return descMatch[1].trim()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fallback: use first non-heading, non-empty line
|
|
210
|
+
const lines = content.split("\n")
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
const trimmed = line.trim()
|
|
213
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
|
|
214
|
+
return trimmed.slice(0, 100) + (trimmed.length > 100 ? "..." : "")
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return "No description available"
|
|
219
|
+
}
|