maxsimcli 5.0.7 → 5.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/README.md +101 -99
  2. package/dist/assets/CHANGELOG.md +7 -0
  3. package/dist/assets/hooks/maxsim-capture-learnings.cjs +128 -0
  4. package/dist/assets/hooks/maxsim-capture-learnings.cjs.map +1 -0
  5. package/dist/assets/hooks/maxsim-check-update.cjs +126 -88
  6. package/dist/assets/hooks/maxsim-check-update.cjs.map +1 -1
  7. package/dist/assets/hooks/maxsim-notification-sound.cjs +87 -43
  8. package/dist/assets/hooks/maxsim-notification-sound.cjs.map +1 -1
  9. package/dist/assets/hooks/maxsim-statusline.cjs +45 -171
  10. package/dist/assets/hooks/maxsim-statusline.cjs.map +1 -1
  11. package/dist/assets/hooks/maxsim-stop-sound.cjs +86 -43
  12. package/dist/assets/hooks/maxsim-stop-sound.cjs.map +1 -1
  13. package/dist/assets/hooks/maxsim-sync-reminder.cjs +72 -21
  14. package/dist/assets/hooks/maxsim-sync-reminder.cjs.map +1 -1
  15. package/dist/assets/templates/agents/AGENTS.md +62 -51
  16. package/dist/assets/templates/agents/executor.md +44 -59
  17. package/dist/assets/templates/agents/planner.md +36 -31
  18. package/dist/assets/templates/agents/researcher.md +35 -43
  19. package/dist/assets/templates/agents/verifier.md +29 -31
  20. package/dist/assets/templates/commands/maxsim/debug.md +20 -154
  21. package/dist/assets/templates/commands/maxsim/execute.md +19 -33
  22. package/dist/assets/templates/commands/maxsim/go.md +21 -20
  23. package/dist/assets/templates/commands/maxsim/help.md +5 -14
  24. package/dist/assets/templates/commands/maxsim/init.md +18 -40
  25. package/dist/assets/templates/commands/maxsim/plan.md +22 -37
  26. package/dist/assets/templates/commands/maxsim/progress.md +15 -16
  27. package/dist/assets/templates/commands/maxsim/quick.md +18 -29
  28. package/dist/assets/templates/commands/maxsim/settings.md +18 -26
  29. package/dist/assets/templates/references/continuation-format.md +2 -4
  30. package/dist/assets/templates/references/model-profiles.md +2 -2
  31. package/dist/assets/templates/references/planning-config.md +10 -11
  32. package/dist/assets/templates/references/self-improvement.md +120 -0
  33. package/dist/assets/templates/rules/conventions.md +1 -1
  34. package/dist/assets/templates/rules/verification-protocol.md +1 -1
  35. package/dist/assets/templates/skills/brainstorming/SKILL.md +35 -26
  36. package/dist/assets/templates/skills/code-review/SKILL.md +78 -55
  37. package/dist/assets/templates/skills/commit-conventions/SKILL.md +70 -36
  38. package/dist/assets/templates/skills/github-operations/SKILL.md +142 -0
  39. package/dist/assets/templates/skills/handoff-contract/SKILL.md +62 -28
  40. package/dist/assets/templates/skills/maxsim-batch/SKILL.md +68 -42
  41. package/dist/assets/templates/skills/maxsim-simplify/SKILL.md +65 -40
  42. package/dist/assets/templates/skills/project-memory/SKILL.md +121 -0
  43. package/dist/assets/templates/skills/research/SKILL.md +126 -0
  44. package/dist/assets/templates/skills/roadmap-writing/SKILL.md +71 -68
  45. package/dist/assets/templates/skills/systematic-debugging/SKILL.md +37 -25
  46. package/dist/assets/templates/skills/tdd/SKILL.md +36 -39
  47. package/dist/assets/templates/skills/using-maxsim/SKILL.md +69 -55
  48. package/dist/assets/templates/skills/verification/SKILL.md +167 -0
  49. package/dist/assets/templates/workflows/batch.md +249 -268
  50. package/dist/assets/templates/workflows/diagnose-issues.md +225 -151
  51. package/dist/assets/templates/workflows/execute-plan.md +191 -981
  52. package/dist/assets/templates/workflows/execute.md +350 -309
  53. package/dist/assets/templates/workflows/go.md +119 -138
  54. package/dist/assets/templates/workflows/health.md +71 -114
  55. package/dist/assets/templates/workflows/help.md +85 -147
  56. package/dist/assets/templates/workflows/init-existing.md +180 -1373
  57. package/dist/assets/templates/workflows/init.md +53 -165
  58. package/dist/assets/templates/workflows/new-milestone.md +91 -334
  59. package/dist/assets/templates/workflows/new-project.md +165 -1384
  60. package/dist/assets/templates/workflows/plan-create.md +182 -73
  61. package/dist/assets/templates/workflows/plan-discuss.md +89 -82
  62. package/dist/assets/templates/workflows/plan-research.md +191 -85
  63. package/dist/assets/templates/workflows/plan.md +122 -58
  64. package/dist/assets/templates/workflows/progress.md +76 -310
  65. package/dist/assets/templates/workflows/quick.md +70 -495
  66. package/dist/assets/templates/workflows/sdd.md +231 -221
  67. package/dist/assets/templates/workflows/settings.md +90 -120
  68. package/dist/assets/templates/workflows/verify-phase.md +296 -258
  69. package/dist/cli.cjs +17 -23465
  70. package/dist/cli.cjs.map +1 -1
  71. package/dist/install.cjs +356 -8358
  72. package/dist/install.cjs.map +1 -1
  73. package/package.json +16 -22
  74. package/dist/assets/templates/skills/agent-system-map/SKILL.md +0 -92
  75. package/dist/assets/templates/skills/evidence-collection/SKILL.md +0 -87
  76. package/dist/assets/templates/skills/github-artifact-protocol/SKILL.md +0 -67
  77. package/dist/assets/templates/skills/github-tools-guide/SKILL.md +0 -89
  78. package/dist/assets/templates/skills/input-validation/SKILL.md +0 -51
  79. package/dist/assets/templates/skills/memory-management/SKILL.md +0 -75
  80. package/dist/assets/templates/skills/research-methodology/SKILL.md +0 -137
  81. package/dist/assets/templates/skills/sdd/SKILL.md +0 -91
  82. package/dist/assets/templates/skills/tool-priority-guide/SKILL.md +0 -80
  83. package/dist/assets/templates/skills/verification-before-completion/SKILL.md +0 -71
  84. package/dist/assets/templates/skills/verification-gates/SKILL.md +0 -169
  85. package/dist/assets/templates/workflows/discuss-phase.md +0 -683
  86. package/dist/assets/templates/workflows/research-phase.md +0 -73
  87. package/dist/assets/templates/workflows/verify-work.md +0 -572
  88. package/dist/core-D5zUr9cb.cjs +0 -4305
  89. package/dist/core-D5zUr9cb.cjs.map +0 -1
  90. package/dist/skills-CjFWZIGM.cjs +0 -6824
  91. package/dist/skills-CjFWZIGM.cjs.map +0 -1
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
1
  //#region \0rolldown/runtime.js
4
2
  var __create = Object.create;
5
3
  var __defProp = Object.defineProperty;
@@ -31,20 +29,17 @@ let node_fs = require("node:fs");
31
29
  node_fs = __toESM(node_fs);
32
30
  let node_path = require("node:path");
33
31
  node_path = __toESM(node_path);
34
- let node_child_process = require("node:child_process");
32
+ require("node:child_process");
33
+ require("node:os");
35
34
 
36
35
  //#region src/hooks/shared.ts
