@grainulation/mill 1.0.0 → 1.0.2

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 (49) hide show
  1. package/CODE_OF_CONDUCT.md +25 -0
  2. package/CONTRIBUTING.md +101 -0
  3. package/README.md +90 -42
  4. package/bin/mill.js +233 -67
  5. package/lib/exporters/csv.js +35 -30
  6. package/lib/exporters/json-ld.js +19 -13
  7. package/lib/exporters/markdown.js +83 -44
  8. package/lib/exporters/pdf.js +15 -15
  9. package/lib/formats/bibtex.mjs +83 -0
  10. package/lib/formats/{changelog.js → changelog.mjs} +27 -26
  11. package/lib/formats/confluence-adf.mjs +312 -0
  12. package/lib/formats/{csv.js → csv.mjs} +41 -37
  13. package/lib/formats/{dot.js → dot.mjs} +45 -34
  14. package/lib/formats/{evidence-matrix.js → evidence-matrix.mjs} +17 -16
  15. package/lib/formats/executive-summary.mjs +178 -0
  16. package/lib/formats/github-issues.mjs +96 -0
  17. package/lib/formats/{graphml.js → graphml.mjs} +45 -32
  18. package/lib/formats/html-report.mjs +228 -0
  19. package/lib/formats/{jira-csv.js → jira-csv.mjs} +30 -29
  20. package/lib/formats/{json-ld.js → json-ld.mjs} +6 -6
  21. package/lib/formats/{markdown.js → markdown.mjs} +53 -36
  22. package/lib/formats/{ndjson.js → ndjson.mjs} +6 -6
  23. package/lib/formats/{obsidian.js → obsidian.mjs} +43 -35
  24. package/lib/formats/{opml.js → opml.mjs} +38 -28
  25. package/lib/formats/ris.mjs +76 -0
  26. package/lib/formats/{rss.js → rss.mjs} +31 -28
  27. package/lib/formats/{sankey.js → sankey.mjs} +16 -15
  28. package/lib/formats/slide-deck.mjs +288 -0
  29. package/lib/formats/sql.mjs +120 -0
  30. package/lib/formats/{static-site.js → static-site.mjs} +64 -52
  31. package/lib/formats/{treemap.js → treemap.mjs} +16 -15
  32. package/lib/formats/typescript-defs.mjs +150 -0
  33. package/lib/formats/{yaml.js → yaml.mjs} +58 -40
  34. package/lib/formats.js +16 -16
  35. package/lib/index.js +5 -5
  36. package/lib/json-ld-common.js +37 -31
  37. package/lib/publishers/clipboard.js +21 -19
  38. package/lib/publishers/static.js +27 -12
  39. package/lib/serve-mcp.js +158 -83
  40. package/lib/server.js +252 -142
  41. package/package.json +6 -3
  42. package/lib/formats/bibtex.js +0 -76
  43. package/lib/formats/executive-summary.js +0 -130
  44. package/lib/formats/github-issues.js +0 -89
  45. package/lib/formats/html-report.js +0 -181
  46. package/lib/formats/ris.js +0 -70
  47. package/lib/formats/slide-deck.js +0 -200
  48. package/lib/formats/sql.js +0 -116
  49. package/lib/formats/typescript-defs.js +0 -147
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('fs');
20
- const path = require('path');
21
- const readline = require('readline');
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 = 'mill';
26
- const SERVER_VERSION = '1.0.0';
27
- const PROTOCOL_VERSION = '2024-11-05';
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, 'formats');
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: '2.0', id, result });
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: '2.0', id, error: { code, message } });
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('.js'));
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('.js', ''),
56
- name: mod.name || file.replace('.js', ''),
57
- extension: mod.extension || '',
58
- mimeType: mod.mimeType || 'text/plain',
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 { status: 'error', message: 'Required field: format (e.g., "csv", "markdown", "json-ld")' };
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 { status: 'error', message: `Unknown format: "${format}". Use mill/formats to list available formats.` };
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 { status: 'error', message: `Format "${format}" does not have a convert function.` };
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, 'compilation.json');
91
- const fallbackFile = path.join(dir, 'claims.json');
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 { status: 'error', message: `No source file found. Tried: ${sourceFile}, ${fallbackFile}` };
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, 'utf8'));
116
+ data = JSON.parse(fs.readFileSync(dataPath, "utf8"));
105
117
  } catch (err) {
106
- return { status: 'error', message: `Failed to parse ${dataPath}: ${err.message}` };
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: 'error', message: `Conversion failed: ${err.message}` };
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 { status: 'ok', message: `Converted to ${format}. Written to ${outPath}`, format: fmt.id, outputPath: outPath, bytes: Buffer.byteLength(result) };
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: 'ok',
160
+ status: "ok",
141
161
  format: fmt.id,
