@minhpnq1807/contextos 0.5.21 → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.24
4
+
5
+ - **Interactive agent selection:** Replaces the comma-separated text input in `ctx setup` with an interactive multi-select prompt — use ↑/↓ to navigate, Space to toggle agents on/off, and Enter to confirm.
6
+
7
+ ## 0.5.23
8
+
9
+ - **Fix Windows install paths:** Replaces all `process.env.HOME || process.cwd()` fallbacks with `os.homedir()` across `ctx.js`, `claude-hooks.js`, `antigravity-hooks.js`, `claude-mcp.js`, `antigravity-mcp.js`, and `ruler-sync.js`. On Windows, `HOME` is not set, causing `.codex/`, `.claude/`, and `.gemini/` directories (with full `node_modules` and source code) to be created inside the project tree instead of the user's home directory.
10
+ - **Fix ephemeral MCP server path:** `ctx sync --rules` now resolves the MCP server path from stable install roots (`~/.codex/marketplaces/contextos/`, `~/.ctx/contextos/agents/`) instead of `rootDir`, which may point to a temporary npm extraction directory (e.g. `/tmp/contextos/`) that disappears after cleanup.
11
+
12
+ ## 0.5.22
13
+
14
+ - Adds `.gitignore` management to `ctx install`: writes inner `.gitignore` (excludes `node_modules/`, `bin/`, `lib/`, `mcp/`) inside installed agent directories and ensures the project root `.gitignore` excludes `.codex/marketplaces/contextos/`, `.claude/settings.json`, and `.gemini/`.
15
+ - Splits the `npm install -g && ctx setup` one-liner into two separate commands in README and LAUNCH docs to avoid shell PATH resolution failures.
16
+
3
17
  ## 0.5.21
4
18
 
5
19
  - Makes prompt hooks fall back to direct scoring when the `ctx-mcp` bridge socket is missing, stale, or unavailable, avoiding empty `hook context` output.
package/LAUNCH.md CHANGED
@@ -37,7 +37,8 @@ It supports Codex, Claude Code, and Antigravity. It is local-first and uses loca
37
37
 
38
38
  Install:
39
39
 
40
- npm install -g @minhpnq1807/contextos && ctx setup
40
+ npm install -g @minhpnq1807/contextos
41
+ ctx setup
41
42
 
42
43
  Repo: https://github.com/khovan123/contextOS
43
44
  ```
@@ -51,7 +52,8 @@ Codex can read AGENTS.md and still ignore the rule that matters.
51
52
 
52
53
  ContextOS ranks rules per prompt, injects the important ones before work starts, then reports followed / ignored / unknown after the task.
53
54
 
54
- npm install -g @minhpnq1807/contextos && ctx setup
55
+ npm install -g @minhpnq1807/contextos
56
+ ctx setup
55
57
 
56
58
  https://github.com/khovan123/contextOS
57
59
  ```
package/README.md CHANGED
@@ -49,10 +49,11 @@ Rule outcomes: 8 followed, 0 ignored, 0 unknown
49
49
  Runtime telemetry: code-review-graph, code-review-graph.query_graph_tool
50
50
  ```
51
51
 
52
- ## Install In One Line
52
+ ## Quick Install
53
53
 
54
54
  ```bash
