codealmanac 0.1.10 → 0.2.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.
Files changed (53) hide show
  1. package/README.md +124 -104
  2. package/dist/agents-A4II4YJC.js +15 -0
  3. package/dist/auth-S5DVUIUJ.js +18 -0
  4. package/dist/{chunk-Z4MWLVS2.js → chunk-447U3GQJ.js} +162 -5
  5. package/dist/chunk-447U3GQJ.js.map +1 -0
  6. package/dist/{chunk-QLHJP2XK.js → chunk-B2AGSRXL.js} +13 -9
  7. package/dist/{chunk-QLHJP2XK.js.map → chunk-B2AGSRXL.js.map} +1 -1
  8. package/dist/{chunk-AXFPUHBN.js → chunk-F53U6JQG.js} +8 -49
  9. package/dist/chunk-F53U6JQG.js.map +1 -0
  10. package/dist/{chunk-3C5SY5SE.js → chunk-KQUVMF27.js} +5 -2
  11. package/dist/chunk-KQUVMF27.js.map +1 -0
  12. package/dist/{chunk-BJVZLP6O.js → chunk-MX2EW5MR.js} +3 -3
  13. package/dist/{chunk-Z6MBJ3D2.js → chunk-QQHIVTXT.js} +6 -4
  14. package/dist/{chunk-Z6MBJ3D2.js.map → chunk-QQHIVTXT.js.map} +1 -1
  15. package/dist/chunk-R3URPHGH.js +194 -0
  16. package/dist/chunk-R3URPHGH.js.map +1 -0
  17. package/dist/chunk-SSYMRT4I.js +126 -0
  18. package/dist/chunk-SSYMRT4I.js.map +1 -0
  19. package/dist/{chunk-QHQ6YH7U.js → chunk-V3QOQSXI.js} +5 -3
  20. package/dist/{chunk-QHQ6YH7U.js.map → chunk-V3QOQSXI.js.map} +1 -1
  21. package/dist/chunk-WRUSDYYE.js +97 -0
  22. package/dist/chunk-WRUSDYYE.js.map +1 -0
  23. package/dist/{chunk-3LC55TG6.js → chunk-ZDJSJIB6.js} +77 -126
  24. package/dist/chunk-ZDJSJIB6.js.map +1 -0
  25. package/dist/{cli-W3OYVJYH.js → cli-MZEXRV6E.js} +238 -24
  26. package/dist/cli-MZEXRV6E.js.map +1 -0
  27. package/dist/codealmanac.js +1 -1
  28. package/dist/doctor-3BYSF3JD.js +17 -0
  29. package/dist/{hook-CRJMWSSO.js → hook-2NP3UE7U.js} +2 -2
  30. package/dist/{register-commands-JHC2OFKM.js → register-commands-DPH4ZWEE.js} +621 -60
  31. package/dist/register-commands-DPH4ZWEE.js.map +1 -0
  32. package/dist/uninstall-FDIOBAAR.js +15 -0
  33. package/dist/uninstall-FDIOBAAR.js.map +1 -0
  34. package/dist/update-RAF7QRYF.js +11 -0
  35. package/dist/update-RAF7QRYF.js.map +1 -0
  36. package/dist/{wiki-IPSRRGOT.js → wiki-IGNRNLUZ.js} +2 -2
  37. package/hooks/almanac-capture.sh +40 -7
  38. package/package.json +3 -2
  39. package/dist/chunk-3C5SY5SE.js.map +0 -1
  40. package/dist/chunk-3LC55TG6.js.map +0 -1
  41. package/dist/chunk-AXFPUHBN.js.map +0 -1
  42. package/dist/chunk-Z4MWLVS2.js.map +0 -1
  43. package/dist/cli-W3OYVJYH.js.map +0 -1
  44. package/dist/doctor-ODFNJUKH.js +0 -15
  45. package/dist/register-commands-JHC2OFKM.js.map +0 -1
  46. package/dist/uninstall-HE2Z2LN2.js +0 -12
  47. package/dist/update-IL243I4E.js +0 -10
  48. /package/dist/{doctor-ODFNJUKH.js.map → agents-A4II4YJC.js.map} +0 -0
  49. /package/dist/{hook-CRJMWSSO.js.map → auth-S5DVUIUJ.js.map} +0 -0
  50. /package/dist/{chunk-BJVZLP6O.js.map → chunk-MX2EW5MR.js.map} +0 -0
  51. /package/dist/{uninstall-HE2Z2LN2.js.map → doctor-3BYSF3JD.js.map} +0 -0
  52. /package/dist/{update-IL243I4E.js.map → hook-2NP3UE7U.js.map} +0 -0
  53. /package/dist/{wiki-IPSRRGOT.js.map → wiki-IGNRNLUZ.js.map} +0 -0
