flow-tracer 0.2.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,286 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FlowTracer</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <style>
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
12
+
13
+ .container { max-width: 1000px; margin: 0 auto; padding: 24px; }
14
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
15
+ .subtitle { color: #94a3b8; margin-bottom: 32px; }
16
+
17
+ /* Setup panel */
18
+ .setup { background: #1e293b; border-radius: 12px; padding: 24px; margin-bottom: 24px; }
19
+ .setup h2 { font-size: 16px; margin-bottom: 16px; }
20
+ .setup label { display: block; font-size: 13px; color: #94a3b8; margin-bottom: 4px; }
21
+ .setup input, .setup textarea { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: 14px; font-family: 'SF Mono', monospace; margin-bottom: 12px; }
22
+ .setup textarea { min-height: 80px; resize: vertical; }
23
+ .btn { padding: 10px 20px; border-radius: 8px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; }
24
+ .btn-primary { background: #3b82f6; color: white; }
25
+ .btn-primary:hover { background: #2563eb; }
26
+ .btn-primary:disabled { background: #334155; color: #64748b; cursor: not-allowed; }
27
+ .status { font-size: 13px; color: #22c55e; margin-top: 8px; }
28
+ .status.error { color: #ef4444; }
29
+
30
+ /* Chat */
31
+ .chat { background: #1e293b; border-radius: 12px; overflow: hidden; }
32
+ .messages { max-height: 70vh; overflow-y: auto; padding: 24px; }
33
+ .message { margin-bottom: 24px; }
34
+ .message.user { padding-left: 40px; }
35
+ .message.user .content { background: #334155; border-radius: 12px; padding: 12px 16px; display: inline-block; }
36
+ .message.assistant .content { line-height: 1.7; }
37
+ .message .content h1, .message .content h2, .message .content h3 { margin: 16px 0 8px; }
38
+ .message .content h1 { font-size: 18px; }
39
+ .message .content h2 { font-size: 16px; }
40
+ .message .content h3 { font-size: 15px; }
41
+ .message .content p { margin-bottom: 8px; }
42
+ .message .content code { background: #334155; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
43
+ .message .content pre { background: #0f172a; border-radius: 8px; padding: 16px; margin: 12px 0; overflow-x: auto; }
44
+ .message .content pre code { background: none; padding: 0; display: block; }
45
+ .message .content ul, .message .content ol { margin: 8px 0; padding-left: 24px; }
46
+ .message .content li { margin-bottom: 4px; }
47
+ .message .content blockquote { border-left: 3px solid #3b82f6; padding-left: 12px; margin: 8px 0; color: #94a3b8; }
48
+
49
+ /* Tables */
50
+ .message .content table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
51
+ .message .content th { background: #334155; padding: 10px 12px; text-align: left; font-weight: 600; border: 1px solid #475569; }
52
+ .message .content td { padding: 8px 12px; border: 1px solid #475569; }
53
+ .message .content tr:nth-child(even) { background: rgba(51, 65, 85, 0.3); }
54
+ .message .content tr:hover { background: rgba(51, 65, 85, 0.5); }
55
+
56
+ /* Mermaid */
57
+ .mermaid-container { background: #ffffff; border-radius: 8px; padding: 20px; margin: 12px 0; overflow-x: auto; }
58
+ .mermaid-container svg { max-width: 100%; height: auto; }
59
+ .mermaid-error { color: #ef4444; background: #1e293b; padding: 12px; border-radius: 8px; font-size: 13px; white-space: pre-wrap; word-break: break-word; }
60
+
61
+ /* Input */
62
+ .input-bar { display: flex; gap: 8px; padding: 16px; border-top: 1px solid #334155; }
63
+ .input-bar input { flex: 1; padding: 12px 16px; border-radius: 8px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: 14px; }
64
+ .input-bar input:focus { outline: none; border-color: #3b82f6; }
65
+
66
+ .loading { display: inline-block; }
67
+ .loading::after { content: ''; display: inline-block; width: 16px; height: 16px; border: 2px solid #64748b; border-top-color: #3b82f6; border-radius: 50%; animation: spin 0.8s linear infinite; margin-left: 8px; vertical-align: middle; }
68
+ @keyframes spin { to { transform: rotate(360deg); } }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <div class="container">
73
+ <h1>FlowTracer</h1>
74
+ <p class="subtitle">Ask questions about code flows across repos — get mermaid diagrams and answers</p>
75
+
76
+ <!-- Setup -->
77
+ <div class="setup" id="setup">
78
+ <h2>1. Register Repos</h2>
79
+ <label>Group Name</label>
80
+ <input type="text" id="groupName" placeholder="e.g., my-platform" value="">
81
+ <label>Repo Paths (one per line)</label>
82
+ <textarea id="repoPaths" placeholder="/Users/you/repos/frontend&#10;/Users/you/repos/backend&#10;/Users/you/repos/shared-lib"></textarea>
83
+ <button class="btn btn-primary" id="indexBtn" onclick="registerRepos()">Index Repos</button>
84
+ <div class="status" id="indexStatus"></div>
85
+ </div>
86
+
87
+ <!-- Chat -->
88
+ <div class="chat" id="chatPanel" style="display: none;">
89
+ <div class="messages" id="messages"></div>
90
+ <div class="input-bar">
91
+ <input type="text" id="questionInput" placeholder="Ask a question... e.g., How does the order flow work for magento?" onkeydown="if(event.key==='Enter')askQ()">
92
+ <button class="btn btn-primary" id="askBtn" onclick="askQ()">Ask</button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <script>
98
+ mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' });
99
+
100
+ let currentGroup = null;
101
+ let sessionId = null;
102
+ let mermaidCounter = 0;
103
+
104
+ async function registerRepos() {
105
+ const name = document.getElementById('groupName').value.trim();
106
+ const paths = document.getElementById('repoPaths').value.trim().split('\n').map(p => p.trim()).filter(Boolean);
107
+ const status = document.getElementById('indexStatus');
108
+ const btn = document.getElementById('indexBtn');
109
+
110
+ if (!name || !paths.length) { status.textContent = 'Fill in group name and at least one repo path'; status.className = 'status error'; return; }
111
+
112
+ btn.disabled = true;
113
+ status.textContent = 'Indexing repos...';
114
+ status.className = 'status';
115
+
116
+ try {
117
+ const res = await fetch('/repos', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ name, paths }),
121
+ });
122
+ const data = await res.json();
123
+ if (!res.ok) throw new Error(data.error);
124
+
125
+ currentGroup = name;
126
+ sessionId = null;
127
+ const repoTags = data.repos.map(r => `${r.name} (${r.files} files)`).join(', ');
128
+ status.textContent = `Indexed: ${repoTags}. Total: ${data.totals.files} files.`;
129
+ document.getElementById('chatPanel').style.display = 'block';
130
+ document.getElementById('messages').innerHTML = '';
131
+ document.getElementById('questionInput').focus();
132
+ } catch (err) {
133
+ status.textContent = err.message;
134
+ status.className = 'status error';
135
+ } finally {
136
+ btn.disabled = false;
137
+ }
138
+ }
139
+
140
+ async function askQ() {
141
+ const input = document.getElementById('questionInput');
142
+ const question = input.value.trim();
143
+ if (!question) return;
144
+
145
+ input.value = '';
146
+ addMessage('user', question);
147
+
148
+ const loadingEl = addMessage('assistant', '<span class="loading">Thinking</span>');
149
+ document.getElementById('askBtn').disabled = true;
150
+
151
+ try {
152
+ const url = sessionId ? `/ask/${sessionId}` : '/ask';
153
+ const body = sessionId ? { question } : { repos: currentGroup, question };
154
+
155
+ const res = await fetch(url, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify(body),
159
+ });
160
+ const data = await res.json();
161
+ if (!res.ok) throw new Error(data.error);
162
+
163
+ sessionId = data.sessionId || sessionId;
164
+ loadingEl.querySelector('.content').innerHTML = renderMarkdown(data.answer);
165
+ await renderMermaidDiagrams();
166
+ } catch (err) {
167
+ loadingEl.querySelector('.content').innerHTML = `<span style="color:#ef4444">${err.message}</span>`;
168
+ } finally {
169
+ document.getElementById('askBtn').disabled = false;
170
+ input.focus();
171
+ }
172
+ }
173
+
174
+ function addMessage(role, html) {
175
+ const messages = document.getElementById('messages');
176
+ const div = document.createElement('div');
177
+ div.className = `message ${role}`;
178
+ div.innerHTML = `<div class="content">${html}</div>`;
179
+ messages.appendChild(div);
180
+ messages.scrollTop = messages.scrollHeight;
181
+ return div;
182
+ }
183
+
184
+ // ── Mermaid Sanitizer ──────────────────────────────────
185
+
186
+ function sanitizeMermaid(code) {
187
+ // 0. Fix multi-line node labels — the #1 cause of mermaid errors.
188
+ // LLM generates: A["Pay Button Click\n(+page.svelte)"]
189
+ // Mermaid needs: A["Pay Button Click (+page.svelte)"]
190
+ // Strategy: join lines that are inside unclosed brackets.
191
+ const joinedLines = [];
192
+ let bracketDepth = 0;
193
+ for (const rawLine of code.split('\n')) {
194
+ const line = rawLine.trimEnd();
195
+ if (bracketDepth > 0) {
196
+ // We're inside an unclosed bracket — append to previous line
197
+ joinedLines[joinedLines.length - 1] += ' ' + line.trim();
198
+ } else {
199
+ joinedLines.push(line);
200
+ }
201
+ // Count brackets on the resulting line
202
+ const current = joinedLines[joinedLines.length - 1];
203
+ const opens = (current.match(/[\["(\{]/g) || []).length;
204
+ const closes = (current.match(/[\]"\)}\]]/g) || []).length;
205
+ // More accurate: count [ and ] specifically for mermaid node syntax
206
+ const openSquare = (current.match(/\[/g) || []).length;
207
+ const closeSquare = (current.match(/\]/g) || []).length;
208
+ bracketDepth = openSquare - closeSquare;
209
+ if (bracketDepth < 0) bracketDepth = 0;
210
+ }
211
+ code = joinedLines.join('\n');
212
+
213
+ // 1. Remove %% comments
214
+ code = code.replace(/%%[^\n]*/g, '');
215
+
216
+ // 2. Fix "end" + "subgraph" on same line, and similar multi-statement issues
217
+ let prev;
218
+ do {
219
+ prev = code;
220
+ code = code.replace(/\bend\b[ \t]+(subgraph\b)/gi, 'end\n $1');
221
+ code = code.replace(/\bend\b[ \t]{2,}(\w)/gi, 'end\n $1');
222
+ code = code.replace(/(\|\s*[\w"]+)[ \t]{2,}(\w+\s*-->)/g, '$1\n $2');
223
+ } while (code !== prev);
224
+
225
+ // 3. Replace actual newlines inside quoted labels with <br/>
226
+ // Catches any remaining multi-line labels after join
227
+ code = code.replace(/"([^"]*?)"/g, (match, inner) => {
228
+ return '"' + inner.replace(/\n\s*/g, '<br/>') + '"';
229
+ });
230
+
231
+ // 4. Clean up
232
+ return code.split('\n')
233
+ .map(l => l.trimEnd())
234
+ .filter((l, i, arr) => !(l === '' && i > 0 && arr[i - 1] === ''))
235
+ .join('\n');
236
+ }
237
+
238
+ // ── Markdown Renderer ──────────────────────────────────
239
+
240
+ function renderMarkdown(text) {
241
+ // Extract mermaid blocks BEFORE markdown parsing to prevent marked from mangling them
242
+ const mermaidPlaceholders = {};
243
+
244
+ text = text.replace(/```mermaid\n([\s\S]*?)```/g, (_, code) => {
245
+ const id = `mermaid-${mermaidCounter++}`;
246
+ const clean = sanitizeMermaid(code.trim());
247
+ mermaidPlaceholders[id] = clean;
248
+ return `<!--MERMAID:${id}-->`;
249
+ });
250
+
251
+ // Parse markdown with marked (handles tables, lists, code blocks, etc.)
252
+ const html = marked.parse(text);
253
+
254
+ // Replace mermaid placeholders with actual diagram containers
255
+ const finalHtml = html.replace(/<!--MERMAID:(\w+-\d+)-->/g, (_, id) => {
256
+ const code = mermaidPlaceholders[id] || '';
257
+ return `<div class="mermaid-container"><div class="mermaid" id="${id}">${escapeHtml(code)}</div></div>`;
258
+ });
259
+
260
+ return finalHtml;
261
+ }
262
+
263
+ function escapeHtml(str) {
264
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
265
+ }
266
+
267
+ // ── Mermaid Renderer ──────────────────────────────────
268
+
269
+ async function renderMermaidDiagrams() {
270
+ const elements = document.querySelectorAll('.mermaid:not([data-processed])');
271
+ for (const el of elements) {
272
+ el.setAttribute('data-processed', 'true');
273
+ try {
274
+ // Decode the escaped HTML back to raw mermaid code
275
+ const raw = el.textContent;
276
+ const { svg } = await mermaid.render(el.id + '-svg', raw);
277
+ el.innerHTML = svg;
278
+ } catch (err) {
279
+ const raw = el.textContent;
280
+ el.innerHTML = `<div class="mermaid-error">Mermaid render error: ${err.message}\n\nSanitized code:\n${raw}</div>`;
281
+ }
282
+ }
283
+ }
284
+ </script>
285
+ </body>
286
+ </html>
package/src/server.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * FlowTracer Server
3
+ *
4
+ * POST /repos — Register repos to index
5
+ * GET /repos — List registered repos
6
+ * POST /ask — Ask a question (returns mermaid + explanation)
7
+ * POST /ask/:sessionId — Follow-up question on an existing conversation
8
+ * GET /sessions — List active sessions
9
+ *
10
+ * Deploy anywhere that runs Node.js.
11
+ * Auth: set ANTHROPIC_API_KEY env var, OR have `claude` CLI installed (Pro plan).
12
+ */
13
+
14
+ import express from "express";
15
+ import crypto from "crypto";
16
+ import { dirname, join } from "path";
17
+ import { indexRepos, selectRelevantCode } from "./indexer.js";
18
+ import { askQuestion } from "./llm.js";
19
+ import { buildManifest } from "./summarizer.js";
20
+
21
+ const __dirname = dirname(new URL(import.meta.url).pathname);
22
+
23
+ const app = express();
24
+ app.use(express.json());
25
+ app.use(express.static(join(__dirname, "public")));
26
+
27
+ // ── State ───────────────────────────────────────────────
28
+
29
+ /** @type {Map<string, { paths: string[], indexed: Array, indexedAt: string }>} */
30
+ const repoStore = new Map();
31
+
32
+ /** @type {Map<string, { history: Array, repoGroup: string, createdAt: string, lastQuestion: string }>} */
33
+ const sessions = new Map();
34
+
35
+ // ── Routes ──────────────────────────────────────────────
36
+
37
+ app.get("/", (_req, res) => {
38
+ res.json({
39
+ name: "FlowTracer",
40
+ description: "Ask questions about code flows across any repos — get mermaid diagrams and conversational answers",
41
+ endpoints: {
42
+ "POST /repos": "Register repos to index. Body: { name: string, paths: string[] }",
43
+ "GET /repos": "List registered repo groups",
44
+ "POST /ask": "Ask a question. Body: { repos: string, question: string }",
45
+ "POST /ask/:sessionId": "Follow-up question. Body: { question: string }",
46
+ "GET /sessions": "List active conversation sessions",
47
+ },
48
+ });
49
+ });
50
+
51
+ /**
52
+ * POST /repos — Register and index a group of repos.
53
+ *
54
+ * Body: { name: "my-platform", paths: ["/path/to/repo1", "/path/to/repo2"] }
55
+ *
56
+ * Indexes all repos and stores the result. Re-posting the same name re-indexes.
57
+ */
58
+ app.post("/repos", async (req, res) => {
59
+ const { name, paths } = req.body;
60
+
61
+ if (!name || !paths?.length) {
62
+ return res.status(400).json({ error: "Required: { name: string, paths: string[] }" });
63
+ }
64
+
65
+ try {
66
+ console.log(`[repos] Indexing "${name}" with ${paths.length} repos...`);
67
+ const indexed = indexRepos(paths);
68
+ const totalFiles = indexed.reduce((sum, r) => sum + r.stats.totalFiles, 0);
69
+
70
+ console.log(`[repos] Building manifest...`);
71
+ const manifest = buildManifest(indexed);
72
+
73
+ repoStore.set(name, { paths, indexed, manifest, indexedAt: new Date().toISOString() });
74
+
75
+ console.log(`[repos] "${name}" indexed: ${totalFiles} files`);
76
+ res.json({
77
+ message: `Indexed ${paths.length} repos as "${name}"`,
78
+ repos: indexed.map((r) => ({
79
+ name: r.repo.name,
80
+ type: r.repo.type,
81
+ languages: r.repo.languages,
82
+ frameworks: r.repo.frameworks,
83
+ files: r.stats.totalFiles,
84
+ })),
85
+ totals: { files: totalFiles },
86
+ });
87
+ } catch (err) {
88
+ console.error(`[repos] Error:`, err.message);
89
+ res.status(500).json({ error: err.message });
90
+ }
91
+ });
92
+
93
+ /**
94
+ * GET /repos — List all registered repo groups.
95
+ */
96
+ app.get("/repos", (_req, res) => {
97
+ const groups = [];
98
+ for (const [name, data] of repoStore) {
99
+ groups.push({
100
+ name,
101
+ paths: data.paths,
102
+ indexedAt: data.indexedAt,
103
+ repos: data.indexed.map((r) => ({
104
+ name: r.repo.name,
105
+ type: r.repo.type,
106
+ files: r.stats.totalFiles,
107
+ })),
108
+ });
109
+ }
110
+ res.json(groups);
111
+ });
112
+
113
+ /**
114
+ * POST /ask — Ask a new question about a repo group.
115
+ *
116
+ * Body: { repos: "my-platform", question: "How does order flow work for magento?" }
117
+ *
118
+ * Returns: { sessionId, answer (with mermaid diagram), repos used }
119
+ */
120
+ app.post("/ask", async (req, res) => {
121
+ const { repos: repoGroupName, question } = req.body;
122
+
123
+ if (!repoGroupName || !question) {
124
+ return res.status(400).json({ error: "Required: { repos: string, question: string }" });
125
+ }
126
+
127
+ const repoGroup = repoStore.get(repoGroupName);
128
+ if (!repoGroup) {
129
+ return res.status(404).json({
130
+ error: `Repo group "${repoGroupName}" not found. Register repos first via POST /repos.`,
131
+ available: [...repoStore.keys()],
132
+ });
133
+ }
134
+
135
+ try {
136
+ console.log(`[ask] Question: "${question}" (repos: ${repoGroupName})`);
137
+
138
+ const relevantCode = await selectRelevantCode(repoGroup.indexed, question, repoGroup.manifest);
139
+ const repoInfos = repoGroup.indexed.map((r) => r.repo);
140
+
141
+ console.log(`[ask] Selected ${relevantCode.length} relevant files`);
142
+
143
+ const { answer, history } = await askQuestion(question, relevantCode, repoInfos);
144
+
145
+ const sessionId = crypto.randomUUID();
146
+ sessions.set(sessionId, {
147
+ history,
148
+ repoGroup: repoGroupName,
149
+ createdAt: new Date().toISOString(),
150
+ lastQuestion: question,
151
+ });
152
+
153
+ console.log(`[ask] Session ${sessionId} created`);
154
+
155
+ res.json({
156
+ sessionId,
157
+ answer,
158
+ filesUsed: relevantCode.length,
159
+ repos: repoInfos.map((r) => r.name),
160
+ });
161
+ } catch (err) {
162
+ console.error(`[ask] Error:`, err.message);
163
+ console.error(`[ask] Stack:`, err.stack);
164
+ res.status(500).json({ error: err.message });
165
+ }
166
+ });
167
+
168
+ /**
169
+ * POST /ask/:sessionId — Follow-up question on an existing conversation.
170
+ *
171
+ * Body: { question: "What happens if the payment fails?" }
172
+ *
173
+ * The LLM already has the code context and previous answers in memory.
174
+ */
175
+ app.post("/ask/:sessionId", async (req, res) => {
176
+ const { sessionId } = req.params;
177
+ const { question } = req.body;
178
+
179
+ if (!question) {
180
+ return res.status(400).json({ error: "Required: { question: string }" });
181
+ }
182
+
183
+ const session = sessions.get(sessionId);
184
+ if (!session) {
185
+ return res.status(404).json({ error: `Session "${sessionId}" not found. Start a new conversation via POST /ask.` });
186
+ }
187
+
188
+ const repoGroup = repoStore.get(session.repoGroup);
189
+
190
+ try {
191
+ console.log(`[follow-up] Session ${sessionId}: "${question}"`);
192
+
193
+ const repoInfos = repoGroup.indexed.map((r) => r.repo);
194
+
195
+ // For follow-ups, check if we need additional code context
196
+ const additionalCode = await selectRelevantCode(repoGroup.indexed, question, repoGroup.manifest);
197
+ let enrichedQuestion = question;
198
+
199
+ // Inject new files not already in conversation
200
+ const newFiles = additionalCode
201
+ .filter((c) => !session.history.some((m) =>
202
+ typeof m.content === "string" && m.content.includes(c.file)
203
+ ))
204
+ .slice(0, 15);
205
+
206
+ if (newFiles.length > 0) {
207
+ let extra = "\n\n[Additional code context for this follow-up]\n\n";
208
+ for (const f of newFiles) {
209
+ extra += `**${f.repo}/${f.file}**:\n\`\`\`\n${f.content}\n\`\`\`\n\n`;
210
+ }
211
+ enrichedQuestion = extra + question;
212
+ }
213
+
214
+ const { answer, history } = await askQuestion(enrichedQuestion, [], repoInfos, session.history);
215
+
216
+ session.history = history;
217
+ session.lastQuestion = question;
218
+
219
+ res.json({
220
+ sessionId,
221
+ answer,
222
+ conversationLength: history.length,
223
+ });
224
+ } catch (err) {
225
+ console.error(`[follow-up] Error:`, err.message);
226
+ res.status(500).json({ error: err.message });
227
+ }
228
+ });
229
+
230
+ /**
231
+ * GET /sessions — List active conversation sessions.
232
+ */
233
+ app.get("/sessions", (_req, res) => {
234
+ const list = [];
235
+ for (const [id, session] of sessions) {
236
+ list.push({
237
+ id,
238
+ repoGroup: session.repoGroup,
239
+ createdAt: session.createdAt,
240
+ lastQuestion: session.lastQuestion,
241
+ messages: session.history.length,
242
+ });
243
+ }
244
+ res.json(list);
245
+ });
246
+
247
+ // ── Start ───────────────────────────────────────────────
248
+
249
+ const PORT = process.env.PORT || 3847;
250
+
251
+ app.listen(PORT, () => {
252
+ console.log(`
253
+ ╔══════════════════════════════════════════════╗
254
+ ║ FlowTracer Server ║
255
+ ║ http://localhost:${PORT} ║
256
+ ╚══════════════════════════════════════════════╝
257
+
258
+ Ready. Usage:
259
+
260
+ 1. Register repos:
261
+ curl -X POST http://localhost:${PORT}/repos \\
262
+ -H "Content-Type: application/json" \\
263
+ -d '{"name": "my-platform", "paths": ["/path/to/repo1", "/path/to/repo2"]}'
264
+
265
+ 2. Ask a question:
266
+ curl -X POST http://localhost:${PORT}/ask \\
267
+ -H "Content-Type: application/json" \\
268
+ -d '{"repos": "my-platform", "question": "How does the order flow work?"}'
269
+
270
+ 3. Follow up:
271
+ curl -X POST http://localhost:${PORT}/ask/<sessionId> \\
272
+ -H "Content-Type: application/json" \\
273
+ -d '{"question": "What happens if payment fails?"}'
274
+ `);
275
+ });