@danielblomma/cortex-mcp 2.4.0 → 2.4.2
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/CHANGELOG.md +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
package/bin/cortex.mjs
CHANGED
|
@@ -1,1610 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import { normalizeProjectRoot } from "./wsl.mjs";
|
|
8
|
-
import {
|
|
9
|
-
bullet,
|
|
10
|
-
printBullet,
|
|
11
|
-
spinner,
|
|
12
|
-
gradient,
|
|
13
|
-
muted,
|
|
14
|
-
accent,
|
|
15
|
-
bold,
|
|
16
|
-
headerBanner
|
|
17
|
-
} from "./style.mjs";
|
|
18
|
-
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname = path.dirname(__filename);
|
|
21
|
-
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
22
|
-
const SCAFFOLD_ROOT = path.join(PACKAGE_ROOT, "scaffold");
|
|
23
|
-
const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, "package.json");
|
|
24
|
-
|
|
25
|
-
// v2.0.5: project layout moved mcp/ under .context/mcp/, and the
|
|
26
|
-
// gitignore policy flipped to "ignore everything in .context/, whitelist
|
|
27
|
-
// only the three editable config files". Generated artifacts (db,
|
|
28
|
-
// embeddings, cache, hooks, mcp/, govern.local.json) never land in git.
|
|
29
|
-
const MCP_PROJECT_REL = path.join(".context", "mcp");
|
|
30
|
-
const CONTEXT_RUNTIME_REL = MCP_PROJECT_REL;
|
|
31
|
-
const CONTEXT_SCRIPTS_REL = path.join(".context", "scripts");
|
|
32
|
-
|
|
33
|
-
// `.context/*` (not `.context/`) so the !-negations below actually re-include
|
|
34
|
-
// the config files — git can't re-include children of an excluded directory.
|
|
35
|
-
const GITIGNORE_LINES = [
|
|
36
|
-
"",
|
|
37
|
-
"# Cortex local storage",
|
|
38
|
-
".context/*",
|
|
39
|
-
"!.context/config.yaml",
|
|
40
|
-
"!.context/rules.yaml",
|
|
41
|
-
"!.context/ontology.cypher",
|
|
42
|
-
".npm-cache/"
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
function printBanner(title) {
|
|
46
|
-
process.stdout.write(headerBanner({ tagline: title }));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Help-row formatter: "<command>" in accent cyan, gap, "<description>" in muted grey.
|
|
50
|
-
function helpRow(cmd, desc) {
|
|
51
|
-
const target = 46;
|
|
52
|
-
const pad = cmd.length >= target ? " " : " ".repeat(target - cmd.length);
|
|
53
|
-
if (desc) {
|
|
54
|
-
return ` ${accent(cmd)}${pad}${muted(desc)}`;
|
|
55
|
-
}
|
|
56
|
-
return ` ${accent(cmd)}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function helpSection(title) {
|
|
60
|
-
return `\n${bold(muted(title))}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function printHelp() {
|
|
64
|
-
console.log(gradient("CORTEX CLI") + muted(" · governance for AI coding agents"));
|
|
65
|
-
console.log(muted(" Cortex is in control. Calm, intelligent, always monitoring."));
|
|
66
|
-
console.log(helpSection("USAGE"));
|
|
67
|
-
console.log(helpRow("cortex <command> [options]"));
|
|
68
|
-
|
|
69
|
-
console.log(helpSection("CONTEXT"));
|
|
70
|
-
console.log(helpRow("init [path]", "Scaffold a project with --force/--bootstrap/--connect/--watch"));
|
|
71
|
-
console.log(helpRow("connect [path]", "Register MCP clients (Codex + Claude Code)"));
|
|
72
|
-
console.log(helpRow("bootstrap", "Install deps, ingest, embed, load graph"));
|
|
73
|
-
console.log(helpRow("update", "Refresh context for changed files"));
|
|
74
|
-
console.log(helpRow("status", "Project context status"));
|
|
75
|
-
console.log(helpRow("doctor", "Diagnose setup health"));
|
|
76
|
-
console.log(helpRow("ingest [--changed] [--verbose]", "Re-index source files"));
|
|
77
|
-
console.log(helpRow("embed [--changed]", "Recompute embeddings"));
|
|
78
|
-
console.log(helpRow("graph-load [--no-reset]", "Reload the dependency graph"));
|
|
79
|
-
console.log(helpRow("search <query> [--json]", "Search local graph+RAG context"));
|
|
80
|
-
console.log(helpRow("related <entity-id> [--json]", "Show related context entities"));
|
|
81
|
-
console.log(helpRow("impact <query|entity-id> [--json]", "Trace likely impact paths"));
|
|
82
|
-
console.log(helpRow("rules [--json]", "List active context rules"));
|
|
83
|
-
console.log(helpRow("explain <query|entity-id> [--json]", "Show search score evidence"));
|
|
84
|
-
console.log(helpRow("pattern-evidence <file|entity-id> [--query <text>] [--top-k <n>] [--json]", "Collect cited repo-local pattern evidence"));
|
|
85
|
-
console.log(helpRow("dashboard [--interval <sec>]", "Live local dashboard"));
|
|
86
|
-
console.log(helpRow("memory-compile [--dry-run] [--verbose]", "Compile memory artifacts"));
|
|
87
|
-
console.log(helpRow("memory-lint [--verbose] [--json]", "Lint compiled memory"));
|
|
88
|
-
console.log(helpRow("watch [start|stop|status|run|once]", "Background sync (--interval, --debounce, --mode)"));
|
|
89
|
-
|
|
90
|
-
console.log(helpSection("GOVERNANCE"));
|
|
91
|
-
console.log(helpRow("enterprise <api-key>", "Install enforcement + hooks + daemon (sudo)"));
|
|
92
|
-
console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
|
|
93
|
-
console.log(helpRow("enterprise status", "Show local enforcement state"));
|
|
94
|
-
console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
|
|
95
|
-
console.log(helpRow("enterprise uninstall", "Remove enforcement (sudo, --break-glass --reason)"));
|
|
96
|
-
console.log(helpRow("enterprise repair", "Verify managed paths, clear tamper-lock (sudo)"));
|
|
97
|
-
console.log(helpRow("run <claude|codex|copilot> [args...]", "Wrap an AI CLI in cortex enforcement"));
|
|
98
|
-
console.log(helpRow("daemon [start|stop|status]", "Local supervisor daemon"));
|
|
99
|
-
console.log(helpRow("hooks [install|uninstall|status] [--project]", "Claude Code hooks"));
|
|
100
|
-
console.log(helpRow("telemetry test", "Smoke-test the push pipeline"));
|
|
101
|
-
|
|
102
|
-
console.log(helpSection("HARNESS"));
|
|
103
|
-
console.log(helpRow("stage start --task-id <id> --description \"...\"", "Start a workflow run for a task"));
|
|
104
|
-
console.log(helpRow("stage status --task-id <id>", "Print run state JSON"));
|
|
105
|
-
console.log(helpRow("stage envelope --task-id <id> [--stage <name>]", "Compose stage prompt envelope"));
|
|
106
|
-
console.log(helpRow("stage advance --task-id <id> --stage <name> --body-file <path>", "Write artifact, advance run"));
|
|
107
|
-
console.log(helpRow("stage run --task-id <id> -- <command>", "Exec a command with CORTEX_ACTIVE_TASK_ID set"));
|
|
108
|
-
|
|
109
|
-
console.log(helpSection("MISC"));
|
|
110
|
-
console.log(helpRow("mcp", "Run the MCP stdio server for the current project"));
|
|
111
|
-
console.log(helpRow("version", "Print CLI version"));
|
|
112
|
-
console.log(helpRow("help", "This screen"));
|
|
113
|
-
console.log("");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function readCliVersion() {
|
|
117
|
-
try {
|
|
118
|
-
const parsed = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf8"));
|
|
119
|
-
return typeof parsed.version === "string" ? parsed.version : "0.0.0";
|
|
120
|
-
} catch {
|
|
121
|
-
return "0.0.0";
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseInitArgs(args) {
|
|
126
|
-
let target = process.cwd();
|
|
127
|
-
let force = false;
|
|
128
|
-
let bootstrap = false;
|
|
129
|
-
let connect = false;
|
|
130
|
-
let watch = true;
|
|
131
|
-
|
|
132
|
-
for (const arg of args) {
|
|
133
|
-
if (arg === "--force") {
|
|
134
|
-
force = true;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (arg === "--bootstrap") {
|
|
139
|
-
bootstrap = true;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (arg === "--connect") {
|
|
144
|
-
connect = true;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (arg === "--no-connect") {
|
|
149
|
-
connect = false;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (arg === "--watch") {
|
|
154
|
-
watch = true;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (arg === "--no-watch") {
|
|
159
|
-
watch = false;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (arg.startsWith("-")) {
|
|
164
|
-
throw new Error(`Unknown init option: ${arg}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
target = path.resolve(arg);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return { target, force, bootstrap, connect, watch };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function parseConnectArgs(args) {
|
|
174
|
-
let target = process.cwd();
|
|
175
|
-
let skipBuild = false;
|
|
176
|
-
|
|
177
|
-
for (const arg of args) {
|
|
178
|
-
if (arg === "--skip-build") {
|
|
179
|
-
skipBuild = true;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (arg.startsWith("-")) {
|
|
184
|
-
throw new Error(`Unknown connect option: ${arg}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
target = path.resolve(arg);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return { target, skipBuild };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ensureScaffoldExists() {
|
|
194
|
-
if (!fs.existsSync(SCAFFOLD_ROOT)) {
|
|
195
|
-
throw new Error(`Scaffold not found at ${SCAFFOLD_ROOT}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Files that should never be overwritten if they already exist in the target.
|
|
200
|
-
// These contain user-specific configuration that would be lost on re-init.
|
|
201
|
-
const PRESERVE_FILES = new Set(["config.yaml", "rules.yaml", "enterprise.yml", "enterprise.yaml", "CLAUDE.md", "AGENTS.md"]);
|
|
202
|
-
const DEFAULT_SOURCE_PATHS = ["."];
|
|
203
|
-
|
|
204
|
-
function copyDirectory(sourceDir, targetDir) {
|
|
205
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
206
|
-
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
207
|
-
|
|
208
|
-
for (const entry of entries) {
|
|
209
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
210
|
-
const targetPath = path.join(targetDir, entry.name);
|
|
211
|
-
|
|
212
|
-
if (entry.isDirectory()) {
|
|
213
|
-
copyDirectory(sourcePath, targetPath);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Skip user-config files that already exist to avoid overwriting custom settings
|
|
218
|
-
if (PRESERVE_FILES.has(entry.name) && fs.existsSync(targetPath)) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
223
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
224
|
-
const sourceMode = fs.statSync(sourcePath).mode;
|
|
225
|
-
fs.chmodSync(targetPath, sourceMode);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function toPosixPath(value) {
|
|
230
|
-
return value.split(path.sep).join("/");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function yamlScalar(value) {
|
|
234
|
-
return /^[A-Za-z0-9._/-]+$/.test(value) ? value : JSON.stringify(value);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function slugifyRepoId(value) {
|
|
238
|
-
const dashed = String(value || "")
|
|
239
|
-
.trim()
|
|
240
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
241
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
242
|
-
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
243
|
-
.replace(/^-+|-+$/g, "")
|
|
244
|
-
.toLowerCase();
|
|
245
|
-
return dashed || "cortex";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function detectInitialSourcePaths(targetDir) {
|
|
249
|
-
return [...DEFAULT_SOURCE_PATHS];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function buildInitialConfig(targetDir) {
|
|
253
|
-
const repoId = slugifyRepoId(path.basename(path.resolve(targetDir)));
|
|
254
|
-
const sourcePaths = detectInitialSourcePaths(targetDir);
|
|
255
|
-
return [
|
|
256
|
-
`repo_id: ${yamlScalar(repoId)}`,
|
|
257
|
-
"source_paths:",
|
|
258
|
-
...sourcePaths.map((sourcePath) => ` - ${yamlScalar(sourcePath)}`),
|
|
259
|
-
"truth_order:",
|
|
260
|
-
" - ADR",
|
|
261
|
-
" - RULE",
|
|
262
|
-
" - CODE",
|
|
263
|
-
" - WIKI",
|
|
264
|
-
"# Tuned together with the midrank-percentile graph_score in searchResults.ts.",
|
|
265
|
-
"ranking:",
|
|
266
|
-
" semantic: 0.40",
|
|
267
|
-
" graph: 0.25",
|
|
268
|
-
" trust: 0.20",
|
|
269
|
-
" recency: 0.15",
|
|
270
|
-
"runtime:",
|
|
271
|
-
" top_k: 5",
|
|
272
|
-
" include_uncertainties: true",
|
|
273
|
-
""
|
|
274
|
-
].join("\n");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function initializeScaffold(targetDir, force) {
|
|
278
|
-
const configPath = path.join(targetDir, ".context", "config.yaml");
|
|
279
|
-
const hasExistingConfig = fs.existsSync(configPath);
|
|
280
|
-
const generatedConfig = hasExistingConfig ? null : buildInitialConfig(targetDir);
|
|
281
|
-
installScaffold(targetDir, force);
|
|
282
|
-
if (!hasExistingConfig && generatedConfig) {
|
|
283
|
-
writeTextFile(configPath, generatedConfig);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function ensurePathWritable(targetPath, force) {
|
|
288
|
-
if (!fs.existsSync(targetPath)) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!force) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
`Refusing to overwrite existing path: ${targetPath}\nRun with --force to overwrite scaffold files.`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function mergeGitignore(targetDir) {
|
|
300
|
-
const gitignorePath = path.join(targetDir, ".gitignore");
|
|
301
|
-
const current = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
302
|
-
const merged = current + GITIGNORE_LINES.filter((line) => !current.includes(line)).join("\n") + "\n";
|
|
303
|
-
fs.writeFileSync(gitignorePath, merged, "utf8");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function migrateLegacyMcpLocation(targetDir) {
|
|
307
|
-
const legacyMcp = path.join(targetDir, "mcp");
|
|
308
|
-
const newMcp = path.join(targetDir, MCP_PROJECT_REL);
|
|
309
|
-
if (!fs.existsSync(legacyMcp)) return;
|
|
310
|
-
if (fs.existsSync(newMcp)) return;
|
|
311
|
-
fs.mkdirSync(path.join(targetDir, ".context"), { recursive: true });
|
|
312
|
-
fs.renameSync(legacyMcp, newMcp);
|
|
313
|
-
console.log(
|
|
314
|
-
"[cortex] migrated legacy mcp/ → .context/mcp/ to keep project root clean. " +
|
|
315
|
-
"Re-run 'cortex connect' if Claude/Codex MCP registrations need to be refreshed.",
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const LEGACY_SCRIPT_ENTRIES = [
|
|
320
|
-
"bootstrap.sh",
|
|
321
|
-
"context.sh",
|
|
322
|
-
"dashboard.mjs",
|
|
323
|
-
"dashboard.sh",
|
|
324
|
-
"doctor.sh",
|
|
325
|
-
"embed.sh",
|
|
326
|
-
"ingest.mjs",
|
|
327
|
-
"ingest.sh",
|
|
328
|
-
"install-git-hooks.sh",
|
|
329
|
-
"load-kuzu.sh",
|
|
330
|
-
"load-ryu.sh",
|
|
331
|
-
"memory-compile.mjs",
|
|
332
|
-
"memory-compile.sh",
|
|
333
|
-
"memory-lint.mjs",
|
|
334
|
-
"memory-lint.sh",
|
|
335
|
-
"refresh.sh",
|
|
336
|
-
"status.sh",
|
|
337
|
-
"update-context.sh",
|
|
338
|
-
"watch.sh",
|
|
339
|
-
"lib",
|
|
340
|
-
"parsers"
|
|
341
|
-
];
|
|
342
|
-
|
|
343
|
-
function looksLikeLegacyCortexScriptsDir(scriptsDir) {
|
|
344
|
-
const contextScript = path.join(scriptsDir, "context.sh");
|
|
345
|
-
if (!fs.existsSync(contextScript)) {
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
const contents = fs.readFileSync(contextScript, "utf8");
|
|
350
|
-
return contents.includes("bootstrap)") && contents.includes("graph-load)") && contents.includes("memory-lint)");
|
|
351
|
-
} catch {
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function removeLegacyCortexScripts(targetDir) {
|
|
357
|
-
if (path.resolve(targetDir) === PACKAGE_ROOT) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const scriptsDir = path.join(targetDir, "scripts");
|
|
362
|
-
if (!looksLikeLegacyCortexScriptsDir(scriptsDir)) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
for (const entry of LEGACY_SCRIPT_ENTRIES) {
|
|
367
|
-
fs.rmSync(path.join(scriptsDir, entry), { recursive: true, force: true });
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
if (fs.existsSync(scriptsDir) && fs.readdirSync(scriptsDir).length === 0) {
|
|
372
|
-
fs.rmdirSync(scriptsDir);
|
|
373
|
-
}
|
|
374
|
-
} catch {
|
|
375
|
-
// Best effort. A user's own files in scripts/ must remain untouched.
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function installScaffold(targetDir, force) {
|
|
380
|
-
migrateLegacyMcpLocation(targetDir);
|
|
381
|
-
removeLegacyCortexScripts(targetDir);
|
|
382
|
-
|
|
383
|
-
const copyMap = [
|
|
384
|
-
[path.join(SCAFFOLD_ROOT, ".context"), path.join(targetDir, ".context")],
|
|
385
|
-
[path.join(SCAFFOLD_ROOT, "scripts"), path.join(targetDir, CONTEXT_SCRIPTS_REL)],
|
|
386
|
-
[path.join(SCAFFOLD_ROOT, "mcp"), path.join(targetDir, MCP_PROJECT_REL)],
|
|
387
|
-
[path.join(SCAFFOLD_ROOT, ".githooks"), path.join(targetDir, ".githooks")]
|
|
388
|
-
];
|
|
389
|
-
|
|
390
|
-
for (const [sourcePath, targetPath] of copyMap) {
|
|
391
|
-
ensurePathWritable(targetPath, force);
|
|
392
|
-
copyDirectory(sourcePath, targetPath);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const fileName of ["CLAUDE.md", "AGENTS.md"]) {
|
|
396
|
-
const sourcePath = path.join(SCAFFOLD_ROOT, fileName);
|
|
397
|
-
const targetPath = path.join(targetDir, fileName);
|
|
398
|
-
if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
|
|
399
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const docsDir = path.join(targetDir, "docs");
|
|
404
|
-
fs.mkdirSync(docsDir, { recursive: true });
|
|
405
|
-
const docsSource = path.join(SCAFFOLD_ROOT, "docs", "architecture.md");
|
|
406
|
-
const docsTarget = path.join(docsDir, "cortex-architecture.md");
|
|
407
|
-
if (!fs.existsSync(docsTarget) || force) {
|
|
408
|
-
fs.copyFileSync(docsSource, docsTarget);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
mergeGitignore(targetDir);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function writeTextFile(targetPath, content) {
|
|
415
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
416
|
-
fs.writeFileSync(targetPath, content, "utf8");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function upsertTextFile(targetPath, content) {
|
|
420
|
-
if (fs.existsSync(targetPath)) {
|
|
421
|
-
const existing = fs.readFileSync(targetPath, "utf8");
|
|
422
|
-
if (existing === content) {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
writeTextFile(targetPath, content);
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function escapeRegex(value) {
|
|
431
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function upsertSectionByMarkers(targetPath, startMarker, endMarker, sectionContent) {
|
|
435
|
-
const block = `${startMarker}\n${sectionContent.trimEnd()}\n${endMarker}`;
|
|
436
|
-
const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
|
|
437
|
-
const hasMarkers = existing.includes(startMarker) && existing.includes(endMarker);
|
|
438
|
-
|
|
439
|
-
if (hasMarkers) {
|
|
440
|
-
const pattern = new RegExp(`${escapeRegex(startMarker)}[\\s\\S]*?${escapeRegex(endMarker)}`);
|
|
441
|
-
const replaced = existing.replace(pattern, block);
|
|
442
|
-
if (replaced === existing) {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
writeTextFile(targetPath, replaced.endsWith("\n") ? replaced : `${replaced}\n`);
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
let next = existing;
|
|
450
|
-
if (next.length > 0 && !next.endsWith("\n")) {
|
|
451
|
-
next += "\n";
|
|
452
|
-
}
|
|
453
|
-
if (next.trim().length > 0 && !next.endsWith("\n\n")) {
|
|
454
|
-
next += "\n";
|
|
455
|
-
}
|
|
456
|
-
next += `${block}\n`;
|
|
457
|
-
writeTextFile(targetPath, next);
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function installClaudeCommands(targetDir) {
|
|
462
|
-
const commandSpecs = [
|
|
463
|
-
{
|
|
464
|
-
file: "context-update.md",
|
|
465
|
-
content: `---
|
|
466
|
-
description: "Refresh Cortex context for changed files"
|
|
467
|
-
---
|
|
468
|
-
Execute: cortex update
|
|
469
|
-
`
|
|
470
|
-
}
|
|
471
|
-
];
|
|
472
|
-
|
|
473
|
-
const commandsDir = path.join(targetDir, ".claude", "commands");
|
|
474
|
-
let changed = 0;
|
|
475
|
-
for (const spec of commandSpecs) {
|
|
476
|
-
const targetPath = path.join(commandsDir, spec.file);
|
|
477
|
-
if (upsertTextFile(targetPath, spec.content)) {
|
|
478
|
-
changed += 1;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return { total: commandSpecs.length, changed };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function installCodexAgentsSection(targetDir) {
|
|
485
|
-
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
486
|
-
const startMarker = "<!-- cortex:auto:start -->";
|
|
487
|
-
const endMarker = "<!-- cortex:auto:end -->";
|
|
488
|
-
const section = `## Cortex Auto Workflow
|
|
489
|
-
- Use the \`using-cortex\` skill if available; otherwise follow the commands below.
|
|
490
|
-
- Search before answering code questions: \`cortex search "<query>" --json\`; never answer from memory.
|
|
491
|
-
- Check \`cortex rules --json\` before suggesting changes and \`cortex impact "<query>" --json\` before refactors.
|
|
492
|
-
- Review changed files with \`cortex pattern-evidence <file> --json\` before finalizing.
|
|
493
|
-
- Run \`cortex update\` before completing substantial code changes.
|
|
494
|
-
- If background sync is enabled, check with \`cortex watch status\`.`;
|
|
495
|
-
const changed = upsertSectionByMarkers(agentsPath, startMarker, endMarker, section);
|
|
496
|
-
return { path: agentsPath, changed };
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function installAssistantHelpers(targetDir) {
|
|
500
|
-
const claude = installClaudeCommands(targetDir);
|
|
501
|
-
const codex = installCodexAgentsSection(targetDir);
|
|
502
|
-
return { claude, codex };
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function runCommand(command, args, cwd) {
|
|
506
|
-
return new Promise((resolve, reject) => {
|
|
507
|
-
const child = spawn(command, args, {
|
|
508
|
-
cwd,
|
|
509
|
-
stdio: "inherit",
|
|
510
|
-
env: process.env
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
child.on("error", reject);
|
|
514
|
-
child.on("exit", (code) => {
|
|
515
|
-
if (code === 0) {
|
|
516
|
-
resolve();
|
|
517
|
-
} else {
|
|
518
|
-
reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function runCommandResult(command, args, cwd, stdio = "ignore") {
|
|
525
|
-
return new Promise((resolve) => {
|
|
526
|
-
let done = false;
|
|
527
|
-
const finish = (result) => {
|
|
528
|
-
if (done) {
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
done = true;
|
|
532
|
-
resolve(result);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const child = spawn(command, args, {
|
|
536
|
-
cwd,
|
|
537
|
-
stdio,
|
|
538
|
-
env: process.env
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
child.on("error", (error) => finish({ ok: false, code: null, error }));
|
|
542
|
-
child.on("exit", (code) => finish({ ok: code === 0, code, error: null }));
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function toErrorMessage(error) {
|
|
547
|
-
return error instanceof Error ? error.message : String(error);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function commandExists(command, cwd) {
|
|
551
|
-
const result = await runCommandResult(command, ["--version"], cwd, "ignore");
|
|
552
|
-
return result.ok;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function normalizeName(value) {
|
|
556
|
-
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
557
|
-
return cleaned || "repo";
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
async function connectCodex(targetDir, serverEntry) {
|
|
561
|
-
if (!(await commandExists("codex", targetDir))) {
|
|
562
|
-
console.log("[cortex] codex CLI not found, skipping Codex MCP registration");
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const repoName = normalizeName(path.basename(targetDir));
|
|
567
|
-
const serverName = `cortex-${repoName}`;
|
|
568
|
-
await runCommandResult("codex", ["mcp", "remove", serverName], targetDir, "ignore");
|
|
569
|
-
await runCommand("codex", ["mcp", "add", serverName, "--", "node", serverEntry], targetDir);
|
|
570
|
-
console.log(`[cortex] connected Codex MCP server: ${serverName}`);
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
async function connectClaude(targetDir) {
|
|
575
|
-
if (!(await commandExists("claude", targetDir))) {
|
|
576
|
-
console.log("[cortex] claude CLI not found, skipping Claude Code MCP registration");
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const serverName = "cortex";
|
|
581
|
-
const projectServerEntry = path.join(MCP_PROJECT_REL, "dist", "server.js");
|
|
582
|
-
await runCommandResult("claude", ["mcp", "remove", "-s", "project", serverName], targetDir, "ignore");
|
|
583
|
-
await runCommand(
|
|
584
|
-
"claude",
|
|
585
|
-
["mcp", "add", "-s", "project", serverName, "--", "node", projectServerEntry],
|
|
586
|
-
targetDir
|
|
587
|
-
);
|
|
588
|
-
console.log("[cortex] connected Claude Code MCP server: cortex (project scope)");
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async function connectMcpClients(targetDir, options = {}) {
|
|
593
|
-
const { skipBuild = false } = options;
|
|
594
|
-
const mcpDir = path.join(targetDir, MCP_PROJECT_REL);
|
|
595
|
-
const packageJson = path.join(mcpDir, "package.json");
|
|
596
|
-
const nodeModules = path.join(mcpDir, "node_modules");
|
|
597
|
-
const serverEntry = path.join(mcpDir, "dist", "server.js");
|
|
598
|
-
|
|
599
|
-
if (!fs.existsSync(packageJson)) {
|
|
600
|
-
throw new Error(`Missing ${packageJson}. Run 'cortex init' first.`);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!skipBuild && fs.existsSync(nodeModules)) {
|
|
604
|
-
try {
|
|
605
|
-
await runCommand("npm", ["--prefix", mcpDir, "run", "build", "--silent"], targetDir);
|
|
606
|
-
} catch (error) {
|
|
607
|
-
console.log(`[cortex] MCP build failed, continuing with existing dist output: ${toErrorMessage(error)}`);
|
|
608
|
-
}
|
|
609
|
-
} else if (!skipBuild) {
|
|
610
|
-
console.log("[cortex] .context/mcp/node_modules not found, skipping build (run cortex bootstrap first)");
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (!fs.existsSync(serverEntry)) {
|
|
614
|
-
console.log(`[cortex] warning: ${serverEntry} not found yet; run cortex bootstrap before first MCP call`);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
let connected = 0;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
if (await connectCodex(targetDir, serverEntry)) {
|
|
621
|
-
connected += 1;
|
|
622
|
-
}
|
|
623
|
-
} catch (error) {
|
|
624
|
-
console.log(`[cortex] failed to connect Codex MCP: ${toErrorMessage(error)}`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
try {
|
|
628
|
-
if (await connectClaude(targetDir)) {
|
|
629
|
-
connected += 1;
|
|
630
|
-
}
|
|
631
|
-
} catch (error) {
|
|
632
|
-
console.log(`[cortex] failed to connect Claude MCP: ${toErrorMessage(error)}`);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if (connected === 0) {
|
|
636
|
-
console.log("[cortex] no MCP clients connected");
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return connected;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
async function maybeInstallGitHooks(targetDir) {
|
|
643
|
-
const installScript = path.join(targetDir, CONTEXT_SCRIPTS_REL, "install-git-hooks.sh");
|
|
644
|
-
if (!fs.existsSync(installScript)) {
|
|
645
|
-
return false;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const gitRepo = await runCommandResult("git", ["rev-parse", "--show-toplevel"], targetDir, "ignore");
|
|
649
|
-
if (!gitRepo.ok) {
|
|
650
|
-
console.log("[cortex] git hooks skipped (not a Git repository)");
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
try {
|
|
655
|
-
await runCommand("bash", [installScript], targetDir);
|
|
656
|
-
return true;
|
|
657
|
-
} catch (error) {
|
|
658
|
-
console.log(`[cortex] failed to install git hooks: ${toErrorMessage(error)}`);
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function ensureProjectInitialized(targetDir) {
|
|
664
|
-
const runtimePackageJson = path.join(targetDir, CONTEXT_RUNTIME_REL, "package.json");
|
|
665
|
-
if (!fs.existsSync(runtimePackageJson)) {
|
|
666
|
-
throw new Error(`Missing ${runtimePackageJson}. Run 'cortex init --bootstrap' first.`);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function isTruthyEnv(value) {
|
|
671
|
-
if (typeof value !== "string") {
|
|
672
|
-
return false;
|
|
673
|
-
}
|
|
674
|
-
const normalized = value.trim().toLowerCase();
|
|
675
|
-
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function canAutoInitialize(targetDir) {
|
|
679
|
-
// Legacy mcp/ at root no longer counted — pre-v2.0.5 projects are migrated
|
|
680
|
-
// by installScaffold rather than blocking auto-init.
|
|
681
|
-
const scaffoldPaths = [".context", ".githooks"].map((entry) => path.join(targetDir, entry));
|
|
682
|
-
return scaffoldPaths.every((entryPath) => !fs.existsSync(entryPath));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function isScaffoldOutOfDate(targetDir) {
|
|
686
|
-
const contextScript = path.join(targetDir, CONTEXT_SCRIPTS_REL, "context.sh");
|
|
687
|
-
const legacyContextScript = path.join(targetDir, "scripts", "context.sh");
|
|
688
|
-
if (!fs.existsSync(contextScript)) {
|
|
689
|
-
return fs.existsSync(legacyContextScript);
|
|
690
|
-
}
|
|
691
|
-
const doctorScript = path.join(targetDir, CONTEXT_SCRIPTS_REL, "doctor.sh");
|
|
692
|
-
if (!fs.existsSync(doctorScript)) {
|
|
693
|
-
return true;
|
|
694
|
-
}
|
|
695
|
-
// Treat legacy mcp/ at project root as out-of-date so existing installs
|
|
696
|
-
// get migrated into .context/mcp/ on the next bootstrap.
|
|
697
|
-
if (fs.existsSync(path.join(targetDir, "mcp", "package.json"))) {
|
|
698
|
-
return true;
|
|
699
|
-
}
|
|
700
|
-
const mcpPackage = path.join(targetDir, MCP_PROJECT_REL, "package.json");
|
|
701
|
-
if (!fs.existsSync(mcpPackage)) {
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
try {
|
|
705
|
-
const contents = fs.readFileSync(contextScript, "utf8");
|
|
706
|
-
if (!/\bdoctor\)\s*\n/.test(contents)) {
|
|
707
|
-
return true;
|
|
708
|
-
}
|
|
709
|
-
} catch {
|
|
710
|
-
return true;
|
|
711
|
-
}
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async function confirmPrompt(message) {
|
|
716
|
-
const { createInterface } = await import("node:readline/promises");
|
|
717
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
718
|
-
try {
|
|
719
|
-
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
720
|
-
return answer === "y" || answer === "yes";
|
|
721
|
-
} finally {
|
|
722
|
-
rl.close();
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
async function maybeMigrateScaffold(targetDir, command) {
|
|
727
|
-
if (!isScaffoldOutOfDate(targetDir)) {
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const autoYes = isTruthyEnv(process.env.CORTEX_AUTO_MIGRATE);
|
|
732
|
-
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
733
|
-
|
|
734
|
-
console.error(
|
|
735
|
-
`[cortex] scaffold in ${targetDir} is out of date ` +
|
|
736
|
-
`(missing .context/scripts/doctor.sh, context runtime package.json, doctor subcommand in context.sh, ` +
|
|
737
|
-
`or carries a legacy mcp/ directory at the project root).`
|
|
738
|
-
);
|
|
739
|
-
|
|
740
|
-
let proceed = autoYes;
|
|
741
|
-
if (!autoYes) {
|
|
742
|
-
if (!interactive) {
|
|
743
|
-
throw new Error(
|
|
744
|
-
`Cortex CLI ${process.env.CORTEX_CLI_VERSION ?? ""} needs an updated scaffold to run '${command}'. ` +
|
|
745
|
-
`Run 'cortex init --bootstrap' to upgrade, or re-run with CORTEX_AUTO_MIGRATE=true.`
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
proceed = await confirmPrompt("[cortex] Upgrade scaffold now (runs 'cortex init --bootstrap')? [y/N] ");
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (!proceed) {
|
|
752
|
-
throw new Error("Scaffold upgrade declined. Run 'cortex init --bootstrap' manually to continue.");
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
console.error(`[cortex] migrating scaffold in ${targetDir}`);
|
|
756
|
-
ensureScaffoldExists();
|
|
757
|
-
initializeScaffold(targetDir, true);
|
|
758
|
-
installAssistantHelpers(targetDir);
|
|
759
|
-
await maybeInstallGitHooks(targetDir);
|
|
760
|
-
await runContextCommand(targetDir, ["bootstrap"]);
|
|
761
|
-
console.error(`[cortex] scaffold upgraded; continuing with '${command}'`);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
async function ensureProjectInitializedForMcp(targetDir) {
|
|
765
|
-
const mcpPackageJson = path.join(targetDir, MCP_PROJECT_REL, "package.json");
|
|
766
|
-
const serverEntry = path.join(targetDir, MCP_PROJECT_REL, "dist", "server.js");
|
|
767
|
-
|
|
768
|
-
if (fs.existsSync(mcpPackageJson) && fs.existsSync(serverEntry)) {
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (isScaffoldOutOfDate(targetDir)) {
|
|
773
|
-
await maybeMigrateScaffold(targetDir, "mcp");
|
|
774
|
-
if (fs.existsSync(mcpPackageJson) && fs.existsSync(serverEntry)) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (!isTruthyEnv(process.env.CORTEX_AUTO_BOOTSTRAP_ON_MCP)) {
|
|
780
|
-
ensureProjectInitialized(targetDir);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (!fs.existsSync(mcpPackageJson)) {
|
|
785
|
-
if (!canAutoInitialize(targetDir)) {
|
|
786
|
-
throw new Error(
|
|
787
|
-
`Cannot auto-initialize Cortex in ${targetDir}: scaffold paths already exist. Run 'cortex init --bootstrap' manually.`
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
ensureScaffoldExists();
|
|
791
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
792
|
-
initializeScaffold(targetDir, false);
|
|
793
|
-
installAssistantHelpers(targetDir);
|
|
794
|
-
await maybeInstallGitHooks(targetDir);
|
|
795
|
-
console.log(`[cortex] auto-init completed in ${targetDir}`);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (!fs.existsSync(serverEntry)) {
|
|
799
|
-
console.log("[cortex] auto-bootstrap: running initial bootstrap for MCP");
|
|
800
|
-
await runContextCommand(targetDir, ["bootstrap"]);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// The session bootstrap hook caches index status for 10 minutes; drop the
|
|
805
|
-
// cache whenever a command may have changed the index so new sessions never
|
|
806
|
-
// see a stale "no index"/"index is old" verdict.
|
|
807
|
-
const INDEX_MUTATING_COMMANDS = new Set(["bootstrap", "update", "refresh", "ingest", "embed", "graph-load"]);
|
|
808
|
-
|
|
809
|
-
function invalidateSessionStatusCache(cwd) {
|
|
810
|
-
try {
|
|
811
|
-
fs.rmSync(path.join(cwd, ".context", "cache", "session-status.json"), { force: true });
|
|
812
|
-
} catch {
|
|
813
|
-
// best effort: a stale cache only delays the status refresh
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
async function runContextCommand(cwd, contextArgs) {
|
|
818
|
-
const contextScript = path.join(cwd, CONTEXT_SCRIPTS_REL, "context.sh");
|
|
819
|
-
if (!fs.existsSync(contextScript)) {
|
|
820
|
-
throw new Error(`Missing ${contextScript}. Run 'cortex init' first.`);
|
|
821
|
-
}
|
|
822
|
-
try {
|
|
823
|
-
await runCommand("bash", [contextScript, ...contextArgs], cwd);
|
|
824
|
-
} finally {
|
|
825
|
-
if (INDEX_MUTATING_COMMANDS.has(contextArgs[0])) {
|
|
826
|
-
invalidateSessionStatusCache(cwd);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
async function run() {
|
|
832
|
-
const cliVersion = readCliVersion();
|
|
833
|
-
process.env.CORTEX_CLI_VERSION = cliVersion;
|
|
834
|
-
|
|
835
|
-
const [rawCommand, ...rest] = process.argv.slice(2);
|
|
836
|
-
const command = rawCommand ?? "help";
|
|
837
|
-
|
|
838
|
-
if (command === "version" || command === "--version" || command === "-V") {
|
|
839
|
-
console.log(cliVersion);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (command === "help" || command === "--help" || command === "-h") {
|
|
844
|
-
printHelp();
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (command === "init") {
|
|
849
|
-
ensureScaffoldExists();
|
|
850
|
-
const { target, force, bootstrap, connect, watch } = parseInitArgs(rest);
|
|
851
|
-
printBanner("Cortex initializes repo-scoped context for AI coding agents.");
|
|
852
|
-
fs.mkdirSync(target, { recursive: true });
|
|
853
|
-
initializeScaffold(target, force);
|
|
854
|
-
const helpers = installAssistantHelpers(target);
|
|
855
|
-
await maybeInstallGitHooks(target);
|
|
856
|
-
|
|
857
|
-
console.log(`[cortex] initialized in ${target}`);
|
|
858
|
-
console.log("[cortex] scaffold copied: .context/, .context/scripts/, context runtime (.context/mcp compatibility path), .githooks/, docs/");
|
|
859
|
-
console.log(`[cortex] Claude commands ready: /context-update (${helpers.claude.total} files)`);
|
|
860
|
-
if (helpers.codex.changed) {
|
|
861
|
-
console.log("[cortex] Codex workflow instructions added to AGENTS.md");
|
|
862
|
-
} else {
|
|
863
|
-
console.log("[cortex] Codex workflow instructions already up to date in AGENTS.md");
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (bootstrap) {
|
|
867
|
-
console.log("[cortex] bootstrap: install deps -> ingest -> embeddings -> graph");
|
|
868
|
-
} else {
|
|
869
|
-
console.log("[cortex] next: cortex bootstrap");
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (connect) {
|
|
873
|
-
console.log("[cortex] MCP connect: Codex + Claude Code (if CLIs are installed)");
|
|
874
|
-
} else {
|
|
875
|
-
console.log("[cortex] MCP connect skipped (run 'cortex connect' or init with --connect)");
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (watch) {
|
|
879
|
-
if (bootstrap) {
|
|
880
|
-
console.log("[cortex] background sync: cortex watch start");
|
|
881
|
-
} else {
|
|
882
|
-
console.log("[cortex] background sync pending: run cortex watch start after bootstrap");
|
|
883
|
-
}
|
|
884
|
-
} else {
|
|
885
|
-
console.log("[cortex] background sync skipped (--no-watch)");
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
if (!bootstrap) {
|
|
889
|
-
console.log("");
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (bootstrap) {
|
|
893
|
-
await runContextCommand(target, ["bootstrap"]);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (connect) {
|
|
897
|
-
await connectMcpClients(target);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (watch && bootstrap) {
|
|
901
|
-
await runContextCommand(target, ["watch", "start"]);
|
|
902
|
-
}
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (command === "connect") {
|
|
907
|
-
const { target, skipBuild } = parseConnectArgs(rest);
|
|
908
|
-
ensureProjectInitialized(target);
|
|
909
|
-
const helpers = installAssistantHelpers(target);
|
|
910
|
-
if (helpers.claude.changed > 0 || helpers.codex.changed) {
|
|
911
|
-
console.log("[cortex] assistant helpers updated (.claude/commands + AGENTS.md)");
|
|
912
|
-
}
|
|
913
|
-
await connectMcpClients(target, { skipBuild });
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (command === "mcp") {
|
|
918
|
-
const rawTarget = process.env.CORTEX_PROJECT_ROOT || process.cwd();
|
|
919
|
-
const target = path.resolve(normalizeProjectRoot(rawTarget));
|
|
920
|
-
process.env.CORTEX_PROJECT_ROOT = target;
|
|
921
|
-
await ensureProjectInitializedForMcp(target);
|
|
922
|
-
ensureProjectInitialized(target);
|
|
923
|
-
const serverEntry = path.join(target, MCP_PROJECT_REL, "dist", "server.js");
|
|
924
|
-
if (!fs.existsSync(serverEntry)) {
|
|
925
|
-
throw new Error(`Missing ${serverEntry}. Run 'cortex bootstrap' in ${target} first.`);
|
|
926
|
-
}
|
|
927
|
-
process.stderr.write(`[cortex] starting MCP stdio server from ${serverEntry}\n`);
|
|
928
|
-
await runCommand("node", [serverEntry], target);
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
if (command === "daemon") {
|
|
933
|
-
return runDaemonCommand(rest);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (command === "hook") {
|
|
937
|
-
return runHookShim(rest);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
if (command === "hooks") {
|
|
941
|
-
return runHooksCommand(rest);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (command === "telemetry") {
|
|
945
|
-
return runTelemetryCommand(rest);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (command === "enterprise") {
|
|
949
|
-
return runEnterpriseCommand(rest);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (command === "run") {
|
|
953
|
-
return runRunCommand(rest);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (command === "stage") {
|
|
957
|
-
return runStageCommandShim(rest);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (QUERY_COMMANDS.has(command)) {
|
|
961
|
-
return runQueryCommandShim(command, rest);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const passthrough = new Set([
|
|
965
|
-
"bootstrap",
|
|
966
|
-
"update",
|
|
967
|
-
"status",
|
|
968
|
-
"ingest",
|
|
969
|
-
"embed",
|
|
970
|
-
"graph-load",
|
|
971
|
-
"dashboard",
|
|
972
|
-
"watch",
|
|
973
|
-
"refresh",
|
|
974
|
-
"memory-compile",
|
|
975
|
-
"memory-lint",
|
|
976
|
-
"doctor"
|
|
977
|
-
]);
|
|
978
|
-
|
|
979
|
-
if (!passthrough.has(command)) {
|
|
980
|
-
throw new Error(`Unknown command: ${command}`);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
await maybeMigrateScaffold(process.cwd(), command);
|
|
984
|
-
await runContextCommand(process.cwd(), [command, ...rest]);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// ---------------------------------------------------------------------------
|
|
988
|
-
// v2.0.0: daemon + hooks commands
|
|
989
|
-
// ---------------------------------------------------------------------------
|
|
990
|
-
|
|
991
|
-
const DAEMON_DIR = path.join(process.env.HOME || "", ".cortex");
|
|
992
|
-
const PID_FILE = path.join(DAEMON_DIR, "daemon.pid");
|
|
993
|
-
|
|
994
|
-
function pidFileExists() {
|
|
995
|
-
return fs.existsSync(PID_FILE);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function readPid() {
|
|
999
|
-
try {
|
|
1000
|
-
const raw = fs.readFileSync(PID_FILE, "utf8").trim();
|
|
1001
|
-
const pid = Number.parseInt(raw, 10);
|
|
1002
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1003
|
-
} catch {
|
|
1004
|
-
return null;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function isPidAlive(pid) {
|
|
1009
|
-
if (!pid) return false;
|
|
1010
|
-
try {
|
|
1011
|
-
process.kill(pid, 0);
|
|
1012
|
-
return true;
|
|
1013
|
-
} catch (err) {
|
|
1014
|
-
if (err && typeof err === "object" && err.code === "EPERM") {
|
|
1015
|
-
return true;
|
|
1016
|
-
}
|
|
1017
|
-
return false;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function resolveProjectRuntimeDist() {
|
|
1022
|
-
// v2.0.5: project layout was moved from <cwd>/mcp/ to <cwd>/.context/mcp/.
|
|
1023
|
-
// The runtime still lives there for compatibility, but CLI commands now
|
|
1024
|
-
// treat it as the local context runtime rather than an MCP-only surface.
|
|
1025
|
-
const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
1026
|
-
return path.join(target, CONTEXT_RUNTIME_REL, "dist");
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function resolveProjectMcpDist() {
|
|
1030
|
-
return resolveProjectRuntimeDist();
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function resolveDaemonEntry() {
|
|
1034
|
-
return path.join(resolveProjectRuntimeDist(), "daemon", "main.js");
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
function resolveHookEntry(name) {
|
|
1038
|
-
return path.join(resolveProjectRuntimeDist(), "hooks", `${name}.js`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function resolveCliEntry(name) {
|
|
1042
|
-
return path.join(resolveProjectRuntimeDist(), "cli", `${name}.js`);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
async function runDaemonCommand(args) {
|
|
1046
|
-
const sub = args[0] || "status";
|
|
1047
|
-
if (sub === "start") {
|
|
1048
|
-
if (isPidAlive(readPid())) {
|
|
1049
|
-
console.log("Daemon already running.");
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
fs.mkdirSync(DAEMON_DIR, { recursive: true });
|
|
1053
|
-
const entry = resolveDaemonEntry();
|
|
1054
|
-
if (!fs.existsSync(entry)) {
|
|
1055
|
-
throw new Error(`Daemon entry not found: ${entry}. Build cortex first.`);
|
|
1056
|
-
}
|
|
1057
|
-
const logFd = fs.openSync(path.join(DAEMON_DIR, "daemon.log"), "a");
|
|
1058
|
-
const child = spawn(process.execPath, [entry], {
|
|
1059
|
-
detached: true,
|
|
1060
|
-
stdio: ["ignore", logFd, logFd],
|
|
1061
|
-
});
|
|
1062
|
-
child.unref();
|
|
1063
|
-
console.log(`Daemon started (pid=${child.pid}). Log: ${path.join(DAEMON_DIR, "daemon.log")}`);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
if (sub === "stop") {
|
|
1067
|
-
const pid = readPid();
|
|
1068
|
-
if (!isPidAlive(pid)) {
|
|
1069
|
-
console.log("Daemon not running.");
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
try {
|
|
1073
|
-
process.kill(pid, "SIGTERM");
|
|
1074
|
-
console.log(`Sent SIGTERM to pid ${pid}`);
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
throw new Error(`Failed to stop daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
1077
|
-
}
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
if (sub === "status") {
|
|
1081
|
-
const pid = readPid();
|
|
1082
|
-
if (isPidAlive(pid)) {
|
|
1083
|
-
console.log(`Daemon running (pid=${pid})`);
|
|
1084
|
-
} else {
|
|
1085
|
-
console.log("Daemon not running.");
|
|
1086
|
-
if (pidFileExists()) {
|
|
1087
|
-
console.log(`(stale pid file at ${PID_FILE})`);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|status`);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async function runHookShim(args) {
|
|
1096
|
-
const name = args[0];
|
|
1097
|
-
if (!name) {
|
|
1098
|
-
throw new Error("Usage: cortex hook <name>");
|
|
1099
|
-
}
|
|
1100
|
-
const entry = resolveHookEntry(name);
|
|
1101
|
-
if (!fs.existsSync(entry)) {
|
|
1102
|
-
throw new Error(`Hook script not found: ${entry}`);
|
|
1103
|
-
}
|
|
1104
|
-
// Forward stdin → child, stdout/stderr → parent. Hook protocol = stdio.
|
|
1105
|
-
const child = spawn(process.execPath, [entry], { stdio: "inherit" });
|
|
1106
|
-
await new Promise((resolve) => {
|
|
1107
|
-
child.on("exit", (code) => {
|
|
1108
|
-
process.exit(code ?? 0);
|
|
1109
|
-
resolve(undefined);
|
|
1110
|
-
});
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
const HOOK_DEFS = [
|
|
1115
|
-
{ event: "PreToolUse", matcher: "Edit|Write|Bash|MultiEdit", name: "pre-tool-use" },
|
|
1116
|
-
{ event: "Stop", matcher: undefined, name: "stop" },
|
|
1117
|
-
{ event: "SessionStart", matcher: undefined, name: "session-start" },
|
|
1118
|
-
{ event: "SessionEnd", matcher: undefined, name: "session-end" },
|
|
1119
|
-
{ event: "UserPromptSubmit", matcher: undefined, name: "user-prompt-submit" },
|
|
1120
|
-
{ event: "PreCompact", matcher: undefined, name: "pre-compact" },
|
|
1121
|
-
];
|
|
1122
|
-
|
|
1123
|
-
function managedClaudeSettingsPath() {
|
|
1124
|
-
if (process.platform === "darwin") {
|
|
1125
|
-
return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
|
1126
|
-
}
|
|
1127
|
-
if (process.platform === "linux") {
|
|
1128
|
-
return "/etc/claude-code/managed-settings.json";
|
|
1129
|
-
}
|
|
1130
|
-
return null;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function settingsPathFor(scope) {
|
|
1134
|
-
if (scope === "project") {
|
|
1135
|
-
return path.join(process.cwd(), ".claude", "settings.json");
|
|
1136
|
-
}
|
|
1137
|
-
let home = process.env.HOME || "";
|
|
1138
|
-
const isRoot = process.getuid && process.getuid() === 0;
|
|
1139
|
-
if (isRoot) {
|
|
1140
|
-
const sudoUidRaw = process.env.SUDO_UID;
|
|
1141
|
-
const sudoUid = sudoUidRaw ? parseInt(sudoUidRaw, 10) : NaN;
|
|
1142
|
-
if (Number.isFinite(sudoUid)) {
|
|
1143
|
-
try {
|
|
1144
|
-
home = os.userInfo({ uid: sudoUid }).homedir;
|
|
1145
|
-
} catch {
|
|
1146
|
-
// Fall back to HOME below.
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return path.join(home, ".claude", "settings.json");
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function readJsonSafe(file) {
|
|
1154
|
-
if (!fs.existsSync(file)) return {};
|
|
1155
|
-
try {
|
|
1156
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1157
|
-
} catch {
|
|
1158
|
-
return {};
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function hookInstalledInSettings(settings, def) {
|
|
1163
|
-
const rows = settings.hooks?.[def.event] || [];
|
|
1164
|
-
return rows.some((row) => (row.hooks?.[0]?.command || "").startsWith(`cortex hook ${def.name}`));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function readManagedClaudeSettings() {
|
|
1168
|
-
const file = managedClaudeSettingsPath();
|
|
1169
|
-
if (!file) return { file: null, settings: {} };
|
|
1170
|
-
return { file, settings: readJsonSafe(file) };
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function hasManagedClaudeHooks() {
|
|
1174
|
-
const { settings } = readManagedClaudeSettings();
|
|
1175
|
-
if (settings.allowManagedHooksOnly !== true) return false;
|
|
1176
|
-
return HOOK_DEFS.every((def) => hookInstalledInSettings(settings, def));
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function writeJson(file, data) {
|
|
1180
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1181
|
-
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Enterprise == govern. One command, sudo-elevated, hard-fail without it.
|
|
1185
|
-
// `cortex enterprise <api-key>` does the full install. Subcommands status/sync/uninstall
|
|
1186
|
-
// dispatch to scaffold/mcp/dist/cli/govern.js.
|
|
1187
|
-
|
|
1188
|
-
function requireSudoElevation() {
|
|
1189
|
-
const isRoot = process.getuid && process.getuid() === 0;
|
|
1190
|
-
if (!isRoot) {
|
|
1191
|
-
process.stderr.write(bullet("fail", "This command requires admin privileges to install non-bypassable enforcement.", process.stderr) + "\n");
|
|
1192
|
-
process.stderr.write(muted(" Re-run as: sudo " + process.argv.slice(1).join(" "), process.stderr) + "\n");
|
|
1193
|
-
process.exit(1);
|
|
1194
|
-
}
|
|
1195
|
-
const sudoUser = process.env.SUDO_USER;
|
|
1196
|
-
const sudoUidRaw = process.env.SUDO_UID;
|
|
1197
|
-
const sudoGidRaw = process.env.SUDO_GID;
|
|
1198
|
-
if (!sudoUser || !sudoUidRaw || !sudoGidRaw) {
|
|
1199
|
-
process.stderr.write(bullet("fail", "Use 'sudo' to elevate (not 'su' or a root login).", process.stderr) + "\n");
|
|
1200
|
-
process.stderr.write(muted(" Cortex needs SUDO_USER/SUDO_UID/SUDO_GID set so that enterprise.yml,", process.stderr) + "\n");
|
|
1201
|
-
process.stderr.write(muted(" Claude Code hooks and the daemon end up owned by your user.", process.stderr) + "\n");
|
|
1202
|
-
process.exit(1);
|
|
1203
|
-
}
|
|
1204
|
-
const uid = parseInt(sudoUidRaw, 10);
|
|
1205
|
-
const gid = parseInt(sudoGidRaw, 10);
|
|
1206
|
-
if (!Number.isFinite(uid) || !Number.isFinite(gid)) {
|
|
1207
|
-
process.stderr.write(bullet("fail", "SUDO_UID/SUDO_GID are not valid integers — refusing to drop privileges.", process.stderr) + "\n");
|
|
1208
|
-
process.exit(1);
|
|
1209
|
-
}
|
|
1210
|
-
return { user: sudoUser, uid, gid };
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function dropPrivileges(sudo) {
|
|
1214
|
-
const sudoInfo = os.userInfo({ uid: sudo.uid });
|
|
1215
|
-
process.setgid(sudo.gid);
|
|
1216
|
-
process.setuid(sudo.uid);
|
|
1217
|
-
process.env.HOME = sudoInfo.homedir;
|
|
1218
|
-
process.env.USER = sudo.user;
|
|
1219
|
-
process.env.LOGNAME = sudo.user;
|
|
1220
|
-
return sudoInfo.homedir;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function loadGovernModule() {
|
|
1224
|
-
const entry = resolveCliEntry("govern");
|
|
1225
|
-
if (!fs.existsSync(entry)) {
|
|
1226
|
-
throw new Error(
|
|
1227
|
-
`Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
return import(pathToFileURL(entry).href);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const ENTERPRISE_SUBCOMMANDS = new Set(["status", "sync", "uninstall", "repair", "help", "--help", "-h"]);
|
|
1234
|
-
|
|
1235
|
-
async function runEnterpriseCommand(args) {
|
|
1236
|
-
if (args.length === 0 || ENTERPRISE_SUBCOMMANDS.has(args[0])) {
|
|
1237
|
-
return runEnterpriseSubcommand(args);
|
|
1238
|
-
}
|
|
1239
|
-
return runEnterpriseInstall(args);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
async function runEnterpriseSubcommand(args) {
|
|
1243
|
-
const sub = args[0] ?? "help";
|
|
1244
|
-
|
|
1245
|
-
if (sub === "help" || sub === "--help" || sub === "-h" || !sub) {
|
|
1246
|
-
console.log(gradient("cortex enterprise") + muted(" · governance, armed."));
|
|
1247
|
-
console.log(helpRow("enterprise <api-key>", "Install (sudo). Managed enforcement + hooks + daemon."));
|
|
1248
|
-
console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
|
|
1249
|
-
console.log(helpRow("enterprise status [--verbose|--json]", "Show local enforcement state"));
|
|
1250
|
-
console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
|
|
1251
|
-
console.log(helpRow("enterprise uninstall", "Remove. [--break-glass --reason \"<text>\"] in enforced mode (sudo)"));
|
|
1252
|
-
console.log(helpRow("enterprise repair", "Verify managed paths, clear .cortex-tamper.lock (sudo)"));
|
|
1253
|
-
console.log("");
|
|
1254
|
-
console.log(muted("Default endpoint: https://cortex-web-rho.vercel.app"));
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
if (sub === "status") {
|
|
1259
|
-
let verbose = false;
|
|
1260
|
-
let json = false;
|
|
1261
|
-
for (let i = 1; i < args.length; i++) {
|
|
1262
|
-
if (args[i] === "--verbose" || args[i] === "-v") verbose = true;
|
|
1263
|
-
else if (args[i] === "--json") json = true;
|
|
1264
|
-
else if (args[i].startsWith("-")) {
|
|
1265
|
-
throw new Error(`Unknown enterprise status option: ${args[i]}`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
const mod = await loadGovernModule();
|
|
1269
|
-
mod.runGovernStatus({ cwd: process.cwd(), verbose, json });
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
if (sub === "sync") {
|
|
1274
|
-
requireSudoElevation();
|
|
1275
|
-
const mod = await loadGovernModule();
|
|
1276
|
-
await mod.runGovernSync({ cwd: process.cwd() });
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (sub === "uninstall") {
|
|
1281
|
-
let breakGlass = false;
|
|
1282
|
-
let reason;
|
|
1283
|
-
for (let i = 1; i < args.length; i++) {
|
|
1284
|
-
if (args[i] === "--break-glass") breakGlass = true;
|
|
1285
|
-
else if (args[i] === "--reason" && args[i + 1]) {
|
|
1286
|
-
reason = args[i + 1];
|
|
1287
|
-
i++;
|
|
1288
|
-
} else if (args[i].startsWith("-")) {
|
|
1289
|
-
throw new Error(`Unknown enterprise uninstall option: ${args[i]}`);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
requireSudoElevation();
|
|
1293
|
-
const mod = await loadGovernModule();
|
|
1294
|
-
const result = await mod.runGovernUninstall({
|
|
1295
|
-
cli: "all",
|
|
1296
|
-
breakGlass,
|
|
1297
|
-
reason,
|
|
1298
|
-
cwd: process.cwd(),
|
|
1299
|
-
});
|
|
1300
|
-
if (!result.ok) {
|
|
1301
|
-
printBullet("fail", result.message, process.stderr);
|
|
1302
|
-
process.exit(1);
|
|
1303
|
-
}
|
|
1304
|
-
printBullet("ok", result.message);
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (sub === "repair") {
|
|
1309
|
-
let reason;
|
|
1310
|
-
for (let i = 1; i < args.length; i++) {
|
|
1311
|
-
if (args[i] === "--reason" && args[i + 1]) {
|
|
1312
|
-
reason = args[i + 1];
|
|
1313
|
-
i++;
|
|
1314
|
-
} else if (args[i].startsWith("-")) {
|
|
1315
|
-
throw new Error(`Unknown enterprise repair option: ${args[i]}`);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
requireSudoElevation();
|
|
1319
|
-
const mod = await loadGovernModule();
|
|
1320
|
-
const result = await mod.runGovernRepair({ cwd: process.cwd(), reason });
|
|
1321
|
-
if (!result.ok) {
|
|
1322
|
-
printBullet("fail", result.message, process.stderr);
|
|
1323
|
-
process.exit(1);
|
|
1324
|
-
}
|
|
1325
|
-
printBullet("ok", result.message);
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
throw new Error(`Unknown enterprise subcommand: ${sub}`);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
async function runEnterpriseInstall(args) {
|
|
1333
|
-
const apiKey = args[0];
|
|
1334
|
-
let endpoint;
|
|
1335
|
-
let frameworks;
|
|
1336
|
-
let installHooks = true;
|
|
1337
|
-
let startDaemon = true;
|
|
1338
|
-
for (let i = 1; i < args.length; i++) {
|
|
1339
|
-
if (args[i] === "--endpoint" && args[i + 1]) {
|
|
1340
|
-
endpoint = args[i + 1];
|
|
1341
|
-
i++;
|
|
1342
|
-
} else if (args[i] === "--frameworks" && args[i + 1]) {
|
|
1343
|
-
frameworks = args[i + 1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
1344
|
-
i++;
|
|
1345
|
-
} else if (args[i] === "--no-hooks") {
|
|
1346
|
-
installHooks = false;
|
|
1347
|
-
} else if (args[i] === "--no-daemon") {
|
|
1348
|
-
startDaemon = false;
|
|
1349
|
-
} else if (args[i].startsWith("-")) {
|
|
1350
|
-
throw new Error(`Unknown enterprise install option: ${args[i]}`);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const sudo = requireSudoElevation();
|
|
1355
|
-
|
|
1356
|
-
process.stdout.write(headerBanner({ tagline: " Cortex enterprise — activating governance" }));
|
|
1357
|
-
|
|
1358
|
-
const enterpriseEntry = resolveCliEntry("enterprise-setup");
|
|
1359
|
-
if (!fs.existsSync(enterpriseEntry)) {
|
|
1360
|
-
printBullet("fail", `Build the project's context runtime first (missing ${enterpriseEntry}). Run 'cortex bootstrap' in the project root.`);
|
|
1361
|
-
process.exit(1);
|
|
1362
|
-
}
|
|
1363
|
-
const enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
|
|
1364
|
-
|
|
1365
|
-
// Step 1 — Initializing Cortex core (license validation + enterprise.yml).
|
|
1366
|
-
const step1 = spinner("Initializing Cortex core");
|
|
1367
|
-
const setupResult = await enterpriseMod.runEnterpriseSetup({ apiKey, endpoint, cwd: process.cwd() });
|
|
1368
|
-
if (!setupResult.ok) {
|
|
1369
|
-
step1.stop("fail", `Initializing Cortex core — ${setupResult.message}`);
|
|
1370
|
-
process.exit(1);
|
|
1371
|
-
}
|
|
1372
|
-
step1.stop("ok", `Initializing Cortex core — license ${setupResult.edition}, expires ${setupResult.expiresAt}`);
|
|
1373
|
-
printBullet("info", muted(`config: ${setupResult.configPath}`));
|
|
1374
|
-
|
|
1375
|
-
// enterprise.yml was just written as root; transfer ownership before we drop privs.
|
|
1376
|
-
try {
|
|
1377
|
-
fs.chownSync(setupResult.configPath, sudo.uid, sudo.gid);
|
|
1378
|
-
} catch (err) {
|
|
1379
|
-
printBullet("warn", `Could not chown ${setupResult.configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Step 2 — Loading policy engine (govern install: managed config + frameworks).
|
|
1383
|
-
const baseUrl = (endpoint ?? "https://cortex-web-rho.vercel.app").replace(/\/$/, "");
|
|
1384
|
-
const step2 = spinner("Loading policy engine");
|
|
1385
|
-
const governMod = await loadGovernModule();
|
|
1386
|
-
const governResult = await governMod.runGovernInstall({
|
|
1387
|
-
cli: "all",
|
|
1388
|
-
mode: "enforced",
|
|
1389
|
-
cwd: process.cwd(),
|
|
1390
|
-
apiKey,
|
|
1391
|
-
baseUrl,
|
|
1392
|
-
frameworks,
|
|
1393
|
-
});
|
|
1394
|
-
if (!governResult.ok) {
|
|
1395
|
-
step2.stop("fail", `Loading policy engine — ${governResult.message}`);
|
|
1396
|
-
process.exit(1);
|
|
1397
|
-
}
|
|
1398
|
-
step2.stop("ok", "Loading policy engine — policies armed");
|
|
1399
|
-
|
|
1400
|
-
// govern.local.json was written as root in cwd/.context. chown it back.
|
|
1401
|
-
const governStatePath = path.join(process.cwd(), ".context", "govern.local.json");
|
|
1402
|
-
if (fs.existsSync(governStatePath)) {
|
|
1403
|
-
try {
|
|
1404
|
-
fs.chownSync(governStatePath, sudo.uid, sudo.gid);
|
|
1405
|
-
} catch (err) {
|
|
1406
|
-
printBullet("warn", `Could not chown ${governStatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// Step 3 — Connecting audit pipeline (telemetry endpoint already wired by govern install).
|
|
1411
|
-
const step3 = spinner("Connecting audit pipeline");
|
|
1412
|
-
step3.stop("ok", `Connecting audit pipeline — endpoint ${baseUrl}`);
|
|
1413
|
-
|
|
1414
|
-
// Drop privileges before user-scope writes (Claude Code hooks in $HOME) and daemon spawn.
|
|
1415
|
-
dropPrivileges(sudo);
|
|
1416
|
-
|
|
1417
|
-
// Step 4 — Preparing MCP gateway (Claude Code hooks bind the MCP surface).
|
|
1418
|
-
if (installHooks) {
|
|
1419
|
-
const step4 = spinner("Preparing MCP gateway");
|
|
1420
|
-
try {
|
|
1421
|
-
if (hasManagedClaudeHooks()) {
|
|
1422
|
-
step4.stop("ok", "Preparing MCP gateway — managed Claude hooks active");
|
|
1423
|
-
} else {
|
|
1424
|
-
await runHooksCommand(["install"]);
|
|
1425
|
-
step4.stop("ok", "Preparing MCP gateway — hooks installed");
|
|
1426
|
-
}
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
step4.stop("fail", `Preparing MCP gateway — ${err instanceof Error ? err.message : String(err)}`);
|
|
1429
|
-
}
|
|
1430
|
-
} else {
|
|
1431
|
-
printBullet("warn", "Preparing MCP gateway — skipped (--no-hooks)");
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// Step 5 — Installing guardrails (supervisor daemon).
|
|
1435
|
-
if (startDaemon) {
|
|
1436
|
-
const step5 = spinner("Installing guardrails");
|
|
1437
|
-
try {
|
|
1438
|
-
await runDaemonCommand(["start"]);
|
|
1439
|
-
step5.stop("ok", "Installing guardrails — daemon online");
|
|
1440
|
-
} catch (err) {
|
|
1441
|
-
step5.stop("fail", `Installing guardrails — ${err instanceof Error ? err.message : String(err)}`);
|
|
1442
|
-
}
|
|
1443
|
-
} else {
|
|
1444
|
-
printBullet("warn", "Installing guardrails — skipped (--no-daemon)");
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
console.log("");
|
|
1448
|
-
console.log(bullet("ok", bold("Cortex is running.")));
|
|
1449
|
-
console.log(muted(" Monitoring AI activity. No violations detected."));
|
|
1450
|
-
console.log(muted(" Next: ") + accent("cortex enterprise status") + muted(" · ") + accent("cortex telemetry test"));
|
|
1451
|
-
console.log("");
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
const RUN_CLIS = new Set(["claude", "codex", "copilot"]);
|
|
1455
|
-
const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
|
|
1456
|
-
|
|
1457
|
-
async function runRunCommand(args) {
|
|
1458
|
-
const sub = args[0];
|
|
1459
|
-
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
1460
|
-
console.log("Usage:");
|
|
1461
|
-
console.log(" cortex run <claude|codex|copilot> [args...]");
|
|
1462
|
-
console.log("");
|
|
1463
|
-
console.log("Wraps the named AI CLI in cortex enforcement:");
|
|
1464
|
-
console.log(" claude/codex: passthrough — their own managed-config + sandbox");
|
|
1465
|
-
console.log(" cover Tier 1 enforcement after 'cortex enterprise <key>'.");
|
|
1466
|
-
console.log(" copilot: Tier 2 — OS-level sandbox (sandbox-exec on macOS,");
|
|
1467
|
-
console.log(" bwrap on Linux). Denies writes to ~/.copilot/,");
|
|
1468
|
-
console.log(" ~/.copilot.local/, /etc/copilot* so AI cannot");
|
|
1469
|
-
console.log(" reconfigure itself out of governance.");
|
|
1470
|
-
console.log("");
|
|
1471
|
-
console.log("Tip: alias copilot='cortex run copilot' so direct 'copilot' invocations");
|
|
1472
|
-
console.log("are also wrapped. Direct invocations are otherwise caught by Tier 3");
|
|
1473
|
-
console.log("ungoverned-session detection (Phase 5).");
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (!RUN_CLIS.has(sub)) {
|
|
1477
|
-
throw new Error(`Unknown AI CLI: ${sub}. Use claude, codex, or copilot.`);
|
|
1478
|
-
}
|
|
1479
|
-
const entry = resolveCliEntry("run");
|
|
1480
|
-
if (!fs.existsSync(entry)) {
|
|
1481
|
-
throw new Error(
|
|
1482
|
-
`Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
1486
|
-
const exitCode = await mod.runAiCli({ cli: sub, args: args.slice(1) });
|
|
1487
|
-
process.exit(exitCode);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
async function runStageCommandShim(args) {
|
|
1491
|
-
const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
1492
|
-
process.env.CORTEX_PROJECT_ROOT = path.resolve(target);
|
|
1493
|
-
const entry = resolveCliEntry("stage");
|
|
1494
|
-
if (!fs.existsSync(entry)) {
|
|
1495
|
-
throw new Error(
|
|
1496
|
-
`Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`,
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
1500
|
-
await mod.runStageCommand(args);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
async function runQueryCommandShim(command, args) {
|
|
1504
|
-
const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
1505
|
-
process.env.CORTEX_PROJECT_ROOT = path.resolve(target);
|
|
1506
|
-
const entry = resolveCliEntry("query");
|
|
1507
|
-
if (!fs.existsSync(entry)) {
|
|
1508
|
-
throw new Error(
|
|
1509
|
-
`Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
|
|
1510
|
-
);
|
|
1511
|
-
}
|
|
1512
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
1513
|
-
await mod.runQueryCommand([command, ...args]);
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
async function runTelemetryCommand(args) {
|
|
1517
|
-
const sub = args[0] || "help";
|
|
1518
|
-
if (sub === "test") {
|
|
1519
|
-
const entry = resolveCliEntry("telemetry-test");
|
|
1520
|
-
if (!fs.existsSync(entry)) {
|
|
1521
|
-
throw new Error(`Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`);
|
|
1522
|
-
}
|
|
1523
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
1524
|
-
const code = await mod.runTelemetryTest();
|
|
1525
|
-
process.exit(code);
|
|
1526
|
-
}
|
|
1527
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1528
|
-
console.log("Usage:");
|
|
1529
|
-
console.log(" cortex telemetry test Smoke-test the push pipeline end-to-end");
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
throw new Error(`Unknown telemetry subcommand: ${sub}`);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
async function runHooksCommand(args) {
|
|
1536
|
-
const sub = args[0] || "status";
|
|
1537
|
-
const scope = args.includes("--project") ? "project" : "user";
|
|
1538
|
-
const target = settingsPathFor(scope);
|
|
1539
|
-
|
|
1540
|
-
if (sub === "install") {
|
|
1541
|
-
const settings = readJsonSafe(target);
|
|
1542
|
-
settings.hooks = settings.hooks || {};
|
|
1543
|
-
for (const def of HOOK_DEFS) {
|
|
1544
|
-
const entry = {
|
|
1545
|
-
...(def.matcher ? { matcher: def.matcher } : {}),
|
|
1546
|
-
hooks: [{ type: "command", command: `cortex hook ${def.name}` }],
|
|
1547
|
-
};
|
|
1548
|
-
const existing = settings.hooks[def.event] || [];
|
|
1549
|
-
const filtered = existing.filter((row) => {
|
|
1550
|
-
const cmd = (row.hooks?.[0]?.command || "");
|
|
1551
|
-
return !cmd.startsWith("cortex hook ");
|
|
1552
|
-
});
|
|
1553
|
-
settings.hooks[def.event] = [...filtered, entry];
|
|
1554
|
-
}
|
|
1555
|
-
writeJson(target, settings);
|
|
1556
|
-
console.log(`Installed cortex hooks into ${target}`);
|
|
1557
|
-
console.log(`Hooks: ${HOOK_DEFS.map((d) => d.name).join(", ")}`);
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
if (sub === "uninstall") {
|
|
1561
|
-
const settings = readJsonSafe(target);
|
|
1562
|
-
if (settings.hooks) {
|
|
1563
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
1564
|
-
settings.hooks[event] = (settings.hooks[event] || []).filter((row) => {
|
|
1565
|
-
const cmd = (row.hooks?.[0]?.command || "");
|
|
1566
|
-
return !cmd.startsWith("cortex hook ");
|
|
1567
|
-
});
|
|
1568
|
-
if (settings.hooks[event].length === 0) {
|
|
1569
|
-
delete settings.hooks[event];
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
1573
|
-
delete settings.hooks;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
writeJson(target, settings);
|
|
1577
|
-
console.log(`Removed cortex hooks from ${target}`);
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
if (sub === "status") {
|
|
1581
|
-
const settings = readJsonSafe(target);
|
|
1582
|
-
const managed = scope === "user" ? readManagedClaudeSettings() : { file: null, settings: {} };
|
|
1583
|
-
const installed = [];
|
|
1584
|
-
for (const def of HOOK_DEFS) {
|
|
1585
|
-
const userFound = hookInstalledInSettings(settings, def);
|
|
1586
|
-
const managedFound = scope === "user" ? hookInstalledInSettings(managed.settings, def) : false;
|
|
1587
|
-
const found = userFound || managedFound;
|
|
1588
|
-
let source = "";
|
|
1589
|
-
if (userFound && managedFound) source = "user+managed";
|
|
1590
|
-
else if (userFound) source = "user";
|
|
1591
|
-
else if (managedFound) source = "managed";
|
|
1592
|
-
installed.push({ name: def.name, event: def.event, found, source });
|
|
1593
|
-
}
|
|
1594
|
-
console.log(`Settings file: ${target}`);
|
|
1595
|
-
if (scope === "user" && managed.file) {
|
|
1596
|
-
console.log(`Managed settings: ${managed.file}`);
|
|
1597
|
-
}
|
|
1598
|
-
for (const row of installed) {
|
|
1599
|
-
console.log(` ${row.found ? "✓" : "✗"} ${row.event} → ${row.name}${row.source ? ` (${row.source})` : ""}`);
|
|
1600
|
-
}
|
|
1601
|
-
if (scope === "user" && managed.settings.allowManagedHooksOnly === true) {
|
|
1602
|
-
console.log(" note: managed Claude hooks are authoritative; user hooks may be intentionally absent");
|
|
1603
|
-
}
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
throw new Error(`Unknown hooks subcommand: ${sub}. Try install|uninstall|status`);
|
|
1607
|
-
}
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { runCli } from "./cli/router.mjs";
|
|
5
|
+
import { bullet } from "./style.mjs";
|
|
1608
6
|
|
|
1609
7
|
function resolveArgv1() {
|
|
1610
8
|
if (!process.argv[1]) return null;
|
|
@@ -1619,11 +17,18 @@ const invokedAsScript =
|
|
|
1619
17
|
process.argv[1] && import.meta.url === pathToFileURL(resolveArgv1()).href;
|
|
1620
18
|
|
|
1621
19
|
if (invokedAsScript) {
|
|
1622
|
-
|
|
20
|
+
runCli().catch((error) => {
|
|
1623
21
|
const message = error instanceof Error ? error.message : String(error);
|
|
1624
22
|
process.stderr.write(bullet("fail", message, process.stderr) + "\n");
|
|
1625
23
|
process.exit(1);
|
|
1626
24
|
});
|
|
1627
25
|
}
|
|
1628
26
|
|
|
1629
|
-
export {
|
|
27
|
+
export {
|
|
28
|
+
buildInitialConfig,
|
|
29
|
+
detectInitialSourcePaths,
|
|
30
|
+
hardenEnterpriseConfigPermissions,
|
|
31
|
+
isScaffoldOutOfDate,
|
|
32
|
+
slugifyRepoId,
|
|
33
|
+
} from "./cli/scaffold.mjs";
|
|
34
|
+
export { runEnterpriseInstall } from "./cli/enterprise.mjs";
|