agentful 0.1.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.
@@ -0,0 +1,360 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import * as fs from "fs"
3
+ import * as path from "path"
4
+ import * as crypto from "crypto"
5
+
6
+ /**
7
+ * imagegen — generate a project-owned image asset via the LLM Gateway.
8
+ *
9
+ * The gateway (POST /v1/images/generations) owns provider/model resolution,
10
+ * data-residency enforcement (air claim), key handling and usage tracking.
11
+ * This tool only validates the output path, calls the gateway with the
12
+ * session token, writes the bytes into the workspace and reports the
13
+ * relative path. The workspace file is the source of truth — no base64 in
14
+ * the tool output, no attachments (keeps WebSocket events and chat
15
+ * persistence small).
16
+ */
17
+
18
+ const GATEWAY_URL = (process.env.LLM_GATEWAY_URL || "").replace(/\/+$/, "")
19
+ const SESSION_TOKEN = process.env.SESSION_TOKEN || ""
20
+ const MAX_PER_SESSION = intEnv("IMAGEGEN_MAX_PER_SESSION", 3)
21
+ const MAX_IMAGE_BYTES = intEnv("IMAGEGEN_MAX_BYTES", 8 * 1024 * 1024)
22
+ const REQUEST_TIMEOUT_MS = intEnv("IMAGEGEN_TIMEOUT_MS", 120_000)
23
+ const MAX_PROMPT_CHARS = 2_000
24
+
25
+ const ALLOWED_EXTENSIONS: Record<string, string> = {
26
+ ".png": "image/png",
27
+ ".webp": "image/webp",
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ }
31
+
32
+ const SIZE_BY_ASPECT: Record<string, string> = {
33
+ "1:1": "1024x1024",
34
+ "4:3": "1408x1056",
35
+ "3:2": "1536x1024",
36
+ "16:9": "1792x1024",
37
+ "9:16": "1024x1792",
38
+ "21:9": "2016x864",
39
+ }
40
+
41
+ function intEnv(name: string, fallback: number): number {
42
+ const raw = Number(process.env[name])
43
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback
44
+ }
45
+
46
+ function slugify(text: string): string {
47
+ const slug = text
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, "-")
50
+ .replace(/^-+|-+$/g, "")
51
+ .slice(0, 48)
52
+ return slug || "image"
53
+ }
54
+
55
+ /**
56
+ * Resolve and validate the output path. The model-supplied value is
57
+ * untrusted input: reject anything that is absolute, traverses out of the
58
+ * workspace, targets hidden/config/tooling paths or a non-image extension.
59
+ * Symlink escapes are caught by realpath-checking the deepest existing
60
+ * ancestor before any directory is created.
61
+ */
62
+ function resolveOutputPath(
63
+ workspaceRoot: string,
64
+ requested: string | undefined,
65
+ defaultRelPath: string,
66
+ extension: string
67
+ ): { absPath: string; relPath: string } {
68
+ const rel = (requested || defaultRelPath).trim()
69
+
70
+ if (!rel) throw new Error("outputPath is empty")
71
+ if (path.isAbsolute(rel)) {
72
+ throw new Error(`outputPath must be relative to the project root: ${rel}`)
73
+ }
74
+
75
+ const normalized = path.normalize(rel)
76
+ const segments = normalized.split(path.sep)
77
+ if (segments.some((seg) => seg === "..")) {
78
+ throw new Error(`outputPath must not traverse outside the project: ${rel}`)
79
+ }
80
+ if (segments.some((seg) => seg.startsWith("."))) {
81
+ throw new Error(`outputPath must not target hidden or config paths: ${rel}`)
82
+ }
83
+ if (segments.includes("node_modules")) {
84
+ throw new Error(`outputPath must not target node_modules: ${rel}`)
85
+ }
86
+
87
+ const ext = path.extname(normalized).toLowerCase()
88
+ if (!(ext in ALLOWED_EXTENSIONS)) {
89
+ throw new Error(
90
+ `outputPath must end in ${Object.keys(ALLOWED_EXTENSIONS).join("/")}: ${rel}`
91
+ )
92
+ }
93
+ if (ext !== extension && !(ext === ".jpeg" && extension === ".jpg")) {
94
+ throw new Error(
95
+ `outputPath extension ${ext} does not match requested format ${extension}`
96
+ )
97
+ }
98
+
99
+ const rootResolved = fs.realpathSync(workspaceRoot)
100
+ const absPath = path.resolve(rootResolved, normalized)
101
+ if (!absPath.startsWith(rootResolved + path.sep)) {
102
+ throw new Error(`outputPath resolves outside the project: ${rel}`)
103
+ }
104
+
105
+ // Symlink containment: realpath the deepest existing ancestor and require
106
+ // it to still be inside the workspace before creating any directories.
107
+ let ancestor = path.dirname(absPath)
108
+ while (!fs.existsSync(ancestor)) ancestor = path.dirname(ancestor)
109
+ const ancestorReal = fs.realpathSync(ancestor)
110
+ if (
111
+ ancestorReal !== rootResolved &&
112
+ !ancestorReal.startsWith(rootResolved + path.sep)
113
+ ) {
114
+ throw new Error(`outputPath resolves outside the project: ${rel}`)
115
+ }
116
+
117
+ return { absPath, relPath: path.relative(rootResolved, absPath) }
118
+ }
119
+
120
+ /**
121
+ * public/ and static/ are only served from the site root by bundlers
122
+ * (Vite/Next/Astro/SvelteKit/Nuxt). A vanilla HTML project has no build
123
+ * step — there public/ is just a folder and must be referenced by its real
124
+ * relative path (prod finding 2026-07-10: /landscape.png 404'd in a vanilla
125
+ * project because the hint assumed framework semantics).
126
+ */
127
+ function isBundlerProject(workspaceRoot: string): boolean {
128
+ const markers = [
129
+ "package.json",
130
+ "vite.config.js", "vite.config.ts", "vite.config.mjs",
131
+ "astro.config.mjs", "astro.config.ts",
132
+ "next.config.js", "next.config.ts", "next.config.mjs",
133
+ "svelte.config.js", "nuxt.config.ts",
134
+ ]
135
+ return markers.some((marker) => fs.existsSync(path.join(workspaceRoot, marker)))
136
+ }
137
+
138
+ function defaultAssetDir(workspaceRoot: string): string {
139
+ if (isBundlerProject(workspaceRoot)) {
140
+ if (fs.existsSync(path.join(workspaceRoot, "static"))) return "static/generated"
141
+ return "public/generated"
142
+ }
143
+ return "assets/generated"
144
+ }
145
+
146
+ function sessionCountFile(sessionID: string): string {
147
+ const safe = sessionID.replace(/[^a-zA-Z0-9_-]/g, "_")
148
+ return path.join("/tmp", `imagegen-count-${safe}`)
149
+ }
150
+
151
+ function readSessionCount(sessionID: string): number {
152
+ try {
153
+ return parseInt(fs.readFileSync(sessionCountFile(sessionID), "utf8"), 10) || 0
154
+ } catch {
155
+ return 0
156
+ }
157
+ }
158
+
159
+ function writeSessionCount(sessionID: string, count: number): void {
160
+ try {
161
+ fs.writeFileSync(sessionCountFile(sessionID), String(count))
162
+ } catch {
163
+ // Counter is best-effort; generation itself already succeeded.
164
+ }
165
+ }
166
+
167
+ export default tool({
168
+ description:
169
+ "Generate an image asset (hero, illustration, texture, product mockup) and save it into the project. " +
170
+ "Returns the saved project path. Vanilla projects must resolve that path relative to the consuming " +
171
+ "HTML/CSS/JS file; bundler public/static assets get a site-root URL. " +
172
+ "Do not use for real people, real venues, logos or brand assets.",
173
+ args: {
174
+ prompt: tool.schema
175
+ .string()
176
+ .describe("Detailed visual description of the image to generate"),
177
+ outputPath: tool.schema
178
+ .string()
179
+ .optional()
180
+ .describe(
181
+ "Relative output path inside the project, e.g. public/generated/hero.webp. " +
182
+ "Extension must match format. Defaults to a generated path."
183
+ ),
184
+ purpose: tool.schema
185
+ .enum(["hero", "card", "background", "icon", "texture", "product", "illustration"])
186
+ .optional()
187
+ .describe("What the image is used for (influences default naming)"),
188
+ aspectRatio: tool.schema
189
+ .enum(["1:1", "4:3", "3:2", "16:9", "9:16", "21:9"])
190
+ .optional()
191
+ .describe("Aspect ratio of the image (default 3:2)"),
192
+ format: tool.schema
193
+ .enum(["png", "webp", "jpeg"])
194
+ .optional()
195
+ .describe("Output format (default webp; use png for transparency)"),
196
+ transparent: tool.schema
197
+ .boolean()
198
+ .optional()
199
+ .describe("Request a transparent background (png/webp only)"),
200
+ },
201
+ async execute(args, context) {
202
+ if (!GATEWAY_URL || !SESSION_TOKEN) {
203
+ throw new Error(
204
+ "Image generation is not configured for this session (no gateway credentials). " +
205
+ "Use CSS/SVG placeholders instead."
206
+ )
207
+ }
208
+
209
+ const prompt = (args.prompt || "").trim()
210
+ if (!prompt) throw new Error("prompt must not be empty")
211
+ if (prompt.length > MAX_PROMPT_CHARS) {
212
+ throw new Error(`prompt too long (${prompt.length} > ${MAX_PROMPT_CHARS} chars)`)
213
+ }
214
+
215
+ const used = readSessionCount(context.sessionID)
216
+ if (used >= MAX_PER_SESSION) {
217
+ throw new Error(
218
+ `Image limit reached for this run (${MAX_PER_SESSION}). ` +
219
+ "Reuse already generated assets or CSS/SVG placeholders."
220
+ )
221
+ }
222
+
223
+ const format = args.format || "webp"
224
+ if (args.transparent && format === "jpeg") {
225
+ throw new Error("transparent backgrounds require png or webp format")
226
+ }
227
+ const extension = format === "jpeg" ? ".jpg" : `.${format}`
228
+ const size = SIZE_BY_ASPECT[args.aspectRatio || "3:2"]
229
+
230
+ const workspaceRoot = context.directory
231
+ const hash = crypto.createHash("sha1").update(prompt).digest("hex").slice(0, 8)
232
+ const baseName = slugify(args.purpose ? `${args.purpose}-${prompt}` : prompt)
233
+ const defaultRelPath = path.join(
234
+ defaultAssetDir(workspaceRoot),
235
+ `${baseName}-${hash}${extension}`
236
+ )
237
+ const { absPath, relPath } = resolveOutputPath(
238
+ workspaceRoot,
239
+ args.outputPath,
240
+ defaultRelPath,
241
+ extension
242
+ )
243
+
244
+ const startedAt = Date.now()
245
+ let response: Response
246
+ try {
247
+ response = await fetch(`${GATEWAY_URL}/v1/images/generations`, {
248
+ method: "POST",
249
+ headers: {
250
+ "Content-Type": "application/json",
251
+ Authorization: `Bearer ${SESSION_TOKEN}`,
252
+ },
253
+ body: JSON.stringify({
254
+ model: process.env.IMAGE_MODEL || "auto",
255
+ prompt,
256
+ size,
257
+ response_format: "b64_json",
258
+ output_format: format,
259
+ ...(args.transparent ? { background: "transparent" } : {}),
260
+ }),
261
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
262
+ })
263
+ } catch (error) {
264
+ throw new Error(`Image generation request failed: ${error}`)
265
+ }
266
+
267
+ if (!response.ok) {
268
+ let message = `${response.status}`
269
+ try {
270
+ const body = (await response.json()) as {
271
+ error?: { message?: string } | string
272
+ }
273
+ const err = body?.error
274
+ message =
275
+ typeof err === "string" ? err : err?.message || JSON.stringify(body).slice(0, 300)
276
+ } catch {
277
+ // keep status-only message
278
+ }
279
+ throw new Error(`Image generation failed: ${message}`)
280
+ }
281
+
282
+ const result = (await response.json()) as {
283
+ data?: Array<{ b64_json?: string }>
284
+ model?: string
285
+ provider?: string
286
+ mime?: string
287
+ }
288
+ const b64 = result.data?.[0]?.b64_json
289
+ if (!b64) {
290
+ throw new Error("Image generation returned no image data")
291
+ }
292
+
293
+ const bytes = Buffer.from(b64, "base64")
294
+ if (bytes.length === 0) {
295
+ throw new Error("Image generation returned empty image data")
296
+ }
297
+ if (bytes.length > MAX_IMAGE_BYTES) {
298
+ throw new Error(
299
+ `Generated image too large (${bytes.length} bytes > ${MAX_IMAGE_BYTES})`
300
+ )
301
+ }
302
+
303
+ // Providers do not always honor the requested format (e.g. Gemini always
304
+ // returns PNG). The gateway reports the actual mime; keep the file
305
+ // extension truthful by swapping it within the image-extension allowlist.
306
+ let finalAbsPath = absPath
307
+ let finalRelPath = relPath
308
+ let finalMime = ALLOWED_EXTENSIONS[extension]
309
+ const reportedMime = result.mime
310
+ if (reportedMime && reportedMime !== finalMime) {
311
+ const actualExt = Object.keys(ALLOWED_EXTENSIONS).find(
312
+ (candidate) => ALLOWED_EXTENSIONS[candidate] === reportedMime && candidate !== ".jpeg"
313
+ )
314
+ if (actualExt) {
315
+ finalAbsPath = absPath.slice(0, -path.extname(absPath).length) + actualExt
316
+ finalRelPath = relPath.slice(0, -path.extname(relPath).length) + actualExt
317
+ finalMime = reportedMime
318
+ }
319
+ }
320
+
321
+ fs.mkdirSync(path.dirname(finalAbsPath), { recursive: true })
322
+ fs.writeFileSync(finalAbsPath, bytes)
323
+ writeSessionCount(context.sessionID, used + 1)
324
+
325
+ // Bundler projects serve public/ and static/ from the site root. Vanilla
326
+ // projects are served as-is, so there is no single drop-in URL: CSS URLs
327
+ // resolve from the stylesheet while HTML URLs resolve from the document.
328
+ const isBundler = isBundlerProject(workspaceRoot)
329
+ const publicUrl = isBundler
330
+ ? finalRelPath.replace(/^(public|static)\//, "/").replace(/^(?!\/)/, "/")
331
+ : null
332
+ const durationMs = Date.now() - startedAt
333
+ const remaining = MAX_PER_SESSION - used - 1
334
+ const referenceGuidance = isBundler
335
+ ? `Reference it as ${publicUrl} in the app.`
336
+ :
337
+ `This is a project-root path, not a drop-in URL. ` +
338
+ `From a root HTML document use ./${finalRelPath}; from css/style.css use ../${finalRelPath}. ` +
339
+ `For any other consuming file, compute the URL relative to that file.`
340
+
341
+ return {
342
+ title: "Generated image",
343
+ output:
344
+ `Generated image at ${finalRelPath} (${bytes.length} bytes, ${size}). ` +
345
+ `${referenceGuidance} ` +
346
+ `${remaining} image generation(s) left in this run.`,
347
+ metadata: {
348
+ path: finalRelPath,
349
+ projectPath: finalRelPath,
350
+ publicUrl,
351
+ mime: finalMime,
352
+ size,
353
+ bytes: bytes.length,
354
+ provider: result.provider,
355
+ model: result.model,
356
+ durationMs,
357
+ },
358
+ }
359
+ },
360
+ })
@@ -0,0 +1,2 @@
1
+ import { Plugin } from "./index.js";
2
+ export declare const ExamplePlugin: Plugin;
@@ -0,0 +1,16 @@
1
+ import { tool } from "./tool.js";
2
+ export const ExamplePlugin = async (ctx) => {
3
+ return {
4
+ tool: {
5
+ mytool: tool({
6
+ description: "This is a custom tool",
7
+ args: {
8
+ foo: tool.schema.string().describe("foo"),
9
+ },
10
+ async execute(args) {
11
+ return `Hello ${args.foo}!`;
12
+ },
13
+ }),
14
+ },
15
+ };
16
+ };
@@ -0,0 +1,267 @@
1
+ import type { Event, createOpencodeClient, Project, Model, Provider, Permission, UserMessage, Message, Part, Auth, Config as SDKConfig } from "@opencode-ai/sdk";
2
+ import type { Provider as ProviderV2, Model as ModelV2 } from "@opencode-ai/sdk/v2";
3
+ import type { BunShell } from "./shell.js";
4
+ import { type ToolDefinition } from "./tool.js";
5
+ export * from "./tool.js";
6
+ export type ProviderContext = {
7
+ source: "env" | "config" | "custom" | "api";
8
+ info: Provider;
9
+ options: Record<string, any>;
10
+ };
11
+ export type PluginInput = {
12
+ client: ReturnType<typeof createOpencodeClient>;
13
+ project: Project;
14
+ directory: string;
15
+ worktree: string;
16
+ serverUrl: URL;
17
+ $: BunShell;
18
+ };
19
+ export type PluginOptions = Record<string, unknown>;
20
+ export type Config = Omit<SDKConfig, "plugin"> & {
21
+ plugin?: Array<string | [string, PluginOptions]>;
22
+ };
23
+ export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks>;
24
+ export type PluginModule = {
25
+ id?: string;
26
+ server: Plugin;
27
+ tui?: never;
28
+ };
29
+ type Rule = {
30
+ key: string;
31
+ op: "eq" | "neq";
32
+ value: string;
33
+ };
34
+ export type AuthHook = {
35
+ provider: string;
36
+ loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>;
37
+ methods: ({
38
+ type: "oauth";
39
+ label: string;
40
+ prompts?: Array<{
41
+ type: "text";
42
+ key: string;
43
+ message: string;
44
+ placeholder?: string;
45
+ validate?: (value: string) => string | undefined;
46
+ /** @deprecated Use `when` instead */
47
+ condition?: (inputs: Record<string, string>) => boolean;
48
+ when?: Rule;
49
+ } | {
50
+ type: "select";
51
+ key: string;
52
+ message: string;
53
+ options: Array<{
54
+ label: string;
55
+ value: string;
56
+ hint?: string;
57
+ }>;
58
+ /** @deprecated Use `when` instead */
59
+ condition?: (inputs: Record<string, string>) => boolean;
60
+ when?: Rule;
61
+ }>;
62
+ authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult>;
63
+ } | {
64
+ type: "api";
65
+ label: string;
66
+ prompts?: Array<{
67
+ type: "text";
68
+ key: string;
69
+ message: string;
70
+ placeholder?: string;
71
+ validate?: (value: string) => string | undefined;
72
+ /** @deprecated Use `when` instead */
73
+ condition?: (inputs: Record<string, string>) => boolean;
74
+ when?: Rule;
75
+ } | {
76
+ type: "select";
77
+ key: string;
78
+ message: string;
79
+ options: Array<{
80
+ label: string;
81
+ value: string;
82
+ hint?: string;
83
+ }>;
84
+ /** @deprecated Use `when` instead */
85
+ condition?: (inputs: Record<string, string>) => boolean;
86
+ when?: Rule;
87
+ }>;
88
+ authorize?(inputs?: Record<string, string>): Promise<{
89
+ type: "success";
90
+ key: string;
91
+ provider?: string;
92
+ } | {
93
+ type: "failed";
94
+ }>;
95
+ })[];
96
+ };
97
+ export type AuthOAuthResult = {
98
+ url: string;
99
+ instructions: string;
100
+ } & ({
101
+ method: "auto";
102
+ callback(): Promise<({
103
+ type: "success";
104
+ provider?: string;
105
+ } & ({
106
+ refresh: string;
107
+ access: string;
108
+ expires: number;
109
+ accountId?: string;
110
+ enterpriseUrl?: string;
111
+ } | {
112
+ key: string;
113
+ })) | {
114
+ type: "failed";
115
+ }>;
116
+ } | {
117
+ method: "code";
118
+ callback(code: string): Promise<({
119
+ type: "success";
120
+ provider?: string;
121
+ } & ({
122
+ refresh: string;
123
+ access: string;
124
+ expires: number;
125
+ accountId?: string;
126
+ enterpriseUrl?: string;
127
+ } | {
128
+ key: string;
129
+ })) | {
130
+ type: "failed";
131
+ }>;
132
+ });
133
+ export type ProviderHookContext = {
134
+ auth?: Auth;
135
+ };
136
+ export type ProviderHook = {
137
+ id: string;
138
+ models?: (provider: ProviderV2, ctx: ProviderHookContext) => Promise<Record<string, ModelV2>>;
139
+ };
140
+ /** @deprecated Use AuthOAuthResult instead. */
141
+ export type AuthOuathResult = AuthOAuthResult;
142
+ export interface Hooks {
143
+ event?: (input: {
144
+ event: Event;
145
+ }) => Promise<void>;
146
+ config?: (input: Config) => Promise<void>;
147
+ tool?: {
148
+ [key: string]: ToolDefinition;
149
+ };
150
+ auth?: AuthHook;
151
+ provider?: ProviderHook;
152
+ /**
153
+ * Called when a new message is received
154
+ */
155
+ "chat.message"?: (input: {
156
+ sessionID: string;
157
+ agent?: string;
158
+ model?: {
159
+ providerID: string;
160
+ modelID: string;
161
+ };
162
+ messageID?: string;
163
+ variant?: string;
164
+ }, output: {
165
+ message: UserMessage;
166
+ parts: Part[];
167
+ }) => Promise<void>;
168
+ /**
169
+ * Modify parameters sent to LLM
170
+ */
171
+ "chat.params"?: (input: {
172
+ sessionID: string;
173
+ agent: string;
174
+ model: Model;
175
+ provider: ProviderContext;
176
+ message: UserMessage;
177
+ }, output: {
178
+ temperature: number;
179
+ topP: number;
180
+ topK: number;
181
+ options: Record<string, any>;
182
+ }) => Promise<void>;
183
+ "chat.headers"?: (input: {
184
+ sessionID: string;
185
+ agent: string;
186
+ model: Model;
187
+ provider: ProviderContext;
188
+ message: UserMessage;
189
+ }, output: {
190
+ headers: Record<string, string>;
191
+ }) => Promise<void>;
192
+ "permission.ask"?: (input: Permission, output: {
193
+ status: "ask" | "deny" | "allow";
194
+ }) => Promise<void>;
195
+ "command.execute.before"?: (input: {
196
+ command: string;
197
+ sessionID: string;
198
+ arguments: string;
199
+ }, output: {
200
+ parts: Part[];
201
+ }) => Promise<void>;
202
+ "tool.execute.before"?: (input: {
203
+ tool: string;
204
+ sessionID: string;
205
+ callID: string;
206
+ }, output: {
207
+ args: any;
208
+ }) => Promise<void>;
209
+ "shell.env"?: (input: {
210
+ cwd: string;
211
+ sessionID?: string;
212
+ callID?: string;
213
+ }, output: {
214
+ env: Record<string, string>;
215
+ }) => Promise<void>;
216
+ "tool.execute.after"?: (input: {
217
+ tool: string;
218
+ sessionID: string;
219
+ callID: string;
220
+ args: any;
221
+ }, output: {
222
+ title: string;
223
+ output: string;
224
+ metadata: any;
225
+ }) => Promise<void>;
226
+ "experimental.chat.messages.transform"?: (input: {}, output: {
227
+ messages: {
228
+ info: Message;
229
+ parts: Part[];
230
+ }[];
231
+ }) => Promise<void>;
232
+ "experimental.chat.system.transform"?: (input: {
233
+ sessionID?: string;
234
+ model: Model;
235
+ }, output: {
236
+ system: string[];
237
+ }) => Promise<void>;
238
+ /**
239
+ * Called before session compaction starts. Allows plugins to customize
240
+ * the compaction prompt.
241
+ *
242
+ * - `context`: Additional context strings appended to the default prompt
243
+ * - `prompt`: If set, replaces the default compaction prompt entirely
244
+ */
245
+ "experimental.session.compacting"?: (input: {
246
+ sessionID: string;
247
+ }, output: {
248
+ context: string[];
249
+ prompt?: string;
250
+ }) => Promise<void>;
251
+ "experimental.text.complete"?: (input: {
252
+ sessionID: string;
253
+ messageID: string;
254
+ partID: string;
255
+ }, output: {
256
+ text: string;
257
+ }) => Promise<void>;
258
+ /**
259
+ * Modify tool definitions (description and parameters) sent to LLM
260
+ */
261
+ "tool.definition"?: (input: {
262
+ toolID: string;
263
+ }, output: {
264
+ description: string;
265
+ parameters: any;
266
+ }) => Promise<void>;
267
+ }
@@ -0,0 +1 @@
1
+ export * from "./tool.js";