@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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DomainManager — manage revela domain definitions (formerly "industries").
|
|
3
|
+
*
|
|
4
|
+
* Domains are stored in ~/.config/revela/domains/<name>/.
|
|
5
|
+
* Each domain directory contains DOMAIN.md (required).
|
|
6
|
+
*
|
|
7
|
+
* Built-in domains are shipped with the npm package under domains/ and seeded
|
|
8
|
+
* to the config directory on first run.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: For backward compatibility, the .md files inside each domain directory
|
|
11
|
+
* are still named INDUSTRY.md. The config key `activeIndustry` is used as
|
|
12
|
+
* fallback for `activeDomain`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
cpSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "fs"
|
|
25
|
+
import { join, resolve, basename } from "path"
|
|
26
|
+
import { tmpdir } from "os"
|
|
27
|
+
import { parseFrontmatter } from "../frontmatter"
|
|
28
|
+
import {
|
|
29
|
+
DOMAINS_DIR,
|
|
30
|
+
DEFAULT_DOMAIN,
|
|
31
|
+
loadConfig,
|
|
32
|
+
saveConfig,
|
|
33
|
+
} from "../config"
|
|
34
|
+
import { childLog } from "../log"
|
|
35
|
+
|
|
36
|
+
const domainLog = childLog("domains")
|
|
37
|
+
|
|
38
|
+
// Seed directory: built-in domains shipped with this package.
|
|
39
|
+
const SEED_DIR = resolve(__dirname, "../..", "domains")
|
|
40
|
+
|
|
41
|
+
/** The markdown filename inside each domain directory. */
|
|
42
|
+
const DOMAIN_FILE = "INDUSTRY.md"
|
|
43
|
+
|
|
44
|
+
export interface DomainInfo {
|
|
45
|
+
name: string
|
|
46
|
+
description: string
|
|
47
|
+
author: string
|
|
48
|
+
version: string
|
|
49
|
+
skillText: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Seed
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Copy built-in domains from the package to ~/.config/revela/domains/.
|
|
58
|
+
* Always overwrites to keep bundled domains up to date.
|
|
59
|
+
* User-created domains (not in the seed directory) are never touched.
|
|
60
|
+
*/
|
|
61
|
+
export function seedBuiltinDomains(): void {
|
|
62
|
+
if (!existsSync(SEED_DIR)) return
|
|
63
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
64
|
+
|
|
65
|
+
for (const entry of readdirSync(SEED_DIR)) {
|
|
66
|
+
const src = join(SEED_DIR, entry)
|
|
67
|
+
if (!statSync(src).isDirectory()) continue
|
|
68
|
+
if (!existsSync(join(src, DOMAIN_FILE))) continue
|
|
69
|
+
|
|
70
|
+
const dst = join(DOMAINS_DIR, entry)
|
|
71
|
+
mkdirSync(dst, { recursive: true })
|
|
72
|
+
cpSync(src, dst, { recursive: true, force: true })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Parse
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/** Parse an INDUSTRY.md file into DomainInfo. Returns null on any error. */
|
|
81
|
+
export function parseDomainFile(filePath: string): DomainInfo | null {
|
|
82
|
+
try {
|
|
83
|
+
const text = readFileSync(filePath, "utf-8")
|
|
84
|
+
const { meta, body } = parseFrontmatter(text)
|
|
85
|
+
return {
|
|
86
|
+
name: meta.name || basename(join(filePath, "..")),
|
|
87
|
+
description: meta.description || "",
|
|
88
|
+
author: meta.author || "unknown",
|
|
89
|
+
version: meta.version || "0.0.0",
|
|
90
|
+
skillText: body,
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
domainLog.warn("failed to parse domain file — skipping", {
|
|
94
|
+
filePath,
|
|
95
|
+
error: e instanceof Error ? e.message : String(e),
|
|
96
|
+
})
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Public API
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/** List all installed domains, sorted by name. */
|
|
106
|
+
export function listDomains(): DomainInfo[] {
|
|
107
|
+
if (!existsSync(DOMAINS_DIR)) return []
|
|
108
|
+
const results: DomainInfo[] = []
|
|
109
|
+
|
|
110
|
+
for (const entry of readdirSync(DOMAINS_DIR).sort()) {
|
|
111
|
+
const dir = join(DOMAINS_DIR, entry)
|
|
112
|
+
if (!statSync(dir).isDirectory()) continue
|
|
113
|
+
const mdPath = join(dir, DOMAIN_FILE)
|
|
114
|
+
if (!existsSync(mdPath)) continue
|
|
115
|
+
const info = parseDomainFile(mdPath)
|
|
116
|
+
if (info) results.push(info)
|
|
117
|
+
}
|
|
118
|
+
return results
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Get the name of the currently active domain. */
|
|
122
|
+
export function activeDomain(): string {
|
|
123
|
+
const cfg = loadConfig()
|
|
124
|
+
return cfg.activeDomain || cfg.activeIndustry || DEFAULT_DOMAIN
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Set the active domain. Throws if domain is not installed. */
|
|
128
|
+
export function activateDomain(name: string): void {
|
|
129
|
+
if (!domainExists(name)) {
|
|
130
|
+
throw new Error(`Domain '${name}' is not installed`)
|
|
131
|
+
}
|
|
132
|
+
const cfg = loadConfig()
|
|
133
|
+
cfg.activeDomain = name
|
|
134
|
+
saveConfig(cfg)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Get the skill text body from a domain's INDUSTRY.md. */
|
|
138
|
+
export function getDomainSkillMd(name?: string): string {
|
|
139
|
+
const domainName = name || activeDomain()
|
|
140
|
+
const mdPath = join(DOMAINS_DIR, domainName, DOMAIN_FILE)
|
|
141
|
+
if (!existsSync(mdPath)) {
|
|
142
|
+
throw new Error(`Domain '${domainName}' is not installed`)
|
|
143
|
+
}
|
|
144
|
+
const info = parseDomainFile(mdPath)
|
|
145
|
+
if (!info) {
|
|
146
|
+
throw new Error(`Failed to parse ${DOMAIN_FILE} for '${domainName}'`)
|
|
147
|
+
}
|
|
148
|
+
return info.skillText
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Remove an installed domain. Throws if not found or is the protected default. */
|
|
152
|
+
export function removeDomain(name: string): void {
|
|
153
|
+
if (name === DEFAULT_DOMAIN) {
|
|
154
|
+
throw new Error(`Cannot remove the built-in '${DEFAULT_DOMAIN}' domain`)
|
|
155
|
+
}
|
|
156
|
+
const dir = join(DOMAINS_DIR, name)
|
|
157
|
+
if (!existsSync(dir)) {
|
|
158
|
+
throw new Error(`Domain '${name}' is not installed`)
|
|
159
|
+
}
|
|
160
|
+
rmSync(dir, { recursive: true, force: true })
|
|
161
|
+
// Reset active domain if it was the removed one
|
|
162
|
+
if (activeDomain() === name) {
|
|
163
|
+
activateDomain(DEFAULT_DOMAIN)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Install a domain from a source.
|
|
169
|
+
*
|
|
170
|
+
* Supported sources:
|
|
171
|
+
* - Local path (starts with `./` or `/` or exists on disk)
|
|
172
|
+
* - URL (starts with `http://` or `https://`) — downloads zip
|
|
173
|
+
* - GitHub shorthand `github:user/repo` — converted to zip URL
|
|
174
|
+
*
|
|
175
|
+
* Returns the installed domain name.
|
|
176
|
+
*/
|
|
177
|
+
export async function installDomain(
|
|
178
|
+
source: string,
|
|
179
|
+
name?: string,
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
182
|
+
return installFromUrl(source, name)
|
|
183
|
+
}
|
|
184
|
+
if (source.startsWith("github:")) {
|
|
185
|
+
const repo = source.slice("github:".length)
|
|
186
|
+
const url = `https://github.com/${repo}/archive/refs/heads/main.zip`
|
|
187
|
+
return installFromUrl(url, name)
|
|
188
|
+
}
|
|
189
|
+
// Local path
|
|
190
|
+
return installFromPath(source, name)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Private helpers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function domainExists(name: string): boolean {
|
|
198
|
+
const dir = join(DOMAINS_DIR, name)
|
|
199
|
+
return existsSync(dir) && existsSync(join(dir, DOMAIN_FILE))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function installFromPath(srcPath: string, name?: string): string {
|
|
203
|
+
const resolved = resolve(srcPath)
|
|
204
|
+
if (!existsSync(resolved)) {
|
|
205
|
+
throw new Error(`Path does not exist: ${resolved}`)
|
|
206
|
+
}
|
|
207
|
+
if (!existsSync(join(resolved, DOMAIN_FILE))) {
|
|
208
|
+
throw new Error(`No ${DOMAIN_FILE} found in ${resolved}`)
|
|
209
|
+
}
|
|
210
|
+
const info = parseDomainFile(join(resolved, DOMAIN_FILE))
|
|
211
|
+
const domainName = name || info?.name || basename(resolved)
|
|
212
|
+
const target = join(DOMAINS_DIR, domainName)
|
|
213
|
+
|
|
214
|
+
mkdirSync(DOMAINS_DIR, { recursive: true })
|
|
215
|
+
if (existsSync(target)) {
|
|
216
|
+
rmSync(target, { recursive: true, force: true })
|
|
217
|
+
}
|
|
218
|
+
cpSync(resolved, target, { recursive: true })
|
|
219
|
+
return domainName
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function installFromUrl(url: string, name?: string): Promise<string> {
|
|
223
|
+
const tmp = join(tmpdir(), `revela-domain-${Date.now()}`)
|
|
224
|
+
mkdirSync(tmp, { recursive: true })
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const zipPath = join(tmp, "domain.zip")
|
|
228
|
+
const response = await fetch(url)
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
|
|
231
|
+
}
|
|
232
|
+
const buffer = new Uint8Array(await response.arrayBuffer())
|
|
233
|
+
writeFileSync(zipPath, buffer)
|
|
234
|
+
|
|
235
|
+
const extractDir = join(tmp, "extracted")
|
|
236
|
+
mkdirSync(extractDir)
|
|
237
|
+
|
|
238
|
+
const proc = Bun.spawnSync(["unzip", "-q", "-o", zipPath, "-d", extractDir])
|
|
239
|
+
if (proc.exitCode !== 0) {
|
|
240
|
+
throw new Error(`Failed to extract zip: ${proc.stderr.toString()}`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const candidates = [extractDir]
|
|
244
|
+
for (const entry of readdirSync(extractDir)) {
|
|
245
|
+
const p = join(extractDir, entry)
|
|
246
|
+
if (statSync(p).isDirectory()) candidates.push(p)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
if (existsSync(join(candidate, DOMAIN_FILE))) {
|
|
251
|
+
return installFromPath(candidate, name)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`No ${DOMAIN_FILE} found inside the downloaded zip`)
|
|
255
|
+
} finally {
|
|
256
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML frontmatter parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles the simple `key: value` format used by DESIGN.md / DOMAIN.md files.
|
|
5
|
+
* No dependency on external YAML libraries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Frontmatter {
|
|
9
|
+
/** Key-value pairs from the YAML block between `---` fences. */
|
|
10
|
+
meta: Record<string, string>
|
|
11
|
+
/** Everything after the closing `---`, trimmed. */
|
|
12
|
+
body: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a markdown file with optional YAML frontmatter.
|
|
17
|
+
*
|
|
18
|
+
* Format:
|
|
19
|
+
* ```
|
|
20
|
+
* ---
|
|
21
|
+
* key1: value1
|
|
22
|
+
* key2: value2
|
|
23
|
+
* ---
|
|
24
|
+
*
|
|
25
|
+
* Body text...
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* If the file does not start with `---`, the entire content is returned as body
|
|
29
|
+
* with an empty meta object.
|
|
30
|
+
*/
|
|
31
|
+
export function parseFrontmatter(text: string): Frontmatter {
|
|
32
|
+
const lines = text.split("\n")
|
|
33
|
+
|
|
34
|
+
if (!lines.length || lines[0].trim() !== "---") {
|
|
35
|
+
return { meta: {}, body: text.trim() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const meta: Record<string, string> = {}
|
|
39
|
+
let endIdx = -1
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i < lines.length; i++) {
|
|
42
|
+
if (lines[i].trim() === "---") {
|
|
43
|
+
endIdx = i
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
const colonPos = lines[i].indexOf(":")
|
|
47
|
+
if (colonPos !== -1) {
|
|
48
|
+
const key = lines[i].slice(0, colonPos).trim()
|
|
49
|
+
const value = lines[i].slice(colonPos + 1).trim()
|
|
50
|
+
if (key) {
|
|
51
|
+
meta[key] = value
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (endIdx === -1) {
|
|
57
|
+
// No closing ---, treat entire content as body
|
|
58
|
+
return { meta: {}, body: text.trim() }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const body = lines.slice(endIdx + 1).join("\n").trim()
|
|
62
|
+
return { meta, body }
|
|
63
|
+
}
|
package/lib/log.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Logger } from "tslog"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Revela structured logger (tslog).
|
|
5
|
+
*
|
|
6
|
+
* Log levels:
|
|
7
|
+
* 0 = silly, 1 = trace, 2 = debug, 3 = info, 4 = warn, 5 = error, 6 = fatal
|
|
8
|
+
*
|
|
9
|
+
* Set REVELA_DEBUG=1 to enable debug-level output (minLevel 2).
|
|
10
|
+
* Default minLevel is 3 (info) in production.
|
|
11
|
+
*/
|
|
12
|
+
const minLevel = process.env.REVELA_DEBUG === "1" ? 2 : 3
|
|
13
|
+
|
|
14
|
+
export const log = new Logger({
|
|
15
|
+
name: "revela",
|
|
16
|
+
minLevel,
|
|
17
|
+
type: "json",
|
|
18
|
+
hideLogPositionForProduction: true,
|
|
19
|
+
overwrite: {
|
|
20
|
+
transportJSON: (logObj: unknown) => {
|
|
21
|
+
process.stderr.write(JSON.stringify(logObj) + "\n")
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a child logger for a specific sub-module.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const qaLog = childLog("qa")
|
|
31
|
+
* qaLog.info("measuring slides", { file: htmlPath })
|
|
32
|
+
*/
|
|
33
|
+
export function childLog(name: string) {
|
|
34
|
+
return log.getSubLogger({ name })
|
|
35
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt builder — assembles the three-layer system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1: SKILL.md — core protocol (conversation flow, HTML rules, quality)
|
|
5
|
+
* Layer 2: DOMAIN.md — domain knowledge (report structure, terminology)
|
|
6
|
+
* Layer 3: DESIGN.md — visual style (colors, fonts, animations, layout)
|
|
7
|
+
*
|
|
8
|
+
* When the active DESIGN.md has @section markers, only the global section,
|
|
9
|
+
* layouts section, and a generated component index are injected into the
|
|
10
|
+
* system prompt. Components, charts, and guide sections are available on
|
|
11
|
+
* demand via the revela-designs tool read action.
|
|
12
|
+
*
|
|
13
|
+
* When no markers are present (third-party designs), the full DESIGN.md body
|
|
14
|
+
* is injected unchanged (backward-compatible fallback).
|
|
15
|
+
*
|
|
16
|
+
* The combined prompt is written to ~/.config/revela/_active-prompt.md
|
|
17
|
+
* and referenced by agents via `{file:~/.config/revela/_active-prompt.md}`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"
|
|
21
|
+
import { join, resolve } from "path"
|
|
22
|
+
import {
|
|
23
|
+
CONFIG_DIR,
|
|
24
|
+
DESIGNS_DIR,
|
|
25
|
+
ACTIVE_PROMPT_FILE,
|
|
26
|
+
} from "./config"
|
|
27
|
+
import {
|
|
28
|
+
activeDesign,
|
|
29
|
+
getDesignSkillMd,
|
|
30
|
+
parseDesignSections,
|
|
31
|
+
generateComponentIndex,
|
|
32
|
+
} from "./design/designs"
|
|
33
|
+
import { activeDomain, getDomainSkillMd } from "./domain/domains"
|
|
34
|
+
import { parseFrontmatter } from "./frontmatter"
|
|
35
|
+
import { SLIDE_TYPES, type SlideType } from "./qa/checks"
|
|
36
|
+
import { childLog } from "./log"
|
|
37
|
+
|
|
38
|
+
const promptLog = childLog("prompt-builder")
|
|
39
|
+
|
|
40
|
+
/** Path to SKILL.md shipped with this package. */
|
|
41
|
+
const SKILL_MD_PATH = resolve(__dirname, "..", "skill", "SKILL.md")
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Human-readable descriptions for each slide type.
|
|
45
|
+
* Used to generate the data-slide-type table injected into SKILL.md.
|
|
46
|
+
* Kept here (not in checks.ts) — these are prose for the AI, not QA logic.
|
|
47
|
+
*/
|
|
48
|
+
const SLIDE_TYPE_DESCRIPTIONS: Record<SlideType, string> = {
|
|
49
|
+
cover: "Title / opening slide",
|
|
50
|
+
toc: "Table of contents",
|
|
51
|
+
content: "Regular content slides (default)",
|
|
52
|
+
summary: "Key takeaways slide",
|
|
53
|
+
closing: "Thank-you / Q&A / contact slide",
|
|
54
|
+
divider: "Section-break / transition slide",
|
|
55
|
+
"thank-you": "Alias for `closing`",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the combined system prompt and write it to _active-prompt.md.
|
|
60
|
+
*
|
|
61
|
+
* @param designName - Override design (defaults to active)
|
|
62
|
+
* @param domainName - Override domain (defaults to active)
|
|
63
|
+
* @returns The path to the written file.
|
|
64
|
+
*/
|
|
65
|
+
export function buildPrompt(designName?: string, domainName?: string): string {
|
|
66
|
+
const design = designName || activeDesign()
|
|
67
|
+
const domain = domainName || activeDomain()
|
|
68
|
+
|
|
69
|
+
// Layer 1 — SKILL.md (with dynamic slide-type table injected)
|
|
70
|
+
let coreSkill = readFileSync(SKILL_MD_PATH, "utf-8")
|
|
71
|
+
coreSkill = injectSlideTypes(coreSkill)
|
|
72
|
+
|
|
73
|
+
// Check for preview.html
|
|
74
|
+
const designDir = join(DESIGNS_DIR, design)
|
|
75
|
+
const hasPreview = existsSync(join(designDir, "preview.html"))
|
|
76
|
+
const previewLine = hasPreview
|
|
77
|
+
? "<!-- - preview.html — canonical visual reference (read this before generating slides) -->"
|
|
78
|
+
: "<!-- - (no preview.html for this design) -->"
|
|
79
|
+
|
|
80
|
+
// Layer 2 — DOMAIN.md skill text (may be empty for "general")
|
|
81
|
+
let domainSkill = ""
|
|
82
|
+
try {
|
|
83
|
+
domainSkill = getDomainSkillMd(domain)
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Domain not installed or empty — proceed without domain layer
|
|
86
|
+
promptLog.warn("domain skill not found — building without domain layer", {
|
|
87
|
+
domain,
|
|
88
|
+
error: e instanceof Error ? e.message : String(e),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Layer 3 — DESIGN.md: marker-aware or full-text fallback
|
|
93
|
+
const designSkill = buildDesignLayer(design)
|
|
94
|
+
|
|
95
|
+
// Assemble header
|
|
96
|
+
const header =
|
|
97
|
+
`<!-- Active design: ${design} -->\n` +
|
|
98
|
+
`<!-- Active domain: ${domain} -->\n` +
|
|
99
|
+
`<!-- Design files: ${designDir}/ -->\n` +
|
|
100
|
+
`<!-- - DESIGN.md — metadata + style instructions (injected below) -->\n` +
|
|
101
|
+
`${previewLine}\n\n`
|
|
102
|
+
|
|
103
|
+
// Three-layer concatenation: Header → SKILL → Domain → Design
|
|
104
|
+
const parts = [header, coreSkill]
|
|
105
|
+
if (domainSkill) {
|
|
106
|
+
parts.push(`\n\n---\n\n${domainSkill}`)
|
|
107
|
+
}
|
|
108
|
+
parts.push(`\n\n---\n\n${designSkill}`)
|
|
109
|
+
|
|
110
|
+
const prompt = parts.join("")
|
|
111
|
+
|
|
112
|
+
// Write to _active-prompt.md
|
|
113
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
114
|
+
writeFileSync(ACTIVE_PROMPT_FILE, prompt, "utf-8")
|
|
115
|
+
promptLog.info("prompt rebuilt", { design, domain, bytes: prompt.length })
|
|
116
|
+
|
|
117
|
+
return ACTIVE_PROMPT_FILE
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the design layer text.
|
|
122
|
+
*
|
|
123
|
+
* If the DESIGN.md has markers:
|
|
124
|
+
* - Always include @section:global
|
|
125
|
+
* - Always include @section:layouts (layout primitives — always needed)
|
|
126
|
+
* - Include a generated Component Index table (lightweight catalog)
|
|
127
|
+
* - Omit @section:components detail, @section:charts, @section:guide
|
|
128
|
+
* (available on demand via revela-designs tool)
|
|
129
|
+
*
|
|
130
|
+
* If no markers: return the full DESIGN.md body unchanged.
|
|
131
|
+
*/
|
|
132
|
+
function buildDesignLayer(designName: string): string {
|
|
133
|
+
const mdPath = join(DESIGNS_DIR, designName, "DESIGN.md")
|
|
134
|
+
if (!existsSync(mdPath)) {
|
|
135
|
+
throw new Error(`Design '${designName}' is not installed`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const raw = readFileSync(mdPath, "utf-8")
|
|
139
|
+
const { body } = parseFrontmatter(raw)
|
|
140
|
+
const { sections, components, hasMarkers } = parseDesignSections(body)
|
|
141
|
+
|
|
142
|
+
if (!hasMarkers) {
|
|
143
|
+
// Backward-compatible: full text injection
|
|
144
|
+
return body
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const layerParts: string[] = []
|
|
148
|
+
|
|
149
|
+
// 1. Global section (colors, typography, CSS, JS class, HTML structure)
|
|
150
|
+
if (sections["global"]) {
|
|
151
|
+
layerParts.push(sections["global"])
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Component Index — compact catalog
|
|
155
|
+
const index = generateComponentIndex(components)
|
|
156
|
+
if (index) {
|
|
157
|
+
layerParts.push(index)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Layouts section — always resident (needed for every slide)
|
|
161
|
+
if (sections["layouts"]) {
|
|
162
|
+
layerParts.push(sections["layouts"])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 4. On-demand note
|
|
166
|
+
layerParts.push(
|
|
167
|
+
[
|
|
168
|
+
"### On-Demand Design Sections",
|
|
169
|
+
"",
|
|
170
|
+
"The following design sections are available on demand. Fetch them with",
|
|
171
|
+
"the `revela-designs` tool (`action: \"read\"`) before using them:",
|
|
172
|
+
"",
|
|
173
|
+
"| Section | Fetch with |",
|
|
174
|
+
"|---|---|",
|
|
175
|
+
"| Component CSS/HTML details | `component: \"<name>\"` (see Component Index above) |",
|
|
176
|
+
"| Data Visualization (ECharts) | `section: \"charts\"` |",
|
|
177
|
+
"| Composition Guide & Do/Don't | `section: \"guide\"` |",
|
|
178
|
+
].join("\n"),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return layerParts.join("\n\n---\n\n")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replace the <!-- @slide-types --> placeholder in SKILL.md with a generated
|
|
186
|
+
* markdown table built from SLIDE_TYPES (single source of truth in checks.ts).
|
|
187
|
+
*/
|
|
188
|
+
function injectSlideTypes(skillMd: string): string {
|
|
189
|
+
const rows = SLIDE_TYPES.map(
|
|
190
|
+
(t) => `| \`${t}\` | ${SLIDE_TYPE_DESCRIPTIONS[t]} |`,
|
|
191
|
+
)
|
|
192
|
+
const table = ["| Value | Use for |", "|-------|---------|", ...rows].join("\n")
|
|
193
|
+
return skillMd.replace("<!-- @slide-types -->", table)
|
|
194
|
+
}
|