my-patina 0.7.0 → 0.9.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,53 @@ var linkedinModule = {
|
|
|
109
114
|
const updated = { ...profile };
|
|
110
115
|
delete updated.linkedin;
|
|
111
116
|
return updated;
|
|
117
|
+
},
|
|
118
|
+
launchTasks: [
|
|
119
|
+
{
|
|
120
|
+
id: "open-drafts",
|
|
121
|
+
label: "Show open LinkedIn drafts",
|
|
122
|
+
template: "- Show any LinkedIn section drafts in `{{CONTENT_DIR}}/linkedin/` that have content to review."
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
readmeBlock(vars) {
|
|
126
|
+
return [
|
|
127
|
+
"## LinkedIn module",
|
|
128
|
+
"",
|
|
129
|
+
"Drafts and refines your LinkedIn profile from your graph.",
|
|
130
|
+
"",
|
|
131
|
+
"### Folder additions",
|
|
132
|
+
"",
|
|
133
|
+
"```",
|
|
134
|
+
`${vars.CONTENT_DIR}/linkedin/`,
|
|
135
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
136
|
+
" LinkedIn Current State.md \u2014 your current live profile copy",
|
|
137
|
+
" LinkedIn About.md \u2014 draft for the About section",
|
|
138
|
+
" LinkedIn Headline.md \u2014 draft for your headline",
|
|
139
|
+
" LinkedIn Experience.md \u2014 draft for your experience entries",
|
|
140
|
+
" LinkedIn Skills.md \u2014 draft for your skills section",
|
|
141
|
+
" LinkedIn Featured.md \u2014 draft for featured content",
|
|
142
|
+
" LinkedIn Activity.md \u2014 draft for activity/posts section",
|
|
143
|
+
"```",
|
|
144
|
+
"",
|
|
145
|
+
"### Commands",
|
|
146
|
+
"",
|
|
147
|
+
"| Command | What it does |",
|
|
148
|
+
"|---------|-------------|",
|
|
149
|
+
"| `/li-all` | Run all LinkedIn section drafts in sequence |",
|
|
150
|
+
"| `/li-about` | Draft or refine your LinkedIn About section |",
|
|
151
|
+
"| `/li-headline` | Draft or refine your LinkedIn headline |",
|
|
152
|
+
"| `/li-experience` | Draft or refine your LinkedIn experience entries |",
|
|
153
|
+
"| `/li-skills` | Draft or refine your LinkedIn skills section |",
|
|
154
|
+
"| `/li-featured` | Draft or refine your LinkedIn featured content |",
|
|
155
|
+
"| `/li-activity` | Draft or refine your LinkedIn activity section |"
|
|
156
|
+
].join("\n");
|
|
112
157
|
}
|
|
113
158
|
};
|
|
114
159
|
|
|
115
160
|
// src/modules/resume/index.ts
|
|
116
161
|
var RESUME_MANAGED_PATHS = [
|
|
117
162
|
".claude/commands/resume-refresh.md",
|
|
163
|
+
".claude/modules/resume/CLAUDE.md",
|
|
118
164
|
".claude/modules/resume/manifest.md"
|
|
119
165
|
];
|
|
120
166
|
var CONTENT_FILE_NAMES2 = [
|
|
@@ -129,10 +175,10 @@ var resumeModule = {
|
|
|
129
175
|
managedPaths: RESUME_MANAGED_PATHS,
|
|
130
176
|
contentFileNames: CONTENT_FILE_NAMES2,
|
|
131
177
|
managedFiles(vars) {
|
|
132
|
-
const [commandPath, manifestPath] = RESUME_MANAGED_PATHS;
|
|
133
178
|
return [
|
|
134
|
-
[
|
|
135
|
-
[
|
|
179
|
+
[".claude/commands/resume-refresh.md", render(tpl("modules/resume/commands/resume-refresh.md"), vars)],
|
|
180
|
+
[".claude/modules/resume/CLAUDE.md", render(tpl("modules/resume/CLAUDE.md"), vars)],
|
|
181
|
+
[".claude/modules/resume/manifest.md", render(tpl("modules/resume/manifest.md"), vars)]
|
|
136
182
|
];
|
|
137
183
|
},
|
|
138
184
|
contentFiles(vars, contentDir) {
|
|
@@ -140,6 +186,35 @@ var resumeModule = {
|
|
|
140
186
|
`${contentDir}/resume/${file}`,
|
|
141
187
|
render(tpl(`modules/resume/graph/${file}`), vars)
|
|
142
188
|
]);
|
|
189
|
+
},
|
|
190
|
+
launchTasks: [
|
|
191
|
+
{
|
|
192
|
+
id: "resume-stale-check",
|
|
193
|
+
label: "Flag resume if it may be out of date",
|
|
194
|
+
template: "- Compare `{{CONTENT_DIR}}/resume/Resume Working Draft.md` against recent notes and flag if the resume looks out of date."
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
readmeBlock(vars) {
|
|
198
|
+
return [
|
|
199
|
+
"## Resume module",
|
|
200
|
+
"",
|
|
201
|
+
"Keeps your resume current by synthesising it from your graph.",
|
|
202
|
+
"",
|
|
203
|
+
"### Folder additions",
|
|
204
|
+
"",
|
|
205
|
+
"```",
|
|
206
|
+
`${vars.CONTENT_DIR}/resume/`,
|
|
207
|
+
" INSTRUCTIONS.md \u2014 module rules and guidance",
|
|
208
|
+
" Resume Working Draft.md \u2014 the resume you are actively editing",
|
|
209
|
+
" Resume Last Submitted.md \u2014 the version you last sent to an employer",
|
|
210
|
+
"```",
|
|
211
|
+
"",
|
|
212
|
+
"### Commands",
|
|
213
|
+
"",
|
|
214
|
+
"| Command | What it does |",
|
|
215
|
+
"|---------|-------------|",
|
|
216
|
+
"| `/resume-refresh` | Refresh your resume working draft from your graph |"
|
|
217
|
+
].join("\n");
|
|
143
218
|
}
|
|
144
219
|
// Resume has no module-specific profile fields — no onAdd/onRemove needed.
|
|
145
220
|
};
|
|
@@ -258,6 +333,28 @@ function mergeSections(existing, newSections, storedChecksums, relativePath, ove
|
|
|
258
333
|
}
|
|
259
334
|
return { content: result, sections: outcomes };
|
|
260
335
|
}
|
|
336
|
+
function removeSection(id, content) {
|
|
337
|
+
const sections = parseSections(content);
|
|
338
|
+
const section = sections.find((s) => s.id === id);
|
|
339
|
+
if (!section) return content;
|
|
340
|
+
const { start, end } = section;
|
|
341
|
+
let before = content.slice(0, start);
|
|
342
|
+
before = before.replace(/(\r?\n)+$/, "");
|
|
343
|
+
let after = content.slice(end);
|
|
344
|
+
after = after.replace(/^(\r?\n)+/, "");
|
|
345
|
+
let result;
|
|
346
|
+
if (before === "" && after === "") {
|
|
347
|
+
result = "";
|
|
348
|
+
} else if (before === "") {
|
|
349
|
+
result = after;
|
|
350
|
+
} else if (after === "") {
|
|
351
|
+
result = before;
|
|
352
|
+
} else {
|
|
353
|
+
result = before + "\n\n" + after;
|
|
354
|
+
}
|
|
355
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
261
358
|
function inspectSections(relativePath, existing, storedChecksums) {
|
|
262
359
|
const sections = parseSections(existing);
|
|
263
360
|
const editedIds = [];
|
|
@@ -387,6 +484,66 @@ function writeState(root, state) {
|
|
|
387
484
|
);
|
|
388
485
|
}
|
|
389
486
|
|
|
487
|
+
// src/launch-tasks.ts
|
|
488
|
+
var BASE_LAUNCH_TASKS = [
|
|
489
|
+
{
|
|
490
|
+
id: "today-focus",
|
|
491
|
+
label: "Ask what to focus on today",
|
|
492
|
+
template: "- Ask the user what they want to focus on today and note it before proceeding."
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
id: "recent-notes",
|
|
496
|
+
label: "Summarise notes changed in the last 7 days",
|
|
497
|
+
template: "- Summarise any notes in `{{CONTENT_DIR}}/notes/` modified in the last 7 days, one line each."
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "open-posts",
|
|
501
|
+
label: "List unfinished posts",
|
|
502
|
+
template: "- List any drafts in `{{CONTENT_DIR}}/posts/` that look unfinished and offer to continue one."
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
function availableLaunchTasks(modules) {
|
|
506
|
+
const tasks = BASE_LAUNCH_TASKS.map((t) => ({
|
|
507
|
+
nsId: `base/${t.id}`,
|
|
508
|
+
label: t.label,
|
|
509
|
+
template: t.template,
|
|
510
|
+
source: "base"
|
|
511
|
+
}));
|
|
512
|
+
for (const moduleId of modules) {
|
|
513
|
+
const def = getModule(moduleId);
|
|
514
|
+
if (def?.launchTasks) {
|
|
515
|
+
for (const t of def.launchTasks) {
|
|
516
|
+
tasks.push({
|
|
517
|
+
nsId: `${moduleId}/${t.id}`,
|
|
518
|
+
label: t.label,
|
|
519
|
+
template: t.template,
|
|
520
|
+
source: moduleId
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return tasks;
|
|
526
|
+
}
|
|
527
|
+
function renderLaunchSection(selectedNsIds, modules) {
|
|
528
|
+
if (!selectedNsIds || selectedNsIds.length === 0) return null;
|
|
529
|
+
const avail = availableLaunchTasks(modules);
|
|
530
|
+
const availMap = new Map(avail.map((t) => [t.nsId, t]));
|
|
531
|
+
const resolved = selectedNsIds.map((id) => availMap.get(id)).filter((t) => t !== void 0);
|
|
532
|
+
if (resolved.length === 0) return null;
|
|
533
|
+
return ["## Launch tasks", "", ...resolved.map((t) => t.template)].join("\n");
|
|
534
|
+
}
|
|
535
|
+
function pruneLaunchTasks(selectedNsIds, modules) {
|
|
536
|
+
if (!selectedNsIds || selectedNsIds.length === 0) return [];
|
|
537
|
+
const avail = new Set(availableLaunchTasks(modules).map((t) => t.nsId));
|
|
538
|
+
return selectedNsIds.filter((id) => avail.has(id));
|
|
539
|
+
}
|
|
540
|
+
function launchSelectionError(values) {
|
|
541
|
+
if (values.length > 5) {
|
|
542
|
+
return "You can select at most 5 launch tasks.";
|
|
543
|
+
}
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
390
547
|
// src/scaffold.ts
|
|
391
548
|
function writeRaw(targetDir, relativePath, content) {
|
|
392
549
|
const full = join5(targetDir, relativePath);
|
|
@@ -417,6 +574,11 @@ function validateManifestFrontmatter(moduleName, content) {
|
|
|
417
574
|
}
|
|
418
575
|
function profileToVars(profile, liProfileUrl) {
|
|
419
576
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
577
|
+
const modulesSection = (profile.modules ?? []).length ? profile.modules.map((id) => {
|
|
578
|
+
const def = getModule(id);
|
|
579
|
+
const label2 = def?.label ?? id;
|
|
580
|
+
return `- [${label2} module context](.claude/modules/${id}/CLAUDE.md)`;
|
|
581
|
+
}).join("\n") : "_No modules installed._";
|
|
420
582
|
return {
|
|
421
583
|
PATINA_NAME: profile.patina_name,
|
|
422
584
|
USER_NAME: profile.name,
|
|
@@ -431,11 +593,13 @@ function profileToVars(profile, liProfileUrl) {
|
|
|
431
593
|
STALENESS_THRESHOLD: (() => {
|
|
432
594
|
const d = Number(profile.staleness_threshold_days ?? 30);
|
|
433
595
|
return String(Number.isFinite(d) && d > 0 ? d : 30);
|
|
434
|
-
})()
|
|
596
|
+
})(),
|
|
597
|
+
MODULES_SECTION: modulesSection
|
|
435
598
|
};
|
|
436
599
|
}
|
|
437
600
|
function baseManagedFiles(vars, editor, targetDir) {
|
|
438
601
|
const files = [
|
|
602
|
+
["README.md", render(tpl("README.md"), vars)],
|
|
439
603
|
["CLAUDE.md", render(tpl("CLAUDE.md"), vars)],
|
|
440
604
|
[".claude/settings.json", tpl(".claude/settings.json")],
|
|
441
605
|
[".claude/commands/add.md", render(tpl(".claude/commands/add.md"), vars)],
|
|
@@ -472,7 +636,8 @@ async function scaffold(opts) {
|
|
|
472
636
|
editor,
|
|
473
637
|
modules,
|
|
474
638
|
liProfileUrl,
|
|
475
|
-
contentDir
|
|
639
|
+
contentDir,
|
|
640
|
+
launchTasks = []
|
|
476
641
|
} = opts;
|
|
477
642
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
478
643
|
const tempProfile = {
|
|
@@ -486,13 +651,27 @@ async function scaffold(opts) {
|
|
|
486
651
|
modules,
|
|
487
652
|
content_dir: contentDir,
|
|
488
653
|
created: today,
|
|
654
|
+
...launchTasks.length ? { launch_tasks: launchTasks } : {},
|
|
489
655
|
...modules.includes("linkedin") && liProfileUrl ? { linkedin: { profile_url: liProfileUrl } } : {}
|
|
490
656
|
};
|
|
491
657
|
const vars = profileToVars(tempProfile, liProfileUrl);
|
|
492
658
|
mkdirSync2(targetDir, { recursive: true });
|
|
493
659
|
const checksums = {};
|
|
660
|
+
const baseFiles = baseManagedFiles(vars, editor, targetDir);
|
|
661
|
+
const readmePath = join5(targetDir, "README.md");
|
|
662
|
+
const filteredBaseFiles = baseFiles.filter(([rel]) => {
|
|
663
|
+
if (rel === "README.md") {
|
|
664
|
+
if (existsSync5(readmePath)) {
|
|
665
|
+
const existing = readFileSync6(readmePath, "utf8");
|
|
666
|
+
if (!hasFences(existing)) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return true;
|
|
672
|
+
});
|
|
494
673
|
const managedFiles = [
|
|
495
|
-
...
|
|
674
|
+
...filteredBaseFiles,
|
|
496
675
|
...modules.flatMap((m) => moduleManagedFiles(m, vars))
|
|
497
676
|
];
|
|
498
677
|
for (const module of modules) {
|
|
@@ -506,6 +685,27 @@ async function scaffold(opts) {
|
|
|
506
685
|
checksums[`${relativePath}:${s.id}`] = s.newChecksum;
|
|
507
686
|
}
|
|
508
687
|
}
|
|
688
|
+
for (const module of modules) {
|
|
689
|
+
const def = getModule(module);
|
|
690
|
+
if (def?.readmeBlock) {
|
|
691
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
692
|
+
const result = writeManagedFile(targetDir, "README.md", block, checksums);
|
|
693
|
+
checksums["README.md"] = result.checksum;
|
|
694
|
+
for (const s of result.sections ?? []) {
|
|
695
|
+
checksums[`README.md:${s.id}`] = s.newChecksum;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const rawLaunch = renderLaunchSection(launchTasks, modules);
|
|
700
|
+
const expandedLaunch = rawLaunch ? render(rawLaunch, vars) : null;
|
|
701
|
+
if (expandedLaunch) {
|
|
702
|
+
const launchBlock = renderSection("launch", expandedLaunch);
|
|
703
|
+
const result = writeManagedFile(targetDir, "CLAUDE.md", launchBlock, checksums);
|
|
704
|
+
checksums["CLAUDE.md"] = result.checksum;
|
|
705
|
+
for (const s of result.sections ?? []) {
|
|
706
|
+
checksums[`CLAUDE.md:${s.id}`] = s.newChecksum;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
509
709
|
const baseDirs = ["notes", "skills", "posts"];
|
|
510
710
|
for (const dir of baseDirs) {
|
|
511
711
|
touch(targetDir, `${contentDir}/${dir}/.gitkeep`);
|
|
@@ -526,16 +726,16 @@ ${STATE_FILENAME}
|
|
|
526
726
|
}
|
|
527
727
|
|
|
528
728
|
// src/validate.ts
|
|
529
|
-
import { existsSync as
|
|
729
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
530
730
|
import { join as join6, relative, sep, basename } from "path";
|
|
531
731
|
var NOTES = CONTENT_SUBDIRS[0];
|
|
532
732
|
var SKILLS = CONTENT_SUBDIRS[1];
|
|
533
733
|
var POSTS = CONTENT_SUBDIRS[2];
|
|
534
734
|
function findPatinaRoot(cwd) {
|
|
535
|
-
return
|
|
735
|
+
return existsSync6(join6(cwd, "profile.yaml")) ? cwd : null;
|
|
536
736
|
}
|
|
537
737
|
function listMarkdownFiles(dir) {
|
|
538
|
-
if (!
|
|
738
|
+
if (!existsSync6(dir)) return [];
|
|
539
739
|
const results = [];
|
|
540
740
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
541
741
|
const fullPath = join6(dir, entry.name);
|
|
@@ -598,7 +798,7 @@ function checkSkillNotes(root, profile) {
|
|
|
598
798
|
const noteSlugs = new Set(noteFiles.map((f) => basename(f, ".md")));
|
|
599
799
|
const issues = [];
|
|
600
800
|
for (const skillFile of listMarkdownFiles(skillsDir)) {
|
|
601
|
-
const content =
|
|
801
|
+
const content = readFileSync7(skillFile, "utf8");
|
|
602
802
|
const links = extractWikiLinks(content);
|
|
603
803
|
for (const { target, line } of links) {
|
|
604
804
|
if (!noteSlugs.has(target)) {
|
|
@@ -625,7 +825,7 @@ function checkWikiLinks(root, profile) {
|
|
|
625
825
|
...listMarkdownFiles(postsDir)
|
|
626
826
|
];
|
|
627
827
|
for (const file of filesToScan) {
|
|
628
|
-
const content =
|
|
828
|
+
const content = readFileSync7(file, "utf8");
|
|
629
829
|
const links = extractWikiLinks(content);
|
|
630
830
|
for (const { target, line } of links) {
|
|
631
831
|
if (!noteSlugs.has(target)) {
|
|
@@ -644,8 +844,8 @@ function checkExclusions(root, profile) {
|
|
|
644
844
|
const contentDir = join6(root, profile.content_dir ?? "graph");
|
|
645
845
|
const notesDir = join6(contentDir, NOTES);
|
|
646
846
|
const exclusionsPath = join6(notesDir, "exclusions.md");
|
|
647
|
-
if (!
|
|
648
|
-
const content =
|
|
847
|
+
if (!existsSync6(exclusionsPath)) return [];
|
|
848
|
+
const content = readFileSync7(exclusionsPath, "utf8");
|
|
649
849
|
const items = parseExclusions(content);
|
|
650
850
|
if (items.length === 0) return [];
|
|
651
851
|
const skillsDir = join6(contentDir, SKILLS);
|
|
@@ -657,7 +857,7 @@ function checkExclusions(root, profile) {
|
|
|
657
857
|
const issues = [];
|
|
658
858
|
const seen = /* @__PURE__ */ new Set();
|
|
659
859
|
for (const file of filesToScan) {
|
|
660
|
-
const fileContent =
|
|
860
|
+
const fileContent = readFileSync7(file, "utf8");
|
|
661
861
|
const lines = fileContent.split("\n");
|
|
662
862
|
for (let i = 0; i < lines.length; i++) {
|
|
663
863
|
const lineText = lines[i];
|
|
@@ -742,6 +942,58 @@ function label(text2) {
|
|
|
742
942
|
var MULTISELECT_HINT = `
|
|
743
943
|
${chalk.hex("#64748B")("\u2191\u2193 to move \xB7 space to select \xB7 enter to confirm")}`;
|
|
744
944
|
var OPTIONAL_HINT = ` ${chalk.dim.italic("optional, but helps a lot \u2014 hit enter to skip")}`;
|
|
945
|
+
function applyLaunchBlock(cwd, launchTasks, modules, vars, checksums, overwrite) {
|
|
946
|
+
const updated = [];
|
|
947
|
+
const skipped = [];
|
|
948
|
+
const keptSections = [];
|
|
949
|
+
const rawLaunch = renderLaunchSection(launchTasks, modules);
|
|
950
|
+
if (rawLaunch) {
|
|
951
|
+
const block = renderSection("launch", render(rawLaunch, vars));
|
|
952
|
+
const result = writeManagedFile(cwd, "CLAUDE.md", block, checksums, overwrite);
|
|
953
|
+
checksums["CLAUDE.md"] = result.checksum;
|
|
954
|
+
for (const s of result.sections ?? []) {
|
|
955
|
+
const sKey = `CLAUDE.md:${s.id}`;
|
|
956
|
+
if (s.outcome !== "skipped") checksums[sKey] = s.newChecksum;
|
|
957
|
+
else keptSections.push(sKey);
|
|
958
|
+
}
|
|
959
|
+
if (result.outcome === "skipped") skipped.push("CLAUDE.md");
|
|
960
|
+
else updated.push("CLAUDE.md");
|
|
961
|
+
} else {
|
|
962
|
+
const claudePath = join7(cwd, "CLAUDE.md");
|
|
963
|
+
if (existsSync7(claudePath)) {
|
|
964
|
+
const before = readFileSync8(claudePath, "utf8");
|
|
965
|
+
const editedIds = inspectSections("CLAUDE.md", before, checksums);
|
|
966
|
+
if (!editedIds.includes("launch") || overwrite?.has("launch")) {
|
|
967
|
+
const after = removeSection("launch", before);
|
|
968
|
+
if (after !== before) {
|
|
969
|
+
writeFileSync4(claudePath, after, "utf8");
|
|
970
|
+
checksums["CLAUDE.md"] = hashContent(after);
|
|
971
|
+
delete checksums["CLAUDE.md:launch"];
|
|
972
|
+
updated.push("CLAUDE.md");
|
|
973
|
+
}
|
|
974
|
+
} else {
|
|
975
|
+
keptSections.push("CLAUDE.md:launch");
|
|
976
|
+
skipped.push("CLAUDE.md");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return { updated, skipped, keptSections };
|
|
981
|
+
}
|
|
982
|
+
async function promptLaunchTasks(avail, initial) {
|
|
983
|
+
const selected = await p.multiselect({
|
|
984
|
+
message: `Which tasks should run every time you launch Patina?${MULTISELECT_HINT}`,
|
|
985
|
+
options: avail.map((t) => ({
|
|
986
|
+
value: t.nsId,
|
|
987
|
+
label: t.label,
|
|
988
|
+
hint: chalk.hex("#64748B")(t.source)
|
|
989
|
+
})),
|
|
990
|
+
initialValues: initial,
|
|
991
|
+
required: false,
|
|
992
|
+
validate: launchSelectionError
|
|
993
|
+
});
|
|
994
|
+
if (p.isCancel(selected)) onCancel();
|
|
995
|
+
return Array.isArray(selected) ? selected : [];
|
|
996
|
+
}
|
|
745
997
|
async function main() {
|
|
746
998
|
printBanner();
|
|
747
999
|
const cwd = process.cwd();
|
|
@@ -848,6 +1100,18 @@ async function runInstall(cwd) {
|
|
|
848
1100
|
});
|
|
849
1101
|
liProfileUrl = typeof url === "string" ? url : "";
|
|
850
1102
|
}
|
|
1103
|
+
let launchTasks = [];
|
|
1104
|
+
const availTasks = availableLaunchTasks(modules);
|
|
1105
|
+
if (availTasks.length > 0) {
|
|
1106
|
+
const setupLaunch = await p.confirm({
|
|
1107
|
+
message: "Would you like to set up launch tasks?",
|
|
1108
|
+
initialValue: false
|
|
1109
|
+
});
|
|
1110
|
+
if (p.isCancel(setupLaunch)) onCancel();
|
|
1111
|
+
if (setupLaunch) {
|
|
1112
|
+
launchTasks = await promptLaunchTasks(availTasks, []);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
851
1115
|
const slug = slugify(identity.patinaName);
|
|
852
1116
|
const targetDir = resolve(cwd, slug);
|
|
853
1117
|
const s = p.spinner();
|
|
@@ -870,7 +1134,8 @@ async function runInstall(cwd) {
|
|
|
870
1134
|
editor: setup.editor,
|
|
871
1135
|
modules,
|
|
872
1136
|
liProfileUrl,
|
|
873
|
-
contentDir: "graph"
|
|
1137
|
+
contentDir: "graph",
|
|
1138
|
+
launchTasks
|
|
874
1139
|
});
|
|
875
1140
|
s.stop(chalk.green("Done."));
|
|
876
1141
|
} catch (err) {
|
|
@@ -893,8 +1158,8 @@ function writeProfile(cwd, profile) {
|
|
|
893
1158
|
}
|
|
894
1159
|
function removeManagedFileIfUnmodified(targetDir, rel, stored) {
|
|
895
1160
|
const fullPath = join7(targetDir, rel);
|
|
896
|
-
if (!
|
|
897
|
-
const fileContent =
|
|
1161
|
+
if (!existsSync7(fullPath)) return "deleted";
|
|
1162
|
+
const fileContent = readFileSync8(fullPath, "utf8");
|
|
898
1163
|
if (hasFences(fileContent)) {
|
|
899
1164
|
const editedIds = inspectSections(rel, fileContent, stored);
|
|
900
1165
|
if (editedIds.length > 0) {
|
|
@@ -916,10 +1181,11 @@ async function runUpdate(cwd) {
|
|
|
916
1181
|
p.intro(chalk.hex("#94A3B8")(`Found: ${chalk.bold.white(profile.patina_name || "patina")}`));
|
|
917
1182
|
p.note(
|
|
918
1183
|
[
|
|
919
|
-
`${chalk.hex("#64748B")("Name:")}
|
|
920
|
-
`${chalk.hex("#64748B")("Title:")}
|
|
921
|
-
`${chalk.hex("#64748B")("Company:")}
|
|
922
|
-
`${chalk.hex("#64748B")("Modules:")}
|
|
1184
|
+
`${chalk.hex("#64748B")("Name:")} ${profile.name}`,
|
|
1185
|
+
`${chalk.hex("#64748B")("Title:")} ${profile.title || "\u2014"}`,
|
|
1186
|
+
`${chalk.hex("#64748B")("Company:")} ${profile.work?.company_name || "\u2014"}`,
|
|
1187
|
+
`${chalk.hex("#64748B")("Modules:")} ${profile.modules?.join(", ") || "none"}`,
|
|
1188
|
+
`${chalk.hex("#64748B")("Launch tasks:")} ${profile.launch_tasks?.length ?? 0}`
|
|
923
1189
|
].join("\n"),
|
|
924
1190
|
label("Current profile")
|
|
925
1191
|
);
|
|
@@ -928,6 +1194,7 @@ async function runUpdate(cwd) {
|
|
|
928
1194
|
options: [
|
|
929
1195
|
{ value: "profile", label: "Update personal info" },
|
|
930
1196
|
{ value: "modules", label: "Add or remove modules" },
|
|
1197
|
+
{ value: "launch-tasks", label: "Set up launch tasks", hint: chalk.hex("#64748B")("tasks Claude runs every session") },
|
|
931
1198
|
{ value: "validate", label: "Run health check", hint: chalk.hex("#64748B")("check for broken links and excluded items") },
|
|
932
1199
|
{ value: "nothing", label: "Nothing \u2014 just checking" }
|
|
933
1200
|
]
|
|
@@ -940,6 +1207,8 @@ async function runUpdate(cwd) {
|
|
|
940
1207
|
await runUpdateProfile(cwd, profile);
|
|
941
1208
|
} else if (action === "modules") {
|
|
942
1209
|
await runUpdateModules(cwd, profile);
|
|
1210
|
+
} else if (action === "launch-tasks") {
|
|
1211
|
+
await runUpdateLaunchTasks(cwd, profile);
|
|
943
1212
|
} else if (action === "validate") {
|
|
944
1213
|
await runValidate(cwd, profile);
|
|
945
1214
|
}
|
|
@@ -985,6 +1254,45 @@ function applyProfileUpdate(cwd, profile, fields, overwrite) {
|
|
|
985
1254
|
updated.push(rel);
|
|
986
1255
|
}
|
|
987
1256
|
}
|
|
1257
|
+
for (const module of updatedProfile.modules) {
|
|
1258
|
+
const def = getModule(module);
|
|
1259
|
+
if (def?.readmeBlock) {
|
|
1260
|
+
const readmePath = join7(cwd, "README.md");
|
|
1261
|
+
const readmeExists = existsSync7(readmePath);
|
|
1262
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1263
|
+
if (!readmeExists || readmeHasFences) {
|
|
1264
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1265
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums, overwrite);
|
|
1266
|
+
newChecksums["README.md"] = result.checksum;
|
|
1267
|
+
for (const s of result.sections ?? []) {
|
|
1268
|
+
const sKey = `README.md:${s.id}`;
|
|
1269
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1270
|
+
else {
|
|
1271
|
+
newChecksums[sKey] = stored[sKey] ?? "";
|
|
1272
|
+
keptSections.push(sKey);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (result.outcome === "skipped") {
|
|
1276
|
+
skipped.push(`README.md:${module}`);
|
|
1277
|
+
} else if (result.outcome !== "updated" || result.sections?.some((s) => s.id === module && s.outcome !== "unchanged")) {
|
|
1278
|
+
updated.push(`README.md:${module}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (updatedProfile.launch_tasks?.length) {
|
|
1284
|
+
const launchResult = applyLaunchBlock(
|
|
1285
|
+
cwd,
|
|
1286
|
+
updatedProfile.launch_tasks,
|
|
1287
|
+
updatedProfile.modules,
|
|
1288
|
+
vars,
|
|
1289
|
+
newChecksums,
|
|
1290
|
+
overwrite
|
|
1291
|
+
);
|
|
1292
|
+
updated.push(...launchResult.updated);
|
|
1293
|
+
skipped.push(...launchResult.skipped);
|
|
1294
|
+
keptSections.push(...launchResult.keptSections);
|
|
1295
|
+
}
|
|
988
1296
|
for (const [rel, hash] of Object.entries(stored)) {
|
|
989
1297
|
if (!(rel in newChecksums)) {
|
|
990
1298
|
newChecksums[rel] = hash;
|
|
@@ -1079,8 +1387,8 @@ async function runUpdateProfile(cwd, profile) {
|
|
|
1079
1387
|
for (const [rel, content] of previewFiles) {
|
|
1080
1388
|
if (hasFences(content)) {
|
|
1081
1389
|
const fullPath = join7(cwd, rel);
|
|
1082
|
-
if (
|
|
1083
|
-
const existingContent =
|
|
1390
|
+
if (existsSync7(fullPath)) {
|
|
1391
|
+
const existingContent = readFileSync8(fullPath, "utf8");
|
|
1084
1392
|
const editedIds = inspectSections(rel, existingContent, storedChecksums);
|
|
1085
1393
|
for (const sectionId of editedIds) {
|
|
1086
1394
|
const confirmed = await p.confirm({
|
|
@@ -1142,12 +1450,28 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1142
1450
|
}
|
|
1143
1451
|
for (const [relativePath, content] of moduleContentFiles(module, vars, contentDir)) {
|
|
1144
1452
|
const fullPath = join7(cwd, relativePath);
|
|
1145
|
-
if (!
|
|
1453
|
+
if (!existsSync7(fullPath)) {
|
|
1146
1454
|
mkdirSync3(dirname4(fullPath), { recursive: true });
|
|
1147
1455
|
writeFileSync4(fullPath, content, "utf8");
|
|
1148
1456
|
added.push(relativePath);
|
|
1149
1457
|
}
|
|
1150
1458
|
}
|
|
1459
|
+
if (def?.readmeBlock) {
|
|
1460
|
+
const readmePath = join7(cwd, "README.md");
|
|
1461
|
+
const readmeExists = existsSync7(readmePath);
|
|
1462
|
+
const readmeHasFences = readmeExists ? hasFences(readFileSync8(readmePath, "utf8")) : false;
|
|
1463
|
+
if (!readmeExists || readmeHasFences) {
|
|
1464
|
+
const block = renderSection(module, def.readmeBlock(vars));
|
|
1465
|
+
const result = writeManagedFile(cwd, "README.md", block, newChecksums);
|
|
1466
|
+
newChecksums["README.md"] = result.checksum;
|
|
1467
|
+
for (const s of result.sections ?? []) {
|
|
1468
|
+
newChecksums[`README.md:${s.id}`] = s.newChecksum;
|
|
1469
|
+
}
|
|
1470
|
+
if (result.outcome !== "skipped") added.push(`README.md:${module}`);
|
|
1471
|
+
} else {
|
|
1472
|
+
kept.push("README.md");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1151
1475
|
if (!updatedProfile.modules.includes(module)) {
|
|
1152
1476
|
updatedProfile.modules = [...updatedProfile.modules, module];
|
|
1153
1477
|
}
|
|
@@ -1168,16 +1492,103 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
|
|
|
1168
1492
|
kept.push(rel);
|
|
1169
1493
|
}
|
|
1170
1494
|
}
|
|
1495
|
+
const readmePath = join7(cwd, "README.md");
|
|
1496
|
+
if (existsSync7(readmePath)) {
|
|
1497
|
+
const before = readFileSync8(readmePath, "utf8");
|
|
1498
|
+
const editedIds = inspectSections("README.md", before, stored);
|
|
1499
|
+
if (!editedIds.includes(module)) {
|
|
1500
|
+
const after = removeSection(module, before);
|
|
1501
|
+
if (after !== before) {
|
|
1502
|
+
writeFileSync4(readmePath, after, "utf8");
|
|
1503
|
+
newChecksums["README.md"] = hashContent(after);
|
|
1504
|
+
delete newChecksums[`README.md:${module}`];
|
|
1505
|
+
deleted.push(`README.md:${module}`);
|
|
1506
|
+
}
|
|
1507
|
+
} else {
|
|
1508
|
+
keptSections.push(`README.md:${module}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1171
1511
|
updatedProfile.modules = updatedProfile.modules.filter((m) => m !== module);
|
|
1172
1512
|
if (def?.onRemove) {
|
|
1173
1513
|
updatedProfile = def.onRemove(updatedProfile);
|
|
1174
1514
|
}
|
|
1175
1515
|
}
|
|
1516
|
+
const prunedTasks = pruneLaunchTasks(updatedProfile.launch_tasks, updatedProfile.modules);
|
|
1517
|
+
updatedProfile = { ...updatedProfile, launch_tasks: prunedTasks.length ? prunedTasks : void 0 };
|
|
1518
|
+
const finalVars = profileToVars(updatedProfile);
|
|
1519
|
+
for (const [rel, content] of baseManagedFiles(finalVars, updatedProfile.editor, cwd)) {
|
|
1520
|
+
const result = writeManagedFile(cwd, rel, content, newChecksums);
|
|
1521
|
+
newChecksums[rel] = result.checksum;
|
|
1522
|
+
for (const s of result.sections ?? []) {
|
|
1523
|
+
const sKey = `${rel}:${s.id}`;
|
|
1524
|
+
if (s.outcome !== "skipped") newChecksums[sKey] = s.newChecksum;
|
|
1525
|
+
else {
|
|
1526
|
+
newChecksums[sKey] = newChecksums[sKey] ?? "";
|
|
1527
|
+
keptSections.push(sKey);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
const launchResult = applyLaunchBlock(cwd, updatedProfile.launch_tasks ?? [], updatedProfile.modules, finalVars, newChecksums);
|
|
1532
|
+
keptSections.push(...launchResult.keptSections);
|
|
1176
1533
|
writeState(cwd, { checksums: newChecksums });
|
|
1177
1534
|
const finalProfile = stripLegacyChecksums(updatedProfile);
|
|
1178
1535
|
writeProfile(cwd, finalProfile);
|
|
1179
1536
|
return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
|
|
1180
1537
|
}
|
|
1538
|
+
function applyLaunchTaskUpdate(cwd, profile, launchTasks, overwrite) {
|
|
1539
|
+
const updatedProfile = {
|
|
1540
|
+
...profile,
|
|
1541
|
+
launch_tasks: launchTasks.length ? launchTasks : void 0
|
|
1542
|
+
};
|
|
1543
|
+
const vars = profileToVars(updatedProfile);
|
|
1544
|
+
const stored = readState(cwd, profile).checksums;
|
|
1545
|
+
const newChecksums = { ...stored };
|
|
1546
|
+
const { updated, skipped, keptSections } = applyLaunchBlock(
|
|
1547
|
+
cwd,
|
|
1548
|
+
launchTasks,
|
|
1549
|
+
profile.modules ?? [],
|
|
1550
|
+
vars,
|
|
1551
|
+
newChecksums,
|
|
1552
|
+
overwrite
|
|
1553
|
+
);
|
|
1554
|
+
writeState(cwd, { checksums: newChecksums });
|
|
1555
|
+
const profileToWrite = stripLegacyChecksums(updatedProfile);
|
|
1556
|
+
writeProfile(cwd, profileToWrite);
|
|
1557
|
+
return { profile: profileToWrite, updated, skipped, keptSections };
|
|
1558
|
+
}
|
|
1559
|
+
async function runUpdateLaunchTasks(cwd, profile) {
|
|
1560
|
+
const avail = availableLaunchTasks(profile.modules ?? []);
|
|
1561
|
+
const initial = pruneLaunchTasks(profile.launch_tasks, profile.modules ?? []);
|
|
1562
|
+
const overwriteSet = /* @__PURE__ */ new Set();
|
|
1563
|
+
const claudePath = join7(cwd, "CLAUDE.md");
|
|
1564
|
+
if (existsSync7(claudePath)) {
|
|
1565
|
+
const storedChecksums = readState(cwd, profile).checksums;
|
|
1566
|
+
const existingContent = readFileSync8(claudePath, "utf8");
|
|
1567
|
+
const editedIds = inspectSections("CLAUDE.md", existingContent, storedChecksums);
|
|
1568
|
+
if (editedIds.includes("launch")) {
|
|
1569
|
+
const confirmed = await p.confirm({
|
|
1570
|
+
message: `Section 'launch' in CLAUDE.md has been manually edited. Overwrite?`,
|
|
1571
|
+
initialValue: false
|
|
1572
|
+
});
|
|
1573
|
+
if (p.isCancel(confirmed)) onCancel();
|
|
1574
|
+
if (confirmed) overwriteSet.add("launch");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const selected = await promptLaunchTasks(avail, initial);
|
|
1578
|
+
const { updated, skipped, keptSections } = applyLaunchTaskUpdate(cwd, profile, selected, overwriteSet);
|
|
1579
|
+
const summaryLines = [];
|
|
1580
|
+
if (updated.length > 0) {
|
|
1581
|
+
summaryLines.push(chalk.hex("#94A3B8")(`Updated: ${updated.join(", ")}`));
|
|
1582
|
+
}
|
|
1583
|
+
if (keptSections.length > 0) {
|
|
1584
|
+
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${keptSections.join(", ")}`));
|
|
1585
|
+
}
|
|
1586
|
+
if (skipped.length > 0) {
|
|
1587
|
+
summaryLines.push(chalk.hex("#FFAB2E")(`Kept your edits: ${skipped.join(", ")}`));
|
|
1588
|
+
}
|
|
1589
|
+
p.note(summaryLines.join("\n") || "No changes.", label("Done"));
|
|
1590
|
+
p.outro(chalk.hex("#94A3B8")("Launch tasks updated."));
|
|
1591
|
+
}
|
|
1181
1592
|
async function runUpdateModules(cwd, profile) {
|
|
1182
1593
|
const currentModules = profile.modules ?? [];
|
|
1183
1594
|
const selected = await p.multiselect({
|
|
@@ -1201,6 +1612,16 @@ async function runUpdateModules(cwd, profile) {
|
|
|
1201
1612
|
p.outro(chalk.hex("#94A3B8")("No changes \u2014 modules unchanged."));
|
|
1202
1613
|
return;
|
|
1203
1614
|
}
|
|
1615
|
+
const changeLines = [];
|
|
1616
|
+
for (const m of toAdd) {
|
|
1617
|
+
const def = getModule(m);
|
|
1618
|
+
changeLines.push(`Adding ${def?.label ?? m}: appends a section to README.md, adds a link to CLAUDE.md`);
|
|
1619
|
+
}
|
|
1620
|
+
for (const m of toRemove) {
|
|
1621
|
+
const def = getModule(m);
|
|
1622
|
+
changeLines.push(`Removing ${def?.label ?? m}: removes its section from README.md and its link from CLAUDE.md`);
|
|
1623
|
+
}
|
|
1624
|
+
p.note(changeLines.join("\n"), label("Planned changes"));
|
|
1204
1625
|
let liProfileUrl;
|
|
1205
1626
|
if (toAdd.includes("linkedin") && !profile.linkedin?.profile_url) {
|
|
1206
1627
|
const url = await p.text({
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -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.
|