@tekmidian/pai 0.9.4 → 0.9.6

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.
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  var WHISPER_FILE = join(homedir(), ".claude", "whisper-rules.md");
8
+ var ADVISOR_FILE = join(homedir(), ".claude", "advisor-mode.json");
8
9
  function getWhisperRules() {
9
10
  if (existsSync(WHISPER_FILE)) {
10
11
  try {
@@ -15,11 +16,66 @@ function getWhisperRules() {
15
16
  }
16
17
  return "";
17
18
  }
19
+ function getAdvisorGuidance() {
20
+ if (!existsSync(ADVISOR_FILE)) return "";
21
+ let config;
22
+ try {
23
+ config = JSON.parse(readFileSync(ADVISOR_FILE, "utf-8"));
24
+ } catch {
25
+ return "";
26
+ }
27
+ let mode = config.mode ?? "auto";
28
+ if (mode === "auto" && typeof config.weeklyBudgetPercent === "number") {
29
+ const pct = config.weeklyBudgetPercent;
30
+ if (pct < 60) mode = "normal";
31
+ else if (pct < 80) mode = "conservative";
32
+ else if (pct < 92) mode = "strict";
33
+ else mode = "critical";
34
+ }
35
+ if (config.forceModel) {
36
+ return `ADVISOR MODE: Use model "${config.forceModel}" for ALL subagents (Agent tool calls). This is a manual override.`;
37
+ }
38
+ switch (mode) {
39
+ case "normal":
40
+ return "";
41
+ // No constraints — use models freely
42
+ case "conservative":
43
+ return [
44
+ `ADVISOR MODE (conservative \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
45
+ "Use HAIKU for all subagents (Agent tool, model: haiku) unless the task explicitly requires deep reasoning.",
46
+ "Main context stays on the current model. Only escalate subagents to sonnet if haiku output quality is insufficient.",
47
+ "Prefer fewer, more focused subagent calls over many parallel ones."
48
+ ].join(" ");
49
+ case "strict":
50
+ return [
51
+ `ADVISOR MODE (strict \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
52
+ "Use HAIKU for ALL subagents without exception (Agent tool, model: haiku).",
53
+ "Minimize subagent spawning \u2014 do simple tasks directly in main context.",
54
+ "Only spawn subagents for genuinely independent parallel work.",
55
+ "Never spawn opus subagents."
56
+ ].join(" ");
57
+ case "critical":
58
+ return [
59
+ `ADVISOR MODE (critical \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
60
+ "MINIMIZE ALL TOKEN USAGE. Do NOT spawn subagents unless absolutely essential.",
61
+ "Work directly in main context. Keep responses concise.",
62
+ "Use haiku model if you must spawn a subagent.",
63
+ "Skip background research, parallel exploration, and spotchecks.",
64
+ "The user is near their weekly limit \u2014 every token counts."
65
+ ].join(" ");
66
+ default:
67
+ return "";
68
+ }
69
+ }
18
70
  function main() {
71
+ const parts = [];
19
72
  const rules = getWhisperRules();
20
- if (!rules) return;
73
+ if (rules) parts.push(rules);
74
+ const advisor = getAdvisorGuidance();
75
+ if (advisor) parts.push(advisor);
76
+ if (parts.length === 0) return;
21
77
  console.log(`<system-reminder>
22
- ${rules}
78
+ ${parts.join("\n")}
23
79
  </system-reminder>`);
24
80
  }
25
81
  main();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/ts/user-prompt/whisper-rules.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-rules.ts\n *\n * UserPromptSubmit hook that injects critical non-negotiable rules into every\n * prompt as a <system-reminder>. This ensures rules survive compaction \u2014 even\n * if CLAUDE.md content is lost during context compression, the whisper\n * re-injects the absolute rules on every single turn.\n *\n * Inspired by Letta's \"claude-subconscious\" whisper pattern.\n *\n * Rules are loaded from ~/.claude/whisper-rules.md if it exists,\n * otherwise falls back to hardcoded critical rules.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst WHISPER_FILE = join(homedir(), \".claude\", \"whisper-rules.md\");\n\nfunction getWhisperRules(): string {\n // User-managed whisper file \u2014 PAI provides the hook, user provides the rules\n // Configure via /whisper skill or edit ~/.claude/whisper-rules.md directly\n if (existsSync(WHISPER_FILE)) {\n try {\n const content = readFileSync(WHISPER_FILE, \"utf-8\").trim();\n if (content) return content;\n } catch { /* ignore read errors */ }\n }\n\n // No rules configured \u2014 silent (no injection)\n return \"\";\n}\n\nfunction main() {\n const rules = getWhisperRules();\n if (!rules) return;\n\n // Output as system-reminder \u2014 Claude Code injects this into the conversation\n console.log(`<system-reminder>\n${rules}\n</system-reminder>`);\n}\n\nmain();\n"],
5
- "mappings": ";;;AAgBA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AAExB,IAAM,eAAe,KAAK,QAAQ,GAAG,WAAW,kBAAkB;AAElE,SAAS,kBAA0B;AAGjC,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI;AACF,YAAM,UAAU,aAAa,cAAc,OAAO,EAAE,KAAK;AACzD,UAAI,QAAS,QAAO;AAAA,IACtB,QAAQ;AAAA,IAA2B;AAAA,EACrC;AAGA,SAAO;AACT;AAEA,SAAS,OAAO;AACd,QAAM,QAAQ,gBAAgB;AAC9B,MAAI,CAAC,MAAO;AAGZ,UAAQ,IAAI;AAAA,EACZ,KAAK;AAAA,mBACY;AACnB;AAEA,KAAK;",
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-rules.ts\n *\n * UserPromptSubmit hook that injects:\n * 1. User-defined whisper rules from ~/.claude/whisper-rules.md\n * 2. Budget-aware model tiering guidance from ~/.claude/advisor-mode.json\n *\n * The advisor mode implements the \"advisor strategy\" pattern:\n * - Normal (budget < 70%): use any model freely\n * - Conservative (70-85%): prefer haiku for subagents, sonnet for main work\n * - Strict (85-95%): haiku only for subagents, main context stays on current model\n * - Critical (>95%): minimize all subagent spawning, essential work only\n *\n * Budget percentage is written by the statusline or manually to advisor-mode.json.\n * If the file doesn't exist, no advisor guidance is injected.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst WHISPER_FILE = join(homedir(), \".claude\", \"whisper-rules.md\");\nconst ADVISOR_FILE = join(homedir(), \".claude\", \"advisor-mode.json\");\n\nfunction getWhisperRules(): string {\n if (existsSync(WHISPER_FILE)) {\n try {\n const content = readFileSync(WHISPER_FILE, \"utf-8\").trim();\n if (content) return content;\n } catch { /* ignore */ }\n }\n return \"\";\n}\n\ninterface AdvisorConfig {\n weeklyBudgetPercent?: number; // 0-100, written by statusline or manually\n mode?: \"normal\" | \"conservative\" | \"strict\" | \"critical\" | \"auto\";\n forceModel?: string; // override: always use this model for subagents\n}\n\nfunction getAdvisorGuidance(): string {\n if (!existsSync(ADVISOR_FILE)) return \"\";\n\n let config: AdvisorConfig;\n try {\n config = JSON.parse(readFileSync(ADVISOR_FILE, \"utf-8\"));\n } catch {\n return \"\";\n }\n\n // Determine mode\n let mode = config.mode ?? \"auto\";\n if (mode === \"auto\" && typeof config.weeklyBudgetPercent === \"number\") {\n const pct = config.weeklyBudgetPercent;\n if (pct < 60) mode = \"normal\";\n else if (pct < 80) mode = \"conservative\";\n else if (pct < 92) mode = \"strict\";\n else mode = \"critical\";\n }\n\n // Force model override\n if (config.forceModel) {\n return `ADVISOR MODE: Use model \"${config.forceModel}\" for ALL subagents (Agent tool calls). This is a manual override.`;\n }\n\n switch (mode) {\n case \"normal\":\n return \"\"; // No constraints \u2014 use models freely\n\n case \"conservative\":\n return [\n `ADVISOR MODE (conservative \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"Use HAIKU for all subagents (Agent tool, model: haiku) unless the task explicitly requires deep reasoning.\",\n \"Main context stays on the current model. Only escalate subagents to sonnet if haiku output quality is insufficient.\",\n \"Prefer fewer, more focused subagent calls over many parallel ones.\",\n ].join(\" \");\n\n case \"strict\":\n return [\n `ADVISOR MODE (strict \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"Use HAIKU for ALL subagents without exception (Agent tool, model: haiku).\",\n \"Minimize subagent spawning \u2014 do simple tasks directly in main context.\",\n \"Only spawn subagents for genuinely independent parallel work.\",\n \"Never spawn opus subagents.\",\n ].join(\" \");\n\n case \"critical\":\n return [\n `ADVISOR MODE (critical \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"MINIMIZE ALL TOKEN USAGE. Do NOT spawn subagents unless absolutely essential.\",\n \"Work directly in main context. Keep responses concise.\",\n \"Use haiku model if you must spawn a subagent.\",\n \"Skip background research, parallel exploration, and spotchecks.\",\n \"The user is near their weekly limit \u2014 every token counts.\",\n ].join(\" \");\n\n default:\n return \"\";\n }\n}\n\nfunction main() {\n const parts: string[] = [];\n\n const rules = getWhisperRules();\n if (rules) parts.push(rules);\n\n const advisor = getAdvisorGuidance();\n if (advisor) parts.push(advisor);\n\n if (parts.length === 0) return;\n\n console.log(`<system-reminder>\\n${parts.join(\"\\n\")}\\n</system-reminder>`);\n}\n\nmain();\n"],
5
+ "mappings": ";;;AAmBA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AAExB,IAAM,eAAe,KAAK,QAAQ,GAAG,WAAW,kBAAkB;AAClE,IAAM,eAAe,KAAK,QAAQ,GAAG,WAAW,mBAAmB;AAEnE,SAAS,kBAA0B;AACjC,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI;AACF,YAAM,UAAU,aAAa,cAAc,OAAO,EAAE,KAAK;AACzD,UAAI,QAAS,QAAO;AAAA,IACtB,QAAQ;AAAA,IAAe;AAAA,EACzB;AACA,SAAO;AACT;AAQA,SAAS,qBAA6B;AACpC,MAAI,CAAC,WAAW,YAAY,EAAG,QAAO;AAEtC,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AAAA,EACzD,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,OAAO,QAAQ;AAC1B,MAAI,SAAS,UAAU,OAAO,OAAO,wBAAwB,UAAU;AACrE,UAAM,MAAM,OAAO;AACnB,QAAI,MAAM,GAAI,QAAO;AAAA,aACZ,MAAM,GAAI,QAAO;AAAA,aACjB,MAAM,GAAI,QAAO;AAAA,QACrB,QAAO;AAAA,EACd;AAGA,MAAI,OAAO,YAAY;AACrB,WAAO,4BAA4B,OAAO,UAAU;AAAA,EACtD;AAEA,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA;AAAA,IAET,KAAK;AACH,aAAO;AAAA,QACL,sDAAiD,OAAO,uBAAuB,GAAG;AAAA,QAClF;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ,KAAK;AACH,aAAO;AAAA,QACL,gDAA2C,OAAO,uBAAuB,GAAG;AAAA,QAC5E;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ,KAAK;AACH,aAAO;AAAA,QACL,kDAA6C,OAAO,uBAAuB,GAAG;AAAA,QAC9E;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,OAAO;AACd,QAAM,QAAkB,CAAC;AAEzB,QAAM,QAAQ,gBAAgB;AAC9B,MAAI,MAAO,OAAM,KAAK,KAAK;AAE3B,QAAM,UAAU,mBAAmB;AACnC,MAAI,QAAS,OAAM,KAAK,OAAO;AAE/B,MAAI,MAAM,WAAW,EAAG;AAExB,UAAQ,IAAI;AAAA,EAAsB,MAAM,KAAK,IAAI,CAAC;AAAA,mBAAsB;AAC1E;AAEA,KAAK;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: Advisor
3
+ description: "Manage budget-aware advisor mode — control model tiering for subagents based on weekly usage. USE WHEN user says 'advisor', 'budget mode', 'set advisor', 'save budget', '/advisor', 'how much budget', OR wants to control model tiering for subagents."
4
+ ---
5
+
6
+ ## Advisor Mode Management
7
+
8
+ USE WHEN user says 'advisor', 'budget mode', 'set advisor', 'save budget', '/advisor', 'how much budget', OR wants to control model tiering for subagents.
9
+
10
+ Advisor mode controls which models subagents use, based on weekly budget consumption.
11
+
12
+ ### Configuration
13
+
14
+ The config file is at `~/.claude/advisor-mode.json`:
15
+ ```json
16
+ {
17
+ "weeklyBudgetPercent": 90,
18
+ "mode": "auto"
19
+ }
20
+ ```
21
+
22
+ ### Usage
23
+
24
+ - `/advisor` — show current mode and budget
25
+ - `/advisor set <percent>` — set weekly budget percentage (triggers auto mode calculation)
26
+ - `/advisor mode <normal|conservative|strict|critical>` — force a specific mode
27
+ - `/advisor auto` — reset to auto mode (derives from weeklyBudgetPercent)
28
+ - `/advisor force <model>` — force all subagents to use a specific model (haiku/sonnet/opus)
29
+ - `/advisor reset` — remove the config file (no advisor guidance injected)
30
+
31
+ ### Mode Thresholds (auto mode)
32
+
33
+ | Budget Used | Mode | Subagent Model | Behavior |
34
+ |-------------|------|----------------|----------|
35
+ | < 70% | normal | Any | No constraints |
36
+ | 70-85% | conservative | Haiku preferred | Escalate to sonnet only if haiku insufficient |
37
+ | 85-95% | strict | Haiku only | Minimize spawning, no opus subagents |
38
+ | > 95% | critical | Haiku or none | Essential work only, minimize all token usage |
39
+
40
+ ### Workflow
41
+
42
+ **Show current status:**
43
+ Read `~/.claude/advisor-mode.json`. Display the mode, budget percentage, and what model constraints are active.
44
+
45
+ **Update budget percentage:**
46
+ The user reads their weekly budget from the statusline (e.g., "7d: 63% → Fr. 08:00").
47
+ Write the percentage to `weeklyBudgetPercent` in the config file.
48
+ If mode is "auto", the whisper hook will compute the appropriate tier.
49
+
50
+ **Force a mode:**
51
+ Set `mode` to the desired value. Overrides auto calculation.
52
+ Useful when the user wants to be aggressive (normal) or cautious (strict) regardless of actual budget.
53
+
54
+ **Force a model:**
55
+ Set `forceModel` to "haiku", "sonnet", or "opus". ALL subagents will use this model.
56
+ Useful for testing or when the user knows exactly what they want.
57
+
58
+ ### Integration
59
+
60
+ The advisor config is read by the whisper-rules hook on every prompt. Changes take effect immediately — no restart needed. The guidance appears as an ADVISOR MODE line in the system-reminder alongside the whisper rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -3,15 +3,18 @@
3
3
  /**
4
4
  * whisper-rules.ts
5
5
  *
6
- * UserPromptSubmit hook that injects critical non-negotiable rules into every
7
- * prompt as a <system-reminder>. This ensures rules survive compaction — even
8
- * if CLAUDE.md content is lost during context compression, the whisper
9
- * re-injects the absolute rules on every single turn.
6
+ * UserPromptSubmit hook that injects:
7
+ * 1. User-defined whisper rules from ~/.claude/whisper-rules.md
8
+ * 2. Budget-aware model tiering guidance from ~/.claude/advisor-mode.json
10
9
  *
11
- * Inspired by Letta's "claude-subconscious" whisper pattern.
10
+ * The advisor mode implements the "advisor strategy" pattern:
11
+ * - Normal (budget < 70%): use any model freely
12
+ * - Conservative (70-85%): prefer haiku for subagents, sonnet for main work
13
+ * - Strict (85-95%): haiku only for subagents, main context stays on current model
14
+ * - Critical (>95%): minimize all subagent spawning, essential work only
12
15
  *
13
- * Rules are loaded from ~/.claude/whisper-rules.md if it exists,
14
- * otherwise falls back to hardcoded critical rules.
16
+ * Budget percentage is written by the statusline or manually to advisor-mode.json.
17
+ * If the file doesn't exist, no advisor guidance is injected.
15
18
  */
16
19
 
17
20
  import { existsSync, readFileSync } from "node:fs";
@@ -19,29 +22,97 @@ import { join } from "node:path";
19
22
  import { homedir } from "node:os";
20
23
 
21
24
  const WHISPER_FILE = join(homedir(), ".claude", "whisper-rules.md");
25
+ const ADVISOR_FILE = join(homedir(), ".claude", "advisor-mode.json");
22
26
 
23
27
  function getWhisperRules(): string {
24
- // User-managed whisper file — PAI provides the hook, user provides the rules
25
- // Configure via /whisper skill or edit ~/.claude/whisper-rules.md directly
26
28
  if (existsSync(WHISPER_FILE)) {
27
29
  try {
28
30
  const content = readFileSync(WHISPER_FILE, "utf-8").trim();
29
31
  if (content) return content;
30
- } catch { /* ignore read errors */ }
32
+ } catch { /* ignore */ }
31
33
  }
32
-
33
- // No rules configured — silent (no injection)
34
34
  return "";
35
35
  }
36
36
 
37
+ interface AdvisorConfig {
38
+ weeklyBudgetPercent?: number; // 0-100, written by statusline or manually
39
+ mode?: "normal" | "conservative" | "strict" | "critical" | "auto";
40
+ forceModel?: string; // override: always use this model for subagents
41
+ }
42
+
43
+ function getAdvisorGuidance(): string {
44
+ if (!existsSync(ADVISOR_FILE)) return "";
45
+
46
+ let config: AdvisorConfig;
47
+ try {
48
+ config = JSON.parse(readFileSync(ADVISOR_FILE, "utf-8"));
49
+ } catch {
50
+ return "";
51
+ }
52
+
53
+ // Determine mode
54
+ let mode = config.mode ?? "auto";
55
+ if (mode === "auto" && typeof config.weeklyBudgetPercent === "number") {
56
+ const pct = config.weeklyBudgetPercent;
57
+ if (pct < 60) mode = "normal";
58
+ else if (pct < 80) mode = "conservative";
59
+ else if (pct < 92) mode = "strict";
60
+ else mode = "critical";
61
+ }
62
+
63
+ // Force model override
64
+ if (config.forceModel) {
65
+ return `ADVISOR MODE: Use model "${config.forceModel}" for ALL subagents (Agent tool calls). This is a manual override.`;
66
+ }
67
+
68
+ switch (mode) {
69
+ case "normal":
70
+ return ""; // No constraints — use models freely
71
+
72
+ case "conservative":
73
+ return [
74
+ `ADVISOR MODE (conservative — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
75
+ "Use HAIKU for all subagents (Agent tool, model: haiku) unless the task explicitly requires deep reasoning.",
76
+ "Main context stays on the current model. Only escalate subagents to sonnet if haiku output quality is insufficient.",
77
+ "Prefer fewer, more focused subagent calls over many parallel ones.",
78
+ ].join(" ");
79
+
80
+ case "strict":
81
+ return [
82
+ `ADVISOR MODE (strict — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
83
+ "Use HAIKU for ALL subagents without exception (Agent tool, model: haiku).",
84
+ "Minimize subagent spawning — do simple tasks directly in main context.",
85
+ "Only spawn subagents for genuinely independent parallel work.",
86
+ "Never spawn opus subagents.",
87
+ ].join(" ");
88
+
89
+ case "critical":
90
+ return [
91
+ `ADVISOR MODE (critical — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
92
+ "MINIMIZE ALL TOKEN USAGE. Do NOT spawn subagents unless absolutely essential.",
93
+ "Work directly in main context. Keep responses concise.",
94
+ "Use haiku model if you must spawn a subagent.",
95
+ "Skip background research, parallel exploration, and spotchecks.",
96
+ "The user is near their weekly limit — every token counts.",
97
+ ].join(" ");
98
+
99
+ default:
100
+ return "";
101
+ }
102
+ }
103
+
37
104
  function main() {
105
+ const parts: string[] = [];
106
+
38
107
  const rules = getWhisperRules();
39
- if (!rules) return;
108
+ if (rules) parts.push(rules);
109
+
110
+ const advisor = getAdvisorGuidance();
111
+ if (advisor) parts.push(advisor);
112
+
113
+ if (parts.length === 0) return;
40
114
 
41
- // Output as system-reminder — Claude Code injects this into the conversation
42
- console.log(`<system-reminder>
43
- ${rules}
44
- </system-reminder>`);
115
+ console.log(`<system-reminder>\n${parts.join("\n")}\n</system-reminder>`);
45
116
  }
46
117
 
47
118
  main();
@@ -427,6 +427,12 @@ if [ -f "$usage_cache" ]; then
427
427
  pace_dot="${pace_color}${spend_per_day}%% / ${budget_per_day}%%${RESET}"
428
428
  fi
429
429
 
430
+ # Write weekly budget to advisor-mode.json for the whisper hook
431
+ _advisor_file="${HOME}/.claude/advisor-mode.json"
432
+ if [ -n "$seven_day_int" ] 2>/dev/null; then
433
+ printf '{"weeklyBudgetPercent":%d,"mode":"auto"}\n' "$seven_day_int" > "$_advisor_file" 2>/dev/null
434
+ fi
435
+
430
436
  # Build usage suffix: 5h: 8% → 00:59 │ 1d: ● 29% / 36% │ 7d: 29% → Fr. 08:00
431
437
  five_label="5h: ${five_hour_int}%%"
432
438
  [ -n "$five_reset_fmt" ] && five_label="${five_label} → ${five_reset_fmt}"