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.
- package/LICENSE +202 -0
- package/NOTICE +23 -0
- package/README.md +93 -0
- package/THIRD-PARTY-LICENSES.txt +35 -0
- package/assets/tools/imagegen.ts +360 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/example.d.ts +2 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/example.js +16 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/index.d.ts +267 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/index.js +1 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/shell.d.ts +109 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/shell.js +1 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/tool.d.ts +46 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/tool.js +5 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/tui.d.ts +429 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/dist/tui.js +1 -0
- package/assets/tools/node_modules/@opencode-ai/plugin/package.json +52 -0
- package/assets/tools/package.json +8 -0
- package/dist/index.cjs +5693 -0
- package/package.json +51 -0
|
@@ -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,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";
|