tycono-server 0.1.0-beta.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.
Files changed (84) hide show
  1. package/bin/cli.js +35 -0
  2. package/bin/server.ts +160 -0
  3. package/package.json +50 -0
  4. package/src/api/package.json +31 -0
  5. package/src/api/src/create-app.ts +90 -0
  6. package/src/api/src/create-server.ts +251 -0
  7. package/src/api/src/engine/agent-loop.ts +738 -0
  8. package/src/api/src/engine/authority-validator.ts +149 -0
  9. package/src/api/src/engine/context-assembler.ts +912 -0
  10. package/src/api/src/engine/index.ts +27 -0
  11. package/src/api/src/engine/knowledge-gate.ts +365 -0
  12. package/src/api/src/engine/llm-adapter.ts +304 -0
  13. package/src/api/src/engine/org-tree.ts +270 -0
  14. package/src/api/src/engine/role-lifecycle.ts +369 -0
  15. package/src/api/src/engine/runners/claude-cli.ts +796 -0
  16. package/src/api/src/engine/runners/direct-api.ts +66 -0
  17. package/src/api/src/engine/runners/index.ts +30 -0
  18. package/src/api/src/engine/runners/types.ts +95 -0
  19. package/src/api/src/engine/skill-template.ts +134 -0
  20. package/src/api/src/engine/tools/definitions.ts +201 -0
  21. package/src/api/src/engine/tools/executor.ts +611 -0
  22. package/src/api/src/routes/active-sessions.ts +134 -0
  23. package/src/api/src/routes/coins.ts +153 -0
  24. package/src/api/src/routes/company.ts +57 -0
  25. package/src/api/src/routes/cost.ts +141 -0
  26. package/src/api/src/routes/engine.ts +220 -0
  27. package/src/api/src/routes/execute.ts +1075 -0
  28. package/src/api/src/routes/git.ts +211 -0
  29. package/src/api/src/routes/knowledge.ts +378 -0
  30. package/src/api/src/routes/operations.ts +309 -0
  31. package/src/api/src/routes/preferences.ts +63 -0
  32. package/src/api/src/routes/presets.ts +123 -0
  33. package/src/api/src/routes/projects.ts +82 -0
  34. package/src/api/src/routes/quests.ts +41 -0
  35. package/src/api/src/routes/roles.ts +112 -0
  36. package/src/api/src/routes/save.ts +152 -0
  37. package/src/api/src/routes/sessions.ts +288 -0
  38. package/src/api/src/routes/setup.ts +437 -0
  39. package/src/api/src/routes/skills.ts +357 -0
  40. package/src/api/src/routes/speech.ts +959 -0
  41. package/src/api/src/routes/supervision.ts +136 -0
  42. package/src/api/src/routes/sync.ts +165 -0
  43. package/src/api/src/server.ts +59 -0
  44. package/src/api/src/services/activity-stream.ts +184 -0
  45. package/src/api/src/services/activity-tracker.ts +115 -0
  46. package/src/api/src/services/claude-md-manager.ts +94 -0
  47. package/src/api/src/services/company-config.ts +115 -0
  48. package/src/api/src/services/database.ts +77 -0
  49. package/src/api/src/services/digest-engine.ts +313 -0
  50. package/src/api/src/services/execution-manager.ts +1036 -0
  51. package/src/api/src/services/file-reader.ts +77 -0
  52. package/src/api/src/services/git-save.ts +614 -0
  53. package/src/api/src/services/job-manager.ts +16 -0
  54. package/src/api/src/services/knowledge-importer.ts +466 -0
  55. package/src/api/src/services/markdown-parser.ts +173 -0
  56. package/src/api/src/services/port-registry.ts +222 -0
  57. package/src/api/src/services/preferences.ts +150 -0
  58. package/src/api/src/services/preset-loader.ts +149 -0
  59. package/src/api/src/services/pricing.ts +34 -0
  60. package/src/api/src/services/scaffold.ts +546 -0
  61. package/src/api/src/services/session-store.ts +340 -0
  62. package/src/api/src/services/supervisor-heartbeat.ts +897 -0
  63. package/src/api/src/services/team-recommender.ts +382 -0
  64. package/src/api/src/services/token-ledger.ts +127 -0
  65. package/src/api/src/services/wave-messages.ts +194 -0
  66. package/src/api/src/services/wave-multiplexer.ts +356 -0
  67. package/src/api/src/services/wave-tracker.ts +359 -0
  68. package/src/api/src/utils/role-level.ts +31 -0
  69. package/src/core/scaffolder.ts +620 -0
  70. package/src/shared/types.ts +224 -0
  71. package/templates/CLAUDE.md.tmpl +239 -0
  72. package/templates/company.md.tmpl +17 -0
  73. package/templates/gitignore.tmpl +28 -0
  74. package/templates/roles.md.tmpl +8 -0
  75. package/templates/skills/_manifest.json +23 -0
  76. package/templates/skills/agent-browser/SKILL.md +159 -0
  77. package/templates/skills/agent-browser/meta.json +19 -0
  78. package/templates/skills/akb-linter/SKILL.md +125 -0
  79. package/templates/skills/akb-linter/meta.json +12 -0
  80. package/templates/skills/knowledge-gate/SKILL.md +120 -0
  81. package/templates/skills/knowledge-gate/meta.json +12 -0
  82. package/templates/teams/agency.json +58 -0
  83. package/templates/teams/research.json +58 -0
  84. package/templates/teams/startup.json +58 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * skills.ts — Skill registry + Role-Skill management API