142
162
  mimeType: fmt.mimeType,
143
- output: truncated ? result.slice(0, maxLen) + '\n\n... (truncated, use output parameter to write full file)' : result,
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: 'ok',
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: 'error', message: 'Required field: format' };
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 === 'error') return result;
195
+ if (result.status === "error") return result;
173
196
 
174
197
  const maxLines = lines || 30;
175
- const outputLines = (result.output || '').split('\n');
176
- const preview = outputLines.slice(0, maxLines).join('\n');
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: 'ok',
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: 'mill/convert',
194
- description: '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.',
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: 'object',
220
+ type: "object",
197
221
  properties: {
198
- format: { type: 'string', description: 'Target format ID (e.g., "csv", "markdown", "json-ld", "yaml", "sql", "obsidian")' },
199
- source: { type: 'string', description: 'Source file path (default: ./compilation.json, falls back to ./claims.json)' },
200
- output: { type: 'string', description: 'Output file path. If omitted, returns content inline.' },
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: ['format'],
237
+ required: ["format"],
203
238
  },
204
239
  },
205
240
  {
206
- name: 'mill/formats',
207
- description: 'List all available export formats with descriptions, file extensions, and MIME types.',
208
- inputSchema: { type: 'object', properties: {} },
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: 'mill/preview',
212
- description: 'Preview a format conversion — shows first N lines without writing to disk.',
247
+ name: "mill/preview",
248
+ description:
249
+ "Preview a format conversion — shows first N lines without writing to disk.",
213
250
  inputSchema: {
214
- type: 'object',
251
+ type: "object",
215
252
  properties: {
216
- format: { type: 'string', description: 'Target format ID' },
217
- source: { type: 'string', description: 'Source file path (default: ./compilation.json)' },
218
- lines: { type: 'number', description: 'Number of lines to preview (default: 30)' },
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: ['format'],
263
+ required: ["format"],
221
264
  },
222
265
  },
223
266
  ];
224
267
 
225
268
  const RESOURCES = [
226
269
  {
227
- uri: 'mill://formats',
228
- name: 'Format Catalog',
229
- description: 'All available export formats with descriptions, extensions, and MIME types.',
230
- mimeType: 'application/json',
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 'initialize':
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 'notifications/initialized':
289
+ case "notifications/initialized":
246
290
  return null;
247
291
 
248
- case 'tools/list':
292
+ case "tools/list":
249
293
  return jsonRpcResponse(id, { tools: TOOLS });
250
294
 
251
- case 'tools/call': {
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 'mill/convert': result = await toolConvert(dir, toolArgs); break;
258
- case 'mill/formats': result = await toolFormats(); break;
259
- case 'mill/preview': result = await toolPreview(dir, toolArgs); break;
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: 'text', text: JSON.stringify(result, null, 2) }],
266
- isError: result.status === 'error',
315
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
316
+ isError: result.status === "error",
267
317
  });
268
318
  }
269
319
 
270
- case 'resources/list':
320
+ case "resources/list":
271
321
  return jsonRpcResponse(id, { resources: RESOURCES });
272
322
 
273
- case 'resources/read': {
274
- if (params.uri === 'mill://formats') {
323
+ case "resources/read": {
324
+ if (params.uri === "mill://formats") {
275
325
  const formats = await discoverFormats();
276
- const text = JSON.stringify(formats.map(f => ({
277
- id: f.id, name: f.name, extension: f.extension, mimeType: f.mimeType, description: f.description,
278
- })), null, 2);
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: 'application/json', text }],
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 'ping':
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({ input: process.stdin, terminal: false });
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('line', async (line) => {
372
+ rl.on("line", async (line) => {
312
373
  if (!line.trim()) return;
313
374
  let msg;
314
- try { msg = JSON.parse(line); } catch {
315
- process.stdout.write(jsonRpcError(null, -32700, 'Parse error') + '\n');
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(dir, msg.method, msg.params || {}, msg.id);
320
- if (response !== null) process.stdout.write(response + '\n');
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('close', () => { closing = true; maybeDrain(); });
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(` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`);
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) { startServer(dir); }
411
+ async function run(dir) {
412
+ startServer(dir);
413
+ }
339
414
 
340
415
  module.exports = { startServer, handleRequest, TOOLS, RESOURCES, run };