@raindrop-ai/opencode-plugin 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  default: () => plugin
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
+ var import_plugin = require("@opencode-ai/plugin");
26
27
 
27
28
  // ../core/dist/chunk-BYBMRLZM.js
28
29
  var EVAL_CORRELATION_ID_ATTRIBUTE = "raindrop.eval_correlation_id";
@@ -86,7 +87,7 @@ function evalScopeAttributes() {
86
87
  return { [EVAL_CORRELATION_ID_ATTRIBUTE]: scope.correlationId };
87
88
  }
88
89
 
89
- // ../core/dist/chunk-WJOR6M7H.js
90
+ // ../core/dist/chunk-I6TVJQEC.js
90
91
  var STATE_KEY = /* @__PURE__ */ Symbol.for("raindrop.tracing.replayTraceRoutingState");
91
92
  function state() {
92
93
  const holder = globalThis;
@@ -2095,6 +2096,151 @@ var TraceShipper = class {
2095
2096
  }
2096
2097
  }
2097
2098
  };
2099
+ var PROMPT_TOOLS_ATTRIBUTE = "ai.prompt.tools";
2100
+ function isRecord(value) {
2101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2102
+ }
2103
+ var JSON_SCHEMA_KEYS = [
2104
+ "type",
2105
+ "properties",
2106
+ "$schema",
2107
+ "$ref",
2108
+ "anyOf",
2109
+ "oneOf",
2110
+ "allOf",
2111
+ "enum",
2112
+ "const",
2113
+ "items",
2114
+ "additionalProperties",
2115
+ "required"
2116
+ ];
2117
+ function isJsonSchema(value) {
2118
+ if (!isRecord(value)) return false;
2119
+ if (typeof value["~standard"] === "object") return false;
2120
+ if (isRecord(value._def) || isRecord(value._zod)) return false;
2121
+ if (Object.keys(value).length === 0) return true;
2122
+ return JSON_SCHEMA_KEYS.some((key) => key in value);
2123
+ }
2124
+ function toJsonSchema(value, options) {
2125
+ if (value === void 0 || value === null) return void 0;
2126
+ if (isJsonSchema(value)) return value;
2127
+ if (isRecord(value) && isJsonSchema(value.jsonSchema)) return value.jsonSchema;
2128
+ if (isRecord(value) && isJsonSchema(value.json)) return value.json;
2129
+ if (options == null ? void 0 : options.schema) {
2130
+ try {
2131
+ return options.schema(value);
2132
+ } catch (e) {
2133
+ return void 0;
2134
+ }
2135
+ }
2136
+ return void 0;
2137
+ }
2138
+ function pickString(value) {
2139
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2140
+ }
2141
+ function functionTool(name, description, schema, options) {
2142
+ const tool2 = { type: "function", name };
2143
+ const desc = pickString(description);
2144
+ if (desc !== void 0) tool2.description = desc;
2145
+ const inputSchema = toJsonSchema(schema, options);
2146
+ if (inputSchema !== void 0) tool2.inputSchema = inputSchema;
2147
+ return tool2;
2148
+ }
2149
+ function normalizeToolDeclaration(tool2, options, fallbackName) {
2150
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
2151
+ if (!isRecord(tool2)) return void 0;
2152
+ if (tool2.type === "provider-defined" || tool2.type === "provider") {
2153
+ const name2 = (_b = (_a = pickString(tool2.name)) != null ? _a : pickString(tool2.id)) != null ? _b : fallbackName;
2154
+ if (name2 === void 0) return void 0;
2155
+ const declaration = { type: "provider-defined", name: name2 };
2156
+ const id = pickString(tool2.id);
2157
+ if (id !== void 0) declaration.id = id;
2158
+ if (isRecord(tool2.args)) declaration.args = tool2.args;
2159
+ return declaration;
2160
+ }
2161
+ if (isRecord(tool2.toolSpec)) {
2162
+ const spec = tool2.toolSpec;
2163
+ const name2 = (_c = pickString(spec.name)) != null ? _c : fallbackName;
2164
+ if (name2 === void 0) return void 0;
2165
+ return functionTool(name2, spec.description, spec.inputSchema, options);
2166
+ }
2167
+ if (isRecord(tool2.function)) {
2168
+ const fn = tool2.function;
2169
+ const name2 = (_d = pickString(fn.name)) != null ? _d : fallbackName;
2170
+ if (name2 === void 0) return void 0;
2171
+ return functionTool(name2, fn.description, (_e = fn.parameters) != null ? _e : fn.input_schema, options);
2172
+ }
2173
+ const name = (_g = (_f = pickString(tool2.name)) != null ? _f : pickString(tool2.id)) != null ? _g : fallbackName;
2174
+ if (name === void 0) return void 0;
2175
+ const type = pickString(tool2.type);
2176
+ if (type !== void 0 && type !== "function" && type !== "dynamic" && type !== "custom") {
2177
+ if (!("input_schema" in tool2) && !("inputSchema" in tool2) && !("parameters" in tool2)) {
2178
+ return { type: "provider-defined", id: type, name };
2179
+ }
2180
+ }
2181
+ return functionTool(
2182
+ name,
2183
+ tool2.description,
2184
+ (_j = (_i = (_h = tool2.inputSchema) != null ? _h : tool2.input_schema) != null ? _i : tool2.parameters) != null ? _j : tool2.schema,
2185
+ options
2186
+ );
2187
+ }
2188
+ var NOT_A_GEMINI_GROUP_KEYS = ["name", "id", "type", "function", "toolSpec", "cachePoint"];
2189
+ function isGeminiToolGroup(tool2) {
2190
+ if (Array.isArray(tool2.functionDeclarations)) return true;
2191
+ const keys = Object.keys(tool2);
2192
+ if (keys.length === 0 || NOT_A_GEMINI_GROUP_KEYS.some((key) => key in tool2)) return false;
2193
+ return keys.every((key) => isRecord(tool2[key]));
2194
+ }
2195
+ function expandGeminiTool(tool2, options) {
2196
+ const declarations = [];
2197
+ for (const [key, value] of Object.entries(tool2)) {
2198
+ if (key === "functionDeclarations" && Array.isArray(value)) {
2199
+ for (const declaration of value) {
2200
+ const normalized = normalizeToolDeclaration(declaration, options);
2201
+ if (normalized) declarations.push(normalized);
2202
+ }
2203
+ } else if (isRecord(value)) {
2204
+ declarations.push({ type: "provider-defined", id: key, name: key, args: value });
2205
+ }
2206
+ }
2207
+ return declarations;
2208
+ }
2209
+ function normalizeToolDeclarations(tools, options) {
2210
+ if (tools === void 0 || tools === null) return void 0;
2211
+ if (Array.isArray(tools)) {
2212
+ return tools.flatMap((tool2) => {
2213
+ if (isRecord(tool2) && isGeminiToolGroup(tool2)) {
2214
+ return expandGeminiTool(tool2, options);
2215
+ }
2216
+ const declaration = normalizeToolDeclaration(tool2, options);
2217
+ return declaration ? [declaration] : [];
2218
+ });
2219
+ }
2220
+ if (!isRecord(tools)) return void 0;
2221
+ return Object.entries(tools).flatMap(([key, tool2]) => {
2222
+ const declaration = normalizeToolDeclaration(tool2, options, key);
2223
+ return declaration ? [declaration] : [];
2224
+ });
2225
+ }
2226
+ function promptToolsAttribute(tools) {
2227
+ const values = promptToolsAttributeValue(tools);
2228
+ if (values === void 0) return void 0;
2229
+ return {
2230
+ key: PROMPT_TOOLS_ATTRIBUTE,
2231
+ value: { arrayValue: { values: values.map((stringValue) => ({ stringValue })) } }
2232
+ };
2233
+ }
2234
+ function promptToolsAttributeValue(tools) {
2235
+ if (tools === void 0) return void 0;
2236
+ return tools.map((tool2) => {
2237
+ try {
2238
+ return JSON.stringify(tool2);
2239
+ } catch (e) {
2240
+ return JSON.stringify({ type: tool2.type, name: tool2.name });
2241
+ }
2242
+ });
2243
+ }
2098
2244
 
