edsger 0.56.3 → 0.57.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 (77) 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 +129 -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/product-test-cases/index.d.ts +25 -0
  43. package/dist/phases/product-test-cases/index.js +174 -0
  44. package/dist/phases/product-test-cases/prompts.d.ts +24 -0
  45. package/dist/phases/product-test-cases/prompts.js +80 -0
  46. package/dist/phases/product-test-cases/types.d.ts +17 -0
  47. package/dist/phases/product-test-cases/types.js +27 -0
  48. package/dist/phases/screen-flow/index.d.ts +23 -0
  49. package/dist/phases/screen-flow/index.js +229 -0
  50. package/dist/phases/screen-flow/prompts.d.ts +19 -0
  51. package/dist/phases/screen-flow/prompts.js +39 -0
  52. package/dist/phases/screen-flow/theme.d.ts +19 -0
  53. package/dist/phases/screen-flow/theme.js +182 -0
  54. package/dist/phases/screen-flow/types.d.ts +130 -0
  55. package/dist/phases/screen-flow/types.js +66 -0
  56. package/dist/phases/user-psychology/agent.d.ts +16 -0
  57. package/dist/phases/user-psychology/agent.js +105 -0
  58. package/dist/phases/user-psychology/context.d.ts +10 -0
  59. package/dist/phases/user-psychology/context.js +65 -0
  60. package/dist/phases/user-psychology/index.d.ts +18 -0
  61. package/dist/phases/user-psychology/index.js +96 -0
  62. package/dist/phases/user-psychology/prompts.d.ts +2 -0
  63. package/dist/phases/user-psychology/prompts.js +41 -0
  64. package/dist/services/audit-logs.js +67 -9
  65. package/dist/services/branches.js +90 -14
  66. package/dist/services/phase-ratings.js +71 -9
  67. package/dist/services/product-logs.js +65 -5
  68. package/dist/services/pull-requests.js +74 -14
  69. package/dist/skills/phase/screen-flow/SKILL.md +78 -0
  70. package/dist/skills/phase/user-psychology/SKILL.md +135 -0
  71. package/dist/supabase/client.d.ts +23 -0
  72. package/dist/supabase/client.js +90 -0
  73. package/dist/system/session-manager.js +97 -24
  74. package/dist/types/index.d.ts +3 -0
  75. package/dist/utils/logger.js +24 -4
  76. package/package.json +4 -3
  77. package/vitest.config.ts +1 -0
