@grainulation/wheat 1.0.2 → 1.0.4

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 (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -31
  3. package/bin/wheat.js +47 -36
  4. package/compiler/detect-sprints.js +126 -92
  5. package/compiler/generate-manifest.js +116 -69
  6. package/compiler/wheat-compiler.js +789 -468
  7. package/lib/compiler.js +11 -6
  8. package/lib/connect.js +273 -134
  9. package/lib/disconnect.js +61 -40
  10. package/lib/guard.js +20 -17
  11. package/lib/index.js +8 -8
  12. package/lib/init.js +217 -142
  13. package/lib/install-prompt.js +26 -26
  14. package/lib/load-claims.js +88 -0
  15. package/lib/quickstart.js +225 -111
  16. package/lib/serve-mcp.js +495 -180
  17. package/lib/server.js +198 -111
  18. package/lib/stats.js +65 -39
  19. package/lib/status.js +65 -34
  20. package/lib/update.js +13 -11
  21. package/package.json +8 -4
  22. package/templates/claude.md +31 -17
  23. package/templates/commands/blind-spot.md +9 -2
  24. package/templates/commands/brief.md +11 -1
  25. package/templates/commands/calibrate.md +3 -1
  26. package/templates/commands/challenge.md +4 -1
  27. package/templates/commands/connect.md +12 -1
  28. package/templates/commands/evaluate.md +4 -0
  29. package/templates/commands/feedback.md +3 -1
  30. package/templates/commands/handoff.md +11 -7
  31. package/templates/commands/init.md +4 -1
  32. package/templates/commands/merge.md +4 -1
  33. package/templates/commands/next.md +1 -0
  34. package/templates/commands/present.md +3 -0
  35. package/templates/commands/prototype.md +2 -0
  36. package/templates/commands/pull.md +103 -0
  37. package/templates/commands/replay.md +8 -0
  38. package/templates/commands/research.md +1 -0
  39. package/templates/commands/resolve.md +4 -1
  40. package/templates/commands/status.md +4 -0
  41. package/templates/commands/sync.md +94 -0
  42. package/templates/commands/witness.md +6 -2
package/lib/serve-mcp.js CHANGED
@@ -11,11 +11,14 @@
11
11
  * wheat/search -- Query claims by topic, type, evidence tier
12
12
  * wheat/status -- Return compilation summary
13
13
  * wheat/init -- Initialize a new research sprint
14
+ * wheat/deepwiki -- Fetch DeepWiki docs for a public GitHub repo
15
+ * wheat/sync-log -- View sync/publish history
14
16
  *
15
17
  * Resources:
16
18
  * wheat://compilation -- Current compilation.json
17
19
  * wheat://claims -- Current claims.json
18
20
  * wheat://brief -- Latest brief (output/brief.html)
21
+ * wheat://sync-log -- Sync/publish history
19
22
  *
20
23
  * Protocol: MCP over stdio (JSON-RPC 2.0, newline-delimited)
21
24
  *
@@ -25,42 +28,54 @@
25
28
  * Zero npm dependencies.
26
29
  */
27
30
 
28
- import fs from 'node:fs';
29
- import path from 'node:path';
30
- import readline from 'node:readline';
31
- import { execFileSync } from 'node:child_process';
32
- import { fileURLToPath } from 'node:url';
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+ import readline from "node:readline";
34
+ import http from "node:http";
35
+ import https from "node:https";
36
+ import { execFileSync } from "node:child_process";
37
+ import { fileURLToPath } from "node:url";
38
+ import { loadClaims } from "./load-claims.js";
33
39
 
34
40
  const __filename = fileURLToPath(import.meta.url);
35
41
  const __dirname = path.dirname(__filename);
36
42
 
37
43
  // --- Constants ---------------------------------------------------------------
38
44
 
39
- const SERVER_NAME = 'wheat';
40
- const SERVER_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
41
- const PROTOCOL_VERSION = '2024-11-05';
42
-
43
- const VALID_TYPES = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
44
- const VALID_EVIDENCE = ['stated', 'web', 'documented', 'tested', 'production'];
45
+ const SERVER_NAME = "wheat";
46
+ const SERVER_VERSION = JSON.parse(
47
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
48
+ ).version;
49
+ const PROTOCOL_VERSION = "2024-11-05";
50
+
51
+ const VALID_TYPES = [
52
+ "constraint",
53
+ "factual",
54
+ "estimate",
55
+ "risk",
56
+ "recommendation",
57
+ "feedback",
58
+ ];
59
+ const VALID_EVIDENCE = ["stated", "web", "documented", "tested", "production"];
45
60
 
46
61
  // --- JSON-RPC helpers --------------------------------------------------------
47
62
 
48
63
  function jsonRpcResponse(id, result) {
49
- return JSON.stringify({ jsonrpc: '2.0', id, result });
64
+ return JSON.stringify({ jsonrpc: "2.0", id, result });
50
65
  }
51
66
 
52
67
  function jsonRpcError(id, code, message) {
53
- return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
68
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
54
69
  }
55
70
 
56
71
  // --- Paths -------------------------------------------------------------------
57
72
 
58
73
  function resolvePaths(dir) {
59
74
  return {
60
- claims: path.join(dir, 'claims.json'),
61
- compilation: path.join(dir, 'compilation.json'),
62
- brief: path.join(dir, 'output', 'brief.html'),
63
- compiler: path.join(dir, 'wheat-compiler.js'),
75
+ claims: path.join(dir, "claims.json"),
76
+ compilation: path.join(dir, "compilation.json"),
77
+ brief: path.join(dir, "output", "brief.html"),
78
+ compiler: path.join(dir, "wheat-compiler.js"),
64
79
  };
65
80
  }
66
81
 
@@ -70,55 +85,90 @@ function toolCompile(dir) {
70
85
  const paths = resolvePaths(dir);
71
86
 
72
87
  if (!fs.existsSync(paths.claims)) {
73
- return { status: 'error', message: 'No claims.json found. Run wheat init first.' };
88
+ return {
89
+ status: "error",
90
+ message: "No claims.json found. Run wheat init first.",
91
+ };
74
92
  }
75
93
 
76
94
  // Find compiler -- check local dir, then package compiler/
77
95
  let compilerPath = paths.compiler;
78
96
  if (!fs.existsSync(compilerPath)) {
79
- compilerPath = path.join(__dirname, '..', 'compiler', 'wheat-compiler.js');
97
+ compilerPath = path.join(__dirname, "..", "compiler", "wheat-compiler.js");
80
98
  }
81
99
  if (!fs.existsSync(compilerPath)) {
82
- return { status: 'error', message: 'Compiler not found. Ensure wheat-compiler.js is in the project root.' };
100
+ return {
101
+ status: "error",
102
+ message:
103
+ "Compiler not found. Ensure wheat-compiler.js is in the project root.",
104
+ };
83
105
  }
84
106
 
85
107
  try {
86
- const output = execFileSync('node', [compilerPath, '--summary', '--dir', dir], {
87
- cwd: dir,
88
- encoding: 'utf8',
89
- timeout: 30000,
90
- stdio: ['pipe', 'pipe', 'pipe'],
91
- });
92
- return { status: 'ok', output: output.trim() };
108
+ const output = execFileSync(
109
+ "node",
110
+ [compilerPath, "--summary", "--dir", dir],
111
+ {
112
+ cwd: dir,
113
+ encoding: "utf8",
114
+ timeout: 30000,
115
+ stdio: ["pipe", "pipe", "pipe"],
116
+ }
117
+ );
118
+ return { status: "ok", output: output.trim() };
93
119
  } catch (err) {
94
- return { status: 'error', output: (err.stdout || '').trim(), error: (err.stderr || '').trim() };
120
+ return {
121
+ status: "error",
122
+ output: (err.stdout || "").trim(),
123
+ error: (err.stderr || "").trim(),
124
+ };
95
125
  }
96
126
  }
97
127
 
98
128
  function toolAddClaim(dir, args) {
99
129
  const paths = resolvePaths(dir);
100
130
  if (!fs.existsSync(paths.claims)) {
101
- return { status: 'error', message: 'No claims.json found. Run wheat init first.' };
131
+ return {
132
+ status: "error",
133
+ message: "No claims.json found. Run wheat init first.",
134
+ };
102
135
  }
103
136
 
104
137
  const { id, type, topic, content, evidence, tags } = args;
105
138
 
106
139
  // Validate
107
140
  if (!id || !type || !topic || !content) {
108
- return { status: 'error', message: 'Required fields: id, type, topic, content' };
141
+ return {
142
+ status: "error",
143
+ message: "Required fields: id, type, topic, content",
144
+ };
109
145
  }
110
146
  if (!VALID_TYPES.includes(type)) {
111
- return { status: 'error', message: `Invalid type "${type}". Valid: ${VALID_TYPES.join(', ')}` };
147
+ return {
148
+ status: "error",
149
+ message: `Invalid type "${type}". Valid: ${VALID_TYPES.join(", ")}`,
150
+ };
112
151
  }
113
152
  if (evidence && !VALID_EVIDENCE.includes(evidence)) {
114
- return { status: 'error', message: `Invalid evidence "${evidence}". Valid: ${VALID_EVIDENCE.join(', ')}` };
153
+ return {
154
+ status: "error",
155
+ message: `Invalid evidence "${evidence}". Valid: ${VALID_EVIDENCE.join(
156
+ ", "
157
+ )}`,
158
+ };
115
159
  }
116
160
 
117
- const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
161
+ const { data, errors: loadErrors } = loadClaims(dir);
162
+ if (!data) {
163
+ return {
164
+ status: "error",
165
+ message: loadErrors[0]?.message || "Failed to load claims.json",
166
+ };
167
+ }
118
168
 
119
169
  // Check for duplicate ID
120
- if (data.claims.some(c => c.id === id)) {
121
- return { status: 'error', message: `Claim ID "${id}" already exists.` };
170
+ if (data.claims.some((c) => c.id === id)) {
171
+ return { status: "error", message: `Claim ID "${id}" already exists.` };
122
172
  }
123
173
 
124
174
  const claim = {
@@ -126,10 +176,10 @@ function toolAddClaim(dir, args) {
126
176
  type,
127
177
  topic,
128
178
  content,
129
- source: { origin: 'mcp', artifact: null, connector: null },
130
- evidence: evidence || 'stated',
131
- status: 'active',
132
- phase_added: data.meta.phase || 'research',
179
+ source: { origin: "mcp", artifact: null, connector: null },
180
+ evidence: evidence || "stated",
181
+ status: "active",
182
+ phase_added: data.meta.phase || "research",
133
183
  timestamp: new Date().toISOString(),
134
184
  conflicts_with: [],
135
185
  resolved_by: null,
@@ -137,109 +187,131 @@ function toolAddClaim(dir, args) {
137
187
  };
138
188
 
139
189
  data.claims.push(claim);
140
- fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + '\n');
190
+ fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + "\n");
141
191
 
142
- return { status: 'ok', message: `Claim ${id} added.`, claim };
192
+ return { status: "ok", message: `Claim ${id} added.`, claim };
143
193
  }
144
194
 
145
195
  function toolResolve(dir, args) {
146
196
  const paths = resolvePaths(dir);
147
197
  if (!fs.existsSync(paths.claims)) {
148
- return { status: 'error', message: 'No claims.json found.' };
198
+ return { status: "error", message: "No claims.json found." };
149
199
  }
150
200
 
151
201
  const { winner, loser, reason } = args;
152
202
  if (!winner || !loser) {
153
- return { status: 'error', message: 'Required fields: winner, loser' };
203
+ return { status: "error", message: "Required fields: winner, loser" };
154
204
  }
155
205
 
156
- const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
157
- const winnerClaim = data.claims.find(c => c.id === winner);
158
- const loserClaim = data.claims.find(c => c.id === loser);
206
+ const { data, errors: loadErrors } = loadClaims(dir);
207
+ if (!data) {
208
+ return {
209
+ status: "error",
210
+ message: loadErrors[0]?.message || "Failed to load claims.json",
211
+ };
212
+ }
213
+ const winnerClaim = data.claims.find((c) => c.id === winner);
214
+ const loserClaim = data.claims.find((c) => c.id === loser);
159
215
 
160
- if (!winnerClaim) return { status: 'error', message: `Claim "${winner}" not found.` };
161
- if (!loserClaim) return { status: 'error', message: `Claim "${loser}" not found.` };
216
+ if (!winnerClaim)
217
+ return { status: "error", message: `Claim "${winner}" not found.` };
218
+ if (!loserClaim)
219
+ return { status: "error", message: `Claim "${loser}" not found.` };
162
220
 
163
221
  // Clear conflict references
164
- winnerClaim.conflicts_with = (winnerClaim.conflicts_with || []).filter(cid => cid !== loser);
222
+ winnerClaim.conflicts_with = (winnerClaim.conflicts_with || []).filter(
223
+ (cid) => cid !== loser
224
+ );
165
225
  loserClaim.conflicts_with = [];
166
- loserClaim.status = 'superseded';
226
+ loserClaim.status = "superseded";
167
227
  loserClaim.resolved_by = winner;
168
228
 
169
- fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + '\n');
229
+ fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + "\n");
170
230
 
171
231
  return {
172
- status: 'ok',
173
- message: `Resolved: ${winner} wins over ${loser}${reason ? ` (${reason})` : ''}.`,
232
+ status: "ok",
233
+ message: `Resolved: ${winner} wins over ${loser}${
234
+ reason ? ` (${reason})` : ""
235
+ }.`,
174
236
  winner: winnerClaim.id,
175
237
  loser: loserClaim.id,
176
238
  };
177
239
  }
