claude-smart 0.2.23 → 0.2.25
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +76 -28
- package/bin/claude-smart.js +355 -11
- package/package.json +11 -1
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/.codex-plugin/plugin.json +35 -0
- package/plugin/LICENSE +202 -0
- package/plugin/README.md +37 -0
- package/plugin/bin/cs-cite +77 -0
- package/plugin/commands/clear-all.md +8 -0
- package/plugin/commands/dashboard.md +8 -0
- package/plugin/commands/learn.md +12 -0
- package/plugin/commands/restart.md +8 -0
- package/plugin/commands/show.md +8 -0
- package/plugin/dashboard/AGENTS.md +6 -0
- package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
- package/plugin/dashboard/app/api/config/route.ts +16 -0
- package/plugin/dashboard/app/api/health/route.ts +10 -0
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
- package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
- package/plugin/dashboard/app/api/sessions/route.ts +14 -0
- package/plugin/dashboard/app/configure/env/page.tsx +318 -0
- package/plugin/dashboard/app/configure/layout.tsx +47 -0
- package/plugin/dashboard/app/configure/page.tsx +5 -0
- package/plugin/dashboard/app/configure/server/page.tsx +258 -0
- package/plugin/dashboard/app/dashboard/page.tsx +227 -0
- package/plugin/dashboard/app/globals.css +129 -0
- package/plugin/dashboard/app/icon.png +0 -0
- package/plugin/dashboard/app/layout.tsx +40 -0
- package/plugin/dashboard/app/page.tsx +5 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
- package/plugin/dashboard/app/preferences/page.tsx +126 -0
- package/plugin/dashboard/app/providers.tsx +12 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
- package/plugin/dashboard/app/sessions/page.tsx +186 -0
- package/plugin/dashboard/app/skills/page.tsx +362 -0
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
- package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
- package/plugin/dashboard/components/common/empty-state.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
- package/plugin/dashboard/components/common/page-header.tsx +34 -0
- package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
- package/plugin/dashboard/components/common/stat-card.tsx +38 -0
- package/plugin/dashboard/components/layout/nav-items.ts +22 -0
- package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
- package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
- package/plugin/dashboard/components/stall-banner.tsx +53 -0
- package/plugin/dashboard/components/ui/badge.tsx +52 -0
- package/plugin/dashboard/components/ui/button.tsx +60 -0
- package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
- package/plugin/dashboard/components/ui/input.tsx +20 -0
- package/plugin/dashboard/components/ui/label.tsx +20 -0
- package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
- package/plugin/dashboard/components/ui/select.tsx +201 -0
- package/plugin/dashboard/components/ui/separator.tsx +25 -0
- package/plugin/dashboard/components/ui/sheet.tsx +135 -0
- package/plugin/dashboard/components/ui/switch.tsx +32 -0
- package/plugin/dashboard/components.json +25 -0
- package/plugin/dashboard/eslint.config.mjs +16 -0
- package/plugin/dashboard/hooks/use-settings.tsx +88 -0
- package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
- package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
- package/plugin/dashboard/lib/config-file.ts +131 -0
- package/plugin/dashboard/lib/format.ts +58 -0
- package/plugin/dashboard/lib/reflexio-client.ts +238 -0
- package/plugin/dashboard/lib/reflexio-url.ts +17 -0
- package/plugin/dashboard/lib/session-reader.ts +245 -0
- package/plugin/dashboard/lib/status.ts +24 -0
- package/plugin/dashboard/lib/types.ts +145 -0
- package/plugin/dashboard/lib/utils.ts +6 -0
- package/plugin/dashboard/next.config.ts +7 -0
- package/plugin/dashboard/package-lock.json +10275 -0
- package/plugin/dashboard/package.json +37 -0
- package/plugin/dashboard/postcss.config.mjs +7 -0
- package/plugin/dashboard/public/claude-smart-icon.png +0 -0
- package/plugin/dashboard/tsconfig.json +34 -0
- package/plugin/hooks/codex-hooks.json +67 -0
- package/plugin/hooks/hooks.json +111 -0
- package/plugin/pyproject.toml +49 -0
- package/plugin/scripts/_codex_env.sh +27 -0
- package/plugin/scripts/_lib.sh +325 -0
- package/plugin/scripts/backend-service.sh +208 -0
- package/plugin/scripts/cli.sh +40 -0
- package/plugin/scripts/dashboard-build.sh +139 -0
- package/plugin/scripts/dashboard-open.sh +107 -0
- package/plugin/scripts/dashboard-service.sh +195 -0
- package/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/scripts/hook_entry.sh +70 -0
- package/plugin/scripts/smart-install.sh +411 -0
- package/plugin/src/claude_smart/__init__.py +3 -0
- package/plugin/src/claude_smart/cli.py +1342 -0
- package/plugin/src/claude_smart/context_format.py +277 -0
- package/plugin/src/claude_smart/context_inject.py +92 -0
- package/plugin/src/claude_smart/cs_cite.py +236 -0
- package/plugin/src/claude_smart/events/__init__.py +1 -0
- package/plugin/src/claude_smart/events/post_tool.py +148 -0
- package/plugin/src/claude_smart/events/pre_tool.py +52 -0
- package/plugin/src/claude_smart/events/session_end.py +20 -0
- package/plugin/src/claude_smart/events/session_start.py +119 -0
- package/plugin/src/claude_smart/events/stop.py +393 -0
- package/plugin/src/claude_smart/events/user_prompt.py +73 -0
- package/plugin/src/claude_smart/hook.py +114 -0
- package/plugin/src/claude_smart/ids.py +56 -0
- package/plugin/src/claude_smart/internal_call.py +89 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
- package/plugin/src/claude_smart/publish.py +71 -0
- package/plugin/src/claude_smart/query_compose.py +51 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
- package/plugin/src/claude_smart/runtime.py +52 -0
- package/plugin/src/claude_smart/stall_banner.py +61 -0
- package/plugin/src/claude_smart/state.py +276 -0
- package/plugin/uv.lock +3720 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""PostToolUse hook — record the tool invocation and its outcome."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from claude_smart import state
|
|
10
|
+
|
|
11
|
+
# Tool inputs are persisted locally and later published to reflexio, so we
|
|
12
|
+
# apply a conservative redaction pass at ingestion time. Chosen to avoid
|
|
13
|
+
# false positives over maximal coverage — the dashboard shows these
|
|
14
|
+
# verbatim, and users noticing a masked command is far less surprising
|
|
15
|
+
# than a masked `LOG_LEVEL=INFO`.
|
|
16
|
+
_MAX_STR_LEN = 4096
|
|
17
|
+
_SECRET_ASSIGNMENT = re.compile(
|
|
18
|
+
r"(?P<key>[A-Z][A-Z0-9_]{2,})=(?P<quote>['\"]?)"
|
|
19
|
+
r"(?P<value>[A-Za-z0-9+/=_\-]{20,})(?P=quote)"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _looks_like_secret(value: str) -> bool:
|
|
24
|
+
"""Heuristic: mixed-case letters plus digits suggest a high-entropy token."""
|
|
25
|
+
has_lower = any(c.islower() for c in value)
|
|
26
|
+
has_upper = any(c.isupper() for c in value)
|
|
27
|
+
has_digit = any(c.isdigit() for c in value)
|
|
28
|
+
return has_lower and has_upper and has_digit
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _mask_secrets(text: str) -> str:
|
|
32
|
+
def sub(match: "re.Match[str]") -> str:
|
|
33
|
+
value = match.group("value")
|
|
34
|
+
if not _looks_like_secret(value):
|
|
35
|
+
return match.group(0)
|
|
36
|
+
key = match.group("key")
|
|
37
|
+
quote = match.group("quote")
|
|
38
|
+
return f"{key}={quote}<redacted:{len(value)}>{quote}"
|
|
39
|
+
|
|
40
|
+
return _SECRET_ASSIGNMENT.sub(sub, text)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _redact_string(value: str) -> str:
|
|
44
|
+
masked = _mask_secrets(value)
|
|
45
|
+
if len(masked) > _MAX_STR_LEN:
|
|
46
|
+
return masked[:_MAX_STR_LEN] + "…(truncated)"
|
|
47
|
+
return masked
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _redact(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
"""Redact obvious secrets and truncate oversized string values.
|
|
52
|
+
|
|
53
|
+
Only top-level string values are inspected — nested dicts/lists and
|
|
54
|
+
non-string scalars pass through unchanged. Claude Code tool payloads
|
|
55
|
+
are flat in practice (Bash.command, Edit.file_path, etc.), so deeper
|
|
56
|
+
recursion isn't worth the false-positive surface.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tool_input (dict[str, Any]): Raw ``tool_input`` from the PostToolUse
|
|
60
|
+
payload.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
dict[str, Any]: New dict with redaction applied.
|
|
64
|
+
"""
|
|
65
|
+
return {
|
|
66
|
+
k: _redact_string(v) if isinstance(v, str) else v for k, v in tool_input.items()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _derive_status(tool_response: Any) -> str:
|
|
71
|
+
"""Classify the tool outcome as 'success' or 'error'.
|
|
72
|
+
|
|
73
|
+
Claude Code's PostToolUse payload puts the tool response under
|
|
74
|
+
``tool_response``, which may be a dict (with an ``is_error`` / ``error``
|
|
75
|
+
field) or a bare string. Unknown shapes default to success.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(tool_response, dict):
|
|
78
|
+
if tool_response.get("is_error") or tool_response.get("error"):
|
|
79
|
+
return "error"
|
|
80
|
+
return "success"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_OUTPUT_TEXT_KEYS = ("stdout", "stderr", "output", "content", "text", "error")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _flatten_tool_response_text(tool_response: Any) -> str:
|
|
87
|
+
"""Flatten ``tool_response`` into a single string for buffering.
|
|
88
|
+
|
|
89
|
+
Claude Code delivers tool responses in heterogeneous shapes — Bash sends
|
|
90
|
+
a dict with ``stdout``/``stderr``, Edit/Read send a string or a dict
|
|
91
|
+
with ``content``/``output``, and failures populate ``error``. Joining
|
|
92
|
+
the well-known string-valued keys preserves the parts most useful for
|
|
93
|
+
downstream learning (failure messages, command output) without
|
|
94
|
+
serializing entire structured payloads.
|
|
95
|
+
|
|
96
|
+
Note: structured ``content`` lists (e.g. ``[{type: 'text', text: ...}]``)
|
|
97
|
+
are not flattened — only top-level string values are joined. If a tool
|
|
98
|
+
starts emitting block-form content we want to capture, add a dedicated
|
|
99
|
+
branch here.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tool_response (Any): The raw ``tool_response`` from the PostToolUse
|
|
103
|
+
payload.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: A flattened string. Empty when no textual content is present.
|
|
107
|
+
"""
|
|
108
|
+
if tool_response is None:
|
|
109
|
+
return ""
|
|
110
|
+
if isinstance(tool_response, str):
|
|
111
|
+
return tool_response
|
|
112
|
+
if isinstance(tool_response, dict):
|
|
113
|
+
parts = [
|
|
114
|
+
tool_response[key]
|
|
115
|
+
for key in _OUTPUT_TEXT_KEYS
|
|
116
|
+
if isinstance(tool_response.get(key), str) and tool_response[key]
|
|
117
|
+
]
|
|
118
|
+
if parts:
|
|
119
|
+
return "\n".join(parts)
|
|
120
|
+
for key in ("text", "content"):
|
|
121
|
+
value = tool_response.get(key)
|
|
122
|
+
if isinstance(value, str):
|
|
123
|
+
return value
|
|
124
|
+
return ""
|
|
125
|
+
for attr in ("text", "content"):
|
|
126
|
+
value = getattr(tool_response, attr, None)
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
return value
|
|
129
|
+
return ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def handle(payload: dict[str, Any]) -> None:
|
|
133
|
+
session_id = payload.get("session_id")
|
|
134
|
+
tool_name = payload.get("tool_name") or ""
|
|
135
|
+
if not session_id or not tool_name:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
tool_response = payload.get("tool_response")
|
|
139
|
+
output_text = _flatten_tool_response_text(tool_response)
|
|
140
|
+
record = {
|
|
141
|
+
"ts": int(time.time()),
|
|
142
|
+
"role": "Assistant_tool",
|
|
143
|
+
"tool_name": tool_name,
|
|
144
|
+
"tool_input": _redact(payload.get("tool_input") or {}),
|
|
145
|
+
"tool_output": _redact_string(output_text) if output_text else "",
|
|
146
|
+
"status": _derive_status(tool_response),
|
|
147
|
+
}
|
|
148
|
+
state.append(session_id, record)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""PreToolUse hook — just-in-time skill + preference inject before a mutating tool.
|
|
2
|
+
|
|
3
|
+
Fires only for tools listed in ``hooks.json``'s PreToolUse matcher
|
|
4
|
+
(Edit/Write/NotebookEdit/Bash). Composes a query from the tool call and
|
|
5
|
+
delegates to ``context_inject.emit_context`` for the shared
|
|
6
|
+
search-render-emit pipeline, falling back to ``hook.emit_continue`` when
|
|
7
|
+
there is nothing to inject or the search raises.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from claude_smart import context_inject, hook, ids, query_compose, runtime
|
|
16
|
+
|
|
17
|
+
_LOGGER = logging.getLogger(__name__)
|
|
18
|
+
_TOP_K = 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle(payload: dict[str, Any]) -> None:
|
|
22
|
+
"""PreToolUse dispatcher — never raises; degrades to ``emit_continue``."""
|
|
23
|
+
if runtime.is_codex():
|
|
24
|
+
hook.emit_continue()
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
session_id = payload.get("session_id")
|
|
28
|
+
tool_name = payload.get("tool_name")
|
|
29
|
+
tool_input = payload.get("tool_input") or {}
|
|
30
|
+
if not session_id or not tool_name:
|
|
31
|
+
hook.emit_continue()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
query = query_compose.from_tool_call(tool_name, tool_input)
|
|
35
|
+
if not query:
|
|
36
|
+
hook.emit_continue()
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
project_id = ids.resolve_project_id(payload.get("cwd"))
|
|
40
|
+
try:
|
|
41
|
+
emitted = context_inject.emit_context(
|
|
42
|
+
session_id=session_id,
|
|
43
|
+
project_id=project_id,
|
|
44
|
+
query=query,
|
|
45
|
+
hook_event_name="PreToolUse",
|
|
46
|
+
top_k=_TOP_K,
|
|
47
|
+
)
|
|
48
|
+
except Exception as exc: # noqa: BLE001 — never block a tool call.
|
|
49
|
+
_LOGGER.debug("pre_tool context inject failed: %s", exc)
|
|
50
|
+
emitted = False
|
|
51
|
+
if not emitted:
|
|
52
|
+
hook.emit_continue()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""SessionEnd hook — flush any remaining interactions; extraction runs async."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from claude_smart import ids, publish
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle(payload: dict[str, Any]) -> None:
|
|
11
|
+
session_id = payload.get("session_id")
|
|
12
|
+
if not session_id:
|
|
13
|
+
return
|
|
14
|
+
project_id = ids.resolve_project_id(payload.get("cwd"))
|
|
15
|
+
publish.publish_unpublished(
|
|
16
|
+
session_id=session_id,
|
|
17
|
+
project_id=project_id,
|
|
18
|
+
force_extraction=False,
|
|
19
|
+
skip_aggregation=False,
|
|
20
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""SessionStart hook — apply startup defaults without broad memory retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from claude_smart import hook
|
|
12
|
+
from claude_smart.reflexio_adapter import Adapter
|
|
13
|
+
from claude_smart.stall_banner import render_banner
|
|
14
|
+
|
|
15
|
+
# Claude-smart's preferred extraction cadence — more frequent, smaller windows
|
|
16
|
+
# than reflexio's out-of-box 10/5. Applied idempotently to the reflexio server
|
|
17
|
+
# on every SessionStart via Adapter.apply_extraction_defaults.
|
|
18
|
+
_CLAUDE_SMART_WINDOW_SIZE = 5
|
|
19
|
+
_CLAUDE_SMART_STRIDE_SIZE = 3
|
|
20
|
+
# Optimizer is on by default. Set this env var to "0" to skip pushing the
|
|
21
|
+
# claude-smart optimizer defaults on SessionStart (kill switch).
|
|
22
|
+
_DISABLE_OPTIMIZER_ENV = "CLAUDE_SMART_ENABLE_OPTIMIZER"
|
|
23
|
+
_OPTIMIZER_TIMEOUT_SECONDS = 300
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _adapter() -> Adapter:
|
|
27
|
+
"""Construct the reflexio adapter for this hook invocation.
|
|
28
|
+
|
|
29
|
+
Indirected through a factory so tests can monkeypatch the adapter
|
|
30
|
+
construction without touching the ``Adapter`` class itself.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Adapter: A fresh adapter bound to the current process env.
|
|
34
|
+
"""
|
|
35
|
+
return Adapter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _stall_banner(adapter: Any) -> str:
|
|
39
|
+
"""Return the prepend-able stall banner, or "" if no banner should fire.
|
|
40
|
+
|
|
41
|
+
Reads ``adapter.fetch_stall_state()``; if it reports an active, not-yet-
|
|
42
|
+
notified stall, renders a one-line banner via ``stall_banner.render_banner``.
|
|
43
|
+
All exceptions are absorbed: this is defense-in-depth — even though the
|
|
44
|
+
hook dispatcher already wraps ``handle`` in try/except, a stall-path bug
|
|
45
|
+
must never block the existing playbook/profile rendering.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
adapter (Any): The adapter to query. Duck-typed so tests can stub.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: The banner text, or ``""`` when there is nothing to show.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
state_obj = adapter.fetch_stall_state()
|
|
55
|
+
except Exception: # noqa: BLE001 — stall path must never crash the hook.
|
|
56
|
+
return ""
|
|
57
|
+
if state_obj is None:
|
|
58
|
+
return ""
|
|
59
|
+
if not getattr(state_obj, "stalled", False):
|
|
60
|
+
return ""
|
|
61
|
+
if getattr(state_obj, "notified_in_cc", False):
|
|
62
|
+
return ""
|
|
63
|
+
try:
|
|
64
|
+
return render_banner(
|
|
65
|
+
reason=getattr(state_obj, "reason", None),
|
|
66
|
+
reset_estimate=getattr(state_obj, "reset_estimate", None),
|
|
67
|
+
)
|
|
68
|
+
except Exception: # noqa: BLE001 — render_banner bug must not block playbook injection.
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def handle(payload: dict[str, Any]) -> None:
|
|
73
|
+
session_id = payload.get("session_id")
|
|
74
|
+
if not session_id:
|
|
75
|
+
hook.emit_continue()
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
adapter = _adapter()
|
|
79
|
+
|
|
80
|
+
# Stall banner — prepended to additionalContext, fires at most once per
|
|
81
|
+
# stall event (controlled server-side via mark_stall_notified).
|
|
82
|
+
banner = _stall_banner(adapter)
|
|
83
|
+
|
|
84
|
+
adapter.apply_extraction_defaults(
|
|
85
|
+
window_size=_CLAUDE_SMART_WINDOW_SIZE,
|
|
86
|
+
stride_size=_CLAUDE_SMART_STRIDE_SIZE,
|
|
87
|
+
)
|
|
88
|
+
if os.environ.get(_DISABLE_OPTIMIZER_ENV) != "0":
|
|
89
|
+
adapter.apply_optimizer_defaults(
|
|
90
|
+
script_path=_optimizer_assistant_path(),
|
|
91
|
+
timeout_seconds=_OPTIMIZER_TIMEOUT_SECONDS,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not banner:
|
|
95
|
+
hook.emit_continue()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
sys.stdout.write(
|
|
99
|
+
json.dumps(
|
|
100
|
+
{
|
|
101
|
+
"hookSpecificOutput": {
|
|
102
|
+
"hookEventName": "SessionStart",
|
|
103
|
+
"additionalContext": banner,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
sys.stdout.write("\n")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
adapter.mark_stall_notified()
|
|
112
|
+
except Exception: # noqa: BLE001 — telemetry must not break session.
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _optimizer_assistant_path() -> str:
|
|
117
|
+
executable = Path(sys.executable)
|
|
118
|
+
suffix = ".exe" if os.name == "nt" else ""
|
|
119
|
+
return str(executable.with_name(f"claude-smart-optimizer-assistant{suffix}"))
|