37
- /**
38
- * Shared utilities for MAXSIM hooks.
39
- */
40
- /**
41
- * Read all stdin as a string, then invoke callback with parsed JSON.
42
- * Used by statusline and sync-reminder hooks.
43
- */
36
+ /** Shared utilities for MAXSIM hooks. */
44
37
  function readStdinJson(callback) {
45
38
  let input = "";
46
39
  process.stdin.setEncoding("utf8");
47
- process.stdin.on("data", (chunk) => input += chunk);
40
+ process.stdin.on("data", (chunk) => {
41
+ input += chunk;
42
+ });
48
43
  process.stdin.on("end", () => {
49
44
  try {
50
45
  callback(JSON.parse(input));
@@ -53,178 +48,57 @@ function readStdinJson(callback) {
53
48
  }
54
49
  });
55
50
  }
56
- /** The '.claude' path segment -- template marker replaced during install. */
57
51
  const CLAUDE_DIR = ".claude";
58
52
 
59
53
  //#endregion
60
54
  //#region src/hooks/maxsim-statusline.ts
61
55
  /**
62
- * Claude Code Statusline - MAXSIM Edition
63
- * Shows: [update] model | P{N} {BoardColumn} | {milestone}: {pct}% | dirname
56
+ * statusLine hook display a brief MaxsimCLI status string in the terminal
57
+ * status bar.
58
+ *
59
+ * Output contract (Claude Code statusLine):
60
+ * Process must write a single line of plain text (or JSON with a "text" key)
61
+ * to stdout and exit 0. The output should be short (<80 chars).
62
+ *
63
+ * Performance target: <100 ms — local file reads only, no network calls.
64
64
  */
65
- const CACHE_TTL_SECONDS = 60;
66
- /**
67
- * Spawn a detached Node child process to refresh the progress cache in the background.
68
- * The child runs gh CLI commands to detect owner/repo, find the first open milestone,
69
- * compute progress, and find the current phase label.
70
- */
71
- function spawnBackgroundRefresh(cacheDir, cacheFile) {
72
- try {
73
- const script = `
74
- const { execSync } = require('child_process');
75
- const fs = require('fs');
76
- const path = require('path');
77
-
78
- try {
79
- // Detect owner/repo
80
- const nameWithOwner = execSync('gh repo view --json nameWithOwner -q .nameWithOwner', {
81
- encoding: 'utf8',
82
- timeout: 10000,
83
- stdio: ['pipe', 'pipe', 'pipe'],
84
- windowsHide: true,
85
- }).trim();
86
-
87
- if (!nameWithOwner || !nameWithOwner.includes('/')) {
88
- process.exit(0);
89
- }
90
-
91
- const [owner, repo] = nameWithOwner.split('/');
92
-
93
- // Get milestones
94
- let milestoneTitle = null;
95
- let milestonePct = 0;
96
- try {
97
- const milestonesRaw = execSync(
98
- 'gh api repos/' + owner + '/' + repo + '/milestones --jq "."',
99
- { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
100
- ).trim();
101
- if (milestonesRaw) {
102
- const milestones = JSON.parse(milestonesRaw);
103
- const openMilestone = milestones.find(function(m) { return m.state === 'open'; });
104
- if (openMilestone) {
105
- milestoneTitle = openMilestone.title || null;
106
- const total = (openMilestone.open_issues || 0) + (openMilestone.closed_issues || 0);
107
- if (total > 0) {
108
- milestonePct = Math.round(((openMilestone.closed_issues || 0) / total) * 100);
109
- }
110
- }
111
- }
112
- } catch (e) {
113
- // gh api failed for milestones, continue with defaults
114
- }
115
-
116
- // Get current phase from open issues with 'phase' label, parse number from title
117
- let phaseNumber = null;
118
- let issueNumber = null;
119
- try {
120
- const phaseRaw = execSync(
121
- 'gh api "repos/' + owner + '/' + repo + '/issues?state=open&labels=phase&per_page=1&sort=updated&direction=desc" --jq ".[0] | {number: .number, title: .title}"',
122
- { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
123
- ).trim();
124
- const phaseData = JSON.parse(phaseRaw || '{}');
125
- const titleMatch = (phaseData.title || '').match(/^\\[Phase\\s+(\\S+)\\]/);
126
- if (titleMatch) {
127
- phaseNumber = titleMatch[1];
128
- }
129
- issueNumber = phaseData.number || null;
130
- } catch (e) {
131
- // gh api failed for phase, continue with null
132
- }
133
-
134
- // Get board column via GraphQL
135
- let boardColumn = null;
136
- if (issueNumber) {
137
- try {
138
- const gqlQuery = '{ repository(owner: "' + owner + '", name: "' + repo + '") { issue(number: ' + issueNumber + ') { projectItems(first: 5, includeArchived: false) { nodes { fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name } } } } } } }';
139
- const boardRaw = execSync(
140
- 'gh api graphql -f query=@-',
141
- { input: gqlQuery, encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
142
- ).trim();
143
- const boardData = JSON.parse(boardRaw);
144
- const nodes = boardData?.data?.repository?.issue?.projectItems?.nodes || [];
145
- if (nodes.length > 0 && nodes[0]?.fieldValueByName?.name) {
146
- boardColumn = nodes[0].fieldValueByName.name;
147
- }
148
- } catch (e) {
149
- boardColumn = null;
150
- }
151
- }
152
-
153
- // Write cache
154
- const cacheData = JSON.stringify({
155
- phase_number: phaseNumber,
156
- milestone_title: milestoneTitle,
157
- milestone_pct: milestonePct,
158
- board_column: boardColumn,
159
- updated: Math.floor(Date.now() / 1000),
160
- });
161
-
162
- const dir = ${JSON.stringify(cacheDir)};
163
- fs.mkdirSync(dir, { recursive: true });
164
- fs.writeFileSync(${JSON.stringify(cacheFile)}, cacheData);
165
- } catch (e) {
166
- try {
167
- const dir = ${JSON.stringify(cacheDir)};
168
- fs.mkdirSync(dir, { recursive: true });
169
- fs.writeFileSync(${JSON.stringify(cacheFile)}, JSON.stringify({
170
- phase_number: null,
171
- milestone_title: null,
172
- milestone_pct: 0,
173
- board_column: null,
174
- offline: true,
175
- updated: Math.floor(Date.now() / 1000),
176
- }));
177
- } catch (_) {}
178
- process.exit(0);
65
+ /** Resolve the project directory from the hook input or fall back to cwd. */
66
+ function resolveProjectDir(input) {
67
+ return input.cwd ?? process.cwd();
179
68
  }
180
- `;
181
- const isWindows = process.platform === "win32";
182
- (0, node_child_process.spawn)(process.execPath, ["-e", script], {
183
- stdio: "ignore",
184
- windowsHide: true,
185
- detached: !isWindows
186
- }).unref();
187
- } catch {}
188
- }
189
- function formatStatusline(data) {
190
- const model = data.model?.display_name || "Claude";
191
- const dir = data.workspace?.project_dir || data.workspace?.current_dir || process.cwd();
192
- const dirname = node_path.basename(dir);
193
- const SEP = " │ ";
194
- const DIM = "\x1B[2m";
195
- const RESET = "\x1B[0m";
196
- let updateIndicator = "";
197
- const updateCacheFile = node_path.join(dir, CLAUDE_DIR, "cache", "maxsim-update-check.json");
198
- if (node_fs.existsSync(updateCacheFile)) try {
199
- if (JSON.parse(node_fs.readFileSync(updateCacheFile, "utf8")).update_available) updateIndicator = "\x1B[33m⬆\x1B[0m ";
200
- } catch {}
201
- const planningDir = node_path.join(dir, ".planning");
202
- if (!node_fs.existsSync(planningDir)) return `${updateIndicator}${DIM}${model}${RESET}${SEP}${DIM}${dirname}${RESET}`;
203
- const cacheDir = node_path.join(dir, CLAUDE_DIR, "cache");
204
- const cacheFile = node_path.join(cacheDir, "maxsim-progress.json");
205
- let cache = null;
206
- let cacheAge = Infinity;
207
- if (node_fs.existsSync(cacheFile)) try {
208
- cache = JSON.parse(node_fs.readFileSync(cacheFile, "utf8"));
209
- cacheAge = Math.floor(Date.now() / 1e3) - (cache.updated || 0);
69
+ /** Read the maxsim config.json; returns null if absent or unreadable. */
70
+ function readMaxsimConfig(projectDir) {
71
+ const configPath = node_path.join(projectDir, CLAUDE_DIR, "maxsim", "config.json");
72
+ try {
73
+ if (!node_fs.existsSync(configPath)) return null;
74
+ return JSON.parse(node_fs.readFileSync(configPath, "utf8"));
210
75
  } catch {
211
- cache = null;
76
+ return null;
212
77
  }
213
- if (cacheAge > CACHE_TTL_SECONDS) spawnBackgroundRefresh(cacheDir, cacheFile);
214
- if (cache?.offline) return `${updateIndicator}${DIM}${model}${RESET}${SEP}${DIM}P? offline${RESET}${SEP}${DIM}${dirname}${RESET}`;
215
- let phaseSegment = "";
216
- if (cache?.phase_number) {
217
- const column = cache.board_column ? ` ${cache.board_column}` : "";
218
- phaseSegment = `${SEP}${DIM}P${cache.phase_number}${column}${RESET}`;
78
+ }
79
+ /** Check whether any maxsim phase plan files exist. */
80
+ function hasPhaseFiles(projectDir) {
81
+ const phasesDir = node_path.join(projectDir, CLAUDE_DIR, "maxsim", "phases");
82
+ try {
83
+ return node_fs.existsSync(phasesDir) && node_fs.readdirSync(phasesDir).length > 0;
84
+ } catch {
85
+ return false;
219
86
  }
220
- let milestoneSegment = "";
221
- if (cache?.milestone_title) milestoneSegment = `${SEP}${DIM}${cache.milestone_title}: ${cache.milestone_pct}%${RESET}`;
222
- return `${updateIndicator}${DIM}${model}${RESET}${phaseSegment}${milestoneSegment}${SEP}${DIM}${dirname}${RESET}`;
223
87
  }
224
- if (require.main === module) readStdinJson((data) => {
225
- process.stdout.write(formatStatusline(data));
88
+ readStdinJson((input) => {
89
+ const projectDir = resolveProjectDir(input);
90
+ const config = readMaxsimConfig(projectDir);
91
+ let statusText;
92
+ if (config !== null) {
93
+ const phase = typeof config.currentPhase === "number" ? config.currentPhase : null;
94
+ const status = typeof config.projectStatus === "string" ? config.projectStatus : "In Progress";
95
+ if (phase !== null) statusText = `MAXSIM \u25ba Phase ${phase} | ${status}`;
96
+ else statusText = `MAXSIM \u25ba ${status}`;
97
+ } else if (hasPhaseFiles(projectDir)) statusText = "MAXSIM ► In Progress";
98
+ else statusText = "MAXSIM ► Ready";
99
+ process.stdout.write(statusText + "\n");
100
+ process.exit(0);
226
101
  });
227
102
 
228
103
  //#endregion
229
- exports.formatStatusline = formatStatusline;
230
104
  //# sourceMappingURL=maxsim-statusline.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"maxsim-statusline.cjs","names":["path","fs"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-statusline.ts"],"sourcesContent":["/**\n * Shared utilities for MAXSIM hooks.\n */\n\n/**\n * Read all stdin as a string, then invoke callback with parsed JSON.\n * Used by statusline and sync-reminder hooks.\n */\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => (input += chunk));\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n // Silent fail -- never block hook execution\n process.exit(0);\n }\n });\n}\n\n/** The '.claude' path segment -- template marker replaced during install. */\nexport const CLAUDE_DIR = '.claude';\n\n/**\n * Play a system sound for notifications. Fire-and-forget, never blocks.\n * Suppressed when MAXSIM_SOUND=0, CI=true, or SSH_CONNECTION is set.\n */\nexport function playSound(type: 'question' | 'stop'): void {\n try {\n if (\n process.env.MAXSIM_SOUND === '0' ||\n process.env.CI === 'true' ||\n process.env.SSH_CONNECTION\n ) {\n return;\n }\n\n const platform = process.platform;\n\n if (platform === 'win32') {\n const file =\n type === 'question'\n ? 'C:\\\\Windows\\\\Media\\\\notify.wav'\n : 'C:\\\\Windows\\\\Media\\\\chimes.wav';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn(\n 'powershell',\n ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true },\n );\n child.unref();\n } else if (platform === 'darwin') {\n const file =\n type === 'question'\n ? '/System/Library/Sounds/Ping.aiff'\n : '/System/Library/Sounds/Glass.aiff';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn('afplay', [file], {\n stdio: 'ignore',\n detached: true,\n });\n child.unref();\n } else {\n // Linux / unknown — terminal bell fallback\n process.stderr.write('\\x07');\n }\n } catch {\n // Silent fail — never block hook execution\n }\n}\n","#!/usr/bin/env node\n/**\n * Claude Code Statusline - MAXSIM Edition\n * Shows: [update] model | P{N} {BoardColumn} | {milestone}: {pct}% | dirname\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport { readStdinJson, CLAUDE_DIR } from './shared';\n\nexport interface StatuslineInput {\n model?: { display_name?: string };\n workspace?: { current_dir?: string; project_dir?: string };\n session_id?: string;\n}\n\nexport interface ProgressCache {\n phase_number: string | null;\n milestone_title: string | null;\n milestone_pct: number;\n board_column: string | null;\n offline?: boolean;\n updated: number;\n}\n\nconst CACHE_TTL_SECONDS = 60;\n\n/**\n * Spawn a detached Node child process to refresh the progress cache in the background.\n * The child runs gh CLI commands to detect owner/repo, find the first open milestone,\n * compute progress, and find the current phase label.\n */\nfunction spawnBackgroundRefresh(cacheDir: string, cacheFile: string): void {\n try {\n const script = `\nconst { execSync } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\ntry {\n // Detect owner/repo\n const nameWithOwner = execSync('gh repo view --json nameWithOwner -q .nameWithOwner', {\n encoding: 'utf8',\n timeout: 10000,\n stdio: ['pipe', 'pipe', 'pipe'],\n windowsHide: true,\n }).trim();\n\n if (!nameWithOwner || !nameWithOwner.includes('/')) {\n process.exit(0);\n }\n\n const [owner, repo] = nameWithOwner.split('/');\n\n // Get milestones\n let milestoneTitle = null;\n let milestonePct = 0;\n try {\n const milestonesRaw = execSync(\n 'gh api repos/' + owner + '/' + repo + '/milestones --jq \".\"',\n { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }\n ).trim();\n if (milestonesRaw) {\n const milestones = JSON.parse(milestonesRaw);\n const openMilestone = milestones.find(function(m) { return m.state === 'open'; });\n if (openMilestone) {\n milestoneTitle = openMilestone.title || null;\n const total = (openMilestone.open_issues || 0) + (openMilestone.closed_issues || 0);\n if (total > 0) {\n milestonePct = Math.round(((openMilestone.closed_issues || 0) / total) * 100);\n }\n }\n }\n } catch (e) {\n // gh api failed for milestones, continue with defaults\n }\n\n // Get current phase from open issues with 'phase' label, parse number from title\n let phaseNumber = null;\n let issueNumber = null;\n try {\n const phaseRaw = execSync(\n 'gh api \"repos/' + owner + '/' + repo + '/issues?state=open&labels=phase&per_page=1&sort=updated&direction=desc\" --jq \".[0] | {number: .number, title: .title}\"',\n { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }\n ).trim();\n const phaseData = JSON.parse(phaseRaw || '{}');\n const titleMatch = (phaseData.title || '').match(/^\\\\[Phase\\\\s+(\\\\S+)\\\\]/);\n if (titleMatch) {\n phaseNumber = titleMatch[1];\n }\n issueNumber = phaseData.number || null;\n } catch (e) {\n // gh api failed for phase, continue with null\n }\n\n // Get board column via GraphQL\n let boardColumn = null;\n if (issueNumber) {\n try {\n const gqlQuery = '{ repository(owner: \"' + owner + '\", name: \"' + repo + '\") { issue(number: ' + issueNumber + ') { projectItems(first: 5, includeArchived: false) { nodes { fieldValueByName(name: \"Status\") { ... on ProjectV2ItemFieldSingleSelectValue { name } } } } } } }';\n const boardRaw = execSync(\n 'gh api graphql -f query=@-',\n { input: gqlQuery, encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }\n ).trim();\n const boardData = JSON.parse(boardRaw);\n const nodes = boardData?.data?.repository?.issue?.projectItems?.nodes || [];\n if (nodes.length > 0 && nodes[0]?.fieldValueByName?.name) {\n boardColumn = nodes[0].fieldValueByName.name;\n }\n } catch (e) {\n boardColumn = null;\n }\n }\n\n // Write cache\n const cacheData = JSON.stringify({\n phase_number: phaseNumber,\n milestone_title: milestoneTitle,\n milestone_pct: milestonePct,\n board_column: boardColumn,\n updated: Math.floor(Date.now() / 1000),\n });\n\n const dir = ${JSON.stringify(cacheDir)};\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(${JSON.stringify(cacheFile)}, cacheData);\n} catch (e) {\n try {\n const dir = ${JSON.stringify(cacheDir)};\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(${JSON.stringify(cacheFile)}, JSON.stringify({\n phase_number: null,\n milestone_title: null,\n milestone_pct: 0,\n board_column: null,\n offline: true,\n updated: Math.floor(Date.now() / 1000),\n }));\n } catch (_) {}\n process.exit(0);\n}\n`;\n\n const isWindows = process.platform === 'win32';\n const child = spawn(process.execPath, ['-e', script], {\n stdio: 'ignore',\n windowsHide: true,\n detached: !isWindows,\n });\n child.unref();\n } catch {\n // Silent fail -- never break statusline\n }\n}\n\nexport function formatStatusline(data: StatuslineInput): string {\n const model = data.model?.display_name || 'Claude';\n const dir = data.workspace?.project_dir || data.workspace?.current_dir || process.cwd();\n const dirname = path.basename(dir);\n\n const SEP = ' \\u2502 ';\n const DIM = '\\x1b[2m';\n const RESET = '\\x1b[0m';\n\n // MAXSIM update available?\n let updateIndicator = '';\n const updateCacheFile = path.join(dir, CLAUDE_DIR, 'cache', 'maxsim-update-check.json');\n if (fs.existsSync(updateCacheFile)) {\n try {\n const cache = JSON.parse(fs.readFileSync(updateCacheFile, 'utf8'));\n if (cache.update_available) {\n updateIndicator = '\\x1b[33m\\u2B06\\x1b[0m ';\n }\n } catch {\n // ignore\n }\n }\n\n // Check if this is a MAXSIM project\n const planningDir = path.join(dir, '.planning');\n const isMaxsimProject = fs.existsSync(planningDir);\n\n if (!isMaxsimProject) {\n return `${updateIndicator}${DIM}${model}${RESET}${SEP}${DIM}${dirname}${RESET}`;\n }\n\n // Read progress cache\n const cacheDir = path.join(dir, CLAUDE_DIR, 'cache');\n const cacheFile = path.join(cacheDir, 'maxsim-progress.json');\n let cache: ProgressCache | null = null;\n let cacheAge = Infinity;\n\n if (fs.existsSync(cacheFile)) {\n try {\n cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) as ProgressCache;\n cacheAge = Math.floor(Date.now() / 1000) - (cache.updated || 0);\n } catch {\n cache = null;\n }\n }\n\n // Spawn background refresh if cache is stale or missing\n if (cacheAge > CACHE_TTL_SECONDS) {\n spawnBackgroundRefresh(cacheDir, cacheFile);\n }\n\n // Offline fallback\n if (cache?.offline) {\n return `${updateIndicator}${DIM}${model}${RESET}${SEP}${DIM}P? offline${RESET}${SEP}${DIM}${dirname}${RESET}`;\n }\n\n // Build phase segment: P{N} {BoardColumn}\n let phaseSegment = '';\n if (cache?.phase_number) {\n const column = cache.board_column ? ` ${cache.board_column}` : '';\n phaseSegment = `${SEP}${DIM}P${cache.phase_number}${column}${RESET}`;\n }\n\n // Build milestone segment\n let milestoneSegment = '';\n if (cache?.milestone_title) {\n milestoneSegment = `${SEP}${DIM}${cache.milestone_title}: ${cache.milestone_pct}%${RESET}`;\n }\n\n return `${updateIndicator}${DIM}${model}${RESET}${phaseSegment}${milestoneSegment}${SEP}${DIM}${dirname}${RESET}`;\n}\n\n// Standalone entry\nif (require.main === module) {\n readStdinJson<StatuslineInput>((data) => {\n process.stdout.write(formatStatusline(data));\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAmB,SAAS,MAAO;AAC7D,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AAEN,WAAQ,KAAK,EAAE;;GAEjB;;;AAIJ,MAAa,aAAa;;;;;;;;ACE1B,MAAM,oBAAoB;;;;;;AAO1B,SAAS,uBAAuB,UAAkB,WAAyB;AACzE,KAAI;EACF,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAyFH,KAAK,UAAU,SAAS,CAAC;;qBAEpB,KAAK,UAAU,UAAU,CAAC;;;kBAG7B,KAAK,UAAU,SAAS,CAAC;;uBAEpB,KAAK,UAAU,UAAU,CAAC;;;;;;;;;;;;EAa7C,MAAM,YAAY,QAAQ,aAAa;AAMvC,gCALoB,QAAQ,UAAU,CAAC,MAAM,OAAO,EAAE;GACpD,OAAO;GACP,aAAa;GACb,UAAU,CAAC;GACZ,CAAC,CACI,OAAO;SACP;;AAKV,SAAgB,iBAAiB,MAA+B;CAC9D,MAAM,QAAQ,KAAK,OAAO,gBAAgB;CAC1C,MAAM,MAAM,KAAK,WAAW,eAAe,KAAK,WAAW,eAAe,QAAQ,KAAK;CACvF,MAAM,UAAUA,UAAK,SAAS,IAAI;CAElC,MAAM,MAAM;CACZ,MAAM,MAAM;CACZ,MAAM,QAAQ;CAGd,IAAI,kBAAkB;CACtB,MAAM,kBAAkBA,UAAK,KAAK,KAAK,YAAY,SAAS,2BAA2B;AACvF,KAAIC,QAAG,WAAW,gBAAgB,CAChC,KAAI;AAEF,MADc,KAAK,MAAMA,QAAG,aAAa,iBAAiB,OAAO,CAAC,CACxD,iBACR,mBAAkB;SAEd;CAMV,MAAM,cAAcD,UAAK,KAAK,KAAK,YAAY;AAG/C,KAAI,CAFoBC,QAAG,WAAW,YAAY,CAGhD,QAAO,GAAG,kBAAkB,MAAM,QAAQ,QAAQ,MAAM,MAAM,UAAU;CAI1E,MAAM,WAAWD,UAAK,KAAK,KAAK,YAAY,QAAQ;CACpD,MAAM,YAAYA,UAAK,KAAK,UAAU,uBAAuB;CAC7D,IAAI,QAA8B;CAClC,IAAI,WAAW;AAEf,KAAIC,QAAG,WAAW,UAAU,CAC1B,KAAI;AACF,UAAQ,KAAK,MAAMA,QAAG,aAAa,WAAW,OAAO,CAAC;AACtD,aAAW,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,IAAI,MAAM,WAAW;SACvD;AACN,UAAQ;;AAKZ,KAAI,WAAW,kBACb,wBAAuB,UAAU,UAAU;AAI7C,KAAI,OAAO,QACT,QAAO,GAAG,kBAAkB,MAAM,QAAQ,QAAQ,MAAM,IAAI,YAAY,QAAQ,MAAM,MAAM,UAAU;CAIxG,IAAI,eAAe;AACnB,KAAI,OAAO,cAAc;EACvB,MAAM,SAAS,MAAM,eAAe,IAAI,MAAM,iBAAiB;AAC/D,iBAAe,GAAG,MAAM,IAAI,GAAG,MAAM,eAAe,SAAS;;CAI/D,IAAI,mBAAmB;AACvB,KAAI,OAAO,gBACT,oBAAmB,GAAG,MAAM,MAAM,MAAM,gBAAgB,IAAI,MAAM,cAAc,GAAG;AAGrF,QAAO,GAAG,kBAAkB,MAAM,QAAQ,QAAQ,eAAe,mBAAmB,MAAM,MAAM,UAAU;;AAI5G,IAAI,QAAQ,SAAS,OACnB,gBAAgC,SAAS;AACvC,SAAQ,OAAO,MAAM,iBAAiB,KAAK,CAAC;EAC5C"}
1
+ {"version":3,"file":"maxsim-statusline.cjs","names":["path","fs"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-statusline.ts"],"sourcesContent":["/** Shared utilities for MAXSIM hooks. */\n\nimport { spawnSync } from 'node:child_process';\nimport * as os from 'node:os';\n\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n input += chunk;\n });\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n process.exit(0);\n }\n });\n}\n\nexport const CLAUDE_DIR = '.claude';\n\n/** Returns true when running on Windows. */\nexport function isWindows(): boolean {\n return os.platform() === 'win32';\n}\n\n/** Returns true when running on macOS. */\nexport function isMac(): boolean {\n return os.platform() === 'darwin';\n}\n\n/**\n * Play a system sound file cross-platform.\n * Never throws — sound failure is always silently swallowed.\n *\n * @param soundFile Absolute path to a WAV/MP3/etc. file, or a named system\n * sound token recognised by the platform helper (e.g. the\n * Windows-only SystemAsterisk token).\n */\nexport function playSound(soundFile: string): void {\n try {\n if (isWindows()) {\n // PowerShell's SoundPlayer works with WAV files synchronously.\n // For named system sounds (no extension) fall back to rundll32.\n const isWav = soundFile.toLowerCase().endsWith('.wav');\n if (isWav) {\n spawnSync(\n 'powershell',\n [\n '-NoProfile',\n '-NonInteractive',\n '-Command',\n `(New-Object System.Media.SoundPlayer '${soundFile.replace(/'/g, \"''\")}').PlaySync()`,\n ],\n { stdio: 'ignore' },\n );\n } else {\n // Named system sound token (e.g. \"SystemAsterisk\") or unsupported format —\n // use the rundll32 winsound bridge.\n spawnSync(\n 'rundll32',\n ['user32.dll,MessageBeep'],\n { stdio: 'ignore' },\n );\n }\n } else if (isMac()) {\n spawnSync('afplay', [soundFile], { stdio: 'ignore' });\n } else {\n // Linux: try paplay (PulseAudio) then aplay (ALSA)\n const paplay = spawnSync('paplay', [soundFile], { stdio: 'ignore' });\n if (paplay.status !== 0) {\n spawnSync('aplay', [soundFile], { stdio: 'ignore' });\n }\n }\n } catch {\n // Never crash on sound failure\n }\n}\n","/**\n * statusLine hook — display a brief MaxsimCLI status string in the terminal\n * status bar.\n *\n * Output contract (Claude Code statusLine):\n * Process must write a single line of plain text (or JSON with a \"text\" key)\n * to stdout and exit 0. The output should be short (<80 chars).\n *\n * Performance target: <100 ms — local file reads only, no network calls.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { readStdinJson, CLAUDE_DIR } from './shared.js';\n\ninterface StatusLineInput {\n cwd?: string;\n session_id?: string;\n [key: string]: unknown;\n}\n\ninterface MaxsimConfig {\n currentPhase?: number;\n projectStatus?: string;\n [key: string]: unknown;\n}\n\n/** Resolve the project directory from the hook input or fall back to cwd. */\nfunction resolveProjectDir(input: StatusLineInput): string {\n return input.cwd ?? process.cwd();\n}\n\n/** Read the maxsim config.json; returns null if absent or unreadable. */\nfunction readMaxsimConfig(projectDir: string): MaxsimConfig | null {\n const configPath = path.join(projectDir, CLAUDE_DIR, 'maxsim', 'config.json');\n try {\n if (!fs.existsSync(configPath)) return null;\n return JSON.parse(fs.readFileSync(configPath, 'utf8')) as MaxsimConfig;\n } catch {\n return null;\n }\n}\n\n/** Check whether any maxsim phase plan files exist. */\nfunction hasPhaseFiles(projectDir: string): boolean {\n const phasesDir = path.join(projectDir, CLAUDE_DIR, 'maxsim', 'phases');\n try {\n return fs.existsSync(phasesDir) && fs.readdirSync(phasesDir).length > 0;\n } catch {\n return false;\n }\n}\n\nreadStdinJson<StatusLineInput>((input) => {\n const projectDir = resolveProjectDir(input);\n const config = readMaxsimConfig(projectDir);\n\n let statusText: string;\n\n if (config !== null) {\n const phase = typeof config.currentPhase === 'number' ? config.currentPhase : null;\n const status = typeof config.projectStatus === 'string' ? config.projectStatus : 'In Progress';\n\n if (phase !== null) {\n statusText = `MAXSIM \\u25ba Phase ${phase} | ${status}`;\n } else {\n statusText = `MAXSIM \\u25ba ${status}`;\n }\n } else if (hasPhaseFiles(projectDir)) {\n statusText = 'MAXSIM \\u25ba In Progress';\n } else {\n statusText = 'MAXSIM \\u25ba Ready';\n }\n\n process.stdout.write(statusText + '\\n');\n process.exit(0);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAkB;AAC1C,WAAS;GACT;AACF,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AACN,WAAQ,KAAK,EAAE;;GAEjB;;AAGJ,MAAa,aAAa;;;;;;;;;;;;;;;ACO1B,SAAS,kBAAkB,OAAgC;AACzD,QAAO,MAAM,OAAO,QAAQ,KAAK;;;AAInC,SAAS,iBAAiB,YAAyC;CACjE,MAAM,aAAaA,UAAK,KAAK,YAAY,YAAY,UAAU,cAAc;AAC7E,KAAI;AACF,MAAI,CAACC,QAAG,WAAW,WAAW,CAAE,QAAO;AACvC,SAAO,KAAK,MAAMA,QAAG,aAAa,YAAY,OAAO,CAAC;SAChD;AACN,SAAO;;;;AAKX,SAAS,cAAc,YAA6B;CAClD,MAAM,YAAYD,UAAK,KAAK,YAAY,YAAY,UAAU,SAAS;AACvE,KAAI;AACF,SAAOC,QAAG,WAAW,UAAU,IAAIA,QAAG,YAAY,UAAU,CAAC,SAAS;SAChE;AACN,SAAO;;;AAIX,eAAgC,UAAU;CACxC,MAAM,aAAa,kBAAkB,MAAM;CAC3C,MAAM,SAAS,iBAAiB,WAAW;CAE3C,IAAI;AAEJ,KAAI,WAAW,MAAM;EACnB,MAAM,QAAQ,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;EAC9E,MAAM,SAAS,OAAO,OAAO,kBAAkB,WAAW,OAAO,gBAAgB;AAEjF,MAAI,UAAU,KACZ,cAAa,uBAAuB,MAAM,KAAK;MAE/C,cAAa,iBAAiB;YAEvB,cAAc,WAAW,CAClC,cAAa;KAEb,cAAa;AAGf,SAAQ,OAAO,MAAM,aAAa,KAAK;AACvC,SAAQ,KAAK,EAAE;EACf"}
@@ -1,18 +1,44 @@
1
- #!/usr/bin/env node
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path);
30
+ let node_child_process = require("node:child_process");
31
+ let node_os = require("node:os");
32
+ node_os = __toESM(node_os);
3
33
 
4
34
  //#region src/hooks/shared.ts
5
- /**
6
- * Shared utilities for MAXSIM hooks.
7
- */
8
- /**
9
- * Read all stdin as a string, then invoke callback with parsed JSON.
10
- * Used by statusline and sync-reminder hooks.
11
- */
35
+ /** Shared utilities for MAXSIM hooks. */
12
36
  function readStdinJson(callback) {
13
37
  let input = "";
14
38
  process.stdin.setEncoding("utf8");
15
- process.stdin.on("data", (chunk) => input += chunk);
39
+ process.stdin.on("data", (chunk) => {
40
+ input += chunk;
41
+ });
16
42
  process.stdin.on("end", () => {
17
43
  try {
18
44
  callback(JSON.parse(input));
@@ -21,51 +47,68 @@ function readStdinJson(callback) {
21
47
  }
22
48
  });
23
49
  }
50
+ /** Returns true when running on Windows. */
51
+ function isWindows() {
52
+ return node_os.platform() === "win32";
53
+ }
54
+ /** Returns true when running on macOS. */
55
+ function isMac() {
56
+ return node_os.platform() === "darwin";
57
+ }
24
58
  /**
25
- * Play a system sound for notifications. Fire-and-forget, never blocks.
26
- * Suppressed when MAXSIM_SOUND=0, CI=true, or SSH_CONNECTION is set.
59
+ * Play a system sound file cross-platform.
60
+ * Never throws sound failure is always silently swallowed.
61
+ *
62
+ * @param soundFile Absolute path to a WAV/MP3/etc. file, or a named system
63
+ * sound token recognised by the platform helper (e.g. the
64
+ * Windows-only SystemAsterisk token).
27
65
  */
28
- function playSound(type) {
66
+ function playSound(soundFile) {
29
67
  try {
30
- if (process.env.MAXSIM_SOUND === "0" || process.env.CI === "true" || process.env.SSH_CONNECTION) return;
31
- const platform = process.platform;
32
- if (platform === "win32") {
33
- const file = type === "question" ? "C:\\Windows\\Media\\notify.wav" : "C:\\Windows\\Media\\chimes.wav";
34
- const { spawn } = require("node:child_process");
35
- spawn("powershell", [
36
- "-NoProfile",
37
- "-WindowStyle",
38
- "Hidden",
39
- "-Command",
40
- `(New-Object Media.SoundPlayer '${file}').PlaySync()`
41
- ], {
42
- stdio: "ignore",
43
- windowsHide: true
44
- }).unref();
45
- } else if (platform === "darwin") {
46
- const file = type === "question" ? "/System/Library/Sounds/Ping.aiff" : "/System/Library/Sounds/Glass.aiff";
47
- const { spawn } = require("node:child_process");
48
- spawn("afplay", [file], {
49
- stdio: "ignore",
50
- detached: true
51
- }).unref();
52
- } else process.stderr.write("\x07");
68
+ if (isWindows()) if (soundFile.toLowerCase().endsWith(".wav")) (0, node_child_process.spawnSync)("powershell", [
69
+ "-NoProfile",
70
+ "-NonInteractive",
71
+ "-Command",
72
+ `(New-Object System.Media.SoundPlayer '${soundFile.replace(/'/g, "''")}').PlaySync()`
73
+ ], { stdio: "ignore" });
74
+ else (0, node_child_process.spawnSync)("rundll32", ["user32.dll,MessageBeep"], { stdio: "ignore" });
75
+ else if (isMac()) (0, node_child_process.spawnSync)("afplay", [soundFile], { stdio: "ignore" });
76
+ else if ((0, node_child_process.spawnSync)("paplay", [soundFile], { stdio: "ignore" }).status !== 0) (0, node_child_process.spawnSync)("aplay", [soundFile], { stdio: "ignore" });
53
77
  } catch {}
54
78
  }
55
79
 
56
80
  //#endregion
57
81
  //#region src/hooks/maxsim-stop-sound.ts
58
82
  /**
59
- * Stop Sound Hook Stop event hook that plays a sound
60
- * when Claude finishes working.
83
+ * Stop hookplay a satisfying completion sound when Claude finishes a task
84
+ * (i.e. the Stop event fires).
85
+ *
86
+ * Uses platform-native system sounds so no external audio files are required.
87
+ * Falls through silently if playback fails.
61
88
  */
62
- function processStopSound(data) {
63
- playSound("stop");
89
+ /** Resolve a bundled WAV asset relative to this script, or return null. */
90
+ function bundledSound(name) {
91
+ const candidates = [node_path.join(node_path.dirname(process.argv[1] ?? __filename), "sounds", name), node_path.join(__dirname, "sounds", name)];
92
+ for (const p of candidates) try {
93
+ if (require("node:fs").existsSync(p)) return p;
94
+ } catch {}
95
+ return null;
96
+ }
97
+ /** Play the best available completion sound for the current platform. */
98
+ function playCompletion() {
99
+ const wav = bundledSound("complete.wav");
100
+ if (wav) {
101
+ playSound(wav);
102
+ return;
103
+ }
104
+ if (isWindows()) playSound("SystemNotification");
105
+ else if (isMac()) playSound("/System/Library/Sounds/Glass.aiff");
106
+ else playSound("/usr/share/sounds/freedesktop/stereo/complete.oga");
64
107
  }
65
- if (require.main === module) readStdinJson((data) => {
66
- processStopSound(data);
108
+ readStdinJson((_input) => {
109
+ playCompletion();
110
+ process.exit(0);
67
111
  });
68
112
 
69
113
  //#endregion
70
- exports.processStopSound = processStopSound;
71
114
  //# sourceMappingURL=maxsim-stop-sound.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"maxsim-stop-sound.cjs","names":[],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-stop-sound.ts"],"sourcesContent":["/**\n * Shared utilities for MAXSIM hooks.\n */\n\n/**\n * Read all stdin as a string, then invoke callback with parsed JSON.\n * Used by statusline and sync-reminder hooks.\n */\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => (input += chunk));\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n // Silent fail -- never block hook execution\n process.exit(0);\n }\n });\n}\n\n/** The '.claude' path segment -- template marker replaced during install. */\nexport const CLAUDE_DIR = '.claude';\n\n/**\n * Play a system sound for notifications. Fire-and-forget, never blocks.\n * Suppressed when MAXSIM_SOUND=0, CI=true, or SSH_CONNECTION is set.\n */\nexport function playSound(type: 'question' | 'stop'): void {\n try {\n if (\n process.env.MAXSIM_SOUND === '0' ||\n process.env.CI === 'true' ||\n process.env.SSH_CONNECTION\n ) {\n return;\n }\n\n const platform = process.platform;\n\n if (platform === 'win32') {\n const file =\n type === 'question'\n ? 'C:\\\\Windows\\\\Media\\\\notify.wav'\n : 'C:\\\\Windows\\\\Media\\\\chimes.wav';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn(\n 'powershell',\n ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true },\n );\n child.unref();\n } else if (platform === 'darwin') {\n const file =\n type === 'question'\n ? '/System/Library/Sounds/Ping.aiff'\n : '/System/Library/Sounds/Glass.aiff';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn('afplay', [file], {\n stdio: 'ignore',\n detached: true,\n });\n child.unref();\n } else {\n // Linux / unknown terminal bell fallback\n process.stderr.write('\\x07');\n }\n } catch {\n // Silent fail never block hook execution\n }\n}\n","#!/usr/bin/env node\n/**\n * Stop Sound Hook Stop event hook that plays a sound\n * when Claude finishes working.\n */\n\nimport { readStdinJson, playSound } from './shared';\n\ninterface StopSoundInput {\n stop_hook_active?: boolean;\n}\n\nexport function processStopSound(data: StopSoundInput): void {\n playSound('stop');\n}\n\n// Standalone entry\nif (require.main === module) {\n readStdinJson<StopSoundInput>((data) => {\n processStopSound(data);\n });\n}\n"],"mappings":";;;;;;;;;;;AAQA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAmB,SAAS,MAAO;AAC7D,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AAEN,WAAQ,KAAK,EAAE;;GAEjB;;;;;;AAUJ,SAAgB,UAAU,MAAiC;AACzD,KAAI;AACF,MACE,QAAQ,IAAI,iBAAiB,OAC7B,QAAQ,IAAI,OAAO,UACnB,QAAQ,IAAI,eAEZ;EAGF,MAAM,WAAW,QAAQ;AAEzB,MAAI,aAAa,SAAS;GACxB,MAAM,OACJ,SAAS,aACL,mCACA;GACN,MAAM,EAAE,UAAU,QAAQ,qBAAqB;AAM/C,GALc,MACZ,cACA;IAAC;IAAc;IAAgB;IAAU;IAAY,kCAAkC,KAAK;IAAe,EAC3G;IAAE,OAAO;IAAU,aAAa;IAAM,CACvC,CACK,OAAO;aACJ,aAAa,UAAU;GAChC,MAAM,OACJ,SAAS,aACL,qCACA;GACN,MAAM,EAAE,UAAU,QAAQ,qBAAqB;AAK/C,GAJc,MAAM,UAAU,CAAC,KAAK,EAAE;IACpC,OAAO;IACP,UAAU;IACX,CAAC,CACI,OAAO;QAGb,SAAQ,OAAO,MAAM,OAAO;SAExB;;;;;;;;;ACzDV,SAAgB,iBAAiB,MAA4B;AAC3D,WAAU,OAAO;;AAInB,IAAI,QAAQ,SAAS,OACnB,gBAA+B,SAAS;AACtC,kBAAiB,KAAK;EACtB"}
1
+ {"version":3,"file":"maxsim-stop-sound.cjs","names":["os","path"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-stop-sound.ts"],"sourcesContent":["/** Shared utilities for MAXSIM hooks. */\n\nimport { spawnSync } from 'node:child_process';\nimport * as os from 'node:os';\n\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n input += chunk;\n });\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n process.exit(0);\n }\n });\n}\n\nexport const CLAUDE_DIR = '.claude';\n\n/** Returns true when running on Windows. */\nexport function isWindows(): boolean {\n return os.platform() === 'win32';\n}\n\n/** Returns true when running on macOS. */\nexport function isMac(): boolean {\n return os.platform() === 'darwin';\n}\n\n/**\n * Play a system sound file cross-platform.\n * Never throws — sound failure is always silently swallowed.\n *\n * @param soundFile Absolute path to a WAV/MP3/etc. file, or a named system\n * sound token recognised by the platform helper (e.g. the\n * Windows-only SystemAsterisk token).\n */\nexport function playSound(soundFile: string): void {\n try {\n if (isWindows()) {\n // PowerShell's SoundPlayer works with WAV files synchronously.\n // For named system sounds (no extension) fall back to rundll32.\n const isWav = soundFile.toLowerCase().endsWith('.wav');\n if (isWav) {\n spawnSync(\n 'powershell',\n [\n '-NoProfile',\n '-NonInteractive',\n '-Command',\n `(New-Object System.Media.SoundPlayer '${soundFile.replace(/'/g, \"''\")}').PlaySync()`,\n ],\n { stdio: 'ignore' },\n );\n } else {\n // Named system sound token (e.g. \"SystemAsterisk\") or unsupported format —\n // use the rundll32 winsound bridge.\n spawnSync(\n 'rundll32',\n ['user32.dll,MessageBeep'],\n { stdio: 'ignore' },\n );\n }\n } else if (isMac()) {\n spawnSync('afplay', [soundFile], { stdio: 'ignore' });\n } else {\n // Linux: try paplay (PulseAudio) then aplay (ALSA)\n const paplay = spawnSync('paplay', [soundFile], { stdio: 'ignore' });\n if (paplay.status !== 0) {\n spawnSync('aplay', [soundFile], { stdio: 'ignore' });\n }\n }\n } catch {\n // Never crash on sound failure\n }\n}\n","/**\n * Stop hookplay a satisfying completion sound when Claude finishes a task\n * (i.e. the Stop event fires).\n *\n * Uses platform-native system sounds so no external audio files are required.\n * Falls through silently if playback fails.\n */\n\nimport * as path from 'node:path';\nimport { readStdinJson, playSound, isWindows, isMac } from './shared.js';\n\ninterface StopInput {\n session_id?: string;\n stop_reason?: string;\n [key: string]: unknown;\n}\n\n/** Resolve a bundled WAV asset relative to this script, or return null. */\nfunction bundledSound(name: string): string | null {\n const candidates = [\n path.join(path.dirname(process.argv[1] ?? __filename), 'sounds', name),\n path.join(__dirname, 'sounds', name),\n ];\n for (const p of candidates) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n if (require('node:fs').existsSync(p)) return p;\n } catch {\n // ignore\n }\n }\n return null;\n}\n\n/** Play the best available completion sound for the current platform. */\nfunction playCompletion(): void {\n // 1. Prefer a bundled WAV if present\n const wav = bundledSound('complete.wav');\n if (wav) {\n playSound(wav);\n return;\n }\n\n // 2. Fall back to a built-in system sound\n if (isWindows()) {\n // SystemNotification maps to the Windows notification toast sound\n playSound('SystemNotification');\n } else if (isMac()) {\n // Glass — a clean, pleasant completion chime\n playSound('/System/Library/Sounds/Glass.aiff');\n } else {\n // Linux: use the freedesktop complete sound\n playSound('/usr/share/sounds/freedesktop/stereo/complete.oga');\n }\n}\n\nreadStdinJson<StopInput>((_input) => {\n playCompletion();\n process.exit(0);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAkB;AAC1C,WAAS;GACT;AACF,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AACN,WAAQ,KAAK,EAAE;;GAEjB;;;AAMJ,SAAgB,YAAqB;AACnC,QAAOA,QAAG,UAAU,KAAK;;;AAI3B,SAAgB,QAAiB;AAC/B,QAAOA,QAAG,UAAU,KAAK;;;;;;;;;;AAW3B,SAAgB,UAAU,WAAyB;AACjD,KAAI;AACF,MAAI,WAAW,CAIb,KADc,UAAU,aAAa,CAAC,SAAS,OAAO,CAEpD,mCACE,cACA;GACE;GACA;GACA;GACA,yCAAyC,UAAU,QAAQ,MAAM,KAAK,CAAC;GACxE,EACD,EAAE,OAAO,UAAU,CACpB;MAID,mCACE,YACA,CAAC,yBAAyB,EAC1B,EAAE,OAAO,UAAU,CACpB;WAEM,OAAO,CAChB,mCAAU,UAAU,CAAC,UAAU,EAAE,EAAE,OAAO,UAAU,CAAC;6CAG5B,UAAU,CAAC,UAAU,EAAE,EAAE,OAAO,UAAU,CAAC,CACzD,WAAW,EACpB,mCAAU,SAAS,CAAC,UAAU,EAAE,EAAE,OAAO,UAAU,CAAC;SAGlD;;;;;;;;;;;;;AC1DV,SAAS,aAAa,MAA6B;CACjD,MAAM,aAAa,CACjBC,UAAK,KAAKA,UAAK,QAAQ,QAAQ,KAAK,MAAM,WAAW,EAAE,UAAU,KAAK,EACtEA,UAAK,KAAK,WAAW,UAAU,KAAK,CACrC;AACD,MAAK,MAAM,KAAK,WACd,KAAI;AAEF,MAAI,QAAQ,UAAU,CAAC,WAAW,EAAE,CAAE,QAAO;SACvC;AAIV,QAAO;;;AAIT,SAAS,iBAAuB;CAE9B,MAAM,MAAM,aAAa,eAAe;AACxC,KAAI,KAAK;AACP,YAAU,IAAI;AACd;;AAIF,KAAI,WAAW,CAEb,WAAU,qBAAqB;UACtB,OAAO,CAEhB,WAAU,oCAAoC;KAG9C,WAAU,oDAAoD;;AAIlE,eAA0B,WAAW;AACnC,iBAAgB;AAChB,SAAQ,KAAK,EAAE;EACf"}
@@ -1,18 +1,43 @@
1
- #!/usr/bin/env node
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path);
30
+ let node_child_process = require("node:child_process");
31
+ require("node:os");
3
32
 
4
33
  //#region src/hooks/shared.ts
5
- /**
6
- * Shared utilities for MAXSIM hooks.
7
- */
8
- /**
9
- * Read all stdin as a string, then invoke callback with parsed JSON.
10
- * Used by statusline and sync-reminder hooks.
11
- */
34
+ /** Shared utilities for MAXSIM hooks. */
12
35
  function readStdinJson(callback) {
13
36
  let input = "";
14
37
  process.stdin.setEncoding("utf8");
15
- process.stdin.on("data", (chunk) => input += chunk);
38
+ process.stdin.on("data", (chunk) => {
39
+ input += chunk;
40
+ });
16
41
  process.stdin.on("end", () => {
17
42
  try {
18
43
  callback(JSON.parse(input));
@@ -21,24 +46,50 @@ function readStdinJson(callback) {
21
46
  }
22
47
  });
23
48
  }
49
+ const CLAUDE_DIR = ".claude";
24
50
 
25
51
  //#endregion
26
52
  //#region src/hooks/maxsim-sync-reminder.ts
27
53
  /**
28
- * Sync Reminder HookNo longer needed.
29
- * GitHub Issues is the sole source of truth for phase artifacts and todos.
30
- * Local .planning/ writes no longer need sync reminders.
54
+ * Stop hook (secondary)remind the user to sync their MaxsimCLI config to
55
+ * version control after a Claude session ends.
56
+ *
57
+ * Behaviour:
58
+ * - Checks whether the project has uncommitted changes in .claude/maxsim/.
59
+ * - If there are dirty tracked files, emits a JSON block that adds a short
60
+ * reminder to Claude's context.
61
+ * - Runs fast (<50 ms): one `git status --short` call, no network I/O.
62
+ * - Always exits 0 — never blocks the user.
31
63
  */
32
- const DEBOUNCE_CALLS = 10;
33
- function processSyncReminder(_data) {
34
- return null;
64
+ /** Returns true if there are modified/untracked files under .claude/maxsim/. */
65
+ function hasUncommittedMaxsimChanges(projectDir) {
66
+ try {
67
+ const result = (0, node_child_process.spawnSync)("git", [
68
+ "status",
69
+ "--short",
70
+ "--",
71
+ node_path.join(CLAUDE_DIR, "maxsim")
72
+ ], {
73
+ cwd: projectDir,
74
+ encoding: "utf8",
75
+ timeout: 3e3,
76
+ stdio: [
77
+ "ignore",
78
+ "pipe",
79
+ "ignore"
80
+ ],
81
+ windowsHide: true
82
+ });
83
+ if (result.status !== 0) return false;
84
+ return (result.stdout ?? "").trim().length > 0;
85
+ } catch {
86
+ return false;
87
+ }
35
88
  }
36
- if (require.main === module) readStdinJson((data) => {
37
- const result = processSyncReminder(data);
38
- if (result) process.stdout.write(JSON.stringify(result));
89
+ readStdinJson((input) => {
90
+ if (hasUncommittedMaxsimChanges(input.cwd ?? process.cwd())) process.stdout.write(JSON.stringify({ additionalContext: "Reminder: you have uncommitted MaxsimCLI changes in .claude/maxsim/. Consider running `git add .claude/maxsim && git commit -m \"chore: sync maxsim config\"` to keep your phase plans and config under version control." }) + "\n");
91
+ process.exit(0);
39
92
  });
40
93
 
41
94
  //#endregion
42
- exports.DEBOUNCE_CALLS = DEBOUNCE_CALLS;
43
- exports.processSyncReminder = processSyncReminder;
44
95
  //# sourceMappingURL=maxsim-sync-reminder.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"maxsim-sync-reminder.cjs","names":[],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-sync-reminder.ts"],"sourcesContent":["/**\n * Shared utilities for MAXSIM hooks.\n */\n\n/**\n * Read all stdin as a string, then invoke callback with parsed JSON.\n * Used by statusline and sync-reminder hooks.\n */\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => (input += chunk));\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n // Silent fail -- never block hook execution\n process.exit(0);\n }\n });\n}\n\n/** The '.claude' path segment -- template marker replaced during install. */\nexport const CLAUDE_DIR = '.claude';\n\n/**\n * Play a system sound for notifications. Fire-and-forget, never blocks.\n * Suppressed when MAXSIM_SOUND=0, CI=true, or SSH_CONNECTION is set.\n */\nexport function playSound(type: 'question' | 'stop'): void {\n try {\n if (\n process.env.MAXSIM_SOUND === '0' ||\n process.env.CI === 'true' ||\n process.env.SSH_CONNECTION\n ) {\n return;\n }\n\n const platform = process.platform;\n\n if (platform === 'win32') {\n const file =\n type === 'question'\n ? 'C:\\\\Windows\\\\Media\\\\notify.wav'\n : 'C:\\\\Windows\\\\Media\\\\chimes.wav';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn(\n 'powershell',\n ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true },\n );\n child.unref();\n } else if (platform === 'darwin') {\n const file =\n type === 'question'\n ? '/System/Library/Sounds/Ping.aiff'\n : '/System/Library/Sounds/Glass.aiff';\n const { spawn } = require('node:child_process') as typeof import('node:child_process');\n const child = spawn('afplay', [file], {\n stdio: 'ignore',\n detached: true,\n });\n child.unref();\n } else {\n // Linux / unknown terminal bell fallback\n process.stderr.write('\\x07');\n }\n } catch {\n // Silent fail never block hook execution\n }\n}\n","#!/usr/bin/env node\n/**\n * Sync Reminder HookNo longer needed.\n * GitHub Issues is the sole source of truth for phase artifacts and todos.\n * Local .planning/ writes no longer need sync reminders.\n */\n\nimport { readStdinJson } from './shared';\n\nexport interface SyncReminderInput {\n session_id?: string;\n cwd?: string;\n tool_input?: { file_path?: string };\n}\n\nexport interface SyncReminderOutput {\n hookSpecificOutput: {\n hookEventName: string;\n additionalContext: string;\n };\n}\n\nexport const DEBOUNCE_CALLS = 10;\n\nexport function processSyncReminder(\n _data: SyncReminderInput,\n): SyncReminderOutput | null {\n // No-op: GitHub Issues is SSOT for phase artifacts and todos.\n return null;\n}\n\n// Standalone entry\nif (require.main === module) {\n readStdinJson<SyncReminderInput>((data) => {\n const result = processSyncReminder(data);\n if (result) {\n process.stdout.write(JSON.stringify(result));\n }\n });\n}\n"],"mappings":";;;;;;;;;;;AAQA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAmB,SAAS,MAAO;AAC7D,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AAEN,WAAQ,KAAK,EAAE;;GAEjB;;;;;;;;;;ACEJ,MAAa,iBAAiB;AAE9B,SAAgB,oBACd,OAC2B;AAE3B,QAAO;;AAIT,IAAI,QAAQ,SAAS,OACnB,gBAAkC,SAAS;CACzC,MAAM,SAAS,oBAAoB,KAAK;AACxC,KAAI,OACF,SAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;EAE9C"}
1
+ {"version":3,"file":"maxsim-sync-reminder.cjs","names":["path"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-sync-reminder.ts"],"sourcesContent":["/** Shared utilities for MAXSIM hooks. */\n\nimport { spawnSync } from 'node:child_process';\nimport * as os from 'node:os';\n\nexport function readStdinJson<T>(callback: (data: T) => void): void {\n let input = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n input += chunk;\n });\n process.stdin.on('end', () => {\n try {\n const data = JSON.parse(input) as T;\n callback(data);\n } catch {\n process.exit(0);\n }\n });\n}\n\nexport const CLAUDE_DIR = '.claude';\n\n/** Returns true when running on Windows. */\nexport function isWindows(): boolean {\n return os.platform() === 'win32';\n}\n\n/** Returns true when running on macOS. */\nexport function isMac(): boolean {\n return os.platform() === 'darwin';\n}\n\n/**\n * Play a system sound file cross-platform.\n * Never throws — sound failure is always silently swallowed.\n *\n * @param soundFile Absolute path to a WAV/MP3/etc. file, or a named system\n * sound token recognised by the platform helper (e.g. the\n * Windows-only SystemAsterisk token).\n */\nexport function playSound(soundFile: string): void {\n try {\n if (isWindows()) {\n // PowerShell's SoundPlayer works with WAV files synchronously.\n // For named system sounds (no extension) fall back to rundll32.\n const isWav = soundFile.toLowerCase().endsWith('.wav');\n if (isWav) {\n spawnSync(\n 'powershell',\n [\n '-NoProfile',\n '-NonInteractive',\n '-Command',\n `(New-Object System.Media.SoundPlayer '${soundFile.replace(/'/g, \"''\")}').PlaySync()`,\n ],\n { stdio: 'ignore' },\n );\n } else {\n // Named system sound token (e.g. \"SystemAsterisk\") or unsupported format —\n // use the rundll32 winsound bridge.\n spawnSync(\n 'rundll32',\n ['user32.dll,MessageBeep'],\n { stdio: 'ignore' },\n );\n }\n } else if (isMac()) {\n spawnSync('afplay', [soundFile], { stdio: 'ignore' });\n } else {\n // Linux: try paplay (PulseAudio) then aplay (ALSA)\n const paplay = spawnSync('paplay', [soundFile], { stdio: 'ignore' });\n if (paplay.status !== 0) {\n spawnSync('aplay', [soundFile], { stdio: 'ignore' });\n }\n }\n } catch {\n // Never crash on sound failure\n }\n}\n","/**\n * Stop hook (secondary)remind the user to sync their MaxsimCLI config to\n * version control after a Claude session ends.\n *\n * Behaviour:\n * - Checks whether the project has uncommitted changes in .claude/maxsim/.\n * - If there are dirty tracked files, emits a JSON block that adds a short\n * reminder to Claude's context.\n * - Runs fast (<50 ms): one `git status --short` call, no network I/O.\n * - Always exits 0 — never blocks the user.\n */\n\nimport * as path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { readStdinJson, CLAUDE_DIR } from './shared.js';\n\ninterface StopInput {\n cwd?: string;\n session_id?: string;\n [key: string]: unknown;\n}\n\n/** Returns true if there are modified/untracked files under .claude/maxsim/. */\nfunction hasUncommittedMaxsimChanges(projectDir: string): boolean {\n try {\n const maxsimDir = path.join(CLAUDE_DIR, 'maxsim');\n const result = spawnSync(\n 'git',\n ['status', '--short', '--', maxsimDir],\n {\n cwd: projectDir,\n encoding: 'utf8',\n timeout: 3000,\n stdio: ['ignore', 'pipe', 'ignore'],\n windowsHide: true,\n },\n );\n if (result.status !== 0) return false;\n return (result.stdout ?? '').trim().length > 0;\n } catch {\n return false;\n }\n}\n\nreadStdinJson<StopInput>((input) => {\n const projectDir = input.cwd ?? process.cwd();\n\n if (hasUncommittedMaxsimChanges(projectDir)) {\n process.stdout.write(\n JSON.stringify({\n additionalContext:\n 'Reminder: you have uncommitted MaxsimCLI changes in .claude/maxsim/. ' +\n 'Consider running `git add .claude/maxsim && git commit -m \"chore: sync maxsim config\"` ' +\n 'to keep your phase plans and config under version control.',\n }) + '\\n',\n );\n }\n\n process.exit(0);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,SAAgB,cAAiB,UAAmC;CAClE,IAAI,QAAQ;AACZ,SAAQ,MAAM,YAAY,OAAO;AACjC,SAAQ,MAAM,GAAG,SAAS,UAAkB;AAC1C,WAAS;GACT;AACF,SAAQ,MAAM,GAAG,aAAa;AAC5B,MAAI;AAEF,YADa,KAAK,MAAM,MAAM,CAChB;UACR;AACN,WAAQ,KAAK,EAAE;;GAEjB;;AAGJ,MAAa,aAAa;;;;;;;;;;;;;;;;ACE1B,SAAS,4BAA4B,YAA6B;AAChE,KAAI;EAEF,MAAM,2CACJ,OACA;GAAC;GAAU;GAAW;GAHNA,UAAK,KAAK,YAAY,SAAS;GAGT,EACtC;GACE,KAAK;GACL,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,aAAa;GACd,CACF;AACD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,UAAQ,OAAO,UAAU,IAAI,MAAM,CAAC,SAAS;SACvC;AACN,SAAO;;;AAIX,eAA0B,UAAU;AAGlC,KAAI,4BAFe,MAAM,OAAO,QAAQ,KAAK,CAEF,CACzC,SAAQ,OAAO,MACb,KAAK,UAAU,EACb,mBACE,4NAGH,CAAC,GAAG,KACN;AAGH,SAAQ,KAAK,EAAE;EACf"}