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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: row.id ?? '',
16
- name: row.role ?? row.name ?? '',
17
- level: row.level ?? '',
18
- reportsTo: row.reports_to ?? '',
19
- status: row.상태 ?? row.status ?? '',
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
+ }
@@ -21,6 +21,7 @@ export interface ScaffoldConfig {
21
21
  projectRoot: string;
22
22
  existingProjectPath?: string;
23
23
  knowledgePaths?: string[];
24
+ language?: string;
24
25
  }
25
26
 
26
27
  interface TeamRole {