Skip to content

Commit 67d2ca1

Browse files
supernormxlclaude
andcommitted
feat: add 'walnut setup' auto-configure command
Detects installed AI platforms (Hermes, Claude Desktop, Cursor, Windsurf) and configures walnut MCP server for each. Scaffolds ~/world/ if missing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d9b735 commit 67d2ca1

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
#!/usr/bin/env node
22

3+
// Check for setup subcommand before starting MCP server
4+
if (process.argv.includes("setup")) {
5+
const { runSetup } = await import("./setup.js");
6+
await runSetup();
7+
process.exit(0);
8+
}
9+
310
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
411
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
512
import { z } from "zod";

src/setup.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
2+
import { join, dirname } from "node:path";
3+
import { homedir } from "node:os";
4+
import { fileURLToPath } from "node:url";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = dirname(__filename);
8+
9+
export async function runSetup() {
10+
const home = homedir();
11+
console.log("\n🐿️ walnut setup\n");
12+
13+
const configured: string[] = [];
14+
const skipped: string[] = [];
15+
16+
// 1. Detect and configure Hermes
17+
const hermesConfig = join(home, ".hermes", "config.yaml");
18+
if (existsSync(hermesConfig)) {
19+
const content = readFileSync(hermesConfig, "utf-8");
20+
if (content.includes("walnut-mcp") || content.includes("@lock-in-lab/walnut")) {
21+
skipped.push("Hermes (already configured)");
22+
} else {
23+
// Append MCP server config
24+
const addition = `\n# Walnut context management\n walnut:\n command: "npx"\n args: ["@lock-in-lab/walnut"]\n`;
25+
if (content.includes("mcp_servers:")) {
26+
// Add under existing mcp_servers block
27+
const updated = content.replace("mcp_servers:", "mcp_servers:" + addition);
28+
writeFileSync(hermesConfig, updated);
29+
} else {
30+
// Add new mcp_servers block
31+
writeFileSync(hermesConfig, content + "\nmcp_servers:" + addition);
32+
}
33+
// Copy Hermes skill
34+
const skillDir = join(home, ".hermes", "skills", "walnuts");
35+
const skillSrc = join(__dirname, "..", "skills", "walnuts", "SKILL.md");
36+
if (existsSync(skillSrc)) {
37+
mkdirSync(skillDir, { recursive: true });
38+
copyFileSync(skillSrc, join(skillDir, "SKILL.md"));
39+
}
40+
configured.push("Hermes (config.yaml + skill copied)");
41+
}
42+
}
43+
44+
// 2. Detect and configure Claude Desktop
45+
const claudeDesktopConfig = join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
46+
if (existsSync(claudeDesktopConfig)) {
47+
try {
48+
const content = JSON.parse(readFileSync(claudeDesktopConfig, "utf-8"));
49+
if (content.mcpServers?.walnut) {
50+
skipped.push("Claude Desktop (already configured)");
51+
} else {
52+
content.mcpServers = content.mcpServers || {};
53+
content.mcpServers.walnut = {
54+
command: "npx",
55+
args: ["@lock-in-lab/walnut"]
56+
};
57+
writeFileSync(claudeDesktopConfig, JSON.stringify(content, null, 2));
58+
configured.push("Claude Desktop");
59+
}
60+
} catch {
61+
skipped.push("Claude Desktop (config parse error)");
62+
}
63+
}
64+
65+
// 3. Detect and configure Cursor
66+
const cursorConfig = join(home, ".cursor", "mcp.json");
67+
if (existsSync(cursorConfig) || existsSync(join(home, ".cursor"))) {
68+
try {
69+
const content = existsSync(cursorConfig)
70+
? JSON.parse(readFileSync(cursorConfig, "utf-8"))
71+
: { mcpServers: {} };
72+
if (content.mcpServers?.walnut) {
73+
skipped.push("Cursor (already configured)");
74+
} else {
75+
content.mcpServers = content.mcpServers || {};
76+
content.mcpServers.walnut = {
77+
command: "npx",
78+
args: ["@lock-in-lab/walnut"]
79+
};
80+
mkdirSync(join(home, ".cursor"), { recursive: true });
81+
writeFileSync(cursorConfig, JSON.stringify(content, null, 2));
82+
configured.push("Cursor");
83+
}
84+
} catch {
85+
skipped.push("Cursor (config parse error)");
86+
}
87+
}
88+
89+
// 4. Detect and configure Windsurf
90+
const windsurfConfig = join(home, ".codeium", "windsurf", "mcp_config.json");
91+
if (existsSync(join(home, ".codeium", "windsurf")) || existsSync(windsurfConfig)) {
92+
try {
93+
const content = existsSync(windsurfConfig)
94+
? JSON.parse(readFileSync(windsurfConfig, "utf-8"))
95+
: { mcpServers: {} };
96+
if (content.mcpServers?.walnut) {
97+
skipped.push("Windsurf (already configured)");
98+
} else {
99+
content.mcpServers = content.mcpServers || {};
100+
content.mcpServers.walnut = {
101+
command: "npx",
102+
args: ["@lock-in-lab/walnut"]
103+
};
104+
mkdirSync(join(home, ".codeium", "windsurf"), { recursive: true });
105+
writeFileSync(windsurfConfig, JSON.stringify(content, null, 2));
106+
configured.push("Windsurf");
107+
}
108+
} catch {
109+
skipped.push("Windsurf (config parse error)");
110+
}
111+
}
112+
113+
// 5. Scaffold world if it doesn't exist
114+
const worldPath = join(home, "world");
115+
if (!existsSync(worldPath)) {
116+
const domains = ["01_Archive", "02_Life", "03_Inputs", "04_Ventures", "05_Experiments"];
117+
for (const d of domains) {
118+
mkdirSync(join(worldPath, d), { recursive: true });
119+
}
120+
mkdirSync(join(worldPath, ".alive"), { recursive: true });
121+
mkdirSync(join(worldPath, "02_Life", "people"), { recursive: true });
122+
configured.push("World scaffolded at ~/world/");
123+
} else {
124+
skipped.push("World (already exists at ~/world/)");
125+
}
126+
127+
// Print summary
128+
console.log(" Configured:");
129+
if (configured.length === 0) {
130+
console.log(" (nothing new — everything was already set up)");
131+
} else {
132+
for (const c of configured) {
133+
console.log(` ✓ ${c}`);
134+
}
135+
}
136+
137+
if (skipped.length > 0) {
138+
console.log("\n Skipped:");
139+
for (const s of skipped) {
140+
console.log(` · ${s}`);
141+
}
142+
}
143+
144+
// Detect what wasn't found
145+
const notFound: string[] = [];
146+
if (!existsSync(hermesConfig)) notFound.push("Hermes (~/.hermes/config.yaml)");
147+
if (!existsSync(claudeDesktopConfig)) notFound.push("Claude Desktop");
148+
if (!existsSync(join(home, ".cursor"))) notFound.push("Cursor");
149+
if (!existsSync(join(home, ".codeium", "windsurf"))) notFound.push("Windsurf");
150+
151+
if (notFound.length > 0) {
152+
console.log("\n Not detected:");
153+
for (const n of notFound) {
154+
console.log(` - ${n}`);
155+
}
156+
}
157+
158+
console.log("\n Done. Your agents now have structured context.\n");
159+
console.log(" Next: open your AI tool and say \"what are my projects?\"\n");
160+
}

0 commit comments

Comments
 (0)