@raindrop-ai/opencode-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  EventShipper,
3
3
  PLUGIN_NAME,
4
4
  PLUGIN_VERSION,
5
+ SELF_DIAGNOSTICS_ACK,
5
6
  TraceShipper,
6
7
  attrString,
7
8
  boundedStringify,
@@ -10,9 +11,14 @@ import {
10
11
  createAppGitContext,
11
12
  generateId,
12
13
  loadConfig,
14
+ normalizeSelfDiagnosticsConfig,
13
15
  nowUnixNanoString,
14
- resolveLocalDebuggerBaseUrl
15
- } from "./chunk-4DAI6CYK.js";
16
+ resolveLocalDebuggerBaseUrl,
17
+ trackSelfDiagnosticsSignal
18
+ } from "./chunk-FCORH4YS.js";
19
+
20
+ // src/index.ts
21
+ import { tool } from "@opencode-ai/plugin";
16
22
 
17
23
  // src/tracing.ts
18
24
  import { hostname as osHostname } from "os";
@@ -129,7 +135,7 @@ function markTaskCallFinished(sessionID, callID) {
129
135
  running.delete(callID);
130
136
  if (running.size === 0) runningTaskCallsBySession.delete(sessionID);
131
137
  }
132
- function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMessage, traceShipper) {
138
+ function endPendingToolSpanWithError(state, callID, tool2, toolCallArgs, errorMessage, traceShipper) {
133
139
  const startInfo = state.toolSpanStarts.get(callID);
134
140
  if (!startInfo) return false;
135
141
  state.toolSpanStarts.delete(callID);
@@ -151,7 +157,7 @@ function endPendingToolSpanWithError(state, callID, tool, toolCallArgs, errorMes
151
157
  startTimeUnixNano: startInfo.startTimeUnixNano,
152
158
  attributes: [
153
159
  attrString("ai.operationId", "ai.toolCall"),
154
- attrString("ai.toolCall.name", tool),
160
+ attrString("ai.toolCall.name", tool2),
155
161
  attrString("ai.toolCall.id", callID)
156
162
  ]
157
163
  });
@@ -188,7 +194,7 @@ function getHostname() {
188
194
  return h;
189
195
  }
190
196
  }
