@raidiant/notifai 8.0.0 → 9.0.0
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 +41 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +71 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +63 -28
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-hooks.d.ts +9 -4
- package/dist/commands-hooks.js +430 -32
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +114 -32
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +61 -5
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +60 -11
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +8 -0
- package/dist/commands-setup-proof.js +31 -2
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +25 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.js +37 -5
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +20 -0
- package/dist/guidance-content.js +73 -8
- package/dist/guidance-content.js.map +1 -1
- package/dist/guidance.d.ts +21 -0
- package/dist/guidance.js +73 -18
- package/dist/guidance.js.map +1 -1
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +68 -7
- package/dist/hooks.js +231 -85
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +2 -13
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +14 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +13 -8
- package/dist/program.js.map +1 -1
- package/dist/send.d.ts +8 -1
- package/dist/send.js +17 -1
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +15 -0
- package/dist/session-activation.js +35 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +3 -3
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +99 -13
- package/src/commands-guidance.ts +75 -29
- package/src/commands-hooks.ts +503 -34
- package/src/commands-init.ts +139 -38
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +73 -10
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1083 -101
- package/src/commands.ts +1 -0
- package/src/config-schema.ts +28 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +49 -5
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +74 -8
- package/src/guidance.test.ts +42 -11
- package/src/guidance.ts +100 -18
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +746 -165
- package/src/hooks.ts +299 -124
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +1 -1
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +2 -14
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +14 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +40 -12
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +45 -0
- package/src/skill.test.ts +120 -4
- package/src/trust-policy.test.ts +701 -0
- package/src/url-policy.ts +399 -0
package/src/commands-send.ts
CHANGED
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
buildDraft,
|
|
15
15
|
formatReceipt,
|
|
16
16
|
receiptExitCode,
|
|
17
|
+
rejectAccidentalEscapedNewlines,
|
|
17
18
|
validateMediaInputs,
|
|
18
19
|
type SendFlags,
|
|
19
20
|
} from './send.js'
|
|
21
|
+
import { recordObservedDeliveryProof } from './commands-setup-proof.js'
|
|
20
22
|
import {
|
|
21
23
|
EXIT,
|
|
22
24
|
authedClient,
|
|
@@ -63,7 +65,7 @@ export async function sendCommand(
|
|
|
63
65
|
if (!flags.reply && flags.kind === undefined) {
|
|
64
66
|
deps.io.err(
|
|
65
67
|
'--kind is required: say what this notification is. ' +
|
|
66
|
-
'update (news) · done (finished) · failed (terminal failure) · blocked (cannot proceed). ' +
|
|
68
|
+
'update (news) · done (finished) · failed (terminal failure) · blocked (cannot proceed; no User reply would resume it). ' +
|
|
67
69
|
'--reply makes it a question without --kind.',
|
|
68
70
|
)
|
|
69
71
|
return EXIT.usage
|
|
@@ -111,6 +113,11 @@ export async function sendCommand(
|
|
|
111
113
|
)
|
|
112
114
|
return EXIT.usage
|
|
113
115
|
}
|
|
116
|
+
const escapedBody = rejectAccidentalEscapedNewlines(flags.body, flags.literalBackslashN)
|
|
117
|
+
if (escapedBody !== null) {
|
|
118
|
+
deps.io.err(escapedBody)
|
|
119
|
+
return EXIT.usage
|
|
120
|
+
}
|
|
114
121
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt)
|
|
115
122
|
if (mediaInputError !== null) {
|
|
116
123
|
deps.io.err(mediaInputError)
|
|
@@ -138,7 +145,7 @@ export async function sendCommand(
|
|
|
138
145
|
mediaIds.push(image)
|
|
139
146
|
continue
|
|
140
147
|
}
|
|
141
|
-
const uploaded = await uploadImage(deps, authed.client, image)
|
|
148
|
+
const uploaded = await uploadImage(deps, authed.client, image, config.media_origins.value)
|
|
142
149
|
if (!uploaded.ok) {
|
|
143
150
|
if (uploaded.error !== null) deps.io.err(uploaded.error)
|
|
144
151
|
return uploaded.exit
|
|
@@ -175,6 +182,15 @@ export async function sendCommand(
|
|
|
175
182
|
}
|
|
176
183
|
const waitSeconds = flags.noWait ? 0 : config.wait_seconds.value
|
|
177
184
|
const idempotencyKey = flags.idempotencyKey ?? `cli-${randomBytes(12).toString('base64url')}`
|
|
185
|
+
// Persist the retry identity before entering the ambiguous network boundary.
|
|
186
|
+
// If the process is killed after the server accepts but before a response,
|
|
187
|
+
// this is the only durable way to reconstruct the same Agent Event key.
|
|
188
|
+
log(deps).info('send.attempt', {
|
|
189
|
+
idempotency_key: idempotencyKey,
|
|
190
|
+
kind: flags.reply ? 'question' : (flags.kind ?? 'update'),
|
|
191
|
+
title: flags.title,
|
|
192
|
+
session_id: source.invocation.source?.session_id ?? null,
|
|
193
|
+
})
|
|
178
194
|
let receipt: SubmissionReceipt
|
|
179
195
|
try {
|
|
180
196
|
receipt = await authed.client.submit(
|
|
@@ -182,7 +198,20 @@ export async function sendCommand(
|
|
|
182
198
|
waitSeconds,
|
|
183
199
|
)
|
|
184
200
|
} catch (err) {
|
|
185
|
-
|
|
201
|
+
const exit = reportError(deps, err)
|
|
202
|
+
if (exit === EXIT.network) {
|
|
203
|
+
deps.io.err(
|
|
204
|
+
'The server may have accepted this Notification Request before the connection failed. ' +
|
|
205
|
+
`Retry the same send with \`--idempotency-key ${idempotencyKey}\` so this Agent Event cannot create a duplicate.`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
if (err instanceof ApiCallError && err.code === 'no_active_devices') {
|
|
209
|
+
deps.io.err(
|
|
210
|
+
'After setup succeeds, repeat this original Notification Request with ' +
|
|
211
|
+
`\`--idempotency-key ${idempotencyKey}\`; the setup verification request does not replace this Agent Event.`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
return exit
|
|
186
215
|
}
|
|
187
216
|
const receiptExit = receiptExitCode(receipt)
|
|
188
217
|
// The single most useful line in the log: it ties the local invocation to
|
|
@@ -191,7 +220,7 @@ export async function sendCommand(
|
|
|
191
220
|
log(deps).info('send.submitted', {
|
|
192
221
|
request_id: receipt.request_id,
|
|
193
222
|
kind: flags.reply ? 'question' : (flags.kind ?? 'update'),
|
|
194
|
-
|
|
223
|
+
title_chars: flags.title.length,
|
|
195
224
|
overall: receipt.overall,
|
|
196
225
|
replayed: receipt.replayed,
|
|
197
226
|
agent_acknowledgement_required: receipt.agent_acknowledgement_required,
|
|
@@ -363,19 +392,52 @@ export async function repliesCommand(
|
|
|
363
392
|
// Every delivered question in the session's queue, in registration order —
|
|
364
393
|
// an agent may have several outstanding at once.
|
|
365
394
|
const state = readSessionState(sessionId, deps.env)
|
|
366
|
-
|
|
395
|
+
const pending = Array.isArray(state.pending) ? state.pending : []
|
|
396
|
+
requestIds = pending
|
|
367
397
|
.map((entry) => entry.request_id)
|
|
368
398
|
.filter((id): id is string => id !== undefined)
|
|
399
|
+
const unsent = pending.filter((entry) => entry.request_id === undefined)
|
|
369
400
|
if (requestIds.length === 0) {
|
|
370
401
|
// `--json` is a promise about stdout, and an empty result is still a
|
|
371
402
|
// result: a caller that asked for machine-readable output must not have
|
|
372
403
|
// to parse an English sentence to learn there was nothing pending.
|
|
373
404
|
if (flags.json === true) {
|
|
374
|
-
deps.io.out(
|
|
405
|
+
deps.io.out(
|
|
406
|
+
JSON.stringify(
|
|
407
|
+
{
|
|
408
|
+
session_id: sessionId,
|
|
409
|
+
pending: unsent.map((entry) => ({
|
|
410
|
+
question: entry.question,
|
|
411
|
+
...(entry.question_id === undefined ? {} : { question_id: entry.question_id }),
|
|
412
|
+
})),
|
|
413
|
+
replies: [],
|
|
414
|
+
},
|
|
415
|
+
null,
|
|
416
|
+
2,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
} else if (unsent.length > 0) {
|
|
420
|
+
for (const entry of unsent) {
|
|
421
|
+
deps.io.out(
|
|
422
|
+
entry.question_id === undefined
|
|
423
|
+
? `unpushed question: ${entry.question}`
|
|
424
|
+
: `unpushed question ${entry.question_id}: ${entry.question}`,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
deps.io.out('Retire one with `notifai close <question_id>` so a later Stop will not send it.')
|
|
375
428
|
} else {
|
|
376
429
|
deps.io.err(`Session ${sessionId} has no pushed question pending.`)
|
|
377
430
|
}
|
|
378
|
-
return EXIT.noReply
|
|
431
|
+
return unsent.length > 0 ? EXIT.ok : EXIT.noReply
|
|
432
|
+
}
|
|
433
|
+
if (unsent.length > 0 && flags.json !== true) {
|
|
434
|
+
for (const entry of unsent) {
|
|
435
|
+
deps.io.out(
|
|
436
|
+
entry.question_id === undefined
|
|
437
|
+
? `unpushed question: ${entry.question}`
|
|
438
|
+
: `unpushed question ${entry.question_id}: ${entry.question}`,
|
|
439
|
+
)
|
|
440
|
+
}
|
|
379
441
|
}
|
|
380
442
|
}
|
|
381
443
|
if (requestIds.length === 0) {
|
|
@@ -527,8 +589,8 @@ function recordReplies(deps: CommandDeps, requestId: string, replies: readonly R
|
|
|
527
589
|
request_id: requestId,
|
|
528
590
|
sequence: reply.seq,
|
|
529
591
|
device: reply.device_name,
|
|
530
|
-
|
|
531
|
-
|
|
592
|
+
text_chars: reply.text.length,
|
|
593
|
+
answers_count: reply.answers?.length ?? 0,
|
|
532
594
|
source: reply.source,
|
|
533
595
|
})
|
|
534
596
|
}
|
|
@@ -587,11 +649,12 @@ export async function statusCommand(
|
|
|
587
649
|
if (!authed) return EXIT.auth
|
|
588
650
|
try {
|
|
589
651
|
const snapshot = await authed.client.evidence(requestId)
|
|
652
|
+
recordObservedDeliveryProof(deps, snapshot, config.project.value)
|
|
590
653
|
if (flags.json) {
|
|
591
654
|
deps.io.out(JSON.stringify(snapshot, null, 2))
|
|
592
655
|
return EXIT.ok
|
|
593
656
|
}
|
|
594
|
-
deps.io.out(`request ${snapshot.request_id}
|
|
657
|
+
deps.io.out(`request ${snapshot.request_id} — ${snapshot.overall}`)
|
|
595
658
|
let anyReplyReceived = false
|
|
596
659
|
for (const d of snapshot.deliveries) {
|
|
597
660
|
deps.io.out(` ${d.device_name}:`)
|
|
@@ -66,6 +66,40 @@ export function observedCompanionReceipt(
|
|
|
66
66
|
deviceId: string,
|
|
67
67
|
): { delivery: EvidenceSnapshot['deliveries'][number]; observedAt: string } | null {
|
|
68
68
|
const delivery = snapshot.deliveries.find((candidate) => candidate.device_id === deviceId)
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
if (!delivery) return null
|
|
70
|
+
if (delivery.companion_receipt.state === 'observed' && delivery.companion_receipt.observed_at) {
|
|
71
|
+
return { delivery, observedAt: delivery.companion_receipt.observed_at }
|
|
72
|
+
}
|
|
73
|
+
const receipt = delivery.events.find((event) => event.stage === 'companion_received')
|
|
74
|
+
return receipt ? { delivery, observedAt: receipt.occurred_at } : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Replace an unknown setup proof after this age; recent in-flight proofs stay. */
|
|
78
|
+
export const SETUP_PROOF_STALE_MS = 24 * 60 * 60 * 1000
|
|
79
|
+
|
|
80
|
+
export function setupProofIsStale(proof: SetupProofRecord, now: number): boolean {
|
|
81
|
+
const started = Date.parse(proof.started_at)
|
|
82
|
+
return Number.isFinite(started) && now - started > SETUP_PROOF_STALE_MS
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Persist ordinary send/status Companion Receipts as the project's delivery
|
|
87
|
+
* proof so doctor does not demand a second verification request.
|
|
88
|
+
*/
|
|
89
|
+
export function recordObservedDeliveryProof(
|
|
90
|
+
deps: CommandDeps,
|
|
91
|
+
snapshot: EvidenceSnapshot,
|
|
92
|
+
project: string | null,
|
|
93
|
+
): boolean {
|
|
94
|
+
for (const delivery of snapshot.deliveries) {
|
|
95
|
+
const observed = observedCompanionReceipt(snapshot, delivery.device_id)
|
|
96
|
+
if (observed === null) continue
|
|
97
|
+
return writeSetupProof(deps, {
|
|
98
|
+
request_id: snapshot.request_id,
|
|
99
|
+
device_id: delivery.device_id,
|
|
100
|
+
project,
|
|
101
|
+
started_at: observed.observedAt,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
71
105
|
}
|
package/src/commands-skill.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { SKILLS_INSTALLER_SPEC, type NativeSkill, type SkillScope } from './native-skills.js'
|
|
2
2
|
import { type ReadinessState } from './readiness.js'
|
|
3
3
|
import { skillsSource } from './release.js'
|
|
4
4
|
import type { CommandDeps } from './commands-core.js'
|
|
@@ -18,6 +18,8 @@ export const SKILLS_SOURCE: string | null = skillsSource()
|
|
|
18
18
|
*/
|
|
19
19
|
const SKILLS_SOURCE_LABEL = SKILLS_SOURCE ?? 'the public release tag matching this CLI'
|
|
20
20
|
|
|
21
|
+
const SKILL_SCOPES: readonly SkillScope[] = ['project', 'global']
|
|
22
|
+
|
|
21
23
|
function skillSourceParts(): { source: string; ref: string } | null {
|
|
22
24
|
if (SKILLS_SOURCE === null) return null
|
|
23
25
|
const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE)
|
|
@@ -35,13 +37,23 @@ function expectedSkill(skill: NativeSkill): boolean {
|
|
|
35
37
|
)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
function skillPin(skill: NativeSkill): string {
|
|
41
|
+
return skill.ref ?? 'unknown pin'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ScopedNotifaiSkills {
|
|
45
|
+
installed: NativeSkill[]
|
|
46
|
+
errors: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Installer-managed notifai skills in both scopes. Duplicate detection has to
|
|
51
|
+
* see the pair; a selected-scope filter would hide the copy the harness still
|
|
52
|
+
* lists.
|
|
53
|
+
*/
|
|
54
|
+
export async function listScopedNotifaiSkills(deps: CommandDeps): Promise<ScopedNotifaiSkills> {
|
|
43
55
|
const results = await Promise.all(
|
|
44
|
-
|
|
56
|
+
SKILL_SCOPES.map(async (scope) => {
|
|
45
57
|
if (deps.nativeSkills === undefined) return { scope, skills: [] as NativeSkill[] }
|
|
46
58
|
try {
|
|
47
59
|
return { scope, ...(await deps.nativeSkills.list(scope, deps.cwd, deps.env)) }
|
|
@@ -50,19 +62,70 @@ export async function skillReadiness(
|
|
|
50
62
|
}
|
|
51
63
|
}),
|
|
52
64
|
)
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
return {
|
|
66
|
+
installed: results.flatMap(({ skills }) => skills.filter((skill) => skill.name === 'notifai')),
|
|
67
|
+
errors: results
|
|
68
|
+
.filter((result) => result.error !== undefined)
|
|
69
|
+
.map((result) => `${result.scope}: ${result.error}`),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function duplicateSkillState(installed: NativeSkill[], selectedScope?: SkillScope): ReadinessState {
|
|
74
|
+
const project = installed.find((skill) => skill.scope === 'project')
|
|
75
|
+
const global = installed.find((skill) => skill.scope === 'global')
|
|
76
|
+
const projectPin = project === undefined ? 'not installed' : skillPin(project)
|
|
77
|
+
const globalPin = global === undefined ? 'not installed' : skillPin(global)
|
|
78
|
+
return {
|
|
79
|
+
id: 'skill',
|
|
80
|
+
title: 'Agent guidance skill',
|
|
81
|
+
status: 'gap',
|
|
82
|
+
detail:
|
|
83
|
+
`project (${projectPin}) and global (${globalPin}) are both installed, so the harness lists both. ` +
|
|
84
|
+
'Keep either project or global and uninstall the other.',
|
|
85
|
+
technical: {
|
|
86
|
+
project:
|
|
87
|
+
project === undefined
|
|
88
|
+
? null
|
|
89
|
+
: { ref: project.ref, path: project.path, current: expectedSkill(project) },
|
|
90
|
+
global:
|
|
91
|
+
global === undefined
|
|
92
|
+
? null
|
|
93
|
+
: { ref: global.ref, path: global.path, current: expectedSkill(global) },
|
|
94
|
+
resolution: 'both-listed',
|
|
95
|
+
},
|
|
96
|
+
remedy:
|
|
97
|
+
selectedScope === undefined
|
|
98
|
+
? {
|
|
99
|
+
by: 'user-here',
|
|
100
|
+
summary:
|
|
101
|
+
'keep either the project or the machine-global skill and uninstall the other; add --global to remove the machine-global copy',
|
|
102
|
+
command: `npx -y ${SKILLS_INSTALLER_SPEC} remove notifai`,
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
by: 'cli',
|
|
106
|
+
summary: `keep the ${selectedScope} skill and uninstall the other`,
|
|
107
|
+
command: `notifai init --skills --setup-scope ${selectedScope}`,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function skillReadiness(
|
|
113
|
+
deps: CommandDeps,
|
|
114
|
+
selectedScope?: SkillScope,
|
|
115
|
+
): Promise<ReadinessState> {
|
|
116
|
+
const { installed, errors } = await listScopedNotifaiSkills(deps)
|
|
117
|
+
if (installed.length > 1) return duplicateSkillState(installed, selectedScope)
|
|
118
|
+
|
|
119
|
+
const current = installed.find(expectedSkill)
|
|
120
|
+
if (current !== undefined) {
|
|
55
121
|
return {
|
|
56
122
|
id: 'skill',
|
|
57
123
|
title: 'Agent guidance skill',
|
|
58
124
|
status: 'ready',
|
|
59
|
-
detail: `installed from ${SKILLS_SOURCE_LABEL} in the ${
|
|
125
|
+
detail: `installed from ${SKILLS_SOURCE_LABEL} in the ${current.scope} scope`,
|
|
60
126
|
}
|
|
61
127
|
}
|
|
62
128
|
|
|
63
|
-
const errors = results
|
|
64
|
-
.filter((result) => result.error !== undefined)
|
|
65
|
-
.map((result) => `${result.scope}: ${result.error}`)
|
|
66
129
|
const scopeText = selectedScope === undefined ? 'project or machine-global scope' : `${selectedScope} scope`
|
|
67
130
|
return {
|
|
68
131
|
id: 'skill',
|
|
@@ -77,8 +140,8 @@ export async function skillReadiness(
|
|
|
77
140
|
summary: 'install the skill agents follow when deciding to notify',
|
|
78
141
|
command:
|
|
79
142
|
selectedScope === undefined
|
|
80
|
-
? 'notifai init --skills'
|
|
81
|
-
: `notifai init --skills --
|
|
143
|
+
? 'notifai init --skills --setup-scope project'
|
|
144
|
+
: `notifai init --skills --setup-scope ${selectedScope}`,
|
|
82
145
|
},
|
|
83
146
|
}
|
|
84
147
|
}
|