@zewstid/zeye-capture 0.2.0 → 0.3.0
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/localHost.d.ts +1 -0
- package/dist/src/localHost.js +5 -0
- 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/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.0",
|
|
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 {};
|
|
@@ -1,122 +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 node_child_process_1 = require("node:child_process");
|
|
8
|
-
const promises_1 = require("node:fs/promises");
|
|
9
|
-
const node_os_1 = require("node:os");
|
|
10
|
-
const node_path_1 = require("node:path");
|
|
11
|
-
const node_test_1 = __importDefault(require("node:test"));
|
|
12
|
-
const localHost_1 = require("../src/localHost");
|
|
13
|
-
function git(directory, args) {
|
|
14
|
-
return (0, node_child_process_1.execFileSync)('git', ['-C', directory, ...args], { encoding: 'utf8' }).trim();
|
|
15
|
-
}
|
|
16
|
-
async function withRepository(run) {
|
|
17
|
-
const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'zeye-capture-local-host-'));
|
|
18
|
-
const configPath = (0, node_path_1.join)(directory, 'private-config', 'host.json');
|
|
19
|
-
try {
|
|
20
|
-
git(directory, ['init', '--initial-branch=main']);
|
|
21
|
-
git(directory, ['config', 'user.email', 'capture@example.test']);
|
|
22
|
-
git(directory, ['config', 'user.name', 'Capture Test']);
|
|
23
|
-
await (0, promises_1.writeFile)((0, node_path_1.join)(directory, 'README.md'), 'fixture\n');
|
|
24
|
-
git(directory, ['add', 'README.md']);
|
|
25
|
-
git(directory, ['commit', '-m', 'fixture']);
|
|
26
|
-
git(directory, ['remote', 'add', 'origin', 'git@github.com:zewst/zeye.git']);
|
|
27
|
-
await run(directory, configPath);
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
await (0, promises_1.rm)(directory, { recursive: true, force: true });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
(0, node_test_1.default)('installs and removes only Zeye-owned Claude hook groups in private local settings', async () => {
|
|
34
|
-
await withRepository(async (repositoryRoot, configPath) => {
|
|
35
|
-
const settingsPath = (0, node_path_1.join)(repositoryRoot, '.claude', 'settings.local.json');
|
|
36
|
-
await (0, promises_1.mkdir)((0, node_path_1.join)(repositoryRoot, '.claude'), { recursive: true });
|
|
37
|
-
await (0, promises_1.writeFile)(settingsPath, JSON.stringify({ hooks: { SessionStart: [{ matcher: 'existing', hooks: [{ type: 'command', command: 'existing-hook' }] }] } }));
|
|
38
|
-
const installed = await (0, localHost_1.installClaudeHooks)({ repositoryRoot, settingsPath, configPath });
|
|
39
|
-
strict_1.default.equal(installed.installedEvents.length, 9);
|
|
40
|
-
const afterInstall = JSON.parse(await (0, promises_1.readFile)(settingsPath, 'utf8'));
|
|
41
|
-
strict_1.default.equal(afterInstall.hooks.SessionStart.length, 2);
|
|
42
|
-
strict_1.default.equal(afterInstall.hooks.PreCompact[0].hooks[0].command, 'zeye-capture claude-hook');
|
|
43
|
-
if (process.platform !== 'win32')
|
|
44
|
-
strict_1.default.equal((await (0, promises_1.stat)(settingsPath)).mode & 0o777, 0o600);
|
|
45
|
-
const hostConfig = await (0, localHost_1.readLocalHostConfig)(configPath);
|
|
46
|
-
strict_1.default.equal(hostConfig.claudeInstalls.length, 1);
|
|
47
|
-
const removed = await (0, localHost_1.uninstallClaudeHooks)({ repositoryRoot, settingsPath, configPath });
|
|
48
|
-
strict_1.default.equal(removed.removedEvents.length, 9);
|
|
49
|
-
const afterRemove = JSON.parse(await (0, promises_1.readFile)(settingsPath, 'utf8'));
|
|
50
|
-
strict_1.default.deepEqual(afterRemove.hooks.SessionStart, [{ matcher: 'existing', hooks: [{ type: 'command', command: 'existing-hook' }] }]);
|
|
51
|
-
strict_1.default.equal(afterRemove.hooks.PreCompact, undefined);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
(0, node_test_1.default)('stores Codex tail sources in an OS-private local host config and removes them by repository', async () => {
|
|
55
|
-
await withRepository(async (repositoryRoot, configPath) => {
|
|
56
|
-
const jsonlPath = (0, node_path_1.join)(repositoryRoot, 'sessions', 'rollout.jsonl');
|
|
57
|
-
await (0, localHost_1.installCodexSource)({ repositoryRoot, jsonlPath, configPath });
|
|
58
|
-
strict_1.default.deepEqual((await (0, localHost_1.readLocalHostConfig)(configPath)).codexSources, [{ repositoryRoot, jsonlPath }]);
|
|
59
|
-
if (process.platform !== 'win32')
|
|
60
|
-
strict_1.default.equal((await (0, promises_1.stat)(configPath)).mode & 0o777, 0o600);
|
|
61
|
-
strict_1.default.equal(await (0, localHost_1.uninstallCodexSource)({ repositoryRoot, jsonlPath, configPath }), 1);
|
|
62
|
-
strict_1.default.equal((await (0, localHost_1.readLocalHostConfig)(configPath)).codexSources.length, 0);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
(0, node_test_1.default)('discovers safe Git repository identity without retaining the raw remote URL', async () => {
|
|
66
|
-
await withRepository(async (repositoryRoot) => {
|
|
67
|
-
const identity = await (0, localHost_1.discoverRepositoryIdentity)(repositoryRoot, { providerRepositoryId: '123' });
|
|
68
|
-
strict_1.default.ok(identity);
|
|
69
|
-
strict_1.default.equal(identity.fullName, 'zewst/zeye');
|
|
70
|
-
strict_1.default.equal(identity.branch, 'main');
|
|
71
|
-
strict_1.default.match(identity.headCommitSha || '', /^[a-f0-9]{40}$/);
|
|
72
|
-
strict_1.default.match(identity.remoteUrlHash || '', /^[a-f0-9]{64}$/);
|
|
73
|
-
strict_1.default.equal(JSON.stringify(identity).includes('git@github.com'), false);
|
|
74
|
-
strict_1.default.deepEqual((0, localHost_1.captureRepositoryReference)(identity, 'repo-internal'), [{ repositoryId: 'repo-internal', providerRepositoryId: '123', canonicalName: 'zewst/zeye', remoteUrlHash: identity.remoteUrlHash, branch: 'main', headCommitSha: identity.headCommitSha, isPrimary: true }]);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
(0, node_test_1.default)('enforces a single capture process per private outbox directory', async () => {
|
|
78
|
-
const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'zeye-capture-lock-'));
|
|
79
|
-
try {
|
|
80
|
-
const first = await (0, localHost_1.acquireCaptureLock)(directory);
|
|
81
|
-
await strict_1.default.rejects((0, localHost_1.acquireCaptureLock)(directory, { maxWaitMs: 0 }), /Capture already running/);
|
|
82
|
-
await first.release();
|
|
83
|
-
const second = await (0, localHost_1.acquireCaptureLock)(directory);
|
|
84
|
-
await second.release();
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
await (0, promises_1.rm)(directory, { recursive: true, force: true });
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
(0, node_test_1.default)('waits briefly for a concurrent capture lock instead of dropping a normal hook event', async () => {
|
|
91
|
-
const directory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'zeye-capture-lock-wait-'));
|
|
92
|
-
try {
|
|
93
|
-
const first = await (0, localHost_1.acquireCaptureLock)(directory);
|
|
94
|
-
const secondPromise = (0, localHost_1.acquireCaptureLock)(directory, { maxWaitMs: 500, initialRetryDelayMs: 5, maxRetryDelayMs: 10 });
|
|
95
|
-
setTimeout(() => { void first.release(); }, 25);
|
|
96
|
-
const second = await secondPromise;
|
|
97
|
-
await second.release();
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
await (0, promises_1.rm)(directory, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
(0, node_test_1.default)('scopes an explicit outbox override to the repository rather than sharing records across checkouts', () => {
|
|
104
|
-
const environment = { ZEYE_CAPTURE_OUTBOX_DIR: '/private/zeye-outbox' };
|
|
105
|
-
strict_1.default.notEqual((0, localHost_1.localOutboxDirectory)('/work/repository-a', environment), (0, localHost_1.localOutboxDirectory)('/work/repository-b', environment));
|
|
106
|
-
strict_1.default.match((0, localHost_1.localOutboxDirectory)('/work/repository-a', environment), /^\/private\/zeye-outbox\//);
|
|
107
|
-
});
|
|
108
|
-
(0, node_test_1.default)('sync reports a non-zero exit when a configured Codex source cannot be imported', async () => {
|
|
109
|
-
await withRepository(async (repositoryRoot) => {
|
|
110
|
-
const configHome = (0, node_path_1.join)(repositoryRoot, 'capture-config-home');
|
|
111
|
-
const missingJsonl = (0, node_path_1.join)(repositoryRoot, 'missing', 'rollout.jsonl');
|
|
112
|
-
const cli = (0, node_path_1.join)(process.cwd(), 'dist', 'src', 'cli.js');
|
|
113
|
-
const environment = { ...process.env, XDG_CONFIG_HOME: configHome, XDG_STATE_HOME: (0, node_path_1.join)(repositoryRoot, 'capture-state-home') };
|
|
114
|
-
for (const key of ['ZEYE_CAPTURE_ENDPOINT', 'ZEYE_CAPTURE_TOKEN', 'ZEYE_CAPTURE_ORGANIZATION_ID', 'ZEYE_CAPTURE_INSTALLATION_ID', 'ZEYE_CAPTURE_ACTOR_JSON', 'ZEYE_CAPTURE_REPOSITORY_ID', 'ZEYE_CAPTURE_PROVIDER_REPOSITORY_ID'])
|
|
115
|
-
delete environment[key];
|
|
116
|
-
const install = (0, node_child_process_1.spawnSync)(process.execPath, [cli, 'install', 'codex', '--jsonl', missingJsonl, '--repo', repositoryRoot], { encoding: 'utf8', env: environment });
|
|
117
|
-
strict_1.default.equal(install.status, 0, install.stderr);
|
|
118
|
-
const sync = (0, node_child_process_1.spawnSync)(process.execPath, [cli, 'sync', '--repo', repositoryRoot], { encoding: 'utf8', env: environment });
|
|
119
|
-
strict_1.default.equal(sync.status, 1, sync.stderr);
|
|
120
|
-
strict_1.default.match(sync.stdout, /configured JSONL path/);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|