@cyber-dash-tech/revela 0.17.6 → 0.17.7

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 (35) hide show
  1. package/README.md +26 -46
  2. package/README.zh-CN.md +26 -46
  3. package/bin/revela.ts +98 -0
  4. package/lib/edit/prompt.ts +6 -2
  5. package/lib/edit/server.ts +2 -2
  6. package/lib/inspect/prompt.ts +5 -1
  7. package/lib/refine/comment-requests.ts +77 -0
  8. package/lib/refine/open.ts +5 -2
  9. package/lib/refine/prompt-bridge.ts +219 -0
  10. package/lib/refine/qa-suppression.ts +41 -0
  11. package/lib/refine/server.ts +122 -34
  12. package/lib/runtime/index.ts +225 -0
  13. package/lib/runtime/research.ts +175 -0
  14. package/lib/runtime/review.ts +270 -0
  15. package/lib/runtime/story.ts +53 -0
  16. package/package.json +6 -1
  17. package/plugin.ts +4 -2
  18. package/plugins/revela/.codex-plugin/plugin.json +37 -0
  19. package/plugins/revela/.mcp.json +11 -0
  20. package/plugins/revela/assets/README.md +2 -0
  21. package/plugins/revela/hooks/hooks.json +28 -0
  22. package/plugins/revela/hooks/revela_guard.ts +10 -0
  23. package/plugins/revela/hooks/revela_post_write_notice.ts +18 -0
  24. package/plugins/revela/mcp/revela-server.ts +504 -0
  25. package/plugins/revela/mcp/runtime-resolver.ts +109 -0
  26. package/plugins/revela/skills/revela-design/SKILL.md +20 -0
  27. package/plugins/revela/skills/revela-domain/SKILL.md +18 -0
  28. package/plugins/revela/skills/revela-export/SKILL.md +21 -0
  29. package/plugins/revela/skills/revela-init/SKILL.md +36 -0
  30. package/plugins/revela/skills/revela-make-deck/SKILL.md +37 -0
  31. package/plugins/revela/skills/revela-research/SKILL.md +38 -0
  32. package/plugins/revela/skills/revela-review-deck/SKILL.md +33 -0
  33. package/plugins/revela/skills/revela-story/SKILL.md +24 -0
  34. package/tools/decks.ts +10 -78
  35. package/tools/research-save.ts +8 -72
