@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.
- package/LICENSE +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +47 -36
- package/compiler/detect-sprints.js +126 -92
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +789 -468
- package/lib/compiler.js +11 -6
- package/lib/connect.js +273 -134
- package/lib/disconnect.js +61 -40
- package/lib/guard.js +20 -17
- package/lib/index.js +8 -8
- package/lib/init.js +217 -142
- package/lib/install-prompt.js +26 -26
- package/lib/load-claims.js +88 -0
- package/lib/quickstart.js +225 -111
- package/lib/serve-mcp.js +495 -180
- package/lib/server.js +198 -111
- package/lib/stats.js +65 -39
- package/lib/status.js +65 -34
- package/lib/update.js +13 -11
- package/package.json +8 -4
- package/templates/claude.md +31 -17
- package/templates/commands/blind-spot.md +9 -2
- package/templates/commands/brief.md +11 -1
- package/templates/commands/calibrate.md +3 -1
- package/templates/commands/challenge.md +4 -1
- package/templates/commands/connect.md +12 -1
- package/templates/commands/evaluate.md +4 -0
- package/templates/commands/feedback.md +3 -1
- package/templates/commands/handoff.md +11 -7
- package/templates/commands/init.md +4 -1
- package/templates/commands/merge.md +4 -1
- package/templates/commands/next.md +1 -0
- package/templates/commands/present.md +3 -0
- package/templates/commands/prototype.md +2 -0
- package/templates/commands/pull.md +103 -0
- package/templates/commands/replay.md +8 -0
- package/templates/commands/research.md +1 -0
- package/templates/commands/resolve.md +4 -1
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +94 -0
- 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
|
|
29
|
-
import path from
|
|
30
|
-
import readline from
|
|
31
|
-
import
|
|
32
|
-
import
|
|
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 =
|
|
40
|
-
const SERVER_VERSION = JSON.parse(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
61
|
-
compilation: path.join(dir,
|
|
62
|
-
brief: path.join(dir,
|
|
63
|
-
compiler: path.join(dir,
|
|
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 {
|
|
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,
|
|
97
|
+
compilerPath = path.join(__dirname, "..", "compiler", "wheat-compiler.js");
|
|
80
98
|
}
|
|
81
99
|
if (!fs.existsSync(compilerPath)) {
|
|
82
|
-
return {
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
153
|
+
return {
|
|
154
|
+
status: "error",
|
|
155
|
+
message: `Invalid evidence "${evidence}". Valid: ${VALID_EVIDENCE.join(
|
|
156
|
+
", "
|
|
157
|
+
)}`,
|
|
158
|
+
};
|
|
115
159
|
}
|
|
116
160
|
|
|
117
|
-
const data =
|
|
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:
|
|
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:
|
|
130
|
-
evidence: evidence ||
|
|
131
|
-
status:
|
|
132
|
-
phase_added: data.meta.phase ||
|
|
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) +
|
|
190
|
+
fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + "\n");
|
|
141
191
|
|
|
142
|
-
return { status:
|
|
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:
|
|
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:
|
|
203
|
+
return { status: "error", message: "Required fields: winner, loser" };
|
|
154
204
|
}
|
|
155
205
|
|
|
156
|
-
const data =
|
|
157
|
-
|
|
158
|
-
|
|
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)
|
|
161
|
-
|
|
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(
|
|
222
|
+
winnerClaim.conflicts_with = (winnerClaim.conflicts_with || []).filter(
|
|
223
|
+
(cid) => cid !== loser
|
|
224
|
+
);
|
|
165
225
|
loserClaim.conflicts_with = [];
|
|
166
|
-
loserClaim.status =
|
|
226
|
+
loserClaim.status = "superseded";
|
|
167
227
|
loserClaim.resolved_by = winner;
|
|
168
228
|
|
|
169
|
-
fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) +
|
|
229
|
+
fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + "\n");
|
|
170
230
|
|
|
171
231
|
return {
|
|
172
|
-
status:
|
|
173
|
-
message: `Resolved: ${winner} wins over ${loser}${
|
|
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
|
|
181
|
-
if (!
|
|
182
|
-
return {
|
|
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:
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 ===
|
|
225
|
-
const conflicted = claims.filter(
|
|
226
|
-
c
|
|
227
|
-
|
|
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 => {
|
|
298
|
+
active.forEach((c) => {
|
|
299
|
+
types[c.type] = (types[c.type] || 0) + 1;
|
|
300
|
+
});
|
|
232
301
|
|
|
233
|
-
|
|
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,
|
|
237
|
-
compilationStatus = comp.status ||
|
|
238
|
-
} catch {
|
|
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:
|
|
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:
|
|
330
|
+
return { status: "error", message: "Required field: question" };
|
|
259
331
|
}
|
|
260
332
|
|
|
261
333
|
if (fs.existsSync(paths.claims) && !args.force) {
|
|
262
|
-
return {
|
|
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,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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(
|
|
357
|
+
if (args.force) initArgs.push("--force");
|
|
276
358
|
|
|
277
359
|
try {
|
|
278
|
-
const output = execFileSync(
|
|
360
|
+
const output = execFileSync("node", initArgs, {
|
|
279
361
|
cwd: dir,
|
|
280
|
-
encoding:
|
|
362
|
+
encoding: "utf8",
|
|
281
363
|
timeout: 15000,
|
|
282
|
-
stdio: [
|
|
364
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
283
365
|
});
|
|
284
|
-
return {
|
|
366
|
+
return {
|
|
367
|
+
status: "ok",
|
|
368
|
+
message: "Sprint initialized.",
|
|
369
|
+
output: output.trim(),
|
|
370
|
+
};
|
|
285
371
|
} catch (err) {
|
|
286
|
-
return {
|
|
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:
|
|
295
|
-
description:
|
|
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:
|
|
504
|
+
type: "object",
|
|
298
505
|
properties: {},
|
|
299
506
|
},
|
|
300
507
|
},
|
|
301
508
|
{
|
|
302
|
-
name:
|
|
303
|
-
description:
|
|
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:
|
|
513
|
+
type: "object",
|
|
306
514
|
properties: {
|
|
307
|
-
id: {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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: [
|
|
539
|
+
required: ["id", "type", "topic", "content"],
|
|
315
540
|
},
|
|
316
541
|
},
|
|
317
542
|
{
|
|
318
|
-
name:
|
|
319
|
-
description:
|
|
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:
|
|
547
|
+
type: "object",
|
|
322
548
|
properties: {
|
|
323
|
-
winner: { type:
|
|
324
|
-
loser: { type:
|
|
325
|
-
reason: {
|
|
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: [
|
|
556
|
+
required: ["winner", "loser"],
|
|
328
557
|
},
|
|
329
558
|
},
|
|
330
559
|
{
|
|
331
|
-
name:
|
|
332
|
-
description:
|
|
560
|
+
name: "wheat/search",
|
|
561
|
+
description:
|
|
562
|
+
"Search active claims by topic, type, evidence tier, or free-text query.",
|
|
333
563
|
inputSchema: {
|
|
334
|
-
type:
|
|
564
|
+
type: "object",
|
|
335
565
|
properties: {
|
|
336
|
-
topic: { type:
|
|
337
|
-
type: {
|
|
338
|
-
|
|
339
|
-
|
|
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:
|
|
345
|
-
description:
|
|
585
|
+
name: "wheat/status",
|
|
586
|
+
description:
|
|
587
|
+
"Get sprint status: question, phase, claim counts, topic count, compilation status.",
|
|
346
588
|
inputSchema: {
|
|
347
|
-
type:
|
|
589
|
+
type: "object",
|
|
348
590
|
properties: {},
|
|
349
591
|
},
|
|
350
592
|
},
|
|
351
593
|
{
|
|
352
|
-
name:
|
|
353
|
-
description:
|
|
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:
|
|
630
|
+
type: "object",
|
|
356
631
|
properties: {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
632
|
+
repo: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description:
|
|
635
|
+
'GitHub repo in "org/name" format (e.g., "grainulation/wheat")',
|
|
636
|
+
},
|
|
362
637
|
},
|
|
363
|
-
required: [
|
|
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:
|
|
371
|
-
name:
|
|
372
|
-
description:
|
|
373
|
-
|
|
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:
|
|
377
|
-
name:
|
|
378
|
-
description:
|
|
379
|
-
mimeType:
|
|
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:
|
|
383
|
-
name:
|
|
384
|
-
description:
|
|
385
|
-
|
|
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
|
|
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
|
|
693
|
+
case "notifications/initialized":
|
|
401
694
|
// No response needed for notifications
|
|
402
695
|
return null;
|
|
403
696
|
|
|
404
|
-
case
|
|
697
|
+
case "tools/list":
|
|
405
698
|
return jsonRpcResponse(id, { tools: TOOLS });
|
|
406
699
|
|
|
407
|
-
case
|
|
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
|
|
706
|
+
case "wheat/compile":
|
|
414
707
|
result = toolCompile(dir);
|
|
415
708
|
break;
|
|
416
|
-
case
|
|
709
|
+
case "wheat/add-claim":
|
|
417
710
|
result = toolAddClaim(dir, toolArgs);
|
|
418
711
|
break;
|
|
419
|
-
case
|
|
712
|
+
case "wheat/resolve":
|
|
420
713
|
result = toolResolve(dir, toolArgs);
|
|
421
714
|
break;
|
|
422
|
-
case
|
|
715
|
+
case "wheat/search":
|
|
423
716
|
result = toolSearch(dir, toolArgs);
|
|
424
717
|
break;
|
|
425
|
-
case
|
|
718
|
+
case "wheat/status":
|
|
426
719
|
result = toolStatus(dir);
|
|
427
720
|
break;
|
|
428
|
-
case
|
|
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 ===
|
|
734
|
+
const isError = result.status === "error";
|
|
436
735
|
return jsonRpcResponse(id, {
|
|
437
|
-
content: [{ type:
|
|
736
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
438
737
|
isError,
|
|
439
738
|
});
|
|
440
739
|
}
|
|
441
740
|
|
|
442
|
-
case
|
|
741
|
+
case "resources/list":
|
|
443
742
|
return jsonRpcResponse(id, { resources: RESOURCES });
|
|
444
743
|
|
|
445
|
-
case
|
|
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
|
|
750
|
+
case "wheat://compilation":
|
|
452
751
|
filePath = paths.compilation;
|
|
453
|
-
mimeType =
|
|
752
|
+
mimeType = "application/json";
|
|
454
753
|
break;
|
|
455
|
-
case
|
|
754
|
+
case "wheat://claims":
|
|
456
755
|
filePath = paths.claims;
|
|
457
|
-
mimeType =
|
|
756
|
+
mimeType = "application/json";
|
|
458
757
|
break;
|
|
459
|
-
case
|
|
758
|
+
case "wheat://brief":
|
|
460
759
|
filePath = paths.brief;
|
|
461
|
-
mimeType =
|
|
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: [
|
|
772
|
+
contents: [
|
|
773
|
+
{ uri, mimeType, text: `Resource not found: ${filePath}` },
|
|
774
|
+
],
|
|
470
775
|
});
|
|
471
776
|
}
|
|
472
777
|
|
|
473
|
-
const text = fs.readFileSync(filePath,
|
|
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
|
|
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(
|
|
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,
|
|
511
|
-
process.stdout.write(resp +
|
|
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(
|
|
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 +
|
|
829
|
+
process.stdout.write(response + "\n");
|
|
520
830
|
}
|
|
521
831
|
});
|
|
522
832
|
|
|
523
|
-
rl.on(
|
|
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(
|
|
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(
|
|
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;
|