@theronap/cortex-mcp 0.7.0 → 0.9.0
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/setup.mjs +45 -1
- package/lib/skills.mjs +81 -63
- package/package.json +1 -1
package/lib/setup.mjs
CHANGED
|
@@ -14,6 +14,34 @@ import { installSkills } from './skills.mjs'
|
|
|
14
14
|
|
|
15
15
|
const PKG = '@theronap/cortex-mcp'
|
|
16
16
|
|
|
17
|
+
// Merge the Cortex MCP server into a Codex config.toml. Pure + idempotent: strips any existing
|
|
18
|
+
// [mcp_servers.cortex] / [mcp_servers.cortex.env] tables (so re-runs update in place rather than
|
|
19
|
+
// duplicating — a duplicate TOML table would break Codex's parser), preserves every other table,
|
|
20
|
+
// and appends a fresh block. Only touches the cortex tables; never rewrites the user's config.
|
|
21
|
+
export function mergeCodexToml(text, spec, token) {
|
|
22
|
+
const targets = new Set(['[mcp_servers.cortex]', '[mcp_servers.cortex.env]'])
|
|
23
|
+
const kept = []
|
|
24
|
+
let skipping = false
|
|
25
|
+
for (const line of (text || '').split('\n')) {
|
|
26
|
+
const t = line.trim()
|
|
27
|
+
if (t.startsWith('[') && t.endsWith(']')) skipping = targets.has(t)
|
|
28
|
+
if (!skipping) kept.push(line)
|
|
29
|
+
}
|
|
30
|
+
while (kept.length && kept[kept.length - 1].trim() === '') kept.pop() // drop trailing blanks
|
|
31
|
+
const block = [
|
|
32
|
+
'',
|
|
33
|
+
'[mcp_servers.cortex]',
|
|
34
|
+
'command = "npx"',
|
|
35
|
+
`args = ["-y", "${spec}"]`,
|
|
36
|
+
'startup_timeout_sec = 60', // first npx fetch can be slow; don't time out the server on cold start
|
|
37
|
+
'',
|
|
38
|
+
'[mcp_servers.cortex.env]',
|
|
39
|
+
`CORTEX_TOKEN = "${token}"`,
|
|
40
|
+
'',
|
|
41
|
+
]
|
|
42
|
+
return [...kept, ...block].join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
function readJson(path) {
|
|
18
46
|
if (!existsSync(path)) return {}
|
|
19
47
|
const raw = readFileSync(path, 'utf8').trim()
|
|
@@ -77,6 +105,22 @@ export async function runSetup(argv, version) {
|
|
|
77
105
|
process.exit(1)
|
|
78
106
|
}
|
|
79
107
|
|
|
108
|
+
// ── 1b. MCP server in Codex (~/.codex/config.toml), only if Codex is installed ──
|
|
109
|
+
// Codex gets the same Cortex context tools as Claude Code. Non-fatal: a Codex hiccup must
|
|
110
|
+
// never block the primary Claude wiring. Capture/skills self-heal stay Claude-driven for now.
|
|
111
|
+
const codexDir = join(home, '.codex')
|
|
112
|
+
if (existsSync(codexDir)) {
|
|
113
|
+
try {
|
|
114
|
+
const codexToml = join(codexDir, 'config.toml')
|
|
115
|
+
const existing = existsSync(codexToml) ? readFileSync(codexToml, 'utf8') : ''
|
|
116
|
+
const bak = backup(codexToml)
|
|
117
|
+
writeFileSync(codexToml, mergeCodexToml(existing, spec, token))
|
|
118
|
+
log(` ✓ MCP server → ${codexToml}${bak ? ' (backup saved)' : ''}`)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
log(` ⚠ Codex MCP wiring skipped: ${e.message} (Claude wiring unaffected)`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
// ── 2. Capture Stop hook in ~/.claude/settings.json ──────────────────────
|
|
81
125
|
try {
|
|
82
126
|
let s
|
|
@@ -134,7 +178,7 @@ export async function runSetup(argv, version) {
|
|
|
134
178
|
process.exit(1)
|
|
135
179
|
}
|
|
136
180
|
|
|
137
|
-
// ── 3. Managed skills
|
|
181
|
+
// ── 3. Managed skills — installed flat into every agent CLI present (Claude + Codex) ──
|
|
138
182
|
try {
|
|
139
183
|
installSkills({ quiet: false })
|
|
140
184
|
} catch (e) {
|
package/lib/skills.mjs
CHANGED
|
@@ -3,24 +3,32 @@ import { homedir } from 'os'
|
|
|
3
3
|
import { join, dirname } from 'path'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
5
|
|
|
6
|
-
// Managed Cortex skills
|
|
6
|
+
// Managed Cortex skills.
|
|
7
7
|
//
|
|
8
|
-
// Cortex
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// core/<name>/SKILL.md ← MANDATORY, self-healing skills shipped in this package
|
|
13
|
-
// (org/ and shared/ are reserved for server-synced skills — not yet populated)
|
|
8
|
+
// Cortex ships mandatory, self-healing skills (e.g. cortex-log) and installs them into EVERY agent
|
|
9
|
+
// CLI present on the machine, at the flat layout each one discovers:
|
|
10
|
+
// ~/.claude/skills/<name>/SKILL.md (Claude Code)
|
|
11
|
+
// ~/.codex/skills/<name>/SKILL.md (OpenAI Codex CLI — only if ~/.codex exists)
|
|
14
12
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
13
|
+
// IMPORTANT: skills MUST be one level under <cli>/skills/ — both Claude Code and Codex discover
|
|
14
|
+
// skills as <cli>/skills/<name>/SKILL.md. An earlier layout nested them under skills/cortex/core/,
|
|
15
|
+
// which is installed but INVISIBLE to discovery (the skill never appears as a slash command).
|
|
16
|
+
//
|
|
17
|
+
// "Inalterable" in practice: the bundled skills are the source of truth. On every install AND on
|
|
18
|
+
// every Claude session start (the SessionStart --repair hook), any managed skill whose on-disk
|
|
19
|
+
// content drifted is RESTORED — the user's version is backed up to SKILL.md.user-bak first, so
|
|
20
|
+
// nothing is lost, but the canonical skill always wins. Identical content is a no-op.
|
|
20
21
|
|
|
21
22
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
23
|
const BUNDLED = join(HERE, '..', 'skills') // packages/cortex-mcp/skills/<name>/SKILL.md
|
|
23
24
|
|
|
25
|
+
// Agent CLIs we install skills into. Claude is primary; Codex is included whenever it's present.
|
|
26
|
+
// Both use the same flat <cli>/skills/<name>/SKILL.md discovery layout.
|
|
27
|
+
const CLIS = [
|
|
28
|
+
{ id: 'Claude Code', dir: join(homedir(), '.claude') },
|
|
29
|
+
{ id: 'Codex', dir: join(homedir(), '.codex') },
|
|
30
|
+
]
|
|
31
|
+
|
|
24
32
|
// djb2 — tiny, dependency-free content fingerprint for the manifest (drift detection, not security).
|
|
25
33
|
function hash(s) {
|
|
26
34
|
let h = 5381
|
|
@@ -32,90 +40,100 @@ function ensureDir(path) {
|
|
|
32
40
|
if (!existsSync(path)) mkdirSync(path, { recursive: true })
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
//
|
|
43
|
+
// The invocation name (slash command) comes from the SKILL.md `name:` frontmatter, not the folder.
|
|
44
|
+
// Use it as the install directory so the layout matches what the user types (/cortex-log).
|
|
45
|
+
function frontmatterName(source, fallback) {
|
|
46
|
+
const m = source.match(/^---[\s\S]*?\bname:\s*([^\n#]+)/)
|
|
47
|
+
return m ? m[1].trim() : fallback
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Discover bundled skills: each subdir of skills/ that contains a SKILL.md.
|
|
36
51
|
function bundledSkills() {
|
|
37
52
|
if (!existsSync(BUNDLED)) return []
|
|
38
53
|
return readdirSync(BUNDLED, { withFileTypes: true })
|
|
39
54
|
.filter((d) => d.isDirectory() && existsSync(join(BUNDLED, d.name, 'SKILL.md')))
|
|
40
|
-
.map((d) =>
|
|
55
|
+
.map((d) => {
|
|
56
|
+
const src = join(BUNDLED, d.name, 'SKILL.md')
|
|
57
|
+
const source = readFileSync(src, 'utf8')
|
|
58
|
+
return { name: frontmatterName(source, d.name), src, source }
|
|
59
|
+
})
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
self-healing**: if you edit or delete one, Cortex restores it on your next Claude session (your edited
|
|
47
|
-
copy is saved as \`SKILL.md.user-bak\` first). To change a core skill, change it upstream in Cortex.
|
|
48
|
-
|
|
49
|
-
- \`core/\` — required Cortex skills, shipped and repaired by this package
|
|
50
|
-
- \`org/\` — your organization's skills (reserved; synced from Cortex)
|
|
51
|
-
- \`shared/\` — skills shared across the org (reserved; synced from Cortex)
|
|
52
|
-
|
|
53
|
-
Managed by: \`npx -y @theronap/cortex-mcp skills\` · repaired automatically each session.
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Install / repair the managed Cortex skills.
|
|
58
|
-
* @param {{ quiet?: boolean }} opts quiet → only emit on actual change (for the SessionStart hook)
|
|
59
|
-
* @returns {{ installed: string[], repaired: string[], unchanged: string[] }}
|
|
60
|
-
*/
|
|
61
|
-
export function installSkills(opts = {}) {
|
|
62
|
-
const quiet = !!opts.quiet
|
|
63
|
-
const root = join(homedir(), '.claude', 'skills', 'cortex')
|
|
64
|
-
const coreDir = join(root, 'core')
|
|
65
|
-
const log = (m) => { if (!quiet) process.stdout.write(m + '\n') }
|
|
66
|
-
|
|
67
|
-
const skills = bundledSkills()
|
|
62
|
+
// Install/repair all bundled skills into one CLI's skills root. Returns per-CLI change lists.
|
|
63
|
+
function installInto(skillsRoot, skills) {
|
|
64
|
+
ensureDir(skillsRoot)
|
|
68
65
|
const result = { installed: [], repaired: [], unchanged: [] }
|
|
69
|
-
if (!skills.length) return result // nothing bundled (shouldn't happen) — never error
|
|
70
|
-
|
|
71
|
-
ensureDir(coreDir)
|
|
72
|
-
// Reserved namespaces so the structure is discoverable even before server sync exists.
|
|
73
|
-
ensureDir(join(root, 'org'))
|
|
74
|
-
ensureDir(join(root, 'shared'))
|
|
75
|
-
writeFileSync(join(root, 'README.md'), README)
|
|
76
|
-
|
|
77
66
|
const manifest = { managed: [], updated_by: 'cortex-mcp', skills: {} }
|
|
78
67
|
|
|
79
68
|
for (const sk of skills) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
manifest.
|
|
83
|
-
manifest.skills[sk.name] = hash(source)
|
|
69
|
+
const dest = join(skillsRoot, sk.name, 'SKILL.md')
|
|
70
|
+
manifest.managed.push(`${sk.name}/SKILL.md`)
|
|
71
|
+
manifest.skills[sk.name] = hash(sk.source)
|
|
84
72
|
|
|
85
73
|
if (!existsSync(dest)) {
|
|
86
74
|
ensureDir(dirname(dest))
|
|
87
|
-
writeFileSync(dest, source)
|
|
75
|
+
writeFileSync(dest, sk.source)
|
|
88
76
|
result.installed.push(sk.name)
|
|
89
77
|
continue
|
|
90
78
|
}
|
|
91
79
|
const current = readFileSync(dest, 'utf8')
|
|
92
|
-
if (current === source) { result.unchanged.push(sk.name); continue }
|
|
80
|
+
if (current === sk.source) { result.unchanged.push(sk.name); continue }
|
|
93
81
|
|
|
94
82
|
// Drift: preserve the user's version, then restore canonical.
|
|
95
83
|
try { copyFileSync(dest, `${dest}.user-bak`) } catch { /* best-effort backup */ }
|
|
96
|
-
writeFileSync(dest, source)
|
|
84
|
+
writeFileSync(dest, sk.source)
|
|
97
85
|
result.repaired.push(sk.name)
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
writeFileSync(join(
|
|
88
|
+
writeFileSync(join(skillsRoot, '.cortex-skills.json'), JSON.stringify(manifest, null, 2))
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Install / repair the managed Cortex skills into every agent CLI present on the machine.
|
|
94
|
+
* @param {{ quiet?: boolean }} opts quiet → only emit on actual change (for the SessionStart hook)
|
|
95
|
+
* @returns {{ installed: string[], repaired: string[], unchanged: string[], targets: string[] }}
|
|
96
|
+
*/
|
|
97
|
+
export function installSkills(opts = {}) {
|
|
98
|
+
const quiet = !!opts.quiet
|
|
99
|
+
const log = (m) => { if (!quiet) process.stdout.write(m + '\n') }
|
|
100
|
+
|
|
101
|
+
const skills = bundledSkills()
|
|
102
|
+
const summary = { installed: [], repaired: [], unchanged: [], targets: [] }
|
|
103
|
+
if (!skills.length) return summary // nothing bundled (shouldn't happen) — never error
|
|
104
|
+
|
|
105
|
+
const targets = CLIS.filter((c) => existsSync(c.dir))
|
|
106
|
+
if (!targets.length) return summary // no agent CLI on this machine
|
|
107
|
+
|
|
108
|
+
for (const cli of targets) {
|
|
109
|
+
const skillsRoot = join(cli.dir, 'skills')
|
|
110
|
+
const r = installInto(skillsRoot, skills)
|
|
111
|
+
summary.targets.push(cli.id)
|
|
112
|
+
summary.installed.push(...r.installed)
|
|
113
|
+
summary.repaired.push(...r.repaired)
|
|
114
|
+
summary.unchanged.push(...r.unchanged)
|
|
115
|
+
const changed = [...r.installed, ...r.repaired]
|
|
116
|
+
if (changed.length) log(` ✓ ${cli.id}: ${changed.join(', ')} → ${skillsRoot}`)
|
|
117
|
+
}
|
|
101
118
|
|
|
102
|
-
if (
|
|
103
|
-
if (result.repaired.length) log(` ✓ Cortex skills restored (backed up your copy): ${result.repaired.join(', ')}`)
|
|
104
|
-
if (quiet && (result.installed.length || result.repaired.length)) {
|
|
119
|
+
if (quiet && (summary.installed.length || summary.repaired.length)) {
|
|
105
120
|
// SessionStart surfaces one line in Claude Code so a silent self-heal isn't invisible.
|
|
106
|
-
process.stdout.write(`Cortex: synced ${
|
|
121
|
+
process.stdout.write(`Cortex: synced managed skill(s) into ${summary.targets.join(' + ')}.\n`)
|
|
107
122
|
}
|
|
108
|
-
return
|
|
123
|
+
return summary
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
// CLI entry: `cortex-mcp skills [--repair] [--quiet]`. (--repair and plain install are the same
|
|
112
127
|
// idempotent operation; --repair is just the name the SessionStart hook uses for intent.)
|
|
113
128
|
export async function runSkills(argv = []) {
|
|
114
129
|
const quiet = argv.includes('--quiet')
|
|
115
|
-
if (!quiet) process.stdout.write('\nCortex skills — installing managed
|
|
130
|
+
if (!quiet) process.stdout.write('\nCortex skills — installing managed skills…\n')
|
|
116
131
|
const r = installSkills({ quiet })
|
|
117
|
-
if (!quiet
|
|
118
|
-
process.stdout.write(
|
|
132
|
+
if (!quiet) {
|
|
133
|
+
if (!r.targets.length) process.stdout.write(' ! No agent CLI found (~/.claude or ~/.codex). Nothing to install.\n')
|
|
134
|
+
else if (!r.installed.length && !r.repaired.length) {
|
|
135
|
+
process.stdout.write(` ✓ Up to date in ${r.targets.join(' + ')} (${[...new Set(r.unchanged)].join(', ') || 'none'}).\n`)
|
|
136
|
+
}
|
|
119
137
|
}
|
|
120
138
|
return 0
|
|
121
139
|
}
|