@luquimbo/bi-superpowers 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +52 -36
  6. package/CHANGELOG.md +295 -0
  7. package/README.md +75 -26
  8. package/bin/build-plugin.js +11 -4
  9. package/bin/cli.js +113 -16
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +7 -3
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +389 -0
  16. package/bin/lib/generators/claude-plugin.js +144 -6
  17. package/bin/lib/generators/shared.js +29 -33
  18. package/bin/lib/mcp-config.js +168 -12
  19. package/bin/lib/skills.js +115 -27
  20. package/bin/postinstall.js +4 -2
  21. package/bin/utils/mcp-detect.js +2 -2
  22. package/commands/bi-start.md +218 -0
  23. package/commands/pbi-connect.md +43 -65
  24. package/commands/project-kickoff.md +393 -673
  25. package/commands/report-design.md +403 -0
  26. package/desktop-extension/manifest.json +3 -3
  27. package/package.json +7 -5
  28. package/skills/bi-start/SKILL.md +220 -0
  29. package/skills/bi-start/scripts/update-check.js +389 -0
  30. package/skills/pbi-connect/SKILL.md +45 -67
  31. package/skills/pbi-connect/scripts/update-check.js +389 -0
  32. package/skills/project-kickoff/SKILL.md +395 -675
  33. package/skills/project-kickoff/scripts/update-check.js +389 -0
  34. package/skills/report-design/SKILL.md +405 -0
  35. package/skills/report-design/references/cli-commands.md +184 -0
  36. package/skills/report-design/references/cli-setup.md +101 -0
  37. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  38. package/skills/report-design/references/layouts/finance.md +65 -0
  39. package/skills/report-design/references/layouts/generic.md +46 -0
  40. package/skills/report-design/references/layouts/hr.md +48 -0
  41. package/skills/report-design/references/layouts/marketing.md +45 -0
  42. package/skills/report-design/references/layouts/operations.md +44 -0
  43. package/skills/report-design/references/layouts/sales.md +50 -0
  44. package/skills/report-design/references/native-visuals.md +341 -0
  45. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  46. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  47. package/skills/report-design/references/slicer.md +89 -0
  48. package/skills/report-design/references/textbox.md +101 -0
  49. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  50. package/skills/report-design/references/troubleshooting.md +135 -0
  51. package/skills/report-design/references/visual-types.md +78 -0
  52. package/skills/report-design/scripts/apply-theme.js +243 -0
  53. package/skills/report-design/scripts/create-visual.js +878 -0
  54. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  55. package/skills/report-design/scripts/update-check.js +389 -0
  56. package/skills/report-design/scripts/validate-pbir.js +322 -0
  57. package/src/content/base.md +12 -68
  58. package/src/content/mcp-requirements.json +0 -25
  59. package/src/content/routing.md +19 -74
  60. package/src/content/skills/bi-start.md +191 -0
  61. package/src/content/skills/pbi-connect.md +22 -65
  62. package/src/content/skills/project-kickoff.md +372 -673
  63. package/src/content/skills/report-design/SKILL.md +376 -0
  64. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  65. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  66. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  67. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  68. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  69. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  70. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  71. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  72. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  73. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  74. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  75. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  76. package/src/content/skills/report-design/references/slicer.md +89 -0
  77. package/src/content/skills/report-design/references/textbox.md +101 -0
  78. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  79. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  80. package/src/content/skills/report-design/references/visual-types.md +78 -0
  81. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  82. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  83. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  84. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  85. package/bin/commands/install.test.js +0 -289
  86. package/bin/commands/lint.test.js +0 -103
  87. package/bin/lib/generators/claude-plugin.test.js +0 -111
  88. package/bin/lib/mcp-config.test.js +0 -310
  89. package/bin/lib/microsoft-mcp.test.js +0 -115
  90. package/bin/utils/mcp-detect.test.js +0 -81
  91. package/bin/utils/tui.test.js +0 -127
