@ian2018cs/agenthub 0.1.34 → 0.1.35
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/assets/{index-Dtp8bFzY.js → index-SA2TCeJ4.js} +6 -6
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/routes/auth.js +3 -3
- package/server/routes/skills.js +10 -10
- package/server/services/system-repo.js +116 -0
- package/server/services/user-directories.js +3 -3
- package/server/builtin-skills/.gitkeep +0 -0
- package/server/builtin-skills/html-deploy/SKILL.md +0 -40
- package/server/builtin-skills/html-deploy/scripts/delete.py +0 -31
- package/server/builtin-skills/html-deploy/scripts/deploy.py +0 -36
- package/server/builtin-skills/html-deploy/scripts/update.py +0 -38
- package/server/services/builtin-skills.js +0 -182
package/dist/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-SA2TCeJ4.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BeVl62c0.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C_VWDoZS.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
|
package/package.json
CHANGED
package/server/routes/auth.js
CHANGED
|
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
5
5
|
import { userDb, verificationDb, domainWhitelistDb, usageDb, settingsDb } from '../database/db.js';
|
|
6
6
|
import { generateToken, authenticateToken, JWT_SECRET } from '../middleware/auth.js';
|
|
7
7
|
import { initUserDirectories } from '../services/user-directories.js';
|
|
8
|
-
import {
|
|
8
|
+
import { initSystemRepoForUser } from '../services/system-repo.js';
|
|
9
9
|
import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
|
|
10
10
|
|
|
11
11
|
const router = express.Router();
|
|
@@ -126,8 +126,8 @@ router.post('/verify-code', async (req, res) => {
|
|
|
126
126
|
// Initialize user directories
|
|
127
127
|
await initUserDirectories(uuid);
|
|
128
128
|
} else {
|
|
129
|
-
//
|
|
130
|
-
await
|
|
129
|
+
// Ensure system repo is linked for existing user
|
|
130
|
+
await initSystemRepoForUser(user.uuid);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// Check if user is disabled
|
package/server/routes/skills.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawn } from 'child_process';
|
|
|
5
5
|
import multer from 'multer';
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths, DATA_DIR } from '../services/user-directories.js';
|
|
8
|
-
import {
|
|
8
|
+
import { SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME } from '../services/system-repo.js';
|
|
9
9
|
|
|
10
10
|
const router = express.Router();
|
|
11
11
|
|
|
@@ -328,8 +328,6 @@ router.get('/', async (req, res) => {
|
|
|
328
328
|
if (repoMatch) {
|
|
329
329
|
repository = `${repoMatch[1]}/${repoMatch[2]}`;
|
|
330
330
|
}
|
|
331
|
-
} else if (realPath.includes('/builtin-skills/')) {
|
|
332
|
-
source = 'builtin';
|
|
333
331
|
}
|
|
334
332
|
}
|
|
335
333
|
|
|
@@ -467,7 +465,6 @@ router.delete('/:name', async (req, res) => {
|
|
|
467
465
|
let realPath = null;
|
|
468
466
|
let isSymlink = false;
|
|
469
467
|
let isImported = false;
|
|
470
|
-
let isBuiltin = false;
|
|
471
468
|
|
|
472
469
|
try {
|
|
473
470
|
const stat = await fs.lstat(linkPath);
|
|
@@ -475,7 +472,6 @@ router.delete('/:name', async (req, res) => {
|
|
|
475
472
|
if (isSymlink) {
|
|
476
473
|
realPath = await fs.realpath(linkPath);
|
|
477
474
|
isImported = realPath.includes('/skills-import/');
|
|
478
|
-
isBuiltin = isBuiltinSkillPath(realPath);
|
|
479
475
|
}
|
|
480
476
|
} catch (err) {
|
|
481
477
|
return res.status(404).json({ error: 'Skill not found' });
|
|
@@ -496,9 +492,6 @@ router.delete('/:name', async (req, res) => {
|
|
|
496
492
|
} catch (err) {
|
|
497
493
|
console.error('Error removing imported skill files:', err);
|
|
498
494
|
}
|
|
499
|
-
} else if (isBuiltin) {
|
|
500
|
-
// Mark as removed so it won't be re-added on next sync
|
|
501
|
-
await markBuiltinSkillRemoved(userUuid, name);
|
|
502
495
|
}
|
|
503
496
|
|
|
504
497
|
res.json({ success: true, message: 'Skill deleted' });
|
|
@@ -793,13 +786,15 @@ router.get('/repos', async (req, res) => {
|
|
|
793
786
|
// Count skills in repo (supports nested dirs like skills/XXX)
|
|
794
787
|
const repoSkills = await scanDirForSkills(realPath, `${owner}/${repo}`, null);
|
|
795
788
|
const skillCount = repoSkills.length;
|
|
789
|
+
const isSystem = owner === SYSTEM_REPO_OWNER && repo === SYSTEM_REPO_NAME;
|
|
796
790
|
|
|
797
791
|
repos.push({
|
|
798
792
|
owner,
|
|
799
793
|
repo,
|
|
800
|
-
url: `https://github.com/${owner}/${repo}`,
|
|
794
|
+
url: isSystem ? null : `https://github.com/${owner}/${repo}`,
|
|
801
795
|
skillCount,
|
|
802
|
-
path: realPath
|
|
796
|
+
path: realPath,
|
|
797
|
+
isSystem
|
|
803
798
|
});
|
|
804
799
|
}
|
|
805
800
|
}
|
|
@@ -988,6 +983,11 @@ router.delete('/repos/:owner/:repo', async (req, res) => {
|
|
|
988
983
|
return res.status(400).json({ error: 'Owner and repo are required' });
|
|
989
984
|
}
|
|
990
985
|
|
|
986
|
+
// System repo cannot be removed
|
|
987
|
+
if (owner === SYSTEM_REPO_OWNER && repo === SYSTEM_REPO_NAME) {
|
|
988
|
+
return res.status(403).json({ error: '内置仓库不能删除' });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
991
|
const userPaths = getUserPaths(userUuid);
|
|
992
992
|
const userRepoPath = path.join(userPaths.skillsRepoDir, owner, repo);
|
|
993
993
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { getUserPaths, getPublicPaths } from './user-directories.js';
|
|
5
|
+
|
|
6
|
+
const SYSTEM_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-skills.git';
|
|
7
|
+
const SYSTEM_REPO_OWNER = 'mcp-server';
|
|
8
|
+
const SYSTEM_REPO_NAME = 'hupoer-skills';
|
|
9
|
+
|
|
10
|
+
function cloneRepository(url, destinationPath) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const gitProcess = spawn('git', ['clone', '--depth', '1', url, destinationPath], {
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let stderr = '';
|
|
18
|
+
gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
19
|
+
|
|
20
|
+
gitProcess.on('close', (code) => {
|
|
21
|
+
if (code === 0) resolve();
|
|
22
|
+
else reject(new Error(stderr || `Git clone failed with code ${code}`));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
gitProcess.on('error', (err) => { reject(err); });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function updateRepository(repoPath) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const gitProcess = spawn('git', ['pull', '--ff-only'], {
|
|
32
|
+
cwd: repoPath,
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let stderr = '';
|
|
38
|
+
gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
39
|
+
|
|
40
|
+
gitProcess.on('close', (code) => {
|
|
41
|
+
if (code === 0) resolve();
|
|
42
|
+
else reject(new Error(stderr || `Git pull failed with code ${code}`));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
gitProcess.on('error', (err) => { reject(err); });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ensure the system repo is cloned to the shared public directory.
|
|
51
|
+
* If already cloned, attempts to pull latest changes.
|
|
52
|
+
* Returns the path to the public clone.
|
|
53
|
+
*/
|
|
54
|
+
async function ensureSystemRepo() {
|
|
55
|
+
const publicPaths = getPublicPaths();
|
|
56
|
+
const publicRepoPath = path.join(publicPaths.skillsRepoDir, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(publicRepoPath);
|
|
60
|
+
// Already cloned — try to update
|
|
61
|
+
try {
|
|
62
|
+
await updateRepository(publicRepoPath);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.log('[SystemRepo] Failed to pull system repo, using existing clone:', err.message);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Not yet cloned
|
|
68
|
+
await fs.mkdir(path.dirname(publicRepoPath), { recursive: true });
|
|
69
|
+
try {
|
|
70
|
+
await cloneRepository(SYSTEM_REPO_URL, publicRepoPath);
|
|
71
|
+
console.log('[SystemRepo] Cloned system repo to', publicRepoPath);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('[SystemRepo] Failed to clone system repo:', err.message);
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return publicRepoPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Initialize the system repo for a user.
|
|
83
|
+
* Idempotent: if the user already has the symlink, skip.
|
|
84
|
+
*/
|
|
85
|
+
export async function initSystemRepoForUser(userUuid) {
|
|
86
|
+
const userPaths = getUserPaths(userUuid);
|
|
87
|
+
const userRepoPath = path.join(userPaths.skillsRepoDir, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME);
|
|
88
|
+
|
|
89
|
+
// If symlink/dir already exists, skip
|
|
90
|
+
try {
|
|
91
|
+
await fs.lstat(userRepoPath);
|
|
92
|
+
// Already set up — nothing to do
|
|
93
|
+
return;
|
|
94
|
+
} catch {
|
|
95
|
+
// Doesn't exist — proceed with setup
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let publicRepoPath;
|
|
99
|
+
try {
|
|
100
|
+
publicRepoPath = await ensureSystemRepo();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[SystemRepo] Could not ensure system repo, skipping user init:', err.message);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create user symlink
|
|
107
|
+
await fs.mkdir(path.dirname(userRepoPath), { recursive: true });
|
|
108
|
+
try {
|
|
109
|
+
await fs.symlink(publicRepoPath, userRepoPath);
|
|
110
|
+
console.log(`[SystemRepo] Linked system repo for user ${userUuid}`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`[SystemRepo] Failed to create symlink for user ${userUuid}:`, err.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME, SYSTEM_REPO_URL };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { initSystemRepoForUser } from './system-repo.js';
|
|
4
4
|
|
|
5
5
|
// Base data directory (configurable via env)
|
|
6
6
|
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
|
@@ -280,8 +280,8 @@ export async function initUserDirectories(userUuid) {
|
|
|
280
280
|
await fs.writeFile(usageScanStatePath, JSON.stringify(scanState, null, 2));
|
|
281
281
|
console.log(`Created .usage-scan-state.json for user ${userUuid}`);
|
|
282
282
|
|
|
283
|
-
// Initialize built-in
|
|
284
|
-
await
|
|
283
|
+
// Initialize system repo (built-in skill repository)
|
|
284
|
+
await initSystemRepoForUser(userUuid);
|
|
285
285
|
|
|
286
286
|
return paths;
|
|
287
287
|
}
|
|
File without changes
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: html-deploy
|
|
3
|
-
description: 将 HTML 内容部署为可访问的网页。使用场景:(1) 用户要求将生成的 HTML/网页部署上线或发布,(2) 用户要求更新已部署的页面内容,(3) 用户要求删除已部署的页面,(4) 用户提到"部署"、"发布"、"上线"HTML 页面,(5) 用户要求预览生成的网页效果。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# HTML Deploy
|
|
7
|
-
|
|
8
|
-
将 HTML 内容部署到远程服务,生成可访问的 URL。
|
|
9
|
-
|
|
10
|
-
## 使用流程
|
|
11
|
-
|
|
12
|
-
1. 根据用户需求生成完整的 HTML 页面,写入文件
|
|
13
|
-
2. 调用脚本部署、更新或删除,传入相应参数
|
|
14
|
-
3. 将结果告知用户
|
|
15
|
-
|
|
16
|
-
### 部署新页面
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
python3 skills/html-deploy/scripts/deploy.py <html_file>
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### 更新已有页面
|
|
23
|
-
|
|
24
|
-
从之前的部署 URL 中提取 page_id(UUID 部分):
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
python3 skills/html-deploy/scripts/update.py <page_id> <html_file>
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
page_id 不存在时会报 404 错误。
|
|
31
|
-
|
|
32
|
-
### 删除已有页面
|
|
33
|
-
|
|
34
|
-
从之前的部署 URL 中提取 page_id(UUID 部分):
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
python3 skills/html-deploy/scripts/delete.py <page_id>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
page_id 不存在时会报 404 错误。
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""删除已部署的页面。
|
|
3
|
-
|
|
4
|
-
用法: delete.py <page_id>
|
|
5
|
-
输出: 删除结果
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
import json
|
|
9
|
-
import urllib.request
|
|
10
|
-
|
|
11
|
-
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main():
|
|
15
|
-
if len(sys.argv) != 2:
|
|
16
|
-
print(f"用法: {sys.argv[0]} <page_id>", file=sys.stderr)
|
|
17
|
-
sys.exit(1)
|
|
18
|
-
|
|
19
|
-
page_id = sys.argv[1]
|
|
20
|
-
req = urllib.request.Request(
|
|
21
|
-
f"{API_BASE}/deploy/{page_id}",
|
|
22
|
-
method="DELETE",
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
with urllib.request.urlopen(req) as resp:
|
|
26
|
-
result = json.loads(resp.read())
|
|
27
|
-
print(result["detail"])
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if __name__ == "__main__":
|
|
31
|
-
main()
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""部署 HTML 文件为网页。
|
|
3
|
-
|
|
4
|
-
用法: deploy.py <html_file>
|
|
5
|
-
输出: 部署成功后的访问 URL
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
import json
|
|
9
|
-
import urllib.request
|
|
10
|
-
|
|
11
|
-
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main():
|
|
15
|
-
if len(sys.argv) != 2:
|
|
16
|
-
print(f"用法: {sys.argv[0]} <html_file>", file=sys.stderr)
|
|
17
|
-
sys.exit(1)
|
|
18
|
-
|
|
19
|
-
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
|
20
|
-
html = f.read()
|
|
21
|
-
|
|
22
|
-
data = json.dumps({"html": html}).encode("utf-8")
|
|
23
|
-
req = urllib.request.Request(
|
|
24
|
-
f"{API_BASE}/deploy",
|
|
25
|
-
data=data,
|
|
26
|
-
headers={"Content-Type": "application/json"},
|
|
27
|
-
method="POST",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
with urllib.request.urlopen(req) as resp:
|
|
31
|
-
result = json.loads(resp.read())
|
|
32
|
-
print(result["url"])
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if __name__ == "__main__":
|
|
36
|
-
main()
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""更新已部署的页面内容。
|
|
3
|
-
|
|
4
|
-
用法: update.py <page_id> <html_file>
|
|
5
|
-
输出: 更新成功后的访问 URL
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
import json
|
|
9
|
-
import urllib.request
|
|
10
|
-
|
|
11
|
-
API_BASE = "http://10.0.0.252:11004"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main():
|
|
15
|
-
if len(sys.argv) != 3:
|
|
16
|
-
print(f"用法: {sys.argv[0]} <page_id> <html_file>", file=sys.stderr)
|
|
17
|
-
sys.exit(1)
|
|
18
|
-
|
|
19
|
-
page_id = sys.argv[1]
|
|
20
|
-
|
|
21
|
-
with open(sys.argv[2], "r", encoding="utf-8") as f:
|
|
22
|
-
html = f.read()
|
|
23
|
-
|
|
24
|
-
data = json.dumps({"html": html}).encode("utf-8")
|
|
25
|
-
req = urllib.request.Request(
|
|
26
|
-
f"{API_BASE}/deploy/{page_id}",
|
|
27
|
-
data=data,
|
|
28
|
-
headers={"Content-Type": "application/json"},
|
|
29
|
-
method="PUT",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
with urllib.request.urlopen(req) as resp:
|
|
33
|
-
result = json.loads(resp.read())
|
|
34
|
-
print(result["url"])
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if __name__ == "__main__":
|
|
38
|
-
main()
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { getUserPaths } from './user-directories.js';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
// Path to built-in skills directory
|
|
10
|
-
const BUILTIN_SKILLS_DIR = path.join(__dirname, '../builtin-skills');
|
|
11
|
-
|
|
12
|
-
// State file version for future migrations
|
|
13
|
-
const STATE_VERSION = 1;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get list of all available built-in skills
|
|
17
|
-
*/
|
|
18
|
-
export async function getBuiltinSkills() {
|
|
19
|
-
const skills = [];
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const entries = await fs.readdir(BUILTIN_SKILLS_DIR, { withFileTypes: true });
|
|
23
|
-
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
26
|
-
const skillPath = path.join(BUILTIN_SKILLS_DIR, entry.name);
|
|
27
|
-
|
|
28
|
-
// Check for SKILLS.md or SKILL.md
|
|
29
|
-
const skillsFile = path.join(skillPath, 'SKILLS.md');
|
|
30
|
-
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Try SKILLS.md first, then SKILL.md
|
|
34
|
-
let found = false;
|
|
35
|
-
try {
|
|
36
|
-
await fs.access(skillsFile);
|
|
37
|
-
found = true;
|
|
38
|
-
} catch {
|
|
39
|
-
await fs.access(skillFile);
|
|
40
|
-
found = true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (found) {
|
|
44
|
-
skills.push({
|
|
45
|
-
name: entry.name,
|
|
46
|
-
path: skillPath
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
// Skip if neither file exists
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
if (err.code !== 'ENOENT') {
|
|
56
|
-
console.error('Error reading builtin skills:', err);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return skills;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get path to user's builtin skills state file
|
|
65
|
-
*/
|
|
66
|
-
function getStatePath(userUuid) {
|
|
67
|
-
const userPaths = getUserPaths(userUuid);
|
|
68
|
-
return path.join(userPaths.claudeDir, '.builtin-skills-state.json');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Load user's builtin skills state
|
|
73
|
-
*/
|
|
74
|
-
export async function loadBuiltinSkillsState(userUuid) {
|
|
75
|
-
try {
|
|
76
|
-
const statePath = getStatePath(userUuid);
|
|
77
|
-
const content = await fs.readFile(statePath, 'utf-8');
|
|
78
|
-
return JSON.parse(content);
|
|
79
|
-
} catch {
|
|
80
|
-
return {
|
|
81
|
-
version: STATE_VERSION,
|
|
82
|
-
removedSkills: []
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Save user's builtin skills state
|
|
89
|
-
*/
|
|
90
|
-
export async function saveBuiltinSkillsState(userUuid, state) {
|
|
91
|
-
const statePath = getStatePath(userUuid);
|
|
92
|
-
await fs.writeFile(statePath, JSON.stringify(state, null, 2));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Initialize built-in skills for a user
|
|
97
|
-
* Creates symlinks for all built-in skills not in user's removed list
|
|
98
|
-
* Also cleans up dangling symlinks from deleted built-in skills
|
|
99
|
-
*/
|
|
100
|
-
export async function initBuiltinSkills(userUuid) {
|
|
101
|
-
const userPaths = getUserPaths(userUuid);
|
|
102
|
-
const builtinSkills = await getBuiltinSkills();
|
|
103
|
-
const state = await loadBuiltinSkillsState(userUuid);
|
|
104
|
-
// Clean up dangling symlinks pointing to removed built-in skills
|
|
105
|
-
try {
|
|
106
|
-
const entries = await fs.readdir(userPaths.skillsDir, { withFileTypes: true });
|
|
107
|
-
for (const entry of entries) {
|
|
108
|
-
const entryPath = path.join(userPaths.skillsDir, entry.name);
|
|
109
|
-
try {
|
|
110
|
-
const stat = await fs.lstat(entryPath);
|
|
111
|
-
if (!stat.isSymbolicLink()) continue;
|
|
112
|
-
|
|
113
|
-
const target = await fs.readlink(entryPath);
|
|
114
|
-
const realBuiltinDir = await fs.realpath(BUILTIN_SKILLS_DIR);
|
|
115
|
-
const resolvedTarget = path.resolve(path.dirname(entryPath), target);
|
|
116
|
-
|
|
117
|
-
// Only clean up symlinks that point into builtin-skills directory
|
|
118
|
-
if (!resolvedTarget.startsWith(realBuiltinDir)) continue;
|
|
119
|
-
|
|
120
|
-
// Check if the symlink target still exists
|
|
121
|
-
try {
|
|
122
|
-
await fs.access(resolvedTarget);
|
|
123
|
-
} catch {
|
|
124
|
-
// Target no longer exists, remove the dangling symlink
|
|
125
|
-
await fs.unlink(entryPath);
|
|
126
|
-
console.log(`Removed dangling builtin skill symlink: ${entry.name}`);
|
|
127
|
-
}
|
|
128
|
-
} catch (err) {
|
|
129
|
-
// Ignore errors on individual entries
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} catch (err) {
|
|
133
|
-
if (err.code !== 'ENOENT') {
|
|
134
|
-
console.error('Error cleaning up builtin skill symlinks:', err);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Create symlinks for new built-in skills
|
|
139
|
-
for (const skill of builtinSkills) {
|
|
140
|
-
// Skip if user has explicitly removed this skill
|
|
141
|
-
if (state.removedSkills.includes(skill.name)) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const linkPath = path.join(userPaths.skillsDir, skill.name);
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
// Check if link already exists
|
|
149
|
-
await fs.lstat(linkPath);
|
|
150
|
-
// Link exists, skip
|
|
151
|
-
} catch {
|
|
152
|
-
// Link doesn't exist, create it
|
|
153
|
-
try {
|
|
154
|
-
await fs.symlink(skill.path, linkPath);
|
|
155
|
-
console.log(`Created builtin skill symlink: ${skill.name}`);
|
|
156
|
-
} catch (err) {
|
|
157
|
-
console.error(`Error creating builtin skill symlink ${skill.name}:`, err);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Mark a built-in skill as removed by user
|
|
165
|
-
*/
|
|
166
|
-
export async function markBuiltinSkillRemoved(userUuid, skillName) {
|
|
167
|
-
const state = await loadBuiltinSkillsState(userUuid);
|
|
168
|
-
|
|
169
|
-
if (!state.removedSkills.includes(skillName)) {
|
|
170
|
-
state.removedSkills.push(skillName);
|
|
171
|
-
await saveBuiltinSkillsState(userUuid, state);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Check if a path is a built-in skill
|
|
177
|
-
*/
|
|
178
|
-
export function isBuiltinSkillPath(realPath) {
|
|
179
|
-
return realPath?.includes('/builtin-skills/') ?? false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export { BUILTIN_SKILLS_DIR };
|