@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,336 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- rmSync,
6
- writeFileSync,
7
- } from 'node:fs'
8
- import path from 'node:path'
9
- import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
10
- import { ensurePrivateDirectory } from './atomic-file.js'
11
- import { acceptedValues, configInfo } from './config-schema.js'
12
- import {
13
- BOOLEAN_CONFIG_KEYS,
14
- CONFIG_KEYS,
15
- NUMERIC_CONFIG_KEYS,
16
- ORIGIN_LIST_CONFIG_KEYS,
17
- USER_OWNED_CONFIG_KEYS,
18
- configBounds,
19
- configDefaultValue,
20
- findProjectConfigPath,
21
- globalConfigPath,
22
- personalProjectConfigPath,
23
- sessionConfigPath,
24
- type ConfigKey,
25
- } from './config.js'
26
- import { normalizeOrigin } from './url-policy.js'
27
- import { renderConfigExplain, renderConfigList, renderConfigPlain } from './ui/config-view.js'
28
- import { EXIT, loadLoggedConfig, type CommandDeps } from './commands-core.js'
29
- import { isCliSoundRef } from './sound-ref.js'
30
-
31
- // ---------------------------------------------------------------------------
32
- // config show / set / unset
33
- // ---------------------------------------------------------------------------
34
-
35
- export function configShowCommand(
36
- deps: CommandDeps,
37
- flags: { json?: boolean; explain?: boolean; plain?: boolean },
38
- ): number {
39
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
40
- if (flags.json) {
41
- const output = Object.fromEntries(
42
- CONFIG_KEYS.map((key) => [
43
- key,
44
- {
45
- value: config[key].value,
46
- source: config[key].source,
47
- summary: configInfo(key).summary,
48
- },
49
- ]),
50
- )
51
- deps.io.out(JSON.stringify(output, null, 2))
52
- return EXIT.ok
53
- }
54
- // Anything that is not a person at a terminal keeps the flat `key = value`
55
- // form it has always had. Scripts parse this, and a prettier layout for an
56
- // audience that cannot see it would only be a breaking change.
57
- if (deps.io.interactive !== true || flags.plain === true) {
58
- for (const line of renderConfigPlain(config, flags.explain === true)) deps.io.out(line)
59
- return EXIT.ok
60
- }
61
- for (const line of renderConfigList(config, { showAdvanced: flags.explain === true })) {
62
- deps.io.out(line)
63
- }
64
- return EXIT.ok
65
- }
66
-
67
- /**
68
- * One setting, explained in full.
69
- *
70
- * The gap this closes: `config show` prints a value and provenance but that is
71
- * not enough to explain the setting's consequences. Every key already had a
72
- * careful explanation — in a TypeScript comment, read by everyone except the
73
- * person who needed it.
74
- */
75
- export function configExplainCommand(
76
- deps: CommandDeps,
77
- key: string,
78
- flags: { json?: boolean } = {},
79
- ): number {
80
- if (!(CONFIG_KEYS as readonly string[]).includes(key)) {
81
- deps.io.err(`Unknown setting "${key}".`)
82
- deps.io.err(`Valid settings: ${CONFIG_KEYS.join(', ')}`)
83
- return EXIT.usage
84
- }
85
- const configKey = key as ConfigKey
86
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
87
- const info = configInfo(configKey)
88
- const entry = config[configKey]
89
-
90
- if (flags.json) {
91
- deps.io.out(
92
- JSON.stringify(
93
- {
94
- key: configKey,
95
- label: info.label,
96
- group: info.group,
97
- kind: info.kind,
98
- summary: info.summary,
99
- detail: info.detail,
100
- accepts: acceptedValues(configKey),
101
- ...(info.choices !== undefined ? { choices: info.choices } : {}),
102
- value: entry.value,
103
- source: entry.source,
104
- },
105
- null,
106
- 2,
107
- ),
108
- )
109
- return EXIT.ok
110
- }
111
-
112
- if (deps.io.interactive !== true) {
113
- deps.io.out(`${configKey} = ${JSON.stringify(entry.value)} [${entry.source}]`)
114
- deps.io.out(info.detail.replace(/\n\n/g, '\n'))
115
- deps.io.out(`accepts: ${acceptedValues(configKey)}`)
116
- return EXIT.ok
117
- }
118
- for (const line of renderConfigExplain(configKey, config)) deps.io.out(line)
119
- return EXIT.ok
120
- }
121
-
122
- /**
123
- * Closest config key by edit distance, or null when nothing is close.
124
- *
125
- * The threshold matters more than the algorithm: suggesting a key that shares
126
- * three letters with the typo sends the reader to the wrong setting with
127
- * confidence, which is worse than listing all fourteen and letting them look.
128
- */
129
- function nearestKey(input: string): string | null {
130
- let best: string | null = null
131
- let bestDistance = Number.POSITIVE_INFINITY
132
- for (const candidate of CONFIG_KEYS) {
133
- const distance = editDistance(input.toLowerCase(), candidate)
134
- if (distance < bestDistance) {
135
- bestDistance = distance
136
- best = candidate
137
- }
138
- }
139
- return best !== null && bestDistance <= Math.max(2, Math.floor(best.length / 3)) ? best : null
140
- }
141
-
142
- function editDistance(a: string, b: string): number {
143
- const previous = Array.from({ length: b.length + 1 }, (_, i) => i)
144
- for (let i = 1; i <= a.length; i += 1) {
145
- let diagonal = previous[0]!
146
- previous[0] = i
147
- for (let j = 1; j <= b.length; j += 1) {
148
- const above = previous[j]!
149
- previous[j] = Math.min(
150
- previous[j]! + 1,
151
- previous[j - 1]! + 1,
152
- diagonal + (a[i - 1] === b[j - 1] ? 0 : 1),
153
- )
154
- diagonal = above
155
- }
156
- }
157
- return previous[b.length]!
158
- }
159
-
160
- export async function configSetCommand(
161
- deps: CommandDeps,
162
- key: string,
163
- rawValue: string,
164
- flags: { project?: boolean; local?: boolean; session?: string; yes?: boolean },
165
- ): Promise<number> {
166
- const configKey = configMutationKey(deps, key)
167
- if (configKey === null) return EXIT.usage
168
- const info = configInfo(configKey)
169
- let value: unknown = rawValue
170
- if (NUMERIC_CONFIG_KEYS.includes(configKey)) {
171
- const numeric = Number(rawValue)
172
- if (!Number.isInteger(numeric)) {
173
- deps.io.err(`${key} takes ${acceptedValues(configKey)}, not "${rawValue}".`)
174
- return EXIT.usage
175
- }
176
- const bounds = configBounds(configKey)
177
- if (bounds !== undefined && (numeric < bounds.min || numeric > bounds.max)) {
178
- deps.io.err(`${key} must be between ${bounds.min} and ${bounds.max}.`)
179
- return EXIT.usage
180
- }
181
- value = numeric
182
- }
183
- if (BOOLEAN_CONFIG_KEYS.includes(configKey)) {
184
- if (rawValue !== 'true' && rawValue !== 'false') {
185
- deps.io.err(`${key} is a toggle — pass "true" or "false", not "${rawValue}".`)
186
- return EXIT.usage
187
- }
188
- value = rawValue === 'true'
189
- }
190
- // Sound is listed as an enum of shipped names so the interactive picker has
191
- // choices, but it also accepts an Account custom name or id. Other enum keys
192
- // stay closed: an unrecognised value used to be written and only fail at send.
193
- if (configKey === 'sound') {
194
- if (!isCliSoundRef(rawValue)) {
195
- deps.io.err(`${key} takes ${acceptedValues(configKey)} — not "${rawValue}".`)
196
- return EXIT.usage
197
- }
198
- } else if (info.kind === 'enum' && info.choices !== undefined && !info.choices.includes(rawValue)) {
199
- deps.io.err(`${key} takes one of: ${info.choices.join(', ')} — not "${rawValue}".`)
200
- return EXIT.usage
201
- }
202
- if (info.kind === 'list') value = rawValue.split(',').map((s) => s.trim()).filter(Boolean)
203
- if (ORIGIN_LIST_CONFIG_KEYS.includes(configKey)) {
204
- const origins = (value as string[]).map((entry) => normalizeOrigin(entry))
205
- const rejected = (value as string[]).filter((_, index) => origins[index] === null)
206
- if (rejected.length > 0) {
207
- deps.io.err(
208
- `${key} entries must be bare http(s) origins like "https://host" or "http://host:8080" — not ${rejected.map((entry) => `"${entry}"`).join(', ')}.`,
209
- )
210
- return EXIT.usage
211
- }
212
- value = origins
213
- }
214
-
215
- const target = await configMutationTarget(deps, flags)
216
- if (target === null) return EXIT.usage
217
- if (target.layer === 'project' && USER_OWNED_CONFIG_KEYS.includes(configKey)) {
218
- deps.io.err(
219
- `${key} widens which origins this machine trusts, so it is never read from the repository's shared config — a cloned repository must not be able to set it.`,
220
- )
221
- deps.io.err('Store it on this machine (no flag) or for this project personally (--local).')
222
- return EXIT.usage
223
- }
224
- if (target.layer === 'global' && Object.is(value, configDefaultValue(configKey))) {
225
- deps.io.err(`${key} is already the shipped default (${JSON.stringify(value)}).`)
226
- deps.io.err(
227
- `Run \`notifai config unset ${key} --yes\` to remove a redundant override instead of creating one.`,
228
- )
229
- return EXIT.usage
230
- }
231
-
232
- if (!flags.yes) {
233
- const confirmed = await deps.io.confirm(`Set ${key} = ${JSON.stringify(value)} in ${target.path}?`)
234
- if (!confirmed) {
235
- deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
236
- return EXIT.usage
237
- }
238
- }
239
-
240
- const existing = existsSync(target.path)
241
- ? (parseToml(readFileSync(target.path, 'utf8')) as Record<string, unknown>)
242
- : {}
243
- existing[key] = value
244
- if (target.layer === 'project') mkdirSync(path.dirname(target.path), { recursive: true })
245
- else ensurePrivateDirectory(path.dirname(target.path))
246
- writeFileSync(target.path, `${stringifyToml(existing)}\n`)
247
- deps.io.out(`Wrote ${key} to ${target.path}`)
248
- return EXIT.ok
249
- }
250
-
251
- function configMutationKey(deps: CommandDeps, key: string): ConfigKey | null {
252
- if (!(CONFIG_KEYS as readonly string[]).includes(key)) {
253
- deps.io.err(`Unknown setting "${key}".`)
254
- const near = nearestKey(key)
255
- if (near !== null) deps.io.err(`Did you mean "${near}"?`)
256
- deps.io.err(`Valid settings: ${CONFIG_KEYS.join(', ')}`)
257
- deps.io.err('Run `notifai config explain <key>` to see what one of them does.')
258
- return null
259
- }
260
- return key as ConfigKey
261
- }
262
-
263
- type ConfigMutationFlags = { project?: boolean; local?: boolean; session?: string; yes?: boolean }
264
- type ConfigMutationLayer = 'global' | 'project' | 'local' | 'session'
265
-
266
- async function configMutationTarget(
267
- deps: CommandDeps,
268
- flags: ConfigMutationFlags,
269
- ): Promise<{ path: string; layer: ConfigMutationLayer } | null> {
270
- let layer = flags.local ? 'local' : flags.project ? 'project' : 'global'
271
- if (
272
- flags.session === undefined &&
273
- flags.local !== true &&
274
- flags.project !== true &&
275
- flags.yes !== true &&
276
- deps.io.interactive === true &&
277
- deps.io.select
278
- ) {
279
- const selected = await deps.io.select('Where should this setting live?', [
280
- { value: 'global', label: 'This machine', hint: 'applies across projects' },
281
- { value: 'project', label: 'This project (shared)', hint: '.notifai/config.toml' },
282
- { value: 'local', label: 'This project (personal)', hint: 'stored on this machine, not in the repo' },
283
- ])
284
- if (selected === null) {
285
- deps.io.err('No configuration layer selected.')
286
- return null
287
- }
288
- layer = selected
289
- }
290
-
291
- const targetPath = flags.session
292
- ? sessionConfigPath(flags.session, deps.env)
293
- : layer === 'local'
294
- ? personalProjectConfigPath(deps.cwd, deps.env)
295
- : layer === 'project'
296
- ? (findProjectConfigPath(deps.cwd) ?? path.join(deps.cwd, '.notifai', 'config.toml'))
297
- : globalConfigPath(deps.env)
298
- return {
299
- path: targetPath,
300
- layer: flags.session ? 'session' : (layer as Exclude<ConfigMutationLayer, 'session'>),
301
- }
302
- }
303
-
304
- export async function configUnsetCommand(
305
- deps: CommandDeps,
306
- key: string,
307
- flags: ConfigMutationFlags,
308
- ): Promise<number> {
309
- const configKey = configMutationKey(deps, key)
310
- if (configKey === null) return EXIT.usage
311
- const target = await configMutationTarget(deps, flags)
312
- if (target === null) return EXIT.usage
313
- const existing = existsSync(target.path)
314
- ? (parseToml(readFileSync(target.path, 'utf8')) as Record<string, unknown>)
315
- : {}
316
- if (!Object.prototype.hasOwnProperty.call(existing, configKey)) {
317
- deps.io.out(`${configKey} is not set in ${target.path}`)
318
- return EXIT.ok
319
- }
320
- if (!flags.yes) {
321
- const confirmed = await deps.io.confirm(`Remove ${configKey} from ${target.path}?`)
322
- if (!confirmed) {
323
- deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
324
- return EXIT.usage
325
- }
326
- }
327
-
328
- delete existing[configKey]
329
- if (Object.keys(existing).length === 0) {
330
- rmSync(target.path, { force: true })
331
- } else {
332
- writeFileSync(target.path, `${stringifyToml(existing)}\n`)
333
- }
334
- deps.io.out(`Removed ${configKey} from ${target.path}; the inherited value now applies.`)
335
- return EXIT.ok
336
- }
@@ -1,298 +0,0 @@
1
- import { SHIPPED_CLI_CAPABILITIES, type RecoveryAction } from '@raidiant/notifai-protocol'
2
- import { type ClaudeWakeAdapters } from './claude-wake.js'
3
- import {
4
- ApiCallError,
5
- NetworkError,
6
- createClient,
7
- type ApiClient,
8
- type ClientOptions,
9
- } from './client.js'
10
- import { type CodexWakeAdapters } from './codex-wake.js'
11
- import { loadConfig, type CliConfig } from './config.js'
12
- import type { CredentialStore, MachineCredential } from './credentials.js'
13
- import { type HookAdapterTarget } from './hook-adapter.js'
14
- import { logConfigResolved, logSettingsFrom, nullLogger, type Logger } from './logging.js'
15
- import type { NativeSkills } from './native-skills.js'
16
- import type { OrcaSessionTitleLookup } from './orca-session-title.js'
17
- import { packageVersion } from './release.js'
18
- import type { Tone } from './ui/theme.js'
19
-
20
- export interface CommandIo {
21
- out(line: string): void
22
- err(line: string): void
23
- /** Interactive confirmation; resolves `fallback` (default false) when not interactive. */
24
- confirm(question: string, fallback?: boolean): Promise<boolean>
25
- openUrl(url: string): void
26
- /**
27
- * True only when a human is demonstrably driving a terminal. Everything below
28
- * is optional sugar that MUST only be called behind this flag: an agent that
29
- * reaches an interactive prompt does not error, it hangs — the prompt
30
- * libraries wait on stdin for ever — so the gate is bypass, not handling.
31
- * Test fakes leave all of this undefined and exercise the plain paths.
32
- */
33
- interactive?: boolean
34
- select?(message: string, options: { value: string; label: string; hint?: string }[]): Promise<string | null>
35
- /** Interactive multi-select. `initial` are pre-selected values. */
36
- multiselect?(
37
- message: string,
38
- options: { value: string; label: string; hint?: string }[],
39
- initial?: string[],
40
- ): Promise<string[] | null>
41
- intro?(title: string): Promise<void>
42
- outro?(message: string): Promise<void>
43
- note?(message: string, title?: string): Promise<void>
44
- spinner?(message: string): Promise<CommandSpinner | null>
45
- /**
46
- * A report line. `tone` distinguishes the two states that are neither pass
47
- * nor fail — something optional the user declined, and something that was
48
- * never evaluated — which a boolean has to round to one or the other.
49
- * Optional so existing implementations keep type-checking.
50
- */
51
- check?(ok: boolean, message: string, tone?: Tone): Promise<void>
52
- }
53
-
54
- export interface CommandSpinner {
55
- message(message: string): void
56
- stop(message: string): void
57
- error(message: string): void
58
- }
59
-
60
- export interface CommandDeps {
61
- io: CommandIo
62
- store: CredentialStore
63
- env: NodeJS.ProcessEnv
64
- cwd: string
65
- /** Test seam; production fixes the hook adapter under os.homedir(). */
66
- hookAdapterHome?: string
67
- /** Test seam; production uses this process's Node and CLI paths. */
68
- hookInstallTarget?: HookAdapterTarget
69
- /** Test seam; production uses process.platform for adapter format and quoting. */
70
- hookPlatform?: NodeJS.Platform
71
- /** Test seam; production uses fetch against base_url. */
72
- clientFactory?: (baseUrl: string, bearer: string | null, options?: ClientOptions) => ApiClient
73
- /** Test seam for bounded polling without wall-clock sleeps. */
74
- now?: () => number
75
- /** Test seam for retry/backoff timing. */
76
- sleep?: (milliseconds: number) => Promise<void>
77
- /** Test seams for Claude liveness, socket delivery, and cold resume. */
78
- claudeWake?: ClaudeWakeAdapters
79
- claudeSourcePid?: number
80
- /** Test seams for the Codex thread-writer probe and cold resume. */
81
- codexWake?: CodexWakeAdapters
82
- codexSourcePid?: number
83
- /** Test seam and production adapter for the external native skills installer. */
84
- nativeSkills?: NativeSkills
85
- /** Test seam for Orca's exact-worktree task/session title lookup. */
86
- orcaSessionTitle?: OrcaSessionTitleLookup
87
- /** Test seam for registry reads; production uses fetch. */
88
- fetchImpl?: typeof fetch
89
- /**
90
- * The local record of what this invocation did. Optional so a test fake need
91
- * not carry one; `log(deps)` supplies a logger that records nothing.
92
- */
93
- logger?: Logger
94
- }
95
-
96
- /** The invocation's logger, or one that discards everything. */
97
- export function log(deps: CommandDeps): Logger {
98
- return deps.logger ?? nullLogger()
99
- }
100
-
101
- export function loadLoggedConfig(
102
- deps: CommandDeps,
103
- options: Parameters<typeof loadConfig>[0],
104
- ): CliConfig {
105
- const config = loadConfig(options)
106
- const logger = log(deps)
107
- logger.adopt(logSettingsFrom(config))
108
- logger.bind({ project: config.project.value })
109
- logConfigResolved(logger, config)
110
- return config
111
- }
112
-
113
- export const EXIT = {
114
- ok: 0,
115
- failed: 1,
116
- usage: 2,
117
- noReply: 3,
118
- auth: 4,
119
- network: 5,
120
- } as const
121
-
122
- export function makeClient(
123
- deps: CommandDeps,
124
- baseUrl: string,
125
- bearer: string | null,
126
- options?: ClientOptions,
127
- ): ApiClient {
128
- return (deps.clientFactory ?? createClient)(baseUrl, bearer, {
129
- ...options,
130
- logger: log(deps),
131
- cliVersion: packageVersion(),
132
- capabilities: SHIPPED_CLI_CAPABILITIES,
133
- })
134
- }
135
-
136
- export function resolvedBaseUrl(config: CliConfig, credential: MachineCredential | null): string {
137
- return credential ? credential.baseUrl : config.base_url.value
138
- }
139
-
140
- /**
141
- * Whether a flag or env origin would send signed-in traffic somewhere other
142
- * than the origin stored with this machine's credential.
143
- *
144
- * Login still honours those overrides: pairing has no credential to pin to.
145
- * Returning the source (not the override URL, and never the bearer) is what
146
- * lets callers diagnose the ignore without leaking either.
147
- */
148
- export function ignoredOriginOverride(
149
- config: CliConfig,
150
- credential: MachineCredential,
151
- ): CliConfig['base_url']['source'] | null {
152
- if (config.base_url.source === 'default') return null
153
- if (config.base_url.value === credential.baseUrl) return null
154
- return config.base_url.source
155
- }
156
-
157
- export function diagnoseIgnoredOriginOverride(
158
- io: Pick<CommandIo, 'err'>,
159
- config: CliConfig,
160
- credential: MachineCredential,
161
- ): void {
162
- const source = ignoredOriginOverride(config, credential)
163
- if (source === null) return
164
- io.err(
165
- `Ignoring base_url from ${source}; authenticated traffic uses the origin stored with this machine's credential. Run \`notifai login\` to pair with a different origin.`,
166
- )
167
- }
168
-
169
- export function authedClient(deps: CommandDeps, config: CliConfig): { client: ApiClient; baseUrl: string } | null {
170
- const credential = deps.store.load()
171
- if (!credential) {
172
- // The commonest reason a command does nothing, and one that leaves no other
173
- // trace: it returns before any request is made, so without this the log
174
- // shows an exit code and no cause.
175
- log(deps).error('cli.error', { kind: 'auth', message: 'not signed in', store: deps.store.describe() })
176
- deps.io.err('Not signed in. Run `notifai init`; it will coordinate machine login and device setup.')
177
- return null
178
- }
179
- diagnoseIgnoredOriginOverride(deps.io, config, credential)
180
- const baseUrl = resolvedBaseUrl(config, credential)
181
- return {
182
- client: makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`),
183
- baseUrl,
184
- }
185
- }
186
-
187
- export const UPDATE_CLI_COMMAND = 'npm install -g @raidiant/notifai'
188
-
189
- /** The one first-run command an unsigned machine is told to run. */
190
- export const SETUP_COMMAND = 'notifai init'
191
-
192
- function affectedDeviceNames(details: unknown): string[] {
193
- if (typeof details !== 'object' || details === null || !('device_names' in details)) return []
194
- const names = (details as { device_names?: unknown }).device_names
195
- if (!Array.isArray(names)) return []
196
- return [...new Set(names.filter((name): name is string => typeof name === 'string' && name.trim() !== ''))]
197
- }
198
-
199
- function naturalList(values: readonly string[]): string {
200
- if (values.length <= 1) return values[0] ?? ''
201
- if (values.length === 2) return `${values[0]} and ${values[1]}`
202
- return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`
203
- }
204
-
205
- function localRecovery(action: RecoveryAction | null, details: unknown): string | null {
206
- switch (action) {
207
- case 'update_cli':
208
- return `next: ${UPDATE_CLI_COMMAND}`
209
- case 'update_companion': {
210
- const names = affectedDeviceNames(details)
211
- return names.length > 0
212
- ? `next: Update Notifai on ${naturalList(names)}.`
213
- : 'next: Update Notifai on the affected devices.'
214
- }
215
- case 'wait_for_service':
216
- return 'next: The service is being updated; try again later.'
217
- default:
218
- return null
219
- }
220
- }
221
-
222
- export function reportError(
223
- deps: CommandDeps,
224
- err: unknown,
225
- context: Record<string, unknown> = {},
226
- ): number {
227
- // Recorded before it is printed: stderr from an agent's shell call is often
228
- // the one thing that does not survive into the next turn, and this is exactly
229
- // the line someone comes back looking for.
230
- log(deps).error('cli.error', {
231
- ...context,
232
- ...(err instanceof ApiCallError
233
- ? {
234
- kind: 'api',
235
- status: err.status,
236
- code: err.code,
237
- message: err.message,
238
- next_action: err.nextAction,
239
- recovery_action: err.recoveryAction,
240
- details: err.details,
241
- }
242
- : err instanceof NetworkError
243
- ? { kind: 'network', message: err.message }
244
- : { kind: 'unknown', message: String(err) }),
245
- })
246
- if (err instanceof ApiCallError) {
247
- deps.io.err(
248
- deps.io.interactive === true && err.recoveryAction !== null
249
- ? err.message
250
- : `${err.code}: ${err.message}`,
251
- )
252
- // The server's field-level details were already being logged above, but
253
- // stderr is the one surface an agent's next turn actually reads — so the
254
- // diagnosis has to be on it, the same way the hook path prints it.
255
- const paths = rejectedPaths(err.details)
256
- if (paths.length > 0) deps.io.err(`the server rejected: ${paths.join(', ')}`)
257
- // A 422 on a request this CLI built is not a user error: the two sides
258
- // disagree about the contract, in whichever direction. `doctor` says which.
259
- // Codes that name a policy the account chose are the exception — the
260
- // contract held, the body simply did not satisfy that account.
261
- if (
262
- err.status === 422 &&
263
- err.code !== 'acknowledgement_text_required' &&
264
- err.code !== 'feature_unavailable'
265
- ) {
266
- deps.io.err(
267
- 'this build sent a field the server did not accept — the CLI and server ' +
268
- 'disagree about the contract; check with `notifai doctor`',
269
- )
270
- }
271
- const recovery =
272
- err.code === 'no_active_devices'
273
- ? 'next: Run `notifai init` to connect an active Companion App; it will complete machine setup and identify any human-only device step.'
274
- : localRecovery(err.recoveryAction, err.details)
275
- if (recovery !== null) deps.io.err(recovery)
276
- else if (err.recoveryAction === null && err.nextAction) deps.io.err(`next: ${err.nextAction}`)
277
- if (err.code === 'auth_required' || err.code === 'machine_revoked') return EXIT.auth
278
- return err.status >= 500 ? EXIT.network : EXIT.failed
279
- }
280
- if (err instanceof NetworkError) {
281
- deps.io.err(err.message)
282
- return EXIT.network
283
- }
284
- deps.io.err(String(err))
285
- return EXIT.failed
286
- }
287
-
288
- export function rejectedPaths(details: unknown): string[] {
289
- if (!Array.isArray(details)) return []
290
- return details
291
- .map((issue) =>
292
- typeof issue === 'object' && issue !== null && typeof (issue as { path?: unknown }).path === 'string'
293
- ? (issue as { path: string }).path
294
- : null,
295
- )
296
- .filter((path): path is string => path !== null && path !== '')
297
- .slice(0, 5)
298
- }
@@ -1,15 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { supportPageUrl } from './commands-devices.js'
3
-
4
- describe('supportPageUrl', () => {
5
- it('routes hosted control-plane origins to the dashboard support surface', () => {
6
- expect(supportPageUrl('https://api.notifai.sh')).toBe('https://app.notifai.sh/support')
7
- expect(supportPageUrl('https://notifai.fly.dev/')).toBe('https://app.notifai.sh/support')
8
- })
9
-
10
- it('keeps a self-hosted deployment on its own support route', () => {
11
- expect(supportPageUrl('https://notifai.example.test/')).toBe(
12
- 'https://notifai.example.test/support',
13
- )
14
- })
15
- })