ticket-to-pr 1.0.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/cli.js ADDED
@@ -0,0 +1,515 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { execSync } from 'node:child_process';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { mask, shellEscape, writeEnvFile, updateProjectsFile } from './lib/utils.js';
6
+ import { getProjectNames, getProjectDir } from './lib/projects.js';
7
+ import { CONFIG_DIR } from './lib/paths.js';
8
+ // -- Colors --
9
+ const RESET = '\x1b[0m';
10
+ const DIM = '\x1b[2m';
11
+ const GREEN = '\x1b[32m';
12
+ const YELLOW = '\x1b[33m';
13
+ const RED = '\x1b[31m';
14
+ const BOLD = '\x1b[1m';
15
+ // -- Shared utilities --
16
+ function printStatus(ok, label, detail) {
17
+ const icon = ok === true ? `${GREEN}✓${RESET}` : ok === false ? `${RED}✗${RESET}` : `${YELLOW}○${RESET}`;
18
+ const line = detail ? `${icon} ${label} ${DIM}${detail}${RESET}` : `${icon} ${label}`;
19
+ console.log(` ${line}`);
20
+ }
21
+ function checkCommand(cmd) {
22
+ try {
23
+ const output = execSync(cmd, { stdio: 'pipe', timeout: 10_000 }).toString().trim();
24
+ return { ok: true, output };
25
+ }
26
+ catch {
27
+ return { ok: false, output: '' };
28
+ }
29
+ }
30
+ function ask(rl, question, opts) {
31
+ return new Promise((resolve) => {
32
+ const suffix = opts?.defaultValue ? ` ${DIM}(${opts.defaultValue})${RESET}` : '';
33
+ rl.question(` ${question}${suffix}: `, (answer) => {
34
+ const value = answer.trim() || opts?.defaultValue || '';
35
+ if (opts?.validate) {
36
+ const error = opts.validate(value);
37
+ if (error) {
38
+ console.log(` ${RED}${error}${RESET}`);
39
+ resolve(ask(rl, question, opts));
40
+ return;
41
+ }
42
+ }
43
+ resolve(value);
44
+ });
45
+ });
46
+ }
47
+ // -- Doctor --
48
+ export async function runDoctor() {
49
+ console.log(`\n${BOLD}TicketToPR Doctor${RESET}\n`);
50
+ let passed = 0;
51
+ let warnings = 0;
52
+ let failed = 0;
53
+ function track(ok) {
54
+ if (ok === true)
55
+ passed++;
56
+ else if (ok === false)
57
+ failed++;
58
+ else
59
+ warnings++;
60
+ }
61
+ // Environment
62
+ console.log(`${BOLD}Environment:${RESET}`);
63
+ const envPath = join(CONFIG_DIR, '.env.local');
64
+ const envExists = existsSync(envPath);
65
+ printStatus(envExists, '.env.local exists');
66
+ track(envExists);
67
+ let envVars = {};
68
+ if (envExists) {
69
+ try {
70
+ const content = readFileSync(envPath, 'utf-8');
71
+ for (const line of content.split('\n')) {
72
+ const trimmed = line.trim();
73
+ if (!trimmed || trimmed.startsWith('#'))
74
+ continue;
75
+ const eqIndex = trimmed.indexOf('=');
76
+ if (eqIndex === -1)
77
+ continue;
78
+ envVars[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
79
+ }
80
+ }
81
+ catch {
82
+ // ignore
83
+ }
84
+ }
85
+ const notionToken = envVars.NOTION_TOKEN || process.env.NOTION_TOKEN || '';
86
+ const hasToken = notionToken.length > 0;
87
+ printStatus(hasToken, 'NOTION_TOKEN set', hasToken ? mask(notionToken) : undefined);
88
+ track(hasToken);
89
+ const dbId = envVars.NOTION_DATABASE_ID || process.env.NOTION_DATABASE_ID || '';
90
+ const hasDbId = dbId.length > 0;
91
+ printStatus(hasDbId, 'NOTION_DATABASE_ID set', hasDbId ? mask(dbId) : undefined);
92
+ track(hasDbId);
93
+ const licenseKey = envVars.LICENSE_KEY || process.env.LICENSE_KEY || '';
94
+ if (licenseKey) {
95
+ // Dynamically import config to check isPro
96
+ const { isPro } = await import('./config.js');
97
+ // Temporarily set env for check
98
+ const prev = process.env.LICENSE_KEY;
99
+ process.env.LICENSE_KEY = licenseKey;
100
+ const pro = isPro();
101
+ process.env.LICENSE_KEY = prev;
102
+ printStatus(pro, 'LICENSE_KEY', pro ? 'Pro' : 'Invalid key');
103
+ track(pro ? true : null);
104
+ }
105
+ else {
106
+ printStatus(null, 'LICENSE_KEY', 'Free tier');
107
+ track(null);
108
+ }
109
+ // Models
110
+ console.log(`\n${BOLD}Models:${RESET}`);
111
+ const { CONFIG } = await import('./config.js');
112
+ printStatus(true, 'Review model', CONFIG.REVIEW_MODEL);
113
+ printStatus(true, 'Execute model', CONFIG.EXECUTE_MODEL);
114
+ // Notion connectivity
115
+ console.log(`\n${BOLD}Notion:${RESET}`);
116
+ if (hasToken) {
117
+ try {
118
+ const { Client } = await import('@notionhq/client');
119
+ const client = new Client({ auth: notionToken });
120
+ try {
121
+ await client.users.me({});
122
+ printStatus(true, 'Token valid', 'connected to workspace');
123
+ track(true);
124
+ }
125
+ catch (e) {
126
+ const msg = e instanceof Error ? e.message : String(e);
127
+ printStatus(false, 'Token valid', msg);
128
+ track(false);
129
+ }
130
+ if (hasDbId) {
131
+ try {
132
+ const db = await client.databases.retrieve({ database_id: dbId });
133
+ printStatus(true, 'Database accessible');
134
+ track(true);
135
+ // Schema validation
136
+ console.log(`\n${BOLD}Database Schema:${RESET}`);
137
+ const requiredProps = [
138
+ { name: 'Name', altName: 'Title', expectedTypes: ['title'] },
139
+ { name: 'Status', expectedTypes: ['status'] },
140
+ { name: 'Project', expectedTypes: ['select', 'rich_text'] },
141
+ { name: 'Ease', expectedTypes: ['number'] },
142
+ { name: 'Confidence', expectedTypes: ['number'] },
143
+ { name: 'Spec', expectedTypes: ['rich_text'] },
144
+ { name: 'Impact', expectedTypes: ['rich_text'] },
145
+ { name: 'Branch', expectedTypes: ['rich_text'] },
146
+ { name: 'Cost', expectedTypes: ['rich_text'] },
147
+ { name: 'PR URL', expectedTypes: ['url', 'rich_text'] },
148
+ ];
149
+ let schemaOk = 0;
150
+ let schemaMissing = 0;
151
+ for (const req of requiredProps) {
152
+ const prop = db.properties[req.name] || (req.altName ? db.properties[req.altName] : undefined);
153
+ if (prop && req.expectedTypes.includes(prop.type)) {
154
+ schemaOk++;
155
+ }
156
+ else if (prop) {
157
+ printStatus(false, `Property "${req.name}"`, `found as ${prop.type}, expected ${req.expectedTypes.join(' or ')}`);
158
+ schemaMissing++;
159
+ track(false);
160
+ }
161
+ else {
162
+ printStatus(false, `Missing property: "${req.name}"`, `(${req.expectedTypes.join(' or ')})`);
163
+ schemaMissing++;
164
+ track(false);
165
+ }
166
+ }
167
+ if (schemaMissing === 0) {
168
+ printStatus(true, `All ${requiredProps.length} required properties found`);
169
+ track(true);
170
+ }
171
+ else {
172
+ printStatus(false, `${schemaMissing} properties missing or misconfigured`);
173
+ }
174
+ // Check Project select options vs projects.json
175
+ const projectProp = db.properties.Project;
176
+ if (projectProp?.type === 'select' && projectProp.select) {
177
+ const notionOptions = projectProp.select.options.map((o) => o.name);
178
+ const configProjects = getProjectNames();
179
+ const inNotionNotConfig = notionOptions.filter((n) => !configProjects.includes(n));
180
+ const inConfigNotNotion = configProjects.filter((n) => !notionOptions.includes(n));
181
+ if (inNotionNotConfig.length > 0) {
182
+ printStatus(null, 'Notion has projects not in projects.json', inNotionNotConfig.join(', '));
183
+ track(null);
184
+ }
185
+ if (inConfigNotNotion.length > 0) {
186
+ printStatus(null, 'projects.json has projects not in Notion', inConfigNotNotion.join(', '));
187
+ track(null);
188
+ }
189
+ if (inNotionNotConfig.length === 0 && inConfigNotNotion.length === 0 && configProjects.length > 0) {
190
+ printStatus(true, 'Project options match projects.json');
191
+ track(true);
192
+ }
193
+ }
194
+ }
195
+ catch (e) {
196
+ const msg = e instanceof Error ? e.message : String(e);
197
+ printStatus(false, 'Database accessible', msg);
198
+ track(false);
199
+ }
200
+ }
201
+ else {
202
+ printStatus(false, 'Database accessible', 'no database ID configured');
203
+ track(false);
204
+ }
205
+ }
206
+ catch {
207
+ printStatus(false, 'Notion client', 'failed to load @notionhq/client');
208
+ track(false);
209
+ printStatus(false, 'Database accessible', 'skipped');
210
+ track(false);
211
+ }
212
+ }
213
+ else {
214
+ printStatus(false, 'Token valid', 'no token configured');
215
+ track(false);
216
+ printStatus(false, 'Database accessible', 'skipped');
217
+ track(false);
218
+ }
219
+ // Tools
220
+ console.log(`\n${BOLD}Tools:${RESET}`);
221
+ const gh = checkCommand('gh --version');
222
+ if (gh.ok) {
223
+ printStatus(true, 'gh installed', gh.output.split('\n')[0]);
224
+ track(true);
225
+ }
226
+ else {
227
+ printStatus(null, 'gh not found', 'Install: brew install gh && gh auth login (required for automatic PR creation)');
228
+ track(null);
229
+ }
230
+ if (gh.ok) {
231
+ const ghAuth = checkCommand('gh auth status');
232
+ printStatus(ghAuth.ok, 'gh authenticated');
233
+ track(ghAuth.ok);
234
+ }
235
+ else {
236
+ printStatus(null, 'gh authenticated', 'skipped — gh not installed');
237
+ track(null);
238
+ }
239
+ const claude = checkCommand('claude --version');
240
+ if (claude.ok) {
241
+ printStatus(true, 'claude installed', claude.output.split('\n')[0]);
242
+ track(true);
243
+ }
244
+ else {
245
+ printStatus(false, 'claude not found', 'Install: npm i -g @anthropic-ai/claude-code — required, agents cannot run without it');
246
+ track(false);
247
+ }
248
+ // Projects
249
+ console.log(`\n${BOLD}Projects:${RESET}`);
250
+ const projectNames = getProjectNames();
251
+ if (projectNames.length === 0) {
252
+ printStatus(null, 'No projects configured', 'add projects to projects.json or run init');
253
+ track(null);
254
+ }
255
+ else {
256
+ for (const name of projectNames) {
257
+ const dir = getProjectDir(name);
258
+ const dirExists = existsSync(dir);
259
+ if (!dirExists) {
260
+ printStatus(false, `${name}`, `${dir} (directory not found)`);
261
+ track(false);
262
+ continue;
263
+ }
264
+ const gitExists = existsSync(join(dir, '.git'));
265
+ if (!gitExists) {
266
+ printStatus(false, `${name}`, `${dir} (not a git repo)`);
267
+ track(false);
268
+ continue;
269
+ }
270
+ const origin = checkCommand(`git -C ${shellEscape(dir)} remote get-url origin`);
271
+ if (!origin.ok) {
272
+ printStatus(false, `${name}`, `${dir} (no origin remote)`);
273
+ track(false);
274
+ continue;
275
+ }
276
+ printStatus(true, `${name}`, `${dir}`);
277
+ track(true);
278
+ }
279
+ }
280
+ // Summary
281
+ console.log(`\n${BOLD}Summary:${RESET} ${GREEN}${passed} passed${RESET}, ${YELLOW}${warnings} warnings${RESET}, ${RED}${failed} failed${RESET}`);
282
+ console.log(`${DIM}Docs: https://www.tickettopr.com${RESET}\n`);
283
+ process.exitCode = failed > 0 ? 1 : 0;
284
+ }
285
+ // -- Init --
286
+ export async function runInit() {
287
+ console.log(`\n${BOLD}TicketToPR Setup${RESET}\n`);
288
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
289
+ const envPath = join(CONFIG_DIR, '.env.local');
290
+ const projectsPath = join(CONFIG_DIR, 'projects.json');
291
+ // Load existing env values for defaults
292
+ let existingEnv = {};
293
+ try {
294
+ const content = readFileSync(envPath, 'utf-8');
295
+ for (const line of content.split('\n')) {
296
+ const trimmed = line.trim();
297
+ if (!trimmed || trimmed.startsWith('#'))
298
+ continue;
299
+ const eqIndex = trimmed.indexOf('=');
300
+ if (eqIndex === -1)
301
+ continue;
302
+ existingEnv[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
303
+ }
304
+ }
305
+ catch {
306
+ // No existing file
307
+ }
308
+ // Re-run detection
309
+ const envExists = existsSync(envPath);
310
+ const projectsExists = existsSync(projectsPath);
311
+ if (envExists && projectsExists) {
312
+ console.log(` ${YELLOW}Existing configuration detected${RESET}`);
313
+ const mode = await ask(rl, 'Update existing config or start fresh?', {
314
+ defaultValue: 'update',
315
+ validate: (v) => {
316
+ const lower = v.toLowerCase();
317
+ return lower === 'update' || lower === 'fresh' ? null : 'Choose: update / fresh';
318
+ },
319
+ });
320
+ if (mode.toLowerCase() === 'fresh') {
321
+ existingEnv = {};
322
+ console.log(` ${DIM}Starting from scratch${RESET}`);
323
+ }
324
+ else {
325
+ console.log(` ${DIM}Pre-filling from existing config${RESET}`);
326
+ }
327
+ console.log('');
328
+ }
329
+ try {
330
+ // Step 1: Notion
331
+ console.log(`${BOLD}Step 1: Notion${RESET}`);
332
+ // -- Notion token with validation loop --
333
+ let notionToken = '';
334
+ let notionClient = null;
335
+ const { Client } = await import('@notionhq/client');
336
+ while (true) {
337
+ const tokenDefault = existingEnv.NOTION_TOKEN ? mask(existingEnv.NOTION_TOKEN) : undefined;
338
+ let tokenInput = await ask(rl, 'Notion token', {
339
+ defaultValue: tokenDefault,
340
+ validate: (v) => (!v ? 'Token is required' : null),
341
+ });
342
+ // If user accepted the masked default, use the actual stored value
343
+ if (tokenInput === tokenDefault && existingEnv.NOTION_TOKEN) {
344
+ tokenInput = existingEnv.NOTION_TOKEN;
345
+ }
346
+ try {
347
+ const client = new Client({ auth: tokenInput });
348
+ const me = await client.users.me({});
349
+ const workspaceName = me.bot?.workspace_name || 'connected';
350
+ printStatus(true, 'Token valid', workspaceName);
351
+ notionToken = tokenInput;
352
+ notionClient = client;
353
+ break;
354
+ }
355
+ catch {
356
+ printStatus(false, 'Token invalid — check your integration token and try again');
357
+ }
358
+ }
359
+ // -- Database ID with validation loop --
360
+ let databaseId = '';
361
+ while (true) {
362
+ const dbDefault = existingEnv.NOTION_DATABASE_ID ? mask(existingEnv.NOTION_DATABASE_ID) : undefined;
363
+ let dbInput = await ask(rl, 'Database ID', {
364
+ defaultValue: dbDefault,
365
+ validate: (v) => (!v ? 'Database ID is required' : null),
366
+ });
367
+ if (dbInput === dbDefault && existingEnv.NOTION_DATABASE_ID) {
368
+ dbInput = existingEnv.NOTION_DATABASE_ID;
369
+ }
370
+ try {
371
+ const db = await notionClient.databases.retrieve({ database_id: dbInput });
372
+ const dbTitle = db.title?.map((t) => t.plain_text).join('') || 'untitled';
373
+ printStatus(true, 'Database accessible', dbTitle);
374
+ databaseId = dbInput;
375
+ break;
376
+ }
377
+ catch {
378
+ printStatus(false, 'Database not accessible — check the ID and make sure the integration is connected to this database');
379
+ }
380
+ }
381
+ console.log('');
382
+ // Step 2: Tools
383
+ console.log(`${BOLD}Step 2: Tools${RESET}`);
384
+ const claude = checkCommand('claude --version');
385
+ if (claude.ok) {
386
+ printStatus(true, 'claude', claude.output.split('\n')[0]);
387
+ }
388
+ else {
389
+ printStatus(false, 'Claude Code CLI not found');
390
+ console.log(` ${DIM}Install: npm i -g @anthropic-ai/claude-code${RESET}`);
391
+ console.log(` ${DIM}Then authenticate: claude (follow the prompts)${RESET}`);
392
+ console.log(` ${RED}This is required — agents cannot run without it.${RESET}`);
393
+ }
394
+ const gh = checkCommand('gh --version');
395
+ if (gh.ok) {
396
+ printStatus(true, 'gh', gh.output.split('\n')[0]);
397
+ const ghAuth = checkCommand('gh auth status');
398
+ printStatus(ghAuth.ok, 'gh authenticated', ghAuth.ok ? undefined : 'run: gh auth login');
399
+ }
400
+ else {
401
+ printStatus(null, 'GitHub CLI not found');
402
+ console.log(` ${DIM}Install: brew install gh && gh auth login${RESET}`);
403
+ console.log(` ${DIM}Required for automatic PR creation. Review/Execute still work without it.${RESET}`);
404
+ }
405
+ console.log('');
406
+ // Step 3: Models
407
+ console.log(`${BOLD}Step 3: Models${RESET}`);
408
+ console.log(` ${DIM}Choose which Claude model each agent uses.${RESET}`);
409
+ console.log(` ${DIM}Sonnet = fast/cheap, Opus = best quality, Haiku = fastest/cheapest${RESET}\n`);
410
+ const modelChoices = [
411
+ { label: 'sonnet', id: 'claude-sonnet-4-5-20250929' },
412
+ { label: 'opus', id: 'claude-opus-4-6' },
413
+ { label: 'haiku', id: 'claude-haiku-4-5-20251001' },
414
+ ];
415
+ const modelLabels = modelChoices.map((m) => m.label).join('/');
416
+ const reviewModelDefault = existingEnv.REVIEW_MODEL
417
+ ? modelChoices.find((m) => m.id === existingEnv.REVIEW_MODEL)?.label ?? existingEnv.REVIEW_MODEL
418
+ : 'sonnet';
419
+ const reviewModelInput = await ask(rl, `Review model (${modelLabels})`, {
420
+ defaultValue: reviewModelDefault,
421
+ validate: (v) => (modelChoices.some((m) => m.label === v || m.id === v) ? null : `Choose: ${modelLabels}`),
422
+ });
423
+ const reviewModel = modelChoices.find((m) => m.label === reviewModelInput || m.id === reviewModelInput)?.id ?? reviewModelInput;
424
+ const executeModelDefault = existingEnv.EXECUTE_MODEL
425
+ ? modelChoices.find((m) => m.id === existingEnv.EXECUTE_MODEL)?.label ?? existingEnv.EXECUTE_MODEL
426
+ : 'opus';
427
+ const executeModelInput = await ask(rl, `Execute model (${modelLabels})`, {
428
+ defaultValue: executeModelDefault,
429
+ validate: (v) => (modelChoices.some((m) => m.label === v || m.id === v) ? null : `Choose: ${modelLabels}`),
430
+ });
431
+ const executeModel = modelChoices.find((m) => m.label === executeModelInput || m.id === executeModelInput)?.id ?? executeModelInput;
432
+ printStatus(true, 'Review model', reviewModel);
433
+ printStatus(true, 'Execute model', executeModel);
434
+ console.log('');
435
+ // Step 4: Projects
436
+ console.log(`${BOLD}Step 4: Projects${RESET}`);
437
+ const projects = [];
438
+ let addMore = true;
439
+ while (addMore) {
440
+ const name = await ask(rl, 'Project name', {
441
+ validate: (v) => (!v ? 'Project name is required' : null),
442
+ });
443
+ const dir = await ask(rl, 'Directory', {
444
+ validate: (v) => {
445
+ if (!v)
446
+ return 'Directory is required';
447
+ if (!existsSync(v))
448
+ return `Directory not found: ${v}`;
449
+ return null;
450
+ },
451
+ });
452
+ // Validate git repo
453
+ const gitExists = existsSync(join(dir, '.git'));
454
+ if (gitExists) {
455
+ const origin = checkCommand(`git -C ${shellEscape(dir)} remote get-url origin`);
456
+ if (origin.ok) {
457
+ printStatus(true, 'Git repo', origin.output);
458
+ }
459
+ else {
460
+ printStatus(null, 'Git repo found but no origin remote', `run: git -C ${dir} remote add origin <url>`);
461
+ }
462
+ }
463
+ else {
464
+ printStatus(null, 'Not a git repo', `${dir} — you can init git later`);
465
+ }
466
+ const buildCmd = await ask(rl, 'Build command (optional)');
467
+ projects.push({ name, dir, buildCmd: buildCmd || undefined });
468
+ console.log('');
469
+ const another = await ask(rl, 'Add another project?', { defaultValue: 'N' });
470
+ addMore = another.toLowerCase() === 'y' || another.toLowerCase() === 'yes';
471
+ if (addMore)
472
+ console.log('');
473
+ }
474
+ // Free tier guard
475
+ if (projects.length > 1) {
476
+ const { isPro } = await import('./config.js');
477
+ if (!isPro()) {
478
+ console.log('');
479
+ console.log(` ${YELLOW}Free tier supports 1 project. You configured ${projects.length}.${RESET}`);
480
+ console.log(` ${DIM}Upgrade to Pro for unlimited projects, or remove extras.${RESET}`);
481
+ const keepAll = await ask(rl, 'Keep only the first project?', { defaultValue: 'Y' });
482
+ if (keepAll.toLowerCase() === 'y' || keepAll.toLowerCase() === 'yes') {
483
+ const removed = projects.splice(1);
484
+ console.log(` ${DIM}Kept "${projects[0].name}", removed: ${removed.map((p) => p.name).join(', ')}${RESET}`);
485
+ }
486
+ else {
487
+ console.log(` ${DIM}Keeping all ${projects.length} projects — startup will fail without a Pro license.${RESET}`);
488
+ }
489
+ }
490
+ }
491
+ console.log('');
492
+ // Step 5: Save
493
+ console.log(`${BOLD}Step 5: Save${RESET}`);
494
+ // Write .env.local
495
+ const envUpdates = {
496
+ NOTION_TOKEN: notionToken,
497
+ NOTION_DATABASE_ID: databaseId,
498
+ REVIEW_MODEL: reviewModel,
499
+ EXECUTE_MODEL: executeModel,
500
+ };
501
+ writeEnvFile(envPath, envUpdates);
502
+ printStatus(true, 'Wrote .env.local');
503
+ // Update projects.json
504
+ if (projects.length > 0) {
505
+ updateProjectsFile(projectsPath, projects);
506
+ printStatus(true, 'Updated projects.json');
507
+ }
508
+ console.log(`\n${BOLD}Ready!${RESET}`);
509
+ console.log(` Test: ${DIM}npx tsx index.ts doctor${RESET}`);
510
+ console.log(` Docs: ${DIM}https://www.tickettopr.com${RESET}\n`);
511
+ }
512
+ finally {
513
+ rl.close();
514
+ }
515
+ }
@@ -0,0 +1,76 @@
1
+ export declare function isPro(): boolean;
2
+ export declare const CONFIG: {
3
+ readonly POLL_INTERVAL_MS: 30000;
4
+ readonly COLUMNS: {
5
+ readonly REVIEW: "Review";
6
+ readonly SCORED: "Scored";
7
+ readonly EXECUTE: "Execute";
8
+ readonly IN_PROGRESS: "In Progress";
9
+ readonly DONE: "PR Ready";
10
+ readonly FAILED: "Failed";
11
+ };
12
+ readonly REVIEW_BUDGET_USD: 2;
13
+ readonly EXECUTE_BUDGET_USD: 15;
14
+ readonly REVIEW_MODEL: string;
15
+ readonly EXECUTE_MODEL: string;
16
+ readonly REVIEW_MAX_TURNS: 25;
17
+ readonly EXECUTE_MAX_TURNS: 50;
18
+ readonly STALE_LOCK_MS: number;
19
+ readonly MAX_CONCURRENT_AGENTS: number;
20
+ readonly FREE_MAX_PROJECTS: 1;
21
+ };
22
+ export declare const REVIEW_OUTPUT_SCHEMA: {
23
+ readonly type: "object";
24
+ readonly properties: {
25
+ readonly easeScore: {
26
+ readonly type: "number";
27
+ readonly minimum: 1;
28
+ readonly maximum: 10;
29
+ };
30
+ readonly confidenceScore: {
31
+ readonly type: "number";
32
+ readonly minimum: 1;
33
+ readonly maximum: 10;
34
+ };
35
+ readonly spec: {
36
+ readonly type: "string";
37
+ };
38
+ readonly impactReport: {
39
+ readonly type: "string";
40
+ };
41
+ readonly affectedFiles: {
42
+ readonly type: "array";
43
+ readonly items: {
44
+ readonly type: "string";
45
+ };
46
+ };
47
+ readonly risks: {
48
+ readonly type: "string";
49
+ };
50
+ };
51
+ readonly required: readonly ["easeScore", "confidenceScore", "spec", "impactReport", "affectedFiles"];
52
+ };
53
+ export interface NotionTicket {
54
+ id: string;
55
+ title: string;
56
+ project: string;
57
+ status: string;
58
+ }
59
+ export interface TicketDetails extends NotionTicket {
60
+ description: string;
61
+ bodyBlocks: string;
62
+ spec?: string;
63
+ impact?: string;
64
+ }
65
+ export interface ReviewOutput {
66
+ easeScore: number;
67
+ confidenceScore: number;
68
+ spec: string;
69
+ impactReport: string;
70
+ affectedFiles: string[];
71
+ risks?: string;
72
+ }
73
+ export interface LockEntry {
74
+ mode: 'review' | 'execute';
75
+ startedAt: number;
76
+ }
package/dist/config.js ADDED
@@ -0,0 +1,80 @@
1
+ // -- License --
2
+ import { verify } from 'node:crypto';
3
+ const LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
4
+ MCowBQYDK2VwAyEAGtiFnwyCAHWl1b1yzm2wY14LiY8e0xfsXhQULcRaStM=
5
+ -----END PUBLIC KEY-----`;
6
+ let _proCache = null;
7
+ export function isPro() {
8
+ if (_proCache !== null)
9
+ return _proCache;
10
+ const key = process.env.LICENSE_KEY;
11
+ if (!key?.startsWith('ncb_pro_')) {
12
+ _proCache = false;
13
+ return false;
14
+ }
15
+ const rest = key.slice('ncb_pro_'.length);
16
+ const dotIndex = rest.indexOf('.');
17
+ if (dotIndex === -1) {
18
+ _proCache = false;
19
+ return false;
20
+ }
21
+ try {
22
+ const buyerId = Buffer.from(rest.slice(0, dotIndex), 'base64url');
23
+ const signature = Buffer.from(rest.slice(dotIndex + 1), 'base64url');
24
+ _proCache = verify(null, buyerId, LICENSE_PUBLIC_KEY, signature);
25
+ }
26
+ catch {
27
+ _proCache = false;
28
+ }
29
+ return _proCache;
30
+ }
31
+ const FREE_MAX_PROJECTS = 1;
32
+ const FREE_MAX_CONCURRENT = 1;
33
+ const PRO_MAX_CONCURRENT = 10;
34
+ export const CONFIG = {
35
+ // Polling
36
+ POLL_INTERVAL_MS: 30_000,
37
+ // Notion column names -> agent modes
38
+ COLUMNS: {
39
+ REVIEW: 'Review',
40
+ SCORED: 'Scored',
41
+ EXECUTE: 'Execute',
42
+ IN_PROGRESS: 'In Progress',
43
+ DONE: 'PR Ready',
44
+ FAILED: 'Failed',
45
+ },
46
+ // Agent budgets
47
+ REVIEW_BUDGET_USD: 2.00,
48
+ EXECUTE_BUDGET_USD: 15.00,
49
+ // Agent models (env override → default)
50
+ get REVIEW_MODEL() {
51
+ return process.env.REVIEW_MODEL || 'claude-sonnet-4-5-20250929';
52
+ },
53
+ get EXECUTE_MODEL() {
54
+ return process.env.EXECUTE_MODEL || 'claude-opus-4-6';
55
+ },
56
+ // Agent limits
57
+ REVIEW_MAX_TURNS: 25,
58
+ EXECUTE_MAX_TURNS: 50,
59
+ // Stale lock timeout (30 minutes)
60
+ STALE_LOCK_MS: 30 * 60 * 1000,
61
+ // Maximum concurrent agents (review + execute combined)
62
+ get MAX_CONCURRENT_AGENTS() {
63
+ return isPro() ? PRO_MAX_CONCURRENT : FREE_MAX_CONCURRENT;
64
+ },
65
+ // Free tier project limit
66
+ FREE_MAX_PROJECTS,
67
+ };
68
+ // JSON schema for review agent structured output
69
+ export const REVIEW_OUTPUT_SCHEMA = {
70
+ type: 'object',
71
+ properties: {
72
+ easeScore: { type: 'number', minimum: 1, maximum: 10 },
73
+ confidenceScore: { type: 'number', minimum: 1, maximum: 10 },
74
+ spec: { type: 'string' },
75
+ impactReport: { type: 'string' },
76
+ affectedFiles: { type: 'array', items: { type: 'string' } },
77
+ risks: { type: 'string' },
78
+ },
79
+ required: ['easeScore', 'confidenceScore', 'spec', 'impactReport', 'affectedFiles'],
80
+ };
@@ -0,0 +1 @@
1
+ export {};