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/LICENSE +21 -0
- package/README.md +191 -0
- package/bin/claude-depester +215 -0
- package/lib/bun-binary.js +530 -0
- package/lib/detector.js +184 -0
- package/lib/hooks.js +181 -0
- package/lib/patcher.js +337 -0
- package/package.json +41 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun binary extraction and repacking utilities
|
|
3
|
+
* Based on tweakcc's approach - proper Bun binary structure handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LIEF = require('node-lief');
|
|
7
|
+
|
|
8
|
+
// Bun trailer that marks the end of embedded data
|
|
9
|
+
const BUN_TRAILER = Buffer.from('\n---- Bun! ----\n');
|
|
10
|
+
|
|
11
|
+
// Size constants for binary structures
|
|
12
|
+
const SIZEOF_OFFSETS = 32;
|
|
13
|
+
const SIZEOF_STRING_POINTER = 8;
|
|
14
|
+
const SIZEOF_MODULE = 4 * SIZEOF_STRING_POINTER + 4;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a StringPointer from buffer at offset
|
|
18
|
+
*/
|
|
19
|
+
function parseStringPointer(buffer, offset) {
|
|
20
|
+
return {
|
|
21
|
+
offset: buffer.readUInt32LE(offset),
|
|
22
|
+
length: buffer.readUInt32LE(offset + 4),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get content from buffer using StringPointer
|
|
28
|
+
*/
|
|
29
|
+
function getStringPointerContent(buffer, stringPointer) {
|
|
30
|
+
return buffer.subarray(
|
|
31
|
+
stringPointer.offset,
|
|
32
|
+
stringPointer.offset + stringPointer.length
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse BunOffsets structure
|
|
38
|
+
*/
|
|
39
|
+
function parseOffsets(buffer) {
|
|
40
|
+
let pos = 0;
|
|
41
|
+
const byteCount = buffer.readBigUInt64LE(pos);
|
|
42
|
+
pos += 8;
|
|
43
|
+
const modulesPtr = parseStringPointer(buffer, pos);
|
|
44
|
+
pos += 8;
|
|
45
|
+
const entryPointId = buffer.readUInt32LE(pos);
|
|
46
|
+
pos += 4;
|
|
47
|
+
const compileExecArgvPtr = parseStringPointer(buffer, pos);
|
|
48
|
+
|
|
49
|
+
return { byteCount, modulesPtr, entryPointId, compileExecArgvPtr };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a compiled module from buffer
|
|
54
|
+
*/
|
|
55
|
+
function parseCompiledModule(buffer, offset) {
|
|
56
|
+
let pos = offset;
|
|
57
|
+
const name = parseStringPointer(buffer, pos);
|
|
58
|
+
pos += 8;
|
|
59
|
+
const contents = parseStringPointer(buffer, pos);
|
|
60
|
+
pos += 8;
|
|
61
|
+
const sourcemap = parseStringPointer(buffer, pos);
|
|
62
|
+
pos += 8;
|
|
63
|
+
const bytecode = parseStringPointer(buffer, pos);
|
|
64
|
+
pos += 8;
|
|
65
|
+
const encoding = buffer.readUInt8(pos);
|
|
66
|
+
pos += 1;
|
|
67
|
+
const loader = buffer.readUInt8(pos);
|
|
68
|
+
pos += 1;
|
|
69
|
+
const moduleFormat = buffer.readUInt8(pos);
|
|
70
|
+
pos += 1;
|
|
71
|
+
const side = buffer.readUInt8(pos);
|
|
72
|
+
|
|
73
|
+
return { name, contents, sourcemap, bytecode, encoding, loader, moduleFormat, side };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if module name is the claude entrypoint
|
|
78
|
+
*/
|
|
79
|
+
function isClaudeModule(moduleName) {
|
|
80
|
+
return (
|
|
81
|
+
moduleName.endsWith('/claude') ||
|
|
82
|
+
moduleName === 'claude' ||
|
|
83
|
+
moduleName.endsWith('/claude.exe') ||
|
|
84
|
+
moduleName === 'claude.exe'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Iterate over modules in Bun data
|
|
90
|
+
*/
|
|
91
|
+
function mapModules(bunData, bunOffsets, visitor) {
|
|
92
|
+
const modulesListBytes = getStringPointerContent(bunData, bunOffsets.modulesPtr);
|
|
93
|
+
const modulesListCount = Math.floor(modulesListBytes.length / SIZEOF_MODULE);
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < modulesListCount; i++) {
|
|
96
|
+
const offset = i * SIZEOF_MODULE;
|
|
97
|
+
const module = parseCompiledModule(modulesListBytes, offset);
|
|
98
|
+
const moduleName = getStringPointerContent(bunData, module.name).toString('utf-8');
|
|
99
|
+
|
|
100
|
+
const result = visitor(module, moduleName, i);
|
|
101
|
+
if (result !== undefined) {
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse Bun data blob that contains: [data][offsets][trailer]
|
|
111
|
+
*/
|
|
112
|
+
function parseBunDataBlob(bunDataContent) {
|
|
113
|
+
if (bunDataContent.length < SIZEOF_OFFSETS + BUN_TRAILER.length) {
|
|
114
|
+
throw new Error('BUN data is too small');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Verify trailer
|
|
118
|
+
const trailerStart = bunDataContent.length - BUN_TRAILER.length;
|
|
119
|
+
const trailerBytes = bunDataContent.subarray(trailerStart);
|
|
120
|
+
|
|
121
|
+
if (!trailerBytes.equals(BUN_TRAILER)) {
|
|
122
|
+
throw new Error('BUN trailer not found');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Parse Offsets
|
|
126
|
+
const offsetsStart = bunDataContent.length - SIZEOF_OFFSETS - BUN_TRAILER.length;
|
|
127
|
+
const offsetsBytes = bunDataContent.subarray(offsetsStart, offsetsStart + SIZEOF_OFFSETS);
|
|
128
|
+
const bunOffsets = parseOffsets(offsetsBytes);
|
|
129
|
+
|
|
130
|
+
return { bunOffsets, bunData: bunDataContent };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract Bun data from section (MachO/PE format)
|
|
135
|
+
*/
|
|
136
|
+
function extractBunDataFromSection(sectionData) {
|
|
137
|
+
if (sectionData.length < 4) {
|
|
138
|
+
throw new Error('Section data too small');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Try u32 header (old format)
|
|
142
|
+
const bunDataSizeU32 = sectionData.readUInt32LE(0);
|
|
143
|
+
const expectedLengthU32 = 4 + bunDataSizeU32;
|
|
144
|
+
|
|
145
|
+
// Try u64 header (new format)
|
|
146
|
+
const bunDataSizeU64 = sectionData.length >= 8 ? Number(sectionData.readBigUInt64LE(0)) : 0;
|
|
147
|
+
const expectedLengthU64 = 8 + bunDataSizeU64;
|
|
148
|
+
|
|
149
|
+
let headerSize, bunDataSize;
|
|
150
|
+
|
|
151
|
+
// Check which format matches
|
|
152
|
+
if (sectionData.length >= 8 && expectedLengthU64 <= sectionData.length && expectedLengthU64 >= sectionData.length - 4096) {
|
|
153
|
+
headerSize = 8;
|
|
154
|
+
bunDataSize = bunDataSizeU64;
|
|
155
|
+
} else if (expectedLengthU32 <= sectionData.length && expectedLengthU32 >= sectionData.length - 4096) {
|
|
156
|
+
headerSize = 4;
|
|
157
|
+
bunDataSize = bunDataSizeU32;
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error('Cannot determine section header format');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const bunDataContent = sectionData.subarray(headerSize, headerSize + bunDataSize);
|
|
163
|
+
const { bunOffsets, bunData } = parseBunDataBlob(bunDataContent);
|
|
164
|
+
|
|
165
|
+
return { bunOffsets, bunData, sectionHeaderSize: headerSize };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract Bun data from ELF overlay
|
|
170
|
+
*/
|
|
171
|
+
function extractBunDataFromELFOverlay(elfBinary) {
|
|
172
|
+
if (!elfBinary.hasOverlay) {
|
|
173
|
+
throw new Error('ELF binary has no overlay data');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const overlayData = elfBinary.overlay;
|
|
177
|
+
|
|
178
|
+
if (overlayData.length < BUN_TRAILER.length + 8 + SIZEOF_OFFSETS) {
|
|
179
|
+
throw new Error('ELF overlay data is too small');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Read totalByteCount from last 8 bytes
|
|
183
|
+
const totalByteCount = overlayData.readBigUInt64LE(overlayData.length - 8);
|
|
184
|
+
|
|
185
|
+
// Verify trailer
|
|
186
|
+
const trailerStart = overlayData.length - 8 - BUN_TRAILER.length;
|
|
187
|
+
const trailerBytes = overlayData.subarray(trailerStart, overlayData.length - 8);
|
|
188
|
+
|
|
189
|
+
if (!trailerBytes.equals(BUN_TRAILER)) {
|
|
190
|
+
throw new Error('BUN trailer not found in ELF overlay');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Parse Offsets
|
|
194
|
+
const offsetsStart = overlayData.length - 8 - BUN_TRAILER.length - SIZEOF_OFFSETS;
|
|
195
|
+
const offsetsBytes = overlayData.subarray(offsetsStart, overlayData.length - 8 - BUN_TRAILER.length);
|
|
196
|
+
const bunOffsets = parseOffsets(offsetsBytes);
|
|
197
|
+
|
|
198
|
+
const byteCount = typeof bunOffsets.byteCount === 'bigint'
|
|
199
|
+
? bunOffsets.byteCount
|
|
200
|
+
: BigInt(bunOffsets.byteCount);
|
|
201
|
+
|
|
202
|
+
// Extract data region
|
|
203
|
+
const tailDataLen = 8 + BUN_TRAILER.length + SIZEOF_OFFSETS;
|
|
204
|
+
const dataStart = overlayData.length - tailDataLen - Number(byteCount);
|
|
205
|
+
const dataRegion = overlayData.subarray(dataStart, overlayData.length - tailDataLen);
|
|
206
|
+
|
|
207
|
+
// Reconstruct blob [data][offsets][trailer]
|
|
208
|
+
const bunDataBlob = Buffer.concat([dataRegion, offsetsBytes, trailerBytes]);
|
|
209
|
+
|
|
210
|
+
return { bunOffsets, bunData: bunDataBlob };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract Bun data from MachO binary
|
|
215
|
+
*/
|
|
216
|
+
function extractBunDataFromMachO(machoBinary) {
|
|
217
|
+
const bunSegment = machoBinary.getSegment('__BUN');
|
|
218
|
+
if (!bunSegment) throw new Error('__BUN segment not found');
|
|
219
|
+
|
|
220
|
+
const bunSection = bunSegment.getSection('__bun');
|
|
221
|
+
if (!bunSection) throw new Error('__bun section not found');
|
|
222
|
+
|
|
223
|
+
return extractBunDataFromSection(bunSection.content);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract Bun data from PE binary
|
|
228
|
+
*/
|
|
229
|
+
function extractBunDataFromPE(peBinary) {
|
|
230
|
+
const bunSection = peBinary.sections().find(s => s.name === '.bun');
|
|
231
|
+
if (!bunSection) throw new Error('.bun section not found');
|
|
232
|
+
|
|
233
|
+
return extractBunDataFromSection(bunSection.content);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get Bun data from binary (auto-detect format)
|
|
238
|
+
*/
|
|
239
|
+
function getBunData(binary) {
|
|
240
|
+
switch (binary.format) {
|
|
241
|
+
case 'MachO':
|
|
242
|
+
return extractBunDataFromMachO(binary);
|
|
243
|
+
case 'PE':
|
|
244
|
+
return extractBunDataFromPE(binary);
|
|
245
|
+
case 'ELF':
|
|
246
|
+
return extractBunDataFromELFOverlay(binary);
|
|
247
|
+
default:
|
|
248
|
+
throw new Error(`Unsupported binary format: ${binary.format}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extract claude.js from native installation binary
|
|
254
|
+
*/
|
|
255
|
+
function extractClaudeJs(binaryPath) {
|
|
256
|
+
try {
|
|
257
|
+
LIEF.logging.disable();
|
|
258
|
+
const binary = LIEF.parse(binaryPath);
|
|
259
|
+
const { bunOffsets, bunData } = getBunData(binary);
|
|
260
|
+
|
|
261
|
+
const result = mapModules(bunData, bunOffsets, (module, moduleName) => {
|
|
262
|
+
if (!isClaudeModule(moduleName)) return undefined;
|
|
263
|
+
|
|
264
|
+
const moduleContents = getStringPointerContent(bunData, module.contents);
|
|
265
|
+
return moduleContents.length > 0 ? moduleContents : undefined;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return result || null;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Rebuild Bun data with modified claude.js
|
|
276
|
+
*/
|
|
277
|
+
function rebuildBunData(bunData, bunOffsets, modifiedClaudeJs) {
|
|
278
|
+
// Collect all string data and module metadata
|
|
279
|
+
const stringsData = [];
|
|
280
|
+
const modulesMetadata = [];
|
|
281
|
+
|
|
282
|
+
mapModules(bunData, bunOffsets, (module, moduleName) => {
|
|
283
|
+
const nameBytes = getStringPointerContent(bunData, module.name);
|
|
284
|
+
|
|
285
|
+
// Use modified contents for claude module
|
|
286
|
+
let contentsBytes;
|
|
287
|
+
if (modifiedClaudeJs && isClaudeModule(moduleName)) {
|
|
288
|
+
contentsBytes = modifiedClaudeJs;
|
|
289
|
+
} else {
|
|
290
|
+
contentsBytes = getStringPointerContent(bunData, module.contents);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const sourcemapBytes = getStringPointerContent(bunData, module.sourcemap);
|
|
294
|
+
const bytecodeBytes = getStringPointerContent(bunData, module.bytecode);
|
|
295
|
+
|
|
296
|
+
modulesMetadata.push({
|
|
297
|
+
name: nameBytes,
|
|
298
|
+
contents: contentsBytes,
|
|
299
|
+
sourcemap: sourcemapBytes,
|
|
300
|
+
bytecode: bytecodeBytes,
|
|
301
|
+
encoding: module.encoding,
|
|
302
|
+
loader: module.loader,
|
|
303
|
+
moduleFormat: module.moduleFormat,
|
|
304
|
+
side: module.side,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
stringsData.push(nameBytes, contentsBytes, sourcemapBytes, bytecodeBytes);
|
|
308
|
+
return undefined;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Calculate buffer layout
|
|
312
|
+
let currentOffset = 0;
|
|
313
|
+
const stringOffsets = [];
|
|
314
|
+
|
|
315
|
+
for (const stringData of stringsData) {
|
|
316
|
+
stringOffsets.push({ offset: currentOffset, length: stringData.length });
|
|
317
|
+
currentOffset += stringData.length + 1; // +1 for null terminator
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const modulesListOffset = currentOffset;
|
|
321
|
+
const modulesListSize = modulesMetadata.length * SIZEOF_MODULE;
|
|
322
|
+
currentOffset += modulesListSize;
|
|
323
|
+
|
|
324
|
+
const compileExecArgvBytes = getStringPointerContent(bunData, bunOffsets.compileExecArgvPtr);
|
|
325
|
+
const compileExecArgvOffset = currentOffset;
|
|
326
|
+
const compileExecArgvLength = compileExecArgvBytes.length;
|
|
327
|
+
currentOffset += compileExecArgvLength + 1;
|
|
328
|
+
|
|
329
|
+
const offsetsOffset = currentOffset;
|
|
330
|
+
currentOffset += SIZEOF_OFFSETS;
|
|
331
|
+
|
|
332
|
+
const trailerOffset = currentOffset;
|
|
333
|
+
currentOffset += BUN_TRAILER.length;
|
|
334
|
+
|
|
335
|
+
// Build new buffer
|
|
336
|
+
const newBuffer = Buffer.allocUnsafe(currentOffset);
|
|
337
|
+
newBuffer.fill(0);
|
|
338
|
+
|
|
339
|
+
// Write strings
|
|
340
|
+
let stringIdx = 0;
|
|
341
|
+
for (const { offset, length } of stringOffsets) {
|
|
342
|
+
if (length > 0) {
|
|
343
|
+
stringsData[stringIdx].copy(newBuffer, offset, 0, length);
|
|
344
|
+
}
|
|
345
|
+
newBuffer[offset + length] = 0;
|
|
346
|
+
stringIdx++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Write compileExecArgv
|
|
350
|
+
if (compileExecArgvLength > 0) {
|
|
351
|
+
compileExecArgvBytes.copy(newBuffer, compileExecArgvOffset, 0, compileExecArgvLength);
|
|
352
|
+
newBuffer[compileExecArgvOffset + compileExecArgvLength] = 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Write module structures
|
|
356
|
+
for (let i = 0; i < modulesMetadata.length; i++) {
|
|
357
|
+
const baseStringIdx = i * 4;
|
|
358
|
+
const moduleOffset = modulesListOffset + i * SIZEOF_MODULE;
|
|
359
|
+
let pos = moduleOffset;
|
|
360
|
+
|
|
361
|
+
// Write StringPointers
|
|
362
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx].offset, pos);
|
|
363
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx].length, pos + 4);
|
|
364
|
+
pos += 8;
|
|
365
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 1].offset, pos);
|
|
366
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 1].length, pos + 4);
|
|
367
|
+
pos += 8;
|
|
368
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 2].offset, pos);
|
|
369
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 2].length, pos + 4);
|
|
370
|
+
pos += 8;
|
|
371
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 3].offset, pos);
|
|
372
|
+
newBuffer.writeUInt32LE(stringOffsets[baseStringIdx + 3].length, pos + 4);
|
|
373
|
+
pos += 8;
|
|
374
|
+
|
|
375
|
+
// Write flags
|
|
376
|
+
newBuffer.writeUInt8(modulesMetadata[i].encoding, pos);
|
|
377
|
+
newBuffer.writeUInt8(modulesMetadata[i].loader, pos + 1);
|
|
378
|
+
newBuffer.writeUInt8(modulesMetadata[i].moduleFormat, pos + 2);
|
|
379
|
+
newBuffer.writeUInt8(modulesMetadata[i].side, pos + 3);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Write Offsets structure
|
|
383
|
+
let offsetsPos = offsetsOffset;
|
|
384
|
+
newBuffer.writeBigUInt64LE(BigInt(offsetsOffset), offsetsPos);
|
|
385
|
+
offsetsPos += 8;
|
|
386
|
+
newBuffer.writeUInt32LE(modulesListOffset, offsetsPos);
|
|
387
|
+
newBuffer.writeUInt32LE(modulesListSize, offsetsPos + 4);
|
|
388
|
+
offsetsPos += 8;
|
|
389
|
+
newBuffer.writeUInt32LE(bunOffsets.entryPointId, offsetsPos);
|
|
390
|
+
offsetsPos += 4;
|
|
391
|
+
newBuffer.writeUInt32LE(compileExecArgvOffset, offsetsPos);
|
|
392
|
+
newBuffer.writeUInt32LE(compileExecArgvLength, offsetsPos + 4);
|
|
393
|
+
|
|
394
|
+
// Write trailer
|
|
395
|
+
BUN_TRAILER.copy(newBuffer, trailerOffset);
|
|
396
|
+
|
|
397
|
+
return newBuffer;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Build section data with size header
|
|
402
|
+
*/
|
|
403
|
+
function buildSectionData(bunBuffer, headerSize = 8) {
|
|
404
|
+
const sectionData = Buffer.allocUnsafe(headerSize + bunBuffer.length);
|
|
405
|
+
if (headerSize === 8) {
|
|
406
|
+
sectionData.writeBigUInt64LE(BigInt(bunBuffer.length), 0);
|
|
407
|
+
} else {
|
|
408
|
+
sectionData.writeUInt32LE(bunBuffer.length, 0);
|
|
409
|
+
}
|
|
410
|
+
bunBuffer.copy(sectionData, headerSize);
|
|
411
|
+
return sectionData;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Repack ELF binary with modified Bun data
|
|
416
|
+
*/
|
|
417
|
+
function repackELF(elfBinary, binaryPath, newBunBuffer, outputPath) {
|
|
418
|
+
const fs = require('fs');
|
|
419
|
+
|
|
420
|
+
// Build new overlay
|
|
421
|
+
const newOverlay = Buffer.allocUnsafe(newBunBuffer.length + 8);
|
|
422
|
+
newBunBuffer.copy(newOverlay, 0);
|
|
423
|
+
newOverlay.writeBigUInt64LE(BigInt(newBunBuffer.length), newBunBuffer.length);
|
|
424
|
+
|
|
425
|
+
elfBinary.overlay = newOverlay;
|
|
426
|
+
|
|
427
|
+
// Write atomically
|
|
428
|
+
const tempPath = outputPath + '.tmp';
|
|
429
|
+
elfBinary.write(tempPath);
|
|
430
|
+
|
|
431
|
+
const origStat = fs.statSync(binaryPath);
|
|
432
|
+
fs.chmodSync(tempPath, origStat.mode);
|
|
433
|
+
fs.renameSync(tempPath, outputPath);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Repack MachO binary with modified Bun data
|
|
438
|
+
*/
|
|
439
|
+
function repackMachO(machoBinary, binaryPath, newBunBuffer, outputPath, sectionHeaderSize) {
|
|
440
|
+
const fs = require('fs');
|
|
441
|
+
const { execSync } = require('child_process');
|
|
442
|
+
|
|
443
|
+
// Remove code signature
|
|
444
|
+
if (machoBinary.hasCodeSignature) {
|
|
445
|
+
machoBinary.removeSignature();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const bunSegment = machoBinary.getSegment('__BUN');
|
|
449
|
+
const bunSection = bunSegment.getSection('__bun');
|
|
450
|
+
|
|
451
|
+
const newSectionData = buildSectionData(newBunBuffer, sectionHeaderSize);
|
|
452
|
+
const sizeDiff = newSectionData.length - Number(bunSection.size);
|
|
453
|
+
|
|
454
|
+
if (sizeDiff > 0) {
|
|
455
|
+
const isARM64 = machoBinary.header.cpuType === LIEF.MachO.Header.CPU_TYPE.ARM64;
|
|
456
|
+
const PAGE_SIZE = isARM64 ? 16384 : 4096;
|
|
457
|
+
const alignedSizeDiff = Math.ceil(sizeDiff / PAGE_SIZE) * PAGE_SIZE;
|
|
458
|
+
|
|
459
|
+
machoBinary.extendSegment(bunSegment, alignedSizeDiff);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
bunSection.content = newSectionData;
|
|
463
|
+
bunSection.size = BigInt(newSectionData.length);
|
|
464
|
+
|
|
465
|
+
// Write atomically
|
|
466
|
+
const tempPath = outputPath + '.tmp';
|
|
467
|
+
machoBinary.write(tempPath);
|
|
468
|
+
|
|
469
|
+
const origStat = fs.statSync(binaryPath);
|
|
470
|
+
fs.chmodSync(tempPath, origStat.mode);
|
|
471
|
+
fs.renameSync(tempPath, outputPath);
|
|
472
|
+
|
|
473
|
+
// Re-sign with ad-hoc signature
|
|
474
|
+
try {
|
|
475
|
+
execSync(`codesign -s - -f "${outputPath}"`, { stdio: 'ignore' });
|
|
476
|
+
} catch (e) {
|
|
477
|
+
// Ignore codesign errors on non-macOS
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Repack PE binary with modified Bun data
|
|
483
|
+
*/
|
|
484
|
+
function repackPE(peBinary, binaryPath, newBunBuffer, outputPath, sectionHeaderSize) {
|
|
485
|
+
const fs = require('fs');
|
|
486
|
+
|
|
487
|
+
const bunSection = peBinary.sections().find(s => s.name === '.bun');
|
|
488
|
+
const newSectionData = buildSectionData(newBunBuffer, sectionHeaderSize);
|
|
489
|
+
|
|
490
|
+
bunSection.content = newSectionData;
|
|
491
|
+
bunSection.virtualSize = BigInt(newSectionData.length);
|
|
492
|
+
bunSection.size = BigInt(newSectionData.length);
|
|
493
|
+
|
|
494
|
+
// Write atomically
|
|
495
|
+
const tempPath = outputPath + '.tmp';
|
|
496
|
+
peBinary.write(tempPath);
|
|
497
|
+
fs.renameSync(tempPath, outputPath);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Repack native installation with modified claude.js
|
|
502
|
+
*/
|
|
503
|
+
function repackNativeInstallation(binaryPath, modifiedClaudeJs, outputPath) {
|
|
504
|
+
LIEF.logging.disable();
|
|
505
|
+
const binary = LIEF.parse(binaryPath);
|
|
506
|
+
|
|
507
|
+
const { bunOffsets, bunData, sectionHeaderSize } = getBunData(binary);
|
|
508
|
+
const newBuffer = rebuildBunData(bunData, bunOffsets, modifiedClaudeJs);
|
|
509
|
+
|
|
510
|
+
switch (binary.format) {
|
|
511
|
+
case 'MachO':
|
|
512
|
+
repackMachO(binary, binaryPath, newBuffer, outputPath, sectionHeaderSize);
|
|
513
|
+
break;
|
|
514
|
+
case 'PE':
|
|
515
|
+
repackPE(binary, binaryPath, newBuffer, outputPath, sectionHeaderSize);
|
|
516
|
+
break;
|
|
517
|
+
case 'ELF':
|
|
518
|
+
repackELF(binary, binaryPath, newBuffer, outputPath);
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
throw new Error(`Unsupported binary format: ${binary.format}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
module.exports = {
|
|
526
|
+
extractClaudeJs,
|
|
527
|
+
repackNativeInstallation,
|
|
528
|
+
isClaudeModule,
|
|
529
|
+
BUN_TRAILER,
|
|
530
|
+
};
|
package/lib/detector.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect Claude Code installation paths
|
|
3
|
+
* Supports: native binary, local npm, global npm, homebrew
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const HOME = os.homedir();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get all potential paths to check (both cli.js and native binary)
|
|
15
|
+
*/
|
|
16
|
+
function getPotentialPaths() {
|
|
17
|
+
const paths = [];
|
|
18
|
+
|
|
19
|
+
// Priority 1: Native binary (recommended installation method)
|
|
20
|
+
// The binary is at ~/.local/bin/claude which symlinks to ~/.local/share/claude/versions/X.Y.Z
|
|
21
|
+
if (process.platform !== 'win32') {
|
|
22
|
+
try {
|
|
23
|
+
const claudePath = execSync('which claude', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
24
|
+
if (claudePath) {
|
|
25
|
+
// Resolve symlinks to get the actual binary
|
|
26
|
+
const realPath = fs.realpathSync(claudePath);
|
|
27
|
+
paths.push({
|
|
28
|
+
method: 'native binary (which claude)',
|
|
29
|
+
path: realPath,
|
|
30
|
+
type: 'binary'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// which failed or claude not found
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Also check versions directory directly
|
|
39
|
+
const versionsDir = path.join(HOME, '.local', 'share', 'claude', 'versions');
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(versionsDir)) {
|
|
42
|
+
const versions = fs.readdirSync(versionsDir)
|
|
43
|
+
.filter(f => /^\d+\.\d+\.\d+$/.test(f))
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
|
|
46
|
+
const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
|
|
47
|
+
return bMajor - aMajor || bMinor - aMinor || bPatch - aPatch;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (versions.length > 0) {
|
|
51
|
+
paths.push({
|
|
52
|
+
method: 'native binary (~/.local/share/claude/versions)',
|
|
53
|
+
path: path.join(versionsDir, versions[0]),
|
|
54
|
+
type: 'binary'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Failed to read versions dir
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Priority 2: Local npm installations
|
|
63
|
+
paths.push({
|
|
64
|
+
method: 'local npm (~/.claude)',
|
|
65
|
+
path: path.join(HOME, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
66
|
+
type: 'js'
|
|
67
|
+
});
|
|
68
|
+
paths.push({
|
|
69
|
+
method: 'local npm (~/.config/claude)',
|
|
70
|
+
path: path.join(HOME, '.config', 'claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
71
|
+
type: 'js'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Priority 3: Global npm (dynamic)
|
|
75
|
+
try {
|
|
76
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
77
|
+
if (npmRoot) {
|
|
78
|
+
paths.push({
|
|
79
|
+
method: 'global npm (npm root -g)',
|
|
80
|
+
path: path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
81
|
+
type: 'js'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// npm not available or failed
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Priority 4: Derive from process.execPath
|
|
89
|
+
try {
|
|
90
|
+
const nodeDir = path.dirname(process.execPath);
|
|
91
|
+
paths.push({
|
|
92
|
+
method: 'derived from node binary',
|
|
93
|
+
path: path.join(nodeDir, '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
94
|
+
type: 'js'
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Failed to derive
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Priority 5: Common homebrew locations (macOS)
|
|
101
|
+
if (process.platform === 'darwin') {
|
|
102
|
+
paths.push({
|
|
103
|
+
method: 'homebrew (arm64)',
|
|
104
|
+
path: '/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
105
|
+
type: 'js'
|
|
106
|
+
});
|
|
107
|
+
paths.push({
|
|
108
|
+
method: 'homebrew (x86)',
|
|
109
|
+
path: '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
110
|
+
type: 'js'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return paths;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a file contains the silly words (works for both JS and binary)
|
|
119
|
+
*/
|
|
120
|
+
function containsSillyWords(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
// Read as buffer to handle both text and binary
|
|
123
|
+
const content = fs.readFileSync(filePath);
|
|
124
|
+
const contentStr = content.toString('utf-8');
|
|
125
|
+
|
|
126
|
+
// Check for marker words
|
|
127
|
+
const markers = ['Flibbertigibbeting', 'Discombobulating', 'Clauding'];
|
|
128
|
+
let found = 0;
|
|
129
|
+
for (const marker of markers) {
|
|
130
|
+
if (contentStr.includes(marker)) {
|
|
131
|
+
found++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return found >= 2;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Find Claude Code installation (cli.js or native binary)
|
|
142
|
+
* @returns {{ path: string, method: string, type: 'js' | 'binary' } | null}
|
|
143
|
+
*/
|
|
144
|
+
function findClaudeCode() {
|
|
145
|
+
const potentialPaths = getPotentialPaths();
|
|
146
|
+
|
|
147
|
+
for (const { method, path: codePath, type } of potentialPaths) {
|
|
148
|
+
try {
|
|
149
|
+
const resolvedPath = path.resolve(codePath);
|
|
150
|
+
if (fs.existsSync(resolvedPath)) {
|
|
151
|
+
const stats = fs.statSync(resolvedPath);
|
|
152
|
+
if (stats.isFile() && containsSillyWords(resolvedPath)) {
|
|
153
|
+
return { path: resolvedPath, method, type };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// File doesn't exist or can't be read
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Legacy alias
|
|
165
|
+
function findCliJs() {
|
|
166
|
+
const result = findClaudeCode();
|
|
167
|
+
if (result) {
|
|
168
|
+
return { path: result.path, method: result.method, type: result.type };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get all searched paths for error reporting
|
|
175
|
+
*/
|
|
176
|
+
function getSearchedPaths() {
|
|
177
|
+
return getPotentialPaths();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
findClaudeCode,
|
|
182
|
+
findCliJs,
|
|
183
|
+
getSearchedPaths
|
|
184
|
+
};
|