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