claude-smart 0.2.39 → 0.2.40

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.39-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.40-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">
@@ -16,6 +16,7 @@
16
16
  const { execSync, spawn, spawnSync } = require("child_process");
17
17
  const crypto = require("crypto");
18
18
  const {
19
+ chmodSync,
19
20
  cpSync,
20
21
  existsSync,
21
22
  lstatSync,
@@ -41,6 +42,8 @@ const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
41
42
  const MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/";
42
43
  const MANAGED_SETUP_ENV = "CLAUDE_SMART_MANAGED_SETUP";
43
44
  const CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY";
45
+ const CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI";
46
+ const CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING";
44
47
  const REFLEXIO_USER_ID_ENV = "REFLEXIO_USER_ID";
45
48
  const REFLEXIO_DIR = join(homedir(), ".reflexio");
46
49
  const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
@@ -90,6 +93,24 @@ const COPYTREE_IGNORE_NAMES = new Set([
90
93
  "node_modules",
91
94
  ".next",
92
95
  ]);
96
+ const LOCAL_DEFAULT_ENV_ENTRIES = [
97
+ [
98
+ "# Route reflexio generation through the local Claude Code CLI",
99
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
100
+ "1",
101
+ ],
102
+ [
103
+ "# Use the in-process ONNX embedder (chromadb) - no API key for semantic search",
104
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
105
+ "1",
106
+ ],
107
+ [null, CLAUDE_SMART_READ_ONLY_ENV, "0"],
108
+ ];
109
+ const LOCAL_MODE_PRUNE_KEYS = new Set([
110
+ "REFLEXIO_URL",
111
+ "REFLEXIO_API_KEY",
112
+ REFLEXIO_USER_ID_ENV,
113
+ ]);
93
114
 
94
115
  function shouldCopyPath(src) {
95
116
  const base = src.split(/[\\/]/).pop() || "";
@@ -216,6 +237,57 @@ function parseEnvLine(line) {
216
237
  return { key, value };
217
238
  }
218
239
 
240
+ function escapeEnvValue(value) {
241
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
242
+ }
243
+
244
+ function ensureLocalReflexioEnv() {
245
+ mkdirSync(dirname(REFLEXIO_ENV_PATH), { recursive: true });
246
+ const existing = existsSync(REFLEXIO_ENV_PATH)
247
+ ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
248
+ : "";
249
+ const present = new Set();
250
+ const keptLines = [];
251
+ let pruned = false;
252
+ for (const line of existing.split(/\r?\n/)) {
253
+ const parsed = parseEnvLine(line);
254
+ if (parsed) {
255
+ if (LOCAL_MODE_PRUNE_KEYS.has(parsed.key)) {
256
+ pruned = true;
257
+ continue;
258
+ }
259
+ present.add(parsed.key);
260
+ }
261
+ keptLines.push(line);
262
+ }
263
+
264
+ const additions = [];
265
+ const added = [];
266
+ for (const [comment, key, value] of LOCAL_DEFAULT_ENV_ENTRIES) {
267
+ if (present.has(key)) continue;
268
+ if (comment) additions.push(comment);
269
+ if (key === CLAUDE_SMART_READ_ONLY_ENV) {
270
+ additions.push(`${key}="${escapeEnvValue(value)}"`);
271
+ } else {
272
+ additions.push(`${key}=${escapeEnvValue(value)}`);
273
+ }
274
+ added.push(key);
275
+ }
276
+
277
+ if (additions.length > 0 || pruned) {
278
+ let content = keptLines.join("\n").replace(/\n*$/, "");
279
+ if (additions.length > 0) {
280
+ const prefix = content ? "\n" : "";
281
+ content = content + prefix + additions.join("\n");
282
+ }
283
+ writeFileSync(REFLEXIO_ENV_PATH, content ? `${content}\n` : "");
284
+ } else if (!existsSync(REFLEXIO_ENV_PATH)) {
285
+ writeFileSync(REFLEXIO_ENV_PATH, "");
286
+ }
287
+ chmodSync(REFLEXIO_ENV_PATH, 0o600);
288
+ return added;
289
+ }
290
+
219
291
  function maskSecret(value) {
220
292
  if (!value) return "";
221
293
  if (value.length <= 8) return "*".repeat(value.length);
@@ -254,7 +326,12 @@ function loadReflexioSetupEnv() {
254
326
  } else {
255
327
  delete process.env.REFLEXIO_URL;
256
328
  delete process.env.REFLEXIO_API_KEY;
329
+ delete process.env[REFLEXIO_USER_ID_ENV];
257
330
  delete process.env[MANAGED_SETUP_ENV];
331
+ const added = ensureLocalReflexioEnv();
332
+ if (added.length > 0) {
333
+ process.stdout.write(`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`);
334
+ }
258
335
  }
259
336
  const readOnly = ["1", "true", "yes", "on"].includes(
260
337
  String(readOnlyValue).trim().toLowerCase(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
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.39",
3
+ "version": "0.2.40",
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.39"
3
+ version = "0.2.40"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -15,6 +15,10 @@ dependencies = [
15
15
  # downloaded on first use, not at install time.
16
16
  "chromadb>=0.5",
17
17
  "einops>=0.8.0",
18
+ # Native SQLite vector KNN for local storage. Reflexio can fall back to
19
+ # Python ranking without this, but installed claude-smart should include
20
+ # the accelerator by default.
21
+ "sqlite-vec>=0.1.6",
18
22
  ]
19
23
 
20
24
  [project.scripts]
@@ -535,7 +535,7 @@ function runHook(root, event) {
535
535
  REFLEXIO_URL: readBackendUrl(),
536
536
  CLAUDE_SMART_HOST: "codex",
537
537
  CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
538
- CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "osc8",
538
+ CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "markdown",
539
539
  },
540
540
  input,
541
541
  stdio: ["pipe", "pipe", "inherit"],
@@ -21,7 +21,7 @@ case "$EVENT" in
21
21
  esac
22
22
  export CLAUDE_SMART_HOST="$HOST"
23
23
  if [ "$HOST" = "codex" ] && [ -z "${CLAUDE_SMART_CITATION_LINK_STYLE:-}" ]; then
24
- export CLAUDE_SMART_CITATION_LINK_STYLE="osc8"
24
+ export CLAUDE_SMART_CITATION_LINK_STYLE="markdown"
25
25
  fi
26
26
 
27
27
  HERE="$(cd "$(dirname "$0")" && pwd)"
@@ -59,8 +59,9 @@ if [ -f "$FAILURE_MARKER" ]; then
59
59
  if [ -z "$stored_fp" ] || [ "$stored_fp" != "$current_fp" ]; then
60
60
  rm -f "$FAILURE_MARKER"
61
61
  else
62
- if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
63
- python3 - "$FAILURE_MARKER" <<'PY'
62
+ failure_py="$(claude_smart_resolve_python 2>/dev/null || true)"
63
+ if [ "$EVENT" = "session-start" ] && [ -n "$failure_py" ]; then
64
+ "$failure_py" - "$FAILURE_MARKER" <<'PY'
64
65
  import json, pathlib, sys
65
66
  first = pathlib.Path(sys.argv[1]).read_text().splitlines()
66
67
  msg = (first[0].strip() if first else "") or "unknown error"
@@ -106,6 +106,12 @@ install_complete() {
106
106
  command -v uv >/dev/null 2>&1 || return 1
107
107
  [ -d "$PLUGIN_ROOT/.venv" ] || return 1
108
108
  claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook || return 1
109
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
110
+ [ -f "$HOME/.reflexio/.env" ] || return 1
111
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
112
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
113
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$HOME/.reflexio/.env" || return 1
114
+ fi
109
115
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
110
116
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
111
117
  fi
@@ -491,6 +497,63 @@ claude_smart_env_upsert() {
491
497
  fi
492
498
  }
493
499
 
500
+ claude_smart_env_append_raw_if_missing() {
501
+ local key value
502
+ key="$1"
503
+ value="$2"
504
+ if ! grep -qE "^(export[[:space:]]+)?${key}=" "$REFLEXIO_ENV"; then
505
+ printf '%s=%s\n' "$key" "$value" >> "$REFLEXIO_ENV"
506
+ fi
507
+ }
508
+
509
+ claude_smart_prune_managed_env_keys_for_local() {
510
+ local tmp line raw_key key
511
+ [ -f "$REFLEXIO_ENV" ] || return 0
512
+ tmp="$(mktemp "${REFLEXIO_ENV}.tmp.XXXXXX")"
513
+ while IFS= read -r line || [ -n "$line" ]; do
514
+ raw_key="$line"
515
+ raw_key="${raw_key#"${raw_key%%[![:space:]]*}"}"
516
+ case "$raw_key" in
517
+ export\ *) raw_key="${raw_key#export }" ;;
518
+ esac
519
+ key="${raw_key%%=*}"
520
+ key="${key%"${key##*[![:space:]]}"}"
521
+ case "$key" in
522
+ REFLEXIO_URL|REFLEXIO_API_KEY|REFLEXIO_USER_ID)
523
+ continue
524
+ ;;
525
+ esac
526
+ printf '%s\n' "$line" >> "$tmp"
527
+ done < "$REFLEXIO_ENV"
528
+ mv "$tmp" "$REFLEXIO_ENV"
529
+ }
530
+
531
+ claude_smart_ensure_local_env_defaults() {
532
+ [ -z "${REFLEXIO_API_KEY:-}" ] || return 0
533
+ mkdir -p "$(dirname "$REFLEXIO_ENV")"
534
+ touch "$REFLEXIO_ENV"
535
+ chmod 600 "$REFLEXIO_ENV"
536
+ claude_smart_prune_managed_env_keys_for_local
537
+ unset REFLEXIO_URL REFLEXIO_API_KEY REFLEXIO_USER_ID CLAUDE_SMART_MANAGED_SETUP
538
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
539
+ printf '# Route reflexio generation through the local Claude Code CLI\n' >> "$REFLEXIO_ENV"
540
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "1"
541
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
542
+ fi
543
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
544
+ printf '# Use the in-process ONNX embedder (chromadb) - no API key for semantic search\n' >> "$REFLEXIO_ENV"
545
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "1"
546
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
547
+ fi
548
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$REFLEXIO_ENV"; then
549
+ claude_smart_env_upsert CLAUDE_SMART_READ_ONLY "0"
550
+ echo "[claude-smart] appended CLAUDE_SMART_READ_ONLY=0 to $REFLEXIO_ENV" >&2
551
+ fi
552
+ chmod 600 "$REFLEXIO_ENV"
553
+ }
554
+
555
+ claude_smart_ensure_local_env_defaults
556
+
494
557
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
495
558
  # plugin backend port (8071). Matches the quoted and unquoted forms but
496
559
  # requires paired quotes, so malformed or deliberately different values
@@ -894,7 +894,7 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
894
894
 
895
895
 
896
896
  def _configure_reflexio_setup() -> bool:
897
- """Load setup state from ``~/.reflexio/.env`` without writing local defaults.
897
+ """Load setup state and ensure local defaults when unmanaged.
898
898
 
899
899
  Returns:
900
900
  bool: Whether read-only mode is enabled.
@@ -939,7 +939,11 @@ def _configure_reflexio_setup() -> bool:
939
939
  else:
940
940
  os.environ.pop(env_config.REFLEXIO_URL_ENV, None)
941
941
  os.environ.pop(env_config.REFLEXIO_API_KEY_ENV, None)
942
+ os.environ.pop("REFLEXIO_USER_ID", None)
942
943
  os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None)
944
+ added = env_config.ensure_local_env_defaults(_REFLEXIO_ENV_PATH)
945
+ if added:
946
+ sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
943
947
  return read_only
944
948
 
945
949
 
@@ -1342,7 +1346,9 @@ def _run_service(script: Path, subcmd: str) -> int:
1342
1346
  return 1
1343
1347
  bash = _resolve_bash()
1344
1348
  if not bash:
1345
- sys.stderr.write("error: bash is required to run claude-smart service scripts\n")
1349
+ sys.stderr.write(
1350
+ "error: bash is required to run claude-smart service scripts\n"
1351
+ )
1346
1352
  return 1
1347
1353
  try:
1348
1354
  subprocess.run([bash, str(script), subcmd], check=True)
@@ -238,6 +238,11 @@ def render_inline_compact_with_registry(
238
238
  item = f"{content} (title: {title}"
239
239
  if rule_url:
240
240
  item += f"; open: {rule_url}"
241
+ marker_parts.append(
242
+ _markdown_link(
243
+ rule_url, _strip_trailing_sentence_punctuation(title)
244
+ )
245
+ )
241
246
  item += ")"
242
247
  if entry.get("kind") == "profile":
243
248
  preference_parts.append(item)
@@ -280,6 +285,17 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
280
285
  "the same linked memory text; keep the link, but do not show the "
281
286
  "URL. Skip when unrelated."
282
287
  )
288
+ if marker_parts:
289
+ marker = f"✨ claude-smart rule applied: {' | '.join(marker_parts)}"
290
+ separator_instruction = (
291
+ " Separate multiple linked memories with the visible ` | ` separator."
292
+ if len(marker_parts) > 1
293
+ else ""
294
+ )
295
+ return _remoteize_citation_instruction(
296
+ f"If used, copy this final marker exactly with markdown links: "
297
+ f"`{marker}`.{separator_instruction} Skip when unrelated."
298
+ )
283
299
  return _remoteize_citation_instruction(
284
300
  "Only if a listed [cs:...] item materially changes your answer, end "
285
301
  "with one final marker like `✨ claude-smart rule applied: "
@@ -368,7 +384,7 @@ def _append_playbook_bullet(
368
384
  item_id = cs_cite.rank_id("playbook", rank, real_id)
369
385
  title = _title_from_content(content)
370
386
  dashboard_url = _dashboard_url("playbook", real_id, source_kind)
371
- rule_url = _rule_url(item_id, "playbook")
387
+ rule_url = _rule_url(item_id, "playbook", real_id, source_kind)
372
388
  bullet = f"- [cs:{item_id}] {content}"
373
389
  if trigger:
374
390
  bullet += f" _(when: {trigger})_"
@@ -407,7 +423,7 @@ def _format_profiles(
407
423
  item_id = cs_cite.rank_id("profile", rank, real_id)
408
424
  title = _title_from_content(content)
409
425
  dashboard_url = _dashboard_url("profile", real_id)
410
- rule_url = _rule_url(item_id, "profile")
426
+ rule_url = _rule_url(item_id, "profile", real_id)
411
427
  bullet = f"- [cs:{item_id}] {content}"
412
428
  if rule_url:
413
429
  bullet += f" _(open: {rule_url})_"
@@ -427,7 +443,7 @@ def _format_profiles(
427
443
 
428
444
 
429
445
  def _dashboard_url(kind: str, real_id: Any, source_kind: str | None = None) -> str:
430
- remote_url = _remote_reflexio_page_url(kind)
446
+ remote_url = _remote_reflexio_item_url(kind, real_id, source_kind)
431
447
  if remote_url:
432
448
  return remote_url
433
449
  if real_id is None:
@@ -442,8 +458,10 @@ def _dashboard_url(kind: str, real_id: Any, source_kind: str | None = None) -> s
442
458
  return ""
443
459
 
444
460
 
445
- def _rule_url(item_id: str, kind: str) -> str:
446
- remote_url = _remote_reflexio_page_url(kind)
461
+ def _rule_url(
462
+ item_id: str, kind: str, real_id: Any = None, source_kind: str | None = None
463
+ ) -> str:
464
+ remote_url = _remote_reflexio_item_url(kind, real_id, source_kind)
447
465
  if remote_url:
448
466
  return remote_url
449
467
  if not item_id:
@@ -453,6 +471,27 @@ def _rule_url(item_id: str, kind: str) -> str:
453
471
  return f"{base}/rules/{encoded_id}"
454
472
 
455
473
 
474
+ def _remote_reflexio_item_url(
475
+ kind: str, real_id: Any, source_kind: str | None = None
476
+ ) -> str:
477
+ origin = _remote_reflexio_origin()
478
+ if not origin:
479
+ return ""
480
+ if real_id is None:
481
+ return _remote_reflexio_page_url(kind)
482
+ encoded_id = quote(str(real_id), safe="")
483
+ if kind == "profile":
484
+ return f"{origin}/profiles?profile_id={encoded_id}"
485
+ if kind == "playbook":
486
+ if source_kind == "user_playbook":
487
+ return (
488
+ f"{origin}/playbooks?resource=user_playbook&"
489
+ f"user_playbook_id={encoded_id}"
490
+ )
491
+ return f"{origin}/playbooks?agent_playbook_id={encoded_id}"
492
+ return ""
493
+
494
+
456
495
  def _remote_reflexio_page_url(kind: str) -> str:
457
496
  origin = _remote_reflexio_origin()
458
497
  if not origin:
@@ -504,6 +543,12 @@ def _osc8_link(url: str, label: str) -> str:
504
543
  return f"\x1b]8;;{url}\x1b\\{label}\x1b]8;;\x1b\\"
505
544
 
506
545
 
546
+ def _markdown_link(url: str, label: str) -> str:
547
+ safe_label = label.replace("[", "\\[").replace("]", "\\]")
548
+ safe_url = url.replace(")", "%29")
549
+ return f"[{safe_label}]({safe_url})"
550
+
551
+
507
552
  def _strip_trailing_sentence_punctuation(text: str) -> str:
508
553
  return text.rstrip().rstrip(".!?")
509
554
 
@@ -39,7 +39,7 @@ from __future__ import annotations
39
39
 
40
40
  import re
41
41
  from typing import Any
42
- from urllib.parse import unquote, urlparse
42
+ from urllib.parse import parse_qs, unquote, urlparse
43
43
 
44
44
  _FINGERPRINT_LEN = 4
45
45
 
@@ -287,6 +287,17 @@ def dashboard_url_token(url: str) -> str:
287
287
  parsed = urlparse(url)
288
288
  path = parsed.path if parsed.scheme else url.split("?", 1)[0].split("#", 1)[0]
289
289
  parts = [unquote(part) for part in path.strip("/").split("/") if part]
290
+ query = parse_qs(parsed.query)
291
+ if len(parts) == 1 and parts[0] == "profiles":
292
+ profile_ids = query.get("profile_id") or []
293
+ return f"route:profile:profile:{profile_ids[0]}" if profile_ids else ""
294
+ if len(parts) == 1 and parts[0] == "playbooks":
295
+ user_playbook_ids = query.get("user_playbook_id") or []
296
+ if query.get("resource") == ["user_playbook"] and user_playbook_ids:
297
+ return f"route:playbook:user_playbook:{user_playbook_ids[0]}"
298
+ agent_playbook_ids = query.get("agent_playbook_id") or []
299
+ if agent_playbook_ids:
300
+ return f"route:playbook:agent_playbook:{agent_playbook_ids[0]}"
290
301
  if len(parts) == 3 and parts[0] == "skills" and parts[1] in {"project", "shared"}:
291
302
  source_kind = "user_playbook" if parts[1] == "project" else "agent_playbook"
292
303
  return f"route:playbook:{source_kind}:{parts[2]}"
@@ -10,6 +10,27 @@ MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/"
10
10
  REFLEXIO_URL_ENV = "REFLEXIO_URL"
11
11
  REFLEXIO_API_KEY_ENV = "REFLEXIO_API_KEY"
12
12
  CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY"
13
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI"
14
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING"
15
+
16
+ _LOCAL_DEFAULT_ENTRIES = (
17
+ (
18
+ "# Route reflexio generation through the local Claude Code CLI",
19
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
20
+ "1",
21
+ ),
22
+ (
23
+ "# Use the in-process ONNX embedder (chromadb) - no API key for semantic search",
24
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
25
+ "1",
26
+ ),
27
+ (None, CLAUDE_SMART_READ_ONLY_ENV, "0"),
28
+ )
29
+ _LOCAL_MODE_PRUNE_KEYS = {
30
+ REFLEXIO_URL_ENV,
31
+ REFLEXIO_API_KEY_ENV,
32
+ "REFLEXIO_USER_ID",
33
+ }
13
34
 
14
35
 
15
36
  def parse_env_line(line: str) -> tuple[str, str] | None:
@@ -89,6 +110,59 @@ def set_env_vars(path: Path, values: dict[str, str]) -> list[str]:
89
110
  return added
90
111
 
91
112
 
113
+ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
114
+ """Create or augment ``~/.reflexio/.env`` for claude-smart local mode.
115
+
116
+ Existing active assignments win. This repairs first installs and deleted
117
+ env files without clobbering explicit user overrides such as
118
+ ``CLAUDE_SMART_READ_ONLY=1``.
119
+ """
120
+ path = path or REFLEXIO_ENV_PATH
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ try:
123
+ existing = path.read_text()
124
+ except OSError:
125
+ existing = ""
126
+
127
+ present: set[str] = set()
128
+ kept_lines: list[str] = []
129
+ pruned = False
130
+ for line in existing.splitlines():
131
+ parsed = parse_env_line(line)
132
+ if parsed is not None:
133
+ key, _value = parsed
134
+ if key in _LOCAL_MODE_PRUNE_KEYS:
135
+ pruned = True
136
+ continue
137
+ present.add(key)
138
+ kept_lines.append(line)
139
+
140
+ additions: list[str] = []
141
+ added_keys: list[str] = []
142
+ for comment, key, value in _LOCAL_DEFAULT_ENTRIES:
143
+ if key in present:
144
+ continue
145
+ if comment:
146
+ additions.append(comment)
147
+ if key == CLAUDE_SMART_READ_ONLY_ENV:
148
+ additions.append(f'{key}="{_escape_env_value(value)}"')
149
+ else:
150
+ additions.append(f"{key}={_escape_env_value(value)}")
151
+ added_keys.append(key)
152
+
153
+ if additions or pruned:
154
+ content = "\n".join(kept_lines)
155
+ if additions:
156
+ prefix = "" if not content or content.endswith("\n") else "\n"
157
+ content = content + prefix + "\n".join(additions)
158
+ content = content + ("\n" if content else "")
159
+ path.write_text(content, encoding="utf-8")
160
+ elif not path.exists():
161
+ path.touch()
162
+ path.chmod(0o600)
163
+ return added_keys
164
+
165
+
92
166
  def env_truthy(name: str) -> bool:
93
167
  """Return True when an environment flag is explicitly enabled."""
94
168
  return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
package/plugin/uv.lock CHANGED
@@ -419,12 +419,13 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.39"
422
+ version = "0.2.40"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },
426
426
  { name = "einops" },
427
427
  { name = "reflexio-ai" },
428
+ { name = "sqlite-vec" },
428
429
  ]
429
430
 
430
431
  [package.dev-dependencies]
@@ -437,6 +438,7 @@ requires-dist = [
437
438
  { name = "chromadb", specifier = ">=0.5" },
438
439
  { name = "einops", specifier = ">=0.8.0" },
439
440
  { name = "reflexio-ai", specifier = ">=0.2.23" },
441
+ { name = "sqlite-vec", specifier = ">=0.1.6" },
440
442
  ]
441
443
 
442
444
  [package.metadata.requires-dev]
@@ -3107,6 +3109,18 @@ wheels = [
3107
3109
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
3108
3110
  ]
3109
3111
 
3112
+ [[package]]
3113
+ name = "sqlite-vec"
3114
+ version = "0.1.9"
3115
+ source = { registry = "https://pypi.org/simple" }
3116
+ wheels = [
3117
+ { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" },
3118
+ { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" },
3119
+ { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" },
3120
+ { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" },
3121
+ { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" },
3122
+ ]
3123
+
3110
3124
  [[package]]
3111
3125
  name = "sseclient-py"
3112
3126
  version = "1.9.0"
@@ -106,6 +106,34 @@ append_env_value() {
106
106
  chmod 600 "$REFLEXIO_ENV"
107
107
  }
108
108
 
109
+ append_env_raw() {
110
+ local key value
111
+ key="$1"
112
+ value="$2"
113
+ mkdir -p "$(dirname "$REFLEXIO_ENV")"
114
+ touch "$REFLEXIO_ENV"
115
+ chmod 600 "$REFLEXIO_ENV"
116
+ printf '%s=%s\n' "$key" "$value" >> "$REFLEXIO_ENV"
117
+ chmod 600 "$REFLEXIO_ENV"
118
+ }
119
+
120
+ ensure_local_env_defaults() {
121
+ mkdir -p "$(dirname "$REFLEXIO_ENV")"
122
+ touch "$REFLEXIO_ENV"
123
+ chmod 600 "$REFLEXIO_ENV"
124
+ if ! get_env_value CLAUDE_SMART_USE_LOCAL_CLI >/dev/null 2>&1; then
125
+ printf '# Route reflexio generation through the local Claude Code CLI\n' >> "$REFLEXIO_ENV"
126
+ append_env_raw CLAUDE_SMART_USE_LOCAL_CLI "1"
127
+ fi
128
+ if ! get_env_value CLAUDE_SMART_USE_LOCAL_EMBEDDING >/dev/null 2>&1; then
129
+ printf '# Use the in-process ONNX embedder (chromadb) - no API key for semantic search\n' >> "$REFLEXIO_ENV"
130
+ append_env_raw CLAUDE_SMART_USE_LOCAL_EMBEDDING "1"
131
+ fi
132
+ if ! get_env_value CLAUDE_SMART_READ_ONLY >/dev/null 2>&1; then
133
+ append_env_value CLAUDE_SMART_READ_ONLY "0"
134
+ fi
135
+ }
136
+
109
137
  mask_secret() {
110
138
  local value len suffix
111
139
  value="$1"
@@ -283,11 +311,11 @@ main() {
283
311
 
284
312
  if [ "$mode" = "local" ]; then
285
313
  if [ -f "$REFLEXIO_ENV" ]; then
286
- remove_env_keys
314
+ remove_env_keys REFLEXIO_API_KEY REFLEXIO_URL REFLEXIO_USER_ID
287
315
  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
316
  fi
317
+ ensure_local_env_defaults
318
+ log "configured local Reflexio defaults in $REFLEXIO_ENV"
291
319
  install_for_host "$host"
292
320
  return 0
293
321
  fi