@theronap/cortex-mcp 0.9.47 → 0.9.49
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/editors/claude.mjs +33 -1
- package/lib/red_link_triage.mjs +30 -0
- package/lib/server.mjs +7 -13
- package/lib/uninstall.mjs +12 -0
- package/package.json +1 -1
package/lib/editors/claude.mjs
CHANGED
|
@@ -15,11 +15,34 @@ export function mergeClaudeMcp(existing, spec, token) {
|
|
|
15
15
|
return cfg
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** The Cortex tools every seat may run WITHOUT a permission prompt: the read surface, the live
|
|
19
|
+
* authoring core, and trivially-reversible maintenance. The contract this enforces: authoring is
|
|
20
|
+
* EXPECTED agent behavior — a page update must never stall on a yes/no dialog the user won't read
|
|
21
|
+
* (the ask-permission failure mode is how pages go stale). Safe because every page edit is
|
|
22
|
+
* CAS-protected + snapshotted (page_revisions → page_history/rollback_page).
|
|
23
|
+
* Deliberately EXCLUDED — these keep prompting: send_imessage (external side effect),
|
|
24
|
+
* set_page_privacy / set_record_privacy / grant_page_access (visibility widening — the
|
|
25
|
+
* unowned-project accessible-default sharp edge, 2026-07-02), rollback_page, decide_page_merge /
|
|
26
|
+
* decide_file_request / request_file / get_file, create_brain / set_active_brain, alias_page,
|
|
27
|
+
* set_writing_style. */
|
|
28
|
+
export const CORTEX_ALLOWED_TOOLS = [
|
|
29
|
+
// read surface
|
|
30
|
+
'grep', 'read_page', 'my_context', 'project_status', 'session_context', 'search_org',
|
|
31
|
+
'list_records', 'page_history', 'timeline_pull', 'my_brains', 'my_sessions', 'writing_style',
|
|
32
|
+
'code_graph_query', 'my_retier_notices', 'list_page_grants', 'file_requests', 'page_merge_requests',
|
|
33
|
+
// live authoring core
|
|
34
|
+
'authoring_context', 'author', 'log_session',
|
|
35
|
+
// routine, reversible maintenance
|
|
36
|
+
'set_page_validity', 'snooze_red_link', 'attribute_thread',
|
|
37
|
+
].map((t) => `mcp__cortex__${t}`)
|
|
38
|
+
|
|
18
39
|
/** Merge Cortex's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
|
|
19
40
|
* any prior cortex entry (old token/path/version) from each hook array before appending the current
|
|
20
41
|
* one — capture (Stop), status + skills-repair + snapshot-context (SessionStart), precompact
|
|
21
42
|
* (PreCompact). Commands carry NO inline token (each subcommand self-resolves it). Mirrors setup.mjs
|
|
22
|
-
* step 2 exactly; foreign hooks are never touched.
|
|
43
|
+
* step 2 exactly; foreign hooks are never touched. Also merges the CORTEX_ALLOWED_TOOLS permission
|
|
44
|
+
* allowlist — additive-only: a user's own allow entries (even extra mcp__cortex__* ones) are never
|
|
45
|
+
* removed, and `deny` is never touched (a user deny always beats our allow). */
|
|
23
46
|
export function mergeClaudeSettings(existing, spec) {
|
|
24
47
|
const s = existing && typeof existing === 'object' ? existing : {}
|
|
25
48
|
s.hooks = s.hooks ?? {}
|
|
@@ -69,6 +92,15 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
69
92
|
pgrp.hooks = pgrp.hooks ?? []
|
|
70
93
|
pgrp.hooks.push({ type: 'command', command: precompactCmd })
|
|
71
94
|
|
|
95
|
+
// Permissions — pre-authorize the read + authoring core so a page update never stalls on a
|
|
96
|
+
// permission prompt. Append-missing only (no filter-and-rebuild like the hooks above): removals
|
|
97
|
+
// ship via uninstall, and rebuilding would delete allows the user added by hand.
|
|
98
|
+
s.permissions = s.permissions && typeof s.permissions === 'object' ? s.permissions : {}
|
|
99
|
+
s.permissions.allow = Array.isArray(s.permissions.allow) ? s.permissions.allow : []
|
|
100
|
+
for (const rule of CORTEX_ALLOWED_TOOLS) {
|
|
101
|
+
if (!s.permissions.allow.includes(rule)) s.permissions.allow.push(rule)
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
return s
|
|
73
105
|
}
|
|
74
106
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Red-link triage rendering (Mechanism 2) — the text an agent sees on a read_page miss.
|
|
2
|
+
//
|
|
3
|
+
// Standalone + dependency-free (like grep_cli.mjs) so it unit-tests without pulling the MCP SDK in.
|
|
4
|
+
// server.mjs owns the fetch; this owns the wording.
|
|
5
|
+
//
|
|
6
|
+
// The demoted arm is the one that matters. A superseded/historical page stays greppable
|
|
7
|
+
// (grep_brain_sections applies no validity filter) but read_page won't serve it (authored_page_tiers
|
|
8
|
+
// filters validity='current'), so an agent that greps a hit and then read_page's it lands here — and
|
|
9
|
+
// used to be told "no page yet, author it now". Authoring is pre-authorized, so the compliant next step
|
|
10
|
+
// was to overwrite a page a human deliberately retired, with nothing to catch it. Absence invites
|
|
11
|
+
// authoring; a demotion forbids it.
|
|
12
|
+
//
|
|
13
|
+
// Keyed off the ADDITIVE `t.demoted` flag, checked BEFORE category — never off a new category value. A
|
|
14
|
+
// server predating the flag omits it and every arm behaves exactly as before, so client and server can
|
|
15
|
+
// ship in either order.
|
|
16
|
+
|
|
17
|
+
// PURE: triage payload → miss text.
|
|
18
|
+
export function renderTriage(t, name) {
|
|
19
|
+
const refs = t.tracked
|
|
20
|
+
? ` It's referenced by ${t.refCount} page${t.refCount === 1 ? '' : 's'}${t.isSteward ? ' and is routed to YOU as its most-likely steward' : ''}.`
|
|
21
|
+
: ''
|
|
22
|
+
const aliasHint = `if it's really an existing page under another title, \`grep "${name}"\` to find it, then \`alias_page name="${name}" target_name="<that page>"\``
|
|
23
|
+
if (t.demoted) {
|
|
24
|
+
return `\n\n[[${name}]] EXISTS but is marked ${t.validity ?? 'superseded'} — it was authored and then deliberately retired, so read_page (which serves only current pages) will not show it. It is NOT missing.${refs} Do NOT author over it: that would silently overwrite a decision someone made on purpose. Read it with \`page_history "${name}"\` then \`read_page "${name}"\` with a version. If it genuinely should be live again, revive it deliberately with \`set_page_validity\`.`
|
|
25
|
+
}
|
|
26
|
+
if (t.category === 'node') {
|
|
27
|
+
return `\n\n[[${name}]] is a wanted page — a ${t.isPerson ? 'person' : 'node'} exists but has no page yet.${refs} Either author it now with \`author\`, or ${aliasHint}.`
|
|
28
|
+
}
|
|
29
|
+
return `\n\n"${name}" isn't authored anywhere yet.${refs} Either author a new page with \`author\`, or ${aliasHint}.`
|
|
30
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -8,26 +8,20 @@ import { createHash, randomUUID } from 'crypto'
|
|
|
8
8
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
9
9
|
import { runSendImessage } from './imessage_send.mjs'
|
|
10
10
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
11
|
+
import { renderTriage } from './red_link_triage.mjs'
|
|
11
12
|
import { runCodeGraphQuery } from './code_graph_cli.mjs'
|
|
12
13
|
|
|
13
14
|
// Reactive red-link triage (Mechanism 2). On a read_page miss, ask the server whether the name is a
|
|
14
|
-
// tracked wanted page
|
|
15
|
-
//
|
|
15
|
+
// tracked wanted page, whether a bare node exists for it, and whether it's a deliberately demoted page,
|
|
16
|
+
// and turn that into an actionable prompt. Wording lives in ./red_link_triage.mjs (pure + tested).
|
|
17
|
+
// Returns '' on any error so a miss never gets worse.
|
|
16
18
|
async function redLinkTriage(BASE, TOKEN, name) {
|
|
17
19
|
try {
|
|
18
20
|
const r = await fetchCortex(`${BASE}/api/brain/red-link?name=${encodeURIComponent(name)}`, {
|
|
19
21
|
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
20
22
|
})
|
|
21
23
|
if (!r.ok) return ''
|
|
22
|
-
|
|
23
|
-
const refs = t.tracked
|
|
24
|
-
? ` It's referenced by ${t.refCount} page${t.refCount === 1 ? '' : 's'}${t.isSteward ? ' and is routed to YOU as its most-likely steward' : ''}.`
|
|
25
|
-
: ''
|
|
26
|
-
const aliasHint = `if it's really an existing page under another title, \`grep "${name}"\` to find it, then \`alias_page name="${name}" target_name="<that page>"\``
|
|
27
|
-
if (t.category === 'node') {
|
|
28
|
-
return `\n\n[[${name}]] is a wanted page — a ${t.isPerson ? 'person' : 'node'} exists but has no page yet.${refs} Either author it now with \`author\`, or ${aliasHint}.`
|
|
29
|
-
}
|
|
30
|
-
return `\n\n"${name}" isn't authored anywhere yet.${refs} Either author a new page with \`author\`, or ${aliasHint}.`
|
|
24
|
+
return renderTriage(await r.json(), name)
|
|
31
25
|
} catch {
|
|
32
26
|
return ''
|
|
33
27
|
}
|
|
@@ -500,7 +494,7 @@ export async function runServer(version) {
|
|
|
500
494
|
const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
|
|
501
495
|
return [head, t.summary, secs].filter(Boolean).join('\n')
|
|
502
496
|
})
|
|
503
|
-
let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it.`
|
|
497
|
+
let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it. Re-authoring is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it.`
|
|
504
498
|
// slice 4: when the page carries identifier stamps, the history projection is one flag away.
|
|
505
499
|
const allBody = m.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
|
|
506
500
|
const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
|
|
@@ -1236,7 +1230,7 @@ export async function runServer(version) {
|
|
|
1236
1230
|
{
|
|
1237
1231
|
title: 'Author a wiki node (live, while it is hot)',
|
|
1238
1232
|
description:
|
|
1239
|
-
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log).',
|
|
1233
|
+
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated.',
|
|
1240
1234
|
inputSchema: {
|
|
1241
1235
|
kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
|
|
1242
1236
|
name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Cortex" or "Theron Peterson"'),
|
package/lib/uninstall.mjs
CHANGED
|
@@ -58,6 +58,18 @@ export function runUninstall(argv = []) {
|
|
|
58
58
|
if (s.hooks[evt].length === 0) delete s.hooks[evt]
|
|
59
59
|
}
|
|
60
60
|
if (changed) act(` - Stop/SessionStart/PreCompact cortex hooks ← ${SETTINGS}`)
|
|
61
|
+
// cortex permission allowlist (setup wires CORTEX_ALLOWED_TOOLS so authoring never stalls on a
|
|
62
|
+
// prompt) — strip every mcp__cortex__* allow rule; the user's deny list is never touched.
|
|
63
|
+
if (Array.isArray(s.permissions?.allow)) {
|
|
64
|
+
const before = s.permissions.allow.length
|
|
65
|
+
s.permissions.allow = s.permissions.allow.filter((r) => !/^mcp__cortex__/.test(String(r)))
|
|
66
|
+
if (s.permissions.allow.length !== before) {
|
|
67
|
+
changed = true
|
|
68
|
+
act(` - mcp__cortex__* permission allow rules ← ${SETTINGS}`)
|
|
69
|
+
if (s.permissions.allow.length === 0) delete s.permissions.allow
|
|
70
|
+
if (Object.keys(s.permissions).length === 0) delete s.permissions
|
|
71
|
+
}
|
|
72
|
+
}
|
|
61
73
|
return changed
|
|
62
74
|
}, write)
|
|
63
75
|
|