cc-transcript 10.1.0__tar.gz → 10.3.0__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 (74) hide show
  1. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/PKG-INFO +1 -1
  2. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/activity.py +5 -3
  3. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/activity_probe.py +1 -3
  4. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/signals.py +1 -1
  5. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/tools.py +23 -9
  6. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/pyproject.toml +1 -1
  7. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/mining.rs +7 -9
  8. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/parse.rs +16 -3
  9. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/Cargo.lock +0 -0
  10. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/Cargo.toml +0 -0
  11. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/LICENSE +0 -0
  12. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/README.md +0 -0
  13. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/__init__.py +0 -0
  14. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/__main__.py +0 -0
  15. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/_parser_rs.pyi +0 -0
  16. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/backend.py +0 -0
  17. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/builders.py +0 -0
  18. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/cli.py +0 -0
  19. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/command.py +0 -0
  20. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/context.py +0 -0
  21. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/corrections.py +0 -0
  22. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/corrections_cli.py +0 -0
  23. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/cost.py +0 -0
  24. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/decisions.py +0 -0
  25. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/discovery.py +0 -0
  26. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/disktruth.py +0 -0
  27. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/evidence.py +0 -0
  28. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/extract/__init__.py +0 -0
  29. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/extract/correct.py +0 -0
  30. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/facts.py +0 -0
  31. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/filterspec.py +0 -0
  32. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/ids.py +0 -0
  33. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/__init__.py +0 -0
  34. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/llm.py +0 -0
  35. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/similar.py +0 -0
  36. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/verdicts.py +0 -0
  37. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/__init__.py +0 -0
  38. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/candidates.py +0 -0
  39. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/confidence.py +0 -0
  40. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/engine.py +0 -0
  41. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/filterspec.py +0 -0
  42. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/formats.py +0 -0
  43. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/sourcekind.py +0 -0
  44. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/spec.py +0 -0
  45. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/store.py +0 -0
  46. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/models.py +0 -0
  47. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/notifications.py +0 -0
  48. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/parser.py +0 -0
  49. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/py.typed +0 -0
  50. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/query.py +0 -0
  51. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/render.py +0 -0
  52. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/rust.py +0 -0
  53. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/__init__.py +0 -0
  54. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/buckets.py +0 -0
  55. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/engine.py +0 -0
  56. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/lexicon.py +0 -0
  57. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/scorespec.py +0 -0
  58. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/store.py +0 -0
  59. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/Cargo.toml +0 -0
  60. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/build.rs +0 -0
  61. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/data/afinn-en-165.tsv +0 -0
  62. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/data/domain_overrides.tsv +0 -0
  63. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/activity.rs +0 -0
  64. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/command.rs +0 -0
  65. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/event.rs +0 -0
  66. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/filter.rs +0 -0
  67. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/lexicon.rs +0 -0
  68. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/lib.rs +0 -0
  69. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/model.rs +0 -0
  70. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/protocol.rs +0 -0
  71. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/python.rs +0 -0
  72. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/score.rs +0 -0
  73. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/types.rs +0 -0
  74. {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/value.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 10.1.0
3
+ Version: 10.3.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -39,10 +39,12 @@ UserClassifier = Callable[["UserEvent"], bool]
39
39
  def native_user_classifier(event: UserEvent) -> bool:
40
40
  """Whether a user event is a real prompt under native Claude Code semantics.
41
41
 
42
- A prompt is non-meta, non-sidechain, not an interruption marker, and not
43
- tool-result-only — it must carry real text.
42
+ A prompt is non-meta, non-sidechain, not a compact summary, not an
43
+ interruption marker, and not tool-result-only — it must carry real text.
44
44
  """
45
- return not (event.meta.is_meta or event.meta.is_sidechain or event.interrupted) and bool(event.text.strip())
45
+ return not (
46
+ event.meta.is_meta or event.meta.is_sidechain or event.meta.is_compact_summary or event.interrupted
47
+ ) and bool(event.text.strip())
46
48
 
47
49
 
48
50
  @dataclass(frozen=True, slots=True)
@@ -129,9 +129,7 @@ def probe_events(
129
129
  (
130
130
  index
131
131
  for index in reversed(range(len(events)))
132
- if isinstance(event := events[index], UserEvent)
133
- and native_user_classifier(event)
134
- and not event.meta.is_compact_summary
132
+ if isinstance(event := events[index], UserEvent) and native_user_classifier(event)
135
133
  ),
136
134
  0,
137
135
  )
@@ -132,7 +132,7 @@ def last_edit_index(events: Sequence[TranscriptEvent], index: int, spec: MiningS
132
132
  i
133
133
  for i in range(index - 1, max(index - spec.reentry_lookback, 0) - 1, -1)
134
134
  if isinstance(event := events[i], AssistantEvent)
135
- if any(isinstance(b, ToolUseBlock) and b.name in spec.edit_tools for b in event.blocks)
135
+ if any(isinstance(b, ToolUseBlock) and matches_names(b.name, spec.edit_tools) for b in event.blocks)
136
136
  ),
137
137
  None,
138
138
  )
