@theronap/cortex-mcp 0.9.59 → 0.9.60
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 +76 -64
- package/package.json +1 -1
- package/skills/log/SKILL.md +18 -16
package/lib/server.mjs
CHANGED
|
@@ -28,6 +28,18 @@ async function redLinkTriage(BASE, TOKEN, name) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// MCP tool results carry an `isError` flag, and a failure that omits it is indistinguishable from a
|
|
32
|
+
// success at the protocol level — the caller just sees a normal result whose text happens to begin
|
|
33
|
+
// "Could not". This file had 41 tools, 64 failure returns and ZERO uses of isError, so nothing
|
|
34
|
+
// downstream could tell a rejected write from a completed one. Measured 2026-07-31: a walker over
|
|
35
|
+
// 397 transcripts scored 18 REJECTED `author` calls as successful writes for exactly this reason,
|
|
36
|
+
// and an agent skimming its own tool result is exposed the same way. Route every failure through
|
|
37
|
+
// here so the flag cannot be forgotten at a new call site.
|
|
38
|
+
//
|
|
39
|
+
// NAME: deliberately not `fail` — runServer already has a local `fail(verb, res)` for the
|
|
40
|
+
// file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
|
|
41
|
+
const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
|
|
42
|
+
|
|
31
43
|
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
32
44
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
33
45
|
|
|
@@ -406,12 +418,12 @@ export async function runServer(version) {
|
|
|
406
418
|
if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
|
|
407
419
|
if (!vr.ok) {
|
|
408
420
|
const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
|
|
409
|
-
return
|
|
421
|
+
return toolError(`Could not read version "${version}" of "${name}": ${d.message}`)
|
|
410
422
|
}
|
|
411
423
|
const v = await vr.json()
|
|
412
424
|
return { content: [{ type: 'text', text: `# ${name} — historical version (rev ${v.revNo} · ${v.op} · ${String(v.createdAt).slice(0, 10)} · ${v.tier})\nversion: ${v.version}\n\n${v.body}\n\n— This is a HISTORICAL snapshot, not the current page. \`read_page "${name}"\` (no version) shows what's live; \`rollback_page\` restores this one as a new version.` }] }
|
|
413
425
|
} catch (e) {
|
|
414
|
-
return
|
|
426
|
+
return toolError(`Could not read version "${version}" of "${name}": ${e.message}`)
|
|
415
427
|
}
|
|
416
428
|
}
|
|
417
429
|
// PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
|
|
@@ -422,7 +434,7 @@ export async function runServer(version) {
|
|
|
422
434
|
if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
|
|
423
435
|
if (!r.ok) {
|
|
424
436
|
const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
|
|
425
|
-
return
|
|
437
|
+
return toolError(`Could not read the timeline for "${name}": ${d.message}`)
|
|
426
438
|
}
|
|
427
439
|
const t = await r.json()
|
|
428
440
|
if (!t.identifiers?.length) {
|
|
@@ -435,7 +447,7 @@ export async function runServer(version) {
|
|
|
435
447
|
if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
|
|
436
448
|
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
437
449
|
} catch (e) {
|
|
438
|
-
return
|
|
450
|
+
return toolError(`Could not read the timeline for "${name}": ${e.message}`)
|
|
439
451
|
}
|
|
440
452
|
}
|
|
441
453
|
// IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
|
|
@@ -474,14 +486,14 @@ export async function runServer(version) {
|
|
|
474
486
|
res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
|
|
475
487
|
{ headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
476
488
|
} catch (e) {
|
|
477
|
-
return
|
|
489
|
+
return toolError(`Could not read "${name}": ${e.message}`)
|
|
478
490
|
}
|
|
479
491
|
if (res.status === 404) {
|
|
480
492
|
return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
|
|
481
493
|
}
|
|
482
494
|
if (!res.ok) {
|
|
483
495
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
484
|
-
return
|
|
496
|
+
return toolError(`Could not read "${name}": ${d.message}`)
|
|
485
497
|
}
|
|
486
498
|
const page = await res.json()
|
|
487
499
|
// Multi-brain (decision 3A): /api/brain/page returns matches[] — one entry per brain that has a
|
|
@@ -559,12 +571,12 @@ export async function runServer(version) {
|
|
|
559
571
|
const qs = new URLSearchParams({ kind: k, key: name, ...(limit ? { limit: String(limit) } : {}) })
|
|
560
572
|
res = await fetchCortex(`${BASE}/api/brain/page-history?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
561
573
|
} catch (e) {
|
|
562
|
-
return
|
|
574
|
+
return toolError(`Could not read history for "${name}": ${e.message}`)
|
|
563
575
|
}
|
|
564
576
|
if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}". Pass kind (person/org) if it isn't a project.` }] }
|
|
565
577
|
if (!res.ok) {
|
|
566
578
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
567
|
-
return
|
|
579
|
+
return toolError(`Could not read history for "${name}": ${d.message}`)
|
|
568
580
|
}
|
|
569
581
|
const out = await res.json()
|
|
570
582
|
const revs = out.revisions ?? []
|
|
@@ -612,12 +624,12 @@ export async function runServer(version) {
|
|
|
612
624
|
})
|
|
613
625
|
res = await fetchCortex(`${BASE}/api/brain/page-diff?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
614
626
|
} catch (e) {
|
|
615
|
-
return
|
|
627
|
+
return toolError(`Could not diff "${name}": ${e.message}`)
|
|
616
628
|
}
|
|
617
629
|
if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}", or it has no version "${to}". Run \`page_history "${name}"\` to list its versions.` }] }
|
|
618
630
|
if (!res.ok) {
|
|
619
631
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
620
|
-
return
|
|
632
|
+
return toolError(`Could not diff "${name}": ${d.message}`)
|
|
621
633
|
}
|
|
622
634
|
const out = await res.json()
|
|
623
635
|
const d = out.diff
|
|
@@ -669,10 +681,10 @@ export async function runServer(version) {
|
|
|
669
681
|
body: JSON.stringify({ kind: k, name, to_version, ...(tier ? { tier } : {}), ...(reason ? { reason } : {}) }),
|
|
670
682
|
})
|
|
671
683
|
} catch (e) {
|
|
672
|
-
return
|
|
684
|
+
return toolError(`Could not roll back "${name}": ${e.message}`)
|
|
673
685
|
}
|
|
674
686
|
const out = await res.json().catch(() => null)
|
|
675
|
-
if (!res.ok) return
|
|
687
|
+
if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
|
|
676
688
|
return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
|
|
677
689
|
},
|
|
678
690
|
)
|
|
@@ -705,7 +717,7 @@ export async function runServer(version) {
|
|
|
705
717
|
}),
|
|
706
718
|
})
|
|
707
719
|
} catch (e) {
|
|
708
|
-
return
|
|
720
|
+
return toolError(`Could not rename the section: ${e.message}`)
|
|
709
721
|
}
|
|
710
722
|
const out = await res.json().catch(() => null)
|
|
711
723
|
if (!res.ok) {
|
|
@@ -718,7 +730,7 @@ export async function runServer(version) {
|
|
|
718
730
|
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
719
731
|
].filter(Boolean).join(' · ')
|
|
720
732
|
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
721
|
-
return
|
|
733
|
+
return toolError(`Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
722
734
|
}
|
|
723
735
|
return { content: [{ type: 'text', text: `Renamed on "${name}" (${out.brain} · ${out.tier} tier): "${out.from}" -> "${out.to}". The section kept its body, position and as-of date. New version: ${out.version}` }] }
|
|
724
736
|
},
|
|
@@ -753,7 +765,7 @@ export async function runServer(version) {
|
|
|
753
765
|
}),
|
|
754
766
|
})
|
|
755
767
|
} catch (e) {
|
|
756
|
-
return
|
|
768
|
+
return toolError(`Could not edit "${name}": ${e.message}`)
|
|
757
769
|
}
|
|
758
770
|
const out = await res.json().catch(() => null)
|
|
759
771
|
if (!res.ok) {
|
|
@@ -784,7 +796,7 @@ export async function runServer(version) {
|
|
|
784
796
|
Array.isArray(out?.concurrentRemoved) && out.concurrentRemoved.length ? `sections REMOVED elsewhere: ${out.concurrentRemoved.join(', ')}` : '',
|
|
785
797
|
Array.isArray(out?.concurrentTruncated) && out.concurrentTruncated.length ? `(truncated, see page_diff for all of: ${out.concurrentTruncated.join(', ')})` : '',
|
|
786
798
|
].filter(Boolean).join('\n')
|
|
787
|
-
return
|
|
799
|
+
return toolError(`Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}`)
|
|
788
800
|
}
|
|
789
801
|
const red = Array.isArray(out.redLinks) && out.redLinks.length
|
|
790
802
|
? `\nRed-links now on this page: ${out.redLinks.map((r) => `[[${r}]]`).join(', ')}` : ''
|
|
@@ -804,11 +816,11 @@ export async function runServer(version) {
|
|
|
804
816
|
try {
|
|
805
817
|
res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
806
818
|
} catch (e) {
|
|
807
|
-
return
|
|
819
|
+
return toolError(`Could not load writing style: ${e.message}`)
|
|
808
820
|
}
|
|
809
821
|
if (!res.ok) {
|
|
810
822
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
811
|
-
return
|
|
823
|
+
return toolError(`Could not load writing style: ${d.message}`)
|
|
812
824
|
}
|
|
813
825
|
const { style } = await res.json()
|
|
814
826
|
return { content: [{ type: 'text', text: style }] }
|
|
@@ -831,11 +843,11 @@ export async function runServer(version) {
|
|
|
831
843
|
body: JSON.stringify({ style_md }),
|
|
832
844
|
})
|
|
833
845
|
} catch (e) {
|
|
834
|
-
return
|
|
846
|
+
return toolError(`Could not save writing style: ${e.message}`)
|
|
835
847
|
}
|
|
836
848
|
if (!res.ok) {
|
|
837
849
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
838
|
-
return
|
|
850
|
+
return toolError(`Could not save writing style: ${d.message}`)
|
|
839
851
|
}
|
|
840
852
|
const r = await res.json()
|
|
841
853
|
return { content: [{ type: 'text', text: r.saved ? `Saved your writing-style profile (${r.chars} chars).` : 'Cleared your writing-style profile.' }] }
|
|
@@ -854,11 +866,11 @@ export async function runServer(version) {
|
|
|
854
866
|
try {
|
|
855
867
|
res = await fetchCortex(`${BASE}/api/brains`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
856
868
|
} catch (e) {
|
|
857
|
-
return
|
|
869
|
+
return toolError(`Could not list brains: ${e.message}`)
|
|
858
870
|
}
|
|
859
871
|
if (!res.ok) {
|
|
860
872
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
861
|
-
return
|
|
873
|
+
return toolError(`Could not list brains: ${d.message}`)
|
|
862
874
|
}
|
|
863
875
|
const { brains, activeIsExplicit, activeSource, sessionOrgId, accountOrgId } = await res.json()
|
|
864
876
|
if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
|
|
@@ -898,11 +910,11 @@ export async function runServer(version) {
|
|
|
898
910
|
try {
|
|
899
911
|
res = await fetchCortex(`${BASE}/api/brains/pages?orgId=${encodeURIComponent(org_id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
900
912
|
} catch (e) {
|
|
901
|
-
return
|
|
913
|
+
return toolError(`Could not list pages: ${e.message}`)
|
|
902
914
|
}
|
|
903
915
|
if (!res.ok) {
|
|
904
916
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
905
|
-
return
|
|
917
|
+
return toolError(`Could not list pages: ${d.message}`)
|
|
906
918
|
}
|
|
907
919
|
const { nodeCount, rowCount, pages } = await res.json()
|
|
908
920
|
if (!pages?.length) return { content: [{ type: 'text', text: `Brain ${org_id} has no authored pages.` }] }
|
|
@@ -939,11 +951,11 @@ export async function runServer(version) {
|
|
|
939
951
|
body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
|
|
940
952
|
})
|
|
941
953
|
} catch (e) {
|
|
942
|
-
return
|
|
954
|
+
return toolError(`Could not set active brain: ${e.message}`)
|
|
943
955
|
}
|
|
944
956
|
if (!res.ok) {
|
|
945
957
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
946
|
-
return
|
|
958
|
+
return toolError(`Could not set active brain: ${d.message}`)
|
|
947
959
|
}
|
|
948
960
|
const r = await res.json()
|
|
949
961
|
// Always say WHICH scope changed. The default is session-only, so a caller expecting the old
|
|
@@ -974,11 +986,11 @@ export async function runServer(version) {
|
|
|
974
986
|
body: JSON.stringify({ name }),
|
|
975
987
|
})
|
|
976
988
|
} catch (e) {
|
|
977
|
-
return
|
|
989
|
+
return toolError(`Could not create brain: ${e.message}`)
|
|
978
990
|
}
|
|
979
991
|
if (!res.ok) {
|
|
980
992
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
981
|
-
return
|
|
993
|
+
return toolError(`Could not create brain: ${d.message}`)
|
|
982
994
|
}
|
|
983
995
|
const r = await res.json()
|
|
984
996
|
return { content: [{ type: 'text', text: `Created brain "${name}" [${r.orgId}]. Reads already span it. Edits to pages in it route themselves; set_active_brain only if you want NEW pages to default here.` }] }
|
|
@@ -1007,11 +1019,11 @@ export async function runServer(version) {
|
|
|
1007
1019
|
try {
|
|
1008
1020
|
res = await fetchCortex(`${BASE}/api/records?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1009
1021
|
} catch (e) {
|
|
1010
|
-
return
|
|
1022
|
+
return toolError(`Could not list records: ${e.message}`)
|
|
1011
1023
|
}
|
|
1012
1024
|
if (!res.ok) {
|
|
1013
1025
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1014
|
-
return
|
|
1026
|
+
return toolError(`Could not list records: ${d.message}`)
|
|
1015
1027
|
}
|
|
1016
1028
|
const { text } = await res.json()
|
|
1017
1029
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1039,11 +1051,11 @@ export async function runServer(version) {
|
|
|
1039
1051
|
try {
|
|
1040
1052
|
res = await fetchCortex(`${BASE}/api/records/daily?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1041
1053
|
} catch (e) {
|
|
1042
|
-
return
|
|
1054
|
+
return toolError(`Could not build daily log: ${e.message}`)
|
|
1043
1055
|
}
|
|
1044
1056
|
if (!res.ok) {
|
|
1045
1057
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1046
|
-
return
|
|
1058
|
+
return toolError(`Could not build daily log: ${d.message}`)
|
|
1047
1059
|
}
|
|
1048
1060
|
const { text } = await res.json()
|
|
1049
1061
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1064,11 +1076,11 @@ export async function runServer(version) {
|
|
|
1064
1076
|
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
1065
1077
|
})
|
|
1066
1078
|
} catch (e) {
|
|
1067
|
-
return
|
|
1079
|
+
return toolError(`Could not list your sessions: ${e.message}`)
|
|
1068
1080
|
}
|
|
1069
1081
|
if (!res.ok) {
|
|
1070
1082
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1071
|
-
return
|
|
1083
|
+
return toolError(`Could not list your sessions: ${d.message}`)
|
|
1072
1084
|
}
|
|
1073
1085
|
const { text } = await res.json()
|
|
1074
1086
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1094,11 +1106,11 @@ export async function runServer(version) {
|
|
|
1094
1106
|
body: JSON.stringify({ privacy }),
|
|
1095
1107
|
})
|
|
1096
1108
|
} catch (e) {
|
|
1097
|
-
return
|
|
1109
|
+
return toolError(`Could not set privacy: ${e.message}`)
|
|
1098
1110
|
}
|
|
1099
1111
|
if (!res.ok) {
|
|
1100
1112
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1101
|
-
return
|
|
1113
|
+
return toolError(`Could not set privacy: ${d.message}`)
|
|
1102
1114
|
}
|
|
1103
1115
|
const out = await res.json()
|
|
1104
1116
|
return { content: [{ type: 'text', text: `Done — record ${out.id} is now "${out.privacy}".` }] }
|
|
@@ -1127,11 +1139,11 @@ export async function runServer(version) {
|
|
|
1127
1139
|
body: JSON.stringify({ kind, name, validity, modality, superseded_by }),
|
|
1128
1140
|
})
|
|
1129
1141
|
} catch (e) {
|
|
1130
|
-
return
|
|
1142
|
+
return toolError(`Could not set validity: ${e.message}`)
|
|
1131
1143
|
}
|
|
1132
1144
|
if (!res.ok) {
|
|
1133
1145
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1134
|
-
return
|
|
1146
|
+
return toolError(`Could not set validity: ${d.message}`)
|
|
1135
1147
|
}
|
|
1136
1148
|
const out = await res.json()
|
|
1137
1149
|
return { content: [{ type: 'text', text: `Done — "${out.name}" is now ${out.validity}${out.modality ? ` / ${out.modality}` : ''} (${out.docs_updated} tier-doc(s) updated).` }] }
|
|
@@ -1158,10 +1170,10 @@ export async function runServer(version) {
|
|
|
1158
1170
|
body: JSON.stringify({ name, target_name, ...(target_kind ? { target_kind } : {}) }),
|
|
1159
1171
|
})
|
|
1160
1172
|
} catch (e) {
|
|
1161
|
-
return
|
|
1173
|
+
return toolError(`Could not alias: ${e.message}`)
|
|
1162
1174
|
}
|
|
1163
1175
|
const out = await res.json().catch(() => null)
|
|
1164
|
-
if (!res.ok) return
|
|
1176
|
+
if (!res.ok) return toolError(`Could not alias "${name}": ${out?.error ?? res.status}`)
|
|
1165
1177
|
if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
|
|
1166
1178
|
return { content: [{ type: 'text', text: `Done — [[${out.alias}]] now resolves to "${out.target}" (${out.target_kind}). It's out of the wanted-page backlog.` }] }
|
|
1167
1179
|
},
|
|
@@ -1186,10 +1198,10 @@ export async function runServer(version) {
|
|
|
1186
1198
|
body: JSON.stringify({ name, ...(days ? { days } : {}) }),
|
|
1187
1199
|
})
|
|
1188
1200
|
} catch (e) {
|
|
1189
|
-
return
|
|
1201
|
+
return toolError(`Could not snooze: ${e.message}`)
|
|
1190
1202
|
}
|
|
1191
1203
|
const out = await res.json().catch(() => null)
|
|
1192
|
-
if (!res.ok) return
|
|
1204
|
+
if (!res.ok) return toolError(`Could not snooze "${name}": ${out?.error ?? res.status}`)
|
|
1193
1205
|
if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
|
|
1194
1206
|
return { content: [{ type: 'text', text: `Snoozed "${out.name}" for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
|
|
1195
1207
|
},
|
|
@@ -1218,7 +1230,7 @@ export async function runServer(version) {
|
|
|
1218
1230
|
body: JSON.stringify({ kind, name, tier, ...(source_tier ? { source_tier } : {}), ...(absorb ? { absorb: true } : {}), ...(target_version ? { target_version } : {}) }),
|
|
1219
1231
|
})
|
|
1220
1232
|
} catch (e) {
|
|
1221
|
-
return
|
|
1233
|
+
return toolError(`Could not set page privacy: ${e.message}`)
|
|
1222
1234
|
}
|
|
1223
1235
|
const out = await res.json().catch(() => null)
|
|
1224
1236
|
if (!res.ok) {
|
|
@@ -1228,10 +1240,10 @@ export async function runServer(version) {
|
|
|
1228
1240
|
const extra = out.collision === 'readable' && out.blocking
|
|
1229
1241
|
? `\nYour ${out.source?.tier} page: ${out.source?.summary ?? out.source?.title}\nExisting ${out.blocking.tier} page (current version: ${out.blocking.version ?? 'none — this page predates content-hash tracking and cannot be re-authored via base_version; ask an admin about a backfill'}): ${out.blocking.summary ?? out.blocking.title}`
|
|
1230
1242
|
: ''
|
|
1231
|
-
return
|
|
1243
|
+
return toolError(`Could not set page privacy: ${out.error}${extra}`)
|
|
1232
1244
|
}
|
|
1233
1245
|
const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
|
|
1234
|
-
return
|
|
1246
|
+
return toolError(`Could not set page privacy: ${d.message}`)
|
|
1235
1247
|
}
|
|
1236
1248
|
const g = out.live_grants?.length
|
|
1237
1249
|
? ` Grants still active for: ${out.live_grants.map((x) => x.grantee_name ?? x.grantee_user_id).join(', ')}.`
|
|
@@ -1262,10 +1274,10 @@ export async function runServer(version) {
|
|
|
1262
1274
|
body: JSON.stringify({ kind, name, grantee, action, ...(tier ? { tier } : {}) }),
|
|
1263
1275
|
})
|
|
1264
1276
|
} catch (e) {
|
|
1265
|
-
return
|
|
1277
|
+
return toolError(`Could not ${action}: ${e.message}`)
|
|
1266
1278
|
}
|
|
1267
1279
|
const out = await res.json().catch(() => null)
|
|
1268
|
-
if (!res.ok) return
|
|
1280
|
+
if (!res.ok) return toolError(`Could not ${action}: ${out?.error ?? res.status}`)
|
|
1269
1281
|
const verb = { granted: 'now has access to', already_granted: 'already had access to', revoked: 'no longer has access to', not_granted: 'had no grant on' }[out.action]
|
|
1270
1282
|
return { content: [{ type: 'text', text: `Done — ${out.grantee} ${verb} the ${out.tier} page.` }] }
|
|
1271
1283
|
},
|
|
@@ -1288,10 +1300,10 @@ export async function runServer(version) {
|
|
|
1288
1300
|
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
1289
1301
|
})
|
|
1290
1302
|
} catch (e) {
|
|
1291
|
-
return
|
|
1303
|
+
return toolError(`Could not list grants: ${e.message}`)
|
|
1292
1304
|
}
|
|
1293
1305
|
const out = await res.json().catch(() => null)
|
|
1294
|
-
if (!res.ok) return
|
|
1306
|
+
if (!res.ok) return toolError(`Could not list grants: ${out?.error ?? res.status}`)
|
|
1295
1307
|
if (!out.grants?.length) return { content: [{ type: 'text', text: 'No grants on this page.' }] }
|
|
1296
1308
|
const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} variant (since ${String(g.created_at).slice(0, 10)})`)
|
|
1297
1309
|
return { content: [{ type: 'text', text: `Grants (${out.grants.length}):\n${lines.join('\n')}` }] }
|
|
@@ -1310,11 +1322,11 @@ export async function runServer(version) {
|
|
|
1310
1322
|
try {
|
|
1311
1323
|
res = await fetchCortex(`${BASE}/api/brain/retier-notices`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1312
1324
|
} catch (e) {
|
|
1313
|
-
return
|
|
1325
|
+
return toolError(`Could not list notices: ${e.message}`)
|
|
1314
1326
|
}
|
|
1315
1327
|
if (!res.ok) {
|
|
1316
1328
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1317
|
-
return
|
|
1329
|
+
return toolError(`Could not list notices: ${d.message}`)
|
|
1318
1330
|
}
|
|
1319
1331
|
const { notices } = await res.json()
|
|
1320
1332
|
if (!notices?.length) return { content: [{ type: 'text', text: 'No re-tier notices.' }] }
|
|
@@ -1336,11 +1348,11 @@ export async function runServer(version) {
|
|
|
1336
1348
|
try {
|
|
1337
1349
|
res = await fetchCortex(`${BASE}/api/brain/page-merge-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1338
1350
|
} catch (e) {
|
|
1339
|
-
return
|
|
1351
|
+
return toolError(`Could not list merge requests: ${e.message}`)
|
|
1340
1352
|
}
|
|
1341
1353
|
if (!res.ok) {
|
|
1342
1354
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1343
|
-
return
|
|
1355
|
+
return toolError(`Could not list merge requests: ${d.message}`)
|
|
1344
1356
|
}
|
|
1345
1357
|
const { requests } = await res.json()
|
|
1346
1358
|
if (!requests?.length) return { content: [{ type: 'text', text: 'No pending page merge requests.' }] }
|
|
@@ -1369,17 +1381,17 @@ export async function runServer(version) {
|
|
|
1369
1381
|
body: JSON.stringify({ id, decision }),
|
|
1370
1382
|
})
|
|
1371
1383
|
} catch (e) {
|
|
1372
|
-
return
|
|
1384
|
+
return toolError(`Could not decide: ${e.message}`)
|
|
1373
1385
|
}
|
|
1374
1386
|
const out = await res.json().catch(() => null)
|
|
1375
|
-
if (!res.ok) return
|
|
1387
|
+
if (!res.ok) return toolError(`Could not decide: ${out?.error ?? res.status}`)
|
|
1376
1388
|
return { content: [{ type: 'text', text: out.decision === 'denied' ? 'Denied — nothing moved.' : `Approved — removed variant(s): ${out.removed_variants.join(', ') || 'none remained'}. ${out.note}` }] }
|
|
1377
1389
|
},
|
|
1378
1390
|
)
|
|
1379
1391
|
|
|
1380
1392
|
// ── File requests: ask the owner for a record's FULL original (lives on their machine) ──
|
|
1381
1393
|
const fail = (verb, res) => async () =>
|
|
1382
|
-
(
|
|
1394
|
+
toolError(`Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}`)
|
|
1383
1395
|
|
|
1384
1396
|
server.registerTool(
|
|
1385
1397
|
'request_file',
|
|
@@ -1395,7 +1407,7 @@ export async function runServer(version) {
|
|
|
1395
1407
|
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1396
1408
|
body: JSON.stringify({ recordId: record_id }),
|
|
1397
1409
|
})
|
|
1398
|
-
} catch (e) { return
|
|
1410
|
+
} catch (e) { return toolError(`Could not request: ${e.message}`) }
|
|
1399
1411
|
if (res.status === 409) {
|
|
1400
1412
|
const j = await res.json().catch(() => ({}))
|
|
1401
1413
|
if (j.error === 'you_own_it') return { content: [{ type: 'text', text: `You own this record — open the original directly:\n${j.uri}` }] }
|
|
@@ -1417,7 +1429,7 @@ export async function runServer(version) {
|
|
|
1417
1429
|
async () => {
|
|
1418
1430
|
let res
|
|
1419
1431
|
try { res = await fetchCortex(`${BASE}/api/file-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
1420
|
-
catch (e) { return
|
|
1432
|
+
catch (e) { return toolError(`Could not load file requests: ${e.message}`) }
|
|
1421
1433
|
if (!res.ok) return (await fail('load file requests', res))()
|
|
1422
1434
|
const { mine, inbox } = await res.json()
|
|
1423
1435
|
const parts = []
|
|
@@ -1441,7 +1453,7 @@ export async function runServer(version) {
|
|
|
1441
1453
|
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1442
1454
|
body: JSON.stringify({ action: decision }),
|
|
1443
1455
|
})
|
|
1444
|
-
} catch (e) { return
|
|
1456
|
+
} catch (e) { return toolError(`Could not decide: ${e.message}`) }
|
|
1445
1457
|
if (!res.ok) return (await fail('decide', res))()
|
|
1446
1458
|
return { content: [{ type: 'text', text: `Request ${decision === 'approve' ? 'approved' : 'denied'}.` }] }
|
|
1447
1459
|
},
|
|
@@ -1457,7 +1469,7 @@ export async function runServer(version) {
|
|
|
1457
1469
|
async ({ request_id }) => {
|
|
1458
1470
|
let res
|
|
1459
1471
|
try { res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/download`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
1460
|
-
catch (e) { return
|
|
1472
|
+
catch (e) { return toolError(`Could not download: ${e.message}`) }
|
|
1461
1473
|
if (res.status === 409) return { content: [{ type: 'text', text: 'Not ready — the owner hasn\'t fulfilled this yet. Try again after they approve.' }] }
|
|
1462
1474
|
if (res.status === 410) return { content: [{ type: 'text', text: 'This file has expired (downloads are available for 7 days). Request it again.' }] }
|
|
1463
1475
|
if (!res.ok) return (await fail('download', res))()
|
|
@@ -1516,11 +1528,11 @@ export async function runServer(version) {
|
|
|
1516
1528
|
try {
|
|
1517
1529
|
res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1518
1530
|
} catch (e) {
|
|
1519
|
-
return
|
|
1531
|
+
return toolError(`Could not fetch authoring context: ${e.message}`)
|
|
1520
1532
|
}
|
|
1521
1533
|
if (!res.ok) {
|
|
1522
1534
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1523
|
-
return
|
|
1535
|
+
return toolError(`Could not fetch authoring context: ${d.message}`)
|
|
1524
1536
|
}
|
|
1525
1537
|
const { connectionRules, namespace, retiredLinks } = await res.json()
|
|
1526
1538
|
const ns = Array.isArray(namespace) ? namespace : []
|
|
@@ -1571,11 +1583,11 @@ export async function runServer(version) {
|
|
|
1571
1583
|
body: JSON.stringify({ kind, name, pages, reason, change_kind }),
|
|
1572
1584
|
})
|
|
1573
1585
|
} catch (e) {
|
|
1574
|
-
return
|
|
1586
|
+
return toolError(`Could not author "${name}": ${e.message}`)
|
|
1575
1587
|
}
|
|
1576
1588
|
if (!res.ok) {
|
|
1577
1589
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1578
|
-
return
|
|
1590
|
+
return toolError(`Could not author "${name}": ${d.message}`)
|
|
1579
1591
|
}
|
|
1580
1592
|
const out = await res.json()
|
|
1581
1593
|
const blue = out?.links?.blue ?? 0
|
package/package.json
CHANGED
package/skills/log/SKILL.md
CHANGED
|
@@ -52,9 +52,8 @@ No arguments. Read the conversation context.
|
|
|
52
52
|
docs are pending, follow the `cortex-author-docs` skill (author each into its page, then
|
|
53
53
|
`docs-scan --mark`). Specs/plans written to disk this session must not die on disk — a spec IS
|
|
54
54
|
a page. If no roots are registered or nothing is pending, skip silently.
|
|
55
|
-
7. **Reconcile
|
|
56
|
-
|
|
57
|
-
write slips through. Before printing the Output:
|
|
55
|
+
7. **Reconcile the sweep (don't trust it).** Step 5 relies on your in-the-moment judgment of "what
|
|
56
|
+
advanced"; this step closes the loop so nothing is silently missed. Before printing the Output:
|
|
58
57
|
a. **Enumerate what you touched** — from the transcript, list the concrete entities this session
|
|
59
58
|
advanced: the project(s), notable files/artifacts, and the people you coordinated with. Derive
|
|
60
59
|
this checklist from what actually *happened*, not from what you remember authoring — the whole
|
|
@@ -62,18 +61,21 @@ No arguments. Read the conversation context.
|
|
|
62
61
|
b. **Assert one outcome per entity** — every item gets exactly `authored [[Page]]` **or**
|
|
63
62
|
`skipped — <reason>` (e.g. "no material change", "not a node", "already current"). Nothing may be
|
|
64
63
|
left unaccounted for. If an entity that genuinely advanced has neither, `author` it now (Step 5).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
Carry the tally into the Output.
|
|
65
|
+
|
|
66
|
+
> **A read-back verification sub-step lived here and was REMOVED 2026-07-31. Do not re-add it
|
|
67
|
+
> without new evidence.** It asked you to `read_page` every page you had just authored, to confirm
|
|
68
|
+
> the write landed. Measured over 397 local transcripts — 18 sessions that wrote a page,
|
|
69
|
+
> 2026-07-29→31 — **18 `author` calls were rejected and all 18 were retried to success: zero
|
|
70
|
+
> silently lost.** An end-of-session pass would have caught nothing, because the rejection reason
|
|
71
|
+
> arrives *in the tool result at the moment of the call*. Session end is the weakest place to
|
|
72
|
+
> verify a write; the result you already have in hand is the strongest.
|
|
73
|
+
>
|
|
74
|
+
> **What replaces it — at the moment of each write, not at the end:** a rejected `author` comes back
|
|
75
|
+
> as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Cortex API 409:
|
|
76
|
+
> <reason>` — and `No change to "<page>"` is a **200 OK that wrote nothing**. Neither is an error at
|
|
77
|
+
> the protocol level, so nothing will interrupt you. **Read the result text of every write; never
|
|
78
|
+
> skim it.** That inline read is where this step's value actually was.
|
|
77
79
|
|
|
78
80
|
## Output
|
|
79
81
|
|
|
@@ -88,7 +90,7 @@ After calling `log_session`, show a short structured summary:
|
|
|
88
90
|
**Coordinated with:** people involved
|
|
89
91
|
**Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
|
|
90
92
|
**Wiki authored:** [[Node A]], [[Node B]] — pages updated (or "— nothing advanced this session")
|
|
91
|
-
**Reconciled:** N touched → M authored, K skipped (reason each)
|
|
93
|
+
**Reconciled:** N touched → M authored, K skipped (reason each)
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
## Safety rules
|