claude-smart 0.2.36 → 0.2.37
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 -6
- package/bin/claude-smart.js +98 -107
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +6 -6
- package/plugin/scripts/codex-hook.js +2 -1
- package/plugin/scripts/smart-install.sh +1 -24
- package/plugin/src/claude_smart/cli.py +74 -99
- package/plugin/src/claude_smart/events/pre_tool.py +1 -1
- package/plugin/src/claude_smart/events/session_end.py +1 -1
- package/plugin/src/claude_smart/events/stop.py +24 -7
- package/plugin/src/claude_smart/events/user_prompt.py +1 -1
- package/plugin/src/claude_smart/hook.py +20 -1
- package/plugin/src/claude_smart/hook_log.py +7 -2
- package/plugin/src/claude_smart/ids.py +26 -0
- package/plugin/src/claude_smart/internal_call.py +6 -6
- package/plugin/uv.lock +1 -1
- package/scripts/setup-claude-smart.sh +329 -0
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.37-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">
|
|
@@ -81,9 +81,6 @@ Four things this changes:
|
|
|
81
81
|
npx claude-smart install # or: uvx claude-smart install
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Use `--read-only` to install context-loading hooks without the hooks that
|
|
85
|
-
publish interactions for learning.
|
|
86
|
-
|
|
87
84
|
Then restart Claude Code.
|
|
88
85
|
|
|
89
86
|
Requires Node.js (for `npx`) or uv (for `uvx`) to already exist.
|
|
@@ -113,8 +110,6 @@ claude plugin uninstall claude-smart@reflexioai
|
|
|
113
110
|
npx claude-smart install --host codex
|
|
114
111
|
```
|
|
115
112
|
|
|
116
|
-
Add `--read-only` to skip the publish interactions hook.
|
|
117
|
-
|
|
118
113
|
Then fully quit and reopen Codex so hooks reload.
|
|
119
114
|
|
|
120
115
|
Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
|
package/bin/claude-smart.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* plugin. For Codex it copies the bundled local marketplace, registers it,
|
|
6
6
|
* and enables plugin hooks. Both paths seed ~/.reflexio/.env with the two
|
|
7
7
|
* local-provider flags so reflexio can route generation through local tools
|
|
8
|
-
* with no API key.
|
|
8
|
+
* with no API key. Managed/read-only/global setup is handled by
|
|
9
|
+
* `npx claude-smart setup`, which writes ~/.reflexio/.env before running this
|
|
10
|
+
* installer.
|
|
9
11
|
*
|
|
10
12
|
* Keep this file dependency-free — it runs via `npx` with no install step.
|
|
11
13
|
*/
|
|
@@ -14,8 +16,6 @@
|
|
|
14
16
|
const { execSync, spawn, spawnSync } = require("child_process");
|
|
15
17
|
const crypto = require("crypto");
|
|
16
18
|
const {
|
|
17
|
-
appendFileSync,
|
|
18
|
-
chmodSync,
|
|
19
19
|
cpSync,
|
|
20
20
|
existsSync,
|
|
21
21
|
lstatSync,
|
|
@@ -41,6 +41,7 @@ const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
|
|
|
41
41
|
const MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/";
|
|
42
42
|
const MANAGED_SETUP_ENV = "CLAUDE_SMART_MANAGED_SETUP";
|
|
43
43
|
const CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY";
|
|
44
|
+
const REFLEXIO_USER_ID_ENV = "REFLEXIO_USER_ID";
|
|
44
45
|
const REFLEXIO_DIR = join(homedir(), ".reflexio");
|
|
45
46
|
const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
|
|
46
47
|
const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml");
|
|
@@ -196,28 +197,6 @@ function runCodex(args) {
|
|
|
196
197
|
});
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
function seedReflexioEnv() {
|
|
200
|
-
mkdirSync(dirname(REFLEXIO_ENV_PATH), { recursive: true });
|
|
201
|
-
const existing = existsSync(REFLEXIO_ENV_PATH)
|
|
202
|
-
? readFileSync(REFLEXIO_ENV_PATH, "utf8")
|
|
203
|
-
: "";
|
|
204
|
-
const flags = [
|
|
205
|
-
"CLAUDE_SMART_USE_LOCAL_CLI",
|
|
206
|
-
"CLAUDE_SMART_USE_LOCAL_EMBEDDING",
|
|
207
|
-
];
|
|
208
|
-
const missing = flags.filter((f) => !new RegExp(`^${f}=`, "m").test(existing));
|
|
209
|
-
if (missing.length === 0) return [];
|
|
210
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
211
|
-
const body = missing.map((f) => `${f}=1`).join("\n") + "\n";
|
|
212
|
-
appendFileSync(REFLEXIO_ENV_PATH, prefix + body);
|
|
213
|
-
chmodSync(REFLEXIO_ENV_PATH, 0o600);
|
|
214
|
-
return missing;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function escapeEnvValue(value) {
|
|
218
|
-
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
200
|
function parseEnvLine(line) {
|
|
222
201
|
let trimmed = String(line || "").trim();
|
|
223
202
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -226,36 +205,15 @@ function parseEnvLine(line) {
|
|
|
226
205
|
if (eq < 0) return null;
|
|
227
206
|
const key = trimmed.slice(0, eq).trim();
|
|
228
207
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return null;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
: "";
|
|
237
|
-
const lines = existing ? existing.split(/\r?\n/) : [];
|
|
238
|
-
const seen = new Set();
|
|
239
|
-
const out = [];
|
|
240
|
-
for (const line of lines) {
|
|
241
|
-
const parsed = parseEnvLine(line);
|
|
242
|
-
if (parsed && Object.prototype.hasOwnProperty.call(values, parsed.key)) {
|
|
243
|
-
out.push(`${parsed.key}="${escapeEnvValue(values[parsed.key])}"`);
|
|
244
|
-
seen.add(parsed.key);
|
|
245
|
-
} else {
|
|
246
|
-
out.push(line);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const added = [];
|
|
250
|
-
for (const [key, value] of Object.entries(values)) {
|
|
251
|
-
if (seen.has(key)) continue;
|
|
252
|
-
out.push(`${key}="${escapeEnvValue(value)}"`);
|
|
253
|
-
added.push(key);
|
|
208
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
209
|
+
if (
|
|
210
|
+
value.length >= 2 &&
|
|
211
|
+
((value[0] === '"' && value[value.length - 1] === '"') ||
|
|
212
|
+
(value[0] === "'" && value[value.length - 1] === "'"))
|
|
213
|
+
) {
|
|
214
|
+
value = value.slice(1, -1);
|
|
254
215
|
}
|
|
255
|
-
|
|
256
|
-
writeFileSync(REFLEXIO_ENV_PATH, content);
|
|
257
|
-
chmodSync(REFLEXIO_ENV_PATH, 0o600);
|
|
258
|
-
return added;
|
|
216
|
+
return { key, value };
|
|
259
217
|
}
|
|
260
218
|
|
|
261
219
|
function maskSecret(value) {
|
|
@@ -265,38 +223,47 @@ function maskSecret(value) {
|
|
|
265
223
|
return `${prefix}****${value.slice(-4)}`;
|
|
266
224
|
}
|
|
267
225
|
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
226
|
+
function loadReflexioSetupEnv() {
|
|
227
|
+
let readOnlyValue = "";
|
|
228
|
+
let fileApiKey = "";
|
|
229
|
+
let fileUrl = "";
|
|
230
|
+
if (existsSync(REFLEXIO_ENV_PATH)) {
|
|
231
|
+
const text = readFileSync(REFLEXIO_ENV_PATH, "utf8");
|
|
232
|
+
for (const line of text.split(/\r?\n/)) {
|
|
233
|
+
const parsed = parseEnvLine(line);
|
|
234
|
+
if (!parsed) continue;
|
|
235
|
+
if (parsed.key === "REFLEXIO_API_KEY") {
|
|
236
|
+
fileApiKey = parsed.value;
|
|
237
|
+
} else if (parsed.key === "REFLEXIO_URL") {
|
|
238
|
+
fileUrl = parsed.value;
|
|
239
|
+
} else if (parsed.key === REFLEXIO_USER_ID_ENV) {
|
|
240
|
+
process.env[parsed.key] = parsed.value;
|
|
241
|
+
} else if (parsed.key === CLAUDE_SMART_READ_ONLY_ENV) {
|
|
242
|
+
readOnlyValue = parsed.value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const apiKey = (fileApiKey || process.env.REFLEXIO_API_KEY || "").trim();
|
|
271
247
|
if (apiKey) {
|
|
272
|
-
const reflexioUrl = parseOptionalArg(args, "--reflexio-url") || MANAGED_REFLEXIO_URL;
|
|
273
|
-
const values = {
|
|
274
|
-
REFLEXIO_URL: reflexioUrl,
|
|
275
|
-
REFLEXIO_API_KEY: apiKey,
|
|
276
|
-
};
|
|
277
|
-
if (readOnly) values[CLAUDE_SMART_READ_ONLY_ENV] = "1";
|
|
278
|
-
const added = setReflexioEnvVars(values);
|
|
279
|
-
const changed = added.length > 0 ? added.join(", ") : "managed Reflexio settings";
|
|
280
|
-
process.stdout.write(
|
|
281
|
-
`Configured ${REFLEXIO_ENV_PATH} for managed Reflexio (${changed}; API key ${maskSecret(apiKey)}).\n`,
|
|
282
|
-
);
|
|
283
|
-
process.env.REFLEXIO_URL = reflexioUrl;
|
|
284
248
|
process.env.REFLEXIO_API_KEY = apiKey;
|
|
249
|
+
process.env.REFLEXIO_URL = (fileUrl || process.env.REFLEXIO_URL || MANAGED_REFLEXIO_URL).trim();
|
|
285
250
|
process.env[MANAGED_SETUP_ENV] = "1";
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const added = seedReflexioEnv();
|
|
290
|
-
if (readOnly) {
|
|
291
|
-
added.push(
|
|
292
|
-
...setReflexioEnvVars({
|
|
293
|
-
[CLAUDE_SMART_READ_ONLY_ENV]: "1",
|
|
294
|
-
}),
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
`Using managed Reflexio at ${process.env.REFLEXIO_URL} (API key ${maskSecret(apiKey)}).\n`,
|
|
295
253
|
);
|
|
254
|
+
} else {
|
|
255
|
+
delete process.env.REFLEXIO_URL;
|
|
256
|
+
delete process.env.REFLEXIO_API_KEY;
|
|
257
|
+
delete process.env[MANAGED_SETUP_ENV];
|
|
296
258
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
259
|
+
const readOnly = ["1", "true", "yes", "on"].includes(
|
|
260
|
+
String(readOnlyValue).trim().toLowerCase(),
|
|
261
|
+
);
|
|
262
|
+
return { readOnly };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function configureReflexioSetup() {
|
|
266
|
+
return loadReflexioSetupEnv();
|
|
300
267
|
}
|
|
301
268
|
|
|
302
269
|
function findClaudeCodePluginRoot() {
|
|
@@ -772,6 +739,18 @@ function prunePublishHooksForReadOnly(pluginRoot) {
|
|
|
772
739
|
}
|
|
773
740
|
}
|
|
774
741
|
|
|
742
|
+
function restorePublishHooksFromSource(pluginRoot) {
|
|
743
|
+
const sourceHooksDir = join(PACKAGE_ROOT, "plugin", "hooks");
|
|
744
|
+
const targetHooksDir = join(pluginRoot, "hooks");
|
|
745
|
+
for (const hookFile of ["hooks.json", "codex-hooks.json"]) {
|
|
746
|
+
const sourcePath = join(sourceHooksDir, hookFile);
|
|
747
|
+
const targetPath = join(targetHooksDir, hookFile);
|
|
748
|
+
if (!existsSync(sourcePath) || !existsSync(targetPath)) continue;
|
|
749
|
+
if (sourcePath === targetPath) continue;
|
|
750
|
+
cpSync(sourcePath, targetPath, { force: true });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
775
754
|
function patchCodexHooksForNode(pluginRoot, nodePath) {
|
|
776
755
|
const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
|
|
777
756
|
const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
|
|
@@ -870,17 +849,14 @@ function printHelp() {
|
|
|
870
849
|
" npx claude-smart install Install the plugin into Claude Code",
|
|
871
850
|
" npx claude-smart install --host codex Register the plugin marketplace for Codex",
|
|
872
851
|
" npx claude-smart install --source <owner/repo> Override the marketplace source",
|
|
873
|
-
" npx claude-smart
|
|
874
|
-
" npx claude-smart install --read-only Install without publish interactions hooks",
|
|
852
|
+
" npx claude-smart setup Configure managed/read-only/global setup",
|
|
875
853
|
" npx claude-smart uninstall --host codex Remove the Codex marketplace registration",
|
|
876
854
|
" npx claude-smart --help Show this help",
|
|
877
855
|
"",
|
|
878
856
|
"Claude Code install:",
|
|
879
857
|
" 1. claude plugin marketplace add <source>",
|
|
880
858
|
` 2. claude plugin install ${PLUGIN_SPEC}`,
|
|
881
|
-
" 3.
|
|
882
|
-
" to ~/.reflexio/.env (idempotent).",
|
|
883
|
-
" Passing --api-key writes REFLEXIO_URL and REFLEXIO_API_KEY instead.",
|
|
859
|
+
" 3. Reads ~/.reflexio/.env when managed/read-only setup was configured.",
|
|
884
860
|
"",
|
|
885
861
|
"Codex install:",
|
|
886
862
|
` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
|
|
@@ -893,7 +869,7 @@ function printHelp() {
|
|
|
893
869
|
"",
|
|
894
870
|
"Update:",
|
|
895
871
|
" npx claude-smart update Update to the latest version",
|
|
896
|
-
" npx claude-smart
|
|
872
|
+
" npx claude-smart setup Configure managed/read-only/global setup",
|
|
897
873
|
"",
|
|
898
874
|
"Uninstall:",
|
|
899
875
|
" npx claude-smart uninstall Remove the plugin from Claude Code",
|
|
@@ -913,17 +889,6 @@ function parseSource(args) {
|
|
|
913
889
|
return value;
|
|
914
890
|
}
|
|
915
891
|
|
|
916
|
-
function parseOptionalArg(args, flag) {
|
|
917
|
-
const idx = args.indexOf(flag);
|
|
918
|
-
if (idx === -1) return "";
|
|
919
|
-
const value = args[idx + 1];
|
|
920
|
-
if (!value) {
|
|
921
|
-
process.stderr.write(`error: ${flag} requires a value\n`);
|
|
922
|
-
process.exit(1);
|
|
923
|
-
}
|
|
924
|
-
return value;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
892
|
function parseHost(args) {
|
|
928
893
|
const idx = args.indexOf("--host");
|
|
929
894
|
if (idx === -1) return "claude-code";
|
|
@@ -939,10 +904,6 @@ function parseHost(args) {
|
|
|
939
904
|
return value;
|
|
940
905
|
}
|
|
941
906
|
|
|
942
|
-
function parseReadOnly(args) {
|
|
943
|
-
return args.includes("--read-only");
|
|
944
|
-
}
|
|
945
|
-
|
|
946
907
|
function copyCodexMarketplace() {
|
|
947
908
|
for (const rel of CODEX_REQUIRED_FILES) {
|
|
948
909
|
const path = join(PACKAGE_ROOT, rel);
|
|
@@ -1324,6 +1285,7 @@ function installCodexPluginCache(pluginRoot) {
|
|
|
1324
1285
|
}
|
|
1325
1286
|
|
|
1326
1287
|
async function runUpdate(args) {
|
|
1288
|
+
const setup = configureReflexioSetup();
|
|
1327
1289
|
if (!hasClaudeCli()) {
|
|
1328
1290
|
process.stderr.write(
|
|
1329
1291
|
"error: 'claude' CLI not found on PATH. " +
|
|
@@ -1341,7 +1303,14 @@ async function runUpdate(args) {
|
|
|
1341
1303
|
}
|
|
1342
1304
|
|
|
1343
1305
|
process.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n");
|
|
1344
|
-
|
|
1306
|
+
const pluginRoot = findClaudeCodePluginRoot();
|
|
1307
|
+
if (pluginRoot) {
|
|
1308
|
+
restorePublishHooksFromSource(pluginRoot);
|
|
1309
|
+
if (setup.readOnly) {
|
|
1310
|
+
prunePublishHooksForReadOnly(pluginRoot);
|
|
1311
|
+
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1345
1314
|
}
|
|
1346
1315
|
|
|
1347
1316
|
async function runUninstall(args) {
|
|
@@ -1379,6 +1348,21 @@ async function runUninstall(args) {
|
|
|
1379
1348
|
);
|
|
1380
1349
|
}
|
|
1381
1350
|
|
|
1351
|
+
async function runSetup(args) {
|
|
1352
|
+
const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
|
|
1353
|
+
if (!bash) {
|
|
1354
|
+
process.stderr.write("error: bash is required to run claude-smart setup.\n");
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
const script = join(PACKAGE_ROOT, "scripts", "setup-claude-smart.sh");
|
|
1358
|
+
if (!existsSync(script)) {
|
|
1359
|
+
process.stderr.write(`error: setup script not found at ${script}\n`);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
const code = await runChecked(bash, [script, ...args], { cwd: PACKAGE_ROOT });
|
|
1363
|
+
if (code !== 0) process.exit(code);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1382
1366
|
async function runInstall(args) {
|
|
1383
1367
|
if (parseHost(args) === "codex") {
|
|
1384
1368
|
await runInstallCodex(args);
|
|
@@ -1394,8 +1378,8 @@ async function runInstall(args) {
|
|
|
1394
1378
|
}
|
|
1395
1379
|
|
|
1396
1380
|
const source = parseSource(args);
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1381
|
+
const setup = configureReflexioSetup();
|
|
1382
|
+
const readOnly = setup.readOnly;
|
|
1399
1383
|
|
|
1400
1384
|
const steps = [
|
|
1401
1385
|
{ args: ["plugin", "marketplace", "add", source], label: "Adding marketplace…" },
|
|
@@ -1414,6 +1398,7 @@ async function runInstall(args) {
|
|
|
1414
1398
|
|
|
1415
1399
|
try {
|
|
1416
1400
|
const pluginRoot = await bootstrapClaudeCodeInstall();
|
|
1401
|
+
restorePublishHooksFromSource(pluginRoot);
|
|
1417
1402
|
if (readOnly) {
|
|
1418
1403
|
prunePublishHooksForReadOnly(pluginRoot);
|
|
1419
1404
|
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
@@ -1448,8 +1433,8 @@ async function runInstallCodex(args) {
|
|
|
1448
1433
|
process.stderr.write("error: 'codex' CLI not found on PATH. Install Codex first.\n");
|
|
1449
1434
|
process.exit(1);
|
|
1450
1435
|
}
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1436
|
+
const setup = configureReflexioSetup();
|
|
1437
|
+
const readOnly = setup.readOnly;
|
|
1453
1438
|
|
|
1454
1439
|
const marketplaceRoot = copyCodexMarketplace();
|
|
1455
1440
|
if (readOnly) {
|
|
@@ -1591,6 +1576,11 @@ async function main() {
|
|
|
1591
1576
|
return;
|
|
1592
1577
|
}
|
|
1593
1578
|
|
|
1579
|
+
if (cmd === "setup") {
|
|
1580
|
+
await runSetup(args.slice(1));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1594
1584
|
if (cmd === "uninstall") {
|
|
1595
1585
|
await runUninstall(args.slice(1));
|
|
1596
1586
|
return;
|
|
@@ -1620,4 +1610,5 @@ module.exports = {
|
|
|
1620
1610
|
patchCodexHooksForNode,
|
|
1621
1611
|
platformSupportError,
|
|
1622
1612
|
prunePublishHooksForReadOnly,
|
|
1613
|
+
restorePublishHooksFromSource,
|
|
1623
1614
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-smart",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin",
|
|
30
|
+
"scripts/setup-claude-smart.sh",
|
|
30
31
|
".agents/plugins/marketplace.json",
|
|
31
32
|
"plugin",
|
|
32
33
|
"!plugin/**/__pycache__",
|
package/plugin/pyproject.toml
CHANGED
package/plugin/scripts/_lib.sh
CHANGED
|
@@ -65,13 +65,13 @@ claude_smart_source_reflexio_env() {
|
|
|
65
65
|
case "$key" in
|
|
66
66
|
# Managed Reflexio config: file always wins so a rotated key/url overrides
|
|
67
67
|
# whatever a long-lived managed backend or dashboard process inherited.
|
|
68
|
-
REFLEXIO_URL|REFLEXIO_API_KEY)
|
|
68
|
+
REFLEXIO_URL|REFLEXIO_API_KEY|REFLEXIO_USER_ID|CLAUDE_SMART_READ_ONLY)
|
|
69
69
|
value="$(claude_smart_env_unquote "$value")"
|
|
70
70
|
export "$key=$value"
|
|
71
71
|
;;
|
|
72
72
|
# CLAUDE_SMART_* flags: respect anything the caller already exported so
|
|
73
73
|
# per-session overrides (e.g., a manual test) take precedence over the file.
|
|
74
|
-
CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|
|
|
74
|
+
CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
|
|
75
75
|
if [ -z "$(eval "printf '%s' \"\${$key:-}\"")" ]; then
|
|
76
76
|
value="$(claude_smart_env_unquote "$value")"
|
|
77
77
|
export "$key=$value"
|
|
@@ -97,10 +97,10 @@ claude_smart_is_internal_invocation_env() {
|
|
|
97
97
|
if [ "${CLAUDE_SMART_INTERNAL:-}" = "1" ]; then
|
|
98
98
|
return 0
|
|
99
99
|
fi
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
case "${CLAUDE_CODE_ENTRYPOINT:-}" in
|
|
101
|
+
""|"cli"|"claude-desktop") return 1 ;;
|
|
102
|
+
*) return 0 ;;
|
|
103
|
+
esac
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
claude_smart_emit_continue() {
|
|
@@ -183,11 +183,12 @@ function loadReflexioEnv() {
|
|
|
183
183
|
const managedReflexioKeys = new Set([
|
|
184
184
|
"REFLEXIO_URL",
|
|
185
185
|
"REFLEXIO_API_KEY",
|
|
186
|
+
"REFLEXIO_USER_ID",
|
|
187
|
+
"CLAUDE_SMART_READ_ONLY",
|
|
186
188
|
]);
|
|
187
189
|
const localConfigKeys = new Set([
|
|
188
190
|
"CLAUDE_SMART_USE_LOCAL_CLI",
|
|
189
191
|
"CLAUDE_SMART_USE_LOCAL_EMBEDDING",
|
|
190
|
-
"CLAUDE_SMART_READ_ONLY",
|
|
191
192
|
"CLAUDE_SMART_BACKEND_AUTOSTART",
|
|
192
193
|
"CLAUDE_SMART_DASHBOARD_AUTOSTART",
|
|
193
194
|
"CLAUDE_SMART_CLI_PATH",
|
|
@@ -105,11 +105,6 @@ install_complete() {
|
|
|
105
105
|
[ "$(cat "$SUCCESS_MARKER" 2>/dev/null || true)" = "$(install_fingerprint)" ] || return 1
|
|
106
106
|
command -v uv >/dev/null 2>&1 || return 1
|
|
107
107
|
[ -d "$PLUGIN_ROOT/.venv" ] || return 1
|
|
108
|
-
[ -f "$HOME/.reflexio/.env" ] || return 1
|
|
109
|
-
if [ -z "${REFLEXIO_API_KEY:-}" ]; then
|
|
110
|
-
grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
|
|
111
|
-
grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
|
|
112
|
-
fi
|
|
113
108
|
if [ -d "$PLUGIN_ROOT/dashboard" ]; then
|
|
114
109
|
[ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
|
|
115
110
|
fi
|
|
@@ -432,8 +427,6 @@ fi
|
|
|
432
427
|
# append our two opt-in flags there so `reflexio services start` picks
|
|
433
428
|
# them up regardless of which directory the user runs it from.
|
|
434
429
|
REFLEXIO_ENV="$HOME/.reflexio/.env"
|
|
435
|
-
mkdir -p "$(dirname "$REFLEXIO_ENV")"
|
|
436
|
-
touch "$REFLEXIO_ENV"
|
|
437
430
|
claude_smart_env_quote() {
|
|
438
431
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
439
432
|
}
|
|
@@ -472,27 +465,11 @@ claude_smart_env_upsert() {
|
|
|
472
465
|
fi
|
|
473
466
|
}
|
|
474
467
|
|
|
475
|
-
if [ "${CLAUDE_SMART_MANAGED_SETUP:-}" = "1" ] && [ -n "${REFLEXIO_API_KEY:-}" ]; then
|
|
476
|
-
REFLEXIO_URL="${REFLEXIO_URL:-https://www.reflexio.ai/}"
|
|
477
|
-
claude_smart_env_upsert REFLEXIO_URL "$REFLEXIO_URL"
|
|
478
|
-
claude_smart_env_upsert REFLEXIO_API_KEY "$REFLEXIO_API_KEY"
|
|
479
|
-
chmod 600 "$REFLEXIO_ENV"
|
|
480
|
-
echo "[claude-smart] configured managed Reflexio in $REFLEXIO_ENV" >&2
|
|
481
|
-
elif [ -z "${REFLEXIO_API_KEY:-}" ]; then
|
|
482
|
-
if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
|
|
483
|
-
printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
|
|
484
|
-
echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
|
|
485
|
-
fi
|
|
486
|
-
if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
|
|
487
|
-
printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
|
|
488
|
-
echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
|
|
489
|
-
fi
|
|
490
|
-
fi
|
|
491
468
|
# Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
|
|
492
469
|
# plugin backend port (8071). Matches the quoted and unquoted forms but
|
|
493
470
|
# requires paired quotes, so malformed or deliberately different values
|
|
494
471
|
# (e.g. a remote reflexio URL) are preserved.
|
|
495
|
-
if grep -qE '^REFLEXIO_URL=("http://localhost:8081/?"|http://localhost:8081/?)$' "$REFLEXIO_ENV"; then
|
|
472
|
+
if [ -f "$REFLEXIO_ENV" ] && grep -qE '^REFLEXIO_URL=("http://localhost:8081/?"|http://localhost:8081/?)$' "$REFLEXIO_ENV"; then
|
|
496
473
|
sed -i.bak -E \
|
|
497
474
|
-e 's|^REFLEXIO_URL="http://localhost:8081(/?)"$|REFLEXIO_URL="http://localhost:8071\1"|' \
|
|
498
475
|
-e 's|^REFLEXIO_URL=http://localhost:8081(/?)$|REFLEXIO_URL=http://localhost:8071\1|' \
|
|
@@ -119,40 +119,6 @@ def _latest_session_id() -> str | None:
|
|
|
119
119
|
return files[0].stem
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
def _seed_reflexio_env() -> list[str]:
|
|
123
|
-
"""Append the two local-provider flags to ``~/.reflexio/.env``, idempotently.
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
list[str]: Flag names that were newly appended (empty if already present).
|
|
127
|
-
"""
|
|
128
|
-
_REFLEXIO_ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
-
_REFLEXIO_ENV_PATH.touch(exist_ok=True)
|
|
130
|
-
existing = _REFLEXIO_ENV_PATH.read_text()
|
|
131
|
-
flags = (
|
|
132
|
-
"CLAUDE_SMART_USE_LOCAL_CLI",
|
|
133
|
-
"CLAUDE_SMART_USE_LOCAL_EMBEDDING",
|
|
134
|
-
)
|
|
135
|
-
missing = [f for f in flags if f"{f}=" not in existing]
|
|
136
|
-
if not missing:
|
|
137
|
-
return []
|
|
138
|
-
prefix = "" if not existing or existing.endswith("\n") else "\n"
|
|
139
|
-
with _REFLEXIO_ENV_PATH.open("a") as fh:
|
|
140
|
-
fh.write(prefix + "\n".join(f"{f}=1" for f in missing) + "\n")
|
|
141
|
-
_REFLEXIO_ENV_PATH.chmod(0o600)
|
|
142
|
-
return missing
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _seed_managed_reflexio_env(*, api_key: str, reflexio_url: str) -> list[str]:
|
|
146
|
-
"""Write managed Reflexio connection settings to ``~/.reflexio/.env``."""
|
|
147
|
-
return env_config.set_env_vars(
|
|
148
|
-
_REFLEXIO_ENV_PATH,
|
|
149
|
-
{
|
|
150
|
-
env_config.REFLEXIO_URL_ENV: reflexio_url,
|
|
151
|
-
env_config.REFLEXIO_API_KEY_ENV: api_key,
|
|
152
|
-
},
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
122
|
def _semver_tuple(path: Path) -> tuple[int, int, int] | None:
|
|
157
123
|
parts = path.name.split(".", 2)
|
|
158
124
|
if len(parts) != 3:
|
|
@@ -356,6 +322,21 @@ def _prune_publish_hooks_for_read_only(plugin_root: Path) -> None:
|
|
|
356
322
|
hook_path.write_text(json.dumps(parsed, indent=2) + "\n")
|
|
357
323
|
|
|
358
324
|
|
|
325
|
+
def _restore_publish_hooks_from_source(plugin_root: Path) -> None:
|
|
326
|
+
"""Restore full hook manifests before applying the current read-only state."""
|
|
327
|
+
source_hooks_dir = _PLUGIN_ROOT / "hooks"
|
|
328
|
+
target_hooks_dir = plugin_root / "hooks"
|
|
329
|
+
for hook_file in ("hooks.json", "codex-hooks.json"):
|
|
330
|
+
source_path = source_hooks_dir / hook_file
|
|
331
|
+
target_path = target_hooks_dir / hook_file
|
|
332
|
+
if (
|
|
333
|
+
source_path.is_file()
|
|
334
|
+
and target_path.is_file()
|
|
335
|
+
and source_path.resolve() != target_path.resolve()
|
|
336
|
+
):
|
|
337
|
+
shutil.copyfile(source_path, target_path)
|
|
338
|
+
|
|
339
|
+
|
|
359
340
|
def _run_codex(args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
360
341
|
"""Invoke the ``codex`` CLI with a hard timeout.
|
|
361
342
|
|
|
@@ -823,38 +804,54 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
|
|
|
823
804
|
return False, output or "Codex CLI does not expose plugin marketplace commands"
|
|
824
805
|
|
|
825
806
|
|
|
826
|
-
def _configure_reflexio_setup(
|
|
827
|
-
|
|
828
|
-
|
|
807
|
+
def _configure_reflexio_setup() -> bool:
|
|
808
|
+
"""Load setup state from ``~/.reflexio/.env`` without writing local defaults.
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
bool: Whether read-only mode is enabled.
|
|
812
|
+
"""
|
|
813
|
+
env_config.load_reflexio_env(_REFLEXIO_ENV_PATH)
|
|
814
|
+
try:
|
|
815
|
+
env_text = _REFLEXIO_ENV_PATH.read_text()
|
|
816
|
+
except OSError:
|
|
817
|
+
env_text = ""
|
|
818
|
+
read_only_value = ""
|
|
819
|
+
file_api_key = ""
|
|
820
|
+
file_url = ""
|
|
821
|
+
for line in env_text.splitlines():
|
|
822
|
+
parsed = env_config.parse_env_line(line)
|
|
823
|
+
if parsed is None:
|
|
824
|
+
continue
|
|
825
|
+
key, value = parsed
|
|
826
|
+
if key == env_config.REFLEXIO_API_KEY_ENV:
|
|
827
|
+
file_api_key = value
|
|
828
|
+
elif key == env_config.REFLEXIO_URL_ENV:
|
|
829
|
+
file_url = value
|
|
830
|
+
elif key == "REFLEXIO_USER_ID":
|
|
831
|
+
os.environ[key] = value
|
|
832
|
+
elif key == env_config.CLAUDE_SMART_READ_ONLY_ENV:
|
|
833
|
+
read_only_value = value
|
|
834
|
+
api_key = (
|
|
835
|
+
file_api_key or os.environ.get(env_config.REFLEXIO_API_KEY_ENV, "")
|
|
836
|
+
).strip()
|
|
837
|
+
read_only = read_only_value.strip().lower() in {"1", "true", "yes", "on"}
|
|
829
838
|
if api_key:
|
|
830
839
|
reflexio_url = (
|
|
831
|
-
|
|
840
|
+
file_url
|
|
841
|
+
or os.environ.get(env_config.REFLEXIO_URL_ENV, _MANAGED_REFLEXIO_URL)
|
|
832
842
|
).strip()
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
env_config.set_env_vars(
|
|
837
|
-
_REFLEXIO_ENV_PATH,
|
|
838
|
-
{env_config.CLAUDE_SMART_READ_ONLY_ENV: "1"},
|
|
839
|
-
)
|
|
840
|
-
)
|
|
841
|
-
changed = ", ".join(added) if added else "managed Reflexio settings"
|
|
843
|
+
os.environ[env_config.REFLEXIO_URL_ENV] = reflexio_url
|
|
844
|
+
os.environ[env_config.REFLEXIO_API_KEY_ENV] = api_key
|
|
845
|
+
os.environ["CLAUDE_SMART_MANAGED_SETUP"] = "1"
|
|
842
846
|
sys.stdout.write(
|
|
843
|
-
f"
|
|
844
|
-
f"(
|
|
845
|
-
)
|
|
846
|
-
return
|
|
847
|
-
|
|
848
|
-
added = _seed_reflexio_env()
|
|
849
|
-
if read_only:
|
|
850
|
-
added.extend(
|
|
851
|
-
env_config.set_env_vars(
|
|
852
|
-
_REFLEXIO_ENV_PATH,
|
|
853
|
-
{env_config.CLAUDE_SMART_READ_ONLY_ENV: "1"},
|
|
854
|
-
)
|
|
847
|
+
f"Using managed Reflexio at {reflexio_url} "
|
|
848
|
+
f"(API key {env_config.mask_secret(api_key)}).\n"
|
|
855
849
|
)
|
|
856
|
-
|
|
857
|
-
|
|
850
|
+
else:
|
|
851
|
+
os.environ.pop(env_config.REFLEXIO_URL_ENV, None)
|
|
852
|
+
os.environ.pop(env_config.REFLEXIO_API_KEY_ENV, None)
|
|
853
|
+
os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None)
|
|
854
|
+
return read_only
|
|
858
855
|
|
|
859
856
|
|
|
860
857
|
def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
@@ -878,7 +875,7 @@ def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
|
878
875
|
"error: 'uv' not found on PATH. Install uv or restart your shell.\n"
|
|
879
876
|
)
|
|
880
877
|
return 1
|
|
881
|
-
_configure_reflexio_setup(
|
|
878
|
+
read_only = _configure_reflexio_setup()
|
|
882
879
|
|
|
883
880
|
missing = _missing_codex_marketplace_files(_REPO_ROOT)
|
|
884
881
|
if missing:
|
|
@@ -889,11 +886,8 @@ def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
|
889
886
|
)
|
|
890
887
|
return 1
|
|
891
888
|
marketplace_root = _prepare_codex_local_marketplace()
|
|
892
|
-
read_only = bool(getattr(args, "read_only", False))
|
|
893
889
|
if read_only:
|
|
894
|
-
_prune_publish_hooks_for_read_only(
|
|
895
|
-
marketplace_root / _CODEX_LOCAL_PLUGIN_PATH
|
|
896
|
-
)
|
|
890
|
+
_prune_publish_hooks_for_read_only(marketplace_root / _CODEX_LOCAL_PLUGIN_PATH)
|
|
897
891
|
|
|
898
892
|
hooks_ok, hooks_msg = _enable_codex_plugin_hooks()
|
|
899
893
|
if hooks_ok:
|
|
@@ -988,7 +982,7 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
988
982
|
"Install Claude Code first: https://claude.com/claude-code\n"
|
|
989
983
|
)
|
|
990
984
|
return 1
|
|
991
|
-
_configure_reflexio_setup(
|
|
985
|
+
read_only = _configure_reflexio_setup()
|
|
992
986
|
|
|
993
987
|
for cmd in (
|
|
994
988
|
["claude", "plugin", "marketplace", "add", args.source],
|
|
@@ -1009,7 +1003,8 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
1009
1003
|
"Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n"
|
|
1010
1004
|
)
|
|
1011
1005
|
return 1
|
|
1012
|
-
|
|
1006
|
+
_restore_publish_hooks_from_source(Path(message))
|
|
1007
|
+
if read_only:
|
|
1013
1008
|
_prune_publish_hooks_for_read_only(Path(message))
|
|
1014
1009
|
sys.stdout.write(
|
|
1015
1010
|
"Installed read-only hook manifest; publish interactions hooks are disabled.\n"
|
|
@@ -1041,6 +1036,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
1041
1036
|
)
|
|
1042
1037
|
return 1
|
|
1043
1038
|
|
|
1039
|
+
read_only = _configure_reflexio_setup()
|
|
1044
1040
|
cmd = ["claude", "plugin", "update", _PLUGIN_SPEC]
|
|
1045
1041
|
try:
|
|
1046
1042
|
subprocess.run(cmd, check=True)
|
|
@@ -1049,8 +1045,14 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
1049
1045
|
return exc.returncode or 1
|
|
1050
1046
|
|
|
1051
1047
|
sys.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n")
|
|
1052
|
-
|
|
1053
|
-
|
|
1048
|
+
plugin_root = _find_claude_code_plugin_root()
|
|
1049
|
+
if plugin_root is not None:
|
|
1050
|
+
_restore_publish_hooks_from_source(plugin_root)
|
|
1051
|
+
if read_only:
|
|
1052
|
+
_prune_publish_hooks_for_read_only(plugin_root)
|
|
1053
|
+
sys.stdout.write(
|
|
1054
|
+
"Installed read-only hook manifest; publish interactions hooks are disabled.\n"
|
|
1055
|
+
)
|
|
1054
1056
|
return 0
|
|
1055
1057
|
|
|
1056
1058
|
|
|
@@ -1143,7 +1145,7 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
1143
1145
|
Returns:
|
|
1144
1146
|
int: 0 on success.
|
|
1145
1147
|
"""
|
|
1146
|
-
project_id = args.project or ids.
|
|
1148
|
+
project_id = args.project or ids.resolve_user_id()
|
|
1147
1149
|
adapter = Adapter()
|
|
1148
1150
|
user_playbooks, agent_playbooks, profiles = adapter.fetch_all(
|
|
1149
1151
|
project_id=project_id,
|
|
@@ -1202,7 +1204,7 @@ def cmd_learn(args: argparse.Namespace) -> int:
|
|
|
1202
1204
|
if not session_id:
|
|
1203
1205
|
sys.stdout.write("No active claude-smart session buffer found.\n")
|
|
1204
1206
|
return 0
|
|
1205
|
-
project_id = args.project or ids.
|
|
1207
|
+
project_id = args.project or ids.resolve_user_id()
|
|
1206
1208
|
|
|
1207
1209
|
note = (args.note or "").strip()
|
|
1208
1210
|
if note:
|
|
@@ -1832,36 +1834,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1832
1834
|
default=_DEFAULT_MARKETPLACE_SOURCE,
|
|
1833
1835
|
help="Marketplace ref — GitHub owner/repo, or a local directory path",
|
|
1834
1836
|
)
|
|
1835
|
-
inst.add_argument(
|
|
1836
|
-
"--api-key",
|
|
1837
|
-
default="",
|
|
1838
|
-
help="Configure managed Reflexio with this API key instead of local mode",
|
|
1839
|
-
)
|
|
1840
|
-
inst.add_argument(
|
|
1841
|
-
"--reflexio-url",
|
|
1842
|
-
default=_MANAGED_REFLEXIO_URL,
|
|
1843
|
-
help="Managed Reflexio URL used only when --api-key is provided",
|
|
1844
|
-
)
|
|
1845
|
-
inst.add_argument(
|
|
1846
|
-
"--read-only",
|
|
1847
|
-
action="store_const",
|
|
1848
|
-
const=True,
|
|
1849
|
-
default=None,
|
|
1850
|
-
help="Install without publish interactions hooks",
|
|
1851
|
-
)
|
|
1852
1837
|
inst.set_defaults(func=cmd_install)
|
|
1853
1838
|
|
|
1854
1839
|
upd = sub.add_parser("update", help="Update claude-smart to the latest version")
|
|
1855
|
-
upd.add_argument(
|
|
1856
|
-
"--api-key",
|
|
1857
|
-
default="",
|
|
1858
|
-
help="Configure managed Reflexio with this API key after updating",
|
|
1859
|
-
)
|
|
1860
|
-
upd.add_argument(
|
|
1861
|
-
"--reflexio-url",
|
|
1862
|
-
default=_MANAGED_REFLEXIO_URL,
|
|
1863
|
-
help="Managed Reflexio URL used only when --api-key is provided",
|
|
1864
|
-
)
|
|
1865
1840
|
upd.set_defaults(func=cmd_update)
|
|
1866
1841
|
|
|
1867
1842
|
uni = sub.add_parser("uninstall", help="Remove claude-smart")
|
|
@@ -36,7 +36,7 @@ def handle(payload: dict[str, Any]) -> None:
|
|
|
36
36
|
hook.emit_continue()
|
|
37
37
|
return
|
|
38
38
|
|
|
39
|
-
project_id = ids.
|
|
39
|
+
project_id = ids.resolve_user_id(payload.get("cwd"))
|
|
40
40
|
try:
|
|
41
41
|
emitted = context_inject.emit_context(
|
|
42
42
|
session_id=session_id,
|
|
@@ -51,7 +51,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
51
51
|
session_id = payload.get("session_id")
|
|
52
52
|
if not session_id:
|
|
53
53
|
return None
|
|
54
|
-
project_id = ids.
|
|
54
|
+
project_id = ids.resolve_user_id(payload.get("cwd"))
|
|
55
55
|
|
|
56
56
|
_maybe_synthesize_assistant_anchor(
|
|
57
57
|
session_id=session_id,
|
|
@@ -9,7 +9,15 @@ import time
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from claude_smart import
|
|
12
|
+
from claude_smart import (
|
|
13
|
+
cs_cite,
|
|
14
|
+
env_config,
|
|
15
|
+
ids,
|
|
16
|
+
internal_call,
|
|
17
|
+
publish,
|
|
18
|
+
runtime,
|
|
19
|
+
state,
|
|
20
|
+
)
|
|
13
21
|
|
|
14
22
|
_LOGGER = logging.getLogger(__name__)
|
|
15
23
|
|
|
@@ -354,7 +362,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
354
362
|
# without this placeholder, those tools would be misattributed to the
|
|
355
363
|
# next assistant turn.
|
|
356
364
|
transcript_path = payload.get("transcript_path")
|
|
357
|
-
project_id = ids.
|
|
365
|
+
project_id = ids.resolve_user_id(payload.get("cwd"))
|
|
358
366
|
|
|
359
367
|
entries: list[dict[str, Any]] = []
|
|
360
368
|
if transcript_path:
|
|
@@ -362,9 +370,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
362
370
|
if path.is_file():
|
|
363
371
|
entries = _load_transcript_with_retry(path)
|
|
364
372
|
|
|
365
|
-
prompt = payload.get("prompt") or (
|
|
366
|
-
_scan_transcript_for_user_text(entries) if runtime.is_codex() else ""
|
|
367
|
-
)
|
|
373
|
+
prompt = payload.get("prompt") or _scan_transcript_for_user_text(entries)
|
|
368
374
|
if runtime.is_codex() and internal_call.is_codex_internal_prompt(prompt):
|
|
369
375
|
return None
|
|
370
376
|
|
|
@@ -393,13 +399,24 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
393
399
|
plan_decisions = _scan_transcript_for_plan_decisions(entries)
|
|
394
400
|
|
|
395
401
|
now = int(time.time())
|
|
396
|
-
|
|
402
|
+
if plan_decisions:
|
|
403
|
+
for decision_text in plan_decisions:
|
|
404
|
+
state.append(
|
|
405
|
+
session_id,
|
|
406
|
+
{
|
|
407
|
+
"ts": now,
|
|
408
|
+
"role": "User",
|
|
409
|
+
"content": decision_text,
|
|
410
|
+
"user_id": project_id,
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
elif prompt and not _has_unpublished_user_turn(session_id):
|
|
397
414
|
state.append(
|
|
398
415
|
session_id,
|
|
399
416
|
{
|
|
400
417
|
"ts": now,
|
|
401
418
|
"role": "User",
|
|
402
|
-
"content":
|
|
419
|
+
"content": prompt,
|
|
403
420
|
"user_id": project_id,
|
|
404
421
|
},
|
|
405
422
|
)
|
|
@@ -68,7 +68,26 @@ def _read_stdin_json() -> dict[str, Any]:
|
|
|
68
68
|
except json.JSONDecodeError as exc:
|
|
69
69
|
_LOGGER.debug("stdin JSON decode failed: %s", exc)
|
|
70
70
|
return {}
|
|
71
|
-
return parsed if isinstance(parsed, dict) else {}
|
|
71
|
+
return _normalize_payload_keys(parsed) if isinstance(parsed, dict) else {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _normalize_payload_keys(payload: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
"""Accept both classic Claude Code and Desktop/Cowork hook key shapes."""
|
|
76
|
+
aliases = {
|
|
77
|
+
"sessionId": "session_id",
|
|
78
|
+
"transcriptPath": "transcript_path",
|
|
79
|
+
"toolName": "tool_name",
|
|
80
|
+
"toolInput": "tool_input",
|
|
81
|
+
"toolResponse": "tool_response",
|
|
82
|
+
"lastAssistantMessage": "last_assistant_message",
|
|
83
|
+
"workingDirectory": "cwd",
|
|
84
|
+
"currentWorkingDirectory": "cwd",
|
|
85
|
+
}
|
|
86
|
+
normalized = dict(payload)
|
|
87
|
+
for source, target in aliases.items():
|
|
88
|
+
if target not in normalized and source in normalized:
|
|
89
|
+
normalized[target] = normalized[source]
|
|
90
|
+
return normalized
|
|
72
91
|
|
|
73
92
|
|
|
74
93
|
def emit_continue() -> None:
|
|
@@ -163,8 +163,8 @@ def log_path() -> Path:
|
|
|
163
163
|
|
|
164
164
|
Resolution order:
|
|
165
165
|
|
|
166
|
-
1. ``CLAUDE_SMART_HOOK_LOG`` env var —
|
|
167
|
-
|
|
166
|
+
1. ``CLAUDE_SMART_HOOK_LOG`` env var — boolean-like values enable the
|
|
167
|
+
default log, while path-like values redirect it.
|
|
168
168
|
2. ``_LOG_PATH`` module attribute — what monkeypatched tests override.
|
|
169
169
|
3. ``~/.claude-smart/hook.log`` — the default.
|
|
170
170
|
|
|
@@ -173,6 +173,11 @@ def log_path() -> Path:
|
|
|
173
173
|
"""
|
|
174
174
|
override = os.environ.get("CLAUDE_SMART_HOOK_LOG")
|
|
175
175
|
if override:
|
|
176
|
+
override = override.strip()
|
|
177
|
+
if not override:
|
|
178
|
+
return _LOG_PATH
|
|
179
|
+
if override.lower() in {"1", "true", "yes", "on"}:
|
|
180
|
+
return _LOG_PATH
|
|
176
181
|
return Path(override)
|
|
177
182
|
return _LOG_PATH
|
|
178
183
|
|
|
@@ -21,8 +21,12 @@ import os
|
|
|
21
21
|
import subprocess # noqa: S404 — git invocation with a fixed flag set.
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
|
+
from claude_smart import env_config
|
|
25
|
+
|
|
24
26
|
_LOGGER = logging.getLogger(__name__)
|
|
25
27
|
|
|
28
|
+
_USER_ID_OVERRIDE_ENV = "REFLEXIO_USER_ID"
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
def resolve_project_id(cwd: str | os.PathLike[str] | None = None) -> str:
|
|
28
32
|
"""Return a stable project identifier for the given working directory.
|
|
@@ -57,3 +61,25 @@ def resolve_project_id(cwd: str | os.PathLike[str] | None = None) -> str:
|
|
|
57
61
|
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
|
58
62
|
_LOGGER.debug("git toplevel resolution failed: %s", exc)
|
|
59
63
|
return base.name or "unknown-project"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_user_id(cwd: str | os.PathLike[str] | None = None) -> str:
|
|
67
|
+
"""Return the Reflexio ``user_id`` for the given working directory.
|
|
68
|
+
|
|
69
|
+
Honors the ``REFLEXIO_USER_ID`` env var as an explicit override (e.g. so a
|
|
70
|
+
user can point multiple projects at a single Reflexio identity), and
|
|
71
|
+
otherwise falls back to :func:`resolve_project_id`. The env var is
|
|
72
|
+
loaded from ``~/.reflexio/.env`` before reading the process environment so
|
|
73
|
+
CLI commands and hooks use the same identity resolution.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
cwd: Working directory to resolve when falling back to the project id.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: The override value if set and non-empty, else the project id.
|
|
80
|
+
"""
|
|
81
|
+
env_config.load_reflexio_env()
|
|
82
|
+
override = os.environ.get(_USER_ID_OVERRIDE_ENV, "").strip()
|
|
83
|
+
if override:
|
|
84
|
+
return override
|
|
85
|
+
return resolve_project_id(cwd)
|
|
@@ -19,10 +19,10 @@ Two distinct sources of unwanted hook fires:
|
|
|
19
19
|
tool..."`` into reflexio as a fake user interaction.
|
|
20
20
|
|
|
21
21
|
Detection signals, OR'd:
|
|
22
|
-
- ``CLAUDE_CODE_ENTRYPOINT`` is anything other than
|
|
23
|
-
|
|
24
|
-
``sdk-cli`` (and the SDKs may set other values). This
|
|
25
|
-
case (2) for any third-party tool, not just claude-mem.
|
|
22
|
+
- ``CLAUDE_CODE_ENTRYPOINT`` is anything other than an interactive entrypoint
|
|
23
|
+
(``"cli"`` or Claude Desktop's ``"claude-desktop"``). Headless
|
|
24
|
+
``claude -p`` sets ``sdk-cli`` (and the SDKs may set other values). This
|
|
25
|
+
catches case (2) for any third-party tool, not just claude-mem.
|
|
26
26
|
- Env var ``CLAUDE_SMART_INTERNAL=1``, set by reflexio's provider
|
|
27
27
|
before spawning ``claude``. Belt-and-suspenders for case (1) in
|
|
28
28
|
case the entrypoint check ever misses a future SDK variant.
|
|
@@ -46,7 +46,7 @@ from typing import Any
|
|
|
46
46
|
from claude_smart import runtime
|
|
47
47
|
|
|
48
48
|
_ENTRYPOINT_VAR = "CLAUDE_CODE_ENTRYPOINT"
|
|
49
|
-
|
|
49
|
+
_INTERACTIVE_ENTRYPOINTS = frozenset({"cli", "claude-desktop"})
|
|
50
50
|
_CODEX_TITLE_PROMPT_PREFIX = (
|
|
51
51
|
"You are a helpful assistant. You will be presented with a user prompt, "
|
|
52
52
|
"and your job is to provide a short title for a task"
|
|
@@ -92,7 +92,7 @@ def is_internal_invocation(payload: dict[str, Any]) -> bool:
|
|
|
92
92
|
if runtime.is_internal_invocation_env():
|
|
93
93
|
return True
|
|
94
94
|
entrypoint = os.environ.get(_ENTRYPOINT_VAR)
|
|
95
|
-
if entrypoint and entrypoint
|
|
95
|
+
if entrypoint and entrypoint not in _INTERACTIVE_ENTRYPOINTS:
|
|
96
96
|
return True
|
|
97
97
|
if runtime.is_codex() and is_codex_internal_prompt(payload.get("prompt")):
|
|
98
98
|
return True
|
package/plugin/uv.lock
CHANGED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Interactive setup for claude-smart's shared ~/.reflexio/.env.
|
|
3
|
+
#
|
|
4
|
+
# Plain installs stay local and do not need this script. Use this when a user
|
|
5
|
+
# wants managed Reflexio, read-only mode, global sharing, or to switch back to
|
|
6
|
+
# local mode after a managed setup.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
REPO_ROOT="$(cd "$HERE/.." && pwd)"
|
|
12
|
+
INSTALLER="$REPO_ROOT/bin/claude-smart.js"
|
|
13
|
+
REFLEXIO_ENV="${REFLEXIO_ENV_PATH:-$HOME/.reflexio/.env}"
|
|
14
|
+
MANAGED_REFLEXIO_URL="https://www.reflexio.ai/"
|
|
15
|
+
GLOBAL_USER_ID="global_user"
|
|
16
|
+
|
|
17
|
+
log() { printf '[claude-smart setup] %s\n' "$*" >&2; }
|
|
18
|
+
|
|
19
|
+
env_unquote() {
|
|
20
|
+
local value first last
|
|
21
|
+
value="$1"
|
|
22
|
+
first="${value%"${value#?}"}"
|
|
23
|
+
last="${value#"${value%?}"}"
|
|
24
|
+
if { [ "$first" = '"' ] && [ "$last" = '"' ]; } || { [ "$first" = "'" ] && [ "$last" = "'" ]; }; then
|
|
25
|
+
value="${value#?}"
|
|
26
|
+
value="${value%?}"
|
|
27
|
+
fi
|
|
28
|
+
printf '%s\n' "$value"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
env_quote() {
|
|
32
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get_env_value() {
|
|
36
|
+
local key line raw_key value
|
|
37
|
+
key="$1"
|
|
38
|
+
[ -f "$REFLEXIO_ENV" ] || return 1
|
|
39
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
40
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
41
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
42
|
+
[ -n "$line" ] || continue
|
|
43
|
+
case "$line" in
|
|
44
|
+
\#*) continue ;;
|
|
45
|
+
export\ *) line="${line#export }" ;;
|
|
46
|
+
esac
|
|
47
|
+
raw_key="${line%%=*}"
|
|
48
|
+
[ "$raw_key" != "$line" ] || continue
|
|
49
|
+
raw_key="${raw_key%"${raw_key##*[![:space:]]}"}"
|
|
50
|
+
[ "$raw_key" = "$key" ] || continue
|
|
51
|
+
value="${line#*=}"
|
|
52
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
53
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
54
|
+
env_unquote "$value"
|
|
55
|
+
return 0
|
|
56
|
+
done < "$REFLEXIO_ENV"
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
remove_env_keys() {
|
|
61
|
+
local tmp line key raw_key remove target
|
|
62
|
+
[ -f "$REFLEXIO_ENV" ] || return 0
|
|
63
|
+
tmp="$(mktemp "${REFLEXIO_ENV}.tmp.XXXXXX")"
|
|
64
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
65
|
+
raw_key="$line"
|
|
66
|
+
raw_key="${raw_key#"${raw_key%%[![:space:]]*}"}"
|
|
67
|
+
case "$raw_key" in
|
|
68
|
+
export\ *) raw_key="${raw_key#export }" ;;
|
|
69
|
+
esac
|
|
70
|
+
key="${raw_key%%=*}"
|
|
71
|
+
key="${key%"${key##*[![:space:]]}"}"
|
|
72
|
+
remove=0
|
|
73
|
+
if [ "$key" != "$raw_key" ]; then
|
|
74
|
+
if [ "$#" -gt 0 ]; then
|
|
75
|
+
for target in "$@"; do
|
|
76
|
+
[ "$key" = "$target" ] && remove=1
|
|
77
|
+
done
|
|
78
|
+
else
|
|
79
|
+
case "$key" in
|
|
80
|
+
REFLEXIO_API_KEY|REFLEXIO_URL|REFLEXIO_USER_ID|CLAUDE_SMART_READ_ONLY)
|
|
81
|
+
remove=1
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
[ "$remove" = "1" ] || printf '%s\n' "$line" >> "$tmp"
|
|
87
|
+
done < "$REFLEXIO_ENV"
|
|
88
|
+
|
|
89
|
+
if ! grep -q '[^[:space:]]' "$tmp"; then
|
|
90
|
+
rm -f "$tmp" "$REFLEXIO_ENV"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
mv "$tmp" "$REFLEXIO_ENV"
|
|
94
|
+
chmod 600 "$REFLEXIO_ENV"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
append_env_value() {
|
|
98
|
+
local key value quoted
|
|
99
|
+
key="$1"
|
|
100
|
+
value="$2"
|
|
101
|
+
quoted="$(env_quote "$value")"
|
|
102
|
+
mkdir -p "$(dirname "$REFLEXIO_ENV")"
|
|
103
|
+
touch "$REFLEXIO_ENV"
|
|
104
|
+
chmod 600 "$REFLEXIO_ENV"
|
|
105
|
+
printf '%s="%s"\n' "$key" "$quoted" >> "$REFLEXIO_ENV"
|
|
106
|
+
chmod 600 "$REFLEXIO_ENV"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mask_secret() {
|
|
110
|
+
local value len suffix
|
|
111
|
+
value="$1"
|
|
112
|
+
len="${#value}"
|
|
113
|
+
if [ "$len" -eq 0 ]; then
|
|
114
|
+
printf '%s\n' ''
|
|
115
|
+
elif [ "$len" -le 4 ]; then
|
|
116
|
+
printf '****%s\n' "$value"
|
|
117
|
+
else
|
|
118
|
+
suffix="${value: -4}"
|
|
119
|
+
printf '****%s\n' "$suffix"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
prompt() {
|
|
124
|
+
local message default answer
|
|
125
|
+
message="$1"
|
|
126
|
+
default="$2"
|
|
127
|
+
if [ -n "$default" ]; then
|
|
128
|
+
printf '%s [%s]: ' "$message" "$default" >&2
|
|
129
|
+
else
|
|
130
|
+
printf '%s: ' "$message" >&2
|
|
131
|
+
fi
|
|
132
|
+
IFS= read -r answer
|
|
133
|
+
if [ -z "$answer" ]; then
|
|
134
|
+
answer="$default"
|
|
135
|
+
fi
|
|
136
|
+
printf '%s\n' "$answer"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
prompt_api_key() {
|
|
140
|
+
local existing masked answer
|
|
141
|
+
existing="$1"
|
|
142
|
+
masked="$(mask_secret "$existing")"
|
|
143
|
+
while :; do
|
|
144
|
+
if [ -n "$existing" ]; then
|
|
145
|
+
printf 'Reflexio API key [%s; press Enter to keep]: ' "$masked" >&2
|
|
146
|
+
else
|
|
147
|
+
printf 'Reflexio API key: ' >&2
|
|
148
|
+
fi
|
|
149
|
+
if [ -t 0 ]; then
|
|
150
|
+
IFS= read -rs answer
|
|
151
|
+
printf '\n' >&2
|
|
152
|
+
else
|
|
153
|
+
IFS= read -r answer
|
|
154
|
+
fi
|
|
155
|
+
if [ -z "$answer" ] && [ -n "$existing" ]; then
|
|
156
|
+
answer="$existing"
|
|
157
|
+
fi
|
|
158
|
+
if [ -z "$answer" ]; then
|
|
159
|
+
log "API key is required for managed Reflexio."
|
|
160
|
+
continue
|
|
161
|
+
fi
|
|
162
|
+
case "$answer" in
|
|
163
|
+
*[[:space:]]*)
|
|
164
|
+
log "API key must not contain whitespace."
|
|
165
|
+
continue
|
|
166
|
+
;;
|
|
167
|
+
esac
|
|
168
|
+
printf '%s\n' "$answer"
|
|
169
|
+
return 0
|
|
170
|
+
done
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
normalize_host() {
|
|
174
|
+
case "$1" in
|
|
175
|
+
1|claude|claude-code|Claude*) printf 'claude-code\n' ;;
|
|
176
|
+
2|codex|Codex*) printf 'codex\n' ;;
|
|
177
|
+
3|both|Both*) printf 'both\n' ;;
|
|
178
|
+
*) return 1 ;;
|
|
179
|
+
esac
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
normalize_mode() {
|
|
183
|
+
case "$1" in
|
|
184
|
+
1|local|Local*) printf 'local\n' ;;
|
|
185
|
+
2|managed|remote|Remote*) printf 'managed\n' ;;
|
|
186
|
+
*) return 1 ;;
|
|
187
|
+
esac
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
normalize_yes_no() {
|
|
191
|
+
case "$1" in
|
|
192
|
+
y|Y|yes|Yes|YES|true|1) printf 'yes\n' ;;
|
|
193
|
+
n|N|no|No|NO|false|0) printf 'no\n' ;;
|
|
194
|
+
*) return 1 ;;
|
|
195
|
+
esac
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
normalize_scope() {
|
|
199
|
+
case "$1" in
|
|
200
|
+
1|project|Project*) printf 'project\n' ;;
|
|
201
|
+
2|global|Global*) printf 'global\n' ;;
|
|
202
|
+
*) return 1 ;;
|
|
203
|
+
esac
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
is_local_url() {
|
|
207
|
+
case "$1" in
|
|
208
|
+
http://localhost|http://localhost/|http://localhost:*|http://127.0.0.1|http://127.0.0.1/|http://127.0.0.1:*|http://0.0.0.0|http://0.0.0.0/|http://0.0.0.0:*)
|
|
209
|
+
return 0
|
|
210
|
+
;;
|
|
211
|
+
*)
|
|
212
|
+
return 1
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
prompt_normalized() {
|
|
218
|
+
local label default normalizer answer normalized
|
|
219
|
+
label="$1"
|
|
220
|
+
default="$2"
|
|
221
|
+
normalizer="$3"
|
|
222
|
+
while :; do
|
|
223
|
+
answer="$(prompt "$label" "$default")"
|
|
224
|
+
if normalized="$("$normalizer" "$answer")"; then
|
|
225
|
+
printf '%s\n' "$normalized"
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
log "Invalid choice: $answer"
|
|
229
|
+
done
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
install_for_host() {
|
|
233
|
+
local host node_bin
|
|
234
|
+
host="$1"
|
|
235
|
+
if [ "${CLAUDE_SMART_SETUP_NO_INSTALL:-}" = "1" ]; then
|
|
236
|
+
log "skipping plugin install because CLAUDE_SMART_SETUP_NO_INSTALL=1"
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
node_bin="${NODE:-node}"
|
|
240
|
+
if [ ! -f "$INSTALLER" ]; then
|
|
241
|
+
log "ERROR: installer not found at $INSTALLER"
|
|
242
|
+
exit 1
|
|
243
|
+
fi
|
|
244
|
+
case "$host" in
|
|
245
|
+
claude-code)
|
|
246
|
+
"$node_bin" "$INSTALLER" install
|
|
247
|
+
;;
|
|
248
|
+
codex)
|
|
249
|
+
"$node_bin" "$INSTALLER" install --host codex
|
|
250
|
+
;;
|
|
251
|
+
both)
|
|
252
|
+
"$node_bin" "$INSTALLER" install
|
|
253
|
+
"$node_bin" "$INSTALLER" install --host codex
|
|
254
|
+
;;
|
|
255
|
+
esac
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main() {
|
|
259
|
+
local existing_api_key existing_url existing_user_id existing_read_only
|
|
260
|
+
local default_mode default_read_only default_scope host mode api_key read_only scope
|
|
261
|
+
local managed_url managed_user_id
|
|
262
|
+
|
|
263
|
+
existing_api_key="$(get_env_value REFLEXIO_API_KEY || true)"
|
|
264
|
+
existing_url="$(get_env_value REFLEXIO_URL || true)"
|
|
265
|
+
existing_user_id="$(get_env_value REFLEXIO_USER_ID || true)"
|
|
266
|
+
existing_read_only="$(get_env_value CLAUDE_SMART_READ_ONLY || true)"
|
|
267
|
+
|
|
268
|
+
default_mode="local"
|
|
269
|
+
if [ -n "$existing_api_key" ] || [ -n "$existing_url" ]; then
|
|
270
|
+
default_mode="managed"
|
|
271
|
+
fi
|
|
272
|
+
default_read_only="no"
|
|
273
|
+
case "$existing_read_only" in
|
|
274
|
+
1|true|yes|on) default_read_only="yes" ;;
|
|
275
|
+
esac
|
|
276
|
+
default_scope="project"
|
|
277
|
+
if [ -n "$existing_user_id" ]; then
|
|
278
|
+
default_scope="global"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
host="$(prompt_normalized "Host (1=Claude Code, 2=Codex, 3=both)" "claude-code" normalize_host)"
|
|
282
|
+
mode="$(prompt_normalized "Setup mode (1=local, 2=managed Reflexio)" "$default_mode" normalize_mode)"
|
|
283
|
+
|
|
284
|
+
if [ "$mode" = "local" ]; then
|
|
285
|
+
if [ -f "$REFLEXIO_ENV" ]; then
|
|
286
|
+
remove_env_keys
|
|
287
|
+
log "removed managed Reflexio settings from $REFLEXIO_ENV"
|
|
288
|
+
else
|
|
289
|
+
log "local mode selected; no $REFLEXIO_ENV exists, so no env file was created"
|
|
290
|
+
fi
|
|
291
|
+
install_for_host "$host"
|
|
292
|
+
return 0
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
api_key="$(prompt_api_key "$existing_api_key")"
|
|
296
|
+
read_only="$(prompt_normalized "Read-only mode? (yes/no)" "$default_read_only" normalize_yes_no)"
|
|
297
|
+
scope="$(prompt_normalized "Sharing scope (1=project, 2=global)" "$default_scope" normalize_scope)"
|
|
298
|
+
|
|
299
|
+
managed_url="${existing_url:-$MANAGED_REFLEXIO_URL}"
|
|
300
|
+
if is_local_url "$managed_url"; then
|
|
301
|
+
managed_url="$MANAGED_REFLEXIO_URL"
|
|
302
|
+
fi
|
|
303
|
+
managed_user_id=""
|
|
304
|
+
if [ "$scope" = "global" ]; then
|
|
305
|
+
managed_user_id="${existing_user_id:-$GLOBAL_USER_ID}"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
remove_env_keys \
|
|
309
|
+
REFLEXIO_API_KEY \
|
|
310
|
+
REFLEXIO_URL \
|
|
311
|
+
REFLEXIO_USER_ID \
|
|
312
|
+
CLAUDE_SMART_READ_ONLY \
|
|
313
|
+
CLAUDE_SMART_USE_LOCAL_CLI \
|
|
314
|
+
CLAUDE_SMART_USE_LOCAL_EMBEDDING
|
|
315
|
+
append_env_value REFLEXIO_URL "$managed_url"
|
|
316
|
+
append_env_value REFLEXIO_API_KEY "$api_key"
|
|
317
|
+
if [ "$read_only" = "yes" ]; then
|
|
318
|
+
append_env_value CLAUDE_SMART_READ_ONLY "1"
|
|
319
|
+
fi
|
|
320
|
+
if [ -n "$managed_user_id" ]; then
|
|
321
|
+
append_env_value REFLEXIO_USER_ID "$managed_user_id"
|
|
322
|
+
fi
|
|
323
|
+
chmod 600 "$REFLEXIO_ENV"
|
|
324
|
+
log "configured managed Reflexio in $REFLEXIO_ENV"
|
|
325
|
+
log "If a real API key was pasted visibly into a terminal or chat, rotate it in Reflexio."
|
|
326
|
+
install_for_host "$host"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
main "$@"
|