@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,311 +0,0 @@
|
|
|
1
|
-
import { NOTIFICATION_IMAGE_MAX_BYTES, type ListRepliesResponse } from '@raidiant/notifai-protocol'
|
|
2
|
-
import { sha256Hex } from '@raidiant/notifai-protocol/node'
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { isRetryableReplyPollError, type ApiClient } from './client.js'
|
|
6
|
-
import { fetchMediaUrl } from './url-policy.js'
|
|
7
|
-
import { buildSourceContext, inferInvocationContext } from './invocation-context.js'
|
|
8
|
-
import { readOrcaSessionTitle } from './orca-session-title.js'
|
|
9
|
-
import { type DraftInvocation, type SendFlags } from './send.js'
|
|
10
|
-
import { EXIT, reportError, type CommandDeps } from './commands-core.js'
|
|
11
|
-
import type { ActiveHarnessSession } from './commands-harness-context.js'
|
|
12
|
-
|
|
13
|
-
function managedSessionTitle(
|
|
14
|
-
deps: CommandDeps,
|
|
15
|
-
flags: Pick<SendFlags, 'sessionLabel'>,
|
|
16
|
-
active: ActiveHarnessSession | null,
|
|
17
|
-
): string | undefined {
|
|
18
|
-
if (active?.sessionLabel !== undefined) return active.sessionLabel
|
|
19
|
-
if (
|
|
20
|
-
active?.sessionId === undefined ||
|
|
21
|
-
flags.sessionLabel !== undefined ||
|
|
22
|
-
deps.env['NOTIFAI_SESSION_LABEL'] !== undefined
|
|
23
|
-
) {
|
|
24
|
-
return undefined
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return (deps.orcaSessionTitle ?? readOrcaSessionTitle)(deps.env)
|
|
28
|
-
} catch {
|
|
29
|
-
return undefined
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function resolveDraftInvocation(
|
|
34
|
-
deps: CommandDeps,
|
|
35
|
-
flags: Pick<SendFlags, 'sessionId' | 'sessionLabel'>,
|
|
36
|
-
active: ActiveHarnessSession | null,
|
|
37
|
-
): { ok: true; invocation: DraftInvocation } | { ok: false; error: string } {
|
|
38
|
-
const inferred = inferInvocationContext(deps.cwd)
|
|
39
|
-
const sessionTitle = managedSessionTitle(deps, flags, active)
|
|
40
|
-
const source = buildSourceContext({
|
|
41
|
-
env: deps.env,
|
|
42
|
-
invocation: inferred,
|
|
43
|
-
...(flags.sessionId !== undefined ? { sessionId: flags.sessionId } : {}),
|
|
44
|
-
...(flags.sessionLabel !== undefined ? { sessionLabel: flags.sessionLabel } : {}),
|
|
45
|
-
...(active === null
|
|
46
|
-
? {}
|
|
47
|
-
: {
|
|
48
|
-
activeHarness: {
|
|
49
|
-
harness: active.harness,
|
|
50
|
-
...(active.sessionId === undefined ? {} : { sessionId: active.sessionId }),
|
|
51
|
-
...(sessionTitle === undefined ? {} : { sessionLabel: sessionTitle }),
|
|
52
|
-
...(active.sessionLabelPending === true ? { sessionLabelPending: true } : {}),
|
|
53
|
-
},
|
|
54
|
-
}),
|
|
55
|
-
now: (deps.now ?? Date.now)(),
|
|
56
|
-
})
|
|
57
|
-
if (!source.ok) return source
|
|
58
|
-
return {
|
|
59
|
-
ok: true,
|
|
60
|
-
invocation: {
|
|
61
|
-
inferredProject: inferred.project,
|
|
62
|
-
...(source.source === undefined ? {} : { source: source.source }),
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface ReplyWaitOptions {
|
|
68
|
-
timeoutSeconds: number
|
|
69
|
-
afterSeq: number
|
|
70
|
-
now?: (() => number) | undefined
|
|
71
|
-
sleep?: ((milliseconds: number) => Promise<void>) | undefined
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface ReplyWaitResult {
|
|
75
|
-
response: ListRepliesResponse
|
|
76
|
-
timedOut: boolean
|
|
77
|
-
/**
|
|
78
|
-
* The wait ended while polls were failing, so silence is unproven: the user
|
|
79
|
-
* may well have answered and we could not see it.
|
|
80
|
-
*/
|
|
81
|
-
degraded: boolean
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Loop over server-capped long polls until a reply arrives or the caller's deadline passes. */
|
|
85
|
-
export async function waitForReply(
|
|
86
|
-
client: ApiClient,
|
|
87
|
-
requestId: string,
|
|
88
|
-
options: ReplyWaitOptions,
|
|
89
|
-
): Promise<ReplyWaitResult> {
|
|
90
|
-
const now = options.now ?? Date.now
|
|
91
|
-
const sleep = options.sleep ?? ((milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)))
|
|
92
|
-
const deadline = now() + options.timeoutSeconds * 1000
|
|
93
|
-
let lastResponse: ListRepliesResponse | null = null
|
|
94
|
-
let lastTransientError: Error | null = null
|
|
95
|
-
let consecutiveTransientErrors = 0
|
|
96
|
-
let firstPoll = true
|
|
97
|
-
|
|
98
|
-
while (firstPoll || now() < deadline) {
|
|
99
|
-
firstPoll = false
|
|
100
|
-
const remainingMs = Math.max(0, deadline - now())
|
|
101
|
-
const waitSeconds = Math.min(25, Math.floor(remainingMs / 1000))
|
|
102
|
-
try {
|
|
103
|
-
const response = await client.replies(requestId, {
|
|
104
|
-
waitSeconds,
|
|
105
|
-
afterSeq: options.afterSeq,
|
|
106
|
-
})
|
|
107
|
-
lastResponse = response
|
|
108
|
-
lastTransientError = null
|
|
109
|
-
consecutiveTransientErrors = 0
|
|
110
|
-
if (response.replies.length > 0) return { response, timedOut: false, degraded: false }
|
|
111
|
-
|
|
112
|
-
const pauseMs = Math.min(250, Math.max(0, deadline - now()))
|
|
113
|
-
if (pauseMs > 0) await sleep(pauseMs)
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (!isRetryableReplyPollError(err)) throw err
|
|
116
|
-
lastTransientError = err instanceof Error ? err : new Error(String(err))
|
|
117
|
-
consecutiveTransientErrors += 1
|
|
118
|
-
const remainingAfterError = Math.max(0, deadline - now())
|
|
119
|
-
if (remainingAfterError === 0) break
|
|
120
|
-
const backoffMs = Math.min(
|
|
121
|
-
250 * 2 ** (consecutiveTransientErrors - 1),
|
|
122
|
-
2_000,
|
|
123
|
-
remainingAfterError,
|
|
124
|
-
)
|
|
125
|
-
await sleep(backoffMs)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Never saw a successful poll: surface the fault so callers get EXIT.network
|
|
130
|
-
// rather than a fake empty silence. The durable request is still on the
|
|
131
|
-
// server; the typed recovery is `notifai replies <id>`.
|
|
132
|
-
if (!lastResponse && lastTransientError) throw lastTransientError
|
|
133
|
-
return {
|
|
134
|
-
response:
|
|
135
|
-
lastResponse ??
|
|
136
|
-
({
|
|
137
|
-
request_id: requestId,
|
|
138
|
-
reply_expires_at: null,
|
|
139
|
-
agent_acknowledgement_required: false,
|
|
140
|
-
agent_acknowledgement_text_required: false,
|
|
141
|
-
agent_acknowledgement: null,
|
|
142
|
-
replies: [],
|
|
143
|
-
} satisfies ListRepliesResponse),
|
|
144
|
-
timedOut: true,
|
|
145
|
-
// A poll succeeded at some point, so we do not throw — but the last thing
|
|
146
|
-
// we know is that we could not reach the server. Reporting that as a plain
|
|
147
|
-
// "no reply" would let an agent treat an unseen refusal as consent.
|
|
148
|
-
degraded: lastTransientError !== null,
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function acknowledgeInvocation(requestId: string, textRequired: boolean): string {
|
|
153
|
-
return textRequired
|
|
154
|
-
? `notifai acknowledge ${requestId} --text <text>`
|
|
155
|
-
: `notifai acknowledge ${requestId}`
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function acknowledgementCommand(
|
|
159
|
-
requestId: string,
|
|
160
|
-
required: boolean,
|
|
161
|
-
textRequired: boolean,
|
|
162
|
-
acknowledgement: ListRepliesResponse['agent_acknowledgement'],
|
|
163
|
-
hasReply = true,
|
|
164
|
-
): string | null {
|
|
165
|
-
return required && acknowledgement === null && hasReply
|
|
166
|
-
? acknowledgeInvocation(requestId, textRequired)
|
|
167
|
-
: null
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function printAcknowledgementStatus(deps: CommandDeps, response: ListRepliesResponse): void {
|
|
171
|
-
if (!response.agent_acknowledgement_required) {
|
|
172
|
-
deps.io.out('Agent Acknowledgement: not required for this request.')
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
const textRequired = response.agent_acknowledgement_text_required
|
|
176
|
-
if (response.agent_acknowledgement !== null) {
|
|
177
|
-
const recorded = `Agent Acknowledgement: recorded at ${response.agent_acknowledgement.created_at}`
|
|
178
|
-
deps.io.out(
|
|
179
|
-
response.agent_acknowledgement.text.length > 0
|
|
180
|
-
? `${recorded}: ${response.agent_acknowledgement.text}`
|
|
181
|
-
: `${recorded}.`,
|
|
182
|
-
)
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
if (response.replies.length > 0) {
|
|
186
|
-
deps.io.out('Agent Acknowledgement required.')
|
|
187
|
-
deps.io.out(
|
|
188
|
-
textRequired
|
|
189
|
-
? `next: Run \`${acknowledgeInvocation(response.request_id, true)}\` with concrete text saying what you will do because of the reply.`
|
|
190
|
-
: `next: Run \`${acknowledgeInvocation(response.request_id, false)}\` so the user sees you read the reply; this account turned acknowledgement text off.`,
|
|
191
|
-
)
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
deps.io.out('Agent Acknowledgement: required after a user reply; no reply is recorded yet.')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const MEDIA_TYPES: Record<string, 'image/jpeg' | 'image/png' | 'image/gif'> = {
|
|
198
|
-
'.jpg': 'image/jpeg',
|
|
199
|
-
'.jpeg': 'image/jpeg',
|
|
200
|
-
'.png': 'image/png',
|
|
201
|
-
'.gif': 'image/gif',
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
type UploadResult =
|
|
205
|
-
| { ok: true; mediaId: string }
|
|
206
|
-
/** `error: null` means `reportError` already said it; do not print it twice. */
|
|
207
|
-
| { ok: false; error: string | null; exit: number }
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Read a remote image without buffering more than the server will accept.
|
|
211
|
-
*
|
|
212
|
-
* Content-Length is an early refusal, not trust: a lying header still hits
|
|
213
|
-
* the byte cap while the body is read. Supported schemes stay http and https.
|
|
214
|
-
*/
|
|
215
|
-
export async function readCappedBytes(
|
|
216
|
-
response: Response,
|
|
217
|
-
maxBytes: number,
|
|
218
|
-
): Promise<Uint8Array | 'too-large'> {
|
|
219
|
-
const contentLength = Number(response.headers.get('content-length'))
|
|
220
|
-
if (Number.isFinite(contentLength) && contentLength > maxBytes) return 'too-large'
|
|
221
|
-
const reader = response.body?.getReader()
|
|
222
|
-
if (reader === undefined) {
|
|
223
|
-
const buf = new Uint8Array(await response.arrayBuffer())
|
|
224
|
-
return buf.byteLength > maxBytes ? 'too-large' : buf
|
|
225
|
-
}
|
|
226
|
-
const chunks: Uint8Array[] = []
|
|
227
|
-
let received = 0
|
|
228
|
-
while (true) {
|
|
229
|
-
const { done, value } = await reader.read()
|
|
230
|
-
if (done) break
|
|
231
|
-
received += value.byteLength
|
|
232
|
-
if (received > maxBytes) {
|
|
233
|
-
await reader.cancel()
|
|
234
|
-
return 'too-large'
|
|
235
|
-
}
|
|
236
|
-
chunks.push(value)
|
|
237
|
-
}
|
|
238
|
-
const out = new Uint8Array(received)
|
|
239
|
-
let offset = 0
|
|
240
|
-
for (const chunk of chunks) {
|
|
241
|
-
out.set(chunk, offset)
|
|
242
|
-
offset += chunk.byteLength
|
|
243
|
-
}
|
|
244
|
-
return out
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* `--image` accepts a media id, a local file path, or an http(s) URL.
|
|
249
|
-
*
|
|
250
|
-
* A remote URL is fetched under the media URL policy (`url-policy.ts`):
|
|
251
|
-
* public HTTPS by default, every redirect hop re-validated, and non-public
|
|
252
|
-
* destinations only through the User's `media_origins` — never a
|
|
253
|
-
* repository's. `--image` values routinely arrive from an agent that read
|
|
254
|
-
* them in a working tree, so the URL is hostile input until proven otherwise.
|
|
255
|
-
*/
|
|
256
|
-
export async function uploadImage(
|
|
257
|
-
deps: CommandDeps,
|
|
258
|
-
client: ApiClient,
|
|
259
|
-
source: string,
|
|
260
|
-
allowOrigins: readonly string[] = [],
|
|
261
|
-
): Promise<UploadResult> {
|
|
262
|
-
let bytes: Uint8Array
|
|
263
|
-
let mediaType: 'image/jpeg' | 'image/png' | 'image/gif' | undefined
|
|
264
|
-
if (/^https?:\/\//.test(source)) {
|
|
265
|
-
try {
|
|
266
|
-
const fetched = await fetchMediaUrl(source, { allowOrigins }, deps.fetchImpl)
|
|
267
|
-
if (!fetched.ok) return { ok: false, error: fetched.reason, exit: EXIT.usage }
|
|
268
|
-
const response = fetched.response
|
|
269
|
-
if (!response.ok) return { ok: false, error: `Could not fetch ${source} (${response.status}).`, exit: EXIT.usage }
|
|
270
|
-
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim() ?? ''
|
|
271
|
-
mediaType = (['image/jpeg', 'image/png', 'image/gif'] as const).find((t) => t === contentType)
|
|
272
|
-
const downloaded = await readCappedBytes(response, NOTIFICATION_IMAGE_MAX_BYTES)
|
|
273
|
-
if (downloaded === 'too-large') {
|
|
274
|
-
return {
|
|
275
|
-
ok: false,
|
|
276
|
-
error: `Remote image exceeds the ${NOTIFICATION_IMAGE_MAX_BYTES} byte media limit.`,
|
|
277
|
-
exit: EXIT.usage,
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
bytes = downloaded
|
|
281
|
-
} catch (err) {
|
|
282
|
-
return { ok: false, error: `Could not fetch ${source}: ${String(err)}`, exit: EXIT.network }
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
if (!existsSync(source)) {
|
|
286
|
-
return { ok: false, error: `--image: "${source}" is not a media id, file, or URL.`, exit: EXIT.usage }
|
|
287
|
-
}
|
|
288
|
-
bytes = new Uint8Array(readFileSync(source))
|
|
289
|
-
mediaType = MEDIA_TYPES[path.extname(source).toLowerCase()]
|
|
290
|
-
}
|
|
291
|
-
if (!mediaType) {
|
|
292
|
-
return { ok: false, error: 'Images must be JPEG, PNG, or GIF.', exit: EXIT.usage }
|
|
293
|
-
}
|
|
294
|
-
try {
|
|
295
|
-
const grant = await client.createMediaUpload({
|
|
296
|
-
media_type: mediaType,
|
|
297
|
-
size_bytes: bytes.byteLength,
|
|
298
|
-
sha256: sha256Hex(bytes),
|
|
299
|
-
})
|
|
300
|
-
await client.uploadMedia(grant, bytes)
|
|
301
|
-
return { ok: true, mediaId: grant.media_id }
|
|
302
|
-
} catch (err) {
|
|
303
|
-
// Every other API failure in this file goes through `reportError`, which
|
|
304
|
-
// maps a revoked credential to the auth code, a server fault to the network
|
|
305
|
-
// one and everything else to plain failure, and records it in the local
|
|
306
|
-
// log with whatever next step the server named. This used to answer
|
|
307
|
-
// `network` to all of them, so an image too large for the account exited
|
|
308
|
-
// the same way an unreachable server did, and nothing about it was logged.
|
|
309
|
-
return { ok: false, error: null, exit: reportError(deps, err) }
|
|
310
|
-
}
|
|
311
|
-
}
|