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,262 @@
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 { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
18
+ import { z } from 'zod';
19
+ export function createScreenFlowCaptureState() {
20
+ return { captured: null };
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Zod schemas (mirror types.ts — kept in sync by tests)
24
+ // ---------------------------------------------------------------------------
25
+ const formFieldSchema = z.object({
26
+ label: z.string(),
27
+ kind: z.enum([
28
+ 'text',
29
+ 'email',
30
+ 'password',
31
+ 'textarea',
32
+ 'select',
33
+ 'checkbox',
34
+ 'date',
35
+ 'number',
36
+ ]),
37
+ placeholder: z.string().optional(),
38
+ value: z.string().optional(),
39
+ required: z.boolean().optional(),
40
+ });
41
+ const listItemSchema = z.object({
42
+ title: z.string(),
43
+ subtitle: z.string().optional(),
44
+ meta: z.string().optional(),
45
+ icon: z.string().optional(),
46
+ });
47
+ const cardItemSchema = z.object({
48
+ title: z.string(),
49
+ subtitle: z.string().optional(),
50
+ meta: z.string().optional(),
51
+ });
52
+ const kanbanColumnSchema = z.object({
53
+ title: z.string(),
54
+ cards: z.array(z.object({ title: z.string(), meta: z.string().optional() })),
55
+ });
56
+ const sectionSchema = z.discriminatedUnion('type', [
57
+ z.object({
58
+ type: z.literal('form'),
59
+ fields: z.array(formFieldSchema),
60
+ submitLabel: z.string(),
61
+ secondaryLabel: z.string().optional(),
62
+ }),
63
+ z.object({
64
+ type: z.literal('list'),
65
+ items: z.array(listItemSchema),
66
+ emptyMessage: z.string().optional(),
67
+ }),
68
+ z.object({
69
+ type: z.literal('card-grid'),
70
+ cards: z.array(cardItemSchema),
71
+ columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).optional(),
72
+ }),
73
+ z.object({
74
+ type: z.literal('table'),
75
+ columns: z.array(z.string()),
76
+ rows: z.array(z.array(z.string())),
77
+ }),
78
+ z.object({
79
+ type: z.literal('kanban'),
80
+ columns: z.array(kanbanColumnSchema),
81
+ }),
82
+ z.object({
83
+ type: z.literal('text'),
84
+ content: z.string(),
85
+ }),
86
+ z.object({
87
+ type: z.literal('image'),
88
+ alt: z.string(),
89
+ aspect: z.enum(['video', 'square', 'wide']).optional(),
90
+ }),
91
+ z.object({
92
+ type: z.literal('chart'),
93
+ chartKind: z.enum(['line', 'bar', 'pie']),
94
+ label: z.string().optional(),
95
+ }),
96
+ z.object({
97
+ type: z.literal('stats'),
98
+ items: z.array(z.object({
99
+ label: z.string(),
100
+ value: z.string(),
101
+ delta: z.string().optional(),
102
+ })),
103
+ }),
104
+ z.object({
105
+ type: z.literal('empty-state'),
106
+ title: z.string(),
107
+ message: z.string().optional(),
108
+ cta: z.string().optional(),
109
+ }),
110
+ z.object({
111
+ type: z.literal('tabs'),
112
+ tabs: z.array(z.string()),
113
+ activeIndex: z.number().optional(),
114
+ }),
115
+ z.object({
116
+ type: z.literal('custom'),
117
+ label: z.string(),
118
+ }),
119
+ ]);
120
+ const screenActionSchema = z.object({
121
+ label: z.string(),
122
+ variant: z.enum(['primary', 'secondary', 'ghost', 'destructive']).optional(),
123
+ icon: z.string().optional(),
124
+ });
125
+ const screenHeaderSchema = z.object({
126
+ title: z.string(),
127
+ subtitle: z.string().optional(),
128
+ back: z.boolean().optional(),
129
+ actions: z.array(screenActionSchema).optional(),
130
+ });
131
+ const screenNodeSchema = z.object({
132
+ slug: z.string().min(1),
133
+ name: z.string().min(1),
134
+ route: z.string().optional(),
135
+ file: z.string().optional(),
136
+ kind: z.enum(['page', 'modal', 'drawer', 'tab', 'state']),
137
+ layout: z.enum([
138
+ 'centered',
139
+ 'sidebar',
140
+ 'split',
141
+ 'list-detail',
142
+ 'tabs',
143
+ 'stacked',
144
+ ]),
145
+ header: screenHeaderSchema.optional(),
146
+ body: z.array(sectionSchema),
147
+ });
148
+ const screenEdgeSchema = z.object({
149
+ fromSlug: z.string().min(1),
150
+ toSlug: z.string().min(1),
151
+ triggerLabel: z.string(),
152
+ triggerFile: z.string().optional(),
153
+ kind: z.enum(['navigate', 'modal', 'redirect', 'back']),
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // Cross-field consistency (Zod can't express this)
157
+ // ---------------------------------------------------------------------------
158
+ export function validateConsistency(extraction) {
159
+ const slugs = new Set();
160
+ for (const node of extraction.nodes) {
161
+ if (slugs.has(node.slug)) {
162
+ return {
163
+ error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the flow. Re-call submit_screen_flow with deduplicated nodes.`,
164
+ };
165
+ }
166
+ slugs.add(node.slug);
167
+ }
168
+ for (const edge of extraction.edges) {
169
+ if (!slugs.has(edge.fromSlug)) {
170
+ return {
171
+ error: `Edge fromSlug "${edge.fromSlug}" → "${edge.toSlug}" does not match any node slug. Either add the missing node or drop the edge, then re-call submit_screen_flow.`,
172
+ };
173
+ }
174
+ if (!slugs.has(edge.toSlug)) {
175
+ return {
176
+ error: `Edge fromSlug "${edge.fromSlug}" → toSlug "${edge.toSlug}" does not match any node slug. Either add the missing node or drop the edge, then re-call submit_screen_flow.`,
177
+ };
178
+ }
179
+ }
180
+ return { error: null };
181
+ }
182
+ // ---------------------------------------------------------------------------
183
+ // Tool factory + server factory
184
+ // ---------------------------------------------------------------------------
185
+ /**
186
+ * Build the `submit_screen_flow` tool. Exported separately from the server
187
+ * so tests can exercise the handler directly without going through the
188
+ * MCP transport.
189
+ */
190
+ export function createSubmitScreenFlowTool(state) {
191
+ return tool('submit_screen_flow', [
192
+ 'Submit the final screen flow extraction. Call this EXACTLY once,',
193
+ 'when you have finished mapping every screen and transition. Pass the',
194
+ 'full structured flow as the argument. After this call succeeds, end',
195
+ 'your turn — do NOT also paste the same data as a fenced code block.',
196
+ 'If validation fails, the error message tells you what to fix; call',
197
+ 'the tool again with corrected data.',
198
+ ].join(' '), {
199
+ summary: z
200
+ .string()
201
+ .min(1)
202
+ .describe('1-3 sentence narrative of what kind of app this is and its primary user flows.'),
203
+ nodes: z
204
+ .array(screenNodeSchema)
205
+ .describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the flow.'),
206
+ edges: z
207
+ .array(screenEdgeSchema)
208
+ .describe('Transitions between screens. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
209
+ }, async (args) => {
210
+ const extraction = {
211
+ summary: args.summary,
212
+ nodes: args.nodes,
213
+ edges: args.edges,
214
+ };
215
+ const { error } = validateConsistency(extraction);
216
+ if (error) {
217
+ return {
218
+ content: [{ type: 'text', text: error }],
219
+ isError: true,
220
+ };
221
+ }
222
+ state.captured = extraction;
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: `Captured ${extraction.nodes.length} screens / ${extraction.edges.length} transitions. End your turn now.`,
228
+ },
229
+ ],
230
+ };
231
+ });
232
+ }
233
+ /**
234
+ * Build the `record_progress` tool. A side-channel that lets the agent
235
+ * push a human-readable status message to the CLI / desktop UI so a
236
+ * multi-minute extraction doesn't look frozen between text emissions.
237
+ * Returning `{ ok: true }` keeps it cheap — it has no semantic effect
238
+ * on the extraction.
239
+ */
240
+ export function createRecordProgressTool(sink) {
241
+ return tool('record_progress', 'Send a short status update to the user. Does not affect the extraction. Call it at each phase boundary (after detecting the framework, after enumerating routes, while mapping screens, when about to submit) so the user sees progress.', {
242
+ phase: z
243
+ .enum(['detection', 'routing', 'screens', 'transitions', 'submission'])
244
+ .describe('Which phase the message belongs to.'),
245
+ message: z.string().min(1).describe('Human-readable status update.'),
246
+ }, async (args) => {
247
+ sink?.({ phase: args.phase, message: args.message });
248
+ return {
249
+ content: [{ type: 'text', text: 'ok' }],
250
+ };
251
+ });
252
+ }
253
+ export function createScreenFlowMcpServer(state, options) {
254
+ return createSdkMcpServer({
255
+ name: 'screen-flow',
256
+ version: '1.0.0',
257
+ tools: [
258
+ createSubmitScreenFlowTool(state),
259
+ createRecordProgressTool(options?.onProgress),
260
+ ],
261
+ });
262
+ }
@@ -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,41 @@
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
+ Call \`mcp__screen-flow__record_progress\` at each phase boundary so the user can see your progress (otherwise the CLI looks frozen).
39
+
40
+ When you are done, return the result by **calling the \`mcp__screen-flow__submit_screen_flow\` tool exactly once** with \`summary\`, \`nodes\`, and \`edges\` as arguments. Do not paste the JSON as a fenced text block — the tool call is the deliverable. If the tool returns an error, fix the issue it describes and call the tool again.`;
41
+ }
@@ -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,193 @@
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
+ }
100
+ const neutralString = matchColorEntry(source, 'neutral');
101
+ if (neutralString) {
102
+ theme.neutral = neutralString;
103
+ }
104
+ // Pull a radius default if defined under theme.borderRadius
105
+ const radiusMatch = source.match(/borderRadius\s*:\s*{[^}]*?(?:DEFAULT|md|lg)\s*:\s*['"]([^'"]+)['"]/);
106
+ if (radiusMatch) {
107
+ theme.radius = radiusMatch[1];
108
+ }
109
+ // Pull the default sans font family
110
+ const fontMatch = source.match(/fontFamily\s*:\s*{[^}]*?sans\s*:\s*\[?\s*['"]([^'"]+)['"]/);
111
+ if (fontMatch) {
112
+ theme.font = fontMatch[1];
113
+ }
114
+ return theme;
115
+ }
116
+ function matchColorEntry(source, key) {
117
+ // Match `<key>: '#abc'` or `<key>: "rgb(…)" / hsl(…) / oklch(…)`
118
+ const stringRe = new RegExp(`${key}\\s*:\\s*['\"\`](#[0-9a-fA-F]{3,8}|rgba?\\([^)]+\\)|hsla?\\([^)]+\\)|oklch\\([^)]+\\))['\"\`]`);
119
+ const strMatch = source.match(stringRe);
120
+ if (strMatch) {
121
+ return strMatch[1];
122
+ }
123
+ // Match nested `<key>: { ... 500: '#abc' ... }`
124
+ const nestedRe = new RegExp(`${key}\\s*:\\s*\\{[^}]*?500\\s*:\\s*['\"\`]([^'\"\`]+)['\"\`]`);
125
+ const nested = source.match(nestedRe);
126
+ if (nested) {
127
+ const value = nested[1];
128
+ if (isColorish(value)) {
129
+ return value;
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+ function parseTokensJson(json) {
135
+ const theme = {};
136
+ const colors = (json.colors ?? json.color);
137
+ if (colors && typeof colors === 'object') {
138
+ const primary = colors.primary;
139
+ if (typeof primary === 'string') {
140
+ theme.primary = primary;
141
+ }
142
+ else if (primary && typeof primary === 'object' && '500' in primary) {
143
+ theme.primary = primary['500'];
144
+ }
145
+ const neutral = colors.neutral;
146
+ if (typeof neutral === 'string') {
147
+ theme.neutral = neutral;
148
+ }
149
+ else if (neutral && typeof neutral === 'object' && '500' in neutral) {
150
+ theme.neutral = neutral['500'];
151
+ }
152
+ }
153
+ if (typeof json.radius === 'string') {
154
+ theme.radius = json.radius;
155
+ }
156
+ if (typeof json.font === 'string') {
157
+ theme.font = json.font;
158
+ }
159
+ return theme;
160
+ }
161
+ function parseGlobalCss(source) {
162
+ const theme = {};
163
+ const primaryMatch = source.match(/--primary\s*:\s*([^;]+);/);
164
+ if (primaryMatch) {
165
+ const value = primaryMatch[1].trim();
166
+ if (isColorish(value)) {
167
+ theme.primary = value;
168
+ }
169
+ else {
170
+ theme.primary = `hsl(${value})`;
171
+ } // common shadcn pattern
172
+ }
173
+ const radiusMatch = source.match(/--radius\s*:\s*([^;]+);/);
174
+ if (radiusMatch) {
175
+ theme.radius = radiusMatch[1].trim();
176
+ }
177
+ return theme;
178
+ }
179
+ function isColorish(value) {
180
+ return (HEX_RE.test(value) ||
181
+ RGB_RE.test(value) ||
182
+ HSL_RE.test(value) ||
183
+ OKLCH_RE.test(value));
184
+ }
185
+ function stripUndefined(obj) {
186
+ const out = {};
187
+ for (const k of Object.keys(obj)) {
188
+ if (obj[k] !== undefined && obj[k] !== '') {
189
+ out[k] = obj[k];
190
+ }
191
+ }
192
+ return out;
193
+ }
@@ -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;