entelligence-cli 0.1.78__tar.gz → 0.1.80__tar.gz

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.
Files changed (68) hide show
  1. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/PKG-INFO +1 -1
  2. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/claude_settings.py +93 -9
  3. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/discovery.py +12 -3
  4. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/PKG-INFO +1 -1
  5. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/SOURCES.txt +1 -0
  6. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/pyproject.toml +1 -1
  7. entelligence_cli-0.1.80/tests/test_discovery_head_unicode.py +34 -0
  8. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/README.md +0 -0
  9. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/__init__.py +0 -0
  10. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/api_client.py +0 -0
  11. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ascii-art.txt +0 -0
  12. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/browser_login.py +0 -0
  13. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/cli.py +0 -0
  14. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/__init__.py +0 -0
  15. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/auth.py +0 -0
  16. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/incident.py +0 -0
  17. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/insights.py +0 -0
  18. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/review.py +0 -0
  19. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/router.py +0 -0
  20. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/security.py +0 -0
  21. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/commands/update.py +0 -0
  22. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/config.py +0 -0
  23. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/exceptions.py +0 -0
  24. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/git_operations.py +0 -0
  25. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/__init__.py +0 -0
  26. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/bedrock.py +0 -0
  27. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/catalog.py +0 -0
  28. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/compress.py +0 -0
  29. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/daemon.py +0 -0
  30. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/engine.py +0 -0
  31. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/gateway.py +0 -0
  32. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/gemini_translate.py +0 -0
  33. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/openai_translate.py +0 -0
  34. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/proxy.py +0 -0
  35. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/router/state.py +0 -0
  36. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/scheduling.py +0 -0
  37. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/textual_ui.py +0 -0
  38. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ui/__init__.py +0 -0
  39. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ui/interactive.py +0 -0
  40. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ui/loaders.py +0 -0
  41. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ui/output.py +0 -0
  42. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/ui/styles.py +0 -0
  43. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/update_checker.py +0 -0
  44. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/__init__.py +0 -0
  45. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/conductor.py +0 -0
  46. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/config.py +0 -0
  47. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/manifest.py +0 -0
  48. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/models.py +0 -0
  49. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/pii.py +0 -0
  50. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/runner.py +0 -0
  51. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/secrets.py +0 -0
  52. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/upload/stores.py +0 -0
  53. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli/wrapped_game.py +0 -0
  54. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/dependency_links.txt +0 -0
  55. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/entry_points.txt +0 -0
  56. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/requires.txt +0 -0
  57. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/entelligence_cli.egg-info/top_level.txt +0 -0
  58. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/setup.cfg +0 -0
  59. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_attach_fix_smoke.py +0 -0
  60. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_attach_ticket_smoke.py +0 -0
  61. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_conductor.py +0 -0
  62. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_discovery_opencode.py +0 -0
  63. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_incident_smoke.py +0 -0
  64. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_pii.py +0 -0
  65. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_security_commands.py +0 -0
  66. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_update_command.py +0 -0
  67. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_upload_repo_filter.py +0 -0
  68. {entelligence_cli-0.1.78 → entelligence_cli-0.1.80}/tests/test_upload_repo_roots.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entelligence-cli
3
- Version: 0.1.78
3
+ Version: 0.1.80
4
4
  Summary: AI-powered code review assistant for your terminal - Get instant, intelligent feedback on your code changes
5
5
  Author-email: EntelligenceAI <info@entelligence.ai>
6
6
  Project-URL: Homepage, https://entelligence.ai
@@ -62,6 +62,76 @@ def _write_settings(path: Path, settings: dict[str, Any]) -> None:
62
62
  tmp.replace(path)
63
63
 
64
64
 
