@raindrop-ai/opencode-plugin 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.cjs +795 -232
- package/dist/index.js +795 -232
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -24,124 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
var import_node_fs = require("fs");
|
|
29
|
-
var import_node_os = require("os");
|
|
30
|
-
var import_node_path = require("path");
|
|
31
|
-
function loadConfig(projectDirectory) {
|
|
32
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
33
|
-
let merged = {};
|
|
34
|
-
const configPaths = [
|
|
35
|
-
(0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
|
|
36
|
-
(0, import_node_path.join)(projectDirectory, ".opencode", "raindrop.json")
|
|
37
|
-
];
|
|
38
|
-
for (const configPath of configPaths) {
|
|
39
|
-
try {
|
|
40
|
-
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
41
|
-
const content = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
42
|
-
const parsed = JSON.parse(content);
|
|
43
|
-
merged = { ...merged, ...parsed };
|
|
44
|
-
}
|
|
45
|
-
} catch (e) {
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
let eventMetadata;
|
|
49
|
-
const envMeta = process.env["RAINDROP_EVENT_METADATA"];
|
|
50
|
-
if (envMeta) {
|
|
51
|
-
try {
|
|
52
|
-
eventMetadata = JSON.parse(envMeta);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
|
|
58
|
-
endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
|
|
59
|
-
eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
|
|
60
|
-
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
|
|
61
|
-
captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
|
|
62
|
-
eventMetadata
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// package.json
|
|
67
|
-
var package_default = {
|
|
68
|
-
name: "@raindrop-ai/opencode-plugin",
|
|
69
|
-
version: "0.0.11",
|
|
70
|
-
description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
|
|
71
|
-
type: "module",
|
|
72
|
-
main: "dist/index.js",
|
|
73
|
-
module: "dist/index.js",
|
|
74
|
-
types: "dist/index.d.ts",
|
|
75
|
-
license: "MIT",
|
|
76
|
-
repository: {
|
|
77
|
-
type: "git",
|
|
78
|
-
url: "git+https://github.com/raindrop-ai/raindrop-js.git",
|
|
79
|
-
directory: "packages/opencode-plugin"
|
|
80
|
-
},
|
|
81
|
-
homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
|
|
82
|
-
bugs: {
|
|
83
|
-
url: "https://github.com/raindrop-ai/raindrop-js/issues"
|
|
84
|
-
},
|
|
85
|
-
exports: {
|
|
86
|
-
".": {
|
|
87
|
-
types: "./dist/index.d.ts",
|
|
88
|
-
import: "./dist/index.js",
|
|
89
|
-
require: "./dist/index.cjs"
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
sideEffects: false,
|
|
93
|
-
files: [
|
|
94
|
-
"dist/**"
|
|
95
|
-
],
|
|
96
|
-
scripts: {
|
|
97
|
-
build: "tsup",
|
|
98
|
-
dev: "tsup --watch",
|
|
99
|
-
clean: "rm -rf dist",
|
|
100
|
-
test: "cd tests && pnpm test"
|
|
101
|
-
},
|
|
102
|
-
peerDependencies: {
|
|
103
|
-
"@opencode-ai/plugin": ">=1.3.0",
|
|
104
|
-
"@opencode-ai/sdk": ">=1.3.0"
|
|
105
|
-
},
|
|
106
|
-
peerDependenciesMeta: {
|
|
107
|
-
"@opencode-ai/sdk": {
|
|
108
|
-
optional: true
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
devDependencies: {
|
|
112
|
-
"@raindrop-ai/core": "workspace:*",
|
|
113
|
-
"@opencode-ai/plugin": "^1.3.3",
|
|
114
|
-
"@opencode-ai/sdk": "^1.3.3",
|
|
115
|
-
"@types/node": "^20.11.17",
|
|
116
|
-
tsup: "^8.4.0",
|
|
117
|
-
typescript: "^5.3.3"
|
|
118
|
-
},
|
|
119
|
-
tsup: {
|
|
120
|
-
entry: [
|
|
121
|
-
"src/index.ts"
|
|
122
|
-
],
|
|
123
|
-
format: [
|
|
124
|
-
"cjs",
|
|
125
|
-
"esm"
|
|
126
|
-
],
|
|
127
|
-
dts: {
|
|
128
|
-
resolve: true
|
|
129
|
-
},
|
|
130
|
-
clean: true,
|
|
131
|
-
noExternal: [
|
|
132
|
-
"@raindrop-ai/core"
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
publishConfig: {
|
|
136
|
-
access: "public"
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// src/package-info.ts
|
|
141
|
-
var PLUGIN_NAME = package_default.name;
|
|
142
|
-
var PLUGIN_VERSION = package_default.version;
|
|
143
|
-
|
|
144
|
-
// ../core/dist/chunk-4UCYIEH4.js
|
|
27
|
+
// ../core/dist/chunk-U5HUTMR5.js
|
|
145
28
|
function getCrypto() {
|
|
146
29
|
const c = globalThis.crypto;
|
|
147
30
|
return c;
|
|
@@ -188,6 +71,8 @@ function base64Encode(bytes) {
|
|
|
188
71
|
function generateId() {
|
|
189
72
|
return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
|
|
190
73
|
}
|
|
74
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
75
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
191
76
|
function wait(ms) {
|
|
192
77
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
193
78
|
}
|
|
@@ -195,6 +80,46 @@ function formatEndpoint(endpoint) {
|
|
|
195
80
|
if (!endpoint) return void 0;
|
|
196
81
|
return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
|
|
197
82
|
}
|
|
83
|
+
function redactUrlForLog(url) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(url);
|
|
86
|
+
parsed.username = "";
|
|
87
|
+
parsed.password = "";
|
|
88
|
+
parsed.search = "";
|
|
89
|
+
parsed.hash = "";
|
|
90
|
+
return parsed.toString();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return "<unparseable-url>";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
|
|
96
|
+
var rateLimitedLogLast = /* @__PURE__ */ new Map();
|
|
97
|
+
function rateLimitedLog(key, log) {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const last = rateLimitedLogLast.get(key);
|
|
100
|
+
if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
rateLimitedLogLast.set(key, now);
|
|
104
|
+
log();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
async function raceWithTimeout(promise, timeoutMs) {
|
|
108
|
+
let timer;
|
|
109
|
+
const settledInTime = await Promise.race([
|
|
110
|
+
promise.then(
|
|
111
|
+
() => true,
|
|
112
|
+
() => true
|
|
113
|
+
),
|
|
114
|
+
new Promise((resolve) => {
|
|
115
|
+
var _a;
|
|
116
|
+
timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
|
|
117
|
+
(_a = timer.unref) == null ? void 0 : _a.call(timer);
|
|
118
|
+
})
|
|
119
|
+
]);
|
|
120
|
+
if (timer) clearTimeout(timer);
|
|
121
|
+
return settledInTime;
|
|
122
|
+
}
|
|
198
123
|
function parseRetryAfter(headers) {
|
|
199
124
|
var _a;
|
|
200
125
|
const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
|
|
@@ -211,12 +136,12 @@ function parseRetryAfter(headers) {
|
|
|
211
136
|
function getRetryDelayMs(attemptNumber, previousError) {
|
|
212
137
|
if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
|
|
213
138
|
const v = previousError.retryAfterMs;
|
|
214
|
-
if (typeof v === "number") return Math.max(0, v);
|
|
139
|
+
if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
|
|
215
140
|
}
|
|
216
141
|
if (attemptNumber <= 1) return 0;
|
|
217
142
|
const base = 500;
|
|
218
143
|
const factor = Math.pow(2, attemptNumber - 2);
|
|
219
|
-
return base * factor;
|
|
144
|
+
return Math.min(base * factor, MAX_RETRY_DELAY_MS);
|
|
220
145
|
}
|
|
221
146
|
async function withRetry(operation, opName, opts) {
|
|
222
147
|
const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
|
|
@@ -251,7 +176,9 @@ async function withRetry(operation, opName, opts) {
|
|
|
251
176
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
252
177
|
}
|
|
253
178
|
async function postJson(url, body, headers, opts) {
|
|
254
|
-
|
|
179
|
+
var _a;
|
|
180
|
+
const opName = `POST ${redactUrlForLog(url)}`;
|
|
181
|
+
const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
255
182
|
await withRetry(
|
|
256
183
|
async () => {
|
|
257
184
|
const resp = await fetch(url, {
|
|
@@ -260,7 +187,8 @@ async function postJson(url, body, headers, opts) {
|
|
|
260
187
|
"Content-Type": "application/json",
|
|
261
188
|
...headers
|
|
262
189
|
},
|
|
263
|
-
body: JSON.stringify(body)
|
|
190
|
+
body: JSON.stringify(body),
|
|
191
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
264
192
|
});
|
|
265
193
|
if (!resp.ok) {
|
|
266
194
|
const text = await resp.text().catch(() => "");
|
|
@@ -277,6 +205,27 @@ async function postJson(url, body, headers, opts) {
|
|
|
277
205
|
opts
|
|
278
206
|
);
|
|
279
207
|
}
|
|
208
|
+
var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
|
|
209
|
+
var TRUNCATION_MARKER = "...[truncated by raindrop]";
|
|
210
|
+
var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
|
|
211
|
+
function resolveMaxTextFieldChars(value) {
|
|
212
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
213
|
+
return Math.floor(value);
|
|
214
|
+
}
|
|
215
|
+
return currentDefaultMaxTextFieldChars;
|
|
216
|
+
}
|
|
217
|
+
function truncateToLimit(text, limit) {
|
|
218
|
+
if (limit > TRUNCATION_MARKER.length) {
|
|
219
|
+
return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
220
|
+
}
|
|
221
|
+
return text.slice(0, Math.max(0, limit));
|
|
222
|
+
}
|
|
223
|
+
function capText(value, limit) {
|
|
224
|
+
if (typeof value !== "string") return value;
|
|
225
|
+
const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
|
|
226
|
+
if (value.length <= max) return value;
|
|
227
|
+
return truncateToLimit(value, max);
|
|
228
|
+
}
|
|
280
229
|
var SpanStatusCode = {
|
|
281
230
|
UNSET: 0,
|
|
282
231
|
OK: 1,
|
|
@@ -334,6 +283,93 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
|
|
|
334
283
|
]
|
|
335
284
|
};
|
|
336
285
|
}
|
|
286
|
+
var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
|
|
287
|
+
var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
|
|
288
|
+
var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
|
|
289
|
+
function readEnvVar(name) {
|
|
290
|
+
var _a;
|
|
291
|
+
try {
|
|
292
|
+
const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
|
|
293
|
+
if (env && typeof env[name] === "string" && env[name].length > 0) {
|
|
294
|
+
return env[name];
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
}
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
function readWorkshopEnv() {
|
|
301
|
+
const raw = readEnvVar(WORKSHOP_ENV_VAR);
|
|
302
|
+
if (raw === void 0) return void 0;
|
|
303
|
+
const trimmed = raw.trim();
|
|
304
|
+
if (trimmed.length === 0) return void 0;
|
|
305
|
+
if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
|
|
306
|
+
if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
|
|
307
|
+
if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
function isLocalDevHost(hostname) {
|
|
311
|
+
if (!hostname) return false;
|
|
312
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
function readRuntimeHostname() {
|
|
319
|
+
try {
|
|
320
|
+
const loc = globalThis == null ? void 0 : globalThis.location;
|
|
321
|
+
if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
|
|
322
|
+
return loc.hostname;
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
}
|
|
326
|
+
return void 0;
|
|
327
|
+
}
|
|
328
|
+
function shouldAutoEnableLocalWorkshop() {
|
|
329
|
+
if (isLocalDevHost(readRuntimeHostname())) return true;
|
|
330
|
+
if (readEnvVar("NODE_ENV") === "development") return true;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function resolveLocalDebuggerBaseUrl(baseUrl) {
|
|
334
|
+
var _a, _b, _c;
|
|
335
|
+
if (baseUrl === null) return null;
|
|
336
|
+
if (typeof baseUrl === "string" && baseUrl.length > 0) {
|
|
337
|
+
return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
|
|
338
|
+
}
|
|
339
|
+
const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
|
|
340
|
+
if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
|
|
341
|
+
const workshopEnv = readWorkshopEnv();
|
|
342
|
+
if (workshopEnv === "disable") return null;
|
|
343
|
+
if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
344
|
+
if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
|
|
345
|
+
if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function mirrorTraceExportToLocalDebugger(body, options = {}) {
|
|
349
|
+
var _a;
|
|
350
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
351
|
+
if (!baseUrl) return;
|
|
352
|
+
void postJson(`${baseUrl}traces`, body, {}, {
|
|
353
|
+
maxAttempts: 1,
|
|
354
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
355
|
+
sdkName: options.sdkName
|
|
356
|
+
}).catch(() => {
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function mirrorPartialEventToLocalDebugger(event, options = {}) {
|
|
360
|
+
var _a;
|
|
361
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
362
|
+
if (!baseUrl) return;
|
|
363
|
+
const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
|
|
364
|
+
void postJson(`${baseUrl}events/track_partial`, event, headers, {
|
|
365
|
+
maxAttempts: 1,
|
|
366
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
367
|
+
sdkName: options.sdkName
|
|
368
|
+
}).catch(() => {
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
var SHUTDOWN_DEADLINE_MS = 1e4;
|
|
372
|
+
var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
337
373
|
function mergePatches(target, source) {
|
|
338
374
|
var _a, _b, _c, _d;
|
|
339
375
|
const out = { ...target, ...source };
|
|
@@ -351,7 +387,8 @@ var EventShipper = class {
|
|
|
351
387
|
this.sticky = /* @__PURE__ */ new Map();
|
|
352
388
|
this.timers = /* @__PURE__ */ new Map();
|
|
353
389
|
this.inFlight = /* @__PURE__ */ new Set();
|
|
354
|
-
|
|
390
|
+
this.hasShutdown = false;
|
|
391
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
355
392
|
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
356
393
|
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
357
394
|
this.enabled = opts.enabled !== false;
|
|
@@ -360,11 +397,16 @@ var EventShipper = class {
|
|
|
360
397
|
this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
|
|
361
398
|
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
362
399
|
this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
|
|
400
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
401
|
+
this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
|
|
402
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
403
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
404
|
+
}
|
|
363
405
|
const isNode = typeof process !== "undefined" && typeof process.version === "string";
|
|
364
406
|
this.context = {
|
|
365
407
|
library: {
|
|
366
|
-
name: (
|
|
367
|
-
version: (
|
|
408
|
+
name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
|
|
409
|
+
version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
|
|
368
410
|
},
|
|
369
411
|
metadata: {
|
|
370
412
|
jsRuntime: isNode ? "node" : "web",
|
|
@@ -378,10 +420,52 @@ var EventShipper = class {
|
|
|
378
420
|
authHeaders() {
|
|
379
421
|
return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
|
|
380
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Build the retry/timeout options for one POST, honoring the shutdown
|
|
425
|
+
* deadline. Returns `null` when the shutdown drain window is exhausted —
|
|
426
|
+
* the caller must drop the payload (with a rate-limited warning) instead
|
|
427
|
+
* of issuing a request that could outlive process exit.
|
|
428
|
+
*
|
|
429
|
+
* Checked fresh on EVERY send, so a shutdown that begins while the flush
|
|
430
|
+
* path is mid-drain takes effect immediately: no further retries, and the
|
|
431
|
+
* per-attempt timeout is clamped to the remaining window. After
|
|
432
|
+
* `shutdown()` returns (deadline cleared, `hasShutdown` still set),
|
|
433
|
+
* sends — late callers, or flush work the deadline abandoned mid-drain —
|
|
434
|
+
* run as a single short attempt rather than regaining the full retry
|
|
435
|
+
* schedule.
|
|
436
|
+
*/
|
|
437
|
+
requestOpts() {
|
|
438
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
439
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
440
|
+
if (remainingMs <= 0) return null;
|
|
441
|
+
return {
|
|
442
|
+
maxAttempts: 1,
|
|
443
|
+
debug: this.debug,
|
|
444
|
+
sdkName: this.sdkName,
|
|
445
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (this.hasShutdown) {
|
|
449
|
+
return {
|
|
450
|
+
maxAttempts: 1,
|
|
451
|
+
debug: this.debug,
|
|
452
|
+
sdkName: this.sdkName,
|
|
453
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
457
|
+
}
|
|
381
458
|
async patch(eventId, patch) {
|
|
382
459
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
383
460
|
if (!this.enabled) return;
|
|
384
461
|
if (!eventId || !eventId.trim()) return;
|
|
462
|
+
const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
|
|
463
|
+
if (typeof patch.input === "string" && patch.input.length > maxChars) {
|
|
464
|
+
patch = { ...patch, input: capText(patch.input, maxChars) };
|
|
465
|
+
}
|
|
466
|
+
if (typeof patch.output === "string" && patch.output.length > maxChars) {
|
|
467
|
+
patch = { ...patch, output: capText(patch.output, maxChars) };
|
|
468
|
+
}
|
|
385
469
|
if (this.debug) {
|
|
386
470
|
console.log(`${this.prefix} queue patch`, {
|
|
387
471
|
eventId,
|
|
@@ -428,9 +512,16 @@ var EventShipper = class {
|
|
|
428
512
|
})));
|
|
429
513
|
}
|
|
430
514
|
async shutdown() {
|
|
431
|
-
|
|
432
|
-
this.
|
|
433
|
-
|
|
515
|
+
this.hasShutdown = true;
|
|
516
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
517
|
+
try {
|
|
518
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
519
|
+
this.timers.clear();
|
|
520
|
+
const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
|
|
521
|
+
if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
|
|
522
|
+
} finally {
|
|
523
|
+
this.shutdownDeadlineAt = void 0;
|
|
524
|
+
}
|
|
434
525
|
}
|
|
435
526
|
async trackSignal(signal) {
|
|
436
527
|
var _a, _b;
|
|
@@ -450,16 +541,21 @@ var EventShipper = class {
|
|
|
450
541
|
}
|
|
451
542
|
}
|
|
452
543
|
];
|
|
544
|
+
if (!this.writeKey) return;
|
|
453
545
|
const url = `${this.baseUrl}signals/track`;
|
|
546
|
+
const opts = this.requestOpts();
|
|
547
|
+
if (!opts) {
|
|
548
|
+
this.warnShutdownDrop("signal");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
454
551
|
try {
|
|
455
|
-
await postJson(url, body, this.authHeaders(),
|
|
456
|
-
maxAttempts: 3,
|
|
457
|
-
debug: this.debug,
|
|
458
|
-
sdkName: this.sdkName
|
|
459
|
-
});
|
|
552
|
+
await postJson(url, body, this.authHeaders(), opts);
|
|
460
553
|
} catch (err) {
|
|
461
554
|
const msg = err instanceof Error ? err.message : String(err);
|
|
462
|
-
|
|
555
|
+
rateLimitedLog(
|
|
556
|
+
`${this.prefix}.send_signal_failed`,
|
|
557
|
+
() => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
|
|
558
|
+
);
|
|
463
559
|
}
|
|
464
560
|
}
|
|
465
561
|
async identify(users) {
|
|
@@ -480,19 +576,30 @@ var EventShipper = class {
|
|
|
480
576
|
traits: (_a = user.traits) != null ? _a : {}
|
|
481
577
|
};
|
|
482
578
|
});
|
|
579
|
+
if (!this.writeKey) return;
|
|
483
580
|
if (body.length === 0) return;
|
|
484
581
|
const url = `${this.baseUrl}users/identify`;
|
|
582
|
+
const opts = this.requestOpts();
|
|
583
|
+
if (!opts) {
|
|
584
|
+
this.warnShutdownDrop("identify");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
485
587
|
try {
|
|
486
|
-
await postJson(url, body, this.authHeaders(),
|
|
487
|
-
maxAttempts: 3,
|
|
488
|
-
debug: this.debug,
|
|
489
|
-
sdkName: this.sdkName
|
|
490
|
-
});
|
|
588
|
+
await postJson(url, body, this.authHeaders(), opts);
|
|
491
589
|
} catch (err) {
|
|
492
590
|
const msg = err instanceof Error ? err.message : String(err);
|
|
493
|
-
|
|
591
|
+
rateLimitedLog(
|
|
592
|
+
`${this.prefix}.send_identify_failed`,
|
|
593
|
+
() => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
|
|
594
|
+
);
|
|
494
595
|
}
|
|
495
596
|
}
|
|
597
|
+
warnShutdownDrop(what) {
|
|
598
|
+
rateLimitedLog(
|
|
599
|
+
`${this.prefix}.shutdown_deadline`,
|
|
600
|
+
() => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
|
|
601
|
+
);
|
|
602
|
+
}
|
|
496
603
|
async flushOne(eventId) {
|
|
497
604
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
498
605
|
if (!this.enabled) return;
|
|
@@ -556,11 +663,25 @@ var EventShipper = class {
|
|
|
556
663
|
endpoint: url
|
|
557
664
|
});
|
|
558
665
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
666
|
+
if (this.localDebuggerUrl) {
|
|
667
|
+
mirrorPartialEventToLocalDebugger(payload, {
|
|
668
|
+
baseUrl: this.localDebuggerUrl,
|
|
669
|
+
writeKey: this.writeKey,
|
|
670
|
+
debug: this.debug,
|
|
671
|
+
sdkName: this.sdkName
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
if (!this.writeKey) {
|
|
675
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const opts = this.requestOpts();
|
|
679
|
+
if (!opts) {
|
|
680
|
+
this.warnShutdownDrop(`track_partial ${eventId}`);
|
|
681
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const p = postJson(url, payload, this.authHeaders(), opts);
|
|
564
685
|
this.inFlight.add(p);
|
|
565
686
|
try {
|
|
566
687
|
try {
|
|
@@ -570,7 +691,10 @@ var EventShipper = class {
|
|
|
570
691
|
}
|
|
571
692
|
} catch (err) {
|
|
572
693
|
const msg = err instanceof Error ? err.message : String(err);
|
|
573
|
-
|
|
694
|
+
rateLimitedLog(
|
|
695
|
+
`${this.prefix}.send_track_partial_failed`,
|
|
696
|
+
() => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
|
|
697
|
+
);
|
|
574
698
|
}
|
|
575
699
|
} finally {
|
|
576
700
|
this.inFlight.delete(p);
|
|
@@ -580,28 +704,114 @@ var EventShipper = class {
|
|
|
580
704
|
}
|
|
581
705
|
}
|
|
582
706
|
};
|
|
583
|
-
var
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
707
|
+
var DEFAULT_SECRET_KEY_NAMES = [
|
|
708
|
+
"apikey",
|
|
709
|
+
"apisecret",
|
|
710
|
+
"apitoken",
|
|
711
|
+
"secretaccesskey",
|
|
712
|
+
"sessiontoken",
|
|
713
|
+
"privatekey",
|
|
714
|
+
"privatekeyid",
|
|
715
|
+
"clientsecret",
|
|
716
|
+
"accesstoken",
|
|
717
|
+
"refreshtoken",
|
|
718
|
+
"oauthtoken",
|
|
719
|
+
"bearertoken",
|
|
720
|
+
"authorization",
|
|
721
|
+
"password",
|
|
722
|
+
"passphrase"
|
|
723
|
+
];
|
|
724
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
725
|
+
function normalizeKeyName(name) {
|
|
726
|
+
return name.toLowerCase().replace(/[-_.]/g, "");
|
|
588
727
|
}
|
|
589
|
-
function
|
|
590
|
-
var _a;
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
728
|
+
function redactSecretsInObject(value, options) {
|
|
729
|
+
var _a, _b;
|
|
730
|
+
const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
|
|
731
|
+
const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
|
|
732
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
733
|
+
const walk = (node) => {
|
|
734
|
+
if (node === null || typeof node !== "object") return node;
|
|
735
|
+
if (seen.has(node)) return "[CIRCULAR]";
|
|
736
|
+
seen.add(node);
|
|
737
|
+
if (Array.isArray(node)) {
|
|
738
|
+
return node.map((item) => walk(item));
|
|
739
|
+
}
|
|
740
|
+
const out = {};
|
|
741
|
+
for (const [k, v] of Object.entries(node)) {
|
|
742
|
+
if (normalizedSecretSet.has(normalizeKeyName(k))) {
|
|
743
|
+
out[k] = placeholder;
|
|
744
|
+
} else {
|
|
745
|
+
out[k] = walk(v);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
};
|
|
750
|
+
return walk(value);
|
|
751
|
+
}
|
|
752
|
+
function buildSecretSet(names) {
|
|
753
|
+
const set = /* @__PURE__ */ new Set();
|
|
754
|
+
for (const name of names) set.add(normalizeKeyName(name));
|
|
755
|
+
return set;
|
|
756
|
+
}
|
|
757
|
+
var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
|
|
758
|
+
"ai.request.providerOptions",
|
|
759
|
+
"ai.response.providerMetadata"
|
|
760
|
+
];
|
|
761
|
+
function defaultTransformSpan(span) {
|
|
762
|
+
const attrs = span.attributes;
|
|
763
|
+
if (!attrs || attrs.length === 0) return span;
|
|
764
|
+
let nextAttrs;
|
|
765
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
766
|
+
const attr = attrs[i];
|
|
767
|
+
const redacted = redactJsonAttributeValue(attr.key, attr.value);
|
|
768
|
+
if (redacted === void 0) continue;
|
|
769
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
770
|
+
nextAttrs[i] = { key: attr.key, value: redacted };
|
|
771
|
+
}
|
|
772
|
+
if (!nextAttrs) return span;
|
|
773
|
+
return { ...span, attributes: nextAttrs };
|
|
774
|
+
}
|
|
775
|
+
var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
|
|
776
|
+
function redactJsonAttributeValue(key, value) {
|
|
777
|
+
if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
|
|
778
|
+
const json = value.stringValue;
|
|
779
|
+
if (typeof json !== "string" || json.length === 0) return void 0;
|
|
780
|
+
let parsed;
|
|
781
|
+
try {
|
|
782
|
+
parsed = JSON.parse(json);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
return void 0;
|
|
785
|
+
}
|
|
786
|
+
const scrubbed = redactSecretsInObject(parsed);
|
|
787
|
+
let scrubbedJson;
|
|
788
|
+
try {
|
|
789
|
+
scrubbedJson = JSON.stringify(scrubbed);
|
|
790
|
+
} catch (e) {
|
|
791
|
+
return void 0;
|
|
792
|
+
}
|
|
793
|
+
if (scrubbedJson === json) return void 0;
|
|
794
|
+
return { stringValue: scrubbedJson };
|
|
795
|
+
}
|
|
796
|
+
function applyOtelSpanAttributeLimit(limit) {
|
|
797
|
+
var _a, _b;
|
|
798
|
+
try {
|
|
799
|
+
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;
|
|
800
|
+
if (!raw) return limit;
|
|
801
|
+
const parsed = Number.parseInt(raw, 10);
|
|
802
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
803
|
+
return Math.min(limit, parsed);
|
|
804
|
+
}
|
|
805
|
+
} catch (e) {
|
|
806
|
+
}
|
|
807
|
+
return limit;
|
|
599
808
|
}
|
|
600
809
|
var TraceShipper = class {
|
|
601
810
|
constructor(opts) {
|
|
602
811
|
this.queue = [];
|
|
603
812
|
this.inFlight = /* @__PURE__ */ new Set();
|
|
604
|
-
|
|
813
|
+
this.hasShutdown = false;
|
|
814
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
605
815
|
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
606
816
|
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
607
817
|
this.enabled = opts.enabled !== false;
|
|
@@ -614,13 +824,79 @@ var TraceShipper = class {
|
|
|
614
824
|
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
615
825
|
this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
|
|
616
826
|
this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
this.
|
|
620
|
-
|
|
621
|
-
|
|
827
|
+
this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
|
|
828
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
829
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
830
|
+
}
|
|
831
|
+
this.transformSpanHook = opts.transformSpan;
|
|
832
|
+
this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
|
|
833
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Cap every string attribute value on the span. O(#attributes) length
|
|
837
|
+
* checks; only oversized values pay a slice. Runs AFTER the redaction
|
|
838
|
+
* pipeline so the default secret-scrub still sees parseable JSON in
|
|
839
|
+
* `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
|
|
840
|
+
* first could cut a JSON blob mid-way, fail the parse, and ship secrets
|
|
841
|
+
* in the surviving prefix).
|
|
842
|
+
*
|
|
843
|
+
* A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
|
|
844
|
+
* for span content, matching the Python SDK and the OTel SDK convention.
|
|
845
|
+
*/
|
|
846
|
+
capSpanAttributes(span) {
|
|
847
|
+
var _a;
|
|
848
|
+
const maxChars = applyOtelSpanAttributeLimit(
|
|
849
|
+
resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
|
|
850
|
+
);
|
|
851
|
+
const attrs = span.attributes;
|
|
852
|
+
if (!attrs || attrs.length === 0) return span;
|
|
853
|
+
let nextAttrs;
|
|
854
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
855
|
+
const attr = attrs[i];
|
|
856
|
+
const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
|
|
857
|
+
if (typeof value !== "string" || value.length <= maxChars) continue;
|
|
858
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
859
|
+
nextAttrs[i] = {
|
|
860
|
+
key: attr.key,
|
|
861
|
+
value: { ...attr.value, stringValue: capText(value, maxChars) }
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (!nextAttrs) return span;
|
|
865
|
+
return { ...span, attributes: nextAttrs };
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Apply the user `transformSpan` hook (if any) followed by the default
|
|
869
|
+
* redactor (unless disabled). Returns either the (possibly new) span to
|
|
870
|
+
* ship, or `null` to drop the span entirely.
|
|
871
|
+
*
|
|
872
|
+
* Ordering: user hook runs first so callers can rewrite the span freely
|
|
873
|
+
* (rename attrs, add new ones, scrub things the default doesn't know
|
|
874
|
+
* about). The default redactor then runs on whatever the user produced,
|
|
875
|
+
* acting as the always-on floor for documented BYOK secrets. If the user
|
|
876
|
+
* sets `disableDefaultRedaction: true`, the floor is skipped.
|
|
877
|
+
*
|
|
878
|
+
* Fail-closed: if the user hook throws, the span is dropped — a buggy
|
|
879
|
+
* hook can never accidentally ship raw, un-redacted spans.
|
|
880
|
+
*/
|
|
881
|
+
redactSpan(span) {
|
|
882
|
+
let current = span;
|
|
883
|
+
if (this.transformSpanHook) {
|
|
884
|
+
try {
|
|
885
|
+
const result = this.transformSpanHook(current);
|
|
886
|
+
if (result === null) return null;
|
|
887
|
+
if (result !== void 0) current = result;
|
|
888
|
+
} catch (err) {
|
|
889
|
+
if (this.debug) {
|
|
890
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
891
|
+
console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
622
894
|
}
|
|
623
895
|
}
|
|
896
|
+
if (!this.disableDefaultRedaction) {
|
|
897
|
+
current = defaultTransformSpan(current);
|
|
898
|
+
}
|
|
899
|
+
return this.capSpanAttributes(current);
|
|
624
900
|
}
|
|
625
901
|
isDebugEnabled() {
|
|
626
902
|
return this.debug;
|
|
@@ -638,8 +914,8 @@ var TraceShipper = class {
|
|
|
638
914
|
];
|
|
639
915
|
if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
|
|
640
916
|
const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
|
|
641
|
-
|
|
642
|
-
|
|
917
|
+
this.mirrorToLocalDebugger(
|
|
918
|
+
buildOtlpSpan({
|
|
643
919
|
ids: span.ids,
|
|
644
920
|
name: span.name,
|
|
645
921
|
startTimeUnixNano: span.startTimeUnixNano,
|
|
@@ -647,16 +923,21 @@ var TraceShipper = class {
|
|
|
647
923
|
// placeholder — will be updated on endSpan
|
|
648
924
|
attributes: span.attributes,
|
|
649
925
|
status: { code: SpanStatusCode.UNSET }
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
653
|
-
baseUrl: this.localDebuggerUrl,
|
|
654
|
-
debug: false,
|
|
655
|
-
sdkName: this.sdkName
|
|
656
|
-
});
|
|
657
|
-
}
|
|
926
|
+
})
|
|
927
|
+
);
|
|
658
928
|
return span;
|
|
659
929
|
}
|
|
930
|
+
mirrorToLocalDebugger(span) {
|
|
931
|
+
if (!this.localDebuggerUrl) return;
|
|
932
|
+
const redacted = this.redactSpan(span);
|
|
933
|
+
if (redacted === null) return;
|
|
934
|
+
const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
|
|
935
|
+
mirrorTraceExportToLocalDebugger(body, {
|
|
936
|
+
baseUrl: this.localDebuggerUrl,
|
|
937
|
+
debug: false,
|
|
938
|
+
sdkName: this.sdkName
|
|
939
|
+
});
|
|
940
|
+
}
|
|
660
941
|
endSpan(span, extra) {
|
|
661
942
|
var _a, _b;
|
|
662
943
|
if (span.endTimeUnixNano) return;
|
|
@@ -678,14 +959,7 @@ var TraceShipper = class {
|
|
|
678
959
|
status
|
|
679
960
|
});
|
|
680
961
|
this.enqueue(otlp);
|
|
681
|
-
|
|
682
|
-
const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
|
|
683
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
684
|
-
baseUrl: this.localDebuggerUrl,
|
|
685
|
-
debug: false,
|
|
686
|
-
sdkName: this.sdkName
|
|
687
|
-
});
|
|
688
|
-
}
|
|
962
|
+
this.mirrorToLocalDebugger(otlp);
|
|
689
963
|
}
|
|
690
964
|
createSpan(args) {
|
|
691
965
|
var _a;
|
|
@@ -703,14 +977,7 @@ var TraceShipper = class {
|
|
|
703
977
|
status: args.status
|
|
704
978
|
});
|
|
705
979
|
this.enqueue(otlp);
|
|
706
|
-
|
|
707
|
-
const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
|
|
708
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
709
|
-
baseUrl: this.localDebuggerUrl,
|
|
710
|
-
debug: false,
|
|
711
|
-
sdkName: this.sdkName
|
|
712
|
-
});
|
|
713
|
-
}
|
|
980
|
+
this.mirrorToLocalDebugger(otlp);
|
|
714
981
|
}
|
|
715
982
|
enqueue(span) {
|
|
716
983
|
if (!this.enabled) return;
|
|
@@ -722,10 +989,12 @@ var TraceShipper = class {
|
|
|
722
989
|
)}`
|
|
723
990
|
);
|
|
724
991
|
}
|
|
992
|
+
const redacted = this.redactSpan(span);
|
|
993
|
+
if (redacted === null) return;
|
|
725
994
|
if (this.queue.length >= this.maxQueueSize) {
|
|
726
995
|
this.queue.shift();
|
|
727
996
|
}
|
|
728
|
-
this.queue.push(
|
|
997
|
+
this.queue.push(redacted);
|
|
729
998
|
if (this.queue.length >= this.maxBatchSize) {
|
|
730
999
|
void this.flush().catch(() => {
|
|
731
1000
|
});
|
|
@@ -747,6 +1016,17 @@ var TraceShipper = class {
|
|
|
747
1016
|
}
|
|
748
1017
|
while (this.queue.length > 0) {
|
|
749
1018
|
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
1019
|
+
if (!this.writeKey) continue;
|
|
1020
|
+
const opts = this.requestOpts();
|
|
1021
|
+
if (!opts) {
|
|
1022
|
+
rateLimitedLog(
|
|
1023
|
+
`${this.prefix}.shutdown_deadline`,
|
|
1024
|
+
() => console.warn(
|
|
1025
|
+
`${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
750
1030
|
const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
|
|
751
1031
|
const url = `${this.baseUrl}traces`;
|
|
752
1032
|
if (this.debug) {
|
|
@@ -755,11 +1035,7 @@ var TraceShipper = class {
|
|
|
755
1035
|
endpoint: url
|
|
756
1036
|
});
|
|
757
1037
|
}
|
|
758
|
-
const p = postJson(url, body, this.authHeaders(),
|
|
759
|
-
maxAttempts: 3,
|
|
760
|
-
debug: this.debug,
|
|
761
|
-
sdkName: this.sdkName
|
|
762
|
-
});
|
|
1038
|
+
const p = postJson(url, body, this.authHeaders(), opts);
|
|
763
1039
|
this.inFlight.add(p);
|
|
764
1040
|
try {
|
|
765
1041
|
try {
|
|
@@ -767,21 +1043,61 @@ var TraceShipper = class {
|
|
|
767
1043
|
if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
|
|
768
1044
|
} catch (err) {
|
|
769
1045
|
const msg = err instanceof Error ? err.message : String(err);
|
|
770
|
-
|
|
1046
|
+
rateLimitedLog(
|
|
1047
|
+
`${this.prefix}.send_spans_failed`,
|
|
1048
|
+
() => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
|
|
1049
|
+
);
|
|
771
1050
|
}
|
|
772
1051
|
} finally {
|
|
773
1052
|
this.inFlight.delete(p);
|
|
774
1053
|
}
|
|
775
1054
|
}
|
|
776
1055
|
}
|
|
1056
|
+
/** See EventShipper.requestOpts — same shutdown-budget semantics. */
|
|
1057
|
+
requestOpts() {
|
|
1058
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
1059
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
1060
|
+
if (remainingMs <= 0) return null;
|
|
1061
|
+
return {
|
|
1062
|
+
maxAttempts: 1,
|
|
1063
|
+
debug: this.debug,
|
|
1064
|
+
sdkName: this.sdkName,
|
|
1065
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
if (this.hasShutdown) {
|
|
1069
|
+
return {
|
|
1070
|
+
maxAttempts: 1,
|
|
1071
|
+
debug: this.debug,
|
|
1072
|
+
sdkName: this.sdkName,
|
|
1073
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
1077
|
+
}
|
|
777
1078
|
async shutdown() {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1079
|
+
this.hasShutdown = true;
|
|
1080
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
1081
|
+
try {
|
|
1082
|
+
if (this.timer) {
|
|
1083
|
+
clearTimeout(this.timer);
|
|
1084
|
+
this.timer = void 0;
|
|
1085
|
+
}
|
|
1086
|
+
const drain = async () => {
|
|
1087
|
+
await this.flush();
|
|
1088
|
+
await Promise.all([...this.inFlight].map((p) => p.catch(() => {
|
|
1089
|
+
})));
|
|
1090
|
+
};
|
|
1091
|
+
const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
|
|
1092
|
+
if (!settled) {
|
|
1093
|
+
rateLimitedLog(
|
|
1094
|
+
`${this.prefix}.shutdown_deadline`,
|
|
1095
|
+
() => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
} finally {
|
|
1099
|
+
this.shutdownDeadlineAt = void 0;
|
|
781
1100
|
}
|
|
782
|
-
await this.flush();
|
|
783
|
-
await Promise.all([...this.inFlight].map((p) => p.catch(() => {
|
|
784
|
-
})));
|
|
785
1101
|
}
|
|
786
1102
|
};
|
|
787
1103
|
|
|
@@ -789,6 +1105,134 @@ var TraceShipper = class {
|
|
|
789
1105
|
var import_async_hooks = require("async_hooks");
|
|
790
1106
|
globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
|
|
791
1107
|
|
|
1108
|
+
// src/config.ts
|
|
1109
|
+
var import_node_fs = require("fs");
|
|
1110
|
+
var import_node_os = require("os");
|
|
1111
|
+
var import_node_path = require("path");
|
|
1112
|
+
function loadConfig(projectDirectory) {
|
|
1113
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1114
|
+
let merged = {};
|
|
1115
|
+
const configPaths = [
|
|
1116
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "opencode", "raindrop.json"),
|
|
1117
|
+
(0, import_node_path.join)(projectDirectory, ".opencode", "raindrop.json")
|
|
1118
|
+
];
|
|
1119
|
+
for (const configPath of configPaths) {
|
|
1120
|
+
try {
|
|
1121
|
+
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
1122
|
+
const content = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
1123
|
+
const parsed = JSON.parse(content);
|
|
1124
|
+
merged = { ...merged, ...parsed };
|
|
1125
|
+
}
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
let eventMetadata;
|
|
1130
|
+
const envMeta = process.env["RAINDROP_EVENT_METADATA"];
|
|
1131
|
+
if (envMeta) {
|
|
1132
|
+
try {
|
|
1133
|
+
eventMetadata = JSON.parse(envMeta);
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
|
|
1139
|
+
endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
|
|
1140
|
+
eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
|
|
1141
|
+
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false,
|
|
1142
|
+
captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_g = merged.capture_system_prompt) != null ? _g : false,
|
|
1143
|
+
eventMetadata,
|
|
1144
|
+
localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url)
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function resolveLocalWorkshopUrl(fileValue) {
|
|
1148
|
+
const envValue = process.env["RAINDROP_LOCAL_WORKSHOP_URL"];
|
|
1149
|
+
if (envValue !== void 0) {
|
|
1150
|
+
if (envValue === "" || envValue.toLowerCase() === "null" || envValue.toLowerCase() === "false") {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
return envValue;
|
|
1154
|
+
}
|
|
1155
|
+
return fileValue;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// package.json
|
|
1159
|
+
var package_default = {
|
|
1160
|
+
name: "@raindrop-ai/opencode-plugin",
|
|
1161
|
+
version: "0.0.13",
|
|
1162
|
+
description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
|
|
1163
|
+
type: "module",
|
|
1164
|
+
main: "dist/index.js",
|
|
1165
|
+
module: "dist/index.js",
|
|
1166
|
+
types: "dist/index.d.ts",
|
|
1167
|
+
license: "MIT",
|
|
1168
|
+
repository: {
|
|
1169
|
+
type: "git",
|
|
1170
|
+
url: "git+https://github.com/raindrop-ai/raindrop-js.git",
|
|
1171
|
+
directory: "packages/opencode-plugin"
|
|
1172
|
+
},
|
|
1173
|
+
homepage: "https://github.com/raindrop-ai/raindrop-js/tree/main/packages/opencode-plugin#readme",
|
|
1174
|
+
bugs: {
|
|
1175
|
+
url: "https://github.com/raindrop-ai/raindrop-js/issues"
|
|
1176
|
+
},
|
|
1177
|
+
exports: {
|
|
1178
|
+
".": {
|
|
1179
|
+
types: "./dist/index.d.ts",
|
|
1180
|
+
import: "./dist/index.js",
|
|
1181
|
+
require: "./dist/index.cjs"
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
sideEffects: false,
|
|
1185
|
+
files: [
|
|
1186
|
+
"dist/**"
|
|
1187
|
+
],
|
|
1188
|
+
scripts: {
|
|
1189
|
+
build: "tsup",
|
|
1190
|
+
dev: "tsup --watch",
|
|
1191
|
+
clean: "rm -rf dist",
|
|
1192
|
+
test: "cd tests && pnpm test"
|
|
1193
|
+
},
|
|
1194
|
+
peerDependencies: {
|
|
1195
|
+
"@opencode-ai/plugin": ">=1.3.0",
|
|
1196
|
+
"@opencode-ai/sdk": ">=1.3.0"
|
|
1197
|
+
},
|
|
1198
|
+
peerDependenciesMeta: {
|
|
1199
|
+
"@opencode-ai/sdk": {
|
|
1200
|
+
optional: true
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
devDependencies: {
|
|
1204
|
+
"@raindrop-ai/core": "workspace:*",
|
|
1205
|
+
"@opencode-ai/plugin": "^1.3.3",
|
|
1206
|
+
"@opencode-ai/sdk": "^1.3.3",
|
|
1207
|
+
"@types/node": "^20.11.17",
|
|
1208
|
+
tsup: "^8.4.0",
|
|
1209
|
+
typescript: "^5.3.3"
|
|
1210
|
+
},
|
|
1211
|
+
tsup: {
|
|
1212
|
+
entry: [
|
|
1213
|
+
"src/index.ts"
|
|
1214
|
+
],
|
|
1215
|
+
format: [
|
|
1216
|
+
"cjs",
|
|
1217
|
+
"esm"
|
|
1218
|
+
],
|
|
1219
|
+
dts: {
|
|
1220
|
+
resolve: true
|
|
1221
|
+
},
|
|
1222
|
+
clean: true,
|
|
1223
|
+
noExternal: [
|
|
1224
|
+
"@raindrop-ai/core"
|
|
1225
|
+
]
|
|
1226
|
+
},
|
|
1227
|
+
publishConfig: {
|
|
1228
|
+
access: "public"
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// src/package-info.ts
|
|
1233
|
+
var PLUGIN_NAME = package_default.name;
|
|
1234
|
+
var PLUGIN_VERSION = package_default.version;
|
|
1235
|
+
|
|
792
1236
|
// src/shipper.ts
|
|
793
1237
|
var EventShipper2 = class extends EventShipper {
|
|
794
1238
|
constructor(opts) {
|
|
@@ -824,6 +1268,86 @@ var TraceShipper2 = class extends TraceShipper {
|
|
|
824
1268
|
}
|
|
825
1269
|
};
|
|
826
1270
|
|
|
1271
|
+
// src/bounded.ts
|
|
1272
|
+
var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
|
|
1273
|
+
var MAX_TEXT_FIELD_CHARS = 1e6;
|
|
1274
|
+
var MAX_BOUNDED_DEPTH = 12;
|
|
1275
|
+
function truncateToLimit2(text, limit) {
|
|
1276
|
+
if (text.length <= limit) return text;
|
|
1277
|
+
if (limit > TRUNCATION_MARKER2.length) {
|
|
1278
|
+
return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
|
|
1279
|
+
}
|
|
1280
|
+
return text.slice(0, limit);
|
|
1281
|
+
}
|
|
1282
|
+
function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
|
|
1283
|
+
if (value.length <= limit) return value;
|
|
1284
|
+
return truncateToLimit2(value, limit);
|
|
1285
|
+
}
|
|
1286
|
+
function boundedClone2(value, budget, depth) {
|
|
1287
|
+
if (budget.remaining <= 0) return TRUNCATION_MARKER2;
|
|
1288
|
+
if (typeof value === "string") {
|
|
1289
|
+
if (value.length > budget.remaining) {
|
|
1290
|
+
const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
|
|
1291
|
+
budget.remaining = 0;
|
|
1292
|
+
return taken;
|
|
1293
|
+
}
|
|
1294
|
+
budget.remaining -= Math.max(value.length, 1);
|
|
1295
|
+
return value;
|
|
1296
|
+
}
|
|
1297
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
1298
|
+
budget.remaining -= 8;
|
|
1299
|
+
return value;
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof value !== "object") {
|
|
1302
|
+
budget.remaining -= 8;
|
|
1303
|
+
return value;
|
|
1304
|
+
}
|
|
1305
|
+
if (depth >= MAX_BOUNDED_DEPTH) {
|
|
1306
|
+
budget.remaining -= 16;
|
|
1307
|
+
return `<max depth: ${TRUNCATION_MARKER2}>`;
|
|
1308
|
+
}
|
|
1309
|
+
const withToJson = value;
|
|
1310
|
+
if (typeof withToJson.toJSON === "function") {
|
|
1311
|
+
try {
|
|
1312
|
+
return boundedClone2(withToJson.toJSON(), budget, depth + 1);
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (Array.isArray(value)) {
|
|
1317
|
+
const out2 = [];
|
|
1318
|
+
for (const item of value) {
|
|
1319
|
+
if (budget.remaining <= 0) {
|
|
1320
|
+
out2.push(TRUNCATION_MARKER2);
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
out2.push(boundedClone2(item, budget, depth + 1));
|
|
1324
|
+
}
|
|
1325
|
+
return out2;
|
|
1326
|
+
}
|
|
1327
|
+
const out = {};
|
|
1328
|
+
for (const key of Object.keys(value)) {
|
|
1329
|
+
if (budget.remaining <= 0) {
|
|
1330
|
+
out["..."] = TRUNCATION_MARKER2;
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
budget.remaining -= Math.max(key.length, 1);
|
|
1334
|
+
out[key] = boundedClone2(value[key], budget, depth + 1);
|
|
1335
|
+
}
|
|
1336
|
+
return out;
|
|
1337
|
+
}
|
|
1338
|
+
function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
|
|
1339
|
+
var _a;
|
|
1340
|
+
try {
|
|
1341
|
+
if (typeof value === "string") {
|
|
1342
|
+
return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
|
|
1343
|
+
}
|
|
1344
|
+
const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
|
|
1345
|
+
return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
return capText2(String(value), limit);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
827
1351
|
// src/tracing-linkage-helpers.ts
|
|
828
1352
|
function createSessionParentMapHelpers({
|
|
829
1353
|
sessions: sessions2,
|
|
@@ -901,6 +1425,15 @@ function createSessionParentMapHelpers({
|
|
|
901
1425
|
}
|
|
902
1426
|
|
|
903
1427
|
// src/tracing.ts
|
|
1428
|
+
var ERROR_LOG_INTERVAL_MS = 3e4;
|
|
1429
|
+
var lastErrorLogAt = /* @__PURE__ */ new Map();
|
|
1430
|
+
function rateLimitedErrorLog(key, message) {
|
|
1431
|
+
const now = Date.now();
|
|
1432
|
+
const last = lastErrorLogAt.get(key);
|
|
1433
|
+
if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
|
|
1434
|
+
lastErrorLogAt.set(key, now);
|
|
1435
|
+
console.log(message);
|
|
1436
|
+
}
|
|
904
1437
|
var sessions = /* @__PURE__ */ new Map();
|
|
905
1438
|
var taskContexts = /* @__PURE__ */ new Map();
|
|
906
1439
|
var runningTaskCallsBySession = /* @__PURE__ */ new Map();
|
|
@@ -959,6 +1492,19 @@ function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMes
|
|
|
959
1492
|
}
|
|
960
1493
|
return true;
|
|
961
1494
|
}
|
|
1495
|
+
function unwrapQuotes(s) {
|
|
1496
|
+
if (s.length < 2) return s;
|
|
1497
|
+
const first = s[0];
|
|
1498
|
+
const last = s[s.length - 1];
|
|
1499
|
+
if ((first === '"' || first === "'") && first === last) return s.slice(1, -1);
|
|
1500
|
+
return s;
|
|
1501
|
+
}
|
|
1502
|
+
function buildPromptMessages(systemPrompt, userInput) {
|
|
1503
|
+
const messages = [];
|
|
1504
|
+
if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
|
|
1505
|
+
messages.push({ role: "user", content: unwrapQuotes(userInput) });
|
|
1506
|
+
return JSON.stringify(messages);
|
|
1507
|
+
}
|
|
962
1508
|
function getHostname() {
|
|
963
1509
|
var _a;
|
|
964
1510
|
try {
|
|
@@ -1040,7 +1586,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
|
|
|
1040
1586
|
const state = sessions.get(partSessionID);
|
|
1041
1587
|
if (!state) return;
|
|
1042
1588
|
if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
|
|
1043
|
-
state.outputParts.set(messageId, partObj["text"]);
|
|
1589
|
+
state.outputParts.set(messageId, capText2(partObj["text"]));
|
|
1044
1590
|
} else if (partObj["type"] === "tool" && messageId) {
|
|
1045
1591
|
const tool = partObj["tool"];
|
|
1046
1592
|
const callID = partObj["callID"];
|
|
@@ -1070,7 +1616,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
|
|
|
1070
1616
|
if (typeof errorMessage !== "string" || errorMessage === "")
|
|
1071
1617
|
throw new Error("message.part.updated: error tool state must have error string");
|
|
1072
1618
|
const input = toolState["input"];
|
|
1073
|
-
const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? input :
|
|
1619
|
+
const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
|
|
1074
1620
|
if (endPendingToolSpanWithError(
|
|
1075
1621
|
state,
|
|
1076
1622
|
callID,
|
|
@@ -1083,7 +1629,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
|
|
|
1083
1629
|
}
|
|
1084
1630
|
}
|
|
1085
1631
|
} else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
|
|
1086
|
-
state.reasoningParts.set(messageId, partObj["text"]);
|
|
1632
|
+
state.reasoningParts.set(messageId, capText2(partObj["text"]));
|
|
1087
1633
|
}
|
|
1088
1634
|
} else if (event.type === "message.updated") {
|
|
1089
1635
|
const msgInfo = props["info"];
|
|
@@ -1184,9 +1730,14 @@ type: ${name != null ? name : "UnknownError"}`;
|
|
|
1184
1730
|
const rootSpan = state.currentRootSpan;
|
|
1185
1731
|
const isCompaction = state.isCompacting === true;
|
|
1186
1732
|
if (state.parentId) {
|
|
1733
|
+
const userInput = unwrapQuotes(state.currentInput);
|
|
1187
1734
|
const llmAttrs = [
|
|
1188
1735
|
attrString("ai.operationId", "generateText"),
|
|
1189
|
-
attrString("ai.prompt",
|
|
1736
|
+
attrString("ai.prompt", userInput),
|
|
1737
|
+
attrString(
|
|
1738
|
+
"ai.prompt.messages",
|
|
1739
|
+
buildPromptMessages(state.currentSystemPrompt, state.currentInput)
|
|
1740
|
+
),
|
|
1190
1741
|
attrString("ai.response.text", outputText),
|
|
1191
1742
|
attrString("gen_ai.system", providerID),
|
|
1192
1743
|
attrString("gen_ai.request.model", modelID),
|
|
@@ -1231,9 +1782,14 @@ type: ${name != null ? name : "UnknownError"}`;
|
|
|
1231
1782
|
});
|
|
1232
1783
|
} else {
|
|
1233
1784
|
rootSpan.name = isCompaction ? "ai.compaction" : modelName;
|
|
1785
|
+
const userInput = unwrapQuotes(state.currentInput);
|
|
1234
1786
|
const rootAttrs = [
|
|
1235
1787
|
attrString("ai.operationId", "generateText"),
|
|
1236
|
-
attrString("ai.prompt",
|
|
1788
|
+
attrString("ai.prompt", userInput),
|
|
1789
|
+
attrString(
|
|
1790
|
+
"ai.prompt.messages",
|
|
1791
|
+
buildPromptMessages(state.currentSystemPrompt, state.currentInput)
|
|
1792
|
+
),
|
|
1237
1793
|
attrString("ai.response.text", outputText),
|
|
1238
1794
|
attrString("gen_ai.system", providerID),
|
|
1239
1795
|
attrString("gen_ai.request.model", modelID),
|
|
@@ -1340,7 +1896,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1340
1896
|
sessions.delete(String(errorSessionID));
|
|
1341
1897
|
}
|
|
1342
1898
|
} catch (err) {
|
|
1343
|
-
|
|
1899
|
+
rateLimitedErrorLog(
|
|
1900
|
+
"event",
|
|
1344
1901
|
`[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1345
1902
|
);
|
|
1346
1903
|
}
|
|
@@ -1396,7 +1953,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1396
1953
|
...promptMeta,
|
|
1397
1954
|
properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
|
|
1398
1955
|
};
|
|
1399
|
-
const userText = textParts.join("\n");
|
|
1956
|
+
const userText = capText2(textParts.join("\n"));
|
|
1400
1957
|
state.currentInput = userText;
|
|
1401
1958
|
if (state.parentId) {
|
|
1402
1959
|
if (!state.parentTaskSpanIds || !state.parentEventContext) {
|
|
@@ -1505,7 +2062,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1505
2062
|
});
|
|
1506
2063
|
}
|
|
1507
2064
|
} catch (err) {
|
|
1508
|
-
|
|
2065
|
+
rateLimitedErrorLog(
|
|
2066
|
+
"tool.execute.before",
|
|
1509
2067
|
`[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1510
2068
|
);
|
|
1511
2069
|
}
|
|
@@ -1520,7 +2078,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1520
2078
|
if (!state || !state.currentEventId) return;
|
|
1521
2079
|
if (args === void 0 || args === null)
|
|
1522
2080
|
throw new Error("tool.execute.after: args is required");
|
|
1523
|
-
const toolCallArgs = typeof args === "string" ? args :
|
|
2081
|
+
const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
|
|
1524
2082
|
const startInfo = state.toolSpanStarts.get(callID);
|
|
1525
2083
|
state.toolSpanStarts.delete(callID);
|
|
1526
2084
|
const resultMetadata = result.metadata;
|
|
@@ -1533,13 +2091,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1533
2091
|
const endTimeUnixNano = nowUnixNanoString();
|
|
1534
2092
|
if (!("output" in result) || result.output === void 0)
|
|
1535
2093
|
throw new Error("tool.execute.after: result.output is required");
|
|
1536
|
-
|
|
1537
|
-
try {
|
|
1538
|
-
toolResult = JSON.stringify(result.output);
|
|
1539
|
-
} catch (err) {
|
|
1540
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1541
|
-
throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
|
|
1542
|
-
}
|
|
2094
|
+
const toolResult = boundedStringify(result.output);
|
|
1543
2095
|
if (startInfo.liveSpan) {
|
|
1544
2096
|
traceShipper.endSpan(startInfo.liveSpan, {
|
|
1545
2097
|
attributes: [
|
|
@@ -1568,7 +2120,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1568
2120
|
markTaskCallFinished(sessionID, callID);
|
|
1569
2121
|
}
|
|
1570
2122
|
} catch (err) {
|
|
1571
|
-
|
|
2123
|
+
rateLimitedErrorLog(
|
|
2124
|
+
"tool.execute.after",
|
|
1572
2125
|
`[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1573
2126
|
);
|
|
1574
2127
|
}
|
|
@@ -1585,7 +2138,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1585
2138
|
state.isCompacting = true;
|
|
1586
2139
|
}
|
|
1587
2140
|
} catch (err) {
|
|
1588
|
-
|
|
2141
|
+
rateLimitedErrorLog(
|
|
2142
|
+
"experimental.session.compacting",
|
|
1589
2143
|
`[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1590
2144
|
);
|
|
1591
2145
|
}
|
|
@@ -1604,11 +2158,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
|
1604
2158
|
sessions.set(sessionID, state);
|
|
1605
2159
|
}
|
|
1606
2160
|
const MAX_SYSTEM_PROMPT_LENGTH = 32768;
|
|
1607
|
-
const
|
|
2161
|
+
const TRUNCATION_MARKER3 = "\n...[truncated]";
|
|
1608
2162
|
const joined = output.system.join("\n\n");
|
|
1609
|
-
state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH -
|
|
2163
|
+
state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
|
|
1610
2164
|
} catch (err) {
|
|
1611
|
-
|
|
2165
|
+
rateLimitedErrorLog(
|
|
2166
|
+
"experimental.chat.system.transform",
|
|
1612
2167
|
`[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1613
2168
|
);
|
|
1614
2169
|
}
|
|
@@ -1624,25 +2179,33 @@ async function plugin(input) {
|
|
|
1624
2179
|
console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
|
|
1625
2180
|
}
|
|
1626
2181
|
appLog("info", `Loading ${PLUGIN_NAME} v${PLUGIN_VERSION}`);
|
|
1627
|
-
|
|
2182
|
+
const resolvedLocalUrl = resolveLocalDebuggerBaseUrl(config.localWorkshopUrl);
|
|
2183
|
+
const hasLocalDestination = resolvedLocalUrl !== null;
|
|
2184
|
+
if (!config.writeKey && !hasLocalDestination) {
|
|
1628
2185
|
appLog(
|
|
1629
2186
|
"warn",
|
|
1630
|
-
"RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set
|
|
2187
|
+
"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."
|
|
1631
2188
|
);
|
|
1632
2189
|
return {};
|
|
1633
2190
|
}
|
|
1634
2191
|
if (config.debug) {
|
|
1635
|
-
|
|
2192
|
+
const destinations = [
|
|
2193
|
+
config.writeKey ? `cloud (${config.endpoint})` : null,
|
|
2194
|
+
resolvedLocalUrl ? `local Workshop (${resolvedLocalUrl})` : null
|
|
2195
|
+
].filter(Boolean);
|
|
2196
|
+
appLog("info", `Raindrop tracing enabled \u2014 destinations: ${destinations.join(", ")}`);
|
|
1636
2197
|
}
|
|
1637
2198
|
const eventShipper = new EventShipper2({
|
|
1638
2199
|
writeKey: config.writeKey,
|
|
1639
2200
|
endpoint: config.endpoint,
|
|
1640
|
-
debug: config.debug
|
|
2201
|
+
debug: config.debug,
|
|
2202
|
+
localDebuggerUrl: config.localWorkshopUrl
|
|
1641
2203
|
});
|
|
1642
2204
|
const traceShipper = new TraceShipper2({
|
|
1643
2205
|
writeKey: config.writeKey,
|
|
1644
2206
|
endpoint: config.endpoint,
|
|
1645
|
-
debug: config.debug
|
|
2207
|
+
debug: config.debug,
|
|
2208
|
+
localDebuggerUrl: config.localWorkshopUrl
|
|
1646
2209
|
});
|
|
1647
2210
|
const worktree = (_a = input.worktree) != null ? _a : input.directory;
|
|
1648
2211
|
const hooks = createHooks(
|