@raidiant/notifai 10.1.1 → 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 (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. 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
- }