claude-smart 0.2.26 → 0.2.28
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 +1 -1
- package/bin/claude-smart.js +250 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -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 +50 -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/src/claude_smart/cli.py +6 -1
- 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/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
|
|
14
14
|
</a>
|
|
15
15
|
<a href="plugin/pyproject.toml">
|
|
16
|
-
<img src="https://img.shields.io/badge/version-0.2.
|
|
16
|
+
<img src="https://img.shields.io/badge/version-0.2.28-green.svg" alt="Version">
|
|
17
17
|
</a>
|
|
18
18
|
<a href="plugin/pyproject.toml">
|
|
19
19
|
<img src="https://img.shields.io/badge/python-%3E%3D3.12-brightgreen.svg" alt="Python">
|
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
|
[
|
|
@@ -760,6 +1007,7 @@ async function runInstallCodex() {
|
|
|
760
1007
|
try {
|
|
761
1008
|
cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
|
|
762
1009
|
process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
|
|
1010
|
+
await bootstrapWindowsCodexCache(cacheDir);
|
|
763
1011
|
} catch (err) {
|
|
764
1012
|
process.stderr.write(
|
|
765
1013
|
`error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
|
package/package.json
CHANGED
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
10
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/ensure-plugin-root.sh\" \"$_R\" || true",
|
|
11
11
|
"timeout": 10
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"type": "command",
|
|
15
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
15
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/backend-service.sh\" start",
|
|
16
16
|
"timeout": 30
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
20
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/dashboard-service.sh\" start",
|
|
21
21
|
"timeout": 10
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"type": "command",
|
|
25
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
25
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex session-start",
|
|
26
26
|
"timeout": 30,
|
|
27
27
|
"statusMessage": "Loading claude-smart context"
|
|
28
28
|
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"hooks": [
|
|
35
35
|
{
|
|
36
36
|
"type": "command",
|
|
37
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
37
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex user-prompt",
|
|
38
38
|
"timeout": 15
|
|
39
39
|
}
|
|
40
40
|
]
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"hooks": [
|
|
47
47
|
{
|
|
48
48
|
"type": "command",
|
|
49
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
49
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex post-tool",
|
|
50
50
|
"timeout": 15
|
|
51
51
|
}
|
|
52
52
|
]
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"hooks": [
|
|
58
58
|
{
|
|
59
59
|
"type": "command",
|
|
60
|
-
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME
|
|
60
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex stop",
|
|
61
61
|
"timeout": 30
|
|
62
62
|
}
|
|
63
63
|
]
|
package/plugin/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-smart"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.28"
|
|
4
4
|
description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
# Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
|
|
11
11
|
# downloaded on first use, not at install time.
|
|
12
12
|
"chromadb>=0.5",
|
|
13
|
+
"einops>=0.8.0",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[project.scripts]
|
|
@@ -29,25 +29,29 @@ PORT=8071
|
|
|
29
29
|
# binds to PORT instead of reflexio's library default (8081).
|
|
30
30
|
export BACKEND_PORT="$PORT"
|
|
31
31
|
|
|
32
|
-
# Default: route extraction through the
|
|
32
|
+
# Default: route extraction through the active host CLI + ONNX embedder
|
|
33
33
|
# so claude-smart works without any LLM API key. Users can opt out by
|
|
34
34
|
# pre-exporting these to 0.
|
|
35
35
|
export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
|
|
36
36
|
export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
|
|
37
|
-
# The backend can be spawned from contexts whose PATH lacks the
|
|
37
|
+
# The backend can be spawned from contexts whose PATH lacks the host
|
|
38
38
|
# CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
|
|
39
39
|
# explicitly if we can resolve it from our own (post-login-path) PATH.
|
|
40
|
+
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
41
|
+
|
|
40
42
|
if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
+
if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
|
|
44
|
+
# Prefer the compatibility executable for older provider entrypoints; the
|
|
45
|
+
# provider can also resolve `codex` directly when this override is absent.
|
|
46
|
+
export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat.py"
|
|
47
|
+
elif _cs_cli_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_cli_path" ]; then
|
|
48
|
+
export CLAUDE_SMART_CLI_PATH="$_cs_cli_path"
|
|
43
49
|
elif [ -x "$HOME/.local/bin/claude" ]; then
|
|
44
50
|
export CLAUDE_SMART_CLI_PATH="$HOME/.local/bin/claude"
|
|
45
51
|
fi
|
|
46
|
-
unset
|
|
52
|
+
unset _cs_cli_path
|
|
47
53
|
fi
|
|
48
54
|
|
|
49
|
-
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
50
|
-
|
|
51
55
|
STATE_DIR="$HOME/.claude-smart"
|
|
52
56
|
PID_FILE="$STATE_DIR/backend.pid"
|
|
53
57
|
LOG_FILE="$STATE_DIR/backend.log"
|
|
@@ -55,6 +59,37 @@ mkdir -p "$STATE_DIR"
|
|
|
55
59
|
|
|
56
60
|
emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
|
|
57
61
|
|
|
62
|
+
emit_start_failure() {
|
|
63
|
+
reason="$1"
|
|
64
|
+
if py=$(claude_smart_resolve_python 2>/dev/null); then
|
|
65
|
+
"$py" - "$reason" <<'PY'
|
|
66
|
+
import json
|
|
67
|
+
import sys
|
|
68
|
+
|
|
69
|
+
reason = sys.argv[1].strip()
|
|
70
|
+
message = (
|
|
71
|
+
"> **claude-smart learning backend is not running.** "
|
|
72
|
+
"Interactions are being buffered locally, but learning will not publish "
|
|
73
|
+
"until the backend starts.\n"
|
|
74
|
+
)
|
|
75
|
+
if reason:
|
|
76
|
+
message += f">\n> Last startup error: `{reason}`\n"
|
|
77
|
+
message += (
|
|
78
|
+
">\n> Make sure the local model provider is available: Claude Code needs "
|
|
79
|
+
"`claude`, Codex needs `codex`. Then run `/claude-smart:restart`."
|
|
80
|
+
)
|
|
81
|
+
print(json.dumps({
|
|
82
|
+
"hookSpecificOutput": {
|
|
83
|
+
"hookEventName": "SessionStart",
|
|
84
|
+
"additionalContext": message,
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
PY
|
|
88
|
+
else
|
|
89
|
+
emit_ok
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
# Tree-kill the recorded process. Delegates to claude_smart_kill_tree
|
|
59
94
|
# (POSIX: signal the process group; Windows: taskkill /T /F /PID).
|
|
60
95
|
kill_group() {
|
|
@@ -184,6 +219,14 @@ case "$CMD" in
|
|
|
184
219
|
backend_healthy && break
|
|
185
220
|
sleep 1
|
|
186
221
|
done
|
|
222
|
+
if ! backend_healthy; then
|
|
223
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
|
224
|
+
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
|
|
225
|
+
reason=$(tail -n 120 "$LOG_FILE" 2>/dev/null | grep -E "No LLM provider available|No generation-capable LLM provider available|CLI not found|skipping provider registration|Application startup failed" | tail -n 1 | sed 's/^[[:space:]]*//')
|
|
226
|
+
emit_start_failure "$reason"
|
|
227
|
+
exit 0
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
187
230
|
emit_ok
|
|
188
231
|
;;
|
|
189
232
|
stop)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Translate Reflexio's Claude CLI provider contract to ``codex exec``.
|
|
3
|
+
|
|
4
|
+
Reflexio's local provider currently shells out to ``CLAUDE_SMART_CLI_PATH`` as
|
|
5
|
+
if it were the Claude Code CLI:
|
|
6
|
+
|
|
7
|
+
<path> -p --output-format json --model <model> [--append-system-prompt ...]
|
|
8
|
+
|
|
9
|
+
When claude-smart runs under Codex, this executable preserves that narrow
|
|
10
|
+
contract while routing the actual model call through ``codex exec``. The
|
|
11
|
+
stdout shape intentionally matches Claude Code's JSON output enough for
|
|
12
|
+
Reflexio's provider to read ``result``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_TIMEOUT_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv: list[str] | None = None) -> int:
|
|
30
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
31
|
+
try:
|
|
32
|
+
output_format, system_prompt = _parse_supported_args(argv)
|
|
33
|
+
content = _run_codex(
|
|
34
|
+
prompt=sys.stdin.read(),
|
|
35
|
+
system_prompt=system_prompt,
|
|
36
|
+
)
|
|
37
|
+
except Exception as exc: # noqa: BLE001 - CLI bridge errors go to stderr.
|
|
38
|
+
print(f"codex-claude-compat: {exc}", file=sys.stderr)
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
payload = {"result": content}
|
|
42
|
+
if output_format == "stream-json":
|
|
43
|
+
payload = {"type": "result", "subtype": "success", "result": content}
|
|
44
|
+
json.dump(payload, sys.stdout, ensure_ascii=False)
|
|
45
|
+
sys.stdout.write("\n")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_supported_args(argv: list[str]) -> tuple[str, str]:
|
|
50
|
+
output_format = "json"
|
|
51
|
+
system_prompt = ""
|
|
52
|
+
idx = 0
|
|
53
|
+
while idx < len(argv):
|
|
54
|
+
arg = argv[idx]
|
|
55
|
+
if arg == "-p":
|
|
56
|
+
idx += 1
|
|
57
|
+
elif arg == "--output-format":
|
|
58
|
+
if idx + 1 >= len(argv):
|
|
59
|
+
raise ValueError("--output-format requires a value")
|
|
60
|
+
output_format = argv[idx + 1]
|
|
61
|
+
idx += 2
|
|
62
|
+
elif arg == "--model":
|
|
63
|
+
idx += 2
|
|
64
|
+
elif arg in {"--verbose", "--include-partial-messages"}:
|
|
65
|
+
idx += 1
|
|
66
|
+
elif arg == "--append-system-prompt":
|
|
67
|
+
if idx + 1 >= len(argv):
|
|
68
|
+
raise ValueError("--append-system-prompt requires a value")
|
|
69
|
+
system_prompt = argv[idx + 1]
|
|
70
|
+
idx += 2
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"unsupported Claude CLI argument: {arg}")
|
|
73
|
+
if output_format not in {"json", "stream-json"}:
|
|
74
|
+
raise ValueError(f"unsupported --output-format: {output_format}")
|
|
75
|
+
return output_format, system_prompt
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_codex(*, prompt: str, system_prompt: str) -> str:
|
|
79
|
+
codex_path = os.environ.get("CLAUDE_SMART_CODEX_PATH") or shutil.which("codex")
|
|
80
|
+
if not codex_path:
|
|
81
|
+
raise FileNotFoundError("codex CLI not found on PATH")
|
|
82
|
+
|
|
83
|
+
output_path = _temporary_output_path()
|
|
84
|
+
cmd = [
|
|
85
|
+
codex_path,
|
|
86
|
+
"exec",
|
|
87
|
+
"--sandbox",
|
|
88
|
+
"read-only",
|
|
89
|
+
"--skip-git-repo-check",
|
|
90
|
+
"--ephemeral",
|
|
91
|
+
"--ignore-rules",
|
|
92
|
+
"--output-last-message",
|
|
93
|
+
str(output_path),
|
|
94
|
+
"-",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
env = os.environ.copy()
|
|
98
|
+
env["CLAUDE_SMART_HOST"] = "codex"
|
|
99
|
+
env["CLAUDE_SMART_INTERNAL"] = "1"
|
|
100
|
+
env["CLAUDE_CODE_ENTRYPOINT"] = "optimizer"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
proc = subprocess.run( # noqa: S603 - fixed command plus resolved executable.
|
|
104
|
+
cmd,
|
|
105
|
+
input=_codex_prompt(prompt=prompt, system_prompt=system_prompt),
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=_TIMEOUT_SECONDS,
|
|
109
|
+
check=False,
|
|
110
|
+
env=env,
|
|
111
|
+
)
|
|
112
|
+
if proc.returncode != 0:
|
|
113
|
+
stderr = proc.stderr.strip()
|
|
114
|
+
raise RuntimeError(f"codex CLI exited {proc.returncode}: {stderr[:500]}")
|
|
115
|
+
content = output_path.read_text(encoding="utf-8").strip()
|
|
116
|
+
except subprocess.TimeoutExpired as exc:
|
|
117
|
+
raise TimeoutError(f"codex CLI timed out after {_TIMEOUT_SECONDS}s") from exc
|
|
118
|
+
finally:
|
|
119
|
+
try:
|
|
120
|
+
output_path.unlink()
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
if not content:
|
|
125
|
+
raise RuntimeError("codex CLI returned empty output")
|
|
126
|
+
return content
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _temporary_output_path() -> Path:
|
|
130
|
+
handle = tempfile.NamedTemporaryFile(prefix="claude-smart-codex-", delete=False)
|
|
131
|
+
try:
|
|
132
|
+
return Path(handle.name)
|
|
133
|
+
finally:
|
|
134
|
+
handle.close()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
|
|
138
|
+
if not system_prompt:
|
|
139
|
+
return prompt
|
|
140
|
+
return f"{system_prompt}\n\n## Task\n{prompt}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
raise SystemExit(main())
|