@raidiant/notifai 10.1.0 → 10.1.2

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 (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. package/tsconfig.json +0 -10
@@ -1,410 +0,0 @@
1
- import {
2
- chmodSync,
3
- existsSync,
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- statSync,
8
- } from 'node:fs'
9
- import os from 'node:os'
10
- import path from 'node:path'
11
- import { atomicWriteFileSync } from './atomic-file.js'
12
- import { withTargetFileLock } from './file-lock.js'
13
-
14
- const ADAPTER_MARKER = '# notifai managed hook adapter'
15
- const WIN32_ADAPTER_MARKER = '// notifai managed hook adapter'
16
- const ADAPTER_VERSION = 2
17
-
18
- export type HookHostPlatform = 'posix' | 'win32'
19
-
20
- export interface HookAdapterFileTarget {
21
- kind?: 'file'
22
- execPath: string
23
- scriptPath: string
24
- }
25
-
26
- export interface HookAdapterNpxTarget {
27
- kind: 'npx'
28
- execPath: string
29
- npmCli: string
30
- spec: string
31
- }
32
-
33
- export type HookAdapterTarget = HookAdapterFileTarget | HookAdapterNpxTarget
34
-
35
- export function isNpxAdapterTarget(target: HookAdapterTarget): target is HookAdapterNpxTarget {
36
- return target.kind === 'npx'
37
- }
38
-
39
- export interface HookAdapterInspection {
40
- path: string
41
- target: HookAdapterTarget | null
42
- problems: string[]
43
- }
44
-
45
- export function hookHostPlatform(
46
- platform: NodeJS.Platform | HookHostPlatform = process.platform,
47
- ): HookHostPlatform {
48
- return platform === 'win32' ? 'win32' : 'posix'
49
- }
50
-
51
- /**
52
- * One user-level identity for every harness and every project/global install.
53
- *
54
- * The pathname is deliberately outside XDG configuration/state routing. XDG
55
- * overrides routinely differ between shells, package managers, and harness
56
- * processes; allowing one to select this path would put mutable environment
57
- * back into every trusted definition. Only the user's home boundary selects
58
- * it; CLI, Node, package-manager, checkout, and preference paths stay behind
59
- * it and may change without changing hook identity.
60
- */
61
- export function hookAdapterPath(homeDir: string = os.userInfo().homedir): string {
62
- return path.join(homeDir, '.notifai', 'bin', 'hook-adapter')
63
- }
64
-
65
- /** Install or repair the stable adapter and atomically retarget its implementation. */
66
- export function installHookAdapter(
67
- target: HookAdapterTarget,
68
- homeDir?: string,
69
- platform: NodeJS.Platform | HookHostPlatform = process.platform,
70
- ): { path: string; changed: boolean } {
71
- const host = hookHostPlatform(platform)
72
- assertUsableTarget(target, host)
73
- const file = hookAdapterPath(homeDir)
74
- ensureManagedDirectories(homeDir ?? os.userInfo().homedir, host)
75
- const source = hookAdapterSource(target, host)
76
- return withTargetFileLock(file, () => {
77
- const existing = existsSync(file) ? readSafeManagedFile(file) : null
78
- const modeDrift =
79
- host === 'posix' && existing !== null && (existing.mode & 0o777) !== 0o700
80
- const changed = existing === null || existing.contents !== source || modeDrift
81
- if (changed) {
82
- atomicWriteFileSync(file, source, {
83
- mode: host === 'posix' ? 0o700 : 0o600,
84
- preserveMode: false,
85
- requireCurrentUserOwner: true,
86
- })
87
- }
88
- return { path: file, changed }
89
- })
90
- }
91
-
92
- function assertUsableTarget(target: HookAdapterTarget, host: HookHostPlatform): void {
93
- let exec
94
- try {
95
- exec = statSync(target.execPath)
96
- } catch {
97
- throw new Error(`Hook adapter runtime ${target.execPath} does not exist.`)
98
- }
99
- if (!exec.isFile() || (host === 'posix' && (exec.mode & 0o111) === 0)) {
100
- throw new Error(`Hook adapter runtime ${target.execPath} is not executable.`)
101
- }
102
- if (isNpxAdapterTarget(target)) {
103
- if (target.spec.trim() === '') {
104
- throw new Error('Hook adapter npx spec is empty.')
105
- }
106
- try {
107
- if (!statSync(target.npmCli).isFile()) {
108
- throw new Error(`Hook adapter npm CLI ${target.npmCli} is not a regular file.`)
109
- }
110
- } catch (err) {
111
- if (err instanceof Error && err.message.startsWith('Hook adapter npm CLI ')) throw err
112
- throw new Error(`Hook adapter npm CLI ${target.npmCli} does not exist.`)
113
- }
114
- return
115
- }
116
- try {
117
- if (!statSync(target.scriptPath).isFile()) {
118
- throw new Error(`Hook adapter CLI ${target.scriptPath} is not a regular file.`)
119
- }
120
- } catch (err) {
121
- if (err instanceof Error && err.message.startsWith('Hook adapter CLI ')) throw err
122
- throw new Error(`Hook adapter CLI ${target.scriptPath} does not exist.`)
123
- }
124
- }
125
-
126
- /** Inspect the shared adapter without executing either it or its registered CLI. */
127
- export function inspectHookAdapter(
128
- homeDir?: string,
129
- platform: NodeJS.Platform | HookHostPlatform = process.platform,
130
- ): HookAdapterInspection {
131
- const host = hookHostPlatform(platform)
132
- const file = hookAdapterPath(homeDir)
133
- if (!existsSync(file)) {
134
- return { path: file, target: null, problems: [`${file} is missing; rerun \`notifai hooks install\``] }
135
- }
136
-
137
- let stat
138
- try {
139
- stat = lstatSync(file)
140
- } catch (err) {
141
- return { path: file, target: null, problems: [String(err)] }
142
- }
143
- if (stat.isSymbolicLink() || !stat.isFile()) {
144
- return {
145
- path: file,
146
- target: null,
147
- problems: [`${file} is not a regular file; refusing the adapter`],
148
- }
149
- }
150
-
151
- const problems: string[] = []
152
- if (host === 'posix') {
153
- const uid = typeof process.getuid === 'function' ? process.getuid() : undefined
154
- if (uid !== undefined && stat.uid !== uid) {
155
- problems.push(`${file} is owned by uid ${stat.uid}, not the current user`)
156
- }
157
- if ((stat.mode & 0o077) !== 0 || (stat.mode & 0o700) !== 0o700) {
158
- problems.push(`${file} permissions are ${(stat.mode & 0o777).toString(8)}, expected 700`)
159
- }
160
- }
161
-
162
- const contents = readFileSync(file, 'utf8')
163
- const format = adapterFormat(contents)
164
- if (format === null) {
165
- problems.push(`${file} has no managed adapter marker`)
166
- } else if (format !== host) {
167
- problems.push(`${file} is not a ${host} managed adapter; rerun \`notifai hooks install\``)
168
- }
169
- const version = Number(meta(contents, 'adapter-version') ?? Number.NaN)
170
- if (version !== ADAPTER_VERSION) problems.push(`${file} uses obsolete adapter version ${version}`)
171
- const target = parseTarget(contents)
172
- if (target === null) {
173
- problems.push(`${file} has malformed target metadata`)
174
- } else {
175
- try {
176
- const exec = statSync(target.execPath)
177
- if (!exec.isFile() || (host === 'posix' && (exec.mode & 0o111) === 0)) {
178
- problems.push(`registered runtime ${target.execPath} is not executable`)
179
- }
180
- } catch {
181
- problems.push(`registered runtime ${target.execPath} is missing`)
182
- }
183
- if (isNpxAdapterTarget(target)) {
184
- if (target.spec.trim() === '') problems.push('registered npx spec is empty')
185
- try {
186
- if (!statSync(target.npmCli).isFile()) {
187
- problems.push(`registered npm CLI ${target.npmCli} is not a regular file`)
188
- }
189
- } catch {
190
- problems.push(`registered npm CLI ${target.npmCli} is missing`)
191
- }
192
- } else {
193
- try {
194
- if (!statSync(target.scriptPath).isFile()) {
195
- problems.push(`registered CLI ${target.scriptPath} is not a regular file`)
196
- }
197
- } catch {
198
- problems.push(`registered CLI ${target.scriptPath} is missing`)
199
- }
200
- }
201
- }
202
- return { path: file, target, problems }
203
- }
204
-
205
- /** Generate the managed adapter source for one host. Exported so tests can pin bytes. */
206
- export function hookAdapterSource(
207
- target: HookAdapterTarget,
208
- platform: NodeJS.Platform | HookHostPlatform = process.platform,
209
- ): string {
210
- return hookHostPlatform(platform) === 'win32'
211
- ? win32HookAdapterSource(target)
212
- : posixHookAdapterSource(target)
213
- }
214
-
215
- /**
216
- * Two rungs, and the difference between them is the whole safety argument.
217
- *
218
- * The registered runtime is a resolved path, so every node version manager
219
- * makes it version-scoped: a Homebrew upgrade renames its Cellar directory, an
220
- * nvm version gets pruned, and the pinned interpreter stops existing. The npm
221
- * shim this adapter wraps already survives that — `main.ts` starts with
222
- * `#!/usr/bin/env node` — so pinning the interpreter here was stricter than the
223
- * binary it stands in front of, and the strictness bought nothing.
224
- *
225
- * So PATH may supply the *interpreter*, and never the *code*: both rungs keep
226
- * the registered script path, and no rung resolves `notifai` from PATH. Which
227
- * Notifai build a trusted hook definition runs stays a property of the
228
- * installation, not of whatever environment the harness happens to hand us.
229
- */
230
- function posixHookAdapterSource(target: HookAdapterTarget): string {
231
- if (isNpxAdapterTarget(target)) {
232
- return `#!/bin/sh
233
- ${ADAPTER_MARKER}
234
- # adapter-version: ${ADAPTER_VERSION}
235
- # target-kind: npx
236
- # target-exec-json: ${JSON.stringify(target.execPath)}
237
- # target-npm-cli-json: ${JSON.stringify(target.npmCli)}
238
- # target-spec-json: ${JSON.stringify(target.spec)}
239
- set -eu
240
-
241
- NOTIFAI_HOOK_SOURCE_PID=$PPID
242
- export NOTIFAI_HOOK_SOURCE_PID
243
-
244
- registered_exec=${quote(target.execPath)}
245
- registered_npm_cli=${quote(target.npmCli)}
246
- registered_spec=${quote(target.spec)}
247
-
248
- if [ -x "$registered_exec" ] && [ -f "$registered_npm_cli" ]; then
249
- exec "$registered_exec" "$registered_npm_cli" exec --yes --package "$registered_spec" -- notifai "$@"
250
- fi
251
-
252
- if [ -f "$registered_npm_cli" ] && command -v node >/dev/null 2>&1; then
253
- exec node "$registered_npm_cli" exec --yes --package "$registered_spec" -- notifai "$@"
254
- fi
255
-
256
- printf '%s\n' 'Notifai hook adapter target is stale; run notifai hooks install.' >&2
257
- exit 127
258
- `
259
- }
260
- return `#!/bin/sh
261
- ${ADAPTER_MARKER}
262
- # adapter-version: ${ADAPTER_VERSION}
263
- # target-exec-json: ${JSON.stringify(target.execPath)}
264
- # target-script-json: ${JSON.stringify(target.scriptPath)}
265
- set -eu
266
-
267
- NOTIFAI_HOOK_SOURCE_PID=$PPID
268
- export NOTIFAI_HOOK_SOURCE_PID
269
-
270
- registered_exec=${quote(target.execPath)}
271
- registered_script=${quote(target.scriptPath)}
272
-
273
- if [ -x "$registered_exec" ] && [ -f "$registered_script" ]; then
274
- exec "$registered_exec" "$registered_script" "$@"
275
- fi
276
-
277
- if [ -f "$registered_script" ] && command -v node >/dev/null 2>&1; then
278
- exec node "$registered_script" "$@"
279
- fi
280
-
281
- printf '%s\n' 'Notifai hook adapter target is stale; run notifai hooks install.' >&2
282
- exit 127
283
- `
284
- }
285
-
286
- function win32HookAdapterSource(target: HookAdapterTarget): string {
287
- const header = isNpxAdapterTarget(target)
288
- ? `${WIN32_ADAPTER_MARKER}
289
- // adapter-version: ${ADAPTER_VERSION}
290
- // target-kind: npx
291
- // target-exec-json: ${JSON.stringify(target.execPath)}
292
- // target-npm-cli-json: ${JSON.stringify(target.npmCli)}
293
- // target-spec-json: ${JSON.stringify(target.spec)}
294
- `
295
- : `${WIN32_ADAPTER_MARKER}
296
- // adapter-version: ${ADAPTER_VERSION}
297
- // target-exec-json: ${JSON.stringify(target.execPath)}
298
- // target-script-json: ${JSON.stringify(target.scriptPath)}
299
- `
300
- const argv = isNpxAdapterTarget(target)
301
- ? `[${JSON.stringify(target.npmCli)}, "exec", "--yes", "--package", ${JSON.stringify(target.spec)}, "--", "notifai", ...process.argv.slice(2)]`
302
- : `[${JSON.stringify(target.scriptPath)}, ...process.argv.slice(2)]`
303
- const companion = isNpxAdapterTarget(target)
304
- ? JSON.stringify(target.npmCli)
305
- : JSON.stringify(target.scriptPath)
306
- return `${header}"use strict";
307
- const { spawnSync } = require("node:child_process");
308
- const { statSync } = require("node:fs");
309
- const registeredExec = ${JSON.stringify(target.execPath)};
310
- const registeredCompanion = ${companion};
311
-
312
- function isFile(file) {
313
- try {
314
- return statSync(file).isFile();
315
- } catch {
316
- return false;
317
- }
318
- }
319
-
320
- // The pinned runtime is preferred; the runtime already executing this shim is
321
- // the fallback. Only the interpreter is allowed to move — the code stays pinned.
322
- const runtime = isFile(registeredExec) ? registeredExec : process.execPath;
323
-
324
- if (isFile(registeredCompanion)) {
325
- const result = spawnSync(runtime, ${argv}, {
326
- stdio: "inherit",
327
- windowsHide: true,
328
- env: { ...process.env, NOTIFAI_HOOK_SOURCE_PID: String(process.ppid) },
329
- });
330
- if (result.error) {
331
- process.stderr.write("Notifai hook adapter target is stale; run notifai hooks install.\\n");
332
- process.exit(127);
333
- }
334
- process.exit(result.status === null ? 1 : result.status);
335
- }
336
-
337
- process.stderr.write("Notifai hook adapter target is stale; run notifai hooks install.\\n");
338
- process.exit(127);
339
- `
340
- }
341
-
342
- function adapterFormat(contents: string): HookHostPlatform | null {
343
- if (contents.startsWith(`#!/bin/sh\n${ADAPTER_MARKER}\n`)) return 'posix'
344
- if (contents.startsWith(`${WIN32_ADAPTER_MARKER}\n`)) return 'win32'
345
- return null
346
- }
347
-
348
- function meta(contents: string, key: string): string | undefined {
349
- return new RegExp(`^(?:#|//) ${key}: (.+)$`, 'm').exec(contents)?.[1]
350
- }
351
-
352
- function parseTarget(contents: string): HookAdapterTarget | null {
353
- const kind = meta(contents, 'target-kind')
354
- const exec = meta(contents, 'target-exec-json')
355
- if (exec === undefined) return null
356
- try {
357
- const execPath: unknown = JSON.parse(exec)
358
- if (typeof execPath !== 'string') return null
359
- if (kind === 'npx') {
360
- const npmCliRaw = meta(contents, 'target-npm-cli-json')
361
- const specRaw = meta(contents, 'target-spec-json')
362
- if (npmCliRaw === undefined || specRaw === undefined) return null
363
- const npmCli: unknown = JSON.parse(npmCliRaw)
364
- const spec: unknown = JSON.parse(specRaw)
365
- return typeof npmCli === 'string' && typeof spec === 'string'
366
- ? { kind: 'npx', execPath, npmCli, spec }
367
- : null
368
- }
369
- const script = meta(contents, 'target-script-json')
370
- if (script === undefined) return null
371
- const scriptPath: unknown = JSON.parse(script)
372
- return typeof scriptPath === 'string' ? { execPath, scriptPath } : null
373
- } catch {
374
- return null
375
- }
376
- }
377
-
378
- function quote(value: string): string {
379
- return `'${value.replaceAll("'", `'\\''`)}'`
380
- }
381
-
382
- function ensureManagedDirectories(homeDir: string, host: HookHostPlatform): void {
383
- if (!existsSync(homeDir)) mkdirSync(homeDir, { recursive: true, mode: 0o700 })
384
- for (const dir of [path.join(homeDir, '.notifai'), path.join(homeDir, '.notifai', 'bin')]) {
385
- if (!existsSync(dir)) mkdirSync(dir, { mode: 0o700 })
386
- const stat = lstatSync(dir)
387
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
388
- throw new Error(`${dir} is not a regular directory; refusing to install the hook adapter.`)
389
- }
390
- if (host === 'posix') {
391
- const uid = typeof process.getuid === 'function' ? process.getuid() : undefined
392
- if (uid !== undefined && stat.uid !== uid) {
393
- throw new Error(`${dir} is owned by uid ${stat.uid}, not the current user.`)
394
- }
395
- chmodSync(dir, 0o700)
396
- }
397
- }
398
- }
399
-
400
- function readSafeManagedFile(file: string): { contents: string; mode: number } {
401
- const stat = lstatSync(file)
402
- if (stat.isSymbolicLink() || !stat.isFile()) {
403
- throw new Error(`${file} is not a regular file; refusing to replace it.`)
404
- }
405
- const uid = typeof process.getuid === 'function' ? process.getuid() : undefined
406
- if (uid !== undefined && stat.uid !== uid) {
407
- throw new Error(`${file} is owned by uid ${stat.uid}, not the current user.`)
408
- }
409
- return { contents: readFileSync(file, 'utf8'), mode: stat.mode }
410
- }
@@ -1,39 +0,0 @@
1
- import { PassThrough } from 'node:stream'
2
- import { describe, expect, it } from 'vitest'
3
- import { readStdinWithTimeout } from './hook-input.js'
4
-
5
- describe('hook input reader', () => {
6
- it('rejects a timeout when the harness pipe delivered no bytes', async () => {
7
- const input = new PassThrough()
8
- await expect(readStdinWithTimeout(input, 5)).rejects.toThrow(/timed out waiting 5ms/)
9
- })
10
-
11
- it('rejects a stream failure when no useful input exists', async () => {
12
- const input = new PassThrough()
13
- const reading = readStdinWithTimeout(input, 1_000)
14
- input.destroy(new Error('stdin disappeared'))
15
- await expect(reading).rejects.toThrow('stdin disappeared')
16
- })
17
-
18
- it('returns partial bytes on timeout so the hook can diagnose truncation', async () => {
19
- const input = new PassThrough()
20
- const reading = readStdinWithTimeout(input, 5)
21
- input.write('{"session_id":')
22
- await expect(reading).resolves.toBe('{"session_id":')
23
- })
24
-
25
- it('preserves complete buffered JSON when the stream fails after delivering it', async () => {
26
- const input = new PassThrough()
27
- const reading = readStdinWithTimeout(input, 1_000)
28
- input.write('{"session_id":"kept"}')
29
- input.destroy(new Error('late pipe failure'))
30
- await expect(reading).resolves.toBe('{"session_id":"kept"}')
31
- })
32
-
33
- it('marks an oversized payload as truncated instead of treating it as empty', async () => {
34
- const input = new PassThrough()
35
- const reading = readStdinWithTimeout(input, 1_000, 12)
36
- input.end('{"session_id":"far-too-long"}')
37
- await expect(reading).resolves.toMatch(/hook input truncated/)
38
- })
39
- })
package/src/hook-input.ts DELETED
@@ -1,72 +0,0 @@
1
- import type { Readable } from 'node:stream'
2
-
3
- const TRUNCATED_INPUT_SUFFIX = '\n[notifai: hook input truncated]\n'
4
-
5
- /**
6
- * Read one harness-hook envelope without letting a stuck pipe hold the prompt.
7
- *
8
- * A timeout or stream error before any bytes arrive rejects, so the hook can
9
- * record that its input was unavailable. Once bytes exist they are more useful
10
- * than the transport failure: complete JSON can still run, while partial or
11
- * capped JSON reaches the hook's malformed/truncated diagnostic.
12
- */
13
- export function readStdinWithTimeout(
14
- input: Readable = process.stdin,
15
- timeoutMs = 2_000,
16
- maxBytes = 1_000_000,
17
- ): Promise<string> {
18
- if ((input as Readable & { isTTY?: boolean }).isTTY === true) return Promise.resolve('')
19
-
20
- return new Promise<string>((resolve, reject) => {
21
- const chunks: Buffer[] = []
22
- let total = 0
23
- let settled = false
24
-
25
- const cleanup = (): void => {
26
- clearTimeout(timer)
27
- input.off('data', onData)
28
- input.off('end', onEnd)
29
- input.off('error', onError)
30
- input.destroy()
31
- }
32
- const finishBuffered = (): void => {
33
- if (settled) return
34
- settled = true
35
- cleanup()
36
- resolve(Buffer.concat(chunks).toString('utf8'))
37
- }
38
- const failEmpty = (err: Error): void => {
39
- if (settled) return
40
- if (chunks.length > 0) return finishBuffered()
41
- settled = true
42
- cleanup()
43
- reject(err)
44
- }
45
- const onData = (chunk: Buffer | string): void => {
46
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
47
- const remaining = Math.max(0, maxBytes - total)
48
- if (bytes.length <= remaining) {
49
- chunks.push(bytes)
50
- total += bytes.length
51
- return
52
- }
53
- if (remaining > 0) {
54
- chunks.push(bytes.subarray(0, remaining))
55
- total += remaining
56
- }
57
- chunks.push(Buffer.from(TRUNCATED_INPUT_SUFFIX))
58
- finishBuffered()
59
- }
60
- const onEnd = (): void => finishBuffered()
61
- const onError = (err: Error): void => failEmpty(err)
62
-
63
- const timer = setTimeout(
64
- () => failEmpty(new Error(`timed out waiting ${timeoutMs}ms for hook input`)),
65
- timeoutMs,
66
- )
67
- timer.unref?.()
68
- input.on('data', onData)
69
- input.once('end', onEnd)
70
- input.once('error', onError)
71
- })
72
- }