@raidiant/notifai 10.1.7 → 11.0.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/CHANGELOG.md +42 -0
- package/README.md +23 -13
- package/dist/claude-wake.d.ts +1 -2
- package/dist/claude-wake.js +15 -61
- package/dist/claude-wake.js.map +1 -1
- package/dist/cli-bin.js +3 -14
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-contract.d.ts +12 -0
- package/dist/cli-contract.js +19 -0
- package/dist/cli-contract.js.map +1 -0
- package/dist/cli-release.d.ts +0 -1
- package/dist/cli-release.js +5 -21
- package/dist/cli-release.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/codex-wake.d.ts +1 -1
- package/dist/codex-wake.js +8 -36
- package/dist/codex-wake.js.map +1 -1
- package/dist/command-session.js +2 -1
- package/dist/command-session.js.map +1 -1
- package/dist/commands-acknowledge.d.ts +13 -0
- package/dist/commands-acknowledge.js +67 -0
- package/dist/commands-acknowledge.js.map +1 -0
- package/dist/commands-ask.d.ts +63 -0
- package/dist/commands-ask.js +407 -0
- package/dist/commands-ask.js.map +1 -0
- package/dist/commands-close.d.ts +6 -0
- package/dist/commands-close.js +197 -0
- package/dist/commands-close.js.map +1 -0
- package/dist/commands-config.js +2 -2
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.js +2 -2
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.js +61 -120
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.d.ts +0 -1
- package/dist/commands-harness-context.js +2 -4
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hook-diagnostics.d.ts +27 -0
- package/dist/commands-hook-diagnostics.js +97 -0
- package/dist/commands-hook-diagnostics.js.map +1 -0
- package/dist/commands-hook-install.d.ts +26 -0
- package/dist/commands-hook-install.js +549 -0
- package/dist/commands-hook-install.js.map +1 -0
- package/dist/commands-hook-run.d.ts +29 -0
- package/dist/commands-hook-run.js +517 -0
- package/dist/commands-hook-run.js.map +1 -0
- package/dist/commands-init.d.ts +2 -7
- package/dist/commands-init.js +59 -92
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.js +35 -3
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +5 -5
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.js +17 -31
- package/dist/commands-update.js.map +1 -1
- package/dist/commands.d.ts +11 -6
- package/dist/commands.js +9 -4
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +14 -4
- package/dist/config.js +35 -9
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +3 -0
- package/dist/file-lock.js +33 -17
- package/dist/file-lock.js.map +1 -1
- package/dist/guidance-content.js +30 -15
- package/dist/guidance-content.js.map +1 -1
- package/dist/hook-acknowledgements.d.ts +42 -0
- package/dist/hook-acknowledgements.js +316 -0
- package/dist/hook-acknowledgements.js.map +1 -0
- package/dist/hook-events.d.ts +6 -0
- package/dist/hook-events.js +22 -0
- package/dist/hook-events.js.map +1 -0
- package/dist/hook-gates.d.ts +7 -0
- package/dist/hook-gates.js +21 -0
- package/dist/hook-gates.js.map +1 -0
- package/dist/hook-lifecycle.d.ts +93 -0
- package/dist/{hooks.js → hook-lifecycle.js} +61 -1359
- package/dist/hook-lifecycle.js.map +1 -0
- package/dist/hook-project-sessions.d.ts +28 -0
- package/dist/hook-project-sessions.js +141 -0
- package/dist/hook-project-sessions.js.map +1 -0
- package/dist/hook-question-lock.d.ts +7 -0
- package/dist/hook-question-lock.js +173 -0
- package/dist/hook-question-lock.js.map +1 -0
- package/dist/hook-question-retirement.d.ts +57 -0
- package/dist/hook-question-retirement.js +261 -0
- package/dist/hook-question-retirement.js.map +1 -0
- package/dist/hook-question-state.d.ts +45 -0
- package/dist/hook-question-state.js +196 -0
- package/dist/hook-question-state.js.map +1 -0
- package/dist/hook-session-state.d.ts +31 -0
- package/dist/hook-session-state.js +249 -0
- package/dist/hook-session-state.js.map +1 -0
- package/dist/{hooks.d.ts → hook-types.d.ts} +17 -236
- package/dist/hook-types.js +4 -0
- package/dist/hook-types.js.map +1 -0
- package/dist/install-hooks.d.ts +72 -60
- package/dist/install-hooks.js +292 -211
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +3 -16
- package/dist/interactive.js +19 -95
- package/dist/interactive.js.map +1 -1
- package/dist/local-path.d.ts +7 -0
- package/dist/local-path.js +27 -0
- package/dist/local-path.js.map +1 -0
- package/dist/node-floor.js +3 -10
- package/dist/node-floor.js.map +1 -1
- package/dist/npm-invocation.d.ts +34 -0
- package/dist/npm-invocation.js +53 -0
- package/dist/npm-invocation.js.map +1 -0
- package/dist/openclaw-plugin.d.ts +12 -3
- package/dist/openclaw-plugin.js +42 -16
- package/dist/openclaw-plugin.js.map +1 -1
- package/dist/opencode-plugin.d.ts +2 -2
- package/dist/opencode-plugin.js +4 -6
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +11 -13
- package/dist/program.js.map +1 -1
- package/dist/project-enablement.d.ts +6 -0
- package/dist/project-enablement.js +11 -27
- package/dist/project-enablement.js.map +1 -1
- package/dist/question-settlement-process.d.ts +1 -1
- package/dist/readiness.d.ts +0 -2
- package/dist/readiness.js +0 -4
- package/dist/readiness.js.map +1 -1
- package/dist/send.d.ts +8 -4
- package/dist/send.js +145 -26
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +3 -1
- package/dist/session-activation.js +16 -0
- package/dist/session-activation.js.map +1 -1
- package/dist/skill-source/manifest.json +4 -4
- package/dist/skill-source/notifai/SKILL.md +60 -60
- package/dist/skill-source/notifai/references/harness-setup.md +41 -16
- package/dist/ui/config-view.d.ts +0 -2
- package/dist/ui/config-view.js +1 -5
- package/dist/ui/config-view.js.map +1 -1
- package/dist/ui/theme.d.ts +0 -3
- package/dist/ui/theme.js +0 -1
- package/dist/ui/theme.js.map +1 -1
- package/dist/version.d.ts +22 -0
- package/dist/version.js +51 -0
- package/dist/version.js.map +1 -0
- package/dist/wake-support.d.ts +8 -0
- package/dist/wake-support.js +38 -0
- package/dist/wake-support.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli-update-contract.d.ts +0 -7
- package/dist/cli-update-contract.js +0 -10
- package/dist/cli-update-contract.js.map +0 -1
- package/dist/commands-hooks.d.ts +0 -160
- package/dist/commands-hooks.js +0 -1800
- package/dist/commands-hooks.js.map +0 -1
- package/dist/hooks.js.map +0 -1
package/dist/install-hooks.js
CHANGED
|
@@ -5,10 +5,11 @@ import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
|
5
5
|
import { atomicWriteFileSync } from './atomic-file.js';
|
|
6
6
|
import { withTargetFileLock } from './file-lock.js';
|
|
7
7
|
import { hookAdapterPath, hookHostPlatform, inspectHookAdapter, } from './hook-adapter.js';
|
|
8
|
-
import { opencodePluginPath, opencodePluginTarget } from './opencode-plugin.js';
|
|
9
|
-
import { OPENCLAW_EVENTS, openclawHasGlobalEvidence, openclawPluginPath, openclawPluginTarget, } from './openclaw-plugin.js';
|
|
8
|
+
import { OPENCODE_PLUGIN_FILENAME, opencodePluginPath, opencodePluginTarget, } from './opencode-plugin.js';
|
|
9
|
+
import { OPENCLAW_EVENTS, legacyOpenclawProjectPluginPath, openclawHasGlobalEvidence, openclawPluginPath, openclawPluginTarget, } from './openclaw-plugin.js';
|
|
10
10
|
import { HOOK_INSTALLABLE_HARNESSES } from './harnesses.js';
|
|
11
11
|
import { accountHome } from './platform.js';
|
|
12
|
+
import { sameLocalPath } from './local-path.js';
|
|
12
13
|
import { NON_ROUTING_STOP_TIMEOUT_SECONDS, QUESTION_STOP_TIMEOUT_SECONDS, } from './question-timing.js';
|
|
13
14
|
/**
|
|
14
15
|
* The three joints the OpenCode plugin hooks into, as (harness event, the
|
|
@@ -142,6 +143,10 @@ export function buildHookConfig(options) {
|
|
|
142
143
|
command: hookCommand(adapterPath, 'session-start', options.harness, commandOptions),
|
|
143
144
|
// Activation is local context only: no config, credentials, or network.
|
|
144
145
|
timeout: 5,
|
|
146
|
+
// Codex spills additionalContext above ~2,500 tokens to a preview
|
|
147
|
+
// file. SessionStart may emit up to 24,000 bytes, so pass the full
|
|
148
|
+
// context. Changing this field requires one `/hooks` re-approval.
|
|
149
|
+
...(options.harness === 'codex' ? { additionalContextLimit: 0 } : {}),
|
|
145
150
|
},
|
|
146
151
|
],
|
|
147
152
|
},
|
|
@@ -226,30 +231,48 @@ export function buildCursorHookConfig(options) {
|
|
|
226
231
|
};
|
|
227
232
|
}
|
|
228
233
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* questions pushed to is not a property of the repository.
|
|
234
|
+
* The one file each harness reads Notifai's lifecycle wiring from: the current
|
|
235
|
+
* User's account, in that harness's active home.
|
|
232
236
|
*/
|
|
233
|
-
export function settingsFile(harness,
|
|
237
|
+
export function settingsFile(harness, env = process.env, platform = process.platform) {
|
|
234
238
|
switch (harness) {
|
|
235
239
|
// OpenCode has no settings document to merge into — its adapter is a
|
|
236
240
|
// generated plugin module, so it owns a whole file instead.
|
|
237
241
|
case 'opencode':
|
|
238
|
-
return opencodePluginPath(
|
|
242
|
+
return opencodePluginPath(env, platform);
|
|
239
243
|
case 'openclaw':
|
|
240
|
-
return openclawPluginPath(
|
|
241
|
-
case 'cursor':
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
return openclawPluginPath(env, platform);
|
|
245
|
+
case 'cursor':
|
|
246
|
+
return path.join(harnessAccountHome(env, platform), '.cursor', 'hooks.json');
|
|
247
|
+
case 'claude-code':
|
|
248
|
+
return path.join(configHome(env, 'CLAUDE_CONFIG_DIR', '.claude', platform), 'settings.json');
|
|
249
|
+
case 'codex':
|
|
250
|
+
return inspectCodexLayer(codexMachineLayerPaths(env, platform)).writeTarget;
|
|
251
|
+
default:
|
|
252
|
+
return assertNeverHarness(harness);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Files an older build may have left inside this Project, which Notifai now
|
|
257
|
+
* only ever reads in order to remove.
|
|
258
|
+
*
|
|
259
|
+
* Nothing installs here any more. Every path is still enumerated because a
|
|
260
|
+
* leftover Project-scoped handler keeps firing beside the Machine one, and
|
|
261
|
+
* silently leaving it would be exactly the duplicate-routing defect dropping
|
|
262
|
+
* the second scope was meant to end.
|
|
263
|
+
*/
|
|
264
|
+
export function legacyProjectHookFiles(harness, cwd) {
|
|
265
|
+
switch (harness) {
|
|
266
|
+
case 'opencode':
|
|
267
|
+
return [path.join(cwd, '.opencode', 'plugins', OPENCODE_PLUGIN_FILENAME)];
|
|
268
|
+
case 'openclaw':
|
|
269
|
+
return [legacyOpenclawProjectPluginPath(cwd)];
|
|
270
|
+
case 'cursor':
|
|
271
|
+
return [path.join(cwd, '.cursor', 'hooks.json')];
|
|
247
272
|
case 'claude-code':
|
|
248
|
-
return
|
|
249
|
-
? path.join(configHome(env, 'CLAUDE_CONFIG_DIR', '.claude', platform), 'settings.json')
|
|
250
|
-
: path.join(cwd, '.claude', 'settings.local.json');
|
|
273
|
+
return [path.join(cwd, '.claude', 'settings.local.json')];
|
|
251
274
|
case 'codex':
|
|
252
|
-
return
|
|
275
|
+
return codexLegacyProjectLayers(cwd).flatMap((paths) => [paths.hooksJson, paths.configToml]);
|
|
253
276
|
default:
|
|
254
277
|
return assertNeverHarness(harness);
|
|
255
278
|
}
|
|
@@ -288,8 +311,7 @@ export function codexProjectRoot(cwd) {
|
|
|
288
311
|
try {
|
|
289
312
|
if (statSync(entry).isDirectory())
|
|
290
313
|
return path.dirname(entry);
|
|
291
|
-
|
|
292
|
-
return linked === null ? cwd : path.dirname(commonGitDir(linked));
|
|
314
|
+
return linkedWorktreeMainRoot(entry) ?? cwd;
|
|
293
315
|
}
|
|
294
316
|
catch {
|
|
295
317
|
// An unreadable or exotic checkout is not worth failing an install over;
|
|
@@ -297,29 +319,6 @@ export function codexProjectRoot(cwd) {
|
|
|
297
319
|
return cwd;
|
|
298
320
|
}
|
|
299
321
|
}
|
|
300
|
-
/**
|
|
301
|
-
* The `.codex` directory that has to exist for Codex to load project hooks at
|
|
302
|
-
* all, or null when nothing extra is needed.
|
|
303
|
-
*
|
|
304
|
-
* Codex splits the two halves of "which project am I in": it discovers the
|
|
305
|
-
* project layer by walking up from cwd for a `.codex` directory, but resolves
|
|
306
|
-
* the hook file inside that layer against the main repository. In an ordinary
|
|
307
|
-
* checkout both land on the same directory and the split is invisible. In a
|
|
308
|
-
* worktree they diverge, and writing only the main repository's file leaves it
|
|
309
|
-
* unread — Codex never looks, because nothing at or above cwd told it there was
|
|
310
|
-
* a project layer to load. Proven 2026-08-03: with cwd in a worktree, an *empty*
|
|
311
|
-
* `.codex` directory there was the difference between the main repository's
|
|
312
|
-
* handler running and nothing running.
|
|
313
|
-
*/
|
|
314
|
-
export function codexLayerDir(cwd) {
|
|
315
|
-
const entry = findGitEntry(cwd);
|
|
316
|
-
if (entry === null)
|
|
317
|
-
return null;
|
|
318
|
-
const worktreeRoot = path.dirname(entry);
|
|
319
|
-
if (path.resolve(worktreeRoot) === path.resolve(codexProjectRoot(cwd)))
|
|
320
|
-
return null;
|
|
321
|
-
return path.join(worktreeRoot, '.codex');
|
|
322
|
-
}
|
|
323
322
|
/** The nearest `.git` at or above `from`, or null outside a repository. */
|
|
324
323
|
function findGitEntry(from) {
|
|
325
324
|
let dir = path.resolve(from);
|
|
@@ -333,22 +332,58 @@ function findGitEntry(from) {
|
|
|
333
332
|
dir = parent;
|
|
334
333
|
}
|
|
335
334
|
}
|
|
336
|
-
/** `.git` in a linked worktree is a file naming that worktree's git directory. */
|
|
337
|
-
function worktreeGitDir(file) {
|
|
338
|
-
const match = /^gitdir:\s*(.+)$/m.exec(readFileSync(file, 'utf8'));
|
|
339
|
-
if (match === null)
|
|
340
|
-
return null;
|
|
341
|
-
return path.resolve(path.dirname(file), match[1].trim());
|
|
342
|
-
}
|
|
343
335
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
336
|
+
* Resolve the main checkout only from Git's reciprocal linked-worktree
|
|
337
|
+
* metadata. A repository can contain an arbitrary `.git` file, so its one-way
|
|
338
|
+
* `gitdir`/`commondir` claims are not authority to write outside the Project.
|
|
346
339
|
*/
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
340
|
+
function linkedWorktreeMainRoot(gitFile) {
|
|
341
|
+
if (!isOwnedRegularFile(gitFile))
|
|
342
|
+
return null;
|
|
343
|
+
const gitdirClaim = readPathMarker(gitFile, /^gitdir:[ \t]+([^\r\n]+)\r?\n?$/);
|
|
344
|
+
if (gitdirClaim === null)
|
|
345
|
+
return null;
|
|
346
|
+
const gitDir = path.resolve(path.dirname(gitFile), gitdirClaim);
|
|
347
|
+
if (!isOwnedDirectory(gitDir))
|
|
348
|
+
return null;
|
|
349
|
+
const commonClaim = readPathMarker(path.join(gitDir, 'commondir'), /^([^\r\n]+)\r?\n?$/);
|
|
350
|
+
const backlinkClaim = readPathMarker(path.join(gitDir, 'gitdir'), /^([^\r\n]+)\r?\n?$/);
|
|
351
|
+
if (commonClaim === null || backlinkClaim === null)
|
|
352
|
+
return null;
|
|
353
|
+
const commonDir = path.resolve(gitDir, commonClaim);
|
|
354
|
+
const backlink = path.resolve(gitDir, backlinkClaim);
|
|
355
|
+
const worktreesDir = path.join(commonDir, 'worktrees');
|
|
356
|
+
if (path.basename(commonDir).toLowerCase() !== '.git' ||
|
|
357
|
+
!isOwnedDirectory(commonDir) ||
|
|
358
|
+
!isOwnedDirectory(worktreesDir) ||
|
|
359
|
+
!sameLocalPath(path.dirname(realpathSync(gitDir)), realpathSync(worktreesDir)) ||
|
|
360
|
+
!sameLocalPath(backlink, gitFile)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const mainRoot = path.dirname(commonDir);
|
|
364
|
+
return isOwnedDirectory(mainRoot) ? mainRoot : null;
|
|
365
|
+
}
|
|
366
|
+
function readPathMarker(file, pattern) {
|
|
367
|
+
if (!isOwnedRegularFile(file))
|
|
368
|
+
return null;
|
|
369
|
+
const match = pattern.exec(readFileSync(file, 'utf8'));
|
|
370
|
+
const claim = match?.[1]?.trim();
|
|
371
|
+
return claim === undefined || claim === '' ? null : claim;
|
|
372
|
+
}
|
|
373
|
+
function isOwnedRegularFile(file) {
|
|
374
|
+
return isOwnedEntry(file, 'file');
|
|
375
|
+
}
|
|
376
|
+
function isOwnedDirectory(file) {
|
|
377
|
+
return isOwnedEntry(file, 'directory');
|
|
378
|
+
}
|
|
379
|
+
function isOwnedEntry(file, kind) {
|
|
380
|
+
const metadata = lstatSync(file);
|
|
381
|
+
if (metadata.isSymbolicLink())
|
|
382
|
+
return false;
|
|
383
|
+
if (kind === 'file' ? !metadata.isFile() : !metadata.isDirectory())
|
|
384
|
+
return false;
|
|
385
|
+
const uid = process.getuid?.();
|
|
386
|
+
return uid === undefined || metadata.uid === uid;
|
|
352
387
|
}
|
|
353
388
|
/**
|
|
354
389
|
* Harness-specific home variables relocate the whole active harness home.
|
|
@@ -379,54 +414,66 @@ export function configHome(env, variable, fallback, platform = process.platform)
|
|
|
379
414
|
export function codexGlobalDir(env = process.env, platform = process.platform) {
|
|
380
415
|
return configHome(env, 'CODEX_HOME', '.codex', platform);
|
|
381
416
|
}
|
|
382
|
-
|
|
383
|
-
export function codexLayerPaths(global, cwd, env = process.env, platform = process.platform) {
|
|
384
|
-
const dir = global
|
|
385
|
-
? codexGlobalDir(env, platform)
|
|
386
|
-
: path.join(codexProjectRoot(cwd), '.codex');
|
|
417
|
+
function codexLayerPathsIn(dir) {
|
|
387
418
|
return {
|
|
388
419
|
dir,
|
|
389
420
|
hooksJson: path.join(dir, 'hooks.json'),
|
|
390
421
|
configToml: path.join(dir, 'config.toml'),
|
|
391
422
|
};
|
|
392
423
|
}
|
|
424
|
+
/** The two files Codex will look at in the Machine layer Notifai installs into. */
|
|
425
|
+
export function codexMachineLayerPaths(env = process.env, platform = process.platform) {
|
|
426
|
+
return codexLayerPathsIn(codexGlobalDir(env, platform));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Project layers an older build may have written into, for removal only.
|
|
430
|
+
*
|
|
431
|
+
* Codex resolves a project hook file against the main repository, so a linked
|
|
432
|
+
* worktree has two candidate directories: the main checkout's `.codex`, which
|
|
433
|
+
* Codex reads, and the worktree's own, which older builds sometimes created and
|
|
434
|
+
* Codex never reads. Both are legacy residue and both are enumerated here.
|
|
435
|
+
*/
|
|
436
|
+
export function codexLegacyProjectLayers(cwd) {
|
|
437
|
+
const dirs = [path.join(codexProjectRoot(cwd), '.codex')];
|
|
438
|
+
const entry = findGitEntry(cwd);
|
|
439
|
+
if (entry !== null) {
|
|
440
|
+
const worktreeLayer = path.join(path.dirname(entry), '.codex');
|
|
441
|
+
if (!dirs.some((dir) => path.resolve(dir) === path.resolve(worktreeLayer))) {
|
|
442
|
+
dirs.push(worktreeLayer);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return dirs.map(codexLayerPathsIn);
|
|
446
|
+
}
|
|
393
447
|
/**
|
|
394
|
-
* Where Notifai writes in this Codex layer:
|
|
395
|
-
*
|
|
396
|
-
* `config.toml` for a hook-empty or already-mixed layer.
|
|
448
|
+
* Where Notifai writes in this Codex layer: `hooks.json`, unless the User's own
|
|
449
|
+
* hooks already live inline in the same layer's `config.toml`.
|
|
397
450
|
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
451
|
+
* `config.toml` is the User's whole Codex configuration and Codex rewrites it
|
|
452
|
+
* itself for `[hooks.state]`; keeping Notifai out of it bounds the blast radius
|
|
453
|
+
* to hooks, removes a second writer racing those trust records, and replaces
|
|
454
|
+
* TOML splicing with the same JSON merge Claude Code uses. Joining an inline
|
|
455
|
+
* layer that already has the User's handlers is the one exception, and it
|
|
456
|
+
* exists only so Codex never prints its dual-representation warning.
|
|
403
457
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
458
|
+
* Notifai's *own* inline handlers are not a reason to stay: the installer moves
|
|
459
|
+
* them to `hooks.json` and strips them from `config.toml`. `[hooks.state]` is
|
|
460
|
+
* the trust store rather than a hook definition, so it never counts as a
|
|
461
|
+
* representation and is preserved through every write.
|
|
408
462
|
*/
|
|
409
463
|
export function inspectCodexLayer(paths) {
|
|
410
464
|
const jsonDocument = tryLoadSettings(paths.hooksJson);
|
|
411
465
|
const tomlDocument = tryLoadSettings(paths.configToml);
|
|
412
|
-
const jsonEvents = hookEventNames(jsonDocument?.hooks);
|
|
413
|
-
const tomlEvents = hookEventNames(tomlDocument?.hooks);
|
|
414
|
-
const ourJsonEvents = ourHandlerEvents(jsonDocument);
|
|
415
466
|
const ourTomlEvents = ourHandlerEvents(tomlDocument);
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
: ourTomlEvents.length > 0
|
|
419
|
-
? paths.configToml
|
|
420
|
-
: jsonEvents.length > 0 && tomlEvents.length === 0
|
|
421
|
-
? paths.hooksJson
|
|
422
|
-
: paths.configToml;
|
|
467
|
+
const foreignTomlHooks = tomlDocument !== null &&
|
|
468
|
+
locateAllHandlers(tomlDocument).some((handler) => !isNotifaiCommand(handler.command));
|
|
423
469
|
return {
|
|
424
470
|
paths,
|
|
425
|
-
jsonEvents,
|
|
426
|
-
tomlEvents,
|
|
427
|
-
ourJsonEvents,
|
|
471
|
+
jsonEvents: hookEventNames(jsonDocument?.hooks),
|
|
472
|
+
tomlEvents: hookEventNames(tomlDocument?.hooks),
|
|
473
|
+
ourJsonEvents: ourHandlerEvents(jsonDocument),
|
|
428
474
|
ourTomlEvents,
|
|
429
|
-
|
|
475
|
+
foreignTomlHooks,
|
|
476
|
+
writeTarget: foreignTomlHooks ? paths.configToml : paths.hooksJson,
|
|
430
477
|
};
|
|
431
478
|
}
|
|
432
479
|
/**
|
|
@@ -436,15 +483,15 @@ export function inspectCodexLayer(paths) {
|
|
|
436
483
|
export function withCodexLayerTransaction(paths, action) {
|
|
437
484
|
return withTargetFileLock(paths.configToml, () => action(inspectCodexLayer(paths)));
|
|
438
485
|
}
|
|
439
|
-
/** Every file
|
|
440
|
-
export function
|
|
486
|
+
/** Every Machine-layer file this harness might already be reading hooks from. */
|
|
487
|
+
export function machineHookFiles(harness, env = process.env, platform = process.platform) {
|
|
441
488
|
if (harness !== 'codex')
|
|
442
|
-
return [settingsFile(harness,
|
|
443
|
-
const paths =
|
|
489
|
+
return [settingsFile(harness, env, platform)];
|
|
490
|
+
const paths = codexMachineLayerPaths(env, platform);
|
|
444
491
|
return [paths.hooksJson, paths.configToml];
|
|
445
492
|
}
|
|
446
493
|
/**
|
|
447
|
-
*
|
|
494
|
+
* The Machine layer with *Notifai* installed in both Codex representations.
|
|
448
495
|
*
|
|
449
496
|
* A layer that uses `hooks.json` and inline `[hooks]` at once is not by itself
|
|
450
497
|
* a Notifai fault: Codex supports both, loads both, runs every matching
|
|
@@ -460,23 +507,21 @@ export function hookDefinitionFiles(harness, global, cwd, env = process.env, pla
|
|
|
460
507
|
* the other one. Uninstall strips Notifai from both files, so the remedy is
|
|
461
508
|
* exact — and it never touches a foreign handler.
|
|
462
509
|
*/
|
|
463
|
-
export function codexRepresentationProblems(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
`Notifai hooks are installed in both ${layer.paths.hooksJson} and ${layer.paths.configToml}; ${consequence}. Run \`notifai hooks uninstall --harness codex${scope}\` and then \`notifai hooks install --harness codex${scope}\` to leave exactly one copy; foreign hooks in either file are left alone.`,
|
|
475
|
-
];
|
|
476
|
-
});
|
|
510
|
+
export function codexRepresentationProblems(env = process.env, platform = process.platform) {
|
|
511
|
+
const layer = inspectCodexLayer(codexMachineLayerPaths(env, platform));
|
|
512
|
+
if (layer.ourJsonEvents.length === 0 || layer.ourTomlEvents.length === 0)
|
|
513
|
+
return [];
|
|
514
|
+
const doubled = layer.ourJsonEvents.filter((event) => layer.ourTomlEvents.includes(event));
|
|
515
|
+
const consequence = doubled.length > 0
|
|
516
|
+
? `Codex runs every matching handler, so this layer notifies twice per turn for ${doubled.join(', ')}`
|
|
517
|
+
: `Codex runs both files, so this layer's Notifai handlers are split between them (${[...new Set([...layer.ourJsonEvents, ...layer.ourTomlEvents])].join(', ')}) and the next install refreshes only one file`;
|
|
518
|
+
return [
|
|
519
|
+
`Notifai hooks are installed in both ${layer.paths.hooksJson} and ${layer.paths.configToml}; ${consequence}. Run \`notifai hooks uninstall --harness codex\` and then \`notifai hooks install --harness codex\` to leave exactly one copy; foreign hooks in either file are left alone.`,
|
|
520
|
+
];
|
|
477
521
|
}
|
|
478
522
|
/**
|
|
479
|
-
*
|
|
523
|
+
* The Machine layer where Notifai and someone else each own one Codex
|
|
524
|
+
* representation.
|
|
480
525
|
*
|
|
481
526
|
* This is not a fault and never fails a check. Codex loads `hooks.json` and
|
|
482
527
|
* inline `[hooks]` together and runs every matching handler, so both sets fire
|
|
@@ -485,25 +530,23 @@ export function codexRepresentationProblems(cwd, env = process.env, platform = p
|
|
|
485
530
|
* Notifai's, which is not, and that Notifai will not touch the one it does not
|
|
486
531
|
* own. Saying nothing here is what made the warning look like Notifai's bug.
|
|
487
532
|
*/
|
|
488
|
-
export function codexCoexistenceNotes(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
];
|
|
506
|
-
});
|
|
533
|
+
export function codexCoexistenceNotes(env = process.env, platform = process.platform) {
|
|
534
|
+
const layer = inspectCodexLayer(codexMachineLayerPaths(env, platform));
|
|
535
|
+
const inJson = layer.ourJsonEvents.length > 0;
|
|
536
|
+
const inToml = layer.ourTomlEvents.length > 0;
|
|
537
|
+
if (inJson === inToml)
|
|
538
|
+
return [];
|
|
539
|
+
const ours = inJson ? layer.paths.hooksJson : layer.paths.configToml;
|
|
540
|
+
const theirs = inJson ? layer.paths.configToml : layer.paths.hooksJson;
|
|
541
|
+
const theirEvents = inJson ? layer.tomlEvents : layer.jsonEvents;
|
|
542
|
+
if (theirEvents.length === 0)
|
|
543
|
+
return [];
|
|
544
|
+
const stop = theirEvents.includes('Stop')
|
|
545
|
+
? " That file's Stop handler can end a turn before Notifai's answer arrives, because Codex lets any Stop handler stop continuation."
|
|
546
|
+
: '';
|
|
547
|
+
return [
|
|
548
|
+
`Notifai's Codex hooks are in ${ours}. ${theirs} defines hooks Notifai does not own (${theirEvents.join(', ')}) and Notifai will not modify it. Codex loads both files and runs every matching handler, so each set fires exactly once; its "prefer one representation per layer" startup warning is reporting that, not a Notifai fault.${stop}`,
|
|
549
|
+
];
|
|
507
550
|
}
|
|
508
551
|
/**
|
|
509
552
|
* What `CODEX_HOME` is doing to this shell, when it is doing anything.
|
|
@@ -679,7 +722,16 @@ export function removeCursorHooks(existing, scriptPath) {
|
|
|
679
722
|
if (foreign.length > 0)
|
|
680
723
|
hooks[event] = foreign;
|
|
681
724
|
}
|
|
682
|
-
|
|
725
|
+
const document = { ...existing };
|
|
726
|
+
if (Object.keys(hooks).length > 0)
|
|
727
|
+
document.hooks = hooks;
|
|
728
|
+
else
|
|
729
|
+
delete document.hooks;
|
|
730
|
+
// `version` is Cursor's schema marker, which Notifai writes only because it
|
|
731
|
+
// is writing hooks. Alone it is residue, not settings.
|
|
732
|
+
if (Object.keys(document).length === 1 && document.version !== undefined)
|
|
733
|
+
delete document.version;
|
|
734
|
+
return { document, added: [], replaced, removed: [] };
|
|
683
735
|
}
|
|
684
736
|
function readSettings(file) {
|
|
685
737
|
if (!existsSync(file))
|
|
@@ -794,7 +846,10 @@ export function applyPlan(file, document) {
|
|
|
794
846
|
const body = isTomlSettingsPath(file)
|
|
795
847
|
? tomlBody(file, document)
|
|
796
848
|
: `${JSON.stringify(document, null, 2)}\n`;
|
|
797
|
-
|
|
849
|
+
// A document with nothing left in it is a file Notifai created and has just
|
|
850
|
+
// emptied. Keeping it would leave `{}` behind as the visible residue of an
|
|
851
|
+
// uninstall; a file that still holds anything of the User's is never empty.
|
|
852
|
+
if ((!isTomlSettingsPath(file) && isEmptyJsonDocument(body)) ||
|
|
798
853
|
(isTomlSettingsPath(file) && body.trim() === '')) {
|
|
799
854
|
if (existsSync(file))
|
|
800
855
|
rmSync(file, { force: true });
|
|
@@ -982,10 +1037,16 @@ function canonicalValue(value) {
|
|
|
982
1037
|
}
|
|
983
1038
|
/**
|
|
984
1039
|
* Best-effort mirror of the identity current Codex builds compare with
|
|
985
|
-
* `hooks.state.*.trusted_hash`.
|
|
986
|
-
*
|
|
987
|
-
*
|
|
1040
|
+
* `hooks.state.*.trusted_hash`. Codex hashes `type`, `command`, `timeout`,
|
|
1041
|
+
* `async`, `statusMessage`, and `additionalContextLimit` (omitting the 2,500
|
|
1042
|
+
* default). This persisted format is diagnostic evidence, not an API: Codex's
|
|
1043
|
+
* `/hooks` UI remains authoritative and Notifai never writes the trust store.
|
|
988
1044
|
*/
|
|
1045
|
+
/**
|
|
1046
|
+
* Codex's default additionalContext spill threshold, in approximate tokens.
|
|
1047
|
+
* Codex strips this value before hashing, so an explicit 2,500 matches omit.
|
|
1048
|
+
*/
|
|
1049
|
+
const CODEX_DEFAULT_ADDITIONAL_CONTEXT_LIMIT = 2_500;
|
|
989
1050
|
export function codexHookIdentityHash(handler) {
|
|
990
1051
|
const command = {
|
|
991
1052
|
type: 'command',
|
|
@@ -993,6 +1054,10 @@ export function codexHookIdentityHash(handler) {
|
|
|
993
1054
|
timeout: Math.max(1, handler.timeout ?? 600),
|
|
994
1055
|
async: handler.async ?? false,
|
|
995
1056
|
...(handler.statusMessage === undefined ? {} : { statusMessage: handler.statusMessage }),
|
|
1057
|
+
...(handler.additionalContextLimit === undefined
|
|
1058
|
+
|| handler.additionalContextLimit === CODEX_DEFAULT_ADDITIONAL_CONTEXT_LIMIT
|
|
1059
|
+
? {}
|
|
1060
|
+
: { additionalContextLimit: handler.additionalContextLimit }),
|
|
996
1061
|
};
|
|
997
1062
|
const normalized = canonicalValue({
|
|
998
1063
|
event_name: codexEventName(handler.event),
|
|
@@ -1096,92 +1161,105 @@ export function codexTrustProblems(installations, env = process.env) {
|
|
|
1096
1161
|
function isNotifaiCommand(command) {
|
|
1097
1162
|
return / hook (session-start|subagent-start|activation-stop|user-prompt-submit|stop|session-end)\b/.test(command);
|
|
1098
1163
|
}
|
|
1099
|
-
/**
|
|
1100
|
-
|
|
1164
|
+
/**
|
|
1165
|
+
* The one Machine installation each harness would read a Notifai handler from.
|
|
1166
|
+
*/
|
|
1167
|
+
export function findInstallations(env = process.env, adapterHome, platform = process.platform) {
|
|
1168
|
+
return HOOK_INSTALLABLE_HARNESSES.flatMap((harness) => collectInstallations(harness, machineHookFiles(harness, env, platform), adapterHome, platform));
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Notifai handlers a previous build left in Project-scoped files.
|
|
1172
|
+
*
|
|
1173
|
+
* These are never an install target and never a fallback: they are residue to
|
|
1174
|
+
* report and remove once the Machine installation is proven current. Foreign
|
|
1175
|
+
* handlers sharing those files are not returned and are never touched.
|
|
1176
|
+
*/
|
|
1177
|
+
export function findLegacyProjectInstallations(cwd, env = process.env, adapterHome, platform = process.platform) {
|
|
1178
|
+
const machine = new Set(HOOK_INSTALLABLE_HARNESSES.flatMap((harness) => machineHookFiles(harness, env, platform).map((file) => path.resolve(file))));
|
|
1179
|
+
return HOOK_INSTALLABLE_HARNESSES.flatMap((harness) => collectInstallations(harness,
|
|
1180
|
+
// A Project checkout that happens to be the harness home would otherwise
|
|
1181
|
+
// report the Machine installation as legacy residue and delete it.
|
|
1182
|
+
legacyProjectHookFiles(harness, cwd).filter((file) => !machine.has(path.resolve(file))), adapterHome, platform));
|
|
1183
|
+
}
|
|
1184
|
+
function collectInstallations(harness, files, adapterHome, platform) {
|
|
1101
1185
|
const nodePath = inspectHookAdapter(adapterHome, platform).target?.execPath;
|
|
1102
1186
|
const commandOptions = {
|
|
1103
1187
|
platform,
|
|
1104
1188
|
...(nodePath === undefined ? {} : { nodePath }),
|
|
1105
1189
|
};
|
|
1106
1190
|
const found = [];
|
|
1107
|
-
for (const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
catch {
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1124
|
-
const target = harness === 'openclaw' ? openclawPluginTarget(source) : opencodePluginTarget(source);
|
|
1125
|
-
if (target === null)
|
|
1126
|
-
continue;
|
|
1127
|
-
const label = harness === 'openclaw' ? 'OpenClaw' : 'OpenCode';
|
|
1128
|
-
const problems = [
|
|
1129
|
-
...(!target.current
|
|
1130
|
-
? [`obsolete ${label} event wiring; rerun \`notifai hooks install --harness ${harness}\``]
|
|
1131
|
-
: []),
|
|
1132
|
-
...(target.adapter !== hookAdapterPath(adapterHome)
|
|
1133
|
-
? [
|
|
1134
|
-
`${label} still names a mutable CLI or runtime path; rerun \`notifai hooks install --harness ${harness}\``,
|
|
1135
|
-
]
|
|
1136
|
-
: []),
|
|
1137
|
-
];
|
|
1138
|
-
const events = harness === 'openclaw' ? OPENCLAW_EVENTS : OPENCODE_EVENTS;
|
|
1139
|
-
found.push({
|
|
1140
|
-
harness,
|
|
1141
|
-
file,
|
|
1142
|
-
global,
|
|
1143
|
-
...(problems.length > 0 ? { problems } : {}),
|
|
1144
|
-
handlers: events.map(([event, hookEvent]) => ({
|
|
1145
|
-
event,
|
|
1146
|
-
groupIndex: 0,
|
|
1147
|
-
handlerIndex: 0,
|
|
1148
|
-
command: hookCommand(target.adapter, hookEvent, harness, {
|
|
1149
|
-
...commandOptions,
|
|
1150
|
-
...(target.nodePath === undefined ? {} : { nodePath: target.nodePath }),
|
|
1151
|
-
}),
|
|
1152
|
-
...(target.timeoutSeconds === undefined ? {} : { timeout: target.timeoutSeconds }),
|
|
1153
|
-
})),
|
|
1154
|
-
});
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
if (harness === 'cursor') {
|
|
1158
|
-
let document;
|
|
1159
|
-
try {
|
|
1160
|
-
document = readCursorSettings(file);
|
|
1161
|
-
}
|
|
1162
|
-
catch {
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
const handlers = locateCursorHandlers(document);
|
|
1166
|
-
if (handlers.length > 0) {
|
|
1167
|
-
const problems = harnessMarkerProblems(harness, handlers, adapterHome, commandOptions);
|
|
1168
|
-
found.push({ harness, file, global, handlers, ...(problems.length > 0 ? { problems } : {}) });
|
|
1169
|
-
}
|
|
1170
|
-
continue;
|
|
1171
|
-
}
|
|
1172
|
-
let document;
|
|
1173
|
-
try {
|
|
1174
|
-
document = loadSettings(file);
|
|
1175
|
-
}
|
|
1176
|
-
catch {
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
|
-
const handlers = locateHandlers(document);
|
|
1180
|
-
if (handlers.length > 0) {
|
|
1181
|
-
const problems = harnessMarkerProblems(harness, handlers, adapterHome, commandOptions);
|
|
1182
|
-
found.push({ harness, file, global, handlers, ...(problems.length > 0 ? { problems } : {}) });
|
|
1183
|
-
}
|
|
1191
|
+
for (const file of files) {
|
|
1192
|
+
if (!existsSync(file))
|
|
1193
|
+
continue;
|
|
1194
|
+
// OpenCode's adapter is a plugin module, not a settings document, so it
|
|
1195
|
+
// is reported as one installation covering all three events rather than
|
|
1196
|
+
// parsed for handlers.
|
|
1197
|
+
if (harness === 'opencode' || harness === 'openclaw') {
|
|
1198
|
+
let source;
|
|
1199
|
+
try {
|
|
1200
|
+
source = readOwnedRegularFile(file);
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
continue;
|
|
1184
1204
|
}
|
|
1205
|
+
const target = harness === 'openclaw' ? openclawPluginTarget(source) : opencodePluginTarget(source);
|
|
1206
|
+
if (target === null)
|
|
1207
|
+
continue;
|
|
1208
|
+
const label = harness === 'openclaw' ? 'OpenClaw' : 'OpenCode';
|
|
1209
|
+
const problems = [
|
|
1210
|
+
...(!target.current
|
|
1211
|
+
? [`obsolete ${label} event wiring; rerun \`notifai hooks install --harness ${harness}\``]
|
|
1212
|
+
: []),
|
|
1213
|
+
...(target.adapter !== hookAdapterPath(adapterHome)
|
|
1214
|
+
? [
|
|
1215
|
+
`${label} still names a mutable CLI or runtime path; rerun \`notifai hooks install --harness ${harness}\``,
|
|
1216
|
+
]
|
|
1217
|
+
: []),
|
|
1218
|
+
];
|
|
1219
|
+
const events = harness === 'openclaw' ? OPENCLAW_EVENTS : OPENCODE_EVENTS;
|
|
1220
|
+
found.push({
|
|
1221
|
+
harness,
|
|
1222
|
+
file,
|
|
1223
|
+
...(problems.length > 0 ? { problems } : {}),
|
|
1224
|
+
handlers: events.map(([event, hookEvent]) => ({
|
|
1225
|
+
event,
|
|
1226
|
+
groupIndex: 0,
|
|
1227
|
+
handlerIndex: 0,
|
|
1228
|
+
command: hookCommand(target.adapter, hookEvent, harness, {
|
|
1229
|
+
...commandOptions,
|
|
1230
|
+
...(target.nodePath === undefined ? {} : { nodePath: target.nodePath }),
|
|
1231
|
+
}),
|
|
1232
|
+
...(target.timeoutSeconds === undefined ? {} : { timeout: target.timeoutSeconds }),
|
|
1233
|
+
})),
|
|
1234
|
+
});
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (harness === 'cursor') {
|
|
1238
|
+
let document;
|
|
1239
|
+
try {
|
|
1240
|
+
document = readCursorSettings(file);
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const handlers = locateCursorHandlers(document);
|
|
1246
|
+
if (handlers.length > 0) {
|
|
1247
|
+
const problems = harnessMarkerProblems(harness, handlers, adapterHome, commandOptions);
|
|
1248
|
+
found.push({ harness, file, handlers, ...(problems.length > 0 ? { problems } : {}) });
|
|
1249
|
+
}
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
let document;
|
|
1253
|
+
try {
|
|
1254
|
+
document = loadSettings(file);
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const handlers = locateHandlers(document);
|
|
1260
|
+
if (handlers.length > 0) {
|
|
1261
|
+
const problems = harnessMarkerProblems(harness, handlers, adapterHome, commandOptions);
|
|
1262
|
+
found.push({ harness, file, handlers, ...(problems.length > 0 ? { problems } : {}) });
|
|
1185
1263
|
}
|
|
1186
1264
|
}
|
|
1187
1265
|
return found;
|
|
@@ -1231,6 +1309,9 @@ function locateAllHandlers(document) {
|
|
|
1231
1309
|
...(handler.timeout === undefined ? {} : { timeout: handler.timeout }),
|
|
1232
1310
|
...(handler.async === undefined ? {} : { async: handler.async }),
|
|
1233
1311
|
...(handler.statusMessage === undefined ? {} : { statusMessage: handler.statusMessage }),
|
|
1312
|
+
...(typeof handler.additionalContextLimit === 'number'
|
|
1313
|
+
? { additionalContextLimit: handler.additionalContextLimit }
|
|
1314
|
+
: {}),
|
|
1234
1315
|
});
|
|
1235
1316
|
});
|
|
1236
1317
|
});
|