@luanpdd/kit-mcp 1.15.0 → 1.16.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/kit/file-manifest.json +2 -2
- package/package.json +5 -3
- package/src/cli/index.js +22 -5
- package/src/core/reverse-sync.js +23 -6
- package/src/core/sync.js +35 -4
- package/src/core/ui.js +19 -1
- package/src/core/watch.js +29 -3
package/kit/file-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"timestamp": "2026-05-
|
|
2
|
+
"version": "1.16.0",
|
|
3
|
+
"timestamp": "2026-05-09T14:17:38.936Z",
|
|
4
4
|
"files": {
|
|
5
5
|
"COMANDOS.md": "d24ec61a6ec35db314cc5f2ae287bfb927b794789c8f1d558c55862f5e6534b2",
|
|
6
6
|
"COMPATIBILITY.md": "794e336a87045cdf0161785b9a7a0975a49abbd80bdd816b8852251fcc8126ca",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,11 +47,13 @@
|
|
|
47
47
|
"prepublishOnly": "node scripts/regen-manifest.js && node scripts/update-readme-counts.js && node test/run.mjs test/unit && node test/run.mjs test/integration"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@inquirer/prompts": "^8.4.2",
|
|
51
50
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
52
|
-
"chokidar": "^5.0.0",
|
|
53
51
|
"commander": "^14.0.3",
|
|
54
52
|
"open": "^11.0.0",
|
|
55
53
|
"picocolors": "^1.1.1"
|
|
54
|
+
},
|
|
55
|
+
"optionalDependencies": {
|
|
56
|
+
"@inquirer/prompts": "^8.4.2",
|
|
57
|
+
"chokidar": "^5.0.0"
|
|
56
58
|
}
|
|
57
59
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -29,11 +29,16 @@ import { listReplays, loadReplay } from '../core/replays.js';
|
|
|
29
29
|
import { installMcp, listInstallTargets } from '../mcp-server/install.js';
|
|
30
30
|
import * as render from './render.js';
|
|
31
31
|
import { c, icons, spinner, progress, select, confirm } from '../core/ui.js';
|
|
32
|
-
import { createServer } from '../ui/server.js';
|
|
33
32
|
import { readLock, lockPathFor } from '../ui/lockfile.js';
|
|
34
|
-
import { wrapProgressForUi } from '../ui/wrapper.js';
|
|
35
|
-
import { openBrowser } from '../ui/browser.js';
|
|
36
33
|
import { checkUpgrade, getLocalVersion } from './upgrade-check.js';
|
|
34
|
+
// PERF-16-04: ui/server.js, ui/wrapper.js, ui/browser.js are loaded LAZILY
|
|
35
|
+
// inside the subcommand handlers that need them. See:
|
|
36
|
+
// - maybeWrapForUi (gated on lockfile presence)
|
|
37
|
+
// - ui.start (createServer + openBrowser)
|
|
38
|
+
// - ui.open (openBrowser)
|
|
39
|
+
// This trims ~700 LOC + transitive deps off the cold-start path of non-UI
|
|
40
|
+
// commands like `kit kit list-agents --terse`. lockfile.js stays eager because
|
|
41
|
+
// readLock() is called by every withProgress() invocation and is dep-free.
|
|
37
42
|
import http from 'node:http';
|
|
38
43
|
import fs from 'node:fs';
|
|
39
44
|
import os from 'node:os';
|
|
@@ -106,7 +111,7 @@ async function withProgress(label, total, fn, { tool, projectRoot } = {}) {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// Auto-wrap if a sidecar is running for this projectRoot.
|
|
109
|
-
const wrapper = maybeWrapForUi(onProgress, { tool, projectRoot });
|
|
114
|
+
const wrapper = await maybeWrapForUi(onProgress, { tool, projectRoot });
|
|
110
115
|
try {
|
|
111
116
|
const r = await fn(wrapper);
|
|
112
117
|
if (p) p.finish(label);
|
|
@@ -121,7 +126,11 @@ async function withProgress(label, total, fn, { tool, projectRoot } = {}) {
|
|
|
121
126
|
|
|
122
127
|
// maybeWrapForUi — returns the original callback unchanged when no sidecar is up
|
|
123
128
|
// or the user opted out. Otherwise returns a wrapped callback with .done/.error.
|
|
124
|
-
|
|
129
|
+
//
|
|
130
|
+
// PERF-16-04: this is async because we lazy-load ../ui/wrapper.js only when a
|
|
131
|
+
// sidecar lockfile is detected. Common path (no sidecar) returns synchronously
|
|
132
|
+
// via passthroughWrapper without touching wrapper.js or its transitive deps.
|
|
133
|
+
async function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
|
|
125
134
|
const globalOpts = program.opts();
|
|
126
135
|
// commander stores `--no-ui` as opts.ui === false
|
|
127
136
|
if (globalOpts.ui === false || process.env.KIT_MCP_NO_UI === '1') {
|
|
@@ -131,6 +140,8 @@ function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
|
|
|
131
140
|
if (!readLock(root)) {
|
|
132
141
|
return passthroughWrapper(onProgress);
|
|
133
142
|
}
|
|
143
|
+
// Lazy import — only paid when a sidecar IS up for this project.
|
|
144
|
+
const { wrapProgressForUi } = await import('../ui/wrapper.js');
|
|
134
145
|
return wrapProgressForUi(onProgress, { projectRoot: root, tool: tool ?? null });
|
|
135
146
|
}
|
|
136
147
|
|
|
@@ -407,6 +418,8 @@ ui.command('start')
|
|
|
407
418
|
const projectRoot = opts.projectRoot || process.cwd();
|
|
408
419
|
const port = opts.port ? Number(opts.port) : undefined;
|
|
409
420
|
const idleMs = opts.idleMs !== undefined ? Number(opts.idleMs) : undefined;
|
|
421
|
+
// PERF-16-04: lazy-load the sidecar HTTP server module only when starting it.
|
|
422
|
+
const { createServer } = await import('../ui/server.js');
|
|
410
423
|
const srv = createServer({ projectRoot, idleMs });
|
|
411
424
|
try {
|
|
412
425
|
const { port: actualPort } = await srv.start({ port });
|
|
@@ -422,6 +435,8 @@ ui.command('start')
|
|
|
422
435
|
}
|
|
423
436
|
}).catch(() => { /* offline / silent */ });
|
|
424
437
|
if (opts.open !== false) {
|
|
438
|
+
// PERF-16-04: lazy-load browser-opener (it lazy-loads `open` package itself).
|
|
439
|
+
const { openBrowser } = await import('../ui/browser.js');
|
|
425
440
|
await openBrowser(url);
|
|
426
441
|
}
|
|
427
442
|
// The server's own SIGINT handler will perform shutdown + cleanup.
|
|
@@ -480,6 +495,8 @@ ui.command('open')
|
|
|
480
495
|
const lock = readLock(projectRoot);
|
|
481
496
|
if (!lock) return fail('no sidecar running — start one with `kit ui start`');
|
|
482
497
|
const url = `http://127.0.0.1:${lock.port}/`;
|
|
498
|
+
// PERF-16-04: lazy-load browser-opener.
|
|
499
|
+
const { openBrowser } = await import('../ui/browser.js');
|
|
483
500
|
const r = await openBrowser(url, { force: true });
|
|
484
501
|
if (!r.opened) {
|
|
485
502
|
process.stderr.write(`${c.yellow(icons.warn)} could not open browser (${r.reason}); copy the URL above\n`);
|
package/src/core/reverse-sync.js
CHANGED
|
@@ -31,17 +31,34 @@ export async function detectReverse(targetId, opts = {}) {
|
|
|
31
31
|
|
|
32
32
|
const candidates = [];
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
// PERF-16-03: parallelize the 5 scans. Each scan reads a distinct subdirectory
|
|
35
|
+
// of the IDE layout (.claude/agents, .claude/commands, .claude/skills,
|
|
36
|
+
// .claude/framework, .claude/hooks) — there is no I/O contention between them.
|
|
37
|
+
//
|
|
38
|
+
// Each scan continues to push into the shared `candidates` array. This is safe
|
|
39
|
+
// under the single-threaded JS event loop: `Array.prototype.push` is a
|
|
40
|
+
// synchronous operation that completes between awaits, so concurrent scans
|
|
41
|
+
// never produce a torn write. The trade-off is that candidate ordering is no
|
|
42
|
+
// longer deterministic across categories — existing reverse-sync tests use
|
|
43
|
+
// `.find` / `.some` / `.filter` and never index `candidates[N]`, so this is a
|
|
44
|
+
// safe widening of the contract.
|
|
45
|
+
//
|
|
46
|
+
// Error semantics: `Promise.all` rejects on the first rejection — identical to
|
|
47
|
+
// the previous sequential `await` chain (which also propagated the first error
|
|
48
|
+
// and aborted the rest). Fail-fast is preserved.
|
|
49
|
+
const pending = [];
|
|
50
|
+
|
|
51
|
+
if (target.agents) pending.push(scanCapability(candidates, 'agent', target.agents, projectRoot, kit.agents, kitRoot));
|
|
52
|
+
if (target.commands) pending.push(scanCapability(candidates, 'command', target.commands, projectRoot, kit.commands, kitRoot));
|
|
53
|
+
if (target.skills) pending.push(scanSkills (candidates, target.skills, projectRoot, [...kit.skills, ...kit.skillsExtras], kitRoot));
|
|
39
54
|
for (const cap of ['framework', 'hooks']) {
|
|
40
55
|
const spec = target[cap];
|
|
41
56
|
if (!spec || spec.mode !== 'mirror-tree') continue;
|
|
42
|
-
|
|
57
|
+
pending.push(scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot));
|
|
43
58
|
}
|
|
44
59
|
|
|
60
|
+
await Promise.all(pending);
|
|
61
|
+
|
|
45
62
|
return { target: targetId, projectRoot, kitRoot, candidates };
|
|
46
63
|
}
|
|
47
64
|
|
package/src/core/sync.js
CHANGED
|
@@ -19,6 +19,18 @@ const STUB_MARKER = '<!-- kit-mcp:reference -->';
|
|
|
19
19
|
const MANAGED_MARKER_FILE = '.kit-mcp-managed';
|
|
20
20
|
const MANAGED_MARKER_BODY = '# Managed by @luanpdd/kit-mcp — this directory is overwritten on every `kit sync install`.\n# Do not edit files here directly; edit the canonical source under kit/ and re-run sync.\n# Removing this file disables `kit sync remove` cleanup of this tree.\n';
|
|
21
21
|
|
|
22
|
+
// PERF-16-01: parallelize file writes in syncTo() via Promise.all batches.
|
|
23
|
+
// BATCH_SIZE=16 default — safe under Linux ulimit 1024 fd default and
|
|
24
|
+
// macOS/Windows equivalents. Configurable via env (e.g. on slow disks).
|
|
25
|
+
// Values outside [1, 256] fall back to 16 (defensive — env vars are strings).
|
|
26
|
+
function resolveBatchSize() {
|
|
27
|
+
const raw = process.env.KIT_MCP_SYNC_BATCH_SIZE;
|
|
28
|
+
if (!raw) return 16;
|
|
29
|
+
const n = Number.parseInt(raw, 10);
|
|
30
|
+
if (!Number.isFinite(n) || n < 1 || n > 256) return 16;
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
export async function syncTo(targetId, opts = {}) {
|
|
23
35
|
const target = getTarget(targetId);
|
|
24
36
|
const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
|
|
@@ -100,16 +112,35 @@ export async function syncTo(targetId, opts = {}) {
|
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
if (!dryRun) {
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
const BATCH_SIZE = resolveBatchSize();
|
|
116
|
+
let completed = 0;
|
|
117
|
+
const total = ops.length;
|
|
118
|
+
|
|
119
|
+
// Apply one op (mkdir + write or copy + onProgress).
|
|
120
|
+
// Each op is independent: ops[] is built so writes don't share parent
|
|
121
|
+
// directories that need ordering — mkdir({recursive:true}) is idempotent
|
|
122
|
+
// even when 16 ops race for the same parent dir.
|
|
123
|
+
const applyOp = async (op) => {
|
|
105
124
|
await fs.mkdir(path.dirname(op.path), { recursive: true });
|
|
106
125
|
if (op.treeCopy) {
|
|
107
126
|
await fs.copyFile(op.srcAbs, op.path);
|
|
108
127
|
} else {
|
|
109
128
|
await fs.writeFile(op.path, op.content, 'utf8');
|
|
110
129
|
}
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
// Counter increment is single-threaded by JS event loop semantics —
|
|
131
|
+
// no torn reads even with 16 ops resolving in any order.
|
|
132
|
+
completed += 1;
|
|
133
|
+
onProgress({ phase: op.kind, current: completed, total, label: path.basename(op.path) });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Sequential batches — within a batch, Promise.all parallelizes writes;
|
|
137
|
+
// between batches, we await to bound max-in-flight at BATCH_SIZE. If any
|
|
138
|
+
// op in a batch rejects, Promise.all rejects on first failure (matches
|
|
139
|
+
// existing behavior — sync.js had no retry logic, so a single fs error
|
|
140
|
+
// already aborted the install).
|
|
141
|
+
for (let i = 0; i < ops.length; i += BATCH_SIZE) {
|
|
142
|
+
const slice = ops.slice(i, i + BATCH_SIZE);
|
|
143
|
+
await Promise.all(slice.map(applyOp));
|
|
113
144
|
}
|
|
114
145
|
}
|
|
115
146
|
|
package/src/core/ui.js
CHANGED
|
@@ -10,7 +10,23 @@
|
|
|
10
10
|
// - Zero hidden globals — every primitive is a plain function/class.
|
|
11
11
|
|
|
12
12
|
import pc from 'picocolors';
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
// PERF-16-05: @inquirer/prompts é optionalDependency. Carregamos lazy dentro
|
|
15
|
+
// de select()/confirm() para que (a) modo MCP server e CI não paguem o custo
|
|
16
|
+
// de boot, e (b) `npm install --omit=optional` produza CLI core funcional
|
|
17
|
+
// (apenas comandos interativos falham com mensagem descritiva).
|
|
18
|
+
let _inquirerModule = null;
|
|
19
|
+
async function loadInquirer() {
|
|
20
|
+
if (_inquirerModule) return _inquirerModule;
|
|
21
|
+
try {
|
|
22
|
+
_inquirerModule = await import('@inquirer/prompts');
|
|
23
|
+
return _inquirerModule;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'Interactive prompts require @inquirer/prompts. Install with `npm i @inquirer/prompts` or pass --yes / --no-interactive to skip the prompt.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
// --- color helpers ---
|
|
16
32
|
|
|
@@ -127,6 +143,7 @@ export async function select(opts) {
|
|
|
127
143
|
if (!process.stdin.isTTY) {
|
|
128
144
|
throw new Error('Interactive prompt unavailable: stdin is not a TTY. Pass the value as a flag instead.');
|
|
129
145
|
}
|
|
146
|
+
const { select: inqSelect } = await loadInquirer();
|
|
130
147
|
return inqSelect(opts);
|
|
131
148
|
}
|
|
132
149
|
|
|
@@ -134,6 +151,7 @@ export async function confirm(opts) {
|
|
|
134
151
|
if (!process.stdin.isTTY) {
|
|
135
152
|
throw new Error('Interactive prompt unavailable: stdin is not a TTY. Pass --yes to skip confirmation.');
|
|
136
153
|
}
|
|
154
|
+
const { confirm: inqConfirm } = await loadInquirer();
|
|
137
155
|
return inqConfirm(opts);
|
|
138
156
|
}
|
|
139
157
|
|
package/src/core/watch.js
CHANGED
|
@@ -11,16 +11,35 @@
|
|
|
11
11
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import fs from 'node:fs/promises';
|
|
14
|
-
import chokidar from 'chokidar';
|
|
15
14
|
import { syncTo } from './sync.js';
|
|
16
15
|
import { listTargets } from './registry.js';
|
|
17
|
-
import { resolveKitRoot } from './kit.js';
|
|
16
|
+
import { resolveKitRoot, clearKitCache } from './kit.js';
|
|
17
|
+
|
|
18
|
+
// PERF-16-06: chokidar é optionalDependency. Carregamos lazy dentro de watchKit()
|
|
19
|
+
// para que (a) `kit sync install` (que NÃO usa watch) não pague o custo de boot,
|
|
20
|
+
// e (b) `npm install --omit=optional` produza CLI core funcional (apenas
|
|
21
|
+
// `kit sync watch` falha com mensagem descritiva).
|
|
22
|
+
let _chokidarModule = null;
|
|
23
|
+
async function loadChokidar() {
|
|
24
|
+
if (_chokidarModule) return _chokidarModule;
|
|
25
|
+
try {
|
|
26
|
+
const mod = await import('chokidar');
|
|
27
|
+
_chokidarModule = mod.default || mod;
|
|
28
|
+
return _chokidarModule;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'kit sync watch requires chokidar. Install with `npm i chokidar` or use `kit sync install <target>` for one-shot syncing instead.'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
18
35
|
|
|
19
36
|
export async function watchKit(targets, opts = {}) {
|
|
20
37
|
const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
|
|
21
38
|
const kitRoot = resolveKitRoot(opts.kitRoot);
|
|
22
39
|
const mode = opts.mode ?? 'reference';
|
|
23
|
-
|
|
40
|
+
// PERF-16-02: bump default 300 → 500ms to coalesce IDE save-bursts (typical
|
|
41
|
+
// IDE auto-save fires 5-10 events in < 500ms during a single user save).
|
|
42
|
+
const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 500;
|
|
24
43
|
const onLog = opts.onLog ?? (() => {});
|
|
25
44
|
|
|
26
45
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
@@ -33,6 +52,7 @@ export async function watchKit(targets, opts = {}) {
|
|
|
33
52
|
onLog(`✓ initial sync → ${t} (${r.written.length} files)`);
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
const chokidar = await loadChokidar();
|
|
36
56
|
const watcher = chokidar.watch(kitRoot, {
|
|
37
57
|
ignored: (p) => /(^|[/\\])\.[^/\\]/.test(p), // ignore dotfiles/dotdirs
|
|
38
58
|
persistent: true,
|
|
@@ -46,6 +66,12 @@ export async function watchKit(targets, opts = {}) {
|
|
|
46
66
|
if (pending) clearTimeout(pending);
|
|
47
67
|
pending = setTimeout(async () => {
|
|
48
68
|
pending = null;
|
|
69
|
+
// PERF-16-02: invalidate kitCache (TTL 30s in kit.js PERF-01) BEFORE
|
|
70
|
+
// re-sync — otherwise listKit() inside syncTo can return the pre-edit
|
|
71
|
+
// cached value if the burst happened within the TTL window. Coalescing
|
|
72
|
+
// the edit-burst via debounce means clearKitCache fires AT MOST ONCE
|
|
73
|
+
// per 500ms window, regardless of how many save events came in.
|
|
74
|
+
clearKitCache();
|
|
49
75
|
for (const t of targets) {
|
|
50
76
|
try {
|
|
51
77
|
const r = await syncTo(t, { projectRoot, kitRoot, mode });
|