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 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.36-green.svg" alt="Version">
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`).
@@ -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
- return { key };
230
- }
231
-
232
- function setReflexioEnvVars(values) {
233
- mkdirSync(dirname(REFLEXIO_ENV_PATH), { recursive: true });
234
- const existing = existsSync(REFLEXIO_ENV_PATH)
235
- ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
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
- const content = out.join("\n").replace(/\n*$/, "\n");
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 configureReflexioSetup(args) {
269
- const apiKey = parseOptionalArg(args, "--api-key").trim();
270
- const readOnly = parseReadOnly(args);
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
- return;
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
- if (added.length > 0) {
298
- process.stdout.write(`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`);
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 install --api-key <key> Use managed Reflexio service",
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. Appends CLAUDE_SMART_USE_LOCAL_CLI=1 and CLAUDE_SMART_USE_LOCAL_EMBEDDING=1",
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 update --api-key <key> Update and configure managed Reflexio",
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
- if (args.includes("--api-key")) configureReflexioSetup(args);
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 readOnly = parseReadOnly(args);
1398
- configureReflexioSetup(args);
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 readOnly = parseReadOnly(args);
1452
- configureReflexioSetup(args);
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.36",
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__",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.36"
3
+ version = "0.2.37"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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|CLAUDE_SMART_READ_ONLY|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
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
- if [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] && [ "${CLAUDE_CODE_ENTRYPOINT:-}" != "cli" ]; then
101
- return 0
102
- fi
103
- return 1
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(args: argparse.Namespace) -> None:
827
- api_key = (getattr(args, "api_key", "") or "").strip()
828
- read_only = getattr(args, "read_only", None)
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
- getattr(args, "reflexio_url", "") or _MANAGED_REFLEXIO_URL
840
+ file_url
841
+ or os.environ.get(env_config.REFLEXIO_URL_ENV, _MANAGED_REFLEXIO_URL)
832
842
  ).strip()
833
- added = _seed_managed_reflexio_env(api_key=api_key, reflexio_url=reflexio_url)
834
- if read_only:
835
- added.extend(
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"Configured {_REFLEXIO_ENV_PATH} for managed Reflexio "
844
- f"({changed}; API key {env_config.mask_secret(api_key)}).\n"
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
- if added:
857
- sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
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(args)
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(args)
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
- if bool(getattr(args, "read_only", False)):
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
- if getattr(args, "api_key", ""):
1053
- _configure_reflexio_setup(args)
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.resolve_project_id()
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.resolve_project_id()
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.resolve_project_id(payload.get("cwd"))
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.resolve_project_id(payload.get("cwd"))
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 cs_cite, env_config, ids, internal_call, publish, runtime, state
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.resolve_project_id(payload.get("cwd"))
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
- for decision_text in plan_decisions:
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": decision_text,
419
+ "content": prompt,
403
420
  "user_id": project_id,
404
421
  },
405
422
  )
@@ -50,7 +50,7 @@ def handle(payload: dict[str, Any]) -> None:
50
50
  if not session_id or not prompt:
51
51
  return
52
52
 
53
- project_id = ids.resolve_project_id(payload.get("cwd"))
53
+ project_id = ids.resolve_user_id(payload.get("cwd"))
54
54
  state.append(
55
55
  session_id,
56
56
  {
@@ -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 — escape hatch for tests and
167
- advanced users who want the log elsewhere.
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 ``"cli"``
23
- the interactive REPL sets ``cli``; headless ``claude -p`` sets
24
- ``sdk-cli`` (and the SDKs may set other values). This catches
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
- _INTERACTIVE_ENTRYPOINT = "cli"
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 != _INTERACTIVE_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
@@ -419,7 +419,7 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.36"
422
+ version = "0.2.37"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },
@@ -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 "$@"