@agentapprove/opencode 0.1.1 → 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.
- package/dist/index.js +316 -81
- 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
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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(
|
|
896
|
+
async function plugin(input) {
|
|
836
897
|
const config = loadConfig();
|
|
837
|
-
openCodeClient =
|
|
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
|
-
|
|
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 (
|
|
852
|
-
|
|
853
|
-
|
|
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:
|
|
948
|
+
sessionId: ids.sessionId,
|
|
949
|
+
conversationId: ids.conversationId,
|
|
866
950
|
cwd: params.workdir || params.cwd || void 0,
|
|
867
|
-
projectPath
|
|
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(
|
|
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(
|
|
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 (
|
|
900
|
-
|
|
901
|
-
|
|
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:
|
|
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 (
|
|
922
|
-
|
|
923
|
-
|
|
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 =
|
|
926
|
-
const response =
|
|
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:
|
|
1049
|
+
sessionId: ids.sessionId,
|
|
1050
|
+
conversationId: ids.conversationId,
|
|
937
1051
|
command: extractCommand(toolName, params),
|
|
938
1052
|
toolInput: params,
|
|
939
|
-
status:
|
|
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 (
|
|
951
|
-
if (
|
|
952
|
-
|
|
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:
|
|
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 (
|
|
969
|
-
const eventName =
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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 (
|
|
978
|
-
|
|
979
|
-
|
|
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(
|
|
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,73 +1200,98 @@ function plugin(context) {
|
|
|
990
1200
|
agent: config.agentType,
|
|
991
1201
|
agentName: config.agentName,
|
|
992
1202
|
hookType: "stop",
|
|
993
|
-
sessionId,
|
|
994
|
-
conversationId:
|
|
995
|
-
projectPath
|
|
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:
|
|
1249
|
+
sessionId: ids.sessionId,
|
|
1250
|
+
conversationId: ids.conversationId,
|
|
1027
1251
|
trigger: eventName,
|
|
1028
|
-
text:
|
|
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 (
|
|
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
|
|
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:
|
|
1052
|
-
trigger:
|
|
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
|
}
|
|
1056
1293
|
};
|
|
1057
1294
|
}
|
|
1058
1295
|
export {
|
|
1059
|
-
|
|
1060
|
-
plugin as default,
|
|
1061
|
-
extractCommand
|
|
1296
|
+
plugin as default
|
|
1062
1297
|
};
|
package/package.json
CHANGED