191
- function createHooks(config, worktree, directory, eventShipper, traceShipper) {
197
+ function createTracing(config, worktree, directory, eventShipper, traceShipper) {
192
198
  function log(msg, data) {
193
199
  if (!config.debug) return;
194
200
  const prefix = `[raindrop-ai/opencode-plugin] [info] ${msg}`;
@@ -214,7 +220,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
214
220
  callKey,
215
221
  log
216
222
  });
217
- return {
223
+ const hooks = {
218
224
  // ------------------------------------------------------------------
219
225
  // event — session lifecycle + streaming parts
220
226
  // ------------------------------------------------------------------
@@ -259,14 +265,14 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
259
265
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
260
266
  state.outputParts.set(messageId, capText(partObj["text"]));
261
267
  } else if (partObj["type"] === "tool" && messageId) {
262
- const tool = partObj["tool"];
268
+ const tool2 = partObj["tool"];
263
269
  const callID = partObj["callID"];
264
270
  const partState = partObj["state"];
265
271
  if (callID == null || callID === "")
266
272
  throw new Error("message.part.updated: part.callID is required for tool part");
267
273
  if (partState == null || typeof partState !== "object")
268
274
  throw new Error("message.part.updated: part.state is required for tool part");
269
- if (tool === "task") {
275
+ if (tool2 === "task") {
270
276
  const metadata = partState["metadata"];
271
277
  const childSessionId = metadata != null && typeof metadata === "object" ? metadata["sessionId"] : void 0;
272
278
  if (childSessionId != null && childSessionId !== "") {
@@ -291,11 +297,11 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
291
297
  if (endPendingToolSpanWithError(
292
298
  state,
293
299
  callID,
294
- tool,
300
+ tool2,
295
301
  toolCallArgs,
296
302
  errorMessage,
297
303
  traceShipper
298
- ) && tool === "task") {
304
+ ) && tool2 === "task") {
299
305
  markTaskCallFinished(partSessionID, callID);
300
306
  }
301
307
  }
@@ -672,20 +678,20 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
672
678
  "tool.execute.before": async (toolInput, _output) => {
673
679
  var _a, _b, _c;
674
680
  try {
675
- const { tool, sessionID, callID } = toolInput;
681
+ const { tool: tool2, sessionID, callID } = toolInput;
676
682
  const state = sessions.get(sessionID);
677
683
  if (!state || !state.currentEventId) return;
678
684
  const spanParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : (_a = state.currentRootSpan) == null ? void 0 : _a.ids;
679
685
  if (!spanParent) return;
680
686
  const startTimeUnixNano = nowUnixNanoString();
681
- if (tool === "task") {
687
+ if (tool2 === "task") {
682
688
  const liveTaskSpan = traceShipper.startSpan({
683
689
  name: "ai.toolCall",
684
690
  parent: spanParent,
685
691
  eventId: state.currentEventId,
686
692
  attributes: [
687
693
  attrString("ai.operationId", "ai.toolCall"),
688
- attrString("ai.toolCall.name", tool),
694
+ attrString("ai.toolCall.name", tool2),
689
695
  attrString("ai.toolCall.id", callID)
690
696
  ]
691
697
  });
@@ -710,7 +716,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
710
716
  startTimeUnixNano,
711
717
  parent: spanParent,
712
718
  eventId: state.currentEventId,
713
- name: tool,
719
+ name: tool2,
714
720
  liveSpan: liveTaskSpan
715
721
  });
716
722
  } else {
@@ -718,7 +724,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
718
724
  startTimeUnixNano,
719
725
  parent: spanParent,
720
726
  eventId: state.currentEventId,
721
- name: tool
727
+ name: tool2
722
728
  });
723
729
  }
724
730
  } catch (err) {
@@ -733,7 +739,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
733
739
  // ------------------------------------------------------------------
734
740
  "tool.execute.after": async (toolInput, result) => {
735
741
  try {
736
- const { tool, sessionID, callID, args } = toolInput;
742
+ const { tool: tool2, sessionID, callID, args } = toolInput;
737
743
  const state = sessions.get(sessionID);
738
744
  if (!state || !state.currentEventId) return;
739
745
  if (args === void 0 || args === null)
@@ -743,7 +749,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
743
749
  state.toolSpanStarts.delete(callID);
744
750
  const resultMetadata = result.metadata;
745
751
  const childSessionId = resultMetadata == null ? void 0 : resultMetadata["sessionId"];
746
- if (tool === "task" && childSessionId) {
752
+ if (tool2 === "task" && childSessionId) {
747
753
  mapChildSessionToParent.set(childSessionId, { parentId: sessionID });
748
754
  attachChildSessionToParentTask(childSessionId, sessionID, callID, "tool.execute.after");
749
755
  }
@@ -768,7 +774,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
768
774
  endTimeUnixNano,
769
775
  attributes: [
770
776
  attrString("ai.operationId", "ai.toolCall"),
771
- attrString("ai.toolCall.name", tool),
777
+ attrString("ai.toolCall.name", tool2),
772
778
  attrString("ai.toolCall.id", callID),
773
779
  attrString("ai.toolCall.args", toolCallArgs),
774
780
  attrString("ai.toolCall.result", toolResult)
@@ -776,7 +782,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
776
782
  });
777
783
  }
778
784
  }
779
- if (tool === "task") {
785
+ if (tool2 === "task") {
780
786
  markTaskCallFinished(sessionID, callID);
781
787
  }
782
788
  } catch (err) {
@@ -829,16 +835,24 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
829
835
  }
830
836
  }
831
837
  };
838
+ return {
839
+ hooks,
840
+ currentEventId(sessionID) {
841
+ var _a;
842
+ return (_a = sessions.get(sessionID)) == null ? void 0 : _a.currentEventId;
843
+ }
844
+ };
832
845
  }
833
846
 
834
847
  // src/index.ts
