@danielblomma/cortex-mcp 1.7.2 → 2.0.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.
Files changed (75) hide show
  1. package/bin/cortex.mjs +679 -32
  2. package/bin/style.mjs +349 -0
  3. package/package.json +4 -3
  4. package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
  5. package/scaffold/mcp/src/cli/govern.ts +987 -0
  6. package/scaffold/mcp/src/cli/run.ts +306 -0
  7. package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
  8. package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
  9. package/scaffold/mcp/src/core/audit/query.ts +81 -0
  10. package/scaffold/mcp/src/core/audit/writer.ts +68 -0
  11. package/scaffold/mcp/src/core/config.ts +329 -0
  12. package/scaffold/mcp/src/core/index.ts +34 -0
  13. package/scaffold/mcp/src/core/license.ts +202 -0
  14. package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
  15. package/scaffold/mcp/src/core/policy/injection.ts +229 -0
  16. package/scaffold/mcp/src/core/policy/store.ts +197 -0
  17. package/scaffold/mcp/src/core/rbac/check.ts +40 -0
  18. package/scaffold/mcp/src/core/telemetry/collector.ts +234 -0
  19. package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
  20. package/scaffold/mcp/src/core/validators/config.ts +47 -0
  21. package/scaffold/mcp/src/core/validators/engine.ts +199 -0
  22. package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
  23. package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
  24. package/scaffold/mcp/src/daemon/client.ts +155 -0
  25. package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
  26. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
  27. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
  28. package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
  29. package/scaffold/mcp/src/daemon/main.ts +300 -0
  30. package/scaffold/mcp/src/daemon/paths.ts +41 -0
  31. package/scaffold/mcp/src/daemon/protocol.ts +101 -0
  32. package/scaffold/mcp/src/daemon/server.ts +227 -0
  33. package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
  34. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
  35. package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
  36. package/scaffold/mcp/src/enterprise/index.ts +415 -0
  37. package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
  38. package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
  39. package/scaffold/mcp/src/enterprise/privacy/boundary.ts +212 -0
  40. package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
  41. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -0
  42. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
  43. package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
  44. package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
  45. package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
  46. package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
  47. package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
  48. package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
  49. package/scaffold/mcp/src/hooks/session-end.ts +73 -0
  50. package/scaffold/mcp/src/hooks/session-start.ts +78 -0
  51. package/scaffold/mcp/src/hooks/shared.ts +134 -0
  52. package/scaffold/mcp/src/hooks/stop.ts +60 -0
  53. package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
  54. package/scaffold/mcp/src/plugin.ts +150 -0
  55. package/scaffold/mcp/src/server.ts +218 -7
  56. package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
  57. package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
  58. package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
  59. package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
  60. package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
  61. package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
  62. package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
  63. package/scaffold/mcp/tests/govern.test.mjs +74 -0
  64. package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
  65. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
  66. package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
  67. package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
  68. package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
  69. package/scaffold/mcp/tests/run.test.mjs +109 -0
  70. package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
  71. package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
  72. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
  73. package/scaffold/scripts/bootstrap.sh +0 -11
  74. package/scaffold/scripts/doctor.sh +24 -4
  75. package/types.js +5 -0
package/bin/cortex.mjs CHANGED
@@ -1,9 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import { spawn } from "node:child_process";
6
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";
7
18
 
8
19
  const __filename = fileURLToPath(import.meta.url);
9
20
  const __dirname = path.dirname(__filename);
@@ -24,43 +35,62 @@ const GITIGNORE_LINES = [
24
35
  "mcp/node_modules/"
25
36
  ];
26
37
 
