@theronap/cortex-mcp 0.9.157 → 0.9.159
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/server.mjs +115 -4
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -193,7 +193,16 @@ export async function runServer(version) {
|
|
|
193
193
|
// seat runs — session-ping carried liveness only. That is why every version gate in this
|
|
194
194
|
// ADR was unobservable. Additive and forward-compatible: the route destructures sessionKey
|
|
195
195
|
// and cwd and ignores the rest, so this is inert until a column exists to store it.
|
|
196
|
-
|
|
196
|
+
// repo (ADR-0066 §7): the repo this session is WORKING IN, so it holds more than what it
|
|
197
|
+
// has read. Same detector the context cache is keyed on, so no new failure mode — a
|
|
198
|
+
// non-GitHub or non-repo cwd yields null and the field is simply omitted. The server drops
|
|
199
|
+
// anything malformed; this is attention data, never routing.
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
sessionKey: SESSION_KEY,
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
mcpVersion: version,
|
|
204
|
+
repo: repoFullNameFrom(process.cwd()) ?? undefined,
|
|
205
|
+
}),
|
|
197
206
|
})
|
|
198
207
|
} catch { /* best-effort heartbeat — never disrupt the session */ }
|
|
199
208
|
}
|
|
@@ -255,6 +264,27 @@ export async function runServer(version) {
|
|
|
255
264
|
//
|
|
256
265
|
// The SERVER decides who gets this; it only sends `nudge` to a session holding a matching identifier.
|
|
257
266
|
// No threshold logic here to drift out of sync with it.
|
|
267
|
+
// ADR-0066 §8 — THE DELTA LINE. "Each turn, when something arrived since the last turn: a delta
|
|
268
|
+
// line only; silent when empty."
|
|
269
|
+
//
|
|
270
|
+
// ⚠ ONE LINE, AND NO LIST. The whole point is that it costs almost nothing to read, so it can be
|
|
271
|
+
// shown often without training its reader to skip it. The pile itself is one tool call away and the
|
|
272
|
+
// session-start block already rendered it in full.
|
|
273
|
+
//
|
|
274
|
+
// ⚠ DIFFERENT FROM renderNudge, WHICH SITS DIRECTLY BELOW IT. That block is identifier-matched — the
|
|
275
|
+
// cascade believes those records are THIS session's business, and it names them. This claims nothing
|
|
276
|
+
// about relevance; it says the queue moved. Both can be silent and both can fire.
|
|
277
|
+
//
|
|
278
|
+
// Server-decided, like the nudge: it is silent when the session is muted, on a session's first
|
|
279
|
+
// sight, and when nothing arrived — so there is no threshold logic here to drift out of sync.
|
|
280
|
+
function renderArrival(payload) {
|
|
281
|
+
const a = payload?.arrival
|
|
282
|
+
const n = Number(a?.count ?? 0)
|
|
283
|
+
if (!a || !n) return ''
|
|
284
|
+
return `\n\n⚡ ${n} new arrival${n === 1 ? '' : 's'} in your pile since this session was last told` +
|
|
285
|
+
` — \`staged_records\` lists them. Not offered to you, just counted; \`ignore_arrivals\` if this is noise right now.`
|
|
286
|
+
}
|
|
287
|
+
|
|
258
288
|
function renderNudge(payload) {
|
|
259
289
|
const n = Array.isArray(payload?.nudge) ? payload.nudge : []
|
|
260
290
|
if (!n.length) return ''
|
|
@@ -1359,7 +1389,7 @@ function renderNudge(payload) {
|
|
|
1359
1389
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
1360
1390
|
}
|
|
1361
1391
|
const payload = await res.json()
|
|
1362
|
-
return { content: [{ type: 'text', text: formatGrepHits(payload, query) + renderNudge(payload) }] }
|
|
1392
|
+
return { content: [{ type: 'text', text: formatGrepHits(payload, query) + renderArrival(payload) + renderNudge(payload) }] }
|
|
1363
1393
|
},
|
|
1364
1394
|
)
|
|
1365
1395
|
|
|
@@ -1668,7 +1698,7 @@ function renderNudge(payload) {
|
|
|
1668
1698
|
// `k` is stated rather than hidden. "Only you" and "you and two others" call for different
|
|
1669
1699
|
// behaviour, and a nudge that claimed certainty it did not have is how an agent learns to stop
|
|
1670
1700
|
// reading them.
|
|
1671
|
-
const nudgeBlock = renderNudge(page)
|
|
1701
|
+
const nudgeBlock = renderArrival(page) + renderNudge(page)
|
|
1672
1702
|
|
|
1673
1703
|
if (matches.length === 1) {
|
|
1674
1704
|
return { content: [{ type: 'text', text: renderMatch(matches[0], false) + nudgeBlock }] }
|
|
@@ -2741,6 +2771,87 @@ function renderNudge(payload) {
|
|
|
2741
2771
|
},
|
|
2742
2772
|
)
|
|
2743
2773
|
|
|
2774
|
+
server.registerTool(
|
|
2775
|
+
'ignore_arrivals',
|
|
2776
|
+
{
|
|
2777
|
+
title: 'Stop offering me arrivals for the rest of this session',
|
|
2778
|
+
description: 'Silence the ⚡ ARRIVED block for THIS session. Use it when you are mid-task and the offers are noise right now — declining is per record (not_mine) and that is not a usable answer to a list of nine. ⚠ THIS IS NOT A DECISION ABOUT THE RECORDS AND IT IS NOT A SUPPRESSION. Every one of them stays unclaimed, stays on the general timeline, and stays offerable to every other session — including your own other sessions. You are saying "stop telling me, here", never "nobody\'s business", and nothing is marked handled. ⚠ IT IS ALSO NOT not_mine: use that one when you LOOKED and a record is not yours, because that answers "why did nobody take this" and this does not. Reversible any time with resume:true, and it dies with this session — a new session is never born muted.',
|
|
2779
|
+
inputSchema: {
|
|
2780
|
+
reason: z.string().optional().describe('optional — why now ("mid-task", "deep in a build"). Unlike not_mine this does not require one, because no ownership question is being answered'),
|
|
2781
|
+
resume: z.boolean().optional().describe('true to start hearing about arrivals again in this session'),
|
|
2782
|
+
},
|
|
2783
|
+
},
|
|
2784
|
+
async ({ reason, resume }) => {
|
|
2785
|
+
let res
|
|
2786
|
+
try {
|
|
2787
|
+
res = await fetchCortex(`${BASE}/api/nudge/mute`, {
|
|
2788
|
+
method: 'POST',
|
|
2789
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2790
|
+
body: JSON.stringify({ on: resume === true ? false : true, reason }),
|
|
2791
|
+
})
|
|
2792
|
+
} catch (e) {
|
|
2793
|
+
return toolError(`Could not change arrival notices: ${e.message}`)
|
|
2794
|
+
}
|
|
2795
|
+
const out = await res.json().catch(() => null)
|
|
2796
|
+
if (!res.ok) return toolError(`Could not change arrival notices: ${out?.error ?? res.status}${out?.detail ? `\n${out.detail}` : ''}`)
|
|
2797
|
+
return {
|
|
2798
|
+
content: [{
|
|
2799
|
+
type: 'text',
|
|
2800
|
+
text: resume === true
|
|
2801
|
+
? 'Arrivals will be offered to this session again.'
|
|
2802
|
+
: 'Quiet for the rest of this session. Nothing was decided about those records — they stay unclaimed, stay on the general timeline, and other sessions still see them. `ignore_arrivals resume:true` turns them back on.',
|
|
2803
|
+
}],
|
|
2804
|
+
}
|
|
2805
|
+
},
|
|
2806
|
+
)
|
|
2807
|
+
|
|
2808
|
+
server.registerTool(
|
|
2809
|
+
'my_source_routes',
|
|
2810
|
+
{
|
|
2811
|
+
title: 'Where a source lands — the routes you declared, and what is waiting for one',
|
|
2812
|
+
description: 'Show the DECLARED landings: (account, source type, key) -> brain, with who decided each one. ⚠ THIS IS A DIFFERENT TABLE FROM my_routing_claims AND ANSWERS A DIFFERENT QUESTION. A claim says which PAGE a record attaches to (and the brain falls out of that); a route says which BRAIN a source\'s raw lands in when nothing else decides. Both can be right and disagree, which is exactly when mail piles up. ⚠ A route settles a claim TIE only when `decided_by` is `explicit` — a person decided it; a `triage` route was an agent unblocking a queue and does not overrule live ambiguity (ADR-0066 §5d). ⚠ APPEND-ONLY BY DESIGN: a route is never updated, because re-pointing a live source splits its stream irreparably — records are unique on the org-scoped (org_id, dedupe_key), so the two halves never reconcile. Creating one is a decision you do not get to take back cheaply. Also lists what is STAGED per source — those are exactly the routes worth creating.',
|
|
2813
|
+
inputSchema: {},
|
|
2814
|
+
},
|
|
2815
|
+
async () => {
|
|
2816
|
+
let res
|
|
2817
|
+
try {
|
|
2818
|
+
res = await fetchCortex(`${BASE}/api/source-routes`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
2819
|
+
} catch (e) {
|
|
2820
|
+
return toolError(`Could not read source routes: ${e.message}`)
|
|
2821
|
+
}
|
|
2822
|
+
const out = await res.json().catch(() => null)
|
|
2823
|
+
if (!res.ok) return toolError(`Could not read source routes: ${out?.error ?? res.status}`)
|
|
2824
|
+
|
|
2825
|
+
const routes = Array.isArray(out?.routes) ? out.routes : []
|
|
2826
|
+
const pending = Array.isArray(out?.pendingStaged) ? out.pendingStaged : []
|
|
2827
|
+
const lines = []
|
|
2828
|
+
|
|
2829
|
+
if (routes.length === 0) {
|
|
2830
|
+
lines.push('No declared routes. Every source resolves by claims alone, and a claim tie has nothing to fall back on.')
|
|
2831
|
+
} else {
|
|
2832
|
+
lines.push(`${routes.length} declared route(s):`)
|
|
2833
|
+
for (const r of routes) {
|
|
2834
|
+
// `account`/`sourceKey` are empty when the route means "any" at that position — say so
|
|
2835
|
+
// rather than printing a blank, because a blank reads as missing data.
|
|
2836
|
+
const acct = r.account || '(any account)'
|
|
2837
|
+
const key = r.sourceKey || '(any key)'
|
|
2838
|
+
const mark = r.decidedBy === 'explicit' ? '✓' : '·'
|
|
2839
|
+
lines.push(` ${mark} ${r.sourceType} · ${acct} · ${key} → ${r.brain} [${r.decidedBy}${r.decidedNote ? `: ${r.decidedNote}` : ''}]`)
|
|
2840
|
+
}
|
|
2841
|
+
lines.push(' ✓ = decided by a person, and therefore able to settle a claim tie. · = decided by triage, which cannot.')
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
if (pending.length) {
|
|
2845
|
+
lines.push('')
|
|
2846
|
+
lines.push('Waiting on a routing decision:')
|
|
2847
|
+
for (const p of pending) {
|
|
2848
|
+
lines.push(` ${p.sourceType} · ${p.account || '(any account)'} — ${p.count} staged, oldest ${String(p.oldest).slice(0, 10)}`)
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
2852
|
+
},
|
|
2853
|
+
)
|
|
2854
|
+
|
|
2744
2855
|
server.registerTool(
|
|
2745
2856
|
'my_routing_claims',
|
|
2746
2857
|
{
|
|
@@ -3293,7 +3404,7 @@ function renderNudge(payload) {
|
|
|
3293
3404
|
return toolError(`Could not list records: ${d.message}`)
|
|
3294
3405
|
}
|
|
3295
3406
|
const payload = await res.json()
|
|
3296
|
-
return { content: [{ type: 'text', text: (payload?.text ?? '') + renderNudge(payload) }] }
|
|
3407
|
+
return { content: [{ type: 'text', text: (payload?.text ?? '') + renderArrival(payload) + renderNudge(payload) }] }
|
|
3297
3408
|
},
|
|
3298
3409
|
)
|
|
3299
3410
|
|
package/package.json
CHANGED