@grudanov-nikolay/agenttrace-opencode-plugin 0.0.2
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 +22 -0
- package/README.md +102 -0
- package/dist/index.cjs +2430 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +2474 -0
- package/package.json +42 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2430 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if ((from && typeof from === "object") || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
17
|
+
|
|
18
|
+
// src/index.ts
|
|
19
|
+
var index_exports = {};
|
|
20
|
+
__export(index_exports, {
|
|
21
|
+
default: () => plugin,
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(index_exports);
|
|
24
|
+
|
|
25
|
+
// ../core/dist/chunk-WKRW55KX.js
|
|
26
|
+
function getCrypto() {
|
|
27
|
+
const c = globalThis.crypto;
|
|
28
|
+
return c;
|
|
29
|
+
}
|
|
30
|
+
function randomBytes(length) {
|
|
31
|
+
const cryptoObj = getCrypto();
|
|
32
|
+
const out = new Uint8Array(length);
|
|
33
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
|
|
34
|
+
cryptoObj.getRandomValues(out);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function base64Encode(bytes) {
|
|
41
|
+
const maybeBuffer = globalThis.Buffer;
|
|
42
|
+
if (maybeBuffer) {
|
|
43
|
+
return maybeBuffer.from(bytes).toString("base64");
|
|
44
|
+
}
|
|
45
|
+
let binary = "";
|
|
46
|
+
for (let i2 = 0; i2 < bytes.length; i2++) {
|
|
47
|
+
binary += String.fromCharCode(bytes[i2]);
|
|
48
|
+
}
|
|
49
|
+
const btoaFn = globalThis.btoa;
|
|
50
|
+
if (typeof btoaFn === "function") return btoaFn(binary);
|
|
51
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
52
|
+
let out = "";
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < binary.length) {
|
|
55
|
+
const c1 = binary.charCodeAt(i++) & 255;
|
|
56
|
+
const c2 = i < binary.length ? binary.charCodeAt(i++) & 255 : NaN;
|
|
57
|
+
const c3 = i < binary.length ? binary.charCodeAt(i++) & 255 : NaN;
|
|
58
|
+
const e1 = c1 >> 2;
|
|
59
|
+
const e2 = ((c1 & 3) << 4) | (Number.isNaN(c2) ? 0 : c2 >> 4);
|
|
60
|
+
const e3 = Number.isNaN(c2) ? 64 : ((c2 & 15) << 2) | (Number.isNaN(c3) ? 0 : c3 >> 6);
|
|
61
|
+
const e4 = Number.isNaN(c3) ? 64 : c3 & 63;
|
|
62
|
+
out += alphabet.charAt(e1);
|
|
63
|
+
out += alphabet.charAt(e2);
|
|
64
|
+
out += e3 === 64 ? "=" : alphabet.charAt(e3);
|
|
65
|
+
out += e4 === 64 ? "=" : alphabet.charAt(e4);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function generateId() {
|
|
70
|
+
return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
function runWithTracingSuppressed(fn) {
|
|
73
|
+
const hook = globalThis.RAINDROP_SUPPRESS_TRACING;
|
|
74
|
+
if (typeof hook !== "function") return fn();
|
|
75
|
+
let started = false;
|
|
76
|
+
try {
|
|
77
|
+
return hook(() => {
|
|
78
|
+
started = true;
|
|
79
|
+
return fn();
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (started) throw err;
|
|
83
|
+
return fn();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
87
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
88
|
+
function wait(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
function formatEndpoint(endpoint) {
|
|
92
|
+
if (!endpoint) return void 0;
|
|
93
|
+
return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
|
|
94
|
+
}
|
|
95
|
+
function redactUrlForLog(url) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(url);
|
|
98
|
+
parsed.username = "";
|
|
99
|
+
parsed.password = "";
|
|
100
|
+
parsed.search = "";
|
|
101
|
+
parsed.hash = "";
|
|
102
|
+
return parsed.toString();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return "<unparseable-url>";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
|
|
108
|
+
var rateLimitedLogLast = /* @__PURE__ */ new Map();
|
|
109
|
+
function rateLimitedLog(key, log) {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const last = rateLimitedLogLast.get(key);
|
|
112
|
+
if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
rateLimitedLogLast.set(key, now);
|
|
116
|
+
log();
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
async function raceWithTimeout(promise, timeoutMs) {
|
|
120
|
+
let timer;
|
|
121
|
+
const settledInTime = await Promise.race([
|
|
122
|
+
promise.then(
|
|
123
|
+
() => true,
|
|
124
|
+
() => true,
|
|
125
|
+
),
|
|
126
|
+
new Promise((resolve) => {
|
|
127
|
+
var _a;
|
|
128
|
+
timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
|
|
129
|
+
(_a = timer.unref) == null ? void 0 : _a.call(timer);
|
|
130
|
+
}),
|
|
131
|
+
]);
|
|
132
|
+
if (timer) clearTimeout(timer);
|
|
133
|
+
return settledInTime;
|
|
134
|
+
}
|
|
135
|
+
function parseRetryAfter(headers) {
|
|
136
|
+
var _a;
|
|
137
|
+
const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
|
|
138
|
+
if (!value) return void 0;
|
|
139
|
+
const asNumber = Number(value);
|
|
140
|
+
if (value.trim() !== "" && !Number.isNaN(asNumber)) return asNumber * 1e3;
|
|
141
|
+
const asDate = new Date(value).getTime();
|
|
142
|
+
if (!Number.isNaN(asDate)) {
|
|
143
|
+
const delta = asDate - Date.now();
|
|
144
|
+
return delta > 0 ? delta : 0;
|
|
145
|
+
}
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
function getRetryDelayMs(attemptNumber, previousError) {
|
|
149
|
+
if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
|
|
150
|
+
const v = previousError.retryAfterMs;
|
|
151
|
+
if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
|
|
152
|
+
}
|
|
153
|
+
if (attemptNumber <= 1) return 0;
|
|
154
|
+
const base = 500;
|
|
155
|
+
const factor = Math.pow(2, attemptNumber - 2);
|
|
156
|
+
return Math.min(base * factor, MAX_RETRY_DELAY_MS);
|
|
157
|
+
}
|
|
158
|
+
async function withRetry(operation, opName, opts) {
|
|
159
|
+
const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
|
|
160
|
+
let lastError = void 0;
|
|
161
|
+
for (let attemptNumber = 1; attemptNumber <= opts.maxAttempts; attemptNumber++) {
|
|
162
|
+
if (attemptNumber > 1) {
|
|
163
|
+
const delay = getRetryDelayMs(attemptNumber, lastError);
|
|
164
|
+
if (opts.debug) {
|
|
165
|
+
console.warn(`${prefix} ${opName} retry ${attemptNumber}/${opts.maxAttempts} in ${delay}ms`);
|
|
166
|
+
}
|
|
167
|
+
if (delay > 0) await wait(delay);
|
|
168
|
+
} else if (opts.debug) {
|
|
169
|
+
console.log(`${prefix} ${opName} attempt ${attemptNumber}/${opts.maxAttempts}`);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return await operation();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
lastError = err;
|
|
175
|
+
if (opts.debug) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
console.warn(`${prefix} ${opName} attempt ${attemptNumber} failed: ${msg}${attemptNumber === opts.maxAttempts ? " (no more retries)" : ""}`);
|
|
178
|
+
}
|
|
179
|
+
if (lastError && typeof lastError === "object" && "retryable" in lastError && !lastError.retryable) break;
|
|
180
|
+
if (attemptNumber === opts.maxAttempts) break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
184
|
+
}
|
|
185
|
+
async function postJson(url, body, headers, opts) {
|
|
186
|
+
var _a;
|
|
187
|
+
const opName = `POST ${redactUrlForLog(url)}`;
|
|
188
|
+
const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
189
|
+
await withRetry(
|
|
190
|
+
async () => {
|
|
191
|
+
const resp = await runWithTracingSuppressed(() =>
|
|
192
|
+
fetch(url, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
...headers,
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify(body),
|
|
199
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
if (!resp.ok) {
|
|
203
|
+
const text = await resp.text().catch(() => "");
|
|
204
|
+
const err = new Error(`HTTP ${resp.status} ${resp.statusText}${text ? `: ${text}` : ""}`);
|
|
205
|
+
const retryAfterMs = parseRetryAfter(resp.headers);
|
|
206
|
+
if (typeof retryAfterMs === "number") err.retryAfterMs = retryAfterMs;
|
|
207
|
+
err.retryable = resp.status === 429 || resp.status >= 500;
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
opName,
|
|
212
|
+
opts,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
|
|
216
|
+
var TRUNCATION_MARKER = "...[truncated by raindrop]";
|
|
217
|
+
var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
|
|
218
|
+
function resolveMaxTextFieldChars(value) {
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
220
|
+
return Math.floor(value);
|
|
221
|
+
}
|
|
222
|
+
return currentDefaultMaxTextFieldChars;
|
|
223
|
+
}
|
|
224
|
+
function truncateToLimit(text, limit) {
|
|
225
|
+
if (limit > TRUNCATION_MARKER.length) {
|
|
226
|
+
return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
227
|
+
}
|
|
228
|
+
return text.slice(0, Math.max(0, limit));
|
|
229
|
+
}
|
|
230
|
+
function capText(value, limit) {
|
|
231
|
+
if (typeof value !== "string") return value;
|
|
232
|
+
const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
|
|
233
|
+
if (value.length <= max) return value;
|
|
234
|
+
return truncateToLimit(value, max);
|
|
235
|
+
}
|
|
236
|
+
var SpanStatusCode = {
|
|
237
|
+
UNSET: 0,
|
|
238
|
+
OK: 1,
|
|
239
|
+
ERROR: 2,
|
|
240
|
+
};
|
|
241
|
+
function createSpanIds(parent) {
|
|
242
|
+
const traceId = parent ? parent.traceIdB64 : base64Encode(randomBytes(16));
|
|
243
|
+
const spanId = base64Encode(randomBytes(8));
|
|
244
|
+
return {
|
|
245
|
+
traceIdB64: traceId,
|
|
246
|
+
spanIdB64: spanId,
|
|
247
|
+
parentSpanIdB64: parent ? parent.spanIdB64 : void 0,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function nowUnixNanoString() {
|
|
251
|
+
return Date.now().toString() + "000000";
|
|
252
|
+
}
|
|
253
|
+
function attrString(key, value) {
|
|
254
|
+
if (value === void 0) return void 0;
|
|
255
|
+
return { key, value: { stringValue: value } };
|
|
256
|
+
}
|
|
257
|
+
function attrInt(key, value) {
|
|
258
|
+
if (value === void 0) return void 0;
|
|
259
|
+
if (!Number.isFinite(value)) return void 0;
|
|
260
|
+
return { key, value: { intValue: String(Math.trunc(value)) } };
|
|
261
|
+
}
|
|
262
|
+
// KOLYA PATCH (F-003): mirror of extractTaskLabel in index.js. See ESM for rationale.
|
|
263
|
+
function extractTaskLabel(rawArgs) {
|
|
264
|
+
// F-010 hardening: OpenCode can deliver tool args as a JSON *string*
|
|
265
|
+
// (the after-hook already tolerates this shape via capText2). Parse
|
|
266
|
+
// before reading, otherwise the description never reaches the span.
|
|
267
|
+
let args = rawArgs;
|
|
268
|
+
if (typeof args === "string") {
|
|
269
|
+
const trimmed = args.trim();
|
|
270
|
+
if (!trimmed.startsWith("{")) return "";
|
|
271
|
+
try {
|
|
272
|
+
args = JSON.parse(trimmed);
|
|
273
|
+
} catch (_err) {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!args || typeof args !== "object") return "";
|
|
278
|
+
const desc = args.description;
|
|
279
|
+
if (typeof desc === "string" && desc.trim().length > 0) return desc.trim().slice(0, 120);
|
|
280
|
+
const prompt = args.prompt;
|
|
281
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) return prompt.trim().slice(0, 60);
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
// KOLYA PATCH (F-010): best-effort sub-agent name recovery from the task
|
|
285
|
+
// prompt text (the child session's first user message, i.e. chat.message
|
|
286
|
+
// parts). Recognises identity preambles: 'You are "name"' (quoted),
|
|
287
|
+
// 'You are name.' (bare), or an explicit 'name: value' header. Returns ""
|
|
288
|
+
// when nothing recognisable is found — Workshop then falls back to task N.
|
|
289
|
+
function extractSubagentNameFromPrompt(text) {
|
|
290
|
+
if (typeof text !== "string" || text.length === 0) return "";
|
|
291
|
+
const head = text.slice(0, 400);
|
|
292
|
+
let m = head.match(/you\s+are\s+["\u201c'\u00ab]([^"\u201d'\u00bb]{1,80})["\u201d'\u00bb]/i);
|
|
293
|
+
if (m) return m[1].trim().slice(0, 120);
|
|
294
|
+
m = head.match(/you\s+are\s+(?:the\s+|an?\s+)?([A-Za-z0-9][A-Za-z0-9._\- ]{1,60}?)(?:[.,;\n]|\s+that\s)/i);
|
|
295
|
+
if (m && m[1].trim().split(/\s+/).length <= 6) return m[1].trim().slice(0, 120);
|
|
296
|
+
m = head.match(/(?:^|\n)\s*["\u201c'\u00ab]?name["\u201d'\u00bb]?\s*[:=]\s*["\u201c'\u00ab]?([A-Za-z0-9][A-Za-z0-9._\- ]{0,60})/i);
|
|
297
|
+
if (m) return m[1].trim().slice(0, 120);
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
// KOLYA PATCH (F-010 v2): see index.js. In OpenCode 1.18 the child's first
|
|
301
|
+
// user message has no identity preamble (system prompt holds it), so we fall
|
|
302
|
+
// back to args.subagent_type stashed by the parent tool.execute.after hook.
|
|
303
|
+
function extractSubagentNameFromTaskArgs(rawArgs) {
|
|
304
|
+
let args = rawArgs;
|
|
305
|
+
if (typeof args === "string") {
|
|
306
|
+
const trimmed = args.trim();
|
|
307
|
+
if (!trimmed.startsWith("{")) return "";
|
|
308
|
+
try {
|
|
309
|
+
args = JSON.parse(trimmed);
|
|
310
|
+
} catch (_err) {
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!args || typeof args !== "object") return "";
|
|
315
|
+
const st = args.subagent_type;
|
|
316
|
+
if (typeof st === "string" && st.trim().length > 0 && /^[A-Za-z0-9._\-]{1,64}$/.test(st.trim())) {
|
|
317
|
+
return st.trim();
|
|
318
|
+
}
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
function buildOtlpSpan(args) {
|
|
322
|
+
const attrs = args.attributes.filter((x) => x !== void 0);
|
|
323
|
+
const span = {
|
|
324
|
+
traceId: args.ids.traceIdB64,
|
|
325
|
+
spanId: args.ids.spanIdB64,
|
|
326
|
+
name: args.name,
|
|
327
|
+
startTimeUnixNano: args.startTimeUnixNano,
|
|
328
|
+
endTimeUnixNano: args.endTimeUnixNano,
|
|
329
|
+
};
|
|
330
|
+
if (args.ids.parentSpanIdB64) span.parentSpanId = args.ids.parentSpanIdB64;
|
|
331
|
+
if (attrs.length) span.attributes = attrs;
|
|
332
|
+
if (args.status) span.status = args.status;
|
|
333
|
+
return span;
|
|
334
|
+
}
|
|
335
|
+
function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", serviceVersion = "0.0.0") {
|
|
336
|
+
return {
|
|
337
|
+
resourceSpans: [
|
|
338
|
+
{
|
|
339
|
+
resource: {
|
|
340
|
+
attributes: [{ key: "service.name", value: { stringValue: serviceName } }],
|
|
341
|
+
},
|
|
342
|
+
scopeSpans: [
|
|
343
|
+
{
|
|
344
|
+
scope: { name: serviceName, version: serviceVersion },
|
|
345
|
+
spans,
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
|
|
353
|
+
var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
|
|
354
|
+
var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
|
|
355
|
+
function readEnvVar(name) {
|
|
356
|
+
var _a;
|
|
357
|
+
try {
|
|
358
|
+
const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
|
|
359
|
+
if (env && typeof env[name] === "string" && env[name].length > 0) {
|
|
360
|
+
return env[name];
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function readWorkshopEnv() {
|
|
366
|
+
const raw = readEnvVar(WORKSHOP_ENV_VAR);
|
|
367
|
+
if (raw === void 0) return void 0;
|
|
368
|
+
const trimmed = raw.trim();
|
|
369
|
+
if (trimmed.length === 0) return void 0;
|
|
370
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
371
|
+
// KOLYA PATCH (F-005): same precedence guard.
|
|
372
|
+
if (!isLocalUrl(trimmed)) {
|
|
373
|
+
rateLimitedLog("workshop_env_non_local", () => console.warn(`[oc-wsp] [warn] RAINDROP_WORKSHOP=${trimmed} is not a local URL; ignoring.`));
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
return { url: trimmed };
|
|
377
|
+
}
|
|
378
|
+
if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
|
|
379
|
+
if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function isLocalDevHost(hostname) {
|
|
383
|
+
if (!hostname) return false;
|
|
384
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
// KOLYA PATCH (F-005): URL-level local-host check used by env-var precedence.
|
|
391
|
+
function isLocalUrl(value) {
|
|
392
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
393
|
+
let hostname;
|
|
394
|
+
try { hostname = new URL(value).hostname.toLowerCase(); } catch (_e) { return false; }
|
|
395
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") return true;
|
|
396
|
+
if (hostname === "[::1]" || hostname.startsWith("::ffff:127.") || hostname.startsWith("::ffff:7f00:1")) return true;
|
|
397
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
function readRuntimeHostname() {
|
|
401
|
+
try {
|
|
402
|
+
const loc = globalThis == null ? void 0 : globalThis.location;
|
|
403
|
+
if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
|
|
404
|
+
return loc.hostname;
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {}
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
function shouldAutoEnableLocalWorkshop() {
|
|
410
|
+
if (isLocalDevHost(readRuntimeHostname())) return true;
|
|
411
|
+
if (readEnvVar("NODE_ENV") === "development") return true;
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
function resolveLocalDebuggerBaseUrl(baseUrl) {
|
|
415
|
+
var _a, _b, _c;
|
|
416
|
+
if (baseUrl === null) return null;
|
|
417
|
+
if (typeof baseUrl === "string" && baseUrl.length > 0) {
|
|
418
|
+
return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
|
|
419
|
+
}
|
|
420
|
+
const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
|
|
421
|
+
if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
|
|
422
|
+
const workshopEnv = readWorkshopEnv();
|
|
423
|
+
if (workshopEnv === "disable") return null;
|
|
424
|
+
if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
425
|
+
if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
|
|
426
|
+
if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
function mirrorTraceExportToLocalDebugger(body, options = {}) {
|
|
430
|
+
var _a;
|
|
431
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
432
|
+
if (!baseUrl) return;
|
|
433
|
+
void postJson(
|
|
434
|
+
`${baseUrl}traces`,
|
|
435
|
+
body,
|
|
436
|
+
{},
|
|
437
|
+
{
|
|
438
|
+
maxAttempts: 1,
|
|
439
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
440
|
+
sdkName: options.sdkName,
|
|
441
|
+
},
|
|
442
|
+
).catch(() => {});
|
|
443
|
+
}
|
|
444
|
+
function mirrorPartialEventToLocalDebugger(event, options = {}) {
|
|
445
|
+
var _a;
|
|
446
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
447
|
+
if (!baseUrl) return;
|
|
448
|
+
const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
|
|
449
|
+
void postJson(`${baseUrl}events/track_partial`, event, headers, {
|
|
450
|
+
maxAttempts: 1,
|
|
451
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
452
|
+
sdkName: options.sdkName,
|
|
453
|
+
}).catch(() => {});
|
|
454
|
+
}
|
|
455
|
+
var PROJECT_ID_HEADER = "X-Raindrop-Project-Id";
|
|
456
|
+
var PROJECT_ID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
457
|
+
function isValidProjectIdSlug(value) {
|
|
458
|
+
return PROJECT_ID_SLUG_PATTERN.test(value);
|
|
459
|
+
}
|
|
460
|
+
function normalizeProjectId(raw, opts) {
|
|
461
|
+
if (typeof raw !== "string") return void 0;
|
|
462
|
+
const trimmed = raw.trim();
|
|
463
|
+
if (!trimmed) return void 0;
|
|
464
|
+
if (!isValidProjectIdSlug(trimmed) && opts.debug) {
|
|
465
|
+
console.warn(`${opts.prefix} projectId "${trimmed}" does not match slug ${PROJECT_ID_SLUG_PATTERN.source}; sending anyway \u2014 backend may reject with HTTP 400`);
|
|
466
|
+
}
|
|
467
|
+
return trimmed;
|
|
468
|
+
}
|
|
469
|
+
function projectIdHeaders(projectId) {
|
|
470
|
+
return projectId ? { [PROJECT_ID_HEADER]: projectId } : {};
|
|
471
|
+
}
|
|
472
|
+
var SHUTDOWN_DEADLINE_MS = 1e4;
|
|
473
|
+
var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
474
|
+
function mergePatches(target, source) {
|
|
475
|
+
var _a, _b, _c, _d;
|
|
476
|
+
const out = { ...target, ...source };
|
|
477
|
+
if (target.properties || source.properties) {
|
|
478
|
+
out.properties = { ...((_a = target.properties) != null ? _a : {}), ...((_b = source.properties) != null ? _b : {}) };
|
|
479
|
+
}
|
|
480
|
+
if (target.attachments || source.attachments) {
|
|
481
|
+
out.attachments = [...((_c = target.attachments) != null ? _c : []), ...((_d = source.attachments) != null ? _d : [])];
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
var EventShipper = class {
|
|
486
|
+
constructor(opts) {
|
|
487
|
+
this.buffers = /* @__PURE__ */ new Map();
|
|
488
|
+
this.sticky = /* @__PURE__ */ new Map();
|
|
489
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
490
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
491
|
+
this.hasShutdown = false;
|
|
492
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
493
|
+
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
494
|
+
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
495
|
+
this.enabled = opts.enabled !== false;
|
|
496
|
+
this.debug = opts.debug;
|
|
497
|
+
this.partialFlushMs = (_c = opts.partialFlushMs) != null ? _c : 1e3;
|
|
498
|
+
this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
|
|
499
|
+
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
500
|
+
this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
|
|
501
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
502
|
+
this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
|
|
503
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
504
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
505
|
+
}
|
|
506
|
+
this.projectId = normalizeProjectId(opts.projectId, {
|
|
507
|
+
debug: this.debug,
|
|
508
|
+
prefix: this.prefix,
|
|
509
|
+
});
|
|
510
|
+
const isNode = typeof process !== "undefined" && typeof process.version === "string";
|
|
511
|
+
this.context = {
|
|
512
|
+
library: {
|
|
513
|
+
name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
|
|
514
|
+
version: (_h = opts.libraryVersion) != null ? _h : "0.0.0",
|
|
515
|
+
},
|
|
516
|
+
metadata: {
|
|
517
|
+
jsRuntime: isNode ? "node" : "web",
|
|
518
|
+
...(isNode ? { nodeVersion: process.version } : {}),
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
isDebugEnabled() {
|
|
523
|
+
return this.debug;
|
|
524
|
+
}
|
|
525
|
+
authHeaders() {
|
|
526
|
+
return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
|
|
527
|
+
}
|
|
528
|
+
requestHeaders() {
|
|
529
|
+
return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Build the retry/timeout options for one POST, honoring the shutdown
|
|
533
|
+
* deadline. Returns `null` when the shutdown drain window is exhausted —
|
|
534
|
+
* the caller must drop the payload (with a rate-limited warning) instead
|
|
535
|
+
* of issuing a request that could outlive process exit.
|
|
536
|
+
*
|
|
537
|
+
* Checked fresh on EVERY send, so a shutdown that begins while the flush
|
|
538
|
+
* path is mid-drain takes effect immediately: no further retries, and the
|
|
539
|
+
* per-attempt timeout is clamped to the remaining window. After
|
|
540
|
+
* `shutdown()` returns (deadline cleared, `hasShutdown` still set),
|
|
541
|
+
* sends — late callers, or flush work the deadline abandoned mid-drain —
|
|
542
|
+
* run as a single short attempt rather than regaining the full retry
|
|
543
|
+
* schedule.
|
|
544
|
+
*/
|
|
545
|
+
requestOpts() {
|
|
546
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
547
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
548
|
+
if (remainingMs <= 0) return null;
|
|
549
|
+
return {
|
|
550
|
+
maxAttempts: 1,
|
|
551
|
+
debug: this.debug,
|
|
552
|
+
sdkName: this.sdkName,
|
|
553
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (this.hasShutdown) {
|
|
557
|
+
return {
|
|
558
|
+
maxAttempts: 1,
|
|
559
|
+
debug: this.debug,
|
|
560
|
+
sdkName: this.sdkName,
|
|
561
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
565
|
+
}
|
|
566
|
+
async patch(eventId, patch) {
|
|
567
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
568
|
+
if (!this.enabled) return;
|
|
569
|
+
if (!eventId || !eventId.trim()) return;
|
|
570
|
+
const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
|
|
571
|
+
if (typeof patch.input === "string" && patch.input.length > maxChars) {
|
|
572
|
+
patch = { ...patch, input: capText(patch.input, maxChars) };
|
|
573
|
+
}
|
|
574
|
+
if (typeof patch.output === "string" && patch.output.length > maxChars) {
|
|
575
|
+
patch = { ...patch, output: capText(patch.output, maxChars) };
|
|
576
|
+
}
|
|
577
|
+
if (this.debug) {
|
|
578
|
+
console.log(`${this.prefix} queue patch`, {
|
|
579
|
+
eventId,
|
|
580
|
+
userId: patch.userId,
|
|
581
|
+
convoId: patch.convoId,
|
|
582
|
+
eventName: patch.eventName,
|
|
583
|
+
hasInput: typeof patch.input === "string" && patch.input.length > 0,
|
|
584
|
+
hasOutput: typeof patch.output === "string" && patch.output.length > 0,
|
|
585
|
+
attachments: (_b = (_a = patch.attachments) == null ? void 0 : _a.length) != null ? _b : 0,
|
|
586
|
+
isPending: patch.isPending,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
const sticky = (_c = this.sticky.get(eventId)) != null ? _c : {};
|
|
590
|
+
const existing = (_d = this.buffers.get(eventId)) != null ? _d : {};
|
|
591
|
+
const merged = mergePatches(existing, patch);
|
|
592
|
+
merged.isPending = (_g = (_f = (_e = patch.isPending) != null ? _e : existing.isPending) != null ? _f : sticky.isPending) != null ? _g : true;
|
|
593
|
+
this.buffers.set(eventId, merged);
|
|
594
|
+
this.sticky.set(eventId, {
|
|
595
|
+
userId: (_h = merged.userId) != null ? _h : sticky.userId,
|
|
596
|
+
convoId: (_i = merged.convoId) != null ? _i : sticky.convoId,
|
|
597
|
+
eventName: (_j = merged.eventName) != null ? _j : sticky.eventName,
|
|
598
|
+
isPending: (_k = merged.isPending) != null ? _k : sticky.isPending,
|
|
599
|
+
});
|
|
600
|
+
const t = this.timers.get(eventId);
|
|
601
|
+
if (t) clearTimeout(t);
|
|
602
|
+
if (merged.isPending === false) {
|
|
603
|
+
await this.flushOne(eventId);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const timeout = setTimeout(() => {
|
|
607
|
+
void this.flushOne(eventId).catch(() => {});
|
|
608
|
+
}, this.partialFlushMs);
|
|
609
|
+
this.timers.set(eventId, timeout);
|
|
610
|
+
}
|
|
611
|
+
async finish(eventId, patch) {
|
|
612
|
+
await this.patch(eventId, { ...patch, isPending: false });
|
|
613
|
+
}
|
|
614
|
+
async flush() {
|
|
615
|
+
if (!this.enabled) return;
|
|
616
|
+
const ids = [...this.buffers.keys()];
|
|
617
|
+
await Promise.all(ids.map((id) => this.flushOne(id)));
|
|
618
|
+
await Promise.all([...this.inFlight].map((p) => p.catch(() => {})));
|
|
619
|
+
}
|
|
620
|
+
async shutdown() {
|
|
621
|
+
this.hasShutdown = true;
|
|
622
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
623
|
+
try {
|
|
624
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
625
|
+
this.timers.clear();
|
|
626
|
+
const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
|
|
627
|
+
if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
|
|
628
|
+
} finally {
|
|
629
|
+
this.shutdownDeadlineAt = void 0;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async trackSignal(signal) {
|
|
633
|
+
var _a, _b;
|
|
634
|
+
if (!this.enabled) return;
|
|
635
|
+
const body = [
|
|
636
|
+
{
|
|
637
|
+
event_id: signal.eventId,
|
|
638
|
+
signal_name: signal.name,
|
|
639
|
+
signal_type: (_a = signal.type) != null ? _a : "default",
|
|
640
|
+
timestamp: signal.timestamp,
|
|
641
|
+
sentiment: signal.sentiment,
|
|
642
|
+
attachment_id: signal.attachmentId,
|
|
643
|
+
properties: {
|
|
644
|
+
...((_b = signal.properties) != null ? _b : {}),
|
|
645
|
+
...(signal.comment ? { comment: signal.comment } : {}),
|
|
646
|
+
...(signal.after ? { after: signal.after } : {}),
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
if (!this.writeKey) return;
|
|
651
|
+
const url = `${this.baseUrl}signals/track`;
|
|
652
|
+
const opts = this.requestOpts();
|
|
653
|
+
if (!opts) {
|
|
654
|
+
this.warnShutdownDrop("signal");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
await postJson(url, body, this.requestHeaders(), opts);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
661
|
+
rateLimitedLog(`${this.prefix}.send_signal_failed`, () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async identify(users) {
|
|
665
|
+
if (!this.enabled) return;
|
|
666
|
+
const list = Array.isArray(users) ? users : [users];
|
|
667
|
+
const body = list
|
|
668
|
+
.filter((user) => {
|
|
669
|
+
if (!(user == null ? void 0 : user.userId) || !user.userId.trim()) {
|
|
670
|
+
if (this.debug) {
|
|
671
|
+
console.warn(`${this.prefix} skipping identify: missing userId`);
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
})
|
|
677
|
+
.map((user) => {
|
|
678
|
+
var _a;
|
|
679
|
+
return {
|
|
680
|
+
user_id: user.userId,
|
|
681
|
+
traits: (_a = user.traits) != null ? _a : {},
|
|
682
|
+
};
|
|
683
|
+
});
|
|
684
|
+
if (!this.writeKey) return;
|
|
685
|
+
if (body.length === 0) return;
|
|
686
|
+
const url = `${this.baseUrl}users/identify`;
|
|
687
|
+
const opts = this.requestOpts();
|
|
688
|
+
if (!opts) {
|
|
689
|
+
this.warnShutdownDrop("identify");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
await postJson(url, body, this.requestHeaders(), opts);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
696
|
+
rateLimitedLog(`${this.prefix}.send_identify_failed`, () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
warnShutdownDrop(what) {
|
|
700
|
+
rateLimitedLog(`${this.prefix}.shutdown_deadline`, () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`));
|
|
701
|
+
}
|
|
702
|
+
async flushOne(eventId) {
|
|
703
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
704
|
+
if (!this.enabled) return;
|
|
705
|
+
const timer = this.timers.get(eventId);
|
|
706
|
+
if (timer) {
|
|
707
|
+
clearTimeout(timer);
|
|
708
|
+
this.timers.delete(eventId);
|
|
709
|
+
}
|
|
710
|
+
const accumulated = this.buffers.get(eventId);
|
|
711
|
+
this.buffers.delete(eventId);
|
|
712
|
+
if (!accumulated) return;
|
|
713
|
+
const sticky = (_a = this.sticky.get(eventId)) != null ? _a : {};
|
|
714
|
+
const eventName = (_c = (_b = accumulated.eventName) != null ? _b : sticky.eventName) != null ? _c : this.defaultEventName;
|
|
715
|
+
const userId = (_d = accumulated.userId) != null ? _d : sticky.userId;
|
|
716
|
+
if (!userId) {
|
|
717
|
+
if (this.debug) {
|
|
718
|
+
console.warn(`${this.prefix} skipping track_partial for ${eventId}: missing userId`);
|
|
719
|
+
}
|
|
720
|
+
this.sticky.delete(eventId);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const { wizardSession, ...restProperties } = (_e = accumulated.properties) != null ? _e : {};
|
|
724
|
+
const convoId = (_f = accumulated.convoId) != null ? _f : sticky.convoId;
|
|
725
|
+
const isPending = (_h = (_g = accumulated.isPending) != null ? _g : sticky.isPending) != null ? _h : true;
|
|
726
|
+
const payload = {
|
|
727
|
+
event_id: eventId,
|
|
728
|
+
user_id: userId,
|
|
729
|
+
event: eventName,
|
|
730
|
+
timestamp: (_i = accumulated.timestamp) != null ? _i : /* @__PURE__ */ new Date().toISOString(),
|
|
731
|
+
ai_data: {
|
|
732
|
+
input: accumulated.input,
|
|
733
|
+
output: accumulated.output,
|
|
734
|
+
model: accumulated.model,
|
|
735
|
+
convo_id: convoId,
|
|
736
|
+
},
|
|
737
|
+
properties: {
|
|
738
|
+
...restProperties,
|
|
739
|
+
...(wizardSession ? { "raindrop.wizardSession": wizardSession } : {}),
|
|
740
|
+
...(this.gitContext && (this.gitContext.project || this.gitContext.branch || this.gitContext.commit) ? { git: this.gitContext } : {}),
|
|
741
|
+
$context: this.context,
|
|
742
|
+
},
|
|
743
|
+
attachments: accumulated.attachments,
|
|
744
|
+
is_pending: isPending,
|
|
745
|
+
};
|
|
746
|
+
const url = `${this.baseUrl}events/track_partial`;
|
|
747
|
+
if (this.debug) {
|
|
748
|
+
console.log(`${this.prefix} sending track_partial`, {
|
|
749
|
+
eventId,
|
|
750
|
+
eventName,
|
|
751
|
+
userId,
|
|
752
|
+
convoId,
|
|
753
|
+
isPending,
|
|
754
|
+
inputPreview: typeof accumulated.input === "string" ? accumulated.input.slice(0, 120) : void 0,
|
|
755
|
+
outputPreview: typeof accumulated.output === "string" ? accumulated.output.slice(0, 120) : void 0,
|
|
756
|
+
attachments: (_k = (_j = accumulated.attachments) == null ? void 0 : _j.length) != null ? _k : 0,
|
|
757
|
+
attachmentKinds:
|
|
758
|
+
(_m =
|
|
759
|
+
(_l = accumulated.attachments) == null
|
|
760
|
+
? void 0
|
|
761
|
+
: _l.map((a) => ({
|
|
762
|
+
type: a.type,
|
|
763
|
+
role: a.role,
|
|
764
|
+
name: a.name,
|
|
765
|
+
valuePreview: a.value.slice(0, 60),
|
|
766
|
+
}))) != null
|
|
767
|
+
? _m
|
|
768
|
+
: [],
|
|
769
|
+
endpoint: url,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (this.localDebuggerUrl) {
|
|
773
|
+
mirrorPartialEventToLocalDebugger(payload, {
|
|
774
|
+
baseUrl: this.localDebuggerUrl,
|
|
775
|
+
writeKey: this.writeKey,
|
|
776
|
+
debug: this.debug,
|
|
777
|
+
sdkName: this.sdkName,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
if (!this.writeKey) {
|
|
781
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const opts = this.requestOpts();
|
|
785
|
+
if (!opts) {
|
|
786
|
+
this.warnShutdownDrop(`track_partial ${eventId}`);
|
|
787
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const p = postJson(url, payload, this.requestHeaders(), opts);
|
|
791
|
+
this.inFlight.add(p);
|
|
792
|
+
try {
|
|
793
|
+
try {
|
|
794
|
+
await p;
|
|
795
|
+
if (this.debug) {
|
|
796
|
+
console.log(`${this.prefix} sent track_partial ${eventId} (${eventName})`);
|
|
797
|
+
}
|
|
798
|
+
} catch (err) {
|
|
799
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
800
|
+
rateLimitedLog(`${this.prefix}.send_track_partial_failed`, () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`));
|
|
801
|
+
}
|
|
802
|
+
} finally {
|
|
803
|
+
this.inFlight.delete(p);
|
|
804
|
+
}
|
|
805
|
+
if (!isPending) {
|
|
806
|
+
this.sticky.delete(eventId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var DEFAULT_SECRET_KEY_NAMES = [
|
|
811
|
+
"apikey",
|
|
812
|
+
"apisecret",
|
|
813
|
+
"apitoken",
|
|
814
|
+
"secretaccesskey",
|
|
815
|
+
"sessiontoken",
|
|
816
|
+
"privatekey",
|
|
817
|
+
"privatekeyid",
|
|
818
|
+
"clientsecret",
|
|
819
|
+
"accesstoken",
|
|
820
|
+
"refreshtoken",
|
|
821
|
+
"oauthtoken",
|
|
822
|
+
"bearertoken",
|
|
823
|
+
"authorization",
|
|
824
|
+
"password",
|
|
825
|
+
"passphrase",
|
|
826
|
+
];
|
|
827
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
828
|
+
function normalizeKeyName(name) {
|
|
829
|
+
return name.toLowerCase().replace(/[-_.]/g, "");
|
|
830
|
+
}
|
|
831
|
+
function redactSecretsInObject(value, options) {
|
|
832
|
+
var _a, _b;
|
|
833
|
+
const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
|
|
834
|
+
const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
|
|
835
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
836
|
+
const walk = (node) => {
|
|
837
|
+
if (node === null || typeof node !== "object") return node;
|
|
838
|
+
if (seen.has(node)) return "[CIRCULAR]";
|
|
839
|
+
seen.add(node);
|
|
840
|
+
if (Array.isArray(node)) {
|
|
841
|
+
return node.map((item) => walk(item));
|
|
842
|
+
}
|
|
843
|
+
const out = {};
|
|
844
|
+
for (const [k, v] of Object.entries(node)) {
|
|
845
|
+
if (normalizedSecretSet.has(normalizeKeyName(k))) {
|
|
846
|
+
out[k] = placeholder;
|
|
847
|
+
} else {
|
|
848
|
+
out[k] = walk(v);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
};
|
|
853
|
+
return walk(value);
|
|
854
|
+
}
|
|
855
|
+
function buildSecretSet(names) {
|
|
856
|
+
const set = /* @__PURE__ */ new Set();
|
|
857
|
+
for (const name of names) set.add(normalizeKeyName(name));
|
|
858
|
+
return set;
|
|
859
|
+
}
|
|
860
|
+
var DEFAULT_REDACT_ATTRIBUTE_KEYS = ["ai.request.providerOptions", "ai.response.providerMetadata"];
|
|
861
|
+
function defaultTransformSpan(span) {
|
|
862
|
+
const attrs = span.attributes;
|
|
863
|
+
if (!attrs || attrs.length === 0) return span;
|
|
864
|
+
let nextAttrs;
|
|
865
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
866
|
+
const attr = attrs[i];
|
|
867
|
+
const redacted = redactJsonAttributeValue(attr.key, attr.value);
|
|
868
|
+
if (redacted === void 0) continue;
|
|
869
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
870
|
+
nextAttrs[i] = { key: attr.key, value: redacted };
|
|
871
|
+
}
|
|
872
|
+
if (!nextAttrs) return span;
|
|
873
|
+
return { ...span, attributes: nextAttrs };
|
|
874
|
+
}
|
|
875
|
+
var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
|
|
876
|
+
function redactJsonAttributeValue(key, value) {
|
|
877
|
+
if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
|
|
878
|
+
const json = value.stringValue;
|
|
879
|
+
if (typeof json !== "string" || json.length === 0) return void 0;
|
|
880
|
+
let parsed;
|
|
881
|
+
try {
|
|
882
|
+
parsed = JSON.parse(json);
|
|
883
|
+
} catch (e) {
|
|
884
|
+
return void 0;
|
|
885
|
+
}
|
|
886
|
+
const scrubbed = redactSecretsInObject(parsed);
|
|
887
|
+
let scrubbedJson;
|
|
888
|
+
try {
|
|
889
|
+
scrubbedJson = JSON.stringify(scrubbed);
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
if (scrubbedJson === json) return void 0;
|
|
894
|
+
return { stringValue: scrubbedJson };
|
|
895
|
+
}
|
|
896
|
+
function applyOtelSpanAttributeLimit(limit) {
|
|
897
|
+
var _a, _b;
|
|
898
|
+
try {
|
|
899
|
+
const raw = (_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
|
|
900
|
+
if (!raw) return limit;
|
|
901
|
+
const parsed = Number.parseInt(raw, 10);
|
|
902
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
903
|
+
return Math.min(limit, parsed);
|
|
904
|
+
}
|
|
905
|
+
} catch (e) {}
|
|
906
|
+
return limit;
|
|
907
|
+
}
|
|
908
|
+
var TraceShipper = class {
|
|
909
|
+
constructor(opts) {
|
|
910
|
+
this.queue = [];
|
|
911
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
912
|
+
this.hasShutdown = false;
|
|
913
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
914
|
+
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
915
|
+
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
916
|
+
this.enabled = opts.enabled !== false;
|
|
917
|
+
this.debug = opts.debug;
|
|
918
|
+
this.debugSpans = opts.debugSpans === true;
|
|
919
|
+
this.flushIntervalMs = (_c = opts.flushIntervalMs) != null ? _c : 1e3;
|
|
920
|
+
this.maxBatchSize = (_d = opts.maxBatchSize) != null ? _d : 50;
|
|
921
|
+
this.maxQueueSize = (_e = opts.maxQueueSize) != null ? _e : 5e3;
|
|
922
|
+
this.sdkName = (_f = opts.sdkName) != null ? _f : "core";
|
|
923
|
+
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
924
|
+
this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
|
|
925
|
+
this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
|
|
926
|
+
this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
|
|
927
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
928
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
929
|
+
}
|
|
930
|
+
this.projectId = normalizeProjectId(opts.projectId, {
|
|
931
|
+
debug: this.debug,
|
|
932
|
+
prefix: this.prefix,
|
|
933
|
+
});
|
|
934
|
+
this.transformSpanHook = opts.transformSpan;
|
|
935
|
+
this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
|
|
936
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Cap every string attribute value on the span. O(#attributes) length
|
|
940
|
+
* checks; only oversized values pay a slice. Runs AFTER the redaction
|
|
941
|
+
* pipeline so the default secret-scrub still sees parseable JSON in
|
|
942
|
+
* `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
|
|
943
|
+
* first could cut a JSON blob mid-way, fail the parse, and ship secrets
|
|
944
|
+
* in the surviving prefix).
|
|
945
|
+
*
|
|
946
|
+
* A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
|
|
947
|
+
* for span content, matching the Python SDK and the OTel SDK convention.
|
|
948
|
+
*/
|
|
949
|
+
capSpanAttributes(span) {
|
|
950
|
+
var _a;
|
|
951
|
+
const maxChars = applyOtelSpanAttributeLimit(resolveMaxTextFieldChars(this.maxTextFieldCharsOpt));
|
|
952
|
+
const attrs = span.attributes;
|
|
953
|
+
if (!attrs || attrs.length === 0) return span;
|
|
954
|
+
let nextAttrs;
|
|
955
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
956
|
+
const attr = attrs[i];
|
|
957
|
+
const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
|
|
958
|
+
if (typeof value !== "string" || value.length <= maxChars) continue;
|
|
959
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
960
|
+
nextAttrs[i] = {
|
|
961
|
+
key: attr.key,
|
|
962
|
+
value: { ...attr.value, stringValue: capText(value, maxChars) },
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (!nextAttrs) return span;
|
|
966
|
+
return { ...span, attributes: nextAttrs };
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Apply the user `transformSpan` hook (if any) followed by the default
|
|
970
|
+
* redactor (unless disabled). Returns either the (possibly new) span to
|
|
971
|
+
* ship, or `null` to drop the span entirely.
|
|
972
|
+
*
|
|
973
|
+
* Ordering: user hook runs first so callers can rewrite the span freely
|
|
974
|
+
* (rename attrs, add new ones, scrub things the default doesn't know
|
|
975
|
+
* about). The default redactor then runs on whatever the user produced,
|
|
976
|
+
* acting as the always-on floor for documented BYOK secrets. If the user
|
|
977
|
+
* sets `disableDefaultRedaction: true`, the floor is skipped.
|
|
978
|
+
*
|
|
979
|
+
* Fail-closed: if the user hook throws, the span is dropped — a buggy
|
|
980
|
+
* hook can never accidentally ship raw, un-redacted spans.
|
|
981
|
+
*/
|
|
982
|
+
redactSpan(span) {
|
|
983
|
+
let current = span;
|
|
984
|
+
if (this.transformSpanHook) {
|
|
985
|
+
try {
|
|
986
|
+
const result = this.transformSpanHook(current);
|
|
987
|
+
if (result === null) return null;
|
|
988
|
+
if (result !== void 0) current = result;
|
|
989
|
+
} catch (err) {
|
|
990
|
+
if (this.debug) {
|
|
991
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
992
|
+
console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
|
|
993
|
+
}
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!this.disableDefaultRedaction) {
|
|
998
|
+
current = defaultTransformSpan(current);
|
|
999
|
+
}
|
|
1000
|
+
return this.capSpanAttributes(current);
|
|
1001
|
+
}
|
|
1002
|
+
isDebugEnabled() {
|
|
1003
|
+
return this.debug;
|
|
1004
|
+
}
|
|
1005
|
+
authHeaders() {
|
|
1006
|
+
return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
|
|
1007
|
+
}
|
|
1008
|
+
requestHeaders() {
|
|
1009
|
+
return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
|
|
1010
|
+
}
|
|
1011
|
+
startSpan(args) {
|
|
1012
|
+
var _a, _b;
|
|
1013
|
+
const ids = createSpanIds(args.parent);
|
|
1014
|
+
const started = (_a = args.startTimeUnixNano) != null ? _a : nowUnixNanoString();
|
|
1015
|
+
const attrs = [attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId), attrString("ai.operationId", args.operationId)];
|
|
1016
|
+
if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
|
|
1017
|
+
const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
|
|
1018
|
+
this.mirrorToLocalDebugger(
|
|
1019
|
+
buildOtlpSpan({
|
|
1020
|
+
ids: span.ids,
|
|
1021
|
+
name: span.name,
|
|
1022
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
1023
|
+
endTimeUnixNano: span.startTimeUnixNano,
|
|
1024
|
+
// placeholder — will be updated on endSpan
|
|
1025
|
+
attributes: span.attributes,
|
|
1026
|
+
status: { code: SpanStatusCode.UNSET },
|
|
1027
|
+
}),
|
|
1028
|
+
);
|
|
1029
|
+
return span;
|
|
1030
|
+
}
|
|
1031
|
+
mirrorToLocalDebugger(span) {
|
|
1032
|
+
if (!this.localDebuggerUrl) return;
|
|
1033
|
+
const redacted = this.redactSpan(span);
|
|
1034
|
+
if (redacted === null) return;
|
|
1035
|
+
const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
|
|
1036
|
+
mirrorTraceExportToLocalDebugger(body, {
|
|
1037
|
+
baseUrl: this.localDebuggerUrl,
|
|
1038
|
+
debug: false,
|
|
1039
|
+
sdkName: this.sdkName,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
endSpan(span, extra) {
|
|
1043
|
+
var _a, _b;
|
|
1044
|
+
if (span.endTimeUnixNano) return;
|
|
1045
|
+
span.endTimeUnixNano = (_a = extra == null ? void 0 : extra.endTimeUnixNano) != null ? _a : nowUnixNanoString();
|
|
1046
|
+
if ((_b = extra == null ? void 0 : extra.attributes) == null ? void 0 : _b.length) {
|
|
1047
|
+
span.attributes.push(...extra.attributes);
|
|
1048
|
+
}
|
|
1049
|
+
let status = extra == null ? void 0 : extra.status;
|
|
1050
|
+
if (!status && (extra == null ? void 0 : extra.error) !== void 0) {
|
|
1051
|
+
const message = extra.error instanceof Error ? extra.error.message : String(extra.error);
|
|
1052
|
+
status = { code: SpanStatusCode.ERROR, message };
|
|
1053
|
+
}
|
|
1054
|
+
const otlp = buildOtlpSpan({
|
|
1055
|
+
ids: span.ids,
|
|
1056
|
+
name: span.name,
|
|
1057
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
1058
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
1059
|
+
attributes: span.attributes,
|
|
1060
|
+
status,
|
|
1061
|
+
});
|
|
1062
|
+
this.enqueue(otlp);
|
|
1063
|
+
this.mirrorToLocalDebugger(otlp);
|
|
1064
|
+
}
|
|
1065
|
+
createSpan(args) {
|
|
1066
|
+
var _a;
|
|
1067
|
+
const ids = createSpanIds(args.parent);
|
|
1068
|
+
const attrs = [attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)];
|
|
1069
|
+
if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
|
|
1070
|
+
const otlp = buildOtlpSpan({
|
|
1071
|
+
ids,
|
|
1072
|
+
name: args.name,
|
|
1073
|
+
startTimeUnixNano: args.startTimeUnixNano,
|
|
1074
|
+
endTimeUnixNano: args.endTimeUnixNano,
|
|
1075
|
+
attributes: attrs,
|
|
1076
|
+
status: args.status,
|
|
1077
|
+
});
|
|
1078
|
+
this.enqueue(otlp);
|
|
1079
|
+
this.mirrorToLocalDebugger(otlp);
|
|
1080
|
+
}
|
|
1081
|
+
enqueue(span) {
|
|
1082
|
+
if (!this.enabled) return;
|
|
1083
|
+
if (this.debugSpans) {
|
|
1084
|
+
const short = (s) => (s ? s.slice(-8) : "none");
|
|
1085
|
+
console.log(`${this.prefix}[span] name=${span.name} trace=${short(span.traceId)} span=${short(span.spanId)} parent=${short(span.parentSpanId)}`);
|
|
1086
|
+
}
|
|
1087
|
+
const redacted = this.redactSpan(span);
|
|
1088
|
+
if (redacted === null) return;
|
|
1089
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
1090
|
+
this.queue.shift();
|
|
1091
|
+
}
|
|
1092
|
+
this.queue.push(redacted);
|
|
1093
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
1094
|
+
void this.flush().catch(() => {});
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!this.timer) {
|
|
1098
|
+
this.timer = setTimeout(() => {
|
|
1099
|
+
this.timer = void 0;
|
|
1100
|
+
void this.flush().catch(() => {});
|
|
1101
|
+
}, this.flushIntervalMs);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
async flush() {
|
|
1105
|
+
if (!this.enabled) return;
|
|
1106
|
+
if (this.timer) {
|
|
1107
|
+
clearTimeout(this.timer);
|
|
1108
|
+
this.timer = void 0;
|
|
1109
|
+
}
|
|
1110
|
+
while (this.queue.length > 0) {
|
|
1111
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
1112
|
+
if (!this.writeKey) continue;
|
|
1113
|
+
const opts = this.requestOpts();
|
|
1114
|
+
if (!opts) {
|
|
1115
|
+
rateLimitedLog(`${this.prefix}.shutdown_deadline`, () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`));
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
|
|
1119
|
+
const url = `${this.baseUrl}traces`;
|
|
1120
|
+
if (this.debug) {
|
|
1121
|
+
console.log(`${this.prefix} sending traces batch`, {
|
|
1122
|
+
spans: batch.length,
|
|
1123
|
+
endpoint: url,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
const p = postJson(url, body, this.requestHeaders(), opts);
|
|
1127
|
+
this.inFlight.add(p);
|
|
1128
|
+
try {
|
|
1129
|
+
try {
|
|
1130
|
+
await p;
|
|
1131
|
+
if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1134
|
+
rateLimitedLog(`${this.prefix}.send_spans_failed`, () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`));
|
|
1135
|
+
}
|
|
1136
|
+
} finally {
|
|
1137
|
+
this.inFlight.delete(p);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/** See EventShipper.requestOpts — same shutdown-budget semantics. */
|
|
1142
|
+
requestOpts() {
|
|
1143
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
1144
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
1145
|
+
if (remainingMs <= 0) return null;
|
|
1146
|
+
return {
|
|
1147
|
+
maxAttempts: 1,
|
|
1148
|
+
debug: this.debug,
|
|
1149
|
+
sdkName: this.sdkName,
|
|
1150
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs),
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
if (this.hasShutdown) {
|
|
1154
|
+
return {
|
|
1155
|
+
maxAttempts: 1,
|
|
1156
|
+
debug: this.debug,
|
|
1157
|
+
sdkName: this.sdkName,
|
|
1158
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
1162
|
+
}
|
|
1163
|
+
async shutdown() {
|
|
1164
|
+
this.hasShutdown = true;
|
|
1165
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
1166
|
+
try {
|
|
1167
|
+
if (this.timer) {
|
|
1168
|
+
clearTimeout(this.timer);
|
|
1169
|
+
this.timer = void 0;
|
|
1170
|
+
}
|
|
1171
|
+
const drain = async () => {
|
|
1172
|
+
await this.flush();
|
|
1173
|
+
await Promise.all([...this.inFlight].map((p) => p.catch(() => {})));
|
|
1174
|
+
};
|
|
1175
|
+
const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
|
|
1176
|
+
if (!settled) {
|
|
1177
|
+
rateLimitedLog(`${this.prefix}.shutdown_deadline`, () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`));
|
|
1178
|
+
}
|
|
1179
|
+
} finally {
|
|
1180
|
+
this.shutdownDeadlineAt = void 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// ../core/dist/index.node.js
|
|
1186
|
+
var import_async_hooks = require("async_hooks");
|
|
1187
|
+
globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
|
|
1188
|
+
var SUPPRESS_TRACING_KEY = /* @__PURE__ */ Symbol.for("OpenTelemetry SDK Context Key SUPPRESS_TRACING");
|
|
1189
|
+
function findOtelContextManager() {
|
|
1190
|
+
var _a;
|
|
1191
|
+
for (const sym of Object.getOwnPropertySymbols(globalThis)) {
|
|
1192
|
+
if (!((_a = sym.description) == null ? void 0 : _a.startsWith("opentelemetry.js.api."))) continue;
|
|
1193
|
+
const api = globalThis[sym];
|
|
1194
|
+
const cm = api == null ? void 0 : api.context;
|
|
1195
|
+
if (cm && typeof cm.with === "function" && typeof cm.active === "function") {
|
|
1196
|
+
return cm;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return void 0;
|
|
1200
|
+
}
|
|
1201
|
+
function installTracingSuppressionHook() {
|
|
1202
|
+
if (typeof globalThis.RAINDROP_SUPPRESS_TRACING === "function") return;
|
|
1203
|
+
const hook = (fn) => {
|
|
1204
|
+
const cm = findOtelContextManager();
|
|
1205
|
+
if (!cm) return fn();
|
|
1206
|
+
return cm.with(cm.active().setValue(SUPPRESS_TRACING_KEY, true), fn);
|
|
1207
|
+
};
|
|
1208
|
+
globalThis.RAINDROP_SUPPRESS_TRACING = hook;
|
|
1209
|
+
}
|
|
1210
|
+
installTracingSuppressionHook();
|
|
1211
|
+
|
|
1212
|
+
// src/config.ts
|
|
1213
|
+
var import_node_fs = require("fs");
|
|
1214
|
+
var import_node_os = require("os");
|
|
1215
|
+
var import_node_path = require("path");
|
|
1216
|
+
function loadConfig(projectDirectory) {
|
|
1217
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1218
|
+
let merged = {};
|
|
1219
|
+
const configPaths = [
|
|
1220
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
|
|
1221
|
+
(0, import_node_path.join)(projectDirectory, ".opencode", "raindrop.json"),
|
|
1222
|
+
(0, import_node_path.join)(projectDirectory, "raindrop.json"),
|
|
1223
|
+
(0, import_node_path.join)(process.cwd(), ".opencode", "raindrop.json"),
|
|
1224
|
+
(0, import_node_path.join)(process.cwd(), "raindrop.json")
|
|
1225
|
+
];
|
|
1226
|
+
for (const configPath of configPaths) {
|
|
1227
|
+
try {
|
|
1228
|
+
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
1229
|
+
const content = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
1230
|
+
const parsed = JSON.parse(content);
|
|
1231
|
+
merged = { ...merged, ...parsed };
|
|
1232
|
+
}
|
|
1233
|
+
} catch (e) {}
|
|
1234
|
+
}
|
|
1235
|
+
let eventMetadata;
|
|
1236
|
+
const envMeta = process.env["RAINDROP_EVENT_METADATA"];
|
|
1237
|
+
if (envMeta) {
|
|
1238
|
+
try {
|
|
1239
|
+
eventMetadata = JSON.parse(envMeta);
|
|
1240
|
+
} catch (e) {}
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
|
|
1244
|
+
endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
|
|
1245
|
+
projectId: (_e = process.env["RAINDROP_PROJECT_ID"]) != null ? _e : merged.project_id,
|
|
1246
|
+
eventName: (_f = merged.event_name ?? merged.eventName) != null ? _f : "opencode_session",
|
|
1247
|
+
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_g = merged.debug) != null ? _g : false,
|
|
1248
|
+
captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_h = merged.capture_system_prompt) != null ? _h : false,
|
|
1249
|
+
eventMetadata,
|
|
1250
|
+
// KOLYA PATCH (F-002.6): trace_only flag — when true, plugin logs go to
|
|
1251
|
+
// ~/.raindrop/trace.log (append) instead of stdout/TUI. Set in raindrop.json
|
|
1252
|
+
// as "trace_only": true, or via env RAINDROP_TRACE_ONLY=true. Default: false
|
|
1253
|
+
// (existing behaviour, writes to stdout).
|
|
1254
|
+
traceOnly: process.env["RAINDROP_TRACE_ONLY"] === "true" ? true : (merged.trace_only === true),
|
|
1255
|
+
localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function resolveLocalWorkshopUrl(fileValue) {
|
|
1259
|
+
const envValue = process.env["RAINDROP_LOCAL_WORKSHOP_URL"];
|
|
1260
|
+
if (envValue !== void 0) {
|
|
1261
|
+
if (envValue === "" || envValue.toLowerCase() === "null" || envValue.toLowerCase() === "false") {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
// KOLYA PATCH (F-005): non-local env guard + rate-limited warning.
|
|
1265
|
+
if (!isLocalUrl(envValue)) {
|
|
1266
|
+
rateLimitedLog("local_workshop_url_non_local", () => console.warn(`[oc-wsp] [warn] RAINDROP_LOCAL_WORKSHOP_URL=${envValue} is not a local URL; falling back to raindrop.json (or auto-detect).`));
|
|
1267
|
+
return fileValue || DEFAULT_LOCAL_WORKSHOP_URL;
|
|
1268
|
+
}
|
|
1269
|
+
return envValue;
|
|
1270
|
+
}
|
|
1271
|
+
return fileValue;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// package.json
|
|
1275
|
+
var package_default = {
|
|
1276
|
+
name: "@grudanov-nikolay/agenttrace-opencode-plugin",
|
|
1277
|
+
version: "0.0.2",
|
|
1278
|
+
description: "agenttrace observability plugin for OpenCode. Drop-in replacement for @raindrop-ai/opencode-plugin. Renamed from @grudanov-nikolay/opencode-workshop-plugin@0.0.1 \u2192 @grudanov-nikolay/agenttrace-opencode-plugin@0.1.0 on 2026-09-17 (F-024). Carries the same 5 fixes: MCP tool.execute.after crash workaround (upstream issue anomalyco/opencode#21149), RAINDROP_LOCAL_WORKSHOP_URL non-local fallback, subagent_name recovery on 1.18 nested sub-agents (task span + child LLM span + Subagent root), result.error propagation into span status=ERROR, and Workshop sidepanel bootstrap (registers `workshop` MCP server and prepends sidepanel system prompt when RAINDROP_SIDEPANEL_ACTIVE=1).",
|
|
1279
|
+
type: "module",
|
|
1280
|
+
main: "dist/index.js",
|
|
1281
|
+
module: "dist/index.js",
|
|
1282
|
+
types: "dist/index.d.ts",
|
|
1283
|
+
license: "MIT",
|
|
1284
|
+
homepage: "https://github.com/nikolay-grudanov/agenttrace-opencode-plugin",
|
|
1285
|
+
bugs: {
|
|
1286
|
+
url: "https://github.com/nikolay-grudanov/agenttrace-opencode-plugin/issues",
|
|
1287
|
+
},
|
|
1288
|
+
author: "Nikolai Grudanov <nikolay-grudanov@users.noreply.github.com>",
|
|
1289
|
+
repository: {
|
|
1290
|
+
type: "git",
|
|
1291
|
+
url: "git+https://github.com/nikolay-grudanov/agenttrace-opencode-plugin.git",
|
|
1292
|
+
},
|
|
1293
|
+
exports: {
|
|
1294
|
+
".": {
|
|
1295
|
+
types: "./dist/index.d.ts",
|
|
1296
|
+
import: "./dist/index.js",
|
|
1297
|
+
require: "./dist/index.cjs",
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
sideEffects: false,
|
|
1301
|
+
files: ["dist/**"],
|
|
1302
|
+
peerDependencies: {
|
|
1303
|
+
"@opencode-ai/plugin": ">=1.3.0",
|
|
1304
|
+
"@opencode-ai/sdk": ">=1.3.0",
|
|
1305
|
+
},
|
|
1306
|
+
peerDependenciesMeta: {
|
|
1307
|
+
"@opencode-ai/sdk": {
|
|
1308
|
+
optional: true,
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
publishConfig: {
|
|
1312
|
+
access: "public",
|
|
1313
|
+
},
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
// src/package-info.ts
|
|
1317
|
+
var PLUGIN_NAME = package_default.name;
|
|
1318
|
+
var PLUGIN_VERSION = package_default.version;
|
|
1319
|
+
|
|
1320
|
+
// src/shipper.ts
|
|
1321
|
+
var EventShipper2 = class extends EventShipper {
|
|
1322
|
+
constructor(opts) {
|
|
1323
|
+
var _a, _b, _c, _d;
|
|
1324
|
+
super({
|
|
1325
|
+
...opts,
|
|
1326
|
+
sdkName: (_a = opts.sdkName) != null ? _a : "opencode-plugin",
|
|
1327
|
+
libraryName: (_b = opts.libraryName) != null ? _b : PLUGIN_NAME,
|
|
1328
|
+
libraryVersion: (_c = opts.libraryVersion) != null ? _c : PLUGIN_VERSION,
|
|
1329
|
+
defaultEventName: (_d = opts.defaultEventName) != null ? _d : "opencode_session",
|
|
1330
|
+
});
|
|
1331
|
+
this.gitContext = opts.gitContext ?? null;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
var TraceShipper2 = class extends TraceShipper {
|
|
1335
|
+
constructor(opts) {
|
|
1336
|
+
var _a, _b, _c;
|
|
1337
|
+
super({
|
|
1338
|
+
...opts,
|
|
1339
|
+
sdkName: (_a = opts.sdkName) != null ? _a : "opencode-plugin",
|
|
1340
|
+
serviceName: (_b = opts.serviceName) != null ? _b : "raindrop.opencode-plugin",
|
|
1341
|
+
serviceVersion: (_c = opts.serviceVersion) != null ? _c : PLUGIN_VERSION,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
enqueue(span) {
|
|
1345
|
+
var _a;
|
|
1346
|
+
const attrs = (_a = span.attributes) != null ? _a : [];
|
|
1347
|
+
attrs.unshift({ key: "span.id", value: { stringValue: span.spanId } }, ...(span.parentSpanId ? [{ key: "span.parent.id", value: { stringValue: span.parentSpanId } }] : []));
|
|
1348
|
+
span.attributes = attrs;
|
|
1349
|
+
super.enqueue(span);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/bounded.ts
|
|
1354
|
+
var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
|
|
1355
|
+
var MAX_TEXT_FIELD_CHARS = 1e6;
|
|
1356
|
+
var MAX_BOUNDED_DEPTH = 12;
|
|
1357
|
+
function truncateToLimit2(text, limit) {
|
|
1358
|
+
if (text.length <= limit) return text;
|
|
1359
|
+
if (limit > TRUNCATION_MARKER2.length) {
|
|
1360
|
+
return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
|
|
1361
|
+
}
|
|
1362
|
+
return text.slice(0, limit);
|
|
1363
|
+
}
|
|
1364
|
+
function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
|
|
1365
|
+
if (value.length <= limit) return value;
|
|
1366
|
+
return truncateToLimit2(value, limit);
|
|
1367
|
+
}
|
|
1368
|
+
function boundedClone2(value, budget, depth) {
|
|
1369
|
+
if (budget.remaining <= 0) return TRUNCATION_MARKER2;
|
|
1370
|
+
if (typeof value === "string") {
|
|
1371
|
+
if (value.length > budget.remaining) {
|
|
1372
|
+
const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
|
|
1373
|
+
budget.remaining = 0;
|
|
1374
|
+
return taken;
|
|
1375
|
+
}
|
|
1376
|
+
budget.remaining -= Math.max(value.length, 1);
|
|
1377
|
+
return value;
|
|
1378
|
+
}
|
|
1379
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
1380
|
+
budget.remaining -= 8;
|
|
1381
|
+
return value;
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof value !== "object") {
|
|
1384
|
+
budget.remaining -= 8;
|
|
1385
|
+
return value;
|
|
1386
|
+
}
|
|
1387
|
+
if (depth >= MAX_BOUNDED_DEPTH) {
|
|
1388
|
+
budget.remaining -= 16;
|
|
1389
|
+
return `<max depth: ${TRUNCATION_MARKER2}>`;
|
|
1390
|
+
}
|
|
1391
|
+
const withToJson = value;
|
|
1392
|
+
if (typeof withToJson.toJSON === "function") {
|
|
1393
|
+
try {
|
|
1394
|
+
return boundedClone2(withToJson.toJSON(), budget, depth + 1);
|
|
1395
|
+
} catch (e) {}
|
|
1396
|
+
}
|
|
1397
|
+
if (Array.isArray(value)) {
|
|
1398
|
+
const out2 = [];
|
|
1399
|
+
for (const item of value) {
|
|
1400
|
+
if (budget.remaining <= 0) {
|
|
1401
|
+
out2.push(TRUNCATION_MARKER2);
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
out2.push(boundedClone2(item, budget, depth + 1));
|
|
1405
|
+
}
|
|
1406
|
+
return out2;
|
|
1407
|
+
}
|
|
1408
|
+
const out = {};
|
|
1409
|
+
for (const key of Object.keys(value)) {
|
|
1410
|
+
if (budget.remaining <= 0) {
|
|
1411
|
+
out["..."] = TRUNCATION_MARKER2;
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
budget.remaining -= Math.max(key.length, 1);
|
|
1415
|
+
out[key] = boundedClone2(value[key], budget, depth + 1);
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
|
|
1420
|
+
var _a;
|
|
1421
|
+
try {
|
|
1422
|
+
if (typeof value === "string") {
|
|
1423
|
+
return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
|
|
1424
|
+
}
|
|
1425
|
+
const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
|
|
1426
|
+
return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
return capText2(String(value), limit);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/tracing-linkage-helpers.ts
|
|
1433
|
+
function createSessionParentMapHelpers({
|
|
1434
|
+
sessions: sessions2,
|
|
1435
|
+
taskContexts: taskContexts2,
|
|
1436
|
+
runningTaskCallsBySession: runningTaskCallsBySession2,
|
|
1437
|
+
mapChildSessionToParent: mapChildSessionToParent2,
|
|
1438
|
+
pendingChildSessionsByCallKey: pendingChildSessionsByCallKey2,
|
|
1439
|
+
callKey: callKey2,
|
|
1440
|
+
log,
|
|
1441
|
+
}) {
|
|
1442
|
+
function applyChildSessionParentToState(childSessionId) {
|
|
1443
|
+
const parentInfo = mapChildSessionToParent2.get(childSessionId);
|
|
1444
|
+
const state = sessions2.get(childSessionId);
|
|
1445
|
+
if (!parentInfo || !state) return;
|
|
1446
|
+
state.parentId = parentInfo.parentId;
|
|
1447
|
+
state.parentTaskSpanIds = parentInfo.parentTaskSpanIds;
|
|
1448
|
+
state.parentEventContext = parentInfo.eventContext;
|
|
1449
|
+
}
|
|
1450
|
+
function queuePendingChildByCallKey(key, childSessionId) {
|
|
1451
|
+
var _a;
|
|
1452
|
+
const pending = (_a = pendingChildSessionsByCallKey2.get(key)) != null ? _a : /* @__PURE__ */ new Set();
|
|
1453
|
+
pending.add(childSessionId);
|
|
1454
|
+
pendingChildSessionsByCallKey2.set(key, pending);
|
|
1455
|
+
}
|
|
1456
|
+
function attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source) {
|
|
1457
|
+
const key = callKey2(parentId, parentCallId);
|
|
1458
|
+
const ctx = taskContexts2.get(key);
|
|
1459
|
+
if (!ctx) {
|
|
1460
|
+
queuePendingChildByCallKey(key, childSessionId);
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
const parentInfo = {
|
|
1464
|
+
parentId,
|
|
1465
|
+
parentTaskSpanIds: ctx.ids,
|
|
1466
|
+
eventContext: ctx.eventContext,
|
|
1467
|
+
};
|
|
1468
|
+
mapChildSessionToParent2.set(childSessionId, parentInfo);
|
|
1469
|
+
applyChildSessionParentToState(childSessionId);
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
function resolvePendingChildrenForCall(parentId, parentCallId, source) {
|
|
1473
|
+
const key = callKey2(parentId, parentCallId);
|
|
1474
|
+
const pending = pendingChildSessionsByCallKey2.get(key);
|
|
1475
|
+
if (pending && pending.size > 0) {
|
|
1476
|
+
for (const childSessionId of [...pending]) {
|
|
1477
|
+
if (attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source)) {
|
|
1478
|
+
pending.delete(childSessionId);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
function cleanupSessionParentMap(sessionId) {
|
|
1485
|
+
mapChildSessionToParent2.delete(sessionId);
|
|
1486
|
+
runningTaskCallsBySession2.delete(sessionId);
|
|
1487
|
+
for (const [key, pending] of pendingChildSessionsByCallKey2.entries()) {
|
|
1488
|
+
pending.delete(sessionId);
|
|
1489
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
1490
|
+
}
|
|
1491
|
+
for (const key of [...taskContexts2.keys()]) {
|
|
1492
|
+
if (key.startsWith(`${sessionId}:`)) taskContexts2.delete(key);
|
|
1493
|
+
}
|
|
1494
|
+
for (const [childSessionId, parentInfo] of [...mapChildSessionToParent2.entries()]) {
|
|
1495
|
+
if (parentInfo.parentId === sessionId) {
|
|
1496
|
+
mapChildSessionToParent2.delete(childSessionId);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return {
|
|
1501
|
+
applyChildSessionParentToState,
|
|
1502
|
+
attachChildSessionToParentTask,
|
|
1503
|
+
cleanupSessionParentMap,
|
|
1504
|
+
resolvePendingChildrenForCall,
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/tracing.ts
|
|
1509
|
+
var ERROR_LOG_INTERVAL_MS = 3e4;
|
|
1510
|
+
var lastErrorLogAt = /* @__PURE__ */ new Map();
|
|
1511
|
+
function rateLimitedErrorLog(key, message) {
|
|
1512
|
+
const now = Date.now();
|
|
1513
|
+
const last = lastErrorLogAt.get(key);
|
|
1514
|
+
if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
|
|
1515
|
+
lastErrorLogAt.set(key, now);
|
|
1516
|
+
console.log(message);
|
|
1517
|
+
}
|
|
1518
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1519
|
+
var taskContexts = /* @__PURE__ */ new Map();
|
|
1520
|
+
var runningTaskCallsBySession = /* @__PURE__ */ new Map();
|
|
1521
|
+
var mapChildSessionToParent = /* @__PURE__ */ new Map();
|
|
1522
|
+
var pendingChildSessionsByCallKey = /* @__PURE__ */ new Map();
|
|
1523
|
+
function createSessionState(sessionId) {
|
|
1524
|
+
return {
|
|
1525
|
+
sessionId,
|
|
1526
|
+
currentInput: "",
|
|
1527
|
+
outputParts: /* @__PURE__ */ new Map(),
|
|
1528
|
+
reasoningParts: /* @__PURE__ */ new Map(),
|
|
1529
|
+
toolSpanStarts: /* @__PURE__ */ new Map(),
|
|
1530
|
+
processedMessages: /* @__PURE__ */ new Set(),
|
|
1531
|
+
subagentName: void 0,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function callKey(sessionId, callId) {
|
|
1535
|
+
return `${sessionId}:${callId}`;
|
|
1536
|
+
}
|
|
1537
|
+
function markTaskCallFinished(sessionID, callID) {
|
|
1538
|
+
const running = runningTaskCallsBySession.get(sessionID);
|
|
1539
|
+
if (!running) return;
|
|
1540
|
+
running.delete(callID);
|
|
1541
|
+
if (running.size === 0) runningTaskCallsBySession.delete(sessionID);
|
|
1542
|
+
}
|
|
1543
|
+
function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMessage, traceShipper) {
|
|
1544
|
+
const startInfo = state.toolSpanStarts.get(callID);
|
|
1545
|
+
if (!startInfo) return false;
|
|
1546
|
+
state.toolSpanStarts.delete(callID);
|
|
1547
|
+
const error = new Error(errorMessage);
|
|
1548
|
+
const endAttrs = [attrString("ai.toolCall.args", toolCallArgs), attrString("error.message", errorMessage)];
|
|
1549
|
+
if (startInfo.liveSpan) {
|
|
1550
|
+
traceShipper.endSpan(startInfo.liveSpan, {
|
|
1551
|
+
attributes: endAttrs,
|
|
1552
|
+
error,
|
|
1553
|
+
});
|
|
1554
|
+
} else {
|
|
1555
|
+
const toolSpan = traceShipper.startSpan({
|
|
1556
|
+
name: "ai.toolCall",
|
|
1557
|
+
parent: startInfo.parent,
|
|
1558
|
+
eventId: startInfo.eventId,
|
|
1559
|
+
startTimeUnixNano: startInfo.startTimeUnixNano,
|
|
1560
|
+
attributes: [attrString("ai.operationId", "ai.toolCall"), attrString("ai.toolCall.name", tool), attrString("ai.toolCall.id", callID)],
|
|
1561
|
+
});
|
|
1562
|
+
traceShipper.endSpan(toolSpan, {
|
|
1563
|
+
attributes: endAttrs,
|
|
1564
|
+
error,
|
|
1565
|
+
endTimeUnixNano: nowUnixNanoString(),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
function unwrapQuotes(s) {
|
|
1571
|
+
if (s.length < 2) return s;
|
|
1572
|
+
const first = s[0];
|
|
1573
|
+
const last = s[s.length - 1];
|
|
1574
|
+
if ((first === '"' || first === "'") && first === last) return s.slice(1, -1);
|
|
1575
|
+
return s;
|
|
1576
|
+
}
|
|
1577
|
+
function buildPromptMessages(systemPrompt, userInput) {
|
|
1578
|
+
const messages = [];
|
|
1579
|
+
if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
|
|
1580
|
+
messages.push({ role: "user", content: unwrapQuotes(userInput) });
|
|
1581
|
+
return JSON.stringify(messages);
|
|
1582
|
+
}
|
|
1583
|
+
function getHostname() {
|
|
1584
|
+
var _a;
|
|
1585
|
+
try {
|
|
1586
|
+
const bunGlobal = globalThis;
|
|
1587
|
+
if ((_a = bunGlobal.Bun) == null ? void 0 : _a.hostname) return bunGlobal.Bun.hostname;
|
|
1588
|
+
return require("os").hostname();
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
const h = process.env["HOSTNAME"];
|
|
1591
|
+
if (h === void 0 || h === "") throw new Error("HOSTNAME env is not set");
|
|
1592
|
+
return h;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function createHooks(config, worktree, directory, eventShipper, traceShipper, resolvedLocalUrl, sidepanelMode) {
|
|
1596
|
+
function log(msg, data) {
|
|
1597
|
+
if (!config.debug) return;
|
|
1598
|
+
const prefix = `[oc-wsp] [info] ${msg}`;
|
|
1599
|
+
if (data !== void 0) {
|
|
1600
|
+
console.log(prefix, data);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
console.log(prefix);
|
|
1604
|
+
}
|
|
1605
|
+
const hostname = getHostname();
|
|
1606
|
+
const os = process.platform;
|
|
1607
|
+
const { applyChildSessionParentToState, attachChildSessionToParentTask, cleanupSessionParentMap, resolvePendingChildrenForCall } = createSessionParentMapHelpers({
|
|
1608
|
+
sessions,
|
|
1609
|
+
taskContexts,
|
|
1610
|
+
runningTaskCallsBySession,
|
|
1611
|
+
mapChildSessionToParent,
|
|
1612
|
+
pendingChildSessionsByCallKey,
|
|
1613
|
+
callKey,
|
|
1614
|
+
log,
|
|
1615
|
+
});
|
|
1616
|
+
return {
|
|
1617
|
+
// ------------------------------------------------------------------
|
|
1618
|
+
// event — session lifecycle + streaming parts
|
|
1619
|
+
// ------------------------------------------------------------------
|
|
1620
|
+
event: async ({ event }) => {
|
|
1621
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1622
|
+
try {
|
|
1623
|
+
const props = event.properties;
|
|
1624
|
+
const info = props["info"];
|
|
1625
|
+
const sessionID = (_b = (_a = props["sessionID"]) != null ? _a : info == null ? void 0 : info["id"]) != null ? _b : props["id"];
|
|
1626
|
+
const sessionEventTypes = ["session.created", "session.compacted", "session.idle", "session.deleted", "session.error"];
|
|
1627
|
+
if (sessionEventTypes.includes(event.type) && (sessionID === void 0 || sessionID === "")) {
|
|
1628
|
+
throw new Error(`event ${event.type}: missing sessionID in properties`);
|
|
1629
|
+
}
|
|
1630
|
+
if (event.type === "session.created") {
|
|
1631
|
+
if (info == null || typeof info !== "object") throw new Error("session.created: props.info is required");
|
|
1632
|
+
const parentID = info["parentID"];
|
|
1633
|
+
let state = sessions.get(sessionID);
|
|
1634
|
+
if (!state) {
|
|
1635
|
+
state = createSessionState(sessionID);
|
|
1636
|
+
sessions.set(sessionID, state);
|
|
1637
|
+
}
|
|
1638
|
+
state.parentId = parentID;
|
|
1639
|
+
if (state.parentId) mapChildSessionToParent.set(sessionID, { parentId: state.parentId });
|
|
1640
|
+
} else if (event.type === "session.compacted") {
|
|
1641
|
+
const state = sessions.get(String(sessionID));
|
|
1642
|
+
if (!state) return;
|
|
1643
|
+
state.isCompacting = true;
|
|
1644
|
+
} else if (event.type === "message.part.updated") {
|
|
1645
|
+
const part = props["part"];
|
|
1646
|
+
if (part == null || typeof part !== "object") throw new Error("message.part.updated: props.part is required");
|
|
1647
|
+
const partObj = part;
|
|
1648
|
+
const partSessionID = partObj["sessionID"];
|
|
1649
|
+
const messageId = partObj["messageID"];
|
|
1650
|
+
if (partSessionID == null || partSessionID === "") throw new Error("message.part.updated: part.sessionID is required");
|
|
1651
|
+
if (messageId == null || messageId === "") throw new Error("message.part.updated: part.messageID is required");
|
|
1652
|
+
const state = sessions.get(partSessionID);
|
|
1653
|
+
if (!state) return;
|
|
1654
|
+
if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
|
|
1655
|
+
state.outputParts.set(messageId, capText2(partObj["text"]));
|
|
1656
|
+
} else if (partObj["type"] === "tool" && messageId) {
|
|
1657
|
+
const tool = partObj["tool"];
|
|
1658
|
+
const callID = partObj["callID"];
|
|
1659
|
+
const partState = partObj["state"];
|
|
1660
|
+
if (callID == null || callID === "") throw new Error("message.part.updated: part.callID is required for tool part");
|
|
1661
|
+
if (partState == null || typeof partState !== "object") throw new Error("message.part.updated: part.state is required for tool part");
|
|
1662
|
+
if (tool === "task") {
|
|
1663
|
+
const metadata = partState["metadata"];
|
|
1664
|
+
const childSessionId = metadata != null && typeof metadata === "object" ? metadata["sessionId"] : void 0;
|
|
1665
|
+
if (childSessionId != null && childSessionId !== "") {
|
|
1666
|
+
mapChildSessionToParent.set(childSessionId, {
|
|
1667
|
+
parentId: partSessionID,
|
|
1668
|
+
});
|
|
1669
|
+
attachChildSessionToParentTask(childSessionId, partSessionID, callID, "part.metadata");
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const toolState = partState;
|
|
1673
|
+
if (toolState["status"] === "error") {
|
|
1674
|
+
const errorMessage = toolState["error"];
|
|
1675
|
+
if (typeof errorMessage !== "string" || errorMessage === "") throw new Error("message.part.updated: error tool state must have error string");
|
|
1676
|
+
const input = toolState["input"];
|
|
1677
|
+
const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
|
|
1678
|
+
if (endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMessage, traceShipper) && tool === "task") {
|
|
1679
|
+
markTaskCallFinished(partSessionID, callID);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
} else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
|
|
1683
|
+
state.reasoningParts.set(messageId, capText2(partObj["text"]));
|
|
1684
|
+
}
|
|
1685
|
+
} else if (event.type === "message.updated") {
|
|
1686
|
+
const msgInfo = props["info"];
|
|
1687
|
+
if (msgInfo == null || typeof msgInfo !== "object") throw new Error("message.updated: props.info is required");
|
|
1688
|
+
const info2 = msgInfo;
|
|
1689
|
+
const role = info2["role"];
|
|
1690
|
+
if (role !== "assistant") return;
|
|
1691
|
+
const msgSessionID = info2["sessionID"];
|
|
1692
|
+
const messageId = info2["id"];
|
|
1693
|
+
const time = info2["time"];
|
|
1694
|
+
if (msgSessionID == null || msgSessionID === "") throw new Error("message.updated: info.sessionID is required");
|
|
1695
|
+
if (messageId == null || messageId === "") throw new Error("message.updated: info.id is required");
|
|
1696
|
+
if (time == null || typeof time !== "object") throw new Error("message.updated: info.time is required");
|
|
1697
|
+
const timeObj = time;
|
|
1698
|
+
if (timeObj["completed"] == null) return;
|
|
1699
|
+
const state = sessions.get(msgSessionID);
|
|
1700
|
+
if (!state || !state.currentEventId || !state.currentRootSpan) return;
|
|
1701
|
+
if (state.processedMessages.has(messageId)) return;
|
|
1702
|
+
state.processedMessages.add(messageId);
|
|
1703
|
+
const tokens = info2["tokens"];
|
|
1704
|
+
if (tokens == null || typeof tokens !== "object") throw new Error("message.updated: info.tokens is required");
|
|
1705
|
+
const tokensObj = tokens;
|
|
1706
|
+
const inputTokens = tokensObj["input"];
|
|
1707
|
+
const outputTokens = tokensObj["output"];
|
|
1708
|
+
const reasoningTokens = tokensObj["reasoning"];
|
|
1709
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number") throw new Error("message.updated: tokens.input and tokens.output must be numbers");
|
|
1710
|
+
const providerID = info2["providerID"];
|
|
1711
|
+
const modelID = info2["modelID"];
|
|
1712
|
+
if (typeof providerID !== "string" || providerID === "" || typeof modelID !== "string" || modelID === "")
|
|
1713
|
+
throw new Error("message.updated: providerID and modelID are required non-empty strings");
|
|
1714
|
+
const modelName = `${providerID}/${modelID}`;
|
|
1715
|
+
const finishReason = info2["finish"];
|
|
1716
|
+
const outputText = state.outputParts.get(messageId);
|
|
1717
|
+
const reasoningText = state.reasoningParts.get(messageId);
|
|
1718
|
+
const isToolCallsCompletion = finishReason === "tool-calls";
|
|
1719
|
+
const hasIntermediateReasoning = reasoningText !== void 0 && reasoningText.trim().length > 0;
|
|
1720
|
+
if (outputText === void 0 && !(isToolCallsCompletion && hasIntermediateReasoning)) return;
|
|
1721
|
+
if (isToolCallsCompletion && !hasIntermediateReasoning) {
|
|
1722
|
+
await traceShipper.flush();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const msgError = info2["error"];
|
|
1726
|
+
const errorForSpan = msgError
|
|
1727
|
+
? (() => {
|
|
1728
|
+
var _a2, _b2;
|
|
1729
|
+
const msg = (_b2 = (_a2 = msgError.data) == null ? void 0 : _a2.message) != null ? _b2 : msgError.name;
|
|
1730
|
+
const name = msgError.name;
|
|
1731
|
+
if (msg == null && name == null) throw new Error("message.updated: error object must have name or data.message");
|
|
1732
|
+
return `${msg != null ? msg : name}
|
|
1733
|
+
|
|
1734
|
+
type: ${name != null ? name : "UnknownError"}`;
|
|
1735
|
+
})()
|
|
1736
|
+
: void 0;
|
|
1737
|
+
if (isToolCallsCompletion) {
|
|
1738
|
+
if (reasoningText === void 0) throw new Error("message.updated: reasoningParts missing for tool-call continuation");
|
|
1739
|
+
const llmAttrs = [
|
|
1740
|
+
attrString("ai.operationId", "generateText"),
|
|
1741
|
+
attrString("ai.response.text", reasoningText),
|
|
1742
|
+
attrString("gen_ai.system", providerID),
|
|
1743
|
+
attrString("gen_ai.request.model", modelID),
|
|
1744
|
+
attrString("gen_ai.response.model", modelID),
|
|
1745
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
1746
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens),
|
|
1747
|
+
];
|
|
1748
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
1749
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
1750
|
+
}
|
|
1751
|
+
if (state.currentSystemPrompt) {
|
|
1752
|
+
llmAttrs.push(attrString("gen_ai.prompt.0.role", "system"), attrString("gen_ai.prompt.0.content", state.currentSystemPrompt));
|
|
1753
|
+
}
|
|
1754
|
+
if (state.parentId && state.subagentName) {
|
|
1755
|
+
llmAttrs.push(attrString("subagent_name", state.subagentName));
|
|
1756
|
+
}
|
|
1757
|
+
const llmParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : state.currentRootSpan.ids;
|
|
1758
|
+
const llmSpan = traceShipper.startSpan({
|
|
1759
|
+
name: modelName,
|
|
1760
|
+
parent: llmParent,
|
|
1761
|
+
eventId: state.currentEventId,
|
|
1762
|
+
attributes: llmAttrs,
|
|
1763
|
+
});
|
|
1764
|
+
if (typeof timeObj["created"] === "number") {
|
|
1765
|
+
llmSpan.startTimeUnixNano = String(Math.floor(timeObj["created"])) + "000000";
|
|
1766
|
+
}
|
|
1767
|
+
traceShipper.endSpan(llmSpan, { error: errorForSpan });
|
|
1768
|
+
await traceShipper.flush();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (outputText === void 0) return;
|
|
1772
|
+
const rootSpan = state.currentRootSpan;
|
|
1773
|
+
const isCompaction = state.isCompacting === true;
|
|
1774
|
+
if (state.parentId) {
|
|
1775
|
+
const userInput = unwrapQuotes(state.currentInput);
|
|
1776
|
+
const llmAttrs = [
|
|
1777
|
+
attrString("ai.operationId", "generateText"),
|
|
1778
|
+
attrString("ai.prompt", userInput),
|
|
1779
|
+
attrString("ai.prompt.messages", buildPromptMessages(state.currentSystemPrompt, state.currentInput)),
|
|
1780
|
+
attrString("ai.response.text", outputText),
|
|
1781
|
+
attrString("gen_ai.system", providerID),
|
|
1782
|
+
attrString("gen_ai.request.model", modelID),
|
|
1783
|
+
attrString("gen_ai.response.model", modelID),
|
|
1784
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
1785
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens),
|
|
1786
|
+
];
|
|
1787
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
1788
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
1789
|
+
}
|
|
1790
|
+
if (state.currentSystemPrompt) {
|
|
1791
|
+
llmAttrs.push(attrString("gen_ai.prompt.0.role", "system"), attrString("gen_ai.prompt.0.content", state.currentSystemPrompt));
|
|
1792
|
+
}
|
|
1793
|
+
if (state.parentId && state.subagentName) {
|
|
1794
|
+
llmAttrs.push(attrString("subagent_name", state.subagentName));
|
|
1795
|
+
}
|
|
1796
|
+
if (!state.parentTaskSpanIds) {
|
|
1797
|
+
traceShipper.endSpan(rootSpan, {
|
|
1798
|
+
error: "Missing strict parent task for child session",
|
|
1799
|
+
});
|
|
1800
|
+
state.currentRootSpan = void 0;
|
|
1801
|
+
state.currentEventId = void 0;
|
|
1802
|
+
await traceShipper.flush();
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const finalLlmSpan = traceShipper.startSpan({
|
|
1806
|
+
name: modelName,
|
|
1807
|
+
parent: state.parentTaskSpanIds,
|
|
1808
|
+
eventId: state.currentEventId,
|
|
1809
|
+
attributes: llmAttrs,
|
|
1810
|
+
});
|
|
1811
|
+
if (typeof timeObj["created"] === "number") {
|
|
1812
|
+
finalLlmSpan.startTimeUnixNano = String(Math.floor(timeObj["created"])) + "000000";
|
|
1813
|
+
}
|
|
1814
|
+
traceShipper.endSpan(finalLlmSpan, { error: errorForSpan });
|
|
1815
|
+
traceShipper.endSpan(rootSpan, {
|
|
1816
|
+
attributes: [
|
|
1817
|
+
attrString("is_subagent", "true"),
|
|
1818
|
+
attrString("parent_session_id", state.parentId),
|
|
1819
|
+
...(state.subagentName ? [attrString("subagent_name", state.subagentName)] : []),
|
|
1820
|
+
],
|
|
1821
|
+
error: errorForSpan,
|
|
1822
|
+
});
|
|
1823
|
+
} else {
|
|
1824
|
+
rootSpan.name = isCompaction ? "ai.compaction" : modelName;
|
|
1825
|
+
const userInput = unwrapQuotes(state.currentInput);
|
|
1826
|
+
const rootAttrs = [
|
|
1827
|
+
attrString("ai.operationId", "generateText"),
|
|
1828
|
+
attrString("ai.prompt", userInput),
|
|
1829
|
+
attrString("ai.prompt.messages", buildPromptMessages(state.currentSystemPrompt, state.currentInput)),
|
|
1830
|
+
attrString("ai.response.text", outputText),
|
|
1831
|
+
attrString("gen_ai.system", providerID),
|
|
1832
|
+
attrString("gen_ai.request.model", modelID),
|
|
1833
|
+
attrString("gen_ai.response.model", modelID),
|
|
1834
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
1835
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens),
|
|
1836
|
+
];
|
|
1837
|
+
if (isCompaction) {
|
|
1838
|
+
rootAttrs.push(attrString("is_compaction", "true"));
|
|
1839
|
+
}
|
|
1840
|
+
rootAttrs.push(attrString("message_id", messageId));
|
|
1841
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
1842
|
+
rootAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
1843
|
+
}
|
|
1844
|
+
if (state.currentSystemPrompt) {
|
|
1845
|
+
rootAttrs.push(attrString("gen_ai.prompt.0.role", "system"), attrString("gen_ai.prompt.0.content", state.currentSystemPrompt));
|
|
1846
|
+
}
|
|
1847
|
+
traceShipper.endSpan(rootSpan, {
|
|
1848
|
+
attributes: rootAttrs,
|
|
1849
|
+
error: errorForSpan,
|
|
1850
|
+
});
|
|
1851
|
+
if (isCompaction) {
|
|
1852
|
+
state.isCompacting = false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
state.currentRootSpan = void 0;
|
|
1856
|
+
const hasAssistantResponse = outputText.trim().length > 0;
|
|
1857
|
+
if (!hasAssistantResponse) {
|
|
1858
|
+
await traceShipper.flush();
|
|
1859
|
+
state.currentEventId = void 0;
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (!state.parentId) {
|
|
1863
|
+
await eventShipper.finish(state.currentEventId, {
|
|
1864
|
+
userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
|
|
1865
|
+
model: modelName,
|
|
1866
|
+
output: outputText,
|
|
1867
|
+
properties: {
|
|
1868
|
+
plugin_version: PLUGIN_VERSION,
|
|
1869
|
+
message_id: messageId,
|
|
1870
|
+
...(isCompaction ? { is_compaction: true } : {}),
|
|
1871
|
+
},
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
await traceShipper.flush();
|
|
1875
|
+
state.currentEventId = void 0;
|
|
1876
|
+
} else if (event.type === "session.idle") {
|
|
1877
|
+
if (!sessionID) return;
|
|
1878
|
+
const state = sessions.get(String(sessionID));
|
|
1879
|
+
if (!state) return;
|
|
1880
|
+
if (state.currentRootSpan) {
|
|
1881
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
1882
|
+
state.currentRootSpan = void 0;
|
|
1883
|
+
}
|
|
1884
|
+
if (state.currentEventId) {
|
|
1885
|
+
state.currentEventId = void 0;
|
|
1886
|
+
}
|
|
1887
|
+
state.isCompacting = false;
|
|
1888
|
+
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
1889
|
+
if (state.parentId) {
|
|
1890
|
+
cleanupSessionParentMap(String(sessionID));
|
|
1891
|
+
sessions.delete(String(sessionID));
|
|
1892
|
+
} else {
|
|
1893
|
+
}
|
|
1894
|
+
} else if (event.type === "session.deleted") {
|
|
1895
|
+
if (!sessionID) return;
|
|
1896
|
+
const state = sessions.get(String(sessionID));
|
|
1897
|
+
if (!state) return;
|
|
1898
|
+
if (state.currentRootSpan) {
|
|
1899
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
1900
|
+
state.currentRootSpan = void 0;
|
|
1901
|
+
}
|
|
1902
|
+
if (state.currentEventId) {
|
|
1903
|
+
state.currentEventId = void 0;
|
|
1904
|
+
}
|
|
1905
|
+
cleanupSessionParentMap(String(sessionID));
|
|
1906
|
+
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
1907
|
+
sessions.delete(String(sessionID));
|
|
1908
|
+
} else if (event.type === "session.error") {
|
|
1909
|
+
const errorSessionID = (_e = props["sessionID"]) != null ? _e : sessionID;
|
|
1910
|
+
if (errorSessionID === void 0 || errorSessionID === "") throw new Error("session.error: missing sessionID");
|
|
1911
|
+
const state = sessions.get(String(errorSessionID));
|
|
1912
|
+
if (!state) return;
|
|
1913
|
+
const errorObj = props["error"];
|
|
1914
|
+
if (errorObj == null) throw new Error("session.error: missing error object");
|
|
1915
|
+
const errorName = errorObj.name;
|
|
1916
|
+
const errorMessage = (_g = (_f = errorObj.data) == null ? void 0 : _f.message) != null ? _g : errorObj.name;
|
|
1917
|
+
if (errorName == null && errorMessage == null) throw new Error("session.error: error object must have name or data.message");
|
|
1918
|
+
const errorStr = `${errorMessage != null ? errorMessage : errorName}
|
|
1919
|
+
|
|
1920
|
+
type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
1921
|
+
if (state.currentRootSpan) {
|
|
1922
|
+
traceShipper.endSpan(state.currentRootSpan, { error: errorStr });
|
|
1923
|
+
state.currentRootSpan = void 0;
|
|
1924
|
+
}
|
|
1925
|
+
if (state.currentEventId) {
|
|
1926
|
+
state.currentEventId = void 0;
|
|
1927
|
+
}
|
|
1928
|
+
cleanupSessionParentMap(String(errorSessionID));
|
|
1929
|
+
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
1930
|
+
sessions.delete(String(errorSessionID));
|
|
1931
|
+
}
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
rateLimitedErrorLog("event", `[oc-wsp] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
// ------------------------------------------------------------------
|
|
1937
|
+
// chat.message — user sent a message; start a new turn with a fresh event ID
|
|
1938
|
+
// ------------------------------------------------------------------
|
|
1939
|
+
// === SECTION: hook: chat.message ===
|
|
1940
|
+
"chat.message": async (messageInput, output) => {
|
|
1941
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1942
|
+
try {
|
|
1943
|
+
const { sessionID } = messageInput;
|
|
1944
|
+
let state = sessions.get(sessionID);
|
|
1945
|
+
if (!state) {
|
|
1946
|
+
state = createSessionState(sessionID);
|
|
1947
|
+
sessions.set(sessionID, state);
|
|
1948
|
+
}
|
|
1949
|
+
applyChildSessionParentToState(sessionID);
|
|
1950
|
+
if (state.currentRootSpan) {
|
|
1951
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
1952
|
+
state.currentRootSpan = void 0;
|
|
1953
|
+
}
|
|
1954
|
+
if (output == null) throw new Error("chat.message: output is required");
|
|
1955
|
+
const parts = output.parts;
|
|
1956
|
+
if (parts == null) throw new Error("chat.message: output.parts is required");
|
|
1957
|
+
const textParts = [];
|
|
1958
|
+
const newAttachments = [];
|
|
1959
|
+
const baseMeta = { ...config.eventMetadata };
|
|
1960
|
+
const promptMeta = {};
|
|
1961
|
+
for (const part of parts) {
|
|
1962
|
+
if (part["type"] === "text" && typeof part["text"] === "string") {
|
|
1963
|
+
textParts.push(part["text"]);
|
|
1964
|
+
const meta = part["metadata"];
|
|
1965
|
+
if (meta != null && typeof meta === "object") {
|
|
1966
|
+
const m = meta;
|
|
1967
|
+
if (typeof m["userId"] === "string") promptMeta.userId = m["userId"];
|
|
1968
|
+
if (typeof m["eventName"] === "string") promptMeta.eventName = m["eventName"];
|
|
1969
|
+
if (m["properties"] != null && typeof m["properties"] === "object") promptMeta.properties = m["properties"];
|
|
1970
|
+
}
|
|
1971
|
+
} else if (part["type"] === "file") {
|
|
1972
|
+
const mimeType = part["mediaType"];
|
|
1973
|
+
const filename = part["filename"];
|
|
1974
|
+
const url = part["url"];
|
|
1975
|
+
if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
|
|
1976
|
+
if (filename === void 0 || filename === "") throw new Error("chat.message: file part must have filename");
|
|
1977
|
+
newAttachments.push({ type: "image", role: "input", name: filename, value: url });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
state.eventMetadata = {
|
|
1982
|
+
...baseMeta,
|
|
1983
|
+
...promptMeta,
|
|
1984
|
+
properties: baseMeta.properties || promptMeta.properties ? { ...((_a = baseMeta.properties) != null ? _a : {}), ...((_b = promptMeta.properties) != null ? _b : {}) } : void 0,
|
|
1985
|
+
};
|
|
1986
|
+
const userText = capText2(textParts.join("\n"));
|
|
1987
|
+
state.currentInput = userText;
|
|
1988
|
+
if (state.parentId) {
|
|
1989
|
+
if (!state.parentTaskSpanIds || !state.parentEventContext) {
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
// KOLYA PATCH (F-010): recover the sub-agent display name by parsing
|
|
1993
|
+
// the chat.message parts. The child session's first user message IS
|
|
1994
|
+
// the task prompt; identity prompts open with 'You are "<name>"'.
|
|
1995
|
+
// This is the fallback path when the task tool's `description` never
|
|
1996
|
+
// reaches the hook (OpenCode 1.17+/1.18 args-shape quirk). Stashed
|
|
1997
|
+
// once per session; the Subagent root and child LLM spans carry
|
|
1998
|
+
// subagent_name so Workshop UI shows the label in the pill.
|
|
1999
|
+
if (state.subagentName === void 0) {
|
|
2000
|
+
const fromPrompt = extractSubagentNameFromPrompt(textParts.join("\n"));
|
|
2001
|
+
// F-010 v2: chain of fallbacks. Priority:
|
|
2002
|
+
// (1) identity preamble in prompt text
|
|
2003
|
+
// (2) parent's taskContexts (set by tool.execute.before)
|
|
2004
|
+
// (3) mapChildSessionToParent (tool.execute.after may be too late)
|
|
2005
|
+
if (fromPrompt) {
|
|
2006
|
+
state.subagentName = fromPrompt;
|
|
2007
|
+
} else {
|
|
2008
|
+
const parentId = state.parentId;
|
|
2009
|
+
if (parentId) {
|
|
2010
|
+
const runningCalls = runningTaskCallsBySession.get(parentId);
|
|
2011
|
+
if (runningCalls) {
|
|
2012
|
+
for (const callID of runningCalls) {
|
|
2013
|
+
const ctx = taskContexts.get(callKey(parentId, callID));
|
|
2014
|
+
if (ctx && ctx.subagentName) {
|
|
2015
|
+
state.subagentName = ctx.subagentName;
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (!state.subagentName) {
|
|
2022
|
+
const childMap = mapChildSessionToParent.get(sessionID);
|
|
2023
|
+
const fromMap = childMap == null ? void 0 : childMap.subagentName;
|
|
2024
|
+
if (typeof fromMap === "string" && fromMap.length > 0) {
|
|
2025
|
+
state.subagentName = fromMap;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
state.currentEventId = state.parentEventContext.eventId;
|
|
2031
|
+
const subagentRootAttrs = [
|
|
2032
|
+
attrString("workspace", worktree),
|
|
2033
|
+
attrString("directory", directory),
|
|
2034
|
+
attrString("hostname", hostname),
|
|
2035
|
+
attrString("os", os),
|
|
2036
|
+
attrString("is_subagent", "true"),
|
|
2037
|
+
attrString("parent_session_id", state.parentId),
|
|
2038
|
+
];
|
|
2039
|
+
if (state.subagentName) subagentRootAttrs.push(attrString("subagent_name", state.subagentName));
|
|
2040
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
2041
|
+
name: "Subagent",
|
|
2042
|
+
parent: state.parentTaskSpanIds,
|
|
2043
|
+
eventId: state.currentEventId,
|
|
2044
|
+
attributes: subagentRootAttrs,
|
|
2045
|
+
});
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
state.currentEventId = generateId();
|
|
2049
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
2050
|
+
name: state.isCompacting ? "ai.compaction" : "ai.event",
|
|
2051
|
+
eventId: state.currentEventId,
|
|
2052
|
+
attributes: [attrString("workspace", worktree), attrString("directory", directory), attrString("hostname", hostname), attrString("os", os)],
|
|
2053
|
+
});
|
|
2054
|
+
await eventShipper.patch(state.currentEventId, {
|
|
2055
|
+
isPending: true,
|
|
2056
|
+
userId: (_d = (_c = state.eventMetadata) == null ? void 0 : _c.userId) != null ? _d : state.sessionId,
|
|
2057
|
+
convoId: state.sessionId,
|
|
2058
|
+
eventName: (_f = (_e = state.eventMetadata) == null ? void 0 : _e.eventName) != null ? _f : config.eventName,
|
|
2059
|
+
input: userText,
|
|
2060
|
+
...(newAttachments.length > 0 ? { attachments: newAttachments } : {}),
|
|
2061
|
+
properties: {
|
|
2062
|
+
workspace: worktree,
|
|
2063
|
+
directory,
|
|
2064
|
+
hostname,
|
|
2065
|
+
os,
|
|
2066
|
+
plugin_version: PLUGIN_VERSION,
|
|
2067
|
+
...((_g = state.eventMetadata) == null ? void 0 : _g.properties),
|
|
2068
|
+
},
|
|
2069
|
+
});
|
|
2070
|
+
} catch (err) {}
|
|
2071
|
+
},
|
|
2072
|
+
// ------------------------------------------------------------------
|
|
2073
|
+
// tool.execute.before — record start time only (span created atomically in after)
|
|
2074
|
+
// For child sessions, tool spans are parented directly to the parent task span.
|
|
2075
|
+
// ------------------------------------------------------------------
|
|
2076
|
+
"tool.execute.before": async (toolInput, _output) => {
|
|
2077
|
+
var _a, _b, _c;
|
|
2078
|
+
try {
|
|
2079
|
+
const { tool, sessionID, callID } = toolInput;
|
|
2080
|
+
const state = sessions.get(sessionID);
|
|
2081
|
+
if (!state || !state.currentEventId) return;
|
|
2082
|
+
const spanParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : (_a = state.currentRootSpan) == null ? void 0 : _a.ids;
|
|
2083
|
+
if (!spanParent) return;
|
|
2084
|
+
const startTimeUnixNano = nowUnixNanoString();
|
|
2085
|
+
if (tool === "task") {
|
|
2086
|
+
// KOLYA PATCH (F-003): mirror of the ESM branch in tool.execute.before.
|
|
2087
|
+
// Attach human-readable subagent_name so Workshop UI can label it.
|
|
2088
|
+
const taskLabel = extractTaskLabel(toolInput.args);
|
|
2089
|
+
const taskAttrs = [
|
|
2090
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
2091
|
+
attrString("ai.toolCall.name", tool),
|
|
2092
|
+
attrString("ai.toolCall.id", callID),
|
|
2093
|
+
];
|
|
2094
|
+
if (taskLabel) taskAttrs.push(attrString("subagent_name", taskLabel));
|
|
2095
|
+
const liveTaskSpan = traceShipper.startSpan({
|
|
2096
|
+
name: "ai.toolCall",
|
|
2097
|
+
parent: spanParent,
|
|
2098
|
+
eventId: state.currentEventId,
|
|
2099
|
+
attributes: taskAttrs,
|
|
2100
|
+
});
|
|
2101
|
+
liveTaskSpan.startTimeUnixNano = startTimeUnixNano;
|
|
2102
|
+
const ctx = {
|
|
2103
|
+
ids: liveTaskSpan.ids,
|
|
2104
|
+
eventContext: {
|
|
2105
|
+
eventId: state.currentEventId,
|
|
2106
|
+
userId: (_c = (_b = state.eventMetadata) == null ? void 0 : _b.userId) != null ? _c : state.sessionId,
|
|
2107
|
+
convoId: state.sessionId,
|
|
2108
|
+
},
|
|
2109
|
+
};
|
|
2110
|
+
taskContexts.set(callKey(sessionID, callID), ctx);
|
|
2111
|
+
let running = runningTaskCallsBySession.get(sessionID);
|
|
2112
|
+
if (running == null) {
|
|
2113
|
+
running = /* @__PURE__ */ new Set();
|
|
2114
|
+
runningTaskCallsBySession.set(sessionID, running);
|
|
2115
|
+
}
|
|
2116
|
+
running.add(callID);
|
|
2117
|
+
resolvePendingChildrenForCall(sessionID, callID, "tool.execute.before");
|
|
2118
|
+
state.toolSpanStarts.set(callID, {
|
|
2119
|
+
startTimeUnixNano,
|
|
2120
|
+
parent: spanParent,
|
|
2121
|
+
eventId: state.currentEventId,
|
|
2122
|
+
name: tool,
|
|
2123
|
+
liveSpan: liveTaskSpan,
|
|
2124
|
+
});
|
|
2125
|
+
} else {
|
|
2126
|
+
state.toolSpanStarts.set(callID, {
|
|
2127
|
+
startTimeUnixNano,
|
|
2128
|
+
parent: spanParent,
|
|
2129
|
+
eventId: state.currentEventId,
|
|
2130
|
+
name: tool,
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
rateLimitedErrorLog("tool.execute.before", `[oc-wsp] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
// ------------------------------------------------------------------
|
|
2138
|
+
// tool.execute.after — create tool span atomically with both start and end times
|
|
2139
|
+
// ------------------------------------------------------------------
|
|
2140
|
+
// === SECTION: hook: tool.execute.after ===
|
|
2141
|
+
"tool.execute.after": async (toolInput, result) => {
|
|
2142
|
+
try {
|
|
2143
|
+
const { tool, sessionID, callID, args } = toolInput;
|
|
2144
|
+
const state = sessions.get(sessionID);
|
|
2145
|
+
if (!state || !state.currentEventId) return;
|
|
2146
|
+
if (args === void 0 || args === null) throw new Error("tool.execute.after: args is required");
|
|
2147
|
+
const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
|
|
2148
|
+
const startInfo = state.toolSpanStarts.get(callID);
|
|
2149
|
+
state.toolSpanStarts.delete(callID);
|
|
2150
|
+
const resultMetadata = result.metadata;
|
|
2151
|
+
const childSessionId = resultMetadata == null ? void 0 : resultMetadata["sessionId"];
|
|
2152
|
+
if (tool === "task" && childSessionId) {
|
|
2153
|
+
// KOLYA PATCH (F-010 v2): stash subagent_type from task args so the
|
|
2154
|
+
// child session can recover its display name when its chat.message
|
|
2155
|
+
// parts lack an identity preamble (OpenCode 1.18).
|
|
2156
|
+
const _taskSubagentName = extractSubagentNameFromTaskArgs(args);
|
|
2157
|
+
mapChildSessionToParent.set(childSessionId, {
|
|
2158
|
+
parentId: sessionID,
|
|
2159
|
+
subagentName: _taskSubagentName || void 0,
|
|
2160
|
+
});
|
|
2161
|
+
attachChildSessionToParentTask(childSessionId, sessionID, callID, "tool.execute.after");
|
|
2162
|
+
}
|
|
2163
|
+
if (startInfo) {
|
|
2164
|
+
const endTimeUnixNano = nowUnixNanoString();
|
|
2165
|
+
// KOLYA PATCH (F-001): for MCP tool calls OpenCode passes raw CallToolResult
|
|
2166
|
+
// ({content: [{type, text}]}) instead of {title, output, metadata}.
|
|
2167
|
+
// Upstream issue anomalyco/opencode#21149 won't be fixed (PR #21150
|
|
2168
|
+
// auto-closed 2026-05-15). We fall back to assembling text from
|
|
2169
|
+
// content[] so MCP spans land in Workshop instead of crashing the agent.
|
|
2170
|
+
let resultOutput = result.output;
|
|
2171
|
+
if (resultOutput === void 0 && Array.isArray(result.content)) {
|
|
2172
|
+
resultOutput = result.content
|
|
2173
|
+
.filter((c) => c && c.type === "text" && typeof c.text === "string")
|
|
2174
|
+
.map((c) => c.text)
|
|
2175
|
+
.join("\n") || "(empty MCP content)";
|
|
2176
|
+
}
|
|
2177
|
+
if (resultOutput === void 0) {
|
|
2178
|
+
resultOutput = "(no output)";
|
|
2179
|
+
}
|
|
2180
|
+
const toolResult = boundedStringify(resultOutput);
|
|
2181
|
+
// F-013: propagate error status from the SDK's tool result.
|
|
2182
|
+
const toolError = (() => {
|
|
2183
|
+
if (!result || typeof result !== "object") return void 0;
|
|
2184
|
+
const rErr = (result).error;
|
|
2185
|
+
if (rErr && typeof rErr === "object" && typeof rErr.message === "string") return rErr.message;
|
|
2186
|
+
if (typeof rErr === "string" && rErr.length > 0) return rErr;
|
|
2187
|
+
return void 0;
|
|
2188
|
+
})();
|
|
2189
|
+
if (startInfo.liveSpan) {
|
|
2190
|
+
traceShipper.endSpan(startInfo.liveSpan, {
|
|
2191
|
+
attributes: [attrString("ai.toolCall.args", toolCallArgs), attrString("ai.toolCall.result", toolResult)],
|
|
2192
|
+
...(toolError ? { error: toolError } : {}),
|
|
2193
|
+
});
|
|
2194
|
+
} else {
|
|
2195
|
+
traceShipper.createSpan({
|
|
2196
|
+
name: "ai.toolCall",
|
|
2197
|
+
parent: startInfo.parent,
|
|
2198
|
+
eventId: startInfo.eventId,
|
|
2199
|
+
startTimeUnixNano: startInfo.startTimeUnixNano,
|
|
2200
|
+
endTimeUnixNano,
|
|
2201
|
+
attributes: [
|
|
2202
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
2203
|
+
attrString("ai.toolCall.name", tool),
|
|
2204
|
+
attrString("ai.toolCall.id", callID),
|
|
2205
|
+
attrString("ai.toolCall.args", toolCallArgs),
|
|
2206
|
+
attrString("ai.toolCall.result", toolResult),
|
|
2207
|
+
],
|
|
2208
|
+
...(toolError ? { error: toolError } : {}),
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
if (tool === "task") {
|
|
2213
|
+
markTaskCallFinished(sessionID, callID);
|
|
2214
|
+
}
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
rateLimitedErrorLog("tool.execute.after", `[oc-wsp] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
// ------------------------------------------------------------------
|
|
2220
|
+
// experimental.session.compacting — fires before compaction LLM call
|
|
2221
|
+
// ------------------------------------------------------------------
|
|
2222
|
+
// === SECTION: hook: experimental.session.compacting ===
|
|
2223
|
+
"experimental.session.compacting": async (input, _output) => {
|
|
2224
|
+
try {
|
|
2225
|
+
const sessionID = input.sessionID;
|
|
2226
|
+
if (!sessionID) return;
|
|
2227
|
+
const state = sessions.get(sessionID);
|
|
2228
|
+
if (state) {
|
|
2229
|
+
state.isCompacting = true;
|
|
2230
|
+
}
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
rateLimitedErrorLog(
|
|
2233
|
+
"experimental.session.compacting",
|
|
2234
|
+
`[oc-wsp] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`,
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
// ------------------------------------------------------------------
|
|
2239
|
+
// experimental.chat.system.transform — capture system prompt (read-only)
|
|
2240
|
+
// ------------------------------------------------------------------
|
|
2241
|
+
// === SECTION: hook: experimental.chat.system.transform ===
|
|
2242
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
2243
|
+
try {
|
|
2244
|
+
// F-006 sidepanel: prepend a short role/MCP block so the agent
|
|
2245
|
+
// understands the Workshop sidepanel context. Only when sidepanelMode.
|
|
2246
|
+
if (sidepanelMode) {
|
|
2247
|
+
const runId = process.env.RAINDROP_SIDEPANEL_RUN_ID || "<none>";
|
|
2248
|
+
output.system = [
|
|
2249
|
+
[
|
|
2250
|
+
"You are the assistant in the Raindrop Workshop sidepanel — the local trace debugger for AI agents.",
|
|
2251
|
+
`Local Workshop MCP server is configured as 'workshop'. Use its tools (workshop__get_current_run, workshop__get_run_outline, workshop__query_traces, workshop__search_run, workshop__get_span_payload, workshop__get_span_context, workshop__annotate, workshop__replay_run, workshop__show_in_ui, workshop__ask_agent) when the user references the focused run, this trace, the current screen, or a captured span.`,
|
|
2252
|
+
`Currently focused Workshop run: ${runId}. Treat the user's first message as the in-flight request, not as a context reset.`,
|
|
2253
|
+
`Reply in the user's language.`,
|
|
2254
|
+
].join("\n"),
|
|
2255
|
+
...output.system,
|
|
2256
|
+
];
|
|
2257
|
+
}
|
|
2258
|
+
if (!config.captureSystemPrompt) return;
|
|
2259
|
+
const sessionID = input.sessionID;
|
|
2260
|
+
if (!sessionID) return;
|
|
2261
|
+
let state = sessions.get(sessionID);
|
|
2262
|
+
if (!state) {
|
|
2263
|
+
state = createSessionState(sessionID);
|
|
2264
|
+
sessions.set(sessionID, state);
|
|
2265
|
+
}
|
|
2266
|
+
const MAX_SYSTEM_PROMPT_LENGTH = 32768;
|
|
2267
|
+
const TRUNCATION_MARKER3 = "\n...[truncated]";
|
|
2268
|
+
const joined = output.system.join("\n\n");
|
|
2269
|
+
state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
rateLimitedErrorLog(
|
|
2272
|
+
"experimental.chat.system.transform",
|
|
2273
|
+
`[oc-wsp] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`,
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
},
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// src/index.ts
|
|
2281
|
+
// F-014 (LABEL_FOURTEEN_PLACEHOLDER): capture project / branch / commit
|
|
2282
|
+
// metadata once at plugin startup so each run can be tagged with the working
|
|
2283
|
+
// tree state. Failures are tolerated: this is best-effort context, not a
|
|
2284
|
+
// correctness path. Runs outside a git worktree return nulls.
|
|
2285
|
+
function collectGitContext(worktreePath) {
|
|
2286
|
+
if (!worktreePath) return { project: null, branch: null, commit: null };
|
|
2287
|
+
const cp = require("node:child_process");
|
|
2288
|
+
const pathModule = require("node:path");
|
|
2289
|
+
const safe = (args) => {
|
|
2290
|
+
try {
|
|
2291
|
+
return cp.execFileSync("git", args, { cwd: worktreePath, encoding: "utf8", timeout: 1500, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2292
|
+
} catch (e) {
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
const commit = safe(["rev-parse", "HEAD"]);
|
|
2297
|
+
const branch = safe(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2298
|
+
const toplevel = safe(["rev-parse", "--show-toplevel"]);
|
|
2299
|
+
return {
|
|
2300
|
+
project: toplevel ? pathModule.basename(toplevel) : null,
|
|
2301
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
2302
|
+
commit,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
async function plugin(input) {
|
|
2306
|
+
var _a;
|
|
2307
|
+
const config = loadConfig(input.directory);
|
|
2308
|
+
// KOLYA PATCH (F-002.6): when trace_only is set, redirect console.log/warn/error
|
|
2309
|
+
// to ~/.raindrop/trace.log (append). Keeps TUI stdout clean for the chat.
|
|
2310
|
+
// Backwards-compatible: default is to write to stdout (existing behaviour).
|
|
2311
|
+
if (config.traceOnly) {
|
|
2312
|
+
const TRACE_LOG_PATH = require("node:os").homedir() + "/.raindrop/trace.log";
|
|
2313
|
+
const fs = require("node:fs");
|
|
2314
|
+
try {
|
|
2315
|
+
fs.mkdirSync(require("node:os").homedir() + "/.raindrop", { recursive: true });
|
|
2316
|
+
} catch (e) {}
|
|
2317
|
+
const stamp = () => new Date().toISOString();
|
|
2318
|
+
console.log = (...args) => {
|
|
2319
|
+
try {
|
|
2320
|
+
fs.appendFileSync(TRACE_LOG_PATH, `[${stamp()}] [log] ${args.join(" ")}\n`);
|
|
2321
|
+
} catch (e) {}
|
|
2322
|
+
};
|
|
2323
|
+
console.warn = (...args) => {
|
|
2324
|
+
try {
|
|
2325
|
+
fs.appendFileSync(TRACE_LOG_PATH, `[${stamp()}] [warn] ${args.join(" ")}\n`);
|
|
2326
|
+
} catch (e) {}
|
|
2327
|
+
};
|
|
2328
|
+
console.error = (...args) => {
|
|
2329
|
+
try {
|
|
2330
|
+
fs.appendFileSync(TRACE_LOG_PATH, `[${stamp()}] [err] ${args.join(" ")}\n`);
|
|
2331
|
+
} catch (e) {}
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function appLog(level, message) {
|
|
2335
|
+
console.log(`[oc-wsp] [${level}] ${message}`);
|
|
2336
|
+
}
|
|
2337
|
+
appLog("info", `Loading ${PLUGIN_NAME} v${PLUGIN_VERSION}`);
|
|
2338
|
+
const resolvedLocalUrl = resolveLocalDebuggerBaseUrl(config.localWorkshopUrl);
|
|
2339
|
+
const hasLocalDestination = resolvedLocalUrl !== null;
|
|
2340
|
+
if (!config.writeKey && !hasLocalDestination) {
|
|
2341
|
+
appLog(
|
|
2342
|
+
"warn",
|
|
2343
|
+
"RAINDROP_WRITE_KEY not set and no local Workshop daemon detected \u2014 Raindrop tracing disabled. Set RAINDROP_WRITE_KEY for cloud, or RAINDROP_LOCAL_WORKSHOP_URL / RAINDROP_LOCAL_DEBUGGER for local-only mode.",
|
|
2344
|
+
);
|
|
2345
|
+
return {};
|
|
2346
|
+
}
|
|
2347
|
+
if (config.debug) {
|
|
2348
|
+
const destinations = [config.writeKey ? `cloud (${config.endpoint})` : null, resolvedLocalUrl ? `local Workshop (${resolvedLocalUrl})` : null].filter(Boolean);
|
|
2349
|
+
appLog("info", `Raindrop tracing enabled \u2014 destinations: ${destinations.join(", ")}`);
|
|
2350
|
+
}
|
|
2351
|
+
const worktree = (_a = input.worktree) != null ? _a : input.directory;
|
|
2352
|
+
const gitContext = collectGitContext(worktree);
|
|
2353
|
+
const eventShipper = new EventShipper2({
|
|
2354
|
+
writeKey: config.writeKey,
|
|
2355
|
+
endpoint: config.endpoint,
|
|
2356
|
+
debug: config.debug,
|
|
2357
|
+
projectId: config.projectId,
|
|
2358
|
+
localDebuggerUrl: config.localWorkshopUrl,
|
|
2359
|
+
gitContext,
|
|
2360
|
+
});
|
|
2361
|
+
const traceShipper = new TraceShipper2({
|
|
2362
|
+
writeKey: config.writeKey,
|
|
2363
|
+
endpoint: config.endpoint,
|
|
2364
|
+
debug: config.debug,
|
|
2365
|
+
projectId: config.projectId,
|
|
2366
|
+
localDebuggerUrl: config.localWorkshopUrl,
|
|
2367
|
+
});
|
|
2368
|
+
// === KOLYA PATCH (F-006): sidepanel bootstrap =============================
|
|
2369
|
+
// When the local Workshop daemon is the destination and the Workshop chat
|
|
2370
|
+
// bridge signals a sidepanel session in flight (RAINDROP_SIDEPANEL_ACTIVE=1,
|
|
2371
|
+
// RAINDROP_SIDEPANEL_RUN_ID=<id>), write a small config dir to
|
|
2372
|
+
// ~/.cache/workshop-sidepanel/<pid>-<ts> containing opencode.json (which
|
|
2373
|
+
// registers the `workshop` stdio MCP server) and export it as
|
|
2374
|
+
// OPENCODE_CONFIG_DIR so the spawning opencode process picks it up.
|
|
2375
|
+
// Sidepanel system prompt is injected via experimental.chat.system.transform
|
|
2376
|
+
// (see above). All errors are silent — bootstrap is best-effort.
|
|
2377
|
+
const sidepanelMode = hasLocalDestination && process.env.RAINDROP_SIDEPANEL_ACTIVE === "1";
|
|
2378
|
+
let workshopConfigDir = null;
|
|
2379
|
+
if (sidepanelMode) {
|
|
2380
|
+
appLog("info", `sidepanel bootstrap: run_id=${process.env.RAINDROP_SIDEPANEL_RUN_ID || ""}`);
|
|
2381
|
+
try {
|
|
2382
|
+
const os = require("node:os");
|
|
2383
|
+
const fs = require("node:fs");
|
|
2384
|
+
const path = require("node:path");
|
|
2385
|
+
const cacheRoot = path.join(os.homedir(), ".cache", "workshop-sidepanel");
|
|
2386
|
+
try { fs.mkdirSync(cacheRoot, { recursive: true }); } catch {}
|
|
2387
|
+
try {
|
|
2388
|
+
const now = Date.now();
|
|
2389
|
+
for (const entry of fs.readdirSync(cacheRoot)) {
|
|
2390
|
+
try {
|
|
2391
|
+
const st = fs.statSync(path.join(cacheRoot, entry));
|
|
2392
|
+
if (now - st.mtimeMs > 60 * 60 * 1000) fs.rmSync(path.join(cacheRoot, entry), { recursive: true, force: true });
|
|
2393
|
+
} catch {}
|
|
2394
|
+
}
|
|
2395
|
+
} catch {}
|
|
2396
|
+
const dir = path.join(cacheRoot, `${process.pid}-${Date.now()}`);
|
|
2397
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2398
|
+
const isCompiled = (process.execPath || "").toLowerCase().endsWith("raindrop");
|
|
2399
|
+
const command = isCompiled
|
|
2400
|
+
? [process.execPath, "workshop", "mcp"]
|
|
2401
|
+
: ["bun", "/home/gna/workspase/projects/opencode-workshop/src/index.ts", "workshop", "mcp"];
|
|
2402
|
+
const oc = {
|
|
2403
|
+
$schema: "https://opencode.ai/config.json",
|
|
2404
|
+
mcp: {
|
|
2405
|
+
workshop: {
|
|
2406
|
+
type: "local",
|
|
2407
|
+
command,
|
|
2408
|
+
enabled: true,
|
|
2409
|
+
environment: {
|
|
2410
|
+
// Workshop mcp server expects the bare daemon URL (no /v1/).
|
|
2411
|
+
RAINDROP_WORKSHOP_URL: (resolvedLocalUrl ?? "http://localhost:5899").replace(/\/v1\/?$/, ""),
|
|
2412
|
+
RAINDROP_WORKSHOP_AGENT_PROVIDER: "opencode",
|
|
2413
|
+
RAINDROP_WORKSHOP_ANNOTATION_SOURCE: "opencode",
|
|
2414
|
+
},
|
|
2415
|
+
},
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
fs.writeFileSync(path.join(dir, "opencode.json"), JSON.stringify(oc, null, 2) + "\n", "utf8");
|
|
2419
|
+
workshopConfigDir = dir;
|
|
2420
|
+
appLog("info", `sidepanel bootstrap: OPENCODE_CONFIG_DIR=${dir}`);
|
|
2421
|
+
} catch (err) {
|
|
2422
|
+
appLog("warn", `sidepanel bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (sidepanelMode && workshopConfigDir) {
|
|
2426
|
+
try { process.env.OPENCODE_CONFIG_DIR = workshopConfigDir; } catch {}
|
|
2427
|
+
}
|
|
2428
|
+
const hooks = createHooks(config, worktree, input.directory, eventShipper, traceShipper, resolvedLocalUrl, sidepanelMode);
|
|
2429
|
+
return hooks;
|
|
2430
|
+
}
|