@@ -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,229 @@
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 { createScreenFlowSystemPrompt, createScreenFlowUserPrompt, } from './prompts.js';
19
+ import { extractTheme } from './theme.js';
20
+ import { isScreenFlowExtraction, } from './types.js';
21
+ const WORKSPACE_KEY = 'screen-flow';
22
+ const MAX_TURNS = 150;
23
+ // Auto-layout: simple grid; users can drag afterwards and we persist positions.
24
+ const COLUMN_WIDTH = 380;
25
+ const ROW_HEIGHT = 480;
26
+ const COLUMNS = 4;
27
+ export async function runScreenFlowPhase(options) {
28
+ const { productId, flowId, guidance, verbose } = options;
29
+ logInfo(`Starting screen-flow generation for product ${productId}`);
30
+ const supabase = getSupabase();
31
+ await markFlowRunning(supabase, flowId);
32
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
33
+ if (!githubConfig.configured ||
34
+ !githubConfig.token ||
35
+ !githubConfig.owner ||
36
+ !githubConfig.repo) {
37
+ const msg = githubConfig.message ||
38
+ 'GitHub repository not configured for this product. Connect a repo first.';
39
+ await markFlowFailed(supabase, flowId, msg);
40
+ return { status: 'error', message: msg };
41
+ }
42
+ let repoPath;
43
+ let succeeded = false;
44
+ try {
45
+ const workspaceRoot = ensureWorkspaceDir();
46
+ const repoKey = `${WORKSPACE_KEY}-${productId}`;
47
+ ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, githubConfig.owner, githubConfig.repo, githubConfig.token));
48
+ const product = await fetchProductBasics(productId);
49
+ const theme = extractTheme(repoPath);
50
+ if (Object.keys(theme).length > 0) {
51
+ logInfo(`Extracted theme: ${Object.entries(theme).map(([k, v]) => `${k}=${v}`).join(', ')}`);
52
+ await persistTheme(supabase, flowId, theme);
53
+ }
54
+ const systemPrompt = await createScreenFlowSystemPrompt({
55
+ projectDir: repoPath,
56
+ hasCodebase: true,
57
+ });
58
+ const userPrompt = createScreenFlowUserPrompt({
59
+ productName: product.name,
60
+ productDescription: product.description,
61
+ guidance,
62
+ });
63
+ logInfo('Running Claude screen-flow extraction...');
64
+ let lastAssistantResponse = '';
65
+ let extraction = null;
66
+ for await (const message of query({
67
+ prompt: createPromptGenerator(userPrompt),
68
+ options: {
69
+ systemPrompt: {
70
+ type: 'preset',
71
+ preset: 'claude_code',
72
+ append: systemPrompt,
73
+ },
74
+ model: DEFAULT_MODEL,
75
+ maxTurns: MAX_TURNS,
76
+ permissionMode: 'bypassPermissions',
77
+ cwd: repoPath,
78
+ },
79
+ })) {
80
+ if (message.type === 'assistant') {
81
+ lastAssistantResponse += extractTextFromContent(message.message?.content ?? [], verbose);
82
+ continue;
83
+ }
84
+ if (message.type !== 'result') {
85
+ continue;
86
+ }
87
+ const responseText = message.subtype === 'success'
88
+ ? message.result || lastAssistantResponse
89
+ : lastAssistantResponse;
90
+ const parsed = tryExtractResult(responseText, 'screen_flow');
91
+ if (isScreenFlowExtraction(parsed)) {
92
+ extraction = parsed;
93
+ }
94
+ else if (message.subtype !== 'success') {
95
+ logError(`Extraction incomplete: ${message.subtype}`);
96
+ }
97
+ }
98
+ if (!extraction) {
99
+ const msg = 'Screen flow extraction failed: could not parse a screen_flow result from the agent';
100
+ await markFlowFailed(supabase, flowId, msg);
101
+ return { status: 'error', message: msg };
102
+ }
103
+ logInfo(`Extraction produced ${extraction.nodes.length} screens / ${extraction.edges.length} transitions`);
104
+ const { nodesCreated, edgesCreated } = await persistFlow(supabase, flowId, extraction);
105
+ await markFlowSuccess(supabase, flowId, extraction.summary);
106
+ succeeded = true;
107
+ logSuccess(`Screen flow generated: ${nodesCreated} screens, ${edgesCreated} transitions`);
108
+ return {
109
+ status: 'success',
110
+ message: `Screen flow generated (${nodesCreated} screens, ${edgesCreated} transitions)`,
111
+ nodesCreated,
112
+ edgesCreated,
113
+ summary: extraction.summary,
114
+ };
115
+ }
116
+ catch (error) {
117
+ const errorMessage = error instanceof Error ? error.message : String(error);
118
+ logError(`Screen flow failed: ${errorMessage}`);
119
+ await markFlowFailed(supabase, flowId, errorMessage);
120
+ return { status: 'error', message: errorMessage };
121
+ }
122
+ finally {
123
+ if (succeeded) {
124
+ cleanupIssueRepo(repoPath);
125
+ }
126
+ }
127
+ }
128
+ // ============================================================================
129
+ // Persistence
130
+ // ============================================================================
131
+ async function markFlowRunning(supabase, flowId) {
132
+ const { error } = await supabase
133
+ .from('screen_flows')
134
+ .update({ status: 'running', error: null })
135
+ .eq('id', flowId);
136
+ if (error) {
137
+ logWarning(`Could not mark flow as running: ${error.message}`);
138
+ }
139
+ }
140
+ async function markFlowFailed(supabase, flowId, errorMessage) {
141
+ await supabase
142
+ .from('screen_flows')
143
+ .update({
144
+ status: 'failed',
145
+ error: errorMessage,
146
+ completed_at: new Date().toISOString(),
147
+ })
148
+ .eq('id', flowId);
149
+ }
150
+ async function persistTheme(supabase, flowId, theme) {
151
+ const { error } = await supabase
152
+ .from('screen_flows')
153
+ .update({ theme })
154
+ .eq('id', flowId);
155
+ if (error) {
156
+ logWarning(`Could not persist extracted theme: ${error.message}`);
157
+ }
158
+ }
159
+ async function markFlowSuccess(supabase, flowId, summary) {
160
+ await supabase
161
+ .from('screen_flows')
162
+ .update({
163
+ status: 'success',
164
+ summary,
165
+ error: null,
166
+ completed_at: new Date().toISOString(),
167
+ })
168
+ .eq('id', flowId);
169
+ }
170
+ async function persistFlow(supabase, flowId, extraction) {
171
+ // Re-runs replace prior content for the same flow row.
172
+ await supabase.from('screen_flow_edges').delete().eq('flow_id', flowId);
173
+ await supabase.from('screen_flow_nodes').delete().eq('flow_id', flowId);
174
+ if (extraction.nodes.length === 0) {
175
+ return { nodesCreated: 0, edgesCreated: 0 };
176
+ }
177
+ const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(flowId, n, i));
178
+ const { data: insertedNodes, error: nodesError } = await supabase
179
+ .from('screen_flow_nodes')
180
+ .insert(nodeRows)
181
+ .select('id, slug');
182
+ if (nodesError) {
183
+ throw new Error(`Failed to insert nodes: ${nodesError.message}`);
184
+ }
185
+ const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
186
+ const edgeRows = extraction.edges
187
+ .map((e) => buildEdgeRow(flowId, e, slugToId))
188
+ .filter((e) => e !== null);
189
+ if (edgeRows.length > 0) {
190
+ const { error: edgesError } = await supabase
191
+ .from('screen_flow_edges')
192
+ .insert(edgeRows);
193
+ if (edgesError) {
194
+ throw new Error(`Failed to insert edges: ${edgesError.message}`);
195
+ }
196
+ }
197
+ return {
198
+ nodesCreated: insertedNodes?.length ?? 0,
199
+ edgesCreated: edgeRows.length,
200
+ };
201
+ }
202
+ function buildNodeRow(flowId, node, index) {
203
+ return {
204
+ flow_id: flowId,
205
+ slug: node.slug,
206
+ name: node.name,
207
+ route: node.route ?? null,
208
+ file: node.file ?? null,
209
+ kind: node.kind,
210
+ schema: node,
211
+ position_x: (index % COLUMNS) * COLUMN_WIDTH,
212
+ position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
213
+ };
214
+ }
215
+ function buildEdgeRow(flowId, edge, slugToId) {
216
+ const fromId = slugToId.get(edge.fromSlug);
217
+ const toId = slugToId.get(edge.toSlug);
218
+ if (!fromId || !toId) {
219
+ return null;
220
+ }
221
+ return {
222
+ flow_id: flowId,
223
+ from_node_id: fromId,
224
+ to_node_id: toId,
225
+ trigger_label: edge.triggerLabel,
226
+ trigger_file: edge.triggerFile ?? null,
227
+ kind: edge.kind,
228
+ };
229
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prompts for the screen-flow phase.
3
+ *
4
+ * The system prompt body lives in the edsger-skills package
5
+ * (`skills/phase/screen-flow/SKILL.md`) so it can be project-overridden via
6
+ * `.claude/skills/edsger/phase/screen-flow/SKILL.md`, edited as plain MD
7
+ * without TS recompilation, and shared with the Claude Code plugin chain.
8
+ * The JSON output contract is appended from `output-contracts.ts` and is NOT
9
+ * user-overridable — the persistence layer relies on its exact shape.
10
+ */
11
+ export declare function createScreenFlowSystemPrompt(options?: {
12
+ projectDir?: string;
13
+ hasCodebase?: boolean;
14
+ }): Promise<string>;
15
+ export declare function createScreenFlowUserPrompt(args: {
16
+ productName: string;
17
+ productDescription?: string;
18
+ guidance?: string;
19
+ }): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Prompts for the screen-flow phase.
3
+ *
4
+ * The system prompt body lives in the edsger-skills package
5
+ * (`skills/phase/screen-flow/SKILL.md`) so it can be project-overridden via
6
+ * `.claude/skills/edsger/phase/screen-flow/SKILL.md`, edited as plain MD
7
+ * without TS recompilation, and shared with the Claude Code plugin chain.
8
+ * The JSON output contract is appended from `output-contracts.ts` and is NOT
9
+ * user-overridable — the persistence layer relies on its exact shape.
10
+ */
11
+ import { processConditionals, resolveSkill, } from '../../services/skill-resolver.js';
12
+ import { OUTPUT_CONTRACTS } from '../output-contracts.js';
13
+ export async function createScreenFlowSystemPrompt(options) {
14
+ const skill = await resolveSkill('phase/screen-flow', {
15
+ projectDir: options?.projectDir,
16
+ });
17
+ if (!skill) {
18
+ throw new Error('Failed to load skill: phase/screen-flow');
19
+ }
20
+ const prompt = processConditionals(skill.prompt, {
21
+ hasCodebase: options?.hasCodebase ?? true,
22
+ });
23
+ return `${prompt}
24
+
25
+ ${OUTPUT_CONTRACTS['screen-flow']}`;
26
+ }
27
+ export function createScreenFlowUserPrompt(args) {
28
+ const guidanceBlock = args.guidance
29
+ ? `\n\n**Human guidance for this run** (focus or exclude as instructed):\n${args.guidance}`
30
+ : '';
31
+ const descBlock = args.productDescription
32
+ ? `\n**Product description**: ${args.productDescription}`
33
+ : '';
34
+ return `Map the user-facing screen flow for **${args.productName}**.${descBlock}${guidanceBlock}
35
+
36
+ Start by detecting the framework (check package.json / pubspec.yaml / Package.swift), then locate the router definition or pages directory. Read just enough source per screen to fill in a useful ScreenSchema — do not need to read everything.
37
+
38
+ When done, emit the single \`screen_flow\` JSON block.`;
39
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Theme extraction for screen-flow renders.
3
+ *
4
+ * The repo is already cloned on disk when this runs; we do best-effort
5
+ * static parsing of Tailwind config (and a couple of common alternatives)
6
+ * to pull out a primary color, a neutral color, a radius, and a font.
7
+ * Misses are fine — the renderer falls back to its own defaults.
8
+ *
9
+ * Intentionally simple: regex over the source text. We're not building an
10
+ * AST evaluator. If the user has anything exotic, they can edit the theme
11
+ * later from the desktop UI.
12
+ */
13
+ export interface ScreenFlowTheme {
14
+ primary?: string;
15
+ neutral?: string;
16
+ radius?: string;
17
+ font?: string;
18
+ }
19
+ export declare function extractTheme(repoPath: string): ScreenFlowTheme;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Theme extraction for screen-flow renders.
3
+ *
4
+ * The repo is already cloned on disk when this runs; we do best-effort
5
+ * static parsing of Tailwind config (and a couple of common alternatives)
6
+ * to pull out a primary color, a neutral color, a radius, and a font.
7
+ * Misses are fine — the renderer falls back to its own defaults.
8
+ *
9
+ * Intentionally simple: regex over the source text. We're not building an
10
+ * AST evaluator. If the user has anything exotic, they can edit the theme
11
+ * later from the desktop UI.
12
+ */
13
+ import { existsSync, readFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ const TAILWIND_CONFIG_CANDIDATES = [
16
+ 'tailwind.config.ts',
17
+ 'tailwind.config.js',
18
+ 'tailwind.config.cjs',
19
+ 'tailwind.config.mjs',
20
+ ];
21
+ // Common alternatives a project might use for a "tokens" file.
22
+ const TOKENS_CANDIDATES = [
23
+ 'src/theme.json',
24
+ 'theme.json',
25
+ 'src/design-tokens.json',
26
+ ];
27
+ const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
28
+ const RGB_RE = /rgba?\([^)]+\)/g;
29
+ const HSL_RE = /hsla?\([^)]+\)/g;
30
+ const OKLCH_RE = /oklch\([^)]+\)/g;
31
+ export function extractTheme(repoPath) {
32
+ const theme = {};
33
+ // Tailwind config — pull primary/neutral colors out of the colors block
34
+ for (const candidate of TAILWIND_CONFIG_CANDIDATES) {
35
+ const fullPath = join(repoPath, candidate);
36
+ if (!existsSync(fullPath)) {
37
+ continue;
38
+ }
39
+ try {
40
+ const source = readFileSync(fullPath, 'utf-8');
41
+ const fromTw = parseTailwindColors(source);
42
+ Object.assign(theme, stripUndefined(fromTw));
43
+ break;
44
+ }
45
+ catch {
46
+ // Best-effort
47
+ }
48
+ }
49
+ // Common project-side tokens
50
+ for (const candidate of TOKENS_CANDIDATES) {
51
+ const fullPath = join(repoPath, candidate);
52
+ if (!existsSync(fullPath)) {
53
+ continue;
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(readFileSync(fullPath, 'utf-8'));
57
+ const fromTokens = parseTokensJson(parsed);
58
+ Object.assign(theme, stripUndefined(fromTokens));
59
+ }
60
+ catch {
61
+ // Skip
62
+ }
63
+ }
64
+ // CSS variables in a global stylesheet — last-ditch fallback
65
+ const globalCssCandidates = [
66
+ 'src/index.css',
67
+ 'src/styles/globals.css',
68
+ 'src/app/globals.css',
69
+ 'app/globals.css',
70
+ 'styles/globals.css',
71
+ ];
72
+ if (!theme.primary || !theme.radius || !theme.font) {
73
+ for (const candidate of globalCssCandidates) {
74
+ const fullPath = join(repoPath, candidate);
75
+ if (!existsSync(fullPath)) {
76
+ continue;
77
+ }
78
+ try {
79
+ const source = readFileSync(fullPath, 'utf-8');
80
+ const fromCss = parseGlobalCss(source);
81
+ Object.assign(theme, stripUndefined({ ...fromCss, ...theme }));
82
+ break;
83
+ }
84
+ catch {
85
+ // Skip
86
+ }
87
+ }
88
+ }
89
+ return theme;
90
+ }
91
+ function parseTailwindColors(source) {
92
+ const theme = {};
93
+ // Look for `primary:` and `neutral:` keys in any nested object. Accept
94
+ // either a single string ("primary: '#ff0066'") or an object containing
95
+ // a 500 key (the Tailwind convention).
96
+ const primaryString = matchColorEntry(source, 'primary');
97
+ if (primaryString)
98
+ theme.primary = primaryString;
99
+ const neutralString = matchColorEntry(source, 'neutral');
100
+ if (neutralString)
101
+ theme.neutral = neutralString;
102
+ // Pull a radius default if defined under theme.borderRadius
103
+ const radiusMatch = source.match(/borderRadius\s*:\s*{[^}]*?(?:DEFAULT|md|lg)\s*:\s*['"]([^'"]+)['"]/);
104
+ if (radiusMatch)
105
+ theme.radius = radiusMatch[1];
106
+ // Pull the default sans font family
107
+ const fontMatch = source.match(/fontFamily\s*:\s*{[^}]*?sans\s*:\s*\[?\s*['"]([^'"]+)['"]/);
108
+ if (fontMatch)
109
+ theme.font = fontMatch[1];
110
+ return theme;
111
+ }
112
+ function matchColorEntry(source, key) {
113
+ // Match `<key>: '#abc'` or `<key>: "rgb(…)" / hsl(…) / oklch(…)`
114
+ const stringRe = new RegExp(`${key}\\s*:\\s*['\"\`](#[0-9a-fA-F]{3,8}|rgba?\\([^)]+\\)|hsla?\\([^)]+\\)|oklch\\([^)]+\\))['\"\`]`);
115
+ const strMatch = source.match(stringRe);
116
+ if (strMatch) {
117
+ return strMatch[1];
118
+ }
119
+ // Match nested `<key>: { ... 500: '#abc' ... }`
120
+ const nestedRe = new RegExp(`${key}\\s*:\\s*\\{[^}]*?500\\s*:\\s*['\"\`]([^'\"\`]+)['\"\`]`);
121
+ const nested = source.match(nestedRe);
122
+ if (nested) {
123
+ const value = nested[1];
124
+ if (isColorish(value)) {
125
+ return value;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+ function parseTokensJson(json) {
131
+ const theme = {};
132
+ const colors = (json.colors ?? json.color);
133
+ if (colors && typeof colors === 'object') {
134
+ const primary = colors.primary;
135
+ if (typeof primary === 'string')
136
+ theme.primary = primary;
137
+ else if (primary && typeof primary === 'object' && '500' in primary) {
138
+ theme.primary = primary['500'];
139
+ }
140
+ const neutral = colors.neutral;
141
+ if (typeof neutral === 'string')
142
+ theme.neutral = neutral;
143
+ else if (neutral && typeof neutral === 'object' && '500' in neutral) {
144
+ theme.neutral = neutral['500'];
145
+ }
146
+ }
147
+ if (typeof json.radius === 'string')
148
+ theme.radius = json.radius;
149
+ if (typeof json.font === 'string')
150
+ theme.font = json.font;
151
+ return theme;
152
+ }
153
+ function parseGlobalCss(source) {
154
+ const theme = {};
155
+ const primaryMatch = source.match(/--primary\s*:\s*([^;]+);/);
156
+ if (primaryMatch) {
157
+ const value = primaryMatch[1].trim();
158
+ if (isColorish(value))
159
+ theme.primary = value;
160
+ else
161
+ theme.primary = `hsl(${value})`; // common shadcn pattern
162
+ }
163
+ const radiusMatch = source.match(/--radius\s*:\s*([^;]+);/);
164
+ if (radiusMatch)
165
+ theme.radius = radiusMatch[1].trim();
166
+ return theme;
167
+ }
168
+ function isColorish(value) {
169
+ return (HEX_RE.test(value) ||
170
+ RGB_RE.test(value) ||
171
+ HSL_RE.test(value) ||
172
+ OKLCH_RE.test(value));
173
+ }
174
+ function stripUndefined(obj) {
175
+ const out = {};
176
+ for (const k of Object.keys(obj)) {
177
+ if (obj[k] !== undefined && obj[k] !== '') {
178
+ out[k] = obj[k];
179
+ }
180
+ }
181
+ return out;
182
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Screen Flow domain types.
3
+ *
4
+ * A ScreenSchema is a structured, framework-agnostic description of one
5
+ * screen in a product. The CLI extracts these from source code and the
6
+ * desktop renders them with a unified <ScreenPreview> component — that's
7
+ * what gives every flow a consistent visual style regardless of the
8
+ * underlying app's design system.
9
+ *
10
+ * The schema deliberately stays high-level (sections, not pixels). When the
11
+ * agent encounters something it can't model cleanly, it falls back to
12
+ * `{ type: 'custom', label }` rather than guessing.
13
+ */
14
+ export type ScreenKind = 'page' | 'modal' | 'drawer' | 'tab' | 'state';
15
+ export type ScreenLayout = 'centered' | 'sidebar' | 'split' | 'list-detail' | 'tabs' | 'stacked';
16
+ export interface ScreenAction {
17
+ label: string;
18
+ variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
19
+ icon?: string;
20
+ }
21
+ export interface ScreenHeader {
22
+ title: string;
23
+ subtitle?: string;
24
+ back?: boolean;
25
+ actions?: ScreenAction[];
26
+ }
27
+ export interface FormField {
28
+ label: string;
29
+ kind: 'text' | 'email' | 'password' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number';
30
+ placeholder?: string;
31
+ value?: string;
32
+ required?: boolean;
33
+ }
34
+ export interface ListItem {
35
+ title: string;
36
+ subtitle?: string;
37
+ meta?: string;
38
+ icon?: string;
39
+ }
40
+ export interface CardItem {
41
+ title: string;
42
+ subtitle?: string;
43
+ meta?: string;
44
+ }
45
+ export interface KanbanColumn {
46
+ title: string;
47
+ cards: {
48
+ title: string;
49
+ meta?: string;
50
+ }[];
51
+ }
52
+ export type ScreenSection = {
53
+ type: 'form';
54
+ fields: FormField[];
55
+ submitLabel: string;
56
+ secondaryLabel?: string;
57
+ } | {
58
+ type: 'list';
59
+ items: ListItem[];
60
+ emptyMessage?: string;
61
+ } | {
62
+ type: 'card-grid';
63
+ cards: CardItem[];
64
+ columns?: 2 | 3 | 4;
65
+ } | {
66
+ type: 'table';
67
+ columns: string[];
68
+ rows: string[][];
69
+ } | {
70
+ type: 'kanban';
71
+ columns: KanbanColumn[];
72
+ } | {
73
+ type: 'text';
74
+ content: string;
75
+ } | {
76
+ type: 'image';
77
+ alt: string;
78
+ aspect?: 'video' | 'square' | 'wide';
79
+ } | {
80
+ type: 'chart';
81
+ chartKind: 'line' | 'bar' | 'pie';
82
+ label?: string;
83
+ } | {
84
+ type: 'stats';
85
+ items: {
86
+ label: string;
87
+ value: string;
88
+ delta?: string;
89
+ }[];
90
+ } | {
91
+ type: 'empty-state';
92
+ title: string;
93
+ message?: string;
94
+ cta?: string;
95
+ } | {
96
+ type: 'tabs';
97
+ tabs: string[];
98
+ activeIndex?: number;
99
+ } | {
100
+ type: 'custom';
101
+ label: string;
102
+ };
103
+ export interface ScreenSchema {
104
+ /** Stable slug within the flow (e.g. 'login', 'product-detail') */
105
+ slug: string;
106
+ /** Human-readable name */
107
+ name: string;
108
+ /** URL path or navigator key; null/undefined for modals */
109
+ route?: string;
110
+ /** Source file path (jump anchor) */
111
+ file?: string;
112
+ kind: ScreenKind;
113
+ layout: ScreenLayout;
114
+ header?: ScreenHeader;
115
+ body: ScreenSection[];
116
+ }
117
+ export type ScreenEdgeKind = 'navigate' | 'modal' | 'redirect' | 'back';
118
+ export interface ScreenEdge {
119
+ fromSlug: string;
120
+ toSlug: string;
121
+ triggerLabel: string;
122
+ triggerFile?: string;
123
+ kind: ScreenEdgeKind;
124
+ }
125
+ export interface ScreenFlowExtraction {
126
+ summary: string;
127
+ nodes: ScreenSchema[];
128
+ edges: ScreenEdge[];
129
+ }
130
+ export declare function isScreenFlowExtraction(value: unknown): value is ScreenFlowExtraction;