65
+ # The UserPromptSubmit hook that stamps every main CC turn with the process's
66
+ # CLAUDE_CODE_SESSION_ID. The backend extracts this from the system prompt and
67
+ # uses it as the session key, giving every turn in a conversation — including
68
+ # subagents that inherit the same env var — a stable, reliable session ID in DB
69
+ # rows instead of a hash that breaks on background tasks or multi-session users.
70
+ _SESSION_HOOK_COMMAND = (
71
+ 'printf "{\\"hookSpecificOutput\\": {\\"hookEventName\\": \\"UserPromptSubmit\\",'
72
+ ' \\"additionalContext\\": \\"x-cc-session-id: $CLAUDE_CODE_SESSION_ID\\"}}"'
73
+ )
74
+
75
+ # Previous command form (single-quoted — variable never expanded). Kept so
76
+ # _remove_session_hook can clean it up on `router off` for users who ran an
77
+ # older version of the CLI before the double-quote fix.
78
+ _SESSION_HOOK_COMMAND_LEGACY = (
79
+ "printf"
80
+ ' \'{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",'
81
+ ' "additionalContext": "x-cc-session-id: $CLAUDE_CODE_SESSION_ID"}}\''
82
+ )
83
+
84
+ _SESSION_HOOK_COMMANDS = {_SESSION_HOOK_COMMAND, _SESSION_HOOK_COMMAND_LEGACY}
85
+
86
+
87
+ def _has_session_hook(settings: dict[str, Any]) -> bool:
88
+ """True when our exact UserPromptSubmit hook command is already present."""
89
+ for entry in settings.get("hooks", {}).get("UserPromptSubmit", []):
90
+ for h in entry.get("hooks", []):
91
+ if h.get("command") == _SESSION_HOOK_COMMAND:
92
+ return True
93
+ return False
94
+
95
+
96
+ def _install_session_hook(settings: dict[str, Any], path: Path, backup: dict[str, Any]) -> None:
97
+ """Add the UserPromptSubmit hook that injects CLAUDE_CODE_SESSION_ID.
98
+
99
+ Idempotent — no-op when our exact hook command is already present. Records
100
+ in the backup whether WE added it so ``unwire`` can remove it without
101
+ touching any hook the user had beforehand.
102
+ """
103
+ if _has_session_hook(settings):
104
+ return
105
+ hooks = settings.setdefault("hooks", {})
106
+ hooks.setdefault("UserPromptSubmit", []).append(
107
+ {
108
+ "matcher": "",
109
+ "hooks": [{"type": "command", "command": _SESSION_HOOK_COMMAND}],
110
+ }
111
+ )
112
+ backup.setdefault(str(path), {})["installed_session_hook"] = True
113
+
114
+
115
+ def _remove_session_hook(settings: dict[str, Any]) -> None:
116
+ """Remove the session hook we installed (called by unwire when backup says we added it).
117
+
118
+ Matches against all known command forms so legacy installs (single-quoted
119
+ version that never expanded the variable) are cleaned up too.
120
+ """
121
+ ups = settings.get("hooks", {}).get("UserPromptSubmit", [])
122
+ filtered = [
123
+ entry
124
+ for entry in ups
125
+ if not any(h.get("command") in _SESSION_HOOK_COMMANDS for h in entry.get("hooks", []))
126
+ ]
127
+ if filtered:
128
+ settings["hooks"]["UserPromptSubmit"] = filtered
129
+ else:
130
+ settings.get("hooks", {}).pop("UserPromptSubmit", None)
131
+ if not settings.get("hooks"):
132
+ settings.pop("hooks", None)
133
+
134
+
65
135
  def base_url(port: int) -> str:
66
136
  """The ANTHROPIC_BASE_URL value pointing at the local router."""
67
137
  return f"http://127.0.0.1:{port}"
@@ -105,25 +175,31 @@ def wire_hosted(scope: str, base: str, token: str) -> None:
105
175
  path = settings_path(scope)
106
176
  settings = _read_settings(path)
107
177
  env = settings.setdefault("env", {})
178
+
179
+ backup = load_backup()
180
+ entry = backup.get(str(path))
108
181
  if env.get("ANTHROPIC_BASE_URL") != base:
109
- backup = load_backup()
110
- entry = backup.get(str(path))
111
182
  if entry is None:
112
183
  # First time we touch this path: capture BOTH baselines.
