@raidiant/notifai 10.1.1 → 10.1.3

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 (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands-core.d.ts +4 -1
  3. package/dist/commands-core.js.map +1 -1
  4. package/dist/commands-doctor.js +2 -7
  5. package/dist/commands-doctor.js.map +1 -1
  6. package/dist/commands-hooks.d.ts +12 -0
  7. package/dist/commands-hooks.js +15 -1
  8. package/dist/commands-hooks.js.map +1 -1
  9. package/dist/commands-init.js +9 -6
  10. package/dist/commands-init.js.map +1 -1
  11. package/dist/commands-send-support.js +10 -11
  12. package/dist/commands-send-support.js.map +1 -1
  13. package/dist/commands-skill.js +72 -54
  14. package/dist/commands-skill.js.map +1 -1
  15. package/dist/harness-session-title.d.ts +22 -0
  16. package/dist/harness-session-title.js +74 -0
  17. package/dist/harness-session-title.js.map +1 -0
  18. package/dist/invocation-context.d.ts +1 -0
  19. package/dist/invocation-context.js +15 -1
  20. package/dist/invocation-context.js.map +1 -1
  21. package/dist/native-skills.d.ts +13 -3
  22. package/dist/native-skills.js +51 -12
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/node-floor.d.ts +2 -1
  25. package/dist/node-floor.js +9 -5
  26. package/dist/node-floor.js.map +1 -1
  27. package/dist/orca-session-title.d.ts +5 -4
  28. package/dist/orca-session-title.js +32 -19
  29. package/dist/orca-session-title.js.map +1 -1
  30. package/dist/platform.d.ts +11 -17
  31. package/dist/platform.js +20 -23
  32. package/dist/platform.js.map +1 -1
  33. package/dist/release.d.ts +5 -5
  34. package/dist/release.js +5 -5
  35. package/dist/session-labels.d.ts +2 -0
  36. package/dist/session-labels.js +20 -9
  37. package/dist/session-labels.js.map +1 -1
  38. package/dist/setup-destinations.js +1 -1
  39. package/dist/setup-destinations.js.map +1 -1
  40. package/dist/skill-integrity.d.ts +49 -0
  41. package/dist/skill-integrity.js +154 -0
  42. package/dist/skill-integrity.js.map +1 -0
  43. package/dist/skill-source/manifest.json +16 -0
  44. package/dist/skill-source/notifai/SKILL.md +390 -0
  45. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  46. package/package.json +11 -14
  47. package/src/atomic-file.ts +0 -198
  48. package/src/claude-wake.test.ts +0 -321
  49. package/src/claude-wake.ts +0 -499
  50. package/src/cli-bin.test.ts +0 -176
  51. package/src/cli-bin.ts +0 -233
  52. package/src/cli-release.test.ts +0 -46
  53. package/src/cli-release.ts +0 -78
  54. package/src/cli-update-contract.ts +0 -9
  55. package/src/cli-update.test.ts +0 -125
  56. package/src/client.test.ts +0 -390
  57. package/src/client.ts +0 -352
  58. package/src/codex-wake.test.ts +0 -254
  59. package/src/codex-wake.ts +0 -301
  60. package/src/command-session.ts +0 -38
  61. package/src/commands-agent-sessions.ts +0 -72
  62. package/src/commands-auth.ts +0 -238
  63. package/src/commands-config.ts +0 -336
  64. package/src/commands-core.ts +0 -312
  65. package/src/commands-devices.ts +0 -180
  66. package/src/commands-doctor.ts +0 -1501
  67. package/src/commands-guidance.ts +0 -181
  68. package/src/commands-harness-context.ts +0 -207
  69. package/src/commands-hook-shape.ts +0 -69
  70. package/src/commands-hooks.ts +0 -2318
  71. package/src/commands-init.ts +0 -1055
  72. package/src/commands-io.ts +0 -108
  73. package/src/commands-logs.ts +0 -243
  74. package/src/commands-project.ts +0 -47
  75. package/src/commands-send-support.test.ts +0 -156
  76. package/src/commands-send-support.ts +0 -311
  77. package/src/commands-send.ts +0 -875
  78. package/src/commands-setup-proof.test.ts +0 -72
  79. package/src/commands-setup-proof.ts +0 -124
  80. package/src/commands-skill.test.ts +0 -44
  81. package/src/commands-skill.ts +0 -204
  82. package/src/commands-sounds.test.ts +0 -106
  83. package/src/commands-sounds.ts +0 -49
  84. package/src/commands-update.ts +0 -180
  85. package/src/commands.test.ts +0 -8297
  86. package/src/commands.ts +0 -77
  87. package/src/config-schema.test.ts +0 -153
  88. package/src/config-schema.ts +0 -349
  89. package/src/config.test.ts +0 -581
  90. package/src/config.ts +0 -475
  91. package/src/credentials.test.ts +0 -403
  92. package/src/credentials.ts +0 -403
  93. package/src/file-lock-process-worker.test.ts +0 -64
  94. package/src/file-lock.test.ts +0 -320
  95. package/src/file-lock.ts +0 -393
  96. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  97. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  98. package/src/generated-session-label.ts +0 -155
  99. package/src/guidance-content.ts +0 -278
  100. package/src/guidance-render.ts +0 -56
  101. package/src/guidance.test.ts +0 -327
  102. package/src/guidance.ts +0 -233
  103. package/src/harness-integration.test.ts +0 -294
  104. package/src/harnesses.test.ts +0 -149
  105. package/src/harnesses.ts +0 -164
  106. package/src/hook-adapter.test.ts +0 -496
  107. package/src/hook-adapter.ts +0 -424
  108. package/src/hook-input.test.ts +0 -39
  109. package/src/hook-input.ts +0 -72
  110. package/src/hooks.test.ts +0 -5824
  111. package/src/hooks.ts +0 -4011
  112. package/src/install-hooks.test.ts +0 -1614
  113. package/src/install-hooks.ts +0 -1501
  114. package/src/interactive.test.ts +0 -387
  115. package/src/interactive.ts +0 -858
  116. package/src/invocation-context.test.ts +0 -352
  117. package/src/invocation-context.ts +0 -197
  118. package/src/logging-process-worker.test.ts +0 -45
  119. package/src/logging.test.ts +0 -544
  120. package/src/logging.ts +0 -698
  121. package/src/main-run.ts +0 -45
  122. package/src/main.ts +0 -12
  123. package/src/native-skills.test.ts +0 -115
  124. package/src/native-skills.ts +0 -162
  125. package/src/node-floor.test.ts +0 -27
  126. package/src/node-floor.ts +0 -37
  127. package/src/onboarding-docs.test.ts +0 -25
  128. package/src/openclaw-plugin.ts +0 -409
  129. package/src/opencode-plugin.ts +0 -298
  130. package/src/orca-session-title.test.ts +0 -54
  131. package/src/orca-session-title.ts +0 -103
  132. package/src/pack-feedback-slice.test.ts +0 -212
  133. package/src/pack-feedback-slice.ts +0 -187
  134. package/src/platform.test.ts +0 -152
  135. package/src/platform.ts +0 -173
  136. package/src/program.test.ts +0 -317
  137. package/src/program.ts +0 -842
  138. package/src/project-enablement.test.ts +0 -55
  139. package/src/project-enablement.ts +0 -99
  140. package/src/question-caps.test.ts +0 -49
  141. package/src/question-settlement-process.ts +0 -47
  142. package/src/question-timing.ts +0 -56
  143. package/src/readiness.test.ts +0 -25
  144. package/src/readiness.ts +0 -212
  145. package/src/release.ts +0 -51
  146. package/src/send-attempts.test.ts +0 -143
  147. package/src/send-attempts.ts +0 -188
  148. package/src/send.test.ts +0 -28
  149. package/src/send.ts +0 -436
  150. package/src/session-activation.ts +0 -61
  151. package/src/session-end-log-lock-worker.test.ts +0 -35
  152. package/src/session-labels.test.ts +0 -490
  153. package/src/session-labels.ts +0 -474
  154. package/src/setup-destinations.test.ts +0 -41
  155. package/src/setup-destinations.ts +0 -50
  156. package/src/skill.test.ts +0 -426
  157. package/src/sound-ref.test.ts +0 -31
  158. package/src/sound-ref.ts +0 -34
  159. package/src/trust-policy.test.ts +0 -701
  160. package/src/ui/banner.ts +0 -95
  161. package/src/ui/config-view.test.ts +0 -90
  162. package/src/ui/config-view.ts +0 -138
  163. package/src/ui/help.test.ts +0 -16
  164. package/src/ui/help.ts +0 -128
  165. package/src/ui/theme.test.ts +0 -92
  166. package/src/ui/theme.ts +0 -265
  167. package/src/url-policy.ts +0 -399
  168. package/tsconfig.json +0 -10
@@ -1,55 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { describe, expect, it } from 'vitest'
6
- import {
7
- disableProject,
8
- enableProject,
9
- projectBinding,
10
- projectEnabled,
11
- } from './project-enablement.js'
12
-
13
- function git(cwd: string, ...args: string[]): void {
14
- execFileSync('git', ['-C', cwd, ...args], { stdio: 'ignore' })
15
- }
16
-
17
- describe('Project enablement', () => {
18
- it('is disabled by default and changes only through its User-owned marker', () => {
19
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-enablement-'))
20
- const env = { XDG_CONFIG_HOME: path.join(root, 'config') }
21
- const binding = projectBinding(root, env)
22
- expect(binding).not.toBeNull()
23
- expect(projectEnabled(binding)).toBe(false)
24
-
25
- enableProject(binding!, new Date('2026-08-27T00:00:00.000Z'))
26
- expect(projectEnabled(binding)).toBe(true)
27
- disableProject(binding!)
28
- expect(projectEnabled(binding)).toBe(false)
29
- })
30
-
31
- it('shares one decision across linked worktrees but not unrelated checkouts with the same name', () => {
32
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-enablement-git-'))
33
- const repo = path.join(root, 'same-name')
34
- const linked = path.join(root, 'linked')
35
- const unrelatedParent = path.join(root, 'other')
36
- const unrelated = path.join(unrelatedParent, 'same-name')
37
- mkdirSync(repo, { recursive: true })
38
- mkdirSync(unrelated, { recursive: true })
39
- for (const cwd of [repo, unrelated]) {
40
- git(cwd, 'init')
41
- git(cwd, 'config', 'user.email', 'test@example.invalid')
42
- git(cwd, 'config', 'user.name', 'Test')
43
- writeFileSync(path.join(cwd, 'file'), 'x')
44
- git(cwd, 'add', 'file')
45
- git(cwd, 'commit', '-m', 'fixture')
46
- }
47
- git(repo, 'worktree', 'add', linked, '-b', 'linked')
48
- const env = { XDG_CONFIG_HOME: path.join(root, 'config') }
49
- const mainBinding = projectBinding(repo, env)
50
- const linkedBinding = projectBinding(linked, env)
51
- const unrelatedBinding = projectBinding(unrelated, env)
52
- expect(linkedBinding?.markerPath).toBe(mainBinding?.markerPath)
53
- expect(unrelatedBinding?.markerPath).not.toBe(mainBinding?.markerPath)
54
- })
55
- })
@@ -1,99 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { createHash } from 'node:crypto'
3
- import { existsSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
4
- import path from 'node:path'
5
- import { atomicWriteFileSync } from './atomic-file.js'
6
- import { globalConfigDir } from './config.js'
7
- import { inferInvocationContext } from './invocation-context.js'
8
-
9
- interface ProjectEnablementMarker {
10
- version: 1
11
- project: string
12
- enabled: true
13
- updated_at: string
14
- }
15
-
16
- export interface ProjectBinding {
17
- project: string
18
- markerPath: string
19
- }
20
-
21
- function gitCommonDirectory(cwd: string): string | null {
22
- try {
23
- const raw = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], {
24
- encoding: 'utf8',
25
- stdio: ['ignore', 'pipe', 'ignore'],
26
- timeout: 2_000,
27
- windowsHide: true,
28
- }).trim()
29
- if (raw === '') return null
30
- const resolved = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw)
31
- try {
32
- return realpathSync(resolved)
33
- } catch {
34
- return path.resolve(resolved)
35
- }
36
- } catch {
37
- return null
38
- }
39
- }
40
-
41
- /**
42
- * Resolve the User-owned enablement record for this exact local Project.
43
- *
44
- * The opaque key binds the semantic Project id to Git's shared common
45
- * directory. Linked worktrees therefore share one decision, while another
46
- * checkout cannot borrow it merely by committing the same Project id.
47
- */
48
- export function projectBinding(
49
- cwd: string,
50
- env: NodeJS.ProcessEnv = process.env,
51
- explicitProject?: string | null,
52
- ): ProjectBinding | null {
53
- const project = explicitProject ?? inferInvocationContext(cwd).project
54
- if (project === null) return null
55
- const repository = gitCommonDirectory(cwd) ?? path.resolve(cwd)
56
- const key = createHash('sha256')
57
- .update('notifai-project-enablement-v1\0')
58
- .update(repository)
59
- .update('\0')
60
- .update(project)
61
- .digest('hex')
62
- return {
63
- project,
64
- markerPath: path.join(globalConfigDir(env), 'project-enablement', `${key}.json`),
65
- }
66
- }
67
-
68
- export function projectEnabled(binding: ProjectBinding | null): boolean {
69
- if (binding === null || !existsSync(binding.markerPath)) return false
70
- try {
71
- const marker = JSON.parse(readFileSync(binding.markerPath, 'utf8')) as Partial<ProjectEnablementMarker>
72
- return marker.version === 1 && marker.enabled === true && marker.project === binding.project
73
- } catch {
74
- return false
75
- }
76
- }
77
-
78
- export function enableProject(binding: ProjectBinding, now: Date = new Date()): void {
79
- const marker: ProjectEnablementMarker = {
80
- version: 1,
81
- project: binding.project,
82
- enabled: true,
83
- updated_at: now.toISOString(),
84
- }
85
- atomicWriteFileSync(binding.markerPath, `${JSON.stringify(marker, null, 2)}\n`, {
86
- mode: 0o600,
87
- preserveMode: false,
88
- requireCurrentUserOwner: true,
89
- directoryMode: 0o700,
90
- })
91
- }
92
-
93
- export function disableProject(binding: ProjectBinding): void {
94
- try {
95
- unlinkSync(binding.markerPath)
96
- } catch (err) {
97
- if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
98
- }
99
- }
@@ -1,49 +0,0 @@
1
- import { mkdtempSync } from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { describe, expect, it } from 'vitest'
5
- import { MAX_LIVE_QUESTIONS, MAX_PENDING_QUESTIONS, registerQuestion } from './hooks.js'
6
-
7
- /**
8
- * `registerQuestion` decides how many questions one session may hold, and these
9
- * exercise it directly against a bare state directory — no harness, no server,
10
- * no clock. They live apart from `hooks.test.ts` because nothing here needs
11
- * what that file spends its setup on.
12
- */
13
- const state = (): NodeJS.ProcessEnv => {
14
- const dir = mkdtempSync(path.join(os.tmpdir(), 'notifai-caps-'))
15
- return { HOME: dir, XDG_STATE_HOME: path.join(dir, 'state') } as NodeJS.ProcessEnv
16
- }
17
-
18
- describe('how many questions one session may hold', () => {
19
- it('stops an agent registering a fifth question nobody has asked yet', () => {
20
- const env = state()
21
- for (let i = 0; i < MAX_PENDING_QUESTIONS; i += 1) {
22
- registerQuestion('loop', env, { question: `Q${i}?` })
23
- }
24
- expect(() => registerQuestion('loop', env, { question: 'One more?' })).toThrow(
25
- /already waiting to be asked/,
26
- )
27
- })
28
-
29
- it('does not count a question the user simply has not answered yet', () => {
30
- // An answer stays accepted for a day. Counting questions already on the
31
- // user's devices against the loop guard would let a patient user stop the
32
- // agent from asking anything at all.
33
- const env = state()
34
- for (let i = 0; i < MAX_PENDING_QUESTIONS; i += 1) {
35
- registerQuestion('live', env, { question: `Asked ${i}?`, request_id: `req_${i}` })
36
- }
37
- expect(() => registerQuestion('live', env, { question: 'A new one?' })).not.toThrow()
38
- })
39
-
40
- it('still bounds what one session can put on the lock screen', () => {
41
- const env = state()
42
- for (let i = 0; i < MAX_LIVE_QUESTIONS; i += 1) {
43
- registerQuestion('flood', env, { question: `Asked ${i}?`, request_id: `req_${i}` })
44
- }
45
- expect(() => registerQuestion('flood', env, { question: 'One too many?' })).toThrow(
46
- /already open/,
47
- )
48
- })
49
- })
@@ -1,47 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { fileURLToPath } from 'node:url'
3
- import type { HookInstallableHarness } from './harnesses.js'
4
- import type { HookEnvelope } from './hooks.js'
5
-
6
- /** Private handoff from the short prompt hook to its detached question owner. */
7
- export const QUESTION_SETTLEMENT_INPUT_ENV = 'NOTIFAI_INTERNAL_QUESTION_SETTLEMENT_INPUT'
8
-
9
- export interface QuestionSettlementLaunch {
10
- envelope: Pick<HookEnvelope, 'session_id' | 'cwd'>
11
- harness: HookInstallableHarness
12
- }
13
-
14
- /**
15
- * Launch the exact installed CLI build as a detached owner.
16
- *
17
- * UserPromptSubmit sits in front of the User's new turn, so it may only pay the
18
- * process-spawn cost. The child owns submission and the complete answer window.
19
- */
20
- export function spawnQuestionSettlement(launch: QuestionSettlementLaunch): void {
21
- const child = spawn(
22
- process.execPath,
23
- [
24
- fileURLToPath(new URL('./main.js', import.meta.url)),
25
- 'hook',
26
- 'question-settlement',
27
- '--owner',
28
- 'notifai',
29
- '--harness',
30
- launch.harness,
31
- ],
32
- {
33
- cwd: launch.envelope.cwd ?? process.cwd(),
34
- detached: true,
35
- env: {
36
- ...process.env,
37
- [QUESTION_SETTLEMENT_INPUT_ENV]: JSON.stringify(launch.envelope),
38
- },
39
- stdio: 'ignore',
40
- windowsHide: true,
41
- },
42
- )
43
- // Spawn failures are diagnosed by the next ordinary lifecycle hook, which
44
- // still owns the durable registration. Never turn one into a prompt delay.
45
- child.once('error', () => undefined)
46
- child.unref()
47
- }
@@ -1,56 +0,0 @@
1
- import { REPLY_MAX_WINDOW_SECONDS } from '@raidiant/notifai-protocol'
2
-
3
- /**
4
- * Time before reply-window admission that the Stop owner may spend reading its
5
- * input, serving terminal-first grace, resolving devices, and submitting.
6
- *
7
- * Grace is capped at six minutes and every network operation is independently
8
- * bounded. Twenty minutes lets setup spend up to ten while preserving the
9
- * separate completion margin below; neither may borrow from the answer window
10
- * the service actually commits.
11
- */
12
- export const QUESTION_WAITER_STARTUP_HEADROOM_SECONDS = 20 * 60
13
-
14
- /**
15
- * Minimum lifetime still available when a question is admitted.
16
- *
17
- * The server window starts during submission, not when the hook process
18
- * started. Keeping ten minutes beyond the requested window covers the bounded
19
- * submit, clock skew, close fence, and route handoff. If setup used more of the
20
- * allowance, the frozen question remains local for a successor owner instead
21
- * of publishing a window this process cannot observe completely.
22
- */
23
- export const QUESTION_SUBMISSION_COMPLETION_HEADROOM_SECONDS = 10 * 60
24
-
25
- /**
26
- * The longest one Stop owner may live from process start.
27
- *
28
- * The service can accept an answer for up to 72 hours. Starting this budget
29
- * before submission is deliberate: the startup allowance above ensures that
30
- * even the maximum reply window still ends before the owner does.
31
- */
32
- export const QUESTION_WAITER_CEILING_SECONDS =
33
- REPLY_MAX_WINDOW_SECONDS + QUESTION_WAITER_STARTUP_HEADROOM_SECONDS
34
-
35
- /** Time for final fencing, diagnostics, stdout handoff, and clean process exit. */
36
- export const QUESTION_STOP_TEARDOWN_HEADROOM_SECONDS = 60
37
-
38
- /**
39
- * Declared harness timeout for every Stop handler that owns Question Routing.
40
- * It must outlive the runtime owner; otherwise the harness silently kills the
41
- * only process capable of continuing the exact Agent Session.
42
- */
43
- export const QUESTION_STOP_TIMEOUT_SECONDS =
44
- QUESTION_WAITER_CEILING_SECONDS + QUESTION_STOP_TEARDOWN_HEADROOM_SECONDS
45
-
46
- /**
47
- * Short Stop budget for harnesses that cannot own asynchronous Question
48
- * Routing. They retire/refuse promptly and never wait through an answer window.
49
- */
50
- export const NON_ROUTING_STOP_TIMEOUT_SECONDS = 540
51
-
52
- /**
53
- * Pre-PID claim files came only from the former eight-minute owner. Retain that
54
- * recovery bound instead of making a corrupt legacy file block for 72 hours.
55
- */
56
- export const LEGACY_QUESTION_CLAIM_TTL_SECONDS = 480
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { refreshAfterMenuAction } from './readiness.js'
3
-
4
- describe('refreshAfterMenuAction', () => {
5
- it('does not re-assess after doctor, a test send, or a device list', () => {
6
- expect(refreshAfterMenuAction('doctor', true)).toBeNull()
7
- expect(refreshAfterMenuAction('test', true)).toBeNull()
8
- expect(refreshAfterMenuAction('devices', true)).toBeNull()
9
- })
10
-
11
- it('does not re-assess when the action changed nothing', () => {
12
- expect(refreshAfterMenuAction('account', false)).toBeNull()
13
- expect(refreshAfterMenuAction('settings', false)).toBeNull()
14
- expect(refreshAfterMenuAction('routing', false)).toBeNull()
15
- expect(refreshAfterMenuAction('setup', false)).toBeNull()
16
- })
17
-
18
- it('re-probes the service only after an action that can change it', () => {
19
- expect(refreshAfterMenuAction('setup', true)).toEqual(['local', 'remote'])
20
- expect(refreshAfterMenuAction('account', true)).toEqual(['local', 'remote'])
21
- expect(refreshAfterMenuAction('settings', true)).toEqual(['local'])
22
- expect(refreshAfterMenuAction('routing', true)).toEqual(['local'])
23
- expect(refreshAfterMenuAction('settings', true, { remote: true })).toEqual(['local', 'remote'])
24
- })
25
- })
package/src/readiness.ts DELETED
@@ -1,212 +0,0 @@
1
- /**
2
- * What "set up" means, in one place.
3
- *
4
- * `init` and `doctor` used to each carry their own idea of this — a fixed
5
- * five-step script in one, a flat list of checks in the other — which is two
6
- * copies of the same knowledge that can disagree, and did: doctor called
7
- * missing hooks fine while init offered to install them, and only doctor knew
8
- * what a revoked credential looked like.
9
- *
10
- * So the model lives here and both commands render it. `doctor` shows every
11
- * state at once, because a report is what it is for. `init` walks the same
12
- * list in order and stops at the first thing standing in the way, because
13
- * someone handed five tasks does none of them. An agent gets the same states
14
- * through a third renderer that never prompts.
15
- *
16
- * The ordering is a dependency chain, not a preference: there is no point
17
- * asking the server about devices before there is a credential to ask with,
18
- * and no point reporting a stale contract to someone who cannot reach the
19
- * server at all. Assessment stops descending when a prerequisite is missing
20
- * and marks what follows `unknown` rather than guessing.
21
- */
22
-
23
- /** Whether the gap can be closed from here, and by whom. */
24
- export type Remedy =
25
- /**
26
- * The CLI can do it unattended — no prompt, no human. `command` is how a
27
- * reader would ask for it later, for the case where it was declined or
28
- * skipped; it is never needed to perform the action.
29
- */
30
- | { by: 'cli'; summary: string; command?: string }
31
- /**
32
- * A human at this terminal. `interactive` marks the ones the CLI can drive
33
- * on their behalf when someone is actually watching — a browser sign-in is
34
- * the CLI's to launch but the human's to complete, so an agent must still
35
- * treat it as out of reach.
36
- */
37
- | {
38
- by: 'user-here'
39
- summary: string
40
- command: string
41
- interactive?: boolean
42
- /** Stable agent-facing description of an unavoidable harness-owned action. */
43
- user_action?: { code: string; harness: string; action: string; message: string }
44
- }
45
- /**
46
- * A human somewhere else entirely — phone in hand, app store, permission
47
- * dialog. The class the old init could only print a sentence about, and the
48
- * one most likely to end an onboarding.
49
- */
50
- | { by: 'user-elsewhere'; summary: string }
51
-
52
- export type StateStatus =
53
- /** Satisfied. */
54
- | 'ready'
55
- /** Not satisfied, and it stands in the way. */
56
- | 'gap'
57
- /** Not satisfied, and that is a legitimate choice. */
58
- | 'optional-gap'
59
- /** A prerequisite is missing, so this was never evaluated. */
60
- | 'unknown'
61
-
62
- export interface ReadinessState {
63
- id: string
64
- /** Human words for what this is, not the field name. */
65
- title: string
66
- status: StateStatus
67
- /** The current condition, phrased for the human renderer. */
68
- detail: string
69
- /** Structured diagnostics emitted only by --json / non-human surfaces. */
70
- technical?: unknown
71
- remedy?: Remedy
72
- }
73
-
74
- export interface Readiness {
75
- states: ReadinessState[]
76
- }
77
-
78
- /** The first thing standing in the way, or null when nothing is. */
79
- export function firstBlocker(readiness: Readiness): ReadinessState | null {
80
- return readiness.states.find((s) => s.status === 'gap') ?? null
81
- }
82
-
83
- /**
84
- * Question Routing: what lets a question reach a phone when nobody is at the
85
- * terminal. Hooks, their sub-states, and the setting that admits them.
86
- *
87
- * Separate from `isOptionalSetup` because these are also the states a human
88
- * should not be handed as leftovers — "Optional, not set up" is a fair line
89
- * about the agent skill and a confusing one about a session pointer.
90
- */
91
- export function isOptionalAutomation(id: string): boolean {
92
- return id === 'hooks' || id.startsWith('hooks-') || id === 'question-routing-settings'
93
- }
94
-
95
- /**
96
- * Automation layered on top of a product that already sends: Question Routing
97
- * and the agent guidance skill.
98
- *
99
- * Real, and reported. `doctor` is the report and judges it strictly. Anything
100
- * whose job is to get a first notification delivered treats it as a line to
101
- * print, never a gate — a hook diagnostic or a duplicate skill install in
102
- * front of the delivery proof means a setup that can already send never proves
103
- * that it can. The one that fires most often, a turn that has not ended yet,
104
- * cannot be closed by the agent standing inside that turn at all.
105
- */
106
- export function isOptionalSetup(id: string): boolean {
107
- return id === 'skill' || isOptionalAutomation(id)
108
- }
109
-
110
- /** The first thing in the way that is not optional automation. */
111
- export function firstRequiredBlocker(readiness: Readiness): ReadinessState | null {
112
- return readiness.states.find((s) => s.status === 'gap' && !isOptionalSetup(s.id)) ?? null
113
- }
114
-
115
- /**
116
- * Ready enough to be useful, which is not the same as every box ticked.
117
- *
118
- * Hooks and the agent skill are genuinely optional — `send` works without
119
- * them — so an install that declined both is finished, not half-done. Saying
120
- * otherwise trains people to ignore the summary line, and it is the summary
121
- * line an agent branches on.
122
- */
123
- export function isReady(readiness: Readiness): boolean {
124
- return firstRequiredBlocker(readiness) === null
125
- }
126
-
127
- const SEND_PREREQUISITES = new Set([
128
- 'cli-bin',
129
- 'credential',
130
- 'server',
131
- 'contract',
132
- 'auth',
133
- 'devices',
134
- ])
135
-
136
- /** Baseline Notification Requests can be submitted in the final assessed state. */
137
- export function canSend(readiness: Readiness): boolean {
138
- return [...SEND_PREREQUISITES].every((id) => {
139
- const state = readiness.states.find((candidate) => candidate.id === id)
140
- return state?.status === 'ready' || state?.status === 'optional-gap'
141
- })
142
- }
143
-
144
- /** This exact invocation has a fully evidenced asynchronous question route. */
145
- export function questionRoutingReady(readiness: Readiness): boolean {
146
- const relevant = readiness.states.filter(
147
- (state) =>
148
- state.id === 'question-routing-settings' ||
149
- (state.id === 'hooks' || state.id.startsWith('hooks-')) && state.id !== 'hooks-detected',
150
- )
151
- return relevant.length > 0 && relevant.every((state) => state.status === 'ready')
152
- }
153
-
154
- /** Stable machine renderer shared by init and doctor. */
155
- export function readinessJson(readiness: Readiness): object {
156
- return {
157
- ready: isReady(readiness),
158
- can_send: canSend(readiness),
159
- question_routing_ready: questionRoutingReady(readiness),
160
- states: readiness.states.map((state) => ({
161
- id: state.id,
162
- title: state.title,
163
- status: state.status,
164
- detail: state.detail,
165
- technical: state.technical ?? null,
166
- remedy: state.remedy ?? null,
167
- })),
168
- }
169
- }
170
-
171
- /** Everything that could still be done, blocking or not, in dependency order. */
172
- export function openItems(readiness: Readiness): ReadinessState[] {
173
- return readiness.states.filter((s) => s.status === 'gap' || s.status === 'optional-gap')
174
- }
175
-
176
- /**
177
- * Work an assessment can do.
178
- *
179
- * `local` is this machine's files: project config, hook installs, skill lock
180
- * files. `remote` is everything that leaves the process — the keychain, the
181
- * service, the account, devices, and delivery evidence. The split exists so a
182
- * redraw after a settings change does not pay for a health check.
183
- */
184
- export type ReadinessRefresh = 'local' | 'remote'
185
-
186
- /**
187
- * After a menu action, which readiness work is worth doing.
188
- *
189
- * `null` means keep the assessment already in hand. Doctor is the report, not
190
- * a mutation: the check it just ran is the redraw. A test send and a device
191
- * list cannot change setup. Settings and hook wiring stay on this machine
192
- * unless the caller says the service URL itself moved.
193
- */
194
- export function refreshAfterMenuAction(
195
- action: 'setup' | 'account' | 'test' | 'devices' | 'settings' | 'routing' | 'doctor',
196
- changed: boolean,
197
- options: { remote?: boolean } = {},
198
- ): readonly ReadinessRefresh[] | null {
199
- switch (action) {
200
- case 'doctor':
201
- case 'test':
202
- case 'devices':
203
- return null
204
- case 'setup':
205
- case 'account':
206
- return changed ? ['local', 'remote'] : null
207
- case 'settings':
208
- case 'routing':
209
- if (!changed) return null
210
- return options.remote === true ? ['local', 'remote'] : ['local']
211
- }
212
- }
package/src/release.ts DELETED
@@ -1,51 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- /**
6
- * What this build is, according to the manifest npm actually shipped.
7
- *
8
- * Read from disk rather than compiled in, because a constant baked into
9
- * `dist/` is only ever as fresh as the last build. A stale `dist/` sitting
10
- * next to a newer `package.json` is a real failure mode — it produces a CLI
11
- * that reports the new version perfectly while running the old code — and the
12
- * only defence that survives it is refusing to carry a second copy of the
13
- * number at all.
14
- *
15
- * The manifest sits one level above both `src/` and `dist/`, so a single
16
- * relative path resolves in a source checkout and in a published install.
17
- *
18
- * Returns null rather than guessing: a build that cannot establish its own
19
- * identity has to say so, so callers can degrade honestly instead of
20
- * asserting something they do not know.
21
- */
22
- export function packageVersion(): string | null {
23
- try {
24
- const manifest = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
25
- const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
26
- if (typeof parsed === 'object' && parsed !== null) {
27
- const { version } = parsed as { version?: unknown }
28
- if (typeof version === 'string' && version.length > 0) return version
29
- }
30
- } catch {
31
- // Fall through: an unreadable manifest must not stop the CLI running.
32
- }
33
- return null
34
- }
35
-
36
- /**
37
- * Where `npx skills add` fetches the agent skill from, as printed to users.
38
- *
39
- * Always an immutable tag naming this exact build. The skill and the CLI ship
40
- * from one repository at one version, and the skill describes how to drive the
41
- * CLI — so a moving ref would hand someone guidance for a tool they do not
42
- * have. Deriving it from the version is what keeps the two from disagreeing;
43
- * it used to be maintained by hand in three places, and the copies drifted.
44
- *
45
- * In the skills CLI grammar `owner/repo#ref` selects a Git ref, while
46
- * `owner/repo@name` selects a skill — so the `#` here is load-bearing.
47
- */
48
- export function skillsSource(): string | null {
49
- const version = packageVersion()
50
- return version === null ? null : `Raidiant-io/notifai#v${version}`
51
- }