@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/codex-wake.ts DELETED
@@ -1,297 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { closeSync, constants, existsSync, openSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { configHome } from './install-hooks.js'
5
- import {
6
- hookContinuationRoute,
7
- type ContinuationEvent,
8
- type DeliveryOutcome,
9
- type EscalationDeliveryRoute,
10
- } from './hooks.js'
11
-
12
- /** Codex's per-thread writer lock directory, relative to `$CODEX_HOME`. */
13
- export const CODEX_THREAD_LOCK_DIR = 'thread-writer-locks'
14
-
15
- /**
16
- * BSD `open(2)` lock flags. Node does not surface them in `fs.constants`, and
17
- * they are the only way to take a non-blocking `flock` from Node without a
18
- * native addon: the kernel takes the lock at open and drops it at close.
19
- */
20
- const O_EXLOCK = 0x20
21
- const O_NONBLOCK = 0x4
22
-
23
- /** Platforms whose `open(2)` implements `O_EXLOCK`. Everything else fails closed. */
24
- const LOCK_PROBE_PLATFORMS: ReadonlySet<string> = new Set(['darwin', 'freebsd', 'openbsd'])
25
-
26
- /** Codex thread ids are UUIDs, and the lock file is named by the exact id. */
27
- const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
28
-
29
- export type CodexWakeObservation =
30
- /** A live writer owns this thread. Resuming it would create a divergent ghost. */
31
- | { state: 'live' }
32
- /** No process holds the thread's writer lock; a cold resume can own it. */
33
- | { state: 'stopped' }
34
- | { state: 'unknown'; reason: string }
35
-
36
- export interface CodexWakeAdapters {
37
- /** Non-blocking writer-lock probe. Never holds the lock beyond the probe. */
38
- probeThreadWriter(lockPath: string): CodexWakeObservation
39
- /** Is the harness process that will read this hook's stdout still running? */
40
- sourceAlive(pid: number): boolean
41
- resume(threadId: string, cwd: string, context: string): Promise<void>
42
- }
43
-
44
- /**
45
- * The one cross-process oracle for "who owns this Codex thread".
46
- *
47
- * Codex's thread store takes a non-blocking `flock(LOCK_EX)` on a zero-byte
48
- * file per thread and holds it for as long as that process owns the thread —
49
- * verified against both a hosted app-server thread sitting idle and an
50
- * in-process `codex exec` run (2026-08-12). Because the kernel holds it, a
51
- * crash, SIGKILL, or reboot releases it instantly, and the leftover file proves
52
- * nothing: only the lock does. Codex fails closed itself with `-32600` when a
53
- * second writer contends, but that is the backstop; this is the gate.
54
- */
55
- export function observeCodexThread(
56
- threadId: string,
57
- env: NodeJS.ProcessEnv,
58
- adapters: Pick<CodexWakeAdapters, 'probeThreadWriter'>,
59
- ): CodexWakeObservation {
60
- if (!THREAD_ID.test(threadId)) {
61
- return { state: 'unknown', reason: 'the Codex session id is not a thread id' }
62
- }
63
- try {
64
- return adapters.probeThreadWriter(codexThreadLockPath(threadId, env))
65
- } catch (err) {
66
- return {
67
- state: 'unknown',
68
- reason: `Codex thread-writer lock probe failed: ${err instanceof Error ? err.message : String(err)}`,
69
- }
70
- }
71
- }
72
-
73
- export function codexThreadLockPath(threadId: string, env: NodeJS.ProcessEnv): string {
74
- return path.join(codexThreadLockDirectory(env), `${threadId}.lock`)
75
- }
76
-
77
- export function codexThreadLockDirectory(env: NodeJS.ProcessEnv): string {
78
- return path.join(configHome(env, 'CODEX_HOME', '.codex'), CODEX_THREAD_LOCK_DIR)
79
- }
80
-
81
- export type CodexResumeReadiness =
82
- | { state: 'ready'; lockDirectory: string }
83
- | { state: 'unavailable'; reason: string }
84
-
85
- /**
86
- * Whether a Codex thread whose Stop hook has already returned could still be
87
- * cold-resumed, decided without taking any lock or spawning anything.
88
- *
89
- * Two things have to hold: this platform must implement the non-blocking
90
- * `flock` the probe is built on, and `$CODEX_HOME` must actually have a
91
- * thread-writer-lock directory — its absence is not evidence that a thread is
92
- * unowned, so the gate fails closed and the answer waits for the next turn.
93
- * Neither affects the ordinary path, where the hook's own stdout continues the
94
- * held turn.
95
- */
96
- export function inspectCodexResume(
97
- env: NodeJS.ProcessEnv,
98
- options: { platform: NodeJS.Platform; directoryExists: (directory: string) => boolean },
99
- ): CodexResumeReadiness {
100
- const lockDirectory = codexThreadLockDirectory(env)
101
- if (!LOCK_PROBE_PLATFORMS.has(options.platform)) {
102
- return {
103
- state: 'unavailable',
104
- reason: `no non-blocking thread-writer lock probe exists on ${options.platform}`,
105
- }
106
- }
107
- if (!options.directoryExists(lockDirectory)) {
108
- return {
109
- state: 'unavailable',
110
- reason: `no thread-writer-lock directory at ${lockDirectory}, so no thread can be proven unowned`,
111
- }
112
- }
113
- return { state: 'ready', lockDirectory }
114
- }
115
-
116
- /**
117
- * Codex's answer-delivery route: the blocking Stop hook, then nothing else.
118
- *
119
- * The default is the hook continuation itself. While the Codex process that
120
- * invoked this hook is alive it is reading this process's stdout, and
121
- * `{"decision":"block"}` there creates a real new user turn — no launch
122
- * ownership, no control plane, no probe required. That path must never depend
123
- * on the lock probe: printing to a dead pipe cannot ghost-write anything, so
124
- * an unreadable `$CODEX_HOME` is not a reason to withhold a continuation.
125
- *
126
- * Only when that process is gone does the answer need another last meter, and
127
- * then exactly one is safe: a cold `codex exec resume` of a thread no live
128
- * writer owns. Anything else — a live writer, an unprobeable platform, a thread
129
- * id we cannot name — is journaled and replayed at the session's next Stop.
130
- * That accepted gap is the honest floor of the Codex adapter: a session that
131
- * went idle after its Stop returned, which Notifai did not launch, cannot be
132
- * woken in place.
133
- */
134
- export function codexWakeRoute(options: {
135
- threadId: string
136
- cwd: string
137
- sourcePid: number
138
- env?: NodeJS.ProcessEnv
139
- adapters?: CodexWakeAdapters
140
- }): EscalationDeliveryRoute {
141
- const env = options.env ?? process.env
142
- const adapters = options.adapters ?? systemCodexWakeAdapters(env)
143
- const continuation = hookContinuationRoute()
144
- return {
145
- kind: 'hook-continuation',
146
- async deliver(event: ContinuationEvent): Promise<DeliveryOutcome> {
147
- if (adapters.sourceAlive(options.sourcePid)) return continuation.deliver(event)
148
-
149
- // Probe immediately before the spawn, twice, and never trust the earlier
150
- // answer: ownership is a kernel fact that can change between two syscalls,
151
- // and resuming a thread someone else owns is the disqualifying failure.
152
- const observed = observeCodexThread(options.threadId, env, adapters)
153
- const held = holdForNextTurn(observed)
154
- if (held !== null) return held
155
- const confirmed = observeCodexThread(options.threadId, env, adapters)
156
- const raced = holdForNextTurn(confirmed)
157
- if (raced !== null) return raced
158
-
159
- if (!event.commitDelivery()) return cancelledDelivery()
160
- await adapters.resume(options.threadId, options.cwd, event.context)
161
- return {
162
- notes: ['cold-resumed the stopped Codex thread with its accepted answer'],
163
- log: { route: 'cold-resume', stage: 'delivered' },
164
- // The resume ran: the thread holds the answer, and no later hook of this
165
- // session will report on a turn that started elsewhere.
166
- acknowledgement: 'delivered',
167
- }
168
- },
169
- }
170
- }
171
-
172
- function cancelledDelivery(): DeliveryOutcome {
173
- return {
174
- notes: ['the Agent Session ended before answer delivery; stopping this observer'],
175
- log: { route: 'hold-for-next-turn', stage: 'queued', reason: 'session-ended' },
176
- acknowledgement: 'held',
177
- }
178
- }
179
-
180
- /** The journal, with the exact reason the thread could not be resumed. */
181
- function holdForNextTurn(observation: CodexWakeObservation): DeliveryOutcome | null {
182
- if (observation.state === 'stopped') return null
183
- const reason =
184
- observation.state === 'live'
185
- ? 'a live writer owns the Codex thread and this hook can no longer continue it'
186
- : observation.reason
187
- return {
188
- notes: [`holding the accepted answer for the next turn: ${reason}`],
189
- log: { route: 'hold-for-next-turn', stage: 'queued', reason },
190
- acknowledgement: 'held',
191
- }
192
- }
193
-
194
- /**
195
- * Take and release the writer lock in one open/close pair.
196
- *
197
- * The probe momentarily owns the lock when it is free, so it is held for
198
- * microseconds and never across a spawn — a probe racing a legitimate resume
199
- * must not be what makes that resume fail.
200
- */
201
- function probeThreadWriterLock(lockPath: string): CodexWakeObservation {
202
- if (!LOCK_PROBE_PLATFORMS.has(process.platform)) {
203
- return {
204
- state: 'unknown',
205
- reason: `no non-blocking thread-writer lock probe exists on ${process.platform}`,
206
- }
207
- }
208
- const directory = path.dirname(lockPath)
209
- if (!existsSync(directory)) {
210
- // No lock directory means this is not a Codex home that has ever opened a
211
- // thread, so its silence is not evidence that nothing owns this one.
212
- return { state: 'unknown', reason: `no Codex thread-writer lock directory at ${directory}` }
213
- }
214
- let handle: number
215
- try {
216
- handle = openSync(lockPath, constants.O_RDONLY | O_EXLOCK | O_NONBLOCK)
217
- } catch (err) {
218
- const code = (err as NodeJS.ErrnoException).code
219
- if (code === 'EAGAIN' || code === 'EWOULDBLOCK') return { state: 'live' }
220
- // Codex unlinks the lock file when its writer drops cleanly, so a missing
221
- // file inside a real lock directory means the thread has no owner.
222
- if (code === 'ENOENT') return { state: 'stopped' }
223
- return {
224
- state: 'unknown',
225
- reason: `Codex thread-writer lock probe failed: ${err instanceof Error ? err.message : String(err)}`,
226
- }
227
- }
228
- closeSync(handle)
229
- return { state: 'stopped' }
230
- }
231
-
232
- function runCodex(
233
- args: string[],
234
- options: { cwd: string; env: NodeJS.ProcessEnv },
235
- ): Promise<string> {
236
- return new Promise((resolve, reject) => {
237
- const child = spawn('codex', args, {
238
- cwd: options.cwd,
239
- env: options.env,
240
- stdio: ['ignore', 'pipe', 'pipe'],
241
- })
242
- const stdout: Buffer[] = []
243
- const stderr: Buffer[] = []
244
- child.stdout?.on('data', (chunk: Buffer | string) => stdout.push(Buffer.from(chunk)))
245
- child.stderr?.on('data', (chunk: Buffer | string) => stderr.push(Buffer.from(chunk)))
246
- child.once('error', reject)
247
- child.once('exit', (code, signal) => {
248
- if (code === 0) {
249
- resolve(Buffer.concat(stdout).toString())
250
- return
251
- }
252
- reject(
253
- new Error(
254
- `codex ${args[0] ?? ''} exited ${code === null ? `on ${String(signal)}` : String(code)}: ${Buffer.concat(stderr).toString().trim()}`,
255
- ),
256
- )
257
- })
258
- })
259
- }
260
-
261
- /**
262
- * The resumed process must not inherit the ended session's identity, or it
263
- * reports itself as that thread to every hook it fires. `CODEX_HOME` is the one
264
- * exception: it names the thread store the resume has to open.
265
- */
266
- function coldResumeEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
267
- const next = { ...env }
268
- for (const key of Object.keys(next)) {
269
- if (key.startsWith('CODEX_') && key !== 'CODEX_HOME') delete next[key]
270
- }
271
- return next
272
- }
273
-
274
- export function systemCodexWakeAdapters(
275
- env: NodeJS.ProcessEnv = process.env,
276
- ): CodexWakeAdapters {
277
- return {
278
- probeThreadWriter: probeThreadWriterLock,
279
- sourceAlive(pid) {
280
- if (!Number.isInteger(pid) || pid <= 1) return false
281
- try {
282
- process.kill(pid, 0)
283
- return true
284
- } catch (err) {
285
- // Signalling a process we may not touch still proves it exists.
286
- return (err as NodeJS.ErrnoException).code === 'EPERM'
287
- }
288
- },
289
- async resume(threadId, cwd, context) {
290
- if (!existsSync(cwd)) throw new Error(`Codex thread cwd no longer exists: ${cwd}`)
291
- await runCodex(['exec', 'resume', threadId, context], {
292
- cwd,
293
- env: coldResumeEnvironment(env),
294
- })
295
- },
296
- }
297
- }
@@ -1,38 +0,0 @@
1
- import type { CommandDeps } from './commands-core.js'
2
- import { resolveActiveHarness } from './commands-harness-context.js'
3
- import { findOwningSession, readProjectSession } from './hooks.js'
4
-
5
- export interface ResolvedCommandSession {
6
- sessionId: string
7
- source: 'exact-harness' | 'stable-id' | 'project-pointer'
8
- }
9
-
10
- /**
11
- * Resolve only Question Routing lifecycle ownership.
12
- *
13
- * Exact harness identity wins across checkouts. A stable question/request id
14
- * wins next. The cwd pointer remains a compatibility fallback for shells that
15
- * genuinely have no exact harness identity; it never owns Project or config
16
- * scope, which stay with the command's invocation directory.
17
- */
18
- export function resolveCommandSession(
19
- deps: CommandDeps,
20
- stableId?: string,
21
- ): ResolvedCommandSession | null {
22
- if (stableId !== undefined) {
23
- const owner = findOwningSession(stableId, deps.env)
24
- if (owner.ambiguous) return null
25
- if (owner.sessionId !== null) return { sessionId: owner.sessionId, source: 'stable-id' }
26
- }
27
- const now = (deps.now ?? Date.now)()
28
- const resolution = resolveActiveHarness(deps.env, deps.cwd, now)
29
- if (resolution.contested.length > 0) return null
30
- if (
31
- resolution.active?.sessionId !== undefined &&
32
- resolution.contested.length === 0
33
- ) {
34
- return { sessionId: resolution.active.sessionId, source: 'exact-harness' }
35
- }
36
- const pointer = readProjectSession(deps.cwd, deps.env, now)
37
- return pointer === null ? null : { sessionId: pointer, source: 'project-pointer' }
38
- }
@@ -1,72 +0,0 @@
1
- import { resolveActiveHarness } from './commands-harness-context.js'
2
- import {
3
- authedClient,
4
- EXIT,
5
- loadLoggedConfig,
6
- reportError,
7
- type CommandDeps,
8
- } from './commands-core.js'
9
- import {
10
- normalizeExplicitSessionLabel,
11
- renameStoredSessionLabel,
12
- } from './session-labels.js'
13
-
14
- export interface AgentSessionRenameFlags {
15
- json?: boolean
16
- }
17
-
18
- /** Rename only the exact harness-owned Agent Session running this command. */
19
- export async function agentSessionRenameCommand(
20
- deps: CommandDeps,
21
- label: string,
22
- flags: AgentSessionRenameFlags = {},
23
- ): Promise<number> {
24
- const now = deps.now?.() ?? Date.now()
25
- const resolution = resolveActiveHarness(deps.env, deps.cwd, now)
26
- const active = resolution.active
27
- if (resolution.contested.length > 0 || active?.sessionId === undefined) {
28
- deps.io.err(
29
- 'No exact active Agent Session can be proven here. Run this command inside the Agent Session you intend to rename.',
30
- )
31
- return EXIT.usage
32
- }
33
-
34
- const normalized = normalizeExplicitSessionLabel({
35
- env: deps.env,
36
- sessionId: active.sessionId,
37
- label,
38
- })
39
- if (!normalized.ok) {
40
- deps.io.err(normalized.error)
41
- return EXIT.usage
42
- }
43
-
44
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
45
- const auth = authedClient(deps, config)
46
- if (!auth) return EXIT.auth
47
- try {
48
- const renamed = await auth.client.putAgentSessionLabel({
49
- session_id: active.sessionId,
50
- label: normalized.label,
51
- })
52
- const local = renameStoredSessionLabel({
53
- env: deps.env,
54
- sessionId: active.sessionId,
55
- label: renamed.label,
56
- harness: active.harness,
57
- now,
58
- })
59
- if (!local.ok) {
60
- deps.io.err(local.error)
61
- return EXIT.failed
62
- }
63
- deps.io.out(
64
- flags.json
65
- ? JSON.stringify(renamed)
66
- : `Agent Session renamed to "${renamed.label}" across your Notifai account.`,
67
- )
68
- return EXIT.ok
69
- } catch (err) {
70
- return reportError(deps, err, { operation: 'agent_session_rename' })
71
- }
72
- }
@@ -1,205 +0,0 @@
1
- import { type AccountAccessResponse } from '@raidiant/notifai-protocol'
2
- import { sha256Hex } from '@raidiant/notifai-protocol/node'
3
- import { randomBytes } from 'node:crypto'
4
- import os from 'node:os'
5
- import { NetworkError } from './client.js'
6
- import { type FlagOverrides } from './config.js'
7
- import { checkApproveUrl } from './url-policy.js'
8
- import {
9
- EXIT,
10
- SETUP_COMMAND,
11
- authedClient,
12
- loadLoggedConfig,
13
- makeClient,
14
- reportError,
15
- type CommandDeps,
16
- } from './commands-core.js'
17
- import { supportPageUrl } from './commands-devices.js'
18
-
19
- // ---------------------------------------------------------------------------
20
- // login / logout / auth status
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * Keep the one-time confirmation secret out of the HTTP request that opens
25
- * the dashboard. URL fragments are browser-local and are not sent by GET,
26
- * prefetchers, referrers, or link scanners.
27
- */
28
- export function pairingApprovalUrl(approveUrl: string, confirmationSecret: string): string {
29
- const url = new URL(approveUrl)
30
- url.hash = new URLSearchParams({ confirmation_secret: confirmationSecret }).toString()
31
- return url.toString()
32
- }
33
-
34
- export async function loginCommand(
35
- deps: CommandDeps,
36
- flags: { name?: string; baseUrl?: string; open?: boolean },
37
- ): Promise<number> {
38
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, flags: { base_url: flags.baseUrl } as FlagOverrides })
39
- const baseUrl = config.base_url.value
40
- const machineName = flags.name ?? os.hostname()
41
- const secret = randomBytes(32).toString('base64url')
42
- const pollVerifier = randomBytes(24).toString('base64url')
43
- const confirmationSecret = randomBytes(32).toString('base64url')
44
- const client = makeClient(deps, baseUrl, null)
45
-
46
- let begin
47
- try {
48
- begin = await client.beginPairing({
49
- machine_name: machineName,
50
- credential_hash: sha256Hex(secret),
51
- poll_verifier_hash: sha256Hex(pollVerifier),
52
- confirmation_hash: sha256Hex(confirmationSecret),
53
- })
54
- } catch (err) {
55
- return reportError(deps, err)
56
- }
57
- const approveUrl = pairingApprovalUrl(begin.approve_url, confirmationSecret)
58
-
59
- // The approval URL is the server's choice, and this machine is about to put
60
- // it in front of the user's browser. A compromised or misconfigured service
61
- // must not be able to aim that anywhere it likes, so the pairing stops here
62
- // rather than showing a link the user would reasonably trust.
63
- const approvable = checkApproveUrl(begin.approve_url, baseUrl, config.approve_origins.value)
64
- if (!approvable.ok) {
65
- deps.io.err(`Pairing stopped: ${approvable.reason}`)
66
- deps.io.err(
67
- 'next: If you self-host with the dashboard on its own origin, allow it with ' +
68
- '`notifai config set approve_origins <origin>` and run `notifai login` again.',
69
- )
70
- return EXIT.auth
71
- }
72
-
73
- const interactive = deps.io.interactive === true
74
- if (interactive) {
75
- await deps.io.intro?.('Notifai sign in')
76
- await deps.io.note?.(`Code: ${begin.code}\n${approveUrl}`, 'Approve this machine')
77
- } else {
78
- deps.io.out(`Pairing code: ${begin.code}`)
79
- deps.io.out(`Approve this machine at: ${approveUrl}`)
80
- deps.io.out('Waiting for approval…')
81
- }
82
- if (flags.open !== false) deps.io.openUrl(approveUrl)
83
-
84
- const expiresAt = new Date(begin.expires_at).getTime()
85
- const intervalMs = Math.max(begin.poll_interval_seconds, 1) * 1000
86
- const now = deps.now ?? Date.now
87
- const sleep = deps.sleep ?? ((milliseconds: number) => new Promise<void>((resolve) => setTimeout(resolve, milliseconds)))
88
- const approvalWaitMessage = (): string => {
89
- const remainingSec = Math.max(0, Math.ceil((expiresAt - now()) / 1000))
90
- const minutes = Math.floor(remainingSec / 60)
91
- const seconds = remainingSec % 60
92
- const remaining =
93
- minutes > 0 ? `${minutes}m ${seconds.toString().padStart(2, '0')}s` : `${seconds}s`
94
- return `Waiting for approval… code ${begin.code} · ${remaining} left`
95
- }
96
- const spinner = interactive ? await deps.io.spinner?.(approvalWaitMessage()) : null
97
- while (now() < expiresAt) {
98
- await sleep(intervalMs)
99
- let poll
100
- try {
101
- poll = await client.pollPairing(begin.pairing_id, pollVerifier)
102
- } catch (err) {
103
- if (err instanceof NetworkError) {
104
- spinner?.message(`Connection lost — retrying… code ${begin.code}`)
105
- continue
106
- }
107
- spinner?.error('Pairing failed')
108
- return reportError(deps, err)
109
- }
110
- if (poll.status === 'approved' && poll.machine_id) {
111
- deps.store.save({ machineId: poll.machine_id, secret, baseUrl, machineName })
112
- if (interactive) {
113
- spinner?.stop(`Machine "${machineName}" approved`)
114
- await deps.io.outro?.(`Credential stored in ${deps.store.describe()}`)
115
- } else {
116
- deps.io.out(`Machine "${machineName}" approved. Credential stored in ${deps.store.describe()}.`)
117
- }
118
- return EXIT.ok
119
- }
120
- if (poll.status === 'denied') {
121
- spinner?.error('Pairing denied')
122
- deps.io.err('Pairing was denied from the dashboard.')
123
- return EXIT.auth
124
- }
125
- if (poll.status === 'no_active_plan') {
126
- const next =
127
- poll.next_action ??
128
- `Open ${supportPageUrl(baseUrl)} to request Alpha access, then retry.`
129
- spinner?.error('Account has no Alpha access')
130
- deps.io.err('This account has no active plan or temporary Alpha access.')
131
- deps.io.err(`next: ${next}`)
132
- deps.io.err(`After access is granted, run \`${SETUP_COMMAND}\` again.`)
133
- return EXIT.auth
134
- }
135
- if (poll.status === 'expired') break
136
- spinner?.message(approvalWaitMessage())
137
- }
138
- spinner?.error('Pairing expired')
139
- deps.io.err(`Pairing expired before it was approved. Run \`${SETUP_COMMAND}\` again.`)
140
- return EXIT.auth
141
- }
142
-
143
- export function logoutCommand(deps: CommandDeps): number {
144
- deps.store.clear()
145
- deps.io.out('Machine credential removed. Revoke it in the dashboard too if the machine is untrusted.')
146
- return EXIT.ok
147
- }
148
-
149
- export function authStatusCommand(deps: CommandDeps, flags: { json?: boolean }): number {
150
- const credential = deps.store.load()
151
- if (flags.json) {
152
- deps.io.out(
153
- JSON.stringify(
154
- credential
155
- ? {
156
- signed_in: true,
157
- machine_id: credential.machineId,
158
- machine_name: credential.machineName,
159
- base_url: credential.baseUrl,
160
- store: deps.store.describe(),
161
- }
162
- : { signed_in: false },
163
- null,
164
- 2,
165
- ),
166
- )
167
- return credential ? EXIT.ok : EXIT.auth
168
- }
169
- if (!credential) {
170
- deps.io.err(`Not signed in. Run \`${SETUP_COMMAND}\`; it will coordinate machine login and device setup.`)
171
- return EXIT.auth
172
- }
173
- deps.io.out(`Signed in as machine "${credential.machineName}" (${credential.machineId})`)
174
- deps.io.out(`Server: ${credential.baseUrl}`)
175
- deps.io.out(`Credential store: ${deps.store.describe()}`)
176
- return EXIT.ok
177
- }
178
-
179
- /** Show the server's account access decision without attempting a product mutation. */
180
- export async function accessStatusCommand(
181
- deps: CommandDeps,
182
- flags: { json?: boolean },
183
- ): Promise<number> {
184
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
185
- const authed = authedClient(deps, config)
186
- if (!authed) return EXIT.auth
187
- try {
188
- const access: AccountAccessResponse = await authed.client.accessStatus()
189
- if (flags.json) {
190
- deps.io.out(JSON.stringify(access, null, 2))
191
- return access.status === 'active' ? EXIT.ok : EXIT.failed
192
- }
193
- if (access.status === 'no_active_plan') {
194
- const supportUrl = supportPageUrl(authed.baseUrl)
195
- deps.io.out('No active plan or temporary Alpha access for this account.')
196
- deps.io.out(`next: Open ${supportUrl} to request Alpha access, then retry.`)
197
- return EXIT.failed
198
- }
199
- const expiry = access.expires_at ? ` until ${access.expires_at}` : ''
200
- deps.io.out(`Access active (${access.reason})${expiry}`)
201
- return EXIT.ok
202
- } catch (err) {
203
- return reportError(deps, err)
204
- }
205
- }