harnessgap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/adapter/correction.d.ts +14 -0
- package/dist/adapter/correction.js +68 -0
- package/dist/adapter/correction.js.map +1 -0
- package/dist/adapter/parse.d.ts +21 -0
- package/dist/adapter/parse.js +261 -0
- package/dist/adapter/parse.js.map +1 -0
- package/dist/adapter/scrub.d.ts +15 -0
- package/dist/adapter/scrub.js +97 -0
- package/dist/adapter/scrub.js.map +1 -0
- package/dist/adapter/stream.d.ts +14 -0
- package/dist/adapter/stream.js +207 -0
- package/dist/adapter/stream.js.map +1 -0
- package/dist/adapter/taxonomy.d.ts +7 -0
- package/dist/adapter/taxonomy.js +21 -0
- package/dist/adapter/taxonomy.js.map +1 -0
- package/dist/aggregate/leaderboard.d.ts +26 -0
- package/dist/aggregate/leaderboard.js +168 -0
- package/dist/aggregate/leaderboard.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +191 -0
- package/dist/config.js.map +1 -0
- package/dist/detector/areas.d.ts +13 -0
- package/dist/detector/areas.js +95 -0
- package/dist/detector/areas.js.map +1 -0
- package/dist/detector/index.d.ts +14 -0
- package/dist/detector/index.js +31 -0
- package/dist/detector/index.js.map +1 -0
- package/dist/detector/record.d.ts +24 -0
- package/dist/detector/record.js +29 -0
- package/dist/detector/record.js.map +1 -0
- package/dist/detector/scoring.d.ts +21 -0
- package/dist/detector/scoring.js +140 -0
- package/dist/detector/scoring.js.map +1 -0
- package/dist/detector/signals.d.ts +6 -0
- package/dist/detector/signals.js +221 -0
- package/dist/detector/signals.js.map +1 -0
- package/dist/egress.d.ts +8 -0
- package/dist/egress.js +37 -0
- package/dist/egress.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.js +59 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/output/calibrate.d.ts +35 -0
- package/dist/output/calibrate.js +149 -0
- package/dist/output/calibrate.js.map +1 -0
- package/dist/output/human.d.ts +24 -0
- package/dist/output/human.js +83 -0
- package/dist/output/human.js.map +1 -0
- package/dist/output/json.d.ts +17 -0
- package/dist/output/json.js +22 -0
- package/dist/output/json.js.map +1 -0
- package/dist/pipeline.d.ts +24 -0
- package/dist/pipeline.js +164 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +12 -0
- package/dist/walk.js +92 -0
- package/dist/walk.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Teryaev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# harnessgap
|
|
2
|
+
|
|
3
|
+
A **stateless, detection-only** CLI that reads Claude Code transcript logs and
|
|
4
|
+
produces a *struggle leaderboard* — the areas of a repo where Claude Code
|
|
5
|
+
sessions show the deterministic signals of friction (rereads, failure streaks,
|
|
6
|
+
oscillating edits, abandonment, etc.).
|
|
7
|
+
|
|
8
|
+
This is **Slice 1**: it writes nothing, installs nothing, persists nothing. It
|
|
9
|
+
only prints a leaderboard to stdout. Diagnosis, synthesis, routing, and
|
|
10
|
+
measurement are deferred to later slices.
|
|
11
|
+
|
|
12
|
+
> **Full manual:** [docs/CONSUMER_GUIDE.md](docs/CONSUMER_GUIDE.md) — output formats, scoring modes, calibration, FAQ. **Internals:** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npx harnessgap
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or build from source:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
node dist/cli.js scan
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Node >= 22.12.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
harnessgap scan [options]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`scan` is the default command. It walks Claude Code transcripts under
|
|
37
|
+
`~/.claude/projects/`, filters to a repo, runs the detector, and prints a
|
|
38
|
+
leaderboard of struggle areas.
|
|
39
|
+
|
|
40
|
+
### Flags
|
|
41
|
+
|
|
42
|
+
| Flag | Description |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `--repo <path>` | Filter to sessions whose resolved repo toplevel matches this path. Defaults to the git toplevel of the current working directory. |
|
|
45
|
+
| `--since <dur>` | Only sessions started within this lookback, e.g. `30d`, `12h`, `5m`, `10s`. Default: all sessions. |
|
|
46
|
+
| `--limit <n>` | Cap the number of sessions scanned. Useful for fast iteration. |
|
|
47
|
+
| `--json` | Emit the JSON envelope (for piping) instead of the human-readable leaderboard table. |
|
|
48
|
+
| `--calibrate` | Print per-signal distributions (min / p50 / p90 / max) plus active thresholds and scoring mode. Aggregate statistics only — no per-session detail. |
|
|
49
|
+
| `--bootstrap` | Force bootstrap (absolute-threshold) scoring mode instead of percentile. |
|
|
50
|
+
| `--config <path>` | Path to a `.harnessgap.yml` config file. Default: looks for `.harnessgap.yml` in the cwd. |
|
|
51
|
+
| `--claude-dir <path>` | Claude Code config directory (contains `projects/`). Default: `~/.claude`. |
|
|
52
|
+
| `--version` | Print the harnessgap version and exit. |
|
|
53
|
+
| `--help` | Print help and exit. |
|
|
54
|
+
|
|
55
|
+
## Configuration (`.harnessgap.yml`)
|
|
56
|
+
|
|
57
|
+
Optional. `scan` runs with built-in defaults if no file is present. The file is
|
|
58
|
+
a YAML object with two top-level keys — `detector` and `areas`. Anything else is
|
|
59
|
+
rejected. Deep-merged over the defaults (arrays replace, they do not concatenate).
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
detector:
|
|
63
|
+
thresholds_as: percentile # percentile (default) | absolute
|
|
64
|
+
flag_pct: 90 # percentile mode: flag the top (100 - flag_pct)% of composites
|
|
65
|
+
bootstrap_session_floor: 30 # below this session count, bootstrap mode is automatic
|
|
66
|
+
bootstrap_flag_pct: 70 # bootstrap mode: flag if composite >= this OR >= 2 signals trip
|
|
67
|
+
reread_threshold: 5 # a file read count at/above this counts as a reread
|
|
68
|
+
correction_window_ms: 120000 # window after an edit in which a follow-up edit counts as a correction
|
|
69
|
+
signal_weights: # composite weights per signal
|
|
70
|
+
explore_ratio: 1.0
|
|
71
|
+
reread: 1.0
|
|
72
|
+
failure_streak: 1.0
|
|
73
|
+
corrections: 1.0
|
|
74
|
+
abandonment: 0.5 # low until task fingerprinting lands
|
|
75
|
+
oscillation: 1.2
|
|
76
|
+
wall_clock_per_line: 1.0
|
|
77
|
+
bootstrap_thresholds: # conservative absolute priors used in bootstrap mode
|
|
78
|
+
explore_ratio: 10
|
|
79
|
+
reread: 5
|
|
80
|
+
failure_streak: 3
|
|
81
|
+
corrections: 2
|
|
82
|
+
abandonment: true
|
|
83
|
+
oscillation: 2
|
|
84
|
+
wall_clock_per_line_ms: 300000
|
|
85
|
+
|
|
86
|
+
areas:
|
|
87
|
+
ignore: [node_modules, build, target, dist, .git, .next, vendor] # path prefixes excluded from area clustering
|
|
88
|
+
min_weight: 0.40 # minimum cumulative touch weight for an area to appear
|
|
89
|
+
min_depth: 2 # minimum path depth for an area key
|
|
90
|
+
touch_weights: { edit: 3, read: 2, exec: 1 } # weight of each touch kind
|
|
91
|
+
tail_fraction: 0.25 # fraction of a session's events that defines the abandonment tail
|
|
92
|
+
explore_ratio_min: 0.8 # abandonment explore-ratio gate
|
|
93
|
+
suppress_abandonment_when_no_exec: true
|
|
94
|
+
test_cmd_patterns: [test, spec, pytest, "npm test", "npm run test", make, "cargo test", "go test", jest, vitest]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Keys not shown here (`docs_dirs`, `synthesizer`, `router`, `tasks`, `repo`) are
|
|
98
|
+
**not** part of Slice 1 and will be rejected.
|
|
99
|
+
|
|
100
|
+
## Success criterion
|
|
101
|
+
|
|
102
|
+
Slice 1 is validated by a **manual dogfood gate**, not an automated test. On a
|
|
103
|
+
real repo with rich Claude Code session history, the user prepares, in advance:
|
|
104
|
+
|
|
105
|
+
- **>= 5 areas they recall as struggle**, and
|
|
106
|
+
- **>= 5 areas they recall as non-struggle.**
|
|
107
|
+
|
|
108
|
+
`harnessgap scan` must then satisfy all three against its leaderboard:
|
|
109
|
+
|
|
110
|
+
- **Precision** — of the tool's top 5 flagged areas, >= 3 are in the user's
|
|
111
|
+
struggle set (>= 60%).
|
|
112
|
+
- **Recall** — of the user's >= 5 struggle areas, >= 3 are flagged (>= 60%).
|
|
113
|
+
- **No false positives in the top 5** — none of the user's non-struggle areas
|
|
114
|
+
appear in the top 5 flagged.
|
|
115
|
+
|
|
116
|
+
The labeled fixture corpus and snapshot test (see `test/corpus.test.ts`,
|
|
117
|
+
`test/snapshot.test.ts`) serve as the automated regression proxy for this gate.
|
|
118
|
+
|
|
119
|
+
## Privacy
|
|
120
|
+
|
|
121
|
+
harnessgap is built to run offline on private transcripts. Five guarantees:
|
|
122
|
+
|
|
123
|
+
1. **No network.** No `fetch` / `http` / `https` / `net` / `undici` imports and
|
|
124
|
+
no `fetch()` calls anywhere in `src/`. Transcripts never leave the machine.
|
|
125
|
+
Enforced by `test/egress.test.ts`, which scans every `src/**/*.ts` file for
|
|
126
|
+
forbidden network imports and fetch calls, and runs in CI.
|
|
127
|
+
2. **No disk writes.** harnessgap writes nothing to disk. It reads transcripts
|
|
128
|
+
and prints to stdout. (OS-level page cache/swap are out of scope and common
|
|
129
|
+
to any process that reads files.)
|
|
130
|
+
3. **Pattern-catalog scrubbing.** Secrets are scrubbed in the adapter, before
|
|
131
|
+
events enter the pipeline, using a fixed pattern catalog (API keys, bearer
|
|
132
|
+
tokens, private keys, connection strings, etc.).
|
|
133
|
+
4. **No raw prose in output.** Only derived signal values and integer warning
|
|
134
|
+
counts are emitted. Raw message text, commands, and transcript line content
|
|
135
|
+
never appear in any output path (human table, `--json`, `--calibrate`,
|
|
136
|
+
warnings).
|
|
137
|
+
5. **Sandboxed git.** `git` is invoked via `execFile` with no shell, so no
|
|
138
|
+
command lands in shell history. Symlinks in transcript directories are
|
|
139
|
+
rejected.
|
|
140
|
+
|
|
141
|
+
## Dependency egress audit
|
|
142
|
+
|
|
143
|
+
Runtime dependencies are exactly two, both no-egress (neither performs network
|
|
144
|
+
I/O):
|
|
145
|
+
|
|
146
|
+
- [`commander`](https://www.npmjs.com/package/commander) — CLI argument parsing.
|
|
147
|
+
- [`yaml`](https://www.npmjs.com/package/yaml) — `.harnessgap.yml` parsing.
|
|
148
|
+
|
|
149
|
+
Dev dependencies (`typescript`, `vitest`, `tsx`, `@types/node`) are never shipped
|
|
150
|
+
and excluded from the published `files` set (`["dist"]`).
|
|
151
|
+
|
|
152
|
+
The audit is locked by `test/packaging.test.ts`, which runs
|
|
153
|
+
`npm ls --all --omit=dev --json` (via `execFile`, no shell) and asserts the
|
|
154
|
+
runtime `dependencies` object has exactly the keys `commander` and `yaml`. The
|
|
155
|
+
`test/egress.test.ts` gate additionally asserts no `src/` file imports a network
|
|
156
|
+
module or calls `fetch()`. Both run in CI.
|
|
157
|
+
|
|
158
|
+
## Spec
|
|
159
|
+
|
|
160
|
+
Full design: [`docs/superpowers/specs/active/2026-07-12-harnessgap-detection-slice-design.md`](docs/superpowers/specs/active/2026-07-12-harnessgap-detection-slice-design.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Correction, ToolKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect whether a user message is a course-correction of the immediately
|
|
4
|
+
* preceding tool call. Returns `{matched, shape}` — never the raw text.
|
|
5
|
+
*
|
|
6
|
+
* Shape catalog (first match wins): negation → undo → redirect → retry-different.
|
|
7
|
+
* Negation is start-anchored (with a word boundary); the other three use
|
|
8
|
+
* substring `includes`. Messages shorter than 3 chars are not matched.
|
|
9
|
+
* `prevToolCall` is part of the interface contract but not used by this
|
|
10
|
+
* content-based heuristic.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectCorrection(prevToolCall: {
|
|
13
|
+
tool: ToolKind;
|
|
14
|
+
} | null, userText: string): Correction;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Content-based correction detector — a heuristic priors catalog that
|
|
2
|
+
// classifies whether a user message is a course-correction of the preceding
|
|
3
|
+
// tool call. Pure function: reads userText internally, emits ONLY
|
|
4
|
+
// {matched, shape}. Never returns the raw text. Spec §12.5 open question
|
|
5
|
+
// (whether interrupts are recorded in transcripts); this is the
|
|
6
|
+
// content-based fallback.
|
|
7
|
+
// Negation tokens are start-anchored: checked against the start of the trimmed
|
|
8
|
+
// lowercased message, with a word boundary so "no" does not match "note".
|
|
9
|
+
const NEGATION_TOKENS = [
|
|
10
|
+
'no',
|
|
11
|
+
'no,',
|
|
12
|
+
"don't",
|
|
13
|
+
'do not',
|
|
14
|
+
'stop',
|
|
15
|
+
'wait',
|
|
16
|
+
'hold on',
|
|
17
|
+
"that's wrong",
|
|
18
|
+
'not that',
|
|
19
|
+
];
|
|
20
|
+
// undo / redirect / retry-different use substring includes (case-insensitive).
|
|
21
|
+
const UNDO_TOKENS = ['undo', 'revert', 'roll back', 'put it back'];
|
|
22
|
+
const REDIRECT_TOKENS = ['instead', 'actually', 'rather', 'try the other'];
|
|
23
|
+
const RETRY_DIFFERENT_TOKENS = ['try again', 'differently', 'another approach'];
|
|
24
|
+
const NOT_MATCHED = { matched: false, shape: null };
|
|
25
|
+
function isLetter(code) {
|
|
26
|
+
return code >= 97 && code <= 122; // a-z (text is already lowercased)
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Test whether `text` starts with `token` followed by a word boundary
|
|
30
|
+
* (non-letter or end-of-string). Prevents "no" from matching "note".
|
|
31
|
+
*/
|
|
32
|
+
function startsWithToken(text, token) {
|
|
33
|
+
if (!text.startsWith(token))
|
|
34
|
+
return false;
|
|
35
|
+
if (text.length === token.length)
|
|
36
|
+
return true;
|
|
37
|
+
return !isLetter(text.charCodeAt(token.length));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect whether a user message is a course-correction of the immediately
|
|
41
|
+
* preceding tool call. Returns `{matched, shape}` — never the raw text.
|
|
42
|
+
*
|
|
43
|
+
* Shape catalog (first match wins): negation → undo → redirect → retry-different.
|
|
44
|
+
* Negation is start-anchored (with a word boundary); the other three use
|
|
45
|
+
* substring `includes`. Messages shorter than 3 chars are not matched.
|
|
46
|
+
* `prevToolCall` is part of the interface contract but not used by this
|
|
47
|
+
* content-based heuristic.
|
|
48
|
+
*/
|
|
49
|
+
export function detectCorrection(prevToolCall, userText) {
|
|
50
|
+
void prevToolCall; // interface contract; not used by this heuristic
|
|
51
|
+
const text = userText.trim().toLowerCase().replace(/’/g, "'");
|
|
52
|
+
if (text.length < 3)
|
|
53
|
+
return NOT_MATCHED;
|
|
54
|
+
if (NEGATION_TOKENS.some((t) => startsWithToken(text, t))) {
|
|
55
|
+
return { matched: true, shape: 'negation' };
|
|
56
|
+
}
|
|
57
|
+
if (UNDO_TOKENS.some((t) => text.includes(t))) {
|
|
58
|
+
return { matched: true, shape: 'undo' };
|
|
59
|
+
}
|
|
60
|
+
if (REDIRECT_TOKENS.some((t) => text.includes(t))) {
|
|
61
|
+
return { matched: true, shape: 'redirect' };
|
|
62
|
+
}
|
|
63
|
+
if (RETRY_DIFFERENT_TOKENS.some((t) => text.includes(t))) {
|
|
64
|
+
return { matched: true, shape: 'retry-different' };
|
|
65
|
+
}
|
|
66
|
+
return NOT_MATCHED;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=correction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction.js","sourceRoot":"","sources":["../../src/adapter/correction.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,4EAA4E;AAC5E,kEAAkE;AAClE,yEAAyE;AACzE,gEAAgE;AAChE,0BAA0B;AAI1B,+EAA+E;AAC/E,0EAA0E;AAC1E,MAAM,eAAe,GAAG;IACtB,IAAI;IACJ,KAAK;IACL,OAAO;IACP,QAAQ;IACR,MAAM;IACN,MAAM;IACN,SAAS;IACT,cAAc;IACd,UAAU;CACX,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;AACnE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;AAC3E,MAAM,sBAAsB,GAAG,CAAC,WAAW,EAAE,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAEhF,MAAM,WAAW,GAAe,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAEhE,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,mCAAmC;AACvE,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa;IAClD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAC9B,YAAuC,EACvC,QAAgB;IAEhB,KAAK,YAAY,CAAC,CAAC,iDAAiD;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,WAAW,CAAC;IAExC,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC1C,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NormalizedEvent, ToolKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Convert one parsed Claude Code JSONL record into a NormalizedEvent, or return
|
|
4
|
+
* null when the record carries no usable event (system entries, ai-title, etc.).
|
|
5
|
+
*
|
|
6
|
+
* Mapping:
|
|
7
|
+
* - assistant tool_use → tool_call (tool + digest from input; ok=true)
|
|
8
|
+
* - user-record tool_result → tool_call (tool=null; ok=!is_error; interrupted)
|
|
9
|
+
* - user text → user_msg (correction via detectCorrection; no raw text)
|
|
10
|
+
* - assistant text → assistant_msg
|
|
11
|
+
* - anything else → null
|
|
12
|
+
*
|
|
13
|
+
* `ctx.prevToolCall` is passed to detectCorrection for user messages; it is NOT
|
|
14
|
+
* mutated here (the stream layer updates it between records). No raw prose is
|
|
15
|
+
* emitted: userText is consumed by detectCorrection and discarded.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeRecord(raw: unknown, ctx: {
|
|
18
|
+
prevToolCall: {
|
|
19
|
+
tool: ToolKind;
|
|
20
|
+
} | null;
|
|
21
|
+
}): NormalizedEvent | null;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Claude Code transcript-record normalizer. Pure function: takes one already-
|
|
2
|
+
// parsed JSONL record object and returns a NormalizedEvent (or null when the
|
|
3
|
+
// record carries no usable event). No I/O, no network. Scrubbing, caps, and the
|
|
4
|
+
// correction flag are applied here so downstream stages never see raw prose.
|
|
5
|
+
//
|
|
6
|
+
// Field-name constants below were confirmed by inspecting real transcripts at
|
|
7
|
+
// ~/.claude/projects/-Users-dmitry-Desktop-CursorProjects-harnessgap/*.jsonl
|
|
8
|
+
// (one JSON object per line). Behavioral contract is pinned to the task-5 brief
|
|
9
|
+
// regardless of upstream schema drift.
|
|
10
|
+
import { scrubCmd, scrubFiles, scrubQuery } from './scrub.js';
|
|
11
|
+
import { detectCorrection } from './correction.js';
|
|
12
|
+
import { mapToolKind } from './taxonomy.js';
|
|
13
|
+
// --- Confirmed Claude Code transcript field paths (encoded as constants) ---
|
|
14
|
+
const RECORD_TYPE = {
|
|
15
|
+
USER: 'user',
|
|
16
|
+
ASSISTANT: 'assistant',
|
|
17
|
+
};
|
|
18
|
+
const FIELD = {
|
|
19
|
+
TYPE: 'type',
|
|
20
|
+
MESSAGE: 'message',
|
|
21
|
+
CONTENT: 'content',
|
|
22
|
+
TIMESTAMP: 'timestamp',
|
|
23
|
+
DURATION_MS: 'duration_ms',
|
|
24
|
+
};
|
|
25
|
+
const CONTENT_TYPE = {
|
|
26
|
+
TOOL_USE: 'tool_use',
|
|
27
|
+
TEXT: 'text',
|
|
28
|
+
TOOL_RESULT: 'tool_result',
|
|
29
|
+
};
|
|
30
|
+
// Tool-use input field names (confirmed: Bash.command, Read.file_path,
|
|
31
|
+
// Edit.{file_path,old_string,new_string}, Write.{file_path,content},
|
|
32
|
+
// Grep/Glob.pattern, LS.path).
|
|
33
|
+
const TOOL_INPUT = {
|
|
34
|
+
COMMAND: 'command',
|
|
35
|
+
FILE_PATH: 'file_path',
|
|
36
|
+
PATH: 'path',
|
|
37
|
+
FILES: 'files',
|
|
38
|
+
PATTERN: 'pattern',
|
|
39
|
+
OLD_STRING: 'old_string',
|
|
40
|
+
NEW_STRING: 'new_string',
|
|
41
|
+
CONTENT: 'content',
|
|
42
|
+
};
|
|
43
|
+
// tool_result content-item field names (confirmed: is_error present on real
|
|
44
|
+
// records; interrupted absent in sample but part of the contract — default false).
|
|
45
|
+
const RESULT_FIELD = {
|
|
46
|
+
IS_ERROR: 'is_error',
|
|
47
|
+
INTERRUPTED: 'interrupted',
|
|
48
|
+
};
|
|
49
|
+
// --- Helpers (module-private; only normalizeRecord is exported) ---
|
|
50
|
+
function emptyDigest() {
|
|
51
|
+
return { files: [], cmd: null, query: null, lines_changed: null };
|
|
52
|
+
}
|
|
53
|
+
function findContentItem(content, type) {
|
|
54
|
+
if (!Array.isArray(content))
|
|
55
|
+
return null;
|
|
56
|
+
for (const item of content) {
|
|
57
|
+
if (item && typeof item === 'object' && item.type === type) {
|
|
58
|
+
return item;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Extract user/assistant prose text from a message content field. */
|
|
64
|
+
function extractText(content) {
|
|
65
|
+
if (typeof content === 'string')
|
|
66
|
+
return content;
|
|
67
|
+
if (Array.isArray(content)) {
|
|
68
|
+
for (const item of content) {
|
|
69
|
+
if (typeof item === 'string')
|
|
70
|
+
return item;
|
|
71
|
+
if (item && typeof item === 'object') {
|
|
72
|
+
const ci = item;
|
|
73
|
+
if (ci.type === CONTENT_TYPE.TEXT && typeof ci.text === 'string')
|
|
74
|
+
return ci.text;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract file paths from a tool-use input. If `input.files` is a non-empty
|
|
82
|
+
* array of strings, use it (multi-file case); otherwise collect the single
|
|
83
|
+
* `file_path` / `path` fields present.
|
|
84
|
+
*/
|
|
85
|
+
function extractPaths(input) {
|
|
86
|
+
const filesRaw = input[TOOL_INPUT.FILES];
|
|
87
|
+
if (Array.isArray(filesRaw)) {
|
|
88
|
+
const files = filesRaw.filter((f) => typeof f === 'string');
|
|
89
|
+
if (files.length > 0)
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
const paths = [];
|
|
93
|
+
const fp = input[TOOL_INPUT.FILE_PATH];
|
|
94
|
+
if (typeof fp === 'string')
|
|
95
|
+
paths.push(fp);
|
|
96
|
+
const p = input[TOOL_INPUT.PATH];
|
|
97
|
+
if (typeof p === 'string')
|
|
98
|
+
paths.push(p);
|
|
99
|
+
return paths;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Count lines in the "new content" of an edit, as a proxy for the amount of
|
|
103
|
+
* code touched (feeds explore_ratio and wall_clock_per_line). For Edit →
|
|
104
|
+
* new_string; for Write/NotebookEdit → content. Counts `\n`-separated lines;
|
|
105
|
+
* returns 0 if the field is absent or empty. A consistent "amount of new code"
|
|
106
|
+
* proxy is what matters downstream.
|
|
107
|
+
*/
|
|
108
|
+
function countNewLines(input) {
|
|
109
|
+
const ns = input[TOOL_INPUT.NEW_STRING];
|
|
110
|
+
const s = typeof ns === 'string'
|
|
111
|
+
? ns
|
|
112
|
+
: typeof input[TOOL_INPUT.CONTENT] === 'string'
|
|
113
|
+
? input[TOOL_INPUT.CONTENT]
|
|
114
|
+
: null;
|
|
115
|
+
if (s === null || s === '')
|
|
116
|
+
return 0;
|
|
117
|
+
return s.split('\n').length;
|
|
118
|
+
}
|
|
119
|
+
/** Build the InputDigest for a tool_use event based on its ToolKind. */
|
|
120
|
+
function digestForTool(tool, input) {
|
|
121
|
+
switch (tool) {
|
|
122
|
+
case 'exec': {
|
|
123
|
+
const cmd = input[TOOL_INPUT.COMMAND];
|
|
124
|
+
return {
|
|
125
|
+
files: [],
|
|
126
|
+
cmd: scrubCmd(typeof cmd === 'string' ? cmd : ''),
|
|
127
|
+
query: null,
|
|
128
|
+
lines_changed: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
case 'read':
|
|
132
|
+
case 'list':
|
|
133
|
+
return {
|
|
134
|
+
files: scrubFiles(extractPaths(input)),
|
|
135
|
+
cmd: null,
|
|
136
|
+
query: null,
|
|
137
|
+
lines_changed: null,
|
|
138
|
+
};
|
|
139
|
+
case 'edit':
|
|
140
|
+
return {
|
|
141
|
+
files: scrubFiles(extractPaths(input)),
|
|
142
|
+
cmd: null,
|
|
143
|
+
query: null,
|
|
144
|
+
lines_changed: countNewLines(input),
|
|
145
|
+
};
|
|
146
|
+
case 'search': {
|
|
147
|
+
const pattern = input[TOOL_INPUT.PATTERN];
|
|
148
|
+
return {
|
|
149
|
+
files: [],
|
|
150
|
+
cmd: null,
|
|
151
|
+
query: scrubQuery(typeof pattern === 'string' ? pattern : ''),
|
|
152
|
+
lines_changed: null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case 'other':
|
|
156
|
+
default:
|
|
157
|
+
return emptyDigest();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function isRecord(raw) {
|
|
161
|
+
return raw !== null && typeof raw === 'object';
|
|
162
|
+
}
|
|
163
|
+
// --- Public API ---
|
|
164
|
+
/**
|
|
165
|
+
* Convert one parsed Claude Code JSONL record into a NormalizedEvent, or return
|
|
166
|
+
* null when the record carries no usable event (system entries, ai-title, etc.).
|
|
167
|
+
*
|
|
168
|
+
* Mapping:
|
|
169
|
+
* - assistant tool_use → tool_call (tool + digest from input; ok=true)
|
|
170
|
+
* - user-record tool_result → tool_call (tool=null; ok=!is_error; interrupted)
|
|
171
|
+
* - user text → user_msg (correction via detectCorrection; no raw text)
|
|
172
|
+
* - assistant text → assistant_msg
|
|
173
|
+
* - anything else → null
|
|
174
|
+
*
|
|
175
|
+
* `ctx.prevToolCall` is passed to detectCorrection for user messages; it is NOT
|
|
176
|
+
* mutated here (the stream layer updates it between records). No raw prose is
|
|
177
|
+
* emitted: userText is consumed by detectCorrection and discarded.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeRecord(raw, ctx) {
|
|
180
|
+
if (!isRecord(raw))
|
|
181
|
+
return null;
|
|
182
|
+
const rec = raw;
|
|
183
|
+
const t = typeof rec[FIELD.TIMESTAMP] === 'string' ? rec[FIELD.TIMESTAMP] : '';
|
|
184
|
+
const duration_ms = typeof rec[FIELD.DURATION_MS] === 'number' ? rec[FIELD.DURATION_MS] : 0;
|
|
185
|
+
const msg = rec[FIELD.MESSAGE];
|
|
186
|
+
// --- User record: may carry a tool_result (nested) or plain text ---
|
|
187
|
+
if (rec[FIELD.TYPE] === RECORD_TYPE.USER && msg && typeof msg === 'object') {
|
|
188
|
+
const content = msg[FIELD.CONTENT];
|
|
189
|
+
// tool_result nested in a user message → tool_call result event.
|
|
190
|
+
const toolResult = findContentItem(content, CONTENT_TYPE.TOOL_RESULT);
|
|
191
|
+
if (toolResult) {
|
|
192
|
+
return {
|
|
193
|
+
t,
|
|
194
|
+
kind: 'tool_call',
|
|
195
|
+
tool: null, // result records carry no tool name
|
|
196
|
+
input_digest: emptyDigest(),
|
|
197
|
+
ok: toolResult[RESULT_FIELD.IS_ERROR] !== true,
|
|
198
|
+
interrupted: toolResult[RESULT_FIELD.INTERRUPTED] === true,
|
|
199
|
+
duration_ms,
|
|
200
|
+
correction: null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Plain text → user_msg (run correction detection; discard the text).
|
|
204
|
+
const userText = extractText(content);
|
|
205
|
+
if (userText !== null) {
|
|
206
|
+
const correction = detectCorrection(ctx.prevToolCall, userText);
|
|
207
|
+
return {
|
|
208
|
+
t,
|
|
209
|
+
kind: 'user_msg',
|
|
210
|
+
tool: null,
|
|
211
|
+
input_digest: emptyDigest(),
|
|
212
|
+
ok: true,
|
|
213
|
+
interrupted: false,
|
|
214
|
+
duration_ms,
|
|
215
|
+
correction,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
// --- Assistant record: tool_use or text ---
|
|
221
|
+
if (rec[FIELD.TYPE] === RECORD_TYPE.ASSISTANT && msg && typeof msg === 'object') {
|
|
222
|
+
const content = msg[FIELD.CONTENT];
|
|
223
|
+
// tool_use → tool_call event with digest extracted from input.
|
|
224
|
+
const toolUse = findContentItem(content, CONTENT_TYPE.TOOL_USE);
|
|
225
|
+
if (toolUse) {
|
|
226
|
+
const name = typeof toolUse.name === 'string' ? toolUse.name : '';
|
|
227
|
+
const input = toolUse.input && typeof toolUse.input === 'object'
|
|
228
|
+
? toolUse.input
|
|
229
|
+
: {};
|
|
230
|
+
const tool = mapToolKind(name);
|
|
231
|
+
return {
|
|
232
|
+
t,
|
|
233
|
+
kind: 'tool_call',
|
|
234
|
+
tool,
|
|
235
|
+
input_digest: digestForTool(tool, input),
|
|
236
|
+
ok: true, // ok is determined by the later tool_result, not the call
|
|
237
|
+
interrupted: false,
|
|
238
|
+
duration_ms,
|
|
239
|
+
correction: null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Text only → assistant_msg.
|
|
243
|
+
const text = extractText(content);
|
|
244
|
+
if (text !== null) {
|
|
245
|
+
return {
|
|
246
|
+
t,
|
|
247
|
+
kind: 'assistant_msg',
|
|
248
|
+
tool: null,
|
|
249
|
+
input_digest: emptyDigest(),
|
|
250
|
+
ok: true,
|
|
251
|
+
interrupted: false,
|
|
252
|
+
duration_ms,
|
|
253
|
+
correction: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
// Unknown record type (ai-title, summary, system entry, etc.) → skip.
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/adapter/parse.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,gFAAgF;AAChF,6EAA6E;AAC7E,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,gFAAgF;AAChF,uCAAuC;AAGvC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,8EAA8E;AAE9E,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM;IACZ,SAAS,EAAE,WAAW;CACd,CAAC;AAEX,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,aAAa;CAClB,CAAC;AAEX,MAAM,YAAY,GAAG;IACnB,QAAQ,EAAE,UAAU;IACpB,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,aAAa;CAClB,CAAC;AAEX,uEAAuE;AACvE,qEAAqE;AACrE,+BAA+B;AAC/B,MAAM,UAAU,GAAG;IACjB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,WAAW;IACtB,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,OAAO,EAAE,SAAS;CACV,CAAC;AAEX,4EAA4E;AAC5E,mFAAmF;AACnF,MAAM,YAAY,GAAG;IACnB,QAAQ,EAAE,UAAU;IACpB,WAAW,EAAE,aAAa;CAClB,CAAC;AAyBX,qEAAqE;AAErE,SAAS,WAAW;IAClB,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AACpE,CAAC;AAED,SAAS,eAAe,CAAC,OAAgB,EAAE,IAAY;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAK,IAAoB,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAC5E,OAAO,IAAmB,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,SAAS,WAAW,CAAC,OAAgB;IACnC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1C,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,MAAM,EAAE,GAAG,IAAmB,CAAC;gBAC/B,IAAI,EAAE,CAAC,IAAI,KAAK,YAAY,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAO,EAAE,CAAC,IAAI,CAAC;YACnF,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,KAA8B;IAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IACrC,CAAC;IACD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,KAA8B;IACnD,MAAM,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IACxC,MAAM,CAAC,GACL,OAAO,EAAE,KAAK,QAAQ;QACpB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,OAAO,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,QAAQ;YAC7C,CAAC,CAAE,KAAK,CAAC,UAAU,CAAC,OAAO,CAAY;YACvC,CAAC,CAAC,IAAI,CAAC;IACb,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AAC9B,CAAC;AAED,wEAAwE;AACxE,SAAS,aAAa,CAAC,IAAc,EAAE,KAA8B;IACnE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO;gBACL,KAAK,EAAE,EAAE;gBACT,GAAG,EAAE,QAAQ,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,KAAK,EAAE,IAAI;gBACX,aAAa,EAAE,IAAI;aACpB,CAAC;QACJ,CAAC;QACD,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM;YACT,OAAO;gBACL,KAAK,EAAE,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;gBACtC,GAAG,EAAE,IAAI;gBACT,KAAK,EAAE,IAAI;gBACX,aAAa,EAAE,IAAI;aACpB,CAAC;QACJ,KAAK,MAAM;YACT,OAAO;gBACL,KAAK,EAAE,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;gBACtC,GAAG,EAAE,IAAI;gBACT,KAAK,EAAE,IAAI;gBACX,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC;aACpC,CAAC;QACJ,KAAK,QAAQ,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC1C,OAAO;gBACL,KAAK,EAAE,EAAE;gBACT,GAAG,EAAE,IAAI;gBACT,KAAK,EAAE,UAAU,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,aAAa,EAAE,IAAI;aACpB,CAAC;QACJ,CAAC;QACD,KAAK,OAAO,CAAC;QACb;YACE,OAAO,WAAW,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,OAAO,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,CAAC;AACjD,CAAC;AAED,qBAAqB;AAErB;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAY,EACZ,GAAgD;IAEhD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,GAAG,GAAqB,GAAG,CAAC;IAClC,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,MAAM,WAAW,GACf,OAAO,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE/B,sEAAsE;IACtE,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,IAAI,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC3E,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEnC,iEAAiE;QACjE,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;QACtE,IAAI,UAAU,EAAE,CAAC;YACf,OAAO;gBACL,CAAC;gBACD,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,IAAI,EAAE,oCAAoC;gBAChD,YAAY,EAAE,WAAW,EAAE;gBAC3B,EAAE,EAAE,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,IAAI;gBAC9C,WAAW,EAAE,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI;gBAC1D,WAAW;gBACX,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QAED,sEAAsE;QACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,MAAM,UAAU,GAAG,gBAAgB,CAAC,GAAG,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAChE,OAAO;gBACL,CAAC;gBACD,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,IAAI;gBACV,YAAY,EAAE,WAAW,EAAE;gBAC3B,EAAE,EAAE,IAAI;gBACR,WAAW,EAAE,KAAK;gBAClB,WAAW;gBACX,UAAU;aACX,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6CAA6C;IAC7C,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,SAAS,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAChF,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEnC,+DAA+D;QAC/D,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAChE,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,MAAM,KAAK,GACT,OAAO,CAAC,KAAK,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;gBAChD,CAAC,CAAE,OAAO,CAAC,KAAiC;gBAC5C,CAAC,CAAC,EAAE,CAAC;YACT,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YAC/B,OAAO;gBACL,CAAC;gBACD,IAAI,EAAE,WAAW;gBACjB,IAAI;gBACJ,YAAY,EAAE,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC;gBACxC,EAAE,EAAE,IAAI,EAAE,0DAA0D;gBACpE,WAAW,EAAE,KAAK;gBAClB,WAAW;gBACX,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QAED,6BAA6B;QAC7B,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO;gBACL,CAAC;gBACD,IAAI,EAAE,eAAe;gBACrB,IAAI,EAAE,IAAI;gBACV,YAAY,EAAE,WAAW,EAAE;gBAC3B,EAAE,EAAE,IAAI;gBACR,WAAW,EAAE,KAAK;gBAClB,WAAW;gBACX,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sEAAsE;IACtE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrub an exec command string: apply the full catalog, then truncate to 512
|
|
3
|
+
* chars (no marker). Replacements use the fixed sentinel `***REDACTED***`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function scrubCmd(cmd: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Scrub a search/query string: same catalog and length cap as `scrubCmd`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function scrubQuery(q: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Scrub a list of file paths: replace any path matching a credential-file glob
|
|
12
|
+
* with the sentinel, leave others unchanged, then drop entries beyond 50 (no
|
|
13
|
+
* marker). Only the credential-file rule applies here — not the full catalog.
|
|
14
|
+
*/
|
|
15
|
+
export declare function scrubFiles(files: string[]): string[];
|