claudekit-cli 4.3.1 → 4.4.0-dev.2
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 +3 -1
- package/bin/postinstall-hook-restore.cjs +192 -0
- package/bin/postinstall-self-heal.cjs +155 -0
- package/cli-manifest.json +12 -2
- package/dist/index.js +6077 -4651
- package/dist/ui/assets/{index-BrzUvlYt.js → index-D3Q_VrJU.js} +59 -43
- package/dist/ui/index.html +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Without a purchased kit and repository access, the CLI will not be able to downl
|
|
|
50
50
|
|
|
51
51
|
The ClaudeKit CLI is published on npm at [npmjs.com/package/claudekit-cli](https://www.npmjs.com/package/claudekit-cli).
|
|
52
52
|
|
|
53
|
-
End-user runtime note: global installs from `npm`, `pnpm`, `yarn`, or `bun` all execute the packaged Node.js CLI. Bun is optional for users and only needed for local ClaudeKit CLI development workflows.
|
|
53
|
+
End-user runtime note: global installs from `npm`, `pnpm`, `yarn`, or `bun` all execute the packaged Node.js CLI. Bun is optional for users and only needed for local ClaudeKit CLI development workflows. Most commands do not require native build tools; `ck content` uses an optional SQLite driver and shows remediation steps if that driver is unavailable.
|
|
54
54
|
|
|
55
55
|
### Using npm (Recommended)
|
|
56
56
|
|
|
@@ -407,6 +407,8 @@ ck content start --verbose
|
|
|
407
407
|
|
|
408
408
|
**Features:** 11-phase pipeline (scan → filter → classify → context → create → validate → review → photo → publish → engage → analyze), noise filtering, context caching (24h TTL), content validation, photo generation, 3 review modes (auto/manual/hybrid), quiet hours scheduling, engagement tracking, SQLite database, platform-specific adapters.
|
|
409
409
|
|
|
410
|
+
**Runtime dependency:** `ck content` uses the optional native SQLite driver `better-sqlite3`. If the driver is unavailable, other CLI commands still work and `ck content` prints reinstall/build-tool guidance.
|
|
411
|
+
|
|
410
412
|
**Config:** `.ck.json` under `content` key. See [docs/ck-content.md](./docs/ck-content.md) for full configuration reference.
|
|
411
413
|
|
|
412
414
|
### Other Commands
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const MAX_SETTINGS_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
const SAFE_KIT_NAMES = new Set(["engineer", "marketing"]);
|
|
11
|
+
const COMPLETE_HOOK_SENTINELS = new Set(["session-init", "session-state", "subagent-init"]);
|
|
12
|
+
const LEGACY_SPARSE_HOOK_SENTINELS = new Set([
|
|
13
|
+
"descriptive-name",
|
|
14
|
+
"privacy-block",
|
|
15
|
+
"scout-block",
|
|
16
|
+
"simplify-gate",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function getHomeDir() {
|
|
20
|
+
return process.env.CK_TEST_HOME || os.homedir();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
26
|
+
const stat = fs.statSync(filePath);
|
|
27
|
+
if (!stat.isFile() || stat.size > MAX_SETTINGS_BYTES) return null;
|
|
28
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeCommand(command) {
|
|
35
|
+
if (!command) return "";
|
|
36
|
+
let normalized = String(command).replace(/"/g, "");
|
|
37
|
+
normalized = normalized.replace(/~\//g, "$HOME/");
|
|
38
|
+
normalized = normalized.replace(/\$\{HOME\}/g, "$HOME");
|
|
39
|
+
normalized = normalized.replace(/\$CLAUDE_PROJECT_DIR/g, "$HOME");
|
|
40
|
+
normalized = normalized.replace(/%USERPROFILE%/g, "$HOME");
|
|
41
|
+
normalized = normalized.replace(/%CLAUDE_PROJECT_DIR%/g, "$HOME");
|
|
42
|
+
normalized = normalized.replace(/(^|\s)(?:\.\/)?\.claude\//g, "$1$HOME/.claude/");
|
|
43
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
44
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function collectHookCommands(settings) {
|
|
48
|
+
const commands = new Set();
|
|
49
|
+
for (const entries of Object.values(settings?.hooks || {})) {
|
|
50
|
+
if (!Array.isArray(entries)) continue;
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (typeof entry?.command === "string") commands.add(normalizeCommand(entry.command));
|
|
53
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
54
|
+
for (const hook of entry.hooks) {
|
|
55
|
+
if (typeof hook?.command === "string") commands.add(normalizeCommand(hook.command));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return commands;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractCkHookName(command) {
|
|
63
|
+
if (!String(command || "").trim().startsWith("node ")) return null;
|
|
64
|
+
const normalized = String(command).replace(/\\/g, "/");
|
|
65
|
+
const match = normalized.match(/\/hooks\/([^/"'\s]+)\.(?:cjs|mjs|js)(?:["'\s]|$)/);
|
|
66
|
+
return match?.[1] || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function installedHookCommands(config) {
|
|
70
|
+
return Object.values(config?.kits || {}).flatMap((entry) =>
|
|
71
|
+
Array.isArray(entry?.installedSettings?.hooks) ? entry.installedSettings.hooks : [],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasInstalledKit(config) {
|
|
76
|
+
return Object.keys(config?.kits || {}).some((kitName) => normalizeKitName(kitName));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasLegacySparseCkHooks(settings, config) {
|
|
80
|
+
if (!hasInstalledKit(config)) return false;
|
|
81
|
+
|
|
82
|
+
const hookNames = new Set();
|
|
83
|
+
for (const command of collectHookCommands(settings)) {
|
|
84
|
+
const hookName = extractCkHookName(command);
|
|
85
|
+
if (hookName) hookNames.add(hookName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hasKnownSparseHook = [...LEGACY_SPARSE_HOOK_SENTINELS].some((hookName) =>
|
|
89
|
+
hookNames.has(hookName),
|
|
90
|
+
);
|
|
91
|
+
if (!hasKnownSparseHook) return false;
|
|
92
|
+
|
|
93
|
+
const disabledHooks = new Set(
|
|
94
|
+
Object.entries(config?.hooks || {})
|
|
95
|
+
.filter(([, enabled]) => enabled === false)
|
|
96
|
+
.map(([name]) => name),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return [...COMPLETE_HOOK_SENTINELS].some(
|
|
100
|
+
(hookName) => !disabledHooks.has(hookName) && !hookNames.has(hookName),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countMissingCkHookRegistrations(claudeDir) {
|
|
105
|
+
const settings = readJsonFile(path.join(claudeDir, "settings.json"));
|
|
106
|
+
const config = readJsonFile(path.join(claudeDir, ".ck.json"));
|
|
107
|
+
if (!settings || !config) return 0;
|
|
108
|
+
|
|
109
|
+
const existingCommands = collectHookCommands(settings);
|
|
110
|
+
const disabledHooks = new Set(
|
|
111
|
+
Object.entries(config.hooks || {})
|
|
112
|
+
.filter(([, enabled]) => enabled === false)
|
|
113
|
+
.map(([name]) => name),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let missing = 0;
|
|
117
|
+
for (const command of installedHookCommands(config)) {
|
|
118
|
+
const hookName = extractCkHookName(command);
|
|
119
|
+
if (hookName && disabledHooks.has(hookName)) continue;
|
|
120
|
+
if (!existingCommands.has(normalizeCommand(command))) missing++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (missing === 0 && installedHookCommands(config).length === 0) {
|
|
124
|
+
return hasLegacySparseCkHooks(settings, config) ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return missing;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeKitName(name) {
|
|
131
|
+
const normalized = String(name || "").toLowerCase();
|
|
132
|
+
if (normalized.includes("engineer")) return "engineer";
|
|
133
|
+
if (normalized.includes("marketing")) return "marketing";
|
|
134
|
+
return SAFE_KIT_NAMES.has(normalized) ? normalized : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getInstalledKit(claudeDir) {
|
|
138
|
+
const metadata = readJsonFile(path.join(claudeDir, "metadata.json"));
|
|
139
|
+
const metadataKit = normalizeKitName(Object.keys(metadata?.kits || {})[0] || metadata?.name);
|
|
140
|
+
if (metadataKit) {
|
|
141
|
+
const kitVersion = metadata?.kits?.[metadataKit]?.version || metadata?.version || "";
|
|
142
|
+
return { kit: metadataKit, isBeta: String(kitVersion).includes("beta") };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const config = readJsonFile(path.join(claudeDir, ".ck.json"));
|
|
146
|
+
const configKit = normalizeKitName(Object.keys(config?.kits || {})[0]);
|
|
147
|
+
return configKit ? { kit: configKit, isBeta: false } : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function collectCandidateClaudeDirs(settingsFiles) {
|
|
151
|
+
const dirs = new Set();
|
|
152
|
+
for (const settingsFile of settingsFiles) dirs.add(path.dirname(settingsFile));
|
|
153
|
+
return [...dirs];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveCkEntrypoint() {
|
|
157
|
+
return process.env.CK_POSTINSTALL_CK_BIN || path.join(__dirname, "ck.js");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function restoreMissingHookRegistrations(settingsFiles) {
|
|
161
|
+
if (process.env.CK_POSTINSTALL_SKIP_RESTORE_CK_HOOKS === "1") return 0;
|
|
162
|
+
|
|
163
|
+
let restored = 0;
|
|
164
|
+
for (const claudeDir of collectCandidateClaudeDirs(settingsFiles)) {
|
|
165
|
+
const missing = countMissingCkHookRegistrations(claudeDir);
|
|
166
|
+
if (missing <= 0) continue;
|
|
167
|
+
|
|
168
|
+
const installedKit = getInstalledKit(claudeDir);
|
|
169
|
+
if (!installedKit?.kit) continue;
|
|
170
|
+
|
|
171
|
+
const homeDir = getHomeDir();
|
|
172
|
+
const globalClaudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, ".claude");
|
|
173
|
+
const isGlobal = path.resolve(claudeDir) === path.resolve(globalClaudeDir);
|
|
174
|
+
const args = [resolveCkEntrypoint(), "init"];
|
|
175
|
+
if (isGlobal) args.push("-g");
|
|
176
|
+
args.push("--kit", installedKit.kit, "--yes", "--restore-ck-hooks", "--install-skills");
|
|
177
|
+
if (installedKit.isBeta) args.push("--beta");
|
|
178
|
+
|
|
179
|
+
const result = spawnSync(process.execPath, args, {
|
|
180
|
+
env: { ...process.env, CK_POSTINSTALL_SKIP_RESTORE_CK_HOOKS: "1" },
|
|
181
|
+
stdio: process.env.CK_POSTINSTALL_DEBUG === "1" ? "inherit" : "ignore",
|
|
182
|
+
});
|
|
183
|
+
if (result.status === 0) restored += missing;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return restored;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
countMissingCkHookRegistrations,
|
|
191
|
+
restoreMissingHookRegistrations,
|
|
192
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const {
|
|
8
|
+
countMissingCkHookRegistrations,
|
|
9
|
+
restoreMissingHookRegistrations,
|
|
10
|
+
} = require("./postinstall-hook-restore.cjs");
|
|
11
|
+
|
|
12
|
+
const MAX_SETTINGS_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
function isLegacyDescriptiveNamePrompt(entry) {
|
|
15
|
+
if (!entry || entry.type !== "prompt" || typeof entry.prompt !== "string") return false;
|
|
16
|
+
|
|
17
|
+
const prompt = entry.prompt;
|
|
18
|
+
const lowerPrompt = prompt.toLowerCase();
|
|
19
|
+
const isOriginalLegacyPrompt =
|
|
20
|
+
prompt.includes("Use kebab-case file naming") &&
|
|
21
|
+
prompt.includes("self-documenting") &&
|
|
22
|
+
prompt.includes("Grep, Glob, Search");
|
|
23
|
+
const isDescriptiveNameKebabPrompt =
|
|
24
|
+
lowerPrompt.includes("descriptive-name") &&
|
|
25
|
+
lowerPrompt.includes("kebab-case") &&
|
|
26
|
+
(lowerPrompt.includes("file") || lowerPrompt.includes("filename"));
|
|
27
|
+
|
|
28
|
+
return isOriginalLegacyPrompt || isDescriptiveNameKebabPrompt;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pruneHooks(settings) {
|
|
32
|
+
if (!settings || typeof settings !== "object" || !settings.hooks) return 0;
|
|
33
|
+
|
|
34
|
+
let pruned = 0;
|
|
35
|
+
for (const [eventName, entries] of Object.entries(settings.hooks)) {
|
|
36
|
+
if (!Array.isArray(entries)) continue;
|
|
37
|
+
|
|
38
|
+
const keptEntries = [];
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (isLegacyDescriptiveNamePrompt(entry)) {
|
|
41
|
+
pruned++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(entry?.hooks)) {
|
|
46
|
+
const keptHooks = entry.hooks.filter((hook) => {
|
|
47
|
+
if (isLegacyDescriptiveNamePrompt(hook)) {
|
|
48
|
+
pruned++;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
if (keptHooks.length > 0) keptEntries.push({ ...entry, hooks: keptHooks });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
keptEntries.push(entry);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (keptEntries.length === 0) {
|
|
61
|
+
delete settings.hooks[eventName];
|
|
62
|
+
} else {
|
|
63
|
+
settings.hooks[eventName] = keptEntries;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Object.keys(settings.hooks).length === 0) settings.hooks = undefined;
|
|
68
|
+
return pruned;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pruneSettingsFile(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
if (!filePath || !fs.existsSync(filePath)) return 0;
|
|
74
|
+
const stat = fs.statSync(filePath);
|
|
75
|
+
if (!stat.isFile() || stat.size > MAX_SETTINGS_BYTES) return 0;
|
|
76
|
+
|
|
77
|
+
const settings = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
78
|
+
const pruned = pruneHooks(settings);
|
|
79
|
+
if (pruned > 0) {
|
|
80
|
+
fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
81
|
+
}
|
|
82
|
+
return pruned;
|
|
83
|
+
} catch {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getHomeDir() {
|
|
89
|
+
return process.env.CK_TEST_HOME || os.homedir();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function addIfSettingsFile(files, filePath) {
|
|
93
|
+
if (!filePath) return;
|
|
94
|
+
const base = path.basename(filePath);
|
|
95
|
+
if (
|
|
96
|
+
base === "settings.json" ||
|
|
97
|
+
base === "settings.local.json" ||
|
|
98
|
+
base.endsWith(".settings.json")
|
|
99
|
+
) {
|
|
100
|
+
files.add(path.resolve(filePath));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function collectCandidateSettingsFiles() {
|
|
105
|
+
const files = new Set();
|
|
106
|
+
const initCwd = process.env.INIT_CWD;
|
|
107
|
+
if (initCwd && path.isAbsolute(initCwd)) {
|
|
108
|
+
addIfSettingsFile(files, path.join(initCwd, ".claude", "settings.json"));
|
|
109
|
+
addIfSettingsFile(files, path.join(initCwd, ".claude", "settings.local.json"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const homeDir = getHomeDir();
|
|
113
|
+
const globalClaudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, ".claude");
|
|
114
|
+
addIfSettingsFile(files, path.join(globalClaudeDir, "settings.json"));
|
|
115
|
+
addIfSettingsFile(files, path.join(globalClaudeDir, "settings.local.json"));
|
|
116
|
+
|
|
117
|
+
const ccsDir = process.env.CK_TEST_CCS_DIR || path.join(homeDir, ".ccs");
|
|
118
|
+
try {
|
|
119
|
+
for (const dirent of fs.readdirSync(ccsDir, { withFileTypes: true })) {
|
|
120
|
+
if (dirent.isFile()) addIfSettingsFile(files, path.join(ccsDir, dirent.name));
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Optional compatibility directory; skip when absent or unreadable.
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [...files];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function main() {
|
|
130
|
+
let pruned = 0;
|
|
131
|
+
const settingsFiles = collectCandidateSettingsFiles();
|
|
132
|
+
for (const filePath of settingsFiles) {
|
|
133
|
+
pruned += pruneSettingsFile(filePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const restored = restoreMissingHookRegistrations(settingsFiles);
|
|
137
|
+
|
|
138
|
+
if (pruned > 0 && process.env.CK_POSTINSTALL_DEBUG === "1") {
|
|
139
|
+
console.warn(`[claudekit-cli] Pruned ${pruned} legacy hook prompt(s)`);
|
|
140
|
+
}
|
|
141
|
+
if (restored > 0 && process.env.CK_POSTINSTALL_DEBUG === "1") {
|
|
142
|
+
console.warn(`[claudekit-cli] Restored ${restored} missing hook registration(s)`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main();
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
collectCandidateSettingsFiles,
|
|
150
|
+
countMissingCkHookRegistrations,
|
|
151
|
+
isLegacyDescriptiveNamePrompt,
|
|
152
|
+
pruneHooks,
|
|
153
|
+
pruneSettingsFile,
|
|
154
|
+
restoreMissingHookRegistrations,
|
|
155
|
+
};
|
package/cli-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
3
|
-
"generatedAt": "2026-05-
|
|
2
|
+
"version": "4.4.0-dev.2",
|
|
3
|
+
"generatedAt": "2026-05-30T14:38:02.947Z",
|
|
4
4
|
"commands": {
|
|
5
5
|
"agents": {
|
|
6
6
|
"name": "agents",
|
|
@@ -1188,6 +1188,10 @@
|
|
|
1188
1188
|
{
|
|
1189
1189
|
"flags": "--force-overwrite-settings",
|
|
1190
1190
|
"description": "Fully replace settings.json instead of selective merge"
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
"flags": "--restore-ck-hooks",
|
|
1194
|
+
"description": "Restore CK-managed hook registrations during update self-heal"
|
|
1191
1195
|
}
|
|
1192
1196
|
]
|
|
1193
1197
|
},
|
|
@@ -1635,6 +1639,12 @@
|
|
|
1635
1639
|
}
|
|
1636
1640
|
]
|
|
1637
1641
|
}
|
|
1642
|
+
],
|
|
1643
|
+
"sections": [
|
|
1644
|
+
{
|
|
1645
|
+
"title": "Agent Editing Reminder",
|
|
1646
|
+
"content": "After creating a plan, Claude Code agents should read `plan.md` and every generated `phase-*.md` before editing. The files already exist; Write/Edit without Read can be rejected after wasting tokens."
|
|
1647
|
+
}
|
|
1638
1648
|
]
|
|
1639
1649
|
},
|
|
1640
1650
|
{
|