my-patina 0.8.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.
Files changed (2) hide show
  1. package/dist/cli.js +232 -6
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -115,6 +115,13 @@ var linkedinModule = {
115
115
  delete updated.linkedin;
116
116
  return updated;
117
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
+ ],
118
125
  readmeBlock(vars) {
119
126
  return [
120
127
  "## LinkedIn module",
@@ -180,6 +187,13 @@ var resumeModule = {
180
187
  render(tpl(`modules/resume/graph/${file}`), vars)
181
188
  ]);
182
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
+ ],
183
197
  readmeBlock(vars) {
184
198
  return [
185
199
  "## Resume module",
@@ -470,6 +484,66 @@ function writeState(root, state) {
470
484
  );
471
485
  }
472
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
+
473
547
  // src/scaffold.ts
474
548
  function writeRaw(targetDir, relativePath, content) {
475
549
  const full = join5(targetDir, relativePath);
@@ -562,7 +636,8 @@ async function scaffold(opts) {
562
636
  editor,
563
637
  modules,
564
638
  liProfileUrl,
565
- contentDir
639
+ contentDir,
640
+ launchTasks = []
566
641
  } = opts;
567
642
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
568
643
  const tempProfile = {
@@ -576,6 +651,7 @@ async function scaffold(opts) {
576
651
  modules,
577
652
  content_dir: contentDir,
578
653
  created: today,
654
+ ...launchTasks.length ? { launch_tasks: launchTasks } : {},
579
655
  ...modules.includes("linkedin") && liProfileUrl ? { linkedin: { profile_url: liProfileUrl } } : {}
580
656
  };
581
657
  const vars = profileToVars(tempProfile, liProfileUrl);
@@ -620,6 +696,16 @@ async function scaffold(opts) {
620
696
  }
621
697
  }
622
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
+ }
623
709
  const baseDirs = ["notes", "skills", "posts"];
624
710
  for (const dir of baseDirs) {
625
711
  touch(targetDir, `${contentDir}/${dir}/.gitkeep`);
@@ -856,6 +942,58 @@ function label(text2) {
856
942
  var MULTISELECT_HINT = `
857
943
  ${chalk.hex("#64748B")("\u2191\u2193 to move \xB7 space to select \xB7 enter to confirm")}`;
858
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
+ }
859
997
  async function main() {
860
998
  printBanner();
861
999
  const cwd = process.cwd();
@@ -962,6 +1100,18 @@ async function runInstall(cwd) {
962
1100
  });
963
1101
  liProfileUrl = typeof url === "string" ? url : "";
964
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
+ }
965
1115
  const slug = slugify(identity.patinaName);
966
1116
  const targetDir = resolve(cwd, slug);
967
1117
  const s = p.spinner();
@@ -984,7 +1134,8 @@ async function runInstall(cwd) {
984
1134
  editor: setup.editor,
985
1135
  modules,
986
1136
  liProfileUrl,
987
- contentDir: "graph"
1137
+ contentDir: "graph",
1138
+ launchTasks
988
1139
  });
989
1140
  s.stop(chalk.green("Done."));
990
1141
  } catch (err) {
@@ -1030,10 +1181,11 @@ async function runUpdate(cwd) {
1030
1181
  p.intro(chalk.hex("#94A3B8")(`Found: ${chalk.bold.white(profile.patina_name || "patina")}`));
1031
1182
  p.note(
1032
1183
  [
1033
- `${chalk.hex("#64748B")("Name:")} ${profile.name}`,
1034
- `${chalk.hex("#64748B")("Title:")} ${profile.title || "\u2014"}`,
1035
- `${chalk.hex("#64748B")("Company:")} ${profile.work?.company_name || "\u2014"}`,
1036
- `${chalk.hex("#64748B")("Modules:")} ${profile.modules?.join(", ") || "none"}`
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}`
1037
1189
  ].join("\n"),
1038
1190
  label("Current profile")
1039
1191
  );
@@ -1042,6 +1194,7 @@ async function runUpdate(cwd) {
1042
1194
  options: [
1043
1195
  { value: "profile", label: "Update personal info" },
1044
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") },
1045
1198
  { value: "validate", label: "Run health check", hint: chalk.hex("#64748B")("check for broken links and excluded items") },
1046
1199
  { value: "nothing", label: "Nothing \u2014 just checking" }
1047
1200
  ]
@@ -1054,6 +1207,8 @@ async function runUpdate(cwd) {
1054
1207
  await runUpdateProfile(cwd, profile);
1055
1208
  } else if (action === "modules") {
1056
1209
  await runUpdateModules(cwd, profile);
1210
+ } else if (action === "launch-tasks") {
1211
+ await runUpdateLaunchTasks(cwd, profile);
1057
1212
  } else if (action === "validate") {
1058
1213
  await runValidate(cwd, profile);
1059
1214
  }
@@ -1125,6 +1280,19 @@ function applyProfileUpdate(cwd, profile, fields, overwrite) {
1125
1280
  }
1126
1281
  }
1127
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
+ }
1128
1296
  for (const [rel, hash] of Object.entries(stored)) {
1129
1297
  if (!(rel in newChecksums)) {
1130
1298
  newChecksums[rel] = hash;
@@ -1345,6 +1513,8 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
1345
1513
  updatedProfile = def.onRemove(updatedProfile);
1346
1514
  }
1347
1515
  }
1516
+ const prunedTasks = pruneLaunchTasks(updatedProfile.launch_tasks, updatedProfile.modules);
1517
+ updatedProfile = { ...updatedProfile, launch_tasks: prunedTasks.length ? prunedTasks : void 0 };
1348
1518
  const finalVars = profileToVars(updatedProfile);
1349
1519
  for (const [rel, content] of baseManagedFiles(finalVars, updatedProfile.editor, cwd)) {
1350
1520
  const result = writeManagedFile(cwd, rel, content, newChecksums);
@@ -1358,11 +1528,67 @@ function applyModuleChanges(cwd, profile, toAdd, toRemove, moduleInputs) {
1358
1528
  }
1359
1529
  }
1360
1530
  }
1531
+ const launchResult = applyLaunchBlock(cwd, updatedProfile.launch_tasks ?? [], updatedProfile.modules, finalVars, newChecksums);
1532
+ keptSections.push(...launchResult.keptSections);
1361
1533
  writeState(cwd, { checksums: newChecksums });
1362
1534
  const finalProfile = stripLegacyChecksums(updatedProfile);
1363
1535
  writeProfile(cwd, finalProfile);
1364
1536
  return { profile: finalProfile, added, skipped: skippedFiles, deleted, kept, keptSections };
1365
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
+ }
1366
1592
  async function runUpdateModules(cwd, profile) {
1367
1593
  const currentModules = profile.modules ?? [];
1368
1594
  const selected = await p.multiselect({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-patina",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Personal professional knowledge graph — setup and management",
5
5
  "type": "module",
6
6
  "bin": {