edsger 0.56.3 → 0.58.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.
Files changed (81) hide show
  1. package/dist/api/chat.js +55 -2
  2. package/dist/api/cross-product.d.ts +8 -1
  3. package/dist/api/cross-product.js +44 -1
  4. package/dist/api/intelligence.js +98 -0
  5. package/dist/api/issues/get-issue.js +26 -0
  6. package/dist/api/issues/issue-utils.js +52 -0
  7. package/dist/api/issues/test-cases.js +89 -14
  8. package/dist/api/issues/update-issue.js +46 -8
  9. package/dist/api/issues/user-stories.js +89 -14
  10. package/dist/api/products/test-cases.d.ts +18 -0
  11. package/dist/api/products/test-cases.js +51 -0
  12. package/dist/api/products.js +21 -0
  13. package/dist/api/release-test-cases.js +38 -0
  14. package/dist/api/releases.js +86 -0
  15. package/dist/api/tasks.js +41 -4
  16. package/dist/api/test-reports.js +22 -4
  17. package/dist/api/user-psychology.d.ts +101 -0
  18. package/dist/api/user-psychology.js +143 -0
  19. package/dist/auth/auth-store.d.ts +33 -0
  20. package/dist/auth/auth-store.js +39 -0
  21. package/dist/commands/agent-workflow/chat-worker.js +187 -15
  22. package/dist/commands/agent-workflow/processor.d.ts +11 -0
  23. package/dist/commands/agent-workflow/processor.js +81 -2
  24. package/dist/commands/product-test-cases/index.d.ts +12 -0
  25. package/dist/commands/product-test-cases/index.js +40 -0
  26. package/dist/commands/screen-flow/index.d.ts +16 -0
  27. package/dist/commands/screen-flow/index.js +45 -0
  28. package/dist/commands/user-psychology/index.d.ts +7 -0
  29. package/dist/commands/user-psychology/index.js +51 -0
  30. package/dist/index.js +65 -0
  31. package/dist/phases/analyze-logs/index.js +27 -6
  32. package/dist/phases/bug-fixing/context-fetcher.js +26 -5
  33. package/dist/phases/find-features/index.js +53 -9
  34. package/dist/phases/find-shared/mcp.js +21 -0
  35. package/dist/phases/growth-analysis/context.d.ts +5 -3
  36. package/dist/phases/growth-analysis/context.js +52 -5
  37. package/dist/phases/output-contracts.js +140 -0
  38. package/dist/phases/pr-resolve/github-reply.d.ts +5 -2
  39. package/dist/phases/pr-resolve/github-reply.js +19 -3
  40. package/dist/phases/pr-resolve/index.js +19 -5
  41. package/dist/phases/pr-resolve/prompts.js +17 -18
  42. package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
  43. package/dist/phases/pr-shared/agent-utils.js +48 -4
  44. package/dist/phases/product-test-cases/index.d.ts +25 -0
  45. package/dist/phases/product-test-cases/index.js +174 -0
  46. package/dist/phases/product-test-cases/prompts.d.ts +24 -0
  47. package/dist/phases/product-test-cases/prompts.js +80 -0
  48. package/dist/phases/product-test-cases/types.d.ts +17 -0
  49. package/dist/phases/product-test-cases/types.js +27 -0
  50. package/dist/phases/screen-flow/index.d.ts +23 -0
  51. package/dist/phases/screen-flow/index.js +285 -0
  52. package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
  53. package/dist/phases/screen-flow/mcp-server.js +262 -0
  54. package/dist/phases/screen-flow/prompts.d.ts +19 -0
  55. package/dist/phases/screen-flow/prompts.js +41 -0
  56. package/dist/phases/screen-flow/theme.d.ts +19 -0
  57. package/dist/phases/screen-flow/theme.js +193 -0
  58. package/dist/phases/screen-flow/types.d.ts +130 -0
  59. package/dist/phases/screen-flow/types.js +81 -0
  60. package/dist/phases/user-psychology/agent.d.ts +16 -0
  61. package/dist/phases/user-psychology/agent.js +105 -0
  62. package/dist/phases/user-psychology/context.d.ts +10 -0
  63. package/dist/phases/user-psychology/context.js +65 -0
  64. package/dist/phases/user-psychology/index.d.ts +18 -0
  65. package/dist/phases/user-psychology/index.js +96 -0
  66. package/dist/phases/user-psychology/prompts.d.ts +2 -0
  67. package/dist/phases/user-psychology/prompts.js +41 -0
  68. package/dist/services/audit-logs.js +67 -9
  69. package/dist/services/branches.js +90 -14
  70. package/dist/services/phase-ratings.js +71 -9
  71. package/dist/services/product-logs.js +65 -5
  72. package/dist/services/pull-requests.js +74 -14
  73. package/dist/skills/phase/screen-flow/SKILL.md +78 -0
  74. package/dist/skills/phase/user-psychology/SKILL.md +135 -0
  75. package/dist/supabase/client.d.ts +23 -0
  76. package/dist/supabase/client.js +90 -0
  77. package/dist/system/session-manager.js +97 -24
  78. package/dist/types/index.d.ts +3 -0
  79. package/dist/utils/logger.js +24 -4
  80. package/package.json +4 -3
  81. package/vitest.config.ts +1 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Prompts for the product-level test cases phase.
