@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 +5 -3
- package/lib/diagnose.mjs +24 -0
- package/lib/redact.mjs +8 -0
- package/lib/redact.test.mjs +33 -0
- package/lib/setup.mjs +52 -18
- package/package.json +1 -1
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
|
-
|
|
90
|
-
|
|
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
|
|
package/lib/redact.test.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
//
|
|
239
|
-
//
|
|
240
|
-
export
|
|
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,
|