2099
2245
  // ../core/dist/index.node.js
2100
2246
  var import_async_hooks = require("async_hooks");
@@ -2308,8 +2454,41 @@ function loadConfig(projectDirectory) {
2308
2454
  captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_h = merged.capture_system_prompt) != null ? _h : false,
2309
2455
  eventMetadata,
2310
2456
  localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url),
2311
- appGit: mapAppGit(merged.app_git)
2457
+ appGit: mapAppGit(merged.app_git),
2458
+ selfDiagnostics: mapSelfDiagnostics(merged.self_diagnostics),
2459
+ tools: Array.isArray(merged.tools) ? merged.tools : void 0
2460
+ };
2461
+ }
2462
+ function isRecord2(value) {
2463
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2464
+ }
2465
+ function mapSelfDiagnostics(value) {
2466
+ const env = process.env["RAINDROP_SELF_DIAGNOSTICS"];
2467
+ const record = isRecord2(value) ? value : {};
2468
+ const enabled = env !== void 0 ? env === "true" : typeof record["enabled"] === "boolean" ? record["enabled"] : void 0;
2469
+ const signals = mapSelfDiagnosticsSignals(record["signals"]);
2470
+ const options = {
2471
+ ...enabled !== void 0 ? { enabled } : {},
2472
+ ...signals ? { signals } : {},
2473
+ ...typeof record["guidance"] === "string" ? { guidance: record["guidance"] } : {},
2474
+ ...typeof record["tool_name"] === "string" ? { toolName: record["tool_name"] } : {}
2312
2475
  };
2476
+ return Object.keys(options).length > 0 ? options : void 0;
2477
+ }
2478
+ function mapSelfDiagnosticsSignals(value) {
2479
+ if (!isRecord2(value)) return void 0;
2480
+ const out = {};
2481
+ for (const [key, entry] of Object.entries(value)) {
2482
+ if (!isRecord2(entry)) continue;
2483
+ const definition = entry;
2484
+ if (typeof definition["description"] !== "string") continue;
2485
+ const sentiment = definition["sentiment"];
2486
+ out[key] = {
2487
+ description: definition["description"],
2488
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2489
+ };
2490
+ }
2491
+ return Object.keys(out).length > 0 ? out : void 0;
2313
2492
  }
