@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.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @zewstid/zeye-capture
2
+
3
+ Local-first capture and continuous sync for Zeye Engineering Memory. Metadata
4
+ capture is the default. Complete provider-visible prompts, responses and tool
5
+ records are captured only after explicit Zeye consent and local secret
6
+ redaction. Hidden model reasoning is never captured.
7
+
8
+ ## What exists now
9
+
10
+ - `FileOutbox`: a JSON-backed durable outbox with atomic state replacement.
11
+ - Idempotent delivery records keyed by `adapter + sessionId + sourceEventId`,
12
+ including immutable repository/branch/HEAD facts observed at enqueue time.
13
+ - Stable content hashes: replays with the same source and content collapse;
14
+ changed content from the same provider event is reported as a conflict.
15
+ - Source checkpoints for JSONL/backfill cursors.
16
+ - A required `RedactionBoundary`, executed before a payload reaches disk.
17
+ - Delivery attempt and acknowledgement state, allowing a future HTTP transport
18
+ to replay safely after an offline period.
19
+ - Claude Code hook and Codex JSONL adapters for lifecycle metadata and,
20
+ when explicitly consented, provider-visible messages and tool records.
21
+ - A one-command background service that continuously imports and retries on
22
+ macOS (launchd), Linux (systemd user service), and Windows (Task Scheduler).
23
+ - `CaptureHttpTransport`, which turns one durable local delivery into one
24
+ authenticated, idempotent `POST /engineering-memory/ingest` batch.
25
+ - A production-oriented local CLI: Claude Code hook install/removal, configured
26
+ Codex JSONL sync, safe Git identity discovery, durable outbox locking,
27
+ explicit heartbeat, and non-secret status diagnostics.
28
+
29
+ ## Security and limits
30
+
31
+ - Transcript content is locally redacted before either the durable outbox or
32
+ network boundary. The API envelope-encrypts each segment with a separate data
33
+ key and keeps ciphertext apart from metadata.
34
+ - The one-time enrollment token is kept in an owner-only local service config,
35
+ never in the service definition, command line, repository, or outbox. Native
36
+ keychain integration remains future hardening.
37
+ - VS Code/GitHub Copilot currently has a safe companion-event boundary; a
38
+ packaged extension is not included in this release.
39
+ - “Complete” means all provider-visible records emitted by supported adapters.
40
+ It never means hidden chain-of-thought or provider-private state.
41
+
42
+ ## Usage
43
+
44
+ ```ts
45
+ import { FileOutbox, type RedactionBoundary } from '@zewstid/zeye-capture'
46
+
47
+ const redactor: RedactionBoundary = {
48
+ redact(event) {
49
+ return { payload: event.payload, redactionCount: 0 }
50
+ },
51
+ }
52
+
53
+ const outbox = new FileOutbox('/private/path/zeye-capture', redactor)
54
+ const result = await outbox.enqueue({
55
+ adapter: 'claude-code',
56
+ sessionId: 'provider-session-id',
57
+ sourceEventId: 'provider-event-id',
58
+ occurredAt: new Date().toISOString(),
59
+ type: 'tool.completed',
60
+ payload: { tool: 'Bash', exitCode: 0 },
61
+ })
62
+ ```
63
+
64
+ The included transport calls `markAttempt()` for unsuccessful uploads and
65
+ `acknowledge(deliveryId, receiptId)` only after a server receipt is durably
66
+ returned. It never regenerates a delivery ID or mutates an already queued
67
+ event.
68
+
69
+ ## Provider adapters
70
+
71
+ ```ts
72
+ import {
73
+ FileOutbox, metadataRedactor, fromClaudeCodeHook,
74
+ fromCodexJsonl, CaptureHttpTransport,
75
+ } from '@zewstid/zeye-capture'
76
+
77
+ const outbox = new FileOutbox('/private/path/zeye-capture', metadataRedactor)
78
+
79
+ // Claude Code hook stdin JSON → local durable evidence.
80
+ const claudeEvent = fromClaudeCodeHook(hookPayload)
81
+ if (claudeEvent) await outbox.enqueue(claudeEvent)
82
+
83
+ // One Codex JSONL record → local durable evidence.
84
+ const codexEvent = fromCodexJsonl(JSON.parse(jsonlLine), 'rollout.jsonl')
85
+ if (codexEvent) await outbox.enqueue(codexEvent)
86
+
87
+ // A VS Code companion extension sends a deliberately metadata-only event to
88
+ // `zeye-capture vscode-copilot-event` over stdin. Its event shape is also
89
+ // available to hosts through fromVSCodeCopilotEvent().
90
+
91
+ // A host application supplies the enrollment token at runtime; it is not
92
+ // written into the outbox. The server still has to enable
93
+ // engineering-memory.session-ledger for the organization.
94
+ const transport = new CaptureHttpTransport(outbox, enrollmentConfiguration)
95
+ await transport.flush()
96
+ ```
97
+
98
+ The adapters intentionally treat unknown provider record shapes as
99
+ `capture.warning`. That keeps capture health honest while provider formats
100
+ change, instead of inventing a complete record.
101
+
102
+ The VS Code adapter is an integration boundary for a companion extension; it
103
+ does not claim to scrape Copilot chats or private extension storage. A future
104
+ packaged VSIX can emit these events using the same contract without changing
105
+ the durable outbox or server API.
106
+
107
+ ## Local host workflow
108
+
109
+ Build the package first, then make the executable available on your `PATH` (or
110
+ use the built `dist/src/cli.js` through Node). Run all commands from the Git
111
+ repository being captured, or pass `--repo /absolute/path`.
112
+
113
+ ```bash
114
+ # Add Zeye-owned groups to the repository-local, untracked Claude settings.
115
+ zeye-capture install claude
116
+
117
+ # Register a known Codex rollout/session JSONL source. This does not guess a
118
+ # provider-private location and does not start a background process.
119
+ zeye-capture install codex --jsonl /absolute/path/to/rollout.jsonl
120
+
121
+ # Import configured Codex sources, upload any durable outbox records, and send
122
+ # a heartbeat when runtime enrollment is available.
123
+ zeye-capture sync
124
+
125
+ # View safe local diagnostics: queue counts, lock state, Git identity, and
126
+ # whether upload variables are complete. It never prints the machine token.
127
+ zeye-capture status
128
+ zeye-capture heartbeat
129
+
130
+ # With ZEYE_CAPTURE_* values present, install and immediately start continuous
131
+ # sync. The service survives logout/restart according to the host OS policy.
132
+ zeye-capture service install --repo /path/to/repo
133
+
134
+ # Stop continuous sync and remove both its definition and private enrollment.
135
+ zeye-capture service uninstall --repo /path/to/repo
136
+
137
+ # Remove only hook groups/sources created for Zeye.
138
+ zeye-capture uninstall claude
139
+ zeye-capture uninstall codex --jsonl /absolute/path/to/rollout.jsonl
140
+ ```
141
+
142
+ `install claude` defaults to `.claude/settings.local.json` and refuses to
143
+ modify that file if Git tracks it. It preserves other hook groups. The host
144
+ configuration is stored under `$XDG_CONFIG_HOME/zeye-capture/host-v1.json` (or
145
+ `~/.config/...`) with owner-only permissions where the OS supports POSIX modes.
146
+ The default outbox is under `$XDG_STATE_HOME/zeye-capture/` (or
147
+ `~/.local/state/...`) and is also owner-restricted. `ZEYE_CAPTURE_OUTBOX_DIR`,
148
+ when set, is a private base directory: Zeye creates a distinct hashed child for
149
+ each repository so records cannot cross repository boundaries. Neither location
150
+ stores the upload token.
151
+
152
+ Interactive setup supplies these variables once. `service install` persists
153
+ them in an owner-only local service enrollment so no scheduler is configured
154
+ by hand:
155
+
156
+ ```text
157
+ ZEYE_CAPTURE_ENDPOINT=https://api.example/engineering-memory/ingest
158
+ ZEYE_CAPTURE_TOKEN=zem_... # runtime only; never written by CLI
159
+ ZEYE_CAPTURE_ORGANIZATION_ID=...
160
+ ZEYE_CAPTURE_INSTALLATION_ID=...
161
+ ZEYE_CAPTURE_ACTOR_JSON={...}
162
+ ZEYE_CAPTURE_REPOSITORY_ID=... # optional internal Zeye binding
163
+ ZEYE_CAPTURE_PROVIDER_REPOSITORY_ID=... # optional provider numeric ID
164
+ ZEYE_CAPTURE_CONTENT_CONSENT_ID=... # required only for transcripts
165
+ ZEYE_CAPTURE_TRANSCRIPT_CONSENT=yes # explicit local opt-in
166
+ ```
167
+
168
+ `ZEYE_CAPTURE_ENDPOINT` must be an HTTPS URL ending in
169
+ `/engineering-memory/ingest`. HTTP is accepted only for an explicit loopback
170
+ development collector. Without both content-consent variables, capture remains
171
+ metadata-only even if the installation is transcript-capable.
172
+
173
+ Without all required values the host remains useful offline: it captures into
174
+ the local outbox and reports `uploadConfigured: false`. Git discovery supplies
175
+ the remote's canonical `owner/repository` name, branch, and HEAD SHA when
176
+ available; the raw remote URL is not stored or printed. A configured internal
177
+ repository ID enables the server to bind those facts to existing Zeye evidence.
178
+
179
+ For continuous capture, run `zeye-capture service install --repo /path/to/repo`
180
+ once. The CLI
181
+ uses an owner-only single-process lock and waits briefly for an overlapping
182
+ hook/run before failing, so normal bursts are queued rather than dropped. A
183
+ `sync` exits non-zero if an input source or upload fails, while retaining its
184
+ outbox records for retry. Hook calls use the same lock.
185
+
186
+ ## Build and test
187
+
188
+ ```bash
189
+ npm -w @zewstid/zeye-capture run build
190
+ npm -w @zewstid/zeye-capture test
191
+ ```
192
+
193
+ The root workspace already includes `packages/*`, so no workspace manifest
194
+ change is required. The package uses only Node built-ins at runtime.
@@ -0,0 +1,3 @@
1
+ export declare function canonicalJson(value: unknown): string;
2
+ export declare function sha256(value: unknown): string;
3
+ export declare function newDeliveryId(): string;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonicalJson = canonicalJson;
4
+ exports.sha256 = sha256;
5
+ exports.newDeliveryId = newDeliveryId;
6
+ const node_crypto_1 = require("node:crypto");
7
+ function normalize(value) {
8
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
9
+ return value;
10
+ if (typeof value === 'number') {
11
+ if (!Number.isFinite(value))
12
+ throw new TypeError('Capture payload cannot contain non-finite numbers');
13
+ return value;
14
+ }
15
+ if (typeof value === 'bigint' || typeof value === 'function' || typeof value === 'symbol' || value === undefined) {
16
+ throw new TypeError(`Capture payload contains unsupported value type: ${typeof value}`);
17
+ }
18
+ if (Array.isArray(value))
19
+ return value.map(normalize);
20
+ if (value instanceof Date)
21
+ return value.toISOString();
22
+ if (typeof value === 'object') {
23
+ const object = value;
24
+ return Object.keys(object)
25
+ .sort()
26
+ .reduce((result, key) => {
27
+ result[key] = normalize(object[key]);
28
+ return result;
29
+ }, {});
30
+ }
31
+ throw new TypeError('Capture payload is not serializable');
32
+ }
33
+ function canonicalJson(value) {
34
+ return JSON.stringify(normalize(value));
35
+ }
36
+ function sha256(value) {
37
+ return (0, node_crypto_1.createHash)('sha256').update(canonicalJson(value), 'utf8').digest('hex');
38
+ }
39
+ function newDeliveryId() {
40
+ return (0, node_crypto_1.randomUUID)();
41
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};