speclock 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "AI continuity engine — MCP server + CLI that kills AI amnesia. Maintains project memory, enforces constraints, and detects drift across AI coding sessions.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -55,5 +55,8 @@
55
55
  "src/",
56
56
  "README.md",
57
57
  "LICENSE"
58
- ]
58
+ ],
59
+ "devDependencies": {
60
+ "esbuild": "^0.27.3"
61
+ }
59
62
  }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * SpecLock MCP HTTP Server — for Railway / remote deployment
3
+ * Wraps the same 19 tools as the stdio server using Streamable HTTP transport.
4
+ * Developed by Sandeep Roy (https://github.com/sgroy10)
5
+ */
6
+
7
+ import os from "os";
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
11
+ import { z } from "zod";
12
+ import {
13
+ ensureInit,
14
+ setGoal,
15
+ addLock,
16
+ removeLock,
17
+ addDecision,
18
+ addNote,
19
+ updateDeployFacts,
20
+ logChange,
21
+ checkConflict,
22
+ getSessionBriefing,
23
+ endSession,
24
+ suggestLocks,
25
+ detectDrift,
26
+ } from "../core/engine.js";
27
+ import { generateContext, generateContextPack } from "../core/context.js";
28
+ import {
29
+ readBrain,
30
+ readEvents,
31
+ newId,
32
+ nowIso,
33
+ appendEvent,
34
+ bumpEvents,
35
+ writeBrain,
36
+ } from "../core/storage.js";
37
+ import {
38
+ captureStatus,
39
+ createTag,
40
+ getDiffSummary,
41
+ } from "../core/git.js";
42
+
43
+ const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
44
+ const VERSION = "1.1.0";
45
+ const AUTHOR = "Sandeep Roy";
46
+
47
+ function createSpecLockServer() {
48
+ const server = new McpServer(
49
+ { name: "speclock", version: VERSION },
50
+ {
51
+ instructions:
52
+ `SpecLock is an AI continuity engine. Developed by ${AUTHOR}. Call speclock_session_briefing at the start of a new session, and speclock_session_summary before ending. Use speclock_get_context to refresh your project understanding at any time.`,
53
+ }
54
+ );
55
+
56
+ // Tool 1: speclock_init
57
+ server.tool("speclock_init", "Initialize SpecLock in the current project directory.", {}, async () => {
58
+ const brain = ensureInit(PROJECT_ROOT);
59
+ return { content: [{ type: "text", text: `SpecLock initialized for "${brain.project.name}" at ${brain.project.root}` }] };
60
+ });
61
+
62
+ // Tool 2: speclock_get_context
63
+ server.tool("speclock_get_context", "THE KEY TOOL. Returns the full structured context pack.", { format: z.enum(["markdown", "json"]).default("markdown").describe("Output format") }, async ({ format }) => {
64
+ ensureInit(PROJECT_ROOT);
65
+ const text = format === "json" ? JSON.stringify(generateContextPack(PROJECT_ROOT), null, 2) : generateContext(PROJECT_ROOT);
66
+ return { content: [{ type: "text", text }] };
67
+ });
68
+
69
+ // Tool 3: speclock_set_goal
70
+ server.tool("speclock_set_goal", "Set or update the project goal.", { text: z.string().min(1).describe("The project goal text") }, async ({ text }) => {
71
+ ensureInit(PROJECT_ROOT);
72
+ setGoal(PROJECT_ROOT, text);
73
+ return { content: [{ type: "text", text: `Goal updated: ${text}` }] };
74
+ });
75
+
76
+ // Tool 4: speclock_add_lock
77
+ server.tool("speclock_add_lock", "Add a non-negotiable constraint (SpecLock).", { text: z.string().min(1).describe("The constraint text"), tags: z.array(z.string()).default([]).describe("Category tags"), source: z.enum(["user", "agent"]).default("agent").describe("Who created this lock") }, async ({ text, tags, source }) => {
78
+ ensureInit(PROJECT_ROOT);
79
+ const lock = addLock(PROJECT_ROOT, text, tags, source);
80
+ return { content: [{ type: "text", text: `Lock added [${lock.id}]: ${text}` }] };
81
+ });
82
+
83
+ // Tool 5: speclock_remove_lock
84
+ server.tool("speclock_remove_lock", "Remove (deactivate) a SpecLock by its ID.", { lockId: z.string().min(1).describe("The lock ID to remove") }, async ({ lockId }) => {
85
+ ensureInit(PROJECT_ROOT);
86
+ removeLock(PROJECT_ROOT, lockId);
87
+ return { content: [{ type: "text", text: `Lock ${lockId} removed.` }] };
88
+ });
89
+
90
+ // Tool 6: speclock_add_decision
91
+ server.tool("speclock_add_decision", "Record an architectural or design decision.", { text: z.string().min(1).describe("The decision text"), tags: z.array(z.string()).default([]), source: z.enum(["user", "agent"]).default("agent") }, async ({ text, tags, source }) => {
92
+ ensureInit(PROJECT_ROOT);
93
+ const d = addDecision(PROJECT_ROOT, text, tags, source);
94
+ return { content: [{ type: "text", text: `Decision recorded [${d.id}]: ${text}` }] };
95
+ });
96
+
97
+ // Tool 7: speclock_add_note
98
+ server.tool("speclock_add_note", "Add a pinned note for reference.", { text: z.string().min(1).describe("The note text"), pinned: z.boolean().default(true).describe("Whether to pin this note") }, async ({ text, pinned }) => {
99
+ ensureInit(PROJECT_ROOT);
100
+ const brain = readBrain(PROJECT_ROOT);
101
+ const note = { id: newId(), text, pinned, createdAt: nowIso() };
102
+ brain.state.notes.push(note);
103
+ writeBrain(PROJECT_ROOT, brain);
104
+ appendEvent(PROJECT_ROOT, { type: "note_added", noteId: note.id, text });
105
+ bumpEvents(PROJECT_ROOT);
106
+ return { content: [{ type: "text", text: `Note added [${note.id}]: ${text}` }] };
107
+ });
108
+
109
+ // Tool 8: speclock_set_deploy_facts
110
+ server.tool("speclock_set_deploy_facts", "Record deployment configuration facts.", { provider: z.string().optional(), branch: z.string().optional(), autoDeploy: z.boolean().optional(), url: z.string().optional(), notes: z.string().optional() }, async (params) => {
111
+ ensureInit(PROJECT_ROOT);
112
+ updateDeployFacts(PROJECT_ROOT, params);
113
+ return { content: [{ type: "text", text: `Deploy facts updated: ${JSON.stringify(params)}` }] };
114
+ });
115
+
116
+ // Tool 9: speclock_log_change
117
+ server.tool("speclock_log_change", "Manually log a significant change.", { summary: z.string().min(1).describe("Brief description of the change"), files: z.array(z.string()).default([]).describe("Files affected") }, async ({ summary, files }) => {
118
+ ensureInit(PROJECT_ROOT);
119
+ logChange(PROJECT_ROOT, summary, files);
120
+ return { content: [{ type: "text", text: `Change logged: ${summary}` }] };
121
+ });
122
+
123
+ // Tool 10: speclock_get_changes
124
+ server.tool("speclock_get_changes", "Get recent file changes tracked by SpecLock.", { limit: z.number().int().min(1).max(100).default(20) }, async ({ limit }) => {
125
+ ensureInit(PROJECT_ROOT);
126
+ const events = readEvents(PROJECT_ROOT);
127
+ const changes = events.filter((e) => ["file_created", "file_changed", "file_deleted", "manual_change"].includes(e.type)).slice(-limit);
128
+ return { content: [{ type: "text", text: changes.length ? JSON.stringify(changes, null, 2) : "No recent changes." }] };
129
+ });
130
+
131
+ // Tool 11: speclock_get_events
132
+ server.tool("speclock_get_events", "Get the event log, optionally filtered by type.", { type: z.string().optional(), limit: z.number().int().min(1).max(200).default(50), since: z.string().optional() }, async ({ type, limit, since }) => {
133
+ ensureInit(PROJECT_ROOT);
134
+ let events = readEvents(PROJECT_ROOT);
135
+ if (type) events = events.filter((e) => e.type === type);
136
+ if (since) events = events.filter((e) => e.ts > since);
137
+ events = events.slice(-limit);
138
+ return { content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No matching events." }] };
139
+ });
140
+
141
+ // Tool 12: speclock_check_conflict
142
+ server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
143
+ ensureInit(PROJECT_ROOT);
144
+ const result = checkConflict(PROJECT_ROOT, proposedAction);
145
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
146
+ });
147
+
148
+ // Tool 13: speclock_session_briefing
149
+ server.tool("speclock_session_briefing", "Start a new session and get a full briefing.", { toolName: z.enum(["claude-code", "cursor", "codex", "windsurf", "cline", "unknown"]).default("unknown") }, async ({ toolName }) => {
150
+ ensureInit(PROJECT_ROOT);
151
+ const briefing = getSessionBriefing(PROJECT_ROOT, toolName);
152
+ return { content: [{ type: "text", text: briefing }] };
153
+ });
154
+
155
+ // Tool 14: speclock_session_summary
156
+ server.tool("speclock_session_summary", "End the current session and record what was accomplished.", { summary: z.string().min(1) }, async ({ summary }) => {
157
+ ensureInit(PROJECT_ROOT);
158
+ endSession(PROJECT_ROOT, summary);
159
+ return { content: [{ type: "text", text: `Session ended. Summary recorded: ${summary}` }] };
160
+ });
161
+
162
+ // Tool 15: speclock_checkpoint
163
+ server.tool("speclock_checkpoint", "Create a named git tag checkpoint for easy rollback.", { name: z.string().min(1) }, async ({ name }) => {
164
+ ensureInit(PROJECT_ROOT);
165
+ const tag = `speclock-${name}`;
166
+ try {
167
+ createTag(PROJECT_ROOT, tag);
168
+ appendEvent(PROJECT_ROOT, { type: "checkpoint_created", tag });
169
+ return { content: [{ type: "text", text: `Checkpoint created: ${tag}` }] };
170
+ } catch (err) {
171
+ return { content: [{ type: "text", text: `Checkpoint failed: ${err.message}` }] };
172
+ }
173
+ });
174
+
175
+ // Tool 16: speclock_repo_status
176
+ server.tool("speclock_repo_status", "Get current git repository status.", {}, async () => {
177
+ ensureInit(PROJECT_ROOT);
178
+ try {
179
+ const status = captureStatus(PROJECT_ROOT);
180
+ const diff = getDiffSummary(PROJECT_ROOT);
181
+ return { content: [{ type: "text", text: JSON.stringify({ ...status, diffSummary: diff }, null, 2) }] };
182
+ } catch (err) {
183
+ return { content: [{ type: "text", text: `Git status failed: ${err.message}` }] };
184
+ }
185
+ });
186
+
187
+ // Tool 17: speclock_suggest_locks
188
+ server.tool("speclock_suggest_locks", "AI-powered lock suggestions based on project patterns.", {}, async () => {
189
+ ensureInit(PROJECT_ROOT);
190
+ const suggestions = suggestLocks(PROJECT_ROOT);
191
+ if (!suggestions.length) return { content: [{ type: "text", text: "No lock suggestions at this time." }] };
192
+ const text = suggestions.map((s, i) => `${i + 1}. **${s.text}**\n Source: ${s.source}\n Reason: ${s.reason}`).join("\n\n");
193
+ return { content: [{ type: "text", text: `## Lock Suggestions\n\n${text}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
194
+ });
195
+
196
+ // Tool 18: speclock_detect_drift
197
+ server.tool("speclock_detect_drift", "Scan recent changes for constraint violations.", {}, async () => {
198
+ ensureInit(PROJECT_ROOT);
199
+ const drift = detectDrift(PROJECT_ROOT);
200
+ if (!drift.length) return { content: [{ type: "text", text: "No drift detected. All changes align with active locks." }] };
201
+ const text = drift.map((d, i) => `${i + 1}. **${d.type}**: ${d.summary}\n Lock: ${d.lockText}\n Confidence: ${d.confidence}`).join("\n\n");
202
+ return { content: [{ type: "text", text: `## Drift Detected\n\n${text}` }] };
203
+ });
204
+
205
+ // Tool 19: speclock_health
206
+ server.tool("speclock_health", "Health check with completeness score and multi-agent timeline.", {}, async () => {
207
+ ensureInit(PROJECT_ROOT);
208
+ const brain = readBrain(PROJECT_ROOT);
209
+ const events = readEvents(PROJECT_ROOT);
210
+ let score = 0;
211
+ const checks = [];
212
+ if (brain.project.goal) { score += 20; checks.push("- [x] Goal set"); } else checks.push("- [ ] Goal missing");
213
+ if (brain.state.locks.filter(l => l.active).length > 0) { score += 20; checks.push("- [x] Locks defined"); } else checks.push("- [ ] No active locks");
214
+ if (brain.state.decisions.length > 0) { score += 15; checks.push("- [x] Decisions recorded"); } else checks.push("- [ ] No decisions");
215
+ if (brain.state.sessions.length > 0) { score += 15; checks.push("- [x] Sessions tracked"); } else checks.push("- [ ] No sessions");
216
+ if (brain.deploy?.provider) { score += 10; checks.push("- [x] Deploy facts set"); } else checks.push("- [ ] Deploy facts missing");
217
+ if (brain.state.notes.length > 0) { score += 10; checks.push("- [x] Notes present"); } else checks.push("- [ ] No notes");
218
+ if (events.length > 10) { score += 10; checks.push("- [x] Rich event history"); } else checks.push("- [ ] Limited events");
219
+ const grade = score >= 90 ? "A" : score >= 70 ? "B" : score >= 50 ? "C" : score >= 30 ? "D" : "F";
220
+ const sessions = brain.state.sessions.slice(-5);
221
+ const agentTimeline = sessions.length ? "\n\n### Recent Sessions\n" + sessions.map(s => `- **${s.tool || "unknown"}** @ ${s.startedAt}${s.summary ? ": " + s.summary : ""}`).join("\n") : "";
222
+ return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${brain.events.count} | Reverts: ${brain.state.reverts.length}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
223
+ });
224
+
225
+ return server;
226
+ }
227
+
228
+ // --- HTTP Server ---
229
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
230
+
231
+ app.post("/mcp", async (req, res) => {
232
+ const server = createSpecLockServer();
233
+ try {
234
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
235
+ await server.connect(transport);
236
+ await transport.handleRequest(req, res, req.body);
237
+ res.on("close", () => {
238
+ transport.close();
239
+ server.close();
240
+ });
241
+ } catch (error) {
242
+ console.error("Error handling MCP request:", error);
243
+ if (!res.headersSent) {
244
+ res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null });
245
+ }
246
+ }
247
+ });
248
+
249
+ app.get("/mcp", async (req, res) => {
250
+ res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
251
+ });
252
+
253
+ app.delete("/mcp", async (req, res) => {
254
+ res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
255
+ });
256
+
257
+ // Health check endpoint
258
+ app.get("/", (req, res) => {
259
+ res.json({
260
+ name: "speclock",
261
+ version: VERSION,
262
+ author: AUTHOR,
263
+ description: "AI Continuity Engine — Kill AI amnesia",
264
+ tools: 19,
265
+ mcp_endpoint: "/mcp",
266
+ npm: "https://www.npmjs.com/package/speclock",
267
+ github: "https://github.com/sgroy10/flowkeeper",
268
+ });
269
+ });
270
+
271
+ const PORT = parseInt(process.env.PORT || "3000", 10);
272
+ app.listen(PORT, "0.0.0.0", () => {
273
+ console.log(`SpecLock MCP HTTP Server v${VERSION} running on port ${PORT} — Developed by ${AUTHOR}`);
274
+ });
package/src/mcp/server.js CHANGED
@@ -721,10 +721,22 @@ server.tool(
721
721
  }
722
722
  );
723
723
 
724
- // --- Start server ---
725
- const transport = new StdioServerTransport();
726
- await server.connect(transport);
724
+ // --- Smithery sandbox export ---
725
+ export default function createSandboxServer() {
726
+ return server;
727
+ }
727
728
 
728
- process.stderr.write(
729
- `SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
730
- );
729
+ // --- Start server (skip when bundled as CJS for Smithery scanning) ---
730
+ const isScanMode = typeof import.meta.url === "undefined";
731
+
732
+ if (!isScanMode) {
733
+ const transport = new StdioServerTransport();
734
+ server.connect(transport).then(() => {
735
+ process.stderr.write(
736
+ `SpecLock MCP v${VERSION} running (stdio) — Developed by ${AUTHOR}. Root: ${PROJECT_ROOT}${os.EOL}`
737
+ );
738
+ }).catch((err) => {
739
+ process.stderr.write(`SpecLock fatal: ${err.message}${os.EOL}`);
740
+ process.exit(1);
741
+ });
742
+ }