@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 +194 -0
- package/dist/src/canonical.d.ts +3 -0
- package/dist/src/canonical.js +41 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +470 -0
- package/dist/src/fileOutbox.d.ts +22 -0
- package/dist/src/fileOutbox.js +219 -0
- package/dist/src/host.d.ts +14 -0
- package/dist/src/host.js +92 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +24 -0
- package/dist/src/localHost.d.ts +81 -0
- package/dist/src/localHost.js +289 -0
- package/dist/src/providers.d.ts +18 -0
- package/dist/src/providers.js +381 -0
- package/dist/src/redaction.d.ts +7 -0
- package/dist/src/redaction.js +80 -0
- package/dist/src/service.d.ts +28 -0
- package/dist/src/service.js +118 -0
- package/dist/src/transport.d.ts +156 -0
- package/dist/src/transport.js +368 -0
- package/dist/src/types.d.ts +125 -0
- package/dist/src/types.js +2 -0
- package/dist/test/fileOutbox.test.d.ts +1 -0
- package/dist/test/fileOutbox.test.js +104 -0
- package/dist/test/host.test.d.ts +1 -0
- package/dist/test/host.test.js +58 -0
- package/dist/test/localHost.test.d.ts +1 -0
- package/dist/test/localHost.test.js +122 -0
- package/dist/test/providers-and-transport.test.d.ts +1 -0
- package/dist/test/providers-and-transport.test.js +222 -0
- package/dist/test/transcript-and-service.test.d.ts +1 -0
- package/dist/test/transcript-and-service.test.js +50 -0
- package/package.json +24 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type CaptureActor, type CaptureCompleteness, type CapturePrivacyProfile, type CaptureRepositoryReference } from "@zewstid/zeye-shared";
|
|
2
|
+
import type { DeliveryRecord, DurableOutbox } from "./types";
|
|
3
|
+
type TransportRequest = (url: string, init: {
|
|
4
|
+
method: "POST";
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
status: number;
|
|
11
|
+
json(): Promise<unknown>;
|
|
12
|
+
}>;
|
|
13
|
+
export interface CaptureTransportConfiguration {
|
|
14
|
+
endpoint: string;
|
|
15
|
+
token: string;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
captureInstallationId: string;
|
|
18
|
+
actor: CaptureActor;
|
|
19
|
+
privacy: CapturePrivacyProfile;
|
|
20
|
+
repositories?: CaptureRepositoryReference[];
|
|
21
|
+
completeness?: CaptureCompleteness[];
|
|
22
|
+
}
|
|
23
|
+
export interface FlushResult {
|
|
24
|
+
delivered: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
deferred: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Machine credentials must never be sent to a cleartext or arbitrary collector.
|
|
30
|
+
* Local loopback HTTP is allowed only for an explicitly local development API.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateCaptureEndpoint(value: string): string;
|
|
33
|
+
export declare function capabilityManifestForAdapter(adapter: string, transcriptContentEnabled?: boolean): {
|
|
34
|
+
provider: "claude_code" | "codex" | "github_copilot" | "cursor" | "gemini_cli" | "ironclad" | "trajectory" | "opentelemetry" | "agent_client_protocol" | "generic";
|
|
35
|
+
clientName: string;
|
|
36
|
+
clientVersion: string;
|
|
37
|
+
contractVersion: "2026-09-05";
|
|
38
|
+
completeness: {
|
|
39
|
+
dimension: "LIFECYCLE" | "MESSAGES" | "TOOLS" | "FILE_CHANGES" | "GIT" | "USAGE" | "SUBAGENT_LINEAGE" | "ARTIFACTS";
|
|
40
|
+
maximumStatus: "COMPLETE" | "PARTIAL" | "METADATA_ONLY" | "UNAVAILABLE" | "CONFLICTED";
|
|
41
|
+
limitation?: string | undefined;
|
|
42
|
+
}[];
|
|
43
|
+
manifestVersion: string;
|
|
44
|
+
transports: ("native_hook" | "local_jsonl" | "provider_api" | "otlp" | "acp" | "webhook" | "import" | "manual")[];
|
|
45
|
+
supportedEventTypes: ("session.started" | "session.resumed" | "session.forked" | "session.compacted" | "session.ended" | "message.recorded" | "tool.started" | "tool.completed" | "file.changed" | "git.observed" | "artifact.claimed" | "artifact.verified" | "usage.recorded" | "subagent.started" | "subagent.ended" | "checkpoint.recorded" | "capture.warning")[];
|
|
46
|
+
supports: {
|
|
47
|
+
rawEvidenceReference: boolean;
|
|
48
|
+
offlineOutbox: boolean;
|
|
49
|
+
idempotentReplay: boolean;
|
|
50
|
+
resume: boolean;
|
|
51
|
+
fork: boolean;
|
|
52
|
+
compaction: boolean;
|
|
53
|
+
subagents: boolean;
|
|
54
|
+
localRedaction: boolean;
|
|
55
|
+
};
|
|
56
|
+
fixtureSetVersion: string;
|
|
57
|
+
};
|
|
58
|
+
export declare function deliveryToBatch(record: DeliveryRecord, config: CaptureTransportConfiguration): {
|
|
59
|
+
contractVersion: "2026-09-05";
|
|
60
|
+
batchId: string;
|
|
61
|
+
sentAt: Date;
|
|
62
|
+
events: {
|
|
63
|
+
capturedAt: Date;
|
|
64
|
+
contractVersion: "2026-09-05";
|
|
65
|
+
eventId: string;
|
|
66
|
+
idempotencyKey: string;
|
|
67
|
+
eventHash: string;
|
|
68
|
+
organizationId: string;
|
|
69
|
+
captureInstallationId: string;
|
|
70
|
+
logicalSessionId: string;
|
|
71
|
+
sessionGenerationId: string;
|
|
72
|
+
sourceSession: {
|
|
73
|
+
provider: "claude_code" | "codex" | "github_copilot" | "cursor" | "gemini_cli" | "ironclad" | "trajectory" | "opentelemetry" | "agent_client_protocol" | "generic";
|
|
74
|
+
clientName: string;
|
|
75
|
+
sourceSessionId: string;
|
|
76
|
+
relationship: "ROOT" | "RESUMED_FROM" | "FORKED_FROM" | "SUBAGENT_OF" | "CONTINUATION_OF";
|
|
77
|
+
clientVersion?: string | undefined;
|
|
78
|
+
sourceGenerationId?: string | undefined;
|
|
79
|
+
relatedSourceSessionId?: string | undefined;
|
|
80
|
+
};
|
|
81
|
+
actor: {
|
|
82
|
+
kind: "HUMAN" | "SERVICE" | "AGENT" | "INSTALLATION";
|
|
83
|
+
confidence: "CONFLICTED" | "VERIFIED" | "ASSERTED" | "INFERRED" | "UNRESOLVED";
|
|
84
|
+
zeyeUserId?: string | undefined;
|
|
85
|
+
providerSubjectId?: string | undefined;
|
|
86
|
+
providerSubjectHash?: string | undefined;
|
|
87
|
+
displayName?: string | undefined;
|
|
88
|
+
resolvedAt?: Date | undefined;
|
|
89
|
+
};
|
|
90
|
+
eventType: "session.started" | "session.resumed" | "session.forked" | "session.compacted" | "session.ended" | "message.recorded" | "tool.started" | "tool.completed" | "file.changed" | "git.observed" | "artifact.claimed" | "artifact.verified" | "usage.recorded" | "subagent.started" | "subagent.ended" | "checkpoint.recorded" | "capture.warning";
|
|
91
|
+
sequence: number;
|
|
92
|
+
occurredAt: Date;
|
|
93
|
+
transport: "native_hook" | "local_jsonl" | "provider_api" | "otlp" | "acp" | "webhook" | "import" | "manual";
|
|
94
|
+
privacy: {
|
|
95
|
+
classification: "PUBLIC" | "INTERNAL" | "CONFIDENTIAL" | "RESTRICTED";
|
|
96
|
+
contentMode: "METADATA_ONLY" | "REDACTED_CONTENT" | "ENCRYPTED_RAW_REFERENCE";
|
|
97
|
+
redactionApplied: boolean;
|
|
98
|
+
retentionClass: "STANDARD" | "SHORT" | "EXTENDED" | "LEGAL_HOLD";
|
|
99
|
+
visibility: "RESTRICTED" | "ACTOR_ONLY" | "TEAM" | "PRODUCT" | "ORGANIZATION";
|
|
100
|
+
redactionPolicyVersion?: string | undefined;
|
|
101
|
+
contentConsentId?: string | undefined;
|
|
102
|
+
};
|
|
103
|
+
repositories: {
|
|
104
|
+
isPrimary: boolean;
|
|
105
|
+
repositoryId?: string | undefined;
|
|
106
|
+
providerRepositoryId?: string | undefined;
|
|
107
|
+
remoteUrlHash?: string | undefined;
|
|
108
|
+
canonicalName?: string | undefined;
|
|
109
|
+
worktreePathHash?: string | undefined;
|
|
110
|
+
branch?: string | undefined;
|
|
111
|
+
headCommitSha?: string | undefined;
|
|
112
|
+
}[];
|
|
113
|
+
completeness: {
|
|
114
|
+
dimension: "LIFECYCLE" | "MESSAGES" | "TOOLS" | "FILE_CHANGES" | "GIT" | "USAGE" | "SUBAGENT_LINEAGE" | "ARTIFACTS";
|
|
115
|
+
status: "COMPLETE" | "PARTIAL" | "METADATA_ONLY" | "UNAVAILABLE" | "CONFLICTED";
|
|
116
|
+
observedAt: Date;
|
|
117
|
+
source: "CLIENT" | "RECONCILIATION" | "IMPORT" | "ADMIN";
|
|
118
|
+
reason?: string | undefined;
|
|
119
|
+
}[];
|
|
120
|
+
payload: Record<string, unknown>;
|
|
121
|
+
payloadSchemaVersion: string;
|
|
122
|
+
boundarySnapshot?: {
|
|
123
|
+
snapshotId: string;
|
|
124
|
+
capturedAt: Date;
|
|
125
|
+
policyVersion: string;
|
|
126
|
+
productIds: string[];
|
|
127
|
+
featureIds: string[];
|
|
128
|
+
workItemIds: string[];
|
|
129
|
+
repositoryBindings: {
|
|
130
|
+
repositoryId: string;
|
|
131
|
+
pathPrefix?: string | null | undefined;
|
|
132
|
+
}[];
|
|
133
|
+
} | undefined;
|
|
134
|
+
providerEventType?: string | undefined;
|
|
135
|
+
rawEvidenceReference?: string | undefined;
|
|
136
|
+
transcript?: {
|
|
137
|
+
redactionPolicyVersion: string;
|
|
138
|
+
kind: "USER_MESSAGE" | "ASSISTANT_MESSAGE" | "SYSTEM_MESSAGE" | "TOOL_INPUT" | "TOOL_OUTPUT";
|
|
139
|
+
text: string;
|
|
140
|
+
redactionCount: number;
|
|
141
|
+
providerMessageId?: string | undefined;
|
|
142
|
+
toolName?: string | undefined;
|
|
143
|
+
} | undefined;
|
|
144
|
+
}[];
|
|
145
|
+
};
|
|
146
|
+
export declare class CaptureHttpTransport {
|
|
147
|
+
private readonly outbox;
|
|
148
|
+
private readonly request;
|
|
149
|
+
private readonly config;
|
|
150
|
+
constructor(outbox: DurableOutbox, config: CaptureTransportConfiguration, request?: TransportRequest);
|
|
151
|
+
flush(limit?: number): Promise<FlushResult>;
|
|
152
|
+
publishManifest(manifest: ReturnType<typeof capabilityManifestForAdapter>): Promise<void>;
|
|
153
|
+
/** Explicit liveness signal for cron/launchd/Task Scheduler sync runs. */
|
|
154
|
+
heartbeat(): Promise<void>;
|
|
155
|
+
}
|
|
156
|
+
export {};
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CaptureHttpTransport = void 0;
|
|
4
|
+
exports.validateCaptureEndpoint = validateCaptureEndpoint;
|
|
5
|
+
exports.capabilityManifestForAdapter = capabilityManifestForAdapter;
|
|
6
|
+
exports.deliveryToBatch = deliveryToBatch;
|
|
7
|
+
const zeye_shared_1 = require("@zewstid/zeye-shared");
|
|
8
|
+
/**
|
|
9
|
+
* Machine credentials must never be sent to a cleartext or arbitrary collector.
|
|
10
|
+
* Local loopback HTTP is allowed only for an explicitly local development API.
|
|
11
|
+
*/
|
|
12
|
+
function validateCaptureEndpoint(value) {
|
|
13
|
+
let endpoint;
|
|
14
|
+
try {
|
|
15
|
+
endpoint = new URL(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error("ZEYE_CAPTURE_ENDPOINT must be an absolute URL");
|
|
19
|
+
}
|
|
20
|
+
const loopback = endpoint.hostname === "localhost" ||
|
|
21
|
+
endpoint.hostname === "127.0.0.1" ||
|
|
22
|
+
endpoint.hostname === "[::1]" ||
|
|
23
|
+
endpoint.hostname === "::1";
|
|
24
|
+
if (endpoint.protocol !== "https:" &&
|
|
25
|
+
!(endpoint.protocol === "http:" && loopback)) {
|
|
26
|
+
throw new Error("ZEYE_CAPTURE_ENDPOINT must use HTTPS (HTTP is allowed only for loopback development)");
|
|
27
|
+
}
|
|
28
|
+
if (endpoint.username ||
|
|
29
|
+
endpoint.password ||
|
|
30
|
+
endpoint.search ||
|
|
31
|
+
endpoint.hash) {
|
|
32
|
+
throw new Error("ZEYE_CAPTURE_ENDPOINT must not contain credentials, query parameters, or a fragment");
|
|
33
|
+
}
|
|
34
|
+
endpoint.pathname = endpoint.pathname.replace(/\/+$/, "");
|
|
35
|
+
if (!endpoint.pathname.endsWith("/engineering-memory/ingest")) {
|
|
36
|
+
throw new Error("ZEYE_CAPTURE_ENDPOINT must end with /engineering-memory/ingest");
|
|
37
|
+
}
|
|
38
|
+
return endpoint.toString();
|
|
39
|
+
}
|
|
40
|
+
const providerByAdapter = {
|
|
41
|
+
"claude-code": "claude_code",
|
|
42
|
+
codex: "codex",
|
|
43
|
+
"vscode-copilot": "github_copilot",
|
|
44
|
+
};
|
|
45
|
+
const eventTypes = new Set([
|
|
46
|
+
"session.started",
|
|
47
|
+
"session.resumed",
|
|
48
|
+
"session.forked",
|
|
49
|
+
"session.compacted",
|
|
50
|
+
"session.ended",
|
|
51
|
+
"message.recorded",
|
|
52
|
+
"tool.started",
|
|
53
|
+
"tool.completed",
|
|
54
|
+
"file.changed",
|
|
55
|
+
"git.observed",
|
|
56
|
+
"artifact.claimed",
|
|
57
|
+
"artifact.verified",
|
|
58
|
+
"usage.recorded",
|
|
59
|
+
"subagent.started",
|
|
60
|
+
"subagent.ended",
|
|
61
|
+
"checkpoint.recorded",
|
|
62
|
+
"capture.warning",
|
|
63
|
+
]);
|
|
64
|
+
const clientNameByAdapter = {
|
|
65
|
+
"claude-code": "Claude Code",
|
|
66
|
+
codex: "Codex",
|
|
67
|
+
"vscode-copilot": "VS Code / GitHub Copilot",
|
|
68
|
+
};
|
|
69
|
+
const transportByAdapter = {
|
|
70
|
+
"claude-code": "native_hook",
|
|
71
|
+
codex: "local_jsonl",
|
|
72
|
+
"vscode-copilot": "manual",
|
|
73
|
+
};
|
|
74
|
+
// A capability manifest is an observation contract. Do not advertise event
|
|
75
|
+
// types merely because the shared schema understands them: Zeye uses this to
|
|
76
|
+
// distinguish unavailable evidence from a misleading zero.
|
|
77
|
+
const adapterCapabilities = {
|
|
78
|
+
"claude-code": {
|
|
79
|
+
supportedEventTypes: [
|
|
80
|
+
"session.started",
|
|
81
|
+
"session.ended",
|
|
82
|
+
"session.compacted",
|
|
83
|
+
"message.recorded",
|
|
84
|
+
"tool.started",
|
|
85
|
+
"tool.completed",
|
|
86
|
+
"subagent.started",
|
|
87
|
+
"subagent.ended",
|
|
88
|
+
"capture.warning",
|
|
89
|
+
],
|
|
90
|
+
completeness: [
|
|
91
|
+
{ dimension: "LIFECYCLE", maximumStatus: "COMPLETE" },
|
|
92
|
+
{ dimension: "MESSAGES", maximumStatus: "METADATA_ONLY" },
|
|
93
|
+
{ dimension: "TOOLS", maximumStatus: "METADATA_ONLY" },
|
|
94
|
+
{ dimension: "SUBAGENT_LINEAGE", maximumStatus: "PARTIAL" },
|
|
95
|
+
{ dimension: "FILE_CHANGES", maximumStatus: "UNAVAILABLE" },
|
|
96
|
+
{ dimension: "GIT", maximumStatus: "UNAVAILABLE" },
|
|
97
|
+
{ dimension: "USAGE", maximumStatus: "UNAVAILABLE" },
|
|
98
|
+
{ dimension: "ARTIFACTS", maximumStatus: "UNAVAILABLE" },
|
|
99
|
+
],
|
|
100
|
+
supports: {
|
|
101
|
+
offlineOutbox: true,
|
|
102
|
+
idempotentReplay: true,
|
|
103
|
+
resume: false,
|
|
104
|
+
fork: false,
|
|
105
|
+
compaction: true,
|
|
106
|
+
subagents: true,
|
|
107
|
+
rawEvidenceReference: false,
|
|
108
|
+
localRedaction: true,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
codex: {
|
|
112
|
+
supportedEventTypes: [
|
|
113
|
+
"session.started",
|
|
114
|
+
"session.resumed",
|
|
115
|
+
"session.forked",
|
|
116
|
+
"session.compacted",
|
|
117
|
+
"session.ended",
|
|
118
|
+
"message.recorded",
|
|
119
|
+
"tool.started",
|
|
120
|
+
"tool.completed",
|
|
121
|
+
"usage.recorded",
|
|
122
|
+
"subagent.started",
|
|
123
|
+
"subagent.ended",
|
|
124
|
+
"capture.warning",
|
|
125
|
+
],
|
|
126
|
+
completeness: [
|
|
127
|
+
{ dimension: "LIFECYCLE", maximumStatus: "COMPLETE" },
|
|
128
|
+
{ dimension: "TOOLS", maximumStatus: "METADATA_ONLY" },
|
|
129
|
+
{ dimension: "USAGE", maximumStatus: "PARTIAL" },
|
|
130
|
+
{ dimension: "SUBAGENT_LINEAGE", maximumStatus: "PARTIAL" },
|
|
131
|
+
{ dimension: "MESSAGES", maximumStatus: "UNAVAILABLE" },
|
|
132
|
+
{ dimension: "FILE_CHANGES", maximumStatus: "UNAVAILABLE" },
|
|
133
|
+
{ dimension: "GIT", maximumStatus: "UNAVAILABLE" },
|
|
134
|
+
{ dimension: "ARTIFACTS", maximumStatus: "UNAVAILABLE" },
|
|
135
|
+
],
|
|
136
|
+
supports: {
|
|
137
|
+
offlineOutbox: true,
|
|
138
|
+
idempotentReplay: true,
|
|
139
|
+
resume: true,
|
|
140
|
+
fork: true,
|
|
141
|
+
compaction: true,
|
|
142
|
+
subagents: true,
|
|
143
|
+
rawEvidenceReference: false,
|
|
144
|
+
localRedaction: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
"vscode-copilot": {
|
|
148
|
+
supportedEventTypes: [
|
|
149
|
+
"session.started",
|
|
150
|
+
"session.resumed",
|
|
151
|
+
"session.compacted",
|
|
152
|
+
"session.ended",
|
|
153
|
+
"message.recorded",
|
|
154
|
+
"tool.started",
|
|
155
|
+
"tool.completed",
|
|
156
|
+
"file.changed",
|
|
157
|
+
"git.observed",
|
|
158
|
+
"usage.recorded",
|
|
159
|
+
"subagent.started",
|
|
160
|
+
"subagent.ended",
|
|
161
|
+
"capture.warning",
|
|
162
|
+
],
|
|
163
|
+
completeness: [
|
|
164
|
+
{ dimension: "LIFECYCLE", maximumStatus: "COMPLETE" },
|
|
165
|
+
{ dimension: "MESSAGES", maximumStatus: "METADATA_ONLY" },
|
|
166
|
+
{ dimension: "TOOLS", maximumStatus: "METADATA_ONLY" },
|
|
167
|
+
{ dimension: "FILE_CHANGES", maximumStatus: "METADATA_ONLY" },
|
|
168
|
+
{ dimension: "GIT", maximumStatus: "METADATA_ONLY" },
|
|
169
|
+
{ dimension: "USAGE", maximumStatus: "PARTIAL" },
|
|
170
|
+
{ dimension: "SUBAGENT_LINEAGE", maximumStatus: "PARTIAL" },
|
|
171
|
+
{ dimension: "ARTIFACTS", maximumStatus: "UNAVAILABLE" },
|
|
172
|
+
],
|
|
173
|
+
supports: {
|
|
174
|
+
offlineOutbox: true,
|
|
175
|
+
idempotentReplay: true,
|
|
176
|
+
resume: true,
|
|
177
|
+
fork: false,
|
|
178
|
+
compaction: true,
|
|
179
|
+
subagents: true,
|
|
180
|
+
rawEvidenceReference: false,
|
|
181
|
+
localRedaction: true,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
function capabilityManifestForAdapter(adapter, transcriptContentEnabled = false) {
|
|
186
|
+
const provider = providerByAdapter[adapter];
|
|
187
|
+
const capability = adapterCapabilities[adapter];
|
|
188
|
+
if (!provider || !capability)
|
|
189
|
+
throw new Error(`Unsupported capture adapter: ${adapter}`);
|
|
190
|
+
return zeye_shared_1.EngineeringMemoryCaptureCapabilityManifest.parse({
|
|
191
|
+
contractVersion: zeye_shared_1.ENGINEERING_MEMORY_CAPTURE_CONTRACT_VERSION,
|
|
192
|
+
manifestVersion: "1",
|
|
193
|
+
provider,
|
|
194
|
+
// Provider releases are event-specific and may not be exposed by the
|
|
195
|
+
// host. `unknown` is an explicit truthful value, unlike a capture-package
|
|
196
|
+
// version masquerading as the provider client version.
|
|
197
|
+
clientName: clientNameByAdapter[adapter],
|
|
198
|
+
clientVersion: "unknown",
|
|
199
|
+
transports: [transportByAdapter[adapter]],
|
|
200
|
+
supportedEventTypes: capability.supportedEventTypes,
|
|
201
|
+
completeness: capability.completeness.map((entry) => transcriptContentEnabled &&
|
|
202
|
+
adapter !== "vscode-copilot" &&
|
|
203
|
+
(entry.dimension === "MESSAGES" || entry.dimension === "TOOLS")
|
|
204
|
+
? { ...entry, maximumStatus: "COMPLETE" }
|
|
205
|
+
: entry),
|
|
206
|
+
supports: capability.supports,
|
|
207
|
+
fixtureSetVersion: "1",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function deliveryToBatch(record, config) {
|
|
211
|
+
const provider = providerByAdapter[record.adapter];
|
|
212
|
+
if (!provider)
|
|
213
|
+
throw new Error(`Unsupported capture adapter: ${record.adapter}`);
|
|
214
|
+
const eventType = eventTypes.has(record.type)
|
|
215
|
+
? record.type
|
|
216
|
+
: "capture.warning";
|
|
217
|
+
// Terminal-only observations are legitimate when capture begins mid-session.
|
|
218
|
+
// They are evidence of a session end, but not proof that its full lifecycle
|
|
219
|
+
// was observed.
|
|
220
|
+
const terminalOnly = eventType === "session.ended" && record.sequence === 0;
|
|
221
|
+
const completeness = terminalOnly
|
|
222
|
+
? [
|
|
223
|
+
...(config.completeness || []).filter((entry) => entry.dimension !== "LIFECYCLE"),
|
|
224
|
+
{
|
|
225
|
+
dimension: "LIFECYCLE",
|
|
226
|
+
status: "PARTIAL",
|
|
227
|
+
reason: "Capture began after the session start.",
|
|
228
|
+
observedAt: record.createdAt,
|
|
229
|
+
source: "CLIENT",
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
: config.completeness || [];
|
|
233
|
+
const privacy = record.transcript
|
|
234
|
+
? config.privacy
|
|
235
|
+
: {
|
|
236
|
+
...config.privacy,
|
|
237
|
+
contentMode: "METADATA_ONLY",
|
|
238
|
+
contentConsentId: undefined,
|
|
239
|
+
};
|
|
240
|
+
return zeye_shared_1.EngineeringMemoryCaptureBatch.parse({
|
|
241
|
+
contractVersion: zeye_shared_1.ENGINEERING_MEMORY_CAPTURE_CONTRACT_VERSION,
|
|
242
|
+
batchId: record.deliveryId,
|
|
243
|
+
sentAt: new Date().toISOString(),
|
|
244
|
+
events: [
|
|
245
|
+
{
|
|
246
|
+
contractVersion: zeye_shared_1.ENGINEERING_MEMORY_CAPTURE_CONTRACT_VERSION,
|
|
247
|
+
eventId: record.deliveryId,
|
|
248
|
+
idempotencyKey: record.sourceKey,
|
|
249
|
+
eventHash: record.payloadHash,
|
|
250
|
+
organizationId: config.organizationId,
|
|
251
|
+
captureInstallationId: config.captureInstallationId,
|
|
252
|
+
logicalSessionId: record.sessionId,
|
|
253
|
+
sessionGenerationId: record.sourceGenerationId || record.sessionId,
|
|
254
|
+
sourceSession: {
|
|
255
|
+
provider,
|
|
256
|
+
clientName: record.clientName || clientNameByAdapter[record.adapter],
|
|
257
|
+
...(record.clientVersion
|
|
258
|
+
? { clientVersion: record.clientVersion }
|
|
259
|
+
: {}),
|
|
260
|
+
sourceSessionId: record.sessionId,
|
|
261
|
+
sourceGenerationId: record.sourceGenerationId || record.sessionId,
|
|
262
|
+
relationship: record.relationship || "ROOT",
|
|
263
|
+
...(record.relatedSourceSessionId
|
|
264
|
+
? { relatedSourceSessionId: record.relatedSourceSessionId }
|
|
265
|
+
: {}),
|
|
266
|
+
},
|
|
267
|
+
actor: config.actor,
|
|
268
|
+
eventType,
|
|
269
|
+
sequence: record.sequence,
|
|
270
|
+
occurredAt: record.occurredAt,
|
|
271
|
+
capturedAt: record.createdAt,
|
|
272
|
+
transport: transportByAdapter[record.adapter],
|
|
273
|
+
privacy,
|
|
274
|
+
// New durable records carry the checkout facts observed at capture time.
|
|
275
|
+
// The runtime configuration remains a compatibility fallback for records
|
|
276
|
+
// created before the context field existed.
|
|
277
|
+
repositories: record.captureRepositories ?? config.repositories ?? [],
|
|
278
|
+
completeness,
|
|
279
|
+
payload: record.payload,
|
|
280
|
+
payloadSchemaVersion: "1",
|
|
281
|
+
providerEventType: record.type,
|
|
282
|
+
...(record.transcript ? { transcript: record.transcript } : {}),
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
class CaptureHttpTransport {
|
|
288
|
+
outbox;
|
|
289
|
+
request;
|
|
290
|
+
config;
|
|
291
|
+
constructor(outbox, config, request = fetch) {
|
|
292
|
+
this.outbox = outbox;
|
|
293
|
+
this.request = request;
|
|
294
|
+
this.config = {
|
|
295
|
+
...config,
|
|
296
|
+
endpoint: validateCaptureEndpoint(config.endpoint),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async flush(limit = 25) {
|
|
300
|
+
const records = await this.outbox.nextBatch(limit);
|
|
301
|
+
let delivered = 0;
|
|
302
|
+
let failed = 0;
|
|
303
|
+
let deferred = 0;
|
|
304
|
+
for (const record of records) {
|
|
305
|
+
try {
|
|
306
|
+
const batch = deliveryToBatch(record, this.config);
|
|
307
|
+
const response = await this.request(this.config.endpoint, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
authorization: `Bearer ${this.config.token}`,
|
|
311
|
+
"content-type": "application/json",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify(batch),
|
|
314
|
+
});
|
|
315
|
+
const body = (await response.json().catch(() => ({})));
|
|
316
|
+
if (!response.ok || !body.receiptId) {
|
|
317
|
+
await this.outbox.markAttempt(record.deliveryId, `capture_upload_${response.status}`);
|
|
318
|
+
if (response.status >= 400 && response.status < 500)
|
|
319
|
+
deferred += 1;
|
|
320
|
+
else
|
|
321
|
+
failed += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
await this.outbox.acknowledge(record.deliveryId, body.receiptId);
|
|
325
|
+
delivered += 1;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
await this.outbox.markAttempt(record.deliveryId, error instanceof Error
|
|
329
|
+
? error.message.slice(0, 200)
|
|
330
|
+
: "capture_upload_failed");
|
|
331
|
+
failed += 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { delivered, failed, deferred };
|
|
335
|
+
}
|
|
336
|
+
async publishManifest(manifest) {
|
|
337
|
+
const endpoint = this.config.endpoint.replace(/\/ingest$/, "/manifest");
|
|
338
|
+
if (endpoint === this.config.endpoint)
|
|
339
|
+
throw new Error("Capture endpoint must end with /ingest to publish a manifest");
|
|
340
|
+
const response = await this.request(endpoint, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: {
|
|
343
|
+
authorization: `Bearer ${this.config.token}`,
|
|
344
|
+
"content-type": "application/json",
|
|
345
|
+
},
|
|
346
|
+
body: JSON.stringify(manifest),
|
|
347
|
+
});
|
|
348
|
+
if (!response.ok)
|
|
349
|
+
throw new Error(`capture_manifest_${response.status}`);
|
|
350
|
+
}
|
|
351
|
+
/** Explicit liveness signal for cron/launchd/Task Scheduler sync runs. */
|
|
352
|
+
async heartbeat() {
|
|
353
|
+
const endpoint = this.config.endpoint.replace(/\/ingest$/, "/heartbeat");
|
|
354
|
+
if (endpoint === this.config.endpoint)
|
|
355
|
+
throw new Error("Capture endpoint must end with /ingest to publish a heartbeat");
|
|
356
|
+
const response = await this.request(endpoint, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: {
|
|
359
|
+
authorization: `Bearer ${this.config.token}`,
|
|
360
|
+
"content-type": "application/json",
|
|
361
|
+
},
|
|
362
|
+
body: "{}",
|
|
363
|
+
});
|
|
364
|
+
if (!response.ok)
|
|
365
|
+
throw new Error(`capture_heartbeat_${response.status}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.CaptureHttpTransport = CaptureHttpTransport;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral data accepted by the local capture boundary. Adapters must
|
|
3
|
+
* use stable sourceEventId values whenever their provider exposes them.
|
|
4
|
+
*/
|
|
5
|
+
export interface CaptureEvent {
|
|
6
|
+
adapter: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
sourceEventId: string;
|
|
9
|
+
occurredAt: string;
|
|
10
|
+
type: string;
|
|
11
|
+
payload: unknown;
|
|
12
|
+
deliveryId?: string;
|
|
13
|
+
/** Provider client metadata and lineage are facts, not payload. Keeping
|
|
14
|
+
* them outside payload preserves the redaction boundary and the server's
|
|
15
|
+
* source-session identity contract. */
|
|
16
|
+
clientName?: string;
|
|
17
|
+
clientVersion?: string;
|
|
18
|
+
sourceGenerationId?: string;
|
|
19
|
+
relationship?: 'ROOT' | 'RESUMED_FROM' | 'FORKED_FROM' | 'SUBAGENT_OF' | 'CONTINUATION_OF';
|
|
20
|
+
relatedSourceSessionId?: string;
|
|
21
|
+
transcript?: {
|
|
22
|
+
kind: 'USER_MESSAGE' | 'ASSISTANT_MESSAGE' | 'SYSTEM_MESSAGE' | 'TOOL_INPUT' | 'TOOL_OUTPUT';
|
|
23
|
+
text: string;
|
|
24
|
+
providerMessageId?: string;
|
|
25
|
+
toolName?: string;
|
|
26
|
+
redactionCount?: number;
|
|
27
|
+
redactionPolicyVersion?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Provider adapters emit only observable metadata. They must never place a
|
|
32
|
+
* prompt, agent response, shell command, file body, environment value, or
|
|
33
|
+
* credential in `payload`.
|
|
34
|
+
*/
|
|
35
|
+
export interface MetadataCaptureEvent extends CaptureEvent {
|
|
36
|
+
provider: 'claude_code' | 'codex' | 'github_copilot';
|
|
37
|
+
clientName: string;
|
|
38
|
+
}
|
|
39
|
+
export interface RedactionResult {
|
|
40
|
+
payload: unknown;
|
|
41
|
+
transcript?: CaptureEvent['transcript'];
|
|
42
|
+
redactionCount: number;
|
|
43
|
+
redactionReasons?: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The capture boundary is deliberately injectable. Production adapters must
|
|
47
|
+
* redact before durable local storage and before any future network delivery.
|
|
48
|
+
*/
|
|
49
|
+
export interface RedactionBoundary {
|
|
50
|
+
redact(event: CaptureEvent): RedactionResult | Promise<RedactionResult>;
|
|
51
|
+
}
|
|
52
|
+
export type DeliveryStatus = 'PENDING' | 'RETRY' | 'ACKNOWLEDGED' | 'CONFLICT';
|
|
53
|
+
export interface DeliveryRecord {
|
|
54
|
+
deliveryId: string;
|
|
55
|
+
sourceKey: string;
|
|
56
|
+
adapter: string;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
sourceEventId: string;
|
|
59
|
+
clientName?: string;
|
|
60
|
+
clientVersion?: string;
|
|
61
|
+
sourceGenerationId?: string;
|
|
62
|
+
relationship?: 'ROOT' | 'RESUMED_FROM' | 'FORKED_FROM' | 'SUBAGENT_OF' | 'CONTINUATION_OF';
|
|
63
|
+
relatedSourceSessionId?: string;
|
|
64
|
+
transcript?: NonNullable<CaptureEvent['transcript']> & {
|
|
65
|
+
redactionCount: number;
|
|
66
|
+
redactionPolicyVersion: string;
|
|
67
|
+
};
|
|
68
|
+
type: string;
|
|
69
|
+
/** Monotonic per local session; assigned durably by the outbox. */
|
|
70
|
+
sequence: number;
|
|
71
|
+
occurredAt: string;
|
|
72
|
+
/**
|
|
73
|
+
* Repository facts observed when this delivery entered the durable outbox.
|
|
74
|
+
* They must not be rebuilt from a later checkout, branch, or enrollment
|
|
75
|
+
* environment when an offline record is replayed.
|
|
76
|
+
*/
|
|
77
|
+
captureRepositories?: CaptureRepositoryReference[];
|
|
78
|
+
payload: unknown;
|
|
79
|
+
payloadHash: string;
|
|
80
|
+
redactionCount: number;
|
|
81
|
+
redactionReasons: string[];
|
|
82
|
+
status: DeliveryStatus;
|
|
83
|
+
attempts: number;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
updatedAt: string;
|
|
86
|
+
lastAttemptAt?: string;
|
|
87
|
+
lastError?: string;
|
|
88
|
+
acknowledgedAt?: string;
|
|
89
|
+
receiptId?: string;
|
|
90
|
+
}
|
|
91
|
+
/** Immutable non-secret evidence context captured with one delivery. */
|
|
92
|
+
export interface CaptureDeliveryContext {
|
|
93
|
+
repositories: CaptureRepositoryReference[];
|
|
94
|
+
}
|
|
95
|
+
export interface SourceCheckpoint {
|
|
96
|
+
adapter: string;
|
|
97
|
+
scope: string;
|
|
98
|
+
cursor: string;
|
|
99
|
+
updatedAt: string;
|
|
100
|
+
}
|
|
101
|
+
export interface CaptureOutboxState {
|
|
102
|
+
version: 1;
|
|
103
|
+
deliveries: Record<string, DeliveryRecord>;
|
|
104
|
+
checkpoints: Record<string, SourceCheckpoint>;
|
|
105
|
+
}
|
|
106
|
+
export type EnqueueResult = {
|
|
107
|
+
status: 'ENQUEUED';
|
|
108
|
+
record: DeliveryRecord;
|
|
109
|
+
} | {
|
|
110
|
+
status: 'DUPLICATE';
|
|
111
|
+
record: DeliveryRecord;
|
|
112
|
+
} | {
|
|
113
|
+
status: 'CONFLICT';
|
|
114
|
+
record: DeliveryRecord;
|
|
115
|
+
};
|
|
116
|
+
export interface DurableOutbox {
|
|
117
|
+
enqueue(event: CaptureEvent, context?: CaptureDeliveryContext): Promise<EnqueueResult>;
|
|
118
|
+
nextBatch(limit: number): Promise<DeliveryRecord[]>;
|
|
119
|
+
markAttempt(deliveryId: string, error?: string): Promise<DeliveryRecord>;
|
|
120
|
+
acknowledge(deliveryId: string, receiptId: string): Promise<DeliveryRecord>;
|
|
121
|
+
checkpoint(adapter: string, scope: string, cursor: string): Promise<SourceCheckpoint>;
|
|
122
|
+
getCheckpoint(adapter: string, scope: string): Promise<SourceCheckpoint | undefined>;
|
|
123
|
+
list(): Promise<DeliveryRecord[]>;
|
|
124
|
+
}
|
|
125
|
+
import type { CaptureRepositoryReference } from '@zewstid/zeye-shared';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|