my-patina 0.7.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,
@@ -431,11 +519,13 @@ function profileToVars(profile, liProfileUrl) {
431
519
  STALENESS_THRESHOLD: (() => {
432
520
  const d = Number(profile.staleness_threshold_days ?? 30);
433
521
  return String(Number.isFinite(d) && d > 0 ? d : 30);
434
- })()
522
+ })(),
523
+ MODULES_SECTION: modulesSection
435
524
  };
436
525
  }
437
526
  function baseManagedFiles(vars, editor, targetDir) {
438
527
  const files = [
528
+ ["README.md", render(tpl("README.md"), vars)],
439
529
  ["CLAUDE.md", render(tpl("CLAUDE.md"), vars)],
440
530
  [".claude/settings.json", tpl(".claude/settings.json")],
441
531
  [".claude/commands/add.md", render(tpl(".claude/commands/add.md"), vars)],
@@ -491,8 +581,21 @@ async function scaffold(opts) {
491
581
  const vars = profileToVars(tempProfile, liProfileUrl);
492
582
  mkdirSync2(targetDir, { recursive: true });
493
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
+ });
494
597
  const managedFiles = [
495
- ...baseManagedFiles(vars, editor, targetDir),
598
+ ...filteredBaseFiles,
496
599
  ...modules.flatMap((m) => moduleManagedFiles(m, vars))
497
600
  ];
498
601
  for (const module of modules) {
@@ -506,6 +609,17 @@ async function scaffold(opts) {
506
609
  checksums[`${relativePath}:${s.id}`] = s.newChecksum;
507
610
  }
508
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
+ }
509
623
  const baseDirs = ["notes", "skills", "posts"];
510
624
  for (const dir of baseDirs) {
511
625
  touch(targetDir, `${contentDir}/${dir}/.gitkeep`);
@@ -526,16 +640,16 @@ ${STATE_FILENAME}
526
640
  }
527
641
 
528
642
  // src/validate.ts
529
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync } from "fs";
643
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync } from "fs";
530
644
  import { join as join6, relative, sep, basename } from "path";
531
645
  var NOTES = CONTENT_SUBDIRS[0];
532
646
  var SKILLS = CONTENT_SUBDIRS[1];
533
647
  var POSTS = CONTENT_SUBDIRS[2];
534
648
  function findPatinaRoot(cwd) {
535
- return existsSync5(join6(cwd, "profile.yaml")) ? cwd : null;
649
+ return existsSync6(join6(cwd, "profile.yaml")) ? cwd : null;
536
650
  }
