@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,72 +0,0 @@
1
- import { mkdtempSync, rmSync } from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { afterEach, describe, expect, it } from 'vitest'
5
- import type { CommandDeps } from './commands-core.js'
6
- import type { MachineCredential } from './credentials.js'
7
- import { readSetupProof, setupProofApplies, writeSetupProof } from './commands-setup-proof.js'
8
-
9
- const roots: string[] = []
10
- afterEach(() => {
11
- while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true })
12
- })
13
-
14
- function deps(root: string, cwd: string, credential: MachineCredential): CommandDeps {
15
- return {
16
- cwd,
17
- env: { XDG_STATE_HOME: path.join(root, 'state'), XDG_CONFIG_HOME: path.join(root, 'config') },
18
- io: { out: () => {}, err: () => {}, confirm: async () => false, openUrl: () => {} },
19
- store: {
20
- load: () => credential,
21
- save: () => {},
22
- clear: () => {},
23
- describe: () => 'test',
24
- },
25
- }
26
- }
27
-
28
- describe('stable setup delivery proof identity', () => {
29
- it('is shared by linked checkouts of one Project and separated by Project', () => {
30
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-proof-identity-'))
31
- roots.push(root)
32
- const credential = {
33
- machineId: 'mac_one',
34
- machineName: 'One',
35
- baseUrl: 'https://app.notifai.test',
36
- secret: 'approval-one',
37
- }
38
- const first = deps(root, path.join(root, 'worktree-a'), credential)
39
- const second = deps(root, path.join(root, 'worktree-b'), credential)
40
- const proof = {
41
- request_id: 'req_proof',
42
- device_id: 'dev_one',
43
- project: 'shared-project',
44
- started_at: '2026-08-26T00:00:00.000Z',
45
- }
46
- expect(writeSetupProof(first, proof)).toBe(true)
47
- expect(readSetupProof(second, 'shared-project')).toEqual(proof)
48
- expect(readSetupProof(second, 'different-project')).toBeNull()
49
- })
50
-
51
- it('invalidates on service, machine approval, machine, or device change', () => {
52
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-proof-invalidation-'))
53
- roots.push(root)
54
- const credential = {
55
- machineId: 'mac_one',
56
- machineName: 'One',
57
- baseUrl: 'https://app.notifai.test',
58
- secret: 'approval-one',
59
- }
60
- const original = deps(root, root, credential)
61
- expect(writeSetupProof(original, {
62
- request_id: 'req_proof',
63
- device_id: 'dev_one',
64
- project: 'project',
65
- started_at: '2026-08-26T00:00:00.000Z',
66
- })).toBe(true)
67
- expect(readSetupProof(deps(root, root, { ...credential, baseUrl: 'https://other.test' }), 'project')).toBeNull()
68
- expect(readSetupProof(deps(root, root, { ...credential, machineId: 'mac_two' }), 'project')).toBeNull()
69
- expect(readSetupProof(deps(root, root, { ...credential, secret: 'approval-two' }), 'project')).toBeNull()
70
- expect(setupProofApplies(readSetupProof(original, 'project'), 'project', ['dev_two'])).toBe(false)
71
- })
72
- })
@@ -1,124 +0,0 @@
1
- import { type EvidenceSnapshot } from '@raidiant/notifai-protocol'
2
- import { createHash } from 'node:crypto'
3
- import {
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- writeFileSync,
8
- } from 'node:fs'
9
- import path from 'node:path'
10
- import { stateDir } from './config.js'
11
- import type { CommandDeps } from './commands-core.js'
12
- import { inferInvocationContext } from './invocation-context.js'
13
-
14
- export interface SetupProofRecord {
15
- request_id: string
16
- device_id: string
17
- project: string | null
18
- started_at: string
19
- }
20
-
21
- export function setupProofProject(deps: CommandDeps, configured: string | null): string | null {
22
- return configured ?? inferInvocationContext(deps.cwd).project
23
- }
24
-
25
- function setupProofPath(deps: CommandDeps, project: string | null): string | null {
26
- const credential = deps.store.load()
27
- if (credential === null) return null
28
- const approval = createHash('sha256')
29
- .update('notifai-setup-proof-approval-v1\0')
30
- .update(credential.secret)
31
- .digest('base64url')
32
- const digest = createHash('sha256')
33
- .update(JSON.stringify({ project, machine_id: credential.machineId, service: credential.baseUrl, approval }))
34
- .digest('hex')
35
- .slice(0, 32)
36
- return path.join(stateDir(deps.env), 'setup-proofs', `${digest}.json`)
37
- }
38
-
39
- export function readSetupProof(deps: CommandDeps, project: string | null): SetupProofRecord | null {
40
- const file = setupProofPath(deps, project)
41
- if (file === null) return null
42
- if (!existsSync(file)) return null
43
- try {
44
- const parsed = JSON.parse(readFileSync(file, 'utf8')) as Partial<SetupProofRecord>
45
- return typeof parsed.request_id === 'string' &&
46
- typeof parsed.device_id === 'string' &&
47
- (typeof parsed.project === 'string' || parsed.project === null) &&
48
- typeof parsed.started_at === 'string'
49
- ? (parsed as SetupProofRecord)
50
- : null
51
- } catch {
52
- // Corrupt local evidence is not readiness. A fresh proof replaces it.
53
- return null
54
- }
55
- }
56
-
57
- export function writeSetupProof(deps: CommandDeps, proof: SetupProofRecord): boolean {
58
- const file = setupProofPath(deps, proof.project)
59
- if (file === null) {
60
- deps.io.err('Could not save setup proof without an Approved Machine credential.')
61
- return false
62
- }
63
- try {
64
- mkdirSync(path.dirname(file), { recursive: true })
65
- writeFileSync(file, `${JSON.stringify(proof, null, 2)}\n`, { mode: 0o600 })
66
- return true
67
- } catch (err) {
68
- deps.io.err(
69
- `Could not save setup proof ${proof.request_id} at ${file}: ${String(err)}`,
70
- )
71
- return false
72
- }
73
- }
74
-
75
- export function observedCompanionReceipt(
76
- snapshot: EvidenceSnapshot,
77
- deviceId: string,
78
- ): { delivery: EvidenceSnapshot['deliveries'][number]; observedAt: string } | null {
79
- const delivery = snapshot.deliveries.find((candidate) => candidate.device_id === deviceId)
80
- if (!delivery) return null
81
- if (delivery.companion_receipt.state === 'observed' && delivery.companion_receipt.observed_at) {
82
- return { delivery, observedAt: delivery.companion_receipt.observed_at }
83
- }
84
- const receipt = delivery.events.find((event) => event.stage === 'companion_received')
85
- return receipt ? { delivery, observedAt: receipt.occurred_at } : null
86
- }
87
-
88
- /** Replace an unknown setup proof after this age; recent in-flight proofs stay. */
89
- export const SETUP_PROOF_STALE_MS = 24 * 60 * 60 * 1000
90
-
91
- export function setupProofIsStale(proof: SetupProofRecord, now: number): boolean {
92
- const started = Date.parse(proof.started_at)
93
- return Number.isFinite(started) && now - started > SETUP_PROOF_STALE_MS
94
- }
95
-
96
- export function setupProofApplies(
97
- proof: SetupProofRecord | null,
98
- project: string | null,
99
- deviceIds: readonly string[],
100
- ): proof is SetupProofRecord {
101
- return proof !== null && proof.project === project && deviceIds.includes(proof.device_id)
102
- }
103
-
104
- /**
105
- * Persist ordinary send/status Companion Receipts as the project's delivery
106
- * proof so doctor does not demand a second verification request.
107
- */
108
- export function recordObservedDeliveryProof(
109
- deps: CommandDeps,
110
- snapshot: EvidenceSnapshot,
111
- project: string | null,
112
- ): boolean {
113
- for (const delivery of snapshot.deliveries) {
114
- const observed = observedCompanionReceipt(snapshot, delivery.device_id)
115
- if (observed === null) continue
116
- return writeSetupProof(deps, {
117
- request_id: snapshot.request_id,
118
- device_id: delivery.device_id,
119
- project,
120
- started_at: observed.observedAt,
121
- })
122
- }
123
- return false
124
- }
@@ -1,44 +0,0 @@
1
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { describe, expect, it } from 'vitest'
5
- import type { CommandDeps } from './commands-core.js'
6
- import { skillReadiness, SKILLS_SOURCE } from './commands-skill.js'
7
-
8
- describe('development CLI skill parity', () => {
9
- it('reports an exact gap instead of calling a stale released skill ready', async () => {
10
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-dev-skill-'))
11
- const installedPath = path.join(root, 'skills', 'notifai')
12
- mkdirSync(installedPath, { recursive: true })
13
- writeFileSync(path.join(installedPath, 'SKILL.md'), '# stale released skill\n')
14
- const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE ?? '')
15
- if (match === null) throw new Error('test build has no skill release pin')
16
- const deps = {
17
- cwd: root,
18
- env: {},
19
- io: { out() {}, err() {}, confirm: async () => false, openUrl() {} },
20
- store: { load: () => null, save() {}, clear() {}, describe: () => 'test' },
21
- nativeSkills: {
22
- list: async (scope: 'project' | 'global') => ({
23
- skills: scope === 'global'
24
- ? [{ name: 'notifai', scope, path: installedPath, source: match[1]!, sourceType: 'github' as const, sourceUrl: `https://github.com/${match[1]}.git`, ref: match[2]! }]
25
- : [],
26
- }),
27
- add: async () => 0,
28
- remove: async () => 0,
29
- },
30
- } satisfies CommandDeps
31
-
32
- const state = await skillReadiness(deps)
33
- expect(state).toMatchObject({
34
- status: 'gap',
35
- technical: {
36
- resolution: 'development-cli-skill-mismatch',
37
- ref: match[2],
38
- checkout_digest: expect.stringMatching(/^sha256:/),
39
- installed_digest: expect.stringMatching(/^sha256:/),
40
- },
41
- })
42
- expect(state.remedy).toBeUndefined()
43
- })
44
- })
@@ -1,204 +0,0 @@
1
- import { SKILLS_INSTALLER_SPEC, type NativeSkill, type SkillScope } from './native-skills.js'
2
- import { createHash } from 'node:crypto'
3
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
4
- import path from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
- import { type ReadinessState } from './readiness.js'
7
- import { skillsSource } from './release.js'
8
- import type { CommandDeps } from './commands-core.js'
9
-
10
- /**
11
- * Where `npx skills add` fetches the optional agent skill from, derived from
12
- * this build's own version so the pin cannot drift from the release it names.
13
- * Null when the build cannot establish its version; see `./release.js`.
14
- */
15
- export const SKILLS_SOURCE: string | null = skillsSource()
16
-
17
- /**
18
- * How to refer to the pin in user-facing text.
19
- *
20
- * Only reached in a corrupted install, where naming the tag is impossible but
21
- * saying nothing would be worse — the sentence still has to read as English.
22
- */
23
- const SKILLS_SOURCE_LABEL = SKILLS_SOURCE ?? 'the public release tag matching this CLI'
24
-
25
- const SKILL_SCOPES: readonly SkillScope[] = ['project', 'global']
26
-
27
- function markdownTreeDigest(root: string): string | null {
28
- if (!existsSync(root)) return null
29
- const files: string[] = []
30
- const walk = (directory: string): void => {
31
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
32
- const absolute = path.join(directory, entry.name)
33
- if (entry.isDirectory()) walk(absolute)
34
- else if (entry.isFile() && entry.name.endsWith('.md')) files.push(absolute)
35
- }
36
- }
37
- try {
38
- walk(root)
39
- const hash = createHash('sha256')
40
- for (const file of files.sort()) {
41
- hash.update(path.relative(root, file))
42
- hash.update('\0')
43
- hash.update(readFileSync(file))
44
- hash.update('\0')
45
- }
46
- return `sha256:${hash.digest('hex')}`
47
- } catch {
48
- return null
49
- }
50
- }
51
-
52
- function developmentSkillMismatch(skill: NativeSkill): { checkout: string; installed: string } | null {
53
- const checkoutRoot = fileURLToPath(new URL('../../../skills/notifai/', import.meta.url))
54
- const checkout = markdownTreeDigest(checkoutRoot)
55
- const installed = markdownTreeDigest(skill.path)
56
- return checkout !== null && installed !== null && checkout !== installed
57
- ? { checkout, installed }
58
- : null
59
- }
60
-
61
- function skillSourceParts(): { source: string; ref: string } | null {
62
- if (SKILLS_SOURCE === null) return null
63
- const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE)
64
- return match === null ? null : { source: match[1]!, ref: match[2]! }
65
- }
66
-
67
- function expectedSkill(skill: NativeSkill): boolean {
68
- const expected = skillSourceParts()
69
- return (
70
- expected !== null &&
71
- skill.name === 'notifai' &&
72
- skill.source === expected.source &&
73
- skill.sourceType === 'github' &&
74
- skill.ref === expected.ref
75
- )
76
- }
77
-
78
- function skillPin(skill: NativeSkill): string {
79
- return skill.ref ?? 'unknown pin'
80
- }
81
-
82
- export interface ScopedNotifaiSkills {
83
- installed: NativeSkill[]
84
- errors: string[]
85
- }
86
-
87
- /**
88
- * Installer-managed notifai skills in both scopes. Duplicate detection has to
89
- * see the pair; a selected-scope filter would hide the copy the harness still
90
- * lists.
91
- */
92
- export async function listScopedNotifaiSkills(deps: CommandDeps): Promise<ScopedNotifaiSkills> {
93
- const results = await Promise.all(
94
- SKILL_SCOPES.map(async (scope) => {
95
- if (deps.nativeSkills === undefined) return { scope, skills: [] as NativeSkill[] }
96
- try {
97
- return { scope, ...(await deps.nativeSkills.list(scope, deps.cwd, deps.env)) }
98
- } catch (err) {
99
- return { scope, skills: [] as NativeSkill[], error: String(err) }
100
- }
101
- }),
102
- )
103
- return {
104
- installed: results.flatMap(({ skills }) => skills.filter((skill) => skill.name === 'notifai')),
105
- errors: results
106
- .filter((result) => result.error !== undefined)
107
- .map((result) => `${result.scope}: ${result.error}`),
108
- }
109
- }
110
-
111
- function duplicateSkillState(installed: NativeSkill[], selectedScope?: SkillScope): ReadinessState {
112
- const project = installed.find((skill) => skill.scope === 'project')
113
- const global = installed.find((skill) => skill.scope === 'global')
114
- const projectPin = project === undefined ? 'not installed' : skillPin(project)
115
- const globalPin = global === undefined ? 'not installed' : skillPin(global)
116
- return {
117
- id: 'skill',
118
- title: 'Agent guidance skill',
119
- status: 'gap',
120
- detail:
121
- `project (${projectPin}) and global (${globalPin}) are both installed, so the harness lists both. ` +
122
- 'Keep either project or global and uninstall the other.',
123
- technical: {
124
- project:
125
- project === undefined
126
- ? null
127
- : { ref: project.ref, path: project.path, current: expectedSkill(project) },
128
- global:
129
- global === undefined
130
- ? null
131
- : { ref: global.ref, path: global.path, current: expectedSkill(global) },
132
- resolution: 'both-listed',
133
- },
134
- remedy:
135
- selectedScope === undefined
136
- ? {
137
- by: 'user-here',
138
- summary:
139
- 'keep either the project or the machine-global skill and uninstall the other; add --global to remove the machine-global copy',
140
- command: `npx -y ${SKILLS_INSTALLER_SPEC} remove notifai`,
141
- }
142
- : {
143
- by: 'cli',
144
- summary: `keep the ${selectedScope} skill and uninstall the other`,
145
- command: `notifai init --skills --setup-scope ${selectedScope}`,
146
- },
147
- }
148
- }
149
-
150
- export async function skillReadiness(
151
- deps: CommandDeps,
152
- selectedScope?: SkillScope,
153
- ): Promise<ReadinessState> {
154
- const { installed, errors } = await listScopedNotifaiSkills(deps)
155
- if (installed.length > 1) return duplicateSkillState(installed, selectedScope)
156
-
157
- const current = installed.find(expectedSkill)
158
- if (current !== undefined) {
159
- const mismatch = developmentSkillMismatch(current)
160
- if (mismatch !== null) {
161
- return {
162
- id: 'skill',
163
- title: 'Agent guidance skill',
164
- status: 'gap',
165
- detail:
166
- `the active development CLI's shipped guidance differs from the installer-managed ${skillPin(current)} skill. ` +
167
- 'Released installs remain immutable; publish a new CLI/skill release before treating this combination as ready.',
168
- technical: {
169
- resolution: 'development-cli-skill-mismatch',
170
- scope: current.scope,
171
- ref: current.ref,
172
- path: current.path,
173
- checkout_digest: mismatch.checkout,
174
- installed_digest: mismatch.installed,
175
- },
176
- }
177
- }
178
- return {
179
- id: 'skill',
180
- title: 'Agent guidance skill',
181
- status: 'ready',
182
- detail: `installed from ${SKILLS_SOURCE_LABEL} in the ${current.scope} scope`,
183
- }
184
- }
185
-
186
- const scopeText = selectedScope === undefined ? 'project or machine-global scope' : `${selectedScope} scope`
187
- return {
188
- id: 'skill',
189
- title: 'Agent guidance skill',
190
- status: 'optional-gap',
191
- detail:
192
- errors.length > 0
193
- ? `could not verify installer-managed state in ${scopeText} (${errors.join('; ')})`
194
- : `not installed from ${SKILLS_SOURCE_LABEL} in ${scopeText}`,
195
- remedy: {
196
- by: 'cli',
197
- summary: 'install the skill agents follow when deciding to notify',
198
- command:
199
- selectedScope === undefined
200
- ? 'notifai init --skills --setup-scope project'
201
- : `notifai init --skills --setup-scope ${selectedScope}`,
202
- },
203
- }
204
- }
@@ -1,106 +0,0 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
3
- import { describe, expect, it } from 'vitest'
4
- import type { ApiClient } from './client.js'
5
- import { EXIT, type CommandDeps, type CommandIo } from './commands-core.js'
6
- import { soundsCommand } from './commands-sounds.js'
7
-
8
- class CapturedIo implements CommandIo {
9
- readonly outLines: string[] = []
10
- readonly errLines: string[] = []
11
- out(line: string): void {
12
- this.outLines.push(line)
13
- }
14
- err(line: string): void {
15
- this.errLines.push(line)
16
- }
17
- async confirm(): Promise<boolean> {
18
- return true
19
- }
20
- openUrl(): void {}
21
- }
22
-
23
- function makeDeps(io: CapturedIo, client: ApiClient): CommandDeps {
24
- const testRoot = path.join(os.tmpdir(), 'notifai-cli-sounds-tests')
25
- return {
26
- io,
27
- store: {
28
- load: () => ({
29
- machineId: 'mac_test',
30
- secret: 'test-secret',
31
- baseUrl: 'https://test.notifai.invalid',
32
- machineName: 'test-machine',
33
- }),
34
- save: () => {},
35
- clear: () => {},
36
- describe: () => 'test credential store',
37
- },
38
- env: {
39
- XDG_CONFIG_HOME: testRoot,
40
- XDG_STATE_HOME: path.join(os.tmpdir(), 'notifai-cli-sounds-tests-state'),
41
- },
42
- cwd: os.tmpdir(),
43
- clientFactory: () => client,
44
- }
45
- }
46
-
47
- const kitchenTimer = {
48
- sound_id: 'snd_kitchen',
49
- name: 'Kitchen timer',
50
- duration_ms: 2400,
51
- content_hash: 'a'.repeat(64),
52
- url: 'https://wav.example/kitchen.wav',
53
- }
54
-
55
- describe('soundsCommand', () => {
56
- it('lists shipped names and Account custom sounds', async () => {
57
- const io = new CapturedIo()
58
- const client = {
59
- listSounds: async () => ({ sounds: [kitchenTimer] }),
60
- } as unknown as ApiClient
61
-
62
- expect(await soundsCommand(makeDeps(io, client), {})).toBe(EXIT.ok)
63
- expect(io.outLines[0]).toBe('default Device default')
64
- expect(io.outLines).toContain('done completion chime')
65
- expect(io.outLines).toContain('none silent')
66
- expect(io.outLines.at(-1)).toBe('snd_kitchen Kitchen timer 2.4s')
67
- })
68
-
69
- it('prints the catalog as JSON when asked', async () => {
70
- const io = new CapturedIo()
71
- const client = {
72
- listSounds: async () => ({ sounds: [kitchenTimer] }),
73
- } as unknown as ApiClient
74
-
75
- expect(await soundsCommand(makeDeps(io, client), { json: true })).toBe(EXIT.ok)
76
- const parsed = JSON.parse(io.outLines.join('\n')) as {
77
- shipped: { ref: string; name: string }[]
78
- custom: typeof kitchenTimer[]
79
- }
80
- expect(parsed.shipped[0]).toEqual({ ref: 'default', name: 'Device default' })
81
- expect(parsed.custom).toEqual([kitchenTimer])
82
- })
83
-
84
- it('refuses to list custom sounds when this machine is not signed in', async () => {
85
- const io = new CapturedIo()
86
- let calls = 0
87
- const deps: CommandDeps = {
88
- ...makeDeps(io, {
89
- listSounds: async () => {
90
- calls += 1
91
- return { sounds: [] }
92
- },
93
- } as unknown as ApiClient),
94
- store: {
95
- load: () => null,
96
- save: () => {},
97
- clear: () => {},
98
- describe: () => 'empty store',
99
- },
100
- }
101
-
102
- expect(await soundsCommand(deps, {})).toBe(EXIT.auth)
103
- expect(calls).toBe(0)
104
- expect(io.errLines.join('\n')).toMatch(/not signed in/i)
105
- })
106
- })
@@ -1,49 +0,0 @@
1
- import type { ListSoundsResponse, SoundView } from '@raidiant/notifai-protocol'
2
- import {
3
- EXIT,
4
- authedClient,
5
- loadLoggedConfig,
6
- reportError,
7
- type CommandDeps,
8
- } from './commands-core.js'
9
- import { SHIPPED_SOUND_LISTING } from './sound-ref.js'
10
-
11
- export interface SoundsListing {
12
- shipped: readonly { ref: string; name: string }[]
13
- custom: SoundView[]
14
- }
15
-
16
- function formatSoundDuration(ms: number): string {
17
- const seconds = ms / 1000
18
- const text = Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1)
19
- return `${text}s`
20
- }
21
-
22
- export async function soundsCommand(
23
- deps: CommandDeps,
24
- flags: { json?: boolean },
25
- ): Promise<number> {
26
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
27
- const authed = authedClient(deps, config)
28
- if (!authed) return EXIT.auth
29
- try {
30
- const result: ListSoundsResponse = await authed.client.listSounds()
31
- const listing: SoundsListing = {
32
- shipped: SHIPPED_SOUND_LISTING,
33
- custom: result.sounds,
34
- }
35
- if (flags.json) {
36
- deps.io.out(JSON.stringify(listing, null, 2))
37
- return EXIT.ok
38
- }
39
- for (const sound of listing.shipped) {
40
- deps.io.out(`${sound.ref} ${sound.name}`)
41
- }
42
- for (const sound of listing.custom) {
43
- deps.io.out(`${sound.sound_id} ${sound.name} ${formatSoundDuration(sound.duration_ms)}`)
44
- }
45
- return EXIT.ok
46
- } catch (err) {
47
- return reportError(deps, err)
48
- }
49
- }