dantelabs-agentic-school 1.1.1 → 1.2.0

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.
@@ -16,96 +16,55 @@
16
16
  "name": "common",
17
17
  "description": "공통 유틸리티 스킬 모음. 인증 관리, 문서 제작 도구(PPTX, PDF, DOCX), 이미지/비디오 생성(Kie.ai) 등 여러 플러그인에서 공통으로 사용하는 기능을 제공합니다.",
18
18
  "version": "1.0.0",
19
- "path": "plugins/common",
20
- "components": {
21
- "skills": ["auth-manager", "pptx", "pdf", "docx", "kie-image-generator", "kie-video-generator"]
22
- }
19
+ "source": "./plugins/common"
23
20
  },
24
21
  {
25
22
  "name": "brand-analytics",
26
23
  "description": "브랜드 소개서를 분석하여 포지셔닝, SWOT, 경쟁사 분석을 수행합니다.",
27
24
  "version": "1.0.0",
28
- "path": "plugins/brand-analytics",
29
- "components": {
30
- "agents": ["brand-strategist", "competitive-analyst"],
31
- "commands": ["analyze-brand"],
32
- "skills": ["brand-positioning"]
33
- }
25
+ "source": "./plugins/brand-analytics"
34
26
  },
35
27
  {
36
28
  "name": "customer-segmentation",
37
29
  "description": "데이터 기반 고객 세그먼트를 설계하고 정의합니다.",
38
30
  "version": "1.0.0",
39
- "path": "plugins/customer-segmentation",
40
- "components": {
41
- "agents": ["segmentation-architect", "data-analyst"],
42
- "commands": ["create-segments"],
43
- "skills": ["segmentation-framework", "activation-map"]
44
- }
31
+ "source": "./plugins/customer-segmentation"
45
32
  },
46
33
  {
47
34
  "name": "persona-builder",
48
35
  "description": "타겟 세그먼트의 상세 페르소나 카드를 생성합니다.",
49
36
  "version": "1.0.0",
50
- "path": "plugins/persona-builder",
51
- "components": {
52
- "agents": ["persona-architect", "customer-insights-partner"],
53
- "commands": ["build-persona"],
54
- "skills": ["persona-framework"]
55
- }
37
+ "source": "./plugins/persona-builder"
56
38
  },
57
39
  {
58
40
  "name": "social-strategy",
59
41
  "description": "페르소나 기반 채널 선정 및 콘텐츠 전략을 수립합니다.",
60
42
  "version": "1.0.0",
61
- "path": "plugins/social-strategy",
62
- "components": {
63
- "agents": ["social-strategy-director", "channel-analyst"],
64
- "commands": ["plan-channels"],
65
- "skills": ["channel-roadmap", "content-pillars"]
66
- }
43
+ "source": "./plugins/social-strategy"
67
44
  },
68
45
  {
69
46
  "name": "content-creation",
70
47
  "description": "채널별 홍보 카피 및 스크립트를 생성합니다.",
71
48
  "version": "1.0.0",
72
- "path": "plugins/content-creation",
73
- "components": {
74
- "agents": ["copy-strategist", "conversion-copywriter", "script-writer"],
75
- "commands": ["generate-copy", "write-script"],
76
- "skills": ["message-architecture", "hook-formulas"]
77
- }
49
+ "source": "./plugins/content-creation"
78
50
  },
79
51
  {
80
52
  "name": "creative-production",
81
53
  "description": "실제 이미지와 비디오를 생성합니다. common 플러그인의 kie-image-generator, kie-video-generator 스킬을 활용합니다.",
82
54
  "version": "1.0.0",
83
- "path": "plugins/creative-production",
84
- "components": {
85
- "agents": ["creative-director", "production-coordinator"],
86
- "commands": ["create-image", "create-video"],
87
- "skills": ["image-prompt-guide", "video-production"]
88
- }
55
+ "source": "./plugins/creative-production"
89
56
  },