27
- const CORTEX_LOGO = [
28
- " CCC OOO RRRR TTTTT EEEEE X X",
29
- " C C O O R R T E X X",
30
- " C O O RRRR T EEEE X",
31
- " C C O O R R T E X X",
32
- " CCC OOO R R T EEEEE X X"
33
- ].join("\n");
34
-
35
38
  function printBanner(title) {
36
- console.log(CORTEX_LOGO);
37
- if (title) {
38
- console.log(title);
39
+ process.stdout.write(headerBanner({ tagline: title }));
40
+ }
41
+
42
+ // Help-row formatter: "<command>" in accent cyan, gap, "<description>" in muted grey.
43
+ function helpRow(cmd, desc) {
44
+ const target = 46;
45
+ const pad = cmd.length >= target ? " " : " ".repeat(target - cmd.length);
46
+ if (desc) {
47
+ return ` ${accent(cmd)}${pad}${muted(desc)}`;
39
48
  }
40
- console.log("");
49
+ return ` ${accent(cmd)}`;
50
+ }
51
+
52
+ function helpSection(title) {
53
+ return `\n${bold(muted(title))}`;
41
54
  }
42
55
 
43
56
  function printHelp() {
44
- console.log("Cortex CLI");
57
+ console.log(gradient("CORTEX CLI") + muted(" · governance for AI coding agents"));
58
+ console.log(muted(" Cortex is in control. Calm, intelligent, always monitoring."));
59
+ console.log(helpSection("USAGE"));
60
+ console.log(helpRow("cortex <command> [options]"));
61
+
62
+ console.log(helpSection("CONTEXT"));
63
+ console.log(helpRow("init [path]", "Scaffold a project with --force/--bootstrap/--connect/--watch"));
64
+ console.log(helpRow("connect [path]", "Re-register MCP clients (Codex + Claude Code)"));
65
+ console.log(helpRow("bootstrap", "Install deps, ingest, embed, load graph"));
66
+ console.log(helpRow("update", "Refresh context for changed files"));
67
+ console.log(helpRow("status", "Project context status"));
68
+ console.log(helpRow("doctor", "Diagnose setup health"));
69
+ console.log(helpRow("ingest [--changed] [--verbose]", "Re-index source files"));
70
+ console.log(helpRow("embed [--changed]", "Recompute embeddings"));
71
+ console.log(helpRow("graph-load [--no-reset]", "Reload the dependency graph"));
72
+ console.log(helpRow("dashboard [--interval <sec>]", "Live local dashboard"));
73
+ console.log(helpRow("memory-compile [--dry-run] [--verbose]", "Compile memory artifacts"));
74
+ console.log(helpRow("memory-lint [--verbose] [--json]", "Lint compiled memory"));
75
+ console.log(helpRow("watch [start|stop|status|run|once]", "Background sync (--interval, --debounce, --mode)"));
76
+
77
+ console.log(helpSection("GOVERNANCE"));
78
+ console.log(helpRow("enterprise <api-key>", "Install enforcement + hooks + daemon (sudo)"));
79
+ console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
80
+ console.log(helpRow("enterprise status", "Show local enforcement state"));
81
+ console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
82
+ console.log(helpRow("enterprise uninstall", "Remove enforcement (sudo, --break-glass --reason)"));
83
+ console.log(helpRow("enterprise repair", "Verify managed paths, clear tamper-lock (sudo)"));
84
+ console.log(helpRow("run <claude|codex|copilot> [args...]", "Wrap an AI CLI in cortex enforcement"));
85
+ console.log(helpRow("daemon [start|stop|status]", "Local supervisor daemon"));
86
+ console.log(helpRow("hooks [install|uninstall|status] [--project]", "Claude Code hooks"));
87
+ console.log(helpRow("telemetry test", "Smoke-test the push pipeline"));
88
+
89
+ console.log(helpSection("MISC"));
90
+ console.log(helpRow("mcp", "Run the MCP stdio server for the current project"));
91
+ console.log(helpRow("version", "Print CLI version"));
92
+ console.log(helpRow("help", "This screen"));
45
93
  console.log("");
46
- console.log("Usage:");
47
- console.log(" cortex init [path] [--force] [--bootstrap] [--connect] [--no-connect] [--watch] [--no-watch]");
48
- console.log(" cortex connect [path] [--skip-build]");
49
- console.log(" cortex mcp");
50
- console.log(
51
- " cortex watch [start|stop|status|run|once] [--interval <sec>] [--debounce <sec>] [--mode <auto|event|poll>]"
52
- );
53
- console.log(" cortex bootstrap");
54
- console.log(" cortex update");
55
- console.log(" cortex status");
56
- console.log(" cortex doctor");
57
- console.log(" cortex ingest [--changed] [--verbose]");
58
- console.log(" cortex embed [--changed]");
59
- console.log(" cortex graph-load [--no-reset]");
60
- console.log(" cortex dashboard [--interval <sec>]");
61
- console.log(" cortex memory-compile [--dry-run] [--verbose]");
62
- console.log(" cortex memory-lint [--verbose] [--json]");
63
- console.log(" cortex help");
64
94
  }
65
95
 
66
96
  function readCliVersion() {
@@ -926,6 +956,30 @@ async function run() {
926
956
  return;
927
957
  }
928
958
 
959
+ if (command === "daemon") {
960
+ return runDaemonCommand(rest);
961
+ }
962
+
963
+ if (command === "hook") {
964
+ return runHookShim(rest);
965
+ }
966
+
967
+ if (command === "hooks") {
968
+ return runHooksCommand(rest);
969
+ }
970
+
971
+ if (command === "telemetry") {
972
+ return runTelemetryCommand(rest);
973
+ }
974
+
975
+ if (command === "enterprise") {
976
+ return runEnterpriseCommand(rest);
977
+ }
978
+
979
+ if (command === "run") {
980
+ return runRunCommand(rest);
981
+ }
982
+
929
983
  const passthrough = new Set([
930
984
  "bootstrap",
931
985
  "update",
@@ -949,6 +1003,598 @@ async function run() {
949
1003
  await runContextCommand(process.cwd(), [command, ...rest]);
950
1004
  }
951
1005
 
1006
+ // ---------------------------------------------------------------------------
1007
+ // v2.0.0: daemon + hooks commands
1008
+ // ---------------------------------------------------------------------------
1009
+
1010
+ const DAEMON_DIR = path.join(process.env.HOME || "", ".cortex");
1011
+ const PID_FILE = path.join(DAEMON_DIR, "daemon.pid");
1012
+
1013
+ function pidFileExists() {
1014
+ return fs.existsSync(PID_FILE);
1015
+ }
1016
+
1017
+ function readPid() {
1018
+ try {
1019
+ const raw = fs.readFileSync(PID_FILE, "utf8").trim();
1020
+ const pid = Number.parseInt(raw, 10);
1021
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
1022
+ } catch {
1023
+ return null;
1024
+ }
1025
+ }
1026
+
1027
+ function isPidAlive(pid) {
1028
+ if (!pid) return false;
1029
+ try {
1030
+ process.kill(pid, 0);
1031
+ return true;
1032
+ } catch (err) {
1033
+ if (err && typeof err === "object" && err.code === "EPERM") {
1034
+ return true;
1035
+ }
1036
+ return false;
1037
+ }
1038
+ }
1039
+
1040
+ function resolveProjectMcpDist() {
1041
+ // v2.0.0: daemon/hooks/cli are built into the project's mcp/dist/
1042
+ // (via 'cortex bootstrap'). PACKAGE_ROOT/scaffold/mcp/ is the source
1043
+ // tree the scaffold is copied from. The actual built code lives in
1044
+ // each project's <cwd>/mcp/dist/ after bootstrap.
1045
+ const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
1046
+ return path.join(target, "mcp", "dist");
1047
+ }
1048
+
1049
+ function resolveDaemonEntry() {
1050
+ return path.join(resolveProjectMcpDist(), "daemon", "main.js");
1051
+ }
1052
+
1053
+ function resolveHookEntry(name) {
1054
+ return path.join(resolveProjectMcpDist(), "hooks", `${name}.js`);
1055
+ }
1056
+
1057
+ function resolveCliEntry(name) {
1058
+ return path.join(resolveProjectMcpDist(), "cli", `${name}.js`);
1059
+ }
1060
+
1061
+ async function runDaemonCommand(args) {
1062
+ const sub = args[0] || "status";
1063
+ if (sub === "start") {
1064
+ if (isPidAlive(readPid())) {
1065
+ console.log("Daemon already running.");
1066
+ return;
1067
+ }
1068
+ fs.mkdirSync(DAEMON_DIR, { recursive: true });
1069
+ const entry = resolveDaemonEntry();
1070
+ if (!fs.existsSync(entry)) {
1071
+ throw new Error(`Daemon entry not found: ${entry}. Build cortex first.`);
1072
+ }
1073
+ const logFd = fs.openSync(path.join(DAEMON_DIR, "daemon.log"), "a");
1074
+ const child = spawn(process.execPath, [entry], {
1075
+ detached: true,
1076
+ stdio: ["ignore", logFd, logFd],
1077
+ });
1078
+ child.unref();
1079
+ console.log(`Daemon started (pid=${child.pid}). Log: ${path.join(DAEMON_DIR, "daemon.log")}`);
1080
+ return;
1081
+ }
1082
+ if (sub === "stop") {
1083
+ const pid = readPid();
1084
+ if (!isPidAlive(pid)) {
1085
+ console.log("Daemon not running.");
1086
+ return;
1087
+ }
1088
+ try {
1089
+ process.kill(pid, "SIGTERM");
1090
+ console.log(`Sent SIGTERM to pid ${pid}`);
1091
+ } catch (err) {
1092
+ throw new Error(`Failed to stop daemon: ${err instanceof Error ? err.message : String(err)}`);
1093
+ }
1094
+ return;
1095
+ }
1096
+ if (sub === "status") {
1097
+ const pid = readPid();
1098
+ if (isPidAlive(pid)) {
1099
+ console.log(`Daemon running (pid=${pid})`);
1100
+ } else {
1101
+ console.log("Daemon not running.");
1102
+ if (pidFileExists()) {
1103
+ console.log(`(stale pid file at ${PID_FILE})`);
1104
+ }
1105
+ }
1106
+ return;
1107
+ }
1108
+ throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|status`);
1109
+ }
1110
+
1111
+ async function runHookShim(args) {
1112
+ const name = args[0];
1113
+ if (!name) {
1114
+ throw new Error("Usage: cortex hook <name>");
1115
+ }
1116
+ const entry = resolveHookEntry(name);
1117
+ if (!fs.existsSync(entry)) {
1118
+ throw new Error(`Hook script not found: ${entry}`);
1119
+ }
1120
+ // Forward stdin → child, stdout/stderr → parent. Hook protocol = stdio.
1121
+ const child = spawn(process.execPath, [entry], { stdio: "inherit" });
1122
+ await new Promise((resolve) => {
1123
+ child.on("exit", (code) => {
1124
+ process.exit(code ?? 0);
1125
+ resolve(undefined);
1126
+ });
1127
+ });
1128
+ }
1129
+
1130
+ const HOOK_DEFS = [
1131
+ { event: "PreToolUse", matcher: "Edit|Write|Bash|MultiEdit", name: "pre-tool-use" },
1132
+ { event: "Stop", matcher: undefined, name: "stop" },
1133
+ { event: "SessionStart", matcher: undefined, name: "session-start" },
1134
+ { event: "SessionEnd", matcher: undefined, name: "session-end" },
1135
+ { event: "UserPromptSubmit", matcher: undefined, name: "user-prompt-submit" },
1136
+ { event: "PreCompact", matcher: undefined, name: "pre-compact" },
1137
+ ];
1138
+
1139
+ function managedClaudeSettingsPath() {
1140
+ if (process.platform === "darwin") {
1141
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
1142
+ }
1143
+ if (process.platform === "linux") {
1144
+ return "/etc/claude-code/managed-settings.json";
1145
+ }
1146
+ return null;
1147
+ }
1148
+
1149
+ function settingsPathFor(scope) {
1150
+ if (scope === "project") {
1151
+ return path.join(process.cwd(), ".claude", "settings.json");
1152
+ }
1153
+ let home = process.env.HOME || "";
1154
+ const isRoot = process.getuid && process.getuid() === 0;
1155
+ if (isRoot) {
1156
+ const sudoUidRaw = process.env.SUDO_UID;
1157
+ const sudoUid = sudoUidRaw ? parseInt(sudoUidRaw, 10) : NaN;
1158
+ if (Number.isFinite(sudoUid)) {
1159
+ try {
1160
+ home = os.userInfo({ uid: sudoUid }).homedir;
1161
+ } catch {
1162
+ // Fall back to HOME below.
1163
+ }
1164
+ }
1165
+ }
1166
+ return path.join(home, ".claude", "settings.json");
1167
+ }
1168
+
1169
+ function readJsonSafe(file) {
1170
+ if (!fs.existsSync(file)) return {};
1171
+ try {
1172
+ return JSON.parse(fs.readFileSync(file, "utf8"));
1173
+ } catch {
1174
+ return {};
1175
+ }
1176
+ }
1177
+
1178
+ function hookInstalledInSettings(settings, def) {
1179
+ const rows = settings.hooks?.[def.event] || [];
1180
+ return rows.some((row) => (row.hooks?.[0]?.command || "").startsWith(`cortex hook ${def.name}`));
1181
+ }
1182
+
1183
+ function readManagedClaudeSettings() {
1184
+ const file = managedClaudeSettingsPath();
1185
+ if (!file) return { file: null, settings: {} };
1186
+ return { file, settings: readJsonSafe(file) };
1187
+ }
1188
+
1189
+ function hasManagedClaudeHooks() {
1190
+ const { settings } = readManagedClaudeSettings();
1191
+ if (settings.allowManagedHooksOnly !== true) return false;
1192
+ return HOOK_DEFS.every((def) => hookInstalledInSettings(settings, def));
1193
+ }
1194
+
1195
+ function writeJson(file, data) {
1196
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1197
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8");
1198
+ }
1199
+
1200
+ // Enterprise == govern. One command, sudo-elevated, hard-fail without it.
1201
+ // `cortex enterprise <api-key>` does the full install. Subcommands status/sync/uninstall
1202
+ // dispatch to scaffold/mcp/dist/cli/govern.js.
1203
+
1204
+ function requireSudoElevation() {
1205
+ const isRoot = process.getuid && process.getuid() === 0;
1206
+ if (!isRoot) {
1207
+ process.stderr.write(bullet("fail", "This command requires admin privileges to install non-bypassable enforcement.", process.stderr) + "\n");
1208
+ process.stderr.write(muted(" Re-run as: sudo " + process.argv.slice(1).join(" "), process.stderr) + "\n");
1209
+ process.exit(1);
1210
+ }
1211
+ const sudoUser = process.env.SUDO_USER;
1212
+ const sudoUidRaw = process.env.SUDO_UID;
1213
+ const sudoGidRaw = process.env.SUDO_GID;
1214
+ if (!sudoUser || !sudoUidRaw || !sudoGidRaw) {
1215
+ process.stderr.write(bullet("fail", "Use 'sudo' to elevate (not 'su' or a root login).", process.stderr) + "\n");
1216
+ process.stderr.write(muted(" Cortex needs SUDO_USER/SUDO_UID/SUDO_GID set so that enterprise.yml,", process.stderr) + "\n");
1217
+ process.stderr.write(muted(" Claude Code hooks and the daemon end up owned by your user.", process.stderr) + "\n");
1218
+ process.exit(1);
1219
+ }
1220
+ const uid = parseInt(sudoUidRaw, 10);
1221
+ const gid = parseInt(sudoGidRaw, 10);
1222
+ if (!Number.isFinite(uid) || !Number.isFinite(gid)) {
1223
+ process.stderr.write(bullet("fail", "SUDO_UID/SUDO_GID are not valid integers — refusing to drop privileges.", process.stderr) + "\n");
1224
+ process.exit(1);
1225
+ }
1226
+ return { user: sudoUser, uid, gid };
1227
+ }
1228
+
1229
+ function dropPrivileges(sudo) {
1230
+ const sudoInfo = os.userInfo({ uid: sudo.uid });
1231
+ process.setgid(sudo.gid);
1232
+ process.setuid(sudo.uid);
1233
+ process.env.HOME = sudoInfo.homedir;
1234
+ process.env.USER = sudo.user;
1235
+ process.env.LOGNAME = sudo.user;
1236
+ return sudoInfo.homedir;
1237
+ }
1238
+
1239
+ function loadGovernModule() {
1240
+ const entry = resolveCliEntry("govern");
1241
+ if (!fs.existsSync(entry)) {
1242
+ throw new Error(
1243
+ `Build the project's MCP first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
1244
+ );
1245
+ }
1246
+ return import(pathToFileURL(entry).href);
1247
+ }
1248
+
1249
+ const ENTERPRISE_SUBCOMMANDS = new Set(["status", "sync", "uninstall", "repair", "help", "--help", "-h"]);
1250
+
1251
+ async function runEnterpriseCommand(args) {
1252
+ if (args.length === 0 || ENTERPRISE_SUBCOMMANDS.has(args[0])) {
1253
+ return runEnterpriseSubcommand(args);
1254
+ }
1255
+ return runEnterpriseInstall(args);
1256
+ }
1257
+
1258
+ async function runEnterpriseSubcommand(args) {
1259
+ const sub = args[0] ?? "help";
1260
+
1261
+ if (sub === "help" || sub === "--help" || sub === "-h" || !sub) {
1262
+ console.log(gradient("cortex enterprise") + muted(" · governance, armed."));
1263
+ console.log(helpRow("enterprise <api-key>", "Install (sudo). Managed enforcement + hooks + daemon."));
1264
+ console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
1265
+ console.log(helpRow("enterprise status [--verbose|--json]", "Show local enforcement state"));
1266
+ console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
1267
+ console.log(helpRow("enterprise uninstall", "Remove. [--break-glass --reason \"<text>\"] in enforced mode (sudo)"));
1268
+ console.log(helpRow("enterprise repair", "Verify managed paths, clear .cortex-tamper.lock (sudo)"));
1269
+ console.log("");
1270
+ console.log(muted("Default endpoint: https://cortex-web-rho.vercel.app"));
1271
+ return;
1272
+ }
1273
+
1274
+ if (sub === "status") {
1275
+ let verbose = false;
1276
+ let json = false;
1277
+ for (let i = 1; i < args.length; i++) {
1278
+ if (args[i] === "--verbose" || args[i] === "-v") verbose = true;
1279
+ else if (args[i] === "--json") json = true;
1280
+ else if (args[i].startsWith("-")) {
1281
+ throw new Error(`Unknown enterprise status option: ${args[i]}`);
1282
+ }
1283
+ }
1284
+ const mod = await loadGovernModule();
1285
+ mod.runGovernStatus({ cwd: process.cwd(), verbose, json });
1286
+ return;
1287
+ }
1288
+
1289
+ if (sub === "sync") {
1290
+ requireSudoElevation();
1291
+ const mod = await loadGovernModule();
1292
+ await mod.runGovernSync({ cwd: process.cwd() });
1293
+ return;
1294
+ }
1295
+
1296
+ if (sub === "uninstall") {
1297
+ let breakGlass = false;
1298
+ let reason;
1299
+ for (let i = 1; i < args.length; i++) {
1300
+ if (args[i] === "--break-glass") breakGlass = true;
1301
+ else if (args[i] === "--reason" && args[i + 1]) {
1302
+ reason = args[i + 1];
1303
+ i++;
1304
+ } else if (args[i].startsWith("-")) {
1305
+ throw new Error(`Unknown enterprise uninstall option: ${args[i]}`);
1306
+ }
1307
+ }
1308
+ requireSudoElevation();
1309
+ const mod = await loadGovernModule();
1310
+ const result = await mod.runGovernUninstall({
1311
+ cli: "all",
1312
+ breakGlass,
1313
+ reason,
1314
+ cwd: process.cwd(),
1315
+ });
1316
+ if (!result.ok) {
1317
+ printBullet("fail", result.message, process.stderr);
1318
+ process.exit(1);
1319
+ }
1320
+ printBullet("ok", result.message);
1321
+ return;
1322
+ }
1323
+
1324
+ if (sub === "repair") {
1325
+ let reason;
1326
+ for (let i = 1; i < args.length; i++) {
1327
+ if (args[i] === "--reason" && args[i + 1]) {
1328
+ reason = args[i + 1];
1329
+ i++;
1330
+ } else if (args[i].startsWith("-")) {
1331
+ throw new Error(`Unknown enterprise repair option: ${args[i]}`);
1332
+ }
1333
+ }
1334
+ requireSudoElevation();
1335
+ const mod = await loadGovernModule();
1336
+ const result = await mod.runGovernRepair({ cwd: process.cwd(), reason });
1337
+ if (!result.ok) {
1338
+ printBullet("fail", result.message, process.stderr);
1339
+ process.exit(1);
1340
+ }
1341
+ printBullet("ok", result.message);
1342
+ return;
1343
+ }
1344
+
1345
+ throw new Error(`Unknown enterprise subcommand: ${sub}`);
1346
+ }
1347
+
1348
+ async function runEnterpriseInstall(args) {
1349
+ const apiKey = args[0];
1350
+ let endpoint;
1351
+ let frameworks;
1352
+ let installHooks = true;
1353
+ let startDaemon = true;
1354
+ for (let i = 1; i < args.length; i++) {
1355
+ if (args[i] === "--endpoint" && args[i + 1]) {
1356
+ endpoint = args[i + 1];
1357
+ i++;
1358
+ } else if (args[i] === "--frameworks" && args[i + 1]) {
1359
+ frameworks = args[i + 1].split(",").map((s) => s.trim()).filter(Boolean);
1360
+ i++;
1361
+ } else if (args[i] === "--no-hooks") {
1362
+ installHooks = false;
1363
+ } else if (args[i] === "--no-daemon") {
1364
+ startDaemon = false;
1365
+ } else if (args[i].startsWith("-")) {
1366
+ throw new Error(`Unknown enterprise install option: ${args[i]}`);
1367
+ }
1368
+ }
1369
+
1370
+ const sudo = requireSudoElevation();
1371
+
1372
+ process.stdout.write(headerBanner({ tagline: " Cortex enterprise — activating governance" }));
1373
+
1374
+ const enterpriseEntry = resolveCliEntry("enterprise-setup");
1375
+ if (!fs.existsSync(enterpriseEntry)) {
1376
+ printBullet("fail", `Build the project's MCP first (missing ${enterpriseEntry}). Run 'cortex bootstrap' in the project root.`);
1377
+ process.exit(1);
1378
+ }
1379
+ const enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
1380
+
1381
+ // Step 1 — Initializing Cortex core (license validation + enterprise.yml).
1382
+ const step1 = spinner("Initializing Cortex core");
1383
+ const setupResult = await enterpriseMod.runEnterpriseSetup({ apiKey, endpoint, cwd: process.cwd() });
1384
+ if (!setupResult.ok) {
1385
+ step1.stop("fail", `Initializing Cortex core — ${setupResult.message}`);
1386
+ process.exit(1);
1387
+ }
1388
+ step1.stop("ok", `Initializing Cortex core — license ${setupResult.edition}, expires ${setupResult.expiresAt}`);
1389
+ printBullet("info", muted(`config: ${setupResult.configPath}`));
1390
+
1391
+ // enterprise.yml was just written as root; transfer ownership before we drop privs.
1392
+ try {
1393
+ fs.chownSync(setupResult.configPath, sudo.uid, sudo.gid);
1394
+ } catch (err) {
1395
+ printBullet("warn", `Could not chown ${setupResult.configPath}: ${err instanceof Error ? err.message : String(err)}`);
1396
+ }
1397
+
1398
+ // Step 2 — Loading policy engine (govern install: managed config + frameworks).
1399
+ const baseUrl = (endpoint ?? "https://cortex-web-rho.vercel.app").replace(/\/$/, "");
1400
+ const step2 = spinner("Loading policy engine");
1401
+ const governMod = await loadGovernModule();
1402
+ const governResult = await governMod.runGovernInstall({
1403
+ cli: "all",
1404
+ mode: "enforced",
1405
+ cwd: process.cwd(),
1406
+ apiKey,
1407
+ baseUrl,
1408
+ frameworks,
1409
+ });
1410
+ if (!governResult.ok) {
1411
+ step2.stop("fail", `Loading policy engine — ${governResult.message}`);
1412
+ process.exit(1);
1413
+ }
1414
+ step2.stop("ok", "Loading policy engine — policies armed");
1415
+
1416
+ // govern.local.json was written as root in cwd/.context. chown it back.
1417
+ const governStatePath = path.join(process.cwd(), ".context", "govern.local.json");
1418
+ if (fs.existsSync(governStatePath)) {
1419
+ try {
1420
+ fs.chownSync(governStatePath, sudo.uid, sudo.gid);
1421
+ } catch (err) {
1422
+ printBullet("warn", `Could not chown ${governStatePath}: ${err instanceof Error ? err.message : String(err)}`);
1423
+ }
1424
+ }
1425
+
1426
+ // Step 3 — Connecting audit pipeline (telemetry endpoint already wired by govern install).
1427
+ const step3 = spinner("Connecting audit pipeline");
1428
+ step3.stop("ok", `Connecting audit pipeline — endpoint ${baseUrl}`);
1429
+
1430
+ // Drop privileges before user-scope writes (Claude Code hooks in $HOME) and daemon spawn.
1431
+ dropPrivileges(sudo);
1432
+
1433
+ // Step 4 — Preparing MCP gateway (Claude Code hooks bind the MCP surface).
1434
+ if (installHooks) {
1435
+ const step4 = spinner("Preparing MCP gateway");
1436
+ try {
1437
+ if (hasManagedClaudeHooks()) {
1438
+ step4.stop("ok", "Preparing MCP gateway — managed Claude hooks active");
1439
+ } else {
1440
+ await runHooksCommand(["install"]);
1441
+ step4.stop("ok", "Preparing MCP gateway — hooks installed");
1442
+ }
1443
+ } catch (err) {
1444
+ step4.stop("fail", `Preparing MCP gateway — ${err instanceof Error ? err.message : String(err)}`);
1445
+ }
1446
+ } else {
1447
+ printBullet("warn", "Preparing MCP gateway — skipped (--no-hooks)");
1448
+ }
1449
+
1450
+ // Step 5 — Installing guardrails (supervisor daemon).
1451
+ if (startDaemon) {
1452
+ const step5 = spinner("Installing guardrails");
1453
+ try {
1454
+ await runDaemonCommand(["start"]);
1455
+ step5.stop("ok", "Installing guardrails — daemon online");
1456
+ } catch (err) {
1457
+ step5.stop("fail", `Installing guardrails — ${err instanceof Error ? err.message : String(err)}`);
1458
+ }
1459
+ } else {
1460
+ printBullet("warn", "Installing guardrails — skipped (--no-daemon)");
1461
+ }
1462
+
1463
+ console.log("");
1464
+ console.log(bullet("ok", bold("Cortex is running.")));
1465
+ console.log(muted(" Monitoring AI activity. No violations detected."));
1466
+ console.log(muted(" Next: ") + accent("cortex enterprise status") + muted(" · ") + accent("cortex telemetry test"));
1467
+ console.log("");
1468
+ }
1469
+
1470
+ const RUN_CLIS = new Set(["claude", "codex", "copilot"]);
1471
+
1472
+ async function runRunCommand(args) {
1473
+ const sub = args[0];
1474
+ if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
1475
+ console.log("Usage:");
1476
+ console.log(" cortex run <claude|codex|copilot> [args...]");
1477
+ console.log("");
1478
+ console.log("Wraps the named AI CLI in cortex enforcement:");
1479
+ console.log(" claude/codex: passthrough — their own managed-config + sandbox");
1480
+ console.log(" cover Tier 1 enforcement after 'cortex enterprise <key>'.");
1481
+ console.log(" copilot: Tier 2 — OS-level sandbox (sandbox-exec on macOS,");
1482
+ console.log(" bwrap on Linux). Denies writes to ~/.copilot/,");
1483
+ console.log(" ~/.copilot.local/, /etc/copilot* so AI cannot");
1484
+ console.log(" reconfigure itself out of governance.");
1485
+ console.log("");
1486
+ console.log("Tip: alias copilot='cortex run copilot' so direct 'copilot' invocations");
1487
+ console.log("are also wrapped. Direct invocations are otherwise caught by Tier 3");
1488
+ console.log("ungoverned-session detection (Phase 5).");
1489
+ return;
1490
+ }
1491
+ if (!RUN_CLIS.has(sub)) {
1492
+ throw new Error(`Unknown AI CLI: ${sub}. Use claude, codex, or copilot.`);
1493
+ }
1494
+ const entry = resolveCliEntry("run");
1495
+ if (!fs.existsSync(entry)) {
1496
+ throw new Error(
1497
+ `Build the project's MCP first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
1498
+ );
1499
+ }
1500
+ const mod = await import(pathToFileURL(entry).href);
1501
+ const exitCode = await mod.runAiCli({ cli: sub, args: args.slice(1) });
1502
+ process.exit(exitCode);
1503
+ }
1504
+
1505
+ async function runTelemetryCommand(args) {
1506
+ const sub = args[0] || "help";
1507
+ if (sub === "test") {
1508
+ const entry = resolveCliEntry("telemetry-test");
1509
+ if (!fs.existsSync(entry)) {
1510
+ throw new Error(`Build the project's MCP first (missing ${entry}). Run 'cortex bootstrap' in the project root.`);
1511
+ }
1512
+ const mod = await import(pathToFileURL(entry).href);
1513
+ const code = await mod.runTelemetryTest();
1514
+ process.exit(code);
1515
+ }
1516
+ if (sub === "help" || sub === "--help" || sub === "-h") {
1517
+ console.log("Usage:");
1518
+ console.log(" cortex telemetry test Smoke-test the push pipeline end-to-end");
1519
+ return;
1520
+ }
1521
+ throw new Error(`Unknown telemetry subcommand: ${sub}`);
1522
+ }
1523
+
1524
+ async function runHooksCommand(args) {
1525
+ const sub = args[0] || "status";
1526
+ const scope = args.includes("--project") ? "project" : "user";
1527
+ const target = settingsPathFor(scope);
1528
+
1529
+ if (sub === "install") {
1530
+ const settings = readJsonSafe(target);
1531
+ settings.hooks = settings.hooks || {};
1532
+ for (const def of HOOK_DEFS) {
1533
+ const entry = {
1534
+ ...(def.matcher ? { matcher: def.matcher } : {}),
1535
+ hooks: [{ type: "command", command: `cortex hook ${def.name}` }],
1536
+ };
1537
+ const existing = settings.hooks[def.event] || [];
1538
+ const filtered = existing.filter((row) => {
1539
+ const cmd = (row.hooks?.[0]?.command || "");
1540
+ return !cmd.startsWith("cortex hook ");
1541
+ });
1542
+ settings.hooks[def.event] = [...filtered, entry];
1543
+ }
1544
+ writeJson(target, settings);
1545
+ console.log(`Installed cortex hooks into ${target}`);
1546
+ console.log(`Hooks: ${HOOK_DEFS.map((d) => d.name).join(", ")}`);
1547
+ return;
1548
+ }
1549
+ if (sub === "uninstall") {
1550
+ const settings = readJsonSafe(target);
1551
+ if (settings.hooks) {
1552
+ for (const event of Object.keys(settings.hooks)) {
1553
+ settings.hooks[event] = (settings.hooks[event] || []).filter((row) => {
1554
+ const cmd = (row.hooks?.[0]?.command || "");
1555
+ return !cmd.startsWith("cortex hook ");
1556
+ });
1557
+ if (settings.hooks[event].length === 0) {
1558
+ delete settings.hooks[event];
1559
+ }
1560
+ }
1561
+ if (Object.keys(settings.hooks).length === 0) {
1562
+ delete settings.hooks;
1563
+ }
1564
+ }
1565
+ writeJson(target, settings);
1566
+ console.log(`Removed cortex hooks from ${target}`);
1567
+ return;
1568
+ }
1569
+ if (sub === "status") {
1570
+ const settings = readJsonSafe(target);
1571
+ const managed = scope === "user" ? readManagedClaudeSettings() : { file: null, settings: {} };
1572
+ const installed = [];
1573
+ for (const def of HOOK_DEFS) {
1574
+ const userFound = hookInstalledInSettings(settings, def);
1575
+ const managedFound = scope === "user" ? hookInstalledInSettings(managed.settings, def) : false;
1576
+ const found = userFound || managedFound;
1577
+ let source = "";
1578
+ if (userFound && managedFound) source = "user+managed";
1579
+ else if (userFound) source = "user";
1580
+ else if (managedFound) source = "managed";
1581
+ installed.push({ name: def.name, event: def.event, found, source });
1582
+ }
1583
+ console.log(`Settings file: ${target}`);
1584
+ if (scope === "user" && managed.file) {
1585
+ console.log(`Managed settings: ${managed.file}`);
1586
+ }
1587
+ for (const row of installed) {
1588
+ console.log(` ${row.found ? "✓" : "✗"} ${row.event} → ${row.name}${row.source ? ` (${row.source})` : ""}`);
1589
+ }
1590
+ if (scope === "user" && managed.settings.allowManagedHooksOnly === true) {
1591
+ console.log(" note: managed Claude hooks are authoritative; user hooks may be intentionally absent");
1592
+ }
1593
+ return;
1594
+ }
1595
+ throw new Error(`Unknown hooks subcommand: ${sub}. Try install|uninstall|status`);
1596
+ }
1597
+
952
1598
  function resolveArgv1() {
953
1599
  if (!process.argv[1]) return null;
954
1600
  try {
@@ -963,7 +1609,8 @@ const invokedAsScript =
963
1609
 
964
1610
  if (invokedAsScript) {
965
1611
  run().catch((error) => {
966
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1612
+ const message = error instanceof Error ? error.message : String(error);
1613
+ process.stderr.write(bullet("fail", message, process.stderr) + "\n");
967
1614
  process.exit(1);
968
1615
  });
969
1616
  }