Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Tunables read by the plugin at runtime. Most users don't need to touch these —
| `REFLEXIO_EMBEDDING_SERVICE_URL` | `http://127.0.0.1:$EMBEDDING_PORT` | OpenAI-compatible embedding endpoint used by `local_service` and `internal_service`. |
| `EMBEDDING_PORT` | `8072` | Local embedding daemon port. Shared by Claude Code and Codex on the same machine. |
| `REFLEXIO_RERANK_DEVICE` | `cpu` | Torch device for the in-process cross-encoder reranker used by search. Defaults to CPU: auto-selected MPS on Apple Silicon accumulates gigabytes of GPU memory in the backend process for no latency win at this model size. Set to `cuda` only for GPU deployments. |
| `CLAUDE_SMART_ENABLE_OPTIMIZER` | enabled unless set to `0` | Hook-side env var that controls shared skill optimization and rollups during `SessionStart`. Set it in Claude Code settings, not `~/.reflexio/.env`. |
| `CLAUDE_SMART_ENABLE_OPTIMIZER` | auto: local Reflexio only | Hook-side env var that controls shared skill optimization and rollups during `SessionStart`. Set to `1` to force optimizer defaults for hosted Reflexio, or `0` to disable everywhere. Set it in Claude Code settings, not `~/.reflexio/.env`. |
| `CLAUDE_SMART_CITATIONS` | `on` | Controls the final `✨ claude-smart rule applied` marker instruction. `on` (default) injects a compact instruction asking the assistant to cite only memories that materially changed the answer. `off` skips injection of the citation instruction entirely and strips any stray marker line from the assistant text before publishing. Legacy values `auto` and `marker-only` are accepted as enabled aliases. Set in Claude Code settings (hook env), not `~/.reflexio/.env`. |
| `CLAUDE_SMART_CITATION_LINK_STYLE` | `markdown` | Controls the visible links in the final `✨ claude-smart rule applied` marker. `markdown` asks the model to emit `[human title](dashboard URL)` links and is the Codex default because Codex renders markdown links in assistant messages. `osc8` asks it to emit terminal-native OSC 8 hyperlinks so the human title can be clickable without showing the URL; unsupported terminals should fall back to markdown links. |
| `CLAUDE_SMART_DASHBOARD_URL` | `http://localhost:3001` | Base URL used in injected citation targets and the final `✨ claude-smart rule applied` marker links. Override only if the local dashboard is exposed on another host or port. Model-facing citation links use short `/rules/{citationId}` resolver URLs; canonical direct links remain `/skills/project/{id}`, `/skills/shared/{id}`, and `/preferences/project/{id}`. `/preferences/{id}` remains a compatibility alias. |
Expand Down
54 changes: 40 additions & 14 deletions plugin/dashboard/app/configure/env/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ import { PageHeader } from "@/components/common/page-header";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { SETTINGS_CHANGED_EVENT } from "@/hooks/use-settings";
import type { ClaudeCodeHookConfig, ClaudeSmartConfig } from "@/lib/types";
import type {
ClaudeCodeHookConfig,
ClaudeSmartConfig,
OptimizerMode,
} from "@/lib/types";

