@aexol/spectral 0.9.254 → 0.9.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/extensions/dataset-uploader/analyzer.d.ts +103 -0
  2. package/dist/extensions/dataset-uploader/analyzer.d.ts.map +1 -0
  3. package/dist/extensions/dataset-uploader/analyzer.js +266 -0
  4. package/dist/extensions/dataset-uploader/cleaner.d.ts +59 -0
  5. package/dist/extensions/dataset-uploader/cleaner.d.ts.map +1 -0
  6. package/dist/extensions/dataset-uploader/cleaner.js +118 -0
  7. package/dist/extensions/dataset-uploader/exporter.d.ts +147 -0
  8. package/dist/extensions/dataset-uploader/exporter.d.ts.map +1 -0
  9. package/dist/extensions/dataset-uploader/exporter.js +313 -0
  10. package/dist/extensions/dataset-uploader/extra-values.d.ts +17 -0
  11. package/dist/extensions/dataset-uploader/extra-values.d.ts.map +1 -0
  12. package/dist/extensions/dataset-uploader/extra-values.js +51 -0
  13. package/dist/extensions/dataset-uploader/index.d.ts +22 -0
  14. package/dist/extensions/dataset-uploader/index.d.ts.map +1 -0
  15. package/dist/extensions/dataset-uploader/index.js +247 -0
  16. package/dist/extensions/dataset-uploader/scrubber.d.ts +34 -0
  17. package/dist/extensions/dataset-uploader/scrubber.d.ts.map +1 -0
  18. package/dist/extensions/dataset-uploader/scrubber.js +91 -0
  19. package/dist/extensions/dataset-uploader/upload.d.ts +105 -0
  20. package/dist/extensions/dataset-uploader/upload.d.ts.map +1 -0
  21. package/dist/extensions/dataset-uploader/upload.js +274 -0
  22. package/dist/generated/zeus/const.d.ts.map +1 -1
  23. package/dist/generated/zeus/const.js +32 -0
  24. package/dist/generated/zeus/index.d.ts +166 -0
  25. package/dist/generated/zeus/index.d.ts.map +1 -1
  26. package/dist/mcp/commands.d.ts.map +1 -1
  27. package/dist/mcp/commands.js +3 -0
  28. package/dist/mcp/direct-tools.d.ts.map +1 -1
  29. package/dist/mcp/direct-tools.js +4 -3
  30. package/dist/mcp/index.d.ts.map +1 -1
  31. package/dist/mcp/index.js +5 -1
  32. package/dist/mcp/mcp-auth-flow.d.ts +1 -1
  33. package/dist/mcp/mcp-auth-flow.d.ts.map +1 -1
  34. package/dist/mcp/mcp-auth-flow.js +13 -0
  35. package/dist/mcp/mcp-auth-start.d.ts +64 -0
  36. package/dist/mcp/mcp-auth-start.d.ts.map +1 -0
  37. package/dist/mcp/mcp-auth-start.js +136 -0
  38. package/dist/mcp/proxy-modes.d.ts +7 -0
  39. package/dist/mcp/proxy-modes.d.ts.map +1 -1
  40. package/dist/mcp/proxy-modes.js +71 -11
  41. package/dist/mcp/server-manager.d.ts +2 -1
  42. package/dist/mcp/server-manager.d.ts.map +1 -1
  43. package/dist/mcp/server-manager.js +8 -4
  44. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  45. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +10 -0
  46. package/dist/server/handlers/mcp-auth.d.ts +7 -4
  47. package/dist/server/handlers/mcp-auth.d.ts.map +1 -1
  48. package/dist/server/handlers/mcp-auth.js +15 -80
  49. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Heuristic quality/coverage analyzer for sharegpt dataset JSONL files