3
+ *
4
+ * Agent's job: explore the cloned product repo and propose a regression-suite
5
+ * worth of test cases for the whole product (not a single issue). Existing
6
+ * test cases — both approved (locked) and unapproved (replaceable) — are
7
+ * included in the context so the agent doesn't duplicate work and so the
8
+ * unapproved set can be refreshed when the codebase has drifted.
9
+ */
10
+ export interface ExistingProductTestCase {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ is_critical: boolean;
15
+ status: string;
16
+ }
17
+ export interface ProductTestCasesPromptContext {
18
+ productName: string;
19
+ productDescription?: string;
20
+ approvedTestCases: ExistingProductTestCase[];
21
+ replaceableTestCases: ExistingProductTestCase[];
22
+ }
23
+ export declare function createProductTestCasesSystemPrompt(): string;
24
+ export declare function createProductTestCasesUserPrompt(context: ProductTestCasesPromptContext): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Prompts for the product-level test cases phase.
3
+ *
4
+ * Agent's job: explore the cloned product repo and propose a regression-suite
5
+ * worth of test cases for the whole product (not a single issue). Existing
6
+ * test cases — both approved (locked) and unapproved (replaceable) — are
7
+ * included in the context so the agent doesn't duplicate work and so the
8
+ * unapproved set can be refreshed when the codebase has drifted.
9
+ */
10
+ export function createProductTestCasesSystemPrompt() {
11
+ return `You are a senior QA engineer drafting a product-level regression test suite.
12
+
13
+ The current working directory is a fresh clone of the product's repository. Use Glob/Grep/Read to explore it and understand the surface area: entry points, public APIs, user-facing flows, critical paths. Look at recent commits with Bash if it helps.
14
+
15
+ Output a single JSON result of the form:
16
+ \`\`\`json
17
+ {
18
+ "test_cases_result": {
19
+ "summary": "Short 1-2 sentence summary of what you covered",
20
+ "created_test_cases": [
21
+ {
22
+ "name": "Concise name (max 200 chars)",
23
+ "description": "Markdown describing Given/When/Then steps + expected result",
24
+ "is_critical": true
25
+ }
26
+ ],
27
+ "deleted_test_case_ids": [
28
+ "uuid-of-replaceable-test-case-you-want-removed"
29
+ ],
30
+ "deletion_reasons": {
31
+ "uuid": "Why this test case is no longer relevant"
32
+ }
33
+ }
34
+ }
35
+ \`\`\`
36
+
37
+ Rules:
38
+ - DO NOT duplicate any test case in the "Approved test cases" or "Replaceable test cases" sections. If an existing case already covers a behaviour, skip it.
39
+ - You MAY propose deletions ONLY for entries in the "Replaceable test cases" list. Approved test cases are locked by the human reviewer and cannot be touched.
40
+ - Mark \`is_critical: true\` only for must-pass flows (auth, payments, data loss risks, primary user journeys).
41
+ - Aim for 5-30 high-signal cases total. Quality over quantity.
42
+ - Each test case must be executable by a human tester reading the description alone.
43
+ - The "deleted_test_case_ids" and "deletion_reasons" fields are optional; omit or use [] / {} when you have nothing to delete.
44
+ - Wrap the final JSON in a single \`\`\`json fenced block. No extra prose after it.`;
45
+ }
46
+ export function createProductTestCasesUserPrompt(context) {
47
+ const lines = [];
48
+ lines.push(`# Product: ${context.productName}`);
49
+ if (context.productDescription) {
50
+ lines.push('');
51
+ lines.push(`## Description`);
52
+ lines.push(context.productDescription);
53
+ }
54
+ lines.push('');
55
+ lines.push('## Approved test cases (locked — do not duplicate, cannot delete)');
56
+ if (context.approvedTestCases.length === 0) {
57
+ lines.push('_(none)_');
58
+ }
59
+ else {
60
+ for (const tc of context.approvedTestCases) {
61
+ lines.push(`- **${tc.name}**${tc.is_critical ? ' [critical]' : ''}`);
62
+ lines.push(` ${tc.description.replace(/\n/g, '\n ')}`);
63
+ }
64
+ }
65
+ lines.push('');
66
+ lines.push('## Replaceable test cases (draft / pending_approval — may be deleted if obsolete or duplicated)');
67
+ if (context.replaceableTestCases.length === 0) {
68
+ lines.push('_(none)_');
69
+ }
70
+ else {
71
+ for (const tc of context.replaceableTestCases) {
72
+ lines.push(`- \`${tc.id}\` (${tc.status})${tc.is_critical ? ' [critical]' : ''} — **${tc.name}**`);
73
+ lines.push(` ${tc.description.replace(/\n/g, '\n ')}`);
74
+ }
75
+ }
76
+ lines.push('');
77
+ lines.push('## Task');
78
+ lines.push('Explore the repository (Glob/Grep/Read/Bash) and propose product-level regression test cases. Follow the JSON output contract in your system prompt.');
79
+ return lines.join('\n');
80
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Wire shape for the product-test-cases agent output. The agent is asked to
3
+ * emit `{ test_cases_result: { ... } }`; we validate it loosely so a single
4
+ * stray field doesn't kill an otherwise-good run.
5
+ */
6
+ export interface ProductTestCaseDraft {
7
+ name: string;
8
+ description: string;
9
+ is_critical?: boolean;
10
+ }
11
+ export interface ProductTestCasesAgentResult {
12
+ summary: string;
13
+ created_test_cases: ProductTestCaseDraft[];
14
+ deleted_test_case_ids?: string[];
15
+ deletion_reasons?: Record<string, string>;
16
+ }
17
+ export declare function isProductTestCasesAgentResult(value: unknown): value is ProductTestCasesAgentResult;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wire shape for the product-test-cases agent output. The agent is asked to
3
+ * emit `{ test_cases_result: { ... } }`; we validate it loosely so a single
4
+ * stray field doesn't kill an otherwise-good run.
5
+ */
6
+ export function isProductTestCasesAgentResult(value) {
7
+ if (!value || typeof value !== 'object') {
8
+ return false;
9
+ }
10
+ const v = value;
11
+ if (typeof v.summary !== 'string') {
12
+ return false;
13
+ }
14
+ if (!Array.isArray(v.created_test_cases)) {
15
+ return false;
16
+ }
17
+ for (const tc of v.created_test_cases) {
18
+ if (!tc || typeof tc !== 'object') {
19
+ return false;
20
+ }
21
+ const t = tc;
22
+ if (typeof t.name !== 'string' || typeof t.description !== 'string') {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * screen-flow phase: clone the product's repo, ask Claude to map every
3
+ * user-facing screen and the transitions between them into a structured
4
+ * ScreenFlowExtraction, then persist the result to screen_flows /
5
+ * screen_flow_nodes / screen_flow_edges via the Supabase SDK.
6
+ *
7
+ * Companion to find-architecture / find-bugs / find-features. Same workspace
8
+ * pattern, but writes to its own tables rather than filing issues.
9
+ */
10
+ export interface ScreenFlowOptions {
11
+ productId: string;
12
+ flowId: string;
13
+ guidance?: string;
14
+ verbose?: boolean;
15
+ }
16
+ export interface ScreenFlowResult {
17
+ status: 'success' | 'error';
18
+ message: string;
19
+ nodesCreated?: number;
20
+ edgesCreated?: number;
21
+ summary?: string;
22
+ }
23
+ export declare function runScreenFlowPhase(options: ScreenFlowOptions): Promise<ScreenFlowResult>;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * screen-flow phase: clone the product's repo, ask Claude to map every
3
+ * user-facing screen and the transitions between them into a structured
4
+ * ScreenFlowExtraction, then persist the result to screen_flows /
5
+ * screen_flow_nodes / screen_flow_edges via the Supabase SDK.
6
+ *
7
+ * Companion to find-architecture / find-bugs / find-features. Same workspace
8
+ * pattern, but writes to its own tables rather than filing issues.
9
+ */
10
+ import { query } from '@anthropic-ai/claude-agent-sdk';
11
+ import { getGitHubConfigByProduct } from '../../api/github.js';
12
+ import { DEFAULT_MODEL } from '../../constants.js';
13
+ import { getSupabase } from '../../supabase/client.js';
14
+ import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
15
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, } from '../../workspace/workspace-manager.js';
16
+ import { fetchProductBasics } from '../find-shared/mcp.js';
17
+ import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
18
+ import { createScreenFlowCaptureState, createScreenFlowMcpServer, validateConsistency, } from './mcp-server.js';
19
+ import { createScreenFlowSystemPrompt, createScreenFlowUserPrompt, } from './prompts.js';
20
+ import { extractTheme } from './theme.js';
21
+ import { isScreenFlowExtraction, } from './types.js';
22
+ const WORKSPACE_KEY = 'screen-flow';
23
+ const MAX_TURNS = 150;
24
+ // Auto-layout: simple grid; users can drag afterwards and we persist positions.
25
+ const COLUMN_WIDTH = 380;
26
+ const ROW_HEIGHT = 480;
27
+ const COLUMNS = 4;
28
+ export async function runScreenFlowPhase(options) {
29
+ const { productId, flowId, guidance, verbose } = options;
30
+ logInfo(`Starting screen-flow generation for product ${productId}`);
31
+ const supabase = getSupabase();
32
+ await markFlowRunning(supabase, flowId);
33
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
34
+ if (!githubConfig.configured ||
35
+ !githubConfig.token ||
36
+ !githubConfig.owner ||
37
+ !githubConfig.repo) {
38
+ const msg = githubConfig.message ||
39
+ 'GitHub repository not configured for this product. Connect a repo first.';
40
+ await markFlowFailed(supabase, flowId, msg);
41
+ return { status: 'error', message: msg };
42
+ }
43
+ let repoPath;
44
+ let succeeded = false;
45
+ try {
46
+ const workspaceRoot = ensureWorkspaceDir();
47
+ const repoKey = `${WORKSPACE_KEY}-${productId}`;
48
+ ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, githubConfig.owner, githubConfig.repo, githubConfig.token));
49
+ const product = await fetchProductBasics(productId);
50
+ const theme = extractTheme(repoPath);
51
+ if (Object.keys(theme).length > 0) {
52
+ logInfo(`Extracted theme: ${Object.entries(theme).map(([k, v]) => `${k}=${v}`).join(', ')}`);
53
+ await persistTheme(supabase, flowId, theme);
54
+ }
55
+ const systemPrompt = await createScreenFlowSystemPrompt({
56
+ projectDir: repoPath,
57
+ hasCodebase: true,
58
+ });
59
+ const userPrompt = createScreenFlowUserPrompt({
60
+ productName: product.name,
61
+ productDescription: product.description,
62
+ guidance,
63
+ });
64
+ logInfo('Running Claude screen-flow extraction...');
65
+ // The agent submits the extraction by calling submit_screen_flow on the
66
+ // in-process MCP server. The handler validates with Zod + cross-field
67
+ // checks and stores the result in `captureState.captured`. If the agent
68
+ // never calls the tool, we fall back to parsing a fenced screen_flow
69
+ // block out of the assistant text.
70
+ const captureState = createScreenFlowCaptureState();
71
+ const mcpServer = createScreenFlowMcpServer(captureState, {
72
+ onProgress: ({ phase, message }) => {
73
+ logInfo(`[${phase}] ${message}`);
74
+ },
75
+ });
76
+ let lastAssistantResponse = '';
77
+ let extraction = null;
78
+ for await (const message of query({
79
+ prompt: createPromptGenerator(userPrompt),
80
+ options: {
81
+ systemPrompt: {
82
+ type: 'preset',
83
+ preset: 'claude_code',
84
+ append: systemPrompt,
85
+ },
86
+ model: DEFAULT_MODEL,
87
+ maxTurns: MAX_TURNS,
88
+ permissionMode: 'bypassPermissions',
89
+ cwd: repoPath,
90
+ mcpServers: {
91
+ 'screen-flow': mcpServer,
92
+ },
93
+ },
94
+ })) {
95
+ const { assistantBuffer, extraction: nextExtraction } = processSdkMessage(message, lastAssistantResponse, captureState, verbose);
96
+ lastAssistantResponse = assistantBuffer;
97
+ if (nextExtraction) {
98
+ extraction = nextExtraction;
99
+ }
100
+ }
101
+ if (!extraction) {
102
+ const msg = 'Screen flow extraction failed: agent did not call submit_screen_flow and no parseable screen_flow block was found in the response';
103
+ await markFlowFailed(supabase, flowId, msg);
104
+ return { status: 'error', message: msg };
105
+ }
106
+ logInfo(`Extraction produced ${extraction.nodes.length} screens / ${extraction.edges.length} transitions`);
107
+ const { nodesCreated, edgesCreated } = await persistFlow(supabase, flowId, extraction);
108
+ await markFlowSuccess(supabase, flowId, extraction.summary);
109
+ succeeded = true;
110
+ logSuccess(`Screen flow generated: ${nodesCreated} screens, ${edgesCreated} transitions`);
111
+ return {
112
+ status: 'success',
113
+ message: `Screen flow generated (${nodesCreated} screens, ${edgesCreated} transitions)`,
114
+ nodesCreated,
115
+ edgesCreated,
116
+ summary: extraction.summary,
117
+ };
118
+ }
119
+ catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ logError(`Screen flow failed: ${errorMessage}`);
122
+ await markFlowFailed(supabase, flowId, errorMessage);
123
+ return { status: 'error', message: errorMessage };
124
+ }
125
+ finally {
126
+ if (succeeded) {
127
+ cleanupIssueRepo(repoPath);
128
+ }
129
+ }
130
+ }
131
+ // Per-message handler — extracted out of the SDK loop to keep
132
+ // runScreenFlowPhase under the eslint complexity ceiling.
133
+ //
134
+ function processSdkMessage(
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ message, assistantBuffer, captureState, verbose) {
137
+ if (message.type === 'assistant') {
138
+ const next = assistantBuffer +
139
+ extractTextFromContent(message.message?.content ?? [], verbose);
140
+ return { assistantBuffer: next, extraction: null };
141
+ }
142
+ if (message.type === 'user' && verbose) {
143
+ // Surface tool_result blocks (incl. submit_screen_flow validation
144
+ // errors) so verbose mode shows the round-trip.
145
+ const userContent = message.message?.content;
146
+ if (Array.isArray(userContent)) {
147
+ extractTextFromContent(userContent, verbose);
148
+ }
149
+ return { assistantBuffer, extraction: null };
150
+ }
151
+ if (message.type !== 'result') {
152
+ return { assistantBuffer, extraction: null };
153
+ }
154
+ if (captureState.captured) {
155
+ return { assistantBuffer, extraction: captureState.captured };
156
+ }
157
+ const fallback = tryFallbackParse(message, assistantBuffer);
158
+ if (fallback) {
159
+ logWarning('Agent emitted a fenced screen_flow block instead of calling submit_screen_flow; using the parsed text as a fallback.');
160
+ return { assistantBuffer, extraction: fallback };
161
+ }
162
+ if (message.subtype !== 'success') {
163
+ logError(`Extraction incomplete: ${message.subtype}`);
164
+ }
165
+ return { assistantBuffer, extraction: null };
166
+ }
167
+ // Fallback parser: extract a screen_flow JSON block from the final assistant
168
+ // text if the agent skipped the submit_screen_flow tool call.
169
+ function tryFallbackParse(resultMessage, assistantText) {
170
+ const responseText = resultMessage.subtype === 'success'
171
+ ? resultMessage.result || assistantText
172
+ : assistantText;
173
+ const parsed = tryExtractResult(responseText, 'screen_flow');
174
+ if (!isScreenFlowExtraction(parsed)) {
175
+ return null;
176
+ }
177
+ const { error } = validateConsistency(parsed);
178
+ if (error) {
179
+ logWarning(`Fallback extraction failed consistency check: ${error}`);
180
+ return null;
181
+ }
182
+ return parsed;
183
+ }
184
+ // ============================================================================
185
+ // Persistence
186
+ // ============================================================================
187
+ async function markFlowRunning(supabase, flowId) {
188
+ const { error } = await supabase
189
+ .from('screen_flows')
190
+ .update({ status: 'running', error: null })
191
+ .eq('id', flowId);
192
+ if (error) {
193
+ logWarning(`Could not mark flow as running: ${error.message}`);
194
+ }
195
+ }
196
+ async function markFlowFailed(supabase, flowId, errorMessage) {
197
+ await supabase
198
+ .from('screen_flows')
199
+ .update({
200
+ status: 'failed',
201
+ error: errorMessage,
202
+ completed_at: new Date().toISOString(),
203
+ })
204
+ .eq('id', flowId);
205
+ }
206
+ async function persistTheme(supabase, flowId, theme) {
207
+ const { error } = await supabase
208
+ .from('screen_flows')
209
+ .update({ theme })
210
+ .eq('id', flowId);
211
+ if (error) {
212
+ logWarning(`Could not persist extracted theme: ${error.message}`);
213
+ }
214
+ }
215
+ async function markFlowSuccess(supabase, flowId, summary) {
216
+ await supabase
217
+ .from('screen_flows')
218
+ .update({
219
+ status: 'success',
220
+ summary,
221
+ error: null,
222
+ completed_at: new Date().toISOString(),
223
+ })
224
+ .eq('id', flowId);
225
+ }
226
+ async function persistFlow(supabase, flowId, extraction) {
227
+ // Re-runs replace prior content for the same flow row.
228
+ await supabase.from('screen_flow_edges').delete().eq('flow_id', flowId);
229
+ await supabase.from('screen_flow_nodes').delete().eq('flow_id', flowId);
230
+ if (extraction.nodes.length === 0) {
231
+ return { nodesCreated: 0, edgesCreated: 0 };
232
+ }
233
+ const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(flowId, n, i));
234
+ const { data: insertedNodes, error: nodesError } = await supabase
235
+ .from('screen_flow_nodes')
236
+ .insert(nodeRows)
237
+ .select('id, slug');
238
+ if (nodesError) {
239
+ throw new Error(`Failed to insert nodes: ${nodesError.message}`);
240
+ }
241
+ const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
242
+ const edgeRows = extraction.edges
243
+ .map((e) => buildEdgeRow(flowId, e, slugToId))
244
+ .filter((e) => e !== null);
245
+ if (edgeRows.length > 0) {
246
+ const { error: edgesError } = await supabase
247
+ .from('screen_flow_edges')
248
+ .insert(edgeRows);
249
+ if (edgesError) {
250
+ throw new Error(`Failed to insert edges: ${edgesError.message}`);
251
+ }
252
+ }
253
+ return {
254
+ nodesCreated: insertedNodes?.length ?? 0,
255
+ edgesCreated: edgeRows.length,
256
+ };
257
+ }
258
+ function buildNodeRow(flowId, node, index) {
259
+ return {
260
+ flow_id: flowId,
261
+ slug: node.slug,
262
+ name: node.name,
263
+ route: node.route ?? null,
264
+ file: node.file ?? null,
265
+ kind: node.kind,
266
+ schema: node,
267
+ position_x: (index % COLUMNS) * COLUMN_WIDTH,
268
+ position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
269
+ };
270
+ }
271
+ function buildEdgeRow(flowId, edge, slugToId) {
272
+ const fromId = slugToId.get(edge.fromSlug);
273
+ const toId = slugToId.get(edge.toSlug);
274
+ if (!fromId || !toId) {
275
+ return null;
276
+ }
277
+ return {
278
+ flow_id: flowId,
279
+ from_node_id: fromId,
280
+ to_node_id: toId,
281
+ trigger_label: edge.triggerLabel,
282
+ trigger_file: edge.triggerFile ?? null,
283
+ kind: edge.kind,
284
+ };
285
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * In-process MCP server exposing a single tool — `submit_screen_flow` —
3
+ * that the Claude Agent SDK session calls to return the structured
4
+ * extraction.
5
+ *
6
+ * Using a tool call instead of parsing a fenced text block lets the SDK
7
+ * enforce the schema (via Zod) and lets the agent self-correct when
8
+ * validation fails — the validation error is returned to the agent as
9
+ * the tool result and it can re-call the tool with corrected data.
10
+ *
11
+ * The capture pattern: callers pass in a `ScreenFlowCaptureState`. The
12
+ * tool handler stores the validated args on `state.captured` and the
13
+ * orchestrator reads it after the SDK loop ends. If the agent never
14
+ * calls the tool, `state.captured` stays null and the caller can fall
15
+ * back to parsing the assistant text.
16
+ */
17
+ import { z } from 'zod';
18
+ import type { ScreenFlowExtraction } from './types.js';
19
+ export interface ScreenFlowCaptureState {
20
+ captured: ScreenFlowExtraction | null;
21
+ }
22
+ export declare function createScreenFlowCaptureState(): ScreenFlowCaptureState;
23
+ /** Optional sink for streaming progress messages from the agent. */
24
+ export type ScreenFlowProgressSink = (event: {
25
+ phase: 'detection' | 'routing' | 'screens' | 'transitions' | 'submission';
26
+ message: string;
27
+ }) => void;
28
+ export declare function validateConsistency(extraction: ScreenFlowExtraction): {
29
+ error: string | null;
30
+ };
31
+ /**
32
+ * Build the `submit_screen_flow` tool. Exported separately from the server
33
+ * so tests can exercise the handler directly without going through the
34
+ * MCP transport.
35
+ */
36
+ export declare function createSubmitScreenFlowTool(state: ScreenFlowCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
37
+ summary: z.ZodString;
38
+ nodes: z.ZodArray<z.ZodObject<{
39
+ slug: z.ZodString;
40
+ name: z.ZodString;
41
+ route: z.ZodOptional<z.ZodString>;
42
+ file: z.ZodOptional<z.ZodString>;
43
+ kind: z.ZodEnum<{
44
+ page: "page";
45
+ modal: "modal";
46
+ drawer: "drawer";
47
+ tab: "tab";
48
+ state: "state";
49
+ }>;
50
+ layout: z.ZodEnum<{
51
+ split: "split";
52
+ centered: "centered";
53
+ sidebar: "sidebar";
54
+ "list-detail": "list-detail";
55
+ tabs: "tabs";
56
+ stacked: "stacked";
57
+ }>;
58
+ header: z.ZodOptional<z.ZodObject<{
59
+ title: z.ZodString;
60
+ subtitle: z.ZodOptional<z.ZodString>;
61
+ back: z.ZodOptional<z.ZodBoolean>;
62
+ actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
63
+ label: z.ZodString;
64
+ variant: z.ZodOptional<z.ZodEnum<{
65
+ primary: "primary";
66
+ secondary: "secondary";
67
+ ghost: "ghost";
68
+ destructive: "destructive";
69
+ }>>;
70
+ icon: z.ZodOptional<z.ZodString>;
71
+ }, z.core.$strip>>>;
72
+ }, z.core.$strip>>;
73
+ body: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
74
+ type: z.ZodLiteral<"form">;
75
+ fields: z.ZodArray<z.ZodObject<{
76
+ label: z.ZodString;
77
+ kind: z.ZodEnum<{
78
+ number: "number";
79
+ text: "text";
80
+ date: "date";
81
+ select: "select";
82
+ email: "email";
83
+ password: "password";
84
+ textarea: "textarea";
85
+ checkbox: "checkbox";
86
+ }>;
87
+ placeholder: z.ZodOptional<z.ZodString>;
88
+ value: z.ZodOptional<z.ZodString>;
89
+ required: z.ZodOptional<z.ZodBoolean>;
90
+ }, z.core.$strip>>;
91
+ submitLabel: z.ZodString;
92
+ secondaryLabel: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strip>, z.ZodObject<{
94
+ type: z.ZodLiteral<"list">;
95
+ items: z.ZodArray<z.ZodObject<{
96
+ title: z.ZodString;
97
+ subtitle: z.ZodOptional<z.ZodString>;
98
+ meta: z.ZodOptional<z.ZodString>;
99
+ icon: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strip>>;
101
+ emptyMessage: z.ZodOptional<z.ZodString>;
102
+ }, z.core.$strip>, z.ZodObject<{
103
+ type: z.ZodLiteral<"card-grid">;
104
+ cards: z.ZodArray<z.ZodObject<{
105
+ title: z.ZodString;
106
+ subtitle: z.ZodOptional<z.ZodString>;
107
+ meta: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strip>>;
109
+ columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
110
+ }, z.core.$strip>, z.ZodObject<{
111
+ type: z.ZodLiteral<"table">;
112
+ columns: z.ZodArray<z.ZodString>;
113
+ rows: z.ZodArray<z.ZodArray<z.ZodString>>;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ type: z.ZodLiteral<"kanban">;
116
+ columns: z.ZodArray<z.ZodObject<{
117
+ title: z.ZodString;
118
+ cards: z.ZodArray<z.ZodObject<{
119
+ title: z.ZodString;
120
+ meta: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
122
+ }, z.core.$strip>>;
123
+ }, z.core.$strip>, z.ZodObject<{
124
+ type: z.ZodLiteral<"text">;
125
+ content: z.ZodString;
126
+ }, z.core.$strip>, z.ZodObject<{
127
+ type: z.ZodLiteral<"image">;
128
+ alt: z.ZodString;
129
+ aspect: z.ZodOptional<z.ZodEnum<{
130
+ video: "video";
131
+ square: "square";
132
+ wide: "wide";
133
+ }>>;
134
+ }, z.core.$strip>, z.ZodObject<{
135
+ type: z.ZodLiteral<"chart">;
136
+ chartKind: z.ZodEnum<{
137
+ line: "line";
138
+ bar: "bar";
139
+ pie: "pie";
140
+ }>;
141
+ label: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>, z.ZodObject<{
143
+ type: z.ZodLiteral<"stats">;
144
+ items: z.ZodArray<z.ZodObject<{
145
+ label: z.ZodString;
146
+ value: z.ZodString;
147
+ delta: z.ZodOptional<z.ZodString>;
148
+ }, z.core.$strip>>;
149
+ }, z.core.$strip>, z.ZodObject<{
150
+ type: z.ZodLiteral<"empty-state">;
151
+ title: z.ZodString;
152
+ message: z.ZodOptional<z.ZodString>;
153
+ cta: z.ZodOptional<z.ZodString>;
154
+ }, z.core.$strip>, z.ZodObject<{
155
+ type: z.ZodLiteral<"tabs">;
156
+ tabs: z.ZodArray<z.ZodString>;
157
+ activeIndex: z.ZodOptional<z.ZodNumber>;
158
+ }, z.core.$strip>, z.ZodObject<{
159
+ type: z.ZodLiteral<"custom">;
160
+ label: z.ZodString;
161
+ }, z.core.$strip>], "type">>;
162
+ }, z.core.$strip>>;
163
+ edges: z.ZodArray<z.ZodObject<{
164
+ fromSlug: z.ZodString;
165
+ toSlug: z.ZodString;
166
+ triggerLabel: z.ZodString;
167
+ triggerFile: z.ZodOptional<z.ZodString>;
168
+ kind: z.ZodEnum<{
169
+ modal: "modal";
170
+ navigate: "navigate";
171
+ redirect: "redirect";
172
+ back: "back";
173
+ }>;
174
+ }, z.core.$strip>>;
175
+ }>;
176
+ /**
177
+ * Build the `record_progress` tool. A side-channel that lets the agent
178
+ * push a human-readable status message to the CLI / desktop UI so a
179
+ * multi-minute extraction doesn't look frozen between text emissions.
180
+ * Returning `{ ok: true }` keeps it cheap — it has no semantic effect
181
+ * on the extraction.
182
+ */
183
+ export declare function createRecordProgressTool(sink?: ScreenFlowProgressSink): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
184
+ phase: z.ZodEnum<{
185
+ detection: "detection";
186
+ routing: "routing";
187
+ screens: "screens";
188
+ transitions: "transitions";
189
+ submission: "submission";
190
+ }>;
191
+ message: z.ZodString;
192
+ }>;
193
+ export declare function createScreenFlowMcpServer(state: ScreenFlowCaptureState, options?: {
194
+ onProgress?: ScreenFlowProgressSink;
195
+ }): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;