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.
- package/bin/cli.js +35 -0
- package/bin/server.ts +160 -0
- package/package.json +50 -0
- package/src/api/package.json +31 -0
- package/src/api/src/create-app.ts +90 -0
- package/src/api/src/create-server.ts +251 -0
- package/src/api/src/engine/agent-loop.ts +738 -0
- package/src/api/src/engine/authority-validator.ts +149 -0
- package/src/api/src/engine/context-assembler.ts +912 -0
- package/src/api/src/engine/index.ts +27 -0
- package/src/api/src/engine/knowledge-gate.ts +365 -0
- package/src/api/src/engine/llm-adapter.ts +304 -0
- package/src/api/src/engine/org-tree.ts +270 -0
- package/src/api/src/engine/role-lifecycle.ts +369 -0
- package/src/api/src/engine/runners/claude-cli.ts +796 -0
- package/src/api/src/engine/runners/direct-api.ts +66 -0
- package/src/api/src/engine/runners/index.ts +30 -0
- package/src/api/src/engine/runners/types.ts +95 -0
- package/src/api/src/engine/skill-template.ts +134 -0
- package/src/api/src/engine/tools/definitions.ts +201 -0
- package/src/api/src/engine/tools/executor.ts +611 -0
- package/src/api/src/routes/active-sessions.ts +134 -0
- package/src/api/src/routes/coins.ts +153 -0
- package/src/api/src/routes/company.ts +57 -0
- package/src/api/src/routes/cost.ts +141 -0
- package/src/api/src/routes/engine.ts +220 -0
- package/src/api/src/routes/execute.ts +1075 -0
- package/src/api/src/routes/git.ts +211 -0
- package/src/api/src/routes/knowledge.ts +378 -0
- package/src/api/src/routes/operations.ts +309 -0
- package/src/api/src/routes/preferences.ts +63 -0
- package/src/api/src/routes/presets.ts +123 -0
- package/src/api/src/routes/projects.ts +82 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/roles.ts +112 -0
- package/src/api/src/routes/save.ts +152 -0
- package/src/api/src/routes/sessions.ts +288 -0
- package/src/api/src/routes/setup.ts +437 -0
- package/src/api/src/routes/skills.ts +357 -0
- package/src/api/src/routes/speech.ts +959 -0
- package/src/api/src/routes/supervision.ts +136 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/server.ts +59 -0
- package/src/api/src/services/activity-stream.ts +184 -0
- package/src/api/src/services/activity-tracker.ts +115 -0
- package/src/api/src/services/claude-md-manager.ts +94 -0
- package/src/api/src/services/company-config.ts +115 -0
- package/src/api/src/services/database.ts +77 -0
- package/src/api/src/services/digest-engine.ts +313 -0
- package/src/api/src/services/execution-manager.ts +1036 -0
- package/src/api/src/services/file-reader.ts +77 -0
- package/src/api/src/services/git-save.ts +614 -0
- package/src/api/src/services/job-manager.ts +16 -0
- package/src/api/src/services/knowledge-importer.ts +466 -0
- package/src/api/src/services/markdown-parser.ts +173 -0
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/preferences.ts +150 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/api/src/services/pricing.ts +34 -0
- package/src/api/src/services/scaffold.ts +546 -0
- package/src/api/src/services/session-store.ts +340 -0
- package/src/api/src/services/supervisor-heartbeat.ts +897 -0
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/api/src/services/token-ledger.ts +127 -0
- package/src/api/src/services/wave-messages.ts +194 -0
- package/src/api/src/services/wave-multiplexer.ts +356 -0
- package/src/api/src/services/wave-tracker.ts +359 -0
- package/src/api/src/utils/role-level.ts +31 -0
- package/src/core/scaffolder.ts +620 -0
- package/src/shared/types.ts +224 -0
- package/templates/CLAUDE.md.tmpl +239 -0
- package/templates/company.md.tmpl +17 -0
- package/templates/gitignore.tmpl +28 -0
- package/templates/roles.md.tmpl +8 -0
- package/templates/skills/_manifest.json +23 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/skills/akb-linter/SKILL.md +125 -0
- package/templates/skills/akb-linter/meta.json +12 -0
- package/templates/skills/knowledge-gate/SKILL.md +120 -0
- package/templates/skills/knowledge-gate/meta.json +12 -0
- package/templates/teams/agency.json +58 -0
- package/templates/teams/research.json +58 -0
- package/templates/teams/startup.json +58 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { readFile, listFiles, fileExists, COMPANY_ROOT } from '../services/file-reader.js';
|
|
3
|
+
import { extractBoldKeyValues } from '../services/markdown-parser.js';
|
|
4
|
+
import { ActivityStream } from '../services/activity-stream.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { type MessageStatus, isMessageActive } from '../../../shared/types.js';
|
|
8
|
+
|
|
9
|
+
export const operationsRouter = Router();
|
|
10
|
+
|
|
11
|
+
// --- Standups ---
|
|
12
|
+
operationsRouter.get('/standups', (_req: Request, res: Response, next: NextFunction) => {
|
|
13
|
+
try {
|
|
14
|
+
const files = listFiles('.tycono/standup');
|
|
15
|
+
const standups = files
|
|
16
|
+
.filter(f => f.endsWith('.md'))
|
|
17
|
+
.map(f => {
|
|
18
|
+
const date = path.basename(f, '.md');
|
|
19
|
+
const content = readFile(`.tycono/standup/${f}`);
|
|
20
|
+
return { date, content };
|
|
21
|
+
})
|
|
22
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
23
|
+
|
|
24
|
+
res.json(standups);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
next(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
operationsRouter.get('/standups/:date', (req: Request, res: Response, next: NextFunction) => {
|
|
31
|
+
try {
|
|
32
|
+
const { date } = req.params;
|
|
33
|
+
const filePath = `.tycono/standup/${date}.md`;
|
|
34
|
+
if (!fileExists(filePath)) {
|
|
35
|
+
res.status(404).json({ error: `Standup not found: ${date}` });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const content = readFile(filePath);
|
|
39
|
+
res.json({ date, content });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
next(err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Waves (JSON-only) ---
|
|
46
|
+
operationsRouter.get('/waves', (_req: Request, res: Response, next: NextFunction) => {
|
|
47
|
+
try {
|
|
48
|
+
const files = listFiles('.tycono/waves', '*.json');
|
|
49
|
+
const waves = files
|
|
50
|
+
.map(f => {
|
|
51
|
+
const id = path.basename(f, '.json');
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(readFile(`.tycono/waves/${f}`));
|
|
54
|
+
const roles = data.roles ?? [];
|
|
55
|
+
const hasRunning = roles.some((r: { status?: string }) => r.status && isMessageActive(r.status as MessageStatus));
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
timestamp: id,
|
|
59
|
+
directive: data.directive ?? '',
|
|
60
|
+
rolesCount: roles.length,
|
|
61
|
+
startedAt: data.startedAt ?? '',
|
|
62
|
+
...(data.commit ? { commit: data.commit } : {}),
|
|
63
|
+
...(hasRunning ? { hasRunning: true } : {}),
|
|
64
|
+
...(data.sessionIds && data.sessionIds.length > 0 ? { sessionIds: data.sessionIds } : {}),
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return { id, timestamp: id, directive: '', rolesCount: 0, startedAt: '' };
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
71
|
+
|
|
72
|
+
res.json(waves);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
next(err);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
operationsRouter.get('/waves/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
79
|
+
try {
|
|
80
|
+
const { id } = req.params;
|
|
81
|
+
const jsonPath = `.tycono/waves/${id}.json`;
|
|
82
|
+
|
|
83
|
+
if (!fileExists(jsonPath)) {
|
|
84
|
+
res.status(404).json({ error: `Wave not found: ${id}` });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = JSON.parse(readFile(jsonPath));
|
|
89
|
+
res.json({ id, timestamp: id, replay: data });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
next(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// PATCH /waves/:id — update wave metadata (e.g. commit info)
|
|
96
|
+
operationsRouter.patch('/waves/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
97
|
+
try {
|
|
98
|
+
const { id } = req.params;
|
|
99
|
+
const jsonPath = `.tycono/waves/${id}.json`;
|
|
100
|
+
|
|
101
|
+
if (!fileExists(jsonPath)) {
|
|
102
|
+
res.status(404).json({ error: `Wave not found: ${id}` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = JSON.parse(readFile(jsonPath));
|
|
107
|
+
const { commitSha, commitMessage, committedAt } = req.body ?? {};
|
|
108
|
+
if (commitSha) {
|
|
109
|
+
data.commit = { sha: commitSha, message: commitMessage ?? '', committedAt: committedAt ?? new Date().toISOString() };
|
|
110
|
+
}
|
|
111
|
+
const absPath = path.resolve(COMPANY_ROOT, jsonPath);
|
|
112
|
+
fs.writeFileSync(absPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
113
|
+
res.json({ ok: true });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
next(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// --- Decisions ---
|
|
120
|
+
operationsRouter.get('/decisions', (_req: Request, res: Response, next: NextFunction) => {
|
|
121
|
+
try {
|
|
122
|
+
const files = listFiles('knowledge/decisions');
|
|
123
|
+
const decisions = files
|
|
124
|
+
.filter(f => f.endsWith('.md'))
|
|
125
|
+
.map(f => {
|
|
126
|
+
const id = path.basename(f, '.md');
|
|
127
|
+
const content = readFile(`knowledge/decisions/${f}`);
|
|
128
|
+
const firstLine = content.split('\n').find(l => l.startsWith('# '));
|
|
129
|
+
const title = firstLine ? firstLine.replace(/^#\s+/, '') : id;
|
|
130
|
+
const kv = extractBoldKeyValues(content);
|
|
131
|
+
const date = kv['날짜'] ?? kv['date'] ?? '';
|
|
132
|
+
return { id, title, date, content };
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
res.json(decisions);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
next(err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
operationsRouter.put('/decisions/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
142
|
+
try {
|
|
143
|
+
const { id } = req.params;
|
|
144
|
+
const { content } = req.body ?? {};
|
|
145
|
+
if (typeof content !== 'string') {
|
|
146
|
+
res.status(400).json({ error: 'content (string) is required' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const filePath = `knowledge/decisions/${id}.md`;
|
|
150
|
+
const absPath = path.resolve(COMPANY_ROOT, filePath);
|
|
151
|
+
// Ensure parent directory exists
|
|
152
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
153
|
+
fs.writeFileSync(absPath, content, 'utf-8');
|
|
154
|
+
// Re-parse for response
|
|
155
|
+
const firstLine = content.split('\n').find(l => l.startsWith('# '));
|
|
156
|
+
const title = firstLine ? firstLine.replace(/^#\s+/, '') : id;
|
|
157
|
+
const kv = extractBoldKeyValues(content);
|
|
158
|
+
const date = kv['날짜'] ?? kv['date'] ?? '';
|
|
159
|
+
res.json({ id, title, date, content });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
next(err);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
operationsRouter.delete('/decisions/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
166
|
+
try {
|
|
167
|
+
const { id } = req.params;
|
|
168
|
+
const filePath = `knowledge/decisions/${id}.md`;
|
|
169
|
+
const absPath = path.resolve(COMPANY_ROOT, filePath);
|
|
170
|
+
if (!fs.existsSync(absPath)) {
|
|
171
|
+
res.status(404).json({ error: `Decision not found: ${id}` });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
fs.unlinkSync(absPath);
|
|
175
|
+
res.json({ ok: true });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
next(err);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// --- Traces (AI-readable agent conversation debugging) ---
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* GET /api/ops/traces/:jobId — Dump full trace for a job
|
|
185
|
+
* Returns all events including full prompt/response for the job
|
|
186
|
+
* and all child jobs in the trace chain.
|
|
187
|
+
*
|
|
188
|
+
* Query params:
|
|
189
|
+
* ?chain=true — include all jobs in the same trace (default: true)
|
|
190
|
+
* ?type=trace — filter to trace:response events (use ?type=prompt for prompt:assembled)
|
|
191
|
+
*/
|
|
192
|
+
operationsRouter.get('/traces/:jobId', (req: Request, res: Response, next: NextFunction) => {
|
|
193
|
+
try {
|
|
194
|
+
const jobId = String(req.params.jobId);
|
|
195
|
+
const includeChain = String(req.query.chain ?? 'true') !== 'false';
|
|
196
|
+
const typeFilter = String(req.query.type ?? '') || undefined;
|
|
197
|
+
|
|
198
|
+
// Read the target job's events
|
|
199
|
+
const events = ActivityStream.readAll(jobId);
|
|
200
|
+
if (events.length === 0) {
|
|
201
|
+
res.status(404).json({ error: `No activity stream found for job: ${jobId}` });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Extract traceId from the first event
|
|
206
|
+
const traceId = events[0]?.traceId ?? events.find(e => e.data?.traceId)?.data?.traceId as string ?? jobId;
|
|
207
|
+
|
|
208
|
+
if (!includeChain) {
|
|
209
|
+
const filtered = typeFilter
|
|
210
|
+
? events.filter(e => e.type.startsWith(typeFilter))
|
|
211
|
+
: events;
|
|
212
|
+
res.json({ traceId, jobs: [{ jobId, events: filtered }] });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Find all jobs in the same trace
|
|
217
|
+
const allJobIds = ActivityStream.listAll();
|
|
218
|
+
const traceJobs: Array<{ jobId: string; roleId: string; events: typeof events }> = [];
|
|
219
|
+
|
|
220
|
+
for (const jid of allJobIds) {
|
|
221
|
+
const jobEvents = ActivityStream.readAll(jid);
|
|
222
|
+
const startEvent = jobEvents.find(e => e.type === 'msg:start' || (e.type as string) === 'job:start');
|
|
223
|
+
const jobTraceId = jobEvents[0]?.traceId ?? startEvent?.data?.traceId;
|
|
224
|
+
|
|
225
|
+
if (jobTraceId === traceId || jid === jobId) {
|
|
226
|
+
const filtered = typeFilter
|
|
227
|
+
? jobEvents.filter(e => e.type.startsWith(typeFilter))
|
|
228
|
+
: jobEvents;
|
|
229
|
+
traceJobs.push({
|
|
230
|
+
jobId: jid,
|
|
231
|
+
roleId: startEvent?.roleId ?? 'unknown',
|
|
232
|
+
events: filtered,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sort by timestamp of first event
|
|
238
|
+
traceJobs.sort((a, b) => {
|
|
239
|
+
const aTs = a.events[0]?.ts ?? '';
|
|
240
|
+
const bTs = b.events[0]?.ts ?? '';
|
|
241
|
+
return aTs.localeCompare(bTs);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
res.json({ traceId, jobCount: traceJobs.length, jobs: traceJobs });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
next(err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* GET /api/ops/traces — List recent traces (grouped by traceId)
|
|
252
|
+
* Query params:
|
|
253
|
+
* ?limit=20 — max traces to return
|
|
254
|
+
* ?roleId=cto — filter by role
|
|
255
|
+
*/
|
|
256
|
+
operationsRouter.get('/traces', (req: Request, res: Response, next: NextFunction) => {
|
|
257
|
+
try {
|
|
258
|
+
const limit = parseInt(String(req.query.limit ?? '20')) || 20;
|
|
259
|
+
const roleFilter = req.query.roleId ? String(req.query.roleId) : undefined;
|
|
260
|
+
|
|
261
|
+
const allJobIds = ActivityStream.listAll();
|
|
262
|
+
const traces = new Map<string, {
|
|
263
|
+
traceId: string;
|
|
264
|
+
startedAt: string;
|
|
265
|
+
rootRole: string;
|
|
266
|
+
rootTask: string;
|
|
267
|
+
jobCount: number;
|
|
268
|
+
status: string;
|
|
269
|
+
}>();
|
|
270
|
+
|
|
271
|
+
for (const jid of allJobIds) {
|
|
272
|
+
const events = ActivityStream.readAll(jid);
|
|
273
|
+
const startEvent = events.find(e => e.type === 'msg:start' || (e.type as string) === 'job:start');
|
|
274
|
+
if (!startEvent) continue;
|
|
275
|
+
|
|
276
|
+
const traceId = events[0]?.traceId ?? startEvent?.data?.traceId as string ?? jid;
|
|
277
|
+
if (roleFilter && startEvent.roleId !== roleFilter) continue;
|
|
278
|
+
|
|
279
|
+
if (!traces.has(traceId)) {
|
|
280
|
+
const doneEvent = events.find(e => e.type === 'msg:done' || (e.type as string) === 'job:done');
|
|
281
|
+
const errorEvent = events.find(e => e.type === 'msg:error' || (e.type as string) === 'job:error');
|
|
282
|
+
const awaitingEvent = events.find(e => e.type === 'msg:awaiting_input' || (e.type as string) === 'job:awaiting_input');
|
|
283
|
+
const status = awaitingEvent ? 'awaiting_input'
|
|
284
|
+
: doneEvent ? 'done'
|
|
285
|
+
: errorEvent ? 'error'
|
|
286
|
+
: 'running';
|
|
287
|
+
|
|
288
|
+
traces.set(traceId, {
|
|
289
|
+
traceId,
|
|
290
|
+
startedAt: startEvent.ts,
|
|
291
|
+
rootRole: startEvent.roleId,
|
|
292
|
+
rootTask: (startEvent.data.task as string ?? '').slice(0, 200),
|
|
293
|
+
jobCount: 1,
|
|
294
|
+
status,
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
297
|
+
traces.get(traceId)!.jobCount++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const sorted = [...traces.values()]
|
|
302
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
303
|
+
.slice(0, limit);
|
|
304
|
+
|
|
305
|
+
res.json(sorted);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
next(err);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
3
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
4
|
+
import { readPreferences, writePreferences, mergePreferences } from '../services/preferences.js';
|
|
5
|
+
|
|
6
|
+
export const preferencesRouter = Router();
|
|
7
|
+
|
|
8
|
+
// GET /api/preferences
|
|
9
|
+
preferencesRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
10
|
+
try {
|
|
11
|
+
res.json(readPreferences(COMPANY_ROOT));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
next(err);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// PUT /api/preferences — full overwrite
|
|
18
|
+
preferencesRouter.put('/', (req: Request, res: Response, next: NextFunction) => {
|
|
19
|
+
try {
|
|
20
|
+
const prefs = req.body;
|
|
21
|
+
if (!prefs || typeof prefs !== 'object') {
|
|
22
|
+
res.status(400).json({ error: 'Invalid preferences body' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const existing = readPreferences(COMPANY_ROOT);
|
|
26
|
+
writePreferences(COMPANY_ROOT, {
|
|
27
|
+
instanceId: existing.instanceId, // preserve — never overwrite from client
|
|
28
|
+
appearances: prefs.appearances ?? {},
|
|
29
|
+
theme: prefs.theme ?? 'default',
|
|
30
|
+
});
|
|
31
|
+
res.json({ ok: true, ...readPreferences(COMPANY_ROOT) });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
next(err);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// POST /api/preferences/regenerate-token — regenerate instanceId
|
|
38
|
+
preferencesRouter.post('/regenerate-token', (_req: Request, res: Response, next: NextFunction) => {
|
|
39
|
+
try {
|
|
40
|
+
const current = readPreferences(COMPANY_ROOT);
|
|
41
|
+
const oldId = current.instanceId;
|
|
42
|
+
current.instanceId = crypto.randomUUID();
|
|
43
|
+
writePreferences(COMPANY_ROOT, current);
|
|
44
|
+
res.json({ ok: true, oldInstanceId: oldId, newInstanceId: current.instanceId });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
next(err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// PATCH /api/preferences — partial merge
|
|
51
|
+
preferencesRouter.patch('/', (req: Request, res: Response, next: NextFunction) => {
|
|
52
|
+
try {
|
|
53
|
+
const partial = req.body;
|
|
54
|
+
if (!partial || typeof partial !== 'object') {
|
|
55
|
+
res.status(400).json({ error: 'Invalid preferences body' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const merged = mergePreferences(COMPANY_ROOT, partial);
|
|
59
|
+
res.json({ ok: true, ...merged });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
next(err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presets.ts — Preset API routes
|
|
3
|
+
*
|
|
4
|
+
* GET /api/presets — list all preset summaries
|
|
5
|
+
* GET /api/presets/:id — get full preset detail
|
|
6
|
+
* POST /api/presets/install — install preset from data
|
|
7
|
+
* DELETE /api/presets/:id — remove installed preset
|
|
8
|
+
*/
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
14
|
+
import { getPresetSummaries, getPresetById, loadPresets } from '../services/preset-loader.js';
|
|
15
|
+
|
|
16
|
+
export const presetsRouter = Router();
|
|
17
|
+
|
|
18
|
+
/** GET /api/presets — list preset summaries */
|
|
19
|
+
presetsRouter.get('/', (_req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const summaries = getPresetSummaries(COMPANY_ROOT);
|
|
22
|
+
res.json(summaries);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(500).json({ error: 'Failed to load presets' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** GET /api/presets/:id — get full preset detail */
|
|
29
|
+
presetsRouter.get('/:id', (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const preset = getPresetById(COMPANY_ROOT, req.params.id);
|
|
32
|
+
if (!preset) {
|
|
33
|
+
res.status(404).json({ error: `Preset not found: ${req.params.id}` });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
res.json(preset.definition);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
res.status(500).json({ error: 'Failed to load preset' });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** POST /api/presets/install — install a preset from provided data */
|
|
43
|
+
presetsRouter.post('/install', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { id, preset } = req.body as { id: string; preset: Record<string, unknown> };
|
|
46
|
+
if (!id || !preset) {
|
|
47
|
+
res.status(400).json({ error: 'id and preset are required' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate preset has required fields
|
|
52
|
+
if (!preset.name || !preset.roles || !Array.isArray(preset.roles)) {
|
|
53
|
+
res.status(400).json({ error: 'preset must have name and roles array' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for conflict with existing preset
|
|
58
|
+
const existing = getPresetById(COMPANY_ROOT, id);
|
|
59
|
+
if (existing && !existing.isDefault) {
|
|
60
|
+
res.status(409).json({ error: `Preset already installed: ${id}` });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create preset directory and write preset.yaml
|
|
65
|
+
const presetDir = path.join(COMPANY_ROOT, 'knowledge', 'presets', id);
|
|
66
|
+
fs.mkdirSync(presetDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
// Write preset.yaml
|
|
69
|
+
const yamlContent = YAML.stringify(preset);
|
|
70
|
+
fs.writeFileSync(path.join(presetDir, 'preset.yaml'), yamlContent);
|
|
71
|
+
|
|
72
|
+
// Create subdirectories for roles/knowledge/skills
|
|
73
|
+
fs.mkdirSync(path.join(presetDir, 'roles'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(path.join(presetDir, 'knowledge'), { recursive: true });
|
|
75
|
+
fs.mkdirSync(path.join(presetDir, 'skills'), { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Write knowledge docs if provided
|
|
78
|
+
const knowledge = req.body.knowledge as Array<{ filename: string; content: string }> | undefined;
|
|
79
|
+
if (knowledge) {
|
|
80
|
+
for (const doc of knowledge) {
|
|
81
|
+
fs.writeFileSync(path.join(presetDir, 'knowledge', doc.filename), doc.content);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Write role yamls if provided
|
|
86
|
+
const roleDefinitions = req.body.roleDefinitions as Array<{ id: string; yaml: string }> | undefined;
|
|
87
|
+
if (roleDefinitions) {
|
|
88
|
+
for (const role of roleDefinitions) {
|
|
89
|
+
const roleDir = path.join(presetDir, 'roles', role.id);
|
|
90
|
+
fs.mkdirSync(roleDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(roleDir, 'role.yaml'), role.yaml);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
res.json({ ok: true, id, path: `knowledge/presets/${id}` });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res.status(500).json({ error: `Install failed: ${err instanceof Error ? err.message : 'unknown'}` });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** DELETE /api/presets/:id — remove installed preset */
|
|
102
|
+
presetsRouter.delete('/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const { id } = req.params;
|
|
105
|
+
if (id === 'default' || id === '_default') {
|
|
106
|
+
res.status(400).json({ error: 'Cannot remove default preset' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const presetDir = path.join(COMPANY_ROOT, 'knowledge', 'presets', id);
|
|
111
|
+
if (!fs.existsSync(presetDir)) {
|
|
112
|
+
res.status(404).json({ error: `Preset not found: ${id}` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove preset directory recursively
|
|
117
|
+
fs.rmSync(presetDir, { recursive: true, force: true });
|
|
118
|
+
|
|
119
|
+
res.json({ ok: true, id });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
res.status(500).json({ error: `Remove failed: ${err instanceof Error ? err.message : 'unknown'}` });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { readFile, fileExists } from '../services/file-reader.js';
|
|
3
|
+
import { parseMarkdownTable } from '../services/markdown-parser.js';
|
|
4
|
+
|
|
5
|
+
export const projectsRouter = Router();
|
|
6
|
+
|
|
7
|
+
// GET /api/projects — 프로젝트 목록
|
|
8
|
+
projectsRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
9
|
+
try {
|
|
10
|
+
const content = readFile('knowledge/projects/projects.md');
|
|
11
|
+
const rows = parseMarkdownTable(content);
|
|
12
|
+
|
|
13
|
+
const projects = rows.map(row => {
|
|
14
|
+
const name = row.project ?? '';
|
|
15
|
+
const id = name.toLowerCase().replace(/\s+/g, '-');
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
status: row.status ?? '',
|
|
20
|
+
created: row.created ?? '',
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
res.json(projects);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
next(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// GET /api/projects/:id — 프로젝트 상세
|
|
31
|
+
projectsRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
32
|
+
try {
|
|
33
|
+
const { id } = req.params;
|
|
34
|
+
|
|
35
|
+
// 기본 정보
|
|
36
|
+
const listContent = readFile('knowledge/projects/projects.md');
|
|
37
|
+
const rows = parseMarkdownTable(listContent);
|
|
38
|
+
const projectRow = rows.find(r => {
|
|
39
|
+
const name = r.project ?? '';
|
|
40
|
+
return name.toLowerCase().replace(/\s+/g, '-') === id;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!projectRow) {
|
|
44
|
+
res.status(404).json({ error: `Project not found: ${id}` });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const name = projectRow.project ?? '';
|
|
49
|
+
const project: Record<string, unknown> = {
|
|
50
|
+
id,
|
|
51
|
+
name,
|
|
52
|
+
status: projectRow.status ?? '',
|
|
53
|
+
created: projectRow.created ?? '',
|
|
54
|
+
prd: '',
|
|
55
|
+
tasks: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// PRD 읽기
|
|
59
|
+
const prdPath = `knowledge/projects/${id}/prd.md`;
|
|
60
|
+
if (fileExists(prdPath)) {
|
|
61
|
+
project.prd = readFile(prdPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Tasks 읽기
|
|
65
|
+
const tasksPath = `knowledge/projects/${id}/tasks.md`;
|
|
66
|
+
if (fileExists(tasksPath)) {
|
|
67
|
+
const tasksContent = readFile(tasksPath);
|
|
68
|
+
const taskRows = parseMarkdownTable(tasksContent);
|
|
69
|
+
project.tasks = taskRows.map(row => ({
|
|
70
|
+
id: row.id ?? '',
|
|
71
|
+
title: row.task ?? row.title ?? '',
|
|
72
|
+
role: row.role ?? '',
|
|
73
|
+
status: row.status ?? '',
|
|
74
|
+
description: row.설명 ?? row.description ?? '',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
res.json(project);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
next(err);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
5
|
+
|
|
6
|
+
export const questsRouter = Router();
|
|
7
|
+
|
|
8
|
+
const QUEST_FILE = () => join(COMPANY_ROOT, '.tycono', 'quest-progress.json');
|
|
9
|
+
|
|
10
|
+
function readProgress(): Record<string, unknown> {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(QUEST_FILE(), 'utf-8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return { completedQuests: [], activeChapter: 1, sideQuestsCompleted: [] };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeProgress(data: Record<string, unknown>) {
|
|
19
|
+
mkdirSync(join(COMPANY_ROOT, '.tycono'), { recursive: true });
|
|
20
|
+
writeFileSync(QUEST_FILE(), JSON.stringify(data, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GET /api/quests/progress
|
|
24
|
+
questsRouter.get('/progress', (_req: Request, res: Response, next: NextFunction) => {
|
|
25
|
+
try {
|
|
26
|
+
res.json(readProgress());
|
|
27
|
+
} catch (err) { next(err); }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// PUT /api/quests/progress
|
|
31
|
+
questsRouter.put('/progress', (req: Request, res: Response, next: NextFunction) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = req.body;
|
|
34
|
+
if (!body || typeof body !== 'object') {
|
|
35
|
+
res.status(400).json({ error: 'Invalid body' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
writeProgress(body);
|
|
39
|
+
res.json({ ok: true, ...body });
|
|
40
|
+
} catch (err) { next(err); }
|
|
41
|
+
});
|