@telepat/snoopy 0.1.13 → 0.1.15

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 (36) hide show
  1. package/README.md +70 -215
  2. package/README.zh-CN.md +137 -0
  3. package/dist/src/agent/install.d.ts +18 -0
  4. package/dist/src/agent/install.js +488 -0
  5. package/dist/src/cli/commands/feedback.d.ts +18 -0
  6. package/dist/src/cli/commands/feedback.js +276 -0
  7. package/dist/src/cli/commands/prompt.d.ts +6 -0
  8. package/dist/src/cli/commands/prompt.js +92 -0
  9. package/dist/src/cli/commands/promptEditor.d.ts +1 -0
  10. package/dist/src/cli/commands/promptEditor.js +17 -0
  11. package/dist/src/cli/flows/jobAddFlow.js +1 -1
  12. package/dist/src/cli/index.js +86 -1
  13. package/dist/src/mcp/helpers.d.ts +46 -0
  14. package/dist/src/mcp/helpers.js +506 -0
  15. package/dist/src/mcp/server.d.ts +1 -0
  16. package/dist/src/mcp/server.js +299 -0
  17. package/dist/src/mcp/tools.d.ts +90 -0
  18. package/dist/src/mcp/tools.js +106 -0
  19. package/dist/src/services/db/migrations/002_feedback_fields.d.ts +7 -0
  20. package/dist/src/services/db/migrations/002_feedback_fields.js +22 -0
  21. package/dist/src/services/db/migrations/index.js +2 -1
  22. package/dist/src/services/db/repositories/jobsRepo.d.ts +2 -0
  23. package/dist/src/services/db/repositories/jobsRepo.js +15 -0
  24. package/dist/src/services/db/repositories/scanItemsRepo.d.ts +17 -0
  25. package/dist/src/services/db/repositories/scanItemsRepo.js +197 -2
  26. package/dist/src/services/feedback/consolidationService.d.ts +28 -0
  27. package/dist/src/services/feedback/consolidationService.js +124 -0
  28. package/dist/src/services/openrouter/client.d.ts +23 -0
  29. package/dist/src/services/openrouter/client.js +67 -0
  30. package/dist/src/types/settings.d.ts +1 -1
  31. package/dist/src/types/settings.js +1 -1
  32. package/dist/src/ui/components/MultilinePrompt.d.ts +10 -0
  33. package/dist/src/ui/components/MultilinePrompt.js +87 -0
  34. package/dist/src/ui/components/multilinePromptModel.d.ts +25 -0
  35. package/dist/src/ui/components/multilinePromptModel.js +76 -0
  36. package/package.json +4 -1
@@ -31,6 +31,8 @@ export class ScanItemsRepository {
31
31
  qualified: row.qualified === 1,
32
32
  viewed: row.viewed === 1,
33
33
  validated: row.validated === 1,
34
+ isValid: row.isValid === 1,
35
+ feedbackConsolidated: row.feedbackConsolidated === 1,
34
36
  processed: row.processed === 1,
35
37
  consumed: row.consumed === 1
36
38
  }));
@@ -63,6 +65,8 @@ export class ScanItemsRepository {
63
65
  id,
64
66
  job_id as jobId,
65
67
  run_id as runId,
68
+ type,
69
+ subreddit,
66
70
  author,
67
71
  title,
68
72
  body,
@@ -70,6 +74,9 @@ export class ScanItemsRepository {
70
74
  reddit_posted_at as redditPostedAt,
71
75
  viewed,
72
76
  validated,
77
+ is_valid as isValid,
78
+ is_valid_reason as isValidReason,
79
+ feedback_consolidated as feedbackConsolidated,
73
80
  processed,
74
81
  consumed,
75
82
  qualification_reason as qualificationReason,
@@ -84,6 +91,8 @@ export class ScanItemsRepository {
84
91
  ...row,
85
92
  viewed: row.viewed === 1,
86
93
  validated: row.validated === 1,
94
+ isValid: row.isValid === 1,
95
+ feedbackConsolidated: row.feedbackConsolidated === 1,
87
96
  processed: row.processed === 1,
88
97
  consumed: row.consumed === 1
89
98
  }));
