@theronap/cortex-mcp 0.9.46 → 0.9.47
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/grep_cli.mjs +8 -1
- package/lib/server.mjs +82 -2
- package/package.json +1 -1
package/lib/grep_cli.mjs
CHANGED
|
@@ -33,7 +33,14 @@ export function parseGrepArgs(argv = []) {
|
|
|
33
33
|
// Pure: render a /api/grep payload to readable ASCII text.
|
|
34
34
|
export function formatGrepHits(payload, query) {
|
|
35
35
|
const hits = (payload && payload.hits) || []
|
|
36
|
-
if (!hits.length)
|
|
36
|
+
if (!hits.length) {
|
|
37
|
+
// Reactive red-link hint — ONLY when the query looks like a page NAME (short, no code/operators), so
|
|
38
|
+
// code and typo searches don't get nagged. read_page carries the full triage (node / new / alias).
|
|
39
|
+
const nameish = /^[\w .'-]{2,40}$/.test(query) && query.split(/\s+/).length <= 5
|
|
40
|
+
return nameish
|
|
41
|
+
? `No matches for "${query}". If you expected a page here, it may be an unauthored red-link — \`read_page "${query}"\` to triage it (author it, or alias it to an existing page).`
|
|
42
|
+
: `No matches for "${query}".`
|
|
43
|
+
}
|
|
37
44
|
const lines = [`${hits.length} match${hits.length === 1 ? '' : 'es'} for "${query}":`, '']
|
|
38
45
|
for (const h of hits) {
|
|
39
46
|
const head = h.heading ? ` > ${h.heading}` : ''
|
package/lib/server.mjs
CHANGED
|
@@ -10,6 +10,29 @@ import { runSendImessage } from './imessage_send.mjs'
|
|
|
10
10
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
11
11
|
import { runCodeGraphQuery } from './code_graph_cli.mjs'
|
|
12
12
|
|
|
13
|
+
// Reactive red-link triage (Mechanism 2). On a read_page miss, ask the server whether the name is a
|
|
14
|
+
// tracked wanted page and whether a bare node exists for it, and turn that into an actionable 3-way
|
|
15
|
+
// prompt (author-for-node / author-new / alias). Returns '' on any error so a miss never gets worse.
|
|
16
|
+
async function redLinkTriage(BASE, TOKEN, name) {
|
|
17
|
+
try {
|
|
18
|
+
const r = await fetchCortex(`${BASE}/api/brain/red-link?name=${encodeURIComponent(name)}`, {
|
|
19
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
20
|
+
})
|
|
21
|
+
if (!r.ok) return ''
|
|
22
|
+
const t = await r.json()
|
|
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}.`
|
|
31
|
+
} catch {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
14
37
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
15
38
|
|
|
@@ -444,7 +467,7 @@ export async function runServer(version) {
|
|
|
444
467
|
return { content: [{ type: 'text', text: `Could not read "${name}": ${e.message}` }] }
|
|
445
468
|
}
|
|
446
469
|
if (res.status === 404) {
|
|
447
|
-
return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org)
|
|
470
|
+
return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
|
|
448
471
|
}
|
|
449
472
|
if (!res.ok) {
|
|
450
473
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
@@ -461,7 +484,7 @@ export async function runServer(version) {
|
|
|
461
484
|
matches = [{ brain: null, authored: true, ref: page.ref, title: page.title, tiers: page.tiers }]
|
|
462
485
|
}
|
|
463
486
|
if (!matches.length) {
|
|
464
|
-
return { content: [{ type: 'text', text: `No authored ${k} page named "${name}" in any of your brains. If it's a person or team, pass kind (person/org)
|
|
487
|
+
return { content: [{ type: 'text', text: `No authored ${k} page named "${name}" in any of your brains. If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
|
|
465
488
|
}
|
|
466
489
|
const day = (d) => (d ? String(d).slice(0, 10) : '')
|
|
467
490
|
const renderMatch = (m, tagBrain) => {
|
|
@@ -812,6 +835,63 @@ export async function runServer(version) {
|
|
|
812
835
|
},
|
|
813
836
|
)
|
|
814
837
|
|
|
838
|
+
server.registerTool(
|
|
839
|
+
'alias_page',
|
|
840
|
+
{
|
|
841
|
+
title: 'Point a wanted name at an existing page',
|
|
842
|
+
description: 'Record that a red-link — a [[Name]] referenced in the wiki but never authored — actually MEANS an existing authored page under a different title. After this, read_page and [[links]] for that name resolve to the target page, and the name leaves the org\'s wanted-page backlog. Use this when read_page says a name is a wanted page but you recognize it as an existing page (e.g. [[tto]] -> "BYU TTO — Technology Transfer Office"). To CREATE a genuinely new page instead, use `author`.',
|
|
843
|
+
inputSchema: {
|
|
844
|
+
name: z.string().describe('the wanted [[Name]] to redirect (the red-link)'),
|
|
845
|
+
target_name: z.string().describe('the exact title of the existing authored page it should resolve to'),
|
|
846
|
+
target_kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('disambiguate the target if two pages share a title'),
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
async ({ name, target_name, target_kind }) => {
|
|
850
|
+
let res
|
|
851
|
+
try {
|
|
852
|
+
res = await fetchCortex(`${BASE}/api/brain/alias`, {
|
|
853
|
+
method: 'POST',
|
|
854
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
855
|
+
body: JSON.stringify({ name, target_name, ...(target_kind ? { target_kind } : {}) }),
|
|
856
|
+
})
|
|
857
|
+
} catch (e) {
|
|
858
|
+
return { content: [{ type: 'text', text: `Could not alias: ${e.message}` }] }
|
|
859
|
+
}
|
|
860
|
+
const out = await res.json().catch(() => null)
|
|
861
|
+
if (!res.ok) return { content: [{ type: 'text', text: `Could not alias "${name}": ${out?.error ?? res.status}` }] }
|
|
862
|
+
if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
|
|
863
|
+
return { content: [{ type: 'text', text: `Done — [[${out.alias}]] now resolves to "${out.target}" (${out.target_kind}). It's out of the wanted-page backlog.` }] }
|
|
864
|
+
},
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
server.registerTool(
|
|
868
|
+
'snooze_red_link',
|
|
869
|
+
{
|
|
870
|
+
title: 'Defer a wanted page routed to you',
|
|
871
|
+
description: 'Stop a wanted page (a red-link the org routed to you as its most-likely steward) from surfacing in your context for a while. Use when you can\'t author it right now but it is genuinely yours to write. It comes back after the snooze passes. To dismiss it permanently, author it (`author`) or alias it to an existing page (`alias_page`).',
|
|
872
|
+
inputSchema: {
|
|
873
|
+
name: z.string().describe('the wanted page name to snooze (as shown in "Pages the org needs you to author")'),
|
|
874
|
+
days: z.number().int().positive().optional().describe('how many days to defer (default 7)'),
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
async ({ name, days }) => {
|
|
878
|
+
let res
|
|
879
|
+
try {
|
|
880
|
+
res = await fetchCortex(`${BASE}/api/brain/red-link/snooze`, {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
883
|
+
body: JSON.stringify({ name, ...(days ? { days } : {}) }),
|
|
884
|
+
})
|
|
885
|
+
} catch (e) {
|
|
886
|
+
return { content: [{ type: 'text', text: `Could not snooze: ${e.message}` }] }
|
|
887
|
+
}
|
|
888
|
+
const out = await res.json().catch(() => null)
|
|
889
|
+
if (!res.ok) return { content: [{ type: 'text', text: `Could not snooze "${name}": ${out?.error ?? res.status}` }] }
|
|
890
|
+
if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
|
|
891
|
+
return { content: [{ type: 'text', text: `Snoozed "${out.name}" for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
|
|
892
|
+
},
|
|
893
|
+
)
|
|
894
|
+
|
|
815
895
|
server.registerTool(
|
|
816
896
|
'set_page_privacy',
|
|
817
897
|
{
|