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.
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/PKG-INFO +1 -1
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/activity.py +5 -3
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/activity_probe.py +1 -3
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/signals.py +1 -1
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/tools.py +23 -9
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/pyproject.toml +1 -1
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/mining.rs +7 -9
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/parse.rs +16 -3
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/Cargo.lock +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/Cargo.toml +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/LICENSE +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/README.md +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/command.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/context.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/models.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/query.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/render.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/cc_transcript/store.py +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/Cargo.toml +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/build.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/activity.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/command.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/event.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/filter.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/lib.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/model.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/protocol.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/python.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/score.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/types.rs +0 -0
- {cc_transcript-10.1.0 → cc_transcript-10.3.0}/rust/src/value.rs +0 -0
|
@@ -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
|
|
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 (
|
|
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
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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>``
|
|
497
|
-
|
|
498
|
-
|
|
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 (
|
|
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:
|
|
@@ -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::{
|
|
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|
|
|
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
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|