@grainulation/mill 1.0.0 → 1.0.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/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +101 -0
- package/README.md +90 -42
- package/bin/mill.js +233 -67
- package/lib/exporters/csv.js +35 -30
- package/lib/exporters/json-ld.js +19 -13
- package/lib/exporters/markdown.js +83 -44
- package/lib/exporters/pdf.js +15 -15
- package/lib/formats/bibtex.js +41 -34
- package/lib/formats/changelog.js +27 -26
- package/lib/formats/confluence-adf.js +312 -0
- package/lib/formats/csv.js +41 -37
- package/lib/formats/dot.js +45 -34
- package/lib/formats/evidence-matrix.js +17 -16
- package/lib/formats/executive-summary.js +89 -41
- package/lib/formats/github-issues.js +40 -33
- package/lib/formats/graphml.js +45 -32
- package/lib/formats/html-report.js +110 -63
- package/lib/formats/jira-csv.js +30 -29
- package/lib/formats/json-ld.js +6 -6
- package/lib/formats/markdown.js +53 -36
- package/lib/formats/ndjson.js +6 -6
- package/lib/formats/obsidian.js +43 -35
- package/lib/formats/opml.js +38 -28
- package/lib/formats/ris.js +29 -23
- package/lib/formats/rss.js +31 -28
- package/lib/formats/sankey.js +16 -15
- package/lib/formats/slide-deck.js +145 -57
- package/lib/formats/sql.js +57 -53
- package/lib/formats/static-site.js +64 -52
- package/lib/formats/treemap.js +16 -15
- package/lib/formats/typescript-defs.js +79 -76
- package/lib/formats/yaml.js +58 -40
- package/lib/formats.js +16 -16
- package/lib/index.js +5 -5
- package/lib/json-ld-common.js +37 -31
- package/lib/publishers/clipboard.js +21 -19
- package/lib/publishers/static.js +27 -12
- package/lib/serve-mcp.js +158 -83
- package/lib/server.js +252 -142
- package/package.json +7 -3
package/lib/serve-mcp.js
CHANGED
|
@@ -16,26 +16,26 @@
|
|
|
16
16
|
* claude mcp add mill -- npx @grainulation/mill serve-mcp
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const fs = require(
|
|
20
|
-
const path = require(
|
|
21
|
-
const readline = require(
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const readline = require("readline");
|
|
22
22
|
|
|
23
23
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
-
const SERVER_NAME =
|
|
26
|
-
const SERVER_VERSION =
|
|
27
|
-
const PROTOCOL_VERSION =
|
|
25
|
+
const SERVER_NAME = "mill";
|
|
26
|
+
const SERVER_VERSION = "1.0.0";
|
|
27
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
28
28
|
|
|
29
|
-
const FORMATS_DIR = path.join(__dirname,
|
|
29
|
+
const FORMATS_DIR = path.join(__dirname, "formats");
|
|
30
30
|
|
|
31
31
|
// ─── JSON-RPC helpers ───────────────────────────────────────────────────────
|
|
32
32
|
|
|
33
33
|
function jsonRpcResponse(id, result) {
|
|
34
|
-
return JSON.stringify({ jsonrpc:
|
|
34
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function jsonRpcError(id, code, message) {
|
|
38
|
-
return JSON.stringify({ jsonrpc:
|
|
38
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// ─── Format discovery ───────────────────────────────────────────────────────
|
|
@@ -47,16 +47,16 @@ async function discoverFormats() {
|
|
|
47
47
|
|
|
48
48
|
const formats = [];
|
|
49
49
|
try {
|
|
50
|
-
const files = fs.readdirSync(FORMATS_DIR).filter(f => f.endsWith(
|
|
50
|
+
const files = fs.readdirSync(FORMATS_DIR).filter((f) => f.endsWith(".js"));
|
|
51
51
|
for (const file of files) {
|
|
52
52
|
try {
|
|
53
53
|
const mod = await import(path.join(FORMATS_DIR, file));
|
|
54
54
|
formats.push({
|
|
55
|
-
id: file.replace(
|
|
56
|
-
name: mod.name || file.replace(
|
|
57
|
-
extension: mod.extension ||
|
|
58
|
-
mimeType: mod.mimeType ||
|
|
59
|
-
description: mod.description ||
|
|
55
|
+
id: file.replace(".js", ""),
|
|
56
|
+
name: mod.name || file.replace(".js", ""),
|
|
57
|
+
extension: mod.extension || "",
|
|
58
|
+
mimeType: mod.mimeType || "text/plain",
|
|
59
|
+
description: mod.description || "",
|
|
60
60
|
convert: mod.convert,
|
|
61
61
|
});
|
|
62
62
|
} catch (err) {
|
|
@@ -74,36 +74,51 @@ async function discoverFormats() {
|
|
|
74
74
|
async function toolConvert(dir, args) {
|
|
75
75
|
const { format, source, output } = args;
|
|
76
76
|
if (!format) {
|
|
77
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
status: "error",
|
|
79
|
+
message: 'Required field: format (e.g., "csv", "markdown", "json-ld")',
|
|
80
|
+
};
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const formats = await discoverFormats();
|
|
81
|
-
const fmt = formats.find(f => f.id === format || f.name === format);
|
|
84
|
+
const fmt = formats.find((f) => f.id === format || f.name === format);
|
|
82
85
|
if (!fmt) {
|
|
83
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
status: "error",
|
|
88
|
+
message: `Unknown format: "${format}". Use mill/formats to list available formats.`,
|
|
89
|
+
};
|
|
84
90
|
}
|
|
85
91
|
if (!fmt.convert) {
|
|
86
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
status: "error",
|
|
94
|
+
message: `Format "${format}" does not have a convert function.`,
|
|
95
|
+
};
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
// Resolve source file
|
|
90
|
-
const sourceFile = source || path.join(dir,
|
|
91
|
-
const fallbackFile = path.join(dir,
|
|
99
|
+
const sourceFile = source || path.join(dir, "compilation.json");
|
|
100
|
+
const fallbackFile = path.join(dir, "claims.json");
|
|
92
101
|
let dataPath = sourceFile;
|
|
93
102
|
|
|
94
103
|
if (!fs.existsSync(dataPath)) {
|
|
95
104
|
if (fs.existsSync(fallbackFile)) {
|
|
96
105
|
dataPath = fallbackFile;
|
|
97
106
|
} else {
|
|
98
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
status: "error",
|
|
109
|
+
message: `No source file found. Tried: ${sourceFile}, ${fallbackFile}`,
|
|
110
|
+
};
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
let data;
|
|
103
115
|
try {
|
|
104
|
-
data = JSON.parse(fs.readFileSync(dataPath,
|
|
116
|
+
data = JSON.parse(fs.readFileSync(dataPath, "utf8"));
|
|
105
117
|
} catch (err) {
|
|
106
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
status: "error",
|
|
120
|
+
message: `Failed to parse ${dataPath}: ${err.message}`,
|
|
121
|
+
};
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
// Normalize: compilation.json uses resolved_claims, claims.json uses claims
|
|
@@ -115,13 +130,12 @@ async function toolConvert(dir, args) {
|
|
|
115
130
|
data.meta = data.sprint_meta;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
|
-
|
|
119
133
|
// Run conversion
|
|
120
134
|
let result;
|
|
121
135
|
try {
|
|
122
136
|
result = fmt.convert(data);
|
|
123
137
|
} catch (err) {
|
|
124
|
-
return { status:
|
|
138
|
+
return { status: "error", message: `Conversion failed: ${err.message}` };
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
// Write output if path provided
|
|
@@ -130,17 +144,26 @@ async function toolConvert(dir, args) {
|
|
|
130
144
|
const outDir = path.dirname(outPath);
|
|
131
145
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
132
146
|
fs.writeFileSync(outPath, result);
|
|
133
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
status: "ok",
|
|
149
|
+
message: `Converted to ${format}. Written to ${outPath}`,
|
|
150
|
+
format: fmt.id,
|
|
151
|
+
outputPath: outPath,
|
|
152
|
+
bytes: Buffer.byteLength(result),
|
|
153
|
+
};
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
// Return inline (truncate large outputs)
|
|
137
157
|
const maxLen = 10000;
|
|
138
158
|
const truncated = result.length > maxLen;
|
|
139
159
|
return {
|
|
140
|
-
status:
|
|
160
|
+
status: "ok",
|
|
141
161
|
format: fmt.id,
|
|
142
162
|
mimeType: fmt.mimeType,
|
|
143
|
-
output: truncated
|
|
163
|
+
output: truncated
|
|
164
|
+
? result.slice(0, maxLen) +
|
|
165
|
+
"\n\n... (truncated, use output parameter to write full file)"
|
|
166
|
+
: result,
|
|
144
167
|
bytes: Buffer.byteLength(result),
|
|
145
168
|
truncated,
|
|
146
169
|
};
|
|
@@ -149,9 +172,9 @@ async function toolConvert(dir, args) {
|
|
|
149
172
|
async function toolFormats() {
|
|
150
173
|
const formats = await discoverFormats();
|
|
151
174
|
return {
|
|
152
|
-
status:
|
|
175
|
+
status: "ok",
|
|
153
176
|
count: formats.length,
|
|
154
|
-
formats: formats.map(f => ({
|
|
177
|
+
formats: formats.map((f) => ({
|
|
155
178
|
id: f.id,
|
|
156
179
|
name: f.name,
|
|
157
180
|
extension: f.extension,
|
|
@@ -164,20 +187,20 @@ async function toolFormats() {
|
|
|
164
187
|
async function toolPreview(dir, args) {
|
|
165
188
|
const { format, source, lines } = args;
|
|
166
189
|
if (!format) {
|
|
167
|
-
return { status:
|
|
190
|
+
return { status: "error", message: "Required field: format" };
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
// Run the same conversion but only return first N lines
|
|
171
194
|
const result = await toolConvert(dir, { format, source });
|
|
172
|
-
if (result.status ===
|
|
195
|
+
if (result.status === "error") return result;
|
|
173
196
|
|
|
174
197
|
const maxLines = lines || 30;
|
|
175
|
-
const outputLines = (result.output ||
|
|
176
|
-
const preview = outputLines.slice(0, maxLines).join(
|
|
198
|
+
const outputLines = (result.output || "").split("\n");
|
|
199
|
+
const preview = outputLines.slice(0, maxLines).join("\n");
|
|
177
200
|
const hasMore = outputLines.length > maxLines;
|
|
178
201
|
|
|
179
202
|
return {
|
|
180
|
-
status:
|
|
203
|
+
status: "ok",
|
|
181
204
|
format: result.format,
|
|
182
205
|
preview,
|
|
183
206
|
totalLines: outputLines.length,
|
|
@@ -190,44 +213,65 @@ async function toolPreview(dir, args) {
|
|
|
190
213
|
|
|
191
214
|
const TOOLS = [
|
|
192
215
|
{
|
|
193
|
-
name:
|
|
194
|
-
description:
|
|
216
|
+
name: "mill/convert",
|
|
217
|
+
description:
|
|
218
|
+
"Convert sprint compilation or claims to any supported format (csv, markdown, json-ld, yaml, sql, ndjson, html-report, executive-summary, slide-deck, jira-csv, github-issues, obsidian, graphml, dot, and more). Returns the converted output inline or writes to a file.",
|
|
195
219
|
inputSchema: {
|
|
196
|
-
type:
|
|
220
|
+
type: "object",
|
|
197
221
|
properties: {
|
|
198
|
-
format: {
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
format: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description:
|
|
225
|
+
'Target format ID (e.g., "csv", "markdown", "json-ld", "yaml", "sql", "obsidian")',
|
|
226
|
+
},
|
|
227
|
+
source: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description:
|
|
230
|
+
"Source file path (default: ./compilation.json, falls back to ./claims.json)",
|
|
231
|
+
},
|
|
232
|
+
output: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Output file path. If omitted, returns content inline.",
|
|
235
|
+
},
|
|
201
236
|
},
|
|
202
|
-
required: [
|
|
237
|
+
required: ["format"],
|
|
203
238
|
},
|
|
204
239
|
},
|
|
205
240
|
{
|
|
206
|
-
name:
|
|
207
|
-
description:
|
|
208
|
-
|
|
241
|
+
name: "mill/formats",
|
|
242
|
+
description:
|
|
243
|
+
"List all available export formats with descriptions, file extensions, and MIME types.",
|
|
244
|
+
inputSchema: { type: "object", properties: {} },
|
|
209
245
|
},
|
|
210
246
|
{
|
|
211
|
-
name:
|
|
212
|
-
description:
|
|
247
|
+
name: "mill/preview",
|
|
248
|
+
description:
|
|
249
|
+
"Preview a format conversion — shows first N lines without writing to disk.",
|
|
213
250
|
inputSchema: {
|
|
214
|
-
type:
|
|
251
|
+
type: "object",
|
|
215
252
|
properties: {
|
|
216
|
-
format: { type:
|
|
217
|
-
source: {
|
|
218
|
-
|
|
253
|
+
format: { type: "string", description: "Target format ID" },
|
|
254
|
+
source: {
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Source file path (default: ./compilation.json)",
|
|
257
|
+
},
|
|
258
|
+
lines: {
|
|
259
|
+
type: "number",
|
|
260
|
+
description: "Number of lines to preview (default: 30)",
|
|
261
|
+
},
|
|
219
262
|
},
|
|
220
|
-
required: [
|
|
263
|
+
required: ["format"],
|
|
221
264
|
},
|
|
222
265
|
},
|
|
223
266
|
];
|
|
224
267
|
|
|
225
268
|
const RESOURCES = [
|
|
226
269
|
{
|
|
227
|
-
uri:
|
|
228
|
-
name:
|
|
229
|
-
description:
|
|
230
|
-
|
|
270
|
+
uri: "mill://formats",
|
|
271
|
+
name: "Format Catalog",
|
|
272
|
+
description:
|
|
273
|
+
"All available export formats with descriptions, extensions, and MIME types.",
|
|
274
|
+
mimeType: "application/json",
|
|
231
275
|
},
|
|
232
276
|
];
|
|
233
277
|
|
|
@@ -235,55 +279,69 @@ const RESOURCES = [
|
|
|
235
279
|
|
|
236
280
|
async function handleRequest(dir, method, params, id) {
|
|
237
281
|
switch (method) {
|
|
238
|
-
case
|
|
282
|
+
case "initialize":
|
|
239
283
|
return jsonRpcResponse(id, {
|
|
240
284
|
protocolVersion: PROTOCOL_VERSION,
|
|
241
285
|
capabilities: { tools: {}, resources: {} },
|
|
242
286
|
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
243
287
|
});
|
|
244
288
|
|
|
245
|
-
case
|
|
289
|
+
case "notifications/initialized":
|
|
246
290
|
return null;
|
|
247
291
|
|
|
248
|
-
case
|
|
292
|
+
case "tools/list":
|
|
249
293
|
return jsonRpcResponse(id, { tools: TOOLS });
|
|
250
294
|
|
|
251
|
-
case
|
|
295
|
+
case "tools/call": {
|
|
252
296
|
const toolName = params.name;
|
|
253
297
|
const toolArgs = params.arguments || {};
|
|
254
298
|
let result;
|
|
255
299
|
|
|
256
300
|
switch (toolName) {
|
|
257
|
-
case
|
|
258
|
-
|
|
259
|
-
|
|
301
|
+
case "mill/convert":
|
|
302
|
+
result = await toolConvert(dir, toolArgs);
|
|
303
|
+
break;
|
|
304
|
+
case "mill/formats":
|
|
305
|
+
result = await toolFormats();
|
|
306
|
+
break;
|
|
307
|
+
case "mill/preview":
|
|
308
|
+
result = await toolPreview(dir, toolArgs);
|
|
309
|
+
break;
|
|
260
310
|
default:
|
|
261
311
|
return jsonRpcError(id, -32601, `Unknown tool: ${toolName}`);
|
|
262
312
|
}
|
|
263
313
|
|
|
264
314
|
return jsonRpcResponse(id, {
|
|
265
|
-
content: [{ type:
|
|
266
|
-
isError: result.status ===
|
|
315
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
316
|
+
isError: result.status === "error",
|
|
267
317
|
});
|
|
268
318
|
}
|
|
269
319
|
|
|
270
|
-
case
|
|
320
|
+
case "resources/list":
|
|
271
321
|
return jsonRpcResponse(id, { resources: RESOURCES });
|
|
272
322
|
|
|
273
|
-
case
|
|
274
|
-
if (params.uri ===
|
|
323
|
+
case "resources/read": {
|
|
324
|
+
if (params.uri === "mill://formats") {
|
|
275
325
|
const formats = await discoverFormats();
|
|
276
|
-
const text = JSON.stringify(
|
|
277
|
-
|
|
278
|
-
|
|
326
|
+
const text = JSON.stringify(
|
|
327
|
+
formats.map((f) => ({
|
|
328
|
+
id: f.id,
|
|
329
|
+
name: f.name,
|
|
330
|
+
extension: f.extension,
|
|
331
|
+
mimeType: f.mimeType,
|
|
332
|
+
description: f.description,
|
|
333
|
+
})),
|
|
334
|
+
null,
|
|
335
|
+
2,
|
|
336
|
+
);
|
|
279
337
|
return jsonRpcResponse(id, {
|
|
280
|
-
contents: [{ uri: params.uri, mimeType:
|
|
338
|
+
contents: [{ uri: params.uri, mimeType: "application/json", text }],
|
|
281
339
|
});
|
|
282
340
|
}
|
|
283
341
|
return jsonRpcError(id, -32602, `Unknown resource: ${params.uri}`);
|
|
284
342
|
}
|
|
285
343
|
|
|
286
|
-
case
|
|
344
|
+
case "ping":
|
|
287
345
|
return jsonRpcResponse(id, {});
|
|
288
346
|
|
|
289
347
|
default:
|
|
@@ -295,7 +353,10 @@ async function handleRequest(dir, method, params, id) {
|
|
|
295
353
|
// ─── Stdio transport ────────────────────────────────────────────────────────
|
|
296
354
|
|
|
297
355
|
function startServer(dir) {
|
|
298
|
-
const rl = readline.createInterface({
|
|
356
|
+
const rl = readline.createInterface({
|
|
357
|
+
input: process.stdin,
|
|
358
|
+
terminal: false,
|
|
359
|
+
});
|
|
299
360
|
|
|
300
361
|
if (process.stdout._handle && process.stdout._handle.setBlocking) {
|
|
301
362
|
process.stdout._handle.setBlocking(true);
|
|
@@ -308,25 +369,37 @@ function startServer(dir) {
|
|
|
308
369
|
if (closing && pending === 0) process.exit(0);
|
|
309
370
|
}
|
|
310
371
|
|
|
311
|
-
rl.on(
|
|
372
|
+
rl.on("line", async (line) => {
|
|
312
373
|
if (!line.trim()) return;
|
|
313
374
|
let msg;
|
|
314
|
-
try {
|
|
315
|
-
|
|
375
|
+
try {
|
|
376
|
+
msg = JSON.parse(line);
|
|
377
|
+
} catch {
|
|
378
|
+
process.stdout.write(jsonRpcError(null, -32700, "Parse error") + "\n");
|
|
316
379
|
return;
|
|
317
380
|
}
|
|
318
381
|
pending++;
|
|
319
|
-
const response = await handleRequest(
|
|
320
|
-
|
|
382
|
+
const response = await handleRequest(
|
|
383
|
+
dir,
|
|
384
|
+
msg.method,
|
|
385
|
+
msg.params || {},
|
|
386
|
+
msg.id,
|
|
387
|
+
);
|
|
388
|
+
if (response !== null) process.stdout.write(response + "\n");
|
|
321
389
|
pending--;
|
|
322
390
|
maybeDrain();
|
|
323
391
|
});
|
|
324
392
|
|
|
325
|
-
rl.on(
|
|
393
|
+
rl.on("close", () => {
|
|
394
|
+
closing = true;
|
|
395
|
+
maybeDrain();
|
|
396
|
+
});
|
|
326
397
|
|
|
327
398
|
process.stderr.write(`mill MCP server v${SERVER_VERSION} ready on stdio\n`);
|
|
328
399
|
process.stderr.write(` Formats dir: ${FORMATS_DIR}\n`);
|
|
329
|
-
process.stderr.write(
|
|
400
|
+
process.stderr.write(
|
|
401
|
+
` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`,
|
|
402
|
+
);
|
|
330
403
|
}
|
|
331
404
|
|
|
332
405
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
@@ -335,6 +408,8 @@ if (require.main === module) {
|
|
|
335
408
|
startServer(process.cwd());
|
|
336
409
|
}
|
|
337
410
|
|
|
338
|
-
async function run(dir) {
|
|
411
|
+
async function run(dir) {
|
|
412
|
+
startServer(dir);
|
|
413
|
+
}
|
|
339
414
|
|
|
340
415
|
module.exports = { startServer, handleRequest, TOOLS, RESOURCES, run };
|