2314
2493
  function mapAppGit(value) {
2315
2494
  if (value === false) return false;
@@ -2337,7 +2516,7 @@ function resolveLocalWorkshopUrl(fileValue) {
2337
2516
  // package.json
2338
2517
  var package_default = {
2339
2518
  name: "@raindrop-ai/opencode-plugin",
2340
- version: "0.2.0",
2519
+ version: "0.3.1",
2341
2520
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
2342
2521
  type: "module",
2343
2522
  main: "dist/index.js",
@@ -2426,6 +2605,167 @@ var package_default = {
2426
2605
  var PLUGIN_NAME = package_default.name;
2427
2606
  var PLUGIN_VERSION = package_default.version;
2428
2607
 
2608
+ // src/self-diagnostics.ts
2609
+ var SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT = "__raindrop_report";
2610
+ var SELF_DIAGNOSTICS_SIGNALS_DEFAULT = {
2611
+ missing_context: {
2612
+ description: "You cannot complete the task because critical information, credentials, or access is missing and the user cannot provide it. Do NOT report this for normal clarifying questions \u2014 only when you are blocked.",
2613
+ sentiment: "NEGATIVE"
2614
+ },
2615
+ repeatedly_broken_tool: {
2616
+ description: "A tool has failed or not returned the expected response on multiple distinct attempts in this conversation, preventing task completion. A single tool error is NOT enough \u2014 the tool must be persistently broken or aberrantly behaving across retries.",
2617
+ sentiment: "NEGATIVE"
2618
+ },
2619
+ capability_gap: {
2620
+ description: "The task requires a tool, permission, or capability that you do not have. For example, the user asks you to perform an action but no suitable tool exists, or you lack the necessary access. Do NOT report this if you simply need more information from the user \u2014 only when the gap is in your own capabilities.",
2621
+ sentiment: "NEGATIVE"
2622
+ },
2623
+ complete_task_failure: {
2624
+ description: "You were unable to accomplish what the user asked despite making genuine attempts. This might be things like, you genuinely do not have the capabilities the user is asking for. You have tried but run into a persistent bug in the environment etc. This is NOT a refusal or policy block \u2014 you tried and failed to deliver the result.",
2625
+ sentiment: "NEGATIVE"
2626
+ }
2627
+ };
2628
+ var SELF_DIAGNOSTICS_TOOL_PREAMBLE = "You have a diagnostic tool for flagging serious failures for developer review. Only call this when you hit a significant, unrecoverable problem \u2014 not for minor hiccups or routine clarifications. If you can still help the user, do that instead of calling this tool. This tool is invisible to the user \u2014 never mention it or its output.";
2629
+ var SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY = "noteworthy";
2630
+ function normalizeString(value) {
2631
+ if (typeof value !== "string") return void 0;
2632
+ const trimmed = value.trim();
2633
+ return trimmed || void 0;
2634
+ }
2635
+ function normalizeSelfDiagnosticsSignals(signals) {
2636
+ if (!signals) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2637
+ const normalizedEntries = Object.entries(signals).map(([key, value]) => {
2638
+ var _a;
2639
+ const signalKey = key.trim();
2640
+ if (!signalKey || !value || typeof value !== "object") return void 0;
2641
+ const description = (_a = value.description) == null ? void 0 : _a.trim();
2642
+ if (!description) return void 0;
2643
+ const sentiment = value.sentiment;
2644
+ return [
2645
+ signalKey,
2646
+ {
2647
+ description,
2648
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2649
+ }
2650
+ ];
2651
+ }).filter(
2652
+ (entry) => entry !== void 0
2653
+ );
2654
+ if (normalizedEntries.length === 0) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2655
+ return Object.fromEntries(normalizedEntries);
2656
+ }
2657
+ function normalizeSelfDiagnosticsConfig(options) {
2658
+ var _a, _b;
2659
+ if (!(options == null ? void 0 : options.enabled)) return void 0;
2660
+ const signalDefinitions = normalizeSelfDiagnosticsSignals(options.signals);
2661
+ const signalKeys = Object.keys(signalDefinitions);
2662
+ const signalDescriptions = {};
2663
+ const signalSentiments = {};
2664
+ for (const signalKey of signalKeys) {
2665
+ const definition = signalDefinitions[signalKey];
2666
+ if (!definition) continue;
2667
+ signalDescriptions[signalKey] = definition.description;
2668
+ signalSentiments[signalKey] = definition.sentiment;
2669
+ }
2670
+ const customGuidance = ((_a = options.guidance) == null ? void 0 : _a.trim()) || "";
2671
+ const toolName = ((_b = options.toolName) == null ? void 0 : _b.trim()) || SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT;
2672
+ const signalList = signalKeys.map((signalKey) => {
2673
+ const sentiment = signalSentiments[signalKey];
2674
+ const sentimentTag = sentiment ? ` [${sentiment.toLowerCase()}]` : "";
2675
+ return `- ${signalKey}: ${signalDescriptions[signalKey]}${sentimentTag}`;
2676
+ }).join("\n");
2677
+ const guidanceBlock = customGuidance ? `
2678
+ Additional guidance: ${customGuidance}
2679
+ ` : "";
2680
+ const toolDescription = `${SELF_DIAGNOSTICS_TOOL_PREAMBLE}
2681
+
2682
+ When to call:
2683
+ - You are blocked from completing the task due to missing information or access that the user cannot provide.
2684
+ - A tool is persistently failing across multiple attempts, not just a single transient error.
2685
+ - The task requires a tool, permission, or capability you do not have.
2686
+ - You genuinely cannot deliver what the user asked for despite trying.
2687
+
2688
+ When NOT to call:
2689
+ - Normal clarifying questions or back-and-forth with the user.
2690
+ - A single tool error that you can recover from or retry.
2691
+ - You successfully completed the task, even if it was difficult.
2692
+ - Policy refusals or content filtering \u2014 those are working as intended.
2693
+
2694
+ Rules:
2695
+ 1. Pick the single best category.
2696
+ 2. Do not fabricate issues. Only report what is evident from the conversation.
2697
+ 3. Err on the side of NOT calling this tool. When in doubt, help the user instead.
2698
+ ${guidanceBlock}
2699
+ Categories:
2700
+ ` + signalList;
2701
+ return {
2702
+ toolName,
2703
+ toolDescription,
2704
+ signalKeys,
2705
+ signalKeySet: new Set(signalKeys),
2706
+ signalDescriptions,
2707
+ signalSentiments
2708
+ };
2709
+ }
2710
+ var SELF_DIAGNOSTICS_ACK = '{"acknowledged":true}';
2711
+ function isRecord3(value) {
2712
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2713
+ }
2714
+ async function trackSelfDiagnosticsSignal(toolInput, config, eventShipper, eventId, debug) {
2715
+ var _a, _b;
2716
+ try {
2717
+ const input = isRecord3(toolInput) ? toolInput : void 0;
2718
+ const fallbackCategory = (_a = config.signalKeys[0]) != null ? _a : "unknown";
2719
+ const categoryCandidate = normalizeString(input == null ? void 0 : input["category"]);
2720
+ const category = categoryCandidate && config.signalKeySet.has(categoryCandidate) ? categoryCandidate : fallbackCategory;
2721
+ const detail = (_b = normalizeString(input == null ? void 0 : input["detail"])) != null ? _b : "";
2722
+ if (!eventId) {
2723
+ if (debug) {
2724
+ console.warn(
2725
+ "[raindrop-ai/opencode-plugin] self diagnostics signal skipped: no open event for this session."
2726
+ );
2727
+ }
2728
+ return;
2729
+ }
2730
+ if (category === SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY) {
2731
+ await eventShipper.trackSignal({
2732
+ eventId,
2733
+ name: "self diagnostics - noteworthy",
2734
+ type: "agent_internal",
2735
+ properties: {
2736
+ source: "agent_flag_event_tool",
2737
+ reason: detail,
2738
+ severity: "medium",
2739
+ sdk: "opencode-plugin",
2740
+ sdk_version: PLUGIN_VERSION
2741
+ }
2742
+ });
2743
+ return;
2744
+ }
2745
+ await eventShipper.trackSignal({
2746
+ eventId,
2747
+ name: `self diagnostics - ${category}`,
2748
+ type: "agent",
2749
+ sentiment: config.signalSentiments[category],
2750
+ properties: {
2751
+ source: "agent_reporting_tool",
2752
+ category,
2753
+ signal_description: config.signalDescriptions[category],
2754
+ sdk: "opencode-plugin",
2755
+ sdk_version: PLUGIN_VERSION,
2756
+ ...detail ? { detail } : {}
2757
+ }
2758
+ });
2759
+ } catch (err) {
2760
+ if (debug) {
2761
+ const message = err instanceof Error ? err.message : String(err);
2762
+ console.warn(
2763
+ `[raindrop-ai/opencode-plugin] selfDiagnostics signal dispatch failed: ${message}`
2764
+ );
2765
+ }
2766
+ }
2767
+ }
2768
+
2429
2769
  // src/shipper.ts
2430
2770
  var EventShipper3 = class extends EventShipper2 {
2431
2771
  constructor(opts) {
@@ -2461,6 +2801,100 @@ var TraceShipper3 = class extends TraceShipper2 {
2461
2801
  }
2462
2802
  };
2463
2803
 
2804
+ // src/tool-catalog.ts
2805
+ var PROMPT_TOOLS_SOURCE_ATTRIBUTE = "ai.prompt.tools.source";
2806
+ var OPENCODE_TOOL_LIST_SOURCE = "opencode.tool.list";
2807
+ var TOOL_CATALOG_TIMEOUT_MS = 2e3;
2808
+ function isRecord4(value) {
2809
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2810
+ }
2811
+ function catalogItems(raw) {
2812
+ var _a;
2813
+ const list = Array.isArray(raw) ? raw : isRecord4(raw) && Array.isArray(raw.data) ? raw.data : void 0;
2814
+ if (!list) return void 0;
2815
+ const items = [];
2816
+ for (const item of list) {
2817
+ if (!isRecord4(item)) continue;
2818
+ const name = typeof item.id === "string" ? item.id : typeof item.name === "string" ? item.name : void 0;
2819
+ if (!name) continue;
2820
+ const schema = (_a = item.parameters) != null ? _a : item.input;
2821
+ items.push({
2822
+ type: "function",
2823
+ name,
2824
+ ...typeof item.description === "string" ? { description: item.description } : {},
2825
+ ...isJsonSchema(schema) ? { inputSchema: schema } : {}
2826
+ });
2827
+ }
2828
+ return items;
2829
+ }
2830
+ function withTimeout(promise, ms) {
2831
+ return new Promise((resolve2) => {
2832
+ const timer = setTimeout(() => resolve2(void 0), ms);
2833
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
2834
+ promise.then(
2835
+ (value) => {
2836
+ clearTimeout(timer);
2837
+ resolve2(value);
2838
+ },
2839
+ () => {
2840
+ clearTimeout(timer);
2841
+ resolve2(void 0);
2842
+ }
2843
+ );
2844
+ });
2845
+ }
2846
+ function createToolCatalog(options = {}) {
2847
+ var _a, _b;
2848
+ const override = options.override !== void 0 ? { tools: (_a = normalizeToolDeclarations(options.override)) != null ? _a : [] } : void 0;
2849
+ const timeoutMs = (_b = options.timeoutMs) != null ? _b : TOOL_CATALOG_TIMEOUT_MS;
2850
+ const cache = /* @__PURE__ */ new Map();
2851
+ async function lookup(query) {
2852
+ const fetch2 = options.fetch;
2853
+ if (!fetch2) return void 0;
2854
+ let raw;
2855
+ try {
2856
+ raw = await withTimeout(Promise.resolve().then(() => fetch2(query)), timeoutMs);
2857
+ } catch (error) {
2858
+ if (options.debug) console.log(`[raindrop-ai/opencode-plugin] tool catalog lookup failed: ${String(error)}`);
2859
+ return void 0;
2860
+ }
2861
+ const tools = normalizeToolDeclarations(catalogItems(raw));
2862
+ if (!tools) {
2863
+ if (options.debug) console.log("[raindrop-ai/opencode-plugin] tool catalog lookup returned no tool list");
2864
+ return void 0;
2865
+ }
2866
+ return { tools, source: OPENCODE_TOOL_LIST_SOURCE };
2867
+ }
2868
+ return {
2869
+ resolve(sessionID, query) {
2870
+ if (override) return Promise.resolve(override);
2871
+ if (!options.fetch) return Promise.resolve(void 0);
2872
+ let perSession = cache.get(sessionID);
2873
+ if (!perSession) {
2874
+ perSession = /* @__PURE__ */ new Map();
2875
+ cache.set(sessionID, perSession);
2876
+ }
2877
+ const key = `${query.provider}\0${query.model}`;
2878
+ let pending = perSession.get(key);
2879
+ if (!pending) {
2880
+ pending = lookup(query).catch(() => void 0);
2881
+ perSession.set(key, pending);
2882
+ }
2883
+ return pending;
2884
+ },
2885
+ forget(sessionID) {
2886
+ cache.delete(sessionID);
2887
+ }
2888
+ };
2889
+ }
2890
+ function promptToolsAttributes(resolved) {
2891
+ if (!resolved) return [];
2892
+ return [
2893
+ promptToolsAttribute(resolved.tools),
2894
+ resolved.source !== void 0 ? attrString(PROMPT_TOOLS_SOURCE_ATTRIBUTE, resolved.source) : void 0
2895
+ ];
2896
+ }
2897
+
2464
2898
  // src/tracing.ts
2465
2899
  var import_node_os2 = require("os");
2466
2900
 
@@ -2697,7 +3131,7 @@ function markTaskCallFinished(sessionID, callID) {
2697
3131
  running.delete(callID);
2698
3132
  if (running.size === 0) runningTaskCallsBySession.delete(sessionID);
2699
3133
  }
2700
- function endPendingToolSpanWithError(state2, callID, tool, toolCallArgs, errorMessage, traceShipper) {
3134
+ function endPendingToolSpanWithError(state2, callID, tool2, toolCallArgs, errorMessage, traceShipper) {
2701
3135
  const startInfo = state2.toolSpanStarts.get(callID);
2702
3136
  if (!startInfo) return false;
2703
3137
  state2.toolSpanStarts.delete(callID);
@@ -2719,7 +3153,7 @@ function endPendingToolSpanWithError(state2, callID, tool, toolCallArgs, errorMe
2719
3153
  startTimeUnixNano: startInfo.startTimeUnixNano,
2720
3154
  attributes: [
2721
3155
  attrString("ai.operationId", "ai.toolCall"),
2722
- attrString("ai.toolCall.name", tool),
3156
+ attrString("ai.toolCall.name", tool2),
2723
3157
  attrString("ai.toolCall.id", callID)
2724
3158
  ]
2725
3159
  });
@@ -2756,7 +3190,7 @@ function getHostname() {
2756
3190
  return h;
2757
3191
  }
2758
3192
  }
2759
- function createHooks(config, worktree, directory, eventShipper, traceShipper) {
3193
+ function createTracing(config, worktree, directory, eventShipper, traceShipper, toolCatalog) {
2760
3194
  function log(msg, data) {
2761
3195
  if (!config.debug) return;
2762
3196
  const prefix = `[raindrop-ai/opencode-plugin] [info] ${msg}`;
@@ -2782,7 +3216,15 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
2782
3216
  callKey,
2783
3217
  log
2784
3218
  });
2785
- return {
3219
+ const hooks = {
3220
+ // Fires before each model call with the model already resolved: start the
3221
+ // catalog lookup now so the span attributes rarely wait on it later.
3222
+ "chat.params": async (input) => {
3223
+ void (toolCatalog == null ? void 0 : toolCatalog.resolve(input.sessionID, {
3224
+ provider: input.model.providerID,
3225
+ model: input.model.id
3226
+ }));
3227
+ },
2786
3228
  // ------------------------------------------------------------------
2787
3229
  // event — session lifecycle + streaming parts
2788
3230
  // ------------------------------------------------------------------
@@ -2827,14 +3269,14 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
2827
3269
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
2828
3270
  state2.outputParts.set(messageId, capText2(partObj["text"]));
2829
3271
  } else if (partObj["type"] === "tool" && messageId) {
2830
- const tool = partObj["tool"];
3272
+ const tool2 = partObj["tool"];
2831
3273
  const callID = partObj["callID"];
2832
3274
  const partState = partObj["state"];
2833
3275
  if (callID == null || callID === "")
2834
3276
  throw new Error("message.part.updated: part.callID is required for tool part");
2835
3277
  if (partState == null || typeof partState !== "object")
2836
3278
  throw new Error("message.part.updated: part.state is required for tool part");
2837
- if (tool === "task") {
3279
+ if (tool2 === "task") {
2838
3280
  const metadata = partState["metadata"];
2839
3281
  const childSessionId = metadata != null && typeof metadata === "object" ? metadata["sessionId"] : void 0;
2840
3282
  if (childSessionId != null && childSessionId !== "") {
@@ -2859,11 +3301,11 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
2859
3301
  if (endPendingToolSpanWithError(
2860
3302
  state2,
2861
3303
  callID,
2862
- tool,
3304
+ tool2,
2863
3305
  toolCallArgs,
2864
3306
  errorMessage,
2865
3307
  traceShipper
2866
- ) && tool === "task") {
3308
+ ) && tool2 === "task") {
2867
3309
  markTaskCallFinished(partSessionID, callID);
2868
3310
  }
2869
3311
  }
@@ -2907,6 +3349,9 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
2907
3349
  "message.updated: providerID and modelID are required non-empty strings"
2908
3350
  );
2909
3351
  const modelName = `${providerID}/${modelID}`;
3352
+ const promptTools = promptToolsAttributes(
3353
+ await (toolCatalog == null ? void 0 : toolCatalog.resolve(String(msgSessionID), { provider: providerID, model: modelID }))
3354
+ );
2910
3355
  const finishReason = info2["finish"];
2911
3356
  const outputText = state2.outputParts.get(messageId);
2912
3357
  const reasoningText = state2.reasoningParts.get(messageId);
@@ -2932,7 +3377,8 @@ type: ${name != null ? name : "UnknownError"}`;
2932
3377
  const llmAttrs = [
2933
3378
  attrString("ai.operationId", "generateText"),
2934
3379
  attrString("ai.response.text", responseText),
2935
- ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj })
3380
+ ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj }),
3381
+ ...promptTools
2936
3382
  ];
2937
3383
  if (state2.currentSystemPrompt) {
2938
3384
  llmAttrs.push(
@@ -2968,7 +3414,8 @@ type: ${name != null ? name : "UnknownError"}`;
2968
3414
  buildPromptMessages(state2.currentSystemPrompt, state2.currentInput)
2969
3415
  ),
2970
3416
  attrString("ai.response.text", turnOutputText),
2971
- ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj })
3417
+ ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj }),
3418
+ ...promptTools
2972
3419
  ];
2973
3420
  if (state2.currentSystemPrompt) {
2974
3421
  llmAttrs.push(
@@ -3013,7 +3460,8 @@ type: ${name != null ? name : "UnknownError"}`;
3013
3460
  buildPromptMessages(state2.currentSystemPrompt, state2.currentInput)
3014
3461
  ),
3015
3462
  attrString("ai.response.text", turnOutputText),
3016
- ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj })
3463
+ ...buildLlmGenAiAttributes({ providerID, modelID, tokens: tokensObj }),
3464
+ ...promptTools
3017
3465
  ];
3018
3466
  if (isCompaction) {
3019
3467
  rootAttrs.push(attrString("is_compaction", "true"));
@@ -3079,6 +3527,7 @@ type: ${name != null ? name : "UnknownError"}`;
3079
3527
  if (state2.parentId) {
3080
3528
  cleanupSessionParentMap(String(sessionID));
3081
3529
  sessions.delete(String(sessionID));
3530
+ toolCatalog == null ? void 0 : toolCatalog.forget(String(sessionID));
3082
3531
  } else {
3083
3532
  }
3084
3533
  } else if (event.type === "session.deleted") {
@@ -3095,6 +3544,7 @@ type: ${name != null ? name : "UnknownError"}`;
3095
3544
  cleanupSessionParentMap(String(sessionID));
3096
3545
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
3097
3546
  sessions.delete(String(sessionID));
3547
+ toolCatalog == null ? void 0 : toolCatalog.forget(String(sessionID));
3098
3548
  } else if (event.type === "session.error") {
3099
3549
  const errorSessionID = (_h = props["sessionID"]) != null ? _h : sessionID;
3100
3550
  if (errorSessionID === void 0 || errorSessionID === "")
@@ -3120,6 +3570,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3120
3570
  cleanupSessionParentMap(String(errorSessionID));
3121
3571
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
3122
3572
  sessions.delete(String(errorSessionID));
3573
+ toolCatalog == null ? void 0 : toolCatalog.forget(String(errorSessionID));
3123
3574
  }
3124
3575
  } catch (err) {
3125
3576
  rateLimitedErrorLog(
@@ -3240,20 +3691,20 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3240
3691
  "tool.execute.before": async (toolInput, _output) => {
3241
3692
  var _a, _b, _c;
3242
3693
  try {
3243
- const { tool, sessionID, callID } = toolInput;
3694
+ const { tool: tool2, sessionID, callID } = toolInput;
3244
3695
  const state2 = sessions.get(sessionID);
3245
3696
  if (!state2 || !state2.currentEventId) return;
3246
3697
  const spanParent = state2.parentId && state2.parentTaskSpanIds ? state2.parentTaskSpanIds : (_a = state2.currentRootSpan) == null ? void 0 : _a.ids;
3247
3698
  if (!spanParent) return;
3248
3699
  const startTimeUnixNano = nowUnixNanoString();
3249
- if (tool === "task") {
3700
+ if (tool2 === "task") {
3250
3701
  const liveTaskSpan = traceShipper.startSpan({
3251
3702
  name: "ai.toolCall",
3252
3703
  parent: spanParent,
3253
3704
  eventId: state2.currentEventId,
3254
3705
  attributes: [
3255
3706
  attrString("ai.operationId", "ai.toolCall"),
3256
- attrString("ai.toolCall.name", tool),
3707
+ attrString("ai.toolCall.name", tool2),
3257
3708
  attrString("ai.toolCall.id", callID)
3258
3709
  ]
3259
3710
  });
@@ -3278,7 +3729,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3278
3729
  startTimeUnixNano,
3279
3730
  parent: spanParent,
3280
3731
  eventId: state2.currentEventId,
3281
- name: tool,
3732
+ name: tool2,
3282
3733
  liveSpan: liveTaskSpan
3283
3734
  });
3284
3735
  } else {
@@ -3286,7 +3737,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3286
3737
  startTimeUnixNano,
3287
3738
  parent: spanParent,
3288
3739
  eventId: state2.currentEventId,
3289
- name: tool
3740
+ name: tool2
3290
3741
  });
3291
3742
  }
3292
3743
  } catch (err) {
@@ -3301,7 +3752,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3301
3752
  // ------------------------------------------------------------------
3302
3753
  "tool.execute.after": async (toolInput, result) => {
3303
3754
  try {
3304
- const { tool, sessionID, callID, args } = toolInput;
3755
+ const { tool: tool2, sessionID, callID, args } = toolInput;
3305
3756
  const state2 = sessions.get(sessionID);
3306
3757
  if (!state2 || !state2.currentEventId) return;
3307
3758
  if (args === void 0 || args === null)
@@ -3311,7 +3762,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3311
3762
  state2.toolSpanStarts.delete(callID);
3312
3763
  const resultMetadata = result.metadata;
3313
3764
  const childSessionId = resultMetadata == null ? void 0 : resultMetadata["sessionId"];
3314
- if (tool === "task" && childSessionId) {
3765
+ if (tool2 === "task" && childSessionId) {
3315
3766
  mapChildSessionToParent.set(childSessionId, { parentId: sessionID });
3316
3767
  attachChildSessionToParentTask(childSessionId, sessionID, callID, "tool.execute.after");
3317
3768
  }
@@ -3336,7 +3787,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3336
3787
  endTimeUnixNano,
3337
3788
  attributes: [
3338
3789
  attrString("ai.operationId", "ai.toolCall"),
3339
- attrString("ai.toolCall.name", tool),
3790
+ attrString("ai.toolCall.name", tool2),
3340
3791
  attrString("ai.toolCall.id", callID),
3341
3792
  attrString("ai.toolCall.args", toolCallArgs),
3342
3793
  attrString("ai.toolCall.result", toolResult)
@@ -3344,7 +3795,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3344
3795
  });
3345
3796
  }
3346
3797
  }
3347
- if (tool === "task") {
3798
+ if (tool2 === "task") {
3348
3799
  markTaskCallFinished(sessionID, callID);
3349
3800
  }
3350
3801
  } catch (err) {
@@ -3397,16 +3848,24 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
3397
3848
  }
3398
3849
  }
3399
3850
  };
3851
+ return {
3852
+ hooks,
3853
+ currentEventId(sessionID) {
3854
+ var _a;
3855
+ return (_a = sessions.get(sessionID)) == null ? void 0 : _a.currentEventId;
3856
+ }
3857
+ };
3400
3858
  }
3401
3859
 
3402
3860
  // src/index.ts
3403
- function createRaindropHandle(eventShipper, traceShipper, appGitContext) {
3861
+ function createRaindropHandle(eventShipper, traceShipper, appGitContext, currentEventId) {
3404
3862
  return {
3405
3863
  users: {
3406
3864
  async identify(users) {
3407
3865
  await eventShipper.identify(users);
3408
3866
  }
3409
3867
  },
3868
+ currentEventId,
3410
3869
  async flush() {
3411
3870
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
3412
3871
  },
@@ -3423,7 +3882,7 @@ function createRaindropHandle(eventShipper, traceShipper, appGitContext) {
3423
3882
  };
3424
3883
  }
3425
3884
  async function plugin(input, opts = {}) {
3426
- var _a, _b, _c;
3885
+ var _a, _b, _c, _d, _e;
3427
3886
  const config = loadConfig(input.directory);
3428
3887
  function appLog(level, message) {
3429
3888
  console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
@@ -3467,12 +3926,59 @@ async function plugin(input, opts = {}) {
3467
3926
  appGitContext
3468
3927
  });
3469
3928
  const worktree = (_c = input.worktree) != null ? _c : input.directory;
3470
- const hooks = createHooks(
3929
+ const toolCatalog = createToolCatalog({
3930
+ override: (_d = opts.tools) != null ? _d : config.tools,
3931
+ fetch: (query) => input.client.tool.list({ query }),
3932
+ debug: config.debug
3933
+ });
3934
+ const tracing = createTracing(
3471
3935
  config,
3472
3936
  worktree,
3473
3937
  input.directory,
3474
3938
  eventShipper,
3475
- traceShipper
3939
+ traceShipper,
3940
+ toolCatalog
3941
+ );
3942
+ const hooks = tracing.hooks;
3943
+ const selfDiagnostics = normalizeSelfDiagnosticsConfig(
3944
+ (_e = opts.selfDiagnostics) != null ? _e : config.selfDiagnostics
3476
3945
  );
3477
- return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper, appGitContext) };
3946
+ if (selfDiagnostics && !config.writeKey.trim()) {
3947
+ appLog(
3948
+ "warn",
3949
+ `selfDiagnostics enabled but RAINDROP_WRITE_KEY is not set \u2014 the ${selfDiagnostics.toolName} tool is not registered (signals require the cloud destination).`
3950
+ );
3951
+ }
3952
+ const [firstSignalKey, ...restSignalKeys] = config.writeKey.trim() && selfDiagnostics ? selfDiagnostics.signalKeys : [];
3953
+ if (selfDiagnostics && firstSignalKey !== void 0) {
3954
+ const cfg = selfDiagnostics;
3955
+ hooks.tool = {
3956
+ [cfg.toolName]: (0, import_plugin.tool)({
3957
+ description: cfg.toolDescription,
3958
+ args: {
3959
+ category: import_plugin.tool.schema.enum([firstSignalKey, ...restSignalKeys]),
3960
+ detail: import_plugin.tool.schema.string()
3961
+ },
3962
+ async execute(args, ctx) {
3963
+ await trackSelfDiagnosticsSignal(
3964
+ args,
3965
+ cfg,
3966
+ eventShipper,
3967
+ tracing.currentEventId(ctx.sessionID),
3968
+ config.debug
3969
+ );
3970
+ return SELF_DIAGNOSTICS_ACK;
3971
+ }
3972
+ })
3973
+ };
3974
+ }
3975
+ return {
3976
+ ...hooks,
3977
+ raindrop: createRaindropHandle(
3978
+ eventShipper,
3979
+ traceShipper,
3980
+ appGitContext,
3981
+ tracing.currentEventId
3982
+ )
3983
+ };
3478
3984
  }