edsger 0.72.6 → 0.74.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.
- package/dist/api/intelligence-normalize.d.ts +25 -0
- package/dist/api/intelligence-normalize.js +103 -0
- package/dist/api/intelligence.js +113 -10
- package/dist/commands/features/index.d.ts +15 -0
- package/dist/commands/features/index.js +34 -0
- package/dist/commands/pr-resolve/index.d.ts +3 -1
- package/dist/commands/pr-resolve/index.js +12 -7
- package/dist/commands/pr-review/index.d.ts +3 -1
- package/dist/commands/pr-review/index.js +10 -6
- package/dist/commands/sync-github-pull-requests/index.d.ts +11 -0
- package/dist/commands/sync-github-pull-requests/index.js +42 -0
- package/dist/index.js +50 -4
- package/dist/phases/features/index.d.ts +65 -0
- package/dist/phases/features/index.js +292 -0
- package/dist/phases/features/mcp-server.d.ts +61 -0
- package/dist/phases/features/mcp-server.js +165 -0
- package/dist/phases/features/prompts.d.ts +32 -0
- package/dist/phases/features/prompts.js +92 -0
- package/dist/phases/features/types.d.ts +34 -0
- package/dist/phases/features/types.js +15 -0
- package/dist/phases/pr-resolve/index.d.ts +3 -1
- package/dist/phases/pr-resolve/index.js +12 -12
- package/dist/phases/pr-review/index.d.ts +3 -1
- package/dist/phases/pr-review/index.js +13 -16
- package/dist/phases/pr-shared/status.d.ts +18 -0
- package/dist/phases/pr-shared/status.js +37 -0
- package/dist/phases/sync-github-pull-requests/index.d.ts +23 -0
- package/dist/phases/sync-github-pull-requests/index.js +210 -0
- package/dist/phases/sync-github-pull-requests/state.d.ts +24 -0
- package/dist/phases/sync-github-pull-requests/state.js +16 -0
- package/dist/phases/sync-github-pull-requests/types.d.ts +22 -0
- package/dist/phases/sync-github-pull-requests/types.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report payload normalization for the direct-SDK write path.
|
|
3
|
+
*
|
|
4
|
+
* The LLM that produces intelligence reports sometimes returns structured
|
|
5
|
+
* arrays (`key_findings`, `recommendations`, …) as bare strings, or as objects
|
|
6
|
+
* keyed under synonyms (`description` instead of `finding`, `recommendation`
|
|
7
|
+
* instead of `action`, …). We canonicalize before writing so every reader — the
|
|
8
|
+
* desktop UI especially — can rely on the shape declared in the migration.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the normalizer in the MCP edge handler
|
|
11
|
+
* (`supabase/functions/mcp/handlers/intelligence.ts`). The two can't share a
|
|
12
|
+
* single module because the edge function runs on Deno (zod v3, URL imports)
|
|
13
|
+
* while this runs on Node — keep them in sync if either changes.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Normalize every structured array field present in the payload. Fields that
|
|
17
|
+
* are `undefined` are left untouched so partial updates don't overwrite stored
|
|
18
|
+
* values with empty arrays.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeReportArrays<T extends Record<string, unknown>>(payload: T): T;
|
|
21
|
+
/**
|
|
22
|
+
* Coerce an LLM-supplied value to a finite number, or null. Handles numbers,
|
|
23
|
+
* numeric strings, and null/garbage uniformly.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toNumberOrNull(value: unknown): number | null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report payload normalization for the direct-SDK write path.
|
|
3
|
+
*
|
|
4
|
+
* The LLM that produces intelligence reports sometimes returns structured
|
|
5
|
+
* arrays (`key_findings`, `recommendations`, …) as bare strings, or as objects
|
|
6
|
+
* keyed under synonyms (`description` instead of `finding`, `recommendation`
|
|
7
|
+
* instead of `action`, …). We canonicalize before writing so every reader — the
|
|
8
|
+
* desktop UI especially — can rely on the shape declared in the migration.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the normalizer in the MCP edge handler
|
|
11
|
+
* (`supabase/functions/mcp/handlers/intelligence.ts`). The two can't share a
|
|
12
|
+
* single module because the edge function runs on Deno (zod v3, URL imports)
|
|
13
|
+
* while this runs on Node — keep them in sync if either changes.
|
|
14
|
+
*/
|
|
15
|
+
const NORMALIZE_SPECS = {
|
|
16
|
+
key_findings: {
|
|
17
|
+
primary: 'finding',
|
|
18
|
+
aliases: ['description', 'insight', 'text', 'title'],
|
|
19
|
+
defaults: { severity: 'fyi', category: '' },
|
|
20
|
+
},
|
|
21
|
+
recommendations: {
|
|
22
|
+
primary: 'action',
|
|
23
|
+
aliases: ['recommendation', 'description', 'text', 'title'],
|
|
24
|
+
defaults: { priority: 'medium', effort: '', impact: '', rationale: '' },
|
|
25
|
+
},
|
|
26
|
+
competitor_highlights: {
|
|
27
|
+
primary: 'highlight',
|
|
28
|
+
aliases: ['description', 'text', 'title'],
|
|
29
|
+
defaults: { competitor_name: '', sentiment: 'neutral' },
|
|
30
|
+
},
|
|
31
|
+
market_signals: {
|
|
32
|
+
primary: 'signal',
|
|
33
|
+
aliases: ['description', 'text', 'title'],
|
|
34
|
+
defaults: { source: '', relevance: '', url: '' },
|
|
35
|
+
},
|
|
36
|
+
trends: {
|
|
37
|
+
primary: 'trend',
|
|
38
|
+
aliases: ['description', 'text', 'title'],
|
|
39
|
+
defaults: { direction: '', evidence: '' },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
function normalizeItem(item, spec) {
|
|
43
|
+
if (typeof item === 'string') {
|
|
44
|
+
return { [spec.primary]: item, ...spec.defaults };
|
|
45
|
+
}
|
|
46
|
+
if (!item || typeof item !== 'object') {
|
|
47
|
+
return { [spec.primary]: '', ...spec.defaults };
|
|
48
|
+
}
|
|
49
|
+
const obj = { ...item };
|
|
50
|
+
if (typeof obj[spec.primary] !== 'string' || !obj[spec.primary]) {
|
|
51
|
+
for (const alias of spec.aliases) {
|
|
52
|
+
const v = obj[alias];
|
|
53
|
+
if (typeof v === 'string' && v.trim().length > 0) {
|
|
54
|
+
obj[spec.primary] = v;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (typeof obj[spec.primary] !== 'string') {
|
|
59
|
+
obj[spec.primary] = '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const [key, fallback] of Object.entries(spec.defaults)) {
|
|
63
|
+
if (obj[key] === undefined || obj[key] === null) {
|
|
64
|
+
obj[key] = fallback;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return obj;
|
|
68
|
+
}
|
|
69
|
+
function normalizeArray(value, spec) {
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return value.map((item) => normalizeItem(item, spec));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Normalize every structured array field present in the payload. Fields that
|
|
77
|
+
* are `undefined` are left untouched so partial updates don't overwrite stored
|
|
78
|
+
* values with empty arrays.
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeReportArrays(payload) {
|
|
81
|
+
const result = { ...payload };
|
|
82
|
+
for (const [field, spec] of Object.entries(NORMALIZE_SPECS)) {
|
|
83
|
+
if (result[field] !== undefined) {
|
|
84
|
+
;
|
|
85
|
+
result[field] = normalizeArray(result[field], spec);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Coerce an LLM-supplied value to a finite number, or null. Handles numbers,
|
|
92
|
+
* numeric strings, and null/garbage uniformly.
|
|
93
|
+
*/
|
|
94
|
+
export function toNumberOrNull(value) {
|
|
95
|
+
if (typeof value === 'number') {
|
|
96
|
+
return Number.isFinite(value) ? value : null;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
99
|
+
const n = Number(value);
|
|
100
|
+
return Number.isFinite(n) ? n : null;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
package/dist/api/intelligence.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
|
|
1
|
+
import { ensureSupabaseSession, getSupabase, hasSupabaseSession, } from '../supabase/client.js';
|
|
2
2
|
import { logError, logInfo } from '../utils/logger.js';
|
|
3
|
+
import { normalizeReportArrays, toNumberOrNull, } from './intelligence-normalize.js';
|
|
3
4
|
import { callMcpEndpoint } from './mcp-client.js';
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// MCP response helper
|
|
@@ -180,8 +181,41 @@ export async function saveSnapshot(snapshot, verbose) {
|
|
|
180
181
|
logInfo(`Saving snapshot for competitor: ${snapshot.competitor_id}`);
|
|
181
182
|
}
|
|
182
183
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
// Direct SDK write under the user's RLS session — no MCP fallback.
|
|
185
|
+
// Await the synced session first so the insert carries the user JWT
|
|
186
|
+
// (otherwise it races out as `anon` and trips RLS).
|
|
187
|
+
const ready = await ensureSupabaseSession();
|
|
188
|
+
if (!ready) {
|
|
189
|
+
logError('Failed to save snapshot: no Supabase session (sign in to the desktop app)');
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const source = ['ai_analysis', 'manual', 'scheduled'].includes(snapshot.source)
|
|
193
|
+
? snapshot.source
|
|
194
|
+
: 'ai_analysis';
|
|
195
|
+
const { data, error } = await getSupabase()
|
|
196
|
+
.from('competitor_snapshots')
|
|
197
|
+
.insert({
|
|
198
|
+
competitor_id: snapshot.competitor_id,
|
|
199
|
+
product_id: snapshot.product_id,
|
|
200
|
+
issues: snapshot.issues ?? [],
|
|
201
|
+
pricing: snapshot.pricing ?? {},
|
|
202
|
+
tech_stack: snapshot.tech_stack ?? [],
|
|
203
|
+
app_rating: toNumberOrNull(snapshot.app_rating),
|
|
204
|
+
app_review_count: toNumberOrNull(snapshot.app_review_count),
|
|
205
|
+
app_version: snapshot.app_version ?? null,
|
|
206
|
+
app_last_updated: snapshot.app_last_updated ?? null,
|
|
207
|
+
recent_reviews: snapshot.recent_reviews ?? [],
|
|
208
|
+
social_mentions: snapshot.social_mentions ?? {},
|
|
209
|
+
changes_detected: snapshot.changes_detected ?? [],
|
|
210
|
+
source,
|
|
211
|
+
raw_data: snapshot.raw_data ?? null,
|
|
212
|
+
})
|
|
213
|
+
.select()
|
|
214
|
+
.single();
|
|
215
|
+
if (error) {
|
|
216
|
+
throw new Error(error.message);
|
|
217
|
+
}
|
|
218
|
+
return data ?? null;
|
|
185
219
|
}
|
|
186
220
|
catch (error) {
|
|
187
221
|
logError(`Failed to save snapshot: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -273,8 +307,54 @@ export async function saveReport(report, verbose) {
|
|
|
273
307
|
logInfo(`Saving intelligence report: ${report.title}`);
|
|
274
308
|
}
|
|
275
309
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
310
|
+
// Direct SDK write — no MCP fallback. intelligence_reports.created_by is
|
|
311
|
+
// NOT NULL and RLS checks `created_by = auth.uid()`, so we resolve the
|
|
312
|
+
// user from the synced session and canonicalize the LLM arrays first.
|
|
313
|
+
const ready = await ensureSupabaseSession();
|
|
314
|
+
if (!ready) {
|
|
315
|
+
logError('Failed to save report: no Supabase session (sign in to the desktop app)');
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const { data: sessionData } = await getSupabase().auth.getSession();
|
|
319
|
+
const userId = sessionData.session?.user?.id;
|
|
320
|
+
if (!userId) {
|
|
321
|
+
logError('Failed to save report: could not resolve user from session');
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const normalized = normalizeReportArrays({
|
|
325
|
+
key_findings: report.key_findings ?? [],
|
|
326
|
+
recommendations: report.recommendations ?? [],
|
|
327
|
+
competitor_highlights: report.competitor_highlights ?? [],
|
|
328
|
+
market_signals: report.market_signals ?? [],
|
|
329
|
+
trends: report.trends ?? [],
|
|
330
|
+
});
|
|
331
|
+
const { data, error } = await getSupabase()
|
|
332
|
+
.from('intelligence_reports')
|
|
333
|
+
.insert({
|
|
334
|
+
product_id: report.product_id,
|
|
335
|
+
report_type: report.report_type,
|
|
336
|
+
title: report.title,
|
|
337
|
+
summary: report.summary ?? null,
|
|
338
|
+
full_report: report.full_report ?? null,
|
|
339
|
+
key_findings: normalized.key_findings,
|
|
340
|
+
recommendations: normalized.recommendations,
|
|
341
|
+
competitor_highlights: normalized.competitor_highlights,
|
|
342
|
+
market_signals: normalized.market_signals,
|
|
343
|
+
trends: normalized.trends,
|
|
344
|
+
persona_updates: report.persona_updates ?? null,
|
|
345
|
+
pmf_score: toNumberOrNull(report.pmf_score),
|
|
346
|
+
pmf_signals: report.pmf_signals ?? null,
|
|
347
|
+
guidance: report.guidance ?? null,
|
|
348
|
+
status: report.status ?? 'completed',
|
|
349
|
+
snapshot_ids: report.snapshot_ids ?? [],
|
|
350
|
+
created_by: userId,
|
|
351
|
+
})
|
|
352
|
+
.select()
|
|
353
|
+
.single();
|
|
354
|
+
if (error) {
|
|
355
|
+
throw new Error(error.message);
|
|
356
|
+
}
|
|
357
|
+
return data ?? null;
|
|
278
358
|
}
|
|
279
359
|
catch (error) {
|
|
280
360
|
logError(`Failed to save report: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -286,11 +366,34 @@ export async function updateReport(reportId, updates, verbose) {
|
|
|
286
366
|
logInfo(`Updating report: ${reportId}`);
|
|
287
367
|
}
|
|
288
368
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
369
|
+
// Direct SDK write — no MCP fallback.
|
|
370
|
+
const ready = await ensureSupabaseSession();
|
|
371
|
+
if (!ready) {
|
|
372
|
+
logError('Failed to update report: no Supabase session (sign in to the desktop app)');
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
// Only send keys the caller provided so a partial update doesn't clobber
|
|
376
|
+
// stored columns. Numeric/array fields are coerced/canonicalized.
|
|
377
|
+
const payload = {};
|
|
378
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
379
|
+
if (value !== undefined) {
|
|
380
|
+
payload[key] = value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if ('pmf_score' in payload) {
|
|
384
|
+
payload.pmf_score = toNumberOrNull(payload.pmf_score);
|
|
385
|
+
}
|
|
386
|
+
const normalized = normalizeReportArrays(payload);
|
|
387
|
+
const { data, error } = await getSupabase()
|
|
388
|
+
.from('intelligence_reports')
|
|
389
|
+
.update(normalized)
|
|
390
|
+
.eq('id', reportId)
|
|
391
|
+
.select()
|
|
392
|
+
.single();
|
|
393
|
+
if (error) {
|
|
394
|
+
throw new Error(error.message);
|
|
395
|
+
}
|
|
396
|
+
return data ?? null;
|
|
294
397
|
}
|
|
295
398
|
catch (error) {
|
|
296
399
|
logError(`Failed to update report: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger features <productId> --scan-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Clones every repository linked to the product, asks Claude to catalogue
|
|
5
|
+
* the user-facing features it delivers, and persists them via the
|
|
6
|
+
* product_features table. The desktop UI creates a pending feature_scans
|
|
7
|
+
* row first then invokes the CLI with --scan-id; the CLI flips status
|
|
8
|
+
* running → success/failed and writes features via the MCP toolkit.
|
|
9
|
+
*/
|
|
10
|
+
export interface FeaturesCliOptions {
|
|
11
|
+
scanId: string;
|
|
12
|
+
guidance?: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function runFeatures(productId: string, options: FeaturesCliOptions): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger features <productId> --scan-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Clones every repository linked to the product, asks Claude to catalogue
|
|
5
|
+
* the user-facing features it delivers, and persists them via the
|
|
6
|
+
* product_features table. The desktop UI creates a pending feature_scans
|
|
7
|
+
* row first then invokes the CLI with --scan-id; the CLI flips status
|
|
8
|
+
* running → success/failed and writes features via the MCP toolkit.
|
|
9
|
+
*/
|
|
10
|
+
import { runFeaturesPhase } from '../../phases/features/index.js';
|
|
11
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
12
|
+
export async function runFeatures(productId, options) {
|
|
13
|
+
const { scanId, guidance, verbose } = options;
|
|
14
|
+
if (!productId) {
|
|
15
|
+
throw new Error('A product ID is required for features');
|
|
16
|
+
}
|
|
17
|
+
if (!scanId) {
|
|
18
|
+
throw new Error('--scan-id is required (the pending feature_scans row id)');
|
|
19
|
+
}
|
|
20
|
+
logInfo(`Starting features scan for product ${productId}`);
|
|
21
|
+
const result = await runFeaturesPhase({
|
|
22
|
+
productId,
|
|
23
|
+
scanId,
|
|
24
|
+
guidance,
|
|
25
|
+
verbose,
|
|
26
|
+
});
|
|
27
|
+
if (result.status === 'success') {
|
|
28
|
+
logSuccess(result.message);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
logError(result.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
export interface PRResolveCliOptions {
|
|
6
6
|
prUrl: string;
|
|
7
7
|
prId?: string;
|
|
8
|
+
/** Repo-only mode: resolve GitHub config from a repositories row. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
verbose?: boolean;
|
|
9
11
|
learn?: boolean;
|
|
10
12
|
}
|
|
11
|
-
export declare function runPRResolve(productId: string, options: PRResolveCliOptions): Promise<void>;
|
|
13
|
+
export declare function runPRResolve(productId: string | undefined, options: PRResolveCliOptions): Promise<void>;
|
|
@@ -2,31 +2,36 @@
|
|
|
2
2
|
* CLI command: edsger pr-resolve <productId> --pr-url <url>
|
|
3
3
|
* Resolves PR change requests: fixes what's genuinely better, explains what it won't change.
|
|
4
4
|
*/
|
|
5
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
5
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
6
6
|
import { resolveStandalonePR } from '../../phases/pr-resolve/index.js';
|
|
7
7
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
8
8
|
export async function runPRResolve(productId, options) {
|
|
9
|
-
const { prUrl, prId, verbose } = options;
|
|
10
|
-
|
|
9
|
+
const { prUrl, prId, repoId, verbose } = options;
|
|
10
|
+
const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
|
|
11
|
+
logInfo(`Starting PR resolve for ${scopeLabel}`);
|
|
11
12
|
logInfo(`PR URL: ${prUrl}`);
|
|
12
|
-
//
|
|
13
|
-
const githubConfig =
|
|
13
|
+
// Resolve GitHub config from the repository (repo-only mode) or the product.
|
|
14
|
+
const githubConfig = repoId
|
|
15
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
16
|
+
: await getGitHubConfigByProduct(productId ?? '', verbose);
|
|
14
17
|
if (!githubConfig.configured ||
|
|
15
18
|
!githubConfig.token ||
|
|
16
19
|
!githubConfig.owner ||
|
|
17
20
|
!githubConfig.repo) {
|
|
18
|
-
logError(`GitHub not configured for
|
|
21
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
19
22
|
process.exit(1);
|
|
20
23
|
}
|
|
21
24
|
const result = await resolveStandalonePR({
|
|
22
25
|
productId,
|
|
26
|
+
repositoryId: repoId,
|
|
23
27
|
pullRequestUrl: prUrl,
|
|
24
28
|
githubToken: githubConfig.token,
|
|
25
29
|
owner: githubConfig.owner,
|
|
26
30
|
repo: githubConfig.repo,
|
|
27
31
|
prId,
|
|
28
32
|
verbose,
|
|
29
|
-
|
|
33
|
+
// Checklist learning is product-scoped; skip it in repo-only mode.
|
|
34
|
+
learn: repoId ? false : options.learn,
|
|
30
35
|
});
|
|
31
36
|
if (result.status === 'success') {
|
|
32
37
|
logSuccess(`PR resolve completed: ${result.message}`);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export interface PRReviewCliOptions {
|
|
6
6
|
prUrl: string;
|
|
7
7
|
prId?: string;
|
|
8
|
+
/** Repo-only mode: resolve GitHub config from a repositories row. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
verbose?: boolean;
|
|
9
11
|
}
|
|
10
|
-
export declare function runPRReview(productId: string, options: PRReviewCliOptions): Promise<void>;
|
|
12
|
+
export declare function runPRReview(productId: string | undefined, options: PRReviewCliOptions): Promise<void>;
|
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
* CLI command: edsger pr-review <productId> --pr-url <url>
|
|
3
3
|
* Reviews a standalone GitHub PR and posts comments.
|
|
4
4
|
*/
|
|
5
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
5
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
6
6
|
import { reviewStandalonePR } from '../../phases/pr-review/index.js';
|
|
7
7
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
8
8
|
export async function runPRReview(productId, options) {
|
|
9
|
-
const { prUrl, prId, verbose } = options;
|
|
10
|
-
|
|
9
|
+
const { prUrl, prId, repoId, verbose } = options;
|
|
10
|
+
const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
|
|
11
|
+
logInfo(`Starting PR review for ${scopeLabel}`);
|
|
11
12
|
logInfo(`PR URL: ${prUrl}`);
|
|
12
|
-
//
|
|
13
|
-
const githubConfig =
|
|
13
|
+
// Resolve GitHub config from the repository (repo-only mode) or the product.
|
|
14
|
+
const githubConfig = repoId
|
|
15
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
16
|
+
: await getGitHubConfigByProduct(productId ?? '', verbose);
|
|
14
17
|
if (!githubConfig.configured ||
|
|
15
18
|
!githubConfig.token ||
|
|
16
19
|
!githubConfig.owner ||
|
|
17
20
|
!githubConfig.repo) {
|
|
18
|
-
logError(`GitHub not configured for
|
|
21
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
19
22
|
process.exit(1);
|
|
20
23
|
}
|
|
21
24
|
const result = await reviewStandalonePR({
|
|
22
25
|
productId,
|
|
26
|
+
repositoryId: repoId,
|
|
23
27
|
pullRequestUrl: prUrl,
|
|
24
28
|
githubToken: githubConfig.token,
|
|
25
29
|
owner: githubConfig.owner,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `edsger sync-github-pull-requests <repoId>`
|
|
3
|
+
*
|
|
4
|
+
* Mirrors a connected GitHub repo's pull requests into the local
|
|
5
|
+
* `pull_requests` table (scoped by repository_id) for the repo detail page.
|
|
6
|
+
* Idempotent — already-synced PRs are refreshed, never duplicated.
|
|
7
|
+
*/
|
|
8
|
+
export interface SyncGithubPullRequestsCliOptions {
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runSyncGithubPullRequests(repositoryId: string, options?: SyncGithubPullRequestsCliOptions): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `edsger sync-github-pull-requests <repoId>`
|
|
3
|
+
*
|
|
4
|
+
* Mirrors a connected GitHub repo's pull requests into the local
|
|
5
|
+
* `pull_requests` table (scoped by repository_id) for the repo detail page.
|
|
6
|
+
* Idempotent — already-synced PRs are refreshed, never duplicated.
|
|
7
|
+
*/
|
|
8
|
+
import { getGitHubConfigByRepository } from '../../api/github.js';
|
|
9
|
+
import { syncGithubPullRequests } from '../../phases/sync-github-pull-requests/index.js';
|
|
10
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
11
|
+
export async function runSyncGithubPullRequests(repositoryId, options = {}) {
|
|
12
|
+
const { verbose } = options;
|
|
13
|
+
logInfo(`Starting GitHub pull request sync for repository ${repositoryId}`);
|
|
14
|
+
const githubConfig = await getGitHubConfigByRepository(repositoryId, verbose);
|
|
15
|
+
if (!githubConfig.configured ||
|
|
16
|
+
!githubConfig.token ||
|
|
17
|
+
!githubConfig.owner ||
|
|
18
|
+
!githubConfig.repo) {
|
|
19
|
+
logError(`GitHub not configured for repository ${repositoryId}: ${githubConfig.message || 'No installation found'}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const result = await syncGithubPullRequests({
|
|
23
|
+
repositoryId,
|
|
24
|
+
githubToken: githubConfig.token,
|
|
25
|
+
owner: githubConfig.owner,
|
|
26
|
+
repo: githubConfig.repo,
|
|
27
|
+
verbose,
|
|
28
|
+
});
|
|
29
|
+
if (result.status === 'success') {
|
|
30
|
+
logSuccess(`GitHub PR sync completed: ${result.message}`);
|
|
31
|
+
if (result.repository) {
|
|
32
|
+
logInfo(`Repository: ${result.repository}`);
|
|
33
|
+
}
|
|
34
|
+
if (result.fetchedCount !== undefined) {
|
|
35
|
+
logInfo(`Fetched ${result.fetchedCount} · created ${result.createdCount ?? 0} · updated ${result.updatedCount ?? 0}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logError(`GitHub PR sync failed: ${result.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { runConfigGet, runConfigList, runConfigSet, runConfigUnset, } from './co
|
|
|
19
19
|
import { runDataFlow } from './commands/data-flow/index.js';
|
|
20
20
|
import { runDiscover } from './commands/discover/index.js';
|
|
21
21
|
import { runErDiagram } from './commands/er-diagram/index.js';
|
|
22
|
+
import { runFeatures } from './commands/features/index.js';
|
|
22
23
|
import { runFinancingDeck } from './commands/financing-deck/index.js';
|
|
23
24
|
import { runFindArchitecture } from './commands/find-architecture/index.js';
|
|
24
25
|
import { runFindBugs } from './commands/find-bugs/index.js';
|
|
@@ -46,6 +47,7 @@ import { runStateDiagram } from './commands/state-diagram/index.js';
|
|
|
46
47
|
import { runSyncAws } from './commands/sync-aws/index.js';
|
|
47
48
|
import { runSyncDatadog } from './commands/sync-datadog/index.js';
|
|
48
49
|
import { runSyncGithubIssues } from './commands/sync-github-issues/index.js';
|
|
50
|
+
import { runSyncGithubPullRequests } from './commands/sync-github-pull-requests/index.js';
|
|
49
51
|
import { runSyncOrgRepos } from './commands/sync-org-repos/index.js';
|
|
50
52
|
import { runSyncSentryIssues } from './commands/sync-sentry-issues/index.js';
|
|
51
53
|
import { runSyncTerraform } from './commands/sync-terraform/index.js';
|
|
@@ -763,13 +765,17 @@ program
|
|
|
763
765
|
// Subcommand: edsger pr-review <productId>
|
|
764
766
|
// ============================================================
|
|
765
767
|
program
|
|
766
|
-
.command('pr-review
|
|
767
|
-
.description('AI-review a GitHub PR for a product')
|
|
768
|
+
.command('pr-review [productId]')
|
|
769
|
+
.description('AI-review a GitHub PR for a product (or standalone repository)')
|
|
768
770
|
.requiredOption('--pr-url <url>', 'GitHub PR URL')
|
|
769
771
|
.option('--pr-id <id>', 'Pull request record ID in database')
|
|
772
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
770
773
|
.option('-v, --verbose', 'Verbose output')
|
|
771
774
|
.action(async (productId, opts) => {
|
|
772
775
|
try {
|
|
776
|
+
if (!productId && !opts.repoId) {
|
|
777
|
+
throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-review');
|
|
778
|
+
}
|
|
773
779
|
await runPRReview(productId, opts);
|
|
774
780
|
}
|
|
775
781
|
catch (error) {
|
|
@@ -842,6 +848,21 @@ program
|
|
|
842
848
|
}
|
|
843
849
|
});
|
|
844
850
|
// ============================================================
|
|
851
|
+
// Subcommand: edsger sync-github-pull-requests <repoId>
|
|
852
|
+
// ============================================================
|
|
853
|
+
program
|
|
854
|
+
.command('sync-github-pull-requests <repoId>')
|
|
855
|
+
.description("Mirror a connected GitHub repo's pull requests into the local pull_requests list for the repo detail page (idempotent — refreshes, never duplicates)")
|
|
856
|
+
.option('-v, --verbose', 'Verbose output')
|
|
857
|
+
.action(async (repoId, opts) => {
|
|
858
|
+
try {
|
|
859
|
+
await runSyncGithubPullRequests(repoId, opts);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
exitWithError(error);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
// ============================================================
|
|
845
866
|
// Subcommand: edsger sync-org-repos <teamId>
|
|
846
867
|
// ============================================================
|
|
847
868
|
program
|
|
@@ -1081,17 +1102,42 @@ program
|
|
|
1081
1102
|
}
|
|
1082
1103
|
});
|
|
1083
1104
|
// ============================================================
|
|
1105
|
+
// Subcommand: edsger features <productId>
|
|
1106
|
+
// ============================================================
|
|
1107
|
+
program
|
|
1108
|
+
.command('features <productId>')
|
|
1109
|
+
.description('Scan every repository linked to a product for the user-facing features it delivers and persist them via the product_features table. Manually defined features are enriched, never rewritten. Writes against the pending feature_scans row identified by --scan-id.')
|
|
1110
|
+
.requiredOption('--scan-id <id>', 'Pending feature_scans row id to drive (created by the desktop UI before invocation)')
|
|
1111
|
+
.option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
|
|
1112
|
+
.option('-v, --verbose', 'Verbose output')
|
|
1113
|
+
.action(async (productId, opts) => {
|
|
1114
|
+
try {
|
|
1115
|
+
await runFeatures(productId, {
|
|
1116
|
+
scanId: opts.scanId,
|
|
1117
|
+
guidance: opts.guidance,
|
|
1118
|
+
verbose: opts.verbose,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
exitWithError(error);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
// ============================================================
|
|
1084
1126
|
// Subcommand: edsger pr-resolve <productId>
|
|
1085
1127
|
// ============================================================
|
|
1086
1128
|
program
|
|
1087
|
-
.command('pr-resolve
|
|
1088
|
-
.description('AI-resolve change requests on a GitHub PR')
|
|
1129
|
+
.command('pr-resolve [productId]')
|
|
1130
|
+
.description('AI-resolve change requests on a GitHub PR for a product (or standalone repository)')
|
|
1089
1131
|
.requiredOption('--pr-url <url>', 'GitHub PR URL')
|
|
1090
1132
|
.option('--pr-id <id>', 'Pull request record ID in database')
|
|
1133
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
1091
1134
|
.option('--no-learn', 'Skip checklist learning after resolve')
|
|
1092
1135
|
.option('-v, --verbose', 'Verbose output')
|
|
1093
1136
|
.action(async (productId, opts) => {
|
|
1094
1137
|
try {
|
|
1138
|
+
if (!productId && !opts.repoId) {
|
|
1139
|
+
throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-resolve');
|
|
1140
|
+
}
|
|
1095
1141
|
await runPRResolve(productId, opts);
|
|
1096
1142
|
}
|
|
1097
1143
|
catch (error) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features phase: clone EVERY repository linked to the product
|
|
3
|
+
* (product_repositories), ask Claude to catalogue the user-facing features
|
|
4
|
+
* the product delivers, and persist them via the product_features table.
|
|
5
|
+
*
|
|
6
|
+
* Multi-repo: unlike recipes (single primary repo), features are discovered
|
|
7
|
+
* across the whole product. Repo resolution + cloning is shared with the
|
|
8
|
+
* diagram phases (cloneDiagramRepos): each repo is cloned into its own
|
|
9
|
+
* subdirectory of a per-product parent dir, and the agent runs in the
|
|
10
|
+
* parent so it can explore all of them in one pass.
|
|
11
|
+
*
|
|
12
|
+
* Production-grade behaviours layered on top of the basic agent loop
|
|
13
|
+
* (mirrors the recipes phase):
|
|
14
|
+
*
|
|
15
|
+
* - Heartbeat: `last_heartbeat_at` on the feature_scans row is refreshed
|
|
16
|
+
* on every assistant message so the reader can detect stalled / crashed
|
|
17
|
+
* runs (see desktop-app/.../services/db/feature-scans.ts for the lazy
|
|
18
|
+
* reaper).
|
|
19
|
+
* - Cancellation-safe writes: markRunning / markSuccess / markFailed only
|
|
20
|
+
* touch rows whose status is in {pending, running}. If the user clicked
|
|
21
|
+
* Stop and the row is now 'cancelled', the final write no-ops.
|
|
22
|
+
* - Per-call MCP writes: agent commits each create / update / remove as
|
|
23
|
+
* it goes. There is no "submit at the end" buffer — partial progress
|
|
24
|
+
* survives even if the agent later errors out.
|
|
25
|
+
*/
|
|
26
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
27
|
+
import { type ClonedRepo } from '../diagram-shared/clone-repos.js';
|
|
28
|
+
import type { FeatureSummary } from './types.js';
|
|
29
|
+
export interface FeaturesPhaseOptions {
|
|
30
|
+
productId: string;
|
|
31
|
+
scanId: string;
|
|
32
|
+
guidance?: string;
|
|
33
|
+
verbose?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface FeaturesPhaseResult {
|
|
36
|
+
status: 'success' | 'error' | 'cancelled';
|
|
37
|
+
message: string;
|
|
38
|
+
counts?: {
|
|
39
|
+
created: number;
|
|
40
|
+
updated: number;
|
|
41
|
+
removed: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Repo-scope note for the agent's user prompt. Unlike the diagram phases'
|
|
46
|
+
* describeRepoScope (which asks for one unified flow), this tells the agent
|
|
47
|
+
* the exact full names it may use in the `repos` field of each feature.
|
|
48
|
+
*/
|
|
49
|
+
export declare function describeFeatureRepoScope(repos: ClonedRepo[]): string;
|
|
50
|
+
export declare function runFeaturesPhase(options: FeaturesPhaseOptions): Promise<FeaturesPhaseResult>;
|
|
51
|
+
export declare function listProductRepositoryIds(supabase: SupabaseClient, productId: string): Promise<string[]>;
|
|
52
|
+
export declare function getScanCreator(supabase: SupabaseClient, scanId: string): Promise<{
|
|
53
|
+
created_by: string;
|
|
54
|
+
} | null>;
|
|
55
|
+
export declare function listProductFeatures(supabase: SupabaseClient, productId: string): Promise<FeatureSummary[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Claim the row by flipping `pending` → `running`. Returns true on success
|
|
58
|
+
* (we won the claim) and false when the row has already moved on (e.g. user
|
|
59
|
+
* cancelled before the CLI started). Bounded by the status filter so we
|
|
60
|
+
* can't accidentally resurrect a 'cancelled' row.
|
|
61
|
+
*/
|
|
62
|
+
export declare function markRunning(supabase: SupabaseClient, scanId: string): Promise<boolean>;
|
|
63
|
+
export declare function heartbeat(supabase: SupabaseClient, scanId: string): Promise<void>;
|
|
64
|
+
export declare function markFailed(supabase: SupabaseClient, scanId: string, errorMessage: string): Promise<boolean>;
|
|
65
|
+
export declare function markSuccess(supabase: SupabaseClient, scanId: string): Promise<boolean>;
|