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
package/bin/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { execFileSync } from 'node:child_process';
7
+
8
+ // Auto-increase heap if not already set
9
+ if (!process.env.__TYCONO_HEAP_SET && !process.execArgv.some(a => a.includes('max-old-space-size'))) {
10
+ process.env.__TYCONO_HEAP_SET = '1';
11
+ try {
12
+ execFileSync(process.execPath, [
13
+ '--max-old-space-size=4096',
14
+ ...process.execArgv,
15
+ fileURLToPath(import.meta.url),
16
+ ...process.argv.slice(2),
17
+ ], { stdio: 'inherit', env: process.env });
18
+ } catch (e) {
19
+ process.exit(e.status ?? 1);
20
+ }
21
+ process.exit(0);
22
+ }
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+
27
+ // Resolve tsx
28
+ const require = createRequire(import.meta.url);
29
+ const tsxApiPath = pathToFileURL(require.resolve('tsx/esm/api')).href;
30
+ const tsx = await import(tsxApiPath);
31
+ tsx.register();
32
+
33
+ const entryPath = pathToFileURL(join(__dirname, 'server.ts')).href;
34
+ const { main } = await import(entryPath);
35
+ await main(process.argv.slice(2));
package/bin/server.ts ADDED
@@ -0,0 +1,160 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import net from 'node:net';
4
+ import { execSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const VERSION = JSON.parse(
11
+ fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')
12
+ ).version;
13
+
14
+ function printHelp(): void {
15
+ console.log(`
16
+ tycono-server v${VERSION}
17
+
18
+ AI team orchestration server. Headless by default.
19
+
20
+ Usage:
21
+ tycono-server [path] Start API server (default: current directory)
22
+ tycono-server --help Show this help
23
+ tycono-server --version Show version
24
+
25
+ Examples:
26
+ tycono-server Start in current directory
27
+ tycono-server ./my-company Start with company directory
28
+ PORT=3001 tycono-server Use specific port
29
+
30
+ Environment:
31
+ PORT Server port (default: auto-detect free port)
32
+ COMPANY_ROOT Company directory (default: current directory)
33
+ `);
34
+ }
35
+
36
+ function findFreePort(): Promise<number> {
37
+ return new Promise((resolve, reject) => {
38
+ const server = net.createServer();
39
+ server.listen(0, () => {
40
+ const addr = server.address();
41
+ if (addr && typeof addr === 'object') {
42
+ const port = addr.port;
43
+ server.close(() => resolve(port));
44
+ } else {
45
+ reject(new Error('Could not find free port'));
46
+ }
47
+ });
48
+ server.on('error', reject);
49
+ });
50
+ }
51
+
52
+ function detectEngine(): string {
53
+ try {
54
+ execSync('claude --version', { stdio: 'pipe' });
55
+ return 'claude-cli';
56
+ } catch {}
57
+ if (process.env.ANTHROPIC_API_KEY) return 'direct-api';
58
+ return 'none';
59
+ }
60
+
61
+ export async function main(args: string[]): Promise<void> {
62
+ const command = args[0];
63
+
64
+ if (command === '--help' || command === '-h') {
65
+ printHelp();
66
+ return;
67
+ }
68
+
69
+ if (command === '--version' || command === '-v') {
70
+ console.log(VERSION);
71
+ return;
72
+ }
73
+
74
+ // Load .env
75
+ const dotenvPath = path.resolve(process.cwd(), '.env');
76
+ if (fs.existsSync(dotenvPath)) {
77
+ const { config } = await import('dotenv');
78
+ config({ path: dotenvPath });
79
+ }
80
+
81
+ // Company root
82
+ if (command && !command.startsWith('-')) {
83
+ process.env.COMPANY_ROOT = path.resolve(command);
84
+ }
85
+ if (!process.env.COMPANY_ROOT) {
86
+ process.env.COMPANY_ROOT = process.cwd();
87
+ }
88
+
89
+ // Check for knowledge/ subdirectory pattern
90
+ const knowledgeDir = path.join(process.env.COMPANY_ROOT, 'knowledge');
91
+ if (fs.existsSync(path.join(knowledgeDir, 'CLAUDE.md'))) {
92
+ // Company root is the parent of knowledge/
93
+ } else if (!fs.existsSync(path.join(process.env.COMPANY_ROOT, 'CLAUDE.md'))) {
94
+ // Scan one level deep
95
+ try {
96
+ const entries = fs.readdirSync(process.env.COMPANY_ROOT, { withFileTypes: true });
97
+ for (const entry of entries) {
98
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
99
+ if (fs.existsSync(path.join(process.env.COMPANY_ROOT, entry.name, '.tycono', 'config.json'))) {
100
+ process.env.COMPANY_ROOT = path.join(process.env.COMPANY_ROOT, entry.name);
101
+ break;
102
+ }
103
+ }
104
+ } catch {}
105
+ }
106
+
107
+ process.env.NODE_ENV = 'production';
108
+
109
+ // Detect engine
110
+ const engine = detectEngine();
111
+ process.env.EXECUTION_ENGINE = engine;
112
+
113
+ // Port
114
+ const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort();
115
+ process.env.PORT = String(port);
116
+
117
+ // Company name
118
+ let companyName = 'My Company';
119
+ try {
120
+ const claudeMdPath = path.join(process.env.COMPANY_ROOT, 'CLAUDE.md');
121
+ if (fs.existsSync(claudeMdPath)) {
122
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
123
+ const match = content.match(/^#\s+(.+)/m);
124
+ if (match) companyName = match[1].trim();
125
+ }
126
+ } catch {}
127
+
128
+ const url = `http://localhost:${port}`;
129
+
130
+ console.log(` tycono-server v${VERSION}`);
131
+ console.log(` API: ${url}`);
132
+ console.log(` Company: ${companyName}`);
133
+ console.log(` Engine: ${engine}`);
134
+ console.log(` PID: ${process.pid}`);
135
+
136
+ // Write headless.json for discovery
137
+ const headlessInfo = { port, pid: process.pid, url, startedAt: new Date().toISOString() };
138
+ const headlessPath = path.join(process.env.COMPANY_ROOT, '.tycono', 'headless.json');
139
+ try {
140
+ fs.mkdirSync(path.dirname(headlessPath), { recursive: true });
141
+ fs.writeFileSync(headlessPath, JSON.stringify(headlessInfo, null, 2));
142
+ } catch {}
143
+
144
+ // Start server
145
+ const { createHttpServer } = await import('../src/api/src/create-server.js');
146
+ const server = createHttpServer();
147
+
148
+ const host = process.env.HOST || '0.0.0.0';
149
+ server.listen(port, host);
150
+
151
+ // Graceful shutdown
152
+ const shutdown = () => {
153
+ console.log('\n Shutting down...');
154
+ try { fs.unlinkSync(headlessPath); } catch {}
155
+ server.close(() => process.exit(0));
156
+ setTimeout(() => process.exit(1), 5000);
157
+ };
158
+ process.on('SIGINT', shutdown);
159
+ process.on('SIGTERM', shutdown);
160
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "tycono-server",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Tycono AI team orchestration server. Dispatch, supervise, and observe AI agents working as a team.",
5
+ "type": "module",
6
+ "bin": {
7
+ "tycono-server": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "templates/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "start": "node bin/cli.js"
19
+ },
20
+ "dependencies": {
21
+ "@anthropic-ai/sdk": "^0.78.0",
22
+ "better-sqlite3": "^12.8.0",
23
+ "cors": "^2.8.5",
24
+ "dotenv": "^16.4.7",
25
+ "express": "^5.0.1",
26
+ "glob": "^13.0.6",
27
+ "gray-matter": "^4.0.3",
28
+ "marked": "^15.0.6",
29
+ "tsx": "^4.19.3",
30
+ "yaml": "^2.7.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/better-sqlite3": "^7.6.13",
34
+ "@types/cors": "^2.8.17",
35
+ "@types/express": "^5.0.0",
36
+ "@types/node": "^22.13.4",
37
+ "typescript": "^5.7.3"
38
+ },
39
+ "keywords": [
40
+ "ai", "tycono", "agent", "multi-agent", "orchestration",
41
+ "dispatch", "supervision", "agentic", "team"
42
+ ],
43
+ "author": "Tycono <hello@tycono.ai>",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/seongsu-kang/tycono.git",
48
+ "directory": "packages/server"
49
+ }
50
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "tycono-api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "start": "tsx src/server.ts",
9
+ "test": "vitest run tests/smoke.test.ts",
10
+ "test:watch": "vitest tests/smoke.test.ts"
11
+ },
12
+ "dependencies": {
13
+ "@anthropic-ai/sdk": "^0.78.0",
14
+ "cors": "^2.8.5",
15
+ "express": "^5.0.1",
16
+ "glob": "^11.0.1",
17
+ "gray-matter": "^4.0.3",
18
+ "marked": "^15.0.6",
19
+ "yaml": "^2.7.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/cors": "^2.8.17",
23
+ "@types/express": "^5.0.0",
24
+ "@types/node": "^22.13.4",
25
+ "@types/supertest": "^7.2.0",
26
+ "supertest": "^7.2.2",
27
+ "tsx": "^4.19.3",
28
+ "typescript": "^5.7.3",
29
+ "vitest": "^4.0.18"
30
+ }
31
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * create-app.ts — Express 앱 팩토리
3
+ *
4
+ * server.ts에서 분리하여 테스트에서 재사용 가능하게 한다.
5
+ * supertest 등에서 import 후 테스트용 앱으로 활용.
6
+ */
7
+ import express from 'express';
8
+ import cors from 'cors';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { COMPANY_ROOT } from './services/file-reader.js';
12
+ import { rolesRouter } from './routes/roles.js';
13
+ import { projectsRouter } from './routes/projects.js';
14
+ import { operationsRouter } from './routes/operations.js';
15
+ import { companyRouter } from './routes/company.js';
16
+ import { engineRouter } from './routes/engine.js';
17
+ import { sessionsRouter } from './routes/sessions.js';
18
+ import { setupRouter } from './routes/setup.js';
19
+ import { skillsRouter } from './routes/skills.js';
20
+ import { presetsRouter } from './routes/presets.js';
21
+
22
+ export function createApp() {
23
+ const app = express();
24
+
25
+ const corsOrigin = process.env.NODE_ENV === 'production' ? true : /^http:\/\/localhost:\d+$/;
26
+ app.use(cors({ origin: corsOrigin }));
27
+ app.use(express.json());
28
+
29
+ // Suppress favicon 404
30
+ app.get('/favicon.ico', (_req, res) => res.status(204).end());
31
+
32
+ // Setup / onboarding (always available)
33
+ app.use('/api/setup', setupRouter);
34
+
35
+ // Status — frontend checks this to decide wizard vs office
36
+ app.get('/api/status', (_req, res) => {
37
+ const claudeMdPath = path.join(COMPANY_ROOT, 'CLAUDE.md');
38
+ const initialized = fs.existsSync(claudeMdPath);
39
+ let companyName: string | null = null;
40
+ if (initialized) {
41
+ try {
42
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
43
+ const match = content.match(/^#\s+(.+)/m);
44
+ if (match) companyName = match[1].trim();
45
+ } catch { /* ignore */ }
46
+ }
47
+ res.json({ initialized, companyName, engine: process.env.EXECUTION_ENGINE || 'none', companyRoot: COMPANY_ROOT });
48
+ });
49
+
50
+ app.use('/api/roles', rolesRouter);
51
+ app.use('/api/projects', projectsRouter);
52
+ app.use('/api/operations', operationsRouter);
53
+ app.use('/api/company', companyRouter);
54
+ app.use('/api/engine', engineRouter);
55
+ app.use('/api/sessions', sessionsRouter);
56
+ app.use('/api/skills', skillsRouter);
57
+ app.use('/api/presets', presetsRouter);
58
+
59
+ app.get('/api/health', (_req, res) => {
60
+ res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
61
+ });
62
+
63
+ // Debug: memory stats for leak detection
64
+ app.get('/api/debug/memory', (_req, res) => {
65
+ const mem = process.memoryUsage();
66
+ // Import dynamically to avoid circular deps
67
+ import('./services/execution-manager.js').then(({ executionManager }) => {
68
+ import('./services/session-store.js').then(({ listSessions }) => {
69
+ res.json({
70
+ heap: { used: Math.round(mem.heapUsed / 1024 / 1024), total: Math.round(mem.heapTotal / 1024 / 1024) },
71
+ rss: Math.round(mem.rss / 1024 / 1024),
72
+ execManager: executionManager.getMemoryStats(),
73
+ sessions: listSessions().length,
74
+ });
75
+ });
76
+ }).catch(() => {
77
+ res.json({
78
+ heap: { used: Math.round(mem.heapUsed / 1024 / 1024), total: Math.round(mem.heapTotal / 1024 / 1024) },
79
+ rss: Math.round(mem.rss / 1024 / 1024),
80
+ });
81
+ });
82
+ });
83
+
84
+ app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
85
+ const status = err.name === 'FileNotFoundError' ? 404 : 500;
86
+ res.status(status).json({ error: err.message });
87
+ });
88
+
89
+ return app;
90
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * create-server.ts — HTTP 서버 팩토리
3
+ *
4
+ * server.ts에서 서버 생성 로직을 분리하여 테스트에서 재사용 가능하게 한다.
5
+ * 반환된 서버 인스턴스는 listen()을 직접 호출하지 않으므로,
6
+ * 호출자가 포트를 지정해서 기동할 수 있다.
7
+ */
8
+ import fs from 'node:fs';
9
+ import http from 'node:http';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import express from 'express';
13
+ import cors from 'cors';
14
+ import { COMPANY_ROOT } from './services/file-reader.js';
15
+ import { rolesRouter } from './routes/roles.js';
16
+ import { projectsRouter } from './routes/projects.js';
17
+ import { operationsRouter } from './routes/operations.js';
18
+ import { companyRouter } from './routes/company.js';
19
+ import { handleExecRequest } from './routes/execute.js';
20
+ import { engineRouter } from './routes/engine.js';
21
+ import { sessionsRouter } from './routes/sessions.js';
22
+ import { setupRouter } from './routes/setup.js';
23
+ // activity-tracker removed — executionManager resets on restart
24
+ import { knowledgeRouter } from './routes/knowledge.js';
25
+ import { preferencesRouter } from './routes/preferences.js';
26
+ import { saveRouter } from './routes/save.js';
27
+ import { speechRouter } from './routes/speech.js';
28
+ import { costRouter } from './routes/cost.js';
29
+ import { syncRouter } from './routes/sync.js';
30
+ import { gitRouter } from './routes/git.js';
31
+ import { skillsRouter } from './routes/skills.js';
32
+ import { questsRouter } from './routes/quests.js';
33
+ import { coinsRouter } from './routes/coins.js';
34
+ import { activeSessionsRouter } from './routes/active-sessions.js';
35
+ import { supervisionRouter } from './routes/supervision.js';
36
+ import { presetsRouter } from './routes/presets.js';
37
+ import { importKnowledge } from './services/knowledge-importer.js';
38
+ import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
39
+ import { readConfig } from './services/company-config.js';
40
+ import { ensureClaudeMd } from './services/claude-md-manager.js';
41
+
42
+ const __filename = fileURLToPath(import.meta.url);
43
+ const __dirname = path.dirname(__filename);
44
+ const isProd = process.env.NODE_ENV === 'production';
45
+ const corsOrigin = isProd ? true : /^http:\/\/localhost:\d+$/;
46
+
47
+ /* ─── Raw HTTP handler for import-knowledge SSE ─── */
48
+
49
+ function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerResponse): void {
50
+ let data = '';
51
+ req.on('data', (chunk) => { data += chunk; });
52
+ req.on('end', () => {
53
+ let body: Record<string, unknown>;
54
+ try { body = JSON.parse(data); } catch { body = {}; }
55
+
56
+ const importPaths = body.paths as string[] | undefined;
57
+ if (!importPaths || !Array.isArray(importPaths) || importPaths.length === 0) {
58
+ res.writeHead(400, { 'Content-Type': 'application/json' });
59
+ res.end(JSON.stringify({ error: 'paths array is required' }));
60
+ return;
61
+ }
62
+
63
+ const root = (body.companyRoot as string) || COMPANY_ROOT;
64
+
65
+ res.writeHead(200, {
66
+ 'Content-Type': 'text/event-stream',
67
+ 'Cache-Control': 'no-cache',
68
+ 'Connection': 'keep-alive',
69
+ 'X-Accel-Buffering': 'no',
70
+ });
71
+
72
+ const sendSSE = (event: string, eventData: unknown) => {
73
+ res.write(`event: ${event}\ndata: ${JSON.stringify(eventData)}\n\n`);
74
+ };
75
+
76
+ // Build LLMProvider from env if available
77
+ const llm: LLMProvider | undefined = process.env.ANTHROPIC_API_KEY
78
+ ? new AnthropicProvider({ model: 'claude-haiku-4-5-20251001' })
79
+ : undefined;
80
+
81
+ importKnowledge(importPaths, root, {
82
+ onScanning: (scanPath, fileCount) => sendSSE('scanning', { path: scanPath, fileCount }),
83
+ onProcessing: (file, index, total) => sendSSE('processing', { file, index, total }),
84
+ onCreated: (filePath, title, summary) => sendSSE('created', { path: filePath, title, summary }),
85
+ onSkipped: (file, reason) => sendSSE('skipped', { file, reason }),
86
+ onDone: (stats) => { sendSSE('done', stats); res.end(); },
87
+ onError: (message) => { sendSSE('error', { message }); res.end(); },
88
+ }, llm).catch((err) => {
89
+ sendSSE('error', { message: err instanceof Error ? err.message : 'Import failed' });
90
+ res.end();
91
+ });
92
+ });
93
+ }
94
+
95
+ export function createHttpServer(): http.Server {
96
+ // Only cleanup/ensure if a company is already initialized (avoid creating dirs in CWD)
97
+ if (COMPANY_ROOT && fs.existsSync(path.join(COMPANY_ROOT, 'knowledge', 'CLAUDE.md'))) {
98
+ ensureClaudeMd(COMPANY_ROOT);
99
+ }
100
+
101
+ const app = createExpressApp();
102
+
103
+ const server = http.createServer((req, res) => {
104
+ const url = req.url ?? '';
105
+ const method = req.method ?? '';
106
+
107
+ // GET /api/waves/active — restore active waves after refresh
108
+ if (url === '/api/waves/active' && method === 'GET') {
109
+ setExecCors(req, res);
110
+ handleExecRequest(req, res);
111
+ return;
112
+ }
113
+
114
+ // SSE multiplexed wave stream (GET /api/waves/:waveId/stream)
115
+ if (url.match(/^\/api\/waves\/[^/]+\/stream/) && method === 'GET') {
116
+ setExecCors(req, res);
117
+ handleExecRequest(req, res);
118
+ return;
119
+ }
120
+
121
+ // D-014: POST /api/sessions/:id/message — delegate to execute handler for SSE streaming
122
+ const sessionMessageMatch = url.match(/^\/api\/sessions\/([^/]+)\/message$/);
123
+ if (sessionMessageMatch && method === 'POST') {
124
+ setExecCors(req, res);
125
+ // Rewrite URL to legacy format for handleExecRequest
126
+ req.url = `/api/exec/session/${sessionMessageMatch[1]}/message`;
127
+ handleExecRequest(req, res);
128
+ return;
129
+ }
130
+
131
+ // SSE 엔드포인트: Express 우회하여 raw HTTP로 처리
132
+ // BUG-008: /api/waves/:waveId/directive and /api/waves/:waveId/question POST도 포함
133
+ if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.startsWith('/api/waves/') || url === '/api/waves/save' || url === '/api/setup/import-knowledge') && method === 'POST') {
134
+ setExecCors(req, res);
135
+ if (url === '/api/setup/import-knowledge') {
136
+ handleImportKnowledge(req, res);
137
+ } else {
138
+ handleExecRequest(req, res);
139
+ }
140
+ return;
141
+ }
142
+
143
+ // CORS preflight for exec/jobs/sessions endpoints
144
+ if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.match(/^\/api\/sessions\/[^/]+\/message$/)) && method === 'OPTIONS') {
145
+ setExecCors(req, res);
146
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
147
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
148
+ res.writeHead(204);
149
+ res.end();
150
+ return;
151
+ }
152
+
153
+ // Non-SSE exec/jobs endpoints (GET, DELETE)
154
+ if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs')) && (method === 'GET' || method === 'DELETE')) {
155
+ setExecCors(req, res);
156
+ handleExecRequest(req, res);
157
+ return;
158
+ }
159
+
160
+ // 나머지는 Express 처리
161
+ (app as (req: http.IncomingMessage, res: http.ServerResponse) => void)(req, res);
162
+ });
163
+
164
+ server.timeout = 0;
165
+ server.requestTimeout = 0;
166
+ server.headersTimeout = 0;
167
+
168
+ return server;
169
+ }
170
+
171
+ export function createExpressApp(): express.Application {
172
+ const app = express();
173
+
174
+ app.use(cors({ origin: corsOrigin }));
175
+ app.use(express.json());
176
+
177
+ // Setup / onboarding
178
+ app.use('/api/setup', setupRouter);
179
+
180
+ // Status — frontend checks this to decide wizard vs office
181
+ app.get('/api/status', (_req, res) => {
182
+ const config = readConfig(COMPANY_ROOT);
183
+ const tyconoDir = path.join(COMPANY_ROOT, '.tycono', 'config.json');
184
+ const initialized = fs.existsSync(tyconoDir);
185
+ let companyName: string | null = null;
186
+ if (initialized) {
187
+ try {
188
+ // Read company name from knowledge/company.md (user-owned data)
189
+ const companyMdPath = path.join(COMPANY_ROOT, 'knowledge', 'company.md');
190
+ const content = fs.readFileSync(companyMdPath, 'utf-8');
191
+ const match = content.match(/^#\s+(.+)/m);
192
+ if (match) companyName = match[1].trim();
193
+ } catch { /* ignore */ }
194
+ }
195
+ res.json({ initialized, companyName, engine: config.engine || process.env.EXECUTION_ENGINE || 'none', companyRoot: COMPANY_ROOT, codeRoot: config.codeRoot || null, hasApiKey: !!process.env.ANTHROPIC_API_KEY });
196
+ });
197
+
198
+ app.use('/api/roles', rolesRouter);
199
+ app.use('/api/projects', projectsRouter);
200
+ app.use('/api/operations', operationsRouter);
201
+ app.use('/api/company', companyRouter);
202
+ app.use('/api/engine', engineRouter);
203
+ app.use('/api/sessions', sessionsRouter);
204
+ app.use('/api/knowledge', knowledgeRouter);
205
+ app.use('/api/preferences', preferencesRouter);
206
+ app.use('/api/speech', speechRouter);
207
+ app.use('/api/save', saveRouter);
208
+ app.use('/api/cost', costRouter);
209
+ app.use('/api/sync', syncRouter);
210
+ app.use('/api/git', gitRouter);
211
+ app.use('/api/skills', skillsRouter);
212
+ app.use('/api/quests', questsRouter);
213
+ app.use('/api/coins', coinsRouter);
214
+ app.use('/api/active-sessions', activeSessionsRouter);
215
+ app.use('/api/supervision', supervisionRouter);
216
+ app.use('/api/presets', presetsRouter);
217
+
218
+ app.get('/api/health', (_req, res) => {
219
+ res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
220
+ });
221
+
222
+ // Production: serve web build as static files (SPA fallback)
223
+ if (isProd) {
224
+ const distPath = path.resolve(__dirname, '../../web/dist');
225
+ app.use(express.static(distPath));
226
+ app.use((_req, res) => {
227
+ res.sendFile(path.join(distPath, 'index.html'));
228
+ });
229
+ }
230
+
231
+ app.use((err: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => {
232
+ const status = err.name === 'FileNotFoundError' ? 404 : 500;
233
+ // Log server errors via console.error (redirected to log file in TUI mode)
234
+ // 404 errors are expected (e.g. fresh install, no company.md) — skip
235
+ if (status >= 500) {
236
+ console.error(`[ERROR] ${req.method} ${req.url} — ${err.message}`);
237
+ }
238
+ res.status(status).json({ error: err.message });
239
+ });
240
+
241
+ return app;
242
+ }
243
+
244
+ function setExecCors(req: http.IncomingMessage, res: http.ServerResponse): void {
245
+ const origin = req.headers.origin;
246
+ if (!origin) return;
247
+ if (isProd || /^http:\/\/localhost:\d+$/.test(origin)) {
248
+ res.setHeader('Access-Control-Allow-Origin', origin);
249
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
250
+ }
251
+ }