@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.
- package/lib/agents/research-prompt.ts +7 -7
- package/lib/commands/narrative.ts +6 -8
- package/lib/commands/research.ts +3 -2
- package/lib/narrative-state/display.ts +12 -40
- package/lib/narrative-state/map-html.ts +23 -137
- package/lib/narrative-state/map.ts +2 -320
- package/lib/refine/server.ts +472 -5
- package/lib/refine/visual-targets.ts +295 -0
- package/package.json +1 -1
- package/plugin.ts +3 -0
- package/tools/narrative-view.ts +6 -10
|
@@ -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, "&").replace(/"/g, """)
|
|
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
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: {
|
package/tools/narrative-view.ts
CHANGED
|
@@ -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(),
|