codealmanac 0.2.6 → 0.2.7

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 (86) hide show
  1. package/LICENSE +21 -133
  2. package/README.md +20 -15
  3. package/dist/{agents-HYRWRHRX.js → agents-V2ZOIACP.js} +6 -5
  4. package/dist/{chunk-PDFS5VFE.js → chunk-447U3GQJ.js} +5 -17
  5. package/dist/chunk-447U3GQJ.js.map +1 -0
  6. package/dist/{chunk-3E7JNMTZ.js → chunk-5BWUMAOX.js} +4 -29
  7. package/dist/chunk-5BWUMAOX.js.map +1 -0
  8. package/dist/{chunk-KQUVMF27.js → chunk-BFIG2CXM.js} +2 -516
  9. package/dist/chunk-BFIG2CXM.js.map +1 -0
  10. package/dist/{chunk-K2JBCB7R.js → chunk-BQY5L3DL.js} +7 -43
  11. package/dist/chunk-BQY5L3DL.js.map +1 -0
  12. package/dist/{chunk-F53U6JQG.js → chunk-CQJVM34R.js} +2 -2
  13. package/dist/chunk-FUBE6KCO.js +124 -0
  14. package/dist/chunk-FUBE6KCO.js.map +1 -0
  15. package/dist/chunk-IZBXXAVL.js +524 -0
  16. package/dist/chunk-IZBXXAVL.js.map +1 -0
  17. package/dist/{chunk-7JUX4ADQ.js → chunk-IZT6RBHS.js} +1 -1
  18. package/dist/{chunk-DW32TL5W.js → chunk-JLQZELHQ.js} +18 -58
  19. package/dist/chunk-JLQZELHQ.js.map +1 -0
  20. package/dist/{chunk-2BNDNGUR.js → chunk-KZXWPG4P.js} +4 -8
  21. package/dist/{chunk-2BNDNGUR.js.map → chunk-KZXWPG4P.js.map} +1 -1
  22. package/dist/{chunk-GPFVEF6V.js → chunk-QIA22IAM.js} +6 -24
  23. package/dist/chunk-QIA22IAM.js.map +1 -0
  24. package/dist/{chunk-J7DNV2DH.js → chunk-RALBM6HZ.js} +43 -355
  25. package/dist/chunk-RALBM6HZ.js.map +1 -0
  26. package/dist/{chunk-HJ3WREGP.js → chunk-U5DLLWIC.js} +3 -3
  27. package/dist/chunk-WL4UE7Q6.js +1386 -0
  28. package/dist/chunk-WL4UE7Q6.js.map +1 -0
  29. package/dist/{chunk-VXDPUOQ5.js → chunk-ZUQN5Y3K.js} +129 -382
  30. package/dist/chunk-ZUQN5Y3K.js.map +1 -0
  31. package/dist/{chunk-ODJAAJGZ.js → chunk-ZZLLOAI6.js} +3 -3
  32. package/dist/{cli-MKXCNEMW.js → cli-XWPNARA6.js} +37 -20
  33. package/dist/cli-XWPNARA6.js.map +1 -0
  34. package/dist/codealmanac.js +1 -1
  35. package/dist/{config-F7FKEQ7F.js → config-KH3JUMG6.js} +4 -4
  36. package/dist/doctor-ENJT665Z.js +18 -0
  37. package/dist/{hook-4SVX446M.js → hook-2NP3UE7U.js} +2 -4
  38. package/dist/paths-O5CZADP2.js +14 -0
  39. package/dist/process-KFSLENL3.js +61 -0
  40. package/dist/{register-commands-2F6SXLDI.js → register-commands-LULZUSPO.js} +999 -1030
  41. package/dist/register-commands-LULZUSPO.js.map +1 -0
  42. package/dist/uninstall-BD4MMQ7M.js +16 -0
  43. package/dist/uninstall-BD4MMQ7M.js.map +1 -0
  44. package/dist/update-XSKPDFMJ.js +11 -0
  45. package/dist/update-XSKPDFMJ.js.map +1 -0
  46. package/dist/{wiki-IGNRNLUZ.js → wiki-O4RWMAE6.js} +8 -6
  47. package/dist/wiki-O4RWMAE6.js.map +1 -0
  48. package/guides/mini.md +8 -6
  49. package/guides/reference.md +89 -32
  50. package/hooks/almanac-capture.sh +7 -8
  51. package/package.json +3 -4
  52. package/prompts/agents/.gitkeep +1 -0
  53. package/prompts/base/notability.md +139 -0
  54. package/prompts/base/purpose.md +85 -0
  55. package/prompts/base/syntax.md +114 -0
  56. package/prompts/operations/absorb.md +43 -0
  57. package/prompts/operations/build.md +49 -0
  58. package/prompts/operations/garden.md +51 -0
  59. package/COMMERCIAL.md +0 -9
  60. package/dist/chunk-3E7JNMTZ.js.map +0 -1
  61. package/dist/chunk-DW32TL5W.js.map +0 -1
  62. package/dist/chunk-GPFVEF6V.js.map +0 -1
  63. package/dist/chunk-J7DNV2DH.js.map +0 -1
  64. package/dist/chunk-K2JBCB7R.js.map +0 -1
  65. package/dist/chunk-KQUVMF27.js.map +0 -1
  66. package/dist/chunk-PDFS5VFE.js.map +0 -1
  67. package/dist/chunk-VXDPUOQ5.js.map +0 -1
  68. package/dist/cli-MKXCNEMW.js.map +0 -1
  69. package/dist/doctor-37UH3HT5.js +0 -17
  70. package/dist/register-commands-2F6SXLDI.js.map +0 -1
  71. package/dist/uninstall-C62ZOK32.js +0 -17
  72. package/dist/update-2UGOFN5C.js +0 -11
  73. package/dist/wiki-IGNRNLUZ.js.map +0 -1
  74. package/prompts/bootstrap.md +0 -176
  75. package/prompts/reviewer.md +0 -129
  76. package/prompts/writer.md +0 -134
  77. /package/dist/{agents-HYRWRHRX.js.map → agents-V2ZOIACP.js.map} +0 -0
  78. /package/dist/{chunk-F53U6JQG.js.map → chunk-CQJVM34R.js.map} +0 -0
  79. /package/dist/{chunk-7JUX4ADQ.js.map → chunk-IZT6RBHS.js.map} +0 -0
  80. /package/dist/{chunk-HJ3WREGP.js.map → chunk-U5DLLWIC.js.map} +0 -0
  81. /package/dist/{chunk-ODJAAJGZ.js.map → chunk-ZZLLOAI6.js.map} +0 -0
  82. /package/dist/{config-F7FKEQ7F.js.map → config-KH3JUMG6.js.map} +0 -0
  83. /package/dist/{doctor-37UH3HT5.js.map → doctor-ENJT665Z.js.map} +0 -0
  84. /package/dist/{hook-4SVX446M.js.map → hook-2NP3UE7U.js.map} +0 -0
  85. /package/dist/{uninstall-C62ZOK32.js.map → paths-O5CZADP2.js.map} +0 -0
  86. /package/dist/{update-2UGOFN5C.js.map → process-KFSLENL3.js.map} +0 -0