@@ -95,6 +104,8 @@ export class ScanItemsRepository {
95
104
  id,
96
105
  job_id as jobId,
97
106
  run_id as runId,
107
+ type,
108
+ subreddit,
98
109
  author,
99
110
  title,
100
111
  body,
@@ -102,6 +113,9 @@ export class ScanItemsRepository {
102
113
  reddit_posted_at as redditPostedAt,
103
114
  viewed,
104
115
  validated,
116
+ is_valid as isValid,
117
+ is_valid_reason as isValidReason,
118
+ feedback_consolidated as feedbackConsolidated,
105
119
  processed,
106
120
  consumed,
107
121
  qualification_reason as qualificationReason,
@@ -117,6 +131,8 @@ export class ScanItemsRepository {
117
131
  ...row,
118
132
  viewed: row.viewed === 1,
119
133
  validated: row.validated === 1,
134
+ isValid: row.isValid === 1,
135
+ feedbackConsolidated: row.feedbackConsolidated === 1,
120
136
  processed: row.processed === 1,
121
137
  consumed: row.consumed === 1
122
138
  }));
@@ -139,6 +155,9 @@ export class ScanItemsRepository {
139
155
  qualified,
140
156
  viewed,
141
157
  validated,
158
+ is_valid as isValid,
159
+ is_valid_reason as isValidReason,
160
+ feedback_consolidated as feedbackConsolidated,
142
161
  processed,
143
162
  consumed,
144
163
  qualification_reason as qualificationReason,
@@ -180,6 +199,9 @@ export class ScanItemsRepository {
180
199
  qualified,
181
200
  viewed,
182
201
  validated,
202
+ is_valid as isValid,
203
+ is_valid_reason as isValidReason,
204
+ feedback_consolidated as feedbackConsolidated,
183
205
  processed,
184
206
  consumed,
185
207
  qualification_reason as qualificationReason,
@@ -298,6 +320,8 @@ export class ScanItemsRepository {
298
320
  id,
299
321
  job_id as jobId,
300
322
  run_id as runId,
323
+ type,
324
+ subreddit,
301
325
  author,
302
326
  title,
303
327
  body,
@@ -305,6 +329,9 @@ export class ScanItemsRepository {
305
329
  reddit_posted_at as redditPostedAt,
306
330
  viewed,
307
331
  validated,
332
+ is_valid as isValid,
333
+ is_valid_reason as isValidReason,
334
+ feedback_consolidated as feedbackConsolidated,
308
335
  processed,
309
336
  consumed,
310
337
  qualification_reason as qualificationReason,
@@ -326,10 +353,175 @@ export class ScanItemsRepository {
326
353
  ...row,
327
354
  viewed: row.viewed === 1,
328
355
  validated: row.validated === 1,
356
+ isValid: row.isValid === 1,
357
+ feedbackConsolidated: row.feedbackConsolidated === 1,
329
358
  processed: row.processed === 1,
330
359
  consumed: row.consumed === 1
331
360
  }));
332
361
  }
362
+ getQualifiedById(id) {
363
+ const rows = this.db
364
+ .prepare(`SELECT
365
+ id,
366
+ job_id as jobId,
367
+ run_id as runId,
368
+ type,
369
+ subreddit,
370
+ author,
371
+ title,
372
+ body,
373
+ url,
374
+ reddit_posted_at as redditPostedAt,
375
+ viewed,
376
+ validated,
377
+ is_valid as isValid,
378
+ is_valid_reason as isValidReason,
379
+ feedback_consolidated as feedbackConsolidated,
380
+ processed,
381
+ consumed,
382
+ qualification_reason as qualificationReason,
383
+ created_at as createdAt
384
+ FROM scan_items
385
+ WHERE id = ?
386
+ AND qualified = 1
387
+ LIMIT 1`)
388
+ .all(id);
389
+ const row = rows[0];
390
+ if (!row) {
391
+ return null;
392
+ }
393
+ return {
394
+ ...row,
395
+ viewed: row.viewed === 1,
396
+ validated: row.validated === 1,
397
+ isValid: row.isValid === 1,
398
+ feedbackConsolidated: row.feedbackConsolidated === 1,
399
+ processed: row.processed === 1,
400
+ consumed: row.consumed === 1
401
+ };
402
+ }
403
+ listUnvalidatedQualified(jobId, limit = 10) {
404
+ const boundedLimit = Math.max(1, Math.floor(limit));
405
+ const rows = this.db
406
+ .prepare(`SELECT
407
+ id,
408
+ job_id as jobId,
409
+ run_id as runId,
410
+ type,
411
+ subreddit,
412
+ author,
413
+ title,
414
+ body,
415
+ url,
416
+ reddit_posted_at as redditPostedAt,
417
+ viewed,
418
+ validated,
419
+ is_valid as isValid,
420
+ is_valid_reason as isValidReason,
421
+ feedback_consolidated as feedbackConsolidated,
422
+ processed,
423
+ consumed,
424
+ qualification_reason as qualificationReason,
425
+ created_at as createdAt
426
+ FROM scan_items
427
+ WHERE qualified = 1
428
+ AND validated = 0
429
+ ${jobId ? 'AND job_id = ?' : ''}
430
+ ORDER BY datetime(created_at) DESC, id DESC
431
+ LIMIT ?`)
432
+ .all(...(jobId ? [jobId, boundedLimit] : [boundedLimit]));
433
+ return rows.map((row) => ({
434
+ ...row,
435
+ viewed: row.viewed === 1,
436
+ validated: row.validated === 1,
437
+ isValid: row.isValid === 1,
438
+ feedbackConsolidated: row.feedbackConsolidated === 1,
439
+ processed: row.processed === 1,
440
+ consumed: row.consumed === 1
441
+ }));
442
+ }
443
+ submitFeedback(resultId, isValid, reason) {
444
+ const normalizedReason = reason?.trim() ?? null;
445
+ const result = this.db
446
+ .prepare(`UPDATE scan_items
447
+ SET validated = 1,
448
+ is_valid = ?,
449
+ is_valid_reason = ?,
450
+ feedback_consolidated = 0
451
+ WHERE id = ?
452
+ AND qualified = 1`)
453
+ .run(isValid ? 1 : 0, normalizedReason, resultId);
454
+ return Number(result.changes) > 0;
455
+ }
456
+ listPendingFeedbackConsolidation(jobId, limit) {
457
+ const hasLimit = limit !== undefined && limit !== null;
458
+ const boundedLimit = hasLimit ? Math.max(1, Math.floor(limit)) : undefined;
459
+ let query = `SELECT
460
+ id,
461
+ job_id as jobId,
462
+ run_id as runId,
463
+ type,
464
+ subreddit,
465
+ author,
466
+ title,
467
+ body,
468
+ url,
469
+ reddit_posted_at as redditPostedAt,
470
+ viewed,
471
+ validated,
472
+ is_valid as isValid,
473
+ is_valid_reason as isValidReason,
474
+ feedback_consolidated as feedbackConsolidated,
475
+ processed,
476
+ consumed,
477
+ qualification_reason as qualificationReason,
478
+ created_at as createdAt
479
+ FROM scan_items
480
+ WHERE qualified = 1
481
+ AND validated = 1
482
+ AND feedback_consolidated = 0`;
483
+ const params = [];
484
+ if (jobId) {
485
+ query += ' AND job_id = ?';
486
+ params.push(jobId);
487
+ }
488
+ query += ' ORDER BY datetime(created_at) DESC, id DESC';
489
+ if (boundedLimit !== undefined) {
490
+ query += ' LIMIT ?';
491
+ params.push(boundedLimit);
492
+ }
493
+ const rows = this.db.prepare(query).all(...params);
494
+ return rows.map((row) => ({
495
+ ...row,
496
+ viewed: row.viewed === 1,
497
+ validated: row.validated === 1,
498
+ isValid: row.isValid === 1,
499
+ feedbackConsolidated: row.feedbackConsolidated === 1,
500
+ processed: row.processed === 1,
501
+ consumed: row.consumed === 1
502
+ }));
503
+ }
504
+ countPendingFeedbackConsolidation(jobId) {
505
+ const row = this.db
506
+ .prepare(`SELECT COUNT(*) as count
507
+ FROM scan_items
508
+ WHERE qualified = 1
509
+ AND validated = 1
510
+ AND feedback_consolidated = 0
511
+ ${jobId ? 'AND job_id = ?' : ''}`)
512
+ .get(...(jobId ? [jobId] : []));
513
+ return Number(row?.count ?? 0);
514
+ }
515
+ markFeedbackConsolidated(ids) {
516
+ if (ids.length === 0) {
517
+ return 0;
518
+ }
519
+ const placeholders = ids.map(() => '?').join(',');
520
+ const result = this.db
521
+ .prepare(`UPDATE scan_items SET feedback_consolidated = 1 WHERE id IN (${placeholders})`)
522
+ .run(...ids);
523
+ return Number(result.changes);
524
+ }
333
525
  markConsumed(ids) {
334
526
  if (ids.length === 0) {
335
527
  return 0;
@@ -360,6 +552,9 @@ export class ScanItemsRepository {
360
552
  qualified,
361
553
  viewed,
362
554
  validated,
555
+ is_valid,
556
+ is_valid_reason,
557
+ feedback_consolidated,
363
558
  processed,
364
559
  consumed,
365
560
  prompt_tokens,
@@ -367,8 +562,8 @@ export class ScanItemsRepository {
367
562
  estimated_cost_usd,
368
563
  qualification_reason,
369
564
  created_at
370
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
371
- .run(newId, newItem.jobId, newItem.runId, newItem.type, newItem.redditPostId, newItem.redditCommentId, newItem.subreddit, newItem.author, newItem.title, newItem.body, newItem.url, newItem.redditPostedAt, newItem.qualified ? 1 : 0, newItem.viewed ? 1 : 0, newItem.validated ? 1 : 0, newItem.processed ? 1 : 0, newItem.consumed ? 1 : 0, newItem.promptTokens ?? 0, newItem.completionTokens ?? 0, newItem.estimatedCostUsd ?? null, newItem.qualificationReason);
565
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
566
+ .run(newId, newItem.jobId, newItem.runId, newItem.type, newItem.redditPostId, newItem.redditCommentId, newItem.subreddit, newItem.author, newItem.title, newItem.body, newItem.url, newItem.redditPostedAt, newItem.qualified ? 1 : 0, newItem.viewed ? 1 : 0, newItem.validated ? 1 : 0, newItem.isValid ? 1 : 0, newItem.isValidReason ?? null, newItem.feedbackConsolidated ? 1 : 0, newItem.processed ? 1 : 0, newItem.consumed ? 1 : 0, newItem.promptTokens ?? 0, newItem.completionTokens ?? 0, newItem.estimatedCostUsd ?? null, newItem.qualificationReason);
372
567
  if (insertResult.changes === 0) {
373
568
  const existingId = this.findExistingScanItemId(newItem.jobId, newItem.redditPostId, newItem.redditCommentId);
374
569
  return {
@@ -0,0 +1,28 @@
1
+ export interface ConsolidateFeedbackOptions {
2
+ jobRef?: string;
3
+ limit?: number;
4
+ }
5
+ export interface ConsolidateFeedbackJobResult {
6
+ jobId: string;
7
+ jobSlug: string;
8
+ jobName: string;
9
+ pendingCount: number;
10
+ consolidatedCount: number;
11
+ promptUpdated: boolean;
12
+ oldPrompt?: string;
13
+ newPrompt?: string;
14
+ changeSummary?: string[];
15
+ rationale?: string;
16
+ promptTokens?: number;
17
+ completionTokens?: number;
18
+ error?: string;
19
+ }
20
+ export interface ConsolidateFeedbackResult {
21
+ jobRef?: string;
22
+ totalPendingBefore: number;
23
+ totalPendingAfter: number;
24
+ totalConsolidated: number;
25
+ requiresConsolidation: boolean;
26
+ jobs: ConsolidateFeedbackJobResult[];
27
+ }
28
+ export declare function consolidateFeedback(options?: ConsolidateFeedbackOptions): Promise<ConsolidateFeedbackResult>;
@@ -0,0 +1,124 @@
1
+ import { JobsRepository } from '../db/repositories/jobsRepo.js';
2
+ import { ScanItemsRepository } from '../db/repositories/scanItemsRepo.js';
3
+ import { SettingsRepository } from '../db/repositories/settingsRepo.js';
4
+ import { getOpenRouterApiKey } from '../security/secretStore.js';
5
+ import { OpenRouterClient, } from '../openrouter/client.js';
6
+ function toFeedbackItem(row) {
7
+ return {
8
+ id: row.id,
9
+ type: row.type,
10
+ subreddit: row.subreddit,
11
+ title: row.title,
12
+ body: row.body,
13
+ qualificationReason: row.qualificationReason,
14
+ userIsValid: row.isValid,
15
+ userReason: row.isValidReason,
16
+ };
17
+ }
18
+ function toJobPromptResult(result) {
19
+ return {
20
+ changeSummary: result.changeSummary,
21
+ rationale: result.rationale,
22
+ promptTokens: result.promptTokens,
23
+ completionTokens: result.completionTokens,
24
+ };
25
+ }
26
+ export async function consolidateFeedback(options = {}) {
27
+ const jobsRepo = new JobsRepository();
28
+ const scanItemsRepo = new ScanItemsRepository();
29
+ let jobId;
30
+ if (options.jobRef) {
31
+ const job = jobsRepo.getByRef(options.jobRef);
32
+ if (!job) {
33
+ throw new Error(`Job not found: ${options.jobRef}`);
34
+ }
35
+ jobId = job.id;
36
+ }
37
+ const pendingRows = scanItemsRepo.listPendingFeedbackConsolidation(jobId, options.limit);
38
+ const totalPendingBefore = pendingRows.length;
39
+ if (totalPendingBefore === 0) {
40
+ return {
41
+ jobRef: options.jobRef,
42
+ totalPendingBefore: 0,
43
+ totalPendingAfter: 0,
44
+ totalConsolidated: 0,
45
+ requiresConsolidation: false,
46
+ jobs: [],
47
+ };
48
+ }
49
+ const apiKey = await getOpenRouterApiKey();
50
+ if (!apiKey) {
51
+ throw new Error('OpenRouter API key not configured. Run snoopy settings to configure it first.');
52
+ }
53
+ const settingsRepo = new SettingsRepository();
54
+ const appSettings = settingsRepo.getAppSettings();
55
+ const openRouterClient = new OpenRouterClient(apiKey);
56
+ const groupedByJob = new Map();
57
+ for (const row of pendingRows) {
58
+ const existing = groupedByJob.get(row.jobId) ?? [];
59
+ existing.push(row);
60
+ groupedByJob.set(row.jobId, existing);
61
+ }
62
+ const jobResults = [];
63
+ let totalConsolidated = 0;
64
+ for (const [currentJobId, rows] of groupedByJob) {
65
+ const job = jobsRepo.getById(currentJobId);
66
+ if (!job) {
67
+ jobResults.push({
68
+ jobId: currentJobId,
69
+ jobSlug: 'unknown',
70
+ jobName: 'Unknown job',
71
+ pendingCount: rows.length,
72
+ consolidatedCount: 0,
73
+ promptUpdated: false,
74
+ error: `Job no longer exists: ${currentJobId}`,
75
+ });
76
+ continue;
77
+ }
78
+ try {
79
+ const oldPrompt = job.qualificationPrompt;
80
+ const result = await openRouterClient.consolidateQualificationPrompt({
81
+ model: appSettings.model,
82
+ modelSettings: appSettings.modelSettings,
83
+ currentQualificationPrompt: oldPrompt,
84
+ feedbackItems: rows.map(toFeedbackItem),
85
+ });
86
+ const newPrompt = result.revisedQualificationPrompt;
87
+ jobsRepo.updateQualificationPromptById(job.id, newPrompt);
88
+ const consolidatedCount = scanItemsRepo.markFeedbackConsolidated(rows.map((row) => row.id));
89
+ totalConsolidated += consolidatedCount;
90
+ jobResults.push({
91
+ jobId: job.id,
92
+ jobSlug: job.slug,
93
+ jobName: job.name,
94
+ pendingCount: rows.length,
95
+ consolidatedCount,
96
+ promptUpdated: true,
97
+ oldPrompt,
98
+ newPrompt,
99
+ ...toJobPromptResult(result),
100
+ });
101
+ }
102
+ catch (error) {
103
+ jobResults.push({
104
+ jobId: job.id,
105
+ jobSlug: job.slug,
106
+ jobName: job.name,
107
+ pendingCount: rows.length,
108
+ consolidatedCount: 0,
109
+ promptUpdated: false,
110
+ error: error instanceof Error ? error.message : String(error),
111
+ });
112
+ }
113
+ }
114
+ const totalPendingAfter = scanItemsRepo.countPendingFeedbackConsolidation(jobId);
115
+ return {
116
+ jobRef: options.jobRef,
117
+ totalPendingBefore,
118
+ totalPendingAfter,
119
+ totalConsolidated,
120
+ requiresConsolidation: totalPendingAfter > 0,
121
+ jobs: jobResults,
122
+ };
123
+ }
124
+ //# sourceMappingURL=consolidationService.js.map
@@ -7,6 +7,23 @@ export interface QualificationResult {
7
7
  promptTokens: number;
8
8
  completionTokens: number;
9
9
  }
10
+ export interface ConsolidationFeedbackItem {
11
+ id: string;
12
+ type: 'post' | 'comment';
13
+ subreddit: string;
14
+ title: string | null;
15
+ body: string;
16
+ qualificationReason: string | null;
17
+ userIsValid: boolean;
18
+ userReason: string | null;
19
+ }
20
+ export interface ConsolidatedPromptResult {
21
+ revisedQualificationPrompt: string;
22
+ changeSummary: string[];
23
+ rationale: string;
24
+ promptTokens: number;
25
+ completionTokens: number;
26
+ }
10
27
  export interface OpenRouterTraceHooks {
11
28
  onRequest?: (operation: string, payload: unknown) => void;
12
29
  onResponse?: (operation: string, payload: unknown) => void;
@@ -51,6 +68,12 @@ export declare class OpenRouterClient {
51
68
  question: string;
52
69
  answer: string;
53
70
  }>, model: string): Promise<GeneratedJobSpec>;
71
+ consolidateQualificationPrompt(input: {
72
+ model: string;
73
+ modelSettings: ModelSettings;
74
+ currentQualificationPrompt: string;
75
+ feedbackItems: ConsolidationFeedbackItem[];
76
+ }): Promise<ConsolidatedPromptResult>;
54
77
  qualifyPost(input: QualifyPostRequest): Promise<QualificationResult>;
55
78
  qualifyCommentThread(input: QualifyCommentThreadRequest): Promise<QualificationResult>;
56
79
  private runQualification;
@@ -16,6 +16,11 @@ const qualifySchema = z.object({
16
16
  qualified: z.boolean(),
17
17
  reason: z.string().min(1).max(80)
18
18
  });
19
+ const consolidatePromptSchema = z.object({
20
+ revisedQualificationPrompt: z.string().min(8),
21
+ changeSummary: z.array(z.string().min(1)).max(10),
22
+ rationale: z.string().min(1)
23
+ });
19
24
  function toAlphaLabel(index) {
20
25
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
21
26
  let value = index;
@@ -241,6 +246,68 @@ export class OpenRouterClient {
241
246
  throw toOpenRouterError(error);
242
247
  }
243
248
  }
249
+ async consolidateQualificationPrompt(input) {
250
+ const feedbackJson = JSON.stringify(input.feedbackItems, null, 2);
251
+ const userMessage = [
252
+ 'Current qualification prompt:',
253
+ input.currentQualificationPrompt,
254
+ '',
255
+ 'User feedback examples (JSON):',
256
+ feedbackJson,
257
+ '',
258
+ 'Revise by extracting general, reusable rules from feedback patterns.',
259
+ 'Do not optimize for any single example or subreddit-specific edge case unless repeatedly supported.',
260
+ 'Prefer consolidating with existing lines over adding many new lines.',
261
+ 'Add a new line only when it represents a clearly distinct rule.',
262
+ 'Return JSON with keys: revisedQualificationPrompt, changeSummary, rationale.'
263
+ ].join('\n');
264
+ const systemMessage = [
265
+ 'You improve qualification prompts using user feedback.',
266
+ 'Goal: make minimal but meaningful edits that improve classification quality over time.',
267
+ 'Primary behavior: infer generally applicable decision rules, not one-off fixes.',
268
+ 'Rules:',
269
+ '- Keep the revised prompt concise and practical; remove redundancy where possible.',
270
+ '- Generalize from repeated patterns in feedback rather than specific single examples.',
271
+ '- Avoid overfitting to named entities, exact wording, or one result unless it represents a broader rule.',
272
+ '- Preserve existing intent unless feedback clearly indicates a problem.',
273
+ '- Consolidate edits into existing points when semantically compatible; do not append endlessly.',
274
+ '- Introduce new lines only for genuinely new criteria not already covered.',
275
+ '- Incorporate invalid-case reasons as clear disqualifiers when they reflect reusable logic.',
276
+ '- Do not mention JSON, tooling, or implementation details in the revised prompt.',
277
+ '- Return valid JSON only with: revisedQualificationPrompt, changeSummary, rationale.'
278
+ ].join('\n');
279
+ try {
280
+ const payload = {
281
+ model: input.model,
282
+ temperature: input.modelSettings.temperature,
283
+ top_p: input.modelSettings.topP,
284
+ max_tokens: Math.max(600, input.modelSettings.maxTokens),
285
+ response_format: {
286
+ type: 'json_object'
287
+ },
288
+ messages: [
289
+ { role: 'system', content: systemMessage },
290
+ { role: 'user', content: userMessage }
291
+ ]
292
+ };
293
+ this.traceRequest('chat.completions.create.consolidate_prompt', payload);
294
+ const completion = (await this.client.chat.completions.create(payload));
295
+ this.traceResponse('chat.completions.create.consolidate_prompt', completion);
296
+ const raw = completion.choices?.[0]?.message?.content ?? '';
297
+ const parsed = consolidatePromptSchema.parse(parseStructuredJson(raw));
298
+ return {
299
+ revisedQualificationPrompt: parsed.revisedQualificationPrompt,
300
+ changeSummary: parsed.changeSummary,
301
+ rationale: parsed.rationale,
302
+ promptTokens: completion.usage?.prompt_tokens ?? 0,
303
+ completionTokens: completion.usage?.completion_tokens ?? 0
304
+ };
305
+ }
306
+ catch (error) {
307
+ this.traceError('chat.completions.create.consolidate_prompt', error);
308
+ throw toOpenRouterError(error);
309
+ }
310
+ }
244
311
  async qualifyPost(input) {
245
312
  const userMessage = [`Post title: ${input.postTitle}`, '', `Post body: ${input.postBody}`].join('\n');
246
313
  return this.runQualification(input, userMessage);
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_MODEL = "moonshotai/kimi-k2.5";
1
+ export declare const DEFAULT_MODEL = "deepseek/deepseek-v4-pro";
2
2
  export declare const DEFAULT_CRON_INTERVAL_MINUTES = 30;
3
3
  export declare const DEFAULT_JOB_TIMEOUT_MS: number;
4
4
  export declare function intervalToCron(minutes: number): string;
@@ -1,4 +1,4 @@
1
- export const DEFAULT_MODEL = 'moonshotai/kimi-k2.5';
1
+ export const DEFAULT_MODEL = 'deepseek/deepseek-v4-pro';
2
2
  export const DEFAULT_CRON_INTERVAL_MINUTES = 30;
3
3
  export const DEFAULT_JOB_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
4
4
  export function intervalToCron(minutes) {
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface MultilinePromptProps {
3
+ label: string;
4
+ initialValue?: string;
5
+ rows?: number;
6
+ onSubmit: (value: string) => void;
7
+ onCancel: () => void;
8
+ }
9
+ export declare function MultilinePrompt({ label, initialValue, rows, onSubmit, onCancel, }: MultilinePromptProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { uiTheme } from '../theme.js';
5
+ import { backspaceAtOffset, deleteAtOffset, insertAtOffset, moveCursorHorizontal, moveCursorVertical, offsetToCursor, splitLines } from './multilinePromptModel.js';
6
+ function renderLineWithCursor(line, cursorColumn) {
7
+ const safeColumn = Math.max(0, Math.min(cursorColumn, line.length));
8
+ const before = line.slice(0, safeColumn);
9
+ const selectedChar = safeColumn < line.length ? line[safeColumn] : ' ';
10
+ const after = line.slice(safeColumn + (safeColumn < line.length ? 1 : 0));
11
+ return (_jsxs(Text, { color: uiTheme.ink.textPrimary, children: [before, _jsx(Text, { inverse: true, children: selectedChar }), after] }));
12
+ }
13
+ export function MultilinePrompt({ label, initialValue = '', rows = 8, onSubmit, onCancel, }) {
14
+ const [value, setValue] = useState(initialValue);
15
+ const [offset, setOffset] = useState(initialValue.length);
16
+ const [preferredColumn, setPreferredColumn] = useState(undefined);
17
+ const lines = useMemo(() => splitLines(value), [value]);
18
+ const cursor = useMemo(() => offsetToCursor(value, offset), [value, offset]);
19
+ const visibleRows = Math.max(3, rows);
20
+ const scrollTop = Math.max(0, Math.min(lines.length - visibleRows, cursor.line - Math.floor(visibleRows / 2)));
21
+ const visibleLines = lines.slice(scrollTop, scrollTop + visibleRows);
22
+ useInput((input, key) => {
23
+ if ((key.ctrl && input === 'c') || key.escape) {
24
+ onCancel();
25
+ return;
26
+ }
27
+ if (key.return) {
28
+ if (key.shift) {
29
+ const next = insertAtOffset(value, offset, '\n');
30
+ setValue(next.value);
31
+ setOffset(next.offset);
32
+ setPreferredColumn(undefined);
33
+ return;
34
+ }
35
+ onSubmit(value);
36
+ return;
37
+ }
38
+ if (key.leftArrow) {
39
+ setOffset((prev) => moveCursorHorizontal(value, prev, -1));
40
+ setPreferredColumn(undefined);
41
+ return;
42
+ }
43
+ if (key.rightArrow) {
44
+ setOffset((prev) => moveCursorHorizontal(value, prev, 1));
45
+ setPreferredColumn(undefined);
46
+ return;
47
+ }
48
+ if (key.upArrow) {
49
+ const moved = moveCursorVertical(value, offset, -1, preferredColumn);
50
+ setOffset(moved.nextOffset);
51
+ setPreferredColumn(moved.preferredColumn);
52
+ return;
53
+ }
54
+ if (key.downArrow) {
55
+ const moved = moveCursorVertical(value, offset, 1, preferredColumn);
56
+ setOffset(moved.nextOffset);
57
+ setPreferredColumn(moved.preferredColumn);
58
+ return;
59
+ }
60
+ if (key.backspace) {
61
+ const next = backspaceAtOffset(value, offset);
62
+ setValue(next.value);
63
+ setOffset(next.offset);
64
+ setPreferredColumn(undefined);
65
+ return;
66
+ }
67
+ if (key.delete) {
68
+ const next = deleteAtOffset(value, offset);
69
+ setValue(next.value);
70
+ setOffset(next.offset);
71
+ setPreferredColumn(undefined);
72
+ return;
73
+ }
74
+ if (input.length > 0 && !key.ctrl && !key.meta) {
75
+ const next = insertAtOffset(value, offset, input);
76
+ setValue(next.value);
77
+ setOffset(next.offset);
78
+ setPreferredColumn(undefined);
79
+ }
80
+ });
81
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: uiTheme.ink.accent, children: label }), _jsx(Box, { borderStyle: "single", borderColor: uiTheme.ink.info, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: visibleLines.map((line, index) => {
82
+ const absoluteLine = scrollTop + index;
83
+ const isCursorLine = absoluteLine === cursor.line;
84
+ return (_jsx(React.Fragment, { children: isCursorLine ? renderLineWithCursor(line, cursor.column) : (_jsx(Text, { color: uiTheme.ink.textPrimary, children: line || ' ' })) }, `${absoluteLine}:${line}`));
85
+ }) }), _jsx(Text, { color: uiTheme.ink.textMuted, children: "Enter submit | Shift+Enter newline | Arrow keys move cursor | Esc cancel" })] }));
86
+ }
87
+ //# sourceMappingURL=MultilinePrompt.js.map