tycono-server 0.1.0-beta.1 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono-server",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Tycono AI team orchestration server. Dispatch, supervise, and observe AI agents working as a team.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -101,7 +101,8 @@ export function createHttpServer(): http.Server {
101
101
  const app = createExpressApp();
102
102
 
103
103
  const server = http.createServer((req, res) => {
104
- const url = req.url ?? '';
104
+ const rawUrl = req.url ?? '';
105
+ const url = rawUrl.split('?')[0]; // Strip query string for route matching
105
106
  const method = req.method ?? '';
106
107
 
107
108
  // GET /api/waves/active — restore active waves after refresh
@@ -150,8 +151,8 @@ export function createHttpServer(): http.Server {
150
151
  return;
151
152
  }
152
153
 
153
- // Non-SSE exec/jobs endpoints (GET, DELETE)
154
- if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs')) && (method === 'GET' || method === 'DELETE')) {
154
+ // Non-SSE exec/jobs/waves endpoints (GET, DELETE)
155
+ if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.startsWith('/api/waves/')) && (method === 'GET' || method === 'DELETE')) {
155
156
  setExecCors(req, res);
156
157
  handleExecRequest(req, res);
157
158
  return;
@@ -126,6 +126,32 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
126
126
  return;
127
127
  }
128
128
 
129
+ // ── GET /api/waves/:waveId — Single wave status ──
130
+ const waveDetailMatch = url.match(/^\/api\/waves\/([^/]+)$/);
131
+ if (method === 'GET' && waveDetailMatch) {
132
+ const waveId = waveDetailMatch[1];
133
+ // Try active waves first
134
+ const activeWaves = waveMultiplexer.getActiveWaves();
135
+ const active = activeWaves.find((w: { waveId: string }) => w.waveId === waveId);
136
+ if (active) {
137
+ jsonResponse(res, 200, active);
138
+ return;
139
+ }
140
+ // Fallback: read wave file from disk
141
+ const wavePath = path.join(COMPANY_ROOT, '.tycono', 'waves', `${waveId}.json`);
142
+ if (fs.existsSync(wavePath)) {
143
+ try {
144
+ const waveData = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
145
+ jsonResponse(res, 200, waveData);
146
+ } catch {
147
+ jsonResponse(res, 500, { error: 'Failed to read wave file' });
148
+ }
149
+ return;
150
+ }
151
+ jsonResponse(res, 404, { error: 'Wave not found' });
152
+ return;
153
+ }
154
+
129
155
  // ── Legacy /api/exec/* routes ──
130
156
  const sessionMatch = url.match(/\/api\/exec\/session\/([^/]+)\/message$/);
131
157
 
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { execSync } from 'node:child_process';
12
13
  import YAML from 'yaml';
13
14
  import type { PresetDefinition, LoadedPreset, PresetSummary } from '../../../shared/types.js';
14
15
 
@@ -123,6 +124,19 @@ export function loadPresets(companyRoot: string): LoadedPreset[] {
123
124
  }
124
125
  }
125
126
 
127
+ // 3. Bundled presets (shipped with tycono-server, fallback if not in user's project)
128
+ const bundledPresetsDir = path.resolve(__dirname, '../../../../presets');
129
+ if (fs.existsSync(bundledPresetsDir)) {
130
+ const loadedIds = new Set(presets.map(p => p.definition.id));
131
+ const entries = fs.readdirSync(bundledPresetsDir, { withFileTypes: true });
132
+ for (const entry of entries) {
133
+ if (!entry.isDirectory()) continue;
134
+ if (loadedIds.has(entry.name)) continue; // user's preset takes priority
135
+ const preset = loadPresetFromDir(path.join(bundledPresetsDir, entry.name));
136
+ if (preset) presets.push(preset);
137
+ }
138
+ }
139
+
126
140
  return presets;
127
141
  }
128
142
 
