@skrr-ai/cli 0.1.102 → 0.1.104
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/dist/base-command.js +24 -0
- package/dist/commands/agent/message/send.js +6 -2
- package/dist/commands/agents/triggers/create.js +7 -17
- package/dist/commands/agents/triggers/update.js +41 -28
- package/dist/commands/commitments/create.js +2 -0
- package/dist/commands/commitments/preflight.js +18 -1
- package/dist/commands/commitments/roster.d.ts +15 -0
- package/dist/commands/commitments/roster.js +39 -0
- package/dist/commands/commitments/show.js +1 -1
- package/dist/commands/commitments/status.d.ts +17 -0
- package/dist/commands/commitments/status.js +41 -0
- package/dist/commands/commitments/update.js +12 -0
- package/dist/commands/daemon/logs.d.ts +106 -1
- package/dist/commands/daemon/logs.js +311 -15
- package/dist/commands/followups/checkin.js +46 -3
- package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
- package/dist/commands/tasks/approvals/resolve.js +21 -5
- package/dist/commands/triggers/create.js +8 -17
- package/dist/commands/triggers/fire.js +3 -0
- package/dist/commands/triggers/update.js +41 -27
- package/dist/commands/widgets/check.d.ts +39 -0
- package/dist/commands/widgets/check.js +179 -0
- package/dist/lib/agent-mailbox.d.ts +0 -1
- package/dist/lib/agent-mailbox.js +0 -1
- package/dist/lib/commitment-loop-status.d.ts +16 -0
- package/dist/lib/commitment-loop-status.js +164 -0
- package/dist/lib/commitments.d.ts +28 -1
- package/dist/lib/commitments.js +184 -5
- package/dist/lib/delegated-cli.d.ts +23 -1
- package/dist/lib/delegated-cli.js +61 -4
- package/dist/lib/first-party-harness.d.ts +1 -1
- package/dist/lib/first-party-harness.js +1 -1
- package/dist/lib/session-task-write.d.ts +13 -0
- package/dist/lib/session-task-write.js +63 -3
- package/dist/lib/triggers.d.ts +26 -0
- package/dist/lib/triggers.js +134 -1
- package/dist/lib/widgetCheck.d.ts +123 -0
- package/dist/lib/widgetCheck.js +998 -0
- package/dist/lib/widgetStaticScan.d.ts +111 -0
- package/dist/lib/widgetStaticScan.js +833 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +11908 -8693
- package/oclif.manifest.json +219 -6
- package/package.json +2 -2
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WIDGET_CHECK_MAX_INPUT_CHARS = exports.WIDGET_CHECK_HEURISTIC_NOTE = exports.WIDGET_CHECK_REPORT_VERSION = void 0;
|
|
37
|
+
exports.inputErrorReport = inputErrorReport;
|
|
38
|
+
exports.checkWidgetInput = checkWidgetInput;
|
|
39
|
+
exports.renderWidgetFences = renderWidgetFences;
|
|
40
|
+
exports.formatWidgetCheckReport = formatWidgetCheckReport;
|
|
41
|
+
/**
|
|
42
|
+
* Generative UI — the offline widget self-check behind `skrr widgets check`.
|
|
43
|
+
*
|
|
44
|
+
* WHY IT EXISTS. A widget is rejected AFTER the message is sent: the ingest
|
|
45
|
+
* refuses a block with a bad header, a section named twice or an oversize
|
|
46
|
+
* stylesheet, the person sees an error state, and only then does the harness
|
|
47
|
+
* learn it. Everything that can be known before sending is known here, so a
|
|
48
|
+
* harness can run this in Bash, read the answer, and fix the block while fixing
|
|
49
|
+
* it is still free.
|
|
50
|
+
*
|
|
51
|
+
* ONE GRAMMAR, ONE CONTRACT. Nothing here re-implements either. A fence block is
|
|
52
|
+
* judged by the shared incremental parser (`createUiWidgetFenceParser`, the same
|
|
53
|
+
* state machine the server ingest and the client's streaming display import), a
|
|
54
|
+
* JSON widget by the contract's own Zod schemas, and every limit is read from
|
|
55
|
+
* `UI_WIDGET_LIMITS`. A rule changed in data-provider changes this check with no
|
|
56
|
+
* edit here, which is the only way an offline check stays honest. (A boundary
|
|
57
|
+
* spec in data-provider fails CI if this tree spells the fence name or a section
|
|
58
|
+
* marker as a literal, so even the examples are generated.)
|
|
59
|
+
*
|
|
60
|
+
* WHAT IS CERTAIN AND WHAT IS NOT. Grammar, schema and size findings are exact:
|
|
61
|
+
* they are the ingest's own verdict. A JavaScript syntax error is exact too, as a
|
|
62
|
+
* classic script (that is how the frame runs the js section). The static findings
|
|
63
|
+
* (`widgetStaticScan.ts`) are a HEURISTIC read of the source, and are tagged so.
|
|
64
|
+
*
|
|
65
|
+
* Pure: strings in, a plain JSON-able report out. No network, no files, no
|
|
66
|
+
* credentials, and the JavaScript is compiled (`vm.Script`), never run.
|
|
67
|
+
*/
|
|
68
|
+
const vm = __importStar(require("node:vm"));
|
|
69
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
70
|
+
const widgetStaticScan_1 = require("./widgetStaticScan");
|
|
71
|
+
// ── report types (the `--json` shape; stable) ───────────────────────────────
|
|
72
|
+
exports.WIDGET_CHECK_REPORT_VERSION = 1;
|
|
73
|
+
exports.WIDGET_CHECK_HEURISTIC_NOTE = 'Grammar, schema, size and JavaScript syntax findings are exact. Static findings are a heuristic ' +
|
|
74
|
+
'scan of the source, not a run in the sandbox: one can be a false positive, and none proves the widget works. ' +
|
|
75
|
+
'The frame decides.';
|
|
76
|
+
/** More than this is refused unread: six widgets at their caps are about 1.2 MB. */
|
|
77
|
+
exports.WIDGET_CHECK_MAX_INPUT_CHARS = 8 * 1024 * 1024;
|
|
78
|
+
const LIMIT_MAX = {
|
|
79
|
+
css: data_provider_1.UI_WIDGET_LIMITS.cssMax,
|
|
80
|
+
html: data_provider_1.UI_WIDGET_LIMITS.htmlMax,
|
|
81
|
+
js: data_provider_1.UI_WIDGET_LIMITS.jsMax,
|
|
82
|
+
total: data_provider_1.UI_WIDGET_LIMITS.totalMax,
|
|
83
|
+
title: data_provider_1.UI_WIDGET_LIMITS.titleMax,
|
|
84
|
+
fallback: data_provider_1.UI_WIDGET_LIMITS.fallbackTextMax,
|
|
85
|
+
};
|
|
86
|
+
/** A stylesheet, markup or script this close to its limit has no room left to edit. */
|
|
87
|
+
const NEAR_LIMIT_PERCENT = 75;
|
|
88
|
+
// ── small helpers ───────────────────────────────────────────────────────────
|
|
89
|
+
const numberFormat = new Intl.NumberFormat('en-US');
|
|
90
|
+
const fmt = (n) => numberFormat.format(n);
|
|
91
|
+
function usageOf(used, max, parserOverflow = false) {
|
|
92
|
+
return {
|
|
93
|
+
used,
|
|
94
|
+
max,
|
|
95
|
+
percent: Math.round((used / max) * 1000) / 10,
|
|
96
|
+
over: parserOverflow || used > max,
|
|
97
|
+
...(parserOverflow ? { atLeast: true } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function sizeBlock(parts, overflow) {
|
|
101
|
+
const css = parts.css.length;
|
|
102
|
+
const html = parts.html.length;
|
|
103
|
+
const js = parts.js.length;
|
|
104
|
+
const byteLength = (s) => Buffer.byteLength(s, 'utf8');
|
|
105
|
+
const bytes = {
|
|
106
|
+
css: byteLength(parts.css),
|
|
107
|
+
html: byteLength(parts.html),
|
|
108
|
+
js: byteLength(parts.js),
|
|
109
|
+
total: 0,
|
|
110
|
+
};
|
|
111
|
+
bytes.total = bytes.css + bytes.html + bytes.js;
|
|
112
|
+
return {
|
|
113
|
+
bytes,
|
|
114
|
+
limits: {
|
|
115
|
+
css: usageOf(css, LIMIT_MAX.css, overflow === 'css'),
|
|
116
|
+
html: usageOf(html, LIMIT_MAX.html, overflow === 'html'),
|
|
117
|
+
js: usageOf(js, LIMIT_MAX.js, overflow === 'js'),
|
|
118
|
+
total: usageOf(css + html + js, LIMIT_MAX.total, overflow === 'total'),
|
|
119
|
+
title: usageOf(parts.title.trim().length, LIMIT_MAX.title, false),
|
|
120
|
+
fallback: usageOf(parts.fallback.trim().length, LIMIT_MAX.fallback, overflow === 'fallback'),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function limitsExceeded(limits) {
|
|
125
|
+
return Object.keys(limits).filter((key) => limits[key].over);
|
|
126
|
+
}
|
|
127
|
+
const LIMIT_SUBJECT = {
|
|
128
|
+
css: 'the css section',
|
|
129
|
+
html: 'the html section',
|
|
130
|
+
js: 'the js section',
|
|
131
|
+
total: 'css + html + js together',
|
|
132
|
+
title: 'the title',
|
|
133
|
+
fallback: 'the fallback text',
|
|
134
|
+
};
|
|
135
|
+
function limitIssue(key, usage) {
|
|
136
|
+
const what = LIMIT_SUBJECT[key];
|
|
137
|
+
const size = usage.atLeast
|
|
138
|
+
? `is longer than ${fmt(usage.max)} characters`
|
|
139
|
+
: `is ${fmt(usage.used)} characters; the limit is ${fmt(usage.max)}`;
|
|
140
|
+
return {
|
|
141
|
+
severity: 'error',
|
|
142
|
+
source: 'limit',
|
|
143
|
+
code: `limit/${key}`,
|
|
144
|
+
section: key,
|
|
145
|
+
message: `${what[0].toUpperCase()}${what.slice(1)} ${size}. Over a limit the widget is refused, never trimmed.`,
|
|
146
|
+
fix: key === 'total' || key === 'css' || key === 'html' || key === 'js'
|
|
147
|
+
? `Shorten it or split the idea into several widgets (a message carries at most ${data_provider_1.UI_WIDGET_LIMITS.perMessageMax}). Aim for under a quarter of each limit.`
|
|
148
|
+
: `Shorten it to at most ${fmt(usage.max)} characters.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function nearLimitIssues(limits) {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const key of Object.keys(limits)) {
|
|
154
|
+
if (key === 'title' || key === 'fallback') {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const usage = limits[key];
|
|
158
|
+
if (!usage.over && usage.percent >= NEAR_LIMIT_PERCENT) {
|
|
159
|
+
out.push({
|
|
160
|
+
severity: 'warn',
|
|
161
|
+
source: 'limit',
|
|
162
|
+
code: `near-limit/${key}`,
|
|
163
|
+
section: key,
|
|
164
|
+
message: `${key === 'total' ? 'css + html + js' : `The ${key} section`} uses ${usage.percent}% of its ${fmt(usage.max)}-character limit.`,
|
|
165
|
+
fix: 'A small edit could push it over. Trim it, or split the idea into two widgets.',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
// ── JavaScript syntax (compile only) ────────────────────────────────────────
|
|
172
|
+
const SYNTAX_FIXES = [
|
|
173
|
+
[
|
|
174
|
+
/Cannot use import statement/,
|
|
175
|
+
"A static import is not allowed in a classic script. Use `const lib = await import('d3')` inside an async function, by bare library name.",
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
/await is only valid/,
|
|
179
|
+
'The js section is a classic script: no top-level await. Put the await inside an async function and call it.',
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
/Illegal return statement/,
|
|
183
|
+
'A classic script has no top-level return; wrap the code in a function.',
|
|
184
|
+
],
|
|
185
|
+
];
|
|
186
|
+
/**
|
|
187
|
+
* The message of anything thrown. Duck-typed, not `instanceof Error`: an error
|
|
188
|
+
* thrown by `vm.Script` or `JSON.parse` can come from another realm (a jest
|
|
189
|
+
* context, a worker), where `instanceof` is false and the real message is lost.
|
|
190
|
+
*/
|
|
191
|
+
function errorMessage(err) {
|
|
192
|
+
const message = err?.message;
|
|
193
|
+
return typeof message === 'string' ? message : String(err);
|
|
194
|
+
}
|
|
195
|
+
function syntaxIssue(script, section, firstLine) {
|
|
196
|
+
try {
|
|
197
|
+
// Compile only. `vm.Script` parses and reports early errors; it never runs the code.
|
|
198
|
+
new vm.Script(script, { filename: 'widget.js' });
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
const message = errorMessage(err);
|
|
203
|
+
const stack = err?.stack;
|
|
204
|
+
const head = typeof stack === 'string' ? stack.split('\n')[0] : '';
|
|
205
|
+
const at = /^widget\.js:(\d+)/.exec(head);
|
|
206
|
+
const fix = SYNTAX_FIXES.find(([pattern]) => pattern.test(message))?.[1];
|
|
207
|
+
return {
|
|
208
|
+
severity: 'error',
|
|
209
|
+
source: 'syntax',
|
|
210
|
+
code: 'syntax/js',
|
|
211
|
+
section,
|
|
212
|
+
line: firstLine + (at ? Number(at[1]) - 1 : 0),
|
|
213
|
+
message: `JavaScript syntax error: ${message}. The script would stop before running.`,
|
|
214
|
+
...(fix ? { fix } : {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function staticIssue(finding) {
|
|
219
|
+
return {
|
|
220
|
+
severity: finding.severity,
|
|
221
|
+
source: 'static',
|
|
222
|
+
code: `static/${finding.code}`,
|
|
223
|
+
section: finding.section,
|
|
224
|
+
line: finding.line,
|
|
225
|
+
message: finding.message,
|
|
226
|
+
...(finding.fix ? { fix: finding.fix } : {}),
|
|
227
|
+
heuristic: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Syntax and static findings for the three sections. Only call it on sections within the limits. */
|
|
231
|
+
function contentIssues(parts) {
|
|
232
|
+
const issues = [];
|
|
233
|
+
if (parts.js.trim() !== '') {
|
|
234
|
+
const syntax = syntaxIssue(parts.js, 'js', 1);
|
|
235
|
+
if (syntax) {
|
|
236
|
+
issues.push(syntax);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const scan = (0, widgetStaticScan_1.scanWidgetSource)(parts);
|
|
240
|
+
for (const script of scan.inlineScripts) {
|
|
241
|
+
const syntax = syntaxIssue(script.content, 'html', script.line);
|
|
242
|
+
if (syntax) {
|
|
243
|
+
issues.push(syntax);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const finding of scan.findings) {
|
|
247
|
+
issues.push(staticIssue(finding));
|
|
248
|
+
}
|
|
249
|
+
return issues;
|
|
250
|
+
}
|
|
251
|
+
// ── finishing a widget ──────────────────────────────────────────────────────
|
|
252
|
+
/** The order a person reads a widget in. The section names come from the contract, not from here. */
|
|
253
|
+
const SECTION_ORDER = [
|
|
254
|
+
'header',
|
|
255
|
+
'title',
|
|
256
|
+
...data_provider_1.UI_WIDGET_FENCE.sections,
|
|
257
|
+
'fallbackText',
|
|
258
|
+
'total',
|
|
259
|
+
];
|
|
260
|
+
function sectionRank(section) {
|
|
261
|
+
const at = section === undefined ? -1 : SECTION_ORDER.indexOf(section);
|
|
262
|
+
return at === -1 ? SECTION_ORDER.length : at;
|
|
263
|
+
}
|
|
264
|
+
/** Errors before warnings, then in the order a person reads the source: section, then line. */
|
|
265
|
+
function sortIssues(issues) {
|
|
266
|
+
return issues
|
|
267
|
+
.map((issue, index) => ({ issue, index }))
|
|
268
|
+
.sort((a, b) => Number(b.issue.severity === 'error') - Number(a.issue.severity === 'error') ||
|
|
269
|
+
sectionRank(a.issue.section) - sectionRank(b.issue.section) ||
|
|
270
|
+
(a.issue.line ?? 0) - (b.issue.line ?? 0) ||
|
|
271
|
+
a.index - b.index)
|
|
272
|
+
.map(({ issue }) => issue);
|
|
273
|
+
}
|
|
274
|
+
function finishWidget(widget, strict) {
|
|
275
|
+
const hasError = widget.issues.some((i) => i.severity === 'error');
|
|
276
|
+
const hasWarn = widget.issues.some((i) => i.severity === 'warn');
|
|
277
|
+
return {
|
|
278
|
+
...widget,
|
|
279
|
+
issues: sortIssues(widget.issues),
|
|
280
|
+
ok: widget.accepted && !hasError && !(strict && hasWarn),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// ── fence input ─────────────────────────────────────────────────────────────
|
|
284
|
+
/** The opener, generated from the contract so this file never spells the grammar. */
|
|
285
|
+
function exampleOpener() {
|
|
286
|
+
return (0, data_provider_1.renderUiWidgetFence)({ title: 'Short title', initialHeight: 420 }).split('\n')[0];
|
|
287
|
+
}
|
|
288
|
+
function grammarFix(reason, message) {
|
|
289
|
+
const order = data_provider_1.UI_WIDGET_FENCE.markers.join(', ');
|
|
290
|
+
switch (reason) {
|
|
291
|
+
case 'invalid-header':
|
|
292
|
+
return `Write the opening line like ${exampleOpener()}: the title in quotes, height an integer, nothing after the closing brace.`;
|
|
293
|
+
case 'invalid-payload':
|
|
294
|
+
if (/more than once/.test(message)) {
|
|
295
|
+
return `Use each section at most once (${order}; the platform reads them in any order). Any of them may be left out.`;
|
|
296
|
+
}
|
|
297
|
+
if (/no html or js/.test(message)) {
|
|
298
|
+
return 'Add an html section or a js section; a stylesheet alone is refused.';
|
|
299
|
+
}
|
|
300
|
+
if (/before the first section marker/.test(message)) {
|
|
301
|
+
return `Start the first line after the opening line with a section marker (${order}).`;
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
case 'oversize':
|
|
305
|
+
return `Limits: css ${fmt(data_provider_1.UI_WIDGET_LIMITS.cssMax)}, html ${fmt(data_provider_1.UI_WIDGET_LIMITS.htmlMax)}, js ${fmt(data_provider_1.UI_WIDGET_LIMITS.jsMax)}, ${fmt(data_provider_1.UI_WIDGET_LIMITS.totalMax)} in all. Shorten it or split it into several widgets.`;
|
|
306
|
+
case 'too-many':
|
|
307
|
+
return `A message carries at most ${data_provider_1.UI_WIDGET_LIMITS.perMessageMax} widgets. Merge ideas or drop the extras.`;
|
|
308
|
+
case 'truncated':
|
|
309
|
+
return 'Close the block with a line of colons at least as long as the opening line. If the reply was cut off by a length limit, write a shorter widget.';
|
|
310
|
+
default:
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function overflowKey(message) {
|
|
315
|
+
for (const section of data_provider_1.UI_WIDGET_FENCE.sections) {
|
|
316
|
+
if (message.startsWith(`${section} exceeds`)) {
|
|
317
|
+
return section;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return 'total';
|
|
321
|
+
}
|
|
322
|
+
function fenceReportFromPartial(partial) {
|
|
323
|
+
return {
|
|
324
|
+
css: partial?.css ?? '',
|
|
325
|
+
html: partial?.html ?? '',
|
|
326
|
+
js: partial?.js ?? '',
|
|
327
|
+
title: partial?.title ?? '',
|
|
328
|
+
fallback: partial?.fallbackText ?? '',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function checkFenceText(text, strict) {
|
|
332
|
+
// CR and CRLF each count as one line break in the grammar, and inside a block
|
|
333
|
+
// they are normalised to LF anyway, so normalising first changes no verdict.
|
|
334
|
+
const normalised = text.replace(/\r\n?/g, '\n');
|
|
335
|
+
const trailingBreak = normalised.endsWith('\n');
|
|
336
|
+
const lines = normalised.split('\n');
|
|
337
|
+
if (trailingBreak) {
|
|
338
|
+
lines.pop();
|
|
339
|
+
}
|
|
340
|
+
const parser = (0, data_provider_1.createUiWidgetFenceParser)();
|
|
341
|
+
const open = new Map();
|
|
342
|
+
const widgets = [];
|
|
343
|
+
let outside = '';
|
|
344
|
+
let sawOpener = false;
|
|
345
|
+
const handle = (event, lineNo, atEnd) => {
|
|
346
|
+
switch (event.type) {
|
|
347
|
+
case 'text':
|
|
348
|
+
outside += event.text;
|
|
349
|
+
break;
|
|
350
|
+
case 'widget-open':
|
|
351
|
+
sawOpener = true;
|
|
352
|
+
open.set(event.id, { id: event.id, title: event.title, line: lineNo, sections: new Set() });
|
|
353
|
+
break;
|
|
354
|
+
case 'widget-section':
|
|
355
|
+
open.get(event.id)?.sections.add(event.section);
|
|
356
|
+
break;
|
|
357
|
+
case 'widget-complete': {
|
|
358
|
+
sawOpener = true;
|
|
359
|
+
const block = open.get(event.id);
|
|
360
|
+
const payload = event.payload;
|
|
361
|
+
const sized = sizeBlock({
|
|
362
|
+
css: payload.css,
|
|
363
|
+
html: payload.html,
|
|
364
|
+
js: payload.js,
|
|
365
|
+
title: payload.title,
|
|
366
|
+
fallback: payload.fallbackText ?? '',
|
|
367
|
+
});
|
|
368
|
+
const issues = [];
|
|
369
|
+
if (!block?.sections.has('fallback')) {
|
|
370
|
+
issues.push({
|
|
371
|
+
severity: 'warn',
|
|
372
|
+
source: 'content',
|
|
373
|
+
code: 'no-fallback',
|
|
374
|
+
section: 'fallback',
|
|
375
|
+
message: 'No fallback text, so the title stands in for it. Search, notifications, voice, chat integrations and older apps show only that.',
|
|
376
|
+
fix: `Add a ${data_provider_1.UI_WIDGET_FENCE.markers[3]} section with one or two plain sentences saying what the widget shows.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
issues.push(...nearLimitIssues(sized.limits), ...contentIssues(payload));
|
|
380
|
+
widgets.push(finishWidget({
|
|
381
|
+
id: payload.id,
|
|
382
|
+
title: payload.title,
|
|
383
|
+
kind: 'fence',
|
|
384
|
+
...(block ? { line: block.line } : {}),
|
|
385
|
+
complete: true,
|
|
386
|
+
measured: true,
|
|
387
|
+
accepted: true,
|
|
388
|
+
...sized,
|
|
389
|
+
issues,
|
|
390
|
+
}, strict));
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case 'widget-error': {
|
|
394
|
+
sawOpener = true;
|
|
395
|
+
const block = open.get(event.id);
|
|
396
|
+
const message = event.message ?? event.reason;
|
|
397
|
+
const overflow = event.reason === 'oversize' ? overflowKey(message) : undefined;
|
|
398
|
+
const sized = sizeBlock(fenceReportFromPartial(event.partial), overflow);
|
|
399
|
+
const startLine = block?.line ?? lineNo;
|
|
400
|
+
let where = `${message}.`;
|
|
401
|
+
if (event.reason === 'truncated') {
|
|
402
|
+
where = `The block that starts at line ${startLine} never closed${atEnd ? ' before the input ended' : ''}.`;
|
|
403
|
+
}
|
|
404
|
+
else if (block) {
|
|
405
|
+
where = `In the block that starts at line ${block.line}: ${message}.`;
|
|
406
|
+
}
|
|
407
|
+
const issues = [
|
|
408
|
+
{
|
|
409
|
+
severity: 'error',
|
|
410
|
+
source: 'grammar',
|
|
411
|
+
code: `fence/${event.reason}`,
|
|
412
|
+
...(event.reason === 'invalid-header' || !block ? { section: 'header' } : {}),
|
|
413
|
+
line: event.reason === 'truncated' ? startLine : lineNo,
|
|
414
|
+
message: where,
|
|
415
|
+
...(grammarFix(event.reason, message)
|
|
416
|
+
? { fix: grammarFix(event.reason, message) }
|
|
417
|
+
: {}),
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
if (event.reason === 'oversize' && overflow) {
|
|
421
|
+
issues.push(limitIssue(overflow, sized.limits[overflow]));
|
|
422
|
+
}
|
|
423
|
+
widgets.push(finishWidget({
|
|
424
|
+
id: event.id,
|
|
425
|
+
title: block?.title ?? null,
|
|
426
|
+
kind: 'fence',
|
|
427
|
+
line: startLine,
|
|
428
|
+
complete: false,
|
|
429
|
+
measured: event.partial !== undefined,
|
|
430
|
+
accepted: false,
|
|
431
|
+
...sized,
|
|
432
|
+
issues,
|
|
433
|
+
}, strict));
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
default:
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
if (normalised.length > 0) {
|
|
441
|
+
lines.forEach((line, index) => {
|
|
442
|
+
const last = index === lines.length - 1;
|
|
443
|
+
const chunk = last && !trailingBreak ? line : `${line}\n`;
|
|
444
|
+
for (const event of parser.push(chunk)) {
|
|
445
|
+
handle(event, index + 1, false);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
for (const event of parser.end()) {
|
|
450
|
+
handle(event, Math.max(lines.length, 1), true);
|
|
451
|
+
}
|
|
452
|
+
return { widgets, sawOpener, residue: outside.includes(data_provider_1.UI_WIDGET_FENCE.name) };
|
|
453
|
+
}
|
|
454
|
+
function isRecord(value) {
|
|
455
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
456
|
+
}
|
|
457
|
+
function stringField(value, key) {
|
|
458
|
+
const field = value[key];
|
|
459
|
+
return typeof field === 'string' ? field : '';
|
|
460
|
+
}
|
|
461
|
+
function allowedKeys(schema) {
|
|
462
|
+
const shape = schema.shape;
|
|
463
|
+
return shape ? Object.keys(shape).join(', ') : '';
|
|
464
|
+
}
|
|
465
|
+
/** Zod's complaint, made plain. Size findings are reported by `limitIssue`, so those are skipped. */
|
|
466
|
+
function schemaIssues(issues, schema, handled) {
|
|
467
|
+
const out = [];
|
|
468
|
+
for (const issue of issues) {
|
|
469
|
+
const field = typeof issue.path[0] === 'string' ? issue.path[0] : undefined;
|
|
470
|
+
if (issue.code === 'too_big' && field && handled.has(field)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (issue.code === 'custom' && /exceeds .* characters/.test(issue.message)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
let message;
|
|
477
|
+
let fix;
|
|
478
|
+
if (issue.code === 'unrecognized_keys') {
|
|
479
|
+
const allowed = allowedKeys(schema);
|
|
480
|
+
message = `Unknown field${(issue.keys?.length ?? 0) === 1 ? '' : 's'}: ${(issue.keys ?? []).join(', ')}. The widget shape is strict.`;
|
|
481
|
+
fix = allowed ? `Allowed fields: ${allowed}.` : undefined;
|
|
482
|
+
}
|
|
483
|
+
else if (field === 'title' && issue.code === 'too_small') {
|
|
484
|
+
message = `title is required (1 to ${data_provider_1.UI_WIDGET_LIMITS.titleMax} characters).`;
|
|
485
|
+
}
|
|
486
|
+
else if (field === 'initialHeight') {
|
|
487
|
+
message = `initialHeight must be an integer from ${data_provider_1.UI_WIDGET_LIMITS.heightMin} to ${data_provider_1.UI_WIDGET_LIMITS.heightHardMax}.`;
|
|
488
|
+
}
|
|
489
|
+
else if (field === 'fallbackText' && issue.code === 'custom') {
|
|
490
|
+
message = 'A complete widget must carry fallbackText.';
|
|
491
|
+
fix =
|
|
492
|
+
'Add one or two plain sentences saying what the widget shows: search, notifications, voice and older apps show it instead of the markup.';
|
|
493
|
+
}
|
|
494
|
+
else if (field === 'v') {
|
|
495
|
+
message = 'v must be 1.';
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
message = `${issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ` : ''}${issue.message}`;
|
|
499
|
+
}
|
|
500
|
+
out.push({
|
|
501
|
+
severity: 'error',
|
|
502
|
+
source: 'schema',
|
|
503
|
+
code: `schema/${issue.code}`,
|
|
504
|
+
...(field ? { section: field } : {}),
|
|
505
|
+
message,
|
|
506
|
+
...(fix ? { fix } : {}),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
const SIZED_FIELDS = new Set([
|
|
512
|
+
...data_provider_1.UI_WIDGET_FENCE.sections.filter((section) => section !== 'fallback'),
|
|
513
|
+
'title',
|
|
514
|
+
'fallbackText',
|
|
515
|
+
]);
|
|
516
|
+
function unwrapPart(value) {
|
|
517
|
+
if (isRecord(value) &&
|
|
518
|
+
value.type === data_provider_1.UI_WIDGET_PART_TYPE &&
|
|
519
|
+
isRecord(value[data_provider_1.UI_WIDGET_PART_TYPE])) {
|
|
520
|
+
return value[data_provider_1.UI_WIDGET_PART_TYPE];
|
|
521
|
+
}
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
function checkJsonWidget(raw, index, strict) {
|
|
525
|
+
const fallbackId = `w${index + 1}`;
|
|
526
|
+
const value = unwrapPart(raw);
|
|
527
|
+
if (!isRecord(value)) {
|
|
528
|
+
return {
|
|
529
|
+
report: finishWidget({
|
|
530
|
+
id: fallbackId,
|
|
531
|
+
title: null,
|
|
532
|
+
kind: 'payload',
|
|
533
|
+
complete: false,
|
|
534
|
+
measured: false,
|
|
535
|
+
accepted: false,
|
|
536
|
+
...sizeBlock({ css: '', html: '', js: '', title: '', fallback: '' }),
|
|
537
|
+
issues: [
|
|
538
|
+
{
|
|
539
|
+
severity: 'error',
|
|
540
|
+
source: 'schema',
|
|
541
|
+
code: 'schema/invalid_type',
|
|
542
|
+
message: `Element ${index + 1} is ${Array.isArray(value) ? 'an array' : typeof value}; a widget is a JSON object.`,
|
|
543
|
+
fix: 'Pass a widget payload, a ui_widget part, or the ui_widget_render tool arguments.',
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
}, strict),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const isPayload = 'v' in value || 'id' in value || 'complete' in value || 'source' in value;
|
|
550
|
+
const kind = isPayload ? 'payload' : 'tool-input';
|
|
551
|
+
const schema = isPayload ? data_provider_1.UiWidgetPayloadSchema : data_provider_1.UiWidgetToolInputSchema;
|
|
552
|
+
const parts = {
|
|
553
|
+
css: stringField(value, 'css'),
|
|
554
|
+
html: stringField(value, 'html'),
|
|
555
|
+
js: stringField(value, 'js'),
|
|
556
|
+
title: stringField(value, 'title'),
|
|
557
|
+
fallback: stringField(value, 'fallbackText'),
|
|
558
|
+
};
|
|
559
|
+
const sized = sizeBlock(parts);
|
|
560
|
+
const issues = [];
|
|
561
|
+
for (const key of limitsExceeded(sized.limits)) {
|
|
562
|
+
issues.push(limitIssue(key, sized.limits[key]));
|
|
563
|
+
}
|
|
564
|
+
const parsed = schema.safeParse(value);
|
|
565
|
+
const accepted = parsed.success;
|
|
566
|
+
if (!parsed.success) {
|
|
567
|
+
issues.push(...schemaIssues(parsed.error.issues, schema, SIZED_FIELDS));
|
|
568
|
+
}
|
|
569
|
+
const complete = isPayload ? value.complete === true : true;
|
|
570
|
+
if (isPayload && value.complete === false) {
|
|
571
|
+
issues.push({
|
|
572
|
+
severity: 'warn',
|
|
573
|
+
source: 'content',
|
|
574
|
+
code: 'incomplete',
|
|
575
|
+
section: 'complete',
|
|
576
|
+
message: 'complete is false: this widget is still being written, so a surface shows it unfinished.',
|
|
577
|
+
fix: 'Send the final widget with complete: true and a fallbackText.',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (parts.html.trim() === '' && parts.js.trim() === '') {
|
|
581
|
+
issues.push({
|
|
582
|
+
severity: 'warn',
|
|
583
|
+
source: 'content',
|
|
584
|
+
code: 'empty-widget',
|
|
585
|
+
message: 'No html and no js: the frame would be empty. (A fence block with neither is refused.)',
|
|
586
|
+
fix: 'Add markup or a script that builds the page.',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
// Content checks read the source; skip them when a size limit is already broken,
|
|
590
|
+
// because the widget is refused anyway and the scan's cost should stay bounded.
|
|
591
|
+
const withinLimits = limitsExceeded(sized.limits).length === 0;
|
|
592
|
+
if (withinLimits) {
|
|
593
|
+
issues.push(...nearLimitIssues(sized.limits), ...contentIssues(parts));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
issues.push({
|
|
597
|
+
severity: 'warn',
|
|
598
|
+
source: 'content',
|
|
599
|
+
code: 'content-not-checked',
|
|
600
|
+
message: 'Syntax and static checks were skipped because a size limit is exceeded. Fix the size first.',
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
const id = isPayload && typeof value.id === 'string' && value.id !== '' ? value.id : fallbackId;
|
|
604
|
+
const render = {
|
|
605
|
+
title: parts.title.trim(),
|
|
606
|
+
css: parts.css,
|
|
607
|
+
html: parts.html,
|
|
608
|
+
js: parts.js,
|
|
609
|
+
...(parts.fallback.trim() ? { fallbackText: parts.fallback.trim() } : {}),
|
|
610
|
+
...(typeof value.initialHeight === 'number' ? { initialHeight: value.initialHeight } : {}),
|
|
611
|
+
...(isPayload && typeof value.id === 'string' && value.id !== '' ? { id: value.id } : {}),
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
report: finishWidget({
|
|
615
|
+
id,
|
|
616
|
+
title: parts.title.trim() || null,
|
|
617
|
+
kind,
|
|
618
|
+
complete,
|
|
619
|
+
measured: true,
|
|
620
|
+
accepted,
|
|
621
|
+
...sized,
|
|
622
|
+
issues,
|
|
623
|
+
}, strict),
|
|
624
|
+
...(accepted ? { render } : {}),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function tooManyWidget(index) {
|
|
628
|
+
return {
|
|
629
|
+
id: `w${index + 1}`,
|
|
630
|
+
title: null,
|
|
631
|
+
kind: 'payload',
|
|
632
|
+
complete: false,
|
|
633
|
+
measured: false,
|
|
634
|
+
accepted: false,
|
|
635
|
+
ok: false,
|
|
636
|
+
...sizeBlock({ css: '', html: '', js: '', title: '', fallback: '' }),
|
|
637
|
+
issues: [
|
|
638
|
+
{
|
|
639
|
+
severity: 'error',
|
|
640
|
+
source: 'grammar',
|
|
641
|
+
code: TOO_MANY_CODE,
|
|
642
|
+
message: `Widget ${index + 1} is over the per-message limit: at most ${data_provider_1.UI_WIDGET_LIMITS.perMessageMax} widgets per message, and the rest are refused.`,
|
|
643
|
+
fix: `Merge ideas or drop the extras (a message carries at most ${data_provider_1.UI_WIDGET_LIMITS.perMessageMax}).`,
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const TOO_MANY_CODE = 'fence/too-many';
|
|
649
|
+
/**
|
|
650
|
+
* Keep the first block past the per-message cap and fold the rest into its
|
|
651
|
+
* message: a hostile input with a hundred thousand blocks must not become a
|
|
652
|
+
* hundred thousand report entries, and the person needs one line, not all of them.
|
|
653
|
+
*/
|
|
654
|
+
function collapseTooMany(widgets) {
|
|
655
|
+
const out = [];
|
|
656
|
+
let first;
|
|
657
|
+
let extra = 0;
|
|
658
|
+
for (const widget of widgets) {
|
|
659
|
+
if (widget.issues[0]?.code !== TOO_MANY_CODE) {
|
|
660
|
+
out.push(widget);
|
|
661
|
+
}
|
|
662
|
+
else if (!first) {
|
|
663
|
+
first = widget;
|
|
664
|
+
out.push(widget);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
extra += 1;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (first) {
|
|
671
|
+
noteExcess(first, extra);
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
function noteExcess(widget, extra) {
|
|
676
|
+
if (extra <= 0) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const issue = widget.issues[0];
|
|
680
|
+
widget.issues[0] = {
|
|
681
|
+
...issue,
|
|
682
|
+
message: `${issue.message} ${fmt(extra)} more after it ${extra === 1 ? 'is' : 'are'} refused too.`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/** `JSON.parse`, and one more round when the result is a string: tool arguments often arrive that way. */
|
|
686
|
+
function parseJsonInput(text) {
|
|
687
|
+
let value;
|
|
688
|
+
try {
|
|
689
|
+
value = JSON.parse(text);
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
return { ok: false, message: errorMessage(err) };
|
|
693
|
+
}
|
|
694
|
+
if (typeof value === 'string') {
|
|
695
|
+
try {
|
|
696
|
+
value = JSON.parse(value);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
message: 'the JSON value is a string, and that string is not itself valid JSON',
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { ok: true, value };
|
|
706
|
+
}
|
|
707
|
+
function finalize(format, widgets, errors, warnings, strict) {
|
|
708
|
+
const widgetErrors = widgets.reduce((n, w) => n + w.issues.filter((i) => i.severity === 'error').length, 0);
|
|
709
|
+
const widgetWarnings = widgets.reduce((n, w) => n + w.issues.filter((i) => i.severity === 'warn').length, 0);
|
|
710
|
+
return {
|
|
711
|
+
reportVersion: exports.WIDGET_CHECK_REPORT_VERSION,
|
|
712
|
+
ok: errors.length === 0 &&
|
|
713
|
+
widgets.length > 0 &&
|
|
714
|
+
widgets.every((w) => w.ok) &&
|
|
715
|
+
!(strict && warnings.length > 0),
|
|
716
|
+
format,
|
|
717
|
+
widgets,
|
|
718
|
+
errors,
|
|
719
|
+
warnings,
|
|
720
|
+
summary: {
|
|
721
|
+
widgets: widgets.length,
|
|
722
|
+
accepted: widgets.filter((w) => w.accepted).length,
|
|
723
|
+
ok: widgets.filter((w) => w.ok).length,
|
|
724
|
+
errors: errors.length + widgetErrors,
|
|
725
|
+
warnings: warnings.length + widgetWarnings,
|
|
726
|
+
},
|
|
727
|
+
heuristic: exports.WIDGET_CHECK_HEURISTIC_NOTE,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** The report for an input that could not even be read (a missing file, a terminal as stdin). */
|
|
731
|
+
function inputErrorReport(error) {
|
|
732
|
+
return finalize('unknown', [], [error], [], false);
|
|
733
|
+
}
|
|
734
|
+
function noWidgetError(text, jsonMessage, forced = false) {
|
|
735
|
+
if (jsonMessage) {
|
|
736
|
+
return {
|
|
737
|
+
code: 'input/invalid-json',
|
|
738
|
+
message: forced
|
|
739
|
+
? `The input is not valid JSON: ${jsonMessage}.`
|
|
740
|
+
: `No widget block found, and the input does not parse as JSON either (${jsonMessage}).`,
|
|
741
|
+
fix: 'Pass a widget payload or the ui_widget_render tool arguments as one JSON value, or a text file that contains a widget block.',
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (text.trim() === '') {
|
|
745
|
+
return {
|
|
746
|
+
code: 'input/empty',
|
|
747
|
+
message: 'The input is empty, so there is no widget to check.',
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (text.includes(data_provider_1.UI_WIDGET_FENCE.name)) {
|
|
751
|
+
return {
|
|
752
|
+
code: 'input/no-widget',
|
|
753
|
+
message: 'The text names the widget fence but contains no block the parser recognises. A block must start at the first column of a line, with four or more colons, outside any code fence, list or quote.',
|
|
754
|
+
fix: `Open with ${exampleOpener()} at column 0, and close with a line of at least as many colons.`,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
code: 'input/no-widget',
|
|
759
|
+
message: 'No widget block found in the input.',
|
|
760
|
+
fix: `A widget block opens with a line like ${exampleOpener()} at column 0. For JSON input, pass a widget payload or the ui_widget_render arguments.`,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Check one input: a file's text that contains a widget block, or a JSON widget.
|
|
765
|
+
* Total: never throws, whatever the input.
|
|
766
|
+
*/
|
|
767
|
+
function checkWidgetInput(raw, options = {}) {
|
|
768
|
+
const strict = options.strict === true;
|
|
769
|
+
const format = options.format ?? 'auto';
|
|
770
|
+
const none = [];
|
|
771
|
+
const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
772
|
+
if (text.length > exports.WIDGET_CHECK_MAX_INPUT_CHARS) {
|
|
773
|
+
return {
|
|
774
|
+
report: finalize('unknown', [], [
|
|
775
|
+
{
|
|
776
|
+
code: 'input/too-large',
|
|
777
|
+
message: `The input is ${fmt(text.length)} characters; the check reads at most ${fmt(exports.WIDGET_CHECK_MAX_INPUT_CHARS)}. A message carries at most ${data_provider_1.UI_WIDGET_LIMITS.perMessageMax} widgets of ${fmt(data_provider_1.UI_WIDGET_LIMITS.totalMax)} characters.`,
|
|
778
|
+
},
|
|
779
|
+
], [], strict),
|
|
780
|
+
renderable: none,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const firstChar = text.trimStart().charAt(0);
|
|
784
|
+
const looksJson = firstChar === '{' || firstChar === '[' || firstChar === '"';
|
|
785
|
+
const asJson = format === 'json' || (format === 'auto' && looksJson);
|
|
786
|
+
let jsonFailure;
|
|
787
|
+
if (asJson) {
|
|
788
|
+
const parsed = parseJsonInput(text);
|
|
789
|
+
if (parsed.ok) {
|
|
790
|
+
const elements = Array.isArray(parsed.value) ? parsed.value : [parsed.value];
|
|
791
|
+
if (elements.length === 0) {
|
|
792
|
+
return {
|
|
793
|
+
report: finalize('json', [], [
|
|
794
|
+
{
|
|
795
|
+
code: 'input/no-widget',
|
|
796
|
+
message: 'The JSON array is empty, so there is no widget to check.',
|
|
797
|
+
},
|
|
798
|
+
], [], strict),
|
|
799
|
+
renderable: none,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const widgets = [];
|
|
803
|
+
const renderable = [];
|
|
804
|
+
const warnings = [];
|
|
805
|
+
const seen = new Set();
|
|
806
|
+
let excess = 0;
|
|
807
|
+
elements.forEach((element, index) => {
|
|
808
|
+
if (index >= data_provider_1.UI_WIDGET_LIMITS.perMessageMax) {
|
|
809
|
+
excess += 1;
|
|
810
|
+
if (excess === 1) {
|
|
811
|
+
widgets.push(tooManyWidget(index));
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const result = checkJsonWidget(element, index, strict);
|
|
816
|
+
if (seen.has(result.report.id)) {
|
|
817
|
+
result.report.issues.push({
|
|
818
|
+
severity: 'warn',
|
|
819
|
+
source: 'content',
|
|
820
|
+
code: 'duplicate-id',
|
|
821
|
+
section: 'id',
|
|
822
|
+
message: `Another widget in this input already uses the id "${result.report.id}".`,
|
|
823
|
+
fix: 'Give each widget of a message its own id.',
|
|
824
|
+
});
|
|
825
|
+
result.report.ok = result.report.ok && !strict;
|
|
826
|
+
}
|
|
827
|
+
seen.add(result.report.id);
|
|
828
|
+
widgets.push(result.report);
|
|
829
|
+
if (result.render) {
|
|
830
|
+
renderable.push({ index, input: result.render });
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
if (excess > 1) {
|
|
834
|
+
noteExcess(widgets[widgets.length - 1], excess - 1);
|
|
835
|
+
}
|
|
836
|
+
return { report: finalize('json', widgets, [], warnings, strict), renderable };
|
|
837
|
+
}
|
|
838
|
+
jsonFailure = parsed.message;
|
|
839
|
+
if (format === 'json') {
|
|
840
|
+
return {
|
|
841
|
+
report: finalize('json', [], [noWidgetError(text, jsonFailure, true)], [], strict),
|
|
842
|
+
renderable: none,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const outcome = checkFenceText(text, strict);
|
|
847
|
+
if (outcome.widgets.length === 0) {
|
|
848
|
+
return {
|
|
849
|
+
report: finalize(looksJson ? 'json' : 'fence', [], [noWidgetError(text, looksJson && !outcome.sawOpener ? jsonFailure : undefined)], [], strict),
|
|
850
|
+
renderable: none,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const warnings = outcome.residue
|
|
854
|
+
? [
|
|
855
|
+
{
|
|
856
|
+
code: 'input/fence-name-in-text',
|
|
857
|
+
message: 'The text outside the recognised blocks still names the widget fence. If one of those is a block that is indented, in a list or quote, or inside a code fence, it is not recognised and will show as plain text.',
|
|
858
|
+
fix: 'Write every real block at the first column, outside code fences. Ignore this if the text is only describing the syntax.',
|
|
859
|
+
},
|
|
860
|
+
]
|
|
861
|
+
: [];
|
|
862
|
+
return {
|
|
863
|
+
report: finalize('fence', collapseTooMany(outcome.widgets), [], warnings, strict),
|
|
864
|
+
renderable: none,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Serialise the JSON widgets that passed the schema to correctly delimited fence
|
|
869
|
+
* blocks, and prove each one: the block is parsed back with the same parser the
|
|
870
|
+
* ingest uses and must yield the same title, sections and fallback. The colon
|
|
871
|
+
* count is chosen by `renderUiWidgetFence`, so a widget whose code contains a line
|
|
872
|
+
* of colons still gets a closing line it cannot end early.
|
|
873
|
+
*/
|
|
874
|
+
function renderWidgetFences(result) {
|
|
875
|
+
const fences = [];
|
|
876
|
+
const errors = [];
|
|
877
|
+
if (result.report.format !== 'json') {
|
|
878
|
+
errors.push({
|
|
879
|
+
code: 'render/needs-json',
|
|
880
|
+
message: 'There is nothing to render: the input is not a JSON widget (it is already text).',
|
|
881
|
+
fix: 'Pass a widget payload or the ui_widget_render arguments as JSON.',
|
|
882
|
+
});
|
|
883
|
+
return { fences, errors };
|
|
884
|
+
}
|
|
885
|
+
for (const { index, input } of result.renderable) {
|
|
886
|
+
const label = `widget ${index + 1}`;
|
|
887
|
+
let block;
|
|
888
|
+
try {
|
|
889
|
+
block = (0, data_provider_1.renderUiWidgetFence)(input);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
errors.push({
|
|
893
|
+
code: 'render/unrepresentable',
|
|
894
|
+
message: `${label}: ${errorMessage(err)}`,
|
|
895
|
+
fix: 'Reword or indent the offending line of code.',
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const back = (0, data_provider_1.parseUiWidgetFences)(block);
|
|
900
|
+
const widget = back.widgets[0];
|
|
901
|
+
const css = (input.css ?? '').replace(/\r\n?/g, '\n');
|
|
902
|
+
const html = (input.html ?? '').replace(/\r\n?/g, '\n');
|
|
903
|
+
const js = (input.js ?? '').replace(/\r\n?/g, '\n');
|
|
904
|
+
const matches = back.errors.length === 0 &&
|
|
905
|
+
back.widgets.length === 1 &&
|
|
906
|
+
widget.title === input.title &&
|
|
907
|
+
widget.css === css &&
|
|
908
|
+
widget.html === html &&
|
|
909
|
+
widget.js === js &&
|
|
910
|
+
widget.fallbackText === (input.fallbackText ?? input.title);
|
|
911
|
+
if (!matches) {
|
|
912
|
+
errors.push({
|
|
913
|
+
code: 'render/roundtrip',
|
|
914
|
+
message: `${label}: the rendered block does not parse back to the same widget${back.errors[0]?.message ? ` (${back.errors[0].message})` : ''}.`,
|
|
915
|
+
fix: 'Check the widget with `skrr widgets check` on the JSON first and fix what it reports.',
|
|
916
|
+
});
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
fences.push(block);
|
|
920
|
+
}
|
|
921
|
+
if (fences.length === 0 && errors.length === 0 && result.report.errors.length === 0) {
|
|
922
|
+
errors.push({
|
|
923
|
+
code: 'render/nothing',
|
|
924
|
+
message: 'No widget in the input passed the schema, so there is nothing to render.',
|
|
925
|
+
fix: 'Run the check on the JSON and fix what it reports.',
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return { fences, errors };
|
|
929
|
+
}
|
|
930
|
+
// ── human output ────────────────────────────────────────────────────────────
|
|
931
|
+
function usageLine(label, usage) {
|
|
932
|
+
const used = usage.atLeast ? `over ${fmt(usage.max)}` : fmt(usage.used);
|
|
933
|
+
const flag = usage.over ? ' OVER LIMIT' : '';
|
|
934
|
+
return ` ${label.padEnd(9)} ${used.padStart(9)} / ${fmt(usage.max).padEnd(9)} ${usage.atLeast ? '' : `${usage.percent}%`}${flag}`.trimEnd();
|
|
935
|
+
}
|
|
936
|
+
function issueLines(issue) {
|
|
937
|
+
const where = [issue.section, issue.line !== undefined ? `line ${issue.line}` : undefined]
|
|
938
|
+
.filter(Boolean)
|
|
939
|
+
.join(' ');
|
|
940
|
+
const tag = issue.severity === 'error' ? 'error' : 'warn ';
|
|
941
|
+
const lines = [
|
|
942
|
+
` ${tag} ${where ? `${where}: ` : ''}${issue.message}${issue.heuristic ? ' (heuristic)' : ''}`,
|
|
943
|
+
];
|
|
944
|
+
if (issue.fix) {
|
|
945
|
+
lines.push(` fix: ${issue.fix}`);
|
|
946
|
+
}
|
|
947
|
+
return lines;
|
|
948
|
+
}
|
|
949
|
+
function verdict(widget) {
|
|
950
|
+
if (!widget.accepted) {
|
|
951
|
+
return 'would be REFUSED: the message would show an error state instead of the widget';
|
|
952
|
+
}
|
|
953
|
+
const errors = widget.issues.filter((i) => i.severity === 'error');
|
|
954
|
+
if (errors.length > 0) {
|
|
955
|
+
return `would be accepted, but ${errors.length} finding${errors.length === 1 ? '' : 's'} predict${errors.length === 1 ? 's' : ''} a failure inside the frame`;
|
|
956
|
+
}
|
|
957
|
+
return 'would be accepted';
|
|
958
|
+
}
|
|
959
|
+
/** The report as lines of plain text, for a person or a model reading a terminal. */
|
|
960
|
+
function formatWidgetCheckReport(report) {
|
|
961
|
+
const out = [];
|
|
962
|
+
for (const widget of report.widgets) {
|
|
963
|
+
const origin = widget.line !== undefined ? `, line ${widget.line}` : '';
|
|
964
|
+
out.push(`Widget ${widget.id}${widget.title ? ` "${widget.title}"` : ''} (${widget.kind}${origin})`);
|
|
965
|
+
if (widget.measured) {
|
|
966
|
+
out.push(usageLine('css', widget.limits.css));
|
|
967
|
+
out.push(usageLine('html', widget.limits.html));
|
|
968
|
+
out.push(usageLine('js', widget.limits.js));
|
|
969
|
+
out.push(usageLine('total', widget.limits.total));
|
|
970
|
+
out.push(usageLine('title', widget.limits.title));
|
|
971
|
+
out.push(usageLine('fallback', widget.limits.fallback));
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
out.push(' size: not measured (the content was never read)');
|
|
975
|
+
}
|
|
976
|
+
for (const issue of widget.issues) {
|
|
977
|
+
out.push(...issueLines(issue));
|
|
978
|
+
}
|
|
979
|
+
out.push(` ${verdict(widget)}.`);
|
|
980
|
+
out.push('');
|
|
981
|
+
}
|
|
982
|
+
for (const error of report.errors) {
|
|
983
|
+
out.push(`error: ${error.message}`);
|
|
984
|
+
if (error.fix) {
|
|
985
|
+
out.push(` fix: ${error.fix}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (const warning of report.warnings) {
|
|
989
|
+
out.push(`warn: ${warning.message}`);
|
|
990
|
+
if (warning.fix) {
|
|
991
|
+
out.push(` fix: ${warning.fix}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const { widgets, accepted, ok, errors, warnings } = report.summary;
|
|
995
|
+
out.push(`${report.ok ? 'OK' : 'NOT OK'}: ${widgets} widget${widgets === 1 ? '' : 's'} checked, ${accepted} accepted, ${ok} clean; ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}.`);
|
|
996
|
+
out.push(report.heuristic);
|
|
997
|
+
return out;
|
|
998
|
+
}
|