Skip to content

Commit 767b148

Browse files
Bruce Haumanclaude
andcommitted
Standardize file I/O to use UTF-8 encoding across all platforms
Replace all slurp/spit calls with UTF-8 utility helpers to ensure consistent encoding, especially on Windows where JVM defaults to Windows-1252 instead of UTF-8. Changes: - Add clojure-mcp.utils.file namespace with slurp-utf8 and spit-utf8 - Convert all inline slurp/spit :encoding "UTF-8" to use new helpers - Update 15 source files with file-utils namespace imports - Add comprehensive tests for new utilities All 361 tests pass with 2576 assertions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 121cdf5 commit 767b148

File tree

17 files changed

+149
-47
lines changed

17 files changed

+149
-47
lines changed

src/clojure_mcp/agent/general_agent.clj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
[clojure-mcp.agent.langchain :as chain]
88
[clojure-mcp.config :as config]
99
[clojure-mcp.tools.project.core :as project-core]
10-
[clojure-mcp.tools :as tools])
10+
[clojure-mcp.tools :as tools]
11+
[clojure-mcp.utils.file :as file-utils])
1112
(:import
1213
[clojure_mcp.agent.langchain AiService]
1314
[dev.langchain4j.data.message UserMessage TextContent]))
@@ -49,7 +50,7 @@
4950
context-strings (cond-> []
5051
(.exists proj-summary-file)
5152
(conj (str "This is a project summary:\n"
52-
(slurp proj-summary-file)))
53+
(file-utils/slurp-utf8 proj-summary-file)))
5354

5455
(and (not error) outputs)
5556
(conj (str "This is the current project structure:\n"
@@ -58,7 +59,7 @@
5859
(.exists code-index-file)
5960
(conj (str "This is a code index of the code in this project.
6061
Please use it to inform you as to which files should be investigated.\n=======================\n"
61-
(slurp code-index-file))))]
62+
(file-utils/slurp-utf8 code-index-file))))]
6263
context-strings)
6364

6465
(sequential? context-config)
@@ -67,7 +68,7 @@ Please use it to inform you as to which files should be investigated.\n=========
6768
:when (.exists file)]
6869
(str "File: " file-path "\n"
6970
"=======================\n"
70-
(slurp file) "\n\n")))
71+
(file-utils/slurp-utf8 file) "\n\n")))
7172

7273
:else
7374
[]))

src/clojure_mcp/code_indexer.clj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
(:require
1313
[clojure-mcp.tools.unified-read-file.pattern-core :as pattern]
1414
[clojure-mcp.tools.glob-files.core :as glob]
15+
[clojure-mcp.utils.file :as file-utils]
1516
[clojure.java.io :as io]
1617
[clojure.string :as str]))
1718

@@ -83,7 +84,7 @@
8384
Returns a map with :files-indexed, :output-file, and :size-bytes."
8485
[source-dir output-file]
8586
(let [indexed-content (index-and-format source-dir)]
86-
(spit output-file indexed-content)
87+
(file-utils/spit-utf8 output-file indexed-content)
8788
{:files-indexed (count (find-clojure-files source-dir))
8889
:output-file output-file
8990
:size-bytes (count indexed-content)}))
@@ -114,7 +115,7 @@
114115
[source-dir output-file]
115116
(let [indexed-files (index-source-directory source-dir)
116117
indexed-content (format-collapsed-views indexed-files)]
117-
(spit output-file indexed-content)
118+
(file-utils/spit-utf8 output-file indexed-content)
118119
{:files-indexed (count indexed-files)
119120
:output-file output-file
120121
:size-bytes (count indexed-content)}))
@@ -177,7 +178,7 @@
177178
(str/join "\n\n" dir-sections))
178179
(format-collapsed-views all-indexed-files))
179180

180-
_ (spit out-file formatted-content)
181+
_ (file-utils/spit-utf8 out-file formatted-content)
181182
end-time (System/currentTimeMillis)]
182183

183184
{:files-indexed (count all-indexed-files)

src/clojure_mcp/config.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[clojure.string :as str]
55
[clojure-mcp.dialects :as dialects]
66
[clojure-mcp.config.schema :as schema]
7+
[clojure-mcp.utils.file :as file-utils]
78
[clojure.edn :as edn]
89
[clojure.tools.logging :as log]))
910

@@ -23,7 +24,7 @@
2324
(let [config-file (io/file config-file-path)]
2425
(if (.exists config-file)
2526
(try
26-
(edn/read-string (slurp config-file))
27+
(edn/read-string (file-utils/slurp-utf8 config-file))
2728
(catch Exception e
2829
(log/warn e "Failed to read config file:" (.getPath config-file))
2930
{}))

src/clojure_mcp/dialects.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
(:require [clojure.edn :as edn]
77
[clojure.java.io :as io]
88
[clojure.tools.logging :as log]
9-
[clojure-mcp.nrepl :as nrepl]))
9+
[clojure-mcp.nrepl :as nrepl]
10+
[clojure-mcp.utils.file :as file-utils]))
1011