178
240
 
179
241
  function toolSearch(dir, args) {
180
- const paths = resolvePaths(dir);
181
- if (!fs.existsSync(paths.claims)) {
182
- return { status: 'error', message: 'No claims.json found.' };
242
+ const { data, errors: loadErrors } = loadClaims(dir);
243
+ if (!data) {
244
+ return {
245
+ status: "error",
246
+ message: loadErrors[0]?.message || "No claims.json found.",
247
+ };
183
248
  }
184
-
185
- const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
186
- let results = data.claims.filter(c => c.status === 'active');
249
+ let results = data.claims.filter((c) => c.status === "active");
187
250
 
188
251
  if (args.topic) {
189
- results = results.filter(c => c.topic === args.topic);
252
+ results = results.filter((c) => c.topic === args.topic);
190
253
  }
191
254
  if (args.type) {
192
- results = results.filter(c => c.type === args.type);
255
+ results = results.filter((c) => c.type === args.type);
193
256
  }
194
257
  if (args.evidence) {
195
- results = results.filter(c => c.evidence === args.evidence);
258
+ results = results.filter((c) => c.evidence === args.evidence);
196
259
  }
197
260
  if (args.query) {
198
261
  const q = args.query.toLowerCase();
199
- results = results.filter(c => c.content.toLowerCase().includes(q));
262
+ results = results.filter((c) => c.content.toLowerCase().includes(q));
200
263
  }
201
264
 
202
265
  return {
203
- status: 'ok',
266
+ status: "ok",
204
267
  count: results.length,
205
- claims: results.map(c => ({
268
+ claims: results.map((c) => ({
206
269
  id: c.id,
207
270
  type: c.type,
208
271
  topic: c.topic,
209
272
  evidence: c.evidence,
210
- content: c.content.slice(0, 200) + (c.content.length > 200 ? '...' : ''),
273
+ content: c.content.slice(0, 200) + (c.content.length > 200 ? "..." : ""),
211
274
  })),
212
275
  };
213
276
  }
214
277
 
215
278
  function toolStatus(dir) {
216
- const paths = resolvePaths(dir);
217
-
218
- if (!fs.existsSync(paths.claims)) {
219
- return { status: 'no_sprint', message: 'No claims.json found. Run wheat init to start a sprint.' };
279
+ const { data, errors: loadErrors } = loadClaims(dir);
280
+ if (!data) {
281
+ return {
282
+ status: "no_sprint",
283
+ message:
284
+ loadErrors[0]?.message ||
285
+ "No claims.json found. Run wheat init to start a sprint.",
286
+ };
220
287
  }
221
288
 
222
- const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
223
289
  const claims = data.claims || [];
224
- const active = claims.filter(c => c.status === 'active');
225
- const conflicted = claims.filter(c =>
226
- c.status === 'conflicted' ||
227
- (c.conflicts_with && c.conflicts_with.length > 0 && c.status === 'active')
290
+ const active = claims.filter((c) => c.status === "active");
291
+ const conflicted = claims.filter(
292
+ (c) =>
293
+ c.status === "conflicted" ||
294
+ (c.conflicts_with && c.conflicts_with.length > 0 && c.status === "active")
228
295
  );
229
- const topics = [...new Set(active.map(c => c.topic))];
296
+ const topics = [...new Set(active.map((c) => c.topic))];
230
297
  const types = {};
231
- active.forEach(c => { types[c.type] = (types[c.type] || 0) + 1; });
298
+ active.forEach((c) => {
299
+ types[c.type] = (types[c.type] || 0) + 1;
300
+ });
232
301
 
233
- let compilationStatus = 'unknown';
302
+ const paths = resolvePaths(dir);
303
+ let compilationStatus = "unknown";
234
304
  if (fs.existsSync(paths.compilation)) {
235
305
  try {
236
- const comp = JSON.parse(fs.readFileSync(paths.compilation, 'utf8'));
237
- compilationStatus = comp.status || 'unknown';
238
- } catch { /* ignore */ }
306
+ const comp = JSON.parse(fs.readFileSync(paths.compilation, "utf8"));
307
+ compilationStatus = comp.status || "unknown";
308
+ } catch {
309
+ /* ignore */
310
+ }
239
311
  }
240
312
 
241
313
  return {
242
- status: 'ok',
314
+ status: "ok",
243
315
  question: data.meta.question,
244
316
  phase: data.meta.phase,
245
317
  total_claims: claims.length,
@@ -255,35 +327,169 @@ function toolInit(dir, args) {
255
327
  const paths = resolvePaths(dir);
256
328
 
257
329
  if (!args.question) {
258
- return { status: 'error', message: 'Required field: question' };
330
+ return { status: "error", message: "Required field: question" };
259
331
  }
260
332
 
261
333
  if (fs.existsSync(paths.claims) && !args.force) {
262
- return { status: 'error', message: 'Sprint already exists (claims.json found). Pass force: true to reinitialize.' };
334
+ return {
335
+ status: "error",
336
+ message:
337
+ "Sprint already exists (claims.json found). Pass force: true to reinitialize.",
338
+ };
263
339
  }
264
340
 
265
341
  const initArgs = [
266
- path.join(__dirname, '..', 'bin', 'wheat.js'),
267
- 'init', '--headless',
268
- '--question', args.question,
269
- '--audience', args.audience || 'self',
270
- '--constraints', args.constraints || 'none',
271
- '--done', args.done || 'A recommendation with evidence',
272
- '--dir', dir,
342
+ path.join(__dirname, "..", "bin", "wheat.js"),
343
+ "init",
344
+ "--headless",
345
+ "--question",
346
+ args.question,
347
+ "--audience",
348
+ args.audience || "self",
349
+ "--constraints",
350
+ args.constraints || "none",
351
+ "--done",
352
+ args.done || "A recommendation with evidence",
353
+ "--dir",
354
+ dir,
273
355
  ];
274
356
 
275
- if (args.force) initArgs.push('--force');
357
+ if (args.force) initArgs.push("--force");
276
358
 
277
359
  try {
278
- const output = execFileSync('node', initArgs, {
360
+ const output = execFileSync("node", initArgs, {
279
361
  cwd: dir,
280
- encoding: 'utf8',
362
+ encoding: "utf8",
281
363
  timeout: 15000,
282
- stdio: ['pipe', 'pipe', 'pipe'],
364
+ stdio: ["pipe", "pipe", "pipe"],
283
365
  });
284
- return { status: 'ok', message: 'Sprint initialized.', output: output.trim() };
366
+ return {
367
+ status: "ok",
368
+ message: "Sprint initialized.",
369
+ output: output.trim(),
370
+ };
285
371
  } catch (err) {
286
- return { status: 'error', message: (err.stderr || err.stdout || err.message).trim() };
372
+ return {
373
+ status: "error",
374
+ message: (err.stderr || err.stdout || err.message).trim(),
375
+ };
376
+ }
377
+ }
378
+
379
+ function toolDeepwiki(_dir, args) {
380
+ const repo = args.repo;
381
+ if (!repo || !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repo)) {
382
+ return {
383
+ status: "error",
384
+ message:
385
+ 'Required: repo in "org/name" format (e.g., "grainulation/wheat")',
386
+ };
387
+ }
388
+
389
+ const url = `https://deepwiki.com/${repo}`;
390
+
391
+ return new Promise((resolve) => {
392
+ const req = https.get(
393
+ url,
394
+ { timeout: 15000, headers: { Accept: "text/html" } },
395
+ (res) => {
396
+ if (res.statusCode === 301 || res.statusCode === 302) {
397
+ resolve({
398
+ status: "ok",
399
+ repo,
400
+ url,
401
+ note: `DeepWiki redirects for ${repo} — repo may not be indexed yet. Visit ${url} to trigger indexing.`,
402
+ content: null,
403
+ });
404
+ res.resume();
405
+ return;
406
+ }
407
+ if (res.statusCode !== 200) {
408
+ resolve({
409
+ status: "error",
410
+ message: `DeepWiki returned HTTP ${res.statusCode} for ${repo}. The repo may not be indexed — visit ${url} to trigger indexing.`,
411
+ });
412
+ res.resume();
413
+ return;
414
+ }
415
+
416
+ let body = "";
417
+ res.setEncoding("utf8");
418
+ res.on("data", (chunk) => {
419
+ body += chunk;
420
+ });
421
+ res.on("end", () => {
422
+ // Extract useful content from DeepWiki HTML
423
+ // Strip script/style tags, extract text content from main sections
424
+ const cleaned = body
425
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
426
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
427
+ .replace(/<nav[\s\S]*?<\/nav>/gi, "")
428
+ .replace(/<footer[\s\S]*?<\/footer>/gi, "");
429
+
430
+ // Extract headings and their content for structured output
431
+ const sections = [];
432
+ const headingRegex = /<h([1-3])[^>]*>([\s\S]*?)<\/h\1>/gi;
433
+ let match;
434
+ while ((match = headingRegex.exec(cleaned)) !== null) {
435
+ const level = parseInt(match[1], 10);
436
+ const title = match[2].replace(/<[^>]+>/g, "").trim();
437
+ if (title) sections.push({ level, title });
438
+ }
439
+
440
+ // Extract paragraph text
441
+ const paragraphs = [];
442
+ const pRegex = /<p[^>]*>([\s\S]*?)<\/p>/gi;
443
+ while ((match = pRegex.exec(cleaned)) !== null) {
444
+ const text = match[1].replace(/<[^>]+>/g, "").trim();
445
+ if (text && text.length > 30) paragraphs.push(text);
446
+ }
447
+
448
+ // Limit output to avoid overwhelming context
449
+ const truncatedParagraphs = paragraphs.slice(0, 30);
450
+
451
+ resolve({
452
+ status: "ok",
453
+ repo,
454
+ url,
455
+ sections: sections.slice(0, 50),
456
+ content_preview: truncatedParagraphs,
457
+ note: `Full DeepWiki documentation available at ${url}. Use /pull deepwiki ${repo} to extract claims from this content.`,
458
+ });
459
+ });
460
+ }
461
+ );
462
+
463
+ req.on("error", (err) => {
464
+ resolve({
465
+ status: "error",
466
+ message: `Failed to reach DeepWiki: ${err.message}. Visit ${url} manually.`,
467
+ });
468
+ });
469
+ req.on("timeout", () => {
470
+ req.destroy();
471
+ resolve({
472
+ status: "error",
473
+ message: `DeepWiki request timed out. Visit ${url} manually.`,
474
+ });
475
+ });
476
+ });
477
+ }
478
+
479
+ function toolSyncLog(dir) {
480
+ const logPath = path.join(dir, "output", "sync-log.json");
481
+ if (!fs.existsSync(logPath)) {
482
+ return {
483
+ status: "ok",
484
+ message: "No sync history found. Use /sync to publish sprint artifacts.",
485
+ entries: [],
486
+ };
487
+ }
488
+ try {
489
+ const entries = JSON.parse(fs.readFileSync(logPath, "utf8"));
490
+ return { status: "ok", count: entries.length, entries };
491
+ } catch {
492
+ return { status: "error", message: "sync-log.json is corrupted." };
287
493
  }
288
494
  }
289
495
 
@@ -291,174 +497,271 @@ function toolInit(dir, args) {
291
497
 
292
498
  const TOOLS = [
293
499
  {
294
- name: 'wheat/compile',
295
- description: 'Run the wheat compiler on claims.json. Returns compilation status, warnings, and errors.',
500
+ name: "wheat/compile",
501
+ description:
502
+ "Run the wheat compiler on claims.json. Returns compilation status, warnings, and errors.",
296
503
  inputSchema: {
297
- type: 'object',
504
+ type: "object",
298
505
  properties: {},
299
506
  },
300
507
  },
301
508
  {
302
- name: 'wheat/add-claim',
303
- description: 'Append a typed claim to claims.json. Validates type, evidence tier, and checks for duplicate IDs.',
509
+ name: "wheat/add-claim",
510
+ description:
511
+ "Append a typed claim to claims.json. Validates type, evidence tier, and checks for duplicate IDs.",
304
512
  inputSchema: {
305
- type: 'object',
513
+ type: "object",
306
514
  properties: {
307
- id: { type: 'string', description: 'Claim ID (e.g., r001, x001, d001)' },
308
- type: { type: 'string', enum: VALID_TYPES, description: 'Claim type' },
309
- topic: { type: 'string', description: 'Topic slug (e.g., database-migration)' },
310
- content: { type: 'string', description: 'The claim content -- specific, verifiable finding' },
311
- evidence: { type: 'string', enum: VALID_EVIDENCE, description: 'Evidence tier (default: stated)' },
312
- tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags' },
515
+ id: {
516
+ type: "string",
517
+ description: "Claim ID (e.g., r001, x001, d001)",
518
+ },
519
+ type: { type: "string", enum: VALID_TYPES, description: "Claim type" },
520
+ topic: {
521
+ type: "string",
522
+ description: "Topic slug (e.g., database-migration)",
523
+ },
524
+ content: {
525
+ type: "string",
526
+ description: "The claim content -- specific, verifiable finding",
527
+ },
528
+ evidence: {
529
+ type: "string",
530
+ enum: VALID_EVIDENCE,
531
+ description: "Evidence tier (default: stated)",
532
+ },
533
+ tags: {
534
+ type: "array",
535
+ items: { type: "string" },
536
+ description: "Optional tags",
537
+ },
313
538
  },
314
- required: ['id', 'type', 'topic', 'content'],
539
+ required: ["id", "type", "topic", "content"],
315
540
  },
316
541
  },
317
542
  {
318
- name: 'wheat/resolve',
319
- description: 'Resolve a conflict between two claims. The winner stays active; the loser is superseded.',
543
+ name: "wheat/resolve",
544
+ description:
545
+ "Resolve a conflict between two claims. The winner stays active; the loser is superseded.",
320
546
  inputSchema: {
321
- type: 'object',
547
+ type: "object",
322
548
  properties: {
323
- winner: { type: 'string', description: 'ID of the winning claim' },
324
- loser: { type: 'string', description: 'ID of the losing claim' },
325
- reason: { type: 'string', description: 'Optional reason for the resolution' },
549
+ winner: { type: "string", description: "ID of the winning claim" },
550
+ loser: { type: "string", description: "ID of the losing claim" },
551
+ reason: {
552
+ type: "string",
553
+ description: "Optional reason for the resolution",
554
+ },
326
555
  },
327
- required: ['winner', 'loser'],
556
+ required: ["winner", "loser"],
328
557
  },
329
558
  },
330
559
  {
331
- name: 'wheat/search',
332
- description: 'Search active claims by topic, type, evidence tier, or free-text query.',
560
+ name: "wheat/search",
561
+ description:
562
+ "Search active claims by topic, type, evidence tier, or free-text query.",
333
563
  inputSchema: {
334
- type: 'object',
564
+ type: "object",
335
565
  properties: {
336
- topic: { type: 'string', description: 'Filter by topic slug' },
337
- type: { type: 'string', enum: VALID_TYPES, description: 'Filter by claim type' },
338
- evidence: { type: 'string', enum: VALID_EVIDENCE, description: 'Filter by evidence tier' },
339
- query: { type: 'string', description: 'Free-text search in claim content' },
566
+ topic: { type: "string", description: "Filter by topic slug" },
567
+ type: {
568
+ type: "string",
569
+ enum: VALID_TYPES,
570
+ description: "Filter by claim type",
571
+ },
572
+ evidence: {
573
+ type: "string",
574
+ enum: VALID_EVIDENCE,
575
+ description: "Filter by evidence tier",
576
+ },
577
+ query: {
578
+ type: "string",
579
+ description: "Free-text search in claim content",
580
+ },
340
581
  },
341
582
  },
342
583
  },
343
584
  {
344
- name: 'wheat/status',
345
- description: 'Get sprint status: question, phase, claim counts, topic count, compilation status.',
585
+ name: "wheat/status",
586
+ description:
587
+ "Get sprint status: question, phase, claim counts, topic count, compilation status.",
346
588
  inputSchema: {
347
- type: 'object',
589
+ type: "object",
348
590
  properties: {},
349
591
  },
350
592
  },
351
593
  {
352
- name: 'wheat/init',
353
- description: 'Initialize a new research sprint. Creates claims.json, CLAUDE.md, and slash commands.',
594
+ name: "wheat/init",
595
+ description:
596
+ "Initialize a new research sprint. Creates claims.json, CLAUDE.md, and slash commands.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {
600
+ question: {
601
+ type: "string",
602
+ description: "The research question for this sprint",
603
+ },
604
+ audience: {
605
+ type: "string",
606
+ description: "Comma-separated list of audience (default: self)",
607
+ },
608
+ constraints: {
609
+ type: "string",
610
+ description: "Semicolon-separated constraints (default: none)",
611
+ },
612
+ done: {
613
+ type: "string",
614
+ description:
615
+ 'What "done" looks like (default: A recommendation with evidence)',
616
+ },
617
+ force: {
618
+ type: "boolean",
619
+ description: "Reinitialize even if sprint exists (default: false)",
620
+ },
621
+ },
622
+ required: ["question"],
623
+ },
624
+ },
625
+ {
626
+ name: "wheat/deepwiki",
627
+ description:
628
+ "Fetch AI-generated documentation from DeepWiki for a public GitHub repo. Returns architecture overview, component descriptions, and section structure. Use with /pull to extract claims.",
354
629
  inputSchema: {
355
- type: 'object',
630
+ type: "object",
356
631
  properties: {
357
- question: { type: 'string', description: 'The research question for this sprint' },
358
- audience: { type: 'string', description: 'Comma-separated list of audience (default: self)' },
359
- constraints: { type: 'string', description: 'Semicolon-separated constraints (default: none)' },
360
- done: { type: 'string', description: 'What "done" looks like (default: A recommendation with evidence)' },
361
- force: { type: 'boolean', description: 'Reinitialize even if sprint exists (default: false)' },
632
+ repo: {
633
+ type: "string",
634
+ description:
635
+ 'GitHub repo in "org/name" format (e.g., "grainulation/wheat")',
636
+ },
362
637
  },
363
- required: ['question'],
638
+ required: ["repo"],
639
+ },
640
+ },
641
+ {
642
+ name: "wheat/sync-log",
643
+ description:
644
+ "View the sync history — records of when sprint artifacts were published to Confluence, Slack, or other targets.",
645
+ inputSchema: {
646
+ type: "object",
647
+ properties: {},
364
648
  },
365
649
  },
366
650
  ];
367
651
 
368
652
  const RESOURCES = [
369
653
  {
370
- uri: 'wheat://compilation',
371
- name: 'Compilation Output',
372
- description: 'Current compilation.json -- the checked, certified output from the wheat compiler.',
373
- mimeType: 'application/json',
654
+ uri: "wheat://compilation",
655
+ name: "Compilation Output",
656
+ description:
657
+ "Current compilation.json -- the checked, certified output from the wheat compiler.",
658
+ mimeType: "application/json",
374
659
  },
375
660
  {
376
- uri: 'wheat://claims',
377
- name: 'Claims Data',
378
- description: 'Current claims.json -- all typed claims in the sprint.',
379
- mimeType: 'application/json',
661
+ uri: "wheat://claims",
662
+ name: "Claims Data",
663
+ description: "Current claims.json -- all typed claims in the sprint.",
664
+ mimeType: "application/json",
380
665
  },
381
666
  {
382
- uri: 'wheat://brief',
383
- name: 'Decision Brief',
384
- description: 'Latest compiled brief (output/brief.html) -- self-contained HTML.',
385
- mimeType: 'text/html',
667
+ uri: "wheat://brief",
668
+ name: "Decision Brief",
669
+ description:
670
+ "Latest compiled brief (output/brief.html) -- self-contained HTML.",
671
+ mimeType: "text/html",
672
+ },
673
+ {
674
+ uri: "wheat://sync-log",
675
+ name: "Sync History",
676
+ description:
677
+ "Log of sprint artifact publishes to Confluence, Slack, Notion, etc.",
678
+ mimeType: "application/json",
386
679
  },
387
680
  ];
388
681
 
389
682
  // --- Request handler ---------------------------------------------------------
390
683
 
391
- function handleRequest(dir, method, params, id) {
684
+ async function handleRequest(dir, method, params, id) {
392
685
  switch (method) {
393
- case 'initialize':
686
+ case "initialize":
394
687
  return jsonRpcResponse(id, {
395
688
  protocolVersion: PROTOCOL_VERSION,
396
689
  capabilities: { tools: {}, resources: {} },
397
690
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
398
691
  });
399
692
 
400
- case 'notifications/initialized':
693
+ case "notifications/initialized":
401
694
  // No response needed for notifications
402
695
  return null;
403
696
 
404
- case 'tools/list':
697
+ case "tools/list":
405
698
  return jsonRpcResponse(id, { tools: TOOLS });
406
699
 
407
- case 'tools/call': {
700
+ case "tools/call": {
408
701
  const toolName = params.name;
409
702
  const toolArgs = params.arguments || {};
410
703
  let result;
411
704
 
412
705
  switch (toolName) {
413
- case 'wheat/compile':
706
+ case "wheat/compile":
414
707
  result = toolCompile(dir);
415
708
  break;
416
- case 'wheat/add-claim':
709
+ case "wheat/add-claim":
417
710
  result = toolAddClaim(dir, toolArgs);
418
711
  break;
419
- case 'wheat/resolve':
712
+ case "wheat/resolve":
420
713
  result = toolResolve(dir, toolArgs);
421
714
  break;
422
- case 'wheat/search':
715
+ case "wheat/search":
423
716
  result = toolSearch(dir, toolArgs);
424
717
  break;
425
- case 'wheat/status':
718
+ case "wheat/status":
426
719
  result = toolStatus(dir);
427
720
  break;
428
- case 'wheat/init':
721
+ case "wheat/init":
429
722
  result = toolInit(dir, toolArgs);
430
723
  break;
724
+ case "wheat/deepwiki":
725
+ result = await toolDeepwiki(dir, toolArgs);
726
+ break;
727
+ case "wheat/sync-log":
728
+ result = toolSyncLog(dir);
729
+ break;
431
730
  default:
432
731
  return jsonRpcError(id, -32601, `Unknown tool: ${toolName}`);
433
732
  }
434
733
 
435
- const isError = result.status === 'error';
734
+ const isError = result.status === "error";
436
735
  return jsonRpcResponse(id, {
437
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
736
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
438
737
  isError,
439
738
  });
440
739
  }
441
740
 
442
- case 'resources/list':
741
+ case "resources/list":
443
742
  return jsonRpcResponse(id, { resources: RESOURCES });
444
743
 
445
- case 'resources/read': {
744
+ case "resources/read": {
446
745
  const uri = params.uri;
447
746
  const paths = resolvePaths(dir);
448
747
  let filePath, mimeType;
449
748
 
450
749
  switch (uri) {
451
- case 'wheat://compilation':
750
+ case "wheat://compilation":
452
751
  filePath = paths.compilation;
453
- mimeType = 'application/json';
752
+ mimeType = "application/json";
454
753
  break;
455
- case 'wheat://claims':
754
+ case "wheat://claims":
456
755
  filePath = paths.claims;
457
- mimeType = 'application/json';
756
+ mimeType = "application/json";
458
757
  break;
459
- case 'wheat://brief':
758
+ case "wheat://brief":
460
759
  filePath = paths.brief;
461
- mimeType = 'text/html';
760
+ mimeType = "text/html";
761
+ break;
762
+ case "wheat://sync-log":
763
+ filePath = path.join(dir, "output", "sync-log.json");
764
+ mimeType = "application/json";
462
765
  break;
463
766
  default:
464
767
  return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);
@@ -466,17 +769,19 @@ function handleRequest(dir, method, params, id) {
466
769
 
467
770
  if (!fs.existsSync(filePath)) {
468
771
  return jsonRpcResponse(id, {
469
- contents: [{ uri, mimeType, text: `Resource not found: ${filePath}` }],
772
+ contents: [
773
+ { uri, mimeType, text: `Resource not found: ${filePath}` },
774
+ ],
470
775
  });
471
776
  }
472
777
 
473
- const text = fs.readFileSync(filePath, 'utf8');
778
+ const text = fs.readFileSync(filePath, "utf8");
474
779
  return jsonRpcResponse(id, {
475
780
  contents: [{ uri, mimeType, text }],
476
781
  });
477
782
  }
478
783
 
479
- case 'ping':
784
+ case "ping":
480
785
  return jsonRpcResponse(id, {});
481
786
 
482
787
  default:
@@ -500,40 +805,47 @@ function startServer(dir) {
500
805
  process.stdout._handle.setBlocking(true);
501
806
  }
502
807
 
503
- rl.on('line', (line) => {
808
+ rl.on("line", async (line) => {
504
809
  if (!line.trim()) return;
505
810
 
506
811
  let msg;
507
812
  try {
508
813
  msg = JSON.parse(line);
509
814
  } catch {
510
- const resp = jsonRpcError(null, -32700, 'Parse error');
511
- process.stdout.write(resp + '\n');
815
+ const resp = jsonRpcError(null, -32700, "Parse error");
816
+ process.stdout.write(resp + "\n");
512
817
  return;
513
818
  }
514
819
 
515
- const response = handleRequest(dir, msg.method, msg.params || {}, msg.id);
820
+ const response = await handleRequest(
821
+ dir,
822
+ msg.method,
823
+ msg.params || {},
824
+ msg.id
825
+ );
516
826
 
517
827
  // Notifications don't get responses
518
828
  if (response !== null) {
519
- process.stdout.write(response + '\n');
829
+ process.stdout.write(response + "\n");
520
830
  }
521
831
  });
522
832
 
523
- rl.on('close', () => {
833
+ rl.on("close", () => {
524
834
  process.exit(0);
525
835
  });
526
836
 
527
837
  // Log to stderr (stdout is reserved for JSON-RPC)
528
838
  process.stderr.write(`wheat MCP server v${SERVER_VERSION} ready on stdio\n`);
529
839
  process.stderr.write(` Target directory: ${dir}\n`);
530
- process.stderr.write(` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`);
840
+ process.stderr.write(
841
+ ` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`
842
+ );
531
843
  }
532
844
 
533
845
  // --- CLI handler -------------------------------------------------------------
534
846
 
535
847
  export async function run(dir, args) {
536
- if (args.includes('--help') || args.includes('-h')) {
848
+ if (args.includes("--help") || args.includes("-h")) {
537
849
  console.log(`wheat mcp -- Local MCP server for Claude Code
538
850
 
539
851
  Usage:
@@ -549,11 +861,14 @@ Tools exposed:
549
861
  wheat/search Search claims
550
862
  wheat/status Sprint status
551
863
  wheat/init Initialize a new sprint
864
+ wheat/deepwiki Fetch DeepWiki docs for a GitHub repo
865
+ wheat/sync-log View sync/publish history
552
866
 
553
867
  Resources exposed:
554
868
  wheat://compilation compilation.json
555
869
  wheat://claims claims.json
556
870
  wheat://brief output/brief.html
871
+ wheat://sync-log output/sync-log.json
557
872
 
558
873
  Protocol: MCP over stdio (JSON-RPC 2.0, newline-delimited)`);
559
874
  return;