@theronap/cortex-mcp 0.9.31 → 0.9.33

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/lib/capture.mjs CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs'
2
2
  import { homedir } from 'os'
3
3
  import { resolve } from 'path'
4
4
  import { createHash } from 'crypto'
5
- import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
5
+ import { fetchCortex, classify, resolveBase, readWiredToken } from './diagnose.mjs'
6
6
  import { extractSession } from './edge_extract.mjs'
7
7
  import { extractTyped } from './extract_typed.mjs'
8
8
  import { redactSecrets } from './redact.mjs'
@@ -86,8 +86,10 @@ export async function runCapture() {
86
86
  // would recurse (and re-ingest the summarizer's prompt as a phantom session). Bail immediately.
87
87
  if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
88
88
 
89
- const token = process.env.CORTEX_TOKEN
90
- if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
89
+ // Env first (explicit override / legacy inlined hooks), else the token wired into this machine's
90
+ // MCP config so hook commands carry no secret (token-hygiene, 2026-07-02).
91
+ const token = process.env.CORTEX_TOKEN || readWiredToken()
92
+ if (!token) { process.stderr.write('cortex: no CORTEX_TOKEN in env or wired config, skipping\n'); return }
91
93
  const base = resolveBase(process.env.CORTEX_URL)
92
94
 
93
95
  let hook = {}
package/lib/diagnose.mjs CHANGED
@@ -9,9 +9,33 @@
9
9
  // 4xx/5xx means infrastructure handled the request, not Cortex auth — re-running setup or
10
10
  // regenerating the token will not help; it is usually transient and worth a retry.
11
11
 
12
+ import { readFileSync } from 'fs'
13
+ import { homedir } from 'os'
14
+ import { join } from 'path'
15
+
12
16
  export const isUuid = (s) =>
13
17
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s ?? '')
14
18
 
19
+ // Find the token already wired on this machine (Claude's ~/.claude.json first, then Codex's
20
+ // config.toml). Lives HERE — the shared leaf module — so both setup/repair and the capture hook
21
+ // use one resolver: hook commands carry NO inline token (token-hygiene, 2026-07-02 — an inlined
22
+ // `CORTEX_TOKEN=…` in hooks.json shows the secret to anything that reads or greps the file, and
23
+ // from there to captured transcripts); `capture` resolves the token itself from the same config
24
+ // the MCP server already holds.
25
+ export function readWiredToken() {
26
+ try {
27
+ const cfg = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8'))
28
+ const t = cfg?.mcpServers?.cortex?.env?.CORTEX_TOKEN
29
+ if (t) return t
30
+ } catch { /* fall through */ }
31
+ try {
32
+ const toml = readFileSync(join(homedir(), '.codex', 'config.toml'), 'utf8')
33
+ const m = toml.match(/\[mcp_servers\.cortex\.env\][\s\S]*?CORTEX_TOKEN\s*=\s*"([^"]+)"/)
34
+ if (m) return m[1]
35
+ } catch { /* fall through */ }
36
+ return null
37
+ }
38
+
15
39
  // The production alias — exempt from Vercel Deployment Protection.
16
40
  export const CANONICAL_BASE = 'https://cortex-console.vercel.app'
17
41
 
