@pimmesz/afterburner 1.0.10 → 1.0.12
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/dist/{chunk-Z3H5GIPM.js → chunk-4TD2KS3A.js} +108 -0
- package/dist/cli/index.js +288 -104
- package/dist/core/index.d.ts +58 -3
- package/dist/core/index.js +9 -1
- package/package.json +1 -1
|
@@ -148,6 +148,10 @@ var scheduleConfigSchema = z.object({
|
|
|
148
148
|
cron: z.string().min(1).default("17 */4 * * *"),
|
|
149
149
|
timezone: z.string().min(1).default("UTC")
|
|
150
150
|
}).prefault({});
|
|
151
|
+
var notifyConfigSchema = z.object({
|
|
152
|
+
/** Desktop banner per completed run, opt-in. The PR stays the primary signal. */
|
|
153
|
+
desktop: z.boolean().default(false)
|
|
154
|
+
}).prefault({});
|
|
151
155
|
var agentConfigSchema = z.object({
|
|
152
156
|
backend: z.enum(["dry-run", "claude-code", "api-key"]).default("dry-run"),
|
|
153
157
|
modelByCategory: z.partialRecord(categoryEnum, modelId).default({}).transform((m) => ({ ...DEFAULT_MODEL_BY_CATEGORY, ...m })),
|
|
@@ -174,6 +178,7 @@ var configSchema = z.object({
|
|
|
174
178
|
budget: budgetConfigSchema,
|
|
175
179
|
schedule: scheduleConfigSchema,
|
|
176
180
|
agent: agentConfigSchema,
|
|
181
|
+
notify: notifyConfigSchema,
|
|
177
182
|
taskCategories: taskCategoriesConfigSchema
|
|
178
183
|
}).superRefine((config, ctx) => {
|
|
179
184
|
for (const [category, model] of Object.entries(config.agent.modelByCategory)) {
|
|
@@ -693,6 +698,85 @@ function systemdQuote(value) {
|
|
|
693
698
|
return /[\s"']/.test(value) ? `"${value.replaceAll('"', '\\"')}"` : value;
|
|
694
699
|
}
|
|
695
700
|
|
|
701
|
+
// src/core/notify/desktop.ts
|
|
702
|
+
import { execFile } from "child_process";
|
|
703
|
+
import { promisify } from "util";
|
|
704
|
+
var run = promisify(execFile);
|
|
705
|
+
var EXEC_TIMEOUT_MS = 1e4;
|
|
706
|
+
var DesktopNotifier = class {
|
|
707
|
+
exec;
|
|
708
|
+
has;
|
|
709
|
+
platform;
|
|
710
|
+
constructor(deps = {}) {
|
|
711
|
+
this.exec = deps.exec ?? ((command, args, opts) => run(command, args, { timeout: EXEC_TIMEOUT_MS, ...opts }));
|
|
712
|
+
this.has = deps.has ?? commandExists;
|
|
713
|
+
this.platform = deps.platform ?? process.platform;
|
|
714
|
+
}
|
|
715
|
+
async notify(record) {
|
|
716
|
+
const title = "Afterburner";
|
|
717
|
+
const subtitle = `${shortRepo(record.repoUrl)} \xB7 ${record.outcome}`;
|
|
718
|
+
const body = record.title;
|
|
719
|
+
try {
|
|
720
|
+
switch (this.platform) {
|
|
721
|
+
case "darwin":
|
|
722
|
+
await this.notifyMac(title, subtitle, body, record.prUrl);
|
|
723
|
+
break;
|
|
724
|
+
case "linux":
|
|
725
|
+
await this.notifyLinux(title, `${subtitle} \u2014 ${body}`);
|
|
726
|
+
break;
|
|
727
|
+
case "win32":
|
|
728
|
+
await this.notifyWindows(title, `${subtitle} \u2014 ${body}`);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async notifyMac(title, subtitle, body, prUrl) {
|
|
735
|
+
if (await this.has("terminal-notifier")) {
|
|
736
|
+
const args = ["-title", title, "-subtitle", subtitle, "-message", body, "-sound", "default"];
|
|
737
|
+
if (prUrl) args.push("-open", prUrl);
|
|
738
|
+
await this.exec("terminal-notifier", args);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const script = 'display notification (system attribute "AB_BODY") with title (system attribute "AB_TITLE") subtitle (system attribute "AB_SUBTITLE")';
|
|
742
|
+
await this.exec("osascript", ["-e", script], {
|
|
743
|
+
env: { ...process.env, AB_TITLE: title, AB_SUBTITLE: subtitle, AB_BODY: body }
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async notifyLinux(title, body) {
|
|
747
|
+
await this.exec("notify-send", ["--app-name=afterburner", "--", title, body]);
|
|
748
|
+
}
|
|
749
|
+
async notifyWindows(title, body) {
|
|
750
|
+
const aumid = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
751
|
+
const script = [
|
|
752
|
+
"[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime]>$null",
|
|
753
|
+
"[Windows.Data.Xml.Dom.XmlDocument,Windows.Data.Xml.Dom,ContentType=WindowsRuntime]>$null",
|
|
754
|
+
"$t=[System.Security.SecurityElement]::Escape($env:AB_TITLE)",
|
|
755
|
+
"$b=[System.Security.SecurityElement]::Escape($env:AB_BODY)",
|
|
756
|
+
'$x="<toast><visual><binding template=`"ToastGeneric`"><text>$t</text><text>$b</text></binding></visual></toast>"',
|
|
757
|
+
"$d=[Windows.Data.Xml.Dom.XmlDocument]::new();$d.LoadXml($x)",
|
|
758
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${aumid}').Show([Windows.UI.Notifications.ToastNotification]::new($d))`
|
|
759
|
+
].join(";");
|
|
760
|
+
await this.exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
761
|
+
env: { ...process.env, AB_TITLE: title, AB_BODY: body }
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function shortRepo(repoUrl) {
|
|
766
|
+
const trimmed = repoUrl.replace(/[/\\]+$/, "");
|
|
767
|
+
const last = trimmed.split(/[/\\]/).pop() ?? trimmed;
|
|
768
|
+
return last.replace(/\.git$/, "") || trimmed;
|
|
769
|
+
}
|
|
770
|
+
async function commandExists(command) {
|
|
771
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
772
|
+
try {
|
|
773
|
+
await run(finder, [command]);
|
|
774
|
+
return true;
|
|
775
|
+
} catch {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
696
780
|
// src/core/store/run-store.ts
|
|
697
781
|
import { appendFile, mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
|
|
698
782
|
import { dirname as dirname2 } from "path";
|
|
@@ -734,6 +818,26 @@ var ConsoleNotifier = class {
|
|
|
734
818
|
}
|
|
735
819
|
};
|
|
736
820
|
|
|
821
|
+
// src/core/notify/factory.ts
|
|
822
|
+
var CompositeNotifier = class {
|
|
823
|
+
constructor(notifiers) {
|
|
824
|
+
this.notifiers = notifiers;
|
|
825
|
+
}
|
|
826
|
+
notifiers;
|
|
827
|
+
async notify(record) {
|
|
828
|
+
for (const notifier of this.notifiers) {
|
|
829
|
+
try {
|
|
830
|
+
await notifier.notify(record);
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function createNotifier(config) {
|
|
837
|
+
const consoleNotifier = new ConsoleNotifier();
|
|
838
|
+
return config.notify.desktop ? new CompositeNotifier([consoleNotifier, new DesktopNotifier()]) : consoleNotifier;
|
|
839
|
+
}
|
|
840
|
+
|
|
737
841
|
// src/core/orchestrator.ts
|
|
738
842
|
async function runOnce(deps) {
|
|
739
843
|
const { config } = deps;
|
|
@@ -1115,8 +1219,12 @@ export {
|
|
|
1115
1219
|
scheduleArtifactPaths,
|
|
1116
1220
|
generateScheduleArtifacts,
|
|
1117
1221
|
looksRemoteRepoUrl,
|
|
1222
|
+
DesktopNotifier,
|
|
1223
|
+
commandExists,
|
|
1118
1224
|
JsonlRunStore,
|
|
1119
1225
|
ConsoleNotifier,
|
|
1226
|
+
CompositeNotifier,
|
|
1227
|
+
createNotifier,
|
|
1120
1228
|
runOnce,
|
|
1121
1229
|
ApiKeyRunner,
|
|
1122
1230
|
taskFingerprint,
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
ConsoleNotifier,
|
|
4
3
|
JsonlRunStore,
|
|
5
4
|
ManualBudgetProvider,
|
|
6
5
|
budgetFromUsageCache,
|
|
7
6
|
claudeConfigDir,
|
|
7
|
+
commandExists,
|
|
8
8
|
createBudgetProvider,
|
|
9
|
+
createNotifier,
|
|
9
10
|
createRunner,
|
|
10
11
|
createSelector,
|
|
11
12
|
dataDir,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
shouldIgnite,
|
|
25
26
|
startWatch,
|
|
26
27
|
writeUsageCache
|
|
27
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-4TD2KS3A.js";
|
|
28
29
|
import {
|
|
29
30
|
MCP_STUB_MESSAGE
|
|
30
31
|
} from "../chunk-2NSOEZWY.js";
|
|
@@ -35,7 +36,7 @@ import { Command } from "commander";
|
|
|
35
36
|
// package.json
|
|
36
37
|
var package_default = {
|
|
37
38
|
name: "@pimmesz/afterburner",
|
|
38
|
-
version: "1.0.
|
|
39
|
+
version: "1.0.12",
|
|
39
40
|
description: "Convert idle Claude subscription quota into shippable engineering work: budget-aware trigger, bounded task selection, PR-only output.",
|
|
40
41
|
license: "Apache-2.0",
|
|
41
42
|
publishConfig: {
|
|
@@ -277,9 +278,9 @@ function resolveCliEntry() {
|
|
|
277
278
|
|
|
278
279
|
// src/cli/commands/doctor.ts
|
|
279
280
|
function registerDoctor(program2, packageInfo2) {
|
|
280
|
-
program2.command("doctor").description("Check prerequisites and configuration; every failure prints an actionable fix").option("--config <path>", "path to a config file").option("--check-updates", "query the npm registry for the latest published version").action(
|
|
281
|
-
|
|
282
|
-
);
|
|
281
|
+
program2.command("doctor").description("Check prerequisites and configuration; every failure prints an actionable fix").option("--config <path>", "path to a config file").option("--check-updates", "query the npm registry for the latest published version").action(async (opts) => {
|
|
282
|
+
await runDoctor({ packageInfo: packageInfo2, configPath: opts.config, checkUpdates: opts.checkUpdates });
|
|
283
|
+
});
|
|
283
284
|
}
|
|
284
285
|
async function runDoctor(opts) {
|
|
285
286
|
console.log(`${deco(emoji.fuel)}${bold("afterburner doctor")}`);
|
|
@@ -325,6 +326,7 @@ async function runDoctor(opts) {
|
|
|
325
326
|
results.push(checkRepos(config, configPath));
|
|
326
327
|
results.push(...checkBackend(config));
|
|
327
328
|
results.push(await checkBudgetProvider(config));
|
|
329
|
+
if (config.notify.desktop) results.push(checkNotify());
|
|
328
330
|
}
|
|
329
331
|
results.push(await checkRunStoreWritable());
|
|
330
332
|
let failures = 0;
|
|
@@ -358,6 +360,7 @@ ${renderDoctorNextSteps({
|
|
|
358
360
|
})}`
|
|
359
361
|
);
|
|
360
362
|
process.exitCode = failures === 0 ? 0 : 1;
|
|
363
|
+
return { ok: failures === 0, config, configPath };
|
|
361
364
|
}
|
|
362
365
|
async function buildRunSummary(config) {
|
|
363
366
|
let budget = null;
|
|
@@ -392,7 +395,7 @@ function renderRunSummary(opts) {
|
|
|
392
395
|
const what = categories.length === 0 ? `Would review ${repoLabel}, but every repo task category is switched off in taskCategories` : `Reviews ${repoLabel} for ${categories.join("/")} tasks`;
|
|
393
396
|
let why;
|
|
394
397
|
if (!budget) {
|
|
395
|
-
why =
|
|
398
|
+
why = `${config.budget.provider} \u2014 no numbers available right now, so a run would refuse to start.`;
|
|
396
399
|
} else {
|
|
397
400
|
const gateConfig = {
|
|
398
401
|
minWeeklyHeadroomPct: config.budget.minWeeklyHeadroomPct,
|
|
@@ -403,22 +406,21 @@ function renderRunSummary(opts) {
|
|
|
403
406
|
const anySize = fullSize.go ? fullSize : shouldIgnite(budget, 0, gateConfig);
|
|
404
407
|
const verdict = fullSize.go ? "the budget gate would let a run through right now" : anySize.go ? `a task at the ${formatTokens(config.agent.maxTaskTokens)}-token cap would not fit right now, though cheaper tasks still could` : `the budget gate would block any run right now (${fullSize.reason})`;
|
|
405
408
|
const usage = `${Math.round(budget.weeklyRemainingPct)}% of the weekly quota left (~${formatTokens(Math.round(budget.weeklyRemainingTokensEst))} tokens), 5-hour session ${budget.sessionAvailable ? "available" : "exhausted"}`;
|
|
406
|
-
why =
|
|
409
|
+
why = `${config.budget.provider} \u2014 ${usage} \u2192 ${verdict}.`;
|
|
407
410
|
}
|
|
408
|
-
return
|
|
409
|
-
${
|
|
410
|
-
${
|
|
411
|
-
${why}`;
|
|
411
|
+
return ` What: ${what} \u2014 at most one PR per run, up to ${formatTokens(config.agent.maxTaskTokens)} tokens.
|
|
412
|
+
When: ${renderWhen(opts)}
|
|
413
|
+
Budget: ${why}`;
|
|
412
414
|
}
|
|
413
415
|
function renderWhen(opts) {
|
|
414
416
|
const { config } = opts;
|
|
415
417
|
const cron = config.schedule.cron;
|
|
416
418
|
const cadence = describeCadence(cron);
|
|
417
419
|
if (opts.scheduleInstalled === false) {
|
|
418
|
-
return `
|
|
420
|
+
return `no afterburner schedule is installed \u2014 it only runs when you start it; \`afterburner schedule install\` would run it ${cadence} ${config.schedule.timezone}.`;
|
|
419
421
|
}
|
|
420
422
|
if (opts.scheduleInstalled === "unknown") {
|
|
421
|
-
return
|
|
423
|
+
return `${cadence} ${config.schedule.timezone}, once the entries from \`afterburner schedule install\` are active (not auto-detectable on this platform).`;
|
|
422
424
|
}
|
|
423
425
|
const zone = opts.scheduleKind === "systemd-user" ? config.schedule.timezone : opts.systemTimezone;
|
|
424
426
|
const kindLabel = opts.scheduleKind === "systemd-user" ? "systemd" : "launchd";
|
|
@@ -434,9 +436,9 @@ function renderWhen(opts) {
|
|
|
434
436
|
minute: "2-digit",
|
|
435
437
|
hourCycle: "h23"
|
|
436
438
|
}).format(next);
|
|
437
|
-
return `
|
|
439
|
+
return `next run ${formatted} (${zone}), then ${cadence} \u2014 via the installed ${kindLabel} entry, if activated and in sync with this config${zoneNote}.`;
|
|
438
440
|
} catch {
|
|
439
|
-
return `
|
|
441
|
+
return `an installed ${kindLabel} entry runs it on cron '${cron}' (${zone}), if activated.`;
|
|
440
442
|
}
|
|
441
443
|
}
|
|
442
444
|
function describeCadence(cron) {
|
|
@@ -447,12 +449,12 @@ function describeCadence(cron) {
|
|
|
447
449
|
} catch {
|
|
448
450
|
return `on cron '${cron}'`;
|
|
449
451
|
}
|
|
450
|
-
if (hours.length === 24) return `every hour at minute ${minute}`;
|
|
451
|
-
const step2 = (hours[1] ?? 0) - (hours[0] ?? 0);
|
|
452
|
-
const uniform = hours.length > 1 && hours[0] === 0 && 24 % step2 === 0 && hours.length === 24 / step2 && hours.every((h, i) => h === i * step2);
|
|
453
|
-
if (uniform) return `every ${step2} hours at minute ${minute}`;
|
|
454
452
|
const mm = String(minute).padStart(2, "0");
|
|
453
|
+
if (hours.length === 24) return `every hour at :${mm}`;
|
|
455
454
|
const times = hours.map((h) => `${String(h).padStart(2, "0")}:${mm}`);
|
|
455
|
+
const step2 = (hours[1] ?? 0) - (hours[0] ?? 0);
|
|
456
|
+
const uniform = hours.length > 1 && hours[0] === 0 && 24 % step2 === 0 && hours.length === 24 / step2 && hours.every((h, i) => h === i * step2);
|
|
457
|
+
if (uniform) return `every ${step2} hours, at ${times.join(", ")}`;
|
|
456
458
|
return `daily at ${times.join(", ")}`;
|
|
457
459
|
}
|
|
458
460
|
function renderDoctorNextSteps(opts) {
|
|
@@ -484,6 +486,17 @@ ${nextCmd(cmd("doctor"))}`;
|
|
|
484
486
|
const engineLine = config.agent.backend === "dry-run" ? "dry-run \u2014 simulation only; set agent.backend: 'claude-code' in the config to do real work" : config.agent.backend === "claude-code" ? "claude-code \u2014 spends subscription quota you already pay for; PRs only with --live" : "api-key \u2014 bills your Anthropic API account per token (real money); PRs only with --live";
|
|
485
487
|
const budgetLine = config.budget.provider === "manual" ? "manual \u2014 trusts budget.manual (automatic option: `afterburner statusline install`, then budget.provider: 'claude-usage')" : config.budget.provider === "claude-usage" ? "claude-usage \u2014 reads your real usage via the status line hook" : "claude-code-transcripts \u2014 estimates from local Claude Code session logs";
|
|
486
488
|
const nextNote = config.agent.backend === "dry-run" ? "previews the next task; nothing is executed or spent" : `previews the next task (live execution ships in a future release; ${cmd("run-once --live")} currently validates and refuses)`;
|
|
489
|
+
const stopLine = `\`afterburner schedule uninstall\` (or Ctrl-C for watch); ${cmd("log")} lists every past run`;
|
|
490
|
+
if (opts.summary) {
|
|
491
|
+
return `${section(emoji.flame, "Ready")}
|
|
492
|
+
Config: ${configPath}
|
|
493
|
+
${opts.summary}
|
|
494
|
+
Engine: ${engineLine}
|
|
495
|
+
Stop: ${stopLine}
|
|
496
|
+
|
|
497
|
+
${next}
|
|
498
|
+
${nextCmd(cmd("run-once --dry-run"), nextNote)}`;
|
|
499
|
+
}
|
|
487
500
|
const headroomClause = `weekly headroom drops below ${config.budget.minWeeklyHeadroomPct}%`;
|
|
488
501
|
const gateLine = config.budget.requireSessionAvailable ? `runs skip themselves when no 5-hour session window is free or ${headroomClause}` : `runs skip themselves when ${headroomClause}`;
|
|
489
502
|
return `${section(emoji.flame, "Ready")}
|
|
@@ -495,11 +508,9 @@ ${nextCmd(cmd("doctor"))}`;
|
|
|
495
508
|
Gate: ${gateLine}
|
|
496
509
|
Safety: live PRs need BOTH a live engine in the config AND --live (two-part opt-in)
|
|
497
510
|
Runs: only when you start them \u2014 \`afterburner schedule install\` (recommended OS scheduler; cron '${config.schedule.cron}' ${config.schedule.timezone}) or \`afterburner watch\` (foreground) make it recurring
|
|
498
|
-
Stop:
|
|
511
|
+
Stop: ${stopLine}
|
|
499
512
|
|
|
500
|
-
${
|
|
501
|
-
|
|
502
|
-
` : ""}${next}
|
|
513
|
+
${next}
|
|
503
514
|
${nextCmd(cmd("run-once --dry-run"), nextNote)}`;
|
|
504
515
|
}
|
|
505
516
|
function checkVersion(opts) {
|
|
@@ -743,6 +754,42 @@ function formatAge(ms) {
|
|
|
743
754
|
if (minutes < 60) return `${minutes}m`;
|
|
744
755
|
return `${Math.round(minutes / 60)}h`;
|
|
745
756
|
}
|
|
757
|
+
function checkNotify(deps = {}) {
|
|
758
|
+
const platform = deps.platform ?? process.platform;
|
|
759
|
+
const has = deps.has ?? commandExistsSync;
|
|
760
|
+
if (platform === "darwin") {
|
|
761
|
+
return has("terminal-notifier") ? {
|
|
762
|
+
name: "notify",
|
|
763
|
+
ok: true,
|
|
764
|
+
detail: "desktop banners via terminal-notifier (clickable PR links)"
|
|
765
|
+
} : {
|
|
766
|
+
name: "notify",
|
|
767
|
+
ok: true,
|
|
768
|
+
detail: "desktop banners via osascript (banners only, not clickable); `brew install terminal-notifier` adds clickable PR links"
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (platform === "linux") {
|
|
772
|
+
return has("notify-send") ? { name: "notify", ok: true, detail: "desktop banners via notify-send" } : {
|
|
773
|
+
name: "notify",
|
|
774
|
+
ok: false,
|
|
775
|
+
detail: "desktop notifications enabled but notify-send is not on PATH",
|
|
776
|
+
fix: "Install libnotify (Debian/Ubuntu: `apt install libnotify-bin`, Fedora: `dnf install libnotify`), or set notify.desktop: false."
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (platform === "win32") {
|
|
780
|
+
return { name: "notify", ok: true, detail: "desktop banners via PowerShell toast" };
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
name: "notify",
|
|
784
|
+
ok: false,
|
|
785
|
+
detail: `desktop notifications enabled but no native backend exists on ${platform}`,
|
|
786
|
+
fix: "Set notify.desktop: false; this platform has no desktop banner support."
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function commandExistsSync(command) {
|
|
790
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
791
|
+
return spawnSync(finder, [command], { stdio: "ignore" }).status === 0;
|
|
792
|
+
}
|
|
746
793
|
function classifyClaudeAuth(exitStatus, stdout) {
|
|
747
794
|
if (exitStatus !== 0) {
|
|
748
795
|
return {
|
|
@@ -790,11 +837,91 @@ async function checkRunStoreWritable() {
|
|
|
790
837
|
}
|
|
791
838
|
|
|
792
839
|
// src/cli/commands/init.ts
|
|
793
|
-
import {
|
|
794
|
-
import {
|
|
840
|
+
import { spawn } from "child_process";
|
|
841
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
842
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
795
843
|
import { createInterface } from "readline/promises";
|
|
796
844
|
import { homedir } from "os";
|
|
797
|
-
import { join as join2, resolve as
|
|
845
|
+
import { join as join2, resolve as resolve4 } from "path";
|
|
846
|
+
|
|
847
|
+
// src/cli/commands/schedule.ts
|
|
848
|
+
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
849
|
+
import { existsSync as existsSync3 } from "fs";
|
|
850
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
851
|
+
function currentPlatform() {
|
|
852
|
+
const platform = process.platform;
|
|
853
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") return platform;
|
|
854
|
+
fail(
|
|
855
|
+
`Unsupported platform "${platform}" for native scheduling. Use \`afterburner watch\` instead.`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
async function performScheduleInstall(platform, configPath, config) {
|
|
859
|
+
const artifacts = generateScheduleArtifacts(platform, {
|
|
860
|
+
cron: config.schedule.cron,
|
|
861
|
+
timezone: config.schedule.timezone,
|
|
862
|
+
nodePath: process.execPath,
|
|
863
|
+
cliPath: resolveCliEntry(),
|
|
864
|
+
configPath
|
|
865
|
+
});
|
|
866
|
+
const written = [];
|
|
867
|
+
for (const file of artifacts.files) {
|
|
868
|
+
await mkdir2(dirname2(file.path), { recursive: true });
|
|
869
|
+
await writeFile2(file.path, file.content, "utf8");
|
|
870
|
+
written.push(file.path);
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
kind: artifacts.kind,
|
|
874
|
+
written,
|
|
875
|
+
activationHint: artifacts.activationHint,
|
|
876
|
+
removalHint: artifacts.removalHint
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function registerSchedule(program2) {
|
|
880
|
+
const schedule = program2.command("schedule").description("Install or remove a native OS scheduler entry (launchd / systemd / schtasks)");
|
|
881
|
+
schedule.command("install").description("Generate and install the native scheduler entry for this OS").option("--config <path>", "path to a config file (recorded in the scheduled command)").action(async (opts) => {
|
|
882
|
+
const { config, filepath } = await loadConfigOrExit(opts.config);
|
|
883
|
+
const configPath = opts.config ? resolve3(opts.config) : filepath;
|
|
884
|
+
const result = await performScheduleInstall(currentPlatform(), configPath, config);
|
|
885
|
+
for (const path of result.written) console.log(`Wrote ${path}`);
|
|
886
|
+
console.log(`
|
|
887
|
+
Kind: ${result.kind}`);
|
|
888
|
+
console.log(`Scheduled runs will use this config: ${configPath}`);
|
|
889
|
+
console.log(`Activate with:
|
|
890
|
+
${result.activationHint}`);
|
|
891
|
+
console.log(`
|
|
892
|
+
Remove later with:
|
|
893
|
+
${result.removalHint}`);
|
|
894
|
+
});
|
|
895
|
+
schedule.command("uninstall").description("Remove the native scheduler entry files for this OS").option("--config <path>", "path to a config file").action(async (opts) => {
|
|
896
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
897
|
+
const artifacts = generateScheduleArtifacts(currentPlatform(), {
|
|
898
|
+
cron: config.schedule.cron,
|
|
899
|
+
timezone: config.schedule.timezone,
|
|
900
|
+
nodePath: process.execPath,
|
|
901
|
+
cliPath: resolveCliEntry()
|
|
902
|
+
});
|
|
903
|
+
if (artifacts.files.length === 0) {
|
|
904
|
+
console.log(
|
|
905
|
+
`Nothing to delete on this platform. Remove the tasks with:
|
|
906
|
+
${artifacts.removalHint}`
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
for (const file of artifacts.files) {
|
|
911
|
+
if (existsSync3(file.path)) {
|
|
912
|
+
await rm2(file.path);
|
|
913
|
+
console.log(`Removed ${file.path}`);
|
|
914
|
+
} else {
|
|
915
|
+
console.log(`Not found (already removed?): ${file.path}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
console.log(`
|
|
919
|
+
Finish deactivation with:
|
|
920
|
+
${artifacts.removalHint}`);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/cli/commands/init.ts
|
|
798
925
|
var BACKENDS = ["dry-run", "claude-code", "api-key"];
|
|
799
926
|
var BUDGET_PROVIDERS = ["manual", "claude-usage", "claude-code-transcripts"];
|
|
800
927
|
var ENGINE_CHOICES = {
|
|
@@ -855,12 +982,18 @@ async function runInit(opts, packageInfo2) {
|
|
|
855
982
|
}
|
|
856
983
|
const { backend, budgetProvider, repoUrl, verifyNow, targetDir, target } = answers;
|
|
857
984
|
assertCanWriteConfig(targetDir, target, opts.force === true || answers.overwriteConsented);
|
|
858
|
-
await
|
|
985
|
+
await writeFile3(target, renderConfig(backend, budgetProvider, repoUrl), "utf8");
|
|
859
986
|
console.log(`
|
|
860
987
|
${step("Config", target)}`);
|
|
861
988
|
if (verifyNow) {
|
|
862
989
|
console.log();
|
|
863
|
-
await runDoctor({ packageInfo: packageInfo2, configPath: target });
|
|
990
|
+
const { ok, config, configPath } = await runDoctor({ packageInfo: packageInfo2, configPath: target });
|
|
991
|
+
if (!opts.yes && ok && config && configPath) {
|
|
992
|
+
await offerScheduleInstall(configPath, config);
|
|
993
|
+
if (await offerDesktopNotifications()) {
|
|
994
|
+
await writeFile3(target, renderConfig(backend, budgetProvider, repoUrl, true), "utf8");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
864
997
|
return;
|
|
865
998
|
}
|
|
866
999
|
console.log(
|
|
@@ -874,6 +1007,118 @@ ${renderOnboardingSummary({
|
|
|
874
1007
|
})}`
|
|
875
1008
|
);
|
|
876
1009
|
}
|
|
1010
|
+
async function offerScheduleInstall(configPath, config, deps = {}) {
|
|
1011
|
+
const platform = deps.platform ?? process.platform;
|
|
1012
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
const created = deps.rl ? null : createInterface({ input: process.stdin, output: process.stdout });
|
|
1016
|
+
created?.on("SIGINT", () => created.close());
|
|
1017
|
+
const rl = deps.rl ?? created;
|
|
1018
|
+
try {
|
|
1019
|
+
const answer = (await rl.question("\nInstall the schedule now, so it runs unattended? [y/N]: ")).trim().toLowerCase();
|
|
1020
|
+
if (answer !== "y" && answer !== "yes") {
|
|
1021
|
+
console.log(
|
|
1022
|
+
dim("Skipped \u2014 run `afterburner schedule install` whenever you want it recurring.")
|
|
1023
|
+
);
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
const install2 = deps.install ?? performScheduleInstall;
|
|
1027
|
+
const result = await install2(platform, configPath, config);
|
|
1028
|
+
for (const path of result.written) console.log(` ${green("\u2713")} wrote ${path}`);
|
|
1029
|
+
console.log(
|
|
1030
|
+
`
|
|
1031
|
+
${bold("One more step \u2014 turn it on")} (the entry is written but not yet active):
|
|
1032
|
+
${result.activationHint}`
|
|
1033
|
+
);
|
|
1034
|
+
console.log(dim("\nStop it later with `afterburner schedule uninstall`."));
|
|
1035
|
+
return true;
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (error?.code === "ABORT_ERR") {
|
|
1038
|
+
console.log(dim("\nSkipped scheduling."));
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
console.log(
|
|
1042
|
+
errYellow(
|
|
1043
|
+
`Could not install a native schedule: ${error instanceof Error ? error.message : String(error)}. Run \`afterburner schedule install\` later, or use \`afterburner watch\`.`
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
1046
|
+
return false;
|
|
1047
|
+
} finally {
|
|
1048
|
+
created?.close();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async function offerDesktopNotifications(deps = {}) {
|
|
1052
|
+
const platform = deps.platform ?? process.platform;
|
|
1053
|
+
const created = deps.rl ? null : createInterface({ input: process.stdin, output: process.stdout });
|
|
1054
|
+
created?.on("SIGINT", () => created.close());
|
|
1055
|
+
const rl = deps.rl ?? created;
|
|
1056
|
+
try {
|
|
1057
|
+
const answer = (await rl.question("\nSend a desktop notification when a run completes? [y/N]: ")).trim().toLowerCase();
|
|
1058
|
+
if (answer !== "y" && answer !== "yes") {
|
|
1059
|
+
console.log(dim("Skipped \u2014 set notify.desktop: true in the config to enable later."));
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
const has = deps.has ?? commandExists;
|
|
1063
|
+
if (platform === "darwin") {
|
|
1064
|
+
if (!await has("terminal-notifier") && await has("brew")) {
|
|
1065
|
+
const sub = (await rl.question(
|
|
1066
|
+
"Install terminal-notifier for clickable banners (brew install terminal-notifier)? [y/N]: "
|
|
1067
|
+
)).trim().toLowerCase();
|
|
1068
|
+
if (sub === "y" || sub === "yes") {
|
|
1069
|
+
try {
|
|
1070
|
+
console.log(dim("Installing terminal-notifier\u2026"));
|
|
1071
|
+
await (deps.install ?? brewInstall)("terminal-notifier");
|
|
1072
|
+
console.log(` ${green("\u2713")} installed terminal-notifier`);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
console.log(
|
|
1075
|
+
errYellow(
|
|
1076
|
+
`Could not install terminal-notifier: ${error instanceof Error ? error.message : String(error)}. Banners will use osascript (no click) until you install it.`
|
|
1077
|
+
)
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const backendReady = platform === "darwin" || platform === "win32" ? true : platform === "linux" ? await has("notify-send") : false;
|
|
1084
|
+
if (backendReady) {
|
|
1085
|
+
console.log(dim("Desktop notifications on \u2014 a banner fires when a run completes."));
|
|
1086
|
+
} else if (platform === "linux") {
|
|
1087
|
+
console.log(
|
|
1088
|
+
errYellow(
|
|
1089
|
+
"Desktop notifications enabled, but notify-send is not on PATH \u2014 install libnotify (Debian/Ubuntu: `apt install libnotify-bin`) for banners to appear."
|
|
1090
|
+
)
|
|
1091
|
+
);
|
|
1092
|
+
} else {
|
|
1093
|
+
console.log(
|
|
1094
|
+
errYellow(
|
|
1095
|
+
`Desktop notifications enabled, but ${platform} has no desktop banner backend; runs still log to the console and run store.`
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (error?.code === "ABORT_ERR") return false;
|
|
1102
|
+
console.log(
|
|
1103
|
+
errYellow(
|
|
1104
|
+
`Could not set up notifications: ${error instanceof Error ? error.message : String(error)}.`
|
|
1105
|
+
)
|
|
1106
|
+
);
|
|
1107
|
+
return false;
|
|
1108
|
+
} finally {
|
|
1109
|
+
created?.close();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function brewInstall(formula) {
|
|
1113
|
+
return new Promise((resolve7, reject) => {
|
|
1114
|
+
const child = spawn("brew", ["install", formula], { stdio: "inherit" });
|
|
1115
|
+
child.on("error", reject);
|
|
1116
|
+
child.on(
|
|
1117
|
+
"close",
|
|
1118
|
+
(code) => code === 0 ? resolve7() : reject(new Error(`brew install exited with code ${code}`))
|
|
1119
|
+
);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
877
1122
|
async function collectAnswers(rl, opts, cwd = process.cwd()) {
|
|
878
1123
|
let targetDir = cwd;
|
|
879
1124
|
let target = join2(targetDir, "afterburner.config.mjs");
|
|
@@ -888,7 +1133,7 @@ async function collectAnswers(rl, opts, cwd = process.cwd()) {
|
|
|
888
1133
|
console.log(step("Engine", backend));
|
|
889
1134
|
console.log(`
|
|
890
1135
|
${bold("Step 2 of 3 \u2014 Repository (the allowlist of what it may touch)")}`);
|
|
891
|
-
const detectedRepo =
|
|
1136
|
+
const detectedRepo = existsSync4(join2(cwd, ".git")) ? cwd : null;
|
|
892
1137
|
if (detectedRepo) {
|
|
893
1138
|
console.log(` - press Enter to use this folder (a git repo): ${detectedRepo}`);
|
|
894
1139
|
}
|
|
@@ -957,7 +1202,7 @@ function assertCanWriteConfig(targetDir, target, force) {
|
|
|
957
1202
|
}
|
|
958
1203
|
function configConflict(targetDir, target) {
|
|
959
1204
|
const existing = CONFIG_FILENAMES.map((name) => join2(targetDir, name)).filter(
|
|
960
|
-
(p) =>
|
|
1205
|
+
(p) => existsSync4(p)
|
|
961
1206
|
);
|
|
962
1207
|
return {
|
|
963
1208
|
exact: existing.includes(target) ? target : null,
|
|
@@ -978,7 +1223,7 @@ Overwrite it with these new answers? Only this file changes \u2014 your repo is
|
|
|
978
1223
|
return answer === "y" || answer === "yes";
|
|
979
1224
|
}
|
|
980
1225
|
function hasConfig(dir) {
|
|
981
|
-
return CONFIG_FILENAMES.some((name) =>
|
|
1226
|
+
return CONFIG_FILENAMES.some((name) => existsSync4(join2(dir, name)));
|
|
982
1227
|
}
|
|
983
1228
|
async function askChoice(rl, prompt, count) {
|
|
984
1229
|
for (; ; ) {
|
|
@@ -991,7 +1236,7 @@ async function askChoice(rl, prompt, count) {
|
|
|
991
1236
|
function localRepoPath(repoUrl, base = process.cwd()) {
|
|
992
1237
|
if (repoUrl === "" || looksRemoteRepoUrl(repoUrl)) return null;
|
|
993
1238
|
const expanded = repoUrl === "~" ? homedir() : repoUrl.replace(/^~(?=\/)/, homedir());
|
|
994
|
-
const absolute =
|
|
1239
|
+
const absolute = resolve4(base, expanded);
|
|
995
1240
|
return statSync2(absolute, { throwIfNoEntry: false })?.isDirectory() ? absolute : null;
|
|
996
1241
|
}
|
|
997
1242
|
function renderOnboardingSummary(opts) {
|
|
@@ -1017,7 +1262,7 @@ function renderOnboardingSummary(opts) {
|
|
|
1017
1262
|
);
|
|
1018
1263
|
return lines.join("\n");
|
|
1019
1264
|
}
|
|
1020
|
-
function renderConfig(backend, budgetProvider, repoUrl) {
|
|
1265
|
+
function renderConfig(backend, budgetProvider, repoUrl, desktopNotify = false) {
|
|
1021
1266
|
const repoBlock = repoUrl ? ` {
|
|
1022
1267
|
url: ${JSON.stringify(repoUrl)},
|
|
1023
1268
|
defaultBranch: 'main',
|
|
@@ -1067,6 +1312,11 @@ ${repoBlock}
|
|
|
1067
1312
|
// money at API rates outside promotional windows.
|
|
1068
1313
|
allowFable: false,
|
|
1069
1314
|
},
|
|
1315
|
+
notify: {
|
|
1316
|
+
// Desktop banner when a run completes (macOS/Linux/Windows). The opened PR
|
|
1317
|
+
// is still the primary notification; this is a local convenience.
|
|
1318
|
+
desktop: ${desktopNotify},
|
|
1319
|
+
},
|
|
1070
1320
|
};
|
|
1071
1321
|
|
|
1072
1322
|
export default config;
|
|
@@ -1166,7 +1416,7 @@ function registerRunOnce(program2) {
|
|
|
1166
1416
|
selector: createSelector(config),
|
|
1167
1417
|
runner,
|
|
1168
1418
|
store: new JsonlRunStore(),
|
|
1169
|
-
notifier:
|
|
1419
|
+
notifier: createNotifier(config)
|
|
1170
1420
|
});
|
|
1171
1421
|
if (outcomes.some((o) => o.status === "completed")) await ignite();
|
|
1172
1422
|
for (const outcome of outcomes) {
|
|
@@ -1199,72 +1449,6 @@ ${section(emoji.rocket, "Next")}`);
|
|
|
1199
1449
|
});
|
|
1200
1450
|
}
|
|
1201
1451
|
|
|
1202
|
-
// src/cli/commands/schedule.ts
|
|
1203
|
-
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1204
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1205
|
-
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
1206
|
-
function currentPlatform() {
|
|
1207
|
-
const platform = process.platform;
|
|
1208
|
-
if (platform === "darwin" || platform === "linux" || platform === "win32") return platform;
|
|
1209
|
-
fail(
|
|
1210
|
-
`Unsupported platform "${platform}" for native scheduling. Use \`afterburner watch\` instead.`
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
function registerSchedule(program2) {
|
|
1214
|
-
const schedule = program2.command("schedule").description("Install or remove a native OS scheduler entry (launchd / systemd / schtasks)");
|
|
1215
|
-
schedule.command("install").description("Generate and install the native scheduler entry for this OS").option("--config <path>", "path to a config file (recorded in the scheduled command)").action(async (opts) => {
|
|
1216
|
-
const { config, filepath } = await loadConfigOrExit(opts.config);
|
|
1217
|
-
const configPath = opts.config ? resolve4(opts.config) : filepath;
|
|
1218
|
-
const artifacts = generateScheduleArtifacts(currentPlatform(), {
|
|
1219
|
-
cron: config.schedule.cron,
|
|
1220
|
-
timezone: config.schedule.timezone,
|
|
1221
|
-
nodePath: process.execPath,
|
|
1222
|
-
cliPath: resolveCliEntry(),
|
|
1223
|
-
configPath
|
|
1224
|
-
});
|
|
1225
|
-
for (const file of artifacts.files) {
|
|
1226
|
-
await mkdir2(dirname2(file.path), { recursive: true });
|
|
1227
|
-
await writeFile3(file.path, file.content, "utf8");
|
|
1228
|
-
console.log(`Wrote ${file.path}`);
|
|
1229
|
-
}
|
|
1230
|
-
console.log(`
|
|
1231
|
-
Kind: ${artifacts.kind}`);
|
|
1232
|
-
console.log(`Scheduled runs will use this config: ${configPath}`);
|
|
1233
|
-
console.log(`Activate with:
|
|
1234
|
-
${artifacts.activationHint}`);
|
|
1235
|
-
console.log(`
|
|
1236
|
-
Remove later with:
|
|
1237
|
-
${artifacts.removalHint}`);
|
|
1238
|
-
});
|
|
1239
|
-
schedule.command("uninstall").description("Remove the native scheduler entry files for this OS").option("--config <path>", "path to a config file").action(async (opts) => {
|
|
1240
|
-
const { config } = await loadConfigOrExit(opts.config);
|
|
1241
|
-
const artifacts = generateScheduleArtifacts(currentPlatform(), {
|
|
1242
|
-
cron: config.schedule.cron,
|
|
1243
|
-
timezone: config.schedule.timezone,
|
|
1244
|
-
nodePath: process.execPath,
|
|
1245
|
-
cliPath: resolveCliEntry()
|
|
1246
|
-
});
|
|
1247
|
-
if (artifacts.files.length === 0) {
|
|
1248
|
-
console.log(
|
|
1249
|
-
`Nothing to delete on this platform. Remove the tasks with:
|
|
1250
|
-
${artifacts.removalHint}`
|
|
1251
|
-
);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
for (const file of artifacts.files) {
|
|
1255
|
-
if (existsSync4(file.path)) {
|
|
1256
|
-
await rm2(file.path);
|
|
1257
|
-
console.log(`Removed ${file.path}`);
|
|
1258
|
-
} else {
|
|
1259
|
-
console.log(`Not found (already removed?): ${file.path}`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
console.log(`
|
|
1263
|
-
Finish deactivation with:
|
|
1264
|
-
${artifacts.removalHint}`);
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
1452
|
// src/cli/commands/skill.ts
|
|
1269
1453
|
import { copyFile, mkdir as mkdir3 } from "fs/promises";
|
|
1270
1454
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -1294,7 +1478,7 @@ function registerSkill(program2) {
|
|
|
1294
1478
|
}
|
|
1295
1479
|
|
|
1296
1480
|
// src/cli/commands/statusline.ts
|
|
1297
|
-
import { spawn } from "child_process";
|
|
1481
|
+
import { spawn as spawn2 } from "child_process";
|
|
1298
1482
|
import { existsSync as existsSync6 } from "fs";
|
|
1299
1483
|
import { copyFile as copyFile2, mkdir as mkdir4, readFile, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
1300
1484
|
import { dirname as dirname3, join as join4 } from "path";
|
|
@@ -1369,7 +1553,7 @@ async function readWrappedState() {
|
|
|
1369
1553
|
}
|
|
1370
1554
|
function passThrough(command, input) {
|
|
1371
1555
|
return new Promise((resolve7) => {
|
|
1372
|
-
const child =
|
|
1556
|
+
const child = spawn2(command, { shell: true, stdio: ["pipe", "inherit", "inherit"] });
|
|
1373
1557
|
child.on("error", () => resolve7());
|
|
1374
1558
|
child.on("close", () => resolve7());
|
|
1375
1559
|
child.stdin.on("error", () => {
|
|
@@ -1689,7 +1873,7 @@ function registerWatch(program2) {
|
|
|
1689
1873
|
selector: createSelector(config),
|
|
1690
1874
|
runner,
|
|
1691
1875
|
store: new JsonlRunStore(),
|
|
1692
|
-
notifier:
|
|
1876
|
+
notifier: createNotifier(config)
|
|
1693
1877
|
});
|
|
1694
1878
|
for (const outcome of outcomes) {
|
|
1695
1879
|
console.log(`[afterburner] ${outcome.repoUrl}: ${outcome.status}, ${outcome.reason}`);
|
package/dist/core/index.d.ts
CHANGED
|
@@ -53,8 +53,8 @@ declare const budgetConfigSchema: z.ZodPrefault<z.ZodObject<{
|
|
|
53
53
|
}, z.core.$strip>>;
|
|
54
54
|
declare const agentConfigSchema: z.ZodPrefault<z.ZodObject<{
|
|
55
55
|
backend: z.ZodDefault<z.ZodEnum<{
|
|
56
|
-
"claude-code": "claude-code";
|
|
57
56
|
"dry-run": "dry-run";
|
|
57
|
+
"claude-code": "claude-code";
|
|
58
58
|
"api-key": "api-key";
|
|
59
59
|
}>>;
|
|
60
60
|
modelByCategory: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
@@ -115,8 +115,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
115
115
|
}, z.core.$strip>>;
|
|
116
116
|
agent: z.ZodPrefault<z.ZodObject<{
|
|
117
117
|
backend: z.ZodDefault<z.ZodEnum<{
|
|
118
|
-
"claude-code": "claude-code";
|
|
119
118
|
"dry-run": "dry-run";
|
|
119
|
+
"claude-code": "claude-code";
|
|
120
120
|
"api-key": "api-key";
|
|
121
121
|
}>>;
|
|
122
122
|
modelByCategory: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
@@ -139,6 +139,9 @@ declare const configSchema: z.ZodObject<{
|
|
|
139
139
|
maxTaskTokens: z.ZodDefault<z.ZodNumber>;
|
|
140
140
|
allowFable: z.ZodDefault<z.ZodBoolean>;
|
|
141
141
|
}, z.core.$strip>>;
|
|
142
|
+
notify: z.ZodPrefault<z.ZodObject<{
|
|
143
|
+
desktop: z.ZodDefault<z.ZodBoolean>;
|
|
144
|
+
}, z.core.$strip>>;
|
|
142
145
|
taskCategories: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
143
146
|
security: "security";
|
|
144
147
|
tests: "tests";
|
|
@@ -683,6 +686,58 @@ declare class ConsoleNotifier implements Notifier {
|
|
|
683
686
|
notify(record: RunRecord): Promise<void>;
|
|
684
687
|
}
|
|
685
688
|
|
|
689
|
+
/** Spawn options we actually use; kept narrow so the injected exec is easy to fake. */
|
|
690
|
+
interface ExecOpts {
|
|
691
|
+
env?: NodeJS.ProcessEnv;
|
|
692
|
+
}
|
|
693
|
+
type Exec = (command: string, args: string[], opts?: ExecOpts) => Promise<unknown>;
|
|
694
|
+
interface DesktopNotifierDeps {
|
|
695
|
+
/** Spawns a command (argv array, never a shell string). Injected in tests. */
|
|
696
|
+
exec?: Exec;
|
|
697
|
+
/** Whether a binary is resolvable on PATH. Injected in tests. */
|
|
698
|
+
has?: (command: string) => Promise<boolean>;
|
|
699
|
+
platform?: NodeJS.Platform;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Fires an OS-native desktop banner per completed run, with zero npm
|
|
703
|
+
* dependencies — it shells out to platform tools: terminal-notifier (clickable)
|
|
704
|
+
* or osascript on macOS, notify-send on Linux, a PowerShell toast on Windows.
|
|
705
|
+
*
|
|
706
|
+
* Two invariants:
|
|
707
|
+
* 1. Untrusted record text (title, repo) is passed as argv or via environment
|
|
708
|
+
* variables, NEVER interpolated into a shell string or a second interpreter's
|
|
709
|
+
* source. osascript evaluates AppleScript and PowerShell evaluates a script,
|
|
710
|
+
* so their dynamic text comes from env (`system attribute` / `$env:`), which
|
|
711
|
+
* closes the AppleScript/script-injection hole an argv array alone leaves open.
|
|
712
|
+
* 2. notify() never throws: a missing tool, denied permission, or headless box
|
|
713
|
+
* must not break a run. The run store stays the source of truth.
|
|
714
|
+
*/
|
|
715
|
+
declare class DesktopNotifier implements Notifier {
|
|
716
|
+
private readonly exec;
|
|
717
|
+
private readonly has;
|
|
718
|
+
private readonly platform;
|
|
719
|
+
constructor(deps?: DesktopNotifierDeps);
|
|
720
|
+
notify(record: RunRecord): Promise<void>;
|
|
721
|
+
private notifyMac;
|
|
722
|
+
private notifyLinux;
|
|
723
|
+
private notifyWindows;
|
|
724
|
+
}
|
|
725
|
+
/** True when `command` resolves on PATH (via which/where). Never throws. */
|
|
726
|
+
declare function commandExists(command: string): Promise<boolean>;
|
|
727
|
+
|
|
728
|
+
/** Runs several notifiers in order, isolating failures so one can't suppress the rest. */
|
|
729
|
+
declare class CompositeNotifier implements Notifier {
|
|
730
|
+
private readonly notifiers;
|
|
731
|
+
constructor(notifiers: Notifier[]);
|
|
732
|
+
notify(record: RunRecord): Promise<void>;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Picks the notifier from config. ConsoleNotifier ALWAYS runs: its line is the
|
|
736
|
+
* audit trail that reaches the scheduler's stdout log, so a swallowed desktop
|
|
737
|
+
* banner never costs the record. The desktop banner is layered on when opted in.
|
|
738
|
+
*/
|
|
739
|
+
declare function createNotifier(config: AfterburnerConfig): Notifier;
|
|
740
|
+
|
|
686
741
|
interface GateConfig {
|
|
687
742
|
minWeeklyHeadroomPct: number;
|
|
688
743
|
safetyMarginTokens: number;
|
|
@@ -778,4 +833,4 @@ interface RepoRunOutcome {
|
|
|
778
833
|
*/
|
|
779
834
|
declare function runOnce(deps: RunOnceDeps): Promise<RepoRunOutcome[]>;
|
|
780
835
|
|
|
781
|
-
export { type AfterburnerConfig, type AfterburnerUserConfig, type AgentConfig, type AgentRunner, ApiKeyRunner, BASE_TASK_TOKENS, type Budget, type BudgetConfig, type BudgetProvider, type BudgetProviderOptions, type CandidateTask, type ClaudeCodeInvocation, ClaudeCodeRunner, ClaudeCodeTranscriptsBudgetProvider, type ClaudeCodeTranscriptsOptions, type ClaudeUsageBudgetOptions, ClaudeUsageBudgetProvider, ConsoleNotifier, DEFAULT_MODEL_BY_CATEGORY, type DeterministicSelectorOptions, DeterministicTaskSelector, DryRunRunner, type GateConfig, type GateDecision, JsonlRunStore, type LoadedConfig, ManualBudgetProvider, type ModelCostTable, type ModelWeightEntry, type NativeScheduleOptions, type Notifier, type RateLimitWindow, type RateLimits, type RepoConfig, type RepoRunOutcome, type RunOnceDeps, type RunOutcome, type RunRecord, type RunResult, type RunStore, type RunnerBackend, type ScheduleArtifacts, type SupportedPlatform, TASK_CATEGORIES, TASK_TAXONOMY, type TaskCategory, type TaskCategoryInfo, type TaskIdentity, type TaskSelector, type TranscriptUsageSummary, type UsageCache, type UsageCacheBudgetOptions, type UsageCacheBudgetResult, type WatchHandle, assertModelAllowed, budgetFromUsageCache, buildClaudeCodeInvocation, claudeConfigDir, configDir, configSchema, createBudgetProvider, createModelCostTable, createRunner, createSelector, dataDir, defaultClaudeProjectsDir, defaultCostTable, defaultRunStorePath, defaultUsageCachePath, defineConfig, deriveBranchName, derivePrTitle, formatConfigError, generateScheduleArtifacts, isFableModel, loadConfig, mapConfigLoadError, parseSimpleCron, readUsageCache, repoConfigSchema, runOnce, sanitizeSpawnEnv, shouldIgnite, startWatch, summarizeTranscriptUsage, taskFingerprint, writeUsageCache };
|
|
836
|
+
export { type AfterburnerConfig, type AfterburnerUserConfig, type AgentConfig, type AgentRunner, ApiKeyRunner, BASE_TASK_TOKENS, type Budget, type BudgetConfig, type BudgetProvider, type BudgetProviderOptions, type CandidateTask, type ClaudeCodeInvocation, ClaudeCodeRunner, ClaudeCodeTranscriptsBudgetProvider, type ClaudeCodeTranscriptsOptions, type ClaudeUsageBudgetOptions, ClaudeUsageBudgetProvider, CompositeNotifier, ConsoleNotifier, DEFAULT_MODEL_BY_CATEGORY, DesktopNotifier, type DeterministicSelectorOptions, DeterministicTaskSelector, DryRunRunner, type GateConfig, type GateDecision, JsonlRunStore, type LoadedConfig, ManualBudgetProvider, type ModelCostTable, type ModelWeightEntry, type NativeScheduleOptions, type Notifier, type RateLimitWindow, type RateLimits, type RepoConfig, type RepoRunOutcome, type RunOnceDeps, type RunOutcome, type RunRecord, type RunResult, type RunStore, type RunnerBackend, type ScheduleArtifacts, type SupportedPlatform, TASK_CATEGORIES, TASK_TAXONOMY, type TaskCategory, type TaskCategoryInfo, type TaskIdentity, type TaskSelector, type TranscriptUsageSummary, type UsageCache, type UsageCacheBudgetOptions, type UsageCacheBudgetResult, type WatchHandle, assertModelAllowed, budgetFromUsageCache, buildClaudeCodeInvocation, claudeConfigDir, commandExists, configDir, configSchema, createBudgetProvider, createModelCostTable, createNotifier, createRunner, createSelector, dataDir, defaultClaudeProjectsDir, defaultCostTable, defaultRunStorePath, defaultUsageCachePath, defineConfig, deriveBranchName, derivePrTitle, formatConfigError, generateScheduleArtifacts, isFableModel, loadConfig, mapConfigLoadError, parseSimpleCron, readUsageCache, repoConfigSchema, runOnce, sanitizeSpawnEnv, shouldIgnite, startWatch, summarizeTranscriptUsage, taskFingerprint, writeUsageCache };
|
package/dist/core/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
ClaudeCodeRunner,
|
|
5
5
|
ClaudeCodeTranscriptsBudgetProvider,
|
|
6
6
|
ClaudeUsageBudgetProvider,
|
|
7
|
+
CompositeNotifier,
|
|
7
8
|
ConsoleNotifier,
|
|
8
9
|
DEFAULT_MODEL_BY_CATEGORY,
|
|
10
|
+
DesktopNotifier,
|
|
9
11
|
DeterministicTaskSelector,
|
|
10
12
|
DryRunRunner,
|
|
11
13
|
JsonlRunStore,
|
|
@@ -16,10 +18,12 @@ import {
|
|
|
16
18
|
budgetFromUsageCache,
|
|
17
19
|
buildClaudeCodeInvocation,
|
|
18
20
|
claudeConfigDir,
|
|
21
|
+
commandExists,
|
|
19
22
|
configDir,
|
|
20
23
|
configSchema,
|
|
21
24
|
createBudgetProvider,
|
|
22
25
|
createModelCostTable,
|
|
26
|
+
createNotifier,
|
|
23
27
|
createRunner,
|
|
24
28
|
createSelector,
|
|
25
29
|
dataDir,
|
|
@@ -45,15 +49,17 @@ import {
|
|
|
45
49
|
summarizeTranscriptUsage,
|
|
46
50
|
taskFingerprint,
|
|
47
51
|
writeUsageCache
|
|
48
|
-
} from "../chunk-
|
|
52
|
+
} from "../chunk-4TD2KS3A.js";
|
|
49
53
|
export {
|
|
50
54
|
ApiKeyRunner,
|
|
51
55
|
BASE_TASK_TOKENS,
|
|
52
56
|
ClaudeCodeRunner,
|
|
53
57
|
ClaudeCodeTranscriptsBudgetProvider,
|
|
54
58
|
ClaudeUsageBudgetProvider,
|
|
59
|
+
CompositeNotifier,
|
|
55
60
|
ConsoleNotifier,
|
|
56
61
|
DEFAULT_MODEL_BY_CATEGORY,
|
|
62
|
+
DesktopNotifier,
|
|
57
63
|
DeterministicTaskSelector,
|
|
58
64
|
DryRunRunner,
|
|
59
65
|
JsonlRunStore,
|
|
@@ -64,10 +70,12 @@ export {
|
|
|
64
70
|
budgetFromUsageCache,
|
|
65
71
|
buildClaudeCodeInvocation,
|
|
66
72
|
claudeConfigDir,
|
|
73
|
+
commandExists,
|
|
67
74
|
configDir,
|
|
68
75
|
configSchema,
|
|
69
76
|
createBudgetProvider,
|
|
70
77
|
createModelCostTable,
|
|
78
|
+
createNotifier,
|
|
71
79
|
createRunner,
|
|
72
80
|
createSelector,
|
|
73
81
|
dataDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pimmesz/afterburner",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Convert idle Claude subscription quota into shippable engineering work: budget-aware trigger, bounded task selection, PR-only output.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|