@theronap/agnoclast-mcp 0.9.219 → 0.9.221

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.
@@ -10,6 +10,12 @@
10
10
  /** Default cap on the content shown. A lecture runs ~50,000 characters; the client's reply budget is finite. */
11
11
  export const READ_RECORD_DEFAULT_CHARS = 40_000
12
12
 
13
+ /** What read_record returns of a transcript (Theron, 2026-10-08: "the agent should first look at the notes"). */
14
+ export const READ_RECORD_PARTS = ['notes', 'transcript', 'all']
15
+
16
+ /** Sources whose content is a transcript (web/lib/engine/transcript_retention.ts TRANSCRIPT_KEPT_SOURCES). */
17
+ const TRANSCRIPT_SOURCES = new Set(['handoff', 'meeting'])
18
+
13
19
  const SOURCE_LABEL = {
14
20
  github: 'GitHub', email: 'Email', 'email-in': 'Email', gmail: 'Email', 'claude-code': 'Claude Code session',
15
21
  imessage: 'Text messages', 'google-calendar': 'Calendar', 'agnoclast-calendar': 'Calendar',
@@ -19,8 +25,15 @@ const SOURCE_LABEL = {
19
25
  const label = (source) => SOURCE_LABEL[source] ?? source
20
26
  const line = (e) => ` · ${e.title || '(untitled)'} — ${label(e.source)} · ${e.occurredAt} — id ${e.id}`
21
27
 
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) {
28
+ /**
29
+ * PURE. The tool's reply for one record. `maxChars` caps the content and says how much was left out.
30
+ *
31
+ * TWO LAYERS ON A TRANSCRIPT (Theron, 2026-10-08): a meeting or lecture carries NOTES (the gist, decisions,
32
+ * action items, written by whoever filed it) and the full transcript. By default a record WITH notes returns
33
+ * the notes and says how long the transcript is, so a session gets the gist without reading 50,000
34
+ * characters; `part: "transcript"` or `"all"` returns the text. A record without notes is unchanged.
35
+ */
36
+ export function renderRecord(rec, maxChars = READ_RECORD_DEFAULT_CHARS, part = 'notes') {
24
37
  if (!rec || typeof rec !== 'object') return 'No record came back.'
25
38
  const cap = Number.isFinite(maxChars) && maxChars > 0 ? Math.trunc(maxChars) : READ_RECORD_DEFAULT_CHARS
26
39
  const out = []
@@ -29,14 +42,28 @@ export function renderRecord(rec, maxChars = READ_RECORD_DEFAULT_CHARS) {
29
42
  out.push(`id: ${rec.id}`)
30
43
  out.push('')
31
44
  const content = typeof rec.content === 'string' ? rec.content : ''
32
- if (!content.trim()) {
45
+ const notes = typeof rec.notes === 'string' && rec.notes.trim() ? rec.notes.trim() : null
46
+ const want = READ_RECORD_PARTS.includes(part) ? part : 'notes'
47
+ if (notes && want !== 'transcript') {
48
+ out.push('## Notes')
49
+ out.push(notes)
50
+ out.push('')
51
+ }
52
+ if (notes && want === 'notes') {
53
+ out.push(`Full transcript: ${content.length.toLocaleString('en-US')} characters, not shown. Call read_record with part: "transcript" when you need the exact words; grep and search already cover it.`)
54
+ } else if (!content.trim()) {
33
55
  out.push('(No content was captured for this record.)')
34
56
  } else if (content.length > cap) {
57
+ if (notes) out.push('## Full transcript')
35
58
  out.push(content.slice(0, cap))
36
59
  out.push(`\n… ${content.length - cap} more characters. Call read_record again with max_chars: ${content.length} to read all of it.`)
37
60
  } else {
61
+ if (notes) out.push('## Full transcript')
38
62
  out.push(content)
39
63
  }
64
+ if (!notes && TRANSCRIPT_SOURCES.has(rec.source) && content.length > 2000) {
65
+ out.push(`\n(No notes on this transcript yet. If it is yours, write them with set_record_notes: the gist, decisions and action items, names spelled right.)`)
66
+ }
40
67
  out.push('')
41
68
  const pages = Array.isArray(rec.pages) ? rec.pages : []
42
69
  out.push(pages.length
package/lib/server.mjs CHANGED
@@ -26,7 +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
+ import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './record_render.mjs'
30
30
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
31
31
  import { formatSends } from './sends_render.mjs'
32
32
 
@@ -1234,6 +1234,7 @@ function renderNudge(payload) {
1234
1234
  'Name the governing page with governingPage (one of documentIds, never a person page) — otherwise the ranking picks it. ' +
1235
1235
  'Pass mode "preview" first when your choice differs from the claim\'s proposal: it returns the pages, the governing page and the tier this exact call would produce, and writes nothing. Never writes private intake into search/history before this call. ' +
1236
1236
  '⚠ ADR-0051: a unit does not finish until you looked for its OTHER homes. A call whose documentIds add nothing beyond the deterministic homes it gets anyway is REFUSED once — name the pages this unit is about (the design it implements, the project, the person), or go look, or say you looked with searchedForOtherHomes. ' +
1237
+ '⚠ A meeting or lecture TRANSCRIPT files with NOTES in `summary` (Theron, 2026-10-08): the gist, decisions, action items and dates, names spelled right. Without them the call is REFUSED; the transcript itself is kept on the record unchanged. ' +
1237
1238
  '⚠ ADR-0077: a GitHub commit or PR does not finish until every changed path has an answer. If a changed file\'s deepest folder (or, in a grab-bag folder like web/lib/engine/ or db/supabase/, the file itself) has no claim and no decline, the call is REFUSED with the open questions; answer each in folderDecisions and call again.',
1238
1239
  inputSchema: {
1239
1240
  intakeItemId: z.string().describe('intake item uuid'),
@@ -1246,7 +1247,10 @@ function renderNudge(payload) {
1246
1247
  orgId: z.string().describe('destination brain org uuid'),
1247
1248
  documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page `ref:` exactly as read_page shows it, or a brain_documents id (deterministic homes are merged automatically)'),
1248
1249
  title: z.string().optional(),
1249
- summary: z.string().optional(),
1250
+ summary: z.string().optional().describe(
1251
+ 'what future retrieval reads first. For a meeting or lecture TRANSCRIPT these are its NOTES, and they are required: ' +
1252
+ 'a few lines of gist, then decisions, action items and dates, with names spelled right. The transcript is kept as it is.',
1253
+ ),
1250
1254
  source: z.string().optional(),
1251
1255
  recordType: z.string().optional(),
1252
1256
  dedupeKey: z.string().optional(),
@@ -1375,6 +1379,11 @@ function renderNudge(payload) {
1375
1379
  ))
1376
1380
  }
1377
1381
  if (err === 'obligations_unanswered') return toolError(OBLIGATIONS_UNANSWERED_TEXT)
1382
+ if (err === 'notes_required') {
1383
+ let parsed = null
1384
+ try { parsed = JSON.parse(body) } catch { /* handled above */ }
1385
+ return toolError(`Not filed yet: this unit is a transcript and ${parsed?.problem ?? 'it has no notes'}. ${parsed?.detail ?? 'Pass summary as the notes and call again.'}`)
1386
+ }
1378
1387
  }
1379
1388
  if (preview && res.status === 404) {
1380
1389
  return toolError('This server cannot preview a materialization yet — nothing was written. Materialize without mode, or wait for the server to update.')
@@ -4571,13 +4580,21 @@ function renderNudge(payload) {
4571
4580
  'session, a meeting), and, when it IS a container, every record inside it. Use it to open what ' +
4572
4581
  'list_records, my_day or timeline_pull only summarize, and to walk a class session: read the session, ' +
4573
4582
  'then read_record each id it lists. A record you may not read answers "not found", exactly like one ' +
4574
- 'that does not exist.',
4583
+ 'that does not exist. ' +
4584
+ 'A meeting or lecture transcript has TWO LAYERS: notes (the gist, decisions, action items) and the full ' +
4585
+ 'transcript. By default you get the NOTES and the transcript\'s length; read the notes first, and ask for ' +
4586
+ 'part: "transcript" only when you need the exact words. To FIND something said in a meeting, grep and ' +
4587
+ 'search already cover the transcript.',
4575
4588
  inputSchema: {
4576
4589
  id: z.string().describe('the record id, from list_records, my_day, timeline_pull or another read_record'),
4577
4590
  max_chars: z.number().optional().describe(`cap on the content shown (default ${READ_RECORD_DEFAULT_CHARS}); the reply says how much was left out`),
4591
+ part: z.enum(READ_RECORD_PARTS).optional().describe(
4592
+ 'for a transcript with notes: "notes" (default) = the notes only; "transcript" = the full text only; "all" = both. ' +
4593
+ 'A record without notes always returns its content.',
4594
+ ),
4578
4595
  },
4579
4596
  },
4580
- async ({ id, max_chars }) => {
4597
+ async ({ id, max_chars, part }) => {
4581
4598
  let res
4582
4599
  try {
4583
4600
  res = await fetchCortex(`${BASE}/api/records/${encodeURIComponent(id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
@@ -4590,7 +4607,46 @@ function renderNudge(payload) {
4590
4607
  return toolError(`Could not read the record: ${d.message}`)
4591
4608
  }
4592
4609
  const body = await res.json().catch(() => null)
4593
- return { content: [{ type: 'text', text: withAskNotice(renderRecord(body?.record, max_chars)) }] }
4610
+ return { content: [{ type: 'text', text: withAskNotice(renderRecord(body?.record, max_chars, part)) }] }
4611
+ },
4612
+ )
4613
+
4614
+ // set_record_notes — write the NOTES layer of one of your own transcripts (Theron, 2026-10-08: "two
4615
+ // layers … a summary and notes from the transcript … the full transcript"). The server keeps the
4616
+ // before-state in record history and refuses anything that is not your own kept transcript.
4617
+ server.registerTool(
4618
+ 'set_record_notes',
4619
+ {
4620
+ title: 'Write the notes on a transcript record',
4621
+ description:
4622
+ 'Write or rewrite the NOTES on one of your own meeting or lecture transcripts: the layer read_record shows ' +
4623
+ 'before the transcript. Read the transcript first (read_record part: "transcript"). Good notes: a few lines of ' +
4624
+ 'gist, then decisions, action items and dates, with people\'s names spelled correctly (check them against person ' +
4625
+ 'pages; speech recognition misspells names). Never change the transcript itself: obligations quote it word for ' +
4626
+ 'word. Only your own records, and only transcripts; anything else answers not found or not a transcript.',
4627
+ inputSchema: {
4628
+ record_id: z.string().describe('the transcript record id'),
4629
+ notes: z.string().describe('the notes, in plain text or simple markdown; the short layer, so well under 12,000 characters'),
4630
+ },
4631
+ },
4632
+ async ({ record_id, notes }) => {
4633
+ let res
4634
+ try {
4635
+ res = await fetchCortex(`${BASE}/api/records/${encodeURIComponent(record_id)}/notes`, {
4636
+ method: 'POST',
4637
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
4638
+ body: JSON.stringify({ notes }),
4639
+ })
4640
+ } catch (e) {
4641
+ return toolError(`Could not reach Agnoclast: ${e.message}`)
4642
+ }
4643
+ const body = await res.json().catch(() => ({}))
4644
+ if (res.ok && body?.ok) return { content: [{ type: 'text', text: `Notes saved on ${record_id}. read_record now shows them before the transcript.` }] }
4645
+ if (res.status === 404 && !body?.error) return toolError('This server does not take transcript notes yet. Nothing was written.')
4646
+ if (res.status === 404) return toolError(`No transcript ${record_id} of yours. Notes go only on your own records.`)
4647
+ if (res.status === 409) return toolError(`${record_id} is not a transcript (${body?.detail ?? 'no kept text'}). Nothing was written.`)
4648
+ if (res.status === 422) return toolError(`Not saved: ${body?.detail ?? 'the notes were refused'}.`)
4649
+ return toolError(`Could not save the notes: HTTP ${res.status} ${JSON.stringify(body)}`)
4594
4650
  },
4595
4651
  )
4596
4652
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.219",
3
+ "version": "0.9.221",
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": {