@hanna84/mcp-writing 3.1.4 → 3.2.1
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 +20 -0
- package/package.json +1 -1
- package/src/tools/styleguide.js +300 -5
- package/src/workflows/workflow-catalogue.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,31 @@ 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.1](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v3.2.0...v3.2.1)
|
|
9
|
+
|
|
10
|
+
- docs(skills): add review comment resolution workflow [`#172`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/172)
|
|
12
|
+
|
|
13
|
+
#### [v3.2.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
|
+
/compare/v3.1.4...v3.2.0)
|
|
15
|
+
|
|
16
|
+
> 3 May 2026
|
|
17
|
+
|
|
18
|
+
- feat(styleguide): publish AI boot files during skill setup [`#171`](https://github.com/hannasdev/mcp-writing.git
|
|
19
|
+
/pull/171)
|
|
20
|
+
- Release 3.2.0 [`ffd8715`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/ffd87159a538ed93ee195becac04ab414e1663ca)
|
|
22
|
+
|
|
7
23
|
#### [v3.1.4](https://github.com/hannasdev/mcp-writing.git
|
|
8
24
|
/compare/v3.1.3...v3.1.4)
|
|
9
25
|
|
|
26
|
+
> 3 May 2026
|
|
27
|
+
|
|
10
28
|
- docs(prd): extract onboarding into shared framework PRD [`#170`](https://github.com/hannasdev/mcp-writing.git
|
|
11
29
|
/pull/170)
|
|
30
|
+
- Release 3.1.4 [`5ed3458`](https://github.com/hannasdev/mcp-writing.git
|
|
31
|
+
/commit/5ed3458f354f89b1e6555924dd2e1c907270cfdc)
|
|
12
32
|
|
|
13
33
|
#### [v3.1.3](https://github.com/hannasdev/mcp-writing.git
|
|
14
34
|
/compare/v3.1.2...v3.1.3)
|
package/package.json
CHANGED
package/src/tools/styleguide.js
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
676
|
-
|
|
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
|
{
|