@pinta-ai/pinta-copilot 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +136 -0
  3. package/README.md +116 -0
  4. package/dist/core/config.d.ts +15 -0
  5. package/dist/core/config.js +19 -0
  6. package/dist/core/config.js.map +1 -0
  7. package/dist/core/env-bridge.d.ts +25 -0
  8. package/dist/core/env-bridge.js +43 -0
  9. package/dist/core/env-bridge.js.map +1 -0
  10. package/dist/core/guard.d.ts +14 -0
  11. package/dist/core/guard.js +48 -0
  12. package/dist/core/guard.js.map +1 -0
  13. package/dist/core/otlp.d.ts +55 -0
  14. package/dist/core/otlp.js +184 -0
  15. package/dist/core/otlp.js.map +1 -0
  16. package/dist/core/redact.d.ts +57 -0
  17. package/dist/core/redact.js +149 -0
  18. package/dist/core/redact.js.map +1 -0
  19. package/dist/core/retry-queue.d.ts +26 -0
  20. package/dist/core/retry-queue.js +127 -0
  21. package/dist/core/retry-queue.js.map +1 -0
  22. package/dist/core/surface.d.ts +21 -0
  23. package/dist/core/surface.js +13 -0
  24. package/dist/core/surface.js.map +1 -0
  25. package/dist/core/trace.d.ts +20 -0
  26. package/dist/core/trace.js +80 -0
  27. package/dist/core/trace.js.map +1 -0
  28. package/dist/core/transport.d.ts +18 -0
  29. package/dist/core/transport.js +127 -0
  30. package/dist/core/transport.js.map +1 -0
  31. package/dist/core/types.d.ts +49 -0
  32. package/dist/core/types.js +124 -0
  33. package/dist/core/types.js.map +1 -0
  34. package/dist/env-file.d.ts +4 -0
  35. package/dist/env-file.js +69 -0
  36. package/dist/env-file.js.map +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +71 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/tools/doctor.d.ts +1 -0
  41. package/dist/tools/doctor.js +81 -0
  42. package/dist/tools/doctor.js.map +1 -0
  43. package/dist/tools/install-hooks.d.ts +1 -0
  44. package/dist/tools/install-hooks.js +101 -0
  45. package/dist/tools/install-hooks.js.map +1 -0
  46. package/package.json +38 -0
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ulidToTraceId = ulidToTraceId;
7
+ exports.newSpanId = newSpanId;
8
+ exports.buildOtlpPayload = buildOtlpPayload;
9
+ exports.mergeBatch = mergeBatch;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const redact_js_1 = require("./redact.js");
13
+ const types_js_1 = require("./types.js");
14
+ const SDK_VERSION = "0.1.0"; // keep in sync with package.json
15
+ /**
16
+ * Copilot CLI/ext version isn't reliably discoverable from the hook process
17
+ * env. Read an optional `COPILOT_CLI_VERSION` override, else "unknown" — a
18
+ * missing version must never fail the hook.
19
+ */
20
+ function getCopilotVersion() {
21
+ return process.env.COPILOT_CLI_VERSION || "unknown";
22
+ }
23
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
24
+ /**
25
+ * Convert a 26-char Crockford ULID into 32 lowercase hex chars (16 bytes)
26
+ * suitable for an OTLP traceId.
27
+ */
28
+ function ulidToTraceId(ulid) {
29
+ if (ulid.length !== 26) {
30
+ throw new Error(`ulidToTraceId: expected 26 chars, got ${ulid.length}`);
31
+ }
32
+ let n = 0n;
33
+ for (const ch of ulid) {
34
+ const idx = CROCKFORD.indexOf(ch);
35
+ if (idx < 0)
36
+ throw new Error(`ulidToTraceId: invalid Crockford char "${ch}"`);
37
+ n = (n << 5n) | BigInt(idx);
38
+ }
39
+ const mask = (1n << 128n) - 1n;
40
+ n &= mask;
41
+ return n.toString(16).padStart(32, "0");
42
+ }
43
+ /** Generate a fresh 16-hex-char (8-byte) span ID. */
44
+ function newSpanId() {
45
+ return crypto_1.default.randomBytes(8).toString("hex");
46
+ }
47
+ /**
48
+ * Identifier/enum keys for which redaction (Tier 1) is skipped (truncation
49
+ * still applies). Both snake (CLI/ext) and camel (permissionRequest) casings
50
+ * are listed since Bronze flattening preserves the incoming key name.
51
+ */
52
+ const SKIP_REDACT_KEYS = new Set([
53
+ "copilot.hook",
54
+ "copilot.tool_name", "copilot.toolName",
55
+ "copilot.tool_use_id", "copilot.toolUseId",
56
+ "copilot.session_id", "copilot.sessionId",
57
+ "copilot.transcript_path", "copilot.transcriptPath",
58
+ "copilot.cwd",
59
+ "copilot.permission_mode",
60
+ "copilot.surface",
61
+ "copilot.agent_id", "copilot.agent_type",
62
+ "copilot.agent_name", "copilot.agent_display_name",
63
+ "copilot.stop_reason", "copilot.notification_type",
64
+ ]);
65
+ /** Keys that may carry shell command / tool payload text → bash redaction context. */
66
+ const BASH_CONTEXT_KEYS = new Set([
67
+ "copilot.tool_input", "copilot.toolInput",
68
+ "copilot.tool_response", "copilot.tool_result",
69
+ ]);
70
+ function maybeRedactString(key, raw) {
71
+ const truncated = (0, redact_js_1.truncate)(raw);
72
+ if (SKIP_REDACT_KEYS.has(key))
73
+ return truncated;
74
+ const context = BASH_CONTEXT_KEYS.has(key) ? "bash" : undefined;
75
+ return (0, redact_js_1.redact)(truncated, { context });
76
+ }
77
+ /** Convert a JS value into an OTLP attribute value. Returns null to omit. */
78
+ function toOtlpValue(key, v) {
79
+ if (v === null || v === undefined)
80
+ return null;
81
+ switch (typeof v) {
82
+ case "string":
83
+ return { stringValue: maybeRedactString(key, v) };
84
+ case "boolean":
85
+ return { boolValue: v };
86
+ case "number":
87
+ if (Number.isInteger(v))
88
+ return { intValue: v };
89
+ return { doubleValue: v };
90
+ case "object":
91
+ try {
92
+ return { stringValue: maybeRedactString(key, JSON.stringify(v)) };
93
+ }
94
+ catch {
95
+ return { stringValue: maybeRedactString(key, String(v)) };
96
+ }
97
+ default:
98
+ return { stringValue: maybeRedactString(key, String(v)) };
99
+ }
100
+ }
101
+ function snakeCase(name) {
102
+ return name
103
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
104
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
105
+ .toLowerCase();
106
+ }
107
+ // Discriminator keys covered by `copilot.hook` — don't re-emit them raw.
108
+ const DISCRIMINATOR_KEYS = new Set(["hook_event_name", "hookEventName", "hookName"]);
109
+ function flattenEvent(event, surface) {
110
+ const out = [];
111
+ // Discriminator first so aware-backend's detectIngestType hits it cheaply.
112
+ out.push({ key: "ingest.type", value: { stringValue: "copilot" } });
113
+ // Canonical hook name regardless of incoming discriminator key (snake/camel/hookName).
114
+ out.push({ key: "copilot.hook", value: { stringValue: (0, types_js_1.eventName)(event) ?? "unknown" } });
115
+ // Runtime surface label (cli | ext | cloud).
116
+ out.push({ key: "copilot.surface", value: { stringValue: surface } });
117
+ for (const [k, v] of Object.entries(event)) {
118
+ if (DISCRIMINATOR_KEYS.has(k))
119
+ continue; // covered by copilot.hook
120
+ const key = `copilot.${k}`;
121
+ const value = toOtlpValue(key, v);
122
+ if (value === null)
123
+ continue;
124
+ out.push({ key, value });
125
+ }
126
+ return out;
127
+ }
128
+ function resourceAttrs() {
129
+ return [
130
+ { key: "service.name", value: { stringValue: "copilot" } },
131
+ { key: "service.version", value: { stringValue: getCopilotVersion() } },
132
+ { key: "telemetry.sdk.name", value: { stringValue: "pinta-copilot" } },
133
+ { key: "telemetry.sdk.language", value: { stringValue: "nodejs" } },
134
+ { key: "telemetry.sdk.version", value: { stringValue: SDK_VERSION } },
135
+ { key: "process.pid", value: { intValue: process.pid } },
136
+ { key: "process.owner", value: { stringValue: os_1.default.userInfo().username } },
137
+ { key: "host.name", value: { stringValue: os_1.default.hostname() } },
138
+ { key: "host.arch", value: { stringValue: os_1.default.arch() } },
139
+ ];
140
+ }
141
+ function buildOtlpPayload(args) {
142
+ const ts = args.now ?? Date.now();
143
+ const tsNano = (BigInt(ts) * 1000000n).toString();
144
+ const attrs = flattenEvent(args.event, args.surface);
145
+ if (args.guard) {
146
+ attrs.push({ key: "pinta.guard.decision", value: { stringValue: args.guard.decision.toLowerCase() } }, { key: "pinta.guard.duration_ms", value: { intValue: args.guard.durationMs } });
147
+ if (args.guard.reason) {
148
+ attrs.push({ key: "pinta.guard.matched_rule", value: { stringValue: args.guard.reason } });
149
+ }
150
+ if (args.guard.failOpenReason) {
151
+ attrs.push({ key: "pinta.guard.fail_open_reason", value: { stringValue: args.guard.failOpenReason } });
152
+ }
153
+ }
154
+ const span = {
155
+ traceId: ulidToTraceId(args.traceId),
156
+ spanId: newSpanId(),
157
+ name: `copilot.${snakeCase((0, types_js_1.eventName)(args.event) ?? "unknown")}`,
158
+ kind: 1, // SPAN_KIND_INTERNAL
159
+ startTimeUnixNano: tsNano,
160
+ endTimeUnixNano: tsNano,
161
+ attributes: attrs,
162
+ };
163
+ return {
164
+ resourceSpans: [
165
+ {
166
+ resource: { attributes: resourceAttrs() },
167
+ scopeSpans: [
168
+ {
169
+ scope: { name: "pinta-copilot", version: SDK_VERSION },
170
+ spans: [span],
171
+ },
172
+ ],
173
+ },
174
+ ],
175
+ };
176
+ }
177
+ /** Concatenate per-hook payloads' resourceSpans into one OTLP payload. */
178
+ function mergeBatch(payloads) {
179
+ const out = [];
180
+ for (const p of payloads)
181
+ out.push(...p.resourceSpans);
182
+ return { resourceSpans: out };
183
+ }
184
+ //# sourceMappingURL=otlp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp.js","sourceRoot":"","sources":["../../src/core/otlp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ export interface RedactOptions {
2
+ /**
3
+ * Optional context hint that enables context-gated patterns. Currently
4
+ * only "bash" enables `cli_password_short` (-p<pass>) since matching it
5
+ * outside Bash command text is too noisy.
6
+ */
7
+ context?: "bash";
8
+ }
9
+ /** Tier 3 cap: per-string byte limit before truncation. */
10
+ export declare const MAX_BYTES = 102400;
11
+ /**
12
+ * Truncate input by UTF-8 byte length. Appends `…[TRUNCATED:<origByteLen>]`
13
+ * to indicate elision. Returns input unchanged when within cap.
14
+ *
15
+ * Uses Buffer for accurate byte counting; slices on byte boundary then
16
+ * decodes — may produce a U+FFFD if the boundary lands mid-codepoint, which
17
+ * is acceptable for telemetry.
18
+ */
19
+ export declare function truncate(input: string): string;
20
+ export interface Pattern {
21
+ /** Token type printed inside `[REDACTED:<type>]`. */
22
+ type: string;
23
+ /** Regex with global flag (`g`). Multiline (`m`) for line-anchored ones. */
24
+ regex: RegExp;
25
+ /**
26
+ * Capture group index whose substring is replaced. 0 (default) = whole match.
27
+ * Use a positive group when the pattern brackets context that should be
28
+ * preserved (e.g. `postgres://user:<pwd>@` keeps the URL shape intact).
29
+ */
30
+ captureGroup?: number;
31
+ /**
32
+ * If set, this pattern only applies when `RedactOptions.context` equals
33
+ * the listed value. Used for false-positive-prone shapes.
34
+ */
35
+ requireContext?: "bash";
36
+ }
37
+ export declare const PATTERNS: ReadonlyArray<Pattern>;
38
+ interface Match {
39
+ start: number;
40
+ end: number;
41
+ replaceStart: number;
42
+ replaceEnd: number;
43
+ type: string;
44
+ }
45
+ export declare function collectMatches(input: string, opts: RedactOptions): Match[];
46
+ export declare function resolveOverlaps(matches: Match[]): Match[];
47
+ export declare function applyMatches(input: string, matches: Match[]): string;
48
+ /**
49
+ * Mask high-confidence secret patterns in `input`. Returns a new string with
50
+ * matched substrings replaced by `[REDACTED:<type>]`.
51
+ *
52
+ * - `opts.context = "bash"` enables context-gated patterns.
53
+ * - Truncation is NOT applied here — see `truncate()`. The caller decides
54
+ * the order (spec §3 Tier 3: truncate first, then redact).
55
+ */
56
+ export declare function redact(input: string, opts?: RedactOptions): string;
57
+ export {};
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PATTERNS = exports.MAX_BYTES = void 0;
4
+ exports.truncate = truncate;
5
+ exports.collectMatches = collectMatches;
6
+ exports.resolveOverlaps = resolveOverlaps;
7
+ exports.applyMatches = applyMatches;
8
+ exports.redact = redact;
9
+ /** Tier 3 cap: per-string byte limit before truncation. */
10
+ exports.MAX_BYTES = 102400;
11
+ /**
12
+ * Truncate input by UTF-8 byte length. Appends `…[TRUNCATED:<origByteLen>]`
13
+ * to indicate elision. Returns input unchanged when within cap.
14
+ *
15
+ * Uses Buffer for accurate byte counting; slices on byte boundary then
16
+ * decodes — may produce a U+FFFD if the boundary lands mid-codepoint, which
17
+ * is acceptable for telemetry.
18
+ */
19
+ function truncate(input) {
20
+ const buf = Buffer.from(input, "utf-8");
21
+ if (buf.length <= exports.MAX_BYTES)
22
+ return input;
23
+ const head = buf.subarray(0, exports.MAX_BYTES).toString("utf-8");
24
+ return `${head}…[TRUNCATED:${buf.length}]`;
25
+ }
26
+ exports.PATTERNS = [
27
+ { type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
28
+ {
29
+ type: "aws_secret_key",
30
+ // Context word `aws_secret`/`AWS_SECRET` (with optional separator) followed
31
+ // by an assignment-ish character then a 40-char base64-ish blob.
32
+ regex: /(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,
33
+ captureGroup: 1,
34
+ },
35
+ {
36
+ type: "gcp_service_account",
37
+ // Whole JSON blob starting with the service-account discriminator.
38
+ regex: /\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g,
39
+ },
40
+ { type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
41
+ { type: "gitlab_token", regex: /glpat-[A-Za-z0-9_-]{20}/g },
42
+ { type: "slack_token", regex: /xox[abrsp]-[0-9A-Za-z-]{10,}/g },
43
+ { type: "openai_key", regex: /sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g },
44
+ { type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9_-]{50,}/g },
45
+ { type: "stripe_key", regex: /(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g },
46
+ {
47
+ type: "jwt",
48
+ regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
49
+ },
50
+ {
51
+ type: "private_key_block",
52
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
53
+ },
54
+ { type: "bearer_token", regex: /bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi, captureGroup: 1 },
55
+ { type: "basic_auth", regex: /basic\s+([A-Za-z0-9+/=]{12,})/gi, captureGroup: 1 },
56
+ {
57
+ type: "db_url_password",
58
+ regex: /\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,
59
+ captureGroup: 1,
60
+ },
61
+ {
62
+ type: "cli_password_flag",
63
+ regex: /(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,
64
+ captureGroup: 1,
65
+ },
66
+ {
67
+ type: "cli_password_short",
68
+ // mysql -p<pass>; only on bash context.
69
+ regex: /\s-p([^\s'"]+)/g,
70
+ captureGroup: 1,
71
+ requireContext: "bash",
72
+ },
73
+ {
74
+ type: "env_var_secret",
75
+ // Known false positive: trailing `[A-Z0-9_]*` is greedy, so names like
76
+ // `OPENAI_API_KEY_DESCRIPTION=Used` still match. Acceptable for Bronze.
77
+ regex: /^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,
78
+ captureGroup: 2,
79
+ },
80
+ ];
81
+ function collectMatches(input, opts) {
82
+ const out = [];
83
+ for (const pattern of exports.PATTERNS) {
84
+ if (pattern.requireContext && pattern.requireContext !== opts.context)
85
+ continue;
86
+ const re = new RegExp(pattern.regex.source, pattern.regex.flags);
87
+ let m;
88
+ while ((m = re.exec(input)) !== null) {
89
+ const cg = pattern.captureGroup ?? 0;
90
+ const captured = m[cg];
91
+ if (captured === undefined) {
92
+ if (m.index === re.lastIndex)
93
+ re.lastIndex++;
94
+ continue;
95
+ }
96
+ const start = m.index;
97
+ const end = m.index + m[0].length;
98
+ const replaceStart = start + m[0].indexOf(captured);
99
+ const replaceEnd = replaceStart + captured.length;
100
+ out.push({ start, end, replaceStart, replaceEnd, type: pattern.type });
101
+ if (m.index === re.lastIndex)
102
+ re.lastIndex++; // guard against zero-length
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ function resolveOverlaps(matches) {
108
+ const sorted = [...matches].sort((a, b) => {
109
+ if (a.start !== b.start)
110
+ return a.start - b.start;
111
+ return (b.end - b.start) - (a.end - a.start); // longer whole-match wins on tie
112
+ });
113
+ const kept = [];
114
+ let lastEnd = -1;
115
+ for (const m of sorted) {
116
+ if (m.start < lastEnd)
117
+ continue; // overlapping later match dropped
118
+ kept.push(m);
119
+ lastEnd = m.end;
120
+ }
121
+ return kept;
122
+ }
123
+ function applyMatches(input, matches) {
124
+ // Apply right-to-left so earlier indices remain valid.
125
+ const sorted = [...matches].sort((a, b) => b.replaceStart - a.replaceStart);
126
+ let out = input;
127
+ for (const m of sorted) {
128
+ out = out.slice(0, m.replaceStart) + `[REDACTED:${m.type}]` + out.slice(m.replaceEnd);
129
+ }
130
+ return out;
131
+ }
132
+ /**
133
+ * Mask high-confidence secret patterns in `input`. Returns a new string with
134
+ * matched substrings replaced by `[REDACTED:<type>]`.
135
+ *
136
+ * - `opts.context = "bash"` enables context-gated patterns.
137
+ * - Truncation is NOT applied here — see `truncate()`. The caller decides
138
+ * the order (spec §3 Tier 3: truncate first, then redact).
139
+ */
140
+ function redact(input, opts = {}) {
141
+ if (input.length === 0)
142
+ return input;
143
+ const all = collectMatches(input, opts);
144
+ if (all.length === 0)
145
+ return input;
146
+ const kept = resolveOverlaps(all);
147
+ return applyMatches(input, kept);
148
+ }
149
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/core/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { OtlpPayload } from "./otlp.js";
2
+ export interface QueueEntry {
3
+ savedAt: string;
4
+ payload: OtlpPayload;
5
+ }
6
+ export declare class RetryQueue {
7
+ private filePath;
8
+ private lockPath;
9
+ constructor(pluginData: string);
10
+ /** Append a single payload. Best-effort: any IO error is swallowed (logged to stderr). */
11
+ enqueue(payload: OtlpPayload): void;
12
+ /**
13
+ * Read all entries oldest-first. Returns [] if the file does not exist or is unreadable.
14
+ * Does NOT delete the file — callers handle persistence via `rewrite`.
15
+ */
16
+ readAll(): QueueEntry[];
17
+ /** Replace the queue with the given entries (or delete the file when empty). */
18
+ rewrite(entries: QueueEntry[]): void;
19
+ /**
20
+ * Try to acquire the lock for ~LOCK_TIMEOUT_MS. Returns true on success.
21
+ * Caller MUST call `release()` if true is returned.
22
+ */
23
+ tryAcquireLock(): boolean;
24
+ release(): void;
25
+ private trim;
26
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RetryQueue = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const MAX_ENTRIES = 1000;
10
+ const LOCK_TIMEOUT_MS = 50;
11
+ const LOCK_POLL_MS = 5;
12
+ class RetryQueue {
13
+ filePath;
14
+ lockPath;
15
+ constructor(pluginData) {
16
+ this.filePath = path_1.default.join(pluginData, "failed-spans.jsonl");
17
+ this.lockPath = this.filePath + ".lock";
18
+ }
19
+ /** Append a single payload. Best-effort: any IO error is swallowed (logged to stderr). */
20
+ enqueue(payload) {
21
+ try {
22
+ fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
23
+ const line = JSON.stringify({ savedAt: new Date().toISOString(), payload }) + "\n";
24
+ fs_1.default.appendFileSync(this.filePath, line);
25
+ this.trim();
26
+ }
27
+ catch (err) {
28
+ process.stderr.write(`[pinta-copilot] retry-queue enqueue failed: ${err}\n`);
29
+ }
30
+ }
31
+ /**
32
+ * Read all entries oldest-first. Returns [] if the file does not exist or is unreadable.
33
+ * Does NOT delete the file — callers handle persistence via `rewrite`.
34
+ */
35
+ readAll() {
36
+ try {
37
+ const raw = fs_1.default.readFileSync(this.filePath, "utf-8");
38
+ const out = [];
39
+ for (const line of raw.split("\n")) {
40
+ if (!line.trim())
41
+ continue;
42
+ try {
43
+ out.push(JSON.parse(line));
44
+ }
45
+ catch {
46
+ // skip malformed line
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
55
+ /** Replace the queue with the given entries (or delete the file when empty). */
56
+ rewrite(entries) {
57
+ try {
58
+ if (entries.length === 0) {
59
+ if (fs_1.default.existsSync(this.filePath))
60
+ fs_1.default.unlinkSync(this.filePath);
61
+ return;
62
+ }
63
+ fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
64
+ fs_1.default.writeFileSync(this.filePath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n");
65
+ }
66
+ catch (err) {
67
+ process.stderr.write(`[pinta-copilot] retry-queue rewrite failed: ${err}\n`);
68
+ }
69
+ }
70
+ /**
71
+ * Try to acquire the lock for ~LOCK_TIMEOUT_MS. Returns true on success.
72
+ * Caller MUST call `release()` if true is returned.
73
+ */
74
+ tryAcquireLock() {
75
+ const start = Date.now();
76
+ fs_1.default.mkdirSync(path_1.default.dirname(this.lockPath), { recursive: true });
77
+ while (Date.now() - start < LOCK_TIMEOUT_MS) {
78
+ try {
79
+ const fd = fs_1.default.openSync(this.lockPath, "wx");
80
+ fs_1.default.writeSync(fd, String(process.pid));
81
+ fs_1.default.closeSync(fd);
82
+ return true;
83
+ }
84
+ catch (err) {
85
+ if (err?.code !== "EEXIST") {
86
+ process.stderr.write(`[pinta-copilot] retry-queue lock open failed: ${err}\n`);
87
+ return false;
88
+ }
89
+ // Stale lock detection: if mtime is older than 30s, drop it.
90
+ try {
91
+ const st = fs_1.default.statSync(this.lockPath);
92
+ if (Date.now() - st.mtimeMs > 30_000) {
93
+ fs_1.default.unlinkSync(this.lockPath);
94
+ continue;
95
+ }
96
+ }
97
+ catch {
98
+ /* ignore */
99
+ }
100
+ const wait = LOCK_POLL_MS;
101
+ const end = Date.now() + wait;
102
+ while (Date.now() < end) {
103
+ /* spin briefly; sync only because hooks are short-lived */
104
+ }
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ release() {
110
+ try {
111
+ fs_1.default.unlinkSync(this.lockPath);
112
+ }
113
+ catch {
114
+ /* already gone */
115
+ }
116
+ }
117
+ trim() {
118
+ const entries = this.readAll();
119
+ if (entries.length <= MAX_ENTRIES)
120
+ return;
121
+ const drop = entries.length - MAX_ENTRIES;
122
+ process.stderr.write(`[pinta-copilot] retry-queue full, dropping ${drop} oldest entries\n`);
123
+ this.rewrite(entries.slice(drop));
124
+ }
125
+ }
126
+ exports.RetryQueue = RetryQueue;
127
+ //# sourceMappingURL=retry-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-queue.js","sourceRoot":"","sources":["../../src/core/retry-queue.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,gDAAwB;AAGxB,MAAM,WAAW,GAAG,IAAI,CAAC;AACzB,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,YAAY,GAAG,CAAC,CAAC;AAOvB,MAAa,UAAU;IACb,QAAQ,CAAS;IACjB,QAAQ,CAAS;IAEzB,YAAY,UAAkB;QAC5B,IAAI,CAAC,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1C,CAAC;IAED,0FAA0F;IAC1F,OAAO,CAAC,OAAoB;QAC1B,IAAI,CAAC;YACH,YAAE,CAAC,SAAS,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;YACnF,YAAE,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,GAAG,IAAI,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpD,MAAM,GAAG,GAAiB,EAAE,CAAC;YAC7B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;oBAAE,SAAS;gBAC3B,IAAI,CAAC;oBACH,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC7B,CAAC;gBAAC,MAAM,CAAC;oBACP,sBAAsB;gBACxB,CAAC;YACH,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,gFAAgF;IAChF,OAAO,CAAC,OAAqB;QAC3B,IAAI,CAAC;YACH,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,IAAI,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YACD,YAAE,CAAC,SAAS,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,YAAE,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,GAAG,IAAI,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,YAAE,CAAC,SAAS,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,eAAe,EAAE,CAAC;YAC5C,IAAI,CAAC;gBACH,MAAM,EAAE,GAAG,YAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;gBAC5C,YAAE,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;gBACtC,YAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACjB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,IAAI,GAAG,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iDAAiD,GAAG,IAAI,CAAC,CAAC;oBAC/E,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,6DAA6D;gBAC7D,IAAI,CAAC;oBACH,MAAM,EAAE,GAAG,YAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBACtC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,GAAG,MAAM,EAAE,CAAC;wBACrC,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;wBAC7B,SAAS;oBACX,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,YAAY;gBACd,CAAC;gBACD,MAAM,IAAI,GAAG,YAAY,CAAC;gBAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;gBAC9B,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;oBACxB,2DAA2D;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO;QACL,IAAI,CAAC;YACH,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;IACH,CAAC;IAEO,IAAI;QACV,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;YAAE,OAAO;QAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC;QAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,IAAI,mBAAmB,CAAC,CAAC;QAC5F,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,CAAC;CACF;AA9GD,gCA8GC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Runtime surface detection — which GitHub Copilot host spawned this hook.
3
+ *
4
+ * A single `pinta-copilot` adapter / a single `~/.copilot/hooks/` file fires
5
+ * across three surfaces (BACKGROUND_RESEARCH §9, D11/D14). We label every span
6
+ * with `copilot.surface` so the backend can distinguish them. The hook is a
7
+ * spawned child process, so we read the inherited `process.env`.
8
+ *
9
+ * Detection is empirically validated (2026-06-08, KU9):
10
+ * - cloud: Copilot cloud agent injects `COPILOT_AGENT_*`.
11
+ * - ext : the VS Code extension host sets `ELECTRON_RUN_AS_NODE` /
12
+ * `VSCODE_PID` / `VSCODE_IPC_HOOK` on its node children.
13
+ * - cli : neither.
14
+ *
15
+ * ⚠️ `TERM_PROGRAM` is deliberately NOT used: VS Code's *integrated terminal*
16
+ * sets `TERM_PROGRAM=vscode` for a standalone CLI run (would misclassify as
17
+ * ext), and a real ext host inherits whatever launched VS Code (e.g. ghostty)
18
+ * — unreliable in both directions. `ELECTRON_RUN_AS_NODE` is the robust signal.
19
+ */
20
+ export type Surface = "cli" | "cloud" | "ext";
21
+ export declare function detectSurface(env?: NodeJS.ProcessEnv): Surface;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectSurface = detectSurface;
4
+ function detectSurface(env = process.env) {
5
+ if (env.COPILOT_AGENT_JOB_ID || env.COPILOT_AGENT_SESSION_ID || env.COPILOT_AGENT_PROMPT) {
6
+ return "cloud";
7
+ }
8
+ if (env.ELECTRON_RUN_AS_NODE || env.VSCODE_IPC_HOOK || env.VSCODE_PID) {
9
+ return "ext";
10
+ }
11
+ return "cli";
12
+ }
13
+ //# sourceMappingURL=surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface.js","sourceRoot":"","sources":["../../src/core/surface.ts"],"names":[],"mappings":";;AAqBA,sCAQC;AARD,SAAgB,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,IAAI,GAAG,CAAC,oBAAoB,IAAI,GAAG,CAAC,wBAAwB,IAAI,GAAG,CAAC,oBAAoB,EAAE,CAAC;QACzF,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,GAAG,CAAC,oBAAoB,IAAI,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { PintaConfig } from "./config.js";
2
+ /**
3
+ * Per-turn trace correlation, keyed by `session_id`.
4
+ *
5
+ * `UserPromptSubmit` starts a new ULID trace for its session; subsequent hooks
6
+ * in the same turn reuse it. Keying by session_id (not a single global file)
7
+ * is required because CLI and the VS Code extension can run concurrently and
8
+ * each emits its own session — verified that `session_id` is stable across a
9
+ * turn on both surfaces (H10). The store is a `{ [sessionId]: traceId }` map.
10
+ */
11
+ export declare class TraceManager {
12
+ private tracePath;
13
+ constructor(config: PintaConfig);
14
+ private readMap;
15
+ private writeMap;
16
+ /** Start (and persist) a new trace for this session. */
17
+ newTrace(sessionId?: string): string;
18
+ /** Current trace for this session; create one if none exists yet. */
19
+ currentTrace(sessionId?: string): string;
20
+ }