@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,499 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { existsSync, readFileSync } from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { createConnection } from 'node:net'
6
- import type {
7
- ContinuationEvent,
8
- DeliveryOutcome,
9
- EscalationDeliveryRoute,
10
- } from './hooks.js'
11
-
12
- /** Claude Code's currently observed inbox protocol. Unknown versions fail closed. */
13
- export const CLAUDE_PEER_PROTOCOL = 1
14
-
15
- /** The first Claude Code release that publishes a session inbox socket. */
16
- export const CLAUDE_MIN_INBOX_VERSION = '2.1.224'
17
-
18
- /** The inbox socket is a Unix domain socket; no Windows implementation exists. */
19
- const INBOX_PLATFORMS: ReadonlySet<string> = new Set(['darwin', 'linux'])
20
-
21
- /** macOS verifies own-child ancestry only while the posting process is alive. */
22
- export const CLAUDE_POST_SEND_LIVENESS_MS = 8_000
23
-
24
- export type ClaudeLiveStatus = 'idle' | 'busy'
25
-
26
- export interface ClaudeSessionDescriptor {
27
- pid: number
28
- sessionId: string
29
- cwd: string
30
- startedAt: number
31
- procStart: string
32
- version: string
33
- peerProtocol: number
34
- messagingSocketPath: string
35
- status: string
36
- }
37
-
38
- export interface ClaudeAgentObservation {
39
- pid: number
40
- sessionId: string
41
- startedAt: number
42
- status: string
43
- }
44
-
45
- export interface ClaudeWakeAdapters {
46
- listAgents(): Promise<unknown>
47
- readDescriptor(pid: number): unknown
48
- sendSocket(socketPath: string, line: string): Promise<void>
49
- resume(sessionId: string, cwd: string, context: string): Promise<void>
50
- sleep(milliseconds: number): Promise<void>
51
- }
52
-
53
- export type ClaudeWakeObservation =
54
- | {
55
- state: 'live-idle' | 'live-busy'
56
- descriptor: ClaudeSessionDescriptor
57
- }
58
- | { state: 'stopped' }
59
- | { state: 'unknown'; reason: string }
60
-
61
- const LIVE_STATUSES: ReadonlySet<string> = new Set(['idle', 'busy'])
62
-
63
- function isRecord(value: unknown): value is Record<string, unknown> {
64
- return typeof value === 'object' && value !== null && !Array.isArray(value)
65
- }
66
-
67
- function parseAgent(value: unknown): ClaudeAgentObservation | null {
68
- if (!isRecord(value)) return null
69
- if (
70
- typeof value['pid'] !== 'number' ||
71
- !Number.isInteger(value['pid']) ||
72
- value['pid'] <= 0 ||
73
- typeof value['sessionId'] !== 'string' ||
74
- value['sessionId'] === '' ||
75
- typeof value['startedAt'] !== 'number' ||
76
- !Number.isFinite(value['startedAt']) ||
77
- typeof value['status'] !== 'string'
78
- ) {
79
- return null
80
- }
81
- return {
82
- pid: value['pid'],
83
- sessionId: value['sessionId'],
84
- startedAt: value['startedAt'],
85
- status: value['status'],
86
- }
87
- }
88
-
89
- function parseDescriptor(value: unknown): ClaudeSessionDescriptor | null {
90
- if (!isRecord(value)) return null
91
- if (
92
- typeof value['pid'] !== 'number' ||
93
- !Number.isInteger(value['pid']) ||
94
- value['pid'] <= 0 ||
95
- typeof value['sessionId'] !== 'string' ||
96
- value['sessionId'] === '' ||
97
- typeof value['cwd'] !== 'string' ||
98
- value['cwd'] === '' ||
99
- typeof value['startedAt'] !== 'number' ||
100
- !Number.isFinite(value['startedAt']) ||
101
- typeof value['procStart'] !== 'string' ||
102
- value['procStart'] === '' ||
103
- typeof value['version'] !== 'string' ||
104
- value['version'] === '' ||
105
- typeof value['peerProtocol'] !== 'number' ||
106
- !Number.isInteger(value['peerProtocol']) ||
107
- typeof value['messagingSocketPath'] !== 'string' ||
108
- value['messagingSocketPath'] === '' ||
109
- typeof value['status'] !== 'string'
110
- ) {
111
- return null
112
- }
113
- return {
114
- pid: value['pid'],
115
- sessionId: value['sessionId'],
116
- cwd: value['cwd'],
117
- startedAt: value['startedAt'],
118
- procStart: value['procStart'],
119
- version: value['version'],
120
- peerProtocol: value['peerProtocol'],
121
- messagingSocketPath: value['messagingSocketPath'],
122
- status: value['status'],
123
- }
124
- }
125
-
126
- /**
127
- * Observe one exact Claude Code session without guessing.
128
- *
129
- * `claude agents --json` is the liveness oracle: Claude Code itself validates
130
- * each registry row against both PID and process start time before returning it.
131
- * The descriptor supplies the socket and peer protocol, but is never trusted as
132
- * proof that a process still owns the session.
133
- */
134
- export async function observeClaudeSession(
135
- sessionId: string,
136
- adapters: Pick<ClaudeWakeAdapters, 'listAgents' | 'readDescriptor'>,
137
- ): Promise<ClaudeWakeObservation> {
138
- let listed: unknown
139
- try {
140
- listed = await adapters.listAgents()
141
- } catch (err) {
142
- return {
143
- state: 'unknown',
144
- reason: `claude agents probe failed: ${err instanceof Error ? err.message : String(err)}`,
145
- }
146
- }
147
- if (!Array.isArray(listed)) {
148
- return { state: 'unknown', reason: 'claude agents returned an invalid document' }
149
- }
150
-
151
- const matchingRaw = listed.filter(
152
- (value) => isRecord(value) && value['sessionId'] === sessionId,
153
- )
154
- if (matchingRaw.length === 0) return { state: 'stopped' }
155
- if (matchingRaw.length !== 1) {
156
- return { state: 'unknown', reason: 'claude agents returned duplicate session owners' }
157
- }
158
- const agent = parseAgent(matchingRaw[0])
159
- if (agent === null) {
160
- return { state: 'unknown', reason: 'claude agents returned an invalid matching row' }
161
- }
162
-
163
- let descriptorRaw: unknown
164
- try {
165
- descriptorRaw = adapters.readDescriptor(agent.pid)
166
- } catch (err) {
167
- return {
168
- state: 'unknown',
169
- reason: `Claude session descriptor could not be read: ${err instanceof Error ? err.message : String(err)}`,
170
- }
171
- }
172
- const descriptor = parseDescriptor(descriptorRaw)
173
- if (descriptor === null) {
174
- return { state: 'unknown', reason: 'Claude session descriptor has an unknown shape' }
175
- }
176
- if (
177
- descriptor.pid !== agent.pid ||
178
- descriptor.sessionId !== agent.sessionId ||
179
- descriptor.startedAt !== agent.startedAt
180
- ) {
181
- return { state: 'unknown', reason: 'Claude liveness probe and descriptor disagree' }
182
- }
183
- if (descriptor.peerProtocol !== CLAUDE_PEER_PROTOCOL) {
184
- return {
185
- state: 'unknown',
186
- reason: `unsupported Claude peer protocol ${descriptor.peerProtocol}`,
187
- }
188
- }
189
- if (!LIVE_STATUSES.has(agent.status)) {
190
- return { state: 'unknown', reason: `Claude session status ${agent.status} is not wakeable` }
191
- }
192
- return {
193
- state: agent.status === 'idle' ? 'live-idle' : 'live-busy',
194
- descriptor,
195
- }
196
- }
197
-
198
- export type ClaudeInboxReadiness =
199
- | { state: 'ready'; socketPath: string; version: string }
200
- | { state: 'unavailable'; reason: string }
201
-
202
- /** Ordered comparison of dotted release numbers, ignoring any suffix. */
203
- function versionAtLeast(version: string, minimum: string): boolean {
204
- const parse = (value: string): number[] =>
205
- value.split('.').map((part) => Number.parseInt(part, 10) || 0)
206
- const left = parse(version)
207
- const right = parse(minimum)
208
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
209
- const a = left[index] ?? 0
210
- const b = right[index] ?? 0
211
- if (a !== b) return a > b
212
- }
213
- return true
214
- }
215
-
216
- /**
217
- * Whether an answer could reach this exact Claude session over its inbox
218
- * socket, decided from evidence already on disk.
219
- *
220
- * Read-only by construction: it reads the session descriptor and asks whether
221
- * the socket file exists, and never connects to it. A diagnostic that delivers
222
- * a message is not a diagnostic.
223
- *
224
- * Every negative answer here is a degradation, not a failure — the accepted
225
- * journal still replays the answer at the session's next turn. What it buys is
226
- * that the reason is nameable in advance instead of being discovered as
227
- * silence. The common one is `--bare`, which binds no socket at all (and runs
228
- * no hooks either, so nothing would have registered a question in the first
229
- * place).
230
- */
231
- export function inspectClaudeInbox(options: {
232
- pid: number
233
- platform: NodeJS.Platform
234
- readDescriptor: (pid: number) => unknown
235
- socketExists: (socketPath: string) => boolean
236
- }): ClaudeInboxReadiness {
237
- if (!INBOX_PLATFORMS.has(options.platform)) {
238
- return {
239
- state: 'unavailable',
240
- reason: `Claude Code publishes no inbox socket on ${options.platform}; it exists on macOS and Linux only`,
241
- }
242
- }
243
- let raw: unknown
244
- try {
245
- raw = options.readDescriptor(options.pid)
246
- } catch (err) {
247
- return {
248
- state: 'unavailable',
249
- reason: `no session descriptor for pid ${options.pid} (${err instanceof Error ? err.message : String(err)}); a session started with \`--bare\` binds no inbox socket`,
250
- }
251
- }
252
- const descriptor = parseDescriptor(raw)
253
- if (descriptor === null) {
254
- return {
255
- state: 'unavailable',
256
- reason: `the session descriptor for pid ${options.pid} has an unknown shape`,
257
- }
258
- }
259
- if (descriptor.peerProtocol !== CLAUDE_PEER_PROTOCOL) {
260
- return {
261
- state: 'unavailable',
262
- reason: `this session speaks inbox protocol ${descriptor.peerProtocol}, and only ${CLAUDE_PEER_PROTOCOL} is known; refusing to guess at an undocumented wire format`,
263
- }
264
- }
265
- if (!versionAtLeast(descriptor.version, CLAUDE_MIN_INBOX_VERSION)) {
266
- return {
267
- state: 'unavailable',
268
- reason: `Claude Code ${descriptor.version} is older than ${CLAUDE_MIN_INBOX_VERSION}, which is where the inbox socket starts`,
269
- }
270
- }
271
- if (!options.socketExists(descriptor.messagingSocketPath)) {
272
- return {
273
- state: 'unavailable',
274
- reason: `the descriptor names ${descriptor.messagingSocketPath}, but no socket exists there`,
275
- }
276
- }
277
- return {
278
- state: 'ready',
279
- socketPath: descriptor.messagingSocketPath,
280
- version: descriptor.version,
281
- }
282
- }
283
-
284
- function socketLine(context: string): string {
285
- return `${JSON.stringify({
286
- type: 'user',
287
- message: { role: 'user', content: context },
288
- })}\n`
289
- }
290
-
291
- /**
292
- * Claude's own-child delivery route.
293
- *
294
- * A successful socket write proves delivery to Claude Code, not model
295
- * consumption — and this route reports exactly that, no more. It reports
296
- * `delivered` all the same, because the write is the strongest acknowledgement
297
- * this route can ever obtain: the message it posts starts a brand-new turn
298
- * rather than continuing this one, so no later hook will confirm anything about
299
- * it. An answer delivered once and settled is strictly better than an answer
300
- * redelivered on every turn-end for ever.
301
- *
302
- * Every path that hands nothing over reports `held`, so the accepted journal
303
- * replays it. Unknown state deliberately returns without throwing, so the
304
- * journal becomes `hold-for-next-turn` instead of failing the hook.
305
- */
306
- export function claudeWakeRoute(options: {
307
- sessionId: string
308
- cwd: string
309
- sourcePid: number
310
- adapters?: ClaudeWakeAdapters
311
- }): EscalationDeliveryRoute {
312
- const adapters = options.adapters ?? systemClaudeWakeAdapters()
313
- let sourceDescriptor: ClaudeSessionDescriptor | null = null
314
- try {
315
- const parsed = parseDescriptor(adapters.readDescriptor(options.sourcePid))
316
- if (
317
- parsed !== null &&
318
- parsed.sessionId === options.sessionId &&
319
- parsed.pid === options.sourcePid
320
- ) {
321
- sourceDescriptor = parsed
322
- }
323
- } catch {
324
- // Delivery fails closed below; route construction must never break the hook.
325
- }
326
- return {
327
- kind: 'inbox-socket',
328
- async deliver(event: ContinuationEvent): Promise<DeliveryOutcome> {
329
- const observation = await observeClaudeSession(options.sessionId, adapters)
330
- if (observation.state === 'unknown') {
331
- return {
332
- notes: [`holding the accepted answer for the next turn: ${observation.reason}`],
333
- log: { route: 'hold-for-next-turn', stage: 'queued', reason: observation.reason },
334
- acknowledgement: 'held',
335
- }
336
- }
337
- if (sourceDescriptor === null) {
338
- const reason = 'the Stop-hook process cannot prove exact Claude session ownership'
339
- return {
340
- notes: [`holding the accepted answer for the next turn: ${reason}`],
341
- log: { route: 'hold-for-next-turn', stage: 'queued', reason },
342
- acknowledgement: 'held',
343
- }
344
- }
345
- if (observation.state === 'stopped') {
346
- // Probe immediately before spawn. A prior descriptor or dead socket is
347
- // never sufficient: resuming a live session creates a divergent ghost.
348
- const confirmed = await observeClaudeSession(options.sessionId, adapters)
349
- if (confirmed.state !== 'stopped') {
350
- const reason =
351
- confirmed.state === 'unknown'
352
- ? confirmed.reason
353
- : 'the Claude session became live before cold resume'
354
- return {
355
- notes: [`holding the accepted answer for the next turn: ${reason}`],
356
- log: { route: 'hold-for-next-turn', stage: 'queued', reason },
357
- acknowledgement: 'held',
358
- }
359
- }
360
- if (!event.commitDelivery()) return cancelledDelivery()
361
- await adapters.resume(options.sessionId, sourceDescriptor.cwd, event.context)
362
- return {
363
- notes: ['cold-resumed the stopped Claude session with its accepted answer'],
364
- log: { route: 'cold-resume', stage: 'delivered' },
365
- acknowledgement: 'delivered',
366
- }
367
- }
368
-
369
- if (
370
- observation.descriptor.pid !== options.sourcePid ||
371
- sourceDescriptor.startedAt !== observation.descriptor.startedAt
372
- ) {
373
- const reason = 'the Stop-hook process is not the observed exact Claude session child'
374
- return {
375
- notes: [`holding the accepted answer for the next turn: ${reason}`],
376
- log: { route: 'hold-for-next-turn', stage: 'queued', reason },
377
- acknowledgement: 'held',
378
- }
379
- }
380
- if (!event.commitDelivery()) return cancelledDelivery()
381
- await adapters.sendSocket(
382
- observation.descriptor.messagingSocketPath,
383
- socketLine(event.context),
384
- )
385
- await adapters.sleep(CLAUDE_POST_SEND_LIVENESS_MS)
386
- return {
387
- notes: [
388
- observation.state === 'live-idle'
389
- ? 'posted the accepted answer to the live Claude session'
390
- : 'queued the accepted answer for the busy Claude session',
391
- ],
392
- log: {
393
- route: 'inbox-socket',
394
- stage: 'delivered',
395
- session_state: observation.state,
396
- },
397
- // The write completed: Claude Code holds the message. Nothing later
398
- // reports on it, so this is where the journal settles.
399
- acknowledgement: 'delivered',
400
- }
401
- },
402
- }
403
- }
404
-
405
- function cancelledDelivery(): DeliveryOutcome {
406
- return {
407
- notes: ['the Agent Session ended before answer delivery; stopping this observer'],
408
- log: { route: 'hold-for-next-turn', stage: 'queued', reason: 'session-ended' },
409
- acknowledgement: 'held',
410
- }
411
- }
412
-
413
- function runClaude(args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}): Promise<string> {
414
- return new Promise((resolve, reject) => {
415
- const child = spawn('claude', args, {
416
- ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
417
- ...(options.env === undefined ? {} : { env: options.env }),
418
- stdio: ['ignore', 'pipe', 'pipe'],
419
- })
420
- const stdout: Buffer[] = []
421
- const stderr: Buffer[] = []
422
- child.stdout?.on('data', (chunk: Buffer | string) => stdout.push(Buffer.from(chunk)))
423
- child.stderr?.on('data', (chunk: Buffer | string) => stderr.push(Buffer.from(chunk)))
424
- child.once('error', reject)
425
- child.once('exit', (code, signal) => {
426
- if (code === 0) {
427
- resolve(Buffer.concat(stdout).toString())
428
- return
429
- }
430
- reject(
431
- new Error(
432
- `claude ${args[0] ?? ''} exited ${code === null ? `on ${String(signal)}` : String(code)}: ${Buffer.concat(stderr).toString().trim()}`,
433
- ),
434
- )
435
- })
436
- })
437
- }
438
-
439
- function coldResumeEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
440
- const next = { ...env }
441
- for (const key of Object.keys(next)) {
442
- if (key === 'CLAUDECODE' || key === 'CLAUDE_PID' || key.startsWith('CLAUDE_CODE_')) {
443
- delete next[key]
444
- }
445
- }
446
- return next
447
- }
448
-
449
- export function systemClaudeWakeAdapters(
450
- env: NodeJS.ProcessEnv = process.env,
451
- ): ClaudeWakeAdapters {
452
- return {
453
- async listAgents() {
454
- const output = await runClaude(['agents', '--json'], { env })
455
- return JSON.parse(output) as unknown
456
- },
457
- readDescriptor(pid) {
458
- const file = path.join(os.homedir(), '.claude', 'sessions', `${pid}.json`)
459
- return JSON.parse(readFileSync(file, 'utf8')) as unknown
460
- },
461
- sendSocket(socketPath, line) {
462
- return new Promise<void>((resolve, reject) => {
463
- const socket = createConnection(socketPath)
464
- let settled = false
465
- const fail = (err: Error): void => {
466
- if (settled) return
467
- settled = true
468
- socket.destroy()
469
- reject(err)
470
- }
471
- socket.once('error', fail)
472
- socket.once('connect', () => {
473
- socket.end(line, () => {
474
- if (settled) return
475
- settled = true
476
- resolve()
477
- })
478
- })
479
- })
480
- },
481
- async resume(sessionId, cwd, context) {
482
- if (!existsSync(cwd)) throw new Error(`Claude session cwd no longer exists: ${cwd}`)
483
- await runClaude(
484
- [
485
- '-p',
486
- '--resume',
487
- sessionId,
488
- '--output-format',
489
- 'json',
490
- context,
491
- ],
492
- { cwd, env: coldResumeEnvironment(env) },
493
- )
494
- },
495
- sleep(milliseconds) {
496
- return new Promise((resolve) => setTimeout(resolve, milliseconds))
497
- },
498
- }
499
- }
@@ -1,38 +0,0 @@
1
- import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { describe, expect, it } from 'vitest'
5
- import { cliBinReadiness, isExecutablePath, pathNotifaiEntries } from './cli-bin.js'
6
-
7
- describe('PATH notifai diagnosis', () => {
8
- it('reports a non-executable PATH binary as a gap', () => {
9
- if (process.platform === 'win32') return
10
- const directory = mkdtempSync(path.join(os.tmpdir(), 'notifai-bin-gap-'))
11
- const file = path.join(directory, 'notifai')
12
- writeFileSync(file, '#!/usr/bin/env node\n')
13
- chmodSync(file, 0o644)
14
- expect(isExecutablePath(file, 'darwin')).toBe(false)
15
- const state = cliBinReadiness({ PATH: directory }, 'darwin')
16
- expect(state.status).toBe('gap')
17
- expect(state.detail).toContain(file)
18
- expect(state.remedy?.command).toBe('npm install -g @raidiant/notifai')
19
- })
20
-
21
- it('treats an executable PATH binary as ready', () => {
22
- if (process.platform === 'win32') return
23
- const directory = mkdtempSync(path.join(os.tmpdir(), 'notifai-bin-ok-'))
24
- const file = path.join(directory, 'notifai')
25
- writeFileSync(file, '#!/usr/bin/env node\n')
26
- chmodSync(file, 0o755)
27
- expect(pathNotifaiEntries({ PATH: directory }, 'darwin')).toEqual([file])
28
- expect(cliBinReadiness({ PATH: directory }, 'darwin').status).toBe('ready')
29
- })
30
-
31
- it('is ready when PATH has no notifai entry', () => {
32
- const directory = mkdtempSync(path.join(os.tmpdir(), 'notifai-bin-empty-'))
33
- mkdirSync(directory, { recursive: true })
34
- const state = cliBinReadiness({ PATH: directory }, process.platform)
35
- expect(state.status).toBe('ready')
36
- expect(state.detail).toContain('no notifai binary was on PATH')
37
- })
38
- })
package/src/cli-bin.ts DELETED
@@ -1,67 +0,0 @@
1
- import { accessSync, constants, existsSync, lstatSync, realpathSync } from 'node:fs'
2
- import path from 'node:path'
3
- import type { ReadinessState } from './readiness.js'
4
-
5
- const POSIX_NAMES = ['notifai']
6
- const WINDOWS_NAMES = ['notifai.cmd', 'notifai.exe', 'notifai']
7
-
8
- export function pathNotifaiEntries(
9
- env: NodeJS.ProcessEnv,
10
- platform: NodeJS.Platform = process.platform,
11
- ): string[] {
12
- const raw = platform === 'win32' ? (env['Path'] ?? env['PATH'] ?? '') : (env['PATH'] ?? '')
13
- const delimiter = platform === 'win32' ? ';' : ':'
14
- const names = platform === 'win32' ? WINDOWS_NAMES : POSIX_NAMES
15
- const found: string[] = []
16
- for (const directory of raw.split(delimiter)) {
17
- if (directory === '') continue
18
- for (const name of names) {
19
- const candidate = path.join(directory, name)
20
- if (!existsSync(candidate)) continue
21
- if (!found.includes(candidate)) found.push(candidate)
22
- }
23
- }
24
- return found
25
- }
26
-
27
- export function isExecutablePath(file: string, platform: NodeJS.Platform = process.platform): boolean {
28
- if (!existsSync(file)) return false
29
- if (platform === 'win32') return true
30
- try {
31
- const target = lstatSync(file).isSymbolicLink() ? realpathSync(file) : file
32
- accessSync(target, constants.X_OK)
33
- return true
34
- } catch {
35
- return false
36
- }
37
- }
38
-
39
- export function cliBinReadiness(
40
- env: NodeJS.ProcessEnv,
41
- platform: NodeJS.Platform = process.platform,
42
- ): ReadinessState {
43
- const entries = pathNotifaiEntries(env, platform)
44
- const broken = entries.find((file) => !isExecutablePath(file, platform))
45
- if (broken !== undefined) {
46
- return {
47
- id: 'cli-bin',
48
- title: 'notifai command',
49
- status: 'gap',
50
- detail: `${broken} is on PATH but is not executable`,
51
- remedy: {
52
- by: 'user-here',
53
- summary: 'repair the global notifai command so it can run',
54
- command: 'npm install -g @raidiant/notifai',
55
- },
56
- }
57
- }
58
- return {
59
- id: 'cli-bin',
60
- title: 'notifai command',
61
- status: 'ready',
62
- detail:
63
- entries[0] === undefined
64
- ? 'this process can run; no notifai binary was on PATH'
65
- : `${entries[0]} is executable`,
66
- }
67
- }
@@ -1,46 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'vitest'
2
- import {
3
- compareCliSemVer,
4
- latestPublishedCliVersion,
5
- newerPublishedCli,
6
- resetLatestPublishedCliVersionForTest,
7
- shouldConsultCliRegistry,
8
- } from './cli-release.js'
9
-
10
- afterEach(() => {
11
- resetLatestPublishedCliVersionForTest()
12
- })
13
-
14
- describe('CLI registry recommendation', () => {
15
- it('is suppressed for JSON, non-interactive, and CI callers', () => {
16
- expect(shouldConsultCliRegistry({ interactive: true })).toBe(true)
17
- expect(shouldConsultCliRegistry({ interactive: true, json: true })).toBe(false)
18
- expect(shouldConsultCliRegistry({ interactive: false })).toBe(false)
19
- expect(shouldConsultCliRegistry({ interactive: true, env: { CI: 'true' } })).toBe(false)
20
- })
21
-
22
- it('never throws when the registry is unreachable', async () => {
23
- const latest = await latestPublishedCliVersion(async () => {
24
- throw new Error('network down')
25
- })
26
- expect(latest).toBeNull()
27
- })
28
-
29
- it('caches a successful dist-tag read', async () => {
30
- let calls = 0
31
- const fetchImpl: typeof fetch = async () => {
32
- calls += 1
33
- return new Response(JSON.stringify({ latest: '9.1.0' }), { status: 200 })
34
- }
35
- expect(await latestPublishedCliVersion(fetchImpl)).toBe('9.1.0')
36
- expect(await latestPublishedCliVersion(fetchImpl)).toBe('9.1.0')
37
- expect(calls).toBe(1)
38
- })
39
-
40
- it('only recommends a strictly newer published version', () => {
41
- expect(newerPublishedCli('8.0.0', '8.0.1')).toBe('8.0.1')
42
- expect(newerPublishedCli('8.0.0', '8.0.0')).toBeNull()
43
- expect(newerPublishedCli('8.0.1', '8.0.0')).toBeNull()
44
- expect(compareCliSemVer('8.0.0', '7.0.2')).toBeGreaterThan(0)
45
- })
46
- })