@theronap/cortex-mcp 0.9.3 → 0.9.5

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/README.md CHANGED
@@ -19,8 +19,9 @@ Connect your AI assistant to **Cortex** — your org's projects, recent activity
19
19
  }
20
20
  ```
21
21
 
22
- 3. Restart Claude Code. Your AI now sees your org context automatically, and the managed startup skill
23
- will prefer query-centered `session_context` on substantive session opens.
22
+ 3. Restart Claude Code. Your AI now sees your org context automatically, the managed startup skill
23
+ will prefer query-centered `session_context` on substantive session opens, and each session start
24
+ will log the exact baseline Cortex context to `~/.cortex/context-snapshots/`.
24
25
 
25
26
  No clone, no path, no build step — `npx` fetches and runs it.
26
27
 
@@ -31,6 +32,13 @@ No clone, no path, no build step — `npx` fetches and runs it.
31
32
  - **search_org** — search your visible activity and projects by keyword.
32
33
  - **project_status** — status of a specific project by key.
33
34
 
35
+ ## Startup snapshots
36
+
37
+ Every Claude session start runs `snapshot-context`, which fetches the exact `my_context` payload and writes:
38
+ - `~/.cortex/context-snapshots/latest.md`
39
+ - `~/.cortex/context-snapshots/<timestamp>.md`
40
+ - `~/.cortex/context-snapshots/index.jsonl`
41
+
34
42
  ## Environment
35
43
 
36
44
  | Var | Required | Default |
@@ -7,6 +7,7 @@
7
7
  * setup <TOKEN> wire BOTH the MCP server + capture hook into your Claude config
8
8
  * doctor live health check — is the token actually working? (no restart needed)
9
9
  * capture the Stop-hook capturer (invoked by Claude Code, not by hand)
10
+ * snapshot-context save the exact startup context served by Cortex to a local log file
10
11
  * --version | -v
11
12
  * --help | -h
12
13
  *
@@ -16,7 +17,7 @@
16
17
  * Get your token from the Cortex console → Connect your AI.
17
18
  */
18
19
 
19
- const VERSION = '0.9.3'
20
+ const VERSION = '0.9.5'
20
21
  const cmd = process.argv[2]
21
22
  const rest = process.argv.slice(3)
22
23
 
@@ -34,9 +35,11 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
34
35
  `sessions flow into the org automatically. Restart Claude Code after.\n\n` +
35
36
  `Subcommands:\n` +
36
37
  ` setup <token> wire MCP server + capture hook into ~/.claude config\n` +
38
+ ` repair re-run setup at the latest version using your existing token (no token needed)\n` +
37
39
  ` doctor live health check — confirm your token works (no restart needed)\n` +
38
40
  ` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
39
41
  ` skills install/repair the managed Cortex skills (also wired by setup)\n` +
42
+ ` snapshot-context save the exact startup context Cortex served to a local snapshot\n` +
40
43
  ` capture Stop-hook capturer (invoked by Claude Code)\n` +
41
44
  ` (no args) run the MCP server (used by your Claude config)\n\n` +
42
45
  `Get your token from the Cortex console → Connect your AI.\n`,
