@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.
- package/README.md +26 -46
- package/README.zh-CN.md +26 -46
- package/bin/revela.ts +98 -0
- package/lib/edit/prompt.ts +6 -2
- package/lib/edit/server.ts +2 -2
- package/lib/inspect/prompt.ts +5 -1
- package/lib/refine/comment-requests.ts +77 -0
- package/lib/refine/open.ts +5 -2
- package/lib/refine/prompt-bridge.ts +219 -0
- package/lib/refine/qa-suppression.ts +41 -0
- package/lib/refine/server.ts +122 -34
- package/lib/runtime/index.ts +225 -0
- package/lib/runtime/research.ts +175 -0
- package/lib/runtime/review.ts +270 -0
- package/lib/runtime/story.ts +53 -0
- package/package.json +6 -1
- package/plugin.ts +4 -2
- package/plugins/revela/.codex-plugin/plugin.json +37 -0
- package/plugins/revela/.mcp.json +11 -0
- package/plugins/revela/assets/README.md +2 -0
- package/plugins/revela/hooks/hooks.json +28 -0
- package/plugins/revela/hooks/revela_guard.ts +10 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +18 -0
- package/plugins/revela/mcp/revela-server.ts +504 -0
- package/plugins/revela/mcp/runtime-resolver.ts +109 -0
- package/plugins/revela/skills/revela-design/SKILL.md +20 -0
- package/plugins/revela/skills/revela-domain/SKILL.md +18 -0
- package/plugins/revela/skills/revela-export/SKILL.md +21 -0
- package/plugins/revela/skills/revela-init/SKILL.md +36 -0
- package/plugins/revela/skills/revela-make-deck/SKILL.md +37 -0
- package/plugins/revela/skills/revela-research/SKILL.md +38 -0
- package/plugins/revela/skills/revela-review-deck/SKILL.md +33 -0
- package/plugins/revela/skills/revela-story/SKILL.md +24 -0
- package/tools/decks.ts +10 -78
- 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.
|