@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 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/ # 31 agents (5 primary + 26 utility: implementers, quality, docs, specialized, concerns)
91
- │ ├── skills/ # 65 skill packages (SKILL.md + scripts/references/assets)
92
- │ │ ├── mk-*/ # 19 workflow commands (/mk-audit, /mk-brainstorm, /mk-log-analysis, /mk-overview, /mk-selftest, etc.)
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 (3)**
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanhcan148/mk",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "CLI to install and manage MyClaudeKit (.claude/) in your projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
@@ -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 = kitSettings.hooks ? { hooks: kitSettings.hooks } : {};
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
- writeFileSync(destPath, JSON.stringify(hooksOnly, null, 2) + '\n', 'utf-8');
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
- userSettings.hooks[event] = kitEntries;
235
- merged.push(event);
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
  );