3
+ * (Dataset-uploader.md §3.1 — the `dataset_analyze` tool's core).
4
+ *
5
+ * Pure read-only analysis: no files are modified. The report is a plain
6
+ * object the extension renders as markdown so the agent can decide whether
7
+ * the dataset is worth cleaning/uploading.
8
+ *
9
+ * Report contents:
10
+ * - total samples + malformed line count
11
+ * - length distribution (p50/p95 chars per sample, rough token est chars/4)
12
+ * - per-tool coverage: how many samples use each function_call tool name,
13
+ * flagged when below LOW_TOOL_COVERAGE_THRESHOLD samples
14
+ * - local duplicates: canonicalized-JSON hash (sorted keys), dup count
15
+ * - dry-run scrub residue: per-kind redaction counts over all samples —
16
+ * this is the "residue red flag": > 0 means the dataset MUST go through
17
+ * `dataset_clean` before upload
18
+ * - quality flags: samples with 0 tool calls, samples with an observation
19
+ * longer than LONG_OBSERVATION_CHARS
20
+ */
21
+ /** Tools with fewer samples than this get flagged in the coverage table. */
22
+ export declare const LOW_TOOL_COVERAGE_THRESHOLD = 20;
23
+ /** Observations longer than this are flagged as oversized. */
24
+ export declare const LONG_OBSERVATION_CHARS = 8000;
25
+ /** Rough token estimate divisor (chars / 4). */
26
+ export declare const TOKEN_ESTIMATE_DIVISOR = 4;
27
+ export interface ToolCoverageEntry {
28
+ tool: string;
29
+ /** Number of samples containing at least one function_call for this tool. */
30
+ samples: number;
31
+ /** True when samples < LOW_TOOL_COVERAGE_THRESHOLD. */
32
+ low: boolean;
33
+ }
34
+ export interface DatasetAnalysisReport {
35
+ inputPath: string;
36
+ totalSamples: number;
37
+ malformedLines: number;
38
+ lengthDistribution: {
39
+ p50Chars: number;
40
+ p95Chars: number;
41
+ p50EstTokens: number;
42
+ p95EstTokens: number;
43
+ };
44
+ /** Per-tool sample coverage, sorted by samples desc, then name. */
45
+ toolCoverage: ToolCoverageEntry[];
46
+ duplicates: {
47
+ total: number;
48
+ unique: number;
49
+ /** total - unique (samples that are exact canonicalized duplicates). */
50
+ duplicateSamples: number;
51
+ };
52
+ /**
53
+ * Dry-run scrub over every sample: how many redactions the scrubber WOULD
54
+ * make. Any total > 0 is the residue red flag — run dataset_clean first.
55
+ */
56
+ scrubResidue: {
57
+ totalRedactions: number;
58
+ byKind: Record<string, number>;
59
+ };
60
+ qualityFlags: {
61
+ /** Samples with zero function_call entries. */
62
+ zeroToolCalls: number;
63
+ /** Samples containing an observation longer than LONG_OBSERVATION_CHARS. */
64
+ longObservations: number;
65
+ };
66
+ }
67
+ export interface LoadedJsonl {
68
+ samples: unknown[];
69
+ malformedLines: number;
70
+ }
71
+ /**
72
+ * Load a JSONL file into parsed values, skipping (and counting) malformed
73
+ * lines. Non-object JSON (e.g. a bare string) counts as malformed too.
74
+ */
75
+ export declare function loadJsonl(inputPath: string): LoadedJsonl;
76
+ /**
77
+ * Total character count of a sharegpt sample: all conversation entry values
78
+ * plus the system prompt (when present). Used for length distribution,
79
+ * maxSampleChars filtering, and token estimates.
80
+ */
81
+ export declare function sampleChars(sample: Record<string, unknown>): number;
82
+ /** Tool names used by a sample's function_call entries (unique, in order). */
83
+ export declare function sampleToolNames(sample: Record<string, unknown>): string[];
84
+ /**
85
+ * Canonicalize a JSON value: recursively sort object keys, drop undefined,
86
+ * then stringify. Deterministic for duplicate detection.
87
+ */
88
+ export declare function canonicalizeJson(value: unknown): string;
89
+ /**
90
+ * Analyze a sharegpt dataset JSONL file. Read-only; never throws on
91
+ * malformed lines (they are counted instead). Throws only when the input
92
+ * file itself cannot be read.
93
+ *
94
+ * `options.extraValues` (same opaque values `dataset_clean` scrubs with —
95
+ * secret-store + .env values) extends the dry-run scrub residue counts;
96
+ * values are never printed.
97
+ */
98
+ export declare function analyzeDataset(inputPath: string, options?: {
99
+ extraValues?: string[];
100
+ }): DatasetAnalysisReport;
101
+ /** Render an analysis report as a markdown table the agent can read. */
102
+ export declare function renderAnalysisMarkdown(report: DatasetAnalysisReport): string;
103
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../../src/extensions/dataset-uploader/analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,gDAAgD;AAChD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,mEAAmE;IACnE,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF;;;OAGG;IACH,YAAY,EAAE;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAChC,CAAC;IACF,YAAY,EAAE;QACZ,+CAA+C;QAC/C,aAAa,EAAE,MAAM,CAAC;QACtB,4EAA4E;QAC5E,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAmBxD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAYnE;AAUD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAyBzE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAWvD;AAcD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACvC,qBAAqB,CAyEvB;AAMD,wEAAwE;AACxE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAmD5E"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Heuristic quality/coverage analyzer for sharegpt dataset JSONL files
3
+ * (Dataset-uploader.md §3.1 — the `dataset_analyze` tool's core).
4
+ *
5
+ * Pure read-only analysis: no files are modified. The report is a plain
6
+ * object the extension renders as markdown so the agent can decide whether
7
+ * the dataset is worth cleaning/uploading.
8
+ *
9
+ * Report contents:
10
+ * - total samples + malformed line count
11
+ * - length distribution (p50/p95 chars per sample, rough token est chars/4)
12
+ * - per-tool coverage: how many samples use each function_call tool name,
13
+ * flagged when below LOW_TOOL_COVERAGE_THRESHOLD samples
14
+ * - local duplicates: canonicalized-JSON hash (sorted keys), dup count
15
+ * - dry-run scrub residue: per-kind redaction counts over all samples —
16
+ * this is the "residue red flag": > 0 means the dataset MUST go through
17
+ * `dataset_clean` before upload
18
+ * - quality flags: samples with 0 tool calls, samples with an observation
19
+ * longer than LONG_OBSERVATION_CHARS
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import { readFileSync } from "node:fs";
23
+ import { scrub } from "./scrubber.js";
24
+ /** Tools with fewer samples than this get flagged in the coverage table. */
25
+ export const LOW_TOOL_COVERAGE_THRESHOLD = 20;
26
+ /** Observations longer than this are flagged as oversized. */
27
+ export const LONG_OBSERVATION_CHARS = 8000;
28
+ /** Rough token estimate divisor (chars / 4). */
29
+ export const TOKEN_ESTIMATE_DIVISOR = 4;
30
+ /**
31
+ * Load a JSONL file into parsed values, skipping (and counting) malformed
32
+ * lines. Non-object JSON (e.g. a bare string) counts as malformed too.
33
+ */
34
+ export function loadJsonl(inputPath) {
35
+ const raw = readFileSync(inputPath, "utf8");
36
+ const samples = [];
37
+ let malformedLines = 0;
38
+ for (const line of raw.split("\n")) {
39
+ const trimmed = line.trim();
40
+ if (!trimmed)
41
+ continue;
42
+ try {
43
+ const value = JSON.parse(trimmed);
44
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
45
+ malformedLines++;
46
+ continue;
47
+ }
48
+ samples.push(value);
49
+ }
50
+ catch {
51
+ malformedLines++;
52
+ }
53
+ }
54
+ return { samples, malformedLines };
55
+ }
56
+ /**
57
+ * Total character count of a sharegpt sample: all conversation entry values
58
+ * plus the system prompt (when present). Used for length distribution,
59
+ * maxSampleChars filtering, and token estimates.
60
+ */
61
+ export function sampleChars(sample) {
62
+ let chars = 0;
63
+ const conversations = sample.conversations;
64
+ if (Array.isArray(conversations)) {
65
+ for (const entry of conversations) {
66
+ if (entry && typeof entry === "object" && typeof entry.value === "string") {
67
+ chars += entry.value.length;
68
+ }
69
+ }
70
+ }
71
+ if (typeof sample.system === "string")
72
+ chars += sample.system.length;
73
+ return chars;
74
+ }
75
+ function entryValue(entry) {
76
+ if (entry && typeof entry === "object") {
77
+ const value = entry.value;
78
+ if (typeof value === "string")
79
+ return value;
80
+ }
81
+ return null;
82
+ }
83
+ /** Tool names used by a sample's function_call entries (unique, in order). */
84
+ export function sampleToolNames(sample) {
85
+ const names = [];
86
+ const seen = new Set();
87
+ const conversations = sample.conversations;
88
+ if (!Array.isArray(conversations))
89
+ return names;
90
+ for (const entry of conversations) {
91
+ if (!(entry && typeof entry === "object") ||
92
+ entry.from !== "function_call") {
93
+ continue;
94
+ }
95
+ const value = entryValue(entry);
96
+ if (value === null)
97
+ continue;
98
+ try {
99
+ const parsed = JSON.parse(value);
100
+ if (typeof parsed.name === "string" && parsed.name.length > 0 && !seen.has(parsed.name)) {
101
+ seen.add(parsed.name);
102
+ names.push(parsed.name);
103
+ }
104
+ }
105
+ catch {
106
+ // Malformed function_call payload — ignore for coverage.
107
+ }
108
+ }
109
+ return names;
110
+ }
111
+ /**
112
+ * Canonicalize a JSON value: recursively sort object keys, drop undefined,
113
+ * then stringify. Deterministic for duplicate detection.
114
+ */
115
+ export function canonicalizeJson(value) {
116
+ if (value === null || typeof value !== "object")
117
+ return JSON.stringify(value) ?? "null";
118
+ if (Array.isArray(value))
119
+ return `[${value.map((v) => canonicalizeJson(v)).join(",")}]`;
120
+ const keys = Object.keys(value)
121
+ .filter((k) => value[k] !== undefined)
122
+ .sort();
123
+ const parts = keys.map((k) => {
124
+ const v = value[k];
125
+ return `${JSON.stringify(k)}:${canonicalizeJson(v)}`;
126
+ });
127
+ return `{${parts.join(",")}}`;
128
+ }
129
+ /** Nearest-rank percentile of a non-negative metric array (unsorted ok). */
130
+ function percentile(values, p) {
131
+ if (values.length === 0)
132
+ return 0;
133
+ const sorted = [...values].sort((a, b) => a - b);
134
+ const idx = Math.max(0, Math.ceil(p * sorted.length) - 1);
135
+ return sorted[idx];
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Analysis
139
+ // ---------------------------------------------------------------------------
140
+ /**
141
+ * Analyze a sharegpt dataset JSONL file. Read-only; never throws on
142
+ * malformed lines (they are counted instead). Throws only when the input
143
+ * file itself cannot be read.
144
+ *
145
+ * `options.extraValues` (same opaque values `dataset_clean` scrubs with —
146
+ * secret-store + .env values) extends the dry-run scrub residue counts;
147
+ * values are never printed.
148
+ */
149
+ export function analyzeDataset(inputPath, options = {}) {
150
+ const { samples, malformedLines } = loadJsonl(inputPath);
151
+ const typed = samples;
152
+ const lengths = typed.map((s) => sampleChars(s));
153
+ const p50 = percentile(lengths, 0.5);
154
+ const p95 = percentile(lengths, 0.95);
155
+ // ---- per-tool coverage (samples using each function_call tool) --------
156
+ const toolSampleCounts = {};
157
+ let zeroToolCalls = 0;
158
+ let longObservations = 0;
159
+ for (const sample of typed) {
160
+ const names = sampleToolNames(sample);
161
+ if (names.length === 0)
162
+ zeroToolCalls++;
163
+ for (const name of names)
164
+ toolSampleCounts[name] = (toolSampleCounts[name] ?? 0) + 1;
165
+ const conversations = sample.conversations;
166
+ if (Array.isArray(conversations)) {
167
+ for (const entry of conversations) {
168
+ if (entry?.from === "observation") {
169
+ const value = entryValue(entry);
170
+ if (value !== null && value.length > LONG_OBSERVATION_CHARS) {
171
+ longObservations++;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ const toolCoverage = Object.entries(toolSampleCounts)
179
+ .map(([tool, samples_]) => ({
180
+ tool,
181
+ samples: samples_,
182
+ low: samples_ < LOW_TOOL_COVERAGE_THRESHOLD,
183
+ }))
184
+ .sort((a, b) => b.samples - a.samples || a.tool.localeCompare(b.tool));
185
+ // ---- local duplicates (canonicalized JSON hash) ------------------------
186
+ const seen = new Set();
187
+ for (const sample of typed) {
188
+ seen.add(createHash("sha256").update(canonicalizeJson(sample)).digest("hex"));
189
+ }
190
+ // ---- dry-run scrub residue (the red flag) -------------------------------
191
+ const residue = {};
192
+ for (const sample of typed) {
193
+ const result = scrub(JSON.stringify(sample), options.extraValues);
194
+ for (const r of result.redactions)
195
+ residue[r.kind] = (residue[r.kind] ?? 0) + r.count;
196
+ }
197
+ const totalRedactions = Object.values(residue).reduce((sum, n) => sum + n, 0);
198
+ return {
199
+ inputPath,
200
+ totalSamples: typed.length,
201
+ malformedLines,
202
+ lengthDistribution: {
203
+ p50Chars: p50,
204
+ p95Chars: p95,
205
+ p50EstTokens: Math.round(p50 / TOKEN_ESTIMATE_DIVISOR),
206
+ p95EstTokens: Math.round(p95 / TOKEN_ESTIMATE_DIVISOR),
207
+ },
208
+ toolCoverage,
209
+ duplicates: {
210
+ total: typed.length,
211
+ unique: seen.size,
212
+ duplicateSamples: typed.length - seen.size,
213
+ },
214
+ scrubResidue: {
215
+ totalRedactions,
216
+ byKind: Object.fromEntries(Object.entries(residue).sort((a, b) => a[0].localeCompare(b[0]))),
217
+ },
218
+ qualityFlags: { zeroToolCalls, longObservations },
219
+ };
220
+ }
221
+ // ---------------------------------------------------------------------------
222
+ // Markdown rendering
223
+ // ---------------------------------------------------------------------------
224
+ /** Render an analysis report as a markdown table the agent can read. */
225
+ export function renderAnalysisMarkdown(report) {
226
+ const lines = [];
227
+ lines.push(`# Dataset analysis — ${report.inputPath}`);
228
+ lines.push("");
229
+ lines.push(`- Samples: **${report.totalSamples}**${report.malformedLines > 0 ? ` (${report.malformedLines} malformed line(s) skipped)` : ""}`);
230
+ lines.push(`- Length: p50 ≈ ${report.lengthDistribution.p50Chars} chars (~${report.lengthDistribution.p50EstTokens} tokens), ` +
231
+ `p95 ≈ ${report.lengthDistribution.p95Chars} chars (~${report.lengthDistribution.p95EstTokens} tokens)`);
232
+ lines.push(`- Local duplicates: ${report.duplicates.duplicateSamples} of ${report.duplicates.total} ` +
233
+ `(${report.duplicates.unique} unique)`);
234
+ lines.push(`- Quality flags: ${report.qualityFlags.zeroToolCalls} sample(s) without tool calls, ` +
235
+ `${report.qualityFlags.longObservations} sample(s) with oversized observations (> ${LONG_OBSERVATION_CHARS} chars)`);
236
+ lines.push("");
237
+ lines.push("## Tool coverage");
238
+ lines.push("");
239
+ if (report.toolCoverage.length === 0) {
240
+ lines.push("_No tool calls found in any sample._");
241
+ }
242
+ else {
243
+ lines.push("| Tool | Samples | < 20 samples |");
244
+ lines.push("| --- | ---: | --- |");
245
+ for (const entry of report.toolCoverage) {
246
+ lines.push(`| ${entry.tool} | ${entry.samples} | ${entry.low ? "⚠️ yes" : ""} |`);
247
+ }
248
+ }
249
+ lines.push("");
250
+ lines.push("## Scrub residue (dry run)");
251
+ lines.push("");
252
+ if (report.scrubResidue.totalRedactions === 0) {
253
+ lines.push("✅ No secret-like patterns detected — dataset is clean by heuristic.");
254
+ }
255
+ else {
256
+ lines.push(`🚩 **Residue red flag: ${report.scrubResidue.totalRedactions} redaction(s) detected. ` +
257
+ "Run `dataset_clean` before uploading.**");
258
+ lines.push("");
259
+ lines.push("| Kind | Count |");
260
+ lines.push("| --- | ---: |");
261
+ for (const [kind, count] of Object.entries(report.scrubResidue.byKind)) {
262
+ lines.push(`| ${kind} | ${count} |`);
263
+ }
264
+ }
265
+ return lines.join("\n");
266
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Dataset cleaner (Dataset-uploader.md §2.3 — the `dataset_clean` tool's core).
3
+ *
4
+ * Loads a sharegpt dataset JSONL, scrubs EVERY string field of every sample
5
+ * (the JSON tree is walked; structure is never changed), applies optional
6
+ * quality filters, and writes `<name>.clean.jsonl` next to the input.
7
+ *
8
+ * - minTurns — drop samples with fewer assistant ("gpt") turns
9
+ * - maxSampleChars — drop samples longer than this many chars
10
+ * - keepOnlyToolCalls — drop samples without any function_call entry
11
+ *
12
+ * The scrubber receives `extraValues` collected by the caller (secret-store
13
+ * values, backend/.env.local values). This module never reads any store and
14
+ * never prints values — only per-kind redaction counts.
15
+ */
16
+ export interface CleanDatasetOptions {
17
+ /** Output path. Default: `<input-without>.clean.jsonl` next to the input. */
18
+ outputPath?: string;
19
+ /** Minimum number of assistant ("gpt") turns per sample. */
20
+ minTurns?: number;
21
+ /** Maximum total characters per sample (conversation values + system). */
22
+ maxSampleChars?: number;
23
+ /** When true, drop samples that contain no function_call entries. */
24
+ keepOnlyToolCalls?: boolean;
25
+ /**
26
+ * Opaque secret values to redact literally (secret-store values, .env
27
+ * values). The cleaner never reads any store itself.
28
+ */
29
+ extraValues?: string[];
30
+ }
31
+ export interface CleanDatasetResult {
32
+ inputPath: string;
33
+ outputPath: string;
34
+ total: number;
35
+ kept: number;
36
+ removed: number;
37
+ malformedLines: number;
38
+ removedByFilter: {
39
+ minTurns: number;
40
+ maxSampleChars: number;
41
+ keepOnlyToolCalls: number;
42
+ };
43
+ /** Per-kind redaction counts (only kinds with count > 0, stable order). */
44
+ redactions: {
45
+ kind: string;
46
+ count: number;
47
+ }[];
48
+ }
49
+ /** Derive the default clean output path: `export-x.jsonl` → `export-x.clean.jsonl`. */
50
+ export declare function defaultCleanPath(inputPath: string): string;
51
+ /**
52
+ * Clean a dataset JSONL file: scrub all string fields, apply optional
53
+ * filters, write `<name>.clean.jsonl` next to the input.
54
+ *
55
+ * Throws when the input file cannot be read. Malformed lines are counted,
56
+ * not fatal.
57
+ */
58
+ export declare function cleanDataset(inputPath: string, options?: CleanDatasetOptions): CleanDatasetResult;
59
+ //# sourceMappingURL=cleaner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../../src/extensions/dataset-uploader/cleaner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,2EAA2E;IAC3E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/C;AA6CD,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAyDpB"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Dataset cleaner (Dataset-uploader.md §2.3 — the `dataset_clean` tool's core).
3
+ *
4
+ * Loads a sharegpt dataset JSONL, scrubs EVERY string field of every sample
5
+ * (the JSON tree is walked; structure is never changed), applies optional
6
+ * quality filters, and writes `<name>.clean.jsonl` next to the input.
7
+ *
8
+ * - minTurns — drop samples with fewer assistant ("gpt") turns
9
+ * - maxSampleChars — drop samples longer than this many chars
10
+ * - keepOnlyToolCalls — drop samples without any function_call entry
11
+ *
12
+ * The scrubber receives `extraValues` collected by the caller (secret-store
13
+ * values, backend/.env.local values). This module never reads any store and
14
+ * never prints values — only per-kind redaction counts.
15
+ */
16
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
+ import { dirname } from "node:path";
18
+ import { loadJsonl, sampleChars } from "./analyzer.js";
19
+ import { scrub } from "./scrubber.js";
20
+ const REDACTION_KINDS = ["api_key", "jwt", "bearer", "home_path", "secret_value"];
21
+ /**
22
+ * Walk a JSON tree and scrub every string leaf. Objects/arrays are rebuilt
23
+ * (structure and key order preserved); non-string leaves pass through.
24
+ */
25
+ function scrubDeep(value, extraValues, counts) {
26
+ if (typeof value === "string") {
27
+ const result = scrub(value, extraValues);
28
+ for (const r of result.redactions)
29
+ counts[r.kind] = (counts[r.kind] ?? 0) + r.count;
30
+ return result.text;
31
+ }
32
+ if (Array.isArray(value))
33
+ return value.map((v) => scrubDeep(v, extraValues, counts));
34
+ if (value !== null && typeof value === "object") {
35
+ const out = {};
36
+ for (const [key, v] of Object.entries(value)) {
37
+ out[key] = scrubDeep(v, extraValues, counts);
38
+ }
39
+ return out;
40
+ }
41
+ return value;
42
+ }
43
+ function countGptTurns(sample) {
44
+ const conversations = sample.conversations;
45
+ if (!Array.isArray(conversations))
46
+ return 0;
47
+ return conversations.filter((e) => e && typeof e === "object" && e.from === "gpt").length;
48
+ }
49
+ function hasToolCall(sample) {
50
+ const conversations = sample.conversations;
51
+ if (!Array.isArray(conversations))
52
+ return false;
53
+ return conversations.some((e) => e && typeof e === "object" && e.from === "function_call");
54
+ }
55
+ /** Derive the default clean output path: `export-x.jsonl` → `export-x.clean.jsonl`. */
56
+ export function defaultCleanPath(inputPath) {
57
+ if (inputPath.endsWith(".jsonl")) {
58
+ return `${inputPath.slice(0, -".jsonl".length)}.clean.jsonl`;
59
+ }
60
+ return `${inputPath}.clean.jsonl`;
61
+ }
62
+ /**
63
+ * Clean a dataset JSONL file: scrub all string fields, apply optional
64
+ * filters, write `<name>.clean.jsonl` next to the input.
65
+ *
66
+ * Throws when the input file cannot be read. Malformed lines are counted,
67
+ * not fatal.
68
+ */
69
+ export function cleanDataset(inputPath, options = {}) {
70
+ if (!existsSync(inputPath)) {
71
+ throw new Error(`Dataset file not found: ${inputPath}`);
72
+ }
73
+ const { samples, malformedLines } = loadJsonl(inputPath);
74
+ const typed = samples;
75
+ const counts = {};
76
+ const kept = [];
77
+ const removedByFilter = { minTurns: 0, maxSampleChars: 0, keepOnlyToolCalls: 0 };
78
+ for (const sample of typed) {
79
+ // Filters first (on the original sample — cheaper and keeps counts
80
+ // independent of scrubbing).
81
+ if (typeof options.minTurns === "number" &&
82
+ options.minTurns > 0 &&
83
+ countGptTurns(sample) < options.minTurns) {
84
+ removedByFilter.minTurns++;
85
+ continue;
86
+ }
87
+ if (typeof options.maxSampleChars === "number" &&
88
+ options.maxSampleChars > 0 &&
89
+ sampleChars(sample) > options.maxSampleChars) {
90
+ removedByFilter.maxSampleChars++;
91
+ continue;
92
+ }
93
+ if (options.keepOnlyToolCalls === true && !hasToolCall(sample)) {
94
+ removedByFilter.keepOnlyToolCalls++;
95
+ continue;
96
+ }
97
+ kept.push(scrubDeep(sample, options.extraValues, counts));
98
+ }
99
+ const outputPath = options.outputPath ?? defaultCleanPath(inputPath);
100
+ mkdirSync(dirname(outputPath), { recursive: true });
101
+ const content = kept.length > 0
102
+ ? kept.map((s) => JSON.stringify(s)).join("\n") + "\n"
103
+ : "";
104
+ writeFileSync(outputPath, content, "utf8");
105
+ const redactions = REDACTION_KINDS
106
+ .filter((kind) => (counts[kind] ?? 0) > 0)
107
+ .map((kind) => ({ kind, count: counts[kind] }));
108
+ return {
109
+ inputPath,
110
+ outputPath,
111
+ total: typed.length,
112
+ kept: kept.length,
113
+ removed: typed.length - kept.length,
114
+ malformedLines,
115
+ removedByFilter,
116
+ redactions,
117
+ };
118
+ }