@zewstid/zeye-capture 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -8,7 +8,7 @@ redaction. Hidden model reasoning is never captured.
8
8
  Install the public CLI with Node 20.18.1 or newer:
9
9
 
10
10
  ```sh
11
- npm install --global @zewstid/zeye-capture@0.3.0
11
+ npm install --global @zewstid/zeye-capture@0.3.1
12
12
  ```
13
13
 
14
14
  Capture `0.3.x` uses the `@zewstid/zeye-shared` `0.5.x` contracts.
package/dist/src/cli.js CHANGED
File without changes
package/dist/src/host.js CHANGED
@@ -13,8 +13,15 @@ function parseCodexCheckpoint(cursor) {
13
13
  try {
14
14
  const parsed = JSON.parse(cursor);
15
15
  const completedLines = parsed.completedLines;
16
- if (parsed.version === 2 && Number.isInteger(completedLines) && completedLines !== undefined && completedLines >= 0 && typeof parsed.sourceGenerationId === 'string' && parsed.sourceGenerationId) {
17
- return { version: 2, completedLines, lastLineHash: typeof parsed.lastLineHash === 'string' ? parsed.lastLineHash : undefined, sourceGenerationId: parsed.sourceGenerationId };
16
+ if ((parsed.version === 2 || parsed.version === 3) && Number.isInteger(completedLines) && completedLines !== undefined && completedLines >= 0 && typeof parsed.sourceGenerationId === 'string' && parsed.sourceGenerationId) {
17
+ return {
18
+ version: parsed.version,
19
+ completedLines,
20
+ lastLineHash: typeof parsed.lastLineHash === 'string' ? parsed.lastLineHash : undefined,
21
+ sourceGenerationId: parsed.sourceGenerationId,
22
+ sourceSessionId: typeof parsed.sourceSessionId === 'string' ? parsed.sourceSessionId : undefined,
23
+ clientVersion: typeof parsed.clientVersion === 'string' ? parsed.clientVersion : undefined,
24
+ };
18
25
  }
19
26
  }
20
27
  catch { /* legacy cursor */ }
@@ -47,17 +54,45 @@ async function importCodexJsonl(outbox, filePath, context, captureContent = fals
47
54
  // replacement without ever storing the source record itself.
48
55
  const checkpointMatches = Boolean(checkpoint && completedLines <= lines.length && (!checkpoint.lastLineHash || completedLines === 0 || checkpoint.lastLineHash === (0, canonical_1.sha256)(lines[completedLines - 1])));
49
56
  const rotated = Boolean(checkpoint && !checkpointMatches);
50
- const start = rotated ? 0 : completedLines;
57
+ // v2 checkpoints advanced past records that omitted a per-line session ID.
58
+ // Reprocess them once under v3; the outbox's source-key idempotency safely
59
+ // collapses the records that were already captured while recovering those
60
+ // that the old parser skipped.
61
+ const checkpointNeedsIdentityUpgrade = Boolean(checkpoint && checkpoint.version !== 3);
62
+ const start = rotated || checkpointNeedsIdentityUpgrade ? 0 : completedLines;
51
63
  const generationId = checkpoint && !rotated && checkpoint.sourceGenerationId
52
64
  ? checkpoint.sourceGenerationId
53
65
  : sourceGenerationId(filePath, lines[0], file);
66
+ let streamIdentity = rotated
67
+ ? {}
68
+ : { sessionId: checkpoint?.sourceSessionId, clientVersion: checkpoint?.clientVersion };
69
+ // Session metadata is normally first, but pre-scan content-blind identity so
70
+ // resumed imports and provider ordering changes remain safe.
71
+ for (const line of lines) {
72
+ try {
73
+ const identity = (0, providers_1.codexStreamIdentity)(JSON.parse(line));
74
+ streamIdentity = {
75
+ sessionId: streamIdentity.sessionId || identity.sessionId,
76
+ clientVersion: streamIdentity.clientVersion || identity.clientVersion,
77
+ };
78
+ if (streamIdentity.sessionId && streamIdentity.clientVersion)
79
+ break;
80
+ }
81
+ catch { /* the ordered import below owns malformed-line handling */ }
82
+ }
54
83
  let imported = 0;
55
84
  let skipped = 0;
56
85
  let deferred = raw.length > 0 && !(raw.endsWith('\n') || raw.endsWith('\r\n')) ? 1 : 0;
57
86
  let consumed = start;
58
87
  for (let index = start; index < lines.length; index += 1) {
59
88
  try {
60
- const observation = (0, providers_1.fromCodexJsonl)(JSON.parse(lines[index]), (0, node_path_1.basename)(filePath), captureContent);
89
+ const parsedLine = JSON.parse(lines[index]);
90
+ const identity = (0, providers_1.codexStreamIdentity)(parsedLine);
91
+ streamIdentity = {
92
+ sessionId: identity.sessionId || streamIdentity.sessionId,
93
+ clientVersion: identity.clientVersion || streamIdentity.clientVersion,
94
+ };
95
+ const observation = (0, providers_1.fromCodexJsonl)(parsedLine, (0, node_path_1.basename)(filePath), captureContent, streamIdentity);
61
96
  if (!observation) {
62
97
  skipped += 1;
63
98
  consumed = index + 1;
@@ -83,7 +118,14 @@ async function importCodexJsonl(outbox, filePath, context, captureContent = fals
83
118
  break;
84
119
  }
85
120
  }
86
- await outbox.checkpoint('codex', scope, checkpointCursor({ version: 2, completedLines: consumed, lastLineHash: consumed ? (0, canonical_1.sha256)(lines[consumed - 1]) : undefined, sourceGenerationId: generationId }));
121
+ await outbox.checkpoint('codex', scope, checkpointCursor({
122
+ version: 3,
123
+ completedLines: consumed,
124
+ lastLineHash: consumed ? (0, canonical_1.sha256)(lines[consumed - 1]) : undefined,
125
+ sourceGenerationId: generationId,
126
+ sourceSessionId: streamIdentity.sessionId,
127
+ clientVersion: streamIdentity.clientVersion,
128
+ }));
87
129
  return { imported, skipped, deferred, rotated };
88
130
  }
89
131
  async function flushIfConfigured(outbox, config) {
@@ -1,4 +1,15 @@
1
1
  import type { MetadataCaptureEvent } from "./types";
2
+ export type CodexStreamIdentity = {
3
+ sessionId?: string;
4
+ clientVersion?: string;
5
+ };
6
+ /**
7
+ * Current Codex rollout logs put stream identity on `session_meta.payload`,
8
+ * while subsequent records carry only turn/item identifiers. Keep this small
9
+ * helper content-blind so the importer can safely inherit identity without
10
+ * inspecting prompts, responses, tool arguments, or reasoning.
11
+ */
12
+ export declare function codexStreamIdentity(input: unknown): CodexStreamIdentity;
2
13
  /** Parse provider-visible Claude transcript JSONL without hidden thinking blocks. */
3
14
  export declare function fromClaudeTranscriptJsonl(input: unknown, fallbackSessionId?: string): MetadataCaptureEvent | null;
4
15
  /** Parse a Claude Code hook payload without retaining prompt/tool/file content. */
@@ -8,7 +19,7 @@ export declare function fromClaudeCodeHook(input: unknown, captureContent?: bool
8
19
  * format evolves, so unknown record kinds become a safe capture warning rather
9
20
  * than being misrepresented as a complete observation.
10
21
  */
11
- export declare function fromCodexJsonl(input: unknown, sourceFile?: string, captureContent?: boolean): MetadataCaptureEvent | null;
22
+ export declare function fromCodexJsonl(input: unknown, sourceFile?: string, captureContent?: boolean, inheritedIdentity?: CodexStreamIdentity): MetadataCaptureEvent | null;
12
23
  /**
13
24
  * Convert a deliberately small event emitted by a VS Code/Copilot companion
14
25
  * extension. This is not a transcript importer: workspace paths, chat text,
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexStreamIdentity = codexStreamIdentity;
3
4
  exports.fromClaudeTranscriptJsonl = fromClaudeTranscriptJsonl;
4
5
  exports.fromClaudeCodeHook = fromClaudeCodeHook;
5
6
  exports.fromCodexJsonl = fromCodexJsonl;
@@ -24,6 +25,29 @@ function opaqueId(value, fallback) {
24
25
  return (candidate ||
25
26
  (0, node_crypto_1.createHash)("sha256").update(fallback).digest("hex").slice(0, 32));
26
27
  }
28
+ /**
29
+ * Current Codex rollout logs put stream identity on `session_meta.payload`,
30
+ * while subsequent records carry only turn/item identifiers. Keep this small
31
+ * helper content-blind so the importer can safely inherit identity without
32
+ * inspecting prompts, responses, tool arguments, or reasoning.
33
+ */
34
+ function codexStreamIdentity(input) {
35
+ const record = asRecord(input);
36
+ const payload = asRecord(record.payload);
37
+ const kind = string(record.type) || string(record.kind);
38
+ return {
39
+ sessionId: string(record.session_id) ||
40
+ string(record.sessionId) ||
41
+ string(payload.session_id) ||
42
+ string(payload.sessionId) ||
43
+ (kind === "session_meta" ? string(payload.id) : undefined),
44
+ clientVersion: string(record.client_version) ||
45
+ string(record.clientVersion) ||
46
+ string(payload.cli_version) ||
47
+ string(payload.client_version) ||
48
+ string(payload.clientVersion),
49
+ };
50
+ }
27
51
  const relationships = new Set([
28
52
  "ROOT",
29
53
  "RESUMED_FROM",
@@ -57,7 +81,11 @@ function usagePayload(value) {
57
81
  inputTokens: ["inputTokens", "input_tokens"],
58
82
  outputTokens: ["outputTokens", "output_tokens"],
59
83
  cachedInputTokens: ["cachedInputTokens", "cached_input_tokens"],
60
- reasoningTokens: ["reasoningTokens", "reasoning_tokens"],
84
+ reasoningTokens: [
85
+ "reasoningTokens",
86
+ "reasoning_tokens",
87
+ "reasoning_output_tokens",
88
+ ],
61
89
  costMicros: ["costMicros", "cost_micros"],
62
90
  };
63
91
  for (const [target, keys] of Object.entries(aliases)) {
@@ -219,17 +247,31 @@ function fromClaudeCodeHook(input, captureContent = false) {
219
247
  * format evolves, so unknown record kinds become a safe capture warning rather
220
248
  * than being misrepresented as a complete observation.
221
249
  */
222
- function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = false) {
250
+ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = false, inheritedIdentity = {}) {
223
251
  const record = asRecord(input);
224
252
  const payload = asRecord(record.payload);
225
- const sourceSessionId = string(record.session_id) ||
226
- string(record.sessionId) ||
227
- string(payload.session_id) ||
228
- string(payload.sessionId);
253
+ const identity = codexStreamIdentity(record);
254
+ const sourceSessionId = identity.sessionId || inheritedIdentity.sessionId;
229
255
  if (!sourceSessionId)
230
256
  return null;
231
257
  const kind = string(record.type) || string(record.kind) || "unknown";
232
258
  const eventName = string(payload.type) || string(payload.event) || kind;
259
+ // Current Codex emits these bookkeeping/wrapper records alongside the
260
+ // canonical response_item/token_usage_record stream. They do not describe a
261
+ // distinct user-visible event, and reasoning is provider-private. Ignoring
262
+ // them avoids duplicate evidence and never turns hidden reasoning into a
263
+ // transcript or misleading schema warning.
264
+ if (kind === "turn_context" ||
265
+ kind === "world_state" ||
266
+ (kind === "response_item" && eventName === "reasoning") ||
267
+ (kind === "event_msg" && [
268
+ "item_completed",
269
+ "token_count",
270
+ "thread_settings_applied",
271
+ "task_started",
272
+ "task_complete",
273
+ ].includes(eventName)))
274
+ return null;
233
275
  let type = "capture.warning";
234
276
  if (kind === "session_meta" ||
235
277
  eventName === "session_started" ||
@@ -244,12 +286,12 @@ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = fa
244
286
  else if (eventName === "task_complete" || eventName === "session_ended")
245
287
  type = "session.ended";
246
288
  else if (kind === "response_item" &&
247
- (string(payload.type) === "function_call" ||
248
- string(payload.item_type) === "function_call"))
289
+ (["function_call", "custom_tool_call"].includes(string(payload.type) || "") ||
290
+ ["function_call", "custom_tool_call"].includes(string(payload.item_type) || "")))
249
291
  type = "tool.started";
250
292
  else if (kind === "response_item" &&
251
- (string(payload.type) === "function_call_output" ||
252
- string(payload.item_type) === "function_call_output"))
293
+ (["function_call_output", "custom_tool_call_output"].includes(string(payload.type) || "") ||
294
+ ["function_call_output", "custom_tool_call_output"].includes(string(payload.item_type) || "")))
253
295
  type = "tool.completed";
254
296
  else if (eventName === "subagent_started")
255
297
  type = "subagent.started";
@@ -259,11 +301,13 @@ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = fa
259
301
  type = "message.recorded";
260
302
  else if (eventName === "user_message" || eventName === "agent_message")
261
303
  type = "message.recorded";
262
- else if (eventName === "usage" || eventName === "usage_recorded")
304
+ else if (kind === "token_usage_record" ||
305
+ eventName === "usage" ||
306
+ eventName === "usage_recorded")
263
307
  type = "usage.recorded";
264
308
  const lineId = string(record.id) ||
265
309
  string(payload.id) ||
266
- `${sourceFile}:${kind}:${record.timestamp || ""}:${eventName}`;
310
+ `${sourceFile}:${kind}:${record.ordinal || ""}:${record.timestamp || ""}:${eventName}`;
267
311
  const parent = string(payload.parent_session_id) ||
268
312
  string(payload.parentSessionId) ||
269
313
  string(record.parent_session_id) ||
@@ -274,9 +318,12 @@ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = fa
274
318
  string(payload.text);
275
319
  const toolName = string(payload.name) || string(payload.tool_name);
276
320
  const toolInput = string(payload.arguments) ||
321
+ string(payload.input) ||
277
322
  (payload.arguments && typeof payload.arguments === "object"
278
323
  ? JSON.stringify(payload.arguments)
279
- : undefined);
324
+ : payload.input && typeof payload.input === "object"
325
+ ? JSON.stringify(payload.input)
326
+ : undefined);
280
327
  const toolOutput = textParts(payload.output) || string(payload.result);
281
328
  const visible = type === "message.recorded"
282
329
  ? transcript(role === "assistant" || eventName === "agent_message"
@@ -299,7 +346,7 @@ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = fa
299
346
  adapter: "codex",
300
347
  provider: "codex",
301
348
  clientName: "Codex",
302
- clientVersion: string(record.client_version) || string(record.clientVersion),
349
+ clientVersion: identity.clientVersion || inheritedIdentity.clientVersion,
303
350
  sessionId: sourceSessionId,
304
351
  sourceEventId: opaqueId(lineId, sourceSessionId),
305
352
  occurredAt: timestamp(record.timestamp || payload.timestamp),
@@ -308,7 +355,7 @@ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = fa
308
355
  recordKind: kind,
309
356
  eventName,
310
357
  sourceFilePresent: Boolean(sourceFile),
311
- ...usagePayload(payload),
358
+ ...usagePayload(kind === "token_usage_record" ? asRecord(payload.usage) : payload),
312
359
  },
313
360
  sourceGenerationId: generation(record, payload),
314
361
  relationship: relationship(payload.relationship ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zewstid/zeye-capture",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Consented coding-session capture and continuous sync for Zeye Engineering Memory.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": { "access": "public" },