@ian2018cs/agenthub 0.1.74 → 0.1.75
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-B5imuFpg.js +192 -0
- package/dist/assets/index-oUz7uC99.css +32 -0
- package/dist/assets/{vendor-icons-Db5w6yrw.js → vendor-icons-DUmFlkZ8.js} +81 -71
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/server/claude-sdk.js +12 -0
- package/server/index.js +28 -5
- package/server/projects.js +18 -0
- package/server/routes/agents.js +23 -7
- package/server/routes/mcp-repos.js +22 -4
- package/server/routes/skills.js +59 -5
- package/dist/assets/index-C9CP8AkI.js +0 -192
- package/dist/assets/index-nfVK3s5x.css +0 -32
package/server/index.js
CHANGED
|
@@ -670,8 +670,9 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
670
670
|
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
671
671
|
try {
|
|
672
672
|
// Query parameters for lazy loading
|
|
673
|
-
const { dirPath, depth = '1' } = req.query;
|
|
673
|
+
const { dirPath, depth = '1', showHidden = 'false' } = req.query;
|
|
674
674
|
const maxDepth = Math.min(parseInt(depth) || 1, 10); // Limit max depth to 10
|
|
675
|
+
const showHiddenFiles = showHidden === 'true';
|
|
675
676
|
|
|
676
677
|
// Use extractProjectDirectory to get the actual project path
|
|
677
678
|
let projectPath;
|
|
@@ -701,7 +702,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
701
702
|
return res.status(404).json({ error: `Path not found: ${targetPath}` });
|
|
702
703
|
}
|
|
703
704
|
|
|
704
|
-
const files = await getFileTree(targetPath, maxDepth, 0,
|
|
705
|
+
const files = await getFileTree(targetPath, maxDepth, 0, showHiddenFiles);
|
|
705
706
|
res.json(files);
|
|
706
707
|
} catch (error) {
|
|
707
708
|
console.error('[ERROR] File tree error:', error.message);
|
|
@@ -2361,14 +2362,36 @@ async function getFileTree(dirPath, maxDepth = 1, currentDepth = 0, showHidden =
|
|
|
2361
2362
|
for (const entry of entries) {
|
|
2362
2363
|
// Skip heavy build directories and VCS directories
|
|
2363
2364
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
2365
|
+
// Skip hidden files/dirs when showHidden is false
|
|
2366
|
+
if (!showHidden && entry.name.startsWith('.')) continue;
|
|
2364
2367
|
|
|
2365
2368
|
const itemPath = path.join(dirPath, entry.name);
|
|
2369
|
+
const isSymlink = entry.isSymbolicLink();
|
|
2366
2370
|
const item = {
|
|
2367
2371
|
name: entry.name,
|
|
2368
2372
|
path: itemPath,
|
|
2369
|
-
type: entry.isDirectory() ? 'directory' : 'file'
|
|
2373
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
2374
|
+
isSymlink,
|
|
2370
2375
|
};
|
|
2371
2376
|
|
|
2377
|
+
// Resolve symlink target and actual type
|
|
2378
|
+
if (isSymlink) {
|
|
2379
|
+
try {
|
|
2380
|
+
item.symlinkTarget = await fsPromises.readlink(itemPath);
|
|
2381
|
+
} catch {
|
|
2382
|
+
item.symlinkTarget = null;
|
|
2383
|
+
}
|
|
2384
|
+
try {
|
|
2385
|
+
// stat() follows the symlink to get target info
|
|
2386
|
+
const targetStats = await fsPromises.stat(itemPath);
|
|
2387
|
+
item.type = targetStats.isDirectory() ? 'directory' : 'file';
|
|
2388
|
+
} catch {
|
|
2389
|
+
// Broken symlink — keep as 'file', mark broken
|
|
2390
|
+
item.type = 'file';
|
|
2391
|
+
item.isBrokenSymlink = true;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2372
2395
|
// Get file stats for additional metadata
|
|
2373
2396
|
try {
|
|
2374
2397
|
const stats = await fsPromises.stat(itemPath);
|
|
@@ -2383,14 +2406,14 @@ async function getFileTree(dirPath, maxDepth = 1, currentDepth = 0, showHidden =
|
|
|
2383
2406
|
item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
|
|
2384
2407
|
item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
|
|
2385
2408
|
} catch (statError) {
|
|
2386
|
-
// If stat fails, provide default values
|
|
2409
|
+
// If stat fails (e.g. broken symlink), provide default values
|
|
2387
2410
|
item.size = 0;
|
|
2388
2411
|
item.modified = null;
|
|
2389
2412
|
item.permissions = '000';
|
|
2390
2413
|
item.permissionsRwx = '---------';
|
|
2391
2414
|
}
|
|
2392
2415
|
|
|
2393
|
-
if (
|
|
2416
|
+
if (item.type === 'directory') {
|
|
2394
2417
|
// For lazy loading: check if directory has children without loading them
|
|
2395
2418
|
if (currentDepth >= maxDepth) {
|
|
2396
2419
|
// Don't load children, just check if they exist
|
package/server/projects.js
CHANGED
|
@@ -903,6 +903,9 @@ async function deleteProject(projectName, userUuid, options = {}) {
|
|
|
903
903
|
actualProjectDir = await extractProjectDirectory(projectName, userUuid);
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
+
// Also resolve the actual path to clean up .claude.json project-scoped MCPs
|
|
907
|
+
const resolvedProjectDir = actualProjectDir || await extractProjectDirectory(projectName, userUuid).catch(() => null);
|
|
908
|
+
|
|
906
909
|
// Remove the Claude sessions directory
|
|
907
910
|
try {
|
|
908
911
|
await fs.rm(sessionDir, { recursive: true, force: true });
|
|
@@ -917,6 +920,21 @@ async function deleteProject(projectName, userUuid, options = {}) {
|
|
|
917
920
|
delete config[projectName];
|
|
918
921
|
await saveProjectConfig(config, userUuid);
|
|
919
922
|
|
|
923
|
+
// Clean up project-scoped MCP servers from .claude.json
|
|
924
|
+
if (resolvedProjectDir) {
|
|
925
|
+
try {
|
|
926
|
+
const claudeJsonPath = path.join(getUserPaths(userUuid).claudeDir, '.claude.json');
|
|
927
|
+
const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
928
|
+
if (claudeConfig.projects?.[resolvedProjectDir]) {
|
|
929
|
+
delete claudeConfig.projects[resolvedProjectDir];
|
|
930
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
931
|
+
}
|
|
932
|
+
} catch (err) {
|
|
933
|
+
// Non-fatal: log and continue
|
|
934
|
+
console.error(`[deleteProject] Failed to clean up .claude.json for ${resolvedProjectDir}:`, err.message);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
920
938
|
// Optionally delete the actual project folder
|
|
921
939
|
if (deleteFolder && actualProjectDir) {
|
|
922
940
|
try {
|
package/server/routes/agents.js
CHANGED
|
@@ -197,7 +197,7 @@ async function ensureSkillRepo(repoUrl, userUuid) {
|
|
|
197
197
|
/**
|
|
198
198
|
* Install a single skill by name from a given repo URL.
|
|
199
199
|
*/
|
|
200
|
-
async function installSkill(skillName, repoUrl, userUuid) {
|
|
200
|
+
async function installSkill(skillName, repoUrl, userUuid, projectPath) {
|
|
201
201
|
const publicRepoPath = await ensureSkillRepo(repoUrl, userUuid);
|
|
202
202
|
const userPaths = getUserPaths(userUuid);
|
|
203
203
|
|
|
@@ -208,7 +208,11 @@ async function installSkill(skillName, repoUrl, userUuid) {
|
|
|
208
208
|
return false;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
const
|
|
211
|
+
const targetDir = projectPath
|
|
212
|
+
? path.join(projectPath, '.claude', 'skills')
|
|
213
|
+
: userPaths.skillsDir;
|
|
214
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
215
|
+
const linkPath = path.join(targetDir, skillName);
|
|
212
216
|
try { await fs.unlink(linkPath); } catch {}
|
|
213
217
|
await fs.symlink(skillPath, linkPath);
|
|
214
218
|
return true;
|
|
@@ -359,8 +363,9 @@ async function ensureMcpRepo(repoUrl, userUuid) {
|
|
|
359
363
|
|
|
360
364
|
/**
|
|
361
365
|
* Install a single MCP server by service dir name from a given repo URL.
|
|
366
|
+
* If projectPath is provided, installs as project-scoped (local); otherwise user-scoped.
|
|
362
367
|
*/
|
|
363
|
-
async function installMcp(mcpName, repoUrl, userUuid) {
|
|
368
|
+
async function installMcp(mcpName, repoUrl, userUuid, projectPath) {
|
|
364
369
|
const publicRepoPath = await ensureMcpRepo(repoUrl, userUuid);
|
|
365
370
|
const userPaths = getUserPaths(userUuid);
|
|
366
371
|
|
|
@@ -383,7 +388,18 @@ async function installMcp(mcpName, repoUrl, userUuid) {
|
|
|
383
388
|
} catch {
|
|
384
389
|
claudeConfig = { hasCompletedOnboarding: true };
|
|
385
390
|
}
|
|
386
|
-
|
|
391
|
+
|
|
392
|
+
if (projectPath) {
|
|
393
|
+
// Project-scoped: write to projects[projectPath].mcpServers
|
|
394
|
+
if (!claudeConfig.projects) claudeConfig.projects = {};
|
|
395
|
+
if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
|
|
396
|
+
if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
|
|
397
|
+
Object.assign(claudeConfig.projects[projectPath].mcpServers, mcpJson);
|
|
398
|
+
} else {
|
|
399
|
+
// User-scoped (fallback)
|
|
400
|
+
claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), ...mcpJson };
|
|
401
|
+
}
|
|
402
|
+
|
|
387
403
|
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
388
404
|
return true;
|
|
389
405
|
}
|
|
@@ -549,7 +565,7 @@ router.post('/install', async (req, res) => {
|
|
|
549
565
|
for (const skill of agent.skills || []) {
|
|
550
566
|
if (!skill.name || !skill.repo) continue;
|
|
551
567
|
try {
|
|
552
|
-
const ok = await installSkill(skill.name, skill.repo, userUuid);
|
|
568
|
+
const ok = await installSkill(skill.name, skill.repo, userUuid, projectDir);
|
|
553
569
|
skillResults.push({ name: skill.name, success: ok });
|
|
554
570
|
} catch (err) {
|
|
555
571
|
console.error(`[AgentInstall] Failed to install skill "${skill.name}":`, err.message);
|
|
@@ -557,12 +573,12 @@ router.post('/install', async (req, res) => {
|
|
|
557
573
|
}
|
|
558
574
|
}
|
|
559
575
|
|
|
560
|
-
// Install MCPs
|
|
576
|
+
// Install MCPs (project-scoped to the newly created project directory)
|
|
561
577
|
const mcpResults = [];
|
|
562
578
|
for (const mcp of agent.mcps || []) {
|
|
563
579
|
if (!mcp.name || !mcp.repo) continue;
|
|
564
580
|
try {
|
|
565
|
-
const ok = await installMcp(mcp.name, mcp.repo, userUuid);
|
|
581
|
+
const ok = await installMcp(mcp.name, mcp.repo, userUuid, projectDir);
|
|
566
582
|
mcpResults.push({ name: mcp.name, success: ok });
|
|
567
583
|
} catch (err) {
|
|
568
584
|
console.error(`[AgentInstall] Failed to install MCP "${mcp.name}":`, err.message);
|
|
@@ -451,7 +451,7 @@ router.post('/install', async (req, res) => {
|
|
|
451
451
|
const userUuid = req.user?.uuid;
|
|
452
452
|
if (!userUuid) return res.status(401).json({ error: 'User authentication required' });
|
|
453
453
|
|
|
454
|
-
const { servicePath, scope = 'user' } = req.body;
|
|
454
|
+
const { servicePath, scope = 'user', projectPath } = req.body;
|
|
455
455
|
if (!servicePath) return res.status(400).json({ error: 'servicePath is required' });
|
|
456
456
|
|
|
457
457
|
// Security: ensure servicePath is within mcp-repo directory (public or user symlink dir)
|
|
@@ -476,11 +476,29 @@ router.post('/install', async (req, res) => {
|
|
|
476
476
|
const installedServers = [];
|
|
477
477
|
const errors = [];
|
|
478
478
|
|
|
479
|
-
// Install each server entry
|
|
479
|
+
// Install each server entry
|
|
480
480
|
for (const [serverName, serverConfig] of Object.entries(mcpJson)) {
|
|
481
481
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
if (scope === 'local' && projectPath) {
|
|
483
|
+
// Bypass Claude CLI for local scope: directly write to .claude.json projects[projectPath].mcpServers
|
|
484
|
+
// (Claude CLI --scope local uses git root as key, which breaks in multi-user deployments)
|
|
485
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
486
|
+
let claudeConfig = {};
|
|
487
|
+
try {
|
|
488
|
+
claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
489
|
+
} catch {
|
|
490
|
+
claudeConfig = { hasCompletedOnboarding: true };
|
|
491
|
+
}
|
|
492
|
+
if (!claudeConfig.projects) claudeConfig.projects = {};
|
|
493
|
+
if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
|
|
494
|
+
if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
|
|
495
|
+
claudeConfig.projects[projectPath].mcpServers[serverName] = serverConfig;
|
|
496
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
497
|
+
installedServers.push(serverName);
|
|
498
|
+
} else {
|
|
499
|
+
await addMcpServerViaJson(serverName, serverConfig, scope, userPaths.claudeDir);
|
|
500
|
+
installedServers.push(serverName);
|
|
501
|
+
}
|
|
484
502
|
} catch (err) {
|
|
485
503
|
console.error(`[MCP Repos] CLI install failed for "${serverName}", using fallback:`, err.message);
|
|
486
504
|
// Fallback: directly write to .claude.json
|
package/server/routes/skills.js
CHANGED
|
@@ -6,6 +6,7 @@ import multer from 'multer';
|
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import { getUserPaths, getPublicPaths, DATA_DIR } from '../services/user-directories.js';
|
|
8
8
|
import { ensureSystemRepo, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME, SYSTEM_REPO_URL } from '../services/system-repo.js';
|
|
9
|
+
import { loadProjectConfig, extractProjectDirectory } from '../projects.js';
|
|
9
10
|
|
|
10
11
|
const router = express.Router();
|
|
11
12
|
|
|
@@ -443,13 +444,54 @@ router.get('/', async (req, res) => {
|
|
|
443
444
|
enabled: true,
|
|
444
445
|
source,
|
|
445
446
|
repository,
|
|
446
|
-
path: realPath
|
|
447
|
+
path: realPath,
|
|
448
|
+
scope: 'user'
|
|
447
449
|
});
|
|
448
450
|
} catch (err) {
|
|
449
451
|
console.error(`Error reading skill ${entry.name}:`, err.message);
|
|
450
452
|
}
|
|
451
453
|
}
|
|
452
454
|
|
|
455
|
+
// Also scan project-scoped skills from each project's .claude/skills/
|
|
456
|
+
try {
|
|
457
|
+
const projectConfig = await loadProjectConfig(userUuid);
|
|
458
|
+
for (const projectName of Object.keys(projectConfig)) {
|
|
459
|
+
let projectDir = null;
|
|
460
|
+
try { projectDir = await extractProjectDirectory(projectName, userUuid); } catch { continue; }
|
|
461
|
+
if (!projectDir) continue;
|
|
462
|
+
const projectSkillsDir = path.join(projectDir, '.claude', 'skills');
|
|
463
|
+
let projectEntries;
|
|
464
|
+
try { projectEntries = await fs.readdir(projectSkillsDir, { withFileTypes: true }); } catch { continue; }
|
|
465
|
+
for (const entry of projectEntries) {
|
|
466
|
+
if (entry.name.startsWith('.') || entry.name.toLowerCase().startsWith('readme')) continue;
|
|
467
|
+
const skillPath = path.join(projectSkillsDir, entry.name);
|
|
468
|
+
let realPath = skillPath;
|
|
469
|
+
let source = 'repo';
|
|
470
|
+
let repository = null;
|
|
471
|
+
try {
|
|
472
|
+
const stat = await fs.lstat(skillPath);
|
|
473
|
+
if (stat.isSymbolicLink()) {
|
|
474
|
+
try { realPath = await fs.realpath(skillPath); }
|
|
475
|
+
catch (e) { if (e.code === 'ENOENT') await fs.unlink(skillPath).catch(() => {}); continue; }
|
|
476
|
+
if (realPath.includes('/skills-import/')) source = 'imported';
|
|
477
|
+
else if (realPath.includes('/skills-repo/')) {
|
|
478
|
+
source = 'repo';
|
|
479
|
+
const repoMatch = realPath.match(/skills-repo\/([^/]+)\/([^/]+)/);
|
|
480
|
+
if (repoMatch) repository = `${repoMatch[1]}/${repoMatch[2]}`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!await isValidSkill(realPath)) continue;
|
|
484
|
+
const metadata = await parseSkillMetadata(realPath);
|
|
485
|
+
skills.push({ name: entry.name, title: metadata.title, description: metadata.description, enabled: true, source, repository, path: realPath, scope: 'local', projectPath: projectDir });
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error(`Error reading project skill ${entry.name}:`, err.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.error('[Skills] Error scanning project-scoped skills:', err.message);
|
|
493
|
+
}
|
|
494
|
+
|
|
453
495
|
// Compute sync status for each skill
|
|
454
496
|
// For builtin skills, batch-check git status once
|
|
455
497
|
const builtinChangedSkills = new Set();
|
|
@@ -472,6 +514,11 @@ router.get('/', async (req, res) => {
|
|
|
472
514
|
}
|
|
473
515
|
|
|
474
516
|
for (const skill of skills) {
|
|
517
|
+
if (skill.scope === 'local') {
|
|
518
|
+
skill.syncable = false;
|
|
519
|
+
skill.syncType = null;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
475
522
|
if (skill.source === 'imported' || skill.source === 'unknown') {
|
|
476
523
|
skill.syncable = true;
|
|
477
524
|
skill.syncType = 'local';
|
|
@@ -814,13 +861,16 @@ router.delete('/:name', async (req, res) => {
|
|
|
814
861
|
}
|
|
815
862
|
|
|
816
863
|
const { name } = req.params;
|
|
864
|
+
const { projectPath } = req.query;
|
|
817
865
|
|
|
818
866
|
if (!SKILL_NAME_REGEX.test(name)) {
|
|
819
867
|
return res.status(400).json({ error: 'Invalid skill name' });
|
|
820
868
|
}
|
|
821
869
|
|
|
822
870
|
const userPaths = getUserPaths(userUuid);
|
|
823
|
-
const linkPath =
|
|
871
|
+
const linkPath = projectPath
|
|
872
|
+
? path.join(projectPath, '.claude', 'skills', name)
|
|
873
|
+
: path.join(userPaths.skillsDir, name);
|
|
824
874
|
|
|
825
875
|
// Check the symlink target to determine source
|
|
826
876
|
let realPath = null;
|
|
@@ -1057,7 +1107,7 @@ router.post('/install/:name', async (req, res) => {
|
|
|
1057
1107
|
}
|
|
1058
1108
|
|
|
1059
1109
|
const { name } = req.params;
|
|
1060
|
-
const { skillPath } = req.body;
|
|
1110
|
+
const { skillPath, projectPath } = req.body;
|
|
1061
1111
|
|
|
1062
1112
|
if (!SKILL_NAME_REGEX.test(name)) {
|
|
1063
1113
|
return res.status(400).json({ error: 'Invalid skill name' });
|
|
@@ -1074,8 +1124,12 @@ router.post('/install/:name', async (req, res) => {
|
|
|
1074
1124
|
return res.status(404).json({ error: 'Skill not found or invalid' });
|
|
1075
1125
|
}
|
|
1076
1126
|
|
|
1077
|
-
//
|
|
1078
|
-
const
|
|
1127
|
+
// Determine target directory: project-scoped or user global
|
|
1128
|
+
const targetDir = projectPath
|
|
1129
|
+
? path.join(projectPath, '.claude', 'skills')
|
|
1130
|
+
: userPaths.skillsDir;
|
|
1131
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
1132
|
+
const userSkillLink = path.join(targetDir, name);
|
|
1079
1133
|
|
|
1080
1134
|
try {
|
|
1081
1135
|
await fs.unlink(userSkillLink);
|