@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/README.zh-CN.md +270 -0
  4. package/designs/default/DESIGN.md +1100 -0
  5. package/designs/editorial-ribbon/DESIGN.md +1092 -0
  6. package/designs/minimal/DESIGN.md +1079 -0
  7. package/domains/consulting/INDUSTRY.md +230 -0
  8. package/domains/deeptech-investment/INDUSTRY.md +160 -0
  9. package/domains/general/INDUSTRY.md +6 -0
  10. package/index.ts +1 -0
  11. package/lib/agents/research-prompt.ts +129 -0
  12. package/lib/commands/designs.ts +59 -0
  13. package/lib/commands/disable.ts +14 -0
  14. package/lib/commands/domains.ts +59 -0
  15. package/lib/commands/enable.ts +48 -0
  16. package/lib/commands/help.ts +35 -0
  17. package/lib/config.ts +65 -0
  18. package/lib/ctx.ts +27 -0
  19. package/lib/design/designs.ts +389 -0
  20. package/lib/domain/domains.ts +258 -0
  21. package/lib/frontmatter.ts +63 -0
  22. package/lib/log.ts +35 -0
  23. package/lib/prompt-builder.ts +194 -0
  24. package/lib/qa/checks.ts +594 -0
  25. package/lib/qa/index.ts +38 -0
  26. package/lib/qa/measure.ts +287 -0
  27. package/lib/read-hooks/extractors/docx.ts +16 -0
  28. package/lib/read-hooks/extractors/pdf.ts +19 -0
  29. package/lib/read-hooks/extractors/pptx.ts +53 -0
  30. package/lib/read-hooks/extractors/xlsx.ts +81 -0
  31. package/lib/read-hooks/image/compress.ts +36 -0
  32. package/lib/read-hooks/index.ts +12 -0
  33. package/lib/read-hooks/post-read.ts +74 -0
  34. package/lib/read-hooks/pre-read.ts +51 -0
  35. package/package.json +65 -0
  36. package/plugin.ts +365 -0
  37. package/skill/SKILL.md +676 -0
  38. package/tools/designs.ts +126 -0
  39. package/tools/domains.ts +73 -0
  40. package/tools/qa.ts +61 -0
  41. package/tools/research-save.ts +96 -0
  42. 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 }