@vanadium-23/dsh-ping 0.1.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/lib/index.js ADDED
@@ -0,0 +1,421 @@
1
+ import { a as DEFAULTS, c as shouldNotify, i as sendWebhook, n as sendToast, o as resolveConfig, s as buildNotice, t as sendConsole } from "./channels-CpGxdbPv.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ //#region lib/types/protocol.js
4
+ /**
5
+ * Local, structural views of the host facts this plugin consumes.
6
+ *
7
+ * Nothing here imports a `@deepseek-ai/dsh-*` package. That is deliberate and
8
+ * is the whole compatibility story: an out-of-tree plugin that named an
9
+ * internal package broke on this machine when `@deepseek-ai/dsh-client-runtime`
10
+ * was renamed, so this one only ever reaches the host through shapes it
11
+ * declares itself, and treats every field as optional.
12
+ * @module dsh-ping/protocol
13
+ */
14
+ /**
15
+ * Narrow an unknown thrown value to a one-line message.
16
+ * @param error - the thrown value.
17
+ * @returns a single-line description.
18
+ */
19
+ function errorText(error) {
20
+ if (error === void 0 || error === null) return "";
21
+ if (typeof error === "string") return error;
22
+ if (error instanceof Error) return error.message;
23
+ const message = error.message;
24
+ if (typeof message === "string") return message;
25
+ try {
26
+ return JSON.stringify(error);
27
+ } catch {
28
+ return String(error);
29
+ }
30
+ }
31
+ /**
32
+ * Extract the visible text of an assistant message.
33
+ * @param message - an unknown message value.
34
+ * @returns concatenated text blocks, or an empty string.
35
+ */
36
+ function textOfMessage(message) {
37
+ if (typeof message !== "object" || message === null) return "";
38
+ const content = message.content;
39
+ if (typeof content === "string") return content;
40
+ if (!Array.isArray(content)) return "";
41
+ const parts = [];
42
+ for (const block of content) {
43
+ if (typeof block !== "object" || block === null) continue;
44
+ const candidate = block;
45
+ if (candidate.type === "text" && typeof candidate.text === "string") parts.push(candidate.text);
46
+ }
47
+ return parts.join("\n");
48
+ }
49
+ /**
50
+ * Whether an agent owns its session outright (the root agent) rather than
51
+ * being a delegated subagent.
52
+ * @param agent - the agent to classify.
53
+ * @returns true for the root agent.
54
+ */
55
+ function isRootAgent(agent) {
56
+ if (agent === void 0) return true;
57
+ if (agent.parentAgent !== void 0 && agent.parentAgent !== null) return false;
58
+ return agent.options?.origin !== "subagent";
59
+ }
60
+ /**
61
+ * Stable identity for one agent, used as the bookkeeping key.
62
+ * @param agent - the agent to identify.
63
+ * @returns the agent id, its session id, or an empty string.
64
+ */
65
+ function agentKey(agent) {
66
+ const id = agent?.id;
67
+ if (typeof id === "string" && id.length > 0) return id;
68
+ if (typeof id === "number") return String(id);
69
+ const sessionId = agent?.session?.id;
70
+ if (typeof sessionId === "string" && sessionId.length > 0) return sessionId;
71
+ return "";
72
+ }
73
+ /**
74
+ * Stable identity for one session.
75
+ * @param session - the session to identify.
76
+ * @returns the session id, or an empty string.
77
+ */
78
+ function sessionKey(session) {
79
+ const id = session?.id;
80
+ if (typeof id === "string" && id.length > 0) return id;
81
+ if (typeof id === "number") return String(id);
82
+ return "";
83
+ }
84
+ /**
85
+ * Read the working directory recorded on a session or agent.
86
+ * @param value - a session-like value.
87
+ * @returns the cwd, or an empty string.
88
+ */
89
+ function cwdOf(value) {
90
+ const cwd = value?.cwd;
91
+ return typeof cwd === "string" ? cwd : "";
92
+ }
93
+ //#endregion
94
+ //#region lib/types/index.js
95
+ /**
96
+ * dsh-ping — desktop notifications for DeepSeek Harness.
97
+ *
98
+ * Watches four host moments and tells a human about them: a turn finished, an
99
+ * agent errored, an approval is pending, or the agent asked a question. The
100
+ * delivery is a Windows toast (plus a console line and an optional webhook).
101
+ *
102
+ * The plugin is host-only and imports exactly one `@deepseek-ai/*` module —
103
+ * `schemastery`, the configuration schema library — at runtime. It has no
104
+ * browser half, so it cannot affect what the Web client loads, and no
105
+ * `dsh-*` internals, so renaming one cannot break it.
106
+ * @module dsh-ping
107
+ */
108
+ /** Stable Cordis plugin name. */
109
+ const name = "dsh-ping";
110
+ /** No service is required: every host fact is read optionally. */
111
+ const inject = [];
112
+ /**
113
+ * The configuration schema.
114
+ *
115
+ * Every default is written twice on purpose — once here so the loader can
116
+ * validate and fill a partial user config, and once in `defaults.ts` so the
117
+ * dependency-free smoke test shares the same values.
118
+ */
119
+ const Config = z.object({
120
+ enabled: z.boolean().default(DEFAULTS.enabled),
121
+ notifyOn: z.object({
122
+ done: z.boolean().default(DEFAULTS.notifyOn.done),
123
+ error: z.boolean().default(DEFAULTS.notifyOn.error),
124
+ approval: z.boolean().default(DEFAULTS.notifyOn.approval),
125
+ question: z.boolean().default(DEFAULTS.notifyOn.question)
126
+ }).default(DEFAULTS.notifyOn),
127
+ rootsOnly: z.boolean().default(DEFAULTS.rootsOnly),
128
+ cooldownMs: z.natural().default(DEFAULTS.cooldownMs),
129
+ minTurnDurationMs: z.natural().default(DEFAULTS.minTurnDurationMs),
130
+ channels: z.object({
131
+ toast: z.boolean().default(DEFAULTS.channels.toast),
132
+ console: z.boolean().default(DEFAULTS.channels.console),
133
+ webhook: z.boolean().default(DEFAULTS.channels.webhook)
134
+ }).default(DEFAULTS.channels),
135
+ webhookUrl: z.string().default(DEFAULTS.webhookUrl),
136
+ webhookTimeoutMs: z.natural().default(DEFAULTS.webhookTimeoutMs),
137
+ url: z.string().default(DEFAULTS.url),
138
+ maxBodyChars: z.natural().default(DEFAULTS.maxBodyChars),
139
+ toastAppId: z.string().default(DEFAULTS.toastAppId),
140
+ toastSoundDone: z.string().default(DEFAULTS.toastSoundDone),
141
+ toastSoundAttention: z.string().default(DEFAULTS.toastSoundAttention),
142
+ powershellPath: z.string().default(DEFAULTS.powershellPath),
143
+ titles: z.object({
144
+ done: z.string().default(DEFAULTS.titles.done),
145
+ error: z.string().default(DEFAULTS.titles.error),
146
+ approval: z.string().default(DEFAULTS.titles.approval),
147
+ question: z.string().default(DEFAULTS.titles.question)
148
+ }).default(DEFAULTS.titles),
149
+ debug: z.boolean().default(DEFAULTS.debug)
150
+ });
151
+ /** Most recently rendered assistant text, keyed by session. */
152
+ const ASSISTANT_CACHE_LIMIT = 64;
153
+ /**
154
+ * Register the notification plugin.
155
+ * @param ctx - the plugin context.
156
+ * @param config - resolved plugin configuration.
157
+ */
158
+ function apply(ctx, config) {
159
+ const cfg = resolveConfig(config);
160
+ const host = ctx;
161
+ const log = (line) => {
162
+ process.stderr.write(`[dsh-ping] ${line}\n`);
163
+ };
164
+ if (!cfg.enabled) {
165
+ log("disabled by configuration");
166
+ return;
167
+ }
168
+ const limits = {
169
+ enabled: cfg.enabled,
170
+ kinds: { ...cfg.notifyOn },
171
+ rootsOnly: cfg.rootsOnly,
172
+ cooldownMs: cfg.cooldownMs,
173
+ minTurnDurationMs: cfg.minTurnDurationMs,
174
+ maxBodyChars: cfg.maxBodyChars
175
+ };
176
+ const turns = /* @__PURE__ */ new Map();
177
+ const lastNotified = /* @__PURE__ */ new Map();
178
+ const lastAssistant = /* @__PURE__ */ new Map();
179
+ /** The URL a toast click opens. */
180
+ const clickUrl = () => {
181
+ if (cfg.url !== "") return cfg.url;
182
+ const server = host.get("webServer");
183
+ const port = server?.port;
184
+ if (typeof port !== "number" || port <= 0) return "";
185
+ const bound = typeof server?.host === "string" ? server.host : "";
186
+ return `http://${bound === "0.0.0.0" || bound === "" ? "127.0.0.1" : bound}:${String(port)}/`;
187
+ };
188
+ /** The host's title for a session, when a title service is mounted. */
189
+ const titleOf = (session) => {
190
+ try {
191
+ const title = host.get("sessionTitle")?.get(session)?.title;
192
+ return typeof title === "string" ? title : "";
193
+ } catch {
194
+ return "";
195
+ }
196
+ };
197
+ const toastDelivery = (attention) => ({
198
+ appId: cfg.toastAppId,
199
+ url: clickUrl(),
200
+ sound: attention ? cfg.toastSoundAttention : cfg.toastSoundDone,
201
+ long: attention,
202
+ ...cfg.powershellPath === "" ? {} : { powershellPath: cfg.powershellPath }
203
+ });
204
+ /**
205
+ * Apply the rate limits, render the notice, and hand it to the channels.
206
+ * @param fact - what happened.
207
+ * @param isRoot - whether the agent owns its session.
208
+ */
209
+ const deliver = (fact, isRoot) => {
210
+ const now = Date.now();
211
+ const key = `${fact.sessionId}:${fact.kind}`;
212
+ const lastNotifiedAt = lastNotified.get(key);
213
+ const verdict = shouldNotify({
214
+ fact,
215
+ limits,
216
+ isRoot,
217
+ ...lastNotifiedAt === void 0 ? {} : { lastNotifiedAt },
218
+ now
219
+ });
220
+ if (!verdict.notify) {
221
+ if (cfg.debug) log(`suppressed ${fact.kind} (${verdict.reason})`);
222
+ return;
223
+ }
224
+ lastNotified.set(key, now);
225
+ const notice = buildNotice(fact, cfg.titles, cfg.maxBodyChars);
226
+ if (cfg.channels.console) sendConsole(notice, log);
227
+ if (cfg.channels.toast) sendToast(notice, toastDelivery(fact.kind === "approval" || fact.kind === "question"), log);
228
+ if (cfg.channels.webhook && cfg.webhookUrl !== "") sendWebhook({
229
+ kind: fact.kind,
230
+ title: notice.title,
231
+ lines: notice.lines,
232
+ sessionId: fact.sessionId,
233
+ at: new Date(now).toISOString()
234
+ }, cfg.webhookUrl, cfg.webhookTimeoutMs, log);
235
+ };
236
+ /** Identity used for bookkeeping and rate limiting. */
237
+ const identityOf = (agent) => {
238
+ const key = agentKey(agent);
239
+ return {
240
+ key,
241
+ sessionId: sessionKey(agent?.session) || key
242
+ };
243
+ };
244
+ const onStatus = (payload) => {
245
+ const agent = payload.agent;
246
+ const { key, sessionId } = identityOf(agent);
247
+ if (key === "") return;
248
+ if (payload.status === "running") {
249
+ turns.set(key, {
250
+ startedAt: Date.now(),
251
+ sawError: false,
252
+ reported: false
253
+ });
254
+ return;
255
+ }
256
+ if (payload.status !== "idle") return;
257
+ const turn = turns.get(key);
258
+ turns.delete(key);
259
+ if (turn === void 0) return;
260
+ if (turn.sawError && turn.reported) return;
261
+ const cwd = cwdOf(agent?.session);
262
+ deliver({
263
+ kind: "done",
264
+ sessionId,
265
+ sessionTitle: titleOf(agent?.session),
266
+ cwd,
267
+ ...lastAssistant.has(sessionId) ? { detail: lastAssistant.get(sessionId) } : {},
268
+ durationMs: Date.now() - turn.startedAt
269
+ }, isRootAgent(agent));
270
+ };
271
+ const onError = (payload) => {
272
+ const agent = payload.agent;
273
+ const { key, sessionId } = identityOf(agent);
274
+ const turn = turns.get(key);
275
+ if (turn !== void 0) {
276
+ turn.sawError = true;
277
+ turn.reported = true;
278
+ }
279
+ deliver({
280
+ kind: "error",
281
+ sessionId,
282
+ sessionTitle: titleOf(agent?.session),
283
+ cwd: cwdOf(agent?.session),
284
+ detail: errorText(payload.error)
285
+ }, isRootAgent(agent));
286
+ };
287
+ const onApproval = (payload) => {
288
+ const agent = payload.agent;
289
+ const { sessionId } = identityOf(agent);
290
+ const toolName = typeof payload.toolName === "string" ? payload.toolName : "";
291
+ const reason = typeof payload.reason === "string" ? payload.reason : "";
292
+ deliver({
293
+ kind: "approval",
294
+ sessionId,
295
+ sessionTitle: titleOf(agent?.session),
296
+ cwd: cwdOf(agent?.session),
297
+ detail: [toolName, reason].filter((part) => part !== "").join(" — ")
298
+ }, isRootAgent(agent));
299
+ };
300
+ const onQuestion = (payload) => {
301
+ const agent = payload.agent;
302
+ const { sessionId } = identityOf(agent);
303
+ const items = Array.isArray(payload.questions) ? payload.questions : [];
304
+ const first = items[0];
305
+ const text = typeof first?.question === "string" ? first.question : "";
306
+ deliver({
307
+ kind: "question",
308
+ sessionId,
309
+ sessionTitle: titleOf(agent?.session),
310
+ cwd: cwdOf(agent?.session),
311
+ detail: items.length > 1 ? `${text}(共 ${String(items.length)} 个问题)` : text
312
+ }, isRootAgent(agent));
313
+ };
314
+ /** Keep the last assistant text so a completion notice can quote it. */
315
+ const rememberAssistant = (session, event) => {
316
+ if (event.type !== "assistant/message") return;
317
+ const id = sessionKey(session);
318
+ if (id === "") return;
319
+ const text = textOfMessage(event.data?.message);
320
+ if (text.trim() === "") return;
321
+ lastAssistant.delete(id);
322
+ lastAssistant.set(id, text);
323
+ while (lastAssistant.size > ASSISTANT_CACHE_LIMIT) {
324
+ const oldest = lastAssistant.keys().next().value;
325
+ if (oldest === void 0) break;
326
+ lastAssistant.delete(oldest);
327
+ }
328
+ };
329
+ /** Run a handler without ever letting it reach the caller. */
330
+ const guard = (label, run) => {
331
+ try {
332
+ run();
333
+ } catch (error) {
334
+ log(`${label} handler failed: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ };
337
+ host.on("agent/status", (payload) => {
338
+ guard("agent/status", () => {
339
+ onStatus(payload);
340
+ });
341
+ });
342
+ host.on("agent/error", (payload) => {
343
+ guard("agent/error", () => {
344
+ onError(payload);
345
+ });
346
+ });
347
+ host.on("session/event", (session, event) => {
348
+ guard("session/event", () => {
349
+ rememberAssistant(session, event);
350
+ });
351
+ });
352
+ host.on("approval/request", (payload, next) => {
353
+ guard("approval/request", () => {
354
+ onApproval(payload);
355
+ });
356
+ return next();
357
+ }, { prepend: true });
358
+ host.on("user-questions/request", (payload, next) => {
359
+ guard("user-questions/request", () => {
360
+ onQuestion(payload);
361
+ });
362
+ return next();
363
+ }, { prepend: true });
364
+ /**
365
+ * Register a self-test tool so the channel can be verified from inside a
366
+ * session, without a terminal and without waiting for a real turn to end.
367
+ */
368
+ const registerTestTool = (scope) => {
369
+ const tools = scope.get("tools");
370
+ if (tools === void 0 || typeof tools.register !== "function") return;
371
+ tools.register({
372
+ name: "dsh_ping_test",
373
+ description: "Send a test desktop notification through dsh-ping and report which channels accepted it. Use this to verify that turn-completion and attention notifications will actually reach the user, or when the user asks whether notifications work.",
374
+ parameters: {
375
+ type: "object",
376
+ properties: { text: {
377
+ type: "string",
378
+ description: "Optional body text for the test notification."
379
+ } },
380
+ required: []
381
+ },
382
+ output: {
383
+ schema: { type: "string" },
384
+ render: (_args, value) => [{
385
+ type: "text",
386
+ text: String(value)
387
+ }]
388
+ },
389
+ async execute(args) {
390
+ const notice = buildNotice({
391
+ kind: "done",
392
+ sessionId: "self-test",
393
+ sessionTitle: "dsh-ping 自检",
394
+ detail: typeof args?.text === "string" && args.text.trim() !== "" ? args.text : "这是一条 dsh-ping 自检通知,看到它就说明通知链路是通的。"
395
+ }, cfg.titles, cfg.maxBodyChars);
396
+ const accepted = [];
397
+ if (cfg.channels.console) {
398
+ sendConsole(notice, log);
399
+ accepted.push("console");
400
+ }
401
+ if (cfg.channels.toast) accepted.push(sendToast(notice, toastDelivery(false), log) ? "toast" : "toast(不可用)");
402
+ if (cfg.channels.webhook && cfg.webhookUrl !== "") {
403
+ accepted.push("webhook");
404
+ await sendWebhook({
405
+ kind: "test",
406
+ title: notice.title,
407
+ lines: notice.lines,
408
+ sessionId: "self-test",
409
+ at: (/* @__PURE__ */ new Date()).toISOString()
410
+ }, cfg.webhookUrl, cfg.webhookTimeoutMs, log);
411
+ }
412
+ return `dsh-ping 自检:已通过 ${accepted.join(" + ") || "(无可用通道)"} 发送「${notice.title}」`;
413
+ }
414
+ });
415
+ };
416
+ if (typeof host.inject === "function") host.inject(["tools"], registerTestTool);
417
+ else registerTestTool(host);
418
+ log(`ready — channels=${Object.entries(cfg.channels).filter(([, on]) => on).map(([channel]) => channel).join("+")} rootsOnly=${String(cfg.rootsOnly)} cooldown=${String(cfg.cooldownMs)}ms`);
419
+ }
420
+ //#endregion
421
+ export { Config, DEFAULTS, apply, inject, name, resolveConfig };
package/lib/smoke.js ADDED
@@ -0,0 +1,65 @@
1
+ import { a as DEFAULTS, o as resolveConfig, r as sendToastSync, s as buildNotice } from "./channels-CpGxdbPv.js";
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region lib/types/smoke.js
6
+ /**
7
+ * Standalone toast smoke test — no harness required.
8
+ *
9
+ * `node lib/smoke.js [text]` raises one real Windows toast with the exact code
10
+ * path the plugin uses, which is how the notification channel gets verified on
11
+ * a machine without starting a session.
12
+ * @module dsh-ping/smoke
13
+ */
14
+ /**
15
+ * Raise one test toast.
16
+ * @param argv - words appended to the default body text.
17
+ * @returns the process exit code.
18
+ */
19
+ function runSmoke(argv) {
20
+ const cfg = resolveConfig(void 0);
21
+ const notice = buildNotice({
22
+ kind: "done",
23
+ sessionId: "smoke",
24
+ sessionTitle: "dsh-ping 自检",
25
+ detail: argv.join(" ").trim() === "" ? "这是一条 dsh-ping 自检通知(命令行)。看到它就说明 Windows 通知链路是通的。" : argv.join(" ").trim(),
26
+ durationMs: 0
27
+ }, cfg.titles, cfg.maxBodyChars);
28
+ const result = sendToastSync(notice, {
29
+ appId: cfg.toastAppId,
30
+ sound: cfg.toastSoundDone,
31
+ long: false,
32
+ ...cfg.powershellPath === "" ? {} : { powershellPath: cfg.powershellPath }
33
+ });
34
+ if (!result.started) {
35
+ process.stdout.write(`dsh-ping: 这台机器上无法发送 Windows 通知(${result.output})\n`);
36
+ return 1;
37
+ }
38
+ if (result.code !== 0) {
39
+ process.stdout.write(`dsh-ping: 通知助手以退出码 ${String(result.code)} 失败\n${result.output}\n`);
40
+ return 1;
41
+ }
42
+ process.stdout.write(`dsh-ping: 已交给 Windows 通知中心「${notice.title}」 — ${notice.lines.join(" · ")}\n`);
43
+ process.stdout.write("屏幕上没弹窗的话,检查 设置 → 系统 → 通知 是否为 Windows PowerShell 放行,以及「专注助手」是否开启。\n");
44
+ return 0;
45
+ }
46
+ /** The configured default headings, exported for the CLI banner. */
47
+ const SMOKE_TITLES = DEFAULTS.titles;
48
+ /**
49
+ * Whether this module is the process entry point.
50
+ * @returns true when argv[1] resolves to this module.
51
+ */
52
+ function invokedAsScript() {
53
+ const entry = process.argv[1];
54
+ if (entry === void 0) return false;
55
+ try {
56
+ const self = realpathSync(fileURLToPath(import.meta.url));
57
+ const invoked = realpathSync(resolve(entry));
58
+ return process.platform === "win32" ? self.toLowerCase() === invoked.toLowerCase() : self === invoked;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ if (invokedAsScript()) process.exitCode = runSmoke(process.argv.slice(2));
64
+ //#endregion
65
+ export { SMOKE_TITLES, runSmoke };
@@ -0,0 +1 @@
1
+ {"root":["../src/channels.ts","../src/decide.ts","../src/defaults.ts","../src/index.ts","../src/protocol.ts","../src/smoke.ts"],"version":"6.0.3"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Delivery channels: Windows toast, console line, optional webhook.
3
+ *
4
+ * All three are best-effort by contract — a notification that cannot be shown
5
+ * must never affect the harness, so every failure is logged and swallowed.
6
+ * @module dsh-ping/channels
7
+ */
8
+ import { type Notice } from './decide.ts';
9
+ /**
10
+ * The toast script, sent as one `-EncodedCommand` payload.
11
+ *
12
+ * It is a constant. No session text is ever concatenated into PowerShell
13
+ * source, so no amount of text in a session title, an error message, or a tool
14
+ * name can reach the PowerShell parser: the text arrives as an XML document in
15
+ * an environment variable and is handed straight to the WinRT XML parser.
16
+ */
17
+ export declare const TOAST_SCRIPT = "$ErrorActionPreference = 'Stop'\n[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime]\n[void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType=WindowsRuntime]\n$xml = New-Object Windows.Data.Xml.Dom.XmlDocument\n$xml.LoadXml([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($env:DSH_PING_XML)))\n$toast = New-Object Windows.UI.Notifications.ToastNotification $xml\n$appId = $env:DSH_PING_APPID\nif ([string]::IsNullOrEmpty($appId)) { $appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' }\n[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)\n";
18
+ /** How one toast is presented. */
19
+ export interface ToastDelivery {
20
+ /** AUMID the toast is attributed to. */
21
+ appId: string;
22
+ /** URL opened when the toast is clicked; omitted makes the toast inert. */
23
+ url?: string;
24
+ /** `ms-winsoundevent:*` source, or empty for a silent toast. */
25
+ sound?: string;
26
+ /** Keep the toast on screen longer (used for attention events). */
27
+ long?: boolean;
28
+ /** Explicit Windows PowerShell path; resolved by default. */
29
+ powershellPath?: string;
30
+ }
31
+ /** Diagnostic sink shared by every channel. */
32
+ export type ChannelLog = (line: string) => void;
33
+ /**
34
+ * Render the toast XML payload.
35
+ * @param notice - the heading and body lines.
36
+ * @param delivery - presentation options.
37
+ * @returns a complete `<toast>` document.
38
+ */
39
+ export declare function buildToastXml(notice: Notice, delivery: ToastDelivery): string;
40
+ /**
41
+ * Encode a script for `powershell.exe -EncodedCommand`.
42
+ * @param script - PowerShell source.
43
+ * @returns base64 of the UTF-16LE script.
44
+ */
45
+ export declare function encodeCommand(script: string): string;
46
+ /**
47
+ * Resolve the Windows PowerShell 5.1 executable.
48
+ *
49
+ * WinRT type projection is a 5.1 feature; `pwsh` is not a substitute, so the
50
+ * in-box path is used rather than a PATH lookup that could find PowerShell 7.
51
+ * @param configured - an explicit path from configuration.
52
+ * @returns the executable path, or `undefined` when unavailable.
53
+ */
54
+ export declare function resolvePowershell(configured?: string): string | undefined;
55
+ /**
56
+ * Build a minimal child environment.
57
+ *
58
+ * A notification process has no business inheriting API keys, so the
59
+ * environment is rebuilt from an allowlist instead of copied.
60
+ * @returns the environment for the toast helper.
61
+ */
62
+ export declare function minimalEnv(): Record<string, string>;
63
+ /**
64
+ * Show a Windows toast. Fire-and-forget by design.
65
+ * @param notice - the heading and body lines.
66
+ * @param delivery - presentation options.
67
+ * @param log - diagnostic sink.
68
+ * @returns true when the helper was started.
69
+ */
70
+ export declare function sendToast(notice: Notice, delivery: ToastDelivery, log: ChannelLog): boolean;
71
+ /**
72
+ * Show a Windows toast and wait for the helper to finish.
73
+ *
74
+ * The plugin never uses this — a live notification must not block a session
75
+ * event — but a diagnostic run has to see the helper's exit code to tell
76
+ * "shown" apart from "silently refused".
77
+ * @param notice - the heading and body lines.
78
+ * @param delivery - presentation options.
79
+ * @returns the helper's exit code and captured error output.
80
+ */
81
+ export declare function sendToastSync(notice: Notice, delivery: ToastDelivery): {
82
+ started: boolean;
83
+ code: number | null;
84
+ output: string;
85
+ };
86
+ /**
87
+ * Write the notice to stderr, where the launching terminal shows it.
88
+ * @param notice - the heading and body lines.
89
+ * @param log - diagnostic sink.
90
+ */
91
+ export declare function sendConsole(notice: Notice, log: ChannelLog): void;
92
+ /** Webhook payload. */
93
+ export interface WebhookPayload {
94
+ kind: string;
95
+ title: string;
96
+ lines: string[];
97
+ sessionId: string;
98
+ at: string;
99
+ }
100
+ /**
101
+ * POST the notice to a webhook.
102
+ * @param payload - the JSON body.
103
+ * @param url - destination URL.
104
+ * @param timeoutMs - request budget.
105
+ * @param log - diagnostic sink.
106
+ * @returns a promise that settles when the attempt finishes.
107
+ */
108
+ export declare function sendWebhook(payload: WebhookPayload, url: string, timeoutMs: number, log: ChannelLog): Promise<void>;
109
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../../src/channels.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,4tBASxB,CAAA;AAED,kCAAkC;AAClC,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAE/C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,MAAM,CAqB7E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKzE;AAQD;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOnD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAkC3F;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoBhI;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAEjE;AAED,uBAAuB;AACvB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,IAAI,CAAC,CAcf"}