537
651
  function listMarkdownFiles(dir) {
538
- if (!existsSync5(dir)) return [];
652
+ if (!existsSync6(dir)) return [];
539
653
  const results = [];
540
654
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
541
655
  const fullPath = join6(dir, entry.name);
@@ -598,7 +712,7 @@ function checkSkillNotes(root, profile) {
598
712
  const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
599
713
  const issues = [];
600
714
  for (const skillFile of listMarkdownFiles(skillsDir)) {
601
- const content = readFileSync6(skillFile, "utf8");
715
+ const content = readFileSync7(skillFile, "utf8");
602
716
  const links = extractWikiLinks(content);
603
717
  for (const { target, line } of links) {
604
718
  if (!noteSlugs.has(target)) {
@@ -625,7 +739,7 @@ function checkWikiLinks(root, profile) {
625
739
  ...listMarkdownFiles(postsDir)
626
740
  ];
627
741
  for (const file of filesToScan) {
628
- const content = readFileSync6(file, "utf8");
742
+ const content = readFileSync7(file, "utf8");
629
743
  const links = extractWikiLinks(content);
630
744
  for (const { target, line } of links) {
631
745
  if (!noteSlugs.has(target)) {
@@ -644,8 +758,8 @@ function checkExclusions(root, profile) {
644
758
  const contentDir = join6(root, profile.content_dir ?? "graph");
645
759
  const notesDir = join6(contentDir, NOTES);
646
760
  const exclusionsPath = join6(notesDir, "exclusions.md");
647
- if (!existsSync5(exclusionsPath)) return [];
648
- const content = readFileSync6(exclusionsPath, "utf8");
761
+ if (!existsSync6(exclusionsPath)) return [];
762
+ const content = readFileSync7(exclusionsPath, "utf8");
649
763
  const items = parseExclusions(content);
650
764
  if (items.length === 0) return [];
651
765
  const skillsDir = join6(contentDir, SKILLS);
@@ -657,7 +771,7 @@ function checkExclusions(root, profile) {
657
771
  const issues = [];
658
772
  const seen = /* @__PURE__ */ new Set();
659
773
  for (const file of filesToScan) {
660
- const fileContent = readFileSync6(file, "utf8");
774
+ const fileContent = readFileSync7(file, "utf8");
661
775
  const lines = fileContent.split("\n");
662
776
  for (let i = 0; i < lines.length; i++) {
663
777
  const lineText = lines[i];
@@ -893,8 +1007,8 @@ function writeProfile(cwd, profile) {
893
1007
  }
894
1008
  function removeManagedFileIfUnmodified(targetDir, rel, stored) {
895
1009
  const fullPath = join7(targetDir, rel);
896
- if (!existsSync6(fullPath)) return "deleted";
897
- const fileContent = readFileSync7(fullPath, "utf8");
1010
+ if (!existsSync7(fullPath)) return "deleted";
1011
+ const fileContent = readFileSync8(fullPath, "utf8");
898
1012
  if (hasFences(fileContent)) {
899
1013
  const editedIds = inspectSections(rel, fileContent, stored);
900
1014
  if (editedIds.length > 0) {
@@ -985,6 +1099,32 @@ function applyProfileUpdate(cwd, profile, fields, overwrite) {
985
1099
  updated.push(rel);
986
1100
  }
987
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
+ }
988
1128
  for (const [rel, hash] of Object.entries(stored)) {
989
1129
  if (!(rel in newChecksums)) {
990
1130
  newChecksums[rel] = hash;
@@ -1079,8 +1219,8 @@ async function runUpdateProfile(cwd, profile) {
1079
1219
  for (const [rel, content] of previewFiles) {
1080
1220
  if (hasFences(content)) {
1081
1221
  const fullPath = join7(cwd, rel);
1082
- if (existsSync6(fullPath)) {
1083
- const existingContent = readFileSync7(fullPath, "utf8");
1222
+ if (existsSync7(fullPath)) {
1223
+ const existingContent = readFileSync8(fullPath, "utf8");
1084
1224
  const editedIds = inspectSections(rel, existingContent, storedChecksums);
1085
1225
  for (const sectionId of editedIds) {
1086
1226
  const confirmed = await p.confirm({
@@ -1142,12 +1282,28 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
1142
1282
  }
1143
1283
  for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
1144
1284
  const fullPath = join7(cwd, relativePath);
1145
- if (!existsSync6(fullPath)) {
1285
+ if (!existsSync7(fullPath)) {
1146
1286
  mkdirSync3(dirname4(fullPath), { recursive: true });
1147
1287
  writeFileSync4(fullPath, content, "utf8");
1148
1288
  added.push(relativePath);
1149
1289
  }
1150
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
+ }
1151
1307
  if (!updatedProfile.modules.includes(module)) {
1152
1308
  updatedProfile.modules = [...updatedProfile.modules, module];
1153
1309
  }
@@ -1168,11 +1324,40 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
1168
1324
  kept.push(rel);
1169
1325
  }
1170
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
+ }
1171
1343
  updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
1172
1344
  if (def?.onRemove) {
1173
1345
  updatedProfile = def.onRemove(updatedProfile);
1174
1346
  }
1175
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
+ }
1176
1361
  writeState(cwd, { checksums: newChecksums });
1177
1362
  const finalProfile = stripLegacyChecksums(updatedProfile);
1178
1363
  writeProfile(cwd, finalProfile);
@@ -1201,6 +1386,16 @@ async function runUpdateModules(cwd, profile) {
1201
1386
  p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
1202
1387
  return;
1203
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"));
1204
1399
  let liProfileUrl;
1205
1400
  if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
1206
1401
  const url = await p.text({
@@ -69,3 +69,9 @@ Skip any area with nothing stale. If everything is fresh, say so in one line. Ke
69
69
  |---------|-------------|
70
70
  | `/add <description>` | Add a skill, project, or experience to your graph |
71
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.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {