@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
|
@@ -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 = "
|
|
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 = '
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface CursorPosition {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
export interface VerticalMoveResult {
|
|
6
|
+
nextOffset: number;
|
|
7
|
+
preferredColumn: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function splitLines(value: string): string[];
|
|
10
|
+
export declare function offsetToCursor(value: string, offset: number): CursorPosition;
|
|
11
|
+
export declare function cursorToOffset(value: string, position: CursorPosition): number;
|
|
12
|
+
export declare function moveCursorHorizontal(value: string, offset: number, delta: -1 | 1): number;
|
|
13
|
+
export declare function moveCursorVertical(value: string, offset: number, direction: -1 | 1, preferredColumn?: number): VerticalMoveResult;
|
|
14
|
+
export declare function insertAtOffset(value: string, offset: number, inserted: string): {
|
|
15
|
+
value: string;
|
|
16
|
+
offset: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function backspaceAtOffset(value: string, offset: number): {
|
|
19
|
+
value: string;
|
|
20
|
+
offset: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function deleteAtOffset(value: string, offset: number): {
|
|
23
|
+
value: string;
|
|
24
|
+
offset: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function splitLines(value) {
|
|
2
|
+
const lines = value.split('\n');
|
|
3
|
+
return lines.length > 0 ? lines : [''];
|
|
4
|
+
}
|
|
5
|
+
export function offsetToCursor(value, offset) {
|
|
6
|
+
const safeOffset = Math.max(0, Math.min(offset, value.length));
|
|
7
|
+
let line = 0;
|
|
8
|
+
let column = 0;
|
|
9
|
+
for (let index = 0; index < safeOffset; index += 1) {
|
|
10
|
+
if (value[index] === '\n') {
|
|
11
|
+
line += 1;
|
|
12
|
+
column = 0;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
column += 1;
|
|
16
|
+
}
|
|
17
|
+
return { line, column };
|
|
18
|
+
}
|
|
19
|
+
export function cursorToOffset(value, position) {
|
|
20
|
+
const lines = splitLines(value);
|
|
21
|
+
const safeLine = Math.max(0, Math.min(position.line, lines.length - 1));
|
|
22
|
+
const lineLength = lines[safeLine]?.length ?? 0;
|
|
23
|
+
const safeColumn = Math.max(0, Math.min(position.column, lineLength));
|
|
24
|
+
let offset = 0;
|
|
25
|
+
for (let index = 0; index < safeLine; index += 1) {
|
|
26
|
+
offset += (lines[index]?.length ?? 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
return offset + safeColumn;
|
|
29
|
+
}
|
|
30
|
+
export function moveCursorHorizontal(value, offset, delta) {
|
|
31
|
+
if (delta < 0) {
|
|
32
|
+
return Math.max(0, offset - 1);
|
|
33
|
+
}
|
|
34
|
+
return Math.min(value.length, offset + 1);
|
|
35
|
+
}
|
|
36
|
+
export function moveCursorVertical(value, offset, direction, preferredColumn) {
|
|
37
|
+
const cursor = offsetToCursor(value, offset);
|
|
38
|
+
const lines = splitLines(value);
|
|
39
|
+
const nextLine = Math.max(0, Math.min(lines.length - 1, cursor.line + direction));
|
|
40
|
+
const targetColumn = preferredColumn ?? cursor.column;
|
|
41
|
+
const maxColumn = lines[nextLine]?.length ?? 0;
|
|
42
|
+
const nextColumn = Math.max(0, Math.min(targetColumn, maxColumn));
|
|
43
|
+
return {
|
|
44
|
+
nextOffset: cursorToOffset(value, { line: nextLine, column: nextColumn }),
|
|
45
|
+
preferredColumn: targetColumn,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function insertAtOffset(value, offset, inserted) {
|
|
49
|
+
const safeOffset = Math.max(0, Math.min(offset, value.length));
|
|
50
|
+
const nextValue = `${value.slice(0, safeOffset)}${inserted}${value.slice(safeOffset)}`;
|
|
51
|
+
return {
|
|
52
|
+
value: nextValue,
|
|
53
|
+
offset: safeOffset + inserted.length,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function backspaceAtOffset(value, offset) {
|
|
57
|
+
const safeOffset = Math.max(0, Math.min(offset, value.length));
|
|
58
|
+
if (safeOffset === 0) {
|
|
59
|
+
return { value, offset: safeOffset };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
value: `${value.slice(0, safeOffset - 1)}${value.slice(safeOffset)}`,
|
|
63
|
+
offset: safeOffset - 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function deleteAtOffset(value, offset) {
|
|
67
|
+
const safeOffset = Math.max(0, Math.min(offset, value.length));
|
|
68
|
+
if (safeOffset >= value.length) {
|
|
69
|
+
return { value, offset: safeOffset };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
value: `${value.slice(0, safeOffset)}${value.slice(safeOffset + 1)}`,
|
|
73
|
+
offset: safeOffset,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=multilinePromptModel.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telepat/snoopy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Snoopy CLI for Reddit conversation monitoring jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"docs:build": "npm --prefix website run build",
|
|
25
25
|
"docs:serve": "npm --prefix website run serve",
|
|
26
26
|
"docs:deploy": "npm --prefix website run deploy",
|
|
27
|
+
"docs:write-translations": "npm --prefix website run write-translations",
|
|
27
28
|
"e2e:smoke": "tsx src/scripts/e2eSmoke.ts",
|
|
28
29
|
"e2e:fresh": "tsx tests/e2e/freshInstall.ts",
|
|
29
30
|
"e2e:upgrade": "tsx tests/e2e/upgrade.ts",
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"better-sqlite3": "^12.8.0",
|
|
58
59
|
"commander": "^14.0.3",
|
|
59
60
|
"ink": "^6.8.0",
|
|
61
|
+
"jest-diff": "^29.7.0",
|
|
60
62
|
"keytar": "^7.9.0",
|
|
61
63
|
"node-cron": "^4.2.1",
|
|
62
64
|
"node-notifier": "^10.0.1",
|