package/README.md CHANGED
@@ -1,18 +1,32 @@
1
1
  # codealmanac
2
2
 
3
- codealmanac maintains a `.almanac/` folder in your repo that AI coding agents populate with decisions, gotchas, flows, and invariants — the context the code itself can't tell you. Pages are atomic markdown files, interlinked by `[[wikilinks]]`, indexed in SQLite, and written by a writer/reviewer agent pair that runs at session end.
3
+ A living wiki for your codebase, maintained by AI agents. It documents what the code can't say decisions, flows, invariants, gotchas as atomic, interlinked markdown pages living at `.almanac/` in your repo.
4
+
5
+ ```
6
+ your-repo/
7
+ ├── src/
8
+ ├── .almanac/
9
+ │ ├── pages/
10
+ │ │ ├── supabase.md
11
+ │ │ ├── checkout-flow.md
12
+ │ │ └── uuid-decision.md
13
+ │ ├── topics.yaml
14
+ │ └── index.db ← auto-generated SQLite index
15
+ ├── .git/
16
+ └── ...
17
+ ```
4
18
 
5
19
  The primary consumer is the AI coding agent. The secondary consumer is humans.
6
20
 
7
21
  ## Why
8
22
 
9
- Claude Code, Cursor, and Copilot can read the code and tell you what it does. They can't tell you why it's shaped that way, what approaches were tried and rejected, what invariants must not be violated, or how a flow spans four files in three services. That knowledge lives in Slack threads, PR descriptions, and people's heads. It dies when threads scroll, people leave, or an agent starts a fresh session.
23
+ Claude Code, Cursor, and Copilot can read the code and tell you what it does. They can't tell you _why_ it's shaped that way, what approaches were tried and rejected, what invariants must not be violated, or how a flow spans four files in three services. That knowledge lives in Slack threads, PR descriptions, and people's heads. It dies when threads scroll, people leave, or an agent starts a fresh session.
10
24
 
11
25
  A single `CLAUDE.md` at the repo root doesn't scale past a few hundred lines, has no graph structure, and gets stale the moment anyone commits without editing it. codealmanac replaces that one flat file with a wiki of atomic pages that agents are prompted to keep current as a side-effect of coding.
12
26
 
13
27
  ## How it works
14
28
 
15
- Each repo gets a committed `.almanac/pages/` directory of markdown files. A SessionEnd hook fires when a Claude Code session ends and runs `almanac capture` in the background. A writer agent reads the session transcript and existing pages, drafts changes, and invokes a reviewer subagent that critiques against the wider graph. The writer applies the final versions. New and updated pages show up in your next `git status`; you review them like any other commit.
29
+ Each repo gets a committed `.almanac/pages/` directory of markdown files. Auto-capture hooks fire when Claude Code, Codex, or Cursor Agent sessions end and run `almanac capture` in the background. A writer agent reads the session transcript and existing pages, drafts changes, and runs a reviewer pass against the wider graph. The writer applies the final versions. New and updated pages show up in your next `git status`; you review them like any other commit.
16
30
 
17
31
  The CLI never reads or writes page content except in `capture` and `bootstrap`. Every other command (`search`, `show`, `topics`, `tag`, `health`) operates on a SQLite index that rebuilds silently whenever pages are newer than the index.
18
32
 
@@ -28,8 +42,9 @@ codealmanac --yes
28
42
  ```
29
43
 
30
44
  `codealmanac` (the bare invocation) routes to a setup wizard that:
31
- - checks Claude auth (subscription via `claude auth login`, or `ANTHROPIC_API_KEY`),
32
- - installs the SessionEnd hook in `~/.claude/settings.json`,
45
+ - lets you choose a default agent: Claude, Codex, or Cursor,
46
+ - checks local agent readiness,
47
+ - installs auto-capture hooks for Claude, Codex, and Cursor,
33
48
  - drops two agent guides into `~/.claude/` (`codealmanac.md` mini, `codealmanac-reference.md` full),
34
49
  - appends `@~/.claude/codealmanac.md` to `~/.claude/CLAUDE.md` so every Claude Code session loads the mini guide.
35
50
 
@@ -37,144 +52,115 @@ The setup is idempotent — safe to re-run. Opt out with `--skip-hook` or `--ski
37
52
 
38
53
  Two binaries ship, both pointing at the same entry: `codealmanac` (install surface) and `almanac` (day-to-day). Requires Node 20 or 22.
39
54
 
