@telepat/snoopy 0.1.4

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/dist/src/cli/commands/analytics.d.ts +3 -0
  4. package/dist/src/cli/commands/analytics.js +147 -0
  5. package/dist/src/cli/commands/analytics.js.map +1 -0
  6. package/dist/src/cli/commands/daemon.d.ts +5 -0
  7. package/dist/src/cli/commands/daemon.js +85 -0
  8. package/dist/src/cli/commands/daemon.js.map +1 -0
  9. package/dist/src/cli/commands/doctor.d.ts +1 -0
  10. package/dist/src/cli/commands/doctor.js +106 -0
  11. package/dist/src/cli/commands/doctor.js.map +1 -0
  12. package/dist/src/cli/commands/errors.d.ts +3 -0
  13. package/dist/src/cli/commands/errors.js +51 -0
  14. package/dist/src/cli/commands/errors.js.map +1 -0
  15. package/dist/src/cli/commands/export.d.ts +1 -0
  16. package/dist/src/cli/commands/export.js +48 -0
  17. package/dist/src/cli/commands/export.js.map +1 -0
  18. package/dist/src/cli/commands/job.d.ts +16 -0
  19. package/dist/src/cli/commands/job.js +350 -0
  20. package/dist/src/cli/commands/job.js.map +1 -0
  21. package/dist/src/cli/commands/logs.d.ts +3 -0
  22. package/dist/src/cli/commands/logs.js +44 -0
  23. package/dist/src/cli/commands/logs.js.map +1 -0
  24. package/dist/src/cli/commands/selection.d.ts +19 -0
  25. package/dist/src/cli/commands/selection.js +182 -0
  26. package/dist/src/cli/commands/selection.js.map +1 -0
  27. package/dist/src/cli/commands/settings.d.ts +1 -0
  28. package/dist/src/cli/commands/settings.js +31 -0
  29. package/dist/src/cli/commands/settings.js.map +1 -0
  30. package/dist/src/cli/commands/startup.d.ts +5 -0
  31. package/dist/src/cli/commands/startup.js +26 -0
  32. package/dist/src/cli/commands/startup.js.map +1 -0
  33. package/dist/src/cli/flows/jobAddFlow.d.ts +26 -0
  34. package/dist/src/cli/flows/jobAddFlow.js +209 -0
  35. package/dist/src/cli/flows/jobAddFlow.js.map +1 -0
  36. package/dist/src/cli/flows/settingsFlow.d.ts +15 -0
  37. package/dist/src/cli/flows/settingsFlow.js +180 -0
  38. package/dist/src/cli/flows/settingsFlow.js.map +1 -0
  39. package/dist/src/cli/flows/settingsFlowModel.d.ts +47 -0
  40. package/dist/src/cli/flows/settingsFlowModel.js +143 -0
  41. package/dist/src/cli/flows/settingsFlowModel.js.map +1 -0
  42. package/dist/src/cli/index.d.ts +2 -0
  43. package/dist/src/cli/index.js +138 -0
  44. package/dist/src/cli/index.js.map +1 -0
  45. package/dist/src/cli/ui/consoleUi.d.ts +13 -0
  46. package/dist/src/cli/ui/consoleUi.js +165 -0
  47. package/dist/src/cli/ui/consoleUi.js.map +1 -0
  48. package/dist/src/cli/ui/time.d.ts +9 -0
  49. package/dist/src/cli/ui/time.js +35 -0
  50. package/dist/src/cli/ui/time.js.map +1 -0
  51. package/dist/src/index.d.ts +1 -0
  52. package/dist/src/index.js +2 -0
  53. package/dist/src/index.js.map +1 -0
  54. package/dist/src/scripts/e2eSmoke.d.ts +1 -0
  55. package/dist/src/scripts/e2eSmoke.js +102 -0
  56. package/dist/src/scripts/e2eSmoke.js.map +1 -0
  57. package/dist/src/services/analytics/analyticsService.d.ts +50 -0
  58. package/dist/src/services/analytics/analyticsService.js +88 -0
  59. package/dist/src/services/analytics/analyticsService.js.map +1 -0
  60. package/dist/src/services/daemonControl.d.ts +12 -0
  61. package/dist/src/services/daemonControl.js +58 -0
  62. package/dist/src/services/daemonControl.js.map +1 -0
  63. package/dist/src/services/db/repositories/jobsRepo.d.ts +19 -0
  64. package/dist/src/services/db/repositories/jobsRepo.js +164 -0
  65. package/dist/src/services/db/repositories/jobsRepo.js.map +1 -0
  66. package/dist/src/services/db/repositories/runsRepo.d.ts +58 -0
  67. package/dist/src/services/db/repositories/runsRepo.js +190 -0
  68. package/dist/src/services/db/repositories/runsRepo.js.map +1 -0
  69. package/dist/src/services/db/repositories/scanItemsRepo.d.ts +69 -0
  70. package/dist/src/services/db/repositories/scanItemsRepo.js +176 -0
  71. package/dist/src/services/db/repositories/scanItemsRepo.js.map +1 -0
  72. package/dist/src/services/db/repositories/settingsRepo.d.ts +14 -0
  73. package/dist/src/services/db/repositories/settingsRepo.js +132 -0
  74. package/dist/src/services/db/repositories/settingsRepo.js.map +1 -0
  75. package/dist/src/services/db/sqlite.d.ts +2 -0
  76. package/dist/src/services/db/sqlite.js +192 -0
  77. package/dist/src/services/db/sqlite.js.map +1 -0
  78. package/dist/src/services/export/csvResults.d.ts +10 -0
  79. package/dist/src/services/export/csvResults.js +42 -0
  80. package/dist/src/services/export/csvResults.js.map +1 -0
  81. package/dist/src/services/logging/logReader.d.ts +4 -0
  82. package/dist/src/services/logging/logReader.js +230 -0
  83. package/dist/src/services/logging/logReader.js.map +1 -0
  84. package/dist/src/services/logging/logRotation.d.ts +1 -0
  85. package/dist/src/services/logging/logRotation.js +30 -0
  86. package/dist/src/services/logging/logRotation.js.map +1 -0
  87. package/dist/src/services/logging/runLogger.d.ts +9 -0
  88. package/dist/src/services/logging/runLogger.js +42 -0
  89. package/dist/src/services/logging/runLogger.js.map +1 -0
  90. package/dist/src/services/openrouter/client.d.ts +60 -0
  91. package/dist/src/services/openrouter/client.js +437 -0
  92. package/dist/src/services/openrouter/client.js.map +1 -0
  93. package/dist/src/services/openrouter/prompts.d.ts +5 -0
  94. package/dist/src/services/openrouter/prompts.js +48 -0
  95. package/dist/src/services/openrouter/prompts.js.map +1 -0
  96. package/dist/src/services/reddit/client.d.ts +25 -0
  97. package/dist/src/services/reddit/client.js +186 -0
  98. package/dist/src/services/reddit/client.js.map +1 -0
  99. package/dist/src/services/scheduler/cronScheduler.d.ts +11 -0
  100. package/dist/src/services/scheduler/cronScheduler.js +76 -0
  101. package/dist/src/services/scheduler/cronScheduler.js.map +1 -0
  102. package/dist/src/services/scheduler/jobRunner.d.ts +76 -0
  103. package/dist/src/services/scheduler/jobRunner.js +414 -0
  104. package/dist/src/services/scheduler/jobRunner.js.map +1 -0
  105. package/dist/src/services/scheduler/jobRunnerStub.d.ts +5 -0
  106. package/dist/src/services/scheduler/jobRunnerStub.js +11 -0
  107. package/dist/src/services/scheduler/jobRunnerStub.js.map +1 -0
  108. package/dist/src/services/security/secretStore.d.ts +6 -0
  109. package/dist/src/services/security/secretStore.js +193 -0
  110. package/dist/src/services/security/secretStore.js.map +1 -0
  111. package/dist/src/services/startup/index.d.ts +13 -0
  112. package/dist/src/services/startup/index.js +120 -0
  113. package/dist/src/services/startup/index.js.map +1 -0
  114. package/dist/src/services/startup/linuxCronFallback.d.ts +2 -0
  115. package/dist/src/services/startup/linuxCronFallback.js +29 -0
  116. package/dist/src/services/startup/linuxCronFallback.js.map +1 -0
  117. package/dist/src/services/startup/linuxSystemd.d.ts +3 -0
  118. package/dist/src/services/startup/linuxSystemd.js +47 -0
  119. package/dist/src/services/startup/linuxSystemd.js.map +1 -0
  120. package/dist/src/services/startup/macosLaunchd.d.ts +2 -0
  121. package/dist/src/services/startup/macosLaunchd.js +40 -0
  122. package/dist/src/services/startup/macosLaunchd.js.map +1 -0
  123. package/dist/src/services/startup/windowsRunFallback.d.ts +2 -0
  124. package/dist/src/services/startup/windowsRunFallback.js +17 -0
  125. package/dist/src/services/startup/windowsRunFallback.js.map +1 -0
  126. package/dist/src/services/startup/windowsTaskScheduler.d.ts +2 -0
  127. package/dist/src/services/startup/windowsTaskScheduler.js +16 -0
  128. package/dist/src/services/startup/windowsTaskScheduler.js.map +1 -0
  129. package/dist/src/types/job.d.ts +34 -0
  130. package/dist/src/types/job.js +2 -0
  131. package/dist/src/types/job.js.map +1 -0
  132. package/dist/src/types/settings.d.ts +35 -0
  133. package/dist/src/types/settings.js +8 -0
  134. package/dist/src/types/settings.js.map +1 -0
  135. package/dist/src/ui/components/AppFrame.d.ts +17 -0
  136. package/dist/src/ui/components/AppFrame.js +26 -0
  137. package/dist/src/ui/components/AppFrame.js.map +1 -0
  138. package/dist/src/ui/components/CliHeader.d.ts +8 -0
  139. package/dist/src/ui/components/CliHeader.js +8 -0
  140. package/dist/src/ui/components/CliHeader.js.map +1 -0
  141. package/dist/src/ui/components/SubredditMultiSelect.d.ts +7 -0
  142. package/dist/src/ui/components/SubredditMultiSelect.js +91 -0
  143. package/dist/src/ui/components/SubredditMultiSelect.js.map +1 -0
  144. package/dist/src/ui/components/TextPrompt.d.ts +10 -0
  145. package/dist/src/ui/components/TextPrompt.js +13 -0
  146. package/dist/src/ui/components/TextPrompt.js.map +1 -0
  147. package/dist/src/ui/components/YesNoSelector.d.ts +10 -0
  148. package/dist/src/ui/components/YesNoSelector.js +25 -0
  149. package/dist/src/ui/components/YesNoSelector.js.map +1 -0
  150. package/dist/src/ui/components/subredditOptions.d.ts +5 -0
  151. package/dist/src/ui/components/subredditOptions.js +14 -0
  152. package/dist/src/ui/components/subredditOptions.js.map +1 -0
  153. package/dist/src/ui/components/yesNoSelectorModel.d.ts +9 -0
  154. package/dist/src/ui/components/yesNoSelectorModel.js +23 -0
  155. package/dist/src/ui/components/yesNoSelectorModel.js.map +1 -0
  156. package/dist/src/ui/theme.d.ts +26 -0
  157. package/dist/src/ui/theme.js +37 -0
  158. package/dist/src/ui/theme.js.map +1 -0
  159. package/dist/src/utils/logger.d.ts +5 -0
  160. package/dist/src/utils/logger.js +15 -0
  161. package/dist/src/utils/logger.js.map +1 -0
  162. package/dist/src/utils/notify.d.ts +6 -0
  163. package/dist/src/utils/notify.js +14 -0
  164. package/dist/src/utils/notify.js.map +1 -0
  165. package/dist/src/utils/paths.d.ts +10 -0
  166. package/dist/src/utils/paths.js +24 -0
  167. package/dist/src/utils/paths.js.map +1 -0
  168. package/dist/src/utils/scanLogFormatting.d.ts +26 -0
  169. package/dist/src/utils/scanLogFormatting.js +60 -0
  170. package/dist/src/utils/scanLogFormatting.js.map +1 -0
  171. package/package.json +74 -0
