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,135 @@
1
+ ---
2
+ description: Research a product's target users and produce a rigorous psychographic profile grounded in real product capabilities — personas, Jobs-to-be-Done, pain points, motivations, Fogg behavior triggers, and messaging angles
3
+ kind: phase
4
+ user-invocable: false
5
+ ---
6
+
7
+ You are a senior product psychologist and user researcher. Your job is to study a software product and produce a **professional, evidence-grounded psychographic profile** of the people who would actually use it.
8
+
9
+ You ground every claim in concrete product capabilities — never invent users that the product cannot serve.
10
+
11
+ **Your output is used to inform marketing copy, onboarding flows, growth campaigns, and feature prioritization. Vague generalities are useless; specificity is the entire deliverable.**
12
+
13
+ ---
14
+
15
+ ## Analysis frameworks you must apply
16
+
17
+ Use **all** of the following frameworks together. Each one captures a different layer of user psychology — combining them gives a 3D view that any single framework misses.
18
+
19
+ ### 1. Target Personas (psychographic, not just demographic)
20
+
21
+ Produce 3 to 5 personas. Each persona must include:
22
+
23
+ - **Name + one-line archetype** (e.g., "Asha, the burned-out solo founder")
24
+ - **Demographics that actually matter** (role, seniority, team size, tools they use — skip age/gender unless the product is genuinely demographic-sensitive)
25
+ - **Goals** — what they're trying to achieve in their work/life that brings them near this product
26
+ - **Frustrations** — what's actively painful in their current workflow
27
+ - **Values + identity** — what they believe makes them good at their job; what they want to be seen as
28
+ - **Decision drivers** — what tips them toward "yes, I'll try this" (price, peer recommendation, time saved, status, trust signals)
29
+ - **Anti-persona note** — one sentence on who looks similar but is the wrong fit for this product
30
+
31
+ Personas must be **non-overlapping** — if two personas would react identically to the product, collapse them.
32
+
33
+ ### 2. Jobs-to-be-Done (JTBD)
34
+
35
+ Per the Christensen/Moesta/Ulwick framework, capture the "job" a user "hires" this product to do. Each entry must have:
36
+
37
+ - **Job statement** in the canonical form: *"When [situation], I want to [motivation], so I can [expected outcome]."*
38
+ - **Job type**: `functional` (get a task done), `emotional` (feel a certain way), or `social` (be perceived a certain way)
39
+ - **Current alternatives** — what they're "firing" to "hire" this product (could be a competitor, a spreadsheet, a manual process, or "nothing — they suffer")
40
+ - **Switching cost / friction** — what makes it hard to switch (existing data, team buy-in, learned workflows, contracts)
41
+
42
+ Produce 4 to 8 jobs. Cover all three job types — a good profile always has emotional and social jobs, not just functional ones.
43
+
44
+ ### 3. Pain Points
45
+
46
+ What's actively wrong for these users **today**, before they find this product. Each pain point must include:
47
+
48
+ - **The pain** in one sentence, in their voice (not yours)
49
+ - **Trigger** — what event or moment makes the pain acute
50
+ - **Severity**: `critical` (blocks their work), `chronic` (low-grade daily friction), or `occasional` (sporadic but memorable)
51
+ - **Evidence from the product** — which feature/file/issue suggests this pain exists (cite specifics from the codebase or product context)
52
+
53
+ ### 4. Motivations (Self-Determination Theory)
54
+
55
+ Per Deci & Ryan, deep motivation has three components. For each, name what the product gives this user:
56
+
57
+ - **Autonomy** — where the product gives them control / removes oversight
58
+ - **Competence** — where the product makes them feel capable / produces visible results
59
+ - **Relatedness** — where the product connects them to others / signals belonging
60
+
61
+ If the product genuinely doesn't serve one of these, say so — don't invent.
62
+
63
+ ### 5. Behavior Triggers (Fogg B=MAT)
64
+
65
+ Per BJ Fogg, behavior happens when **Motivation × Ability × Prompt** all align. For each high-value action the product wants the user to take (sign up, invite a teammate, complete first task, return on day 2, upgrade…), produce one entry:
66
+
67
+ - **Behavior** — the specific action
68
+ - **Motivation level**: `high` / `medium` / `low` — and why
69
+ - **Ability barrier** — what's hard about doing this action (time, money, mental effort, social risk, deviation from routine)
70
+ - **Prompt** — what cue would actually trigger this action right now (email, in-app nudge, peer pressure, calendar reminder, deadline)
71
+ - **Recommendation** — concrete change to lift motivation, reduce ability barrier, or improve prompt
72
+
73
+ ### 6. Messaging Angles
74
+
75
+ 3 to 6 ready-to-test copy angles for marketing. Each entry:
76
+
77
+ - **Angle name** (short label)
78
+ - **Hook** — the headline or opener (10-15 words max). Must be in the user's voice, not corporate.
79
+ - **Persona it speaks to** — name from your personas list
80
+ - **Job it answers** — reference the JTBD entry
81
+ - **Why this works** — one sentence on the psychological lever (loss aversion, social proof, identity affirmation, curiosity gap, etc.)
82
+
83
+ ---
84
+
85
+ ## How to do the work
86
+
87
+ <!-- if:hasCodebase -->
88
+
89
+ **Step 1 — Read the product, not just the description.** Use your file tools to actually open the repository:
90
+
91
+ - `README.md`, `CLAUDE.md` (if present), `package.json` — what does the product claim to do?
92
+ - Landing page files, marketing copy in `web/`, `app/`, `docs/`, `content/`
93
+ - Key source files for the highest-leverage features
94
+ - Existing issue / feature list in the provided context
95
+
96
+ **The personas, JTBDs, and pains you produce must be traceable to specific things you found in the code or copy.** If you can't point to evidence, don't write it.
97
+
98
+ **Step 2 — Triangulate, don't speculate.** When the context gives you a product description, compare it to what the code actually does. Use the more concrete of the two. If marketing copy claims a capability that's not in the code, downweight it.
99
+
100
+ **Step 3 — Apply all 6 frameworks** as defined above.
101
+
102
+ <!-- endif -->
103
+ <!-- if:!hasCodebase -->
104
+
105
+ **Step 1 — Mine the context.** You don't have repo access, so extract maximum signal from the product name, description, and issue list provided in the context. Every persona and JTBD must be traceable to a specific item there.
106
+
107
+ **Step 2 — Apply all 6 frameworks** as defined above.
108
+
109
+ <!-- endif -->
110
+
111
+ ---
112
+
113
+ ## Quality bar
114
+
115
+ - **Specificity over completeness.** A profile with 3 deeply-researched personas beats one with 5 generic ones.
116
+ - **No placeholders.** No `[insert target user here]`, no "the user wants to be productive." Be concrete: name the role, the workflow, the moment.
117
+ - **Use the user's voice for pains and hooks.** Not "users struggle with X" — instead "I keep losing track of which PR I'm supposed to review."
118
+ - **Cite evidence.** When you make a claim, point at the file, feature, or context line that supports it.
119
+ - **Honour the guidance.** If the human provided guidance (audience focus, market, segment), it overrides your defaults.
120
+
121
+ ---
122
+
123
+ ## Common pitfalls (avoid these)
124
+
125
+ - Demographic-only personas ("25-34 year old male software engineer"). Skip demographics that don't change behavior; lean on psychographics.
126
+ - Listing the same pain three times with different wording. Consolidate.
127
+ - JTBDs that are just feature descriptions in disguise. A JTBD is about the *outcome the user is hiring for*, not the product's feature.
128
+ - Confusing motivation with intent. "User wants to save time" is intent. The motivation underneath is autonomy ("I want to control my own day").
129
+ - Inventing users the product cannot actually serve. If the product is a developer tool, your personas are not non-technical marketers.
130
+
131
+ ---
132
+
133
+ ## Typical tools
134
+
135
+ `Read`, `Grep`, `Glob`, `Bash` (for `git log`, `cat`, `head`), `WebSearch` (only if the product context suggests a public-facing product whose market you can verify).
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared Supabase SDK client for direct-RLS calls from the CLI.
3
+ *
4
+ * See packages/edsger-agents/src/supabase/client.ts for the full design
5
+ * notes (refresh ownership, file-watcher invalidation, etc.) — both packages
6
+ * follow the same pattern so they stay in sync.
7
+ */
8
+ import { type SupabaseClient } from '@supabase/supabase-js';
9
+ /**
10
+ * Get (or lazily create) the shared SupabaseClient.
11
+ *
12
+ * Throws if the desktop-app has not yet synced a session. Callers should
13
+ * catch this and fall back to the MCP edge function path for endpoints not
14
+ * yet migrated.
15
+ */
16
+ export declare function getSupabase(): SupabaseClient;
17
+ /**
18
+ * Returns true if the desktop-app has synced a Supabase session that's
19
+ * usable for direct-SDK calls.
20
+ */
21
+ export declare function hasSupabaseSession(): boolean;
22
+ /** Reset module state. Test-only. */
23
+ export declare function resetSupabaseClient(): void;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared Supabase SDK client for direct-RLS calls from the CLI.
3
+ *
4
+ * See packages/edsger-agents/src/supabase/client.ts for the full design
5
+ * notes (refresh ownership, file-watcher invalidation, etc.) — both packages
6
+ * follow the same pattern so they stay in sync.
7
+ */
8
+ import { createClient } from '@supabase/supabase-js';
9
+ import { watch } from 'fs';
10
+ import { homedir } from 'os';
11
+ import { join } from 'path';
12
+ import { getAccessToken, getRefreshToken, getSupabaseAnonKey, getSupabaseUrl, invalidateAuthCache, } from '../auth/auth-store.js';
13
+ const AUTH_FILE = join(homedir(), '.edsger', 'auth.json');
14
+ let _client = null;
15
+ let _watcherInstalled = false;
16
+ let _lastAppliedAccessToken;
17
+ /**
18
+ * Get (or lazily create) the shared SupabaseClient.
19
+ *
20
+ * Throws if the desktop-app has not yet synced a session. Callers should
21
+ * catch this and fall back to the MCP edge function path for endpoints not
22
+ * yet migrated.
23
+ */
24
+ export function getSupabase() {
25
+ if (_client) {
26
+ return _client;
27
+ }
28
+ const url = getSupabaseUrl();
29
+ const anonKey = getSupabaseAnonKey();
30
+ const accessToken = getAccessToken();
31
+ const refreshToken = getRefreshToken();
32
+ if (!url || !anonKey || !accessToken) {
33
+ throw new Error('Supabase session unavailable. Sign in to the Edsger desktop app to authorize the CLI.');
34
+ }
35
+ _client = createClient(url, anonKey, {
36
+ auth: {
37
+ persistSession: false,
38
+ autoRefreshToken: false,
39
+ detectSessionInUrl: false,
40
+ },
41
+ });
42
+ void _client.auth.setSession({
43
+ access_token: accessToken,
44
+ refresh_token: refreshToken ?? '',
45
+ });
46
+ _lastAppliedAccessToken = accessToken;
47
+ installAuthWatcher();
48
+ return _client;
49
+ }
50
+ /**
51
+ * Returns true if the desktop-app has synced a Supabase session that's
52
+ * usable for direct-SDK calls.
53
+ */
54
+ export function hasSupabaseSession() {
55
+ return Boolean(getSupabaseUrl() && getSupabaseAnonKey() && getAccessToken());
56
+ }
57
+ function installAuthWatcher() {
58
+ if (_watcherInstalled) {
59
+ return;
60
+ }
61
+ _watcherInstalled = true;
62
+ try {
63
+ const watcher = watch(AUTH_FILE, () => {
64
+ invalidateAuthCache();
65
+ const nextAccess = getAccessToken();
66
+ const nextRefresh = getRefreshToken();
67
+ if (!_client ||
68
+ !nextAccess ||
69
+ nextAccess === _lastAppliedAccessToken) {
70
+ return;
71
+ }
72
+ _lastAppliedAccessToken = nextAccess;
73
+ void _client.auth.setSession({
74
+ access_token: nextAccess,
75
+ refresh_token: nextRefresh ?? '',
76
+ });
77
+ _client.realtime.setAuth(nextAccess);
78
+ });
79
+ watcher.unref();
80
+ }
81
+ catch {
82
+ // File missing or watch unsupported — best effort.
83
+ }
84
+ }
85
+ /** Reset module state. Test-only. */
86
+ export function resetSupabaseClient() {
87
+ _client = null;
88
+ _watcherInstalled = false;
89
+ _lastAppliedAccessToken = undefined;
90
+ }
@@ -8,7 +8,9 @@
8
8
  */