113
184
  backup[str(path)] = {
114
185
  "ANTHROPIC_BASE_URL": env.get("ANTHROPIC_BASE_URL"),
115
186
  "ANTHROPIC_AUTH_TOKEN": env.get("ANTHROPIC_AUTH_TOKEN"),
116
187
  }
117
- save_backup(backup)
118
188
  elif "ANTHROPIC_AUTH_TOKEN" not in entry:
119
189
  # A prior LOCAL `wire` captured only the base URL (it never sets a token).
120
190
  # Record the token baseline now — the current value is still the genuine
121
191
  # pre-router one — so `unwire` knows to clear/restore it. Without this the
122
192
  # Entelligence auth token is left in settings.json after `router off`.
123
193
  entry["ANTHROPIC_AUTH_TOKEN"] = env.get("ANTHROPIC_AUTH_TOKEN")
124
- save_backup(backup)
194
+
125
195
  env["ANTHROPIC_BASE_URL"] = base
126
196
  env["ANTHROPIC_AUTH_TOKEN"] = token
197
+
198
+ # Install the UserPromptSubmit hook so every main turn carries
199
+ # CLAUDE_CODE_SESSION_ID — backend uses it as the session key in DB rows.
200
+ _install_session_hook(settings, path, backup)
201
+
202
+ save_backup(backup)
127
203
  _write_settings(path, settings)
128
204
 
129
205
 
@@ -143,14 +219,18 @@ def wire(scope: str, port: int) -> None:
143
219
 
144
220
  previous = env.get("ANTHROPIC_BASE_URL")
145
221
  ours = base_url(port)
146
- if previous != ours:
147
- backup = load_backup()
222
+ backup = load_backup()
223
+ if previous != ours and str(path) not in backup:
148
224
  # Only capture a backup once: re-running `on` must not overwrite the
149
225
  # genuine pre-router value with our own URL from a stale state.
150
- if str(path) not in backup:
151
- backup[str(path)] = {"ANTHROPIC_BASE_URL": previous}
152
- save_backup(backup)
226
+ backup[str(path)] = {"ANTHROPIC_BASE_URL": previous}
153
227
  env["ANTHROPIC_BASE_URL"] = ours
228
+
229
+ # Install the UserPromptSubmit hook so every main turn carries
230
+ # CLAUDE_CODE_SESSION_ID — backend uses it as the session key in DB rows.
231
+ _install_session_hook(settings, path, backup)
232
+
233
+ save_backup(backup)
154
234
  _write_settings(path, settings)
155
235
 
156
236
 
@@ -190,6 +270,10 @@ def unwire(scope: str) -> bool:
190
270
  env.pop("ANTHROPIC_AUTH_TOKEN", None)
191
271
  if not env:
192
272
  settings.pop("env", None)
273
+ # Remove the session hook only if we installed it — leave any pre-existing
274
+ # user hook alone (backup records installed_session_hook=True only when we added it).
275
+ if saved.get("installed_session_hook"):
276
+ _remove_session_hook(settings)
193
277
  # Persist settings FIRST, then drop the backup entry — so if the settings write
194
278
  # fails (permissions/disk), the backup is still intact for a retry rather than
195
279
  # consumed against a file that still points at the router.
@@ -49,7 +49,10 @@ def _is_claude_code_sidechain(head: bytes) -> bool:
49
49
  continue
50
50
  try:
51
51
  obj = json.loads(raw)
52
- except json.JSONDecodeError:
52
+ except (json.JSONDecodeError, UnicodeDecodeError):
53
+ # UnicodeDecodeError: the fixed-size head can slice a multi-byte UTF-8 char at the
54
+ # boundary (or a line carries invalid bytes) — json.loads decodes bytes and raises it,
55
+ # not JSONDecodeError. Skip the line instead of crashing the whole discovery walk.
53
56
  continue
54
57
  if obj.get("isSidechain") is True:
55
58
  return True
@@ -67,7 +70,10 @@ def _cc_cwd_from_head(head: bytes) -> Path | None:
67
70
  continue
68
71
  try:
