claude-depester 1.0.0

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/hooks.js ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Manage Claude Code SessionStart hooks for auto-patching
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ const HOME = os.homedir();
10
+ const SETTINGS_PATH = path.join(HOME, '.claude', 'settings.json');
11
+ const HOOK_COMMAND = 'npx claude-depester --silent';
12
+
13
+ /**
14
+ * Read Claude Code settings
15
+ * @returns {object}
16
+ */
17
+ function readSettings() {
18
+ try {
19
+ if (fs.existsSync(SETTINGS_PATH)) {
20
+ const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
21
+ return JSON.parse(content);
22
+ }
23
+ } catch (e) {
24
+ // File doesn't exist or invalid JSON
25
+ }
26
+ return {};
27
+ }
28
+
29
+ /**
30
+ * Write Claude Code settings
31
+ * @param {object} settings
32
+ */
33
+ function writeSettings(settings) {
34
+ const dir = path.dirname(SETTINGS_PATH);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
39
+ }
40
+
41
+ /**
42
+ * Check if our hook is installed
43
+ * @returns {boolean}
44
+ */
45
+ function isHookInstalled() {
46
+ const settings = readSettings();
47
+
48
+ if (!settings.hooks?.SessionStart) return false;
49
+
50
+ // Check if any SessionStart hook has our command
51
+ for (const hookGroup of settings.hooks.SessionStart) {
52
+ if (!hookGroup.hooks) continue;
53
+ for (const hook of hookGroup.hooks) {
54
+ if (hook.command && hook.command.includes('claude-depester')) {
55
+ return true;
56
+ }
57
+ }
58
+ }
59
+
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * Install SessionStart hook for auto-patching
65
+ * @returns {{ success: boolean, message: string }}
66
+ */
67
+ function installHook() {
68
+ try {
69
+ const settings = readSettings();
70
+
71
+ // Initialize hooks structure if needed
72
+ if (!settings.hooks) {
73
+ settings.hooks = {};
74
+ }
75
+ if (!settings.hooks.SessionStart) {
76
+ settings.hooks.SessionStart = [];
77
+ }
78
+
79
+ // Check if already installed
80
+ if (isHookInstalled()) {
81
+ return {
82
+ success: true,
83
+ message: 'Hook already installed'
84
+ };
85
+ }
86
+
87
+ // Add our hook
88
+ settings.hooks.SessionStart.push({
89
+ hooks: [
90
+ {
91
+ type: 'command',
92
+ command: HOOK_COMMAND,
93
+ timeout: 30
94
+ }
95
+ ]
96
+ });
97
+
98
+ writeSettings(settings);
99
+
100
+ return {
101
+ success: true,
102
+ message: `Hook installed. Claude Code will auto-patch on startup.\nSettings file: ${SETTINGS_PATH}`
103
+ };
104
+
105
+ } catch (err) {
106
+ return {
107
+ success: false,
108
+ message: `Failed to install hook: ${err.message}`
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Remove our SessionStart hook
115
+ * @returns {{ success: boolean, message: string }}
116
+ */
117
+ function removeHook() {
118
+ try {
119
+ const settings = readSettings();
120
+
121
+ if (!settings.hooks?.SessionStart) {
122
+ return {
123
+ success: true,
124
+ message: 'No hook to remove'
125
+ };
126
+ }
127
+
128
+ // Filter out our hooks
129
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(hookGroup => {
130
+ if (!hookGroup.hooks) return true;
131
+ // Remove hook groups that only contain our depester hook
132
+ const nonDepesterHooks = hookGroup.hooks.filter(
133
+ hook => !hook.command?.includes('claude-depester')
134
+ );
135
+ if (nonDepesterHooks.length === 0) return false;
136
+ hookGroup.hooks = nonDepesterHooks;
137
+ return true;
138
+ });
139
+
140
+ // Clean up empty arrays
141
+ if (settings.hooks.SessionStart.length === 0) {
142
+ delete settings.hooks.SessionStart;
143
+ }
144
+ if (Object.keys(settings.hooks).length === 0) {
145
+ delete settings.hooks;
146
+ }
147
+
148
+ writeSettings(settings);
149
+
150
+ return {
151
+ success: true,
152
+ message: 'Hook removed'
153
+ };
154
+
155
+ } catch (err) {
156
+ return {
157
+ success: false,
158
+ message: `Failed to remove hook: ${err.message}`
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get hook status
165
+ * @returns {{ installed: boolean, settingsPath: string }}
166
+ */
167
+ function getHookStatus() {
168
+ return {
169
+ installed: isHookInstalled(),
170
+ settingsPath: SETTINGS_PATH
171
+ };
172
+ }
173
+
174
+ module.exports = {
175
+ installHook,
176
+ removeHook,
177
+ isHookInstalled,
178
+ getHookStatus,
179
+ SETTINGS_PATH,
180
+ HOOK_COMMAND
181
+ };
package/lib/patcher.js ADDED
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Patch Claude Code to remove silly thinking words
3
+ * Uses proper Bun binary extraction/repacking via node-lief
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { extractClaudeJs, repackNativeInstallation } = require('./bun-binary');
9
+
10
+ // Distinctive words to verify we found the right array
11
+ const MARKER_WORDS = [
12
+ 'Flibbertigibbeting',
13
+ 'Discombobulating',
14
+ 'Clauding',
15
+ 'Smooshing',
16
+ 'Wibbling',
17
+ 'Schlepping'
18
+ ];
19
+
20
+ // What to replace with
21
+ const REPLACEMENT_WORD = 'Thinking';
22
+
23
+ /**
24
+ * Check if content contains the silly words array
25
+ * @param {string|Buffer} content - File content
26
+ * @returns {boolean}
27
+ */
28
+ function hasSillyWords(content) {
29
+ const str = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
30
+ let found = 0;
31
+ for (const word of MARKER_WORDS) {
32
+ if (str.includes(`"${word}"`)) {
33
+ found++;
34
+ if (found >= 3) return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * Check if content is already patched
42
+ * @param {string|Buffer} content - File content
43
+ * @returns {boolean}
44
+ */
45
+ function isPatched(content) {
46
+ const str = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
47
+ const hasReplacement = /=\["Thinking"\]/.test(str);
48
+ const hasOriginalArray = str.includes('["Accomplishing"') && str.includes('"Zigzagging"]');
49
+ return hasReplacement && !hasOriginalArray;
50
+ }
51
+
52
+ /**
53
+ * Find and replace the silly words array in JavaScript content
54
+ * @param {Buffer} jsContent - JavaScript content as buffer
55
+ * @returns {{ patched: Buffer, count: number } | null}
56
+ */
57
+ function patchJsContent(jsContent) {
58
+ let str = jsContent.toString('utf-8');
59
+
60
+ // Pattern to match the array assignment: varName=["Accomplishing",...,"Zigzagging"]
61
+ // We need to find arrays that contain our marker words
62
+ const arrayPattern = /([a-zA-Z_$][a-zA-Z0-9_$]*)=\["Accomplishing"[^\]]*"Zigzagging"\]/g;
63
+
64
+ let count = 0;
65
+ str = str.replace(arrayPattern, (match, varName) => {
66
+ // Verify it contains marker words
67
+ let markerCount = 0;
68
+ for (const marker of MARKER_WORDS) {
69
+ if (match.includes(`"${marker}"`)) markerCount++;
70
+ }
71
+
72
+ if (markerCount >= 3) {
73
+ count++;
74
+ return `${varName}=["${REPLACEMENT_WORD}"]`;
75
+ }
76
+ return match;
77
+ });
78
+
79
+ if (count === 0) return null;
80
+
81
+ return {
82
+ patched: Buffer.from(str, 'utf-8'),
83
+ count
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Create backup of file
89
+ * @param {string} filePath - Path to file
90
+ * @returns {string} - Backup path
91
+ */
92
+ function createBackup(filePath) {
93
+ const backupPath = filePath + '.depester.backup';
94
+
95
+ if (!fs.existsSync(backupPath)) {
96
+ fs.copyFileSync(filePath, backupPath);
97
+ }
98
+
99
+ return backupPath;
100
+ }
101
+
102
+ /**
103
+ * Restore from backup
104
+ * @param {string} filePath - Path to file
105
+ * @returns {boolean} - Success
106
+ */
107
+ function restoreBackup(filePath) {
108
+ const backupPath = filePath + '.depester.backup';
109
+
110
+ if (!fs.existsSync(backupPath)) {
111
+ return false;
112
+ }
113
+
114
+ fs.copyFileSync(backupPath, filePath);
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Check if backup exists
120
+ * @param {string} filePath - Path to file
121
+ * @returns {boolean}
122
+ */
123
+ function hasBackup(filePath) {
124
+ return fs.existsSync(filePath + '.depester.backup');
125
+ }
126
+
127
+ /**
128
+ * Detect if file is a native binary (vs plain JS)
129
+ * @param {string} filePath - Path to file
130
+ * @returns {boolean}
131
+ */
132
+ function isNativeBinary(filePath) {
133
+ try {
134
+ const fd = fs.openSync(filePath, 'r');
135
+ const buffer = Buffer.alloc(4);
136
+ fs.readSync(fd, buffer, 0, 4, 0);
137
+ fs.closeSync(fd);
138
+
139
+ // Check for ELF magic
140
+ if (buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46) {
141
+ return true;
142
+ }
143
+ // Check for MachO magic (32/64 bit, both endians)
144
+ const magic = buffer.readUInt32LE(0);
145
+ if (magic === 0xfeedface || magic === 0xfeedfacf ||
146
+ magic === 0xcefaedfe || magic === 0xcffaedfe) {
147
+ return true;
148
+ }
149
+ // Check for PE (MZ header)
150
+ if (buffer[0] === 0x4d && buffer[1] === 0x5a) {
151
+ return true;
152
+ }
153
+
154
+ return false;
155
+ } catch (e) {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Main patch function
162
+ * @param {string} filePath - Path to cli.js or binary
163
+ * @param {object} options - Options
164
+ * @returns {{ success: boolean, message: string, alreadyPatched?: boolean }}
165
+ */
166
+ function patch(filePath, options = {}) {
167
+ const { dryRun = false } = options;
168
+
169
+ try {
170
+ const isBinary = isNativeBinary(filePath);
171
+
172
+ if (isBinary) {
173
+ // Native binary - extract JS, patch, repack
174
+ const claudeJs = extractClaudeJs(filePath);
175
+
176
+ if (!claudeJs) {
177
+ return {
178
+ success: false,
179
+ message: 'Could not extract claude.js from binary. Binary format may not be supported.'
180
+ };
181
+ }
182
+
183
+ // Check if already patched
184
+ if (isPatched(claudeJs)) {
185
+ return {
186
+ success: true,
187
+ message: 'Already patched',
188
+ alreadyPatched: true
189
+ };
190
+ }
191
+
192
+ // Check for silly words
193
+ if (!hasSillyWords(claudeJs)) {
194
+ return {
195
+ success: false,
196
+ message: 'Could not find silly words array in extracted JavaScript.'
197
+ };
198
+ }
199
+
200
+ // Patch the JS content
201
+ const result = patchJsContent(claudeJs);
202
+ if (!result) {
203
+ return {
204
+ success: false,
205
+ message: 'Could not locate words array pattern in JavaScript.'
206
+ };
207
+ }
208
+
209
+ if (dryRun) {
210
+ return {
211
+ success: true,
212
+ message: `Dry run - would patch ${result.count} occurrence(s) of silly words array in binary`,
213
+ dryRun: true
214
+ };
215
+ }
216
+
217
+ // Create backup
218
+ const backupPath = createBackup(filePath);
219
+
220
+ // Repack binary with patched JS
221
+ repackNativeInstallation(filePath, result.patched, filePath);
222
+
223
+ return {
224
+ success: true,
225
+ message: `Patched ${result.count} occurrence(s) successfully. Backup at: ${backupPath}`,
226
+ backupPath
227
+ };
228
+
229
+ } else {
230
+ // Plain JS file (npm installation)
231
+ const content = fs.readFileSync(filePath);
232
+
233
+ if (isPatched(content)) {
234
+ return {
235
+ success: true,
236
+ message: 'Already patched',
237
+ alreadyPatched: true
238
+ };
239
+ }
240
+
241
+ if (!hasSillyWords(content)) {
242
+ return {
243
+ success: false,
244
+ message: 'Could not find silly words array. Claude Code version may not be supported.'
245
+ };
246
+ }
247
+
248
+ const result = patchJsContent(content);
249
+ if (!result) {
250
+ return {
251
+ success: false,
252
+ message: 'Could not locate words array pattern'
253
+ };
254
+ }
255
+
256
+ if (dryRun) {
257
+ return {
258
+ success: true,
259
+ message: `Dry run - would patch ${result.count} occurrence(s) of silly words array`,
260
+ dryRun: true
261
+ };
262
+ }
263
+
264
+ // Create backup
265
+ const backupPath = createBackup(filePath);
266
+
267
+ // Write patched content
268
+ fs.writeFileSync(filePath, result.patched);
269
+
270
+ return {
271
+ success: true,
272
+ message: `Patched ${result.count} occurrence(s) successfully. Backup at: ${backupPath}`,
273
+ backupPath
274
+ };
275
+ }
276
+
277
+ } catch (err) {
278
+ return {
279
+ success: false,
280
+ message: `Error: ${err.message}`
281
+ };
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Check patch status
287
+ * @param {string} filePath - Path to cli.js or binary
288
+ * @returns {{ patched: boolean, hasSillyWords: boolean, hasBackup: boolean, isBinary: boolean }}
289
+ */
290
+ function checkStatus(filePath) {
291
+ try {
292
+ const isBinary = isNativeBinary(filePath);
293
+
294
+ let content;
295
+ if (isBinary) {
296
+ content = extractClaudeJs(filePath);
297
+ if (!content) {
298
+ return {
299
+ patched: false,
300
+ hasSillyWords: false,
301
+ hasBackup: hasBackup(filePath),
302
+ isBinary: true,
303
+ error: 'Could not extract JavaScript from binary'
304
+ };
305
+ }
306
+ } else {
307
+ content = fs.readFileSync(filePath);
308
+ }
309
+
310
+ return {
311
+ patched: isPatched(content),
312
+ hasSillyWords: hasSillyWords(content),
313
+ hasBackup: hasBackup(filePath),
314
+ isBinary
315
+ };
316
+ } catch (err) {
317
+ return {
318
+ patched: false,
319
+ hasSillyWords: false,
320
+ hasBackup: false,
321
+ isBinary: false,
322
+ error: err.message
323
+ };
324
+ }
325
+ }
326
+
327
+ module.exports = {
328
+ patch,
329
+ checkStatus,
330
+ restoreBackup,
331
+ hasBackup,
332
+ hasSillyWords,
333
+ isPatched,
334
+ isNativeBinary,
335
+ MARKER_WORDS,
336
+ REPLACEMENT_WORD
337
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "claude-depester",
3
+ "version": "1.0.0",
4
+ "description": "Remove silly thinking words from Claude Code. Auto-patches and survives updates via SessionStart hook.",
5
+ "main": "lib/patcher.js",
6
+ "bin": {
7
+ "claude-depester": "./bin/claude-depester"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/claude-depester --check"
11
+ },
12
+ "keywords": [
13
+ "claude-code",
14
+ "anthropic",
15
+ "cli",
16
+ "patch",
17
+ "thinking-words"
18
+ ],
19
+ "author": "ominiverdi",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ominiverdi/claude-depester.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/ominiverdi/claude-depester/issues"
27
+ },
28
+ "homepage": "https://github.com/ominiverdi/claude-depester#readme",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "files": [
33
+ "bin/",
34
+ "lib/",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "dependencies": {
39
+ "node-lief": "^0.1.8"
40
+ }
41
+ }