ucode-agent 1.19.1 → 1.20.1
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/package.json +1 -1
- package/src/core/htmlcheck.js +59 -0
- package/src/core/loop.js +19 -2
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* htmlcheck.js — the JavaScript inside an HTML file is still JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* A single-file app keeps everything in one <script>, and nothing was looking
|
|
5
|
+
* at it: the checks run `node --check` on .js files and tsc on TypeScript
|
|
6
|
+
* projects, so an index.html whose script does not parse passed every one of
|
|
7
|
+
* them. The page renders, the CSS is right, and not one button works.
|
|
8
|
+
*
|
|
9
|
+
* The failure that prompted this: a model writing an HTML-escaping map inside
|
|
10
|
+
* an HTML file produced `"'":'''` — three quotes, a syntax error, the whole
|
|
11
|
+
* script dead. Everything looked finished.
|
|
12
|
+
*
|
|
13
|
+
* Parsing is Babel's, which ucode already carries. Only syntax is checked:
|
|
14
|
+
* an undefined variable is a runtime problem and this is not the place for it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parse } from '@babel/parser';
|
|
18
|
+
|
|
19
|
+
/** Inline scripts worth parsing: not src=, not JSON, not a template. */
|
|
20
|
+
const SCRIPTS = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
|
|
21
|
+
|
|
22
|
+
const runnable = (attrs) => {
|
|
23
|
+
if (/\bsrc\s*=/i.test(attrs)) return false; // a separate file, checked on its own
|
|
24
|
+
const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
|
|
25
|
+
if (!type) return true;
|
|
26
|
+
return type === 'module' || type === 'text/javascript' || type === 'application/javascript';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Syntax errors in a file's inline scripts, with lines counted in the HTML
|
|
31
|
+
* rather than in the extracted fragment — a line number that does not match
|
|
32
|
+
* the file is worse than none.
|
|
33
|
+
*/
|
|
34
|
+
export function checkHtml(text) {
|
|
35
|
+
const html = String(text ?? '');
|
|
36
|
+
const problems = [];
|
|
37
|
+
|
|
38
|
+
for (const match of html.matchAll(SCRIPTS)) {
|
|
39
|
+
const [whole, attrs, body] = match;
|
|
40
|
+
if (!runnable(attrs) || !body.trim()) continue;
|
|
41
|
+
|
|
42
|
+
const before = html.slice(0, match.index + whole.indexOf(body));
|
|
43
|
+
const offset = before.split('\n').length - 1;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
parse(body, {
|
|
47
|
+
sourceType: 'module', // accepts a plain script too
|
|
48
|
+
allowReturnOutsideFunction: true,
|
|
49
|
+
errorRecovery: false,
|
|
50
|
+
plugins: ['topLevelAwait'],
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const line = (err.loc?.line ?? 1) + offset;
|
|
54
|
+
problems.push({ line, message: String(err.message ?? err).replace(/\s*\(\d+:\d+\)$/, '') });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return problems;
|
|
59
|
+
}
|
package/src/core/loop.js
CHANGED
|
@@ -17,6 +17,7 @@ import { appendFileSync } from 'node:fs';
|
|
|
17
17
|
import { readFile, access, mkdir } from 'node:fs/promises';
|
|
18
18
|
import { testRunnerFor, relatedCommand, summariseFailures } from './tests.js';
|
|
19
19
|
import { LogWatch } from './livelog.js';
|
|
20
|
+
import { checkHtml } from './htmlcheck.js';
|
|
20
21
|
import { runningServers } from '../tools/shell.js';
|
|
21
22
|
import { spawn } from 'node:child_process';
|
|
22
23
|
|
|
@@ -100,7 +101,7 @@ const SILENT = new Set(['update_plan']);
|
|
|
100
101
|
const MAX_FIX_ROUNDS = 3;
|
|
101
102
|
|
|
102
103
|
/** Files worth checking after they change. */
|
|
103
|
-
const CHECKABLE = /\.(?:[cm]?[jt]sx?|py)$/i;
|
|
104
|
+
const CHECKABLE = /\.(?:[cm]?[jt]sx?|py|html?)$/i;
|
|
104
105
|
|
|
105
106
|
/** Where TypeScript keeps what it learned, so the next check is a quick one. */
|
|
106
107
|
export const TSBUILDINFO = 'node_modules/.cache/ucode/types.tsbuildinfo';
|
|
@@ -468,6 +469,11 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
468
469
|
' - One line when you move between the big pieces of work: "The layout is done,',
|
|
469
470
|
' now the animations."',
|
|
470
471
|
' - One line at the end saying what it does and how to try it.',
|
|
472
|
+
'That closing line is ONE OR TWO SENTENCES. Never a checklist, never a feature',
|
|
473
|
+
'list, never ticks or bullets walking through the request item by item. "Tide is',
|
|
474
|
+
'built - open tide/index.html, or serve the folder and visit it." Anything longer',
|
|
475
|
+
'is a status report nobody asked for, and it is the last thing on screen, so it',
|
|
476
|
+
'is what the whole session looks like.',
|
|
471
477
|
'That is all. A line before every tool call is not narration, it is noise: the',
|
|
472
478
|
'steps already show on screen, and repeating them in words buries the few',
|
|
473
479
|
'sentences worth reading.',
|
|
@@ -1656,11 +1662,23 @@ export class Agent {
|
|
|
1656
1662
|
const root = path.resolve(this.cwd);
|
|
1657
1663
|
const tsRoots = new Set();
|
|
1658
1664
|
const singles = [];
|
|
1665
|
+
const problems = [];
|
|
1659
1666
|
|
|
1660
1667
|
for (const rel of changed) {
|
|
1661
1668
|
const abs = path.resolve(root, rel);
|
|
1662
1669
|
if (!(await exists(abs))) continue;
|
|
1663
1670
|
if (/\.py$/i.test(rel)) { singles.push({ abs, rel, command: `python -m py_compile "${abs}"` }); continue; }
|
|
1671
|
+
// A single-file app keeps all its logic in an inline <script>, which no
|
|
1672
|
+
// other check here ever looks at.
|
|
1673
|
+
if (/\.html?$/i.test(rel)) {
|
|
1674
|
+
const text = await readFile(abs, 'utf8').catch(() => null);
|
|
1675
|
+
const bad = text === null ? [] : checkHtml(text);
|
|
1676
|
+
if (bad.length) {
|
|
1677
|
+
problems.push(`${rel} — the script in this page does not parse, so none of it runs:\n` +
|
|
1678
|
+
bad.map((b) => ` line ${b.line}: ${b.message}`).join('\n'));
|
|
1679
|
+
}
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1664
1682
|
let dir = path.dirname(abs);
|
|
1665
1683
|
let owner = null;
|
|
1666
1684
|
while (dir.startsWith(root)) {
|
|
@@ -1673,7 +1691,6 @@ export class Agent {
|
|
|
1673
1691
|
else if (/\.[cm]?js$/i.test(rel)) singles.push({ abs, rel, command: `node --check "${abs}"` });
|
|
1674
1692
|
}
|
|
1675
1693
|
|
|
1676
|
-
const problems = [];
|
|
1677
1694
|
const check = async (label, command, cwd) => {
|
|
1678
1695
|
this.ui.toolCall(label);
|
|
1679
1696
|
this.ui.startSpinner(label);
|