@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.
@@ -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 linkPath = path.join(userPaths.skillsDir, skillName);
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
- claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), ...mcpJson };
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 using claude mcp add-json
479
+ // Install each server entry
480
480
  for (const [serverName, serverConfig] of Object.entries(mcpJson)) {
481
481
  try {
482
- await addMcpServerViaJson(serverName, serverConfig, scope, userPaths.claudeDir);
483
- installedServers.push(serverName);
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
@@ -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 = path.join(userPaths.skillsDir, name);
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
- // Create symlink directly from user's skills directory to skill in repo
1078
- const userSkillLink = path.join(userPaths.skillsDir, name);
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` +