context-mode 1.0.148 → 1.0.149

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.
@@ -60,6 +60,42 @@ export declare function resolveProjectDirFromTranscript(opts: {
60
60
  /** Test seam for maxAgeMs. Defaults to Date.now(). */
61
61
  nowMs?: number;
62
62
  }): string | undefined;
63
+ /**
64
+ * Issue #45 / c4529042182 — recover the project-cwd from a Codex CLI
65
+ * session log when the spawned MCP child inherits a non-project cwd
66
+ * (e.g. $HOME when Codex was launched from anywhere outside the project).
67
+ *
68
+ * Codex writes its session transcripts to
69
+ * `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl`. The first line is a
70
+ * `SessionMeta` JSON struct whose `meta.cwd` field carries the literal
71
+ * project directory the CLI was launched from (see refs/platforms/codex/
72
+ * codex-rs SessionMeta). Codex publishes NO workspace env var to its child
73
+ * MCP processes — so unlike Claude/Pi/Cursor, we have no env signal at all.
74
+ * The session log is the strongest available signal.
75
+ *
76
+ * Mirror of `resolveProjectDirFromTranscript` for Claude Code; differences:
77
+ * • Sessions live flat in `${codexHome}/sessions/*.jsonl` (no per-project
78
+ * encoded subdir like Claude's `~/.claude/projects/<encoded>/`).
79
+ * • The cwd is on `meta.cwd` (nested), not top-level `cwd`.
80
+ *
81
+ * Returns `null` when:
82
+ * • `codexHome` or its `sessions/` subdir does not exist.
83
+ * • No `.jsonl` files exist or none has a parseable `meta.cwd` string.
84
+ * • The newest log is older than `transcriptMaxAgeMs` (multi-window guard).
85
+ * • The resolved `meta.cwd` points at a plugin install path (poisoned).
86
+ */
87
+ export declare function resolveCodexSessionCwd(opts?: {
88
+ /** Defaults to `process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")`. */
89
+ codexHome?: string;
90
+ /**
91
+ * Optional freshness guard — Codex appends to the active log while the
92
+ * session is running, so a stale log from days ago must not become a
93
+ * global project-dir signal.
94
+ */
95
+ transcriptMaxAgeMs?: number;
96
+ /** Test seam for transcriptMaxAgeMs. Defaults to Date.now(). */
97
+ now?: number;
98
+ }): string | null;
63
99
  /**
64
100
  * Pure project-dir resolver. Mirror of the env-var chain inside
65
101
  * `src/server.ts getProjectDir()`, but takes its inputs explicitly so the
@@ -100,4 +136,11 @@ export declare function resolveProjectDir(opts: {
100
136
  * for `start.mjs` and any non-strict consumer).
101
137
  */
102
138
  strictPlatform?: PlatformId;
139
+ /**
140
+ * Issue #45 — override `${CODEX_HOME ?? ~/.codex}` for tests. When
141
+ * `strictPlatform === "codex"` and the env cascade yields nothing, the
142
+ * resolver reads `meta.cwd` from the newest session.jsonl under
143
+ * `${codexHome}/sessions/`.
144
+ */
145
+ codexHome?: string;
103
146
  }): string;
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { workspaceEnvVarsFor } from "../adapters/detect.js";
4
5
  /**
@@ -156,6 +157,94 @@ export function resolveProjectDirFromTranscript(opts) {
156
157
  catch { /* file vanished mid-read */ }
157
158
  return undefined;
158
159
  }
