@ian2018cs/agenthub 0.1.73 → 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-CFgKYN6c.js → vendor-icons-DUmFlkZ8.js} +49 -49
- 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/shared/brand.js +8 -0
- package/dist/assets/index-CyYbCDk1.js +0 -192
- package/dist/assets/index-DQaPJRqa.css +0 -32
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);
|
package/shared/brand.js
CHANGED
|
@@ -19,6 +19,14 @@ export const PRODUCT_WEB_URL =
|
|
|
19
19
|
(typeof process !== 'undefined' && process.env?.PRODUCT_WEB_URL) ||
|
|
20
20
|
'http://localhost:6175';
|
|
21
21
|
|
|
22
|
+
// 对话空白页示例提示词(点击后自动填充到输入框)
|
|
23
|
+
export const CHAT_EXAMPLE_PROMPTS = [
|
|
24
|
+
{ icon: '📋', text: '帮我整理今天的工作计划' },
|
|
25
|
+
{ icon: '📊', text: '分析这段数据并给出洞察' },
|
|
26
|
+
{ icon: '✍️', text: '帮我润色这段文字' },
|
|
27
|
+
{ icon: '💡', text: '给我一些解决问题的思路' },
|
|
28
|
+
];
|
|
29
|
+
|
|
22
30
|
// 飞书绑定引导文案(出现 3 次,统一成函数避免漂移)
|
|
23
31
|
export const feishuBindingGuide = () =>
|
|
24
32
|
`👋 你好!请先完成账号绑定:\n\n` +
|