@theronap/cortex-mcp 0.9.87 → 0.9.88

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 +90 -3
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -486,6 +486,45 @@ export async function runServer(version) {
486
486
  },
487
487
  )
488
488
 
489
+ server.registerTool(
490
+ 'intake_discard',
491
+ {
492
+ title: 'Discard a private intake item',
493
+ description:
494
+ 'The OTHER terminal outcome for a claimed intake unit: this is nothing, drop it. Use it for content that should never become a record — unsubscribe receipts, empty greetings, marketing blasts, a stranger\'s photo — instead of materializing junk into a brain because materialize was the only verb available. IRREVERSIBLE: the ciphertext and nonces are destroyed in the same transaction, and a content-free tombstone stops the connector re-delivering the unit. You must already hold the claim (discarding something you never read is refused), `reason` is required and is stored, and it is one item per call — a loop discarding a whole pile on one decision is a bulk job, not judgment.',
495
+ inputSchema: {
496
+ intakeItemId: z.string().describe('intake item uuid from intake_claim'),
497
+ reason: z.string().describe('why this is nothing — recorded on the claim, and the only surviving trace of the decision'),
498
+ },
499
+ },
500
+ async ({ intakeItemId, reason }) => {
501
+ let res
502
+ try {
503
+ res = await fetchCortex(`${BASE}/api/intake/discard`, {
504
+ method: 'POST',
505
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
506
+ body: JSON.stringify({ intakeItemId, reason }),
507
+ })
508
+ } catch (e) {
509
+ return toolError(`Could not discard: ${e.message}`)
510
+ }
511
+ const out = await res.json().catch(() => null)
512
+ if (!res.ok) {
513
+ if (out?.error === 'not_claimed') {
514
+ return toolError('You do not hold a claim on that item — intake_claim it first, so the discard follows from having read it.')
515
+ }
516
+ if (out?.error === 'already_materialized') {
517
+ return toolError('That unit already became a record. Discarding it now would orphan the record from its source — detach or retier the record instead.')
518
+ }
519
+ return toolError(`Could not discard: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
520
+ }
521
+ if (out?.alreadyDiscarded) {
522
+ return { content: [{ type: 'text', text: 'Already discarded — nothing to do.' }] }
523
+ }
524
+ return { content: [{ type: 'text', text: 'Discarded. Content destroyed, tombstone written so the source cannot re-deliver it.' }] }
525
+ },
526
+ )
527
+
489
528
  server.registerTool(
490
529
  'intake_cleanup_status',
491
530
  {
@@ -1654,16 +1693,19 @@ export async function runServer(version) {
1654
1693
  },
1655
1694
  )
1656
1695
 
1657
- // ── Gate 4 record triage ──────────────────────────────────────────────────────────────────
1696
+ // ── Gate 4 record triage, over timeline_claims ────────────────────────────────────────────
1658
1697
  // A connector event materializes in seconds and has no idea what the work WAS. The session that
1659
1698
  // did the work knows exactly, and arrives later. These three tools are that handoff: look at what
1660
1699
  // landed, claim what is yours, route it when you know where it goes.
1700
+ //
1701
+ // The ledger is `timeline_claims` (0105) — one claim discipline over one stream. Unclaimed means
1702
+ // NO claim row: absence IS the backlog, and nothing is written at ingest.
1661
1703
 
1662
1704
  server.registerTool(
1663
1705
  'pending_records',
1664
1706
  {
1665
1707
  title: 'Records waiting for a home',
1666
- description: 'List connector records (GitHub pushes, PRs, email) that landed WITHOUT a confident home and are waiting for judgment. Check this when your session starts if the headline count sounds related to what you are about to work on records from your own recent commits are usually in here. Returns titles and current homes only, never payloads. Use view "sweep" on one record to get graded page-name candidates without reading any pages.',
1708
+ description: 'List recent connector records (GitHub pushes, PRs, email) that NOBODY HAS ATTENDED TO yet no claim row in the Gate 4 ledger. Check this when your session starts if the headline count sounds related to what you are about to work on; records from your own recent commits are usually in here, and you are the only one who can recognize them as yours. Returns titles and current homes only, never payloads. Use view "sweep" on one record to get graded page-name candidates without reading any pages.',
1667
1709
  inputSchema: {
1668
1710
  view: z.enum(['digest', 'sweep']).optional().describe('digest = what is waiting (default); sweep = cheap graded candidates for one record'),
1669
1711
  recordId: z.string().optional().describe('required for view "sweep"'),
@@ -1728,7 +1770,7 @@ export async function runServer(version) {
1728
1770
  'claim_record',
1729
1771
  {
1730
1772
  title: 'Claim a pending record as your work',
1731
- description: 'Say "this record is mine, I will route it once I know where it goes." Use it as soon as you recognize your own work in pending_records, even before you know the destination page — the claim stops a cheap automatic sweep from guessing at something you have real context on. Claims expire, so a dead session never holds a record hostage. Pass release=true to give one back when it turns out not to be yours.',
1773
+ description: 'Say "this record is mine, I will route it once I know where it goes." Use it as soon as you recognize your own work in pending_records, even before you know the destination page — the claim takes it out of the backlog so nothing else guesses at something you have real context on. Claims are leased and expire, so a dead session never holds a record hostage, and a record held by another LIVE session cannot be taken. Pass release=true to give one back when it turns out not to be yours — that deletes the claim, so the record looks untouched again rather than attended-to.',
1732
1774
  inputSchema: {
1733
1775
  recordId: z.string().describe('record id from pending_records'),
1734
1776
  note: z.string().optional().describe('what you think this is — kept for audit'),
@@ -1790,6 +1832,51 @@ export async function runServer(version) {
1790
1832
  },
1791
1833
  )
1792
1834
 
1835
+ server.registerTool(
1836
+ 'unroute_record',
1837
+ {
1838
+ title: 'Remove pages a record should not be on',
1839
+ description:
1840
+ 'Detach pages a record does not belong on — the inverse of route_record, and the only way a wrong placement can be undone. route_record is purely ADDITIVE, so attaching more pages can never fix a bad one. Use this when you can see a record sitting on a page it has no real relationship to — the classic case is a fuzzy title match, e.g. a commit attached to a page merely because both contain a common word. Two things it will refuse rather than surprise you: it will not remove every attachment (a record with no home is invisible, which is worse than a wrong home — route or park it instead), and detaching the page that GOVERNS the tier can tighten the record but never republish it, since a widening is pinned and proposed for a human to confirm.',
1841
+ inputSchema: {
1842
+ recordId: z.string().describe('record id from pending_records'),
1843
+ documentIds: z.array(z.string()).describe('page document ids to REMOVE from this record'),
1844
+ reason: z.string().describe('why these placements are wrong — recorded with the removal'),
1845
+ },
1846
+ },
1847
+ async ({ recordId, documentIds, reason }) => {
1848
+ let res
1849
+ try {
1850
+ res = await fetchCortex(`${BASE}/api/brain/triage`, {
1851
+ method: 'POST',
1852
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1853
+ body: JSON.stringify({ action: 'detach', recordId, documentIds, reason }),
1854
+ })
1855
+ } catch (e) {
1856
+ return toolError(`Could not detach: ${e.message}`)
1857
+ }
1858
+ const out = await res.json().catch(() => null)
1859
+ if (!res.ok) {
1860
+ // These two are guard rails, not faults — say what to do instead of just naming the code.
1861
+ if (out?.error === 'would_strand') {
1862
+ return toolError(`Refused: ${out.detail ?? 'that would leave the record with no pages at all.'}`)
1863
+ }
1864
+ if (out?.error === 'not_attached') {
1865
+ return toolError(`Nothing removed: ${out.detail ?? 'those pages are not attached to this record.'}`)
1866
+ }
1867
+ return toolError(`Could not detach: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
1868
+ }
1869
+ const n = out?.detached?.length ?? 0
1870
+ const left = out?.remaining ?? 0
1871
+ return {
1872
+ content: [{
1873
+ type: 'text',
1874
+ text: `Detached ${n} page(s); ${left} attachment(s) remain. Record tier: ${out?.privacy ?? 'unchanged'}. Recorded as a session judgment.`,
1875
+ }],
1876
+ }
1877
+ },
1878
+ )
1879
+
1793
1880
  server.registerTool(
1794
1881
  'snooze_red_link',
1795
1882
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.87",
3
+ "version": "0.9.88",
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": {