tycono 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/api/src/engine/role-lifecycle.ts +50 -0
- package/src/api/src/routes/roles.ts +41 -8
- package/src/api/src/routes/setup.ts +6 -1
- package/src/api/src/routes/skills.ts +135 -0
- package/src/api/src/services/scaffold.ts +1 -0
- package/src/web/dist/assets/{index-BaLxr7Du.css → index-BdKLLYdc.css} +1 -1
- package/src/web/dist/assets/index-fwQJALjZ.js +96 -0
- package/src/web/dist/assets/{preview-app-BR4p4AQ9.js → preview-app-Bi90FWsr.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-B0GiFOxN.js +0 -96
package/package.json
CHANGED
|
@@ -6,6 +6,16 @@ import { generateSkillMd } from './skill-template.js';
|
|
|
6
6
|
|
|
7
7
|
/* ─── Types ──────────────────────────────────── */
|
|
8
8
|
|
|
9
|
+
export interface SkillContentDef {
|
|
10
|
+
frontmatter: Record<string, unknown>;
|
|
11
|
+
body: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SkillExportDef {
|
|
15
|
+
primary: SkillContentDef | null;
|
|
16
|
+
shared: Array<{ id: string } & SkillContentDef>;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export interface RoleDefinition {
|
|
10
20
|
id: string;
|
|
11
21
|
name: string;
|
|
@@ -14,6 +24,7 @@ export interface RoleDefinition {
|
|
|
14
24
|
persona: string;
|
|
15
25
|
skills?: string[];
|
|
16
26
|
source?: RoleSource;
|
|
27
|
+
skillContent?: SkillExportDef;
|
|
17
28
|
authority: {
|
|
18
29
|
autonomous: string[];
|
|
19
30
|
needsApproval: string[];
|
|
@@ -68,6 +79,24 @@ export class RoleLifecycleManager {
|
|
|
68
79
|
const skillContent = generateSkillMd(orgNode);
|
|
69
80
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
|
|
70
81
|
|
|
82
|
+
// 4b. Store에서 온 skillContent가 있으면 덮어쓰기
|
|
83
|
+
if (def.skillContent?.primary) {
|
|
84
|
+
const content = serializeSkillMd(def.skillContent.primary);
|
|
85
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4c. Shared skills 설치 (이미 있으면 건너뜀)
|
|
89
|
+
if (def.skillContent?.shared) {
|
|
90
|
+
for (const shared of def.skillContent.shared) {
|
|
91
|
+
const sharedDir = path.join(this.companyRoot, '.claude', 'skills', '_shared', shared.id);
|
|
92
|
+
const sharedSkillPath = path.join(sharedDir, 'SKILL.md');
|
|
93
|
+
if (!fs.existsSync(sharedSkillPath)) {
|
|
94
|
+
fs.mkdirSync(sharedDir, { recursive: true });
|
|
95
|
+
fs.writeFileSync(sharedSkillPath, serializeSkillMd(shared));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
// 5. Update roles.md Hub
|
|
72
101
|
this.addToRolesHub(def);
|
|
73
102
|
|
|
@@ -351,3 +380,24 @@ ${def.authority.needsApproval.map((a) => `- ${a}`).join('\n')}
|
|
|
351
380
|
fs.writeFileSync(claudeMdPath, lines.join('\n'));
|
|
352
381
|
}
|
|
353
382
|
}
|
|
383
|
+
|
|
384
|
+
/* ─── Helpers ──────────────────────────────── */
|
|
385
|
+
|
|
386
|
+
function serializeSkillMd(skill: SkillContentDef): string {
|
|
387
|
+
const fm = skill.frontmatter;
|
|
388
|
+
const yamlLines: string[] = [];
|
|
389
|
+
if (fm.name) yamlLines.push(`name: ${fm.name}`);
|
|
390
|
+
if (fm.description) yamlLines.push(`description: ${JSON.stringify(fm.description)}`);
|
|
391
|
+
if (fm.allowedTools && Array.isArray(fm.allowedTools)) {
|
|
392
|
+
yamlLines.push(`allowedTools:\n${(fm.allowedTools as string[]).map(t => ` - ${t}`).join('\n')}`);
|
|
393
|
+
}
|
|
394
|
+
if (fm.model) yamlLines.push(`model: ${fm.model}`);
|
|
395
|
+
if (fm.tags && Array.isArray(fm.tags)) {
|
|
396
|
+
yamlLines.push(`tags:\n${(fm.tags as string[]).map(t => ` - ${t}`).join('\n')}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (yamlLines.length === 0) {
|
|
400
|
+
return skill.body;
|
|
401
|
+
}
|
|
402
|
+
return `---\n${yamlLines.join('\n')}\n---\n\n${skill.body}`;
|
|
403
|
+
}
|
|
@@ -11,13 +11,27 @@ rolesRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
|
11
11
|
const content = readFile('roles/roles.md');
|
|
12
12
|
const rows = parseMarkdownTable(content);
|
|
13
13
|
|
|
14
|
-
const roles = rows.map(row =>
|
|
15
|
-
id
|
|
16
|
-
name
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const roles = rows.map(row => {
|
|
15
|
+
const id = row.id ?? '';
|
|
16
|
+
let name = row.role ?? row.name ?? '';
|
|
17
|
+
|
|
18
|
+
// role.yaml의 name이 있으면 우선 사용 (rename 반영)
|
|
19
|
+
const yamlPath = `roles/${id}/role.yaml`;
|
|
20
|
+
if (id && fileExists(yamlPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = YAML.parse(readFile(yamlPath)) as Record<string, unknown>;
|
|
23
|
+
if (raw.name) name = raw.name as string;
|
|
24
|
+
} catch { /* fallback to roles.md name */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name,
|
|
30
|
+
level: row.level ?? '',
|
|
31
|
+
reportsTo: row.reports_to ?? '',
|
|
32
|
+
status: row.상태 ?? row.status ?? '',
|
|
33
|
+
};
|
|
34
|
+
});
|
|
21
35
|
|
|
22
36
|
res.json(roles);
|
|
23
37
|
} catch (err) {
|
|
@@ -51,11 +65,13 @@ rolesRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
|
51
65
|
journal: '',
|
|
52
66
|
};
|
|
53
67
|
|
|
54
|
-
// role.yaml에서 persona + authority 읽기
|
|
68
|
+
// role.yaml에서 name + persona + authority + skills 읽기
|
|
55
69
|
const yamlPath = `roles/${id}/role.yaml`;
|
|
56
70
|
if (fileExists(yamlPath)) {
|
|
57
71
|
const raw = YAML.parse(readFile(yamlPath)) as Record<string, unknown>;
|
|
72
|
+
if (raw.name) role.name = raw.name;
|
|
58
73
|
if (raw.persona) role.persona = raw.persona;
|
|
74
|
+
if (Array.isArray(raw.skills)) role.skills = raw.skills;
|
|
59
75
|
const auth = raw.authority as Record<string, string[]> | undefined;
|
|
60
76
|
if (auth) {
|
|
61
77
|
role.authority = {
|
|
@@ -65,6 +81,23 @@ rolesRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
// SKILL.md에서 스킬 메타 자동 추출
|
|
85
|
+
const skillMdPath = `.claude/skills/${id}/SKILL.md`;
|
|
86
|
+
if (fileExists(skillMdPath)) {
|
|
87
|
+
const skillContent = readFile(skillMdPath);
|
|
88
|
+
const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/);
|
|
89
|
+
if (fmMatch) {
|
|
90
|
+
try {
|
|
91
|
+
const meta = YAML.parse(fmMatch[1]) as Record<string, unknown>;
|
|
92
|
+
role.skillMeta = {
|
|
93
|
+
name: meta.name || id,
|
|
94
|
+
description: meta.description || '',
|
|
95
|
+
...(meta.allowedTools ? { allowedTools: meta.allowedTools } : {}),
|
|
96
|
+
};
|
|
97
|
+
} catch { /* ignore parse errors */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
// 오늘 저널 읽기
|
|
69
102
|
const today = new Date().toISOString().slice(0, 10);
|
|
70
103
|
const journalPath = `roles/${id}/journal/${today}.md`;
|
|
@@ -15,6 +15,7 @@ import { importKnowledge } from '../services/knowledge-importer.js';
|
|
|
15
15
|
import { AnthropicProvider, type LLMProvider } from '../engine/llm-adapter.js';
|
|
16
16
|
import { jobManager } from '../services/job-manager.js';
|
|
17
17
|
import { applyConfig, readConfig, writeConfig } from '../services/company-config.js';
|
|
18
|
+
import { mergePreferences } from '../services/preferences.js';
|
|
18
19
|
|
|
19
20
|
export const setupRouter = Router();
|
|
20
21
|
|
|
@@ -81,7 +82,7 @@ setupRouter.post('/validate-path', (req, res) => {
|
|
|
81
82
|
* POST /api/setup/scaffold
|
|
82
83
|
*/
|
|
83
84
|
setupRouter.post('/scaffold', (req, res) => {
|
|
84
|
-
const { companyName, description, apiKey, team, existingProjectPath, knowledgePaths, codeRoot } = req.body;
|
|
85
|
+
const { companyName, description, apiKey, team, existingProjectPath, knowledgePaths, codeRoot, language } = req.body;
|
|
85
86
|
|
|
86
87
|
if (!companyName || typeof companyName !== 'string') {
|
|
87
88
|
res.status(400).json({ error: 'companyName is required' });
|
|
@@ -110,6 +111,10 @@ setupRouter.post('/scaffold', (req, res) => {
|
|
|
110
111
|
if (codeRoot && typeof codeRoot === 'string') {
|
|
111
112
|
writeConfig(projectRoot, { ...scaffoldConfig, codeRoot });
|
|
112
113
|
}
|
|
114
|
+
// Save language preference
|
|
115
|
+
if (language && typeof language === 'string') {
|
|
116
|
+
mergePreferences(projectRoot, { language });
|
|
117
|
+
}
|
|
113
118
|
jobManager.refreshRunner();
|
|
114
119
|
|
|
115
120
|
res.json({ ok: true, companyName, projectRoot, created });
|
|
@@ -49,6 +49,84 @@ skillsRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
// GET /api/skills/registry — Browse external skill registries
|
|
53
|
+
skillsRouter.get('/registry', async (_req: Request, res: Response, next: NextFunction) => {
|
|
54
|
+
try {
|
|
55
|
+
// Known skill registries (curated list of quality skills)
|
|
56
|
+
const REGISTRIES = [
|
|
57
|
+
{
|
|
58
|
+
source: 'anthropics/skills',
|
|
59
|
+
label: 'Anthropic Official',
|
|
60
|
+
skills: [
|
|
61
|
+
{ id: 'frontend-design', name: 'Frontend Design', description: 'Create distinctive, production-grade frontend interfaces with high design quality', category: 'design', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md' },
|
|
62
|
+
{ id: 'webapp-testing', name: 'Web App Testing', description: 'Playwright-based toolkit for testing local web applications', category: 'testing', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/webapp-testing/SKILL.md' },
|
|
63
|
+
{ id: 'mcp-builder', name: 'MCP Builder', description: 'Guide for creating high-quality MCP servers for LLM tool integration', category: 'development', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/mcp-builder/SKILL.md' },
|
|
64
|
+
{ id: 'internal-comms', name: 'Internal Comms', description: 'Write internal communications: status reports, newsletters, 3P updates', category: 'operations', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/internal-comms/SKILL.md' },
|
|
65
|
+
{ id: 'web-artifacts-builder', name: 'Web Artifacts Builder', description: 'React + Tailwind + shadcn/ui component development and bundling', category: 'development', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/web-artifacts-builder/SKILL.md' },
|
|
66
|
+
{ id: 'skill-creator', name: 'Skill Creator', description: 'Interactive guide for building new Claude Code skills', category: 'meta', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/skill-creator/SKILL.md' },
|
|
67
|
+
{ id: 'algorithmic-art', name: 'Algorithmic Art', description: 'Generative art using p5.js with flow fields and particle systems', category: 'creative', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/algorithmic-art/SKILL.md' },
|
|
68
|
+
{ id: 'canvas-design', name: 'Canvas Design', description: 'Visual art creation in PNG and PDF formats', category: 'creative', url: 'https://raw.githubusercontent.com/anthropics/skills/main/skills/canvas-design/SKILL.md' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
source: 'community',
|
|
73
|
+
label: 'Community',
|
|
74
|
+
skills: [
|
|
75
|
+
{ id: 'tdd-superpowers', name: 'TDD (Test-Driven Dev)', description: 'Test-first development with Red-Green-Refactor cycle', category: 'development', url: 'https://raw.githubusercontent.com/obra/superpowers/main/skills/tdd/SKILL.md' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Mark which ones are already installed
|
|
81
|
+
const result = REGISTRIES.map(registry => ({
|
|
82
|
+
...registry,
|
|
83
|
+
skills: registry.skills.map(skill => ({
|
|
84
|
+
...skill,
|
|
85
|
+
installed: isSkillInstalled(skill.id),
|
|
86
|
+
})),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
res.json(result);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
next(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// POST /api/skills/registry/install — Install a skill from external registry
|
|
96
|
+
skillsRouter.post('/registry/install', async (req: Request, res: Response, next: NextFunction) => {
|
|
97
|
+
try {
|
|
98
|
+
const { skillId, url } = req.body;
|
|
99
|
+
|
|
100
|
+
if (!skillId || !url) {
|
|
101
|
+
res.status(400).json({ error: 'skillId and url are required' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Already installed?
|
|
106
|
+
if (isSkillInstalled(skillId)) {
|
|
107
|
+
res.json({ ok: true, message: 'Already installed', skillId });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fetch SKILL.md from URL
|
|
112
|
+
const response = await fetch(url);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
res.status(502).json({ error: `Failed to fetch skill: ${response.status}` });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const content = await response.text();
|
|
118
|
+
|
|
119
|
+
// Install to .claude/skills/_shared/{skillId}/SKILL.md
|
|
120
|
+
const destDir = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', skillId);
|
|
121
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
122
|
+
fs.writeFileSync(path.join(destDir, 'SKILL.md'), content);
|
|
123
|
+
|
|
124
|
+
res.json({ ok: true, skillId, installed: true });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
next(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
52
130
|
// GET /api/skills/:id — Skill detail
|
|
53
131
|
skillsRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
54
132
|
try {
|
|
@@ -77,6 +155,40 @@ skillsRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
|
77
155
|
}
|
|
78
156
|
});
|
|
79
157
|
|
|
158
|
+
/* ─── Skill Export (for Store publish) ──── */
|
|
159
|
+
|
|
160
|
+
// GET /api/skills/export/:roleId — Export full SKILL.md content for publishing
|
|
161
|
+
skillsRouter.get('/export/:roleId', (req: Request, res: Response, next: NextFunction) => {
|
|
162
|
+
try {
|
|
163
|
+
const roleId = req.params.roleId as string;
|
|
164
|
+
|
|
165
|
+
// 1. Primary skill: .claude/skills/{roleId}/SKILL.md
|
|
166
|
+
let primary: { frontmatter: Record<string, unknown>; body: string } | null = null;
|
|
167
|
+
const primaryPath = path.join(COMPANY_ROOT, '.claude', 'skills', roleId, 'SKILL.md');
|
|
168
|
+
if (fs.existsSync(primaryPath)) {
|
|
169
|
+
const content = fs.readFileSync(primaryPath, 'utf-8');
|
|
170
|
+
primary = parseSkillContent(content, roleId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Shared skills from role.yaml skills[] array
|
|
174
|
+
const sharedIds = getRoleSkills(roleId);
|
|
175
|
+
const shared: Array<{ id: string; frontmatter: Record<string, unknown>; body: string }> = [];
|
|
176
|
+
|
|
177
|
+
for (const sharedId of sharedIds) {
|
|
178
|
+
const sharedPath = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', sharedId, 'SKILL.md');
|
|
179
|
+
if (fs.existsSync(sharedPath)) {
|
|
180
|
+
const content = fs.readFileSync(sharedPath, 'utf-8');
|
|
181
|
+
const parsed = parseSkillContent(content, sharedId);
|
|
182
|
+
shared.push({ id: sharedId, ...parsed });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
res.json({ primary, shared });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
next(err);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
80
192
|
/* ─── Role-Skill Management ─────────────── */
|
|
81
193
|
|
|
82
194
|
// GET /api/roles/:id/skills — Skills equipped to a role
|
|
@@ -219,3 +331,26 @@ function extractSkillMeta(content: string, id: string): Record<string, unknown>
|
|
|
219
331
|
return { id, name: id, description: '' };
|
|
220
332
|
}
|
|
221
333
|
}
|
|
334
|
+
|
|
335
|
+
function parseSkillContent(content: string, id: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
336
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/);
|
|
337
|
+
if (!fmMatch) {
|
|
338
|
+
return { frontmatter: { name: id, description: '' }, body: content };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const meta = YAML.parse(fmMatch[1]) as Record<string, unknown>;
|
|
343
|
+
return {
|
|
344
|
+
frontmatter: {
|
|
345
|
+
name: meta.name || id,
|
|
346
|
+
description: (meta.description as string) || '',
|
|
347
|
+
...(meta.allowedTools ? { allowedTools: meta.allowedTools } : {}),
|
|
348
|
+
...(meta.model ? { model: meta.model } : {}),
|
|
349
|
+
...(meta.tags ? { tags: meta.tags } : {}),
|
|
350
|
+
},
|
|
351
|
+
body: fmMatch[2].trim(),
|
|
352
|
+
};
|
|
353
|
+
} catch {
|
|
354
|
+
return { frontmatter: { name: id, description: '' }, body: content };
|
|
355
|
+
}
|
|
356
|
+
}
|