@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.
- package/README.md +55 -10
- package/dist/src/cli.js +52 -16
- package/dist/src/fileOutbox.d.ts +7 -0
- package/dist/src/fileOutbox.js +134 -5
- package/dist/src/host.js +47 -5
- package/dist/src/localHost.d.ts +1 -0
- package/dist/src/localHost.js +5 -0
- package/dist/src/providers.d.ts +12 -1
- package/dist/src/providers.js +62 -15
- package/dist/src/service.d.ts +28 -1
- package/dist/src/service.js +85 -6
- package/dist/src/transport.js +4 -1
- package/dist/src/types.d.ts +3 -0
- package/package.json +4 -3
- package/dist/test/fileOutbox.test.d.ts +0 -1
- package/dist/test/fileOutbox.test.js +0 -104
- package/dist/test/host.test.d.ts +0 -1
- package/dist/test/host.test.js +0 -58
- package/dist/test/localHost.test.d.ts +0 -1
- package/dist/test/localHost.test.js +0 -122
- package/dist/test/providers-and-transport.test.d.ts +0 -1
- package/dist/test/providers-and-transport.test.js +0 -222
- package/dist/test/transcript-and-service.test.d.ts +0 -1
- package/dist/test/transcript-and-service.test.js +0 -50
package/dist/src/providers.js
CHANGED
|
@@ -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: [
|
|
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
|
|
226
|
-
|
|
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)
|
|
248
|
-
string(payload.item_type)
|
|
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)
|
|
252
|
-
string(payload.item_type)
|
|
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 (
|
|
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
|
-
:
|
|
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:
|
|
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/dist/src/service.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/src/service.js
CHANGED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
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
|
|
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) {
|
package/dist/src/transport.js
CHANGED
|
@@ -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",
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
});
|
package/dist/test/host.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test/host.test.js
DELETED
|
@@ -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 {};
|