@khanhcan148/mk 0.1.15 → 0.1.17
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 +7 -4
- package/package.json +1 -1
- package/src/commands/update.js +12 -2
- package/src/lib/constants.js +8 -0
- package/src/lib/copy.js +69 -5
package/README.md
CHANGED
|
@@ -81,15 +81,16 @@ cp -r .claude ~/.claude/
|
|
|
81
81
|
/mk-audit # Code archaeology chain: orientation, testing, heatmap, breaking-change analysis, technical debt, domain extraction
|
|
82
82
|
/mk-selftest # Self-validation checks on kit agents, skills, docs, workflows
|
|
83
83
|
/mk-log-analysis # Datadog + Azure App Insights log analysis with severity triage and Mermaid visual reports
|
|
84
|
+
/mk-wiki # Wiki management: staleness detection, batch refresh, bootstrap, stats, structural validation
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
## Structure
|
|
87
88
|
|
|
88
89
|
```
|
|
89
90
|
├── .claude/
|
|
90
|
-
│ ├── agents/ #
|
|
91
|
-
│ ├── skills/ #
|
|
92
|
-
│ │ ├── mk-*/ #
|
|
91
|
+
│ ├── agents/ # 32 agents (5 primary + 27 utility: implementers, quality, docs, specialized, concerns)
|
|
92
|
+
│ ├── skills/ # 67 skill packages (SKILL.md + scripts/references/assets)
|
|
93
|
+
│ │ ├── mk-*/ # 20 workflow commands (/mk-audit, /mk-brainstorm, /mk-log-analysis, /mk-overview, /mk-wiki, etc.)
|
|
93
94
|
│ │ └── ... # Domain skills (frontend, backend, testing, browser automation, etc.)
|
|
94
95
|
│ └── workflows/ # Development protocols
|
|
95
96
|
├── bin/ # CLI entry point (mk command)
|
|
@@ -139,6 +140,7 @@ User → /mk-* command (skill) → spawns utility agents → agents use knowledg
|
|
|
139
140
|
| `/mk-workflow` | Trace REST endpoint call chains with upstream caller detection, variant branching, side effects/feature flags, Mermaid diagrams |
|
|
140
141
|
| `/mk-log-analysis` | Analyze production logs from Datadog or Azure Application Insights via MCP; progressive severity triage, pattern detection, mandatory stack trace investigation, mk-debug integration |
|
|
141
142
|
| `/mk-selftest` | Run self-validation checks on kit agents, skills, docs, and workflows |
|
|
143
|
+
| `/mk-wiki` | Manage wiki health: staleness detection, batch refresh, bootstrap, stats, structural validation |
|
|
142
144
|
|
|
143
145
|
## Primary Agents
|
|
144
146
|
|
|
@@ -166,12 +168,13 @@ Spawned by primary agents for domain-specific work:
|
|
|
166
168
|
| **debugger** | Issue investigation and diagnosis |
|
|
167
169
|
| **refactorer** | Refactoring with TFD-first safety workflow |
|
|
168
170
|
|
|
169
|
-
**Docs & Project Management (
|
|
171
|
+
**Docs & Project Management (4)**
|
|
170
172
|
| Agent | Purpose |
|
|
171
173
|
|-------|---------|
|
|
172
174
|
| **project-manager** | Progress tracking, roadmap updates |
|
|
173
175
|
| **docs-manager** | Documentation maintenance |
|
|
174
176
|
| **journal-writer** | Technical difficulty documentation |
|
|
177
|
+
| **wiki-maintainer** | Background wiki writes to docs/llm-wiki/ (spawned by mk-* skills) |
|
|
175
178
|
|
|
176
179
|
**Research & Design (3)**
|
|
177
180
|
| Agent | Purpose |
|
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -133,6 +133,10 @@ export async function runUpdate(params = {}) {
|
|
|
133
133
|
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8'));
|
|
134
134
|
|
|
135
135
|
if (upToDate) {
|
|
136
|
+
// Always merge settings.json hooks even when kit files are unchanged.
|
|
137
|
+
// A user who installed before hooks were added (or whose settings.json was
|
|
138
|
+
// edited/reset) would otherwise never receive hook updates via `mk update`.
|
|
139
|
+
mergeSettingsJson(sourceDir, targetDir);
|
|
136
140
|
// Files are unchanged but we may still need to record the release version.
|
|
137
141
|
// Without this, manifest.version stays at the old value and the next `mk update`
|
|
138
142
|
// will always report "Update available" even though nothing changed on disk.
|
|
@@ -290,7 +294,8 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
290
294
|
compareVersions: cmpVersions = compareVersions,
|
|
291
295
|
promptUser = defaultPromptUser,
|
|
292
296
|
// Injectable for tests — allows overriding resolved paths without touching CWD
|
|
293
|
-
manifestPath: injectedManifestPath
|
|
297
|
+
manifestPath: injectedManifestPath,
|
|
298
|
+
targetDir: injectedTargetDir
|
|
294
299
|
} = deps;
|
|
295
300
|
|
|
296
301
|
// Read local package version (used as fallback when manifest has no version)
|
|
@@ -298,7 +303,7 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
298
303
|
readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8')
|
|
299
304
|
);
|
|
300
305
|
|
|
301
|
-
const targetDir = resolveTargetDir(options);
|
|
306
|
+
const targetDir = injectedTargetDir ?? resolveTargetDir(options);
|
|
302
307
|
const manifestPath = injectedManifestPath ?? resolveManifestPath(options);
|
|
303
308
|
|
|
304
309
|
let tempDir = null;
|
|
@@ -345,6 +350,11 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
345
350
|
|
|
346
351
|
if (!needsUpdate) {
|
|
347
352
|
process.stdout.write(chalk.green(`Already up to date (v${local}).\n`));
|
|
353
|
+
// Still merge settings.json hooks from the bundled local source.
|
|
354
|
+
// The installed settings.json may be missing hooks if it was created
|
|
355
|
+
// before hooks were added to the kit or if it was manually edited.
|
|
356
|
+
// resolveSourceDir() points to the CLI package's own .claude/ — no download needed.
|
|
357
|
+
mergeSettingsJson(resolveSourceDir(), targetDir);
|
|
348
358
|
return;
|
|
349
359
|
}
|
|
350
360
|
|
package/src/lib/constants.js
CHANGED
|
@@ -8,6 +8,14 @@ export const KIT_SUBDIRS = ['agents', 'skills', 'workflows', 'hooks'];
|
|
|
8
8
|
*/
|
|
9
9
|
export const MANIFEST_FILENAME = '.mk-manifest.json';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Skills that are internal to the kit and must NOT be distributed to end users.
|
|
13
|
+
* Each entry is matched as a full directory segment: `/skills/<name>/`.
|
|
14
|
+
* The trailing `/` in the match pattern prevents false positives on substring names
|
|
15
|
+
* (e.g. `mk-selftest-extended` is NOT matched by `mk-selftest`).
|
|
16
|
+
*/
|
|
17
|
+
export const KIT_INTERNAL_SKILLS = ['mk-selftest'];
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* Files/patterns to exclude during copy
|
|
13
21
|
*/
|
package/src/lib/copy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, relative } from 'node:path';
|
|
2
2
|
import { statSync, lstatSync, existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
3
3
|
import fsExtra from 'fs-extra';
|
|
4
|
-
import { KIT_SUBDIRS, COPY_FILTER_PATTERNS, WINDOWS_PATH_WARN_LENGTH } from './constants.js';
|
|
4
|
+
import { KIT_SUBDIRS, COPY_FILTER_PATTERNS, KIT_INTERNAL_SKILLS, WINDOWS_PATH_WARN_LENGTH } from './constants.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Check if a path should be filtered out during copy.
|
|
@@ -13,6 +13,10 @@ function shouldFilter(filePath) {
|
|
|
13
13
|
for (const pattern of COPY_FILTER_PATTERNS) {
|
|
14
14
|
if (normalized.includes(pattern)) return true;
|
|
15
15
|
}
|
|
16
|
+
// Exclude internal skills — see KIT_INTERNAL_SKILLS JSDoc in constants.js.
|
|
17
|
+
for (const skillName of KIT_INTERNAL_SKILLS) {
|
|
18
|
+
if (normalized.includes(`/skills/${skillName}/`)) return true;
|
|
19
|
+
}
|
|
16
20
|
return false;
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -85,6 +89,9 @@ export function collectDiskFiles(targetDir) {
|
|
|
85
89
|
* @param {string} targetDir - Absolute path to target .claude/
|
|
86
90
|
* @param {{ dryRun: boolean }} options
|
|
87
91
|
* @returns {Array<{ relativePath: string, absolutePath: string, sourceAbsPath: string, size: number }>}
|
|
92
|
+
* @remarks Naming convention: `absolutePath` is the destination (under targetDir),
|
|
93
|
+
* `sourceAbsPath` is the source (under sourceDir). The asymmetry is intentional —
|
|
94
|
+
* renaming would break consumers (update.js). See DEBT-016.
|
|
88
95
|
*/
|
|
89
96
|
export function copyKitFiles(sourceDir, targetDir, options = {}) {
|
|
90
97
|
const { dryRun = false } = options;
|
|
@@ -172,6 +179,36 @@ function backupFile(filePath) {
|
|
|
172
179
|
return backupPath;
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Returns true if siblingHooks already has an entry for the given event+matcher.
|
|
184
|
+
* @param {object|null} siblingHooks - Parsed hooks from settings.local.json, or null
|
|
185
|
+
* @param {string} event - Hook event name (e.g. 'PostToolUse')
|
|
186
|
+
* @param {string} matcher - Hook matcher (e.g. '*', 'Task')
|
|
187
|
+
*/
|
|
188
|
+
function isInSibling(siblingHooks, event, matcher) {
|
|
189
|
+
return !!siblingHooks?.[event]?.some(s => (s.matcher || '*') === matcher);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Filter kitEntries removing any whose event+matcher already exists in siblingHooks.
|
|
194
|
+
* Emits a stderr info message per skipped entry.
|
|
195
|
+
* @param {object[]} kitEntries - Hook entries from kit source
|
|
196
|
+
* @param {object|null} siblingHooks - Parsed hooks from settings.local.json, or null
|
|
197
|
+
* @param {string} event - Hook event name
|
|
198
|
+
* @returns {object[]} Entries not present in sibling
|
|
199
|
+
*/
|
|
200
|
+
function dedupeAgainstSibling(kitEntries, siblingHooks, event) {
|
|
201
|
+
if (!siblingHooks?.[event]) return kitEntries;
|
|
202
|
+
return kitEntries.filter(ke => {
|
|
203
|
+
const m = ke.matcher || '*';
|
|
204
|
+
if (isInSibling(siblingHooks, event, m)) {
|
|
205
|
+
process.stderr.write(`mk: hook ${event}[${m}] skipped (exists in settings.local.json)\n`);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
/**
|
|
176
213
|
* Merge kit's settings.json into user's existing settings.json.
|
|
177
214
|
* Strategy: merge "hooks" key from kit source; preserve all other user keys.
|
|
@@ -201,14 +238,33 @@ export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
|
201
238
|
return { action: 'skipped' };
|
|
202
239
|
}
|
|
203
240
|
|
|
241
|
+
// Read settings.local.json once for sibling dedup guard.
|
|
242
|
+
// Applies to both the 'created' and 'merged' paths — Claude Code loads hooks from
|
|
243
|
+
// all settings files simultaneously; if a user already has a hook in settings.local.json,
|
|
244
|
+
// writing the same event+matcher to settings.json would cause both to fire.
|
|
245
|
+
let siblingHooks = null;
|
|
246
|
+
try {
|
|
247
|
+
const siblingPath = join(targetDir, 'settings.local.json');
|
|
248
|
+
siblingHooks = JSON.parse(readFileSync(siblingPath, 'utf-8')).hooks || null;
|
|
249
|
+
} catch {
|
|
250
|
+
// Missing or malformed sibling — proceed as if no sibling exists.
|
|
251
|
+
}
|
|
252
|
+
|
|
204
253
|
// No existing user settings — create with hooks only (not permissions or other keys).
|
|
205
254
|
// Copying the full kit settings.json would duplicate permissions.deny entries when both
|
|
206
255
|
// global (~/.claude/settings.json) and project (.claude/settings.json) are initialised.
|
|
207
256
|
if (!existsSync(destPath)) {
|
|
208
257
|
if (!dryRun) {
|
|
209
|
-
const hooksOnly =
|
|
258
|
+
const hooksOnly = {};
|
|
259
|
+
if (kitSettings.hooks) {
|
|
260
|
+
for (const [event, kitEntries] of Object.entries(kitSettings.hooks)) {
|
|
261
|
+
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event);
|
|
262
|
+
if (deduped.length > 0) hooksOnly[event] = deduped;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
210
265
|
mkdirSync(targetDir, { recursive: true });
|
|
211
|
-
|
|
266
|
+
const payload = Object.keys(hooksOnly).length > 0 ? { hooks: hooksOnly } : {};
|
|
267
|
+
writeFileSync(destPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
212
268
|
}
|
|
213
269
|
return { action: 'created' };
|
|
214
270
|
}
|
|
@@ -231,11 +287,19 @@ export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
|
231
287
|
if (!userSettings.hooks) userSettings.hooks = {};
|
|
232
288
|
for (const [event, kitEntries] of Object.entries(kitSettings.hooks)) {
|
|
233
289
|
if (!userSettings.hooks[event]) {
|
|
234
|
-
|
|
235
|
-
|
|
290
|
+
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event);
|
|
291
|
+
if (deduped.length > 0) {
|
|
292
|
+
userSettings.hooks[event] = deduped;
|
|
293
|
+
merged.push(event);
|
|
294
|
+
}
|
|
236
295
|
} else {
|
|
237
296
|
for (const kitEntry of kitEntries) {
|
|
238
297
|
const kitMatcher = kitEntry.matcher || '*';
|
|
298
|
+
// Dedup guard: skip if sibling already has this event+matcher
|
|
299
|
+
if (isInSibling(siblingHooks, event, kitMatcher)) {
|
|
300
|
+
process.stderr.write(`mk: hook ${event}[${kitMatcher}] skipped (exists in settings.local.json)\n`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
239
303
|
const idx = userSettings.hooks[event].findIndex(
|
|
240
304
|
e => (e.matcher || '*') === kitMatcher
|
|
241
305
|
);
|