40
- `bootstrap` and `capture` invoke Claude via the bundled Claude Agent SDK. The query commands (`search`, `show`, `health`, `topics`) need no credentials at all.
55
+ `bootstrap` and `capture` invoke your configured default agent. Claude uses the bundled Claude Agent SDK, Codex uses `codex exec --json`, and Cursor uses `cursor-agent --print --output-format stream-json`. The query commands (`search`, `show`, `health`, `topics`) need no credentials at all.
41
56
 
42
57
  ## Authentication
43
58
 
44
- Pick one `bootstrap` and `capture` accept either:
59
+ Pick the agent you want CodeAlmanac to use:
45
60
 
46
61
  ```bash
47
- # Option A — your Claude subscription (Pro/Max). Preferred if you already
48
- # use Claude Code; no separate bill, no copy-pasted keys.
62
+ # Claude
49
63
  claude auth login --claudeai
50
-
51
- # Option B — a pay-per-token API key from https://console.anthropic.com.
64
+ # or:
52
65
  export ANTHROPIC_API_KEY=sk-ant-...
53
66
 
54
- # Either way, verify with:
55
- claude auth status
56
- # or:
67
+ # Codex
68
+ codex login
69
+
70
+ # Cursor
71
+ cursor-agent login
72
+
73
+ # Verify all providers:
74
+ almanac agents list
57
75
  almanac doctor
58
76
  ```
59
77
 
60
- codealmanac itself never sees your credentials — auth is handled by the bundled Claude Agent SDK CLI, which reads the same `~/.claude/credentials/` store Claude Code uses.
78
+ Set or change the default at any time:
79
+
80
+ ```bash
81
+ almanac set default-agent codex
82
+ almanac set model codex gpt-5.3-codex
83
+ ```
84
+
85
+ codealmanac itself never stores your provider credentials. Auth stays in each agent's normal local credential store.
61
86
 
62
87
  ## Quickstart
63
88
 
64
89
  ```bash
65
90
  npm install -g codealmanac
66
- codealmanac # interactive setup wizard
91
+ codealmanac # interactive setup wizard; choose Claude/Codex/Cursor
67
92
  # (or: codealmanac --yes)
68
93
 
69
94
  cd your-repo
70
- almanac bootstrap # agent reads the repo and seeds stub pages + topic DAG
95
+ almanac bootstrap # default agent reads the repo and seeds pages + topic DAG
71
96
 
72
- almanac search "auth" # full-text search across pages; prints slugs one per line
97
+ almanac search "auth" # full-text search across pages
73
98
  almanac show checkout-flow # read a page
74
99
 
75
- # From here on, just code as usual — the SessionEnd hook invokes
100
+ # From here on, just code as usual — the installed hooks invoke
76
101
  # `almanac capture` at session end, which writes and updates pages
77
102
  # based on what happened in the session.
78
103
  ```
79
104
 
80
105
  No `almanac init`. A wiki is scaffolded two ways: run `almanac bootstrap` yourself, or clone a repo that already has `.almanac/` committed (codealmanac auto-registers it on the first query).
81
106
 
82
- Sanity-check the install with `almanac doctor` — it reports binary location, native SQLite binding, Claude auth, hook status, guides, import line, and current-wiki stats with a one-line fix for each ✗.
83
-
84
- ## Command reference
85
-
86
- Grouped the same way as `almanac --help`:
87
-
88
- | Group | Command | What it does |
89
- |---|---|---|
90
- | Query | `almanac search [query]` | FTS, `--topic`, `--mentions <path>`, `--since`, `--stale`, `--orphan`, `--archived`, `--limit`, `--json` |
91
- | Query | `almanac show <slug>` | Read a page. Field flags: `--raw`, `--meta`, `--lead`, `--title`, `--topics`, `--files`, `--links`, `--backlinks`, `--xwiki`, `--lineage`, `--updated`, `--path`, `--json`. Absorbs the old `info` and `path` commands. |
92
- | Query | `almanac health` | Eight-category graph integrity report (`orphans`, `stale`, `dead-refs`, `broken-links`, `broken-xwiki`, `empty-topics`, `empty-pages`, `slug-collisions`) |
93
- | Query | `almanac list` | List registered wikis; `--drop <name>` to remove |
94
- | Edit | `almanac tag <page> <topics...>` | Add topics; auto-creates missing ones; `--stdin` for bulk |
95
- | Edit | `almanac untag <page> <topic>` | Remove a topic |
96
- | Edit | `almanac topics ...` | `list`, `show`, `create`, `link`, `unlink`, `rename`, `delete`, `describe` — DAG management |
97
- | Wiki lifecycle | `almanac bootstrap` | Agent reads the repo and seeds stub entity pages + topic DAG (requires Claude auth) |
98
- | Wiki lifecycle | `almanac capture [transcript]` | Writer + reviewer on a session transcript (usually invoked by the hook) |
99
- | Wiki lifecycle | `almanac hook install\|uninstall\|status` | Manage the SessionEnd hook in `~/.claude/settings.json` |
100
- | Wiki lifecycle | `almanac reindex` | Force rebuild of `.almanac/index.db` |
101
- | Setup | `almanac setup` | Install hook + guides + CLAUDE.md import (bare `codealmanac` alias) |
102
- | Setup | `almanac uninstall` | Reverse `setup`: remove hook + guides + import line |
103
- | Setup | `almanac doctor` | Report on install + current wiki with one-line fixes |
107
+ Sanity-check the install with `almanac doctor` and `almanac agents list` they report binary location, native SQLite binding, provider readiness, hook status, guides, import line, and current-wiki stats.
104
108
 