55
- npm install -g @minhpnq1807/contextos && ctx setup
55
+ npm install -g @minhpnq1807/contextos
56
+ ctx setup
56
57
  ```
57
58
 
58
59
  No postinstall surprise: `npm install` only installs the CLI. Setup runs only when you call `ctx setup`.
package/bin/ctx.js CHANGED
@@ -1,5 +1,6 @@
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 readline from "node:readline/promises";
5
6
  import { stdin as input, stdout as output } from "node:process";
@@ -24,10 +25,12 @@ import { installClaudeMcp } from "../plugins/ctx/lib/claude-mcp.js";
24
25
  import { installAntigravityHooks } from "../plugins/ctx/lib/antigravity-hooks.js";
25
26
  import { installAntigravityMcp } from "../plugins/ctx/lib/antigravity-mcp.js";
26
27
  import { syncRules } from "../plugins/ctx/lib/ruler-sync.js";
28
+ import { writeInnerGitignore, ensureRootGitignore } from "../plugins/ctx/lib/gitignore.js";
27
29
  import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
28
30
  import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
29
31
  import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
30
32
  import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
33
+ import { multiSelect } from "../plugins/ctx/lib/multi-select.js";
31
34
  import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
32
35
 
33
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -162,7 +165,7 @@ function packageVersion() {
162
165
  }
163
166
 
164
167
  function codexHome() {
165
- return process.env.CODEX_HOME || path.join(process.env.HOME || process.cwd(), ".codex");
168
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
166
169
  }
167
170
 
168
171
  function copyInstall() {
@@ -194,6 +197,9 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
194
197
  const hooksPath = installClaudeHooks({ installRoot, injectPromptContext: inject });
195
198
  progress.step(40, "installing mcp");
196
199
  const mcpConfigPath = installClaudeMcp({ installRoot });
200
+ progress.step(50, "configuring gitignore");
201
+ writeInnerGitignore(installRoot);
202
+ ensureRootGitignore(process.cwd());
197
203
  progress.step(55, "warming embeddings");
198
204
  const warmResult = await warmInstallEmbeddings();
199
205
  progress.done("claude installed");
@@ -218,6 +224,9 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
218
224
  const hooksPath = installAntigravityHooks({ installRoot, injectPromptContext: inject });
219
225
  progress.step(40, "installing mcp");
220
226
  const mcpConfigPaths = installAntigravityMcp({ installRoot });
227
+ progress.step(50, "configuring gitignore");
228
+ writeInnerGitignore(installRoot);
229
+ ensureRootGitignore(process.cwd());
221
230
  progress.step(55, "warming embeddings");
222
231
  const warmResult = await warmInstallEmbeddings();
223
232
  progress.done("agy installed");
@@ -256,6 +265,10 @@ async function install({ copy = false, inject = true, agent = "codex" } = {}) {
256
265
  progress.step(60, "installing hooks");
257
266
  const hooksPath = installGlobalHooks({ codexHome: codexHome(), marketplaceRoot, injectPromptContext: inject });
258
267
 
268
+ progress.step(65, "configuring gitignore");
269
+ writeInnerGitignore(marketplaceRoot);
270
+ ensureRootGitignore(process.cwd());
271
+
259
272
  progress.step(70, "warming embeddings");
260
273
  const warmResult = await warmInstallEmbeddings();
261
274
  progress.done("codex installed");
@@ -481,23 +494,40 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
481
494
 
482
495
  if (interactive) {
483
496
  const rl = readline.createInterface({ input, output });
484
- try {
485
- const proceed = await askSetupYesNo(rl, "Install to this directory?", true);
486
- if (!proceed) {
487
- console.log("Setup cancelled.");
488
- return;
497
+ const proceed = await askSetupYesNo(rl, "Install to this directory?", true);
498
+ if (!proceed) {
499
+ rl.close();
500
+ console.log("Setup cancelled.");
501
+ return;
502
+ }
503
+ if (!options.agentsProvided) {
504
+ rl.close();
505
+ const selected = await multiSelect({
506
+ message: "Select agents to install:",
507
+ options: [
508
+ { label: "Codex", value: "codex", selected: options.agents.includes("codex") },
509
+ { label: "Claude", value: "claude", selected: options.agents.includes("claude") },
510
+ { label: "Antigravity (agy)", value: "agy", selected: options.agents.includes("agy") }
511
+ ]
512
+ });
513
+ options.agents = selected;
514
+ const rl2 = readline.createInterface({ input, output });
515
+ try {
516
+ options.inject = await askSetupYesNo(rl2, "Enable prompt context injection?", options.inject);
517
+ options.syncRules = await askSetupYesNo(rl2, "Sync project rules and MCP servers through Ruler?", options.syncRules);
518
+ options.syncSkills = await askSetupYesNo(rl2, "Sync skills through skillshare?", options.syncSkills);
519
+ } finally {
520
+ rl2.close();
489
521
  }
490
- if (!options.agentsProvided) {
491
- const agents = await askSetupQuestion(rl, "Install for agents? comma-separated", options.agents.join(","));
492
- options.agents = parseAgentList(agents);
493
- } else {
522
+ } else {
523
+ try {
494
524
  console.log(`◇ Install for agents:\n│ ${options.agents.join(", ")}`);
525
+ options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
526
+ options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
527
+ options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
528
+ } finally {
529
+ rl.close();
495
530
  }
496
- options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
497
- options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
498
- options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
499
- } finally {
500
- rl.close();
501
531
  }
502
532
  }
503
533
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.5.21",
3
+ "version": "0.5.24",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  function shellQuote(value) {
@@ -19,7 +20,7 @@ function commandFor(installRoot, scriptName, { injectPromptContext = true } = {}
19
20
 
20
21
  export function antigravityHooksPath() {
21
22
  return process.env.ANTIGRAVITY_HOOKS_PATH
22
- || path.join(process.env.HOME || process.cwd(), ".gemini", "config", "hooks.json");
23
+ || path.join(os.homedir(), ".gemini", "config", "hooks.json");
23
24
  }
24
25
 
25
26
  export function buildAntigravityHooksConfig(existingConfig, { installRoot, injectPromptContext = true } = {}) {
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  function readJsonFile(filePath, fallback) {
@@ -12,7 +13,7 @@ export function antigravityMcpConfigPaths() {
12
13
  if (process.env.ANTIGRAVITY_MCP_CONFIG_PATH) {
13
14
  return [process.env.ANTIGRAVITY_MCP_CONFIG_PATH];
14
15
  }
15
- const home = process.env.HOME || process.cwd();
16
+ const home = os.homedir();
16
17
  return [
17
18
  path.join(home, ".gemini", "antigravity", "mcp_config.json"),
18
19
  path.join(home, ".gemini", "antigravity-cli", "mcp_config.json"),
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  import { buildGlobalHooksConfig } from "./global-hooks.js";
@@ -11,7 +12,7 @@ function readJsonFile(filePath, fallback) {
11
12
  }
12
13
 
13
14
  export function claudeHome() {
14
- return process.env.CLAUDE_HOME || path.join(process.env.HOME || process.cwd(), ".claude");
15
+ return process.env.CLAUDE_HOME || path.join(os.homedir(), ".claude");
15
16
  }
16
17
 
17
18
  export function installClaudeHooks({ claudeHome: home = claudeHome(), installRoot, injectPromptContext = true } = {}) {
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  function readJsonFile(filePath, fallback) {
@@ -9,7 +10,7 @@ function readJsonFile(filePath, fallback) {
9
10
  }
10
11
 
11
12
  export function claudeConfigPath() {
12
- return process.env.CLAUDE_CONFIG_PATH || path.join(process.env.HOME || process.cwd(), ".claude.json");
13
+ return process.env.CLAUDE_CONFIG_PATH || path.join(os.homedir(), ".claude.json");
13
14
  }
14
15
 
15
16
  export function buildClaudeMcpConfig(existingConfig, { installRoot } = {}) {
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Entries to exclude inside the installed contextos directory.
6
+ * Keeps node_modules, bin, lib, and mcp out of version control.
7
+ */
8
+ const INNER_GITIGNORE_ENTRIES = [
9
+ "node_modules/",
10
+ "bin/",
11
+ "lib/",
12
+ "mcp/",
13
+ ];
14
+
15
+ /**
16
+ * Entries that ctx install should add to the project root .gitignore.
17
+ *
18
+ * .codex/marketplaces/contextos/ — Codex agent install dir
19
+ * .claude/settings.json — Claude hooks written by ctx install
20
+ * .gemini/ — Antigravity hooks/config
21
+ */
22
+ const ROOT_GITIGNORE_ENTRIES = [
23
+ ".codex/marketplaces/contextos/",
24
+ ".claude/settings.json",
25
+ ".gemini/",
26
+ ];
27
+
28
+ /**
29
+ * Write a .gitignore inside `dir` that excludes build/runtime artefacts.
30
+ * Creates the directory if it does not exist yet.
31
+ */
32
+ export function writeInnerGitignore(dir) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ const gitignorePath = path.join(dir, ".gitignore");
35
+ const content = INNER_GITIGNORE_ENTRIES.join("\n") + "\n";
36
+ fs.writeFileSync(gitignorePath, content, "utf8");
37
+ return gitignorePath;
38
+ }
39
+
40
+ /**
41
+ * Ensure the project root .gitignore exists and contains the entries
42
+ * needed to keep ctx install artefacts out of version control.
43
+ *
44
+ * Only appends entries that are not already present.
45
+ * Creates the file if it does not exist.
46
+ */
47
+ export function ensureRootGitignore(projectRoot) {
48
+ const gitignorePath = path.join(projectRoot, ".gitignore");
49
+ let existing = "";
50
+ if (fs.existsSync(gitignorePath)) {
51
+ existing = fs.readFileSync(gitignorePath, "utf8");
52
+ }
53
+
54
+ const lines = existing.split("\n");
55
+ const missing = ROOT_GITIGNORE_ENTRIES.filter(
56
+ (entry) => !lines.some((line) => line.trim() === entry)
57
+ );
58
+
59
+ if (missing.length === 0) return gitignorePath;
60
+
61
+ const block = [
62
+ "",
63
+ "# ContextOS install artefacts",
64
+ ...missing,
65
+ ].join("\n") + "\n";
66
+
67
+ fs.writeFileSync(gitignorePath, existing.trimEnd() + "\n" + block, "utf8");
68
+ return gitignorePath;
69
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Interactive multi-select prompt using raw stdin.
3
+ *
4
+ * Usage:
5
+ * const selected = await multiSelect({
6
+ * message: "Select agents:",
7
+ * options: [
8
+ * { label: "Codex", value: "codex", selected: true },
9
+ * { label: "Claude", value: "claude", selected: true },
10
+ * { label: "Antigravity (agy)", value: "agy", selected: true }
11
+ * ]
12
+ * });
13
+ */
14
+
15
+ const KEYS = {
16
+ UP: ["\x1B[A", "\x1Bk"], // Arrow Up, Alt+k
17
+ DOWN: ["\x1B[B", "\x1Bj"], // Arrow Down, Alt+j
18
+ SPACE: [" "],
19
+ ENTER: ["\r", "\n"],
20
+ CTRL_C: ["\x03"],
21
+ // j/k vim-style (single char)
22
+ J: ["j"],
23
+ K: ["k"]
24
+ };
25
+
26
+ const DIM = "\x1B[2m";
27
+ const RESET = "\x1B[0m";
28
+ const CYAN = "\x1B[36m";
29
+ const GREEN = "\x1B[32m";
30
+ const BOLD = "\x1B[1m";
31
+ const HIDE_CURSOR = "\x1B[?25l";
32
+ const SHOW_CURSOR = "\x1B[?25h";
33
+
34
+ function matchKey(data, keySet) {
35
+ return keySet.some((k) => data === k);
36
+ }
37
+
38
+ /**
39
+ * @param {{ message: string, options: Array<{label: string, value: string, selected?: boolean}> }} config
40
+ * @returns {Promise<string[]>} selected values
41
+ */
42
+ export function multiSelect({ message, options }) {
43
+ return new Promise((resolve, reject) => {
44
+ if (!process.stdin.isTTY) {
45
+ // Non-interactive: return all pre-selected
46
+ resolve(options.filter((o) => o.selected).map((o) => o.value));
47
+ return;
48
+ }
49
+
50
+ let cursor = 0;
51
+ const selections = options.map((o) => o.selected !== false);
52
+
53
+ function render() {
54
+ // Move cursor up to overwrite previous render (except first time)
55
+ const lines = [];
56
+ lines.push(`${CYAN}◇${RESET} ${message}`);
57
+ lines.push(`${DIM} Use ↑/↓ to navigate, Space to toggle, Enter to confirm${RESET}`);
58
+ for (let i = 0; i < options.length; i++) {
59
+ const isCursor = i === cursor;
60
+ const isSelected = selections[i];
61
+ const checkbox = isSelected ? `${GREEN}◉${RESET}` : `${DIM}○${RESET}`;
62
+ const label = isCursor ? `${BOLD}${CYAN}${options[i].label}${RESET}` : options[i].label;
63
+ const pointer = isCursor ? `${CYAN}❯${RESET}` : " ";
64
+ lines.push(` ${pointer} ${checkbox} ${label}`);
65
+ }
66
+ return lines;
67
+ }
68
+
69
+ let prevLineCount = 0;
70
+
71
+ function draw() {
72
+ const lines = render();
73
+ // Clear previous output
74
+ if (prevLineCount > 0) {
75
+ process.stdout.write(`\x1B[${prevLineCount}A`); // move up
76
+ for (let i = 0; i < prevLineCount; i++) {
77
+ process.stdout.write("\x1B[2K"); // clear line
78
+ if (i < prevLineCount - 1) process.stdout.write("\x1B[1B"); // move down
79
+ }
80
+ process.stdout.write(`\x1B[${prevLineCount - 1}A`); // back to top
81
+ }
82
+ process.stdout.write(lines.join("\n") + "\n");
83
+ prevLineCount = lines.length;
84
+ }
85
+
86
+ process.stdout.write(HIDE_CURSOR);
87
+
88
+ const wasRaw = process.stdin.isRaw;
89
+ process.stdin.setRawMode(true);
90
+ process.stdin.resume();
91
+ process.stdin.setEncoding("utf8");
92
+
93
+ function cleanup() {
94
+ process.stdin.setRawMode(wasRaw || false);
95
+ process.stdin.pause();
96
+ process.stdin.removeListener("data", onData);
97
+ process.stdout.write(SHOW_CURSOR);
98
+ }
99
+
100
+ function onData(data) {
101
+ if (matchKey(data, KEYS.CTRL_C)) {
102
+ cleanup();
103
+ process.exit(0);
104
+ }
105
+
106
+ if (matchKey(data, KEYS.UP) || matchKey(data, KEYS.K)) {
107
+ cursor = (cursor - 1 + options.length) % options.length;
108
+ draw();
109
+ } else if (matchKey(data, KEYS.DOWN) || matchKey(data, KEYS.J)) {
110
+ cursor = (cursor + 1) % options.length;
111
+ draw();
112
+ } else if (matchKey(data, KEYS.SPACE)) {
113
+ selections[cursor] = !selections[cursor];
114
+ draw();
115
+ } else if (matchKey(data, KEYS.ENTER)) {
116
+ cleanup();
117
+ const selected = options
118
+ .filter((_, i) => selections[i])
119
+ .map((o) => o.value);
120
+ // Print final summary
121
+ const summary = selected.length > 0
122
+ ? selected.join(", ")
123
+ : "(none)";
124
+ // Overwrite prompt area with final state
125
+ if (prevLineCount > 0) {
126
+ process.stdout.write(`\x1B[${prevLineCount}A`);
127
+ for (let i = 0; i < prevLineCount; i++) {
128
+ process.stdout.write("\x1B[2K");
129
+ if (i < prevLineCount - 1) process.stdout.write("\x1B[1B");
130
+ }
131
+ process.stdout.write(`\x1B[${prevLineCount - 1}A`);
132
+ }
133
+ process.stdout.write(`${CYAN}◇${RESET} ${message}\n`);
134
+ process.stdout.write(`${DIM}│${RESET} ${GREEN}${summary}${RESET}\n`);
135
+ resolve(selected);
136
+ }
137
+ }
138
+
139
+ process.stdin.on("data", onData);
140
+ draw();
141
+ });
142
+ }
@@ -5,9 +5,12 @@ import readline from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { execFileSync } from "node:child_process";
7
7
 
8
+ import { defaultDataRoot } from "./workspace-data.js";
9
+
8
10
  const DEFAULT_AGENTS = ["codex", "claude", "antigravity"];
9
11
  const CTX_MCP_NAME = "ctx-mcp";
10
12
  const CONTEXTOS_PROXY_MARKER = "/contextos/plugins/ctx/mcp/proxy.js";
13
+ const MCP_SERVER_RELATIVE = path.join("plugins", "ctx", "mcp", "server.js");
11
14
  const AGENT_ALIASES = new Map([
12
15
  ["agy", "antigravity"],
13
16
  ["antigravity", "antigravity"],
@@ -54,11 +57,11 @@ function displayAgentName(agent) {
54
57
  }
55
58
 
56
59
  function codexConfigPath() {
57
- return path.join(process.env.CODEX_HOME || path.join(process.env.HOME || process.cwd(), ".codex"), "config.toml");
60
+ return path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "config.toml");
58
61
  }
59
62
 
60
63
  function claudeUserConfigPath() {
61
- return process.env.CLAUDE_CONFIG_PATH || path.join(process.env.HOME || process.cwd(), ".claude.json");
64
+ return process.env.CLAUDE_CONFIG_PATH || path.join(os.homedir(), ".claude.json");
62
65
  }
63
66
 
64
67
  export function rulerTomlPath(cwd = process.cwd()) {
@@ -273,7 +276,7 @@ function readRulerMcpServer({ tomlPath, name } = {}) {
273
276
  }
274
277
 
275
278
  function antigravityMcpConfigPaths() {
276
- const home = process.env.HOME || process.cwd();
279
+ const home = os.homedir();
277
280
  return [
278
281
  path.join(home, ".gemini", "antigravity", "mcp_config.json"),
279
282
  path.join(home, ".gemini", "antigravity-cli", "mcp_config.json"),
@@ -456,7 +459,7 @@ export function verifySync({ cwd = process.cwd(), agents = DEFAULT_AGENTS } = {}
456
459
  const checks = [];
457
460
  const definitions = {
458
461
  codex: [path.join(cwd, ".codex", "config.toml")],
459
- claude: [path.join(cwd, ".mcp.json"), path.join(cwd, ".claude", "settings.json"), path.join(process.env.HOME || "", ".claude.json")],
462
+ claude: [path.join(cwd, ".mcp.json"), path.join(cwd, ".claude", "settings.json"), path.join(os.homedir(), ".claude.json")],
460
463
  antigravity: [
461
464
  path.join(cwd, ".gemini", "settings.json"),
462
465
  path.join(cwd, ".gemini", "mcp.json"),
@@ -473,6 +476,21 @@ export function verifySync({ cwd = process.cwd(), agents = DEFAULT_AGENTS } = {}
473
476
  return checks;
474
477
  }
475
478
 
479
+ function resolveStableMcpServerPath(rootDir) {
480
+ const codexRoot = path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "marketplaces", "contextos");
481
+ const dataRoot = defaultDataRoot();
482
+ const candidates = [
483
+ path.join(codexRoot, MCP_SERVER_RELATIVE),
484
+ path.join(dataRoot, "agents", "claude", "contextos", MCP_SERVER_RELATIVE),
485
+ path.join(dataRoot, "agents", "agy", "contextos", MCP_SERVER_RELATIVE),
486
+ path.join(rootDir, MCP_SERVER_RELATIVE)
487
+ ];
488
+ for (const candidate of candidates) {
489
+ if (fs.existsSync(candidate)) return candidate;
490
+ }
491
+ return path.join(rootDir, MCP_SERVER_RELATIVE);
492
+ }
493
+
476
494
  export async function syncRules({
477
495
  cwd = process.cwd(),
478
496
  rootDir,
@@ -495,7 +513,7 @@ export async function syncRules({
495
513
  const init = ensureRulerInit({ cwd, run, dryRun: options.dryRun });
496
514
  logger(statusLine("Checking .ruler/ruler.toml...", init.created ? "✓ created" : "✓ found"));
497
515
 
498
- const mcpServerPath = path.join(rootDir, "plugins", "ctx", "mcp", "server.js");
516
+ const mcpServerPath = resolveStableMcpServerPath(rootDir);
499
517
  const injected = injectCtxMcp({
500
518
  tomlPath: init.tomlPath,
501
519
  mcpServerPath,