@@ -56,6 +59,13 @@ if (cmd === 'setup') {
56
59
  await runSetup(rest, VERSION)
57
60
  const { closeFetch } = await import('../lib/diagnose.mjs')
58
61
  await closeFetch()
62
+ } else if (cmd === 'repair' || cmd === 'update') {
63
+ // Re-run setup at THIS version using the already-wired token (no token arg needed). Fixes a
64
+ // machine set up with an older version: re-pins MCP + hooks, reinstalls skills to the flat path.
65
+ const { runRepair } = await import('../lib/setup.mjs')
66
+ await runRepair(VERSION)
67
+ const { closeFetch } = await import('../lib/diagnose.mjs')
68
+ await closeFetch()
59
69
  } else if (cmd === 'doctor') {
60
70
  const { runDoctor } = await import('../lib/doctor.mjs')
61
71
  process.exitCode = await runDoctor()
@@ -72,6 +82,11 @@ if (cmd === 'setup') {
72
82
  await runCapture()
73
83
  const { closeFetch } = await import('../lib/diagnose.mjs')
74
84
  await closeFetch()
85
+ } else if (cmd === 'snapshot-context') {
86
+ const { runSnapshotContext } = await import('../lib/context_log.mjs')
87
+ process.exitCode = await runSnapshotContext()
88
+ const { closeFetch } = await import('../lib/diagnose.mjs')
89
+ await closeFetch()
75
90
  } else if (cmd === 'skills') {
76
91
  // Install / repair the managed Cortex skills repository. No network — exits naturally.
77
92
  const { runSkills } = await import('../lib/skills.mjs')
@@ -0,0 +1,80 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
2
+ import { homedir } from 'os'
3
+ import { join } from 'path'
4
+ import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
5
+
6
+ function resolveToken() {
7
+ if (process.env.CORTEX_TOKEN) return process.env.CORTEX_TOKEN
8
+ const claudeJson = join(homedir(), '.claude.json')
9
+ if (!existsSync(claudeJson)) return null
10
+ try {
11
+ const cfg = JSON.parse(readFileSync(claudeJson, 'utf8'))
12
+ return cfg?.mcpServers?.cortex?.env?.CORTEX_TOKEN ?? null
13
+ } catch {
14
+ return null
15
+ }
16
+ }
17
+
18
+ function ensureDir(path) {
19
+ if (!existsSync(path)) mkdirSync(path, { recursive: true })
20
+ }
21
+
22
+ function stamp(now = new Date()) {
23
+ const pad = (n) => String(n).padStart(2, '0')
24
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`
25
+ }
26
+
27
+ function snapshotDir() {
28
+ return join(homedir(), '.cortex', 'context-snapshots')
29
+ }
30
+
31
+ export async function runSnapshotContext() {
32
+ const out = (m) => process.stdout.write(m + '\n')
33
+ const token = resolveToken()
34
+ if (!token) {
35
+ out('Cortex: context snapshot skipped — no token found.')
36
+ return 0
37
+ }
38
+
39
+ const base = resolveBase(process.env.CORTEX_URL)
40
+ let res
41
+ try {
42
+ res = await fetchCortex(`${base}/api/mcp-context`, { headers: { Authorization: `Bearer ${token}` } })
43
+ } catch (e) {
44
+ out(`Cortex: context snapshot failed — ${e?.message ?? String(e)}`)
45
+ return 0
46
+ }
47
+
48
+ if (!res.ok) {
49
+ const body = await res.text()
50
+ out(`Cortex: context snapshot failed — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
51
+ return 0
52
+ }
53
+
54
+ const payload = await res.json().catch(() => ({}))
55
+ const context = typeof payload?.context === 'string' ? payload.context : ''
56
+ const dir = snapshotDir()
57
+ ensureDir(dir)
58
+
59
+ const capturedAt = new Date().toISOString()
60
+ const file = join(dir, `${stamp()}.md`)
61
+ const header = [
62
+ '# Cortex startup context snapshot',
63
+ `- Captured: ${capturedAt}`,
64
+ `- Source: ${base}/api/mcp-context`,
65
+ '',
66
+ ].join('\n')
67
+ const text = `${header}${context}\n`
68
+
69
+ writeFileSync(file, text)
70
+ writeFileSync(join(dir, 'latest.md'), text)
71
+ writeFileSync(join(dir, 'index.jsonl'), JSON.stringify({
72
+ captured_at: capturedAt,
73
+ file,
74
+ base,
75
+ chars: context.length,
76
+ }) + '\n', { flag: 'a' })
77
+
78
+ out(`Cortex: logged startup context → ${file}`)
79
+ return 0
80
+ }
package/lib/setup.mjs CHANGED
@@ -170,6 +170,14 @@ export async function runSetup(argv, version) {
170
170
  }
171
171
  sgrp.hooks.push({ type: 'command', command: skillsCmd })
172
172
 
173
+ const snapshotCmd = `npx -y ${spec} snapshot-context`
174
+ for (const sg of s.hooks.SessionStart) {
175
+ if (Array.isArray(sg.hooks)) {
176
+ sg.hooks = sg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? snapshot-context/.test(h.command ?? ''))
177
+ }
178
+ }
179
+ sgrp.hooks.push({ type: 'command', command: snapshotCmd })
180
+
173
181
  ensureDir(settingsJson)
174
182
  writeFileSync(settingsJson, JSON.stringify(s, null, 2))
175
183
  log(` ✓ Capture hook + status line → ${settingsJson}${bak ? ' (backup saved)' : ''}`)
@@ -207,3 +215,35 @@ export async function runSetup(argv, version) {
207
215
  log(` Console: ${base}`)
208
216
  log('')
209
217
  }
218
+
219
+ // Find the token already wired on this machine, so `repair` can re-run setup without re-pasting it.
220
+ // Checks Claude's config first, then Codex's config.toml.
221
+ export function readWiredToken() {
222
+ try {
223
+ const cfg = readJson(join(homedir(), '.claude.json'))
224
+ const t = cfg?.mcpServers?.cortex?.env?.CORTEX_TOKEN
225
+ if (t) return t
226
+ } catch { /* fall through */ }
227
+ try {
228
+ const toml = readFileSync(join(homedir(), '.codex', 'config.toml'), 'utf8')
229
+ const m = toml.match(/\[mcp_servers\.cortex\.env\][\s\S]*?CORTEX_TOKEN\s*=\s*"([^"]+)"/)
230
+ if (m) return m[1]
231
+ } catch { /* fall through */ }
232
+ return null
233
+ }
234
+
235
+ // `repair`: re-run the FULL setup at THIS version using the already-wired token. The one-command fix
236
+ // for a machine set up with an older version (e.g. when skills were installed to the old nested path,
237
+ // or the hooks are pinned to a stale version). No token argument needed.
238
+ export async function runRepair(version) {
239
+ const token = readWiredToken()
240
+ if (!token) {
241
+ process.stderr.write(
242
+ 'No existing Cortex token found in ~/.claude.json or ~/.codex/config.toml.\n' +
243
+ 'Run setup once with your token: npx -y @theronap/cortex-mcp setup <YOUR_TOKEN>\n',
244
+ )
245
+ process.exit(1)
246
+ }
247
+ process.stdout.write('Cortex repair — re-running setup with your existing token at this version…\n')
248
+ await runSetup([token], version)
249
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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": {