105
- Every command that returns pages prints slugs one per line; pass `--json` for structured output; pipe slugs into commands that accept `--stdin` (`show`, `tag`, `health`). Run `almanac <command> --help` for the full flag surface, or import the full reference on demand: `@~/.claude/codealmanac-reference.md`.
109
+ New to codealmanac? Read the [Concepts guide](./docs/concepts.md) for a walkthrough of pages, topics, files, the database, and the CLI.
106
110
 
107
- ## Concepts
111
+ ## Commands
108
112
 
109
- ### Page shapes (suggestions, not rules)
110
-
111
- The wiki tends to organize around four kinds of pages, but nothing in the system enforces them:
112
-
113
- - **Entity pages** stable named things (Supabase, Stripe, a custom auth system). These are the anchors other pages link to.
114
- - **Decision pages** why X over Y, with context and consequences.
115
- - **Flow pages** — how a multi-file process works end-to-end.
116
- - **Gotcha pages** specific failures or constraints, usually anchored to an entity.
117
-
118
- A page that doesn't fit any of these is fine. Pick the shape that serves the knowledge.
119
-
120
- ### Topics as a DAG
121
-
122
- One organizational axis: topics. Topics form a directed acyclic graph a topic can have multiple parents, and a page can belong to multiple topics. No page type system.
123
-
124
- ```
125
- decisions stack flows
126
- └─ database
127
- └─ supabase ← a page tagged [stack, database]
113
+ ```bash
114
+ # Search & read
115
+ almanac search "auth" # full-text search across pages
116
+ almanac search --topic database # filter by topic
117
+ almanac search --mentions src/lib/stripe.ts # pages referencing a file
118
+ almanac show checkout-flow # read a page
119
+ almanac show checkout-flow --meta # metadata only
120
+ almanac show checkout-flow --raw # body only
121
+
122
+ # Organize
123
+ almanac topics list # all topics with page counts
124
+ almanac topics show database --descendants # topic + its subtree
125
+ almanac tag <page> <topic...> # add topics to a page
126
+ almanac health # graph integrity report
127
+
128
+ # Wiki lifecycle
129
+ almanac bootstrap --agent codex # seed a new wiki from the repo
130
+ almanac capture --agent cursor <transcript> # update wiki from a transcript
131
+ almanac hook install --source all # auto-capture for Claude/Codex/Cursor
132
+
133
+ # Setup & diagnose
134
+ almanac agents list # provider readiness + default
135
+ almanac set default-agent codex # change default provider
136
+ almanac doctor # check install + wiki health
137
+ almanac update # update to latest version
128
138
  ```
129
139
 
130
- `almanac topics show database --descendants` walks the subgraph and returns every page in `database` or `supabase`. Cycles are prevented by a `CHECK` constraint and a depth cap.
131
-
132
- ### The unified `[[...]]` link syntax
140
+ All commands output slugs one per line. Add `--json` for structured output. Pipe with `--stdin`:
133
141
 
134
- One link form, disambiguated by content:
135
-
136
- ```markdown
137
- See [[checkout-flow]] for the full sequence. ← page slug (no slash)
138
- The handler [[src/checkout/handler.ts]] does X. ← file (has slash)
139
- This spans [[src/checkout/]] generally. ← folder (trailing slash)
140
- See [[openalmanac:supabase]] for cross-wiki context. ← cross-wiki (colon prefix)
142
+ ```bash
143
+ almanac search --topic flows | almanac show --stdin
144
+ almanac search --stale 90d | almanac tag --stdin needs-review
141
145
  ```
142
146
 
