@theronap/agnoclast-mcp 0.9.200 → 0.9.202

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/server.mjs CHANGED
@@ -20,6 +20,7 @@ import { repoFullNameFrom, workingFilesFrom } from './capture.mjs'
20
20
  import { findTranscript, createWorkedFilesTracker } from './working_files.mjs'
21
21
  import { spawnObligationWorker } from './obligations_worker.mjs'
22
22
  import { renderObligations, renderPageObligations, tierReads, who } from './obligations_render.mjs'
23
+ import { renderSettings } from './settings_render.mjs'
23
24
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
24
25
 
25
26
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
@@ -2837,57 +2838,8 @@ function renderNudge(payload) {
2837
2838
  },
2838
2839
  )
2839
2840
 
2840
- server.registerTool(
2841
- 'obligations_from_calendar',
2842
- {
2843
- title: 'Meetings that ended and were never written up',
2844
- description: 'Turn meetings that have already ENDED into "capture what came out of this" obligations. ⚠ THE OBLIGATION IS NOT "ATTEND" — your calendar already does that, and putting every scheduled meeting into your obligations list would bury it (90 of 120 calendar records are recurring birthdays). The gap this fills is real and documented: meeting substance lands wherever the session that heard about it happened to be working, and a walkthrough with no note at all is the normal outcome. ⚠ DRY-RUN BY DEFAULT — it reports what it WOULD create, including every meeting it skipped and why, and writes nothing until apply: true. Excluded on purpose: meetings with no attendee but you (a birthday or a solo hold owes nobody anything), all-day markers, cancelled events, anything with no recorded end, and anything outside the lookback window — no backfill, because obligations nobody will ever action are how a list stops being read. Each one is discharged by a captured transcript ARRIVING, never by one being absent.',
2845
- inputSchema: {
2846
- lookback_days: z.number().optional().describe('how far back to sweep (default 14, max 90)'),
2847
- apply: z.boolean().optional().describe('false/absent = dry run (default). true actually creates them.'),
2848
- },
2849
- },
2850
- async ({ lookback_days, apply }) => {
2851
- let res
2852
- try {
2853
- res = await fetchCortex(`${BASE}/api/obligations/from-calendar`, {
2854
- method: 'POST',
2855
- headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2856
- body: JSON.stringify({ lookbackDays: lookback_days, apply: apply === true }),
2857
- })
2858
- } catch (e) { return toolError(`Could not reach the producer: ${e.message}`) }
2859
- const out = await res.json().catch(() => null)
2860
- if (!res.ok) return toolError(`Could not run: ${out?.error ?? res.status}`)
2861
-
2862
- const cands = out?.candidates ?? []
2863
- const eligible = cands.filter((c) => !c.skipped)
2864
- const lines = []
2865
- if (out.mode === 'dry-run') {
2866
- lines.push(`DRY RUN \u2014 nothing written. ${out.scanned} meeting(s) scanned, ${eligible.length} would become obligations.`)
2867
- } else {
2868
- lines.push(`${out.created} obligation(s) created from ${out.scanned} meeting(s) scanned.`)
2869
- }
2870
- if (eligible.length) {
2871
- lines.push('', 'Would track:')
2872
- for (const c of eligible.slice(0, 15)) {
2873
- lines.push(` ${c.endedAt.slice(0, 10)} ${c.title ?? '(untitled)'} \u2014 ${c.attendees.length} attendee(s)`)
2874
- }
2875
- }
2876
- // \u26a0 THE SKIPS ARE PRINTED, NOT SUMMARISED AWAY. A producer that silently drops most of its
2877
- // input reads as "there was nothing to do", which is indistinguishable from a broken filter.
2878
- const skipped = cands.filter((c) => c.skipped)
2879
- if (skipped.length) {
2880
- const byReason = {}
2881
- for (const c of skipped) byReason[c.skipped] = (byReason[c.skipped] ?? 0) + 1
2882
- lines.push('', `Skipped ${skipped.length}:`)
2883
- for (const [reason, n] of Object.entries(byReason).sort((a, b) => b[1] - a[1])) {
2884
- lines.push(` ${n} ${reason}`)
2885
- }
2886
- }
2887
- if (out.mode === 'dry-run' && eligible.length) lines.push('', 'Re-run with apply: true to create them.')
2888
- return { content: [{ type: 'text', text: lines.join('\n') }] }
2889
- },
2890
- )
2841
+ // `obligations_from_calendar` was removed in 0.9.201 (2026-10-02): meetings no longer create
2842
+ // obligations by themselves. The server answers 410 to older clients that still offer it.
2891
2843
 
2892
2844
  server.registerTool(
2893
2845
  'my_obligations',
@@ -3029,6 +2981,58 @@ function renderNudge(payload) {
3029
2981
  },
3030
2982
  )
3031
2983
 
