tycono 0.3.13-beta.7 → 0.3.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
|
@@ -17,6 +17,7 @@ import { engineRouter } from './routes/engine.js';
|
|
|
17
17
|
import { sessionsRouter } from './routes/sessions.js';
|
|
18
18
|
import { setupRouter } from './routes/setup.js';
|
|
19
19
|
import { skillsRouter } from './routes/skills.js';
|
|
20
|
+
import { presetsRouter } from './routes/presets.js';
|
|
20
21
|
|
|
21
22
|
export function createApp() {
|
|
22
23
|
const app = express();
|
|
@@ -53,6 +54,7 @@ export function createApp() {
|
|
|
53
54
|
app.use('/api/engine', engineRouter);
|
|
54
55
|
app.use('/api/sessions', sessionsRouter);
|
|
55
56
|
app.use('/api/skills', skillsRouter);
|
|
57
|
+
app.use('/api/presets', presetsRouter);
|
|
56
58
|
|
|
57
59
|
app.get('/api/health', (_req, res) => {
|
|
58
60
|
res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
9
|
+
import { getPresetSummaries, getPresetById } from '../services/preset-loader.js';
|
|
10
|
+
|
|
11
|
+
export const presetsRouter = Router();
|
|
12
|
+
|
|
13
|
+
/** GET /api/presets — list preset summaries for TUI */
|
|
14
|
+
presetsRouter.get('/', (_req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const summaries = getPresetSummaries(COMPANY_ROOT);
|
|
17
|
+
res.json(summaries);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
res.status(500).json({ error: 'Failed to load presets' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** GET /api/presets/:id — get full preset detail */
|
|
24
|
+
presetsRouter.get('/:id', (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const preset = getPresetById(COMPANY_ROOT, req.params.id);
|
|
27
|
+
if (!preset) {
|
|
28
|
+
res.status(404).json({ error: `Preset not found: ${req.params.id}` });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
res.json(preset.definition);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(500).json({ error: 'Failed to load preset' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-loader.ts — Load presets from company/presets/
|
|
3
|
+
*
|
|
4
|
+
* Scans company/presets/ for:
|
|
5
|
+
* - _default.yaml (auto-generated from existing roles/)
|
|
6
|
+
* - {name}/preset.yaml (installed presets with roles/skills/knowledge)
|
|
7
|
+
*
|
|
8
|
+
* Returns PresetSummary[] for TUI display and full LoadedPreset for wave creation.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import type { PresetDefinition, LoadedPreset, PresetSummary } from '../../../shared/types.js';
|
|
14
|
+
|
|
15
|
+
const PRESETS_DIR = 'company/presets';
|
|
16
|
+
const DEFAULT_PRESET_FILE = '_default.yaml';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a default preset definition from existing roles/ directory.
|
|
20
|
+
* This is generated on-the-fly — no need to persist _default.yaml.
|
|
21
|
+
*/
|
|
22
|
+
function buildDefaultPreset(companyRoot: string): LoadedPreset {
|
|
23
|
+
const rolesDir = path.join(companyRoot, 'roles');
|
|
24
|
+
const roles: string[] = [];
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(rolesDir)) {
|
|
27
|
+
const entries = fs.readdirSync(rolesDir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isDirectory()) continue;
|
|
30
|
+
const yamlPath = path.join(rolesDir, entry.name, 'role.yaml');
|
|
31
|
+
if (fs.existsSync(yamlPath)) {
|
|
32
|
+
roles.push(entry.name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
definition: {
|
|
39
|
+
spec: 'preset/v1',
|
|
40
|
+
id: 'default',
|
|
41
|
+
name: 'Default Team',
|
|
42
|
+
tagline: 'Your current team',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
roles,
|
|
45
|
+
},
|
|
46
|
+
path: null,
|
|
47
|
+
isDefault: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load a single preset from a directory containing preset.yaml.
|
|
53
|
+
*/
|
|
54
|
+
function loadPresetFromDir(presetDir: string): LoadedPreset | null {
|
|
55
|
+
const yamlPath = path.join(presetDir, 'preset.yaml');
|
|
56
|
+
if (!fs.existsSync(yamlPath)) return null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as PresetDefinition;
|
|
60
|
+
if (!raw.id || !raw.name || !Array.isArray(raw.roles)) return null;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
definition: {
|
|
64
|
+
spec: raw.spec || 'preset/v1',
|
|
65
|
+
id: raw.id,
|
|
66
|
+
name: raw.name,
|
|
67
|
+
tagline: raw.tagline,
|
|
68
|
+
version: raw.version || '1.0.0',
|
|
69
|
+
description: raw.description,
|
|
70
|
+
author: raw.author,
|
|
71
|
+
category: raw.category,
|
|
72
|
+
industry: raw.industry,
|
|
73
|
+
stage: raw.stage,
|
|
74
|
+
use_case: raw.use_case,
|
|
75
|
+
roles: raw.roles,
|
|
76
|
+
knowledge_docs: raw.knowledge_docs,
|
|
77
|
+
skills_count: raw.skills_count,
|
|
78
|
+
pricing: raw.pricing,
|
|
79
|
+
tags: raw.tags,
|
|
80
|
+
languages: raw.languages,
|
|
81
|
+
stats: raw.stats,
|
|
82
|
+
wave_scoped: raw.wave_scoped,
|
|
83
|
+
},
|
|
84
|
+
path: presetDir,
|
|
85
|
+
isDefault: false,
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load all presets from company/presets/ + auto-generated default.
|
|
94
|
+
* Returns [default, ...installed] — default is always first.
|
|
95
|
+
*/
|
|
96
|
+
export function loadPresets(companyRoot: string): LoadedPreset[] {
|
|
97
|
+
const presets: LoadedPreset[] = [];
|
|
98
|
+
|
|
99
|
+
// 1. Default preset (always present)
|
|
100
|
+
const defaultPreset = buildDefaultPreset(companyRoot);
|
|
101
|
+
|
|
102
|
+
// Check if _default.yaml exists with overrides
|
|
103
|
+
const defaultYamlPath = path.join(companyRoot, PRESETS_DIR, DEFAULT_PRESET_FILE);
|
|
104
|
+
if (fs.existsSync(defaultYamlPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = YAML.parse(fs.readFileSync(defaultYamlPath, 'utf-8')) as Partial<PresetDefinition>;
|
|
107
|
+
if (raw.name) defaultPreset.definition.name = raw.name;
|
|
108
|
+
if (raw.tagline) defaultPreset.definition.tagline = raw.tagline;
|
|
109
|
+
if (raw.description) defaultPreset.definition.description = raw.description;
|
|
110
|
+
} catch { /* ignore malformed _default.yaml */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
presets.push(defaultPreset);
|
|
114
|
+
|
|
115
|
+
// 2. Installed presets from company/presets/{name}/preset.yaml
|
|
116
|
+
const presetsDir = path.join(companyRoot, PRESETS_DIR);
|
|
117
|
+
if (fs.existsSync(presetsDir)) {
|
|
118
|
+
const entries = fs.readdirSync(presetsDir, { withFileTypes: true });
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.isDirectory()) continue;
|
|
121
|
+
const preset = loadPresetFromDir(path.join(presetsDir, entry.name));
|
|
122
|
+
if (preset) presets.push(preset);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return presets;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get preset summaries for TUI display.
|
|
131
|
+
*/
|
|
132
|
+
export function getPresetSummaries(companyRoot: string): PresetSummary[] {
|
|
133
|
+
return loadPresets(companyRoot).map(p => ({
|
|
134
|
+
id: p.definition.id,
|
|
135
|
+
name: p.definition.name,
|
|
136
|
+
description: p.definition.description ?? p.definition.tagline,
|
|
137
|
+
rolesCount: p.definition.roles.length,
|
|
138
|
+
roles: p.definition.roles,
|
|
139
|
+
isDefault: p.isDefault,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Find a specific preset by ID.
|
|
145
|
+
*/
|
|
146
|
+
export function getPresetById(companyRoot: string, presetId: string): LoadedPreset | null {
|
|
147
|
+
const presets = loadPresets(companyRoot);
|
|
148
|
+
return presets.find(p => p.definition.id === presetId) ?? null;
|
|
149
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -144,3 +144,81 @@ export function eventTypeToMessageStatus(eventType: ActivityEventType): MessageS
|
|
|
144
144
|
|
|
145
145
|
/** TeamStatus — Role별 현재 상태 + 작업 내용 (context-assembler, runner에서 공유) */
|
|
146
146
|
export type TeamStatus = Record<string, { status: RoleStatus; task?: string }>;
|
|
147
|
+
|
|
148
|
+
/* ═══════════════════════════════════════════════
|
|
149
|
+
* Preset — Wave-scoped team configuration
|
|
150
|
+
* ═══════════════════════════════════════════════ */
|
|
151
|
+
|
|
152
|
+
/** preset.yaml 스키마 */
|
|
153
|
+
export interface PresetDefinition {
|
|
154
|
+
/** Spec version (e.g. "preset/v1") */
|
|
155
|
+
spec: string;
|
|
156
|
+
/** Unique identifier (directory name) */
|
|
157
|
+
id: string;
|
|
158
|
+
/** Display name */
|
|
159
|
+
name: string;
|
|
160
|
+
/** Short tagline */
|
|
161
|
+
tagline?: string;
|
|
162
|
+
/** Version string */
|
|
163
|
+
version: string;
|
|
164
|
+
/** Full description */
|
|
165
|
+
description?: string;
|
|
166
|
+
/** Author info */
|
|
167
|
+
author?: {
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
verified?: boolean;
|
|
171
|
+
};
|
|
172
|
+
/** Category / classification */
|
|
173
|
+
category?: string;
|
|
174
|
+
industry?: string;
|
|
175
|
+
stage?: string;
|
|
176
|
+
use_case?: string[];
|
|
177
|
+
/** Role IDs included in this preset */
|
|
178
|
+
roles: string[];
|
|
179
|
+
/** Counts */
|
|
180
|
+
knowledge_docs?: number;
|
|
181
|
+
skills_count?: number;
|
|
182
|
+
/** Pricing */
|
|
183
|
+
pricing?: {
|
|
184
|
+
type: 'one-time' | 'subscription';
|
|
185
|
+
price: number;
|
|
186
|
+
wave_scoped_tier?: string;
|
|
187
|
+
};
|
|
188
|
+
/** Tags for search */
|
|
189
|
+
tags?: string[];
|
|
190
|
+
languages?: string[];
|
|
191
|
+
/** Stats (marketplace) */
|
|
192
|
+
stats?: {
|
|
193
|
+
installs: number;
|
|
194
|
+
rating: number;
|
|
195
|
+
reviews: number;
|
|
196
|
+
waves_used: number;
|
|
197
|
+
};
|
|
198
|
+
/** Wave-scoped recommendations */
|
|
199
|
+
wave_scoped?: {
|
|
200
|
+
recommended_tasks?: string[];
|
|
201
|
+
task_keywords?: string[];
|
|
202
|
+
avg_wave_duration?: string;
|
|
203
|
+
complexity?: string;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Loaded preset with resolved path info */
|
|
208
|
+
export interface LoadedPreset {
|
|
209
|
+
definition: PresetDefinition;
|
|
210
|
+
/** Absolute path to preset directory (or null for _default) */
|
|
211
|
+
path: string | null;
|
|
212
|
+
/** Whether this is the _default preset */
|
|
213
|
+
isDefault: boolean;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Preset summary for TUI display */
|
|
217
|
+
export interface PresetSummary {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
description?: string;
|
|
221
|
+
rolesCount: number;
|
|
222
|
+
roles: string[];
|
|
223
|
+
isDefault: boolean;
|
|
224
|
+
}
|
package/src/tui/api.ts
CHANGED
|
@@ -136,6 +136,7 @@ export async function fetchExecStatus(): Promise<ExecStatus> {
|
|
|
136
136
|
export async function dispatchWave(directive?: string, options?: {
|
|
137
137
|
targetRoles?: string[];
|
|
138
138
|
continuous?: boolean;
|
|
139
|
+
preset?: string;
|
|
139
140
|
}): Promise<WaveResponse> {
|
|
140
141
|
return fetchJson<WaveResponse>('/api/jobs', {
|
|
141
142
|
method: 'POST',
|
|
@@ -144,6 +145,7 @@ export async function dispatchWave(directive?: string, options?: {
|
|
|
144
145
|
directive: directive ?? '',
|
|
145
146
|
targetRoles: options?.targetRoles,
|
|
146
147
|
continuous: options?.continuous ?? false,
|
|
148
|
+
preset: options?.preset,
|
|
147
149
|
},
|
|
148
150
|
});
|
|
149
151
|
}
|
|
@@ -211,6 +213,21 @@ export async function cleanupSessions(): Promise<{ cleaned: number; remaining: n
|
|
|
211
213
|
return fetchJson<{ cleaned: number; remaining: number }>('/api/active-sessions/cleanup', { method: 'POST' });
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
/* ─── Presets ─── */
|
|
217
|
+
|
|
218
|
+
export interface PresetSummary {
|
|
219
|
+
id: string;
|
|
220
|
+
name: string;
|
|
221
|
+
description?: string;
|
|
222
|
+
rolesCount: number;
|
|
223
|
+
roles: string[];
|
|
224
|
+
isDefault: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function fetchPresets(): Promise<PresetSummary[]> {
|
|
228
|
+
return fetchJson<PresetSummary[]>('/api/presets');
|
|
229
|
+
}
|
|
230
|
+
|
|
214
231
|
/* ─── Knowledge docs ─── */
|
|
215
232
|
|
|
216
233
|
export interface KnowledgeDoc {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { useCallback } from 'react';
|
|
19
|
-
import { dispatchWave, sendDirective, stopWave, fetchJson, killSession, cleanupSessions, fetchActiveSessions } from '../api';
|
|
19
|
+
import { dispatchWave, sendDirective, stopWave, fetchJson, killSession, cleanupSessions, fetchActiveSessions, fetchPresets } from '../api';
|
|
20
|
+
import type { PresetSummary } from '../api';
|
|
20
21
|
|
|
21
22
|
export interface WaveInfo {
|
|
22
23
|
waveId: string;
|
|
@@ -25,9 +26,10 @@ export interface WaveInfo {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface CommandResult {
|
|
28
|
-
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup' | 'docs' | 'read_file' | 'open_file';
|
|
29
|
+
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup' | 'docs' | 'read_file' | 'open_file' | 'preset_list' | 'preset_select';
|
|
29
30
|
message: string;
|
|
30
31
|
waveId?: string;
|
|
32
|
+
presets?: PresetSummary[];
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
async function postAssign(roleId: string, task: string): Promise<{ waveId?: string }> {
|
|
@@ -80,6 +82,15 @@ export function useCommand(options: UseCommandOptions) {
|
|
|
80
82
|
|
|
81
83
|
case 'new': {
|
|
82
84
|
const directive = args || undefined;
|
|
85
|
+
if (!directive) {
|
|
86
|
+
// No args → show preset selection UI
|
|
87
|
+
try {
|
|
88
|
+
const presets = await fetchPresets();
|
|
89
|
+
return { type: 'preset_select', message: '', presets };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { type: 'error', message: `Failed to load presets: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
try {
|
|
84
95
|
const result = await dispatchWave(directive);
|
|
85
96
|
onWaveCreated(result.waveId, directive ?? '');
|
|
@@ -168,6 +179,19 @@ export function useCommand(options: UseCommandOptions) {
|
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
case 'preset': {
|
|
183
|
+
const subCmd = args.split(/\s+/)[0]?.toLowerCase() || 'list';
|
|
184
|
+
if (subCmd === 'list' || !subCmd) {
|
|
185
|
+
try {
|
|
186
|
+
const presets = await fetchPresets();
|
|
187
|
+
return { type: 'preset_list', message: '', presets };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { type: 'error', message: `Failed to load presets: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { type: 'error', message: `Unknown preset command: ${subCmd}. Try: /preset list` };
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
case 'roles':
|
|
172
196
|
onShowPanel();
|
|
173
197
|
return { type: 'panel', message: '' };
|