@telepat/snoopy 0.1.11 → 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 +27 -16
- package/dist/src/cli/commands/consume.d.ts +7 -0
- package/dist/src/cli/commands/consume.js +60 -0
- package/dist/src/cli/commands/doctor.js +13 -2
- package/dist/src/cli/index.js +11 -0
- package/dist/src/services/db/migrations/001_baseline.d.ts +7 -0
- package/dist/src/services/db/migrations/001_baseline.js +132 -0
- package/dist/src/services/db/migrations/index.d.ts +7 -0
- package/dist/src/services/db/migrations/index.js +3 -0
- package/dist/src/services/db/migrations/runner.d.ts +9 -0
- package/dist/src/services/db/migrations/runner.js +43 -0
- package/dist/src/services/db/repositories/scanItemsRepo.d.ts +5 -0
- package/dist/src/services/db/repositories/scanItemsRepo.js +63 -5
- package/dist/src/services/db/sqlite.js +2 -193
- package/dist/src/utils/paths.js +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
<p align="center"><img src="./snoopy-logo.webp" width="128" alt="Snoopy"></p>
|
|
2
|
+
<h1 align="center">Snoopy</h1>
|
|
3
|
+
<hr>
|
|
4
|
+
<p align="center"><em>Sniff out the conversations that matter.</em></p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://docs.telepat.io/snoopy">📖 Docs</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
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>
|
|
14
16
|
|
|
15
17
|
Snoopy helps you monitor Reddit for high-intent conversations that match your business goals.
|
|
16
18
|
|
|
@@ -159,7 +161,15 @@ snoopy export
|
|
|
159
161
|
snoopy export <jobRef> --json --last-run
|
|
160
162
|
```
|
|
161
163
|
|
|
162
|
-
8.
|
|
164
|
+
8. Consume unconsumed qualified results (read-once, most recent first):
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
snoopy consume
|
|
168
|
+
snoopy consume <jobRef> --limit 10
|
|
169
|
+
snoopy consume <jobRef> --json --dry-run
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
9. Inspect one run's detailed log output:
|
|
163
173
|
|
|
164
174
|
```bash
|
|
165
175
|
snoopy logs
|
|
@@ -169,13 +179,13 @@ snoopy logs <runId> --raw
|
|
|
169
179
|
|
|
170
180
|
When `runId` is omitted for `logs`, Snoopy first prompts for a job, then prompts for a run from that job (up/down arrows + Enter).
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
10. Show recent errors for one job:
|
|
173
183
|
|
|
174
184
|
```bash
|
|
175
185
|
snoopy errors <jobRef>
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
11. Enable daemon mode:
|
|
179
189
|
|
|
180
190
|
```bash
|
|
181
191
|
snoopy daemon start
|
|
@@ -191,6 +201,7 @@ snoopy daemon reload
|
|
|
191
201
|
- `analytics [jobRef] --days <N>`
|
|
192
202
|
- `results [jobRef]`
|
|
193
203
|
- `export [jobRef] --csv|--json [--last-run]`
|
|
204
|
+
- `consume [jobRef] [--limit <N>] [--json] [--dry-run]`
|
|
194
205
|
- `logs [runId]`
|
|
195
206
|
- `errors [jobRef] --hours <N>`
|
|
196
207
|
- `start [jobRef]` / `stop [jobRef]`
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { JobsRepository } from '../../services/db/repositories/jobsRepo.js';
|
|
2
|
+
import { ScanItemsRepository } from '../../services/db/repositories/scanItemsRepo.js';
|
|
3
|
+
import { printCommandScreen, printError, printInfo, printKeyValue, printMuted, printSuccess, printWarning } from '../ui/consoleUi.js';
|
|
4
|
+
function truncateContent(content, maxLength) {
|
|
5
|
+
if (content.length <= maxLength) {
|
|
6
|
+
return content;
|
|
7
|
+
}
|
|
8
|
+
return `${content.slice(0, maxLength)}...`;
|
|
9
|
+
}
|
|
10
|
+
function renderResultRow(row, index) {
|
|
11
|
+
printInfo(`#${index + 1}`);
|
|
12
|
+
printKeyValue('Job', row.jobId);
|
|
13
|
+
printKeyValue('Author', row.author);
|
|
14
|
+
if (row.title) {
|
|
15
|
+
printKeyValue('Title', row.title);
|
|
16
|
+
}
|
|
17
|
+
printKeyValue('URL', row.url);
|
|
18
|
+
printKeyValue('Date', row.redditPostedAt);
|
|
19
|
+
if (row.qualificationReason) {
|
|
20
|
+
printKeyValue('Reason', truncateContent(row.qualificationReason, 120));
|
|
21
|
+
}
|
|
22
|
+
printMuted(truncateContent(row.body, 200));
|
|
23
|
+
console.log('');
|
|
24
|
+
}
|
|
25
|
+
export function consumeResults(jobRef, options = {}) {
|
|
26
|
+
printCommandScreen('Consume results', 'List and consume unconsumed qualified results');
|
|
27
|
+
const jobsRepo = new JobsRepository();
|
|
28
|
+
const scanItemsRepo = new ScanItemsRepository();
|
|
29
|
+
let jobId;
|
|
30
|
+
if (jobRef) {
|
|
31
|
+
const job = jobsRepo.getByRef(jobRef);
|
|
32
|
+
if (!job) {
|
|
33
|
+
printError(`Job not found: ${jobRef}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
jobId = job.id;
|
|
37
|
+
}
|
|
38
|
+
const rows = scanItemsRepo.listUnconsumedQualified(jobId, options.limit);
|
|
39
|
+
if (rows.length === 0) {
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify([], null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
printWarning('No unconsumed qualified results found.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (options.json) {
|
|
48
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
rows.forEach((row, index) => renderResultRow(row, index));
|
|
52
|
+
}
|
|
53
|
+
if (options.dryRun) {
|
|
54
|
+
printSuccess(`Found ${rows.length} result(s) (dry run — not consumed).`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const consumedCount = scanItemsRepo.markConsumed(rows.map((r) => r.id));
|
|
58
|
+
printSuccess(`Consumed ${consumedCount} result(s).`);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=consume.js.map
|
|
@@ -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
|
-
|
|
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}`);
|
package/dist/src/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { daemonReload, daemonRun, daemonStart, daemonStatus, daemonStop } from '
|
|
|
7
7
|
import { showRunLogs } from './commands/logs.js';
|
|
8
8
|
import { showJobErrors } from './commands/errors.js';
|
|
9
9
|
import { exportCsv } from './commands/export.js';
|
|
10
|
+
import { consumeResults } from './commands/consume.js';
|
|
10
11
|
import { showAnalytics } from './commands/analytics.js';
|
|
11
12
|
import { showResults } from './commands/results.js';
|
|
12
13
|
import { disableStartupCommand, enableStartupCommand, installStartupCommand, startupStatusCommand, uninstallStartupCommand } from './commands/startup.js';
|
|
@@ -142,6 +143,16 @@ program
|
|
|
142
143
|
.action((jobRef, options) => {
|
|
143
144
|
exportCsv(jobRef, options);
|
|
144
145
|
});
|
|
146
|
+
program
|
|
147
|
+
.command('consume')
|
|
148
|
+
.argument('[jobRef]', 'Optional job ID or slug')
|
|
149
|
+
.description('List and mark unconsumed qualified results')
|
|
150
|
+
.option('--limit <count>', 'Maximum number of results to consume', parsePositiveInteger)
|
|
151
|
+
.option('--json', 'Output raw JSON array to stdout')
|
|
152
|
+
.option('--dry-run', 'Preview results without marking them consumed')
|
|
153
|
+
.action((jobRef, options) => {
|
|
154
|
+
consumeResults(jobRef, options);
|
|
155
|
+
});
|
|
145
156
|
program.parseAsync(process.argv).catch((error) => {
|
|
146
157
|
console.error(`Error: ${String(error)}`);
|
|
147
158
|
process.exit(1);
|
|
@@ -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,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
|
|
@@ -15,6 +15,7 @@ export interface NewScanItem {
|
|
|
15
15
|
viewed?: boolean;
|
|
16
16
|
validated?: boolean;
|
|
17
17
|
processed?: boolean;
|
|
18
|
+
consumed?: boolean;
|
|
18
19
|
promptTokens?: number;
|
|
19
20
|
completionTokens?: number;
|
|
20
21
|
estimatedCostUsd?: number | null;
|
|
@@ -46,6 +47,7 @@ export interface QualifiedScanItemRow {
|
|
|
46
47
|
viewed: boolean;
|
|
47
48
|
validated: boolean;
|
|
48
49
|
processed: boolean;
|
|
50
|
+
consumed: boolean;
|
|
49
51
|
qualificationReason: string | null;
|
|
50
52
|
createdAt: string;
|
|
51
53
|
}
|
|
@@ -66,6 +68,7 @@ export interface ScanItemRow {
|
|
|
66
68
|
viewed: boolean;
|
|
67
69
|
validated: boolean;
|
|
68
70
|
processed: boolean;
|
|
71
|
+
consumed: boolean;
|
|
69
72
|
qualificationReason: string | null;
|
|
70
73
|
promptTokens: number;
|
|
71
74
|
completionTokens: number;
|
|
@@ -113,6 +116,8 @@ export declare class ScanItemsRepository {
|
|
|
113
116
|
listAnalyticsBySubreddit(filter: AnalyticsFilter): AnalyticsBySubredditRow[];
|
|
114
117
|
listAnalyticsByJob(days: number): AnalyticsByJobRow[];
|
|
115
118
|
existsComment(jobId: string, postId: string, commentId: string): boolean;
|
|
119
|
+
listUnconsumedQualified(jobId?: string, limit?: number): QualifiedScanItemRow[];
|
|
120
|
+
markConsumed(ids: string[]): number;
|
|
116
121
|
createWithStatus(item: NewScanItem): CreateScanItemResult;
|
|
117
122
|
create(item: NewScanItem): string;
|
|
118
123
|
listCommentThreadNodes(scanItemId: string): CommentThreadNodeRow[];
|
|
@@ -31,7 +31,8 @@ export class ScanItemsRepository {
|
|
|
31
31
|
qualified: row.qualified === 1,
|
|
32
32
|
viewed: row.viewed === 1,
|
|
33
33
|
validated: row.validated === 1,
|
|
34
|
-
processed: row.processed === 1
|
|
34
|
+
processed: row.processed === 1,
|
|
35
|
+
consumed: row.consumed === 1
|
|
35
36
|
}));
|
|
36
37
|
}
|
|
37
38
|
buildFilterClause(alias, filter) {
|
|
@@ -70,6 +71,7 @@ export class ScanItemsRepository {
|
|
|
70
71
|
viewed,
|
|
71
72
|
validated,
|
|
72
73
|
processed,
|
|
74
|
+
consumed,
|
|
73
75
|
qualification_reason as qualificationReason,
|
|
74
76
|
created_at as createdAt
|
|
75
77
|
FROM scan_items
|
|
@@ -82,7 +84,8 @@ export class ScanItemsRepository {
|
|
|
82
84
|
...row,
|
|
83
85
|
viewed: row.viewed === 1,
|
|
84
86
|
validated: row.validated === 1,
|
|
85
|
-
processed: row.processed === 1
|
|
87
|
+
processed: row.processed === 1,
|
|
88
|
+
consumed: row.consumed === 1
|
|
86
89
|
}));
|
|
87
90
|
}
|
|
88
91
|
listQualifiedByJobRun(jobId, runId, limit = 100) {
|
|
@@ -100,6 +103,7 @@ export class ScanItemsRepository {
|
|
|
100
103
|
viewed,
|
|
101
104
|
validated,
|
|
102
105
|
processed,
|
|
106
|
+
consumed,
|
|
103
107
|
qualification_reason as qualificationReason,
|
|
104
108
|
created_at as createdAt
|
|
105
109
|
FROM scan_items
|
|
@@ -113,7 +117,8 @@ export class ScanItemsRepository {
|
|
|
113
117
|
...row,
|
|
114
118
|
viewed: row.viewed === 1,
|
|
115
119
|
validated: row.validated === 1,
|
|
116
|
-
processed: row.processed === 1
|
|
120
|
+
processed: row.processed === 1,
|
|
121
|
+
consumed: row.consumed === 1
|
|
117
122
|
}));
|
|
118
123
|
}
|
|
119
124
|
listByJob(jobId) {
|
|
@@ -135,6 +140,7 @@ export class ScanItemsRepository {
|
|
|
135
140
|
viewed,
|
|
136
141
|
validated,
|
|
137
142
|
processed,
|
|
143
|
+
consumed,
|
|
138
144
|
qualification_reason as qualificationReason,
|
|
139
145
|
prompt_tokens as promptTokens,
|
|
140
146
|
completion_tokens as completionTokens,
|
|
@@ -175,6 +181,7 @@ export class ScanItemsRepository {
|
|
|
175
181
|
viewed,
|
|
176
182
|
validated,
|
|
177
183
|
processed,
|
|
184
|
+
consumed,
|
|
178
185
|
qualification_reason as qualificationReason,
|
|
179
186
|
prompt_tokens as promptTokens,
|
|
180
187
|
completion_tokens as completionTokens,
|
|
@@ -283,6 +290,56 @@ export class ScanItemsRepository {
|
|
|
283
290
|
.get(jobId, postId, commentId);
|
|
284
291
|
return Boolean(row);
|
|
285
292
|
}
|
|
293
|
+
listUnconsumedQualified(jobId, limit) {
|
|
294
|
+
const hasJobId = Boolean(jobId);
|
|
295
|
+
const hasLimit = limit !== undefined && limit !== null;
|
|
296
|
+
const boundedLimit = hasLimit ? Math.max(1, Math.floor(limit)) : undefined;
|
|
297
|
+
let query = `SELECT
|
|
298
|
+
id,
|
|
299
|
+
job_id as jobId,
|
|
300
|
+
run_id as runId,
|
|
301
|
+
author,
|
|
302
|
+
title,
|
|
303
|
+
body,
|
|
304
|
+
url,
|
|
305
|
+
reddit_posted_at as redditPostedAt,
|
|
306
|
+
viewed,
|
|
307
|
+
validated,
|
|
308
|
+
processed,
|
|
309
|
+
consumed,
|
|
310
|
+
qualification_reason as qualificationReason,
|
|
311
|
+
created_at as createdAt
|
|
312
|
+
FROM scan_items
|
|
313
|
+
WHERE qualified = 1 AND consumed = 0`;
|
|
314
|
+
const params = [];
|
|
315
|
+
if (hasJobId) {
|
|
316
|
+
query += ' AND job_id = ?';
|
|
317
|
+
params.push(jobId);
|
|
318
|
+
}
|
|
319
|
+
query += ' ORDER BY datetime(created_at) DESC, id DESC';
|
|
320
|
+
if (boundedLimit !== undefined) {
|
|
321
|
+
query += ' LIMIT ?';
|
|
322
|
+
params.push(boundedLimit);
|
|
323
|
+
}
|
|
324
|
+
const rows = this.db.prepare(query).all(...params);
|
|
325
|
+
return rows.map((row) => ({
|
|
326
|
+
...row,
|
|
327
|
+
viewed: row.viewed === 1,
|
|
328
|
+
validated: row.validated === 1,
|
|
329
|
+
processed: row.processed === 1,
|
|
330
|
+
consumed: row.consumed === 1
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
markConsumed(ids) {
|
|
334
|
+
if (ids.length === 0) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
338
|
+
const result = this.db
|
|
339
|
+
.prepare(`UPDATE scan_items SET consumed = 1 WHERE id IN (${placeholders})`)
|
|
340
|
+
.run(...ids);
|
|
341
|
+
return Number(result.changes);
|
|
342
|
+
}
|
|
286
343
|
createWithStatus(item) {
|
|
287
344
|
const id = crypto.randomUUID();
|
|
288
345
|
const createInTransaction = this.db.transaction((newId, newItem) => {
|
|
@@ -304,13 +361,14 @@ export class ScanItemsRepository {
|
|
|
304
361
|
viewed,
|
|
305
362
|
validated,
|
|
306
363
|
processed,
|
|
364
|
+
consumed,
|
|
307
365
|
prompt_tokens,
|
|
308
366
|
completion_tokens,
|
|
309
367
|
estimated_cost_usd,
|
|
310
368
|
qualification_reason,
|
|
311
369
|
created_at
|
|
312
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
|
|
313
|
-
.run(newId, newItem.jobId, newItem.runId, newItem.type, newItem.redditPostId, newItem.redditCommentId, newItem.subreddit, newItem.author, newItem.title, newItem.body, newItem.url, newItem.redditPostedAt, newItem.qualified ? 1 : 0, newItem.viewed ? 1 : 0, newItem.validated ? 1 : 0, newItem.processed ? 1 : 0, newItem.promptTokens ?? 0, newItem.completionTokens ?? 0, newItem.estimatedCostUsd ?? null, newItem.qualificationReason);
|
|
370
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
|
|
371
|
+
.run(newId, newItem.jobId, newItem.runId, newItem.type, newItem.redditPostId, newItem.redditCommentId, newItem.subreddit, newItem.author, newItem.title, newItem.body, newItem.url, newItem.redditPostedAt, newItem.qualified ? 1 : 0, newItem.viewed ? 1 : 0, newItem.validated ? 1 : 0, newItem.processed ? 1 : 0, newItem.consumed ? 1 : 0, newItem.promptTokens ?? 0, newItem.completionTokens ?? 0, newItem.estimatedCostUsd ?? null, newItem.qualificationReason);
|
|
314
372
|
if (insertResult.changes === 0) {
|
|
315
373
|
const existingId = this.findExistingScanItemId(newItem.jobId, newItem.redditPostId, newItem.redditCommentId);
|
|
316
374
|
return {
|
|
@@ -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,199 +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
|
|
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
|
-
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
72
|
-
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
73
|
-
estimated_cost_usd REAL,
|
|
74
|
-
qualification_reason TEXT,
|
|
75
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
76
|
-
FOREIGN KEY (job_id) REFERENCES jobs(id),
|
|
77
|
-
FOREIGN KEY (run_id) REFERENCES job_runs(id)
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
CREATE TABLE IF NOT EXISTS comment_thread_nodes (
|
|
81
|
-
id TEXT PRIMARY KEY,
|
|
82
|
-
scan_item_id TEXT NOT NULL,
|
|
83
|
-
reddit_comment_id TEXT NOT NULL,
|
|
84
|
-
parent_reddit_comment_id TEXT,
|
|
85
|
-
author TEXT NOT NULL,
|
|
86
|
-
body TEXT NOT NULL,
|
|
87
|
-
depth INTEGER NOT NULL,
|
|
88
|
-
is_target INTEGER NOT NULL DEFAULT 0,
|
|
89
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
90
|
-
FOREIGN KEY (scan_item_id) REFERENCES scan_items(id)
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
CREATE TABLE IF NOT EXISTS daemon_state (
|
|
94
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
95
|
-
is_running INTEGER NOT NULL,
|
|
96
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
97
|
-
);
|
|
98
|
-
`);
|
|
99
|
-
try {
|
|
100
|
-
db.exec('ALTER TABLE jobs ADD COLUMN slug TEXT');
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Column already exists.
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
db.exec('ALTER TABLE jobs ADD COLUMN monitor_comments INTEGER NOT NULL DEFAULT 1');
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Column already exists.
|
|
110
|
-
}
|
|
111
|
-
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_slug ON jobs(slug)');
|
|
112
|
-
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_job_runs_active_job ON job_runs(job_id) WHERE status = 'running'");
|
|
113
|
-
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_scan_items_dedup ON scan_items(job_id, reddit_post_id, COALESCE(reddit_comment_id, ''))");
|
|
114
|
-
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)');
|
|
115
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_job_created ON scan_items(job_id, created_at DESC)');
|
|
116
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_scan_items_created ON scan_items(created_at DESC)');
|
|
117
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_scan_item_depth ON comment_thread_nodes(scan_item_id, depth ASC)');
|
|
118
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_comment_thread_nodes_parent ON comment_thread_nodes(parent_reddit_comment_id)');
|
|
119
|
-
try {
|
|
120
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN started_at TEXT');
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// Column already exists.
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN finished_at TEXT');
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Column already exists.
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN items_discovered INTEGER NOT NULL DEFAULT 0');
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// Column already exists.
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN items_new INTEGER NOT NULL DEFAULT 0');
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
// Column already exists.
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN items_qualified INTEGER NOT NULL DEFAULT 0');
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// Column already exists.
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0');
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// Column already exists.
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0');
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Column already exists.
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN estimated_cost_usd REAL');
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// Column already exists.
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
db.exec('ALTER TABLE job_runs ADD COLUMN log_file_path TEXT');
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Column already exists.
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN viewed INTEGER NOT NULL DEFAULT 0');
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
// Column already exists.
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN validated INTEGER NOT NULL DEFAULT 0');
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// Column already exists.
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN processed INTEGER NOT NULL DEFAULT 0');
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
// Column already exists.
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0');
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// Column already exists.
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0');
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// Column already exists.
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
db.exec('ALTER TABLE scan_items ADD COLUMN estimated_cost_usd REAL');
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
// Column already exists.
|
|
208
|
-
}
|
|
17
|
+
runMigrations(db);
|
|
209
18
|
return db;
|
|
210
19
|
}
|
|
211
20
|
//# sourceMappingURL=sqlite.js.map
|
package/dist/src/utils/paths.js
CHANGED
|
@@ -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.
|
|
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",
|