claude-smart 0.2.48 → 0.2.49
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +15 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +150 -37
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +140 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +16 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +12 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +52 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +28 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +23 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +22 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-smart",
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.49",
|
|
13
13
|
"source": "./plugin",
|
|
14
14
|
"description": "Turns corrections into Preferences, Project-specific skills, and Shared skills — uses Claude Code, Codex, or OpenCode as the LLM backend (no external API key required)"
|
|
15
15
|
}
|
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.49-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">
|
|
@@ -113,8 +113,12 @@ npx claude-smart install --host opencode
|
|
|
113
113
|
|
|
114
114
|
Then restart OpenCode in your project so it loads the plugin from `opencode.json`, the documented project config file. If that project does not already have a root config but does have `.opencode/opencode.json` or `.opencode/opencode.jsonc`, the installer updates that existing file instead of creating a second config. If both locations exist, the root config wins. Use `--global` to install into `~/.config/opencode/opencode.json` for all OpenCode projects on this machine. The installer copies the active package to `~/.claude-smart/opencode/claude-smart`, prepares that runtime immediately, and writes a `file://` plugin entry so OpenCode reloads the same prepared package after restart.
|
|
115
115
|
|
|
116
|
+
The installer prepares claude-smart's runtime dependencies, but expects the OpenCode CLI to already be installed. It resolves and records the OpenCode executable in `~/.claude-smart/.env` for later backend restarts; set `CLAUDE_SMART_OPENCODE_PATH` before install if `opencode` is not on `PATH`.
|
|
117
|
+
|
|
116
118
|
OpenCode support is new and uses OpenCode's plugin loader to inject relevant learned context before each model request. Learning extraction runs `opencode run --pure` from an isolated temp project, so it uses OpenCode's default model unless you set `CLAUDE_SMART_OPENCODE_MODEL=provider/model`. Set that env var if your normal project config pins a different provider or model.
|
|
117
119
|
|
|
120
|
+
On native Windows, install Git for Windows and make `bash.exe` available on `PATH` before starting OpenCode, or run OpenCode from WSL. The installer also checks the Windows local embedding runtime and writes actionable setup failures to `~/.claude-smart/install-failed`.
|
|
121
|
+
|
|
118
122
|
To uninstall:
|
|
119
123
|
|
|
120
124
|
```bash
|
|
@@ -237,7 +241,7 @@ Advanced users can tune claude-smart via environment variables — see [DEVELOPE
|
|
|
237
241
|
| Path | What |
|
|
238
242
|
| --- | --- |
|
|
239
243
|
| `~/.reflexio/data/reflexio.db` | Source of truth for learned preferences, skills, interactions, full-text indexes, and embedding tables (plus `.db-shm` / `.db-wal` WAL sidecars). Inspect with `sqlite3`. |
|
|
240
|
-
| `~/.
|
|
244
|
+
| `~/.claude-smart/.env` | claude-smart local runtime config — `CLAUDE_SMART_HOST`, `CLAUDE_SMART_USE_LOCAL_CLI`, `CLAUDE_SMART_USE_LOCAL_EMBEDDING`, and `CLAUDE_SMART_OPENCODE_PATH`. |
|
|
241
245
|
| `.claude/settings.local.json` or `~/.claude/settings.json` | Claude Code hook environment, such as `CLAUDE_SMART_ENABLE_OPTIMIZER`; use project-local settings for one repo or user settings for all projects. |
|
|
242
246
|
| `~/.codex/config.toml` | Codex plugin state, hook feature flags, and per-hook trust entries after `claude-smart install --host codex`. |
|
|
243
247
|
| `~/.codex/plugins/cache/reflexioai/claude-smart/<version>/` | Codex's cached install of the `claude-smart` plugin from the `ReflexioAI` marketplace. |
|
package/bin/claude-smart.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* CLIs. Both Claude Code and Codex install from the bundled marketplace in
|
|
5
5
|
* this npm package: Claude Code registers the package root as a local
|
|
6
6
|
* marketplace, and Codex copies the bundled plugin into its own marketplace
|
|
7
|
-
* wrapper.
|
|
8
|
-
*
|
|
7
|
+
* wrapper. The npm path reads setup/bootstrap config from ~/.reflexio/.env and
|
|
8
|
+
* seeds runtime ~/.claude-smart/.env with local-provider defaults so reflexio
|
|
9
|
+
* can route generation through local tools with no API key.
|
|
9
10
|
* Managed/read-only/global setup is handled by `npx claude-smart setup`,
|
|
10
11
|
* which writes ~/.reflexio/.env before running this installer.
|
|
11
12
|
*
|
|
@@ -44,15 +45,24 @@ const CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI";
|
|
|
44
45
|
const CODEX_PLUGIN_ID = `claude-smart@${CODEX_MARKETPLACE_NAME}`;
|
|
45
46
|
const OPENCODE_BARE_PLUGIN_SPEC = "claude-smart";
|
|
46
47
|
const OPENCODE_CONFIG_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
47
|
-
const
|
|
48
|
+
const REFLEXIO_SETUP_ENV_PATH = join(homedir(), ".reflexio", ".env");
|
|
49
|
+
const CLAUDE_SMART_ENV_PATH = join(homedir(), ".claude-smart", ".env");
|
|
48
50
|
const MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/";
|
|
49
51
|
const MANAGED_SETUP_ENV = "CLAUDE_SMART_MANAGED_SETUP";
|
|
50
52
|
const CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY";
|
|
51
53
|
const CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI";
|
|
52
54
|
const CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING";
|
|
55
|
+
const CLAUDE_SMART_HOST_ENV = "CLAUDE_SMART_HOST";
|
|
56
|
+
const CLAUDE_SMART_OPENCODE_PATH_ENV = "CLAUDE_SMART_OPENCODE_PATH";
|
|
53
57
|
const REFLEXIO_USER_ID_ENV = "REFLEXIO_USER_ID";
|
|
58
|
+
const HOST_CLAUDE_CODE = "claude-code";
|
|
59
|
+
const HOST_CODEX = "codex";
|
|
60
|
+
const HOST_OPENCODE = "opencode";
|
|
61
|
+
const SUPPORTED_HOSTS = [HOST_CLAUDE_CODE, HOST_CODEX, HOST_OPENCODE];
|
|
62
|
+
const DEFAULT_CLAUDE_SMART_HOST = HOST_CLAUDE_CODE;
|
|
54
63
|
const REFLEXIO_DIR = join(homedir(), ".reflexio");
|
|
55
64
|
const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
|
|
65
|
+
const INSTALL_FAILURE_MARKER = join(CLAUDE_SMART_STATE_DIR, "install-failed");
|
|
56
66
|
const OPENCODE_LOCAL_PACKAGE_DIR = join(CLAUDE_SMART_STATE_DIR, "opencode", "claude-smart");
|
|
57
67
|
const OPENCODE_PACKAGE_LOCK_TIMEOUT_MS = 120_000;
|
|
58
68
|
const OPENCODE_PACKAGE_LOCK_STALE_MS = 10 * 60_000;
|
|
@@ -96,7 +106,7 @@ const CODEX_REQUIRED_FILES = [
|
|
|
96
106
|
"plugin/scripts/_codex_env.sh",
|
|
97
107
|
];
|
|
98
108
|
const CODEX_CLI_TIMEOUT_MS = 30_000;
|
|
99
|
-
const PLUGIN_SERVICE_TIMEOUT_MS =
|
|
109
|
+
const PLUGIN_SERVICE_TIMEOUT_MS = 45_000;
|
|
100
110
|
const COPYTREE_IGNORE_NAMES = new Set([
|
|
101
111
|
"__pycache__",
|
|
102
112
|
".venv",
|
|
@@ -108,7 +118,7 @@ const COPYTREE_IGNORE_NAMES = new Set([
|
|
|
108
118
|
]);
|
|
109
119
|
const LOCAL_DEFAULT_ENV_ENTRIES = [
|
|
110
120
|
[
|
|
111
|
-
"# Route reflexio generation through the local
|
|
121
|
+
"# Route reflexio generation through the configured local host CLI",
|
|
112
122
|
CLAUDE_SMART_USE_LOCAL_CLI_ENV,
|
|
113
123
|
"1",
|
|
114
124
|
],
|
|
@@ -118,13 +128,17 @@ const LOCAL_DEFAULT_ENV_ENTRIES = [
|
|
|
118
128
|
"1",
|
|
119
129
|
],
|
|
120
130
|
[null, CLAUDE_SMART_READ_ONLY_ENV, "0"],
|
|
131
|
+
[null, CLAUDE_SMART_HOST_ENV, DEFAULT_CLAUDE_SMART_HOST],
|
|
121
132
|
];
|
|
133
|
+
const ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS = new Set([
|
|
134
|
+
CLAUDE_SMART_USE_LOCAL_CLI_ENV,
|
|
135
|
+
CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
|
|
136
|
+
]);
|
|
122
137
|
const LOCAL_MODE_PRUNE_KEYS = new Set([
|
|
123
138
|
"REFLEXIO_URL",
|
|
124
139
|
"REFLEXIO_API_KEY",
|
|
125
140
|
REFLEXIO_USER_ID_ENV,
|
|
126
141
|
]);
|
|
127
|
-
|
|
128
142
|
function shouldCopyPath(src) {
|
|
129
143
|
const base = src.split(/[\\/]/).pop() || "";
|
|
130
144
|
if (COPYTREE_IGNORE_NAMES.has(base)) return false;
|
|
@@ -254,14 +268,25 @@ function escapeEnvValue(value) {
|
|
|
254
268
|
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
255
269
|
}
|
|
256
270
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
271
|
+
function resolveLocalEnvDefault(key, fallback, installHost) {
|
|
272
|
+
if (key === CLAUDE_SMART_HOST_ENV) return installHost;
|
|
273
|
+
if (ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS.has(key)) {
|
|
274
|
+
const explicit = (process.env[key] || "").trim();
|
|
275
|
+
if (explicit) return explicit;
|
|
276
|
+
}
|
|
277
|
+
return fallback;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function ensureLocalEnvFile(path, installHost) {
|
|
281
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
282
|
+
const existing = existsSync(path)
|
|
283
|
+
? readFileSync(path, "utf8")
|
|
261
284
|
: "";
|
|
262
285
|
const present = new Set();
|
|
263
286
|
const keptLines = [];
|
|
264
287
|
let pruned = false;
|
|
288
|
+
let changed = false;
|
|
289
|
+
let hostWritten = false;
|
|
265
290
|
for (const line of existing.split(/\r?\n/)) {
|
|
266
291
|
const parsed = parseEnvLine(line);
|
|
267
292
|
if (parsed) {
|
|
@@ -269,6 +294,18 @@ function ensureLocalReflexioEnv() {
|
|
|
269
294
|
pruned = true;
|
|
270
295
|
continue;
|
|
271
296
|
}
|
|
297
|
+
if (parsed.key === CLAUDE_SMART_HOST_ENV) {
|
|
298
|
+
present.add(parsed.key);
|
|
299
|
+
if (!hostWritten) {
|
|
300
|
+
const replacement = `${CLAUDE_SMART_HOST_ENV}=${escapeEnvValue(installHost)}`;
|
|
301
|
+
keptLines.push(replacement);
|
|
302
|
+
hostWritten = true;
|
|
303
|
+
if (line !== replacement || parsed.value !== installHost) changed = true;
|
|
304
|
+
} else {
|
|
305
|
+
changed = true;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
272
309
|
present.add(parsed.key);
|
|
273
310
|
}
|
|
274
311
|
keptLines.push(line);
|
|
@@ -278,29 +315,62 @@ function ensureLocalReflexioEnv() {
|
|
|
278
315
|
const added = [];
|
|
279
316
|
for (const [comment, key, value] of LOCAL_DEFAULT_ENV_ENTRIES) {
|
|
280
317
|
if (present.has(key)) continue;
|
|
318
|
+
const effectiveValue = resolveLocalEnvDefault(key, value, installHost);
|
|
281
319
|
if (comment) additions.push(comment);
|
|
282
320
|
if (key === CLAUDE_SMART_READ_ONLY_ENV) {
|
|
283
|
-
additions.push(`${key}="${escapeEnvValue(
|
|
321
|
+
additions.push(`${key}="${escapeEnvValue(effectiveValue)}"`);
|
|
284
322
|
} else {
|
|
285
|
-
additions.push(`${key}=${escapeEnvValue(
|
|
323
|
+
additions.push(`${key}=${escapeEnvValue(effectiveValue)}`);
|
|
286
324
|
}
|
|
287
325
|
added.push(key);
|
|
288
326
|
}
|
|
289
327
|
|
|
290
|
-
if (additions.length > 0 || pruned) {
|
|
328
|
+
if (additions.length > 0 || pruned || changed) {
|
|
291
329
|
let content = keptLines.join("\n").replace(/\n*$/, "");
|
|
292
330
|
if (additions.length > 0) {
|
|
293
331
|
const prefix = content ? "\n" : "";
|
|
294
332
|
content = content + prefix + additions.join("\n");
|
|
295
333
|
}
|
|
296
|
-
writeFileSync(
|
|
297
|
-
} else if (!existsSync(
|
|
298
|
-
writeFileSync(
|
|
334
|
+
writeFileSync(path, content ? `${content}\n` : "");
|
|
335
|
+
} else if (!existsSync(path)) {
|
|
336
|
+
writeFileSync(path, "");
|
|
299
337
|
}
|
|
300
|
-
chmodSync(
|
|
338
|
+
chmodSync(path, 0o600);
|
|
301
339
|
return added;
|
|
302
340
|
}
|
|
303
341
|
|
|
342
|
+
function setEnvVars(path, values) {
|
|
343
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
344
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
345
|
+
const seen = new Set();
|
|
346
|
+
const out = [];
|
|
347
|
+
for (const line of existing ? existing.split(/\r?\n/) : []) {
|
|
348
|
+
const parsed = parseEnvLine(line);
|
|
349
|
+
if (!parsed || !(parsed.key in values)) {
|
|
350
|
+
out.push(line);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
out.push(`${parsed.key}="${escapeEnvValue(values[parsed.key])}"`);
|
|
354
|
+
seen.add(parsed.key);
|
|
355
|
+
}
|
|
356
|
+
const added = [];
|
|
357
|
+
for (const [key, value] of Object.entries(values)) {
|
|
358
|
+
if (seen.has(key)) continue;
|
|
359
|
+
out.push(`${key}="${escapeEnvValue(value)}"`);
|
|
360
|
+
added.push(key);
|
|
361
|
+
}
|
|
362
|
+
const content = out.join("\n").replace(/\n*$/, "");
|
|
363
|
+
writeFileSync(path, content ? `${content}\n` : "");
|
|
364
|
+
chmodSync(path, 0o600);
|
|
365
|
+
return added;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function ensureLocalInstallEnvDefaults(installHost = DEFAULT_CLAUDE_SMART_HOST) {
|
|
369
|
+
const added = ensureLocalEnvFile(REFLEXIO_SETUP_ENV_PATH, installHost);
|
|
370
|
+
const runtimeAdded = ensureLocalEnvFile(CLAUDE_SMART_ENV_PATH, installHost);
|
|
371
|
+
return Array.from(new Set([...added, ...runtimeAdded]));
|
|
372
|
+
}
|
|
373
|
+
|
|
304
374
|
function maskSecret(value) {
|
|
305
375
|
if (!value) return "";
|
|
306
376
|
if (value.length <= 8) return "*".repeat(value.length);
|
|
@@ -308,12 +378,12 @@ function maskSecret(value) {
|
|
|
308
378
|
return `${prefix}****${value.slice(-4)}`;
|
|
309
379
|
}
|
|
310
380
|
|
|
311
|
-
function loadReflexioSetupEnv() {
|
|
381
|
+
function loadReflexioSetupEnv(installHost = DEFAULT_CLAUDE_SMART_HOST) {
|
|
312
382
|
let readOnlyValue = "";
|
|
313
383
|
let fileApiKey = "";
|
|
314
384
|
let fileUrl = "";
|
|
315
|
-
if (existsSync(
|
|
316
|
-
const text = readFileSync(
|
|
385
|
+
if (existsSync(REFLEXIO_SETUP_ENV_PATH)) {
|
|
386
|
+
const text = readFileSync(REFLEXIO_SETUP_ENV_PATH, "utf8");
|
|
317
387
|
for (const line of text.split(/\r?\n/)) {
|
|
318
388
|
const parsed = parseEnvLine(line);
|
|
319
389
|
if (!parsed) continue;
|
|
@@ -341,9 +411,9 @@ function loadReflexioSetupEnv() {
|
|
|
341
411
|
delete process.env.REFLEXIO_API_KEY;
|
|
342
412
|
delete process.env[REFLEXIO_USER_ID_ENV];
|
|
343
413
|
delete process.env[MANAGED_SETUP_ENV];
|
|
344
|
-
const added =
|
|
414
|
+
const added = ensureLocalInstallEnvDefaults(installHost);
|
|
345
415
|
if (added.length > 0) {
|
|
346
|
-
process.stdout.write(`Seeded ${
|
|
416
|
+
process.stdout.write(`Seeded ${REFLEXIO_SETUP_ENV_PATH} with ${added.join(", ")}.\n`);
|
|
347
417
|
}
|
|
348
418
|
}
|
|
349
419
|
const readOnly = ["1", "true", "yes", "on"].includes(
|
|
@@ -352,8 +422,8 @@ function loadReflexioSetupEnv() {
|
|
|
352
422
|
return { readOnly };
|
|
353
423
|
}
|
|
354
424
|
|
|
355
|
-
function configureReflexioSetup() {
|
|
356
|
-
return loadReflexioSetupEnv();
|
|
425
|
+
function configureReflexioSetup(installHost = DEFAULT_CLAUDE_SMART_HOST) {
|
|
426
|
+
return loadReflexioSetupEnv(installHost);
|
|
357
427
|
}
|
|
358
428
|
|
|
359
429
|
function stripJsonc(text) {
|
|
@@ -529,7 +599,7 @@ function hasExtractionProvider() {
|
|
|
529
599
|
if ((process.env.REFLEXIO_API_KEY || "").trim()) return true;
|
|
530
600
|
const cliPath = (process.env.CLAUDE_SMART_CLI_PATH || "").trim();
|
|
531
601
|
if (cliPath && isExecutableFile(cliPath)) return true;
|
|
532
|
-
return hasCli("claude") || hasCli("codex") ||
|
|
602
|
+
return hasCli("claude") || hasCli("codex") || Boolean(resolveOpenCodePath());
|
|
533
603
|
}
|
|
534
604
|
|
|
535
605
|
function isExecutableFile(path) {
|
|
@@ -550,6 +620,94 @@ function extractionProviderError() {
|
|
|
550
620
|
);
|
|
551
621
|
}
|
|
552
622
|
|
|
623
|
+
function hasOpenCodeCli() {
|
|
624
|
+
return Boolean(resolveOpenCodePath());
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function resolveOpenCodePath() {
|
|
628
|
+
const opencodePath = (process.env[CLAUDE_SMART_OPENCODE_PATH_ENV] || "").trim();
|
|
629
|
+
if (opencodePath && isExecutableFile(opencodePath)) return opencodePath;
|
|
630
|
+
return resolveCommand(isWindows() ? ["opencode.cmd", "opencode.exe", "opencode"] : ["opencode"]);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function persistOpenCodePath() {
|
|
634
|
+
const resolved = resolveOpenCodePath();
|
|
635
|
+
if (!resolved) return [];
|
|
636
|
+
process.env[CLAUDE_SMART_OPENCODE_PATH_ENV] = resolved;
|
|
637
|
+
return setEnvVars(CLAUDE_SMART_ENV_PATH, { [CLAUDE_SMART_OPENCODE_PATH_ENV]: resolved });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const WINDOWS_SYSTEM_BASH_SUFFIXES = [
|
|
641
|
+
"\\windows\\system32\\bash.exe",
|
|
642
|
+
"\\windows\\sysnative\\bash.exe",
|
|
643
|
+
"\\windows\\syswow64\\bash.exe",
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
function windowsPathText(path) {
|
|
647
|
+
return path.replace(/\//g, "\\").toLowerCase();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isWindowsSystemBash(path) {
|
|
651
|
+
const normalized = windowsPathText(path);
|
|
652
|
+
return WINDOWS_SYSTEM_BASH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function pathCommandCandidates(names) {
|
|
656
|
+
// Return every PATH match so Windows can skip System32 bash and still find Git Bash.
|
|
657
|
+
const delimiter = isWindows() ? ";" : ":";
|
|
658
|
+
const pathParts = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
|
659
|
+
const candidates = [];
|
|
660
|
+
for (const dir of pathParts) {
|
|
661
|
+
for (const name of names) {
|
|
662
|
+
const candidate = join(dir, name);
|
|
663
|
+
if (existsSync(candidate)) candidates.push(candidate);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return candidates;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function firstUsableBash(candidates) {
|
|
670
|
+
for (const candidate of candidates) {
|
|
671
|
+
const resolved = existsSync(candidate) ? candidate : resolveCommand([candidate]);
|
|
672
|
+
if (resolved && !isWindowsSystemBash(resolved)) return resolved;
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function resolveUsableBash() {
|
|
678
|
+
if (!isWindows()) return resolveCommand(["bash"]);
|
|
679
|
+
const sources = [];
|
|
680
|
+
const bashEnv = (process.env.BASH || "").trim();
|
|
681
|
+
if (bashEnv) sources.push([bashEnv]);
|
|
682
|
+
sources.push([
|
|
683
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
684
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
685
|
+
]);
|
|
686
|
+
sources.push(pathCommandCandidates(["bash.exe", "bash"]));
|
|
687
|
+
sources.push(["bash.exe", "bash"]);
|
|
688
|
+
for (const source of sources) {
|
|
689
|
+
const resolved = firstUsableBash(source);
|
|
690
|
+
if (resolved) return resolved;
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function opencodePrerequisiteError() {
|
|
696
|
+
if (!hasOpenCodeCli()) {
|
|
697
|
+
return (
|
|
698
|
+
"error: OpenCode CLI not found on PATH. Install OpenCode first, " +
|
|
699
|
+
"or set CLAUDE_SMART_OPENCODE_PATH to the OpenCode executable.\n"
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (isWindows() && !resolveUsableBash()) {
|
|
703
|
+
return (
|
|
704
|
+
"error: Git Bash is required for claude-smart OpenCode support on Windows. " +
|
|
705
|
+
"Install Git for Windows and ensure bash.exe is on PATH, or run OpenCode from WSL.\n"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
553
711
|
function findClaudeCodePluginRoot() {
|
|
554
712
|
const cacheRoot = join(homedir(), ".claude", "plugins", "cache", CODEX_MARKETPLACE_NAME, "claude-smart");
|
|
555
713
|
const candidates = [];
|
|
@@ -789,11 +947,7 @@ async function bootstrapClaudeCodeInstall() {
|
|
|
789
947
|
if (code !== 0) {
|
|
790
948
|
throw new Error(`smart-install.sh failed in ${pluginRoot}`);
|
|
791
949
|
}
|
|
792
|
-
|
|
793
|
-
if (existsSync(failureMarker)) {
|
|
794
|
-
const reason = readFileSync(failureMarker, "utf8").trim() || "unknown error";
|
|
795
|
-
throw new Error(reason);
|
|
796
|
-
}
|
|
950
|
+
throwIfInstallFailureMarker();
|
|
797
951
|
return pluginRoot;
|
|
798
952
|
}
|
|
799
953
|
|
|
@@ -870,8 +1024,14 @@ function runSilentStatus(command, args, options = {}) {
|
|
|
870
1024
|
function runPluginService(pluginRoot, scriptName, subcommand, envOverrides = {}) {
|
|
871
1025
|
const script = join(pluginRoot, "scripts", scriptName);
|
|
872
1026
|
if (!existsSync(script)) return false;
|
|
873
|
-
const bash =
|
|
874
|
-
if (!bash)
|
|
1027
|
+
const bash = resolveUsableBash();
|
|
1028
|
+
if (!bash) {
|
|
1029
|
+
const reason = isWindows()
|
|
1030
|
+
? "Git Bash is required for claude-smart services on Windows. Install Git for Windows and ensure bash.exe is on PATH, or run from WSL"
|
|
1031
|
+
: "bash is required but was not found on PATH";
|
|
1032
|
+
process.stderr.write(`warning: ${scriptName} ${subcommand} ${reason}; continuing.\n`);
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
875
1035
|
const result = spawnSync(bash, [script, subcommand], {
|
|
876
1036
|
cwd: pluginRoot,
|
|
877
1037
|
env: { ...runtimeEnv(), ...envOverrides },
|
|
@@ -1224,24 +1384,84 @@ function patchCodexHooksForNode(pluginRoot, nodePath) {
|
|
|
1224
1384
|
}
|
|
1225
1385
|
|
|
1226
1386
|
function ensurePluginRoot(pluginRoot) {
|
|
1227
|
-
const reflexioDir = dirname(
|
|
1228
|
-
const
|
|
1387
|
+
const reflexioDir = dirname(REFLEXIO_SETUP_ENV_PATH);
|
|
1388
|
+
const pluginRootLink = join(reflexioDir, "plugin-root");
|
|
1229
1389
|
mkdirSync(reflexioDir, { recursive: true });
|
|
1230
|
-
|
|
1390
|
+
let pathNotReplaceable = false;
|
|
1231
1391
|
try {
|
|
1232
|
-
|
|
1392
|
+
const existing = lstatSync(pluginRootLink);
|
|
1393
|
+
if (existing.isSymbolicLink() || existing.isFile()) {
|
|
1394
|
+
rmSync(pluginRootLink, { recursive: true, force: true });
|
|
1395
|
+
} else {
|
|
1396
|
+
pathNotReplaceable = true;
|
|
1397
|
+
}
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
if (!err || err.code !== "ENOENT") pathNotReplaceable = true;
|
|
1400
|
+
}
|
|
1401
|
+
if (pathNotReplaceable) {
|
|
1402
|
+
writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
symlinkSync(pluginRoot, pluginRootLink, isWindows() ? "junction" : "dir");
|
|
1407
|
+
writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
1233
1408
|
} catch {
|
|
1234
1409
|
writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
|
|
1235
1410
|
}
|
|
1236
1411
|
}
|
|
1237
1412
|
|
|
1413
|
+
function pluginPythonPath(pluginRoot) {
|
|
1414
|
+
// Python venvs created by uv use Scripts/python.exe on Windows across x64/arm64.
|
|
1415
|
+
return isWindows()
|
|
1416
|
+
? join(pluginRoot, ".venv", "Scripts", "python.exe")
|
|
1417
|
+
: join(pluginRoot, ".venv", "bin", "python");
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function installFailureReason() {
|
|
1421
|
+
// smart-install.sh owns the install-failed marker format; Node surfaces the
|
|
1422
|
+
// first line as the actionable reason and leaves fingerprint handling to shell.
|
|
1423
|
+
const text = readFileSync(INSTALL_FAILURE_MARKER, "utf8");
|
|
1424
|
+
const first = text.split(/\r?\n/, 1)[0].trim();
|
|
1425
|
+
return first || "unknown error";
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function throwIfInstallFailureMarker() {
|
|
1429
|
+
if (existsSync(INSTALL_FAILURE_MARKER)) throw new Error(installFailureReason());
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function verifyWindowsLocalEmbeddingRuntime(pluginRoot, env) {
|
|
1433
|
+
if (!isWindows()) return;
|
|
1434
|
+
// Reflexio owns local embeddings, but this installer is the first place a
|
|
1435
|
+
// user sees whether the prepared Reflexio runtime can import onnxruntime.
|
|
1436
|
+
const script = join(pluginRoot, "scripts", "smart-install.sh");
|
|
1437
|
+
const bash = resolveUsableBash();
|
|
1438
|
+
if (!bash) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
"Git Bash is required for claude-smart dependency checks on Windows. Install Git for Windows and ensure bash.exe is on PATH, or run from WSL.",
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const result = spawnSync(bash, [script, "verify-windows-embedding"], {
|
|
1444
|
+
cwd: pluginRoot,
|
|
1445
|
+
env,
|
|
1446
|
+
encoding: "utf8",
|
|
1447
|
+
windowsHide: true,
|
|
1448
|
+
});
|
|
1449
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
1450
|
+
const detail = result.error
|
|
1451
|
+
? result.error.message
|
|
1452
|
+
: result.signal
|
|
1453
|
+
? `terminated by ${result.signal}`
|
|
1454
|
+
: (result.stderr || "").trim() || `exited with status ${result.status}`;
|
|
1455
|
+
throw new Error(`Windows local embedding preflight failed: ${detail}`);
|
|
1456
|
+
}
|
|
1457
|
+
throwIfInstallFailureMarker();
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1238
1460
|
async function installVendoredReflexio(pluginRoot, uv, env) {
|
|
1239
1461
|
const vendorRoot = join(pluginRoot, "vendor", "reflexio");
|
|
1240
1462
|
if (!existsSync(join(vendorRoot, "pyproject.toml"))) return;
|
|
1241
1463
|
|
|
1242
|
-
const pythonPath =
|
|
1243
|
-
? join(pluginRoot, ".venv", "Scripts", "python.exe")
|
|
1244
|
-
: join(pluginRoot, ".venv", "bin", "python");
|
|
1464
|
+
const pythonPath = pluginPythonPath(pluginRoot);
|
|
1245
1465
|
if (!existsSync(pythonPath)) {
|
|
1246
1466
|
throw new Error(`plugin Python was not created by uv sync: ${pythonPath}`);
|
|
1247
1467
|
}
|
|
@@ -1321,6 +1541,7 @@ async function syncPluginPythonEnv(pluginRoot, uv, env) {
|
|
|
1321
1541
|
async function bootstrapPluginRuntime(pluginRoot, options = {}) {
|
|
1322
1542
|
assertSupportedRuntimePlatform();
|
|
1323
1543
|
process.stdout.write("Preparing claude-smart runtime for hooks...\n");
|
|
1544
|
+
rmSync(INSTALL_FAILURE_MARKER, { force: true });
|
|
1324
1545
|
const nodeRuntime = await ensurePrivateNode();
|
|
1325
1546
|
if (options.patchCodexHooks !== false) {
|
|
1326
1547
|
patchCodexHooksForNode(pluginRoot, nodeRuntime.node);
|
|
@@ -1341,6 +1562,7 @@ async function bootstrapPluginRuntime(pluginRoot, options = {}) {
|
|
|
1341
1562
|
}
|
|
1342
1563
|
await syncPluginPythonEnv(pluginRoot, uv, env);
|
|
1343
1564
|
await installVendoredReflexio(pluginRoot, uv, env);
|
|
1565
|
+
verifyWindowsLocalEmbeddingRuntime(pluginRoot, env);
|
|
1344
1566
|
|
|
1345
1567
|
const dashboardDir = join(pluginRoot, "dashboard");
|
|
1346
1568
|
if (existsSync(dashboardDir)) {
|
|
@@ -1368,7 +1590,7 @@ function printHelp() {
|
|
|
1368
1590
|
"Claude Code install:",
|
|
1369
1591
|
" 1. claude plugin marketplace add <this package>",
|
|
1370
1592
|
` 2. claude plugin install ${PLUGIN_SPEC}`,
|
|
1371
|
-
" 3. Reads
|
|
1593
|
+
" 3. Reads setup/bootstrap config when managed/read-only setup was configured.",
|
|
1372
1594
|
"",
|
|
1373
1595
|
"Codex install:",
|
|
1374
1596
|
` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
|
|
@@ -1400,14 +1622,14 @@ function printHelp() {
|
|
|
1400
1622
|
|
|
1401
1623
|
function parseHost(args) {
|
|
1402
1624
|
const idx = args.indexOf("--host");
|
|
1403
|
-
if (idx === -1) return
|
|
1625
|
+
if (idx === -1) return DEFAULT_CLAUDE_SMART_HOST;
|
|
1404
1626
|
const value = args[idx + 1];
|
|
1405
1627
|
if (!value) {
|
|
1406
|
-
process.stderr.write(
|
|
1628
|
+
process.stderr.write(`error: --host requires a value: ${SUPPORTED_HOSTS.join(", ")}\n`);
|
|
1407
1629
|
process.exit(1);
|
|
1408
1630
|
}
|
|
1409
|
-
if (value
|
|
1410
|
-
process.stderr.write(
|
|
1631
|
+
if (!SUPPORTED_HOSTS.includes(value)) {
|
|
1632
|
+
process.stderr.write(`error: --host must be ${SUPPORTED_HOSTS.join(", ")}\n`);
|
|
1411
1633
|
process.exit(1);
|
|
1412
1634
|
}
|
|
1413
1635
|
return value;
|
|
@@ -1822,11 +2044,11 @@ function findCodexPluginRoot() {
|
|
|
1822
2044
|
}
|
|
1823
2045
|
|
|
1824
2046
|
async function runUpdate(args) {
|
|
1825
|
-
if (parseHost(args) ===
|
|
2047
|
+
if (parseHost(args) === HOST_CODEX) {
|
|
1826
2048
|
await runUpdateCodex(args);
|
|
1827
2049
|
return;
|
|
1828
2050
|
}
|
|
1829
|
-
if (parseHost(args) ===
|
|
2051
|
+
if (parseHost(args) === HOST_OPENCODE) {
|
|
1830
2052
|
await runUpdateOpenCode(args);
|
|
1831
2053
|
return;
|
|
1832
2054
|
}
|
|
@@ -1854,11 +2076,11 @@ async function runUpdateOpenCode(args) {
|
|
|
1854
2076
|
}
|
|
1855
2077
|
|
|
1856
2078
|
async function runUninstall(args) {
|
|
1857
|
-
if (parseHost(args) ===
|
|
2079
|
+
if (parseHost(args) === HOST_CODEX) {
|
|
1858
2080
|
await runUninstallCodex();
|
|
1859
2081
|
return;
|
|
1860
2082
|
}
|
|
1861
|
-
if (parseHost(args) ===
|
|
2083
|
+
if (parseHost(args) === HOST_OPENCODE) {
|
|
1862
2084
|
await runUninstallOpenCode(args);
|
|
1863
2085
|
return;
|
|
1864
2086
|
}
|
|
@@ -1908,11 +2130,11 @@ async function runSetup(args) {
|
|
|
1908
2130
|
}
|
|
1909
2131
|
|
|
1910
2132
|
async function runInstall(args, options = {}) {
|
|
1911
|
-
if (parseHost(args) ===
|
|
2133
|
+
if (parseHost(args) === HOST_CODEX) {
|
|
1912
2134
|
await runInstallCodex(args);
|
|
1913
2135
|
return;
|
|
1914
2136
|
}
|
|
1915
|
-
if (parseHost(args) ===
|
|
2137
|
+
if (parseHost(args) === HOST_OPENCODE) {
|
|
1916
2138
|
await runInstallOpenCode(args);
|
|
1917
2139
|
return;
|
|
1918
2140
|
}
|
|
@@ -1926,7 +2148,7 @@ async function runInstall(args, options = {}) {
|
|
|
1926
2148
|
}
|
|
1927
2149
|
|
|
1928
2150
|
const source = PACKAGE_ROOT;
|
|
1929
|
-
const setup = configureReflexioSetup();
|
|
2151
|
+
const setup = configureReflexioSetup(HOST_CLAUDE_CODE);
|
|
1930
2152
|
const readOnly = setup.readOnly;
|
|
1931
2153
|
|
|
1932
2154
|
const steps = [
|
|
@@ -1966,7 +2188,7 @@ async function runInstall(args, options = {}) {
|
|
|
1966
2188
|
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
1967
2189
|
}
|
|
1968
2190
|
process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
|
|
1969
|
-
if (startBackendService(pluginRoot,
|
|
2191
|
+
if (startBackendService(pluginRoot, HOST_CLAUDE_CODE)) {
|
|
1970
2192
|
process.stdout.write("Started claude-smart backend service.\n");
|
|
1971
2193
|
}
|
|
1972
2194
|
if (refreshDashboardService(pluginRoot)) {
|
|
@@ -1998,7 +2220,7 @@ async function runInstallCodex(args) {
|
|
|
1998
2220
|
process.stderr.write("error: 'codex' CLI not found on PATH. Install Codex first.\n");
|
|
1999
2221
|
process.exit(1);
|
|
2000
2222
|
}
|
|
2001
|
-
const setup = configureReflexioSetup();
|
|
2223
|
+
const setup = configureReflexioSetup(HOST_CODEX);
|
|
2002
2224
|
const readOnly = setup.readOnly;
|
|
2003
2225
|
|
|
2004
2226
|
const marketplaceRoot = copyCodexMarketplace();
|
|
@@ -2049,7 +2271,7 @@ async function runInstallCodex(args) {
|
|
|
2049
2271
|
if (readOnly) {
|
|
2050
2272
|
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
2051
2273
|
}
|
|
2052
|
-
if (startBackendService(cacheDir,
|
|
2274
|
+
if (startBackendService(cacheDir, HOST_CODEX)) {
|
|
2053
2275
|
process.stdout.write("Started claude-smart backend service.\n");
|
|
2054
2276
|
}
|
|
2055
2277
|
if (refreshDashboardService(cacheDir)) {
|
|
@@ -2099,8 +2321,14 @@ async function runInstallCodex(args) {
|
|
|
2099
2321
|
}
|
|
2100
2322
|
|
|
2101
2323
|
async function runInstallOpenCode(args) {
|
|
2102
|
-
const
|
|
2324
|
+
const prerequisiteError = opencodePrerequisiteError();
|
|
2325
|
+
if (prerequisiteError) {
|
|
2326
|
+
process.stderr.write(prerequisiteError);
|
|
2327
|
+
process.exit(1);
|
|
2328
|
+
}
|
|
2329
|
+
const setup = configureReflexioSetup(HOST_OPENCODE);
|
|
2103
2330
|
const readOnly = setup.readOnly;
|
|
2331
|
+
persistOpenCodePath();
|
|
2104
2332
|
if (!hasExtractionProvider()) {
|
|
2105
2333
|
process.stderr.write(extractionProviderError());
|
|
2106
2334
|
process.exit(1);
|
|
@@ -2139,7 +2367,7 @@ async function runInstallOpenCode(args) {
|
|
|
2139
2367
|
if (readOnly) {
|
|
2140
2368
|
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
2141
2369
|
}
|
|
2142
|
-
if (startBackendService(pluginRoot,
|
|
2370
|
+
if (startBackendService(pluginRoot, HOST_OPENCODE)) {
|
|
2143
2371
|
process.stdout.write("Started claude-smart backend service.\n");
|
|
2144
2372
|
}
|
|
2145
2373
|
if (refreshDashboardService(pluginRoot)) {
|
|
@@ -2274,7 +2502,12 @@ module.exports = {
|
|
|
2274
2502
|
opencodeLocalPluginSpec,
|
|
2275
2503
|
installOpenCodePluginPackage,
|
|
2276
2504
|
patchOpenCodePluginConfig,
|
|
2505
|
+
parseHost,
|
|
2277
2506
|
hasExtractionProvider,
|
|
2507
|
+
hasOpenCodeCli,
|
|
2508
|
+
persistOpenCodePath,
|
|
2509
|
+
resolveOpenCodePath,
|
|
2510
|
+
opencodePrerequisiteError,
|
|
2278
2511
|
platformSupportError,
|
|
2279
2512
|
prunePublishHooksForReadOnly,
|
|
2280
2513
|
restorePublishHooksFromSource,
|
package/package.json
CHANGED