69
72
  obj = json.loads(raw)
70
- except json.JSONDecodeError:
73
+ except (json.JSONDecodeError, UnicodeDecodeError):
74
+ # UnicodeDecodeError: the fixed-size head can slice a multi-byte UTF-8 char at the
75
+ # boundary (or a line carries invalid bytes) — json.loads decodes bytes and raises it,
76
+ # not JSONDecodeError. Skip the line instead of crashing the whole discovery walk.
71
77
  continue
72
78
  cwd = obj.get("cwd")
73
79
  if isinstance(cwd, str) and cwd:
@@ -153,7 +159,10 @@ def _codex_cwd_from_head(head: bytes) -> Path | None:
153
159
  continue
154
160
  try:
155
161
  obj = json.loads(raw)
156
- except json.JSONDecodeError:
162
+ except (json.JSONDecodeError, UnicodeDecodeError):
163
+ # UnicodeDecodeError: the fixed-size head can slice a multi-byte UTF-8 char at the
164
+ # boundary (or a line carries invalid bytes) — json.loads decodes bytes and raises it,
165
+ # not JSONDecodeError. Skip the line instead of crashing the whole discovery walk.
157
166
  continue
158
167
  if obj.get("type") == "session_meta":
159
168
  cwd = (obj.get("payload") or {}).get("cwd")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entelligence-cli
3
- Version: 0.1.78
3
+ Version: 0.1.80
4
4
  Summary: AI-powered code review assistant for your terminal - Get instant, intelligent feedback on your code changes
5
5
  Author-email: EntelligenceAI <info@entelligence.ai>
6
6
  Project-URL: Homepage, https://entelligence.ai
@@ -56,6 +56,7 @@ entelligence_cli/upload/stores.py
56
56
  tests/test_attach_fix_smoke.py
57
57
  tests/test_attach_ticket_smoke.py
58
58
  tests/test_conductor.py
59
+ tests/test_discovery_head_unicode.py
59
60
  tests/test_discovery_opencode.py
60
61
  tests/test_incident_smoke.py
61
62
  tests/test_pii.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "entelligence-cli"
7
- version = "0.1.78"
7
+ version = "0.1.80"
8
8
  description = "AI-powered code review assistant for your terminal - Get instant, intelligent feedback on your code changes"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,34 @@
1
+ """Regression: a transcript head with a sliced/invalid multi-byte UTF-8 char must not crash
2
+ discovery. The fixed-size head read (JSONL_HEAD_BYTES) can cut a multi-byte char at the boundary;
3
+ json.loads decodes the bytes and raises UnicodeDecodeError (not JSONDecodeError), which previously
4
+ propagated out of the whole `insights sync --transcripts` walk."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from entelligence_cli.upload.discovery import (
11
+ _cc_cwd_from_head,
12
+ _codex_cwd_from_head,
13
+ _is_claude_code_sidechain,
14
+ )
15
+
16
+
17
+ # A line whose bytes end mid 3-byte char (0xe2 0x98 = 2 of 3) -> UnicodeDecodeError on json.loads.
18
+ _TRUNCATED = b'{"x":"\xe2\x98'
19
+
20
+
21
+ def test_sidechain_check_skips_undecodable_line():
22
+ head = _TRUNCATED + b'\n{"type":"user","sessionId":"s","cwd":"/tmp/p"}'
23
+ # Must not raise; the bad first line is skipped, the user line decides it's not a sidechain.
24
+ assert _is_claude_code_sidechain(head) is False
25
+
26
+
27
+ def test_cc_cwd_skips_undecodable_line():
28
+ head = _TRUNCATED + b'\n{"type":"user","cwd":"/tmp/proj"}'
29
+ assert _cc_cwd_from_head(head) == Path("/tmp/proj")
30
+
31
+
32
+ def test_codex_cwd_skips_undecodable_line():
33
+ head = _TRUNCATED + b'\n{"type":"session_meta","payload":{"cwd":"/tmp/cx"}}'
34
+ assert _codex_cwd_from_head(head) == Path("/tmp/cx")