my-patina 0.6.0 → 0.8.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/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Command } from "commander";
|
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
10
|
-
import { existsSync as
|
|
10
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
11
11
|
import yaml3 from "js-yaml";
|
|
12
12
|
|
|
13
13
|
// src/detect.ts
|
|
@@ -23,7 +23,7 @@ function loadProfile(root) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/scaffold.ts
|
|
26
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
26
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
27
27
|
import { join as join5, dirname as dirname3 } from "path";
|
|
28
28
|
import yaml2 from "js-yaml";
|
|
29
29
|
|
|
@@ -64,7 +64,8 @@ var LI_COMMANDS = [
|
|
|
64
64
|
];
|
|
65
65
|
var LI_MANAGED_PATHS = [
|
|
66
66
|
...LI_COMMANDS.map((c) => `.claude/commands/${c}`),
|
|
67
|
-
".claude/modules/linkedin/manifest.md"
|
|
67
|
+
".claude/modules/linkedin/manifest.md",
|
|
68
|
+
".claude/modules/linkedin/CLAUDE.md"
|
|
68
69
|
];
|
|
69
70
|
var CONTENT_FILE_NAMES = [
|
|
70
71
|
"INSTRUCTIONS.md",
|
|
@@ -91,6 +92,10 @@ var linkedinModule = {
|
|
|
91
92
|
".claude/modules/linkedin/manifest.md",
|
|
92
93
|
render(tpl("modules/linkedin/manifest.md"), vars)
|
|
93
94
|
]);
|
|
95
|
+
files.push([
|
|
96
|
+
".claude/modules/linkedin/CLAUDE.md",
|
|
97
|
+
render(tpl("modules/linkedin/CLAUDE.md"), vars)
|
|
98
|
+
]);
|
|
94
99
|
return files;
|
|
95
100
|
},
|
|
96
101
|
contentFiles(vars, contentDir) {
|
|
@@ -109,12 +114,46 @@ var linkedinModule = {
|
|
|
109
114
|
const updated = { ...profile };
|
|
110
115
|
delete updated.linkedin;
|
|
111
116
|
return updated;
|
|
117
|
+
},
|
|
118
|
+
readmeBlock(vars) {
|
|
119
|
+
return [
|
|
120
|
+
"## LinkedIn module",
|
|
121
|
+
"",
|
|
122
|
+
"Drafts and refines your LinkedIn profile from your graph.",
|
|
123
|
+
"",
|
|
124
|
+
"### Folder additions",
|
|
125
|
+
"",
|
|
126
|
+
"```",
|
|
127
|
+
`${vars.CONTENT_DIR}/linkedin/`,
|
|
128
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
129
|
+
" LinkedIn Current State.md \u2014 your current live profile copy",
|
|
130
|
+
" LinkedIn About.md \u2014 draft for the About section",
|
|
131
|
+
" LinkedIn Headline.md \u2014 draft for your headline",
|
|
132
|
+
" LinkedIn Experience.md \u2014 draft for your experience entries",
|
|
133
|
+
" LinkedIn Skills.md \u2014 draft for your skills section",
|
|
134
|
+
" LinkedIn Featured.md \u2014 draft for featured content",
|
|
135
|
+
" LinkedIn Activity.md \u2014 draft for activity/posts section",
|
|
136
|
+
"```",
|
|
137
|
+
"",
|
|
138
|
+
"### Commands",
|
|
139
|
+
"",
|
|
140
|
+
"| Command | What it does |",
|
|
141
|
+
"|---------|-------------|",
|
|
142
|
+
"| `/li-all` | Run all LinkedIn section drafts in sequence |",
|
|
143
|
+
"| `/li-about` | Draft or refine your LinkedIn About section |",
|
|
144
|
+
"| `/li-headline` | Draft or refine your LinkedIn headline |",
|
|
145
|
+
"| `/li-experience` | Draft or refine your LinkedIn experience entries |",
|
|
146
|
+
"| `/li-skills` | Draft or refine your LinkedIn skills section |",
|
|
147
|
+
"| `/li-featured` | Draft or refine your LinkedIn featured content |",
|
|
148
|
+
"| `/li-activity` | Draft or refine your LinkedIn activity section |"
|
|
149
|
+
].join("\n");
|
|
112
150
|
}
|
|
113
151
|
};
|
|
114
152
|
|
|
115
153
|
// src/modules/resume/index.ts
|
|
116
154
|
var RESUME_MANAGED_PATHS = [
|
|
117
155
|
".claude/commands/resume-refresh.md",
|
|
156
|
+
".claude/modules/resume/CLAUDE.md",
|
|
118
157
|
".claude/modules/resume/manifest.md"
|
|
119
158
|
];
|
|
120
159
|
var CONTENT_FILE_NAMES2 = [
|
|
@@ -129,10 +168,10 @@ var resumeModule = {
|
|
|
129
168
|
managedPaths: RESUME_MANAGED_PATHS,
|
|
130
169
|
contentFileNames: CONTENT_FILE_NAMES2,
|
|
131
170
|
managedFiles(vars) {
|
|
132
|
-
const [commandPath, manifestPath] = RESUME_MANAGED_PATHS;
|
|
133
171
|
return [
|
|
134
|
-
[
|
|
135
|
-
[
|
|
172
|
+
[".claude/commands/resume-refresh.md", render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
|
|
173
|
+
[".claude/modules/resume/CLAUDE.md", render(tpl("modules/resume/CLAUDE.md"), vars)],
|
|
174
|
+
[".claude/modules/resume/manifest.md", render(tpl("modules/resume/manifest.md"), vars)]
|
|
136
175
|
];
|
|
137
176
|
},
|
|
138
177
|
contentFiles(vars, contentDir) {
|
|
@@ -140,6 +179,28 @@ var resumeModule = {
|
|
|
140
179
|
`${contentDir}/resume/${file}`,
|
|
141
180
|
render(tpl(`modules/resume/graph/${file}`), vars)
|
|
142
181
|
]);
|
|
182
|
+
},
|
|
183
|
+
readmeBlock(vars) {
|
|
184
|
+
return [
|
|
185
|
+
"## Resume module",
|
|
186
|
+
"",
|
|
187
|
+
"Keeps your resume current by synthesising it from your graph.",
|
|
188
|
+
"",
|
|
189
|
+
"### Folder additions",
|
|
190
|
+
"",
|
|
191
|
+
"```",
|
|
192
|
+
`${vars.CONTENT_DIR}/resume/`,
|
|
193
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
194
|
+
" Resume Working Draft.md \u2014 the resume you are actively editing",
|
|
195
|
+
" Resume Last Submitted.md \u2014 the version you last sent to an employer",
|
|
196
|
+
"```",
|
|
197
|
+
"",
|
|
198
|
+
"### Commands",
|
|
199
|
+
"",
|
|
200
|
+
"| Command | What it does |",
|
|
201
|
+
"|---------|-------------|",
|
|
202
|
+
"| `/resume-refresh` | Refresh your resume working draft from your graph |"
|
|
203
|
+
].join("\n");
|
|
143
204
|
}
|
|
144
205
|
// Resume has no module-specific profile fields — no onAdd/onRemove needed.
|
|
145
206
|
};
|
|
@@ -258,6 +319,28 @@ function mergeSections(existing, newSections, storedChecksums, relativePath, ove
|
|
|
258
319
|
}
|
|
259
320
|
return { content: result, sections: outcomes };
|
|
260
321
|
}
|
|
322
|
+
function removeSection(id, content) {
|
|
323
|
+
const sections = parseSections(content);
|
|
324
|
+
const section = sections.find((s) => s.id === id);
|
|
325
|
+
if (!section) return content;
|
|
326
|
+
const { start, end } = section;
|
|
327
|
+
let before = content.slice(0, start);
|
|
328
|
+
before = before.replace(/(\r?\n)+$/, "");
|
|
329
|
+
let after = content.slice(end);
|
|
330
|
+
after = after.replace(/^(\r?\n)+/, "");
|
|
331
|
+
let result;
|
|
332
|
+
if (before === "" && after === "") {
|
|
333
|
+
result = "";
|
|
334
|
+
} else if (before === "") {
|
|
335
|
+
result = after;
|
|
336
|
+
} else if (after === "") {
|
|
337
|
+
result = before;
|
|
338
|
+
} else {
|
|
339
|
+
result = before + "\n\n" + after;
|
|
340
|
+
}
|
|
341
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
261
344
|
function inspectSections(relativePath, existing, storedChecksums) {
|
|
262
345
|
const sections = parseSections(existing);
|
|
263
346
|
const editedIds = [];
|
|
@@ -417,6 +500,11 @@ function validateManifestFrontmatter(moduleName, content) {
|
|
|
417
500
|
}
|
|
418
501
|
function profileToVars(profile, liProfileUrl) {
|
|
419
502
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
503
|
+
const modulesSection = (profile.modules ?? []).length ? profile.modules.map((id) => {
|
|
504
|
+
const def = getModule(id);
|
|
505
|
+
const label2 = def?.label ?? id;
|
|
506
|
+
return `- [${label2} module context](.claude/modules/${id}/CLAUDE.md)`;
|
|
507
|
+
}).join("\n") : "_No modules installed._";
|
|
420
508
|
return {
|
|
421
509
|
PATINA_NAME: profile.patina_name,
|
|
422
510
|
USER_NAME: profile.name,
|
|
@@ -427,11 +515,17 @@ function profileToVars(profile, liProfileUrl) {
|
|
|
427
515
|
CONTENT_DIR: profile.content_dir,
|
|
428
516
|
EDITOR: profile.editor,
|
|
429
517
|
LI_PROFILE_URL: liProfileUrl ?? profile.linkedin?.profile_url ?? "",
|
|
430
|
-
TODAY: today
|
|
518
|
+
TODAY: today,
|
|
519
|
+
STALENESS_THRESHOLD: (() => {
|
|
520
|
+
const d = Number(profile.staleness_threshold_days ?? 30);
|
|
521
|
+
return String(Number.isFinite(d) && d > 0 ? d : 30);
|
|
522
|
+
})(),
|
|
523
|
+
MODULES_SECTION: modulesSection
|
|
431
524
|
};
|
|
432
525
|
}
|
|
433
526
|
function baseManagedFiles(vars, editor, targetDir) {
|
|
434
527
|
const files = [
|
|
528
|
+
["README.md", render(tpl("README.md"), vars)],
|
|
435
529
|
["CLAUDE.md", render(tpl("CLAUDE.md"), vars)],
|
|
436
530
|
[".claude/settings.json", tpl(".claude/settings.json")],
|
|
437
531
|
[".claude/commands/add.md", render(tpl(".claude/commands/add.md"), vars)],
|
|
@@ -484,22 +578,24 @@ async function scaffold(opts) {
|
|
|
484
578
|
created: today,
|
|
485
579
|
...modules.includes("linkedin") && liProfileUrl ? { linkedin: { profile_url: liProfileUrl } } : {}
|
|
486
580
|
};
|
|
487
|
-
const vars =
|
|
488
|
-
PATINA_NAME: patinaName,
|
|
489
|
-
USER_NAME: userName,
|
|
490
|
-
USER_TITLE: title ?? "",
|
|
491
|
-
ROLE_DESCRIPTION: roleDescription,
|
|
492
|
-
COMPANY_NAME: work.company_name,
|
|
493
|
-
COMPANY_DESCRIPTION: work.company_description ?? "",
|
|
494
|
-
CONTENT_DIR: contentDir,
|
|
495
|
-
EDITOR: editor,
|
|
496
|
-
LI_PROFILE_URL: liProfileUrl,
|
|
497
|
-
TODAY: today
|
|
498
|
-
};
|
|
581
|
+
const vars = profileToVars(tempProfile, liProfileUrl);
|
|
499
582
|
mkdirSync2(targetDir, { recursive: true });
|
|
500
583
|
const checksums = {};
|
|
584
|
+
const baseFiles = baseManagedFiles(vars, editor, targetDir);
|
|
585
|
+
const readmePath = join5(targetDir, "README.md");
|
|
586
|
+
const filteredBaseFiles = baseFiles.filter(([rel]) => {
|
|
587
|
+
if (rel === "README.md") {
|
|
588
|
+
if (existsSync5(readmePath)) {
|
|
589
|
+
const existing = readFileSync6(readmePath, "utf8");
|
|
590
|
+
if (!hasFences(existing)) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
});
|
|
501
597
|
const managedFiles = [
|
|
502
|
-
...
|
|
598
|
+
...filteredBaseFiles,
|
|
503
599
|
...modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
504
600
|
];
|
|
505
601
|
for (const module of modules) {
|
|
@@ -513,6 +609,17 @@ async function scaffold(opts) {
|
|
|
513
609
|
checksums[`${relativePath}:${s.id}`] = s.newChecksum;
|
|
514
610
|
}
|
|
515
611
|
}
|
|
612
|
+
for (const module of modules) {
|
|
613
|
+
const def = getModule(module);
|
|
614
|
+
if (def?.readmeBlock) {
|
|
615
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
616
|
+
const result = writeManagedFile(targetDir, "README.md", block, checksums);
|
|
617
|
+
checksums["README.md"] = result.checksum;
|
|
618
|
+
for (const s of result.sections ?? []) {
|
|
619
|
+
checksums[`README.md:${s.id}`] = s.newChecksum;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
516
623
|
const baseDirs = ["notes", "skills", "posts"];
|
|
517
624
|
for (const dir of baseDirs) {
|
|
518
625
|
touch(targetDir, `${contentDir}/${dir}/.gitkeep`);
|
|
@@ -533,16 +640,16 @@ ${STATE_FILENAME}
|
|
|
533
640
|
}
|
|
534
641
|
|
|
535
642
|
// src/validate.ts
|
|
536
|
-
import { existsSync as
|
|
643
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
537
644
|
import { join as join6, relative, sep, basename } from "path";
|
|
538
645
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
539
646
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
540
647
|
var POSTS = CONTENT_SUBDIRS[2];
|
|
541
648
|
function findPatinaRoot(cwd) {
|
|
542
|
-
return
|
|
649
|
+
return existsSync6(join6(cwd, "profile.yaml")) ? cwd : null;
|
|
543
650
|
}
|
|
544
651
|
function listMarkdownFiles(dir) {
|
|
545
|
-
if (!
|
|
652
|
+
if (!existsSync6(dir)) return [];
|
|
546
653
|
const results = [];
|
|
547
654
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
548
655
|
const fullPath = join6(dir, entry.name);
|
|
@@ -605,7 +712,7 @@ function checkSkillNotes(root, profile) {
|
|
|
605
712
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
606
713
|
const issues = [];
|
|
607
714
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
608
|
-
const content =
|
|
715
|
+
const content = readFileSync7(skillFile, "utf8");
|
|
609
716
|
const links = extractWikiLinks(content);
|
|
610
717
|
for (const { target, line } of links) {
|
|
611
718
|
if (!noteSlugs.has(target)) {
|
|
@@ -632,7 +739,7 @@ function checkWikiLinks(root, profile) {
|
|
|
632
739
|
...listMarkdownFiles(postsDir)
|
|
633
740
|
];
|
|
634
741
|
for (const file of filesToScan) {
|
|
635
|
-
const content =
|
|
742
|
+
const content = readFileSync7(file, "utf8");
|
|
636
743
|
const links = extractWikiLinks(content);
|
|
637
744
|
for (const { target, line } of links) {
|
|
638
745
|
if (!noteSlugs.has(target)) {
|
|
@@ -651,8 +758,8 @@ function checkExclusions(root, profile) {
|
|
|
651
758
|
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
652
759
|
const notesDir = join6(contentDir, NOTES);
|
|
653
760
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
654
|
-
if (!
|
|
655
|
-
const content =
|
|
761
|
+
if (!existsSync6(exclusionsPath)) return [];
|
|
762
|
+
const content = readFileSync7(exclusionsPath, "utf8");
|
|
656
763
|
const items = parseExclusions(content);
|
|
657
764
|
if (items.length === 0) return [];
|
|
658
765
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -664,7 +771,7 @@ function checkExclusions(root, profile) {
|
|
|
664
771
|
const issues = [];
|
|
665
772
|
const seen = /* @__PURE__ */ new Set();
|
|
666
773
|
for (const file of filesToScan) {
|
|
667
|
-
const fileContent =
|
|
774
|
+
const fileContent = readFileSync7(file, "utf8");
|
|
668
775
|
const lines = fileContent.split("\n");
|
|
669
776
|
for (let i = 0; i < lines.length; i++) {
|
|
670
777
|
const lineText = lines[i];
|
|
@@ -900,8 +1007,8 @@ function writeProfile(cwd, profile) {
|
|
|
900
1007
|
}
|
|
901
1008
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
902
1009
|
const fullPath = join7(targetDir, rel);
|
|
903
|
-
if (!
|
|
904
|
-
const fileContent =
|
|
1010
|
+
if (!existsSync7(fullPath)) return "deleted";
|
|
1011
|
+
const fileContent = readFileSync8(fullPath, "utf8");
|
|
905
1012
|
if (hasFences(fileContent)) {
|
|
906
1013
|
const editedIds = inspectSections(rel, fileContent, stored);
|
|
907
1014
|
if (editedIds.length > 0) {
|
|
@@ -992,6 +1099,32 @@ function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
|
992
1099
|
updated.push(rel);
|
|
993
1100
|
}
|
|
994
1101
|
}
|
|
1102
|
+
for (const module of updatedProfile.modules) {
|
|
1103
|
+
const def = getModule(module);
|
|
1104
|
+
if (def?.readmeBlock) {
|
|
1105
|
+
const readmePath = join7(cwd, "README.md");
|
|
1106
|
+
const readmeExists = existsSync7(readmePath);
|
|
1107
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1108
|
+
if (!readmeExists || readmeHasFences) {
|
|
1109
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1110
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums, overwrite);
|
|
1111
|
+
newChecksums["README.md"] = result.checksum;
|
|
1112
|
+
for (const s of result.sections ?? []) {
|
|
1113
|
+
const sKey = `README.md:${s.id}`;
|
|
1114
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1115
|
+
else {
|
|
1116
|
+
newChecksums[sKey] = stored[sKey] ?? "";
|
|
1117
|
+
keptSections.push(sKey);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (result.outcome === "skipped") {
|
|
1121
|
+
skipped.push(`README.md:${module}`);
|
|
1122
|
+
} else if (result.outcome !== "updated" || result.sections?.some((s) => s.id === module && s.outcome !== "unchanged")) {
|
|
1123
|
+
updated.push(`README.md:${module}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
995
1128
|
for (const [rel, hash] of Object.entries(stored)) {
|
|
996
1129
|
if (!(rel in newChecksums)) {
|
|
997
1130
|
newChecksums[rel] = hash;
|
|
@@ -1086,8 +1219,8 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
1086
1219
|
for (const [rel, content] of previewFiles) {
|
|
1087
1220
|
if (hasFences(content)) {
|
|
1088
1221
|
const fullPath = join7(cwd, rel);
|
|
1089
|
-
if (
|
|
1090
|
-
const existingContent =
|
|
1222
|
+
if (existsSync7(fullPath)) {
|
|
1223
|
+
const existingContent = readFileSync8(fullPath, "utf8");
|
|
1091
1224
|
const editedIds = inspectSections(rel, existingContent, storedChecksums);
|
|
1092
1225
|
for (const sectionId of editedIds) {
|
|
1093
1226
|
const confirmed = await p.confirm({
|
|
@@ -1149,12 +1282,28 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1149
1282
|
}
|
|
1150
1283
|
for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
|
|
1151
1284
|
const fullPath = join7(cwd, relativePath);
|
|
1152
|
-
if (!
|
|
1285
|
+
if (!existsSync7(fullPath)) {
|
|
1153
1286
|
mkdirSync3(dirname4(fullPath), { recursive: true });
|
|
1154
1287
|
writeFileSync4(fullPath, content, "utf8");
|
|
1155
1288
|
added.push(relativePath);
|
|
1156
1289
|
}
|
|
1157
1290
|
}
|
|
1291
|
+
if (def?.readmeBlock) {
|
|
1292
|
+
const readmePath = join7(cwd, "README.md");
|
|
1293
|
+
const readmeExists = existsSync7(readmePath);
|
|
1294
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1295
|
+
if (!readmeExists || readmeHasFences) {
|
|
1296
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1297
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums);
|
|
1298
|
+
newChecksums["README.md"] = result.checksum;
|
|
1299
|
+
for (const s of result.sections ?? []) {
|
|
1300
|
+
newChecksums[`README.md:${s.id}`] = s.newChecksum;
|
|
1301
|
+
}
|
|
1302
|
+
if (result.outcome !== "skipped") added.push(`README.md:${module}`);
|
|
1303
|
+
} else {
|
|
1304
|
+
kept.push("README.md");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1158
1307
|
if (!updatedProfile.modules.includes(module)) {
|
|
1159
1308
|
updatedProfile.modules = [...updatedProfile.modules, module];
|
|
1160
1309
|
}
|
|
@@ -1175,11 +1324,40 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1175
1324
|
kept.push(rel);
|
|
1176
1325
|
}
|
|
1177
1326
|
}
|
|
1327
|
+
const readmePath = join7(cwd, "README.md");
|
|
1328
|
+
if (existsSync7(readmePath)) {
|
|
1329
|
+
const before = readFileSync8(readmePath, "utf8");
|
|
1330
|
+
const editedIds = inspectSections("README.md", before, stored);
|
|
1331
|
+
if (!editedIds.includes(module)) {
|
|
1332
|
+
const after = removeSection(module, before);
|
|
1333
|
+
if (after !== before) {
|
|
1334
|
+
writeFileSync4(readmePath, after, "utf8");
|
|
1335
|
+
newChecksums["README.md"] = hashContent(after);
|
|
1336
|
+
delete newChecksums[`README.md:${module}`];
|
|
1337
|
+
deleted.push(`README.md:${module}`);
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
keptSections.push(`README.md:${module}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1178
1343
|
updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
|
|
1179
1344
|
if (def?.onRemove) {
|
|
1180
1345
|
updatedProfile = def.onRemove(updatedProfile);
|
|
1181
1346
|
}
|
|
1182
1347
|
}
|
|
1348
|
+
const finalVars = profileToVars(updatedProfile);
|
|
1349
|
+
for (const [rel, content] of baseManagedFiles(finalVars, updatedProfile.editor, cwd)) {
|
|
1350
|
+
const result = writeManagedFile(cwd, rel, content, newChecksums);
|
|
1351
|
+
newChecksums[rel] = result.checksum;
|
|
1352
|
+
for (const s of result.sections ?? []) {
|
|
1353
|
+
const sKey = `${rel}:${s.id}`;
|
|
1354
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1355
|
+
else {
|
|
1356
|
+
newChecksums[sKey] = newChecksums[sKey] ?? "";
|
|
1357
|
+
keptSections.push(sKey);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1183
1361
|
writeState(cwd, { checksums: newChecksums });
|
|
1184
1362
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
1185
1363
|
writeProfile(cwd, finalProfile);
|
|
@@ -1208,6 +1386,16 @@ async function runUpdateModules(cwd, profile) {
|
|
|
1208
1386
|
p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
|
|
1209
1387
|
return;
|
|
1210
1388
|
}
|
|
1389
|
+
const changeLines = [];
|
|
1390
|
+
for (const m of toAdd) {
|
|
1391
|
+
const def = getModule(m);
|
|
1392
|
+
changeLines.push(`Adding ${def?.label ?? m}: appends a section to README.md, adds a link to CLAUDE.md`);
|
|
1393
|
+
}
|
|
1394
|
+
for (const m of toRemove) {
|
|
1395
|
+
const def = getModule(m);
|
|
1396
|
+
changeLines.push(`Removing ${def?.label ?? m}: removes its section from README.md and its link from CLAUDE.md`);
|
|
1397
|
+
}
|
|
1398
|
+
p.note(changeLines.join("\n"), label("Planned changes"));
|
|
1211
1399
|
let liProfileUrl;
|
|
1212
1400
|
if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
|
|
1213
1401
|
const url = await p.text({
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -44,9 +44,34 @@ The graph is the source of truth. Nothing gets added to generated content unless
|
|
|
44
44
|
- Never delete skill files automatically. Surface them to the user and wait for confirmation.
|
|
45
45
|
- `{{CONTENT_DIR}}/notes/exclusions.md` overrides everything. If something is listed there, it must not appear in any generated output.
|
|
46
46
|
|
|
47
|
+
## On session start
|
|
48
|
+
|
|
49
|
+
Before anything else, scan the graph for stale content and surface a brief report.
|
|
50
|
+
|
|
51
|
+
Read the file modification times for all three areas — skip `.gitkeep`, `README.md`, and `exclusions.md` in every directory:
|
|
52
|
+
- `{{CONTENT_DIR}}/notes/`
|
|
53
|
+
- `{{CONTENT_DIR}}/skills/`
|
|
54
|
+
- `{{CONTENT_DIR}}/posts/`
|
|
55
|
+
|
|
56
|
+
List items not modified in the last **{{STALENESS_THRESHOLD}} days**, grouped by area:
|
|
57
|
+
|
|
58
|
+
- **Notes** — stale note slugs
|
|
59
|
+
- **Skills** — stale skill slugs
|
|
60
|
+
- **Posts** — stale draft slugs
|
|
61
|
+
|
|
62
|
+
Skip any area with nothing stale. If everything is fresh, say so in one line. Keep the report brief — one line per area. Then ask:
|
|
63
|
+
|
|
64
|
+
> What are we working on today?
|
|
65
|
+
|
|
47
66
|
## Slash commands
|
|
48
67
|
|
|
49
68
|
| Command | What it does |
|
|
50
69
|
|---------|-------------|
|
|
51
70
|
| `/add <description>` | Add a skill, project, or experience to your graph |
|
|
52
71
|
| `/reflect [slug]` | Review your graph for gaps, completions, and stale skills — also runs all installed module hooks |
|
|
72
|
+
|
|
73
|
+
## Modules
|
|
74
|
+
|
|
75
|
+
<!-- patina:modules:start -->
|
|
76
|
+
{{MODULES_SECTION}}
|
|
77
|
+
<!-- patina:modules:end -->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# {{PATINA_NAME}}
|
|
2
|
+
|
|
3
|
+
<!-- patina:base:start -->
|
|
4
|
+
This is your patina — a personal knowledge base for your professional story.
|
|
5
|
+
|
|
6
|
+
- Profile and config live in `profile.yaml` and `CLAUDE.md`.
|
|
7
|
+
- Your notes, skills, and posts live in `{{CONTENT_DIR}}/`.
|
|
8
|
+
|
|
9
|
+
## Installed modules
|
|
10
|
+
|
|
11
|
+
Module-specific sections are appended below as you install modules.
|
|
12
|
+
<!-- patina:base:end -->
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# LinkedIn Module Context
|
|
2
|
+
|
|
3
|
+
This module helps you draft and refine your LinkedIn profile using your patina graph as the source of truth.
|
|
4
|
+
|
|
5
|
+
## Folder structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{CONTENT_DIR}}/linkedin/
|
|
9
|
+
INSTRUCTIONS.md — module-specific rules and guidance
|
|
10
|
+
LinkedIn Current State.md — your current live profile copy
|
|
11
|
+
LinkedIn About.md — draft for the About section
|
|
12
|
+
LinkedIn Headline.md — draft for your headline
|
|
13
|
+
LinkedIn Experience.md — draft for your experience entries
|
|
14
|
+
LinkedIn Skills.md — draft for your skills section
|
|
15
|
+
LinkedIn Featured.md — draft for featured content
|
|
16
|
+
LinkedIn Activity.md — draft for activity/posts section
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Slash commands
|
|
20
|
+
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| `/li-all` | Run all LinkedIn section drafts in sequence |
|
|
24
|
+
| `/li-about` | Draft or refine your LinkedIn About section |
|
|
25
|
+
| `/li-headline` | Draft or refine your LinkedIn headline |
|
|
26
|
+
| `/li-experience` | Draft or refine your LinkedIn experience entries |
|
|
27
|
+
| `/li-skills` | Draft or refine your LinkedIn skills section |
|
|
28
|
+
| `/li-featured` | Draft or refine your LinkedIn featured content |
|
|
29
|
+
| `/li-activity` | Draft or refine your LinkedIn activity section |
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
LinkedIn commands read your `{{CONTENT_DIR}}/` graph — notes, skills, and posts — and draft profile copy grounded in that evidence. They never invent claims not supported by your notes.
|
|
34
|
+
|
|
35
|
+
The `/reflect` command also runs the LinkedIn reflect hook (`/li-all`) to keep your drafts current.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Resume Module Context
|
|
2
|
+
|
|
3
|
+
This module helps you keep your resume current by synthesising it from your patina graph.
|
|
4
|
+
|
|
5
|
+
## Folder structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{CONTENT_DIR}}/resume/
|
|
9
|
+
INSTRUCTIONS.md — module-specific rules and guidance
|
|
10
|
+
Resume Working Draft.md — the resume you are actively editing
|
|
11
|
+
Resume Last Submitted.md — the version you last sent to an employer
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Slash commands
|
|
15
|
+
|
|
16
|
+
| Command | What it does |
|
|
17
|
+
|---------|-------------|
|
|
18
|
+
| `/resume-refresh` | Refresh your resume working draft from your graph |
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The `/resume-refresh` command reads your `{{CONTENT_DIR}}/` graph — notes, skills, and experience — and updates your Resume Working Draft to reflect your current professional state. It never overwrites Resume Last Submitted; that file is yours to update manually when you send an application.
|
|
23
|
+
|
|
24
|
+
The working draft is compared against the last submitted version so you can see what has changed before sending.
|