@gobing-ai/ts-llm-jsonl-importer 0.4.49 → 0.4.50

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/dist/mappers.d.ts CHANGED
@@ -3,12 +3,11 @@ import type { JsonObject, SplitEntry, TransformContext } from './types';
3
3
  /** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
4
4
  export declare function argsDigest(args: unknown): string;
5
5
  /**
6
- * Return retained raw args for tools whose arguments carry forensic value, otherwise
7
- * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
8
- * retain just the command string (capped). Codex `arguments` arrives as a JSON string
9
- * already — store as-is for todo tools.
6
+ * Return retained raw args for a typed tool call: the shell command string for
7
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
8
+ * undefined when the event carries no invocation arguments.
10
9
  */
11
- export declare function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined;
10
+ export declare function maybeArgsRaw(_source: string, toolName: string, args: unknown): string | undefined;
12
11
  /** Map a Claude Code JSONL record into one or more ETL split entries. */
13
12
  export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
14
13
  /** Map a Pi JSONL record into one or more ETL split entries. */
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
package/dist/mappers.js CHANGED
@@ -91,79 +91,122 @@ function redactArgs(args) {
91
91
  return args;
92
92
  }
93
93
  /**
94
- * Per-source todo-writing tool names whose raw arguments are retained for phase detection.
95
- *
96
- * Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
97
- * session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
98
- * 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
99
- * opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
100
- * rows with NULL args_raw).
101
- */
102
- const TODO_TOOL_ALLOWLIST = {
103
- claude: ['TodoWrite'],
104
- opencode: ['todowrite', 'todoread'],
105
- codex: ['update_plan'],
106
- omp: ['TodoWrite', 'todo', 'todo_write'],
107
- grok: ['todo_write'],
108
- pi: ['todo', 'manage_todo_list'],
109
- agy: [],
110
- gemini: [],
111
- };
112
- /**
113
- * Per-source shell/exec tool names whose command string is retained in args_raw.
114
- *
115
- * WHY: bash commands are the primary forensic signal for session-task attribution and ops
116
- * history (spur's task classifier regexes `spur task <verb> <wbs>` out of args_raw). Only
117
- * evidenced sources are listed; unverified shapes stay empty the same way the todo allowlist
118
- * treats agy/gemini. Commands pass through the importer's redaction layer like any other
119
- * retained field (importer.ts redacts every split record before insert).
94
+ * Retained tool-call arguments: recognized shell tools keep the extracted plain
95
+ * command (attribution compatibility, capped); every other typed tool call keeps
96
+ * sanitized-but-valid JSON. `args_digest` is computed elsewhere from the unpruned
97
+ * args and is unaffected; secret redaction stays at the persistence `redactRecord`
98
+ * seam (task 0064 R5).
120
99
  */
121
- const BASH_TOOL_ALLOWLIST = {
122
- claude: ['Bash'], // input: { command: string } (mappers.test.ts fixture)
123
- opencode: ['bash'], // input: { command: string } (opencode-importer.test.ts)
124
- codex: [], // arguments arrive as a pre-stringified JSON string; command shape unverified
125
- omp: [],
126
- grok: [],
127
- pi: ['bash'], // arguments: { command: string } (live pi session transcripts)
128
- agy: [],
129
- gemini: [],
130
- };
131
- /** Retained bash commands are capped; attribution only needs the invocation line. */
132
100
  const BASH_COMMAND_MAX_CHARS = 8192;
101
+ const GENERIC_ARGS_MAX_CHARS = 8192;
133
102
  /** Extract the shell command from a bash tool's raw input; undefined when absent. */
134
103
  function bashCommandOf(args) {
135
104
  if (args === null || typeof args !== 'object')
136
105
  return undefined;
137
- const command = args.command;
106
+ const rec = args;
107
+ const command = rec.command ?? rec.CommandLine ?? rec.cmd ?? rec.script;
138
108
  if (typeof command === 'string')
139
109
  return command;
140
110
  if (Array.isArray(command))
141
111
  return command.map(String).join(' ');
142
112
  return undefined;
143
113
  }
