@danielblomma/cortex-mcp 2.4.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +5 -0
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +6 -1834
  23. package/mcp-registry-submission.json +1 -1
  24. package/package.json +3 -2
  25. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  26. package/scaffold/ownership/current.json +4 -0
  27. package/scaffold/ownership/v1.json +456 -0
  28. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  29. package/scaffold/scripts/ingest-worker.mjs +4 -1
  30. package/scaffold/scripts/ingest.mjs +5 -3899
  31. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  32. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  33. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  34. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  35. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  36. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  37. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  38. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  39. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  40. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  41. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  42. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  43. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  44. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  45. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,199 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { resolveHookEntry } from "./project-runtime.mjs";
6
+
7
+ const HOOK_DEFS = [
8
+ {
9
+ event: "PreToolUse",
10
+ matcher: "Edit|Write|Bash|MultiEdit",
11
+ name: "pre-tool-use",
12
+ },
13
+ { event: "Stop", matcher: undefined, name: "stop" },
14
+ { event: "SessionStart", matcher: undefined, name: "session-start" },
15
+ { event: "SessionEnd", matcher: undefined, name: "session-end" },
16
+ {
17
+ event: "UserPromptSubmit",
18
+ matcher: undefined,
19
+ name: "user-prompt-submit",
20
+ },
21
+ { event: "PreCompact", matcher: undefined, name: "pre-compact" },
22
+ ];
23
+
24
+ export async function runHookShim(args) {
25
+ const name = args[0];
26
+ if (!name) {
27
+ throw new Error("Usage: cortex hook <name>");
28
+ }
29
+ const entry = resolveHookEntry(name);
30
+ if (!fs.existsSync(entry)) {
31
+ throw new Error(`Hook script not found: ${entry}`);
32
+ }
33
+ const child = spawn(process.execPath, [entry], { stdio: "inherit" });
34
+ await new Promise((resolve) => {
35
+ child.on("exit", (code) => {
36
+ process.exit(code ?? 0);
37
+ resolve(undefined);
38
+ });
39
+ });
40
+ }
41
+
42
+ function managedClaudeSettingsPath() {
43
+ if (process.platform === "darwin") {
44
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
45
+ }
46
+ if (process.platform === "linux") {
47
+ return "/etc/claude-code/managed-settings.json";
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function settingsPathFor(scope) {
53
+ if (scope === "project") {
54
+ return path.join(process.cwd(), ".claude", "settings.json");
55
+ }
56
+ let home = process.env.HOME || "";
57
+ const isRoot = process.getuid && process.getuid() === 0;
58
+ if (isRoot) {
59
+ const sudoUidRaw = process.env.SUDO_UID;
60
+ const sudoUid = sudoUidRaw ? parseInt(sudoUidRaw, 10) : NaN;
61
+ if (Number.isFinite(sudoUid)) {
62
+ try {
63
+ home = os.userInfo({ uid: sudoUid }).homedir;
64
+ } catch {
65
+ // Fall back to HOME below.
66
+ }
67
+ }
68
+ }
69
+ return path.join(home, ".claude", "settings.json");
70
+ }
71
+
72
+ function readJsonSafe(file) {
73
+ if (!fs.existsSync(file)) return {};
74
+ try {
75
+ return JSON.parse(fs.readFileSync(file, "utf8"));
76
+ } catch {
77
+ return {};
78
+ }
79
+ }
80
+
81
+ function hookInstalledInSettings(settings, def) {
82
+ const rows = settings.hooks?.[def.event] || [];
83
+ return rows.some((row) =>
84
+ (row.hooks?.[0]?.command || "").startsWith(`cortex hook ${def.name}`),
85
+ );
86
+ }
87
+
88
+ function readManagedClaudeSettings() {
89
+ const file = managedClaudeSettingsPath();
90
+ if (!file) return { file: null, settings: {} };
91
+ return { file, settings: readJsonSafe(file) };
92
+ }
93
+
94
+ export function hasManagedClaudeHooks() {
95
+ const { settings } = readManagedClaudeSettings();
96
+ if (settings.allowManagedHooksOnly !== true) return false;
97
+ return HOOK_DEFS.every((def) => hookInstalledInSettings(settings, def));
98
+ }
99
+
100
+ function writeJson(file, data) {
101
+ fs.mkdirSync(path.dirname(file), { recursive: true });
102
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8");
103
+ }
104
+
105
+ export async function runHooksCommand(args) {
106
+ const sub = args[0] || "status";
107
+ const scope = args.includes("--project") ? "project" : "user";
108
+ const target = settingsPathFor(scope);
109
+
110
+ if (sub === "install") {
111
+ const settings = readJsonSafe(target);
112
+ settings.hooks = settings.hooks || {};
113
+ for (const def of HOOK_DEFS) {
114
+ const entry = {
115
+ ...(def.matcher ? { matcher: def.matcher } : {}),
116
+ hooks: [{ type: "command", command: `cortex hook ${def.name}` }],
117
+ };
118
+ const existing = settings.hooks[def.event] || [];
119
+ const filtered = existing.filter((row) => {
120
+ const cmd = row.hooks?.[0]?.command || "";
121
+ return !cmd.startsWith("cortex hook ");
122
+ });
123
+ settings.hooks[def.event] = [...filtered, entry];
124
+ }
125
+ writeJson(target, settings);
126
+ console.log(`Installed cortex hooks into ${target}`);
127
+ console.log(`Hooks: ${HOOK_DEFS.map((def) => def.name).join(", ")}`);
128
+ return;
129
+ }
130
+ if (sub === "uninstall") {
131
+ const settings = readJsonSafe(target);
132
+ if (settings.hooks) {
133
+ for (const event of Object.keys(settings.hooks)) {
134
+ settings.hooks[event] = (settings.hooks[event] || []).filter((row) => {
135
+ const cmd = row.hooks?.[0]?.command || "";
136
+ return !cmd.startsWith("cortex hook ");
137
+ });
138
+ if (settings.hooks[event].length === 0) {
139
+ delete settings.hooks[event];
140
+ }
141
+ }
142
+ if (Object.keys(settings.hooks).length === 0) {
143
+ delete settings.hooks;
144
+ }
145
+ }
146
+ writeJson(target, settings);
147
+ console.log(`Removed cortex hooks from ${target}`);
148
+ return;
149
+ }
150
+ if (sub === "status") {
151
+ const settings = readJsonSafe(target);
152
+ const managed =
153
+ scope === "user"
154
+ ? readManagedClaudeSettings()
155
+ : { file: null, settings: {} };
156
+ const installed = [];
157
+ for (const def of HOOK_DEFS) {
158
+ const userFound = hookInstalledInSettings(settings, def);
159
+ const managedFound =
160
+ scope === "user"
161
+ ? hookInstalledInSettings(managed.settings, def)
162
+ : false;
163
+ const found = userFound || managedFound;
164
+ let source = "";
165
+ if (userFound && managedFound) source = "user+managed";
166
+ else if (userFound) source = "user";
167
+ else if (managedFound) source = "managed";
168
+ installed.push({
169
+ name: def.name,
170
+ event: def.event,
171
+ found,
172
+ source,
173
+ });
174
+ }
175
+ console.log(`Settings file: ${target}`);
176
+ if (scope === "user" && managed.file) {
177
+ console.log(`Managed settings: ${managed.file}`);
178
+ }
179
+ for (const row of installed) {
180
+ console.log(
181
+ ` ${row.found ? "✓" : "✗"} ${row.event} → ${row.name}${
182
+ row.source ? ` (${row.source})` : ""
183
+ }`,
184
+ );
185
+ }
186
+ if (
187
+ scope === "user" &&
188
+ managed.settings.allowManagedHooksOnly === true
189
+ ) {
190
+ console.log(
191
+ " note: managed Claude hooks are authoritative; user hooks may be intentionally absent",
192
+ );
193
+ }
194
+ return;
195
+ }
196
+ throw new Error(
197
+ `Unknown hooks subcommand: ${sub}. Try install|uninstall|status`,
198
+ );
199
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizeProjectRoot } from "../wsl.mjs";
4
+ import {
5
+ maybeMigrateScaffold,
6
+ runContextCommand,
7
+ } from "./context-passthrough.mjs";
8
+ import { MCP_PROJECT_REL } from "./paths.mjs";
9
+ import { runCommand } from "./process.mjs";
10
+ import {
11
+ canAutoInitialize,
12
+ ensureProjectInitialized,
13
+ ensureScaffoldExists,
14
+ initializeScaffold,
15
+ installAssistantHelpers,
16
+ isScaffoldOutOfDate,
17
+ isTruthyEnv,
18
+ maybeInstallGitHooks,
19
+ } from "./scaffold.mjs";
20
+
21
+ async function ensureProjectInitializedForMcp(targetDir) {
22
+ const mcpPackageJson = path.join(
23
+ targetDir,
24
+ MCP_PROJECT_REL,
25
+ "package.json",
26
+ );
27
+ const serverEntry = path.join(
28
+ targetDir,
29
+ MCP_PROJECT_REL,
30
+ "dist",
31
+ "server.js",
32
+ );
33
+
34
+ if (fs.existsSync(mcpPackageJson) && fs.existsSync(serverEntry)) return;
35
+
36
+ if (isScaffoldOutOfDate(targetDir)) {
37
+ await maybeMigrateScaffold(targetDir, "mcp");
38
+ if (fs.existsSync(mcpPackageJson) && fs.existsSync(serverEntry)) return;
39
+ }
40
+
41
+ if (!isTruthyEnv(process.env.CORTEX_AUTO_BOOTSTRAP_ON_MCP)) {
42
+ ensureProjectInitialized(targetDir);
43
+ return;
44
+ }
45
+
46
+ if (!fs.existsSync(mcpPackageJson)) {
47
+ if (!canAutoInitialize(targetDir)) {
48
+ throw new Error(
49
+ `Cannot auto-initialize Cortex in ${targetDir}: scaffold paths already exist. Run 'cortex init --bootstrap' manually.`,
50
+ );
51
+ }
52
+ ensureScaffoldExists();
53
+ fs.mkdirSync(targetDir, { recursive: true });
54
+ initializeScaffold(targetDir, false);
55
+ installAssistantHelpers(targetDir);
56
+ await maybeInstallGitHooks(targetDir);
57
+ console.log(`[cortex] auto-init completed in ${targetDir}`);
58
+ }
59
+
60
+ if (!fs.existsSync(serverEntry)) {
61
+ console.log("[cortex] auto-bootstrap: running initial bootstrap for MCP");
62
+ await runContextCommand(targetDir, ["bootstrap"]);
63
+ }
64
+ }
65
+
66
+ export async function runMcpCommand() {
67
+ const rawTarget = process.env.CORTEX_PROJECT_ROOT || process.cwd();
68
+ const target = path.resolve(normalizeProjectRoot(rawTarget));
69
+ process.env.CORTEX_PROJECT_ROOT = target;
70
+ await ensureProjectInitializedForMcp(target);
71
+ ensureProjectInitialized(target);
72
+ const serverEntry = path.join(
73
+ target,
74
+ MCP_PROJECT_REL,
75
+ "dist",
76
+ "server.js",
77
+ );
78
+ if (!fs.existsSync(serverEntry)) {
79
+ throw new Error(
80
+ `Missing ${serverEntry}. Run 'cortex bootstrap' in ${target} first.`,
81
+ );
82
+ }
83
+ process.stderr.write(
84
+ `[cortex] starting MCP stdio server from ${serverEntry}\n`,
85
+ );
86
+ await runCommand("node", [serverEntry], target);
87
+ }
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ export const PACKAGE_ROOT = path.resolve(CLI_DIR, "../..");
7
+ export const SCAFFOLD_ROOT = path.join(PACKAGE_ROOT, "scaffold");
8
+ export const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, "package.json");
9
+
10
+ // The project runtime keeps the .context/mcp compatibility name even though
11
+ // it now serves the full local context surface.
12
+ export const MCP_PROJECT_REL = path.join(".context", "mcp");
13
+ export const CONTEXT_RUNTIME_REL = MCP_PROJECT_REL;
14
+ export const CONTEXT_SCRIPTS_REL = path.join(".context", "scripts");
@@ -0,0 +1,49 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function runCommand(command, args, cwd) {
4
+ return new Promise((resolve, reject) => {
5
+ const child = spawn(command, args, {
6
+ cwd,
7
+ stdio: "inherit",
8
+ env: process.env,
9
+ });
10
+
11
+ child.on("error", reject);
12
+ child.on("exit", (code) => {
13
+ if (code === 0) {
14
+ resolve();
15
+ } else {
16
+ reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
17
+ }
18
+ });
19
+ });
20
+ }
21
+
22
+ export function runCommandResult(command, args, cwd, stdio = "ignore") {
23
+ return new Promise((resolve) => {
24
+ let done = false;
25
+ const finish = (result) => {
26
+ if (done) return;
27
+ done = true;
28
+ resolve(result);
29
+ };
30
+
31
+ const child = spawn(command, args, {
32
+ cwd,
33
+ stdio,
34
+ env: process.env,
35
+ });
36
+
37
+ child.on("error", (error) => finish({ ok: false, code: null, error }));
38
+ child.on("exit", (code) => finish({ ok: code === 0, code, error: null }));
39
+ });
40
+ }
41
+
42
+ export function toErrorMessage(error) {
43
+ return error instanceof Error ? error.message : String(error);
44
+ }
45
+
46
+ export async function commandExists(command, cwd) {
47
+ const result = await runCommandResult(command, ["--version"], cwd, "ignore");
48
+ return result.ok;
49
+ }
@@ -0,0 +1,237 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parseConnectArgs, parseInitArgs } from "./arguments.mjs";
4
+ import { runContextCommand } from "./context-passthrough.mjs";
5
+ import { restartDaemonAfterRuntimeUpgrade } from "./daemon.mjs";
6
+ import { printBanner } from "./help.mjs";
7
+ import { MCP_PROJECT_REL } from "./paths.mjs";
8
+ import {
9
+ commandExists,
10
+ runCommand,
11
+ runCommandResult,
12
+ toErrorMessage,
13
+ } from "./process.mjs";
14
+ import {
15
+ ensureProjectInitialized,
16
+ ensureScaffoldExists,
17
+ hardenEnterpriseConfigPermissions,
18
+ initializeScaffold,
19
+ installAssistantHelpers,
20
+ maybeInstallGitHooks,
21
+ } from "./scaffold.mjs";
22
+
23
+ function normalizeName(value) {
24
+ const cleaned = value
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, "-")
27
+ .replace(/^-+|-+$/g, "");
28
+ return cleaned || "repo";
29
+ }
30
+
31
+ async function connectCodex(targetDir, serverEntry) {
32
+ if (!(await commandExists("codex", targetDir))) {
33
+ console.log(
34
+ "[cortex] codex CLI not found, skipping Codex MCP registration",
35
+ );
36
+ return false;
37
+ }
38
+
39
+ const repoName = normalizeName(path.basename(targetDir));
40
+ const serverName = `cortex-${repoName}`;
41
+ await runCommandResult(
42
+ "codex",
43
+ ["mcp", "remove", serverName],
44
+ targetDir,
45
+ "ignore",
46
+ );
47
+ await runCommand(
48
+ "codex",
49
+ ["mcp", "add", serverName, "--", "node", serverEntry],
50
+ targetDir,
51
+ );
52
+ console.log(`[cortex] connected Codex MCP server: ${serverName}`);
53
+ return true;
54
+ }
55
+
56
+ async function connectClaude(targetDir) {
57
+ if (!(await commandExists("claude", targetDir))) {
58
+ console.log(
59
+ "[cortex] claude CLI not found, skipping Claude Code MCP registration",
60
+ );
61
+ return false;
62
+ }
63
+
64
+ const serverName = "cortex";
65
+ const projectServerEntry = path.join(
66
+ MCP_PROJECT_REL,
67
+ "dist",
68
+ "server.js",
69
+ );
70
+ await runCommandResult(
71
+ "claude",
72
+ ["mcp", "remove", "-s", "project", serverName],
73
+ targetDir,
74
+ "ignore",
75
+ );
76
+ await runCommand(
77
+ "claude",
78
+ [
79
+ "mcp",
80
+ "add",
81
+ "-s",
82
+ "project",
83
+ serverName,
84
+ "--",
85
+ "node",
86
+ projectServerEntry,
87
+ ],
88
+ targetDir,
89
+ );
90
+ console.log(
91
+ "[cortex] connected Claude Code MCP server: cortex (project scope)",
92
+ );
93
+ return true;
94
+ }
95
+
96
+ async function connectMcpClients(targetDir, options = {}) {
97
+ const { skipBuild = false } = options;
98
+ const mcpDir = path.join(targetDir, MCP_PROJECT_REL);
99
+ const packageJson = path.join(mcpDir, "package.json");
100
+ const nodeModules = path.join(mcpDir, "node_modules");
101
+ const serverEntry = path.join(mcpDir, "dist", "server.js");
102
+
103
+ if (!fs.existsSync(packageJson)) {
104
+ throw new Error(`Missing ${packageJson}. Run 'cortex init' first.`);
105
+ }
106
+
107
+ if (!skipBuild && fs.existsSync(nodeModules)) {
108
+ try {
109
+ await runCommand(
110
+ "npm",
111
+ ["--prefix", mcpDir, "run", "build", "--silent"],
112
+ targetDir,
113
+ );
114
+ } catch (error) {
115
+ console.log(
116
+ `[cortex] MCP build failed, continuing with existing dist output: ${toErrorMessage(error)}`,
117
+ );
118
+ }
119
+ } else if (!skipBuild) {
120
+ console.log(
121
+ "[cortex] .context/mcp/node_modules not found, skipping build (run cortex bootstrap first)",
122
+ );
123
+ }
124
+
125
+ if (!fs.existsSync(serverEntry)) {
126
+ console.log(
127
+ `[cortex] warning: ${serverEntry} not found yet; run cortex bootstrap before first MCP call`,
128
+ );
129
+ }
130
+
131
+ let connected = 0;
132
+
133
+ try {
134
+ if (await connectCodex(targetDir, serverEntry)) connected += 1;
135
+ } catch (error) {
136
+ console.log(
137
+ `[cortex] failed to connect Codex MCP: ${toErrorMessage(error)}`,
138
+ );
139
+ }
140
+
141
+ try {
142
+ if (await connectClaude(targetDir)) connected += 1;
143
+ } catch (error) {
144
+ console.log(
145
+ `[cortex] failed to connect Claude MCP: ${toErrorMessage(error)}`,
146
+ );
147
+ }
148
+
149
+ if (connected === 0) {
150
+ console.log("[cortex] no MCP clients connected");
151
+ }
152
+
153
+ return connected;
154
+ }
155
+
156
+ export async function runInitCommand(args) {
157
+ ensureScaffoldExists();
158
+ const { target, force, bootstrap, connect, watch } = parseInitArgs(args);
159
+ printBanner("Cortex initializes repo-scoped context for AI coding agents.");
160
+ fs.mkdirSync(target, { recursive: true });
161
+ initializeScaffold(target, force);
162
+ hardenEnterpriseConfigPermissions(target);
163
+ const helpers = installAssistantHelpers(target);
164
+ await maybeInstallGitHooks(target);
165
+
166
+ console.log(`[cortex] initialized in ${target}`);
167
+ console.log(
168
+ "[cortex] scaffold copied: .context/, .context/scripts/, context runtime (.context/mcp compatibility path), .githooks/, docs/",
169
+ );
170
+ console.log(
171
+ `[cortex] Claude commands ready: /context-update (${helpers.claude.total} files)`,
172
+ );
173
+ if (helpers.codex.changed) {
174
+ console.log("[cortex] Codex workflow instructions added to AGENTS.md");
175
+ } else {
176
+ console.log(
177
+ "[cortex] Codex workflow instructions already up to date in AGENTS.md",
178
+ );
179
+ }
180
+
181
+ if (bootstrap) {
182
+ console.log(
183
+ "[cortex] bootstrap: install deps -> ingest -> embeddings -> graph",
184
+ );
185
+ } else {
186
+ console.log("[cortex] next: cortex bootstrap");
187
+ }
188
+
189
+ if (connect) {
190
+ console.log(
191
+ "[cortex] MCP connect: Codex + Claude Code (if CLIs are installed)",
192
+ );
193
+ } else {
194
+ console.log(
195
+ "[cortex] MCP connect skipped (run 'cortex connect' or init with --connect)",
196
+ );
197
+ }
198
+
199
+ if (watch) {
200
+ if (bootstrap) {
201
+ console.log("[cortex] background sync: cortex watch start");
202
+ } else {
203
+ console.log(
204
+ "[cortex] background sync pending: run cortex watch start after bootstrap",
205
+ );
206
+ }
207
+ } else {
208
+ console.log("[cortex] background sync skipped (--no-watch)");
209
+ }
210
+
211
+ if (!bootstrap) console.log("");
212
+
213
+ if (bootstrap) {
214
+ await runContextCommand(target, ["bootstrap"]);
215
+ await restartDaemonAfterRuntimeUpgrade(target);
216
+ }
217
+
218
+ if (connect) {
219
+ await connectMcpClients(target);
220
+ }
221
+
222
+ if (watch && bootstrap) {
223
+ await runContextCommand(target, ["watch", "start"]);
224
+ }
225
+ }
226
+
227
+ export async function runConnectCommand(args) {
228
+ const { target, skipBuild } = parseConnectArgs(args);
229
+ ensureProjectInitialized(target);
230
+ const helpers = installAssistantHelpers(target);
231
+ if (helpers.claude.changed > 0 || helpers.codex.changed) {
232
+ console.log(
233
+ "[cortex] assistant helpers updated (.claude/commands + AGENTS.md)",
234
+ );
235
+ }
236
+ await connectMcpClients(target, { skipBuild });
237
+ }
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { CONTEXT_RUNTIME_REL } from "./paths.mjs";
5
+
6
+ export function resolveProjectRuntimeDist(projectRoot) {
7
+ const target =
8
+ projectRoot ||
9
+ process.env.CORTEX_PROJECT_ROOT?.trim() ||
10
+ process.cwd();
11
+ return path.join(target, CONTEXT_RUNTIME_REL, "dist");
12
+ }
13
+
14
+ export function resolveDaemonEntry(projectRoot) {
15
+ return path.join(resolveProjectRuntimeDist(projectRoot), "daemon", "main.js");
16
+ }
17
+
18
+ export function resolveHookEntry(name) {
19
+ return path.join(resolveProjectRuntimeDist(), "hooks", `${name}.js`);
20
+ }
21
+
22
+ export function resolveCliEntry(name) {
23
+ return path.join(resolveProjectRuntimeDist(), "cli", `${name}.js`);
24
+ }
25
+
26
+ export async function loadProjectCliModule(name) {
27
+ const entry = resolveCliEntry(name);
28
+ if (!fs.existsSync(entry)) {
29
+ throw new Error(
30
+ `Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`,
31
+ );
32
+ }
33
+ return import(pathToFileURL(entry).href);
34
+ }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { loadProjectCliModule } from "./project-runtime.mjs";
3
+
4
+ export const QUERY_COMMANDS = new Set([
5
+ "search",
6
+ "related",
7
+ "impact",
8
+ "rules",
9
+ "explain",
10
+ "pattern-evidence",
11
+ ]);
12
+
13
+ export async function runQueryCommandShim(command, args) {
14
+ const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
15
+ process.env.CORTEX_PROJECT_ROOT = path.resolve(target);
16
+ const mod = await loadProjectCliModule("query");
17
+ await mod.runQueryCommand([command, ...args]);
18
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ PASSTHROUGH_COMMANDS,
3
+ runPassthroughCommand,
4
+ } from "./context-passthrough.mjs";
5
+ import { runDaemonCommand } from "./daemon.mjs";
6
+ import { runEnterpriseCommand } from "./enterprise.mjs";
7
+ import { printHelp, readCliVersion } from "./help.mjs";
8
+ import { runHookShim, runHooksCommand } from "./hooks.mjs";
9
+ import { runMcpCommand } from "./mcp-command.mjs";
10
+ import {
11
+ runConnectCommand,
12
+ runInitCommand,
13
+ } from "./project-commands.mjs";
14
+ import {
15
+ QUERY_COMMANDS,
16
+ runQueryCommandShim,
17
+ } from "./query-command.mjs";
18
+ import { runRunCommand } from "./run-command.mjs";
19
+ import { runStageCommandShim } from "./stage-command.mjs";
20
+ import { runTelemetryCommand } from "./telemetry-command.mjs";
21
+
22
+ const COMMAND_HANDLERS = new Map([
23
+ ["init", runInitCommand],
24
+ ["connect", runConnectCommand],
25
+ ["mcp", runMcpCommand],
26
+ ["daemon", runDaemonCommand],
27
+ ["hook", runHookShim],
28
+ ["hooks", runHooksCommand],
29
+ ["telemetry", runTelemetryCommand],
30
+ ["enterprise", runEnterpriseCommand],
31
+ ["run", runRunCommand],
32
+ ["stage", runStageCommandShim],
33
+ ]);
34
+
35
+ export async function runCli(argv = process.argv.slice(2)) {
36
+ const cliVersion = readCliVersion();
37
+ process.env.CORTEX_CLI_VERSION = cliVersion;
38
+
39
+ const [rawCommand, ...rest] = argv;
40
+ const command = rawCommand ?? "help";
41
+
42
+ if (command === "version" || command === "--version" || command === "-V") {
43
+ console.log(cliVersion);
44
+ return;
45
+ }
46
+
47
+ if (command === "help" || command === "--help" || command === "-h") {
48
+ printHelp();
49
+ return;
50
+ }
51
+
52
+ const handler = COMMAND_HANDLERS.get(command);
53
+ if (handler) {
54
+ return handler(rest);
55
+ }
56
+
57
+ if (QUERY_COMMANDS.has(command)) {
58
+ return runQueryCommandShim(command, rest);
59
+ }
60
+
61
+ if (PASSTHROUGH_COMMANDS.has(command)) {
62
+ return runPassthroughCommand(command, rest);
63
+ }
64
+
65
+ throw new Error(`Unknown command: ${command}`);
66
+ }