granite-mem 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +44 -4
  2. package/dist/index.js +1181 -46
  3. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
4
+ import { Command, InvalidArgumentError } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import fs2 from "fs";
8
- import path2 from "path";
7
+ import fs3 from "fs";
8
+ import path3 from "path";
9
9
 
10
10
  // src/core/config.ts
11
11
  import fs from "fs";
@@ -116,59 +116,76 @@ function loadConfig(vaultRoot) {
116
116
  return parsed;
117
117
  }
118
118
 
119
+ // src/core/vault.ts
120
+ import fs2 from "fs";
121
+ import os from "os";
122
+ import path2 from "path";
123
+ var DEFAULT_VAULT_DIRNAME = ".granite";
124
+ function hasVaultConfig(dir) {
125
+ return fs2.existsSync(path2.join(dir, CONFIG_FILENAME));
126
+ }
127
+ function getDefaultVaultRoot(homeDir = os.homedir()) {
128
+ return path2.resolve(homeDir, DEFAULT_VAULT_DIRNAME);
129
+ }
130
+ function findVaultRoot(from) {
131
+ let dir = from ? path2.resolve(from) : process.cwd();
132
+ while (true) {
133
+ if (hasVaultConfig(dir)) {
134
+ return dir;
135
+ }
136
+ const parent = path2.dirname(dir);
137
+ if (parent === dir) break;
138
+ dir = parent;
139
+ }
140
+ const defaultVaultRoot = getDefaultVaultRoot();
141
+ return hasVaultConfig(defaultVaultRoot) ? defaultVaultRoot : null;
142
+ }
143
+ function requireVaultRoot(from) {
144
+ const root = findVaultRoot(from);
145
+ if (!root) {
146
+ throw new Error(`No Granite vault found. Run "mem init" to create ${getDefaultVaultRoot()}.`);
147
+ }
148
+ return root;
149
+ }
150
+ function getIndexDbPath(vaultRoot) {
151
+ if (path2.basename(path2.resolve(vaultRoot)) === DEFAULT_VAULT_DIRNAME) {
152
+ return path2.join(vaultRoot, "index.db");
153
+ }
154
+ return path2.join(vaultRoot, ".granite", "index.db");
155
+ }
156
+
119
157
  // src/commands/init.ts