3
+ */
4
+ import { Router, Request, Response, NextFunction } from 'express';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import YAML from 'yaml';
9
+ import { COMPANY_ROOT } from '../services/file-reader.js';
10
+ import { getAvailableSkills } from '../services/scaffold.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ export const skillsRouter = Router();
16
+
17
+ /* ─── Skill Registry ─────────────────────── */
18
+
19
+ // GET /api/skills — Available skills (from templates + installed)
20
+ skillsRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
21
+ try {
22
+ // 1. Template skills (bundled with the product)
23
+ const templateSkills = getAvailableSkills().map(s => ({
24
+ ...s,
25
+ source: 'template' as const,
26
+ installed: isSkillInstalled(s.id),
27
+ }));
28
+
29
+ // 2. Installed skills not in templates (user-added)
30
+ const sharedDir = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared');
31
+ const installedIds = new Set(templateSkills.map(s => s.id));
32
+ const userSkills: Array<Record<string, unknown>> = [];
33
+
34
+ if (fs.existsSync(sharedDir)) {
35
+ for (const entry of fs.readdirSync(sharedDir, { withFileTypes: true })) {
36
+ if (!entry.isDirectory() || installedIds.has(entry.name)) continue;
37
+ const skillMdPath = path.join(sharedDir, entry.name, 'SKILL.md');
38
+ if (!fs.existsSync(skillMdPath)) continue;
39
+
40
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
41
+ const meta = extractSkillMeta(content, entry.name);
42
+ userSkills.push({ ...meta, source: 'user', installed: true });
43
+ }
44
+ }
45
+
46
+ res.json([...templateSkills, ...userSkills]);
47
+ } catch (err) {
48
+ next(err);
49
+ }
50
+ });
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
+
130
+ /* ─── Skill Export (for Store publish) ──── */
131
+
132
+ // GET /api/skills/export/:roleId — Export full SKILL.md content for publishing
133
+ // NOTE: Must be registered BEFORE /:id to avoid "export" matching as an id
134
+ skillsRouter.get('/export/:roleId', (req: Request, res: Response, next: NextFunction) => {
135
+ try {
136
+ const roleId = req.params.roleId as string;
137
+
138
+ // 1. Primary skill: .claude/skills/{roleId}/SKILL.md
139
+ let primary: { frontmatter: Record<string, unknown>; body: string } | null = null;
140
+ const primaryPath = path.join(COMPANY_ROOT, '.claude', 'skills', roleId, 'SKILL.md');
141
+ if (fs.existsSync(primaryPath)) {
142
+ const content = fs.readFileSync(primaryPath, 'utf-8');
143
+ primary = parseSkillContent(content, roleId);
144
+ }
145
+
146
+ // 2. Shared skills from role.yaml skills[] array
147
+ const sharedIds = getRoleSkills(roleId);
148
+ const shared: Array<{ id: string; frontmatter: Record<string, unknown>; body: string }> = [];
149
+
150
+ for (const sharedId of sharedIds) {
151
+ const sharedPath = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', sharedId, 'SKILL.md');
152
+ if (fs.existsSync(sharedPath)) {
153
+ const content = fs.readFileSync(sharedPath, 'utf-8');
154
+ const parsed = parseSkillContent(content, sharedId);
155
+ shared.push({ id: sharedId, ...parsed });
156
+ }
157
+ }
158
+
159
+ res.json({ primary, shared });
160
+ } catch (err) {
161
+ next(err);
162
+ }
163
+ });
164
+
165
+ // GET /api/skills/:id — Skill detail (wildcard — must be LAST among GET routes)
166
+ skillsRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
167
+ try {
168
+ const id = req.params.id as string;
169
+
170
+ // Check installed first
171
+ const installedPath = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', id, 'SKILL.md');
172
+ if (fs.existsSync(installedPath)) {
173
+ const content = fs.readFileSync(installedPath, 'utf-8');
174
+ const meta = extractSkillMeta(content, id);
175
+ res.json({ ...meta, installed: true, content });
176
+ return;
177
+ }
178
+
179
+ // Check template
180
+ const templateSkills = getAvailableSkills();
181
+ const templateSkill = templateSkills.find(s => s.id === id);
182
+ if (templateSkill) {
183
+ res.json({ ...templateSkill, installed: false });
184
+ return;
185
+ }
186
+
187
+ res.status(404).json({ error: `Skill not found: ${id}` });
188
+ } catch (err) {
189
+ next(err);
190
+ }
191
+ });
192
+
193
+ /* ─── Role-Skill Management ─────────────── */
194
+
195
+ // GET /api/roles/:id/skills — Skills equipped to a role
196
+ skillsRouter.get('/role/:roleId', (req: Request, res: Response, next: NextFunction) => {
197
+ try {
198
+ const roleId = req.params.roleId as string;
199
+ const skills = getRoleSkills(roleId);
200
+ res.json(skills);
201
+ } catch (err) {
202
+ next(err);
203
+ }
204
+ });
205
+
206
+ // POST /api/roles/:id/skills — Equip a skill to a role
207
+ skillsRouter.post('/role/:roleId', (req: Request, res: Response, next: NextFunction) => {
208
+ try {
209
+ const roleId = req.params.roleId as string;
210
+ const { skillId } = req.body;
211
+
212
+ if (!skillId || typeof skillId !== 'string') {
213
+ res.status(400).json({ error: 'skillId is required' });
214
+ return;
215
+ }
216
+
217
+ // Verify skill exists (installed or template)
218
+ if (!isSkillInstalled(skillId) && !isSkillInTemplate(skillId)) {
219
+ res.status(404).json({ error: `Skill not found: ${skillId}` });
220
+ return;
221
+ }
222
+
223
+ // Install skill if not already installed
224
+ if (!isSkillInstalled(skillId)) {
225
+ installSkillFromTemplate(skillId);
226
+ }
227
+
228
+ // Add to role.yaml skills array
229
+ const yamlPath = path.join(COMPANY_ROOT, 'knowledge', 'roles', roleId, 'role.yaml');
230
+ if (!fs.existsSync(yamlPath)) {
231
+ res.status(404).json({ error: `Role not found: ${roleId}` });
232
+ return;
233
+ }
234
+
235
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
236
+ const skills = (raw.skills as string[]) || [];
237
+
238
+ if (skills.includes(skillId)) {
239
+ res.json({ ok: true, message: 'Skill already equipped' });
240
+ return;
241
+ }
242
+
243
+ skills.push(skillId);
244
+ raw.skills = skills;
245
+ fs.writeFileSync(yamlPath, YAML.stringify(raw));
246
+
247
+ res.json({ ok: true, roleId, skillId, skills });
248
+ } catch (err) {
249
+ next(err);
250
+ }
251
+ });
252
+
253
+ // DELETE /api/roles/:roleId/skills/:skillId — Unequip a skill
254
+ skillsRouter.delete('/role/:roleId/:skillId', (req: Request, res: Response, next: NextFunction) => {
255
+ try {
256
+ const roleId = req.params.roleId as string;
257
+ const skillId = req.params.skillId as string;
258
+
259
+ const yamlPath = path.join(COMPANY_ROOT, 'knowledge', 'roles', roleId, 'role.yaml');
260
+ if (!fs.existsSync(yamlPath)) {
261
+ res.status(404).json({ error: `Role not found: ${roleId}` });
262
+ return;
263
+ }
264
+
265
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
266
+ const skills = (raw.skills as string[]) || [];
267
+ const idx = skills.indexOf(skillId);
268
+
269
+ if (idx === -1) {
270
+ res.json({ ok: true, message: 'Skill not equipped' });
271
+ return;
272
+ }
273
+
274
+ skills.splice(idx, 1);
275
+ raw.skills = skills.length > 0 ? skills : undefined;
276
+ fs.writeFileSync(yamlPath, YAML.stringify(raw));
277
+
278
+ res.json({ ok: true, roleId, skillId, skills });
279
+ } catch (err) {
280
+ next(err);
281
+ }
282
+ });
283
+
284
+ /* ─── Helpers ────────────────────────────── */
285
+
286
+ function isSkillInstalled(skillId: string): boolean {
287
+ return fs.existsSync(
288
+ path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', skillId, 'SKILL.md')
289
+ );
290
+ }
291
+
292
+ function isSkillInTemplate(skillId: string): boolean {
293
+ return getAvailableSkills().some(s => s.id === skillId);
294
+ }
295
+
296
+ function installSkillFromTemplate(skillId: string): void {
297
+ const srcDir = path.resolve(__dirname, '../../../../../templates/skills', skillId);
298
+ const destDir = path.join(COMPANY_ROOT, '.claude', 'skills', '_shared', skillId);
299
+
300
+ if (!fs.existsSync(srcDir)) return;
301
+ fs.mkdirSync(destDir, { recursive: true });
302
+
303
+ const skillMdSrc = path.join(srcDir, 'SKILL.md');
304
+ if (fs.existsSync(skillMdSrc)) {
305
+ fs.copyFileSync(skillMdSrc, path.join(destDir, 'SKILL.md'));
306
+ }
307
+ }
308
+
309
+ function getRoleSkills(roleId: string): string[] {
310
+ const yamlPath = path.join(COMPANY_ROOT, 'knowledge', 'roles', roleId, 'role.yaml');
311
+ if (!fs.existsSync(yamlPath)) return [];
312
+
313
+ const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
314
+ return (raw.skills as string[]) || [];
315
+ }
316
+
317
+ function extractSkillMeta(content: string, id: string): Record<string, unknown> {
318
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
319
+ if (!frontmatter) {
320
+ return { id, name: id, description: '' };
321
+ }
322
+
323
+ try {
324
+ const meta = YAML.parse(frontmatter[1]) as Record<string, unknown>;
325
+ return {
326
+ id,
327
+ name: meta.name || id,
328
+ description: meta.description || '',
329
+ ...(meta.tags ? { tags: meta.tags } : {}),
330
+ };
331
+ } catch {
332
+ return { id, name: id, description: '' };
333
+ }
334
+ }
335
+
336
+ function parseSkillContent(content: string, id: string): { frontmatter: Record<string, unknown>; body: string } {
337
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/);
338
+ if (!fmMatch) {
339
+ return { frontmatter: { name: id, description: '' }, body: content };
340
+ }
341
+
342
+ try {
343
+ const meta = YAML.parse(fmMatch[1]) as Record<string, unknown>;
344
+ return {
345
+ frontmatter: {
346
+ name: meta.name || id,
347
+ description: (meta.description as string) || '',
348
+ ...(meta.allowedTools ? { allowedTools: meta.allowedTools } : {}),
349
+ ...(meta.model ? { model: meta.model } : {}),
350
+ ...(meta.tags ? { tags: meta.tags } : {}),
351
+ },
352
+ body: fmMatch[2].trim(),
353
+ };
354
+ } catch {
355
+ return { frontmatter: { name: id, description: '' }, body: content };
356
+ }
357
+ }