@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,414 @@
1
+ import { SettingsRepository } from '../db/repositories/settingsRepo.js';
2
+ import { RunsRepository } from '../db/repositories/runsRepo.js';
3
+ import { ScanItemsRepository } from '../db/repositories/scanItemsRepo.js';
4
+ import { getOpenRouterApiKey } from '../security/secretStore.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ import { sendJobNotification } from '../../utils/notify.js';
7
+ import { OpenRouterClient } from '../openrouter/client.js';
8
+ import { getRecentSubredditPosts, getRedditPostComments } from '../reddit/client.js';
9
+ import { createRunLogger } from '../logging/runLogger.js';
10
+ import { cleanupOldLogs } from '../logging/logRotation.js';
11
+ import { toSnippet } from '../../utils/scanLogFormatting.js';
12
+ function getThreads(comments, previousThreadComments = []) {
13
+ let threads = [];
14
+ for (const comment of comments) {
15
+ if (comment.replies.length > 0) {
16
+ threads = [...threads, ...getThreads(comment.replies, [...previousThreadComments, comment])];
17
+ continue;
18
+ }
19
+ threads.push([...previousThreadComments, comment]);
20
+ }
21
+ return threads;
22
+ }
23
+ function getAllAuthors(comments) {
24
+ const authors = new Set();
25
+ for (const comment of comments) {
26
+ if (comment.author) {
27
+ authors.add(comment.author);
28
+ }
29
+ for (const replyAuthor of getAllAuthors(comment.replies)) {
30
+ authors.add(replyAuthor);
31
+ }
32
+ }
33
+ return Array.from(authors);
34
+ }
35
+ function getThreadsByAuthor(threads, author) {
36
+ const trimmedAuthorThreads = [];
37
+ const threadsIncludingAuthor = threads.filter((thread) => thread.some((comment) => comment.author === author));
38
+ for (const thread of threadsIncludingAuthor) {
39
+ const userComments = thread.filter((comment) => comment.author === author);
40
+ const lastUserComment = userComments[userComments.length - 1];
41
+ if (!lastUserComment) {
42
+ continue;
43
+ }
44
+ const index = thread.indexOf(lastUserComment);
45
+ trimmedAuthorThreads.push(thread.slice(0, index + 1));
46
+ }
47
+ const uniqueThreads = [];
48
+ const lastIds = new Set();
49
+ for (const thread of trimmedAuthorThreads) {
50
+ const lastComment = thread[thread.length - 1];
51
+ if (!lastComment || lastIds.has(lastComment.id)) {
52
+ continue;
53
+ }
54
+ lastIds.add(lastComment.id);
55
+ uniqueThreads.push(thread);
56
+ }
57
+ return uniqueThreads;
58
+ }
59
+ function toCurrency(value) {
60
+ return Number(value.toFixed(6));
61
+ }
62
+ function buildCommentUrl(postUrl, comment) {
63
+ if (comment.url) {
64
+ return comment.url;
65
+ }
66
+ const basePostUrl = postUrl.endsWith('/') ? postUrl : `${postUrl}/`;
67
+ return `${basePostUrl}${comment.id}/`;
68
+ }
69
+ export class JobRunner {
70
+ settingsRepo = new SettingsRepository();
71
+ runsRepo = new RunsRepository();
72
+ scanItemsRepo = new ScanItemsRepository();
73
+ emit(options, event) {
74
+ options.onProgress?.(event);
75
+ }
76
+ async run(job, options = {}) {
77
+ const redditCredentials = await this.settingsRepo.getRedditCredentials();
78
+ const apiKey = await getOpenRouterApiKey();
79
+ if (!apiKey) {
80
+ const message = `Skipped job ${job.name} (${job.id}): OpenRouter API key is not configured.`;
81
+ this.runsRepo.addRun(job.id, 'skipped', message);
82
+ this.emit(options, { type: 'run_skipped', reason: 'missing_api_key', message });
83
+ logger.warn(message);
84
+ return;
85
+ }
86
+ const appSettings = this.settingsRepo.getAppSettings();
87
+ const model = appSettings.model;
88
+ const modelSettings = appSettings.modelSettings;
89
+ const runId = this.runsRepo.startRun(job.id);
90
+ const runLogger = createRunLogger(runId);
91
+ this.runsRepo.setLogFilePath(runId, runLogger.getLogFilePath());
92
+ const redditTraceHooks = {
93
+ onRequest: (operation, payload) => runLogger.logRequest('GET', operation, payload),
94
+ onResponse: (operation, payload) => runLogger.logResponse(operation, payload),
95
+ onError: (operation, payload) => runLogger.error(`${operation}\n${JSON.stringify(payload, null, 2)}`)
96
+ };
97
+ const openRouter = new OpenRouterClient(apiKey, {
98
+ onRequest: (operation, payload) => runLogger.logRequest('POST', operation, payload),
99
+ onResponse: (operation, payload) => runLogger.logResponse(operation, payload),
100
+ onError: (operation, payload) => runLogger.error(`${operation}\n${JSON.stringify(payload, null, 2)}`)
101
+ });
102
+ const runStats = {
103
+ itemsDiscovered: 0,
104
+ itemsNew: 0,
105
+ itemsQualified: 0,
106
+ promptTokens: 0,
107
+ completionTokens: 0,
108
+ estimatedCostUsd: null
109
+ };
110
+ try {
111
+ let limitReachedEmitted = false;
112
+ runLogger.info(JSON.stringify({
113
+ event: 'run_start',
114
+ jobId: job.id,
115
+ jobName: job.name,
116
+ jobSlug: job.slug,
117
+ subreddits: job.subreddits,
118
+ maxNewItems: options.maxNewItems ?? null,
119
+ monitorComments: job.monitorComments,
120
+ qualificationPrompt: job.qualificationPrompt,
121
+ model,
122
+ modelSettings
123
+ }, null, 2));
124
+ logger.info(`Running job ${job.name} (${job.id}) against ${job.subreddits.length} subreddits${options.maxNewItems ? ` with maxNewItems=${options.maxNewItems}` : ''}.`);
125
+ this.emit(options, {
126
+ type: 'run_start',
127
+ jobId: job.id,
128
+ jobName: job.name,
129
+ subredditCount: job.subreddits.length,
130
+ maxNewItems: options.maxNewItems
131
+ });
132
+ const posts = [];
133
+ for (const subreddit of job.subreddits) {
134
+ const subredditPosts = await getRecentSubredditPosts(subreddit, redditCredentials, redditTraceHooks);
135
+ posts.push(...subredditPosts);
136
+ runLogger.info(JSON.stringify({
137
+ event: 'subreddit_fetched',
138
+ subreddit,
139
+ postCount: subredditPosts.length
140
+ }, null, 2));
141
+ this.emit(options, {
142
+ type: 'subreddit_fetched',
143
+ subreddit,
144
+ postCount: subredditPosts.length
145
+ });
146
+ }
147
+ runStats.itemsDiscovered = posts.length;
148
+ for (const post of posts) {
149
+ if (this.hasReachedLimit(runStats.itemsNew, options.maxNewItems)) {
150
+ if (!limitReachedEmitted && typeof options.maxNewItems === 'number') {
151
+ this.emit(options, {
152
+ type: 'limit_reached',
153
+ maxNewItems: options.maxNewItems,
154
+ itemsNew: runStats.itemsNew
155
+ });
156
+ limitReachedEmitted = true;
157
+ }
158
+ break;
159
+ }
160
+ if (!this.scanItemsRepo.existsPost(job.id, post.id)) {
161
+ runLogger.info(JSON.stringify({
162
+ event: 'post_qualify_start',
163
+ postId: post.id,
164
+ subreddit: post.subreddit,
165
+ title: post.title,
166
+ body: post.body,
167
+ url: post.url
168
+ }, null, 2));
169
+ const result = await openRouter.qualifyPost({
170
+ model,
171
+ modelSettings,
172
+ qualificationPrompt: job.qualificationPrompt,
173
+ postTitle: post.title,
174
+ postBody: post.body
175
+ });
176
+ runLogger.info(JSON.stringify({ event: 'post_qualify_result', postId: post.id, result }, null, 2));
177
+ this.scanItemsRepo.create({
178
+ jobId: job.id,
179
+ runId,
180
+ type: 'post',
181
+ redditPostId: post.id,
182
+ redditCommentId: null,
183
+ subreddit: post.subreddit,
184
+ author: post.author,
185
+ title: post.title,
186
+ body: post.body,
187
+ url: post.url,
188
+ redditPostedAt: post.postedAt,
189
+ qualified: result.qualified,
190
+ promptTokens: result.promptTokens,
191
+ completionTokens: result.completionTokens,
192
+ estimatedCostUsd: this.estimateCost(result.promptTokens, result.completionTokens),
193
+ qualificationReason: result.reason
194
+ });
195
+ runStats.itemsNew += 1;
196
+ if (result.qualified) {
197
+ runStats.itemsQualified += 1;
198
+ }
199
+ this.accumulateTokens(runStats, result);
200
+ this.emit(options, {
201
+ type: 'post_scanned',
202
+ postId: post.id,
203
+ subreddit: post.subreddit,
204
+ status: 'new',
205
+ title: post.title,
206
+ bodySnippet: toSnippet(post.body),
207
+ postUrl: post.url,
208
+ qualified: result.qualified,
209
+ qualificationReason: result.reason,
210
+ itemsNew: runStats.itemsNew,
211
+ itemsQualified: runStats.itemsQualified
212
+ });
213
+ }
214
+ else {
215
+ this.emit(options, {
216
+ type: 'post_scanned',
217
+ postId: post.id,
218
+ subreddit: post.subreddit,
219
+ status: 'existing',
220
+ itemsNew: runStats.itemsNew,
221
+ itemsQualified: runStats.itemsQualified
222
+ });
223
+ }
224
+ if (this.hasReachedLimit(runStats.itemsNew, options.maxNewItems)) {
225
+ if (!limitReachedEmitted && typeof options.maxNewItems === 'number') {
226
+ this.emit(options, {
227
+ type: 'limit_reached',
228
+ maxNewItems: options.maxNewItems,
229
+ itemsNew: runStats.itemsNew
230
+ });
231
+ limitReachedEmitted = true;
232
+ }
233
+ continue;
234
+ }
235
+ if (!job.monitorComments) {
236
+ continue;
237
+ }
238
+ const comments = await getRedditPostComments(post.id, redditCredentials, redditTraceHooks);
239
+ const threads = getThreads(comments);
240
+ const authors = getAllAuthors(comments);
241
+ runLogger.info(JSON.stringify({
242
+ event: 'comments_loaded',
243
+ postId: post.id,
244
+ subreddit: post.subreddit,
245
+ authors: authors.length,
246
+ threads: threads.length
247
+ }, null, 2));
248
+ this.emit(options, {
249
+ type: 'comments_loaded',
250
+ postId: post.id,
251
+ subreddit: post.subreddit,
252
+ authors: authors.length,
253
+ threads: threads.length
254
+ });
255
+ for (const author of authors) {
256
+ if (this.hasReachedLimit(runStats.itemsNew, options.maxNewItems)) {
257
+ if (!limitReachedEmitted && typeof options.maxNewItems === 'number') {
258
+ this.emit(options, {
259
+ type: 'limit_reached',
260
+ maxNewItems: options.maxNewItems,
261
+ itemsNew: runStats.itemsNew
262
+ });
263
+ limitReachedEmitted = true;
264
+ }
265
+ break;
266
+ }
267
+ const authorThreads = getThreadsByAuthor(threads, author);
268
+ for (const thread of authorThreads) {
269
+ if (this.hasReachedLimit(runStats.itemsNew, options.maxNewItems)) {
270
+ if (!limitReachedEmitted && typeof options.maxNewItems === 'number') {
271
+ this.emit(options, {
272
+ type: 'limit_reached',
273
+ maxNewItems: options.maxNewItems,
274
+ itemsNew: runStats.itemsNew
275
+ });
276
+ limitReachedEmitted = true;
277
+ }
278
+ break;
279
+ }
280
+ const lastComment = thread[thread.length - 1];
281
+ if (!lastComment) {
282
+ continue;
283
+ }
284
+ if (this.scanItemsRepo.existsComment(job.id, post.id, lastComment.id)) {
285
+ this.emit(options, {
286
+ type: 'comment_scanned',
287
+ postId: post.id,
288
+ commentId: lastComment.id,
289
+ author,
290
+ status: 'existing',
291
+ itemsNew: runStats.itemsNew,
292
+ itemsQualified: runStats.itemsQualified
293
+ });
294
+ continue;
295
+ }
296
+ runLogger.info(JSON.stringify({
297
+ event: 'comment_qualify_start',
298
+ postId: post.id,
299
+ commentId: lastComment.id,
300
+ author,
301
+ postUrl: post.url,
302
+ commentUrl: buildCommentUrl(post.url, lastComment),
303
+ commentBody: lastComment.body,
304
+ thread
305
+ }, null, 2));
306
+ const result = await openRouter.qualifyCommentThread({
307
+ model,
308
+ modelSettings,
309
+ qualificationPrompt: job.qualificationPrompt,
310
+ postTitle: post.title,
311
+ targetAuthor: author,
312
+ thread
313
+ });
314
+ runLogger.info(JSON.stringify({
315
+ event: 'comment_qualify_result',
316
+ postId: post.id,
317
+ commentId: lastComment.id,
318
+ author,
319
+ result
320
+ }, null, 2));
321
+ this.scanItemsRepo.create({
322
+ jobId: job.id,
323
+ runId,
324
+ type: 'comment',
325
+ redditPostId: post.id,
326
+ redditCommentId: lastComment.id,
327
+ subreddit: post.subreddit,
328
+ author,
329
+ title: post.title,
330
+ body: lastComment.body,
331
+ url: buildCommentUrl(post.url, lastComment),
332
+ redditPostedAt: post.postedAt,
333
+ qualified: result.qualified,
334
+ promptTokens: result.promptTokens,
335
+ completionTokens: result.completionTokens,
336
+ estimatedCostUsd: this.estimateCost(result.promptTokens, result.completionTokens),
337
+ qualificationReason: result.reason
338
+ });
339
+ runStats.itemsNew += 1;
340
+ if (result.qualified) {
341
+ runStats.itemsQualified += 1;
342
+ }
343
+ this.accumulateTokens(runStats, result);
344
+ this.emit(options, {
345
+ type: 'comment_scanned',
346
+ postId: post.id,
347
+ commentId: lastComment.id,
348
+ author,
349
+ status: 'new',
350
+ commentSnippet: toSnippet(lastComment.body),
351
+ postUrl: post.url,
352
+ commentUrl: buildCommentUrl(post.url, lastComment),
353
+ qualified: result.qualified,
354
+ qualificationReason: result.reason,
355
+ itemsNew: runStats.itemsNew,
356
+ itemsQualified: runStats.itemsQualified
357
+ });
358
+ }
359
+ }
360
+ }
361
+ runStats.estimatedCostUsd = this.estimateCost(runStats.promptTokens, runStats.completionTokens);
362
+ this.runsRepo.completeRun(runId, runStats);
363
+ runLogger.info(JSON.stringify({ event: 'run_complete', runId, stats: runStats }, null, 2));
364
+ this.emit(options, {
365
+ type: 'run_complete',
366
+ itemsDiscovered: runStats.itemsDiscovered,
367
+ itemsNew: runStats.itemsNew,
368
+ itemsQualified: runStats.itemsQualified,
369
+ promptTokens: runStats.promptTokens,
370
+ completionTokens: runStats.completionTokens,
371
+ estimatedCostUsd: runStats.estimatedCostUsd
372
+ });
373
+ logger.info(`Completed job ${job.name} (${job.id}): discovered=${runStats.itemsDiscovered}, new=${runStats.itemsNew}, qualified=${runStats.itemsQualified}`);
374
+ if (appSettings.notificationsEnabled) {
375
+ sendJobNotification({
376
+ jobName: job.name,
377
+ qualifiedCount: runStats.itemsQualified,
378
+ discoveredCount: runStats.itemsDiscovered,
379
+ newCount: runStats.itemsNew
380
+ });
381
+ }
382
+ }
383
+ catch (error) {
384
+ const message = error instanceof Error ? error.message : String(error);
385
+ this.runsRepo.failRun(runId, message);
386
+ runLogger.error(JSON.stringify({
387
+ event: 'run_failed',
388
+ runId,
389
+ message,
390
+ stack: error instanceof Error ? error.stack : undefined
391
+ }, null, 2));
392
+ this.emit(options, { type: 'run_failed', message });
393
+ logger.error(`Job ${job.name} (${job.id}) failed: ${message}`);
394
+ }
395
+ finally {
396
+ cleanupOldLogs();
397
+ }
398
+ }
399
+ accumulateTokens(total, next) {
400
+ total.promptTokens += next.promptTokens;
401
+ total.completionTokens += next.completionTokens;
402
+ }
403
+ hasReachedLimit(processedItems, maxNewItems) {
404
+ return typeof maxNewItems === 'number' && maxNewItems > 0 && processedItems >= maxNewItems;
405
+ }
406
+ estimateCost(promptTokens, completionTokens) {
407
+ // Temporary heuristic until per-model pricing is fetched from OpenRouter model metadata.
408
+ const promptRatePerThousand = 0.0015;
409
+ const completionRatePerThousand = 0.002;
410
+ const estimated = (promptTokens / 1000) * promptRatePerThousand + (completionTokens / 1000) * completionRatePerThousand;
411
+ return toCurrency(estimated);
412
+ }
413
+ }
414
+ //# sourceMappingURL=jobRunner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobRunner.js","sourceRoot":"","sources":["../../../../src/services/scheduler/jobRunner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAmF7D,SAAS,UAAU,CAAC,QAAyB,EAAE,yBAA0C,EAAE;IACzF,IAAI,OAAO,GAAsB,EAAE,CAAC;IACpC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,GAAG,CAAC,GAAG,OAAO,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,sBAAsB,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;YAC7F,SAAS;QACX,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,sBAAsB,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,QAAyB;IAC9C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;QAED,KAAK,MAAM,WAAW,IAAI,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACzD,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,kBAAkB,CAAC,OAA0B,EAAE,MAAc;IACpE,MAAM,oBAAoB,GAAsB,EAAE,CAAC;IACnD,MAAM,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC;IAE/G,KAAK,MAAM,MAAM,IAAI,sBAAsB,EAAE,CAAC;QAC5C,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;QAC3E,MAAM,eAAe,GAAG,YAAY,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC9C,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,aAAa,GAAsB,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,MAAM,IAAI,oBAAoB,EAAE,CAAC;QAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;YAChD,SAAS;QACX,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QAC5B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,eAAe,CAAC,OAAe,EAAE,OAAsB;IAC9D,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,OAAO,OAAO,CAAC,GAAG,CAAC;IACrB,CAAC;IAED,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC;IACpE,OAAO,GAAG,WAAW,GAAG,OAAO,CAAC,EAAE,GAAG,CAAC;AACxC,CAAC;AAED,MAAM,OAAO,SAAS;IACH,YAAY,GAAG,IAAI,kBAAkB,EAAE,CAAC;IACxC,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IAChC,aAAa,GAAG,IAAI,mBAAmB,EAAE,CAAC;IAEnD,IAAI,CAAC,OAAsB,EAAE,KAA0B;QAC7D,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAQ,EAAE,UAAyB,EAAE;QAC7C,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,oBAAoB,EAAE,CAAC;QAEzE,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,eAAe,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,0CAA0C,CAAC;YAC7F,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC;YAChF,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC;QAChC,MAAM,aAAa,GAAkB,WAAW,CAAC,aAAa,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,cAAc,EAAE,CAAC,CAAC;QAChE,MAAM,gBAAgB,GAAG;YACvB,SAAS,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC;YACnG,UAAU,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC;YAC9F,OAAO,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;SACvH,CAAC;QACF,MAAM,UAAU,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAC9C,SAAS,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;YACpG,UAAU,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC;YAC9F,OAAO,EAAE,CAAC,SAAiB,EAAE,OAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;SACvH,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG;YACf,eAAe,EAAE,CAAC;YAClB,QAAQ,EAAE,CAAC;YACX,cAAc,EAAE,CAAC;YACjB,YAAY,EAAE,CAAC;YACf,gBAAgB,EAAE,CAAC;YACnB,gBAAgB,EAAE,IAAqB;SACxC,CAAC;QAEF,IAAI,CAAC;YACH,IAAI,mBAAmB,GAAG,KAAK,CAAC;YAChC,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;gBACE,KAAK,EAAE,WAAW;gBAClB,KAAK,EAAE,GAAG,CAAC,EAAE;gBACb,OAAO,EAAE,GAAG,CAAC,IAAI;gBACjB,OAAO,EAAE,GAAG,CAAC,IAAI;gBACjB,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;gBACxC,eAAe,EAAE,GAAG,CAAC,eAAe;gBACpC,mBAAmB,EAAE,GAAG,CAAC,mBAAmB;gBAC5C,KAAK;gBACL,aAAa;aACd,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;YACF,MAAM,CAAC,IAAI,CACT,eAAe,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,aAAa,GAAG,CAAC,UAAU,CAAC,MAAM,cAClE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,qBAAqB,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EACrE,GAAG,CACJ,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBACjB,IAAI,EAAE,WAAW;gBACjB,KAAK,EAAE,GAAG,CAAC,EAAE;gBACb,OAAO,EAAE,GAAG,CAAC,IAAI;gBACjB,cAAc,EAAE,GAAG,CAAC,UAAU,CAAC,MAAM;gBACrC,WAAW,EAAE,OAAO,CAAC,WAAW;aACjC,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,EAAyD,CAAC;YACxE,KAAK,MAAM,SAAS,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;gBACvC,MAAM,cAAc,GAAG,MAAM,uBAAuB,CAAC,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;gBACrG,KAAK,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,CAAC;gBAC9B,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;oBACE,KAAK,EAAE,mBAAmB;oBAC1B,SAAS;oBACT,SAAS,EAAE,cAAc,CAAC,MAAM;iBACjC,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;oBACjB,IAAI,EAAE,mBAAmB;oBACzB,SAAS;oBACT,SAAS,EAAE,cAAc,CAAC,MAAM;iBACjC,CAAC,CAAC;YACL,CAAC;YAED,QAAQ,CAAC,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC;YAExC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;oBACjE,IAAI,CAAC,mBAAmB,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;wBACpE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;4BACjB,IAAI,EAAE,eAAe;4BACrB,WAAW,EAAE,OAAO,CAAC,WAAW;4BAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;yBAC5B,CAAC,CAAC;wBACH,mBAAmB,GAAG,IAAI,CAAC;oBAC7B,CAAC;oBACD,MAAM;gBACR,CAAC;gBAED,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;oBACpD,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;wBACE,KAAK,EAAE,oBAAoB;wBAC3B,MAAM,EAAE,IAAI,CAAC,EAAE;wBACf,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,GAAG,EAAE,IAAI,CAAC,GAAG;qBACd,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;oBACF,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC;wBAC1C,KAAK;wBACL,aAAa;wBACb,mBAAmB,EAAE,GAAG,CAAC,mBAAmB;wBAC5C,SAAS,EAAE,IAAI,CAAC,KAAK;wBACrB,QAAQ,EAAE,IAAI,CAAC,IAAI;qBACpB,CAAC,CAAC;oBACH,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;oBAEnG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;wBACxB,KAAK,EAAE,GAAG,CAAC,EAAE;wBACb,KAAK;wBACL,IAAI,EAAE,MAAM;wBACZ,YAAY,EAAE,IAAI,CAAC,EAAE;wBACrB,eAAe,EAAE,IAAI;wBACrB,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,GAAG,EAAE,IAAI,CAAC,GAAG;wBACb,cAAc,EAAE,IAAI,CAAC,QAAQ;wBAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;wBAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;wBACjC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;wBACzC,gBAAgB,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,gBAAgB,CAAC;wBACjF,mBAAmB,EAAE,MAAM,CAAC,MAAM;qBACnC,CAAC,CAAC;oBAEH,QAAQ,CAAC,QAAQ,IAAI,CAAC,CAAC;oBACvB,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;wBACrB,QAAQ,CAAC,cAAc,IAAI,CAAC,CAAC;oBAC/B,CAAC;oBACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;oBACxC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;wBACjB,IAAI,EAAE,cAAc;wBACpB,MAAM,EAAE,IAAI,CAAC,EAAE;wBACf,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,MAAM,EAAE,KAAK;wBACb,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,WAAW,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;wBACjC,OAAO,EAAE,IAAI,CAAC,GAAG;wBACjB,SAAS,EAAE,MAAM,CAAC,SAAS;wBAC3B,mBAAmB,EAAE,MAAM,CAAC,MAAM;wBAClC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;wBAC3B,cAAc,EAAE,QAAQ,CAAC,cAAc;qBACxC,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;wBACjB,IAAI,EAAE,cAAc;wBACpB,MAAM,EAAE,IAAI,CAAC,EAAE;wBACf,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,MAAM,EAAE,UAAU;wBAClB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;wBAC3B,cAAc,EAAE,QAAQ,CAAC,cAAc;qBACxC,CAAC,CAAC;gBACL,CAAC;gBAED,IAAI,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;oBACjE,IAAI,CAAC,mBAAmB,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;wBACpE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;4BACjB,IAAI,EAAE,eAAe;4BACrB,WAAW,EAAE,OAAO,CAAC,WAAW;4BAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;yBAC5B,CAAC,CAAC;wBACH,mBAAmB,GAAG,IAAI,CAAC;oBAC7B,CAAC;oBACD,SAAS;gBACX,CAAC;gBAED,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC;oBACzB,SAAS;gBACX,CAAC;gBAED,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,EAAE,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;gBAC3F,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;gBACrC,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;gBACxC,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;oBACE,KAAK,EAAE,iBAAiB;oBACxB,MAAM,EAAE,IAAI,CAAC,EAAE;oBACf,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,OAAO,EAAE,OAAO,CAAC,MAAM;oBACvB,OAAO,EAAE,OAAO,CAAC,MAAM;iBACxB,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;oBACjB,IAAI,EAAE,iBAAiB;oBACvB,MAAM,EAAE,IAAI,CAAC,EAAE;oBACf,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,OAAO,EAAE,OAAO,CAAC,MAAM;oBACvB,OAAO,EAAE,OAAO,CAAC,MAAM;iBACxB,CAAC,CAAC;gBAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;oBAC7B,IAAI,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;wBACjE,IAAI,CAAC,mBAAmB,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;4BACpE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gCACjB,IAAI,EAAE,eAAe;gCACrB,WAAW,EAAE,OAAO,CAAC,WAAW;gCAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;6BAC5B,CAAC,CAAC;4BACH,mBAAmB,GAAG,IAAI,CAAC;wBAC7B,CAAC;wBACD,MAAM;oBACR,CAAC;oBAED,MAAM,aAAa,GAAG,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBAC1D,KAAK,MAAM,MAAM,IAAI,aAAa,EAAE,CAAC;wBACnC,IAAI,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;4BACjE,IAAI,CAAC,mBAAmB,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;gCACpE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;oCACjB,IAAI,EAAE,eAAe;oCACrB,WAAW,EAAE,OAAO,CAAC,WAAW;oCAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;iCAC5B,CAAC,CAAC;gCACH,mBAAmB,GAAG,IAAI,CAAC;4BAC7B,CAAC;4BACD,MAAM;wBACR,CAAC;wBAED,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;wBAC9C,IAAI,CAAC,WAAW,EAAE,CAAC;4BACjB,SAAS;wBACX,CAAC;wBAED,IAAI,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;4BACtE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gCACjB,IAAI,EAAE,iBAAiB;gCACvB,MAAM,EAAE,IAAI,CAAC,EAAE;gCACf,SAAS,EAAE,WAAW,CAAC,EAAE;gCACzB,MAAM;gCACN,MAAM,EAAE,UAAU;gCAClB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gCAC3B,cAAc,EAAE,QAAQ,CAAC,cAAc;6BACxC,CAAC,CAAC;4BACH,SAAS;wBACX,CAAC;wBAED,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;4BACE,KAAK,EAAE,uBAAuB;4BAC9B,MAAM,EAAE,IAAI,CAAC,EAAE;4BACf,SAAS,EAAE,WAAW,CAAC,EAAE;4BACzB,MAAM;4BACN,OAAO,EAAE,IAAI,CAAC,GAAG;4BACjB,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC;4BAClD,WAAW,EAAE,WAAW,CAAC,IAAI;4BAC7B,MAAM;yBACP,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;wBACF,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,oBAAoB,CAAC;4BACnD,KAAK;4BACL,aAAa;4BACb,mBAAmB,EAAE,GAAG,CAAC,mBAAmB;4BAC5C,SAAS,EAAE,IAAI,CAAC,KAAK;4BACrB,YAAY,EAAE,MAAM;4BACpB,MAAM;yBACP,CAAC,CAAC;wBACH,SAAS,CAAC,IAAI,CACZ,IAAI,CAAC,SAAS,CACZ;4BACE,KAAK,EAAE,wBAAwB;4BAC/B,MAAM,EAAE,IAAI,CAAC,EAAE;4BACf,SAAS,EAAE,WAAW,CAAC,EAAE;4BACzB,MAAM;4BACN,MAAM;yBACP,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;wBAEF,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;4BACxB,KAAK,EAAE,GAAG,CAAC,EAAE;4BACb,KAAK;4BACL,IAAI,EAAE,SAAS;4BACf,YAAY,EAAE,IAAI,CAAC,EAAE;4BACrB,eAAe,EAAE,WAAW,CAAC,EAAE;4BAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;4BACzB,MAAM;4BACN,KAAK,EAAE,IAAI,CAAC,KAAK;4BACjB,IAAI,EAAE,WAAW,CAAC,IAAI;4BACtB,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC;4BAC3C,cAAc,EAAE,IAAI,CAAC,QAAQ;4BAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;4BACjC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;4BACzC,gBAAgB,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,gBAAgB,CAAC;4BACjF,mBAAmB,EAAE,MAAM,CAAC,MAAM;yBACnC,CAAC,CAAC;wBAEH,QAAQ,CAAC,QAAQ,IAAI,CAAC,CAAC;wBACvB,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;4BACrB,QAAQ,CAAC,cAAc,IAAI,CAAC,CAAC;wBAC/B,CAAC;wBACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;wBACxC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;4BACjB,IAAI,EAAE,iBAAiB;4BACvB,MAAM,EAAE,IAAI,CAAC,EAAE;4BACf,SAAS,EAAE,WAAW,CAAC,EAAE;4BACzB,MAAM;4BACN,MAAM,EAAE,KAAK;4BACb,cAAc,EAAE,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC;4BAC3C,OAAO,EAAE,IAAI,CAAC,GAAG;4BACjB,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC;4BAClD,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,mBAAmB,EAAE,MAAM,CAAC,MAAM;4BAClC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;4BAC3B,cAAc,EAAE,QAAQ,CAAC,cAAc;yBACxC,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;YAED,QAAQ,CAAC,gBAAgB,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;YAChG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC3C,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3F,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBACjB,IAAI,EAAE,cAAc;gBACpB,eAAe,EAAE,QAAQ,CAAC,eAAe;gBACzC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,cAAc,EAAE,QAAQ,CAAC,cAAc;gBACvC,YAAY,EAAE,QAAQ,CAAC,YAAY;gBACnC,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;gBAC3C,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;aAC5C,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,CACT,iBAAiB,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,iBAAiB,QAAQ,CAAC,eAAe,SAAS,QAAQ,CAAC,QAAQ,eAAe,QAAQ,CAAC,cAAc,EAAE,CAChJ,CAAC;YACF,IAAI,WAAW,CAAC,oBAAoB,EAAE,CAAC;gBACrC,mBAAmB,CAAC;oBAClB,OAAO,EAAE,GAAG,CAAC,IAAI;oBACjB,cAAc,EAAE,QAAQ,CAAC,cAAc;oBACvC,eAAe,EAAE,QAAQ,CAAC,eAAe;oBACzC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;iBAC5B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACtC,SAAS,CAAC,KAAK,CACb,IAAI,CAAC,SAAS,CACZ;gBACE,KAAK,EAAE,YAAY;gBACnB,KAAK;gBACL,OAAO;gBACP,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;aACxD,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;YACpD,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,aAAa,OAAO,EAAE,CAAC,CAAC;QACjE,CAAC;gBAAS,CAAC;YACT,cAAc,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,KAAmB,EAAE,IAAkB;QAC9D,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC;QACxC,KAAK,CAAC,gBAAgB,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAClD,CAAC;IAEO,eAAe,CAAC,cAAsB,EAAE,WAAoB;QAClE,OAAO,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,GAAG,CAAC,IAAI,cAAc,IAAI,WAAW,CAAC;IAC7F,CAAC;IAEO,YAAY,CAAC,YAAoB,EAAE,gBAAwB;QACjE,yFAAyF;QACzF,MAAM,qBAAqB,GAAG,MAAM,CAAC;QACrC,MAAM,yBAAyB,GAAG,KAAK,CAAC;QACxC,MAAM,SAAS,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,qBAAqB,GAAG,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,yBAAyB,CAAC;QACxH,OAAO,UAAU,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ import type { Job } from '../../types/job.js';
2
+ export declare class JobRunnerStub {
3
+ private readonly runsRepo;
4
+ run(job: Job): Promise<void>;
5
+ }
@@ -0,0 +1,11 @@
1
+ import { RunsRepository } from '../db/repositories/runsRepo.js';
2
+ import { logger } from '../../utils/logger.js';
3
+ export class JobRunnerStub {
4
+ runsRepo = new RunsRepository();
5
+ async run(job) {
6
+ const message = `Stub execution: job ${job.name} (${job.id}) scheduled. Reddit scanning not implemented yet.`;
7
+ this.runsRepo.addRun(job.id, 'noop', message);
8
+ logger.info(message);
9
+ }
10
+ }
11
+ //# sourceMappingURL=jobRunnerStub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobRunnerStub.js","sourceRoot":"","sources":["../../../../src/services/scheduler/jobRunnerStub.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,MAAM,OAAO,aAAa;IACP,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IAEjD,KAAK,CAAC,GAAG,CAAC,GAAQ;QAChB,MAAM,OAAO,GAAG,uBAAuB,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,mDAAmD,CAAC;QAC9G,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export declare function setOpenRouterApiKey(apiKey: string): Promise<void>;
2
+ export declare function deleteOpenRouterApiKey(): Promise<void>;
3
+ export declare function getOpenRouterApiKey(): Promise<string | null>;
4
+ export declare function setRedditClientSecret(secret: string): Promise<void>;
5
+ export declare function getRedditClientSecret(): Promise<string | null>;
6
+ export declare function deleteRedditClientSecret(): Promise<void>;
@@ -0,0 +1,193 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ensureAppDirs } from '../../utils/paths.js';
5
+ const SERVICE_NAME = 'snoopy';
6
+ const OPENROUTER_ACCOUNT_NAME = 'openrouter_api_key';
7
+ const REDDIT_CLIENT_SECRET_ACCOUNT_NAME = 'reddit_client_secret';
8
+ const FILE_NAME = 'secrets.enc';
9
+ let keytarClientPromise;
10
+ async function getKeytarClient() {
11
+ if (keytarClientPromise) {
12
+ return keytarClientPromise;
13
+ }
14
+ keytarClientPromise = (async () => {
15
+ try {
16
+ const imported = (await import('keytar'));
17
+ const candidate = imported.default;
18
+ if (candidate &&
19
+ typeof candidate === 'object' &&
20
+ typeof candidate.setPassword === 'function' &&
21
+ typeof candidate.getPassword === 'function' &&
22
+ typeof candidate.deletePassword === 'function') {
23
+ return candidate;
24
+ }
25
+ }
26
+ catch {
27
+ // Keytar can fail to load on systems missing native dependencies.
28
+ }
29
+ return null;
30
+ })();
31
+ return keytarClientPromise;
32
+ }
33
+ function getFallbackPath() {
34
+ const paths = ensureAppDirs();
35
+ return path.join(paths.rootDir, FILE_NAME);
36
+ }
37
+ function getMachineKey() {
38
+ return crypto
39
+ .createHash('sha256')
40
+ .update(`${process.platform}:${process.arch}:${process.env.USER ?? process.env.USERNAME ?? 'user'}`)
41
+ .digest();
42
+ }
43
+ function encrypt(value) {
44
+ const iv = crypto.randomBytes(16);
45
+ const key = getMachineKey();
46
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
47
+ const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
48
+ return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
49
+ }
50
+ function decrypt(value) {
51
+ const [ivHex, contentHex] = value.split(':');
52
+ if (!ivHex || !contentHex) {
53
+ throw new Error('Invalid encrypted payload');
54
+ }
55
+ const iv = Buffer.from(ivHex, 'hex');
56
+ const content = Buffer.from(contentHex, 'hex');
57
+ const decipher = crypto.createDecipheriv('aes-256-cbc', getMachineKey(), iv);
58
+ const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);
59
+ return decrypted.toString('utf8');
60
+ }
61
+ function readFallbackSecrets() {
62
+ const filePath = getFallbackPath();
63
+ if (!fs.existsSync(filePath)) {
64
+ return {};
65
+ }
66
+ try {
67
+ const decrypted = decrypt(fs.readFileSync(filePath, 'utf8'));
68
+ const parsed = JSON.parse(decrypted);
69
+ if (parsed && typeof parsed === 'object') {
70
+ return parsed;
71
+ }
72
+ }
73
+ catch {
74
+ // Legacy fallback stored only OpenRouter API key as a plaintext payload before encryption.
75
+ }
76
+ try {
77
+ const legacyValue = decrypt(fs.readFileSync(filePath, 'utf8'));
78
+ return legacyValue ? { openrouter_api_key: legacyValue } : {};
79
+ }
80
+ catch {
81
+ return {};
82
+ }
83
+ }
84
+ function writeFallbackSecrets(secrets) {
85
+ const sanitized = {
86
+ openrouter_api_key: secrets.openrouter_api_key?.trim() ? secrets.openrouter_api_key : undefined,
87
+ reddit_client_secret: secrets.reddit_client_secret?.trim() ? secrets.reddit_client_secret : undefined
88
+ };
89
+ if (!sanitized.openrouter_api_key && !sanitized.reddit_client_secret) {
90
+ const filePath = getFallbackPath();
91
+ if (fs.existsSync(filePath)) {
92
+ fs.unlinkSync(filePath);
93
+ }
94
+ return;
95
+ }
96
+ const payload = encrypt(JSON.stringify(sanitized));
97
+ fs.writeFileSync(getFallbackPath(), payload, { mode: 0o600 });
98
+ }
99
+ function setFallbackSecret(key, value) {
100
+ const current = readFallbackSecrets();
101
+ current[key] = value;
102
+ writeFallbackSecrets(current);
103
+ }
104
+ function getFallbackSecret(key) {
105
+ const current = readFallbackSecrets();
106
+ return current[key] ?? null;
107
+ }
108
+ function deleteFallbackSecret(key) {
109
+ const current = readFallbackSecrets();
110
+ delete current[key];
111
+ writeFallbackSecrets(current);
112
+ }
113
+ export async function setOpenRouterApiKey(apiKey) {
114
+ const keytarClient = await getKeytarClient();
115
+ if (keytarClient) {
116
+ try {
117
+ await keytarClient.setPassword(SERVICE_NAME, OPENROUTER_ACCOUNT_NAME, apiKey);
118
+ return;
119
+ }
120
+ catch {
121
+ // Fall through to encrypted file fallback.
122
+ }
123
+ }
124
+ setFallbackSecret('openrouter_api_key', apiKey);
125
+ }
126
+ export async function deleteOpenRouterApiKey() {
127
+ const keytarClient = await getKeytarClient();
128
+ if (keytarClient) {
129
+ try {
130
+ await keytarClient.deletePassword(SERVICE_NAME, OPENROUTER_ACCOUNT_NAME);
131
+ }
132
+ catch {
133
+ // Ignore keychain deletion failures and continue with file cleanup.
134
+ }
135
+ }
136
+ deleteFallbackSecret('openrouter_api_key');
137
+ }
138
+ export async function getOpenRouterApiKey() {
139
+ const keytarClient = await getKeytarClient();
140
+ if (keytarClient) {
141
+ try {
142
+ const fromKeytar = await keytarClient.getPassword(SERVICE_NAME, OPENROUTER_ACCOUNT_NAME);
143
+ if (fromKeytar) {
144
+ return fromKeytar;
145
+ }
146
+ }
147
+ catch {
148
+ // Fallback to encrypted file when keytar is unavailable.
149
+ }
150
+ }
151
+ return getFallbackSecret('openrouter_api_key');
152
+ }
153
+ export async function setRedditClientSecret(secret) {
154
+ const keytarClient = await getKeytarClient();
155
+ if (keytarClient) {
156
+ try {
157
+ await keytarClient.setPassword(SERVICE_NAME, REDDIT_CLIENT_SECRET_ACCOUNT_NAME, secret);
158
+ return;
159
+ }
160
+ catch {
161
+ // Fall through to encrypted file fallback.
162
+ }
163
+ }
164
+ setFallbackSecret('reddit_client_secret', secret);
165
+ }
166
+ export async function getRedditClientSecret() {
167
+ const keytarClient = await getKeytarClient();
168
+ if (keytarClient) {
169
+ try {
170
+ const fromKeytar = await keytarClient.getPassword(SERVICE_NAME, REDDIT_CLIENT_SECRET_ACCOUNT_NAME);
171
+ if (fromKeytar) {
172
+ return fromKeytar;
173
+ }
174
+ }
175
+ catch {
176
+ // Fallback to encrypted file when keytar is unavailable.
177
+ }
178
+ }
179
+ return getFallbackSecret('reddit_client_secret');
180
+ }
181
+ export async function deleteRedditClientSecret() {
182
+ const keytarClient = await getKeytarClient();
183
+ if (keytarClient) {
184
+ try {
185
+ await keytarClient.deletePassword(SERVICE_NAME, REDDIT_CLIENT_SECRET_ACCOUNT_NAME);
186
+ }
187
+ catch {
188
+ // Ignore keychain deletion failures and continue with file cleanup.
189
+ }
190
+ }
191
+ deleteFallbackSecret('reddit_client_secret');
192
+ }
193
+ //# sourceMappingURL=secretStore.js.map