@telepat/snoopy 0.1.12 → 0.1.14

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.
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ export declare const snoopyDoctorToolInputSchema: {};
3
+ export declare const snoopyDaemonStatusToolInputSchema: {};
4
+ export declare const snoopyDaemonStartToolInputSchema: {};
5
+ export declare const snoopyDaemonStopToolInputSchema: {};
6
+ export declare const snoopyDaemonReloadToolInputSchema: {};
7
+ export declare const snoopyJobListToolInputSchema: {};
8
+ export declare const snoopyJobRunsToolInputSchema: {
9
+ jobRef: z.ZodOptional<z.ZodString>;
10
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
11
+ };
12
+ export declare const snoopyJobAddToolInputSchema: {
13
+ name: z.ZodString;
14
+ description: z.ZodOptional<z.ZodString>;
15
+ subreddits: z.ZodArray<z.ZodString>;
16
+ qualificationPrompt: z.ZodString;
17
+ scheduleCron: z.ZodOptional<z.ZodString>;
18
+ enabled: z.ZodOptional<z.ZodBoolean>;
19
+ monitorComments: z.ZodOptional<z.ZodBoolean>;
20
+ };
21
+ export declare const snoopyJobDeleteToolInputSchema: {
22
+ jobRef: z.ZodString;
23
+ };
24
+ export declare const snoopyJobEnableToolInputSchema: {
25
+ jobRef: z.ZodString;
26
+ };
27
+ export declare const snoopyJobDisableToolInputSchema: {
28
+ jobRef: z.ZodString;
29
+ };
30
+ export declare const snoopyJobRunToolInputSchema: {
31
+ jobRef: z.ZodString;
32
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
33
+ };
34
+ export declare const snoopyAnalyticsToolInputSchema: {
35
+ jobRef: z.ZodOptional<z.ZodString>;
36
+ days: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
37
+ };
38
+ export declare const snoopyExportToolInputSchema: {
39
+ jobRef: z.ZodOptional<z.ZodString>;
40
+ format: z.ZodOptional<z.ZodEnum<{
41
+ csv: "csv";
42
+ json: "json";
43
+ }>>;
44
+ lastRun: z.ZodOptional<z.ZodBoolean>;
45
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
46
+ };
47
+ export declare const snoopyConsumeToolInputSchema: {
48
+ jobRef: z.ZodOptional<z.ZodString>;
49
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
50
+ dryRun: z.ZodOptional<z.ZodBoolean>;
51
+ };
52
+ export declare const snoopyErrorsToolInputSchema: {
53
+ jobRef: z.ZodString;
54
+ hours: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
55
+ };
56
+ export declare const snoopyLogsToolInputSchema: {
57
+ runId: z.ZodString;
58
+ };
59
+ export declare const snoopySettingsGetToolInputSchema: {};
60
+ export declare const snoopySettingsSetToolInputSchema: {
61
+ key: z.ZodEnum<{
62
+ model: "model";
63
+ cronIntervalMinutes: "cronIntervalMinutes";
64
+ temperature: "temperature";
65
+ maxTokens: "maxTokens";
66
+ topP: "topP";
67
+ notificationsEnabled: "notificationsEnabled";
68
+ jobTimeoutMs: "jobTimeoutMs";
69
+ }>;
70
+ value: z.ZodString;
71
+ };
72
+ export interface ToolContract {
73
+ name: string;
74
+ required: string[];
75
+ enums: Record<string, string[]>;
76
+ }
77
+ export declare const snoopyToolContracts: ToolContract[];
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ export const snoopyDoctorToolInputSchema = {};
3
+ export const snoopyDaemonStatusToolInputSchema = {};
4
+ export const snoopyDaemonStartToolInputSchema = {};
5
+ export const snoopyDaemonStopToolInputSchema = {};
6
+ export const snoopyDaemonReloadToolInputSchema = {};
7
+ export const snoopyJobListToolInputSchema = {};
8
+ export const snoopyJobRunsToolInputSchema = {
9
+ jobRef: z.string().optional().describe('Job ID or slug. If omitted, returns runs for all jobs.'),
10
+ limit: z.coerce.number().int().positive().optional().describe('Max runs to return. Default: 20.'),
11
+ };
12
+ export const snoopyJobAddToolInputSchema = {
13
+ name: z.string().min(1).describe('Job name.'),
14
+ description: z.string().optional().describe('Job description. Default: empty.'),
15
+ subreddits: z.array(z.string()).min(1).describe('Subreddits to monitor (e.g. ["startups","SaaS"]).'),
16
+ qualificationPrompt: z.string().min(1).describe('Plain-language criteria for qualifying posts/comments.'),
17
+ scheduleCron: z.string().optional().describe('Cron expression. Default: "*/30 * * * *" (every 30 min).'),
18
+ enabled: z.boolean().optional().describe('Enable scheduling immediately. Default: true.'),
19
+ monitorComments: z.boolean().optional().describe('Also monitor comments. Default: true.'),
20
+ };
21
+ export const snoopyJobDeleteToolInputSchema = {
22
+ jobRef: z.string().min(1).describe('Job ID or slug to delete.'),
23
+ };
24
+ export const snoopyJobEnableToolInputSchema = {
25
+ jobRef: z.string().min(1).describe('Job ID or slug to enable.'),
26
+ };
27
+ export const snoopyJobDisableToolInputSchema = {
28
+ jobRef: z.string().min(1).describe('Job ID or slug to disable.'),
29
+ };
30
+ export const snoopyJobRunToolInputSchema = {
31
+ jobRef: z.string().min(1).describe('Job ID or slug to run immediately.'),
32
+ limit: z.coerce.number().int().positive().optional().describe('Max new items to qualify.'),
33
+ };
34
+ export const snoopyAnalyticsToolInputSchema = {
35
+ jobRef: z.string().optional().describe('Job ID or slug. If omitted, returns analytics for all jobs.'),
36
+ days: z.coerce.number().int().positive().optional().describe('Lookback window in days. Default: 30.'),
37
+ };
38
+ export const snoopyExportToolInputSchema = {
39
+ jobRef: z.string().optional().describe('Job ID or slug. If omitted, exports all jobs.'),
40
+ format: z.enum(['json', 'csv']).optional().describe('Export format. Default: "json".'),
41
+ lastRun: z.boolean().optional().describe('Only export items from the latest run.'),
42
+ limit: z.coerce.number().int().positive().optional().describe('Max rows per job. Default: 100.'),
43
+ };
44
+ export const snoopyConsumeToolInputSchema = {
45
+ jobRef: z.string().optional().describe('Job ID or slug. If omitted, applies to all jobs.'),
46
+ limit: z.coerce.number().int().positive().optional().describe('Max results to consume.'),
47
+ dryRun: z.boolean().optional().describe('Preview without marking consumed.'),
48
+ };
49
+ export const snoopyErrorsToolInputSchema = {
50
+ jobRef: z.string().min(1).describe('Job ID or slug.'),
51
+ hours: z.coerce.number().int().positive().optional().describe('Look back hours. Default: 24.'),
52
+ };
53
+ export const snoopyLogsToolInputSchema = {
54
+ runId: z.string().min(1).describe('Run ID to view logs for.'),
55
+ };
56
+ export const snoopySettingsGetToolInputSchema = {};
57
+ export const snoopySettingsSetToolInputSchema = {
58
+ key: z.enum([
59
+ 'model',
60
+ 'temperature',
61
+ 'maxTokens',
62
+ 'topP',
63
+ 'cronIntervalMinutes',
64
+ 'jobTimeoutMs',
65
+ 'notificationsEnabled',
66
+ ]).describe('Setting key to update.'),
67
+ value: z.string().describe('New value for the setting.'),
68
+ };
69
+ export const snoopyToolContracts = [
70
+ { name: 'snoopy_doctor', required: [], enums: {} },
71
+ { name: 'snoopy_daemon_status', required: [], enums: {} },
72
+ { name: 'snoopy_daemon_start', required: [], enums: {} },
73
+ { name: 'snoopy_daemon_stop', required: [], enums: {} },
74
+ { name: 'snoopy_daemon_reload', required: [], enums: {} },
75
+ { name: 'snoopy_job_list', required: [], enums: {} },
76
+ { name: 'snoopy_job_runs', required: [], enums: {} },
77
+ { name: 'snoopy_job_add', required: ['name', 'subreddits', 'qualificationPrompt'], enums: {} },
78
+ { name: 'snoopy_job_delete', required: ['jobRef'], enums: {} },
79
+ { name: 'snoopy_job_enable', required: ['jobRef'], enums: {} },
80
+ { name: 'snoopy_job_disable', required: ['jobRef'], enums: {} },
81
+ { name: 'snoopy_job_run', required: ['jobRef'], enums: {} },
82
+ { name: 'snoopy_analytics', required: [], enums: {} },
83
+ { name: 'snoopy_export', required: [], enums: { format: ['json', 'csv'] } },
84
+ { name: 'snoopy_consume', required: [], enums: {} },
85
+ { name: 'snoopy_errors', required: ['jobRef'], enums: {} },
86
+ { name: 'snoopy_logs', required: ['runId'], enums: {} },
87
+ { name: 'snoopy_settings_get', required: [], enums: {} },
88
+ { name: 'snoopy_settings_set', required: ['key', 'value'], enums: { key: ['model', 'temperature', 'maxTokens', 'topP', 'cronIntervalMinutes', 'jobTimeoutMs', 'notificationsEnabled'] } },
89
+ ];
90
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,7 @@
1
+ import type Database from 'better-sqlite3';
2
+ declare const _default: {
3
+ id: number;
4
+ name: string;
5
+ up(db: Database.Database): void;
6
+ };
7
+ export default _default;
@@ -0,0 +1,132 @@
1
+ export default {
2
+ id: 1,
3
+ name: 'baseline',
4
+ up(db) {
5
+ db.exec(`
6
+ CREATE TABLE IF NOT EXISTS settings (
7
+ key TEXT PRIMARY KEY,
8
+ value TEXT NOT NULL,
9
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10
+ );
11
+
12
+ CREATE TABLE IF NOT EXISTS jobs (
13
+ id TEXT PRIMARY KEY,
14
+ slug TEXT UNIQUE,
15
+ name TEXT NOT NULL UNIQUE,
16
+ description TEXT NOT NULL,
17
+ qualification_prompt TEXT NOT NULL,
18
+ subreddits_json TEXT NOT NULL,
19
+ schedule_cron TEXT NOT NULL DEFAULT '*/30 * * * *',
20
+ enabled INTEGER NOT NULL DEFAULT 1,
21
+ monitor_comments INTEGER NOT NULL DEFAULT 1,
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
23
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS job_runs (
27
+ id TEXT PRIMARY KEY,
28
+ job_id TEXT NOT NULL,
29
+ status TEXT NOT NULL,
30
+ message TEXT,
31
+ started_at TEXT,
32
+ finished_at TEXT,
33
+ items_discovered INTEGER NOT NULL DEFAULT 0,
34
+ items_new INTEGER NOT NULL DEFAULT 0,
35
+ items_qualified INTEGER NOT NULL DEFAULT 0,
36
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
37
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
38
+ estimated_cost_usd REAL,
39
+ log_file_path TEXT,
40
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
41
+ FOREIGN KEY (job_id) REFERENCES jobs(id)
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS scan_items (
45
+ id TEXT PRIMARY KEY,
46
+ job_id TEXT NOT NULL,
47
+ run_id TEXT NOT NULL,
48
+ type TEXT NOT NULL CHECK (type IN ('post', 'comment')),
49
+ reddit_post_id TEXT NOT NULL,
50
+ reddit_comment_id TEXT,
51
+ subreddit TEXT NOT NULL,
52
+ author TEXT NOT NULL,
53
+ title TEXT,
54
+ body TEXT NOT NULL,
55
+ url TEXT NOT NULL,
56
+ reddit_posted_at TEXT NOT NULL,
57
+ qualified INTEGER NOT NULL DEFAULT 0,
58
+ viewed INTEGER NOT NULL DEFAULT 0,
59
+ validated INTEGER NOT NULL DEFAULT 0,
60
+ processed INTEGER NOT NULL DEFAULT 0,
61
+ consumed INTEGER NOT NULL DEFAULT 0,
62
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
63
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
64
+ estimated_cost_usd REAL,
65
+ qualification_reason TEXT,
66
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
67
+ FOREIGN KEY (job_id) REFERENCES jobs(id),
68
+ FOREIGN KEY (run_id) REFERENCES job_runs(id)
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS comment_thread_nodes (
72
+ id TEXT PRIMARY KEY,
73
+ scan_item_id TEXT NOT NULL,
74
+ reddit_comment_id TEXT NOT NULL,
75
+ parent_reddit_comment_id TEXT,
76
+ author TEXT NOT NULL,
77
+ body TEXT NOT NULL,
78
+ depth INTEGER NOT NULL,
79
+ is_target INTEGER NOT NULL DEFAULT 0,
80
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
81
+ FOREIGN KEY (scan_item_id) REFERENCES scan_items(id)
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS daemon_state (
85
+ id INTEGER PRIMARY KEY CHECK (id = 1),
86
+ is_running INTEGER NOT NULL,
87
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
88
+ );
89
+ `);
90
+ // Belt-and-suspenders: these columns were historically added via inline
91
+ // ALTER TABLE blocks. For edge-case databases that may be missing them,
92
+ // we safely attempt to add each column and ignore "already exists" errors.
93
+ const safeAddColumn = (table, column, type) => {
94
+ try {
95
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
96
+ }
97
+ catch {
98
+ // Column already exists or table does not exist.
99
+ }
100
+ };
101
+ safeAddColumn('jobs', 'slug', 'TEXT');
102
+ safeAddColumn('jobs', 'monitor_comments', 'INTEGER NOT NULL DEFAULT 1');
103
+ safeAddColumn('job_runs', 'started_at', 'TEXT');
104
+ safeAddColumn('job_runs', 'finished_at', 'TEXT');
105
+ safeAddColumn('job_runs', 'items_discovered', 'INTEGER NOT NULL DEFAULT 0');
106
+ safeAddColumn('job_runs', 'items_new', 'INTEGER NOT NULL DEFAULT 0');
107
+ safeAddColumn('job_runs', 'items_qualified', 'INTEGER NOT NULL DEFAULT 0');
108
+ safeAddColumn('job_runs', 'prompt_tokens', 'INTEGER NOT NULL DEFAULT 0');
109
+ safeAddColumn('job_runs', 'completion_tokens', 'INTEGER NOT NULL DEFAULT 0');
110
+ safeAddColumn('job_runs', 'estimated_cost_usd', 'REAL');
111
+ safeAddColumn('job_runs', 'log_file_path', 'TEXT');
112
+ safeAddColumn('scan_items', 'viewed', 'INTEGER NOT NULL DEFAULT 0');
113
+ safeAddColumn('scan_items', 'validated', 'INTEGER NOT NULL DEFAULT 0');
114
+ safeAddColumn('scan_items', 'processed', 'INTEGER NOT NULL DEFAULT 0');
115
+ safeAddColumn('scan_items', 'consumed', 'INTEGER NOT NULL DEFAULT 0');
116
+ safeAddColumn('scan_items', 'prompt_tokens', 'INTEGER NOT NULL DEFAULT 0');
117
+ safeAddColumn('scan_items', 'completion_tokens', 'INTEGER NOT NULL DEFAULT 0');
118
+ safeAddColumn('scan_items', 'estimated_cost_usd', 'REAL');
119
+ db.exec(`
120
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_slug ON jobs(slug);
121
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_job_runs_active_job ON job_runs(job_id) WHERE status = 'running';
122
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_scan_items_dedup ON scan_items(job_id, reddit_post_id, COALESCE(reddit_comment_id, ''));
123
+ CREATE INDEX IF NOT EXISTS idx_scan_items_job_qualified_posted ON scan_items(job_id, qualified, reddit_posted_at DESC, created_at DESC);
124
+ CREATE INDEX IF NOT EXISTS idx_scan_items_job_created ON scan_items(job_id, created_at DESC);
125
+ CREATE INDEX IF NOT EXISTS idx_scan_items_created ON scan_items(created_at DESC);
126
+ CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_scan_item_depth ON comment_thread_nodes(scan_item_id, depth ASC);
127
+ CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_parent ON comment_thread_nodes(parent_reddit_comment_id);
128
+ CREATE INDEX IF NOT EXISTS idx_scan_items_consumed ON scan_items(job_id, qualified, consumed, created_at DESC);
129
+ `);
130
+ }
131
+ };
132
+ //# sourceMappingURL=001_baseline.js.map
@@ -0,0 +1,7 @@
1
+ import type Database from 'better-sqlite3';
2
+ export interface Migration {
3
+ id: number;
4
+ name: string;
5
+ up: (db: Database.Database) => void;
6
+ }
7
+ export declare const migrations: Migration[];
@@ -0,0 +1,3 @@
1
+ import baseline from './001_baseline.js';
2
+ export const migrations = [baseline];
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type Database from 'better-sqlite3';
2
+ import { type Migration } from './index.js';
3
+ export declare function runMigrations(db: Database.Database): void;
4
+ export declare function getAppliedMigrations(db: Database.Database): Array<{
5
+ id: number;
6
+ name: string;
7
+ appliedAt: string;
8
+ }>;
9
+ export declare function getPendingMigrations(db: Database.Database): Migration[];
@@ -0,0 +1,43 @@
1
+ import { migrations } from './index.js';
2
+ export function runMigrations(db) {
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS migrations (
5
+ id INTEGER PRIMARY KEY,
6
+ name TEXT NOT NULL,
7
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
8
+ )
9
+ `);
10
+ const appliedRows = db.prepare('SELECT id FROM migrations').all();
11
+ const appliedIds = new Set(appliedRows.map((r) => r.id));
12
+ for (const migration of migrations) {
13
+ if (appliedIds.has(migration.id)) {
14
+ continue;
15
+ }
16
+ const runInTransaction = db.transaction((mig) => {
17
+ mig.up(db);
18
+ db.prepare('INSERT INTO migrations (id, name) VALUES (?, ?)').run(mig.id, mig.name);
19
+ });
20
+ runInTransaction(migration);
21
+ }
22
+ }
23
+ export function getAppliedMigrations(db) {
24
+ try {
25
+ return db
26
+ .prepare('SELECT id, name, applied_at as appliedAt FROM migrations ORDER BY id ASC')
27
+ .all();
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ }
33
+ export function getPendingMigrations(db) {
34
+ try {
35
+ const appliedRows = db.prepare('SELECT id FROM migrations').all();
36
+ const appliedIds = new Set(appliedRows.map((r) => r.id));
37
+ return migrations.filter((m) => !appliedIds.has(m.id));
38
+ }
39
+ catch {
40
+ return [...migrations];
41
+ }
42
+ }
43
+ //# sourceMappingURL=runner.js.map
@@ -1,5 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { ensureAppDirs } from '../../utils/paths.js';
3
+ import { runMigrations } from './migrations/runner.js';
3
4
  let db = null;
4
5
  export function getDb() {
5
6
  if (db) {
@@ -13,207 +14,7 @@ export function getDb() {
13
14
  catch {
14
15
  // In rare concurrent startup cases (for example tests), DB may already be locked.
15
16
  }
16
- db.exec(`
17
- CREATE TABLE IF NOT EXISTS settings (
18
- key TEXT PRIMARY KEY,
19
- value TEXT NOT NULL,
20
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
21
- );
22
-
23
- CREATE TABLE IF NOT EXISTS jobs (
24
- id TEXT PRIMARY KEY,
25
- slug TEXT UNIQUE,
26
- name TEXT NOT NULL UNIQUE,
27
- description TEXT NOT NULL,
28
- qualification_prompt TEXT NOT NULL,
29
- subreddits_json TEXT NOT NULL,
30
- schedule_cron TEXT NOT NULL DEFAULT '*/30 * * * *',
31
- enabled INTEGER NOT NULL DEFAULT 1,
32
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
34
- );
35
-
36
- CREATE TABLE IF NOT EXISTS job_runs (
37
- id TEXT PRIMARY KEY,
38
- job_id TEXT NOT NULL,
39
- status TEXT NOT NULL,
40
- message TEXT,
41
- started_at TEXT,
42
- finished_at TEXT,
43
- items_discovered INTEGER NOT NULL DEFAULT 0,
44
- items_new INTEGER NOT NULL DEFAULT 0,
45
- items_qualified INTEGER NOT NULL DEFAULT 0,
46
- prompt_tokens INTEGER NOT NULL DEFAULT 0,
47
- completion_tokens INTEGER NOT NULL DEFAULT 0,
48
- estimated_cost_usd REAL,
49
- log_file_path TEXT,
50
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
51
- FOREIGN KEY (job_id) REFERENCES jobs(id)
52
- );
53
-
54
- CREATE TABLE IF NOT EXISTS scan_items (
55
- id TEXT PRIMARY KEY,
56
- job_id TEXT NOT NULL,
57
- run_id TEXT NOT NULL,
58
- type TEXT NOT NULL CHECK (type IN ('post', 'comment')),
59
- reddit_post_id TEXT NOT NULL,
60
- reddit_comment_id TEXT,
61
- subreddit TEXT NOT NULL,
62
- author TEXT NOT NULL,
63
- title TEXT,
64
- body TEXT NOT NULL,
65
- url TEXT NOT NULL,
66
- reddit_posted_at TEXT NOT NULL,
67
- qualified INTEGER NOT NULL DEFAULT 0,
68
- viewed INTEGER NOT NULL DEFAULT 0,
69
- validated INTEGER NOT NULL DEFAULT 0,
70
- processed INTEGER NOT NULL DEFAULT 0,
71
- consumed INTEGER NOT NULL DEFAULT 0,
72
- prompt_tokens INTEGER NOT NULL DEFAULT 0,
73
- completion_tokens INTEGER NOT NULL DEFAULT 0,
74
- estimated_cost_usd REAL,
75
- qualification_reason TEXT,
76
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
77
- FOREIGN KEY (job_id) REFERENCES jobs(id),
78
- FOREIGN KEY (run_id) REFERENCES job_runs(id)
79
- );
80
-
81
- CREATE TABLE IF NOT EXISTS comment_thread_nodes (
82
- id TEXT PRIMARY KEY,
83
- scan_item_id TEXT NOT NULL,
84
- reddit_comment_id TEXT NOT NULL,
85
- parent_reddit_comment_id TEXT,
86
- author TEXT NOT NULL,
87
- body TEXT NOT NULL,
88
- depth INTEGER NOT NULL,
89
- is_target INTEGER NOT NULL DEFAULT 0,
90
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
91
- FOREIGN KEY (scan_item_id) REFERENCES scan_items(id)
92
- );
93
-
94
- CREATE TABLE IF NOT EXISTS daemon_state (
95
- id INTEGER PRIMARY KEY CHECK (id = 1),
96
- is_running INTEGER NOT NULL,
97
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
98
- );
99
- `);
100
- try {
101
- db.exec('ALTER TABLE jobs ADD COLUMN slug TEXT');
102
- }
103
- catch {
104
- // Column already exists.
105
- }
106
- try {
107
- db.exec('ALTER TABLE jobs ADD COLUMN monitor_comments INTEGER NOT NULL DEFAULT 1');
108
- }
109
- catch {
110
- // Column already exists.
111
- }
112
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_slug ON jobs(slug)');
113
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_job_runs_active_job ON job_runs(job_id) WHERE status = 'running'");
114
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_scan_items_dedup ON scan_items(job_id, reddit_post_id, COALESCE(reddit_comment_id, ''))");
115
- db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_job_qualified_posted ON scan_items(job_id, qualified, reddit_posted_at DESC, created_at DESC)');
116
- db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_job_created ON scan_items(job_id, created_at DESC)');
117
- db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_created ON scan_items(created_at DESC)');
118
- db.exec('CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_scan_item_depth ON comment_thread_nodes(scan_item_id, depth ASC)');
119
- db.exec('CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_parent ON comment_thread_nodes(parent_reddit_comment_id)');
120
- try {
121
- db.exec('ALTER TABLE job_runs ADD COLUMN started_at TEXT');
122
- }
123
- catch {
124
- // Column already exists.
125
- }
126
- try {
127
- db.exec('ALTER TABLE job_runs ADD COLUMN finished_at TEXT');
128
- }
129
- catch {
130
- // Column already exists.
131
- }
132
- try {
133
- db.exec('ALTER TABLE job_runs ADD COLUMN items_discovered INTEGER NOT NULL DEFAULT 0');
134
- }
135
- catch {
136
- // Column already exists.
137
- }
138
- try {
139
- db.exec('ALTER TABLE job_runs ADD COLUMN items_new INTEGER NOT NULL DEFAULT 0');
140
- }
141
- catch {
142
- // Column already exists.
143
- }
144
- try {
145
- db.exec('ALTER TABLE job_runs ADD COLUMN items_qualified INTEGER NOT NULL DEFAULT 0');
146
- }
147
- catch {
148
- // Column already exists.
149
- }
150
- try {
151
- db.exec('ALTER TABLE job_runs ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0');
152
- }
153
- catch {
154
- // Column already exists.
155
- }
156
- try {
157
- db.exec('ALTER TABLE job_runs ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0');
158
- }
159
- catch {
160
- // Column already exists.
161
- }
162
- try {
163
- db.exec('ALTER TABLE job_runs ADD COLUMN estimated_cost_usd REAL');
164
- }
165
- catch {
166
- // Column already exists.
167
- }
168
- try {
169
- db.exec('ALTER TABLE job_runs ADD COLUMN log_file_path TEXT');
170
- }
171
- catch {
172
- // Column already exists.
173
- }
174
- try {
175
- db.exec('ALTER TABLE scan_items ADD COLUMN viewed INTEGER NOT NULL DEFAULT 0');
176
- }
177
- catch {
178
- // Column already exists.
179
- }
180
- try {
181
- db.exec('ALTER TABLE scan_items ADD COLUMN validated INTEGER NOT NULL DEFAULT 0');
182
- }
183
- catch {
184
- // Column already exists.
185
- }
186
- try {
187
- db.exec('ALTER TABLE scan_items ADD COLUMN processed INTEGER NOT NULL DEFAULT 0');
188
- }
189
- catch {
190
- // Column already exists.
191
- }
192
- try {
193
- db.exec('ALTER TABLE scan_items ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0');
194
- }
195
- catch {
196
- // Column already exists.
197
- }
198
- try {
199
- db.exec('ALTER TABLE scan_items ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0');
200
- }
201
- catch {
202
- // Column already exists.
203
- }
204
- try {
205
- db.exec('ALTER TABLE scan_items ADD COLUMN estimated_cost_usd REAL');
206
- }
207
- catch {
208
- // Column already exists.
209
- }
210
- try {
211
- db.exec('ALTER TABLE scan_items ADD COLUMN consumed INTEGER NOT NULL DEFAULT 0');
212
- }
213
- catch {
214
- // Column already exists.
215
- }
216
- db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_consumed ON scan_items(job_id, qualified, consumed, created_at DESC)');
17
+ runMigrations(db);
217
18
  return db;
218
19
  }
219
20
  //# sourceMappingURL=sqlite.js.map
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  export function getAppPaths() {
5
- const rootDir = process.env.SNOOPY_ROOT_DIR || path.join(os.homedir(), '.snoopy');
5
+ const rootDir = process.env.SNOOPY_E2E_ROOT_DIR || process.env.SNOOPY_ROOT_DIR || path.join(os.homedir(), '.snoopy');
6
6
  return {
7
7
  rootDir,
8
8
  dbPath: path.join(rootDir, 'snoopy.db'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telepat/snoopy",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Snoopy CLI for Reddit conversation monitoring jobs.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -25,6 +25,9 @@
25
25
  "docs:serve": "npm --prefix website run serve",
26
26
  "docs:deploy": "npm --prefix website run deploy",
27
27
  "e2e:smoke": "tsx src/scripts/e2eSmoke.ts",
28
+ "e2e:fresh": "tsx tests/e2e/freshInstall.ts",
29
+ "e2e:upgrade": "tsx tests/e2e/upgrade.ts",
30
+ "e2e": "npm run e2e:fresh && npm run e2e:upgrade",
28
31
  "lint": "eslint src tests --ext .ts,.tsx",
29
32
  "test": "jest",
30
33
  "test:coverage": "jest --coverage",
@@ -50,6 +53,7 @@
50
53
  "license": "ISC",
51
54
  "dependencies": {
52
55
  "@inkjs/ui": "^2.0.0",
56
+ "@modelcontextprotocol/sdk": "^1.12.1",
53
57
  "better-sqlite3": "^12.8.0",
54
58
  "commander": "^14.0.3",
55
59
  "ink": "^6.8.0",