@raidiant/notifai 10.1.1 → 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 (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. package/tsconfig.json +0 -10
package/src/send.ts DELETED
@@ -1,436 +0,0 @@
1
- import {
2
- COLLAPSE_KEY_MAX_BYTES,
3
- INTERRUPTION_LEVELS,
4
- MEDIA_MAX_ITEMS,
5
- NOTIFICATION_SCHEMA_VERSION,
6
- QUESTION_TEXT_MAX_LENGTH,
7
- bannerExcerpt,
8
- type AndroidOptionsT,
9
- type IosOptionsT,
10
- type LifecycleT,
11
- type MacosOptionsT,
12
- type MediaItemT,
13
- type NotificationDraftT,
14
- NOTIFICATION_KINDS,
15
- PLATFORMS,
16
- type Platform,
17
- type QuestionT,
18
- type SourceContextT,
19
- type SubmissionReceipt,
20
- } from '@raidiant/notifai-protocol'
21
- import type { CliConfig } from './config.js'
22
- import { isCliSoundRef, unknownSoundMessage } from './sound-ref.js'
23
-
24
- export interface SendFlags {
25
- title: string
26
- /** The one canonical Markdown body. */
27
- body: string
28
- subtitle?: string
29
- /** What this notification is; absent means update. */
30
- kind?: string
31
- project?: string
32
- /** Deliberately omit Project identity even when config or cwd could supply one. */
33
- projectless?: boolean
34
- /** Opaque exact Agent Session override. */
35
- sessionId?: string
36
- /** Human-readable Agent Session label override. */
37
- sessionLabel?: string
38
- device?: string[]
39
- all?: boolean
40
- ttl?: number
41
- collapseKey?: string
42
- sound?: string
43
- threadId?: string
44
- /** Apple interruption level; null is reserved for internal callers that must suppress config. */
45
- level?: string | null
46
- data?: string[]
47
- /** Ready media ids in author-supplied order. Commands resolve paths and URLs first. */
48
- image?: string[]
49
- /** Optional alt text paired with images by position. */
50
- imageAlt?: string[]
51
- /**
52
- * Complete canonical media manifest for internal callers that already froze
53
- * uploads and alt text. Mutually exclusive with the CLI's parallel inputs.
54
- */
55
- media?: MediaItemT[]
56
- /** Enable the inline reply action for this Notification Request. */
57
- reply?: boolean
58
- /** How long the server accepts a reply after submission. */
59
- replyWindow?: number
60
- /**
61
- * Answer labels; turns the reply into a closed question. One flag occurrence
62
- * per label, always — a label is a label, whatever characters it contains.
63
- */
64
- choice?: string[]
65
- /** The user may select several of the offered answers. */
66
- multi?: boolean
67
- /**
68
- * A full question set, overriding the single question the flags describe.
69
- * Not a user-facing flag: `ask --form` and the hooks build it.
70
- */
71
- questions?: QuestionT[]
72
- /** Platform whose optional notification fields these flags configure. */
73
- platform?: string
74
- /**
75
- * Lifecycle position of this send; not a user-facing flag. Hooks set it so
76
- * questions and their retirements ride the wire as what they are.
77
- */
78
- lifecycle?: LifecycleT
79
- /**
80
- * Allow the two-character sequence backslash-n to be sent as visible text.
81
- * Without this, a likely accidental escaped newline is refused.
82
- */
83
- literalBackslashN?: boolean
84
- }
85
-
86
- /** A backslash-n pair that was not itself escaped as a literal backslash. */
87
- const ACCIDENTAL_ESCAPED_NEWLINE = /(?:^|[^\\])\\n/
88
-
89
- export function hasAccidentalEscapedNewlines(body: string): boolean {
90
- return ACCIDENTAL_ESCAPED_NEWLINE.test(body)
91
- }
92
-
93
- export function accidentalEscapedNewlineMessage(): string {
94
- return [
95
- 'This body contains the two-character sequence \\n, which is sent literally and shows as \\n on the device.',
96
- 'To send a real line break, pass a multiline --body or --body-file -.',
97
- 'If you meant the visible characters \\n, pass --literal-backslash-n.',
98
- ].join('\n')
99
- }
100
-
101
- export function rejectAccidentalEscapedNewlines(
102
- body: string | undefined,
103
- allowLiteral: boolean | undefined,
104
- ): string | null {
105
- if (allowLiteral === true || body === undefined) return null
106
- return hasAccidentalEscapedNewlines(body) ? accidentalEscapedNewlineMessage() : null
107
- }
108
-
109
- export type DraftBuild =
110
- | { ok: true; draft: NotificationDraftT; platform: Platform }
111
- | { ok: false; error: string }
112
-
113
- /** Invocation-derived values beneath explicit and configured author input. */
114
- export interface DraftInvocation {
115
- inferredProject?: string | null
116
- source?: SourceContextT
117
- }
118
-
119
- const POSITIONAL_MEDIA_REFERENCE = /media:(\d+)(?![A-Za-z0-9_-])/g
120
-
121
- /** Validate media cardinality and positional alt pairing before any upload starts. */
122
- export function validateMediaInputs(
123
- images: readonly string[] | undefined,
124
- alts: readonly string[] | undefined,
125
- ): string | null {
126
- const imageCount = images?.length ?? 0
127
- const imageAlts = alts ?? []
128
- if (imageCount > MEDIA_MAX_ITEMS) return `Attach at most ${MEDIA_MAX_ITEMS} images.`
129
- if (imageAlts.length > imageCount) {
130
- return `Received ${imageAlts.length} --image-alt values for ${imageCount} --image values.`
131
- }
132
- if (imageAlts.some((alt) => alt.length < 1 || alt.length > 256)) {
133
- return '--image-alt must be 1-256 characters.'
134
- }
135
- return null
136
- }
137
-
138
- /** Validate a frozen value-object manifest without collapsing sparse alt text. */
139
- function validateMediaManifest(media: readonly MediaItemT[]): string | null {
140
- if (media.length > MEDIA_MAX_ITEMS) return `Attach at most ${MEDIA_MAX_ITEMS} images.`
141
- if (media.some((item) => item.alt !== undefined && (item.alt.length < 1 || item.alt.length > 256))) {
142
- return 'Media alt text must be 1-256 characters.'
143
- }
144
- return null
145
- }
146
-
147
- /** Rewrite authorable 1-based media positions to canonical ready media ids. */
148
- export function rewriteMediaReferences(
149
- body: string,
150
- mediaIds: readonly string[],
151
- ): { ok: true; body: string } | { ok: false; error: string } {
152
- let error: string | undefined
153
- const rewritten = body.replace(POSITIONAL_MEDIA_REFERENCE, (reference, digits: string) => {
154
- const position = Number(digits)
155
- if (
156
- !Number.isSafeInteger(position) ||
157
- position < 1 ||
158
- position > MEDIA_MAX_ITEMS ||
159
- digits !== String(position) ||
160
- mediaIds[position - 1] === undefined
161
- ) {
162
- error = `Body reference "${reference}" has no matching --image occurrence.`
163
- return reference
164
- }
165
- return `media:${mediaIds[position - 1]}`
166
- })
167
- return error === undefined ? { ok: true, body: rewritten } : { ok: false, error }
168
- }
169
-
170
- /** Merge flags over resolved config into a Notification Request draft. */
171
- export function buildDraft(
172
- config: CliConfig,
173
- flags: SendFlags,
174
- invocation: DraftInvocation = {},
175
- ): DraftBuild {
176
- if (!flags.title || !flags.body) return { ok: false, error: 'Both --title and --body are required.' }
177
- const platform = resolvePlatform(flags.platform)
178
- if (!platform) {
179
- return { ok: false, error: `Unknown platform "${flags.platform}" — use ${PLATFORMS.join(' or ')}.` }
180
- }
181
-
182
- if (flags.media !== undefined && (flags.image !== undefined || flags.imageAlt !== undefined)) {
183
- return { ok: false, error: 'Internal media manifests cannot be combined with --image or --image-alt.' }
184
- }
185
- const imageIds = flags.media?.map((item) => item.media_id) ?? flags.image ?? []
186
- const imageAlts = flags.imageAlt ?? []
187
- const mediaInputError =
188
- flags.media === undefined
189
- ? validateMediaInputs(imageIds, imageAlts)
190
- : validateMediaManifest(flags.media)
191
- if (mediaInputError !== null) return { ok: false, error: mediaInputError }
192
- const media =
193
- flags.media ??
194
- imageIds.map((media_id, index) => ({
195
- media_id,
196
- ...(imageAlts[index] !== undefined ? { alt: imageAlts[index] } : {}),
197
- }))
198
- const body = rewriteMediaReferences(flags.body, imageIds)
199
- if (!body.ok) return body
200
-
201
- const deviceIds = flags.all ? null : (flags.device?.length ? flags.device : config.devices.value)
202
- const targets: NotificationDraftT['targets'] =
203
- deviceIds && deviceIds.length > 0 ? { mode: 'selected', device_ids: deviceIds } : { mode: 'all' }
204
-
205
- if (
206
- flags.kind !== undefined &&
207
- !NOTIFICATION_KINDS.includes(flags.kind as (typeof NOTIFICATION_KINDS)[number])
208
- ) {
209
- return {
210
- ok: false,
211
- error: `Unknown kind "${flags.kind}" — supported: ${NOTIFICATION_KINDS.map((kind) => `"${kind}"`).join(', ')}.`,
212
- }
213
- }
214
-
215
- // Explicit --sound and the saved sound key outrank kind. The server is the
216
- // one that applies kind, Project, and Account maps — stamping a kind default
217
- // here would look like an explicit choice and skip those layers. `none` is
218
- // still silent.
219
- const sound = flags.sound !== undefined ? flags.sound : config.sound.value
220
- if (sound !== null && sound !== undefined && !isCliSoundRef(sound)) {
221
- return {
222
- ok: false,
223
- error: unknownSoundMessage(sound),
224
- }
225
- }
226
- const level =
227
- flags.level !== undefined ? flags.level : config.interruption_level.value
228
- if (
229
- level !== null &&
230
- level !== undefined &&
231
- !INTERRUPTION_LEVELS.includes(level as (typeof INTERRUPTION_LEVELS)[number])
232
- ) {
233
- return { ok: false, error: `Unknown interruption level "${level}".` }
234
- }
235
- if (platform === 'android' && level !== null && level !== undefined) {
236
- return {
237
- ok: false,
238
- error:
239
- 'Android does not support caller-selected interruption levels. Remove --level or unset the configured interruption_level; kind, notification channels, and device settings own attention on Android.',
240
- }
241
- }
242
-
243
- const appleOptions: IosOptionsT & MacosOptionsT = {}
244
- const androidOptions: AndroidOptionsT = {}
245
- if (sound === 'none') {
246
- appleOptions.sound = null
247
- androidOptions.sound = null
248
- } else if (sound !== null && sound !== undefined) {
249
- appleOptions.sound = sound
250
- androidOptions.sound = sound
251
- }
252
- if (flags.threadId !== undefined) {
253
- appleOptions.thread_id = flags.threadId
254
- androidOptions.thread_id = flags.threadId
255
- }
256
- if (level !== null && level !== undefined) {
257
- appleOptions.interruption_level = level as (typeof INTERRUPTION_LEVELS)[number]
258
- }
259
- if (flags.data?.length) {
260
- const data: Record<string, string> = {}
261
- for (const pair of flags.data) {
262
- const eq = pair.indexOf('=')
263
- if (eq <= 0) return { ok: false, error: `--data expects key=value, got "${pair}".` }
264
- data[pair.slice(0, eq)] = pair.slice(eq + 1)
265
- }
266
- appleOptions.custom_data = data
267
- androidOptions.custom_data = data
268
- }
269
-
270
- const ttl = flags.ttl ?? config.ttl_seconds.value
271
- const collapse = flags.collapseKey ?? config.collapse_key.value
272
- if (
273
- collapse !== null &&
274
- collapse !== undefined &&
275
- Buffer.byteLength(collapse, 'utf8') > COLLAPSE_KEY_MAX_BYTES
276
- ) {
277
- return {
278
- ok: false,
279
- error: `--collapse-key must be at most ${COLLAPSE_KEY_MAX_BYTES} UTF-8 bytes.`,
280
- }
281
- }
282
-
283
- if (flags.projectless === true && flags.project !== undefined) {
284
- return { ok: false, error: 'Choose either --project or --projectless, not both.' }
285
- }
286
- const project = flags.projectless === true
287
- ? undefined
288
- : flags.project ?? config.project.value ?? invocation.inferredProject
289
-
290
- // Ids are derived from labels so an agent writing a one-liner does not have
291
- // to invent them, but they stay the stable thing it branches on afterwards.
292
- const choices = parseChoices(flags.choice)
293
- if (choices === 'invalid') {
294
- return {
295
- ok: false,
296
- error: CHOICE_USAGE,
297
- }
298
- }
299
- if (flags.multi && choices === null && flags.questions === undefined) {
300
- return { ok: false, error: '--multi needs answers to select between; add --choice.' }
301
- }
302
- // A single reply question is the first readable block of the canonical body.
303
- // Context may follow after a blank line without changing what the user answers.
304
- const derivedQuestion = bannerExcerpt(body.body).split('\n', 1)[0]!.trim()
305
- if (
306
- flags.reply &&
307
- flags.questions === undefined &&
308
- derivedQuestion.length > QUESTION_TEXT_MAX_LENGTH
309
- ) {
310
- return {
311
- ok: false,
312
- error:
313
- `Keep the question within ${QUESTION_TEXT_MAX_LENGTH} characters and put ` +
314
- 'the longer context after a blank line — the first paragraph is the question.',
315
- }
316
- }
317
-
318
- const hasAppleOptions = Object.keys(appleOptions).length > 0
319
- const hasAndroidOptions = Object.keys(androidOptions).length > 0
320
- let platformOptions: NonNullable<NotificationDraftT['platform']> | undefined
321
- if (flags.platform === undefined) {
322
- if (hasAppleOptions || hasAndroidOptions) {
323
- platformOptions = {
324
- ...(hasAppleOptions ? { ios: appleOptions, macos: appleOptions } : {}),
325
- ...(hasAndroidOptions ? { android: androidOptions } : {}),
326
- }
327
- }
328
- } else if (platform === 'android') {
329
- if (hasAndroidOptions) platformOptions = { android: androidOptions }
330
- } else if (hasAppleOptions) {
331
- platformOptions =
332
- platform === 'ios' ? { ios: appleOptions } : { macos: appleOptions }
333
- }
334
-
335
- const draft: NotificationDraftT = {
336
- schema_version: NOTIFICATION_SCHEMA_VERSION,
337
- ...(flags.kind !== undefined
338
- ? { kind: flags.kind as (typeof NOTIFICATION_KINDS)[number] }
339
- : {}),
340
- ...(flags.lifecycle !== undefined ? { lifecycle: flags.lifecycle } : {}),
341
- ...(project !== null && project !== undefined ? { project } : {}),
342
- ...(invocation.source !== undefined ? { source: invocation.source } : {}),
343
- presentation: {
344
- title: flags.title,
345
- body: body.body,
346
- ...(flags.subtitle !== undefined ? { subtitle: flags.subtitle } : {}),
347
- ...(media.length > 0 ? { media } : {}),
348
- },
349
- targets,
350
- delivery: { ttl_seconds: ttl, collapse_key: collapse },
351
- ...(flags.reply
352
- ? {
353
- reply: {
354
- expires_in_seconds: flags.replyWindow ?? config.reply_window_seconds.value,
355
- questions: flags.questions ?? [
356
- {
357
- id: 'q1',
358
- text: derivedQuestion,
359
- ...(choices !== null ? { choices } : {}),
360
- ...(flags.multi ? { multi: true } : {}),
361
- },
362
- ],
363
- },
364
- }
365
- : {}),
366
- ...(platformOptions !== undefined ? { platform: platformOptions } : {}),
367
- }
368
- return { ok: true, draft, platform }
369
- }
370
-
371
- function resolvePlatform(value: string | undefined): Platform | null {
372
- if (value === undefined) return 'ios'
373
- return (PLATFORMS as readonly string[]).includes(value) ? (value as Platform) : null
374
- }
375
-
376
- /** Human one-screen receipt summary; agents use --json instead. */
377
- export function formatReceipt(receipt: SubmissionReceipt): string {
378
- const lines: string[] = []
379
- const overall =
380
- receipt.overall === 'provider_accepted_all'
381
- ? 'accepted by the push provider for every device'
382
- : receipt.overall === 'provider_accepted_partial'
383
- ? 'accepted for some devices'
384
- : receipt.overall === 'provider_rejected_all'
385
- ? 'rejected for every device'
386
- : 'still in flight'
387
- lines.push(`request ${receipt.request_id}${receipt.replayed ? ' (replayed)' : ''}: ${overall}`)
388
- for (const d of receipt.deliveries) {
389
- const detail = d.provider_reason ? ` (${d.provider_reason})` : ''
390
- lines.push(` ${d.device_name}: ${d.state}${detail}`)
391
- }
392
- lines.push(
393
- ` device receipt is not assessed here; run notifai status ${receipt.request_id} for Companion Receipt evidence`,
394
- )
395
- for (const w of receipt.warnings) lines.push(` warning ${w.path}: ${w.message}`)
396
- return lines.join('\n')
397
- }
398
-
399
- /** Stable exit codes: 0 ok/pending, 1 rejected everywhere. */
400
- export function receiptExitCode(receipt: SubmissionReceipt): number {
401
- return receipt.overall === 'provider_rejected_all' ? 1 : 0
402
- }
403
-
404
- /**
405
- * One flag occurrence, one answer: `--choice Staging --choice Production` ->
406
- * `[{id:'staging',label:'Staging'}, ...]`.
407
- *
408
- * Commas are ordinary characters. The old single-value comma split produced
409
- * buttons the agent never intended ("Nothing, I'll review" became two answers)
410
- * and nothing downstream could detect it, so the delimiter form is gone
411
- * rather than guarded: a label is a label.
412
- */
413
- export function parseChoices(
414
- value: string[] | undefined,
415
- ): { id: string; label: string }[] | null | 'invalid' {
416
- if (value === undefined || value.length === 0) return null
417
- const labels = value.map((part) => part.trim()).filter((part) => part.length > 0)
418
- if (labels.length < 2 || labels.length > 6) return 'invalid'
419
- const choices = labels.map((label) => ({ id: slugify(label), label }))
420
- if (choices.some((choice) => choice.id === '')) return 'invalid'
421
- if (new Set(choices.map((choice) => choice.id)).size !== choices.length) return 'invalid'
422
- return choices
423
- }
424
-
425
- /** Message shared by every surface that accepts choice labels. */
426
- export const CHOICE_USAGE =
427
- 'Offer 2-6 answers, one --choice flag per answer, unique once slugified — ' +
428
- 'e.g. --choice "Yes, ship it" --choice "Not yet".'
429
-
430
- export function slugify(label: string): string {
431
- return label
432
- .toLowerCase()
433
- .replace(/[^a-z0-9]+/g, '-')
434
- .replace(/^-+|-+$/g, '')
435
- .slice(0, 32)
436
- }
@@ -1,61 +0,0 @@
1
- import type { HookHarness } from './hooks.js'
2
- import { Buffer } from 'node:buffer'
3
- import { GUIDANCE_CONTEXT_MAX_BYTES, boundedEffectiveGuidance } from './guidance-render.js'
4
-
5
- const ROOT_OWNERSHIP =
6
- 'Notifai is enabled for this Project. The effective, provenance-marked guidance below governs Notification Requests. ' +
7
- 'Missing CLI readiness is handled through `notifai init --json`.'
8
-
9
- export const WORKER_ACTIVATION_CONTEXT =
10
- 'Notifai worker context: an Agent Event is a meaningful occurrence in your work; a Notification Request is a deliberate User-visible message about one. Report Agent Events to the parent and do not send Notification Requests unless the parent explicitly delegated that ownership. If delegated, load the Notifai skill and run `notifai guidance` before composing one.'
11
-
12
- export const MISSING_LIFECYCLE_GUIDANCE_CONTEXT =
13
- 'Notifai is active, but lifecycle guidance could not be loaded. Run `notifai guidance` once before deciding whether or how to send a Notification Request.'
14
-
15
- function rootActivationContext(cwd: string, env: NodeJS.ProcessEnv): string {
16
- const guidance = boundedEffectiveGuidance({
17
- cwd,
18
- env,
19
- maxBytes: GUIDANCE_CONTEXT_MAX_BYTES - Buffer.byteLength(`${ROOT_OWNERSHIP}\n\n`, 'utf8'),
20
- })
21
- return `${ROOT_OWNERSHIP}\n\n${guidance.ok ? guidance.content : guidance.fallback}`
22
- }
23
-
24
- /** One lifecycle meaning, encoded for each harness output contract. */
25
- export function sessionActivationOutput(
26
- harness: HookHarness | undefined,
27
- hookEventName: 'SessionStart' | 'SubagentStart',
28
- cwd: string = process.cwd(),
29
- env: NodeJS.ProcessEnv = process.env,
30
- ): string | undefined {
31
- const context = hookEventName === 'SubagentStart'
32
- ? WORKER_ACTIVATION_CONTEXT
33
- : rootActivationContext(cwd, env)
34
- if (harness === 'opencode' || harness === 'openclaw') return context
35
- if (harness === 'cursor') {
36
- return JSON.stringify({ additional_context: context })
37
- }
38
- if (harness === 'claude-code' || harness === 'codex') {
39
- return JSON.stringify({
40
- hookSpecificOutput: {
41
- hookEventName,
42
- additionalContext: context,
43
- },
44
- })
45
- }
46
- return undefined
47
- }
48
-
49
- /**
50
- * Cursor currently accepts sessionStart context without reliably delivering it
51
- * to the model. Its native post-Stop follow-up is the narrow fallback and is
52
- * claimed once per conversation by the hook-state layer.
53
- */
54
- export function cursorStopActivationOutput(
55
- cwd: string = process.cwd(),
56
- env: NodeJS.ProcessEnv = process.env,
57
- ): string {
58
- return JSON.stringify({
59
- followup_message: rootActivationContext(cwd, env),
60
- })
61
- }
@@ -1,35 +0,0 @@
1
- import { existsSync, writeFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { withFileLock } from './file-lock.js'
4
-
5
- const workerMode = process.env['NOTIFAI_SESSION_END_LOG_LOCK_WORKER_MODE']
6
- const sleep = new Int32Array(new SharedArrayBuffer(4))
7
-
8
- function waitFor(file: string): void {
9
- const deadline = Date.now() + 15_000
10
- while (!existsSync(file)) {
11
- if (Date.now() >= deadline) throw new Error(`timed out waiting for ${file}`)
12
- Atomics.wait(sleep, 0, 0, 2)
13
- }
14
- }
15
-
16
- if (workerMode === undefined) {
17
- describe('SessionEnd log lock worker', () => {
18
- it('stays dormant outside the CLI deadline regression', () => {
19
- expect(workerMode).toBeUndefined()
20
- })
21
- })
22
- } else {
23
- describe('SessionEnd log lock worker', () => {
24
- it('holds a live bakery ticket until the parent releases it', () => {
25
- withFileLock(
26
- process.env['NOTIFAI_SESSION_END_LOG_LOCK_PATH']!,
27
- () => {
28
- writeFileSync(process.env['NOTIFAI_SESSION_END_LOG_LOCK_READY']!, workerMode)
29
- waitFor(process.env['NOTIFAI_SESSION_END_LOG_LOCK_RELEASE']!)
30
- },
31
- { waitMs: 15_000 },
32
- )
33
- })
34
- })
35
- }