90
57
  {
91
58
  "name": "campaign-orchestration",
92
59
  "description": "전체 마케팅 워크플로우를 통합하고 순차 실행합니다.",
93
60
  "version": "1.0.0",
94
- "path": "plugins/campaign-orchestration",
95
- "components": {
96
- "agents": ["campaign-director", "workflow-coordinator"],
97
- "commands": ["run-full-pipeline", "run-phase"],
98
- "skills": ["pipeline-framework"]
99
- }
61
+ "source": "./plugins/campaign-orchestration"
100
62
  },
101
63
  {
102
64
  "name": "market-research",
103
65
  "description": "시장 분석 리포트 및 데이터 시각화를 생성합니다.",
104
66
  "version": "1.0.0",
105
- "path": "plugins/market-research",
106
- "components": {
107
- "skills": ["analysis-reports", "diagram-generator"]
108
- }
67
+ "source": "./plugins/market-research"
109
68
  }
110
69
  ],
111
70
  "categories": [
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getMarketplaceConfig } from '../lib/config.js';
2
+ import { getMarketplaceConfig, enrichPluginWithComponents } from '../lib/config.js';
3
3
  import logger from '../utils/logger.js';
4
4
  import { t } from '../i18n/index.js';
5
5
 
@@ -11,7 +11,7 @@ export default function infoCommand(program) {
11
11
  .action(async (pluginName, options) => {
12
12
  try {
13
13
  const config = await getMarketplaceConfig();
14
- const plugin = config.plugins.find((p) => p.name === pluginName);
14
+ let plugin = config.plugins.find((p) => p.name === pluginName);
15
15
 
16
16
  if (!plugin) {
17
17
  logger.error(t('info.pluginNotFound', { name: pluginName }));
@@ -23,6 +23,9 @@ export default function infoCommand(program) {
23
23
  process.exit(1);
24
24
  }
25
25
 
26
+ // Enrich plugin with discovered components
27
+ plugin = await enrichPluginWithComponents(plugin);
28
+
26
29
  if (options.json) {
27
30
  console.log(JSON.stringify(plugin, null, 2));
28
31
  return;
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getMarketplaceConfig } from '../lib/config.js';
2
+ import { getMarketplaceConfig, enrichPluginWithComponents } from '../lib/config.js';
3
3
  import logger from '../utils/logger.js';
4
4
  import { t } from '../i18n/index.js';
5
5
 
@@ -14,8 +14,13 @@ export default function listCommand(program) {
14
14
  try {
15
15
  const config = await getMarketplaceConfig();
16
16
 
17
+ // Enrich plugins with discovered components
18
+ const enrichedPlugins = await Promise.all(
19
+ config.plugins.map(p => enrichPluginWithComponents(p))
20
+ );
21
+
17
22
  if (options.json) {
18
- console.log(JSON.stringify(config.plugins, null, 2));
23
+ console.log(JSON.stringify(enrichedPlugins, null, 2));
19
24
  return;
20
25
  }
21
26
 
@@ -24,7 +29,7 @@ export default function listCommand(program) {
24
29
  console.log(chalk.gray('━'.repeat(60)));
25
30
  console.log();
26
31
 
27
- for (const plugin of config.plugins) {
32
+ for (const plugin of enrichedPlugins) {
28
33
  console.log(
29
34
  chalk.bold.cyan(`${plugin.name}`),
30
35
  chalk.gray(`v${plugin.version}`)
@@ -59,15 +64,15 @@ export default function listCommand(program) {
59
64
  }
60
65
 
61
66
  // Summary
62
- const totalAgents = config.plugins.reduce(
67
+ const totalAgents = enrichedPlugins.reduce(
63
68
  (sum, p) => sum + (p.components?.agents?.length || 0),
64
69
  0
65
70
  );
66
- const totalCommands = config.plugins.reduce(
71
+ const totalCommands = enrichedPlugins.reduce(
67
72
  (sum, p) => sum + (p.components?.commands?.length || 0),
68
73
  0
69
74
  );
70
- const totalSkills = config.plugins.reduce(
75
+ const totalSkills = enrichedPlugins.reduce(
71
76
  (sum, p) => sum + (p.components?.skills?.length || 0),
72
77
  0
73
78
  );
@@ -76,7 +81,7 @@ export default function listCommand(program) {
76
81
  console.log(
77
82
  chalk.bold(`${t('common.summary')}:`),
78
83
  t('list.summaryText', {
79
- plugins: config.plugins.length,
84
+ plugins: enrichedPlugins.length,
80
85
  agents: totalAgents,
81
86
  commands: totalCommands,
82
87
  skills: totalSkills
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from 'fs/promises';
1
+ import { readFile, writeFile, readdir } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
@@ -7,6 +7,9 @@ import { dirname } from 'path';
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
9
 
10
+ // Package root directory
11
+ const PACKAGE_ROOT = join(__dirname, '../../..');
12
+
10
13
  const CONFIG_CACHE_FILE = '.dantelabs-cache.json';
11
14
  const CACHE_TTL = 1000 * 60 * 60; // 1 hour
12
15
 
@@ -124,6 +127,118 @@ async function saveCachedConfig(config) {
124
127
  }
125
128
  }
126
129
 
130
+ /**
131
+ * Normalize source path (handle both 'path' and 'source' fields)
132
+ */
133
+ export function getSourcePath(plugin) {
134
+ const source = plugin.source || plugin.path;
135
+ if (!source) return `plugins/${plugin.name}`;
136
+ return source.replace(/^\.\//, '');
137
+ }
138
+
139
+ /**
140
+ * Discover components by scanning local plugin directory
141
+ */
142
+ async function discoverLocalComponents(pluginPath) {
143
+ const components = { agents: [], commands: [], skills: [] };
144
+
145
+ // Scan agents directory
146
+ const agentsDir = join(pluginPath, 'agents');
147
+ if (existsSync(agentsDir)) {
148
+ const files = await readdir(agentsDir);
149
+ components.agents = files
150
+ .filter(f => f.endsWith('.md'))
151
+ .map(f => f.replace('.md', ''));
152
+ }
153
+
154
+ // Scan commands directory
155
+ const commandsDir = join(pluginPath, 'commands');
156
+ if (existsSync(commandsDir)) {
157
+ const files = await readdir(commandsDir);
158
+ components.commands = files
159
+ .filter(f => f.endsWith('.md'))
160
+ .map(f => f.replace('.md', ''));
161
+ }
162
+
163
+ // Scan skills directory
164
+ const skillsDir = join(pluginPath, 'skills');
165
+ if (existsSync(skillsDir)) {
166
+ const entries = await readdir(skillsDir, { withFileTypes: true });
167
+ components.skills = entries
168
+ .filter(e => e.isDirectory())
169
+ .map(e => e.name);
170
+ }
171
+
172
+ return components;
173
+ }
174
+
175
+ /**
176
+ * Discover components from remote GitHub directory
177
+ */
178
+ async function discoverRemoteComponents(remotePath) {
179
+ const components = { agents: [], commands: [], skills: [] };
180
+
181
+ const fetchDir = async (path) => {
182
+ const url = `${GITHUB_CONFIG.apiBase}/contents/${path}?ref=${GITHUB_CONFIG.branch}`;
183
+ const response = await fetch(url, {
184
+ headers: {
185
+ 'Accept': 'application/vnd.github.v3+json',
186
+ 'User-Agent': 'dantelabs-agentic-school-cli'
187
+ }
188
+ });
189
+ if (!response.ok) return [];
190
+ return response.json();
191
+ };
192
+
193
+ // Scan agents directory
194
+ try {
195
+ const agentsContents = await fetchDir(`${remotePath}/agents`);
196
+ components.agents = agentsContents
197
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
198
+ .map(f => f.name.replace('.md', ''));
199
+ } catch (e) { /* no agents dir */ }
200
+
201
+ // Scan commands directory
202
+ try {
203
+ const commandsContents = await fetchDir(`${remotePath}/commands`);
204
+ components.commands = commandsContents
205
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
206
+ .map(f => f.name.replace('.md', ''));
207
+ } catch (e) { /* no commands dir */ }
208
+
209
+ // Scan skills directory
210
+ try {
211
+ const skillsContents = await fetchDir(`${remotePath}/skills`);
212
+ components.skills = skillsContents
213
+ .filter(f => f.type === 'dir')
214
+ .map(f => f.name);
215
+ } catch (e) { /* no skills dir */ }
216
+
217
+ return components;
218
+ }
219
+
220
+ /**
221
+ * Enrich plugin with discovered components
222
+ */
223
+ export async function enrichPluginWithComponents(plugin) {
224
+ // If components already exist, return as is
225
+ if (plugin.components && Object.keys(plugin.components).length > 0) {
226
+ return plugin;
227
+ }
228
+
229
+ const sourcePath = getSourcePath(plugin);
230
+ const localPath = join(PACKAGE_ROOT, sourcePath);
231
+
232
+ let components;
233
+ if (existsSync(localPath)) {
234
+ components = await discoverLocalComponents(localPath);
235
+ } else {
236
+ components = await discoverRemoteComponents(sourcePath);
237
+ }
238
+
239
+ return { ...plugin, components };
240
+ }
241
+
127
242
  /**
128
243
  * Validate plugin name
129
244
  */
@@ -46,6 +46,87 @@ async function copyDirectory(src, dest) {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * Discover components by scanning plugin directory
51
+ */
52
+ async function discoverComponents(localPath) {
53
+ const components = { agents: [], commands: [], skills: [] };
54
+
55
+ // Scan agents directory
56
+ const agentsDir = join(localPath, 'agents');
57
+ if (existsSync(agentsDir)) {
58
+ const files = await readdir(agentsDir);
59
+ components.agents = files
60
+ .filter(f => f.endsWith('.md'))
61
+ .map(f => f.replace('.md', ''));
62
+ }
63
+
64
+ // Scan commands directory
65
+ const commandsDir = join(localPath, 'commands');
66
+ if (existsSync(commandsDir)) {
67
+ const files = await readdir(commandsDir);
68
+ components.commands = files
69
+ .filter(f => f.endsWith('.md'))
70
+ .map(f => f.replace('.md', ''));
71
+ }
72
+
73
+ // Scan skills directory
74
+ const skillsDir = join(localPath, 'skills');
75
+ if (existsSync(skillsDir)) {
76
+ const entries = await readdir(skillsDir, { withFileTypes: true });
77
+ components.skills = entries
78
+ .filter(e => e.isDirectory())
79
+ .map(e => e.name);
80
+ }
81
+
82
+ return components;
83
+ }
84
+
85
+ /**
86
+ * Discover components from remote GitHub directory
87
+ */
88
+ async function discoverRemoteComponents(remotePath) {
89
+ const { getDirectoryContents } = await import('./downloader.js');
90
+ const components = { agents: [], commands: [], skills: [] };
91
+
92
+ // Scan agents directory
93
+ try {
94
+ const agentsContents = await getDirectoryContents(`${remotePath}/agents`);
95
+ components.agents = agentsContents
96
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
97
+ .map(f => f.name.replace('.md', ''));
98
+ } catch (e) { /* no agents dir */ }
99
+
100
+ // Scan commands directory
101
+ try {
102
+ const commandsContents = await getDirectoryContents(`${remotePath}/commands`);
103
+ components.commands = commandsContents
104
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
105
+ .map(f => f.name.replace('.md', ''));
106
+ } catch (e) { /* no commands dir */ }
107
+
108
+ // Scan skills directory
109
+ try {
110
+ const skillsContents = await getDirectoryContents(`${remotePath}/skills`);
111
+ components.skills = skillsContents
112
+ .filter(f => f.type === 'dir')
113
+ .map(f => f.name);
114
+ } catch (e) { /* no skills dir */ }
115
+
116
+ return components;
117
+ }
118
+
119
+ /**
120
+ * Normalize source path (handle both 'path' and 'source' fields)
121
+ */
122
+ function getSourcePath(plugin) {
123
+ // Support both old 'path' format and new 'source' format
124
+ const source = plugin.source || plugin.path;
125
+ if (!source) return `plugins/${plugin.name}`;
126
+ // Remove leading './' if present
127
+ return source.replace(/^\.\//, '');
128
+ }
129
+
49
130
  /**
50
131
  * Install a single plugin to .claude directory
51
132
  *
@@ -64,13 +145,22 @@ async function copyDirectory(src, dest) {
64
145
  export async function installPlugin(plugin, claudeDir, options = {}) {
65
146
  const { force = false, onProgress } = options;
66
147
  const pluginName = plugin.name;
67
- const components = plugin.components || {};
68
- const sourcePath = plugin.path; // e.g., "plugins/brand-analytics"
148
+ const sourcePath = getSourcePath(plugin); // e.g., "plugins/brand-analytics"
69
149
 
70
150
  // Determine if we should use local or remote source
71
151
  const useLocalSource = hasLocalSource(sourcePath);
72
152
  const localSourcePath = join(PACKAGE_ROOT, sourcePath);
73
153
 
154
+ // Discover components dynamically (or use provided components for backward compatibility)
155
+ let components = plugin.components;
156
+ if (!components || Object.keys(components).length === 0) {
157
+ if (useLocalSource) {
158
+ components = await discoverComponents(localSourcePath);
159
+ } else {
160
+ components = await discoverRemoteComponents(sourcePath);
161
+ }
162
+ }
163
+
74
164
  // Create base directories
75
165
  await ensureDir(join(claudeDir, 'agents'));
76
166
  await ensureDir(join(claudeDir, 'commands'));
@@ -194,7 +284,20 @@ export async function installPlugin(plugin, claudeDir, options = {}) {
194
284
  */
195
285
  export async function uninstallPlugin(plugin, claudeDir) {
196
286
  const pluginName = plugin.name;
197
- const components = plugin.components || {};
287
+ const sourcePath = getSourcePath(plugin);
288
+
289
+ // Discover components dynamically (or use provided components for backward compatibility)
290
+ let components = plugin.components;
291
+ if (!components || Object.keys(components).length === 0) {
292
+ const useLocalSource = hasLocalSource(sourcePath);
293
+ const localSourcePath = join(PACKAGE_ROOT, sourcePath);
294
+
295
+ if (useLocalSource) {
296
+ components = await discoverComponents(localSourcePath);
297
+ } else {
298
+ components = await discoverRemoteComponents(sourcePath);
299
+ }
300
+ }
198
301
 
199
302
  const results = {
200
303
  plugin: pluginName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dantelabs-agentic-school",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "CLI tool for installing Dante Labs agentic plugins for Claude Code",
5
5
  "main": "cli/src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "brand-analytics",
3
+ "description": "브랜드 소개서를 분석하여 포지셔닝, SWOT, 경쟁사 분석을 수행합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "campaign-orchestration",
3
+ "description": "전체 마케팅 워크플로우를 통합하고 순차 실행합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "common",
3
+ "description": "공통 유틸리티 스킬 모음. 인증 관리, 문서 제작 도구(PPTX, PDF, DOCX), 이미지/비디오 생성(Kie.ai) 등 여러 플러그인에서 공통으로 사용하는 기능을 제공합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "content-creation",
3
+ "description": "채널별 홍보 카피 및 스크립트를 생성합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "creative-production",
3
+ "description": "실제 이미지와 비디오를 생성합니다. common 플러그인의 kie-image-generator, kie-video-generator 스킬을 활용합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "customer-segmentation",
3
+ "description": "데이터 기반 고객 세그먼트를 설계하고 정의합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "market-research",
3
+ "description": "시장 분석 리포트 및 데이터 시각화를 생성합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "persona-builder",
3
+ "description": "타겟 세그먼트의 상세 페르소나 카드를 생성합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "social-strategy",
3
+ "description": "페르소나 기반 채널 선정 및 콘텐츠 전략을 수립합니다.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Dante Labs",
7
+ "email": "datapod.k@gmail.com"
8
+ }
9
+ }