ppt-template-reuse 0.4.0-rc.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
@@ -0,0 +1,81 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export async function exists(candidate) {
6
+ return Boolean(await fs.stat(candidate).catch(() => undefined));
7
+ }
8
+
9
+ export async function readJson(candidate) {
10
+ return JSON.parse(await fs.readFile(candidate, "utf8"));
11
+ }
12
+
13
+ export async function writeJsonAtomic(candidate, value) {
14
+ await fs.mkdir(path.dirname(candidate), { recursive: true });
15
+ const temporary = `${candidate}.${process.pid}.${Date.now()}.tmp`;
16
+ await fs.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
17
+ await fs.rename(temporary, candidate);
18
+ }
19
+
20
+ export async function sha256File(candidate) {
21
+ return crypto
22
+ .createHash("sha256")
23
+ .update(await fs.readFile(candidate))
24
+ .digest("hex");
25
+ }
26
+
27
+ export function fingerprint(value) {
28
+ const stable = (input) => {
29
+ if (Array.isArray(input)) return input.map(stable);
30
+ if (input && typeof input === "object") {
31
+ return Object.fromEntries(
32
+ Object.keys(input)
33
+ .sort()
34
+ .map((key) => [key, stable(input[key])]),
35
+ );
36
+ }
37
+ return input;
38
+ };
39
+ return crypto
40
+ .createHash("sha256")
41
+ .update(JSON.stringify(stable(value)))
42
+ .digest("hex");
43
+ }
44
+
45
+ export function normalizePath(candidate) {
46
+ return path.resolve(String(candidate));
47
+ }
48
+
49
+ export function isWithin(candidate, root) {
50
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
51
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
52
+ }
53
+
54
+ export function assertAllowedPath(candidate, allowedRoots) {
55
+ const resolved = path.resolve(candidate);
56
+ if (!allowedRoots.some((root) => isWithin(resolved, root))) {
57
+ const error = new Error(`Path is outside allowed roots: ${resolved}`);
58
+ error.code = "PATH_NOT_ALLOWED";
59
+ throw error;
60
+ }
61
+ return resolved;
62
+ }
63
+
64
+ export function parseCliArgs(argv) {
65
+ const result = { _: [] };
66
+ for (let index = 0; index < argv.length; index += 1) {
67
+ const token = argv[index];
68
+ if (!token.startsWith("--")) {
69
+ result._.push(token);
70
+ continue;
71
+ }
72
+ const key = token.slice(2);
73
+ const next = argv[index + 1];
74
+ if (!next || next.startsWith("--")) result[key] = true;
75
+ else {
76
+ result[key] = next;
77
+ index += 1;
78
+ }
79
+ }
80
+ return result;
81
+ }
@@ -0,0 +1,382 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { fingerprint, readJson, sha256File, writeJsonAtomic } from "./io.mjs";
5
+ import { resolveLibraryDir } from "./paths.mjs";
6
+ import { inspectTemplate } from "./template-inspector.mjs";
7
+ import { buildTemplateIntelligence } from "./template-intelligence.mjs";
8
+
9
+ function normalized(value) {
10
+ return String(value || "").trim().toLowerCase();
11
+ }
12
+
13
+ function exists(candidate) {
14
+ return fs.stat(candidate).then(
15
+ () => true,
16
+ () => false,
17
+ );
18
+ }
19
+
20
+ function effectiveRecipe(recipe, correction = {}) {
21
+ const next = structuredClone(recipe);
22
+ for (const key of [
23
+ "layoutFamily",
24
+ "layoutVariant",
25
+ "narrativeRole",
26
+ "confidence",
27
+ "autoUseStatus",
28
+ "approvedForAutoUse",
29
+ "excludedFromAutoUse",
30
+ "exclusionReason",
31
+ ]) {
32
+ if (correction[key] !== undefined) next[key] = correction[key];
33
+ }
34
+ if (correction.slots) {
35
+ next.slots = (next.slots || []).map((slot) => ({
36
+ ...slot,
37
+ ...(correction.slots[slot.slotId] || {}),
38
+ }));
39
+ }
40
+ return next;
41
+ }
42
+
43
+ function familyForUniversalProfile(value) {
44
+ const family = normalized(value);
45
+ if (["text-content", "statement", "quote"].includes(family)) return "analysis";
46
+ if (
47
+ ["two-column", "three-column", "four-column", "multi-column", "matrix"].includes(
48
+ family,
49
+ )
50
+ ) {
51
+ return "comparison";
52
+ }
53
+ if (family === "image-led") return "image";
54
+ if (family === "references") return "reference";
55
+ return family || "analysis";
56
+ }
57
+
58
+ function scaleBox(box, sourceCanvas, targetCanvas) {
59
+ if (!Array.isArray(box) || box.length !== 4) return undefined;
60
+ const sourceWidth = Number(sourceCanvas?.width || targetCanvas.width);
61
+ const sourceHeight = Number(sourceCanvas?.height || targetCanvas.height);
62
+ return {
63
+ x: (Number(box[0]) * targetCanvas.width) / sourceWidth,
64
+ y: (Number(box[1]) * targetCanvas.height) / sourceHeight,
65
+ width: (Number(box[2]) * targetCanvas.width) / sourceWidth,
66
+ height: (Number(box[3]) * targetCanvas.height) / sourceHeight,
67
+ };
68
+ }
69
+
70
+ function capabilityInventory(slides) {
71
+ const result = {};
72
+ const add = (name, slide) => {
73
+ if (!name) return;
74
+ result[name] ||= { count: 0, safeCount: 0, sourceSlides: [] };
75
+ result[name].count += 1;
76
+ if (slide.safetyStatus === "automatic") result[name].safeCount += 1;
77
+ result[name].sourceSlides.push(slide.sourceSlide);
78
+ };
79
+ for (const slide of slides) {
80
+ const capabilities = new Set([
81
+ slide.layoutFamily,
82
+ ...(slide.communicationFunctions || []),
83
+ ...(slide.narrativeRoles || []),
84
+ ]);
85
+ if (slide.layoutFamily === "cover") capabilities.add("cover");
86
+ if (slide.layoutFamily === "closing") capabilities.add("closing");
87
+ if (slide.layoutFamily === "agenda") capabilities.add("roadmap");
88
+ if (slide.layoutFamily === "divider") capabilities.add("section");
89
+ if (slide.layoutFamily === "timeline") {
90
+ capabilities.add("timeline");
91
+ capabilities.add("chronology");
92
+ capabilities.add("process");
93
+ capabilities.add("evidence");
94
+ }
95
+ if (
96
+ ["analysis", "comparison", "table", "process", "reference"].includes(
97
+ slide.layoutFamily,
98
+ )
99
+ ) {
100
+ for (const capability of [
101
+ "analysis",
102
+ "issue",
103
+ "counterargument",
104
+ "conclusion",
105
+ "summary",
106
+ "evidence",
107
+ "background",
108
+ "problem",
109
+ "solution",
110
+ "comparison",
111
+ ]) {
112
+ capabilities.add(capability);
113
+ }
114
+ }
115
+ if (["chart", "table"].includes(slide.layoutFamily)) capabilities.add("data");
116
+ if (slide.layoutFamily === "image") {
117
+ capabilities.add("imagery");
118
+ capabilities.add("image-evidence");
119
+ capabilities.add("evidence");
120
+ }
121
+ for (const capability of capabilities) add(normalized(capability), slide);
122
+ }
123
+ for (const capability of Object.values(result)) {
124
+ capability.sourceSlides = [...new Set(capability.sourceSlides)].sort(
125
+ (left, right) => left - right,
126
+ );
127
+ }
128
+ return result;
129
+ }
130
+
131
+ function reviewSummary(slides) {
132
+ return {
133
+ automaticSlides: slides
134
+ .filter((slide) => slide.safetyStatus === "automatic")
135
+ .map((slide) => slide.sourceSlide),
136
+ reviewSlides: slides
137
+ .filter((slide) => slide.safetyStatus === "review")
138
+ .map((slide) => slide.sourceSlide),
139
+ blockedSlides: slides
140
+ .filter((slide) => slide.safetyStatus === "blocked")
141
+ .map((slide) => slide.sourceSlide),
142
+ };
143
+ }
144
+
145
+ export async function importLegacyCapsule({
146
+ capsuleDir,
147
+ templatePath,
148
+ approvedSourceSlides = [],
149
+ openingSourceSlide,
150
+ closingSourceSlide,
151
+ home,
152
+ persist = true,
153
+ }) {
154
+ const resolvedCapsule = path.resolve(capsuleDir);
155
+ const resolvedTemplate = path.resolve(templatePath);
156
+ const [manifest, recipeDoc, inspection] = await Promise.all([
157
+ readJson(path.join(resolvedCapsule, "manifest.json")),
158
+ readJson(path.join(resolvedCapsule, "slide-recipes.json")),
159
+ inspectTemplate(resolvedTemplate),
160
+ ]);
161
+ const correctionsPath = path.join(resolvedCapsule, "user-corrections.json");
162
+ const grammarPath = path.join(resolvedCapsule, "deck-grammar.json");
163
+ const corrections = (await exists(correctionsPath))
164
+ ? await readJson(correctionsPath)
165
+ : { slides: {} };
166
+ const deckGrammar = (await exists(grammarPath))
167
+ ? await readJson(grammarPath)
168
+ : {};
169
+ const actualSha256 = await sha256File(resolvedTemplate);
170
+ const legacySha256 =
171
+ manifest.source?.sha256 || manifest.sourceSha256 || manifest.sha256;
172
+ if (!legacySha256 || legacySha256 !== actualSha256) {
173
+ const error = new Error(
174
+ "Legacy capsule does not match the selected template SHA256",
175
+ );
176
+ error.code = "LEGACY_CAPSULE_SOURCE_MISMATCH";
177
+ throw error;
178
+ }
179
+
180
+ const intelligence = buildTemplateIntelligence({
181
+ manifest,
182
+ recipeDoc,
183
+ deckGrammar,
184
+ corrections,
185
+ });
186
+ const approved = new Set(
187
+ approvedSourceSlides.map(Number).filter(Number.isFinite),
188
+ );
189
+ for (const [slideNumber, correction] of Object.entries(
190
+ corrections.slides || {},
191
+ )) {
192
+ if (correction.approvedForAutoUse === true) approved.add(Number(slideNumber));
193
+ }
194
+ if (openingSourceSlide) approved.add(Number(openingSourceSlide));
195
+ if (closingSourceSlide) approved.add(Number(closingSourceSlide));
196
+
197
+ const recipeBySlide = new Map(
198
+ (recipeDoc.recipes || []).map((recipe) => [
199
+ Number(recipe.sourceSlide),
200
+ effectiveRecipe(
201
+ recipe,
202
+ corrections.slides?.[String(recipe.sourceSlide)] || {},
203
+ ),
204
+ ]),
205
+ );
206
+ const intelligenceBySlide = new Map(
207
+ intelligence.slides.map((slide) => [Number(slide.sourceSlide), slide]),
208
+ );
209
+ const sourceCanvas =
210
+ manifest.styleProfile?.slideSize || manifest.slideSize || inspection.canvas;
211
+ const slides = inspection.slides.map((baseSlide) => {
212
+ const sourceSlide = Number(baseSlide.sourceSlide);
213
+ const recipe = recipeBySlide.get(sourceSlide);
214
+ const semantic = intelligenceBySlide.get(sourceSlide);
215
+ if (!recipe || !semantic) {
216
+ return {
217
+ ...baseSlide,
218
+ safetyStatus: "blocked",
219
+ confidence: 0,
220
+ legacyImport: { available: false },
221
+ };
222
+ }
223
+ const correction = corrections.slides?.[String(sourceSlide)] || {};
224
+ const hardBlocked =
225
+ recipe.excludedFromAutoUse === true ||
226
+ (recipe.hardBlockReasons || []).length > 0;
227
+ const safetyStatus =
228
+ !hardBlocked && approved.has(sourceSlide) ? "automatic" : "blocked";
229
+ const slotSchema = (recipe.slots || []).map((slot) => ({
230
+ slotId: slot.slotId,
231
+ sourceShapeId: slot.sourceShapeId,
232
+ sourceElementName: slot.name,
233
+ type: slot.type || "text",
234
+ semanticType: slot.semanticType || slot.type || "text",
235
+ purpose: slot.purpose || "body",
236
+ required:
237
+ slot.required === true ||
238
+ !["pageNumber", "date", "footer"].includes(slot.purpose),
239
+ maxChars: slot.maxChars,
240
+ maxTextUnits: slot.maxTextUnits,
241
+ maxLines: slot.maxLines,
242
+ rows: slot.rows,
243
+ columns: slot.columns,
244
+ sourceText: slot.originalText || "",
245
+ bbox: scaleBox(slot.bbox, sourceCanvas, inspection.canvas),
246
+ operation: slot.operation || slot.action || "rewrite",
247
+ }));
248
+ const correctedFamily = familyForUniversalProfile(
249
+ correction.layoutFamily || recipe.layoutFamily,
250
+ );
251
+ const correctedRole = normalized(
252
+ correction.narrativeRole || recipe.narrativeRole,
253
+ );
254
+ return {
255
+ ...baseSlide,
256
+ sourceSlide,
257
+ sourcePart: baseSlide.sourcePart,
258
+ layoutFamily: correctedFamily,
259
+ layoutVariant:
260
+ correction.layoutVariant ||
261
+ recipe.layoutVariant ||
262
+ `${correctedFamily}-${slotSchema.length}`,
263
+ communicationFunctions: semantic.communicationFunctions,
264
+ narrativeRoles: [
265
+ ...new Set([
266
+ correctedRole,
267
+ ...(semantic.narrativeRoles || []),
268
+ ...(sourceSlide === Number(openingSourceSlide) ? ["opening"] : []),
269
+ ...(sourceSlide === Number(closingSourceSlide) ? ["closing"] : []),
270
+ ]),
271
+ ].filter(Boolean),
272
+ slotSchema,
273
+ assetRequirements: (recipe.visualRequirements || []).map((item) => ({
274
+ requirementId: item.requirementId,
275
+ type: item.type,
276
+ required: item.required !== false,
277
+ status: item.status,
278
+ slotId: item.slotId,
279
+ })),
280
+ contentRecipe: semantic.contentRecipe,
281
+ safetyStatus,
282
+ confidence: safetyStatus === "automatic" ? 1 : Number(semantic.confidence || 0),
283
+ editability: "native-ooxml",
284
+ protectedElements: baseSlide.protectedElements,
285
+ legacyImport: {
286
+ available: true,
287
+ capsulePath: resolvedCapsule,
288
+ recipeFingerprint: recipe.recipeFingerprint,
289
+ previewSha256: recipe.previewSha256,
290
+ reviewBinding: correction.reviewBinding || null,
291
+ explicitlyApproved: approved.has(sourceSlide),
292
+ },
293
+ };
294
+ });
295
+
296
+ const openingCandidates = slides.filter(
297
+ (slide) =>
298
+ slide.safetyStatus === "automatic" &&
299
+ (slide.layoutFamily === "cover" ||
300
+ slide.narrativeRoles.includes("opening")),
301
+ );
302
+ const closingCandidates = slides.filter(
303
+ (slide) =>
304
+ slide.safetyStatus === "automatic" &&
305
+ (slide.layoutFamily === "closing" ||
306
+ slide.narrativeRoles.includes("closing")),
307
+ );
308
+ if (openingCandidates.length !== 1 || closingCandidates.length !== 1) {
309
+ const error = new Error(
310
+ `Strict capsule import requires exactly one approved opening and closing; opening=${openingCandidates
311
+ .map((slide) => slide.sourceSlide)
312
+ .join(",")}, closing=${closingCandidates
313
+ .map((slide) => slide.sourceSlide)
314
+ .join(",")}`,
315
+ );
316
+ error.code = "LEGACY_CAPSULE_VARIANT_UNRESOLVED";
317
+ throw error;
318
+ }
319
+
320
+ const profile = {
321
+ ...inspection,
322
+ schemaVersion: 4,
323
+ engine: "ppt-template-reuse-legacy-capsule-import",
324
+ source: {
325
+ ...inspection.source,
326
+ path: resolvedTemplate,
327
+ sha256: actualSha256,
328
+ },
329
+ templateKind: intelligence.templateKind,
330
+ templateKindConfidence: intelligence.templateKindConfidence,
331
+ purposeProfile: intelligence.purposeProfile,
332
+ densityRange: intelligence.densityRange,
333
+ visualBalance: intelligence.visualBalance,
334
+ slides,
335
+ capabilities: capabilityInventory(slides),
336
+ variantSelections: {},
337
+ reviewSummary: reviewSummary(slides),
338
+ actionRequired: null,
339
+ legacyImport: {
340
+ capsulePath: resolvedCapsule,
341
+ sourceSha256: actualSha256,
342
+ importedAt: new Date().toISOString(),
343
+ strictDenyByDefault: true,
344
+ approvedSourceSlides: [...approved].sort((left, right) => left - right),
345
+ openingSourceSlide: openingCandidates[0].sourceSlide,
346
+ closingSourceSlide: closingCandidates[0].sourceSlide,
347
+ },
348
+ };
349
+ profile.profileFingerprint = fingerprint({
350
+ source: profile.source,
351
+ slides: profile.slides,
352
+ purposeProfile: profile.purposeProfile,
353
+ legacyImport: {
354
+ sourceSha256: profile.legacyImport.sourceSha256,
355
+ strictDenyByDefault: true,
356
+ approvedSourceSlides: profile.legacyImport.approvedSourceSlides,
357
+ openingSourceSlide: profile.legacyImport.openingSourceSlide,
358
+ closingSourceSlide: profile.legacyImport.closingSourceSlide,
359
+ },
360
+ });
361
+
362
+ let persisted;
363
+ if (persist) {
364
+ const capsuleDirectory = path.join(resolveLibraryDir(home), actualSha256);
365
+ await fs.mkdir(capsuleDirectory, { recursive: true });
366
+ const profilePath = path.join(
367
+ capsuleDirectory,
368
+ "template-profile.lock.json",
369
+ );
370
+ await writeJsonAtomic(profilePath, profile);
371
+ await writeJsonAtomic(path.join(capsuleDirectory, "manifest.json"), {
372
+ schemaVersion: 2,
373
+ engine: "ppt-template-reuse-legacy-capsule-import",
374
+ sourceSha256: actualSha256,
375
+ profileFingerprint: profile.profileFingerprint,
376
+ importedFrom: resolvedCapsule,
377
+ updatedAt: new Date().toISOString(),
378
+ });
379
+ persisted = { capsuleDirectory, profilePath };
380
+ }
381
+ return { profile, persisted };
382
+ }