@zewstid/zeye-capture 0.2.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.
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromClaudeTranscriptJsonl = fromClaudeTranscriptJsonl;
4
+ exports.fromClaudeCodeHook = fromClaudeCodeHook;
5
+ exports.fromCodexJsonl = fromCodexJsonl;
6
+ exports.fromVSCodeCopilotEvent = fromVSCodeCopilotEvent;
7
+ const node_crypto_1 = require("node:crypto");
8
+ function asRecord(value) {
9
+ return value && typeof value === "object" && !Array.isArray(value)
10
+ ? value
11
+ : {};
12
+ }
13
+ function string(value) {
14
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
15
+ }
16
+ function timestamp(value) {
17
+ const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
18
+ return Number.isNaN(parsed)
19
+ ? new Date().toISOString()
20
+ : new Date(parsed).toISOString();
21
+ }
22
+ function opaqueId(value, fallback) {
23
+ const candidate = string(value);
24
+ return (candidate ||
25
+ (0, node_crypto_1.createHash)("sha256").update(fallback).digest("hex").slice(0, 32));
26
+ }
27
+ const relationships = new Set([
28
+ "ROOT",
29
+ "RESUMED_FROM",
30
+ "FORKED_FROM",
31
+ "SUBAGENT_OF",
32
+ "CONTINUATION_OF",
33
+ ]);
34
+ function relationship(value, inferred, parent) {
35
+ const explicit = string(value)?.toUpperCase();
36
+ const candidate = explicit && relationships.has(explicit)
37
+ ? explicit
38
+ : inferred;
39
+ // The capture contract correctly rejects a non-root lineage without its
40
+ // parent. Do not publish a false relationship merely because an adapter saw
41
+ // a lifecycle label; retain ROOT until the provider supplies the parent.
42
+ return candidate !== "ROOT" && !parent ? "ROOT" : candidate;
43
+ }
44
+ function generation(record, payload) {
45
+ return (string(record.source_generation_id) ||
46
+ string(record.sourceGenerationId) ||
47
+ string(record.session_generation_id) ||
48
+ string(record.sessionGenerationId) ||
49
+ string(payload.source_generation_id) ||
50
+ string(payload.sourceGenerationId) ||
51
+ string(payload.session_generation_id) ||
52
+ string(payload.sessionGenerationId));
53
+ }
54
+ function usagePayload(value) {
55
+ const usage = {};
56
+ const aliases = {
57
+ inputTokens: ["inputTokens", "input_tokens"],
58
+ outputTokens: ["outputTokens", "output_tokens"],
59
+ cachedInputTokens: ["cachedInputTokens", "cached_input_tokens"],
60
+ reasoningTokens: ["reasoningTokens", "reasoning_tokens"],
61
+ costMicros: ["costMicros", "cost_micros"],
62
+ };
63
+ for (const [target, keys] of Object.entries(aliases)) {
64
+ const number = keys
65
+ .map((key) => value[key])
66
+ .find((candidate) => typeof candidate === "number");
67
+ if (typeof number === "number" &&
68
+ Number.isSafeInteger(number) &&
69
+ number >= 0)
70
+ usage[target] = number;
71
+ }
72
+ const model = string(value.model);
73
+ if (model && /^[a-zA-Z0-9._:/-]{1,100}$/.test(model))
74
+ usage.model = model;
75
+ return usage;
76
+ }
77
+ function textParts(value) {
78
+ if (typeof value === "string")
79
+ return value.trim() || undefined;
80
+ if (!Array.isArray(value))
81
+ return undefined;
82
+ const parts = value.flatMap((item) => {
83
+ const record = asRecord(item);
84
+ const text = string(record.text) || string(record.content) || string(record.output);
85
+ return text ? [text] : [];
86
+ });
87
+ return parts.length ? parts.join("\n") : undefined;
88
+ }
89
+ function transcript(kind, text, input) {
90
+ return text
91
+ ? {
92
+ kind,
93
+ text,
94
+ ...(string(input.id) ? { providerMessageId: string(input.id) } : {}),
95
+ ...(input.toolName ? { toolName: input.toolName } : {}),
96
+ }
97
+ : undefined;
98
+ }
99
+ function claudeVisibleContent(value) {
100
+ if (typeof value === "string")
101
+ return value.trim() || undefined;
102
+ if (!Array.isArray(value))
103
+ return undefined;
104
+ const visible = value.flatMap((item) => {
105
+ const block = asRecord(item);
106
+ const type = string(block.type);
107
+ if (type === "text")
108
+ return string(block.text) ? [string(block.text)] : [];
109
+ if (type === "tool_use")
110
+ return [`Tool ${string(block.name) || "unknown"} input:\n${JSON.stringify(block.input ?? {})}`];
111
+ if (type === "tool_result")
112
+ return [`Tool result:\n${textParts(block.content) || string(block.content) || ""}`];
113
+ // Thinking/signature and provider-private blocks are deliberately excluded.
114
+ return [];
115
+ });
116
+ return visible.length ? visible.join("\n\n") : undefined;
117
+ }
118
+ /** Parse provider-visible Claude transcript JSONL without hidden thinking blocks. */
119
+ function fromClaudeTranscriptJsonl(input, fallbackSessionId) {
120
+ const record = asRecord(input);
121
+ const message = asRecord(record.message);
122
+ const role = string(message.role) || string(record.type);
123
+ const sessionId = string(record.sessionId) || string(record.session_id) || fallbackSessionId;
124
+ if (!sessionId ||
125
+ (role !== "user" && role !== "assistant" && role !== "system"))
126
+ return null;
127
+ const visibleText = claudeVisibleContent(message.content ?? record.content);
128
+ if (!visibleText)
129
+ return null;
130
+ const id = record.uuid || message.id || record.id;
131
+ return {
132
+ adapter: "claude-code",
133
+ provider: "claude_code",
134
+ clientName: "Claude Code",
135
+ sessionId,
136
+ sourceEventId: opaqueId(id, `${sessionId}:${record.timestamp || ""}:${role}:${visibleText}`),
137
+ occurredAt: timestamp(record.timestamp),
138
+ type: "message.recorded",
139
+ payload: { transcriptRecord: true, role },
140
+ transcript: transcript(role === "assistant"
141
+ ? "ASSISTANT_MESSAGE"
142
+ : role === "system"
143
+ ? "SYSTEM_MESSAGE"
144
+ : "USER_MESSAGE", visibleText, { id }),
145
+ };
146
+ }
147
+ /** Parse a Claude Code hook payload without retaining prompt/tool/file content. */
148
+ function fromClaudeCodeHook(input, captureContent = false) {
149
+ const record = asRecord(input);
150
+ const hook = string(record.hook_event_name) || string(record.hookEventName);
151
+ const sessionId = string(record.session_id) || string(record.sessionId);
152
+ if (!hook || !sessionId)
153
+ return null;
154
+ const eventType = {
155
+ SessionStart: "session.started",
156
+ SessionEnd: "session.ended",
157
+ UserPromptSubmit: "message.recorded",
158
+ PreToolUse: "tool.started",
159
+ PostToolUse: "tool.completed",
160
+ PostToolUseFailure: "tool.completed",
161
+ SubagentStart: "subagent.started",
162
+ SubagentStop: "subagent.ended",
163
+ PreCompact: "session.compacted",
164
+ };
165
+ const type = eventType[hook] || "capture.warning";
166
+ const toolName = string(record.tool_name) || string(record.toolName);
167
+ const path = string(record.cwd);
168
+ const parent = string(record.parent_session_id) || string(record.parentSessionId);
169
+ const rawToolInput = record.tool_input ?? record.toolInput;
170
+ const visibleText = hook === "UserPromptSubmit"
171
+ ? string(record.prompt)
172
+ : hook === "PreToolUse" && rawToolInput !== undefined
173
+ ? typeof rawToolInput === "string"
174
+ ? rawToolInput
175
+ : JSON.stringify(rawToolInput)
176
+ : hook === "PostToolUse" || hook === "PostToolUseFailure"
177
+ ? textParts(record.tool_response ?? record.toolResponse)
178
+ : undefined;
179
+ const visibleKind = hook === "UserPromptSubmit"
180
+ ? "USER_MESSAGE"
181
+ : hook === "PreToolUse"
182
+ ? "TOOL_INPUT"
183
+ : hook === "PostToolUse" || hook === "PostToolUseFailure"
184
+ ? "TOOL_OUTPUT"
185
+ : undefined;
186
+ return {
187
+ adapter: "claude-code",
188
+ provider: "claude_code",
189
+ clientName: "Claude Code",
190
+ clientVersion: string(record.client_version) || string(record.clientVersion),
191
+ sessionId,
192
+ sourceEventId: opaqueId(record.event_id, `${sessionId}:${hook}:${record.timestamp || ""}`),
193
+ occurredAt: timestamp(record.timestamp),
194
+ type,
195
+ payload: {
196
+ hook,
197
+ ...(toolName ? { toolName } : {}),
198
+ // CWD presence is useful coverage metadata; do not retain the path.
199
+ workingDirectoryPresent: Boolean(path),
200
+ toolFailure: hook === "PostToolUseFailure",
201
+ },
202
+ sourceGenerationId: generation(record, {}),
203
+ relationship: relationship(record.relationship || record.session_relationship, hook === "SubagentStart" || hook === "SubagentStop"
204
+ ? "SUBAGENT_OF"
205
+ : "ROOT", parent),
206
+ relatedSourceSessionId: parent,
207
+ ...(captureContent && visibleKind && visibleText
208
+ ? {
209
+ transcript: transcript(visibleKind, visibleText, {
210
+ id: record.event_id,
211
+ toolName,
212
+ }),
213
+ }
214
+ : {}),
215
+ };
216
+ }
217
+ /**
218
+ * Parse one JSONL record from Codex local session/rollout logs. The Codex log
219
+ * format evolves, so unknown record kinds become a safe capture warning rather
220
+ * than being misrepresented as a complete observation.
221
+ */
222
+ function fromCodexJsonl(input, sourceFile = "session.jsonl", captureContent = false) {
223
+ const record = asRecord(input);
224
+ const payload = asRecord(record.payload);
225
+ const sourceSessionId = string(record.session_id) ||
226
+ string(record.sessionId) ||
227
+ string(payload.session_id) ||
228
+ string(payload.sessionId);
229
+ if (!sourceSessionId)
230
+ return null;
231
+ const kind = string(record.type) || string(record.kind) || "unknown";
232
+ const eventName = string(payload.type) || string(payload.event) || kind;
233
+ let type = "capture.warning";
234
+ if (kind === "session_meta" ||
235
+ eventName === "session_started" ||
236
+ eventName === "task_started")
237
+ type = "session.started";
238
+ else if (eventName === "session_resumed")
239
+ type = "session.resumed";
240
+ else if (eventName === "session_forked")
241
+ type = "session.forked";
242
+ else if (eventName === "context_compacted" || eventName === "compaction")
243
+ type = "session.compacted";
244
+ else if (eventName === "task_complete" || eventName === "session_ended")
245
+ type = "session.ended";
246
+ else if (kind === "response_item" &&
247
+ (string(payload.type) === "function_call" ||
248
+ string(payload.item_type) === "function_call"))
249
+ type = "tool.started";
250
+ else if (kind === "response_item" &&
251
+ (string(payload.type) === "function_call_output" ||
252
+ string(payload.item_type) === "function_call_output"))
253
+ type = "tool.completed";
254
+ else if (eventName === "subagent_started")
255
+ type = "subagent.started";
256
+ else if (eventName === "subagent_ended")
257
+ type = "subagent.ended";
258
+ else if (kind === "response_item" && string(payload.type) === "message")
259
+ type = "message.recorded";
260
+ else if (eventName === "user_message" || eventName === "agent_message")
261
+ type = "message.recorded";
262
+ else if (eventName === "usage" || eventName === "usage_recorded")
263
+ type = "usage.recorded";
264
+ const lineId = string(record.id) ||
265
+ string(payload.id) ||
266
+ `${sourceFile}:${kind}:${record.timestamp || ""}:${eventName}`;
267
+ const parent = string(payload.parent_session_id) ||
268
+ string(payload.parentSessionId) ||
269
+ string(record.parent_session_id) ||
270
+ string(record.parentSessionId);
271
+ const role = string(payload.role);
272
+ const messageText = textParts(payload.content) ||
273
+ string(payload.message) ||
274
+ string(payload.text);
275
+ const toolName = string(payload.name) || string(payload.tool_name);
276
+ const toolInput = string(payload.arguments) ||
277
+ (payload.arguments && typeof payload.arguments === "object"
278
+ ? JSON.stringify(payload.arguments)
279
+ : undefined);
280
+ const toolOutput = textParts(payload.output) || string(payload.result);
281
+ const visible = type === "message.recorded"
282
+ ? transcript(role === "assistant" || eventName === "agent_message"
283
+ ? "ASSISTANT_MESSAGE"
284
+ : role === "system"
285
+ ? "SYSTEM_MESSAGE"
286
+ : "USER_MESSAGE", messageText, { id: payload.id || record.id })
287
+ : type === "tool.started"
288
+ ? transcript("TOOL_INPUT", toolInput, {
289
+ id: payload.call_id || payload.id,
290
+ toolName,
291
+ })
292
+ : type === "tool.completed"
293
+ ? transcript("TOOL_OUTPUT", toolOutput, {
294
+ id: payload.call_id || payload.id,
295
+ toolName,
296
+ })
297
+ : undefined;
298
+ return {
299
+ adapter: "codex",
300
+ provider: "codex",
301
+ clientName: "Codex",
302
+ clientVersion: string(record.client_version) || string(record.clientVersion),
303
+ sessionId: sourceSessionId,
304
+ sourceEventId: opaqueId(lineId, sourceSessionId),
305
+ occurredAt: timestamp(record.timestamp || payload.timestamp),
306
+ type,
307
+ payload: {
308
+ recordKind: kind,
309
+ eventName,
310
+ sourceFilePresent: Boolean(sourceFile),
311
+ ...usagePayload(payload),
312
+ },
313
+ sourceGenerationId: generation(record, payload),
314
+ relationship: relationship(payload.relationship ||
315
+ payload.session_relationship ||
316
+ record.relationship ||
317
+ record.session_relationship, eventName === "subagent_started" || eventName === "subagent_ended"
318
+ ? "SUBAGENT_OF"
319
+ : eventName === "session_resumed"
320
+ ? "RESUMED_FROM"
321
+ : eventName === "session_forked"
322
+ ? "FORKED_FROM"
323
+ : "ROOT", parent),
324
+ relatedSourceSessionId: parent,
325
+ ...(captureContent && visible ? { transcript: visible } : {}),
326
+ };
327
+ }
328
+ /**
329
+ * Convert a deliberately small event emitted by a VS Code/Copilot companion
330
+ * extension. This is not a transcript importer: workspace paths, chat text,
331
+ * tool arguments, file bodies, and authentication state are all excluded at
332
+ * the extension boundary and again by the outbox redactor.
333
+ */
334
+ function fromVSCodeCopilotEvent(input) {
335
+ const record = asRecord(input);
336
+ const sessionId = string(record.sessionId) || string(record.session_id);
337
+ const eventName = string(record.event) || string(record.type) || string(record.kind);
338
+ if (!sessionId || !eventName)
339
+ return null;
340
+ const eventType = {
341
+ "session.started": "session.started",
342
+ "session.resumed": "session.resumed",
343
+ "session.ended": "session.ended",
344
+ "session.compacted": "session.compacted",
345
+ "chat.request.started": "message.recorded",
346
+ "chat.request.completed": "message.recorded",
347
+ "tool.started": "tool.started",
348
+ "tool.completed": "tool.completed",
349
+ "file.changed": "file.changed",
350
+ "git.observed": "git.observed",
351
+ "usage.recorded": "usage.recorded",
352
+ "subagent.started": "subagent.started",
353
+ "subagent.ended": "subagent.ended",
354
+ };
355
+ const type = eventType[eventName] || "capture.warning";
356
+ const toolName = string(record.toolName) || string(record.tool_name);
357
+ const parent = string(record.parentSessionId) || string(record.parent_session_id);
358
+ return {
359
+ adapter: "vscode-copilot",
360
+ provider: "github_copilot",
361
+ clientName: "VS Code / GitHub Copilot",
362
+ clientVersion: string(record.extensionVersion) || string(record.extension_version),
363
+ sessionId,
364
+ sourceEventId: opaqueId(record.eventId || record.event_id, `${sessionId}:${eventName}:${record.timestamp || ""}`),
365
+ occurredAt: timestamp(record.timestamp || record.occurredAt),
366
+ type,
367
+ payload: {
368
+ eventName,
369
+ ...(toolName ? { toolName } : {}),
370
+ workspacePresent: Boolean(string(record.workspaceUri) || string(record.workspace_uri)),
371
+ ...usagePayload(record),
372
+ },
373
+ sourceGenerationId: generation(record, {}),
374
+ relationship: relationship(record.relationship || record.sessionRelationship, eventName === "subagent.started" || eventName === "subagent.ended"
375
+ ? "SUBAGENT_OF"
376
+ : eventName === "session.resumed"
377
+ ? "RESUMED_FROM"
378
+ : "ROOT", parent),
379
+ relatedSourceSessionId: parent,
380
+ };
381
+ }
@@ -0,0 +1,7 @@
1
+ import type { RedactionBoundary } from './types';
2
+ export declare const metadataRedactor: RedactionBoundary;
3
+ export declare function redactTranscriptText(text: string): {
4
+ text: string;
5
+ redactionCount: number;
6
+ };
7
+ export declare const consentedTranscriptRedactor: RedactionBoundary;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.consentedTranscriptRedactor = exports.metadataRedactor = void 0;
4
+ exports.redactTranscriptText = redactTranscriptText;
5
+ // A deny-first metadata boundary. Provider adapters should not emit content in
6
+ // the first place; this boundary protects the durable queue if a future adapter
7
+ // accidentally includes a conventional content field.
8
+ const BLOCKED_KEY = /(?:prompt|message|content|transcript|command|argument|environment|secret|token|password|authorization|cookie)/i;
9
+ const SAFE_USAGE_NUMBER_KEY = /^(?:inputTokens|outputTokens|cachedInputTokens|reasoningTokens|costMicros)$/;
10
+ function redact(value, key) {
11
+ if (key && SAFE_USAGE_NUMBER_KEY.test(key) && typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
12
+ return { value, count: 0 };
13
+ if (key && BLOCKED_KEY.test(key))
14
+ return { value: '[REDACTED]', count: 1 };
15
+ if (value === null || typeof value === 'boolean' || typeof value === 'number')
16
+ return { value, count: 0 };
17
+ if (typeof value === 'string')
18
+ return { value: value.slice(0, 500), count: value.length > 500 ? 1 : 0 };
19
+ if (Array.isArray(value)) {
20
+ return value.reduce((result, item) => {
21
+ const nested = redact(item);
22
+ result.value.push(nested.value);
23
+ result.count += nested.count;
24
+ return result;
25
+ }, { value: [], count: 0 });
26
+ }
27
+ if (value && typeof value === 'object') {
28
+ return Object.entries(value).reduce((result, [nestedKey, nestedValue]) => {
29
+ const nested = redact(nestedValue, nestedKey);
30
+ result.value[nestedKey] = nested.value;
31
+ result.count += nested.count;
32
+ return result;
33
+ }, { value: {}, count: 0 });
34
+ }
35
+ return { value: '[REDACTED]', count: 1 };
36
+ }
37
+ exports.metadataRedactor = {
38
+ redact(event) {
39
+ const result = redact(event.payload);
40
+ return {
41
+ payload: result.value,
42
+ redactionCount: result.count,
43
+ redactionReasons: result.count ? ['content_or_sensitive_field'] : [],
44
+ };
45
+ },
46
+ };
47
+ const TRANSCRIPT_REDACTION_POLICY_VERSION = 'zeye-secrets-v1';
48
+ const SECRET_PATTERNS = [
49
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]'],
50
+ [/\b(?:sk(?:[-_]proj)?[-_]|zmp_|zem_|ghp_|github_pat_)[A-Za-z0-9_-]{16,}\b/g, '[REDACTED_TOKEN]'],
51
+ [/\b(?:authorization|api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]{6,}/gi, '$1=[REDACTED]'],
52
+ [/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, '[REDACTED_HIGH_ENTROPY_VALUE]'],
53
+ ];
54
+ function redactTranscriptText(text) {
55
+ let output = text;
56
+ let redactionCount = 0;
57
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
58
+ output = output.replace(pattern, match => { redactionCount += 1; return replacement.replace('$1', match.split(/[:=]/, 1)[0] || 'secret'); });
59
+ }
60
+ return { text: output.slice(0, 262_144), redactionCount: redactionCount + (output.length > 262_144 ? 1 : 0) };
61
+ }
62
+ exports.consentedTranscriptRedactor = {
63
+ redact(event) {
64
+ const metadata = exports.metadataRedactor.redact(event);
65
+ if (!event.transcript)
66
+ return metadata;
67
+ const redacted = redactTranscriptText(event.transcript.text);
68
+ return {
69
+ ...metadata,
70
+ transcript: {
71
+ ...event.transcript,
72
+ text: redacted.text,
73
+ redactionCount: redacted.redactionCount,
74
+ redactionPolicyVersion: TRANSCRIPT_REDACTION_POLICY_VERSION,
75
+ },
76
+ redactionCount: metadata.redactionCount + redacted.redactionCount,
77
+ redactionReasons: [...(metadata.redactionReasons || []), ...(redacted.redactionCount ? ['transcript_secret'] : [])],
78
+ };
79
+ },
80
+ };
@@ -0,0 +1,28 @@
1
+ export type ServiceEnrollment = {
2
+ endpoint: string;
3
+ token: string;
4
+ organizationId: string;
5
+ captureInstallationId: string;
6
+ actorJson: string;
7
+ contentConsentId?: string;
8
+ transcriptConsent?: 'yes';
9
+ intervalSeconds: number;
10
+ };
11
+ export type ServiceFiles = {
12
+ serviceId: string;
13
+ enrollmentPath: string;
14
+ definitionPath: string;
15
+ kind: 'launchd' | 'systemd' | 'windows-task';
16
+ };
17
+ export declare function serviceFiles(repositoryRoot: string, environment?: NodeJS.ProcessEnv, os?: NodeJS.Platform): ServiceFiles;
18
+ export declare function serviceDefinition(files: ServiceFiles, repositoryRoot: string, nodePath: string, cliPath: string): string;
19
+ export declare function installCaptureService(input: {
20
+ repositoryRoot: string;
21
+ enrollment: ServiceEnrollment;
22
+ cliPath: string;
23
+ nodePath?: string;
24
+ os?: NodeJS.Platform;
25
+ }): Promise<ServiceFiles>;
26
+ export declare function uninstallCaptureService(repositoryRoot: string, os?: NodeJS.Platform): Promise<ServiceFiles>;
27
+ export declare function readServiceEnrollment(path: string): Promise<ServiceEnrollment>;
28
+ export declare function applyServiceEnrollment(enrollment: ServiceEnrollment): void;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serviceFiles = serviceFiles;
4
+ exports.serviceDefinition = serviceDefinition;
5
+ exports.installCaptureService = installCaptureService;
6
+ exports.uninstallCaptureService = uninstallCaptureService;
7
+ exports.readServiceEnrollment = readServiceEnrollment;
8
+ exports.applyServiceEnrollment = applyServiceEnrollment;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_os_1 = require("node:os");
12
+ const node_path_1 = require("node:path");
13
+ const node_child_process_1 = require("node:child_process");
14
+ function idFor(repositoryRoot) { return (0, node_crypto_1.createHash)('sha256').update(repositoryRoot).digest('hex').slice(0, 16); }
15
+ function xml(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
16
+ 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)}`;
19
+ const configHome = environment.XDG_CONFIG_HOME || (os === 'win32' ? environment.APPDATA : undefined) || (0, node_path_1.join)((0, node_os_1.homedir)(), '.config');
20
+ const enrollmentPath = (0, node_path_1.join)(configHome, 'zeye-capture', 'services', `${serviceId}.json`);
21
+ if (os === 'darwin')
22
+ return { serviceId, enrollmentPath, definitionPath: (0, node_path_1.join)((0, node_os_1.homedir)(), 'Library', 'LaunchAgents', `${serviceId}.plist`), kind: 'launchd' };
23
+ if (os === 'win32')
24
+ return { serviceId, enrollmentPath, definitionPath: (0, node_path_1.join)(configHome, 'zeye-capture', 'services', `${serviceId}.cmd`), kind: 'windows-task' };
25
+ return { serviceId, enrollmentPath, definitionPath: (0, node_path_1.join)(configHome, 'systemd', 'user', `${serviceId}.service`), kind: 'systemd' };
26
+ }
27
+ function serviceDefinition(files, repositoryRoot, nodePath, cliPath) {
28
+ if (files.kind === 'launchd')
29
+ return `<?xml version="1.0" encoding="UTF-8"?>
30
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
+ <plist version="1.0"><dict>
32
+ <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>
34
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ProcessType</key><string>Background</string>
35
+ </dict></plist>
36
+ `;
37
+ if (files.kind === 'systemd')
38
+ return `[Unit]
39
+ Description=Zeye Engineering Memory continuous capture for ${repositoryRoot.replace(/\n/g, ' ')}
40
+ After=network-online.target
41
+
42
+ [Service]
43
+ Type=simple
44
+ ExecStart=${systemd(nodePath)} ${systemd(cliPath)} service-run --repo ${systemd(repositoryRoot)} --config ${systemd(files.enrollmentPath)}
45
+ Restart=always
46
+ RestartSec=15
47
+ NoNewPrivileges=true
48
+ PrivateTmp=true
49
+
50
+ [Install]
51
+ WantedBy=default.target
52
+ `;
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`;
54
+ }
55
+ async function run(command, args) {
56
+ await new Promise((resolve, reject) => {
57
+ const child = (0, node_child_process_1.spawn)(command, args, { stdio: 'ignore', shell: false });
58
+ child.once('error', reject);
59
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error(`${command} exited with status ${code}`)));
60
+ });
61
+ }
62
+ async function installCaptureService(input) {
63
+ const os = input.os || (0, node_os_1.platform)();
64
+ const files = serviceFiles(input.repositoryRoot, process.env, os);
65
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(files.enrollmentPath), { recursive: true, mode: 0o700 });
66
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(files.definitionPath), { recursive: true, mode: 0o700 });
67
+ await (0, promises_1.writeFile)(files.enrollmentPath, `${JSON.stringify(input.enrollment)}\n`, { encoding: 'utf8', mode: 0o600 });
68
+ await (0, promises_1.writeFile)(files.definitionPath, serviceDefinition(files, input.repositoryRoot, input.nodePath || process.execPath, input.cliPath), { encoding: 'utf8', mode: 0o600 });
69
+ if (os === 'darwin') {
70
+ const domain = `gui/${process.getuid?.()}`;
71
+ await run('launchctl', ['bootout', domain, files.definitionPath]).catch(() => undefined);
72
+ await run('launchctl', ['bootstrap', domain, files.definitionPath]);
73
+ await run('launchctl', ['kickstart', '-k', `${domain}/${files.serviceId}`]);
74
+ }
75
+ else if (os === 'win32') {
76
+ await run('icacls', [files.enrollmentPath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:(F)`]);
77
+ await run('schtasks', ['/Create', '/F', '/SC', 'ONLOGON', '/TN', files.serviceId, '/TR', files.definitionPath]);
78
+ await run('schtasks', ['/Run', '/TN', files.serviceId]);
79
+ }
80
+ else {
81
+ await run('systemctl', ['--user', 'daemon-reload']);
82
+ await run('systemctl', ['--user', 'enable', '--now', files.serviceId]);
83
+ }
84
+ return files;
85
+ }
86
+ async function uninstallCaptureService(repositoryRoot, os = (0, node_os_1.platform)()) {
87
+ const files = serviceFiles(repositoryRoot, process.env, os);
88
+ if (os === 'darwin')
89
+ await run('launchctl', ['bootout', `gui/${process.getuid?.()}`, files.definitionPath]).catch(() => undefined);
90
+ else if (os === 'win32')
91
+ await run('schtasks', ['/Delete', '/F', '/TN', files.serviceId]).catch(() => undefined);
92
+ else {
93
+ await run('systemctl', ['--user', 'disable', '--now', files.serviceId]).catch(() => undefined);
94
+ await run('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
95
+ }
96
+ await (0, promises_1.rm)(files.definitionPath, { force: true });
97
+ await (0, promises_1.rm)(files.enrollmentPath, { force: true });
98
+ return files;
99
+ }
100
+ async function readServiceEnrollment(path) {
101
+ const parsed = JSON.parse(await (0, promises_1.readFile)(path, 'utf8'));
102
+ if (!parsed.endpoint || !parsed.token || !parsed.organizationId || !parsed.captureInstallationId || !parsed.actorJson)
103
+ throw new Error('Service enrollment is incomplete');
104
+ if (!Number.isInteger(parsed.intervalSeconds) || parsed.intervalSeconds < 15 || parsed.intervalSeconds > 3600)
105
+ throw new Error('Service interval is invalid');
106
+ return parsed;
107
+ }
108
+ function applyServiceEnrollment(enrollment) {
109
+ process.env.ZEYE_CAPTURE_ENDPOINT = enrollment.endpoint;
110
+ process.env.ZEYE_CAPTURE_TOKEN = enrollment.token;
111
+ process.env.ZEYE_CAPTURE_ORGANIZATION_ID = enrollment.organizationId;
112
+ process.env.ZEYE_CAPTURE_INSTALLATION_ID = enrollment.captureInstallationId;
113
+ process.env.ZEYE_CAPTURE_ACTOR_JSON = enrollment.actorJson;
114
+ if (enrollment.contentConsentId)
115
+ process.env.ZEYE_CAPTURE_CONTENT_CONSENT_ID = enrollment.contentConsentId;
116
+ if (enrollment.transcriptConsent)
117
+ process.env.ZEYE_CAPTURE_TRANSCRIPT_CONSENT = enrollment.transcriptConsent;
118
+ }