@zewstid/zeye-capture 0.2.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.
@@ -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 ||
@@ -1,3 +1,4 @@
1
+ import type { LocalHostConfig } from './localHost';
1
2
  export type ServiceEnrollment = {
2
3
  endpoint: string;
3
4
  token: string;
@@ -7,6 +8,11 @@ export type ServiceEnrollment = {
7
8
  contentConsentId?: string;
8
9
  transcriptConsent?: 'yes';
9
10
  intervalSeconds: number;
11
+ repositories?: Array<{
12
+ repositoryRoot: string;
13
+ repositoryId?: string;
14
+ providerRepositoryId?: string;
15
+ }>;
10
16
  };
11
17
  export type ServiceFiles = {
12
18
  serviceId: string;
@@ -14,7 +20,21 @@ export type ServiceFiles = {
14
20
  definitionPath: string;
15
21
  kind: 'launchd' | 'systemd' | 'windows-task';
16
22
  };
17
- export declare function serviceFiles(repositoryRoot: string, environment?: NodeJS.ProcessEnv, os?: NodeJS.Platform): ServiceFiles;
23
+ export declare function mergeRepositoryRegistry(existing: NonNullable<ServiceEnrollment["repositories"]> | undefined, repository: NonNullable<ServiceEnrollment['repositories']>[number]): {
24
+ repositoryRoot: string;
25
+ repositoryId?: string;
26
+ providerRepositoryId?: string;
27
+ }[];
28
+ /**
29
+ * Build the service's live repository set from both explicit Zeye bindings and
30
+ * capture adapters installed on this machine. The result stays local: callers
31
+ * use roots only to open repository-scoped outboxes, while the wire contract
32
+ * receives hashes/canonical repository facts from captureRepositoryReference.
33
+ * Explicit enrollment bindings win so internal/provider IDs are preserved.
34
+ */
35
+ export declare function discoverServiceRepositories(enrolled: NonNullable<ServiceEnrollment["repositories"]> | undefined, hostConfig: LocalHostConfig, fallbackRepositoryRoot?: string): NonNullable<ServiceEnrollment['repositories']>;
36
+ export declare function serviceFiles(repositoryRoot: string, environment?: NodeJS.ProcessEnv, os?: NodeJS.Platform, machineHost?: boolean): ServiceFiles;
37
+ export declare function hostServiceFiles(environment?: NodeJS.ProcessEnv, os?: NodeJS.Platform): ServiceFiles;
18
38
  export declare function serviceDefinition(files: ServiceFiles, repositoryRoot: string, nodePath: string, cliPath: string): string;
19
39
  export declare function installCaptureService(input: {
20
40
  repositoryRoot: string;
@@ -23,6 +43,13 @@ export declare function installCaptureService(input: {
23
43
  nodePath?: string;
24
44
  os?: NodeJS.Platform;
25
45
  }): Promise<ServiceFiles>;
46
+ export declare function installCaptureHostService(input: {
47
+ enrollment: ServiceEnrollment;
48
+ cliPath: string;
49
+ nodePath?: string;
50
+ os?: NodeJS.Platform;
51
+ }): Promise<ServiceFiles>;
26
52
  export declare function uninstallCaptureService(repositoryRoot: string, os?: NodeJS.Platform): Promise<ServiceFiles>;
53
+ export declare function uninstallCaptureHostService(os?: NodeJS.Platform): Promise<ServiceFiles>;
27
54
  export declare function readServiceEnrollment(path: string): Promise<ServiceEnrollment>;
28
55
  export declare function applyServiceEnrollment(enrollment: ServiceEnrollment): void;
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeRepositoryRegistry = mergeRepositoryRegistry;
4
+ exports.discoverServiceRepositories = discoverServiceRepositories;
3
5
  exports.serviceFiles = serviceFiles;
6
+ exports.hostServiceFiles = hostServiceFiles;
4
7
  exports.serviceDefinition = serviceDefinition;
5
8
  exports.installCaptureService = installCaptureService;
9
+ exports.installCaptureHostService = installCaptureHostService;
6
10
  exports.uninstallCaptureService = uninstallCaptureService;
11
+ exports.uninstallCaptureHostService = uninstallCaptureHostService;
7
12
  exports.readServiceEnrollment = readServiceEnrollment;
8
13
  exports.applyServiceEnrollment = applyServiceEnrollment;
9
14
  const node_crypto_1 = require("node:crypto");
@@ -11,11 +16,41 @@ const promises_1 = require("node:fs/promises");
11
16
  const node_os_1 = require("node:os");
12
17
  const node_path_1 = require("node:path");
13
18
  const node_child_process_1 = require("node:child_process");
19
+ function mergeRepositoryRegistry(existing = [], repository) {
20
+ return [...existing.filter(entry => entry.repositoryRoot !== repository.repositoryRoot), repository]
21
+ .sort((left, right) => left.repositoryRoot.localeCompare(right.repositoryRoot));
22
+ }
23
+ /**
24
+ * Build the service's live repository set from both explicit Zeye bindings and
25
+ * capture adapters installed on this machine. The result stays local: callers
26
+ * use roots only to open repository-scoped outboxes, while the wire contract
27
+ * receives hashes/canonical repository facts from captureRepositoryReference.
28
+ * Explicit enrollment bindings win so internal/provider IDs are preserved.
29
+ */
30
+ function discoverServiceRepositories(enrolled = [], hostConfig, fallbackRepositoryRoot) {
31
+ const discoveredRoots = [
32
+ ...hostConfig.claudeInstalls.map(entry => entry.repositoryRoot),
33
+ ...hostConfig.codexSources.map(entry => entry.repositoryRoot),
34
+ ...(fallbackRepositoryRoot ? [fallbackRepositoryRoot] : []),
35
+ ];
36
+ const byRoot = new Map();
37
+ for (const root of discoveredRoots.slice(0, 500)) {
38
+ if (typeof root !== 'string' || !root.trim())
39
+ continue;
40
+ const repositoryRoot = (0, node_path_1.resolve)(root);
41
+ byRoot.set(repositoryRoot, { repositoryRoot });
42
+ }
43
+ for (const binding of enrolled.slice(0, 500)) {
44
+ const repositoryRoot = (0, node_path_1.resolve)(binding.repositoryRoot);
45
+ byRoot.set(repositoryRoot, { ...binding, repositoryRoot });
46
+ }
47
+ return [...byRoot.values()].sort((left, right) => left.repositoryRoot.localeCompare(right.repositoryRoot));
48
+ }
14
49
  function idFor(repositoryRoot) { return (0, node_crypto_1.createHash)('sha256').update(repositoryRoot).digest('hex').slice(0, 16); }
15
50
  function xml(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
16
51
  function systemd(value) { return `"${value.replace(/([\\"])/g, '\\$1')}"`; }
17
- function serviceFiles(repositoryRoot, environment = process.env, os = (0, node_os_1.platform)()) {
18
- const serviceId = `com.zewst.zeye-capture.${idFor(repositoryRoot)}`;
52
+ function serviceFiles(repositoryRoot, environment = process.env, os = (0, node_os_1.platform)(), machineHost = false) {
53
+ const serviceId = machineHost ? 'com.zewst.zeye-capture.host' : `com.zewst.zeye-capture.${idFor(repositoryRoot)}`;
19
54
  const configHome = environment.XDG_CONFIG_HOME || (os === 'win32' ? environment.APPDATA : undefined) || (0, node_path_1.join)((0, node_os_1.homedir)(), '.config');
20
55
  const enrollmentPath = (0, node_path_1.join)(configHome, 'zeye-capture', 'services', `${serviceId}.json`);
21
56
  if (os === 'darwin')
@@ -24,24 +59,28 @@ function serviceFiles(repositoryRoot, environment = process.env, os = (0, node_o
24
59
  return { serviceId, enrollmentPath, definitionPath: (0, node_path_1.join)(configHome, 'zeye-capture', 'services', `${serviceId}.cmd`), kind: 'windows-task' };
25
60
  return { serviceId, enrollmentPath, definitionPath: (0, node_path_1.join)(configHome, 'systemd', 'user', `${serviceId}.service`), kind: 'systemd' };
26
61
  }
62
+ function hostServiceFiles(environment = process.env, os = (0, node_os_1.platform)()) {
63
+ return serviceFiles('machine-host', environment, os, true);
64
+ }
27
65
  function serviceDefinition(files, repositoryRoot, nodePath, cliPath) {
66
+ const repositoryArguments = repositoryRoot ? `<string>--repo</string><string>${xml(repositoryRoot)}</string>` : '';
28
67
  if (files.kind === 'launchd')
29
68
  return `<?xml version="1.0" encoding="UTF-8"?>
30
69
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
70
  <plist version="1.0"><dict>
32
71
  <key>Label</key><string>${xml(files.serviceId)}</string>
33
- <key>ProgramArguments</key><array><string>${xml(nodePath)}</string><string>${xml(cliPath)}</string><string>service-run</string><string>--repo</string><string>${xml(repositoryRoot)}</string><string>--config</string><string>${xml(files.enrollmentPath)}</string></array>
72
+ <key>ProgramArguments</key><array><string>${xml(nodePath)}</string><string>${xml(cliPath)}</string><string>service-run</string>${repositoryArguments}<string>--config</string><string>${xml(files.enrollmentPath)}</string></array>
34
73
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ProcessType</key><string>Background</string>
35
74
  </dict></plist>
36
75
  `;
37
76
  if (files.kind === 'systemd')
38
77
  return `[Unit]
39
- Description=Zeye Engineering Memory continuous capture for ${repositoryRoot.replace(/\n/g, ' ')}
78
+ Description=Zeye Engineering Memory continuous capture${repositoryRoot ? ` for ${repositoryRoot.replace(/\n/g, ' ')}` : ' host'}
40
79
  After=network-online.target
41
80
 
42
81
  [Service]
43
82
  Type=simple
44
- ExecStart=${systemd(nodePath)} ${systemd(cliPath)} service-run --repo ${systemd(repositoryRoot)} --config ${systemd(files.enrollmentPath)}
83
+ ExecStart=${systemd(nodePath)} ${systemd(cliPath)} service-run${repositoryRoot ? ` --repo ${systemd(repositoryRoot)}` : ''} --config ${systemd(files.enrollmentPath)}
45
84
  Restart=always
46
85
  RestartSec=15
47
86
  NoNewPrivileges=true
@@ -50,7 +89,7 @@ PrivateTmp=true
50
89
  [Install]
51
90
  WantedBy=default.target
52
91
  `;
53
- return `@echo off\r\n"${nodePath.replace(/"/g, '""')}" "${cliPath.replace(/"/g, '""')}" service-run --repo "${repositoryRoot.replace(/"/g, '""')}" --config "${files.enrollmentPath.replace(/"/g, '""')}"\r\n`;
92
+ return `@echo off\r\n"${nodePath.replace(/"/g, '""')}" "${cliPath.replace(/"/g, '""')}" service-run${repositoryRoot ? ` --repo "${repositoryRoot.replace(/"/g, '""')}"` : ''} --config "${files.enrollmentPath.replace(/"/g, '""')}"\r\n`;
54
93
  }
55
94
  async function run(command, args) {
56
95
  await new Promise((resolve, reject) => {
@@ -83,6 +122,30 @@ async function installCaptureService(input) {
83
122
  }
84
123
  return files;
85
124
  }
125
+ async function installCaptureHostService(input) {
126
+ const os = input.os || (0, node_os_1.platform)();
127
+ const files = hostServiceFiles(process.env, os);
128
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(files.enrollmentPath), { recursive: true, mode: 0o700 });
129
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(files.definitionPath), { recursive: true, mode: 0o700 });
130
+ await (0, promises_1.writeFile)(files.enrollmentPath, `${JSON.stringify(input.enrollment)}\n`, { encoding: 'utf8', mode: 0o600 });
131
+ await (0, promises_1.writeFile)(files.definitionPath, serviceDefinition(files, '', input.nodePath || process.execPath, input.cliPath), { encoding: 'utf8', mode: 0o600 });
132
+ if (os === 'darwin') {
133
+ const domain = `gui/${process.getuid?.()}`;
134
+ await run('launchctl', ['bootout', domain, files.definitionPath]).catch(() => undefined);
135
+ await run('launchctl', ['bootstrap', domain, files.definitionPath]);
136
+ await run('launchctl', ['kickstart', '-k', `${domain}/${files.serviceId}`]);
137
+ }
138
+ else if (os === 'win32') {
139
+ await run('icacls', [files.enrollmentPath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:(F)`]);
140
+ await run('schtasks', ['/Create', '/F', '/SC', 'ONLOGON', '/TN', files.serviceId, '/TR', files.definitionPath]);
141
+ await run('schtasks', ['/Run', '/TN', files.serviceId]);
142
+ }
143
+ else {
144
+ await run('systemctl', ['--user', 'daemon-reload']);
145
+ await run('systemctl', ['--user', 'enable', '--now', files.serviceId]);
146
+ }
147
+ return files;
148
+ }
86
149
  async function uninstallCaptureService(repositoryRoot, os = (0, node_os_1.platform)()) {
87
150
  const files = serviceFiles(repositoryRoot, process.env, os);
88
151
  if (os === 'darwin')
@@ -97,12 +160,28 @@ async function uninstallCaptureService(repositoryRoot, os = (0, node_os_1.platfo
97
160
  await (0, promises_1.rm)(files.enrollmentPath, { force: true });
98
161
  return files;
99
162
  }
163
+ async function uninstallCaptureHostService(os = (0, node_os_1.platform)()) {
164
+ const files = hostServiceFiles(process.env, os);
165
+ if (os === 'darwin')
166
+ await run('launchctl', ['bootout', `gui/${process.getuid?.()}`, files.definitionPath]).catch(() => undefined);
167
+ else if (os === 'win32')
168
+ await run('schtasks', ['/Delete', '/F', '/TN', files.serviceId]).catch(() => undefined);
169
+ else {
170
+ await run('systemctl', ['--user', 'disable', '--now', files.serviceId]).catch(() => undefined);
171
+ await run('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
172
+ }
173
+ await (0, promises_1.rm)(files.definitionPath, { force: true });
174
+ await (0, promises_1.rm)(files.enrollmentPath, { force: true });
175
+ return files;
176
+ }
100
177
  async function readServiceEnrollment(path) {
101
178
  const parsed = JSON.parse(await (0, promises_1.readFile)(path, 'utf8'));
102
179
  if (!parsed.endpoint || !parsed.token || !parsed.organizationId || !parsed.captureInstallationId || !parsed.actorJson)
103
180
  throw new Error('Service enrollment is incomplete');
104
181
  if (!Number.isInteger(parsed.intervalSeconds) || parsed.intervalSeconds < 15 || parsed.intervalSeconds > 3600)
105
182
  throw new Error('Service interval is invalid');
183
+ if (parsed.repositories && (!Array.isArray(parsed.repositories) || parsed.repositories.some(repository => !repository.repositoryRoot)))
184
+ throw new Error('Service repository registry is invalid');
106
185
  return parsed;
107
186
  }
108
187
  function applyServiceEnrollment(enrollment) {
@@ -211,6 +211,9 @@ function deliveryToBatch(record, config) {
211
211
  const provider = providerByAdapter[record.adapter];
212
212
  if (!provider)
213
213
  throw new Error(`Unsupported capture adapter: ${record.adapter}`);
214
+ if (record.transcript && (!record.contentConsentId || record.contentConsentId !== config.privacy.contentConsentId)) {
215
+ throw new Error('Queued transcript consent is missing or no longer active');
216
+ }
214
217
  const eventType = eventTypes.has(record.type)
215
218
  ? record.type
216
219
  : "capture.warning";
@@ -231,7 +234,7 @@ function deliveryToBatch(record, config) {
231
234
  ]
232
235
  : config.completeness || [];
233
236
  const privacy = record.transcript
234
- ? config.privacy
237
+ ? { ...config.privacy, contentConsentId: record.contentConsentId }
235
238
  : {
236
239
  ...config.privacy,
237
240
  contentMode: "METADATA_ONLY",
@@ -61,6 +61,8 @@ export interface DeliveryRecord {
61
61
  sourceGenerationId?: string;
62
62
  relationship?: 'ROOT' | 'RESUMED_FROM' | 'FORKED_FROM' | 'SUBAGENT_OF' | 'CONTINUATION_OF';
63
63
  relatedSourceSessionId?: string;
64
+ /** Consent active when transcript content entered durable storage. */
65
+ contentConsentId?: string;
64
66
  transcript?: NonNullable<CaptureEvent['transcript']> & {
65
67
  redactionCount: number;
66
68
  redactionPolicyVersion: string;
@@ -91,6 +93,7 @@ export interface DeliveryRecord {
91
93
  /** Immutable non-secret evidence context captured with one delivery. */
92
94
  export interface CaptureDeliveryContext {
93
95
  repositories: CaptureRepositoryReference[];
96
+ contentConsentId?: string;
94
97
  }
95
98
  export interface SourceCheckpoint {
96
99
  adapter: string;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@zewstid/zeye-capture",
3
- "version": "0.2.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" },
7
+ "engines": { "node": ">=20.18.1" },
7
8
  "main": "dist/src/index.js",
8
9
  "types": "dist/src/index.d.ts",
9
- "files": ["dist", "README.md"],
10
+ "files": ["dist/src", "README.md"],
10
11
  "bin": {
11
12
  "zeye-capture": "dist/src/cli.js"
12
13
  },
@@ -16,7 +17,7 @@
16
17
  "prepublishOnly": "npm test"
17
18
  },
18
19
  "dependencies": {
19
- "@zewstid/zeye-shared": "^0.4.0"
20
+ "@zewstid/zeye-shared": "^0.5.0"
20
21
  },
21
22
  "devDependencies": {
22
23
  "typescript": "^5.4.3"
@@ -1 +0,0 @@
1
- export {};
@@ -1,104 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strict_1 = __importDefault(require("node:assert/strict"));
7
- const promises_1 = require("node:fs/promises");
8
- const node_os_1 = require("node:os");
9
- const node_path_1 = require("node:path");
10
- const node_test_1 = __importDefault(require("node:test"));
11
- const fileOutbox_1 = require("../src/fileOutbox");
12
- const redactor = {
13
- redact(event) {
14
- const payload = event.payload;
15
- return {
16
- payload: { note: payload.note.replace(/sk-[A-Za-z0-9_-]+/g, '[REDACTED]') },
17
- redactionCount: payload.note.includes('sk-') ? 1 : 0,
18
- redactionReasons: payload.note.includes('sk-') ? ['api_key'] : [],
19
- };
20
- },
21
- };
22
- function event(overrides = {}) {
23
- return {
24
- adapter: 'claude-code',
25
- sessionId: 'session-1',
26
- sourceEventId: 'event-1',
27
- occurredAt: '2026-09-05T12:00:00.000Z',
28
- type: 'tool.completed',
29
- payload: { note: 'token sk-secret' },
30
- ...overrides,
31
- };
32
- }
33
- async function withOutbox(run) {
34
- const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'zeye-capture-test-'));
35
- try {
36
- await run(new fileOutbox_1.FileOutbox(directory, redactor), directory);
37
- }
38
- finally {
39
- await (0, promises_1.rm)(directory, { recursive: true, force: true });
40
- }
41
- }
42
- (0, node_test_1.default)('durably records redacted events and collapses idempotent replays', async () => {
43
- await withOutbox(async (outbox, directory) => {
44
- const first = await outbox.enqueue(event());
45
- strict_1.default.equal(first.status, 'ENQUEUED');
46
- strict_1.default.deepEqual(first.record.payload, { note: 'token [REDACTED]' });
47
- strict_1.default.equal(first.record.redactionCount, 1);
48
- const replay = await outbox.enqueue(event());
49
- strict_1.default.equal(replay.status, 'DUPLICATE');
50
- strict_1.default.equal(replay.record.deliveryId, first.record.deliveryId);
51
- strict_1.default.equal((await outbox.nextBatch(10)).length, 1);
52
- const persisted = await (0, promises_1.readFile)((0, node_path_1.join)(directory, 'outbox-v1.json'), 'utf8');
53
- strict_1.default.match(persisted, /\[REDACTED\]/);
54
- strict_1.default.doesNotMatch(persisted, /sk-secret/);
55
- });
56
- });
57
- (0, node_test_1.default)('flags changed content from the same provider source event as a conflict', async () => {
58
- await withOutbox(async (outbox, directory) => {
59
- await outbox.enqueue(event());
60
- const conflicting = await outbox.enqueue(event({ payload: { note: 'different payload' } }));
61
- strict_1.default.equal(conflicting.status, 'CONFLICT');
62
- strict_1.default.equal((await outbox.list())[0]?.status, 'PENDING');
63
- });
64
- });
65
- (0, node_test_1.default)('persists repository capture facts with the delivery so replay cannot adopt a later checkout', async () => {
66
- await withOutbox(async (outbox, directory) => {
67
- const context = { repositories: [{ repositoryId: 'repo-at-capture', canonicalName: 'zewst/original', branch: 'release', headCommitSha: 'a'.repeat(40), isPrimary: true }] };
68
- const queued = await outbox.enqueue(event(), context);
69
- strict_1.default.deepEqual(queued.record.captureRepositories, context.repositories);
70
- // Same provider event observed under a different repository context is not
71
- // an idempotent replay: accepting it would silently rewrite attribution.
72
- const changed = await outbox.enqueue(event(), { repositories: [{ repositoryId: 'repo-later', canonicalName: 'zewst/other', isPrimary: true }] });
73
- strict_1.default.equal(changed.status, 'CONFLICT');
74
- const persisted = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, 'outbox-v1.json'), 'utf8'));
75
- strict_1.default.deepEqual(persisted.deliveries[queued.record.deliveryId].captureRepositories, context.repositories);
76
- });
77
- });
78
- (0, node_test_1.default)('persists delivery attempts, acknowledgements, and source checkpoints', async () => {
79
- await withOutbox(async (outbox) => {
80
- const queued = await outbox.enqueue(event());
81
- const retry = await outbox.markAttempt(queued.record.deliveryId, 'offline');
82
- strict_1.default.equal(retry.status, 'RETRY');
83
- strict_1.default.equal(retry.attempts, 1);
84
- strict_1.default.equal((await outbox.nextBatch(1))[0]?.deliveryId, queued.record.deliveryId);
85
- const acknowledged = await outbox.acknowledge(queued.record.deliveryId, 'receipt-1');
86
- strict_1.default.equal(acknowledged.status, 'ACKNOWLEDGED');
87
- strict_1.default.equal((await outbox.nextBatch(1)).length, 0);
88
- strict_1.default.equal((await outbox.acknowledge(queued.record.deliveryId, 'receipt-1')).status, 'ACKNOWLEDGED');
89
- await outbox.checkpoint('claude-code', '/repo/.claude/session.jsonl', 'byte:1024');
90
- strict_1.default.deepEqual(await outbox.getCheckpoint('claude-code', '/repo/.claude/session.jsonl'), {
91
- adapter: 'claude-code',
92
- scope: '/repo/.claude/session.jsonl',
93
- cursor: 'byte:1024',
94
- updatedAt: (await outbox.getCheckpoint('claude-code', '/repo/.claude/session.jsonl'))?.updatedAt,
95
- });
96
- });
97
- });
98
- (0, node_test_1.default)('recovers the local queue after a rejected caller operation', async () => {
99
- await withOutbox(async (outbox) => {
100
- await strict_1.default.rejects(outbox.acknowledge('missing-delivery', 'receipt-1'), /Unknown delivery/);
101
- const result = await outbox.enqueue(event());
102
- strict_1.default.equal(result.status, 'ENQUEUED');
103
- });
104
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,58 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strict_1 = __importDefault(require("node:assert/strict"));
7
- const promises_1 = require("node:fs/promises");
8
- const node_os_1 = require("node:os");
9
- const node_path_1 = require("node:path");
10
- const node_test_1 = __importDefault(require("node:test"));
11
- const fileOutbox_1 = require("../src/fileOutbox");
12
- const host_1 = require("../src/host");
13
- const redaction_1 = require("../src/redaction");
14
- async function withFixture(run) {
15
- const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'zeye-capture-host-'));
16
- try {
17
- await run(new fileOutbox_1.FileOutbox((0, node_path_1.join)(directory, 'outbox'), redaction_1.metadataRedactor), (0, node_path_1.join)(directory, 'codex.jsonl'));
18
- }
19
- finally {
20
- await (0, promises_1.rm)(directory, { recursive: true, force: true });
21
- }
22
- }
23
- (0, node_test_1.default)('Codex importer checkpoints completed JSONL lines and leaves a partial tail for later', async () => {
24
- await withFixture(async (outbox, file) => {
25
- const started = JSON.stringify({ type: 'session_meta', id: 'one', session_id: 'codex-1', timestamp: '2026-09-05T12:00:00.000Z', payload: { type: 'session_started' } });
26
- const ended = JSON.stringify({ type: 'event_msg', id: 'two', session_id: 'codex-1', timestamp: '2026-09-05T12:01:00.000Z', payload: { type: 'session_ended' } });
27
- await (0, promises_1.writeFile)(file, `${started}\n${ended.slice(0, 20)}`, 'utf8');
28
- strict_1.default.deepEqual(await (0, host_1.importCodexJsonl)(outbox, file), { imported: 1, skipped: 0, deferred: 1, rotated: false });
29
- strict_1.default.equal((await outbox.list()).length, 1);
30
- await (0, promises_1.writeFile)(file, `${started}\n${ended}\n`, 'utf8');
31
- strict_1.default.deepEqual(await (0, host_1.importCodexJsonl)(outbox, file), { imported: 1, skipped: 0, deferred: 0, rotated: false });
32
- strict_1.default.equal((await outbox.list()).length, 2);
33
- });
34
- });
35
- (0, node_test_1.default)('Codex importer detects a truncation and starts the new source file generation safely', async () => {
36
- await withFixture(async (outbox, file) => {
37
- const one = JSON.stringify({ type: 'session_meta', id: 'one', session_id: 'codex-1', timestamp: '2026-09-05T12:00:00.000Z', payload: { type: 'session_started' } });
38
- const two = JSON.stringify({ type: 'event_msg', id: 'two', session_id: 'codex-1', timestamp: '2026-09-05T12:01:00.000Z', payload: { type: 'session_ended' } });
39
- await (0, promises_1.writeFile)(file, `${one}\n${two}\n`, 'utf8');
40
- await (0, host_1.importCodexJsonl)(outbox, file);
41
- await (0, promises_1.writeFile)(file, `${two}\n`, 'utf8');
42
- strict_1.default.deepEqual(await (0, host_1.importCodexJsonl)(outbox, file), { imported: 1, skipped: 0, deferred: 0, rotated: true });
43
- });
44
- });
45
- (0, node_test_1.default)('Codex importer does not checkpoint across a malformed terminated line', async () => {
46
- await withFixture(async (outbox, file) => {
47
- const one = JSON.stringify({ type: 'session_meta', id: 'one', session_id: 'codex-1', timestamp: '2026-09-05T12:00:00.000Z', payload: { type: 'session_started' } });
48
- const two = JSON.stringify({ type: 'event_msg', id: 'two', session_id: 'codex-1', timestamp: '2026-09-05T12:01:00.000Z', payload: { type: 'session_ended' } });
49
- await (0, promises_1.writeFile)(file, `${one}\n{not-json}\n${two}\n`, 'utf8');
50
- strict_1.default.deepEqual(await (0, host_1.importCodexJsonl)(outbox, file), { imported: 1, skipped: 0, deferred: 2, rotated: false });
51
- strict_1.default.equal((await outbox.list()).length, 1);
52
- // Once the writer replaces the malformed line, the last durable line
53
- // still matches and the importer resumes from the correct checkpoint.
54
- await (0, promises_1.writeFile)(file, `${one}\n${two}\n`, 'utf8');
55
- strict_1.default.deepEqual(await (0, host_1.importCodexJsonl)(outbox, file), { imported: 1, skipped: 0, deferred: 0, rotated: false });
56
- strict_1.default.equal((await outbox.list()).length, 2);
57
- });
58
- });
@@ -1 +0,0 @@
1
- export {};