@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
package/src/program.ts DELETED
@@ -1,818 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { Command } from 'commander'
3
- import {
4
- acknowledgeCommand,
5
- askCommand,
6
- accessStatusCommand,
7
- agentSessionRenameCommand,
8
- authStatusCommand,
9
- capabilitiesCommand,
10
- closeCommand,
11
- configExplainCommand,
12
- configSetCommand,
13
- configShowCommand,
14
- configUnsetCommand,
15
- devicesCommand,
16
- doctorCommand,
17
- guidanceSetCommand,
18
- guidanceShowCommand,
19
- guidanceUnsetCommand,
20
- hookDefersDiagnosticsUntilAfterCleanup,
21
- hookRunCommand,
22
- hooksInstallCommand,
23
- hooksUninstallCommand,
24
- initCommand,
25
- loginCommand,
26
- logoutCommand,
27
- projectDisableCommand,
28
- projectEnableCommand,
29
- projectStatusCommand,
30
- logsCommand,
31
- repliesCommand,
32
- reportAskFailure,
33
- sendCommand,
34
- soundsCommand,
35
- statusCommand,
36
- type CommandDeps,
37
- } from './commands.js'
38
- import { packageVersion } from './release.js'
39
- import { HOOK_INSTALLABLE_HARNESSES } from './harnesses.js'
40
- import type { Platform } from '@raidiant/notifai-protocol'
41
- import type { SkillScope } from './native-skills.js'
42
- import { GROUP, SEND_GROUP, helpConfiguration, rootHelpFooter } from './ui/help.js'
43
- import { readStdinWithTimeout } from './hook-input.js'
44
- import { argvFlagNames } from './logging.js'
45
-
46
- /**
47
- * One source of truth for the version: the manifest npm actually published.
48
- *
49
- * It was hardcoded here as well, and the first patch release proved why that
50
- * does not hold — `package.json` moved and `notifai --version` kept reporting
51
- * the old number, which is exactly the string someone pastes into a bug report
52
- * to say what they are running.
53
- *
54
- * `unknown` is the display form of "this build cannot tell"; the skill pin
55
- * derived from the same source refuses instead, because a wrong ref installs
56
- * the wrong thing while a wrong version string only misinforms.
57
- */
58
- function version(): string {
59
- return packageVersion() ?? 'unknown'
60
- }
61
-
62
- /**
63
- * The full command path, e.g. `config set`, so a filter on `cmd` distinguishes
64
- * subcommands that share a leaf name.
65
- */
66
- function commandPath(command: Command): string {
67
- const parts: string[] = []
68
- for (let node: Command | null = command; node?.parent != null; node = node.parent) {
69
- parts.unshift(node.name())
70
- }
71
- return parts.join(' ') || 'notifai'
72
- }
73
-
74
- /**
75
- * Every command implementation the tree dispatches to, injectable so a test
76
- * can parse real argv and assert the exact flags a command would receive —
77
- * the layer where `--no-wait` disentangling and collector defaults live, which
78
- * unit tests on the commands themselves can never see.
79
- */
80
- const defaultRunners = {
81
- init: initCommand,
82
- doctor: doctorCommand,
83
- login: loginCommand,
84
- logout: logoutCommand,
85
- authStatus: authStatusCommand,
86
- accessStatus: accessStatusCommand,
87
- devices: devicesCommand,
88
- sounds: soundsCommand,
89
- capabilities: capabilitiesCommand,
90
- projectEnable: projectEnableCommand,
91
- projectDisable: projectDisableCommand,
92
- projectStatus: projectStatusCommand,
93
- agentSessionRename: agentSessionRenameCommand,
94
- send: sendCommand,
95
- replies: repliesCommand,
96
- acknowledge: acknowledgeCommand,
97
- status: statusCommand,
98
- ask: askCommand,
99
- close: closeCommand,
100
- hookRun: hookRunCommand,
101
- hooksInstall: hooksInstallCommand,
102
- hooksUninstall: hooksUninstallCommand,
103
- logs: logsCommand,
104
- configShow: configShowCommand,
105
- configExplain: configExplainCommand,
106
- configSet: configSetCommand,
107
- configUnset: configUnsetCommand,
108
- guidanceShow: guidanceShowCommand,
109
- guidanceSet: guidanceSetCommand,
110
- guidanceUnset: guidanceUnsetCommand,
111
- /**
112
- * Dynamic so that the prompt library, the banner and the whole interactive
113
- * tree cost nothing to the paths that never draw them — `send`, `ask`, and
114
- * the hook that runs in front of every prompt the user types.
115
- */
116
- interactive: async (deps: CommandDeps, versionString: string): Promise<number> => {
117
- const { interactiveCommand } = await import('./interactive.js')
118
- return interactiveCommand(deps, versionString)
119
- },
120
- }
121
-
122
- export type ProgramRunners = typeof defaultRunners
123
-
124
- export interface BuildProgramOptions {
125
- /** Test seam; production ends the process with the command's exit code. */
126
- exit?: (code: number) => void
127
- runners?: Partial<ProgramRunners>
128
- }
129
-
130
- export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {}): Command {
131
- const exit = options.exit ?? ((code: number) => process.exit(code))
132
- const runners: ProgramRunners = { ...defaultRunners, ...options.runners }
133
- const logger = deps.logger
134
-
135
- const program = new Command('notifai')
136
- .description('Send native device notifications from agents and local programs')
137
- .version(version())
138
- .configureHelp(helpConfiguration)
139
- // Lazy on purpose. `addHelpText` also takes a string, but that builds the
140
- // footer on every invocation — including the hook that runs in front of
141
- // every prompt the user types, which never renders help at all.
142
- .addHelpText('after', () => rootHelpFooter())
143
- // Commander's default is to print the full help and exit 0 for an unknown
144
- // `help` topic, so `notifai help nonsense` looked like it had worked. A
145
- // reader who mistyped a command deserves to be told, and a script deserves
146
- // a nonzero code.
147
- .showSuggestionAfterError(true)
148
- // Commander drops the implicit `help [command]` as soon as the program
149
- // itself has an action handler — which the interactive default below gives
150
- // it. Without this, adding that default would silently break
151
- // `notifai help send` for everyone who reaches for it before `--help`.
152
- .helpCommand(true)
153
- .hook('preAction', (_program, actionCommand) => {
154
- logger?.bind({ cmd: commandPath(actionCommand) })
155
- // SessionEnd uses the hook policy shared with commands.ts: local cleanup
156
- // precedes every diagnostic that can wait on the shared log lock.
157
- const deferDiagnostics =
158
- actionCommand.name() === 'hook' &&
159
- hookDefersDiagnosticsUntilAfterCleanup(actionCommand.processedArgs[0])
160
- // Values only at `debug`; `cli.end` carries the flag names at every level.
161
- if (!deferDiagnostics) {
162
- logger?.debug('cli.start', {
163
- version: version(),
164
- flags: argvFlagNames(process.argv.slice(2)),
165
- cwd: process.cwd(),
166
- })
167
- }
168
- })
169
-
170
- /**
171
- * Commander exits 1 for every usage error it detects itself, which contradicts
172
- * the exit vocabulary this CLI documents and every hand-written usage error it
173
- * returns. A caller branching on exit codes should not have to know which layer
174
- * rejected it. Help and version stay successful.
175
- */
176
- program.exitOverride((err) => {
177
- exit(err.exitCode === 0 ? 0 : 2)
178
- })
179
-
180
- /**
181
- * Bare `notifai`.
182
- *
183
- * At a human terminal this opens the interactive app, which enters `init`
184
- * when the machine is not yet paired and otherwise offers daily actions.
185
- * Anywhere else it prints help exactly as before. The check is the same one
186
- * every other prompt in this CLI is gated on — stdin and stdout both TTYs,
187
- * not CI, `NOTIFAI_NO_INPUT` unset — because an agent that reaches a prompt
188
- * does not fail, it hangs for ever waiting on a stdin nobody is typing into.
189
- */
190
- /**
191
- * An unrecognised subcommand reaches the root as an excess argument, and
192
- * "expected 0 arguments but got 1" tells a caller nothing about what it typed
193
- * or what exists. Naming it — with the nearest real command when there is one —
194
- * turns a dead end into one more try.
195
- */
196
- program.usage('[options] [command]')
197
- program.argument('[command]').action(async (command?: string) => {
198
- if (command !== undefined) {
199
- const names = program.commands.map((cmd) => cmd.name())
200
- const near = names.filter(
201
- (name) =>
202
- name.startsWith(command.slice(0, 2)) ||
203
- command.startsWith(name.slice(0, 2)) ||
204
- name.includes(command) ||
205
- command.includes(name),
206
- )
207
- deps.io.err(
208
- `Unknown command "${command}".` +
209
- (near.length > 0 ? ` Did you mean: ${near.slice(0, 3).join(', ')}?` : '') +
210
- ' Run notifai --help for the full list.',
211
- )
212
- return exit(2)
213
- }
214
- if (deps.io.interactive !== true) {
215
- program.outputHelp()
216
- return
217
- }
218
- exit(await runners.interactive(deps, version()))
219
- })
220
-
221
- // Setup leads: `init` is the single entry command for first-run friendliness.
222
- program
223
- .command('init')
224
- .helpGroup(GROUP.start)
225
- .summary('Set this project up, step by step')
226
- .description(
227
- 'Set up Notifai here, idempotently: sign-in, project id, hooks, device readiness, live receipt proof. ' +
228
- 'Interactive at a human terminal; never prompts otherwise (agents: pass flags)',
229
- )
230
- .option('--project-id <id>', 'Project identifier slug (default: derived from the directory name)')
231
- .option('--json', 'machine-readable final readiness; never prompts')
232
- .option('--skills', 'install/update the agent skill from its pinned public release')
233
- .option('--no-skills', 'suppress the optional agent-skill status line')
234
- .option(
235
- '--setup-scope <scope>',
236
- 'unattended setup scope: project or global (skill, hooks, and config). The CLI binary is always a global install',
237
- )
238
- .option('--skills-scope <scope>', 'unattended skill scope; same values as --setup-scope')
239
- .option('--hooks', 'install harness hooks for registered-question routing')
240
- .option('--no-hooks', 'skip the hooks without being asked')
241
- .action(
242
- async (opts: {
243
- projectId?: string
244
- json?: boolean
245
- skills?: boolean
246
- setupScope?: SkillScope
247
- skillsScope?: SkillScope
248
- hooks?: boolean
249
- }) => {
250
- exit(await runners.init(deps, opts))
251
- },
252
- )
253
-
254
- program
255
- .command('doctor')
256
- .helpGroup(GROUP.daily)
257
- .summary('Check every part of the setup')
258
- .description('Audit config, credential, server, contract, device, hook, and saved receipt proof; exits nonzero when any line is FAIL (no live send)')
259
- .option('--json', 'machine-readable output')
260
- .action(async (opts: { json?: boolean }) => {
261
- exit(await runners.doctor(deps, opts))
262
- })
263
-
264
- const project = program
265
- .command('project')
266
- .helpGroup(GROUP.daily)
267
- .summary('Control Notifai for this Project')
268
- .description('Inspect or change this Project\'s User-owned lifecycle enablement')
269
- project.command('enable').description('Enable lifecycle guidance for this Project').action(() => {
270
- exit(runners.projectEnable(deps))
271
- })
272
- project.command('disable').description('Disable lifecycle guidance for this Project').action(() => {
273
- exit(runners.projectDisable(deps))
274
- })
275
- project.command('status').description('Show lifecycle enablement for this Project').option('--json').action((opts: { json?: boolean }) => {
276
- exit(runners.projectStatus(deps, opts.json === true))
277
- })
278
-
279
- const session = program
280
- .command('session')
281
- .helpGroup(GROUP.agent)
282
- .summary('Inspect or rename the current Agent Session')
283
- .description('Operate on the exact Agent Session owned by the active harness')
284
- session
285
- .command('rename <label>')
286
- .description(
287
- 'Rename this exact Agent Session; agents use this only after its job changes completely enough that the old label would mislead',
288
- )
289
- .option('--json', 'machine-readable output')
290
- .action(async (label: string, opts: { json?: boolean }) => {
291
- exit(await runners.agentSessionRename(deps, label, opts))
292
- })
293
-
294
- program
295
- .command('login')
296
- .helpGroup(GROUP.advanced)
297
- .summary('Sign in and pair this machine')
298
- .description('Pair this machine with your Notifai account via browser approval')
299
- .option('--name <name>', 'machine name shown in the dashboard (default: hostname)')
300
- .option('--base-url <url>', 'pairing override for the service origin (also NOTIFAI_BASE_URL)')
301
- .option('--no-open', 'do not open the approval page in a browser')
302
- .action(async (opts: { name?: string; baseUrl?: string; open?: boolean }) => {
303
- exit(await runners.login(deps, opts))
304
- })
305
-
306
- program
307
- .command('logout')
308
- .helpGroup(GROUP.advanced)
309
- .summary('Remove the stored machine credential')
310
- .description('Remove the stored machine credential')
311
- .action(() => {
312
- exit(runners.logout(deps))
313
- })
314
-
315
- const auth = program
316
- .command('auth')
317
- .description('Authentication helpers')
318
- .summary('Machine identity and account plan')
319
- .helpGroup(GROUP.advanced)
320
- auth
321
- .command('status')
322
- .description('Show the stored machine identity')
323
- .option('--json', 'machine-readable output')
324
- .action((opts: { json?: boolean }) => {
325
- exit(runners.authStatus(deps, opts))
326
- })
327
- auth
328
- .command('access')
329
- .description('Show the account plan and access decision')
330
- .option('--json', 'machine-readable output')
331
- .action(async (opts: { json?: boolean }) => {
332
- exit(await runners.accessStatus(deps, opts))
333
- })
334
-
335
- program
336
- .command('devices')
337
- .helpGroup(GROUP.daily)
338
- .summary('List your devices and whether they can receive')
339
- .description('List registered devices and their delivery readiness')
340
- .option('--platform <platform>', 'show only devices on one supported platform')
341
- .option('--json', 'machine-readable output')
342
- .action(async (opts: { json?: boolean; platform?: string }) => {
343
- exit(await runners.devices(deps, opts))
344
- })
345
-
346
- program
347
- .command('sounds')
348
- .helpGroup(GROUP.daily)
349
- .summary('List shipped and Account custom sounds')
350
- .description(
351
- 'List the sounds --sound and the saved sound key accept: shipped names and this Account\'s custom sounds',
352
- )
353
- .option('--json', 'machine-readable output')
354
- .action(async (opts: { json?: boolean }) => {
355
- exit(await runners.sounds(deps, opts))
356
- })
357
-
358
- program
359
- .command('capabilities')
360
- .helpGroup(GROUP.advanced)
361
- .summary('Show what a platform can render')
362
- .description('Show a platform capability contract')
363
- .option('--platform <platform>', 'platform to describe (default: ios)')
364
- .option('--json', 'machine-readable output')
365
- .action(async (opts: { json?: boolean; platform?: Platform }) => {
366
- exit(await runners.capabilities(deps, opts))
367
- })
368
-
369
- const send = program
370
- .command('send')
371
- .helpGroup(GROUP.agent)
372
- .summary('Send a notification')
373
- .description('Send a notification')
374
- .optionsGroup(SEND_GROUP.content)
375
- .requiredOption('--title <title>', 'brief title whose substance is immediately understandable')
376
- .option('--body <markdown>', 'canonical Markdown body')
377
- .option('--body-file <path>', 'read the canonical Markdown body from a file (use - for stdin)')
378
- .option(
379
- '--literal-backslash-n',
380
- 'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
381
- )
382
- .option('--subtitle <subtitle>', 'a plain-text line between the title and body')
383
- .option(
384
- '--image <path|url|media_id>',
385
- 'upload or attach an image in collection order (repeatable, maximum 8)',
386
- (v: string, all: string[] = []) => [...all, v],
387
- )
388
- .option(
389
- '--image-alt <text>',
390
- 'alt text paired with --image occurrences by position (repeatable)',
391
- (v: string, all: string[] = []) => [...all, v],
392
- )
393
- .optionsGroup(SEND_GROUP.routing)
394
- .option('--kind <kind>', 'what this is (required): update | done | failed | blocked (no User reply would resume it) — question is set by --reply')
395
- .option('--project <id>', 'Project identifier override (otherwise configured or inferred)')
396
- .option('--projectless', 'deliberately omit Project identity for this notification')
397
- .option('--device <id>', 'target a device id (repeatable)', (v: string, all: string[] = []) => [...all, v])
398
- .option('--all', 'target all routable devices (overrides configured devices)')
399
- .option('--session-id <id>', 'low-level automation override for an opaque exact Agent Session (env: NOTIFAI_SESSION_ID)')
400
- .option(
401
- '--session-label <text>',
402
- 'human Agent Session name; safe to repeat, with the first accepted name frozen (env: NOTIFAI_SESSION_LABEL)',
403
- )
404
- .optionsGroup(SEND_GROUP.presentation)
405
- .option('--collapse-key <key>', 'replace earlier notifications with the same key')
406
- .option('--thread-id <id>', 'group related notifications')
407
- .option('--ttl <seconds>', 'delivery window in seconds', (v: string) => Number(v))
408
- .option('--platform <platform>', 'limit optional fields to one supported platform')
409
- .optionsGroup(SEND_GROUP.reply)
410
- .option('--reply', 'enable the inline reply action and block for the answer')
411
- .option('--reply-timeout <seconds>', 'how long to wait for a reply (default: 900)', (v: string) => Number(v))
412
- .option(
413
- '--reply-window <seconds>',
414
- 'how long an answer is still accepted, 60-259200 (default: reply_window_seconds, a day)',
415
- (v: string) => Number(v),
416
- )
417
- .option(
418
- '--choice <label>',
419
- 'with --reply, ask a closed question; repeat the flag once per answer (2-6)',
420
- (v: string, all: string[] = []) => [...all, v],
421
- )
422
- .option('--multi', 'with --choice, several answers may be selected')
423
- .optionsGroup(SEND_GROUP.advanced)
424
- .option(
425
- '--sound <sound>',
426
- 'override saved sound: default | done | attention | alert | none, or a custom name or id from notifai sounds',
427
- )
428
- .option(
429
- '--level <level>',
430
- 'Apple-only interruption level: passive | active | time_sensitive (unsupported with --platform android)',
431
- )
432
- .option('--wait <seconds>', 'how long to wait for provider outcomes', (v: string) => Number(v))
433
- .option('--no-wait', 'return immediately after acceptance')
434
- .option('--data <key=value>', 'custom data (repeatable)', (v: string, all: string[] = []) => [...all, v])
435
- .option('--idempotency-key <key>', 'safe-retry key (default: random)')
436
- .option('--retry', 'explicitly retry one unresolved exact semantic send using its opaque saved attempt')
437
- .option(
438
- '--base-url <url>',
439
- 'pairing override for the service origin; ignored when this machine is already paired (also NOTIFAI_BASE_URL)',
440
- )
441
- .option('--json', 'print the full submission receipt as JSON')
442
- .action(async (opts: Record<string, unknown>) => {
443
- // commander maps --no-wait onto the same "wait" flag; disentangle.
444
- const noWait = opts['wait'] === false
445
- const wait = typeof opts['wait'] === 'number' ? opts['wait'] : undefined
446
- const sendOpts = { ...opts }
447
- // Commander collectors default to []; an empty list means "not passed".
448
- for (const key of ['choice', 'image', 'imageAlt']) {
449
- if (Array.isArray(sendOpts[key]) && sendOpts[key].length === 0) delete sendOpts[key]
450
- }
451
- const bodyFile = sendOpts['bodyFile']
452
- delete sendOpts['bodyFile']
453
- if (typeof bodyFile === 'string') {
454
- if (sendOpts['body'] !== undefined) {
455
- deps.io.err('Pass either --body or --body-file, not both.')
456
- return exit(2)
457
- }
458
- try {
459
- sendOpts['body'] = readFileSync(bodyFile === '-' ? 0 : bodyFile, 'utf8')
460
- } catch (err) {
461
- deps.io.err(`Could not read ${bodyFile}: ${String(err)}`)
462
- return exit(2)
463
- }
464
- }
465
- const flags = { ...sendOpts, noWait } as Parameters<typeof sendCommand>[1]
466
- if (wait === undefined) delete (flags as { wait?: number }).wait
467
- else flags.wait = wait
468
- exit(await runners.send(deps, flags))
469
- })
470
-
471
- send.addHelpText(
472
- 'after',
473
- `\nKind is required, and it selects the sound the notification arrives with: done rings the completion chime, failed the most insistent tone, blocked and question a distinct attention tone, update Device default.\nAn explicit --sound or saved sound preference outranks that default. --sound accepts a shipped name, an Account custom sound name or id from \`notifai sounds\`, or none. --level is Apple-only; Android attention is owned by kind, product channels, and device settings.\n`,
474
- )
475
-
476
- program
477
- .command('replies [request_id]')
478
- .helpGroup(GROUP.agent)
479
- .summary('Retrieve replies to a question')
480
- .description('Retrieve replies for a notification request')
481
- .option('--pending', 'use the pushed question pending for this Project and Agent Session')
482
- .option('--wait <seconds>', 'how long to wait for a reply', (v: string) => Number(v))
483
- .option('--after <seq>', 'return replies after this sequence number', (v: string) => Number(v))
484
- .option('--json', 'machine-readable output')
485
- .action(async (requestId: string | undefined, opts: { wait?: number; after?: number; json?: boolean; pending?: boolean }) => {
486
- exit(await runners.replies(deps, requestId, opts))
487
- })
488
-
489
- program
490
- .command('acknowledge <request_id>')
491
- .helpGroup(GROUP.agent)
492
- .summary("Tell the user what you'll do because of their reply")
493
- .description(
494
- 'Record the required Agent Acknowledgement for a replied-to notification request; never prompts',
495
- )
496
- .option('--text <text>', 'concrete work you will do because of the reply')
497
- .option('--json', 'machine-readable output')
498
- .action(async (requestId: string, opts: { text?: string; json?: boolean }) => {
499
- exit(await runners.acknowledge(deps, requestId, opts))
500
- })
501
-
502
- program
503
- .command('status <request_id>')
504
- .helpGroup(GROUP.agent)
505
- .summary('Show the evidence trail for a request')
506
- .description('Show the evidence trail for a notification request')
507
- .option('--json', 'machine-readable output')
508
- .action(async (requestId: string, opts: { json?: boolean }) => {
509
- exit(await runners.status(deps, requestId, opts))
510
- })
511
-
512
- program
513
- .command('ask [question]')
514
- .helpGroup(GROUP.agent)
515
- .summary('Register a question, then end the turn')
516
- .description('Register a question for the turn-end hook to route to your devices, subject to your question-routing settings')
517
- .option(
518
- '--choice <label>',
519
- 'answers to offer instead of free text; repeat the flag once per answer (2-6)',
520
- (v: string, all: string[] = []) => [...all, v],
521
- )
522
- .option('--multi', 'with --choice, several answers may be selected')
523
- .option('--json', 'machine-readable output')
524
- .option('--body <markdown>', 'optional Markdown context appended after the question block')
525
- .option('--body-file <path>', 'read optional Markdown context from a file (use - for stdin)')
526
- .option(
527
- '--literal-backslash-n',
528
- 'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
529
- )
530
- .option('--form <path>', 'ask several questions as one form; JSON file (use - for stdin)')
531
- .option(
532
- '--image <path|url|media_id>',
533
- 'upload or attach an image in collection order (repeatable, maximum 8)',
534
- (v: string, all: string[] = []) => [...all, v],
535
- )
536
- .option(
537
- '--image-alt <text>',
538
- 'alt text paired with --image occurrences by position (repeatable)',
539
- (v: string, all: string[] = []) => [...all, v],
540
- )
541
- .option('--project <id>', 'Project identifier override (otherwise configured or inferred)')
542
- .option(
543
- '--session-label <text>',
544
- 'human Agent Session name; safe to repeat, with the first accepted name frozen (env: NOTIFAI_SESSION_LABEL)',
545
- )
546
- .action(async (question: string | undefined, opts: {
547
- choice?: string[]
548
- multi?: boolean
549
- json?: boolean
550
- body?: string
551
- bodyFile?: string
552
- literalBackslashN?: boolean
553
- form?: string
554
- image?: string[]
555
- imageAlt?: string[]
556
- project?: string
557
- sessionLabel?: string
558
- }) => {
559
- let body = opts.body
560
- if (typeof opts.bodyFile === 'string') {
561
- if (body !== undefined) {
562
- return exit(reportAskFailure(deps, opts, {
563
- code: 'invalid_input', check_id: 'body', exit_code: 2,
564
- remedy: 'pass either --body or --body-file, then retry',
565
- message: 'Pass either --body or --body-file, not both.',
566
- }))
567
- }
568
- try {
569
- body = readFileSync(opts.bodyFile === '-' ? 0 : opts.bodyFile, 'utf8')
570
- } catch (err) {
571
- return exit(reportAskFailure(deps, opts, {
572
- code: 'input_unreadable', check_id: 'body_file', exit_code: 2,
573
- remedy: 'fix the body-file path or permissions, then retry',
574
- message: `Could not read ${opts.bodyFile}: ${String(err)}`,
575
- }))
576
- }
577
- }
578
- let form: string | undefined
579
- if (typeof opts.form === 'string') {
580
- try {
581
- form = readFileSync(opts.form === '-' ? 0 : opts.form, 'utf8')
582
- } catch (err) {
583
- return exit(reportAskFailure(deps, opts, {
584
- code: 'input_unreadable', check_id: 'form_file', exit_code: 2,
585
- remedy: 'fix the form-file path or permissions, then retry',
586
- message: `Could not read ${opts.form}: ${String(err)}`,
587
- }))
588
- }
589
- }
590
- // Commander collectors default to []; an empty list means "not passed".
591
- const flags: Parameters<typeof askCommand>[2] = {
592
- ...(opts.choice?.length ? { choice: opts.choice } : {}),
593
- ...(opts.multi ? { multi: true } : {}),
594
- ...(body !== undefined ? { body } : {}),
595
- ...(form !== undefined ? { form } : {}),
596
- ...(opts.image?.length ? { image: opts.image } : {}),
597
- ...(opts.imageAlt?.length ? { imageAlt: opts.imageAlt } : {}),
598
- ...(opts.project !== undefined ? { project: opts.project } : {}),
599
- ...(opts.sessionLabel !== undefined ? { sessionLabel: opts.sessionLabel } : {}),
600
- ...(opts.json === true ? { json: true } : {}),
601
- ...(opts.literalBackslashN === true ? { literalBackslashN: true } : {}),
602
- }
603
- exit(await runners.ask(deps, question, flags))
604
- })
605
-
606
- program
607
- .command('close [request_id]')
608
- .helpGroup(GROUP.agent)
609
- .summary('Retire a question so late answers are rejected')
610
- .description(
611
- 'Retire a question so late answers are rejected rather than lost. Pass --pending to withdraw this Agent Session\'s outstanding registrations, including ones the turn-end hook has not pushed yet.',
612
- )
613
- .option('--pending', 'retire this Agent Session\'s outstanding questions, including ones not yet pushed')
614
- .option('--json', 'machine-readable output')
615
- .action(async (requestId: string | undefined, opts: { json?: boolean; pending?: boolean }) => {
616
- exit(await runners.close(deps, requestId, opts))
617
- })
618
-
619
- // Hidden, not removed. It is an entry point the hook installer writes into a
620
- // harness config file, never something a person types — and listing it beside
621
- // `send` invited exactly one reading, which is that you were supposed to.
622
- program
623
- .command('hook <event>', { hidden: true })
624
- .description('Internal: run a harness hook (reads hook JSON on stdin)')
625
- // Inert, and the point of it is that it is inert: the installed command line
626
- // carries a marker that says "Notifai wrote this" independently of which
627
- // checkout wrote it.
628
- .option('--owner <name>', 'internal ownership marker')
629
- .option('--harness <name>', 'internal harness output adapter')
630
- .action(async (event: string, opts: { harness?: string }) => {
631
- const harness = HOOK_INSTALLABLE_HARNESSES.find((candidate) => candidate === opts.harness)
632
- exit(await runners.hookRun(deps, event, () => readStdinWithTimeout(), harness))
633
- })
634
-
635
- const hooks = program
636
- .command('hooks')
637
- .description('Install harness hooks for registered-question routing')
638
- .summary('Wire a harness to route questions to your devices')
639
- .helpGroup(GROUP.advanced)
640
- hooks
641
- .command('install')
642
- .description('Wire this harness to route registered questions to your devices')
643
- .option(
644
- '--harness <name>',
645
- 'claude-code | codex | cursor | opencode | openclaw (default: every detected harness)',
646
- )
647
- .option('--global', 'install for every project instead of just this one')
648
- .action((opts: { harness?: string; global?: boolean }) => {
649
- exit(runners.hooksInstall(deps, opts))
650
- })
651
- hooks
652
- .command('uninstall')
653
- .description('Remove the hooks this CLI installed')
654
- .option('--harness <name>', 'claude-code | codex | cursor | opencode | openclaw (default: detected)')
655
- .option('--global', 'remove the machine-wide install')
656
- .action((opts: { harness?: string; global?: boolean }) => {
657
- exit(runners.hooksUninstall(deps, opts))
658
- })
659
-
660
- program
661
- .command('logs')
662
- .helpGroup(GROUP.daily)
663
- .summary('Show what this machine recorded about what it did')
664
- .description(
665
- 'Read the local record of commands, notifications, and the question decisions harness hooks made. ' +
666
- 'Bounded and scoped to this project by default; never leaves this machine',
667
- )
668
- .option('-n, --limit <count>', 'how many records to show (default: 30)', (v: string) => Number(v))
669
- .option('--all', 'lift the default limit, up to a hard cap')
670
- .option('--since <when>', 'only records newer than this: 10m, 2h, 1d, or an ISO 8601 instant')
671
- .option('--level <level>', 'minimum severity: error | info | debug (error shows only failures)')
672
- .option('--event <name>', 'only this event; repeatable', (v: string, all: string[] = []) => [...all, v])
673
- .option('--run <id>', 'everything one command invocation did')
674
- .option('--request <id>', 'everything about one notification request')
675
- .option('--session <id>', 'only this Agent Session')
676
- .option('--project <id>', 'only this Project')
677
- .option('--all-projects', 'do not scope to the Project in this directory')
678
- .option('--grep <text>', 'only records containing this text')
679
- .option('--json', 'one JSON record per line on stdout')
680
- .option('--path', 'print the log file paths instead of the records')
681
- .option('--clear', 'delete the log files')
682
- .action((opts: Record<string, unknown>) => {
683
- const flags = { ...opts } as Parameters<typeof logsCommand>[1]
684
- if (Array.isArray(flags.event) && flags.event.length === 0) delete flags.event
685
- exit(runners.logs(deps, flags))
686
- })
687
-
688
- const config = program
689
- .command('config')
690
- .description('Show or change settings')
691
- .summary('Show or change settings')
692
- .helpGroup(GROUP.daily)
693
- config
694
- .command('show')
695
- .description('Show every setting, what it does, and where its value came from')
696
- .option('--explain', 'include advanced settings and the file each value came from')
697
- .option('--plain', 'the flat key = value form, even at a terminal')
698
- .option('--json', 'machine-readable output')
699
- .action((opts: { json?: boolean; explain?: boolean; plain?: boolean }) => {
700
- exit(runners.configShow(deps, opts))
701
- })
702
- config
703
- .command('explain <key>')
704
- .description('Explain one setting in full: what it does, what it accepts, what it is now')
705
- .option('--json', 'machine-readable output')
706
- .action((key: string, opts: { json?: boolean }) => {
707
- exit(runners.configExplain(deps, key, opts))
708
- })
709
- config
710
- .command('set <key> <value>')
711
- .description(
712
- 'Set a configuration value (choose a layer interactively; unattended defaults machine-global and requires --yes)',
713
- )
714
- .option('--project', 'write to the shared .notifai/config.toml instead')
715
- .option('--local', 'write a personal project preference on this machine (not in the repo)')
716
- .option('--session <id>', 'apply only to one Agent Session')
717
- .option('--yes', 'skip the confirmation gate')
718
- .action(
719
- async (
720
- key: string,
721
- value: string,
722
- opts: { project?: boolean; local?: boolean; session?: string; yes?: boolean },
723
- ) => {
724
- exit(await runners.configSet(deps, key, value, opts))
725
- },
726
- )
727
- config
728
- .command('unset <key>')
729
- .description(
730
- 'Remove a configuration value so the next layer or shipped default applies (choose a layer interactively; unattended defaults machine-global and requires --yes)',
731
- )
732
- .option('--project', 'remove from the shared .notifai/config.toml instead')
733
- .option('--local', 'remove a personal project preference stored on this machine')
734
- .option('--session <id>', 'remove from one Agent Session')
735
- .option('--yes', 'skip the confirmation gate')
736
- .action(
737
- async (
738
- key: string,
739
- opts: { project?: boolean; local?: boolean; session?: string; yes?: boolean },
740
- ) => {
741
- exit(await runners.configUnset(deps, key, opts))
742
- },
743
- )
744
-
745
- const guidance = program
746
- .command('guidance')
747
- .description('How notifications are written: the shipped guidance and your overrides, per topic')
748
- .summary('Show or change how notifications are written')
749
- .helpGroup(GROUP.daily)
750
- guidance
751
- .command('show', { isDefault: true })
752
- .description('Print the effective guidance, every topic under the layer that supplied it')
753
- .option('--json', 'machine-readable output')
754
- .action((opts: { json?: boolean }) => {
755
- exit(runners.guidanceShow(deps, opts))
756
- })
757
- guidance
758
- .command('set <topic> [text]')
759
- .description(
760
- 'Write one guidance topic in your words; it replaces the shipped topic of the same name for that scope ' +
761
- '(choose a layer interactively; unattended defaults machine-global and requires --yes)',
762
- )
763
- .option('--file <path>', 'read the guidance text from a file (use - for stdin)')
764
- .option('--project', 'write to the shared .notifai/guidance/ instead')
765
- .option('--local', 'write a personal project preference on this machine (not in the repo)')
766
- .option('--yes', 'skip the confirmation gate')
767
- .action(
768
- async (
769
- topic: string,
770
- text: string | undefined,
771
- opts: { file?: string; project?: boolean; local?: boolean; yes?: boolean },
772
- ) => {
773
- let content = text
774
- if (typeof opts.file === 'string') {
775
- if (content !== undefined) {
776
- deps.io.err('Pass either the text or --file, not both.')
777
- exit(2)
778
- return
779
- }
780
- try {
781
- content = readFileSync(opts.file === '-' ? 0 : opts.file, 'utf8')
782
- } catch (err) {
783
- deps.io.err(`Could not read ${opts.file}: ${String(err)}`)
784
- exit(2)
785
- return
786
- }
787
- }
788
- if (content === undefined) {
789
- deps.io.err('Pass the guidance text, or --file <path|-> to read it.')
790
- exit(2)
791
- return
792
- }
793
- exit(
794
- await runners.guidanceSet(deps, topic, content, {
795
- project: opts.project,
796
- local: opts.local,
797
- yes: opts.yes,
798
- }),
799
- )
800
- },
801
- )
802
- guidance
803
- .command('unset <topic>')
804
- .description(
805
- 'Remove a guidance override so the next layer or the shipped topic applies ' +
806
- '(choose a layer interactively; unattended defaults machine-global and requires --yes)',
807
- )
808
- .option('--project', 'remove from the shared .notifai/guidance/ instead')
809
- .option('--local', 'remove a personal project preference stored on this machine')
810
- .option('--yes', 'skip the confirmation gate')
811
- .action(
812
- async (topic: string, opts: { project?: boolean; local?: boolean; yes?: boolean }) => {
813
- exit(await runners.guidanceUnset(deps, topic, opts))
814
- },
815
- )
816
-
817
- return program
818
- }