lockstep-mcp 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/dist/cli.js ADDED
@@ -0,0 +1,367 @@
1
+ import { installMcpEntry, uninstallMcpEntry, getInstallStatus } from "./install.js";
2
+ import { getAutopilotPrompts, getPlannerPrompt, getImplementerPrompt } from "./prompts.js";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ // Read version from package.json
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const packageJsonPath = path.resolve(__dirname, "../package.json");
10
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
11
+ const VERSION = packageJson.version;
12
+ function parseArgs(argv) {
13
+ const args = {};
14
+ const positional = [];
15
+ for (let i = 0; i < argv.length; i += 1) {
16
+ const token = argv[i];
17
+ if (token.startsWith("--")) {
18
+ const value = argv[i + 1];
19
+ if (!value || value.startsWith("--")) {
20
+ args[token] = true;
21
+ }
22
+ else {
23
+ args[token] = value;
24
+ i += 1;
25
+ }
26
+ }
27
+ else {
28
+ positional.push(token);
29
+ }
30
+ }
31
+ return { positional, args };
32
+ }
33
+ const LOCKSTEP_MARKER_START = "<!-- LOCKSTEP COORDINATION -->";
34
+ const LOCKSTEP_MARKER_END = "<!-- END LOCKSTEP -->";
35
+ const LOCKSTEP_INSTRUCTIONS = `${LOCKSTEP_MARKER_START}
36
+ This project uses lockstep-mcp for multi-agent coordination.
37
+
38
+ When starting a coordination session, call \`coordination_init\` with your role:
39
+ - role: "planner" - You'll plan tasks and create work items
40
+ - role: "implementer" - You'll claim and complete tasks
41
+
42
+ The planner will be asked to provide project context if not already set.
43
+ The implementer will see available tasks or be told to wait for the planner.
44
+
45
+ **If the user says "don't use lockstep" or "work independently", stop using lockstep tools and work normally.**
46
+
47
+ For more info: https://github.com/anthropics/lockstep-mcp
48
+ ${LOCKSTEP_MARKER_END}`;
49
+ function printHelp() {
50
+ const text = `lockstep-mcp v${VERSION} - Multi-agent coordination for Claude and Codex
51
+
52
+ Usage:
53
+ lockstep-mcp install [--claude] [--codex] [--all] [--config <path>] [--mode open|strict] [--roots <paths>] [--storage sqlite|json]
54
+ lockstep-mcp uninstall [--claude] [--codex] [--all] [--name <server-name>]
55
+ lockstep-mcp init [--force]
56
+ lockstep-mcp disable
57
+ lockstep-mcp enable
58
+ lockstep-mcp status
59
+ lockstep-mcp server [--mode open|strict] [--roots <paths>] [--storage sqlite|json] [--db-path <path>]
60
+ lockstep-mcp dashboard [--host <host>] [--port <port>] [--poll-ms <ms>]
61
+ lockstep-mcp tmux [--repo <path>] [--session <name>] [--layout windows|panes]
62
+ lockstep-mcp macos [--repo <path>]
63
+ lockstep-mcp prompts [--role planner|implementer]
64
+ lockstep-mcp version
65
+
66
+ Commands:
67
+ install Add lockstep-mcp to Claude and/or Codex MCP configs
68
+ uninstall Remove lockstep-mcp from configs
69
+ init Add coordination instructions to CLAUDE.md (creates if needed)
70
+ disable Remove coordination instructions from CLAUDE.md
71
+ enable Re-add coordination instructions to CLAUDE.md
72
+ status Show installation status
73
+ server Start the MCP server (called by Claude/Codex)
74
+ dashboard Start the web dashboard
75
+ tmux Launch Claude + Codex in tmux
76
+ macos Launch Claude + Codex in macOS Terminal
77
+ version Show version number
78
+
79
+ Examples:
80
+ lockstep-mcp install --all # Install for both Claude and Codex
81
+ lockstep-mcp install --codex --mode strict # Install for Codex only
82
+ lockstep-mcp init # Add instructions to current project
83
+ lockstep-mcp status # Check installation status
84
+ lockstep-mcp --version # Show version
85
+ `;
86
+ process.stdout.write(text);
87
+ }
88
+ function findInstructionsFile() {
89
+ // Look for existing files in order of preference
90
+ const candidates = ["CLAUDE.md", "AGENTS.md"];
91
+ for (const candidate of candidates) {
92
+ const fullPath = path.resolve(process.cwd(), candidate);
93
+ if (fs.existsSync(fullPath))
94
+ return fullPath;
95
+ }
96
+ // Default to CLAUDE.md
97
+ return path.resolve(process.cwd(), "CLAUDE.md");
98
+ }
99
+ function initProject(force) {
100
+ const filePath = findInstructionsFile();
101
+ const fileName = path.basename(filePath);
102
+ if (fs.existsSync(filePath)) {
103
+ const content = fs.readFileSync(filePath, "utf8");
104
+ // Check if already has lockstep instructions
105
+ if (content.includes(LOCKSTEP_MARKER_START)) {
106
+ if (!force) {
107
+ return { file: filePath, action: "already_exists" };
108
+ }
109
+ // Remove existing section and re-add
110
+ const newContent = removeLockstepSection(content);
111
+ fs.writeFileSync(filePath, newContent + "\n\n" + LOCKSTEP_INSTRUCTIONS + "\n");
112
+ return { file: filePath, action: "updated" };
113
+ }
114
+ // Append to existing file
115
+ fs.writeFileSync(filePath, content.trimEnd() + "\n\n" + LOCKSTEP_INSTRUCTIONS + "\n");
116
+ return { file: filePath, action: "appended" };
117
+ }
118
+ // Create new file
119
+ fs.writeFileSync(filePath, `# Project Instructions\n\n${LOCKSTEP_INSTRUCTIONS}\n`);
120
+ return { file: filePath, action: "created" };
121
+ }
122
+ function removeLockstepSection(content) {
123
+ const startIdx = content.indexOf(LOCKSTEP_MARKER_START);
124
+ if (startIdx === -1)
125
+ return content;
126
+ const endIdx = content.indexOf(LOCKSTEP_MARKER_END);
127
+ if (endIdx === -1)
128
+ return content;
129
+ const before = content.slice(0, startIdx).trimEnd();
130
+ const after = content.slice(endIdx + LOCKSTEP_MARKER_END.length).trimStart();
131
+ return before + (after ? "\n\n" + after : "");
132
+ }
133
+ function disableProject() {
134
+ const filePath = findInstructionsFile();
135
+ if (!fs.existsSync(filePath)) {
136
+ return { file: filePath, action: "not_found" };
137
+ }
138
+ const content = fs.readFileSync(filePath, "utf8");
139
+ if (!content.includes(LOCKSTEP_MARKER_START)) {
140
+ return { file: filePath, action: "not_enabled" };
141
+ }
142
+ const newContent = removeLockstepSection(content);
143
+ fs.writeFileSync(filePath, newContent.trimEnd() + "\n");
144
+ return { file: filePath, action: "disabled" };
145
+ }
146
+ function enableProject() {
147
+ return initProject(false);
148
+ }
149
+ function getProjectStatus() {
150
+ const candidates = ["CLAUDE.md", "AGENTS.md"];
151
+ for (const candidate of candidates) {
152
+ const fullPath = path.resolve(process.cwd(), candidate);
153
+ if (fs.existsSync(fullPath)) {
154
+ const content = fs.readFileSync(fullPath, "utf8");
155
+ if (content.includes(LOCKSTEP_MARKER_START)) {
156
+ return { enabled: true, file: fullPath };
157
+ }
158
+ }
159
+ }
160
+ return { enabled: false, file: null };
161
+ }
162
+ async function main() {
163
+ const { positional, args } = parseArgs(process.argv.slice(2));
164
+ const command = positional[0] ?? "server";
165
+ if (command === "help" || command === "--help" || command === "-h") {
166
+ printHelp();
167
+ return;
168
+ }
169
+ if (command === "version" || command === "--version" || command === "-v" || args["--version"]) {
170
+ process.stdout.write(`lockstep-mcp v${VERSION}\n`);
171
+ return;
172
+ }
173
+ if (command === "install") {
174
+ // Determine target
175
+ let target = "config";
176
+ if (args["--all"])
177
+ target = "all";
178
+ else if (args["--claude"])
179
+ target = "claude";
180
+ else if (args["--codex"])
181
+ target = "codex";
182
+ else if (args["--config"])
183
+ target = "config";
184
+ else
185
+ target = "all"; // Default to all
186
+ const result = installMcpEntry({
187
+ target,
188
+ configPath: typeof args["--config"] === "string" ? args["--config"] : undefined,
189
+ name: typeof args["--name"] === "string" ? args["--name"] : undefined,
190
+ mode: typeof args["--mode"] === "string" ? args["--mode"] : "open",
191
+ roots: typeof args["--roots"] === "string" ? args["--roots"] : undefined,
192
+ storage: typeof args["--storage"] === "string" ? args["--storage"] : "sqlite",
193
+ dbPath: typeof args["--db-path"] === "string" ? args["--db-path"] : undefined,
194
+ dataDir: typeof args["--data-dir"] === "string" ? args["--data-dir"] : undefined,
195
+ logDir: typeof args["--log-dir"] === "string" ? args["--log-dir"] : undefined,
196
+ commandMode: typeof args["--command-mode"] === "string" ? args["--command-mode"] : undefined,
197
+ commandAllow: typeof args["--command-allow"] === "string" ? args["--command-allow"] : undefined,
198
+ });
199
+ if ("results" in result && result.results) {
200
+ for (const r of result.results) {
201
+ process.stdout.write(`✓ Installed lockstep to ${r.target}: ${r.configPath}\n`);
202
+ }
203
+ process.stdout.write(`\nNext step: Run 'lockstep-mcp init' in your project directory to enable coordination.\n`);
204
+ }
205
+ else if ("configPath" in result) {
206
+ process.stdout.write(`Wrote MCP server entry "${result.name}" to ${result.configPath}\n`);
207
+ }
208
+ return;
209
+ }
210
+ if (command === "uninstall") {
211
+ let target = "all";
212
+ if (args["--claude"])
213
+ target = "claude";
214
+ else if (args["--codex"])
215
+ target = "codex";
216
+ const result = uninstallMcpEntry({
217
+ target,
218
+ name: typeof args["--name"] === "string" ? args["--name"] : undefined,
219
+ configPath: typeof args["--config"] === "string" ? args["--config"] : undefined,
220
+ });
221
+ for (const r of result.results) {
222
+ if (r.removed) {
223
+ process.stdout.write(`✓ Removed lockstep from ${r.target}\n`);
224
+ }
225
+ else {
226
+ process.stdout.write(`- lockstep not found in ${r.target}\n`);
227
+ }
228
+ }
229
+ return;
230
+ }
231
+ if (command === "init") {
232
+ const force = !!args["--force"];
233
+ const result = initProject(force);
234
+ if (result.action === "already_exists") {
235
+ process.stdout.write(`Lockstep instructions already exist in ${result.file}\n`);
236
+ process.stdout.write(`Use --force to update them.\n`);
237
+ }
238
+ else if (result.action === "created") {
239
+ process.stdout.write(`✓ Created ${result.file} with lockstep instructions\n`);
240
+ }
241
+ else if (result.action === "appended") {
242
+ process.stdout.write(`✓ Added lockstep instructions to ${result.file}\n`);
243
+ }
244
+ else if (result.action === "updated") {
245
+ process.stdout.write(`✓ Updated lockstep instructions in ${result.file}\n`);
246
+ }
247
+ return;
248
+ }
249
+ if (command === "disable") {
250
+ const result = disableProject();
251
+ if (result.action === "not_found") {
252
+ process.stdout.write(`No CLAUDE.md or AGENTS.md found in current directory.\n`);
253
+ }
254
+ else if (result.action === "not_enabled") {
255
+ process.stdout.write(`Lockstep instructions not found in ${result.file}\n`);
256
+ }
257
+ else {
258
+ process.stdout.write(`✓ Removed lockstep instructions from ${result.file}\n`);
259
+ }
260
+ return;
261
+ }
262
+ if (command === "enable") {
263
+ const result = enableProject();
264
+ if (result.action === "already_exists") {
265
+ process.stdout.write(`Lockstep already enabled in ${result.file}\n`);
266
+ }
267
+ else {
268
+ process.stdout.write(`✓ Enabled lockstep in ${result.file}\n`);
269
+ }
270
+ return;
271
+ }
272
+ if (command === "status") {
273
+ const installStatus = getInstallStatus();
274
+ const projectStatus = getProjectStatus();
275
+ process.stdout.write(`\nLockstep MCP Status\n`);
276
+ process.stdout.write(`${"─".repeat(50)}\n\n`);
277
+ process.stdout.write(`Global Installation:\n`);
278
+ process.stdout.write(` Claude: ${installStatus.claude ? "✓ Installed" : "✗ Not installed"}\n`);
279
+ process.stdout.write(` ${installStatus.claudePath}\n`);
280
+ process.stdout.write(` Codex: ${installStatus.codex ? "✓ Installed" : "✗ Not installed"}\n`);
281
+ process.stdout.write(` ${installStatus.codexPath}\n\n`);
282
+ process.stdout.write(`Current Project (${process.cwd()}):\n`);
283
+ if (projectStatus.enabled) {
284
+ process.stdout.write(` Coordination: ✓ Enabled\n`);
285
+ process.stdout.write(` Instructions: ${projectStatus.file}\n`);
286
+ }
287
+ else {
288
+ process.stdout.write(` Coordination: ✗ Not enabled\n`);
289
+ process.stdout.write(` Run 'lockstep-mcp init' to enable.\n`);
290
+ }
291
+ process.stdout.write(`\n`);
292
+ return;
293
+ }
294
+ if (command === "prompts") {
295
+ const role = typeof args["--role"] === "string" ? args["--role"] : undefined;
296
+ if (role === "planner") {
297
+ process.stdout.write(getPlannerPrompt());
298
+ return;
299
+ }
300
+ if (role === "implementer") {
301
+ process.stdout.write(getImplementerPrompt());
302
+ return;
303
+ }
304
+ process.stdout.write(getAutopilotPrompts());
305
+ return;
306
+ }
307
+ if (command === "dashboard") {
308
+ const { startDashboard } = await import("./dashboard.js");
309
+ const port = typeof args["--port"] === "string" ? Number(args["--port"]) : undefined;
310
+ const host = typeof args["--host"] === "string" ? args["--host"] : undefined;
311
+ const pollMs = typeof args["--poll-ms"] === "string" ? Number(args["--poll-ms"]) : undefined;
312
+ await startDashboard({ port, host, pollMs });
313
+ return;
314
+ }
315
+ if (command === "tmux") {
316
+ const { launchTmux } = await import("./tmux.js");
317
+ const repo = typeof args["--repo"] === "string" ? args["--repo"] : undefined;
318
+ const session = typeof args["--session"] === "string" ? args["--session"] : undefined;
319
+ const claudeCmd = typeof args["--claude-cmd"] === "string" ? args["--claude-cmd"] : undefined;
320
+ const codexCmd = typeof args["--codex-cmd"] === "string" ? args["--codex-cmd"] : undefined;
321
+ const injectPrompts = args["--no-prompts"] ? false : true;
322
+ const layout = typeof args["--layout"] === "string" ? args["--layout"] : undefined;
323
+ const split = typeof args["--split"] === "string" ? args["--split"] : undefined;
324
+ const showDashboard = args["--no-dashboard"] ? false : true;
325
+ const dashboardHost = typeof args["--dashboard-host"] === "string" ? args["--dashboard-host"] : "127.0.0.1";
326
+ const dashboardPort = typeof args["--dashboard-port"] === "string" ? Number(args["--dashboard-port"]) : 8787;
327
+ const cliPath = path.resolve(fileURLToPath(import.meta.url));
328
+ const nodePath = process.execPath;
329
+ const dashboardArgs = ["dashboard", "--host", dashboardHost, "--port", String(dashboardPort)];
330
+ const dashboardCmd = cliPath.endsWith(".ts")
331
+ ? `${nodePath} --import tsx ${cliPath} ${dashboardArgs.join(" ")}`
332
+ : `${nodePath} ${cliPath} ${dashboardArgs.join(" ")}`;
333
+ await launchTmux({
334
+ repo,
335
+ session,
336
+ claudeCmd,
337
+ codexCmd,
338
+ injectPrompts,
339
+ layout: layout === "panes" ? "panes" : "windows",
340
+ split: split === "horizontal" ? "horizontal" : "vertical",
341
+ dashboard: showDashboard,
342
+ dashboardCmd,
343
+ });
344
+ return;
345
+ }
346
+ if (command === "macos") {
347
+ const { launchMacos } = await import("./macos.js");
348
+ const repo = typeof args["--repo"] === "string" ? args["--repo"] : undefined;
349
+ const claudeCmd = typeof args["--claude-cmd"] === "string" ? args["--claude-cmd"] : undefined;
350
+ const codexCmd = typeof args["--codex-cmd"] === "string" ? args["--codex-cmd"] : undefined;
351
+ const dashboardHost = typeof args["--dashboard-host"] === "string" ? args["--dashboard-host"] : "127.0.0.1";
352
+ const dashboardPort = typeof args["--dashboard-port"] === "string" ? Number(args["--dashboard-port"]) : 8787;
353
+ await launchMacos({ repo, claudeCmd, codexCmd, dashboardHost, dashboardPort });
354
+ return;
355
+ }
356
+ if (command === "server") {
357
+ const { startServer } = await import("./server.js");
358
+ await startServer();
359
+ return;
360
+ }
361
+ throw new Error(`Unknown command: ${command}`);
362
+ }
363
+ main().catch((error) => {
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ process.stderr.write(`${message}\n`);
366
+ process.exit(1);
367
+ });
package/dist/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { expandHome, normalizeRoots } from "./utils.js";
3
+ const DEFAULT_ROOT = process.cwd();
4
+ function parseArgValue(args, key) {
5
+ const idx = args.indexOf(key);
6
+ if (idx === -1 || idx + 1 >= args.length)
7
+ return undefined;
8
+ return args[idx + 1];
9
+ }
10
+ export function loadConfig() {
11
+ const args = process.argv.slice(2);
12
+ const serverName = parseArgValue(args, "--server-name") || process.env.COORD_SERVER_NAME || "lockstep-mcp";
13
+ const serverVersion = parseArgValue(args, "--server-version") || process.env.COORD_SERVER_VERSION || "0.1.0";
14
+ const mode = (parseArgValue(args, "--mode") || process.env.COORD_MODE || "open");
15
+ const rootsRaw = parseArgValue(args, "--roots") || process.env.COORD_ROOTS || DEFAULT_ROOT;
16
+ const roots = normalizeRoots(rootsRaw
17
+ .split(",")
18
+ .map((root) => root.trim())
19
+ .filter(Boolean));
20
+ const dataDirRaw = parseArgValue(args, "--data-dir") || process.env.COORD_DATA_DIR || "~/.lockstep-mcp/data";
21
+ const logDirRaw = parseArgValue(args, "--log-dir") || process.env.COORD_LOG_DIR || "~/.lockstep-mcp/logs";
22
+ const dataDir = path.resolve(expandHome(dataDirRaw));
23
+ const logDir = path.resolve(expandHome(logDirRaw));
24
+ const storage = (parseArgValue(args, "--storage") || process.env.COORD_STORAGE || "sqlite");
25
+ const dbPathRaw = parseArgValue(args, "--db-path") || process.env.COORD_DB_PATH || path.join(dataDir, "coordinator.db");
26
+ const commandMode = (parseArgValue(args, "--command-mode") ||
27
+ process.env.COORD_COMMAND_MODE ||
28
+ "open");
29
+ const commandAllowRaw = parseArgValue(args, "--command-allow") || process.env.COORD_COMMAND_ALLOW || "";
30
+ const commandAllow = commandAllowRaw
31
+ .split(",")
32
+ .map((cmd) => cmd.trim())
33
+ .filter(Boolean);
34
+ return {
35
+ serverName,
36
+ serverVersion,
37
+ dataDir,
38
+ logDir,
39
+ storage: storage === "json" ? "json" : "sqlite",
40
+ dbPath: path.resolve(expandHome(dbPathRaw)),
41
+ mode: mode === "strict" ? "strict" : "open",
42
+ roots,
43
+ command: {
44
+ mode: commandMode === "allowlist" ? "allowlist" : "open",
45
+ allow: commandAllow,
46
+ },
47
+ };
48
+ }