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 +1 -1
- package/bin/claude-smart.js +77 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/pyproject.toml +5 -1
- package/plugin/scripts/codex-hook.js +1 -1
- package/plugin/scripts/hook_entry.sh +4 -3
- package/plugin/scripts/smart-install.sh +63 -0
- package/plugin/src/claude_smart/cli.py +8 -2
- package/plugin/src/claude_smart/context_format.py +50 -5
- package/plugin/src/claude_smart/cs_cite.py +12 -1
- package/plugin/src/claude_smart/env_config.py +74 -0
- package/plugin/uv.lock +15 -1
- package/scripts/setup-claude-smart.sh +31 -3
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.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">
|
package/bin/claude-smart.js
CHANGED
|
@@ -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
package/plugin/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-smart"
|
|
3
|
-
version = "0.2.
|
|
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 || "
|
|
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="
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
446
|
-
|
|
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.
|
|
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
|