@theronap/agnoclast-mcp 0.9.266 → 0.9.267

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', '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,72 @@ function renderNudge(payload) {
7416
7416
  },
7417
7417
  )
7418
7418
 
7419
+ // UPLOADS (slice 4). The server refuses an assistant unless the person turned on "Let assistants upload
7420
+ // files" in Agnoclast → Settings — a switch only the console can flip. These tools never ask for it.
7421
+ // Bytes go straight to storage on the signed URL the server hands out, then the server reads them back.
7422
+ const uploadStored = async (target, path, extra = {}) => {
7423
+ let st
7424
+ try { st = statSync(path) } catch { return toolError(`No file at ${path}.`) }
7425
+ if (!st.isFile()) return toolError(`${path} is not a file.`)
7426
+ const filename = path.split(/[\\/]/).pop()
7427
+ let res
7428
+ try {
7429
+ res = await fetchCortex(`${BASE}/api/stored-files/uploads`, {
7430
+ method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
7431
+ body: JSON.stringify({ ...target, ...extra, filename, size: st.size }),
7432
+ })
7433
+ } catch (e) { return toolError(`Could not start the upload: ${e.message}`) }
7434
+ const begin = await res.json().catch(() => null)
7435
+ if (!res.ok) return toolError(`Not uploaded: ${begin?.detail ?? begin?.error ?? res.status}`)
7436
+ let put
7437
+ try {
7438
+ put = await fetch(begin.signedUrl, { method: 'PUT', headers: { 'content-type': begin.contentType ?? 'application/octet-stream', 'x-upsert': 'false' }, body: readFileSync(path) })
7439
+ } catch (e) { return toolError(`Not uploaded: storage could not be reached (${e.message}).`) }
7440
+ if (!put.ok) return toolError(`Not uploaded: storage refused the file (${put.status}).`)
7441
+ let commit
7442
+ try {
7443
+ commit = await fetchCortex(`${BASE}/api/stored-files/uploads/${begin.uploadId}/commit`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } })
7444
+ } catch (e) { return toolError(`Uploaded but not saved: ${e.message}`) }
7445
+ const c = await commit.json().catch(() => null)
7446
+ if (!commit.ok) return toolError(`Uploaded but not saved: ${c?.detail ?? c?.error ?? commit.status}`)
7447
+ const lines = c.unchanged
7448
+ ? [`"${c.title}" is byte-for-byte identical to its current version (v${c.version}); nothing was added.`]
7449
+ : [`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' : ''}.`]
7450
+ if (!c.unchanged && c.contentUnchanged) lines.push('Its contents read the same as the previous version (only the bytes changed).')
7451
+ if (c.extractStatus && c.extractStatus !== 'ok' && c.extractStatus !== 'unchanged') lines.push(`Not readable as text: ${c.extractNote ?? c.extractStatus}.`)
7452
+ else if (c.extractNote) lines.push(c.extractNote)
7453
+ 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.`)
7454
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
7455
+ }
7456
+
7457
+ server.registerTool(
7458
+ 'attach_document',
7459
+ {
7460
+ title: 'Store a file on a page',
7461
+ 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.',
7462
+ inputSchema: {
7463
+ page: z.string().describe('the page it lives on: exact title, `ref:` or document id'),
7464
+ path: z.string().describe('absolute path of the local file'),
7465
+ title: z.string().optional().describe('a name for it; default the filename without its extension'),
7466
+ pin: z.boolean().optional().describe('pin it to the top of the page and its timeline'),
7467
+ },
7468
+ },
7469
+ async ({ page, path, title, pin }) => uploadStored({ page }, path, { ...(title ? { title } : {}), ...(pin ? { pin: true } : {}) }),
7470
+ )
7471
+
7472
+ server.registerTool(
7473
+ 'upload_document_version',
7474
+ {
7475
+ title: 'Upload a new version of a stored file',
7476
+ 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.',
7477
+ inputSchema: {
7478
+ file_id: z.string().describe('the stored file\'s id'),
7479
+ path: z.string().describe('absolute path of the local file'),
7480
+ },
7481
+ },
7482
+ async ({ file_id, path }) => uploadStored({ fileId: file_id }, path),
7483
+ )
7484
+
7419
7485
  // send_imessage — ask to send a text from the user's own Mac (ADR-0076). It no longer texts from this
7420
7486
  // machine: it creates a request on Agnoclast that waits on /email → Waiting for the user's OK (or their
7421
7487
  // 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.267",
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": {