@cyber-dash-tech/revela 0.6.1 → 0.7.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/commands/edit.ts +69 -0
- package/lib/commands/help.ts +3 -2
- package/lib/commands/init.ts +1 -9
- package/lib/commands/remember.ts +1 -6
- package/lib/commands/review.ts +0 -7
- package/lib/decks-memory.ts +0 -464
- package/lib/edit/deck-state.ts +125 -0
- package/lib/edit/prompt.ts +81 -0
- package/lib/edit/resolve-deck.ts +99 -0
- package/lib/edit/server.ts +851 -0
- package/package.json +1 -1
- package/plugin.ts +8 -13
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync } from "fs"
|
|
2
|
+
import { activeDesign } from "../design/designs"
|
|
3
|
+
import { activeDomain } from "../domain/domains"
|
|
4
|
+
import {
|
|
5
|
+
defaultRequiredInputs,
|
|
6
|
+
readOrCreateDecksState,
|
|
7
|
+
reviewDeckState,
|
|
8
|
+
upsertDeck,
|
|
9
|
+
upsertSlides,
|
|
10
|
+
writeDecksState,
|
|
11
|
+
type DeckStateReadinessResult,
|
|
12
|
+
type SlideSpec,
|
|
13
|
+
} from "../decks-state"
|
|
14
|
+
import type { EditableDeck } from "./resolve-deck"
|
|
15
|
+
|
|
16
|
+
export interface EditDeckStatePreflightResult {
|
|
17
|
+
changed: boolean
|
|
18
|
+
readiness: DeckStateReadinessResult
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDeck): EditDeckStatePreflightResult {
|
|
22
|
+
let state = readOrCreateDecksState(workspaceRoot)
|
|
23
|
+
const existing = state.decks[deck.slug]
|
|
24
|
+
const existingReady = existing?.writeReadiness?.status === "ready" && existing.writeReadiness.blockers.length === 0
|
|
25
|
+
let changed = !existing || existing.outputPath !== deck.file
|
|
26
|
+
|
|
27
|
+
state = upsertDeck(state, {
|
|
28
|
+
...existing,
|
|
29
|
+
slug: deck.slug,
|
|
30
|
+
goal: existing?.goal || `Edit existing Revela deck ${deck.slug}.`,
|
|
31
|
+
audience: existing?.audience || "Existing deck viewers",
|
|
32
|
+
language: existing?.language || "en",
|
|
33
|
+
slideCount: existing?.slideCount || inferSlideCount(deck.absoluteFile),
|
|
34
|
+
outputPath: deck.file,
|
|
35
|
+
theme: {
|
|
36
|
+
design: existing?.theme?.design || safeActiveDesign(),
|
|
37
|
+
domain: existing?.theme?.domain || safeActiveDomain(),
|
|
38
|
+
},
|
|
39
|
+
requiredInputs: defaultRequiredInputs({
|
|
40
|
+
...existing?.requiredInputs,
|
|
41
|
+
topicClarified: true,
|
|
42
|
+
audienceClarified: true,
|
|
43
|
+
slideCountDecided: true,
|
|
44
|
+
languageDecided: true,
|
|
45
|
+
visualStyleSelected: true,
|
|
46
|
+
sourceMaterialsIdentified: true,
|
|
47
|
+
researchNeedAssessed: true,
|
|
48
|
+
researchFindingsRead: true,
|
|
49
|
+
slidePlanConfirmed: true,
|
|
50
|
+
designLayoutsFetched: true,
|
|
51
|
+
}),
|
|
52
|
+
researchPlan: existing?.researchPlan || [],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const current = state.decks[deck.slug]
|
|
56
|
+
if (current.slides.length === 0) {
|
|
57
|
+
state = upsertSlides(state, deck.slug, inferSlides(deck.absoluteFile))
|
|
58
|
+
changed = true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reviewed = reviewDeckState(state, deck.slug)
|
|
62
|
+
writeDecksState(workspaceRoot, reviewed.state)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
changed: changed || !existingReady,
|
|
66
|
+
readiness: reviewed.result,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferSlideCount(filePath: string): number {
|
|
71
|
+
return inferSlides(filePath).length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferSlides(filePath: string): SlideSpec[] {
|
|
75
|
+
const html = readFileSync(filePath, "utf-8")
|
|
76
|
+
const chunks = html.match(/<section\b[\s\S]*?<\/section>/gi) || [html]
|
|
77
|
+
return chunks.map((chunk, index) => {
|
|
78
|
+
const title = extractTitle(chunk) || `Slide ${index + 1}`
|
|
79
|
+
return {
|
|
80
|
+
index: index + 1,
|
|
81
|
+
title,
|
|
82
|
+
purpose: "Existing HTML slide prepared for targeted visual edits.",
|
|
83
|
+
layout: "existing-html",
|
|
84
|
+
qa: /slide-qa=["']true["']/i.test(chunk),
|
|
85
|
+
components: ["existing-html"],
|
|
86
|
+
content: {
|
|
87
|
+
headline: title,
|
|
88
|
+
body: [extractText(chunk) || "Existing HTML slide content."],
|
|
89
|
+
},
|
|
90
|
+
evidence: [],
|
|
91
|
+
visuals: [],
|
|
92
|
+
status: "ready",
|
|
93
|
+
notes: "Inferred automatically by /revela edit preflight.",
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTitle(html: string): string {
|
|
99
|
+
const match = /<(?:h1|h2|h3|title)\b[^>]*>([\s\S]*?)<\/(?:h1|h2|h3|title)>/i.exec(html)
|
|
100
|
+
return normalizeText(match?.[1] || "").slice(0, 160)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractText(html: string): string {
|
|
104
|
+
return normalizeText(html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ")).slice(0, 600)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeText(value: string): string {
|
|
108
|
+
return value.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\s+/g, " ").trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function safeActiveDesign(): string {
|
|
112
|
+
try {
|
|
113
|
+
return activeDesign()
|
|
114
|
+
} catch {
|
|
115
|
+
return "aurora"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeActiveDomain(): string {
|
|
120
|
+
try {
|
|
121
|
+
return activeDomain()
|
|
122
|
+
} catch {
|
|
123
|
+
return "general"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface EditSelectedElementPayload {
|
|
2
|
+
slideIndex?: number
|
|
3
|
+
slideTitle?: string
|
|
4
|
+
selector?: string
|
|
5
|
+
domPath?: string
|
|
6
|
+
tagName?: string
|
|
7
|
+
id?: string
|
|
8
|
+
classList?: string[]
|
|
9
|
+
text?: string
|
|
10
|
+
outerHTMLExcerpt?: string
|
|
11
|
+
nearbyText?: string
|
|
12
|
+
boundingBox?: Record<string, unknown>
|
|
13
|
+
viewport?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EditCommentDraftPayload {
|
|
17
|
+
comment: string
|
|
18
|
+
elements: EditSelectedElementPayload[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EditCommentPayload extends EditSelectedElementPayload {
|
|
22
|
+
deck: string
|
|
23
|
+
file: string
|
|
24
|
+
comment: string
|
|
25
|
+
elements?: EditSelectedElementPayload[]
|
|
26
|
+
comments?: EditCommentDraftPayload[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildEditPrompt(payload: EditCommentPayload): string {
|
|
30
|
+
const elements = payload.elements?.length
|
|
31
|
+
? payload.elements
|
|
32
|
+
: [{
|
|
33
|
+
slideIndex: payload.slideIndex,
|
|
34
|
+
slideTitle: payload.slideTitle,
|
|
35
|
+
selector: payload.selector,
|
|
36
|
+
domPath: payload.domPath,
|
|
37
|
+
tagName: payload.tagName,
|
|
38
|
+
id: payload.id,
|
|
39
|
+
classList: payload.classList ?? [],
|
|
40
|
+
text: payload.text,
|
|
41
|
+
outerHTMLExcerpt: payload.outerHTMLExcerpt,
|
|
42
|
+
nearbyText: payload.nearbyText,
|
|
43
|
+
boundingBox: payload.boundingBox,
|
|
44
|
+
viewport: payload.viewport,
|
|
45
|
+
}]
|
|
46
|
+
const comments = payload.comments?.length
|
|
47
|
+
? payload.comments
|
|
48
|
+
: [{
|
|
49
|
+
comment: payload.comment,
|
|
50
|
+
elements,
|
|
51
|
+
}]
|
|
52
|
+
|
|
53
|
+
const compact = {
|
|
54
|
+
deck: payload.deck,
|
|
55
|
+
file: payload.file,
|
|
56
|
+
comments,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `The user left a visual edit comment on a Revela slide deck.
|
|
60
|
+
|
|
61
|
+
Target deck: ${payload.deck}
|
|
62
|
+
Target file: ${payload.file}
|
|
63
|
+
|
|
64
|
+
Structured selection payload:
|
|
65
|
+
|
|
66
|
+
\`\`\`json
|
|
67
|
+
${JSON.stringify(compact, null, 2)}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
Instructions:
|
|
71
|
+
- Make the smallest targeted change that satisfies the user's comment.
|
|
72
|
+
- If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
|
|
73
|
+
- Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
|
|
74
|
+
- Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
|
|
75
|
+
- Do not rewrite unrelated slides or broad sections of the deck.
|
|
76
|
+
- Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.
|
|
77
|
+
- Before patching or writing ${"`decks/*.html`"}, ensure ${"`DECKS.json`"} contains this deck and call ${"`revela-decks`"} with action ${"`review`"}. If ${"`DECKS.json`"} or the deck entry is missing, initialize/upsert the deck state with ${"`revela-decks`"} first. If readiness remains blocked, explain the blockers instead of forcing the edit.
|
|
78
|
+
- Apply the edit to ${payload.file} only after readiness allows deck HTML changes.
|
|
79
|
+
- After editing, run ${"`revela-qa`"} on ${payload.file} and fix any relevant regressions caused by the edit.
|
|
80
|
+
- If the comment is ambiguous, ask one concise clarification question instead of guessing.`
|
|
81
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from "fs"
|
|
2
|
+
import { basename, relative, resolve, sep } from "path"
|
|
3
|
+
import { DECKS_STATE_FILE, hasDecksState, isDeckHtmlPath, readDecksState } from "../decks-state"
|
|
4
|
+
|
|
5
|
+
export interface EditableDeck {
|
|
6
|
+
slug: string
|
|
7
|
+
file: string
|
|
8
|
+
absoluteFile: string
|
|
9
|
+
source: "decks-state" | "fallback" | "file-path"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveEditableDeck(workspaceRoot: string, input: string): EditableDeck {
|
|
13
|
+
const requested = input.trim()
|
|
14
|
+
if (!requested) throw new Error("Usage: /revela edit <deck-slug|decks/file.html>")
|
|
15
|
+
|
|
16
|
+
const slug = normalizeSlug(requested)
|
|
17
|
+
|
|
18
|
+
if (hasDecksState(workspaceRoot)) {
|
|
19
|
+
const state = readDecksState(workspaceRoot)
|
|
20
|
+
const deck = state.decks[requested] ?? (slug ? state.decks[slug] : undefined)
|
|
21
|
+
if (deck) {
|
|
22
|
+
return resolveDeckFile(workspaceRoot, deck.slug, deck.outputPath, "decks-state")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (looksLikePath(requested)) {
|
|
27
|
+
return resolvePathTarget(workspaceRoot, requested)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!slug) throw new Error("Deck target must be a deck slug or decks/*.html path.")
|
|
31
|
+
|
|
32
|
+
return resolveDeckFile(workspaceRoot, slug, `decks/${slug}.html`, "fallback")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolvePathTarget(workspaceRoot: string, requested: string): EditableDeck {
|
|
36
|
+
if (isAbsoluteLike(requested)) {
|
|
37
|
+
throw new Error("/revela edit only accepts workspace-relative decks/*.html paths.")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalized = normalizePath(requested).replace(/^\.\//, "")
|
|
41
|
+
if (!isDeckHtmlPath(normalized)) {
|
|
42
|
+
throw new Error("/revela edit file paths must point to decks/*.html.")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const slug = normalizeSlug(basename(normalized, ".html"))
|
|
46
|
+
if (!slug) throw new Error("Deck target must be a deck slug or decks/*.html path.")
|
|
47
|
+
return resolveDeckFile(workspaceRoot, slug, normalized, "file-path")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveDeckFile(
|
|
51
|
+
workspaceRoot: string,
|
|
52
|
+
slug: string,
|
|
53
|
+
file: string,
|
|
54
|
+
source: EditableDeck["source"],
|
|
55
|
+
): EditableDeck {
|
|
56
|
+
if (!isDeckHtmlPath(file)) {
|
|
57
|
+
throw new Error(`${DECKS_STATE_FILE} deck outputPath must be decks/*.html, got ${file || "missing"}.`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const root = resolve(workspaceRoot)
|
|
61
|
+
const absoluteFile = resolve(root, file)
|
|
62
|
+
if (!isInside(root, absoluteFile)) {
|
|
63
|
+
throw new Error(`Resolved deck file is outside the workspace: ${file}`)
|
|
64
|
+
}
|
|
65
|
+
if (!existsSync(absoluteFile)) {
|
|
66
|
+
throw new Error(`Deck HTML not found: ${workspaceRelative(root, absoluteFile)}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
slug: normalizeSlug(slug),
|
|
71
|
+
file: workspaceRelative(root, absoluteFile),
|
|
72
|
+
absoluteFile,
|
|
73
|
+
source,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isInside(root: string, target: string): boolean {
|
|
78
|
+
return target === root || target.startsWith(root.endsWith(sep) ? root : root + sep)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function workspaceRelative(root: string, target: string): string {
|
|
82
|
+
return relative(root, target).split(sep).join("/")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function looksLikePath(value: string): boolean {
|
|
86
|
+
return value.includes("/") || value.includes("\\") || value.endsWith(".html")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isAbsoluteLike(value: string): boolean {
|
|
90
|
+
return value.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizePath(value: string): string {
|
|
94
|
+
return value.replace(/\\/g, "/")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeSlug(value: string): string {
|
|
98
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
99
|
+
}
|