maxsimcli 4.16.0 → 5.0.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 (37) hide show
  1. package/dist/assets/CHANGELOG.md +7 -0
  2. package/dist/assets/hooks/maxsim-check-update.cjs.map +1 -1
  3. package/dist/assets/hooks/maxsim-notification-sound.cjs +70 -0
  4. package/dist/assets/hooks/maxsim-notification-sound.cjs.map +1 -0
  5. package/dist/assets/hooks/maxsim-statusline.cjs.map +1 -1
  6. package/dist/assets/hooks/maxsim-stop-sound.cjs +70 -0
  7. package/dist/assets/hooks/maxsim-stop-sound.cjs.map +1 -0
  8. package/dist/assets/hooks/maxsim-sync-reminder.cjs.map +1 -1
  9. package/dist/assets/templates/agents/executor.md +2 -2
  10. package/dist/assets/templates/agents/planner.md +1 -1
  11. package/dist/assets/templates/agents/verifier.md +1 -1
  12. package/dist/assets/templates/commands/maxsim/quick.md +2 -2
  13. package/dist/assets/templates/skills/github-artifact-protocol/SKILL.md +8 -8
  14. package/dist/assets/templates/skills/github-tools-guide/SKILL.md +96 -0
  15. package/dist/assets/templates/workflows/execute-plan.md +82 -68
  16. package/dist/assets/templates/workflows/execute.md +51 -36
  17. package/dist/assets/templates/workflows/go.md +21 -16
  18. package/dist/assets/templates/workflows/init-existing.md +13 -16
  19. package/dist/assets/templates/workflows/new-milestone.md +23 -1
  20. package/dist/assets/templates/workflows/new-project.md +13 -16
  21. package/dist/assets/templates/workflows/plan-create.md +16 -24
  22. package/dist/assets/templates/workflows/plan-discuss.md +6 -8
  23. package/dist/assets/templates/workflows/plan-research.md +7 -6
  24. package/dist/assets/templates/workflows/plan.md +21 -14
  25. package/dist/assets/templates/workflows/progress.md +45 -20
  26. package/dist/assets/templates/workflows/quick.md +21 -13
  27. package/dist/assets/templates/workflows/sdd.md +13 -11
  28. package/dist/assets/templates/workflows/verify-phase.md +77 -58
  29. package/dist/cli.cjs +4410 -2265
  30. package/dist/cli.cjs.map +1 -1
  31. package/dist/install.cjs +60 -37
  32. package/dist/install.cjs.map +1 -1
  33. package/package.json +2 -4
  34. package/dist/assets/templates/workflows/discovery-phase.md +0 -292
  35. package/dist/assets/templates/workflows/execute-phase.md +0 -752
  36. package/dist/mcp-server.cjs +0 -41031
  37. package/dist/mcp-server.cjs.map +0 -1
