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
package/scripts/ecc.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { listAvailableLanguages } = require('./lib/install-executor');
|
|
6
|
+
|
|
7
|
+
const COMMANDS = {
|
|
8
|
+
install: {
|
|
9
|
+
script: 'install-apply.js',
|
|
10
|
+
description: 'Install cc4pm content into a supported target',
|
|
11
|
+
},
|
|
12
|
+
plan: {
|
|
13
|
+
script: 'install-plan.js',
|
|
14
|
+
description: 'Inspect selective-install manifests and resolved plans',
|
|
15
|
+
},
|
|
16
|
+
'install-plan': {
|
|
17
|
+
script: 'install-plan.js',
|
|
18
|
+
description: 'Alias for plan',
|
|
19
|
+
},
|
|
20
|
+
'list-installed': {
|
|
21
|
+
script: 'list-installed.js',
|
|
22
|
+
description: 'Inspect install-state files for the current context',
|
|
23
|
+
},
|
|
24
|
+
doctor: {
|
|
25
|
+
script: 'doctor.js',
|
|
26
|
+
description: 'Diagnose missing or drifted ECC-managed files',
|
|
27
|
+
},
|
|
28
|
+
repair: {
|
|
29
|
+
script: 'repair.js',
|
|
30
|
+
description: 'Restore drifted or missing ECC-managed files',
|
|
31
|
+
},
|
|
32
|
+
'session-inspect': {
|
|
33
|
+
script: 'session-inspect.js',
|
|
34
|
+
description: 'Emit canonical cc4pm session snapshots from dmux or Claude history targets',
|
|
35
|
+
},
|
|
36
|
+
uninstall: {
|
|
37
|
+
script: 'uninstall.js',
|
|
38
|
+
description: 'Remove ECC-managed files recorded in install-state',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const PRIMARY_COMMANDS = [
|
|
43
|
+
'install',
|
|
44
|
+
'plan',
|
|
45
|
+
'list-installed',
|
|
46
|
+
'doctor',
|
|
47
|
+
'repair',
|
|
48
|
+
'session-inspect',
|
|
49
|
+
'uninstall',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function showHelp(exitCode = 0) {
|
|
53
|
+
console.log(`
|
|
54
|
+
cc4pm selective-install CLI
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
ecc <command> [args...]
|
|
58
|
+
ecc [install args...]
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
${PRIMARY_COMMANDS.map(command => ` ${command.padEnd(15)} ${COMMANDS[command].description}`).join('\n')}
|
|
62
|
+
|
|
63
|
+
Compatibility:
|
|
64
|
+
ecc-install Legacy install entrypoint retained for existing flows
|
|
65
|
+
ecc [args...] Without a command, args are routed to "install"
|
|
66
|
+
ecc help <command> Show help for a specific command
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
ecc typescript
|
|
70
|
+
ecc install --profile developer --target claude
|
|
71
|
+
ecc plan --profile core --target cursor
|
|
72
|
+
ecc list-installed --json
|
|
73
|
+
ecc doctor --target cursor
|
|
74
|
+
ecc repair --dry-run
|
|
75
|
+
ecc session-inspect claude:latest
|
|
76
|
+
ecc uninstall --target antigravity --dry-run
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
process.exit(exitCode);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveCommand(argv) {
|
|
83
|
+
const args = argv.slice(2);
|
|
84
|
+
|
|
85
|
+
if (args.length === 0) {
|
|
86
|
+
return { mode: 'help' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [firstArg, ...restArgs] = args;
|
|
90
|
+
|
|
91
|
+
if (firstArg === '--help' || firstArg === '-h') {
|
|
92
|
+
return { mode: 'help' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (firstArg === 'help') {
|
|
96
|
+
return {
|
|
97
|
+
mode: 'help-command',
|
|
98
|
+
command: restArgs[0] || null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (COMMANDS[firstArg]) {
|
|
103
|
+
return {
|
|
104
|
+
mode: 'command',
|
|
105
|
+
command: firstArg,
|
|
106
|
+
args: restArgs,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const knownLegacyLanguages = listAvailableLanguages();
|
|
111
|
+
const shouldTreatAsImplicitInstall = (
|
|
112
|
+
firstArg.startsWith('-')
|
|
113
|
+
|| knownLegacyLanguages.includes(firstArg)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (!shouldTreatAsImplicitInstall) {
|
|
117
|
+
throw new Error(`Unknown command: ${firstArg}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
mode: 'command',
|
|
122
|
+
command: 'install',
|
|
123
|
+
args,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runCommand(commandName, args) {
|
|
128
|
+
const command = COMMANDS[commandName];
|
|
129
|
+
if (!command) {
|
|
130
|
+
throw new Error(`Unknown command: ${commandName}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = spawnSync(
|
|
134
|
+
process.execPath,
|
|
135
|
+
[path.join(__dirname, command.script), ...args],
|
|
136
|
+
{
|
|
137
|
+
cwd: process.cwd(),
|
|
138
|
+
env: process.env,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (result.error) {
|
|
144
|
+
throw result.error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.stdout) {
|
|
148
|
+
process.stdout.write(result.stdout);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.stderr) {
|
|
152
|
+
process.stderr.write(result.stderr);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof result.status === 'number') {
|
|
156
|
+
return result.status;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (result.signal) {
|
|
160
|
+
throw new Error(`Command "${commandName}" terminated by signal ${result.signal}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function main() {
|
|
167
|
+
try {
|
|
168
|
+
const resolution = resolveCommand(process.argv);
|
|
169
|
+
|
|
170
|
+
if (resolution.mode === 'help') {
|
|
171
|
+
showHelp(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (resolution.mode === 'help-command') {
|
|
175
|
+
if (!resolution.command) {
|
|
176
|
+
showHelp(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!COMMANDS[resolution.command]) {
|
|
180
|
+
throw new Error(`Unknown command: ${resolution.command}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.exitCode = runCommand(resolution.command, ['--help']);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
process.exitCode = runCommand(resolution.command, resolution.args);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`Error: ${error.message}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
main();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Auto-Tmux Dev Hook - Start dev servers in tmux/cmd automatically
|
|
4
|
+
*
|
|
5
|
+
* macOS/Linux: Runs dev server in a named tmux session (non-blocking).
|
|
6
|
+
* Falls back to original command if tmux is not installed.
|
|
7
|
+
* Windows: Opens dev server in a new cmd window (non-blocking).
|
|
8
|
+
*
|
|
9
|
+
* Runs before Bash tool use. If command is a dev server (npm run dev, pnpm dev, yarn dev, bun run dev),
|
|
10
|
+
* transforms it to run in a detached session.
|
|
11
|
+
*
|
|
12
|
+
* Benefits:
|
|
13
|
+
* - Dev server runs detached (doesn't block Claude Code)
|
|
14
|
+
* - Session persists (can run `tmux capture-pane -t <session> -p` to see logs on Unix)
|
|
15
|
+
* - Session name matches project directory (allows multiple projects simultaneously)
|
|
16
|
+
*
|
|
17
|
+
* Session management (Unix):
|
|
18
|
+
* - Checks tmux availability before transforming
|
|
19
|
+
* - Kills any existing session with the same name (clean restart)
|
|
20
|
+
* - Creates new detached session
|
|
21
|
+
* - Reports session name and how to view logs
|
|
22
|
+
*
|
|
23
|
+
* Session management (Windows):
|
|
24
|
+
* - Opens new cmd window with descriptive title
|
|
25
|
+
* - Allows multiple dev servers to run simultaneously
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { spawnSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
32
|
+
let data = '';
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
|
|
35
|
+
process.stdin.on('data', chunk => {
|
|
36
|
+
if (data.length < MAX_STDIN) {
|
|
37
|
+
const remaining = MAX_STDIN - data.length;
|
|
38
|
+
data += chunk.substring(0, remaining);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.stdin.on('end', () => {
|
|
43
|
+
let input;
|
|
44
|
+
try {
|
|
45
|
+
input = JSON.parse(data);
|
|
46
|
+
const cmd = input.tool_input?.command || '';
|
|
47
|
+
|
|
48
|
+
// Detect dev server commands: npm run dev, pnpm dev, yarn dev, bun run dev
|
|
49
|
+
// Use word boundary (\b) to avoid matching partial commands
|
|
50
|
+
const devServerRegex = /(npm run dev\b|pnpm( run)? dev\b|yarn dev\b|bun run dev\b)/;
|
|
51
|
+
|
|
52
|
+
if (devServerRegex.test(cmd)) {
|
|
53
|
+
// Get session name from current directory basename, sanitize for shell safety
|
|
54
|
+
// e.g., /home/user/Portfolio → "Portfolio", /home/user/my-app-v2 → "my-app-v2"
|
|
55
|
+
const rawName = path.basename(process.cwd());
|
|
56
|
+
// Replace non-alphanumeric characters (except - and _) with underscore to prevent shell injection
|
|
57
|
+
const sessionName = rawName.replace(/[^a-zA-Z0-9_-]/g, '_') || 'dev';
|
|
58
|
+
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
// Windows: open in a new cmd window (non-blocking)
|
|
61
|
+
// Escape double quotes in cmd for cmd /k syntax
|
|
62
|
+
const escapedCmd = cmd.replace(/"/g, '""');
|
|
63
|
+
input.tool_input.command = `start "DevServer-${sessionName}" cmd /k "${escapedCmd}"`;
|
|
64
|
+
} else {
|
|
65
|
+
// Unix (macOS/Linux): Check tmux is available before transforming
|
|
66
|
+
const tmuxCheck = spawnSync('which', ['tmux'], { encoding: 'utf8' });
|
|
67
|
+
if (tmuxCheck.status === 0) {
|
|
68
|
+
// Escape single quotes for shell safety: 'text' -> 'text'\''text'
|
|
69
|
+
const escapedCmd = cmd.replace(/'/g, "'\\''");
|
|
70
|
+
|
|
71
|
+
// Build the transformed command:
|
|
72
|
+
// 1. Kill existing session (silent if doesn't exist)
|
|
73
|
+
// 2. Create new detached session with the dev command
|
|
74
|
+
// 3. Echo confirmation message with instructions for viewing logs
|
|
75
|
+
const transformedCmd = `SESSION="${sessionName}"; tmux kill-session -t "$SESSION" 2>/dev/null || true; tmux new-session -d -s "$SESSION" '${escapedCmd}' && echo "[Hook] Dev server started in tmux session '${sessionName}'. View logs: tmux capture-pane -t ${sessionName} -p -S -100"`;
|
|
76
|
+
|
|
77
|
+
input.tool_input.command = transformedCmd;
|
|
78
|
+
}
|
|
79
|
+
// else: tmux not found, pass through original command unchanged
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(JSON.stringify(input));
|
|
83
|
+
} catch {
|
|
84
|
+
// Invalid input — pass through original data unchanged
|
|
85
|
+
process.stdout.write(data);
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop Hook: Check for console.log statements in modified files
|
|
5
|
+
*
|
|
6
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
7
|
+
*
|
|
8
|
+
* Runs after each response and checks if any modified JavaScript/TypeScript
|
|
9
|
+
* files contain console.log statements. Provides warnings to help developers
|
|
10
|
+
* remember to remove debug statements before committing.
|
|
11
|
+
*
|
|
12
|
+
* Exclusions: test files, config files, and scripts/ directory (where
|
|
13
|
+
* console.log is often intentional).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { isGitRepo, getGitModifiedFiles, readFile, log } = require('../lib/utils');
|
|
18
|
+
|
|
19
|
+
// Files where console.log is expected and should not trigger warnings
|
|
20
|
+
const EXCLUDED_PATTERNS = [
|
|
21
|
+
/\.test\.[jt]sx?$/,
|
|
22
|
+
/\.spec\.[jt]sx?$/,
|
|
23
|
+
/\.config\.[jt]s$/,
|
|
24
|
+
/scripts\//,
|
|
25
|
+
/__tests__\//,
|
|
26
|
+
/__mocks__\//,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
30
|
+
let data = '';
|
|
31
|
+
process.stdin.setEncoding('utf8');
|
|
32
|
+
|
|
33
|
+
process.stdin.on('data', chunk => {
|
|
34
|
+
if (data.length < MAX_STDIN) {
|
|
35
|
+
const remaining = MAX_STDIN - data.length;
|
|
36
|
+
data += chunk.substring(0, remaining);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.stdin.on('end', () => {
|
|
41
|
+
try {
|
|
42
|
+
if (!isGitRepo()) {
|
|
43
|
+
process.stdout.write(data);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const files = getGitModifiedFiles(['\\.tsx?$', '\\.jsx?$'])
|
|
48
|
+
.filter(f => fs.existsSync(f))
|
|
49
|
+
.filter(f => !EXCLUDED_PATTERNS.some(pattern => pattern.test(f)));
|
|
50
|
+
|
|
51
|
+
let hasConsole = false;
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const content = readFile(file);
|
|
55
|
+
if (content && content.includes('console.log')) {
|
|
56
|
+
log(`[Hook] WARNING: console.log found in ${file}`);
|
|
57
|
+
hasConsole = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (hasConsole) {
|
|
62
|
+
log('[Hook] Remove console.log statements before committing');
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
log(`[Hook] check-console-log error: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Always output the original data
|
|
69
|
+
process.stdout.write(data);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { isHookEnabled } = require('../lib/hook-flags');
|
|
5
|
+
|
|
6
|
+
const [, , hookId, profilesCsv] = process.argv;
|
|
7
|
+
if (!hookId) {
|
|
8
|
+
process.stdout.write('yes');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process.stdout.write(isHookEnabled(hookId, { profiles: profilesCsv }) ? 'yes' : 'no');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cost Tracker Hook
|
|
4
|
+
*
|
|
5
|
+
* Appends lightweight session usage metrics to ~/.claude/metrics/costs.jsonl.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const {
|
|
12
|
+
ensureDir,
|
|
13
|
+
appendFile,
|
|
14
|
+
getClaudeDir,
|
|
15
|
+
} = require('../lib/utils');
|
|
16
|
+
|
|
17
|
+
const MAX_STDIN = 1024 * 1024;
|
|
18
|
+
let raw = '';
|
|
19
|
+
|
|
20
|
+
function toNumber(value) {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
return Number.isFinite(n) ? n : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
26
|
+
// Approximate per-1M-token blended rates. Conservative defaults.
|
|
27
|
+
const table = {
|
|
28
|
+
'haiku': { in: 0.8, out: 4.0 },
|
|
29
|
+
'sonnet': { in: 3.0, out: 15.0 },
|
|
30
|
+
'opus': { in: 15.0, out: 75.0 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const normalized = String(model || '').toLowerCase();
|
|
34
|
+
let rates = table.sonnet;
|
|
35
|
+
if (normalized.includes('haiku')) rates = table.haiku;
|
|
36
|
+
if (normalized.includes('opus')) rates = table.opus;
|
|
37
|
+
|
|
38
|
+
const cost = (inputTokens / 1_000_000) * rates.in + (outputTokens / 1_000_000) * rates.out;
|
|
39
|
+
return Math.round(cost * 1e6) / 1e6;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', chunk => {
|
|
44
|
+
if (raw.length < MAX_STDIN) {
|
|
45
|
+
const remaining = MAX_STDIN - raw.length;
|
|
46
|
+
raw += chunk.substring(0, remaining);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
process.stdin.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const input = raw.trim() ? JSON.parse(raw) : {};
|
|
53
|
+
const usage = input.usage || input.token_usage || {};
|
|
54
|
+
const inputTokens = toNumber(usage.input_tokens || usage.prompt_tokens || 0);
|
|
55
|
+
const outputTokens = toNumber(usage.output_tokens || usage.completion_tokens || 0);
|
|
56
|
+
|
|
57
|
+
const model = String(input.model || input._cursor?.model || process.env.CLAUDE_MODEL || 'unknown');
|
|
58
|
+
const sessionId = String(process.env.CLAUDE_SESSION_ID || 'default');
|
|
59
|
+
|
|
60
|
+
const metricsDir = path.join(getClaudeDir(), 'metrics');
|
|
61
|
+
ensureDir(metricsDir);
|
|
62
|
+
|
|
63
|
+
const row = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
session_id: sessionId,
|
|
66
|
+
model,
|
|
67
|
+
input_tokens: inputTokens,
|
|
68
|
+
output_tokens: outputTokens,
|
|
69
|
+
estimated_cost_usd: estimateCost(model, inputTokens, outputTokens),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
appendFile(path.join(metricsDir, 'costs.jsonl'), `${JSON.stringify(row)}\n`);
|
|
73
|
+
} catch {
|
|
74
|
+
// Keep hook non-blocking.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
process.stdout.write(raw);
|
|
78
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Doc file warning hook (PreToolUse - Write)
|
|
4
|
+
* Warns about non-standard documentation files.
|
|
5
|
+
* Exit code 0 always (warns only, never blocks).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const MAX_STDIN = 1024 * 1024;
|
|
13
|
+
let data = '';
|
|
14
|
+
|
|
15
|
+
function isAllowedDocPath(filePath) {
|
|
16
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
17
|
+
const basename = path.basename(filePath);
|
|
18
|
+
|
|
19
|
+
if (!/\.(md|txt)$/i.test(filePath)) return true;
|
|
20
|
+
|
|
21
|
+
if (/^(README|CLAUDE|AGENTS|CONTRIBUTING|CHANGELOG|LICENSE|SKILL|MEMORY|WORKLOG)\.md$/i.test(basename)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (/\.claude\/(commands|plans|projects)\//.test(normalized)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (/(^|\/)(docs|skills|\.history|memory)\//.test(normalized)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (/\.plan\.md$/i.test(basename)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdin.setEncoding('utf8');
|
|
41
|
+
process.stdin.on('data', c => {
|
|
42
|
+
if (data.length < MAX_STDIN) {
|
|
43
|
+
const remaining = MAX_STDIN - data.length;
|
|
44
|
+
data += c.substring(0, remaining);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.stdin.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const input = JSON.parse(data);
|
|
51
|
+
const filePath = String(input.tool_input?.file_path || '');
|
|
52
|
+
|
|
53
|
+
if (filePath && !isAllowedDocPath(filePath)) {
|
|
54
|
+
console.error('[Hook] WARNING: Non-standard documentation file detected');
|
|
55
|
+
console.error(`[Hook] File: ${filePath}`);
|
|
56
|
+
console.error('[Hook] Consider consolidating into README.md or docs/ directory');
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore parse errors
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process.stdout.write(data);
|
|
63
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Continuous Learning - Session Evaluator
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs on Stop hook to extract reusable patterns from Claude Code sessions.
|
|
8
|
+
* Reads transcript_path from stdin JSON (Claude Code hook input).
|
|
9
|
+
*
|
|
10
|
+
* Why Stop hook instead of UserPromptSubmit:
|
|
11
|
+
* - Stop runs once at session end (lightweight)
|
|
12
|
+
* - UserPromptSubmit runs every message (heavy, adds latency)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const {
|
|
18
|
+
getLearnedSkillsDir,
|
|
19
|
+
ensureDir,
|
|
20
|
+
readFile,
|
|
21
|
+
countInFile,
|
|
22
|
+
log
|
|
23
|
+
} = require('../lib/utils');
|
|
24
|
+
|
|
25
|
+
// Read hook input from stdin (Claude Code provides transcript_path via stdin JSON)
|
|
26
|
+
const MAX_STDIN = 1024 * 1024;
|
|
27
|
+
let stdinData = '';
|
|
28
|
+
process.stdin.setEncoding('utf8');
|
|
29
|
+
|
|
30
|
+
process.stdin.on('data', chunk => {
|
|
31
|
+
if (stdinData.length < MAX_STDIN) {
|
|
32
|
+
const remaining = MAX_STDIN - stdinData.length;
|
|
33
|
+
stdinData += chunk.substring(0, remaining);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
process.stdin.on('end', () => {
|
|
38
|
+
main().catch(err => {
|
|
39
|
+
console.error('[ContinuousLearning] Error:', err.message);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
// Parse stdin JSON to get transcript_path
|
|
46
|
+
let transcriptPath = null;
|
|
47
|
+
try {
|
|
48
|
+
const input = JSON.parse(stdinData);
|
|
49
|
+
transcriptPath = input.transcript_path;
|
|
50
|
+
} catch {
|
|
51
|
+
// Fallback: try env var for backwards compatibility
|
|
52
|
+
transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get script directory to find config
|
|
56
|
+
const scriptDir = __dirname;
|
|
57
|
+
const configFile = path.join(scriptDir, '..', '..', 'skills', 'continuous-learning', 'config.json');
|
|
58
|
+
|
|
59
|
+
// Default configuration
|
|
60
|
+
let minSessionLength = 10;
|
|
61
|
+
let learnedSkillsPath = getLearnedSkillsDir();
|
|
62
|
+
|
|
63
|
+
// Load config if exists
|
|
64
|
+
const configContent = readFile(configFile);
|
|
65
|
+
if (configContent) {
|
|
66
|
+
try {
|
|
67
|
+
const config = JSON.parse(configContent);
|
|
68
|
+
minSessionLength = config.min_session_length ?? 10;
|
|
69
|
+
|
|
70
|
+
if (config.learned_skills_path) {
|
|
71
|
+
// Handle ~ in path
|
|
72
|
+
learnedSkillsPath = config.learned_skills_path.replace(/^~/, require('os').homedir());
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log(`[ContinuousLearning] Failed to parse config: ${err.message}, using defaults`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Ensure learned skills directory exists
|
|
80
|
+
ensureDir(learnedSkillsPath);
|
|
81
|
+
|
|
82
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Count user messages in session (allow optional whitespace around colon)
|
|
87
|
+
const messageCount = countInFile(transcriptPath, /"type"\s*:\s*"user"/g);
|
|
88
|
+
|
|
89
|
+
// Skip short sessions
|
|
90
|
+
if (messageCount < minSessionLength) {
|
|
91
|
+
log(`[ContinuousLearning] Session too short (${messageCount} messages), skipping`);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Signal to Claude that session should be evaluated for extractable patterns
|
|
96
|
+
log(`[ContinuousLearning] Session has ${messageCount} messages - evaluate for extractable patterns`);
|
|
97
|
+
log(`[ContinuousLearning] Save learned skills to: ${learnedSkillsPath}`);
|
|
98
|
+
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|