114
+ /** Bulky payload keys (normalized) pruned to an omission marker; camelCase and snake_case share one rule. */
115
+ const BULKY_PAYLOAD_KEYS = new Set([
116
+ 'content',
117
+ 'codecontent',
118
+ 'replacementcontent',
119
+ 'targetcontent',
120
+ 'filedata',
121
+ 'buffer',
122
+ 'blob',
123
+ 'image',
124
+ 'imagebase64',
125
+ ]);
126
+ /** Lowercase a key and drop separators so code_content and codeContent compare equal. */
127
+ function normalizeKey(key) {
128
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '');
129
+ }
130
+ /** Tool names whose command-shaped args retain the existing plain-string representation. */
131
+ function isShellTool(toolName) {
132
+ return /^(?:bash|shell|cmd|command|exec|execcommand|executecommand|runcommand|shellcommand|runshellcommand)$/.test(normalizeKey(toolName));
133
+ }
144
134
  /**
145
- * Return retained raw args for tools whose arguments carry forensic value, otherwise
146
- * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
147
- * retain just the command string (capped). Codex `arguments` arrives as a JSON string
148
- * already — store as-is for todo tools.
135
+ * Sanitize any JSON value recursively through objects AND arrays: bulky payload
136
+ * strings under known keys become `[omitted N chars]`, other strings longer than
137
+ * 1,000 characters keep a prefix plus an explicit original-length marker, and
138
+ * every primitive and short payload is preserved untouched (task 0064 R2).
149
139
  */