120
- function initVault(dir) {
121
- const targetDir = path2.resolve(dir);
122
- if (fs2.existsSync(path2.join(targetDir, CONFIG_FILENAME))) {
158
+ function initVault(dir = getDefaultVaultRoot()) {
159
+ const targetDir = path3.resolve(dir);
160
+ fs3.mkdirSync(targetDir, { recursive: true });
161
+ if (fs3.existsSync(path3.join(targetDir, CONFIG_FILENAME))) {
123
162
  console.error(`Vault already exists in ${targetDir}`);
124
163
  process.exit(1);
125
164
  }
126
165
  writeDefaultConfig(targetDir);
127
- fs2.mkdirSync(path2.join(targetDir, ".granite"), { recursive: true });
166
+ fs3.mkdirSync(path3.dirname(getIndexDbPath(targetDir)), { recursive: true });
128
167
  const config = loadConfig(targetDir);
129
168
  for (const typeConfig of Object.values(config.note_types)) {
130
- fs2.mkdirSync(path2.join(targetDir, typeConfig.folder), { recursive: true });
169
+ fs3.mkdirSync(path3.join(targetDir, typeConfig.folder), { recursive: true });
131
170
  }
132
171
  console.log(`Vault initialized in ${targetDir}`);
133
172
  console.log("");
134
173
  console.log("Created:");
135
174
  console.log(` ${CONFIG_FILENAME}`);
136
- console.log(" .granite/");
175
+ const indexDir = path3.relative(targetDir, path3.dirname(getIndexDbPath(targetDir)));
176
+ if (indexDir) {
177
+ console.log(` ${indexDir}/`);
178
+ }
137
179
  for (const [name, typeConfig] of Object.entries(config.note_types)) {
138
180
  console.log(` ${typeConfig.folder}/ (${name})`);
139
181
  }
140
182
  console.log("");
141
- console.log('Next: mem new fleeting "My first note"');
183
+ console.log('Next: mem new "My first note" --type fleeting');
142
184
  }
143
185
 
144
186
  // src/commands/new.ts
145
187
  import fs6 from "fs";
146
188
 
147
- // src/core/vault.ts
148
- import fs3 from "fs";
149
- import path3 from "path";
150
- function findVaultRoot(from) {
151
- let dir = from ? path3.resolve(from) : process.cwd();
152
- while (true) {
153
- if (fs3.existsSync(path3.join(dir, CONFIG_FILENAME))) {
154
- return dir;
155
- }
156
- const parent = path3.dirname(dir);
157
- if (parent === dir) return null;
158
- dir = parent;
159
- }
160
- }
161
- function requireVaultRoot(from) {
162
- const root = findVaultRoot(from);
163
- if (!root) {
164
- throw new Error('Not inside a Granite vault. Run "mem init" to create one.');
165
- }
166
- return root;
167
- }
168
- function getIndexDbPath(vaultRoot) {
169
- return path3.join(vaultRoot, ".granite", "index.db");
170
- }
171
-
172
189
  // src/core/note.ts
173
190
  import fs4 from "fs";
174
191
  import path4 from "path";
@@ -516,8 +533,8 @@ function countNoteFiles(vaultRoot, config) {
516
533
  }
517
534
  function upsertNoteAndLinks(db, note) {
518
535
  const upsertNote = db.prepare(`
519
- INSERT INTO notes (slug, id, title, type, created, modified, tags, aliases, body, filepath)
520
- VALUES (@slug, @id, @title, @type, @created, @modified, @tags, @aliases, @body, @filepath)
536
+ INSERT INTO notes (slug, id, title, type, created, modified, tags, aliases, body, filepath, status, source)
537
+ VALUES (@slug, @id, @title, @type, @created, @modified, @tags, @aliases, @body, @filepath, @status, @source)
521
538
  ON CONFLICT(slug) DO UPDATE SET
522
539
  id = excluded.id,
523
540
  title = excluded.title,
@@ -527,7 +544,9 @@ function upsertNoteAndLinks(db, note) {
527
544
  tags = excluded.tags,
528
545
  aliases = excluded.aliases,
529
546
  body = excluded.body,
530
- filepath = excluded.filepath
547
+ filepath = excluded.filepath,
548
+ status = excluded.status,
549
+ source = excluded.source
531
550
  `);
532
551
  const deleteLinks = db.prepare("DELETE FROM links WHERE source_slug = ?");
533
552
  const insertLink = db.prepare(`
@@ -545,7 +564,9 @@ function upsertNoteAndLinks(db, note) {
545
564
  tags: JSON.stringify(note.frontmatter.tags),
546
565
  aliases: JSON.stringify(note.frontmatter.aliases),
547
566
  body: note.body,
548
- filepath: note.filepath
567
+ filepath: note.filepath,
568
+ status: note.frontmatter.status ?? "active",
569
+ source: note.frontmatter.source ?? "human"
549
570
  });
550
571
  deleteLinks.run(note.slug);
551
572
  const links = parseWikilinks(note.body);
@@ -1337,7 +1358,8 @@ function showCommand(slug, options) {
1337
1358
  }
1338
1359
 
1339
1360
  // src/core/search.ts
1340
- function searchNotes(db, query) {
1361
+ function searchNotes(db, query, limit = 20) {
1362
+ const cappedLimit = Math.max(1, Math.min(limit, 100));
1341
1363
  const stmt = db.prepare(`
1342
1364
  SELECT
1343
1365
  n.slug,
@@ -1348,9 +1370,9 @@ function searchNotes(db, query) {
1348
1370
  JOIN notes n ON n.rowid = notes_fts.rowid
1349
1371
  WHERE notes_fts MATCH ?
1350
1372
  ORDER BY rank
1351
- LIMIT 20
1373
+ LIMIT ?
1352
1374
  `);
1353
- const rows = stmt.all(query);
1375
+ const rows = stmt.all(query, cappedLimit);
1354
1376
  return rows.map((r) => ({
1355
1377
  slug: r.slug,
1356
1378
  title: r.title,
@@ -1942,11 +1964,1111 @@ function printRecommendations(vaultRoot, config, filepath, strategy) {
1942
1964
  }
1943
1965
  }
1944
1966
 
1967
+ // src/commands/mcp.ts
1968
+ import path9 from "path";
1969
+
1970
+ // src/mcp/server.ts
1971
+ import { serve as serve2 } from "@hono/node-server";
1972
+ import { Hono as Hono2 } from "hono";
1973
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1974
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1975
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1976
+ import * as z from "zod/v4";
1977
+
1978
+ // src/version.ts
1979
+ var GRANITE_VERSION = "0.1.2";
1980
+
1981
+ // src/mcp/server.ts
1982
+ var readOnlyAnnotations = {
1983
+ readOnlyHint: true,
1984
+ destructiveHint: false,
1985
+ idempotentHint: true,
1986
+ openWorldHint: false
1987
+ };
1988
+ var writeAnnotations = {
1989
+ readOnlyHint: false,
1990
+ destructiveHint: false,
1991
+ idempotentHint: false,
1992
+ openWorldHint: false
1993
+ };
1994
+ var fieldDefinitionSchema = z.object({
1995
+ type: z.enum(["text", "date", "number", "boolean", "wikilink", "list", "enum"]),
1996
+ of: z.string().optional(),
1997
+ options: z.array(z.string()).optional(),
1998
+ required: z.boolean().optional(),
1999
+ default: z.string().optional(),
2000
+ description: z.string().optional()
2001
+ });
2002
+ var noteTypeInfoSchema = z.object({
2003
+ name: z.string(),
2004
+ description: z.string(),
2005
+ folder: z.string(),
2006
+ line_limit: z.number(),
2007
+ warn_only: z.boolean(),
2008
+ slug_format: z.enum(["title", "date"]),
2009
+ instructions: z.string().optional(),
2010
+ fields: z.record(z.string(), fieldDefinitionSchema).optional()
2011
+ });
2012
+ var noteSummarySchema = z.object({
2013
+ slug: z.string(),
2014
+ title: z.string(),
2015
+ type: z.string(),
2016
+ created: z.string(),
2017
+ modified: z.string(),
2018
+ tags: z.array(z.string()),
2019
+ aliases: z.array(z.string()),
2020
+ status: z.enum(["inbox", "active", "archived"]),
2021
+ source: z.enum(["human", "agent", "extraction"]),
2022
+ filepath: z.string(),
2023
+ resource_uri: z.string()
2024
+ });
2025
+ var wikiLinkSchema = z.object({
2026
+ raw: z.string(),
2027
+ target: z.string(),
2028
+ display: z.string(),
2029
+ resolved: z.boolean(),
2030
+ resolved_slug: z.string().optional()
2031
+ });
2032
+ var noteDetailsSchema = noteSummarySchema.extend({
2033
+ body: z.string(),
2034
+ frontmatter: z.record(z.string(), z.unknown()),
2035
+ outgoing_links: z.array(wikiLinkSchema)
2036
+ });
2037
+ var searchResultSchema = z.object({
2038
+ slug: z.string(),
2039
+ title: z.string(),
2040
+ snippet: z.string(),
2041
+ score: z.number()
2042
+ });
2043
+ var backlinkSchema = z.object({
2044
+ source_slug: z.string(),
2045
+ source_title: z.string(),
2046
+ context: z.string()
2047
+ });
2048
+ var linkSuggestionSchema = z.object({
2049
+ target_slug: z.string(),
2050
+ target_title: z.string(),
2051
+ mentions: z.number()
2052
+ });
2053
+ var recommendationSchema = z.object({
2054
+ additions: z.array(z.object({ text: z.string() })),
2055
+ links: z.array(z.object({
2056
+ slug: z.string(),
2057
+ title: z.string(),
2058
+ type: z.string(),
2059
+ reason: z.string(),
2060
+ source: z.enum(["mention", "search"])
2061
+ })),
2062
+ tags: z.array(z.object({
2063
+ tag: z.string(),
2064
+ weight: z.number(),
2065
+ source_slugs: z.array(z.string())
2066
+ })),
2067
+ next_steps: z.array(z.object({
2068
+ type: z.string(),
2069
+ title_hint: z.string().optional(),
2070
+ reason: z.string()
2071
+ }))
2072
+ });
2073
+ var doctorIssueSchema = z.object({
2074
+ level: z.enum(["error", "warning", "info"]),
2075
+ file: z.string(),
2076
+ message: z.string()
2077
+ });
2078
+ var vaultOverviewSchema = z.object({
2079
+ vault_root: z.string(),
2080
+ vault_name: z.string(),
2081
+ default_type: z.string(),
2082
+ auto_rebuild: z.boolean(),
2083
+ index_last_rebuild: z.string().optional(),
2084
+ note_count: z.number(),
2085
+ notes_by_type: z.record(z.string(), z.number()),
2086
+ recent_notes: z.array(noteSummarySchema)
2087
+ });
2088
+ function createGraniteMcpServer(runtime) {
2089
+ const server = new McpServer(
2090
+ {
2091
+ name: "granite",
2092
+ version: GRANITE_VERSION,
2093
+ title: "Granite MCP Server"
2094
+ },
2095
+ {
2096
+ capabilities: { logging: {} },
2097
+ instructions: [
2098
+ "Granite exposes a local-first markdown vault.",
2099
+ "Prefer read tools and resources first, then write with granite_create_note or granite_update_note.",
2100
+ "Resources are read-only views; notes live on disk and the SQLite index is derived state.",
2101
+ "Use granite_recommend_note_actions to preserve Granite\u2019s capture -> link -> recommend loop."
2102
+ ].join(" ")
2103
+ }
2104
+ );
2105
+ registerTools(server, runtime);
2106
+ registerResources(server, runtime);
2107
+ registerPrompts(server, runtime);
2108
+ return server;
2109
+ }
2110
+ async function startGraniteMcpStdioServer(runtime) {
2111
+ const server = createGraniteMcpServer(runtime);
2112
+ const transport = new StdioServerTransport();
2113
+ await server.connect(transport);
2114
+ console.error(`Granite MCP server listening on stdio for ${runtime.vaultRoot}`);
2115
+ }
2116
+ function startGraniteMcpHttpServer(runtime, options) {
2117
+ const app = createGraniteMcpHttpApp(runtime, options);
2118
+ console.error(`Granite MCP server listening on http://${options.host}:${options.port}/mcp`);
2119
+ console.error(`Health check: http://${options.host}:${options.port}/health`);
2120
+ console.error(`Vault: ${runtime.vaultRoot}`);
2121
+ serve2({
2122
+ fetch: app.fetch,
2123
+ hostname: options.host,
2124
+ port: options.port
2125
+ });
2126
+ }
2127
+ async function withResponseCleanup(response, cleanup) {
2128
+ let cleanedUp = false;
2129
+ const cleanupOnce = async () => {
2130
+ if (cleanedUp) {
2131
+ return;
2132
+ }
2133
+ cleanedUp = true;
2134
+ await cleanup();
2135
+ };
2136
+ if (!response.body) {
2137
+ await cleanupOnce();
2138
+ return response;
2139
+ }
2140
+ const reader = response.body.getReader();
2141
+ const wrappedBody = new ReadableStream({
2142
+ async pull(controller) {
2143
+ try {
2144
+ const { done, value } = await reader.read();
2145
+ if (done) {
2146
+ controller.close();
2147
+ await cleanupOnce();
2148
+ return;
2149
+ }
2150
+ controller.enqueue(value);
2151
+ } catch (error) {
2152
+ controller.error(error);
2153
+ await cleanupOnce();
2154
+ }
2155
+ },
2156
+ async cancel(reason) {
2157
+ try {
2158
+ await reader.cancel(reason);
2159
+ } finally {
2160
+ await cleanupOnce();
2161
+ }
2162
+ }
2163
+ });
2164
+ return new Response(wrappedBody, {
2165
+ status: response.status,
2166
+ statusText: response.statusText,
2167
+ headers: response.headers
2168
+ });
2169
+ }
2170
+ function registerTools(server, runtime) {
2171
+ server.registerTool("granite_get_vault_overview", {
2172
+ title: "Granite Vault Overview",
2173
+ description: "Summarize the current Granite vault, including counts by type and recent notes.",
2174
+ inputSchema: {
2175
+ recent_limit: z.number().int().min(1).max(20).optional().describe("How many recent notes to include. Defaults to 10.")
2176
+ },
2177
+ outputSchema: vaultOverviewSchema,
2178
+ annotations: readOnlyAnnotations
2179
+ }, async ({ recent_limit }) => {
2180
+ const overview = runtime.getVaultOverview(recent_limit ?? 10);
2181
+ return toolResult(
2182
+ overview,
2183
+ `Vault "${overview.vault_name}" with ${overview.note_count} notes.`
2184
+ );
2185
+ });
2186
+ server.registerTool("granite_list_note_types", {
2187
+ title: "Granite Note Types",
2188
+ description: "List the note types configured in the vault.",
2189
+ outputSchema: z.object({
2190
+ default_type: z.string(),
2191
+ note_types: z.array(noteTypeInfoSchema)
2192
+ }),
2193
+ annotations: readOnlyAnnotations
2194
+ }, async () => {
2195
+ const noteTypes = runtime.listNoteTypes();
2196
+ return toolResult({
2197
+ default_type: runtime.getDefaultNoteType(),
2198
+ note_types: noteTypes
2199
+ }, `Loaded ${noteTypes.length} Granite note types.`);
2200
+ });
2201
+ server.registerTool("granite_list_notes", {
2202
+ title: "List Granite Notes",
2203
+ description: "List notes from the Granite vault with optional filters.",
2204
+ inputSchema: {
2205
+ type: z.string().optional().describe("Filter by note type."),
2206
+ status: z.enum(["inbox", "active", "archived"]).optional().describe("Filter by note status."),
2207
+ source: z.enum(["human", "agent", "extraction"]).optional().describe("Filter by note source."),
2208
+ since: z.string().optional().describe("Only notes modified on or after this ISO or YYYY-MM-DD value."),
2209
+ limit: z.number().int().min(1).max(200).optional().describe("Maximum number of notes to return. Defaults to 25.")
2210
+ },
2211
+ outputSchema: z.object({
2212
+ notes: z.array(noteSummarySchema),
2213
+ total: z.number()
2214
+ }),
2215
+ annotations: readOnlyAnnotations
2216
+ }, async (args) => {
2217
+ const notes = runtime.listNotes(args);
2218
+ return toolResult({
2219
+ notes,
2220
+ total: notes.length
2221
+ }, `Returned ${notes.length} note(s).`);
2222
+ });
2223
+ server.registerTool("granite_get_note", {
2224
+ title: "Get Granite Note",
2225
+ description: "Read a Granite note by slug, including frontmatter, body, and resolved outgoing links.",
2226
+ inputSchema: {
2227
+ slug: z.string().describe("The note slug.")
2228
+ },
2229
+ outputSchema: noteDetailsSchema,
2230
+ annotations: readOnlyAnnotations
2231
+ }, async ({ slug }) => {
2232
+ const note = runtime.getNote(slug);
2233
+ return {
2234
+ ...toolResult(note, `Loaded "${note.title}" (${note.slug}).`),
2235
+ content: [
2236
+ { type: "text", text: `Loaded "${note.title}" (${note.slug}).` },
2237
+ createNoteResourceLink(note.title, note.resource_uri)
2238
+ ]
2239
+ };
2240
+ });
2241
+ server.registerTool("granite_search_notes", {
2242
+ title: "Search Granite Notes",
2243
+ description: "Run full-text search against the Granite index.",
2244
+ inputSchema: {
2245
+ query: z.string().describe("Full-text query for the SQLite FTS index."),
2246
+ limit: z.number().int().min(1).max(50).optional().describe("Maximum number of search results. Defaults to 10.")
2247
+ },
2248
+ outputSchema: z.object({
2249
+ query: z.string(),
2250
+ results: z.array(searchResultSchema)
2251
+ }),
2252
+ annotations: readOnlyAnnotations
2253
+ }, async ({ query, limit }) => {
2254
+ const results = runtime.search(query, limit ?? 10);
2255
+ return toolResult({
2256
+ query,
2257
+ results
2258
+ }, `Found ${results.length} result(s) for "${query}".`);
2259
+ });
2260
+ server.registerTool("granite_create_note", {
2261
+ title: "Create Granite Note",
2262
+ description: "Create a new note in the Granite vault.",
2263
+ inputSchema: {
2264
+ title: z.string().describe("Note title."),
2265
+ type: z.string().optional().describe("Note type. Defaults to the vault default type."),
2266
+ body: z.string().optional().describe("Optional note body. If omitted, Granite uses the type template."),
2267
+ tags: z.array(z.string()).optional().describe("Tags to add immediately."),
2268
+ aliases: z.array(z.string()).optional().describe("Aliases to add immediately."),
2269
+ status: z.enum(["inbox", "active", "archived"]).optional().describe("Initial note status."),
2270
+ source: z.enum(["human", "agent", "extraction"]).optional().describe("Initial note source.")
2271
+ },
2272
+ outputSchema: z.object({
2273
+ note: noteDetailsSchema,
2274
+ recommendations: recommendationSchema
2275
+ }),
2276
+ annotations: writeAnnotations
2277
+ }, async (args) => {
2278
+ const result = runtime.createNote(args);
2279
+ return toolResult(result, `Created "${result.note.title}" (${result.note.slug}).`);
2280
+ });
2281
+ server.registerTool("granite_capture_note", {
2282
+ title: "Capture Granite Note",
2283
+ description: "Quick-capture a note from free-form text, similar to mem add.",
2284
+ inputSchema: {
2285
+ text: z.string().describe("Raw capture text."),
2286
+ type: z.string().optional().describe("Optional note type override. Defaults to the vault default type."),
2287
+ tags: z.array(z.string()).optional().describe("Tags to add immediately."),
2288
+ aliases: z.array(z.string()).optional().describe("Aliases to add immediately."),
2289
+ status: z.enum(["inbox", "active", "archived"]).optional().describe("Initial note status."),
2290
+ source: z.enum(["human", "agent", "extraction"]).optional().describe("Initial note source.")
2291
+ },
2292
+ outputSchema: z.object({
2293
+ note: noteDetailsSchema,
2294
+ recommendations: recommendationSchema
2295
+ }),
2296
+ annotations: writeAnnotations
2297
+ }, async (args) => {
2298
+ const result = runtime.captureNote(args);
2299
+ return toolResult(result, `Captured "${result.note.title}" (${result.note.slug}).`);
2300
+ });
2301
+ server.registerTool("granite_update_note", {
2302
+ title: "Update Granite Note",
2303
+ description: "Update frontmatter or body fields for an existing Granite note.",
2304
+ inputSchema: {
2305
+ slug: z.string().describe("Slug of the note to update."),
2306
+ title: z.string().optional().describe("Replace the note title."),
2307
+ body: z.string().optional().describe("Replace the entire note body."),
2308
+ append: z.string().optional().describe("Append text to the existing note body."),
2309
+ tags: z.array(z.string()).optional().describe("Tags to add."),
2310
+ aliases: z.array(z.string()).optional().describe("Aliases to add."),
2311
+ status: z.enum(["inbox", "active", "archived"]).optional().describe("New note status."),
2312
+ source: z.enum(["human", "agent", "extraction"]).optional().describe("New note source.")
2313
+ },
2314
+ outputSchema: z.object({
2315
+ note: noteDetailsSchema,
2316
+ recommendations: recommendationSchema
2317
+ }),
2318
+ annotations: writeAnnotations
2319
+ }, async ({ slug, ...updates }) => {
2320
+ const result = runtime.updateNote(slug, updates);
2321
+ return toolResult(result, `Updated "${result.note.title}" (${result.note.slug}).`);
2322
+ });
2323
+ server.registerTool("granite_get_backlinks", {
2324
+ title: "Granite Backlinks",
2325
+ description: "List notes that link to a given Granite note.",
2326
+ inputSchema: {
2327
+ slug: z.string().describe("Slug of the target note.")
2328
+ },
2329
+ outputSchema: z.object({
2330
+ slug: z.string(),
2331
+ backlinks: z.array(backlinkSchema)
2332
+ }),
2333
+ annotations: readOnlyAnnotations
2334
+ }, async ({ slug }) => {
2335
+ const backlinks = runtime.getBacklinks(slug);
2336
+ return toolResult({
2337
+ slug,
2338
+ backlinks
2339
+ }, `Found ${backlinks.length} backlink(s) for "${slug}".`);
2340
+ });
2341
+ server.registerTool("granite_suggest_links", {
2342
+ title: "Suggest Granite Links",
2343
+ description: "Suggest missing wikilinks for a Granite note based on mentions.",
2344
+ inputSchema: {
2345
+ slug: z.string().describe("Slug of the note to inspect.")
2346
+ },
2347
+ outputSchema: z.object({
2348
+ slug: z.string(),
2349
+ suggestions: z.array(linkSuggestionSchema)
2350
+ }),
2351
+ annotations: readOnlyAnnotations
2352
+ }, async ({ slug }) => {
2353
+ const suggestions = runtime.suggestLinks(slug);
2354
+ return toolResult({
2355
+ slug,
2356
+ suggestions
2357
+ }, `Found ${suggestions.length} suggested link(s) for "${slug}".`);
2358
+ });
2359
+ server.registerTool("granite_recommend_note_actions", {
2360
+ title: "Recommend Granite Actions",
2361
+ description: "Recommend follow-up links, tags, additions, and next notes for a Granite note.",
2362
+ inputSchema: {
2363
+ slug: z.string().describe("Slug of the note to analyze.")
2364
+ },
2365
+ outputSchema: z.object({
2366
+ slug: z.string(),
2367
+ recommendations: recommendationSchema
2368
+ }),
2369
+ annotations: readOnlyAnnotations
2370
+ }, async ({ slug }) => {
2371
+ const recommendations = runtime.recommend(slug);
2372
+ return toolResult({
2373
+ slug,
2374
+ recommendations
2375
+ }, recommendationSummary(recommendations));
2376
+ });
2377
+ server.registerTool("granite_run_doctor", {
2378
+ title: "Run Granite Doctor",
2379
+ description: "Validate vault health and report structural issues.",
2380
+ outputSchema: z.object({
2381
+ issues: z.array(doctorIssueSchema),
2382
+ counts: z.object({
2383
+ errors: z.number(),
2384
+ warnings: z.number(),
2385
+ info: z.number()
2386
+ })
2387
+ }),
2388
+ annotations: readOnlyAnnotations
2389
+ }, async () => {
2390
+ const result = runtime.runDoctor();
2391
+ return toolResult(
2392
+ result,
2393
+ `${result.counts.errors} error(s), ${result.counts.warnings} warning(s), ${result.counts.info} info item(s).`
2394
+ );
2395
+ });
2396
+ }
2397
+ function registerResources(server, runtime) {
2398
+ server.registerResource("granite-vault-config", "granite://vault/config", {
2399
+ title: "Granite Config",
2400
+ description: "Raw granite.yml configuration for the current vault.",
2401
+ mimeType: "text/yaml"
2402
+ }, async () => ({
2403
+ contents: [{
2404
+ uri: "granite://vault/config",
2405
+ text: runtime.readVaultConfigRaw(),
2406
+ mimeType: "text/yaml"
2407
+ }]
2408
+ }));
2409
+ server.registerResource("granite-vault-overview", "granite://vault/overview", {
2410
+ title: "Granite Vault Overview",
2411
+ description: "Structured summary of the current vault.",
2412
+ mimeType: "application/json"
2413
+ }, async () => ({
2414
+ contents: [{
2415
+ uri: "granite://vault/overview",
2416
+ text: runtime.readVaultOverviewJson(),
2417
+ mimeType: "application/json"
2418
+ }]
2419
+ }));
2420
+ server.registerResource("granite-note-types", "granite://vault/types", {
2421
+ title: "Granite Note Types",
2422
+ description: "Structured list of the note types configured in the current vault.",
2423
+ mimeType: "application/json"
2424
+ }, async () => ({
2425
+ contents: [{
2426
+ uri: "granite://vault/types",
2427
+ text: runtime.readVaultTypesJson(),
2428
+ mimeType: "application/json"
2429
+ }]
2430
+ }));
2431
+ const noteTemplate = new ResourceTemplate("granite://notes/{slug}", {
2432
+ list: void 0,
2433
+ complete: {
2434
+ slug: async (value) => runtime.completeSlugs(value)
2435
+ }
2436
+ });
2437
+ server.registerResource("granite-note", noteTemplate, {
2438
+ title: "Granite Note",
2439
+ description: "Read a Granite note as markdown with YAML frontmatter.",
2440
+ mimeType: "text/markdown"
2441
+ }, async (uri, variables) => {
2442
+ const variable = variables.slug;
2443
+ const slug = Array.isArray(variable) ? variable[0] : variable;
2444
+ if (!slug) {
2445
+ throw new Error("Resource URI is missing the note slug.");
2446
+ }
2447
+ return {
2448
+ contents: [{
2449
+ uri: uri.toString(),
2450
+ text: runtime.readNoteMarkdown(decodeURIComponent(slug)),
2451
+ mimeType: "text/markdown"
2452
+ }]
2453
+ };
2454
+ });
2455
+ }
2456
+ function registerPrompts(server, runtime) {
2457
+ server.registerPrompt("granite_refine_note", {
2458
+ title: "Refine Granite Note",
2459
+ description: "Create a prompt for turning a note into a cleaner permanent note draft.",
2460
+ argsSchema: {
2461
+ slug: z.string().describe("Slug of the note to refine.")
2462
+ }
2463
+ }, async ({ slug }) => {
2464
+ const note = runtime.getNote(slug);
2465
+ return {
2466
+ description: `Refine ${note.slug} into a durable Granite note.`,
2467
+ messages: [
2468
+ {
2469
+ role: "user",
2470
+ content: {
2471
+ type: "text",
2472
+ text: [
2473
+ "Refine the attached Granite note into a durable, well-structured permanent note.",
2474
+ "Keep the meaning intact, avoid inventing facts, preserve useful wikilinks, and use Granite-style headings when appropriate."
2475
+ ].join(" ")
2476
+ }
2477
+ },
2478
+ {
2479
+ role: "user",
2480
+ content: {
2481
+ type: "resource",
2482
+ resource: {
2483
+ uri: note.resource_uri,
2484
+ text: runtime.readNoteMarkdown(slug),
2485
+ mimeType: "text/markdown"
2486
+ }
2487
+ }
2488
+ }
2489
+ ]
2490
+ };
2491
+ });
2492
+ server.registerPrompt("granite_review_connections", {
2493
+ title: "Review Granite Connections",
2494
+ description: "Create a prompt for improving note links, tags, and next steps.",
2495
+ argsSchema: {
2496
+ slug: z.string().describe("Slug of the note to inspect.")
2497
+ }
2498
+ }, async ({ slug }) => {
2499
+ const note = runtime.getNote(slug);
2500
+ const backlinks = runtime.getBacklinks(slug);
2501
+ const recommendations = runtime.recommend(slug);
2502
+ return {
2503
+ description: `Review the connections around ${note.slug}.`,
2504
+ messages: [
2505
+ {
2506
+ role: "user",
2507
+ content: {
2508
+ type: "text",
2509
+ text: [
2510
+ "Review this Granite note and propose better links, tags, and the most useful follow-up note.",
2511
+ "Prefer concrete suggestions grounded in the vault context below."
2512
+ ].join(" ")
2513
+ }
2514
+ },
2515
+ {
2516
+ role: "user",
2517
+ content: {
2518
+ type: "resource",
2519
+ resource: {
2520
+ uri: note.resource_uri,
2521
+ text: runtime.readNoteMarkdown(slug),
2522
+ mimeType: "text/markdown"
2523
+ }
2524
+ }
2525
+ },
2526
+ {
2527
+ role: "user",
2528
+ content: {
2529
+ type: "text",
2530
+ text: JSON.stringify({ backlinks, recommendations }, null, 2)
2531
+ }
2532
+ }
2533
+ ]
2534
+ };
2535
+ });
2536
+ }
2537
+ function toolResult(structuredContent, summary) {
2538
+ return {
2539
+ content: [{ type: "text", text: summary }],
2540
+ structuredContent: asStructuredContent(structuredContent)
2541
+ };
2542
+ }
2543
+ function createNoteResourceLink(name, uri) {
2544
+ return {
2545
+ type: "resource_link",
2546
+ name,
2547
+ uri,
2548
+ mimeType: "text/markdown",
2549
+ description: "Read the markdown resource for this note."
2550
+ };
2551
+ }
2552
+ function recommendationSummary(recommendations) {
2553
+ return [
2554
+ `${recommendations.links.length} link suggestion(s)`,
2555
+ `${recommendations.tags.length} tag suggestion(s)`,
2556
+ `${recommendations.next_steps.length} next-step suggestion(s)`
2557
+ ].join(", ");
2558
+ }
2559
+ function asStructuredContent(value) {
2560
+ return JSON.parse(JSON.stringify(value));
2561
+ }
2562
+ function createGraniteMcpHttpApp(runtime, options) {
2563
+ const app = new Hono2();
2564
+ const allowedHosts = buildAllowedHosts(options.host, options.port);
2565
+ const allowedOrigins = buildAllowedOrigins(options.host, options.port, options.allowedOrigins ?? []);
2566
+ app.use("/mcp", async (c, next) => {
2567
+ const requestHost = (c.req.header("host") ?? "").toLowerCase();
2568
+ if (allowedHosts.size > 0 && !allowedHosts.has(requestHost)) {
2569
+ return c.json({
2570
+ jsonrpc: "2.0",
2571
+ error: { code: -32e3, message: "Forbidden host header." },
2572
+ id: null
2573
+ }, 403);
2574
+ }
2575
+ const origin = c.req.header("origin");
2576
+ if (origin && !allowedOrigins.has(origin)) {
2577
+ return c.json({
2578
+ jsonrpc: "2.0",
2579
+ error: { code: -32e3, message: "Forbidden origin." },
2580
+ id: null
2581
+ }, 403);
2582
+ }
2583
+ if (c.req.method === "OPTIONS") {
2584
+ return new Response(null, {
2585
+ status: 204,
2586
+ headers: corsHeaders(origin, allowedOrigins)
2587
+ });
2588
+ }
2589
+ await next();
2590
+ });
2591
+ app.get("/health", (c) => c.json({ status: "ok", name: "granite-mcp", version: GRANITE_VERSION }));
2592
+ app.all("/mcp", async (c) => {
2593
+ const origin = c.req.header("origin");
2594
+ const server = createGraniteMcpServer(runtime);
2595
+ const transport = new WebStandardStreamableHTTPServerTransport({
2596
+ sessionIdGenerator: void 0,
2597
+ enableJsonResponse: options.jsonResponse ?? false
2598
+ });
2599
+ try {
2600
+ await server.connect(transport);
2601
+ const response = await transport.handleRequest(c.req.raw);
2602
+ const managedResponse = await withResponseCleanup(response, async () => {
2603
+ await server.close();
2604
+ });
2605
+ return withCors(managedResponse, origin, allowedOrigins);
2606
+ } catch (error) {
2607
+ await server.close();
2608
+ throw error;
2609
+ }
2610
+ });
2611
+ return app;
2612
+ }
2613
+ function buildAllowedHosts(host, port) {
2614
+ const normalizedHost = host.toLowerCase();
2615
+ if (normalizedHost === "0.0.0.0" || normalizedHost === "::" || normalizedHost === "[::]") {
2616
+ return /* @__PURE__ */ new Set();
2617
+ }
2618
+ const allowed = /* @__PURE__ */ new Set([`${normalizedHost}:${port}`]);
2619
+ if (normalizedHost === "127.0.0.1" || normalizedHost === "localhost") {
2620
+ allowed.add(`127.0.0.1:${port}`);
2621
+ allowed.add(`localhost:${port}`);
2622
+ }
2623
+ if (normalizedHost === "::1" || normalizedHost === "[::1]") {
2624
+ allowed.add(`[::1]:${port}`);
2625
+ }
2626
+ return allowed;
2627
+ }
2628
+ function buildAllowedOrigins(host, port, extraOrigins) {
2629
+ const allowed = new Set(extraOrigins);
2630
+ const normalizedHost = host.toLowerCase();
2631
+ if (normalizedHost === "::1" || normalizedHost === "[::1]") {
2632
+ allowed.add(`http://[::1]:${port}`);
2633
+ } else if (normalizedHost !== "0.0.0.0" && normalizedHost !== "::" && normalizedHost !== "[::]") {
2634
+ allowed.add(`http://${normalizedHost}:${port}`);
2635
+ }
2636
+ if (normalizedHost === "127.0.0.1" || normalizedHost === "localhost") {
2637
+ allowed.add(`http://127.0.0.1:${port}`);
2638
+ allowed.add(`http://localhost:${port}`);
2639
+ }
2640
+ return allowed;
2641
+ }
2642
+ function withCors(response, origin, allowedOrigins) {
2643
+ const headers = new Headers(response.headers);
2644
+ for (const [key, value] of corsHeaders(origin, allowedOrigins)) {
2645
+ headers.set(key, value);
2646
+ }
2647
+ return new Response(response.body, {
2648
+ status: response.status,
2649
+ statusText: response.statusText,
2650
+ headers
2651
+ });
2652
+ }
2653
+ function corsHeaders(origin, allowedOrigins) {
2654
+ const headers = new Headers();
2655
+ if (origin && allowedOrigins.has(origin)) {
2656
+ headers.set("Access-Control-Allow-Origin", origin);
2657
+ headers.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
2658
+ headers.set("Access-Control-Allow-Headers", "Content-Type, MCP-Protocol-Version, Last-Event-ID");
2659
+ headers.set("Access-Control-Expose-Headers", "MCP-Session-Id, MCP-Protocol-Version");
2660
+ headers.set("Vary", "Origin");
2661
+ }
2662
+ return headers;
2663
+ }
2664
+
2665
+ // src/mcp/runtime.ts
2666
+ import fs12 from "fs";
2667
+ import path8 from "path";
2668
+ var GraniteMcpRuntime = class {
2669
+ vaultRoot;
2670
+ config;
2671
+ db;
2672
+ indexCheckIntervalMs;
2673
+ lastSignature;
2674
+ lastIndexCheckAt = 0;
2675
+ constructor(vaultRoot, options = {}) {
2676
+ this.vaultRoot = path8.resolve(vaultRoot);
2677
+ this.config = loadConfig(this.vaultRoot);
2678
+ this.db = openDatabase(this.vaultRoot);
2679
+ this.indexCheckIntervalMs = options.indexCheckIntervalMs ?? 1500;
2680
+ }
2681
+ close() {
2682
+ this.db.close();
2683
+ }
2684
+ getVaultOverview(recentLimit = 10) {
2685
+ const notes = this.readAllNotes().sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified));
2686
+ const byType = {};
2687
+ for (const note of notes) {
2688
+ byType[note.frontmatter.type] = (byType[note.frontmatter.type] ?? 0) + 1;
2689
+ }
2690
+ return {
2691
+ vault_root: this.vaultRoot,
2692
+ vault_name: this.config.vault_name,
2693
+ default_type: this.config.defaults.note_type,
2694
+ auto_rebuild: this.config.index.auto_rebuild,
2695
+ index_last_rebuild: this.getMetaValue("last_rebuild"),
2696
+ note_count: notes.length,
2697
+ notes_by_type: byType,
2698
+ recent_notes: notes.slice(0, clampLimit(recentLimit, 20)).map((note) => this.toNoteSummary(note))
2699
+ };
2700
+ }
2701
+ listNoteTypes() {
2702
+ return Object.entries(this.config.note_types).map(([name, typeConfig]) => ({
2703
+ name,
2704
+ description: typeConfig.description,
2705
+ folder: typeConfig.folder,
2706
+ line_limit: typeConfig.line_limit,
2707
+ warn_only: typeConfig.warn_only,
2708
+ slug_format: typeConfig.slug_format ?? "title",
2709
+ instructions: typeConfig.instructions,
2710
+ fields: typeConfig.fields
2711
+ }));
2712
+ }
2713
+ getDefaultNoteType() {
2714
+ return this.config.defaults.note_type;
2715
+ }
2716
+ listNotes(input = {}) {
2717
+ let notes = this.readAllNotes();
2718
+ if (input.type) {
2719
+ notes = notes.filter((note) => note.frontmatter.type === input.type);
2720
+ }
2721
+ if (input.status) {
2722
+ notes = notes.filter((note) => note.frontmatter.status === input.status);
2723
+ }
2724
+ if (input.source) {
2725
+ notes = notes.filter((note) => note.frontmatter.source === input.source);
2726
+ }
2727
+ if (input.since) {
2728
+ const since = input.since;
2729
+ notes = notes.filter((note) => note.frontmatter.modified >= since);
2730
+ }
2731
+ notes.sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified));
2732
+ return notes.slice(0, clampLimit(input.limit ?? 25, 200)).map((note) => this.toNoteSummary(note));
2733
+ }
2734
+ getNote(slug) {
2735
+ const note = this.requireNote(slug);
2736
+ const allNotes = this.readAllNotes();
2737
+ const outgoingLinks = resolveWikilinks(parseWikilinks(note.body), allNotes);
2738
+ return {
2739
+ ...this.toNoteSummary(note),
2740
+ body: note.body,
2741
+ frontmatter: note.frontmatter,
2742
+ outgoing_links: outgoingLinks
2743
+ };
2744
+ }
2745
+ search(query, limit = 10) {
2746
+ this.refreshIndex();
2747
+ return searchNotes(this.db, query, clampLimit(limit, 50));
2748
+ }
2749
+ getBacklinks(slug) {
2750
+ this.requireNote(slug);
2751
+ this.refreshIndex();
2752
+ return getBacklinks(this.db, slug);
2753
+ }
2754
+ suggestLinks(slug) {
2755
+ const note = this.requireNote(slug);
2756
+ this.refreshIndex();
2757
+ return suggestLinks(this.db, note);
2758
+ }
2759
+ recommend(slug) {
2760
+ const note = this.requireNote(slug);
2761
+ this.refreshIndex();
2762
+ return getRecommendations(this.db, note, this.config);
2763
+ }
2764
+ runDoctor() {
2765
+ this.refreshIndex();
2766
+ const issues = runDoctor(this.vaultRoot, this.config, this.db);
2767
+ return {
2768
+ counts: {
2769
+ errors: issues.filter((issue) => issue.level === "error").length,
2770
+ warnings: issues.filter((issue) => issue.level === "warning").length,
2771
+ info: issues.filter((issue) => issue.level === "info").length
2772
+ },
2773
+ issues
2774
+ };
2775
+ }
2776
+ createNote(input) {
2777
+ const resolvedType = input.type ?? this.config.defaults.note_type;
2778
+ const typeConfig = this.config.note_types[resolvedType];
2779
+ if (!typeConfig) {
2780
+ throw new Error(`Unknown note type: "${resolvedType}"`);
2781
+ }
2782
+ const bodyOverride = input.body !== void 0 ? ensureTrailingNewline(input.body) : typeConfig.slug_format === "date" ? ensureTrailingNewline(input.title) : void 0;
2783
+ const created = createNote(this.vaultRoot, this.config, resolvedType, input.title, bodyOverride);
2784
+ const metadataMutations = {
2785
+ tags: input.tags,
2786
+ aliases: input.aliases,
2787
+ status: input.status,
2788
+ source: input.source
2789
+ };
2790
+ if (hasMetadataMutations(metadataMutations)) {
2791
+ this.applyMutations(created.filepath, metadataMutations);
2792
+ }
2793
+ return this.afterWrite(created.slug, true);
2794
+ }
2795
+ captureNote(input) {
2796
+ const content = input.text.trim();
2797
+ if (!content) {
2798
+ throw new Error("Capture text cannot be empty.");
2799
+ }
2800
+ const firstLine = content.split("\n")[0] ?? "Untitled";
2801
+ const title = firstLine.length > 60 ? `${firstLine.slice(0, 60).trim()}...` : firstLine;
2802
+ return this.createNote({
2803
+ title,
2804
+ type: input.type,
2805
+ body: ensureTrailingNewline(content),
2806
+ tags: input.tags,
2807
+ aliases: input.aliases,
2808
+ status: input.status,
2809
+ source: input.source
2810
+ });
2811
+ }
2812
+ updateNote(slug, input) {
2813
+ const note = this.requireNote(slug);
2814
+ const needsFullRebuild = input.title !== void 0 || (input.aliases?.length ?? 0) > 0;
2815
+ this.applyMutations(note.filepath, input);
2816
+ if (needsFullRebuild) {
2817
+ return this.afterWrite(slug, true);
2818
+ }
2819
+ this.refreshIndex();
2820
+ const updated = readNote(note.filepath);
2821
+ syncNoteInIndex(this.vaultRoot, this.config, this.db, updated);
2822
+ this.captureSignature();
2823
+ return {
2824
+ note: this.getNote(updated.slug),
2825
+ recommendations: getRecommendations(this.db, updated, this.config)
2826
+ };
2827
+ }
2828
+ readVaultConfigRaw() {
2829
+ return fs12.readFileSync(path8.join(this.vaultRoot, CONFIG_FILENAME), "utf-8");
2830
+ }
2831
+ readVaultTypesJson() {
2832
+ return JSON.stringify({
2833
+ default_type: this.config.defaults.note_type,
2834
+ note_types: this.listNoteTypes()
2835
+ }, null, 2);
2836
+ }
2837
+ readVaultOverviewJson() {
2838
+ return JSON.stringify(this.getVaultOverview(), null, 2);
2839
+ }
2840
+ readNoteMarkdown(slug) {
2841
+ const note = this.requireNote(slug);
2842
+ return fs12.readFileSync(note.filepath, "utf-8");
2843
+ }
2844
+ completeSlugs(prefix = "") {
2845
+ const normalizedPrefix = prefix.toLowerCase();
2846
+ return this.readAllNotes().sort((a, b) => b.frontmatter.modified.localeCompare(a.frontmatter.modified)).map((note) => note.slug).filter((slug) => slug.toLowerCase().startsWith(normalizedPrefix)).slice(0, 25);
2847
+ }
2848
+ noteResourceUri(slug) {
2849
+ return `granite://notes/${encodeURIComponent(slug)}`;
2850
+ }
2851
+ refreshIndex(force = false) {
2852
+ const now = Date.now();
2853
+ if (!force && now - this.lastIndexCheckAt < this.indexCheckIntervalMs) {
2854
+ return;
2855
+ }
2856
+ const currentConfigMtimeMs = this.getConfigMtimeMs();
2857
+ if (!this.lastSignature || currentConfigMtimeMs !== this.lastSignature.configMtimeMs) {
2858
+ this.config = loadConfig(this.vaultRoot);
2859
+ }
2860
+ const signature = this.computeSignature();
2861
+ let shouldRebuild = false;
2862
+ if (force) {
2863
+ shouldRebuild = true;
2864
+ } else if (this.config.index.auto_rebuild) {
2865
+ if (!this.lastSignature) {
2866
+ shouldRebuild = signature.noteCount !== this.getIndexedNoteCount() || signature.latestMutationMs > this.getLastRebuildMs();
2867
+ } else {
2868
+ shouldRebuild = signature.noteCount !== this.lastSignature.noteCount || signature.latestMutationMs !== this.lastSignature.latestMutationMs || signature.configMtimeMs !== this.lastSignature.configMtimeMs;
2869
+ }
2870
+ }
2871
+ if (shouldRebuild) {
2872
+ rebuildIndex(this.vaultRoot, this.config, this.db);
2873
+ }
2874
+ this.lastSignature = signature;
2875
+ this.lastIndexCheckAt = now;
2876
+ }
2877
+ readAllNotes() {
2878
+ return listNotes(this.vaultRoot, this.config);
2879
+ }
2880
+ requireNote(slug) {
2881
+ const note = findNoteBySlug(this.vaultRoot, this.config, slug);
2882
+ if (!note) {
2883
+ throw new Error(`Note not found: ${slug}`);
2884
+ }
2885
+ return note;
2886
+ }
2887
+ toNoteSummary(note) {
2888
+ return {
2889
+ slug: note.slug,
2890
+ title: note.frontmatter.title,
2891
+ type: note.frontmatter.type,
2892
+ created: note.frontmatter.created,
2893
+ modified: note.frontmatter.modified,
2894
+ tags: note.frontmatter.tags,
2895
+ aliases: note.frontmatter.aliases,
2896
+ status: note.frontmatter.status,
2897
+ source: note.frontmatter.source,
2898
+ filepath: note.filepath,
2899
+ resource_uri: this.noteResourceUri(note.slug)
2900
+ };
2901
+ }
2902
+ applyMutations(filepath, input) {
2903
+ const raw = fs12.readFileSync(filepath, "utf-8");
2904
+ const { frontmatter, body: existingBody } = parseFrontmatter(raw);
2905
+ let body = existingBody;
2906
+ if (input.title !== void 0) {
2907
+ frontmatter.title = input.title;
2908
+ }
2909
+ if (input.tags && input.tags.length > 0) {
2910
+ frontmatter.tags = mergeUnique(frontmatter.tags, input.tags);
2911
+ }
2912
+ if (input.aliases && input.aliases.length > 0) {
2913
+ frontmatter.aliases = mergeUnique(frontmatter.aliases, input.aliases);
2914
+ }
2915
+ if (input.status !== void 0) {
2916
+ validateStatus(input.status);
2917
+ frontmatter.status = input.status;
2918
+ }
2919
+ if (input.source !== void 0) {
2920
+ validateSource(input.source);
2921
+ frontmatter.source = input.source;
2922
+ }
2923
+ if (input.body !== void 0) {
2924
+ body = ensureTrailingNewline(input.body);
2925
+ }
2926
+ if (input.append !== void 0) {
2927
+ body = `${body.trimEnd()}
2928
+ ${input.append}
2929
+ `;
2930
+ }
2931
+ frontmatter.modified = (/* @__PURE__ */ new Date()).toISOString();
2932
+ fs12.writeFileSync(filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2933
+ }
2934
+ afterWrite(slug, fullRebuild) {
2935
+ if (fullRebuild) {
2936
+ this.refreshIndex(true);
2937
+ } else {
2938
+ this.refreshIndex();
2939
+ const note2 = this.requireNote(slug);
2940
+ syncNoteInIndex(this.vaultRoot, this.config, this.db, note2);
2941
+ this.captureSignature();
2942
+ }
2943
+ const note = this.requireNote(slug);
2944
+ return {
2945
+ note: this.getNote(note.slug),
2946
+ recommendations: getRecommendations(this.db, note, this.config)
2947
+ };
2948
+ }
2949
+ captureSignature() {
2950
+ this.lastSignature = this.computeSignature();
2951
+ this.lastIndexCheckAt = Date.now();
2952
+ }
2953
+ computeSignature() {
2954
+ let noteCount = 0;
2955
+ let latestNoteMtimeMs = 0;
2956
+ for (const typeConfig of Object.values(this.config.note_types)) {
2957
+ const folder = path8.join(this.vaultRoot, typeConfig.folder);
2958
+ if (!fs12.existsSync(folder)) {
2959
+ continue;
2960
+ }
2961
+ for (const entry of fs12.readdirSync(folder)) {
2962
+ if (!entry.endsWith(".md")) {
2963
+ continue;
2964
+ }
2965
+ noteCount += 1;
2966
+ const stat = fs12.statSync(path8.join(folder, entry));
2967
+ latestNoteMtimeMs = Math.max(latestNoteMtimeMs, stat.mtimeMs);
2968
+ }
2969
+ }
2970
+ const configMtimeMs = this.getConfigMtimeMs();
2971
+ return {
2972
+ noteCount,
2973
+ latestMutationMs: Math.max(latestNoteMtimeMs, configMtimeMs),
2974
+ configMtimeMs
2975
+ };
2976
+ }
2977
+ getConfigMtimeMs() {
2978
+ const configPath = path8.join(this.vaultRoot, CONFIG_FILENAME);
2979
+ return fs12.existsSync(configPath) ? fs12.statSync(configPath).mtimeMs : 0;
2980
+ }
2981
+ getLastRebuildMs() {
2982
+ const value = this.getMetaValue("last_rebuild");
2983
+ return value ? Date.parse(value) || 0 : 0;
2984
+ }
2985
+ getMetaValue(key) {
2986
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
2987
+ return row?.value;
2988
+ }
2989
+ getIndexedNoteCount() {
2990
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM notes").get();
2991
+ return row.count;
2992
+ }
2993
+ };
2994
+ function ensureTrailingNewline(value) {
2995
+ return value.endsWith("\n") ? value : `${value}
2996
+ `;
2997
+ }
2998
+ function clampLimit(value, max) {
2999
+ return Math.max(1, Math.min(value, max));
3000
+ }
3001
+ function mergeUnique(existing, incoming) {
3002
+ const merged = new Set(existing);
3003
+ for (const value of incoming) {
3004
+ const trimmed = value.trim();
3005
+ if (trimmed) {
3006
+ merged.add(trimmed);
3007
+ }
3008
+ }
3009
+ return [...merged];
3010
+ }
3011
+ function hasMetadataMutations(input) {
3012
+ return (input.tags?.length ?? 0) > 0 || (input.aliases?.length ?? 0) > 0 || input.status !== void 0 || input.source !== void 0;
3013
+ }
3014
+
3015
+ // src/commands/mcp.ts
3016
+ async function mcpCommand(options) {
3017
+ const transport = parseTransport(options.transport);
3018
+ const vaultRoot = resolveVaultRoot(options.vault);
3019
+ const runtime = new GraniteMcpRuntime(vaultRoot);
3020
+ const shutdown = () => {
3021
+ runtime.close();
3022
+ process.exit(0);
3023
+ };
3024
+ process.once("SIGINT", shutdown);
3025
+ process.once("SIGTERM", shutdown);
3026
+ if (transport === "http") {
3027
+ const port = parsePort(options.port);
3028
+ startGraniteMcpHttpServer(runtime, {
3029
+ host: options.host ?? "127.0.0.1",
3030
+ port,
3031
+ allowedOrigins: options.allowOrigin ?? [],
3032
+ jsonResponse: options.jsonResponse ?? false
3033
+ });
3034
+ return;
3035
+ }
3036
+ await startGraniteMcpStdioServer(runtime);
3037
+ }
3038
+ function resolveVaultRoot(explicitVault) {
3039
+ const fromEnv = process.env.GRANITE_VAULT;
3040
+ if (explicitVault) {
3041
+ return path9.resolve(explicitVault);
3042
+ }
3043
+ if (fromEnv) {
3044
+ return path9.resolve(fromEnv);
3045
+ }
3046
+ return requireVaultRoot();
3047
+ }
3048
+ function parseTransport(value) {
3049
+ const transport = value ?? "stdio";
3050
+ if (transport !== "stdio" && transport !== "http") {
3051
+ throw new Error(`Invalid MCP transport: ${transport}. Expected "stdio" or "http".`);
3052
+ }
3053
+ return transport;
3054
+ }
3055
+ function parsePort(value) {
3056
+ const raw = (value ?? process.env.MCP_PORT ?? "3321").trim();
3057
+ if (!/^\d+$/.test(raw)) {
3058
+ throw new Error(`Invalid MCP port: ${raw}`);
3059
+ }
3060
+ const parsed = Number.parseInt(raw, 10);
3061
+ if (parsed <= 0) {
3062
+ throw new Error(`Invalid MCP port: ${raw}`);
3063
+ }
3064
+ return parsed;
3065
+ }
3066
+
1945
3067
  // src/index.ts
1946
3068
  var program = new Command();
1947
- program.name("mem").description("Granite \u2014 a local-first markdown memory system").version("0.1.0");
1948
- program.command("init").description("Initialize a new vault in the current directory").action(() => {
1949
- initVault(process.cwd());
3069
+ program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(GRANITE_VERSION);
3070
+ program.command("init").description("Initialize the default vault in ~/.granite").action(() => {
3071
+ initVault();
1950
3072
  });
1951
3073
  program.command("new").description("Create a new note").argument("<title>", "Note title").option("-t, --type <type>", "Note type (e.g. fleeting, permanent, reference)").option("--source <source>", "Set source (human, agent, extraction)").option("--status <status>", "Set status (inbox, active, archived)").option("--json", "Output as JSON (agent-friendly)").action((title, options) => {
1952
3074
  newNote(title, options);
@@ -1987,4 +3109,17 @@ program.command("doctor").description("Validate vault health").action(() => {
1987
3109
  program.command("serve").description("Start the local web UI").option("-p, --port <port>", "Port number", "4321").action((options) => {
1988
3110
  serveCommand(options);
1989
3111
  });
1990
- program.parse();
3112
+ program.command("mcp").description("Start Granite as an MCP server").option("--vault <path>", "Vault root. Defaults to the current Granite vault or $GRANITE_VAULT").option("--transport <transport>", "Transport to use: stdio or http", parseTransportOption, "stdio").option("--host <host>", "Host for HTTP transport", "127.0.0.1").option("--port <port>", "Port for HTTP transport", "3321").option("--allow-origin <origin>", "Allow an HTTP Origin for browser-based HTTP clients", collectValues, []).option("--json-response", "Prefer JSON HTTP responses instead of SSE streams").action(async (options) => {
3113
+ await mcpCommand(options);
3114
+ });
3115
+ await program.parseAsync();
3116
+ function collectValues(value, previous) {
3117
+ return [...previous, value];
3118
+ }
3119
+ function parseTransportOption(value) {
3120
+ try {
3121
+ return parseTransport(value);
3122
+ } catch (error) {
3123
+ throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
3124
+ }
3125
+ }