@geraldmaron/construct 1.0.21 → 1.0.24
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 +23 -7
- package/bin/construct +206 -228
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +31 -10
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +39 -163
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +72 -10
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +169 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +59 -252
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +104 -480
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -43
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +7 -6
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -11
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +427 -107
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +3 -12
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +28 -36
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +15 -15
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/server/static/index.html +0 -1
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/vector-store.mjs +0 -100
package/lib/mcp/tools/memory.mjs
CHANGED
|
@@ -56,13 +56,13 @@ export async function memorySearch(args) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function memoryAddObservations(args) {
|
|
59
|
+
export async function memoryAddObservations(args) {
|
|
60
60
|
const cwd = args.cwd ? resolve(String(args.cwd)) : process.cwd();
|
|
61
61
|
const project = args.project || cwd.split('/').pop() || 'unknown';
|
|
62
62
|
const observations = (args.observations || []).slice(0, 10);
|
|
63
63
|
const created = [];
|
|
64
64
|
for (const obs of observations) {
|
|
65
|
-
const record = addObservation(cwd, {
|
|
65
|
+
const record = await addObservation(cwd, {
|
|
66
66
|
role: obs.role || 'unknown',
|
|
67
67
|
category: obs.category || 'insight',
|
|
68
68
|
summary: obs.summary || '',
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/tools/orchestration-run.mjs — MCP client for the local orchestration daemon.
|
|
3
|
+
*
|
|
4
|
+
* Executes a real multi-specialist run by driving the daemon's HTTP API
|
|
5
|
+
* (ADR-0022), so MCP hosts with no subagent primitive — VS Code/Copilot, Cursor —
|
|
6
|
+
* reach orchestrated outcomes through a tool they actually expose: the engine
|
|
7
|
+
* owns orchestration; this tool is the thin client. The daemon URL resolves from
|
|
8
|
+
* dashboard state (else the default loopback port) and the bearer token from
|
|
9
|
+
* ~/.construct/config.env. When the daemon is unreachable the tool fails fast
|
|
10
|
+
* with an actionable error rather than silently degrading to a single-persona
|
|
11
|
+
* pass — the absence of an executable path is made loud, not hidden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readDashboardState } from '../../service-manager.mjs';
|
|
15
|
+
import { getDashboardToken } from '../../server/auth.mjs';
|
|
16
|
+
|
|
17
|
+
const TERMINAL = ['completed', 'completed-with-failures', 'cancelled', 'error'];
|
|
18
|
+
|
|
19
|
+
function resolveDaemon(env = process.env) {
|
|
20
|
+
const state = readDashboardState();
|
|
21
|
+
const base = (env.CONSTRUCT_ORCHESTRATION_URL || state?.url || `http://${env.BIND_HOST || '127.0.0.1'}:${env.PORT || '4242'}`).replace(/\/$/, '');
|
|
22
|
+
return { base, token: getDashboardToken(), daemonRunning: Boolean(state) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A reachability failure is the fail-fast signal MCP hosts need: report how to
|
|
26
|
+
// start the engine instead of letting the host fall back to a manual pass.
|
|
27
|
+
|
|
28
|
+
function unreachable(base, detail) {
|
|
29
|
+
return {
|
|
30
|
+
error: `Orchestration daemon not reachable at ${base}: ${detail}. Start it with \`construct dashboard\`, then retry. The engine runs orchestration; this host is a thin client (ADR-0022).`,
|
|
31
|
+
daemon: base,
|
|
32
|
+
failFast: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function orchestrationRun(args = {}, { fetchImpl = fetch, env = process.env } = {}) {
|
|
37
|
+
const {
|
|
38
|
+
request,
|
|
39
|
+
workflow_type,
|
|
40
|
+
requested_strategy = 'auto',
|
|
41
|
+
worker_backend,
|
|
42
|
+
host,
|
|
43
|
+
host_model,
|
|
44
|
+
host_provider,
|
|
45
|
+
file_count,
|
|
46
|
+
module_count,
|
|
47
|
+
wait = true,
|
|
48
|
+
timeout_ms = 120000,
|
|
49
|
+
} = args;
|
|
50
|
+
|
|
51
|
+
if (!request || typeof request !== 'string') return { error: 'Missing "request" string describing the work to orchestrate.' };
|
|
52
|
+
|
|
53
|
+
const { base, token } = resolveDaemon(env);
|
|
54
|
+
const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) };
|
|
55
|
+
const body = JSON.stringify({
|
|
56
|
+
request,
|
|
57
|
+
workflowType: workflow_type,
|
|
58
|
+
requestedStrategy: requested_strategy,
|
|
59
|
+
workerBackend: worker_backend,
|
|
60
|
+
host,
|
|
61
|
+
hostModel: host_model,
|
|
62
|
+
hostProvider: host_provider,
|
|
63
|
+
fileCount: file_count,
|
|
64
|
+
moduleCount: module_count,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let started;
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetchImpl(`${base}/api/orchestration/runs`, { method: 'POST', headers, body });
|
|
70
|
+
if (res.status === 401 || res.status === 403) {
|
|
71
|
+
return { error: `Daemon rejected the request (HTTP ${res.status}). The dashboard token is missing or wrong — check CONSTRUCT_DASHBOARD_TOKEN in ~/.construct/config.env.`, daemon: base, failFast: true };
|
|
72
|
+
}
|
|
73
|
+
const envelope = await res.json();
|
|
74
|
+
if (!res.ok) return { error: `Daemon error (HTTP ${res.status}): ${envelope.error || 'unknown'}`, daemon: base };
|
|
75
|
+
started = envelope.data;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return unreachable(base, err.message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!wait) {
|
|
81
|
+
return { runId: started.runId, status: started.status, executionMode: started.execution?.executionMode, daemon: base, poll: 'orchestration_status with run_id' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const deadline = Date.now() + timeout_ms;
|
|
85
|
+
let run = started;
|
|
86
|
+
while (!TERMINAL.includes(run.status)) {
|
|
87
|
+
if (Date.now() > deadline) {
|
|
88
|
+
return { runId: started.runId, status: run.status, timedOut: true, daemon: base, note: 'Run is still executing; poll it with orchestration_status.' };
|
|
89
|
+
}
|
|
90
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetchImpl(`${base}/api/orchestration/runs/${encodeURIComponent(started.runId)}`, { headers });
|
|
93
|
+
const envelope = await res.json();
|
|
94
|
+
if (!res.ok) return { error: `Daemon error while polling (HTTP ${res.status}): ${envelope.error || 'unknown'}`, runId: started.runId, daemon: base };
|
|
95
|
+
run = envelope.data;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { error: `Lost connection to the daemon while polling run ${started.runId}: ${err.message}`, runId: started.runId, daemon: base, failFast: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
runId: run.runId,
|
|
103
|
+
status: run.status,
|
|
104
|
+
executionMode: run.execution?.executionMode,
|
|
105
|
+
degraded: run.execution?.degraded ?? false,
|
|
106
|
+
degradationReason: run.execution?.degradationReason ?? null,
|
|
107
|
+
tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, output: t.output ?? null, reasoning: t.reasoning ?? null, error: t.error ?? null })),
|
|
108
|
+
daemon: base,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function orchestrationStatus(args = {}, { fetchImpl = fetch, env = process.env } = {}) {
|
|
113
|
+
const { run_id, limit = 20 } = args;
|
|
114
|
+
const { base, token } = resolveDaemon(env);
|
|
115
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
116
|
+
const path = run_id ? `/api/orchestration/runs/${encodeURIComponent(run_id)}` : `/api/orchestration/runs?limit=${encodeURIComponent(limit)}`;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetchImpl(`${base}${path}`, { headers });
|
|
119
|
+
const envelope = await res.json();
|
|
120
|
+
if (!res.ok) return { error: `Daemon error (HTTP ${res.status}): ${envelope.error || 'unknown'}`, daemon: base };
|
|
121
|
+
return envelope.data;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return unreachable(base, err.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -37,14 +37,13 @@ export async function storageReset(args) {
|
|
|
37
37
|
return resetStorage(cwd, {
|
|
38
38
|
env: process.env,
|
|
39
39
|
project,
|
|
40
|
-
resetSql: args.reset_sql !== false,
|
|
41
40
|
resetVector: args.reset_vector !== false,
|
|
42
41
|
resetIngested: args.reset_ingested === true,
|
|
43
42
|
confirm: true,
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
export function deleteIngestedArtifactsTool(args) {
|
|
46
|
+
export async function deleteIngestedArtifactsTool(args) {
|
|
48
47
|
const cwd = args.cwd ? resolve(String(args.cwd)) : process.cwd();
|
|
49
48
|
if (args.confirm !== true) {
|
|
50
49
|
return { error: 'delete_ingested_artifacts requires confirm=true' };
|
|
@@ -53,7 +52,7 @@ export function deleteIngestedArtifactsTool(args) {
|
|
|
53
52
|
? args.files.map((value) => String(value))
|
|
54
53
|
: [];
|
|
55
54
|
try {
|
|
56
|
-
return deleteIngestedArtifacts(cwd, { files, confirm: true });
|
|
55
|
+
return await deleteIngestedArtifacts(cwd, { files, confirm: true });
|
|
57
56
|
} catch (error) {
|
|
58
57
|
return { error: error.message ?? String(error) };
|
|
59
58
|
}
|
package/lib/mcp-catalog.json
CHANGED
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
"id": "playwright",
|
|
86
86
|
"name": "Playwright",
|
|
87
87
|
"category": "optional",
|
|
88
|
-
"description": "Browser automation for E2E testing and web research.",
|
|
89
|
-
"package": "@playwright/mcp@
|
|
88
|
+
"description": "Browser automation for E2E testing and web research. Opt-in via `construct mcp add playwright` — not synced by default (downloads browser binaries on first use).",
|
|
89
|
+
"package": "@playwright/mcp@0.0.75",
|
|
90
90
|
"command": "npx",
|
|
91
91
|
"args": [
|
|
92
92
|
"-y",
|
|
93
|
-
"@playwright/mcp@
|
|
93
|
+
"@playwright/mcp@0.0.75"
|
|
94
94
|
],
|
|
95
95
|
"env": {},
|
|
96
96
|
"requiredEnv": [],
|
package/lib/mcp-manager.mjs
CHANGED
|
@@ -33,6 +33,56 @@ function getClaudeSettingsPath() {
|
|
|
33
33
|
return join(homedir(), '.claude', 'settings.json');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// VS Code's "MCP: Open User Configuration" edits a per-profile mcp.json keyed
|
|
37
|
+
// on top-level `servers`; Cursor uses ~/.cursor/mcp.json keyed on `mcpServers`.
|
|
38
|
+
// construct sync writes both surfaces, so remove must clean them too or a
|
|
39
|
+
// removed MCP lingers there.
|
|
40
|
+
|
|
41
|
+
function getVSCodeUserMcpPaths(home = homedir()) {
|
|
42
|
+
const plat = platform();
|
|
43
|
+
if (plat === 'darwin') {
|
|
44
|
+
return [
|
|
45
|
+
join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'),
|
|
46
|
+
join(home, 'Library', 'Application Support', 'Code - Insiders', 'User', 'mcp.json'),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
if (plat === 'win32') {
|
|
50
|
+
const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
51
|
+
return [join(appData, 'Code', 'User', 'mcp.json'), join(appData, 'Code - Insiders', 'User', 'mcp.json')];
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
join(home, '.config', 'Code', 'User', 'mcp.json'),
|
|
55
|
+
join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getCursorMcpPath(home = homedir()) {
|
|
60
|
+
return join(home, '.cursor', 'mcp.json');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mcpFileHasEntry(file, key, id) {
|
|
64
|
+
if (!existsSync(file)) return false;
|
|
65
|
+
try {
|
|
66
|
+
return Boolean(JSON.parse(readFileSync(file, 'utf8'))?.[key]?.[id]);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeMcpFromJsonFile(file, key, id) {
|
|
73
|
+
if (!existsSync(file)) return false;
|
|
74
|
+
let config;
|
|
75
|
+
try {
|
|
76
|
+
config = JSON.parse(readFileSync(file, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (!config?.[key] || !(id in config[key])) return false;
|
|
81
|
+
delete config[key][id];
|
|
82
|
+
writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
36
86
|
function openUrl(url) {
|
|
37
87
|
try {
|
|
38
88
|
const opener = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
@@ -426,10 +476,14 @@ export function cmdMcpRemove(id) {
|
|
|
426
476
|
const openCodeState = readOpenCodeConfig();
|
|
427
477
|
const oc = openCodeState.config ?? {};
|
|
428
478
|
const openCodeId = getOpenCodeMcpId(id);
|
|
479
|
+
const vscodePaths = getVSCodeUserMcpPaths();
|
|
480
|
+
const cursorPath = getCursorMcpPath();
|
|
429
481
|
const hasClaudeEntry = Boolean(settings.mcpServers?.[id]);
|
|
430
482
|
const hasOpenCodeEntry = Boolean(oc.mcp?.[openCodeId] || oc.mcp?.[id] || (id === 'memory' && oc.mcp?.cass));
|
|
431
483
|
const hasCodexEntry = readCodexConfig().includes(`[mcp_servers.${id}]`) || readCodexConfig().includes(`[mcp_servers.${tomlString(id)}]`);
|
|
432
|
-
|
|
484
|
+
const hasVscodeEntry = vscodePaths.some((p) => mcpFileHasEntry(p, 'servers', id));
|
|
485
|
+
const hasCursorEntry = mcpFileHasEntry(cursorPath, 'mcpServers', id);
|
|
486
|
+
if (!hasClaudeEntry && !hasOpenCodeEntry && !hasCodexEntry && !hasVscodeEntry && !hasCursorEntry) {
|
|
433
487
|
console.log(`${id} is not installed. Nothing to remove.`);
|
|
434
488
|
return;
|
|
435
489
|
}
|
|
@@ -448,9 +502,11 @@ export function cmdMcpRemove(id) {
|
|
|
448
502
|
if (openCodeState.config || hasOpenCodeEntry) saveOpenCodeConfig(oc);
|
|
449
503
|
else if (openCodeState.file && existsSync(openCodeState.file)) rmSync(openCodeState.file, { force: true });
|
|
450
504
|
removeCodexMcp(id);
|
|
505
|
+
for (const p of vscodePaths) removeMcpFromJsonFile(p, 'servers', id);
|
|
506
|
+
removeMcpFromJsonFile(cursorPath, 'mcpServers', id);
|
|
451
507
|
|
|
452
|
-
console.log(`✓ ${name} removed from Claude Code, OpenCode, and
|
|
453
|
-
console.log('Restart
|
|
508
|
+
console.log(`✓ ${name} removed from Claude Code, OpenCode, Codex, VS Code, and Cursor config`);
|
|
509
|
+
console.log('Restart the editor to deactivate it.');
|
|
454
510
|
}
|
|
455
511
|
|
|
456
512
|
/**
|
package/lib/model-registry.mjs
CHANGED
|
@@ -20,17 +20,14 @@ import path from 'node:path';
|
|
|
20
20
|
* 1. Environment variables (CX_MODEL_*): Emergency overrides
|
|
21
21
|
* 2. User config (~/.construct/config.env): Personal defaults
|
|
22
22
|
* 3. Registry (specialists/registry.json): Project recommendations
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* Construct ships with NO implicit model defaults (ADR-0027,
|
|
25
|
+
* tests/model-router-no-defaults.test.mjs). When none of the three sources
|
|
26
|
+
* resolves a tier, that tier returns null with source 'not configured'.
|
|
27
|
+
* Callers must surface a clear remediation hint rather than silently
|
|
28
|
+
* substituting a vendor-specific default.
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
|
-
const BUILTIN_DEFAULTS = {
|
|
29
|
-
reasoning: 'anthropic/claude-3-5-sonnet',
|
|
30
|
-
standard: 'anthropic/claude-3-haiku',
|
|
31
|
-
fast: 'anthropic/claude-3-haiku',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
31
|
const ENV_PREFIX = 'CX_MODEL';
|
|
35
32
|
|
|
36
33
|
// ---------------------------------------------------------------------------
|
|
@@ -86,28 +83,30 @@ export function resolveModelTiers(options = {}) {
|
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
// No source resolved this tier — leave null + 'not configured' so callers
|
|
87
|
+
// can surface a clear remediation hint. Aligns with the wired path
|
|
88
|
+
// (lib/model-router.mjs:resolveTierAssignments) and the no-defaults
|
|
89
|
+
// contract locked in by tests/model-router-no-defaults.test.mjs.
|
|
90
|
+
|
|
91
|
+
result[tier] = null;
|
|
92
|
+
sources[tier] = 'not configured';
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
// Validation
|
|
94
|
+
|
|
95
95
|
const configured = Object.values(result).filter(Boolean).length;
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
if (strict && configured < 3) {
|
|
98
98
|
const missing = Object.entries(result)
|
|
99
99
|
.filter(([, v]) => !v)
|
|
100
100
|
.map(([k]) => k);
|
|
101
101
|
errors.push(`Missing configuration for tiers: ${missing.join(', ')}`);
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
return {
|
|
105
105
|
models: result,
|
|
106
106
|
sources,
|
|
107
107
|
configured,
|
|
108
108
|
complete: configured === 3,
|
|
109
109
|
errors: errors.length > 0 ? errors : null,
|
|
110
|
-
defaultsUsed: Object.values(sources).filter(s => s === 'default').length,
|
|
111
110
|
};
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -117,7 +116,7 @@ export function resolveModelTiers(options = {}) {
|
|
|
117
116
|
|
|
118
117
|
export function getModelForTier(tier, options = {}) {
|
|
119
118
|
const resolved = resolveModelTiers(options);
|
|
120
|
-
return resolved.models[tier]
|
|
119
|
+
return resolved.models[tier];
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
export function getModelSource(tier, options = {}) {
|
|
@@ -131,12 +130,15 @@ export function getModelSource(tier, options = {}) {
|
|
|
131
130
|
|
|
132
131
|
export function validateModelTiers(options = {}) {
|
|
133
132
|
const resolved = resolveModelTiers({ ...options, strict: true });
|
|
134
|
-
|
|
133
|
+
const unconfigured = Object.entries(resolved.sources)
|
|
134
|
+
.filter(([, source]) => source === 'not configured')
|
|
135
|
+
.map(([tier]) => tier);
|
|
136
|
+
|
|
135
137
|
return {
|
|
136
138
|
valid: resolved.complete && !resolved.errors,
|
|
137
139
|
errors: resolved.errors,
|
|
138
|
-
warnings:
|
|
139
|
-
? [`
|
|
140
|
+
warnings: unconfigured.length > 0
|
|
141
|
+
? [`Unconfigured tier${unconfigured.length === 1 ? '' : 's'}: ${unconfigured.join(', ')}. Run 'construct models --apply' or set CX_MODEL_<TIER>.`]
|
|
140
142
|
: [],
|
|
141
143
|
resolution: resolved,
|
|
142
144
|
};
|
|
@@ -144,25 +146,30 @@ export function validateModelTiers(options = {}) {
|
|
|
144
146
|
|
|
145
147
|
export function formatModelStatus(options = {}) {
|
|
146
148
|
const resolved = resolveModelTiers(options);
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
let output = 'Model Configuration:\n\n';
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
151
153
|
const model = resolved.models[tier];
|
|
152
154
|
const source = resolved.sources[tier];
|
|
153
|
-
const icon = source === '
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
const icon = source === 'not configured' ? '⚠' : '✓';
|
|
156
|
+
const display = model ?? '(not configured)';
|
|
157
|
+
|
|
158
|
+
output += `${icon} ${tier.padEnd(10)} ${display}\n`;
|
|
156
159
|
output += ` Source: ${source}\n\n`;
|
|
157
160
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
output += '
|
|
161
|
+
|
|
162
|
+
const unconfigured = Object.entries(resolved.sources)
|
|
163
|
+
.filter(([, source]) => source === 'not configured')
|
|
164
|
+
.map(([tier]) => tier);
|
|
165
|
+
if (unconfigured.length > 0) {
|
|
166
|
+
output += `\nUnconfigured: ${unconfigured.join(', ')}. To configure:\n`;
|
|
167
|
+
output += ' construct models --apply (interactive, picks from available providers)\n';
|
|
168
|
+
output += ' export CX_MODEL_REASONING=... (env override)\n';
|
|
169
|
+
output += ' export CX_MODEL_STANDARD=...\n';
|
|
170
|
+
output += ' export CX_MODEL_FAST=...\n';
|
|
164
171
|
}
|
|
165
|
-
|
|
172
|
+
|
|
166
173
|
return output;
|
|
167
174
|
}
|
|
168
175
|
|