835
- function createRaindropHandle(eventShipper, traceShipper, appGitContext) {
848
+ function createRaindropHandle(eventShipper, traceShipper, appGitContext, currentEventId) {
836
849
  return {
837
850
  users: {
838
851
  async identify(users) {
839
852
  await eventShipper.identify(users);
840
853
  }
841
854
  },
855
+ currentEventId,
842
856
  async flush() {
843
857
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
844
858
  },
@@ -855,7 +869,7 @@ function createRaindropHandle(eventShipper, traceShipper, appGitContext) {
855
869
  };
856
870
  }
857
871
  async function plugin(input, opts = {}) {
858
- var _a, _b, _c;
872
+ var _a, _b, _c, _d;
859
873
  const config = loadConfig(input.directory);
860
874
  function appLog(level, message) {
861
875
  console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
@@ -899,14 +913,55 @@ async function plugin(input, opts = {}) {
899
913
  appGitContext
900
914
  });
901
915
  const worktree = (_c = input.worktree) != null ? _c : input.directory;
902
- const hooks = createHooks(
916
+ const tracing = createTracing(
903
917
  config,
904
918
  worktree,
905
919
  input.directory,
906
920
  eventShipper,
907
921
  traceShipper
908
922
  );
909
- return { ...hooks, raindrop: createRaindropHandle(eventShipper, traceShipper, appGitContext) };
923
+ const hooks = tracing.hooks;
924
+ const selfDiagnostics = normalizeSelfDiagnosticsConfig(
925
+ (_d = opts.selfDiagnostics) != null ? _d : config.selfDiagnostics
926
+ );
927
+ if (selfDiagnostics && !config.writeKey.trim()) {
928
+ appLog(
929
+ "warn",
930
+ `selfDiagnostics enabled but RAINDROP_WRITE_KEY is not set \u2014 the ${selfDiagnostics.toolName} tool is not registered (signals require the cloud destination).`
931
+ );
932
+ }
933
+ const [firstSignalKey, ...restSignalKeys] = config.writeKey.trim() && selfDiagnostics ? selfDiagnostics.signalKeys : [];
934
+ if (selfDiagnostics && firstSignalKey !== void 0) {
935
+ const cfg = selfDiagnostics;
936
+ hooks.tool = {
937
+ [cfg.toolName]: tool({
938
+ description: cfg.toolDescription,
939
+ args: {
940
+ category: tool.schema.enum([firstSignalKey, ...restSignalKeys]),
941
+ detail: tool.schema.string()
942
+ },
943
+ async execute(args, ctx) {
944
+ await trackSelfDiagnosticsSignal(
945
+ args,
946
+ cfg,
947
+ eventShipper,
948
+ tracing.currentEventId(ctx.sessionID),
949
+ config.debug
950
+ );
951
+ return SELF_DIAGNOSTICS_ACK;
952
+ }
953
+ })
954
+ };
955
+ }
956
+ return {
957
+ ...hooks,
958
+ raindrop: createRaindropHandle(
959
+ eventShipper,
960
+ traceShipper,
961
+ appGitContext,
962
+ tracing.currentEventId
963
+ )
964
+ };
910
965
  }
911
966
  export {
912
967
  plugin as default
@@ -86,4 +86,37 @@ declare global {
86
86
  var RAINDROP_ASYNC_LOCAL_STORAGE: (new <T>() => AsyncLocalStorageLike<T>) | undefined;
87
87
  }
88
88
 
89
- export type { AppGitOptions as A, IdentifyInput as I, SignalInput as S };
89
+ type SelfDiagnosticsSignalDefinition = {
90
+ /**
91
+ * Human-readable meaning of this signal.
92
+ * Used in the injected tool schema and stored in signal properties.
93
+ */
94
+ description: string;
95
+ /**
96
+ * Optional default sentiment for this signal key.
97
+ * This can help dashboards classify negative/positive programmatic signals.
98
+ */
99
+ sentiment?: "POSITIVE" | "NEGATIVE";
100
+ };
101
+ type SelfDiagnosticsSignalDefinitions = Record<string, SelfDiagnosticsSignalDefinition>;
102
+ type SelfDiagnosticsOptions = {
103
+ /** Enable automatic injection of the self diagnostics tool. Default: false */
104
+ enabled?: boolean;
105
+ /**
106
+ * Signal keys and descriptions exposed to the model.
107
+ * Defaults to a built-in set when omitted.
108
+ */
109
+ signals?: SelfDiagnosticsSignalDefinitions;
110
+ /**
111
+ * Optional extra guidance for when the model should emit self diagnostics.
112
+ * The SDK still generates a full tool prompt from `signals`.
113
+ */
114
+ guidance?: string;
115
+ /**
116
+ * Optional tool name override for the injected self diagnostics tool.
117
+ * Defaults to "__raindrop_report".
118
+ */
119
+ toolName?: string;
120
+ };
121
+
122
+ export type { AppGitOptions as A, IdentifyInput as I, SelfDiagnosticsOptions as S, SelfDiagnosticsSignalDefinition as a, SelfDiagnosticsSignalDefinitions as b, SignalInput as c };
@@ -86,4 +86,37 @@ declare global {
86
86
  var RAINDROP_ASYNC_LOCAL_STORAGE: (new <T>() => AsyncLocalStorageLike<T>) | undefined;
87
87
  }
88
88
 
89
- export type { AppGitOptions as A, IdentifyInput as I, SignalInput as S };
89
+ type SelfDiagnosticsSignalDefinition = {
90
+ /**
91
+ * Human-readable meaning of this signal.
92
+ * Used in the injected tool schema and stored in signal properties.
93
+ */
94
+ description: string;
95
+ /**
96
+ * Optional default sentiment for this signal key.
97
+ * This can help dashboards classify negative/positive programmatic signals.
98
+ */
99
+ sentiment?: "POSITIVE" | "NEGATIVE";
100
+ };
101
+ type SelfDiagnosticsSignalDefinitions = Record<string, SelfDiagnosticsSignalDefinition>;
102
+ type SelfDiagnosticsOptions = {
103
+ /** Enable automatic injection of the self diagnostics tool. Default: false */
104
+ enabled?: boolean;
105
+ /**
106
+ * Signal keys and descriptions exposed to the model.
107
+ * Defaults to a built-in set when omitted.
108
+ */
109
+ signals?: SelfDiagnosticsSignalDefinitions;
110
+ /**
111
+ * Optional extra guidance for when the model should emit self diagnostics.
112
+ * The SDK still generates a full tool prompt from `signals`.
113
+ */
114
+ guidance?: string;
115
+ /**
116
+ * Optional tool name override for the injected self diagnostics tool.
117
+ * Defaults to "__raindrop_report".
118
+ */
119
+ toolName?: string;
120
+ };
121
+
122
+ export type { AppGitOptions as A, IdentifyInput as I, SelfDiagnosticsOptions as S, SelfDiagnosticsSignalDefinition as a, SelfDiagnosticsSignalDefinitions as b, SignalInput as c };
package/dist/v2.cjs CHANGED
@@ -2309,9 +2309,41 @@ function loadConfig(projectDirectory) {
2309
2309
  captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_h = merged.capture_system_prompt) != null ? _h : false,
2310
2310
  eventMetadata,
2311
2311
  localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url),
2312
- appGit: mapAppGit(merged.app_git)
2312
+ appGit: mapAppGit(merged.app_git),
2313
+ selfDiagnostics: mapSelfDiagnostics(merged.self_diagnostics)
2313
2314
  };
2314
2315
  }
2316
+ function isRecord(value) {
2317
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2318
+ }
2319
+ function mapSelfDiagnostics(value) {
2320
+ const env = process.env["RAINDROP_SELF_DIAGNOSTICS"];
2321
+ const record = isRecord(value) ? value : {};
2322
+ const enabled = env !== void 0 ? env === "true" : typeof record["enabled"] === "boolean" ? record["enabled"] : void 0;
2323
+ const signals = mapSelfDiagnosticsSignals(record["signals"]);
2324
+ const options = {
2325
+ ...enabled !== void 0 ? { enabled } : {},
2326
+ ...signals ? { signals } : {},
2327
+ ...typeof record["guidance"] === "string" ? { guidance: record["guidance"] } : {},
2328
+ ...typeof record["tool_name"] === "string" ? { toolName: record["tool_name"] } : {}
2329
+ };
2330
+ return Object.keys(options).length > 0 ? options : void 0;
2331
+ }
2332
+ function mapSelfDiagnosticsSignals(value) {
2333
+ if (!isRecord(value)) return void 0;
2334
+ const out = {};
2335
+ for (const [key, entry] of Object.entries(value)) {
2336
+ if (!isRecord(entry)) continue;
2337
+ const definition = entry;
2338
+ if (typeof definition["description"] !== "string") continue;
2339
+ const sentiment = definition["sentiment"];
2340
+ out[key] = {
2341
+ description: definition["description"],
2342
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2343
+ };
2344
+ }
2345
+ return Object.keys(out).length > 0 ? out : void 0;
2346
+ }
2315
2347
  function mapAppGit(value) {
2316
2348
  if (value === false) return false;
2317
2349
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -2338,7 +2370,7 @@ function resolveLocalWorkshopUrl(fileValue) {
2338
2370
  // package.json
2339
2371
  var package_default = {
2340
2372
  name: "@raindrop-ai/opencode-plugin",
2341
- version: "0.2.0",
2373
+ version: "0.3.0",
2342
2374
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
2343
2375
  type: "module",
2344
2376
  main: "dist/index.js",
@@ -2427,6 +2459,185 @@ var package_default = {
2427
2459
  var PLUGIN_NAME = package_default.name;
2428
2460
  var PLUGIN_VERSION = package_default.version;
2429
2461
 
2462
+ // src/self-diagnostics.ts
2463
+ var SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT = "__raindrop_report";
2464
+ var SELF_DIAGNOSTICS_SIGNALS_DEFAULT = {
2465
+ missing_context: {
2466
+ 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.",
2467
+ sentiment: "NEGATIVE"
2468
+ },
2469
+ repeatedly_broken_tool: {
2470
+ 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.",
2471
+ sentiment: "NEGATIVE"
2472
+ },
2473
+ capability_gap: {
2474
+ 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.",
2475
+ sentiment: "NEGATIVE"
2476
+ },
2477
+ complete_task_failure: {
2478
+ 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.",
2479
+ sentiment: "NEGATIVE"
2480
+ }
2481
+ };
2482
+ 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.";
2483
+ var SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY = "noteworthy";
2484
+ function normalizeString(value) {
2485
+ if (typeof value !== "string") return void 0;
2486
+ const trimmed = value.trim();
2487
+ return trimmed || void 0;
2488
+ }
2489
+ function normalizeSelfDiagnosticsSignals(signals) {
2490
+ if (!signals) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2491
+ const normalizedEntries = Object.entries(signals).map(([key, value]) => {
2492
+ var _a;
2493
+ const signalKey = key.trim();
2494
+ if (!signalKey || !value || typeof value !== "object") return void 0;
2495
+ const description = (_a = value.description) == null ? void 0 : _a.trim();
2496
+ if (!description) return void 0;
2497
+ const sentiment = value.sentiment;
2498
+ return [
2499
+ signalKey,
2500
+ {
2501
+ description,
2502
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2503
+ }
2504
+ ];
2505
+ }).filter(
2506
+ (entry) => entry !== void 0
2507
+ );
2508
+ if (normalizedEntries.length === 0) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2509
+ return Object.fromEntries(normalizedEntries);
2510
+ }
2511
+ function normalizeSelfDiagnosticsConfig(options) {
2512
+ var _a, _b;
2513
+ if (!(options == null ? void 0 : options.enabled)) return void 0;
2514
+ const signalDefinitions = normalizeSelfDiagnosticsSignals(options.signals);
2515
+ const signalKeys = Object.keys(signalDefinitions);
2516
+ const signalDescriptions = {};
2517
+ const signalSentiments = {};
2518
+ for (const signalKey of signalKeys) {
2519
+ const definition = signalDefinitions[signalKey];
2520
+ if (!definition) continue;
2521
+ signalDescriptions[signalKey] = definition.description;
2522
+ signalSentiments[signalKey] = definition.sentiment;
2523
+ }
2524
+ const customGuidance = ((_a = options.guidance) == null ? void 0 : _a.trim()) || "";
2525
+ const toolName = ((_b = options.toolName) == null ? void 0 : _b.trim()) || SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT;
2526
+ const signalList = signalKeys.map((signalKey) => {
2527
+ const sentiment = signalSentiments[signalKey];
2528
+ const sentimentTag = sentiment ? ` [${sentiment.toLowerCase()}]` : "";
2529
+ return `- ${signalKey}: ${signalDescriptions[signalKey]}${sentimentTag}`;
2530
+ }).join("\n");
2531
+ const guidanceBlock = customGuidance ? `
2532
+ Additional guidance: ${customGuidance}
2533
+ ` : "";
2534
+ const toolDescription = `${SELF_DIAGNOSTICS_TOOL_PREAMBLE}
2535
+
2536
+ When to call:
2537
+ - You are blocked from completing the task due to missing information or access that the user cannot provide.
2538
+ - A tool is persistently failing across multiple attempts, not just a single transient error.
2539
+ - The task requires a tool, permission, or capability you do not have.
2540
+ - You genuinely cannot deliver what the user asked for despite trying.
2541
+
2542
+ When NOT to call:
2543
+ - Normal clarifying questions or back-and-forth with the user.
2544
+ - A single tool error that you can recover from or retry.
2545
+ - You successfully completed the task, even if it was difficult.
2546
+ - Policy refusals or content filtering \u2014 those are working as intended.
2547
+
2548
+ Rules:
2549
+ 1. Pick the single best category.
2550
+ 2. Do not fabricate issues. Only report what is evident from the conversation.
2551
+ 3. Err on the side of NOT calling this tool. When in doubt, help the user instead.
2552
+ ${guidanceBlock}
2553
+ Categories:
2554
+ ` + signalList;
2555
+ return {
2556
+ toolName,
2557
+ toolDescription,
2558
+ signalKeys,
2559
+ signalKeySet: new Set(signalKeys),
2560
+ signalDescriptions,
2561
+ signalSentiments
2562
+ };
2563
+ }
2564
+ var SELF_DIAGNOSTICS_ACK = '{"acknowledged":true}';
2565
+ function selfDiagnosticsInputJsonSchema(config) {
2566
+ return {
2567
+ type: "object",
2568
+ properties: {
2569
+ category: {
2570
+ type: "string",
2571
+ enum: config.signalKeys,
2572
+ description: "The single best-matching category from the list above."
2573
+ },
2574
+ detail: {
2575
+ type: "string",
2576
+ description: "One sentence of factual context: what happened and why it matters. Do not include PII or secrets."
2577
+ }
2578
+ },
2579
+ required: ["category", "detail"],
2580
+ additionalProperties: false
2581
+ };
2582
+ }
2583
+ function isRecord2(value) {
2584
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2585
+ }
2586
+ async function trackSelfDiagnosticsSignal(toolInput, config, eventShipper, eventId, debug) {
2587
+ var _a, _b;
2588
+ try {
2589
+ const input = isRecord2(toolInput) ? toolInput : void 0;
2590
+ const fallbackCategory = (_a = config.signalKeys[0]) != null ? _a : "unknown";
2591
+ const categoryCandidate = normalizeString(input == null ? void 0 : input["category"]);
2592
+ const category = categoryCandidate && config.signalKeySet.has(categoryCandidate) ? categoryCandidate : fallbackCategory;
2593
+ const detail = (_b = normalizeString(input == null ? void 0 : input["detail"])) != null ? _b : "";
2594
+ if (!eventId) {
2595
+ if (debug) {
2596
+ console.warn(
2597
+ "[raindrop-ai/opencode-plugin] self diagnostics signal skipped: no open event for this session."
2598
+ );
2599
+ }
2600
+ return;
2601
+ }
2602
+ if (category === SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY) {
2603
+ await eventShipper.trackSignal({
2604
+ eventId,
2605
+ name: "self diagnostics - noteworthy",
2606
+ type: "agent_internal",
2607
+ properties: {
2608
+ source: "agent_flag_event_tool",
2609
+ reason: detail,
2610
+ severity: "medium",
2611
+ sdk: "opencode-plugin",
2612
+ sdk_version: PLUGIN_VERSION
2613
+ }
2614
+ });
2615
+ return;
2616
+ }
2617
+ await eventShipper.trackSignal({
2618
+ eventId,
2619
+ name: `self diagnostics - ${category}`,
2620
+ type: "agent",
2621
+ sentiment: config.signalSentiments[category],
2622
+ properties: {
2623
+ source: "agent_reporting_tool",
2624
+ category,
2625
+ signal_description: config.signalDescriptions[category],
2626
+ sdk: "opencode-plugin",
2627
+ sdk_version: PLUGIN_VERSION,
2628
+ ...detail ? { detail } : {}
2629
+ }
2630
+ });
2631
+ } catch (err) {
2632
+ if (debug) {
2633
+ const message = err instanceof Error ? err.message : String(err);
2634
+ console.warn(
2635
+ `[raindrop-ai/opencode-plugin] selfDiagnostics signal dispatch failed: ${message}`
2636
+ );
2637
+ }
2638
+ }
2639
+ }
2640
+
2430
2641
  // src/shipper.ts
2431
2642
  var EventShipper3 = class extends EventShipper2 {
2432
2643
  constructor(opts) {
@@ -3223,7 +3434,7 @@ function raindrop(opts = {}) {
3223
3434
  }
3224
3435
  }
3225
3436
  function createShared(ctx) {
3226
- var _a, _b, _c;
3437
+ var _a, _b, _c, _d;
3227
3438
  const config = loadConfig((_a = opts.directory) != null ? _a : ctx.location.directory);
3228
3439
  const resolvedLocalUrl = resolveLocalDebuggerBaseUrl(config.localWorkshopUrl);
3229
3440
  const hasLocalDestination = resolvedLocalUrl !== null;
@@ -3256,9 +3467,20 @@ function raindrop(opts = {}) {
3256
3467
  };
3257
3468
  const eventShipper = new EventShipper3(shipperOptions);
3258
3469
  const traceShipper = new TraceShipper3(shipperOptions);
3470
+ const selfDiagnostics = normalizeSelfDiagnosticsConfig(
3471
+ (_d = opts.selfDiagnostics) != null ? _d : config.selfDiagnostics
3472
+ );
3473
+ if (selfDiagnostics && !config.writeKey.trim()) {
3474
+ appLog(
3475
+ "warn",
3476
+ `selfDiagnostics enabled but RAINDROP_WRITE_KEY is not set \u2014 the ${selfDiagnostics.toolName} tool is not registered (signals require the cloud destination).`
3477
+ );
3478
+ }
3259
3479
  const s = {
3260
3480
  eventShipper,
3261
3481
  traceShipper,
3482
+ config,
3483
+ selfDiagnostics: config.writeKey.trim() ? selfDiagnostics : void 0,
3262
3484
  appGitContext,
3263
3485
  abort: new AbortController(),
3264
3486
  instances: /* @__PURE__ */ new Set(),
@@ -3310,6 +3532,34 @@ function raindrop(opts = {}) {
3310
3532
  const directory = ctx.location.directory;
3311
3533
  instance.disposables.push(await ctx.session.hook("prompt", (e) => runtime.onPrompt(e, directory)));
3312
3534
  instance.disposables.push(await ctx.session.hook("context", (e) => runtime.onContext(e)));
3535
+ const selfDiagnostics = s.selfDiagnostics;
3536
+ if (selfDiagnostics) {
3537
+ const cfg = selfDiagnostics;
3538
+ const eventShipper = s.eventShipper;
3539
+ const debug = s.config.debug;
3540
+ instance.disposables.push(
3541
+ await ctx.tool.transform(
3542
+ (editor) => editor.add({
3543
+ name: cfg.toolName,
3544
+ description: cfg.toolDescription,
3545
+ input: selfDiagnosticsInputJsonSchema(cfg),
3546
+ // Direct tool — the host routes everything else through the
3547
+ // codemode `execute` wrapper, where this name isn't callable.
3548
+ options: { codemode: false },
3549
+ async execute(input, context) {
3550
+ await trackSelfDiagnosticsSignal(
3551
+ input,
3552
+ cfg,
3553
+ eventShipper,
3554
+ runtime.currentEventId(context.sessionID),
3555
+ debug
3556
+ );
3557
+ return { content: SELF_DIAGNOSTICS_ACK };
3558
+ }
3559
+ })
3560
+ )
3561
+ );
3562
+ }
3313
3563
  if (isFirst) void runEventLoop(s);
3314
3564
  const owner = s;
3315
3565
  return async () => {
package/dist/v2.d.cts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Plugin } from '@opencode/plugin';
2
- import { I as IdentifyInput, S as SignalInput, A as AppGitOptions } from './index.d-vgBcEZbX.cjs';
2
+ import { I as IdentifyInput, c as SignalInput, A as AppGitOptions, S as SelfDiagnosticsOptions } from './self-diagnostics-NiAq-SZ8.cjs';
3
+ export { a as SelfDiagnosticsSignalDefinition, b as SelfDiagnosticsSignalDefinitions } from './self-diagnostics-NiAq-SZ8.cjs';
3
4
 
4
5
  interface RaindropOpencodeV2Options {
5
6
  /** Application Git identity override. false disables enrichment. */
6
7
  appGit?: AppGitOptions | false;
7
8
  /** Config directory override; defaults to the first instance's ctx.location.directory. */
8
9
  directory?: string;
10
+ /** Built-in self-diagnostics: injects a `__raindrop_report` tool when enabled. */
11
+ selfDiagnostics?: SelfDiagnosticsOptions;
9
12
  }
10
13
  /**
11
14
  * The Raindrop control handle exposed on the v2 plugin. Unlike v1 (where the
@@ -38,4 +41,4 @@ interface RaindropOpencodePluginV2 extends Plugin.Plugin {
38
41
  */
39
42
  declare function raindrop(opts?: RaindropOpencodeV2Options): RaindropOpencodePluginV2;
40
43
 
41
- export { IdentifyInput, type RaindropOpencodePluginV2, type RaindropOpencodeV2Handle, type RaindropOpencodeV2Options, SignalInput, raindrop as default };
44
+ export { IdentifyInput, type RaindropOpencodePluginV2, type RaindropOpencodeV2Handle, type RaindropOpencodeV2Options, SelfDiagnosticsOptions, SignalInput, raindrop as default };
package/dist/v2.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Plugin } from '@opencode/plugin';
2
- import { I as IdentifyInput, S as SignalInput, A as AppGitOptions } from './index.d-vgBcEZbX.js';
2
+ import { I as IdentifyInput, c as SignalInput, A as AppGitOptions, S as SelfDiagnosticsOptions } from './self-diagnostics-NiAq-SZ8.js';
3
+ export { a as SelfDiagnosticsSignalDefinition, b as SelfDiagnosticsSignalDefinitions } from './self-diagnostics-NiAq-SZ8.js';
3
4
 
4
5
  interface RaindropOpencodeV2Options {
5
6
  /** Application Git identity override. false disables enrichment. */
6
7
  appGit?: AppGitOptions | false;
7
8
  /** Config directory override; defaults to the first instance's ctx.location.directory. */
8
9
  directory?: string;
10
+ /** Built-in self-diagnostics: injects a `__raindrop_report` tool when enabled. */
11
+ selfDiagnostics?: SelfDiagnosticsOptions;
9
12
  }
10
13
  /**
11
14
  * The Raindrop control handle exposed on the v2 plugin. Unlike v1 (where the
@@ -38,4 +41,4 @@ interface RaindropOpencodePluginV2 extends Plugin.Plugin {
38
41
  */
39
42
  declare function raindrop(opts?: RaindropOpencodeV2Options): RaindropOpencodePluginV2;
40
43
 
41
- export { IdentifyInput, type RaindropOpencodePluginV2, type RaindropOpencodeV2Handle, type RaindropOpencodeV2Options, SignalInput, raindrop as default };
44
+ export { IdentifyInput, type RaindropOpencodePluginV2, type RaindropOpencodeV2Handle, type RaindropOpencodeV2Options, SelfDiagnosticsOptions, SignalInput, raindrop as default };