@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,48 @@
1
+ const SYSTEM_CONTEXT = `\
2
+ You are configuring a job for an automated Reddit monitoring system.
3
+ How the system works:
4
+ - The user defines monitoring jobs, each with a set of subreddits and a qualification prompt.
5
+ - Every 30 minutes, the system fetches all new posts and comments from those subreddits.
6
+ - Each post/comment is individually evaluated by an LLM using the job's qualification prompt.
7
+ - Items that pass are surfaced to the user as relevant matches.
8
+ The goal is to capture content the user genuinely cares about while filtering out noise.`;
9
+ export function buildClarificationPrompt(criteria) {
10
+ return [
11
+ SYSTEM_CONTEXT,
12
+ '',
13
+ 'The user has described what they want to monitor. Ask 2 to 4 short clarification questions',
14
+ 'that will help you write a precise, effective qualification prompt later.',
15
+ 'Focus on intent, signal vs. noise, and edge cases — not on which subreddits to watch',
16
+ '(subreddits are configured separately).',
17
+ '',
18
+ 'Output only JSON with this exact schema:',
19
+ '{"questions":[{"id":"q1","question":"..."}]}',
20
+ '',
21
+ `User criteria: ${criteria}`
22
+ ].join('\n');
23
+ }
24
+ export function buildSpecPrompt(criteria, answers) {
25
+ return [
26
+ SYSTEM_CONTEXT,
27
+ '',
28
+ 'Using the user criteria and their answers to the clarification questions, produce a monitoring job spec.',
29
+ '',
30
+ 'The most important field is qualificationPrompt — this is the exact prompt that will be prepended',
31
+ 'to each Reddit post or comment and sent to an LLM to decide whether that content is a match.',
32
+ 'It should be written in second-person imperative ("Decide whether this post…"), be specific about',
33
+ 'what counts as a match vs. not a match, and be resilient to tangential or off-topic content.',
34
+ '',
35
+ 'Return strict JSON only with this schema:',
36
+ '{"name":"...","slug":"...","description":"...","qualificationPrompt":"...","suggestedSubreddits":["sub1","sub2"]}',
37
+ '',
38
+ 'Rules:',
39
+ '- slug: a memorable, scannable identifier for this job shown in job listings (e.g. "ai-funding-rounds", "rust-job-postings"). kebab-case, lowercase, 2-40 chars, letters/numbers/hyphens only.',
40
+ '- description: one tight sentence a user can read at a glance to know exactly what this job is watching for. Prefer active phrasing, e.g. "Monitors posts asking for SaaS tool recommendations" rather than generic summaries.',
41
+ '- qualificationPrompt: 2-5 sentences; precise, actionable, unambiguous.',
42
+ '- suggestedSubreddits: 3 to 8 relevant subreddit names without the r/ prefix.',
43
+ '',
44
+ `Original criteria: ${criteria}`,
45
+ `Clarifications: ${JSON.stringify(answers)}`
46
+ ].join('\n');
47
+ }
48
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../../src/services/openrouter/prompts.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG;;;;;;;yFAOkE,CAAC;AAE1F,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,OAAO;QACL,cAAc;QACd,EAAE;QACF,4FAA4F;QAC5F,2EAA2E;QAC3E,sFAAsF;QACtF,yCAAyC;QACzC,EAAE;QACF,0CAA0C;QAC1C,8CAA8C;QAC9C,EAAE;QACF,kBAAkB,QAAQ,EAAE;KAC7B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,OAAoD;IACpG,OAAO;QACL,cAAc;QACd,EAAE;QACF,0GAA0G;QAC1G,EAAE;QACF,mGAAmG;QACnG,8FAA8F;QAC9F,mGAAmG;QACnG,8FAA8F;QAC9F,EAAE;QACF,2CAA2C;QAC3C,mHAAmH;QACnH,EAAE;QACF,QAAQ;QACR,gMAAgM;QAChM,gOAAgO;QAChO,yEAAyE;QACzE,+EAA+E;QAC/E,EAAE;QACF,sBAAsB,QAAQ,EAAE;QAChC,mBAAmB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE;KAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { RedditCredentials } from '../../types/settings.js';
2
+ export interface RedditTraceHooks {
3
+ onRequest?: (operation: string, payload: unknown) => void;
4
+ onResponse?: (operation: string, payload: unknown) => void;
5
+ onError?: (operation: string, payload: unknown) => void;
6
+ }
7
+ export interface RedditPost {
8
+ id: string;
9
+ subreddit: string;
10
+ title: string;
11
+ body: string;
12
+ author: string;
13
+ url: string;
14
+ postedAt: string;
15
+ }
16
+ export interface RedditComment {
17
+ id: string;
18
+ author: string;
19
+ body: string;
20
+ permalink?: string;
21
+ url?: string;
22
+ replies: RedditComment[];
23
+ }
24
+ export declare function getRecentSubredditPosts(subreddit: string, credentials?: RedditCredentials | null, traceHooks?: RedditTraceHooks): Promise<RedditPost[]>;
25
+ export declare function getRedditPostComments(postId: string, credentials?: RedditCredentials | null, traceHooks?: RedditTraceHooks): Promise<RedditComment[]>;
@@ -0,0 +1,186 @@
1
+ const DEFAULT_USER_AGENT = 'snoopy:reddit-client:v1';
2
+ let tokenState = null;
3
+ function assertCredentials(credentials) {
4
+ if (!credentials.clientId || !credentials.clientSecret || !credentials.appName) {
5
+ throw new Error('Reddit credentials are incomplete. clientId, clientSecret, and appName are required.');
6
+ }
7
+ }
8
+ async function getAccessToken(credentials) {
9
+ const params = new URLSearchParams();
10
+ params.append('grant_type', 'client_credentials');
11
+ const authHeader = Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString('base64');
12
+ const response = await fetch('https://www.reddit.com/api/v1/access_token', {
13
+ method: 'POST',
14
+ body: params,
15
+ headers: {
16
+ Authorization: `Basic ${authHeader}`,
17
+ 'Content-Type': 'application/x-www-form-urlencoded',
18
+ 'User-Agent': credentials.appName
19
+ }
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`Failed to authenticate with Reddit (${response.status} ${response.statusText}).`);
23
+ }
24
+ const data = (await response.json());
25
+ if (!data.access_token) {
26
+ throw new Error(`Failed to get Reddit access token${data.error ? `: ${data.error}` : ''}`);
27
+ }
28
+ const expiresInSec = typeof data.expires_in === 'number' ? data.expires_in : 3600;
29
+ tokenState = {
30
+ accessToken: data.access_token,
31
+ // Keep a small safety margin so token is refreshed before hard expiry.
32
+ expiresAt: Date.now() + Math.max(expiresInSec - 30, 1) * 1000
33
+ };
34
+ return data.access_token;
35
+ }
36
+ async function ensureToken(credentials) {
37
+ if (tokenState && Date.now() < tokenState.expiresAt) {
38
+ return tokenState.accessToken;
39
+ }
40
+ return getAccessToken(credentials);
41
+ }
42
+ async function callOAuthRedditApi(endpoint, credentials, traceHooks) {
43
+ assertCredentials(credentials);
44
+ const makeRequest = async (token) => {
45
+ const response = await fetch(`https://oauth.reddit.com${endpoint}`, {
46
+ headers: {
47
+ Authorization: `Bearer ${token}`,
48
+ 'User-Agent': credentials.appName
49
+ }
50
+ });
51
+ if (response.status === 401 && tokenState?.accessToken === token) {
52
+ tokenState = null;
53
+ return null;
54
+ }
55
+ return response;
56
+ };
57
+ let token = await ensureToken(credentials);
58
+ let response = await makeRequest(token);
59
+ if (!response) {
60
+ token = await ensureToken(credentials);
61
+ response = await makeRequest(token);
62
+ }
63
+ if (!response) {
64
+ throw new Error('Failed to authenticate with Reddit API.');
65
+ }
66
+ if (!response.ok) {
67
+ traceHooks?.onError?.('reddit.oauth', {
68
+ endpoint,
69
+ status: response.status,
70
+ statusText: response.statusText
71
+ });
72
+ throw new Error(`Reddit API request failed (${response.status} ${response.statusText}).`);
73
+ }
74
+ traceHooks?.onRequest?.('reddit.oauth', {
75
+ method: 'GET',
76
+ url: `https://oauth.reddit.com${endpoint}`,
77
+ headers: {
78
+ Authorization: 'Bearer [redacted]',
79
+ 'User-Agent': credentials.appName
80
+ }
81
+ });
82
+ const json = await response.json();
83
+ traceHooks?.onResponse?.('reddit.oauth', {
84
+ endpoint,
85
+ status: response.status,
86
+ body: json
87
+ });
88
+ return json;
89
+ }
90
+ function isFallbackEligibleStatus(status) {
91
+ return status === 401 || status === 403 || status === 429;
92
+ }
93
+ function getUserAgent(credentials) {
94
+ return credentials?.appName?.trim() || DEFAULT_USER_AGENT;
95
+ }
96
+ async function callRedditApi(endpoint, credentials, traceHooks) {
97
+ const userAgent = getUserAgent(credentials);
98
+ const unauthUrl = `https://www.reddit.com${endpoint}`;
99
+ try {
100
+ traceHooks?.onRequest?.('reddit.public', {
101
+ method: 'GET',
102
+ url: unauthUrl,
103
+ headers: {
104
+ 'User-Agent': userAgent
105
+ }
106
+ });
107
+ const unauthResponse = await fetch(unauthUrl, {
108
+ headers: {
109
+ 'User-Agent': userAgent
110
+ }
111
+ });
112
+ if (unauthResponse.ok) {
113
+ const json = await unauthResponse.json();
114
+ traceHooks?.onResponse?.('reddit.public', {
115
+ endpoint,
116
+ status: unauthResponse.status,
117
+ body: json
118
+ });
119
+ return json;
120
+ }
121
+ traceHooks?.onError?.('reddit.public', {
122
+ endpoint,
123
+ status: unauthResponse.status,
124
+ statusText: unauthResponse.statusText
125
+ });
126
+ if (!isFallbackEligibleStatus(unauthResponse.status)) {
127
+ throw new Error(`Reddit API request failed (${unauthResponse.status} ${unauthResponse.statusText}).`);
128
+ }
129
+ }
130
+ catch (error) {
131
+ if (!credentials) {
132
+ if (error instanceof Error) {
133
+ throw error;
134
+ }
135
+ throw new Error('Failed to fetch Reddit content without OAuth credentials.');
136
+ }
137
+ }
138
+ if (!credentials) {
139
+ throw new Error('Reddit denied unauthenticated access. Configure OAuth fallback credentials in settings.');
140
+ }
141
+ return callOAuthRedditApi(endpoint, credentials, traceHooks);
142
+ }
143
+ function parseComment(thing) {
144
+ const data = thing.data ?? {};
145
+ const replies = data.replies?.data?.children ?? [];
146
+ const permalink = typeof data.permalink === 'string' ? data.permalink : undefined;
147
+ return {
148
+ id: String(data.id ?? ''),
149
+ author: String(data.author ?? '[deleted]'),
150
+ body: String(data.body ?? data.selftext ?? ''),
151
+ permalink,
152
+ url: permalink ? `https://www.reddit.com${permalink}` : undefined,
153
+ replies: replies
154
+ .filter((child) => child.kind === 't1')
155
+ .map((child) => parseComment(child))
156
+ };
157
+ }
158
+ export async function getRecentSubredditPosts(subreddit, credentials, traceHooks) {
159
+ const response = (await callRedditApi(`/r/${encodeURIComponent(subreddit)}/new.json?raw_json=1&limit=100`, credentials, traceHooks));
160
+ const children = response.data?.children ?? [];
161
+ return children
162
+ .filter((item) => item.kind === 't3')
163
+ .map((item) => {
164
+ const data = item.data ?? {};
165
+ const postId = String(data.id ?? '');
166
+ const postSubreddit = String(data.subreddit ?? subreddit);
167
+ const permalink = String(data.permalink ?? `/r/${postSubreddit}/comments/${postId}/`);
168
+ return {
169
+ id: postId,
170
+ subreddit: postSubreddit,
171
+ title: String(data.title ?? ''),
172
+ body: String(data.selftext ?? ''),
173
+ author: String(data.author ?? '[deleted]'),
174
+ url: `https://www.reddit.com${permalink}`,
175
+ postedAt: new Date((Number(data.created_utc ?? Date.now() / 1000) || Date.now() / 1000) * 1000).toISOString()
176
+ };
177
+ })
178
+ .filter((post) => post.author !== 'AutoModerator');
179
+ }
180
+ export async function getRedditPostComments(postId, credentials, traceHooks) {
181
+ const response = (await callRedditApi(`/comments/${encodeURIComponent(postId)}.json?raw_json=1&limit=500`, credentials, traceHooks));
182
+ const listing = response[1];
183
+ const comments = listing?.data?.children ?? [];
184
+ return comments.filter((item) => item.kind === 't1').map((item) => parseComment(item));
185
+ }
186
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../../src/services/reddit/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export declare class CronScheduler {
2
+ private readonly jobsRepo;
3
+ private readonly settingsRepo;
4
+ private readonly runner;
5
+ private readonly tasks;
6
+ private readonly running;
7
+ start(): void;
8
+ stop(): void;
9
+ reload(): void;
10
+ registerJob(jobId: string): void;
11
+ }
@@ -0,0 +1,76 @@
1
+ import cron from 'node-cron';
2
+ import { JobsRepository } from '../db/repositories/jobsRepo.js';
3
+ import { SettingsRepository } from '../db/repositories/settingsRepo.js';
4
+ import { JobRunner } from './jobRunner.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ import { intervalToCron } from '../../types/settings.js';
7
+ export class CronScheduler {
8
+ jobsRepo = new JobsRepository();
9
+ settingsRepo = new SettingsRepository();
10
+ runner = new JobRunner();
11
+ tasks = new Map();
12
+ running = new Set();
13
+ start() {
14
+ const jobs = this.jobsRepo.listEnabled();
15
+ jobs.forEach((job) => this.registerJob(job.id));
16
+ logger.info(`Scheduler started with ${jobs.length} enabled jobs.`);
17
+ }
18
+ stop() {
19
+ this.tasks.forEach((task) => task.stop());
20
+ this.tasks.clear();
21
+ logger.info('Scheduler stopped.');
22
+ }
23
+ reload() {
24
+ this.stop();
25
+ this.start();
26
+ }
27
+ registerJob(jobId) {
28
+ const job = this.jobsRepo.getById(jobId);
29
+ if (!job || !job.enabled) {
30
+ return;
31
+ }
32
+ const previous = this.tasks.get(jobId);
33
+ if (previous) {
34
+ previous.stop();
35
+ }
36
+ const { cronIntervalMinutes, jobTimeoutMs } = this.settingsRepo.getAppSettings();
37
+ const cronExpression = intervalToCron(cronIntervalMinutes);
38
+ const task = cron.schedule(cronExpression, async () => {
39
+ if (this.running.has(jobId)) {
40
+ logger.warn(`Job ${jobId} is already running, skipping tick.`);
41
+ return;
42
+ }
43
+ this.running.add(jobId);
44
+ try {
45
+ const fresh = this.jobsRepo.getById(jobId);
46
+ if (!fresh || !fresh.enabled) {
47
+ return;
48
+ }
49
+ const runPromise = this.runner.run(fresh);
50
+ if (jobTimeoutMs > 0) {
51
+ let timeoutHandle;
52
+ const timeoutPromise = new Promise((_, reject) => {
53
+ timeoutHandle = setTimeout(() => reject(new Error(`Job ${jobId} timed out after ${jobTimeoutMs / 1000}s`)), jobTimeoutMs);
54
+ });
55
+ try {
56
+ await Promise.race([runPromise, timeoutPromise]);
57
+ }
58
+ finally {
59
+ clearTimeout(timeoutHandle);
60
+ }
61
+ }
62
+ else {
63
+ await runPromise;
64
+ }
65
+ }
66
+ catch (error) {
67
+ logger.error(`Scheduler error for job ${jobId}: ${String(error)}`);
68
+ }
69
+ finally {
70
+ this.running.delete(jobId);
71
+ }
72
+ });
73
+ this.tasks.set(jobId, task);
74
+ }
75
+ }
76
+ //# sourceMappingURL=cronScheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cronScheduler.js","sourceRoot":"","sources":["../../../../src/services/scheduler/cronScheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import type { Job } from '../../types/job.js';
2
+ export interface JobRunOptions {
3
+ maxNewItems?: number;
4
+ onProgress?: (event: JobRunProgressEvent) => void;
5
+ }
6
+ export type JobRunProgressEvent = {
7
+ type: 'run_start';
8
+ jobId: string;
9
+ jobName: string;
10
+ subredditCount: number;
11
+ maxNewItems?: number;
12
+ } | {
13
+ type: 'run_skipped';
14
+ reason: 'missing_api_key';
15
+ message: string;
16
+ } | {
17
+ type: 'subreddit_fetched';
18
+ subreddit: string;
19
+ postCount: number;
20
+ } | {
21
+ type: 'post_scanned';
22
+ postId: string;
23
+ subreddit: string;
24
+ status: 'existing' | 'new';
25
+ title?: string;
26
+ bodySnippet?: string;
27
+ postUrl?: string;
28
+ qualified?: boolean;
29
+ qualificationReason?: string;
30
+ itemsNew: number;
31
+ itemsQualified: number;
32
+ } | {
33
+ type: 'comments_loaded';
34
+ postId: string;
35
+ subreddit: string;
36
+ authors: number;
37
+ threads: number;
38
+ } | {
39
+ type: 'comment_scanned';
40
+ postId: string;
41
+ commentId: string;
42
+ author: string;
43
+ status: 'existing' | 'new';
44
+ commentSnippet?: string;
45
+ postUrl?: string;
46
+ commentUrl?: string;
47
+ qualified?: boolean;
48
+ qualificationReason?: string;
49
+ itemsNew: number;
50
+ itemsQualified: number;
51
+ } | {
52
+ type: 'limit_reached';
53
+ maxNewItems: number;
54
+ itemsNew: number;
55
+ } | {
56
+ type: 'run_complete';
57
+ itemsDiscovered: number;
58
+ itemsNew: number;
59
+ itemsQualified: number;
60
+ promptTokens: number;
61
+ completionTokens: number;
62
+ estimatedCostUsd: number | null;
63
+ } | {
64
+ type: 'run_failed';
65
+ message: string;
66
+ };
67
+ export declare class JobRunner {
68
+ private readonly settingsRepo;
69
+ private readonly runsRepo;
70
+ private readonly scanItemsRepo;
71
+ private emit;
72
+ run(job: Job, options?: JobRunOptions): Promise<void>;
73
+ private accumulateTokens;
74
+ private hasReachedLimit;
75
+ private estimateCost;
76
+ }