1112
(defn handle-bash-over-nrepl? [nrepl-env-type]
1213
(boolean (#{:clj :bb} nrepl-env-type)))
@@ -65,7 +66,7 @@
6566
(defmethod load-repl-helpers-exp :clj
6667
[_]
6768
;; For Clojure, we load the helpers from resources
68-
[(slurp (io/resource "clojure-mcp/repl_helpers.clj"))
69+
[(file-utils/slurp-utf8 (io/resource "clojure-mcp/repl_helpers.clj"))
6970
"(in-ns 'user)"])
7071

7172
(defmethod load-repl-helpers-exp :default

src/clojure_mcp/prompt_cli.clj

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
[clojure-mcp.agent.general-agent :as general-agent]
1717
[clojure-mcp.agent.langchain.chat-listener :as listener]
1818
[clojure-mcp.agent.langchain.message-conv :as msg-conv]
19-
[clojure-mcp.tool-format :as tool-format])
19+
[clojure-mcp.tool-format :as tool-format]
20+
[clojure-mcp.utils.file :as file-utils])
2021
(:import [dev.langchain4j.data.message ChatMessageSerializer ChatMessageDeserializer]
2122
[java.time LocalDateTime]
2223
[java.time.format DateTimeFormatter])
@@ -49,9 +50,7 @@
4950
(when-not config-source
5051
(throw (ex-info "Agent configuration not found"
5152
{:config-path config-path})))
52-
(-> config-source
53-
slurp
54-
edn/read-string)))
53+
(-> config-source file-utils/slurp-utf8 edn/read-string)))
5554

5655
(defn load-system-message
5756
"Load system message from resource if it's a path, otherwise return as-is"
@@ -60,7 +59,7 @@
6059
(or (str/ends-with? system-message ".md")
6160
(str/ends-with? system-message ".txt")))
6261
(if-let [resource (io/resource system-message)]
63-
(slurp resource)
62+
(file-utils/slurp-utf8 resource)
6463
system-message)
6564
system-message))
6665

@@ -95,7 +94,7 @@
9594
:created created
9695
:messages messages-json}
9796
json-str (json/write-str session-data :escape-slash false)]
98-
(spit session-file json-str)
97+
(file-utils/spit-utf8 session-file json-str)
9998
session-file))
10099

101100
(defn find-latest-session-file
@@ -114,7 +113,7 @@
114113
"Load a session from a file.
115114
Returns {:model \"...\" :messages [...]} or throws if file doesn't exist or is invalid."
116115
[session-file]
117-
(let [json-str (slurp session-file)
116+
(let [json-str (file-utils/slurp-utf8 session-file)
118117
session-data (json/read-str json-str :key-fn keyword)
119118
messages-json (:messages session-data)
120119
messages (ChatMessageDeserializer/messagesFromJson messages-json)]

src/clojure_mcp/prompts.clj

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
[pogonos.core :as pg]
66
[clojure-mcp.config :as config]
77
[clojure-mcp.tools.scratch-pad.tool :as scratch-pad]
8-
[clojure-mcp.tools.scratch-pad.core :as scratch-pad-core]))
8+
[clojure-mcp.tools.scratch-pad.core :as scratch-pad-core]
9+
[clojure-mcp.utils.file :as file-utils]))
910

1011
(defn simple-content-prompt-fn
1112
"Returns a prompt-fn that ignores request arguments and returns
@@ -20,7 +21,7 @@
2021
"Loads prompt content from a classpath resource file."
2122
[filename]
2223
(if-let [resource (io/resource filename)]
23-
(slurp resource)
24+
(file-utils/slurp-utf8 resource)
2425
(str "Error: Prompt file not found on classpath: " filename)))
2526

2627
;; --- Prompt Definitions ---
@@ -210,7 +211,7 @@ After doing this provide a very brief (8 lines) summary of where we are and then
210211
(try
211212
;; Load the file
212213
(if (.exists file)
213-
(let [data (clojure.edn/read-string (slurp file))
214+
(let [data (clojure.edn/read-string (file-utils/slurp-utf8 file))
214215
;; Update the scratch pad atom
215216
_ (scratch-pad/update-scratch-pad! nrepl-client-atom (constantly data))
216217
;; Get shallow inspect of the data
@@ -262,7 +263,7 @@ After doing this provide a very brief (8 lines) summary of where we are and then
262263
(when-not (.exists dir)
263264
(.mkdirs dir)))
264265
;; Save the data
265-
(spit file (pr-str current-data))
266+
(file-utils/spit-utf8 file (pr-str current-data))
266267
;; Get shallow inspect for confirmation
267268
(let [inspect-result (:result (scratch-pad-core/execute-inspect current-data 1 nil))]
268269
(clj-result-k
@@ -323,7 +324,7 @@ and using the following format:
323324
file-path
324325
(.getCanonicalPath (io/file working-dir file-path)))]
325326
(when (.exists (io/file full-path))
326-
(slurp full-path)))
327+
(file-utils/slurp-utf8 full-path)))
327328
:else nil)]
328329
(when template-content
329330
{:name prompt-name

src/clojure_mcp/resources.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
[clojure-mcp.nrepl :as mcp-nrepl]
77
[clojure-mcp.config :as config]
88
[clojure-mcp.file-content :as file-content]
9-
[clojure-mcp.tools.project.core :as project])
9+
[clojure-mcp.tools.project.core :as project]
10+
[clojure-mcp.utils.file :as file-utils])
1011
(:import [io.modelcontextprotocol.spec McpSchema$Resource McpSchema$ReadResourceResult]))
1112

