@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.
Files changed (73) hide show
  1. package/cli/audit/prompts.js +1 -1
  2. package/cli/commands/cmd-project.js +100 -0
  3. package/cli/commands/cmd-router.js +21 -1
  4. package/cli/commands-schema.js +14 -0
  5. package/cli/create.spec.js +2 -2
  6. package/cli/help.js +1 -1
  7. package/cli/index.js +16 -6
  8. package/cli/installer-templates.js +1 -1
  9. package/cli/mcp/installer.js +7 -7
  10. package/cli/mcp/installer.spec.js +3 -3
  11. package/cli/mcp/manifests.js +2 -1
  12. package/cli/mcp/prompts.js +1 -1
  13. package/cli/mcp/server.js +2 -0
  14. package/cli/mcp/tools-audit.js +95 -0
  15. package/cli/mcp/tools-generate.js +62 -0
  16. package/cli/mcp/tools-patch.js +72 -0
  17. package/cli/mcp/tools-patterns.js +71 -0
  18. package/cli/mcp/tools-project.js +75 -0
  19. package/cli/mcp/tools-project.spec.js +51 -0
  20. package/cli/mcp/tools-q-modes.js +69 -0
  21. package/cli/mcp/tools-q.js +58 -0
  22. package/cli/mcp/tools-read.js +69 -0
  23. package/cli/mcp/tools-search-util.js +35 -0
  24. package/cli/mcp/tools-search.js +15 -300
  25. package/cli/mcp/tools-team-feed.js +57 -0
  26. package/cli/mcp/tools-team-locks.js +63 -0
  27. package/cli/mcp/tools-team-mailbox.js +32 -0
  28. package/cli/mcp/tools-team-tasks.js +84 -0
  29. package/cli/mcp/tools-team.js +37 -183
  30. package/cli/mcp/tools-verify.js +47 -0
  31. package/cli/mcp/tools-write.js +33 -0
  32. package/cli/mcp/tools.js +72 -407
  33. package/cli/pillars-schema.js +3 -3
  34. package/cli/reader-shorthand.spec.js +60 -0
  35. package/cli/reader.js +67 -24
  36. package/cli/search-bulletproof.spec.js +104 -0
  37. package/cli/search-db.js +33 -5
  38. package/cli/search-schema.js +24 -4
  39. package/cli/search.js +9 -2
  40. package/cli/team/index.js +9 -0
  41. package/cli/team/team-commands.js +104 -64
  42. package/cli/team/team-concurrency-stress.spec.js +95 -0
  43. package/cli/team/team-db-mailbox.js +86 -0
  44. package/cli/team/team-db-task-helpers.js +28 -30
  45. package/cli/team/team-db-tasks.js +5 -2
  46. package/cli/team/team-flags.js +69 -0
  47. package/cli/team/team-format.js +87 -46
  48. package/cli/team/team-mailbox-empirical.spec.js +191 -0
  49. package/cli/team/team-mailbox.spec.js +93 -0
  50. package/cli/team/team-memory.js +84 -0
  51. package/cli/team/team-memory.spec.js +74 -0
  52. package/cli/team/team-projects-coordinator.js +100 -0
  53. package/cli/team/team-projects-format.js +45 -0
  54. package/cli/team/team-projects-memory.js +70 -0
  55. package/cli/team/team-projects.js +94 -0
  56. package/cli/team/team-projects.spec.js +116 -0
  57. package/cli/team/team-schema-memory.js +25 -0
  58. package/cli/team/team-schema-projects.js +50 -0
  59. package/cli/team/team-schema.js +27 -12
  60. package/cli/team/team-tasks-hierarchy.spec.js +80 -0
  61. package/cli/team/team-tokens.js +95 -0
  62. package/cli/team/team-triage.js +130 -10
  63. package/cli/team/team-verification-gate.spec.js +65 -4
  64. package/cli/team/team.spec.js +1 -1
  65. package/cli/ui-e2e-verification.spec.js +10 -2
  66. package/cli/ui-handlers.js +10 -2
  67. package/cli/ui-index.html +33 -10
  68. package/cli/ui-server.js +7 -0
  69. package/cli/ui-sse.js +57 -0
  70. package/cli/ui-sse.spec.js +74 -0
  71. package/cli/ui.spec.js +6 -1
  72. package/package.json +1 -1
  73. package/scripts/pre-commit.sh +2 -7
@@ -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 strictly limit returns to 3 to 5 properties (State + Status + Actions).');
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;
@@ -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
  ];
@@ -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: 5000
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: 5000
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} Strictly limit returns to State + Status + Actions (max 3-5 properties).`,
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 — lazy proxy exports (zero startup cost for consumers)
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
- if (!firstArg || firstArg === 'help' || firstArg === '--help' || firstArg === '-h') {
117
+ const isHelpRequested = !firstArg || HELP_FLAGS.has(firstArg);
118
+ if (isHelpRequested) {
113
119
  printHelp();
114
120
  return;
115
121
  }
116
122
 
117
- if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
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
- if (!ALLOWED_COMMANDS.has(firstArg) && !isCapsulePrefix(firstArg)) {
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
@@ -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 (hasCreateChemx) {
51
+ if (hasInRepoStarterKit) {
52
52
  return {
53
53
  command: 'node',
54
- args: ['./node_modules/create-chemx/cli/index.js', 'mcp']
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 (hasInRepoStarterKit) {
65
+ if (hasCreateChemx) {
66
66
  return {
67
67
  command: 'node',
68
- args: ['./apps/chemical-x/starter-kit/cli/index.js', 'mcp']
68
+ args: ['./node_modules/create-chemx/cli/index.js', 'mcp']
69
69
  };
70
70
  }
71
71
 
72
72
  return {
73
- command: 'npx',
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: 'npx', args: ['-y', 'chemx', 'mcp'] };
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 npx when no local node_modules found', () => {
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, 'npx');
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
  }
@@ -32,7 +32,8 @@ export const MASTER_MCP_TOOL = {
32
32
  'autofix',
33
33
  'generate',
34
34
  'patterns',
35
- 'issue'
35
+ 'issue',
36
+ 'project'
36
37
  ],
37
38
  description: 'The Chemical X subsystem action to execute.'
38
39
  },
@@ -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: Limit hook/composable returns to 3 to 5 properties (State + Status + Actions).',
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
+ };