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.
- package/README.md +124 -104
- package/dist/agents-A4II4YJC.js +15 -0
- package/dist/auth-S5DVUIUJ.js +18 -0
- package/dist/{chunk-Z4MWLVS2.js → chunk-447U3GQJ.js} +162 -5
- package/dist/chunk-447U3GQJ.js.map +1 -0
- package/dist/{chunk-QLHJP2XK.js → chunk-B2AGSRXL.js} +13 -9
- package/dist/{chunk-QLHJP2XK.js.map → chunk-B2AGSRXL.js.map} +1 -1
- package/dist/{chunk-AXFPUHBN.js → chunk-F53U6JQG.js} +8 -49
- package/dist/chunk-F53U6JQG.js.map +1 -0
- package/dist/{chunk-3C5SY5SE.js → chunk-KQUVMF27.js} +5 -2
- package/dist/chunk-KQUVMF27.js.map +1 -0
- package/dist/{chunk-BJVZLP6O.js → chunk-MX2EW5MR.js} +3 -3
- package/dist/{chunk-Z6MBJ3D2.js → chunk-QQHIVTXT.js} +6 -4
- package/dist/{chunk-Z6MBJ3D2.js.map → chunk-QQHIVTXT.js.map} +1 -1
- package/dist/chunk-R3URPHGH.js +194 -0
- package/dist/chunk-R3URPHGH.js.map +1 -0
- package/dist/chunk-SSYMRT4I.js +126 -0
- package/dist/chunk-SSYMRT4I.js.map +1 -0
- package/dist/{chunk-QHQ6YH7U.js → chunk-V3QOQSXI.js} +5 -3
- package/dist/{chunk-QHQ6YH7U.js.map → chunk-V3QOQSXI.js.map} +1 -1
- package/dist/chunk-WRUSDYYE.js +97 -0
- package/dist/chunk-WRUSDYYE.js.map +1 -0
- package/dist/{chunk-3LC55TG6.js → chunk-ZDJSJIB6.js} +77 -126
- package/dist/chunk-ZDJSJIB6.js.map +1 -0
- package/dist/{cli-W3OYVJYH.js → cli-MZEXRV6E.js} +238 -24
- package/dist/cli-MZEXRV6E.js.map +1 -0
- package/dist/codealmanac.js +1 -1
- package/dist/doctor-3BYSF3JD.js +17 -0
- package/dist/{hook-CRJMWSSO.js → hook-2NP3UE7U.js} +2 -2
- package/dist/{register-commands-JHC2OFKM.js → register-commands-DPH4ZWEE.js} +621 -60
- package/dist/register-commands-DPH4ZWEE.js.map +1 -0
- package/dist/uninstall-FDIOBAAR.js +15 -0
- package/dist/uninstall-FDIOBAAR.js.map +1 -0
- package/dist/update-RAF7QRYF.js +11 -0
- package/dist/update-RAF7QRYF.js.map +1 -0
- package/dist/{wiki-IPSRRGOT.js → wiki-IGNRNLUZ.js} +2 -2
- package/hooks/almanac-capture.sh +40 -7
- package/package.json +3 -2
- package/dist/chunk-3C5SY5SE.js.map +0 -1
- package/dist/chunk-3LC55TG6.js.map +0 -1
- package/dist/chunk-AXFPUHBN.js.map +0 -1
- package/dist/chunk-Z4MWLVS2.js.map +0 -1
- package/dist/cli-W3OYVJYH.js.map +0 -1
- package/dist/doctor-ODFNJUKH.js +0 -15
- package/dist/register-commands-JHC2OFKM.js.map +0 -1
- package/dist/uninstall-HE2Z2LN2.js +0 -12
- package/dist/update-IL243I4E.js +0 -10
- /package/dist/{doctor-ODFNJUKH.js.map → agents-A4II4YJC.js.map} +0 -0
- /package/dist/{hook-CRJMWSSO.js.map → auth-S5DVUIUJ.js.map} +0 -0
- /package/dist/{chunk-BJVZLP6O.js.map → chunk-MX2EW5MR.js.map} +0 -0
- /package/dist/{uninstall-HE2Z2LN2.js.map → doctor-3BYSF3JD.js.map} +0 -0
- /package/dist/{update-IL243I4E.js.map → hook-2NP3UE7U.js.map} +0 -0
- /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
|
-
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
32
|
-
-
|
|
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
|
|
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
|
|
59
|
+
Pick the agent you want CodeAlmanac to use:
|
|
45
60
|
|
|
46
61
|
```bash
|
|
47
|
-
#
|
|
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
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
95
|
+
almanac bootstrap # default agent reads the repo and seeds pages + topic DAG
|
|
71
96
|
|
|
72
|
-
almanac search "auth" # full-text search across pages
|
|
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
|
|
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` —
|
|
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
|
-
|
|
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
|
-
##
|
|
111
|
+
## Commands
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
### The notability bar
|
|
169
158
|
|
|
170
|
-
|
|
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
|
-
|
|
161
|
+
### Archive vs edit
|
|
176
162
|
|
|
177
|
-
|
|
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
|
|
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 ===
|
|
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 ${
|
|
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:
|
|
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: ${
|
|
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-
|
|
512
|
+
//# sourceMappingURL=chunk-447U3GQJ.js.map
|