@astrosheep/keiyaku 0.1.0

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/build/index.js ADDED
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6
+ import { handleKeiyaku, handleAsk, handleDrive, invokeJudgment, resolveJudgmentOath, } from "./logic.js";
7
+ import * as git from "./git.js";
8
+ import { isSubagentExecError } from "./subagent-exec/index.js";
9
+ import { appendDebugLog } from "./debug-log.js";
10
+ import { asMessage, isFlowError, pickHintFromError } from "./errors.js";
11
+ import { buildKeiyakuSuccessResponse, buildAskResponse, buildDriveResponse, buildToolErrorResponse, buildJudgmentDoneResponse, buildJudgmentDropResponse, } from "./response-builders.js";
12
+ import { askToolSchema, issueKeiyakuToolSchema, driveKeiyakuToolSchema, judgmentToolSchema, cliHelpToolSchema, } from "./tool-schemas.js";
13
+ import { listTermPresets, resolveTermPreset } from "./term-presets.js";
14
+ function classifyToolError(error) {
15
+ if (isFlowError(error)) {
16
+ return { errorType: "keiyaku_error", errorCode: error.code };
17
+ }
18
+ if (isSubagentExecError(error)) {
19
+ return { errorType: "subagent_exec_error", errorCode: error.code };
20
+ }
21
+ if (error instanceof Error && error.name === "AbortError") {
22
+ return { errorType: "abort_error", errorCode: "ABORTED" };
23
+ }
24
+ return { errorType: "runtime_error", errorCode: "INTERNAL_ERROR" };
25
+ }
26
+ function formatCriteria(criteria) {
27
+ return criteria.map((item) => `- ${item}`).join("\n");
28
+ }
29
+ function registerTools(server) {
30
+ const preset = resolveTermPreset();
31
+ const summonPreset = preset.tools.summon;
32
+ const drivePreset = preset.tools.drive;
33
+ const askPreset = preset.tools.ask;
34
+ const judgmentPreset = preset.tools.judgment;
35
+ const helpPreset = preset.tools.help;
36
+ const presetIdentities = listTermPresets()
37
+ .map((item) => `${item.id}=${item.identity}`)
38
+ .join(", ");
39
+ const currentOath = resolveJudgmentOath();
40
+ const dynamicJudgmentSchema = judgmentToolSchema.extend({
41
+ oath: judgmentToolSchema.shape.oath.describe(judgmentToolSchema.shape.oath.description.replace("${OATH_TEXT}", `'${currentOath}'`)),
42
+ });
43
+ const dynamicIssueSchema = issueKeiyakuToolSchema.extend({
44
+ subagentName: issueKeiyakuToolSchema.shape.subagentName.describe(issueKeiyakuToolSchema.shape.subagentName.description
45
+ .replace("${IDENTITY}", preset.identity)
46
+ .replace("${PRESET_IDENTITIES}", presetIdentities)),
47
+ });
48
+ const dynamicDriveSchema = driveKeiyakuToolSchema.extend({
49
+ subagentName: driveKeiyakuToolSchema.shape.subagentName.describe(driveKeiyakuToolSchema.shape.subagentName.description
50
+ .replace("${IDENTITY}", preset.identity)
51
+ .replace("${PRESET_IDENTITIES}", presetIdentities)),
52
+ });
53
+ const dynamicAskSchema = askToolSchema.extend({
54
+ subagentName: askToolSchema.shape.subagentName.describe(askToolSchema.shape.subagentName.description
55
+ .replace("${IDENTITY}", preset.identity)
56
+ .replace("${PRESET_IDENTITIES}", presetIdentities)),
57
+ });
58
+ server.registerTool(summonPreset.name, {
59
+ title: summonPreset.title,
60
+ description: summonPreset.description,
61
+ inputSchema: dynamicIssueSchema,
62
+ }, async ({ title, goal, directive, context, constraints, criteria, subagentName, cwd }, extra) => {
63
+ const workingDir = cwd || process.cwd();
64
+ try {
65
+ appendDebugLog(`tool summon start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
66
+ const finalContext = context?.trim() || "No additional context provided.";
67
+ const result = await handleKeiyaku({
68
+ cwd: workingDir,
69
+ title,
70
+ goal,
71
+ directive,
72
+ context: finalContext,
73
+ criteria: formatCriteria(criteria),
74
+ constraints,
75
+ subagentName,
76
+ signal: extra.signal,
77
+ });
78
+ appendDebugLog(`tool summon success branch=${result.branch} base=${result.baseBranch} round=${result.round}`, {
79
+ cwd: workingDir,
80
+ section: "script",
81
+ });
82
+ const { identity } = resolveTermPreset();
83
+ return buildKeiyakuSuccessResponse(result, {
84
+ title,
85
+ goal,
86
+ directive,
87
+ context,
88
+ constraints,
89
+ criteria,
90
+ subagentName,
91
+ cwd: workingDir,
92
+ });
93
+ }
94
+ catch (err) {
95
+ const message = asMessage(err);
96
+ appendDebugLog(`tool summon error: ${message}`, { cwd: workingDir, section: "script" });
97
+ const hint = pickHintFromError(err, message);
98
+ const { errorType, errorCode } = classifyToolError(err);
99
+ const { identity } = resolveTermPreset();
100
+ return buildToolErrorResponse({
101
+ tool: "summon",
102
+ title: "Keiyaku",
103
+ message,
104
+ hint,
105
+ errorType,
106
+ errorCode,
107
+ inputEcho: [
108
+ "Inputs:",
109
+ `Title: ${title}`,
110
+ `Goal: ${goal}`,
111
+ ...(directive ? [`Directive: ${directive}`] : []),
112
+ `Criteria (${criteria.length}): ${criteria.join("; ")}`,
113
+ ...(context ? [`Context: ${context}`] : []),
114
+ ...(constraints ? [`Constraints: ${constraints}`] : []),
115
+ ...(subagentName ? [`${identity}: ${subagentName}`] : []),
116
+ `CWD: ${workingDir}`,
117
+ ],
118
+ });
119
+ }
120
+ });
121
+ server.registerTool(drivePreset.name, {
122
+ title: drivePreset.title,
123
+ description: drivePreset.description,
124
+ inputSchema: dynamicDriveSchema,
125
+ }, async ({ directive, context, subagentName, cwd }, extra) => {
126
+ const workingDir = cwd || process.cwd();
127
+ try {
128
+ appendDebugLog(`tool drive start cwd=${workingDir}`, {
129
+ cwd: workingDir,
130
+ section: "script",
131
+ });
132
+ const outcome = await handleDrive({
133
+ cwd: workingDir,
134
+ directive,
135
+ context,
136
+ subagentName,
137
+ signal: extra.signal,
138
+ });
139
+ appendDebugLog(`tool drive success round=${outcome.round}`, {
140
+ cwd: workingDir,
141
+ section: "script",
142
+ });
143
+ return buildDriveResponse(outcome, { directive, context, subagentName, cwd: workingDir });
144
+ }
145
+ catch (err) {
146
+ const message = asMessage(err);
147
+ appendDebugLog(`tool drive error: ${message}`, { cwd: workingDir, section: "script" });
148
+ const hint = pickHintFromError(err, message);
149
+ const { errorType, errorCode } = classifyToolError(err);
150
+ const { identity } = resolveTermPreset();
151
+ return buildToolErrorResponse({
152
+ tool: "drive",
153
+ title: "Drive",
154
+ message,
155
+ hint,
156
+ errorType,
157
+ errorCode,
158
+ inputEcho: [
159
+ "Inputs:",
160
+ `Directive: ${directive}`,
161
+ ...(context ? [`Context: ${context}`] : []),
162
+ ...(subagentName ? [`${identity}: ${subagentName}`] : []),
163
+ `CWD: ${workingDir}`,
164
+ ],
165
+ });
166
+ }
167
+ });
168
+ server.registerTool(askPreset.name, {
169
+ title: askPreset.title,
170
+ description: askPreset.description,
171
+ inputSchema: dynamicAskSchema,
172
+ }, async ({ request, context, subagentName, cwd }, extra) => {
173
+ const workingDir = cwd || process.cwd();
174
+ try {
175
+ appendDebugLog(`tool ask start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
176
+ const result = await handleAsk({
177
+ cwd: workingDir,
178
+ request,
179
+ context,
180
+ subagentName,
181
+ signal: extra.signal,
182
+ });
183
+ appendDebugLog(`tool ask success`, { cwd: workingDir, section: "script" });
184
+ return buildAskResponse(result, { request, context, subagentName, cwd: workingDir });
185
+ }
186
+ catch (err) {
187
+ const message = asMessage(err);
188
+ appendDebugLog(`tool ask error: ${message}`, { cwd: workingDir, section: "script" });
189
+ const hint = pickHintFromError(err, message);
190
+ const { errorType, errorCode } = classifyToolError(err);
191
+ const { identity } = resolveTermPreset();
192
+ return buildToolErrorResponse({
193
+ tool: "ask",
194
+ title: "Ask",
195
+ message,
196
+ hint,
197
+ errorType,
198
+ errorCode,
199
+ inputEcho: [
200
+ "Inputs:",
201
+ `Request: ${request}`,
202
+ `Context: ${context}`,
203
+ ...(subagentName ? [`${identity}: ${subagentName}`] : []),
204
+ `CWD: ${workingDir}`,
205
+ ],
206
+ });
207
+ }
208
+ });
209
+ server.registerTool(judgmentPreset.name, {
210
+ title: judgmentPreset.title,
211
+ description: judgmentPreset.description,
212
+ inputSchema: dynamicJudgmentSchema,
213
+ }, async ({ intent, criteriaChecks, metPrecise, metMinimal, metIsolated, metIdiomatic, metCohesive, oath, cwd }, extra) => {
214
+ const workingDir = cwd || process.cwd();
215
+ const criteriaCheckParts = criteriaChecks;
216
+ try {
217
+ appendDebugLog(`tool judgment start intent=${intent} cwd=${workingDir} criteriaChecks=${criteriaCheckParts.length}`, {
218
+ cwd: workingDir,
219
+ section: "script",
220
+ });
221
+ const outcome = await invokeJudgment({
222
+ cwd: workingDir,
223
+ intent,
224
+ criteriaChecks: criteriaCheckParts,
225
+ metPrecise,
226
+ metMinimal,
227
+ metIsolated,
228
+ metIdiomatic,
229
+ metCohesive,
230
+ oath,
231
+ signal: extra.signal,
232
+ });
233
+ if (intent === "INVOKE") {
234
+ if (!("status" in outcome) || outcome.status !== "done") {
235
+ throw new Error("Unexpected INVOKE outcome shape");
236
+ }
237
+ const finalOutcome = outcome;
238
+ appendDebugLog(`tool judgment INVOKE success branch=${finalOutcome.branch} base=${finalOutcome.baseBranch}`, {
239
+ cwd: workingDir,
240
+ section: "script",
241
+ });
242
+ return buildJudgmentDoneResponse(finalOutcome, {
243
+ criteriaChecks: criteriaCheckParts,
244
+ metPrecise,
245
+ metMinimal,
246
+ metIsolated,
247
+ metIdiomatic,
248
+ metCohesive,
249
+ oath,
250
+ cwd: workingDir,
251
+ });
252
+ }
253
+ if (!("status" in outcome) || outcome.status !== "dropped") {
254
+ throw new Error("Unexpected DROP outcome shape");
255
+ }
256
+ const finalOutcome = outcome;
257
+ appendDebugLog(`tool judgment DROP success branch=${finalOutcome.branch} base=${finalOutcome.baseBranch}`, {
258
+ cwd: workingDir,
259
+ section: "script",
260
+ });
261
+ return buildJudgmentDropResponse(finalOutcome, {
262
+ criteriaChecks: criteriaCheckParts,
263
+ metPrecise,
264
+ metMinimal,
265
+ metIsolated,
266
+ metIdiomatic,
267
+ metCohesive,
268
+ oath,
269
+ cwd: workingDir,
270
+ });
271
+ }
272
+ catch (err) {
273
+ const message = asMessage(err);
274
+ appendDebugLog(`tool judgment error: ${message}`, { cwd: workingDir, section: "script" });
275
+ const hint = pickHintFromError(err, message);
276
+ const { errorType, errorCode } = classifyToolError(err);
277
+ return buildToolErrorResponse({
278
+ tool: "invoke_judgment",
279
+ title: "Invoke Judgment",
280
+ message,
281
+ hint,
282
+ errorType,
283
+ errorCode,
284
+ inputEcho: [
285
+ "Inputs:",
286
+ `Intent: ${intent}`,
287
+ `Criteria checks (${criteriaCheckParts.length}): ${criteriaCheckParts.join("; ")}`,
288
+ `Quality flags: metPrecise=${metPrecise} metMinimal=${metMinimal} metIsolated=${metIsolated} metIdiomatic=${metIdiomatic} metCohesive=${metCohesive}`,
289
+ ...(oath ? [`Oath: ${oath}`] : []),
290
+ `CWD: ${workingDir}`,
291
+ ],
292
+ });
293
+ }
294
+ });
295
+ server.registerTool(helpPreset.name, {
296
+ title: helpPreset.title,
297
+ description: helpPreset.description,
298
+ inputSchema: cliHelpToolSchema,
299
+ }, async ({ question }) => {
300
+ const workingDir = process.cwd();
301
+ let branchState = "Unknown";
302
+ try {
303
+ const currentBranch = await git.getCurrentBranch(workingDir);
304
+ const activeKeiyaku = await git.getActiveKeiyakuBranch(workingDir);
305
+ if (activeKeiyaku) {
306
+ const base = await git.getKeiyakuBase(workingDir, activeKeiyaku);
307
+ branchState = `Active Keiyaku: ${activeKeiyaku}\n Base Branch: ${base || "unknown"}`;
308
+ }
309
+ else {
310
+ branchState = `No active keiyaku. Current branch: ${currentBranch}`;
311
+ }
312
+ }
313
+ catch {
314
+ branchState = "Not a git repository or git error.";
315
+ }
316
+ const helpContent = [
317
+ "# Keiyaku System Help",
318
+ "",
319
+ "## Current State",
320
+ branchState,
321
+ "",
322
+ "## Core Files (.keiyaku/)",
323
+ "These files define the 'Law' of the project. **CRITICAL**: Use Markdown lists (`- item`).",
324
+ "",
325
+ "- **base-criteria.md**: Universal standards for task completion. Automatically inherited.",
326
+ "- **base-constraints.md**: Mandatory architectural boundaries and coding standards.",
327
+ "",
328
+ "## Workflow",
329
+ "1. `summon`: Start a new keiyaku (creates branch, begins round 1).",
330
+ "2. `drive`: Provide feedback or new instructions for the next round.",
331
+ "3. `invoke_judgment`: Express intent (INVOKE/DROP) to merge or discard changes.",
332
+ "",
333
+ "## Protocol Files (Pact Roots)",
334
+ "- **KEIYAKU.md**: The immutable mission definition for the current task.",
335
+ "- **KEIYAKU_TRACE.md**: The audit log of all rounds and reviews.",
336
+ "",
337
+ `Regarding: "${question}"`,
338
+ ].join("\n");
339
+ return {
340
+ content: [{ type: "text", text: helpContent }],
341
+ structuredContent: { ok: true, tool: "cli_help", status: "completed" },
342
+ };
343
+ });
344
+ }
345
+ function createServer() {
346
+ const server = new McpServer({
347
+ name: "keiyaku",
348
+ version: "1.0.0",
349
+ });
350
+ registerTools(server);
351
+ return server;
352
+ }
353
+ function sendJsonRpcError(res, code, message) {
354
+ const body = JSON.stringify({
355
+ jsonrpc: "2.0",
356
+ error: { code, message },
357
+ id: null,
358
+ });
359
+ res.writeHead(code === -32603 ? 500 : 405, { "content-type": "application/json" });
360
+ res.end(body);
361
+ }
362
+ async function startStdio() {
363
+ const server = createServer();
364
+ const transport = new StdioServerTransport();
365
+ await server.connect(transport);
366
+ console.error("Keiyaku MCP Server running on stdio");
367
+ }
368
+ async function startStreamableHttp() {
369
+ const host = process.env.KEIYAKU_MCP_HTTP_HOST || "127.0.0.1";
370
+ const portRaw = process.env.KEIYAKU_MCP_HTTP_PORT || "3000";
371
+ const port = Number(portRaw);
372
+ if (!Number.isFinite(port) || port <= 0) {
373
+ throw new Error(`invalid KEIYAKU_MCP_HTTP_PORT: ${portRaw}`);
374
+ }
375
+ const app = createMcpExpressApp({ host });
376
+ app.post("/mcp", async (req, res) => {
377
+ const server = createServer();
378
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
379
+ try {
380
+ await server.connect(transport);
381
+ await transport.handleRequest(req, res, req.body);
382
+ }
383
+ catch (error) {
384
+ console.error("Error handling MCP Streamable HTTP request:", error);
385
+ if (!res.headersSent) {
386
+ sendJsonRpcError(res, -32603, "Internal server error");
387
+ }
388
+ }
389
+ finally {
390
+ await transport.close().catch(() => undefined);
391
+ await server.close().catch(() => undefined);
392
+ }
393
+ });
394
+ app.get("/mcp", (_req, res) => {
395
+ sendJsonRpcError(res, -32000, "Method not allowed.");
396
+ });
397
+ app.delete("/mcp", (_req, res) => {
398
+ sendJsonRpcError(res, -32000, "Method not allowed.");
399
+ });
400
+ await new Promise((resolve, reject) => {
401
+ const listener = app.listen(port, host, () => {
402
+ console.error(`Keiyaku MCP Server running on streamable-http http://${host}:${port}/mcp`);
403
+ resolve();
404
+ });
405
+ listener.on("error", reject);
406
+ });
407
+ }
408
+ async function main() {
409
+ const transport = (process.env.KEIYAKU_MCP_TRANSPORT || "stdio").toLowerCase();
410
+ if (transport === "streamable-http") {
411
+ await startStreamableHttp();
412
+ return;
413
+ }
414
+ if (transport === "stdio") {
415
+ await startStdio();
416
+ return;
417
+ }
418
+ throw new Error(`Unsupported KEIYAKU_MCP_TRANSPORT '${transport}'. Expected 'stdio' or 'streamable-http'.`);
419
+ }
420
+ main().catch((err) => {
421
+ console.error("Fatal error:", err);
422
+ process.exit(1);
423
+ });