package/LICENSE CHANGED
@@ -1,133 +1,21 @@
1
- # PolyForm Noncommercial License 1.0.0
2
-
3
- <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
-
5
- Required Notice: Copyright (c) 2026 Rohan Sheth
6
-
7
- ## Acceptance
8
-
9
- In order to get any license under these terms, you must agree
10
- to them as both strict obligations and conditions to all
11
- your licenses.
12
-
13
- ## Copyright License
14
-
15
- The licensor grants you a copyright license for the
16
- software to do everything you might do with the software
17
- that would otherwise infringe the licensor's copyright
18
- in it for any permitted purpose. However, you may
19
- only distribute the software according to [Distribution
20
- License](#distribution-license) and make changes or new works
21
- based on the software according to [Changes and New Works
22
- License](#changes-and-new-works-license).
23
-
24
- ## Distribution License
25
-
26
- The licensor grants you an additional copyright license
27
- to distribute copies of the software. Your license
28
- to distribute covers distributing the software with
29
- changes and new works permitted by [Changes and New Works
30
- License](#changes-and-new-works-license).
31
-
32
- ## Notices
33
-
34
- You must ensure that anyone who gets a copy of any part of
35
- the software from you also gets a copy of these terms or the
36
- URL for them above, as well as copies of any plain-text lines
37
- beginning with `Required Notice:` that the licensor provided
38
- with the software. For example:
39
-
40
- > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
41
-
42
- ## Changes and New Works License
43
-
44
- The licensor grants you an additional copyright license to
45
- make changes and new works based on the software for any
46
- permitted purpose.
47
-
48
- ## Patent License
49
-
50
- The licensor grants you a patent license for the software that
51
- covers patent claims the licensor can license, or becomes able
52
- to license, that you would infringe by using the software.
53
-
54
- ## Noncommercial Purposes
55
-
56
- Any noncommercial purpose is a permitted purpose.
57
-
58
- ## Personal Uses
59
-
60
- Personal use for research, experiment, and testing for
61
- the benefit of public knowledge, personal study, private
62
- entertainment, hobby projects, amateur pursuits, or religious
63
- observance, without any anticipated commercial application,
64
- is use for a permitted purpose.
65
-
66
- ## Noncommercial Organizations
67
-
68
- Use by any charitable organization, educational institution,
69
- public research organization, public safety or health
70
- organization, environmental protection organization,
71
- or government institution is use for a permitted purpose
72
- regardless of the source of funding or obligations resulting
73
- from the funding.
74
-
75
- ## Fair Use
76
-
77
- You may have "fair use" rights for the software under the
78
- law. These terms do not limit them.
79
-
80
- ## No Other Rights
81
-
82
- These terms do not allow you to sublicense or transfer any of
83
- your licenses to anyone else, or prevent the licensor from
84
- granting licenses to anyone else. These terms do not imply
85
- any other licenses.
86
-
87
- ## Patent Defense
88
-
89
- If you make any written claim that the software infringes or
90
- contributes to infringement of any patent, your patent license
91
- for the software granted under these terms ends immediately. If
92
- your company makes such a claim, your patent license ends
93
- immediately for work on behalf of your company.
94
-
95
- ## Violations
96
-
97
- The first time you are notified in writing that you have
98
- violated any of these terms, or done anything with the software
99
- not covered by your licenses, your licenses can nonetheless
100
- continue if you come into full compliance with these terms,
101
- and take practical steps to correct past violations, within
102
- 32 days of receiving notice. Otherwise, all your licenses
103
- end immediately.
104
-
105
- ## No Liability
106
-
107
- ***As far as the law allows, the software comes as is, without
108
- any warranty or condition, and the licensor will not be liable
109
- to you for any damages arising out of these terms or the use
110
- or nature of the software, under any kind of legal claim.***
111
-
112
- ## Definitions
113
-
114
- The **licensor** is the individual or entity offering these
115
- terms, and the **software** is the software the licensor makes
116
- available under these terms.
117
-
118
- **You** refers to the individual or entity agreeing to these
119
- terms.
120
-
121
- **Your company** is any legal entity, sole proprietorship,
122
- or other kind of organization that you work for, plus all
123
- organizations that have control over, are under the control of,
124
- or are under common control with that organization. **Control**
125
- means ownership of substantially all the assets of an entity,
126
- or the power to direct its management and policies by vote,
127
- contract, or otherwise. Control can be direct or indirect.
128
-
129
- **Your licenses** are all the licenses granted to you for the
130
- software under these terms.
131
-
132
- **Use** means anything you do with the software requiring one
133
- of your licenses.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rohan Sheth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -26,9 +26,9 @@ A single `CLAUDE.md` at the repo root doesn't scale past a few hundred lines, ha
26
26
 
27
27
  ## How it works
28
28
 
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.
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. CodeAlmanac builds one provider-neutral run spec, starts it through the process manager, and records local run state in `.almanac/runs/`. New and updated pages show up in your next `git status`; you review them like any other commit.
30
30
 
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.
31
+ The CLI only invokes AI for the write-capable lifecycle commands: `init`, `capture`, `ingest`, and `garden`. Every query or organization command (`search`, `show`, `topics`, `tag`, `health`) operates on a SQLite index that rebuilds silently whenever pages are newer than the index.
32
32
 
33
33
  ## Install
34
34
 
@@ -53,7 +53,7 @@ The setup is idempotent — safe to re-run. Opt out with `--skip-hook` or `--ski
53
53
 
54
54
  Two binaries ship, both pointing at the same entry: `codealmanac` (install surface) and `almanac` (day-to-day). Requires Node 20 or 22.
55
55
 
56
- `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.
56
+ `init`, `capture`, `ingest`, and `garden` invoke your configured default provider unless `--using <provider[/model]>` overrides it. Claude uses the bundled Claude Agent SDK, Codex uses `codex exec --json`, and Cursor is currently an explicit future-work adapter. The query commands (`search`, `show`, `health`, `topics`) need no credentials at all.
57
57
 
58
58
  ## Authentication
59
59
 
@@ -99,7 +99,7 @@ codealmanac # interactive setup wizard; choose provider + mode
99
99
  # (or: codealmanac --yes)
100
100
 
101
101
  cd your-repo
102
- almanac bootstrap # default agent reads the repo and seeds pages + topic DAG
102
+ almanac init # default provider reads the repo and builds the wiki
103
103
 
104
104
  almanac search "auth" # full-text search across pages
105
105
  almanac show checkout-flow # read a page
@@ -109,7 +109,7 @@ almanac show checkout-flow # read a page
109
109
  # based on what happened in the session.
110
110
  ```
111
111
 
112
- 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).
112
+ A wiki is scaffolded two ways: run `almanac init` yourself, or clone a repo that already has `.almanac/` committed (codealmanac auto-registers it on the first query).
113
113
 
114
114
  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.
115
115
 
@@ -133,9 +133,12 @@ almanac tag <page> <topic...> # add topics to a page
133
133
  almanac health # graph integrity report
134
134
 
135
135
  # Wiki lifecycle
136
- almanac bootstrap --agent codex # seed a new wiki from the repo
137
- almanac capture --agent cursor <transcript> # update wiki from a transcript
136
+ almanac init --using codex # build a new wiki from the repo
137
+ almanac capture --using claude <transcript> # update wiki from a session transcript
138
138
  almanac capture --json <transcript> # structured CommandOutcome output
139
+ almanac ingest docs/adr.md # absorb files or folders into the wiki
140
+ almanac garden # audit and improve the wiki
141
+ almanac jobs # list local background runs
139
142
  almanac hook install --source all # auto-capture for Claude/Codex/Cursor
140
143
 
141
144
  # Setup & diagnose
@@ -147,8 +150,7 @@ almanac doctor # check install + wiki health
147
150
  almanac update # update to latest version
148
151
  ```
149
152
 
150
- `bootstrap` and `capture` resolve provider settings in the standard order:
151
- `--agent` / `--model`, then `ALMANAC_AGENT` / `ALMANAC_MODEL`, then config.
153
+ `init`, `capture`, `ingest`, and `garden` resolve provider settings through `--using <provider[/model]>`, then provider config.
152
154
 
153
155
  All query commands output slugs one per line. Add `--json` for structured output. Pipe with `--stdin`:
154
156
 
@@ -161,15 +163,15 @@ Run `almanac <command> --help` for the full flag surface.
161
163
 
162
164
  ## How capture works
163
165
 
164
- 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.
166
+ When a Claude, Codex, or Cursor session ends, the installed hook backgrounds `almanac capture`. Capture resolves the session transcript, builds the same Absorb operation used by `almanac ingest`, and starts a provider run through the process manager. The provider adapter decides how to express the requested prompt, tools, and future subagents for Claude, Codex, or Cursor.
165
167
 
166
168
  Capture writes nothing if nothing in the session meets the notability bar — silence is a valid outcome.
167
169
 
168
- 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.
170
+ No proposal files, no `--apply` step, no hardcoded reviewer/scout/researcher pipeline. The changes land in `git status` and you commit them like anything else.
169
171
 
170
172
  ### The notability bar
171
173
 
172
- 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
+ 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 operation prompt consults the bar before writing. Edit the bar to match your repo's taste.
173
175
 
174
176
  ### Archive vs edit
175
177
 
@@ -195,7 +197,7 @@ Intelligence lives in the prompt, not in the pipeline. Whenever a task calls for
195
197
 
196
198
  ## Contributing
197
199
 
198
- codealmanac is source-available for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial use requires a separate paid commercial license; see [COMMERCIAL.md](./COMMERCIAL.md). To set up a development environment:
200
+ codealmanac is open source under the MIT license. To set up a development environment:
199
201
 
200
202
  ```bash
201
203
  git clone https://github.com/AlmanacCode/codealmanac.git
@@ -223,7 +225,10 @@ src/
223
225
  │ └── paths.ts ← path normalization
224
226
  ├── registry/ ← global wiki registry (~/.almanac/registry.json)
225
227
  ├── topics/ ← topic DAG + frontmatter rewriting
226
- ├── agent/ Claude Agent SDK integration
228
+ ├── harness/ provider-neutral run specs and provider adapters
229
+ ├── process/ ← local run records, logs, background jobs
230
+ ├── operations/ ← build, absorb, and garden operation specs
231
+ ├── agent/ ← provider setup/status helpers and prompt loading
227
232
  ├── paths.ts ← find nearest .almanac/ (like git finds .git/)
228
233
  └── slug.ts ← kebab-case canonicalization
229
234
  ```
@@ -238,4 +243,4 @@ codealmanac is part of the [OpenAlmanac](https://www.openalmanac.org) family. Op
238
243
 
239
244
  ## License
240
245
 
241
- PolyForm Noncommercial License 1.0.0. Commercial use requires a separate paid commercial license; see [COMMERCIAL.md](./COMMERCIAL.md). Copyright (c) 2026 Rohan Sheth. See [LICENSE](./LICENSE).
246
+ MIT. Copyright (c) 2026 Rohan Sheth. See [LICENSE](./LICENSE).
@@ -8,10 +8,11 @@ import {
8
8
  runDeprecatedSetDefaultAgent,
9
9
  runSetAgentModel,
10
10
  runSetDefaultAgent
11
- } from "./chunk-GPFVEF6V.js";
12
- import "./chunk-J7DNV2DH.js";
13
- import "./chunk-3E7JNMTZ.js";
14
- import "./chunk-7JUX4ADQ.js";
11
+ } from "./chunk-QIA22IAM.js";
12
+ import "./chunk-RALBM6HZ.js";
13
+ import "./chunk-FUBE6KCO.js";
14
+ import "./chunk-5BWUMAOX.js";
15
+ import "./chunk-IZT6RBHS.js";
15
16
  export {
16
17
  runAgentsDoctor,
17
18
  runAgentsList,
@@ -22,4 +23,4 @@ export {
22
23
  runSetAgentModel,
23
24
  runSetDefaultAgent
24
25
  };
25
- //# sourceMappingURL=agents-HYRWRHRX.js.map
26
+ //# sourceMappingURL=agents-V2ZOIACP.js.map
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- isCursorEnabled
4
- } from "./chunk-3E7JNMTZ.js";
5
2
 
6
3
  // src/commands/hook.ts
7
4
  import { existsSync as existsSync2 } from "fs";
@@ -134,17 +131,15 @@ async function runHookInstall(options = {}) {
134
131
  eventName: "Stop",
135
132
  shape: "wrapped",
136
133
  scriptPath: script.path
137
- })
138
- ];
139
- if (isCursorEnabled()) {
140
- results.push(await installGenericHook({
134
+ }),
135
+ await installGenericHook({
141
136
  label: "Cursor sessionEnd",
142
137
  settingsPath: path2.join(homedir2(), ".cursor", "hooks.json"),
143
138
  eventName: "sessionEnd",
144
139
  shape: "flat",
145
140
  scriptPath: script.path
146
- }));
147
- }
141
+ })
142
+ ];
148
143
  const failed = results.find((r) => r.exitCode !== 0);
149
144
  if (failed !== void 0) return failed;
150
145
  return {
@@ -163,13 +158,6 @@ async function runHookInstall(options = {}) {
163
158
  });
164
159
  }
165
160
  if (source === "cursor") {
166
- if (!isCursorEnabled()) {
167
- return {
168
- stdout: "",
169
- stderr: "almanac: cursor hooks are disabled. Set CODEALMANAC_ENABLE_CURSOR=1 to enable experimental Cursor support.\n",
170
- exitCode: 1
171
- };
172
- }
173
161
  return await installGenericHook({
174
162
  label: "Cursor sessionEnd",
175
163
  settingsPath: path2.join(homedir2(), ".cursor", "hooks.json"),
@@ -521,4 +509,4 @@ export {
521
509
  runHookUninstall,
522
510
  runHookStatus
523
511
  };
524
- //# sourceMappingURL=chunk-PDFS5VFE.js.map
512
+ //# sourceMappingURL=chunk-447U3GQJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/hook.ts","../src/commands/hook/script.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport {\n copyToStableHooksDir,\n resolveHookScriptPath,\n resolveSettingsPath,\n type ScriptResolution,\n} from \"./hook/script.js\";\n\n/**\n * `almanac hook install|uninstall|status` — wires the bundled\n * `hooks/almanac-capture.sh` into `~/.claude/settings.json` as a\n * `SessionEnd` hook.\n *\n * Design notes:\n *\n * - **Schema.** Claude Code validates `settings.json` against a strict\n * schema: each entry in an event array (like `SessionEnd`) is a\n * `{matcher, hooks: [...]}` container, and the actual command objects\n * live in the nested `hooks` array. v0.1.0–v0.1.4 wrote command objects\n * directly at the event-array level; newer Claude Code versions now\n * reject that shape. We produce the wrapped form on install, and when\n * encountering a legacy unwrapped entry that we recognize as ours (by\n * `command` ending in `almanac-capture.sh`) we migrate it on next\n * install. `SessionEnd` never uses the `matcher` field to discriminate\n * anything — we always emit an empty `matcher: \"\"` (matches\n * everything, which is what session-end lifecycle hooks want).\n *\n * - **Idempotent.** `install` twice leaves one entry, not two. We match by\n * `command` string equality on the inner `hooks[]` entries. If the user\n * replaces our absolute path with a symlink pointing at the same\n * script, we'll treat it as foreign. That's acceptable; the `status`\n * output shows the path we'd use, so the user can reconcile manually.\n *\n * - **Refuse foreign entries.** If `SessionEnd` is already populated with\n * a command we don't recognize, we print the existing value and exit\n * non-zero. Claude Code lets users wire their own hooks (notifications,\n * git autocommit scripts, etc.) and silently replacing them would be\n * rude. Foreign wrapped containers that don't reference our script are\n * preserved byte-for-byte.\n *\n * - **Atomic write.** `settings.json` is small but heavily touched by\n * Claude Code. Writing via tmp-file + rename avoids corrupting the file\n * if we crash mid-write.\n *\n * - **Non-interactive.** No prompts, no confirmations. The caller is\n * already making an intentional choice by running `almanac hook\n * install`.\n */\n\nexport interface HookCommandOptions {\n /** Which agent app to install hooks for. Default keeps legacy Claude behavior. */\n source?: \"claude\" | \"codex\" | \"cursor\" | \"all\";\n /**\n * Override the hook script path. Production code leaves this undefined\n * and we resolve the bundled `hooks/almanac-capture.sh`. Tests pass a\n * fixture path to avoid depending on the runtime-install layout.\n */\n hookScriptPath?: string;\n /**\n * Override `~/.claude/settings.json`. Tests sandbox this to a tmpdir;\n * production code leaves it undefined.\n */\n settingsPath?: string;\n /**\n * Override the stable hooks directory where we copy the script.\n * Defaults to `~/.claude/hooks/`. Tests sandbox this to a tmpdir.\n *\n * Bug #1 fix: we always copy the bundled script to this stable path\n * before writing it into settings.json. This way the settings entry\n * points at a user-owned location that survives npm version bumps,\n * npx cache evictions, and nvm version switches — instead of an\n * ephemeral path inside ~/.npm/_npx/<sha>/... or the nvm-versioned\n * node_modules/.\n */\n stableHooksDir?: string;\n}\n\nexport interface HookCommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst HOOK_TIMEOUT_SECONDS = 10;\n\n/** A single command invocation inside a wrapper's `hooks[]` array. */\ninterface HookCommand {\n type: \"command\";\n command: string;\n timeout?: number;\n}\n\n/** A wrapped SessionEnd entry per Claude Code's schema. */\ninterface WrappedEntry {\n matcher: string;\n hooks: HookCommand[];\n}\n\n/**\n * What we read from `settings.hooks.SessionEnd`. During a read we may\n * encounter the legacy unwrapped shape (`HookCommand` directly) written\n * by v0.1.0–v0.1.4 — we recognize and migrate it. Unknown entries we\n * can't classify are preserved as-is via `unknown`.\n */\ntype RawEntry = WrappedEntry | HookCommand | unknown;\n\n/**\n * Claude Code's `settings.json` is a free-form JSON object; we only care\n * about the `hooks.SessionEnd` array. Preserve everything else verbatim\n * so we don't drop user settings when we write the file back.\n */\ntype SettingsJson = Record<string, unknown> & {\n hooks?: Record<string, RawEntry[] | undefined>;\n};\n\n/**\n * Heuristic: does this command path look like one we installed?\n *\n * We match on the filename `almanac-capture.sh` regardless of the parent\n * directory. This covers:\n * - the stable path: `~/.claude/hooks/almanac-capture.sh`\n * - legacy paths from v0.1.0–v0.1.5: inside the nvm node_modules or\n * npx cache\n * The stable path is what new installs produce; legacy paths are what\n * we migrate when the user runs `almanac hook install` again.\n */\nfunction isOurCommandPath(command: string): boolean {\n return command.endsWith(\"almanac-capture.sh\");\n}\n\n/**\n * Classify a raw SessionEnd entry. Wrapped entries are the canonical\n * shape; unwrapped-command entries are legacy output from v0.1.0–v0.1.4.\n * Anything else (random user JSON) is `unknown` and we leave it alone.\n */\ntype Classified =\n | { kind: \"wrapped\"; entry: WrappedEntry }\n | { kind: \"legacy\"; entry: HookCommand }\n | { kind: \"unknown\"; entry: unknown };\n\nfunction classifyEntry(raw: RawEntry): Classified {\n if (raw === null || typeof raw !== \"object\") {\n return { kind: \"unknown\", entry: raw };\n }\n const obj = raw as Record<string, unknown>;\n if (Array.isArray(obj.hooks)) {\n // Wrapped shape. `matcher` may be absent in hand-edited files; treat\n // absent as \"\" so we don't throw on slightly malformed input.\n const matcher = typeof obj.matcher === \"string\" ? obj.matcher : \"\";\n const hooks: HookCommand[] = [];\n for (const h of obj.hooks as unknown[]) {\n if (h !== null && typeof h === \"object\") {\n const ho = h as Record<string, unknown>;\n if (ho.type === \"command\" && typeof ho.command === \"string\") {\n const cmd: HookCommand = {\n type: \"command\",\n command: ho.command,\n };\n if (typeof ho.timeout === \"number\") cmd.timeout = ho.timeout;\n hooks.push(cmd);\n }\n }\n }\n return { kind: \"wrapped\", entry: { matcher, hooks } };\n }\n if (obj.type === \"command\" && typeof obj.command === \"string\") {\n // Legacy unwrapped shape — v0.1.0–v0.1.4 wrote this form.\n const cmd: HookCommand = {\n type: \"command\",\n command: obj.command as string,\n };\n if (typeof obj.timeout === \"number\") cmd.timeout = obj.timeout;\n return { kind: \"legacy\", entry: cmd };\n }\n return { kind: \"unknown\", entry: raw };\n}\n\n/** True when the entry references our script and is safely ours to manage. */\nfunction isOurWrapped(entry: WrappedEntry): boolean {\n return entry.hooks.some((h) => isOurCommandPath(h.command));\n}\n\nexport async function runHookInstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const bundled = resolveHookScriptPath(options);\n if (!bundled.ok) {\n return { stdout: \"\", stderr: `almanac: ${bundled.error}\\n`, exitCode: 1 };\n }\n\n // Copy the bundled hook script to a stable user-owned location before\n // writing that path into settings.json. This is the Bug #1 fix:\n //\n // OLD behavior: settings.json pointed at the bundled path (inside\n // ~/.nvm/versions/node/<ver>/lib/node_modules/codealmanac/hooks/... or\n // ~/.npm/_npx/<sha>/node_modules/codealmanac/hooks/...). When the user\n // switches Node versions or the npx cache is evicted, the path breaks\n // silently and captures stop firing.\n //\n // NEW behavior: we copy almanac-capture.sh to ~/.claude/hooks/ (same\n // directory Claude Code uses for its own built-in hooks, always present)\n // and point settings.json there. The stable path is independent of\n // Node version and npm cache state. When the user upgrades codealmanac,\n // `almanac hook install` copies a fresh script and updates settings.json\n // if the path changed.\n //\n // When `hookScriptPath` is explicitly provided (test injection), the\n // caller has already specified the destination path — skip the copy and\n // use that path directly. The stable-copy concern only applies to the\n // production flow where we resolved from the bundled package layout.\n const script: ScriptResolution = options.hookScriptPath !== undefined\n ? bundled // already the caller-provided path, no copy needed\n : await copyToStableHooksDir(bundled.path, options);\n if (!script.ok) {\n return { stdout: \"\", stderr: `almanac: ${script.error}\\n`, exitCode: 1 };\n }\n\n const source = options.source ?? \"claude\";\n if (source === \"all\") {\n const results = [\n await installClaudeHook(options, script.path),\n await installGenericHook({\n label: \"Codex Stop\",\n settingsPath: path.join(homedir(), \".codex\", \"hooks.json\"),\n eventName: \"Stop\",\n shape: \"wrapped\",\n scriptPath: script.path,\n }),\n await installGenericHook({\n label: \"Cursor sessionEnd\",\n settingsPath: path.join(homedir(), \".cursor\", \"hooks.json\"),\n eventName: \"sessionEnd\",\n shape: \"flat\",\n scriptPath: script.path,\n }),\n ];\n const failed = results.find((r) => r.exitCode !== 0);\n if (failed !== undefined) return failed;\n return {\n stdout: results.map((r) => r.stdout.trimEnd()).join(\"\\n\") + \"\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n }\n if (source === \"codex\") {\n return await installGenericHook({\n label: \"Codex Stop\",\n settingsPath: path.join(homedir(), \".codex\", \"hooks.json\"),\n eventName: \"Stop\",\n shape: \"wrapped\",\n scriptPath: script.path,\n });\n }\n if (source === \"cursor\") {\n return await installGenericHook({\n label: \"Cursor sessionEnd\",\n settingsPath: path.join(homedir(), \".cursor\", \"hooks.json\"),\n eventName: \"sessionEnd\",\n shape: \"flat\",\n scriptPath: script.path,\n });\n }\n\n return await installClaudeHook(options, script.path);\n}\n\nasync function installClaudeHook(\n options: HookCommandOptions,\n scriptPath: string,\n): Promise<HookCommandResult> {\n\n const settingsPath = resolveSettingsPath(options);\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n // Walk existing entries and split them into buckets:\n // - `preserved` — foreign wrapped/unknown entries we leave alone.\n // - `oursAlready` — a wrapped entry that already points at OUR exact\n // script path (makes install a no-op).\n // - `oursStale` — a wrapped or legacy entry that references our\n // capture script but at a different absolute path\n // (old install, `npm i` moved us) or in the legacy\n // unwrapped shape. We'll collapse these into a\n // single fresh entry at the new path.\n const preserved: RawEntry[] = [];\n let oursAlready: WrappedEntry | null = null;\n const staleCount = { n: 0 };\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n if (!isOurWrapped(c.entry)) {\n preserved.push(raw);\n continue;\n }\n // Entry belongs to us. Does it already point at the exact script\n // path? If every command in its `hooks[]` that looks like ours is\n // already at `script.path`, it's up to date.\n const exactMatch = c.entry.hooks.some(\n (h) => h.command === scriptPath,\n );\n if (exactMatch && oursAlready === null) {\n oursAlready = c.entry;\n } else {\n staleCount.n += 1;\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n // Legacy unwrapped entry of ours — always migrate to wrapped.\n staleCount.n += 1;\n } else {\n // Foreign legacy entry (user had their own script before\n // settings.json required wrapping). Leave it alone.\n preserved.push(raw);\n }\n } else {\n // Unknown shape — we can't classify it. Preserve verbatim.\n preserved.push(raw);\n }\n }\n\n // If every non-ours entry is a foreign unwrapped command (not a\n // wrapped one) we refuse to touch the file — Claude Code's newer\n // schema will already reject such files, but surfacing it here lets\n // the user clean up before we stack our entry on top. Wrapped foreign\n // entries are fine to leave alongside ours.\n const foreignLegacy = preserved.filter((raw) => {\n const c = classifyEntry(raw);\n return c.kind === \"legacy\";\n });\n if (foreignLegacy.length > 0) {\n const lines = foreignLegacy\n .map((raw) => {\n const c = classifyEntry(raw);\n if (c.kind === \"legacy\") return ` - ${c.entry.command}`;\n return \" - <unrecognized>\";\n })\n .join(\"\\n\");\n return {\n stdout: \"\",\n stderr:\n `almanac: SessionEnd has a foreign legacy entry:\\n${lines}\\n` +\n `Remove or rewrap it manually in ${settingsPath} before installing.\\n`,\n exitCode: 1,\n };\n }\n\n if (oursAlready !== null && staleCount.n === 0) {\n return {\n stdout: `almanac: SessionEnd hook already installed at ${scriptPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Build the fresh wrapped entry and append to preserved foreign\n // entries. Stale entries of ours are dropped (we only ever want a\n // single active entry; multiple copies of the capture hook would\n // double-fire on session end).\n const fresh: WrappedEntry = {\n matcher: \"\",\n hooks: [\n {\n type: \"command\",\n command: scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ],\n };\n\n const newEntries: RawEntry[] = [...preserved, fresh];\n\n settings.hooks = { ...(settings.hooks ?? {}), SessionEnd: newEntries };\n await writeSettings(settingsPath, settings);\n\n return {\n stdout:\n `almanac: SessionEnd hook installed\\n` +\n ` script: ${scriptPath}\\n` +\n ` settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nasync function installGenericHook(args: {\n label: string;\n settingsPath: string;\n eventName: string;\n shape: \"flat\" | \"wrapped\";\n scriptPath: string;\n}): Promise<HookCommandResult> {\n const settings = await readSettings(args.settingsPath);\n const hooksObj =\n settings.hooks !== undefined &&\n settings.hooks !== null &&\n typeof settings.hooks === \"object\"\n ? settings.hooks\n : {};\n const existing = Array.isArray(hooksObj[args.eventName])\n ? (hooksObj[args.eventName] as RawEntry[])\n : [];\n const kept = existing.filter((entry) => !entryHasOurCommand(entry));\n const already = existing.some((entry) =>\n entryHasExactCommand(entry, args.scriptPath),\n );\n if (already && kept.length === existing.length - 1) {\n return {\n stdout: `almanac: ${args.label} hook already installed at ${args.scriptPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n const fresh =\n args.shape === \"wrapped\"\n ? {\n hooks: [\n {\n type: \"command\",\n command: args.scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ],\n }\n : {\n command: args.scriptPath,\n timeout: HOOK_TIMEOUT_SECONDS,\n };\n hooksObj[args.eventName] = [\n ...kept,\n fresh,\n ];\n settings.hooks = hooksObj;\n await writeSettings(args.settingsPath, settings);\n if (args.label.startsWith(\"Codex \")) {\n await ensureCodexHooksFeature(path.join(homedir(), \".codex\", \"config.toml\"));\n }\n return {\n stdout:\n `almanac: ${args.label} hook installed\\n` +\n ` script: ${args.scriptPath}\\n` +\n ` settings: ${args.settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nfunction entryHasOurCommand(entry: unknown): boolean {\n return collectHookCommands(entry).some(isOurCommandPath);\n}\n\nfunction entryHasExactCommand(entry: unknown, command: string): boolean {\n return collectHookCommands(entry).some((candidate) => candidate === command);\n}\n\nfunction collectHookCommands(entry: unknown): string[] {\n if (entry === null || typeof entry !== \"object\") return [];\n const obj = entry as Record<string, unknown>;\n const direct = typeof obj.command === \"string\" ? [obj.command] : [];\n const nested = Array.isArray(obj.hooks)\n ? obj.hooks.flatMap((hook) => collectHookCommands(hook))\n : [];\n return [...direct, ...nested];\n}\n\nasync function ensureCodexHooksFeature(configPath: string): Promise<void> {\n let body = \"\";\n if (existsSync(configPath)) {\n body = await readFile(configPath, \"utf8\");\n }\n if (/^\\s*codex_hooks\\s*=\\s*true\\s*$/m.test(body)) return;\n\n const next = setTomlFeatureFlag(body, \"codex_hooks\", true);\n await mkdir(path.dirname(configPath), { recursive: true });\n const tmp = `${configPath}.almanac-tmp-${process.pid}`;\n await writeFile(tmp, next.endsWith(\"\\n\") ? next : `${next}\\n`, \"utf8\");\n await rename(tmp, configPath);\n}\n\nfunction setTomlFeatureFlag(\n body: string,\n key: string,\n value: boolean,\n): string {\n const desired = `${key} = ${value ? \"true\" : \"false\"}`;\n const lines = body.split(/\\r?\\n/);\n let featuresStart = -1;\n let featuresEnd = lines.length;\n\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*\\[features\\]\\s*$/.test(lines[i] ?? \"\")) {\n featuresStart = i;\n continue;\n }\n if (featuresStart !== -1 && i > featuresStart && /^\\s*\\[.*\\]\\s*$/.test(lines[i] ?? \"\")) {\n featuresEnd = i;\n break;\n }\n }\n\n if (featuresStart === -1) {\n const prefix = body.trim().length === 0 ? \"\" : `${body.trimEnd()}\\n\\n`;\n return `${prefix}[features]\\n${desired}\\n`;\n }\n\n const keyPattern = new RegExp(`^\\\\s*${escapeRegex(key)}\\\\s*=`);\n for (let i = featuresStart + 1; i < featuresEnd; i++) {\n if (keyPattern.test(lines[i] ?? \"\")) {\n lines[i] = desired;\n return lines.join(\"\\n\");\n }\n }\n\n lines.splice(featuresStart + 1, 0, desired);\n return lines.join(\"\\n\");\n}\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport async function runHookUninstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout: `almanac: SessionEnd hook not installed (no settings file)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n const kept: RawEntry[] = [];\n let removed = 0;\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n // Filter out our command(s) from the inner hooks array. Keep\n // anything else in the array intact — a foreign wrapper that\n // happened to include our script alongside its own commands\n // (unusual, but survivable) loses our entry and keeps theirs.\n const innerKept = c.entry.hooks.filter(\n (h) => !isOurCommandPath(h.command),\n );\n const innerRemoved = c.entry.hooks.length - innerKept.length;\n removed += innerRemoved;\n if (innerKept.length === 0) {\n // Only drop the outer wrapper when it was entirely ours. A\n // foreign wrapper that never contained our script stays verbatim\n // below (handled by `innerRemoved === 0`, which leaves\n // `innerKept.length === c.entry.hooks.length`, hence we fall\n // through to the else-branch).\n if (innerRemoved === 0) kept.push(raw);\n // else: fully owned by us, drop the container.\n } else if (innerRemoved === 0) {\n // Untouched foreign wrapper — preserve the raw object to keep\n // any fields (like matcher) byte-for-byte.\n kept.push(raw);\n } else {\n // Partial: rebuild with just the kept inner entries, preserving\n // the original matcher string.\n kept.push({ matcher: c.entry.matcher, hooks: innerKept });\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n removed += 1;\n } else {\n kept.push(raw);\n }\n } else {\n kept.push(raw);\n }\n }\n\n if (removed === 0) {\n return {\n stdout: `almanac: SessionEnd hook not installed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n if (settings.hooks !== undefined) {\n if (kept.length === 0) {\n // Empty SessionEnd array confuses some linters; drop the key when\n // nothing's left.\n const { SessionEnd: _dropped, ...rest } = settings.hooks;\n void _dropped;\n settings.hooks = rest;\n } else {\n settings.hooks = { ...settings.hooks, SessionEnd: kept };\n }\n\n // If `hooks` itself is now empty (user had only our SessionEnd entry\n // and no other hook categories), drop the `hooks` key entirely so\n // uninstall leaves the settings file in the same shape it would be\n // in had we never run install. An empty `\"hooks\": {}` is an obvious\n // breadcrumb in commit diffs.\n if (Object.keys(settings.hooks).length === 0) {\n delete settings.hooks;\n }\n }\n\n await writeSettings(settingsPath, settings);\n\n return {\n stdout: `almanac: SessionEnd hook removed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nexport async function runHookStatus(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const script = resolveHookScriptPath(options);\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath} (does not exist)\\n` +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = settings.hooks?.SessionEnd ?? [];\n\n // Walk the array looking for any entry (wrapped or legacy) that\n // references our capture script. Gathering foreign entries separately\n // lets us show them to the user if nothing of ours was found.\n let ourCommand: string | null = null;\n const foreignSummary: string[] = [];\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n for (const h of c.entry.hooks) {\n if (isOurCommandPath(h.command)) {\n ourCommand ??= h.command;\n } else {\n foreignSummary.push(h.command);\n }\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n ourCommand ??= c.entry.command;\n } else {\n foreignSummary.push(c.entry.command);\n }\n }\n }\n\n if (ourCommand === null) {\n const foreignLines = foreignSummary\n .map((c) => ` - ${c}`)\n .join(\"\\n\");\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath}\\n` +\n (foreignSummary.length > 0\n ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? \"y\" : \"ies\"} present:\\n${foreignLines})\\n`\n : \"\") +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout:\n `SessionEnd hook: installed\\n` +\n `script: ${ourCommand}\\n` +\n `settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Settings JSON helpers ───────────────────────────────────────────\n\nasync function readSettings(settingsPath: string): Promise<SettingsJson> {\n if (!existsSync(settingsPath)) return {};\n try {\n const raw = await readFile(settingsPath, \"utf8\");\n if (raw.trim().length === 0) return {};\n const parsed = JSON.parse(raw) as unknown;\n if (parsed === null || typeof parsed !== \"object\") return {};\n return parsed as SettingsJson;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`failed to read ${settingsPath}: ${msg}`);\n }\n}\n\nasync function writeSettings(\n settingsPath: string,\n settings: SettingsJson,\n): Promise<void> {\n const dir = path.dirname(settingsPath);\n await mkdir(dir, { recursive: true });\n\n // Atomic write: JSON.stringify → tmp file → rename. `rename` within the\n // same filesystem is atomic on POSIX; Claude Code never sees a partial\n // file. Formatted with 2-space indent to match the existing settings.\n const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;\n const body = `${JSON.stringify(settings, null, 2)}\\n`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, settingsPath);\n}\n","import { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface HookPathOptions {\n hookScriptPath?: string;\n settingsPath?: string;\n stableHooksDir?: string;\n}\n\nexport type ScriptResolution =\n | { ok: true; path: string }\n | { ok: false; error: string };\n\nexport function resolveSettingsPath(options: HookPathOptions): string {\n if (options.settingsPath !== undefined) return options.settingsPath;\n return path.join(homedir(), \".claude\", \"settings.json\");\n}\n\n/**\n * Copy the bundled hook script to `~/.claude/hooks/almanac-capture.sh`.\n *\n * This stable, user-owned destination survives Node version switches and\n * npm/npx cache evictions. The copy is idempotent: if bytes already match\n * we skip writing so repeated setup runs do not bump mtimes.\n */\nexport async function copyToStableHooksDir(\n bundledPath: string,\n options: HookPathOptions,\n): Promise<ScriptResolution> {\n const stableHooksDir =\n options.stableHooksDir ?? path.join(homedir(), \".claude\", \"hooks\");\n const dest = path.join(stableHooksDir, \"almanac-capture.sh\");\n\n try {\n await mkdir(stableHooksDir, { recursive: true });\n const srcBytes = await readFile(bundledPath);\n let needsCopy = true;\n if (existsSync(dest)) {\n try {\n const destBytes = await readFile(dest);\n if (srcBytes.equals(destBytes)) needsCopy = false;\n } catch {\n // Can't read dest — overwrite.\n }\n }\n if (needsCopy) {\n await copyFile(bundledPath, dest);\n }\n return { ok: true, path: dest };\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n ok: false,\n error: `could not copy hook script to ${dest}: ${msg}`,\n };\n }\n}\n\n/**\n * Locate the bundled `hooks/almanac-capture.sh`. Mirrors\n * `resolvePromptsDir` from `src/agent/prompts.ts`: two plausible layouts\n * (installed dist vs. source dev), probe each.\n */\nexport function resolveHookScriptPath(\n options: HookPathOptions,\n): ScriptResolution {\n if (options.hookScriptPath !== undefined) {\n return { ok: true, path: options.hookScriptPath };\n }\n\n const here = path.dirname(fileURLToPath(import.meta.url));\n\n const candidates = [\n // Bundled: `.../codealmanac/dist/codealmanac.js` → `../hooks/…`\n path.resolve(here, \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source after ts-node-style module layout or nested dist helpers.\n path.resolve(here, \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source: `.../codealmanac/src/commands/hook/script.ts` → `../../../hooks/…`\n path.resolve(here, \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Defensive nested fallback.\n path.resolve(here, \"..\", \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return { ok: true, path: candidate };\n }\n }\n\n return {\n ok: false,\n error:\n `could not locate hooks/almanac-capture.sh. Tried:\\n` +\n candidates.map((c) => ` - ${c}`).join(\"\\n\"),\n };\n}\n"],"mappings":"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","names":["existsSync","mkdir","readFile","homedir","path","path","homedir","existsSync","readFile","mkdir"]}
@@ -3,35 +3,15 @@ import {
3
3
  findNearestAlmanacDir,
4
4
  getGlobalAlmanacDir,
5
5
  getRepoAlmanacDir
6
- } from "./chunk-7JUX4ADQ.js";
6
+ } from "./chunk-IZT6RBHS.js";
7
7
 
8
8
  // src/update/config.ts
9
9
  import { existsSync } from "fs";
10
10
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
11
11
  import { dirname, join } from "path";
12
- var ALL_AGENT_PROVIDER_IDS = ["claude", "codex", "cursor"];
13
- var AGENT_PROVIDER_IDS = ALL_AGENT_PROVIDER_IDS;
14
- var DEFAULT_AGENT_PROVIDER_IDS = ["claude", "codex"];
15
- function isCursorEnabled(env = process.env) {
16
- return env.CODEALMANAC_ENABLE_CURSOR === "1";
17
- }
18
- function getEnabledAgentProviderIds(env = process.env) {
19
- return isCursorEnabled(env) ? [...ALL_AGENT_PROVIDER_IDS] : [...DEFAULT_AGENT_PROVIDER_IDS];
20
- }
21
- function isEnabledAgentProviderId(value, env = process.env) {
22
- return getEnabledAgentProviderIds(env).includes(value);
23
- }
24
- function formatEnabledAgentProviderList(env = process.env) {
25
- return getEnabledAgentProviderIds(env).join(", ");
26
- }
27
- function disabledAgentProviderMessage(provider) {
28
- if (provider === "cursor") {
29
- return "cursor support is disabled. Set CODEALMANAC_ENABLE_CURSOR=1 to enable experimental Cursor support.";
30
- }
31
- return `${provider} support is disabled.`;
32
- }
12
+ var AGENT_PROVIDER_IDS = ["claude", "codex", "cursor"];
33
13
  function isAgentProviderId(value) {
34
- return ALL_AGENT_PROVIDER_IDS.includes(value);
14
+ return AGENT_PROVIDER_IDS.includes(value);
35
15
  }
36
16
  function defaultConfig() {
37
17
  return {
@@ -366,11 +346,6 @@ function pruneEmptyObjects(raw) {
366
346
 
367
347
  export {
368
348
  AGENT_PROVIDER_IDS,
369
- isCursorEnabled,
370
- getEnabledAgentProviderIds,
371
- isEnabledAgentProviderId,
372
- formatEnabledAgentProviderList,
373
- disabledAgentProviderMessage,
374
349
  isAgentProviderId,
375
350
  getConfigPath,
376
351
  getLegacyConfigPath,
@@ -381,4 +356,4 @@ export {
381
356
  parseConfigText,
382
357
  serializeConfig
383
358
  };
384
- //# sourceMappingURL=chunk-3E7JNMTZ.js.map
359
+ //# sourceMappingURL=chunk-5BWUMAOX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/update/config.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport {\n findNearestAlmanacDir,\n getGlobalAlmanacDir,\n getRepoAlmanacDir,\n} from \"../paths.js\";\n\nexport const AGENT_PROVIDER_IDS = [\"claude\", \"codex\", \"cursor\"] as const;\nexport type AgentProviderId = (typeof AGENT_PROVIDER_IDS)[number];\n\nexport function isAgentProviderId(value: string): value is AgentProviderId {\n return (AGENT_PROVIDER_IDS as readonly string[]).includes(value);\n}\n\nexport interface AgentConfig {\n /** Default provider for agent-backed lifecycle commands. Default: \"claude\". */\n default: AgentProviderId;\n /** Optional per-provider model override. `null` means provider default. */\n models: Partial<Record<AgentProviderId, string | null>>;\n}\n\n/**\n * `~/.almanac/config.toml` — global, cross-wiki configuration. Legacy\n * `config.json` is read and migrated forward on first normal access.\n *\n * Missing or malformed → defaults. Same tolerance as `UpdateState`:\n * the CLI must not be able to fail because this file drifted.\n */\nexport interface GlobalConfig {\n /** When `false`, suppress the pre-command update-nag banner. Default: true. */\n update_notifier: boolean;\n /** Agent-provider settings for agent-backed lifecycle commands. */\n agent: AgentConfig;\n}\n\nexport function defaultConfig(): GlobalConfig {\n return {\n update_notifier: true,\n agent: {\n default: \"claude\",\n models: {\n claude: null,\n codex: null,\n cursor: null,\n },\n },\n };\n}\n\nexport function getConfigPath(): string {\n return join(getGlobalAlmanacDir(), \"config.toml\");\n}\n\nexport function getLegacyConfigPath(): string {\n return join(getGlobalAlmanacDir(), \"config.json\");\n}\n\nexport function getProjectConfigPath(cwd: string): string | null {\n const repoRoot = findNearestAlmanacDir(cwd);\n return repoRoot === null ? null : join(getRepoAlmanacDir(repoRoot), \"config.toml\");\n}\n\nexport type ConfigOrigin = \"default\" | \"user\" | \"project\";\n\nexport interface ConfigReadOptions {\n path?: string;\n cwd?: string;\n}\n\nexport interface ConfigReadResult {\n config: GlobalConfig;\n origins: Record<string, ConfigOrigin>;\n raw: Record<string, unknown>;\n}\n\nexport async function readConfig(\n input?: string | ConfigReadOptions,\n): Promise<GlobalConfig> {\n return (await readConfigWithOrigins(input)).config;\n}\n\nexport async function readConfigWithOrigins(\n input?: string | ConfigReadOptions,\n): Promise<ConfigReadResult> {\n const opts = normalizeReadOptions(input);\n if (opts.path !== undefined) {\n const raw = await readRawConfigObject(opts.path);\n return {\n config: normalizeRawConfig(raw),\n origins: originsFromRaw(raw, \"user\"),\n raw,\n };\n }\n\n const file = getConfigPath();\n await migrateLegacyConfigIfNeeded(file);\n const userRaw = await readRawConfigObject(file);\n const mergedRaw = cloneJsonObject(userRaw);\n const origins = originsFromRaw(userRaw, \"user\");\n const projectPath = opts.cwd !== undefined ? getProjectConfigPath(opts.cwd) : null;\n if (projectPath !== null) {\n const projectRaw = await readRawConfigObject(projectPath);\n applyProjectConfig(mergedRaw, projectRaw);\n Object.assign(origins, originsFromRaw(projectRaw, \"project\", true));\n }\n return {\n config: normalizeRawConfig(mergedRaw),\n origins,\n raw: mergedRaw,\n };\n}\n\nfunction normalizeReadOptions(\n input?: string | ConfigReadOptions,\n): ConfigReadOptions {\n return typeof input === \"string\" ? { path: input } : input ?? {};\n}\n\nasync function migrateLegacyConfigIfNeeded(file: string): Promise<void> {\n if (existsSync(file)) return;\n const legacy = getLegacyConfigPath();\n if (!existsSync(legacy)) return;\n const raw = await readRawConfigObject(legacy);\n if (Object.keys(raw).length === 0) return;\n await writeConfig(normalizeRawConfig(raw), file);\n}\n\nfunction normalizeRawConfig(raw: Record<string, unknown>): GlobalConfig {\n const defaults = defaultConfig();\n const rawAgent =\n raw.agent !== undefined &&\n raw.agent !== null &&\n typeof raw.agent === \"object\" &&\n !Array.isArray(raw.agent)\n ? (raw.agent as Partial<AgentConfig>)\n : {};\n const rawDefault =\n typeof rawAgent.default === \"string\" &&\n isAgentProviderId(rawAgent.default)\n ? rawAgent.default\n : defaults.agent.default;\n const rawModels =\n rawAgent.models !== undefined &&\n rawAgent.models !== null &&\n typeof rawAgent.models === \"object\" &&\n !Array.isArray(rawAgent.models)\n ? (rawAgent.models as Record<string, unknown>)\n : {};\n const models: Partial<Record<AgentProviderId, string | null>> = {\n ...defaults.agent.models,\n };\n for (const id of AGENT_PROVIDER_IDS) {\n const value = rawModels[id];\n if (typeof value === \"string\" && value.length > 0) {\n models[id] = value === \"default\" || value === \"null\" ? null : value;\n } else if (value === null) {\n models[id] = null;\n }\n }\n return {\n update_notifier:\n typeof raw.update_notifier === \"boolean\"\n ? raw.update_notifier\n : defaults.update_notifier,\n agent: {\n default: rawDefault,\n models,\n },\n };\n}\n\nfunction applyProjectConfig(\n target: Record<string, unknown>,\n projectRaw: Record<string, unknown>,\n): void {\n const projectAgent =\n projectRaw.agent !== null &&\n typeof projectRaw.agent === \"object\" &&\n !Array.isArray(projectRaw.agent)\n ? projectRaw.agent as Record<string, unknown>\n : {};\n if (Object.keys(projectAgent).length === 0) return;\n const targetAgent =\n target.agent !== null &&\n typeof target.agent === \"object\" &&\n !Array.isArray(target.agent)\n ? target.agent as Record<string, unknown>\n : {};\n target.agent = targetAgent;\n if (typeof projectAgent.default === \"string\") {\n targetAgent.default = projectAgent.default;\n }\n const projectModels =\n projectAgent.models !== null &&\n typeof projectAgent.models === \"object\" &&\n !Array.isArray(projectAgent.models)\n ? projectAgent.models as Record<string, unknown>\n : {};\n if (Object.keys(projectModels).length === 0) return;\n const targetModels =\n targetAgent.models !== null &&\n typeof targetAgent.models === \"object\" &&\n !Array.isArray(targetAgent.models)\n ? targetAgent.models as Record<string, unknown>\n : {};\n targetAgent.models = targetModels;\n for (const id of AGENT_PROVIDER_IDS) {\n if (Object.prototype.hasOwnProperty.call(projectModels, id)) {\n targetModels[id] = projectModels[id];\n }\n }\n}\n\nfunction originsFromRaw(\n raw: Record<string, unknown>,\n origin: ConfigOrigin,\n agentOnly = false,\n): Record<string, ConfigOrigin> {\n const origins: Record<string, ConfigOrigin> = {};\n if (!agentOnly && Object.prototype.hasOwnProperty.call(raw, \"update_notifier\")) {\n origins.update_notifier = origin;\n }\n const agent =\n raw.agent !== null &&\n typeof raw.agent === \"object\" &&\n !Array.isArray(raw.agent)\n ? raw.agent as Record<string, unknown>\n : {};\n if (Object.prototype.hasOwnProperty.call(agent, \"default\")) {\n origins[\"agent.default\"] = origin;\n }\n const models =\n agent.models !== null &&\n typeof agent.models === \"object\" &&\n !Array.isArray(agent.models)\n ? agent.models as Record<string, unknown>\n : {};\n for (const id of AGENT_PROVIDER_IDS) {\n if (Object.prototype.hasOwnProperty.call(models, id)) {\n origins[`agent.models.${id}`] = origin;\n }\n }\n return origins;\n}\n\nasync function readSingleConfig(file: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(file, \"utf8\");\n } catch {\n return defaultConfig();\n }\n const trimmed = raw.trim();\n if (trimmed.length === 0) return defaultConfig();\n try {\n return normalizeRawConfig(parseConfigText(trimmed, file));\n } catch {\n return defaultConfig();\n }\n}\n\nexport async function writeConfig(\n config: GlobalConfig | Partial<GlobalConfig>,\n path?: string,\n): Promise<void> {\n const file = path ?? getConfigPath();\n await mkdir(dirname(file), { recursive: true });\n const current = await readSingleConfig(file);\n const existingRaw = await readRawConfigObject(file);\n const stored = toStoredConfigPatch(config, current, existingRaw);\n const body = serializeConfig(stored, file);\n const tmp = `${file}.tmp`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, file);\n}\n\nfunction normalizeConfig(config: GlobalConfig | Partial<GlobalConfig>): GlobalConfig {\n const defaults = defaultConfig();\n return {\n update_notifier:\n typeof config.update_notifier === \"boolean\"\n ? config.update_notifier\n : defaults.update_notifier,\n agent: {\n default:\n config.agent !== undefined && isAgentProviderId(config.agent.default)\n ? config.agent.default\n : defaults.agent.default,\n models: {\n ...defaults.agent.models,\n ...(config.agent?.models ?? {}),\n },\n },\n };\n}\n\nasync function readRawConfigObject(\n path: string,\n): Promise<Record<string, unknown>> {\n try {\n return parseConfigText(await readFile(path, \"utf8\"), path);\n } catch {\n // Fall through to empty.\n }\n return {};\n}\n\nexport function parseConfigText(\n raw: string,\n path = \"config.toml\",\n): Record<string, unknown> {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return {};\n if (path.endsWith(\".json\") || trimmed.startsWith(\"{\")) {\n const parsed = JSON.parse(trimmed) as unknown;\n return parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {};\n }\n return parseTomlConfig(trimmed);\n}\n\nexport function serializeConfig(\n raw: Record<string, unknown>,\n path = \"config.toml\",\n): string {\n return path.endsWith(\".json\")\n ? `${JSON.stringify(raw, null, 2)}\\n`\n : serializeTomlConfig(raw);\n}\n\nfunction parseTomlConfig(raw: string): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n let section: string[] = [];\n for (const original of raw.split(/\\r?\\n/)) {\n const line = stripTomlComment(original).trim();\n if (line.length === 0) continue;\n const sectionMatch = line.match(/^\\[([A-Za-z0-9_.-]+)\\]$/);\n if (sectionMatch !== null) {\n section = sectionMatch[1]!.split(\".\");\n continue;\n }\n const eq = line.indexOf(\"=\");\n if (eq === -1) continue;\n const key = line.slice(0, eq).trim();\n const value = parseTomlValue(line.slice(eq + 1).trim());\n setObjectPath(result, [...section, key], value);\n }\n return result;\n}\n\nfunction serializeTomlConfig(raw: Record<string, unknown>): string {\n const lines: string[] = [];\n if (typeof raw.update_notifier === \"boolean\") {\n lines.push(`update_notifier = ${raw.update_notifier ? \"true\" : \"false\"}`);\n }\n const agent =\n raw.agent !== null &&\n typeof raw.agent === \"object\" &&\n !Array.isArray(raw.agent)\n ? raw.agent as Record<string, unknown>\n : {};\n if (typeof agent.default === \"string\") {\n if (lines.length > 0) lines.push(\"\");\n lines.push(\"[agent]\");\n lines.push(`default = ${tomlString(agent.default)}`);\n }\n const models =\n agent.models !== null &&\n typeof agent.models === \"object\" &&\n !Array.isArray(agent.models)\n ? agent.models as Record<string, unknown>\n : {};\n const modelLines: string[] = [];\n for (const id of AGENT_PROVIDER_IDS) {\n if (!Object.prototype.hasOwnProperty.call(models, id)) continue;\n const value = models[id] === null ? \"default\" : models[id];\n if (typeof value === \"string\" && value.length > 0) {\n modelLines.push(`${id} = ${tomlString(value)}`);\n }\n }\n if (modelLines.length > 0) {\n if (lines.length > 0) lines.push(\"\");\n lines.push(\"[agent.models]\", ...modelLines);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction stripTomlComment(line: string): string {\n let inString = false;\n let escaped = false;\n for (let i = 0; i < line.length; i++) {\n const ch = line[i];\n if (escaped) {\n escaped = false;\n continue;\n }\n if (ch === \"\\\\\") {\n escaped = true;\n continue;\n }\n if (ch === \"\\\"\") inString = !inString;\n if (ch === \"#\" && !inString) return line.slice(0, i);\n }\n return line;\n}\n\nfunction parseTomlValue(raw: string): string | boolean {\n if (raw === \"true\") return true;\n if (raw === \"false\") return false;\n if (raw.startsWith(\"\\\"\") && raw.endsWith(\"\\\"\")) {\n return JSON.parse(raw) as string;\n }\n return raw;\n}\n\nfunction tomlString(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction setObjectPath(\n raw: Record<string, unknown>,\n path: string[],\n value: string | boolean,\n): void {\n let cursor = raw;\n for (const part of path.slice(0, -1)) {\n const next = cursor[part];\n if (next === null || typeof next !== \"object\" || Array.isArray(next)) {\n cursor[part] = {};\n }\n cursor = cursor[part] as Record<string, unknown>;\n }\n const leaf = path[path.length - 1];\n if (leaf !== undefined) cursor[leaf] = value;\n}\n\nfunction toStoredConfigPatch(\n config: GlobalConfig | Partial<GlobalConfig>,\n current: GlobalConfig,\n raw: Record<string, unknown>,\n): Record<string, unknown> {\n const normalized = normalizeConfig(config);\n const defaults = defaultConfig();\n const stored = cloneJsonObject(raw);\n\n if (\n config.update_notifier !== undefined &&\n normalized.update_notifier !== current.update_notifier\n ) {\n setStoredValue(\n stored,\n [\"update_notifier\"],\n normalized.update_notifier,\n defaults.update_notifier,\n );\n }\n\n if (config.agent !== undefined) {\n if (\n config.agent.default !== undefined &&\n normalized.agent.default !== current.agent.default\n ) {\n setStoredValue(\n stored,\n [\"agent\", \"default\"],\n normalized.agent.default,\n defaults.agent.default,\n );\n }\n\n const inputModels = config.agent.models ?? {};\n for (const id of AGENT_PROVIDER_IDS) {\n if (!Object.prototype.hasOwnProperty.call(inputModels, id)) continue;\n const value = normalized.agent.models[id] ?? null;\n const currentValue = current.agent.models[id] ?? null;\n const defaultValue = defaults.agent.models[id] ?? null;\n if (value !== currentValue) {\n setStoredValue(stored, [\"agent\", \"models\", id], value, defaultValue);\n }\n }\n }\n pruneEmptyObjects(stored);\n return stored;\n}\n\nfunction setStoredValue(\n raw: Record<string, unknown>,\n path: string[],\n value: string | boolean | null,\n defaultValue: string | boolean | null,\n): void {\n let cursor = raw;\n for (const part of path.slice(0, -1)) {\n const next = cursor[part];\n if (next === null || typeof next !== \"object\" || Array.isArray(next)) {\n cursor[part] = {};\n }\n cursor = cursor[part] as Record<string, unknown>;\n }\n const leaf = path[path.length - 1];\n if (leaf === undefined) return;\n cursor[leaf] = value;\n if (value !== defaultValue) return;\n // Keep explicit defaults only when the caller changed the value to the\n // default. Unchanged explicit defaults are preserved by cloning `raw`.\n}\n\nfunction cloneJsonObject(raw: Record<string, unknown>): Record<string, unknown> {\n return JSON.parse(JSON.stringify(raw)) as Record<string, unknown>;\n}\n\nfunction pruneEmptyObjects(raw: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(raw)) {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n continue;\n }\n pruneEmptyObjects(value as Record<string, unknown>);\n if (Object.keys(value).length === 0) delete raw[key];\n }\n}\n"],"mappings":"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","names":[]}