package/lib/redact.mjs CHANGED
@@ -21,6 +21,14 @@ const PATTERNS = [
21
21
  [/xox[baprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack]'],
22
22
  [/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED:jwt]'],
23
23
  [/(Bearer\s+)[A-Za-z0-9._-]{20,}/g, '$1[REDACTED]'],
24
+ // Cortex's own login token wherever it appears in KEY=value / KEY: value form (hooks.json,
25
+ // config.toml, shell commands — the 2026-07-02 finding: a grep of hooks.json put the live token
26
+ // in a transcript and nothing below caught it). Specific pattern first for the accurate label.
27
+ [/(CORTEX_TOKEN["']?\s*[=:]\s*["']?)[0-9a-fA-F][0-9a-fA-F-]{30,}/g, '$1[REDACTED:cortex-token]'],
28
+ // Generic secret-shaped assignment: an UPPER_SNAKE name ending in TOKEN/SECRET/PASSWORD/
29
+ // API_KEY/APIKEY assigned a ≥16-char value. Conservative: the name-suffix + length floor keep
30
+ // ordinary prose, short placeholders, and bare UUIDs (record ids) intact.
31
+ [/([A-Z][A-Z0-9_]{2,}(?:TOKEN|SECRET|PASSWORD|API_KEY|APIKEY)["']?\s*[=:]\s*["']?)[A-Za-z0-9._/+-]{16,}/g, '$1[REDACTED:env]'],
24
32
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED:private-key]'],
25
33
  ]
26
34
 
@@ -48,6 +48,39 @@ test('leaves ordinary prose and record UUIDs untouched', () => {
48
48
  expect(redactSecrets(prose)).toBe(prose)
49
49
  })
50
50
 
51
+ // The 2026-07-02 finding: a grep of ~/.codex/hooks.json printed the live CORTEX_TOKEN (a bare
52
+ // UUID — no pattern above catches those) into a transcript. The inline form is gone from hooks
53
+ // (capture self-resolves now), but transcripts can still carry config lines from ANY tool read.
54
+ test('redacts CORTEX_TOKEN in env-assignment and TOML/JSON forms', () => {
55
+ const uuid = 'ee0455d0-49bd-4141-984d-ece9311f9407'
56
+ const cases = [
57
+ `CORTEX_TOKEN=${uuid} npx -y @theronap/cortex-mcp@latest capture`, // legacy hook command
58
+ `"CORTEX_TOKEN": "${uuid}"`, // ~/.claude.json
59
+ `CORTEX_TOKEN = "${uuid}"`, // config.toml
60
+ ]
61
+ for (const s of cases) {
62
+ const out = redactSecrets(s)
63
+ expect(out).toContain('[REDACTED:cortex-token]')
64
+ expect(out).not.toContain(uuid)
65
+ }
66
+ })
67
+
68
+ test('redacts generic secret-shaped env assignments', () => {
69
+ const out = redactSecrets('export SUPABASE_SERVICE_SECRET=abcDEF0123456789xyz && run')
70
+ expect(out).toBe('export SUPABASE_SERVICE_SECRET=[REDACTED:env] && run')
71
+ const out2 = redactSecrets('MY_API_KEY: "0123456789abcdef0123"')
72
+ expect(out2).toContain('[REDACTED:env]')
73
+ })
74
+
75
+ test('generic env pattern leaves short placeholders and prose intact', () => {
76
+ const cases = [
77
+ 'set YOUR_TOKEN=xxx in the env', // short placeholder value
78
+ 'the CORTEX_TOKEN is stored in ~/.claude.json', // prose, no assignment value
79
+ 'record 5203eacd-321b-43f7-bae2-4d6e7cae96ab', // bare UUID, no key name
80
+ ]
81
+ for (const s of cases) expect(redactSecrets(s)).toBe(s)
82
+ })
83
+
51
84
  test('handles empty / non-string input safely', () => {
52
85
  expect(redactSecrets('')).toBe('')
53
86
  expect(redactSecrets(null)).toBe(null)
package/lib/setup.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'
2
2
  import { homedir } from 'os'
3
3
  import { join, dirname } from 'path'
4
- import { checkToken, resolveBase } from './diagnose.mjs'
4
+ import { checkToken, resolveBase, readWiredToken } from './diagnose.mjs'
5
5
  import { installSkills } from './skills.mjs'
6
6
 
7
7
  // One-command employee onboarding. Wires both:
@@ -61,6 +61,28 @@ function ensureDir(path) {
61
61
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
62
62
  }
63
63
 
64
+ // Merge the capture Stop hook into a Codex hooks.json object. Pure + idempotent: drops any prior
65
+ // cortex capture entry (old token / old path / old pinned version) before appending the current one,
66
+ // so re-running never duplicates and never leaves a stale version pinned behind a live one.
67
+ // Mirrors the Claude Stop-hook merge in step 2 below, minus the SessionStart/PreCompact hooks Codex
68
+ // doesn't support (confirmed: Codex's hook runtime only recognizes PreToolUse/PostToolUse/PreCompact/
69
+ // UserPromptSubmit/Stop — no SessionStart, so status/skills-repair/snapshot-context stay Claude-only).
70
+ export function mergeCodexHooks(existing, captureCmd) {
71
+ const h = existing && typeof existing === 'object' ? existing : {}
72
+ h.hooks = h.hooks && typeof h.hooks === 'object' ? h.hooks : {}
73
+ h.hooks.Stop = Array.isArray(h.hooks.Stop) ? h.hooks.Stop : []
74
+ for (const grp of h.hooks.Stop) {
75
+ if (Array.isArray(grp.hooks)) {
76
+ grp.hooks = grp.hooks.filter((c) => !/cortex-mcp.*capture|capture-session-cloud/.test(c.command ?? ''))
77
+ }
78
+ }
79
+ let grp = h.hooks.Stop.find((g) => (g.matcher ?? '') === '')
80
+ if (!grp) { grp = { matcher: '', hooks: [] }; h.hooks.Stop.push(grp) }
81
+ grp.hooks = grp.hooks ?? []
82
+ grp.hooks.push({ type: 'command', command: captureCmd })
83
+ return h
84
+ }
85
+
64
86
  export async function runSetup(argv, version) {
65
87
  const token = argv[0]
66
88
  if (!token || token.startsWith('-')) {
@@ -110,7 +132,7 @@ export async function runSetup(argv, version) {
110
132
 
111
133
  // ── 1b. MCP server in Codex (~/.codex/config.toml), only if Codex is installed ──
112
134
  // Codex gets the same Cortex context tools as Claude Code. Non-fatal: a Codex hiccup must
113
- // never block the primary Claude wiring. Capture/skills self-heal stay Claude-driven for now.
135
+ // never block the primary Claude wiring.
114
136
  const codexDir = join(home, '.codex')
115
137
  if (existsSync(codexDir)) {
116
138
  try {
@@ -122,6 +144,28 @@ export async function runSetup(argv, version) {
122
144
  } catch (e) {
123
145
  log(` ⚠ Codex MCP wiring skipped: ${e.message} (Claude wiring unaffected)`)
124
146
  }
147
+
148
+ // ── 1c. Capture Stop hook in Codex (~/.codex/hooks.json) ──
149
+ // Without this, Codex sessions get context tools + skills but never capture — a silent gap
150
+ // (found 2026-07-02: a hand-set hook on Theron's machine was pinned to a stale 0.4.5, years
151
+ // behind `stable`, because nothing in setup/repair ever refreshed it). Same idempotent
152
+ // merge pattern as the Claude Stop hook below; non-fatal on failure.
153
+ try {
154
+ const codexHooks = join(codexDir, 'hooks.json')
155
+ let existingHooks
156
+ try { existingHooks = readJson(codexHooks) } catch { existingHooks = {} } // malformed → start fresh, don't block
157
+ const bak = backup(codexHooks)
158
+ // NO inline token (token-hygiene, 2026-07-02): `capture` resolves CORTEX_TOKEN itself via
159
+ // readWiredToken() from the config.toml this same setup run writes. An inlined secret in
160
+ // hooks.json shows up in every read/grep of the file — and from there in captured transcripts.
161
+ const captureCmd = `npx -y ${spec} capture`
162
+ ensureDir(codexHooks)
163
+ writeFileSync(codexHooks, JSON.stringify(mergeCodexHooks(existingHooks, captureCmd), null, 2))
164
+ log(` ✓ Capture hook → ${codexHooks}${bak ? ' (backup saved)' : ''}`)
165
+ log(' Codex will ask you to re-approve this hook once (it hashes hooks.json for tamper-detection).')
166
+ } catch (e) {
167
+ log(` ⚠ Codex capture hook skipped: ${e.message} (Claude wiring unaffected)`)
168
+ }
125
169
  }
126
170
 
127
171
  // ── 2. Capture Stop hook in ~/.claude/settings.json ──────────────────────
@@ -135,7 +179,9 @@ export async function runSetup(argv, version) {
135
179
  s.hooks = s.hooks ?? {}
136
180
  s.hooks.Stop = Array.isArray(s.hooks.Stop) ? s.hooks.Stop : []
137
181
 
138
- const captureCmd = `CORTEX_TOKEN=${token} npx -y ${spec} capture`
182
+ // NO inline token same hygiene as the Codex hook above; `capture` self-resolves from
183
+ // ~/.claude.json (which this setup run writes). The filter below drops the old inlined form.
184
+ const captureCmd = `npx -y ${spec} capture`
139
185
  // Remove any prior cortex capture hook (idempotent: drop old token / old path forms).
140
186
  for (const grp of s.hooks.Stop) {
141
187
  if (Array.isArray(grp.hooks)) {
@@ -235,21 +281,9 @@ export async function runSetup(argv, version) {
235
281
  log('')
236
282
  }
237
283
 
238
- // Find the token already wired on this machine, so `repair` can re-run setup without re-pasting it.
239
- // Checks Claude's config first, then Codex's config.toml.
240
- export function readWiredToken() {
241
- try {
242
- const cfg = readJson(join(homedir(), '.claude.json'))
243
- const t = cfg?.mcpServers?.cortex?.env?.CORTEX_TOKEN
244
- if (t) return t
245
- } catch { /* fall through */ }
246
- try {
247
- const toml = readFileSync(join(homedir(), '.codex', 'config.toml'), 'utf8')
248
- const m = toml.match(/\[mcp_servers\.cortex\.env\][\s\S]*?CORTEX_TOKEN\s*=\s*"([^"]+)"/)
249
- if (m) return m[1]
250
- } catch { /* fall through */ }
251
- return null
252
- }
284
+ // readWiredToken moved to diagnose.mjs (the shared leaf module) so `capture` can use the same
285
+ // resolver hook commands no longer inline the token. Re-exported for compatibility.
286
+ export { readWiredToken }
253
287
 
254
288
  // `repair`: re-run the FULL setup at THIS version using the already-wired token. The one-command fix
255
289
  // for a machine set up with an older version (e.g. when skills were installed to the old nested path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.31",
3
+ "version": "0.9.33",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {