@chemx/starter-kit 26.9.21-432 → 26.9.23-1269
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/cli/audit/prompts.js +1 -1
- package/cli/commands/cmd-project.js +100 -0
- package/cli/commands/cmd-router.js +21 -1
- package/cli/commands-schema.js +14 -0
- package/cli/create.spec.js +2 -2
- package/cli/help.js +1 -1
- package/cli/index.js +16 -6
- package/cli/installer-templates.js +1 -1
- package/cli/mcp/installer.js +7 -7
- package/cli/mcp/installer.spec.js +3 -3
- package/cli/mcp/manifests.js +2 -1
- package/cli/mcp/prompts.js +1 -1
- package/cli/mcp/server.js +2 -0
- package/cli/mcp/tools-audit.js +95 -0
- package/cli/mcp/tools-generate.js +62 -0
- package/cli/mcp/tools-patch.js +72 -0
- package/cli/mcp/tools-patterns.js +71 -0
- package/cli/mcp/tools-project.js +75 -0
- package/cli/mcp/tools-project.spec.js +51 -0
- package/cli/mcp/tools-q-modes.js +69 -0
- package/cli/mcp/tools-q.js +58 -0
- package/cli/mcp/tools-read.js +69 -0
- package/cli/mcp/tools-search-util.js +35 -0
- package/cli/mcp/tools-search.js +15 -300
- package/cli/mcp/tools-team-feed.js +57 -0
- package/cli/mcp/tools-team-locks.js +63 -0
- package/cli/mcp/tools-team-mailbox.js +32 -0
- package/cli/mcp/tools-team-tasks.js +84 -0
- package/cli/mcp/tools-team.js +37 -183
- package/cli/mcp/tools-verify.js +47 -0
- package/cli/mcp/tools-write.js +33 -0
- package/cli/mcp/tools.js +72 -407
- package/cli/pillars-schema.js +3 -3
- package/cli/reader-shorthand.spec.js +60 -0
- package/cli/reader.js +67 -24
- package/cli/search-bulletproof.spec.js +104 -0
- package/cli/search-db.js +33 -5
- package/cli/search-schema.js +24 -4
- package/cli/search.js +9 -2
- package/cli/team/index.js +9 -0
- package/cli/team/team-commands.js +104 -64
- package/cli/team/team-concurrency-stress.spec.js +95 -0
- package/cli/team/team-db-mailbox.js +86 -0
- package/cli/team/team-db-task-helpers.js +28 -30
- package/cli/team/team-db-tasks.js +5 -2
- package/cli/team/team-flags.js +69 -0
- package/cli/team/team-format.js +87 -46
- package/cli/team/team-mailbox-empirical.spec.js +191 -0
- package/cli/team/team-mailbox.spec.js +93 -0
- package/cli/team/team-memory.js +84 -0
- package/cli/team/team-memory.spec.js +74 -0
- package/cli/team/team-projects-coordinator.js +100 -0
- package/cli/team/team-projects-format.js +45 -0
- package/cli/team/team-projects-memory.js +70 -0
- package/cli/team/team-projects.js +94 -0
- package/cli/team/team-projects.spec.js +116 -0
- package/cli/team/team-schema-memory.js +25 -0
- package/cli/team/team-schema-projects.js +50 -0
- package/cli/team/team-schema.js +27 -12
- package/cli/team/team-tasks-hierarchy.spec.js +80 -0
- package/cli/team/team-tokens.js +95 -0
- package/cli/team/team-triage.js +130 -10
- package/cli/team/team-verification-gate.spec.js +65 -4
- package/cli/team/team.spec.js +1 -1
- package/cli/ui-e2e-verification.spec.js +10 -2
- package/cli/ui-handlers.js +10 -2
- package/cli/ui-index.html +33 -10
- package/cli/ui-server.js +7 -0
- package/cli/ui-sse.js +57 -0
- package/cli/ui-sse.spec.js +74 -0
- package/cli/ui.spec.js +6 -1
- package/package.json +1 -1
- package/scripts/pre-commit.sh +2 -7
package/cli/audit/prompts.js
CHANGED
|
@@ -296,7 +296,7 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
296
296
|
lines.push('1. Pre-Split Pattern Discovery: Audit recurring UI layouts, state machines, and predicates across monoliths first; extract canonical shared capsules before slicing to prevent bespoke pattern duplication.');
|
|
297
297
|
lines.push('2. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
|
|
298
298
|
lines.push('3. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
|
|
299
|
-
lines.push('4. Composable Return Contracts: Custom hooks/composables must
|
|
299
|
+
lines.push('4. Composable Return Contracts: Custom hooks/composables must classify returns into flat State, Status, and verb-prefixed Actions buckets (Directive 4.A).');
|
|
300
300
|
lines.push('5. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
|
|
301
301
|
lines.push('6. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
302
302
|
if (!isSubSection) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-project.js: Persistent Coordinator and Projects CLI Handler
|
|
3
|
+
* Dispatches init, status, step, chat, pause, resume, and learnings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { openIndexDb } from '../search-db.js';
|
|
7
|
+
import {
|
|
8
|
+
initProjectSession, getActiveProjectSession, getProjectSession,
|
|
9
|
+
postProjectMessage, getProjectMessages, updateProjectSession
|
|
10
|
+
} from '../team/team-projects.js';
|
|
11
|
+
import { queryRelevantLearnings, formatLearningsCard } from '../team/team-projects-memory.js';
|
|
12
|
+
import { executeCoordinatorStep } from '../team/team-projects-coordinator.js';
|
|
13
|
+
import { formatProjectStatusCard } from '../team/team-projects-format.js';
|
|
14
|
+
import { listTasks } from '../team/team-db-tasks.js';
|
|
15
|
+
|
|
16
|
+
export const runProjectCli = async (rawArgs = [], isCli = false, cwd = process.cwd()) => {
|
|
17
|
+
const db = openIndexDb(cwd);
|
|
18
|
+
if (!db) {
|
|
19
|
+
if (isCli) process.stderr.write('\x1b[31m✕ SQLite database unavailable.\x1b[0m\n');
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isJson = rawArgs.includes('--json');
|
|
24
|
+
const nonFlag = rawArgs.filter((a) => !a.startsWith('-'));
|
|
25
|
+
const subCommand = nonFlag[0] || 'status';
|
|
26
|
+
const restText = nonFlag.slice(1).join(' ');
|
|
27
|
+
|
|
28
|
+
if (subCommand === 'init' || subCommand === 'start') {
|
|
29
|
+
const goal = restText || 'Autonomous codebase optimization';
|
|
30
|
+
const session = initProjectSession(db, { goal, title: goal.slice(0, 40) });
|
|
31
|
+
if (isCli) {
|
|
32
|
+
if (isJson) process.stdout.write(`${JSON.stringify(session, null, 2)}\n`);
|
|
33
|
+
else process.stdout.write(`\x1b[32m✔\x1b[0m Initialized project session #${session.id}: "${session.title}"\n`);
|
|
34
|
+
}
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (subCommand === 'step' || subCommand === 'turn') {
|
|
39
|
+
const res = executeCoordinatorStep(db, { cwd });
|
|
40
|
+
if (isCli) {
|
|
41
|
+
if (isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
42
|
+
else process.stdout.write(`\x1b[36m⚡ [Turn ${res.turn || 0}]\x1b[0m ${res.action || res.status} - Status: ${res.status}\n`);
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (subCommand === 'chat' || subCommand === 'msg') {
|
|
48
|
+
const active = getActiveProjectSession(db);
|
|
49
|
+
if (!active) {
|
|
50
|
+
if (isCli) process.stderr.write('\x1b[31m✕ No active project. Run "chemx project init <goal>" first.\x1b[0m\n');
|
|
51
|
+
return { error: 'no_active_session' };
|
|
52
|
+
}
|
|
53
|
+
const msg = postProjectMessage(db, {
|
|
54
|
+
projectId: active.id,
|
|
55
|
+
turnIndex: active.current_turn,
|
|
56
|
+
authorId: '@user',
|
|
57
|
+
role: 'user',
|
|
58
|
+
message: restText || 'Status report'
|
|
59
|
+
});
|
|
60
|
+
if (isCli) {
|
|
61
|
+
if (isJson) process.stdout.write(`${JSON.stringify(msg, null, 2)}\n`);
|
|
62
|
+
else process.stdout.write(`\x1b[32m✔\x1b[0m Sent message to coordinator: "${msg.message}"\n`);
|
|
63
|
+
}
|
|
64
|
+
return msg;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (subCommand === 'learnings' || subCommand === 'memory') {
|
|
68
|
+
const learnings = queryRelevantLearnings(db, { limit: 10 });
|
|
69
|
+
if (isCli) {
|
|
70
|
+
if (isJson) process.stdout.write(`${JSON.stringify(learnings, null, 2)}\n`);
|
|
71
|
+
else process.stdout.write(formatLearningsCard(learnings));
|
|
72
|
+
}
|
|
73
|
+
return learnings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (subCommand === 'pause' || subCommand === 'resume') {
|
|
77
|
+
const active = getActiveProjectSession(db);
|
|
78
|
+
if (!active) return null;
|
|
79
|
+
const newStatus = subCommand === 'pause' ? 'paused' : 'active';
|
|
80
|
+
const updated = updateProjectSession(db, active.id, { status: newStatus });
|
|
81
|
+
if (isCli) process.stdout.write(`\x1b[32m✔\x1b[0m Project #${active.id} status updated to: ${newStatus}\n`);
|
|
82
|
+
return updated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default: status
|
|
86
|
+
const session = getActiveProjectSession(db);
|
|
87
|
+
const messages = session ? getProjectMessages(db, session.id, 5) : [];
|
|
88
|
+
const activeTasks = session ? listTasks(db, { status: 'in_progress' }) : [];
|
|
89
|
+
const learnings = queryRelevantLearnings(db, { limit: 5 });
|
|
90
|
+
|
|
91
|
+
if (isJson) {
|
|
92
|
+
const payload = { session, messages, activeTasks, learnings };
|
|
93
|
+
if (isCli) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const card = formatProjectStatusCard({ session, messages, activeTasks, learnings });
|
|
98
|
+
if (isCli) process.stdout.write(card);
|
|
99
|
+
return { session, messages, activeTasks, learnings };
|
|
100
|
+
};
|
|
@@ -23,6 +23,25 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
23
23
|
runTeamCli(rawArgs.slice(1), true);
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
|
+
case 'project':
|
|
27
|
+
case 'coordinator': {
|
|
28
|
+
const { runProjectCli } = await import('./cmd-project.js');
|
|
29
|
+
await runProjectCli(rawArgs.slice(1), true);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case 'tokens':
|
|
33
|
+
case 'telemetry': {
|
|
34
|
+
const { runTeamCli } = await import('../team/index.js');
|
|
35
|
+
runTeamCli(['tokens', ...rawArgs.slice(1)], true);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case 'benchmark':
|
|
39
|
+
case 'ablation':
|
|
40
|
+
case 'memory': {
|
|
41
|
+
const { runTeamCli } = await import('../team/index.js');
|
|
42
|
+
runTeamCli(['benchmark', ...rawArgs.slice(1)], true);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
26
45
|
case 'mcp':
|
|
27
46
|
case 'mcp-server':
|
|
28
47
|
case 'server': {
|
|
@@ -31,7 +50,8 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
31
50
|
break;
|
|
32
51
|
}
|
|
33
52
|
case 'read':
|
|
34
|
-
case 'view':
|
|
53
|
+
case 'view':
|
|
54
|
+
case 'r': {
|
|
35
55
|
const { runReaderCli } = await import('../reader.js');
|
|
36
56
|
runReaderCli(rawArgs.slice(1), true);
|
|
37
57
|
break;
|
package/cli/commands-schema.js
CHANGED
|
@@ -194,5 +194,19 @@ export const COMMANDS_SCHEMA = [
|
|
|
194
194
|
'npx chemx pillars --preset=recommended',
|
|
195
195
|
'npx chemx pillars --preset=minimal --dry-run'
|
|
196
196
|
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'ui',
|
|
200
|
+
aliases: ['dashboard', 'preview'],
|
|
201
|
+
usage: 'npx chemx ui [options]',
|
|
202
|
+
summary: 'Launch the Chemical X interactive web dashboard.',
|
|
203
|
+
description: 'Provides real-time Kanban task boards, agent rails, live SQLite Database Studio, and codebase AST tree exploration.',
|
|
204
|
+
flags: [
|
|
205
|
+
{ flag: '--port=<N>', desc: 'Server port (default: 4173)' }
|
|
206
|
+
],
|
|
207
|
+
examples: [
|
|
208
|
+
'npx chemx ui',
|
|
209
|
+
'npx chemx dashboard --port=3000'
|
|
210
|
+
]
|
|
197
211
|
}
|
|
198
212
|
];
|
package/cli/create.spec.js
CHANGED
|
@@ -63,7 +63,7 @@ describe('Scaffolding Entrypoint Invocations', () => {
|
|
|
63
63
|
const result = spawnSync('node', [CREATE_BIN, '--help'], {
|
|
64
64
|
encoding: 'utf-8',
|
|
65
65
|
cwd: tmpBase,
|
|
66
|
-
timeout:
|
|
66
|
+
timeout: 15000
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
assert.equal(result.status, 0, 'Should exit with code 0 on --help');
|
|
@@ -78,7 +78,7 @@ describe('Scaffolding Entrypoint Invocations', () => {
|
|
|
78
78
|
const result = spawnSync('node', [CREATE_BIN, '--version'], {
|
|
79
79
|
encoding: 'utf-8',
|
|
80
80
|
cwd: tmpBase,
|
|
81
|
-
timeout:
|
|
81
|
+
timeout: 15000
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
assert.equal(result.status, 0, 'Should exit with code 0 on --version');
|
package/cli/help.js
CHANGED
|
@@ -43,7 +43,7 @@ export const printHelp = () => {
|
|
|
43
43
|
lines.push(
|
|
44
44
|
`${BOLD}REACTIVE COMPOSABLE RULES${RESET}`,
|
|
45
45
|
` ${YELLOW}1.${RESET} Return plain objects with individual ref/computed. Never raw reactive().`,
|
|
46
|
-
` ${YELLOW}2.${RESET}
|
|
46
|
+
` ${YELLOW}2.${RESET} Structure returns into flat State, Status, and verb Actions (HOOK_SHAPE_CONTRACT).`,
|
|
47
47
|
` ${YELLOW}3.${RESET} Clean up side-effects automatically onScopeDispose().`,
|
|
48
48
|
'',
|
|
49
49
|
`${BOLD}COMMUNITY & SUPPORT${RESET}`,
|
package/cli/index.js
CHANGED
|
@@ -45,7 +45,7 @@ const loadProjectConfig = () => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// Public API
|
|
48
|
+
// Public API: lazy proxy exports (zero startup cost for consumers)
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
|
|
51
51
|
export const runAudit = async (customDir = null, isCli = false) => {
|
|
@@ -77,7 +77,7 @@ export { handleError, withErrorCatcher, publishIssue };
|
|
|
77
77
|
|
|
78
78
|
export const ALLOWED_COMMANDS = new Set([
|
|
79
79
|
'search', 'q', 'query', 'find',
|
|
80
|
-
'read', 'view',
|
|
80
|
+
'read', 'view', 'r',
|
|
81
81
|
'patch', 'edit',
|
|
82
82
|
'write',
|
|
83
83
|
'generate', 'g', 'gen', 'capsule', 'add',
|
|
@@ -85,7 +85,9 @@ export const ALLOWED_COMMANDS = new Set([
|
|
|
85
85
|
'audit',
|
|
86
86
|
'trend', 'trends',
|
|
87
87
|
'verify', 'check:all',
|
|
88
|
-
'team', 'swarm', 'feed',
|
|
88
|
+
'team', 'swarm', 'feed', 'tokens', 'telemetry',
|
|
89
|
+
'project', 'coordinator',
|
|
90
|
+
'benchmark', 'ablation', 'memory',
|
|
89
91
|
'pillars', 'rules', 'config:pillars',
|
|
90
92
|
'mcp', 'mcp-server', 'server', 'install-mcp', 'setup-mcp',
|
|
91
93
|
'build', 'run', 'wrap',
|
|
@@ -106,20 +108,28 @@ export const ALLOWED_COMMANDS = new Set([
|
|
|
106
108
|
// main: thin orchestrator; real dispatch lives in cmd-router.js
|
|
107
109
|
// ---------------------------------------------------------------------------
|
|
108
110
|
|
|
111
|
+
const HELP_FLAGS = new Set(['help', '--help', '-h']);
|
|
112
|
+
const VERSION_FLAGS = new Set(['version', '--version', '-v']);
|
|
113
|
+
|
|
109
114
|
const main = async () => {
|
|
110
115
|
const firstArg = rawArgs[0];
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
const isHelpRequested = !firstArg || HELP_FLAGS.has(firstArg);
|
|
118
|
+
if (isHelpRequested) {
|
|
113
119
|
printHelp();
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
const isVersionRequested = VERSION_FLAGS.has(firstArg);
|
|
124
|
+
if (isVersionRequested) {
|
|
118
125
|
process.stdout.write(`create-chemx v${getPackageVersion()}\n`);
|
|
119
126
|
return;
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
|
|
129
|
+
const isAllowedCommand = ALLOWED_COMMANDS.has(firstArg);
|
|
130
|
+
const isCapsuleCmd = isCapsulePrefix(firstArg);
|
|
131
|
+
const isCommandValid = isAllowedCommand || isCapsuleCmd;
|
|
132
|
+
if (!isCommandValid) {
|
|
123
133
|
process.stderr.write(`Unknown command "${firstArg}". Run --help for usage.\n`);
|
|
124
134
|
process.exit(1);
|
|
125
135
|
}
|
|
@@ -99,7 +99,7 @@ fi
|
|
|
99
99
|
|
|
100
100
|
if [ -n "\$AUDIT_BIN" ]; then
|
|
101
101
|
printf "%s[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...%s\\n" "\$C_BLUE" "\$MIN_GRADE" "\$MIN_SCORE" "\$C_RESET"
|
|
102
|
-
if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
|
|
102
|
+
if ! \$AUDIT_BIN audit --git --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
|
|
103
103
|
printf "\\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
|
|
104
104
|
printf "%s💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol%s\\n\\n" "\$C_CYAN" "\$C_RESET"
|
|
105
105
|
exit 1
|
package/cli/mcp/installer.js
CHANGED
|
@@ -48,10 +48,10 @@ export const resolveMcpServerCommand = (targetDir = '.') => {
|
|
|
48
48
|
const hasScopedChemx = fs.existsSync(localScopedChemx);
|
|
49
49
|
const hasInRepoStarterKit = fs.existsSync(inRepoStarterKit);
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (hasInRepoStarterKit) {
|
|
52
52
|
return {
|
|
53
53
|
command: 'node',
|
|
54
|
-
args: ['./
|
|
54
|
+
args: ['./apps/chemical-x/starter-kit/cli/index.js', 'mcp']
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -62,16 +62,16 @@ export const resolveMcpServerCommand = (targetDir = '.') => {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
if (
|
|
65
|
+
if (hasCreateChemx) {
|
|
66
66
|
return {
|
|
67
67
|
command: 'node',
|
|
68
|
-
args: ['./
|
|
68
|
+
args: ['./node_modules/create-chemx/cli/index.js', 'mcp']
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
|
-
command: '
|
|
74
|
-
args: ['-y', 'chemx', 'mcp']
|
|
73
|
+
command: 'npm',
|
|
74
|
+
args: ['exec', '-y', '--', 'chemx', 'mcp']
|
|
75
75
|
};
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -173,7 +173,7 @@ export const installAntigravityMcpConfig = (targetDir = '.', options = {}) => {
|
|
|
173
173
|
|
|
174
174
|
const serverDef = localStarter
|
|
175
175
|
? { command: process.execPath, args: [localStarter, 'mcp'] }
|
|
176
|
-
: { command: '
|
|
176
|
+
: { command: 'npm', args: ['exec', '-y', '--', 'chemx', 'mcp'] };
|
|
177
177
|
|
|
178
178
|
try {
|
|
179
179
|
const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
|
|
@@ -35,12 +35,12 @@ test('mergeMcpServerConfig: preserves existing servers while updating chemical-x
|
|
|
35
35
|
assert.strictEqual(parsed.mcpServers['chemical-x'].command, 'npx');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
test('resolveMcpServerCommand: falls back to
|
|
38
|
+
test('resolveMcpServerCommand: falls back to npm exec when no local node_modules found', () => {
|
|
39
39
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-cmd-test-'));
|
|
40
40
|
try {
|
|
41
41
|
const cmd = resolveMcpServerCommand(tmpDir);
|
|
42
|
-
assert.strictEqual(cmd.command, '
|
|
43
|
-
assert.deepStrictEqual(cmd.args, ['-y', 'chemx', 'mcp']);
|
|
42
|
+
assert.strictEqual(cmd.command, 'npm');
|
|
43
|
+
assert.deepStrictEqual(cmd.args, ['exec', '-y', '--', 'chemx', 'mcp']);
|
|
44
44
|
} finally {
|
|
45
45
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
46
46
|
}
|
package/cli/mcp/manifests.js
CHANGED
package/cli/mcp/prompts.js
CHANGED
|
@@ -72,7 +72,7 @@ export const getMcpPrompt = async (name, args = {}) => {
|
|
|
72
72
|
'2. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.',
|
|
73
73
|
'3. Table-of-Contents Views: Top-level page views must be 10-20 line declarative templates assembling components via named slots.',
|
|
74
74
|
'4. Two-Stage Atomic Booleans: Break complex multi-clause conditionals into atomic single-concept booleans.',
|
|
75
|
-
'5. Composable Return Contracts:
|
|
75
|
+
'5. Composable Return Contracts: Classify hook/composable returns into flat State, Status, and verb-prefixed Actions buckets (HOOK_SHAPE_CONTRACT).',
|
|
76
76
|
'6. Co-located Types: Co-locate granular types/*.d.ts inside each capsule (< 100 lines). Avoid type monoliths.',
|
|
77
77
|
'7. Zero synthetic or mock data: Return live data or explicit empty states.',
|
|
78
78
|
`8. Framework: Calibrate bindings for ${framework}.`,
|
package/cli/mcp/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { MCP_TOOLS, executeMcpTool } from './tools.js';
|
|
5
5
|
import { MCP_RESOURCES, readMcpResource } from './resources.js';
|
|
6
6
|
import { MCP_PROMPTS, getMcpPrompt } from './prompts.js';
|
|
7
|
+
import { warmIndexDb } from '../search-db.js';
|
|
7
8
|
|
|
8
9
|
export const SERVER_INFO = {
|
|
9
10
|
name: 'chemical-x-mcp',
|
|
@@ -34,6 +35,7 @@ export const createMcpHandler = (options = {}) => {
|
|
|
34
35
|
} else if (Array.isArray(params?.workspaceFolders) && params.workspaceFolders[0]?.uri?.startsWith('file://')) {
|
|
35
36
|
cwd = new URL(params.workspaceFolders[0].uri).pathname;
|
|
36
37
|
}
|
|
38
|
+
try { warmIndexDb(cwd); } catch {}
|
|
37
39
|
return {
|
|
38
40
|
jsonrpc: '2.0',
|
|
39
41
|
id,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runAudit as executeAstAudit, auditFile } from '../audit.js';
|
|
4
|
+
import {
|
|
5
|
+
buildMasterPrompt,
|
|
6
|
+
buildGradeFPrompt,
|
|
7
|
+
buildGradeDPrompt,
|
|
8
|
+
buildGradeCPrompt,
|
|
9
|
+
buildGradeBPrompt,
|
|
10
|
+
buildAiSlopPrompt,
|
|
11
|
+
buildHotspotsPrompt
|
|
12
|
+
} from '../audit/prompts.js';
|
|
13
|
+
import { syncSearchIndex, syncViolationsIndex, recordAuditSnapshot } from '../search.js';
|
|
14
|
+
import { resolveTargetCwd } from './tools-search.js';
|
|
15
|
+
|
|
16
|
+
export const handleAudit = (args = {}, cwd = process.cwd()) => {
|
|
17
|
+
const baseCwd = resolveTargetCwd(cwd);
|
|
18
|
+
const rawTarget = args.path || args.dir || (fs.existsSync(path.resolve(baseCwd, 'src')) ? 'src' : '.');
|
|
19
|
+
const resolvedTarget = path.isAbsolute(rawTarget) ? rawTarget : path.resolve(baseCwd, rawTarget);
|
|
20
|
+
const isFile = fs.existsSync(resolvedTarget) && fs.statSync(resolvedTarget).isFile();
|
|
21
|
+
|
|
22
|
+
if (isFile) {
|
|
23
|
+
const relPath = path.relative(baseCwd, resolvedTarget);
|
|
24
|
+
const violations = auditFile(resolvedTarget, relPath);
|
|
25
|
+
return {
|
|
26
|
+
type: 'file',
|
|
27
|
+
target: relPath,
|
|
28
|
+
violationCount: violations.length,
|
|
29
|
+
violations
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const options = {
|
|
34
|
+
cwd: baseCwd,
|
|
35
|
+
model: args.model || 'blended',
|
|
36
|
+
outputFile: null
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const report = executeAstAudit(resolvedTarget, options);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const syncRes = syncSearchIndex(resolvedTarget, baseCwd);
|
|
43
|
+
if (syncRes?.db) {
|
|
44
|
+
syncViolationsIndex(syncRes.db, report.violations);
|
|
45
|
+
recordAuditSnapshot(syncRes.db, report);
|
|
46
|
+
}
|
|
47
|
+
} catch (syncError) {
|
|
48
|
+
process.stderr.write(`[chemx] Search index sync bypassed: ${syncError?.message || String(syncError)}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isSevereViolation = (v) => v.severity === 'CRITICAL' || v.severity === 'HIGH';
|
|
52
|
+
const hasCriticalOrHigh = report.violations.some(isSevereViolation);
|
|
53
|
+
const isStrictFail = Boolean(args.strict) && report.violations.length > 0;
|
|
54
|
+
const isScoreFail = typeof args.minScore === 'number' && report.health.score < args.minScore;
|
|
55
|
+
const isPassing = !isStrictFail && !isScoreFail && !hasCriticalOrHigh;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
type: 'directory',
|
|
59
|
+
target: rawTarget,
|
|
60
|
+
health: report.health,
|
|
61
|
+
metrics: report.metrics,
|
|
62
|
+
aiSlop: report.aiSlop,
|
|
63
|
+
hotspots: report.hotspots,
|
|
64
|
+
totalViolations: report.totalViolations,
|
|
65
|
+
violations: report.violations,
|
|
66
|
+
contextAnalysis: report.contextAnalysis,
|
|
67
|
+
isPassing
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const handleGetRefactorPrompt = (args = {}, cwd = process.cwd()) => {
|
|
72
|
+
const targetDir = args.dir || (fs.existsSync(path.resolve(cwd, 'src')) ? 'src' : '.');
|
|
73
|
+
const scope = args.scope || 'master';
|
|
74
|
+
const report = executeAstAudit(targetDir, {});
|
|
75
|
+
|
|
76
|
+
const PROMPT_BUILDERS = {
|
|
77
|
+
'grade-f': buildGradeFPrompt,
|
|
78
|
+
'grade-d': buildGradeDPrompt,
|
|
79
|
+
'grade-c': buildGradeCPrompt,
|
|
80
|
+
'grade-b': buildGradeBPrompt,
|
|
81
|
+
'ai-slop': buildAiSlopPrompt,
|
|
82
|
+
'hotspots': buildHotspotsPrompt,
|
|
83
|
+
'master': buildMasterPrompt
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const builder = Object.prototype.hasOwnProperty.call(PROMPT_BUILDERS, scope)
|
|
87
|
+
? PROMPT_BUILDERS[scope]
|
|
88
|
+
: buildMasterPrompt;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
targetDir,
|
|
92
|
+
scope,
|
|
93
|
+
prompt: builder(report) || 'No violations found for the requested scope.'
|
|
94
|
+
};
|
|
95
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createCapsuleFiles } from '../generator.js';
|
|
2
|
+
import { openIndexDb } from '../search-schema.js';
|
|
3
|
+
import { fetchScoreTrends, formatTrendReport, renderSparkline } from '../trend.js';
|
|
4
|
+
import { resolveTargetCwd } from './tools-search.js';
|
|
5
|
+
|
|
6
|
+
export const handleGenerateCapsule = (args = {}, cwd = process.cwd()) => {
|
|
7
|
+
const { name, framework, tier = 'm', targetDir = null, lean = false, desc = '', description = '', dryRun = false } = args;
|
|
8
|
+
|
|
9
|
+
const hasName = Boolean(name && name.trim());
|
|
10
|
+
const hasValidFramework = Boolean(framework && ['react', 'vue', 'svelte'].includes(framework.toLowerCase()));
|
|
11
|
+
const canGenerate = hasName && hasValidFramework;
|
|
12
|
+
|
|
13
|
+
if (!canGenerate) {
|
|
14
|
+
throw new Error('chemx_generate_capsule requires "name" and "framework" (react, vue, svelte).');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = createCapsuleFiles({
|
|
18
|
+
name: name.trim(),
|
|
19
|
+
framework: framework.toLowerCase(),
|
|
20
|
+
tier: tier.toLowerCase(),
|
|
21
|
+
targetParent: targetDir,
|
|
22
|
+
isLean: Boolean(lean),
|
|
23
|
+
cwd,
|
|
24
|
+
desc: desc || description,
|
|
25
|
+
dryRun: Boolean(dryRun)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
dryRun: result.dryRun,
|
|
31
|
+
capsuleName: result.capsuleName,
|
|
32
|
+
pascalName: result.pascalName,
|
|
33
|
+
framework: result.framework,
|
|
34
|
+
tier: result.tier,
|
|
35
|
+
targetDir: result.targetDir,
|
|
36
|
+
relativeDir: result.relativeDir,
|
|
37
|
+
filesCreated: result.filesCreated,
|
|
38
|
+
previews: result.previews
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const handleChemxTrend = async (args = {}, cwd = process.cwd()) => {
|
|
43
|
+
const targetCwd = resolveTargetCwd(args.cwd || cwd);
|
|
44
|
+
const db = openIndexDb(targetCwd);
|
|
45
|
+
const limit = args.limit || 10;
|
|
46
|
+
const snapshots = fetchScoreTrends(db, limit);
|
|
47
|
+
if (args.json) {
|
|
48
|
+
const scores = snapshots.map((s) => s.score);
|
|
49
|
+
const hasScores = snapshots.length > 0;
|
|
50
|
+
const latestScore = hasScores ? snapshots[snapshots.length - 1].score : null;
|
|
51
|
+
const hasDelta = snapshots.length >= 2;
|
|
52
|
+
const delta = hasDelta ? snapshots[snapshots.length - 1].score - snapshots[0].score : 0;
|
|
53
|
+
return {
|
|
54
|
+
count: snapshots.length,
|
|
55
|
+
sparkline: renderSparkline(scores),
|
|
56
|
+
latestScore,
|
|
57
|
+
delta,
|
|
58
|
+
snapshots
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return formatTrendReport(snapshots);
|
|
62
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { syncSingleFileIndex } from '../search.js';
|
|
3
|
+
import { patchFile } from '../patcher.js';
|
|
4
|
+
import { handleCheckCommand } from '../search-commands.js';
|
|
5
|
+
|
|
6
|
+
export const isSevereViolation = (v) => {
|
|
7
|
+
const isCritical = v.severity === 'CRITICAL';
|
|
8
|
+
const isHigh = v.severity === 'HIGH';
|
|
9
|
+
return isCritical || isHigh;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const formatPatchWarnings = (result) => {
|
|
13
|
+
const warnings = [];
|
|
14
|
+
const hasLineBudget = Boolean(result.lineBudget);
|
|
15
|
+
const isBudgetExceeded = hasLineBudget && !result.lineBudget.passed;
|
|
16
|
+
if (isBudgetExceeded) {
|
|
17
|
+
warnings.push(`[Directive 1.A] ${result.lineBudget.warning}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const violations = result.violations || [];
|
|
21
|
+
for (const v of violations) {
|
|
22
|
+
if (isSevereViolation(v)) {
|
|
23
|
+
warnings.push(`[${v.severity} - ${v.rule}] Line ${v.line}: ${v.hazard} -> ${v.directive || ''}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return warnings.length > 0 ? warnings : undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const handleChemxPatch = (args = {}, cwd = process.cwd()) => {
|
|
31
|
+
const hasPath = Boolean(args.path);
|
|
32
|
+
const hasTargetContent = args.targetContent !== undefined;
|
|
33
|
+
const hasReplacementContent = args.replacementContent !== undefined;
|
|
34
|
+
const hasRequiredArgs = hasPath && hasTargetContent && hasReplacementContent;
|
|
35
|
+
|
|
36
|
+
if (!hasRequiredArgs) {
|
|
37
|
+
throw new Error('chemx_patch requires "path", "targetContent", and "replacementContent" arguments.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const targetPath = path.isAbsolute(args.path) ? args.path : path.resolve(cwd, args.path);
|
|
41
|
+
const result = patchFile(targetPath, {
|
|
42
|
+
targetContent: args.targetContent,
|
|
43
|
+
replacementContent: args.replacementContent,
|
|
44
|
+
allowMultiple: Boolean(args.allowMultiple),
|
|
45
|
+
cwd
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
syncSingleFileIndex(targetPath, cwd);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (process.env.CHEMX_DEBUG) {
|
|
52
|
+
process.stderr.write(`[patch-sync] Auto-index skipped for ${targetPath}: ${err.message}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...result,
|
|
58
|
+
warnings: formatPatchWarnings(result)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const handleChemxCheck = (args = {}, cwd = process.cwd()) => {
|
|
63
|
+
if (args.path === 'RESTART_MCP') {
|
|
64
|
+
setTimeout(() => process.exit(0), 50);
|
|
65
|
+
return { restarting: true };
|
|
66
|
+
}
|
|
67
|
+
if (!args.path) {
|
|
68
|
+
throw new Error('chemx_check requires "path" argument.');
|
|
69
|
+
}
|
|
70
|
+
const targetPath = path.isAbsolute(args.path) ? args.path : path.resolve(cwd, args.path);
|
|
71
|
+
return handleCheckCommand(targetPath, { isJson: true, isCli: false });
|
|
72
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runAudit as executeAstAudit } from '../audit.js';
|
|
4
|
+
import { runAutofix } from '../audit/autofix.js';
|
|
5
|
+
import { resolveTargetCwd } from './tools-search.js';
|
|
6
|
+
|
|
7
|
+
export const handleQueryPatterns = (args = {}, cwd = process.cwd()) => {
|
|
8
|
+
const baseCwd = resolveTargetCwd(cwd);
|
|
9
|
+
const targetDir = args.dir || (fs.existsSync(path.resolve(baseCwd, 'src')) ? 'src' : '.');
|
|
10
|
+
const resolvedTarget = path.isAbsolute(targetDir) ? targetDir : path.resolve(baseCwd, targetDir);
|
|
11
|
+
const report = executeAstAudit(resolvedTarget, { cwd: baseCwd });
|
|
12
|
+
const rawPatterns = report.patterns || [];
|
|
13
|
+
const filterType = args.type || 'ALL';
|
|
14
|
+
const minOccurrences = typeof args.minOccurrences === 'number' ? args.minOccurrences : 2;
|
|
15
|
+
const isCompact = args.compact !== false;
|
|
16
|
+
|
|
17
|
+
const matchesType = (p) => filterType === 'ALL' || p.type === filterType;
|
|
18
|
+
const matchesCount = (p) => p.fileCount >= minOccurrences;
|
|
19
|
+
|
|
20
|
+
const candidates = rawPatterns
|
|
21
|
+
.filter((p) => matchesType(p) && matchesCount(p))
|
|
22
|
+
.map((p) => {
|
|
23
|
+
const base = {
|
|
24
|
+
id: p.id,
|
|
25
|
+
type: p.type,
|
|
26
|
+
label: p.label,
|
|
27
|
+
detail: p.detail,
|
|
28
|
+
suggestedCapsule: p.suggestedCapsule,
|
|
29
|
+
recommendation: p.recommendation,
|
|
30
|
+
fileCount: p.fileCount,
|
|
31
|
+
totalHits: p.totalHits,
|
|
32
|
+
impactScore: p.impactScore
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (isCompact) {
|
|
36
|
+
const sampleOccurrences = [];
|
|
37
|
+
const seenSampleFiles = new Set();
|
|
38
|
+
for (const occ of (p.occurrences || [])) {
|
|
39
|
+
if (!seenSampleFiles.has(occ.filePath)) {
|
|
40
|
+
seenSampleFiles.add(occ.filePath);
|
|
41
|
+
sampleOccurrences.push({ file: occ.filePath, line: occ.line });
|
|
42
|
+
if (sampleOccurrences.length >= 3) break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
files: p.uniqueFiles || Array.from(new Set((p.occurrences || []).map((o) => o.filePath))),
|
|
48
|
+
sampleOccurrences
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { ...base, occurrences: p.occurrences };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
scannedDir: targetDir,
|
|
56
|
+
totalCandidates: candidates.length,
|
|
57
|
+
compact: isCompact,
|
|
58
|
+
candidates
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const handleAutofix = (args = {}, cwd = process.cwd()) => {
|
|
63
|
+
const baseCwd = resolveTargetCwd(cwd);
|
|
64
|
+
const rawTarget = args.path || args.dir || (fs.existsSync(path.resolve(baseCwd, 'src')) ? 'src' : '.');
|
|
65
|
+
const targetDir = path.isAbsolute(rawTarget) ? rawTarget : path.resolve(baseCwd, rawTarget);
|
|
66
|
+
return runAutofix(targetDir, {
|
|
67
|
+
dryRun: Boolean(args.dryRun),
|
|
68
|
+
rules: args.rules,
|
|
69
|
+
cwd: baseCwd
|
|
70
|
+
});
|
|
71
|
+
};
|