@cyber-dash-tech/revela 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 +21 -0
- package/README.md +239 -0
- package/README.zh-CN.md +270 -0
- package/designs/default/DESIGN.md +1100 -0
- package/designs/editorial-ribbon/DESIGN.md +1092 -0
- package/designs/minimal/DESIGN.md +1079 -0
- package/domains/consulting/INDUSTRY.md +230 -0
- package/domains/deeptech-investment/INDUSTRY.md +160 -0
- package/domains/general/INDUSTRY.md +6 -0
- package/index.ts +1 -0
- package/lib/agents/research-prompt.ts +129 -0
- package/lib/commands/designs.ts +59 -0
- package/lib/commands/disable.ts +14 -0
- package/lib/commands/domains.ts +59 -0
- package/lib/commands/enable.ts +48 -0
- package/lib/commands/help.ts +35 -0
- package/lib/config.ts +65 -0
- package/lib/ctx.ts +27 -0
- package/lib/design/designs.ts +389 -0
- package/lib/domain/domains.ts +258 -0
- package/lib/frontmatter.ts +63 -0
- package/lib/log.ts +35 -0
- package/lib/prompt-builder.ts +194 -0
- package/lib/qa/checks.ts +594 -0
- package/lib/qa/index.ts +38 -0
- package/lib/qa/measure.ts +287 -0
- package/lib/read-hooks/extractors/docx.ts +16 -0
- package/lib/read-hooks/extractors/pdf.ts +19 -0
- package/lib/read-hooks/extractors/pptx.ts +53 -0
- package/lib/read-hooks/extractors/xlsx.ts +81 -0
- package/lib/read-hooks/image/compress.ts +36 -0
- package/lib/read-hooks/index.ts +12 -0
- package/lib/read-hooks/post-read.ts +74 -0
- package/lib/read-hooks/pre-read.ts +51 -0
- package/package.json +65 -0
- package/plugin.ts +365 -0
- package/skill/SKILL.md +676 -0
- package/tools/designs.ts +126 -0
- package/tools/domains.ts +73 -0
- package/tools/qa.ts +61 -0
- package/tools/research-save.ts +96 -0
- package/tools/workspace-scan.ts +154 -0
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cyber-dash-tech/revela",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin that turns AI into an HTML slide deck generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"plugin.ts",
|
|
12
|
+
"lib/",
|
|
13
|
+
"tools/",
|
|
14
|
+
"skill/",
|
|
15
|
+
"designs/default/DESIGN.md",
|
|
16
|
+
"designs/minimal/DESIGN.md",
|
|
17
|
+
"designs/editorial-ribbon/DESIGN.md",
|
|
18
|
+
"domains/general/INDUSTRY.md",
|
|
19
|
+
"domains/deeptech-investment/INDUSTRY.md",
|
|
20
|
+
"domains/consulting/INDUSTRY.md",
|
|
21
|
+
"index.ts"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"opencode",
|
|
25
|
+
"opencode-plugin",
|
|
26
|
+
"slides",
|
|
27
|
+
"presentation",
|
|
28
|
+
"ai",
|
|
29
|
+
"html",
|
|
30
|
+
"deck"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/cyber-dash-tech/revela.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/cyber-dash-tech/revela#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/cyber-dash-tech/revela/issues"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"author": "cyber-dash-tech",
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"typecheck": "tsc"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@opencode-ai/plugin": "*"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@xmldom/xmldom": "^0.9.9",
|
|
54
|
+
"fflate": "^0.8.2",
|
|
55
|
+
"jimp": "^1.6.1",
|
|
56
|
+
"mammoth": "^1.12.0",
|
|
57
|
+
"puppeteer-core": "^24.40.0",
|
|
58
|
+
"tslog": "^4.10.2",
|
|
59
|
+
"unpdf": "^1.4.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/bun": "^1.3.12",
|
|
63
|
+
"typescript": "^6.0.2"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/plugin.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* revela — Core OpenCode Plugin
|
|
3
|
+
*
|
|
4
|
+
* Architecture: enable/disable mode + single /revela command (DCP style)
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* 1. On load: seed built-in designs/domains + build initial _active-prompt.md
|
|
8
|
+
* 2. config hook: register /revela command (empty template, no .md file needed)
|
|
9
|
+
* 3. command.execute.before: route all sub-commands to lib/commands/ handlers
|
|
10
|
+
* 4. tool: expose revela-designs + revela-domains tools to LLM
|
|
11
|
+
* 5. experimental.chat.system.transform: inject three-layer prompt when enabled
|
|
12
|
+
* 6. chat.message: intercept @-referenced / pasted binary files → extract text → replace FilePart with TextPart
|
|
13
|
+
* 7. tool.execute.before: intercept read on DOCX/PPTX/XLSX → preRead()
|
|
14
|
+
* 8. tool.execute.after: intercept read on PDF/images → postRead()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
18
|
+
import { existsSync, readFileSync } from "fs"
|
|
19
|
+
import { extname, basename } from "path"
|
|
20
|
+
import { seedBuiltinDesigns } from "./lib/design/designs"
|
|
21
|
+
import { seedBuiltinDomains } from "./lib/domain/domains"
|
|
22
|
+
import { buildPrompt } from "./lib/prompt-builder"
|
|
23
|
+
import { ACTIVE_PROMPT_FILE } from "./lib/config"
|
|
24
|
+
import { ctx } from "./lib/ctx"
|
|
25
|
+
import { preRead } from "./lib/read-hooks"
|
|
26
|
+
import { postRead } from "./lib/read-hooks"
|
|
27
|
+
import { extractDocx } from "./lib/read-hooks/extractors/docx"
|
|
28
|
+
import { extractPptx } from "./lib/read-hooks/extractors/pptx"
|
|
29
|
+
import { extractXlsx } from "./lib/read-hooks/extractors/xlsx"
|
|
30
|
+
import { extractPdfText } from "./lib/read-hooks/extractors/pdf"
|
|
31
|
+
import { handleHelp } from "./lib/commands/help"
|
|
32
|
+
import { handleEnable } from "./lib/commands/enable"
|
|
33
|
+
import { handleDisable } from "./lib/commands/disable"
|
|
34
|
+
import {
|
|
35
|
+
handleDesignsList,
|
|
36
|
+
handleDesignsActivate,
|
|
37
|
+
handleDesignsAdd,
|
|
38
|
+
} from "./lib/commands/designs"
|
|
39
|
+
import {
|
|
40
|
+
handleDomainsList,
|
|
41
|
+
handleDomainsActivate,
|
|
42
|
+
handleDomainsAdd,
|
|
43
|
+
} from "./lib/commands/domains"
|
|
44
|
+
import designsTool from "./tools/designs"
|
|
45
|
+
import domainsTool from "./tools/domains"
|
|
46
|
+
import researchSaveTool from "./tools/research-save"
|
|
47
|
+
import workspaceScanTool from "./tools/workspace-scan"
|
|
48
|
+
import qaTool from "./tools/qa"
|
|
49
|
+
import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
|
|
50
|
+
import { runQA, formatReport } from "./lib/qa"
|
|
51
|
+
import { log, childLog } from "./lib/log"
|
|
52
|
+
|
|
53
|
+
// OpenCode internal agent signatures — used to skip system prompt injection
|
|
54
|
+
// for built-in system agents (title, summary, compaction).
|
|
55
|
+
const INTERNAL_AGENT_SIGNATURES = [
|
|
56
|
+
"You are a title generator",
|
|
57
|
+
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
58
|
+
"Summarize what was done in this conversation",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Display a message in the conversation UI without triggering LLM
|
|
65
|
+
* and without polluting future context. Pattern from DCP.
|
|
66
|
+
*/
|
|
67
|
+
async function sendIgnoredMessage(
|
|
68
|
+
client: any,
|
|
69
|
+
sessionID: string,
|
|
70
|
+
text: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
await client.session.prompt({
|
|
74
|
+
path: { id: sessionID },
|
|
75
|
+
body: {
|
|
76
|
+
noReply: true,
|
|
77
|
+
parts: [{ type: "text", text, ignored: true }],
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
} catch (e) {
|
|
81
|
+
log.error("sendIgnoredMessage failed", { error: e instanceof Error ? e.message : String(e) })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Plugin ─────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const server: Plugin = (async (pluginCtx) => {
|
|
88
|
+
const client = pluginCtx.client
|
|
89
|
+
|
|
90
|
+
// ── Startup: seed + build initial prompt ────────────────────────────────
|
|
91
|
+
try {
|
|
92
|
+
seedBuiltinDesigns()
|
|
93
|
+
seedBuiltinDomains()
|
|
94
|
+
buildPrompt()
|
|
95
|
+
log.info("revela initialized", { promptFile: ACTIVE_PROMPT_FILE })
|
|
96
|
+
} catch (e) {
|
|
97
|
+
log.error("startup failed — prompt may not be injected", { error: e instanceof Error ? e.message : String(e) })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
// ── Register /revela command + revela-research subagent ───────────────
|
|
102
|
+
config: async (opencodeConfig) => {
|
|
103
|
+
opencodeConfig.command ??= {}
|
|
104
|
+
opencodeConfig.command["revela"] = {
|
|
105
|
+
template: "",
|
|
106
|
+
description: "Revela — AI slide deck generator (enable/disable, manage designs & domains)",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Register the research subagent.
|
|
110
|
+
// mode: "subagent" — not shown in Tab cycle, invoked via @revela-research or Task tool.
|
|
111
|
+
// Permissions: read-only on edit/bash; write allowed to create researches/ files.
|
|
112
|
+
// No model override — inherits from the calling primary agent.
|
|
113
|
+
opencodeConfig.agent ??= {}
|
|
114
|
+
opencodeConfig.agent["revela-research"] = {
|
|
115
|
+
description: "Revela research agent — searches and collects raw materials for presentations",
|
|
116
|
+
mode: "subagent",
|
|
117
|
+
prompt: RESEARCH_PROMPT,
|
|
118
|
+
permission: {
|
|
119
|
+
edit: "deny",
|
|
120
|
+
bash: {
|
|
121
|
+
"*": "deny",
|
|
122
|
+
"ls *": "allow",
|
|
123
|
+
"ls": "allow",
|
|
124
|
+
},
|
|
125
|
+
webfetch: "allow",
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// ── Route all sub-commands to lib/commands/ handlers ──────────────────
|
|
131
|
+
"command.execute.before": async (input, output) => {
|
|
132
|
+
if (input.command !== "revela") return
|
|
133
|
+
|
|
134
|
+
const sessionID: string = input.sessionID ?? ""
|
|
135
|
+
const args = (input.arguments ?? "").trim().split(/\s+/).filter(Boolean) as string[]
|
|
136
|
+
const sub = args[0]?.toLowerCase() ?? ""
|
|
137
|
+
const param = args.slice(1).join(" ")
|
|
138
|
+
|
|
139
|
+
const send = (text: string) => sendIgnoredMessage(client, sessionID, text)
|
|
140
|
+
|
|
141
|
+
if (!sub) {
|
|
142
|
+
await handleHelp(send)
|
|
143
|
+
throw new Error("__REVELA_STATUS_HANDLED__")
|
|
144
|
+
}
|
|
145
|
+
if (sub === "enable") {
|
|
146
|
+
await handleEnable(send)
|
|
147
|
+
throw new Error("__REVELA_ENABLE_HANDLED__")
|
|
148
|
+
}
|
|
149
|
+
if (sub === "disable") {
|
|
150
|
+
await handleDisable(send)
|
|
151
|
+
throw new Error("__REVELA_DISABLE_HANDLED__")
|
|
152
|
+
}
|
|
153
|
+
if (sub === "designs" && !param) {
|
|
154
|
+
await handleDesignsList(send)
|
|
155
|
+
throw new Error("__REVELA_DESIGNS_LIST_HANDLED__")
|
|
156
|
+
}
|
|
157
|
+
if (sub === "designs" && param) {
|
|
158
|
+
await handleDesignsActivate(param, send)
|
|
159
|
+
throw new Error("__REVELA_DESIGNS_ACTIVATE_HANDLED__")
|
|
160
|
+
}
|
|
161
|
+
if (sub === "domains" && !param) {
|
|
162
|
+
await handleDomainsList(send)
|
|
163
|
+
throw new Error("__REVELA_DOMAINS_LIST_HANDLED__")
|
|
164
|
+
}
|
|
165
|
+
if (sub === "domains" && param) {
|
|
166
|
+
await handleDomainsActivate(param, send)
|
|
167
|
+
throw new Error("__REVELA_DOMAINS_ACTIVATE_HANDLED__")
|
|
168
|
+
}
|
|
169
|
+
if (sub === "designs-add") {
|
|
170
|
+
await handleDesignsAdd(param, send)
|
|
171
|
+
throw new Error("__REVELA_DESIGNS_ADD_HANDLED__")
|
|
172
|
+
}
|
|
173
|
+
if (sub === "domains-add") {
|
|
174
|
+
await handleDomainsAdd(param, send)
|
|
175
|
+
throw new Error("__REVELA_DOMAINS_ADD_HANDLED__")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await send(`**Unknown sub-command:** \`${sub}\`\nRun \`/revela\` to see available commands.`)
|
|
179
|
+
throw new Error("__REVELA_UNKNOWN_HANDLED__")
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ── LLM tools: designs, domains, research, qa ─────────────────────────
|
|
183
|
+
tool: {
|
|
184
|
+
"revela-designs": designsTool,
|
|
185
|
+
"revela-domains": domainsTool,
|
|
186
|
+
"revela-research-save": researchSaveTool,
|
|
187
|
+
"revela-workspace-scan": workspaceScanTool,
|
|
188
|
+
"revela-qa": qaTool,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// ── chat.message: intercept @-referenced / pasted binary files ────────
|
|
192
|
+
// When user uses @ or pastes a file, OpenCode injects it as a FilePart
|
|
193
|
+
// directly — the read tool is never called, so tool.execute.before/after
|
|
194
|
+
// hooks don't fire. This hook intercepts FileParts before LLM sees them.
|
|
195
|
+
//
|
|
196
|
+
// DOCX/PPTX/XLSX/PDF → extract text → replace with TextPart
|
|
197
|
+
// Images → replace with TextPart hint (LLM can use read tool)
|
|
198
|
+
"chat.message": async (input, output) => {
|
|
199
|
+
if (!ctx.enabled) return
|
|
200
|
+
|
|
201
|
+
const IMAGE_EXTS = new Set([".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp", ".gif"])
|
|
202
|
+
const DOC_HANDLERS: Record<string, (buf: Buffer) => Promise<string>> = {
|
|
203
|
+
".docx": extractDocx,
|
|
204
|
+
".pptx": extractPptx,
|
|
205
|
+
".xlsx": extractXlsx,
|
|
206
|
+
".pdf": extractPdfText,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < output.parts.length; i++) {
|
|
210
|
+
const part = output.parts[i] as any
|
|
211
|
+
if (part.type !== "file") continue
|
|
212
|
+
if (part.source?.type !== "file") continue
|
|
213
|
+
|
|
214
|
+
const filePath: string = part.source.path
|
|
215
|
+
const ext = extname(filePath).toLowerCase()
|
|
216
|
+
const name = basename(filePath)
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (DOC_HANDLERS[ext]) {
|
|
220
|
+
const buf = readFileSync(filePath)
|
|
221
|
+
const text = await DOC_HANDLERS[ext](buf)
|
|
222
|
+
output.parts[i] = {
|
|
223
|
+
...part,
|
|
224
|
+
type: "text",
|
|
225
|
+
text: `[Extracted from: ${name}]\n\n${text}`,
|
|
226
|
+
} as any
|
|
227
|
+
} else if (IMAGE_EXTS.has(ext)) {
|
|
228
|
+
output.parts[i] = {
|
|
229
|
+
...part,
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `[Image: ${name} — use the read tool if you need to view this image]`,
|
|
232
|
+
} as any
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
childLog("chat.message").warn("failed to process file", {
|
|
236
|
+
file: name,
|
|
237
|
+
error: e instanceof Error ? e.message : String(e),
|
|
238
|
+
})
|
|
239
|
+
// Keep original FilePart on failure — graceful degradation
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// ── Inject three-layer prompt when enabled ─────────────────────────────
|
|
245
|
+
// Skip injection for:
|
|
246
|
+
// 1. revela-research subagent (has its own research-focused prompt)
|
|
247
|
+
// 2. OpenCode internal agents (title, summary, compaction)
|
|
248
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
249
|
+
if (!ctx.enabled) return
|
|
250
|
+
try {
|
|
251
|
+
// Detect which agent is running by fingerprinting output.system content.
|
|
252
|
+
// The plugin API does not expose agent name on this hook's input.
|
|
253
|
+
const systemText = output.system.join("\n")
|
|
254
|
+
|
|
255
|
+
// Skip revela-research subagent — it has its own research prompt.
|
|
256
|
+
// Also mark ctx so tool.execute.before can allow websearch for research agents.
|
|
257
|
+
if (systemText.includes(RESEARCH_AGENT_SIGNATURE)) {
|
|
258
|
+
ctx.isResearchAgent = true
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
ctx.isResearchAgent = false
|
|
262
|
+
|
|
263
|
+
// Skip OpenCode internal system agents (title generator, summary, compaction)
|
|
264
|
+
if (INTERNAL_AGENT_SIGNATURES.some((sig) => systemText.includes(sig))) return
|
|
265
|
+
|
|
266
|
+
const prompt = readFileSync(ACTIVE_PROMPT_FILE, "utf-8")
|
|
267
|
+
if (output.system.length > 0) {
|
|
268
|
+
output.system[output.system.length - 1] += "\n\n" + prompt
|
|
269
|
+
} else {
|
|
270
|
+
output.system.push(prompt)
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
log.error("failed to inject system prompt", { error: e instanceof Error ? e.message : String(e) })
|
|
274
|
+
// Surface the failure in the system prompt so the LLM and user are aware.
|
|
275
|
+
// This prevents a silent "revela enabled but not working" scenario.
|
|
276
|
+
output.system.push(
|
|
277
|
+
"\n\n[REVELA ERROR: Failed to load the slide generation prompt. " +
|
|
278
|
+
"Run /revela disable then /revela enable to reinitialize.]"
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// ── Pre-read: intercept binary files before read executes ──────────────
|
|
284
|
+
// Handles DOCX/PPTX/XLSX — read tool would Effect.fail on these.
|
|
285
|
+
// Extracts text → writes temp .txt → redirects args.filePath.
|
|
286
|
+
//
|
|
287
|
+
// Also blocks websearch for the primary agent — websearch must be delegated
|
|
288
|
+
// to the revela-research subagent. Use webfetch for specific URLs instead.
|
|
289
|
+
"tool.execute.before": async (input, output) => {
|
|
290
|
+
if (!ctx.enabled) return
|
|
291
|
+
|
|
292
|
+
// ── Block websearch for primary agent ──────────────────────────────
|
|
293
|
+
if (input.tool === "websearch" && !ctx.isResearchAgent) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
"[revela] websearch is not available for the primary agent. " +
|
|
296
|
+
"Delegate web research to the revela-research subagent via the Task tool — " +
|
|
297
|
+
"it searches systematically and saves structured findings for reuse across sessions. " +
|
|
298
|
+
"Use the webfetch tool if you need to read a specific URL directly.",
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (input.tool !== "read") return
|
|
303
|
+
try {
|
|
304
|
+
await preRead(output.args)
|
|
305
|
+
} catch (e) {
|
|
306
|
+
childLog("preRead").warn("extraction failed", {
|
|
307
|
+
filePath: (output.args as any)?.filePath,
|
|
308
|
+
error: e instanceof Error ? e.message : String(e),
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// ── Post-read: transform PDF text + compress images ────────────────────
|
|
314
|
+
// Handles PDF and images — read tool succeeds with base64 attachment.
|
|
315
|
+
// PDF: extract text, remove base64. Images: jimp compress.
|
|
316
|
+
//
|
|
317
|
+
// Also handles: auto layout QA after writing slides/*.html
|
|
318
|
+
"tool.execute.after": async (input, output) => {
|
|
319
|
+
if (!ctx.enabled) return
|
|
320
|
+
|
|
321
|
+
// ── Post-read processing ───────────────────────────────────────────
|
|
322
|
+
if (input.tool === "read") {
|
|
323
|
+
try {
|
|
324
|
+
await postRead(input.args, output)
|
|
325
|
+
} catch (e) {
|
|
326
|
+
childLog("postRead").warn("processing failed", {
|
|
327
|
+
filePath: (input.args as any)?.filePath,
|
|
328
|
+
error: e instanceof Error ? e.message : String(e),
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Auto layout QA after writing slides/*.html ─────────────────────
|
|
335
|
+
if (input.tool === "write") {
|
|
336
|
+
const filePath: string = input.args?.filePath ?? ""
|
|
337
|
+
// Only trigger for HTML files inside a slides/ directory
|
|
338
|
+
if (!filePath.match(/slides\/[^/]+\.html$/)) return
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const report = await runQA(filePath)
|
|
342
|
+
// Only append QA report to tool output if there are issues
|
|
343
|
+
if (report.totalIssues > 0) {
|
|
344
|
+
const formatted = formatReport(report)
|
|
345
|
+
// Append to the write tool's output so the LLM sees it immediately
|
|
346
|
+
const existing = (output as any).result ?? ""
|
|
347
|
+
;(output as any).result =
|
|
348
|
+
(existing ? existing + "\n\n" : "") +
|
|
349
|
+
"---\n\n**[revela layout QA]** Auto-check completed:\n\n" +
|
|
350
|
+
formatted
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
childLog("qa").warn("auto QA failed", {
|
|
354
|
+
filePath,
|
|
355
|
+
error: e instanceof Error ? e.message : String(e),
|
|
356
|
+
})
|
|
357
|
+
// Don't surface errors to the LLM — fail silently
|
|
358
|
+
}
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
}) satisfies Plugin
|
|
364
|
+
|
|
365
|
+
export default { id: "revela", server }
|