160
+ /**
161
+ * Issue #45 / c4529042182 — recover the project-cwd from a Codex CLI
162
+ * session log when the spawned MCP child inherits a non-project cwd
163
+ * (e.g. $HOME when Codex was launched from anywhere outside the project).
164
+ *
165
+ * Codex writes its session transcripts to
166
+ * `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl`. The first line is a
167
+ * `SessionMeta` JSON struct whose `meta.cwd` field carries the literal
168
+ * project directory the CLI was launched from (see refs/platforms/codex/
169
+ * codex-rs SessionMeta). Codex publishes NO workspace env var to its child
170
+ * MCP processes — so unlike Claude/Pi/Cursor, we have no env signal at all.
171
+ * The session log is the strongest available signal.
172
+ *
173
+ * Mirror of `resolveProjectDirFromTranscript` for Claude Code; differences:
174
+ * • Sessions live flat in `${codexHome}/sessions/*.jsonl` (no per-project
175
+ * encoded subdir like Claude's `~/.claude/projects/<encoded>/`).
176
+ * • The cwd is on `meta.cwd` (nested), not top-level `cwd`.
177
+ *
178
+ * Returns `null` when:
179
+ * • `codexHome` or its `sessions/` subdir does not exist.
180
+ * • No `.jsonl` files exist or none has a parseable `meta.cwd` string.
181
+ * • The newest log is older than `transcriptMaxAgeMs` (multi-window guard).
182
+ * • The resolved `meta.cwd` points at a plugin install path (poisoned).
183
+ */
184
+ export function resolveCodexSessionCwd(opts) {
185
+ const codexHome = opts?.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
186
+ const sessionsDir = path.join(codexHome, "sessions");
187
+ if (!fs.existsSync(sessionsDir))
188
+ return null;
189
+ let bestPath;
190
+ let bestMtime = 0;
191
+ try {
192
+ for (const f of fs.readdirSync(sessionsDir)) {
193
+ if (!f.endsWith(".jsonl"))
194
+ continue;
195
+ const fp = path.join(sessionsDir, f);
196
+ try {
197
+ const m = fs.statSync(fp).mtimeMs;
198
+ if (m > bestMtime) {
199
+ bestMtime = m;
200
+ bestPath = fp;
201
+ }
202
+ }
203
+ catch { /* skip */ }
204
+ }
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ if (!bestPath)
210
+ return null;
211
+ if (typeof opts?.transcriptMaxAgeMs === "number") {
212
+ const nowMs = opts.now ?? Date.now();
213
+ if (nowMs - bestMtime > opts.transcriptMaxAgeMs)
214
+ return null;
215
+ }
216
+ // Read first ~8KB; the SessionMeta JSON is line 1 and small. Stream-cap
217
+ // mirrors `resolveProjectDirFromTranscript` for memory safety on long logs.
218
+ try {
219
+ const fd = fs.openSync(bestPath, "r");
220
+ try {
221
+ const buf = Buffer.alloc(8192);
222
+ const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
223
+ const text = buf.subarray(0, bytes).toString("utf-8");
224
+ const firstLine = text.split("\n", 1)[0];
225
+ if (!firstLine || !firstLine.trim())
226
+ return null;
227
+ try {
228
+ const obj = JSON.parse(firstLine);
229
+ const cwd = obj?.meta?.cwd;
230
+ if (typeof cwd !== "string" || cwd.length === 0)
231
+ return null;
232
+ if (isPluginInstallPath(cwd))
233
+ return null;
234
+ return cwd;
235
+ }
236
+ catch {
237
+ return null; /* malformed first line */
238
+ }
239
+ }
240
+ finally {
241
+ fs.closeSync(fd);
242
+ }
243
+ }
244
+ catch {
245
+ return null; /* file vanished mid-read */
246
+ }
247
+ }
159
248
  /**
160
249
  * Pure project-dir resolver. Mirror of the env-var chain inside
161
250
  * `src/server.ts getProjectDir()`, but takes its inputs explicitly so the
@@ -177,7 +266,7 @@ export function resolveProjectDirFromTranscript(opts) {
177
266
  * operation of project-independent tools (sandbox execute, fetch).
178
267
  */
179
268
  export function resolveProjectDir(opts) {
180
- const { env, cwd, pwd, transcriptsRoot, transcriptMaxAgeMs, nowMs, strictPlatform } = opts;
269
+ const { env, cwd, pwd, transcriptsRoot, transcriptMaxAgeMs, nowMs, strictPlatform, codexHome, } = opts;
181
270
  // Build candidate list. Strict path: own workspace vars + universal escape
182
271
  // hatch — NO foreign workspace vars, in any order, can win. Non-strict
183
272
  // path: frozen legacy literal order for backwards compatibility.
@@ -198,6 +287,18 @@ export function resolveProjectDir(opts) {
198
287
  if (fromTranscript && !isPluginInstallPath(fromTranscript))
199
288
  return fromTranscript;
200
289
  }
290
+ // Issue #45 — Codex has no workspace env var, so when running under
291
+ // strictPlatform="codex" we fall back to the session-log heuristic
292
+ // between env and PWD. Non-codex platforms skip this branch entirely.
293
+ if (strictPlatform === "codex") {
294
+ const fromCodex = resolveCodexSessionCwd({
295
+ codexHome,
296
+ transcriptMaxAgeMs,
297
+ now: nowMs,
298
+ });
299
+ if (fromCodex)
300
+ return fromCodex;
301
+ }
201
302
  if (pwd && !isPluginInstallPath(pwd))
202
303
  return pwd;
203
304
  return cwd;