2984
+ // ADR-0074 — a person's own settings. The August 2026 ruling is "notices are on by default, and you
2985
+ // turn one off by telling the assistant"; these two tools are how the assistant does that.
2986
+ server.registerTool(
2987
+ 'my_settings',
2988
+ {
2989
+ title: 'My settings',
2990
+ description: "The person's own Agnoclast settings: which notices they get without asking (meetings about to start, deadlines within the hour, what is due today, hand-offs, lapses) and their timezone. The same settings as the Settings page in the console. Read this before answering \"why am I (not) being told about X\".",
2991
+ inputSchema: {},
2992
+ },
2993
+ async () => {
2994
+ let res
2995
+ try {
2996
+ res = await fetchCortex(`${BASE}/api/settings`, { headers: { Authorization: `Bearer ${TOKEN}` } })
2997
+ } catch (e) { return toolError(`Could not reach settings: ${e.message}`) }
2998
+ const out = await res.json().catch(() => null)
2999
+ if (!res.ok) return toolError(`Could not read settings: ${out?.detail ?? out?.error ?? res.status}`)
3000
+ return { content: [{ type: 'text', text: renderSettings(out) }] }
3001
+ },
3002
+ )
3003
+
3004
+ server.registerTool(
3005
+ 'set_setting',
3006
+ {
3007
+ title: 'Change one of my settings',
3008
+ description: "Change one of the person's own settings — ONLY when they asked you to (\"stop telling me what's due today\", \"set my timezone to Denver\"). Never on your own initiative, and never to quiet something you find noisy: a notice the person did not turn off is one they rely on. Pass `key` and `value` for a notice (keys from my_settings, e.g. notices.due_today = false), or `timezone` alone with an IANA name such as America/Denver. Say what you changed, and that they can change it back in Settings.",
3009
+ inputSchema: {
3010
+ key: z.string().optional().describe('the setting, exactly as my_settings lists it, e.g. notices.due_today'),
3011
+ value: z.boolean().optional().describe('true = on, false = off'),
3012
+ timezone: z.string().optional().describe('instead of key/value: an IANA timezone, e.g. America/Denver'),
3013
+ },
3014
+ },
3015
+ async ({ key, value, timezone }) => {
3016
+ const body = timezone ? { timezone } : { key, value }
3017
+ let res
3018
+ try {
3019
+ res = await fetchCortex(`${BASE}/api/settings`, {
3020
+ method: 'POST',
3021
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3022
+ body: JSON.stringify(body),
3023
+ })
3024
+ } catch (e) { return toolError(`Could not reach settings: ${e.message}`) }
3025
+ const out = await res.json().catch(() => null)
3026
+ if (!res.ok) return toolError(`Not changed: ${out?.detail ?? out?.error ?? res.status}`)
3027
+ const text = timezone
3028
+ ? `Timezone set to ${out.timezone} on ${out.memberships} brain${out.memberships === 1 ? '' : 's'}. They can change it in Settings.`
3029
+ : !out.changed
3030
+ ? `${out.key} was already ${out.value ? 'on' : 'off'} — nothing changed.`
3031
+ : `${out.key} is now ${out.value ? 'ON' : 'OFF'}. They can turn it back ${out.value ? 'off' : 'on'} in Settings (or ask you).`
3032
+ return { content: [{ type: 'text', text }] }
3033
+ },
3034
+ )
3035
+
3032
3036
  server.registerTool(
3033
3037
  'set_obligation_tier',
3034
3038
  {
@@ -0,0 +1,21 @@
1
+ // PURE: what my_settings prints (ADR-0074). Out of server.mjs so a test can reach it.
2
+ //
3
+ // Says ON/OFF per notice with the person's own words for it, and names the default when a setting has
4
+ // been changed from it — "off (default on)" is the line that answers "why am I not hearing about X?".
5
+ const one = (s) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 160)
6
+
7
+ export function renderSettings(view) {
8
+ const settings = Array.isArray(view?.settings) ? view.settings : []
9
+ const lines = ['NOTICES — what Agnoclast tells this person without being asked (sessions and desktop):']
10
+ for (const s of settings) {
11
+ const state = s.value ? 'ON ' : 'OFF'
12
+ const changed = s.value !== s.default ? ` (changed — default ${s.default ? 'on' : 'off'})` : ''
13
+ lines.push(` ${state} ${one(s.key)} — ${one(s.label)}${changed}`)
14
+ }
15
+ if (!settings.length) lines.push(' (none returned — the server may predate settings)')
16
+ lines.push('')
17
+ lines.push(`TIMEZONE: ${view?.timezone ? one(view.timezone) : 'not set'}`)
18
+ lines.push('')
19
+ lines.push('Change one only when the person asks: set_setting. They can also change these in Settings (console avatar menu, or ⌘, in the desktop app).')
20
+ return lines.join('\n')
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.200",
3
+ "version": "0.9.202",
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": {