14 lines
74 KiB
Text
14 lines
74 KiB
Text
|
|
{"parentUuid":null,"isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":"Read these files from the local filesystem and return their full contents:\n- /tmp/wd-design/zampp-platform/project/ — actually these aren't here.\n\nInstead, use the Forgejo MCP tools to get these files from repo `zgaetano/wild-dragon` branch `main`:\n- services/mam-api/src/routes/assets.js\n- services/mam-api/src/routes/projects.js\n- services/mam-api/src/routes/recorders.js\n- services/mam-api/src/routes/jobs.js\n- services/mam-api/src/routes/cluster.js\n\nReturn the full content of each file. I need to understand the exact JSON response shapes (field names, structure) for each endpoint so I can wire a React frontend to them."},"uuid":"d6f7d0ae-c25b-4df9-a132-b584f6595ffd","timestamp":"2026-05-22T13:50:42.321Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"d6f7d0ae-c25b-4df9-a132-b584f6595ffd","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"text","text":"I'll retrieve these files from the Forgejo repository for you."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"60451e9d-f90e-4d45-8b28-98f604613a3f","timestamp":"2026-05-22T13:50:45.660Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"60451e9d-f90e-4d45-8b28-98f604613a3f","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KBWTu4tMV9ehtSXS7aMDDg","name":"mcp__claude_ai_mcp-server__forgejo_forgejo_get_file","input":{"owner":"zgaetano","repo":"wild-dragon","path":"services/mam-api/src/routes/assets.js","ref":"main"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"9b23815f-f4ba-4367-a6e6-8b80464466a6","timestamp":"2026-05-22T13:50:46.095Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"9b23815f-f4ba-4367-a6e6-8b80464466a6","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012733CgrLo1zKaxKrB7E9SD","name":"mcp__claude_ai_mcp-server__forgejo_forgejo_get_file","input":{"owner":"zgaetano","repo":"wild-dragon","path":"services/mam-api/src/routes/projects.js","ref":"main"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"f6f87521-df9b-4a90-b64b-db23fa49bc07","timestamp":"2026-05-22T13:50:46.515Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"f6f87521-df9b-4a90-b64b-db23fa49bc07","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0145hWtaX9xmYGu34a4yKXm1","name":"mcp__claude_ai_mcp-server__forgejo_forgejo_get_file","input":{"owner":"zgaetano","repo":"wild-dragon","path":"services/mam-api/src/routes/recorders.js","ref":"main"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"d72ef5c2-3816-4b37-9926-e5be15519b90","timestamp":"2026-05-22T13:50:46.936Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"d72ef5c2-3816-4b37-9926-e5be15519b90","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01H4cMhAJe2PXzyV3MAgLk1Y","name":"mcp__claude_ai_mcp-server__forgejo_forgejo_get_file","input":{"owner":"zgaetano","repo":"wild-dragon","path":"services/mam-api/src/routes/jobs.js","ref":"main"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"1cb11b76-f3f0-4ffb-9dbb-88be85132b9e","timestamp":"2026-05-22T13:50:47.362Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"1cb11b76-f3f0-4ffb-9dbb-88be85132b9e","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TGXsTN8jQeCWdEpQrKS7AX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PZ5ALMVz7bmALJ9ZDNd1wF","name":"mcp__claude_ai_mcp-server__forgejo_forgejo_get_file","input":{"owner":"zgaetano","repo":"wild-dragon","path":"services/mam-api/src/routes/cluster.js","ref":"main"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100976,"cache_read_input_tokens":0,"output_tokens":648,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":100976},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":648,"cache_read_input_tokens":0,"cache_creation_input_tokens":100976,"cache_creation":{"ephemeral_5m_input_tokens":100976,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbHiTMbo7yRkwpfHqPF6E","attributionAgent":"Explore","type":"assistant","uuid":"5c1c8750-0026-4a79-9a1e-02771c98f6bb","timestamp":"2026-05-22T13:50:47.594Z","userType":"external","entrypoint":"sdk-ts","cwd":"/home/node","sessionId":"822a2121-d8ec-424f-ace5-d65e0b717cb0","version":"2.1.129","gitBranch":"HEAD","slug":"velvet-chasing-karp"}
|
|||
|
|
{"parentUuid":"9b23815f-f4ba-4367-a6e6-8b80464466a6","isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KBWTu4tMV9ehtSXS7aMDDg","type":"tool_result","content":"{\"result\":\"File: services/mam-api/src/routes/assets.js\\n\\nimport express from 'express';\\nimport { Queue } from 'bullmq';\\nimport { v4 as uuidv4 } from 'uuid';\\nimport pool from '../db/pool.js';\\nimport { getSignedUrlForObject, deleteObject, s3Client, getS3Bucket } from '../s3/client.js';\\nimport { GetObjectCommand } from '@aws-sdk/client-s3';\\nimport { requireAuth } from '../middleware/auth.js';\\n\\nconst router = express.Router();\\n\\nrouter.use(requireAuth);\\n\\n// BullMQ queue connection (mirrors worker/src/index.js)\\nconst parseRedisUrl = (url) => {\\n const parsed = new URL(url);\\n return { host: parsed.hostname, port: parseInt(parsed.port, 10) };\\n};\\n\\nconst proxyQueue = new Queue('proxy', {\\n connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),\\n});\\n\\nconst thumbnailQueue = new Queue('thumbnail', {\\n connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),\\n});\\n\\n// GET / - List assets with filtering\\nrouter.get('/', async (req, res, next) => {\\n try {\\n const {\\n project_id,\\n bin_id,\\n status,\\n search,\\n media_type,\\n limit = 50,\\n offset = 0,\\n include_archived,\\n } = req.query;\\n\\n let query = `\\n SELECT a.*,\\n COUNT(*) OVER() AS full_count\\n FROM assets a\\n WHERE 1=1\\n `;\\n const params = [];\\n let paramCount = 1;\\n\\n if (!status && include_archived !== 'true') {\\n query += ` AND a.status <> 'archived'`;\\n }\\n\\n if (project_id) {\\n query += ` AND a.project_id = $${paramCount++}`;\\n params.push(project_id);\\n }\\n\\n if (bin_id) {\\n query += ` AND a.bin_id = $${paramCount++}`;\\n params.push(bin_id);\\n }\\n\\n if (status) {\\n query += ` AND a.status = $${paramCount++}`;\\n params.push(status);\\n }\\n\\n if (media_type) {\\n query += ` AND a.media_type = $${paramCount++}`;\\n params.push(media_type);\\n }\\n\\n if (search) {\\n query += ` AND (a.display_name ILIKE $${paramCount} OR a.filename ILIKE $${paramCount} OR a.notes ILIKE $${paramCount})`;\\n params.push(`%${search}%`);\\n paramCount++;\\n }\\n\\n query += ` ORDER BY a.created_at DESC`;\\n query += ` LIMIT $${paramCount++} OFFSET $${paramCount++}`;\\n params.push(limit, offset);\\n\\n const result = await pool.query(query, params);\\n const total = result.rows.length > 0 ? parseInt(result.rows[0].full_count, 10) : 0;\\n\\n res.json({\\n assets: result.rows.map(({ full_count, ...rest }) => rest),\\n total,\\n });\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\n// POST / - Register a new asset from a completed capture session\\nrouter.post('/', async (req, res, next) => {\\n try {\\n const {\\n projectId,\\n binId,\\n clipName,\\n hiresKey,\\n proxyKey,\\n duration,\\n capturedAt,\\n } = req.body;\\n\\n if (!projectId || !clipName) {\\n return res.status(400).json({ error: 'projectId and clipName are required' });\\n }\\n\\n const durationMs = duration ? Math.round(duration * 1000) : null;\\n\\n const existing = await pool.query(\\n `SELECT * FROM assets\\n WHERE project_id = $1 AND display_name = $2 AND status = 'live'\\n ORDER BY created_at DESC LIMIT 1`,\\n [projectId, clipName]\\n );\\n\\n let id;\\n let asset;\\n if (existing.rows.length > 0) {\\n id = existing.rows[0].id;\\n const upd = await pool.query(\\n `UPDATE assets\\n SET status = 'processing',\\n original_s3_key = COALESCE($2, original_s3_key),\\n proxy_s3_key = COALESCE($3, proxy_s3_key),\\n duration_ms = COALESCE
|
|||
|
|
{"parentUuid":"f6f87521-df9b-4a90-b64b-db23fa49bc07","isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012733CgrLo1zKaxKrB7E9SD","type":"tool_result","content":"{\"result\":\"File: services/mam-api/src/routes/projects.js\\n\\nimport express from 'express';\\nimport pool from '../db/pool.js';\\nimport { requireAuth } from '../middleware/auth.js';\\nimport { v4 as uuidv4 } from 'uuid';\\n\\nconst router = express.Router();\\n\\nrouter.use(requireAuth);\\n\\n// Helper function to slugify\\nconst slugify = (str) => {\\n return str\\n .toLowerCase()\\n .trim()\\n .replace(/[^\\\\w\\\\s-]/g, '')\\n .replace(/\\\\s+/g, '-')\\n .replace(/-+/g, '-');\\n};\\n\\n// GET / - List all projects\\nrouter.get('/', async (req, res, next) => {\\n try {\\n const result = await pool.query('SELECT * FROM projects ORDER BY created_at DESC');\\n res.json(result.rows);\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\n// POST / - Create project\\nrouter.post('/', async (req, res, next) => {\\n try {\\n const { name, description } = req.body;\\n\\n if (!name) {\\n return res.status(400).json({ error: 'Name is required' });\\n }\\n\\n const id = uuidv4();\\n const s3_prefix = slugify(name);\\n\\n const result = await pool.query(\\n `INSERT INTO projects (id, name, description, s3_prefix, created_at, updated_at)\\n VALUES ($1, $2, $3, $4, NOW(), NOW())\\n RETURNING *`,\\n [id, name, description || null, s3_prefix]\\n );\\n\\n res.status(201).json(result.rows[0]);\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\n// GET /:id - Single project with asset count\\nrouter.get('/:id', async (req, res, next) => {\\n try {\\n const { id } = req.params;\\n\\n const result = await pool.query(\\n `SELECT p.*,\\n COUNT(a.id) AS asset_count\\n FROM projects p\\n LEFT JOIN assets a ON a.project_id = p.id AND a.status != 'archived'\\n WHERE p.id = $1\\n GROUP BY p.id`,\\n [id]\\n );\\n\\n if (result.rows.length === 0) {\\n return res.status(404).json({ error: 'Project not found' });\\n }\\n\\n res.json(result.rows[0]);\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\n// PATCH /:id - Update project\\nrouter.patch('/:id', async (req, res, next) => {\\n try {\\n const { id } = req.params;\\n const { name, description } = req.body;\\n\\n const updates = [];\\n const params = [];\\n let paramCount = 1;\\n\\n if (name !== undefined) {\\n updates.push(`name = $${paramCount++}`);\\n params.push(name);\\n }\\n\\n if (description !== undefined) {\\n updates.push(`description = $${paramCount++}`);\\n params.push(description);\\n }\\n\\n if (updates.length === 0) {\\n return res.status(400).json({ error: 'No fields to update' });\\n }\\n\\n updates.push(`updated_at = NOW()`);\\n params.push(id);\\n\\n const query = `\\n UPDATE projects\\n SET ${updates.join(', ')}\\n WHERE id = $${paramCount}\\n RETURNING *\\n `;\\n\\n const result = await pool.query(query, params);\\n\\n if (result.rows.length === 0) {\\n return res.status(404).json({ error: 'Project not found' });\\n }\\n\\n res.json(result.rows[0]);\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\n// DELETE /:id - Delete project and cascade\\nrouter.delete('/:id', async (req, res, next) => {\\n try {\\n const { id } = req.params;\\n\\n // Delete project (cascade should handle related records)\\n const result = await pool.query(\\n 'DELETE FROM projects WHERE id = $1 RETURNING *',\\n [id]\\n );\\n\\n if (result.rows.length === 0) {\\n return res.status(404).json({ error: 'Project not found' });\\n }\\n\\n res.json({ message: 'Project deleted', project: result.rows[0] });\\n } catch (err) {\\n next(err);\\n }\\n});\\n\\nexport default router;\\n\"}"}]},"uuid":"f8b90bda-5d7c-48ea-aa6f-67c795f0bb28"
|
|||
|
|
{"parentUuid":"d72ef5c2-3816-4b37-9926-e5be15519b90","isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0145hWtaX9xmYGu34a4yKXm1","type":"tool_result","content":"{\"result\":\"File: services/mam-api/src/routes/recorders.js\\n\\nimport express from 'express';\\nimport http from 'http';\\nimport pool from '../db/pool.js';\\nimport { requireAuth } from '../middleware/auth.js';\\nimport { v4 as uuidv4 } from 'uuid';\\n\\nconst router = express.Router();\\n\\nrouter.use(requireAuth);\\n\\n// Base port for on-demand SDI sidecar containers on remote worker nodes.\\n// Device index 0 → 7438, index 1 → 7439, etc.\\nconst SIDECAR_PORT_BASE = 7438;\\n\\n// Docker API helper function\\nfunction dockerApi(method, path, body = null) {\\n return new Promise((resolve, reject) => {\\n const options = {\\n socketPath: '/var/run/docker.sock',\\n path: `/v1.43${path}`,\\n method,\\n headers: { 'Content-Type': 'application/json' },\\n };\\n const req = http.request(options, (res) => {\\n let data = '';\\n res.on('data', chunk => data += chunk);\\n res.on('end', () => {\\n try {\\n resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} });\\n } catch {\\n resolve({ status: res.statusCode, data });\\n }\\n });\\n });\\n req.on('error', reject);\\n if (body) req.write(JSON.stringify(body));\\n req.end();\\n });\\n}\\n\\n// Look up the cluster node for a recorder and decide if it is remote.\\n// Returns { remote: false } when the node is local or unset;\\n// { remote: true, apiUrl, ip } when it is a different host.\\nasync function resolveNodeTarget(nodeId) {\\n if (!nodeId) return { remote: false };\\n const r = await pool.query(\\n 'SELECT hostname, ip_address, api_url FROM cluster_nodes WHERE id = $1',\\n [nodeId]\\n );\\n if (r.rows.length === 0) return { remote: false };\\n const node = r.rows[0];\\n const localHostname = process.env.NODE_HOSTNAME || '';\\n if (!node.api_url || node.hostname === localHostname) return { remote: false };\\n return { remote: true, apiUrl: node.api_url, ip: node.ip_address };\\n}\\n\\n// Helper function to generate clip name with timestamp\\nfunction generateClipName(recorderName) {\\n const now = new Date();\\n const year = now.getFullYear();\\n const month = String(now.getMonth() + 1).padStart(2, '0');\\n const day = String(now.getDate()).padStart(2, '0');\\n const hours = String(now.getHours()).padStart(2, '0');\\n const minutes = String(now.getMinutes()).padStart(2, '0');\\n const seconds = String(now.getSeconds()).padStart(2, '0');\\n return `${recorderName}_${year}${month}${day}_${hours}${minutes}${seconds}`;\\n}\\n\\n/**\\n * Build Docker PortBindings and ExposedPorts for listener-mode recorders.\\n */\\nfunction buildPortConfig(sourceType, sourceConfig) {\\n const portBindings = {};\\n const exposedPorts = {};\\n\\n if (sourceConfig && sourceConfig.mode === 'listener') {\\n if (sourceType === 'srt') {\\n const port = String(sourceConfig.listen_port || 9000);\\n const proto = `${port}/udp`;\\n portBindings[proto] = [{ HostPort: port }];\\n exposedPorts[proto] = {};\\n } else if (sourceType === 'rtmp') {\\n const port = String(sourceConfig.listen_port || 1935);\\n const proto = `${port}/tcp`;\\n portBindings[proto] = [{ HostPort: port }];\\n exposedPorts[proto] = {};\\n }\\n }\\n\\n return { portBindings, exposedPorts };\\n}\\n\\n// Whitelist of recorder columns the API accepts on POST/PATCH. Keeping it\\n// explicit prevents accidental writes to status / container_id / timestamps.\\nconst RECORDER_FIELDS = [\\n 'name', 'source_type', 'source_config',\\n 'recording_codec', 'recording_resolution',\\n 'recording_video_bitrate', 'recording_framerate',\\n 'recording_audio_codec', 'recording_audio_bitrate', 'recording_audio_channels',\\n 'recording_container',\\n 'proxy_enabled', 'proxy_codec'
|
|||
|
|
{"parentUuid":"1cb11b76-f3f0-4ffb-9dbb-88be85132b9e","isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01H4cMhAJe2PXzyV3MAgLk1Y","type":"tool_result","content":"{\"result\":\"File: services/mam-api/src/routes/jobs.js\\n\\nimport express from 'express';\\nimport pool from '../db/pool.js';\\nimport { requireAuth } from '../middleware/auth.js';\\nimport { Queue } from 'bullmq';\\n\\nconst router = express.Router();\\nrouter.use(requireAuth);\\n\\n// ── Redis connection ──────────────────────────────────────────────────────────\\nconst parseRedisUrl = (url) => {\\n try {\\n const parsed = new URL(url);\\n return { host: parsed.hostname, port: parseInt(parsed.port, 10) || 6379 };\\n } catch {\\n return { host: 'localhost', port: 6379 };\\n }\\n};\\n\\nconst redisConn = parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379');\\n\\nconst proxyQueue = new Queue('proxy', { connection: redisConn });\\nconst thumbnailQueue = new Queue('thumbnail', { connection: redisConn });\\nconst conformQueue = new Queue('conform', { connection: redisConn });\\n\\nconst QUEUES = [\\n { queue: proxyQueue, type: 'proxy' },\\n { queue: thumbnailQueue, type: 'thumbnail' },\\n { queue: conformQueue, type: 'conform' },\\n];\\n\\n// BullMQ state → API status mapping\\nconst STATE_MAP = {\\n waiting: 'waiting',\\n active: 'active',\\n completed: 'completed',\\n failed: 'failed',\\n delayed: 'waiting',\\n paused: 'waiting',\\n};\\n\\n// Ordered state buckets used for bulk fetch — avoids N+1 getState() calls.\\nconst STATE_BUCKETS = ['active', 'waiting', 'completed', 'failed', 'delayed', 'paused'];\\n\\nfunction normalizeJob(bullJob, type, apiStatus) {\\n const isCompleted = apiStatus === 'completed';\\n const isFailed = apiStatus === 'failed';\\n return {\\n id: `${type}:${bullJob.id}`,\\n type,\\n status: apiStatus,\\n progress: typeof bullJob.progress === 'number' ? bullJob.progress : 0,\\n asset_id: bullJob.data?.assetId || null,\\n asset_name: bullJob.data?.assetName || null,\\n created_at: bullJob.timestamp ? new Date(bullJob.timestamp).toISOString() : null,\\n started_at: bullJob.processedOn ? new Date(bullJob.processedOn).toISOString() : null,\\n completed_at: isCompleted && bullJob.finishedOn ? new Date(bullJob.finishedOn).toISOString() : null,\\n failed_at: isFailed && bullJob.finishedOn ? new Date(bullJob.finishedOn).toISOString() : null,\\n error: bullJob.failedReason || null,\\n metadata: bullJob.data || {},\\n };\\n}\\n\\n// Fetch all jobs from all queues in bulk by state bucket (no per-job getState() calls).\\nasync function getAllBullMQJobs() {\\n const results = [];\\n for (const { queue, type } of QUEUES) {\\n for (const bucket of STATE_BUCKETS) {\\n try {\\n const apiStatus = STATE_MAP[bucket] || bucket;\\n // getJobs([state], start, end) returns at most 200 per bucket\\n const jobs = await queue.getJobs([bucket], 0, 200);\\n for (const job of jobs) {\\n results.push(normalizeJob(job, type, apiStatus));\\n }\\n } catch {\\n // queue or bucket unavailable — skip\\n }\\n }\\n }\\n return results;\\n}\\n\\n// ── GET /events – Server-Sent Events stream of live job updates ───────────────\\n//\\n// Must be declared BEFORE GET /:id so the literal path \\\"events\\\" isn't treated\\n// as a job-id parameter.\\n//\\nrouter.get('/events', async (req, res) => {\\n res.setHeader('Content-Type', 'text/event-stream');\\n res.setHeader('Cache-Control', 'no-cache');\\n res.setHeader('Connection', 'keep-alive');\\n res.setHeader('X-Accel-Buffering', 'no'); // disable nginx proxy buffering\\n res.flushHeaders();\\n\\n le
|
|||
|
|
{"parentUuid":"5c1c8750-0026-4a79-9a1e-02771c98f6bb","isSidechain":true,"promptId":"a1e2ae2f-8095-470a-adc9-743a67df8a3c","agentId":"ab46ea77f9d510c47","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PZ5ALMVz7bmALJ9ZDNd1wF","type":"tool_result","content":"{\"result\":\"File: services/mam-api/src/routes/cluster.js\\n\\nimport express from 'express';\\nimport pool from '../db/pool.js';\\nimport { requireAuth } from '../middleware/auth.js';\\n\\nconst router = express.Router();\\nrouter.use(requireAuth);\\n\\n// If the agent reported Docker's default bridge IP (172.17.x) but the request\\n// itself came from a real LAN address, prefer the request source IP instead.\\n// We only check 172.17.x — the default docker0 bridge — not the full RFC1918\\n// 172.16/12 block, since real LANs (e.g. 172.18.91.x) fall in that range.\\nfunction pickIp(reportedIp, reqIp) {\\n const clean = (s) => (s || '').replace(/^::ffff:/, '');\\n const isDockerBridge = (ip) => /^172\\\\.17\\\\./.test(ip || '');\\n const r = clean(reqIp);\\n if (!reportedIp) return r || null;\\n if (isDockerBridge(reportedIp) && r && !isDockerBridge(r)) return r;\\n return reportedIp;\\n}\\n\\n// GET / – list all registered cluster nodes with online status\\nrouter.get('/', async (req, res, next) => {\\n try {\\n const r = await pool.query(\\n `SELECT *,\\n EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds\\n FROM cluster_nodes\\n ORDER BY registered_at ASC`\\n );\\n res.json(r.rows.map(row => ({\\n ...row,\\n online: Number(row.stale_seconds) < 120,\\n })));\\n } catch (err) { next(err); }\\n});\\n\\n// POST /heartbeat – upsert this node's registration (includes hardware capabilities)\\nrouter.post('/heartbeat', async (req, res, next) => {\\n try {\\n const {\\n hostname, ip_address,\\n role = 'worker', version, api_url,\\n cpu_usage, mem_used_mb, mem_total_mb,\\n capabilities, metadata,\\n } = req.body;\\n\\n if (!hostname) return res.status(400).json({ error: 'hostname is required' });\\n\\n const effectiveIp = pickIp(ip_address, req.ip || req.socket?.remoteAddress);\\n\\n const r = await pool.query(\\n `INSERT INTO cluster_nodes\\n (hostname, ip_address, role, version, api_url,\\n cpu_usage, mem_used_mb, mem_total_mb, last_seen, capabilities, metadata)\\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10)\\n ON CONFLICT (hostname) DO UPDATE SET\\n ip_address = EXCLUDED.ip_address,\\n role = EXCLUDED.role,\\n version = EXCLUDED.version,\\n api_url = EXCLUDED.api_url,\\n cpu_usage = EXCLUDED.cpu_usage,\\n mem_used_mb = EXCLUDED.mem_used_mb,\\n mem_total_mb = EXCLUDED.mem_total_mb,\\n last_seen = NOW(),\\n capabilities = EXCLUDED.capabilities,\\n metadata = EXCLUDED.metadata\\n RETURNING *`,\\n [\\n hostname,\\n effectiveIp,\\n role,\\n version || null,\\n api_url || null,\\n cpu_usage != null ? cpu_usage : null,\\n mem_used_mb != null ? mem_used_mb : null,\\n mem_total_mb != null ? mem_total_mb : null,\\n capabilities != null ? JSON.stringify(capabilities) : '{}',\\n metadata != null ? JSON.stringify(metadata) : null,\\n ]\\n );\\n res.json(r.rows[0]);\\n } catch (err) { next(err); }\\n});\\n\\n// GET /devices/blackmagic – flatten every node's DeckLink cards for the\\n// recorder picker. Returns one entry per device with the host node info.\\nrouter.get('/devices/blackmagic', async (req, res, next) => {\\n try {\\n const r = await pool.query(\\n `SELECT id, hostname, ip_address, role, capabilities,\\n EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds\\n FROM cluster_nodes\\n WHERE capabilities IS NOT NULL`\\n );\\n const out = [];\\n for (const row of r.rows) {\\n const online = Number(row.stale_seconds) < 120;\\n const b
|
|||
|
|
{"parentUuid":"b0771692-8843-463d-8440-c74471a647bd","isSidechain":true,"agentId":"ab46ea77f9d510c47","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Mti65Hh5krZCJgnNgkCe8q","type":"message","role":"assistant","content":[{"type":"text","text":"Here are the complete JSON response shapes for each endpoint in the wild-dragon MAM API:\n\n## **assets.js** (`/api/v1/assets`)\n\n**GET /** - List assets\n```json\n{\n \"assets\": [\n {\n \"id\": \"uuid\",\n \"project_id\": \"uuid\",\n \"bin_id\": \"uuid|null\",\n \"filename\": \"string\",\n \"display_name\": \"string\",\n \"status\": \"live|processing|ready|error|archived\",\n \"media_type\": \"string\",\n \"original_s3_key\": \"string|null\",\n \"proxy_s3_key\": \"string|null\",\n \"thumbnail_s3_key\": \"string|null\",\n \"codec\": \"string|null\",\n \"resolution\": \"string|null\",\n \"fps\": \"number|null\",\n \"duration_ms\": \"number|null\",\n \"start_tc\": \"string|null\",\n \"file_size\": \"number|null\",\n \"tags\": \"array|null\",\n \"notes\": \"string|null\",\n \"created_at\": \"iso8601\",\n \"updated_at\": \"iso8601\"\n }\n ],\n \"total\": \"number\"\n}\n```\n\n**POST /** - Register new asset\nReturns single asset object (same schema as list item)\n\n**POST /cleanup-live** - Clean up expired live assets\n```json\n{\n \"cleaned\": \"number\",\n \"assets\": [\n {\n \"id\": \"uuid\",\n \"display_name\": \"string\",\n \"project_id\": \"uuid\",\n \"created_at\": \"iso8601\"\n }\n ]\n}\n```\n\n**GET /:id** - Get single asset\nReturns asset object\n\n**PATCH /:id** - Update asset metadata\nReturns updated asset object\n\n**POST /:id/copy** - Copy asset\nReturns new asset object (201)\n\n**POST /:id/retry** - Retry failed asset processing\nReturns updated asset object\n\n**DELETE /:id** - Archive or permanently delete asset\nReturns updated asset object (status=archived) or `{ message: \"Asset deleted permanently\" }`\n\n**GET /:id/stream** - Get streaming info\n```json\n{\n \"url\": \"string|null\",\n \"type\": \"hls|mp4|null\",\n \"live\": \"boolean|undefined\",\n \"reason\": \"string|undefined\"\n}\n```\n\n**GET /:id/video** - Stream proxy/original video\nReturns video file (HTTP 200/206 with video/mp4 content type)\n\n**GET /:id/hires** - Get hi-res download link\n```json\n{\n \"url\": \"string (signed S3 URL)\",\n \"filename\": \"string\",\n \"ext\": \"string\",\n \"file_size\": \"number|null\",\n \"type\": \"hires\"\n}\n```\n\n**GET /:id/thumbnail** - Get thumbnail URL\n```json\n{\n \"url\": \"string (signed S3 URL)\"\n}\n```\n\n---\n\n## **projects.js** (`/api/v1/projects`)\n\n**GET /** - List all projects\n```json\n[\n {\n \"id\": \"uuid\",\n \"name\": \"string\",\n \"description\": \"string|null\",\n \"s3_prefix\": \"string\",\n \"created_at\": \"iso8601\",\n \"updated_at\": \"iso8601\"\n }\n]\n```\n\n**POST /** - Create project\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"string\",\n \"description\": \"string|null\",\n \"s3_prefix\": \"string\",\n \"created_at\": \"iso8601\",\n \"updated_at\": \"iso8601\"\n}\n```\n\n**GET /:id** - Get single project with asset count\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"string\",\n \"description\": \"string|null\",\n \"s3_prefix\": \"string\",\n \"asset_count\": \"number\",\n \"created_at\": \"iso8601\",\n \"updated_at\": \"iso8601\"\n}\n```\n\n**PATCH /:id** - Update project\nReturns updated project object\n\n**DELETE /:id** - Delete project\n```json\n{\n \"message\": \"Project deleted\",\n \"project\": { /* project object */ }\n}\n```\n\n---\n\n## **recorders.js** (`/api/v1/recorders`)\n\n**GET /** - List all recorders\n```json\n[\n {\n \"id\": \"uuid\",\n \"name\": \"string\",\n \"source_type\": \"sdi|srt|rtmp\",\n \"source_config\": \"object\",\n \"recording_codec\": \"string\",\n \"recording_resolution\": \"string\",\n \"recording_video_bitrate\": \"string|null\",\n \"recording_framerate\": \"string|null
|