@theronap/agnoclast-mcp 0.9.215 → 0.9.216
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/editors/claude.mjs +1 -1
- package/lib/record_render.mjs +53 -0
- package/lib/server.mjs +39 -1
- package/package.json +1 -1
package/lib/editors/claude.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function mergeClaudeMcp(existing, spec, token) {
|
|
|
30
30
|
export const ALLOWED_TOOL_NAMES = [
|
|
31
31
|
// read surface
|
|
32
32
|
'grep', 'read_page', 'my_context', 'project_status', 'session_context', 'search_org',
|
|
33
|
-
'list_records', 'page_history', 'page_diff', 'timeline_pull', 'my_brains', 'my_sessions', 'writing_style',
|
|
33
|
+
'list_records', 'read_record', 'page_history', 'page_diff', 'timeline_pull', 'my_brains', 'my_sessions', 'writing_style',
|
|
34
34
|
'code_graph_query', 'my_retier_notices', 'list_page_grants', 'list_project_members', 'file_requests',
|
|
35
35
|
// live authoring core
|
|
36
36
|
'authoring_context', 'author', 'log_session',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// read_record — ONE record in full (Theron, 2026-10-07: "there's really no way to see what's inside a
|
|
2
|
+
// container like a class for the transcript or any other associated records").
|
|
3
|
+
//
|
|
4
|
+
// list_records gives a title and a summary per row. That is the right shape for a list and the wrong one
|
|
5
|
+
// for "what did the professor say": the transcript itself was not on the record at all until 2026-10-07,
|
|
6
|
+
// and even now it lives in the record's content, which only GET /api/records/:id returns. This renders
|
|
7
|
+
// that response — content, the pages it is filed on, the container around it, and, for a container, what
|
|
8
|
+
// is inside — so an agent can walk a class session the way a person does on /record.
|
|
9
|
+
|
|
10
|
+
/** Default cap on the content shown. A lecture runs ~50,000 characters; the client's reply budget is finite. */
|
|
11
|
+
export const READ_RECORD_DEFAULT_CHARS = 40_000
|
|
12
|
+
|
|
13
|
+
const SOURCE_LABEL = {
|
|
14
|
+
github: 'GitHub', email: 'Email', 'email-in': 'Email', gmail: 'Email', 'claude-code': 'Claude Code session',
|
|
15
|
+
imessage: 'Text messages', 'google-calendar': 'Calendar', 'agnoclast-calendar': 'Calendar',
|
|
16
|
+
handoff: 'Transcript or document', meeting: 'Meeting',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const label = (source) => SOURCE_LABEL[source] ?? source
|
|
20
|
+
const line = (e) => ` · ${e.title || '(untitled)'} — ${label(e.source)} · ${e.occurredAt} — id ${e.id}`
|
|
21
|
+
|
|
22
|
+
/** PURE. The tool's reply for one record. `maxChars` caps the content and says how much was left out. */
|
|
23
|
+
export function renderRecord(rec, maxChars = READ_RECORD_DEFAULT_CHARS) {
|
|
24
|
+
if (!rec || typeof rec !== 'object') return 'No record came back.'
|
|
25
|
+
const cap = Number.isFinite(maxChars) && maxChars > 0 ? Math.trunc(maxChars) : READ_RECORD_DEFAULT_CHARS
|
|
26
|
+
const out = []
|
|
27
|
+
out.push(`# ${rec.title || '(untitled record)'}`)
|
|
28
|
+
out.push(`${label(rec.source)} · ${rec.occurredAt} · brain ${rec.brain?.name ?? rec.brain ?? '?'}${rec.superseded ? ' · ⚠ superseded by a newer version' : ''}`)
|
|
29
|
+
out.push(`id: ${rec.id}`)
|
|
30
|
+
out.push('')
|
|
31
|
+
const content = typeof rec.content === 'string' ? rec.content : ''
|
|
32
|
+
if (!content.trim()) {
|
|
33
|
+
out.push('(No content was captured for this record.)')
|
|
34
|
+
} else if (content.length > cap) {
|
|
35
|
+
out.push(content.slice(0, cap))
|
|
36
|
+
out.push(`\n… ${content.length - cap} more characters. Call read_record again with max_chars: ${content.length} to read all of it.`)
|
|
37
|
+
} else {
|
|
38
|
+
out.push(content)
|
|
39
|
+
}
|
|
40
|
+
out.push('')
|
|
41
|
+
const pages = Array.isArray(rec.pages) ? rec.pages : []
|
|
42
|
+
out.push(pages.length
|
|
43
|
+
? `Filed on: ${pages.map((p) => `“${p.title}” (ref ${p.ref})`).join(' · ')}`
|
|
44
|
+
: 'Filed on: no page you can see.')
|
|
45
|
+
const inside = Array.isArray(rec.containedIn) ? rec.containedIn : []
|
|
46
|
+
if (inside.length) out.push(`Inside:\n${inside.map(line).join('\n')}`)
|
|
47
|
+
if (Array.isArray(rec.contains)) {
|
|
48
|
+
out.push(rec.contains.length
|
|
49
|
+
? `Contains (${rec.contains.length}) — read any with read_record:\n${rec.contains.map(line).join('\n')}`
|
|
50
|
+
: 'Contains: nothing has been filed inside this container yet.')
|
|
51
|
+
}
|
|
52
|
+
return out.join('\n')
|
|
53
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs
|
|
|
26
26
|
import { renderSettings } from './settings_render.mjs'
|
|
27
27
|
import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_render.mjs'
|
|
28
28
|
import { obligationsForUnits, renderFilingObligations, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
|
|
29
|
+
import { renderRecord, READ_RECORD_DEFAULT_CHARS } from './record_render.mjs'
|
|
29
30
|
import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
|
|
30
31
|
|
|
31
32
|
// PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
|
|
@@ -4522,10 +4523,12 @@ function renderNudge(payload) {
|
|
|
4522
4523
|
order: z.enum(['newest', 'oldest']).optional().describe('newest first (default), or oldest first to read a series in sequence'),
|
|
4523
4524
|
offset: z.number().optional().describe('skip this many records — page through more than 50 by calling again with offset 50, 100…'),
|
|
4524
4525
|
match: z.string().optional().describe('only records whose title or text contains this (case-insensitive, literal) — e.g. one book on a channel that carries several, one ticket in a queue'),
|
|
4526
|
+
container: z.string().optional().describe('a container\'s record id (a class session, a meeting) — lists the records INSIDE it. read_record on the container shows the same list with its own content'),
|
|
4525
4527
|
},
|
|
4526
4528
|
},
|
|
4527
|
-
async ({ type, project, since_days, limit, session, page, source, order, offset, match }) => {
|
|
4529
|
+
async ({ type, project, since_days, limit, session, page, source, order, offset, match, container }) => {
|
|
4528
4530
|
const qs = new URLSearchParams()
|
|
4531
|
+
if (container) qs.set('container', container)
|
|
4529
4532
|
if (page) qs.set('page', page)
|
|
4530
4533
|
if (match) qs.set('match', match)
|
|
4531
4534
|
if (source) qs.set('source', source)
|
|
@@ -4551,6 +4554,41 @@ function renderNudge(payload) {
|
|
|
4551
4554
|
},
|
|
4552
4555
|
)
|
|
4553
4556
|
|
|
4557
|
+
// read_record — ONE record in full, with what it sits inside and what sits inside it (Theron,
|
|
4558
|
+
// 2026-10-07: "there's really no way to see what's inside a container"). Rendering: record_render.mjs.
|
|
4559
|
+
server.registerTool(
|
|
4560
|
+
'read_record',
|
|
4561
|
+
{
|
|
4562
|
+
title: 'Read one record in full',
|
|
4563
|
+
description:
|
|
4564
|
+
'Read ONE record by id: its full content as captured (a lecture or meeting transcript, a text thread, ' +
|
|
4565
|
+
'an email\'s headers, a commit), the pages it is filed on, the container it sits inside (a class ' +
|
|
4566
|
+
'session, a meeting), and, when it IS a container, every record inside it. Use it to open what ' +
|
|
4567
|
+
'list_records, my_day or timeline_pull only summarize, and to walk a class session: read the session, ' +
|
|
4568
|
+
'then read_record each id it lists. A record you may not read answers "not found", exactly like one ' +
|
|
4569
|
+
'that does not exist.',
|
|
4570
|
+
inputSchema: {
|
|
4571
|
+
id: z.string().describe('the record id, from list_records, my_day, timeline_pull or another read_record'),
|
|
4572
|
+
max_chars: z.number().optional().describe(`cap on the content shown (default ${READ_RECORD_DEFAULT_CHARS}); the reply says how much was left out`),
|
|
4573
|
+
},
|
|
4574
|
+
},
|
|
4575
|
+
async ({ id, max_chars }) => {
|
|
4576
|
+
let res
|
|
4577
|
+
try {
|
|
4578
|
+
res = await fetchCortex(`${BASE}/api/records/${encodeURIComponent(id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
4579
|
+
} catch (e) {
|
|
4580
|
+
return toolError(`Could not read the record: ${e.message}`)
|
|
4581
|
+
}
|
|
4582
|
+
if (res.status === 404 || res.status === 400) return toolError(`No record ${id} that you can read.`)
|
|
4583
|
+
if (!res.ok) {
|
|
4584
|
+
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
4585
|
+
return toolError(`Could not read the record: ${d.message}`)
|
|
4586
|
+
}
|
|
4587
|
+
const body = await res.json().catch(() => null)
|
|
4588
|
+
return { content: [{ type: 'text', text: withAskNotice(renderRecord(body?.record, max_chars)) }] }
|
|
4589
|
+
},
|
|
4590
|
+
)
|
|
4591
|
+
|
|
4554
4592
|
// ── custom sources: set up, list, re-home (Theron, 2026-09-29) ─────────────────────────────────
|
|
4555
4593
|
// "I don't think it's that good of an idea to let the user do the whole setup without having an agent
|
|
4556
4594
|
// involved." The agent can read the system being connected, describe what its data is ABOUT, and judge
|
package/package.json
CHANGED