wendkeep 0.75.2 → 0.75.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/packages/integrations/src/host-hooks.mjs +6 -0
- package/src/init.mjs +40 -10
- package/src/taxonomy.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.75.3] — 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Sync seguro no self-checkout.** O instalador agora reconhece o repositório do próprio
|
|
12
|
+
WendKeep e migra hooks Claude/Codex para `node ./bin/wendkeep.mjs` sem duplicá-los. Projetos
|
|
13
|
+
consumidores continuam usando `npx --no-install`, e executar `sync` no checkout de
|
|
14
|
+
desenvolvimento não reintroduz a autodependência publicada.
|
|
15
|
+
|
|
7
16
|
## [0.75.2] — 2026-08-20
|
|
8
17
|
|
|
9
18
|
### Fixed
|
package/README.en.md
CHANGED
|
@@ -180,6 +180,10 @@ later stages. An invalid `.wendkeep.json` stops at `init` without falling back t
|
|
|
180
180
|
The install stays outside `sync` on purpose: a running process cannot replace itself and
|
|
181
181
|
keep going — the code in memory would still be the old one.
|
|
182
182
|
|
|
183
|
+
In WendKeep's own development checkout, do not install `wendkeep` in `devDependencies`. Use
|
|
184
|
+
`node ./bin/wendkeep.mjs sync --project . --yes`: the installer recognizes the self-checkout and
|
|
185
|
+
keeps hooks on the working tree without duplicating consumer `npx` commands.
|
|
186
|
+
|
|
183
187
|
In a **pnpm** monorepo the install command differs (`npm` in a pnpm repo fails with
|
|
184
188
|
`Cannot read properties of null (reading 'matches')`). Resolve the published version first and
|
|
185
189
|
reuse exactly the value returned:
|
package/README.md
CHANGED
|
@@ -180,6 +180,10 @@ later stages. An invalid `.wendkeep.json` stops at `init` without falling back t
|
|
|
180
180
|
The install stays outside `sync` on purpose: a running process cannot replace itself and
|
|
181
181
|
keep going — the code in memory would still be the old one.
|
|
182
182
|
|
|
183
|
+
In WendKeep's own development checkout, do not install `wendkeep` in `devDependencies`. Use
|
|
184
|
+
`node ./bin/wendkeep.mjs sync --project . --yes`: the installer recognizes the self-checkout and
|
|
185
|
+
keeps hooks on the working tree without duplicating consumer `npx` commands.
|
|
186
|
+
|
|
183
187
|
In a **pnpm** monorepo the install command differs (`npm` in a pnpm repo fails with
|
|
184
188
|
`Cannot read properties of null (reading 'matches')`). Resolve the published version first and
|
|
185
189
|
reuse exactly the value returned:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.3",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -36,6 +36,12 @@ export function hookCommand(name) {
|
|
|
36
36
|
return `npx --no-install wendkeep hook ${name}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// O checkout do próprio WendKeep não depende do pacote publicado. Seus hooks versionados
|
|
40
|
+
// executam o binário do working tree para que sync/dogfood não recrie a autodependência.
|
|
41
|
+
export function hookCommandWorkingTree(name) {
|
|
42
|
+
return `node ./bin/wendkeep.mjs hook ${name}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
// Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
|
|
40
46
|
// de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
|
|
41
47
|
// quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
|
package/src/init.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CHANGE_NUDGE_HOOKS,
|
|
14
14
|
CHANGE_GATE_HOOKS,
|
|
15
15
|
hookCommand,
|
|
16
|
+
hookCommandWorkingTree,
|
|
16
17
|
hookCommandLocal,
|
|
17
18
|
hookCommandLocalLegacy,
|
|
18
19
|
codexHookSpecs,
|
|
@@ -109,7 +110,19 @@ function backup(path) {
|
|
|
109
110
|
|
|
110
111
|
// Comando preferido para um hook: node-direto quando o projeto tem o pacote local (alta
|
|
111
112
|
// frequência sem cold-start de npx — ver R3 do design 0.31.0); senão o npx portátil.
|
|
113
|
+
export function isWendkeepSelfCheckout(projectPath) {
|
|
114
|
+
try {
|
|
115
|
+
if (!projectPath || !existsSync(join(projectPath, 'bin', 'wendkeep.mjs'))) return false;
|
|
116
|
+
const pkg = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8'));
|
|
117
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.wendkeep;
|
|
118
|
+
return pkg.name === 'wendkeep' && String(bin || '').replace(/\\/g, '/') === 'bin/wendkeep.mjs';
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
export function hookCommandFor(name, projectPath) {
|
|
125
|
+
if (isWendkeepSelfCheckout(projectPath)) return hookCommandWorkingTree(name);
|
|
113
126
|
try {
|
|
114
127
|
if (projectPath && existsSync(join(projectPath, 'node_modules', 'wendkeep', 'hooks', `${name}.mjs`))) {
|
|
115
128
|
return hookCommandLocal(name);
|
|
@@ -142,13 +155,22 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
142
155
|
...CHANGE_GATE_HOOKS,
|
|
143
156
|
...companionHookSpecs(companions, { dotcontextHookLevel }),
|
|
144
157
|
].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
158
|
+
const selfCheckout = isWendkeepSelfCheckout(projectPath);
|
|
145
159
|
for (const h of allSpecs) {
|
|
160
|
+
const useWorkingTree = !h.command && selfCheckout;
|
|
146
161
|
const useLocal = !h.command && h.preferLocal && localHookAvailable(h.name, projectPath);
|
|
147
|
-
const command = h.command ?? (
|
|
148
|
-
|
|
162
|
+
const command = h.command ?? (useWorkingTree
|
|
163
|
+
? hookCommandWorkingTree(h.name)
|
|
164
|
+
: (useLocal ? 'node' : hookCommand(h.name)));
|
|
165
|
+
const args = !useWorkingTree && useLocal ? [localHookArg(h.name)] : undefined;
|
|
149
166
|
// Dual-recognition: um hook nomeado é reconhecido tanto na forma npx quanto na node-direta,
|
|
150
167
|
// para que trocar a forma preferida (ou re-initar noutra máquina) nunca duplique o grupo.
|
|
151
|
-
const candidates = h.command ? [h.command] : [
|
|
168
|
+
const candidates = h.command ? [h.command] : [
|
|
169
|
+
hookCommand(h.name),
|
|
170
|
+
hookCommandWorkingTree(h.name),
|
|
171
|
+
hookCommandLocal(h.name),
|
|
172
|
+
hookCommandLocalLegacy(h.name),
|
|
173
|
+
];
|
|
152
174
|
const ownsHook = (x) => candidates.includes(x.command)
|
|
153
175
|
|| (x.command === 'node' && Array.isArray(x.args) && x.args[0] === localHookArg(h.name));
|
|
154
176
|
const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
|
|
@@ -159,7 +181,8 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
159
181
|
// entry's fields in place — without disturbing any sibling hooks the user grouped with it.
|
|
160
182
|
const hk = owning.hooks.find(ownsHook);
|
|
161
183
|
const brokenRelative = hk?.command === hookCommandLocalLegacy(h.name);
|
|
162
|
-
|
|
184
|
+
const wrongRuntime = !h.command && hk?.command !== command;
|
|
185
|
+
if (force || brokenRelative || wrongRuntime) {
|
|
163
186
|
hk.command = command;
|
|
164
187
|
if (args) hk.args = args;
|
|
165
188
|
else delete hk.args;
|
|
@@ -208,14 +231,19 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
208
231
|
// it does handle is the legacy `timeout` key, which Codex silently ignores in favour of a
|
|
209
232
|
// 600s default; rewriting it to `timeoutSec` invalidates the stored trusted_hash and costs
|
|
210
233
|
// the user one "Hooks need review" prompt. That is the point.
|
|
211
|
-
export function mergeCodexHooks(existing, { force = false } = {}) {
|
|
234
|
+
export function mergeCodexHooks(existing, { force = false, projectPath = '' } = {}) {
|
|
212
235
|
const file = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
213
236
|
file.hooks = { ...(file.hooks || {}) };
|
|
214
237
|
const specs = codexHookSpecs([...SESSION_HOOKS, ...CHANGE_NUDGE_HOOKS, ...CHANGE_GATE_HOOKS])
|
|
215
238
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
239
|
+
const selfCheckout = isWendkeepSelfCheckout(projectPath);
|
|
216
240
|
for (const h of specs) {
|
|
217
|
-
const entry =
|
|
218
|
-
|
|
241
|
+
const entry = {
|
|
242
|
+
...codexHookEntry(h),
|
|
243
|
+
...(selfCheckout ? { command: hookCommandWorkingTree(h.name) } : {}),
|
|
244
|
+
};
|
|
245
|
+
const candidates = new Set([hookCommand(h.name), hookCommandWorkingTree(h.name)]);
|
|
246
|
+
const owns = (x) => candidates.has(x.command);
|
|
219
247
|
const groups = Array.isArray(file.hooks[h.event]) ? [...file.hooks[h.event]] : [];
|
|
220
248
|
const owning = groups.find((g) => (g.hooks || []).some(owns));
|
|
221
249
|
if (owning) {
|
|
@@ -224,7 +252,8 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
|
|
|
224
252
|
// `timeout` is the pre-0.46 key: Codex never read it. Migrate it even without --force,
|
|
225
253
|
// otherwise the hook keeps running at the 600s default forever.
|
|
226
254
|
const legacyTimeout = 'timeout' in hk;
|
|
227
|
-
|
|
255
|
+
const wrongRuntime = hk.command !== entry.command;
|
|
256
|
+
if (force || legacyTimeout || wrongRuntime) {
|
|
228
257
|
if (legacyTimeout) {
|
|
229
258
|
if (hk.timeoutSec === undefined) hk.timeoutSec = hk.timeout;
|
|
230
259
|
delete hk.timeout;
|
|
@@ -233,6 +262,7 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
|
|
|
233
262
|
hk.timeoutSec = entry.timeoutSec;
|
|
234
263
|
if (entry.statusMessage) hk.statusMessage = entry.statusMessage;
|
|
235
264
|
}
|
|
265
|
+
if (wrongRuntime) hk.command = entry.command;
|
|
236
266
|
}
|
|
237
267
|
if (force && matcher) owning.matcher = matcher;
|
|
238
268
|
if (force && !matcher) delete owning.matcher;
|
|
@@ -593,12 +623,12 @@ export async function runInit(argv) {
|
|
|
593
623
|
const codexPath = join(projectPath, '.codex', 'hooks.json');
|
|
594
624
|
const codexRead = readJsonSafe(codexPath);
|
|
595
625
|
if (!codexRead.ok) {
|
|
596
|
-
writeJson(`${codexPath}.new`, mergeCodexHooks(null, { force: true }));
|
|
626
|
+
writeJson(`${codexPath}.new`, mergeCodexHooks(null, { force: true, projectPath }));
|
|
597
627
|
log(M.codexBadJson(codexPath));
|
|
598
628
|
} else {
|
|
599
629
|
const hadFile = codexRead.data !== null;
|
|
600
630
|
if (hadFile) backup(codexPath);
|
|
601
|
-
writeJson(codexPath, mergeCodexHooks(codexRead.data, { force: args.force }));
|
|
631
|
+
writeJson(codexPath, mergeCodexHooks(codexRead.data, { force: args.force, projectPath }));
|
|
602
632
|
log(M.codexHooks(hadFile ? M.merged : M.created, hadFile ? M.bakSaved : ''));
|
|
603
633
|
}
|
|
604
634
|
log(M.codexTrust);
|
package/src/taxonomy.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
codexHookEntry,
|
|
12
12
|
codexHookSpecs,
|
|
13
13
|
hookCommand,
|
|
14
|
+
hookCommandWorkingTree,
|
|
14
15
|
hookCommandLocal,
|
|
15
16
|
hookCommandLocalLegacy,
|
|
16
17
|
} from '../packages/integrations/src/host-hooks.mjs';
|
|
@@ -24,6 +25,7 @@ export {
|
|
|
24
25
|
codexHookEntry,
|
|
25
26
|
codexHookSpecs,
|
|
26
27
|
hookCommand,
|
|
28
|
+
hookCommandWorkingTree,
|
|
27
29
|
hookCommandLocal,
|
|
28
30
|
hookCommandLocalLegacy,
|
|
29
31
|
mcpServerEntry,
|