granite-mem 0.1.3 → 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.
- package/README.md +40 -1
- package/dist/index.js +1128 -10
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ Granite is designed to be easy for agents to read and act on:
|
|
|
60
60
|
- notes are plain files
|
|
61
61
|
- metadata is explicit
|
|
62
62
|
- commands support `--json`
|
|
63
|
+
- Granite ships with an MCP server
|
|
63
64
|
- vault structure is predictable
|
|
64
65
|
- search, backlinks, and recommendations are available from the CLI
|
|
65
66
|
|
|
@@ -85,7 +86,13 @@ mem add "Talked to Alice about local-first sync tradeoffs"
|
|
|
85
86
|
mem new "Local-first sync tradeoffs" --type permanent
|
|
86
87
|
mem list
|
|
87
88
|
mem search "sync"
|
|
88
|
-
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Start one long-running interface when you need it:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mem serve # local web UI
|
|
95
|
+
mem mcp # MCP server for agent clients
|
|
89
96
|
```
|
|
90
97
|
|
|
91
98
|
`mem new` does more than create a file. It can immediately suggest related links, tags, and the next note to create, which is the core of Granite's value loop.
|
|
@@ -175,6 +182,37 @@ mem recommend sync-constraints --json
|
|
|
175
182
|
|
|
176
183
|
That makes Granite a useful substrate for local workflows, scripts, and agent memory.
|
|
177
184
|
|
|
185
|
+
## MCP Server
|
|
186
|
+
|
|
187
|
+
Granite ships with an MCP server so LLM clients can control the vault directly through tools, resources, and prompts.
|
|
188
|
+
|
|
189
|
+
Start it over stdio for local MCP clients:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
mem mcp --vault /path/to/vault
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Start it over Streamable HTTP:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
mem mcp --transport http --host 127.0.0.1 --port 3321
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The server exposes:
|
|
202
|
+
|
|
203
|
+
- tools for vault overview, list/get/search, create/update, backlinks, link suggestions, recommendations, and doctor
|
|
204
|
+
- resources for `granite.yml`, vault overview, note types, and individual notes via `granite://notes/{slug}`
|
|
205
|
+
- prompts for refining notes and reviewing links/next steps
|
|
206
|
+
|
|
207
|
+
Example stdio client configuration:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"command": "mem",
|
|
212
|
+
"args": ["mcp", "--vault", "/path/to/vault"]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
178
216
|
## Commands
|
|
179
217
|
|
|
180
218
|
```bash
|
|
@@ -192,6 +230,7 @@ mem recommend <slug> [--json]
|
|
|
192
230
|
mem types
|
|
193
231
|
mem doctor
|
|
194
232
|
mem serve [-p <port>]
|
|
233
|
+
mem mcp [--vault <path>] [--transport <stdio|http>]
|
|
195
234
|
```
|
|
196
235
|
|
|
197
236
|
## Development
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
7
|
import fs3 from "fs";
|
|
@@ -533,8 +533,8 @@ function countNoteFiles(vaultRoot, config) {
|
|
|
533
533
|
}
|
|
534
534
|
function upsertNoteAndLinks(db, note) {
|
|
535
535
|
const upsertNote = db.prepare(`
|
|
536
|
-
INSERT INTO notes (slug, id, title, type, created, modified, tags, aliases, body, filepath)
|
|
537
|
-
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)
|
|
538
538
|
ON CONFLICT(slug) DO UPDATE SET
|
|
539
539
|
id = excluded.id,
|
|
540
540
|
title = excluded.title,
|
|
@@ -544,7 +544,9 @@ function upsertNoteAndLinks(db, note) {
|
|
|
544
544
|
tags = excluded.tags,
|
|
545
545
|
aliases = excluded.aliases,
|
|
546
546
|
body = excluded.body,
|
|
547
|
-
filepath = excluded.filepath
|
|
547
|
+
filepath = excluded.filepath,
|
|
548
|
+
status = excluded.status,
|
|
549
|
+
source = excluded.source
|
|
548
550
|
`);
|
|
549
551
|
const deleteLinks = db.prepare("DELETE FROM links WHERE source_slug = ?");
|
|
550
552
|
const insertLink = db.prepare(`
|
|
@@ -562,7 +564,9 @@ function upsertNoteAndLinks(db, note) {
|
|
|
562
564
|
tags: JSON.stringify(note.frontmatter.tags),
|
|
563
565
|
aliases: JSON.stringify(note.frontmatter.aliases),
|
|
564
566
|
body: note.body,
|
|
565
|
-
filepath: note.filepath
|
|
567
|
+
filepath: note.filepath,
|
|
568
|
+
status: note.frontmatter.status ?? "active",
|
|
569
|
+
source: note.frontmatter.source ?? "human"
|
|
566
570
|
});
|
|
567
571
|
deleteLinks.run(note.slug);
|
|
568
572
|
const links = parseWikilinks(note.body);
|
|
@@ -1354,7 +1358,8 @@ function showCommand(slug, options) {
|
|
|
1354
1358
|
}
|
|
1355
1359
|
|
|
1356
1360
|
// src/core/search.ts
|
|
1357
|
-
function searchNotes(db, query) {
|
|
1361
|
+
function searchNotes(db, query, limit = 20) {
|
|
1362
|
+
const cappedLimit = Math.max(1, Math.min(limit, 100));
|
|
1358
1363
|
const stmt = db.prepare(`
|
|
1359
1364
|
SELECT
|
|
1360
1365
|
n.slug,
|
|
@@ -1365,9 +1370,9 @@ function searchNotes(db, query) {
|
|
|
1365
1370
|
JOIN notes n ON n.rowid = notes_fts.rowid
|
|
1366
1371
|
WHERE notes_fts MATCH ?
|
|
1367
1372
|
ORDER BY rank
|
|
1368
|
-
LIMIT
|
|
1373
|
+
LIMIT ?
|
|
1369
1374
|
`);
|
|
1370
|
-
const rows = stmt.all(query);
|
|
1375
|
+
const rows = stmt.all(query, cappedLimit);
|
|
1371
1376
|
return rows.map((r) => ({
|
|
1372
1377
|
slug: r.slug,
|
|
1373
1378
|
title: r.title,
|
|
@@ -1959,9 +1964,1109 @@ function printRecommendations(vaultRoot, config, filepath, strategy) {
|
|
|
1959
1964
|
}
|
|
1960
1965
|
}
|
|
1961
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
|
+
|
|
1962
3067
|
// src/index.ts
|
|
1963
3068
|
var program = new Command();
|
|
1964
|
-
program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(
|
|
3069
|
+
program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(GRANITE_VERSION);
|
|
1965
3070
|
program.command("init").description("Initialize the default vault in ~/.granite").action(() => {
|
|
1966
3071
|
initVault();
|
|
1967
3072
|
});
|
|
@@ -2004,4 +3109,17 @@ program.command("doctor").description("Validate vault health").action(() => {
|
|
|
2004
3109
|
program.command("serve").description("Start the local web UI").option("-p, --port <port>", "Port number", "4321").action((options) => {
|
|
2005
3110
|
serveCommand(options);
|
|
2006
3111
|
});
|
|
2007
|
-
program.
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granite-mem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A local-first markdown memory system for humans and agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,18 +27,21 @@
|
|
|
27
27
|
"build": "tsup && rm -rf dist/public && cp -r src/web/public dist/public",
|
|
28
28
|
"dev": "tsx src/index.ts",
|
|
29
29
|
"test": "vitest run",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
30
31
|
"test:watch": "vitest",
|
|
31
32
|
"lint": "tsc --noEmit",
|
|
32
33
|
"prepare": "husky"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
36
|
+
"@hono/node-server": "^1.13.0",
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
38
|
"better-sqlite3": "^11.0.0",
|
|
36
39
|
"commander": "^13.0.0",
|
|
37
40
|
"gray-matter": "^4.0.3",
|
|
38
41
|
"hono": "^4.0.0",
|
|
39
|
-
"@hono/node-server": "^1.13.0",
|
|
40
42
|
"js-yaml": "^4.1.0",
|
|
41
|
-
"uuid": "^11.0.0"
|
|
43
|
+
"uuid": "^11.0.0",
|
|
44
|
+
"zod": "^3.25.76"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@commitlint/cli": "^20.0.0",
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"@types/js-yaml": "^4.0.0",
|
|
48
51
|
"@types/node": "^22.0.0",
|
|
49
52
|
"@types/uuid": "^10.0.0",
|
|
53
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
54
|
"commitizen": "^4.3.1",
|
|
51
55
|
"cz-conventional-changelog": "^3.3.0",
|
|
52
56
|
"husky": "^9.1.7",
|