ucode-agent 1.6.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
@@ -53,5 +53,8 @@
53
53
  "marked-terminal": "^7.3.0",
54
54
  "openai": "^7.4.0",
55
55
  "playwright-core": "^1.63.0"
56
+ },
57
+ "devDependencies": {
58
+ "typescript": "^5.9.3"
56
59
  }
57
60
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * livelog.js — errors from the running app, without being asked.
3
+ *
4
+ * A dev server reports a broken import or a thrown render the moment it
5
+ * happens, into a log nobody is reading. The model finds out much later, from
6
+ * a build, or from the user saying the page is blank. This reads what the
7
+ * server has written since the last look and hands back anything that is
8
+ * actually an error.
9
+ *
10
+ * Only new bytes are read, so a server running for an hour costs one small
11
+ * read. Errors a dev server repeats on every request are reported once, not
12
+ * once per refresh.
13
+ */
14
+
15
+ import { promises as fs } from 'node:fs';
16
+
17
+ /** Lines that mean something is broken. */
18
+ const ERROR = /(?:^|\s)(?:⨯|✘|ERROR|Error:|TypeError:|ReferenceError:|SyntaxError:|RangeError:)|Failed to compile|Module not found|Cannot find module|Unhandled(?:Promise)?Rejection|ERR_[A-Z_]+|error TS\d+/;
19
+
20
+ /** Lines that look alarming but are not: warnings, notices, and the ready banner. */
21
+ const NOT_AN_ERROR = /\b(?:warn|warning|deprecat|notice|experimental|✓|ready in|compiled successfully|No errors? found)\b/i;
22
+
23
+ /** Whatever colour a terminal put on it is not part of the message. */
24
+ const stripAnsi = (s) => s.replace(/\[[0-9;]*[A-Za-z]/g, '');
25
+
26
+ /**
27
+ * The error blocks in a chunk of log. An error's first line is the message and
28
+ * the indented lines under it are its stack, which is where the file is named,
29
+ * so they come along.
30
+ */
31
+ export function errorsIn(text) {
32
+ const lines = stripAnsi(String(text ?? '')).split('\n');
33
+ const found = [];
34
+ for (let i = 0; i < lines.length; i++) {
35
+ const line = lines[i];
36
+ if (!ERROR.test(line) || NOT_AN_ERROR.test(line)) continue;
37
+ const block = [line.trimEnd()];
38
+ // Take the indented continuation, which holds the file and line number.
39
+ for (let j = i + 1; j < lines.length && block.length < 8; j++) {
40
+ if (!/^\s+\S/.test(lines[j])) break;
41
+ block.push(lines[j].trimEnd());
42
+ i = j;
43
+ }
44
+ found.push(block.join('\n').trim());
45
+ }
46
+ return found;
47
+ }
48
+
49
+ /**
50
+ * What is worth telling the model about, given what it has already been told.
51
+ * A dev server prints the same failure on every request; it is news once.
52
+ */
53
+ export function freshErrors(errors, alreadySeen) {
54
+ const out = [];
55
+ for (const e of errors) {
56
+ const key = e.split('\n')[0].replace(/\d+/g, '#').slice(0, 200);
57
+ if (alreadySeen.has(key)) continue;
58
+ alreadySeen.add(key);
59
+ out.push(e);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Watches each server log from wherever it was last read.
66
+ *
67
+ * A log that is deleted or replaced starts again from nothing rather than
68
+ * throwing; a server's log going away is not worth failing a turn over.
69
+ */
70
+ export class LogWatch {
71
+ constructor() {
72
+ this.at = new Map(); // log path -> bytes already read
73
+ this.seen = new Set(); // error signatures already reported
74
+ }
75
+
76
+ /** New error text across these logs, or null when everything is quiet. */
77
+ async since(servers) {
78
+ const blocks = [];
79
+ for (const server of servers) {
80
+ if (!server?.log) continue;
81
+ const from = this.at.get(server.log) ?? 0;
82
+ let text = '';
83
+ try {
84
+ const { size } = await fs.stat(server.log);
85
+ if (size < from) { this.at.set(server.log, 0); continue; } // truncated: start over
86
+ if (size === from) continue;
87
+ const handle = await fs.open(server.log, 'r');
88
+ try {
89
+ const length = Math.min(size - from, 200_000);
90
+ const buffer = Buffer.alloc(length);
91
+ await handle.read(buffer, 0, length, size - length);
92
+ text = buffer.toString('utf8');
93
+ } finally {
94
+ await handle.close();
95
+ }
96
+ this.at.set(server.log, size);
97
+ } catch {
98
+ continue; // the log went away; nothing to report
99
+ }
100
+
101
+ const fresh = freshErrors(errorsIn(text), this.seen);
102
+ if (fresh.length) blocks.push({ server, errors: fresh });
103
+ }
104
+
105
+ if (!blocks.length) return null;
106
+
107
+ return blocks
108
+ .map(({ server, errors }) =>
109
+ `The app running at ${server.url ?? server.command ?? 'the dev server'} reported this:\n` +
110
+ errors.slice(0, 5).join('\n\n'))
111
+ .join('\n\n');
112
+ }
113
+ }