@theronap/agnoclast-mcp 0.9.266 → 0.9.268

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.
@@ -5,7 +5,7 @@
5
5
  // that web/lib/engine/release.test.ts pins to the server's registry.
6
6
 
7
7
  /** Tools any release flag gates — hidden when the server cannot be asked. Mirror of allGatedTools(). */
8
- export const GATED_FALLBACK = ['document_diff', 'get_document', 'intake_shadow', 'list_documents', 'my_messages', 'pin_document', 'read_conversation', 'read_document', 'send_message', 'set_manager']
8
+ export const GATED_FALLBACK = ['attach_document', 'document_diff', 'export_page', 'get_document', 'intake_shadow', 'list_documents', 'my_messages', 'pin_document', 'read_conversation', 'read_document', 'send_message', 'set_manager', 'upload_document_version']
9
9
 
10
10
  export const RELEASE_FETCH_TIMEOUT_MS = 2500
11
11
 
package/lib/server.mjs CHANGED
@@ -7416,6 +7416,101 @@ function renderNudge(payload) {
7416
7416
  },
7417
7417
  )
7418
7418
 
7419
+ // TEXT-FIRST TEST, slice A (2026-10-26): a page rendered as a styled Word doc, in its brain's style.
7420
+ server.registerTool(
7421
+ 'export_page',
7422
+ {
7423
+ title: 'Export a page as a styled Word document',
7424
+ description: 'Render a wiki page as a .docx in its brain\'s document style (set by the brain\'s owner or admins in the console) and save it to ~/Downloads/cortex/. Use it when someone needs to SEND or print a page: the page stays the master; the Word file is a copy of it. For PDF, the person uses Export ▸ PDF on the page in Agnoclast.',
7425
+ inputSchema: {
7426
+ page: z.string().describe('the page: its exact title, its `ref:` from read_page, or a document id'),
7427
+ tier: z.string().optional().describe('which copy of a two-copy page (e.g. "scoped"); default the most recently written'),
7428
+ },
7429
+ },
7430
+ async ({ page, tier }) => {
7431
+ const q = new URLSearchParams({ page })
7432
+ if (tier) q.set('tier', tier)
7433
+ let res
7434
+ try { res = await fetchCortex(`${BASE}/api/brain/page/export?${q}`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
7435
+ catch (e) { return toolError(`Could not export: ${e.message}`) }
7436
+ if (!res.ok) return docFail('export the page', res)
7437
+ const buf = Buffer.from(await res.arrayBuffer())
7438
+ const m = /filename="([^"]+)"/.exec(res.headers.get('content-disposition') ?? '')
7439
+ const name = (m ? m[1] : 'page.docx').replace(/[^\w.\- ]/g, '_')
7440
+ const dir = join(homedir(), 'Downloads', 'cortex')
7441
+ mkdirSync(dir, { recursive: true })
7442
+ const path = join(dir, name)
7443
+ writeFileSync(path, buf)
7444
+ return { content: [{ type: 'text', text: `Saved "${page}" as a styled Word document: ${path} (${Math.round(buf.length / 1024)} KB).` }] }
7445
+ },
7446
+ )
7447
+
7448
+ // UPLOADS (slice 4). The server refuses an assistant unless the person turned on "Let assistants upload
7449
+ // files" in Agnoclast → Settings — a switch only the console can flip. These tools never ask for it.
7450
+ // Bytes go straight to storage on the signed URL the server hands out, then the server reads them back.
7451
+ const uploadStored = async (target, path, extra = {}) => {
7452
+ let st
7453
+ try { st = statSync(path) } catch { return toolError(`No file at ${path}.`) }
7454
+ if (!st.isFile()) return toolError(`${path} is not a file.`)
7455
+ const filename = path.split(/[\\/]/).pop()
7456
+ let res
7457
+ try {
7458
+ res = await fetchCortex(`${BASE}/api/stored-files/uploads`, {
7459
+ method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
7460
+ body: JSON.stringify({ ...target, ...extra, filename, size: st.size }),
7461
+ })
7462
+ } catch (e) { return toolError(`Could not start the upload: ${e.message}`) }
7463
+ const begin = await res.json().catch(() => null)
7464
+ if (!res.ok) return toolError(`Not uploaded: ${begin?.detail ?? begin?.error ?? res.status}`)
7465
+ let put
7466
+ try {
7467
+ put = await fetch(begin.signedUrl, { method: 'PUT', headers: { 'content-type': begin.contentType ?? 'application/octet-stream', 'x-upsert': 'false' }, body: readFileSync(path) })
7468
+ } catch (e) { return toolError(`Not uploaded: storage could not be reached (${e.message}).`) }
7469
+ if (!put.ok) return toolError(`Not uploaded: storage refused the file (${put.status}).`)
7470
+ let commit
7471
+ try {
7472
+ commit = await fetchCortex(`${BASE}/api/stored-files/uploads/${begin.uploadId}/commit`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } })
7473
+ } catch (e) { return toolError(`Uploaded but not saved: ${e.message}`) }
7474
+ const c = await commit.json().catch(() => null)
7475
+ if (!commit.ok) return toolError(`Uploaded but not saved: ${c?.detail ?? c?.error ?? commit.status}`)
7476
+ const lines = c.unchanged
7477
+ ? [`"${c.title}" is byte-for-byte identical to its current version (v${c.version}); nothing was added.`]
7478
+ : [`Saved "${c.title}" as version ${c.version} (id ${c.fileId}, ${Math.round(c.sizeBytes / 1024)} KB, sha256 ${c.byteSha256.slice(0, 12)}…)${c.pinned ? ', pinned to its page' : ''}.`]
7479
+ if (!c.unchanged && c.contentUnchanged) lines.push('Its contents read the same as the previous version (only the bytes changed).')
7480
+ if (c.extractStatus && c.extractStatus !== 'ok' && c.extractStatus !== 'unchanged') lines.push(`Not readable as text: ${c.extractNote ?? c.extractStatus}.`)
7481
+ else if (c.extractNote) lines.push(c.extractNote)
7482
+ if (!c.unchanged && c.version > 1 && !c.contentUnchanged) lines.push(`See what changed with document_diff file_id:"${c.fileId}", and update the page if what it says about the file is no longer true.`)
7483
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
7484
+ }
7485
+
7486
+ server.registerTool(
7487
+ 'attach_document',
7488
+ {
7489
+ title: 'Store a file on a page',
7490
+ description: 'Keep a local file (Excel, Word, CSV, PDF…) in Agnoclast on a page: it is versioned, readable with read_document, and visible to exactly who can read that page. Needs edit access to the page, and the person must have turned on "Let assistants upload files" in Agnoclast → Settings — if it is off this is refused; tell them, do not work around it. Up to 50 MB. Pass pin:true for the file the page is about.',
7491
+ inputSchema: {
7492
+ page: z.string().describe('the page it lives on: exact title, `ref:` or document id'),
7493
+ path: z.string().describe('absolute path of the local file'),
7494
+ title: z.string().optional().describe('a name for it; default the filename without its extension'),
7495
+ pin: z.boolean().optional().describe('pin it to the top of the page and its timeline'),
7496
+ },
7497
+ },
7498
+ async ({ page, path, title, pin }) => uploadStored({ page }, path, { ...(title ? { title } : {}), ...(pin ? { pin: true } : {}) }),
7499
+ )
7500
+
7501
+ server.registerTool(
7502
+ 'upload_document_version',
7503
+ {
7504
+ title: 'Upload a new version of a stored file',
7505
+ description: 'Upload a local file as the NEW VERSION of a file already stored in Agnoclast. Every earlier version is kept. Identical bytes add nothing. Same permission as attach_document: refused unless the person turned on "Let assistants upload files". Afterwards, check document_diff and update the page if it no longer describes the file.',
7506
+ inputSchema: {
7507
+ file_id: z.string().describe('the stored file\'s id'),
7508
+ path: z.string().describe('absolute path of the local file'),
7509
+ },
7510
+ },
7511
+ async ({ file_id, path }) => uploadStored({ fileId: file_id }, path),
7512
+ )
7513
+
7419
7514
  // send_imessage — ask to send a text from the user's own Mac (ADR-0076). It no longer texts from this
7420
7515
  // machine: it creates a request on Agnoclast that waits on /email → Waiting for the user's OK (or their
7421
7516
  // web send rule approves it), and the user's Mac — the desktop app, through Messages — sends it and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.266",
3
+ "version": "0.9.268",
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": {