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.
- package/dist/assets/CHANGELOG.md +7 -0
- package/dist/assets/hooks/maxsim-check-update.cjs.map +1 -1
- package/dist/assets/hooks/maxsim-notification-sound.cjs +70 -0
- package/dist/assets/hooks/maxsim-notification-sound.cjs.map +1 -0
- package/dist/assets/hooks/maxsim-statusline.cjs.map +1 -1
- package/dist/assets/hooks/maxsim-stop-sound.cjs +70 -0
- package/dist/assets/hooks/maxsim-stop-sound.cjs.map +1 -0
- package/dist/assets/hooks/maxsim-sync-reminder.cjs.map +1 -1
- package/dist/assets/templates/agents/executor.md +2 -2
- package/dist/assets/templates/agents/planner.md +1 -1
- package/dist/assets/templates/agents/verifier.md +1 -1
- package/dist/assets/templates/commands/maxsim/quick.md +2 -2
- package/dist/assets/templates/skills/github-artifact-protocol/SKILL.md +8 -8
- package/dist/assets/templates/skills/github-tools-guide/SKILL.md +96 -0
- package/dist/assets/templates/workflows/execute-plan.md +82 -68
- package/dist/assets/templates/workflows/execute.md +51 -36
- package/dist/assets/templates/workflows/go.md +21 -16
- package/dist/assets/templates/workflows/init-existing.md +13 -16
- package/dist/assets/templates/workflows/new-milestone.md +23 -1
- package/dist/assets/templates/workflows/new-project.md +13 -16
- package/dist/assets/templates/workflows/plan-create.md +16 -24
- package/dist/assets/templates/workflows/plan-discuss.md +6 -8
- package/dist/assets/templates/workflows/plan-research.md +7 -6
- package/dist/assets/templates/workflows/plan.md +21 -14
- package/dist/assets/templates/workflows/progress.md +45 -20
- package/dist/assets/templates/workflows/quick.md +21 -13
- package/dist/assets/templates/workflows/sdd.md +13 -11
- package/dist/assets/templates/workflows/verify-phase.md +77 -58
- package/dist/cli.cjs +4410 -2265
- package/dist/cli.cjs.map +1 -1
- package/dist/install.cjs +60 -37
- package/dist/install.cjs.map +1 -1
- package/package.json +2 -4
- package/dist/assets/templates/workflows/discovery-phase.md +0 -292
- package/dist/assets/templates/workflows/execute-phase.md +0 -752
- package/dist/mcp-server.cjs +0 -41031
- package/dist/mcp-server.cjs.map +0 -1
package/dist/assets/CHANGELOG.md
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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` | `
|
|
15
|
-
| Research findings | `research` | `
|
|
16
|
-
| Plan content | (plan header) | `
|
|
17
|
-
| Summary | `summary` | `
|
|
18
|
-
| Verification | `verification` | `
|
|
19
|
-
| UAT | `uat` | `
|
|
20
|
-
| Completion | (structured) | `
|
|
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
|
|
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.
|