@cyber-dash-tech/revela 0.16.2 → 0.16.4

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,295 @@
1
+ import { readFileSync, statSync, writeFileSync } from "fs"
2
+
3
+ export type VisualEditKind = "image" | "text-width" | "box"
4
+
5
+ export interface VisualEditTarget {
6
+ editId: string
7
+ kind: VisualEditKind
8
+ tagName: "img" | "h1" | "h2" | "h3" | "h4" | "p" | "div" | "figure"
9
+ startOffset: number
10
+ openEndOffset: number
11
+ originalOpenTag: string
12
+ originalStyle: string
13
+ }
14
+
15
+ export interface VisualTargetResizeChange {
16
+ type: "resize"
17
+ editId: string
18
+ kind?: VisualEditKind
19
+ after: {
20
+ stylePatch?: Record<string, unknown>
21
+ width?: number
22
+ height?: number
23
+ }
24
+ }
25
+
26
+ export interface VisualTargetMoveChange {
27
+ type: "move"
28
+ editId: string
29
+ kind?: VisualEditKind
30
+ after: {
31
+ stylePatch?: Record<string, unknown>
32
+ dx?: number
33
+ dy?: number
34
+ }
35
+ }
36
+
37
+ export type VisualTargetChange = VisualTargetResizeChange | VisualTargetMoveChange
38
+
39
+ export interface ApplyVisualTargetChangesInput {
40
+ file: string
41
+ deckVersion?: string
42
+ targetDeckVersion?: string
43
+ targets: Map<string, VisualEditTarget>
44
+ changes: VisualTargetChange[]
45
+ }
46
+
47
+ export interface ApplyVisualTargetChangesResult {
48
+ ok: true
49
+ deckVersion: string
50
+ changeCount: number
51
+ }
52
+
53
+ const TEXT_TAGS = new Set(["h1", "h2", "h3", "h4", "p"])
54
+ const BOX_TAGS = new Set(["div", "figure"])
55
+ const EDIT_TAGS = new Set(["img", ...TEXT_TAGS, ...BOX_TAGS])
56
+ const MAX_DIMENSION_PX = 3840
57
+ const MIN_IMAGE_DIMENSION_PX = 24
58
+ const MIN_TEXT_WIDTH_PX = 80
59
+ const MIN_BOX_WIDTH_PX = 40
60
+ const MIN_BOX_HEIGHT_PX = 24
61
+
62
+ export function annotateVisualEditTargets(html: string): { html: string; targets: Map<string, VisualEditTarget> } {
63
+ const targets = new Map<string, VisualEditTarget>()
64
+ const insertions: Array<{ offset: number; text: string }> = []
65
+ let nextId = 1
66
+
67
+ for (const tag of scanOpeningTags(html)) {
68
+ if (!EDIT_TAGS.has(tag.tagName)) continue
69
+ if (BOX_TAGS.has(tag.tagName) && !isSafeBoxTarget(tag.openTag)) continue
70
+ const editId = `rve-${nextId++}`
71
+ const kind: VisualEditKind = tag.tagName === "img" ? "image" : BOX_TAGS.has(tag.tagName) ? "box" : "text-width"
72
+ targets.set(editId, {
73
+ editId,
74
+ kind,
75
+ tagName: tag.tagName as VisualEditTarget["tagName"],
76
+ startOffset: tag.startOffset,
77
+ openEndOffset: tag.openEndOffset,
78
+ originalOpenTag: tag.openTag,
79
+ originalStyle: attrValue(tag.openTag, "style") || "",
80
+ })
81
+ insertions.push({ offset: tag.openEndOffset - (tag.openTag.endsWith("/>") ? 2 : 1), text: ` data-revela-edit-id="${editId}" data-revela-edit-kind="${kind}"` })
82
+ }
83
+
84
+ let annotated = html
85
+ for (const insertion of insertions.reverse()) {
86
+ annotated = annotated.slice(0, insertion.offset) + insertion.text + annotated.slice(insertion.offset)
87
+ }
88
+ return { html: annotated, targets }
89
+ }
90
+
91
+ export function applyVisualTargetChanges(input: ApplyVisualTargetChangesInput): ApplyVisualTargetChangesResult {
92
+ const currentVersion = readDeckVersion(input.file).version
93
+ if (input.deckVersion && input.deckVersion !== currentVersion) throw new Error("Deck changed outside Review. Refresh Review before saving visual edits.")
94
+ if (input.targetDeckVersion && input.targetDeckVersion !== currentVersion) throw new Error("Review visual targets are stale. Refresh Review before saving visual edits.")
95
+ if (!input.changes.length) throw new Error("No visual changes to save.")
96
+
97
+ let html = readFileSync(input.file, "utf-8")
98
+ const resolved = new Map<string, { target: VisualEditTarget; patch: Record<string, string> }>()
99
+ for (const change of input.changes) {
100
+ const target = input.targets.get(change.editId)
101
+ if (!target) throw new Error("Target is no longer editable. Refresh Review and try again.")
102
+ if (change.kind && change.kind !== target.kind) throw new Error("Visual edit target kind changed. Refresh Review and try again.")
103
+ const currentOpenTag = html.slice(target.startOffset, target.openEndOffset)
104
+ if (currentOpenTag !== target.originalOpenTag) throw new Error("Target is no longer editable. Refresh Review and try again.")
105
+ const patch = normalizeStylePatch(target, change)
106
+ if (!Object.keys(patch).length) throw new Error("Visual change does not contain valid style updates.")
107
+ const existing = resolved.get(target.editId)
108
+ resolved.set(target.editId, { target, patch: { ...(existing?.patch ?? {}), ...patch } })
109
+ }
110
+
111
+ for (const { target, patch } of Array.from(resolved.values()).sort((a, b) => b.target.startOffset - a.target.startOffset)) {
112
+ const updatedOpenTag = patchOpenTagStyle(target.originalOpenTag, patch)
113
+ html = html.slice(0, target.startOffset) + updatedOpenTag + html.slice(target.openEndOffset)
114
+ }
115
+
116
+ writeFileSync(input.file, html, "utf-8")
117
+ return { ok: true, deckVersion: readDeckVersion(input.file).version, changeCount: input.changes.length }
118
+ }
119
+
120
+ export function readDeckVersion(file: string): { mtimeMs: number; size: number; version: string } {
121
+ const stat = statSync(file)
122
+ return { mtimeMs: stat.mtimeMs, size: stat.size, version: `${stat.mtimeMs}:${stat.size}` }
123
+ }
124
+
125
+ function normalizeStylePatch(target: VisualEditTarget, change: VisualTargetChange): Record<string, string> {
126
+ const input = change.after.stylePatch ?? {}
127
+ if (change.type === "move") return normalizeMovePatch(target, input)
128
+ if (change.type !== "resize") throw new Error(`Unsupported visual change type: ${(change as any).type}`)
129
+ return normalizeResizePatch(target.kind, input)
130
+ }
131
+
132
+ function normalizeResizePatch(kind: VisualEditKind, input: Record<string, unknown>): Record<string, string> {
133
+ const patch: Record<string, string> = {}
134
+ const width = typeof input.width === "string" ? normalizePxValue(input.width, kind === "image" ? MIN_IMAGE_DIMENSION_PX : kind === "box" ? MIN_BOX_WIDTH_PX : MIN_TEXT_WIDTH_PX) : null
135
+ if (width) patch.width = width
136
+ if (kind === "image" || kind === "box") {
137
+ const height = typeof input.height === "string" ? normalizePxValue(input.height, kind === "image" ? MIN_IMAGE_DIMENSION_PX : MIN_BOX_HEIGHT_PX) : null
138
+ if (height) patch.height = height
139
+ } else {
140
+ const maxWidth = typeof input["max-width"] === "string" ? normalizePxValue(input["max-width"], MIN_TEXT_WIDTH_PX) : null
141
+ if (maxWidth) patch["max-width"] = maxWidth
142
+ }
143
+ return patch
144
+ }
145
+
146
+ function normalizeMovePatch(target: VisualEditTarget, input: Record<string, unknown>): Record<string, string> {
147
+ const translate = typeof input.translate === "string" ? normalizeTranslateValue(input.translate) : null
148
+ if (translate) return { translate }
149
+ const transform = typeof input.transform === "string" ? normalizeLegacyTransformValue(input.transform) : null
150
+ return transform ? { translate: transform } : {}
151
+ }
152
+
153
+ function normalizeTranslateValue(value: string): string | null {
154
+ const direct = parseTranslateProperty(value)
155
+ if (direct) return normalizeTranslatePoint(direct)
156
+ return normalizeLegacyTransformValue(value)
157
+ }
158
+
159
+ function normalizeLegacyTransformValue(value: string): string | null {
160
+ const parsed = parseSimpleTranslate(value)
161
+ if (!parsed) return null
162
+ return normalizeTranslatePoint(parsed)
163
+ }
164
+
165
+ function normalizeTranslatePoint(parsed: { x: number; y: number }): string | null {
166
+ const { x, y } = parsed
167
+ if (Math.abs(x) > MAX_DIMENSION_PX || Math.abs(y) > MAX_DIMENSION_PX) return null
168
+ return `${Math.round(x)}px ${Math.round(y)}px`
169
+ }
170
+
171
+ function parseTranslateProperty(value: string): { x: number; y: number } | null {
172
+ const normalized = value.trim()
173
+ const match = /^(-?\d+(?:\.\d+)?)px(?:\s+|\s*,\s*)(-?\d+(?:\.\d+)?)px$/.exec(normalized)
174
+ return match ? finitePoint(Number(match[1]), Number(match[2])) : null
175
+ }
176
+
177
+ function parseSimpleTranslate(value: string): { x: number; y: number } | null {
178
+ const normalized = value.trim()
179
+ if (!normalized || normalized === "none") return { x: 0, y: 0 }
180
+ const translate = /^translate\(\s*(-?\d+(?:\.\d+)?)px(?:\s*,\s*|\s+)(-?\d+(?:\.\d+)?)px\s*\)$/.exec(normalized)
181
+ if (translate) return finitePoint(Number(translate[1]), Number(translate[2]))
182
+ const matrix = /^matrix\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)$/.exec(normalized)
183
+ if (!matrix) return null
184
+ const a = Number(matrix[1])
185
+ const b = Number(matrix[2])
186
+ const c = Number(matrix[3])
187
+ const d = Number(matrix[4])
188
+ if (a !== 1 || b !== 0 || c !== 0 || d !== 1) return null
189
+ return finitePoint(Number(matrix[5]), Number(matrix[6]))
190
+ }
191
+
192
+ function finitePoint(x: number, y: number): { x: number; y: number } | null {
193
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null
194
+ }
195
+
196
+ function normalizePxValue(value: string, min: number): string | null {
197
+ const match = /^(\d+(?:\.\d+)?)px$/.exec(value.trim())
198
+ if (!match) return null
199
+ const number = Number(match[1])
200
+ if (!Number.isFinite(number) || number < min || number > MAX_DIMENSION_PX) return null
201
+ return `${Math.round(number)}px`
202
+ }
203
+
204
+ function patchOpenTagStyle(openTag: string, patch: Record<string, string>): string {
205
+ const styleMatch = /\sstyle=("([^"]*)"|'([^']*)')/i.exec(openTag)
206
+ const current = styleMatch ? parseStyle(styleMatch[2] ?? styleMatch[3] ?? "") : {}
207
+ const next = serializeStyle({ ...current, ...patch })
208
+ if (styleMatch) return openTag.slice(0, styleMatch.index) + ` style="${escapeAttr(next)}"` + openTag.slice(styleMatch.index + styleMatch[0].length)
209
+ return openTag.replace(/\s*\/?>$/, (ending) => ` style="${escapeAttr(next)}"${ending}`)
210
+ }
211
+
212
+ function parseStyle(style: string): Record<string, string> {
213
+ const result: Record<string, string> = {}
214
+ for (const part of style.split(";")) {
215
+ const index = part.indexOf(":")
216
+ if (index < 0) continue
217
+ const key = part.slice(0, index).trim().toLowerCase()
218
+ const value = part.slice(index + 1).trim()
219
+ if (key && value) result[key] = value
220
+ }
221
+ return result
222
+ }
223
+
224
+ function serializeStyle(style: Record<string, string>): string {
225
+ return Object.entries(style).map(([key, value]) => `${key}: ${value}`).join("; ")
226
+ }
227
+
228
+ function escapeAttr(value: string): string {
229
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;")
230
+ }
231
+
232
+ interface OpenTagRegion {
233
+ tagName: string
234
+ startOffset: number
235
+ openEndOffset: number
236
+ openTag: string
237
+ }
238
+
239
+ function scanOpeningTags(html: string): OpenTagRegion[] {
240
+ const tags: OpenTagRegion[] = []
241
+ let index = 0
242
+ while (index < html.length) {
243
+ const open = html.indexOf("<", index)
244
+ if (open < 0) break
245
+ if (html.startsWith("<!--", open)) {
246
+ const close = html.indexOf("-->", open + 4)
247
+ index = close < 0 ? html.length : close + 3
248
+ continue
249
+ }
250
+ const close = html.indexOf(">", open + 1)
251
+ if (close < 0) break
252
+ const raw = html.slice(open, close + 1)
253
+ const tagName = normalizeName(/^<\s*([a-zA-Z0-9-]+)/.exec(raw)?.[1] || "")
254
+ if (!tagName || /^<\s*[!/]/.test(raw) || /^<\s*\//.test(raw)) {
255
+ index = close + 1
256
+ continue
257
+ }
258
+ tags.push({ tagName, startOffset: open, openEndOffset: close + 1, openTag: raw })
259
+ if (tagName === "script" || tagName === "style") {
260
+ const closeTag = new RegExp(`</\\s*${tagName}\\s*>`, "ig")
261
+ closeTag.lastIndex = close + 1
262
+ const match = closeTag.exec(html)
263
+ index = match ? match.index + match[0].length : close + 1
264
+ } else {
265
+ index = close + 1
266
+ }
267
+ }
268
+ return tags
269
+ }
270
+
271
+ function attrValue(openTag: string, name: string): string | undefined {
272
+ const escaped = escapeRegExp(name)
273
+ const match = new RegExp(`\\s${escaped}=("([^"]*)"|'([^']*)')`, "i").exec(openTag)
274
+ return match ? match[2] ?? match[3] : undefined
275
+ }
276
+
277
+ function isSafeBoxTarget(openTag: string): boolean {
278
+ const tagName = normalizeName(/^<\s*([a-zA-Z0-9-]+)/.exec(openTag)?.[1] || "")
279
+ const className = attrValue(openTag, "class") || ""
280
+ const classes = className.split(/\s+/).map((item) => item.trim().toLowerCase()).filter(Boolean)
281
+ if (tagName === "div" && classes.length === 0) return false
282
+ if (/\s(?:data-chart|data-echarts|_echarts_instance_)\b/i.test(openTag)) return false
283
+ if (classes.some((name) => name === "slide" || name === "slide-canvas" || name === "deck" || name === "page")) return false
284
+ if (classes.some((name) => name.startsWith("revela-") || name.startsWith("echarts-") || name === "echart-container" || name === "echart-panel")) return false
285
+ if (classes.some((name) => /(^|-)echart($|-)/.test(name) || name === "chart-container" || name === "chart-panel")) return false
286
+ return true
287
+ }
288
+
289
+ function normalizeName(value: string | undefined): string {
290
+ return (value || "").trim().toLowerCase()
291
+ }
292
+
293
+ function escapeRegExp(value: string): string {
294
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/plugin.ts CHANGED
@@ -243,6 +243,9 @@ const server: Plugin = (async (pluginCtx) => {
243
243
  description: "Revela research agent — searches and collects raw materials for presentations",
244
244
  mode: "subagent",
245
245
  prompt: RESEARCH_PROMPT,
246
+ tools: {
247
+ "revela-decks": false,
248
+ },
246
249
  permission: {
247
250
  edit: "deny",
248
251
  bash: {
@@ -34,16 +34,6 @@ export default tool({
34
34
  risks: tool.schema.string().optional(),
35
35
  researchGaps: tool.schema.string().optional(),
36
36
  coveredSlides: tool.schema.string().optional(),
37
- storyWorkbench: tool.schema.string().optional(),
38
- workbenchNote: tool.schema.string().optional(),
39
- artifactCoverage: tool.schema.string().optional(),
40
- noRenderTargets: tool.schema.string().optional(),
41
- nextActions: tool.schema.string().optional(),
42
- missingClaims: tool.schema.string().optional(),
43
- affectedClaims: tool.schema.string().optional(),
44
- affectedSlides: tool.schema.string().optional(),
45
- notes: tool.schema.string().optional(),
46
- recommendedNextCommand: tool.schema.string().optional(),
47
37
  noClaims: tool.schema.string().optional(),
48
38
  none: tool.schema.string().optional(),
49
39
  }).optional(),
@@ -53,7 +43,13 @@ export default tool({
53
43
  roleLabel: tool.schema.string().optional(),
54
44
  narrativeJob: tool.schema.string().optional(),
55
45
  evidenceSummary: tool.schema.string().optional(),
46
+ supportRationale: tool.schema.string().optional().describe("Display-only localized explanation of why the evidence supports this claim. Do not add new causal logic beyond canonical evidence, supported scope, and relation rationale."),
47
+ supportedScope: tool.schema.string().optional().describe("Display-only localized version of the canonical supported scope."),
48
+ unsupportedScope: tool.schema.string().optional().describe("Display-only localized version of the canonical unsupported scope or evidence boundary."),
49
+ objectionsSummary: tool.schema.string().optional().describe("Display-only localized summary of objections tied to this claim."),
50
+ risksSummary: tool.schema.string().optional().describe("Display-only localized summary of risks tied to this claim."),
56
51
  riskOrGapSummary: tool.schema.string().optional(),
52
+ researchGapsSummary: tool.schema.string().optional().describe("Display-only localized summary of research gaps tied to this claim."),
57
53
  })).optional(),
58
54
  relations: tool.schema.array(tool.schema.object({
59
55
  fromClaimId: tool.schema.string(),