prism-mcp-server 20.2.4 → 20.2.6

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.
Files changed (2) hide show
  1. package/dist/connect.js +124 -111
  2. package/package.json +9 -4
package/dist/connect.js CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as win32Path } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -69,6 +69,72 @@ const CODEX_LOCAL_FIRST_POLICY = {
69
69
  };
70
70
  const CODEX_POLICY_MARKER_PREFIX = "# >>> prism connect managed: local-first";
71
71
  const CODEX_POLICY_MARKER_SUFFIX = "# <<< prism connect managed: local-first";
72
+ /**
73
+ * Read a host-owned text file through one descriptor, then verify that the
74
+ * pathname still identifies that same regular file. This prevents a path from
75
+ * being swapped between a metadata check and the read while retaining Prism's
76
+ * documented support for symlinked dotfiles.
77
+ */
78
+ function readRegularTextSnapshot(filePath, allowSymlink = true) {
79
+ const nonBlocking = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
80
+ const noFollow = !allowSymlink && typeof constants.O_NOFOLLOW === "number"
81
+ ? constants.O_NOFOLLOW
82
+ : 0;
83
+ let descriptor;
84
+ try {
85
+ descriptor = openSync(filePath, constants.O_RDONLY | nonBlocking | noFollow);
86
+ }
87
+ catch (error) {
88
+ // A dangling symlink is an existing, unsafe entry rather than an absent
89
+ // config. Detect it only after the descriptor open failed, so this probe
90
+ // cannot become a check-then-read race.
91
+ if (isErrno(error, "ENOENT")) {
92
+ let pathInfo;
93
+ try {
94
+ pathInfo = lstatSync(filePath);
95
+ }
96
+ catch (probeError) {
97
+ if (!isErrno(probeError, "ENOENT"))
98
+ throw probeError;
99
+ }
100
+ if (pathInfo?.isSymbolicLink()) {
101
+ throw new Error(`symlink target is unavailable: ${filePath}`);
102
+ }
103
+ }
104
+ throw error;
105
+ }
106
+ try {
107
+ const openedInfo = fstatSync(descriptor);
108
+ if (!openedInfo.isFile())
109
+ throw new Error("target is not a regular file");
110
+ const pathInfo = lstatSync(filePath);
111
+ let writePath = filePath;
112
+ let symlinkPath;
113
+ if (pathInfo.isSymbolicLink()) {
114
+ if (!allowSymlink)
115
+ throw new Error("symlink is not allowed");
116
+ writePath = realpathSync(filePath);
117
+ symlinkPath = filePath;
118
+ }
119
+ else if (!pathInfo.isFile()) {
120
+ throw new Error("target is not a regular file");
121
+ }
122
+ const currentInfo = statSync(writePath);
123
+ if (!currentInfo.isFile()
124
+ || currentInfo.dev !== openedInfo.dev
125
+ || currentInfo.ino !== openedInfo.ino) {
126
+ throw new Error("file changed while Prism was opening it; retry");
127
+ }
128
+ return {
129
+ text: readFileSync(descriptor, "utf8"),
130
+ writePath,
131
+ symlinkPath,
132
+ };
133
+ }
134
+ finally {
135
+ closeSync(descriptor);
136
+ }
137
+ }
72
138
  export function normalizeHostName(value) {
73
139
  const normalized = value.trim().toLowerCase();
74
140
  const host = HOST_ALIASES[normalized];
@@ -188,11 +254,7 @@ export function writeInstallationReceipt(homeDir, receipt, beforeCommit) {
188
254
  }
189
255
  let originalText;
190
256
  try {
191
- const receiptInfo = lstatSync(receiptPath);
192
- if (receiptInfo.isSymbolicLink() || !receiptInfo.isFile()) {
193
- throw new Error("receipt must be a regular Prism-owned file");
194
- }
195
- originalText = readFileSync(receiptPath, "utf8");
257
+ originalText = readRegularTextSnapshot(receiptPath, false).text;
196
258
  const current = JSON.parse(originalText);
197
259
  if (!isJsonObject(current)
198
260
  || current.schema_version !== INSTALLATION_RECEIPT_SCHEMA_VERSION
@@ -251,14 +313,10 @@ export function migrateLegacyClaudeProjectMcp(homeDir = homedir(), cwd = process
251
313
  let symlinkPath;
252
314
  let originalText;
253
315
  try {
254
- const pathInfo = lstatSync(configPath);
255
- if (pathInfo.isSymbolicLink()) {
256
- writePath = realpathSync(configPath);
257
- symlinkPath = configPath;
258
- }
259
- if (!statSync(writePath).isFile())
260
- throw new Error("config is not a regular file");
261
- originalText = readFileSync(writePath, "utf8");
316
+ const snapshot = readRegularTextSnapshot(configPath);
317
+ writePath = snapshot.writePath;
318
+ symlinkPath = snapshot.symlinkPath;
319
+ originalText = snapshot.text;
262
320
  }
263
321
  catch (error) {
264
322
  throw new Error(`Could not inspect Claude project MCP config: ${error instanceof Error ? error.message : String(error)}`);
@@ -321,14 +379,10 @@ export function migrateLegacyClaudeHooks(homeDir = homedir(), dryRun = false, be
321
379
  let symlinkPath;
322
380
  let originalText;
323
381
  try {
324
- const pathInfo = lstatSync(configPath);
325
- if (pathInfo.isSymbolicLink()) {
326
- writePath = realpathSync(configPath);
327
- symlinkPath = configPath;
328
- if (!statSync(writePath).isFile())
329
- throw new Error("target is not a regular file");
330
- }
331
- originalText = readFileSync(writePath, "utf8");
382
+ const snapshot = readRegularTextSnapshot(configPath);
383
+ writePath = snapshot.writePath;
384
+ symlinkPath = snapshot.symlinkPath;
385
+ originalText = snapshot.text;
332
386
  }
333
387
  catch (error) {
334
388
  if (isErrno(error, "ENOENT"))
@@ -403,15 +457,11 @@ export function migrateLegacyClaudeInstructions(homeDir = homedir(), dryRun = fa
403
457
  let originalText;
404
458
  let instructionEntryExists = false;
405
459
  try {
406
- const pathInfo = lstatSync(instructionPath);
460
+ const snapshot = readRegularTextSnapshot(instructionPath);
407
461
  instructionEntryExists = true;
408
- if (pathInfo.isSymbolicLink()) {
409
- writePath = realpathSync(instructionPath);
410
- symlinkPath = instructionPath;
411
- if (!statSync(writePath).isFile())
412
- throw new Error("target is not a regular file");
413
- }
414
- originalText = readFileSync(writePath, "utf8");
462
+ writePath = snapshot.writePath;
463
+ symlinkPath = snapshot.symlinkPath;
464
+ originalText = snapshot.text;
415
465
  }
416
466
  catch (error) {
417
467
  if (!instructionEntryExists && isErrno(error, "ENOENT")) {
@@ -447,15 +497,11 @@ export function migrateLegacyClaudeManagedStartup(homeDir = homedir(), dryRun =
447
497
  let originalText;
448
498
  let instructionEntryExists = false;
449
499
  try {
450
- const pathInfo = lstatSync(instructionPath);
500
+ const snapshot = readRegularTextSnapshot(instructionPath);
451
501
  instructionEntryExists = true;
452
- if (pathInfo.isSymbolicLink()) {
453
- writePath = realpathSync(instructionPath);
454
- symlinkPath = instructionPath;
455
- if (!statSync(writePath).isFile())
456
- throw new Error("target is not a regular file");
457
- }
458
- originalText = readFileSync(writePath, "utf8");
502
+ writePath = snapshot.writePath;
503
+ symlinkPath = snapshot.symlinkPath;
504
+ originalText = snapshot.text;
459
505
  }
460
506
  catch (error) {
461
507
  if (!instructionEntryExists && isErrno(error, "ENOENT")) {
@@ -508,15 +554,11 @@ export function configureClaudeNativeStartup(homeDir = homedir(), dryRun = false
508
554
  let originalText;
509
555
  let instructionEntryExists = false;
510
556
  try {
511
- const pathInfo = lstatSync(instructionPath);
557
+ const snapshot = readRegularTextSnapshot(instructionPath);
512
558
  instructionEntryExists = true;
513
- if (pathInfo.isSymbolicLink()) {
514
- writePath = realpathSync(instructionPath);
515
- symlinkPath = instructionPath;
516
- if (!statSync(writePath).isFile())
517
- throw new Error("target is not a regular file");
518
- }
519
- originalText = readFileSync(writePath, "utf8");
559
+ writePath = snapshot.writePath;
560
+ symlinkPath = snapshot.symlinkPath;
561
+ originalText = snapshot.text;
520
562
  }
521
563
  catch (error) {
522
564
  if (instructionEntryExists || !isErrno(error, "ENOENT")) {
@@ -604,15 +646,11 @@ export function configureGeminiNativeStartup(homeDir = homedir(), dryRun = false
604
646
  let originalText;
605
647
  let instructionEntryExists = false;
606
648
  try {
607
- const pathInfo = lstatSync(instructionPath);
649
+ const snapshot = readRegularTextSnapshot(instructionPath);
608
650
  instructionEntryExists = true;
609
- if (pathInfo.isSymbolicLink()) {
610
- writePath = realpathSync(instructionPath);
611
- symlinkPath = instructionPath;
612
- if (!statSync(writePath).isFile())
613
- throw new Error("target is not a regular file");
614
- }
615
- originalText = readFileSync(writePath, "utf8");
651
+ writePath = snapshot.writePath;
652
+ symlinkPath = snapshot.symlinkPath;
653
+ originalText = snapshot.text;
616
654
  }
617
655
  catch (error) {
618
656
  if (instructionEntryExists || !isErrno(error, "ENOENT")) {
@@ -680,15 +718,11 @@ export function configureCodexNativeStartup(homeDir, dryRun = false, beforeCommi
680
718
  let originalText;
681
719
  let instructionEntryExists = false;
682
720
  try {
683
- const pathInfo = lstatSync(instructionPath);
721
+ const snapshot = readRegularTextSnapshot(instructionPath);
684
722
  instructionEntryExists = true;
685
- if (pathInfo.isSymbolicLink()) {
686
- writePath = realpathSync(instructionPath);
687
- symlinkPath = instructionPath;
688
- if (!statSync(writePath).isFile())
689
- throw new Error("target is not a regular file");
690
- }
691
- originalText = readFileSync(writePath, "utf8");
723
+ writePath = snapshot.writePath;
724
+ symlinkPath = snapshot.symlinkPath;
725
+ originalText = snapshot.text;
692
726
  }
693
727
  catch (error) {
694
728
  if (instructionEntryExists || !isErrno(error, "ENOENT")) {
@@ -737,14 +771,10 @@ function configureJsonAgentPolicy(configPath, label, mutate, dryRun, beforeCommi
737
771
  let originalText;
738
772
  let config = {};
739
773
  try {
740
- const pathInfo = lstatSync(configPath);
741
- if (pathInfo.isSymbolicLink()) {
742
- writePath = realpathSync(configPath);
743
- symlinkPath = configPath;
744
- if (!statSync(writePath).isFile())
745
- throw new Error("target is not a regular file");
746
- }
747
- originalText = readFileSync(writePath, "utf8");
774
+ const snapshot = readRegularTextSnapshot(configPath);
775
+ writePath = snapshot.writePath;
776
+ symlinkPath = snapshot.symlinkPath;
777
+ originalText = snapshot.text;
748
778
  const parsed = JSON.parse(originalText);
749
779
  if (!isJsonObject(parsed))
750
780
  throw new Error("top-level JSON value must be an object");
@@ -858,14 +888,10 @@ export function configureCodexAgentPolicy(homeDir, dryRun = false, beforeCommit,
858
888
  let symlinkPath;
859
889
  let originalText;
860
890
  try {
861
- const pathInfo = lstatSync(configPath);
862
- if (pathInfo.isSymbolicLink()) {
863
- writePath = realpathSync(configPath);
864
- symlinkPath = configPath;
865
- if (!statSync(writePath).isFile())
866
- throw new Error("target is not a regular file");
867
- }
868
- originalText = readFileSync(writePath, "utf8");
891
+ const snapshot = readRegularTextSnapshot(configPath);
892
+ writePath = snapshot.writePath;
893
+ symlinkPath = snapshot.symlinkPath;
894
+ originalText = snapshot.text;
869
895
  parseToml(originalText);
870
896
  }
871
897
  catch (error) {
@@ -1063,18 +1089,18 @@ function registerJsonHost(definition, entry, dryRun, refresh, beforeCommit) {
1063
1089
  let writePath = configPath;
1064
1090
  let symlinkPath;
1065
1091
  try {
1066
- const pathInfo = lstatSync(configPath);
1067
- if (pathInfo.isSymbolicLink()) {
1068
- try {
1069
- writePath = realpathSync(configPath);
1070
- symlinkPath = configPath;
1071
- }
1072
- catch (error) {
1073
- return result(definition, "error", `config symlink target is unavailable: ${error instanceof Error ? error.message : String(error)}`);
1074
- }
1092
+ const snapshot = readRegularTextSnapshot(configPath);
1093
+ writePath = snapshot.writePath;
1094
+ symlinkPath = snapshot.symlinkPath;
1095
+ originalText = snapshot.text;
1096
+ }
1097
+ catch (error) {
1098
+ if (!isErrno(error, "ENOENT")) {
1099
+ return result(definition, "error", `could not inspect config: ${error instanceof Error ? error.message : String(error)}`);
1075
1100
  }
1101
+ }
1102
+ if (originalText !== undefined) {
1076
1103
  try {
1077
- originalText = readFileSync(writePath, "utf8");
1078
1104
  const parsed = JSON.parse(originalText);
1079
1105
  if (!isJsonObject(parsed)) {
1080
1106
  throw new Error("top-level JSON value must be an object");
@@ -1085,11 +1111,6 @@ function registerJsonHost(definition, entry, dryRun, refresh, beforeCommit) {
1085
1111
  return result(definition, "error", `could not parse config: ${error instanceof Error ? error.message : String(error)}`);
1086
1112
  }
1087
1113
  }
1088
- catch (error) {
1089
- if (!isErrno(error, "ENOENT")) {
1090
- return result(definition, "error", `could not inspect config: ${error instanceof Error ? error.message : String(error)}`);
1091
- }
1092
- }
1093
1114
  const currentServers = config.mcpServers;
1094
1115
  if (currentServers !== undefined && !isJsonObject(currentServers)) {
1095
1116
  return result(definition, "error", '"mcpServers" must be a JSON object');
@@ -1145,21 +1166,18 @@ function registerCodexTomlHost(definition, entry, dryRun, refresh, beforeCommit)
1145
1166
  let writePath = configPath;
1146
1167
  let symlinkPath;
1147
1168
  try {
1148
- const pathInfo = lstatSync(configPath);
1149
- if (pathInfo.isSymbolicLink()) {
1150
- try {
1151
- writePath = realpathSync(configPath);
1152
- symlinkPath = configPath;
1153
- if (!statSync(writePath).isFile()) {
1154
- throw new Error("config symlink target is not a regular file");
1155
- }
1156
- }
1157
- catch (error) {
1158
- return result(definition, "error", `config symlink target is unavailable: ${error instanceof Error ? error.message : String(error)}`);
1159
- }
1169
+ const snapshot = readRegularTextSnapshot(configPath);
1170
+ writePath = snapshot.writePath;
1171
+ symlinkPath = snapshot.symlinkPath;
1172
+ originalText = snapshot.text;
1173
+ }
1174
+ catch (error) {
1175
+ if (!isErrno(error, "ENOENT")) {
1176
+ return result(definition, "error", `could not inspect config: ${error instanceof Error ? error.message : String(error)}`);
1160
1177
  }
1178
+ }
1179
+ if (originalText !== undefined) {
1161
1180
  try {
1162
- originalText = readFileSync(writePath, "utf8");
1163
1181
  const parsed = parseToml(originalText);
1164
1182
  if (!isJsonObject(parsed)) {
1165
1183
  throw new Error("top-level TOML value must be a table");
@@ -1170,11 +1188,6 @@ function registerCodexTomlHost(definition, entry, dryRun, refresh, beforeCommit)
1170
1188
  return result(definition, "error", `could not parse config: ${error instanceof Error ? error.message : String(error)}`);
1171
1189
  }
1172
1190
  }
1173
- catch (error) {
1174
- if (!isErrno(error, "ENOENT")) {
1175
- return result(definition, "error", `could not inspect config: ${error instanceof Error ? error.message : String(error)}`);
1176
- }
1177
- }
1178
1191
  let managedBlock;
1179
1192
  try {
1180
1193
  managedBlock = locateCodexManagedBlock(originalText ?? "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.2.4",
3
+ "version": "20.2.6",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
6
6
  "module": "index.ts",
@@ -125,9 +125,14 @@
125
125
  }
126
126
  },
127
127
  "overrides": {
128
- "hono": "^4.12.18",
129
- "fast-uri": "^3.1.2",
130
- "ip-address": "^10.2.0"
128
+ "@hono/node-server": "^2.0.5",
129
+ "body-parser": "^2.3.0",
130
+ "fast-uri": "^3.1.4",
131
+ "hono": "^4.12.27",
132
+ "ip-address": "^10.2.0",
133
+ "protobufjs": "^7.6.5",
134
+ "sharp": "^0.35.0",
135
+ "tar": "^7.5.19"
131
136
  },
132
137
  "dependencies": {
133
138
  "@anthropic-ai/sdk": "^0.92.0",