@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.
@@ -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.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.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 {};
@@ -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 {};