claude-smart 0.2.28 → 0.2.29
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 +22 -1
- package/bin/claude-smart.js +333 -73
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +4 -0
- package/plugin/hooks/codex-hooks.json +5 -0
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +38 -0
- package/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/scripts/backend-service.sh +15 -11
- package/plugin/scripts/cli.sh +27 -3
- package/plugin/scripts/codex-claude-compat +9 -0
- package/plugin/scripts/codex-claude-compat.cmd +4 -0
- package/plugin/scripts/codex-claude-compat.js +162 -0
- package/plugin/scripts/codex-hook.js +30 -2
- package/plugin/scripts/smart-install.sh +136 -50
- package/plugin/src/claude_smart/cli.py +101 -2
- package/plugin/src/claude_smart/context_inject.py +2 -4
- package/plugin/src/claude_smart/cs_cite.py +2 -90
- package/plugin/src/claude_smart/events/stop.py +16 -42
- package/plugin/src/claude_smart/internal_call.py +23 -0
- package/plugin/src/claude_smart/state.py +3 -3
- package/plugin/uv.lock +73 -76
- package/plugin/bin/cs-cite +0 -77
- package/plugin/scripts/codex-claude-compat.py +0 -144
|
@@ -69,6 +69,8 @@ _DASHBOARD_DIR = _PLUGIN_ROOT / "dashboard"
|
|
|
69
69
|
_BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh"
|
|
70
70
|
_DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh"
|
|
71
71
|
_REFLEXIO_DIR = Path.home() / ".reflexio"
|
|
72
|
+
_STATE_DIR = Path.home() / ".claude-smart"
|
|
73
|
+
_INSTALL_FAILURE_MARKER = _STATE_DIR / "install-failed"
|
|
72
74
|
_DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
|
|
73
75
|
_REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json"
|
|
74
76
|
_LOCAL_STORAGE_ENV = "LOCAL_STORAGE_PATH"
|
|
@@ -76,8 +78,11 @@ _CODEX_REQUIRED_FILES = (
|
|
|
76
78
|
Path(".agents/plugins/marketplace.json"),
|
|
77
79
|
Path("plugin/.codex-plugin/plugin.json"),
|
|
78
80
|
Path("plugin/hooks/codex-hooks.json"),
|
|
79
|
-
Path("plugin/scripts/codex-claude-compat
|
|
81
|
+
Path("plugin/scripts/codex-claude-compat"),
|
|
82
|
+
Path("plugin/scripts/codex-claude-compat.cmd"),
|
|
83
|
+
Path("plugin/scripts/codex-claude-compat.js"),
|
|
80
84
|
Path("plugin/scripts/codex-hook.js"),
|
|
85
|
+
Path("plugin/scripts/backend-log-runner.sh"),
|
|
81
86
|
Path("plugin/scripts/_codex_env.sh"),
|
|
82
87
|
)
|
|
83
88
|
_COPYTREE_IGNORE = shutil.ignore_patterns(
|
|
@@ -125,6 +130,86 @@ def _seed_reflexio_env() -> list[str]:
|
|
|
125
130
|
return missing
|
|
126
131
|
|
|
127
132
|
|
|
133
|
+
def _find_claude_code_plugin_root() -> Path | None:
|
|
134
|
+
"""Locate the installed Claude Code plugin root after native install."""
|
|
135
|
+
cache_root = (
|
|
136
|
+
Path.home()
|
|
137
|
+
/ ".claude"
|
|
138
|
+
/ "plugins"
|
|
139
|
+
/ "cache"
|
|
140
|
+
/ _CODEX_MARKETPLACE_NAME
|
|
141
|
+
/ "claude-smart"
|
|
142
|
+
)
|
|
143
|
+
candidates: list[Path] = []
|
|
144
|
+
if cache_root.is_dir():
|
|
145
|
+
for child in cache_root.iterdir():
|
|
146
|
+
if (
|
|
147
|
+
child.is_dir()
|
|
148
|
+
and (child / "pyproject.toml").is_file()
|
|
149
|
+
and (child / "scripts" / "smart-install.sh").is_file()
|
|
150
|
+
):
|
|
151
|
+
candidates.append(child)
|
|
152
|
+
candidates.sort(key=lambda path: path.stat().st_mtime, reverse=True)
|
|
153
|
+
candidates.extend(
|
|
154
|
+
[
|
|
155
|
+
Path.home()
|
|
156
|
+
/ ".claude"
|
|
157
|
+
/ "plugins"
|
|
158
|
+
/ "marketplaces"
|
|
159
|
+
/ _CODEX_MARKETPLACE_NAME
|
|
160
|
+
/ "plugin",
|
|
161
|
+
_PLUGIN_ROOT,
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
for candidate in candidates:
|
|
165
|
+
if (
|
|
166
|
+
(candidate / "pyproject.toml").is_file()
|
|
167
|
+
and (candidate / "scripts" / "smart-install.sh").is_file()
|
|
168
|
+
):
|
|
169
|
+
return candidate
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _force_plugin_root(plugin_root: Path) -> None:
|
|
174
|
+
"""Point ~/.reflexio/plugin-root at the installed plugin root."""
|
|
175
|
+
_REFLEXIO_DIR.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
link = _REFLEXIO_DIR / "plugin-root"
|
|
177
|
+
if link.is_symlink() or link.is_file():
|
|
178
|
+
link.unlink()
|
|
179
|
+
elif link.exists():
|
|
180
|
+
raise OSError(f"refusing to replace non-symlink plugin-root at {link}")
|
|
181
|
+
try:
|
|
182
|
+
link.symlink_to(plugin_root, target_is_directory=True)
|
|
183
|
+
except OSError:
|
|
184
|
+
if link.exists():
|
|
185
|
+
raise
|
|
186
|
+
(_REFLEXIO_DIR / "plugin-root.txt").write_text(f"{plugin_root}\n")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _bootstrap_claude_code_install() -> tuple[bool, str]:
|
|
190
|
+
"""Run smart-install immediately for the installed Claude Code plugin."""
|
|
191
|
+
plugin_root = _find_claude_code_plugin_root()
|
|
192
|
+
if plugin_root is None:
|
|
193
|
+
return False, "could not locate installed Claude Code plugin root after install"
|
|
194
|
+
try:
|
|
195
|
+
_force_plugin_root(plugin_root)
|
|
196
|
+
except OSError as exc:
|
|
197
|
+
return False, str(exc)
|
|
198
|
+
bash = shutil.which("bash")
|
|
199
|
+
if not bash:
|
|
200
|
+
return False, "bash is required to bootstrap claude-smart dependencies"
|
|
201
|
+
result = subprocess.run(
|
|
202
|
+
[bash, str(plugin_root / "scripts" / "smart-install.sh")],
|
|
203
|
+
cwd=plugin_root,
|
|
204
|
+
)
|
|
205
|
+
if result.returncode != 0:
|
|
206
|
+
return False, f"smart-install.sh failed in {plugin_root}"
|
|
207
|
+
if _INSTALL_FAILURE_MARKER.is_file():
|
|
208
|
+
reason = _INSTALL_FAILURE_MARKER.read_text().strip() or "unknown error"
|
|
209
|
+
return False, reason
|
|
210
|
+
return True, str(plugin_root)
|
|
211
|
+
|
|
212
|
+
|
|
128
213
|
def _missing_codex_marketplace_files(root: Path) -> list[Path]:
|
|
129
214
|
return [entry for entry in _CODEX_REQUIRED_FILES if not (root / entry).is_file()]
|
|
130
215
|
|
|
@@ -789,7 +874,21 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
789
874
|
if added:
|
|
790
875
|
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
791
876
|
|
|
792
|
-
|
|
877
|
+
bootstrapped, message = _bootstrap_claude_code_install()
|
|
878
|
+
if not bootstrapped:
|
|
879
|
+
sys.stderr.write(
|
|
880
|
+
f"error: claude-smart installed, but dependency bootstrap failed: {message}\n"
|
|
881
|
+
)
|
|
882
|
+
sys.stderr.write(
|
|
883
|
+
"Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n"
|
|
884
|
+
)
|
|
885
|
+
return 1
|
|
886
|
+
sys.stdout.write(f"Prepared claude-smart runtime at {message}.\n")
|
|
887
|
+
|
|
888
|
+
sys.stdout.write(
|
|
889
|
+
"\nclaude-smart installed and dependencies are prepared. "
|
|
890
|
+
"Restart Claude Code in your project.\n"
|
|
891
|
+
)
|
|
793
892
|
return 0
|
|
794
893
|
|
|
795
894
|
|
|
@@ -6,8 +6,7 @@ search, (b) render the hits with ``context_format.render_inline_with_registry``,
|
|
|
6
6
|
(d) emit a Claude Code ``hookSpecificOutput.additionalContext`` envelope
|
|
7
7
|
on stdout. This module owns that shared pipeline so the two hook
|
|
8
8
|
handlers keep exactly one source of truth for the injection contract —
|
|
9
|
-
the envelope shape, the registry schema, and the
|
|
10
|
-
``ensure_installed`` / ``append_injected``.
|
|
9
|
+
the envelope shape, the registry schema, and the injected context append.
|
|
11
10
|
|
|
12
11
|
The caller remains responsible for handler-specific framing (PreToolUse
|
|
13
12
|
needs ``hook.emit_continue()`` on the empty path; UserPromptSubmit wraps
|
|
@@ -21,7 +20,7 @@ import json
|
|
|
21
20
|
import sys
|
|
22
21
|
import time
|
|
23
22
|
|
|
24
|
-
from claude_smart import context_format,
|
|
23
|
+
from claude_smart import context_format, state
|
|
25
24
|
from claude_smart.reflexio_adapter import Adapter
|
|
26
25
|
|
|
27
26
|
|
|
@@ -69,7 +68,6 @@ def emit_context(
|
|
|
69
68
|
if not markdown:
|
|
70
69
|
return False
|
|
71
70
|
|
|
72
|
-
cs_cite.ensure_installed()
|
|
73
71
|
state.append_injected(
|
|
74
72
|
session_id,
|
|
75
73
|
(dict(entry, ts=int(time.time())) for entry in registry),
|
|
@@ -11,8 +11,7 @@ impactful replies with a marker like::
|
|
|
11
11
|
|
|
12
12
|
The Stop hook later scans the assistant text for those markers and resolves
|
|
13
13
|
the ids against a per-session registry persisted at
|
|
14
|
-
``~/.claude-smart/sessions/<session_id>.injected.jsonl``.
|
|
15
|
-
Bash tool calls are still accepted as a fallback for older instructions.
|
|
14
|
+
``~/.claude-smart/sessions/<session_id>.injected.jsonl``.
|
|
16
15
|
|
|
17
16
|
Why rank + fingerprint: rank alone resets at every injection, so a
|
|
18
17
|
later injection's ``s1`` would silently overwrite an earlier entry in
|
|
@@ -27,48 +26,19 @@ This module holds:
|
|
|
27
26
|
- ``rank_id``: ``p{n}-{fp}`` / ``s{n}-{fp}`` tag for a given
|
|
28
27
|
(kind, rank, real_id) tuple. Fingerprint is omitted when no real id
|
|
29
28
|
is available. ``p`` is preference, ``s`` is skill.
|
|
30
|
-
- ``CITATION_CMD_RE``: regex matching a valid legacy ``cs-cite`` command line.
|
|
31
|
-
- ``ensure_installed``: idempotent copy of ``plugin/bin/cs-cite`` to
|
|
32
|
-
``~/.claude-smart/bin/cs-cite`` with the executable bit set.
|
|
33
29
|
- ``CITATION_INSTRUCTION``: the trailer text appended to injected context
|
|
34
30
|
so the assistant knows when and how to emit the citation marker.
|
|
35
31
|
"""
|
|
36
32
|
|
|
37
33
|
from __future__ import annotations
|
|
38
34
|
|
|
39
|
-
import logging
|
|
40
35
|
import re
|
|
41
|
-
import shutil
|
|
42
|
-
import stat as stat_
|
|
43
|
-
from pathlib import Path
|
|
44
36
|
from typing import Any
|
|
45
37
|
|
|
46
|
-
_LOGGER = logging.getLogger(__name__)
|
|
47
|
-
|
|
48
|
-
_THIS_DIR = Path(__file__).resolve().parent
|
|
49
|
-
_PLUGIN_ROOT = _THIS_DIR.parents[1] # plugin/src/claude_smart/ -> plugin/
|
|
50
|
-
_SOURCE_SCRIPT = _PLUGIN_ROOT / "bin" / "cs-cite"
|
|
51
|
-
_INSTALL_DIR = Path.home() / ".claude-smart" / "bin"
|
|
52
|
-
INSTALL_PATH = _INSTALL_DIR / "cs-cite"
|
|
53
|
-
|
|
54
38
|
_FINGERPRINT_LEN = 4
|
|
55
39
|
|
|
56
|
-
# Match a bare `cs-cite <ids>` invocation. Ids are rank tokens of the
|
|
57
|
-
# form `p<N>` (preference) or `s<N>` (skill) with an optional
|
|
58
|
-
# `-<fp>` fingerprint (1-4 alphanumeric chars), optionally
|
|
59
|
-
# `cs:`-prefixed (since bullets render as `[cs:p1-ab12]` and the model
|
|
60
|
-
# often copies the tag verbatim). The `(?i:...)` inline flags make the
|
|
61
|
-
# prefix, kind letter, and fingerprint case-insensitive so `CS:P1-AB12`
|
|
62
|
-
# is accepted — matching the `re.IGNORECASE` used by the standalone
|
|
63
|
-
# `cs-cite` script. Tokens may be comma- and/or whitespace-separated.
|
|
64
|
-
# Chained commands (&&, |, ;) and extra trailing tokens remain rejected
|
|
65
|
-
# by the anchored `\s*$` terminator so accidental mentions don't
|
|
66
|
-
# register as citations.
|
|
67
40
|
_ID_TOKEN = r"(?i:cs:)?(?i:[ps])\d+(?:-(?i:[a-z0-9]){1,4})?"
|
|
68
41
|
_ID_SEP = r"[,\s]+"
|
|
69
|
-
CITATION_CMD_RE = re.compile(
|
|
70
|
-
rf"^\s*(?:[^\s]*/)?cs-cite\s+({_ID_TOKEN}(?:{_ID_SEP}{_ID_TOKEN})*)\s*$"
|
|
71
|
-
)
|
|
72
42
|
_CLEAN_ID_RE = re.compile(r"^(?i:cs:)?((?i:[ps])\d+(?:-(?i:[a-z0-9]){1,4})?)$")
|
|
73
43
|
_SPLIT_RE = re.compile(_ID_SEP)
|
|
74
44
|
_TEXT_CITATION_LINE_RE = re.compile(
|
|
@@ -82,7 +52,7 @@ CITATION_INSTRUCTION = (
|
|
|
82
52
|
"only if — an injected `[cs:…]` item materially changed your reply "
|
|
83
53
|
"(different wording, action, or conclusion than you would have produced "
|
|
84
54
|
"without it), append exactly one final citation line after your answer. "
|
|
85
|
-
"Do not call
|
|
55
|
+
"Do not call a shell command or any other tool for citations. Ids come verbatim "
|
|
86
56
|
"from the `[cs:…]` tags — keep the leading `p` (preference) or `s` "
|
|
87
57
|
"(skill) and the `-<fp>` suffix. Use this exact format for one id: "
|
|
88
58
|
"`✨ 1 claude-smart learning applied [cs:s1-ab12]`. Use this exact format "
|
|
@@ -155,29 +125,6 @@ def rank_id(kind: str, rank: int, real_id: Any = None) -> str:
|
|
|
155
125
|
return f"{prefix}{rank}-{fp}" if fp else f"{prefix}{rank}"
|
|
156
126
|
|
|
157
127
|
|
|
158
|
-
def parse_citation_command(command: str) -> list[str]:
|
|
159
|
-
"""Extract citation ids from a ``cs-cite`` Bash command string.
|
|
160
|
-
|
|
161
|
-
Returns an empty list when the command does not match the expected
|
|
162
|
-
shape (chained commands, extra arguments, or anything other than a
|
|
163
|
-
bare ``cs-cite <ids>`` invocation are rejected to avoid false
|
|
164
|
-
positives from accidental mentions).
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
command: The raw ``input.command`` value from a Bash tool_use
|
|
168
|
-
block.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
list[str]: Lowercase rank ids (e.g. ``"p1"``, ``"s3"``), in the
|
|
172
|
-
order Claude cited them. Empty when the command does not
|
|
173
|
-
match.
|
|
174
|
-
"""
|
|
175
|
-
match = CITATION_CMD_RE.match(command or "")
|
|
176
|
-
if not match:
|
|
177
|
-
return []
|
|
178
|
-
return _parse_id_tokens(match.group(1))
|
|
179
|
-
|
|
180
|
-
|
|
181
128
|
def parse_text_citations(text: str) -> list[str]:
|
|
182
129
|
"""Extract Codex text-only citation ids from a final learning marker line.
|
|
183
130
|
|
|
@@ -199,38 +146,3 @@ def _parse_id_tokens(raw_ids: str) -> list[str]:
|
|
|
199
146
|
if clean := _CLEAN_ID_RE.match(tok):
|
|
200
147
|
ids.append(clean.group(1).lower())
|
|
201
148
|
return ids
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def ensure_installed() -> Path:
|
|
205
|
-
"""Idempotently install ``cs-cite`` into ``~/.claude-smart/bin/``.
|
|
206
|
-
|
|
207
|
-
Called from every PreToolUse / UserPromptSubmit inject, so we
|
|
208
|
-
short-circuit when the target file already exists with
|
|
209
|
-
the executable bit set — the steady-state path is one ``stat`` syscall
|
|
210
|
-
instead of mkdir + copy + stat + chmod. Keying on filesystem state
|
|
211
|
-
(rather than a module-level boolean) keeps test isolation working when
|
|
212
|
-
tests monkeypatch ``INSTALL_PATH`` to a fresh tmpdir.
|
|
213
|
-
|
|
214
|
-
Never raises — filesystem errors are logged at DEBUG and the caller
|
|
215
|
-
proceeds with injection regardless (the citation feature degrades to
|
|
216
|
-
silent if the script is unreachable).
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
Path: Target path, whether or not install succeeded.
|
|
220
|
-
"""
|
|
221
|
-
try:
|
|
222
|
-
if (
|
|
223
|
-
INSTALL_PATH.is_file()
|
|
224
|
-
and INSTALL_PATH.stat().st_mode & stat_.S_IXUSR
|
|
225
|
-
and _SOURCE_SCRIPT.is_file()
|
|
226
|
-
and INSTALL_PATH.read_bytes() == _SOURCE_SCRIPT.read_bytes()
|
|
227
|
-
):
|
|
228
|
-
return INSTALL_PATH
|
|
229
|
-
_INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
|
230
|
-
if _SOURCE_SCRIPT.is_file():
|
|
231
|
-
shutil.copy2(_SOURCE_SCRIPT, INSTALL_PATH)
|
|
232
|
-
mode = INSTALL_PATH.stat().st_mode
|
|
233
|
-
INSTALL_PATH.chmod(mode | stat_.S_IXUSR | stat_.S_IXGRP | stat_.S_IXOTH)
|
|
234
|
-
except OSError as exc:
|
|
235
|
-
_LOGGER.debug("cs-cite install failed: %s", exc)
|
|
236
|
-
return INSTALL_PATH
|
|
@@ -34,7 +34,7 @@ _EXIT_PLAN_MODE_TOOL = "ExitPlanMode"
|
|
|
34
34
|
def _read_transcript_entries(path: Path) -> list[dict[str, Any]]:
|
|
35
35
|
"""Parse the transcript JSONL once into a list of entries.
|
|
36
36
|
|
|
37
|
-
Stop's
|
|
37
|
+
Stop's scanners (assistant text and plan decisions) both
|
|
38
38
|
need the same parsed view; reading once and passing the list around keeps
|
|
39
39
|
the hook's wall-clock cost to a single ``read_text`` per fire even on
|
|
40
40
|
multi-megabyte transcripts.
|
|
@@ -267,43 +267,10 @@ def _parse_plan_decision(text: str) -> str | None:
|
|
|
267
267
|
return None
|
|
268
268
|
|
|
269
269
|
|
|
270
|
-
def
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
the current turn. Multiple calls are merged; order follows Claude's
|
|
275
|
-
emission order (earliest first).
|
|
276
|
-
|
|
277
|
-
Args:
|
|
278
|
-
entries (list[dict[str, Any]]): Pre-parsed transcript entries.
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
list[str]: Rank ids (e.g. ``"s1-ab12"``, ``"p2-cd34"``) in
|
|
282
|
-
emission order. Empty when no ``cs-cite`` call is found.
|
|
283
|
-
"""
|
|
284
|
-
out: list[str] = []
|
|
285
|
-
for entry in _current_turn_assistant_entries(entries):
|
|
286
|
-
message = entry.get("message") or {}
|
|
287
|
-
out.extend(_extract_cs_cite_ids(message.get("content")))
|
|
288
|
-
return out
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _extract_cs_cite_ids(content: Any) -> list[str]:
|
|
292
|
-
"""Return citation ids from all Bash ``cs-cite`` tool_use blocks in ``content``."""
|
|
293
|
-
if not isinstance(content, list):
|
|
294
|
-
return []
|
|
295
|
-
out: list[str] = []
|
|
296
|
-
for block in content:
|
|
297
|
-
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
|
298
|
-
continue
|
|
299
|
-
if block.get("name") != "Bash":
|
|
300
|
-
continue
|
|
301
|
-
tool_input = block.get("input") or {}
|
|
302
|
-
command = tool_input.get("command")
|
|
303
|
-
if not isinstance(command, str):
|
|
304
|
-
continue
|
|
305
|
-
out.extend(cs_cite.parse_citation_command(command))
|
|
306
|
-
return out
|
|
270
|
+
def _has_unpublished_user_turn(session_id: str) -> bool:
|
|
271
|
+
"""True when the session buffer already has a user turn awaiting publish."""
|
|
272
|
+
_, interactions = state.unpublished_slice(state.read_all(session_id))
|
|
273
|
+
return any(item.get("role") == "User" for item in interactions)
|
|
307
274
|
|
|
308
275
|
|
|
309
276
|
def _resolve_cited_items(session_id: str, cited_ids: list[str]) -> list[dict[str, Any]]:
|
|
@@ -360,8 +327,10 @@ def handle(payload: dict[str, Any]) -> None:
|
|
|
360
327
|
if path.is_file():
|
|
361
328
|
entries = _load_transcript_with_retry(path)
|
|
362
329
|
|
|
330
|
+
prompt = payload.get("prompt") or (
|
|
331
|
+
_scan_transcript_for_user_text(entries) if runtime.is_codex() else ""
|
|
332
|
+
)
|
|
363
333
|
if runtime.is_codex():
|
|
364
|
-
prompt = payload.get("prompt") or _scan_transcript_for_user_text(entries)
|
|
365
334
|
if internal_call.is_codex_internal_prompt(prompt):
|
|
366
335
|
return
|
|
367
336
|
|
|
@@ -373,10 +342,15 @@ def handle(payload: dict[str, Any]) -> None:
|
|
|
373
342
|
and last_assistant_message
|
|
374
343
|
else _scan_transcript_for_assistant_text(entries)
|
|
375
344
|
)
|
|
376
|
-
|
|
345
|
+
if (
|
|
346
|
+
runtime.is_codex()
|
|
347
|
+
and internal_call.is_codex_title_response(assistant_text)
|
|
348
|
+
and not prompt
|
|
349
|
+
and not _has_unpublished_user_turn(session_id)
|
|
350
|
+
):
|
|
351
|
+
return
|
|
377
352
|
text_cited_ids = cs_cite.parse_text_citations(assistant_text)
|
|
378
|
-
|
|
379
|
-
cited_items = _resolve_cited_items(session_id, cited_ids)
|
|
353
|
+
cited_items = _resolve_cited_items(session_id, text_cited_ids)
|
|
380
354
|
plan_decisions = _scan_transcript_for_plan_decisions(entries)
|
|
381
355
|
|
|
382
356
|
now = int(time.time())
|
|
@@ -36,6 +36,7 @@ Detection signals, OR'd:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
+
import json
|
|
39
40
|
import os
|
|
40
41
|
from pathlib import Path
|
|
41
42
|
from typing import Any
|
|
@@ -117,3 +118,25 @@ def is_codex_internal_prompt(prompt: Any) -> bool:
|
|
|
117
118
|
and _CODEX_SUGGESTIONS_PROMPT_MARKER in text
|
|
118
119
|
and _CODEX_SUGGESTIONS_APPS_MARKER in text
|
|
119
120
|
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_codex_title_response(content: Any) -> bool:
|
|
124
|
+
"""True for Codex's title-generator response body.
|
|
125
|
+
|
|
126
|
+
Codex can run a separate title-generation task whose Stop payload contains
|
|
127
|
+
only the assistant response, e.g. ``{"title":"Fix tests"}``, with no
|
|
128
|
+
corresponding user turn. That metadata is useful to Codex's UI, but it is
|
|
129
|
+
not a user interaction and should not be published to reflexio.
|
|
130
|
+
"""
|
|
131
|
+
if not isinstance(content, str):
|
|
132
|
+
return False
|
|
133
|
+
try:
|
|
134
|
+
parsed = json.loads(content)
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
return False
|
|
137
|
+
return (
|
|
138
|
+
isinstance(parsed, dict)
|
|
139
|
+
and set(parsed) == {"title"}
|
|
140
|
+
and isinstance(parsed.get("title"), str)
|
|
141
|
+
and bool(parsed["title"].strip())
|
|
142
|
+
)
|
|
@@ -77,7 +77,7 @@ def session_path(session_id: str) -> Path:
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def injected_path(session_id: str) -> Path:
|
|
80
|
-
"""Return the JSONL path for the per-session
|
|
80
|
+
"""Return the JSONL path for the per-session citation registry."""
|
|
81
81
|
return state_dir() / f"{session_id}.injected.jsonl"
|
|
82
82
|
|
|
83
83
|
|
|
@@ -85,8 +85,8 @@ def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None:
|
|
|
85
85
|
"""Append citation-registry entries to the per-session injected-items file.
|
|
86
86
|
|
|
87
87
|
Each entry maps a short ``id`` (4-hex-char) back to the skill or
|
|
88
|
-
preference it came from so the Stop hook can resolve ids
|
|
89
|
-
|
|
88
|
+
preference it came from so the Stop hook can resolve citation ids into
|
|
89
|
+
human-readable titles for the dashboard.
|
|
90
90
|
Silently no-ops when ``entries`` is empty.
|
|
91
91
|
"""
|
|
92
92
|
records = list(entries)
|