@@ -32,6 +32,14 @@ TOOL_ALIASES: dict[str, str] = {
32
32
 
33
33
  TOOL_ALIASES_REVERSE: dict[str, str] = {v: k for k, v in TOOL_ALIASES.items()}
34
34
 
35
+ # cc-context routes real file edits through MCP tools whose bare names match no
36
+ # builtin edit gate; alias its write surface to the builtins those gates watch so
37
+ # an edit through the MCP can't slip past a Tool("Edit"/"Write"/"MultiEdit") guard.
38
+ MCP_TOOL_ALIASES: dict[str, str] = {
39
+ "ccx_code_edit": "Edit",
40
+ "ccx_code_replace": "Write",
41
+ }
42
+
35
43
  READ_VERBS = frozenset({"get", "list", "search", "read", "view", "fetch", "query", "describe", "show", "find"})
36
44
 
37
45
 
@@ -482,28 +490,34 @@ def file_path_of(call: ToolCall) -> str | None:
482
490
 
483
491
 
484
492
  def expand_tool_names(spec: str) -> frozenset[str]:
485
- """Expand a pipe-separated tool spec to include both alias spellings."""
486
- return frozenset(
487
- (base := set(spec.split("|")))
488
- | {alias for n in base for alias in (TOOL_ALIASES.get(n), TOOL_ALIASES_REVERSE.get(n)) if alias}
489
- )
493
+ """Expand a pipe-separated tool spec to include alias and MCP bare spellings."""
494
+ base = set(spec.split("|"))
495
+ expanded = base | {alias for n in base for alias in (TOOL_ALIASES.get(n), TOOL_ALIASES_REVERSE.get(n)) if alias}
496
+ return frozenset(expanded | {bare for bare, builtin in MCP_TOOL_ALIASES.items() if builtin in expanded})
490
497
 
491
498
 
492
499
  def matches_names(actual: str, names: Container[str]) -> bool:
493
500
  """Whether ``actual`` is one of ``names``, exactly or as an MCP tool suffix.
494
501
 
495
502
  True when ``actual`` is in ``names``, or when it splits as
496
- ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` is in
497
- ``names``. ``names`` is taken verbatim — no alias closure; pre-expand with
498
- :func:`expand_tool_names` when aliases should match.
503
+ ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or its
504
+ :data:`MCP_TOOL_ALIASES` builtin equivalent — is in ``names``. That alias
505
+ closes the edit-gate bypass where cc-context's ``ccx_code_edit`` /
506
+ ``ccx_code_replace`` write through the MCP under names no ``Edit``/``Write``
507
+ gate would catch. Harness-rename aliases are not closed over — ``names`` is
508
+ taken verbatim; pre-expand with :func:`expand_tool_names` for those.
499
509
 
500
510
  Example:
501
511
  >>> matches_names("mcp__github__Grep", {"Grep"})
502
512
  True
513
+ >>> matches_names("mcp__cc-context__ccx_code_edit", {"Edit"})
514
+ True
503
515
  >>> matches_names("Execute", {"Bash"})
504
516
  False
505
517
  """
506
- return actual in names or ((mp := mcp_parts(actual)) is not None and mp[1] in names)
518
+ return actual in names or (
519
+ (mp := mcp_parts(actual)) is not None and (mp[1] in names or MCP_TOOL_ALIASES.get(mp[1]) in names)
520
+ )
507
521
 
508
522
 
509
523
  def tool_name_matches(actual: str, spec: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "10.1.0"
3
+ version = "10.3.0"
4
4
  description = "Grep every Claude Code session you've ever run."
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -8,7 +8,7 @@ use regex::Regex;
8
8
  use sonic_rs::{Index, JsonContainerTrait, JsonValueTrait, Value};
9
9
 
10
10
  use crate::filter::compile_group_array;
11
- use crate::parse::{parse_entry, ParseError};
11
+ use crate::parse::{parse_bytes, ParseError};
12
12
  use crate::protocol::{
13
13
  embedded_user_text, interrupt_marker, is_bare_interrupt_marker, ANSWERED_PREFIX, ANSWERED_TRAILER,
14
14
  DENIAL_PREFIX, INTERRUPT_MARKER_RE,
@@ -434,13 +434,7 @@ impl Events {
434
434
  /// non-object values (parser.py decode_line) so event indices agree with the
435
435
  /// Python reference; a malformed entry fails the mine, as in the reference.
436
436
  fn parse(raw: &[u8]) -> Result<Self, ParseError> {
437
- let entries = raw
438
- .split(|&b| b == b'\n')
439
- .filter(|line| !line.iter().all(u8::is_ascii_whitespace))
440
- .filter_map(|line| sonic_rs::from_slice::<Value>(line).ok())
441
- .filter(|value| value.is_object())
442
- .map(parse_entry)
443
- .collect::<Result<Vec<_>, _>>()?;
437
+ let entries = parse_bytes(raw, |_| true)?;
444
438
  let texts = entries
445
439
  .iter()
446
440
  .map(|entry| match entry {
@@ -479,7 +473,7 @@ impl Events {
479
473
  let lo = index.saturating_sub(spec.reentry_lookback);
480
474
  (lo..index)
481
475
  .rev()
482
- .find(|&i| self.entries[i].tool_uses().any(|tu| spec.edit_tools.contains(&tu.name)))
476
+ .find(|&i| self.entries[i].tool_uses().any(|tu| matches_names(&tu.name, &spec.edit_tools)))
483
477
  .map(|i| i as i64)
484
478
  }
485
479
  }
@@ -1322,6 +1316,10 @@ mod tests {
1322
1316
  assert!(matches_names("mcp__conductor__ExitPlanMode", &names));
1323
1317
  assert!(!matches_names("mcp__ExitPlanMode", &names));
1324
1318
  assert!(!matches_names("AskUserQuestion", &names));
1319
+
1320
+ let edit_names: HashSet<String> = ["Edit".to_string(), "ccx_code_edit".to_string()].into();
1321
+ assert!(matches_names("mcp__cc-context__ccx_code_edit", &edit_names));
1322
+ assert!(!matches_names("mcp__cc-context__ccx_code_read", &edit_names));
1325
1323
  }
1326
1324
 
1327
1325
  #[test]
@@ -265,9 +265,10 @@ pub fn parse_entry(data: Value) -> Result<Entry, ParseError> {
265
265
  Ok(Entry::Other(OtherEntry { ty, raw: data }))
266
266
  }
267
267
 
268
- // Lines that are not valid JSON are skipped; a JSON line that fails the typed
269
- // parse (e.g. a missing required field) fails the whole file — whole-file
270
- // parity with PythonBackend, which parses every line before filtering.
268
+ // Non-JSON lines and valid-JSON lines that are not objects (bare scalars or
269
+ // arrays) are skipped; a JSON object that fails the typed parse (e.g. a missing
270
+ // required field) fails the whole file — whole-file parity with PythonBackend,
271
+ // which decodes every line, skips non-objects, then parses the rest.
271
272
  fn parse_line<F: Fn(&Entry) -> bool>(
272
273
  line: &[u8],
273
274
  lines: &mut Vec<Entry>,
@@ -277,6 +278,9 @@ fn parse_line<F: Fn(&Entry) -> bool>(
277
278
  return Ok(());
278
279
  }
279
280
  if let Ok(value) = sonic_rs::from_slice::<Value>(line) {
281
+ if !value.is_object() {
282
+ return Ok(());
283
+ }
280
284
  let entry = parse_entry(value)?;
281
285
  if keep(&entry) {
282
286
  lines.push(entry);
@@ -614,6 +618,15 @@ mod tests {
614
618
  assert_eq!(other.raw, parse(raw));
615
619
  }
616
620
 
621
+ #[test]
622
+ fn parse_bytes_skips_valid_json_non_object_lines() {
623
+ let real = format!(r#"{{"type":"user",{META},"message":{{"content":"hi"}}}}"#);
624
+ let bytes = format!("{real}\n42\n\"bare\"\n[1,2,3]\n{real}");
625
+ let entries = parse_bytes(bytes.as_bytes(), |_| true).unwrap();
626
+ assert_eq!(entries.len(), 2, "bare scalar and array lines are skipped");
627
+ assert!(entries.iter().all(|e| matches!(e, Entry::User(_))));
628
+ }
629
+
617
630
  #[test]
618
631
  fn print_envelope_requires_result_element() {
619
632
  assert!(matches!(
File without changes
File without changes