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.
- package/.claude-plugin/README.md +17 -0
- package/.claude-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README.zh-CN.md +134 -0
- package/contexts/dev.md +20 -0
- package/contexts/research.md +26 -0
- package/contexts/review.md +22 -0
- package/examples/CLAUDE.md +100 -0
- package/examples/statusline.json +19 -0
- package/examples/user-CLAUDE.md +109 -0
- package/install.sh +17 -0
- package/manifests/install-components.json +173 -0
- package/manifests/install-modules.json +335 -0
- package/manifests/install-profiles.json +75 -0
- package/package.json +117 -0
- package/schemas/ecc-install-config.schema.json +58 -0
- package/schemas/hooks.schema.json +197 -0
- package/schemas/install-components.schema.json +56 -0
- package/schemas/install-modules.schema.json +105 -0
- package/schemas/install-profiles.schema.json +45 -0
- package/schemas/install-state.schema.json +210 -0
- package/schemas/package-manager.schema.json +23 -0
- package/schemas/plugin.schema.json +58 -0
- package/scripts/ci/catalog.js +83 -0
- package/scripts/ci/validate-agents.js +81 -0
- package/scripts/ci/validate-commands.js +135 -0
- package/scripts/ci/validate-hooks.js +239 -0
- package/scripts/ci/validate-install-manifests.js +211 -0
- package/scripts/ci/validate-no-personal-paths.js +63 -0
- package/scripts/ci/validate-rules.js +81 -0
- package/scripts/ci/validate-skills.js +54 -0
- package/scripts/claw.js +468 -0
- package/scripts/doctor.js +110 -0
- package/scripts/ecc.js +194 -0
- package/scripts/hooks/auto-tmux-dev.js +88 -0
- package/scripts/hooks/check-console-log.js +71 -0
- package/scripts/hooks/check-hook-enabled.js +12 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +100 -0
- package/scripts/hooks/insaits-security-monitor.py +269 -0
- package/scripts/hooks/insaits-security-wrapper.js +88 -0
- package/scripts/hooks/post-bash-build-complete.js +27 -0
- package/scripts/hooks/post-bash-pr-created.js +36 -0
- package/scripts/hooks/post-edit-console-warn.js +54 -0
- package/scripts/hooks/post-edit-format.js +109 -0
- package/scripts/hooks/post-edit-typecheck.js +96 -0
- package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
- package/scripts/hooks/pre-compact.js +48 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +168 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +120 -0
- package/scripts/hooks/session-end-marker.js +15 -0
- package/scripts/hooks/session-end.js +299 -0
- package/scripts/hooks/session-start.js +97 -0
- package/scripts/hooks/suggest-compact.js +80 -0
- package/scripts/install-apply.js +137 -0
- package/scripts/install-plan.js +254 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install/apply.js +23 -0
- package/scripts/lib/install/config.js +82 -0
- package/scripts/lib/install/request.js +113 -0
- package/scripts/lib/install/runtime.js +42 -0
- package/scripts/lib/install-executor.js +605 -0
- package/scripts/lib/install-lifecycle.js +763 -0
- package/scripts/lib/install-manifests.js +305 -0
- package/scripts/lib/install-state.js +120 -0
- package/scripts/lib/install-targets/antigravity-project.js +9 -0
- package/scripts/lib/install-targets/claude-home.js +10 -0
- package/scripts/lib/install-targets/codex-home.js +10 -0
- package/scripts/lib/install-targets/cursor-project.js +10 -0
- package/scripts/lib/install-targets/helpers.js +89 -0
- package/scripts/lib/install-targets/opencode-home.js +10 -0
- package/scripts/lib/install-targets/registry.js +64 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.d.ts +119 -0
- package/scripts/lib/package-manager.js +431 -0
- package/scripts/lib/project-detect.js +428 -0
- package/scripts/lib/resolve-formatter.js +185 -0
- package/scripts/lib/session-adapters/canonical-session.js +138 -0
- package/scripts/lib/session-adapters/claude-history.js +149 -0
- package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
- package/scripts/lib/session-adapters/registry.js +111 -0
- package/scripts/lib/session-aliases.d.ts +136 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.d.ts +131 -0
- package/scripts/lib/session-manager.js +464 -0
- package/scripts/lib/shell-split.js +86 -0
- package/scripts/lib/skill-improvement/amendify.js +89 -0
- package/scripts/lib/skill-improvement/evaluate.js +59 -0
- package/scripts/lib/skill-improvement/health.js +118 -0
- package/scripts/lib/skill-improvement/observations.js +108 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
- package/scripts/lib/utils.d.ts +183 -0
- package/scripts/lib/utils.js +543 -0
- package/scripts/list-installed.js +90 -0
- package/scripts/orchestrate-codex-worker.sh +92 -0
- package/scripts/orchestrate-worktrees.js +108 -0
- package/scripts/orchestration-status.js +62 -0
- package/scripts/repair.js +97 -0
- package/scripts/session-inspect.js +150 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/skill-create-output.js +244 -0
- 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();
|