@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 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.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` | Tag for entries created via MCP |
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.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×.
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)(opts.tags),
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.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
+ }
@@ -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.1" }, { capabilities: { tools: {} } });
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)(input.tags),
151
- related: input.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorajing/zuun",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Persistent memory for AI-assisted work — local-first markdown + SQLite, hybrid search, Claude Code plugin.",
5
5
  "author": "Jin Choi",
6
6
  "license": "MIT",