claude-smart 0.2.28 → 0.2.29
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 +22 -1
- package/bin/claude-smart.js +333 -73
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +4 -0
- package/plugin/hooks/codex-hooks.json +5 -0
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +38 -0
- package/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/scripts/backend-service.sh +15 -11
- package/plugin/scripts/cli.sh +27 -3
- package/plugin/scripts/codex-claude-compat +9 -0
- package/plugin/scripts/codex-claude-compat.cmd +4 -0
- package/plugin/scripts/codex-claude-compat.js +162 -0
- package/plugin/scripts/codex-hook.js +30 -2
- package/plugin/scripts/smart-install.sh +136 -50
- package/plugin/src/claude_smart/cli.py +101 -2
- package/plugin/src/claude_smart/context_inject.py +2 -4
- package/plugin/src/claude_smart/cs_cite.py +2 -90
- package/plugin/src/claude_smart/events/stop.py +16 -42
- package/plugin/src/claude_smart/internal_call.py +23 -0
- package/plugin/src/claude_smart/state.py +3 -3
- package/plugin/uv.lock +73 -76
- package/plugin/bin/cs-cite +0 -77
- package/plugin/scripts/codex-claude-compat.py +0 -144
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.29-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">
|
|
@@ -98,6 +98,8 @@ claude plugin install claude-smart@reflexioai
|
|
|
98
98
|
The plugin Setup hook installs its own `uv`, Python 3.12 environment, and
|
|
99
99
|
private Node.js/npm runtime under `~/.claude-smart/` when they are missing, so
|
|
100
100
|
you do not need to install Python or Node globally for the plugin/dashboard.
|
|
101
|
+
On native Windows, Claude Code hooks still need a Git Bash-compatible `bash`
|
|
102
|
+
until Claude Code exposes a single cross-platform hook command shape.
|
|
101
103
|
|
|
102
104
|
To uninstall:
|
|
103
105
|
|
|
@@ -121,6 +123,11 @@ Then fully quit and reopen Codex so hooks reload.
|
|
|
121
123
|
|
|
122
124
|
Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
|
|
123
125
|
|
|
126
|
+
The `npx` command needs Node.js only to launch the installer. The installed
|
|
127
|
+
plugin uses a private Node.js/npm runtime and a `uv`-managed Python 3.12
|
|
128
|
+
environment under `~/.claude-smart/`, so hooks and the dashboard do not depend
|
|
129
|
+
on global Python, uv, Node, or npm after install.
|
|
130
|
+
|
|
124
131
|
To uninstall:
|
|
125
132
|
|
|
126
133
|
```bash
|
|
@@ -133,6 +140,19 @@ Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-local
|
|
|
133
140
|
|
|
134
141
|
> **Not supported:** Claude Code Cowork, claude.ai/code web, or remote Codex environments without local plugin hooks — they run outside your local machine, so the local backend/dashboard and `~/.reflexio/` aren't reachable.
|
|
135
142
|
|
|
143
|
+
### Vanilla OS support
|
|
144
|
+
|
|
145
|
+
| Platform | Status | Notes |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| Apple Silicon macOS 14+ | Supported | Runtime bootstrap installs private Node/npm, uv, Python 3.12 deps, and dashboard deps. |
|
|
148
|
+
| Windows x64 | Supported | Runtime bootstrap uses PowerShell for uv/Node archive extraction and patches Codex hooks to the Node wrapper. |
|
|
149
|
+
| Linux | Supported when host hooks are local | Existing Linux behavior is preserved; install coverage depends on available Python wheels. |
|
|
150
|
+
| Intel Mac, macOS 13 or older, Windows ARM | Not supported | Current local embedding/ML dependencies do not publish a complete native wheel set for these targets. |
|
|
151
|
+
|
|
152
|
+
Network access is required during first install for npm, PyPI/uv, Node.js, and
|
|
153
|
+
the first local embedding model download. The ONNX model cache lives at
|
|
154
|
+
`~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/`.
|
|
155
|
+
|
|
136
156
|
---
|
|
137
157
|
|
|
138
158
|
## Key Features
|
|
@@ -216,6 +236,7 @@ Advanced users can tune claude-smart via environment variables — see [DEVELOPE
|
|
|
216
236
|
| `~/.codex/plugins/cache/reflexioai/claude-smart/<version>/` | Codex's cached install of the `claude-smart` plugin from the `ReflexioAI` marketplace. |
|
|
217
237
|
| `~/.reflexio/plugin-root` | Self-healed symlink to the active plugin dir (managed by `ensure-plugin-root.sh` — written on install, refreshed each `SessionStart`). Claude Code slash commands and Codex shell-command helpers resolve through it, so don't delete it; if you do, the next session will recreate it. |
|
|
218
238
|
| `~/.claude-smart/sessions/{session_id}.jsonl` | Per-session buffer. User turns, assistant turns, tool invocations, `{"published_up_to": N}` watermarks. Safe to inspect and safe to delete — everything past the latest watermark has already been written to reflexio's DB. |
|
|
239
|
+
| `~/.claude-smart/node/current/` | Private Node.js/npm runtime used by hooks and the dashboard after install. |
|
|
219
240
|
| `~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/` | Cached ONNX weights (~86 MB, downloaded once). Delete to force a re-download. |
|
|
220
241
|
|
|
221
242
|
For troubleshooting, see [TROUBLESHOOTING.md](./TROUBLESHOOTING.md).
|
package/bin/claude-smart.js
CHANGED
|
@@ -17,13 +17,18 @@ const {
|
|
|
17
17
|
appendFileSync,
|
|
18
18
|
cpSync,
|
|
19
19
|
existsSync,
|
|
20
|
+
lstatSync,
|
|
20
21
|
mkdirSync,
|
|
21
22
|
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
renameSync,
|
|
22
25
|
rmSync,
|
|
26
|
+
statSync,
|
|
27
|
+
symlinkSync,
|
|
23
28
|
writeFileSync,
|
|
24
29
|
} = require("fs");
|
|
25
30
|
const https = require("https");
|
|
26
|
-
const { homedir, tmpdir } = require("os");
|
|
31
|
+
const { arch, homedir, platform, release, tmpdir } = require("os");
|
|
27
32
|
const { dirname, join } = require("path");
|
|
28
33
|
|
|
29
34
|
const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
|
|
@@ -32,6 +37,8 @@ const CODEX_MARKETPLACE_NAME = "reflexioai";
|
|
|
32
37
|
const CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI";
|
|
33
38
|
const CODEX_PLUGIN_ID = `claude-smart@${CODEX_MARKETPLACE_NAME}`;
|
|
34
39
|
const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
|
|
40
|
+
const REFLEXIO_DIR = join(homedir(), ".reflexio");
|
|
41
|
+
const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
|
|
35
42
|
const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml");
|
|
36
43
|
const PACKAGE_ROOT = dirname(dirname(__filename));
|
|
37
44
|
const CODEX_MARKETPLACE_DIR = join(
|
|
@@ -54,7 +61,9 @@ const CODEX_REQUIRED_FILES = [
|
|
|
54
61
|
".agents/plugins/marketplace.json",
|
|
55
62
|
"plugin/.codex-plugin/plugin.json",
|
|
56
63
|
"plugin/hooks/codex-hooks.json",
|
|
57
|
-
"plugin/scripts/codex-claude-compat
|
|
64
|
+
"plugin/scripts/codex-claude-compat",
|
|
65
|
+
"plugin/scripts/codex-claude-compat.cmd",
|
|
66
|
+
"plugin/scripts/codex-claude-compat.js",
|
|
58
67
|
"plugin/scripts/codex-hook.js",
|
|
59
68
|
"plugin/scripts/_codex_env.sh",
|
|
60
69
|
];
|
|
@@ -191,8 +200,132 @@ function seedReflexioEnv() {
|
|
|
191
200
|
return missing;
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
function findClaudeCodePluginRoot() {
|
|
204
|
+
const cacheRoot = join(homedir(), ".claude", "plugins", "cache", CODEX_MARKETPLACE_NAME, "claude-smart");
|
|
205
|
+
const candidates = [];
|
|
206
|
+
try {
|
|
207
|
+
for (const entry of readdirSync(cacheRoot, { withFileTypes: true })) {
|
|
208
|
+
if (!entry.isDirectory()) continue;
|
|
209
|
+
const candidate = join(cacheRoot, entry.name);
|
|
210
|
+
if (
|
|
211
|
+
existsSync(join(candidate, "pyproject.toml")) &&
|
|
212
|
+
existsSync(join(candidate, "scripts", "smart-install.sh"))
|
|
213
|
+
) {
|
|
214
|
+
candidates.push(candidate);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Fall through to marketplace/package fallbacks.
|
|
219
|
+
}
|
|
220
|
+
candidates.sort((a, b) => {
|
|
221
|
+
try {
|
|
222
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
223
|
+
} catch {
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const fallbacks = [
|
|
228
|
+
join(homedir(), ".claude", "plugins", "marketplaces", CODEX_MARKETPLACE_NAME, "plugin"),
|
|
229
|
+
join(PACKAGE_ROOT, "plugin"),
|
|
230
|
+
];
|
|
231
|
+
for (const candidate of [...candidates, ...fallbacks]) {
|
|
232
|
+
if (
|
|
233
|
+
existsSync(join(candidate, "pyproject.toml")) &&
|
|
234
|
+
existsSync(join(candidate, "scripts", "smart-install.sh"))
|
|
235
|
+
) {
|
|
236
|
+
return candidate;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function forcePluginRoot(pluginRoot) {
|
|
243
|
+
mkdirSync(REFLEXIO_DIR, { recursive: true });
|
|
244
|
+
const link = join(REFLEXIO_DIR, "plugin-root");
|
|
245
|
+
try {
|
|
246
|
+
const existing = lstatSync(link);
|
|
247
|
+
if (existing.isSymbolicLink() || existing.isFile()) {
|
|
248
|
+
rmSync(link, { force: true });
|
|
249
|
+
} else {
|
|
250
|
+
throw new Error(`refusing to replace non-symlink plugin-root at ${link}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err && err.code !== "ENOENT") throw err;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
// Use a symlink when possible so slash commands follow the active plugin root.
|
|
257
|
+
symlinkSync(pluginRoot, link, isWindows() ? "junction" : "dir");
|
|
258
|
+
} catch {
|
|
259
|
+
writeFileSync(join(REFLEXIO_DIR, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function bootstrapClaudeCodeInstall() {
|
|
264
|
+
const pluginRoot = findClaudeCodePluginRoot();
|
|
265
|
+
if (!pluginRoot) {
|
|
266
|
+
throw new Error("could not locate installed Claude Code plugin root after install");
|
|
267
|
+
}
|
|
268
|
+
forcePluginRoot(pluginRoot);
|
|
269
|
+
const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
|
|
270
|
+
if (!bash) {
|
|
271
|
+
throw new Error("bash is required to bootstrap claude-smart dependencies");
|
|
272
|
+
}
|
|
273
|
+
const code = await runChecked(bash, [join(pluginRoot, "scripts", "smart-install.sh")], {
|
|
274
|
+
cwd: pluginRoot,
|
|
275
|
+
});
|
|
276
|
+
if (code !== 0) {
|
|
277
|
+
throw new Error(`smart-install.sh failed in ${pluginRoot}`);
|
|
278
|
+
}
|
|
279
|
+
const failureMarker = join(CLAUDE_SMART_STATE_DIR, "install-failed");
|
|
280
|
+
if (existsSync(failureMarker)) {
|
|
281
|
+
const reason = readFileSync(failureMarker, "utf8").trim() || "unknown error";
|
|
282
|
+
throw new Error(reason);
|
|
283
|
+
}
|
|
284
|
+
return pluginRoot;
|
|
285
|
+
}
|
|
286
|
+
|
|
194
287
|
function isWindows() {
|
|
195
|
-
return
|
|
288
|
+
return currentPlatform() === "win32";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function currentPlatform() {
|
|
292
|
+
return process.env.CLAUDE_SMART_TEST_PLATFORM || platform();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function currentArch() {
|
|
296
|
+
return process.env.CLAUDE_SMART_TEST_ARCH || arch();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function currentRelease() {
|
|
300
|
+
return process.env.CLAUDE_SMART_TEST_RELEASE || release();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function platformSupportError() {
|
|
304
|
+
const os = currentPlatform();
|
|
305
|
+
const cpu = currentArch();
|
|
306
|
+
if (os === "darwin") {
|
|
307
|
+
if (cpu !== "arm64") {
|
|
308
|
+
return "claude-smart currently supports Apple Silicon macOS 14+ only; Intel Mac is not supported because native ML wheels are unavailable.";
|
|
309
|
+
}
|
|
310
|
+
const darwinMajor = Number.parseInt(currentRelease().split(".")[0] || "0", 10);
|
|
311
|
+
if (!Number.isFinite(darwinMajor) || darwinMajor < 23) {
|
|
312
|
+
return "claude-smart currently supports macOS 14+ on Apple Silicon; macOS 13 and older are not supported because native ML wheels are unavailable.";
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (os === "win32") {
|
|
317
|
+
if (cpu !== "x64") {
|
|
318
|
+
return "claude-smart currently supports Windows x64 only; Windows ARM is not supported because native ML wheels are unavailable.";
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
if (os === "linux") return null;
|
|
323
|
+
return "claude-smart currently supports Apple Silicon macOS 14+, Windows x64, and Linux for vanilla installs.";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function assertSupportedRuntimePlatform() {
|
|
327
|
+
const message = platformSupportError();
|
|
328
|
+
if (message) throw new Error(message);
|
|
196
329
|
}
|
|
197
330
|
|
|
198
331
|
function runChecked(command, args, options = {}) {
|
|
@@ -243,7 +376,7 @@ function downloadFile(url, dest) {
|
|
|
243
376
|
function resolveCommand(names, extraDirs = []) {
|
|
244
377
|
const pathParts = [
|
|
245
378
|
...extraDirs,
|
|
246
|
-
...(process.env.PATH || "").split(
|
|
379
|
+
...(process.env.PATH || "").split(isWindows() ? ";" : ":"),
|
|
247
380
|
].filter(Boolean);
|
|
248
381
|
for (const dir of pathParts) {
|
|
249
382
|
for (const name of names) {
|
|
@@ -263,19 +396,26 @@ function privateNodeBinDirs() {
|
|
|
263
396
|
return [join(root, "bin"), root];
|
|
264
397
|
}
|
|
265
398
|
|
|
399
|
+
function resolvePrivateCommand(names) {
|
|
400
|
+
for (const dir of privateNodeBinDirs()) {
|
|
401
|
+
for (const name of names) {
|
|
402
|
+
const candidate = join(dir, name);
|
|
403
|
+
if (existsSync(candidate)) return candidate;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
266
409
|
function resolvePrivateNode() {
|
|
267
|
-
return
|
|
410
|
+
return resolvePrivateCommand(isWindows() ? ["node.exe", "node"] : ["node"]);
|
|
268
411
|
}
|
|
269
412
|
|
|
270
413
|
function resolvePrivateNpm() {
|
|
271
|
-
return
|
|
272
|
-
isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"],
|
|
273
|
-
privateNodeBinDirs(),
|
|
274
|
-
);
|
|
414
|
+
return resolvePrivateCommand(isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
|
|
275
415
|
}
|
|
276
416
|
|
|
277
417
|
function runtimeEnv(extraDirs = []) {
|
|
278
|
-
const delimiter =
|
|
418
|
+
const delimiter = isWindows() ? ";" : ":";
|
|
279
419
|
const dirs = [
|
|
280
420
|
...extraDirs,
|
|
281
421
|
...privateNodeBinDirs(),
|
|
@@ -288,14 +428,35 @@ function runtimeEnv(extraDirs = []) {
|
|
|
288
428
|
};
|
|
289
429
|
}
|
|
290
430
|
|
|
291
|
-
|
|
292
|
-
|
|
431
|
+
function nodeArchiveSpec() {
|
|
432
|
+
const os = currentPlatform();
|
|
433
|
+
const cpu = currentArch();
|
|
434
|
+
let nodeOs = null;
|
|
435
|
+
let archiveExt = null;
|
|
436
|
+
if (os === "darwin") {
|
|
437
|
+
nodeOs = "darwin";
|
|
438
|
+
archiveExt = "tar.gz";
|
|
439
|
+
} else if (os === "win32") {
|
|
440
|
+
nodeOs = "win";
|
|
441
|
+
archiveExt = "zip";
|
|
442
|
+
} else if (os === "linux") {
|
|
443
|
+
nodeOs = "linux";
|
|
444
|
+
archiveExt = "tar.gz";
|
|
445
|
+
} else {
|
|
446
|
+
throw new Error(`unsupported OS for private Node.js install: ${os}`);
|
|
447
|
+
}
|
|
448
|
+
const nodeArch = cpu === "arm64" ? "arm64" : "x64";
|
|
449
|
+
return { nodeOs, nodeArch, archiveExt };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function ensurePrivateNode() {
|
|
293
453
|
const existing = resolvePrivateNode();
|
|
294
454
|
const existingNpm = resolvePrivateNpm();
|
|
295
455
|
if (existing && existingNpm) return { node: existing, npm: existingNpm };
|
|
296
456
|
|
|
457
|
+
assertSupportedRuntimePlatform();
|
|
297
458
|
const major = process.env.CLAUDE_SMART_NODE_LTS_MAJOR || "22";
|
|
298
|
-
const
|
|
459
|
+
const { nodeOs, nodeArch, archiveExt } = nodeArchiveSpec();
|
|
299
460
|
const baseUrl = process.env.CLAUDE_SMART_NODE_BASE_URL || `https://nodejs.org/dist/latest-v${major}.x`;
|
|
300
461
|
const nodeRoot = join(homedir(), ".claude-smart", "node");
|
|
301
462
|
const temp = join(tmpdir(), `claude-smart-node-${process.pid}`);
|
|
@@ -309,8 +470,8 @@ async function ensureWindowsPrivateNode() {
|
|
|
309
470
|
const match = sums
|
|
310
471
|
.split(/\r?\n/)
|
|
311
472
|
.map((line) => line.trim().split(/\s+/))
|
|
312
|
-
.find((parts) => parts[1] && new RegExp(`^node-v[^ ]
|
|
313
|
-
if (!match) throw new Error(`could not resolve Node.js
|
|
473
|
+
.find((parts) => parts[1] && new RegExp(`^node-v[^ ]+-${nodeOs}-${nodeArch}\\.${archiveExt.replace(/\./g, "\\.")}$`).test(parts[1]));
|
|
474
|
+
if (!match) throw new Error(`could not resolve Node.js ${nodeOs}-${nodeArch} archive from ${baseUrl}`);
|
|
314
475
|
const [expectedHash, archiveName] = match;
|
|
315
476
|
const archivePath = join(temp, archiveName);
|
|
316
477
|
await downloadFile(`${baseUrl}/${archiveName}`, archivePath);
|
|
@@ -319,26 +480,56 @@ async function ensureWindowsPrivateNode() {
|
|
|
319
480
|
throw new Error(`Node.js checksum verification failed for ${archiveName}`);
|
|
320
481
|
}
|
|
321
482
|
|
|
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
483
|
const extractDir = join(temp, "extract");
|
|
325
484
|
mkdirSync(extractDir, { recursive: true });
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
[
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
485
|
+
let code = 0;
|
|
486
|
+
if (archiveExt === "zip") {
|
|
487
|
+
const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
|
|
488
|
+
if (!powershell) throw new Error("PowerShell is required to extract private Node.js on Windows");
|
|
489
|
+
code = await runChecked(
|
|
490
|
+
powershell,
|
|
491
|
+
[
|
|
492
|
+
"-NoProfile",
|
|
493
|
+
"-ExecutionPolicy",
|
|
494
|
+
"Bypass",
|
|
495
|
+
"-Command",
|
|
496
|
+
"$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force",
|
|
497
|
+
],
|
|
498
|
+
{ env: { ...process.env, ARCHIVE_PATH: archivePath, DEST_DIR: extractDir } },
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
const tar = resolveCommand(["tar"]);
|
|
502
|
+
if (!tar) throw new Error("tar is required to extract private Node.js on macOS");
|
|
503
|
+
code = await runChecked(tar, ["-xzf", archivePath, "-C", extractDir]);
|
|
504
|
+
}
|
|
337
505
|
if (code !== 0) throw new Error(`Node.js archive extraction failed for ${archiveName}`);
|
|
338
|
-
const extracted = join(extractDir, archiveName.replace(/\.zip$/, ""));
|
|
506
|
+
const extracted = join(extractDir, archiveName.replace(/\.zip$/, "").replace(/\.tar\.gz$/, ""));
|
|
339
507
|
const current = privateNodeRoot();
|
|
340
|
-
|
|
341
|
-
|
|
508
|
+
// Atomic swap with rollback: move existing `current` to a backup first
|
|
509
|
+
// so a non-EXDEV failure (EACCES, EBUSY) does not leave the user with no
|
|
510
|
+
// private node at all. EXDEV (cross-device) falls back to cpSync.
|
|
511
|
+
const backup = `${current}.prev.${process.pid}`;
|
|
512
|
+
rmSync(backup, { recursive: true, force: true });
|
|
513
|
+
const hadCurrent = existsSync(current);
|
|
514
|
+
if (hadCurrent) renameSync(current, backup);
|
|
515
|
+
try {
|
|
516
|
+
try {
|
|
517
|
+
renameSync(extracted, current);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (!err || err.code !== "EXDEV") throw err;
|
|
520
|
+
cpSync(extracted, current, {
|
|
521
|
+
recursive: true,
|
|
522
|
+
force: true,
|
|
523
|
+
verbatimSymlinks: true,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
if (hadCurrent) {
|
|
528
|
+
try { renameSync(backup, current); } catch { /* leave backup for manual recovery */ }
|
|
529
|
+
}
|
|
530
|
+
throw err;
|
|
531
|
+
}
|
|
532
|
+
rmSync(backup, { recursive: true, force: true });
|
|
342
533
|
rmSync(temp, { recursive: true, force: true });
|
|
343
534
|
|
|
344
535
|
const node = resolvePrivateNode();
|
|
@@ -354,21 +545,33 @@ function resolveUv() {
|
|
|
354
545
|
]);
|
|
355
546
|
}
|
|
356
547
|
|
|
357
|
-
async function
|
|
548
|
+
async function ensureUv() {
|
|
358
549
|
let uv = resolveUv();
|
|
359
550
|
if (uv) return uv;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
551
|
+
assertSupportedRuntimePlatform();
|
|
552
|
+
let code = 0;
|
|
553
|
+
if (isWindows()) {
|
|
554
|
+
const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
|
|
555
|
+
if (!powershell) throw new Error("PowerShell is required to install uv on Windows");
|
|
556
|
+
code = await runChecked(powershell, [
|
|
557
|
+
"-NoProfile",
|
|
558
|
+
"-ExecutionPolicy",
|
|
559
|
+
"Bypass",
|
|
560
|
+
"-Command",
|
|
561
|
+
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
562
|
+
]);
|
|
563
|
+
if (code !== 0) throw new Error("uv install via PowerShell failed");
|
|
564
|
+
} else {
|
|
565
|
+
const installer = join(homedir(), ".claude-smart", "uv-install.sh");
|
|
566
|
+
mkdirSync(dirname(installer), { recursive: true });
|
|
567
|
+
await downloadFile("https://astral.sh/uv/install.sh", installer);
|
|
568
|
+
const sh = resolveCommand(["sh"]);
|
|
569
|
+
if (!sh) throw new Error("sh is required to install uv on macOS");
|
|
570
|
+
code = await runChecked(sh, [installer]);
|
|
571
|
+
if (code !== 0) throw new Error("uv install failed");
|
|
572
|
+
}
|
|
370
573
|
uv = resolveUv();
|
|
371
|
-
if (!uv) throw new Error("uv install reported success but uv
|
|
574
|
+
if (!uv) throw new Error("uv install reported success but uv was not found");
|
|
372
575
|
return uv;
|
|
373
576
|
}
|
|
374
577
|
|
|
@@ -376,50 +579,83 @@ function quoteCommandPart(part) {
|
|
|
376
579
|
return `"${String(part).replace(/"/g, '\\"')}"`;
|
|
377
580
|
}
|
|
378
581
|
|
|
379
|
-
function
|
|
582
|
+
function patchCodexHooksForNode(pluginRoot, nodePath) {
|
|
380
583
|
const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
|
|
381
584
|
const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
|
|
382
585
|
const runner = join(pluginRoot, "scripts", "codex-hook.js");
|
|
383
586
|
const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
587
|
+
// Dispatch by command content rather than index — entries can be added or
|
|
588
|
+
// reordered (e.g. the SessionStart install hook at index 0) without
|
|
589
|
+
// breaking the patch. Entries that must run as bash (smart-install.sh)
|
|
590
|
+
// are left untouched.
|
|
591
|
+
const patchOne = (original) => {
|
|
592
|
+
if (typeof original !== "string") return original;
|
|
593
|
+
if (original.includes("smart-install.sh")) return original;
|
|
594
|
+
if (original.includes("ensure-plugin-root.sh")) return command("ensure-root");
|
|
595
|
+
if (original.includes("backend-service.sh")) return command("backend");
|
|
596
|
+
if (original.includes("dashboard-service.sh")) return command("dashboard");
|
|
597
|
+
// Match `hook_entry.sh" codex session-start` and similar — between
|
|
598
|
+
// the script name, the host token, and the subcommand there may be
|
|
599
|
+
// closing quotes plus whitespace, so allow both as separators.
|
|
600
|
+
const hookMatch = original.match(/hook_entry\.sh\b[\s"']+(?:codex|claude-code)[\s"']+([\w-]+)/);
|
|
601
|
+
if (hookMatch) return command("hook", hookMatch[1]);
|
|
602
|
+
return original;
|
|
603
|
+
};
|
|
604
|
+
for (const event of Object.keys(parsed.hooks || {})) {
|
|
605
|
+
for (const block of parsed.hooks[event] || []) {
|
|
606
|
+
for (const hook of block.hooks || []) {
|
|
607
|
+
hook.command = patchOne(hook.command);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
395
611
|
writeFileSync(hookPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
396
612
|
}
|
|
397
613
|
|
|
398
|
-
function
|
|
614
|
+
function ensurePluginRoot(pluginRoot) {
|
|
399
615
|
const reflexioDir = dirname(REFLEXIO_ENV_PATH);
|
|
400
616
|
const link = join(reflexioDir, "plugin-root");
|
|
401
617
|
mkdirSync(reflexioDir, { recursive: true });
|
|
402
618
|
rmSync(link, { recursive: true, force: true });
|
|
403
619
|
try {
|
|
404
|
-
require("fs").symlinkSync(pluginRoot, link, "junction");
|
|
620
|
+
require("fs").symlinkSync(pluginRoot, link, isWindows() ? "junction" : "dir");
|
|
405
621
|
} catch {
|
|
406
622
|
writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
407
623
|
}
|
|
408
624
|
}
|
|
409
625
|
|
|
410
|
-
async function
|
|
411
|
-
|
|
412
|
-
process.stdout.write("Preparing
|
|
413
|
-
const nodeRuntime = await
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const uv = await
|
|
626
|
+
async function bootstrapPluginRuntime(pluginRoot) {
|
|
627
|
+
assertSupportedRuntimePlatform();
|
|
628
|
+
process.stdout.write("Preparing claude-smart runtime for hooks...\n");
|
|
629
|
+
const nodeRuntime = await ensurePrivateNode();
|
|
630
|
+
patchCodexHooksForNode(pluginRoot, nodeRuntime.node);
|
|
631
|
+
ensurePluginRoot(pluginRoot);
|
|
632
|
+
const uv = await ensureUv();
|
|
417
633
|
const env = runtimeEnv([dirname(uv), ...privateNodeBinDirs()]);
|
|
634
|
+
const pyprojectPath = join(pluginRoot, "pyproject.toml");
|
|
635
|
+
const pyproject = existsSync(pyprojectPath) ? readFileSync(pyprojectPath, "utf8") : "";
|
|
636
|
+
if (/^\s*\[tool\.uv\.sources\]\s*$/m.test(pyproject)) {
|
|
637
|
+
const lockCode = await runChecked(
|
|
638
|
+
uv,
|
|
639
|
+
["lock", "--quiet"],
|
|
640
|
+
{ cwd: pluginRoot, env },
|
|
641
|
+
);
|
|
642
|
+
if (lockCode !== 0) throw new Error(`uv lock failed in ${pluginRoot}`);
|
|
643
|
+
}
|
|
418
644
|
let code = await runChecked(
|
|
419
645
|
uv,
|
|
420
646
|
["sync", "--locked", "--python", "3.12", "--quiet"],
|
|
421
647
|
{ cwd: pluginRoot, env },
|
|
422
648
|
);
|
|
649
|
+
if (code !== 0) {
|
|
650
|
+
process.stderr.write(
|
|
651
|
+
`warning: quiet uv sync failed in ${pluginRoot}; retrying with full output.\n`,
|
|
652
|
+
);
|
|
653
|
+
code = await runChecked(
|
|
654
|
+
uv,
|
|
655
|
+
["sync", "--locked", "--python", "3.12"],
|
|
656
|
+
{ cwd: pluginRoot, env },
|
|
657
|
+
);
|
|
658
|
+
}
|
|
423
659
|
if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
|
|
424
660
|
|
|
425
661
|
const dashboardDir = join(pluginRoot, "dashboard");
|
|
@@ -453,9 +689,10 @@ function printHelp() {
|
|
|
453
689
|
` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
|
|
454
690
|
" 2. codex plugin marketplace add <copied marketplace>",
|
|
455
691
|
" 3. codex features enable hooks && codex features enable plugin_hooks",
|
|
456
|
-
" 4. Installs
|
|
457
|
-
" 5.
|
|
458
|
-
" 6.
|
|
692
|
+
" 4. Installs private Node/npm, uv, Python deps, and dashboard deps as needed",
|
|
693
|
+
" 5. Installs claude-smart into Codex's plugin cache and enables it",
|
|
694
|
+
" 6. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
|
|
695
|
+
" 7. Restart Codex.",
|
|
459
696
|
"",
|
|
460
697
|
"Update:",
|
|
461
698
|
" npx claude-smart update Update to the latest version",
|
|
@@ -948,11 +1185,23 @@ async function runInstall(args) {
|
|
|
948
1185
|
`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`,
|
|
949
1186
|
);
|
|
950
1187
|
}
|
|
1188
|
+
try {
|
|
1189
|
+
const pluginRoot = await bootstrapClaudeCodeInstall();
|
|
1190
|
+
process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
process.stderr.write(
|
|
1193
|
+
`error: claude-smart installed, but dependency bootstrap failed: ${err && err.message ? err.message : err}\n`,
|
|
1194
|
+
);
|
|
1195
|
+
process.stderr.write(
|
|
1196
|
+
"Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n",
|
|
1197
|
+
);
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
951
1200
|
|
|
952
1201
|
process.stdout.write(
|
|
953
1202
|
[
|
|
954
1203
|
"",
|
|
955
|
-
"claude-smart installed. Restart Claude Code in your project.",
|
|
1204
|
+
"claude-smart installed and dependencies are prepared. Restart Claude Code in your project.",
|
|
956
1205
|
"The reflexio backend and dashboard auto-start on session start.",
|
|
957
1206
|
"Opt out with CLAUDE_SMART_BACKEND_AUTOSTART=0 or CLAUDE_SMART_DASHBOARD_AUTOSTART=0.",
|
|
958
1207
|
"",
|
|
@@ -1007,7 +1256,7 @@ async function runInstallCodex() {
|
|
|
1007
1256
|
try {
|
|
1008
1257
|
cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
|
|
1009
1258
|
process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
|
|
1010
|
-
await
|
|
1259
|
+
await bootstrapPluginRuntime(cacheDir);
|
|
1011
1260
|
} catch (err) {
|
|
1012
1261
|
process.stderr.write(
|
|
1013
1262
|
`error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
|
|
@@ -1111,7 +1360,18 @@ async function main() {
|
|
|
1111
1360
|
process.exit(1);
|
|
1112
1361
|
}
|
|
1113
1362
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1363
|
+
if (require.main === module) {
|
|
1364
|
+
main().catch((err) => {
|
|
1365
|
+
process.stderr.write(`claude-smart: ${err && err.message ? err.message : err}\n`);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
module.exports = {
|
|
1371
|
+
assertSupportedRuntimePlatform,
|
|
1372
|
+
bootstrapPluginRuntime,
|
|
1373
|
+
ensurePrivateNode,
|
|
1374
|
+
ensureUv,
|
|
1375
|
+
patchCodexHooksForNode,
|
|
1376
|
+
platformSupportError,
|
|
1377
|
+
};
|
package/package.json
CHANGED
package/plugin/README.md
CHANGED
|
@@ -16,6 +16,10 @@ under `~/.claude-smart/` when they are missing. If Node.js is already installed,
|
|
|
16
16
|
`npx claude-smart install` is equivalent; if uv is already installed,
|
|
17
17
|
`uvx claude-smart install` is equivalent.
|
|
18
18
|
|
|
19
|
+
Supported vanilla native targets are Apple Silicon macOS 14+ and Windows x64.
|
|
20
|
+
Intel Mac, macOS 13 or older, and Windows ARM fail early because the local
|
|
21
|
+
embedding/ML dependency stack does not provide a complete native wheel set.
|
|
22
|
+
|
|
19
23
|
Then restart Claude Code.
|
|
20
24
|
|
|
21
25
|
## Uninstall
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
{
|
|
6
6
|
"matcher": "startup|resume",
|
|
7
7
|
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
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/smart-install.sh\" || true",
|
|
11
|
+
"timeout": 300
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
"type": "command",
|
|
10
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/ensure-plugin-root.sh\" \"$_R\" || true",
|