@theronap/cortex-mcp 0.4.6 → 0.7.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/bin/cortex-mcp.mjs +6 -1
- package/lib/server.mjs +123 -5
- package/lib/setup.mjs +20 -1
- package/lib/skills.mjs +121 -0
- package/package.json +3 -2
- package/skills/log/SKILL.md +50 -0
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Get your token from the Cortex console → Connect your AI.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const VERSION = '0.
|
|
19
|
+
const VERSION = '0.6.0'
|
|
20
20
|
const cmd = process.argv[2]
|
|
21
21
|
const rest = process.argv.slice(3)
|
|
22
22
|
|
|
@@ -36,6 +36,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
36
36
|
` setup <token> wire MCP server + capture hook into ~/.claude config\n` +
|
|
37
37
|
` doctor live health check — confirm your token works (no restart needed)\n` +
|
|
38
38
|
` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
|
|
39
|
+
` skills install/repair the managed Cortex skills (also wired by setup)\n` +
|
|
39
40
|
` capture Stop-hook capturer (invoked by Claude Code)\n` +
|
|
40
41
|
` (no args) run the MCP server (used by your Claude config)\n\n` +
|
|
41
42
|
`Get your token from the Cortex console → Connect your AI.\n`,
|
|
@@ -71,6 +72,10 @@ if (cmd === 'setup') {
|
|
|
71
72
|
await runCapture()
|
|
72
73
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
73
74
|
await closeFetch()
|
|
75
|
+
} else if (cmd === 'skills') {
|
|
76
|
+
// Install / repair the managed Cortex skills repository. No network — exits naturally.
|
|
77
|
+
const { runSkills } = await import('../lib/skills.mjs')
|
|
78
|
+
process.exitCode = await runSkills(rest)
|
|
74
79
|
} else {
|
|
75
80
|
// Default: run the MCP server (stays alive; never exits).
|
|
76
81
|
const { runServer } = await import('../lib/server.mjs')
|
package/lib/server.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
+
import { writeFileSync, mkdirSync } from 'fs'
|
|
5
|
+
import { homedir } from 'os'
|
|
6
|
+
import { join } from 'path'
|
|
4
7
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
5
8
|
|
|
6
9
|
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
@@ -49,13 +52,32 @@ export async function runServer(version) {
|
|
|
49
52
|
{
|
|
50
53
|
title: 'Search the org',
|
|
51
54
|
description: 'Search your visible work activity and projects by keyword.',
|
|
52
|
-
inputSchema: { query: z.string().describe('keyword to search for') },
|
|
55
|
+
inputSchema: { query: z.string().describe('keyword to search for — people or work activity') },
|
|
53
56
|
},
|
|
54
57
|
async ({ query }) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
58
|
+
// Real search via /api/search: records (RLS-scoped retrieve) + graph entities (people),
|
|
59
|
+
// instead of substring-grepping the cached context. People were previously invisible to search.
|
|
60
|
+
const res = await fetchCortex(`${BASE}/api/search?q=${encodeURIComponent(query)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const body = await res.text()
|
|
63
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
64
|
+
}
|
|
65
|
+
const { people = [], records = [] } = await res.json()
|
|
66
|
+
if (!people.length && !records.length) return { content: [{ type: 'text', text: `No visible results for "${query}".` }] }
|
|
67
|
+
const lines = []
|
|
68
|
+
if (people.length) {
|
|
69
|
+
lines.push('People:')
|
|
70
|
+
for (const p of people) {
|
|
71
|
+
const meta = [p.title, p.company].filter(Boolean).join(', ')
|
|
72
|
+
lines.push(`- ${p.name}${meta ? ` (${meta})` : ''} — mentioned in ${p.mentions} record${p.mentions === 1 ? '' : 's'}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (records.length) {
|
|
76
|
+
if (lines.length) lines.push('')
|
|
77
|
+
lines.push('Activity:')
|
|
78
|
+
for (const r of records) lines.push(`- [${r.source}] ${r.title}${r.project ? ` (${r.project})` : ''}`)
|
|
79
|
+
}
|
|
80
|
+
return { content: [{ type: 'text', text: `Results for "${query}":\n${lines.join('\n')}` }] }
|
|
59
81
|
},
|
|
60
82
|
)
|
|
61
83
|
|
|
@@ -160,5 +182,101 @@ export async function runServer(version) {
|
|
|
160
182
|
},
|
|
161
183
|
)
|
|
162
184
|
|
|
185
|
+
// ── File requests: ask the owner for a record's FULL original (lives on their machine) ──
|
|
186
|
+
const fail = (verb, res) => async () =>
|
|
187
|
+
({ content: [{ type: 'text', text: `Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}` }] })
|
|
188
|
+
|
|
189
|
+
server.registerTool(
|
|
190
|
+
'request_file',
|
|
191
|
+
{
|
|
192
|
+
title: 'Request the full original of a record',
|
|
193
|
+
description: 'You have a record\'s SUMMARY but want the full original file (it lives on the owner\'s machine). This asks the owner to share it; once they approve, fetch it with get_file. Get record_id from a story Sources list or the "id:" on a my_context activity line.',
|
|
194
|
+
inputSchema: { record_id: z.string().describe('the record id (uuid)') },
|
|
195
|
+
},
|
|
196
|
+
async ({ record_id }) => {
|
|
197
|
+
let res
|
|
198
|
+
try {
|
|
199
|
+
res = await fetchCortex(`${BASE}/api/file-requests`, {
|
|
200
|
+
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
201
|
+
body: JSON.stringify({ recordId: record_id }),
|
|
202
|
+
})
|
|
203
|
+
} catch (e) { return { content: [{ type: 'text', text: `Could not request: ${e.message}` }] } }
|
|
204
|
+
if (res.status === 409) {
|
|
205
|
+
const j = await res.json().catch(() => ({}))
|
|
206
|
+
if (j.error === 'you_own_it') return { content: [{ type: 'text', text: `You own this record — open the original directly:\n${j.uri}` }] }
|
|
207
|
+
if (j.error === 'no_original') return { content: [{ type: 'text', text: 'This record has no retrievable original.' }] }
|
|
208
|
+
}
|
|
209
|
+
if (!res.ok) return (await fail('request the file', res))()
|
|
210
|
+
const { request } = await res.json()
|
|
211
|
+
return { content: [{ type: 'text', text: `Requested. The owner will be asked to approve. Check status with file_requests; once approved + fulfilled, run get_file ${request.id}.` }] }
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
server.registerTool(
|
|
216
|
+
'file_requests',
|
|
217
|
+
{
|
|
218
|
+
title: 'My file requests + approval inbox',
|
|
219
|
+
description: 'Lists file requests you made (with status) and requests from others awaiting YOUR approval. Approve/deny with decide_file_request; fetch an approved+fulfilled file with get_file.',
|
|
220
|
+
inputSchema: {},
|
|
221
|
+
},
|
|
222
|
+
async () => {
|
|
223
|
+
let res
|
|
224
|
+
try { res = await fetchCortex(`${BASE}/api/file-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
225
|
+
catch (e) { return { content: [{ type: 'text', text: `Could not load file requests: ${e.message}` }] } }
|
|
226
|
+
if (!res.ok) return (await fail('load file requests', res))()
|
|
227
|
+
const { mine, inbox } = await res.json()
|
|
228
|
+
const parts = []
|
|
229
|
+
if (inbox?.length) parts.push('Awaiting YOUR approval:\n' + inbox.map((r) => ` - [${r.id}] ${r.requester} wants "${r.title}" (${r.source}) → decide_file_request ${r.id}`).join('\n'))
|
|
230
|
+
if (mine?.length) parts.push('Your requests:\n' + mine.map((r) => ` - [${r.id}] "${r.title}" — ${r.status}${r.status === 'fulfilled' ? ` → get_file ${r.id}` : ''}`).join('\n'))
|
|
231
|
+
return { content: [{ type: 'text', text: parts.length ? parts.join('\n\n') : 'No file requests.' }] }
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
server.registerTool(
|
|
236
|
+
'decide_file_request',
|
|
237
|
+
{
|
|
238
|
+
title: 'Approve or deny a file request',
|
|
239
|
+
description: 'As the OWNER of a record, approve or deny someone\'s request for its full original. On approve, your Cortex desktop app fetches + shares the file. Get request_id from file_requests.',
|
|
240
|
+
inputSchema: { request_id: z.string(), decision: z.enum(['approve', 'deny']) },
|
|
241
|
+
},
|
|
242
|
+
async ({ request_id, decision }) => {
|
|
243
|
+
let res
|
|
244
|
+
try {
|
|
245
|
+
res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/decide`, {
|
|
246
|
+
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ action: decision }),
|
|
248
|
+
})
|
|
249
|
+
} catch (e) { return { content: [{ type: 'text', text: `Could not decide: ${e.message}` }] } }
|
|
250
|
+
if (!res.ok) return (await fail('decide', res))()
|
|
251
|
+
return { content: [{ type: 'text', text: `Request ${decision === 'approve' ? 'approved' : 'denied'}.` }] }
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
server.registerTool(
|
|
256
|
+
'get_file',
|
|
257
|
+
{
|
|
258
|
+
title: 'Download an approved file',
|
|
259
|
+
description: 'Download the full original for a request the owner approved + fulfilled. Saves it under ~/Downloads/cortex/ and returns the local path.',
|
|
260
|
+
inputSchema: { request_id: z.string() },
|
|
261
|
+
},
|
|
262
|
+
async ({ request_id }) => {
|
|
263
|
+
let res
|
|
264
|
+
try { res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/download`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
265
|
+
catch (e) { return { content: [{ type: 'text', text: `Could not download: ${e.message}` }] } }
|
|
266
|
+
if (res.status === 409) return { content: [{ type: 'text', text: 'Not ready — the owner hasn\'t fulfilled this yet. Try again after they approve.' }] }
|
|
267
|
+
if (res.status === 410) return { content: [{ type: 'text', text: 'This file has expired (downloads are available for 7 days). Request it again.' }] }
|
|
268
|
+
if (!res.ok) return (await fail('download', res))()
|
|
269
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
270
|
+
const cd = res.headers.get('content-disposition') ?? ''
|
|
271
|
+
const m = /filename="([^"]+)"/.exec(cd)
|
|
272
|
+
const name = (m ? m[1] : `cortex-file-${request_id}`).replace(/[^\w.\- ]/g, '_')
|
|
273
|
+
const dir = join(homedir(), 'Downloads', 'cortex')
|
|
274
|
+
mkdirSync(dir, { recursive: true })
|
|
275
|
+
const path = join(dir, name)
|
|
276
|
+
writeFileSync(path, buf)
|
|
277
|
+
return { content: [{ type: 'text', text: `Saved the full original to ${path} (${buf.length} bytes).` }] }
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
163
281
|
await server.connect(new StdioServerTransport())
|
|
164
282
|
}
|
package/lib/setup.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from
|
|
|
2
2
|
import { homedir } from 'os'
|
|
3
3
|
import { join, dirname } from 'path'
|
|
4
4
|
import { checkToken, resolveBase } from './diagnose.mjs'
|
|
5
|
+
import { installSkills } from './skills.mjs'
|
|
5
6
|
|
|
6
7
|
// One-command employee onboarding. Wires both:
|
|
7
8
|
// 1. ~/.claude.json → the cortex MCP server (context-serving)
|
|
@@ -115,6 +116,16 @@ export async function runSetup(argv, version) {
|
|
|
115
116
|
sgrp.hooks = sgrp.hooks ?? []
|
|
116
117
|
sgrp.hooks.push({ type: 'command', command: statusCmd })
|
|
117
118
|
|
|
119
|
+
// Skills self-heal: every session start, restore any drifted managed Cortex skill (quiet — only
|
|
120
|
+
// speaks up if it actually changed something). This is what makes the core skills "inalterable".
|
|
121
|
+
const skillsCmd = `npx -y ${spec} skills --repair --quiet`
|
|
122
|
+
for (const sg of s.hooks.SessionStart) {
|
|
123
|
+
if (Array.isArray(sg.hooks)) {
|
|
124
|
+
sg.hooks = sg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? skills/.test(h.command ?? ''))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
sgrp.hooks.push({ type: 'command', command: skillsCmd })
|
|
128
|
+
|
|
118
129
|
ensureDir(settingsJson)
|
|
119
130
|
writeFileSync(settingsJson, JSON.stringify(s, null, 2))
|
|
120
131
|
log(` ✓ Capture hook + status line → ${settingsJson}${bak ? ' (backup saved)' : ''}`)
|
|
@@ -123,7 +134,15 @@ export async function runSetup(argv, version) {
|
|
|
123
134
|
process.exit(1)
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
// ── 3.
|
|
137
|
+
// ── 3. Managed skills repository (~/.claude/skills/cortex/) ──────────────
|
|
138
|
+
try {
|
|
139
|
+
installSkills({ quiet: false })
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Non-fatal: a skills hiccup must never block the core connection. Repair runs each session.
|
|
142
|
+
log(` ⚠ skills install skipped: ${e.message} (will retry on next session)`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 4. Self-verify — writing config proves "files written", NOT "connection works".
|
|
127
146
|
// Actually call the API so a bad/expired token is caught HERE, not 40 minutes into debugging.
|
|
128
147
|
log('')
|
|
129
148
|
log('Verifying your token against Cortex…')
|
package/lib/skills.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, copyFileSync } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
// Managed Cortex skills repository.
|
|
7
|
+
//
|
|
8
|
+
// Cortex owns a namespace under the user's Claude skills dir: ~/.claude/skills/cortex/
|
|
9
|
+
// cortex/
|
|
10
|
+
// README.md ← explains the namespace is Cortex-managed
|
|
11
|
+
// .cortex-manifest.json ← what's managed + content hashes (machine-readable)
|
|
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)
|
|
14
|
+
//
|
|
15
|
+
// "Inalterable" in practice: the bundled core skills are the source of truth. On every install AND
|
|
16
|
+
// on every session start (the SessionStart --repair hook), any core skill whose on-disk content
|
|
17
|
+
// drifted from the bundled source is RESTORED — the user's version is backed up to <file>.user-bak
|
|
18
|
+
// first, so nothing is lost, but the canonical skill always wins. Idempotent: identical content is a
|
|
19
|
+
// no-op (no backup, no write, no churn).
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const BUNDLED = join(HERE, '..', 'skills') // packages/cortex-mcp/skills/<name>/SKILL.md
|
|
23
|
+
|
|
24
|
+
// djb2 — tiny, dependency-free content fingerprint for the manifest (drift detection, not security).
|
|
25
|
+
function hash(s) {
|
|
26
|
+
let h = 5381
|
|
27
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0
|
|
28
|
+
return h.toString(16)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureDir(path) {
|
|
32
|
+
if (!existsSync(path)) mkdirSync(path, { recursive: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Discover bundled core skills: each subdir of skills/ that contains a SKILL.md.
|
|
36
|
+
function bundledSkills() {
|
|
37
|
+
if (!existsSync(BUNDLED)) return []
|
|
38
|
+
return readdirSync(BUNDLED, { withFileTypes: true })
|
|
39
|
+
.filter((d) => d.isDirectory() && existsSync(join(BUNDLED, d.name, 'SKILL.md')))
|
|
40
|
+
.map((d) => ({ name: d.name, src: join(BUNDLED, d.name, 'SKILL.md') }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const README = `# Cortex-managed skills
|
|
44
|
+
|
|
45
|
+
This folder is owned by Cortex (\`@theronap/cortex-mcp\`). Skills under \`core/\` are **mandatory and
|
|
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()
|
|
68
|
+
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
|
+
const manifest = { managed: [], updated_by: 'cortex-mcp', skills: {} }
|
|
78
|
+
|
|
79
|
+
for (const sk of skills) {
|
|
80
|
+
const source = readFileSync(sk.src, 'utf8')
|
|
81
|
+
const dest = join(coreDir, sk.name, 'SKILL.md')
|
|
82
|
+
manifest.managed.push(`core/${sk.name}/SKILL.md`)
|
|
83
|
+
manifest.skills[sk.name] = hash(source)
|
|
84
|
+
|
|
85
|
+
if (!existsSync(dest)) {
|
|
86
|
+
ensureDir(dirname(dest))
|
|
87
|
+
writeFileSync(dest, source)
|
|
88
|
+
result.installed.push(sk.name)
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
const current = readFileSync(dest, 'utf8')
|
|
92
|
+
if (current === source) { result.unchanged.push(sk.name); continue }
|
|
93
|
+
|
|
94
|
+
// Drift: preserve the user's version, then restore canonical.
|
|
95
|
+
try { copyFileSync(dest, `${dest}.user-bak`) } catch { /* best-effort backup */ }
|
|
96
|
+
writeFileSync(dest, source)
|
|
97
|
+
result.repaired.push(sk.name)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeFileSync(join(root, '.cortex-manifest.json'), JSON.stringify(manifest, null, 2))
|
|
101
|
+
|
|
102
|
+
if (result.installed.length) log(` ✓ Cortex skills installed: ${result.installed.join(', ')} → ${coreDir}`)
|
|
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)) {
|
|
105
|
+
// SessionStart surfaces one line in Claude Code so a silent self-heal isn't invisible.
|
|
106
|
+
process.stdout.write(`Cortex: synced ${result.installed.length + result.repaired.length} managed skill(s).\n`)
|
|
107
|
+
}
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// CLI entry: `cortex-mcp skills [--repair] [--quiet]`. (--repair and plain install are the same
|
|
112
|
+
// idempotent operation; --repair is just the name the SessionStart hook uses for intent.)
|
|
113
|
+
export async function runSkills(argv = []) {
|
|
114
|
+
const quiet = argv.includes('--quiet')
|
|
115
|
+
if (!quiet) process.stdout.write('\nCortex skills — installing managed repository…\n')
|
|
116
|
+
const r = installSkills({ quiet })
|
|
117
|
+
if (!quiet && !r.installed.length && !r.repaired.length) {
|
|
118
|
+
process.stdout.write(` ✓ Up to date (${r.unchanged.join(', ') || 'none'}).\n`)
|
|
119
|
+
}
|
|
120
|
+
return 0
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theronap/cortex-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"lib"
|
|
11
|
+
"lib",
|
|
12
|
+
"skills"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=18"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cortex-log
|
|
3
|
+
description: Close out a work session into Cortex — summarize what happened, confirm it reached the org, and surface anything teammates should know. Run at or near the end of any working session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **Cortex-managed skill.** This file is installed and kept up to date by Cortex. Local edits are
|
|
7
|
+
> restored on the next session (a backup of your version is saved alongside). Don't rely on changes here.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
At the end of a Claude Code session, or after finishing a meaningful phase of work. Cortex already
|
|
12
|
+
captures your sessions automatically in the background — this skill is the *deliberate* close-out: it
|
|
13
|
+
produces a clean, structured summary and confirms the org received it.
|
|
14
|
+
|
|
15
|
+
## Inputs
|
|
16
|
+
|
|
17
|
+
No arguments. Read the conversation context.
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. **Summarize the session** — what was worked on, what was decided, what changed. Be concrete: name
|
|
22
|
+
the projects, files, and people involved.
|
|
23
|
+
2. **Surface org-relevant signal** — blockers, decisions, handoffs, and anyone you coordinated with.
|
|
24
|
+
These are the things a teammate or manager would want to know without reading the whole transcript.
|
|
25
|
+
3. **Confirm capture** — check the Cortex MCP is connected (`my_context` returns your context). Your
|
|
26
|
+
session flows to the org automatically at session end via the capture hook; if `my_context` errors,
|
|
27
|
+
tell the user their session may not be captured and to run `npx -y @theronap/cortex-mcp doctor`.
|
|
28
|
+
4. **Flag privacy** — if any record from this session should be confidential, note it so the user can
|
|
29
|
+
mark it (`set_record_privacy`). Default is org-visible under access rules.
|
|
30
|
+
|
|
31
|
+
## Output
|
|
32
|
+
|
|
33
|
+
A short structured summary:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
## Session summary
|
|
37
|
+
|
|
38
|
+
**Worked on:** brief description
|
|
39
|
+
**Decisions:** decision 1; decision 2
|
|
40
|
+
**Open / blocked:** anything unresolved or waiting on someone
|
|
41
|
+
**Coordinated with:** people involved
|
|
42
|
+
**Capture:** ✅ flowing to Cortex (or ⚠ not connected — run doctor)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Safety rules
|
|
46
|
+
|
|
47
|
+
- This skill only summarizes and reports. It never sends external messages, never deletes anything,
|
|
48
|
+
and never changes access on a record without the user explicitly asking.
|
|
49
|
+
- Raw session text stays on this machine — only the summary-grade record reaches the org, scoped by
|
|
50
|
+
access rules.
|