@telepat/snoopy 0.1.14 → 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.
- package/README.md +34 -9
- package/README.zh-CN.md +34 -9
- package/dist/src/cli/commands/feedback.d.ts +18 -0
- package/dist/src/cli/commands/feedback.js +276 -0
- package/dist/src/cli/commands/prompt.d.ts +6 -0
- package/dist/src/cli/commands/prompt.js +92 -0
- package/dist/src/cli/commands/promptEditor.d.ts +1 -0
- package/dist/src/cli/commands/promptEditor.js +17 -0
- package/dist/src/cli/flows/jobAddFlow.js +1 -1
- package/dist/src/cli/index.js +49 -1
- package/dist/src/mcp/helpers.d.ts +3 -0
- package/dist/src/mcp/helpers.js +78 -5
- package/dist/src/mcp/server.js +41 -2
- package/dist/src/mcp/tools.d.ts +13 -0
- package/dist/src/mcp/tools.js +16 -0
- package/dist/src/services/db/migrations/002_feedback_fields.d.ts +7 -0
- package/dist/src/services/db/migrations/002_feedback_fields.js +22 -0
- package/dist/src/services/db/migrations/index.js +2 -1
- package/dist/src/services/db/repositories/jobsRepo.d.ts +2 -0
- package/dist/src/services/db/repositories/jobsRepo.js +15 -0
- package/dist/src/services/db/repositories/scanItemsRepo.d.ts +17 -0
- package/dist/src/services/db/repositories/scanItemsRepo.js +197 -2
- package/dist/src/services/feedback/consolidationService.d.ts +28 -0
- package/dist/src/services/feedback/consolidationService.js +124 -0
- package/dist/src/services/openrouter/client.d.ts +23 -0
- package/dist/src/services/openrouter/client.js +67 -0
- package/dist/src/types/settings.d.ts +1 -1
- package/dist/src/types/settings.js +1 -1
- package/dist/src/ui/components/MultilinePrompt.d.ts +10 -0
- package/dist/src/ui/components/MultilinePrompt.js +87 -0
- package/dist/src/ui/components/multilinePromptModel.d.ts +25 -0
- package/dist/src/ui/components/multilinePromptModel.js +76 -0
- package/package.json +3 -1
package/dist/src/mcp/helpers.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { getDb } from '../services/db/sqlite.js';
|
|
4
5
|
import { JobsRepository } from '../services/db/repositories/jobsRepo.js';
|
|
5
6
|
import { RunsRepository } from '../services/db/repositories/runsRepo.js';
|
|
6
7
|
import { ScanItemsRepository } from '../services/db/repositories/scanItemsRepo.js';
|
|
7
8
|
import { SettingsRepository } from '../services/db/repositories/settingsRepo.js';
|
|
9
|
+
import { consolidateFeedback } from '../services/feedback/consolidationService.js';
|
|
8
10
|
import { AnalyticsService } from '../services/analytics/analyticsService.js';
|
|
9
11
|
import { extractErrorEntries, readRunLog } from '../services/logging/logReader.js';
|
|
10
12
|
import { getOpenRouterApiKey, isKeytarAvailable } from '../services/security/secretStore.js';
|
|
11
13
|
import { getStartupStatus } from '../services/startup/index.js';
|
|
12
14
|
import { isDaemonRunning, ensureDaemonRunning, requestDaemonReload } from '../services/daemonControl.js';
|
|
13
15
|
import { ensureAppDirs } from '../utils/paths.js';
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
16
|
export function getSnoopyVersion() {
|
|
16
|
-
|
|
17
|
+
const envVersion = process.env.npm_package_version;
|
|
18
|
+
if (envVersion) {
|
|
19
|
+
return envVersion;
|
|
20
|
+
}
|
|
21
|
+
const argvDir = process.argv[1] ? path.dirname(process.argv[1]) : process.cwd();
|
|
22
|
+
const candidates = [
|
|
23
|
+
path.resolve(process.cwd(), 'package.json'),
|
|
24
|
+
path.resolve(process.cwd(), 'snoopy/package.json'),
|
|
25
|
+
path.resolve(argvDir, '../package.json'),
|
|
26
|
+
path.resolve(argvDir, '../../package.json'),
|
|
27
|
+
];
|
|
28
|
+
for (const candidate of candidates) {
|
|
17
29
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
30
|
+
const raw = fs.readFileSync(candidate, 'utf8');
|
|
31
|
+
const pkg = JSON.parse(raw);
|
|
32
|
+
if (pkg.name === '@telepat/snoopy') {
|
|
20
33
|
return pkg.version ?? '0.0.0';
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
catch { /* try next depth */ }
|
|
23
37
|
}
|
|
@@ -308,6 +322,65 @@ export function consumeReport(jobRef, limit, dryRun) {
|
|
|
308
322
|
const consumedCount = scanItemsRepo.markConsumed(rows.map((r) => r.id));
|
|
309
323
|
return { consumed: consumedCount, items: rows };
|
|
310
324
|
}
|
|
325
|
+
export function feedbackReviewReport(jobRef, limit) {
|
|
326
|
+
const jobsRepo = new JobsRepository();
|
|
327
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
328
|
+
let jobId;
|
|
329
|
+
if (jobRef) {
|
|
330
|
+
const job = jobsRepo.getByRef(jobRef);
|
|
331
|
+
if (!job) {
|
|
332
|
+
throw new Error(`Job not found: ${jobRef}`);
|
|
333
|
+
}
|
|
334
|
+
jobId = job.id;
|
|
335
|
+
}
|
|
336
|
+
const boundedLimit = limit ?? 10;
|
|
337
|
+
const items = scanItemsRepo.listUnvalidatedQualified(jobId, boundedLimit);
|
|
338
|
+
return {
|
|
339
|
+
count: items.length,
|
|
340
|
+
limit: boundedLimit,
|
|
341
|
+
items,
|
|
342
|
+
workflow: {
|
|
343
|
+
nextStep: 'collect-user-feedback-and-call-snoopy_feedback_submit',
|
|
344
|
+
then: 'call-snoopy_feedback_consolidate'
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
export function feedbackSubmitReport(resultId, isValid, reason) {
|
|
349
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
350
|
+
const row = scanItemsRepo.getQualifiedById(resultId);
|
|
351
|
+
if (!row) {
|
|
352
|
+
throw new Error(`Qualified result not found: ${resultId}`);
|
|
353
|
+
}
|
|
354
|
+
const normalizedReason = reason?.trim() ?? '';
|
|
355
|
+
if (!isValid && normalizedReason.length === 0) {
|
|
356
|
+
throw new Error('reason is required when isValid=false');
|
|
357
|
+
}
|
|
358
|
+
const updated = scanItemsRepo.submitFeedback(resultId, isValid, isValid ? null : normalizedReason);
|
|
359
|
+
if (!updated) {
|
|
360
|
+
throw new Error(`Failed to save feedback for result: ${resultId}`);
|
|
361
|
+
}
|
|
362
|
+
const pendingFeedbackConsolidationCount = scanItemsRepo.countPendingFeedbackConsolidation(row.jobId);
|
|
363
|
+
return {
|
|
364
|
+
resultId,
|
|
365
|
+
saved: true,
|
|
366
|
+
feedback: {
|
|
367
|
+
validated: true,
|
|
368
|
+
isValid,
|
|
369
|
+
isValidReason: isValid ? null : normalizedReason,
|
|
370
|
+
feedbackConsolidated: false,
|
|
371
|
+
},
|
|
372
|
+
pendingFeedbackConsolidationCount,
|
|
373
|
+
requiresConsolidation: pendingFeedbackConsolidationCount > 0,
|
|
374
|
+
recommendedNextCommand: 'snoopy feedback consolidate',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export async function feedbackConsolidateReport(jobRef, limit) {
|
|
378
|
+
const result = await consolidateFeedback({ jobRef, limit });
|
|
379
|
+
return {
|
|
380
|
+
...result,
|
|
381
|
+
recommendedNextAction: result.requiresConsolidation ? 'run snoopy feedback consolidate again' : 'none',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
311
384
|
export function errorsReport(jobRef, hours) {
|
|
312
385
|
const jobsRepo = new JobsRepository();
|
|
313
386
|
const runsRepo = new RunsRepository();
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { snoopyDoctorToolInputSchema, snoopyDaemonStatusToolInputSchema, snoopyDaemonStartToolInputSchema, snoopyDaemonStopToolInputSchema, snoopyDaemonReloadToolInputSchema, snoopyJobListToolInputSchema, snoopyJobRunsToolInputSchema, snoopyJobAddToolInputSchema, snoopyJobDeleteToolInputSchema, snoopyJobEnableToolInputSchema, snoopyJobDisableToolInputSchema, snoopyJobRunToolInputSchema, snoopyAnalyticsToolInputSchema, snoopyExportToolInputSchema, snoopyConsumeToolInputSchema, snoopyErrorsToolInputSchema, snoopyLogsToolInputSchema, snoopySettingsGetToolInputSchema, snoopySettingsSetToolInputSchema, } from './tools.js';
|
|
4
|
-
import { getSnoopyVersion, formatToolError, formatToolResult, buildDoctorReport, buildDaemonStatusReport, startDaemonReport, stopDaemonReport, reloadDaemonReport, listJobsReport, listJobRunsReport, addJobReport, deleteJobReport, enableJobReport, disableJobReport, runJobReport, analyticsReport, exportReport, consumeReport, errorsReport, logsReport, settingsGetReport, settingsSetReport, } from './helpers.js';
|
|
3
|
+
import { snoopyDoctorToolInputSchema, snoopyDaemonStatusToolInputSchema, snoopyDaemonStartToolInputSchema, snoopyDaemonStopToolInputSchema, snoopyDaemonReloadToolInputSchema, snoopyJobListToolInputSchema, snoopyJobRunsToolInputSchema, snoopyJobAddToolInputSchema, snoopyJobDeleteToolInputSchema, snoopyJobEnableToolInputSchema, snoopyJobDisableToolInputSchema, snoopyJobRunToolInputSchema, snoopyAnalyticsToolInputSchema, snoopyExportToolInputSchema, snoopyConsumeToolInputSchema, snoopyFeedbackReviewToolInputSchema, snoopyFeedbackSubmitToolInputSchema, snoopyFeedbackConsolidateToolInputSchema, snoopyErrorsToolInputSchema, snoopyLogsToolInputSchema, snoopySettingsGetToolInputSchema, snoopySettingsSetToolInputSchema, } from './tools.js';
|
|
4
|
+
import { getSnoopyVersion, formatToolError, formatToolResult, buildDoctorReport, buildDaemonStatusReport, startDaemonReport, stopDaemonReport, reloadDaemonReport, listJobsReport, listJobRunsReport, addJobReport, deleteJobReport, enableJobReport, disableJobReport, runJobReport, analyticsReport, exportReport, consumeReport, feedbackReviewReport, feedbackSubmitReport, feedbackConsolidateReport, errorsReport, logsReport, settingsGetReport, settingsSetReport, } from './helpers.js';
|
|
5
5
|
export async function startSnoopyMcpServer() {
|
|
6
6
|
const version = getSnoopyVersion();
|
|
7
7
|
const server = new McpServer({ name: 'snoopy', version });
|
|
@@ -201,6 +201,45 @@ export async function startSnoopyMcpServer() {
|
|
|
201
201
|
return formatToolError(error);
|
|
202
202
|
}
|
|
203
203
|
});
|
|
204
|
+
// --- snoopy_feedback_review ---
|
|
205
|
+
server.registerTool('snoopy_feedback_review', {
|
|
206
|
+
title: 'Review Feedback Queue',
|
|
207
|
+
description: 'List unvalidated qualified results for user feedback collection.',
|
|
208
|
+
inputSchema: snoopyFeedbackReviewToolInputSchema,
|
|
209
|
+
}, (input) => {
|
|
210
|
+
try {
|
|
211
|
+
return formatToolResult(feedbackReviewReport(input.jobRef, input.limit));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return formatToolError(error);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// --- snoopy_feedback_submit ---
|
|
218
|
+
server.registerTool('snoopy_feedback_submit', {
|
|
219
|
+
title: 'Submit Feedback',
|
|
220
|
+
description: 'Submit validity feedback for a qualified result.',
|
|
221
|
+
inputSchema: snoopyFeedbackSubmitToolInputSchema,
|
|
222
|
+
}, (input) => {
|
|
223
|
+
try {
|
|
224
|
+
return formatToolResult(feedbackSubmitReport(input.resultId, input.isValid, input.reason));
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
return formatToolError(error);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// --- snoopy_feedback_consolidate ---
|
|
231
|
+
server.registerTool('snoopy_feedback_consolidate', {
|
|
232
|
+
title: 'Consolidate Feedback',
|
|
233
|
+
description: 'Consolidate feedback into improved qualification prompts.',
|
|
234
|
+
inputSchema: snoopyFeedbackConsolidateToolInputSchema,
|
|
235
|
+
}, async (input) => {
|
|
236
|
+
try {
|
|
237
|
+
return formatToolResult(await feedbackConsolidateReport(input.jobRef, input.limit));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return formatToolError(error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
204
243
|
// --- snoopy_errors ---
|
|
205
244
|
server.registerTool('snoopy_errors', {
|
|
206
245
|
title: 'Recent Errors',
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -49,6 +49,19 @@ export declare const snoopyConsumeToolInputSchema: {
|
|
|
49
49
|
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
50
50
|
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
51
51
|
};
|
|
52
|
+
export declare const snoopyFeedbackReviewToolInputSchema: {
|
|
53
|
+
jobRef: z.ZodOptional<z.ZodString>;
|
|
54
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
55
|
+
};
|
|
56
|
+
export declare const snoopyFeedbackSubmitToolInputSchema: {
|
|
57
|
+
resultId: z.ZodString;
|
|
58
|
+
isValid: z.ZodBoolean;
|
|
59
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
60
|
+
};
|
|
61
|
+
export declare const snoopyFeedbackConsolidateToolInputSchema: {
|
|
62
|
+
jobRef: z.ZodOptional<z.ZodString>;
|
|
63
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
64
|
+
};
|
|
52
65
|
export declare const snoopyErrorsToolInputSchema: {
|
|
53
66
|
jobRef: z.ZodString;
|
|
54
67
|
hours: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -46,6 +46,19 @@ export const snoopyConsumeToolInputSchema = {
|
|
|
46
46
|
limit: z.coerce.number().int().positive().optional().describe('Max results to consume.'),
|
|
47
47
|
dryRun: z.boolean().optional().describe('Preview without marking consumed.'),
|
|
48
48
|
};
|
|
49
|
+
export const snoopyFeedbackReviewToolInputSchema = {
|
|
50
|
+
jobRef: z.string().optional().describe('Job ID or slug. If omitted, review queue spans all jobs.'),
|
|
51
|
+
limit: z.coerce.number().int().positive().optional().describe('Max unvalidated results to return. Default: 10.'),
|
|
52
|
+
};
|
|
53
|
+
export const snoopyFeedbackSubmitToolInputSchema = {
|
|
54
|
+
resultId: z.string().min(1).describe('Qualified result ID to update.'),
|
|
55
|
+
isValid: z.boolean().describe('Whether the result should be considered valid.'),
|
|
56
|
+
reason: z.string().optional().describe('Required when isValid=false; ignored when isValid=true.'),
|
|
57
|
+
};
|
|
58
|
+
export const snoopyFeedbackConsolidateToolInputSchema = {
|
|
59
|
+
jobRef: z.string().optional().describe('Job ID or slug. If omitted, consolidates across all jobs.'),
|
|
60
|
+
limit: z.coerce.number().int().positive().optional().describe('Max pending feedback items to process.'),
|
|
61
|
+
};
|
|
49
62
|
export const snoopyErrorsToolInputSchema = {
|
|
50
63
|
jobRef: z.string().min(1).describe('Job ID or slug.'),
|
|
51
64
|
hours: z.coerce.number().int().positive().optional().describe('Look back hours. Default: 24.'),
|
|
@@ -82,6 +95,9 @@ export const snoopyToolContracts = [
|
|
|
82
95
|
{ name: 'snoopy_analytics', required: [], enums: {} },
|
|
83
96
|
{ name: 'snoopy_export', required: [], enums: { format: ['json', 'csv'] } },
|
|
84
97
|
{ name: 'snoopy_consume', required: [], enums: {} },
|
|
98
|
+
{ name: 'snoopy_feedback_review', required: [], enums: {} },
|
|
99
|
+
{ name: 'snoopy_feedback_submit', required: ['resultId', 'isValid'], enums: {} },
|
|
100
|
+
{ name: 'snoopy_feedback_consolidate', required: [], enums: {} },
|
|
85
101
|
{ name: 'snoopy_errors', required: ['jobRef'], enums: {} },
|
|
86
102
|
{ name: 'snoopy_logs', required: ['runId'], enums: {} },
|
|
87
103
|
{ name: 'snoopy_settings_get', required: [], enums: {} },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
id: 2,
|
|
3
|
+
name: 'feedback_fields',
|
|
4
|
+
up(db) {
|
|
5
|
+
const safeAddColumn = (table, column, type) => {
|
|
6
|
+
try {
|
|
7
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Column already exists or table does not exist.
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
safeAddColumn('scan_items', 'is_valid', 'INTEGER NOT NULL DEFAULT 0');
|
|
14
|
+
safeAddColumn('scan_items', 'is_valid_reason', 'TEXT');
|
|
15
|
+
safeAddColumn('scan_items', 'feedback_consolidated', 'INTEGER NOT NULL DEFAULT 0');
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_scan_items_feedback_pending
|
|
18
|
+
ON scan_items(job_id, qualified, validated, feedback_consolidated, created_at DESC)
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=002_feedback_fields.js.map
|
|
@@ -28,6 +28,8 @@ export declare class JobsRepository {
|
|
|
28
28
|
listEnabled(): Job[];
|
|
29
29
|
setEnabled(id: string, enabled: boolean): void;
|
|
30
30
|
setEnabledByRef(ref: string, enabled: boolean): Job | null;
|
|
31
|
+
updateQualificationPromptById(id: string, qualificationPrompt: string): Job | null;
|
|
32
|
+
updateQualificationPromptByRef(ref: string, qualificationPrompt: string): Job | null;
|
|
31
33
|
remove(id: string): void;
|
|
32
34
|
removeByRef(ref: string): Job | null;
|
|
33
35
|
listWithStats(): JobSummaryRow[];
|
|
@@ -141,6 +141,21 @@ export class JobsRepository {
|
|
|
141
141
|
this.setEnabled(job.id, enabled);
|
|
142
142
|
return this.getById(job.id);
|
|
143
143
|
}
|
|
144
|
+
updateQualificationPromptById(id, qualificationPrompt) {
|
|
145
|
+
this.db
|
|
146
|
+
.prepare(`UPDATE jobs
|
|
147
|
+
SET qualification_prompt = ?, updated_at = datetime('now')
|
|
148
|
+
WHERE id = ?`)
|
|
149
|
+
.run(qualificationPrompt, id);
|
|
150
|
+
return this.getById(id);
|
|
151
|
+
}
|
|
152
|
+
updateQualificationPromptByRef(ref, qualificationPrompt) {
|
|
153
|
+
const job = this.getByRef(ref);
|
|
154
|
+
if (!job) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return this.updateQualificationPromptById(job.id, qualificationPrompt);
|
|
158
|
+
}
|
|
144
159
|
remove(id) {
|
|
145
160
|
this.removeCascadeStmt(id);
|
|
146
161
|
}
|
|
@@ -14,6 +14,9 @@ export interface NewScanItem {
|
|
|
14
14
|
qualified: boolean;
|
|
15
15
|
viewed?: boolean;
|
|
16
16
|
validated?: boolean;
|
|
17
|
+
isValid?: boolean;
|
|
18
|
+
isValidReason?: string | null;
|
|
19
|
+
feedbackConsolidated?: boolean;
|
|
17
20
|
processed?: boolean;
|
|
18
21
|
consumed?: boolean;
|
|
19
22
|
promptTokens?: number;
|
|
@@ -39,6 +42,8 @@ export interface QualifiedScanItemRow {
|
|
|
39
42
|
id: string;
|
|
40
43
|
jobId: string;
|
|
41
44
|
runId: string;
|
|
45
|
+
type: ScanItemType;
|
|
46
|
+
subreddit: string;
|
|
42
47
|
author: string;
|
|
43
48
|
title: string | null;
|
|
44
49
|
body: string;
|
|
@@ -46,6 +51,9 @@ export interface QualifiedScanItemRow {
|
|
|
46
51
|
redditPostedAt: string;
|
|
47
52
|
viewed: boolean;
|
|
48
53
|
validated: boolean;
|
|
54
|
+
isValid: boolean;
|
|
55
|
+
isValidReason: string | null;
|
|
56
|
+
feedbackConsolidated: boolean;
|
|
49
57
|
processed: boolean;
|
|
50
58
|
consumed: boolean;
|
|
51
59
|
qualificationReason: string | null;
|
|
@@ -67,6 +75,9 @@ export interface ScanItemRow {
|
|
|
67
75
|
qualified: boolean;
|
|
68
76
|
viewed: boolean;
|
|
69
77
|
validated: boolean;
|
|
78
|
+
isValid: boolean;
|
|
79
|
+
isValidReason: string | null;
|
|
80
|
+
feedbackConsolidated: boolean;
|
|
70
81
|
processed: boolean;
|
|
71
82
|
consumed: boolean;
|
|
72
83
|
qualificationReason: string | null;
|
|
@@ -117,6 +128,12 @@ export declare class ScanItemsRepository {
|
|
|
117
128
|
listAnalyticsByJob(days: number): AnalyticsByJobRow[];
|
|
118
129
|
existsComment(jobId: string, postId: string, commentId: string): boolean;
|
|
119
130
|
listUnconsumedQualified(jobId?: string, limit?: number): QualifiedScanItemRow[];
|
|
131
|
+
getQualifiedById(id: string): QualifiedScanItemRow | null;
|
|
132
|
+
listUnvalidatedQualified(jobId?: string, limit?: number): QualifiedScanItemRow[];
|
|
133
|
+
submitFeedback(resultId: string, isValid: boolean, reason: string | null): boolean;
|
|
134
|
+
listPendingFeedbackConsolidation(jobId?: string, limit?: number): QualifiedScanItemRow[];
|
|
135
|
+
countPendingFeedbackConsolidation(jobId?: string): number;
|
|
136
|
+
markFeedbackConsolidated(ids: string[]): number;
|
|
120
137
|
markConsumed(ids: string[]): number;
|
|
121
138
|
createWithStatus(item: NewScanItem): CreateScanItemResult;
|
|
122
139
|
create(item: NewScanItem): string;
|
|
@@ -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>;
|