ucode-agent 1.6.0 → 1.8.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/README.md +399 -379
- package/package.json +4 -1
- package/src/core/livelog.js +113 -0
- package/src/core/login.js +59 -0
- package/src/core/loop.js +2105 -1948
- package/src/core/tests.js +86 -0
- package/src/tools/blocks.js +117 -0
- package/src/tools/cache.js +105 -0
- package/src/tools/index.js +634 -529
- package/src/tools/rename.js +157 -0
- package/src/tools/scaffold.js +24 -5
- package/src/tools/shell.js +799 -789
- package/src/tools/symbols.js +218 -0
- package/src/tools/types.js +179 -0
- package/src/ui/screen.js +1252 -1253
- package/src/ui/theme.js +49 -1
- package/templates/blocks/app-shell.tsx +81 -0
- package/templates/blocks/data-table.tsx +117 -0
- package/templates/blocks/empty-state.tsx +41 -0
- package/templates/blocks/page-header.tsx +27 -0
- package/templates/blocks/stat-cards.tsx +46 -0
- package/ucode.js +132 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* login.js — putting the API key somewhere ucode will always find it.
|
|
3
|
+
*
|
|
4
|
+
* A key in a project's .env is a key you set up again in the next folder, and
|
|
5
|
+
* on the next machine. This writes it once to ~/.ucode/.env, which every
|
|
6
|
+
* project on the machine reads, so setting ucode up somewhere new — a borrowed
|
|
7
|
+
* laptop, a machine you are demonstrating on — is one command.
|
|
8
|
+
*
|
|
9
|
+
* The file is written with owner-only permissions, and the existing contents
|
|
10
|
+
* are kept: a key is replaced in place rather than by rewriting the file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { promises as fs } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { ENV_FILE } from './provider.js';
|
|
16
|
+
|
|
17
|
+
const NAME = 'OPENROUTER_API_KEY';
|
|
18
|
+
|
|
19
|
+
/** A plausible OpenRouter key, so a typo is caught here and not mid-answer. */
|
|
20
|
+
export function looksLikeKey(key) {
|
|
21
|
+
return typeof key === 'string' && /^sk-[A-Za-z0-9_-]{20,}$/.test(key.trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Put `key` in the machine-wide env file, keeping whatever else is in it. */
|
|
25
|
+
export function withKey(existing, key) {
|
|
26
|
+
const line = `${NAME}=${key}`;
|
|
27
|
+
const lines = String(existing ?? '').split('\n');
|
|
28
|
+
let replaced = false;
|
|
29
|
+
const out = lines.map((l) => {
|
|
30
|
+
if (new RegExp(`^\\s*(?:export\\s+)?${NAME}\\s*=`).test(l)) {
|
|
31
|
+
replaced = true;
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
34
|
+
return l;
|
|
35
|
+
});
|
|
36
|
+
if (!replaced) {
|
|
37
|
+
if (out.length && out[out.length - 1].trim() !== '') out.push('');
|
|
38
|
+
out.splice(out.length - 1, 0, line);
|
|
39
|
+
}
|
|
40
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function saveKey(key) {
|
|
44
|
+
const trimmed = String(key ?? '').trim();
|
|
45
|
+
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return ` Usage: ucode login <key>\n\n Get one free at https://openrouter.ai/keys\n It is saved to ${ENV_FILE} and used by every project on this machine.`;
|
|
48
|
+
}
|
|
49
|
+
if (!looksLikeKey(trimmed)) {
|
|
50
|
+
return ` That does not look like an OpenRouter key — they start with "sk-".\n Get one at https://openrouter.ai/keys`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const existing = await fs.readFile(ENV_FILE, 'utf8').catch(() => '');
|
|
54
|
+
await fs.mkdir(path.dirname(ENV_FILE), { recursive: true });
|
|
55
|
+
await fs.writeFile(ENV_FILE, withKey(existing, trimmed), { encoding: 'utf8', mode: 0o600 });
|
|
56
|
+
await fs.chmod(ENV_FILE, 0o600).catch(() => {});
|
|
57
|
+
|
|
58
|
+
return ` Key saved to ${ENV_FILE}\n Every project on this machine will use it. Run ucode anywhere to start.`;
|
|
59
|
+
}
|