@agentapprove/opencode 0.1.2 → 0.1.3

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.
Files changed (2) hide show
  1. package/dist/index.js +315 -78
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -62,6 +62,50 @@ function debugLogRawInline(data) {
62
62
  } catch {
63
63
  }
64
64
  }
65
+ function safeStringify(value) {
66
+ try {
67
+ const seen = /* @__PURE__ */ new WeakSet();
68
+ return JSON.stringify(value, (_key, current) => {
69
+ if (typeof current === "bigint") return `BigInt(${current.toString()})`;
70
+ if (typeof current === "function") {
71
+ const fn = current;
72
+ return `[Function ${fn.name || "anonymous"}]`;
73
+ }
74
+ if (typeof current === "symbol") return current.toString();
75
+ if (current instanceof Error) {
76
+ return {
77
+ name: current.name,
78
+ message: current.message,
79
+ stack: current.stack
80
+ };
81
+ }
82
+ if (current && typeof current === "object") {
83
+ const obj = current;
84
+ if (seen.has(obj)) return "[Circular]";
85
+ seen.add(obj);
86
+ }
87
+ return current;
88
+ }) ?? "null";
89
+ } catch (error) {
90
+ const msg = error instanceof Error ? error.message : String(error);
91
+ return `"[Unserializable: ${msg}]"`;
92
+ }
93
+ }
94
+ function debugLogRaw(data, hookName = "opencode-plugin") {
95
+ try {
96
+ ensureLogFile();
97
+ const ts = localTimestamp();
98
+ const rawData = typeof data === "string" ? data : safeStringify(data);
99
+ appendFileSync(
100
+ DEBUG_LOG_PATH,
101
+ `[${ts}] [${hookName}] === RAW INPUT ===
102
+ ${rawData}
103
+ [${ts}] [${hookName}] === END RAW ===
104
+ `
105
+ );
106
+ } catch {
107
+ }
108
+ }
65
109
 
66
110
  // src/e2e-crypto.ts
