@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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- 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
|
-
})
|
package/src/commands-skill.ts
DELETED
|
@@ -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
|
-
})
|
package/src/commands-sounds.ts
DELETED
|
@@ -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
|
-
}
|