9
9
  import { hostname } from 'os';
10
10
  import { callMcpEndpoint } from '../api/mcp-client.js';
11
+ import { getUserId } from '../auth/auth-store.js';
11
12
  import { getVersion } from '../constants.js';
13
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
12
14
  import { initLogSync, logInfo, logWarning, stopLogSync, } from '../utils/logger.js';
13
15
  let currentSessionId = null;
14
16
  let heartbeatTimer;
@@ -31,19 +33,45 @@ export async function registerSession(options) {
31
33
  const sessionId = generateSessionId();
32
34
  currentSessionId = sessionId;
33
35
  try {
34
- const payload = {
35
- session_id: sessionId,
36
- hostname: hostname(),
37
- version: getVersion(),
38
- status: 'running',
39
- };
40
- if (options?.command) {
41
- payload.command = options.command;
36
+ const userId = getUserId();
37
+ if (hasSupabaseSession() && userId) {
38
+ const row = {
39
+ session_id: sessionId,
40
+ user_id: userId,
41
+ hostname: hostname(),
42
+ version: getVersion(),
43
+ status: 'running',
44
+ started_at: new Date().toISOString(),
45
+ last_heartbeat: new Date().toISOString(),
46
+ };
47
+ if (options?.command) {
48
+ row.command = options.command;
49
+ }
50
+ if (options?.productId) {
51
+ row.product_id = options.productId;
52
+ }
53
+ const { error } = await getSupabase()
54
+ .from('cli_sessions')
55
+ .upsert(row, { onConflict: 'session_id' });
56
+ if (error) {
57
+ throw new Error(error.message);
58
+ }
42
59
  }
43
- if (options?.productId) {
44
- payload.product_id = options.productId;
60
+ else {
61
+ const payload = {
62
+ session_id: sessionId,
63
+ hostname: hostname(),
64
+ version: getVersion(),
65
+ status: 'running',
66
+ };
67
+ if (options?.command) {
68
+ payload.command = options.command;
69
+ }
70
+ if (options?.productId) {
71
+ payload.product_id = options.productId;
72
+ }
73
+ await callMcpEndpoint('cli_sessions/register', payload);
45
74
  }
46
- await callMcpEndpoint('cli_sessions/register', payload);
47
75
  logInfo(`CLI session registered: ${sessionId}`);
48
76
  }
49
77
  catch {
@@ -62,17 +90,43 @@ export async function sendHeartbeat(currentIssueId, currentIssueName) {
62
90
  return 'running';
63
91
  }
64
92
  try {
65
- const result = (await callMcpEndpoint('cli_sessions/heartbeat', {
66
- session_id: currentSessionId,
67
- version: getVersion(),
68
- status: _sessionStatus,
69
- current_issue_id: currentIssueId,
70
- current_issue_name: currentIssueName,
71
- }));
93
+ let serverStatus;
94
+ const userId = getUserId();
95
+ if (hasSupabaseSession() && userId) {
96
+ // Single UPDATE...RETURNING reads back the row's current status, which
97
+ // the dashboard may have flipped to 'paused' / 'stopped' since the
98
+ // last heartbeat.
99
+ const { data, error } = await getSupabase()
100
+ .from('cli_sessions')
101
+ .update({
102
+ last_heartbeat: new Date().toISOString(),
103
+ version: getVersion(),
104
+ current_issue_id: currentIssueId ?? null,
105
+ current_issue_name: currentIssueName ?? null,
106
+ })
107
+ .eq('session_id', currentSessionId)
108
+ .eq('user_id', userId)
109
+ .select('status')
110
+ .maybeSingle();
111
+ if (error) {
112
+ throw new Error(error.message);
113
+ }
114
+ serverStatus = data?.status ?? undefined;
115
+ }
116
+ else {
117
+ const result = (await callMcpEndpoint('cli_sessions/heartbeat', {
118
+ session_id: currentSessionId,
119
+ version: getVersion(),
120
+ status: _sessionStatus,
121
+ current_issue_id: currentIssueId,
122
+ current_issue_name: currentIssueName,
123
+ }));
124
+ serverStatus = result.status;
125
+ }
72
126
  // Server may respond with a new status (e.g., 'paused' or 'stopped')
73
- if (result.status && result.status !== _sessionStatus) {
74
- logInfo(`Session status changed: ${_sessionStatus} -> ${result.status}`);
75
- _sessionStatus = result.status;
127
+ if (serverStatus && serverStatus !== _sessionStatus) {
128
+ logInfo(`Session status changed: ${_sessionStatus} -> ${serverStatus}`);
129
+ _sessionStatus = serverStatus;
76
130
  }
77
131
  return _sessionStatus;
78
132
  }
@@ -111,9 +165,28 @@ export async function deregisterSession() {
111
165
  // Flush remaining logs before deregistering
112
166
  await stopLogSync();
113
167
  try {
114
- await callMcpEndpoint('cli_sessions/deregister', {
115
- session_id: currentSessionId,
116
- });
168
+ const userId = getUserId();
169
+ if (hasSupabaseSession() && userId) {
170
+ // Match the MCP handler's behaviour: keep the row so historical logs
171
+ // can still link to it, just mark it stopped + clear current issue.
172
+ const { error } = await getSupabase()
173
+ .from('cli_sessions')
174
+ .update({
175
+ status: 'stopped',
176
+ current_issue_id: null,
177
+ current_issue_name: null,
178
+ })
179
+ .eq('session_id', currentSessionId)
180
+ .eq('user_id', userId);
181
+ if (error) {
182
+ throw new Error(error.message);
183
+ }
184
+ }
185
+ else {
186
+ await callMcpEndpoint('cli_sessions/deregister', {
187
+ session_id: currentSessionId,
188
+ });
189
+ }
117
190
  }
118
191
  catch {
119
192
  // Best-effort
@@ -45,6 +45,9 @@ export interface CliOptions {
45
45
  growthAnalysisId?: string;
46
46
  financingDeck?: string;
47
47
  financingTargetStage?: string;
48
+ userPsychology?: string;
49
+ userPsychologyGuidance?: string;
50
+ userPsychologyAnalysisId?: string;
48
51
  productLevel?: boolean;
49
52
  concurrency?: number;
50
53
  watchTasks?: string;
@@ -1,5 +1,7 @@
1
1
  import { callMcpEndpoint } from '../api/mcp-client.js';
2
+ import { getUserId } from '../auth/auth-store.js';
2
3
  import { getVersion } from '../constants.js';
4
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
3
5
  export const colors = {
4
6
  reset: '\x1b[0m',
5
7
  bright: '\x1b[1m',
@@ -48,10 +50,28 @@ export async function flushLogs() {
48
50
  }
49
51
  const batch = _logBuffer.splice(0);
50
52
  try {
51
- await callMcpEndpoint('cli_logs/batch', {
52
- session_id: _logSyncSessionId,
53
- logs: batch,
54
- });
53
+ const userId = getUserId();
54
+ if (hasSupabaseSession() && userId) {
55
+ // Direct-SDK path. user_id is set explicitly per row to satisfy the
56
+ // cli_logs RLS check (auth.uid() = user_id).
57
+ const rows = batch.slice(0, 200).map((log) => ({
58
+ session_id: _logSyncSessionId,
59
+ user_id: userId,
60
+ level: log.level,
61
+ message: log.message,
62
+ created_at: log.created_at,
63
+ }));
64
+ const { error } = await getSupabase().from('cli_logs').insert(rows);
65
+ if (error) {
66
+ throw new Error(error.message);
67
+ }
68
+ }
69
+ else {
70
+ await callMcpEndpoint('cli_logs/batch', {
71
+ session_id: _logSyncSessionId,
72
+ logs: batch,
73
+ });
74
+ }
55
75
  }
56
76
  catch {
57
77
  // Best-effort: put logs back if send fails (drop if buffer is huge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.56.3",
3
+ "version": "0.58.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"
@@ -44,13 +44,14 @@
44
44
  "dependencies": {
45
45
  "@anthropic-ai/claude-agent-sdk": "0.2.141",
46
46
  "@octokit/plugin-retry": "8.1.0",
47
+ "@supabase/supabase-js": "^2.47.10",
47
48
  "@octokit/plugin-throttling": "11.0.3",
48
49
  "@octokit/rest": "^20.0.0",
49
50
  "commander": "^12.0.0",
50
51
  "cosmiconfig": "^9.0.0",
51
52
  "dotenv": "^16.4.5",
52
- "edsger-contract": "0.3.1",
53
- "edsger-tools": "0.3.1",
53
+ "edsger-contract": "0.4.0",
54
+ "edsger-tools": "0.4.0",
54
55
  "gray-matter": "^4.0.3",
55
56
  "zod": "^4.0.0"
56
57
  },
package/vitest.config.ts CHANGED
@@ -15,6 +15,7 @@ export default defineConfig({
15
15
  'src/phases/sync-github-issues/__tests__/**/*.test.ts',
16
16
  'src/phases/sync-sentry-issues/__tests__/**/*.test.ts',
17
17
  'src/phases/sync-shared/__tests__/**/*.test.ts',
18
+ 'src/phases/screen-flow/__tests__/**/*.test.ts',
18
19
  'src/types/__tests__/**/*.test.ts',
19
20
  'src/commands/find-smells/__tests__/**/*.test.ts',
20
21
  ],