@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.
- package/README.md +70 -215
- package/README.zh-CN.md +137 -0
- package/dist/src/agent/install.d.ts +18 -0
- package/dist/src/agent/install.js +488 -0
- 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 +86 -1
- package/dist/src/mcp/helpers.d.ts +46 -0
- package/dist/src/mcp/helpers.js +506 -0
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +299 -0
- package/dist/src/mcp/tools.d.ts +90 -0
- package/dist/src/mcp/tools.js +106 -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 +4 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { diff } from 'jest-diff';
|
|
3
|
+
import { JobsRepository } from '../../services/db/repositories/jobsRepo.js';
|
|
4
|
+
import { ScanItemsRepository } from '../../services/db/repositories/scanItemsRepo.js';
|
|
5
|
+
import { consolidateFeedback } from '../../services/feedback/consolidationService.js';
|
|
6
|
+
import { isRichTty, printCommandScreen, printError, printInfo, printKeyValue, printMuted, printSection, printSuccess, printWarning, } from '../ui/consoleUi.js';
|
|
7
|
+
const MAX_DIFF_OUTPUT_LINES = 120;
|
|
8
|
+
function formatPromptDiff(oldPrompt, newPrompt) {
|
|
9
|
+
const output = diff(oldPrompt, newPrompt, {
|
|
10
|
+
aAnnotation: 'old prompt',
|
|
11
|
+
bAnnotation: 'new prompt',
|
|
12
|
+
expand: false,
|
|
13
|
+
});
|
|
14
|
+
if (!output) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const lines = output.split('\n');
|
|
18
|
+
if (lines.length <= MAX_DIFF_OUTPUT_LINES) {
|
|
19
|
+
return { text: output, truncated: false };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
text: lines.slice(0, MAX_DIFF_OUTPUT_LINES).join('\n'),
|
|
23
|
+
truncated: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function promptLine(message) {
|
|
27
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
try {
|
|
29
|
+
return (await rl.question(message)).trim();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function promptValidity() {
|
|
36
|
+
while (true) {
|
|
37
|
+
const answer = (await promptLine('Mark as (v)alid, (i)nvalid, or (q)uit: ')).toLowerCase();
|
|
38
|
+
if (answer === 'v' || answer === 'valid') {
|
|
39
|
+
return 'valid';
|
|
40
|
+
}
|
|
41
|
+
if (answer === 'i' || answer === 'invalid') {
|
|
42
|
+
return 'invalid';
|
|
43
|
+
}
|
|
44
|
+
if (answer === 'q' || answer === 'quit' || answer === 'exit') {
|
|
45
|
+
return 'quit';
|
|
46
|
+
}
|
|
47
|
+
printWarning('Please enter v, i, or q.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function promptRequiredReason() {
|
|
51
|
+
while (true) {
|
|
52
|
+
const answer = await promptLine('Reason (required for invalid feedback): ');
|
|
53
|
+
if (answer.length > 0) {
|
|
54
|
+
return answer;
|
|
55
|
+
}
|
|
56
|
+
printWarning('A reason is required when marking a result as invalid.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function promptConsolidateBeforeExit() {
|
|
60
|
+
const answer = (await promptLine('Run consolidate now before exiting? [Y/n]: ')).toLowerCase();
|
|
61
|
+
if (!answer) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return answer !== 'n' && answer !== 'no';
|
|
65
|
+
}
|
|
66
|
+
function validateSubmitFlags(options) {
|
|
67
|
+
const hasValid = options.valid === true;
|
|
68
|
+
const hasInvalid = options.invalid === true;
|
|
69
|
+
if (hasValid === hasInvalid) {
|
|
70
|
+
throw new Error('Choose exactly one: --valid or --invalid.');
|
|
71
|
+
}
|
|
72
|
+
if (hasInvalid) {
|
|
73
|
+
const reason = options.reason?.trim() ?? '';
|
|
74
|
+
if (!reason) {
|
|
75
|
+
throw new Error('A reason is required when using --invalid.');
|
|
76
|
+
}
|
|
77
|
+
return { isValid: false, reason };
|
|
78
|
+
}
|
|
79
|
+
return { isValid: true, reason: null };
|
|
80
|
+
}
|
|
81
|
+
function printThread(commentThreadNodes) {
|
|
82
|
+
if (commentThreadNodes.length === 0) {
|
|
83
|
+
printWarning('Thread unavailable for this comment.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
printSection('Thread (root -> target)');
|
|
87
|
+
commentThreadNodes.forEach((node, index) => {
|
|
88
|
+
const marker = node.isTarget ? 'target' : `depth ${node.depth}`;
|
|
89
|
+
printInfo(`${index + 1}. ${node.author} (${marker})`);
|
|
90
|
+
printMuted(` ${node.body}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function printReviewItem(row, commentThreadNodes) {
|
|
94
|
+
printSection(`Result ${row.id}`);
|
|
95
|
+
printKeyValue('Type', row.type);
|
|
96
|
+
printKeyValue('Subreddit', `r/${row.subreddit}`);
|
|
97
|
+
printKeyValue('Author', row.author);
|
|
98
|
+
printKeyValue('URL', row.url);
|
|
99
|
+
printKeyValue('Posted', row.redditPostedAt);
|
|
100
|
+
if (row.title) {
|
|
101
|
+
printKeyValue('Title', row.title);
|
|
102
|
+
}
|
|
103
|
+
if (row.qualificationReason) {
|
|
104
|
+
printKeyValue('Qualification reason', row.qualificationReason);
|
|
105
|
+
}
|
|
106
|
+
if (row.type === 'comment') {
|
|
107
|
+
printThread(commentThreadNodes);
|
|
108
|
+
}
|
|
109
|
+
printSection('Content');
|
|
110
|
+
printMuted(row.body);
|
|
111
|
+
}
|
|
112
|
+
export async function feedbackSubmit(resultId, options = {}) {
|
|
113
|
+
printCommandScreen('Feedback', 'Submit feedback');
|
|
114
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
115
|
+
const row = scanItemsRepo.getQualifiedById(resultId);
|
|
116
|
+
if (!row) {
|
|
117
|
+
throw new Error(`Qualified result not found: ${resultId}`);
|
|
118
|
+
}
|
|
119
|
+
const { isValid, reason } = validateSubmitFlags(options);
|
|
120
|
+
const updated = scanItemsRepo.submitFeedback(resultId, isValid, reason);
|
|
121
|
+
if (!updated) {
|
|
122
|
+
throw new Error(`Failed to save feedback for result: ${resultId}`);
|
|
123
|
+
}
|
|
124
|
+
const pendingCount = scanItemsRepo.countPendingFeedbackConsolidation(row.jobId);
|
|
125
|
+
const payload = {
|
|
126
|
+
resultId,
|
|
127
|
+
saved: true,
|
|
128
|
+
feedback: {
|
|
129
|
+
validated: true,
|
|
130
|
+
isValid,
|
|
131
|
+
isValidReason: reason,
|
|
132
|
+
feedbackConsolidated: false,
|
|
133
|
+
},
|
|
134
|
+
pendingFeedbackConsolidationCount: pendingCount,
|
|
135
|
+
requiresConsolidation: pendingCount > 0,
|
|
136
|
+
recommendedNextCommand: 'snoopy feedback consolidate',
|
|
137
|
+
};
|
|
138
|
+
if (options.json) {
|
|
139
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
printSuccess(`Saved feedback for result ${resultId}.`);
|
|
143
|
+
printKeyValue('Valid', isValid ? 'yes' : 'no');
|
|
144
|
+
if (!isValid && reason) {
|
|
145
|
+
printKeyValue('Reason', reason);
|
|
146
|
+
}
|
|
147
|
+
if (pendingCount > 0) {
|
|
148
|
+
printInfo(`Consolidation pending for ${pendingCount} result(s). Run snoopy feedback consolidate.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function runInteractiveReview(rows, runConsolidateOnExit) {
|
|
152
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
153
|
+
let validatedCount = 0;
|
|
154
|
+
let invalidCount = 0;
|
|
155
|
+
let earlyExit = false;
|
|
156
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
157
|
+
const row = rows[index];
|
|
158
|
+
const commentThreadNodes = row.type === 'comment' ? scanItemsRepo.listCommentThreadNodes(row.id) : [];
|
|
159
|
+
printCommandScreen('Feedback review', `Item ${index + 1}/${rows.length}`);
|
|
160
|
+
printReviewItem(row, commentThreadNodes);
|
|
161
|
+
const verdict = await promptValidity();
|
|
162
|
+
if (verdict === 'quit') {
|
|
163
|
+
earlyExit = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const isValid = verdict === 'valid';
|
|
167
|
+
const reason = isValid ? null : await promptRequiredReason();
|
|
168
|
+
scanItemsRepo.submitFeedback(row.id, isValid, reason);
|
|
169
|
+
validatedCount += 1;
|
|
170
|
+
if (!isValid) {
|
|
171
|
+
invalidCount += 1;
|
|
172
|
+
}
|
|
173
|
+
printSuccess(`Saved feedback for ${row.id}.`);
|
|
174
|
+
}
|
|
175
|
+
printSection('Review summary');
|
|
176
|
+
printKeyValue('Validated this session', String(validatedCount));
|
|
177
|
+
printKeyValue('Marked invalid', String(invalidCount));
|
|
178
|
+
printKeyValue('Early exit', earlyExit ? 'yes' : 'no');
|
|
179
|
+
if (earlyExit) {
|
|
180
|
+
const shouldConsolidateNow = await promptConsolidateBeforeExit();
|
|
181
|
+
if (shouldConsolidateNow) {
|
|
182
|
+
await runConsolidateOnExit();
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
printWarning('Skipping consolidate. Prompt quality will not improve until consolidate runs.');
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await runConsolidateOnExit();
|
|
190
|
+
}
|
|
191
|
+
export async function feedbackReview(jobRef, options = {}) {
|
|
192
|
+
const jobsRepo = new JobsRepository();
|
|
193
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
194
|
+
let jobId;
|
|
195
|
+
if (jobRef) {
|
|
196
|
+
const job = jobsRepo.getByRef(jobRef);
|
|
197
|
+
if (!job) {
|
|
198
|
+
throw new Error(`Job not found: ${jobRef}`);
|
|
199
|
+
}
|
|
200
|
+
jobId = job.id;
|
|
201
|
+
}
|
|
202
|
+
const limit = options.limit ?? 10;
|
|
203
|
+
const rows = scanItemsRepo.listUnvalidatedQualified(jobId, limit);
|
|
204
|
+
if (options.json) {
|
|
205
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
printCommandScreen('Feedback', 'Review feedback queue');
|
|
209
|
+
if (rows.length === 0) {
|
|
210
|
+
printInfo('No more results to validate. Exiting review.');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!isRichTty()) {
|
|
214
|
+
printError('Interactive review requires a rich terminal. Use --json in non-interactive mode.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const runConsolidateOnExit = async () => {
|
|
218
|
+
printInfo('Running feedback consolidation...');
|
|
219
|
+
const result = await consolidateFeedback({ jobRef, limit });
|
|
220
|
+
printSuccess(`Consolidated ${result.totalConsolidated} result(s).`);
|
|
221
|
+
if (result.requiresConsolidation) {
|
|
222
|
+
printWarning(`Still pending: ${result.totalPendingAfter}. Run snoopy feedback consolidate again.`);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
await runInteractiveReview(rows, runConsolidateOnExit);
|
|
226
|
+
}
|
|
227
|
+
export async function feedbackConsolidate(jobRef, options = {}) {
|
|
228
|
+
printCommandScreen('Feedback', 'Consolidate feedback');
|
|
229
|
+
const result = await consolidateFeedback({
|
|
230
|
+
jobRef,
|
|
231
|
+
limit: options.limit,
|
|
232
|
+
});
|
|
233
|
+
if (options.json) {
|
|
234
|
+
console.log(JSON.stringify(result, null, 2));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (result.totalPendingBefore === 0) {
|
|
238
|
+
printInfo('No pending feedback to consolidate.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
result.jobs.forEach((jobResult) => {
|
|
242
|
+
printInfo(`${jobResult.jobName} (${jobResult.jobSlug})`);
|
|
243
|
+
printKeyValue('Pending', String(jobResult.pendingCount));
|
|
244
|
+
printKeyValue('Consolidated', String(jobResult.consolidatedCount));
|
|
245
|
+
if (jobResult.error) {
|
|
246
|
+
printError(jobResult.error);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (jobResult.changeSummary && jobResult.changeSummary.length > 0) {
|
|
250
|
+
printKeyValue('Changes', jobResult.changeSummary.join(' | '));
|
|
251
|
+
}
|
|
252
|
+
if (!jobResult.promptUpdated || !jobResult.oldPrompt || !jobResult.newPrompt) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (jobResult.oldPrompt === jobResult.newPrompt) {
|
|
256
|
+
printMuted('Prompt did not change.');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const promptDiff = formatPromptDiff(jobResult.oldPrompt, jobResult.newPrompt);
|
|
260
|
+
if (!promptDiff) {
|
|
261
|
+
printMuted('Prompt updated, but no textual diff was produced.');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
printSection(`Prompt diff (${jobResult.jobSlug})`);
|
|
265
|
+
console.log(promptDiff.text);
|
|
266
|
+
if (promptDiff.truncated) {
|
|
267
|
+
printWarning(`Prompt diff truncated to ${MAX_DIFF_OUTPUT_LINES} lines.`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
if (result.requiresConsolidation) {
|
|
271
|
+
printWarning(`Consolidation incomplete. ${result.totalPendingAfter} result(s) still pending.`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
printSuccess(`Consolidated ${result.totalConsolidated} result(s) across ${result.jobs.length} job(s).`);
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { JobsRepository } from '../../services/db/repositories/jobsRepo.js';
|
|
3
|
+
import { isRichTty, printCommandScreen, printError, printInfo, printKeyValue, printMuted, printSection, printSuccess, printWarning } from '../ui/consoleUi.js';
|
|
4
|
+
import { editPromptInteractively } from './promptEditor.js';
|
|
5
|
+
async function promptLine(message) {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
try {
|
|
8
|
+
return (await rl.question(message)).trim();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
rl.close();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function confirmEditPrompt() {
|
|
15
|
+
const answer = (await promptLine('Edit qualification prompt now? [y/N]: ')).toLowerCase();
|
|
16
|
+
if (!answer) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return answer === 'y' || answer === 'yes';
|
|
20
|
+
}
|
|
21
|
+
function printPromptEnvelope(job) {
|
|
22
|
+
printKeyValue('Job', `${job.name} (${job.slug})`);
|
|
23
|
+
printKeyValue('Job ID', job.id);
|
|
24
|
+
printKeyValue('Updated', job.updatedAt);
|
|
25
|
+
printSection('Qualification prompt');
|
|
26
|
+
printMuted(job.qualificationPrompt);
|
|
27
|
+
}
|
|
28
|
+
export async function showPrompt(jobRef, options = {}) {
|
|
29
|
+
const jobsRepo = new JobsRepository();
|
|
30
|
+
const job = jobsRepo.getByRef(jobRef);
|
|
31
|
+
if (!job) {
|
|
32
|
+
printError(`Job not found: ${jobRef}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (options.raw) {
|
|
36
|
+
process.stdout.write(`${job.qualificationPrompt}\n`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
printCommandScreen('Prompt', 'View Prompt');
|
|
40
|
+
printPromptEnvelope(job);
|
|
41
|
+
if (!isRichTty()) {
|
|
42
|
+
printInfo('Use --raw for non-interactive prompt retrieval.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const shouldEdit = await confirmEditPrompt();
|
|
46
|
+
if (!shouldEdit) {
|
|
47
|
+
printInfo('Prompt unchanged.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const updatedPrompt = await editPromptInteractively(job.qualificationPrompt);
|
|
51
|
+
if (updatedPrompt === null) {
|
|
52
|
+
printWarning('Prompt edit cancelled.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const normalizedPrompt = updatedPrompt.trim();
|
|
56
|
+
if (!normalizedPrompt) {
|
|
57
|
+
printError('Prompt cannot be empty.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (normalizedPrompt === job.qualificationPrompt.trim()) {
|
|
61
|
+
printInfo('Prompt unchanged.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const updated = jobsRepo.updateQualificationPromptByRef(job.id, normalizedPrompt);
|
|
65
|
+
if (!updated) {
|
|
66
|
+
printError(`Failed to update prompt for job: ${job.id}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
printSuccess(`Updated qualification prompt for ${updated.name} (${updated.slug}).`);
|
|
70
|
+
}
|
|
71
|
+
export async function setPrompt(jobRef, promptText) {
|
|
72
|
+
const normalizedPrompt = promptText.trim();
|
|
73
|
+
if (!normalizedPrompt) {
|
|
74
|
+
throw new Error('Prompt cannot be empty.');
|
|
75
|
+
}
|
|
76
|
+
const jobsRepo = new JobsRepository();
|
|
77
|
+
const existing = jobsRepo.getByRef(jobRef);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
printError(`Job not found: ${jobRef}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const updated = jobsRepo.updateQualificationPromptByRef(existing.id, normalizedPrompt);
|
|
83
|
+
if (!updated) {
|
|
84
|
+
printError(`Failed to update prompt for job: ${existing.id}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
printCommandScreen('Prompt', 'Set Prompt');
|
|
88
|
+
printSuccess(`Updated qualification prompt for ${updated.name} (${updated.slug}).`);
|
|
89
|
+
printKeyValue('Job ID', updated.id);
|
|
90
|
+
printKeyValue('Updated', updated.updatedAt);
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function editPromptInteractively(initialValue: string): Promise<string | null>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { AppFrame, Panel } from '../../ui/components/AppFrame.js';
|
|
4
|
+
import { MultilinePrompt } from '../../ui/components/MultilinePrompt.js';
|
|
5
|
+
export async function editPromptInteractively(initialValue) {
|
|
6
|
+
let result = null;
|
|
7
|
+
const app = render(_jsx(AppFrame, { subtitle: "Prompt", statusText: "Interactive editor", statusTone: "info", hints: ['Enter submit', 'Shift+Enter newline', 'Esc cancel'], children: _jsx(Panel, { title: "Edit Qualification Prompt", children: _jsx(MultilinePrompt, { label: "Qualification prompt", initialValue: initialValue, rows: 10, onSubmit: (value) => {
|
|
8
|
+
result = value;
|
|
9
|
+
app.unmount();
|
|
10
|
+
}, onCancel: () => {
|
|
11
|
+
result = null;
|
|
12
|
+
app.unmount();
|
|
13
|
+
} }) }) }));
|
|
14
|
+
await app.waitUntilExit();
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=promptEditor.js.map
|
|
@@ -128,7 +128,7 @@ export function JobAddFlow({ hasApiKey, existingApiKey, canPersistApiKey, startu
|
|
|
128
128
|
} })] }) }));
|
|
129
129
|
}
|
|
130
130
|
if (stage === 'model') {
|
|
131
|
-
return (_jsx(FlowFrame, { transcript: transcript, statusText: "Choose model", statusTone: "info", children: _jsxs(Panel, { title: "Step 3: Model", children: [_jsx(Text, { color: uiTheme.ink.warning, children: "Model selection (default
|
|
131
|
+
return (_jsx(FlowFrame, { transcript: transcript, statusText: "Choose model", statusTone: "info", children: _jsxs(Panel, { title: "Step 3: Model", children: [_jsx(Text, { color: uiTheme.ink.warning, children: "Model selection (default deepseek/deepseek-v4-pro)" }), _jsx(TextPrompt, { label: "Model ID", initialValue: model.trim() || defaultModel, onSubmit: (value) => {
|
|
132
132
|
const chosenModel = value.trim() || defaultModel;
|
|
133
133
|
setModel(chosenModel);
|
|
134
134
|
appendTranscript('Model', chosenModel);
|
package/dist/src/cli/index.js
CHANGED
|
@@ -8,8 +8,10 @@ import { showRunLogs } from './commands/logs.js';
|
|
|
8
8
|
import { showJobErrors } from './commands/errors.js';
|
|
9
9
|
import { exportCsv } from './commands/export.js';
|
|
10
10
|
import { consumeResults } from './commands/consume.js';
|
|
11
|
+
import { feedbackConsolidate, feedbackReview, feedbackSubmit } from './commands/feedback.js';
|
|
11
12
|
import { showAnalytics } from './commands/analytics.js';
|
|
12
13
|
import { showResults } from './commands/results.js';
|
|
14
|
+
import { showPrompt, setPrompt } from './commands/prompt.js';
|
|
13
15
|
import { disableStartupCommand, enableStartupCommand, installStartupCommand, startupStatusCommand, uninstallStartupCommand } from './commands/startup.js';
|
|
14
16
|
import { runDoctor } from './commands/doctor.js';
|
|
15
17
|
import { ensureAppDirs } from '../utils/paths.js';
|
|
@@ -38,7 +40,7 @@ function parsePositiveInteger(value) {
|
|
|
38
40
|
return parsed;
|
|
39
41
|
}
|
|
40
42
|
const program = new Command();
|
|
41
|
-
program.name('snoopy').description('Monitor
|
|
43
|
+
program.name('snoopy').description('Monitor online conversations for high-intent signals with AI — plain language criteria, continuous scanning, zero infrastructure.').version(readVersion());
|
|
42
44
|
const job = program.command('job').description('Manage monitoring jobs');
|
|
43
45
|
job.command('add').description('Add a monitoring job').action(async () => {
|
|
44
46
|
await addJob();
|
|
@@ -153,6 +155,89 @@ program
|
|
|
153
155
|
.action((jobRef, options) => {
|
|
154
156
|
consumeResults(jobRef, options);
|
|
155
157
|
});
|
|
158
|
+
const feedback = program.command('feedback').description('Collect qualification feedback and consolidate prompt improvements');
|
|
159
|
+
feedback
|
|
160
|
+
.command('review')
|
|
161
|
+
.argument('[jobRef]', 'Optional job ID or slug')
|
|
162
|
+
.description('Review unvalidated qualified results and submit feedback')
|
|
163
|
+
.option('--json', 'Output raw JSON array to stdout')
|
|
164
|
+
.option('--limit <count>', 'Maximum number of results to review (default: 10)', parsePositiveInteger, 10)
|
|
165
|
+
.action(async (jobRef, options) => {
|
|
166
|
+
await feedbackReview(jobRef, options);
|
|
167
|
+
});
|
|
168
|
+
feedback
|
|
169
|
+
.command('submit')
|
|
170
|
+
.argument('<resultId>', 'Qualified result ID')
|
|
171
|
+
.description('Submit validity feedback for a qualified result')
|
|
172
|
+
.option('--valid', 'Mark this result as valid')
|
|
173
|
+
.option('--invalid', 'Mark this result as invalid')
|
|
174
|
+
.option('--reason <text>', 'Reason for invalid feedback (required with --invalid)')
|
|
175
|
+
.option('--json', 'Output machine-readable JSON status')
|
|
176
|
+
.action(async (resultId, options) => {
|
|
177
|
+
await feedbackSubmit(resultId, options);
|
|
178
|
+
});
|
|
179
|
+
feedback
|
|
180
|
+
.command('consolidate')
|
|
181
|
+
.argument('[jobRef]', 'Optional job ID or slug')
|
|
182
|
+
.description('Consolidate user feedback into improved qualification prompts')
|
|
183
|
+
.option('--limit <count>', 'Maximum pending feedback items to process', parsePositiveInteger)
|
|
184
|
+
.option('--json', 'Output machine-readable JSON status')
|
|
185
|
+
.action(async (jobRef, options) => {
|
|
186
|
+
await feedbackConsolidate(jobRef, options);
|
|
187
|
+
});
|
|
188
|
+
const prompt = program.command('prompt').description('View and update job qualification prompts');
|
|
189
|
+
prompt
|
|
190
|
+
.argument('<jobRef>', 'Job ID or slug')
|
|
191
|
+
.description('View the qualification prompt for a specific job')
|
|
192
|
+
.option('--raw', 'Output only the prompt text and exit')
|
|
193
|
+
.action(async (jobRef, options) => {
|
|
194
|
+
await showPrompt(jobRef, options);
|
|
195
|
+
});
|
|
196
|
+
prompt
|
|
197
|
+
.command('set')
|
|
198
|
+
.argument('<jobRef>', 'Job ID or slug')
|
|
199
|
+
.argument('<prompt>', 'New qualification prompt text')
|
|
200
|
+
.description('Set a new qualification prompt for a specific job')
|
|
201
|
+
.action(async (jobRef, promptText) => {
|
|
202
|
+
await setPrompt(jobRef, promptText);
|
|
203
|
+
});
|
|
204
|
+
// --- MCP server ---
|
|
205
|
+
program
|
|
206
|
+
.command('mcp')
|
|
207
|
+
.description('Start MCP server for agent integration (stdio transport)')
|
|
208
|
+
.action(async () => {
|
|
209
|
+
const { startSnoopyMcpServer } = await import('../mcp/server.js');
|
|
210
|
+
await startSnoopyMcpServer();
|
|
211
|
+
});
|
|
212
|
+
// --- Agent framework registration ---
|
|
213
|
+
const agent = program.command('agent').description('Manage agent framework integrations');
|
|
214
|
+
agent
|
|
215
|
+
.command('install')
|
|
216
|
+
.argument('<runtime>', 'Agent runtime (claude, claude-desktop, chatgpt, gemini, codex, cursor, vscode, opencode, generic-mcp)')
|
|
217
|
+
.description('Register Snoopy MCP server with an agent framework')
|
|
218
|
+
.action(async (runtime) => {
|
|
219
|
+
const { agentInstall } = await import('../agent/install.js');
|
|
220
|
+
await agentInstall(runtime);
|
|
221
|
+
});
|
|
222
|
+
agent
|
|
223
|
+
.command('uninstall')
|
|
224
|
+
.argument('<runtime>', 'Agent runtime')
|
|
225
|
+
.description('Remove Snoopy MCP server from an agent framework')
|
|
226
|
+
.action(async (runtime) => {
|
|
227
|
+
const { agentUninstall } = await import('../agent/install.js');
|
|
228
|
+
await agentUninstall(runtime);
|
|
229
|
+
});
|
|
230
|
+
agent
|
|
231
|
+
.command('status')
|
|
232
|
+
.description('Show agent framework registration status')
|
|
233
|
+
.action(async () => {
|
|
234
|
+
const { agentStatus } = await import('../agent/install.js');
|
|
235
|
+
const status = await agentStatus();
|
|
236
|
+
for (const entry of status.runtimes) {
|
|
237
|
+
const marker = entry.installed ? '✓' : '·';
|
|
238
|
+
console.log(`${marker} ${entry.runtime.padEnd(16)} ${entry.configPath}`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
156
241
|
program.parseAsync(process.argv).catch((error) => {
|
|
157
242
|
console.error(`Error: ${String(error)}`);
|
|
158
243
|
process.exit(1);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare function getSnoopyVersion(): string;
|
|
2
|
+
declare function formatToolError(error: unknown): {
|
|
3
|
+
content: {
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError: true;
|
|
8
|
+
};
|
|
9
|
+
declare function formatToolResult(data: unknown): {
|
|
10
|
+
content: {
|
|
11
|
+
type: 'text';
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
structuredContent: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
export { formatToolError, formatToolResult };
|
|
17
|
+
export declare function buildDoctorReport(): Promise<Record<string, unknown>>;
|
|
18
|
+
export declare function buildDaemonStatusReport(): Record<string, unknown>;
|
|
19
|
+
export declare function startDaemonReport(): Record<string, unknown>;
|
|
20
|
+
export declare function stopDaemonReport(): Record<string, unknown>;
|
|
21
|
+
export declare function reloadDaemonReport(): Record<string, unknown>;
|
|
22
|
+
export declare function listJobsReport(): Record<string, unknown>;
|
|
23
|
+
export declare function listJobRunsReport(jobRef?: string, limit?: number): Record<string, unknown>;
|
|
24
|
+
export declare function addJobReport(input: {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
subreddits: string[];
|
|
28
|
+
qualificationPrompt: string;
|
|
29
|
+
scheduleCron?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
monitorComments?: boolean;
|
|
32
|
+
}): Record<string, unknown>;
|
|
33
|
+
export declare function deleteJobReport(jobRef: string): Record<string, unknown>;
|
|
34
|
+
export declare function enableJobReport(jobRef: string): Record<string, unknown>;
|
|
35
|
+
export declare function disableJobReport(jobRef: string): Record<string, unknown>;
|
|
36
|
+
export declare function runJobReport(jobRef: string, limit?: number): Record<string, unknown>;
|
|
37
|
+
export declare function analyticsReport(jobRef?: string, days?: number): Record<string, unknown>;
|
|
38
|
+
export declare function exportReport(jobRef?: string, format?: string, lastRun?: boolean, limit?: number): Record<string, unknown>;
|
|
39
|
+
export declare function consumeReport(jobRef?: string, limit?: number, dryRun?: boolean): Record<string, unknown>;
|
|
40
|
+
export declare function feedbackReviewReport(jobRef?: string, limit?: number): Record<string, unknown>;
|
|
41
|
+
export declare function feedbackSubmitReport(resultId: string, isValid: boolean, reason?: string): Record<string, unknown>;
|
|
42
|
+
export declare function feedbackConsolidateReport(jobRef?: string, limit?: number): Promise<Record<string, unknown>>;
|
|
43
|
+
export declare function errorsReport(jobRef: string, hours?: number): Record<string, unknown>;
|
|
44
|
+
export declare function logsReport(runId: string): Record<string, unknown>;
|
|
45
|
+
export declare function settingsGetReport(): Promise<Record<string, unknown>>;
|
|
46
|
+
export declare function settingsSetReport(key: string, value: string): Record<string, unknown>;
|