@@ -142,8 +156,58 @@ export function getPresetSummaries(companyRoot: string): PresetSummary[] {
142
156
 
143
157
  /**
144
158
  * Find a specific preset by ID.
159
+ * Falls back to remote download from tycono.ai if not found locally.
145
160
  */
146
161
  export function getPresetById(companyRoot: string, presetId: string): LoadedPreset | null {
147
162
  const presets = loadPresets(companyRoot);
148
- return presets.find(p => p.definition.id === presetId) ?? null;
163
+ const local = presets.find(p => p.definition.id === presetId);
164
+ if (local) return local;
165
+
166
+ // Try downloading from tycono.ai preset registry
167
+ const downloaded = downloadPreset(companyRoot, presetId);
168
+ return downloaded;
169
+ }
170
+
171
+ /**
172
+ * Download a preset from the remote registry (tycono.ai).
173
+ * Saves to knowledge/presets/{id}/ for future use.
174
+ */
175
+ function downloadPreset(companyRoot: string, presetId: string): LoadedPreset | null {
176
+ const REGISTRY_URL = process.env.TYCONO_PRESET_REGISTRY || 'https://tycono.ai/api/presets';
177
+
178
+ try {
179
+ // Synchronous HTTP request (preset download is a blocking init step)
180
+ const response = execSync(
181
+ `curl -s --max-time 10 "${REGISTRY_URL}/${presetId}/download"`,
182
+ { encoding: 'utf-8' },
183
+ );
184
+
185
+ const data = JSON.parse(response);
186
+ if (!data.preset || !data.files) return null;
187
+
188
+ // Save to local presets directory
189
+ const targetDir = path.join(companyRoot, PRESETS_DIR, presetId);
190
+ fs.mkdirSync(targetDir, { recursive: true });
191
+
192
+ // Write preset.yaml
193
+ fs.writeFileSync(
194
+ path.join(targetDir, 'preset.yaml'),
195
+ YAML.stringify(data.preset),
196
+ );
197
+
198
+ // Write knowledge files
199
+ if (data.files && typeof data.files === 'object') {
200
+ for (const [filePath, content] of Object.entries(data.files)) {
201
+ const fullPath = path.join(targetDir, filePath);
202
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
203
+ fs.writeFileSync(fullPath, content as string);
204
+ }
205
+ }
206
+
207
+ console.log(`[Preset] Downloaded "${presetId}" from ${REGISTRY_URL}`);
208
+ return loadPresetFromDir(targetDir);
209
+ } catch (err) {
210
+ console.warn(`[Preset] Failed to download "${presetId}": ${(err as Error).message}`);
211
+ return null;
212
+ }
149
213
  }
@@ -300,14 +300,14 @@ class WaveMultiplexer {
300
300
  getActiveWaves(): Array<{
301
301
  id: string;
302
302
  directive: string;
303
- dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
303
+ dispatches: Array<{ sessionId: string; roleId: string; roleName: string; status: string }>;
304
304
  startedAt: number;
305
305
  sessionIds: string[];
306
306
  }> {
307
307
  const result: Array<{
308
308
  id: string;
309
309
  directive: string;
310
- dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
310
+ dispatches: Array<{ sessionId: string; roleId: string; roleName: string; status: string }>;
311
311
  startedAt: number;
312
312
  sessionIds: string[];
313
313
  }> = [];
@@ -316,12 +316,13 @@ class WaveMultiplexer {
316
316
  const hasActive = Array.from(sessions.values()).some(e => e.status === 'running' || e.status === 'awaiting_input');
317
317
  if (!hasActive) continue;
318
318
 
319
+ // Include ALL sessions (not just root) so plugin can show full team status
319
320
  const rootSessions = Array.from(sessions.values())
320
- .filter(e => !e.parentSessionId || !sessions.has(e.parentSessionId))
321
321
  .map(e => ({
322
322
  sessionId: e.sessionId,
323
323
  roleId: e.roleId,
324
324
  roleName: e.roleId.toUpperCase(),
325
+ status: e.status,
325
326
  }));
326
327
 
327
328
  const firstExec = rootSessions.length > 0