@agentapprove/opencode 0.1.2 → 0.1.4
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 +423 -86
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir as homedir3 } from "os";
|
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
10
|
|
|
11
11
|
// src/e2e-crypto.ts
|
|
12
|
-
import { createHash, createHmac, createCipheriv, randomBytes } from "crypto";
|
|
12
|
+
import { createHash, createHmac, createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from "crypto";
|
|
13
13
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, copyFileSync } from "fs";
|
|
14
14
|
import { join as join2 } from "path";
|
|
15
15
|
import { homedir as homedir2 } from "os";
|
|
@@ -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) {
|
|
@@ -91,6 +135,33 @@ function e2eEncrypt(keyHex, plaintext) {
|
|
|
91
135
|
const hmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest("hex");
|
|
92
136
|
return `E2E:v1:${kid}:${ivHex}:${ciphertextBase64}:${hmac}`;
|
|
93
137
|
}
|
|
138
|
+
function isE2EEncrypted(value) {
|
|
139
|
+
return value.startsWith("E2E:v1:");
|
|
140
|
+
}
|
|
141
|
+
function e2eDecrypt(keyHex, encrypted) {
|
|
142
|
+
try {
|
|
143
|
+
if (!isE2EEncrypted(encrypted)) return null;
|
|
144
|
+
const parts = encrypted.split(":");
|
|
145
|
+
if (parts.length !== 6) return null;
|
|
146
|
+
const ivHex = parts[3];
|
|
147
|
+
const ciphertextBase64 = parts[4];
|
|
148
|
+
const hmacHex = parts[5];
|
|
149
|
+
const encKey = deriveEncKey(keyHex);
|
|
150
|
+
const macKey = deriveMacKey(keyHex);
|
|
151
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
152
|
+
const ciphertext = Buffer.from(ciphertextBase64, "base64");
|
|
153
|
+
const expectedHmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest();
|
|
154
|
+
const actualHmac = Buffer.from(hmacHex, "hex");
|
|
155
|
+
if (expectedHmac.length !== actualHmac.length || !timingSafeEqual(expectedHmac, actualHmac)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const decipher = createDecipheriv("aes-256-ctr", encKey, iv);
|
|
159
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
160
|
+
return plaintext.toString("utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
94
165
|
function applyApprovalE2E(payload, userKey, serverKey) {
|
|
95
166
|
const sensitiveFields = {};
|
|
96
167
|
for (const field of ["command", "toolInput", "cwd"]) {
|
|
@@ -687,6 +758,45 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
687
758
|
processConfigSync(parsed, config);
|
|
688
759
|
return parsed;
|
|
689
760
|
}
|
|
761
|
+
async function sendStopRequest(request, config, pluginPath) {
|
|
762
|
+
if (!config.token) {
|
|
763
|
+
throw new Error("No Agent Approve token configured");
|
|
764
|
+
}
|
|
765
|
+
const payload = applyEventPrivacyFilter(
|
|
766
|
+
{ ...request },
|
|
767
|
+
config.privacyTier
|
|
768
|
+
);
|
|
769
|
+
const bodyStr = JSON.stringify(payload);
|
|
770
|
+
const pluginHash = getPluginHash(pluginPath, config.debug);
|
|
771
|
+
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
772
|
+
const headers = {
|
|
773
|
+
"Authorization": `Bearer ${config.token}`,
|
|
774
|
+
...hmacHeaders
|
|
775
|
+
};
|
|
776
|
+
const url = `${config.apiUrl}/${config.apiVersion}/stop`;
|
|
777
|
+
const stopTimeoutMs = 3e5;
|
|
778
|
+
if (config.debug) {
|
|
779
|
+
debugLog(`Sending stop request to ${url}`);
|
|
780
|
+
debugLog(`=== SENT TO ${url} ===`);
|
|
781
|
+
debugLogRawInline(bodyStr);
|
|
782
|
+
debugLog("=== END SENT ===");
|
|
783
|
+
}
|
|
784
|
+
const response = await httpPost(url, bodyStr, headers, stopTimeoutMs);
|
|
785
|
+
if (config.debug) {
|
|
786
|
+
debugLog(`Stop response: ${response.body || "<empty>"}`);
|
|
787
|
+
}
|
|
788
|
+
if (response.status !== 200) {
|
|
789
|
+
throw new Error(`Stop API returned status ${response.status}: ${response.body.slice(0, 200)}`);
|
|
790
|
+
}
|
|
791
|
+
let parsed;
|
|
792
|
+
try {
|
|
793
|
+
parsed = JSON.parse(response.body);
|
|
794
|
+
} catch {
|
|
795
|
+
parsed = {};
|
|
796
|
+
}
|
|
797
|
+
processConfigSync(parsed, config);
|
|
798
|
+
return parsed;
|
|
799
|
+
}
|
|
690
800
|
async function sendEvent(event, config, pluginPath) {
|
|
691
801
|
if (!config.token) return;
|
|
692
802
|
const eventType = event.eventType;
|
|
@@ -734,25 +844,37 @@ async function sendEvent(event, config, pluginPath) {
|
|
|
734
844
|
}
|
|
735
845
|
|
|
736
846
|
// src/index.ts
|
|
847
|
+
var HOOK_PLUGIN = "opencode-plugin";
|
|
848
|
+
var HOOK_PERMISSION = "opencode-permission";
|
|
849
|
+
var HOOK_TOOL_BEFORE = "opencode-tool-before";
|
|
850
|
+
var HOOK_TOOL_AFTER = "opencode-tool-after";
|
|
851
|
+
var HOOK_CHAT_MSG = "opencode-chat-msg";
|
|
852
|
+
var HOOK_EVENT = "opencode-event";
|
|
853
|
+
var HOOK_COMPACTING = "opencode-compacting";
|
|
737
854
|
var pluginFilePath;
|
|
738
855
|
try {
|
|
739
856
|
pluginFilePath = fileURLToPath(import.meta.url);
|
|
740
857
|
} catch {
|
|
741
858
|
pluginFilePath = __filename;
|
|
742
859
|
}
|
|
743
|
-
var
|
|
860
|
+
var fallbackSessionId = randomBytes2(12).toString("hex");
|
|
744
861
|
var COMPACTION_DEDUP_WINDOW_MS = 2e3;
|
|
745
862
|
var openCodeClient = void 0;
|
|
746
863
|
var lastCompactionEventAt = 0;
|
|
864
|
+
var processingIdleSessions = /* @__PURE__ */ new Set();
|
|
865
|
+
var seenUnmappedEventTypes = /* @__PURE__ */ new Set();
|
|
866
|
+
var sentFinalMessageIds = /* @__PURE__ */ new Set();
|
|
867
|
+
var messageParts = /* @__PURE__ */ new Map();
|
|
868
|
+
var NOISY_EVENT_TYPES = /* @__PURE__ */ new Set(["message.part.delta"]);
|
|
747
869
|
function classifyTool(toolName) {
|
|
748
870
|
const lower = toolName.toLowerCase();
|
|
749
871
|
if (lower === "bash") {
|
|
750
872
|
return { toolType: "shell_command", displayName: toolName };
|
|
751
873
|
}
|
|
752
|
-
if (lower === "write" || lower === "edit" || lower === "patch") {
|
|
874
|
+
if (lower === "write" || lower === "edit" || lower === "patch" || lower === "apply_patch") {
|
|
753
875
|
return { toolType: "file_write", displayName: toolName };
|
|
754
876
|
}
|
|
755
|
-
if (lower === "read" || lower === "grep" || lower === "glob" || lower === "list") {
|
|
877
|
+
if (lower === "read" || lower === "grep" || lower === "glob" || lower === "list" || lower === "ls" || lower === "codesearch") {
|
|
756
878
|
return { toolType: "file_read", displayName: toolName };
|
|
757
879
|
}
|
|
758
880
|
if (lower === "webfetch" || lower === "websearch") {
|
|
@@ -772,10 +894,10 @@ function extractCommand(toolName, params) {
|
|
|
772
894
|
return params.command || void 0;
|
|
773
895
|
}
|
|
774
896
|
if (lower === "write" || lower === "edit" || lower === "patch") {
|
|
775
|
-
return params.file_path || params.path || void 0;
|
|
897
|
+
return params.file_path || params.filePath || params.path || void 0;
|
|
776
898
|
}
|
|
777
899
|
if (lower === "read") {
|
|
778
|
-
return params.file_path || params.path || void 0;
|
|
900
|
+
return params.file_path || params.filePath || params.path || void 0;
|
|
779
901
|
}
|
|
780
902
|
if (lower === "grep") {
|
|
781
903
|
return params.pattern || void 0;
|
|
@@ -794,9 +916,25 @@ function extractCommand(toolName, params) {
|
|
|
794
916
|
}
|
|
795
917
|
return void 0;
|
|
796
918
|
}
|
|
919
|
+
function resolveIds(preferredSessionId) {
|
|
920
|
+
const id = preferredSessionId || fallbackSessionId;
|
|
921
|
+
return { sessionId: id, conversationId: id };
|
|
922
|
+
}
|
|
923
|
+
function getEventSessionId(eventName, properties) {
|
|
924
|
+
const sessionID = properties.sessionID;
|
|
925
|
+
if (typeof sessionID === "string") return sessionID;
|
|
926
|
+
const info = properties.info;
|
|
927
|
+
if (info) {
|
|
928
|
+
if (typeof info.sessionID === "string") return info.sessionID;
|
|
929
|
+
if (eventName === "session.created" && typeof info.id === "string") return info.id;
|
|
930
|
+
}
|
|
931
|
+
const part = properties.part;
|
|
932
|
+
if (part && typeof part.sessionID === "string") return part.sessionID;
|
|
933
|
+
return void 0;
|
|
934
|
+
}
|
|
797
935
|
function handleFailBehavior(config, error, toolName) {
|
|
798
936
|
if (config.debug) {
|
|
799
|
-
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}
|
|
937
|
+
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`, HOOK_PERMISSION);
|
|
800
938
|
}
|
|
801
939
|
switch (config.failBehavior) {
|
|
802
940
|
case "deny":
|
|
@@ -809,20 +947,11 @@ function handleFailBehavior(config, error, toolName) {
|
|
|
809
947
|
}
|
|
810
948
|
}
|
|
811
949
|
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
|
-
}
|
|
950
|
+
if (eventName.startsWith("session.created")) return "session_start";
|
|
951
|
+
if (eventName === "session.compacted") return "context_compact";
|
|
952
|
+
if (eventName === "session.updated") return void 0;
|
|
953
|
+
if (eventName.startsWith("session.error") || eventName === "session.event.error") return "error";
|
|
954
|
+
return void 0;
|
|
826
955
|
}
|
|
827
956
|
function shouldSkipCompactionEvent() {
|
|
828
957
|
const now = Date.now();
|
|
@@ -832,9 +961,10 @@ function shouldSkipCompactionEvent() {
|
|
|
832
961
|
lastCompactionEventAt = now;
|
|
833
962
|
return false;
|
|
834
963
|
}
|
|
835
|
-
function plugin(
|
|
964
|
+
async function plugin(input) {
|
|
836
965
|
const config = loadConfig();
|
|
837
|
-
openCodeClient =
|
|
966
|
+
openCodeClient = input.client;
|
|
967
|
+
const projectPath = input.directory || process.cwd();
|
|
838
968
|
if (!config.token) {
|
|
839
969
|
console.warn(
|
|
840
970
|
"Agent Approve: No token found. Run the Agent Approve installer to pair with your account."
|
|
@@ -842,17 +972,39 @@ function plugin(context) {
|
|
|
842
972
|
return {};
|
|
843
973
|
}
|
|
844
974
|
if (config.debug) {
|
|
845
|
-
|
|
975
|
+
const e2eStatus = !config.e2eEnabled ? "disabled" : !config.e2eUserKey ? "enabled (key missing)" : "enabled";
|
|
976
|
+
debugLog(
|
|
977
|
+
`Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}`,
|
|
978
|
+
HOOK_PLUGIN
|
|
979
|
+
);
|
|
980
|
+
debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}`, HOOK_PLUGIN);
|
|
846
981
|
}
|
|
847
982
|
return {
|
|
848
983
|
// -------------------------------------------------------------------
|
|
849
984
|
// permission.ask: Primary approval gate (blocking)
|
|
985
|
+
//
|
|
986
|
+
// Source signature (opencode/packages/plugin/src/index.ts L179):
|
|
987
|
+
// (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
|
988
|
+
//
|
|
989
|
+
// Permission = { id, type, pattern?, sessionID, messageID, callID?,
|
|
990
|
+
// message, metadata, time: { created } }
|
|
991
|
+
//
|
|
992
|
+
// Called from opencode/packages/opencode/src/permission/index.ts L134:
|
|
993
|
+
// Plugin.trigger("permission.ask", info, { status: "ask" })
|
|
850
994
|
// -------------------------------------------------------------------
|
|
851
|
-
"permission.ask": async (
|
|
852
|
-
|
|
853
|
-
|
|
995
|
+
"permission.ask": async (input2, output) => {
|
|
996
|
+
if (config.debug) {
|
|
997
|
+
debugLogRaw({ input: input2, output }, HOOK_PERMISSION);
|
|
998
|
+
debugLog("Started permission.ask hook", HOOK_PERMISSION);
|
|
999
|
+
}
|
|
1000
|
+
const toolName = input2.type;
|
|
1001
|
+
const params = input2.metadata || {};
|
|
854
1002
|
const { toolType, displayName } = classifyTool(toolName);
|
|
855
1003
|
const command = extractCommand(toolName, params);
|
|
1004
|
+
if (config.debug) {
|
|
1005
|
+
debugLog(`Tool: ${displayName} (${toolType}) [agent: ${config.agentName}]`, HOOK_PERMISSION);
|
|
1006
|
+
}
|
|
1007
|
+
const ids = resolveIds(input2.sessionID);
|
|
856
1008
|
const request = {
|
|
857
1009
|
toolName: displayName,
|
|
858
1010
|
toolType,
|
|
@@ -861,27 +1013,28 @@ function plugin(context) {
|
|
|
861
1013
|
agent: config.agentType,
|
|
862
1014
|
agentName: config.agentName,
|
|
863
1015
|
hookType: "permission_ask",
|
|
864
|
-
sessionId,
|
|
865
|
-
conversationId:
|
|
1016
|
+
sessionId: ids.sessionId,
|
|
1017
|
+
conversationId: ids.conversationId,
|
|
866
1018
|
cwd: params.workdir || params.cwd || void 0,
|
|
867
|
-
projectPath
|
|
1019
|
+
projectPath,
|
|
868
1020
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
869
1021
|
};
|
|
870
1022
|
try {
|
|
871
1023
|
const response = await sendApprovalRequest(request, config, pluginFilePath);
|
|
1024
|
+
if (config.debug) {
|
|
1025
|
+
debugLog(`Decision: ${response.decision}, Reason: ${response.reason || ""}`, HOOK_PERMISSION);
|
|
1026
|
+
}
|
|
872
1027
|
if (response.decision === "approve" || response.decision === "allow") {
|
|
873
1028
|
if (config.debug) {
|
|
874
|
-
debugLog(
|
|
1029
|
+
debugLog("Tool approved", HOOK_PERMISSION);
|
|
875
1030
|
}
|
|
876
1031
|
output.status = "allow";
|
|
877
|
-
if (response.reason) output.reason = response.reason;
|
|
878
1032
|
return;
|
|
879
1033
|
}
|
|
880
1034
|
if (config.debug) {
|
|
881
|
-
debugLog(
|
|
1035
|
+
debugLog("Tool denied", HOOK_PERMISSION);
|
|
882
1036
|
}
|
|
883
1037
|
output.status = "deny";
|
|
884
|
-
output.reason = response.reason || "Denied by Agent Approve";
|
|
885
1038
|
} catch (error) {
|
|
886
1039
|
const result = handleFailBehavior(
|
|
887
1040
|
config,
|
|
@@ -895,10 +1048,24 @@ function plugin(context) {
|
|
|
895
1048
|
},
|
|
896
1049
|
// -------------------------------------------------------------------
|
|
897
1050
|
// tool.execute.before: Tool start monitoring (non-blocking)
|
|
1051
|
+
//
|
|
1052
|
+
// Source signature (opencode/packages/plugin/src/index.ts L184-187):
|
|
1053
|
+
// (input: { tool: string; sessionID: string; callID: string },
|
|
1054
|
+
// output: { args: any }) => Promise<void>
|
|
1055
|
+
//
|
|
1056
|
+
// Called from opencode/packages/opencode/src/session/prompt.ts L792-802:
|
|
1057
|
+
// Plugin.trigger("tool.execute.before",
|
|
1058
|
+
// { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
|
|
1059
|
+
// { args })
|
|
898
1060
|
// -------------------------------------------------------------------
|
|
899
|
-
"tool.execute.before": async (
|
|
900
|
-
|
|
901
|
-
|
|
1061
|
+
"tool.execute.before": async (input2, output) => {
|
|
1062
|
+
if (config.debug) {
|
|
1063
|
+
debugLogRaw({ input: input2, output }, HOOK_TOOL_BEFORE);
|
|
1064
|
+
debugLog(`Tool start: ${input2.tool} [session: ${input2.sessionID}]`, HOOK_TOOL_BEFORE);
|
|
1065
|
+
}
|
|
1066
|
+
const ids = resolveIds(input2.sessionID);
|
|
1067
|
+
const toolName = input2.tool;
|
|
1068
|
+
const params = output.args && typeof output.args === "object" ? output.args : {};
|
|
902
1069
|
const { toolType } = classifyTool(toolName);
|
|
903
1070
|
void sendEvent({
|
|
904
1071
|
toolName,
|
|
@@ -907,8 +1074,8 @@ function plugin(context) {
|
|
|
907
1074
|
agent: config.agentType,
|
|
908
1075
|
agentName: config.agentName,
|
|
909
1076
|
hookType: "tool_execute_before",
|
|
910
|
-
sessionId,
|
|
911
|
-
conversationId:
|
|
1077
|
+
sessionId: ids.sessionId,
|
|
1078
|
+
conversationId: ids.conversationId,
|
|
912
1079
|
command: extractCommand(toolName, params),
|
|
913
1080
|
toolInput: params,
|
|
914
1081
|
cwd: params.workdir || params.cwd || void 0,
|
|
@@ -917,13 +1084,28 @@ function plugin(context) {
|
|
|
917
1084
|
},
|
|
918
1085
|
// -------------------------------------------------------------------
|
|
919
1086
|
// tool.execute.after: Tool completion logging (non-blocking)
|
|
1087
|
+
//
|
|
1088
|
+
// Source signature (opencode/packages/plugin/src/index.ts L192-199):
|
|
1089
|
+
// (input: { tool: string; sessionID: string; callID: string; args: any },
|
|
1090
|
+
// output: { title: string; output: string; metadata: any }) => Promise<void>
|
|
1091
|
+
//
|
|
1092
|
+
// Called from opencode/packages/opencode/src/session/prompt.ts L813-822:
|
|
1093
|
+
// Plugin.trigger("tool.execute.after",
|
|
1094
|
+
// { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
|
|
1095
|
+
// output)
|
|
920
1096
|
// -------------------------------------------------------------------
|
|
921
|
-
"tool.execute.after": async (
|
|
922
|
-
|
|
923
|
-
|
|
1097
|
+
"tool.execute.after": async (input2, output) => {
|
|
1098
|
+
if (config.debug) {
|
|
1099
|
+
debugLogRaw({ input: input2, output }, HOOK_TOOL_AFTER);
|
|
1100
|
+
const resultLen = typeof output?.output === "string" ? output.output.length : 0;
|
|
1101
|
+
debugLog(`Tool complete: ${input2.tool}, output=${resultLen} chars${output?.metadata?.error ? ", ERROR" : ""}`, HOOK_TOOL_AFTER);
|
|
1102
|
+
}
|
|
1103
|
+
const ids = resolveIds(input2.sessionID);
|
|
1104
|
+
const toolName = input2.tool;
|
|
1105
|
+
const params = input2.args && typeof input2.args === "object" ? input2.args : {};
|
|
924
1106
|
const { toolType } = classifyTool(toolName);
|
|
925
|
-
const resultStr =
|
|
926
|
-
const response =
|
|
1107
|
+
const resultStr = typeof output?.output === "string" ? output.output : void 0;
|
|
1108
|
+
const response = resultStr || void 0;
|
|
927
1109
|
const responsePreview = resultStr && resultStr.length > 1e3 ? resultStr.slice(0, 1e3) + "..." : resultStr;
|
|
928
1110
|
void sendEvent({
|
|
929
1111
|
toolName,
|
|
@@ -932,31 +1114,48 @@ function plugin(context) {
|
|
|
932
1114
|
agent: config.agentType,
|
|
933
1115
|
agentName: config.agentName,
|
|
934
1116
|
hookType: "tool_execute_after",
|
|
935
|
-
sessionId,
|
|
936
|
-
conversationId:
|
|
1117
|
+
sessionId: ids.sessionId,
|
|
1118
|
+
conversationId: ids.conversationId,
|
|
937
1119
|
command: extractCommand(toolName, params),
|
|
938
1120
|
toolInput: params,
|
|
939
|
-
status:
|
|
1121
|
+
status: output?.metadata?.error ? "error" : "success",
|
|
940
1122
|
response,
|
|
941
1123
|
responsePreview,
|
|
942
|
-
durationMs: input.durationMs,
|
|
943
1124
|
cwd: params.workdir || params.cwd || void 0,
|
|
944
1125
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
945
1126
|
}, config, pluginFilePath);
|
|
946
1127
|
},
|
|
947
1128
|
// -------------------------------------------------------------------
|
|
948
1129
|
// chat.message: User prompt capture (non-blocking)
|
|
1130
|
+
//
|
|
1131
|
+
// Source signature (opencode/packages/plugin/src/index.ts L158-167):
|
|
1132
|
+
// (input: { sessionID: string; agent?: string; model?: {...};
|
|
1133
|
+
// messageID?: string; variant?: string },
|
|
1134
|
+
// output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
|
1135
|
+
//
|
|
1136
|
+
// Called from opencode/packages/opencode/src/session/prompt.ts L1295-1308:
|
|
1137
|
+
// Plugin.trigger("chat.message",
|
|
1138
|
+
// { sessionID, agent, model, messageID, variant },
|
|
1139
|
+
// { message: info, parts })
|
|
1140
|
+
//
|
|
1141
|
+
// Only fires for user messages (inside createUserMessage).
|
|
1142
|
+
// User text is in output.parts where part.type === "text" && !part.synthetic
|
|
949
1143
|
// -------------------------------------------------------------------
|
|
950
|
-
"chat.message": async (
|
|
951
|
-
if (
|
|
952
|
-
|
|
1144
|
+
"chat.message": async (input2, output) => {
|
|
1145
|
+
if (config.debug) {
|
|
1146
|
+
debugLogRaw({ input: input2, output }, HOOK_CHAT_MSG);
|
|
1147
|
+
debugLog(`Chat message: session=${input2.sessionID}, parts=${output.parts?.length ?? 0}, agent=${input2.agent || "default"}`, HOOK_CHAT_MSG);
|
|
1148
|
+
}
|
|
1149
|
+
const ids = resolveIds(input2.sessionID);
|
|
1150
|
+
const textParts = (output.parts || []).filter((p) => p.type === "text" && !p.synthetic && p.text).map((p) => p.text);
|
|
1151
|
+
const text = textParts.join("\n");
|
|
953
1152
|
void sendEvent({
|
|
954
1153
|
eventType: "user_prompt",
|
|
955
1154
|
agent: config.agentType,
|
|
956
1155
|
agentName: config.agentName,
|
|
957
1156
|
hookType: "chat_message",
|
|
958
|
-
sessionId,
|
|
959
|
-
conversationId:
|
|
1157
|
+
sessionId: ids.sessionId,
|
|
1158
|
+
conversationId: ids.conversationId,
|
|
960
1159
|
prompt: text,
|
|
961
1160
|
textLength: text.length,
|
|
962
1161
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -964,56 +1163,181 @@ function plugin(context) {
|
|
|
964
1163
|
},
|
|
965
1164
|
// -------------------------------------------------------------------
|
|
966
1165
|
// event: Catch-all session lifecycle events (blocking for session.idle)
|
|
1166
|
+
//
|
|
1167
|
+
// Source signature (opencode/packages/plugin/src/index.ts L149):
|
|
1168
|
+
// event?: (input: { event: Event }) => Promise<void>
|
|
1169
|
+
//
|
|
1170
|
+
// Dispatched from opencode/packages/opencode/src/plugin/index.ts L134-141:
|
|
1171
|
+
// Bus.subscribeAll(async (input) => {
|
|
1172
|
+
// for (const hook of hooks) { hook["event"]?.({ event: input }) }
|
|
1173
|
+
// })
|
|
1174
|
+
//
|
|
1175
|
+
// Bus events have shape: { type: string, properties: ... }
|
|
967
1176
|
// -------------------------------------------------------------------
|
|
968
|
-
"event": async (
|
|
969
|
-
const eventName =
|
|
970
|
-
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
1177
|
+
"event": async (input2) => {
|
|
1178
|
+
const eventName = input2.event?.type;
|
|
1179
|
+
if (!eventName) return;
|
|
1180
|
+
if (NOISY_EVENT_TYPES.has(eventName)) return;
|
|
1181
|
+
const eventProperties = input2.event.properties || {};
|
|
1182
|
+
const eventSessionId = getEventSessionId(eventName, eventProperties);
|
|
1183
|
+
const ids = resolveIds(eventSessionId);
|
|
1184
|
+
if (config.debug) {
|
|
1185
|
+
debugLog(`Event received: ${eventName}`, HOOK_EVENT);
|
|
1186
|
+
}
|
|
1187
|
+
if (eventName === "message.part.updated") {
|
|
1188
|
+
const part = eventProperties.part;
|
|
1189
|
+
if (!part) return;
|
|
1190
|
+
const messageID = part.messageID;
|
|
1191
|
+
const partID = part.id;
|
|
1192
|
+
const partType = part.type;
|
|
1193
|
+
if (typeof messageID !== "string" || typeof partID !== "string" || typeof partType !== "string") return;
|
|
1194
|
+
const partsByMessage = messageParts.get(messageID) || /* @__PURE__ */ new Map();
|
|
1195
|
+
partsByMessage.set(partID, {
|
|
1196
|
+
type: partType,
|
|
1197
|
+
text: typeof part.text === "string" ? part.text : void 0,
|
|
1198
|
+
synthetic: part.synthetic === true,
|
|
1199
|
+
ignored: part.ignored === true,
|
|
1200
|
+
sessionID: typeof part.sessionID === "string" ? part.sessionID : void 0
|
|
1201
|
+
});
|
|
1202
|
+
messageParts.set(messageID, partsByMessage);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (eventName === "message.part.removed") {
|
|
1206
|
+
const messageID = eventProperties.messageID;
|
|
1207
|
+
const partID = eventProperties.partID;
|
|
1208
|
+
if (typeof messageID === "string" && typeof partID === "string") {
|
|
1209
|
+
messageParts.get(messageID)?.delete(partID);
|
|
974
1210
|
}
|
|
975
1211
|
return;
|
|
976
1212
|
}
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
1213
|
+
if (eventName === "message.updated") {
|
|
1214
|
+
const info = eventProperties.info;
|
|
1215
|
+
if (!info) return;
|
|
1216
|
+
if (info.role !== "assistant") return;
|
|
1217
|
+
const messageID = info.id;
|
|
1218
|
+
const finish = info.finish;
|
|
1219
|
+
if (typeof messageID !== "string" || typeof finish !== "string") return;
|
|
1220
|
+
if (finish === "tool-calls" || finish === "unknown") return;
|
|
1221
|
+
if (sentFinalMessageIds.has(messageID)) return;
|
|
1222
|
+
sentFinalMessageIds.add(messageID);
|
|
1223
|
+
const partsByMessage = messageParts.get(messageID);
|
|
1224
|
+
const parts = partsByMessage ? Array.from(partsByMessage.values()) : [];
|
|
1225
|
+
const thoughtText = parts.filter((p) => p.type === "reasoning" && typeof p.text === "string" && p.text.length > 0).map((p) => p.text).join("\n");
|
|
1226
|
+
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");
|
|
1227
|
+
if (thoughtText) {
|
|
1228
|
+
void sendEvent({
|
|
1229
|
+
eventType: "thought",
|
|
1230
|
+
agent: config.agentType,
|
|
1231
|
+
agentName: config.agentName,
|
|
1232
|
+
hookType: "event",
|
|
1233
|
+
sessionId: ids.sessionId,
|
|
1234
|
+
conversationId: ids.conversationId,
|
|
1235
|
+
trigger: eventName,
|
|
1236
|
+
text: thoughtText,
|
|
1237
|
+
textPreview: thoughtText.slice(0, 200),
|
|
1238
|
+
textLength: thoughtText.length,
|
|
1239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1240
|
+
}, config, pluginFilePath);
|
|
980
1241
|
}
|
|
1242
|
+
if (responseText) {
|
|
1243
|
+
void sendEvent({
|
|
1244
|
+
eventType: "response",
|
|
1245
|
+
agent: config.agentType,
|
|
1246
|
+
agentName: config.agentName,
|
|
1247
|
+
hookType: "event",
|
|
1248
|
+
sessionId: ids.sessionId,
|
|
1249
|
+
conversationId: ids.conversationId,
|
|
1250
|
+
trigger: eventName,
|
|
1251
|
+
text: responseText,
|
|
1252
|
+
textPreview: responseText.slice(0, 200),
|
|
1253
|
+
textLength: responseText.length,
|
|
1254
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1255
|
+
}, config, pluginFilePath);
|
|
1256
|
+
}
|
|
1257
|
+
messageParts.delete(messageID);
|
|
981
1258
|
return;
|
|
982
1259
|
}
|
|
983
|
-
if (eventName === "session.idle")
|
|
1260
|
+
if (eventName === "session.idle") return;
|
|
1261
|
+
const status = eventProperties.status;
|
|
1262
|
+
if (eventName === "session.status" && status?.type !== "idle") {
|
|
1263
|
+
processingIdleSessions.delete(ids.sessionId);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (eventName === "session.status" && status?.type === "idle") {
|
|
1267
|
+
if (processingIdleSessions.has(ids.sessionId)) {
|
|
1268
|
+
if (config.debug) {
|
|
1269
|
+
debugLog("Skipping duplicate session.status idle (already processing)", HOOK_EVENT);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
processingIdleSessions.add(ids.sessionId);
|
|
984
1274
|
if (config.debug) {
|
|
985
|
-
debugLog("Session idle detected, sending
|
|
1275
|
+
debugLog("Session idle detected via session.status, sending stop request", HOOK_EVENT);
|
|
986
1276
|
}
|
|
987
1277
|
const stopRequest = {
|
|
988
|
-
toolName: "session_idle",
|
|
989
|
-
toolType: "session",
|
|
990
1278
|
agent: config.agentType,
|
|
991
1279
|
agentName: config.agentName,
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1280
|
+
eventType: "stop",
|
|
1281
|
+
status: "completed",
|
|
1282
|
+
loopCount: 0,
|
|
1283
|
+
sessionId: ids.sessionId,
|
|
1284
|
+
conversationId: ids.conversationId,
|
|
1285
|
+
timestamp: Date.now(),
|
|
1286
|
+
...config.e2eEnabled && config.e2eUserKey ? { e2eKeyId: keyId(config.e2eUserKey) } : {}
|
|
997
1287
|
};
|
|
998
1288
|
try {
|
|
999
|
-
const response = await
|
|
1000
|
-
if (
|
|
1289
|
+
const response = await sendStopRequest(stopRequest, config, pluginFilePath);
|
|
1290
|
+
if (config.debug) {
|
|
1291
|
+
debugLog(`Stop response: followup=${response.followup_message ? "yes" : "no"}`, HOOK_EVENT);
|
|
1292
|
+
}
|
|
1293
|
+
if (response.followup_message && openCodeClient) {
|
|
1294
|
+
let followupText = response.followup_message;
|
|
1295
|
+
if (config.e2eEnabled && config.e2eUserKey && isE2EEncrypted(followupText)) {
|
|
1296
|
+
const decrypted = e2eDecrypt(config.e2eUserKey, followupText);
|
|
1297
|
+
if (decrypted) {
|
|
1298
|
+
if (config.debug) {
|
|
1299
|
+
debugLog(`Decrypted follow-up: ${decrypted.slice(0, 50)}...`, HOOK_EVENT);
|
|
1300
|
+
}
|
|
1301
|
+
followupText = decrypted;
|
|
1302
|
+
} else {
|
|
1303
|
+
if (config.debug) {
|
|
1304
|
+
debugLog("E2E decryption failed for follow-up message, skipping injection", HOOK_EVENT);
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1001
1309
|
if (config.debug) {
|
|
1002
|
-
debugLog(`
|
|
1310
|
+
debugLog(`Injecting user follow-up: ${followupText.slice(0, 100)}`, HOOK_EVENT);
|
|
1003
1311
|
}
|
|
1004
1312
|
try {
|
|
1005
|
-
await openCodeClient.tui.appendPrompt({ body: { text:
|
|
1313
|
+
await openCodeClient.tui.appendPrompt({ body: { text: followupText } });
|
|
1006
1314
|
await openCodeClient.tui.submitPrompt();
|
|
1007
1315
|
} catch (injectionError) {
|
|
1008
1316
|
if (config.debug) {
|
|
1009
|
-
debugLog(`Failed to inject follow-up
|
|
1317
|
+
debugLog(`Failed to inject follow-up: ${injectionError instanceof Error ? injectionError.message : String(injectionError)}`, HOOK_EVENT);
|
|
1010
1318
|
}
|
|
1011
1319
|
}
|
|
1012
1320
|
}
|
|
1013
1321
|
} catch (error) {
|
|
1014
1322
|
if (config.debug) {
|
|
1015
|
-
debugLog(`Stop
|
|
1323
|
+
debugLog(`Stop request error: ${error instanceof Error ? error.message : String(error)}`, HOOK_EVENT);
|
|
1016
1324
|
}
|
|
1325
|
+
} finally {
|
|
1326
|
+
processingIdleSessions.delete(ids.sessionId);
|
|
1327
|
+
}
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const agEventType = mapEventType(eventName);
|
|
1331
|
+
if (!agEventType) {
|
|
1332
|
+
if (config.debug && !seenUnmappedEventTypes.has(eventName)) {
|
|
1333
|
+
seenUnmappedEventTypes.add(eventName);
|
|
1334
|
+
debugLog(`Ignoring unmapped event type: ${eventName}`, HOOK_EVENT);
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (agEventType === "context_compact" && shouldSkipCompactionEvent()) {
|
|
1339
|
+
if (config.debug) {
|
|
1340
|
+
debugLog(`Skipping duplicate context_compact from ${eventName}`, HOOK_EVENT);
|
|
1017
1341
|
}
|
|
1018
1342
|
return;
|
|
1019
1343
|
}
|
|
@@ -1022,34 +1346,47 @@ function plugin(context) {
|
|
|
1022
1346
|
agent: config.agentType,
|
|
1023
1347
|
agentName: config.agentName,
|
|
1024
1348
|
hookType: "event",
|
|
1025
|
-
sessionId,
|
|
1026
|
-
conversationId:
|
|
1349
|
+
sessionId: ids.sessionId,
|
|
1350
|
+
conversationId: ids.conversationId,
|
|
1027
1351
|
trigger: eventName,
|
|
1028
|
-
text:
|
|
1352
|
+
text: Object.keys(eventProperties).length > 0 ? JSON.stringify(eventProperties).slice(0, 500) : void 0,
|
|
1029
1353
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1030
1354
|
}, config, pluginFilePath);
|
|
1031
1355
|
},
|
|
1032
1356
|
// -------------------------------------------------------------------
|
|
1033
1357
|
// experimental.session.compacting: Pre-compaction event (non-blocking)
|
|
1358
|
+
//
|
|
1359
|
+
// Source signature (opencode/packages/plugin/src/index.ts L222-225):
|
|
1360
|
+
// (input: { sessionID: string },
|
|
1361
|
+
// output: { context: string[]; prompt?: string }) => Promise<void>
|
|
1362
|
+
//
|
|
1363
|
+
// Called from opencode/packages/opencode/src/session/compaction.ts L146-150:
|
|
1364
|
+
// Plugin.trigger("experimental.session.compacting",
|
|
1365
|
+
// { sessionID: input.sessionID },
|
|
1366
|
+
// { context: [], prompt: undefined })
|
|
1034
1367
|
// -------------------------------------------------------------------
|
|
1035
|
-
"experimental.session.compacting": async (
|
|
1368
|
+
"experimental.session.compacting": async (input2, _output) => {
|
|
1369
|
+
if (config.debug) {
|
|
1370
|
+
debugLogRaw({ input: input2 }, HOOK_COMPACTING);
|
|
1371
|
+
}
|
|
1372
|
+
const ids = resolveIds(input2.sessionID);
|
|
1036
1373
|
if (shouldSkipCompactionEvent()) {
|
|
1037
1374
|
if (config.debug) {
|
|
1038
|
-
debugLog("Skipping duplicate context_compact from experimental.session.compacting");
|
|
1375
|
+
debugLog("Skipping duplicate context_compact from experimental.session.compacting", HOOK_COMPACTING);
|
|
1039
1376
|
}
|
|
1040
1377
|
return;
|
|
1041
1378
|
}
|
|
1042
1379
|
if (config.debug) {
|
|
1043
|
-
debugLog(`Context compaction
|
|
1380
|
+
debugLog(`Context compaction starting for session ${input2.sessionID}`, HOOK_COMPACTING);
|
|
1044
1381
|
}
|
|
1045
1382
|
void sendEvent({
|
|
1046
1383
|
eventType: "context_compact",
|
|
1047
1384
|
agent: config.agentType,
|
|
1048
1385
|
agentName: config.agentName,
|
|
1049
1386
|
hookType: "session_compacting",
|
|
1050
|
-
sessionId,
|
|
1051
|
-
conversationId:
|
|
1052
|
-
trigger:
|
|
1387
|
+
sessionId: ids.sessionId,
|
|
1388
|
+
conversationId: ids.conversationId,
|
|
1389
|
+
trigger: "experimental.session.compacting",
|
|
1053
1390
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1054
1391
|
}, config, pluginFilePath);
|
|
1055
1392
|
}
|
package/package.json
CHANGED