@theronap/agnoclast-mcp 0.9.153 → 0.9.155

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/diagnose.mjs CHANGED
@@ -479,7 +479,7 @@ export async function fetchCortex(url, opts = {}, policy = {}) {
479
479
  export async function checkToken(token, base) {
480
480
  if (!token) {
481
481
  return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
482
- message: 'No CORTEX_TOKEN found. Run: npx -y @theronap/cortex-mcp setup <your-token>' } }
482
+ message: 'No CORTEX_TOKEN found. Run: npx -y @theronap/cortex-mcp login (opens your browser; or `setup <token>` with a token from the console → Connect your AI)' } }
483
483
  }
484
484
  if (!isUuid(token)) {
485
485
  return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
package/lib/doctor.mjs CHANGED
@@ -56,7 +56,7 @@ async function statusLine(out, { capturesStuck = false } = {}) {
56
56
  const base = resolveBase(process.env.CORTEX_URL)
57
57
  const { token } = resolveToken()
58
58
  if (!token) {
59
- out('Agnoclast: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
59
+ out('Agnoclast: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp login (opens your browser; or `setup <token>` with a token from the console → Connect your AI)')
60
60
  return
61
61
  }
62
62
  try {
package/lib/server.mjs CHANGED
@@ -1438,7 +1438,7 @@ function renderNudge(payload) {
1438
1438
  {
1439
1439
  title: 'Read a wiki page in full',
1440
1440
  description:
1441
- 'READ the full authored wiki page for one node (project/person/org/you) by its canonical name — every section, every tier you can see. This is how you READ a node; `grep` only LOCATES pages (snippets + their [[links]]), it does not read them. Navigate like a researcher: read the page you need, then FOLLOW its inline [[links]] by calling read_page on each linked name — keep following while the linked pages stay relevant, stop when they do not. You decide how deep to go. Returns only what you are permitted to see.',
1441
+ 'READ the full authored wiki page for one node (project/person/org/you) by its canonical name — every section you can see. This is how you READ a node; `grep` only LOCATES pages (snippets + their [[links]]), it does not read them. Navigate like a researcher: read the page you need, then FOLLOW its inline [[links]] by calling read_page on each linked name — keep following while the linked pages stay relevant, stop when they do not. You decide how deep to go. Returns only what you are permitted to see.',
1442
1442
  inputSchema: {
1443
1443
  name: z.string().describe('the canonical node name exactly as written (e.g. "Agnoclast", "Ben", or a [[link]] target) — identifier links ([[repo:owner/name]]) resolve to their authored HOME + a visible-event count'),
1444
1444
  kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind — a PREFERENCE, not a filter. The lookup matches every kind by title and only prefers this one when several share a name, so omitting it never hides a person/org/user page. Pass it to disambiguate a name that exists as more than one kind.'),
@@ -1598,7 +1598,7 @@ function renderNudge(payload) {
1598
1598
  // unrecoverable base_version guessing loop; say so explicitly instead.
1599
1599
  const versionLine = t.version
1600
1600
  ? `\nversion: ${t.version}`
1601
- : `\nversion: none (this variant predates content-hash tracking — base_version writes will always fail here; ask an admin about the ADR-0018 backfill)`
1601
+ : `\nversion: none (this page predates content-hash tracking — base_version writes will always fail here; ask an admin about the ADR-0018 backfill)`
1602
1602
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
1603
1603
  // ADR-0030 step 3b (2026-08-24): the summary header is NO LONGER RENDERED here either.
1604
1604
  // ⚠ There were TWO of these — project_status and read_page — and the first patch fixed only
@@ -1824,19 +1824,18 @@ function renderNudge(payload) {
1824
1824
  'rename_section',
1825
1825
  {
1826
1826
  title: 'Rename one section heading on a wiki page',
1827
- description: 'Rename ONE section\'s heading on a wiki page, in place. Use this instead of re-authoring the page: `author` cannot express a rename, because a section\'s IDENTITY is its heading string — sending a new title reads as "dropped the old section, added a new one" and is rejected 409 would_drop_sections. This route changes the heading and NOTHING else: the section keeps its body, its position, and crucially its as-of date, so renaming does not reset the currency stamp on a claim nobody re-verified. It is also the ONLY way to fix a heading longer than the 80-char limit, which cannot be re-authored at all. Requires base_version (the `version` read_page prints) — that is also how it finds the right brain, so it can never rename on the wrong one. Refuses, rather than guessing, when the page exists at several tiers, when the heading repeats, or when the new heading is already taken.',
1827
+ description: 'Rename ONE section\'s heading on a wiki page, in place. Use this instead of re-authoring the page: `author` cannot express a rename, because a section\'s IDENTITY is its heading string — sending a new title reads as "dropped the old section, added a new one" and is rejected 409 would_drop_sections. This route changes the heading and NOTHING else: the section keeps its body, its position, and crucially its as-of date, so renaming does not reset the currency stamp on a claim nobody re-verified. It is also the ONLY way to fix a heading longer than the 80-char limit, which cannot be re-authored at all. Requires base_version (the `version` read_page prints) — that is also how it finds the right brain, so it can never rename on the wrong one. Refuses, rather than guessing, when the heading repeats, or when the new heading is already taken.',
1828
1828
  inputSchema: {
1829
1829
  name: z.string().describe('the exact page name, as read_page shows it'),
1830
1830
  from: z.string().describe('the CURRENT heading, exactly as stored (match is case- and whitespace-insensitive)'),
1831
1831
  to: z.string().describe('the new heading. Max 80 chars. Must not already be used by another section on this page.'),
1832
1832
  base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved. A per-SECTION hash is not valid here.'),
1833
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to rename in. A rename never moves content between tiers.'),
1834
1833
  ordinal: z.number().optional().describe('only when the same heading appears more than once on the page — which occurrence to rename (the error lists the ordinals).'),
1835
1834
  reason: z.string().optional().describe('why you are renaming it — recorded in page_history like any other edit'),
1836
1835
  identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
1837
1836
  },
1838
1837
  },
1839
- async ({ name, from, to, base_version, tier, ordinal, reason, identity_claim_ack }) => {
1838
+ async ({ name, from, to, base_version, ordinal, reason, identity_claim_ack }) => {
1840
1839
  let res
1841
1840
  try {
1842
1841
  res = await fetchCortex(`${BASE}/api/brain/rename-section`, {
@@ -1844,7 +1843,7 @@ function renderNudge(payload) {
1844
1843
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1845
1844
  body: JSON.stringify({
1846
1845
  name, from, to, base_version,
1847
- ...(tier ? { tier } : {}), ...(ordinal !== undefined ? { ordinal } : {}),
1846
+ ...(ordinal !== undefined ? { ordinal } : {}),
1848
1847
  ...(reason ? { reason } : {}), ...(identity_claim_ack ? { identity_claim_ack: true } : {}),
1849
1848
  }),
1850
1849
  })
@@ -1880,7 +1879,6 @@ function renderNudge(payload) {
1880
1879
  into: z.string().optional().describe('the heading of the section that SURVIVES and absorbs the content. Omit to drop `from` outright without folding it anywhere.'),
1881
1880
  new_body: z.string().optional().describe('the condensed body the surviving section should read AFTER the merge — you write it, this route does not summarize. Required with `into`; must be absent without it.'),
1882
1881
  base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved. A per-SECTION hash is not valid here.'),
1883
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to condense in. A merge never moves content between tiers.'),
1884
1882
  from_ordinal: z.number().optional().describe('only when `from`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
1885
1883
  into_ordinal: z.number().optional().describe('only when `into`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
1886
1884
  reason: z.string().optional().describe('WHY you are condensing, in one short phrase — recorded in page_history. Say what became redundant, not what you did.'),
@@ -1888,7 +1886,7 @@ function renderNudge(payload) {
1888
1886
  identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier.'),
1889
1887
  },
1890
1888
  },
1891
- async ({ name, from, into, new_body, base_version, tier, from_ordinal, into_ordinal, reason, drop_links_ack, identity_claim_ack }) => {
1889
+ async ({ name, from, into, new_body, base_version, from_ordinal, into_ordinal, reason, drop_links_ack, identity_claim_ack }) => {
1892
1890
  let res
1893
1891
  try {
1894
1892
  res = await fetchCortex(`${BASE}/api/brain/merge-sections`, {
@@ -1897,7 +1895,6 @@ function renderNudge(payload) {
1897
1895
  body: JSON.stringify({
1898
1896
  name, from, base_version,
1899
1897
  ...(into ? { into } : {}), ...(new_body !== undefined ? { new_body } : {}),
1900
- ...(tier ? { tier } : {}),
1901
1898
  ...(from_ordinal !== undefined ? { from_ordinal } : {}),
1902
1899
  ...(into_ordinal !== undefined ? { into_ordinal } : {}),
1903
1900
  ...(reason ? { reason } : {}),
@@ -2754,11 +2751,10 @@ function renderNudge(payload) {
2754
2751
  summary: z.string().describe('the new summary: one sentence saying what this is and where it stands. May contain [[links]]. Max 2,000 chars — detail belongs in a section.'),
2755
2752
  base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved. A per-SECTION hash is not valid here.'),
2756
2753
  reason: z.string().optional().describe('WHY the summary was wrong, in one short phrase — recorded in page_history. Say what changed ("blocker resolved 08-04; was still claiming BLOCKED"), not what you did.'),
2757
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to rewrite. This never moves content between tiers.'),
2758
2754
  identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
2759
2755
  },
2760
2756
  },
2761
- async ({ name, summary, base_version, reason, tier, identity_claim_ack }) => {
2757
+ async ({ name, summary, base_version, reason, identity_claim_ack }) => {
2762
2758
  let res
2763
2759
  try {
2764
2760
  res = await fetchCortex(`${BASE}/api/brain/set-summary`, {
@@ -2766,7 +2762,7 @@ function renderNudge(payload) {
2766
2762
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2767
2763
  body: JSON.stringify({
2768
2764
  name, summary, base_version,
2769
- ...(tier ? { tier } : {}), ...(reason ? { reason } : {}),
2765
+ ...(reason ? { reason } : {}),
2770
2766
  ...(identity_claim_ack ? { identity_claim_ack: true } : {}),
2771
2767
  }),
2772
2768
  })
@@ -2803,12 +2799,11 @@ function renderNudge(payload) {
2803
2799
  new_string: z.string().describe('what to replace it with. May be empty, which deletes the anchored text.'),
2804
2800
  base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved.'),
2805
2801
  reason: z.string().describe('WHY you are making this change, in one short phrase — recorded in page_history exactly like an author edit.'),
2806
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to edit. An edit never moves content between tiers.'),
2807
2802
  ordinal: z.number().optional().describe('only when the same heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
2808
2803
  identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier — e.g. the subject is publishing their OWN contact detail on their own page. Leave unset otherwise; the alternatives the refusal names (a confidential section, or set_routing_identifier for a private claim) are the right answer in every other case.'),
2809
2804
  },
2810
2805
  },
2811
- async ({ name, heading, old_string, new_string, base_version, reason, tier, ordinal, identity_claim_ack }) => {
2806
+ async ({ name, heading, old_string, new_string, base_version, reason, ordinal, identity_claim_ack }) => {
2812
2807
  let res
2813
2808
  try {
2814
2809
  res = await fetchCortex(`${BASE}/api/brain/edit-page`, {
@@ -2816,7 +2811,7 @@ function renderNudge(payload) {
2816
2811
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2817
2812
  body: JSON.stringify({
2818
2813
  name, heading, old_string, new_string, base_version, reason,
2819
- ...(tier ? { tier } : {}), ...(ordinal !== undefined ? { ordinal } : {}),
2814
+ ...(ordinal !== undefined ? { ordinal } : {}),
2820
2815
  ...(identity_claim_ack ? { identity_claim_ack: true } : {}),
2821
2816
  }),
2822
2817
  })
@@ -3148,7 +3143,7 @@ function renderNudge(payload) {
3148
3143
  org_id: z.string().describe('the org id of the brain to enumerate (from my_brains)'),
3149
3144
  owner: z.string().optional().describe('only pages owned by this person — a user id or an EXACT display name. A name matching no member of the brain is an error, never a silently empty list. Owner applies to scoped/confidential pages; accessible pages have no owner.'),
3150
3145
  updated_within_days: z.number().optional().describe('only pages touched in the last N days'),
3151
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only nodes that have a row at this tier. The row still reports every tier you can see, so a multi-tier page does not come back describing itself as single-tier.'),
3146
+ tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only pages at this tier'),
3152
3147
  kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('only nodes of this kind'),
3153
3148
  validity: z.enum(['current', 'superseded', 'all']).optional().describe("default 'current'. Pass 'all' for the pre-2026-08 behavior, which mixed superseded pages in with nothing marking them."),
3154
3149
  name_contains: z.string().optional().describe('case-insensitive substring match on the node name or page title'),
@@ -3960,16 +3955,15 @@ function renderNudge(payload) {
3960
3955
  ref: z.string().optional().describe('the page\'s stable id, printed as `ref:` by read_page. Prefer this over name: unique across brains, so it addresses exactly one page.'),
3961
3956
  grantee: z.string().describe('display name or email of a member OF THE PAGE\'S BRAIN (must resolve uniquely — use email if ambiguous)'),
3962
3957
  action: z.enum(['grant', 'revoke']).describe('grant or revoke'),
3963
- tier: z.enum(['scoped', 'confidential']).optional().describe('which variant (default: the most restrictive one)'),
3964
3958
  },
3965
3959
  },
3966
- async ({ kind, name, ref, grantee, action, tier }) => {
3960
+ async ({ kind, name, ref, grantee, action }) => {
3967
3961
  let res
3968
3962
  try {
3969
3963
  res = await fetchCortex(`${BASE}/api/brain/page-grants`, {
3970
3964
  method: 'POST',
3971
3965
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3972
- body: JSON.stringify({ kind, ...(name ? { name } : {}), ...(ref ? { ref } : {}), grantee, action, ...(tier ? { tier } : {}) }),
3966
+ body: JSON.stringify({ kind, ...(name ? { name } : {}), ...(ref ? { ref } : {}), grantee, action }),
3973
3967
  })
3974
3968
  } catch (e) {
3975
3969
  return toolError(`Could not ${action}: ${e.message}`)
@@ -3985,7 +3979,7 @@ function renderNudge(payload) {
3985
3979
  'list_page_grants',
3986
3980
  {
3987
3981
  title: 'List who has granted access to your page',
3988
- description: 'Show every explicit access grant on YOUR page\'s tier variants (owner-only). Use after re-tiering a page — grants survive tier changes and keep granting until revoked.',
3982
+ description: 'Show every explicit access grant on YOUR pages (owner-only). Use after re-tiering a page — grants survive tier changes and keep granting until revoked.',
3989
3983
  inputSchema: {
3990
3984
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
3991
3985
  name: z.string().optional().describe('the exact page name (or pass `ref` instead — one of the two is required)'),
@@ -4005,7 +3999,7 @@ function renderNudge(payload) {
4005
3999
  const out = await res.json().catch(() => null)
4006
4000
  if (!res.ok) return toolError(`Could not list grants: ${out?.error ?? res.status}`)
4007
4001
  if (!out.grants?.length) return { content: [{ type: 'text', text: 'No grants on this page.' }] }
4008
- const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} variant (since ${String(g.created_at).slice(0, 10)})`)
4002
+ const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} page (since ${String(g.created_at).slice(0, 10)})`)
4009
4003
  return { content: [{ type: 'text', text: `Grants (${out.grants.length}):\n${lines.join('\n')}` }] }
4010
4004
  },
4011
4005
  )
@@ -4367,8 +4361,7 @@ function renderNudge(payload) {
4367
4361
  // last chance to notice a page was fixed somewhere nobody reads.
4368
4362
  const corrections = Array.isArray(out?.tierCorrections) && out.tierCorrections.length
4369
4363
  ? `\n⚠ TIER: this wrote to ${out.tierCorrections.map((c) => `${c.actualTier} (you asked for ${c.requestedTier})`).join('; ')}.` +
4370
- ` If that is not the tier you meant, read_page and check which copy you just changed —` +
4371
- ` a page can exist at several tiers and they drift apart independently.`
4364
+ ` A page has one tier and author writes at it; to change who can see the page, use set_page_privacy.`
4372
4365
  : ''
4373
4366
  // KWA-26 — the advisory undated flag. The server has computed `undatedSections` since #558 and
4374
4367
  // the route has returned it ever since; NOTHING PRINTED IT, so the one consumer the item names
package/lib/use_brain.mjs CHANGED
@@ -45,7 +45,7 @@ export async function runUseBrain(args) {
45
45
  const base = resolveBase(process.env.CORTEX_URL)
46
46
  const { token } = resolveToken()
47
47
  if (!token) {
48
- out('Agnoclast: no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
48
+ out('Agnoclast: no token found. Run: npx -y @theronap/cortex-mcp login (opens your browser; or `setup <token>` with a token from the console → Connect your AI)')
49
49
  return 1
50
50
  }
51
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.153",
3
+ "version": "0.9.155",
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": {