cc-transcript 14.26.0__tar.gz → 14.27.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.
Potentially problematic release.
This version of cc-transcript might be problematic. Click here for more details.
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/PKG-INFO +1 -1
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/pyproject.toml +1 -1
- cc_transcript-14.27.0/rust/crates/cli/src/commands/grep.rs +561 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/lib.rs +24 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/activity.rs +34 -8
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/facts.rs +11 -17
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/lib.rs +2 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/render.rs +235 -5
- cc_transcript-14.27.0/rust/crates/core/src/scan.rs +648 -0
- cc_transcript-14.27.0/rust/crates/core/src/scan_grep.rs +1042 -0
- cc_transcript-14.27.0/rust/crates/core/src/scan_regressions.rs +451 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot.rs +163 -8
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_projection.rs +3 -3
- cc_transcript-14.26.0/rust/crates/cli/src/commands/grep.rs +0 -366
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/Cargo.lock +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/Cargo.toml +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/LICENSE +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/README.md +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/__init__.pyi +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/_native.pyi +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/activity_probe.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/codex.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/command.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/context.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/decision_store.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/heartbeats.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/ledger.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/literals.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/sampling.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/models.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/nlp.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/query.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/render.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/config.schema.json +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/context.schema.json +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/request.schema.json +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/response.schema.json +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/tool_registry.schema.json +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshots.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/synthetic.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/watch.py +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/Cargo.toml +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/blame.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/corpus.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/digest.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/facts.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/list.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/mod.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/show.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/slice.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/stats.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/watch.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/main.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/output.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/target.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/timearg.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/Cargo.toml +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/parse.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/render.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/toolcall.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/actor.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/blame.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/buckets.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/discovery.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/lower.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/mod.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/parse.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/probe.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/protocol.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/types.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/command.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/context.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/corrections.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/cost.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/discovery.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/feedback.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/filter.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/gateway.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/ids.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/judge.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/command.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/corrections.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/feedback.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/mining.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/mod.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/protocol.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/notifications.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/parse.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/protocol.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/pystr.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/query.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/rng.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/schema.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_activity.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_codec.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_labels.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_memory.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_owned.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_regressions.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_text.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/sqlite.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/toolcall.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/types.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/value.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/watch.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/Cargo.toml +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/build.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/actor_bridge.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/codex.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/command.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/context.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/corrections.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/discovery.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/feedback.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/judge.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/lexicon.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/lib.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/mining.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/nlp.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/python.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/render.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/score.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/snapshot_decode.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/snapshots.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/sqlite.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/toolcall.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/attachment.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/blocks.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/command.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/convert.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/dunder.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/events.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/meta.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/mod.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/print.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/store.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/system.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/toolcall.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/toolresult.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/transcript.rs +0 -0
- {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/watch.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cc-transcript"
|
|
3
|
-
version = "14.
|
|
3
|
+
version = "14.27.0" # informational — releases stamp the version from the git tag
|
|
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"
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use cc_transcript_core::render::{compact_line, display_path, event_json, transcript_header, Json};
|
|
4
|
+
use cc_transcript_core::scan::{
|
|
5
|
+
scan_corpus, ScanBudget, ScanControl, ScanOutcome, ScanPlan, ScanSession,
|
|
6
|
+
};
|
|
7
|
+
use cc_transcript_core::scan_grep::{GrepOptions, GrepPatternSpec, GrepReducer, GrepSourceResult};
|
|
8
|
+
use cc_transcript_core::snapshot::{Cancellation, NativeStore, SnapshotError, Status, WorkLimits};
|
|
9
|
+
use cc_transcript_core::types::{ContentBlock, Entry};
|
|
10
|
+
use regex::{Regex, RegexBuilder};
|
|
11
|
+
use sonic_rs::json;
|
|
12
|
+
|
|
13
|
+
use crate::output::{click_error, eline, usage_error, CliExit, Out};
|
|
14
|
+
use crate::target::py_path;
|
|
15
|
+
use crate::DiscoveryOpts;
|
|
16
|
+
|
|
17
|
+
const USAGE: &str = "cc-transcript grep [OPTIONS] PATTERN [PATHS]...";
|
|
18
|
+
const HELP_PATH: &str = "cc-transcript grep";
|
|
19
|
+
const DEFAULT_MAX_MATCHES: usize = 20;
|
|
20
|
+
|
|
21
|
+
#[derive(clap::Args, Default)]
|
|
22
|
+
pub struct ScanOptions {
|
|
23
|
+
#[arg(long, help = "Maximum cumulative source and logical projection bytes.")]
|
|
24
|
+
pub max_read_bytes: Option<usize>,
|
|
25
|
+
#[arg(
|
|
26
|
+
long,
|
|
27
|
+
help = "Maximum cumulative preparation and event traversal work."
|
|
28
|
+
)]
|
|
29
|
+
pub max_events: Option<usize>,
|
|
30
|
+
#[arg(long, help = "Maximum emitted output bytes.")]
|
|
31
|
+
pub max_output_bytes: Option<usize>,
|
|
32
|
+
#[arg(long, help = "Maximum filesystem entries examined during discovery.")]
|
|
33
|
+
pub max_discovery_entries: Option<usize>,
|
|
34
|
+
#[arg(long, help = "Maximum sources in the discovered inventory.")]
|
|
35
|
+
pub max_sources: Option<usize>,
|
|
36
|
+
#[arg(long, help = "Deadline for the complete scan in milliseconds.")]
|
|
37
|
+
pub timeout_ms: Option<u64>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl ScanOptions {
|
|
41
|
+
fn apply(&self, mut limits: WorkLimits) -> Result<WorkLimits, CliExit> {
|
|
42
|
+
if let Some(n) = self.max_read_bytes {
|
|
43
|
+
limits.max_read_bytes = n;
|
|
44
|
+
}
|
|
45
|
+
if let Some(n) = self.max_events {
|
|
46
|
+
limits.max_events = n;
|
|
47
|
+
limits.max_items = n;
|
|
48
|
+
}
|
|
49
|
+
if let Some(n) = self.max_output_bytes {
|
|
50
|
+
limits.max_output_bytes = n;
|
|
51
|
+
}
|
|
52
|
+
if let Some(n) = self.max_discovery_entries {
|
|
53
|
+
limits.max_discovery_entries = n;
|
|
54
|
+
}
|
|
55
|
+
if let Some(n) = self.max_sources {
|
|
56
|
+
limits.max_sources = n;
|
|
57
|
+
}
|
|
58
|
+
if let Some(n) = self.timeout_ms {
|
|
59
|
+
limits.deadline_unix_ms = cc_transcript_core::snapshot::now_ms().saturating_add(n);
|
|
60
|
+
}
|
|
61
|
+
if [
|
|
62
|
+
limits.max_read_bytes,
|
|
63
|
+
limits.max_events,
|
|
64
|
+
limits.max_output_bytes,
|
|
65
|
+
limits.max_discovery_entries,
|
|
66
|
+
limits.max_sources,
|
|
67
|
+
]
|
|
68
|
+
.contains(&0)
|
|
69
|
+
|| self.timeout_ms == Some(0)
|
|
70
|
+
{
|
|
71
|
+
return Err(usage_error(
|
|
72
|
+
USAGE,
|
|
73
|
+
HELP_PATH,
|
|
74
|
+
"scan work limits must be positive",
|
|
75
|
+
));
|
|
76
|
+
}
|
|
77
|
+
Ok(limits)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub struct GrepArgs {
|
|
82
|
+
pub pattern: String,
|
|
83
|
+
pub patterns: Vec<String>,
|
|
84
|
+
pub scan_json: bool,
|
|
85
|
+
pub scan_limits: ScanOptions,
|
|
86
|
+
pub paths: Vec<PathBuf>,
|
|
87
|
+
pub discovery: DiscoveryOpts,
|
|
88
|
+
pub corpus: Option<PathBuf>,
|
|
89
|
+
pub kinds: Vec<String>,
|
|
90
|
+
pub tool: Option<String>,
|
|
91
|
+
pub errors: bool,
|
|
92
|
+
pub ignore_case: bool,
|
|
93
|
+
pub wheres: Vec<String>,
|
|
94
|
+
pub context: usize,
|
|
95
|
+
pub max_matches: Option<usize>,
|
|
96
|
+
pub width: usize,
|
|
97
|
+
pub uuids: bool,
|
|
98
|
+
pub with_result: bool,
|
|
99
|
+
pub json: bool,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub fn compile_pattern(
|
|
103
|
+
pattern: &str,
|
|
104
|
+
ignore_case: bool,
|
|
105
|
+
usage: &str,
|
|
106
|
+
help_path: &str,
|
|
107
|
+
) -> Result<Regex, CliExit> {
|
|
108
|
+
RegexBuilder::new(pattern)
|
|
109
|
+
.case_insensitive(ignore_case)
|
|
110
|
+
.build()
|
|
111
|
+
.map_err(|e| usage_error(usage, help_path, &format!("invalid pattern: {e}")))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// An empty `--where` searches everywhere (cli.py's default).
|
|
115
|
+
pub fn where_flags(wheres: &[String]) -> (bool, bool, bool) {
|
|
116
|
+
if wheres.is_empty() {
|
|
117
|
+
return (true, true, true);
|
|
118
|
+
}
|
|
119
|
+
(
|
|
120
|
+
wheres.iter().any(|w| w == "text"),
|
|
121
|
+
wheres.iter().any(|w| w == "thinking"),
|
|
122
|
+
wheres.iter().any(|w| w == "tools"),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn cap(args: &GrepArgs) -> Option<usize> {
|
|
127
|
+
args.max_matches.map_or_else(
|
|
128
|
+
|| args.corpus.is_none().then_some(DEFAULT_MAX_MATCHES),
|
|
129
|
+
|n| (n > 0).then_some(n),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn snapshot_error(error: SnapshotError) -> CliExit {
|
|
134
|
+
click_error(&format!("{}: {}", error.status.as_str(), error.reason))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn output_error() -> SnapshotError {
|
|
138
|
+
SnapshotError::new(Status::OutputLimit, "stdout write failed")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
struct Emitter {
|
|
142
|
+
out: Out,
|
|
143
|
+
scan_json: bool,
|
|
144
|
+
first: bool,
|
|
145
|
+
exit: Option<i32>,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
impl Emitter {
|
|
149
|
+
fn new(
|
|
150
|
+
scan_json: bool,
|
|
151
|
+
budget: &mut ScanBudget,
|
|
152
|
+
_cancel: &Cancellation,
|
|
153
|
+
) -> Result<Self, CliExit> {
|
|
154
|
+
let mut out = Out::new();
|
|
155
|
+
if scan_json {
|
|
156
|
+
if budget.remaining().max_output_bytes < 4096 {
|
|
157
|
+
return Err(click_error(
|
|
158
|
+
"structured scan output requires 4096 status bytes",
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
budget.progress.output_bytes += 4096;
|
|
162
|
+
out.line("{\"schema\":\"cc-transcript.scan/1\",\"matches\":[")?;
|
|
163
|
+
}
|
|
164
|
+
Ok(Self {
|
|
165
|
+
out,
|
|
166
|
+
scan_json,
|
|
167
|
+
first: true,
|
|
168
|
+
exit: None,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn emit(
|
|
173
|
+
&mut self,
|
|
174
|
+
line: String,
|
|
175
|
+
budget: &mut ScanBudget,
|
|
176
|
+
cancel: &Cancellation,
|
|
177
|
+
) -> Result<(), SnapshotError> {
|
|
178
|
+
budget.charge_output(line.len().saturating_add(2), cancel)?;
|
|
179
|
+
let result = if self.scan_json && !self.first {
|
|
180
|
+
self.out.line(",").and_then(|()| self.out.line(&line))
|
|
181
|
+
} else {
|
|
182
|
+
self.out.line(&line)
|
|
183
|
+
};
|
|
184
|
+
self.first = false;
|
|
185
|
+
result.map_err(|error| {
|
|
186
|
+
self.exit = Some(error.0);
|
|
187
|
+
output_error()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn finish(mut self, outcome: &ScanOutcome, counts: &[usize]) -> Result<(), CliExit> {
|
|
192
|
+
if let Some(code) = self.exit {
|
|
193
|
+
return Err(CliExit(code));
|
|
194
|
+
}
|
|
195
|
+
if self.scan_json {
|
|
196
|
+
let status = cc_transcript_core::snapshot_codec::encode(
|
|
197
|
+
&json!({"outcome":outcome,"counts":counts}),
|
|
198
|
+
4032,
|
|
199
|
+
)
|
|
200
|
+
.map_err(snapshot_error)?;
|
|
201
|
+
if status.len().saturating_add(64) > 4096 {
|
|
202
|
+
return Err(click_error("scan status exceeds reserved output"));
|
|
203
|
+
}
|
|
204
|
+
self.out.line(&format!("],{}", &status[1..]))?;
|
|
205
|
+
}
|
|
206
|
+
self.out.finish()?;
|
|
207
|
+
if let Some(reason) = &outcome.reason {
|
|
208
|
+
if reason == "result_limit" {
|
|
209
|
+
eline(&format!("warning: stopped at --max-matches {}; more matches may exist — raise it, or pass --max-matches 0 for no cap",counts.first().copied().unwrap_or(0)));
|
|
210
|
+
} else {
|
|
211
|
+
eline(&format!("warning: scan incomplete: {reason}"));
|
|
212
|
+
return Err(CliExit(3));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if counts.iter().all(|count| *count == 0) {
|
|
216
|
+
return Err(CliExit(1));
|
|
217
|
+
}
|
|
218
|
+
Ok(())
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn result_fields(event: &Entry, result: &GrepSourceResult<'_, '_>) -> Vec<(String, Json)> {
|
|
223
|
+
if !matches!(event, Entry::Assistant(_)) {
|
|
224
|
+
return Vec::new();
|
|
225
|
+
}
|
|
226
|
+
event
|
|
227
|
+
.blocks()
|
|
228
|
+
.iter()
|
|
229
|
+
.filter_map(|block| {
|
|
230
|
+
let ContentBlock::ToolUse(tool) = block else {
|
|
231
|
+
return None;
|
|
232
|
+
};
|
|
233
|
+
let value = result.results.get(tool.id.as_str())?;
|
|
234
|
+
Some((
|
|
235
|
+
tool.id.clone(),
|
|
236
|
+
Json::Obj(vec![
|
|
237
|
+
("is_error".into(), Json::Bool(value.is_error)),
|
|
238
|
+
("denied".into(), Json::Bool(value.denied)),
|
|
239
|
+
(
|
|
240
|
+
"duration_ms".into(),
|
|
241
|
+
value.duration_ms.map_or(Json::Null, Json::Int),
|
|
242
|
+
),
|
|
243
|
+
]),
|
|
244
|
+
))
|
|
245
|
+
})
|
|
246
|
+
.collect()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn result_suffix(event: &Entry, result: &GrepSourceResult<'_, '_>) -> String {
|
|
250
|
+
if !matches!(event, Entry::Assistant(_)) {
|
|
251
|
+
return String::new();
|
|
252
|
+
}
|
|
253
|
+
let markers: Vec<_> = event
|
|
254
|
+
.blocks()
|
|
255
|
+
.iter()
|
|
256
|
+
.filter_map(|block| {
|
|
257
|
+
let ContentBlock::ToolUse(tool) = block else {
|
|
258
|
+
return None;
|
|
259
|
+
};
|
|
260
|
+
let value = result.results.get(tool.id.as_str())?;
|
|
261
|
+
let status = if value.denied {
|
|
262
|
+
"[denied]"
|
|
263
|
+
} else if value.is_error {
|
|
264
|
+
"[err]"
|
|
265
|
+
} else {
|
|
266
|
+
""
|
|
267
|
+
};
|
|
268
|
+
let duration = value
|
|
269
|
+
.duration_ms
|
|
270
|
+
.map(|ms| format!("({ms}ms)"))
|
|
271
|
+
.unwrap_or_default();
|
|
272
|
+
let marker = [status.to_owned(), duration]
|
|
273
|
+
.into_iter()
|
|
274
|
+
.filter(|part| !part.is_empty())
|
|
275
|
+
.collect::<Vec<_>>()
|
|
276
|
+
.join(" ");
|
|
277
|
+
(!marker.is_empty()).then_some(marker)
|
|
278
|
+
})
|
|
279
|
+
.collect();
|
|
280
|
+
if markers.is_empty() {
|
|
281
|
+
String::new()
|
|
282
|
+
} else {
|
|
283
|
+
format!(" {}", markers.join(" "))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
288
|
+
let store = NativeStore::new(&json!({})).map_err(snapshot_error)?;
|
|
289
|
+
let limits = args.scan_limits.apply(store.scan_limits())?;
|
|
290
|
+
let cancel = Cancellation::default();
|
|
291
|
+
*crate::SCAN_CANCELLATION.lock().expect("scan cancellation") = Some(cancel.clone());
|
|
292
|
+
let _interrupt = ScanInterrupt;
|
|
293
|
+
if let Some(path) = &args.corpus {
|
|
294
|
+
return run_over_corpus(&store, &args, path, limits, cancel);
|
|
295
|
+
}
|
|
296
|
+
let mut session = ScanSession::new(&store, limits, cancel.clone());
|
|
297
|
+
let mut reducer = match prepare_reducer(&args, &mut session.budget, &cancel) {
|
|
298
|
+
Ok(reducer) => reducer,
|
|
299
|
+
Err(error) if error.status == Status::InvalidRequest => {
|
|
300
|
+
return Err(usage_error(USAGE, HELP_PATH, &error.reason))
|
|
301
|
+
}
|
|
302
|
+
Err(error) => {
|
|
303
|
+
let emitter = Emitter::new(
|
|
304
|
+
args.scan_json,
|
|
305
|
+
&mut session.budget,
|
|
306
|
+
&Cancellation::default(),
|
|
307
|
+
)?;
|
|
308
|
+
return emitter.finish(
|
|
309
|
+
&failed_outcome(error, &session.budget),
|
|
310
|
+
&vec![0; args.patterns.len() + 1],
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
let plan = ScanPlan {
|
|
315
|
+
paths: args
|
|
316
|
+
.paths
|
|
317
|
+
.iter()
|
|
318
|
+
.map(|path| py_path(&path.to_string_lossy()))
|
|
319
|
+
.collect(),
|
|
320
|
+
root: args.discovery.validated_root(USAGE, HELP_PATH)?,
|
|
321
|
+
project: args.discovery.project.clone(),
|
|
322
|
+
contains: args.discovery.contains.clone(),
|
|
323
|
+
source_limit: args.discovery.effective_limit(),
|
|
324
|
+
};
|
|
325
|
+
let mut emitter = Emitter::new(args.scan_json, &mut session.budget, &cancel)?;
|
|
326
|
+
let mut files = 0usize;
|
|
327
|
+
let mut outcome = session.run(&plan, |path, snapshot, budget, cancel| {
|
|
328
|
+
let result = reducer.scan_source(snapshot, budget, cancel)?;
|
|
329
|
+
if result.hits.is_empty() {
|
|
330
|
+
return Ok(if result.quota_reached {
|
|
331
|
+
ScanControl::Stop {
|
|
332
|
+
source_complete: result.source_complete,
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
ScanControl::Continue
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
files += 1;
|
|
339
|
+
if !args.json && !args.scan_json {
|
|
340
|
+
emitter.emit(transcript_header(&path.to_string_lossy()), budget, cancel)?;
|
|
341
|
+
}
|
|
342
|
+
for (window_index, window) in result.windows.iter().enumerate() {
|
|
343
|
+
if !args.json && !args.scan_json && args.context > 0 && window_index > 0 {
|
|
344
|
+
emitter.emit("--".to_owned(), budget, cancel)?;
|
|
345
|
+
}
|
|
346
|
+
for index in window.clone() {
|
|
347
|
+
let _render_staging = result.preflight_render(snapshot, index, budget, cancel)?;
|
|
348
|
+
let event = snapshot.entry(index);
|
|
349
|
+
let hit = result.hits.iter().find(|hit| hit.event_index == index);
|
|
350
|
+
let line = if args.json || args.scan_json {
|
|
351
|
+
let Json::Obj(mut fields) = event_json(index, event) else {
|
|
352
|
+
unreachable!("event_json object")
|
|
353
|
+
};
|
|
354
|
+
fields.insert(
|
|
355
|
+
0,
|
|
356
|
+
(
|
|
357
|
+
"path".into(),
|
|
358
|
+
Json::Str(path.to_string_lossy().into_owned()),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
if hit.is_none() {
|
|
362
|
+
fields.push(("context".into(), Json::Bool(true)));
|
|
363
|
+
}
|
|
364
|
+
if args.with_result {
|
|
365
|
+
let results = result_fields(event, &result);
|
|
366
|
+
if !results.is_empty() {
|
|
367
|
+
fields.push(("results".into(), Json::Obj(results)));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if args.scan_json {
|
|
371
|
+
fields.push(("generation".into(), Json::Str(snapshot.id.clone())));
|
|
372
|
+
fields.push((
|
|
373
|
+
"pattern_ids".into(),
|
|
374
|
+
Json::Arr(
|
|
375
|
+
hit.map(|hit| {
|
|
376
|
+
hit.pattern_ids
|
|
377
|
+
.iter()
|
|
378
|
+
.map(|id| Json::Int(*id as i64))
|
|
379
|
+
.collect()
|
|
380
|
+
})
|
|
381
|
+
.unwrap_or_default(),
|
|
382
|
+
),
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
Json::Obj(fields).dumps()
|
|
386
|
+
} else {
|
|
387
|
+
let mut line =
|
|
388
|
+
compact_line(index, event, &result.names, args.width, false, args.uuids);
|
|
389
|
+
if args.with_result {
|
|
390
|
+
line.push_str(&result_suffix(event, &result));
|
|
391
|
+
}
|
|
392
|
+
line
|
|
393
|
+
};
|
|
394
|
+
emitter.emit(line, budget, cancel)?;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
Ok(if result.quota_reached {
|
|
398
|
+
ScanControl::Stop {
|
|
399
|
+
source_complete: result.source_complete,
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
ScanControl::Continue
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
if outcome.complete && !reducer.complete() {
|
|
406
|
+
outcome.complete = false;
|
|
407
|
+
outcome.reason = Some("result_limit".to_owned());
|
|
408
|
+
}
|
|
409
|
+
let counts = reducer.counts();
|
|
410
|
+
if !args.json && !args.scan_json {
|
|
411
|
+
let note = if outcome.selected_sources < outcome.available_sources {
|
|
412
|
+
format!(
|
|
413
|
+
" · searched {} of {} transcripts — use --all",
|
|
414
|
+
outcome.selected_sources, outcome.available_sources
|
|
415
|
+
)
|
|
416
|
+
} else {
|
|
417
|
+
String::new()
|
|
418
|
+
};
|
|
419
|
+
if let Err(error) = emitter.emit(
|
|
420
|
+
format!(
|
|
421
|
+
"{files} files, {} matches{note}",
|
|
422
|
+
counts.iter().sum::<usize>()
|
|
423
|
+
),
|
|
424
|
+
&mut session.budget,
|
|
425
|
+
&cancel,
|
|
426
|
+
) {
|
|
427
|
+
outcome.complete = false;
|
|
428
|
+
outcome.reason = Some(error.reason);
|
|
429
|
+
}
|
|
430
|
+
outcome.progress = session.budget.progress.clone();
|
|
431
|
+
}
|
|
432
|
+
emitter.finish(&outcome, counts)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fn run_over_corpus(
|
|
436
|
+
store: &NativeStore,
|
|
437
|
+
args: &GrepArgs,
|
|
438
|
+
path: &Path,
|
|
439
|
+
limits: WorkLimits,
|
|
440
|
+
cancel: Cancellation,
|
|
441
|
+
) -> Result<(), CliExit> {
|
|
442
|
+
let mut budget = ScanBudget::new(store, limits);
|
|
443
|
+
let mut reducer = match prepare_reducer(args, &mut budget, &cancel) {
|
|
444
|
+
Ok(reducer) => reducer,
|
|
445
|
+
Err(error) if error.status == Status::InvalidRequest => {
|
|
446
|
+
return Err(usage_error(USAGE, HELP_PATH, &error.reason))
|
|
447
|
+
}
|
|
448
|
+
Err(error) => {
|
|
449
|
+
let emitter = Emitter::new(args.scan_json, &mut budget, &Cancellation::default())?;
|
|
450
|
+
return emitter.finish(
|
|
451
|
+
&failed_outcome(error, &budget),
|
|
452
|
+
&vec![0; args.patterns.len() + 1],
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
let mut emitter = Emitter::new(args.scan_json, &mut budget, &cancel)?;
|
|
457
|
+
let mut outcome = scan_corpus(
|
|
458
|
+
path,
|
|
459
|
+
&mut budget,
|
|
460
|
+
&cancel,
|
|
461
|
+
|line_number, line, budget, cancel| {
|
|
462
|
+
let (ids, _match_staging) = reducer.scan_text(line, budget, cancel)?;
|
|
463
|
+
if !ids.is_empty() {
|
|
464
|
+
let render_bound = if args.scan_json {
|
|
465
|
+
line.len()
|
|
466
|
+
.saturating_add(path.as_os_str().len())
|
|
467
|
+
.saturating_mul(6)
|
|
468
|
+
.saturating_add(1024)
|
|
469
|
+
} else {
|
|
470
|
+
line.len().saturating_add(2)
|
|
471
|
+
};
|
|
472
|
+
if render_bound > budget.remaining().max_output_bytes {
|
|
473
|
+
return Err(SnapshotError::new(
|
|
474
|
+
Status::OutputLimit,
|
|
475
|
+
"corpus match exceeds remaining output budget",
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
let _render_staging = budget.reserve_staging(render_bound, cancel)?;
|
|
479
|
+
budget.charge_projection(
|
|
480
|
+
line.len().saturating_add(path.as_os_str().len()),
|
|
481
|
+
0,
|
|
482
|
+
cancel,
|
|
483
|
+
)?;
|
|
484
|
+
let rendered = if args.scan_json {
|
|
485
|
+
sonic_rs::to_string(&json!({"path":path.to_string_lossy().as_ref(),"line":line_number,"text":line,"pattern_ids":ids})).map_err(|e|SnapshotError::new(Status::OutputLimit,e.to_string()))?
|
|
486
|
+
} else {
|
|
487
|
+
line.to_owned()
|
|
488
|
+
};
|
|
489
|
+
emitter.emit(rendered, budget, cancel)?;
|
|
490
|
+
}
|
|
491
|
+
Ok(reducer.satisfied())
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
if outcome.complete && !reducer.complete() {
|
|
495
|
+
outcome.complete = false;
|
|
496
|
+
outcome.reason = Some("result_limit".to_owned());
|
|
497
|
+
}
|
|
498
|
+
let counts = reducer.counts();
|
|
499
|
+
if !args.scan_json {
|
|
500
|
+
if let Err(error) = emitter.emit(
|
|
501
|
+
format!(
|
|
502
|
+
"{} matches in {}",
|
|
503
|
+
counts.iter().sum::<usize>(),
|
|
504
|
+
display_path(&path.to_string_lossy())
|
|
505
|
+
),
|
|
506
|
+
&mut budget,
|
|
507
|
+
&cancel,
|
|
508
|
+
) {
|
|
509
|
+
outcome.complete = false;
|
|
510
|
+
outcome.reason = Some(error.reason);
|
|
511
|
+
}
|
|
512
|
+
outcome.progress = budget.progress.clone();
|
|
513
|
+
}
|
|
514
|
+
emitter.finish(&outcome, counts)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
fn failed_outcome(error: SnapshotError, budget: &ScanBudget) -> ScanOutcome {
|
|
518
|
+
ScanOutcome {
|
|
519
|
+
complete: false,
|
|
520
|
+
reason: Some(format!("{}: {}", error.status.as_str(), error.reason)),
|
|
521
|
+
selected_sources: 0,
|
|
522
|
+
available_sources: 0,
|
|
523
|
+
progress: budget.progress.clone(),
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
struct ScanInterrupt;
|
|
528
|
+
impl Drop for ScanInterrupt {
|
|
529
|
+
fn drop(&mut self) {
|
|
530
|
+
*crate::SCAN_CANCELLATION.lock().expect("scan cancellation") = None;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
fn prepare_reducer<'store>(
|
|
535
|
+
args: &GrepArgs,
|
|
536
|
+
budget: &mut ScanBudget<'store>,
|
|
537
|
+
cancel: &Cancellation,
|
|
538
|
+
) -> Result<GrepReducer<'store>, SnapshotError> {
|
|
539
|
+
let (where_text, where_thinking, where_tools) = where_flags(&args.wheres);
|
|
540
|
+
let patterns = std::iter::once(&args.pattern)
|
|
541
|
+
.chain(args.patterns.iter())
|
|
542
|
+
.enumerate()
|
|
543
|
+
.map(|(id, pattern)| GrepPatternSpec {
|
|
544
|
+
id,
|
|
545
|
+
pattern: pattern.clone(),
|
|
546
|
+
max_matches: cap(&args),
|
|
547
|
+
})
|
|
548
|
+
.collect();
|
|
549
|
+
let options = GrepOptions {
|
|
550
|
+
kinds: args.kinds.clone(),
|
|
551
|
+
tool: args.tool.clone(),
|
|
552
|
+
errors: args.errors,
|
|
553
|
+
ignore_case: args.ignore_case,
|
|
554
|
+
where_text,
|
|
555
|
+
where_thinking,
|
|
556
|
+
where_tools,
|
|
557
|
+
context: args.context,
|
|
558
|
+
with_result: args.with_result,
|
|
559
|
+
};
|
|
560
|
+
GrepReducer::new(patterns, options, budget, cancel)
|
|
561
|
+
}
|
|
@@ -18,6 +18,9 @@ use output::CliExit;
|
|
|
18
18
|
/// (cli.py watch_ catches KeyboardInterrupt); outside watch it means exit 130.
|
|
19
19
|
pub static WATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
|
|
20
20
|
pub static WATCH_INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
|
21
|
+
pub(crate) static SCAN_CANCELLATION: std::sync::Mutex<
|
|
22
|
+
Option<cc_transcript_core::snapshot::Cancellation>,
|
|
23
|
+
> = std::sync::Mutex::new(None);
|
|
21
24
|
|
|
22
25
|
/// The real SIGINT handler spike C mandates: with the GIL detached, Python-side
|
|
23
26
|
/// KeyboardInterrupt delivery never fires, so the Rust side owns the signal.
|
|
@@ -25,6 +28,12 @@ pub fn install_sigint_handler() {
|
|
|
25
28
|
let _ = ctrlc::set_handler(|| {
|
|
26
29
|
if WATCH_ACTIVE.load(Ordering::SeqCst) {
|
|
27
30
|
WATCH_INTERRUPTED.store(true, Ordering::SeqCst);
|
|
31
|
+
} else if let Some(cancel) = SCAN_CANCELLATION
|
|
32
|
+
.lock()
|
|
33
|
+
.expect("scan cancellation")
|
|
34
|
+
.as_ref()
|
|
35
|
+
{
|
|
36
|
+
cancel.cancel();
|
|
28
37
|
} else {
|
|
29
38
|
std::process::exit(130);
|
|
30
39
|
}
|
|
@@ -192,8 +201,17 @@ pub enum Cmd {
|
|
|
192
201
|
json: bool,
|
|
193
202
|
},
|
|
194
203
|
/// Search transcript events for a regex pattern.
|
|
204
|
+
#[command(
|
|
205
|
+
long_about = "Search transcripts with finite read, event, source, output, and time limits. --all removes only the selected-file limit. Use repeated --pattern with --scan-json to share one traversal; incomplete scans cannot establish absence."
|
|
206
|
+
)]
|
|
195
207
|
Grep {
|
|
196
208
|
pattern: String,
|
|
209
|
+
#[arg(long = "pattern", requires = "scan_json")]
|
|
210
|
+
patterns: Vec<String>,
|
|
211
|
+
#[arg(long, conflicts_with = "json")]
|
|
212
|
+
scan_json: bool,
|
|
213
|
+
#[command(flatten)]
|
|
214
|
+
scan_limits: commands::grep::ScanOptions,
|
|
197
215
|
paths: Vec<PathBuf>,
|
|
198
216
|
#[command(flatten)]
|
|
199
217
|
discovery: DiscoveryOpts,
|
|
@@ -545,6 +563,9 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
|
|
|
545
563
|
}),
|
|
546
564
|
Cmd::Grep {
|
|
547
565
|
pattern,
|
|
566
|
+
patterns,
|
|
567
|
+
scan_json,
|
|
568
|
+
scan_limits,
|
|
548
569
|
paths,
|
|
549
570
|
discovery,
|
|
550
571
|
corpus,
|
|
@@ -561,6 +582,9 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
|
|
|
561
582
|
json,
|
|
562
583
|
} => commands::grep::run(commands::grep::GrepArgs {
|
|
563
584
|
pattern,
|
|
585
|
+
patterns,
|
|
586
|
+
scan_json,
|
|
587
|
+
scan_limits,
|
|
564
588
|
paths,
|
|
565
589
|
discovery,
|
|
566
590
|
corpus,
|
|
@@ -313,16 +313,42 @@ pub struct ToolUse<'a> {
|
|
|
313
313
|
pub edits: Vec<(String, Vec<Hunk>)>,
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
pub(crate) fn tool_duration_ms(
|
|
317
|
+
start: DateTime<FixedOffset>,
|
|
318
|
+
end: Option<DateTime<FixedOffset>>,
|
|
319
|
+
) -> Option<i64> {
|
|
320
|
+
end.map(|end| {
|
|
321
|
+
let micros = (end - start)
|
|
322
|
+
.num_microseconds()
|
|
323
|
+
.expect("duration fits i64 microseconds");
|
|
324
|
+
((micros as f64 / 1_000_000.0) * 1000.0).round_ties_even() as i64
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
329
|
+
pub struct ToolResultMetadata {
|
|
330
|
+
pub is_error: bool,
|
|
331
|
+
pub denied: bool,
|
|
332
|
+
pub duration_ms: Option<i64>,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn tool_result_metadata(
|
|
336
|
+
start: DateTime<FixedOffset>,
|
|
337
|
+
result: Option<&ToolResultBlock>,
|
|
338
|
+
end: Option<DateTime<FixedOffset>>,
|
|
339
|
+
) -> ToolResultMetadata {
|
|
340
|
+
ToolResultMetadata {
|
|
341
|
+
is_error: result.is_some_and(|result| result.is_error),
|
|
342
|
+
denied: result.is_some_and(|result| {
|
|
343
|
+
result.denial_kind.as_deref() == Some(crate::protocol::DENIAL_KIND_USER_REJECTED)
|
|
344
|
+
}),
|
|
345
|
+
duration_ms: tool_duration_ms(start, end),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
316
349
|
impl ToolUse<'_> {
|
|
317
|
-
/// Milliseconds from the call to its result, or None without a result
|
|
318
|
-
/// timestamp (activity.py ToolUse.duration_ms).
|
|
319
350
|
pub fn duration_ms(&self) -> Option<i64> {
|
|
320
|
-
self.result_ts
|
|
321
|
-
let micros = (rt - self.ts)
|
|
322
|
-
.num_microseconds()
|
|
323
|
-
.expect("duration fits i64 microseconds");
|
|
324
|
-
((micros as f64 / 1_000_000.0) * 1000.0).round_ties_even() as i64
|
|
325
|
-
})
|
|
351
|
+
tool_duration_ms(self.ts, self.result_ts)
|
|
326
352
|
}
|
|
327
353
|
}
|
|
328
354
|
|