claude-smart 0.2.48 → 0.2.50
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/.claude-plugin/marketplace.json +2 -2
- package/README.md +11 -40
- package/bin/claude-smart.js +393 -55
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +255 -7
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +766 -123
- package/plugin/scripts/codex-hook.js +79 -229
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +118 -137
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +116 -21
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +93 -29
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +56 -11
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -6,7 +6,7 @@ import re
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from claude_smart import state
|
|
9
|
+
from claude_smart import runtime, state
|
|
10
10
|
|
|
11
11
|
# Tool inputs are persisted locally and later published to reflexio, so we
|
|
12
12
|
# apply a conservative redaction pass at ingestion time. Chosen to avoid
|
|
@@ -144,5 +144,6 @@ def handle(payload: dict[str, Any]) -> None:
|
|
|
144
144
|
"tool_input": _redact(payload.get("tool_input") or {}),
|
|
145
145
|
"tool_output": _redact_string(output_text) if output_text else "",
|
|
146
146
|
"status": _derive_status(tool_response),
|
|
147
|
+
"host": runtime.attribution_host(),
|
|
147
148
|
}
|
|
148
149
|
state.append(session_id, record)
|
|
@@ -21,7 +21,7 @@ import time
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
-
from claude_smart import env_config, ids, publish, state
|
|
24
|
+
from claude_smart import env_config, ids, publish, runtime, state
|
|
25
25
|
from claude_smart.events.stop import (
|
|
26
26
|
_read_transcript_entries,
|
|
27
27
|
_scan_transcript_for_assistant_text,
|
|
@@ -119,6 +119,7 @@ def _maybe_synthesize_assistant_anchor(
|
|
|
119
119
|
"content": assistant_text,
|
|
120
120
|
"user_id": project_id,
|
|
121
121
|
"synthesised_by": "session_end_anchor",
|
|
122
|
+
"host": runtime.attribution_host(),
|
|
122
123
|
},
|
|
123
124
|
)
|
|
124
125
|
_LOGGER.info(
|
|
@@ -408,6 +408,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
408
408
|
"role": "User",
|
|
409
409
|
"content": decision_text,
|
|
410
410
|
"user_id": project_id,
|
|
411
|
+
"host": runtime.attribution_host(),
|
|
411
412
|
},
|
|
412
413
|
)
|
|
413
414
|
elif prompt and not _has_unpublished_user_turn(session_id):
|
|
@@ -418,6 +419,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
418
419
|
"role": "User",
|
|
419
420
|
"content": prompt,
|
|
420
421
|
"user_id": project_id,
|
|
422
|
+
"host": runtime.attribution_host(),
|
|
421
423
|
},
|
|
422
424
|
)
|
|
423
425
|
|
|
@@ -426,6 +428,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
|
|
|
426
428
|
"role": "Assistant",
|
|
427
429
|
"content": assistant_text,
|
|
428
430
|
"user_id": project_id,
|
|
431
|
+
"host": runtime.attribution_host(),
|
|
429
432
|
}
|
|
430
433
|
if cited_items:
|
|
431
434
|
record["cited_items"] = cited_items
|
|
@@ -28,7 +28,7 @@ import logging
|
|
|
28
28
|
import time
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
-
from claude_smart import context_inject, ids, state
|
|
31
|
+
from claude_smart import context_inject, ids, runtime, state
|
|
32
32
|
|
|
33
33
|
_LOGGER = logging.getLogger(__name__)
|
|
34
34
|
_TOP_K = 3
|
|
@@ -58,6 +58,7 @@ def handle(payload: dict[str, Any]) -> None:
|
|
|
58
58
|
"role": "User",
|
|
59
59
|
"content": prompt,
|
|
60
60
|
"user_id": project_id,
|
|
61
|
+
"host": runtime.attribution_host(),
|
|
61
62
|
},
|
|
62
63
|
)
|
|
63
64
|
|
|
@@ -20,7 +20,8 @@ Two distinct sources of unwanted hook fires:
|
|
|
20
20
|
|
|
21
21
|
Detection signals, OR'd:
|
|
22
22
|
- ``CLAUDE_CODE_ENTRYPOINT`` is anything other than an interactive entrypoint
|
|
23
|
-
(``"cli"
|
|
23
|
+
(``"cli"``, Claude Desktop's ``"claude-desktop"``, or Claude Code IDE
|
|
24
|
+
entrypoints such as ``"claude-vscode"``/``"claude-jetbrains"``). Headless
|
|
24
25
|
``claude -p`` sets ``sdk-cli`` (and the SDKs may set other values). This
|
|
25
26
|
catches case (2) for any third-party tool, not just claude-mem.
|
|
26
27
|
- Env var ``CLAUDE_SMART_INTERNAL=1``, set by reflexio's provider
|
|
@@ -46,7 +47,9 @@ from typing import Any
|
|
|
46
47
|
from claude_smart import runtime
|
|
47
48
|
|
|
48
49
|
_ENTRYPOINT_VAR = "CLAUDE_CODE_ENTRYPOINT"
|
|
49
|
-
_INTERACTIVE_ENTRYPOINTS = frozenset(
|
|
50
|
+
_INTERACTIVE_ENTRYPOINTS = frozenset(
|
|
51
|
+
{"cli", "claude-desktop", "claude-vscode", "claude-jetbrains"}
|
|
52
|
+
)
|
|
50
53
|
_CODEX_TITLE_PROMPT_PREFIX = (
|
|
51
54
|
"You are a helpful assistant. You will be presented with a user prompt, "
|
|
52
55
|
"and your job is to provide a short title for a task"
|
|
@@ -4,6 +4,13 @@ Reflexio's ``LocalScriptAssistant`` sends one JSON payload on stdin and expects
|
|
|
4
4
|
one JSON object on stdout. This module bridges that protocol to a guarded
|
|
5
5
|
local CLI subprocess so candidate playbooks can be evaluated against the active
|
|
6
6
|
host without re-entering claude-smart/reflexio hooks.
|
|
7
|
+
|
|
8
|
+
Host CLI resolution: when ``CLAUDE_SMART_CLI_PATH`` is set in the environment
|
|
9
|
+
(``backend-service.sh`` writes it for ``opencode`` and ``codex`` hosts so
|
|
10
|
+
generation routes through the host-specific compatibility bridge instead of
|
|
11
|
+
the real Claude CLI), this script honours that override. Falling back to
|
|
12
|
+
``shutil.which("claude")`` preserves the legacy Claude Code host behaviour
|
|
13
|
+
where no override is configured.
|
|
7
14
|
"""
|
|
8
15
|
|
|
9
16
|
from __future__ import annotations
|
|
@@ -22,6 +29,20 @@ from claude_smart import internal_call, runtime
|
|
|
22
29
|
_CLI_TIMEOUT_SECONDS = 300
|
|
23
30
|
_READ_ONLY_TOOLS = "Read,Grep,Glob,LS"
|
|
24
31
|
_MUTATING_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit"
|
|
32
|
+
# Mirrors ``reflexio.server.llm.providers.claude_code_provider._ENV_CLI_PATH``
|
|
33
|
+
# so backend-service.sh can route both the reflexio Claude CLI provider and
|
|
34
|
+
# this assistant script through the same host-specific bridge.
|
|
35
|
+
_ENV_CLI_PATH = "CLAUDE_SMART_CLI_PATH"
|
|
36
|
+
_CLAUDE_SMART_COMPAT_BRIDGE_NAMES = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"codex-claude-compat",
|
|
39
|
+
"codex-claude-compat.cmd",
|
|
40
|
+
"codex-claude-compat.js",
|
|
41
|
+
"opencode-claude-compat",
|
|
42
|
+
"opencode-claude-compat.cmd",
|
|
43
|
+
"opencode-claude-compat.js",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
25
46
|
|
|
26
47
|
|
|
27
48
|
class OptimizerAssistantError(Exception):
|
|
@@ -142,26 +163,51 @@ def _run_local_cli(*, prompt: str, system_prompt: str) -> str:
|
|
|
142
163
|
return _run_claude(prompt=prompt, system_prompt=system_prompt)
|
|
143
164
|
|
|
144
165
|
|
|
166
|
+
def _resolve_claude_cli_path() -> str:
|
|
167
|
+
"""Return the host CLI to invoke for evaluation rollouts.
|
|
168
|
+
|
|
169
|
+
When ``CLAUDE_SMART_CLI_PATH`` is set, prefer it so backend-service.sh can
|
|
170
|
+
route the assistant through the same bridge it uses for the reflexio
|
|
171
|
+
``claude-code`` provider (e.g. ``opencode-claude-compat`` for the OpenCode
|
|
172
|
+
host). Falls back to ``shutil.which("claude")`` so Claude Code hosts — and
|
|
173
|
+
any environment that never set the override — continue to use the real
|
|
174
|
+
``claude`` CLI.
|
|
175
|
+
"""
|
|
176
|
+
override = os.environ.get(_ENV_CLI_PATH)
|
|
177
|
+
if override:
|
|
178
|
+
return override
|
|
179
|
+
return shutil.which("claude") or "claude"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _is_claude_smart_compat_bridge(cli_path: str) -> bool:
|
|
183
|
+
return Path(cli_path).name.lower() in _CLAUDE_SMART_COMPAT_BRIDGE_NAMES
|
|
184
|
+
|
|
185
|
+
|
|
145
186
|
def _run_claude(*, prompt: str, system_prompt: str) -> str:
|
|
146
|
-
cli_path =
|
|
147
|
-
# This is an evaluation rollout, not a real user session: allow local
|
|
148
|
-
# inspection, but prevent filesystem, shell, MCP, and session mutations.
|
|
187
|
+
cli_path = _resolve_claude_cli_path()
|
|
149
188
|
cmd = [
|
|
150
189
|
cli_path,
|
|
151
190
|
"-p",
|
|
152
191
|
"--output-format",
|
|
153
192
|
"json",
|
|
154
|
-
"--permission-mode",
|
|
155
|
-
"plan",
|
|
156
|
-
"--tools",
|
|
157
|
-
_READ_ONLY_TOOLS,
|
|
158
|
-
"--disallowedTools",
|
|
159
|
-
_MUTATING_TOOLS,
|
|
160
|
-
"--no-session-persistence",
|
|
161
|
-
"--mcp-config",
|
|
162
|
-
'{"mcpServers": {}}',
|
|
163
|
-
"--strict-mcp-config",
|
|
164
193
|
]
|
|
194
|
+
if not _is_claude_smart_compat_bridge(cli_path):
|
|
195
|
+
# This is an evaluation rollout, not a real user session: allow local
|
|
196
|
+
# inspection, but prevent filesystem, shell, MCP, and session mutations.
|
|
197
|
+
cmd.extend(
|
|
198
|
+
[
|
|
199
|
+
"--permission-mode",
|
|
200
|
+
"plan",
|
|
201
|
+
"--tools",
|
|
202
|
+
_READ_ONLY_TOOLS,
|
|
203
|
+
"--disallowedTools",
|
|
204
|
+
_MUTATING_TOOLS,
|
|
205
|
+
"--no-session-persistence",
|
|
206
|
+
"--mcp-config",
|
|
207
|
+
'{"mcpServers": {}}',
|
|
208
|
+
"--strict-mcp-config",
|
|
209
|
+
]
|
|
210
|
+
)
|
|
165
211
|
if system_prompt:
|
|
166
212
|
cmd.extend(["--append-system-prompt", system_prompt])
|
|
167
213
|
|
|
@@ -8,7 +8,8 @@ messaging without peeking at the adapter.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Any, Literal
|
|
12
13
|
|
|
13
14
|
from claude_smart import state
|
|
14
15
|
from claude_smart.reflexio_adapter import Adapter
|
|
@@ -16,6 +17,50 @@ from claude_smart.reflexio_adapter import Adapter
|
|
|
16
17
|
PublishStatus = Literal["nothing", "ok", "failed"]
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def _publish_request_id(session_id: str, start: int, end: int) -> str:
|
|
21
|
+
"""Return a stable request ID for one buffered publish range."""
|
|
22
|
+
|
|
23
|
+
name = f"claude-smart:{session_id}:{start}:{end}"
|
|
24
|
+
return str(uuid.uuid5(uuid.NAMESPACE_URL, name))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _prepare_publish_batch(
|
|
28
|
+
session_id: str,
|
|
29
|
+
) -> tuple[int, int, list[dict[str, Any]]] | None:
|
|
30
|
+
"""Freeze and return one canonical unpublished record range."""
|
|
31
|
+
|
|
32
|
+
while True:
|
|
33
|
+
records = state.read_all(session_id)
|
|
34
|
+
published, available = state.unpublished_slice(records)
|
|
35
|
+
if not available:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
publish_end = state.pending_publish_end(records, published)
|
|
39
|
+
if publish_end is None:
|
|
40
|
+
publish_end = state.safe_publish_end(records, published)
|
|
41
|
+
_, proposed = state.unpublished_slice(
|
|
42
|
+
records[:publish_end], published_override=published
|
|
43
|
+
)
|
|
44
|
+
if not proposed:
|
|
45
|
+
return None
|
|
46
|
+
state.append(
|
|
47
|
+
session_id,
|
|
48
|
+
{"publish_attempt": {"start": published, "end": publish_end}},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
canonical = state.read_all(session_id)
|
|
52
|
+
if state.published_record_offset(canonical) != published:
|
|
53
|
+
continue
|
|
54
|
+
canonical_end = state.pending_publish_end(canonical, published)
|
|
55
|
+
if canonical_end is None:
|
|
56
|
+
continue
|
|
57
|
+
_, interactions = state.unpublished_slice(
|
|
58
|
+
canonical[:canonical_end], published_override=published
|
|
59
|
+
)
|
|
60
|
+
if interactions:
|
|
61
|
+
return published, canonical_end, interactions
|
|
62
|
+
|
|
63
|
+
|
|
19
64
|
def publish_unpublished(
|
|
20
65
|
*,
|
|
21
66
|
session_id: str,
|
|
@@ -56,20 +101,27 @@ def publish_unpublished(
|
|
|
56
101
|
was unreachable. On ``"failed"`` the watermark is not advanced,
|
|
57
102
|
so the next hook retries the same batch.
|
|
58
103
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if not interactions:
|
|
104
|
+
batch = _prepare_publish_batch(session_id)
|
|
105
|
+
if batch is None:
|
|
62
106
|
return ("nothing", 0)
|
|
107
|
+
published_record_offset, publish_end, interactions = batch
|
|
108
|
+
|
|
63
109
|
client = adapter if adapter is not None else Adapter()
|
|
64
|
-
|
|
110
|
+
request_id = _publish_request_id(session_id, published_record_offset, publish_end)
|
|
111
|
+
result = client.publish(
|
|
65
112
|
session_id=session_id,
|
|
66
113
|
project_id=project_id,
|
|
114
|
+
request_id=request_id,
|
|
67
115
|
interactions=interactions,
|
|
68
116
|
force_extraction=force_extraction,
|
|
69
117
|
override_learning_stall=override_learning_stall,
|
|
70
118
|
skip_aggregation=skip_aggregation,
|
|
71
119
|
)
|
|
72
|
-
if
|
|
73
|
-
|
|
120
|
+
if result:
|
|
121
|
+
marker: dict[str, Any] = {"published_up_to": publish_end}
|
|
122
|
+
confirmed_request_id = result.request_id
|
|
123
|
+
if isinstance(confirmed_request_id, str) and confirmed_request_id:
|
|
124
|
+
marker["request_id"] = confirmed_request_id
|
|
125
|
+
state.append(session_id, marker)
|
|
74
126
|
return ("ok", len(interactions))
|
|
75
127
|
return ("failed", len(interactions))
|
|
@@ -20,7 +20,6 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
_ENV_URL = "REFLEXIO_URL"
|
|
22
22
|
_ENV_API_KEY = "REFLEXIO_API_KEY"
|
|
23
|
-
_DEFAULT_URL = "http://localhost:8071/"
|
|
24
23
|
# Cap every HTTP round-trip from a hook so a hung backend can't stall the
|
|
25
24
|
# Claude Code / Codex session. reflexio's client default is 300s, which is
|
|
26
25
|
# fine for batch workloads but unacceptable on the hook path — a single
|
|
@@ -28,18 +27,48 @@ _DEFAULT_URL = "http://localhost:8071/"
|
|
|
28
27
|
# in ``ids.py`` for short-lived hook HTTP calls.
|
|
29
28
|
_HTTP_TIMEOUT_SECONDS = 5
|
|
30
29
|
_SEARCH_MODE_HYBRID = "hybrid" # reflexio.models.config_schema.SearchMode.HYBRID
|
|
30
|
+
_SOURCE = "claude-smart"
|
|
31
31
|
_UNIFIED_ENTITY_TYPES = ("profiles", "user_playbooks", "agent_playbooks")
|
|
32
32
|
_AGENT_PLAYBOOK_APPROVAL_STATUSES = ("pending", "approved")
|
|
33
33
|
_REJECTED_AGENT_PLAYBOOK_STATUS = "rejected"
|
|
34
|
+
_LOCAL_8071_URLS = {
|
|
35
|
+
"http://localhost:8071",
|
|
36
|
+
"http://localhost:8071/",
|
|
37
|
+
"http://127.0.0.1:8071",
|
|
38
|
+
"http://127.0.0.1:8071/",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_url() -> str:
|
|
43
|
+
return f"http://localhost:{os.environ.get('BACKEND_PORT', '8071')}/"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _configured_url() -> str:
|
|
47
|
+
url = os.environ.get(_ENV_URL, "")
|
|
48
|
+
backend_port = os.environ.get("BACKEND_PORT", "")
|
|
49
|
+
if url in _LOCAL_8071_URLS and backend_port not in {"", "8071"}:
|
|
50
|
+
return _default_url()
|
|
51
|
+
return url or _default_url()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class PublishResult:
|
|
56
|
+
"""Outcome and server-confirmed lineage for one publish call."""
|
|
57
|
+
|
|
58
|
+
ok: bool
|
|
59
|
+
request_id: str | None = None
|
|
60
|
+
|
|
61
|
+
def __bool__(self) -> bool:
|
|
62
|
+
return self.ok
|
|
34
63
|
|
|
35
64
|
|
|
36
65
|
@dataclass
|
|
37
66
|
class Adapter:
|
|
38
67
|
"""Wraps the reflexio client and absorbs connection errors.
|
|
39
68
|
|
|
40
|
-
All methods degrade to a neutral no-op
|
|
41
|
-
connection failure so a missing or down reflexio server never
|
|
42
|
-
a
|
|
69
|
+
All methods degrade to a neutral no-op result (an empty list or a falsey
|
|
70
|
+
result) on connection failure so a missing or down reflexio server never
|
|
71
|
+
crashes a host hook.
|
|
43
72
|
"""
|
|
44
73
|
|
|
45
74
|
url: str = ""
|
|
@@ -48,7 +77,7 @@ class Adapter:
|
|
|
48
77
|
|
|
49
78
|
def __post_init__(self) -> None:
|
|
50
79
|
env_config.load_reflexio_env()
|
|
51
|
-
self.url = self.url or
|
|
80
|
+
self.url = self.url or _configured_url()
|
|
52
81
|
self.api_key = self.api_key or os.environ.get(_ENV_API_KEY, "")
|
|
53
82
|
self._client: Any | None = None
|
|
54
83
|
|
|
@@ -92,38 +121,96 @@ class Adapter:
|
|
|
92
121
|
*,
|
|
93
122
|
session_id: str,
|
|
94
123
|
project_id: str,
|
|
124
|
+
request_id: str | None = None,
|
|
95
125
|
interactions: Sequence[dict[str, Any]],
|
|
96
126
|
force_extraction: bool = False,
|
|
97
127
|
override_learning_stall: bool = False,
|
|
98
128
|
skip_aggregation: bool = False,
|
|
99
|
-
) ->
|
|
100
|
-
"""Publish
|
|
129
|
+
) -> PublishResult:
|
|
130
|
+
"""Publish interactions and return this call's confirmed request ID."""
|
|
101
131
|
if not interactions:
|
|
102
|
-
return True
|
|
132
|
+
return PublishResult(True)
|
|
103
133
|
client = self._get_client()
|
|
104
134
|
if client is None:
|
|
105
|
-
return False
|
|
135
|
+
return PublishResult(False)
|
|
106
136
|
try:
|
|
137
|
+
interaction_list = list(interactions)
|
|
138
|
+
raw_request = getattr(client, "_make_request", None)
|
|
139
|
+
supports_source = _supports_keyword(client.publish_interaction, "source")
|
|
140
|
+
if request_id is not None and callable(raw_request):
|
|
141
|
+
payload: dict[str, Any] = {
|
|
142
|
+
"request_id": request_id,
|
|
143
|
+
"user_id": project_id,
|
|
144
|
+
"interaction_data_list": interaction_list,
|
|
145
|
+
"agent_version": runtime.agent_version(),
|
|
146
|
+
"session_id": session_id,
|
|
147
|
+
"skip_aggregation": skip_aggregation,
|
|
148
|
+
"force_extraction": force_extraction,
|
|
149
|
+
"evaluation_only": False,
|
|
150
|
+
"override_learning_stall": override_learning_stall,
|
|
151
|
+
}
|
|
152
|
+
if supports_source:
|
|
153
|
+
payload["source"] = _SOURCE
|
|
154
|
+
try:
|
|
155
|
+
raw_request(
|
|
156
|
+
"POST",
|
|
157
|
+
"/api/publish_interaction",
|
|
158
|
+
json=payload,
|
|
159
|
+
params=None,
|
|
160
|
+
)
|
|
161
|
+
return PublishResult(True, request_id)
|
|
162
|
+
except Exception as exc: # noqa: BLE001
|
|
163
|
+
if not _needs_raw_retrieved_learning_publish(interaction_list):
|
|
164
|
+
raise
|
|
165
|
+
_LOGGER.warning(
|
|
166
|
+
"Could not confirm retrieved-learning links; retrying "
|
|
167
|
+
"the base interaction with the same request ID: %s",
|
|
168
|
+
exc,
|
|
169
|
+
)
|
|
170
|
+
fallback_payload = {
|
|
171
|
+
**payload,
|
|
172
|
+
"interaction_data_list": _without_retrieved_learnings(
|
|
173
|
+
interaction_list
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
raw_request(
|
|
177
|
+
"POST",
|
|
178
|
+
"/api/publish_interaction",
|
|
179
|
+
json=fallback_payload,
|
|
180
|
+
params=None,
|
|
181
|
+
)
|
|
182
|
+
return PublishResult(True, request_id)
|
|
183
|
+
if _needs_raw_retrieved_learning_publish(interaction_list):
|
|
184
|
+
_LOGGER.warning(
|
|
185
|
+
"Stable raw publishing is unavailable; publishing "
|
|
186
|
+
"without optional retrieved-learning links"
|
|
187
|
+
)
|
|
188
|
+
interaction_list = _without_retrieved_learnings(interaction_list)
|
|
107
189
|
kwargs = {
|
|
108
190
|
"user_id": project_id,
|
|
109
|
-
"interactions":
|
|
191
|
+
"interactions": interaction_list,
|
|
110
192
|
"agent_version": runtime.agent_version(),
|
|
111
193
|
"session_id": session_id,
|
|
112
194
|
"wait_for_response": False,
|
|
113
195
|
"force_extraction": force_extraction,
|
|
114
196
|
"skip_aggregation": skip_aggregation,
|
|
115
197
|
}
|
|
198
|
+
if supports_source:
|
|
199
|
+
kwargs["source"] = _SOURCE
|
|
116
200
|
if _supports_keyword(client.publish_interaction, "override_learning_stall"):
|
|
117
201
|
kwargs["override_learning_stall"] = override_learning_stall
|
|
118
202
|
elif override_learning_stall:
|
|
119
203
|
_LOGGER.debug(
|
|
120
204
|
"publish_interaction client does not support override_learning_stall"
|
|
121
205
|
)
|
|
122
|
-
client.publish_interaction(**kwargs)
|
|
123
|
-
|
|
206
|
+
response = client.publish_interaction(**kwargs)
|
|
207
|
+
response_request_id = getattr(response, "request_id", None)
|
|
208
|
+
if isinstance(response_request_id, str) and response_request_id:
|
|
209
|
+
return PublishResult(True, response_request_id)
|
|
210
|
+
return PublishResult(True)
|
|
124
211
|
except Exception as exc: # noqa: BLE001
|
|
125
212
|
_LOGGER.warning("publish_interaction failed: %s", exc)
|
|
126
|
-
return False
|
|
213
|
+
return PublishResult(False)
|
|
127
214
|
|
|
128
215
|
def apply_extraction_defaults(self, *, window_size: int, stride_size: int) -> bool:
|
|
129
216
|
"""Push claude-smart's preferred extraction defaults to the reflexio server.
|
|
@@ -340,7 +427,12 @@ class Adapter:
|
|
|
340
427
|
# -----------------------------------------------------------------
|
|
341
428
|
|
|
342
429
|
def search_all(
|
|
343
|
-
self,
|
|
430
|
+
self,
|
|
431
|
+
*,
|
|
432
|
+
project_id: str,
|
|
433
|
+
query: str,
|
|
434
|
+
top_k: int = 5,
|
|
435
|
+
session_id: str | None = None,
|
|
344
436
|
) -> tuple[list[Any], list[Any], list[Any]]:
|
|
345
437
|
"""Unified hybrid search → ``(user_playbooks, agent_playbooks, preferences)``.
|
|
346
438
|
|
|
@@ -354,6 +446,10 @@ class Adapter:
|
|
|
354
446
|
project_id (str): reflexio ``user_id`` for this repo.
|
|
355
447
|
query (str): Free-text query routed through BM25 + vector RRF.
|
|
356
448
|
top_k (int): Cap on results per entity type.
|
|
449
|
+
session_id (str | None): Claude Code session id. When set, the
|
|
450
|
+
server skips results it already returned to this session
|
|
451
|
+
and backfills next-best matches, so repeated hook searches
|
|
452
|
+
within one session stop re-injecting the same rules.
|
|
357
453
|
|
|
358
454
|
Returns:
|
|
359
455
|
tuple[list[Any], list[Any], list[Any]]: ``(user_playbooks,
|
|
@@ -374,6 +470,7 @@ class Adapter:
|
|
|
374
470
|
enable_agent_answer=False,
|
|
375
471
|
top_k=top_k,
|
|
376
472
|
search_mode=_SEARCH_MODE_HYBRID,
|
|
473
|
+
session_id=session_id,
|
|
377
474
|
)
|
|
378
475
|
except Exception as exc: # noqa: BLE001
|
|
379
476
|
self._record_read_error("unified search", exc)
|
|
@@ -449,6 +546,32 @@ def _supports_keyword(callable_obj: Any, keyword: str) -> bool:
|
|
|
449
546
|
return keyword in signature.parameters
|
|
450
547
|
|
|
451
548
|
|
|
549
|
+
def _needs_raw_retrieved_learning_publish(
|
|
550
|
+
interactions: Sequence[dict[str, Any]],
|
|
551
|
+
) -> bool:
|
|
552
|
+
"""Return whether links require a caller-stable raw request.
|
|
553
|
+
|
|
554
|
+
The public client does not accept a caller-supplied request ID, and older
|
|
555
|
+
clients also strip this field. The authenticated helper preserves both.
|
|
556
|
+
"""
|
|
557
|
+
return any(item.get("retrieved_learnings") for item in interactions)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _without_retrieved_learnings(
|
|
561
|
+
interactions: Sequence[dict[str, Any]],
|
|
562
|
+
) -> list[dict[str, Any]]:
|
|
563
|
+
"""Copy interactions without the optional compatibility-only field."""
|
|
564
|
+
|
|
565
|
+
return [
|
|
566
|
+
{
|
|
567
|
+
key: value
|
|
568
|
+
for key, value in interaction.items()
|
|
569
|
+
if key != "retrieved_learnings"
|
|
570
|
+
}
|
|
571
|
+
for interaction in interactions
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
|
|
452
575
|
def _filter_rejected_agent_playbooks(items: list[Any]) -> list[Any]:
|
|
453
576
|
"""Drop rejected shared skills defensively, even if an older backend ignores filters."""
|
|
454
577
|
return [
|
|
@@ -15,27 +15,36 @@ INTERNAL_ENV = "CLAUDE_SMART_INTERNAL"
|
|
|
15
15
|
HOST_CLAUDE_CODE = "claude-code"
|
|
16
16
|
HOST_CODEX = "codex"
|
|
17
17
|
HOST_OPENCODE = "opencode"
|
|
18
|
+
HOST_UNKNOWN = "unknown"
|
|
18
19
|
VALID_HOSTS = frozenset({HOST_CLAUDE_CODE, HOST_CODEX, HOST_OPENCODE})
|
|
19
20
|
|
|
20
21
|
_SHARED_AGENT_VERSION = "claude-code"
|
|
21
22
|
_current_host: str | None = None
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _resolve_host(value: str | None, fallback: str) -> str:
|
|
26
|
+
return value if value in VALID_HOSTS else fallback
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
def set_host(value: str | None) -> str:
|
|
25
30
|
"""Set the current host, returning the normalized value."""
|
|
26
31
|
global _current_host
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
_current_host = value if value is not None else HOST_UNKNOWN
|
|
33
|
+
host = _resolve_host(_current_host, HOST_CLAUDE_CODE)
|
|
29
34
|
os.environ[HOST_ENV] = host
|
|
30
35
|
return host
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
def host() -> str:
|
|
34
39
|
"""Return the current host, defaulting to Claude Code for compatibility."""
|
|
35
|
-
if _current_host is not None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
|
|
41
|
+
return _resolve_host(value, HOST_CLAUDE_CODE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def attribution_host() -> str:
|
|
45
|
+
"""Return the explicitly selected record host, or unknown when unset."""
|
|
46
|
+
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
|
|
47
|
+
return _resolve_host(value, HOST_UNKNOWN)
|
|
39
48
|
|
|
40
49
|
|
|
41
50
|
def is_codex() -> bool:
|