sapiens-mcp 1.46.5 → 1.48.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/dist/registry.js +16 -7
- package/dist/resume/coverLetter.js +146 -0
- package/dist/resume/template.js +1046 -0
- package/dist/skills.js +42 -0
- package/dist/tools/character.js +29 -0
- package/dist/tools/gallery.js +5 -0
- package/dist/tools/resume.js +154 -0
- package/package.json +3 -2
package/dist/registry.js
CHANGED
|
@@ -33,6 +33,7 @@ import { trilhas, trilhasSchema } from "./tools/trilhas.js";
|
|
|
33
33
|
import { semana, semanaSchema } from "./tools/semana.js";
|
|
34
34
|
import { shareDrop, shareDropSchema } from "./tools/shareDrop.js";
|
|
35
35
|
import { skill, skillSchema } from "./tools/skill.js";
|
|
36
|
+
import { resume, resumeSchema } from "./tools/resume.js";
|
|
36
37
|
import { describeConvexError } from "./convexClient.js";
|
|
37
38
|
import { skillMenuLine } from "./skills.js";
|
|
38
39
|
/**
|
|
@@ -69,7 +70,7 @@ export const TOOLS = {
|
|
|
69
70
|
handler: repertorio,
|
|
70
71
|
},
|
|
71
72
|
sapiens_gallery: {
|
|
72
|
-
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N imagens, com prompt/model/url + isPublic), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen — devolve publicPageUrl), unpublish (volta a privada), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), refs (acopla peças da casa como REFERÊNCIA numa peça que já existe, de upload ou de ingest; ADMIN, e só na própria peça). Use list/get pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation) ou pra mostrar pro user o que ele já tem; publish quando o user quer divulgar a imagem dele. IMPORTANTE sobre upload: a peça entra PRIVADA e continua privada — o servidor recusa publicar e recusa compartilhar na comunidade, porque ela é do usuário e a responsabilidade é dele. O caminho pra ela virar coisa pública é usar como referência numa geração daqui e publicar o resultado. O refs escreve só proveniência (de que ficha/folha a peça nasceu): não promove nada, e um upload continua sem publicar depois de ganhar referência.",
|
|
73
|
+
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N imagens, com prompt/model/url + isPublic), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen — devolve publicPageUrl), unpublish (volta a privada), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), ingest (peça que a CASA dirigiu e um motor externo só renderizou: entra NATIVA na timeline, publica e vai pra comunidade, com custo 0 em Sinapses e o custo real guardado em externalCost; carrega o prompt inteiro, as referências da casa (referenceImageIds) e o personagem (characterId), e é idempotente por sourceUrl, então re-ingerir corrige a ficha em vez de duplicar; ADMIN), refs (acopla peças da casa como REFERÊNCIA numa peça que já existe, de upload ou de ingest; ADMIN, e só na própria peça). Use list/get pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation) ou pra mostrar pro user o que ele já tem; publish quando o user quer divulgar a imagem dele. IMPORTANTE sobre upload: a peça entra PRIVADA e continua privada — o servidor recusa publicar e recusa compartilhar na comunidade, porque ela é do usuário e a responsabilidade é dele. O caminho pra ela virar coisa pública é usar como referência numa geração daqui e publicar o resultado. O refs escreve só proveniência (de que ficha/folha a peça nasceu): não promove nada, e um upload continua sem publicar depois de ganhar referência.",
|
|
73
74
|
schema: gallerySchema,
|
|
74
75
|
handler: gallery,
|
|
75
76
|
},
|
|
@@ -144,7 +145,7 @@ export const TOOLS = {
|
|
|
144
145
|
handler: brand,
|
|
145
146
|
},
|
|
146
147
|
sapiens_character: {
|
|
147
|
-
description: "Personagens (character sheets) do Sapiens — a tabela `influencers`: personagem reutilizável com imagens (pra character-lock em geração) + alma (systemPrompt), tudo amarrado à conta do dono do token (sem admin). Sub-actions: list_public (catálogo global de personagens públicos do Explorar; cada um traz mainImageUrl/imageUrls usáveis direto como referenceImageUrls em sapiens_image; sem custo, sem login), get (detalhe de 1 por characterId — público+ativo qualquer um vê, draft/privado só o dono; systemPrompt só volta pro dono), list_mine (os personagens do próprio user, inclui drafts/privados), create (cria rascunho na conta: name + gender + opcional title/systemPrompt), add_image (adiciona imagem ao próprio personagem via imageUrl público OU sourceImageId da galeria; 1ª vira principal), set_card (edita alma/título/nome do próprio), activate (publica, sai de draft, exige ≥1 imagem), set_visibility (isPublic true=Explorar+slug / false=privado). GESTÃO de imagem (por url, pegue as urls atuais em action=get campo imageUrls): remove_image (tira uma), set_main_image (define a principal), reorder_images (nova ordem via orderedUrls, posição 0=principal), e delete (apaga o personagem, permanente). FICHA (generate_sheet, COBRA 450 Sinapses): desenha a página-pôster do personagem no traço das imagens que ele já tem (exige pelo menos uma), em duas orientações (arg `orientation`): 'portrait' (default) é a página de processo, pose, expressões, trocas de roupa e adereços soltos na folha; 'landscape' é a prancha larga, com a volta completa à esquerda, a figura grande no meio, poses à direita, estudos de silhueta, expressão e detalhe embaixo e um painel CHARACTER ID na ponta. A personalidade sai da alma (systemPrompt) e é ela que escolhe roupa e objeto, então personagem com alma escrita rende ficha melhor. Cada geração é um estilo NOVO e elas acumulam no personagem (campo sheetUrls em action=get), nenhuma apaga a anterior, e a ficha já entra na galeria dele como referência das próximas gerações. É geração síncrona: se voltar Timeout, cheque action=get antes de repetir, senão cobra duas vezes. Fluxo de criação: create → add_image (1+) → set_card (opcional) → activate → set_visibility isPublic=true. Pra usar um personagem público como referência numa geração, pegue mainImageUrl em list_public/get e passe em sapiens_image referenceImageUrls.",
|
|
148
|
+
description: "Personagens (character sheets) do Sapiens — a tabela `influencers`: personagem reutilizável com imagens (pra character-lock em geração) + alma (systemPrompt), tudo amarrado à conta do dono do token (sem admin). Sub-actions: list_public (catálogo global de personagens públicos do Explorar; cada um traz mainImageUrl/imageUrls usáveis direto como referenceImageUrls em sapiens_image; sem custo, sem login), get (detalhe de 1 por characterId — público+ativo qualquer um vê, draft/privado só o dono; systemPrompt só volta pro dono), list_mine (os personagens do próprio user, inclui drafts/privados), create (cria rascunho na conta: name + gender + opcional title/systemPrompt), add_image (adiciona imagem ao próprio personagem via imageUrl público OU sourceImageId da galeria; 1ª vira principal), set_card (edita alma/título/nome do próprio), activate (publica, sai de draft, exige ≥1 imagem), set_visibility (isPublic true=Explorar+slug / false=privado). GESTÃO de imagem (por url, pegue as urls atuais em action=get campo imageUrls): remove_image (tira uma), set_main_image (define a principal), reorder_images (nova ordem via orderedUrls, posição 0=principal), e delete (apaga o personagem, permanente). FICHA (generate_sheet, COBRA 450 Sinapses): desenha a página-pôster do personagem no traço das imagens que ele já tem (exige pelo menos uma), em duas orientações (arg `orientation`): 'portrait' (default) é a página de processo, pose, expressões, trocas de roupa e adereços soltos na folha; 'landscape' é a prancha larga, com a volta completa à esquerda, a figura grande no meio, poses à direita, estudos de silhueta, expressão e detalhe embaixo e um painel CHARACTER ID na ponta. A personalidade sai da alma (systemPrompt) e é ela que escolhe roupa e objeto, então personagem com alma escrita rende ficha melhor. Cada geração é um estilo NOVO e elas acumulam no personagem (campo sheetUrls em action=get), nenhuma apaga a anterior, e a ficha já entra na galeria dele como referência das próximas gerações. É geração síncrona: se voltar Timeout, cheque action=get antes de repetir, senão cobra duas vezes. FIGURINHAS (generate_stickers, COBRA 550 Sinapses): desenha 5 figurinhas do personagem DE UMA VEZ. Uma folha só, em 2K, com as cinco figuras separadas sobre um fundo verde chroma, recortada por código em peças 512x512 transparentes abaixo de 100KB (o teto do WhatsApp). É por isso que sai o preço de UMA imagem em 2K e não de cinco: quem paga é a folha. As peças entram no pack do personagem (um por personagem, os lotes acumulam) e já ficam no picker de expressão do dono, no chat e no Fórum. Arg `moods`: até 5 humores do vocabulário comum por slug (kkkkk, amei, isso, hmm, chega, que, aff, bora, socorro, seinao, valeu, ainao, seila, ideia, calma, contatudo, perfeito, euavisei, naovourir, zzz, somaisum, sextou, merecido, quedia, tudobem); faltando, a casa completa com os mais usados. Arg `hint`: direcionamento curto do autor (roupa, adereço, clima), até 140 caracteres. Sem legenda queimada na imagem, de propósito: modelo erra acento em português, então o rótulo fica na row e serve de busca no picker. Exige pelo menos uma imagem no personagem (é dela que sai a cara) e é geração síncrona: se voltar Timeout, cheque sapiens_gallery antes de repetir. Quando a resposta vem com ok=false, a folha foi gerada e paga mas o corte falhou; ela está na galeria e o recorte de novo é de graça, pela web. Publicar o pack na vitrine e o carimbo da casa (que é o que põe no picker de todo mundo) são gestos da web, não desta tool. Fluxo de criação: create → add_image (1+) → set_card (opcional) → activate → set_visibility isPublic=true. Pra usar um personagem público como referência numa geração, pegue mainImageUrl em list_public/get e passe em sapiens_image referenceImageUrls.",
|
|
148
149
|
schema: characterSchema,
|
|
149
150
|
handler: character,
|
|
150
151
|
},
|
|
@@ -208,6 +209,11 @@ export const TOOLS = {
|
|
|
208
209
|
schema: shareDropSchema,
|
|
209
210
|
handler: shareDrop,
|
|
210
211
|
},
|
|
212
|
+
sapiens_resume: {
|
|
213
|
+
description: "Currículo PRO — o gerador de currículo da casa, DE GRAÇA e sem login (mundo fechado: não fala com o backend, não cobra Sinapse). VOCÊ conduz a entrevista (ou lê o texto de LinkedIn/currículo antigo que a pessoa colar), monta o JSON no contrato e recebe o HTML pronto pra imprimir em PDF (A4). Sub-actions: 'schema' (o contrato de dados + os 4 modelos; leia ANTES do primeiro render), 'render' (data -> HTML completo). Modelos (arg theme): 'pro' (default, duas colunas com sidebar verde-jade: foto/monograma, contato, chips de competências, barras de idioma, timeline de experiência), 'executivo' (duas colunas grafite + bronze, nome em serifa), 'moderno' (coluna única, roxo da casa), 'minimal' (coluna única preto e branco); accentColor troca a cor de destaque. FOTO DE PERFIL: opcional e NUNCA persistida em banco. No MCP instalado (stdio), photoPath lê a foto local e embute como data URI direto no HTML, sem sair da máquina; no remoto, data.photo aceita URL https. No stdio, outPath salva o .html direto no disco e a resposta volta enxuta (sem trafegar o HTML inteiro). Regra de conteúdo: melhore a redação, jamais invente fato; lang ('pt'|'en') casa os títulos de seção com o idioma do conteúdo. É o MESMO template do Lab /experimentos/resume-builder.",
|
|
214
|
+
schema: resumeSchema,
|
|
215
|
+
handler: resume,
|
|
216
|
+
},
|
|
211
217
|
};
|
|
212
218
|
// Guia de uso server-level: o protocolo MCP devolve isto no handshake (initialize)
|
|
213
219
|
// e TODO cliente (Helen no Hermes, Gemini CLI, Cursor, Antigravity) injeta no modelo.
|
|
@@ -221,7 +227,7 @@ O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por es
|
|
|
221
227
|
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
222
228
|
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
223
229
|
Slugs: ${skillMenuLine()}
|
|
224
|
-
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas). Perdido no começo: primeiros-passos.
|
|
230
|
+
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas), montar currículo (curriculo). Perdido no começo: primeiros-passos.
|
|
225
231
|
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
226
232
|
|
|
227
233
|
REGRA DE OURO:
|
|
@@ -249,10 +255,12 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
249
255
|
"sapiens_reference",
|
|
250
256
|
"sapiens_skill",
|
|
251
257
|
]);
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
258
|
+
// As tools que NÃO falam com o backend: skills são conteúdo estático do pacote
|
|
259
|
+
// e o currículo é render puro (data -> HTML). openWorldHint=false é honesto e
|
|
260
|
+
// ajuda o client a auto-aprovar sem nem pensar (nenhuma ação na conta de
|
|
261
|
+
// ninguém). sapiens_resume fica FORA do READ_ONLY_TOOLS: outPath escreve um
|
|
262
|
+
// .html no disco da pessoa (no stdio), então read-only seria mentira.
|
|
263
|
+
const CLOSED_WORLD_TOOLS = new Set(["sapiens_skill", "sapiens_resume"]);
|
|
256
264
|
const TOOL_TITLES = {
|
|
257
265
|
sapiens_pipeline: "Pipeline de Conteúdo",
|
|
258
266
|
sapiens_image: "Gerar Imagem",
|
|
@@ -286,6 +294,7 @@ const TOOL_TITLES = {
|
|
|
286
294
|
sapiens_semana: "Minha Semana",
|
|
287
295
|
sapiens_instagram: "Auto-DM Instagram",
|
|
288
296
|
sapiens_share: "Share Drop",
|
|
297
|
+
sapiens_resume: "Currículo PRO",
|
|
289
298
|
};
|
|
290
299
|
// Tools ADMIN-ONLY de ponta a ponta (o Convex recusa user comum em toda action):
|
|
291
300
|
// escondidas do tools/list quando o servidor SABE que o tier é user. São as
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carta de apresentação — o template irmão do Currículo PRO.
|
|
3
|
+
*
|
|
4
|
+
* Mesma casa do currículo (fonte única no pacote, código puro data -> HTML,
|
|
5
|
+
* nada persiste em servidor): recebe o TEXTO estruturado que a IA (ou a
|
|
6
|
+
* pessoa) escreveu + os dados de identidade do currículo, e devolve uma folha
|
|
7
|
+
* A4 de uma página na mesma linha visual do tema escolhido. O PDF sai igual ao
|
|
8
|
+
* do currículo: abrir no navegador e salvar como PDF.
|
|
9
|
+
*/
|
|
10
|
+
import { RESUME_THEMES } from "./template.js";
|
|
11
|
+
function escapeHtml(text) {
|
|
12
|
+
if (text === null || text === undefined)
|
|
13
|
+
return "";
|
|
14
|
+
return String(text)
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """);
|
|
19
|
+
}
|
|
20
|
+
function contactLine(resume) {
|
|
21
|
+
return [resume.location, resume.phone, resume.email, resume.linkedin?.label]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map(escapeHtml)
|
|
24
|
+
.join(" · ");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Gera o documento HTML completo da carta. `resume` é o mesmo objeto do
|
|
28
|
+
* currículo (só identidade/contato são lidos); `theme` casa a carta com a
|
|
29
|
+
* folha escolhida.
|
|
30
|
+
*/
|
|
31
|
+
export function generateCoverLetterHTML(cover, resume, options = {}) {
|
|
32
|
+
const theme = options.theme && RESUME_THEMES[options.theme] ? options.theme : "pro";
|
|
33
|
+
const spec = RESUME_THEMES[theme];
|
|
34
|
+
const lang = cover.language === "en" ? "en" : "pt";
|
|
35
|
+
const title = lang === "en" ? "Cover Letter" : "Carta de Apresentação";
|
|
36
|
+
const paragraphs = (cover.paragraphs || [])
|
|
37
|
+
.map((p) => `<p class="body-p">${escapeHtml(p)}</p>`)
|
|
38
|
+
.join("\n");
|
|
39
|
+
return `<!DOCTYPE html>
|
|
40
|
+
<html lang="${lang === "en" ? "en" : "pt-BR"}">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="UTF-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
|
+
<title>${escapeHtml(resume.name)} · ${title}</title>
|
|
45
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
46
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
47
|
+
<link href="https://fonts.googleapis.com/css2?${spec.fontsQuery}&display=swap" rel="stylesheet">
|
|
48
|
+
<style>
|
|
49
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
50
|
+
html { font-size: 15.5px; -webkit-font-smoothing: antialiased; }
|
|
51
|
+
body {
|
|
52
|
+
font-family: ${spec.bodyFamily};
|
|
53
|
+
color: #1B1E23;
|
|
54
|
+
background: ${spec.pageBg};
|
|
55
|
+
line-height: 1.6;
|
|
56
|
+
padding: 2.2rem 1rem;
|
|
57
|
+
}
|
|
58
|
+
.sheet {
|
|
59
|
+
max-width: 760px;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
background: #fff;
|
|
62
|
+
border-radius: 12px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
box-shadow: 0 1px 2px rgba(20,22,27,.05), 0 14px 44px rgba(20,22,27,.09);
|
|
65
|
+
padding: 3rem 3.2rem 2.6rem;
|
|
66
|
+
position: relative;
|
|
67
|
+
}
|
|
68
|
+
.sheet::before {
|
|
69
|
+
content: '';
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 0; left: 0; right: 0;
|
|
72
|
+
height: 3px;
|
|
73
|
+
background: ${spec.accent};
|
|
74
|
+
}
|
|
75
|
+
.head { border-bottom: 1px solid #E5E6E8; padding-bottom: 1.2rem; margin-bottom: 1.6rem; }
|
|
76
|
+
.head-name {
|
|
77
|
+
font-family: ${spec.displayFamily};
|
|
78
|
+
font-size: 1.7rem;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
letter-spacing: -0.02em;
|
|
81
|
+
margin-bottom: .15rem;
|
|
82
|
+
}
|
|
83
|
+
.head-title {
|
|
84
|
+
font-size: .78rem;
|
|
85
|
+
font-weight: 700;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: .14em;
|
|
88
|
+
color: ${spec.accent};
|
|
89
|
+
margin-bottom: .55rem;
|
|
90
|
+
}
|
|
91
|
+
.head-contact { font-size: .76rem; color: #82868E; }
|
|
92
|
+
.doc-label {
|
|
93
|
+
font-size: .68rem;
|
|
94
|
+
font-weight: 700;
|
|
95
|
+
text-transform: uppercase;
|
|
96
|
+
letter-spacing: .16em;
|
|
97
|
+
color: #82868E;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: .65rem;
|
|
101
|
+
margin-bottom: 1.2rem;
|
|
102
|
+
}
|
|
103
|
+
.doc-label::before { content: ''; width: 22px; height: 3px; border-radius: 2px; background: ${spec.accent}; }
|
|
104
|
+
.date-line { font-size: .8rem; color: #82868E; margin-bottom: 1.4rem; }
|
|
105
|
+
.subject { font-size: .92rem; font-weight: 700; margin-bottom: 1.2rem; }
|
|
106
|
+
.greeting { font-size: .9rem; margin-bottom: 1rem; }
|
|
107
|
+
.body-p { font-size: .88rem; color: #4A4E57; line-height: 1.8; margin-bottom: 1rem; text-align: justify; }
|
|
108
|
+
.closing { margin-top: 1.6rem; font-size: .9rem; }
|
|
109
|
+
.signature {
|
|
110
|
+
margin-top: .4rem;
|
|
111
|
+
font-family: ${spec.displayFamily};
|
|
112
|
+
font-size: 1.05rem;
|
|
113
|
+
font-weight: 700;
|
|
114
|
+
}
|
|
115
|
+
@media (max-width: 640px) {
|
|
116
|
+
body { padding: .5rem; }
|
|
117
|
+
.sheet { border-radius: 0; padding: 1.8rem 1.4rem; }
|
|
118
|
+
}
|
|
119
|
+
@media print {
|
|
120
|
+
@page { margin: 0.7in; size: A4; }
|
|
121
|
+
html { font-size: 13px; }
|
|
122
|
+
body { padding: 0; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
123
|
+
.sheet { max-width: 100%; border-radius: 0; box-shadow: none; padding: 0; }
|
|
124
|
+
.sheet::before { display: none; }
|
|
125
|
+
.body-p { break-inside: avoid; }
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
<div class="sheet">
|
|
131
|
+
<header class="head">
|
|
132
|
+
<h1 class="head-name">${escapeHtml(resume.name)}</h1>
|
|
133
|
+
${resume.title ? `<p class="head-title">${escapeHtml(resume.title)}</p>` : ""}
|
|
134
|
+
<p class="head-contact">${contactLine(resume)}</p>
|
|
135
|
+
</header>
|
|
136
|
+
<p class="doc-label">${title}</p>
|
|
137
|
+
${cover.dateLine ? `<p class="date-line">${escapeHtml(cover.dateLine)}</p>` : ""}
|
|
138
|
+
${cover.subject ? `<p class="subject">${escapeHtml(cover.subject)}</p>` : ""}
|
|
139
|
+
<p class="greeting">${escapeHtml(cover.greeting)}</p>
|
|
140
|
+
${paragraphs}
|
|
141
|
+
<p class="closing">${escapeHtml(cover.closing)}</p>
|
|
142
|
+
<p class="signature">${escapeHtml(resume.name)}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|