export default function ConfigureEnvPage() {
const [config, setConfig] = useState<ClaudeSmartConfig | null>(null);
Expand Down Expand Up @@ -44,14 +55,14 @@ export default function ConfigureEnvPage() {
setSaved(false);
};

const updateOptimizer = (enabled: boolean) => {
const updateOptimizer = (mode: OptimizerMode) => {
setHookConfig((prev) =>
prev
? {
...prev,
CLAUDE_SMART_ENABLE_OPTIMIZER: enabled,
effectiveValue: enabled,
localValue: enabled,
CLAUDE_SMART_ENABLE_OPTIMIZER: mode,
effectiveValue: mode,
localValue: mode === "auto" ? null : mode,
}
: prev,
);
Expand Down Expand Up @@ -280,15 +291,29 @@ export default function ConfigureEnvPage() {
CLAUDE_SMART_ENABLE_OPTIMIZER
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
Enable shared skill optimization and rollups during
SessionStart.
Control shared skill optimization and rollups during
SessionStart. Auto applies only to local Reflexio.
</p>
</div>
<Switch
id="enable-optimizer"
checked={!!hookConfig.CLAUDE_SMART_ENABLE_OPTIMIZER}
onCheckedChange={updateOptimizer}
/>
<Select
value={hookConfig.CLAUDE_SMART_ENABLE_OPTIMIZER}
onValueChange={(value) =>
updateOptimizer((value as OptimizerMode) ?? "auto")
}
>
<SelectTrigger
id="enable-optimizer"
size="sm"
className="w-40 text-xs bg-background/80"
>
<SelectValue placeholder="Mode" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="enabled">Enabled</SelectItem>
<SelectItem value="disabled">Disabled</SelectItem>
</SelectContent>
</Select>
</div>
<div className="rounded-md border border-border bg-background/60 p-3 text-xs text-muted-foreground space-y-2">
<div className="flex items-center justify-between gap-4">
Expand Down Expand Up @@ -332,7 +357,8 @@ export default function ConfigureEnvPage() {
);
}

function formatSettingValue(value: boolean | null): string {
function formatSettingValue(value: OptimizerMode | null): string {
if (value === null) return "Not set";
return value ? "Enabled" : "Disabled";
if (value === "auto") return "Auto (local only)";
return value === "enabled" ? "Enabled" : "Disabled";
}
30 changes: 20 additions & 10 deletions plugin/dashboard/lib/claude-settings-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import type { ClaudeCodeHookConfig } from "./types";
import type { ClaudeCodeHookConfig, OptimizerMode } from "./types";

const ENV_KEY = "CLAUDE_SMART_ENABLE_OPTIMIZER";

Expand Down Expand Up @@ -52,17 +52,22 @@ function asRecord(value: unknown): Record<string, unknown> {
return value as Record<string, unknown>;
}

function parseEnabled(value: unknown): boolean {
if (value === undefined || value === null || value === "") return true;
if (value === false || value === 0) return false;
if (typeof value === "string" && value.trim() === "0") return false;
return true;
function parseOptimizerMode(value: unknown): OptimizerMode {
if (
value === false ||
value === 0 ||
(typeof value === "string" &&
["0", "false", "no", "off"].includes(value.trim().toLowerCase()))
) {
return "disabled";
}
return "enabled";
}

function optimizerValue(settings: Record<string, unknown>): boolean | null {
function optimizerValue(settings: Record<string, unknown>): OptimizerMode | null {
const env = asRecord(settings.env);
if (!(ENV_KEY in env)) return null;
return parseEnabled(env[ENV_KEY]);
return parseOptimizerMode(env[ENV_KEY]);
}

async function readSettingsFile(file: string): Promise<Record<string, unknown>> {
Expand All @@ -84,7 +89,7 @@ export async function readClaudeCodeHookConfig(): Promise<ClaudeCodeHookConfig>
]);
const localValue = optimizerValue(localSettings);
const userValue = optimizerValue(userSettings);
const effectiveValue = localValue ?? userValue ?? true;
const effectiveValue = localValue ?? userValue ?? "auto";
return {
CLAUDE_SMART_ENABLE_OPTIMIZER: effectiveValue,
effectiveValue,
Expand All @@ -103,7 +108,12 @@ export async function writeClaudeCodeHookConfig(
const env = asRecord(settings.env);

if (!("CLAUDE_SMART_ENABLE_OPTIMIZER" in update)) return;
env[ENV_KEY] = update.CLAUDE_SMART_ENABLE_OPTIMIZER ? "1" : "0";
if (update.CLAUDE_SMART_ENABLE_OPTIMIZER === "auto") {
delete env[ENV_KEY];
} else {
env[ENV_KEY] =
update.CLAUDE_SMART_ENABLE_OPTIMIZER === "enabled" ? "1" : "0";
}

settings.env = env;
await fs.mkdir(path.dirname(file), { recursive: true });
Expand Down
10 changes: 6 additions & 4 deletions plugin/dashboard/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ export interface ClaudeSmartConfig {
[extra: string]: string | boolean | undefined;
}

export type OptimizerMode = "auto" | "enabled" | "disabled";

export interface ClaudeCodeHookConfig {
CLAUDE_SMART_ENABLE_OPTIMIZER: boolean;
effectiveValue: boolean;
localValue: boolean | null;
userValue: boolean | null;
CLAUDE_SMART_ENABLE_OPTIMIZER: OptimizerMode;
effectiveValue: OptimizerMode;
localValue: OptimizerMode | null;
userValue: OptimizerMode | null;
settingsPath: string;
userSettingsPath: string;
}
Expand Down
33 changes: 26 additions & 7 deletions plugin/src/claude_smart/events/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import json
import os
import sys
from contextlib import suppress
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from claude_smart import hook
from claude_smart.reflexio_adapter import Adapter
Expand Down Expand Up @@ -41,10 +43,12 @@ def _int_env(name: str, default: int) -> int:

_CLAUDE_SMART_WINDOW_SIZE = _int_env("CLAUDE_SMART_WINDOW_SIZE", 5)
_CLAUDE_SMART_STRIDE_SIZE = _int_env("CLAUDE_SMART_STRIDE_SIZE", 3)
# Optimizer is on by default. Set this env var to "0" to skip pushing the
# claude-smart optimizer defaults on SessionStart (kill switch).
_DISABLE_OPTIMIZER_ENV = "CLAUDE_SMART_ENABLE_OPTIMIZER"
# Optimizer defaults are local-only by default because they store an absolute
# path to the claude-smart helper process. Set this env var to "1" to force
# enabling against a non-local Reflexio URL, or "0" to disable everywhere.
_OPTIMIZER_ENV = "CLAUDE_SMART_ENABLE_OPTIMIZER"
_OPTIMIZER_TIMEOUT_SECONDS = 300
_LOCAL_REFLEXIO_HOSTS = {"", "localhost", "127.0.0.1", "::1"}


def _adapter() -> Adapter:
Expand Down Expand Up @@ -109,7 +113,7 @@ def handle(payload: dict[str, Any]) -> None:
window_size=_CLAUDE_SMART_WINDOW_SIZE,
stride_size=_CLAUDE_SMART_STRIDE_SIZE,
)
if os.environ.get(_DISABLE_OPTIMIZER_ENV) != "0":
if _should_apply_optimizer_defaults(adapter):
adapter.apply_optimizer_defaults(
script_path=_optimizer_assistant_path(),
timeout_seconds=_OPTIMIZER_TIMEOUT_SECONDS,
Expand All @@ -131,13 +135,28 @@ def handle(payload: dict[str, Any]) -> None:
)
sys.stdout.write("\n")

try:
with suppress(Exception):
adapter.mark_stall_notified()
except Exception: # noqa: BLE001 — telemetry must not break session.
pass


def _optimizer_assistant_path() -> str:
executable = Path(sys.executable)
suffix = ".exe" if os.name == "nt" else ""
return str(executable.with_name(f"claude-smart-optimizer-assistant{suffix}"))


def _should_apply_optimizer_defaults(adapter: Any) -> bool:
flag = os.environ.get(_OPTIMIZER_ENV, "").strip().lower()
if flag in {"0", "false", "no", "off"}:
return False
if flag in {"1", "true", "yes", "on"}:
return True
return _is_local_reflexio_url(getattr(adapter, "url", ""))


def _is_local_reflexio_url(url: str) -> bool:
if not url:
return True
parsed = urlparse(url)
host = parsed.hostname if parsed.scheme else url.split(":", 1)[0]
return (host or "").lower() in _LOCAL_REFLEXIO_HOSTS
10 changes: 5 additions & 5 deletions plugin/src/claude_smart/reflexio_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@

from __future__ import annotations

import inspect
import logging
import os
import inspect
from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from typing import Any

from claude_smart import env_config
from claude_smart import runtime
from claude_smart import env_config, runtime

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -174,8 +173,9 @@ def apply_optimizer_defaults(

Idempotent compare-then-write: reads ``Config``, only issues a
``set_config`` when the server-side values differ from the desired
dict below. Called unconditionally from SessionStart; the caller's
only escape hatch is ``CLAUDE_SMART_ENABLE_OPTIMIZER=0``.
dict below. SessionStart calls this only for local Reflexio URLs by
default; ``CLAUDE_SMART_ENABLE_OPTIMIZER=1`` forces hosted URLs, and
``CLAUDE_SMART_ENABLE_OPTIMIZER=0`` disables it everywhere.
"""
client = self._get_client()
if client is None:
Expand Down
71 changes: 71 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,8 @@ def test_session_start_ignores_available_memory_and_continues(
session_dir, monkeypatch
) -> None:
class Stub:
url = "http://localhost:8071/"

def apply_extraction_defaults(self, **_kw):
return True

Expand Down Expand Up @@ -1580,6 +1582,8 @@ def test_session_start_skips_optimizer_defaults_when_opted_out(
optimizer_calls: list[dict[str, Any]] = []

class Stub:
url = "http://localhost:8071/"

def apply_extraction_defaults(self, **_kw):
return True

Expand Down Expand Up @@ -1608,6 +1612,72 @@ def test_session_start_applies_optimizer_defaults_by_default(
optimizer_calls: list[dict[str, Any]] = []

class Stub:
url = "http://localhost:8071/"

def apply_extraction_defaults(self, **_kw):
return True

def apply_optimizer_defaults(self, **kwargs):
optimizer_calls.append(kwargs)
return True

def fetch_all(self, **_kw):
return ([], [], [])

monkeypatch.setattr(session_start.sys, "executable", "/tmp/venv/bin/python")
monkeypatch.setattr(
"claude_smart.events.session_start.Adapter", lambda *a, **kw: Stub()
)
buf = io.StringIO()
monkeypatch.setattr(sys, "stdout", buf)

session_start.handle({"session_id": "s1", "source": "startup"})

assert optimizer_calls == [
{
"script_path": "/tmp/venv/bin/claude-smart-optimizer-assistant",
"timeout_seconds": 300,
}
]


def test_session_start_skips_optimizer_defaults_for_hosted_reflexio_by_default(
session_dir, monkeypatch
) -> None:
optimizer_calls: list[dict[str, Any]] = []

class Stub:
url = "https://www.reflexio.ai/"

def apply_extraction_defaults(self, **_kw):
return True

def apply_optimizer_defaults(self, **kwargs):
optimizer_calls.append(kwargs)
return True

def fetch_all(self, **_kw):
return ([], [], [])

monkeypatch.setattr(
"claude_smart.events.session_start.Adapter", lambda *a, **kw: Stub()
)
buf = io.StringIO()
monkeypatch.setattr(sys, "stdout", buf)

session_start.handle({"session_id": "s1", "source": "startup"})

assert optimizer_calls == []


def test_session_start_can_force_optimizer_defaults_for_hosted_reflexio(
session_dir, monkeypatch
) -> None:
optimizer_calls: list[dict[str, Any]] = []

class Stub:
url = "https://www.reflexio.ai/"

def apply_extraction_defaults(self, **_kw):
return True

Expand All @@ -1618,6 +1688,7 @@ def apply_optimizer_defaults(self, **kwargs):
def fetch_all(self, **_kw):
return ([], [], [])

monkeypatch.setenv("CLAUDE_SMART_ENABLE_OPTIMIZER", "1")
monkeypatch.setattr(session_start.sys, "executable", "/tmp/venv/bin/python")
monkeypatch.setattr(
"claude_smart.events.session_start.Adapter", lambda *a, **kw: Stub()
Expand Down
Loading