+232
-8
@@ -11,2 +11,42 @@ #!/usr/bin/env node | ||
| // src/cli-args.ts | ||
| function parseCliIntent(argv) { | ||
| if (argv[0] === "init") return "init"; | ||
| for (const arg of argv) { | ||
| if (arg === "--version" || arg === "-v") return "version"; | ||
| if (arg === "--help" || arg === "-h") return "help"; | ||
| } | ||
| return "run"; | ||
| } | ||
| function helpText(version) { | ||
| return `seekstone ${version} \u2014 an Obsidian MCP server (filesystem-direct, low context-tax) | ||
| Seekstone runs as a Model Context Protocol stdio server. It is normally | ||
| launched by an MCP client (Claude Desktop, Claude Code), not run by hand. | ||
| Usage: | ||
| seekstone Start the MCP server (reads from stdin/stdout) | ||
| seekstone init Validate a vault and print/patch the Claude config | ||
| seekstone --version Print the version and exit | ||
| seekstone --help Print this help and exit | ||
| "seekstone init" options: | ||
| --vault <path> Vault to use (default: $SEEKSTONE_VAULT) | ||
| --client <name> desktop (default) | code | ||
| --write Patch the Claude Desktop config in place (backs it up first) | ||
| Required environment: | ||
| SEEKSTONE_VAULT Absolute path to your Obsidian vault | ||
| Optional environment: | ||
| SEEKSTONE_LOG_LEVEL error | warn | info (default) | debug | ||
| SEEKSTONE_LOG_FILE Absolute path; append JSON-line logs here | ||
| SEEKSTONE_WATCH_POLL Set to 1 to poll for changes (network drives, WSL) | ||
| Add to Claude Code: | ||
| claude mcp add seekstone --env SEEKSTONE_VAULT=/path/to/vault -- npx -y seekstone | ||
| Docs: https://github.com/shaqmughal/seekstone`; | ||
| } | ||
| // src/tools/append_note.ts | ||
@@ -229,4 +269,4 @@ import { readFile, writeFile } from "fs/promises"; | ||
| const results = []; | ||
| for (const [path, note] of ctx2.notes) { | ||
| if (input.folder && !path.startsWith(input.folder)) continue; | ||
| for (const [path2, note] of ctx2.notes) { | ||
| if (input.folder && !path2.startsWith(input.folder)) continue; | ||
| if (tag) { | ||
@@ -237,3 +277,3 @@ const noteTags = note.tags.split(" ").filter(Boolean); | ||
| results.push({ | ||
| path, | ||
| path: path2, | ||
| title: note.title, | ||
@@ -642,2 +682,164 @@ tags: note.tags.split(" ").filter(Boolean), | ||
| // src/init.ts | ||
| import { access as access3, copyFile, mkdir as mkdir3, readFile as readFile5, readdir, stat as stat2, writeFile as writeFile4 } from "fs/promises"; | ||
| import path, { dirname as dirname3, join as join8 } from "path"; | ||
| function parseInitArgs(argv) { | ||
| const opts = { write: false, client: "desktop" }; | ||
| for (let i = 0; i < argv.length; i++) { | ||
| const a = argv[i]; | ||
| if (a === "--write") opts.write = true; | ||
| else if (a === "--vault") opts.vault = argv[++i]; | ||
| else if (a === "--client") { | ||
| const v = argv[++i]; | ||
| if (v === "desktop" || v === "code") opts.client = v; | ||
| } | ||
| } | ||
| return opts; | ||
| } | ||
| function seekstoneServerConfig(vaultPath) { | ||
| return { | ||
| command: "npx", | ||
| args: ["-y", "seekstone"], | ||
| env: { SEEKSTONE_VAULT: vaultPath } | ||
| }; | ||
| } | ||
| function mcpAddCommand(vaultPath) { | ||
| return `claude mcp add seekstone --env SEEKSTONE_VAULT=${vaultPath} -- npx -y seekstone`; | ||
| } | ||
| function claudeDesktopConfigPath(platform, env) { | ||
| if (platform === "win32") { | ||
| const j2 = path.win32.join; | ||
| const base = env.appData ?? j2(env.home ?? "", "AppData", "Roaming"); | ||
| return j2(base, "Claude", "claude_desktop_config.json"); | ||
| } | ||
| const j = path.posix.join; | ||
| const home = env.home ?? ""; | ||
| if (platform === "darwin") { | ||
| return j(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"); | ||
| } | ||
| return j(home, ".config", "Claude", "claude_desktop_config.json"); | ||
| } | ||
| function mergeSeekstoneConfig(existing, vaultPath) { | ||
| const base = existing ? structuredClone(existing) : {}; | ||
| const servers = { ...base.mcpServers ?? {} }; | ||
| servers.seekstone = seekstoneServerConfig(vaultPath); | ||
| return { ...base, mcpServers: servers }; | ||
| } | ||
| async function validateVault(vaultPath) { | ||
| try { | ||
| const st = await stat2(vaultPath); | ||
| if (!st.isDirectory()) return { ok: false, error: `Not a directory: ${vaultPath}` }; | ||
| } catch { | ||
| return { ok: false, error: `Path does not exist: ${vaultPath}` }; | ||
| } | ||
| try { | ||
| await access3(join8(vaultPath, ".obsidian")); | ||
| } catch { | ||
| return { | ||
| ok: false, | ||
| error: `No ".obsidian" folder found in ${vaultPath} \u2014 this doesn't look like an Obsidian vault. Open the folder in Obsidian once to initialize it, or pass the correct --vault path.` | ||
| }; | ||
| } | ||
| return { ok: true, noteCount: await countMarkdown(vaultPath) }; | ||
| } | ||
| async function countMarkdown(root) { | ||
| let count = 0; | ||
| async function walk(dir) { | ||
| let entries; | ||
| try { | ||
| entries = await readdir(dir, { withFileTypes: true }); | ||
| } catch { | ||
| return; | ||
| } | ||
| for (const e of entries) { | ||
| if (e.name.startsWith(".")) continue; | ||
| const full = join8(dir, e.name); | ||
| if (e.isDirectory()) await walk(full); | ||
| else if (e.name.endsWith(".md")) count++; | ||
| } | ||
| } | ||
| await walk(root); | ||
| return count; | ||
| } | ||
| async function runInit(opts, deps) { | ||
| const vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT; | ||
| if (!vaultPath) { | ||
| return { | ||
| ok: false, | ||
| exitCode: 1, | ||
| output: [ | ||
| "No vault specified. Pass --vault <path> or set SEEKSTONE_VAULT.", | ||
| 'Example: seekstone init --vault "/Users/you/Obsidian/My Vault"' | ||
| ] | ||
| }; | ||
| } | ||
| const check = await validateVault(vaultPath); | ||
| if (!check.ok) { | ||
| return { ok: false, exitCode: 1, output: [`\u2717 ${check.error}`] }; | ||
| } | ||
| const out = [ | ||
| `\u2713 Vault looks good: ${vaultPath}`, | ||
| ` ${check.noteCount} markdown note${check.noteCount === 1 ? "" : "s"} found.`, | ||
| "" | ||
| ]; | ||
| if (opts.client === "code") { | ||
| out.push("Add to Claude Code by running:", "", ` ${mcpAddCommand(vaultPath)}`, ""); | ||
| return { ok: true, exitCode: 0, output: out }; | ||
| } | ||
| const configPath = claudeDesktopConfigPath(deps.platform, { | ||
| home: deps.env.HOME, | ||
| appData: deps.env.APPDATA | ||
| }); | ||
| const block = JSON.stringify( | ||
| { mcpServers: { seekstone: seekstoneServerConfig(vaultPath) } }, | ||
| null, | ||
| 2 | ||
| ); | ||
| if (!opts.write) { | ||
| out.push( | ||
| "Add this to your Claude Desktop config:", | ||
| ` ${configPath}`, | ||
| "", | ||
| block, | ||
| "", | ||
| "Then restart Claude Desktop. Or re-run with --write to patch it automatically." | ||
| ); | ||
| return { ok: true, exitCode: 0, output: out }; | ||
| } | ||
| let existing = null; | ||
| let existingRaw = null; | ||
| try { | ||
| existingRaw = await readFile5(configPath, "utf8"); | ||
| existing = JSON.parse(existingRaw); | ||
| } catch (err) { | ||
| if (existingRaw !== null) { | ||
| return { | ||
| ok: false, | ||
| exitCode: 1, | ||
| output: [ | ||
| `\u2717 ${configPath} exists but is not valid JSON; not modifying it.`, | ||
| ` ${err instanceof Error ? err.message : String(err)}` | ||
| ] | ||
| }; | ||
| } | ||
| existing = null; | ||
| } | ||
| const merged = mergeSeekstoneConfig(existing, vaultPath); | ||
| let backupPath; | ||
| await mkdir3(dirname3(configPath), { recursive: true }); | ||
| if (existingRaw !== null) { | ||
| backupPath = `${configPath}.backup-${deps.timestamp}`; | ||
| await copyFile(configPath, backupPath); | ||
| } | ||
| await writeFile4(configPath, `${JSON.stringify(merged, null, 2)} | ||
| `, "utf8"); | ||
| out.push( | ||
| `\u2713 Patched ${configPath}`, | ||
| backupPath ? ` Backup saved to ${backupPath}` : " (created a new config file)", | ||
| "", | ||
| "Restart Claude Desktop to load seekstone." | ||
| ); | ||
| return { ok: true, exitCode: 0, output: out, wrotePath: configPath, backupPath }; | ||
| } | ||
| // src/log.ts | ||
@@ -704,3 +906,3 @@ import { appendFileSync, renameSync, statSync } from "fs"; | ||
| } | ||
| function writeFile4(record) { | ||
| function writeFile5(record) { | ||
| if (!fileOk || filePath === void 0) return; | ||
@@ -727,3 +929,3 @@ const line = `${JSON.stringify(record)} | ||
| `); | ||
| writeFile4({ t: ts, level: lvl, msg, ...f }); | ||
| writeFile5({ t: ts, level: lvl, msg, ...f }); | ||
| } | ||
@@ -740,4 +942,4 @@ return { | ||
| // src/watcher.ts | ||
| import { readFile as readFile5 } from "fs/promises"; | ||
| import { join as join8, relative as relative2, sep as sep2 } from "path"; | ||
| import { readFile as readFile6 } from "fs/promises"; | ||
| import { join as join9, relative as relative2, sep as sep2 } from "path"; | ||
| import chokidar from "chokidar"; | ||
@@ -749,3 +951,3 @@ function startWatcher(ctx2, log2, opts) { | ||
| try { | ||
| const raw = await readFile5(join8(ctx2.vaultRoot, relPath), "utf8"); | ||
| const raw = await readFile6(join9(ctx2.vaultRoot, relPath), "utf8"); | ||
| upsertDoc(ctx2, buildDoc(relPath, raw)); | ||
@@ -791,2 +993,24 @@ log2?.debug("index updated", { path: relPath, op }); | ||
| // src/index.ts | ||
| var VERSION = true ? "0.2.0" : "0.0.0-dev"; | ||
| var intent = parseCliIntent(process.argv.slice(2)); | ||
| if (intent === "version") { | ||
| process.stdout.write(`${VERSION} | ||
| `); | ||
| process.exit(0); | ||
| } | ||
| if (intent === "help") { | ||
| process.stdout.write(`${helpText(VERSION)} | ||
| `); | ||
| process.exit(0); | ||
| } | ||
| if (intent === "init") { | ||
| const result = await runInit(parseInitArgs(process.argv.slice(3)), { | ||
| env: process.env, | ||
| platform: process.platform, | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-") | ||
| }); | ||
| process.stdout.write(`${result.output.join("\n")} | ||
| `); | ||
| process.exit(result.exitCode); | ||
| } | ||
| var log = createLogger(); | ||
@@ -793,0 +1017,0 @@ var vaultRoot = process.env.SEEKSTONE_VAULT; |
+1
-1
| { | ||
| "name": "seekstone", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "Seekstone MCP server — filesystem-direct Obsidian vault access, low context-tax.", |
+10
-0
@@ -32,2 +32,12 @@ # seekstone | ||
| ### Guided setup | ||
| `seekstone init` validates your vault and prints the config to paste, or patches the Claude Desktop config for you (with a backup, leaving other MCP servers untouched): | ||
| ```bash | ||
| npx -y seekstone init --vault "/absolute/path/to/your/vault" # print config | ||
| npx -y seekstone init --vault "/absolute/path/to/your/vault" --write # patch Claude Desktop | ||
| npx -y seekstone init --vault "/absolute/path/to/your/vault" --client code # print Claude Code command | ||
| ``` | ||
| ## Configuration | ||
@@ -34,0 +44,0 @@ |
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
133223
21.88%1134
23.39%73
15.87%17
21.43%