150
- export function maybeArgsRaw(source, toolName, args) {
151
- const todo = TODO_TOOL_ALLOWLIST[source];
152
- if (todo?.includes(toolName)) {
153
- if (typeof args === 'string')
154
- return args;
155
- if (args === undefined || args === null)
156
- return undefined;
157
- return JSON.stringify(args);
140
+ function sanitizeValue(value) {
141
+ if (typeof value === 'string') {
142
+ return value.length > 1000 ? `${value.slice(0, 1000)}[truncated ${value.length} chars]` : value;
158
143
  }
159
- const bash = BASH_TOOL_ALLOWLIST[source];
160
- if (bash?.includes(toolName)) {
161
- const command = bashCommandOf(args);
162
- if (command === undefined)
163
- return undefined;
164
- return command.length > BASH_COMMAND_MAX_CHARS ? command.slice(0, BASH_COMMAND_MAX_CHARS) : command;
144
+ if (Array.isArray(value))
145
+ return value.map(sanitizeValue);
146
+ if (value !== null && typeof value === 'object') {
147
+ const out = {};
148
+ for (const [key, entry] of Object.entries(value)) {
149
+ out[key] =
150
+ typeof entry === 'string' && entry.length > 120 && BULKY_PAYLOAD_KEYS.has(normalizeKey(key))
151
+ ? `[omitted ${entry.length} chars]`
152
+ : sanitizeValue(entry);
153
+ }
154
+ return out;
165
155
  }
166
- return undefined;
156
+ // JSON leaves (number/boolean/null) pass through unchanged; JSON args never
157
+ // reach here as undefined/bigint/symbol/function.
158
+ return value;
159
+ }
160
+ /**
161
+ * Serialize sanitized args within the retention ceiling without ever slicing JSON
162
+ * text: values that still serialize past the ceiling collapse into a small valid
163
+ * JSON wrapper carrying `_truncated`, the omitted character count, and a bounded
164
+ * preview reduced until the wrapper itself fits (task 0064 R3/AC4).
165
+ */
166
+ function serializeBounded(value) {
167
+ const serialized = JSON.stringify(sanitizeValue(value));
168
+ if (serialized.length <= GENERIC_ARGS_MAX_CHARS)
169
+ return serialized;
170
+ let preview = serialized;
171
+ for (;;) {
172
+ const wrapper = JSON.stringify({
173
+ _truncated: true,
174
+ omittedChars: serialized.length - preview.length,
175
+ preview,
176
+ });
177
+ if (wrapper.length <= GENERIC_ARGS_MAX_CHARS)
178
+ return wrapper;
179
+ preview = preview.slice(0, preview.length - (wrapper.length - GENERIC_ARGS_MAX_CHARS));
180
+ }
181
+ }
182
+ /**
183
+ * Return retained raw args for a typed tool call: the shell command string for
184
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
185
+ * undefined when the event carries no invocation arguments.
186
+ */
187
+ export function maybeArgsRaw(_source, toolName, args) {
188
+ if (args === undefined || args === null)
189
+ return undefined;
190
+ let value = args;
191
+ if (typeof args === 'string') {
192
+ // Pre-stringified arguments (Codex) re-enter the structured path so the
193
+ // retained value parses and prunes like native JSON.
194
+ try {
195
+ value = JSON.parse(args);
196
+ }
197
+ catch {
198
+ return args.length > BASH_COMMAND_MAX_CHARS ? args.slice(0, BASH_COMMAND_MAX_CHARS) : args;
199
+ }
200
+ }
201
+ if (value !== null && typeof value === 'object') {
202
+ const cmd = bashCommandOf(value);
203
+ if (cmd !== undefined && isShellTool(toolName)) {
204
+ return cmd.length > BASH_COMMAND_MAX_CHARS ? cmd.slice(0, BASH_COMMAND_MAX_CHARS) : cmd;
205
+ }
206
+ return serializeBounded(value);
207
+ }
208
+ // JSON primitives (number/boolean) retain as valid JSON scalars.
209
+ return JSON.stringify(value);
167
210
  }
168
211
  // ---------------------------------------------------------------------------
169
212
  // Claude mapper
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.49",
3
+ "version": "0.4.50",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.49",
51
- "@gobing-ai/ts-runtime": "^0.4.49",
52
- "@gobing-ai/ts-utils": "^0.4.49",
50
+ "@gobing-ai/ts-db": "^0.4.50",
51
+ "@gobing-ai/ts-runtime": "^0.4.50",
52
+ "@gobing-ai/ts-utils": "^0.4.50",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/mappers.ts CHANGED
@@ -101,77 +101,129 @@ function redactArgs(args: unknown): RedactedValue {
101
101
  }
102
102
 
103
103
  /**
104
- * Per-source todo-writing tool names whose raw arguments are retained for phase detection.
105
- *
106
- * Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
107
- * session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
108
- * 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
109
- * opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
110
- * rows with NULL args_raw).
111
- */
112
- const TODO_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
113
- claude: ['TodoWrite'],
114
- opencode: ['todowrite', 'todoread'],
115
- codex: ['update_plan'],
116
- omp: ['TodoWrite', 'todo', 'todo_write'],
117
- grok: ['todo_write'],
118
- pi: ['todo', 'manage_todo_list'],
119
- agy: [],
120
- gemini: [],
121
- };
122
-
123
- /**
124
- * Per-source shell/exec tool names whose command string is retained in args_raw.
125
- *
126
- * WHY: bash commands are the primary forensic signal for session-task attribution and ops
127
- * history (spur's task classifier regexes `spur task <verb> <wbs>` out of args_raw). Only
128
- * evidenced sources are listed; unverified shapes stay empty the same way the todo allowlist
129
- * treats agy/gemini. Commands pass through the importer's redaction layer like any other
130
- * retained field (importer.ts redacts every split record before insert).
104
+ * Retained tool-call arguments: recognized shell tools keep the extracted plain
105
+ * command (attribution compatibility, capped); every other typed tool call keeps
106
+ * sanitized-but-valid JSON. `args_digest` is computed elsewhere from the unpruned
107
+ * args and is unaffected; secret redaction stays at the persistence `redactRecord`
108
+ * seam (task 0064 R5).
131
109
  */
132
- const BASH_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
133
- claude: ['Bash'], // input: { command: string } (mappers.test.ts fixture)
134
- opencode: ['bash'], // input: { command: string } (opencode-importer.test.ts)
135
- codex: [], // arguments arrive as a pre-stringified JSON string; command shape unverified
136
- omp: [],
137
- grok: [],
138
- pi: ['bash'], // arguments: { command: string } (live pi session transcripts)
139
- agy: [],
140
- gemini: [],
141
- };
142
-
143
- /** Retained bash commands are capped; attribution only needs the invocation line. */
144
110
  const BASH_COMMAND_MAX_CHARS = 8192;
111
+ const GENERIC_ARGS_MAX_CHARS = 8192;
145
112
 
146
113
  /** Extract the shell command from a bash tool's raw input; undefined when absent. */
147
114
  function bashCommandOf(args: unknown): string | undefined {
148
115
  if (args === null || typeof args !== 'object') return undefined;
149
- const command = (args as Record<string, unknown>).command;
116
+ const rec = args as Record<string, unknown>;
117
+ const command = rec.command ?? rec.CommandLine ?? rec.cmd ?? rec.script;
150
118
  if (typeof command === 'string') return command;
151
119
  if (Array.isArray(command)) return command.map(String).join(' ');
152
120
  return undefined;
153
121
  }
154
122
 
123
+ /** Bulky payload keys (normalized) pruned to an omission marker; camelCase and snake_case share one rule. */
124
+ const BULKY_PAYLOAD_KEYS: ReadonlySet<string> = new Set([
125
+ 'content',
126
+ 'codecontent',
127
+ 'replacementcontent',
128
+ 'targetcontent',
129
+ 'filedata',
130
+ 'buffer',
131
+ 'blob',
132
+ 'image',
133
+ 'imagebase64',
134
+ ]);
135
+
136
+ /** Lowercase a key and drop separators so code_content and codeContent compare equal. */
137
+ function normalizeKey(key: string): string {
138
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '');
139
+ }
140
+
141
+ /** Tool names whose command-shaped args retain the existing plain-string representation. */
142
+ function isShellTool(toolName: string): boolean {
143
+ return /^(?:bash|shell|cmd|command|exec|execcommand|executecommand|runcommand|shellcommand|runshellcommand)$/.test(
144
+ normalizeKey(toolName),
145
+ );
146
+ }
147
+
148
+ /** Sanitized tool args: JSON shape preserved, bulky payload strings elided. */
149
+ type SanitizedValue = string | number | boolean | null | SanitizedValue[] | { [key: string]: SanitizedValue };
150
+
155
151
  /**
156
- * Return retained raw args for tools whose arguments carry forensic value, otherwise
157
- * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
158
- * retain just the command string (capped). Codex `arguments` arrives as a JSON string
159
- * already — store as-is for todo tools.
152
+ * Sanitize any JSON value recursively through objects AND arrays: bulky payload
153
+ * strings under known keys become `[omitted N chars]`, other strings longer than
154
+ * 1,000 characters keep a prefix plus an explicit original-length marker, and
155
+ * every primitive and short payload is preserved untouched (task 0064 R2).
160
156
  */
