@insitue/capture-core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rod Leviton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Pure DOM → data helpers. Browser APIs only — no transport/agent/fs,
3
+ * no dependencies. Shared by every vehicle.
4
+ */
5
+
6
+ /** Prune + sanitize a subtree: depth/breadth-capped, event handlers
7
+ * and secret-looking attrs stripped, text truncated. */
8
+ declare function serializeNode(el: Element, depth?: number, maxChildren?: number): SerializedNode;
9
+ declare function curateComputedStyles(el: Element): Record<string, string>;
10
+ /** The verbatim class list — the agent edits Tailwind classes, not
11
+ * inline styles, so the source-of-truth is `className`. */
12
+ declare function extractTailwindClasses(el: Element): string[];
13
+ /** A robust, reasonably-stable CSS path. Prefers #id, then a
14
+ * data-testid, else tag + nth-of-type up to a shortest unique path.
15
+ * Always present — the last-resort locator. */
16
+ declare function buildSelector(el: Element): string;
17
+ /** Tailwind-ish breakpoint label from viewport width. */
18
+ declare function breakpointFor(w: number): string;
19
+
20
+ /**
21
+ * Pure React-fiber → source resolver. No React dependency: reads the
22
+ * `__reactFiber$*` expando React attaches to host DOM nodes in dev,
23
+ * walks `_debugOwner`/`return`, and harvests `_debugSource`
24
+ * ({ fileName, lineNumber, columnNumber }) plus component names.
25
+ * Falls back to a build-injected `data-insitu-source` attribute.
26
+ */
27
+
28
+ declare function resolveTarget(el: Element): CaptureTarget;
29
+
30
+ /**
31
+ * @insitue/capture-core — THE SEAM.
32
+ *
33
+ * Pure, serializable data model + interfaces shared by every InSitue
34
+ * vehicle (dev SDK now; production capture-only, browser extension or
35
+ * Electron later). This package MUST NOT import transport (ws), an
36
+ * agent SDK, or fs/git — that is enforced by lint (see eslint config)
37
+ * and by keeping this file dependency-free. The whole point: a
38
+ * `CaptureBundle` can cross any boundary unchanged, and swapping the
39
+ * `CaptureSink` is what turns "local agentic edit" into "prod
40
+ * capture-only" without a rewrite.
41
+ */
42
+ /** Bump when `CaptureBundle`'s shape changes; sinks branch on it.
43
+ * v2: additive `screenshotUnavailable` (M5 — honest screenshots).
44
+ * v3: additive `screenshot.source` + `screenshot.qualityNote`
45
+ * (pixel-perfect layered capture — rasterise vs display-media). */
46
+ declare const CAPTURE_SCHEMA_VERSION: 3;
47
+ /** Bump when the WS envelope below changes; companion/SDK pin it.
48
+ * v2: agent edit-loop messages (M2). v3: session undo/commit (M3).
49
+ * v4: agent-activity (M6 — live "what it's doing" feedback). */
50
+ declare const PROTOCOL_VERSION: 4;
51
+ interface SourceLoc {
52
+ /** Repo-relative POSIX path, e.g. `components/MainBar.tsx`. */
53
+ file: string;
54
+ line: number;
55
+ column: number;
56
+ }
57
+ type SelectionMode = "element" | "rect" | "multi";
58
+ interface SelectionInput {
59
+ mode: SelectionMode;
60
+ /** `elementsFromPoint` chain at the click (outermost→innermost). */
61
+ pointerPath?: Element[];
62
+ /** Freeform region in viewport CSS px. */
63
+ rect?: {
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ height: number;
68
+ };
69
+ }
70
+ interface SerializedNode {
71
+ tag: string;
72
+ attrs: Record<string, string>;
73
+ /** Truncated text content for leaf-ish nodes. */
74
+ text?: string;
75
+ children: SerializedNode[];
76
+ }
77
+ interface ConsoleEntry {
78
+ level: "log" | "info" | "warn" | "error" | "debug";
79
+ args: string[];
80
+ ts: number;
81
+ }
82
+ interface NetworkEntry {
83
+ url: string;
84
+ method: string;
85
+ status?: number;
86
+ ok: boolean;
87
+ ts: number;
88
+ }
89
+ interface RuntimeError {
90
+ message: string;
91
+ stack?: string;
92
+ ts: number;
93
+ }
94
+ /** How confident the DOM→source resolution is. Surfaced in the UI and
95
+ * handed to the agent so it knows whether file:line is trustworthy. */
96
+ type SourceConfidence = "exact" | "approximate" | "selector-only";
97
+ interface CaptureTarget {
98
+ source?: SourceLoc;
99
+ confidence: SourceConfidence;
100
+ /** Outer→inner component ownership chain (React fiber `_debugOwner`). */
101
+ componentStack: Array<{
102
+ name: string;
103
+ source?: SourceLoc;
104
+ }>;
105
+ /** Robust CSS path — always present, the last-resort locator. */
106
+ selector: string;
107
+ }
108
+ interface CaptureBundle {
109
+ schemaVersion: typeof CAPTURE_SCHEMA_VERSION;
110
+ id: string;
111
+ createdAt: string;
112
+ target: CaptureTarget | null;
113
+ domSubtree: SerializedNode;
114
+ computedStyles: Record<string, string>;
115
+ tailwindClasses: string[];
116
+ screenshot?: {
117
+ mime: "image/png";
118
+ dataUrl: string;
119
+ bounds: {
120
+ x: number;
121
+ y: number;
122
+ width: number;
123
+ height: number;
124
+ };
125
+ /** Which capture path produced this screenshot. `rasterise` =
126
+ * html-to-image full-document render + crop (no permission).
127
+ * `display-media` = `getDisplayMedia` OS-compositor capture (one
128
+ * permission per session, pixel-perfect across any content). */
129
+ source?: "rasterise" | "display-media";
130
+ /** Human-readable note when the capture is structurally complete
131
+ * but visually imperfect — e.g. some non-CORS images fell back
132
+ * to a placeholder because the user declined `getDisplayMedia`. */
133
+ qualityNote?: string;
134
+ };
135
+ /** Set (and `screenshot` omitted) when an in-browser rasterise was
136
+ * impossible — e.g. cross-origin media taints the canvas. Honest
137
+ * signal so the UI/sink never shows a blank box claiming success. */
138
+ screenshotUnavailable?: string;
139
+ viewport: {
140
+ w: number;
141
+ h: number;
142
+ dpr: number;
143
+ breakpoint?: string;
144
+ };
145
+ runtime: {
146
+ url: string;
147
+ route?: string;
148
+ console: ConsoleEntry[];
149
+ network: NetworkEntry[];
150
+ errors: RuntimeError[];
151
+ };
152
+ userNote?: string;
153
+ }
154
+ /** Builds bundles from a selection. Implemented by the SDK; the
155
+ * contract is what an extension/Electron vehicle reuses verbatim. */
156
+ interface CaptureCore {
157
+ beginPick(opts?: {
158
+ mode?: SelectionMode;
159
+ }): Promise<SelectionInput>;
160
+ buildBundle(sel: SelectionInput): Promise<CaptureBundle>;
161
+ }
162
+ /** The swap point. v1 local = WebSocketAgentSink (→ companion → agent
163
+ * edits). Future prod = IssueTrackerSink (same bundle → hosted task,
164
+ * no auto-edit). capture-core never knows which. */
165
+ interface CaptureSink {
166
+ readonly kind: string;
167
+ submit(bundle: CaptureBundle): Promise<void>;
168
+ }
169
+ interface IssueDraft {
170
+ title: string;
171
+ /** Markdown summary a human/tracker can read at a glance. */
172
+ body: string;
173
+ /** Full bundle, attached verbatim for the agent-ready future. */
174
+ bundle: CaptureBundle;
175
+ }
176
+ /** Turn a bundle into a tracker-ready draft. Source is included when
177
+ * present (fiber/attribute resolved it client-side) and degrades
178
+ * gracefully to the always-present selector when it isn't. */
179
+ declare function toIssueDraft(bundle: CaptureBundle): IssueDraft;
180
+ /** A CaptureSink that produces an `IssueDraft` and hands it to a
181
+ * caller-supplied delivery fn (download/gh/HTTP — not our concern).
182
+ * Same bundle, different sink: this is the local→prod door. */
183
+ declare class IssueTrackerSink implements CaptureSink {
184
+ private readonly deliver;
185
+ readonly kind = "issue-tracker";
186
+ constructor(deliver: (draft: IssueDraft) => void | Promise<void>);
187
+ submit(bundle: CaptureBundle): Promise<void>;
188
+ }
189
+ interface HelloMsg {
190
+ t: "hello";
191
+ protocolVersion: typeof PROTOCOL_VERSION;
192
+ token: string;
193
+ }
194
+ interface HelloOkMsg {
195
+ t: "hello-ok";
196
+ companionVersion: string;
197
+ }
198
+ interface PingMsg {
199
+ t: "ping";
200
+ nonce: string;
201
+ }
202
+ interface PongMsg {
203
+ t: "pong";
204
+ nonce: string;
205
+ }
206
+ interface ErrorMsg {
207
+ t: "error";
208
+ code: "bad-token" | "bad-origin" | "bad-protocol" | "internal";
209
+ message: string;
210
+ }
211
+ interface CaptureSubmitMsg {
212
+ t: "capture";
213
+ bundle: CaptureBundle;
214
+ }
215
+ /** Companion's resolution of a submitted bundle's source target. */
216
+ interface ResolvedSource {
217
+ /** Repo-relative POSIX path the companion resolved. */
218
+ file: string;
219
+ line: number;
220
+ column: number;
221
+ /** A few lines around `line` from the real file. */
222
+ snippet: string;
223
+ /** Whole component file path, if distinct/known. */
224
+ componentFile?: string;
225
+ }
226
+ interface CaptureResolvedMsg {
227
+ t: "capture-resolved";
228
+ id: string;
229
+ resolved: ResolvedSource | null;
230
+ /** Human note, e.g. why resolution was selector-only. */
231
+ note: string;
232
+ }
233
+ type AgentErrorCode = "not-logged-in" | "api-key-set" | "claude-missing" | "transport" | "internal";
234
+ /** A normalized file edit the agent proposes (never auto-applied). */
235
+ interface ProposedEdit {
236
+ /** Repo-relative POSIX path. */
237
+ file: string;
238
+ /** Whole new file contents (companion diffs vs disk). */
239
+ contents: string;
240
+ /** Optional one-line rationale from the agent. */
241
+ why?: string;
242
+ }
243
+ type AgentEvent = {
244
+ t: "agent-text";
245
+ turnId: string;
246
+ delta: string;
247
+ } | {
248
+ t: "agent-thinking";
249
+ turnId: string;
250
+ note: string;
251
+ }
252
+ /** Live "what it's doing" signal (tool use / phase) — UI progress
253
+ * only, never part of the transcript. */
254
+ | {
255
+ t: "agent-activity";
256
+ turnId: string;
257
+ kind: "tool" | "thinking" | "start";
258
+ label: string;
259
+ } | {
260
+ t: "agent-tool-proposal";
261
+ turnId: string;
262
+ edit: ProposedEdit;
263
+ } | {
264
+ t: "agent-turn-complete";
265
+ turnId: string;
266
+ } | {
267
+ t: "agent-error";
268
+ turnId: string;
269
+ code: AgentErrorCode;
270
+ message: string;
271
+ };
272
+ /** Companion preflight result for the active provider/transport. */
273
+ interface AgentStatusMsg {
274
+ t: "agent-status";
275
+ ready: boolean;
276
+ transport: "cli-headless" | "mcp" | "sdk";
277
+ warnings: string[];
278
+ blockers: string[];
279
+ }
280
+ interface AgentStreamMsg {
281
+ t: "agent-stream";
282
+ event: AgentEvent;
283
+ }
284
+ interface ChangesetProposedMsg {
285
+ t: "changeset-proposed";
286
+ turnId: string;
287
+ files: Array<{
288
+ file: string;
289
+ diff: string;
290
+ bytes: number;
291
+ }>;
292
+ }
293
+ interface ChangesetAppliedMsg {
294
+ t: "changeset-applied";
295
+ turnId: string;
296
+ files: string[];
297
+ checkpointRef: string;
298
+ }
299
+ interface AgentUndoneMsg {
300
+ t: "agent-undone";
301
+ turnId: string;
302
+ restored: string[];
303
+ }
304
+ interface AgentTurnMsg {
305
+ t: "agent-turn";
306
+ turnId: string;
307
+ bundleId: string;
308
+ userMessage: string;
309
+ }
310
+ interface AgentDecisionMsg {
311
+ t: "agent-decision";
312
+ turnId: string;
313
+ decision: "approve" | "reject";
314
+ /** Subset of files to act on; omitted = whole changeset. */
315
+ files?: string[];
316
+ reason?: string;
317
+ }
318
+ interface AgentCancelMsg {
319
+ t: "agent-cancel";
320
+ turnId: string;
321
+ }
322
+ interface AgentUndoMsg {
323
+ t: "agent-undo";
324
+ turnId: string;
325
+ }
326
+ /** Undo every checkpoint applied this session (reverse order). */
327
+ interface AgentUndoSessionMsg {
328
+ t: "agent-undo-session";
329
+ }
330
+ /** Explicit, user-initiated git commit of ONLY the files InSitue
331
+ * applied this session. Never auto; never pushes. */
332
+ interface AgentCommitSessionMsg {
333
+ t: "agent-commit-session";
334
+ message?: string;
335
+ }
336
+ interface AgentSessionUndoneMsg {
337
+ t: "agent-session-undone";
338
+ restored: string[];
339
+ }
340
+ interface AgentSessionCommittedMsg {
341
+ t: "agent-session-committed";
342
+ /** Short commit sha. */
343
+ commit: string;
344
+ files: string[];
345
+ }
346
+ /** Client→server messages. */
347
+ type ClientMessage = HelloMsg | PingMsg | CaptureSubmitMsg | AgentTurnMsg | AgentDecisionMsg | AgentCancelMsg | AgentUndoMsg | AgentUndoSessionMsg | AgentCommitSessionMsg;
348
+ /** Server→client messages. */
349
+ type ServerMessage = HelloOkMsg | PongMsg | ErrorMsg | CaptureResolvedMsg | AgentStatusMsg | AgentStreamMsg | ChangesetProposedMsg | ChangesetAppliedMsg | AgentUndoneMsg | AgentSessionUndoneMsg | AgentSessionCommittedMsg;
350
+
351
+ export { type AgentCancelMsg, type AgentCommitSessionMsg, type AgentDecisionMsg, type AgentErrorCode, type AgentEvent, type AgentSessionCommittedMsg, type AgentSessionUndoneMsg, type AgentStatusMsg, type AgentStreamMsg, type AgentTurnMsg, type AgentUndoMsg, type AgentUndoSessionMsg, type AgentUndoneMsg, CAPTURE_SCHEMA_VERSION, type CaptureBundle, type CaptureCore, type CaptureResolvedMsg, type CaptureSink, type CaptureSubmitMsg, type CaptureTarget, type ChangesetAppliedMsg, type ChangesetProposedMsg, type ClientMessage, type ConsoleEntry, type ErrorMsg, type HelloMsg, type HelloOkMsg, type IssueDraft, IssueTrackerSink, type NetworkEntry, PROTOCOL_VERSION, type PingMsg, type PongMsg, type ProposedEdit, type ResolvedSource, type RuntimeError, type SelectionInput, type SelectionMode, type SerializedNode, type ServerMessage, type SourceConfidence, type SourceLoc, breakpointFor, buildSelector, curateComputedStyles, extractTailwindClasses, resolveTarget, serializeNode, toIssueDraft };
package/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ // src/dom.ts
2
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
3
+ var ATTR_DENY = /^(on|data-insitu)/i;
4
+ var SECRETISH = /(token|secret|key|password|authorization|bearer)/i;
5
+ function serializeNode(el, depth = 3, maxChildren = 12) {
6
+ const attrs = {};
7
+ for (const a of Array.from(el.attributes)) {
8
+ if (ATTR_DENY.test(a.name)) continue;
9
+ const v = SECRETISH.test(a.name) ? "[redacted]" : a.value;
10
+ attrs[a.name] = v.length > 300 ? v.slice(0, 300) + "\u2026" : v;
11
+ }
12
+ const node = {
13
+ tag: el.tagName.toLowerCase(),
14
+ attrs,
15
+ children: []
16
+ };
17
+ const directText = Array.from(el.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent ?? "").join(" ").trim();
18
+ if (directText) node.text = directText.slice(0, 200);
19
+ if (depth > 0) {
20
+ const kids = Array.from(el.children).filter((c) => !SKIP_TAGS.has(c.tagName)).slice(0, maxChildren);
21
+ node.children = kids.map((c) => serializeNode(c, depth - 1, maxChildren));
22
+ }
23
+ return node;
24
+ }
25
+ var STYLE_KEYS = [
26
+ "display",
27
+ "position",
28
+ "boxSizing",
29
+ "width",
30
+ "height",
31
+ "margin",
32
+ "padding",
33
+ "border",
34
+ "flexDirection",
35
+ "alignItems",
36
+ "justifyContent",
37
+ "gap",
38
+ "gridTemplateColumns",
39
+ "color",
40
+ "backgroundColor",
41
+ "fontFamily",
42
+ "fontSize",
43
+ "fontWeight",
44
+ "lineHeight",
45
+ "letterSpacing",
46
+ "textAlign",
47
+ "borderRadius",
48
+ "boxShadow",
49
+ "opacity",
50
+ "zIndex"
51
+ ];
52
+ function curateComputedStyles(el) {
53
+ const cs = getComputedStyle(el);
54
+ const out = {};
55
+ for (const k of STYLE_KEYS) {
56
+ const v = cs[k];
57
+ if (typeof v === "string" && v && v !== "normal" && v !== "none") {
58
+ out[k] = v;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function extractTailwindClasses(el) {
64
+ const cls = typeof el.className === "string" ? el.className : el.getAttribute("class") ?? "";
65
+ return cls.split(/\s+/).filter(Boolean);
66
+ }
67
+ function buildSelector(el) {
68
+ if (el.id && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) {
69
+ return `#${CSS.escape(el.id)}`;
70
+ }
71
+ const parts = [];
72
+ let cur = el;
73
+ while (cur && cur.nodeType === Node.ELEMENT_NODE && parts.length < 6) {
74
+ const tid = cur.getAttribute("data-testid");
75
+ if (tid) {
76
+ parts.unshift(`[data-testid="${CSS.escape(tid)}"]`);
77
+ break;
78
+ }
79
+ const tag = cur.tagName.toLowerCase();
80
+ const parent = cur.parentElement;
81
+ if (!parent) {
82
+ parts.unshift(tag);
83
+ break;
84
+ }
85
+ const sibs = Array.from(parent.children).filter(
86
+ (c) => c.tagName === cur.tagName
87
+ );
88
+ const idx = sibs.indexOf(cur);
89
+ parts.unshift(
90
+ sibs.length > 1 ? `${tag}:nth-of-type(${idx + 1})` : tag
91
+ );
92
+ if (cur.id) {
93
+ parts.unshift(`#${CSS.escape(cur.id)}`);
94
+ break;
95
+ }
96
+ cur = parent;
97
+ }
98
+ return parts.join(" > ");
99
+ }
100
+ function breakpointFor(w) {
101
+ if (w < 640) return "xs";
102
+ if (w < 768) return "sm";
103
+ if (w < 1024) return "md";
104
+ if (w < 1280) return "lg";
105
+ if (w < 1536) return "xl";
106
+ return "2xl";
107
+ }
108
+
109
+ // src/react-source.ts
110
+ function getFiber(el) {
111
+ for (const k of Object.keys(el)) {
112
+ if (k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")) {
113
+ return el[k] ?? null;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ function compName(type) {
119
+ if (typeof type === "function") {
120
+ const f = type;
121
+ return f.displayName || f.name || "Anonymous";
122
+ }
123
+ if (type && typeof type === "object") {
124
+ const o = type;
125
+ return o.displayName || o.render?.name || null;
126
+ }
127
+ return null;
128
+ }
129
+ function toLoc(workspaceCwdRelative) {
130
+ if (!workspaceCwdRelative.fileName) return null;
131
+ return {
132
+ // React gives an absolute path in dev; the companion re-roots it.
133
+ file: workspaceCwdRelative.fileName,
134
+ line: workspaceCwdRelative.lineNumber ?? 1,
135
+ column: workspaceCwdRelative.columnNumber ?? 1
136
+ };
137
+ }
138
+ function fromAttribute(el) {
139
+ let cur = el;
140
+ for (let i = 0; cur && i < 8; i++, cur = cur.parentElement) {
141
+ const raw = cur.getAttribute("data-insitu-source");
142
+ if (raw) {
143
+ const m = /^(.*):(\d+):(\d+)$/.exec(raw);
144
+ if (m) return { file: m[1], line: Number(m[2]), column: Number(m[3]) };
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+ function resolveTarget(el) {
150
+ const selector = buildSelector(el);
151
+ const fiber = getFiber(el);
152
+ const componentStack = [];
153
+ if (fiber) {
154
+ let f = fiber;
155
+ let guard = 0;
156
+ while (f && guard++ < 60) {
157
+ const name = compName(f.type);
158
+ if (name) {
159
+ const src = f._debugSource ? toLoc(f._debugSource) : null;
160
+ componentStack.push(
161
+ src ? { name, source: src } : { name }
162
+ );
163
+ }
164
+ f = f._debugOwner ?? f.return ?? null;
165
+ }
166
+ }
167
+ let source;
168
+ let confidence = "selector-only";
169
+ const hostSrc = fiber?._debugSource ? toLoc(fiber._debugSource) : null;
170
+ if (hostSrc) {
171
+ source = hostSrc;
172
+ confidence = "exact";
173
+ } else {
174
+ const attrSrc = fromAttribute(el);
175
+ if (attrSrc) {
176
+ source = attrSrc;
177
+ confidence = "exact";
178
+ } else {
179
+ const ownerWithSrc = componentStack.find((c) => c.source);
180
+ if (ownerWithSrc?.source) {
181
+ source = ownerWithSrc.source;
182
+ confidence = "approximate";
183
+ }
184
+ }
185
+ }
186
+ return source === void 0 ? { confidence, componentStack, selector } : { source, confidence, componentStack, selector };
187
+ }
188
+
189
+ // src/index.ts
190
+ var CAPTURE_SCHEMA_VERSION = 3;
191
+ var PROTOCOL_VERSION = 4;
192
+ function toIssueDraft(bundle) {
193
+ const t = bundle.target;
194
+ const where = t?.source ? `\`${t.source.file}:${t.source.line}\` (${t.confidence})` : t ? `\`${t.selector}\` (selector-only \u2014 no source resolver)` : "(empty selection)";
195
+ const stack = t?.componentStack.map((c) => c.name).join(" < ") || "(none)";
196
+ const errs = bundle.runtime.errors.length;
197
+ const title = `[InSitue] ${t?.componentStack[0]?.name ?? t?.selector ?? "selection"} on ${bundle.runtime.route ?? new URL(bundle.runtime.url).pathname}`;
198
+ const body = [
199
+ `**Where:** ${where}`,
200
+ `**Components:** ${stack}`,
201
+ `**URL:** ${bundle.runtime.url}`,
202
+ `**Viewport:** ${bundle.viewport.w}\xD7${bundle.viewport.h}${bundle.viewport.breakpoint ? ` (${bundle.viewport.breakpoint})` : ""}`,
203
+ `**Tailwind:** ${bundle.tailwindClasses.join(" ") || "\u2014"}`,
204
+ `**Runtime:** ${bundle.runtime.console.length} log \xB7 ${bundle.runtime.network.length} net \xB7 ${errs} err`,
205
+ `**Screenshot:** ${bundle.screenshot ? `attached` + (bundle.screenshot.source ? ` (${bundle.screenshot.source})` : "") + (bundle.screenshot.qualityNote ? ` \u2014 ${bundle.screenshot.qualityNote}` : "") : bundle.screenshotUnavailable ? `unavailable \u2014 ${bundle.screenshotUnavailable}` : "\u2014"}`,
206
+ bundle.userNote ? `
207
+ > ${bundle.userNote}` : "",
208
+ `
209
+ _Captured ${bundle.createdAt} \xB7 schema v${bundle.schemaVersion}_`
210
+ ].join("\n");
211
+ return { title, body, bundle };
212
+ }
213
+ var IssueTrackerSink = class {
214
+ constructor(deliver) {
215
+ this.deliver = deliver;
216
+ }
217
+ deliver;
218
+ kind = "issue-tracker";
219
+ async submit(bundle) {
220
+ await this.deliver(toIssueDraft(bundle));
221
+ }
222
+ };
223
+ export {
224
+ CAPTURE_SCHEMA_VERSION,
225
+ IssueTrackerSink,
226
+ PROTOCOL_VERSION,
227
+ breakpointFor,
228
+ buildSelector,
229
+ curateComputedStyles,
230
+ extractTailwindClasses,
231
+ resolveTarget,
232
+ serializeNode,
233
+ toIssueDraft
234
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@insitue/capture-core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "The seam: pure, transport/agent/fs-free capture model shared by every InSitue vehicle.",
9
+ "license": "MIT",
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "devDependencies": {
23
+ "tsup": "^8.3.5",
24
+ "typescript": "^5.6.3"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup src/index.ts --format esm --dts --clean",
28
+ "dev": "tsup src/index.ts --format esm --dts --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "tsc --noEmit"
31
+ }
32
+ }