143
- The indexer classifies each link by those rules and writes it to `wikilinks`, `file_refs`, or `cross_wiki_links`. `almanac search --mentions src/checkout/handler.ts` returns every page referencing that file or any folder containing it.
144
-
145
- ### Archive vs edit
146
-
147
- Most changes are edits — the page is updated in place to reflect current truth, with git history as the archive. When a page's central decision is reversed (not just refined), the old page is marked `archived_at` and `superseded_by`, a new page is created with `supersedes`, and both live side by side. Archived pages are excluded from `almanac search` by default and exempt from dead-ref health checks.
148
-
149
- ### The notability bar
150
-
151
- Every repo's `.almanac/README.md` contains a notability bar: the threshold for what deserves a page. The default is "non-obvious knowledge that will help a future agent" — decisions that took research, gotchas discovered through failure, cross-cutting flows, constraints not visible in code. The writer consults the bar before writing; the reviewer enforces it. Edit the bar to match your repo's taste.
147
+ Run `almanac <command> --help` for the full flag surface.
152
148
 
153
149
  ## How capture works
154
150
 
155
- A page looks like this:
151
+ When a Claude, Codex, or Cursor session ends, the installed hook backgrounds `almanac capture`. The writer agent reads the session transcript, runs `almanac search` and `almanac show` against the existing wiki, drafts changes to pages under `.almanac/pages/`, and performs a reviewer pass. Claude uses its SDK's read-only reviewer subagent; Codex and Cursor perform the reviewer pass from prompt guidance until stricter provider enforcement lands. The reviewer checks duplicates, missing wikilinks, missing topics, inference dressed as fact, and cohesion problems. The writer decides what to incorporate and writes the final versions.
156
152
 
157
- ```markdown
158
- ---
159
- title: Supabase
160
- topics: [stack, database]
161
- files:
162
- - src/lib/supabase.ts
163
- - backend/src/models/
164
- ---
153
+ Capture writes nothing if nothing in the session meets the notability bar — silence is a valid outcome.
165
154
 
166
- # Supabase
155
+ No proposal files, no `--apply` step, no state machine between writer and reviewer. The changes land in `git status` and you commit them like anything else.
167
156
 
168
- PostgreSQL hosted on Supabase. Connection pooling via Supavisor.
157
+ ### The notability bar
169
158
 
170
- ## Gotchas
171
- - Supavisor has a 30s idle timeout — long transactions get killed ([[supavisor-timeout]]).
172
- - UUIDs as primary keys, not `serial` ([[uuid-decision]]).
173
- ```
159
+ Every repo's `.almanac/README.md` contains a notability bar: the threshold for what deserves a page. The default is "non-obvious knowledge that will help a future agent" — decisions that took research, gotchas discovered through failure, cross-cutting flows, constraints not visible in code. The writer consults the bar before writing; the reviewer enforces it. Edit the bar to match your repo's taste.
174
160
 
175
- When a Claude Code session ends, the SessionEnd hook backgrounds `almanac capture <transcript>`. The writer agent reads the transcript, runs `almanac search` and `almanac show` against the existing wiki, drafts changes to pages under `.almanac/pages/`, and invokes the reviewer subagent. The reviewer reads across the graph, flags duplicates, missing wikilinks, missing topics, inference dressed as fact, and cohesion problems, then returns a text critique. The writer decides what to incorporate and writes the final versions. Capture writes nothing if nothing in the session meets the notability bar — silence is a valid outcome.
161
+ ### Archive vs edit
176
162
 
177
- No proposal files, no `--apply` step, no state machine between writer and reviewer. The changes land in `git status` and you commit them like anything else.
163
+ Most changes are edits — the page is updated in place to reflect current truth, with git history as the archive. When a page's central decision is reversed (not just refined), the old page is marked `archived_at` and `superseded_by`, a new page is created with `supersedes`, and both live side by side. Archived pages are excluded from `almanac search` by default and exempt from dead-ref health checks.
178
164
 
179
165
  ## Multi-wiki
180
166
 
@@ -187,18 +173,52 @@ almanac search --wiki openalmanac "RLS" # specific wiki
187
173
 
188
174
  Cross-wiki references use a colon prefix: `[[openalmanac:supabase]]`. The segment before `:` resolves via the registry; unreachable wikis are silently skipped rather than erroring. Cloning a repo with a committed `.almanac/` auto-registers it on the first `almanac` command.
189
175
 
190
- ## Writing conventions
191
-
192
- Pages are neutral-tone encyclopedia-style prose — every sentence contains a specific fact, no significance inflation, no hedging, no formulaic conclusions. Prose first, bullets for genuine lists, tables only for structured comparison. The conventions are described in each repo's `.almanac/README.md` (generated by `bootstrap`); the reviewer loads them at runtime and enforces them on every proposal.
193
-
194
176
  ## Status
195
177
 