161
- export function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined {
162
- const todo = TODO_TOOL_ALLOWLIST[source];
163
- if (todo?.includes(toolName)) {
164
- if (typeof args === 'string') return args;
165
- if (args === undefined || args === null) return undefined;
166
- return JSON.stringify(args);
157
+ function sanitizeValue(value: unknown): SanitizedValue {
158
+ if (typeof value === 'string') {
159
+ return value.length > 1000 ? `${value.slice(0, 1000)}[truncated ${value.length} chars]` : value;
167
160
  }
168
- const bash = BASH_TOOL_ALLOWLIST[source];
169
- if (bash?.includes(toolName)) {
170
- const command = bashCommandOf(args);
171
- if (command === undefined) return undefined;
172
- return command.length > BASH_COMMAND_MAX_CHARS ? command.slice(0, BASH_COMMAND_MAX_CHARS) : command;
161
+ if (Array.isArray(value)) return value.map(sanitizeValue);
162
+ if (value !== null && typeof value === 'object') {
163
+ const out: Record<string, SanitizedValue> = {};
164
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
165
+ out[key] =
166
+ typeof entry === 'string' && entry.length > 120 && BULKY_PAYLOAD_KEYS.has(normalizeKey(key))
167
+ ? `[omitted ${entry.length} chars]`
168
+ : sanitizeValue(entry);
169
+ }
170
+ return out;
173
171
  }
174
- return undefined;
172
+ // JSON leaves (number/boolean/null) pass through unchanged; JSON args never
173
+ // reach here as undefined/bigint/symbol/function.
174
+ return value as SanitizedValue;
175
+ }
176
+
177
+ /**
178
+ * Serialize sanitized args within the retention ceiling without ever slicing JSON
179
+ * text: values that still serialize past the ceiling collapse into a small valid
180
+ * JSON wrapper carrying `_truncated`, the omitted character count, and a bounded
181
+ * preview reduced until the wrapper itself fits (task 0064 R3/AC4).
182
+ */
183
+ function serializeBounded(value: unknown): string {
184
+ const serialized = JSON.stringify(sanitizeValue(value));
185
+ if (serialized.length <= GENERIC_ARGS_MAX_CHARS) return serialized;
186
+ let preview = serialized;
187
+ for (;;) {
188
+ const wrapper = JSON.stringify({
189
+ _truncated: true,
190
+ omittedChars: serialized.length - preview.length,
191
+ preview,
192
+ });
193
+ if (wrapper.length <= GENERIC_ARGS_MAX_CHARS) return wrapper;
194
+ preview = preview.slice(0, preview.length - (wrapper.length - GENERIC_ARGS_MAX_CHARS));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Return retained raw args for a typed tool call: the shell command string for
200
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
201
+ * undefined when the event carries no invocation arguments.
202
+ */
203
+ export function maybeArgsRaw(_source: string, toolName: string, args: unknown): string | undefined {
204
+ if (args === undefined || args === null) return undefined;
205
+
206
+ let value = args;
207
+ if (typeof args === 'string') {
208
+ // Pre-stringified arguments (Codex) re-enter the structured path so the
209
+ // retained value parses and prunes like native JSON.
210
+ try {
211
+ value = JSON.parse(args);
212
+ } catch {
213
+ return args.length > BASH_COMMAND_MAX_CHARS ? args.slice(0, BASH_COMMAND_MAX_CHARS) : args;
214
+ }
215
+ }
216
+
217
+ if (value !== null && typeof value === 'object') {
218
+ const cmd = bashCommandOf(value);
219
+ if (cmd !== undefined && isShellTool(toolName)) {
220
+ return cmd.length > BASH_COMMAND_MAX_CHARS ? cmd.slice(0, BASH_COMMAND_MAX_CHARS) : cmd;
221
+ }
222
+ return serializeBounded(value);
223
+ }
224
+
225
+ // JSON primitives (number/boolean) retain as valid JSON scalars.
226
+ return JSON.stringify(value);
175
227
  }
176
228
 
177
229
  // ---------------------------------------------------------------------------