@pinta-ai/pinta-opencode 0.4.0 → 0.5.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/dist/index.mjs +1417 -0
- package/dist/plugin.js +1 -1
- package/package.json +5 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
import { createRequire as __pintaCreateRequire } from 'module'; const require = __pintaCreateRequire(import.meta.url);
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
14
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
15
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
16
|
+
};
|
|
17
|
+
var __copyProps = (to, from, except, desc) => {
|
|
18
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
+
for (let key of __getOwnPropNames(from))
|
|
20
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
+
mod
|
|
32
|
+
));
|
|
33
|
+
|
|
34
|
+
// node_modules/@pinta-ai/core/dist/redact.js
|
|
35
|
+
var require_redact = __commonJS({
|
|
36
|
+
"node_modules/@pinta-ai/core/dist/redact.js"(exports) {
|
|
37
|
+
"use strict";
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PATTERNS = exports.MAX_BYTES = void 0;
|
|
40
|
+
exports.truncate = truncate;
|
|
41
|
+
exports.collectMatches = collectMatches;
|
|
42
|
+
exports.resolveOverlaps = resolveOverlaps;
|
|
43
|
+
exports.applyMatches = applyMatches;
|
|
44
|
+
exports.redact = redact;
|
|
45
|
+
exports.MAX_BYTES = 102400;
|
|
46
|
+
function truncate(input) {
|
|
47
|
+
const buf = Buffer.from(input, "utf-8");
|
|
48
|
+
if (buf.length <= exports.MAX_BYTES)
|
|
49
|
+
return input;
|
|
50
|
+
const head = buf.subarray(0, exports.MAX_BYTES).toString("utf-8");
|
|
51
|
+
return `${head}\u2026[TRUNCATED:${buf.length}]`;
|
|
52
|
+
}
|
|
53
|
+
exports.PATTERNS = [
|
|
54
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
55
|
+
{
|
|
56
|
+
type: "aws_secret_key",
|
|
57
|
+
// Context word `aws_secret`/`AWS_SECRET` (with optional separator) followed
|
|
58
|
+
// by an assignment-ish character then a 40-char base64-ish blob.
|
|
59
|
+
regex: /(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,
|
|
60
|
+
captureGroup: 1
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: "gcp_service_account",
|
|
64
|
+
// Whole JSON blob starting with the service-account discriminator.
|
|
65
|
+
regex: /\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g
|
|
66
|
+
},
|
|
67
|
+
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
68
|
+
{ type: "gitlab_token", regex: /glpat-[A-Za-z0-9_-]{20}/g },
|
|
69
|
+
{ type: "slack_token", regex: /xox[abrsp]-[0-9A-Za-z-]{10,}/g },
|
|
70
|
+
{ type: "openai_key", regex: /sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g },
|
|
71
|
+
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9_-]{50,}/g },
|
|
72
|
+
{ type: "stripe_key", regex: /(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g },
|
|
73
|
+
{
|
|
74
|
+
type: "jwt",
|
|
75
|
+
regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "private_key_block",
|
|
79
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
80
|
+
},
|
|
81
|
+
{ type: "bearer_token", regex: /bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi, captureGroup: 1 },
|
|
82
|
+
{ type: "basic_auth", regex: /basic\s+([A-Za-z0-9+/=]{12,})/gi, captureGroup: 1 },
|
|
83
|
+
{
|
|
84
|
+
type: "db_url_password",
|
|
85
|
+
regex: /\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,
|
|
86
|
+
captureGroup: 1
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "cli_password_flag",
|
|
90
|
+
regex: /(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,
|
|
91
|
+
captureGroup: 1
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "cli_password_short",
|
|
95
|
+
// mysql -p<pass>; only on bash context.
|
|
96
|
+
regex: /\s-p([^\s'"]+)/g,
|
|
97
|
+
captureGroup: 1,
|
|
98
|
+
requireContext: "bash"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "env_var_secret",
|
|
102
|
+
// Known false positive: trailing `[A-Z0-9_]*` is greedy, so names like
|
|
103
|
+
// `OPENAI_API_KEY_DESCRIPTION=Used` still match. Acceptable for Bronze.
|
|
104
|
+
regex: /^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,
|
|
105
|
+
captureGroup: 2
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
function collectMatches(input, opts) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const pattern of exports.PATTERNS) {
|
|
111
|
+
if (pattern.requireContext && pattern.requireContext !== opts.context)
|
|
112
|
+
continue;
|
|
113
|
+
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
114
|
+
let m;
|
|
115
|
+
while ((m = re.exec(input)) !== null) {
|
|
116
|
+
const cg = pattern.captureGroup ?? 0;
|
|
117
|
+
const captured = m[cg];
|
|
118
|
+
if (captured === void 0) {
|
|
119
|
+
if (m.index === re.lastIndex)
|
|
120
|
+
re.lastIndex++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const start = m.index;
|
|
124
|
+
const end = m.index + m[0].length;
|
|
125
|
+
const replaceStart = start + m[0].indexOf(captured);
|
|
126
|
+
const replaceEnd = replaceStart + captured.length;
|
|
127
|
+
out.push({ start, end, replaceStart, replaceEnd, type: pattern.type });
|
|
128
|
+
if (m.index === re.lastIndex)
|
|
129
|
+
re.lastIndex++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function resolveOverlaps(matches) {
|
|
135
|
+
const sorted = [...matches].sort((a, b) => {
|
|
136
|
+
if (a.start !== b.start)
|
|
137
|
+
return a.start - b.start;
|
|
138
|
+
return b.end - b.start - (a.end - a.start);
|
|
139
|
+
});
|
|
140
|
+
const kept = [];
|
|
141
|
+
let lastEnd = -1;
|
|
142
|
+
for (const m of sorted) {
|
|
143
|
+
if (m.start < lastEnd)
|
|
144
|
+
continue;
|
|
145
|
+
kept.push(m);
|
|
146
|
+
lastEnd = m.end;
|
|
147
|
+
}
|
|
148
|
+
return kept;
|
|
149
|
+
}
|
|
150
|
+
function applyMatches(input, matches) {
|
|
151
|
+
const sorted = [...matches].sort((a, b) => b.replaceStart - a.replaceStart);
|
|
152
|
+
let out = input;
|
|
153
|
+
for (const m of sorted) {
|
|
154
|
+
out = out.slice(0, m.replaceStart) + `[REDACTED:${m.type}]` + out.slice(m.replaceEnd);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function redact(input, opts = {}) {
|
|
159
|
+
if (input.length === 0)
|
|
160
|
+
return input;
|
|
161
|
+
const all = collectMatches(input, opts);
|
|
162
|
+
if (all.length === 0)
|
|
163
|
+
return input;
|
|
164
|
+
const kept = resolveOverlaps(all);
|
|
165
|
+
return applyMatches(input, kept);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// node_modules/@pinta-ai/core/dist/otlp.js
|
|
171
|
+
var require_otlp = __commonJS({
|
|
172
|
+
"node_modules/@pinta-ai/core/dist/otlp.js"(exports) {
|
|
173
|
+
"use strict";
|
|
174
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
175
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
176
|
+
};
|
|
177
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
178
|
+
exports.ulidToTraceId = ulidToTraceId2;
|
|
179
|
+
exports.newSpanId = newSpanId;
|
|
180
|
+
exports.snakeCase = snakeCase;
|
|
181
|
+
exports.toOtlpValue = toOtlpValue;
|
|
182
|
+
exports.attrsFromRecord = attrsFromRecord2;
|
|
183
|
+
exports.guardAttrs = guardAttrs;
|
|
184
|
+
exports.clientAttrs = clientAttrs;
|
|
185
|
+
exports.buildPayload = buildPayload2;
|
|
186
|
+
exports.mergeBatch = mergeBatch2;
|
|
187
|
+
var crypto_1 = __importDefault(__require("crypto"));
|
|
188
|
+
var redact_js_1 = require_redact();
|
|
189
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
190
|
+
function ulidToTraceId2(ulid) {
|
|
191
|
+
if (ulid.length !== 26) {
|
|
192
|
+
throw new Error(`ulidToTraceId: expected 26 chars, got ${ulid.length}`);
|
|
193
|
+
}
|
|
194
|
+
let n = 0n;
|
|
195
|
+
for (const ch of ulid) {
|
|
196
|
+
const idx = CROCKFORD.indexOf(ch);
|
|
197
|
+
if (idx < 0)
|
|
198
|
+
throw new Error(`ulidToTraceId: invalid Crockford char "${ch}"`);
|
|
199
|
+
n = n << 5n | BigInt(idx);
|
|
200
|
+
}
|
|
201
|
+
const mask = (1n << 128n) - 1n;
|
|
202
|
+
n &= mask;
|
|
203
|
+
return n.toString(16).padStart(32, "0");
|
|
204
|
+
}
|
|
205
|
+
function newSpanId() {
|
|
206
|
+
return crypto_1.default.randomBytes(8).toString("hex");
|
|
207
|
+
}
|
|
208
|
+
function snakeCase(name) {
|
|
209
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
212
|
+
function maybeRedactString(key, raw, policy) {
|
|
213
|
+
const truncated = (0, redact_js_1.truncate)(raw);
|
|
214
|
+
if ((policy.skipRedactKeys ?? EMPTY_SET).has(key))
|
|
215
|
+
return truncated;
|
|
216
|
+
const context = (policy.bashContextKeys ?? EMPTY_SET).has(key) ? "bash" : void 0;
|
|
217
|
+
return (0, redact_js_1.redact)(truncated, { context });
|
|
218
|
+
}
|
|
219
|
+
function toOtlpValue(key, v, policy = {}) {
|
|
220
|
+
if (v === null || v === void 0)
|
|
221
|
+
return null;
|
|
222
|
+
switch (typeof v) {
|
|
223
|
+
case "string":
|
|
224
|
+
return { stringValue: maybeRedactString(key, v, policy) };
|
|
225
|
+
case "boolean":
|
|
226
|
+
return { boolValue: v };
|
|
227
|
+
case "number":
|
|
228
|
+
if (Number.isInteger(v))
|
|
229
|
+
return { intValue: v };
|
|
230
|
+
return { doubleValue: v };
|
|
231
|
+
case "object":
|
|
232
|
+
try {
|
|
233
|
+
return { stringValue: maybeRedactString(key, JSON.stringify(v), policy) };
|
|
234
|
+
} catch {
|
|
235
|
+
return { stringValue: maybeRedactString(key, String(v), policy) };
|
|
236
|
+
}
|
|
237
|
+
default:
|
|
238
|
+
return { stringValue: maybeRedactString(key, String(v), policy) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function attrsFromRecord2(record, prefix, policy = {}) {
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const [k, v] of Object.entries(record)) {
|
|
244
|
+
const key = `${prefix}.${k}`;
|
|
245
|
+
const value = toOtlpValue(key, v, policy);
|
|
246
|
+
if (value === null)
|
|
247
|
+
continue;
|
|
248
|
+
out.push({ key, value });
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function guardAttrs(guard) {
|
|
253
|
+
const out = [
|
|
254
|
+
{ key: "pinta.guard.decision", value: { stringValue: guard.decision.toLowerCase() } },
|
|
255
|
+
{ key: "pinta.guard.duration_ms", value: { intValue: guard.durationMs } }
|
|
256
|
+
];
|
|
257
|
+
if (guard.reason) {
|
|
258
|
+
out.push({ key: "pinta.guard.matched_rule", value: { stringValue: guard.reason } });
|
|
259
|
+
}
|
|
260
|
+
if (guard.failOpenReason) {
|
|
261
|
+
out.push({ key: "pinta.guard.fail_open_reason", value: { stringValue: guard.failOpenReason } });
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
function clientAttrs(timing) {
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
key: "pinta.client.rtt_ms",
|
|
269
|
+
value: Number.isInteger(timing.rttMs) ? { intValue: timing.rttMs } : { doubleValue: timing.rttMs }
|
|
270
|
+
},
|
|
271
|
+
{ key: "pinta.client.op", value: { stringValue: timing.op } }
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
function buildPayload2(args) {
|
|
275
|
+
const ts = args.now ?? Date.now();
|
|
276
|
+
const tsNano = (BigInt(ts) * 1000000n).toString();
|
|
277
|
+
const attrs = [...args.attributes];
|
|
278
|
+
if (args.guard)
|
|
279
|
+
attrs.push(...guardAttrs(args.guard));
|
|
280
|
+
const timing = args.clientTiming ?? (args.guard?.clientRttMs !== void 0 ? { op: "guard", rttMs: args.guard.clientRttMs } : null);
|
|
281
|
+
if (timing)
|
|
282
|
+
attrs.push(...clientAttrs(timing));
|
|
283
|
+
const span = {
|
|
284
|
+
traceId: ulidToTraceId2(args.traceId),
|
|
285
|
+
spanId: newSpanId(),
|
|
286
|
+
name: args.spanName,
|
|
287
|
+
kind: args.spanKind ?? 1,
|
|
288
|
+
startTimeUnixNano: tsNano,
|
|
289
|
+
endTimeUnixNano: tsNano,
|
|
290
|
+
attributes: attrs
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
resourceSpans: [
|
|
294
|
+
{
|
|
295
|
+
resource: { attributes: args.resource },
|
|
296
|
+
scopeSpans: [{ scope: args.scope, spans: [span] }]
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function mergeBatch2(payloads) {
|
|
302
|
+
const out = [];
|
|
303
|
+
for (const p of payloads)
|
|
304
|
+
out.push(...p.resourceSpans);
|
|
305
|
+
return { resourceSpans: out };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// node_modules/@pinta-ai/core/dist/guard.js
|
|
311
|
+
var require_guard = __commonJS({
|
|
312
|
+
"node_modules/@pinta-ai/core/dist/guard.js"(exports) {
|
|
313
|
+
"use strict";
|
|
314
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
315
|
+
exports.evaluateGuard = evaluateGuard2;
|
|
316
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
317
|
+
var DEFAULT_UA = "pinta-core";
|
|
318
|
+
function failOpen2(clientRttMs, failOpenReason) {
|
|
319
|
+
return {
|
|
320
|
+
decision: "ALLOW",
|
|
321
|
+
reason: null,
|
|
322
|
+
userMessage: null,
|
|
323
|
+
durationMs: clientRttMs,
|
|
324
|
+
clientRttMs,
|
|
325
|
+
failOpenReason
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function sleep2(ms) {
|
|
329
|
+
return new Promise((_, reject) => setTimeout(() => {
|
|
330
|
+
const err = new Error("Guard request timed out");
|
|
331
|
+
err.name = "TimeoutError";
|
|
332
|
+
reject(err);
|
|
333
|
+
}, ms));
|
|
334
|
+
}
|
|
335
|
+
async function evaluateGuard2(input, endpoint, opts = {}) {
|
|
336
|
+
if (!endpoint)
|
|
337
|
+
return null;
|
|
338
|
+
if (opts.disabled)
|
|
339
|
+
return null;
|
|
340
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
341
|
+
const start = Date.now();
|
|
342
|
+
try {
|
|
343
|
+
const res = await Promise.race([
|
|
344
|
+
fetch(endpoint, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"content-type": "application/json",
|
|
348
|
+
"user-agent": opts.userAgent ?? DEFAULT_UA,
|
|
349
|
+
"x-pinta-relay-token": opts.token ?? ""
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify({ input })
|
|
352
|
+
}),
|
|
353
|
+
sleep2(timeoutMs)
|
|
354
|
+
]);
|
|
355
|
+
if (res.status !== 200) {
|
|
356
|
+
return failOpen2(Date.now() - start, "error");
|
|
357
|
+
}
|
|
358
|
+
const body = await res.json();
|
|
359
|
+
const clientRttMs = Date.now() - start;
|
|
360
|
+
return {
|
|
361
|
+
decision: body.decision,
|
|
362
|
+
reason: body.reason,
|
|
363
|
+
userMessage: body.userMessage ?? null,
|
|
364
|
+
durationMs: body.durationMs ?? clientRttMs,
|
|
365
|
+
clientRttMs
|
|
366
|
+
};
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const reason = err.name === "TimeoutError" ? "timeout" : "error";
|
|
369
|
+
return failOpen2(Date.now() - start, reason);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// node_modules/@pinta-ai/core/dist/retry-queue.js
|
|
376
|
+
var require_retry_queue = __commonJS({
|
|
377
|
+
"node_modules/@pinta-ai/core/dist/retry-queue.js"(exports) {
|
|
378
|
+
"use strict";
|
|
379
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
380
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
381
|
+
};
|
|
382
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
383
|
+
exports.MemoryRetryQueue = exports.DiskRetryQueue = void 0;
|
|
384
|
+
var fs_1 = __importDefault(__require("fs"));
|
|
385
|
+
var path_1 = __importDefault(__require("path"));
|
|
386
|
+
var MAX_ENTRIES = 1e3;
|
|
387
|
+
var LOCK_TIMEOUT_MS = 50;
|
|
388
|
+
var LOCK_POLL_MS = 5;
|
|
389
|
+
var DiskRetryQueue = class {
|
|
390
|
+
filePath;
|
|
391
|
+
lockPath;
|
|
392
|
+
logPrefix;
|
|
393
|
+
constructor(pluginData, logPrefix) {
|
|
394
|
+
this.filePath = path_1.default.join(pluginData, "failed-spans.jsonl");
|
|
395
|
+
this.lockPath = this.filePath + ".lock";
|
|
396
|
+
this.logPrefix = logPrefix;
|
|
397
|
+
}
|
|
398
|
+
/** Append a single payload. Best-effort: any IO error is swallowed (logged to stderr). */
|
|
399
|
+
enqueue(payload) {
|
|
400
|
+
try {
|
|
401
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
|
|
402
|
+
const line = JSON.stringify({ savedAt: (/* @__PURE__ */ new Date()).toISOString(), payload }) + "\n";
|
|
403
|
+
fs_1.default.appendFileSync(this.filePath, line);
|
|
404
|
+
this.trim();
|
|
405
|
+
} catch (err) {
|
|
406
|
+
process.stderr.write(`[${this.logPrefix}] retry-queue enqueue failed: ${err}
|
|
407
|
+
`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Read all entries oldest-first. Returns [] if the file does not exist or is unreadable.
|
|
412
|
+
* Does NOT delete the file — callers handle persistence via `rewrite`.
|
|
413
|
+
*/
|
|
414
|
+
readAll() {
|
|
415
|
+
try {
|
|
416
|
+
const raw = fs_1.default.readFileSync(this.filePath, "utf-8");
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const line of raw.split("\n")) {
|
|
419
|
+
if (!line.trim())
|
|
420
|
+
continue;
|
|
421
|
+
try {
|
|
422
|
+
out.push(JSON.parse(line));
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
} catch {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Replace the queue with the given entries (or delete the file when empty). */
|
|
432
|
+
rewrite(entries) {
|
|
433
|
+
try {
|
|
434
|
+
if (entries.length === 0) {
|
|
435
|
+
if (fs_1.default.existsSync(this.filePath))
|
|
436
|
+
fs_1.default.unlinkSync(this.filePath);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
|
|
440
|
+
fs_1.default.writeFileSync(this.filePath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
441
|
+
} catch (err) {
|
|
442
|
+
process.stderr.write(`[${this.logPrefix}] retry-queue rewrite failed: ${err}
|
|
443
|
+
`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Try to acquire the lock for ~LOCK_TIMEOUT_MS. Returns true on success.
|
|
448
|
+
* Caller MUST call `release()` if true is returned.
|
|
449
|
+
*/
|
|
450
|
+
tryAcquireLock() {
|
|
451
|
+
const start = Date.now();
|
|
452
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.lockPath), { recursive: true });
|
|
453
|
+
while (Date.now() - start < LOCK_TIMEOUT_MS) {
|
|
454
|
+
try {
|
|
455
|
+
const fd = fs_1.default.openSync(this.lockPath, "wx");
|
|
456
|
+
fs_1.default.writeSync(fd, String(process.pid));
|
|
457
|
+
fs_1.default.closeSync(fd);
|
|
458
|
+
return true;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (err?.code !== "EEXIST") {
|
|
461
|
+
process.stderr.write(`[${this.logPrefix}] retry-queue lock open failed: ${err}
|
|
462
|
+
`);
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const st = fs_1.default.statSync(this.lockPath);
|
|
467
|
+
if (Date.now() - st.mtimeMs > 3e4) {
|
|
468
|
+
fs_1.default.unlinkSync(this.lockPath);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
const wait = LOCK_POLL_MS;
|
|
474
|
+
const end = Date.now() + wait;
|
|
475
|
+
while (Date.now() < end) {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
release() {
|
|
482
|
+
try {
|
|
483
|
+
fs_1.default.unlinkSync(this.lockPath);
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
trim() {
|
|
488
|
+
const entries = this.readAll();
|
|
489
|
+
if (entries.length <= MAX_ENTRIES)
|
|
490
|
+
return;
|
|
491
|
+
const drop = entries.length - MAX_ENTRIES;
|
|
492
|
+
process.stderr.write(`[${this.logPrefix}] retry-queue full, dropping ${drop} oldest entries
|
|
493
|
+
`);
|
|
494
|
+
this.rewrite(entries.slice(drop));
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
exports.DiskRetryQueue = DiskRetryQueue;
|
|
498
|
+
var MemoryRetryQueue = class {
|
|
499
|
+
entries = [];
|
|
500
|
+
enqueue(payload) {
|
|
501
|
+
this.entries.push(payload);
|
|
502
|
+
if (this.entries.length > MAX_ENTRIES) {
|
|
503
|
+
this.entries.splice(0, this.entries.length - MAX_ENTRIES);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** Remove and return all buffered payloads. */
|
|
507
|
+
drain() {
|
|
508
|
+
const out = this.entries;
|
|
509
|
+
this.entries = [];
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
get size() {
|
|
513
|
+
return this.entries.length;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
exports.MemoryRetryQueue = MemoryRetryQueue;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// node_modules/@pinta-ai/core/dist/transport.js
|
|
521
|
+
var require_transport = __commonJS({
|
|
522
|
+
"node_modules/@pinta-ai/core/dist/transport.js"(exports) {
|
|
523
|
+
"use strict";
|
|
524
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
525
|
+
exports.MemoryTransport = exports.DiskTransport = void 0;
|
|
526
|
+
exports.parseHeadersEnv = parseHeadersEnv2;
|
|
527
|
+
exports.envOptionsResolver = envOptionsResolver;
|
|
528
|
+
exports.postOtlp = postOtlp;
|
|
529
|
+
var retry_queue_js_1 = require_retry_queue();
|
|
530
|
+
var otlp_js_1 = require_otlp();
|
|
531
|
+
var TIMEOUT_MS = 5e3;
|
|
532
|
+
function parseHeadersEnv2(raw) {
|
|
533
|
+
if (!raw)
|
|
534
|
+
return {};
|
|
535
|
+
if (typeof raw === "object")
|
|
536
|
+
return { ...raw };
|
|
537
|
+
const out = {};
|
|
538
|
+
for (const pair of raw.split(",")) {
|
|
539
|
+
const [k, ...rest] = pair.split("=");
|
|
540
|
+
if (k && rest.length > 0)
|
|
541
|
+
out[k.trim()] = rest.join("=").trim();
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
function envOptionsResolver() {
|
|
546
|
+
const tracesEndpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
|
|
547
|
+
const baseEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
548
|
+
let endpoint;
|
|
549
|
+
if (tracesEndpoint) {
|
|
550
|
+
endpoint = tracesEndpoint.replace(/\/+$/, "");
|
|
551
|
+
} else if (baseEndpoint) {
|
|
552
|
+
endpoint = baseEndpoint.replace(/\/+$/, "") + "/v1/traces";
|
|
553
|
+
}
|
|
554
|
+
if (!endpoint)
|
|
555
|
+
return null;
|
|
556
|
+
return {
|
|
557
|
+
endpoint,
|
|
558
|
+
headers: parseHeadersEnv2(process.env.OTEL_EXPORTER_OTLP_HEADERS)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
async function postOtlp(payload, opts, logPrefix) {
|
|
562
|
+
const ctrl = new AbortController();
|
|
563
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
564
|
+
try {
|
|
565
|
+
const res = await fetch(opts.endpoint, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: { "Content-Type": "application/json", ...opts.headers },
|
|
568
|
+
body: JSON.stringify(payload),
|
|
569
|
+
signal: ctrl.signal
|
|
570
|
+
});
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
let body = "";
|
|
573
|
+
try {
|
|
574
|
+
body = (await res.text()).slice(0, 200);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
const hint = res.status === 401 || res.status === 403 ? " \u2014 check OTEL_EXPORTER_OTLP_HEADERS (relay token)" : res.status === 404 ? " \u2014 check OTEL_EXPORTER_OTLP_TRACES_ENDPOINT path" : res.status >= 500 ? " \u2014 collector may be down" : "";
|
|
578
|
+
process.stderr.write(`[${logPrefix}] OTLP POST ${res.status} ${opts.endpoint}${hint}${body ? ` body=${body}` : ""}
|
|
579
|
+
`);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
process.stderr.write(`[${logPrefix}] OTLP POST failed: ${err.message ?? String(err)}
|
|
585
|
+
`);
|
|
586
|
+
return false;
|
|
587
|
+
} finally {
|
|
588
|
+
clearTimeout(timer);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
var DiskTransport = class {
|
|
592
|
+
queue;
|
|
593
|
+
logPrefix;
|
|
594
|
+
resolveOptions;
|
|
595
|
+
constructor(opts) {
|
|
596
|
+
this.queue = new retry_queue_js_1.DiskRetryQueue(opts.pluginData, opts.logPrefix);
|
|
597
|
+
this.logPrefix = opts.logPrefix;
|
|
598
|
+
this.resolveOptions = opts.resolveOptions ?? envOptionsResolver;
|
|
599
|
+
}
|
|
600
|
+
async send(payload) {
|
|
601
|
+
const opts = this.resolveOptions();
|
|
602
|
+
if (!opts)
|
|
603
|
+
return;
|
|
604
|
+
const ok = await postOtlp(payload, opts, this.logPrefix);
|
|
605
|
+
if (!ok)
|
|
606
|
+
this.queue.enqueue(payload);
|
|
607
|
+
}
|
|
608
|
+
async flush() {
|
|
609
|
+
const opts = this.resolveOptions();
|
|
610
|
+
if (!opts)
|
|
611
|
+
return;
|
|
612
|
+
if (!this.queue.tryAcquireLock())
|
|
613
|
+
return;
|
|
614
|
+
try {
|
|
615
|
+
const entries = this.queue.readAll();
|
|
616
|
+
if (entries.length === 0)
|
|
617
|
+
return;
|
|
618
|
+
const merged = (0, otlp_js_1.mergeBatch)(entries.map((e) => e.payload));
|
|
619
|
+
const ok = await postOtlp(merged, opts, this.logPrefix);
|
|
620
|
+
if (ok)
|
|
621
|
+
this.queue.rewrite([]);
|
|
622
|
+
} finally {
|
|
623
|
+
this.queue.release();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
exports.DiskTransport = DiskTransport;
|
|
628
|
+
var MemoryTransport2 = class {
|
|
629
|
+
queue = new retry_queue_js_1.MemoryRetryQueue();
|
|
630
|
+
logPrefix;
|
|
631
|
+
resolveOptions;
|
|
632
|
+
constructor(opts) {
|
|
633
|
+
this.logPrefix = opts.logPrefix;
|
|
634
|
+
this.resolveOptions = opts.resolveOptions;
|
|
635
|
+
}
|
|
636
|
+
async send(payload) {
|
|
637
|
+
const opts = this.resolveOptions();
|
|
638
|
+
if (!opts)
|
|
639
|
+
return;
|
|
640
|
+
const ok = await postOtlp(payload, opts, this.logPrefix);
|
|
641
|
+
if (!ok)
|
|
642
|
+
this.queue.enqueue(payload);
|
|
643
|
+
}
|
|
644
|
+
async flush() {
|
|
645
|
+
const opts = this.resolveOptions();
|
|
646
|
+
if (!opts)
|
|
647
|
+
return;
|
|
648
|
+
const buffered = this.queue.drain();
|
|
649
|
+
if (buffered.length === 0)
|
|
650
|
+
return;
|
|
651
|
+
const merged = (0, otlp_js_1.mergeBatch)(buffered);
|
|
652
|
+
const ok = await postOtlp(merged, opts, this.logPrefix);
|
|
653
|
+
if (!ok)
|
|
654
|
+
for (const p of buffered)
|
|
655
|
+
this.queue.enqueue(p);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
exports.MemoryTransport = MemoryTransport2;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// node_modules/@pinta-ai/core/dist/trace.js
|
|
663
|
+
var require_trace = __commonJS({
|
|
664
|
+
"node_modules/@pinta-ai/core/dist/trace.js"(exports) {
|
|
665
|
+
"use strict";
|
|
666
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
667
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
668
|
+
};
|
|
669
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
670
|
+
exports.TraceManager = void 0;
|
|
671
|
+
exports.generateUlid = generateUlid;
|
|
672
|
+
var fs_1 = __importDefault(__require("fs"));
|
|
673
|
+
var path_1 = __importDefault(__require("path"));
|
|
674
|
+
var crypto_1 = __importDefault(__require("crypto"));
|
|
675
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
676
|
+
function generateUlid() {
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
let ts = "";
|
|
679
|
+
let t = now;
|
|
680
|
+
for (let i = 0; i < 10; i++) {
|
|
681
|
+
ts = CROCKFORD[t & 31] + ts;
|
|
682
|
+
t = Math.floor(t / 32);
|
|
683
|
+
}
|
|
684
|
+
const rand = crypto_1.default.randomBytes(10);
|
|
685
|
+
let r = "";
|
|
686
|
+
for (let i = 0; i < 10; i++) {
|
|
687
|
+
r += CROCKFORD[rand[i] & 31];
|
|
688
|
+
}
|
|
689
|
+
while (r.length < 16)
|
|
690
|
+
r += CROCKFORD[0];
|
|
691
|
+
return ts + r;
|
|
692
|
+
}
|
|
693
|
+
var TraceManager = class {
|
|
694
|
+
tracePath;
|
|
695
|
+
constructor(tracePath) {
|
|
696
|
+
this.tracePath = tracePath;
|
|
697
|
+
}
|
|
698
|
+
/** Generate and persist a fresh trace id (e.g. on UserPromptSubmit). */
|
|
699
|
+
newTrace() {
|
|
700
|
+
const traceId = generateUlid();
|
|
701
|
+
this.save(traceId);
|
|
702
|
+
return traceId;
|
|
703
|
+
}
|
|
704
|
+
/** Return the current trace id, generating one if no trace file exists. */
|
|
705
|
+
currentTrace() {
|
|
706
|
+
try {
|
|
707
|
+
const data = fs_1.default.readFileSync(this.tracePath, "utf-8");
|
|
708
|
+
const { traceId } = JSON.parse(data);
|
|
709
|
+
if (traceId)
|
|
710
|
+
return traceId;
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
return this.newTrace();
|
|
714
|
+
}
|
|
715
|
+
save(traceId) {
|
|
716
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.tracePath), { recursive: true });
|
|
717
|
+
fs_1.default.writeFileSync(this.tracePath, JSON.stringify({ traceId }));
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
exports.TraceManager = TraceManager;
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// node_modules/@pinta-ai/core/dist/session-trace.js
|
|
725
|
+
var require_session_trace = __commonJS({
|
|
726
|
+
"node_modules/@pinta-ai/core/dist/session-trace.js"(exports) {
|
|
727
|
+
"use strict";
|
|
728
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
729
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
730
|
+
};
|
|
731
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
732
|
+
exports.MemorySessionTraceManager = exports.DiskSessionTraceManager = void 0;
|
|
733
|
+
var fs_1 = __importDefault(__require("fs"));
|
|
734
|
+
var path_1 = __importDefault(__require("path"));
|
|
735
|
+
var trace_js_1 = require_trace();
|
|
736
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
737
|
+
var DiskSessionTraceManager = class {
|
|
738
|
+
tracePath;
|
|
739
|
+
maxSessions;
|
|
740
|
+
constructor(tracePath, opts = {}) {
|
|
741
|
+
this.tracePath = tracePath;
|
|
742
|
+
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
743
|
+
}
|
|
744
|
+
read() {
|
|
745
|
+
try {
|
|
746
|
+
const data = JSON.parse(fs_1.default.readFileSync(this.tracePath, "utf-8"));
|
|
747
|
+
if (data && typeof data === "object" && !("traceId" in data)) {
|
|
748
|
+
return data;
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
return {};
|
|
753
|
+
}
|
|
754
|
+
write(map) {
|
|
755
|
+
try {
|
|
756
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.tracePath), { recursive: true });
|
|
757
|
+
const entries = Object.entries(map);
|
|
758
|
+
const capped = entries.length > this.maxSessions ? Object.fromEntries(entries.slice(-this.maxSessions)) : map;
|
|
759
|
+
fs_1.default.writeFileSync(this.tracePath, JSON.stringify(capped));
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
newTrace(sessionId) {
|
|
764
|
+
const key = sessionId || "default";
|
|
765
|
+
const traceId = (0, trace_js_1.generateUlid)();
|
|
766
|
+
const map = this.read();
|
|
767
|
+
map[key] = traceId;
|
|
768
|
+
this.write(map);
|
|
769
|
+
return traceId;
|
|
770
|
+
}
|
|
771
|
+
currentTrace(sessionId) {
|
|
772
|
+
const key = sessionId || "default";
|
|
773
|
+
const existing = this.read()[key];
|
|
774
|
+
if (existing)
|
|
775
|
+
return existing;
|
|
776
|
+
return this.newTrace(key);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
exports.DiskSessionTraceManager = DiskSessionTraceManager;
|
|
780
|
+
var MemorySessionTraceManager2 = class {
|
|
781
|
+
map = /* @__PURE__ */ new Map();
|
|
782
|
+
maxSessions;
|
|
783
|
+
constructor(opts = {}) {
|
|
784
|
+
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
785
|
+
}
|
|
786
|
+
newTrace(sessionId) {
|
|
787
|
+
const key = sessionId || "default";
|
|
788
|
+
const traceId = (0, trace_js_1.generateUlid)();
|
|
789
|
+
this.map.set(key, traceId);
|
|
790
|
+
if (this.map.size > this.maxSessions) {
|
|
791
|
+
const oldest = this.map.keys().next().value;
|
|
792
|
+
if (oldest !== void 0)
|
|
793
|
+
this.map.delete(oldest);
|
|
794
|
+
}
|
|
795
|
+
return traceId;
|
|
796
|
+
}
|
|
797
|
+
currentTrace(sessionId) {
|
|
798
|
+
const key = sessionId || "default";
|
|
799
|
+
return this.map.get(key) ?? this.newTrace(key);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
exports.MemorySessionTraceManager = MemorySessionTraceManager2;
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// node_modules/@pinta-ai/core/dist/env-file.js
|
|
807
|
+
var require_env_file = __commonJS({
|
|
808
|
+
"node_modules/@pinta-ai/core/dist/env-file.js"(exports) {
|
|
809
|
+
"use strict";
|
|
810
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
811
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
812
|
+
};
|
|
813
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
814
|
+
exports.envFilePath = envFilePath2;
|
|
815
|
+
exports.parseEnvFile = parseEnvFile2;
|
|
816
|
+
exports.loadEnvFile = loadEnvFile2;
|
|
817
|
+
var node_fs_1 = __importDefault(__require("node:fs"));
|
|
818
|
+
var node_os_1 = __importDefault(__require("node:os"));
|
|
819
|
+
var node_path_1 = __importDefault(__require("node:path"));
|
|
820
|
+
function envFilePath2(dir, filename, overrideEnvVar) {
|
|
821
|
+
const override = overrideEnvVar ? process.env[overrideEnvVar] : void 0;
|
|
822
|
+
const base = override && override.length > 0 ? override : node_path_1.default.join(node_os_1.default.homedir(), dir);
|
|
823
|
+
return node_path_1.default.join(base, filename);
|
|
824
|
+
}
|
|
825
|
+
function parseEnvFile2(content) {
|
|
826
|
+
const out = {};
|
|
827
|
+
for (const raw of content.split("\n")) {
|
|
828
|
+
const line = raw.trim();
|
|
829
|
+
if (!line || line.startsWith("#"))
|
|
830
|
+
continue;
|
|
831
|
+
const idx = line.indexOf("=");
|
|
832
|
+
if (idx < 0)
|
|
833
|
+
continue;
|
|
834
|
+
const key = line.slice(0, idx).trim();
|
|
835
|
+
let value = line.slice(idx + 1).trim();
|
|
836
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
837
|
+
value = value.slice(1, -1);
|
|
838
|
+
}
|
|
839
|
+
if (key)
|
|
840
|
+
out[key] = value;
|
|
841
|
+
}
|
|
842
|
+
return out;
|
|
843
|
+
}
|
|
844
|
+
function loadEnvFile2(filePath) {
|
|
845
|
+
let content;
|
|
846
|
+
try {
|
|
847
|
+
content = node_fs_1.default.readFileSync(filePath, "utf-8");
|
|
848
|
+
} catch {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const parsed = parseEnvFile2(content);
|
|
852
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
853
|
+
if (process.env[key] === void 0) {
|
|
854
|
+
process.env[key] = value;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// node_modules/@pinta-ai/core/dist/index.js
|
|
862
|
+
var require_dist = __commonJS({
|
|
863
|
+
"node_modules/@pinta-ai/core/dist/index.js"(exports) {
|
|
864
|
+
"use strict";
|
|
865
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
866
|
+
exports.loadEnvFile = exports.parseEnvFile = exports.envFilePath = exports.MemorySessionTraceManager = exports.DiskSessionTraceManager = exports.generateUlid = exports.TraceManager = exports.parseHeadersEnv = exports.envOptionsResolver = exports.postOtlp = exports.MemoryTransport = exports.DiskTransport = exports.MemoryRetryQueue = exports.DiskRetryQueue = exports.evaluateGuard = exports.mergeBatch = exports.buildPayload = exports.clientAttrs = exports.guardAttrs = exports.attrsFromRecord = exports.toOtlpValue = exports.snakeCase = exports.newSpanId = exports.ulidToTraceId = exports.MAX_BYTES = exports.PATTERNS = exports.applyMatches = exports.resolveOverlaps = exports.collectMatches = exports.truncate = exports.redact = void 0;
|
|
867
|
+
var redact_js_1 = require_redact();
|
|
868
|
+
Object.defineProperty(exports, "redact", { enumerable: true, get: function() {
|
|
869
|
+
return redact_js_1.redact;
|
|
870
|
+
} });
|
|
871
|
+
Object.defineProperty(exports, "truncate", { enumerable: true, get: function() {
|
|
872
|
+
return redact_js_1.truncate;
|
|
873
|
+
} });
|
|
874
|
+
Object.defineProperty(exports, "collectMatches", { enumerable: true, get: function() {
|
|
875
|
+
return redact_js_1.collectMatches;
|
|
876
|
+
} });
|
|
877
|
+
Object.defineProperty(exports, "resolveOverlaps", { enumerable: true, get: function() {
|
|
878
|
+
return redact_js_1.resolveOverlaps;
|
|
879
|
+
} });
|
|
880
|
+
Object.defineProperty(exports, "applyMatches", { enumerable: true, get: function() {
|
|
881
|
+
return redact_js_1.applyMatches;
|
|
882
|
+
} });
|
|
883
|
+
Object.defineProperty(exports, "PATTERNS", { enumerable: true, get: function() {
|
|
884
|
+
return redact_js_1.PATTERNS;
|
|
885
|
+
} });
|
|
886
|
+
Object.defineProperty(exports, "MAX_BYTES", { enumerable: true, get: function() {
|
|
887
|
+
return redact_js_1.MAX_BYTES;
|
|
888
|
+
} });
|
|
889
|
+
var otlp_js_1 = require_otlp();
|
|
890
|
+
Object.defineProperty(exports, "ulidToTraceId", { enumerable: true, get: function() {
|
|
891
|
+
return otlp_js_1.ulidToTraceId;
|
|
892
|
+
} });
|
|
893
|
+
Object.defineProperty(exports, "newSpanId", { enumerable: true, get: function() {
|
|
894
|
+
return otlp_js_1.newSpanId;
|
|
895
|
+
} });
|
|
896
|
+
Object.defineProperty(exports, "snakeCase", { enumerable: true, get: function() {
|
|
897
|
+
return otlp_js_1.snakeCase;
|
|
898
|
+
} });
|
|
899
|
+
Object.defineProperty(exports, "toOtlpValue", { enumerable: true, get: function() {
|
|
900
|
+
return otlp_js_1.toOtlpValue;
|
|
901
|
+
} });
|
|
902
|
+
Object.defineProperty(exports, "attrsFromRecord", { enumerable: true, get: function() {
|
|
903
|
+
return otlp_js_1.attrsFromRecord;
|
|
904
|
+
} });
|
|
905
|
+
Object.defineProperty(exports, "guardAttrs", { enumerable: true, get: function() {
|
|
906
|
+
return otlp_js_1.guardAttrs;
|
|
907
|
+
} });
|
|
908
|
+
Object.defineProperty(exports, "clientAttrs", { enumerable: true, get: function() {
|
|
909
|
+
return otlp_js_1.clientAttrs;
|
|
910
|
+
} });
|
|
911
|
+
Object.defineProperty(exports, "buildPayload", { enumerable: true, get: function() {
|
|
912
|
+
return otlp_js_1.buildPayload;
|
|
913
|
+
} });
|
|
914
|
+
Object.defineProperty(exports, "mergeBatch", { enumerable: true, get: function() {
|
|
915
|
+
return otlp_js_1.mergeBatch;
|
|
916
|
+
} });
|
|
917
|
+
var guard_js_1 = require_guard();
|
|
918
|
+
Object.defineProperty(exports, "evaluateGuard", { enumerable: true, get: function() {
|
|
919
|
+
return guard_js_1.evaluateGuard;
|
|
920
|
+
} });
|
|
921
|
+
var retry_queue_js_1 = require_retry_queue();
|
|
922
|
+
Object.defineProperty(exports, "DiskRetryQueue", { enumerable: true, get: function() {
|
|
923
|
+
return retry_queue_js_1.DiskRetryQueue;
|
|
924
|
+
} });
|
|
925
|
+
Object.defineProperty(exports, "MemoryRetryQueue", { enumerable: true, get: function() {
|
|
926
|
+
return retry_queue_js_1.MemoryRetryQueue;
|
|
927
|
+
} });
|
|
928
|
+
var transport_js_1 = require_transport();
|
|
929
|
+
Object.defineProperty(exports, "DiskTransport", { enumerable: true, get: function() {
|
|
930
|
+
return transport_js_1.DiskTransport;
|
|
931
|
+
} });
|
|
932
|
+
Object.defineProperty(exports, "MemoryTransport", { enumerable: true, get: function() {
|
|
933
|
+
return transport_js_1.MemoryTransport;
|
|
934
|
+
} });
|
|
935
|
+
Object.defineProperty(exports, "postOtlp", { enumerable: true, get: function() {
|
|
936
|
+
return transport_js_1.postOtlp;
|
|
937
|
+
} });
|
|
938
|
+
Object.defineProperty(exports, "envOptionsResolver", { enumerable: true, get: function() {
|
|
939
|
+
return transport_js_1.envOptionsResolver;
|
|
940
|
+
} });
|
|
941
|
+
Object.defineProperty(exports, "parseHeadersEnv", { enumerable: true, get: function() {
|
|
942
|
+
return transport_js_1.parseHeadersEnv;
|
|
943
|
+
} });
|
|
944
|
+
var trace_js_1 = require_trace();
|
|
945
|
+
Object.defineProperty(exports, "TraceManager", { enumerable: true, get: function() {
|
|
946
|
+
return trace_js_1.TraceManager;
|
|
947
|
+
} });
|
|
948
|
+
Object.defineProperty(exports, "generateUlid", { enumerable: true, get: function() {
|
|
949
|
+
return trace_js_1.generateUlid;
|
|
950
|
+
} });
|
|
951
|
+
var session_trace_js_1 = require_session_trace();
|
|
952
|
+
Object.defineProperty(exports, "DiskSessionTraceManager", { enumerable: true, get: function() {
|
|
953
|
+
return session_trace_js_1.DiskSessionTraceManager;
|
|
954
|
+
} });
|
|
955
|
+
Object.defineProperty(exports, "MemorySessionTraceManager", { enumerable: true, get: function() {
|
|
956
|
+
return session_trace_js_1.MemorySessionTraceManager;
|
|
957
|
+
} });
|
|
958
|
+
var env_file_js_1 = require_env_file();
|
|
959
|
+
Object.defineProperty(exports, "envFilePath", { enumerable: true, get: function() {
|
|
960
|
+
return env_file_js_1.envFilePath;
|
|
961
|
+
} });
|
|
962
|
+
Object.defineProperty(exports, "parseEnvFile", { enumerable: true, get: function() {
|
|
963
|
+
return env_file_js_1.parseEnvFile;
|
|
964
|
+
} });
|
|
965
|
+
Object.defineProperty(exports, "loadEnvFile", { enumerable: true, get: function() {
|
|
966
|
+
return env_file_js_1.loadEnvFile;
|
|
967
|
+
} });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// src/config.ts
|
|
972
|
+
var import_core2 = __toESM(require_dist(), 1);
|
|
973
|
+
|
|
974
|
+
// src/env-file.ts
|
|
975
|
+
var import_core = __toESM(require_dist(), 1);
|
|
976
|
+
function envFilePath() {
|
|
977
|
+
return (0, import_core.envFilePath)(".config/opencode", "pinta-opencode.env", "OPENCODE_CONFIG_DIR");
|
|
978
|
+
}
|
|
979
|
+
function loadEnvFile(filePath = envFilePath()) {
|
|
980
|
+
(0, import_core.loadEnvFile)(filePath);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/config.ts
|
|
984
|
+
function firstSet(...vals) {
|
|
985
|
+
return vals.find((v) => v) || void 0;
|
|
986
|
+
}
|
|
987
|
+
function resolveEndpoint(options) {
|
|
988
|
+
const full = firstSet(
|
|
989
|
+
options.endpoint,
|
|
990
|
+
process.env.PINTA_OPENCODE_ENDPOINT,
|
|
991
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
992
|
+
);
|
|
993
|
+
if (full) return full.replace(/\/+$/, "");
|
|
994
|
+
const base = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
995
|
+
if (base) return base.replace(/\/+$/, "") + "/v1/traces";
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
function resolveConfig(options = {}) {
|
|
999
|
+
loadEnvFile();
|
|
1000
|
+
const relayToken = firstSet(options.token, process.env.PINTA_OPENCODE_TOKEN);
|
|
1001
|
+
const headers = (0, import_core2.parseHeadersEnv)(
|
|
1002
|
+
options.headers ?? process.env.PINTA_OPENCODE_HEADERS ?? process.env.OTEL_EXPORTER_OTLP_HEADERS
|
|
1003
|
+
);
|
|
1004
|
+
if (relayToken && !Object.keys(headers).some((k) => k.toLowerCase() === "x-pinta-relay-token")) {
|
|
1005
|
+
headers["x-pinta-relay-token"] = relayToken;
|
|
1006
|
+
}
|
|
1007
|
+
const guardTimeoutMs = options.guardTimeoutMs ?? (Number(process.env.PINTA_OPENCODE_GUARD_TIMEOUT_MS) || 50);
|
|
1008
|
+
return {
|
|
1009
|
+
endpoint: resolveEndpoint(options),
|
|
1010
|
+
headers,
|
|
1011
|
+
guardEndpoint: firstSet(options.guard, process.env.PINTA_OPENCODE_GUARD),
|
|
1012
|
+
relayToken,
|
|
1013
|
+
guardTimeoutMs,
|
|
1014
|
+
guardDisabled: process.env.PINTA_OPENCODE_GUARD_DISABLED === "1",
|
|
1015
|
+
serviceVersion: process.env.OPENCODE_VERSION || "unknown"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/core/transport.ts
|
|
1020
|
+
var import_core3 = __toESM(require_dist(), 1);
|
|
1021
|
+
var Transport = class extends import_core3.MemoryTransport {
|
|
1022
|
+
constructor(config) {
|
|
1023
|
+
super({
|
|
1024
|
+
logPrefix: "pinta-opencode",
|
|
1025
|
+
resolveOptions: () => config.endpoint ? { endpoint: config.endpoint, headers: config.headers } : null
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// src/core/trace.ts
|
|
1031
|
+
var import_core4 = __toESM(require_dist(), 1);
|
|
1032
|
+
|
|
1033
|
+
// src/core/guard.ts
|
|
1034
|
+
var import_core5 = __toESM(require_dist(), 1);
|
|
1035
|
+
var DEFAULT_TIMEOUT_MS = 50;
|
|
1036
|
+
var GUARD_UA = "pinta-opencode/0.5.0";
|
|
1037
|
+
function evaluateGuard(input, endpoint, opts = {}) {
|
|
1038
|
+
return (0, import_core5.evaluateGuard)(input, endpoint, {
|
|
1039
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
1040
|
+
token: opts.token,
|
|
1041
|
+
disabled: opts.disabled,
|
|
1042
|
+
userAgent: GUARD_UA
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/core/otlp.ts
|
|
1047
|
+
var import_core6 = __toESM(require_dist(), 1);
|
|
1048
|
+
import os from "os";
|
|
1049
|
+
var SDK_VERSION = "0.5.0";
|
|
1050
|
+
var SKIP_REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
1051
|
+
"opencode.kind",
|
|
1052
|
+
"opencode.event_type",
|
|
1053
|
+
"opencode.tool",
|
|
1054
|
+
"opencode.session_id",
|
|
1055
|
+
"opencode.sessionID",
|
|
1056
|
+
"opencode.call_id",
|
|
1057
|
+
"opencode.callID",
|
|
1058
|
+
"opencode.agent",
|
|
1059
|
+
"opencode.model",
|
|
1060
|
+
"opencode.exit",
|
|
1061
|
+
"opencode.truncated",
|
|
1062
|
+
"opencode.title"
|
|
1063
|
+
]);
|
|
1064
|
+
var BASH_CONTEXT_KEYS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"opencode.args",
|
|
1066
|
+
"opencode.input",
|
|
1067
|
+
"opencode.output",
|
|
1068
|
+
"opencode.tool_input"
|
|
1069
|
+
]);
|
|
1070
|
+
var ATTR_POLICY = {
|
|
1071
|
+
skipRedactKeys: SKIP_REDACT_KEYS,
|
|
1072
|
+
bashContextKeys: BASH_CONTEXT_KEYS
|
|
1073
|
+
};
|
|
1074
|
+
function flattenFields(fields) {
|
|
1075
|
+
const out = [{ key: "ingest.type", value: { stringValue: "opencode" } }];
|
|
1076
|
+
out.push(...(0, import_core6.attrsFromRecord)(fields, "opencode", ATTR_POLICY));
|
|
1077
|
+
return out;
|
|
1078
|
+
}
|
|
1079
|
+
function resourceAttrs(serviceVersion) {
|
|
1080
|
+
return [
|
|
1081
|
+
{ key: "service.name", value: { stringValue: "opencode" } },
|
|
1082
|
+
{ key: "service.version", value: { stringValue: serviceVersion } },
|
|
1083
|
+
{ key: "telemetry.sdk.name", value: { stringValue: "pinta-opencode" } },
|
|
1084
|
+
{ key: "telemetry.sdk.language", value: { stringValue: "nodejs" } },
|
|
1085
|
+
{ key: "telemetry.sdk.version", value: { stringValue: SDK_VERSION } },
|
|
1086
|
+
{ key: "process.pid", value: { intValue: process.pid } },
|
|
1087
|
+
{ key: "process.owner", value: { stringValue: os.userInfo().username } },
|
|
1088
|
+
{ key: "host.name", value: { stringValue: os.hostname() } },
|
|
1089
|
+
{ key: "host.arch", value: { stringValue: os.arch() } }
|
|
1090
|
+
];
|
|
1091
|
+
}
|
|
1092
|
+
function buildOtlpPayload(args) {
|
|
1093
|
+
return (0, import_core6.buildPayload)({
|
|
1094
|
+
traceId: args.traceId,
|
|
1095
|
+
spanName: args.name,
|
|
1096
|
+
attributes: flattenFields(args.fields),
|
|
1097
|
+
resource: resourceAttrs(args.serviceVersion),
|
|
1098
|
+
scope: { name: "pinta-opencode", version: SDK_VERSION },
|
|
1099
|
+
now: args.now,
|
|
1100
|
+
guard: args.guard
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/telemetry.ts
|
|
1105
|
+
var Telemetry = class {
|
|
1106
|
+
constructor(transport, trace, config) {
|
|
1107
|
+
this.transport = transport;
|
|
1108
|
+
this.trace = trace;
|
|
1109
|
+
this.config = config;
|
|
1110
|
+
}
|
|
1111
|
+
async emit(name, sessionId, fields, guard) {
|
|
1112
|
+
const traceId = this.trace.currentTrace(sessionId);
|
|
1113
|
+
const payload = buildOtlpPayload({ name, traceId, fields, guard, serviceVersion: this.config.serviceVersion });
|
|
1114
|
+
await this.transport.send(payload);
|
|
1115
|
+
}
|
|
1116
|
+
/** Lifecycle span from the `event` hook. Flushes the retry buffer on turn-END. */
|
|
1117
|
+
async lifecycle(ev) {
|
|
1118
|
+
const props = ev.properties ?? {};
|
|
1119
|
+
const sessionId = typeof props.sessionID === "string" ? props.sessionID : void 0;
|
|
1120
|
+
await this.emit(`opencode.event.${ev.type ?? "unknown"}`, sessionId, {
|
|
1121
|
+
kind: "event",
|
|
1122
|
+
event_type: ev.type,
|
|
1123
|
+
session_id: sessionId,
|
|
1124
|
+
...props
|
|
1125
|
+
});
|
|
1126
|
+
if (ev.type === "session.idle") await this.transport.flush();
|
|
1127
|
+
}
|
|
1128
|
+
/** Tool span from `tool.execute.before`, carrying the guard decision. */
|
|
1129
|
+
async toolBefore(input, args, guard) {
|
|
1130
|
+
await this.emit(
|
|
1131
|
+
"opencode.tool.before",
|
|
1132
|
+
input.sessionID,
|
|
1133
|
+
{ ...toolIdentity("tool.before", input), args },
|
|
1134
|
+
guard
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
/** Tool result span from `tool.execute.after`, incl. exit code / truncation. */
|
|
1138
|
+
async toolAfter(input, output) {
|
|
1139
|
+
const meta = output.metadata ?? {};
|
|
1140
|
+
await this.emit("opencode.tool.after", input.sessionID, {
|
|
1141
|
+
...toolIdentity("tool.after", input),
|
|
1142
|
+
title: output.title,
|
|
1143
|
+
output: output.output,
|
|
1144
|
+
exit: meta.exit,
|
|
1145
|
+
truncated: meta.truncated
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
function toolIdentity(kind, input) {
|
|
1150
|
+
return { kind, tool: input.tool, session_id: input.sessionID, call_id: input.callID };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/plugin.ts
|
|
1154
|
+
function warn(scope, err) {
|
|
1155
|
+
process.stderr.write(`[pinta-opencode] ${scope}: ${err?.message ?? String(err)}
|
|
1156
|
+
`);
|
|
1157
|
+
}
|
|
1158
|
+
function failOpen(scope, fn) {
|
|
1159
|
+
return async (...args) => {
|
|
1160
|
+
try {
|
|
1161
|
+
await fn(...args);
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
warn(scope, err);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
var PintaOpencode = async (_input, options) => {
|
|
1168
|
+
const config = resolveConfig(options ?? {});
|
|
1169
|
+
const transport = new Transport({ endpoint: config.endpoint, headers: config.headers });
|
|
1170
|
+
const trace = new import_core4.MemorySessionTraceManager();
|
|
1171
|
+
const telemetry = new Telemetry(transport, trace, config);
|
|
1172
|
+
async function guardAndTrace(input, args) {
|
|
1173
|
+
let guard = null;
|
|
1174
|
+
try {
|
|
1175
|
+
guard = await evaluateGuard(
|
|
1176
|
+
{
|
|
1177
|
+
spanId: input.sessionID,
|
|
1178
|
+
toolName: input.tool,
|
|
1179
|
+
toolInput: args,
|
|
1180
|
+
rawTextFields: { toolInput: safeStringify(args) }
|
|
1181
|
+
},
|
|
1182
|
+
config.guardEndpoint,
|
|
1183
|
+
{ timeoutMs: config.guardTimeoutMs, token: config.relayToken, disabled: config.guardDisabled }
|
|
1184
|
+
);
|
|
1185
|
+
await telemetry.toolBefore(input, args, guard);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
warn("tool.execute.before", err);
|
|
1188
|
+
}
|
|
1189
|
+
return guard;
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
// turn-START → rotate a new trace for this session.
|
|
1193
|
+
"chat.message": failOpen("chat.message", async (input) => {
|
|
1194
|
+
trace.newTrace(input?.sessionID);
|
|
1195
|
+
}),
|
|
1196
|
+
// lifecycle telemetry; flushes the retry buffer on session.idle (turn-END).
|
|
1197
|
+
event: failOpen("event", async (input) => {
|
|
1198
|
+
if (input?.event) await telemetry.lifecycle(input.event);
|
|
1199
|
+
}),
|
|
1200
|
+
// ★ governance gate: guard query → DENY throws (blocks just this tool).
|
|
1201
|
+
// Telemetry/guard-infra errors are fail-open; only a DENY decision escapes.
|
|
1202
|
+
"tool.execute.before": async (input, output) => {
|
|
1203
|
+
const guard = await guardAndTrace(input, output?.args);
|
|
1204
|
+
if (guard?.decision === "DENY") {
|
|
1205
|
+
throw new Error(guard.userMessage ?? guard.reason ?? "guard_deny");
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
// tool-result telemetry.
|
|
1209
|
+
"tool.execute.after": failOpen(
|
|
1210
|
+
"tool.execute.after",
|
|
1211
|
+
(input, output) => telemetry.toolAfter(input, output)
|
|
1212
|
+
)
|
|
1213
|
+
};
|
|
1214
|
+
};
|
|
1215
|
+
function safeStringify(v) {
|
|
1216
|
+
try {
|
|
1217
|
+
return typeof v === "string" ? v : JSON.stringify(v) ?? "";
|
|
1218
|
+
} catch {
|
|
1219
|
+
return String(v);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var plugin_default = PintaOpencode;
|
|
1223
|
+
|
|
1224
|
+
// src/lifecycle/scanner.ts
|
|
1225
|
+
import { opendir, stat as stat2 } from "node:fs/promises";
|
|
1226
|
+
import { homedir } from "node:os";
|
|
1227
|
+
import path2 from "node:path";
|
|
1228
|
+
|
|
1229
|
+
// src/lifecycle/snapshot.ts
|
|
1230
|
+
import { execFile } from "node:child_process";
|
|
1231
|
+
import { copyFile, mkdtemp, stat } from "node:fs/promises";
|
|
1232
|
+
import { tmpdir } from "node:os";
|
|
1233
|
+
import path from "node:path";
|
|
1234
|
+
import { promisify } from "node:util";
|
|
1235
|
+
var execFileAsync = promisify(execFile);
|
|
1236
|
+
var DEFAULT_STABLE_MS = 2e3;
|
|
1237
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1238
|
+
var sqlite3Probe;
|
|
1239
|
+
function detectSqlite3() {
|
|
1240
|
+
if (sqlite3Probe === void 0) {
|
|
1241
|
+
sqlite3Probe = execFileAsync("sqlite3", ["-version"]).then(() => "sqlite3").catch(() => null);
|
|
1242
|
+
}
|
|
1243
|
+
return sqlite3Probe;
|
|
1244
|
+
}
|
|
1245
|
+
async function sleep(ms) {
|
|
1246
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1247
|
+
}
|
|
1248
|
+
async function makeSnapshotDir(base) {
|
|
1249
|
+
return mkdtemp(path.join(base, "pinta-opencode-snap-"));
|
|
1250
|
+
}
|
|
1251
|
+
async function backupViaCli(sqlite3Path, dbPath, outPath) {
|
|
1252
|
+
const backupCmd = `.backup '${outPath.replace(/'/g, "''")}'`;
|
|
1253
|
+
await execFileAsync(sqlite3Path, ["-cmd", ".timeout 5000", dbPath, backupCmd]);
|
|
1254
|
+
}
|
|
1255
|
+
async function quiesceCopy(file, outDir, stableMs, maxRetries) {
|
|
1256
|
+
const dbPath = file.absPath;
|
|
1257
|
+
const outDbPath = path.join(outDir, path.basename(dbPath));
|
|
1258
|
+
let lastErr;
|
|
1259
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1260
|
+
try {
|
|
1261
|
+
let before = await stat(dbPath);
|
|
1262
|
+
while (true) {
|
|
1263
|
+
await sleep(stableMs);
|
|
1264
|
+
const now = await stat(dbPath);
|
|
1265
|
+
if (now.mtimeMs === before.mtimeMs && now.size === before.size) break;
|
|
1266
|
+
before = now;
|
|
1267
|
+
}
|
|
1268
|
+
await copyFile(dbPath, outDbPath);
|
|
1269
|
+
await copyIfPresent(`${dbPath}-wal`, `${outDbPath}-wal`);
|
|
1270
|
+
await copyIfPresent(`${dbPath}-shm`, `${outDbPath}-shm`);
|
|
1271
|
+
const after = await stat(dbPath);
|
|
1272
|
+
if (after.mtimeMs === before.mtimeMs && after.size === before.size) {
|
|
1273
|
+
return outDbPath;
|
|
1274
|
+
}
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
lastErr = err;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
await stat(outDbPath);
|
|
1281
|
+
return outDbPath;
|
|
1282
|
+
} catch {
|
|
1283
|
+
throw lastErr ?? new Error(`snapshot quiesce-copy failed for ${dbPath}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async function copyIfPresent(src, dst) {
|
|
1287
|
+
try {
|
|
1288
|
+
await copyFile(src, dst);
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async function snapshot(file, opts = {}) {
|
|
1293
|
+
const base = opts.tmpDir ?? tmpdir();
|
|
1294
|
+
const stableMs = opts.stableMs ?? DEFAULT_STABLE_MS;
|
|
1295
|
+
const maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1296
|
+
const sqlite3Path = opts.sqlite3Path === void 0 ? await detectSqlite3() : opts.sqlite3Path;
|
|
1297
|
+
const outDir = await makeSnapshotDir(base);
|
|
1298
|
+
if (sqlite3Path) {
|
|
1299
|
+
const outPath = path.join(outDir, `${path.basename(file.absPath)}.snapshot`);
|
|
1300
|
+
try {
|
|
1301
|
+
await backupViaCli(sqlite3Path, file.absPath, outPath);
|
|
1302
|
+
return outPath;
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return quiesceCopy(file, outDir, stableMs, maxRetries);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/lifecycle/scanner.ts
|
|
1310
|
+
var WRAPPER_ID = "pinta-opencode";
|
|
1311
|
+
function dataRoot() {
|
|
1312
|
+
const explicit = process.env.OPENCODE_DATA;
|
|
1313
|
+
if (explicit && explicit.length > 0) {
|
|
1314
|
+
return explicit;
|
|
1315
|
+
}
|
|
1316
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
1317
|
+
if (xdg && xdg.length > 0) {
|
|
1318
|
+
return path2.join(xdg, "opencode");
|
|
1319
|
+
}
|
|
1320
|
+
return path2.join(homedir(), ".local", "share", "opencode");
|
|
1321
|
+
}
|
|
1322
|
+
function toPosixRelPath(root, absPath) {
|
|
1323
|
+
return path2.relative(root, absPath).split(path2.sep).join("/");
|
|
1324
|
+
}
|
|
1325
|
+
function isExcluded(relPath) {
|
|
1326
|
+
if (relPath === "auth.json") return true;
|
|
1327
|
+
const top = relPath.split("/")[0];
|
|
1328
|
+
if (top === "log" || top === "repos") return true;
|
|
1329
|
+
if (relPath.endsWith(".db-wal") || relPath.endsWith(".db-shm")) return true;
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
function isExcludedDir(relPath) {
|
|
1333
|
+
return relPath === "log" || relPath === "repos";
|
|
1334
|
+
}
|
|
1335
|
+
function semanticsFor(relPath) {
|
|
1336
|
+
return relPath.endsWith(".db") ? "database" : "rewritten-doc";
|
|
1337
|
+
}
|
|
1338
|
+
function sessionIdFor(relPath) {
|
|
1339
|
+
const base = path2.posix.basename(relPath);
|
|
1340
|
+
if (!base.startsWith("ses_")) return void 0;
|
|
1341
|
+
const ext = path2.posix.extname(base);
|
|
1342
|
+
return ext ? base.slice(0, -ext.length) : base;
|
|
1343
|
+
}
|
|
1344
|
+
function classify(relPath) {
|
|
1345
|
+
if (relPath.endsWith(".db")) {
|
|
1346
|
+
return "session-log";
|
|
1347
|
+
}
|
|
1348
|
+
if (relPath.endsWith(".json")) {
|
|
1349
|
+
return "meta";
|
|
1350
|
+
}
|
|
1351
|
+
return "other";
|
|
1352
|
+
}
|
|
1353
|
+
async function roots() {
|
|
1354
|
+
return [dataRoot()];
|
|
1355
|
+
}
|
|
1356
|
+
async function* walkFiles(root, dir) {
|
|
1357
|
+
let entries;
|
|
1358
|
+
try {
|
|
1359
|
+
entries = await opendir(dir);
|
|
1360
|
+
} catch {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
for await (const entry of entries) {
|
|
1365
|
+
const absPath = path2.join(dir, entry.name);
|
|
1366
|
+
const relPath = toPosixRelPath(root, absPath);
|
|
1367
|
+
if (entry.isDirectory()) {
|
|
1368
|
+
if (isExcludedDir(relPath)) continue;
|
|
1369
|
+
yield* walkFiles(root, absPath);
|
|
1370
|
+
} else if (entry.isFile()) {
|
|
1371
|
+
if (isExcluded(relPath)) continue;
|
|
1372
|
+
yield { absPath, relPath };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
} catch {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async function* scan(opts) {
|
|
1380
|
+
const [root] = await roots();
|
|
1381
|
+
const sinceMs = opts.since?.getTime();
|
|
1382
|
+
for await (const { absPath, relPath } of walkFiles(root, root)) {
|
|
1383
|
+
let st;
|
|
1384
|
+
try {
|
|
1385
|
+
st = await stat2(absPath);
|
|
1386
|
+
} catch {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (sinceMs !== void 0 && st.mtime.getTime() <= sinceMs) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
yield {
|
|
1393
|
+
relPath,
|
|
1394
|
+
absPath,
|
|
1395
|
+
size: st.size,
|
|
1396
|
+
mtime: st.mtime,
|
|
1397
|
+
sessionId: sessionIdFor(relPath),
|
|
1398
|
+
semantics: semanticsFor(relPath)
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
var lifecycle = {
|
|
1403
|
+
id: WRAPPER_ID,
|
|
1404
|
+
roots,
|
|
1405
|
+
scan,
|
|
1406
|
+
classify,
|
|
1407
|
+
// `database` files (opencode.db) must be snapshot()'d before read — the live
|
|
1408
|
+
// file has `-wal`/`-shm` and would torn-read (plan §4.2, §6).
|
|
1409
|
+
snapshot
|
|
1410
|
+
};
|
|
1411
|
+
export {
|
|
1412
|
+
PintaOpencode,
|
|
1413
|
+
classify,
|
|
1414
|
+
plugin_default as default,
|
|
1415
|
+
lifecycle,
|
|
1416
|
+
roots
|
|
1417
|
+
};
|