196
- `v0.1.3`, pre-release. Node 20.x or 22.x. Release process is documented in [RELEASE.md](./RELEASE.md). Breaking changes are possible before 1.0; they will be called out in release notes.
197
-
178
+ `v0.2.1`, pre-release. Node 20.x or 22.x. Release process is documented in [RELEASE.md](./RELEASE.md). Breaking changes are possible before 1.0; they will be called out in release notes.
198
179
  ## Philosophy
199
180
 
200
181
  Intelligence lives in the prompt, not in the pipeline. Whenever a task calls for judgment — deciding what from a session is worth capturing, evaluating a proposal against the graph, picking between editing and archiving — codealmanac hands a concrete-but-open prompt to an agent. It does not wrap agents in propose/review/apply state machines, intermediate proposal files, or `--dry-run` rehearsal flags. The CLI finds and organizes; the agents do the thinking. If a future change can be expressed as a longer prompt or as more pipeline code, the prompt almost always wins.
201
182
 
183
+ ## Contributing
184
+
185
+ codealmanac is open source under the MIT license. To set up a development environment:
186
+
187
+ ```bash
188
+ git clone https://github.com/AlmanacCode/codealmanac.git
189
+ cd codealmanac
190
+ npm install
191
+ npm run build
192
+ npm link # makes `almanac` and `codealmanac` available globally
193
+ npm test # run the test suite (vitest)
194
+ ```
195
+
196
+ The codebase is TypeScript, built with [tsup](https://tsup.egoist.dev/), tested with [Vitest](https://vitest.dev/). SQLite via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3). AI features use the [Claude Agent SDK](https://docs.anthropic.com/en/docs/agents/agent-sdk).
197
+
198
+ ### Project structure
199
+
200
+ ```
201
+ src/
202
+ ├── cli.ts ← entry point and shortcut routing
203
+ ├── cli/ ← commander command registration and help layout
204
+ ├── commands/ ← one file per CLI command
205
+ ├── indexer/ ← parses markdown → SQLite index
206
+ │ ├── schema.ts ← DDL (CREATE TABLE statements)
207
+ │ ├── index.ts ← incremental indexer (mtime-based freshness)
208
+ │ ├── frontmatter.ts ← YAML frontmatter parser
209
+ │ ├── wikilinks.ts ← [[link]] extractor + classifier
210
+ │ └── paths.ts ← path normalization
211
+ ├── registry/ ← global wiki registry (~/.almanac/registry.json)
212
+ ├── topics/ ← topic DAG + frontmatter rewriting
213
+ ├── agent/ ← Claude Agent SDK integration
214
+ ├── paths.ts ← find nearest .almanac/ (like git finds .git/)
215
+ └── slug.ts ← kebab-case canonicalization
216
+ ```
217
+
218
+ ## Status
219
+
220
+ v0.1.10, pre-release. Node 20.x or 22.x. Release process is documented in [RELEASE.md](./RELEASE.md). Breaking changes are possible before 1.0; they will be called out in release notes.
221
+
202
222
  ## Related
203
223
 
204
224
  codealmanac is part of the [OpenAlmanac](https://www.openalmanac.org) family. OpenAlmanac is a knowledge base for curious people; codealmanac is knowledge for codebases. Same writing standards, different reader.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runAgentsList,
4
+ runSetAgentModel,
5
+ runSetDefaultAgent
6
+ } from "./chunk-R3URPHGH.js";
7
+ import "./chunk-SSYMRT4I.js";
8
+ import "./chunk-WRUSDYYE.js";
9
+ import "./chunk-7JUX4ADQ.js";
10
+ export {
11
+ runAgentsList,
12
+ runSetAgentModel,
13
+ runSetDefaultAgent
14
+ };
15
+ //# sourceMappingURL=agents-A4II4YJC.js.map
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ UNAUTHENTICATED_MESSAGE,
4
+ assertClaudeAuth,
5
+ checkClaudeAuth,
6
+ defaultSpawnCli,
7
+ legacySdkSpawnCli,
8
+ resolveClaudeExecutable
9
+ } from "./chunk-SSYMRT4I.js";
10
+ export {
11
+ UNAUTHENTICATED_MESSAGE,
12
+ assertClaudeAuth,
13
+ checkClaudeAuth,
14
+ defaultSpawnCli,
15
+ legacySdkSpawnCli,
16
+ resolveClaudeExecutable
17
+ };
18
+ //# sourceMappingURL=auth-S5DVUIUJ.js.map
@@ -3,6 +3,7 @@
3
3
  // src/commands/hook.ts
4
4
  import { existsSync as existsSync2 } from "fs";
