Skip to content

Commit 5173601

Browse files
committed
feat(plugin): add lifecycle hooks with debug logging support
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent d090efa commit 5173601

File tree

2 files changed

+115
-28
lines changed

2 files changed

+115
-28
lines changed

src/plugin.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,69 @@
88
* does not currently support dynamic agent registration.
99
*/
1010

11-
import type { Plugin } from "@opencode-ai/plugin"
11+
import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin"
12+
13+
/** Plugin metadata for logging */
14+
const PLUGIN_NAME = "opencoder"
15+
16+
/**
17+
* Creates lifecycle hooks for debugging and visibility.
18+
*
19+
* These hooks provide optional logging points for plugin activity.
20+
* Set OPENCODER_DEBUG=1 environment variable to enable debug logging.
21+
*
22+
* @param ctx - Plugin context from OpenCode
23+
* @returns Hooks object with lifecycle callbacks
24+
*/
25+
function createLifecycleHooks(ctx: PluginInput): Hooks {
26+
const debug = process.env.OPENCODER_DEBUG === "1"
27+
28+
const log = (message: string, data?: Record<string, unknown>) => {
29+
if (debug) {
30+
const timestamp = new Date().toISOString()
31+
const prefix = `[${timestamp}] [${PLUGIN_NAME}]`
32+
const context = { directory: ctx.directory, ...data }
33+
console.log(prefix, message, JSON.stringify(context, null, 2))
34+
}
35+
}
36+
37+
return {
38+
/**
39+
* Called on OpenCode events (sessions, messages, etc.)
40+
*/
41+
event: async ({ event }) => {
42+
log("Event received", {
43+
type: event.type,
44+
properties: Object.keys(event.properties),
45+
})
46+
},
47+
48+
/**
49+
* Called before tool execution
50+
*/
51+
"tool.execute.before": async ({ tool, sessionID, callID }, output) => {
52+
log("Tool executing", {
53+
tool,
54+
sessionID,
55+
callID,
56+
argsKeys: Object.keys(output.args || {}),
57+
})
58+
},
59+
60+
/**
61+
* Called after tool execution completes
62+
*/
63+
"tool.execute.after": async ({ tool, sessionID, callID }, output) => {
64+
log("Tool completed", {
65+
tool,
66+
sessionID,
67+
callID,
68+
title: output.title,
69+
outputLength: output.output?.length ?? 0,
70+
})
71+
},
72+
}
73+
}
1274

1375
/**
1476
* The OpenCoder plugin function.
@@ -22,10 +84,8 @@ import type { Plugin } from "@opencode-ai/plugin"
2284
* opencode @opencoder
2385
*
2486
* @param ctx - Plugin context provided by OpenCode
25-
* @returns Hooks object for event subscriptions (minimal for now)
87+
* @returns Hooks object with lifecycle callbacks for debugging visibility
2688
*/
27-
export const OpenCoderPlugin: Plugin = async (_ctx) => {
28-
// Return minimal hooks object
29-
// Can be extended with event handlers in the future
30-
return {}
89+
export const OpenCoderPlugin: Plugin = async (ctx) => {
90+
return createLifecycleHooks(ctx)
3191
}

tests/plugin.test.ts

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,66 @@ import type { PluginInput } from "@opencode-ai/plugin"
33
import { OpenCoderPlugin } from "../src/plugin"
44

55
describe("OpenCoderPlugin", () => {
6-
it("should be an async function", () => {
7-
expect(OpenCoderPlugin).toBeInstanceOf(Function)
8-
})
9-
10-
it("should return a hooks object when called", async () => {
11-
// Create a mock context (minimal implementation for testing)
12-
const mockContext = {
6+
// Create a mock context (minimal implementation for testing)
7+
const createMockContext = () =>
8+
({
139
project: {},
1410
client: {},
1511
$: () => {},
16-
directory: "/tmp",
17-
worktree: "/tmp",
12+
directory: "/tmp/test-project",
13+
worktree: "/tmp/test-project",
1814
serverUrl: new URL("http://localhost:3000"),
19-
} as unknown as PluginInput
15+
}) as unknown as PluginInput
16+
17+
it("should be an async function", () => {
18+
expect(OpenCoderPlugin).toBeInstanceOf(Function)
19+
})
2020

21-
const result = await OpenCoderPlugin(mockContext)
21+
it("should return a hooks object when called", async () => {
22+
const result = await OpenCoderPlugin(createMockContext())
2223

2324
expect(result).toBeDefined()
2425
expect(typeof result).toBe("object")
2526
})
2627

27-
it("should return an empty hooks object (minimal implementation)", async () => {
28-
const mockContext = {
29-
project: {},
30-
client: {},
31-
$: () => {},
32-
directory: "/tmp",
33-
worktree: "/tmp",
34-
serverUrl: new URL("http://localhost:3000"),
35-
} as unknown as PluginInput
28+
it("should return hooks object with lifecycle callbacks", async () => {
29+
const result = await OpenCoderPlugin(createMockContext())
30+
31+
// Verify expected hooks are present
32+
expect(result.event).toBeDefined()
33+
expect(typeof result.event).toBe("function")
34+
expect(result["tool.execute.before"]).toBeDefined()
35+
expect(typeof result["tool.execute.before"]).toBe("function")
36+
expect(result["tool.execute.after"]).toBeDefined()
37+
expect(typeof result["tool.execute.after"]).toBe("function")
38+
})
39+
40+
it("should have callable event hook", async () => {
41+
const result = await OpenCoderPlugin(createMockContext())
42+
43+
// Event hook should be callable without throwing
44+
const mockEvent = {
45+
type: "session.created" as const,
46+
properties: { sessionID: "test-123" },
47+
}
48+
await expect(result.event?.({ event: mockEvent as any })).resolves.toBeUndefined()
49+
})
50+
51+
it("should have callable tool.execute.before hook", async () => {
52+
const result = await OpenCoderPlugin(createMockContext())
53+
54+
const input = { tool: "bash", sessionID: "test-123", callID: "call-456" }
55+
const output = { args: { command: "ls" } }
56+
57+
await expect(result["tool.execute.before"]?.(input, output)).resolves.toBeUndefined()
58+
})
59+
60+
it("should have callable tool.execute.after hook", async () => {
61+
const result = await OpenCoderPlugin(createMockContext())
3662

37-
const result = await OpenCoderPlugin(mockContext)
63+
const input = { tool: "bash", sessionID: "test-123", callID: "call-456" }
64+
const output = { title: "Command executed", output: "file1.txt\nfile2.txt", metadata: {} }
3865

39-
expect(Object.keys(result)).toHaveLength(0)
66+
await expect(result["tool.execute.after"]?.(input, output)).resolves.toBeUndefined()
4067
})
4168
})

0 commit comments

Comments
 (0)