thinking-phrases 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -142
- package/configs/hn-top.config.json +60 -27
- package/launchd/rss-update.error.log +3 -27
- package/launchd/rss-update.log +308 -0
- package/launchd/task-health.json +54 -0
- package/out/dwyl-quotes.json +1621 -0
- package/out/javascript-tips.json +107 -0
- package/out/league-loading-screen-tips.json +107 -0
- package/out/ruby-tips.json +115 -0
- package/out/settings-linux.json +87 -0
- package/out/settings-mac.json +87 -0
- package/out/settings-windows.json +87 -0
- package/out/typescript-tips.json +131 -0
- package/out/vscode-tips.json +87 -0
- package/out/wow-loading-screen-tips.json +116 -0
- package/package.json +19 -12
- package/scripts/build.ts +3 -3
- package/scripts/debug-hn-hydration.ts +33 -0
- package/scripts/run-rss-update.zsh +25 -3
- package/scripts/show-thinking-phrases-health.ts +74 -0
- package/scripts/trigger-thinking-phrases-scheduler.zsh +50 -0
- package/src/core/config.ts +65 -3
- package/src/core/githubModels.ts +200 -112
- package/src/core/interactive.ts +49 -67
- package/src/core/phraseCache.ts +242 -0
- package/src/core/phraseFormats.ts +243 -0
- package/src/core/presets.ts +1 -1
- package/src/core/runner.ts +246 -113
- package/src/core/scheduler.ts +1 -1
- package/src/core/taskHealth.ts +213 -0
- package/src/core/types.ts +32 -8
- package/src/core/utils.ts +27 -2
- package/src/sources/customJson.ts +28 -18
- package/src/sources/earthquakes.ts +4 -4
- package/src/sources/githubActivity.ts +120 -48
- package/src/sources/hackerNews.ts +19 -7
- package/src/sources/rss.ts +25 -11
- package/src/sources/stocks.ts +31 -10
- package/src/sources/weatherAlerts.ts +173 -7
- package/tsconfig.json +1 -1
- package/scripts/update-rss-settings.ts +0 -7
package/src/core/runner.ts
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import process from 'node:process';
|
|
3
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import process from 'node:process';
|
|
4
4
|
import { outro, spinner } from '@clack/prompts';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
|
|
6
7
|
import { DEFAULT_CONFIG, mergeConfig, parseArgs, readConfigFile, resolveConfigPath, validateConfig, writeConfigFile } from './config.js';
|
|
7
8
|
import { buildModelArticlePhrases } from './githubModels.js';
|
|
8
9
|
import {
|
|
9
10
|
promptForConfigName,
|
|
10
|
-
promptForDynamicSchedulerAfterDryRun,
|
|
11
11
|
promptForInteractiveOverrides,
|
|
12
12
|
promptForPostDryRunAction,
|
|
13
13
|
promptForStaticSchedulerAfterDryRun,
|
|
14
14
|
} from './interactive.js';
|
|
15
|
+
import { cacheModelResults, clearModelCache, getMergedPhrases, isSourceStale, markSourceFetched, partitionArticlesByModelCache, storePhrases } from './phraseCache.js';
|
|
15
16
|
import { DEFAULT_SCHEDULER_INTERVAL_SECONDS, formatConfigPathForDisplay, getInstalledSchedulerInfo } from './scheduler.js';
|
|
16
17
|
import { dynamicSources } from './sourceCatalog.js';
|
|
17
18
|
import { getStaticPackByPath } from './staticPacks.js';
|
|
19
|
+
import { TaskHealthTracker } from './taskHealth.js';
|
|
20
|
+
import { formatArticlePhrase } from './phraseFormats.js';
|
|
18
21
|
import type { ArticleItem, Config } from './types.js';
|
|
19
22
|
import { dedupePhrases, loadDotEnv, logInfo, resolveSettingsPath, truncate } from './utils.js';
|
|
23
|
+
|
|
24
|
+
import { hydrateArticleContent } from '../sources/rss.js';
|
|
20
25
|
import { buildStockPhrase } from '../sources/stocks.js';
|
|
21
26
|
import { removeVsCodeThinkingPhrases, writeVsCodeSettings } from '../sinks/vscodeSettings.js';
|
|
22
27
|
|
|
@@ -29,18 +34,17 @@ function buildBasicArticlePhrase(article: ArticleItem, config: Config): string |
|
|
|
29
34
|
return null;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return truncate(parts.join(' — '), config.phraseFormatting.maxLength);
|
|
37
|
+
return truncate(
|
|
38
|
+
formatArticlePhrase(
|
|
39
|
+
{ source: article.source, title: article.title, time: article.time },
|
|
40
|
+
{
|
|
41
|
+
includeSource: config.phraseFormatting.includeSource,
|
|
42
|
+
includeTime: config.phraseFormatting.includeTime,
|
|
43
|
+
template: config.phraseFormatting.templates?.article,
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
config.phraseFormatting.maxLength,
|
|
47
|
+
);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function buildBasicArticlePhrases(articles: ArticleItem[], config: Config): string[] {
|
|
@@ -66,6 +70,11 @@ function uninstallMacScheduler(): void {
|
|
|
66
70
|
execFileSync('zsh', [uninstallPath], { stdio: 'inherit' });
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
function triggerMacSchedulerNow(): void {
|
|
74
|
+
const triggerPath = resolve(process.cwd(), 'scripts/trigger-thinking-phrases-scheduler.zsh');
|
|
75
|
+
execFileSync('zsh', [triggerPath], { stdio: 'inherit' });
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
function syncGitHubLookbackToScheduler(config: Config, intervalSeconds: number): Config {
|
|
70
79
|
if (!config.githubActivity.enabled) {
|
|
71
80
|
return config;
|
|
@@ -91,6 +100,8 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
91
100
|
const args = parseArgs(process.argv.slice(2));
|
|
92
101
|
const isInteractive = Boolean(args.interactive);
|
|
93
102
|
let uninstall = Boolean(args.uninstall);
|
|
103
|
+
const clearCache = Boolean(args.clearCache);
|
|
104
|
+
let triggerSchedulerNow = Boolean(args.triggerSchedulerNow);
|
|
94
105
|
let createNewConfig = Boolean(args.createNewConfig);
|
|
95
106
|
let dryRun = Boolean(args.dryRun);
|
|
96
107
|
let installScheduler = Boolean(args.installScheduler);
|
|
@@ -101,6 +112,12 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
101
112
|
let configPath: string | undefined = resolveConfigPath(args.configPath);
|
|
102
113
|
let fileConfig = configPath ? readConfigFile(configPath) : {};
|
|
103
114
|
let config = mergeConfig(DEFAULT_CONFIG, fileConfig, args);
|
|
115
|
+
|
|
116
|
+
if (clearCache) {
|
|
117
|
+
clearModelCache();
|
|
118
|
+
logInfo(config, 'Model cache cleared');
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
let interactivePass = 0;
|
|
105
122
|
const interactiveSpinner = isInteractive ? spinner({ indicator: 'timer' }) : null;
|
|
106
123
|
let interactiveSpinnerActive = false;
|
|
@@ -151,6 +168,7 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
151
168
|
|
|
152
169
|
interactivePass += 1;
|
|
153
170
|
uninstall = Boolean(interactiveOverrides.uninstall);
|
|
171
|
+
triggerSchedulerNow = Boolean(interactiveOverrides.triggerSchedulerNow);
|
|
154
172
|
createNewConfig = Boolean(interactiveOverrides.createNewConfig);
|
|
155
173
|
dryRun = Boolean(interactiveOverrides.dryRun);
|
|
156
174
|
installScheduler = Boolean(interactiveOverrides.installScheduler);
|
|
@@ -165,12 +183,27 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
165
183
|
|
|
166
184
|
const settingsPath = resolveSettingsPath(config.target, config.settingsPath);
|
|
167
185
|
|
|
186
|
+
if (triggerSchedulerNow) {
|
|
187
|
+
if (process.platform !== 'darwin') {
|
|
188
|
+
throw new Error('Scheduler triggering is currently only available on macOS.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const installedScheduler = getInstalledSchedulerInfo();
|
|
192
|
+
if (!installedScheduler.installed) {
|
|
193
|
+
throw new Error('No macOS scheduler is currently installed.');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(pc.cyan('Triggering macOS launchd scheduler now...'));
|
|
197
|
+
triggerMacSchedulerNow();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
if (uninstall) {
|
|
169
202
|
const removedThinkingPhrases = removeVsCodeThinkingPhrases(settingsPath);
|
|
170
203
|
if (removedThinkingPhrases) {
|
|
171
|
-
console.log(pc.green(`Removed chat.agent.thinking.phrases from ${settingsPath}`));
|
|
204
|
+
console.log(pc.green(`Removed chat.agent.thinking.phrases from "${settingsPath}"`));
|
|
172
205
|
} else {
|
|
173
|
-
console.log(pc.dim(`No chat.agent.thinking.phrases entry found in ${settingsPath}`));
|
|
206
|
+
console.log(pc.dim(`No chat.agent.thinking.phrases entry found in "${settingsPath}"`));
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
if (process.platform === 'darwin') {
|
|
@@ -194,7 +227,7 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
194
227
|
}
|
|
195
228
|
|
|
196
229
|
if (dryRun) {
|
|
197
|
-
console.log(pc.bold(pc.cyan(`Dry run only — would write ${pack.phrases.length} phrases from ${pack.name} to ${settingsPath}`)));
|
|
230
|
+
console.log(pc.bold(pc.cyan(`Dry run only — would write ${pack.phrases.length} phrases from ${pack.name} to "${settingsPath}"`)));
|
|
198
231
|
console.log(pc.dim('Preview:'));
|
|
199
232
|
for (const phrase of pack.phrases.slice(0, 5)) {
|
|
200
233
|
console.log(`${pc.green('•')} ${phrase}`);
|
|
@@ -228,7 +261,7 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
228
261
|
}
|
|
229
262
|
|
|
230
263
|
writeVsCodeSettings(settingsPath, pack.phrases, pack.mode);
|
|
231
|
-
console.log(pc.green(`Updated ${settingsPath}`));
|
|
264
|
+
console.log(pc.green(`Updated "${settingsPath}"`));
|
|
232
265
|
console.log(pc.bold(`Installed static pack ${pack.name} with ${pack.phrases.length} phrases.`));
|
|
233
266
|
|
|
234
267
|
if (uninstallScheduler && process.platform === 'darwin') {
|
|
@@ -249,127 +282,227 @@ export async function runDynamicPhrases(): Promise<void> {
|
|
|
249
282
|
|
|
250
283
|
validateConfig(config);
|
|
251
284
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
285
|
+
const healthTracker = new TaskHealthTracker({
|
|
286
|
+
dryRun,
|
|
287
|
+
configPath,
|
|
288
|
+
settingsPath,
|
|
289
|
+
});
|
|
256
290
|
|
|
257
291
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}).catch((error: unknown) => {
|
|
270
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
271
|
-
console.warn(`GitHub Models formatting skipped — ${message}`);
|
|
272
|
-
startInteractiveProgress('GitHub Models unavailable, falling back to feed phrases');
|
|
273
|
-
return fallbackArticlePhrases;
|
|
292
|
+
const enabledSources = dynamicSources.filter(source => source.isEnabled(config));
|
|
293
|
+
// In dry-run or interactive mode, fetch everything regardless of intervals
|
|
294
|
+
const respectIntervals = !dryRun && !isInteractive;
|
|
295
|
+
const sourcesToFetch = respectIntervals
|
|
296
|
+
? enabledSources.filter(source => {
|
|
297
|
+
const stale = isSourceStale(source.type, config);
|
|
298
|
+
if (!stale) {
|
|
299
|
+
logInfo(config, `Skipping ${source.type} — interval not elapsed`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return stale;
|
|
274
303
|
})
|
|
275
|
-
:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
304
|
+
: enabledSources;
|
|
305
|
+
|
|
306
|
+
healthTracker.setSources(enabledSources.map(source => source.type));
|
|
307
|
+
healthTracker.setPhase('fetching-sources', `Running ${sourcesToFetch.length} of ${enabledSources.length} source${enabledSources.length === 1 ? '' : 's'}`);
|
|
308
|
+
logInfo(config, `Running ${sourcesToFetch.length} of ${enabledSources.length} enabled source(s) (${enabledSources.length - sourcesToFetch.length} skipped — not yet stale)`);
|
|
309
|
+
|
|
310
|
+
startInteractiveProgress(`Fetching ${sourcesToFetch.length} dynamic source${sourcesToFetch.length === 1 ? '' : 's'}`);
|
|
311
|
+
const sourceResults = await Promise.all(
|
|
312
|
+
sourcesToFetch.map(async source => {
|
|
313
|
+
healthTracker.startSource(source.type);
|
|
314
|
+
try {
|
|
315
|
+
const items = await source.fetch(config);
|
|
316
|
+
markSourceFetched(source.type);
|
|
317
|
+
healthTracker.completeSource(source.type, items.length);
|
|
318
|
+
return { type: source.type, items };
|
|
319
|
+
} catch (error: unknown) {
|
|
320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
321
|
+
healthTracker.failSource(source.type, message);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Build phrases per-source and persist to the phrase store
|
|
328
|
+
let totalArticles = 0;
|
|
329
|
+
let totalStocks = 0;
|
|
330
|
+
for (const { type, items } of sourceResults) {
|
|
331
|
+
const articles = items.filter((item): item is ArticleItem => item.type === 'article');
|
|
332
|
+
const stocks = items.filter(item => item.type === 'stock');
|
|
333
|
+
totalArticles += articles.length;
|
|
334
|
+
totalStocks += stocks.length;
|
|
335
|
+
|
|
336
|
+
startInteractiveProgress(`Preparing ${items.length} item${items.length === 1 ? '' : 's'} from ${type}`);
|
|
337
|
+
healthTracker.setPhase('formatting-phrases', `Preparing ${items.length} item${items.length === 1 ? '' : 's'} from ${type}`);
|
|
338
|
+
|
|
339
|
+
const stockPhrases = dedupePhrases(stocks.map(stock => buildStockPhrase(stock, config)));
|
|
340
|
+
|
|
341
|
+
// Articles with skipModelRewrite keep their displayPhrase as-is (e.g. weather conditions)
|
|
342
|
+
const modelEligible = articles.filter(a => !a.skipModelRewrite);
|
|
343
|
+
const preFormatted = articles.filter(a => a.skipModelRewrite);
|
|
344
|
+
const preFormattedPhrases = buildBasicArticlePhrases(preFormatted, config);
|
|
345
|
+
|
|
346
|
+
const hydratedArticles = config.githubModels.enabled && config.githubModels.fetchArticleContent
|
|
347
|
+
? await hydrateArticleContent(modelEligible, config)
|
|
348
|
+
: modelEligible;
|
|
349
|
+
|
|
350
|
+
const fallbackArticlePhrases = buildBasicArticlePhrases(hydratedArticles, config);
|
|
351
|
+
let articlePhrases: string[];
|
|
352
|
+
if (config.githubModels.enabled && hydratedArticles.length > 0) {
|
|
353
|
+
const { uncached, cachedPhrases } = partitionArticlesByModelCache(hydratedArticles, config);
|
|
354
|
+
if (uncached.length === 0) {
|
|
355
|
+
logInfo(config, `All ${type} articles already in model cache — skipping GitHub Models API`);
|
|
356
|
+
articlePhrases = cachedPhrases.length > 0 ? cachedPhrases : fallbackArticlePhrases;
|
|
357
|
+
} else {
|
|
358
|
+
try {
|
|
359
|
+
const newPhrases = await buildModelArticlePhrases(uncached, config, {
|
|
360
|
+
sourceType: type,
|
|
361
|
+
onProgress: (message: string) => {
|
|
362
|
+
startInteractiveProgress(message);
|
|
363
|
+
healthTracker.setPhase('formatting-phrases', message);
|
|
364
|
+
},
|
|
365
|
+
onPhrases: (phrases: string[]) => {
|
|
366
|
+
const latest = phrases[phrases.length - 1];
|
|
367
|
+
if (latest) {
|
|
368
|
+
const truncated = latest.length > 100 ? latest.slice(0, 100) + '…' : latest;
|
|
369
|
+
startInteractiveProgress(`${pc.dim('•')} ${truncated}`);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
cacheModelResults(uncached, newPhrases, config);
|
|
374
|
+
articlePhrases = [...cachedPhrases, ...newPhrases];
|
|
375
|
+
} catch (error: unknown) {
|
|
376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
+
const warning = `GitHub Models formatting skipped for ${type} — ${message}`;
|
|
378
|
+
logInfo(config, warning);
|
|
379
|
+
healthTracker.addWarning(warning);
|
|
380
|
+
startInteractiveProgress('GitHub Models unavailable, falling back to feed phrases');
|
|
381
|
+
healthTracker.setPhase('formatting-phrases', 'GitHub Models unavailable, falling back to feed phrases');
|
|
382
|
+
articlePhrases = cachedPhrases.length > 0 ? [...cachedPhrases, ...fallbackArticlePhrases] : fallbackArticlePhrases;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
articlePhrases = fallbackArticlePhrases;
|
|
387
|
+
}
|
|
287
388
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.log(`${pc.green('•')} ${phrase}`);
|
|
389
|
+
articlePhrases = dedupePhrases([...preFormattedPhrases, ...articlePhrases]);
|
|
390
|
+
const sourcePhrases = dedupePhrases([...stockPhrases, ...articlePhrases]);
|
|
391
|
+
storePhrases(type, sourcePhrases);
|
|
392
|
+
logInfo(config, `Stored ${sourcePhrases.length} phrases for ${type}`);
|
|
293
393
|
}
|
|
294
394
|
|
|
295
|
-
|
|
395
|
+
// Merge all stored phrases (freshly fetched + retained from previous runs)
|
|
396
|
+
// Fair round-robin ensures every source gets representation
|
|
397
|
+
const phrases = dedupePhrases(getMergedPhrases(config.limit));
|
|
398
|
+
stopInteractiveProgress(dryRun ? `Dry run ready — generated ${phrases.length} phrases` : `Generated ${phrases.length} phrases`);
|
|
399
|
+
if (phrases.length === 0 && sourcesToFetch.length === 0) {
|
|
400
|
+
logInfo(config, 'All sources still fresh — nothing to update this cycle');
|
|
401
|
+
healthTracker.succeed({ sourceCount: 0, articleCount: 0, stockCount: 0, phraseCount: 0 }, 'All sources still fresh');
|
|
296
402
|
return;
|
|
297
403
|
}
|
|
298
404
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
outro(pc.yellow('Interactive run finished after preview. No settings were changed.'));
|
|
302
|
-
return;
|
|
405
|
+
if (phrases.length === 0) {
|
|
406
|
+
throw new Error('No thinking phrases were generated from the configured sources.');
|
|
303
407
|
}
|
|
304
408
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
409
|
+
const summary = {
|
|
410
|
+
sourceCount: enabledSources.length,
|
|
411
|
+
articleCount: totalArticles,
|
|
412
|
+
stockCount: totalStocks,
|
|
413
|
+
phraseCount: phrases.length,
|
|
414
|
+
};
|
|
308
415
|
|
|
309
|
-
dryRun
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
416
|
+
if (dryRun) {
|
|
417
|
+
console.log(pc.bold(pc.cyan(`Dry run only — would write ${phrases.length} phrases to "${settingsPath}"`)));
|
|
418
|
+
console.log(pc.dim('Preview:'));
|
|
419
|
+
for (const phrase of phrases.slice(0, 5)) {
|
|
420
|
+
console.log(`${pc.green('•')} ${phrase}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!isInteractive) {
|
|
424
|
+
healthTracker.succeed(summary, `Dry run generated ${phrases.length} phrases`);
|
|
313
425
|
return;
|
|
314
426
|
}
|
|
315
427
|
|
|
316
|
-
|
|
317
|
-
|
|
428
|
+
const nextAction = await promptForPostDryRunAction('dynamic phrases');
|
|
429
|
+
if (!nextAction || nextAction === 'exit') {
|
|
430
|
+
healthTracker.succeed(summary, `Dry run generated ${phrases.length} phrases`);
|
|
431
|
+
outro(pc.yellow('Interactive run finished after preview. No settings were changed.'));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (nextAction === 'edit') {
|
|
436
|
+
healthTracker.succeed(summary, `Dry run generated ${phrases.length} phrases`);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
dryRun = false;
|
|
441
|
+
healthTracker.setDryRun(false);
|
|
442
|
+
// Scheduler config was already collected upfront in promptForInteractiveOverrides
|
|
318
443
|
config = syncGitHubLookbackToScheduler(config, schedulerIntervalSeconds);
|
|
444
|
+
|
|
445
|
+
outro(pc.green('Installing dynamic phrases…'));
|
|
319
446
|
}
|
|
320
447
|
|
|
321
|
-
|
|
322
|
-
|
|
448
|
+
if (isInteractive) {
|
|
449
|
+
if (createNewConfig) {
|
|
450
|
+
const namedConfigPath = await promptForConfigName(config);
|
|
451
|
+
if (!namedConfigPath) {
|
|
452
|
+
outro(pc.yellow('Interactive run cancelled. No settings were changed.'));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
323
455
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (!namedConfigPath) {
|
|
328
|
-
outro(pc.yellow('Interactive run cancelled. No settings were changed.'));
|
|
329
|
-
return;
|
|
456
|
+
configPath = resolveConfigPath(namedConfigPath);
|
|
457
|
+
schedulerConfigPath = namedConfigPath;
|
|
458
|
+
createNewConfig = false;
|
|
330
459
|
}
|
|
331
460
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
461
|
+
if (!configPath) {
|
|
462
|
+
throw new Error('Interactive config path was not resolved before save.');
|
|
463
|
+
}
|
|
336
464
|
|
|
337
|
-
|
|
338
|
-
|
|
465
|
+
writeConfigFile(configPath, config);
|
|
466
|
+
console.log(pc.dim(`Saved dynamic config → ${formatConfigPathForDisplay(configPath)}`));
|
|
339
467
|
}
|
|
340
468
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
console.log(pc.green(`Scheduler installed for every ${schedulerIntervalSeconds} seconds using ${resolvedSchedulerConfigPath}.`));
|
|
360
|
-
}
|
|
469
|
+
healthTracker.setPhase('writing-settings', `Writing ${phrases.length} phrases to VS Code settings`);
|
|
470
|
+
writeVsCodeSettings(settingsPath, phrases, config.mode);
|
|
471
|
+
console.log(pc.green(`Updated "${settingsPath}"`));
|
|
472
|
+
console.log(
|
|
473
|
+
pc.bold(
|
|
474
|
+
`Replaced thinking phrases with ${phrases.length} phrases from ${enabledSources.length} source(s)${config.githubModels.enabled ? ' using GitHub Models formatting when available' : ''}`,
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (installScheduler && process.platform === 'darwin') {
|
|
479
|
+
healthTracker.setPhase('updating-scheduler', `Installing macOS scheduler for every ${schedulerIntervalSeconds} seconds`);
|
|
480
|
+
config = syncGitHubLookbackToScheduler(config, schedulerIntervalSeconds);
|
|
481
|
+
const resolvedSchedulerConfigPath = resolveConfigPath(schedulerConfigPath ?? configPath ?? args.configPath);
|
|
482
|
+
writeConfigFile(resolvedSchedulerConfigPath, config);
|
|
483
|
+
console.log(pc.cyan(`Installing macOS launchd scheduler for every ${schedulerIntervalSeconds} seconds...`));
|
|
484
|
+
installMacScheduler(schedulerIntervalSeconds, resolvedSchedulerConfigPath);
|
|
485
|
+
console.log(pc.green(`Scheduler installed for every ${schedulerIntervalSeconds} seconds using ${resolvedSchedulerConfigPath}.`));
|
|
486
|
+
}
|
|
361
487
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
488
|
+
if (process.platform === 'darwin') {
|
|
489
|
+
const installedScheduler = getInstalledSchedulerInfo();
|
|
490
|
+
if (installedScheduler.installed) {
|
|
491
|
+
console.log(
|
|
492
|
+
pc.dim(
|
|
493
|
+
`Scheduler status: installed${installedScheduler.intervalSeconds ? ` every ${installedScheduler.intervalSeconds}s` : ''} → ${formatConfigPathForDisplay(installedScheduler.configPath ?? configPath ?? resolveConfigPath(args.configPath))}`,
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
370
497
|
}
|
|
371
|
-
}
|
|
372
498
|
|
|
373
|
-
|
|
499
|
+
healthTracker.succeed(summary, `Completed run with ${phrases.length} phrases from ${enabledSources.length} source(s)`);
|
|
500
|
+
return;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
503
|
+
failInteractiveProgress('Dynamic run failed');
|
|
504
|
+
healthTracker.fail(message);
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
374
507
|
}
|
|
375
508
|
}
|
package/src/core/scheduler.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { InstalledSchedulerInfo } from './types.js';
|
|
|
5
5
|
|
|
6
6
|
export const SCHEDULER_LABEL = 'com.austenstone.thinking-phrases.rss';
|
|
7
7
|
export const INSTALLED_PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${SCHEDULER_LABEL}.plist`);
|
|
8
|
-
export const DEFAULT_SCHEDULER_INTERVAL_SECONDS =
|
|
8
|
+
export const DEFAULT_SCHEDULER_INTERVAL_SECONDS = 60;
|
|
9
9
|
|
|
10
10
|
const IGNORED_DIRECTORIES = new Set(['.git', 'dist', 'node_modules', 'out']);
|
|
11
11
|
|