@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/DESIGN.md +168 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/bin/dsh-ping.mjs +8 -0
- package/cordis.patch.yml +8 -0
- package/lib/channels-CpGxdbPv.js +426 -0
- package/lib/index.js +421 -0
- package/lib/smoke.js +65 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/channels.d.ts +109 -0
- package/lib/types/channels.d.ts.map +1 -0
- package/lib/types/channels.js +204 -0
- package/lib/types/channels.js.map +1 -0
- package/lib/types/decide.d.ts +110 -0
- package/lib/types/decide.d.ts.map +1 -0
- package/lib/types/decide.js +145 -0
- package/lib/types/decide.js.map +1 -0
- package/lib/types/defaults.d.ts +64 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/defaults.js +51 -0
- package/lib/types/defaults.js.map +1 -0
- package/lib/types/index.d.ts +36 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +330 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/protocol.d.ts +122 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/protocol.js +107 -0
- package/lib/types/protocol.js.map +1 -0
- package/lib/types/smoke.d.ts +17 -0
- package/lib/types/smoke.d.ts.map +1 -0
- package/lib/types/smoke.js +66 -0
- package/lib/types/smoke.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,204 @@
|
|
|
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 { spawn, spawnSync } from 'node:child_process';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { escapeXml } from "./decide.js";
|
|
12
|
+
/**
|
|
13
|
+
* The toast script, sent as one `-EncodedCommand` payload.
|
|
14
|
+
*
|
|
15
|
+
* It is a constant. No session text is ever concatenated into PowerShell
|
|
16
|
+
* source, so no amount of text in a session title, an error message, or a tool
|
|
17
|
+
* name can reach the PowerShell parser: the text arrives as an XML document in
|
|
18
|
+
* an environment variable and is handed straight to the WinRT XML parser.
|
|
19
|
+
*/
|
|
20
|
+
export const TOAST_SCRIPT = `$ErrorActionPreference = 'Stop'
|
|
21
|
+
[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime]
|
|
22
|
+
[void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType=WindowsRuntime]
|
|
23
|
+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
24
|
+
$xml.LoadXml([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($env:DSH_PING_XML)))
|
|
25
|
+
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
26
|
+
$appId = $env:DSH_PING_APPID
|
|
27
|
+
if ([string]::IsNullOrEmpty($appId)) { $appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' }
|
|
28
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
|
|
29
|
+
`;
|
|
30
|
+
/**
|
|
31
|
+
* Render the toast XML payload.
|
|
32
|
+
* @param notice - the heading and body lines.
|
|
33
|
+
* @param delivery - presentation options.
|
|
34
|
+
* @returns a complete `<toast>` document.
|
|
35
|
+
*/
|
|
36
|
+
export function buildToastXml(notice, delivery) {
|
|
37
|
+
const attributes = [
|
|
38
|
+
delivery.url === undefined || delivery.url === '' ? '' : `activationType="protocol" launch="${escapeXml(delivery.url)}"`,
|
|
39
|
+
delivery.long === true ? 'duration="long"' : '',
|
|
40
|
+
].filter(part => part !== '').join(' ');
|
|
41
|
+
const texts = [notice.title, ...notice.lines]
|
|
42
|
+
.map(line => ` <text>${escapeXml(line)}</text>`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
const audio = delivery.sound === undefined || delivery.sound === ''
|
|
45
|
+
? ' <audio silent="true"/>'
|
|
46
|
+
: ` <audio src="${escapeXml(delivery.sound)}" loop="false" silent="false"/>`;
|
|
47
|
+
return [
|
|
48
|
+
`<toast ${attributes}>`.replace(' >', '>'),
|
|
49
|
+
' <visual>',
|
|
50
|
+
' <binding template="ToastGeneric">',
|
|
51
|
+
texts,
|
|
52
|
+
' </binding>',
|
|
53
|
+
' </visual>',
|
|
54
|
+
audio,
|
|
55
|
+
'</toast>',
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Encode a script for `powershell.exe -EncodedCommand`.
|
|
60
|
+
* @param script - PowerShell source.
|
|
61
|
+
* @returns base64 of the UTF-16LE script.
|
|
62
|
+
*/
|
|
63
|
+
export function encodeCommand(script) {
|
|
64
|
+
return Buffer.from(script, 'utf16le').toString('base64');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the Windows PowerShell 5.1 executable.
|
|
68
|
+
*
|
|
69
|
+
* WinRT type projection is a 5.1 feature; `pwsh` is not a substitute, so the
|
|
70
|
+
* in-box path is used rather than a PATH lookup that could find PowerShell 7.
|
|
71
|
+
* @param configured - an explicit path from configuration.
|
|
72
|
+
* @returns the executable path, or `undefined` when unavailable.
|
|
73
|
+
*/
|
|
74
|
+
export function resolvePowershell(configured) {
|
|
75
|
+
if (configured !== undefined && configured !== '' && existsSync(configured))
|
|
76
|
+
return configured;
|
|
77
|
+
if (process.platform !== 'win32')
|
|
78
|
+
return undefined;
|
|
79
|
+
const path = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
80
|
+
return existsSync(path) ? path : undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Environment names Windows PowerShell needs to start cleanly. */
|
|
83
|
+
const KEEP_ENV = [
|
|
84
|
+
'SystemRoot', 'windir', 'SystemDrive', 'PATH', 'Path', 'PATHEXT', 'COMSPEC', 'TEMP', 'TMP',
|
|
85
|
+
'USERPROFILE', 'USERNAME', 'COMPUTERNAME', 'NUMBER_OF_PROCESSORS', 'PSModulePath', 'LANG', 'LC_ALL',
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* Build a minimal child environment.
|
|
89
|
+
*
|
|
90
|
+
* A notification process has no business inheriting API keys, so the
|
|
91
|
+
* environment is rebuilt from an allowlist instead of copied.
|
|
92
|
+
* @returns the environment for the toast helper.
|
|
93
|
+
*/
|
|
94
|
+
export function minimalEnv() {
|
|
95
|
+
const env = {};
|
|
96
|
+
for (const name of KEEP_ENV) {
|
|
97
|
+
const value = process.env[name];
|
|
98
|
+
if (value !== undefined)
|
|
99
|
+
env[name] = value;
|
|
100
|
+
}
|
|
101
|
+
return env;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Show a Windows toast. Fire-and-forget by design.
|
|
105
|
+
* @param notice - the heading and body lines.
|
|
106
|
+
* @param delivery - presentation options.
|
|
107
|
+
* @param log - diagnostic sink.
|
|
108
|
+
* @returns true when the helper was started.
|
|
109
|
+
*/
|
|
110
|
+
export function sendToast(notice, delivery, log) {
|
|
111
|
+
const powershell = resolvePowershell(delivery.powershellPath);
|
|
112
|
+
if (powershell === undefined) {
|
|
113
|
+
log('toast channel unavailable: Windows PowerShell 5.1 not found');
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const xml = buildToastXml(notice, delivery);
|
|
117
|
+
try {
|
|
118
|
+
const child = spawn(powershell, ['-NoProfile', '-NonInteractive', '-STA', '-WindowStyle', 'Hidden', '-EncodedCommand', encodeCommand(TOAST_SCRIPT)], {
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
// NOT detached. A detached Windows process has no console, and in that
|
|
121
|
+
// state Windows PowerShell 5.1 loads the WinRT toast types, runs
|
|
122
|
+
// Show(), and exits 0 while no notification is ever raised — a silent
|
|
123
|
+
// failure that cost three lost notifications before it was caught.
|
|
124
|
+
// `unref()` below is what keeps the call non-blocking.
|
|
125
|
+
detached: false,
|
|
126
|
+
stdio: 'ignore',
|
|
127
|
+
env: {
|
|
128
|
+
...minimalEnv(),
|
|
129
|
+
DSH_PING_XML: Buffer.from(xml, 'utf16le').toString('base64'),
|
|
130
|
+
DSH_PING_APPID: delivery.appId,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
child.on('error', (error) => { log(`toast helper failed: ${error.message}`); });
|
|
134
|
+
child.unref();
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
log(`toast helper could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Show a Windows toast and wait for the helper to finish.
|
|
144
|
+
*
|
|
145
|
+
* The plugin never uses this — a live notification must not block a session
|
|
146
|
+
* event — but a diagnostic run has to see the helper's exit code to tell
|
|
147
|
+
* "shown" apart from "silently refused".
|
|
148
|
+
* @param notice - the heading and body lines.
|
|
149
|
+
* @param delivery - presentation options.
|
|
150
|
+
* @returns the helper's exit code and captured error output.
|
|
151
|
+
*/
|
|
152
|
+
export function sendToastSync(notice, delivery) {
|
|
153
|
+
const powershell = resolvePowershell(delivery.powershellPath);
|
|
154
|
+
if (powershell === undefined)
|
|
155
|
+
return { started: false, code: null, output: 'Windows PowerShell 5.1 not found' };
|
|
156
|
+
const xml = buildToastXml(notice, delivery);
|
|
157
|
+
const result = spawnSync(powershell, ['-NoProfile', '-NonInteractive', '-STA', '-WindowStyle', 'Hidden', '-EncodedCommand', encodeCommand(TOAST_SCRIPT)], {
|
|
158
|
+
windowsHide: true,
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
timeout: 20_000,
|
|
161
|
+
env: {
|
|
162
|
+
...minimalEnv(),
|
|
163
|
+
DSH_PING_XML: Buffer.from(xml, 'utf16le').toString('base64'),
|
|
164
|
+
DSH_PING_APPID: delivery.appId,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const output = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
|
|
168
|
+
return { started: true, code: result.status, output };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Write the notice to stderr, where the launching terminal shows it.
|
|
172
|
+
* @param notice - the heading and body lines.
|
|
173
|
+
* @param log - diagnostic sink.
|
|
174
|
+
*/
|
|
175
|
+
export function sendConsole(notice, log) {
|
|
176
|
+
log(`${notice.title}${notice.lines.length === 0 ? '' : ` — ${notice.lines.join(' · ')}`}`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* POST the notice to a webhook.
|
|
180
|
+
* @param payload - the JSON body.
|
|
181
|
+
* @param url - destination URL.
|
|
182
|
+
* @param timeoutMs - request budget.
|
|
183
|
+
* @param log - diagnostic sink.
|
|
184
|
+
* @returns a promise that settles when the attempt finishes.
|
|
185
|
+
*/
|
|
186
|
+
export async function sendWebhook(payload, url, timeoutMs, log) {
|
|
187
|
+
if (url === '')
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'content-type': 'application/json' },
|
|
193
|
+
body: JSON.stringify(payload),
|
|
194
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
195
|
+
});
|
|
196
|
+
await response.body?.cancel();
|
|
197
|
+
if (!response.ok)
|
|
198
|
+
log(`webhook answered ${String(response.status)}`);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
log(`webhook failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/channels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decision core: whether to notify, and what the notification says.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a pure function so the interesting behaviour — the
|
|
5
|
+
* "don't be annoying" rules and the text assembly — is testable without a
|
|
6
|
+
* live harness, a desktop, or a PowerShell.
|
|
7
|
+
* @module dsh-ping/decide
|
|
8
|
+
*/
|
|
9
|
+
/** The four moments worth interrupting a human for. */
|
|
10
|
+
export type NotifyKind = 'done' | 'error' | 'approval' | 'question';
|
|
11
|
+
/** One observable fact about the harness, ready to be turned into a notice. */
|
|
12
|
+
export interface NotifyFact {
|
|
13
|
+
kind: NotifyKind;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
/** Human title of the session, when the host has one. */
|
|
16
|
+
sessionTitle?: string;
|
|
17
|
+
/** Working directory the session runs in. */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Kind-specific payload: answer excerpt, error text, tool name, question. */
|
|
20
|
+
detail?: string;
|
|
21
|
+
/** Wall-clock duration of the turn that just ended. */
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/** The user's "don't be annoying" settings, already resolved. */
|
|
25
|
+
export interface NotifyLimits {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
kinds: Record<NotifyKind, boolean>;
|
|
28
|
+
/** Only the root agent notifies; delegated subagents stay silent. */
|
|
29
|
+
rootsOnly: boolean;
|
|
30
|
+
/** Minimum gap between two notices for the same session and kind. */
|
|
31
|
+
cooldownMs: number;
|
|
32
|
+
/** Turns shorter than this are considered interactive and stay silent. */
|
|
33
|
+
minTurnDurationMs: number;
|
|
34
|
+
/** Maximum characters of `detail` kept in the notice. */
|
|
35
|
+
maxBodyChars: number;
|
|
36
|
+
}
|
|
37
|
+
/** Why a notice was or was not emitted. */
|
|
38
|
+
export interface NotifyVerdict {
|
|
39
|
+
notify: boolean;
|
|
40
|
+
reason: string;
|
|
41
|
+
}
|
|
42
|
+
/** Inputs to the notification decision. */
|
|
43
|
+
export interface ShouldNotifyInput {
|
|
44
|
+
fact: NotifyFact;
|
|
45
|
+
limits: NotifyLimits;
|
|
46
|
+
isRoot: boolean;
|
|
47
|
+
/** When this session and kind last produced a notice. */
|
|
48
|
+
lastNotifiedAt?: number;
|
|
49
|
+
now: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decide whether one fact should reach the user.
|
|
53
|
+
*
|
|
54
|
+
* Order matters: a disabled kind is a configuration statement, a subagent is a
|
|
55
|
+
* structural statement, and both outrank the rate limits, so the reported
|
|
56
|
+
* reason always names the real cause.
|
|
57
|
+
* @param input - the fact plus the limits and bookkeeping it is judged against.
|
|
58
|
+
* @returns the verdict and a short reason.
|
|
59
|
+
*/
|
|
60
|
+
export declare function shouldNotify(input: ShouldNotifyInput): NotifyVerdict;
|
|
61
|
+
/** A rendered notice: one heading plus up to two body lines. */
|
|
62
|
+
export interface Notice {
|
|
63
|
+
title: string;
|
|
64
|
+
lines: string[];
|
|
65
|
+
}
|
|
66
|
+
/** Per-kind headings, so a deployment can localize or reword them. */
|
|
67
|
+
export type NotifyTitles = Record<NotifyKind, string>;
|
|
68
|
+
/**
|
|
69
|
+
* Collapse a value into one printable line.
|
|
70
|
+
*
|
|
71
|
+
* Newlines are removed rather than kept: the Windows toast path carries text
|
|
72
|
+
* through an environment variable and a here-string, and a single line cannot
|
|
73
|
+
* terminate either of them.
|
|
74
|
+
* @param value - raw text.
|
|
75
|
+
* @param maxChars - maximum kept characters (0 keeps everything).
|
|
76
|
+
* @returns the flattened, truncated text.
|
|
77
|
+
*/
|
|
78
|
+
export declare function flatten(value: string, maxChars?: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* Last path segment, used as a short workspace label.
|
|
81
|
+
* @param cwd - an absolute working directory.
|
|
82
|
+
* @returns the final segment, or an empty string.
|
|
83
|
+
*/
|
|
84
|
+
export declare function workspaceOf(cwd: string | undefined): string;
|
|
85
|
+
/**
|
|
86
|
+
* Human duration, Chinese units to match the default headings.
|
|
87
|
+
* @param ms - duration in milliseconds.
|
|
88
|
+
* @returns a compact label such as `2 分 13 秒`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatDuration(ms: number | undefined): string;
|
|
91
|
+
/**
|
|
92
|
+
* Render the notice for one fact.
|
|
93
|
+
*
|
|
94
|
+
* Shape: the heading states what happened, the first line carries the payload
|
|
95
|
+
* (or the session identity when there is none), and the last line always says
|
|
96
|
+
* which workspace and how long — that is what makes several concurrent
|
|
97
|
+
* sessions distinguishable at a glance.
|
|
98
|
+
* @param fact - the fact to render.
|
|
99
|
+
* @param titles - per-kind headings.
|
|
100
|
+
* @param maxBodyChars - maximum characters of the payload line.
|
|
101
|
+
* @returns the notice.
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildNotice(fact: NotifyFact, titles: NotifyTitles, maxBodyChars: number): Notice;
|
|
104
|
+
/**
|
|
105
|
+
* Escape text for inclusion in the toast XML.
|
|
106
|
+
* @param value - raw text.
|
|
107
|
+
* @returns XML-safe text.
|
|
108
|
+
*/
|
|
109
|
+
export declare function escapeXml(value: string): string;
|
|
110
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/decide.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,CAAA;AAEnE,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IAClC,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAA;IACzB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAA;IAChB,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,aAAa,CAmBpE;AAED,gEAAgE;AAChE,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;AAErD;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAQ3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAU7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAsBhG;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO/C"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decision core: whether to notify, and what the notification says.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a pure function so the interesting behaviour — the
|
|
5
|
+
* "don't be annoying" rules and the text assembly — is testable without a
|
|
6
|
+
* live harness, a desktop, or a PowerShell.
|
|
7
|
+
* @module dsh-ping/decide
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Decide whether one fact should reach the user.
|
|
11
|
+
*
|
|
12
|
+
* Order matters: a disabled kind is a configuration statement, a subagent is a
|
|
13
|
+
* structural statement, and both outrank the rate limits, so the reported
|
|
14
|
+
* reason always names the real cause.
|
|
15
|
+
* @param input - the fact plus the limits and bookkeeping it is judged against.
|
|
16
|
+
* @returns the verdict and a short reason.
|
|
17
|
+
*/
|
|
18
|
+
export function shouldNotify(input) {
|
|
19
|
+
const { fact, limits } = input;
|
|
20
|
+
if (!limits.enabled)
|
|
21
|
+
return { notify: false, reason: 'plugin-disabled' };
|
|
22
|
+
if (!limits.kinds[fact.kind])
|
|
23
|
+
return { notify: false, reason: `kind-disabled:${fact.kind}` };
|
|
24
|
+
if (limits.rootsOnly && !input.isRoot)
|
|
25
|
+
return { notify: false, reason: 'subagent' };
|
|
26
|
+
if (limits.cooldownMs > 0 && input.lastNotifiedAt !== undefined
|
|
27
|
+
&& input.now - input.lastNotifiedAt < limits.cooldownMs) {
|
|
28
|
+
return { notify: false, reason: 'cooldown' };
|
|
29
|
+
}
|
|
30
|
+
// The duration gate answers one question only: "could the user have walked
|
|
31
|
+
// away?" A short turn means they were still at the keyboard, so a completion
|
|
32
|
+
// toast would interrupt someone who is already reading the answer. Errors
|
|
33
|
+
// and pending decisions are exempt — those are the moments the user asked to
|
|
34
|
+
// be told about regardless of how long the turn took.
|
|
35
|
+
if (fact.kind === 'done') {
|
|
36
|
+
const duration = fact.durationMs ?? 0;
|
|
37
|
+
if (duration < limits.minTurnDurationMs)
|
|
38
|
+
return { notify: false, reason: 'too-short' };
|
|
39
|
+
}
|
|
40
|
+
return { notify: true, reason: 'ok' };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Collapse a value into one printable line.
|
|
44
|
+
*
|
|
45
|
+
* Newlines are removed rather than kept: the Windows toast path carries text
|
|
46
|
+
* through an environment variable and a here-string, and a single line cannot
|
|
47
|
+
* terminate either of them.
|
|
48
|
+
* @param value - raw text.
|
|
49
|
+
* @param maxChars - maximum kept characters (0 keeps everything).
|
|
50
|
+
* @returns the flattened, truncated text.
|
|
51
|
+
*/
|
|
52
|
+
export function flatten(value, maxChars = 0) {
|
|
53
|
+
const cleaned = value
|
|
54
|
+
// eslint-disable-next-line no-control-regex -- control characters break the toast carrier.
|
|
55
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
58
|
+
if (maxChars <= 0 || cleaned.length <= maxChars)
|
|
59
|
+
return cleaned;
|
|
60
|
+
return `${cleaned.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Last path segment, used as a short workspace label.
|
|
64
|
+
* @param cwd - an absolute working directory.
|
|
65
|
+
* @returns the final segment, or an empty string.
|
|
66
|
+
*/
|
|
67
|
+
export function workspaceOf(cwd) {
|
|
68
|
+
if (cwd === undefined)
|
|
69
|
+
return '';
|
|
70
|
+
const trimmed = cwd.replace(/[\\/]+$/, '');
|
|
71
|
+
if (trimmed === '')
|
|
72
|
+
return '';
|
|
73
|
+
const parts = trimmed.split(/[\\/]/);
|
|
74
|
+
return parts[parts.length - 1] ?? '';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Human duration, Chinese units to match the default headings.
|
|
78
|
+
* @param ms - duration in milliseconds.
|
|
79
|
+
* @returns a compact label such as `2 分 13 秒`.
|
|
80
|
+
*/
|
|
81
|
+
export function formatDuration(ms) {
|
|
82
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
83
|
+
return '';
|
|
84
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
85
|
+
if (totalSeconds < 60)
|
|
86
|
+
return `${String(totalSeconds)} 秒`;
|
|
87
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
88
|
+
const seconds = totalSeconds % 60;
|
|
89
|
+
if (minutes < 60)
|
|
90
|
+
return seconds === 0 ? `${String(minutes)} 分` : `${String(minutes)} 分 ${String(seconds)} 秒`;
|
|
91
|
+
const hours = Math.floor(minutes / 60);
|
|
92
|
+
const rest = minutes % 60;
|
|
93
|
+
return rest === 0 ? `${String(hours)} 小时` : `${String(hours)} 小时 ${String(rest)} 分`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Render the notice for one fact.
|
|
97
|
+
*
|
|
98
|
+
* Shape: the heading states what happened, the first line carries the payload
|
|
99
|
+
* (or the session identity when there is none), and the last line always says
|
|
100
|
+
* which workspace and how long — that is what makes several concurrent
|
|
101
|
+
* sessions distinguishable at a glance.
|
|
102
|
+
* @param fact - the fact to render.
|
|
103
|
+
* @param titles - per-kind headings.
|
|
104
|
+
* @param maxBodyChars - maximum characters of the payload line.
|
|
105
|
+
* @returns the notice.
|
|
106
|
+
*/
|
|
107
|
+
export function buildNotice(fact, titles, maxBodyChars) {
|
|
108
|
+
const title = titles[fact.kind];
|
|
109
|
+
const workspace = workspaceOf(fact.cwd);
|
|
110
|
+
const subject = flatten(fact.sessionTitle ?? '', 80) || workspace || 'DSH 会话';
|
|
111
|
+
const detail = fact.detail === undefined ? '' : flatten(fact.detail, maxBodyChars);
|
|
112
|
+
// The workspace is already the subject when the session has no title;
|
|
113
|
+
// repeating it in the attribution line reads as a bug to the user.
|
|
114
|
+
const meta = [
|
|
115
|
+
workspace === subject ? '' : workspace,
|
|
116
|
+
formatDuration(fact.durationMs),
|
|
117
|
+
].filter(part => part !== '').join(' · ');
|
|
118
|
+
const lines = [];
|
|
119
|
+
if (detail !== '') {
|
|
120
|
+
lines.push(detail);
|
|
121
|
+
const attribution = [subject, meta].filter(part => part !== '').join(' · ');
|
|
122
|
+
if (attribution !== '')
|
|
123
|
+
lines.push(attribution);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
lines.push(subject);
|
|
127
|
+
if (meta !== '' && meta !== subject)
|
|
128
|
+
lines.push(meta);
|
|
129
|
+
}
|
|
130
|
+
return { title, lines: lines.slice(0, 2) };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Escape text for inclusion in the toast XML.
|
|
134
|
+
* @param value - raw text.
|
|
135
|
+
* @returns XML-safe text.
|
|
136
|
+
*/
|
|
137
|
+
export function escapeXml(value) {
|
|
138
|
+
return value
|
|
139
|
+
.replace(/&/g, '&')
|
|
140
|
+
.replace(/</g, '<')
|
|
141
|
+
.replace(/>/g, '>')
|
|
142
|
+
.replace(/"/g, '"')
|
|
143
|
+
.replace(/'/g, ''');
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=decide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/decide.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration shape and defaults.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of every runtime import — including the schema library — so the
|
|
5
|
+
* standalone smoke test can reuse the defaults without pulling a
|
|
6
|
+
* `@deepseek-ai/*` module into a CLI that has to run outside the harness.
|
|
7
|
+
* @module dsh-ping/defaults
|
|
8
|
+
*/
|
|
9
|
+
import type { NotifyKind, NotifyTitles } from './decide.ts';
|
|
10
|
+
/** Plugin configuration. */
|
|
11
|
+
export interface Config {
|
|
12
|
+
/** Master switch. */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Which moments notify. */
|
|
15
|
+
notifyOn: Record<NotifyKind, boolean>;
|
|
16
|
+
/** Only the root agent notifies; delegated subagents stay silent. */
|
|
17
|
+
rootsOnly: boolean;
|
|
18
|
+
/** Minimum gap between two notices for the same session and kind. */
|
|
19
|
+
cooldownMs: number;
|
|
20
|
+
/**
|
|
21
|
+
* Completed turns shorter than this stay silent, because the user was still
|
|
22
|
+
* at the keyboard. Applies to `done` only: errors and pending decisions
|
|
23
|
+
* always notify. Set to 0 to be told about every turn.
|
|
24
|
+
*/
|
|
25
|
+
minTurnDurationMs: number;
|
|
26
|
+
/** Enabled delivery channels. */
|
|
27
|
+
channels: {
|
|
28
|
+
toast: boolean;
|
|
29
|
+
console: boolean;
|
|
30
|
+
webhook: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Webhook destination; empty disables the channel even when enabled. */
|
|
33
|
+
webhookUrl: string;
|
|
34
|
+
/** Webhook request budget. */
|
|
35
|
+
webhookTimeoutMs: number;
|
|
36
|
+
/** Click-through URL; empty derives the loopback Web URL when one exists. */
|
|
37
|
+
url: string;
|
|
38
|
+
/** Maximum characters of the payload line. */
|
|
39
|
+
maxBodyChars: number;
|
|
40
|
+
/** AUMID the toast is attributed to; empty uses the Windows PowerShell identity. */
|
|
41
|
+
toastAppId: string;
|
|
42
|
+
/** Sound for completed turns and errors. */
|
|
43
|
+
toastSoundDone: string;
|
|
44
|
+
/** Sound for pending approvals and questions. */
|
|
45
|
+
toastSoundAttention: string;
|
|
46
|
+
/** Explicit Windows PowerShell 5.1 path; empty resolves the in-box one. */
|
|
47
|
+
powershellPath: string;
|
|
48
|
+
/** Per-moment headings. */
|
|
49
|
+
titles: NotifyTitles;
|
|
50
|
+
/** Log every suppressed notice and its reason. */
|
|
51
|
+
debug: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The defaults, restated by the schema in `index.ts` so the loader applies them too. */
|
|
54
|
+
export declare const DEFAULTS: Config;
|
|
55
|
+
/**
|
|
56
|
+
* Merge a partially applied configuration over the defaults.
|
|
57
|
+
*
|
|
58
|
+
* The loader normally hands over a fully defaulted object, but a deployment
|
|
59
|
+
* that instantiates the plugin directly should not have to.
|
|
60
|
+
* @param config - raw configuration.
|
|
61
|
+
* @returns a complete configuration.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveConfig(config: Partial<Config> | undefined): Config;
|
|
64
|
+
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE3D,4BAA4B;AAC5B,MAAM,WAAW,MAAM;IACrB,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IACrC,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAA;IACzB,iCAAiC;IACjC,QAAQ,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;IAChE,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,gBAAgB,EAAE,MAAM,CAAA;IACxB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAA;IACX,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAA;IAClB,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,2BAA2B;IAC3B,MAAM,EAAE,YAAY,CAAA;IACpB,kDAAkD;IAClD,KAAK,EAAE,OAAO,CAAA;CACf;AAED,yFAAyF;AACzF,eAAO,MAAM,QAAQ,EAAE,MAsBtB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,CASzE"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration shape and defaults.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of every runtime import — including the schema library — so the
|
|
5
|
+
* standalone smoke test can reuse the defaults without pulling a
|
|
6
|
+
* `@deepseek-ai/*` module into a CLI that has to run outside the harness.
|
|
7
|
+
* @module dsh-ping/defaults
|
|
8
|
+
*/
|
|
9
|
+
/** The defaults, restated by the schema in `index.ts` so the loader applies them too. */
|
|
10
|
+
export const DEFAULTS = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
notifyOn: { done: true, error: true, approval: true, question: true },
|
|
13
|
+
rootsOnly: true,
|
|
14
|
+
cooldownMs: 30_000,
|
|
15
|
+
minTurnDurationMs: 20_000,
|
|
16
|
+
channels: { toast: true, console: true, webhook: false },
|
|
17
|
+
webhookUrl: '',
|
|
18
|
+
webhookTimeoutMs: 5_000,
|
|
19
|
+
url: '',
|
|
20
|
+
maxBodyChars: 180,
|
|
21
|
+
toastAppId: '',
|
|
22
|
+
toastSoundDone: 'ms-winsoundevent:Notification.Default',
|
|
23
|
+
toastSoundAttention: 'ms-winsoundevent:Notification.Reminder',
|
|
24
|
+
powershellPath: '',
|
|
25
|
+
titles: {
|
|
26
|
+
done: 'DSH · 任务完成',
|
|
27
|
+
error: 'DSH · 出错了',
|
|
28
|
+
approval: 'DSH · 等你批准',
|
|
29
|
+
question: 'DSH · 等你回答',
|
|
30
|
+
},
|
|
31
|
+
debug: false,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Merge a partially applied configuration over the defaults.
|
|
35
|
+
*
|
|
36
|
+
* The loader normally hands over a fully defaulted object, but a deployment
|
|
37
|
+
* that instantiates the plugin directly should not have to.
|
|
38
|
+
* @param config - raw configuration.
|
|
39
|
+
* @returns a complete configuration.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveConfig(config) {
|
|
42
|
+
const source = config ?? {};
|
|
43
|
+
return {
|
|
44
|
+
...DEFAULTS,
|
|
45
|
+
...source,
|
|
46
|
+
notifyOn: { ...DEFAULTS.notifyOn, ...source.notifyOn },
|
|
47
|
+
channels: { ...DEFAULTS.channels, ...source.channels },
|
|
48
|
+
titles: { ...DEFAULTS.titles, ...source.titles },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA4CH,yFAAyF;AACzF,MAAM,CAAC,MAAM,QAAQ,GAAW;IAC9B,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;IACrE,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,MAAM;IAClB,iBAAiB,EAAE,MAAM;IACzB,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE;IACxD,UAAU,EAAE,EAAE;IACd,gBAAgB,EAAE,KAAK;IACvB,GAAG,EAAE,EAAE;IACP,YAAY,EAAE,GAAG;IACjB,UAAU,EAAE,EAAE;IACd,cAAc,EAAE,uCAAuC;IACvD,mBAAmB,EAAE,wCAAwC;IAC7D,cAAc,EAAE,EAAE;IAClB,MAAM,EAAE;QACN,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,WAAW;QAClB,QAAQ,EAAE,YAAY;QACtB,QAAQ,EAAE,YAAY;KACvB;IACD,KAAK,EAAE,KAAK;CACb,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmC;IAC/D,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;IAC3B,OAAO;QACL,GAAG,QAAQ;QACX,GAAG,MAAM;QACT,QAAQ,EAAE,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE;QACtD,QAAQ,EAAE,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE;QACtD,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE;KACjD,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ping — desktop notifications for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Watches four host moments and tells a human about them: a turn finished, an
|
|
5
|
+
* agent errored, an approval is pending, or the agent asked a question. The
|
|
6
|
+
* delivery is a Windows toast (plus a console line and an optional webhook).
|
|
7
|
+
*
|
|
8
|
+
* The plugin is host-only and imports exactly one `@deepseek-ai/*` module —
|
|
9
|
+
* `schemastery`, the configuration schema library — at runtime. It has no
|
|
10
|
+
* browser half, so it cannot affect what the Web client loads, and no
|
|
11
|
+
* `dsh-*` internals, so renaming one cannot break it.
|
|
12
|
+
* @module dsh-ping
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
import { type Config as PingConfig } from './defaults.ts';
|
|
17
|
+
/** Stable Cordis plugin name. */
|
|
18
|
+
export declare const name = "dsh-ping";
|
|
19
|
+
/** No service is required: every host fact is read optionally. */
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
/**
|
|
22
|
+
* The configuration schema.
|
|
23
|
+
*
|
|
24
|
+
* Every default is written twice on purpose — once here so the loader can
|
|
25
|
+
* validate and fill a partial user config, and once in `defaults.ts` so the
|
|
26
|
+
* dependency-free smoke test shares the same values.
|
|
27
|
+
*/
|
|
28
|
+
export declare const Config: z<PingConfig>;
|
|
29
|
+
export { DEFAULTS, resolveConfig } from './defaults.ts';
|
|
30
|
+
/**
|
|
31
|
+
* Register the notification plugin.
|
|
32
|
+
* @param ctx - the plugin context.
|
|
33
|
+
* @param config - resolved plugin configuration.
|
|
34
|
+
*/
|
|
35
|
+
export declare function apply(ctx: Context, config: PingConfig): void;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,EAA2B,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,eAAe,CAAA;AASlF,iCAAiC;AACjC,eAAO,MAAM,IAAI,aAAa,CAAA;AAE9B,kEAAkE;AAClE,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,UAAU,CA+B/B,CAAA;AAEF,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAYvD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAiR5D"}
|