@@ -0,0 +1,504 @@
1
+ import { resolveRevelaRuntime } from "./runtime-resolver"
2
+ import { appendFileSync } from "fs"
3
+
4
+ type JsonRpcRequest = {
5
+ jsonrpc?: string
6
+ id?: string | number | null
7
+ method?: string
8
+ params?: any
9
+ }
10
+
11
+ type RuntimeModule = {
12
+ doctor(input?: any): any
13
+ compileNarrative(input?: any): any
14
+ markdownQa(input?: any): any
15
+ readDeckPlan(input?: any): any
16
+ createDeckFoundation(input: any): any
17
+ runDeckQa(input: any): Promise<any>
18
+ exportPdf(input: any): Promise<any>
19
+ exportPptx(input: any): Promise<any>
20
+ designList(): any
21
+ designRead(input?: any): any
22
+ designActivate(input: any): any
23
+ domainList(): any
24
+ domainRead(input?: any): any
25
+ domainActivate(input: any): any
26
+ storyRead(input?: any): any
27
+ reviewDeckRead(input: any): Promise<any>
28
+ reviewDeckOpen(input: any): Promise<any>
29
+ researchTargets(input?: any): any
30
+ researchSave(input: any): any
31
+ evaluateResearchFindings(input: any): any
32
+ bindResearchFindings(input: any): any
33
+ }
34
+
35
+ type MessageMode = "framed" | "raw"
36
+
37
+ const tools = [
38
+ {
39
+ name: "revela_doctor",
40
+ description: "Inspect Revela workspace availability and basic file-native state.",
41
+ inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
42
+ },
43
+ {
44
+ name: "revela_compile_narrative",
45
+ description: "Compile revela-narrative/ Markdown into canonical NarrativeStateV1 diagnostics.",
46
+ inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
47
+ },
48
+ {
49
+ name: "revela_markdown_qa",
50
+ description: "Run Markdown QA for the Revela narrative vault.",
51
+ inputSchema: objectSchema({
52
+ workspaceRoot: stringProp("Optional workspace root."),
53
+ scope: enumProp(["touched", "affected", "full"], "QA scope."),
54
+ strictness: enumProp(["authoring", "readiness", "render"], "QA strictness."),
55
+ touched: arrayProp("Touched vault files."),
56
+ }),
57
+ },
58
+ {
59
+ name: "revela_read_deck_plan",
60
+ description: "Read the file-native deck-plan/ projection and diagnostics.",
61
+ inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
62
+ },
63
+ {
64
+ name: "revela_create_deck_foundation",
65
+ description: "Create or repair a file-native Revela HTML deck foundation shell.",
66
+ inputSchema: objectSchema({
67
+ workspaceRoot: stringProp("Optional workspace root."),
68
+ outputPath: requiredStringProp("Workspace-relative HTML output path."),
69
+ title: requiredStringProp("HTML title."),
70
+ language: requiredStringProp("HTML language tag."),
71
+ designName: stringProp("Optional design name."),
72
+ mode: enumProp(["create", "repair"], "Create or repair mode."),
73
+ overwrite: booleanProp("Whether create mode may overwrite an existing file."),
74
+ }, ["outputPath", "title", "language"]),
75
+ },
76
+ {
77
+ name: "revela_run_deck_qa",
78
+ description: "Run Revela artifact QA on a generated HTML deck.",
79
+ inputSchema: objectSchema({
80
+ workspaceRoot: stringProp("Optional workspace root."),
81
+ file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
82
+ }, ["file"]),
83
+ },
84
+ {
85
+ name: "revela_export_pdf",
86
+ description: "Run export QA and export a Revela HTML deck to PDF.",
87
+ inputSchema: objectSchema({
88
+ workspaceRoot: stringProp("Optional workspace root."),
89
+ file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
90
+ }, ["file"]),
91
+ },
92
+ {
93
+ name: "revela_export_pptx",
94
+ description: "Run export QA and export a Revela HTML deck to PPTX.",
95
+ inputSchema: objectSchema({
96
+ workspaceRoot: stringProp("Optional workspace root."),
97
+ file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
98
+ }, ["file"]),
99
+ },
100
+ {
101
+ name: "revela_design_list",
102
+ description: "List installed Revela designs and the active design.",
103
+ inputSchema: objectSchema({}),
104
+ },
105
+ {
106
+ name: "revela_design_read",
107
+ description: "Read Revela design instructions for the active or requested design.",
108
+ inputSchema: objectSchema({ name: stringProp("Optional design name.") }),
109
+ },
110
+ {
111
+ name: "revela_design_activate",
112
+ description: "Activate a Revela design for future deck planning and artifact generation.",
113
+ inputSchema: objectSchema({ name: requiredStringProp("Design name to activate.") }, ["name"]),
114
+ },
115
+ {
116
+ name: "revela_domain_list",
117
+ description: "List installed Revela narrative domains and the active domain.",
118
+ inputSchema: objectSchema({}),
119
+ },
120
+ {
121
+ name: "revela_domain_read",
122
+ description: "Read Revela narrative domain guidance for the active or requested domain.",
123
+ inputSchema: objectSchema({ name: stringProp("Optional domain name.") }),
124
+ },
125
+ {
126
+ name: "revela_domain_activate",
127
+ description: "Activate a Revela narrative domain for future narrative authoring guidance.",
128
+ inputSchema: objectSchema({ name: requiredStringProp("Domain name to activate.") }, ["name"]),
129
+ },
130
+ {
131
+ name: "revela_story_read",
132
+ description: "Read a deterministic Revela Story map and optional Markdown view from the canonical narrative vault without mutating files.",
133
+ inputSchema: objectSchema({
134
+ workspaceRoot: stringProp("Optional workspace root."),
135
+ format: enumProp(["map", "markdown"], "Return only the map, or include a formatted Markdown reading view."),
136
+ }),
137
+ },
138
+ {
139
+ name: "revela_review_deck_read",
140
+ description: "Read-only aggregate Review diagnostics for a Revela HTML deck: artifact QA, deck-plan diagnostics, narrative/vault diagnostics, artifact coverage, and available evidence trace.",
141
+ inputSchema: objectSchema({
142
+ workspaceRoot: stringProp("Optional workspace root."),
143
+ file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
144
+ format: enumProp(["json", "markdown"], "Return JSON only, or include a Markdown review summary."),
145
+ }, ["file"]),
146
+ },
147
+ {
148
+ name: "revela_review_deck_open",
149
+ description: "Open a local Codex-backed Review UI for a Revela HTML deck from the current MCP server process.",
150
+ inputSchema: objectSchema({
151
+ workspaceRoot: stringProp("Optional workspace root."),
152
+ file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
153
+ bridge: enumProp(["codex-exec"], "Prompt bridge for browser Insight and Comment interactions."),
154
+ openBrowser: booleanProp("Whether the tool should open the browser itself. Defaults to true when omitted."),
155
+ }, ["file"]),
156
+ },
157
+ {
158
+ name: "revela_research_targets",
159
+ description: "Derive current Revela research targets from canonical narrative state, saved findings, and evidence gaps.",
160
+ inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
161
+ },
162
+ {
163
+ name: "revela_research_save",
164
+ description: "Save research findings under researches/{topic}/{filename}.md and evaluate binding readiness when workspace state exists.",
165
+ inputSchema: objectSchema({
166
+ workspaceRoot: stringProp("Optional workspace root."),
167
+ topic: requiredStringProp("Research topic key."),
168
+ filename: requiredStringProp("Findings filename without extension."),
169
+ content: requiredStringProp("Structured Markdown findings content."),
170
+ sources: arrayProp("Source URLs or workspace files for YAML frontmatter."),
171
+ }, ["topic", "filename", "content"]),
172
+ },
173
+ {
174
+ name: "revela_evaluate_research_findings",
175
+ description: "Evaluate whether a saved researches/**/*.md findings file is safely bindable as canonical evidence.",
176
+ inputSchema: objectSchema({
177
+ workspaceRoot: stringProp("Optional workspace root."),
178
+ findingsFile: requiredStringProp("Workspace-relative researches/**/*.md findings file."),
179
+ }, ["findingsFile"]),
180
+ },
181
+ {
182
+ name: "revela_bind_research_findings",
183
+ description: "Bind a safely evaluated findings file into canonical revela-narrative/evidence/*.md evidence.",
184
+ inputSchema: objectSchema({
185
+ workspaceRoot: stringProp("Optional workspace root."),
186
+ findingsFile: requiredStringProp("Workspace-relative researches/**/*.md findings file."),
187
+ evidenceId: stringProp("Optional canonical evidence node id override."),
188
+ }, ["findingsFile"]),
189
+ },
190
+ ]
191
+
192
+ let runtimePromise: Promise<RuntimeModule> | undefined
193
+ const debugEnabled = process.env.REVELA_MCP_DEBUG === "1"
194
+ const bootLogEnabled = process.env.REVELA_MCP_BOOT_LOG === "1"
195
+ const bootLogPath = "/tmp/revela-mcp-boot.log"
196
+ let activeResponseMode: MessageMode = "framed"
197
+
198
+ async function runtime(): Promise<RuntimeModule> {
199
+ runtimePromise ??= import(runtimeUrl()) as Promise<RuntimeModule>
200
+ return runtimePromise
201
+ }
202
+
203
+ function runtimeUrl(): string {
204
+ const pluginRoot = new URL("..", import.meta.url).pathname
205
+ const resolved = resolveRevelaRuntime({ pluginRoot })
206
+ if (!resolved.ok || !resolved.runtimePath) {
207
+ throw new Error(`Could not resolve Revela runtime. ${resolved.diagnostics.join(" ")}`)
208
+ }
209
+ debug("runtime", { pluginRoot, source: resolved.source, runtimePath: resolved.runtimePath })
210
+ return new URL(`file://${resolved.runtimePath}`).href
211
+ }
212
+
213
+ async function handle(req: JsonRpcRequest): Promise<any | undefined> {
214
+ if (!req.id && String(req.method || "").startsWith("notifications/")) return undefined
215
+
216
+ try {
217
+ debug("request", { id: req.id, method: req.method })
218
+ bootLog("request", { id: req.id, method: req.method })
219
+ if (req.method === "initialize") {
220
+ bootLog("initialize-received", { id: req.id, protocolVersion: req.params?.protocolVersion })
221
+ return result(req.id, {
222
+ protocolVersion: req.params?.protocolVersion || "2024-11-05",
223
+ capabilities: { tools: {} },
224
+ serverInfo: { name: "revela", version: "0.1.0" },
225
+ })
226
+ }
227
+ if (req.method === "tools/list") {
228
+ return result(req.id, { tools })
229
+ }
230
+ if (req.method === "tools/call") {
231
+ const name = req.params?.name
232
+ const args = req.params?.arguments ?? {}
233
+ const value = await callTool(name, args)
234
+ return result(req.id, {
235
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
236
+ })
237
+ }
238
+ return error(req.id, -32601, `Unknown method: ${req.method}`)
239
+ } catch (e) {
240
+ return error(req.id, -32000, e instanceof Error ? e.message : String(e))
241
+ }
242
+ }
243
+
244
+ async function callTool(name: string, args: any): Promise<any> {
245
+ const r = await runtime()
246
+ if (name === "revela_doctor") return r.doctor(args)
247
+ if (name === "revela_compile_narrative") return r.compileNarrative(args)
248
+ if (name === "revela_markdown_qa") return r.markdownQa(args)
249
+ if (name === "revela_read_deck_plan") return r.readDeckPlan(args)
250
+ if (name === "revela_create_deck_foundation") return r.createDeckFoundation(args)
251
+ if (name === "revela_run_deck_qa") return r.runDeckQa(args)
252
+ if (name === "revela_export_pdf") return r.exportPdf(args)
253
+ if (name === "revela_export_pptx") return r.exportPptx(args)
254
+ if (name === "revela_design_list") return r.designList()
255
+ if (name === "revela_design_read") return r.designRead(args)
256
+ if (name === "revela_design_activate") return r.designActivate(args)
257
+ if (name === "revela_domain_list") return r.domainList()
258
+ if (name === "revela_domain_read") return r.domainRead(args)
259
+ if (name === "revela_domain_activate") return r.domainActivate(args)
260
+ if (name === "revela_story_read") return r.storyRead(args)
261
+ if (name === "revela_review_deck_read") return r.reviewDeckRead(args)
262
+ if (name === "revela_review_deck_open") return r.reviewDeckOpen(args)
263
+ if (name === "revela_research_targets") return r.researchTargets(args)
264
+ if (name === "revela_research_save") return r.researchSave(args)
265
+ if (name === "revela_evaluate_research_findings") return r.evaluateResearchFindings(args)
266
+ if (name === "revela_bind_research_findings") return r.bindResearchFindings(args)
267
+ throw new Error(`Unknown tool: ${name}`)
268
+ }
269
+
270
+ function result(id: JsonRpcRequest["id"], value: any): any {
271
+ return { jsonrpc: "2.0", id, result: value }
272
+ }
273
+
274
+ function error(id: JsonRpcRequest["id"], code: number, message: string): any {
275
+ return { jsonrpc: "2.0", id, error: { code, message } }
276
+ }
277
+
278
+ function objectSchema(properties: Record<string, any>, required: string[] = []) {
279
+ return { type: "object", properties, required, additionalProperties: false }
280
+ }
281
+
282
+ function requiredStringProp(description: string) {
283
+ return { type: "string", description }
284
+ }
285
+
286
+ function stringProp(description: string) {
287
+ return { type: "string", description }
288
+ }
289
+
290
+ function booleanProp(description: string) {
291
+ return { type: "boolean", description }
292
+ }
293
+
294
+ function enumProp(values: string[], description: string) {
295
+ return { type: "string", enum: values, description }
296
+ }
297
+
298
+ function arrayProp(description: string) {
299
+ return { type: "array", items: { type: "string" }, description }
300
+ }
301
+
302
+ function writeMessage(message: any, mode: MessageMode = activeResponseMode): void {
303
+ activeResponseMode = mode
304
+ const body = JSON.stringify(message)
305
+ debug("response", {
306
+ id: message?.id,
307
+ mode,
308
+ result: message?.result ? Object.keys(message.result) : undefined,
309
+ error: message?.error?.message,
310
+ })
311
+ bootLog("response-written", {
312
+ id: message?.id,
313
+ mode,
314
+ result: message?.result ? Object.keys(message.result) : undefined,
315
+ error: message?.error?.message,
316
+ bytes: Buffer.byteLength(body, "utf8"),
317
+ })
318
+ if (mode === "raw") {
319
+ process.stdout.write(`${body}\n`)
320
+ return
321
+ }
322
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`)
323
+ }
324
+
325
+ async function main(): Promise<void> {
326
+ debug("startup", { script: import.meta.path })
327
+ bootLog("server-loaded", { script: import.meta.path, cwd: process.cwd() })
328
+ const reader = Bun.stdin.stream().getReader()
329
+ bootLog("stdin-reader-started", {})
330
+ let buffer: Uint8Array<ArrayBufferLike> = new Uint8Array()
331
+
332
+ while (true) {
333
+ const { value, done } = await reader.read()
334
+ if (done) {
335
+ bootLog("stdin-done", { bufferedBytes: buffer.byteLength })
336
+ break
337
+ }
338
+ buffer = concatBytes(buffer, value)
339
+ const parsed = parseMessages(buffer)
340
+ const responseMode = parsed.mode || activeResponseMode
341
+ activeResponseMode = responseMode
342
+ if (parsed.messages.length > 0) {
343
+ debug("parse", { mode: parsed.mode, messages: parsed.messages.length })
344
+ bootLog("messages-parsed", { mode: parsed.mode, messages: parsed.messages.length, remainingBytes: parsed.remaining.byteLength })
345
+ }
346
+ buffer = parsed.remaining
347
+ for (const message of parsed.messages) {
348
+ const response = await handle(message)
349
+ if (response) writeMessage(response, responseMode)
350
+ }
351
+ }
352
+
353
+ const trimmed = decode(buffer).trim()
354
+ if (trimmed) {
355
+ bootLog("line-buffer-parse", { chars: trimmed.length })
356
+ for (const message of parseLineMessages(trimmed)) {
357
+ const response = await handle(message)
358
+ if (response) writeMessage(response, "raw")
359
+ }
360
+ }
361
+ }
362
+
363
+ function parseFramedMessages(input: Uint8Array<ArrayBufferLike>): {
364
+ messages: JsonRpcRequest[]
365
+ remaining: Uint8Array<ArrayBufferLike>
366
+ } {
367
+ const messages: JsonRpcRequest[] = []
368
+ let cursor = 0
369
+ while (cursor < input.byteLength) {
370
+ const headerEnd = indexOfHeaderEnd(input, cursor)
371
+ if (headerEnd === -1) break
372
+ const header = decode(input.slice(cursor, headerEnd))
373
+ const match = /Content-Length:\s*(\d+)/i.exec(header)
374
+ if (!match) break
375
+ const length = Number(match[1])
376
+ const start = headerEnd + 4
377
+ const end = start + length
378
+ if (input.byteLength < end) break
379
+ messages.push(JSON.parse(decode(input.slice(start, end))))
380
+ cursor = end
381
+ }
382
+ return { messages, remaining: input.slice(cursor) }
383
+ }
384
+
385
+ function parseMessages(input: Uint8Array<ArrayBufferLike>): {
386
+ messages: JsonRpcRequest[]
387
+ remaining: Uint8Array<ArrayBufferLike>
388
+ mode?: "framed" | "raw"
389
+ } {
390
+ const framed = parseFramedMessages(input)
391
+ if (framed.messages.length > 0) return { ...framed, mode: "framed" }
392
+
393
+ const remainingText = decode(framed.remaining)
394
+ if (/^\s*Content-Length:/i.test(remainingText) && !remainingText.includes("\r\n\r\n")) {
395
+ return { messages: [], remaining: input }
396
+ }
397
+
398
+ const raw = parseRawJsonMessages(remainingText)
399
+ if (raw.messages.length > 0) return { messages: raw.messages, remaining: encode(raw.remaining), mode: "raw" }
400
+
401
+ return framed
402
+ }
403
+
404
+ function parseLineMessages(input: string): JsonRpcRequest[] {
405
+ const messages: JsonRpcRequest[] = []
406
+ for (const line of input.split(/\n/).map((item) => item.trim()).filter(Boolean)) {
407
+ messages.push(JSON.parse(line))
408
+ }
409
+ return messages
410
+ }
411
+
412
+ function parseRawJsonMessages(input: string): { messages: JsonRpcRequest[]; remaining: string } {
413
+ const messages: JsonRpcRequest[] = []
414
+ let start = -1
415
+ let depth = 0
416
+ let inString = false
417
+ let escaped = false
418
+ let cursor = 0
419
+
420
+ for (let i = 0; i < input.length; i++) {
421
+ const char = input[i]
422
+
423
+ if (start === -1) {
424
+ if (/\s/.test(char)) {
425
+ cursor = i + 1
426
+ continue
427
+ }
428
+ if (char !== "{" && char !== "[") return { messages, remaining: input.slice(cursor) }
429
+ start = i
430
+ depth = 1
431
+ inString = false
432
+ escaped = false
433
+ continue
434
+ }
435
+
436
+ if (inString) {
437
+ if (escaped) {
438
+ escaped = false
439
+ } else if (char === "\\") {
440
+ escaped = true
441
+ } else if (char === "\"") {
442
+ inString = false
443
+ }
444
+ continue
445
+ }
446
+
447
+ if (char === "\"") {
448
+ inString = true
449
+ } else if (char === "{" || char === "[") {
450
+ depth++
451
+ } else if (char === "}" || char === "]") {
452
+ depth--
453
+ if (depth === 0) {
454
+ const end = i + 1
455
+ messages.push(JSON.parse(input.slice(start, end)))
456
+ cursor = end
457
+ start = -1
458
+ }
459
+ }
460
+ }
461
+
462
+ return { messages, remaining: input.slice(start === -1 ? cursor : start) }
463
+ }
464
+
465
+ function concatBytes(a: Uint8Array<ArrayBufferLike>, b: Uint8Array<ArrayBufferLike>): Uint8Array<ArrayBufferLike> {
466
+ const next = new Uint8Array(a.byteLength + b.byteLength)
467
+ next.set(a)
468
+ next.set(b, a.byteLength)
469
+ return next
470
+ }
471
+
472
+ function indexOfHeaderEnd(input: Uint8Array<ArrayBufferLike>, offset: number): number {
473
+ for (let i = offset; i <= input.byteLength - 4; i++) {
474
+ if (input[i] === 13 && input[i + 1] === 10 && input[i + 2] === 13 && input[i + 3] === 10) return i
475
+ }
476
+ return -1
477
+ }
478
+
479
+ function decode(input: Uint8Array<ArrayBufferLike>): string {
480
+ return new TextDecoder().decode(input)
481
+ }
482
+
483
+ function encode(input: string): Uint8Array {
484
+ return new TextEncoder().encode(input)
485
+ }
486
+
487
+ function debug(event: string, data: Record<string, unknown>): void {
488
+ if (!debugEnabled) return
489
+ process.stderr.write(`[revela-mcp] ${event} ${JSON.stringify(data)}\n`)
490
+ }
491
+
492
+ function bootLog(event: string, data: Record<string, unknown>): void {
493
+ if (!bootLogEnabled) return
494
+ try {
495
+ appendFileSync(bootLogPath, `${new Date().toISOString()} ${event} ${JSON.stringify(data)}\n`, "utf8")
496
+ } catch {
497
+ // Diagnostics must never interfere with the MCP stdio protocol.
498
+ }
499
+ }
500
+
501
+ main().catch((e) => {
502
+ bootLog("top-level-error", { error: e instanceof Error ? e.stack || e.message : String(e) })
503
+ writeMessage(error(null, -32000, e instanceof Error ? e.message : String(e)))
504
+ })
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from "fs"
2
+ import { dirname, join, resolve } from "path"
3
+
4
+ export interface ResolveRuntimeOptions {
5
+ pluginRoot: string
6
+ env?: Record<string, string | undefined>
7
+ homeDir?: string
8
+ }
9
+
10
+ export interface ResolveRuntimeResult {
11
+ ok: boolean
12
+ repoRoot?: string
13
+ runtimePath?: string
14
+ source: "env" | "source-checkout" | "codex-marketplace" | "bundled" | "missing"
15
+ diagnostics: string[]
16
+ }
17
+
18
+ export function resolveRevelaRuntime(options: ResolveRuntimeOptions): ResolveRuntimeResult {
19
+ const env = options.env ?? process.env
20
+ const pluginRoot = resolve(options.pluginRoot)
21
+ const diagnostics: string[] = []
22
+
23
+ const explicit = env.REVELA_REPO_ROOT
24
+ if (explicit) {
25
+ const result = runtimeAt(explicit, "env", diagnostics)
26
+ if (result.ok) return result
27
+ diagnostics.push(`REVELA_REPO_ROOT did not contain lib/runtime/index.ts: ${explicit}`)
28
+ }
29
+
30
+ const checkout = findSourceCheckoutRoot(pluginRoot)
31
+ if (checkout) return runtimeAt(checkout, "source-checkout", diagnostics)
32
+ diagnostics.push(`No source checkout root found above plugin root: ${pluginRoot}`)
33
+
34
+ const marketplaceName = marketplaceNameFromPluginRoot(pluginRoot)
35
+ if (marketplaceName) {
36
+ const source = marketplaceSourceFromCodexConfig(marketplaceName, options.homeDir ?? env.HOME)
37
+ if (source) {
38
+ const result = runtimeAt(source, "codex-marketplace", diagnostics)
39
+ if (result.ok) return result
40
+ diagnostics.push(`Marketplace ${marketplaceName} source did not contain lib/runtime/index.ts: ${source}`)
41
+ } else {
42
+ diagnostics.push(`Marketplace ${marketplaceName} was not found in Codex config.`)
43
+ }
44
+ } else {
45
+ diagnostics.push(`Could not infer marketplace name from plugin root: ${pluginRoot}`)
46
+ }
47
+
48
+ const bundled = runtimeAt(pluginRoot, "bundled", diagnostics)
49
+ if (bundled.ok) return bundled
50
+ diagnostics.push(`No bundled runtime found under plugin root: ${pluginRoot}`)
51
+
52
+ return { ok: false, source: "missing", diagnostics }
53
+ }
54
+
55
+ function runtimeAt(root: string, source: ResolveRuntimeResult["source"], diagnostics: string[]): ResolveRuntimeResult {
56
+ const repoRoot = resolve(root)
57
+ const runtimePath = join(repoRoot, "lib", "runtime", "index.ts")
58
+ return existsSync(runtimePath)
59
+ ? { ok: true, repoRoot, runtimePath, source, diagnostics }
60
+ : { ok: false, source: "missing", diagnostics }
61
+ }
62
+
63
+ function findSourceCheckoutRoot(pluginRoot: string): string | undefined {
64
+ let current = resolve(pluginRoot)
65
+ for (let i = 0; i < 6; i++) {
66
+ if (
67
+ existsSync(join(current, "package.json")) &&
68
+ existsSync(join(current, "lib", "runtime", "index.ts")) &&
69
+ existsSync(join(current, "plugins", "revela", ".codex-plugin", "plugin.json"))
70
+ ) {
71
+ return current
72
+ }
73
+ const parent = dirname(current)
74
+ if (parent === current) break
75
+ current = parent
76
+ }
77
+ return undefined
78
+ }
79
+
80
+ function marketplaceNameFromPluginRoot(pluginRoot: string): string | undefined {
81
+ const parts = resolve(pluginRoot).split(/[\\/]+/)
82
+ const cacheIndex = parts.lastIndexOf("cache")
83
+ if (cacheIndex === -1) return undefined
84
+ return parts[cacheIndex + 1] || undefined
85
+ }
86
+
87
+ function marketplaceSourceFromCodexConfig(marketplaceName: string, homeDir: string | undefined): string | undefined {
88
+ if (!homeDir) return undefined
89
+ const configPath = join(homeDir, ".codex", "config.toml")
90
+ if (!existsSync(configPath)) return undefined
91
+ const text = readFileSync(configPath, "utf-8")
92
+ const section = sectionBody(text, `marketplaces.${marketplaceName}`)
93
+ if (!section) return undefined
94
+ const match = section.match(/^\s*source\s*=\s*"([^"]+)"/m)
95
+ return match?.[1]
96
+ }
97
+
98
+ function sectionBody(text: string, sectionName: string): string | undefined {
99
+ const lines = text.split(/\r?\n/)
100
+ const header = `[${sectionName}]`
101
+ const start = lines.findIndex((line) => line.trim() === header)
102
+ if (start === -1) return undefined
103
+ const body: string[] = []
104
+ for (const line of lines.slice(start + 1)) {
105
+ if (/^\s*\[/.test(line)) break
106
+ body.push(line)
107
+ }
108
+ return body.join("\n")
109
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: revela-design
3
+ description: Use Revela design guidance in Codex for deck planning and artifact generation.
4
+ ---
5
+
6
+ # Revela Design
7
+
8
+ Use this skill when the user asks about Revela designs or when generating deck HTML.
9
+
10
+ ## Workflow
11
+
12
+ 1. Call `revela_design_list` to inspect installed designs.
13
+ 2. Call `revela_design_read` for the active or requested design.
14
+ 3. When the user asks to switch designs for future work, call `revela_design_activate` with the requested design name, then read the active design again.
15
+ 4. For one-off deck generation with a requested design, read that design by name and pass `designName` to `revela_create_deck_foundation` without changing active design unless the user asked to switch.
16
+ 5. Use the current simplified built-in design grammar: `box`, `text-panel`, `media`, `echart-panel`, `data-table`, `steps`, `roadmap-horizontal`, `roadmap-vertical`, `hero`, `stat-card`, `quote`, `toc`, `page-number`, and `brand-watermark`.
17
+ 6. Fetch chart/design guidance before creating ECharts or complex layouts.
18
+ 7. Do not invent unsupported component names.
19
+
20
+ Design changes are visual/artifact-level unless they change claim meaning, evidence boundaries, decision, or recommendation.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: revela-domain
3
+ description: Use or switch Revela narrative domain guidance in Codex for init, research, and story work.
4
+ ---
5
+
6
+ # Revela Domain
7
+
8
+ Use this skill when the user asks about Revela domains, wants domain-specific narrative guidance, or asks to switch the active domain.
9
+
10
+ ## Workflow
11
+
12
+ 1. Call `revela_domain_list` to inspect installed domains and the active domain.
13
+ 2. Call `revela_domain_read` for the active or requested domain.
14
+ 3. When the user asks to switch domains for future narrative work, call `revela_domain_activate` with the requested domain name, then read the active domain again.
15
+ 4. Use domain guidance for audience, decision, claim framing, objections, risks, and research-gap interpretation.
16
+ 5. Do not treat domain guidance as evidence, source material, or proof for factual claims.
17
+
18
+ Domain changes are narrative-framing preferences. They do not rewrite existing claims, evidence boundaries, artifacts, or deck plans unless the user asks for those updates.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: revela-export
3
+ description: Export Revela deck artifacts from Codex to PDF or PPTX after artifact QA.
4
+ ---
5
+
6
+ # Revela Export
7
+
8
+ Use this skill when the user asks to export a Revela deck.
9
+
10
+ ## Workflow
11
+
12
+ 1. Resolve the target HTML deck path.
13
+ 2. Call `revela_run_deck_qa` before export.
14
+ 3. If QA hard errors exist, repair the HTML before exporting.
15
+ 4. For PDF, call `revela_export_pdf`.
16
+ 5. For PPTX, call `revela_export_pptx`.
17
+ 6. Report output path and any export diagnostics.
18
+
19
+ `revela_run_deck_qa`, `revela_export_pdf`, and `revela_export_pptx` may launch a browser. In sandboxed Codex sessions, request user-approved command escalation when the browser cannot start inside the default sandbox.
20
+
21
+ Do not treat narrative gaps as export blockers unless they affect technical artifact validity or data safety.