@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,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
- }