claude-code-extensions 0.1.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/ast.js +276 -0
- package/cx +228 -0
- package/cx-setup +101 -0
- package/package.json +41 -0
- package/patch-worker.js +31 -0
- package/patches/always-show-context.js +123 -0
- package/patches/always-show-thinking.js +65 -0
- package/patches/banner.js +58 -0
- package/patches/cd-command.js +104 -0
- package/patches/cx-badge.js +112 -0
- package/patches/cx-resume-commands.js +58 -0
- package/patches/disable-paste-collapse.js +52 -0
- package/patches/disable-telemetry.js +84 -0
- package/patches/index.js +17 -0
- package/patches/no-attribution.js +55 -0
- package/patches/no-npm-warning.js +32 -0
- package/patches/no-tips.js +29 -0
- package/patches/persist-max-effort.js +70 -0
- package/patches/queue.js +215 -0
- package/patches/random-clawd.js +52 -0
- package/patches/reload.js +68 -0
- package/patches/show-file-in-collapsed-read.js +178 -0
- package/patches/swap-enter-submit.js +188 -0
- package/setup.js +222 -0
- package/transform-worker.js +38 -0
- package/transform.js +99 -0
package/ast.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST indexing and query helpers for cx patches.
|
|
3
|
+
*
|
|
4
|
+
* ASTIndex builds lookup tables in a single walk so that subsequent
|
|
5
|
+
* queries are O(matches) instead of O(all nodes). The generator-based
|
|
6
|
+
* walkAST is retained for backward compat — some patches iterate it
|
|
7
|
+
* directly over subtrees.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// Walk (generator, for direct use by patches)
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
export function* walkAST(node) {
|
|
15
|
+
if (!node || typeof node !== 'object') return;
|
|
16
|
+
if (node.type) yield node;
|
|
17
|
+
for (const key of Object.keys(node)) {
|
|
18
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'raw') continue;
|
|
19
|
+
const child = node[key];
|
|
20
|
+
if (Array.isArray(child)) {
|
|
21
|
+
for (const item of child) {
|
|
22
|
+
if (item && typeof item === 'object' && item.type) yield* walkAST(item);
|
|
23
|
+
}
|
|
24
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
25
|
+
yield* walkAST(child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// Index
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
export class ASTIndex {
|
|
35
|
+
constructor(ast) {
|
|
36
|
+
this.ast = ast;
|
|
37
|
+
this.nodesByType = new Map();
|
|
38
|
+
this.literalsByValue = new Map();
|
|
39
|
+
this.parentMap = new WeakMap();
|
|
40
|
+
/** Pre-order flat list — sorted by start position. */
|
|
41
|
+
this.allNodes = [];
|
|
42
|
+
this._build(ast, null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_build(node, parent) {
|
|
46
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
47
|
+
|
|
48
|
+
this.allNodes.push(node);
|
|
49
|
+
if (parent) this.parentMap.set(node, parent);
|
|
50
|
+
|
|
51
|
+
let byType = this.nodesByType.get(node.type);
|
|
52
|
+
if (!byType) { byType = []; this.nodesByType.set(node.type, byType); }
|
|
53
|
+
byType.push(node);
|
|
54
|
+
|
|
55
|
+
if (node.type === 'Literal' && node.value != null) {
|
|
56
|
+
let byVal = this.literalsByValue.get(node.value);
|
|
57
|
+
if (!byVal) { byVal = []; this.literalsByValue.set(node.value, byVal); }
|
|
58
|
+
byVal.push(node);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const key of Object.keys(node)) {
|
|
62
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'raw') continue;
|
|
63
|
+
const child = node[key];
|
|
64
|
+
if (Array.isArray(child)) {
|
|
65
|
+
for (const item of child) this._build(item, node);
|
|
66
|
+
} else {
|
|
67
|
+
this._build(child, node);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Generic queries ────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
findFirst(root, predicate) {
|
|
75
|
+
if (!root || root === this.ast) {
|
|
76
|
+
for (const n of this.allNodes) if (predicate(n)) return n;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const lo = this._lowerBound(root.start);
|
|
80
|
+
for (let i = lo; i < this.allNodes.length; i++) {
|
|
81
|
+
const n = this.allNodes[i];
|
|
82
|
+
if (n.start >= root.end) break;
|
|
83
|
+
if (n.end <= root.end && predicate(n)) return n;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
findAll(root, predicate) {
|
|
89
|
+
const results = [];
|
|
90
|
+
if (!root || root === this.ast) {
|
|
91
|
+
for (const n of this.allNodes) if (predicate(n)) results.push(n);
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
const lo = this._lowerBound(root.start);
|
|
95
|
+
for (let i = lo; i < this.allNodes.length; i++) {
|
|
96
|
+
const n = this.allNodes[i];
|
|
97
|
+
if (n.start >= root.end) break;
|
|
98
|
+
if (n.end <= root.end && predicate(n)) results.push(n);
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Specialized queries (indexed) ──────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
findArrayWithConsecutiveStrings(root, str1, str2) {
|
|
106
|
+
for (const lit of this._inRange(this.literalsByValue.get(str1), root)) {
|
|
107
|
+
const parent = this.parentMap.get(lit);
|
|
108
|
+
if (!parent || parent.type !== 'ArrayExpression') continue;
|
|
109
|
+
const idx = parent.elements.indexOf(lit);
|
|
110
|
+
if (idx >= 0 && idx < parent.elements.length - 1) {
|
|
111
|
+
const next = parent.elements[idx + 1];
|
|
112
|
+
if (next?.type === 'Literal' && next.value === str2) return parent;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
findObjectWithStringProps(root, propPairs) {
|
|
119
|
+
let bestVal = propPairs[0][1], bestCount = Infinity;
|
|
120
|
+
for (const [, v] of propPairs) {
|
|
121
|
+
const c = (this.literalsByValue.get(v) || []).length;
|
|
122
|
+
if (c < bestCount) { bestVal = v; bestCount = c; }
|
|
123
|
+
}
|
|
124
|
+
for (const lit of this._inRange(this.literalsByValue.get(bestVal), root)) {
|
|
125
|
+
let obj = this.parentMap.get(lit);
|
|
126
|
+
if (obj?.type === 'Property') obj = this.parentMap.get(obj);
|
|
127
|
+
if (!obj || obj.type !== 'ObjectExpression') continue;
|
|
128
|
+
if (propPairs.every(([key, value]) =>
|
|
129
|
+
obj.properties.some(prop => {
|
|
130
|
+
if (prop.type !== 'Property') return false;
|
|
131
|
+
const kMatch = (prop.key.type === 'Identifier' && prop.key.name === key) ||
|
|
132
|
+
(prop.key.type === 'Literal' && prop.key.value === key);
|
|
133
|
+
return kMatch && prop.value.type === 'Literal' && prop.value.value === value;
|
|
134
|
+
})
|
|
135
|
+
)) return obj;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
findHookCallWithObjectKeys(root, hookName, keys) {
|
|
141
|
+
for (const node of this._inRange(this.nodesByType.get('CallExpression'), root)) {
|
|
142
|
+
const c = node.callee;
|
|
143
|
+
if (c.type !== 'MemberExpression' || c.property.name !== hookName) continue;
|
|
144
|
+
const firstArg = node.arguments[0];
|
|
145
|
+
if (!firstArg) continue;
|
|
146
|
+
for (const obj of this._inRange(this.nodesByType.get('ObjectExpression'), firstArg)) {
|
|
147
|
+
if (keys.every(k => obj.properties.some(p =>
|
|
148
|
+
p.type === 'Property' &&
|
|
149
|
+
((p.key.type === 'Literal' && p.key.value === k) || (p.key.type === 'Identifier' && p.key.name === k))
|
|
150
|
+
))) return node;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
findFunctionsContainingStrings(root, ...strings) {
|
|
157
|
+
let rarest = strings[0];
|
|
158
|
+
let rarestCount = (this.literalsByValue.get(rarest) || []).length;
|
|
159
|
+
for (let i = 1; i < strings.length; i++) {
|
|
160
|
+
const count = (this.literalsByValue.get(strings[i]) || []).length;
|
|
161
|
+
if (count < rarestCount) { rarest = strings[i]; rarestCount = count; }
|
|
162
|
+
}
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const results = [];
|
|
165
|
+
for (const lit of this._inRange(this.literalsByValue.get(rarest), root)) {
|
|
166
|
+
const fn = this.enclosingFunction(lit);
|
|
167
|
+
if (!fn || seen.has(fn)) continue;
|
|
168
|
+
seen.add(fn);
|
|
169
|
+
if (strings.every(s =>
|
|
170
|
+
(this.literalsByValue.get(s) || []).some(l => l.start >= fn.start && l.end <= fn.end)
|
|
171
|
+
)) results.push(fn);
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getDestructuredName(objPattern, propKey) {
|
|
177
|
+
for (const prop of objPattern.properties) {
|
|
178
|
+
if (prop.type === 'RestElement') continue;
|
|
179
|
+
const k = prop.key;
|
|
180
|
+
if ((k.type === 'Identifier' && k.name === propKey) || (k.type === 'Literal' && k.value === propKey)) {
|
|
181
|
+
if (prop.value.type === 'Identifier') return prop.value.name;
|
|
182
|
+
if (prop.value.type === 'AssignmentPattern' && prop.value.left.type === 'Identifier') return prop.value.left.name;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Internal helpers ───────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
_inRange(nodes, root) {
|
|
191
|
+
if (!nodes) return [];
|
|
192
|
+
if (!root || root === this.ast) return nodes;
|
|
193
|
+
return nodes.filter(n => n.start >= root.start && n.end <= root.end);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_lowerBound(target) {
|
|
197
|
+
let lo = 0, hi = this.allNodes.length;
|
|
198
|
+
while (lo < hi) {
|
|
199
|
+
const mid = (lo + hi) >> 1;
|
|
200
|
+
if (this.allNodes[mid].start < target) lo = mid + 1; else hi = mid;
|
|
201
|
+
}
|
|
202
|
+
return lo;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Walk up the parent chain to find the nearest ancestor of a given type. */
|
|
206
|
+
ancestor(node, type) {
|
|
207
|
+
let current = this.parentMap.get(node);
|
|
208
|
+
while (current) {
|
|
209
|
+
if (current.type === type) return current;
|
|
210
|
+
current = this.parentMap.get(current);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
enclosingFunction(node) {
|
|
216
|
+
let current = this.parentMap.get(node);
|
|
217
|
+
while (current) {
|
|
218
|
+
if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression' || current.type === 'ArrowFunctionExpression') return current;
|
|
219
|
+
current = this.parentMap.get(current);
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
+
// Source Editor
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
|
|
229
|
+
export class SourceEditor {
|
|
230
|
+
constructor() { this.edits = []; }
|
|
231
|
+
insertAt(pos, text) { this.edits.push({ pos, deleteCount: 0, text }); }
|
|
232
|
+
replaceRange(start, end, text) { this.edits.push({ pos: start, deleteCount: end - start, text }); }
|
|
233
|
+
apply(src) {
|
|
234
|
+
const sorted = [...this.edits].sort((a, b) => b.pos - a.pos);
|
|
235
|
+
let result = src;
|
|
236
|
+
for (const edit of sorted) {
|
|
237
|
+
result = result.slice(0, edit.pos) + edit.text + result.slice(edit.pos + edit.deleteCount);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build the standard patch context from source + AST index + editor.
|
|
245
|
+
* Shared by transform.js (sequential) and patch-worker.js (parallel).
|
|
246
|
+
*/
|
|
247
|
+
export function buildContext(source, index, editor) {
|
|
248
|
+
return {
|
|
249
|
+
ast: index.ast,
|
|
250
|
+
source,
|
|
251
|
+
editor,
|
|
252
|
+
index,
|
|
253
|
+
find: {
|
|
254
|
+
findFirst: (root, pred) => index.findFirst(root, pred),
|
|
255
|
+
findAll: (root, pred) => index.findAll(root, pred),
|
|
256
|
+
walkAST,
|
|
257
|
+
},
|
|
258
|
+
query: {
|
|
259
|
+
findArrayWithConsecutiveStrings: (root, s1, s2) => index.findArrayWithConsecutiveStrings(root, s1, s2),
|
|
260
|
+
findObjectWithStringProps: (root, pp) => index.findObjectWithStringProps(root, pp),
|
|
261
|
+
findHookCallWithObjectKeys: (root, hn, keys) => index.findHookCallWithObjectKeys(root, hn, keys),
|
|
262
|
+
findFunctionsContainingStrings: (root, ...strings) => index.findFunctionsContainingStrings(root, ...strings),
|
|
263
|
+
getDestructuredName: (obj, key) => index.getDestructuredName(obj, key),
|
|
264
|
+
},
|
|
265
|
+
src: node => source.slice(node.start, node.end),
|
|
266
|
+
assert(cond, msg) {
|
|
267
|
+
if (!cond) throw new Error(`transform: ${msg}`);
|
|
268
|
+
},
|
|
269
|
+
getFunctionName(fn) {
|
|
270
|
+
if (fn.type === 'FunctionDeclaration' && fn.id) return fn.id.name;
|
|
271
|
+
const before = source.slice(Math.max(0, fn.start - 100), fn.start);
|
|
272
|
+
const m = before.match(/(?:^|[,;{}\s])(\w+)\s*=\s*$/);
|
|
273
|
+
return m ? m[1] : null;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
package/cx
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cx — Claude Code Extensions
|
|
4
|
+
*
|
|
5
|
+
* Drop-in replacement for `claude`. Applies enabled patches at runtime
|
|
6
|
+
* via AST transformation. The original cli.js is never modified.
|
|
7
|
+
* All arguments pass through to claude untouched.
|
|
8
|
+
*
|
|
9
|
+
* Subcommands:
|
|
10
|
+
* cx setup — interactive patch configurator
|
|
11
|
+
* cx list — show patch status
|
|
12
|
+
* cx reload — signal a running cx instance to reload Claude
|
|
13
|
+
* cx [args] — run patched Claude (with auto-reload support)
|
|
14
|
+
*
|
|
15
|
+
* Reload: type `! cx reload` inside Claude to restart the session
|
|
16
|
+
* with fresh patches applied. The conversation resumes via --continue.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, unlinkSync } from 'fs';
|
|
20
|
+
import { execSync, spawn as nodeSpawn } from 'child_process';
|
|
21
|
+
import { resolve, dirname } from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
import { transformAsync, listPatches } from './transform.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const CONFIG_PATH = resolve(__dirname, '.cx-patches.json');
|
|
27
|
+
const cacheDir = resolve(__dirname, '.cache');
|
|
28
|
+
const cachedCliPath = resolve(cacheDir, 'cli.mjs');
|
|
29
|
+
const metaPath = resolve(cacheDir, 'meta.json');
|
|
30
|
+
const PID_FILE = resolve(__dirname, '.cx-pid');
|
|
31
|
+
const RELOAD_EXIT_CODE = 75;
|
|
32
|
+
|
|
33
|
+
// ── Subcommands (fast path, before any heavy work) ───────────────────────
|
|
34
|
+
|
|
35
|
+
const sub = process.argv[2];
|
|
36
|
+
|
|
37
|
+
if (sub === 'reload') {
|
|
38
|
+
try {
|
|
39
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
40
|
+
process.kill(pid, 'SIGUSR1');
|
|
41
|
+
process.stderr.write('\x1b[2mcx: reload signal sent\x1b[0m\n');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const msg = e.code === 'ENOENT' ? 'no cx instance running'
|
|
44
|
+
: e.code === 'ESRCH' ? 'cx process not running'
|
|
45
|
+
: e.message;
|
|
46
|
+
process.stderr.write(`cx reload: ${msg}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function loadConfig() {
|
|
55
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
56
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getEnabledPatches() {
|
|
60
|
+
const config = loadConfig();
|
|
61
|
+
const all = listPatches();
|
|
62
|
+
if (!config?.patches) return all.map(p => p.id);
|
|
63
|
+
return all.filter(p => {
|
|
64
|
+
if (p.id in config.patches) return config.patches[p.id] !== false;
|
|
65
|
+
return p.defaultEnabled !== false;
|
|
66
|
+
}).map(p => p.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (sub === 'list') {
|
|
70
|
+
const all = listPatches();
|
|
71
|
+
const enabled = getEnabledPatches();
|
|
72
|
+
for (const p of all) {
|
|
73
|
+
const on = enabled.includes(p.id);
|
|
74
|
+
process.stdout.write(` ${on ? '\x1b[32m✓\x1b[0m' : '\x1b[90m✗\x1b[0m'} ${p.id} — ${p.description ?? p.name}\n`);
|
|
75
|
+
}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (sub === 'setup') {
|
|
80
|
+
await import('./setup.js');
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── First run ────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
87
|
+
process.stderr.write('\x1b[2mcx: first run — all patches enabled. run cx setup to configure.\x1b[0m\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Locate cli.js ────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
let cliPath;
|
|
93
|
+
try {
|
|
94
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
95
|
+
cliPath = resolve(npmRoot, '@anthropic-ai/claude-code/cli.js');
|
|
96
|
+
} catch { /* fallthrough */ }
|
|
97
|
+
|
|
98
|
+
if (!cliPath || !existsSync(cliPath)) {
|
|
99
|
+
console.error('cx: could not find @anthropic-ai/claude-code. Install it:');
|
|
100
|
+
console.error(' npm install -g @anthropic-ai/claude-code');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Cache ────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
async function ensureCache() {
|
|
107
|
+
const enabled = getEnabledPatches();
|
|
108
|
+
const stat = statSync(cliPath);
|
|
109
|
+
const key = `${stat.size}:${stat.mtimeMs}:${[...enabled].sort().join(',')}`;
|
|
110
|
+
|
|
111
|
+
let valid = false;
|
|
112
|
+
if (existsSync(cachedCliPath) && existsSync(metaPath)) {
|
|
113
|
+
try { valid = JSON.parse(readFileSync(metaPath, 'utf-8')).key === key; } catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!valid) {
|
|
117
|
+
const t0 = performance.now();
|
|
118
|
+
const total = enabled.length;
|
|
119
|
+
|
|
120
|
+
// Line 0: prepare with elapsed timer, Lines 1..N: patch checklist
|
|
121
|
+
process.stderr.write(`\x1b[2m ◇ preparing (0s)\x1b[0m\n`);
|
|
122
|
+
for (const id of enabled) {
|
|
123
|
+
process.stderr.write(`\x1b[2m ◻ ${id}\x1b[0m\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const timer = setInterval(() => {
|
|
127
|
+
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
|
|
128
|
+
const up = total + 1;
|
|
129
|
+
process.stderr.write(`\x1b[${up}A\r\x1b[2m ◇ preparing (${elapsed}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
|
|
130
|
+
}, 100);
|
|
131
|
+
|
|
132
|
+
const original = readFileSync(cliPath, 'utf-8');
|
|
133
|
+
const patched = await transformAsync(original, enabled, {
|
|
134
|
+
onReady() {
|
|
135
|
+
clearInterval(timer);
|
|
136
|
+
const up = total + 1;
|
|
137
|
+
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
|
|
138
|
+
process.stderr.write(`\x1b[${up}A\r\x1b[2m ◇ ready (${elapsed}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
|
|
139
|
+
},
|
|
140
|
+
onDone(id) {
|
|
141
|
+
const idx = enabled.indexOf(id);
|
|
142
|
+
const up = total - idx;
|
|
143
|
+
process.stderr.write(`\x1b[${up}A\r\x1b[32m ✔ ${id}\x1b[0m\x1b[K\x1b[${up}B\r`);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Replace prepare line with summary
|
|
148
|
+
const up = total + 1;
|
|
149
|
+
process.stderr.write(`\x1b[${up}A\r\x1b[2m ◆ ${total} patches applied (${((performance.now() - t0) / 1000).toFixed(1)}s)\x1b[0m\x1b[K\x1b[${up}B\r`);
|
|
150
|
+
|
|
151
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
152
|
+
writeFileSync(cachedCliPath, patched);
|
|
153
|
+
writeFileSync(metaPath, JSON.stringify({ key, ts: new Date().toISOString() }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── PID file ─────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
160
|
+
function cleanPid() { try { unlinkSync(PID_FILE); } catch {} }
|
|
161
|
+
process.on('exit', cleanPid);
|
|
162
|
+
|
|
163
|
+
// ── Reload loop ──────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
let child = null;
|
|
166
|
+
let shouldReload = false;
|
|
167
|
+
|
|
168
|
+
process.on('SIGUSR1', () => {
|
|
169
|
+
shouldReload = true;
|
|
170
|
+
if (child) child.kill('SIGTERM');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Let child handle Ctrl+C — don't let it kill the wrapper
|
|
174
|
+
process.on('SIGINT', () => {
|
|
175
|
+
if (!child) { cleanPid(); process.exit(130); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
process.on('SIGTERM', () => {
|
|
179
|
+
if (child) child.kill('SIGTERM');
|
|
180
|
+
cleanPid();
|
|
181
|
+
process.exit(143);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build args for a reload: strip --continue/--resume, prepend --continue.
|
|
186
|
+
*/
|
|
187
|
+
function reloadArgs(original) {
|
|
188
|
+
const skip = new Set(['--continue', '-c', '--resume', '-r']);
|
|
189
|
+
const result = ['--continue'];
|
|
190
|
+
for (let i = 0; i < original.length; i++) {
|
|
191
|
+
if (skip.has(original[i])) {
|
|
192
|
+
// --resume/-r may have an optional value; skip it too
|
|
193
|
+
if ((original[i] === '--resume' || original[i] === '-r') &&
|
|
194
|
+
i + 1 < original.length && !original[i + 1].startsWith('-')) {
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
result.push(original[i]);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const userArgs = process.argv.slice(2);
|
|
205
|
+
let isReload = false;
|
|
206
|
+
|
|
207
|
+
while (true) {
|
|
208
|
+
await ensureCache();
|
|
209
|
+
|
|
210
|
+
const args = isReload ? reloadArgs(userArgs) : userArgs;
|
|
211
|
+
|
|
212
|
+
child = nodeSpawn(process.execPath, [cachedCliPath, ...args], {
|
|
213
|
+
stdio: 'inherit',
|
|
214
|
+
env: process.env,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const code = await new Promise(r => child.on('close', (c) => r(c)));
|
|
218
|
+
child = null;
|
|
219
|
+
|
|
220
|
+
if (shouldReload || code === RELOAD_EXIT_CODE) {
|
|
221
|
+
shouldReload = false;
|
|
222
|
+
isReload = true;
|
|
223
|
+
process.stderr.write('\n\x1b[2mcx: reloading…\x1b[0m\n');
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
process.exit(code ?? 0);
|
|
228
|
+
}
|
package/cx-setup
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cx-setup — Configure Claude Code Extensions
|
|
4
|
+
*
|
|
5
|
+
* Standalone tool for managing which patches cx applies.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* cx-setup Interactive TUI
|
|
9
|
+
* cx-setup list Show patch status
|
|
10
|
+
* cx-setup enable <id> Enable a patch
|
|
11
|
+
* cx-setup disable <id> Disable a patch
|
|
12
|
+
* cx-setup reset Re-enable all patches
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
16
|
+
import { resolve, dirname } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { listPatches } from './transform.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const CONFIG_PATH = resolve(__dirname, '.cx-patches.json');
|
|
22
|
+
const CACHE_DIR = resolve(__dirname, '.cache');
|
|
23
|
+
|
|
24
|
+
const DIM = '\x1b[2m', BOLD = '\x1b[1m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';
|
|
25
|
+
|
|
26
|
+
// ── Config helpers ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
30
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).patches || {}; } catch { return {}; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveConfig(patches) {
|
|
34
|
+
writeFileSync(CONFIG_PATH, JSON.stringify({ patches }, null, 2) + '\n');
|
|
35
|
+
invalidateCache();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function invalidateCache() {
|
|
39
|
+
try { rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const cmd = process.argv[2];
|
|
45
|
+
const arg = process.argv[3];
|
|
46
|
+
|
|
47
|
+
if (!cmd) {
|
|
48
|
+
// Interactive TUI
|
|
49
|
+
const { default: setup } = await import('./setup.js');
|
|
50
|
+
setup();
|
|
51
|
+
|
|
52
|
+
} else if (cmd === 'list') {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const all = listPatches();
|
|
55
|
+
console.log(`\n ${BOLD}cx patches${RESET}\n`);
|
|
56
|
+
for (const p of all) {
|
|
57
|
+
const on = config[p.id] !== false;
|
|
58
|
+
const icon = on ? `${GREEN}✔${RESET}` : `${DIM}○${RESET}`;
|
|
59
|
+
console.log(` ${icon} ${p.id.padEnd(16)}${DIM}${p.description}${RESET}`);
|
|
60
|
+
}
|
|
61
|
+
console.log(`\n Run ${BOLD}cx-setup${RESET} to toggle interactively.\n`);
|
|
62
|
+
|
|
63
|
+
} else if (cmd === 'enable') {
|
|
64
|
+
if (!arg) { console.error('Usage: cx-setup enable <patch-id>'); process.exit(1); }
|
|
65
|
+
const all = listPatches();
|
|
66
|
+
if (!all.find(p => p.id === arg)) {
|
|
67
|
+
console.error(`Unknown patch: "${arg}". Available: ${all.map(p => p.id).join(', ')}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
config[arg] = true;
|
|
72
|
+
saveConfig(config);
|
|
73
|
+
console.log(` ${GREEN}✔${RESET} ${BOLD}${arg}${RESET} enabled`);
|
|
74
|
+
|
|
75
|
+
} else if (cmd === 'disable') {
|
|
76
|
+
if (!arg) { console.error('Usage: cx-setup disable <patch-id>'); process.exit(1); }
|
|
77
|
+
const all = listPatches();
|
|
78
|
+
if (!all.find(p => p.id === arg)) {
|
|
79
|
+
console.error(`Unknown patch: "${arg}". Available: ${all.map(p => p.id).join(', ')}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
config[arg] = false;
|
|
84
|
+
saveConfig(config);
|
|
85
|
+
console.log(` ${DIM}○${RESET} ${BOLD}${arg}${RESET} disabled`);
|
|
86
|
+
|
|
87
|
+
} else if (cmd === 'reset') {
|
|
88
|
+
try { rmSync(CONFIG_PATH, { force: true }); } catch { /* ok */ }
|
|
89
|
+
invalidateCache();
|
|
90
|
+
console.log(` ${GREEN}✔${RESET} Config reset — all patches enabled.`);
|
|
91
|
+
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`Unknown command: ${cmd}`);
|
|
94
|
+
console.error(`\nUsage:`);
|
|
95
|
+
console.error(` cx-setup Interactive configurator`);
|
|
96
|
+
console.error(` cx-setup list Show patch status`);
|
|
97
|
+
console.error(` cx-setup enable <id> Enable a patch`);
|
|
98
|
+
console.error(` cx-setup disable <id> Disable a patch`);
|
|
99
|
+
console.error(` cx-setup reset Re-enable all patches`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-extensions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Modular, opt-in patches for @anthropic-ai/claude-code applied at runtime via AST transformation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cx": "./cx",
|
|
8
|
+
"cx-setup": "./cx-setup"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cx",
|
|
12
|
+
"cx-setup",
|
|
13
|
+
"ast.js",
|
|
14
|
+
"transform.js",
|
|
15
|
+
"transform-worker.js",
|
|
16
|
+
"patch-worker.js",
|
|
17
|
+
"setup.js",
|
|
18
|
+
"patches/"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"acorn": "^8.16.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@anthropic-ai/claude-code": "*"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@anthropic-ai/claude-code": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"claude",
|
|
33
|
+
"claude-code",
|
|
34
|
+
"extensions",
|
|
35
|
+
"patches"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/patch-worker.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker thread that runs a single patch and returns its edits.
|
|
3
|
+
* Receives: { source, patchId, patchesDir }
|
|
4
|
+
* Returns: { edits: [{pos, deleteCount, text}] } or { error: string }
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
8
|
+
import * as acorn from 'acorn';
|
|
9
|
+
import { ASTIndex, buildContext } from './ast.js';
|
|
10
|
+
|
|
11
|
+
const { source, patchId, patchesDir } = workerData;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const ast = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
15
|
+
const index = new ASTIndex(ast);
|
|
16
|
+
parentPort.postMessage({ type: 'ready' });
|
|
17
|
+
|
|
18
|
+
const edits = [];
|
|
19
|
+
const editor = {
|
|
20
|
+
insertAt(pos, text) { edits.push({ pos, deleteCount: 0, text }); },
|
|
21
|
+
replaceRange(start, end, text) { edits.push({ pos: start, deleteCount: end - start, text }); },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ctx = buildContext(source, index, editor);
|
|
25
|
+
const patchModule = await import(`${patchesDir}/${patchId}.js`);
|
|
26
|
+
patchModule.default.apply(ctx);
|
|
27
|
+
|
|
28
|
+
parentPort.postMessage({ type: 'done', edits });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
parentPort.postMessage({ type: 'error', error: err.message });
|
|
31
|
+
}
|