@@ -0,0 +1,165 @@
1
+ import { SNOOPY_WORDMARK } from '../../ui/components/CliHeader.js';
2
+ import { uiTheme } from '../../ui/theme.js';
3
+ import { toSnippet } from '../../utils/scanLogFormatting.js';
4
+ const ANSI = {
5
+ reset: '\u001b[0m',
6
+ bold: '\u001b[1m',
7
+ cyan: '\u001b[36m',
8
+ blue: '\u001b[34m',
9
+ green: '\u001b[32m',
10
+ yellow: '\u001b[33m',
11
+ red: '\u001b[31m',
12
+ gray: '\u001b[90m'
13
+ };
14
+ let headerPrinted = false;
15
+ export function isRichTty() {
16
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
17
+ }
18
+ function colorize(value, color) {
19
+ if (!isRichTty()) {
20
+ return value;
21
+ }
22
+ return `${ANSI[color]}${value}${ANSI.reset}`;
23
+ }
24
+ function bold(value) {
25
+ if (!isRichTty()) {
26
+ return value;
27
+ }
28
+ return `${ANSI.bold}${value}${ANSI.reset}`;
29
+ }
30
+ function normalizeWhitespace(value) {
31
+ if (!value) {
32
+ return '';
33
+ }
34
+ return value.replace(/\s+/g, ' ').trim();
35
+ }
36
+ function formatQualifiedStatus(qualified) {
37
+ if (qualified === undefined) {
38
+ return colorize('pending', 'yellow');
39
+ }
40
+ return qualified ? colorize('qualified', 'green') : colorize('not qualified', 'red');
41
+ }
42
+ function formatLabel(label) {
43
+ return colorize(`${label}:`, uiTheme.ink.info);
44
+ }
45
+ function formatScanDetailLine(label, value) {
46
+ return ` ${formatLabel(label)} ${value}`;
47
+ }
48
+ function terminalColumns() {
49
+ return process.stdout.columns ?? 120;
50
+ }
51
+ function padLabel(label) {
52
+ const maxWidth = Math.max(10, Math.min(24, Math.floor(terminalColumns() / 4)));
53
+ return label.length >= maxWidth ? label : `${label}${' '.repeat(maxWidth - label.length)}`;
54
+ }
55
+ function formatQualificationReason(qualified, qualificationReason) {
56
+ const normalizedReason = normalizeWhitespace(qualificationReason);
57
+ if (qualified === undefined && !normalizedReason) {
58
+ return null;
59
+ }
60
+ return normalizedReason || 'No justification provided.';
61
+ }
62
+ export function formatPostScanBlock(input) {
63
+ const title = toSnippet(input.title, 80);
64
+ const snippet = toSnippet(input.bodySnippet);
65
+ const reason = formatQualificationReason(input.qualified, input.qualificationReason);
66
+ const lines = ['Post'];
67
+ lines.push(formatScanDetailLine('ID', input.postId));
68
+ if (title) {
69
+ lines.push(formatScanDetailLine('Title', title));
70
+ }
71
+ if (snippet) {
72
+ lines.push(formatScanDetailLine('Snippet', snippet));
73
+ }
74
+ lines.push(formatScanDetailLine('Status', formatQualifiedStatus(input.qualified)));
75
+ if (reason) {
76
+ lines.push(formatScanDetailLine('Reason', reason));
77
+ }
78
+ if (input.postUrl) {
79
+ lines.push(formatScanDetailLine('Post', input.postUrl));
80
+ }
81
+ if (typeof input.itemsNew === 'number' && typeof input.itemsQualified === 'number') {
82
+ lines.push(formatScanDetailLine('Totals', `new=${input.itemsNew}, qualified=${input.itemsQualified}`));
83
+ }
84
+ return lines.join('\n');
85
+ }
86
+ export function formatCommentScanBlock(input) {
87
+ const snippet = toSnippet(input.commentSnippet);
88
+ const reason = formatQualificationReason(input.qualified, input.qualificationReason);
89
+ const lines = ['Comment'];
90
+ lines.push(formatScanDetailLine('Comment ID', input.commentId));
91
+ lines.push(formatScanDetailLine('Post ID', input.postId));
92
+ lines.push(formatScanDetailLine('Author', input.author));
93
+ if (snippet) {
94
+ lines.push(formatScanDetailLine('Snippet', snippet));
95
+ }
96
+ lines.push(formatScanDetailLine('Status', formatQualifiedStatus(input.qualified)));
97
+ if (reason) {
98
+ lines.push(formatScanDetailLine('Reason', reason));
99
+ }
100
+ if (input.commentUrl) {
101
+ lines.push(formatScanDetailLine('Comment', input.commentUrl));
102
+ }
103
+ if (input.postUrl) {
104
+ lines.push(formatScanDetailLine('Post', input.postUrl));
105
+ }
106
+ if (typeof input.itemsNew === 'number' && typeof input.itemsQualified === 'number') {
107
+ lines.push(formatScanDetailLine('Totals', `new=${input.itemsNew}, qualified=${input.itemsQualified}`));
108
+ }
109
+ return lines.join('\n');
110
+ }
111
+ export function printCliHeader(subtitle = 'Reddit conversation scanner') {
112
+ if (!isRichTty() || headerPrinted) {
113
+ return;
114
+ }
115
+ headerPrinted = true;
116
+ console.log('');
117
+ console.log(colorize(bold(SNOOPY_WORDMARK), 'cyan'));
118
+ console.log(colorize(subtitle, 'blue'));
119
+ console.log('');
120
+ }
121
+ export function printCommandScreen(title, section) {
122
+ printCliHeader(title);
123
+ if (section) {
124
+ printSection(section);
125
+ }
126
+ if (isRichTty()) {
127
+ printMuted('Tab-friendly controls: arrows navigate selectors, Enter confirms, Esc cancels.');
128
+ }
129
+ }
130
+ export function printSection(title) {
131
+ if (isRichTty()) {
132
+ console.log(colorize(bold(title), 'cyan'));
133
+ console.log(colorize('─'.repeat(Math.max(8, Math.min(36, title.length + 4))), 'gray'));
134
+ return;
135
+ }
136
+ console.log(title);
137
+ }
138
+ export function printMuted(text) {
139
+ console.log(colorize(text, 'gray'));
140
+ }
141
+ export function printSuccess(text) {
142
+ const prefix = isRichTty() ? colorize('✓', 'green') : '[ok]';
143
+ console.log(`${prefix} ${text}`);
144
+ }
145
+ export function printWarning(text) {
146
+ const prefix = isRichTty() ? colorize('!', 'yellow') : '[warn]';
147
+ console.log(`${prefix} ${text}`);
148
+ }
149
+ export function printError(text) {
150
+ const prefix = isRichTty() ? colorize('✗', 'red') : '[error]';
151
+ console.log(`${prefix} ${text}`);
152
+ }
153
+ export function printInfo(text) {
154
+ const prefix = isRichTty() ? colorize(uiTheme.symbols.sentimentInfo, 'blue') : '-';
155
+ console.log(`${prefix} ${text}`);
156
+ }
157
+ export function printKeyValue(key, value) {
158
+ const label = `${padLabel(key)}:`;
159
+ if (isRichTty()) {
160
+ console.log(`${colorize(label, 'blue')} ${value}`);
161
+ return;
162
+ }
163
+ console.log(`${label} ${value}`);
164
+ }
165
+ //# sourceMappingURL=consoleUi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consoleUi.js","sourceRoot":"","sources":["../../../../src/cli/ui/consoleUi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ interface RunTimestampInput {
2
+ createdAt: string;
3
+ startedAt?: string | null;
4
+ finishedAt?: string | null;
5
+ }
6
+ export declare function formatLocalTimestamp(value: string | null | undefined): string;
7
+ export declare function formatRunDisplayTimestamp(run: RunTimestampInput): string;
8
+ export declare function formatRunDisplayLabel(run: RunTimestampInput): string;
9
+ export {};
@@ -0,0 +1,35 @@
1
+ function parseSqliteUtcTimestamp(value) {
2
+ if (!value) {
3
+ return null;
4
+ }
5
+ const normalized = value.includes('T') ? value : value.replace(' ', 'T');
6
+ const withTimezone = /[zZ]|[+-]\d{2}:?\d{2}$/.test(normalized) ? normalized : `${normalized}Z`;
7
+ const date = new Date(withTimezone);
8
+ return Number.isNaN(date.getTime()) ? null : date;
9
+ }
10
+ export function formatLocalTimestamp(value) {
11
+ const parsed = parseSqliteUtcTimestamp(value);
12
+ if (!parsed) {
13
+ return value ?? '-';
14
+ }
15
+ return new Intl.DateTimeFormat(undefined, {
16
+ year: 'numeric',
17
+ month: 'short',
18
+ day: 'numeric',
19
+ hour: 'numeric',
20
+ minute: '2-digit'
21
+ }).format(parsed);
22
+ }
23
+ export function formatRunDisplayTimestamp(run) {
24
+ return formatLocalTimestamp(run.finishedAt ?? run.startedAt ?? run.createdAt);
25
+ }
26
+ export function formatRunDisplayLabel(run) {
27
+ if (run.finishedAt) {
28
+ return `Completed ${formatLocalTimestamp(run.finishedAt)}`;
29
+ }
30
+ if (run.startedAt) {
31
+ return `Started ${formatLocalTimestamp(run.startedAt)}`;
32
+ }
33
+ return `Created ${formatLocalTimestamp(run.createdAt)}`;
34
+ }
35
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../../../../src/cli/ui/time.ts"],"names":[],"mappings":"AAMA,SAAS,uBAAuB,CAAC,KAAgC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACzE,MAAM,YAAY,GAAG,wBAAwB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,CAAC;IAC/F,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,MAAM,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,IAAI,GAAG,CAAC;IACtB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE;QACxC,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,OAAO;QACd,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;KAClB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,GAAsB;IAC9D,OAAO,oBAAoB,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAsB;IAC1D,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACnB,OAAO,aAAa,oBAAoB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;IAC7D,CAAC;IAED,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QAClB,OAAO,WAAW,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;IAC1D,CAAC;IAED,OAAO,WAAW,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import { JobsRepository } from '../services/db/repositories/jobsRepo.js';
2
+ import { RunsRepository } from '../services/db/repositories/runsRepo.js';
3
+ import { getOpenRouterApiKey } from '../services/security/secretStore.js';
4
+ import { removeJob, runInitialJobAndEnable } from '../cli/commands/job.js';
5
+ function parsePositiveInt(value, fallback) {
6
+ if (!value) {
7
+ return fallback;
8
+ }
9
+ const parsed = Number.parseInt(value, 10);
10
+ if (!Number.isInteger(parsed) || parsed <= 0) {
11
+ throw new Error(`Invalid positive integer: ${value}`);
12
+ }
13
+ return parsed;
14
+ }
15
+ function parseBoolean(value, fallback) {
16
+ if (!value) {
17
+ return fallback;
18
+ }
19
+ const normalized = value.trim().toLowerCase();
20
+ if (['1', 'true', 'yes', 'y'].includes(normalized)) {
21
+ return true;
22
+ }
23
+ if (['0', 'false', 'no', 'n'].includes(normalized)) {
24
+ return false;
25
+ }
26
+ throw new Error(`Invalid boolean: ${value}`);
27
+ }
28
+ function parseSubreddits(value) {
29
+ if (!value) {
30
+ return ['startups', 'entrepreneur'];
31
+ }
32
+ const parsed = value
33
+ .split(',')
34
+ .map((item) => item.trim().replace(/^r\//i, ''))
35
+ .filter((item) => item.length > 0);
36
+ if (parsed.length === 0) {
37
+ throw new Error('SNOOPY_E2E_SUBREDDITS must contain at least one subreddit.');
38
+ }
39
+ return parsed;
40
+ }
41
+ async function main() {
42
+ const openRouterKey = await getOpenRouterApiKey();
43
+ if (!openRouterKey) {
44
+ throw new Error('Cannot run smoke test: OpenRouter API key is not configured.');
45
+ }
46
+ const jobsRepo = new JobsRepository();
47
+ const runsRepo = new RunsRepository();
48
+ const runLimit = parsePositiveInt(process.env.SNOOPY_E2E_LIMIT, 5);
49
+ const keepJob = parseBoolean(process.env.SNOOPY_E2E_KEEP_JOB, false);
50
+ const subreddits = parseSubreddits(process.env.SNOOPY_E2E_SUBREDDITS);
51
+ const stamp = new Date().toISOString().replace(/[^0-9]/g, '').slice(0, 14);
52
+ const slug = `e2e-smoke-${stamp}`;
53
+ const job = jobsRepo.create({
54
+ slug,
55
+ name: `E2E Smoke ${stamp}`,
56
+ description: 'Temporary smoke-test job created by src/scripts/e2eSmoke.ts',
57
+ qualificationPrompt: 'Qualify content when the author is actively building or operating a startup, SaaS, or online business and is asking for concrete help, feedback, users, growth, hiring, product validation, or operations support. Return unqualified for broad news, memes, or non-builder chatter.',
58
+ subreddits,
59
+ scheduleCron: '*/30 * * * *',
60
+ enabled: false,
61
+ monitorComments: false
62
+ });
63
+ let deleted = false;
64
+ console.log(`[e2e-smoke] Created disabled job ${job.slug} (${job.id}) with subreddits: ${subreddits.join(', ')}`);
65
+ if (job.enabled) {
66
+ throw new Error('Expected smoke test job to start disabled before initial run attempt.');
67
+ }
68
+ try {
69
+ await runInitialJobAndEnable(job.id, {
70
+ limit: runLimit,
71
+ installSignalHandlers: false,
72
+ printLifecycleMessages: false
73
+ });
74
+ const refreshed = jobsRepo.getById(job.id);
75
+ if (!refreshed || !refreshed.enabled) {
76
+ throw new Error('Expected smoke test job to be enabled after initial run attempt.');
77
+ }
78
+ const latestRun = runsRepo.listByJob(job.id, 1)[0];
79
+ if (!latestRun) {
80
+ throw new Error('Expected smoke test to create a run record for the initial run attempt.');
81
+ }
82
+ console.log(`[e2e-smoke] Initial run status=${latestRun.status}, discovered=${latestRun.itemsDiscovered}, new=${latestRun.itemsNew}, qualified=${latestRun.itemsQualified}`);
83
+ }
84
+ finally {
85
+ if (!keepJob) {
86
+ removeJob(job.slug);
87
+ deleted = true;
88
+ }
89
+ }
90
+ if (deleted) {
91
+ console.log(`[e2e-smoke] Cleanup complete: deleted ${job.slug}`);
92
+ }
93
+ else {
94
+ console.log(`[e2e-smoke] Preserved job ${job.slug} because SNOOPY_E2E_KEEP_JOB=true`);
95
+ }
96
+ }
97
+ main().catch((error) => {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ console.error(`[e2e-smoke] ${message}`);
100
+ process.exit(1);
101
+ });
102
+ //# sourceMappingURL=e2eSmoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2eSmoke.js","sourceRoot":"","sources":["../../../src/scripts/e2eSmoke.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { type RunAnalyticsRow } from '../db/repositories/runsRepo.js';
2
+ import { type AnalyticsByJobRow, type AnalyticsBySubredditRow, type AnalyticsTotalsRow } from '../db/repositories/scanItemsRepo.js';
3
+ export interface DerivedAnalyticsMetrics extends AnalyticsTotalsRow {
4
+ totalTokens: number;
5
+ avgNewPostsPerDay: number;
6
+ avgNewCommentsPerDay: number;
7
+ avgTotalTokensPerDay: number;
8
+ avgEstimatedCostUsdPerDay: number;
9
+ tokensPerPost: number | null;
10
+ costPerPost: number | null;
11
+ }
12
+ export interface AnalyticsRunView extends RunAnalyticsRow {
13
+ durationSeconds: number | null;
14
+ totalTokens: number;
15
+ tokensPerPost: number | null;
16
+ costPerPost: number | null;
17
+ }
18
+ export interface AnalyticsGlobalView {
19
+ windowDays: number;
20
+ runCount: number;
21
+ totals: DerivedAnalyticsMetrics;
22
+ byJob: Array<AnalyticsByJobRow & {
23
+ metrics: DerivedAnalyticsMetrics;
24
+ }>;
25
+ bySubreddit: Array<AnalyticsBySubredditRow & {
26
+ metrics: DerivedAnalyticsMetrics;
27
+ }>;
28
+ recentRuns: AnalyticsRunView[];
29
+ }
30
+ export interface AnalyticsJobView {
31
+ windowDays: number;
32
+ runCount: number;
33
+ totals: DerivedAnalyticsMetrics;
34
+ bySubreddit: Array<AnalyticsBySubredditRow & {
35
+ metrics: DerivedAnalyticsMetrics;
36
+ }>;
37
+ recentRuns: AnalyticsRunView[];
38
+ }
39
+ export declare class AnalyticsService {
40
+ private readonly runsRepo;
41
+ private readonly scanItemsRepo;
42
+ getGlobalAnalytics(options?: {
43
+ days?: number;
44
+ runLimit?: number;
45
+ }): AnalyticsGlobalView;
46
+ getJobAnalytics(jobId: string, options?: {
47
+ days?: number;
48
+ runLimit?: number;
49
+ }): AnalyticsJobView;
50
+ }
@@ -0,0 +1,88 @@
1
+ import { RunsRepository } from '../db/repositories/runsRepo.js';
2
+ import { ScanItemsRepository } from '../db/repositories/scanItemsRepo.js';
3
+ const DEFAULT_DAYS = 30;
4
+ const DEFAULT_RUN_LIMIT = 20;
5
+ function normalizeDays(days) {
6
+ if (typeof days !== 'number' || !Number.isInteger(days) || days <= 0) {
7
+ return DEFAULT_DAYS;
8
+ }
9
+ return days;
10
+ }
11
+ function toDerivedMetrics(input, days) {
12
+ const totalTokens = input.promptTokens + input.completionTokens;
13
+ const tokensPerPost = input.newPosts > 0 ? totalTokens / input.newPosts : null;
14
+ const costPerPost = input.newPosts > 0 ? input.estimatedCostUsd / input.newPosts : null;
15
+ return {
16
+ ...input,
17
+ totalTokens,
18
+ avgNewPostsPerDay: input.newPosts / days,
19
+ avgNewCommentsPerDay: input.newComments / days,
20
+ avgTotalTokensPerDay: totalTokens / days,
21
+ avgEstimatedCostUsdPerDay: input.estimatedCostUsd / days,
22
+ tokensPerPost,
23
+ costPerPost
24
+ };
25
+ }
26
+ function toRunView(run) {
27
+ const started = run.startedAt ? Date.parse(run.startedAt) : Number.NaN;
28
+ const finished = run.finishedAt ? Date.parse(run.finishedAt) : Number.NaN;
29
+ const durationSeconds = Number.isNaN(started) || Number.isNaN(finished) || finished < started
30
+ ? null
31
+ : Math.round((finished - started) / 1000);
32
+ const totalTokens = run.promptTokens + run.completionTokens;
33
+ const tokensPerPost = run.newPosts > 0 ? totalTokens / run.newPosts : null;
34
+ const costPerPost = run.newPosts > 0 && run.estimatedCostUsd !== null ? run.estimatedCostUsd / run.newPosts : null;
35
+ return {
36
+ ...run,
37
+ durationSeconds,
38
+ totalTokens,
39
+ tokensPerPost,
40
+ costPerPost
41
+ };
42
+ }
43
+ export class AnalyticsService {
44
+ runsRepo = new RunsRepository();
45
+ scanItemsRepo = new ScanItemsRepository();
46
+ getGlobalAnalytics(options = {}) {
47
+ const days = normalizeDays(options.days);
48
+ const runLimit = options.runLimit ?? DEFAULT_RUN_LIMIT;
49
+ const totals = this.scanItemsRepo.getAnalyticsTotals({ days });
50
+ const bySubreddit = this.scanItemsRepo.listAnalyticsBySubreddit({ days });
51
+ const byJob = this.scanItemsRepo.listAnalyticsByJob(days);
52
+ const recentRuns = this.runsRepo.listAnalyticsRuns({ days, limit: runLimit }).map(toRunView);
53
+ const runCount = this.runsRepo.countRuns({ days });
54
+ return {
55
+ windowDays: days,
56
+ runCount,
57
+ totals: toDerivedMetrics(totals, days),
58
+ byJob: byJob.map((row) => ({
59
+ ...row,
60
+ metrics: toDerivedMetrics(row, days)
61
+ })),
62
+ bySubreddit: bySubreddit.map((row) => ({
63
+ ...row,
64
+ metrics: toDerivedMetrics(row, days)
65
+ })),
66
+ recentRuns
67
+ };
68
+ }
69
+ getJobAnalytics(jobId, options = {}) {
70
+ const days = normalizeDays(options.days);
71
+ const runLimit = options.runLimit ?? DEFAULT_RUN_LIMIT;
72
+ const totals = this.scanItemsRepo.getAnalyticsTotals({ jobId, days });
73
+ const bySubreddit = this.scanItemsRepo.listAnalyticsBySubreddit({ jobId, days });
74
+ const recentRuns = this.runsRepo.listAnalyticsRuns({ jobId, days, limit: runLimit }).map(toRunView);
75
+ const runCount = this.runsRepo.countRuns({ jobId, days });
76
+ return {
77
+ windowDays: days,
78
+ runCount,
79
+ totals: toDerivedMetrics(totals, days),
80
+ bySubreddit: bySubreddit.map((row) => ({
81
+ ...row,
82
+ metrics: toDerivedMetrics(row, days)
83
+ })),
84
+ recentRuns
85
+ };
86
+ }
87
+ }
88
+ //# sourceMappingURL=analyticsService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyticsService.js","sourceRoot":"","sources":["../../../../src/services/analytics/analyticsService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare function isDaemonRunning(): {
2
+ running: boolean;
3
+ pid: number | null;
4
+ };
5
+ export declare function ensureDaemonRunning(): {
6
+ started: boolean;
7
+ pid: number | null;
8
+ };
9
+ export declare function requestDaemonReload(): {
10
+ reloaded: boolean;
11
+ pid: number | null;
12
+ };
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { ensureAppDirs } from '../utils/paths.js';
4
+ function getDaemonPid() {
5
+ const paths = ensureAppDirs();
6
+ if (!fs.existsSync(paths.pidFilePath)) {
7
+ return null;
8
+ }
9
+ const pid = Number(fs.readFileSync(paths.pidFilePath, 'utf8'));
10
+ return Number.isFinite(pid) ? pid : null;
11
+ }
12
+ export function isDaemonRunning() {
13
+ const pid = getDaemonPid();
14
+ if (!pid) {
15
+ return { running: false, pid: null };
16
+ }
17
+ try {
18
+ process.kill(pid, 0);
19
+ return { running: true, pid };
20
+ }
21
+ catch {
22
+ return { running: false, pid };
23
+ }
24
+ }
25
+ export function ensureDaemonRunning() {
26
+ const paths = ensureAppDirs();
27
+ const status = isDaemonRunning();
28
+ if (status.running) {
29
+ return { started: false, pid: status.pid };
30
+ }
31
+ if (fs.existsSync(paths.pidFilePath)) {
32
+ fs.unlinkSync(paths.pidFilePath);
33
+ }
34
+ const child = spawn(process.execPath, [process.argv[1], 'daemon', 'run'], {
35
+ detached: true,
36
+ stdio: 'ignore'
37
+ });
38
+ child.unref();
39
+ const pid = child.pid ?? null;
40
+ if (pid !== null) {
41
+ fs.writeFileSync(paths.pidFilePath, String(pid));
42
+ }
43
+ return { started: true, pid };
44
+ }
45
+ export function requestDaemonReload() {
46
+ const status = isDaemonRunning();
47
+ if (!status.running || !status.pid) {
48
+ return { reloaded: false, pid: status.pid };
49
+ }
50
+ try {
51
+ process.kill(status.pid, 'SIGUSR2');
52
+ return { reloaded: true, pid: status.pid };
53
+ }
54
+ catch {
55
+ return { reloaded: false, pid: status.pid };
56
+ }
57
+ }
58
+ //# sourceMappingURL=daemonControl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemonControl.js","sourceRoot":"","sources":["../../../src/services/daemonControl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { Job, NewJob } from '../../../types/job.js';
2
+ export declare class JobsRepository {
3
+ private readonly db;
4
+ private readonly removeCascadeStmt;
5
+ constructor();
6
+ private slugExists;
7
+ private ensureUniqueSlug;
8
+ private backfillMissingSlugs;
9
+ create(input: NewJob): Job;
10
+ getById(id: string): Job | null;
11
+ getBySlug(slug: string): Job | null;
12
+ getByRef(ref: string): Job | null;
13
+ list(): Job[];
14
+ listEnabled(): Job[];
15
+ setEnabled(id: string, enabled: boolean): void;
16
+ setEnabledByRef(ref: string, enabled: boolean): Job | null;
17
+ remove(id: string): void;
18
+ removeByRef(ref: string): Job | null;
19
+ }