cc4pm 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.claude-plugin/README.md +17 -0
  2. package/.claude-plugin/plugin.json +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +157 -0
  5. package/README.zh-CN.md +134 -0
  6. package/contexts/dev.md +20 -0
  7. package/contexts/research.md +26 -0
  8. package/contexts/review.md +22 -0
  9. package/examples/CLAUDE.md +100 -0
  10. package/examples/statusline.json +19 -0
  11. package/examples/user-CLAUDE.md +109 -0
  12. package/install.sh +17 -0
  13. package/manifests/install-components.json +173 -0
  14. package/manifests/install-modules.json +335 -0
  15. package/manifests/install-profiles.json +75 -0
  16. package/package.json +117 -0
  17. package/schemas/ecc-install-config.schema.json +58 -0
  18. package/schemas/hooks.schema.json +197 -0
  19. package/schemas/install-components.schema.json +56 -0
  20. package/schemas/install-modules.schema.json +105 -0
  21. package/schemas/install-profiles.schema.json +45 -0
  22. package/schemas/install-state.schema.json +210 -0
  23. package/schemas/package-manager.schema.json +23 -0
  24. package/schemas/plugin.schema.json +58 -0
  25. package/scripts/ci/catalog.js +83 -0
  26. package/scripts/ci/validate-agents.js +81 -0
  27. package/scripts/ci/validate-commands.js +135 -0
  28. package/scripts/ci/validate-hooks.js +239 -0
  29. package/scripts/ci/validate-install-manifests.js +211 -0
  30. package/scripts/ci/validate-no-personal-paths.js +63 -0
  31. package/scripts/ci/validate-rules.js +81 -0
  32. package/scripts/ci/validate-skills.js +54 -0
  33. package/scripts/claw.js +468 -0
  34. package/scripts/doctor.js +110 -0
  35. package/scripts/ecc.js +194 -0
  36. package/scripts/hooks/auto-tmux-dev.js +88 -0
  37. package/scripts/hooks/check-console-log.js +71 -0
  38. package/scripts/hooks/check-hook-enabled.js +12 -0
  39. package/scripts/hooks/cost-tracker.js +78 -0
  40. package/scripts/hooks/doc-file-warning.js +63 -0
  41. package/scripts/hooks/evaluate-session.js +100 -0
  42. package/scripts/hooks/insaits-security-monitor.py +269 -0
  43. package/scripts/hooks/insaits-security-wrapper.js +88 -0
  44. package/scripts/hooks/post-bash-build-complete.js +27 -0
  45. package/scripts/hooks/post-bash-pr-created.js +36 -0
  46. package/scripts/hooks/post-edit-console-warn.js +54 -0
  47. package/scripts/hooks/post-edit-format.js +109 -0
  48. package/scripts/hooks/post-edit-typecheck.js +96 -0
  49. package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
  50. package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
  51. package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
  52. package/scripts/hooks/pre-compact.js +48 -0
  53. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  54. package/scripts/hooks/quality-gate.js +168 -0
  55. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  56. package/scripts/hooks/run-with-flags.js +120 -0
  57. package/scripts/hooks/session-end-marker.js +15 -0
  58. package/scripts/hooks/session-end.js +299 -0
  59. package/scripts/hooks/session-start.js +97 -0
  60. package/scripts/hooks/suggest-compact.js +80 -0
  61. package/scripts/install-apply.js +137 -0
  62. package/scripts/install-plan.js +254 -0
  63. package/scripts/lib/hook-flags.js +74 -0
  64. package/scripts/lib/install/apply.js +23 -0
  65. package/scripts/lib/install/config.js +82 -0
  66. package/scripts/lib/install/request.js +113 -0
  67. package/scripts/lib/install/runtime.js +42 -0
  68. package/scripts/lib/install-executor.js +605 -0
  69. package/scripts/lib/install-lifecycle.js +763 -0
  70. package/scripts/lib/install-manifests.js +305 -0
  71. package/scripts/lib/install-state.js +120 -0
  72. package/scripts/lib/install-targets/antigravity-project.js +9 -0
  73. package/scripts/lib/install-targets/claude-home.js +10 -0
  74. package/scripts/lib/install-targets/codex-home.js +10 -0
  75. package/scripts/lib/install-targets/cursor-project.js +10 -0
  76. package/scripts/lib/install-targets/helpers.js +89 -0
  77. package/scripts/lib/install-targets/opencode-home.js +10 -0
  78. package/scripts/lib/install-targets/registry.js +64 -0
  79. package/scripts/lib/orchestration-session.js +299 -0
  80. package/scripts/lib/package-manager.d.ts +119 -0
  81. package/scripts/lib/package-manager.js +431 -0
  82. package/scripts/lib/project-detect.js +428 -0
  83. package/scripts/lib/resolve-formatter.js +185 -0
  84. package/scripts/lib/session-adapters/canonical-session.js +138 -0
  85. package/scripts/lib/session-adapters/claude-history.js +149 -0
  86. package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
  87. package/scripts/lib/session-adapters/registry.js +111 -0
  88. package/scripts/lib/session-aliases.d.ts +136 -0
  89. package/scripts/lib/session-aliases.js +481 -0
  90. package/scripts/lib/session-manager.d.ts +131 -0
  91. package/scripts/lib/session-manager.js +464 -0
  92. package/scripts/lib/shell-split.js +86 -0
  93. package/scripts/lib/skill-improvement/amendify.js +89 -0
  94. package/scripts/lib/skill-improvement/evaluate.js +59 -0
  95. package/scripts/lib/skill-improvement/health.js +118 -0
  96. package/scripts/lib/skill-improvement/observations.js +108 -0
  97. package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
  98. package/scripts/lib/utils.d.ts +183 -0
  99. package/scripts/lib/utils.js +543 -0
  100. package/scripts/list-installed.js +90 -0
  101. package/scripts/orchestrate-codex-worker.sh +92 -0
  102. package/scripts/orchestrate-worktrees.js +108 -0
  103. package/scripts/orchestration-status.js +62 -0
  104. package/scripts/repair.js +97 -0
  105. package/scripts/session-inspect.js +150 -0
  106. package/scripts/setup-package-manager.js +204 -0
  107. package/scripts/skill-create-output.js +244 -0
  108. package/scripts/uninstall.js +96 -0
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop Hook (Session End) - Persist learnings during active sessions
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs on Stop events (after each response). Extracts a meaningful summary
8
+ * from the session transcript (via stdin JSON transcript_path) and updates a
9
+ * session file for cross-session continuity.
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const {
15
+ getSessionsDir,
16
+ getDateString,
17
+ getTimeString,
18
+ getSessionIdShort,
19
+ getProjectName,
20
+ ensureDir,
21
+ readFile,
22
+ writeFile,
23
+ runCommand,
24
+ log
25
+ } = require('../lib/utils');
26
+
27
+ const SUMMARY_START_MARKER = '<!-- ECC:SUMMARY:START -->';
28
+ const SUMMARY_END_MARKER = '<!-- ECC:SUMMARY:END -->';
29
+ const SESSION_SEPARATOR = '\n---\n';
30
+
31
+ /**
32
+ * Extract a meaningful summary from the session transcript.
33
+ * Reads the JSONL transcript and pulls out key information:
34
+ * - User messages (tasks requested)
35
+ * - Tools used
36
+ * - Files modified
37
+ */
38
+ function extractSessionSummary(transcriptPath) {
39
+ const content = readFile(transcriptPath);
40
+ if (!content) return null;
41
+
42
+ const lines = content.split('\n').filter(Boolean);
43
+ const userMessages = [];
44
+ const toolsUsed = new Set();
45
+ const filesModified = new Set();
46
+ let parseErrors = 0;
47
+
48
+ for (const line of lines) {
49
+ try {
50
+ const entry = JSON.parse(line);
51
+
52
+ // Collect user messages (first 200 chars each)
53
+ if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') {
54
+ // Support both direct content and nested message.content (Claude Code JSONL format)
55
+ const rawContent = entry.message?.content ?? entry.content;
56
+ const text = typeof rawContent === 'string'
57
+ ? rawContent
58
+ : Array.isArray(rawContent)
59
+ ? rawContent.map(c => (c && c.text) || '').join(' ')
60
+ : '';
61
+ if (text.trim()) {
62
+ userMessages.push(text.trim().slice(0, 200));
63
+ }
64
+ }
65
+
66
+ // Collect tool names and modified files (direct tool_use entries)
67
+ if (entry.type === 'tool_use' || entry.tool_name) {
68
+ const toolName = entry.tool_name || entry.name || '';
69
+ if (toolName) toolsUsed.add(toolName);
70
+
71
+ const filePath = entry.tool_input?.file_path || entry.input?.file_path || '';
72
+ if (filePath && (toolName === 'Edit' || toolName === 'Write')) {
73
+ filesModified.add(filePath);
74
+ }
75
+ }
76
+
77
+ // Extract tool uses from assistant message content blocks (Claude Code JSONL format)
78
+ if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
79
+ for (const block of entry.message.content) {
80
+ if (block.type === 'tool_use') {
81
+ const toolName = block.name || '';
82
+ if (toolName) toolsUsed.add(toolName);
83
+
84
+ const filePath = block.input?.file_path || '';
85
+ if (filePath && (toolName === 'Edit' || toolName === 'Write')) {
86
+ filesModified.add(filePath);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ } catch {
92
+ parseErrors++;
93
+ }
94
+ }
95
+
96
+ if (parseErrors > 0) {
97
+ log(`[SessionEnd] Skipped ${parseErrors}/${lines.length} unparseable transcript lines`);
98
+ }
99
+
100
+ if (userMessages.length === 0) return null;
101
+
102
+ return {
103
+ userMessages: userMessages.slice(-10), // Last 10 user messages
104
+ toolsUsed: Array.from(toolsUsed).slice(0, 20),
105
+ filesModified: Array.from(filesModified).slice(0, 30),
106
+ totalMessages: userMessages.length
107
+ };
108
+ }
109
+
110
+ // Read hook input from stdin (Claude Code provides transcript_path via stdin JSON)
111
+ const MAX_STDIN = 1024 * 1024;
112
+ let stdinData = '';
113
+ process.stdin.setEncoding('utf8');
114
+
115
+ process.stdin.on('data', chunk => {
116
+ if (stdinData.length < MAX_STDIN) {
117
+ const remaining = MAX_STDIN - stdinData.length;
118
+ stdinData += chunk.substring(0, remaining);
119
+ }
120
+ });
121
+
122
+ process.stdin.on('end', () => {
123
+ runMain();
124
+ });
125
+
126
+ function runMain() {
127
+ main().catch(err => {
128
+ console.error('[SessionEnd] Error:', err.message);
129
+ process.exit(0);
130
+ });
131
+ }
132
+
133
+ function getSessionMetadata() {
134
+ const branchResult = runCommand('git rev-parse --abbrev-ref HEAD');
135
+
136
+ return {
137
+ project: getProjectName() || 'unknown',
138
+ branch: branchResult.success ? branchResult.output : 'unknown',
139
+ worktree: process.cwd()
140
+ };
141
+ }
142
+
143
+ function extractHeaderField(header, label) {
144
+ const match = header.match(new RegExp(`\\*\\*${escapeRegExp(label)}:\\*\\*\\s*(.+)$`, 'm'));
145
+ return match ? match[1].trim() : null;
146
+ }
147
+
148
+ function buildSessionHeader(today, currentTime, metadata, existingContent = '') {
149
+ const headingMatch = existingContent.match(/^#\s+.+$/m);
150
+ const heading = headingMatch ? headingMatch[0] : `# Session: ${today}`;
151
+ const date = extractHeaderField(existingContent, 'Date') || today;
152
+ const started = extractHeaderField(existingContent, 'Started') || currentTime;
153
+
154
+ return [
155
+ heading,
156
+ `**Date:** ${date}`,
157
+ `**Started:** ${started}`,
158
+ `**Last Updated:** ${currentTime}`,
159
+ `**Project:** ${metadata.project}`,
160
+ `**Branch:** ${metadata.branch}`,
161
+ `**Worktree:** ${metadata.worktree}`,
162
+ ''
163
+ ].join('\n');
164
+ }
165
+
166
+ function mergeSessionHeader(content, today, currentTime, metadata) {
167
+ const separatorIndex = content.indexOf(SESSION_SEPARATOR);
168
+ if (separatorIndex === -1) {
169
+ return null;
170
+ }
171
+
172
+ const existingHeader = content.slice(0, separatorIndex);
173
+ const body = content.slice(separatorIndex + SESSION_SEPARATOR.length);
174
+ const nextHeader = buildSessionHeader(today, currentTime, metadata, existingHeader);
175
+ return `${nextHeader}${SESSION_SEPARATOR}${body}`;
176
+ }
177
+
178
+ async function main() {
179
+ // Parse stdin JSON to get transcript_path
180
+ let transcriptPath = null;
181
+ try {
182
+ const input = JSON.parse(stdinData);
183
+ transcriptPath = input.transcript_path;
184
+ } catch {
185
+ // Fallback: try env var for backwards compatibility
186
+ transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
187
+ }
188
+
189
+ const sessionsDir = getSessionsDir();
190
+ const today = getDateString();
191
+ const shortId = getSessionIdShort();
192
+ const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
193
+ const sessionMetadata = getSessionMetadata();
194
+
195
+ ensureDir(sessionsDir);
196
+
197
+ const currentTime = getTimeString();
198
+
199
+ // Try to extract summary from transcript
200
+ let summary = null;
201
+
202
+ if (transcriptPath) {
203
+ if (fs.existsSync(transcriptPath)) {
204
+ summary = extractSessionSummary(transcriptPath);
205
+ } else {
206
+ log(`[SessionEnd] Transcript not found: ${transcriptPath}`);
207
+ }
208
+ }
209
+
210
+ if (fs.existsSync(sessionFile)) {
211
+ const existing = readFile(sessionFile);
212
+ let updatedContent = existing;
213
+
214
+ if (existing) {
215
+ const merged = mergeSessionHeader(existing, today, currentTime, sessionMetadata);
216
+ if (merged) {
217
+ updatedContent = merged;
218
+ } else {
219
+ log(`[SessionEnd] Failed to normalize header in ${sessionFile}`);
220
+ }
221
+ }
222
+
223
+ // If we have a new summary, update only the generated summary block.
224
+ // This keeps repeated Stop invocations idempotent and preserves
225
+ // user-authored sections in the same session file.
226
+ if (summary && updatedContent) {
227
+ const summaryBlock = buildSummaryBlock(summary);
228
+
229
+ if (updatedContent.includes(SUMMARY_START_MARKER) && updatedContent.includes(SUMMARY_END_MARKER)) {
230
+ updatedContent = updatedContent.replace(
231
+ new RegExp(`${escapeRegExp(SUMMARY_START_MARKER)}[\\s\\S]*?${escapeRegExp(SUMMARY_END_MARKER)}`),
232
+ summaryBlock
233
+ );
234
+ } else {
235
+ // Migration path for files created before summary markers existed.
236
+ updatedContent = updatedContent.replace(
237
+ /## (?:Session Summary|Current State)[\s\S]*?$/,
238
+ `${summaryBlock}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\`\n`
239
+ );
240
+ }
241
+ }
242
+
243
+ if (updatedContent) {
244
+ writeFile(sessionFile, updatedContent);
245
+ }
246
+
247
+ log(`[SessionEnd] Updated session file: ${sessionFile}`);
248
+ } else {
249
+ // Create new session file
250
+ const summarySection = summary
251
+ ? `${buildSummaryBlock(summary)}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``
252
+ : `## Current State\n\n[Session context goes here]\n\n### Completed\n- [ ]\n\n### In Progress\n- [ ]\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``;
253
+
254
+ const template = `${buildSessionHeader(today, currentTime, sessionMetadata)}${SESSION_SEPARATOR}${summarySection}
255
+ `;
256
+
257
+ writeFile(sessionFile, template);
258
+ log(`[SessionEnd] Created session file: ${sessionFile}`);
259
+ }
260
+
261
+ process.exit(0);
262
+ }
263
+
264
+ function buildSummarySection(summary) {
265
+ let section = '## Session Summary\n\n';
266
+
267
+ // Tasks (from user messages — collapse newlines and escape backticks to prevent markdown breaks)
268
+ section += '### Tasks\n';
269
+ for (const msg of summary.userMessages) {
270
+ section += `- ${msg.replace(/\n/g, ' ').replace(/`/g, '\\`')}\n`;
271
+ }
272
+ section += '\n';
273
+
274
+ // Files modified
275
+ if (summary.filesModified.length > 0) {
276
+ section += '### Files Modified\n';
277
+ for (const f of summary.filesModified) {
278
+ section += `- ${f}\n`;
279
+ }
280
+ section += '\n';
281
+ }
282
+
283
+ // Tools used
284
+ if (summary.toolsUsed.length > 0) {
285
+ section += `### Tools Used\n${summary.toolsUsed.join(', ')}\n\n`;
286
+ }
287
+
288
+ section += `### Stats\n- Total user messages: ${summary.totalMessages}\n`;
289
+
290
+ return section;
291
+ }
292
+
293
+ function buildSummaryBlock(summary) {
294
+ return `${SUMMARY_START_MARKER}\n${buildSummarySection(summary).trim()}\n${SUMMARY_END_MARKER}`;
295
+ }
296
+
297
+ function escapeRegExp(value) {
298
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
299
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart Hook - Load previous context on new session
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs when a new Claude session starts. Loads the most recent session
8
+ * summary into Claude's context via stdout, and reports available
9
+ * sessions and learned skills.
10
+ */
11
+
12
+ const {
13
+ getSessionsDir,
14
+ getLearnedSkillsDir,
15
+ findFiles,
16
+ ensureDir,
17
+ readFile,
18
+ log,
19
+ output
20
+ } = require('../lib/utils');
21
+ const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
22
+ const { listAliases } = require('../lib/session-aliases');
23
+ const { detectProjectType } = require('../lib/project-detect');
24
+
25
+ async function main() {
26
+ const sessionsDir = getSessionsDir();
27
+ const learnedDir = getLearnedSkillsDir();
28
+
29
+ // Ensure directories exist
30
+ ensureDir(sessionsDir);
31
+ ensureDir(learnedDir);
32
+
33
+ // Check for recent session files (last 7 days)
34
+ const recentSessions = findFiles(sessionsDir, '*-session.tmp', { maxAge: 7 });
35
+
36
+ if (recentSessions.length > 0) {
37
+ const latest = recentSessions[0];
38
+ log(`[SessionStart] Found ${recentSessions.length} recent session(s)`);
39
+ log(`[SessionStart] Latest: ${latest.path}`);
40
+
41
+ // Read and inject the latest session content into Claude's context
42
+ const content = readFile(latest.path);
43
+ if (content && !content.includes('[Session context goes here]')) {
44
+ // Only inject if the session has actual content (not the blank template)
45
+ output(`Previous session summary:\n${content}`);
46
+ }
47
+ }
48
+
49
+ // Check for learned skills
50
+ const learnedSkills = findFiles(learnedDir, '*.md');
51
+
52
+ if (learnedSkills.length > 0) {
53
+ log(`[SessionStart] ${learnedSkills.length} learned skill(s) available in ${learnedDir}`);
54
+ }
55
+
56
+ // Check for available session aliases
57
+ const aliases = listAliases({ limit: 5 });
58
+
59
+ if (aliases.length > 0) {
60
+ const aliasNames = aliases.map(a => a.name).join(', ');
61
+ log(`[SessionStart] ${aliases.length} session alias(es) available: ${aliasNames}`);
62
+ log(`[SessionStart] Use /sessions load <alias> to continue a previous session`);
63
+ }
64
+
65
+ // Detect and report package manager
66
+ const pm = getPackageManager();
67
+ log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`);
68
+
69
+ // If no explicit package manager config was found, show selection prompt
70
+ if (pm.source === 'default') {
71
+ log('[SessionStart] No package manager preference found.');
72
+ log(getSelectionPrompt());
73
+ }
74
+
75
+ // Detect project type and frameworks (#293)
76
+ const projectInfo = detectProjectType();
77
+ if (projectInfo.languages.length > 0 || projectInfo.frameworks.length > 0) {
78
+ const parts = [];
79
+ if (projectInfo.languages.length > 0) {
80
+ parts.push(`languages: ${projectInfo.languages.join(', ')}`);
81
+ }
82
+ if (projectInfo.frameworks.length > 0) {
83
+ parts.push(`frameworks: ${projectInfo.frameworks.join(', ')}`);
84
+ }
85
+ log(`[SessionStart] Project detected — ${parts.join('; ')}`);
86
+ output(`Project type: ${JSON.stringify(projectInfo)}`);
87
+ } else {
88
+ log('[SessionStart] No specific project type detected');
89
+ }
90
+
91
+ process.exit(0);
92
+ }
93
+
94
+ main().catch(err => {
95
+ console.error('[SessionStart] Error:', err.message);
96
+ process.exit(0); // Don't block on errors
97
+ });
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Strategic Compact Suggester
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
8
+ *
9
+ * Why manual over auto-compact:
10
+ * - Auto-compact happens at arbitrary points, often mid-task
11
+ * - Strategic compacting preserves context through logical phases
12
+ * - Compact after exploration, before execution
13
+ * - Compact after completing a milestone, before starting next
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const {
19
+ getTempDir,
20
+ writeFile,
21
+ log
22
+ } = require('../lib/utils');
23
+
24
+ async function main() {
25
+ // Track tool call count (increment in a temp file)
26
+ // Use a session-specific counter file based on session ID from environment
27
+ // or parent PID as fallback
28
+ const sessionId = (process.env.CLAUDE_SESSION_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
29
+ const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
30
+ const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
31
+ const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
32
+ ? rawThreshold
33
+ : 50;
34
+
35
+ let count = 1;
36
+
37
+ // Read existing count or start at 1
38
+ // Use fd-based read+write to reduce (but not eliminate) race window
39
+ // between concurrent hook invocations
40
+ try {
41
+ const fd = fs.openSync(counterFile, 'a+');
42
+ try {
43
+ const buf = Buffer.alloc(64);
44
+ const bytesRead = fs.readSync(fd, buf, 0, 64, 0);
45
+ if (bytesRead > 0) {
46
+ const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10);
47
+ // Clamp to reasonable range — corrupted files could contain huge values
48
+ // that pass Number.isFinite() (e.g., parseInt('9'.repeat(30)) => 1e+29)
49
+ count = (Number.isFinite(parsed) && parsed > 0 && parsed <= 1000000)
50
+ ? parsed + 1
51
+ : 1;
52
+ }
53
+ // Truncate and write new value
54
+ fs.ftruncateSync(fd, 0);
55
+ fs.writeSync(fd, String(count), 0);
56
+ } finally {
57
+ fs.closeSync(fd);
58
+ }
59
+ } catch {
60
+ // Fallback: just use writeFile if fd operations fail
61
+ writeFile(counterFile, String(count));
62
+ }
63
+
64
+ // Suggest compact after threshold tool calls
65
+ if (count === threshold) {
66
+ log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
67
+ }
68
+
69
+ // Suggest at regular intervals after threshold (every 25 calls from threshold)
70
+ if (count > threshold && (count - threshold) % 25 === 0) {
71
+ log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
72
+ }
73
+
74
+ process.exit(0);
75
+ }
76
+
77
+ main().catch(err => {
78
+ console.error('[StrategicCompact] Error:', err.message);
79
+ process.exit(0);
80
+ });
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Refactored cc4pm installer runtime.
4
+ *
5
+ * Keeps the legacy language-based install entrypoint intact while moving
6
+ * target-specific mutation logic into testable Node code.
7
+ */
8
+
9
+ const {
10
+ SUPPORTED_INSTALL_TARGETS,
11
+ listAvailableLanguages,
12
+ } = require('./lib/install-executor');
13
+ const {
14
+ LEGACY_INSTALL_TARGETS,
15
+ normalizeInstallRequest,
16
+ parseInstallArgs,
17
+ } = require('./lib/install/request');
18
+ const { loadInstallConfig } = require('./lib/install/config');
19
+ const { applyInstallPlan } = require('./lib/install/apply');
20
+ const { createInstallPlanFromRequest } = require('./lib/install/runtime');
21
+
22
+ function showHelp(exitCode = 0) {
23
+ const languages = listAvailableLanguages();
24
+
25
+ console.log(`
26
+ Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] <language> [<language> ...]
27
+ install.sh [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] --profile <name> [--with <component>]... [--without <component>]...
28
+ install.sh [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] --modules <id,id,...> [--with <component>]... [--without <component>]...
29
+ install.sh [--dry-run] [--json] --config <path>
30
+
31
+ Targets:
32
+ claude (default) - Install rules to ~/.claude/rules/
33
+ cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/
34
+ antigravity - Install rules, workflows, skills, and agents to ./.agent/
35
+
36
+ Options:
37
+ --profile <name> Resolve and install a manifest profile
38
+ --modules <ids> Resolve and install explicit module IDs
39
+ --with <component> Include a user-facing install component
40
+ --without <component>
41
+ Exclude a user-facing install component
42
+ --config <path> Load install intent from ecc-install.json
43
+ --dry-run Show the install plan without copying files
44
+ --json Emit machine-readable plan/result JSON
45
+ --help Show this help text
46
+
47
+ Available languages:
48
+ ${languages.map(language => ` - ${language}`).join('\n')}
49
+ `);
50
+
51
+ process.exit(exitCode);
52
+ }
53
+
54
+ function printHumanPlan(plan, dryRun) {
55
+ console.log(`${dryRun ? 'Dry-run install plan' : 'Applying install plan'}:\n`);
56
+ console.log(`Mode: ${plan.mode}`);
57
+ console.log(`Target: ${plan.target}`);
58
+ console.log(`Adapter: ${plan.adapter.id}`);
59
+ console.log(`Install root: ${plan.installRoot}`);
60
+ console.log(`Install-state: ${plan.installStatePath}`);
61
+ if (plan.mode === 'legacy') {
62
+ console.log(`Languages: ${plan.languages.join(', ')}`);
63
+ } else {
64
+ console.log(`Profile: ${plan.profileId || '(custom modules)'}`);
65
+ console.log(`Included components: ${plan.includedComponentIds.join(', ') || '(none)'}`);
66
+ console.log(`Excluded components: ${plan.excludedComponentIds.join(', ') || '(none)'}`);
67
+ console.log(`Requested modules: ${plan.requestedModuleIds.join(', ') || '(none)'}`);
68
+ console.log(`Selected modules: ${plan.selectedModuleIds.join(', ') || '(none)'}`);
69
+ if (plan.skippedModuleIds.length > 0) {
70
+ console.log(`Skipped modules: ${plan.skippedModuleIds.join(', ')}`);
71
+ }
72
+ if (plan.excludedModuleIds.length > 0) {
73
+ console.log(`Excluded modules: ${plan.excludedModuleIds.join(', ')}`);
74
+ }
75
+ }
76
+ console.log(`Operations: ${plan.operations.length}`);
77
+
78
+ if (plan.warnings.length > 0) {
79
+ console.log('\nWarnings:');
80
+ for (const warning of plan.warnings) {
81
+ console.log(`- ${warning}`);
82
+ }
83
+ }
84
+
85
+ console.log('\nPlanned file operations:');
86
+ for (const operation of plan.operations) {
87
+ console.log(`- ${operation.sourceRelativePath} -> ${operation.destinationPath}`);
88
+ }
89
+
90
+ if (!dryRun) {
91
+ console.log(`\nDone. Install-state written to ${plan.installStatePath}`);
92
+ }
93
+ }
94
+
95
+ function main() {
96
+ try {
97
+ const options = parseInstallArgs(process.argv);
98
+
99
+ if (options.help) {
100
+ showHelp(0);
101
+ }
102
+
103
+ const config = options.configPath
104
+ ? loadInstallConfig(options.configPath, { cwd: process.cwd() })
105
+ : null;
106
+ const request = normalizeInstallRequest({
107
+ ...options,
108
+ config,
109
+ });
110
+ const plan = createInstallPlanFromRequest(request, {
111
+ projectRoot: process.cwd(),
112
+ homeDir: process.env.HOME,
113
+ claudeRulesDir: process.env.CLAUDE_RULES_DIR || null,
114
+ });
115
+
116
+ if (options.dryRun) {
117
+ if (options.json) {
118
+ console.log(JSON.stringify({ dryRun: true, plan }, null, 2));
119
+ } else {
120
+ printHumanPlan(plan, true);
121
+ }
122
+ return;
123
+ }
124
+
125
+ const result = applyInstallPlan(plan);
126
+ if (options.json) {
127
+ console.log(JSON.stringify({ dryRun: false, result }, null, 2));
128
+ } else {
129
+ printHumanPlan(result, false);
130
+ }
131
+ } catch (error) {
132
+ console.error(`Error: ${error.message}`);
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ main();