claude-smart 0.2.25 → 0.2.27
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/README.md +30 -53
- package/bin/claude-smart.js +516 -11
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +2 -1
- package/plugin/hooks/codex-hooks.json +7 -7
- package/plugin/pyproject.toml +2 -1
- package/plugin/scripts/_codex_env.sh +1 -0
- package/plugin/scripts/backend-service.sh +12 -7
- package/plugin/scripts/codex-claude-compat.py +144 -0
- package/plugin/scripts/codex-hook.js +386 -0
- package/plugin/scripts/ensure-plugin-root.sh +3 -2
- package/plugin/scripts/smart-install.sh +0 -1
- package/plugin/skills/claude-smart/SKILL.md +32 -0
- package/plugin/src/claude_smart/cli.py +234 -17
- package/plugin/src/claude_smart/events/stop.py +16 -1
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +86 -6
- package/plugin/uv.lock +12 -1
package/bin/claude-smart.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"use strict";
|
|
13
13
|
|
|
14
14
|
const { execSync, spawn } = require("child_process");
|
|
15
|
+
const crypto = require("crypto");
|
|
15
16
|
const {
|
|
16
17
|
appendFileSync,
|
|
17
18
|
cpSync,
|
|
@@ -21,7 +22,8 @@ const {
|
|
|
21
22
|
rmSync,
|
|
22
23
|
writeFileSync,
|
|
23
24
|
} = require("fs");
|
|
24
|
-
const
|
|
25
|
+
const https = require("https");
|
|
26
|
+
const { homedir, tmpdir } = require("os");
|
|
25
27
|
const { dirname, join } = require("path");
|
|
26
28
|
|
|
27
29
|
const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
|
|
@@ -52,6 +54,8 @@ const CODEX_REQUIRED_FILES = [
|
|
|
52
54
|
".agents/plugins/marketplace.json",
|
|
53
55
|
"plugin/.codex-plugin/plugin.json",
|
|
54
56
|
"plugin/hooks/codex-hooks.json",
|
|
57
|
+
"plugin/scripts/codex-claude-compat.py",
|
|
58
|
+
"plugin/scripts/codex-hook.js",
|
|
55
59
|
"plugin/scripts/_codex_env.sh",
|
|
56
60
|
];
|
|
57
61
|
const CODEX_CLI_TIMEOUT_MS = 30_000;
|
|
@@ -175,7 +179,10 @@ function seedReflexioEnv() {
|
|
|
175
179
|
const existing = existsSync(REFLEXIO_ENV_PATH)
|
|
176
180
|
? readFileSync(REFLEXIO_ENV_PATH, "utf8")
|
|
177
181
|
: "";
|
|
178
|
-
const flags = [
|
|
182
|
+
const flags = [
|
|
183
|
+
"CLAUDE_SMART_USE_LOCAL_CLI",
|
|
184
|
+
"CLAUDE_SMART_USE_LOCAL_EMBEDDING",
|
|
185
|
+
];
|
|
179
186
|
const missing = flags.filter((f) => !new RegExp(`^${f}=`, "m").test(existing));
|
|
180
187
|
if (missing.length === 0) return [];
|
|
181
188
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -184,6 +191,246 @@ function seedReflexioEnv() {
|
|
|
184
191
|
return missing;
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
function isWindows() {
|
|
195
|
+
return process.platform === "win32";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function runChecked(command, args, options = {}) {
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
const child = spawn(command, args, {
|
|
201
|
+
cwd: options.cwd,
|
|
202
|
+
env: options.env || process.env,
|
|
203
|
+
shell: isWindows() && /\.(?:cmd|bat)$/i.test(command),
|
|
204
|
+
stdio: "inherit",
|
|
205
|
+
windowsHide: true,
|
|
206
|
+
});
|
|
207
|
+
child.on("exit", (code) => resolve(typeof code === "number" ? code : 1));
|
|
208
|
+
child.on("error", () => resolve(1));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function downloadFile(url, dest) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const request = https.get(url, (response) => {
|
|
215
|
+
if (
|
|
216
|
+
response.statusCode &&
|
|
217
|
+
response.statusCode >= 300 &&
|
|
218
|
+
response.statusCode < 400 &&
|
|
219
|
+
response.headers.location
|
|
220
|
+
) {
|
|
221
|
+
downloadFile(new URL(response.headers.location, url).toString(), dest)
|
|
222
|
+
.then(resolve, reject);
|
|
223
|
+
response.resume();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (response.statusCode !== 200) {
|
|
227
|
+
response.resume();
|
|
228
|
+
reject(new Error(`download failed (${response.statusCode}) for ${url}`));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const chunks = [];
|
|
232
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
233
|
+
response.on("end", () => {
|
|
234
|
+
writeFileSync(dest, Buffer.concat(chunks));
|
|
235
|
+
resolve();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
request.on("error", reject);
|
|
239
|
+
request.setTimeout(120_000, () => request.destroy(new Error(`download timed out for ${url}`)));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveCommand(names, extraDirs = []) {
|
|
244
|
+
const pathParts = [
|
|
245
|
+
...extraDirs,
|
|
246
|
+
...(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":"),
|
|
247
|
+
].filter(Boolean);
|
|
248
|
+
for (const dir of pathParts) {
|
|
249
|
+
for (const name of names) {
|
|
250
|
+
const candidate = join(dir, name);
|
|
251
|
+
if (existsSync(candidate)) return candidate;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function privateNodeRoot() {
|
|
258
|
+
return join(homedir(), ".claude-smart", "node", "current");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function privateNodeBinDirs() {
|
|
262
|
+
const root = privateNodeRoot();
|
|
263
|
+
return [join(root, "bin"), root];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolvePrivateNode() {
|
|
267
|
+
return resolveCommand(isWindows() ? ["node.exe", "node"] : ["node"], privateNodeBinDirs());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolvePrivateNpm() {
|
|
271
|
+
return resolveCommand(
|
|
272
|
+
isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"],
|
|
273
|
+
privateNodeBinDirs(),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function runtimeEnv(extraDirs = []) {
|
|
278
|
+
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
279
|
+
const dirs = [
|
|
280
|
+
...extraDirs,
|
|
281
|
+
...privateNodeBinDirs(),
|
|
282
|
+
join(homedir(), ".local", "bin"),
|
|
283
|
+
join(homedir(), ".cargo", "bin"),
|
|
284
|
+
];
|
|
285
|
+
return {
|
|
286
|
+
...process.env,
|
|
287
|
+
PATH: `${dirs.join(delimiter)}${delimiter}${process.env.PATH || ""}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function ensureWindowsPrivateNode() {
|
|
292
|
+
if (!isWindows()) return null;
|
|
293
|
+
const existing = resolvePrivateNode();
|
|
294
|
+
const existingNpm = resolvePrivateNpm();
|
|
295
|
+
if (existing && existingNpm) return { node: existing, npm: existingNpm };
|
|
296
|
+
|
|
297
|
+
const major = process.env.CLAUDE_SMART_NODE_LTS_MAJOR || "22";
|
|
298
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
299
|
+
const baseUrl = process.env.CLAUDE_SMART_NODE_BASE_URL || `https://nodejs.org/dist/latest-v${major}.x`;
|
|
300
|
+
const nodeRoot = join(homedir(), ".claude-smart", "node");
|
|
301
|
+
const temp = join(tmpdir(), `claude-smart-node-${process.pid}`);
|
|
302
|
+
mkdirSync(nodeRoot, { recursive: true });
|
|
303
|
+
rmSync(temp, { recursive: true, force: true });
|
|
304
|
+
mkdirSync(temp, { recursive: true });
|
|
305
|
+
|
|
306
|
+
const sumsPath = join(temp, "SHASUMS256.txt");
|
|
307
|
+
await downloadFile(`${baseUrl}/SHASUMS256.txt`, sumsPath);
|
|
308
|
+
const sums = readFileSync(sumsPath, "utf8");
|
|
309
|
+
const match = sums
|
|
310
|
+
.split(/\r?\n/)
|
|
311
|
+
.map((line) => line.trim().split(/\s+/))
|
|
312
|
+
.find((parts) => parts[1] && new RegExp(`^node-v[^ ]+-win-${arch}\\.zip$`).test(parts[1]));
|
|
313
|
+
if (!match) throw new Error(`could not resolve Node.js win-${arch} archive from ${baseUrl}`);
|
|
314
|
+
const [expectedHash, archiveName] = match;
|
|
315
|
+
const archivePath = join(temp, archiveName);
|
|
316
|
+
await downloadFile(`${baseUrl}/${archiveName}`, archivePath);
|
|
317
|
+
const actualHash = crypto.createHash("sha256").update(readFileSync(archivePath)).digest("hex");
|
|
318
|
+
if (actualHash !== expectedHash) {
|
|
319
|
+
throw new Error(`Node.js checksum verification failed for ${archiveName}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
|
|
323
|
+
if (!powershell) throw new Error("PowerShell is required to extract private Node.js on Windows");
|
|
324
|
+
const extractDir = join(temp, "extract");
|
|
325
|
+
mkdirSync(extractDir, { recursive: true });
|
|
326
|
+
const code = await runChecked(
|
|
327
|
+
powershell,
|
|
328
|
+
[
|
|
329
|
+
"-NoProfile",
|
|
330
|
+
"-ExecutionPolicy",
|
|
331
|
+
"Bypass",
|
|
332
|
+
"-Command",
|
|
333
|
+
"$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force",
|
|
334
|
+
],
|
|
335
|
+
{ env: { ...process.env, ARCHIVE_PATH: archivePath, DEST_DIR: extractDir } },
|
|
336
|
+
);
|
|
337
|
+
if (code !== 0) throw new Error(`Node.js archive extraction failed for ${archiveName}`);
|
|
338
|
+
const extracted = join(extractDir, archiveName.replace(/\.zip$/, ""));
|
|
339
|
+
const current = privateNodeRoot();
|
|
340
|
+
rmSync(current, { recursive: true, force: true });
|
|
341
|
+
cpSync(extracted, current, { recursive: true, force: true });
|
|
342
|
+
rmSync(temp, { recursive: true, force: true });
|
|
343
|
+
|
|
344
|
+
const node = resolvePrivateNode();
|
|
345
|
+
const npm = resolvePrivateNpm();
|
|
346
|
+
if (!node || !npm) throw new Error("private Node.js install completed but node/npm are not usable");
|
|
347
|
+
return { node, npm };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveUv() {
|
|
351
|
+
return resolveCommand(isWindows() ? ["uv.exe", "uv"] : ["uv"], [
|
|
352
|
+
join(homedir(), ".local", "bin"),
|
|
353
|
+
join(homedir(), ".cargo", "bin"),
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function ensureWindowsUv() {
|
|
358
|
+
let uv = resolveUv();
|
|
359
|
+
if (uv) return uv;
|
|
360
|
+
const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
|
|
361
|
+
if (!powershell) throw new Error("PowerShell is required to install uv on Windows");
|
|
362
|
+
const code = await runChecked(powershell, [
|
|
363
|
+
"-NoProfile",
|
|
364
|
+
"-ExecutionPolicy",
|
|
365
|
+
"Bypass",
|
|
366
|
+
"-Command",
|
|
367
|
+
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
368
|
+
]);
|
|
369
|
+
if (code !== 0) throw new Error("uv install via PowerShell failed");
|
|
370
|
+
uv = resolveUv();
|
|
371
|
+
if (!uv) throw new Error("uv install reported success but uv.exe was not found");
|
|
372
|
+
return uv;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function quoteCommandPart(part) {
|
|
376
|
+
return `"${String(part).replace(/"/g, '\\"')}"`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function patchCodexHooksForWindows(pluginRoot, nodePath) {
|
|
380
|
+
const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
|
|
381
|
+
const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
|
|
382
|
+
const runner = join(pluginRoot, "scripts", "codex-hook.js");
|
|
383
|
+
const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
|
|
384
|
+
const sessionHooks = parsed.hooks.SessionStart?.[0]?.hooks || [];
|
|
385
|
+
if (sessionHooks[0]) sessionHooks[0].command = command("ensure-root");
|
|
386
|
+
if (sessionHooks[1]) sessionHooks[1].command = command("backend");
|
|
387
|
+
if (sessionHooks[2]) sessionHooks[2].command = command("dashboard");
|
|
388
|
+
if (sessionHooks[3]) sessionHooks[3].command = command("hook", "session-start");
|
|
389
|
+
const userPrompt = parsed.hooks.UserPromptSubmit?.[0]?.hooks?.[0];
|
|
390
|
+
if (userPrompt) userPrompt.command = command("hook", "user-prompt");
|
|
391
|
+
const postTool = parsed.hooks.PostToolUse?.[0]?.hooks?.[0];
|
|
392
|
+
if (postTool) postTool.command = command("hook", "post-tool");
|
|
393
|
+
const stop = parsed.hooks.Stop?.[0]?.hooks?.[0];
|
|
394
|
+
if (stop) stop.command = command("hook", "stop");
|
|
395
|
+
writeFileSync(hookPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function ensureWindowsPluginRoot(pluginRoot) {
|
|
399
|
+
const reflexioDir = dirname(REFLEXIO_ENV_PATH);
|
|
400
|
+
const link = join(reflexioDir, "plugin-root");
|
|
401
|
+
mkdirSync(reflexioDir, { recursive: true });
|
|
402
|
+
rmSync(link, { recursive: true, force: true });
|
|
403
|
+
try {
|
|
404
|
+
require("fs").symlinkSync(pluginRoot, link, "junction");
|
|
405
|
+
} catch {
|
|
406
|
+
writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function bootstrapWindowsCodexCache(pluginRoot) {
|
|
411
|
+
if (!isWindows()) return;
|
|
412
|
+
process.stdout.write("Preparing Windows Codex runtime for claude-smart hooks...\n");
|
|
413
|
+
const nodeRuntime = await ensureWindowsPrivateNode();
|
|
414
|
+
patchCodexHooksForWindows(pluginRoot, nodeRuntime.node);
|
|
415
|
+
ensureWindowsPluginRoot(pluginRoot);
|
|
416
|
+
const uv = await ensureWindowsUv();
|
|
417
|
+
const env = runtimeEnv([dirname(uv), ...privateNodeBinDirs()]);
|
|
418
|
+
let code = await runChecked(
|
|
419
|
+
uv,
|
|
420
|
+
["sync", "--locked", "--python", "3.12", "--quiet"],
|
|
421
|
+
{ cwd: pluginRoot, env },
|
|
422
|
+
);
|
|
423
|
+
if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
|
|
424
|
+
|
|
425
|
+
const dashboardDir = join(pluginRoot, "dashboard");
|
|
426
|
+
if (existsSync(dashboardDir)) {
|
|
427
|
+
code = await runChecked(nodeRuntime.npm, ["ci"], { cwd: dashboardDir, env });
|
|
428
|
+
if (code !== 0) throw new Error(`npm ci failed in ${dashboardDir}`);
|
|
429
|
+
code = await runChecked(nodeRuntime.npm, ["run", "build"], { cwd: dashboardDir, env });
|
|
430
|
+
if (code !== 0) throw new Error(`npm run build failed in ${dashboardDir}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
187
434
|
function printHelp() {
|
|
188
435
|
process.stdout.write(
|
|
189
436
|
[
|
|
@@ -205,9 +452,10 @@ function printHelp() {
|
|
|
205
452
|
"Codex install:",
|
|
206
453
|
` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
|
|
207
454
|
" 2. codex plugin marketplace add <copied marketplace>",
|
|
208
|
-
" 3. codex features enable plugin_hooks",
|
|
455
|
+
" 3. codex features enable hooks && codex features enable plugin_hooks",
|
|
209
456
|
" 4. Installs claude-smart into Codex's plugin cache and enables it",
|
|
210
|
-
" 5.
|
|
457
|
+
" 5. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
|
|
458
|
+
" 6. Restart Codex.",
|
|
211
459
|
"",
|
|
212
460
|
"Update:",
|
|
213
461
|
" npx claude-smart update Update to the latest version",
|
|
@@ -359,6 +607,227 @@ function setCodexPluginEnabled() {
|
|
|
359
607
|
writeFileSync(CODEX_CONFIG_PATH, next);
|
|
360
608
|
}
|
|
361
609
|
|
|
610
|
+
function tomlDottedQuoted(name) {
|
|
611
|
+
return `"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function setTomlFeature(feature, value) {
|
|
615
|
+
// Minimal port of `_set_toml_feature` in plugin/src/claude_smart/cli.py:
|
|
616
|
+
// ensures `[features]\n<feature> = <bool>\n` is present in
|
|
617
|
+
// ~/.codex/config.toml, replacing any prior value for the same key.
|
|
618
|
+
const desired = `${feature} = ${value ? "true" : "false"}`;
|
|
619
|
+
const sectionRe = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/;
|
|
620
|
+
const featureRe = new RegExp(`^\\s*${feature.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`);
|
|
621
|
+
const text = existsSync(CODEX_CONFIG_PATH)
|
|
622
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
623
|
+
: "";
|
|
624
|
+
const lines = text.split("\n");
|
|
625
|
+
let inFeatures = false;
|
|
626
|
+
let featuresIdx = null;
|
|
627
|
+
let insertIdx = null;
|
|
628
|
+
let changed = false;
|
|
629
|
+
const out = [];
|
|
630
|
+
for (const line of lines) {
|
|
631
|
+
const sectionMatch = line.match(sectionRe);
|
|
632
|
+
if (sectionMatch) {
|
|
633
|
+
if (inFeatures && insertIdx === null) insertIdx = out.length;
|
|
634
|
+
inFeatures = sectionMatch[1].trim() === "features";
|
|
635
|
+
if (inFeatures) featuresIdx = out.length;
|
|
636
|
+
out.push(line);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (inFeatures && featureRe.test(line)) {
|
|
640
|
+
out.push(desired);
|
|
641
|
+
changed = changed || line !== desired;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
out.push(line);
|
|
645
|
+
}
|
|
646
|
+
if (featuresIdx === null) {
|
|
647
|
+
if (out.length && out[out.length - 1].trim()) out.push("");
|
|
648
|
+
out.push("[features]", desired);
|
|
649
|
+
changed = true;
|
|
650
|
+
} else {
|
|
651
|
+
const sectionEnd = insertIdx !== null ? insertIdx : out.length;
|
|
652
|
+
let hasFeature = false;
|
|
653
|
+
for (let i = featuresIdx + 1; i < sectionEnd; i++) {
|
|
654
|
+
if (featureRe.test(out[i])) { hasFeature = true; break; }
|
|
655
|
+
}
|
|
656
|
+
if (!hasFeature) {
|
|
657
|
+
const idx = insertIdx !== null ? insertIdx : out.length;
|
|
658
|
+
out.splice(idx, 0, desired);
|
|
659
|
+
changed = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!changed && text.endsWith("\n")) return true;
|
|
663
|
+
mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
664
|
+
let payload = out.join("\n");
|
|
665
|
+
if (!payload.endsWith("\n")) payload += "\n";
|
|
666
|
+
writeFileSync(CODEX_CONFIG_PATH, payload);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function setCodexHookStates(states) {
|
|
671
|
+
const entries = Object.entries(states);
|
|
672
|
+
if (entries.length === 0) return false;
|
|
673
|
+
removeTomlSections(CODEX_CONFIG_PATH, {
|
|
674
|
+
exact: new Set(),
|
|
675
|
+
prefixes: [`hooks.state."${CODEX_PLUGIN_ID}:`],
|
|
676
|
+
});
|
|
677
|
+
const existing = existsSync(CODEX_CONFIG_PATH)
|
|
678
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
679
|
+
: "";
|
|
680
|
+
let next = existing;
|
|
681
|
+
if (next && !next.endsWith("\n")) next += "\n";
|
|
682
|
+
if (!next.includes("[hooks.state]")) {
|
|
683
|
+
if (next.trim()) next += "\n";
|
|
684
|
+
next += "[hooks.state]\n";
|
|
685
|
+
}
|
|
686
|
+
if (next.trim()) next += "\n";
|
|
687
|
+
for (const [key, currentHash] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
688
|
+
next += `[hooks.state.${tomlDottedQuoted(key)}]\n`;
|
|
689
|
+
next += "enabled = true\n";
|
|
690
|
+
next += `trusted_hash = "${currentHash}"\n\n`;
|
|
691
|
+
}
|
|
692
|
+
mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
693
|
+
writeFileSync(CODEX_CONFIG_PATH, next.trimEnd() + "\n");
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function createCodexAppServerClient(child) {
|
|
698
|
+
// A single long-lived stdout listener that demultiplexes JSON-RPC responses
|
|
699
|
+
// by id. Avoids losing messages between sequential requests.
|
|
700
|
+
const pending = new Map();
|
|
701
|
+
let buffer = "";
|
|
702
|
+
let exited = false;
|
|
703
|
+
|
|
704
|
+
const onData = (chunk) => {
|
|
705
|
+
buffer += chunk.toString();
|
|
706
|
+
let newline;
|
|
707
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
708
|
+
const line = buffer.slice(0, newline);
|
|
709
|
+
buffer = buffer.slice(newline + 1);
|
|
710
|
+
if (!line.trim()) continue;
|
|
711
|
+
let message;
|
|
712
|
+
try {
|
|
713
|
+
message = JSON.parse(line);
|
|
714
|
+
} catch {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const entry = pending.get(message.id);
|
|
718
|
+
if (!entry) continue;
|
|
719
|
+
pending.delete(message.id);
|
|
720
|
+
clearTimeout(entry.timer);
|
|
721
|
+
if (message.error) {
|
|
722
|
+
entry.reject(new Error(JSON.stringify(message.error)));
|
|
723
|
+
} else {
|
|
724
|
+
entry.resolve(message);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
const onExit = () => {
|
|
729
|
+
exited = true;
|
|
730
|
+
for (const entry of pending.values()) {
|
|
731
|
+
clearTimeout(entry.timer);
|
|
732
|
+
entry.reject(new Error("Codex app-server exited before responding"));
|
|
733
|
+
}
|
|
734
|
+
pending.clear();
|
|
735
|
+
};
|
|
736
|
+
child.stdout.on("data", onData);
|
|
737
|
+
child.on("exit", onExit);
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
request(id, method, params, timeoutMs) {
|
|
741
|
+
return new Promise((resolve, reject) => {
|
|
742
|
+
if (exited) {
|
|
743
|
+
reject(new Error("Codex app-server exited before responding"));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const timer = setTimeout(() => {
|
|
747
|
+
pending.delete(id);
|
|
748
|
+
reject(new Error(`Codex app-server ${method} timed out`));
|
|
749
|
+
}, timeoutMs);
|
|
750
|
+
pending.set(id, { resolve, reject, timer });
|
|
751
|
+
child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
|
|
752
|
+
});
|
|
753
|
+
},
|
|
754
|
+
notify(method, params) {
|
|
755
|
+
if (exited) return;
|
|
756
|
+
child.stdin.write(JSON.stringify({ method, params }) + "\n");
|
|
757
|
+
},
|
|
758
|
+
close() {
|
|
759
|
+
child.stdout.off("data", onData);
|
|
760
|
+
child.off("exit", onExit);
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function listCodexPluginHooks(cwd) {
|
|
766
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
767
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
768
|
+
});
|
|
769
|
+
const client = createCodexAppServerClient(child);
|
|
770
|
+
try {
|
|
771
|
+
await client.request(
|
|
772
|
+
1,
|
|
773
|
+
"initialize",
|
|
774
|
+
{
|
|
775
|
+
clientInfo: {
|
|
776
|
+
name: "claude_smart_installer",
|
|
777
|
+
title: "claude-smart installer",
|
|
778
|
+
version: "0.0.0",
|
|
779
|
+
},
|
|
780
|
+
capabilities: { experimentalApi: true },
|
|
781
|
+
},
|
|
782
|
+
CODEX_CLI_TIMEOUT_MS,
|
|
783
|
+
);
|
|
784
|
+
client.notify("initialized", {});
|
|
785
|
+
const response = await client.request(
|
|
786
|
+
2,
|
|
787
|
+
"hooks/list",
|
|
788
|
+
{ cwds: [cwd] },
|
|
789
|
+
CODEX_CLI_TIMEOUT_MS,
|
|
790
|
+
);
|
|
791
|
+
const hooks = response.result?.data?.[0]?.hooks;
|
|
792
|
+
if (!Array.isArray(hooks)) {
|
|
793
|
+
throw new Error("Codex app-server hook metadata was malformed");
|
|
794
|
+
}
|
|
795
|
+
return hooks.filter(
|
|
796
|
+
(hook) =>
|
|
797
|
+
hook &&
|
|
798
|
+
(hook.pluginId === CODEX_PLUGIN_ID ||
|
|
799
|
+
String(hook.key || "").startsWith(`${CODEX_PLUGIN_ID}:`)),
|
|
800
|
+
);
|
|
801
|
+
} finally {
|
|
802
|
+
client.close();
|
|
803
|
+
child.stdin.destroy();
|
|
804
|
+
child.stdout.destroy();
|
|
805
|
+
child.kill("SIGTERM");
|
|
806
|
+
child.unref();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function trustCodexPluginHooks(cwd) {
|
|
811
|
+
const hooks = await listCodexPluginHooks(cwd);
|
|
812
|
+
const states = {};
|
|
813
|
+
for (const hook of hooks) {
|
|
814
|
+
if (
|
|
815
|
+
typeof hook.key === "string" &&
|
|
816
|
+
hook.key.startsWith(`${CODEX_PLUGIN_ID}:`) &&
|
|
817
|
+
typeof hook.currentHash === "string"
|
|
818
|
+
) {
|
|
819
|
+
states[hook.key] = hook.currentHash;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (Object.keys(states).length === 0) {
|
|
823
|
+
throw new Error("Codex did not report trust hashes for claude-smart hooks");
|
|
824
|
+
}
|
|
825
|
+
if (!setCodexHookStates(states)) {
|
|
826
|
+
throw new Error(`could not write claude-smart hook trust state to ${CODEX_CONFIG_PATH}`);
|
|
827
|
+
}
|
|
828
|
+
return Object.keys(states).length;
|
|
829
|
+
}
|
|
830
|
+
|
|
362
831
|
function codexPluginVersion(pluginRoot) {
|
|
363
832
|
try {
|
|
364
833
|
const manifest = JSON.parse(
|
|
@@ -515,24 +984,60 @@ async function runInstallCodex() {
|
|
|
515
984
|
process.exit(code);
|
|
516
985
|
}
|
|
517
986
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
987
|
+
for (const feature of ["hooks", "plugin_hooks"]) {
|
|
988
|
+
code = await runCodex(["features", "enable", feature]);
|
|
989
|
+
if (code !== 0) {
|
|
990
|
+
// Older Codex builds may not recognize the `hooks` feature name; fall
|
|
991
|
+
// through to writing the flag directly under [features] in config.toml.
|
|
992
|
+
try {
|
|
993
|
+
setTomlFeature(feature, true);
|
|
994
|
+
process.stdout.write(`Enabled Codex ${feature} via ${CODEX_CONFIG_PATH}.\n`);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
process.stderr.write(
|
|
997
|
+
`error: could not enable Codex ${feature} feature: ${err && err.message ? err.message : err}\n`,
|
|
998
|
+
);
|
|
999
|
+
process.exit(code);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
522
1002
|
}
|
|
523
1003
|
|
|
524
1004
|
let cacheDir = null;
|
|
1005
|
+
let trustedHookCount = 0;
|
|
1006
|
+
let trustError = null;
|
|
525
1007
|
try {
|
|
526
1008
|
cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
|
|
527
1009
|
process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
|
|
1010
|
+
await bootstrapWindowsCodexCache(cacheDir);
|
|
528
1011
|
} catch (err) {
|
|
529
1012
|
process.stderr.write(
|
|
530
1013
|
`error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
|
|
531
1014
|
);
|
|
532
1015
|
process.stderr.write(
|
|
533
|
-
`Open Codex, run /plugins,
|
|
1016
|
+
`Open Codex, run /plugins, install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex.\n`,
|
|
1017
|
+
);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1022
|
+
try {
|
|
1023
|
+
trustedHookCount = await trustCodexPluginHooks(process.cwd());
|
|
1024
|
+
trustError = null;
|
|
1025
|
+
break;
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
trustError = err;
|
|
1028
|
+
if (attempt === 0) await new Promise((r) => setTimeout(r, 500));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (trustError) {
|
|
1032
|
+
process.stderr.write(
|
|
1033
|
+
`warning: ${trustError && trustError.message ? trustError.message : trustError}\n`,
|
|
1034
|
+
);
|
|
1035
|
+
process.stderr.write(
|
|
1036
|
+
`Fully quit and reopen Codex in this repo, run /hooks, trust the claude-smart hooks, and restart Codex.\n`,
|
|
534
1037
|
);
|
|
535
1038
|
process.exit(1);
|
|
1039
|
+
} else {
|
|
1040
|
+
process.stdout.write(`Trusted and enabled ${trustedHookCount} claude-smart Codex hooks.\n`);
|
|
536
1041
|
}
|
|
537
1042
|
|
|
538
1043
|
const added = seedReflexioEnv();
|
|
@@ -544,7 +1049,7 @@ async function runInstallCodex() {
|
|
|
544
1049
|
[
|
|
545
1050
|
"",
|
|
546
1051
|
"claude-smart Codex support is installed.",
|
|
547
|
-
`Restart Codex so the installed plugin and hooks reload. /plugins should show claude-smart as installed from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace.`,
|
|
1052
|
+
`Restart Codex so the installed plugin and trusted hooks reload. /plugins should show claude-smart as installed from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace.`,
|
|
548
1053
|
"Local data is shared with Claude Code under ~/.reflexio/ and ~/.claude-smart/.",
|
|
549
1054
|
"",
|
|
550
1055
|
].join("\n"),
|
|
@@ -570,7 +1075,7 @@ async function runUninstallCodex() {
|
|
|
570
1075
|
[
|
|
571
1076
|
"",
|
|
572
1077
|
"claude-smart Codex plugin and marketplace state removed. Restart Codex to apply.",
|
|
573
|
-
"Codex's global
|
|
1078
|
+
"Codex's global hook feature flags and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
|
|
574
1079
|
"",
|
|
575
1080
|
].join("\n"),
|
|
576
1081
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-smart",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Yi Lu"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"playbook",
|
|
18
18
|
"learning"
|
|
19
19
|
],
|
|
20
|
+
"skills": "./skills/",
|
|
20
21
|
"hooks": "./hooks/codex-hooks.json",
|
|
21
22
|
"interface": {
|
|
22
23
|
"displayName": "claude-smart",
|