1213
(defn read-file [full-path]
1314
(let [file (io/file full-path)]
1415
(if (.exists file)
1516
(try
16-
(slurp file)
17+
(file-utils/slurp-utf8 file)
1718
(catch Exception e
1819
(throw (ex-info (str "reading file- " full-path
1920
"\nException- " (.getMessage e))

src/clojure_mcp/tools/agent_tool_builder/file_changes.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
(:require
44
[clojure.java.io :as io]
55
[clojure-mcp.utils.diff :as diff-utils]
6+
[clojure-mcp.utils.file :as file-utils]
67
[clojure.string :as str]
78
[clojure.tools.logging :as log]))
89

@@ -86,7 +87,7 @@
8687
(let [diffs (for [[canonical-path original-content] changed-files]
8788
(let [current-file (io/file canonical-path)]
8889
(if (.exists current-file)
89-
(let [current-content (slurp current-file)]
90+
(let [current-content (file-utils/slurp-utf8 current-file)]
9091
(format-file-diff canonical-path original-content current-content))
9192
;; File was deleted
9293
(format-file-diff canonical-path original-content ""))))]

src/clojure_mcp/tools/file_edit/core.clj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
This namespace contains the pure functionality without any MCP-specific code."
44
(:require
55
[clojure.java.io :as io]
6-
[clojure.string :as str]))
6+
[clojure.string :as str]
7+
[clojure-mcp.utils.file :as file-utils]))
78

89
(defn load-file-content
910
"Load file content from a path.
@@ -19,7 +20,7 @@
1920
(try
2021
(let [file (io/file path)]
2122
(if (.exists file)
22-
{:content (slurp file)
23+
{:content (file-utils/slurp-utf8 file)
2324
:error false}
2425
{:error true
2526
:message (str "File not found: " path)}))
@@ -130,7 +131,7 @@
130131
;; Create parent directories if they don't exist
131132
(when (and parent (not (.exists parent)))
132133
(.mkdirs parent))
133-
(spit file content)
134+
(file-utils/spit-utf8 file content)
134135
{:success true})
135136
(catch Exception e
136137
{:success false

src/clojure_mcp/tools/file_write/core.clj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
[clojure-mcp.utils.diff :as diff-utils]
88
[clojure-mcp.linting :as linting]
99
[clojure-mcp.utils.valid-paths :as valid-paths]
10-
[rewrite-clj.zip :as z]))
10+
[rewrite-clj.zip :as z]
11+
[clojure-mcp.utils.file :as file-utils]))
1112

1213
(defn is-clojure-file?
1314
"Check if a file is a Clojure-related file based on its extension or Babashka shebang.
@@ -32,7 +33,7 @@
3233
[nrepl-client-atom file-path content dry_run]
3334
(let [file (io/file file-path)
3435
file-exists? (.exists file)
35-
old-content (if file-exists? (slurp file) "")
36+
old-content (if file-exists? (file-utils/slurp-utf8 file) "")
3637

3738
;; Create a context map for the pipeline
3839
initial-ctx {::pipeline/nrepl-client-atom nrepl-client-atom
@@ -95,7 +96,7 @@
9596
(try
9697
(let [file (io/file file-path)
9798
file-exists? (.exists file)
98-
old-content (if file-exists? (slurp file) "")
99+
old-content (if file-exists? (file-utils/slurp-utf8 file) "")
99100
;; Only generate diff if the file already exists
100101
diff (if file-exists?
101102
(if (= old-content content)
@@ -119,7 +120,7 @@
119120
;; Normal operation - write and return full result
120121
:else
121122
(do
122-
(spit file content)
123+
(file-utils/spit-utf8 file content)
123124
{:error false
124125
:type (if file-exists? "update" "create")
125126
:file-path file-path

0 commit comments

Comments
 (0)