@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jin Choi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # Zuun
2
+
3
+ **Persistent memory for AI-assisted work.** A Claude Code plugin that captures durable insights from your sessions and surfaces them into future ones โ€” so you stop re-explaining yourself to the agent.
4
+
5
+ - ๐Ÿง  **MCP tools** โ€” `remember` and `context_for` the agent can call mid-session
6
+ - ๐Ÿช„ **SessionStart injection** โ€” every new Claude Code session opens with relevant prior decisions pre-loaded
7
+ - ๐Ÿ“ **`/zuun:reflect` skill** โ€” user-invoked end-of-session reflection
8
+ - ๐Ÿช **Git post-commit hook** โ€” every commit you make becomes a captured pattern
9
+ - ๐Ÿ”Ž **Hybrid search** โ€” FTS5 + vector (via Ollama) + recency, with per-component score explain
10
+ - ๐Ÿ’พ **Local-first** โ€” plain markdown + SQLite on your disk. No cloud. No account. `grep`-able, `git`-able, `rm -rf`-able.
11
+
12
+ ```
13
+ $ zuun doctor
14
+ schema_version: 2
15
+ entries on disk: 17
16
+ entries in db: 17
17
+ broken related refs: 0
18
+ ollama: up
19
+ recent log:
20
+ 2026-04-19T13:16:11Z capture {"id":"ENT-260419-D75E","kind":"pattern","source":"git"}
21
+ 2026-04-19T13:17:02Z session_start.inject {"project":"/Users/you/Code/zuun","hits":3,"chars":890}
22
+ ```
23
+
24
+ ---
25
+
26
+ ## The problem
27
+
28
+ Every Claude Code session starts from zero. Yesterday's architectural decisions, the reason you rejected library X, the gotcha you discovered in module Y โ€” the agent forgets all of it the moment the session closes. You either re-type the context every morning, stuff it into CLAUDE.md (and watch it grow stale), or accept that the agent will propose things you already rejected last week.
29
+
30
+ Zuun is the smallest useful fix: a local store that captures the durable claims from a session and injects the relevant ones into the next one.
31
+
32
+ **Unit of improvement: the multi-session trajectory, not the single commit.** You won't see Zuun in a one-off task. You'll see it in week 4, when the agent is already calibrated to your preferences without you re-calibrating it.
33
+
34
+ ---
35
+
36
+ ## How it works
37
+
38
+ ```
39
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
40
+ โ”‚ Claude Code session โ”‚
41
+ โ”‚ โ”‚
42
+ โ”‚ remember โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€ /zuun: โ”‚
43
+ โ”‚ context_for โ—€โ”€โ”€โ”ค โ”‚ reflect โ”‚
44
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
45
+ โ”‚ โ”‚
46
+ โ–ผ โ–ผ
47
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
48
+ โ”‚ MCP server (stdio) + CLI (zuun โ€ฆ) โ”‚
49
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
50
+ โ”‚ โ”‚
51
+ โ–ผ โ–ผ
52
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
53
+ โ”‚ ~/.zuun/ โ”‚
54
+ โ”‚ entries/ ENT-YYMMDD-XXXX.mdโ”‚
55
+ โ”‚ index.db (SQLite + FTS5 โ”‚
56
+ โ”‚ + sqlite-vec) โ”‚
57
+ โ”‚ log.jsonl (append-only) โ”‚
58
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
59
+ โ–ฒ โ–ฒ
60
+ โ”‚ โ”‚
61
+ git post-commit SessionStart hook
62
+ (captures) (injects)
63
+ ```
64
+
65
+ Every mutation writes to both disk (markdown) and index (SQLite), in that order. If a crash hits between, `zuun reindex` rebuilds the DB from disk. The markdown files are the source of truth; SQLite is a derived, throwaway index.
66
+
67
+ ---
68
+
69
+ ## Install
70
+
71
+ Prerequisites:
72
+
73
+ - Node **20.x or 22.x**
74
+ - [Claude Code](https://docs.anthropic.com/claude-code) v2.0+
75
+ - [Ollama](https://ollama.com/) with `nomic-embed-text` *(optional โ€” search works without it via pure FTS; embeddings just improve semantic match)*
76
+ - `git` (for the post-commit hook)
77
+
78
+ ### 1. Install the Claude Code plugin
79
+
80
+ ```bash
81
+ claude plugin marketplace add github:gorajing/zuun
82
+ claude plugin install zuun@zuun
83
+ ```
84
+
85
+ Relaunch Claude Code. Run `/mcp` โ€” `zuun` should appear as **connected**. That's the signal the MCP server booted (fetched from npm on first run via `npx`). No data setup required: the store (`~/.zuun/`) auto-creates on first write.
86
+
87
+ ### 2. Install the CLI globally (optional but recommended)
88
+
89
+ For the git post-commit hook, manual captures, and shell pipelines:
90
+
91
+ ```bash
92
+ npm install -g @gorajing/zuun
93
+ zuun --version # should print 0.1.1
94
+ ```
95
+
96
+ (The npm package is scoped as `@gorajing/zuun` because `zuun` is too close to an existing unscoped package. The CLI binary is still `zuun` โ€” the scope is just for the registry namespace.)
97
+
98
+ The Claude Code plugin and the global CLI share the same `~/.zuun/` store, so entries captured from one are visible to the other.
99
+
100
+ ### 3. First run โ€” zero setup
101
+
102
+ Start a new Claude Code session, then prompt the agent:
103
+
104
+ > *"Use the zuun remember tool to save: 'Local-first beats cloud-first because tar is the moat.'"*
105
+
106
+ It'll return `saved ENT-YYMMDD-XXXX`. That's your first entry. The store was created, the index was built, and future sessions will see this entry in their SessionStart context when you're in the same project directory.
107
+
108
+ From here, the loop starts: capture what's hard-won, let SessionStart re-surface it, use `/zuun:reflect` at natural breakpoints.
109
+
110
+ ### 4. Install the git post-commit hook in your active repos
111
+
112
+ Requires the global CLI from step 2.
113
+
114
+ ```bash
115
+ # In each repo you want to capture commits from:
116
+ cd ~/path/to/my-project
117
+ zuun install-git-hook
118
+ ```
119
+
120
+ The installer writes an absolute path into `.git/hooks/post-commit` (one per repo, opt-in). Hooks fire outside Claude Code too โ€” every `git commit` becomes an `ENT-*.md` with `source: git`, the commit SHA as `origin`, and the commit message + changed files as the body.
121
+
122
+ To remove: `rm .git/hooks/post-commit`. It's one file per repo.
123
+
124
+ ### Developing on zuun itself
125
+
126
+ If you want to hack on zuun's source:
127
+
128
+ ```bash
129
+ git clone https://github.com/gorajing/zuun.git
130
+ cd zuun
131
+ npm install
132
+ npm test # 167 tests should pass in ~5s
133
+
134
+ # Load your local working copy into Claude Code instead of the marketplace version:
135
+ claude --plugin-dir "$PWD"
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Usage
141
+
142
+ ### Agent-facing tools (MCP)
143
+
144
+ Two tools, intentionally. Expanding this surface area comes with a context-cost tax on every Claude Code turn โ€” [attention budget is a feature](#philosophy).
145
+
146
+ **`remember`** โ€” save a durable insight.
147
+
148
+ ```
149
+ # Agent-invoked mid-session:
150
+ body: "Local-first beats cloud-first for portability because tar is the moat."
151
+ kind: "decision" # or pattern | commitment | reference | observation
152
+ tags: ["architecture", "roadmap"]
153
+ # optional:
154
+ stance: "local-first is the right default for single-user dev tools"
155
+ origin: "src/lib/paths.ts" # file path, git sha, URL, session marker
156
+ ```
157
+
158
+ Dedupes same-body entries within a 10-minute window. Returns `saved ENT-<id>` or `already remembered ENT-<id>`.
159
+
160
+ **`context_for`** โ€” retrieve relevant past entries for the current work.
161
+
162
+ ```
163
+ task: "picking between SQLite and Postgres for session storage"
164
+ limit: 8
165
+ ```
166
+
167
+ Returns a markdown list. Retrieval is automatically scoped to the current git project (or "global" for entries captured outside any git repo). Hybrid-scored across FTS5, vector similarity, and recency โ€” see `zuun explain <query>` for per-component scores.
168
+
169
+ ### User-facing skill
170
+
171
+ **`/zuun:reflect`** โ€” type this at a natural breakpoint (end of session, before a break). The agent looks back at what was decided, learned, or built and calls `remember` for the 2โ€“5 highest-signal items. If the session produced nothing worth preserving, it says so and skips โ€” which is a feature, not a bug.
172
+
173
+ ### Hooks
174
+
175
+ **SessionStart injection** โ€” fires every time you open a Claude Code session. Pulls the N most recent `decision`/`pattern`/`commitment`/`reference` entries for the current project, caps at 2000 chars, and injects them as `additionalContext`. The agent's first response benefits without you saying a word. Every fire writes `session_start.inject` or `session_start.miss` to `log.jsonl` โ€” always inspectable.
176
+
177
+ **Git post-commit** โ€” opt-in per repo. After install, every `git commit` writes an entry with the message, files changed, and SHA as origin.
178
+
179
+ ### CLI reference
180
+
181
+ ```
182
+ zuun init create ~/.zuun store (entries dir + SQLite DB)
183
+ zuun capture read body from stdin, save as entry
184
+ --kind <kind> one of: decision|observation|pattern|commitment|reference
185
+ --tag <tag> repeatable
186
+
187
+ zuun search <query> hybrid search, top 10 hits
188
+ zuun explain <query> per-component scores (fts/vec/recency/final) for top 10
189
+ zuun reindex rebuild SQLite from markdown files
190
+ zuun embed backfill missing vectors (requires Ollama)
191
+
192
+ zuun forget <id> delete entry (disk + DB, crash-safe order)
193
+ zuun edit <id> open in $EDITOR; re-validate on save; DB untouched on schema failure
194
+
195
+ zuun install-git-hook install post-commit hook in current repo
196
+ zuun capture-commit (invoked by hook; not for direct use)
197
+
198
+ zuun doctor health check: disk vs DB, schema, ollama, broken refs, log tail
199
+ zuun version print version
200
+ zuun help show commands
201
+
202
+ zuun mcp run the MCP server over stdio (Claude Code invokes this)
203
+ ```
204
+
205
+ Usage examples:
206
+
207
+ ```bash
208
+ # Capture manually (or from a script, or a | pipe):
209
+ echo "Prefer execFileSync over exec to avoid shell injection." \
210
+ | zuun capture --kind pattern --tag security --tag shell
211
+
212
+ # Search:
213
+ zuun search "shell injection"
214
+
215
+ # Debug a retrieval:
216
+ zuun explain "shell injection"
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Data model
222
+
223
+ One record type โ€” `Entry`. Everything else is emergent. Full schema in [`src/lib/entry.ts`](./src/lib/entry.ts).
224
+
225
+ Required fields:
226
+
227
+ | Field | Type | Notes |
228
+ |---------|------------|-------|
229
+ | `id` | `ENT-YYMMDD-XXXX` | Content-hashed; stable across re-captures |
230
+ | `created` | ISO 8601 | UTC |
231
+ | `body` | string | One self-contained claim. "Local-first beats cloud-first becauseโ€ฆ" โ€” not a paragraph. |
232
+ | `kind` | enum | `decision` ยท `observation` ยท `pattern` ยท `commitment` ยท `reference` |
233
+ | `source`| enum | `claude-code` ยท `cursor` ยท `git` ยท `manual` ยท `import` |
234
+
235
+ Optional fields: `stance`, `tags[]`, `related[]`, `confidence`, `origin`, `project`.
236
+
237
+ Why so minimal? Schema migrations are cheap at 5 entries and painful at 5000. Biased toward "small required set, rich optional set" so auto-capture paths that can't infer stance/confidence still succeed.
238
+
239
+ ---
240
+
241
+ ## Configuration
242
+
243
+ Environment variables:
244
+
245
+ | Variable | Default | Purpose |
246
+ |----------|---------|---------|
247
+ | `ZUUN_HOME` | `~/.zuun` | Store location (entries + DB + log) |
248
+ | `OLLAMA_URL` | `http://127.0.0.1:11434` | Ollama server for embeddings |
249
+ | `ZUUN_EMBED_MODEL` | `nomic-embed-text` | Embedding model name |
250
+ | `ZUUN_SEARCH_BLEND` | `fts=0.45,vec=0.45,recency=0.1` | Hybrid search weights |
251
+ | `ZUUN_MCP_SOURCE` | `claude-code` | Tag for entries created via MCP |
252
+ | `ZUUN_BIN` | *(set by shim)* | Absolute path to `bin/zuun.js`, used by git hook installer |
253
+ | `EDITOR` | `vi` | Editor for `zuun edit` |
254
+
255
+ ---
256
+
257
+ ## Development
258
+
259
+ ```bash
260
+ npm install
261
+ npm test # full suite (167 tests, ~5s)
262
+ npm run cli -- <subcommand> # run CLI without building
263
+ npm run dev # run the MCP server directly (debugging)
264
+ ```
265
+
266
+ ### Running the test suite
267
+
268
+ Tests use Vitest with a single-fork pool (database integration tests would race under parallel isolates). The suite includes:
269
+
270
+ - **Unit tests** per module (`src/**/*.test.ts`)
271
+ - **MCP integration test** โ€” spawns `tsx src/mcp.ts`, sends JSON-RPC
272
+ - **E2E tests** (`tests/e2e.test.ts`) โ€” spawn the real `bin/zuun.js` shim to catch dispatcher bugs unit tests can't
273
+
274
+ ### Perf sanity check
275
+
276
+ ```bash
277
+ # Seeds 1000 synthetic entries with vectors and measures warm search latency:
278
+ rm -rf /tmp/zuun-perf
279
+ ZUUN_HOME=/tmp/zuun-perf bin/zuun.js init
280
+ # (see docs/plans/2026-04-16-v0-plan.md Task 25 for a recent measurement)
281
+ ```
282
+
283
+ Budget: **warm search <100ms on 1k entries**. Current on M-series Mac: ~2ms hybrid / <1ms FTS โ€” ~50ร— headroom.
284
+
285
+ ---
286
+
287
+ ## Philosophy
288
+
289
+ **Local-first.** Everything is markdown + SQLite on your disk. You can `grep` it, `git log` it, `rm -rf` it. No cloud, no account, no sync daemon. Cross-device sync is a `git push` to a private remote today; first-class support is v0.1.
290
+
291
+ **Structured, not soup.** Every entry has a `kind`; optional `stance` gives retrieval a directional claim to weight against. Rich unstructured text is easy; usefulness at scale is hard โ€” and structure is the cheapest bet on usefulness.
292
+
293
+ **Boring is the point.** Daily-use products win on invisible compounding, not cleverness. Zuun tries to be uninteresting enough that you forget it's running and only notice its absence.
294
+
295
+ **Explicit capture before automation.** v0 ships with `remember` + `/zuun:reflect` + git hooks. No LLM-based auto-distillation. Three costs to avoid until usage data justifies them: corpus pollution, context burn on every turn, and judgment quality (agents are poor self-editors mid-task).
296
+
297
+ **Attention budget is a feature.** Two MCP tools, not eight. The agent has a limited context budget; every tool schema shipped is a fixed tax on every turn. Adding surface area requires evidence, not enthusiasm.
298
+
299
+ **Passive signals over explicit ones.** Any future "was this entry useful?" feedback must be inferred from behavior (agent cites an injected entry's ID, user edits vs. forgets it) โ€” never an explicit rate-this-entry UI. Friction kills capture rates; absence of friction is the whole product.
300
+
301
+ ---
302
+
303
+ ## Project status
304
+
305
+ **v0.1.1 released.** Published to npm; plugin installable from GitHub via the marketplace flow. 167 tests green. Full plugin surface verified end-to-end on a real Claude Code session (MCP tools, slash command, SessionStart hook, git post-commit hook). Perf within budget by ~50ร—.
306
+
307
+ This is pre-1.0 software. Schema is versioned (`schema_version: 2`); breaking changes will get a migration path, not a silent reset.
308
+
309
+ ### Roadmap
310
+
311
+ Explicitly deferred past v0.1. Each has a reason:
312
+
313
+ - **`~/.zuun` as a git repo with commit-on-write** โ€” cross-device sync + audit history. Next substantive build.
314
+ - **Cursor integration** โ€” schema supports `source: cursor`; needs a capture path.
315
+ - **`forget` / `edit` as MCP tools** โ€” CLI-only today per attention-budget principle. Adds iff usage data says agents ask users to delete/edit.
316
+ - **`export` / `import`** โ€” `tar -czf ~/zuun.tgz ~/.zuun` works today.
317
+ - **Multi-layer memory hierarchy** (short-term / long-term / reasoning traces) โ€” the `kind` field is the hook. v0.2.
318
+ - **Auto-distillation** โ€” LLM summarizes sessions into entries. Requires explicit-path usage data first (see philosophy).
319
+ - **Tag ontology** โ€” intentional non-goal. "No ontology, no hierarchy" per the schema docstring.
320
+
321
+ ---
322
+
323
+ ## Acknowledgments
324
+
325
+ Zuun was built with [Claude Code](https://docs.anthropic.com/claude-code) and used Claude Code as the primary smoke-test environment throughout development. The first working plugin smoke session found two dispatch-layer bugs that 165 passing unit tests had missed โ€” an object lesson in what "shipped" actually means.
326
+
327
+ ---
328
+
329
+ ## License
330
+
331
+ MIT ยฉ Jin Choi
package/bin/zuun.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { spawn } = require("child_process");
5
+
6
+ const distCli = path.join(__dirname, "..", "dist", "cli.js");
7
+ const srcCli = path.join(__dirname, "..", "src", "cli.ts");
8
+
9
+ async function main() {
10
+ if (fs.existsSync(distCli)) {
11
+ // Production path: compiled JS, dynamic imports resolve natively.
12
+ const mod = require(distCli);
13
+ const code = await mod.runCli(process.argv.slice(2));
14
+ process.exit(code);
15
+ }
16
+
17
+ // Dev path: run via tsx so TypeScript + dynamic imports (./foo.js โ†’ foo.ts) work.
18
+ // Spawning tsx as a subprocess is the portable way to get both the CJS require tree
19
+ // and ESM dynamic-import resolution working at once.
20
+ const tsxBin = require.resolve("tsx/cli");
21
+ const child = spawn(process.execPath, [tsxBin, srcCli, ...process.argv.slice(2)], {
22
+ stdio: "inherit",
23
+ env: { ...process.env, ZUUN_BIN: __filename },
24
+ });
25
+ child.on("exit", (code, signal) => {
26
+ if (signal) process.kill(process.pid, signal);
27
+ else process.exit(code ?? 1);
28
+ });
29
+ }
30
+
31
+ main().catch((err) => {
32
+ console.error(err);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capture = capture;
4
+ const db_1 = require("./lib/db");
5
+ const store_1 = require("./lib/store");
6
+ const entry_io_1 = require("./lib/entry-io");
7
+ const id_1 = require("./lib/id");
8
+ const embed_provider_1 = require("./lib/embed-provider");
9
+ const embed_1 = require("./lib/embed");
10
+ const entry_1 = require("./lib/entry");
11
+ const tags_1 = require("./lib/tags");
12
+ const dedup_1 = require("./lib/dedup");
13
+ const project_1 = require("./lib/project");
14
+ const log_1 = require("./lib/log");
15
+ function parseArgs(argv) {
16
+ const opts = { kind: "observation", tags: [] };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a === "--kind") {
20
+ const val = argv[++i] ?? "";
21
+ opts.kind = entry_1.EntryKind.parse(val);
22
+ }
23
+ else if (a === "--tag") {
24
+ const val = argv[++i];
25
+ if (val)
26
+ opts.tags.push(val);
27
+ }
28
+ }
29
+ return opts;
30
+ }
31
+ async function readStdin() {
32
+ const chunks = [];
33
+ for await (const chunk of process.stdin) {
34
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
35
+ }
36
+ return Buffer.concat(chunks).toString("utf8").trim();
37
+ }
38
+ async function capture(argv) {
39
+ let opts;
40
+ try {
41
+ opts = parseArgs(argv);
42
+ }
43
+ catch (err) {
44
+ process.stderr.write(`capture: ${err.message}\n`);
45
+ return 1;
46
+ }
47
+ const body = await readStdin();
48
+ if (body.length === 0) {
49
+ process.stderr.write("capture: no body on stdin\n");
50
+ return 1;
51
+ }
52
+ const now = new Date();
53
+ const db = (0, db_1.openDb)();
54
+ try {
55
+ const existing = (0, dedup_1.findRecentDuplicate)(db, body, now);
56
+ if (existing) {
57
+ process.stdout.write(`${existing}\n`);
58
+ (0, log_1.appendLog)("capture.dedup", { id: existing });
59
+ return 0;
60
+ }
61
+ const id = (0, id_1.newEntryId)(body, now);
62
+ const entry = {
63
+ id,
64
+ created: now.toISOString(),
65
+ body,
66
+ kind: opts.kind,
67
+ source: "manual",
68
+ tags: (0, tags_1.normalizeTags)(opts.tags),
69
+ related: [],
70
+ project: (0, project_1.resolveProject)(),
71
+ };
72
+ (0, entry_io_1.writeEntry)(entry);
73
+ (0, store_1.upsertEntry)(db, entry);
74
+ const vec = await embed_provider_1.defaultProvider.embed(body);
75
+ if (vec)
76
+ (0, embed_1.setEmbedding)(db, id, vec);
77
+ (0, log_1.appendLog)("capture", { id, kind: entry.kind, tags: entry.tags, project: entry.project });
78
+ process.stdout.write(`${id}\n`);
79
+ return 0;
80
+ }
81
+ finally {
82
+ db.close();
83
+ }
84
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,205 @@
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.runCli = runCli;
37
+ const fs = __importStar(require("fs"));
38
+ const db_1 = require("./lib/db");
39
+ const paths_1 = require("./lib/paths");
40
+ const reindex_1 = require("./scripts/reindex");
41
+ const embed_1 = require("./lib/embed");
42
+ const embed_provider_1 = require("./lib/embed-provider");
43
+ const search_1 = require("./lib/search");
44
+ const doctor_1 = require("./lib/doctor");
45
+ const log_1 = require("./lib/log");
46
+ // Kept in sync with package.json on release. If this drifts again, switch to
47
+ // a runtime read of package.json (deferred: avoiding rootDir + tsc complexity).
48
+ const VERSION = "0.1.1";
49
+ const HELP = `usage: zuun <command>
50
+
51
+ commands:
52
+ init create the store directory and index
53
+ mcp run the MCP server over stdio
54
+ capture read an entry body from stdin and save it
55
+ search QRY print top matches for QRY
56
+ explain QRY show per-component scores (fts/vec/recency) for top hits
57
+ reindex rebuild the sqlite index from markdown files
58
+ embed embed all entries missing vectors (requires Ollama)
59
+ forget ID delete an entry cleanly (disk + db)
60
+ edit ID open an entry in $EDITOR and re-validate on save
61
+ install-git-hook install a git post-commit hook in the current repo
62
+ capture-commit capture the latest git commit (invoked by the post-commit hook)
63
+ doctor health check: entries, db, ollama, broken refs
64
+ version print the zuun version
65
+ help show this message
66
+ `;
67
+ async function runCli(argv) {
68
+ const [cmd, ...rest] = argv;
69
+ if (!cmd) {
70
+ process.stderr.write(HELP);
71
+ return 1;
72
+ }
73
+ switch (cmd) {
74
+ case "init":
75
+ return cmdInit();
76
+ case "mcp":
77
+ await Promise.resolve().then(() => __importStar(require("./mcp.js")));
78
+ // mcp.ts registers stdin handlers (StdioServerTransport) that keep the
79
+ // process alive. Returning here would cause the outer
80
+ // runCli(...).then(process.exit) to terminate the server before it
81
+ // handles a single request. Hang forever; the MCP server's own lifecycle
82
+ // (stdin EOF โ†’ process exits naturally) handles shutdown.
83
+ return new Promise(() => { });
84
+ case "reindex":
85
+ return cmdReindex();
86
+ case "embed":
87
+ return cmdEmbed();
88
+ case "capture": {
89
+ const { capture } = await Promise.resolve().then(() => __importStar(require("./capture.js")));
90
+ return capture(rest);
91
+ }
92
+ case "search":
93
+ return cmdSearch(rest);
94
+ case "forget": {
95
+ const { forget } = await Promise.resolve().then(() => __importStar(require("./commands/forget.js")));
96
+ return forget(rest);
97
+ }
98
+ case "edit": {
99
+ const { edit } = await Promise.resolve().then(() => __importStar(require("./commands/edit.js")));
100
+ return edit(rest);
101
+ }
102
+ case "explain": {
103
+ const { explain } = await Promise.resolve().then(() => __importStar(require("./commands/explain.js")));
104
+ return explain(rest);
105
+ }
106
+ case "install-git-hook": {
107
+ const { installGitHook } = await Promise.resolve().then(() => __importStar(require("./commands/install-git-hook.js")));
108
+ return installGitHook(rest);
109
+ }
110
+ case "capture-commit": {
111
+ const { captureCommit } = await Promise.resolve().then(() => __importStar(require("./commands/capture-commit.js")));
112
+ return captureCommit(rest);
113
+ }
114
+ case "session-start": {
115
+ const { runSessionStart } = await Promise.resolve().then(() => __importStar(require("./hook-scripts/session-start.js")));
116
+ let raw = "";
117
+ process.stdin.setEncoding("utf8");
118
+ for await (const chunk of process.stdin)
119
+ raw += chunk;
120
+ let cwd = process.cwd();
121
+ try {
122
+ const parsed = JSON.parse(raw);
123
+ if (parsed.cwd)
124
+ cwd = parsed.cwd;
125
+ }
126
+ catch {
127
+ /* ignore */
128
+ }
129
+ await runSessionStart({ cwd });
130
+ return 0;
131
+ }
132
+ case "doctor":
133
+ return cmdDoctor();
134
+ case "version":
135
+ process.stdout.write(`zuun ${VERSION}\n`);
136
+ return 0;
137
+ case "help":
138
+ process.stdout.write(HELP);
139
+ return 0;
140
+ default:
141
+ process.stderr.write(`unknown command: ${cmd}\n\n${HELP}`);
142
+ return 1;
143
+ }
144
+ }
145
+ function cmdInit() {
146
+ fs.mkdirSync((0, paths_1.entriesDir)(), { recursive: true });
147
+ (0, db_1.openDb)().close();
148
+ (0, log_1.appendLog)("init", { root: (0, paths_1.storeRoot)() });
149
+ process.stdout.write(`initialized zuun store at ${(0, paths_1.storeRoot)()}\n`);
150
+ return 0;
151
+ }
152
+ function cmdReindex() {
153
+ const r = (0, reindex_1.reindex)();
154
+ process.stdout.write(`indexed ${r.indexed}, failed ${r.failed.length}\n`);
155
+ for (const line of r.failed)
156
+ process.stderr.write(` ${line}\n`);
157
+ return r.failed.length === 0 ? 0 : 1;
158
+ }
159
+ async function cmdEmbed() {
160
+ const db = (0, db_1.openDb)();
161
+ try {
162
+ const r = await (0, embed_1.embedMissing)(db, embed_provider_1.defaultProvider);
163
+ process.stdout.write(`embedded ${r.embedded}, skipped ${r.skipped}\n`);
164
+ return 0;
165
+ }
166
+ finally {
167
+ db.close();
168
+ }
169
+ }
170
+ async function cmdSearch(args) {
171
+ const query = args.join(" ");
172
+ if (!query) {
173
+ process.stderr.write("usage: zuun search <query>\n");
174
+ return 1;
175
+ }
176
+ const db = (0, db_1.openDb)();
177
+ try {
178
+ const qVec = await embed_provider_1.defaultProvider.embed(query);
179
+ const results = (0, search_1.search)(db, { query, queryVec: qVec ?? undefined, limit: 10 });
180
+ if (results.length === 0) {
181
+ process.stdout.write("(no results)\n");
182
+ return 0;
183
+ }
184
+ for (const r of results) {
185
+ process.stdout.write(`${r.entry.id} ยท ${r.entry.kind} ยท ${r.entry.created}\n ${r.entry.body.replace(/\n/g, " ")}\n\n`);
186
+ }
187
+ return 0;
188
+ }
189
+ finally {
190
+ db.close();
191
+ }
192
+ }
193
+ async function cmdDoctor() {
194
+ const report = await (0, doctor_1.runDoctor)();
195
+ process.stdout.write(report.text);
196
+ return report.healthy ? 0 : 1;
197
+ }
198
+ // Direct invocation via `tsx src/cli.ts ...` (e.g., from bin/zuun.js dev path):
199
+ // process.argv[1] points at cli.ts. Run the dispatcher.
200
+ if (process.argv[1] && process.argv[1].endsWith("cli.ts")) {
201
+ runCli(process.argv.slice(2)).then((code) => process.exit(code));
202
+ }
203
+ else if (require.main === module) {
204
+ runCli(process.argv.slice(2)).then((code) => process.exit(code));
205
+ }