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.
@@ -0,0 +1,252 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import os from "node:os";
5
+ function expandHome(input) {
6
+ if (!input.startsWith("~"))
7
+ return input;
8
+ const home = os.homedir();
9
+ return path.join(home, input.slice(1));
10
+ }
11
+ function getClaudeConfigPath() {
12
+ // Claude Code uses project-level .mcp.json or we create in home
13
+ const localConfig = path.resolve(process.cwd(), ".mcp.json");
14
+ if (fs.existsSync(localConfig))
15
+ return localConfig;
16
+ return localConfig; // Default to creating in current directory
17
+ }
18
+ function getCodexConfigPath() {
19
+ return path.join(os.homedir(), ".codex", "config.toml");
20
+ }
21
+ function resolveConfigPath(configPath) {
22
+ if (configPath)
23
+ return path.resolve(expandHome(configPath));
24
+ const localConfig = path.resolve(process.cwd(), ".mcp.json");
25
+ if (fs.existsSync(localConfig))
26
+ return localConfig;
27
+ return undefined;
28
+ }
29
+ function loadJsonConfig(configPath) {
30
+ if (!fs.existsSync(configPath)) {
31
+ return { mcpServers: {} };
32
+ }
33
+ const raw = fs.readFileSync(configPath, "utf8");
34
+ const parsed = JSON.parse(raw);
35
+ if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") {
36
+ parsed.mcpServers = {};
37
+ }
38
+ return parsed;
39
+ }
40
+ function resolveServerEntry() {
41
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
42
+ const distCli = path.join(repoRoot, "dist", "cli.js");
43
+ const srcCli = path.join(repoRoot, "src", "cli.ts");
44
+ if (fs.existsSync(distCli)) {
45
+ return { command: "node", args: [distCli, "server"], distCli };
46
+ }
47
+ return { command: "node", args: ["--import", "tsx", srcCli, "server"], distCli };
48
+ }
49
+ function getNodePath() {
50
+ // Try to find node in common locations
51
+ const candidates = [
52
+ "/opt/homebrew/bin/node",
53
+ "/usr/local/bin/node",
54
+ "/usr/bin/node",
55
+ process.execPath,
56
+ ];
57
+ for (const candidate of candidates) {
58
+ if (fs.existsSync(candidate))
59
+ return candidate;
60
+ }
61
+ return "node";
62
+ }
63
+ function buildServerArgs(options) {
64
+ const entry = resolveServerEntry();
65
+ const args = [...entry.args];
66
+ if (options.mode) {
67
+ args.push("--mode", options.mode);
68
+ }
69
+ if (options.roots) {
70
+ args.push("--roots", options.roots);
71
+ }
72
+ if (options.dataDir) {
73
+ args.push("--data-dir", options.dataDir);
74
+ }
75
+ if (options.logDir) {
76
+ args.push("--log-dir", options.logDir);
77
+ }
78
+ if (options.storage) {
79
+ args.push("--storage", options.storage);
80
+ }
81
+ if (options.dbPath) {
82
+ args.push("--db-path", options.dbPath);
83
+ }
84
+ if (options.commandMode) {
85
+ args.push("--command-mode", options.commandMode);
86
+ }
87
+ if (options.commandAllow) {
88
+ args.push("--command-allow", options.commandAllow);
89
+ }
90
+ return args;
91
+ }
92
+ // Install to Claude's .mcp.json
93
+ function installToClaude(options) {
94
+ const configPath = options.configPath ? expandHome(options.configPath) : getClaudeConfigPath();
95
+ const config = loadJsonConfig(configPath);
96
+ const entryName = options.name ?? "lockstep";
97
+ const entry = resolveServerEntry();
98
+ const args = buildServerArgs(options);
99
+ config.mcpServers[entryName] = {
100
+ command: getNodePath(),
101
+ args: args.slice(1), // Remove 'node' from args since command is node
102
+ };
103
+ // Ensure directory exists
104
+ const dir = path.dirname(configPath);
105
+ if (!fs.existsSync(dir)) {
106
+ fs.mkdirSync(dir, { recursive: true });
107
+ }
108
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
109
+ return { configPath, name: entryName };
110
+ }
111
+ // Install to Codex's config.toml
112
+ function installToCodex(options) {
113
+ const configPath = getCodexConfigPath();
114
+ const entryName = options.name ?? "lockstep";
115
+ const entry = resolveServerEntry();
116
+ const args = buildServerArgs(options);
117
+ // Ensure directory exists
118
+ const dir = path.dirname(configPath);
119
+ if (!fs.existsSync(dir)) {
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ }
122
+ // Read existing config or create new
123
+ let existingContent = "";
124
+ if (fs.existsSync(configPath)) {
125
+ existingContent = fs.readFileSync(configPath, "utf8");
126
+ }
127
+ // Check if entry already exists
128
+ const sectionRegex = new RegExp(`\\[mcp_servers\\.${entryName}\\]`);
129
+ if (sectionRegex.test(existingContent)) {
130
+ // Update existing entry - remove old section first
131
+ const sectionStart = existingContent.search(sectionRegex);
132
+ const nextSectionMatch = existingContent.slice(sectionStart + 1).search(/\n\[/);
133
+ const sectionEnd = nextSectionMatch === -1
134
+ ? existingContent.length
135
+ : sectionStart + 1 + nextSectionMatch;
136
+ existingContent = existingContent.slice(0, sectionStart) + existingContent.slice(sectionEnd);
137
+ }
138
+ // Build TOML entry
139
+ const nodePath = getNodePath();
140
+ const argsWithoutNode = args.slice(1); // Remove 'node' since command handles it
141
+ const argsToml = argsWithoutNode.map(a => `"${a}"`).join(", ");
142
+ const tomlEntry = `
143
+ [mcp_servers.${entryName}]
144
+ command = "${nodePath}"
145
+ args = [${argsToml}]
146
+ env = { }
147
+ `;
148
+ // Append to config
149
+ const newContent = existingContent.trim() + "\n" + tomlEntry;
150
+ fs.writeFileSync(configPath, newContent);
151
+ return { configPath, name: entryName };
152
+ }
153
+ // Uninstall from Claude's .mcp.json
154
+ function uninstallFromClaude(name, configPath) {
155
+ const fullPath = configPath ? expandHome(configPath) : getClaudeConfigPath();
156
+ if (!fs.existsSync(fullPath))
157
+ return false;
158
+ const config = loadJsonConfig(fullPath);
159
+ if (!config.mcpServers[name])
160
+ return false;
161
+ delete config.mcpServers[name];
162
+ fs.writeFileSync(fullPath, JSON.stringify(config, null, 2));
163
+ return true;
164
+ }
165
+ // Uninstall from Codex's config.toml
166
+ function uninstallFromCodex(name) {
167
+ const configPath = getCodexConfigPath();
168
+ if (!fs.existsSync(configPath))
169
+ return false;
170
+ let content = fs.readFileSync(configPath, "utf8");
171
+ const sectionRegex = new RegExp(`\\[mcp_servers\\.${name}\\]`);
172
+ if (!sectionRegex.test(content))
173
+ return false;
174
+ // Remove the section
175
+ const sectionStart = content.search(sectionRegex);
176
+ const nextSectionMatch = content.slice(sectionStart + 1).search(/\n\[/);
177
+ const sectionEnd = nextSectionMatch === -1
178
+ ? content.length
179
+ : sectionStart + 1 + nextSectionMatch;
180
+ content = content.slice(0, sectionStart) + content.slice(sectionEnd);
181
+ fs.writeFileSync(configPath, content.trim() + "\n");
182
+ return true;
183
+ }
184
+ export function installMcpEntry(options) {
185
+ const target = options.target ?? "config";
186
+ const results = [];
187
+ if (target === "config") {
188
+ // Legacy behavior: install to specified config path
189
+ const configPath = resolveConfigPath(options.configPath);
190
+ if (!configPath) {
191
+ throw new Error("Missing --config. Provide the MCP config path (or run from a repo with .mcp.json).");
192
+ }
193
+ const config = loadJsonConfig(configPath);
194
+ const entryName = options.name ?? "lockstep-mcp";
195
+ const entry = resolveServerEntry();
196
+ const args = buildServerArgs(options);
197
+ config.mcpServers[entryName] = {
198
+ command: entry.command,
199
+ args,
200
+ };
201
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
202
+ return { configPath, name: entryName, serverPath: args.join(" ") };
203
+ }
204
+ if (target === "claude" || target === "all") {
205
+ const result = installToClaude(options);
206
+ results.push({ target: "claude", ...result });
207
+ }
208
+ if (target === "codex" || target === "all") {
209
+ const result = installToCodex(options);
210
+ results.push({ target: "codex", ...result });
211
+ }
212
+ return { results };
213
+ }
214
+ export function uninstallMcpEntry(options) {
215
+ const target = options.target ?? "all";
216
+ const name = options.name ?? "lockstep";
217
+ const results = [];
218
+ if (target === "claude" || target === "all") {
219
+ const removed = uninstallFromClaude(name, options.configPath);
220
+ results.push({ target: "claude", removed });
221
+ }
222
+ if (target === "codex" || target === "all") {
223
+ const removed = uninstallFromCodex(name);
224
+ results.push({ target: "codex", removed });
225
+ }
226
+ return { results };
227
+ }
228
+ export function getInstallStatus() {
229
+ const claudePath = getClaudeConfigPath();
230
+ const codexPath = getCodexConfigPath();
231
+ let claudeInstalled = false;
232
+ let codexInstalled = false;
233
+ if (fs.existsSync(claudePath)) {
234
+ try {
235
+ const config = loadJsonConfig(claudePath);
236
+ claudeInstalled = !!config.mcpServers["lockstep"] || !!config.mcpServers["lockstep-mcp"];
237
+ }
238
+ catch {
239
+ // ignore
240
+ }
241
+ }
242
+ if (fs.existsSync(codexPath)) {
243
+ try {
244
+ const content = fs.readFileSync(codexPath, "utf8");
245
+ codexInstalled = /\[mcp_servers\.lockstep/.test(content);
246
+ }
247
+ catch {
248
+ // ignore
249
+ }
250
+ }
251
+ return { claude: claudeInstalled, codex: codexInstalled, claudePath, codexPath };
252
+ }
package/dist/macos.js ADDED
@@ -0,0 +1,55 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { sleep } from "./utils.js";
5
+ export async function launchMacos(options = {}) {
6
+ if (process.platform !== "darwin") {
7
+ throw new Error("macos launcher is only supported on macOS");
8
+ }
9
+ const repo = path.resolve(options.repo ?? process.cwd());
10
+ const claudeCmd = options.claudeCmd ?? "claude";
11
+ const codexCmd = options.codexCmd ?? "codex";
12
+ const dashboardHost = options.dashboardHost ?? "127.0.0.1";
13
+ const dashboardPort = options.dashboardPort ?? 8787;
14
+ const macosPath = path.resolve(fileURLToPath(import.meta.url));
15
+ const baseDir = path.dirname(macosPath);
16
+ const nodePath = process.execPath;
17
+ const cliPath = macosPath.endsWith(".ts")
18
+ ? path.join(baseDir, "cli.ts")
19
+ : path.join(baseDir, "cli.js");
20
+ const dashboardArgs = ["dashboard", "--host", dashboardHost, "--port", String(dashboardPort)];
21
+ const dashboardCmd = cliPath.endsWith(".ts")
22
+ ? `${nodePath} --import tsx ${cliPath} ${dashboardArgs.join(" ")}`
23
+ : `${nodePath} ${cliPath} ${dashboardArgs.join(" ")}`;
24
+ const commands = [
25
+ `cd "${repo}" && ${claudeCmd}`,
26
+ `cd "${repo}" && ${codexCmd}`,
27
+ dashboardCmd,
28
+ ];
29
+ for (const command of commands) {
30
+ const openResult = spawnSync("open", ["-na", "Terminal"]);
31
+ if (openResult.status !== 0) {
32
+ throw new Error("Failed to open Terminal window");
33
+ }
34
+ const escaped = command.replace(/\"/g, "\\\"");
35
+ const script = `tell application \"Terminal\" to do script \"${escaped}\"`;
36
+ let success = false;
37
+ for (let attempt = 0; attempt < 5; attempt += 1) {
38
+ await sleep(500);
39
+ if (process.env.LOCKSTEP_DEBUG) {
40
+ process.stderr.write(`osascript: ${script}\n`);
41
+ }
42
+ const result = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
43
+ if (result.status === 0) {
44
+ success = true;
45
+ break;
46
+ }
47
+ if (result.stderr) {
48
+ process.stderr.write(result.stderr);
49
+ }
50
+ }
51
+ if (!success) {
52
+ throw new Error("Failed to run command in Terminal window");
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,173 @@
1
+ export function getPlannerPrompt() {
2
+ return `You are the PLANNER for this lockstep coordination session.
3
+
4
+ ⛔ ABSOLUTE PROHIBITIONS - VIOLATING THESE IS A CRITICAL FAILURE:
5
+ - NEVER use file write/edit/update tools - you are NOT allowed to modify files
6
+ - NEVER run build commands (pnpm build, npm build, tsc, etc.)
7
+ - NEVER run test commands (pnpm test, npm test, vitest, jest, etc.)
8
+ - NEVER fix code errors yourself - CREATE A TASK for an implementer
9
+ - NEVER create/modify source files (.ts, .tsx, .js, .jsx, .swift, etc.)
10
+ - If you catch yourself about to edit a file - STOP and create a task instead
11
+
12
+ YOUR ONLY ALLOWED ACTIONS:
13
+ 1. Call lockstep_mcp tools (coordination_init, task_create, task_list, etc.)
14
+ 2. Read files to understand the codebase (READ ONLY, never write)
15
+ 3. Communicate with the user
16
+ 4. Launch implementers with launch_implementer
17
+ 5. Review and approve/reject tasks submitted by implementers
18
+
19
+ If you see a bug, build error, or code issue:
20
+ → DO NOT FIX IT YOURSELF
21
+ → CREATE A TASK with task_create and assign appropriate complexity
22
+ → LAUNCH AN IMPLEMENTER if none are active
23
+
24
+ TASK COMPLEXITY - Set appropriately when creating tasks:
25
+ - SIMPLE: 1-2 files, obvious fix, no architectural decisions
26
+ - MEDIUM: 3-5 files, some ambiguity, needs verification
27
+ - COMPLEX: 6+ files, architectural decisions, cross-system impact
28
+ - CRITICAL: Database schema, security, affects other products (REQUIRES your approval)
29
+
30
+ TASK ISOLATION - Choose based on task nature:
31
+ - SHARED (default): Implementer works in main directory with file locks. Good for simple/medium tasks.
32
+ - WORKTREE: Implementer gets isolated git worktree with own branch. Use for:
33
+ - Complex refactoring that touches many files
34
+ - Parallel independent features
35
+ - Changes that might conflict with other implementers
36
+ - When you want clean git history per feature
37
+
38
+ When using worktree isolation:
39
+ - Use worktree_status to check implementer's progress (commits, changes)
40
+ - Use worktree_merge to merge their changes after approval
41
+ - If merge has conflicts, use task_request_changes to have implementer resolve
42
+
43
+ INITIALIZATION:
44
+ 1. Call coordination_init({ role: "planner" }) to check project state
45
+ 2. Follow the instructions in the response EXACTLY
46
+
47
+ PHASE 1 (gather_info):
48
+ If no project context exists:
49
+ 1. ASK: "What project or task are we working on today?"
50
+ 2. EXPLORE: Read README.md, package.json, CLAUDE.md to understand the codebase
51
+ 3. SUMMARIZE: Tell user what you found about the project
52
+ 4. ASK CLARIFYING QUESTIONS for anything missing:
53
+ - What is the desired end state/goal?
54
+ - Any specific requirements or constraints?
55
+ - What are the acceptance criteria?
56
+ - What tests should pass?
57
+ - What type of implementer - Claude or Codex?
58
+ 5. SAVE: Call project_context_set with combined info
59
+
60
+ PHASE 2 (create_plan):
61
+ - Create implementation plan based on user's answers
62
+ - EXPLAIN the plan to the user (steps, reasoning, trade-offs)
63
+ - ASK for feedback: "Any additional context or changes needed?"
64
+ - ASK for permission: "Do I have your permission to proceed?"
65
+ - ONLY AFTER user approves: call project_context_set with implementationPlan
66
+ - Set status to "ready"
67
+
68
+ PHASE 3 (create_tasks):
69
+ - Create tasks using task_create with COMPLEXITY field (required!)
70
+ - Use launch_implementer to spawn workers (type based on user's preference)
71
+ - 1-2 implementers for simple projects, more for complex
72
+
73
+ PHASE 4 (monitor and review):
74
+ ⚠️ FIRST: Check implementer_list - if NO active implementers, call launch_implementer IMMEDIATELY
75
+ - Check task_list FREQUENTLY - look for tasks in "review" status
76
+ - Check note_list for [REVIEW] notifications
77
+ - Check discussion_inbox({ agent: "planner" }) for discussions
78
+ - If tasks exist but no implementers are working, LAUNCH AN IMPLEMENTER
79
+
80
+ REVIEWING TASKS (critical responsibility):
81
+ When a task is in "review" status:
82
+ 1. Read the task's reviewNotes to see what the implementer did
83
+ 2. Consider: Does this fit the big picture? Will it work with other changes?
84
+ 3. If good: task_approve({ id: "task-id", feedback: "optional notes" })
85
+ 4. If needs work: task_request_changes({ id: "task-id", feedback: "what to fix" })
86
+
87
+ COORDINATION RESPONSIBILITIES:
88
+ - When implementers start COMPLEX/CRITICAL tasks, they should discuss with you first
89
+ - Respond promptly to discussion_inbox items
90
+ - Verify changes won't conflict with other implementers' work
91
+ - Keep the big picture in mind - individual tasks must fit together
92
+
93
+ DISCUSSIONS:
94
+ When you need implementer input on architectural/implementation decisions:
95
+ - discussion_start({ topic, message, author: "planner", waitingOn: "impl-1" })
96
+ - Check discussion_inbox periodically for replies
97
+ - discussion_resolve when a decision is reached
98
+
99
+ Use project_status_set with "complete" when ALL work is done, "stopped" to halt`;
100
+ }
101
+ export function getImplementerPrompt() {
102
+ return `You are an IMPLEMENTER for this lockstep coordination session.
103
+
104
+ INITIALIZATION:
105
+ 1. Call coordination_init({ role: "implementer" }) to get your name and instructions
106
+ 2. Follow the continuous work loop
107
+
108
+ TASK COMPLEXITY PROTOCOL:
109
+ When you claim a task, check its complexity field and follow the appropriate protocol:
110
+
111
+ | Complexity | Before Starting | While Working | On Completion |
112
+ |------------|-----------------|---------------|---------------|
113
+ | SIMPLE | Start immediately | Work independently | Mark done directly |
114
+ | MEDIUM | Brief review of approach | Work, note concerns | Submit for review |
115
+ | COMPLEX | Discuss approach with planner | Checkpoint mid-task | Submit for review, await approval |
116
+ | CRITICAL | MUST get planner approval first | Verify each step | Submit for review, WAIT for approval |
117
+
118
+ CONTINUOUS WORK LOOP:
119
+ 1. Call task_list to see available tasks and check projectStatus
120
+ 2. Call discussion_inbox({ agent: "YOUR_NAME" }) to check for discussions/feedback
121
+ 3. If projectStatus is "stopped" or "complete" -> STOP working
122
+ 4. If discussions waiting on you -> respond with discussion_reply
123
+ 5. Check if any tasks in "review" status got feedback from planner
124
+ 6. If tasks available, call task_claim to take a "todo" task
125
+ 7. Read the complexity and follow the protocol above
126
+ 8. Call lock_acquire before editing any file
127
+ 9. Do the work
128
+ 10. Call lock_release when done with file
129
+ 11. Based on complexity:
130
+ - SIMPLE: task_update to mark "done"
131
+ - MEDIUM/COMPLEX/CRITICAL: task_submit_for_review with notes on what you did
132
+ 12. REPEAT from step 1
133
+
134
+ WHEN TO DISCUSS WITH PLANNER:
135
+ - ALWAYS for critical tasks before starting
136
+ - When the task description is ambiguous
137
+ - When you discover the scope is larger than expected
138
+ - When your changes might affect other parts of the system
139
+ - When you're unsure about architectural decisions
140
+
141
+ HOW TO SUBMIT FOR REVIEW:
142
+ task_submit_for_review({
143
+ id: "task-id",
144
+ owner: "your-name",
145
+ reviewNotes: "Summary: modified X files. Approach: used Y pattern. Notes: ..."
146
+ })
147
+
148
+ WORKTREE MODE:
149
+ If you are told you're working in a worktree (isolated branch):
150
+ - Your changes are on your own branch, not affecting others
151
+ - You don't need file locks (lock_acquire/lock_release) - you have full isolation
152
+ - Commit your changes frequently with clear messages
153
+ - When done, submit_for_review - planner will use worktree_merge to merge your changes
154
+ - If there are merge conflicts, planner will request changes for you to resolve
155
+
156
+ IMPORTANT:
157
+ - Keep working until all tasks are done or project is stopped
158
+ - Do NOT wait for user input between tasks
159
+ - For complex/critical tasks, coordination with planner is REQUIRED`;
160
+ }
161
+ export function getAutopilotPrompts() {
162
+ return `Lockstep MCP Coordination Prompts
163
+ =====================================
164
+
165
+ PLANNER PROMPT:
166
+ ${getPlannerPrompt()}
167
+
168
+ =====================================
169
+
170
+ IMPLEMENTER PROMPT:
171
+ ${getImplementerPrompt()}
172
+ `;
173
+ }