@@ -1,3 +1,10 @@
1
+ # [4.16.0](https://github.com/maystudios/maxsimcli/compare/v4.15.4...v4.16.0) (2026-03-12)
2
+
3
+
4
+ ### Features
5
+
6
+ * make GitHub Issues single source of truth for task/plan tracking ([d8e7191](https://github.com/maystudios/maxsimcli/commit/d8e7191467b12b2ba6c147a65f5e6539f77470ad))
7
+
1
8
  ## [4.15.4](https://github.com/maystudios/maxsimcli/compare/v4.15.3...v4.15.4) (2026-03-12)
2
9
 
3
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"maxsim-check-update.cjs","names":["path","fs","os"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-check-update.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","#!/usr/bin/env node\n/**\n * Check for MAXSIM updates in background, write result to cache.\n * Called by SessionStart hook - runs once per session.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\nimport { spawn } from 'node:child_process';\nimport { CLAUDE_DIR } from './shared';\n\nexport interface UpdateCheckResult {\n update_available: boolean;\n installed: string;\n latest: string;\n checked: number;\n}\n\nexport interface CheckForUpdateOptions {\n homeDir: string;\n cwd: string;\n}\n\nexport function checkForUpdate(options: CheckForUpdateOptions): void {\n const { homeDir, cwd } = options;\n const cacheDir = path.join(homeDir, CLAUDE_DIR, 'cache');\n const cacheFile = path.join(cacheDir, 'maxsim-update-check.json');\n\n // VERSION file locations (check project first, then global)\n const projectVersionFile = path.join(cwd, CLAUDE_DIR, 'maxsim', 'VERSION');\n const globalVersionFile = path.join(homeDir, CLAUDE_DIR, 'maxsim', 'VERSION');\n\n // Ensure cache directory exists\n if (!fs.existsSync(cacheDir)) {\n fs.mkdirSync(cacheDir, { recursive: true });\n }\n\n // Run check in background (spawn background process, windowsHide prevents console flash)\n const child = spawn(process.execPath, ['-e', `\n const fs = require('fs');\n const { execSync } = require('child_process');\n\n const cacheFile = ${JSON.stringify(cacheFile)};\n const projectVersionFile = ${JSON.stringify(projectVersionFile)};\n const globalVersionFile = ${JSON.stringify(globalVersionFile)};\n\n // Check project directory first (local install), then global\n let installed = '0.0.0';\n try {\n if (fs.existsSync(projectVersionFile)) {\n installed = fs.readFileSync(projectVersionFile, 'utf8').trim();\n } else if (fs.existsSync(globalVersionFile)) {\n installed = fs.readFileSync(globalVersionFile, 'utf8').trim();\n }\n } catch (e) {}\n\n let latest = null;\n try {\n latest = execSync('npm view maxsimcli version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();\n } catch (e) {}\n\n const result = {\n update_available: latest && installed !== latest,\n installed,\n latest: latest || 'unknown',\n checked: Math.floor(Date.now() / 1000)\n };\n\n fs.writeFileSync(cacheFile, JSON.stringify(result));\n`], {\n stdio: 'ignore',\n windowsHide: true,\n detached: true,\n });\n\n child.unref();\n}\n\n/**\n * Create a backup of the current MAXSIM installation before an update.\n * Called by the installer (not by the SessionStart hook).\n *\n * @param cwd - The project working directory containing .claude/\n * @returns The backup directory path on success, null on failure.\n */\nexport function createBackupBeforeUpdate(cwd: string): string | null {\n try {\n const sourceDir = path.join(cwd, CLAUDE_DIR);\n const backupDir = path.join(sourceDir, 'maxsim-backup');\n\n fs.mkdirSync(backupDir, { recursive: true });\n\n // Key directories to back up\n const dirsToBackup = [\n 'commands/maxsim',\n 'maxsim',\n 'hooks',\n 'agents',\n 'skills',\n ];\n\n for (const relDir of dirsToBackup) {\n const src = path.join(sourceDir, relDir);\n if (!fs.existsSync(src)) continue;\n\n const dest = path.join(backupDir, relDir);\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.cpSync(src, dest, { recursive: true });\n }\n\n // Write backup metadata\n let version = 'unknown';\n const versionFile = path.join(sourceDir, 'maxsim', 'VERSION');\n if (fs.existsSync(versionFile)) {\n version = fs.readFileSync(versionFile, 'utf8').trim();\n }\n\n fs.writeFileSync(\n path.join(backupDir, 'backup-meta.json'),\n JSON.stringify(\n { created: new Date().toISOString(), version },\n null,\n 2,\n ),\n );\n\n return backupDir;\n } catch {\n // Backup failure should not block the update\n return null;\n }\n}\n\n// Standalone entry\nif (require.main === module) {\n checkForUpdate({ homeDir: os.homedir(), cwd: process.cwd() });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,aAAa;;;;;;;;ACA1B,SAAgB,eAAe,SAAsC;CACnE,MAAM,EAAE,SAAS,QAAQ;CACzB,MAAM,WAAWA,UAAK,KAAK,SAAS,YAAY,QAAQ;CACxD,MAAM,YAAYA,UAAK,KAAK,UAAU,2BAA2B;CAGjE,MAAM,qBAAqBA,UAAK,KAAK,KAAK,YAAY,UAAU,UAAU;CAC1E,MAAM,oBAAoBA,UAAK,KAAK,SAAS,YAAY,UAAU,UAAU;AAG7E,KAAI,CAACC,QAAG,WAAW,SAAS,CAC1B,SAAG,UAAU,UAAU,EAAE,WAAW,MAAM,CAAC;AAyC7C,+BArCoB,QAAQ,UAAU,CAAC,MAAM;;;;sBAIzB,KAAK,UAAU,UAAU,CAAC;+BACjB,KAAK,UAAU,mBAAmB,CAAC;8BACpC,KAAK,UAAU,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;EAyB9D,EAAE;EACA,OAAO;EACP,aAAa;EACb,UAAU;EACX,CAAC,CAEI,OAAO;;;;;;;;;AAUf,SAAgB,yBAAyB,KAA4B;AACnE,KAAI;EACF,MAAM,YAAYD,UAAK,KAAK,KAAK,WAAW;EAC5C,MAAM,YAAYA,UAAK,KAAK,WAAW,gBAAgB;AAEvD,UAAG,UAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAW5C,OAAK,MAAM,UARU;GACnB;GACA;GACA;GACA;GACA;GACD,EAEkC;GACjC,MAAM,MAAMA,UAAK,KAAK,WAAW,OAAO;AACxC,OAAI,CAACC,QAAG,WAAW,IAAI,CAAE;GAEzB,MAAM,OAAOD,UAAK,KAAK,WAAW,OAAO;AACzC,WAAG,UAAUA,UAAK,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACrD,WAAG,OAAO,KAAK,MAAM,EAAE,WAAW,MAAM,CAAC;;EAI3C,IAAI,UAAU;EACd,MAAM,cAAcA,UAAK,KAAK,WAAW,UAAU,UAAU;AAC7D,MAAIC,QAAG,WAAW,YAAY,CAC5B,WAAUA,QAAG,aAAa,aAAa,OAAO,CAAC,MAAM;AAGvD,UAAG,cACDD,UAAK,KAAK,WAAW,mBAAmB,EACxC,KAAK,UACH;GAAE,0BAAS,IAAI,MAAM,EAAC,aAAa;GAAE;GAAS,EAC9C,MACA,EACD,CACF;AAED,SAAO;SACD;AAEN,SAAO;;;AAKX,IAAI,QAAQ,SAAS,OACnB,gBAAe;CAAE,SAASE,QAAG,SAAS;CAAE,KAAK,QAAQ,KAAK;CAAE,CAAC"}
1
+ {"version":3,"file":"maxsim-check-update.cjs","names":["path","fs","os"],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-check-update.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', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true, detached: 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 * Check for MAXSIM updates in background, write result to cache.\n * Called by SessionStart hook - runs once per session.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\nimport { spawn } from 'node:child_process';\nimport { CLAUDE_DIR } from './shared';\n\nexport interface UpdateCheckResult {\n update_available: boolean;\n installed: string;\n latest: string;\n checked: number;\n}\n\nexport interface CheckForUpdateOptions {\n homeDir: string;\n cwd: string;\n}\n\nexport function checkForUpdate(options: CheckForUpdateOptions): void {\n const { homeDir, cwd } = options;\n const cacheDir = path.join(homeDir, CLAUDE_DIR, 'cache');\n const cacheFile = path.join(cacheDir, 'maxsim-update-check.json');\n\n // VERSION file locations (check project first, then global)\n const projectVersionFile = path.join(cwd, CLAUDE_DIR, 'maxsim', 'VERSION');\n const globalVersionFile = path.join(homeDir, CLAUDE_DIR, 'maxsim', 'VERSION');\n\n // Ensure cache directory exists\n if (!fs.existsSync(cacheDir)) {\n fs.mkdirSync(cacheDir, { recursive: true });\n }\n\n // Run check in background (spawn background process, windowsHide prevents console flash)\n const child = spawn(process.execPath, ['-e', `\n const fs = require('fs');\n const { execSync } = require('child_process');\n\n const cacheFile = ${JSON.stringify(cacheFile)};\n const projectVersionFile = ${JSON.stringify(projectVersionFile)};\n const globalVersionFile = ${JSON.stringify(globalVersionFile)};\n\n // Check project directory first (local install), then global\n let installed = '0.0.0';\n try {\n if (fs.existsSync(projectVersionFile)) {\n installed = fs.readFileSync(projectVersionFile, 'utf8').trim();\n } else if (fs.existsSync(globalVersionFile)) {\n installed = fs.readFileSync(globalVersionFile, 'utf8').trim();\n }\n } catch (e) {}\n\n let latest = null;\n try {\n latest = execSync('npm view maxsimcli version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();\n } catch (e) {}\n\n const result = {\n update_available: latest && installed !== latest,\n installed,\n latest: latest || 'unknown',\n checked: Math.floor(Date.now() / 1000)\n };\n\n fs.writeFileSync(cacheFile, JSON.stringify(result));\n`], {\n stdio: 'ignore',\n windowsHide: true,\n detached: true,\n });\n\n child.unref();\n}\n\n/**\n * Create a backup of the current MAXSIM installation before an update.\n * Called by the installer (not by the SessionStart hook).\n *\n * @param cwd - The project working directory containing .claude/\n * @returns The backup directory path on success, null on failure.\n */\nexport function createBackupBeforeUpdate(cwd: string): string | null {\n try {\n const sourceDir = path.join(cwd, CLAUDE_DIR);\n const backupDir = path.join(sourceDir, 'maxsim-backup');\n\n fs.mkdirSync(backupDir, { recursive: true });\n\n // Key directories to back up\n const dirsToBackup = [\n 'commands/maxsim',\n 'maxsim',\n 'hooks',\n 'agents',\n 'skills',\n ];\n\n for (const relDir of dirsToBackup) {\n const src = path.join(sourceDir, relDir);\n if (!fs.existsSync(src)) continue;\n\n const dest = path.join(backupDir, relDir);\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.cpSync(src, dest, { recursive: true });\n }\n\n // Write backup metadata\n let version = 'unknown';\n const versionFile = path.join(sourceDir, 'maxsim', 'VERSION');\n if (fs.existsSync(versionFile)) {\n version = fs.readFileSync(versionFile, 'utf8').trim();\n }\n\n fs.writeFileSync(\n path.join(backupDir, 'backup-meta.json'),\n JSON.stringify(\n { created: new Date().toISOString(), version },\n null,\n 2,\n ),\n );\n\n return backupDir;\n } catch {\n // Backup failure should not block the update\n return null;\n }\n}\n\n// Standalone entry\nif (require.main === module) {\n checkForUpdate({ homeDir: os.homedir(), cwd: process.cwd() });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,aAAa;;;;;;;;ACA1B,SAAgB,eAAe,SAAsC;CACnE,MAAM,EAAE,SAAS,QAAQ;CACzB,MAAM,WAAWA,UAAK,KAAK,SAAS,YAAY,QAAQ;CACxD,MAAM,YAAYA,UAAK,KAAK,UAAU,2BAA2B;CAGjE,MAAM,qBAAqBA,UAAK,KAAK,KAAK,YAAY,UAAU,UAAU;CAC1E,MAAM,oBAAoBA,UAAK,KAAK,SAAS,YAAY,UAAU,UAAU;AAG7E,KAAI,CAACC,QAAG,WAAW,SAAS,CAC1B,SAAG,UAAU,UAAU,EAAE,WAAW,MAAM,CAAC;AAyC7C,+BArCoB,QAAQ,UAAU,CAAC,MAAM;;;;sBAIzB,KAAK,UAAU,UAAU,CAAC;+BACjB,KAAK,UAAU,mBAAmB,CAAC;8BACpC,KAAK,UAAU,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;EAyB9D,EAAE;EACA,OAAO;EACP,aAAa;EACb,UAAU;EACX,CAAC,CAEI,OAAO;;;;;;;;;AAUf,SAAgB,yBAAyB,KAA4B;AACnE,KAAI;EACF,MAAM,YAAYD,UAAK,KAAK,KAAK,WAAW;EAC5C,MAAM,YAAYA,UAAK,KAAK,WAAW,gBAAgB;AAEvD,UAAG,UAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAW5C,OAAK,MAAM,UARU;GACnB;GACA;GACA;GACA;GACA;GACD,EAEkC;GACjC,MAAM,MAAMA,UAAK,KAAK,WAAW,OAAO;AACxC,OAAI,CAACC,QAAG,WAAW,IAAI,CAAE;GAEzB,MAAM,OAAOD,UAAK,KAAK,WAAW,OAAO;AACzC,WAAG,UAAUA,UAAK,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACrD,WAAG,OAAO,KAAK,MAAM,EAAE,WAAW,MAAM,CAAC;;EAI3C,IAAI,UAAU;EACd,MAAM,cAAcA,UAAK,KAAK,WAAW,UAAU,UAAU;AAC7D,MAAIC,QAAG,WAAW,YAAY,CAC5B,WAAUA,QAAG,aAAa,aAAa,OAAO,CAAC,MAAM;AAGvD,UAAG,cACDD,UAAK,KAAK,WAAW,mBAAmB,EACxC,KAAK,UACH;GAAE,0BAAS,IAAI,MAAM,EAAC,aAAa;GAAE;GAAS,EAC9C,MACA,EACD,CACF;AAED,SAAO;SACD;AAEN,SAAO;;;AAKX,IAAI,QAAQ,SAAS,OACnB,gBAAe;CAAE,SAASE,QAAG,SAAS;CAAE,KAAK,QAAQ,KAAK;CAAE,CAAC"}
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
+
4
+ //#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
+ */
12
+ function readStdinJson(callback) {
13
+ let input = "";
14
+ process.stdin.setEncoding("utf8");
15
+ process.stdin.on("data", (chunk) => input += chunk);
16
+ process.stdin.on("end", () => {
17
+ try {
18
+ callback(JSON.parse(input));
19
+ } catch {
20
+ process.exit(0);
21
+ }
22
+ });
23
+ }
24
+ /**
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.
27
+ */
28
+ function playSound(type) {
29
+ 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
+ "-Command",
38
+ `(New-Object Media.SoundPlayer '${file}').PlaySync()`
39
+ ], {
40
+ stdio: "ignore",
41
+ windowsHide: true,
42
+ detached: true
43
+ }).unref();
44
+ } else if (platform === "darwin") {
45
+ const file = type === "question" ? "/System/Library/Sounds/Ping.aiff" : "/System/Library/Sounds/Glass.aiff";
46
+ const { spawn } = require("node:child_process");
47
+ spawn("afplay", [file], {
48
+ stdio: "ignore",
49
+ detached: true
50
+ }).unref();
51
+ } else process.stderr.write("\x07");
52
+ } catch {}
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/hooks/maxsim-notification-sound.ts
57
+ /**
58
+ * Notification Sound Hook — PostToolUse hook that plays a sound
59
+ * when Claude asks the user a question (AskUserQuestion tool).
60
+ */
61
+ function processNotificationSound(data) {
62
+ playSound("question");
63
+ }
64
+ if (require.main === module) readStdinJson((data) => {
65
+ processNotificationSound(data);
66
+ });
67
+
68
+ //#endregion
69
+ exports.processNotificationSound = processNotificationSound;
70
+ //# sourceMappingURL=maxsim-notification-sound.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maxsim-notification-sound.cjs","names":[],"sources":["../../../src/hooks/shared.ts","../../../src/hooks/maxsim-notification-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', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true, detached: 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 * Notification Sound Hook — PostToolUse hook that plays a sound\n * when Claude asks the user a question (AskUserQuestion tool).\n */\n\nimport { readStdinJson, playSound } from './shared';\n\ninterface NotificationSoundInput {\n tool_name?: string;\n}\n\nexport function processNotificationSound(data: NotificationSoundInput): void {\n playSound('question');\n}\n\n// Standalone entry\nif (require.main === module) {\n readStdinJson<NotificationSoundInput>((data) => {\n processNotificationSound(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;IAAY,kCAAkC,KAAK;IAAe,EACjF;IAAE,OAAO;IAAU,aAAa;IAAM,UAAU;IAAM,CACvD,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,yBAAyB,MAAoC;AAC3E,WAAU,WAAW;;AAIvB,IAAI,QAAQ,SAAS,OACnB,gBAAuC,SAAS;AAC9C,0BAAyB,KAAK;EAC9B"}
@@ -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","#!/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 }).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'] }\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'] }\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'] }\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 child = spawn(process.execPath, ['-e', script], {\n stdio: 'ignore',\n windowsHide: true,\n detached: true,\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAwFH,KAAK,UAAU,SAAS,CAAC;;qBAEpB,KAAK,UAAU,UAAU,CAAC;;;kBAG7B,KAAK,UAAU,SAAS,CAAC;;uBAEpB,KAAK,UAAU,UAAU,CAAC;;;;;;;;;;;;AAkB7C,gCALoB,QAAQ,UAAU,CAAC,MAAM,OAAO,EAAE;GACpD,OAAO;GACP,aAAa;GACb,UAAU;GACX,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":["/**\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', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true, detached: 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 }).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'] }\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'] }\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'] }\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 child = spawn(process.execPath, ['-e', script], {\n stdio: 'ignore',\n windowsHide: true,\n detached: true,\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAwFH,KAAK,UAAU,SAAS,CAAC;;qBAEpB,KAAK,UAAU,UAAU,CAAC;;;kBAG7B,KAAK,UAAU,SAAS,CAAC;;uBAEpB,KAAK,UAAU,UAAU,CAAC;;;;;;;;;;;;AAkB7C,gCALoB,QAAQ,UAAU,CAAC,MAAM,OAAO,EAAE;GACpD,OAAO;GACP,aAAa;GACb,UAAU;GACX,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"}
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
+
4
+ //#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
+ */
12
+ function readStdinJson(callback) {
13
+ let input = "";
14
+ process.stdin.setEncoding("utf8");
15
+ process.stdin.on("data", (chunk) => input += chunk);
16
+ process.stdin.on("end", () => {
17
+ try {
18
+ callback(JSON.parse(input));
19
+ } catch {
20
+ process.exit(0);
21
+ }
22
+ });
23
+ }
24
+ /**
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.
27
+ */
28
+ function playSound(type) {
29
+ 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
+ "-Command",
38
+ `(New-Object Media.SoundPlayer '${file}').PlaySync()`
39
+ ], {
40
+ stdio: "ignore",
41
+ windowsHide: true,
42
+ detached: true
43
+ }).unref();
44
+ } else if (platform === "darwin") {
45
+ const file = type === "question" ? "/System/Library/Sounds/Ping.aiff" : "/System/Library/Sounds/Glass.aiff";
46
+ const { spawn } = require("node:child_process");
47
+ spawn("afplay", [file], {
48
+ stdio: "ignore",
49
+ detached: true
50
+ }).unref();
51
+ } else process.stderr.write("\x07");
52
+ } catch {}
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/hooks/maxsim-stop-sound.ts
57
+ /**
58
+ * Stop Sound Hook — Stop event hook that plays a sound
59
+ * when Claude finishes working.
60
+ */
61
+ function processStopSound(data) {
62
+ playSound("stop");
63
+ }
64
+ if (require.main === module) readStdinJson((data) => {
65
+ processStopSound(data);
66
+ });
67
+
68
+ //#endregion
69
+ exports.processStopSound = processStopSound;
70
+ //# sourceMappingURL=maxsim-stop-sound.cjs.map
@@ -0,0 +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', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true, detached: 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;IAAY,kCAAkC,KAAK;IAAe,EACjF;IAAE,OAAO;IAAU,aAAa;IAAM,UAAU;IAAM,CACvD,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 +1 @@
1
- {"version":3,"file":"maxsim-sync-reminder.cjs","names":["path","os","fs"],"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","#!/usr/bin/env node\n/**\n * Sync Reminder Hook — PostToolUse hook that detects .planning/ file writes\n * and gently reminds the user to sync changes to GitHub Issues.\n *\n * Debounces reminders: fires on the first .planning/ write per session,\n * then every DEBOUNCE_CALLS writes thereafter.\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\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\ninterface DebounceState {\n callsSinceRemind: number;\n reminded: boolean;\n}\n\n/** Number of .planning/ writes between repeated reminders. */\nexport const DEBOUNCE_CALLS = 10;\n\nconst REMINDER_MESSAGE =\n '.planning/ files changed locally. Consider syncing to GitHub Issues when ready.';\n\nexport function processSyncReminder(\n data: SyncReminderInput,\n): SyncReminderOutput | null {\n const sessionId = data.session_id;\n const filePath = data.tool_input?.file_path;\n\n if (!sessionId || !filePath) {\n return null;\n }\n\n // Normalize path for cross-platform (Windows backslash handling)\n const normalized = path.normalize(filePath);\n\n // Check if the file is inside a .planning/ directory\n const planningSegment = `${path.sep}.planning${path.sep}`;\n const planningEnd = `${path.sep}.planning`;\n if (\n !normalized.includes(planningSegment) &&\n !normalized.endsWith(planningEnd)\n ) {\n return null;\n }\n\n // Load debounce state from temp file\n const stateFile = path.join(\n os.tmpdir(),\n `maxsim-sync-${sessionId}.json`,\n );\n\n let state: DebounceState;\n try {\n if (fs.existsSync(stateFile)) {\n state = JSON.parse(fs.readFileSync(stateFile, 'utf8')) as DebounceState;\n } else {\n state = { callsSinceRemind: 0, reminded: false };\n }\n } catch {\n state = { callsSinceRemind: 0, reminded: false };\n }\n\n state.callsSinceRemind++;\n\n // Fire reminder on first write OR after debounce interval expires\n if (!state.reminded || state.callsSinceRemind >= DEBOUNCE_CALLS) {\n state.callsSinceRemind = 0;\n state.reminded = true;\n\n try {\n fs.writeFileSync(stateFile, JSON.stringify(state));\n } catch {\n // Silent fail -- never block hook execution\n }\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext: REMINDER_MESSAGE,\n },\n };\n }\n\n // Not time for a reminder yet\n try {\n fs.writeFileSync(stateFile, JSON.stringify(state));\n } catch {\n // Silent fail -- never block hook execution\n }\n\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;;;;;;;;;;;;;ACaJ,MAAa,iBAAiB;AAE9B,MAAM,mBACJ;AAEF,SAAgB,oBACd,MAC2B;CAC3B,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,KAAK,YAAY;AAElC,KAAI,CAAC,aAAa,CAAC,SACjB,QAAO;CAIT,MAAM,aAAaA,UAAK,UAAU,SAAS;CAG3C,MAAM,kBAAkB,GAAGA,UAAK,IAAI,WAAWA,UAAK;CACpD,MAAM,cAAc,GAAGA,UAAK,IAAI;AAChC,KACE,CAAC,WAAW,SAAS,gBAAgB,IACrC,CAAC,WAAW,SAAS,YAAY,CAEjC,QAAO;CAIT,MAAM,YAAYA,UAAK,KACrBC,QAAG,QAAQ,EACX,eAAe,UAAU,OAC1B;CAED,IAAI;AACJ,KAAI;AACF,MAAIC,QAAG,WAAW,UAAU,CAC1B,SAAQ,KAAK,MAAMA,QAAG,aAAa,WAAW,OAAO,CAAC;MAEtD,SAAQ;GAAE,kBAAkB;GAAG,UAAU;GAAO;SAE5C;AACN,UAAQ;GAAE,kBAAkB;GAAG,UAAU;GAAO;;AAGlD,OAAM;AAGN,KAAI,CAAC,MAAM,YAAY,MAAM,oBAAoB,gBAAgB;AAC/D,QAAM,mBAAmB;AACzB,QAAM,WAAW;AAEjB,MAAI;AACF,WAAG,cAAc,WAAW,KAAK,UAAU,MAAM,CAAC;UAC5C;AAIR,SAAO,EACL,oBAAoB;GAClB,eAAe;GACf,mBAAmB;GACpB,EACF;;AAIH,KAAI;AACF,UAAG,cAAc,WAAW,KAAK,UAAU,MAAM,CAAC;SAC5C;AAIR,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","os","fs"],"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', '-Command', `(New-Object Media.SoundPlayer '${file}').PlaySync()`],\n { stdio: 'ignore', windowsHide: true, detached: 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 Hook — PostToolUse hook that detects .planning/ file writes\n * and gently reminds the user to sync changes to GitHub Issues.\n *\n * Debounces reminders: fires on the first .planning/ write per session,\n * then every DEBOUNCE_CALLS writes thereafter.\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\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\ninterface DebounceState {\n callsSinceRemind: number;\n reminded: boolean;\n}\n\n/** Number of .planning/ writes between repeated reminders. */\nexport const DEBOUNCE_CALLS = 10;\n\nconst REMINDER_MESSAGE =\n '.planning/ files changed locally. Consider syncing to GitHub Issues when ready.';\n\nexport function processSyncReminder(\n data: SyncReminderInput,\n): SyncReminderOutput | null {\n const sessionId = data.session_id;\n const filePath = data.tool_input?.file_path;\n\n if (!sessionId || !filePath) {\n return null;\n }\n\n // Normalize path for cross-platform (Windows backslash handling)\n const normalized = path.normalize(filePath);\n\n // Check if the file is inside a .planning/ directory\n const planningSegment = `${path.sep}.planning${path.sep}`;\n const planningEnd = `${path.sep}.planning`;\n if (\n !normalized.includes(planningSegment) &&\n !normalized.endsWith(planningEnd)\n ) {\n return null;\n }\n\n // Load debounce state from temp file\n const stateFile = path.join(\n os.tmpdir(),\n `maxsim-sync-${sessionId}.json`,\n );\n\n let state: DebounceState;\n try {\n if (fs.existsSync(stateFile)) {\n state = JSON.parse(fs.readFileSync(stateFile, 'utf8')) as DebounceState;\n } else {\n state = { callsSinceRemind: 0, reminded: false };\n }\n } catch {\n state = { callsSinceRemind: 0, reminded: false };\n }\n\n state.callsSinceRemind++;\n\n // Fire reminder on first write OR after debounce interval expires\n if (!state.reminded || state.callsSinceRemind >= DEBOUNCE_CALLS) {\n state.callsSinceRemind = 0;\n state.reminded = true;\n\n try {\n fs.writeFileSync(stateFile, JSON.stringify(state));\n } catch {\n // Silent fail -- never block hook execution\n }\n\n return {\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext: REMINDER_MESSAGE,\n },\n };\n }\n\n // Not time for a reminder yet\n try {\n fs.writeFileSync(stateFile, JSON.stringify(state));\n } catch {\n // Silent fail -- never block hook execution\n }\n\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;;;;;;;;;;;;;ACaJ,MAAa,iBAAiB;AAE9B,MAAM,mBACJ;AAEF,SAAgB,oBACd,MAC2B;CAC3B,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,KAAK,YAAY;AAElC,KAAI,CAAC,aAAa,CAAC,SACjB,QAAO;CAIT,MAAM,aAAaA,UAAK,UAAU,SAAS;CAG3C,MAAM,kBAAkB,GAAGA,UAAK,IAAI,WAAWA,UAAK;CACpD,MAAM,cAAc,GAAGA,UAAK,IAAI;AAChC,KACE,CAAC,WAAW,SAAS,gBAAgB,IACrC,CAAC,WAAW,SAAS,YAAY,CAEjC,QAAO;CAIT,MAAM,YAAYA,UAAK,KACrBC,QAAG,QAAQ,EACX,eAAe,UAAU,OAC1B;CAED,IAAI;AACJ,KAAI;AACF,MAAIC,QAAG,WAAW,UAAU,CAC1B,SAAQ,KAAK,MAAMA,QAAG,aAAa,WAAW,OAAO,CAAC;MAEtD,SAAQ;GAAE,kBAAkB;GAAG,UAAU;GAAO;SAE5C;AACN,UAAQ;GAAE,kBAAkB;GAAG,UAAU;GAAO;;AAGlD,OAAM;AAGN,KAAI,CAAC,MAAM,YAAY,MAAM,oBAAoB,gBAAgB;AAC/D,QAAM,mBAAmB;AACzB,QAAM,WAAW;AAEjB,MAAI;AACF,WAAG,cAAc,WAAW,KAAK,UAAU,MAAM,CAAC;UAC5C;AAIR,SAAO,EACL,oBAAoB;GAClB,eAAe;GACf,mBAAmB;GACpB,EACF;;AAIH,KAAI;AACF,UAAG,cAAc,WAAW,KAAK,UAAU,MAAM,CAAC;SAC5C;AAIR,QAAO;;AAIT,IAAI,QAAQ,SAAS,OACnB,gBAAkC,SAAS;CACzC,MAAM,SAAS,oBAAoB,KAAK;AACxC,KAAI,OACF,SAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;EAE9C"}
@@ -52,7 +52,7 @@ For each task in the plan:
52
52
 
53
53
  When creating the summary, populate the `## Requirement Evidence` section.
54
54
 
55
- The summary is posted as a GitHub comment by the orchestrator via `mcp_post_comment` with `type: 'summary'`. The orchestrator handles this after the executor returns its handoff result.
55
+ The summary is posted as a GitHub comment by the orchestrator via `github post-comment --type summary`. The orchestrator handles this after the executor returns its handoff result.
56
56
 
57
57
  Note: The orchestrator handles board transitions. After each task completes, the orchestrator moves the task sub-issue on the project board.
58
58
 
@@ -88,7 +88,7 @@ When running in a worktree (orchestrator passes `<constraints>` block with workt
88
88
 
89
89
  1. **Do NOT modify** `.planning/STATE.md` or `.planning/ROADMAP.md` -- the orchestrator handles all metadata
90
90
  2. **Do NOT run** `state advance-plan`, `state update-progress`, or `roadmap update-plan-progress` -- skip these steps
91
- 3. **Return summary content** in your handoff result -- the orchestrator posts it as a GitHub comment via `mcp_post_comment` with `type: 'summary'`
91
+ 3. **Return summary content** in your handoff result -- the orchestrator posts it as a GitHub comment via `github post-comment --type summary`
92
92
  4. **Commit code normally** -- commits go to the worktree branch, orchestrator merges after wave completion
93
93
  5. **Skip** the `update_current_position`, `update_session_continuity`, `update_roadmap`, and `extract_decisions_and_issues` steps -- orchestrator handles these centrally
94
94
 
@@ -15,7 +15,7 @@ available_skills:
15
15
 
16
16
  You are a plan creator. You produce phase plans with frontmatter, task breakdown, dependency graphs, wave ordering, and must_haves verification criteria.
17
17
 
18
- The plan is posted as a GitHub Issue comment via `mcp_post_plan_comment` by the orchestrator after the planner returns its output. Task sub-issues are created via `mcp_batch_create_tasks` after the plan is posted.
18
+ The plan is posted as a GitHub Issue comment via `github post-plan-comment` by the orchestrator after the planner returns its output. Task sub-issues are created via `github batch-create-tasks` after the plan is posted.
19
19
 
20
20
  Context and research input is provided from GitHub Issue comments (type: `context` and type: `research`) -- the orchestrator supplies these in the spawn prompt.
21
21
 
@@ -89,4 +89,4 @@ Return results using the handoff-contract format (loaded via skills). Include:
89
89
  - Evidence blocks for every criterion
90
90
  - Findings summary with counts (X pass, Y fail, Z warnings)
91
91
 
92
- Verification results are posted as a GitHub comment by the orchestrator via `mcp_post_comment` with `type: 'verification'`.
92
+ Verification results are posted as a GitHub comment by the orchestrator via `github post-comment --type verification`.
@@ -17,14 +17,14 @@ Execute small, ad-hoc tasks with MAXSIM guarantees (atomic commits, STATE.md tra
17
17
 
18
18
  Quick mode is the same system with a shorter path:
19
19
  - Spawns planner (quick mode) + executor(s)
20
- - Quick tasks live in `.planning/quick/` separate from planned phases
20
+ - Quick tasks are tracked via GitHub Issues (label: "quick"), separate from planned phases
21
21
  - Updates STATE.md "Quick Tasks Completed" table (NOT ROADMAP.md)
22
22
 
23
23
  **Default:** Skips research, plan-checker, verifier. Use when you know exactly what to do.
24
24
 
25
25
  **`--full` flag:** Enables plan-checking (max 2 iterations) and post-execution verification. Use when you want quality guarantees without full milestone ceremony.
26
26
 
27
- **`--todo` flag:** Enters todo management mode. List, capture, complete, and triage todos without executing tasks. "Save for later" creates a local todo + best-effort GitHub Issue with 'todo' label.
27
+ **`--todo` flag:** Enters todo management mode. List, capture, complete, and triage todos without executing tasks. "Save for later" creates a GitHub Issue with 'todo' label (primary) with local file cache.
28
28
  </objective>
29
29
 
30
30
  <execution_context>
@@ -11,13 +11,13 @@ All MAXSIM artifacts are stored as GitHub Issue comments with type metadata:
11
11
 
12
12
  | Artifact | Comment Type | Tool | Format |
13
13
  |----------|-------------|------|--------|
14
- | Context decisions | `context` | `mcp_post_comment` | `<!-- maxsim:type=context -->` header |
15
- | Research findings | `research` | `mcp_post_comment` | `<!-- maxsim:type=research -->` header |
16
- | Plan content | (plan header) | `mcp_post_plan_comment` | `<!-- maxsim:type=plan -->` header |
17
- | Summary | `summary` | `mcp_post_comment` | `<!-- maxsim:type=summary -->` header |
18
- | Verification | `verification` | `mcp_post_comment` | `<!-- maxsim:type=verification -->` header |
19
- | UAT | `uat` | `mcp_post_comment` | `<!-- maxsim:type=uat -->` header |
20
- | Completion | (structured) | `mcp_post_completion` | Commit SHA + files |
14
+ | Context decisions | `context` | `github post-comment --type context` | `<!-- maxsim:type=context -->` header |
15
+ | Research findings | `research` | `github post-comment --type research` | `<!-- maxsim:type=research -->` header |
16
+ | Plan content | (plan header) | `github post-plan-comment` | `<!-- maxsim:type=plan -->` header |
17
+ | Summary | `summary` | `github post-comment --type summary` | `<!-- maxsim:type=summary -->` header |
18
+ | Verification | `verification` | `github post-comment --type verification` | `<!-- maxsim:type=verification -->` header |
19
+ | UAT | `uat` | `github post-comment --type uat` | `<!-- maxsim:type=uat -->` header |
20
+ | Completion | (structured) | `github post-completion` | Commit SHA + files |
21
21
 
22
22
  ### Issue Lifecycle State Machine
23
23
 
@@ -37,7 +37,7 @@ Done --> In Progress (review failed, reopened)
37
37
  ### Write Order (WIRE-01)
38
38
 
39
39
  1. Build content in memory
40
- 2. POST to GitHub via MCP tool
40
+ 2. POST to GitHub via CLI command
41
41
  3. If successful, operation succeeds
42
42
  4. If failed, operation aborts entirely -- no partial state
43
43
 
@@ -0,0 +1,96 @@
1
+ # Skill: GitHub Tools Guide
2
+
3
+ ## Trigger
4
+ When interacting with GitHub Issues, project boards, or progress tracking in MAXSIM.
5
+
6
+ ## CLI Invocation
7
+
8
+ All GitHub operations use the MAXSIM CLI tools router:
9
+
10
+ ```bash
11
+ node ~/.claude/maxsim/bin/maxsim-tools.cjs github <command> [--flag value] [--raw]
12
+ ```
13
+
14
+ Add `--raw` to get machine-readable JSON output (no formatting).
15
+
16
+ ## Command Reference
17
+
18
+ ### Setup
19
+ | Command | Description |
20
+ |---------|-------------|
21
+ | `github setup [--milestone-title "T"]` | Set up GitHub integration: board, labels, milestone, templates |
22
+
23
+ ### Phase Lifecycle
24
+ | Command | Description |
25
+ |---------|-------------|
26
+ | `github create-phase --phase-number "01" --phase-name "Name" --goal "Goal" [--requirements "R1,R2"] [--success-criteria "SC1,SC2"]` | Create phase issue + add to board + set "To Do" |
27
+ | `github create-task --phase-number "01" --task-id "T1" --title "Title" --body "Body" --parent-issue-number N` | Create task sub-issue |
28
+ | `github batch-create-tasks --phase-number "01" --parent-issue-number N --tasks-json '[...]'` | Batch create tasks with rollback |
29
+ | `github post-plan-comment --phase-issue-number N --plan-number "01" --plan-content "..." [--plan-content-file F]` | Post plan comment on phase issue |
30
+
31
+ ### Comments
32
+ | Command | Description |
33
+ |---------|-------------|
34
+ | `github post-comment --issue-number N --body "..." [--body-file F] [--type TYPE]` | Post comment (types: research, context, summary, verification, uat, general) |
35
+ | `github post-completion --issue-number N --commit-sha "SHA" --files-changed "a.ts,b.ts"` | Post completion comment |
36
+
37
+ ### Issue Operations
38
+ | Command | Description |
39
+ |---------|-------------|
40
+ | `github get-issue N [--comments]` | Get issue details (with optional comments) |
41
+ | `github list-sub-issues N` | List sub-issues of a phase issue |
42
+ | `github close-issue N [--reason "..."] [--state-reason completed\|not_planned]` | Close issue |
43
+ | `github reopen-issue N` | Reopen closed issue |
44
+ | `github bounce-issue --issue-number N --reason "feedback"` | Bounce to In Progress with feedback |
45
+ | `github move-issue --project-number P --item-id ID --status "STATUS"` | Move to board column (To Do, In Progress, In Review, Done) |
46
+ | `github detect-external-edits --phase-number "01"` | Check body hash mismatch |
47
+
48
+ ### Board Operations
49
+ | Command | Description |
50
+ |---------|-------------|
51
+ | `github query-board --project-number N [--status "STATUS"] [--phase "01"]` | Query board items |
52
+ | `github add-to-board --project-number N --issue-number M` | Add issue to board |
53
+ | `github search-issues [--labels "L1,L2"] [--state open\|closed\|all] [--query "text"]` | Search issues |
54
+ | `github sync-check` | Verify local mapping matches GitHub state |
55
+
56
+ ### Progress
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `github phase-progress --phase-issue-number N` | Phase progress from sub-issues |
60
+ | `github all-progress` | All phases progress overview |
61
+ | `github detect-interrupted --phase-issue-number N` | Detect interrupted phase |
62
+
63
+ ### Todos
64
+ | Command | Description |
65
+ |---------|-------------|
66
+ | `github add-todo --title "T" [--description "D"] [--area A] [--phase P]` | Create todo issue |
67
+ | `github complete-todo --todo-id "file.md" [--github-issue-number N]` | Complete todo |
68
+ | `github list-todos [--area A] [--status pending\|completed\|all]` | List todos |
69
+
70
+ ### Convenience
71
+ | Command | Description |
72
+ |---------|-------------|
73
+ | `github status` | Combined progress + interrupted + board overview |
74
+ | `github sync` | Sync check + repair actions |
75
+ | `github overview` | Board summary grouped by column |
76
+
77
+ ## Text Arguments
78
+
79
+ For large text (body, plan-content), write to a tmpfile and use `--*-file`:
80
+
81
+ ```bash
82
+ TMPFILE=$(mktemp)
83
+ cat > "$TMPFILE" << 'EOF'
84
+ Multi-line content here...
85
+ EOF
86
+ node ~/.claude/maxsim/bin/maxsim-tools.cjs github post-comment --issue-number 42 --body-file "$TMPFILE" --type summary
87
+ ```
88
+
89
+ ## Output Format
90
+
91
+ All commands return JSON when `--raw` is passed:
92
+ ```json
93
+ {"ok": true, "result": "...", "rawValue": {...}}
94
+ ```
95
+
96
+ Without `--raw`, returns human-readable formatted output.