@telepat/snoopy 0.1.12 → 0.1.13

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 CHANGED
@@ -1,12 +1,18 @@
1
1
  <p align="center"><img src="./snoopy-logo.webp" width="128" alt="Snoopy"></p>
2
2
  <h1 align="center">Snoopy</h1>
3
+ <hr>
3
4
  <p align="center"><em>Sniff out the conversations that matter.</em></p>
4
5
 
5
- [![Build](https://img.shields.io/github/actions/workflow/status/telepat-io/snoopy/ci.yml?branch=main&label=build)](https://github.com/telepat-io/snoopy/actions/workflows/ci.yml)
6
- [![Coverage](https://codecov.io/gh/telepat-io/snoopy/graph/badge.svg)](https://codecov.io/gh/telepat-io/snoopy)
7
- [![npm](https://img.shields.io/npm/v/@telepat/snoopy)](https://www.npmjs.com/package/@telepat/snoopy)
6
+ <p align="center">
7
+ <a href="https://docs.telepat.io/snoopy">📖 Docs</a>
8
+ </p>
8
9
 
9
- 📖 [Full documentation](https://docs.telepat.io/ideon/)
10
+ <p align="center">
11
+ <a href="https://github.com/telepat-io/snoopy/actions/workflows/ci.yml"><img src="https://github.com/telepat-io/snoopy/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build"></a>
12
+ <a href="https://codecov.io/gh/telepat-io/snoopy"><img src="https://codecov.io/gh/telepat-io/snoopy/graph/badge.svg" alt="Codecov"></a>
13
+ <a href="https://www.npmjs.com/package/@telepat/snoopy"><img src="https://img.shields.io/npm/v/@telepat/snoopy" alt="npm"></a>
14
+ <a href="https://github.com/telepat-io/snoopy/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="License"></a>
15
+ </p>
10
16
 
11
17
  Snoopy helps you monitor Reddit for high-intent conversations that match your business goals.
12
18
 
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import { getDb } from '../../services/db/sqlite.js';
3
+ import { getAppliedMigrations, getPendingMigrations } from '../../services/db/migrations/runner.js';
3
4
  import { JobsRepository } from '../../services/db/repositories/jobsRepo.js';
4
5
  import { RunsRepository } from '../../services/db/repositories/runsRepo.js';
5
6
  import { extractErrorEntries, readRunLog } from '../../services/logging/logReader.js';
@@ -37,8 +38,9 @@ export async function runDoctor() {
37
38
  const paths = ensureAppDirs();
38
39
  let dbOk = false;
39
40
  let dbDetails = `DB file: ${paths.dbPath}`;
41
+ let db = null;
40
42
  try {
41
- const db = getDb();
43
+ db = getDb();
42
44
  db.prepare('SELECT 1').get();
43
45
  dbOk = true;
44
46
  dbDetails = `DB reachable at ${paths.dbPath}`;
@@ -56,8 +58,17 @@ export async function runDoctor() {
56
58
  const daemon = getDaemonHealth();
57
59
  printKeyValue('Platform', process.platform);
58
60
  printKeyValue('Node', process.version);
59
- if (dbOk) {
61
+ if (dbOk && db) {
60
62
  printSuccess(`Database: ${dbDetails}`);
63
+ const applied = getAppliedMigrations(db);
64
+ const pending = getPendingMigrations(db);
65
+ if (pending.length === 0) {
66
+ printSuccess(`Migrations: ${applied.length} applied, 0 pending`);
67
+ }
68
+ else {
69
+ printWarning(`Migrations: ${applied.length} applied, ${pending.length} pending`);
70
+ pending.forEach((m) => printMuted(` → pending: ${m.id} ${m.name}`));
71
+ }
61
72
  }
62
73
  else {
63
74
  printError(`Database: ${dbDetails}`);
@@ -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.13",
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",