@@ -0,0 +1,220 @@
1
+ ---
2
+ name: "bi-start"
3
+ description: "Use when the user asks about BI Start Skill, especially phrases like \"bi-start\", \"bi start\", \"/bi-start\", \"empezar\", \"comenzar\", \"arranco\"."
4
+ version: "4.1.0"
5
+ ---
6
+
7
+ <!-- Generated by BI Agent Superpowers. Edit src/content/skills/bi-start.md instead. -->
8
+
9
+ <!-- bi-superpowers update-check preamble (auto-generated, do not edit) -->
10
+ ## ⬆ Update check
11
+
12
+ Before handling the user's request below, run this command **once per conversation** and interpret the output:
13
+
14
+ ```bash
15
+ node "{skillBundleDir}/scripts/update-check.js" --silent-if-uptodate --silent-if-snoozed
16
+ ```
17
+
18
+ - Empty output or `UPTODATE` — proceed with the skill silently. No message.
19
+ - `UPDATE_AVAILABLE <installed> <latest>` — tell the user exactly once this conversation, before diving into the skill:
20
+ > "Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). Actualizá con `super upgrade` (o `/plugin update bi-superpowers` en Claude Code) cuando te venga bien."
21
+
22
+ Then continue with the skill below.
23
+ - `SNOOZED <iso>` — proceed silently.
24
+
25
+ If the command fails (missing binary, permissions, offline), ignore the error and proceed with the skill. The update check must never block the user's request.
26
+
27
+ ---
28
+ <!-- /bi-superpowers update-check preamble -->
29
+
30
+ # BI Start Skill
31
+
32
+ ## Trigger
33
+ Activate this skill when the user mentions:
34
+ - "bi-start", "bi start", "/bi-start"
35
+ - "empezar", "comenzar", "arranco", "arrancar"
36
+ - "get started", "start session", "new session"
37
+ - "qué puedo hacer", "what can I do", "qué hago", "what's here"
38
+ - "ayuda bi", "bi help", "help me start", "orientame"
39
+ - "mostrame los skills", "show me what you can do", "lista los comandos"
40
+
41
+ Also activate:
42
+ - At the start of any fresh conversation when the user hasn't yet picked a skill.
43
+ - When the user seems lost about which skill to invoke (e.g. says "no sé qué usar").
44
+
45
+ ## Identity
46
+ You are **BI Session Orchestrator**. Your job is to welcome the user at the start of a chat session, show them the available skills, check for updates, and — if Power BI Desktop is involved — offer to connect right away. You are **not** a project analyst (that's `/project-kickoff`), **not** a connection specialist (that's `/pbi-connect`), and **not** a report author (that's `/report-design`). You are the front desk.
47
+
48
+ You are the session-opener, **not** the project-opener. If the user's intent is clearly "I'm creating a brand-new BI project from scratch", delegate to `/project-kickoff`. Otherwise, this skill is the right home for general-purpose entry, discovery, environment checks, and pointing the user at the right specialist.
49
+
50
+ ## MANDATORY RULES
51
+
52
+ 1. **ONE QUESTION AT A TIME.** Only ask when you need a decision from the user (update yes/no, connect yes/no). Never stack multiple questions.
53
+ 2. **INFORMATIVE MENU — DON'T FORCE A CHOICE.** Show the 3 skills as a table with 1-line descriptions. The user decides organically either by invoking `/<skill>` or by describing what they want. Do NOT say "pick 1, 2, or 3" — that's quiz-style and annoying for returning users.
54
+ 3. **PROACTIVE ON UPDATE + CONNECT.** When you detect (a) an available update or (b) Power BI Desktop running without a configured MCP, **ask once** and then dispatch the action yourself. Don't force the user to remember the exact command.
55
+ 4. **SAFE DEFAULTS ON SAY-NOTHING.** If the user greets you and then goes silent, show the menu and stop. Don't auto-dispatch anything without an explicit "sí" / "yes".
56
+ 5. **OS-AWARE, NOT OS-GATING.** Works on any OS. Mark Windows-only skills clearly. On macOS/Linux, `/project-kickoff` still has partial value (writes `AGENTS.md` and stops); mention that honestly instead of refusing.
57
+ 6. **DELEGATE CLEANLY.** When dispatching to another skill, say "dispatching /X" so the user sees the hand-off, then stop being the orchestrator for this turn. If they come back with "estoy en X, ahora qué", you can re-orient.
58
+
59
+ ---
60
+
61
+ ## PHASE 0: Update check (proactive)
62
+
63
+ Run the update check at the very start:
64
+
65
+ ```bash
66
+ node "{skillBundleDir}/scripts/update-check.js" --silent-if-snoozed
67
+ ```
68
+
69
+ Interpret the single-line output:
70
+
71
+ - **`UPTODATE`** — silent, continue to PHASE 1.
72
+ - **`UPDATE_AVAILABLE <installed> <latest>`** — say:
73
+ > _"Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). ¿Lo actualizo ahora? (`sí` / `no`)"_
74
+
75
+ On `sí` / `yes`:
76
+ - If the user installed via Claude Code marketplace, dispatch: _"Corré `/plugin update bi-superpowers` en Claude Code — eso hace el update nativo sin pasar por npm."_ (you can't execute `/plugin` yourself).
77
+ - Otherwise, run `super upgrade` in the shell:
78
+ ```bash
79
+ super upgrade
80
+ ```
81
+ After it finishes, remind: _"Corré `super install --yes` cuando puedas para propagar las skills nuevas a tus agentes."_
82
+
83
+ On `no` — respect it, continue to PHASE 1 silently. The update-state.json already tracks the user's snooze per `update-check.js` semantics.
84
+
85
+ - **`SNOOZED <iso>`** — silent, continue.
86
+
87
+ - **Command failed / no output** — silent, continue. The update check must never block this skill.
88
+
89
+ ---
90
+
91
+ ## PHASE 1: Environment snapshot
92
+
93
+ Do these detections in order:
94
+
95
+ 1. **OS**: `process.platform` via a short shell command:
96
+ ```bash
97
+ node -e "console.log(process.platform)"
98
+ ```
99
+ - `win32` → full workflow available.
100
+ - `darwin` / `linux` → limited (report-design + local Modeling MCP don't work).
101
+
102
+ 2. **Project context** (CWD-based):
103
+ - `./pbip-files/*.pbip` present? → `$hasPbip = true`.
104
+ - `./AGENTS.md` present? → `$hasAgentsMd = true`.
105
+
106
+ 3. **Power BI Desktop running** (Windows only):
107
+ ```bash
108
+ tasklist /FI "IMAGENAME eq PBIDesktop.exe" 2>&1 | findstr /I "PBIDesktop.exe"
109
+ ```
110
+ (or equivalent). `$pbiDesktopRunning = true` if present.
111
+
112
+ 4. **MCP configured**: look for `.mcp.json` in project root OR `powerbi-modeling-mcp` entry in the agent's config file (`~/.claude.json`, `~/.codex/config.toml`, etc). Keep the check shallow — no need to deep-diff.
113
+
114
+ ### Emit the context in 3-4 lines max
115
+
116
+ Example on Windows, full setup:
117
+
118
+ ```
119
+ 📍 Windows · .pbip detectado en ./pbip-files/MyProj.pbip (con AGENTS.md)
120
+ PBI Desktop: corriendo · MCP: configurado
121
+ ```
122
+
123
+ Example on macOS:
124
+
125
+ ```
126
+ 📍 macOS · sin .pbip en CWD
127
+ Power BI Desktop no corre en macOS — los skills que requieren Desktop quedan limitados.
128
+ ```
129
+
130
+ Keep it 3-4 lines. The point is situational awareness, not a status page.
131
+
132
+ ---
133
+
134
+ ## PHASE 2: Skills menu (informativo)
135
+
136
+ Show the 3 skills as a table. Plain, no prompt. Do NOT number them or ask "which one?".
137
+
138
+ ```
139
+ Skills disponibles:
140
+
141
+ /project-kickoff Arrancar un proyecto BI nuevo (crea AGENTS.md, plantea modelo) · Win / Mac / Linux (parcial fuera de Win)
142
+ /pbi-connect Conectar el agente a Power BI Desktop vía MCP · Windows
143
+ /report-design Generar las páginas PBIR desde el modelo · Windows + PBI Desktop
144
+
145
+ Invocá el que necesites con /<nombre>, o decime en lenguaje natural lo que querés
146
+ hacer (ej: "crear reportes", "conectar Power BI", "arranco proyecto nuevo") y te ruteo.
147
+ ```
148
+
149
+ If the user is on macOS/Linux and says they want `/report-design` or `/pbi-connect`, remind them once: _"Ese skill requiere Windows + PBI Desktop. Para este proyecto, podés arrancar con `/project-kickoff` — escribe `AGENTS.md` con el scope y cuando tengas acceso a una máquina Windows retomás los otros dos."_
150
+
151
+ ---
152
+
153
+ ## PHASE 3: Proactive connect (if applicable)
154
+
155
+ Skip this phase entirely if `$pbiDesktopRunning === false && $hasPbip === false`.
156
+
157
+ Three cases:
158
+
159
+ **Case A — PBI Desktop running + MCP configured**:
160
+ Don't ask, just confirm:
161
+ > _"✓ Power BI Desktop está abierto y el MCP está conectado. Listo para cualquier skill que necesite el modelo."_
162
+
163
+ Continue to PHASE 4.
164
+
165
+ **Case B — PBI Desktop running + MCP NOT configured** (Windows only):
166
+ Offer once:
167
+ > _"Power BI Desktop está abierto pero todavía no conectaste el agente al MCP. ¿Corro `/pbi-connect`? (`sí` / `no`)"_
168
+
169
+ - `sí` → dispatch `/pbi-connect` cleanly. Say "dispatching /pbi-connect" and stop being the orchestrator for this turn.
170
+ - `no` → continue to PHASE 4 silently.
171
+
172
+ **Case C — PBI Desktop NOT running + `.pbip` exists in CWD** (Windows only):
173
+ Offer once:
174
+ > _"No veo Power BI Desktop abierto. Para conectar el agente al modelo necesitás abrir el .pbip. ¿Lo abro yo y corro `/pbi-connect`? (`sí` / `no`)"_
175
+
176
+ - `sí`:
177
+ 1. Launch Desktop with the project's .pbip (use the standalone path per `/report-design` PHASE 5 launch pattern — see `references/pbi-desktop-installation.md` in `/report-design`):
178
+ ```bash
179
+ powershell -Command "Start-Process -FilePath 'C:\Program Files\Microsoft Power BI Desktop\bin\PBIDesktop.exe' -ArgumentList '\"<absolute-path-to.pbip>\"'"
180
+ ```
181
+ 2. Wait ~15-20 seconds for Desktop to finish loading.
182
+ 3. Dispatch `/pbi-connect`.
183
+
184
+ - `no` → continue to PHASE 4.
185
+
186
+ On macOS/Linux, skip Case B and Case C — mention once:
187
+ > _"PBI Desktop no corre fuera de Windows. El Modeling MCP queda solo disponible en una máquina Windows. `/project-kickoff` sí funciona parcialmente acá (escribe `AGENTS.md` y para)."_
188
+
189
+ ---
190
+
191
+ ## PHASE 4: Stand by
192
+
193
+ If you got here without dispatching, close with:
194
+
195
+ > _"Listo — invocá el skill que necesites, o pedime ayuda específica sobre cualquiera de los 3. Si abrís una sesión nueva mañana, `/bi-start` te orienta de nuevo."_
196
+
197
+ Stop. Don't hover. The user will tell you what they want next.
198
+
199
+ ---
200
+
201
+ ## What this skill does NOT do
202
+
203
+ - **Project analysis or setup**: that's `/project-kickoff`. If the user says "analizar mi proyecto", "armar el modelo base", "arrancar uno nuevo desde cero", delegate.
204
+ - **MCP wiring details**: that's `/pbi-connect`. bi-start just offers to dispatch it; the actual configuration work is in that skill.
205
+ - **Report authoring**: that's `/report-design`. Same pattern.
206
+ - **Running the update**: bi-start offers + dispatches `super upgrade`; the actual npm install + subsequent `super install --yes` chain is owned by `/bin/cli.js`.
207
+
208
+ ## Related Skills
209
+
210
+ - `/project-kickoff` — when it's a brand-new project that needs `AGENTS.md` + model scaffolding.
211
+ - `/pbi-connect` — when you need the agent talking to PBI Desktop via MCP.
212
+ - `/report-design` — when you're generating report pages via the bundled Node scripts.
213
+
214
+ ## Bundle contents
215
+
216
+ - `scripts/update-check.js` — the update-check helper invoked in PHASE 0. Same helper that every skill's preamble uses.
217
+
218
+ ---
219
+
220
+ _Session orchestrator — welcome, update, route._
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * update-check — cross-agent version-check helper for bi-superpowers.
4
+ *
5
+ * The SKILL.md preamble (see lib/generators/claude-plugin.js) invokes this
6
+ * script at the start of every skill so the agent can surface an update
7
+ * notice to the user when a newer version is on npm — without hitting the
8
+ * network on every invocation. Cache TTL is 24h; repeated calls inside
9
+ * that window are served from `~/.bi-superpowers/update-state.json`.
10
+ *
11
+ * Output (stdout, one line):
12
+ * UPTODATE when installed >= latest
13
+ * UPDATE_AVAILABLE <installed> <latest> when installed < latest
14
+ * SNOOZED <iso> when user deferred the notice
15
+ *
16
+ * Flags:
17
+ * --force bypass cache (re-fetch npm, ignore snooze TTL)
18
+ * --silent-if-uptodate suppress UPTODATE line (used by the preamble)
19
+ * --silent-if-snoozed suppress SNOOZED line (used by the preamble)
20
+ * --json emit JSON instead of text
21
+ * --snooze 24h|48h|7d|clear set (or clear) the snooze state and exit
22
+ * --reset delete the state file and exit (used post-upgrade)
23
+ * --state-dir <path> override ~/.bi-superpowers/ (for tests)
24
+ * --package-name <name> override the package name (for tests)
25
+ * -h, --help show this help
26
+ *
27
+ * Exit code is always 0 when the script itself ran — errors during the
28
+ * network fetch degrade to "no output" so the caller never blocks. A
29
+ * non-zero exit means a user error (bad flags).
30
+ *
31
+ * Pure helpers (compareVersions, isCacheFresh, isSnoozed,
32
+ * computeNextSnoozeUntil, readState, writeState, fetchLatestVersion) are
33
+ * exported so unit tests can exercise them without spawning child
34
+ * processes or hitting the network.
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('fs');
40
+ const os = require('os');
41
+ const path = require('path');
42
+ const https = require('https');
43
+
44
+ const PACKAGE_NAME = '@luquimbo/bi-superpowers';
45
+ const CACHE_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours
46
+ const HTTPS_TIMEOUT_MS = 5000;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Argument parsing
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function parseArgs(argv) {
53
+ const out = {
54
+ force: false,
55
+ silentIfUptodate: false,
56
+ silentIfSnoozed: false,
57
+ json: false,
58
+ snooze: null,
59
+ reset: false,
60
+ help: false,
61
+ stateDir: null,
62
+ packageName: null,
63
+ };
64
+ for (let i = 0; i < argv.length; i += 1) {
65
+ const a = argv[i];
66
+ if (a === '--force') out.force = true;
67
+ else if (a === '--silent-if-uptodate') out.silentIfUptodate = true;
68
+ else if (a === '--silent-if-snoozed') out.silentIfSnoozed = true;
69
+ else if (a === '--json') out.json = true;
70
+ else if (a === '--snooze') out.snooze = argv[++i];
71
+ else if (a === '--reset') out.reset = true;
72
+ else if (a === '--state-dir') out.stateDir = argv[++i];
73
+ else if (a === '--package-name') out.packageName = argv[++i];
74
+ else if (a === '-h' || a === '--help') out.help = true;
75
+ else {
76
+ process.stderr.write(`update-check: unknown flag: ${a}\n`);
77
+ process.exit(1);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function help() {
84
+ process.stdout.write(
85
+ [
86
+ 'Usage: update-check [options]',
87
+ '',
88
+ 'Prints one of: UPTODATE, UPDATE_AVAILABLE <installed> <latest>, SNOOZED <iso>.',
89
+ '',
90
+ 'Options:',
91
+ ' --force Bypass cache and snooze TTL',
92
+ ' --silent-if-uptodate Skip the UPTODATE line',
93
+ ' --silent-if-snoozed Skip the SNOOZED line',
94
+ ' --json Emit JSON',
95
+ ' --snooze <dur> Set snooze state (24h|48h|7d) or "clear" to reset snooze',
96
+ ' --reset Delete the state file (used after a successful upgrade)',
97
+ ' --state-dir <path> Override ~/.bi-superpowers/ (tests)',
98
+ ' --package-name <name> Override the package name (tests)',
99
+ ' -h, --help Show this help',
100
+ '',
101
+ ].join('\n')
102
+ );
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Version comparison (semver-ish: MAJOR.MINOR.PATCH with optional -prerelease)
107
+ // No deps; handles the shapes @luquimbo/bi-superpowers uses today.
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Compare two semver strings.
112
+ * Returns -1 if a < b, 0 if equal, 1 if a > b.
113
+ * Pre-release tags (`-alpha.1`) sort before the release per semver.
114
+ */
115
+ function compareVersions(a, b) {
116
+ const parse = (v) => {
117
+ const [main, pre] = String(v).split('-');
118
+ const parts = main.split('.').map((n) => parseInt(n, 10) || 0);
119
+ while (parts.length < 3) parts.push(0);
120
+ return { parts, pre: pre || null };
121
+ };
122
+ const va = parse(a);
123
+ const vb = parse(b);
124
+ for (let i = 0; i < 3; i += 1) {
125
+ if (va.parts[i] !== vb.parts[i]) return va.parts[i] < vb.parts[i] ? -1 : 1;
126
+ }
127
+ // Main equal — pre-release < release.
128
+ if (va.pre && !vb.pre) return -1;
129
+ if (!va.pre && vb.pre) return 1;
130
+ if (va.pre && vb.pre) {
131
+ if (va.pre < vb.pre) return -1;
132
+ if (va.pre > vb.pre) return 1;
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Cache + snooze state
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function defaultStateDir() {
142
+ return path.join(os.homedir(), '.bi-superpowers');
143
+ }
144
+
145
+ function stateFilePath(stateDir) {
146
+ return path.join(stateDir, 'update-state.json');
147
+ }
148
+
149
+ function readState(stateDir) {
150
+ const filePath = stateFilePath(stateDir);
151
+ if (!fs.existsSync(filePath)) return null;
152
+ try {
153
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
154
+ } catch (_) {
155
+ // Malformed → treat as no cache.
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function writeState(stateDir, state) {
161
+ fs.mkdirSync(stateDir, { recursive: true });
162
+ fs.writeFileSync(stateFilePath(stateDir), JSON.stringify(state, null, 2) + '\n');
163
+ }
164
+
165
+ function resetState(stateDir) {
166
+ const filePath = stateFilePath(stateDir);
167
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
168
+ }
169
+
170
+ function isCacheFresh(state, now, ttlMs) {
171
+ if (!state || !state.checkedAt) return false;
172
+ const checkedAt = Date.parse(state.checkedAt);
173
+ if (!Number.isFinite(checkedAt)) return false;
174
+ return now - checkedAt < ttlMs;
175
+ }
176
+
177
+ function isSnoozed(state, now) {
178
+ if (!state || !state.snoozeUntil) return false;
179
+ const until = Date.parse(state.snoozeUntil);
180
+ if (!Number.isFinite(until)) return false;
181
+ return until > now;
182
+ }
183
+
184
+ // Snooze escalation: 24h → 48h → 7d (capped).
185
+ function computeNextSnoozeUntil(currentLevel, now) {
186
+ const levels = [
187
+ 1000 * 60 * 60 * 24, // 24h
188
+ 1000 * 60 * 60 * 48, // 48h
189
+ 1000 * 60 * 60 * 24 * 7, // 7d
190
+ ];
191
+ const idx = Math.min(Math.max(currentLevel, 0), levels.length - 1);
192
+ return new Date(now + levels[idx]).toISOString();
193
+ }
194
+
195
+ function parseSnoozeArg(arg, now, currentLevel) {
196
+ if (arg === 'clear') return { clear: true };
197
+ if (arg === '24h') return { until: new Date(now + 1000 * 60 * 60 * 24).toISOString(), level: 0 };
198
+ if (arg === '48h') return { until: new Date(now + 1000 * 60 * 60 * 48).toISOString(), level: 1 };
199
+ if (arg === '7d')
200
+ return { until: new Date(now + 1000 * 60 * 60 * 24 * 7).toISOString(), level: 2 };
201
+ if (arg === 'auto')
202
+ return {
203
+ until: computeNextSnoozeUntil(currentLevel, now),
204
+ level: Math.min(currentLevel + 1, 2),
205
+ };
206
+ throw new Error(`invalid --snooze value: ${arg}. Expected 24h|48h|7d|auto|clear.`);
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // npm registry fetch
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /**
214
+ * Fetch the latest published version of a package from the npm registry.
215
+ * Never rejects with a network error — resolves null on timeout / failure
216
+ * so callers always degrade gracefully.
217
+ *
218
+ * @param {string} packageName - e.g. "@luquimbo/bi-superpowers"
219
+ * @returns {Promise<string|null>}
220
+ */
221
+ function fetchLatestVersion(packageName) {
222
+ return new Promise((resolve) => {
223
+ const encoded = packageName.replace('/', '%2F');
224
+ const url = `https://registry.npmjs.org/${encoded}/latest`;
225
+
226
+ const req = https.get(
227
+ url,
228
+ { headers: { Accept: 'application/vnd.npm.install-v1+json' } },
229
+ (res) => {
230
+ if (res.statusCode !== 200) {
231
+ res.resume();
232
+ resolve(null);
233
+ return;
234
+ }
235
+ let body = '';
236
+ res.setEncoding('utf8');
237
+ res.on('data', (chunk) => (body += chunk));
238
+ res.on('end', () => {
239
+ try {
240
+ const json = JSON.parse(body);
241
+ resolve(typeof json.version === 'string' ? json.version : null);
242
+ } catch (_) {
243
+ resolve(null);
244
+ }
245
+ });
246
+ }
247
+ );
248
+ req.on('error', () => resolve(null));
249
+ req.setTimeout(HTTPS_TIMEOUT_MS, () => {
250
+ req.destroy();
251
+ resolve(null);
252
+ });
253
+ });
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Installed version — read from our own package.json
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function readInstalledVersion() {
261
+ try {
262
+ return require(path.join(__dirname, '..', '..', 'package.json')).version;
263
+ } catch (_) {
264
+ return null;
265
+ }
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Emit helpers
270
+ // ---------------------------------------------------------------------------
271
+
272
+ function emit(args, kind, payload) {
273
+ if (args.json) {
274
+ process.stdout.write(JSON.stringify({ status: kind, ...payload }) + '\n');
275
+ return;
276
+ }
277
+ if (kind === 'UPTODATE' && args.silentIfUptodate) return;
278
+ if (kind === 'SNOOZED' && args.silentIfSnoozed) return;
279
+
280
+ if (kind === 'UPTODATE') process.stdout.write('UPTODATE\n');
281
+ else if (kind === 'UPDATE_AVAILABLE')
282
+ process.stdout.write(`UPDATE_AVAILABLE ${payload.installed} ${payload.latest}\n`);
283
+ else if (kind === 'SNOOZED') process.stdout.write(`SNOOZED ${payload.until}\n`);
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // main
288
+ // ---------------------------------------------------------------------------
289
+
290
+ async function main() {
291
+ const args = parseArgs(process.argv.slice(2));
292
+ if (args.help) {
293
+ help();
294
+ return;
295
+ }
296
+
297
+ const stateDir = args.stateDir || defaultStateDir();
298
+ const packageName = args.packageName || PACKAGE_NAME;
299
+
300
+ if (args.reset) {
301
+ resetState(stateDir);
302
+ return;
303
+ }
304
+
305
+ if (args.snooze) {
306
+ const now = Date.now();
307
+ const prior = readState(stateDir) || {};
308
+ const parsed = parseSnoozeArg(args.snooze, now, prior.snoozeLevel || 0);
309
+ if (parsed.clear) {
310
+ writeState(stateDir, { ...prior, snoozeUntil: null, snoozeLevel: 0 });
311
+ } else {
312
+ writeState(stateDir, {
313
+ ...prior,
314
+ snoozeUntil: parsed.until,
315
+ snoozeLevel: parsed.level,
316
+ });
317
+ }
318
+ return;
319
+ }
320
+
321
+ const installed = readInstalledVersion();
322
+ if (!installed) {
323
+ // Installed version undetermined — nothing useful to report.
324
+ return;
325
+ }
326
+
327
+ const now = Date.now();
328
+ let state = readState(stateDir);
329
+
330
+ // Snooze short-circuits everything except --force.
331
+ if (!args.force && isSnoozed(state, now)) {
332
+ emit(args, 'SNOOZED', { until: state.snoozeUntil });
333
+ return;
334
+ }
335
+
336
+ // Use cached `latest` when the cache is fresh (unless --force).
337
+ let latest = state && state.latest;
338
+ if (args.force || !isCacheFresh(state, now, CACHE_TTL_MS)) {
339
+ const fetched = await fetchLatestVersion(packageName);
340
+ if (fetched) {
341
+ latest = fetched;
342
+ const nextState = {
343
+ installed,
344
+ latest,
345
+ checkedAt: new Date(now).toISOString(),
346
+ snoozeUntil: (state && state.snoozeUntil) || null,
347
+ snoozeLevel: (state && state.snoozeLevel) || 0,
348
+ };
349
+ writeState(stateDir, nextState);
350
+ state = nextState;
351
+ }
352
+ // If fetched is null (network fail), we keep using the previous cache
353
+ // — or emit nothing if there's no cache at all.
354
+ }
355
+
356
+ if (!latest) {
357
+ // No cached value and no fetch — nothing to say.
358
+ return;
359
+ }
360
+
361
+ if (compareVersions(installed, latest) < 0) {
362
+ emit(args, 'UPDATE_AVAILABLE', { installed, latest });
363
+ } else {
364
+ emit(args, 'UPTODATE', { installed, latest });
365
+ }
366
+ }
367
+
368
+ module.exports = {
369
+ parseArgs,
370
+ compareVersions,
371
+ isCacheFresh,
372
+ isSnoozed,
373
+ computeNextSnoozeUntil,
374
+ parseSnoozeArg,
375
+ readState,
376
+ writeState,
377
+ resetState,
378
+ fetchLatestVersion,
379
+ CACHE_TTL_MS,
380
+ PACKAGE_NAME,
381
+ };
382
+
383
+ if (require.main === module) {
384
+ main().catch((err) => {
385
+ // Never throw out of the CLI — the preamble must not break skill invocation.
386
+ process.stderr.write(`update-check: ${err.message}\n`);
387
+ process.exit(0);
388
+ });
389
+ }