singleton-pipeline 0.4.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.
@@ -0,0 +1,146 @@
1
+ import blessed from 'blessed';
2
+ import { C } from './shell.js';
3
+
4
+ const FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
5
+
6
+ // widgets: { screen, logPanel, statusBox } — provided by shell when running inside the TUI.
7
+ // If omitted, a standalone blessed screen is created.
8
+ export function createTimeline(stepNames, widgets = null) {
9
+ const N = stepNames.length;
10
+ const statuses = stepNames.map(() => 'pending');
11
+ const meta = stepNames.map(() => '');
12
+ let runningIdx = -1;
13
+ let spinnerInterval = null;
14
+ let spinnerFrame = 0;
15
+
16
+ let screen, logPanel, statusBox, ownScreen = false;
17
+
18
+ if (widgets) {
19
+ ({ screen, logPanel, statusBox } = widgets);
20
+ } else {
21
+ ownScreen = true;
22
+ screen = blessed.screen({ smartCSR: true, title: 'Singleton' });
23
+
24
+ logPanel = blessed.log({
25
+ top: 0, left: 0,
26
+ width: '100%', height: '100%-6',
27
+ tags: true,
28
+ scrollable: true,
29
+ alwaysScroll: true,
30
+ padding: { left: 2, top: 1, right: 2 }
31
+ });
32
+
33
+ const separator = blessed.line({
34
+ orientation: 'horizontal',
35
+ bottom: 5, left: 0,
36
+ width: '100%',
37
+ style: { fg: C.line }
38
+ });
39
+
40
+ statusBox = blessed.box({
41
+ bottom: 1, left: 0,
42
+ width: '100%', height: 4,
43
+ tags: true,
44
+ padding: { left: 2, right: 2 }
45
+ });
46
+
47
+ screen.append(logPanel);
48
+ screen.append(separator);
49
+ screen.append(statusBox);
50
+
51
+ screen.key(['C-c'], () => { screen.destroy(); process.exit(0); });
52
+ }
53
+
54
+ function dot(status, frame = 0) {
55
+ if (status === 'done') return `{${C.mint}-fg}●{/}`;
56
+ if (status === 'running') return `{#FFFFFF-fg}${FRAMES[frame % FRAMES.length]}{/}`;
57
+ if (status === 'paused') return `{${C.peach}-fg}●{/}`;
58
+ if (status === 'error') return `{${C.salmon}-fg}●{/}`;
59
+ return `{${C.ghost}-fg}○{/}`;
60
+ }
61
+
62
+ function shimmerName(text, frame = 0) {
63
+ const peak = frame % (text.length + 6);
64
+ return text.split('').map((ch, i) => {
65
+ const dist = Math.abs(i - peak);
66
+ let color = C.violet;
67
+ if (dist === 0) color = '#FFFFFF';
68
+ else if (dist === 1) color = '#EDD9FF';
69
+ else if (dist === 2) color = '#D4B0FE';
70
+ return ch === ' ' ? ch : `{${color}-fg}{bold}${ch}{/}`;
71
+ }).join('');
72
+ }
73
+
74
+ function compactDots(frame = 0) {
75
+ return stepNames.map((_name, i) => dot(statuses[i], frame)).join(` {${C.ghost}-fg}─{/} `);
76
+ }
77
+
78
+ function renderTimeline(frame = 0) {
79
+ const currentMeta = runningIdx >= 0 && meta[runningIdx]
80
+ ? ` {${C.ghost}-fg}${meta[runningIdx]}{/}`
81
+ : '';
82
+ const isPaused = runningIdx >= 0 && statuses[runningIdx] === 'paused';
83
+ const activityLabel = isPaused
84
+ ? `{${C.peach}-fg}{bold}Paused{/}`
85
+ : `{bold}Running{/}`;
86
+ const activityIcon = isPaused
87
+ ? `{${C.peach}-fg}●{/}`
88
+ : `{#FFFFFF-fg}${FRAMES[frame % FRAMES.length]}{/}`;
89
+ const runningLabel = runningIdx >= 0
90
+ ? `${activityLabel} ${activityIcon} ${shimmerName(stepNames[runningIdx], frame)} {${C.ghost}-fg}step ${runningIdx + 1}/${N}{/}${currentMeta}`
91
+ : `{bold}Running:{/} {${C.dimV}-fg}idle{/}`;
92
+ const statusLines = [
93
+ '',
94
+ runningLabel,
95
+ '',
96
+ compactDots(frame)
97
+ ];
98
+ statusBox.setContent(statusLines.join('\n'));
99
+ screen.render();
100
+ }
101
+
102
+ renderTimeline();
103
+
104
+ return {
105
+ log(text) { logPanel.log(`{${C.blue}-fg}${text}{/}`); screen.render(); },
106
+ logMuted(text) { logPanel.log(`{${C.dimV}-fg}${text}{/}`); screen.render(); },
107
+
108
+ setRunning(i, info = '') {
109
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
110
+ runningIdx = i;
111
+ statuses[i] = 'running';
112
+ meta[i] = info;
113
+ renderTimeline();
114
+ spinnerInterval = setInterval(() => { spinnerFrame++; renderTimeline(spinnerFrame); }, 80);
115
+ },
116
+
117
+ setPaused(i, info = '') {
118
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
119
+ runningIdx = i;
120
+ statuses[i] = 'paused';
121
+ meta[i] = info;
122
+ renderTimeline();
123
+ },
124
+
125
+ setDone(i, info = '') {
126
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
127
+ statuses[i] = 'done';
128
+ meta[i] = info;
129
+ if (runningIdx === i) runningIdx = -1;
130
+ renderTimeline();
131
+ },
132
+
133
+ setError(i, info = '') {
134
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
135
+ statuses[i] = 'error';
136
+ meta[i] = info;
137
+ if (runningIdx === i) runningIdx = -1;
138
+ renderTimeline();
139
+ },
140
+
141
+ end() {
142
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
143
+ if (ownScreen) screen.destroy();
144
+ }
145
+ };
146
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@singleton/server",
3
+ "version": "0.4.0-beta.0",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "dependencies": {
7
+ "cors": "^2.8.5",
8
+ "express": "^4.21.2",
9
+ "@singleton/cli": "*"
10
+ }
11
+ }
@@ -0,0 +1,43 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const WEB_DIST = path.resolve(__dirname, '../../web/dist');
9
+ import { agentsRouter } from './routes/agents.js';
10
+ import { pipelinesRouter } from './routes/pipelines.js';
11
+ import { filesRouter } from './routes/files.js';
12
+
13
+ export async function startServer({ port = 4317, root = process.cwd(), logger = console.log } = {}) {
14
+ const app = express();
15
+ app.use(cors());
16
+ app.use(express.json({ limit: '2mb' }));
17
+
18
+ const ctx = {
19
+ root,
20
+ pipelinesDir: path.join(root, '.singleton', 'pipelines'),
21
+ agentsCacheFile: path.join(root, '.singleton', 'agents.json')
22
+ };
23
+
24
+ app.get('/api/health', (_req, res) => res.json({ ok: true, root }));
25
+ app.use('/api/agents', agentsRouter(ctx));
26
+ app.use('/api/pipelines', pipelinesRouter(ctx));
27
+ app.use('/api/files', filesRouter(ctx));
28
+
29
+ app.use(express.static(WEB_DIST));
30
+ app.get('*', (_req, res) => res.sendFile(path.join(WEB_DIST, 'index.html')));
31
+
32
+ return new Promise((resolve) => {
33
+ const server = app.listen(port, () => {
34
+ logger(`Singleton server listening on http://localhost:${port}`);
35
+ logger(`Project root: ${root}`);
36
+ resolve(server);
37
+ });
38
+ });
39
+ }
40
+
41
+ if (import.meta.url === `file://${process.argv[1]}`) {
42
+ startServer({ port: Number(process.env.PORT) || 4317, root: process.cwd() });
43
+ }
@@ -0,0 +1,32 @@
1
+ import { Router } from 'express';
2
+ import fs from 'node:fs/promises';
3
+ import { scanAgents } from '../../../cli/src/scanner.js';
4
+
5
+ export function agentsRouter(ctx) {
6
+ const r = Router();
7
+
8
+ r.get('/', async (_req, res) => {
9
+ try {
10
+ try {
11
+ const raw = await fs.readFile(ctx.agentsCacheFile, 'utf8');
12
+ return res.json(JSON.parse(raw));
13
+ } catch {
14
+ const agents = await scanAgents(ctx.root);
15
+ return res.json({ scannedAt: new Date().toISOString(), root: ctx.root, agents });
16
+ }
17
+ } catch (err) {
18
+ res.status(500).json({ error: err.message });
19
+ }
20
+ });
21
+
22
+ r.post('/rescan', async (_req, res) => {
23
+ try {
24
+ const agents = await scanAgents(ctx.root);
25
+ res.json({ scannedAt: new Date().toISOString(), root: ctx.root, agents });
26
+ } catch (err) {
27
+ res.status(500).json({ error: err.message });
28
+ }
29
+ });
30
+
31
+ return r;
32
+ }
@@ -0,0 +1,42 @@
1
+ import { Router } from 'express';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.singleton', 'dist', 'build', '.next', '.cache']);
6
+
7
+ async function walk(root, rel = '', out = []) {
8
+ const abs = path.join(root, rel);
9
+ const entries = await fs.readdir(abs, { withFileTypes: true });
10
+ for (const e of entries) {
11
+ if (e.name.startsWith('.') && e.name !== '.singleton') {
12
+ if (e.isDirectory()) continue;
13
+ }
14
+ if (e.isDirectory()) {
15
+ if (SKIP_DIRS.has(e.name)) continue;
16
+ await walk(root, path.join(rel, e.name), out);
17
+ } else if (e.isFile()) {
18
+ out.push(path.join(rel, e.name));
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ export function filesRouter(ctx) {
25
+ const r = Router();
26
+
27
+ r.get('/', async (req, res) => {
28
+ try {
29
+ const ext = String(req.query.ext || '').replace(/^\./, '').toLowerCase();
30
+ const all = await walk(ctx.root);
31
+ const filtered = ext
32
+ ? all.filter((f) => f.toLowerCase().endsWith(`.${ext}`))
33
+ : all;
34
+ filtered.sort();
35
+ res.json({ root: ctx.root, files: filtered });
36
+ } catch (err) {
37
+ res.status(500).json({ error: err.message });
38
+ }
39
+ });
40
+
41
+ return r;
42
+ }
@@ -0,0 +1,74 @@
1
+ import { Router } from 'express';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ function safeName(name) {
6
+ return String(name).replace(/[^a-zA-Z0-9_-]+/g, '-').slice(0, 80) || 'pipeline';
7
+ }
8
+
9
+ export function pipelinesRouter(ctx) {
10
+ const r = Router();
11
+
12
+ r.get('/', async (_req, res) => {
13
+ try {
14
+ await fs.mkdir(ctx.pipelinesDir, { recursive: true });
15
+ const files = (await fs.readdir(ctx.pipelinesDir)).filter((f) => f.endsWith('.json'));
16
+ const items = (await Promise.all(files.map(async (f) => {
17
+ try {
18
+ const raw = await fs.readFile(path.join(ctx.pipelinesDir, f), 'utf8');
19
+ const parsed = JSON.parse(raw);
20
+ if (!parsed.name) return null;
21
+ return { file: f, ...parsed };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }))).filter(Boolean);
26
+ res.json({ pipelines: items });
27
+ } catch (err) {
28
+ res.status(500).json({ error: err.message });
29
+ }
30
+ });
31
+
32
+ r.get('/:name', async (req, res) => {
33
+ try {
34
+ const file = path.join(ctx.pipelinesDir, `${safeName(req.params.name)}.json`);
35
+ const raw = await fs.readFile(file, 'utf8');
36
+ res.json(JSON.parse(raw));
37
+ } catch (err) {
38
+ res.status(404).json({ error: 'not found' });
39
+ }
40
+ });
41
+
42
+ r.post('/', async (req, res) => {
43
+ try {
44
+ const { name, steps = [], nodes, edges } = req.body || {};
45
+ if (!name) return res.status(400).json({ error: 'name required' });
46
+ await fs.mkdir(ctx.pipelinesDir, { recursive: true });
47
+ const safe = safeName(name);
48
+ const file = path.join(ctx.pipelinesDir, `${safe}.json`);
49
+ const payload = {
50
+ name: safe,
51
+ created: new Date().toISOString(),
52
+ steps,
53
+ nodes,
54
+ edges
55
+ };
56
+ await fs.writeFile(file, JSON.stringify(payload, null, 2));
57
+ res.json({ ok: true, file: `${safe}.json`, ...payload });
58
+ } catch (err) {
59
+ res.status(500).json({ error: err.message });
60
+ }
61
+ });
62
+
63
+ r.delete('/:name', async (req, res) => {
64
+ try {
65
+ const file = path.join(ctx.pipelinesDir, `${safeName(req.params.name)}.json`);
66
+ await fs.unlink(file);
67
+ res.json({ ok: true });
68
+ } catch (err) {
69
+ res.status(404).json({ error: 'not found' });
70
+ }
71
+ });
72
+
73
+ return r;
74
+ }
@@ -0,0 +1 @@
1
+ .sidebar[data-v-82474bb7]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;width:288px;border-right:1px solid #262b34;background-color:#14171c;flex-shrink:0}.sidebar__header[data-v-82474bb7]{padding:12px;border-bottom:1px solid #262b34}.sidebar__title[data-v-82474bb7]{margin:0;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#94a3b8}.sidebar__hint[data-v-82474bb7]{margin:4px 0 0;font-size:11px;color:#64748b}.sidebar__list[data-v-82474bb7]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:8px}.sidebar__list[data-v-82474bb7]::-webkit-scrollbar{width:10px;height:10px}.sidebar__list[data-v-82474bb7]::-webkit-scrollbar-track{background:transparent}.sidebar__list[data-v-82474bb7]::-webkit-scrollbar-thumb{background:#262b34;border-radius:4px}.sidebar__list[data-v-82474bb7]::-webkit-scrollbar-thumb:hover{background:#3a4150}.sidebar__list[data-v-82474bb7]{flex:1;overflow-y:auto;padding:12px}.sidebar__state[data-v-82474bb7]{font-size:12px;color:#94a3b8}.sidebar__state--error[data-v-82474bb7]{color:#ef4444}.sidebar__state code[data-v-82474bb7]{font-size:11px;background:#1c2027;padding:2px 4px;border-radius:4px}.sidebar__nodes[data-v-82474bb7]{padding:12px;border-bottom:1px solid #262b34}.sidebar__subtitle[data-v-82474bb7]{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#64748b;margin-bottom:4px}.sidebar__io-row[data-v-82474bb7]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px}.io-card[data-v-82474bb7]{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:2px;flex:1;padding:8px;border-radius:6px;border:1px solid #3a4150;border-left:3px solid #7dd3fc;background-color:#1c2027;cursor:grab;transition:background-color .12s ease,border-color .12s ease}.io-card[data-v-82474bb7]:hover{background-color:#262b34;border-color:#38bdf8}.io-card[data-v-82474bb7]:active{cursor:grabbing}.io-card__label[data-v-82474bb7]{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:11px;color:#7dd3fc}.io-card__hint[data-v-82474bb7]{font-size:10px;color:#64748b}.agent-card[data-v-82474bb7]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:4px;padding:12px;border-radius:8px;border:1px solid #3a4150;background-color:#1c2027;cursor:grab;transition:background-color .12s ease,border-color .12s ease}.agent-card[data-v-82474bb7]:hover{background-color:#262b34;border-color:#38bdf8}.agent-card[data-v-82474bb7]:active{cursor:grabbing}.agent-card__head[data-v-82474bb7]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:8px}.agent-card__id[data-v-82474bb7]{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:12px;color:#7dd3fc}.agent-card__model[data-v-82474bb7]{font-size:10px;color:#64748b}.agent-card__desc[data-v-82474bb7]{margin:0;font-size:11px;color:#94a3b8;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.agent-card__tags[data-v-82474bb7]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:4px;flex-wrap:wrap}.agent-card__tag[data-v-82474bb7]{font-size:10px;padding:2px 6px;border-radius:4px;background:#3a4150;color:#94a3b8}.agent-card__ports[data-v-82474bb7]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:12px;font-size:10px;color:#64748b}.node-agent[data-v-9ea2f689]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;min-width:220px;border-radius:8px;border:1px solid #3a4150;background-color:#1c2027;box-shadow:0 4px 12px #00000059;color:#f1f5f9}.node-agent--stale[data-v-9ea2f689]{border-color:#7f1d1d;background-color:#450a0a}.node-agent--stale .node-agent__head[data-v-9ea2f689]{background-color:#7f1d1d;border-bottom-color:#7f1d1d}.node-agent--stale .node-agent__id[data-v-9ea2f689]{color:#fee2e2}.node-agent__head[data-v-9ea2f689]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border-bottom:1px solid #3a4150;background-color:#14171c;border-radius:8px 8px 0 0}.node-agent__id[data-v-9ea2f689]{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:12px;color:#7dd3fc}.node-agent__badge[data-v-9ea2f689]{margin-left:4px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:#fee2e2}.node-agent__close[data-v-9ea2f689]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;font-size:11px;color:#64748b;transition:color .12s ease}.node-agent__close[data-v-9ea2f689]:hover{color:#ef4444}.node-agent__desc[data-v-9ea2f689]{margin:0;padding:8px 12px;font-size:11px;color:#94a3b8;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.node-agent__warn[data-v-9ea2f689]{margin:0;padding:8px 12px;font-size:11px;color:#fee2e2}.node-agent__ports[data-v-9ea2f689]{display:flex;flex-direction:row;align-items:stretch;justify-content:flex-start;gap:8px;padding:8px 12px;border-top:1px solid #3a4150}.node-agent__col[data-v-9ea2f689]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:4px;flex:1}.node-agent__col--out[data-v-9ea2f689]{text-align:right}.node-agent__col-title[data-v-9ea2f689]{font-size:10px;text-transform:uppercase;color:#64748b;margin-bottom:4px}.node-agent__port[data-v-9ea2f689]{position:relative;font-size:11px;color:#94a3b8;padding:4px 0}.node-agent__port--in[data-v-9ea2f689]{padding-left:8px}.node-agent__port--out[data-v-9ea2f689]{padding-right:8px}.file-picker[data-v-be890d20]{display:flex;align-items:center;justify-content:center;position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background-color:#0009}.file-picker__dialog[data-v-be890d20]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;width:720px;max-width:95vw;max-height:80vh;border-radius:8px;border:1px solid #3a4150;background-color:#14171c;box-shadow:0 8px 24px #00000073}.file-picker__head[data-v-be890d20]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:12px;padding:12px;border-bottom:1px solid #262b34}.file-picker__title[data-v-be890d20]{margin:0;font-size:14px;font-weight:600}.file-picker__close[data-v-be890d20]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;color:#94a3b8}.file-picker__close[data-v-be890d20]:hover{color:#f1f5f9}.file-picker__body[data-v-be890d20]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:8px;padding:12px;overflow:hidden}.file-picker__search[data-v-be890d20]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;outline:0;font:inherit;color:inherit;padding:4px 8px;font-size:12px;border-radius:6px;border:1px solid #3a4150;background-color:#1c2027;color:#f1f5f9}.file-picker__search[data-v-be890d20]:focus{border-color:#38bdf8}.file-picker__state[data-v-be890d20]{padding:12px;font-size:12px;color:#94a3b8;text-align:center}.file-picker__state--error[data-v-be890d20]{color:#ef4444}.file-picker__list[data-v-be890d20]::-webkit-scrollbar{width:10px;height:10px}.file-picker__list[data-v-be890d20]::-webkit-scrollbar-track{background:transparent}.file-picker__list[data-v-be890d20]::-webkit-scrollbar-thumb{background:#262b34;border-radius:4px}.file-picker__list[data-v-be890d20]::-webkit-scrollbar-thumb:hover{background:#3a4150}.file-picker__list[data-v-be890d20]{list-style:none;margin:0;padding:0;overflow-y:auto;max-height:55vh;border:1px solid #262b34;border-radius:6px}.file-picker__item[data-v-be890d20]{padding:4px 8px;font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:11px;color:#f1f5f9;cursor:pointer;border-bottom:1px solid #262b34}.file-picker__item[data-v-be890d20]:last-child{border-bottom:0}.file-picker__item[data-v-be890d20]:hover{background-color:#1c2027;color:#7dd3fc}.node-input[data-v-95ec91bc]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;min-width:220px;border-radius:8px;border:1px solid #3a4150;background-color:#1c2027;box-shadow:0 4px 12px #00000059;color:#f1f5f9;border-left:3px solid #7dd3fc}.node-input__head[data-v-95ec91bc]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border-bottom:1px solid #3a4150;background-color:#14171c;border-radius:8px 8px 0 0}.node-input__meta[data-v-95ec91bc]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px}.node-input__kind[data-v-95ec91bc]{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:11px;color:#7dd3fc;text-transform:uppercase;letter-spacing:.06em}.node-input__subtypes[data-v-95ec91bc]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:2px}.node-input__st[data-v-95ec91bc]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;font:inherit;color:inherit;cursor:pointer;font-size:10px;padding:1px 4px;border-radius:4px;color:#64748b;border:1px solid transparent;transition:color .12s ease,border-color .12s ease}.node-input__st[data-v-95ec91bc]:hover{color:#94a3b8}.node-input__st--active[data-v-95ec91bc]{color:#7dd3fc;border-color:#3a4150}.node-input__close[data-v-95ec91bc]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;font-size:11px;color:#64748b}.node-input__close[data-v-95ec91bc]:hover{color:#ef4444}.node-input__body[data-v-95ec91bc]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0}.node-input__field[data-v-95ec91bc]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;outline:0;font:inherit;color:inherit;padding:4px 12px;font-size:11px;color:#f1f5f9;border-bottom:1px solid #3a4150;background:transparent;transition:background-color .12s ease}.node-input__field[data-v-95ec91bc]::placeholder{color:#64748b;font-style:italic}.node-input__field[data-v-95ec91bc]:focus{background-color:#262b34}.node-input__field--dim[data-v-95ec91bc]{color:#94a3b8}.node-input__file-btn[data-v-95ec91bc]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;font:inherit;color:inherit;cursor:pointer;padding:8px 12px;text-align:left;font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:11px;transition:background-color .12s ease;border-bottom:1px solid #3a4150}.node-input__file-btn[data-v-95ec91bc]:hover{background-color:#262b34}.node-input__path[data-v-95ec91bc]{color:#7dd3fc}.node-input__placeholder[data-v-95ec91bc]{color:#64748b;font-style:italic}.node-input__port[data-v-95ec91bc]{position:relative;font-size:11px;color:#94a3b8;padding:4px 12px;text-align:right}.canvas[data-v-b93ee04b]{width:100%;height:100%}.canvas[data-v-b93ee04b] .canvas__minimap{background-color:#14171c!important}.toolbar[data-v-770bf2a0]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px}.toolbar__input[data-v-770bf2a0]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;outline:0;font:inherit;color:inherit;width:160px;padding:4px 8px;font-size:11px;border-radius:6px;border:1px solid #3a4150;background-color:#1c2027;color:#f1f5f9;transition:border-color .12s ease}.toolbar__input[data-v-770bf2a0]:focus{border-color:#38bdf8}.toolbar__btn[data-v-770bf2a0]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;font:inherit;color:inherit;cursor:pointer;padding:4px 12px;font-size:11px;border-radius:6px;border:1px solid #3a4150;background-color:#1c2027;color:#f1f5f9;transition:background-color .12s ease,border-color .12s ease}.toolbar__btn[data-v-770bf2a0]:hover{background-color:#262b34}.toolbar__btn--primary[data-v-770bf2a0]{background-color:#0ea5e9;border-color:#0ea5e9;color:#fff}.toolbar__btn--primary[data-v-770bf2a0]:hover{background-color:#38bdf8;border-color:#38bdf8}.toolbar__btn--danger[data-v-770bf2a0]:hover{background-color:#7f1d1d;border-color:#7f1d1d}.modal[data-v-770bf2a0]{display:flex;align-items:center;justify-content:center;position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background-color:#0009}.modal__dialog[data-v-770bf2a0]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;max-width:95vw;max-height:85vh;border-radius:8px;border:1px solid #3a4150;background-color:#14171c;box-shadow:0 8px 24px #00000073}.modal__dialog--sm[data-v-770bf2a0]{width:560px}.modal__dialog--md[data-v-770bf2a0]{width:720px}.modal__header[data-v-770bf2a0]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:12px;padding:12px;border-bottom:1px solid #262b34}.modal__title[data-v-770bf2a0]{margin:0;font-size:14px;font-weight:600}.modal__close[data-v-770bf2a0]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;color:#94a3b8;transition:color .12s ease}.modal__close[data-v-770bf2a0]:hover{color:#f1f5f9}.modal__body[data-v-770bf2a0]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:16px}.modal__body[data-v-770bf2a0]::-webkit-scrollbar{width:10px;height:10px}.modal__body[data-v-770bf2a0]::-webkit-scrollbar-track{background:transparent}.modal__body[data-v-770bf2a0]::-webkit-scrollbar-thumb{background:#262b34;border-radius:4px}.modal__body[data-v-770bf2a0]::-webkit-scrollbar-thumb:hover{background:#3a4150}.modal__body[data-v-770bf2a0]{padding:16px;overflow-y:auto}.modal__section[data-v-770bf2a0]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:4px}.modal__section-head[data-v-770bf2a0]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:8px}.modal__label[data-v-770bf2a0]{font-size:11px;text-transform:uppercase;color:#94a3b8}.modal__copy[data-v-770bf2a0]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;font-size:11px;color:#7dd3fc}.modal__copy[data-v-770bf2a0]:hover{color:#38bdf8}.modal__code[data-v-770bf2a0]{margin:0;padding:8px 12px;font-size:11px;font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;color:#7dd3fc;background-color:#0b0d10;border:1px solid #262b34;border-radius:6px}.modal__code--inline[data-v-770bf2a0]{white-space:pre-wrap}.modal__code--scroll[data-v-770bf2a0]{overflow:auto;max-height:50vh;color:#f1f5f9}.pipeline-list[data-v-770bf2a0]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;list-style:none;margin:0;padding:0}.pipeline-list__empty[data-v-770bf2a0]{margin:0;padding:16px;text-align:center;font-size:12px;color:#64748b}.pipeline-list__item[data-v-770bf2a0]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:8px;padding:8px 0;border-bottom:1px solid #262b34}.pipeline-list__item[data-v-770bf2a0]:last-child{border-bottom:0}.pipeline-list__info[data-v-770bf2a0]{min-width:0;flex:1}.pipeline-list__name[data-v-770bf2a0]{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace;font-size:12px;color:#7dd3fc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pipeline-list__meta[data-v-770bf2a0]{font-size:10px;color:#64748b}.pipeline-list__actions[data-v-770bf2a0]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:4px;flex-shrink:0}.toast-stack[data-v-ac6df039]{display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-start;gap:8px;position:fixed;right:16px;bottom:16px;z-index:60}.toast[data-v-ac6df039]{padding:8px 12px;max-width:360px;font-size:11px;border-radius:8px;border:1px solid;box-shadow:0 4px 12px #00000059}.toast--info[data-v-ac6df039]{background-color:#1c2027;border-color:#3a4150;color:#f1f5f9}.toast--warn[data-v-ac6df039]{background-color:#b45309d9;border-color:#d97706;color:#fde68a}.toast--error[data-v-ac6df039]{background-color:#7f1d1dd9;border-color:#dc2626;color:#fee2e2}.toast--success[data-v-ac6df039]{background-color:#064e3bd9;border-color:#10b981;color:#d1fae5}.app[data-v-599c0f81]{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;height:100vh}.app__header[data-v-599c0f81]{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:12px;height:48px;padding:0 16px;border-bottom:1px solid #262b34;background-color:#14171c;flex-shrink:0}.app__brand[data-v-599c0f81]{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px}.app__logo[data-v-599c0f81]{color:#7dd3fc;font-weight:700}.app__title[data-v-599c0f81]{margin:0;font-size:14px;font-weight:600}.app__tagline[data-v-599c0f81]{font-size:11px;color:#64748b}.app__body[data-v-599c0f81]{display:flex;flex-direction:row;align-items:stretch;justify-content:flex-start;gap:0;flex:1;overflow:hidden}.app__canvas[data-v-599c0f81]{flex:1;position:relative}.vue-flow{position:relative;width:100%;height:100%;overflow:hidden;z-index:0;direction:ltr}.vue-flow__container{position:absolute;height:100%;width:100%;left:0;top:0}.vue-flow__pane{z-index:1}.vue-flow__pane.draggable{cursor:grab}.vue-flow__pane.selection{cursor:pointer}.vue-flow__pane.dragging{cursor:grabbing}.vue-flow__transformationpane{transform-origin:0 0;z-index:2;pointer-events:none}.vue-flow__viewport{z-index:4;overflow:clip}.vue-flow__selection{z-index:6}.vue-flow__edge-labels{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vue-flow__nodesselection-rect:focus,.vue-flow__nodesselection-rect:focus-visible{outline:none}.vue-flow .vue-flow__edges{pointer-events:none;overflow:visible}.vue-flow__edge-path,.vue-flow__connection-path{stroke:#b1b1b7;stroke-width:1;fill:none}.vue-flow__edge{pointer-events:visibleStroke;cursor:pointer}.vue-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.vue-flow__edge.animated path.vue-flow__edge-interaction{stroke-dasharray:none;animation:none}.vue-flow__edge.inactive{pointer-events:none}.vue-flow__edge.selected,.vue-flow__edge:focus,.vue-flow__edge:focus-visible{outline:none}.vue-flow__edge.selected .vue-flow__edge-path,.vue-flow__edge:focus .vue-flow__edge-path,.vue-flow__edge:focus-visible .vue-flow__edge-path{stroke:#555}.vue-flow__edge-textwrapper{pointer-events:all}.vue-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vue-flow__connection{pointer-events:none}.vue-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.vue-flow__connectionline{z-index:1001}.vue-flow__nodes{pointer-events:none;transform-origin:0 0}.vue-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.vue-flow__node.draggable{cursor:grab;pointer-events:all}.vue-flow__node.draggable.dragging{cursor:grabbing}.vue-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.vue-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.vue-flow__nodesselection-rect.dragging{cursor:grabbing}.vue-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px}.vue-flow__handle.connectable{pointer-events:all;cursor:crosshair}.vue-flow__handle-bottom{left:50%;bottom:0;transform:translate(-50%,50%)}.vue-flow__handle-top{left:50%;top:0;transform:translate(-50%,-50%)}.vue-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.vue-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.vue-flow__edgeupdater{cursor:move;pointer-events:all}.vue-flow__panel{position:absolute;z-index:5;margin:15px}.vue-flow__panel.top{top:0}.vue-flow__panel.bottom{bottom:0}.vue-flow__panel.left{left:0}.vue-flow__panel.right{right:0}.vue-flow__panel.center{left:50%;transform:translate(-50%)}@keyframes dashdraw{0%{stroke-dashoffset:10}}:root{--vf-node-bg: #fff;--vf-node-text: #222;--vf-connection-path: #b1b1b7;--vf-handle: #555}.vue-flow__edge.updating .vue-flow__edge-path{stroke:#777}.vue-flow__edge-text{font-size:10px}.vue-flow__edge-textbg{fill:#fff}.vue-flow__connection-path{stroke:var(--vf-connection-path)}.vue-flow__node{cursor:grab}.vue-flow__node.selectable:focus,.vue-flow__node.selectable:focus-visible{outline:none}.vue-flow__node-default,.vue-flow__node-input,.vue-flow__node-output{padding:10px;border-radius:3px;width:150px;font-size:12px;text-align:center;border-width:1px;border-style:solid;color:var(--vf-node-text);background-color:var(--vf-node-bg);border-color:var(--vf-node-color)}.vue-flow__node-default.selected,.vue-flow__node-default.selected:hover,.vue-flow__node-input.selected,.vue-flow__node-input.selected:hover,.vue-flow__node-output.selected,.vue-flow__node-output.selected:hover{box-shadow:0 0 0 .5px var(--vf-box-shadow)}.vue-flow__node-default.selected,.vue-flow__node-default:focus,.vue-flow__node-default:focus-visible,.vue-flow__node-input.selected,.vue-flow__node-input:focus,.vue-flow__node-input:focus-visible,.vue-flow__node-output.selected,.vue-flow__node-output:focus,.vue-flow__node-output:focus-visible{outline:none;border:1px solid #555}.vue-flow__node-default .vue-flow__handle,.vue-flow__node-input .vue-flow__handle,.vue-flow__node-output .vue-flow__handle{background:var(--vf-handle)}.vue-flow__node-default.selectable:hover,.vue-flow__node-input.selectable:hover,.vue-flow__node-output.selectable:hover{box-shadow:0 1px 4px 1px #00000014}.vue-flow__node-input{--vf-node-color: var(--vf-node-color, #0041d0);--vf-handle: var(--vf-node-color, #0041d0);--vf-box-shadow: var(--vf-node-color, #0041d0);background:var(--vf-node-bg);border-color:var(--vf-node-color, #0041d0)}.vue-flow__node-input.selected,.vue-flow__node-input:focus,.vue-flow__node-input:focus-visible{outline:none;border:1px solid var(--vf-node-color, #0041d0)}.vue-flow__node-default{--vf-handle: var(--vf-node-color, #1a192b);--vf-box-shadow: var(--vf-node-color, #1a192b);background:var(--vf-node-bg);border-color:var(--vf-node-color, #1a192b)}.vue-flow__node-default.selected,.vue-flow__node-default:focus,.vue-flow__node-default:focus-visible{outline:none;border:1px solid var(--vf-node-color, #1a192b)}.vue-flow__node-output{--vf-handle: var(--vf-node-color, #ff0072);--vf-box-shadow: var(--vf-node-color, #ff0072);background:var(--vf-node-bg);border-color:var(--vf-node-color, #ff0072)}.vue-flow__node-output.selected,.vue-flow__node-output:focus,.vue-flow__node-output:focus-visible{outline:none;border:1px solid var(--vf-node-color, #ff0072)}.vue-flow__nodesselection-rect,.vue-flow__selection{background:#0059dc14;border:1px dotted rgba(0,89,220,.8)}.vue-flow__nodesselection-rect:focus,.vue-flow__nodesselection-rect:focus-visible,.vue-flow__selection:focus,.vue-flow__selection:focus-visible{outline:none}.vue-flow__handle{width:6px;height:6px;background:var(--vf-handle);border:1px solid #fff;border-radius:100%}.vue-flow__controls{box-shadow:0 0 2px 1px #00000014}.vue-flow__controls-button{background:#fefefe;border:none;border-bottom:1px solid #eee;box-sizing:content-box;display:flex;justify-content:center;align-items:center;width:16px;height:16px;cursor:pointer;-webkit-user-select:none;user-select:none;padding:5px}.vue-flow__controls-button svg{width:100%;max-width:12px;max-height:12px}.vue-flow__controls-button:hover{background:#f4f4f4}.vue-flow__controls-button:disabled{pointer-events:none}.vue-flow__controls-button:disabled svg{fill-opacity:.4}.vue-flow__minimap{background-color:#fff}.vue-flow__minimap.pannable{cursor:grab}.vue-flow__minimap.dragging{cursor:grabbing}.vue-flow__minimap-mask.pannable{cursor:grab}*,*:before,*:after{box-sizing:border-box}html,body,#app{height:100%;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:14px;background-color:#0b0d10;color:#f1f5f9;-webkit-font-smoothing:antialiased}a{color:#38bdf8}code,pre{font-family:SF Mono,JetBrains Mono,Fira Code,Menlo,monospace}.vue-flow__node-agent{padding:0;border:none;background:transparent;box-shadow:none}.vue-flow__handle{width:10px;height:10px;background:#38bdf8;border:2px solid #0b0d10}.vue-flow__edge-path{stroke:#38bdf8;stroke-width:2}.vue-flow__background{background-color:#0b0d10}.vue-flow__controls button{background:#1c2027;border-color:#3a4150;color:#f1f5f9}.vue-flow__controls button:hover{background:#262b34}.vue-flow__controls button svg{fill:#f1f5f9}.vue-flow__minimap{background:#14171c!important}