@theronap/cortex-mcp 0.9.158 → 0.9.160

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.
Files changed (2) hide show
  1. package/lib/server.mjs +103 -3
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -264,6 +264,27 @@ export async function runServer(version) {
264
264
  //
265
265
  // The SERVER decides who gets this; it only sends `nudge` to a session holding a matching identifier.
266
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
+
267
288
  function renderNudge(payload) {
268
289
  const n = Array.isArray(payload?.nudge) ? payload.nudge : []
269
290
  if (!n.length) return ''
@@ -1368,7 +1389,7 @@ function renderNudge(payload) {
1368
1389
  throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
1369
1390
  }
1370
1391
  const payload = await res.json()
1371
- return { content: [{ type: 'text', text: formatGrepHits(payload, query) + renderNudge(payload) }] }
1392
+ return { content: [{ type: 'text', text: formatGrepHits(payload, query) + renderArrival(payload) + renderNudge(payload) }] }
1372
1393
  },
1373
1394
  )
1374
1395
 
@@ -1677,7 +1698,7 @@ function renderNudge(payload) {
1677
1698
  // `k` is stated rather than hidden. "Only you" and "you and two others" call for different
1678
1699
  // behaviour, and a nudge that claimed certainty it did not have is how an agent learns to stop
1679
1700
  // reading them.
1680
- const nudgeBlock = renderNudge(page)
1701
+ const nudgeBlock = renderArrival(page) + renderNudge(page)
1681
1702
 
1682
1703
  if (matches.length === 1) {
1683
1704
  return { content: [{ type: 'text', text: renderMatch(matches[0], false) + nudgeBlock }] }
@@ -2130,6 +2151,51 @@ function renderNudge(payload) {
2130
2151
  },
2131
2152
  )
2132
2153
 
2154
+ server.registerTool(
2155
+ 'staged_look',
2156
+ {
2157
+ title: 'Read a staged arrival before deciding where it goes',
2158
+ description: 'Read ONE staged arrival\'s envelope — sender, subject, recipients, thread, the generated summary, and the identifiers holding it — without claiming or changing anything. Use it before `place_staged_record` whenever the subject alone does not tell you WHAT KIND of thing this is. ⚠ THE SUBJECT LIES MORE OFTEN THAN YOU WOULD THINK: `staged_records` lists subjects only, and a message reading `Re: [owner/repo] fix(triage)…` is as likely to be a bot\'s deployment-status comment as a human discussing the PR — measured 2026-09-21, `vercel[bot]` accounts for 26 of the pending arrivals whose subject names that repo. The `from` header settles it in one look. ⚠ THIS IS THE ENVELOPE, NOT THE LETTER. A staged arrival stores headers and a generated summary; the message BODY is not persisted, so `bodyStored` is always false. Do not describe an arrival as something you READ — you saw who sent it and what it was titled. Looking is free, changes nothing, and needs no release.',
2159
+ inputSchema: {
2160
+ stagedId: z.string().describe('staged arrival id, from `staged_records` or the session-start arrivals block'),
2161
+ },
2162
+ },
2163
+ async ({ stagedId }) => {
2164
+ let res
2165
+ try {
2166
+ res = await fetchCortex(`${BASE}/api/staged/look?stagedId=${encodeURIComponent(stagedId)}`, {
2167
+ headers: { Authorization: `Bearer ${TOKEN}` },
2168
+ })
2169
+ } catch (e) {
2170
+ return toolError(`Could not read that staged arrival: ${e.message}`)
2171
+ }
2172
+ const out = await res.json().catch(() => null)
2173
+ if (res.status === 404) {
2174
+ return toolError(`No staged arrival ${stagedId} of yours. Staged rows are per-person and in no brain, so this means it is not yours or does not exist — the two are deliberately indistinguishable.`)
2175
+ }
2176
+ if (!res.ok) return toolError(`Could not read staged arrival ${stagedId}: ${out?.error ?? res.status}`)
2177
+
2178
+ const l = out?.look ?? {}
2179
+ const line = (label, v) => (v ? ` ${label.padEnd(9)} ${v}\n` : '')
2180
+ const held = Array.isArray(l.blockingIdentifiers) && l.blockingIdentifiers.length
2181
+ ? `\n⚠ HELD ON UNCLAIMED IDENTIFIERS — no page claims these, so it cannot finish until they are dispositioned:\n${l.blockingIdentifiers.map((i) => ` · ${i}`).join('\n')}\n`
2182
+ : ''
2183
+ return {
2184
+ content: [{
2185
+ type: 'text',
2186
+ text:
2187
+ `${l.title ?? '(untitled)'}\n` +
2188
+ ` ${l.sourceType} · arrived ${l.receivedAt ?? '?'} · occurred ${l.occurredAt ?? '?'}\n\n` +
2189
+ line('from', l.from) + line('subject', l.subject) + line('to', l.to) +
2190
+ line('account', l.account) + line('thread', l.threadId) +
2191
+ (l.summary ? `\n summary ${l.summary}\n` : '') +
2192
+ held +
2193
+ `\n⚠ This is the ENVELOPE. The message body was never stored (bodyStored: ${l.bodyStored === true}), so you have seen who sent it and what it was called — not what it says. Judge accordingly, and do not report having read it.`,
2194
+ }],
2195
+ }
2196
+ },
2197
+ )
2198
+
2133
2199
  server.registerTool(
2134
2200
  'discard_staged',
2135
2201
  {
@@ -2750,6 +2816,40 @@ function renderNudge(payload) {
2750
2816
  },
2751
2817
  )
2752
2818
 
2819
+ server.registerTool(
2820
+ 'ignore_arrivals',
2821
+ {
2822
+ title: 'Stop offering me arrivals for the rest of this session',
2823
+ 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.',
2824
+ inputSchema: {
2825
+ 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'),
2826
+ resume: z.boolean().optional().describe('true to start hearing about arrivals again in this session'),
2827
+ },
2828
+ },
2829
+ async ({ reason, resume }) => {
2830
+ let res
2831
+ try {
2832
+ res = await fetchCortex(`${BASE}/api/nudge/mute`, {
2833
+ method: 'POST',
2834
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2835
+ body: JSON.stringify({ on: resume === true ? false : true, reason }),
2836
+ })
2837
+ } catch (e) {
2838
+ return toolError(`Could not change arrival notices: ${e.message}`)
2839
+ }
2840
+ const out = await res.json().catch(() => null)
2841
+ if (!res.ok) return toolError(`Could not change arrival notices: ${out?.error ?? res.status}${out?.detail ? `\n${out.detail}` : ''}`)
2842
+ return {
2843
+ content: [{
2844
+ type: 'text',
2845
+ text: resume === true
2846
+ ? 'Arrivals will be offered to this session again.'
2847
+ : '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.',
2848
+ }],
2849
+ }
2850
+ },
2851
+ )
2852
+
2753
2853
  server.registerTool(
2754
2854
  'my_source_routes',
2755
2855
  {
@@ -3349,7 +3449,7 @@ function renderNudge(payload) {
3349
3449
  return toolError(`Could not list records: ${d.message}`)
3350
3450
  }
3351
3451
  const payload = await res.json()
3352
- return { content: [{ type: 'text', text: (payload?.text ?? '') + renderNudge(payload) }] }
3452
+ return { content: [{ type: 'text', text: (payload?.text ?? '') + renderArrival(payload) + renderNudge(payload) }] }
3353
3453
  },
3354
3454
  )
3355
3455
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.158",
3
+ "version": "0.9.160",
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": {