@theronap/cortex-mcp 0.9.93 → 0.9.95

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/capture.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'fs'
1
+ import { readFileSync, accessSync, constants } from 'fs'
2
2
  import { spawn, execFileSync } from 'child_process'
3
3
  import { homedir } from 'os'
4
4
  import { resolve, dirname, join } from 'path'
@@ -180,6 +180,16 @@ function readStdin() {
180
180
  // though only the tail ships) and append the flagged spans.
181
181
  const LIFECYCLE_RE = /\b(paus\w*|resum\w*|shipp?\w*|kill\w*|cancel\w*|on hold|sunset\w*)\b/i
182
182
 
183
+ // Whether the Stop hook's transcript_path actually resolves to a READABLE file.
184
+ //
185
+ // This distinguishes "no transcript was ever written" from "transcript exists but yielded no prose" —
186
+ // transcriptTail() flattens both into the same empty string, which is exactly why the empty-session
187
+ // guard below could never see the first case.
188
+ export function transcriptReadable(path) {
189
+ if (!path) return false
190
+ try { accessSync(path, constants.R_OK); return true } catch { return false }
191
+ }
192
+
183
193
  function transcriptTail(path) {
184
194
  let raw = ''
185
195
  try { raw = readFileSync(path, 'utf8') } catch { return '' }
@@ -272,6 +282,26 @@ async function captureWork(stdinRaw) {
272
282
  // must never be transmitted or stored. See redact.mjs.
273
283
  const transcript = hook.transcript_path ? redactSecrets(transcriptTail(hook.transcript_path)) : ''
274
284
 
285
+ // A transcript_path that was SUPPLIED but is unreadable means the session ran with transcript writes
286
+ // disabled — `claude --print --no-session-persistence`, or the SDK's `persistSession:false`. Verified
287
+ // A/B 2026-08-18: with the flag, Claude Code writes no JSONL at all; without it, one appears.
288
+ //
289
+ // The guard below cannot catch that case, and never could: `session_id` is ALWAYS present in a Stop
290
+ // payload, so `!transcript && !hook.session_id` can only ever fire on the session_id half. Capture
291
+ // therefore sailed straight on and shipped `{...common, transcript: ''}` — a contentless record that
292
+ // ALSO lost its repo stamp (repoFullNamesFrom throws on the missing file and is swallowed), leaving it
293
+ // permanently private. That is worse than capturing nothing: it reports `cortex: captured`, and it
294
+ // inflates the very record count that capture health is judged by (there is no freshness monitor).
295
+ //
296
+ // Safe to skip rather than degrade, because capture is not one-shot: the Stop hook fires after EVERY
297
+ // assistant turn and the server upserts ONE record per session_id. A transcript that is merely
298
+ // unreadable for a moment is picked up by the next turn, so a transient miss self-heals; only a
299
+ // genuinely persistence-disabled session is dropped, and that session has no content to capture.
300
+ if (hook.transcript_path && !transcriptReadable(hook.transcript_path)) {
301
+ process.stderr.write('cortex: transcript_path unreadable (session persistence disabled?), skipping\n')
302
+ return
303
+ }
304
+
275
305
  if (!transcript && !hook.session_id) { process.stderr.write('cortex: empty session, skipping\n'); return }
276
306
 
277
307
  // T11: digest node refs surfaced to this session (stashed by the MCP server's my_context, keyed by
package/lib/server.mjs CHANGED
@@ -916,6 +916,40 @@ export async function runServer(version) {
916
916
  },
917
917
  )
918
918
 
919
+ server.registerTool(
920
+ 'search_spam_email',
921
+ {
922
+ title: 'Search Gmail Spam intentionally',
923
+ description: 'Search a connected Gmail Spam folder only when the user explicitly asks you to find a message that may have been marked as Spam. This is read-only: results are returned for this request only and are NOT added to Agnoclast records, timeline, or future context. Give a specific Gmail search such as a sender, subject words, or date. Normal Gmail sync never reads Spam.',
924
+ inputSchema: {
925
+ query: z.string().min(2).describe('specific Gmail search within Spam, e.g. `from:billing@example.com`, `subject:(appointment reminder)`, or `after:2026/08/01`'),
926
+ account: z.string().email().optional().describe('which connected Gmail account to search when more than one is available'),
927
+ limit: z.number().int().min(1).max(10).optional().describe('maximum matches to return (default 5; max 10)'),
928
+ },
929
+ },
930
+ async ({ query, account, limit }) => {
931
+ const res = await fetchCortex(`${BASE}/api/gmail/search-spam`, {
932
+ method: 'POST',
933
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
934
+ body: JSON.stringify({ query, ...(account ? { account } : {}), ...(limit ? { limit } : {}) }),
935
+ })
936
+ const body = await res.json().catch(() => null)
937
+ if (!res.ok) {
938
+ if (res.status === 409 && Array.isArray(body?.accounts)) {
939
+ return toolError(`Choose a connected Gmail account and call again with account: ${body.accounts.join(', ')}`)
940
+ }
941
+ return toolError(body?.error ?? `Spam search failed (${res.status}).`)
942
+ }
943
+ const rows = Array.isArray(body?.messages) ? body.messages : []
944
+ if (!rows.length) return { content: [{ type: 'text', text: `No Spam matches in ${body?.account ?? 'the connected Gmail account'}. Nothing was saved to Agnoclast.` }] }
945
+ const lines = [`Spam matches in ${body.account} (read-only; not saved to Agnoclast):`]
946
+ for (const m of rows) {
947
+ lines.push(`- ${m.date || 'unknown date'} · ${m.from || 'unknown sender'} · ${m.subject}\n ${m.snippet || '(no preview)'}\n ${m.sourceUri}`)
948
+ }
949
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
950
+ },
951
+ )
952
+
919
953
  server.registerTool(
920
954
  'grep',
921
955
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.93",
3
+ "version": "0.9.95",
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": {