opencode-froggy 0.12.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/command/diff-summary.md +27 -8
- package/command/doc-changes.md +1 -0
- package/command/review-changes.md +1 -0
- package/command/review-pr.md +1 -0
- package/command/simplify-changes.md +1 -0
- package/dist/command-installer.d.ts +6 -0
- package/dist/command-installer.js +49 -0
- package/dist/command-installer.test.d.ts +1 -0
- package/dist/command-installer.test.js +58 -0
- package/dist/config-paths.d.ts +2 -0
- package/dist/config-paths.js +6 -0
- package/dist/config-paths.test.js +16 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +326 -259
- package/dist/index.test.js +3 -3
- package/dist/loaders.d.ts +2 -0
- package/dist/loaders.js +1 -0
- package/dist/session-children.d.ts +14 -0
- package/dist/session-children.js +27 -0
- package/dist/session-children.test.d.ts +1 -0
- package/dist/session-children.test.js +25 -0
- package/dist/skill-activation.js +1 -1
- package/dist/tools/agent-promote.d.ts +38 -11
- package/dist/tools/agent-promote.js +35 -33
- package/dist/tools/blockchain/eth-address-balance.d.ts +18 -8
- package/dist/tools/blockchain/eth-address-balance.js +15 -14
- package/dist/tools/blockchain/eth-address-txs.d.ts +22 -10
- package/dist/tools/blockchain/eth-address-txs.js +19 -18
- package/dist/tools/blockchain/eth-token-transfers.d.ts +22 -10
- package/dist/tools/blockchain/eth-token-transfers.js +19 -18
- package/dist/tools/blockchain/eth-transaction.d.ts +30 -14
- package/dist/tools/blockchain/eth-transaction.js +24 -21
- package/dist/tools/gitingest.d.ts +27 -15
- package/dist/tools/gitingest.js +28 -21
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/list-child-sessions.d.ts +11 -7
- package/dist/tools/list-child-sessions.js +22 -17
- package/dist/tools/pdf-to-markdown.d.ts +18 -8
- package/dist/tools/pdf-to-markdown.js +20 -13
- package/dist/tools/prompt-session.d.ts +26 -11
- package/dist/tools/prompt-session.js +23 -28
- package/package.json +13 -5
- package/tui.ts +24 -0
package/dist/index.js
CHANGED
|
@@ -1,298 +1,365 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { Plugin } from "@opencode/plugin";
|
|
4
|
+
import { loadAgents, loadHooks, loadSkills, mergeHooks, } from "./loaders";
|
|
5
|
+
import { getGlobalAgentDir, getGlobalCommandDir, getGlobalHookDir, getProjectHookDir } from "./config-paths";
|
|
5
6
|
import { hasCodeExtension } from "./code-files";
|
|
6
7
|
import { log } from "./logger";
|
|
7
8
|
import { executeBashAction, DEFAULT_BASH_TIMEOUT, } from "./bash-executor";
|
|
8
|
-
import { gitingestTool, pdfToMarkdownTool, createPromptSessionTool, createListChildSessionsTool, createAgentPromoteTool, getPromotedAgents, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
|
|
9
|
+
import { gitingestTool, pdfToMarkdownTool, createPromptSessionTool, createListChildSessionsTool, createAgentPromoteTool, setPromotedAgent, getPromotedAgents, AGENT_PROMOTE_STORAGE_KEY, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
|
|
10
|
+
import { buildSkillActivationBlock } from "./skill-activation";
|
|
11
|
+
import { installBundledFiles } from "./command-installer";
|
|
12
|
+
import { ChildSessionTracker } from "./session-children";
|
|
9
13
|
export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
|
|
10
14
|
export { buildSkillActivationBlock } from "./skill-activation";
|
|
11
|
-
import { buildSkillActivationBlock } from "./skill-activation";
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// CONSTANTS
|
|
14
|
-
// ============================================================================
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
const PLUGIN_ROOT = join(__dirname, "..");
|
|
18
18
|
const AGENT_DIR = join(PLUGIN_ROOT, "agent");
|
|
19
19
|
const COMMAND_DIR = join(PLUGIN_ROOT, "command");
|
|
20
20
|
const SKILL_DIR = join(PLUGIN_ROOT, "skill");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
21
|
+
const TRACKED_FILE_TOOLS = new Set(["write", "edit", "patch"]);
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
function readSessionID(event) {
|
|
29
|
+
const data = asRecord(asRecord(event).data);
|
|
30
|
+
const sessionID = data.sessionID;
|
|
31
|
+
return typeof sessionID === "string" ? sessionID : undefined;
|
|
32
|
+
}
|
|
33
|
+
function readParentID(event) {
|
|
34
|
+
const data = asRecord(asRecord(event).data);
|
|
35
|
+
const parentID = data.parentID;
|
|
36
|
+
return typeof parentID === "string" ? parentID : undefined;
|
|
37
|
+
}
|
|
38
|
+
function readTitle(event) {
|
|
39
|
+
const data = asRecord(asRecord(event).data);
|
|
40
|
+
const title = data.title;
|
|
41
|
+
return typeof title === "string" ? title : undefined;
|
|
42
|
+
}
|
|
43
|
+
export default Plugin.define({
|
|
44
|
+
id: "opencode-froggy",
|
|
45
|
+
async setup(ctx) {
|
|
46
|
+
const agents = loadAgents(AGENT_DIR);
|
|
47
|
+
const skills = loadSkills(SKILL_DIR);
|
|
48
|
+
const emptyInstall = { installed: [], skipped: [], updated: [] };
|
|
49
|
+
let installedCommands = { ...emptyInstall };
|
|
50
|
+
let installedAgents = { ...emptyInstall };
|
|
51
|
+
try {
|
|
52
|
+
installedCommands = installBundledFiles(COMMAND_DIR, getGlobalCommandDir());
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
log("[init] failed to install commands", { error: String(error) });
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
installedAgents = installBundledFiles(AGENT_DIR, getGlobalAgentDir());
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
log("[init] failed to install agents", { error: String(error) });
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await ctx.agent.reload();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
log("[init] failed to reload agents", { error: String(error) });
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await ctx.command.reload();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
log("[init] failed to reload commands", { error: String(error) });
|
|
74
|
+
}
|
|
75
|
+
const globalHooks = loadHooks(getGlobalHookDir());
|
|
76
|
+
const projectHooks = loadHooks(getProjectHookDir(ctx.location.directory));
|
|
77
|
+
const hooks = mergeHooks(globalHooks, projectHooks);
|
|
78
|
+
const modifiedCodeFiles = new Map();
|
|
79
|
+
const tracker = new ChildSessionTracker();
|
|
80
|
+
try {
|
|
81
|
+
const stored = await ctx.storage.get(AGENT_PROMOTE_STORAGE_KEY);
|
|
82
|
+
if (stored && typeof stored === "object" && !Array.isArray(stored)) {
|
|
83
|
+
for (const [name, mode] of Object.entries(stored)) {
|
|
84
|
+
if (mode === "primary" || mode === "subagent" || mode === "all") {
|
|
85
|
+
setPromotedAgent(name, mode);
|
|
86
|
+
}
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
catch (error) {
|
|
91
|
+
log("[init] failed to load promoted agents", { error: String(error) });
|
|
92
|
+
}
|
|
93
|
+
const skillsWithTriggers = skills.filter((s) => s.useWhen);
|
|
94
|
+
const skillActivationBlock = skillsWithTriggers.length > 0 ? buildSkillActivationBlock(skillsWithTriggers) : null;
|
|
95
|
+
log("[init] Plugin loaded", {
|
|
96
|
+
agents: Object.keys(agents),
|
|
97
|
+
commandsInstalled: installedCommands.installed,
|
|
98
|
+
commandsUpdated: installedCommands.updated,
|
|
99
|
+
agentsInstalled: installedAgents.installed,
|
|
100
|
+
agentsUpdated: installedAgents.updated,
|
|
101
|
+
skills: skills.map((s) => s.name),
|
|
102
|
+
skillsWithTriggers: skillsWithTriggers.map((s) => s.name),
|
|
103
|
+
hooks: Array.from(hooks.keys()),
|
|
104
|
+
tools: [
|
|
105
|
+
"gitingest",
|
|
106
|
+
"pdf-to-markdown",
|
|
107
|
+
"prompt-session",
|
|
108
|
+
"list-child-sessions",
|
|
109
|
+
"agent-promote",
|
|
110
|
+
"eth-transaction",
|
|
111
|
+
"eth-address-txs",
|
|
112
|
+
"eth-address-balance",
|
|
113
|
+
"eth-token-transfers",
|
|
114
|
+
],
|
|
78
115
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
command: name,
|
|
91
|
-
arguments: args,
|
|
92
|
-
agent,
|
|
93
|
-
model,
|
|
94
|
-
},
|
|
95
|
-
query: { directory: ctx.directory },
|
|
96
|
-
});
|
|
97
|
-
log(`${prefix} command result`, { command: name, status: result.response?.status, error: result.error });
|
|
98
|
-
}
|
|
99
|
-
else if ("tool" in action) {
|
|
100
|
-
log(`${prefix} executing tool`, { tool: action.tool.name });
|
|
101
|
-
const result = await ctx.client.session.prompt({
|
|
102
|
-
path: { id: sessionID },
|
|
103
|
-
body: { parts: [{ type: "text", text: `Use the ${action.tool.name} tool with these arguments: ${JSON.stringify(action.tool.args)}` }] },
|
|
104
|
-
query: { directory: ctx.directory },
|
|
105
|
-
});
|
|
106
|
-
log(`${prefix} tool result`, { tool: action.tool.name, status: result.response?.status, error: result.error });
|
|
107
|
-
}
|
|
108
|
-
else if ("bash" in action) {
|
|
109
|
-
const { command, timeout } = typeof action.bash === "string"
|
|
110
|
-
? { command: action.bash, timeout: DEFAULT_BASH_TIMEOUT }
|
|
111
|
-
: { command: action.bash.command, timeout: action.bash.timeout ?? DEFAULT_BASH_TIMEOUT };
|
|
112
|
-
const startTime = Date.now();
|
|
113
|
-
log(`${prefix} executing bash`, { command, timeout });
|
|
114
|
-
const bashContext = {
|
|
115
|
-
session_id: sessionID,
|
|
116
|
-
event: hook.event,
|
|
117
|
-
cwd: ctx.directory,
|
|
118
|
-
files: extraLog?.files,
|
|
119
|
-
tool_name: extraLog?.tool_name,
|
|
120
|
-
tool_args: extraLog?.tool_args,
|
|
121
|
-
};
|
|
122
|
-
const result = await executeBashAction(command, timeout, bashContext, ctx.directory);
|
|
123
|
-
const duration = Date.now() - startTime;
|
|
124
|
-
const statusIcon = result.exitCode === 0 ? "✓" : "✗";
|
|
125
|
-
const hookMessage = [
|
|
126
|
-
`[BASH HOOK ${statusIcon}] ${command}`,
|
|
127
|
-
`Exit: ${result.exitCode} | Duration: ${duration}ms`,
|
|
128
|
-
result.stdout.trim() ? `Stdout: ${result.stdout.slice(0, 500).trim()}` : null,
|
|
129
|
-
result.stderr.trim() ? `Stderr: ${result.stderr.slice(0, 500).trim()}` : null,
|
|
130
|
-
].filter(Boolean).join("\n");
|
|
131
|
-
await ctx.client.session.prompt({
|
|
132
|
-
path: { id: sessionID },
|
|
133
|
-
body: {
|
|
134
|
-
noReply: true,
|
|
135
|
-
parts: [{ type: "text", text: hookMessage }],
|
|
136
|
-
},
|
|
137
|
-
query: { directory: ctx.directory },
|
|
138
|
-
}).catch((err) => {
|
|
139
|
-
log(`${prefix} failed to send hook message`, { error: String(err) });
|
|
140
|
-
});
|
|
141
|
-
if (result.exitCode === 2) {
|
|
142
|
-
log(`${prefix} bash exit code 2`, { stderr: result.stderr, canBlock });
|
|
143
|
-
if (canBlock) {
|
|
144
|
-
const blockReason = result.stderr.trim() || "Blocked by hook";
|
|
145
|
-
return { blocked: true, blockReason };
|
|
116
|
+
async function executeHookActions(hook, sessionID, extraLog, options) {
|
|
117
|
+
const prefix = `[hook:${hook.event}]`;
|
|
118
|
+
const canBlock = options?.canBlock ?? false;
|
|
119
|
+
const conditions = hook.conditions ?? [];
|
|
120
|
+
for (const condition of conditions) {
|
|
121
|
+
if (condition === "isMainSession") {
|
|
122
|
+
try {
|
|
123
|
+
const sessionInfo = (await ctx.session.get({ sessionID }));
|
|
124
|
+
if (sessionInfo?.parentID) {
|
|
125
|
+
log(`${prefix} condition not met, skipping`, { sessionID, condition });
|
|
126
|
+
return { blocked: false };
|
|
146
127
|
}
|
|
147
|
-
return { blocked: false };
|
|
148
128
|
}
|
|
149
|
-
|
|
150
|
-
log(`${prefix}
|
|
129
|
+
catch (error) {
|
|
130
|
+
log(`${prefix} failed to check session, continuing`, { error: String(error) });
|
|
151
131
|
}
|
|
152
|
-
|
|
153
|
-
|
|
132
|
+
}
|
|
133
|
+
if (condition === "hasCodeChange") {
|
|
134
|
+
const files = extraLog?.files;
|
|
135
|
+
if (!files || !files.some(hasCodeExtension)) {
|
|
136
|
+
log(`${prefix} condition not met, skipping`, { sessionID, condition });
|
|
137
|
+
return { blocked: false };
|
|
154
138
|
}
|
|
155
139
|
}
|
|
156
140
|
}
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
log(`${prefix} starting`, {
|
|
142
|
+
sessionID,
|
|
143
|
+
conditions,
|
|
144
|
+
actions: hook.actions.length,
|
|
145
|
+
...extraLog,
|
|
146
|
+
});
|
|
147
|
+
for (const action of hook.actions) {
|
|
148
|
+
try {
|
|
149
|
+
if ("command" in action) {
|
|
150
|
+
const { name, args = "" } = typeof action.command === "string" ? { name: action.command } : action.command;
|
|
151
|
+
log(`${prefix} executing command`, { command: name, args });
|
|
152
|
+
await ctx.session.command({ sessionID, command: name, text: args });
|
|
153
|
+
}
|
|
154
|
+
else if ("tool" in action) {
|
|
155
|
+
log(`${prefix} executing tool`, { tool: action.tool.name });
|
|
156
|
+
await ctx.session.prompt({
|
|
157
|
+
sessionID,
|
|
158
|
+
text: `Use the ${action.tool.name} tool with these arguments: ${JSON.stringify(action.tool.args)}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else if ("bash" in action) {
|
|
162
|
+
const { command, timeout } = typeof action.bash === "string"
|
|
163
|
+
? { command: action.bash, timeout: DEFAULT_BASH_TIMEOUT }
|
|
164
|
+
: { command: action.bash.command, timeout: action.bash.timeout ?? DEFAULT_BASH_TIMEOUT };
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
log(`${prefix} executing bash`, { command, timeout });
|
|
167
|
+
const bashContext = {
|
|
168
|
+
session_id: sessionID,
|
|
169
|
+
event: hook.event,
|
|
170
|
+
cwd: ctx.location.directory,
|
|
171
|
+
files: extraLog?.files,
|
|
172
|
+
tool_name: extraLog?.tool_name,
|
|
173
|
+
tool_args: extraLog?.tool_args,
|
|
174
|
+
};
|
|
175
|
+
const result = await executeBashAction(command, timeout, bashContext, ctx.location.directory);
|
|
176
|
+
const duration = Date.now() - startTime;
|
|
177
|
+
const statusIcon = result.exitCode === 0 ? "✓" : "✗";
|
|
178
|
+
const hookMessage = [
|
|
179
|
+
`[BASH HOOK ${statusIcon}] ${command}`,
|
|
180
|
+
`Exit: ${result.exitCode} | Duration: ${duration}ms`,
|
|
181
|
+
result.stdout.trim() ? `Stdout: ${result.stdout.slice(0, 500).trim()}` : null,
|
|
182
|
+
result.stderr.trim() ? `Stderr: ${result.stderr.slice(0, 500).trim()}` : null,
|
|
183
|
+
]
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join("\n");
|
|
186
|
+
await ctx.session
|
|
187
|
+
.synthetic({ sessionID, text: hookMessage })
|
|
188
|
+
.catch((err) => {
|
|
189
|
+
log(`${prefix} failed to send hook message`, { error: String(err) });
|
|
190
|
+
});
|
|
191
|
+
if (result.exitCode === 2) {
|
|
192
|
+
log(`${prefix} bash exit code 2`, { stderr: result.stderr, canBlock });
|
|
193
|
+
if (canBlock) {
|
|
194
|
+
const blockReason = result.stderr.trim() || "Blocked by hook";
|
|
195
|
+
return { blocked: true, blockReason };
|
|
196
|
+
}
|
|
197
|
+
return { blocked: false };
|
|
198
|
+
}
|
|
199
|
+
if (result.exitCode !== 0) {
|
|
200
|
+
log(`${prefix} bash failed (non-blocking)`, {
|
|
201
|
+
exitCode: result.exitCode,
|
|
202
|
+
stderr: result.stderr,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
log(`${prefix} bash completed`, { stdout: result.stdout.slice(0, 200) });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
log(`${prefix} action failed, continuing`, { error: String(error) });
|
|
212
|
+
}
|
|
159
213
|
}
|
|
160
|
-
|
|
161
|
-
log(`${prefix} completed`);
|
|
162
|
-
return { blocked: false };
|
|
163
|
-
}
|
|
164
|
-
async function triggerHooks(event, sessionID, extraLog, options) {
|
|
165
|
-
const eventHooks = hooks.get(event);
|
|
166
|
-
if (!eventHooks)
|
|
214
|
+
log(`${prefix} completed`);
|
|
167
215
|
return { blocked: false };
|
|
168
|
-
for (const hook of eventHooks) {
|
|
169
|
-
const result = await executeHookActions(hook, sessionID, extraLog, options);
|
|
170
|
-
if (result.blocked)
|
|
171
|
-
return result;
|
|
172
216
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return wildcardResult;
|
|
182
|
-
const specificEvent = `tool.${phase}.${toolName}`;
|
|
183
|
-
const specificResult = await triggerHooks(specificEvent, sessionID, extraLog, { canBlock });
|
|
184
|
-
return specificResult;
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
config: async (config) => {
|
|
188
|
-
const loadedAgents = loadAgents(AGENT_DIR);
|
|
189
|
-
for (const [name, mode] of getPromotedAgents()) {
|
|
190
|
-
if (loadedAgents[name]) {
|
|
191
|
-
loadedAgents[name].mode = mode;
|
|
192
|
-
}
|
|
217
|
+
async function triggerHooks(event, sessionID, extraLog, options) {
|
|
218
|
+
const eventHooks = hooks.get(event);
|
|
219
|
+
if (!eventHooks)
|
|
220
|
+
return { blocked: false };
|
|
221
|
+
for (const hook of eventHooks) {
|
|
222
|
+
const result = await executeHookActions(hook, sessionID, extraLog, options);
|
|
223
|
+
if (result.blocked)
|
|
224
|
+
return result;
|
|
193
225
|
}
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
return { blocked: false };
|
|
227
|
+
}
|
|
228
|
+
async function triggerToolHooks(phase, toolName, sessionID, toolArgs) {
|
|
229
|
+
const canBlock = phase === "before";
|
|
230
|
+
const extraLog = { tool_name: toolName, tool_args: toolArgs };
|
|
231
|
+
const wildcardEvent = `tool.${phase}.*`;
|
|
232
|
+
const wildcardResult = await triggerHooks(wildcardEvent, sessionID, extraLog, { canBlock });
|
|
233
|
+
if (wildcardResult.blocked)
|
|
234
|
+
return wildcardResult;
|
|
235
|
+
const specificEvent = `tool.${phase}.${toolName}`;
|
|
236
|
+
return triggerHooks(specificEvent, sessionID, extraLog, { canBlock });
|
|
237
|
+
}
|
|
238
|
+
function trackModifiedFile(sessionID, toolName, toolArgs) {
|
|
239
|
+
if (!TRACKED_FILE_TOOLS.has(toolName))
|
|
240
|
+
return;
|
|
241
|
+
const filePath = (toolArgs.filePath ?? toolArgs.file_path ?? toolArgs.path);
|
|
242
|
+
if (!filePath)
|
|
243
|
+
return;
|
|
244
|
+
log("[tool.execute.before] File modified", { sessionID, filePath, tool: toolName });
|
|
245
|
+
let files = modifiedCodeFiles.get(sessionID);
|
|
246
|
+
if (!files) {
|
|
247
|
+
files = new Set();
|
|
248
|
+
modifiedCodeFiles.set(sessionID, files);
|
|
196
249
|
}
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
files.add(filePath);
|
|
251
|
+
}
|
|
252
|
+
await ctx.agent.transform((editor) => {
|
|
253
|
+
for (const [name, mode] of getPromotedAgents()) {
|
|
254
|
+
if (!editor.get(name))
|
|
255
|
+
continue;
|
|
256
|
+
editor.update(name, (agent) => {
|
|
257
|
+
agent.mode = mode;
|
|
258
|
+
});
|
|
199
259
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
260
|
+
});
|
|
261
|
+
await ctx.skill.transform((editor) => {
|
|
262
|
+
for (const skill of skills) {
|
|
263
|
+
editor.add({
|
|
264
|
+
id: skill.name,
|
|
265
|
+
name: skill.name,
|
|
266
|
+
description: skill.description || undefined,
|
|
267
|
+
location: skill.path,
|
|
268
|
+
content: skill.body,
|
|
269
|
+
});
|
|
209
270
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
271
|
+
});
|
|
272
|
+
const promptSessionTool = createPromptSessionTool(ctx.session, tracker);
|
|
273
|
+
const listChildSessionsTool = createListChildSessionsTool(tracker);
|
|
274
|
+
const agentPromoteTool = createAgentPromoteTool(ctx.agent, ctx.agent, ctx.storage, Object.keys(agents));
|
|
275
|
+
await ctx.tool.transform((editor) => {
|
|
276
|
+
editor.add(gitingestTool);
|
|
277
|
+
editor.add(pdfToMarkdownTool);
|
|
278
|
+
editor.add(promptSessionTool);
|
|
279
|
+
editor.add(listChildSessionsTool);
|
|
280
|
+
editor.add(agentPromoteTool);
|
|
281
|
+
editor.add(ethTransactionTool);
|
|
282
|
+
editor.add(ethAddressTxsTool);
|
|
283
|
+
editor.add(ethAddressBalanceTool);
|
|
284
|
+
editor.add(ethTokenTransfersTool);
|
|
285
|
+
});
|
|
286
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
287
|
+
if (!event.sessionID)
|
|
225
288
|
return;
|
|
226
|
-
const toolArgs =
|
|
227
|
-
|
|
228
|
-
const result = await triggerToolHooks("before", input.tool, sessionID, toolArgs);
|
|
289
|
+
const toolArgs = asRecord(event.input);
|
|
290
|
+
const result = await triggerToolHooks("before", event.tool, event.sessionID, toolArgs);
|
|
229
291
|
if (result.blocked) {
|
|
230
|
-
pendingToolArgs.delete(input.callID);
|
|
231
292
|
throw new Error(result.blockReason ?? "Blocked by hook");
|
|
232
293
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let files = modifiedCodeFiles.get(sessionID);
|
|
238
|
-
if (!files) {
|
|
239
|
-
files = new Set();
|
|
240
|
-
modifiedCodeFiles.set(sessionID, files);
|
|
241
|
-
}
|
|
242
|
-
files.add(filePath);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
"tool.execute.after": async (input, _output) => {
|
|
247
|
-
const sessionID = input.sessionID;
|
|
248
|
-
if (!sessionID)
|
|
294
|
+
trackModifiedFile(event.sessionID, event.tool, toolArgs);
|
|
295
|
+
});
|
|
296
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
297
|
+
if (!event.sessionID)
|
|
249
298
|
return;
|
|
250
|
-
const toolArgs =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
299
|
+
const toolArgs = asRecord(event.input);
|
|
300
|
+
await triggerToolHooks("after", event.tool, event.sessionID, toolArgs);
|
|
301
|
+
});
|
|
302
|
+
if (skillActivationBlock) {
|
|
303
|
+
await ctx.session.hook("context", (event) => {
|
|
304
|
+
event.system.push({ type: "text", text: skillActivationBlock });
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
void (async () => {
|
|
309
|
+
try {
|
|
310
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
311
|
+
const record = event;
|
|
312
|
+
if (record.type === "session.created") {
|
|
313
|
+
const sessionID = readSessionID(event);
|
|
314
|
+
if (!sessionID)
|
|
315
|
+
continue;
|
|
316
|
+
const parentID = readParentID(event);
|
|
317
|
+
if (parentID) {
|
|
318
|
+
tracker.trackChild({
|
|
319
|
+
id: sessionID,
|
|
320
|
+
parentID,
|
|
321
|
+
title: readTitle(event),
|
|
322
|
+
created: Date.now(),
|
|
323
|
+
updated: Date.now(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
log("[event] session.created - main session", { sessionID });
|
|
328
|
+
}
|
|
329
|
+
await triggerHooks("session.created", sessionID);
|
|
330
|
+
}
|
|
331
|
+
if (record.type === "session.deleted") {
|
|
332
|
+
const sessionID = readSessionID(event);
|
|
333
|
+
if (!sessionID)
|
|
334
|
+
continue;
|
|
335
|
+
log("[event] session.deleted", { sessionID });
|
|
336
|
+
await triggerHooks("session.deleted", sessionID);
|
|
337
|
+
modifiedCodeFiles.delete(sessionID);
|
|
338
|
+
tracker.removeSession(sessionID);
|
|
339
|
+
}
|
|
340
|
+
if (record.type === "session.idle") {
|
|
341
|
+
const sessionID = readSessionID(event);
|
|
342
|
+
if (!sessionID)
|
|
343
|
+
continue;
|
|
344
|
+
log("[event] session.idle", { sessionID });
|
|
345
|
+
if (!hooks.has("session.idle")) {
|
|
346
|
+
log("[event] session.idle - no hooks defined, skipping");
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const files = modifiedCodeFiles.get(sessionID);
|
|
350
|
+
modifiedCodeFiles.delete(sessionID);
|
|
351
|
+
await triggerHooks("session.idle", sessionID, {
|
|
352
|
+
files: files ? Array.from(files) : [],
|
|
353
|
+
});
|
|
354
|
+
}
|
|
263
355
|
}
|
|
264
|
-
await triggerHooks("session.created", sessionID);
|
|
265
356
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (!sessionID)
|
|
270
|
-
return;
|
|
271
|
-
log("[event] session.deleted", { sessionID });
|
|
272
|
-
await triggerHooks("session.deleted", sessionID);
|
|
273
|
-
modifiedCodeFiles.delete(sessionID);
|
|
274
|
-
}
|
|
275
|
-
if (event.type === "session.idle") {
|
|
276
|
-
const sessionID = props?.sessionID;
|
|
277
|
-
if (!sessionID)
|
|
278
|
-
return;
|
|
279
|
-
log("[event] session.idle", { sessionID });
|
|
280
|
-
if (!hooks.has("session.idle")) {
|
|
281
|
-
log("[event] session.idle - no hooks defined, skipping");
|
|
282
|
-
return;
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (error?.name !== "AbortError") {
|
|
359
|
+
log("[event] subscription ended", { error: String(error) });
|
|
283
360
|
}
|
|
284
|
-
const files = modifiedCodeFiles.get(sessionID);
|
|
285
|
-
modifiedCodeFiles.delete(sessionID);
|
|
286
|
-
await triggerHooks("session.idle", sessionID, { files: files ? Array.from(files) : [] });
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
290
|
-
// The activation block relies on OpenCode's native `skill` tool after we
|
|
291
|
-
// expose the plugin's bundled skills through `config.skills.paths`.
|
|
292
|
-
if (skillActivationBlock) {
|
|
293
|
-
output.system.push(skillActivationBlock);
|
|
294
361
|
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
362
|
+
})();
|
|
363
|
+
return () => controller.abort();
|
|
364
|
+
},
|
|
365
|
+
});
|
package/dist/index.test.js
CHANGED
|
@@ -822,7 +822,7 @@ describe("buildSkillActivationBlock", () => {
|
|
|
822
822
|
];
|
|
823
823
|
const result = buildSkillActivationBlock(skills);
|
|
824
824
|
expect(result).toContain("MANDATORY");
|
|
825
|
-
expect(result).toContain('skill({
|
|
825
|
+
expect(result).toContain('skill({ id: "code-review" })');
|
|
826
826
|
expect(result).toContain("After writing code");
|
|
827
827
|
});
|
|
828
828
|
it("should preserve quotes in trigger text", () => {
|
|
@@ -871,8 +871,8 @@ describe("buildSkillActivationBlock", () => {
|
|
|
871
871
|
{ name: "skill-b", description: "B", useWhen: "Trigger B", path: "/b", body: "" },
|
|
872
872
|
];
|
|
873
873
|
const result = buildSkillActivationBlock(skills);
|
|
874
|
-
expect(result).toContain('skill({
|
|
875
|
-
expect(result).toContain('skill({
|
|
874
|
+
expect(result).toContain('skill({ id: "skill-a" })');
|
|
875
|
+
expect(result).toContain('skill({ id: "skill-b" })');
|
|
876
876
|
expect(result).toContain("Trigger A");
|
|
877
877
|
expect(result).toContain("Trigger B");
|
|
878
878
|
expect(result.match(/MANDATORY/g)).toHaveLength(2);
|