@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.
@@ -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-Z3H5GIPM.js";
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.10",
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
- (opts) => runDoctor({ packageInfo: packageInfo2, configPath: opts.config, checkUpdates: opts.checkUpdates })
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 = `Based on ${config.budget.provider}: no numbers available right now, so a run would refuse to start.`;
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 = `Based on ${config.budget.provider}: ${usage} \u2192 ${verdict}.`;
409
+ why = `${config.budget.provider} \u2014 ${usage} \u2192 ${verdict}.`;
407
410
  }
408
- return `${section(emoji.thrust, "Summary")}
409
- ${what} \u2014 at most one PR per run, up to ${formatTokens(config.agent.maxTaskTokens)} tokens.
410
- ${renderWhen(opts)}
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 `When: no afterburner schedule is installed \u2014 it only runs when you start it; \`afterburner schedule install\` would run it ${cadence}.`;
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 `When: ${cadence}, once the entries from \`afterburner schedule install\` are active (not auto-detectable on this platform).`;
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 `When: next run ${formatted} (${zone}), then ${cadence} \u2014 via the installed ${kindLabel} entry, if activated and in sync with this config${zoneNote}.`;
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 `When: an installed ${kindLabel} entry runs it on cron '${cron}' (${zone}), if activated.`;
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: \`afterburner schedule uninstall\` (or Ctrl-C for watch); ${cmd("log")} lists every past run
511
+ Stop: ${stopLine}
499
512
 
500
- ${opts.summary ? `${opts.summary}
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 { existsSync as existsSync3, statSync as statSync2 } from "fs";
794
- import { writeFile as writeFile2 } from "fs/promises";
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 resolve3 } from "path";
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 writeFile2(target, renderConfig(backend, budgetProvider, repoUrl), "utf8");
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 = existsSync3(join2(cwd, ".git")) ? cwd : null;
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) => existsSync3(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) => existsSync3(join2(dir, 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 = resolve3(base, expanded);
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: new ConsoleNotifier()
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 = spawn(command, { shell: true, stdio: ["pipe", "inherit", "inherit"] });
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: new ConsoleNotifier()
1876
+ notifier: createNotifier(config)
1693
1877
  });
1694
1878
  for (const outcome of outcomes) {
1695
1879
  console.log(`[afterburner] ${outcome.repoUrl}: ${outcome.status}, ${outcome.reason}`);
@@ -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 };
@@ -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-Z3H5GIPM.js";
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.10",
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": {