@hanna84/mcp-writing 3.1.4 → 3.2.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/CHANGELOG.md CHANGED
@@ -4,11 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v3.2.0](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v3.1.4...v3.2.0)
9
+
10
+ - feat(styleguide): publish AI boot files during skill setup [`#171`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/171)
12
+
7
13
  #### [v3.1.4](https://github.com/hannasdev/mcp-writing.git
8
14
  /compare/v3.1.3...v3.1.4)
9
15
 
16
+ > 3 May 2026
17
+
10
18
  - docs(prd): extract onboarding into shared framework PRD [`#170`](https://github.com/hannasdev/mcp-writing.git
11
19
  /pull/170)
20
+ - Release 3.1.4 [`5ed3458`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/5ed3458f354f89b1e6555924dd2e1c907270cfdc)
12
22
 
13
23
  #### [v3.1.3](https://github.com/hannasdev/mcp-writing.git
14
24
  /compare/v3.1.2...v3.1.3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "homepage": "https://hannasdev.github.io/mcp-writing/",
6
6
  "type": "module",
@@ -24,6 +24,155 @@ import {
24
24
  } from "../styleguide/prose-styleguide-skill.js";
25
25
  import { validateProjectId } from "../sync/importer.js";
26
26
 
27
+ const CLAUDE_BOOT_BASENAME = "CLAUDE.md";
28
+ const COPILOT_BOOT_RELATIVE_PATH = ".github/copilot-instructions.md";
29
+ const PROSE_STYLEGUIDE_IMPORT_LINE = "@skills/prose-styleguide/SKILL.md";
30
+ const COPILOT_STYLEGUIDE_MARKER_START = "<!-- MCP-WRITING:PROSE-STYLEGUIDE START -->";
31
+ const COPILOT_STYLEGUIDE_MARKER_END = "<!-- MCP-WRITING:PROSE-STYLEGUIDE END -->";
32
+
33
+ function buildSafeMarkdownFence(text) {
34
+ const runs = text.match(/`+/g) ?? [];
35
+ const longest = runs.reduce((max, run) => Math.max(max, run.length), 0);
36
+ return "`".repeat(Math.max(3, longest + 1));
37
+ }
38
+
39
+ function hasStandaloneClaudeImportLine(content) {
40
+ const lines = content.split(/\r?\n/);
41
+ let inFence = false;
42
+ let fenceChar = "";
43
+ let fenceLength = 0;
44
+
45
+ for (const line of lines) {
46
+ const trimmed = line.trim();
47
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
48
+
49
+ if (fenceMatch) {
50
+ const nextFenceChar = fenceMatch[1][0];
51
+ const nextFenceLength = fenceMatch[1].length;
52
+
53
+ if (!inFence) {
54
+ inFence = true;
55
+ fenceChar = nextFenceChar;
56
+ fenceLength = nextFenceLength;
57
+ continue;
58
+ }
59
+
60
+ if (nextFenceChar === fenceChar && nextFenceLength >= fenceLength) {
61
+ inFence = false;
62
+ fenceChar = "";
63
+ fenceLength = 0;
64
+ }
65
+ continue;
66
+ }
67
+
68
+ if (!inFence && trimmed === PROSE_STYLEGUIDE_IMPORT_LINE) {
69
+ return true;
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ function upsertClaudeBootFile({ syncDir, overwrite = false }) {
77
+ const targetPath = path.join(syncDir, CLAUDE_BOOT_BASENAME);
78
+ const exists = fs.existsSync(targetPath);
79
+
80
+ if (exists && !overwrite) {
81
+ const existing = fs.readFileSync(targetPath, "utf8");
82
+ if (hasStandaloneClaudeImportLine(existing)) {
83
+ return { path: path.resolve(targetPath), status: "unchanged" };
84
+ }
85
+
86
+ const suffix = existing.endsWith("\n") ? "\n" : "\n\n";
87
+ fs.writeFileSync(targetPath, `${existing}${suffix}${PROSE_STYLEGUIDE_IMPORT_LINE}\n`, "utf8");
88
+ return { path: path.resolve(targetPath), status: "updated" };
89
+ }
90
+
91
+ const content = [
92
+ "# Writing Assistant Boot File",
93
+ "",
94
+ "Load the prose styleguide by default:",
95
+ "",
96
+ PROSE_STYLEGUIDE_IMPORT_LINE,
97
+ "",
98
+ ].join("\n");
99
+
100
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
101
+ fs.writeFileSync(targetPath, content, "utf8");
102
+ return { path: path.resolve(targetPath), status: exists ? "overwritten" : "created" };
103
+ }
104
+
105
+ function renderCopilotStyleguideBlock({ skillMarkdown }) {
106
+ const trimmedSkillMarkdown = skillMarkdown.trim();
107
+ const fence = buildSafeMarkdownFence(trimmedSkillMarkdown);
108
+
109
+ return [
110
+ COPILOT_STYLEGUIDE_MARKER_START,
111
+ "## Prose Styleguide",
112
+ "",
113
+ "This section is generated by mcp-writing setup. `skills/prose-styleguide/SKILL.md` is the canonical file.",
114
+ "",
115
+ "Since Copilot does not support imports, this is an inline snapshot:",
116
+ "",
117
+ `${fence}markdown`,
118
+ trimmedSkillMarkdown,
119
+ fence,
120
+ COPILOT_STYLEGUIDE_MARKER_END,
121
+ "",
122
+ ].join("\n");
123
+ }
124
+
125
+ function upsertCopilotBootFile({ syncDir, skillMarkdown, overwrite = false }) {
126
+ const targetPath = path.join(syncDir, COPILOT_BOOT_RELATIVE_PATH);
127
+ const exists = fs.existsSync(targetPath);
128
+ const styleguideBlock = renderCopilotStyleguideBlock({ skillMarkdown });
129
+
130
+ if (!exists || overwrite) {
131
+ const content = [
132
+ "# Copilot Instructions",
133
+ "",
134
+ styleguideBlock,
135
+ ].join("\n");
136
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
137
+ fs.writeFileSync(targetPath, content, "utf8");
138
+ return { path: path.resolve(targetPath), status: exists ? "overwritten" : "created" };
139
+ }
140
+
141
+ const existing = fs.readFileSync(targetPath, "utf8");
142
+ const markerRegex = new RegExp(
143
+ `${COPILOT_STYLEGUIDE_MARKER_START}[\\s\\S]*?${COPILOT_STYLEGUIDE_MARKER_END}`,
144
+ "m"
145
+ );
146
+
147
+ const next = markerRegex.test(existing)
148
+ ? existing.replace(markerRegex, styleguideBlock.trimEnd())
149
+ : `${existing}${existing.endsWith("\n") ? "\n" : "\n\n"}${styleguideBlock}`;
150
+
151
+ fs.writeFileSync(targetPath, next, "utf8");
152
+ return { path: path.resolve(targetPath), status: markerRegex.test(existing) ? "updated" : "appended" };
153
+ }
154
+
155
+ function validateParentDirectory(targetPath, label) {
156
+ const parent = path.dirname(targetPath);
157
+ if (fs.existsSync(parent)) {
158
+ const parentStat = fs.statSync(parent);
159
+ if (!parentStat.isDirectory()) {
160
+ return {
161
+ ok: false,
162
+ error: {
163
+ code: "INVALID_TARGET_PARENT",
164
+ message: `${label} parent path must be a directory: ${path.resolve(parent)}`,
165
+ details: {
166
+ target_path: path.resolve(targetPath),
167
+ parent_path: path.resolve(parent),
168
+ },
169
+ },
170
+ };
171
+ }
172
+ }
173
+ return { ok: true };
174
+ }
175
+
27
176
  export function registerStyleguideTools(s, {
28
177
  db,
29
178
  SYNC_DIR,
@@ -603,19 +752,32 @@ export function registerStyleguideTools(s, {
603
752
 
604
753
  s.tool(
605
754
  "setup_prose_styleguide_skill",
606
- "Generate skills/prose-styleguide/SKILL.md from the resolved prose styleguide config and universal craft rules.",
755
+ "Generate skills/prose-styleguide/SKILL.md from the resolved prose styleguide config and universal craft rules. Optionally publish AI boot files (CLAUDE.md and .github/copilot-instructions.md) when using sync-root config scope.",
607
756
  {
608
- project_id: z.string().optional().describe("Optional project ID for scoped config resolution (e.g. 'the-lamb' or 'universe-1/book-1')."),
757
+ project_id: z.string().optional().describe("Project-scoped skill generation is unsupported because this tool writes a shared sync-root skills/prose-styleguide/SKILL.md file."),
609
758
  overwrite: z.boolean().optional().describe("If true, replaces an existing skills/prose-styleguide/SKILL.md file."),
759
+ publish_boot_files: z.boolean().optional().describe("If true, also upserts CLAUDE.md and .github/copilot-instructions.md at sync root. Defaults to true."),
760
+ boot_files_overwrite: z.boolean().optional().describe("If true, rewrites existing boot files instead of in-place updates."),
610
761
  },
611
- async ({ project_id, overwrite = false }) => {
762
+ async ({ project_id, overwrite = false, publish_boot_files, boot_files_overwrite = false }) => {
612
763
  if (project_id !== undefined) {
613
764
  const projectIdCheck = validateProjectId(project_id);
614
765
  if (!projectIdCheck.ok) {
615
766
  return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
616
767
  }
768
+
769
+ return errorResponse(
770
+ "PROJECT_SCOPED_SKILL_UNSUPPORTED",
771
+ "setup_prose_styleguide_skill writes a shared sync-root skills/prose-styleguide/SKILL.md file and cannot run with project_id.",
772
+ {
773
+ project_id,
774
+ next_step: "Run setup_prose_styleguide_skill without project_id for workspace-wide skill setup.",
775
+ }
776
+ );
617
777
  }
618
778
 
779
+ const shouldPublishBootFiles = publish_boot_files ?? true;
780
+
619
781
  if (!SYNC_DIR_WRITABLE) {
620
782
  return errorResponse(
621
783
  "SYNC_DIR_NOT_WRITABLE",
@@ -672,8 +834,139 @@ export function registerStyleguideTools(s, {
672
834
  return errorResponse(generated.error.code, generated.error.message);
673
835
  }
674
836
 
675
- fs.mkdirSync(path.dirname(skillPath), { recursive: true });
676
- fs.writeFileSync(skillPath, generated.markdown, "utf8");
837
+ const skillPathCheck = validateParentDirectory(skillPath, "Skill file");
838
+ if (!skillPathCheck.ok) {
839
+ return errorResponse(
840
+ skillPathCheck.error.code,
841
+ skillPathCheck.error.message,
842
+ skillPathCheck.error.details
843
+ );
844
+ }
845
+
846
+ let bootFiles = [];
847
+ const claudePath = path.join(SYNC_DIR, CLAUDE_BOOT_BASENAME);
848
+ const copilotPath = path.join(SYNC_DIR, COPILOT_BOOT_RELATIVE_PATH);
849
+
850
+ if (shouldPublishBootFiles) {
851
+ if (!isPathCandidateInsideSyncDir(claudePath) || !isPathCandidateInsideSyncDir(copilotPath)) {
852
+ return errorResponse(
853
+ "INVALID_BOOT_FILE_PATH",
854
+ "Resolved AI boot file paths must be inside WRITING_SYNC_DIR.",
855
+ {
856
+ claude_path: path.resolve(claudePath),
857
+ copilot_path: path.resolve(copilotPath),
858
+ sync_dir: SYNC_DIR_ABS,
859
+ }
860
+ );
861
+ }
862
+
863
+ const claudePathCheck = validateParentDirectory(claudePath, "CLAUDE boot file");
864
+ if (!claudePathCheck.ok) {
865
+ return errorResponse(
866
+ claudePathCheck.error.code,
867
+ claudePathCheck.error.message,
868
+ claudePathCheck.error.details
869
+ );
870
+ }
871
+ const copilotPathCheck = validateParentDirectory(copilotPath, "Copilot boot file");
872
+ if (!copilotPathCheck.ok) {
873
+ return errorResponse(
874
+ copilotPathCheck.error.code,
875
+ copilotPathCheck.error.message,
876
+ copilotPathCheck.error.details
877
+ );
878
+ }
879
+
880
+ }
881
+
882
+ const mutationTargets = shouldPublishBootFiles
883
+ ? [skillPath, claudePath, copilotPath]
884
+ : [skillPath];
885
+ const backups = mutationTargets.map((targetPath) => {
886
+ if (!fs.existsSync(targetPath)) {
887
+ return { targetPath, existed: false, content: null };
888
+ }
889
+ return {
890
+ targetPath,
891
+ existed: true,
892
+ content: fs.readFileSync(targetPath, "utf8"),
893
+ };
894
+ });
895
+
896
+ function rollbackMutations() {
897
+ for (let i = backups.length - 1; i >= 0; i -= 1) {
898
+ const backup = backups[i];
899
+ if (backup.existed) {
900
+ fs.mkdirSync(path.dirname(backup.targetPath), { recursive: true });
901
+ fs.writeFileSync(backup.targetPath, backup.content, "utf8");
902
+ continue;
903
+ }
904
+ fs.rmSync(backup.targetPath, { force: true });
905
+ }
906
+ }
907
+
908
+ let failedStep = "";
909
+
910
+ try {
911
+ if (shouldPublishBootFiles) {
912
+ failedStep = "claude_boot_file";
913
+ const claudeResult = upsertClaudeBootFile({
914
+ syncDir: SYNC_DIR,
915
+ overwrite: boot_files_overwrite,
916
+ });
917
+
918
+ failedStep = "copilot_boot_file";
919
+ const copilotResult = upsertCopilotBootFile({
920
+ syncDir: SYNC_DIR,
921
+ skillMarkdown: generated.markdown,
922
+ overwrite: boot_files_overwrite,
923
+ });
924
+ bootFiles = [
925
+ { type: "claude", ...claudeResult },
926
+ { type: "copilot", ...copilotResult },
927
+ ];
928
+ }
929
+
930
+ failedStep = "skill_file";
931
+ fs.mkdirSync(path.dirname(skillPath), { recursive: true });
932
+ fs.writeFileSync(skillPath, generated.markdown, "utf8");
933
+ } catch (error) {
934
+ try {
935
+ rollbackMutations();
936
+ } catch (rollbackError) {
937
+ return errorResponse(
938
+ "SETUP_WRITE_ROLLBACK_FAILED",
939
+ "Failed to publish prose styleguide setup files and rollback was not clean.",
940
+ {
941
+ failed_step: failedStep,
942
+ reason: String(error?.message ?? error),
943
+ rollback_reason: String(rollbackError?.message ?? rollbackError),
944
+ target_path: path.resolve(skillPath),
945
+ }
946
+ );
947
+ }
948
+
949
+ if (failedStep === "skill_file") {
950
+ return errorResponse(
951
+ "STYLEGUIDE_SKILL_WRITE_FAILED",
952
+ "Failed to write skills/prose-styleguide/SKILL.md.",
953
+ {
954
+ failed_step: failedStep,
955
+ reason: String(error?.message ?? error),
956
+ target_path: path.resolve(skillPath),
957
+ }
958
+ );
959
+ }
960
+
961
+ return errorResponse(
962
+ "BOOT_FILE_WRITE_FAILED",
963
+ "Failed to publish AI boot files while setting up prose styleguide skill.",
964
+ {
965
+ failed_step: failedStep,
966
+ reason: String(error?.message ?? error),
967
+ }
968
+ );
969
+ }
677
970
 
678
971
  return jsonResponse({
679
972
  ok: true,
@@ -681,6 +974,8 @@ export function registerStyleguideTools(s, {
681
974
  project_id: project_id ?? null,
682
975
  injected_rules: generated.injected_rules,
683
976
  source_count: resolved.sources.length,
977
+ boot_files: bootFiles,
978
+ warnings: [],
684
979
  });
685
980
  }
686
981
  );
@@ -101,6 +101,7 @@ export const WORKFLOW_CATALOGUE = [
101
101
  { tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
102
102
  { tool: "setup_prose_styleguide_config", note: "Only if ALL context.styleguide_exists fields are false — a config at any scope is sufficient. Create at project_root scope (requires project_id and language e.g. 'english_us'), or sync_root if no project_id is known." },
103
103
  { tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
104
+ { tool: "setup_prose_styleguide_skill", note: "Run only for sync-root setup. This tool writes shared skills/prose-styleguide/SKILL.md (and optional boot files), so do not run it after project_root setup." },
104
105
  ],
105
106
  },
106
107
  {