@gorajing/zuun 0.1.1 → 0.1.3
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 +5 -5
- package/dist/capture.js +16 -2
- package/dist/cli.js +2 -2
- package/dist/lib/audit.js +37 -0
- package/dist/lib/doctor.js +18 -0
- package/dist/lib/entry.js +1 -0
- package/dist/lib/lints.js +62 -0
- package/dist/mcp.js +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ For the git post-commit hook, manual captures, and shell pipelines:
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
npm install -g @gorajing/zuun
|
|
93
|
-
zuun --version # should print 0.1.
|
|
93
|
+
zuun --version # should print 0.1.3
|
|
94
94
|
```
|
|
95
95
|
|
|
96
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.)
|
|
@@ -195,7 +195,7 @@ zuun edit <id> open in $EDITOR; re-validate on save; DB untouched
|
|
|
195
195
|
zuun install-git-hook install post-commit hook in current repo
|
|
196
196
|
zuun capture-commit (invoked by hook; not for direct use)
|
|
197
197
|
|
|
198
|
-
zuun doctor health check: disk vs DB, schema, ollama, broken refs, log tail
|
|
198
|
+
zuun doctor health check: disk vs DB, schema, ollama, broken refs, audit cadence, log tail
|
|
199
199
|
zuun version print version
|
|
200
200
|
zuun help show commands
|
|
201
201
|
|
|
@@ -230,7 +230,7 @@ Required fields:
|
|
|
230
230
|
| `created` | ISO 8601 | UTC |
|
|
231
231
|
| `body` | string | One self-contained claim. "Local-first beats cloud-first because…" — not a paragraph. |
|
|
232
232
|
| `kind` | enum | `decision` · `observation` · `pattern` · `commitment` · `reference` |
|
|
233
|
-
| `source`| enum | `claude-code` · `cursor` · `git` · `manual` · `import` |
|
|
233
|
+
| `source`| enum | `claude-code` · `cursor` · `codex` · `git` · `manual` · `import` |
|
|
234
234
|
|
|
235
235
|
Optional fields: `stance`, `tags[]`, `related[]`, `confidence`, `origin`, `project`.
|
|
236
236
|
|
|
@@ -248,7 +248,7 @@ Environment variables:
|
|
|
248
248
|
| `OLLAMA_URL` | `http://127.0.0.1:11434` | Ollama server for embeddings |
|
|
249
249
|
| `ZUUN_EMBED_MODEL` | `nomic-embed-text` | Embedding model name |
|
|
250
250
|
| `ZUUN_SEARCH_BLEND` | `fts=0.45,vec=0.45,recency=0.1` | Hybrid search weights |
|
|
251
|
-
| `ZUUN_MCP_SOURCE` | `claude-code` |
|
|
251
|
+
| `ZUUN_MCP_SOURCE` | `claude-code` | `source` tag for entries created via MCP — any `source` enum value (e.g. `codex` when wired into Codex). Unrecognized values fall back to `claude-code`. |
|
|
252
252
|
| `ZUUN_BIN` | *(set by shim)* | Absolute path to `bin/zuun.js`, used by git hook installer |
|
|
253
253
|
| `EDITOR` | `vi` | Editor for `zuun edit` |
|
|
254
254
|
|
|
@@ -302,7 +302,7 @@ Budget: **warm search <100ms on 1k entries**. Current on M-series Mac: ~2ms hybr
|
|
|
302
302
|
|
|
303
303
|
## Project status
|
|
304
304
|
|
|
305
|
-
**v0.1.
|
|
305
|
+
**v0.1.3 released.** `codex` is now a first-class entry source: wire `zuun mcp` into Codex (OpenAI CLI) via `[mcp_servers.zuun]` with `ZUUN_MCP_SOURCE=codex`, and Codex-captured memories are attributed correctly instead of falling back to `claude-code`. 203 tests green. Published to npm; plugin installable from GitHub via the marketplace flow, with the 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×. See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
306
306
|
|
|
307
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
308
|
|
package/dist/capture.js
CHANGED
|
@@ -9,6 +9,7 @@ const embed_provider_1 = require("./lib/embed-provider");
|
|
|
9
9
|
const embed_1 = require("./lib/embed");
|
|
10
10
|
const entry_1 = require("./lib/entry");
|
|
11
11
|
const tags_1 = require("./lib/tags");
|
|
12
|
+
const lints_1 = require("./lib/lints");
|
|
12
13
|
const dedup_1 = require("./lib/dedup");
|
|
13
14
|
const project_1 = require("./lib/project");
|
|
14
15
|
const log_1 = require("./lib/log");
|
|
@@ -58,6 +59,14 @@ async function capture(argv) {
|
|
|
58
59
|
(0, log_1.appendLog)("capture.dedup", { id: existing });
|
|
59
60
|
return 0;
|
|
60
61
|
}
|
|
62
|
+
// Lint on RAW tags before normalization: normalizeTags lowercases, but an
|
|
63
|
+
// entry ID is uppercase-hex, so a polluting ENT token must be detected and
|
|
64
|
+
// routed to related here or it becomes undetectable (ENT-260514-4BA0).
|
|
65
|
+
const routing = (0, lints_1.routeEntTokensFromTags)(opts.tags, []);
|
|
66
|
+
if (routing.routed.length > 0) {
|
|
67
|
+
process.stderr.write(`capture: moved entry id(s) from tags to related: ${routing.routed.join(", ")} ` +
|
|
68
|
+
`(tags are for topics; related links entries)\n`);
|
|
69
|
+
}
|
|
61
70
|
const id = (0, id_1.newEntryId)(body, now);
|
|
62
71
|
const entry = {
|
|
63
72
|
id,
|
|
@@ -65,10 +74,15 @@ async function capture(argv) {
|
|
|
65
74
|
body,
|
|
66
75
|
kind: opts.kind,
|
|
67
76
|
source: "manual",
|
|
68
|
-
tags: (0, tags_1.normalizeTags)(
|
|
69
|
-
related:
|
|
77
|
+
tags: (0, tags_1.normalizeTags)(routing.tags),
|
|
78
|
+
related: routing.related,
|
|
70
79
|
project: (0, project_1.resolveProject)(),
|
|
71
80
|
};
|
|
81
|
+
// short-decision-no-tags lint; provenance ENT-260514-75A3 (see lints.ts).
|
|
82
|
+
if ((0, lints_1.isShortDecisionNoTags)(entry)) {
|
|
83
|
+
process.stderr.write(`capture: ${id} is a short decision with no tags — likely an accidental ` +
|
|
84
|
+
`auto-capture; 'zuun forget ${id}' if it is not a durable choice\n`);
|
|
85
|
+
}
|
|
72
86
|
(0, entry_io_1.writeEntry)(entry);
|
|
73
87
|
(0, store_1.upsertEntry)(db, entry);
|
|
74
88
|
const vec = await embed_provider_1.defaultProvider.embed(body);
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ const doctor_1 = require("./lib/doctor");
|
|
|
45
45
|
const log_1 = require("./lib/log");
|
|
46
46
|
// Kept in sync with package.json on release. If this drifts again, switch to
|
|
47
47
|
// a runtime read of package.json (deferred: avoiding rootDir + tsc complexity).
|
|
48
|
-
const VERSION = "0.1.
|
|
48
|
+
const VERSION = "0.1.3";
|
|
49
49
|
const HELP = `usage: zuun <command>
|
|
50
50
|
|
|
51
51
|
commands:
|
|
@@ -60,7 +60,7 @@ commands:
|
|
|
60
60
|
edit ID open an entry in $EDITOR and re-validate on save
|
|
61
61
|
install-git-hook install a git post-commit hook in the current repo
|
|
62
62
|
capture-commit capture the latest git commit (invoked by the post-commit hook)
|
|
63
|
-
doctor health check: entries, db, ollama, broken refs
|
|
63
|
+
doctor health check: entries, db, ollama, broken refs, audit cadence
|
|
64
64
|
version print the zuun version
|
|
65
65
|
help show this message
|
|
66
66
|
`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUDIT_THRESHOLD = void 0;
|
|
4
|
+
exports.auditStatus = auditStatus;
|
|
5
|
+
/**
|
|
6
|
+
* Re-audit cadence. ENT-260420-4BDD committed to a corpus re-audit "at ~50
|
|
7
|
+
* entries"; ENT-260514-98FC observed that passive scheduling let the corpus
|
|
8
|
+
* run 67 entries past that point before an audit fired. doctor enforces it.
|
|
9
|
+
*/
|
|
10
|
+
exports.AUDIT_THRESHOLD = 50;
|
|
11
|
+
/** Entries tagged this are treated as the marker an audit was performed. */
|
|
12
|
+
const AUDIT_TAG = "audit";
|
|
13
|
+
/**
|
|
14
|
+
* Audit state derived from the corpus itself — no sidecar file to drift out of
|
|
15
|
+
* sync. An audit run leaves entries tagged `audit`; the newest one marks when
|
|
16
|
+
* the last audit happened. Entries created after it are the un-audited backlog.
|
|
17
|
+
*/
|
|
18
|
+
function auditStatus(entries) {
|
|
19
|
+
let last = null;
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
if (e.tags.includes(AUDIT_TAG) && (last === null || e.created > last.created)) {
|
|
22
|
+
last = e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (last === null) {
|
|
26
|
+
const sinceAudit = entries.length;
|
|
27
|
+
return { lastAuditId: null, lastAuditAt: null, sinceAudit, overdue: sinceAudit >= exports.AUDIT_THRESHOLD };
|
|
28
|
+
}
|
|
29
|
+
const cutoff = last.created;
|
|
30
|
+
const sinceAudit = entries.reduce((n, e) => (e.created > cutoff ? n + 1 : n), 0);
|
|
31
|
+
return {
|
|
32
|
+
lastAuditId: last.id,
|
|
33
|
+
lastAuditAt: last.created,
|
|
34
|
+
sinceAudit,
|
|
35
|
+
overdue: sinceAudit >= exports.AUDIT_THRESHOLD,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/lib/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ const entry_io_1 = require("./entry-io");
|
|
|
6
6
|
const store_1 = require("./store");
|
|
7
7
|
const embed_provider_1 = require("./embed-provider");
|
|
8
8
|
const log_1 = require("./log");
|
|
9
|
+
const audit_1 = require("./audit");
|
|
9
10
|
async function runDoctor() {
|
|
10
11
|
const db = (0, db_1.openDb)();
|
|
11
12
|
const lines = [];
|
|
@@ -29,6 +30,23 @@ async function runDoctor() {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
lines.push(`broken related refs: ${broken}`);
|
|
33
|
+
// Audit-cadence enforcement; provenance: ENT-260420-4BDD, ENT-260514-98FC
|
|
34
|
+
// (see audit.ts). Spec IDs stay in comments — output must stand alone for
|
|
35
|
+
// users who don't have this corpus.
|
|
36
|
+
const audit = (0, audit_1.auditStatus)(inDb);
|
|
37
|
+
if (audit.overdue) {
|
|
38
|
+
healthy = false;
|
|
39
|
+
const baseline = audit.lastAuditId
|
|
40
|
+
? `last audit ${audit.lastAuditId} (${audit.lastAuditAt})`
|
|
41
|
+
: "no audit on record";
|
|
42
|
+
lines.push(`WARN: audit overdue — ${audit.sinceAudit} entries since ${audit.lastAuditId ? "last audit" : "first capture"} (threshold ${audit_1.AUDIT_THRESHOLD}); ${baseline}; run a corpus re-audit`);
|
|
43
|
+
}
|
|
44
|
+
else if (audit.lastAuditId) {
|
|
45
|
+
lines.push(`audit: ${audit.sinceAudit} entries since last audit ${audit.lastAuditId} (threshold ${audit_1.AUDIT_THRESHOLD})`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
lines.push(`audit: ${audit.sinceAudit} entries, never audited (threshold ${audit_1.AUDIT_THRESHOLD})`);
|
|
49
|
+
}
|
|
32
50
|
const vec = await embed_provider_1.defaultProvider.embed("doctor-check");
|
|
33
51
|
lines.push(`ollama: ${vec ? "up" : "down"}`);
|
|
34
52
|
if (!vec)
|
package/dist/lib/entry.js
CHANGED
|
@@ -28,6 +28,7 @@ exports.EntryKind = zod_1.z.enum([
|
|
|
28
28
|
exports.EntrySource = zod_1.z.enum([
|
|
29
29
|
"claude-code", // auto-captured from a Claude Code session
|
|
30
30
|
"cursor", // auto-captured from Cursor
|
|
31
|
+
"codex", // captured from a Codex (OpenAI CLI) session via the MCP server
|
|
31
32
|
"git", // captured from a commit / PR / diff
|
|
32
33
|
"manual", // user typed it in via remember() or CLI
|
|
33
34
|
"import", // bulk-imported from another system
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SHORT_DECISION_MAX = void 0;
|
|
4
|
+
exports.routeEntTokensFromTags = routeEntTokensFromTags;
|
|
5
|
+
exports.isShortDecisionNoTags = isShortDecisionNoTags;
|
|
6
|
+
const entry_1 = require("./entry");
|
|
7
|
+
/**
|
|
8
|
+
* Capture-path lints derived from the 2026-05-14 corpus audit.
|
|
9
|
+
*
|
|
10
|
+
* These run on the *raw* capture input, before tag normalization — by design.
|
|
11
|
+
* `normalizeTags` lowercases, but ENTRY_ID_REGEX is uppercase-hex only, so an
|
|
12
|
+
* ENT token routed after normalization would be undetectable (ENT-260514-4BA0).
|
|
13
|
+
*/
|
|
14
|
+
function isEntToken(raw) {
|
|
15
|
+
return entry_1.ENTRY_ID_REGEX.test(raw.trim().toUpperCase());
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* ENT-token-in-tags lint (ENT-260514-4BA0). Tag-index pollution: entry IDs
|
|
19
|
+
* listed under `tags:` instead of `related:`. Strip ENT-shaped tokens from
|
|
20
|
+
* tags and route them to related as canonical uppercase IDs.
|
|
21
|
+
*/
|
|
22
|
+
function routeEntTokensFromTags(tags, related) {
|
|
23
|
+
const keptTags = [];
|
|
24
|
+
const relatedSeen = new Set(related.map((r) => r.toUpperCase()));
|
|
25
|
+
const mergedRelated = [...related];
|
|
26
|
+
const routedSeen = new Set();
|
|
27
|
+
const routed = [];
|
|
28
|
+
for (const t of tags) {
|
|
29
|
+
if (!isEntToken(t)) {
|
|
30
|
+
keptTags.push(t);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const id = t.trim().toUpperCase();
|
|
34
|
+
// routed = every ENT token stripped from tags (deduped), even if it was
|
|
35
|
+
// already in related — the polluting tag was still removed and should be
|
|
36
|
+
// reported. mergedRelated only gains IDs not already linked.
|
|
37
|
+
if (!routedSeen.has(id)) {
|
|
38
|
+
routedSeen.add(id);
|
|
39
|
+
routed.push(id);
|
|
40
|
+
}
|
|
41
|
+
if (!relatedSeen.has(id)) {
|
|
42
|
+
relatedSeen.add(id);
|
|
43
|
+
mergedRelated.push(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { tags: keptTags, related: mergedRelated, routed };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A decision shorter than this (trimmed) with no tags reads as transient
|
|
50
|
+
* state, not a durable choice — likely an accidental auto-capture.
|
|
51
|
+
*/
|
|
52
|
+
exports.SHORT_DECISION_MAX = 80;
|
|
53
|
+
/**
|
|
54
|
+
* short-decision-no-tags lint (ENT-260514-75A3). A `kind=decision` entry whose
|
|
55
|
+
* trimmed body is under SHORT_DECISION_MAX with no tags is a stub-capture
|
|
56
|
+
* candidate. Soft signal — capture still succeeds; the author verifies.
|
|
57
|
+
*/
|
|
58
|
+
function isShortDecisionNoTags(input) {
|
|
59
|
+
return (input.kind === "decision" &&
|
|
60
|
+
input.body.trim().length < exports.SHORT_DECISION_MAX &&
|
|
61
|
+
input.tags.length === 0);
|
|
62
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ const embed_provider_1 = require("./lib/embed-provider");
|
|
|
14
14
|
const embed_1 = require("./lib/embed");
|
|
15
15
|
const entry_1 = require("./lib/entry");
|
|
16
16
|
const tags_1 = require("./lib/tags");
|
|
17
|
+
const lints_1 = require("./lib/lints");
|
|
17
18
|
const dedup_1 = require("./lib/dedup");
|
|
18
19
|
const project_1 = require("./lib/project");
|
|
19
20
|
const log_1 = require("./lib/log");
|
|
@@ -58,7 +59,7 @@ SCOPING: Retrieval is automatically scoped to the current project (resolved from
|
|
|
58
59
|
|
|
59
60
|
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
61
|
async function main() {
|
|
61
|
-
const server = new index_js_1.Server({ name: "zuun", version: "0.1.
|
|
62
|
+
const server = new index_js_1.Server({ name: "zuun", version: "0.1.3" }, { capabilities: { tools: {} } });
|
|
62
63
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
63
64
|
tools: [
|
|
64
65
|
{
|
|
@@ -140,6 +141,9 @@ async function handleRemember(args) {
|
|
|
140
141
|
],
|
|
141
142
|
};
|
|
142
143
|
}
|
|
144
|
+
// Lint raw tags before normalization (normalizeTags lowercases; entry IDs
|
|
145
|
+
// are uppercase-hex, so an ENT token must be routed here — ENT-260514-4BA0).
|
|
146
|
+
const routing = (0, lints_1.routeEntTokensFromTags)(input.tags, input.related);
|
|
143
147
|
const id = (0, id_1.newEntryId)(input.body, now);
|
|
144
148
|
const entry = {
|
|
145
149
|
id,
|
|
@@ -147,12 +151,23 @@ async function handleRemember(args) {
|
|
|
147
151
|
body: input.body,
|
|
148
152
|
kind: input.kind,
|
|
149
153
|
source: SOURCE,
|
|
150
|
-
tags: (0, tags_1.normalizeTags)(
|
|
151
|
-
related:
|
|
154
|
+
tags: (0, tags_1.normalizeTags)(routing.tags),
|
|
155
|
+
related: routing.related,
|
|
152
156
|
stance: input.stance,
|
|
153
157
|
origin: input.origin,
|
|
154
158
|
project: input.project ?? (0, project_1.resolveProject)(),
|
|
155
159
|
};
|
|
160
|
+
// Lint notes for the calling agent. Provenance (ENT-260514-4BA0,
|
|
161
|
+
// ENT-260514-75A3) lives in lints.ts; keep it out of agent-facing text.
|
|
162
|
+
const notes = [];
|
|
163
|
+
if (routing.routed.length > 0) {
|
|
164
|
+
notes.push(`note: moved ${routing.routed.join(", ")} from tags to related — ` +
|
|
165
|
+
`tags are for topics, related links entries`);
|
|
166
|
+
}
|
|
167
|
+
if ((0, lints_1.isShortDecisionNoTags)(entry)) {
|
|
168
|
+
notes.push(`note: this is a short decision with no tags — confirm it is a durable ` +
|
|
169
|
+
`choice, not transient state, or it reads as an accidental capture`);
|
|
170
|
+
}
|
|
156
171
|
(0, entry_io_1.writeEntry)(entry);
|
|
157
172
|
(0, store_1.upsertEntry)(db, entry);
|
|
158
173
|
(0, log_1.appendLog)("remember", { id, kind: entry.kind, tags: entry.tags });
|
|
@@ -172,9 +187,10 @@ async function handleRemember(args) {
|
|
|
172
187
|
})
|
|
173
188
|
.catch(() => { });
|
|
174
189
|
const tagLine = entry.tags.length ? ` · tags: ${entry.tags.join(", ")}` : "";
|
|
190
|
+
const noteLine = notes.length ? `\n${notes.join("\n")}` : "";
|
|
175
191
|
return {
|
|
176
192
|
content: [
|
|
177
|
-
{ type: "text", text: `saved ${id} (${entry.kind}${tagLine})\n${entry.body}` },
|
|
193
|
+
{ type: "text", text: `saved ${id} (${entry.kind}${tagLine})\n${entry.body}${noteLine}` },
|
|
178
194
|
],
|
|
179
195
|
};
|
|
180
196
|
}
|
package/package.json
CHANGED