@gorajing/zuun 0.1.1
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/LICENSE +21 -0
- package/README.md +331 -0
- package/bin/zuun.js +34 -0
- package/dist/capture.js +84 -0
- package/dist/cli.js +205 -0
- package/dist/commands/capture-commit.js +75 -0
- package/dist/commands/edit.js +83 -0
- package/dist/commands/explain.js +31 -0
- package/dist/commands/forget.js +72 -0
- package/dist/commands/install-git-hook.js +117 -0
- package/dist/hook-scripts/session-start.js +81 -0
- package/dist/lib/db.js +93 -0
- package/dist/lib/dedup.js +53 -0
- package/dist/lib/doctor.js +47 -0
- package/dist/lib/embed-provider.js +42 -0
- package/dist/lib/embed.js +36 -0
- package/dist/lib/entry-io.js +82 -0
- package/dist/lib/entry.js +58 -0
- package/dist/lib/id.js +49 -0
- package/dist/lib/log.js +66 -0
- package/dist/lib/paths.js +53 -0
- package/dist/lib/project.js +67 -0
- package/dist/lib/search.js +121 -0
- package/dist/lib/store.js +64 -0
- package/dist/lib/tags.js +19 -0
- package/dist/mcp.js +231 -0
- package/dist/scripts/reindex.js +71 -0
- package/package.json +61 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const db_1 = require("./lib/db");
|
|
9
|
+
const store_1 = require("./lib/store");
|
|
10
|
+
const entry_io_1 = require("./lib/entry-io");
|
|
11
|
+
const id_1 = require("./lib/id");
|
|
12
|
+
const search_1 = require("./lib/search");
|
|
13
|
+
const embed_provider_1 = require("./lib/embed-provider");
|
|
14
|
+
const embed_1 = require("./lib/embed");
|
|
15
|
+
const entry_1 = require("./lib/entry");
|
|
16
|
+
const tags_1 = require("./lib/tags");
|
|
17
|
+
const dedup_1 = require("./lib/dedup");
|
|
18
|
+
const project_1 = require("./lib/project");
|
|
19
|
+
const log_1 = require("./lib/log");
|
|
20
|
+
const SOURCE = entry_1.EntrySource.safeParse(process.env.ZUUN_MCP_SOURCE).success
|
|
21
|
+
? process.env.ZUUN_MCP_SOURCE
|
|
22
|
+
: "claude-code";
|
|
23
|
+
const RememberInput = zod_1.z.object({
|
|
24
|
+
body: zod_1.z.string().min(1, "body must not be empty"),
|
|
25
|
+
kind: entry_1.EntryKind.default("observation"),
|
|
26
|
+
tags: zod_1.z.array(zod_1.z.string()).default([]),
|
|
27
|
+
stance: zod_1.z.string().optional(),
|
|
28
|
+
related: zod_1.z.array(zod_1.z.string()).default([]),
|
|
29
|
+
origin: zod_1.z.string().optional(),
|
|
30
|
+
project: zod_1.z.string().optional(),
|
|
31
|
+
});
|
|
32
|
+
const ContextForInput = zod_1.z.object({
|
|
33
|
+
task: zod_1.z.string().min(1, "task must not be empty"),
|
|
34
|
+
limit: zod_1.z.number().int().positive().max(50).default(8),
|
|
35
|
+
});
|
|
36
|
+
const REMEMBER_DESC = `Save a durable insight from this session so future sessions can recall it.
|
|
37
|
+
|
|
38
|
+
WHEN TO CALL: Something you just decided, observed, or learned that a new session would have to re-derive — an architectural choice, a "we don't do X because Y," a rule of thumb, a commitment to future-self, or an external fact worth preserving. Call as soon as the thing is true; you don't have to batch.
|
|
39
|
+
|
|
40
|
+
WHEN NOT TO CALL: (1) Ephemeral task state — use your own notes. (2) Content already in the codebase — code is its own memory. (3) Unverified speculation — capture after confirming.
|
|
41
|
+
|
|
42
|
+
INPUT SHAPE: One self-contained claim per call. "Local-first beats cloud-first for portability because tar is the moat." — not a paragraph, not a list. If you have five things to remember, make five calls.
|
|
43
|
+
|
|
44
|
+
KINDS: "decision" (a choice with reasoning), "observation" (something noticed about how the world works), "pattern" (a reusable approach), "commitment" (a promise to future-self), "reference" (a durable context snippet).
|
|
45
|
+
|
|
46
|
+
OPTIONAL \`origin\`: Pass the file path, git sha, PR number, or session marker this entry came from — lets future retrieval cite the source. Use it when the memory is tied to a specific artifact.
|
|
47
|
+
|
|
48
|
+
PROJECT SCOPING: The \`project\` field is auto-populated from the MCP server's cwd (git root, or pwd). This scopes the entry so SessionStart / context_for retrieve it only for sessions in the same project. Override \`project\` only if the memory belongs to a different project than the current session's cwd — e.g., a cross-repo observation.
|
|
49
|
+
|
|
50
|
+
OUTPUT: The generated entry id, kind, and echoed body on success. If an identical body was remembered in the last 10 minutes, returns "already remembered <id>" without duplicating.`;
|
|
51
|
+
const CONTEXT_FOR_DESC = `Surface the most relevant past entries for what you're working on right now.
|
|
52
|
+
|
|
53
|
+
WHEN TO CALL: At the start of a new task, or when switching context. Once per context switch — not repeatedly inside the same task. Results are stable-enough that re-querying wastes attention budget.
|
|
54
|
+
|
|
55
|
+
INPUT SHAPE: Free-form, specific description of the current work. "picking between SQLite and Postgres for session storage" beats "databases". More specific phrasing retrieves more specific matches.
|
|
56
|
+
|
|
57
|
+
SCOPING: Retrieval is automatically scoped to the current project (resolved from the server's cwd). Global entries (captured outside any git repo) are always included — they apply everywhere. Entries from other projects are excluded. You don't control this; it's structural.
|
|
58
|
+
|
|
59
|
+
OUTPUT: A markdown list of up to \`limit\` entries. Each line shows id, kind, relative age, and body. If no matching prior context exists, returns "no prior context" — treat that as signal, not an error. The entries you see are the entries your past self already thought were worth remembering; cite their ids when referencing them.`;
|
|
60
|
+
async function main() {
|
|
61
|
+
const server = new index_js_1.Server({ name: "zuun", version: "0.1.1" }, { capabilities: { tools: {} } });
|
|
62
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
63
|
+
tools: [
|
|
64
|
+
{
|
|
65
|
+
name: "remember",
|
|
66
|
+
description: REMEMBER_DESC,
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
body: { type: "string", description: "The content to remember — one self-contained claim." },
|
|
71
|
+
kind: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["decision", "observation", "pattern", "commitment", "reference"],
|
|
74
|
+
description: "Defaults to observation if omitted.",
|
|
75
|
+
},
|
|
76
|
+
tags: {
|
|
77
|
+
type: "array",
|
|
78
|
+
items: { type: "string" },
|
|
79
|
+
description: "Freeform tags. Normalized to lowercase-dashed.",
|
|
80
|
+
},
|
|
81
|
+
stance: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Optional one-line directional claim, assertable as true or false.",
|
|
84
|
+
},
|
|
85
|
+
related: {
|
|
86
|
+
type: "array",
|
|
87
|
+
items: { type: "string" },
|
|
88
|
+
description: "Optional entry IDs this one builds on. Soft refs; not validated.",
|
|
89
|
+
},
|
|
90
|
+
origin: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Optional provenance: file path, git sha, PR number, or session marker. Lets future retrieval cite sources.",
|
|
93
|
+
},
|
|
94
|
+
project: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Optional absolute path scoping this entry to a project. Auto-populated from the server's cwd (git root or pwd) if omitted — override only if the memory genuinely belongs to a different project than the session's cwd.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["body"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "context_for",
|
|
104
|
+
description: CONTEXT_FOR_DESC,
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
task: { type: "string", description: "What you're working on, in your own words." },
|
|
109
|
+
limit: { type: "number", description: "Max results. Default 8, max 50.", default: 8 },
|
|
110
|
+
},
|
|
111
|
+
required: ["task"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}));
|
|
116
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
117
|
+
if (req.params.name === "remember")
|
|
118
|
+
return handleRemember(req.params.arguments);
|
|
119
|
+
if (req.params.name === "context_for")
|
|
120
|
+
return handleContextFor(req.params.arguments);
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: `unknown tool: ${req.params.name}` }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
127
|
+
await server.connect(transport);
|
|
128
|
+
}
|
|
129
|
+
async function handleRemember(args) {
|
|
130
|
+
const input = RememberInput.parse(args);
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const db = (0, db_1.openDb)();
|
|
133
|
+
try {
|
|
134
|
+
const existing = (0, dedup_1.findRecentDuplicate)(db, input.body, now);
|
|
135
|
+
if (existing) {
|
|
136
|
+
(0, log_1.appendLog)("remember.dedup", { id: existing });
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{ type: "text", text: `already remembered ${existing} (same body within 10 minutes)` },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const id = (0, id_1.newEntryId)(input.body, now);
|
|
144
|
+
const entry = {
|
|
145
|
+
id,
|
|
146
|
+
created: now.toISOString(),
|
|
147
|
+
body: input.body,
|
|
148
|
+
kind: input.kind,
|
|
149
|
+
source: SOURCE,
|
|
150
|
+
tags: (0, tags_1.normalizeTags)(input.tags),
|
|
151
|
+
related: input.related,
|
|
152
|
+
stance: input.stance,
|
|
153
|
+
origin: input.origin,
|
|
154
|
+
project: input.project ?? (0, project_1.resolveProject)(),
|
|
155
|
+
};
|
|
156
|
+
(0, entry_io_1.writeEntry)(entry);
|
|
157
|
+
(0, store_1.upsertEntry)(db, entry);
|
|
158
|
+
(0, log_1.appendLog)("remember", { id, kind: entry.kind, tags: entry.tags });
|
|
159
|
+
// Fire-and-forget embed: search works without it; rerun via `zuun embed`.
|
|
160
|
+
void embed_provider_1.defaultProvider
|
|
161
|
+
.embed(entry.body)
|
|
162
|
+
.then((vec) => {
|
|
163
|
+
if (!vec)
|
|
164
|
+
return;
|
|
165
|
+
const db2 = (0, db_1.openDb)();
|
|
166
|
+
try {
|
|
167
|
+
(0, embed_1.setEmbedding)(db2, id, vec);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
db2.close();
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
.catch(() => { });
|
|
174
|
+
const tagLine = entry.tags.length ? ` · tags: ${entry.tags.join(", ")}` : "";
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{ type: "text", text: `saved ${id} (${entry.kind}${tagLine})\n${entry.body}` },
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
db.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function handleContextFor(args) {
|
|
186
|
+
const input = ContextForInput.parse(args);
|
|
187
|
+
const db = (0, db_1.openDb)();
|
|
188
|
+
try {
|
|
189
|
+
const project = (0, project_1.resolveProject)();
|
|
190
|
+
const qVec = await embed_provider_1.defaultProvider.embed(input.task);
|
|
191
|
+
const results = (0, search_1.search)(db, {
|
|
192
|
+
query: input.task,
|
|
193
|
+
queryVec: qVec ?? undefined,
|
|
194
|
+
limit: input.limit,
|
|
195
|
+
project: project ?? undefined,
|
|
196
|
+
});
|
|
197
|
+
(0, log_1.appendLog)("context_for", { task: input.task.slice(0, 120), project, hits: results.length });
|
|
198
|
+
const text = results.length === 0 ? "no prior context" : formatResults(results);
|
|
199
|
+
return { content: [{ type: "text", text }] };
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
db.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function formatResults(results) {
|
|
206
|
+
const lines = [`Found ${results.length} entries:\n`];
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
for (const r of results) {
|
|
209
|
+
const age = relativeAge(now - new Date(r.entry.created).getTime());
|
|
210
|
+
const tags = r.entry.tags.length ? ` · tags: ${r.entry.tags.join(", ")}` : "";
|
|
211
|
+
lines.push(`- ${r.entry.id} · ${r.entry.kind} · ${age}${tags}`);
|
|
212
|
+
lines.push(` ${r.entry.body.replace(/\n/g, " ")}`);
|
|
213
|
+
}
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
function relativeAge(ms) {
|
|
217
|
+
const d = Math.floor(ms / 86_400_000);
|
|
218
|
+
if (d < 1)
|
|
219
|
+
return "today";
|
|
220
|
+
if (d < 2)
|
|
221
|
+
return "yesterday";
|
|
222
|
+
if (d < 14)
|
|
223
|
+
return `${d}d ago`;
|
|
224
|
+
if (d < 60)
|
|
225
|
+
return `${Math.floor(d / 7)}w ago`;
|
|
226
|
+
return `${Math.floor(d / 30)}mo ago`;
|
|
227
|
+
}
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
console.error(err);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.reindex = reindex;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const db_1 = require("../lib/db");
|
|
39
|
+
const entry_io_1 = require("../lib/entry-io");
|
|
40
|
+
const store_1 = require("../lib/store");
|
|
41
|
+
const paths_1 = require("../lib/paths");
|
|
42
|
+
const log_1 = require("../lib/log");
|
|
43
|
+
function reindex() {
|
|
44
|
+
if (fs.existsSync((0, paths_1.dbPath)()))
|
|
45
|
+
fs.rmSync((0, paths_1.dbPath)());
|
|
46
|
+
const db = (0, db_1.openDb)();
|
|
47
|
+
const failed = [];
|
|
48
|
+
let indexed = 0;
|
|
49
|
+
for (const id of (0, entry_io_1.listEntryIds)()) {
|
|
50
|
+
try {
|
|
51
|
+
(0, store_1.upsertEntry)(db, (0, entry_io_1.readEntry)(id));
|
|
52
|
+
indexed++;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
failed.push(`${id}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
db.close();
|
|
59
|
+
(0, log_1.appendLog)("reindex", { indexed, failed: failed.length });
|
|
60
|
+
return { indexed, failed };
|
|
61
|
+
}
|
|
62
|
+
if (require.main === module) {
|
|
63
|
+
const r = reindex();
|
|
64
|
+
console.log(`indexed ${r.indexed} entries`);
|
|
65
|
+
if (r.failed.length > 0) {
|
|
66
|
+
console.error(`failed: ${r.failed.length}`);
|
|
67
|
+
for (const line of r.failed)
|
|
68
|
+
console.error(` ${line}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gorajing/zuun",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Persistent memory for AI-assisted work — local-first markdown + SQLite, hybrid search, Claude Code plugin.",
|
|
5
|
+
"author": "Jin Choi",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/gorajing/zuun",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/gorajing/zuun.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/gorajing/zuun/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"mcp",
|
|
18
|
+
"memory",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"local-first",
|
|
21
|
+
"typescript",
|
|
22
|
+
"plugin"
|
|
23
|
+
],
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": "20.x || 22.x"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"zuun": "bin/zuun.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin/",
|
|
33
|
+
"dist/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"dev": "tsx src/mcp.ts",
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"cli": "tsx src/cli.ts",
|
|
43
|
+
"reindex": "tsx src/scripts/reindex.ts",
|
|
44
|
+
"prepack": "npm run build",
|
|
45
|
+
"prepublishOnly": "npm test"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
49
|
+
"better-sqlite3": "^12.8.0",
|
|
50
|
+
"gray-matter": "^4.0.3",
|
|
51
|
+
"sqlite-vec": "^0.1.7",
|
|
52
|
+
"zod": "^4.3.6"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
56
|
+
"@types/node": "^25.5.0",
|
|
57
|
+
"tsx": "^4.21.0",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vitest": "^4.1.0"
|
|
60
|
+
}
|
|
61
|
+
}
|