5
5
  import { mkdir as mkdir2, readFile as readFile2, rename, writeFile } from "fs/promises";
6
+ import { homedir as homedir2 } from "os";
6
7
  import path2 from "path";
7
8
 
8
9
  // src/commands/hook/script.ts
@@ -120,6 +121,54 @@ async function runHookInstall(options = {}) {
120
121
  return { stdout: "", stderr: `almanac: ${script.error}
121
122
  `, exitCode: 1 };
122
123
  }
124
+ const source = options.source ?? "claude";
125
+ if (source === "all") {
126
+ const results = [
127
+ await installClaudeHook(options, script.path),
128
+ await installGenericHook({
129
+ label: "Codex Stop",
130
+ settingsPath: path2.join(homedir2(), ".codex", "hooks.json"),
131
+ eventName: "Stop",
132
+ shape: "wrapped",
133
+ scriptPath: script.path
134
+ }),
135
+ await installGenericHook({
136
+ label: "Cursor sessionEnd",
137
+ settingsPath: path2.join(homedir2(), ".cursor", "hooks.json"),
138
+ eventName: "sessionEnd",
139
+ shape: "flat",
140
+ scriptPath: script.path
141
+ })
142
+ ];
143
+ const failed = results.find((r) => r.exitCode !== 0);
144
+ if (failed !== void 0) return failed;
145
+ return {
146
+ stdout: results.map((r) => r.stdout.trimEnd()).join("\n") + "\n",
147
+ stderr: "",
148
+ exitCode: 0
149
+ };
150
+ }
151
+ if (source === "codex") {
152
+ return await installGenericHook({
153
+ label: "Codex Stop",
154
+ settingsPath: path2.join(homedir2(), ".codex", "hooks.json"),
155
+ eventName: "Stop",
156
+ shape: "wrapped",
157
+ scriptPath: script.path
158
+ });
159
+ }
160
+ if (source === "cursor") {
161
+ return await installGenericHook({
162
+ label: "Cursor sessionEnd",
163
+ settingsPath: path2.join(homedir2(), ".cursor", "hooks.json"),
164
+ eventName: "sessionEnd",
165
+ shape: "flat",
166
+ scriptPath: script.path
167
+ });
168
+ }
169
+ return await installClaudeHook(options, script.path);
170
+ }
171
+ async function installClaudeHook(options, scriptPath) {
123
172
  const settingsPath = resolveSettingsPath(options);
124
173
  const settings = await readSettings(settingsPath);
125
174
  const existing = (settings.hooks?.SessionEnd ?? []).slice();
@@ -134,7 +183,7 @@ async function runHookInstall(options = {}) {
134
183
  continue;
135
184
  }
136
185
  const exactMatch = c.entry.hooks.some(
137
- (h) => h.command === script.path
186
+ (h) => h.command === scriptPath
138
187
  );
139
188
  if (exactMatch && oursAlready === null) {
140
189
  oursAlready = c.entry;
@@ -172,7 +221,7 @@ Remove or rewrap it manually in ${settingsPath} before installing.
172
221
  }
173
222
  if (oursAlready !== null && staleCount.n === 0) {
174
223
  return {
175
- stdout: `almanac: SessionEnd hook already installed at ${script.path}
224
+ stdout: `almanac: SessionEnd hook already installed at ${scriptPath}
176
225
  `,
177
226
  stderr: "",
178
227
  exitCode: 0
@@ -183,7 +232,7 @@ Remove or rewrap it manually in ${settingsPath} before installing.
183
232
  hooks: [
184
233
  {
185
234
  type: "command",
186
- command: script.path,
235
+ command: scriptPath,
187
236
  timeout: HOOK_TIMEOUT_SECONDS
188
237
  }
189
238
  ]
@@ -193,13 +242,121 @@ Remove or rewrap it manually in ${settingsPath} before installing.
193
242
  await writeSettings(settingsPath, settings);
194
243
  return {
195
244
  stdout: `almanac: SessionEnd hook installed
196
- script: ${script.path}
245
+ script: ${scriptPath}
197
246
  settings: ${settingsPath}
198
247
  `,
199
248
  stderr: "",
200
249
  exitCode: 0
201
250
  };
202
251
  }
252
+ async function installGenericHook(args) {
253
+ const settings = await readSettings(args.settingsPath);
254
+ const hooksObj = settings.hooks !== void 0 && settings.hooks !== null && typeof settings.hooks === "object" ? settings.hooks : {};
255
+ const existing = Array.isArray(hooksObj[args.eventName]) ? hooksObj[args.eventName] : [];
256
+ const kept = existing.filter((entry) => !entryHasOurCommand(entry));
257
+ const already = existing.some(
258
+ (entry) => entryHasExactCommand(entry, args.scriptPath)
259
+ );
260
+ if (already && kept.length === existing.length - 1) {
261
+ return {
262
+ stdout: `almanac: ${args.label} hook already installed at ${args.scriptPath}
263
+ `,
264
+ stderr: "",
265
+ exitCode: 0
266
+ };
267
+ }
268
+ const fresh = args.shape === "wrapped" ? {
269
+ hooks: [
270
+ {
271
+ type: "command",
272
+ command: args.scriptPath,
273
+ timeout: HOOK_TIMEOUT_SECONDS
274
+ }
275
+ ]
276
+ } : {
277
+ command: args.scriptPath,
278
+ timeout: HOOK_TIMEOUT_SECONDS
279
+ };
280
+ hooksObj[args.eventName] = [
281
+ ...kept,
282
+ fresh
283
+ ];
284
+ settings.hooks = hooksObj;
285
+ await writeSettings(args.settingsPath, settings);
286
+ if (args.label.startsWith("Codex ")) {
287
+ await ensureCodexHooksFeature(path2.join(homedir2(), ".codex", "config.toml"));
288
+ }
289
+ return {
290
+ stdout: `almanac: ${args.label} hook installed
291
+ script: ${args.scriptPath}
292
+ settings: ${args.settingsPath}
293
+ `,
294
+ stderr: "",
295
+ exitCode: 0
296
+ };
297
+ }
298
+ function entryHasOurCommand(entry) {
299
+ return collectHookCommands(entry).some(isOurCommandPath);
300
+ }
301
+ function entryHasExactCommand(entry, command) {
302
+ return collectHookCommands(entry).some((candidate) => candidate === command);
303
+ }
304
+ function collectHookCommands(entry) {
305
+ if (entry === null || typeof entry !== "object") return [];
306
+ const obj = entry;
307
+ const direct = typeof obj.command === "string" ? [obj.command] : [];
308
+ const nested = Array.isArray(obj.hooks) ? obj.hooks.flatMap((hook) => collectHookCommands(hook)) : [];
309
+ return [...direct, ...nested];
310
+ }
311
+ async function ensureCodexHooksFeature(configPath) {
312
+ let body = "";
313
+ if (existsSync2(configPath)) {
314
+ body = await readFile2(configPath, "utf8");
315
+ }
316
+ if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(body)) return;
317
+ const next = setTomlFeatureFlag(body, "codex_hooks", true);
318
+ await mkdir2(path2.dirname(configPath), { recursive: true });
319
+ const tmp = `${configPath}.almanac-tmp-${process.pid}`;
320
+ await writeFile(tmp, next.endsWith("\n") ? next : `${next}
321
+ `, "utf8");
322
+ await rename(tmp, configPath);
323
+ }
324
+ function setTomlFeatureFlag(body, key, value) {
325
+ const desired = `${key} = ${value ? "true" : "false"}`;
326
+ const lines = body.split(/\r?\n/);
327
+ let featuresStart = -1;
328
+ let featuresEnd = lines.length;
329
+ for (let i = 0; i < lines.length; i++) {
330
+ if (/^\s*\[features\]\s*$/.test(lines[i] ?? "")) {
331
+ featuresStart = i;
332
+ continue;
333
+ }
334
+ if (featuresStart !== -1 && i > featuresStart && /^\s*\[.*\]\s*$/.test(lines[i] ?? "")) {
335
+ featuresEnd = i;
336
+ break;
337
+ }
338
+ }
339
+ if (featuresStart === -1) {
340
+ const prefix = body.trim().length === 0 ? "" : `${body.trimEnd()}
341
+
342
+ `;
343
+ return `${prefix}[features]
344
+ ${desired}
345
+ `;
346
+ }
347
+ const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
348
+ for (let i = featuresStart + 1; i < featuresEnd; i++) {
349
+ if (keyPattern.test(lines[i] ?? "")) {
350
+ lines[i] = desired;
351
+ return lines.join("\n");
352
+ }
353
+ }
354
+ lines.splice(featuresStart + 1, 0, desired);
355
+ return lines.join("\n");
356
+ }
357
+ function escapeRegex(value) {
358
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
359
+ }
203
360
  async function runHookUninstall(options = {}) {
204
361
  const settingsPath = resolveSettingsPath(options);
205
362
  if (!existsSync2(settingsPath)) {
@@ -352,4 +509,4 @@ export {
352
509
  runHookUninstall,
353
510
  runHookStatus
354
511
  };
355
- //# sourceMappingURL=chunk-Z4MWLVS2.js.map
512
+ //# sourceMappingURL=chunk-447U3GQJ.js.map