67
111
  function keyId(keyHex) {
@@ -734,25 +778,35 @@ async function sendEvent(event, config, pluginPath) {
734
778
  }
735
779
 
736
780
  // src/index.ts
781
+ var HOOK_PLUGIN = "opencode-plugin";
782
+ var HOOK_PERMISSION = "opencode-permission";
783
+ var HOOK_TOOL_BEFORE = "opencode-tool-before";
784
+ var HOOK_TOOL_AFTER = "opencode-tool-after";
785
+ var HOOK_CHAT_MSG = "opencode-chat-msg";
786
+ var HOOK_EVENT = "opencode-event";
787
+ var HOOK_COMPACTING = "opencode-compacting";
737
788
  var pluginFilePath;
738
789
  try {
739
790
  pluginFilePath = fileURLToPath(import.meta.url);
740
791
  } catch {
741
792
  pluginFilePath = __filename;
742
793
  }
743
- var sessionId = randomBytes2(12).toString("hex");
794
+ var fallbackSessionId = randomBytes2(12).toString("hex");
744
795
  var COMPACTION_DEDUP_WINDOW_MS = 2e3;
745
796
  var openCodeClient = void 0;
746
797
  var lastCompactionEventAt = 0;
798
+ var seenUnmappedEventTypes = /* @__PURE__ */ new Set();
799
+ var sentFinalMessageIds = /* @__PURE__ */ new Set();
800
+ var messageParts = /* @__PURE__ */ new Map();
747
801
  function classifyTool(toolName) {
748
802
  const lower = toolName.toLowerCase();
749
803
  if (lower === "bash") {
750
804
  return { toolType: "shell_command", displayName: toolName };
751
805
  }
752
- if (lower === "write" || lower === "edit" || lower === "patch") {
806
+ if (lower === "write" || lower === "edit" || lower === "patch" || lower === "apply_patch") {
753
807
  return { toolType: "file_write", displayName: toolName };
754
808
  }
755
- if (lower === "read" || lower === "grep" || lower === "glob" || lower === "list") {
809
+ if (lower === "read" || lower === "grep" || lower === "glob" || lower === "list" || lower === "ls" || lower === "codesearch") {
756
810
  return { toolType: "file_read", displayName: toolName };
757
811
  }
758
812
  if (lower === "webfetch" || lower === "websearch") {
@@ -772,10 +826,10 @@ function extractCommand(toolName, params) {
772
826
  return params.command || void 0;
773
827
  }
774
828
  if (lower === "write" || lower === "edit" || lower === "patch") {
775
- return params.file_path || params.path || void 0;
829
+ return params.file_path || params.filePath || params.path || void 0;
776
830
  }
777
831
  if (lower === "read") {
778
- return params.file_path || params.path || void 0;
832
+ return params.file_path || params.filePath || params.path || void 0;
779
833
  }
780
834
  if (lower === "grep") {
781
835
  return params.pattern || void 0;
@@ -794,9 +848,25 @@ function extractCommand(toolName, params) {
794
848
  }
795
849
  return void 0;
796
850
  }
851
+ function resolveIds(preferredSessionId) {
852
+ const id = preferredSessionId || fallbackSessionId;
853
+ return { sessionId: id, conversationId: id };
854
+ }
855
+ function getEventSessionId(eventName, properties) {
856
+ const sessionID = properties.sessionID;
857
+ if (typeof sessionID === "string") return sessionID;
858
+ const info = properties.info;
859
+ if (info) {
860
+ if (typeof info.sessionID === "string") return info.sessionID;
861
+ if (eventName === "session.created" && typeof info.id === "string") return info.id;
862
+ }
863
+ const part = properties.part;
864
+ if (part && typeof part.sessionID === "string") return part.sessionID;
865
+ return void 0;
866
+ }
797
867
  function handleFailBehavior(config, error, toolName) {
798
868
  if (config.debug) {
799
- debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
869
+ debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`, HOOK_PERMISSION);
800
870
  }
801
871
  switch (config.failBehavior) {
802
872
  case "deny":
@@ -809,20 +879,11 @@ function handleFailBehavior(config, error, toolName) {
809
879
  }
810
880
  }
811
881
  function mapEventType(eventName) {
812
- switch (eventName) {
813
- case "session.created":
814
- return "session_start";
815
- case "session.idle":
816
- return "stop";
817
- case "session.compacted":
818
- return "context_compact";
819
- case "session.error":
820
- return "error";
821
- case "message.updated":
822
- return "response";
823
- default:
824
- return void 0;
825
- }
882
+ if (eventName.startsWith("session.created")) return "session_start";
883
+ if (eventName === "session.compacted") return "context_compact";
884
+ if (eventName === "session.updated") return void 0;
885
+ if (eventName.startsWith("session.error") || eventName === "session.event.error") return "error";
886
+ return void 0;
826
887
  }
827
888
  function shouldSkipCompactionEvent() {
828
889
  const now = Date.now();
@@ -832,9 +893,10 @@ function shouldSkipCompactionEvent() {
832
893
  lastCompactionEventAt = now;
833
894
  return false;
834
895
  }
835
- function plugin(context) {
896
+ async function plugin(input) {
836
897
  const config = loadConfig();
837
- openCodeClient = context.client;
898
+ openCodeClient = input.client;
899
+ const projectPath = input.directory || process.cwd();
838
900
  if (!config.token) {
839
901
  console.warn(
840
902
  "Agent Approve: No token found. Run the Agent Approve installer to pair with your account."
@@ -842,17 +904,39 @@ function plugin(context) {
842
904
  return {};
843
905
  }
844
906
  if (config.debug) {
845
- debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
907
+ const e2eStatus = !config.e2eEnabled ? "disabled" : !config.e2eUserKey ? "enabled (key missing)" : "enabled";
908
+ debugLog(
909
+ `Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}`,
910
+ HOOK_PLUGIN
911
+ );
912
+ debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}`, HOOK_PLUGIN);
846
913
  }
847
914
  return {
848
915
  // -------------------------------------------------------------------
849
916
  // permission.ask: Primary approval gate (blocking)
917
+ //
918
+ // Source signature (opencode/packages/plugin/src/index.ts L179):
919
+ // (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
920
+ //
921
+ // Permission = { id, type, pattern?, sessionID, messageID, callID?,
922
+ // message, metadata, time: { created } }
923
+ //
924
+ // Called from opencode/packages/opencode/src/permission/index.ts L134:
925
+ // Plugin.trigger("permission.ask", info, { status: "ask" })
850
926
  // -------------------------------------------------------------------
851
- "permission.ask": async (input, output) => {
852
- const toolName = input.tool.name;
853
- const params = input.tool.input || {};
927
+ "permission.ask": async (input2, output) => {
928
+ if (config.debug) {
929
+ debugLogRaw({ input: input2, output }, HOOK_PERMISSION);
930
+ debugLog("Started permission.ask hook", HOOK_PERMISSION);
931
+ }
932
+ const toolName = input2.type;
933
+ const params = input2.metadata || {};
854
934
  const { toolType, displayName } = classifyTool(toolName);
855
935
  const command = extractCommand(toolName, params);
936
+ if (config.debug) {
937
+ debugLog(`Tool: ${displayName} (${toolType}) [agent: ${config.agentName}]`, HOOK_PERMISSION);
938
+ }
939
+ const ids = resolveIds(input2.sessionID);
856
940
  const request = {
857
941
  toolName: displayName,
858
942
  toolType,
@@ -861,27 +945,28 @@ function plugin(context) {
861
945
  agent: config.agentType,
862
946
  agentName: config.agentName,
863
947
  hookType: "permission_ask",
864
- sessionId,
865
- conversationId: sessionId,
948
+ sessionId: ids.sessionId,
949
+ conversationId: ids.conversationId,
866
950
  cwd: params.workdir || params.cwd || void 0,
867
- projectPath: process.cwd(),
951
+ projectPath,
868
952
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
869
953
  };
870
954
  try {
871
955
  const response = await sendApprovalRequest(request, config, pluginFilePath);
956
+ if (config.debug) {
957
+ debugLog(`Decision: ${response.decision}, Reason: ${response.reason || ""}`, HOOK_PERMISSION);
958
+ }
872
959
  if (response.decision === "approve" || response.decision === "allow") {
873
960
  if (config.debug) {
874
- debugLog(`Tool "${toolName}" approved${response.reason ? ": " + response.reason : ""}`);
961
+ debugLog("Tool approved", HOOK_PERMISSION);
875
962
  }
876
963
  output.status = "allow";
877
- if (response.reason) output.reason = response.reason;
878
964
  return;
879
965
  }
880
966
  if (config.debug) {
881
- debugLog(`Tool "${toolName}" denied${response.reason ? ": " + response.reason : ""}`);
967
+ debugLog("Tool denied", HOOK_PERMISSION);
882
968
  }
883
969
  output.status = "deny";
884
- output.reason = response.reason || "Denied by Agent Approve";
885
970
  } catch (error) {
886
971
  const result = handleFailBehavior(
887
972
  config,
@@ -895,10 +980,24 @@ function plugin(context) {
895
980
  },
896
981
  // -------------------------------------------------------------------
897
982
  // tool.execute.before: Tool start monitoring (non-blocking)
983
+ //
984
+ // Source signature (opencode/packages/plugin/src/index.ts L184-187):
985
+ // (input: { tool: string; sessionID: string; callID: string },
986
+ // output: { args: any }) => Promise<void>
987
+ //
988
+ // Called from opencode/packages/opencode/src/session/prompt.ts L792-802:
989
+ // Plugin.trigger("tool.execute.before",
990
+ // { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
991
+ // { args })
898
992
  // -------------------------------------------------------------------
899
- "tool.execute.before": async (input) => {
900
- const toolName = input.tool.name;
901
- const params = input.tool.input || {};
993
+ "tool.execute.before": async (input2, output) => {
994
+ if (config.debug) {
995
+ debugLogRaw({ input: input2, output }, HOOK_TOOL_BEFORE);
996
+ debugLog(`Tool start: ${input2.tool} [session: ${input2.sessionID}]`, HOOK_TOOL_BEFORE);
997
+ }
998
+ const ids = resolveIds(input2.sessionID);
999
+ const toolName = input2.tool;
1000
+ const params = output.args && typeof output.args === "object" ? output.args : {};
902
1001
  const { toolType } = classifyTool(toolName);
903
1002
  void sendEvent({
904
1003
  toolName,
@@ -907,8 +1006,8 @@ function plugin(context) {
907
1006
  agent: config.agentType,
908
1007
  agentName: config.agentName,
909
1008
  hookType: "tool_execute_before",
910
- sessionId,
911
- conversationId: sessionId,
1009
+ sessionId: ids.sessionId,
1010
+ conversationId: ids.conversationId,
912
1011
  command: extractCommand(toolName, params),
913
1012
  toolInput: params,
914
1013
  cwd: params.workdir || params.cwd || void 0,
@@ -917,13 +1016,28 @@ function plugin(context) {
917
1016
  },
918
1017
  // -------------------------------------------------------------------
919
1018
  // tool.execute.after: Tool completion logging (non-blocking)
1019
+ //
1020
+ // Source signature (opencode/packages/plugin/src/index.ts L192-199):
1021
+ // (input: { tool: string; sessionID: string; callID: string; args: any },
1022
+ // output: { title: string; output: string; metadata: any }) => Promise<void>
1023
+ //
1024
+ // Called from opencode/packages/opencode/src/session/prompt.ts L813-822:
1025
+ // Plugin.trigger("tool.execute.after",
1026
+ // { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
1027
+ // output)
920
1028
  // -------------------------------------------------------------------
921
- "tool.execute.after": async (input) => {
922
- const toolName = input.tool.name;
923
- const params = input.tool.input || {};
1029
+ "tool.execute.after": async (input2, output) => {
1030
+ if (config.debug) {
1031
+ debugLogRaw({ input: input2, output }, HOOK_TOOL_AFTER);
1032
+ const resultLen = typeof output?.output === "string" ? output.output.length : 0;
1033
+ debugLog(`Tool complete: ${input2.tool}, output=${resultLen} chars${output?.metadata?.error ? ", ERROR" : ""}`, HOOK_TOOL_AFTER);
1034
+ }
1035
+ const ids = resolveIds(input2.sessionID);
1036
+ const toolName = input2.tool;
1037
+ const params = input2.args && typeof input2.args === "object" ? input2.args : {};
924
1038
  const { toolType } = classifyTool(toolName);
925
- const resultStr = input.result != null ? typeof input.result === "string" ? input.result : JSON.stringify(input.result) : void 0;
926
- const response = input.error || resultStr || void 0;
1039
+ const resultStr = typeof output?.output === "string" ? output.output : void 0;
1040
+ const response = resultStr || void 0;
927
1041
  const responsePreview = resultStr && resultStr.length > 1e3 ? resultStr.slice(0, 1e3) + "..." : resultStr;
928
1042
  void sendEvent({
929
1043
  toolName,
@@ -932,31 +1046,48 @@ function plugin(context) {
932
1046
  agent: config.agentType,
933
1047
  agentName: config.agentName,
934
1048
  hookType: "tool_execute_after",
935
- sessionId,
936
- conversationId: sessionId,
1049
+ sessionId: ids.sessionId,
1050
+ conversationId: ids.conversationId,
937
1051
  command: extractCommand(toolName, params),
938
1052
  toolInput: params,
939
- status: input.error ? "error" : "success",
1053
+ status: output?.metadata?.error ? "error" : "success",
940
1054
  response,
941
1055
  responsePreview,
942
- durationMs: input.durationMs,
943
1056
  cwd: params.workdir || params.cwd || void 0,
944
1057
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
945
1058
  }, config, pluginFilePath);
946
1059
  },
947
1060
  // -------------------------------------------------------------------
948
1061
  // chat.message: User prompt capture (non-blocking)
1062
+ //
1063
+ // Source signature (opencode/packages/plugin/src/index.ts L158-167):
1064
+ // (input: { sessionID: string; agent?: string; model?: {...};
1065
+ // messageID?: string; variant?: string },
1066
+ // output: { message: UserMessage; parts: Part[] }) => Promise<void>
1067
+ //
1068
+ // Called from opencode/packages/opencode/src/session/prompt.ts L1295-1308:
1069
+ // Plugin.trigger("chat.message",
1070
+ // { sessionID, agent, model, messageID, variant },
1071
+ // { message: info, parts })
1072
+ //
1073
+ // Only fires for user messages (inside createUserMessage).
1074
+ // User text is in output.parts where part.type === "text" && !part.synthetic
949
1075
  // -------------------------------------------------------------------
950
- "chat.message": async (input) => {
951
- if (input.role && input.role !== "user") return;
952
- const text = input.content || "";
1076
+ "chat.message": async (input2, output) => {
1077
+ if (config.debug) {
1078
+ debugLogRaw({ input: input2, output }, HOOK_CHAT_MSG);
1079
+ debugLog(`Chat message: session=${input2.sessionID}, parts=${output.parts?.length ?? 0}, agent=${input2.agent || "default"}`, HOOK_CHAT_MSG);
1080
+ }
1081
+ const ids = resolveIds(input2.sessionID);
1082
+ const textParts = (output.parts || []).filter((p) => p.type === "text" && !p.synthetic && p.text).map((p) => p.text);
1083
+ const text = textParts.join("\n");
953
1084
  void sendEvent({
954
1085
  eventType: "user_prompt",
955
1086
  agent: config.agentType,
956
1087
  agentName: config.agentName,
957
1088
  hookType: "chat_message",
958
- sessionId,
959
- conversationId: sessionId,
1089
+ sessionId: ids.sessionId,
1090
+ conversationId: ids.conversationId,
960
1091
  prompt: text,
961
1092
  textLength: text.length,
962
1093
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -964,25 +1095,104 @@ function plugin(context) {
964
1095
  },
965
1096
  // -------------------------------------------------------------------
966
1097
  // event: Catch-all session lifecycle events (blocking for session.idle)
1098
+ //
1099
+ // Source signature (opencode/packages/plugin/src/index.ts L149):
1100
+ // event?: (input: { event: Event }) => Promise<void>
1101
+ //
1102
+ // Dispatched from opencode/packages/opencode/src/plugin/index.ts L134-141:
1103
+ // Bus.subscribeAll(async (input) => {
1104
+ // for (const hook of hooks) { hook["event"]?.({ event: input }) }
1105
+ // })
1106
+ //
1107
+ // Bus events have shape: { type: string, properties: ... }
967
1108
  // -------------------------------------------------------------------
968
- "event": async (input) => {
969
- const eventName = input.type;
970
- const agEventType = mapEventType(eventName);
971
- if (!agEventType) {
972
- if (config.debug) {
973
- debugLog(`Ignoring unmapped event: ${eventName}`);
1109
+ "event": async (input2) => {
1110
+ const eventName = input2.event?.type;
1111
+ if (!eventName) return;
1112
+ const eventProperties = input2.event.properties || {};
1113
+ const eventSessionId = getEventSessionId(eventName, eventProperties);
1114
+ const ids = resolveIds(eventSessionId);
1115
+ if (config.debug && !seenUnmappedEventTypes.has(`_logged_${eventName}`)) {
1116
+ debugLog(`Event received: ${eventName}`, HOOK_EVENT);
1117
+ }
1118
+ if (eventName === "message.part.updated") {
1119
+ const part = eventProperties.part;
1120
+ if (!part) return;
1121
+ const messageID = part.messageID;
1122
+ const partID = part.id;
1123
+ const partType = part.type;
1124
+ if (typeof messageID !== "string" || typeof partID !== "string" || typeof partType !== "string") return;
1125
+ const partsByMessage = messageParts.get(messageID) || /* @__PURE__ */ new Map();
1126
+ partsByMessage.set(partID, {
1127
+ type: partType,
1128
+ text: typeof part.text === "string" ? part.text : void 0,
1129
+ synthetic: part.synthetic === true,
1130
+ ignored: part.ignored === true,
1131
+ sessionID: typeof part.sessionID === "string" ? part.sessionID : void 0
1132
+ });
1133
+ messageParts.set(messageID, partsByMessage);
1134
+ return;
1135
+ }
1136
+ if (eventName === "message.part.removed") {
1137
+ const messageID = eventProperties.messageID;
1138
+ const partID = eventProperties.partID;
1139
+ if (typeof messageID === "string" && typeof partID === "string") {
1140
+ messageParts.get(messageID)?.delete(partID);
974
1141
  }
975
1142
  return;
976
1143
  }
977
- if (agEventType === "context_compact" && shouldSkipCompactionEvent()) {
978
- if (config.debug) {
979
- debugLog(`Skipping duplicate context_compact from ${eventName}`);
1144
+ if (eventName === "message.updated") {
1145
+ const info = eventProperties.info;
1146
+ if (!info) return;
1147
+ if (info.role !== "assistant") return;
1148
+ const messageID = info.id;
1149
+ const finish = info.finish;
1150
+ if (typeof messageID !== "string" || typeof finish !== "string") return;
1151
+ if (finish === "tool-calls" || finish === "unknown") return;
1152
+ if (sentFinalMessageIds.has(messageID)) return;
1153
+ sentFinalMessageIds.add(messageID);
1154
+ const partsByMessage = messageParts.get(messageID);
1155
+ const parts = partsByMessage ? Array.from(partsByMessage.values()) : [];
1156
+ const thoughtText = parts.filter((p) => p.type === "reasoning" && typeof p.text === "string" && p.text.length > 0).map((p) => p.text).join("\n");
1157
+ const responseText = parts.filter((p) => p.type === "text" && !p.synthetic && !p.ignored && typeof p.text === "string" && p.text.length > 0).map((p) => p.text).join("\n");
1158
+ if (thoughtText) {
1159
+ void sendEvent({
1160
+ eventType: "thought",
1161
+ agent: config.agentType,
1162
+ agentName: config.agentName,
1163
+ hookType: "event",
1164
+ sessionId: ids.sessionId,
1165
+ conversationId: ids.conversationId,
1166
+ trigger: eventName,
1167
+ text: thoughtText,
1168
+ textPreview: thoughtText.slice(0, 200),
1169
+ textLength: thoughtText.length,
1170
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1171
+ }, config, pluginFilePath);
1172
+ }
1173
+ if (responseText) {
1174
+ void sendEvent({
1175
+ eventType: "response",
1176
+ agent: config.agentType,
1177
+ agentName: config.agentName,
1178
+ hookType: "event",
1179
+ sessionId: ids.sessionId,
1180
+ conversationId: ids.conversationId,
1181
+ trigger: eventName,
1182
+ text: responseText,
1183
+ textPreview: responseText.slice(0, 200),
1184
+ textLength: responseText.length,
1185
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1186
+ }, config, pluginFilePath);
980
1187
  }
1188
+ messageParts.delete(messageID);
981
1189
  return;
982
1190
  }
983
- if (eventName === "session.idle") {
1191
+ if (eventName === "session.idle") return;
1192
+ const status = eventProperties.status;
1193
+ if (eventName === "session.status" && status?.type === "idle") {
984
1194
  if (config.debug) {
985
- debugLog("Session idle detected, sending blocking stop event");
1195
+ debugLog(`Session idle detected via session.status, sending blocking stop event`, HOOK_EVENT);
986
1196
  }
987
1197
  const stopRequest = {
988
1198
  toolName: "session_idle",
@@ -990,66 +1200,93 @@ function plugin(context) {
990
1200
  agent: config.agentType,
991
1201
  agentName: config.agentName,
992
1202
  hookType: "stop",
993
- sessionId,
994
- conversationId: sessionId,
995
- projectPath: process.cwd(),
1203
+ sessionId: ids.sessionId,
1204
+ conversationId: ids.conversationId,
1205
+ projectPath,
996
1206
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
997
1207
  };
998
1208
  try {
999
1209
  const response = await sendApprovalRequest(stopRequest, config, pluginFilePath);
1000
1210
  if ((response.decision === "approve" || response.decision === "allow") && response.reason && openCodeClient) {
1001
1211
  if (config.debug) {
1002
- debugLog(`Stop approved with follow-up input: ${response.reason.slice(0, 100)}`);
1212
+ debugLog(`Stop approved with follow-up input: ${response.reason.slice(0, 100)}`, HOOK_EVENT);
1003
1213
  }
1004
1214
  try {
1005
1215
  await openCodeClient.tui.appendPrompt({ body: { text: response.reason } });
1006
1216
  await openCodeClient.tui.submitPrompt();
1007
1217
  } catch (injectionError) {
1008
1218
  if (config.debug) {
1009
- debugLog(`Failed to inject follow-up input: ${injectionError instanceof Error ? injectionError.message : String(injectionError)}`);
1219
+ debugLog(`Failed to inject follow-up input: ${injectionError instanceof Error ? injectionError.message : String(injectionError)}`, HOOK_EVENT);
1010
1220
  }
1011
1221
  }
1012
1222
  }
1013
1223
  } catch (error) {
1014
1224
  if (config.debug) {
1015
- debugLog(`Stop event error: ${error instanceof Error ? error.message : String(error)}`);
1225
+ debugLog(`Stop event error: ${error instanceof Error ? error.message : String(error)}`, HOOK_EVENT);
1016
1226
  }
1017
1227
  }
1018
1228
  return;
1019
1229
  }
1230
+ const agEventType = mapEventType(eventName);
1231
+ if (!agEventType) {
1232
+ if (config.debug && !seenUnmappedEventTypes.has(eventName)) {
1233
+ seenUnmappedEventTypes.add(eventName);
1234
+ debugLog(`Ignoring unmapped event type: ${eventName}`, HOOK_EVENT);
1235
+ }
1236
+ return;
1237
+ }
1238
+ if (agEventType === "context_compact" && shouldSkipCompactionEvent()) {
1239
+ if (config.debug) {
1240
+ debugLog(`Skipping duplicate context_compact from ${eventName}`, HOOK_EVENT);
1241
+ }
1242
+ return;
1243
+ }
1020
1244
  void sendEvent({
1021
1245
  eventType: agEventType,
1022
1246
  agent: config.agentType,
1023
1247
  agentName: config.agentName,
1024
1248
  hookType: "event",
1025
- sessionId,
1026
- conversationId: sessionId,
1249
+ sessionId: ids.sessionId,
1250
+ conversationId: ids.conversationId,
1027
1251
  trigger: eventName,
1028
- text: input.data ? JSON.stringify(input.data).slice(0, 500) : void 0,
1252
+ text: Object.keys(eventProperties).length > 0 ? JSON.stringify(eventProperties).slice(0, 500) : void 0,
1029
1253
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1030
1254
  }, config, pluginFilePath);
1031
1255
  },
1032
1256
  // -------------------------------------------------------------------
1033
1257
  // experimental.session.compacting: Pre-compaction event (non-blocking)
1258
+ //
1259
+ // Source signature (opencode/packages/plugin/src/index.ts L222-225):
1260
+ // (input: { sessionID: string },
1261
+ // output: { context: string[]; prompt?: string }) => Promise<void>
1262
+ //
1263
+ // Called from opencode/packages/opencode/src/session/compaction.ts L146-150:
1264
+ // Plugin.trigger("experimental.session.compacting",
1265
+ // { sessionID: input.sessionID },
1266
+ // { context: [], prompt: undefined })
1034
1267
  // -------------------------------------------------------------------
1035
- "experimental.session.compacting": async (input) => {
1268
+ "experimental.session.compacting": async (input2, _output) => {
1269
+ if (config.debug) {
1270
+ debugLogRaw({ input: input2 }, HOOK_COMPACTING);
1271
+ }
1272
+ const ids = resolveIds(input2.sessionID);
1036
1273
  if (shouldSkipCompactionEvent()) {
1037
1274
  if (config.debug) {
1038
- debugLog("Skipping duplicate context_compact from experimental.session.compacting");
1275
+ debugLog("Skipping duplicate context_compact from experimental.session.compacting", HOOK_COMPACTING);
1039
1276
  }
1040
1277
  return;
1041
1278
  }
1042
1279
  if (config.debug) {
1043
- debugLog(`Context compaction: ${input.messageCount ?? "?"} messages${input.tokenCount ? `, ${input.tokenCount} tokens` : ""}`);
1280
+ debugLog(`Context compaction starting for session ${input2.sessionID}`, HOOK_COMPACTING);
1044
1281
  }
1045
1282
  void sendEvent({
1046
1283
  eventType: "context_compact",
1047
1284
  agent: config.agentType,
1048
1285
  agentName: config.agentName,
1049
1286
  hookType: "session_compacting",
1050
- sessionId,
1051
- conversationId: sessionId,
1052
- trigger: `${input.messageCount ?? "?"} messages${input.tokenCount ? `, ${input.tokenCount} tokens` : ""}`,
1287
+ sessionId: ids.sessionId,
1288
+ conversationId: ids.conversationId,
1289
+ trigger: "experimental.session.compacting",
1053
1290
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1054
1291
  }, config, pluginFilePath);
1055
1292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentapprove/opencode",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Agent Approve plugin for OpenCode - approve or deny AI agent tool calls from your iPhone and Apple Watch",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {