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 existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
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
- [commandPath, render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
135
- [manifestPath, render(tpl("modules/resume/manifest.md"), vars)]
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
- ...baseManagedFiles(vars, editor, targetDir),
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 existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
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 existsSync5(join6(cwd, "profile.yaml")) ? cwd : null;
649
+ return existsSync6(join6(cwd, "profile.yaml")) ? cwd : null;
543
650
  }
544
651
  function listMarkdownFiles(dir) {
545
- if (!existsSync5(dir)) return [];
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 = readFileSync6(skillFile, "utf8");
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 = readFileSync6(file, "utf8");
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 (!existsSync5(exclusionsPath)) return [];
655
- const content = readFileSync6(exclusionsPath, "utf8");
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 = readFileSync6(file, "utf8");
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 (!existsSync6(fullPath)) return "deleted";
904
- const fileContent = readFileSync7(fullPath, "utf8");
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 (existsSync6(fullPath)) {
1090
- const existingContent = readFileSync7(fullPath, "utf8");
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 (!existsSync6(fullPath)) {
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({
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-patina",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {