ucode-agent 1.5.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/README.md +399 -327
- package/package.json +6 -1
- package/skills/ui-ux/SKILL.md +2 -2
- package/src/core/doctor.js +122 -0
- package/src/core/livelog.js +113 -0
- package/src/core/loop.js +2105 -1659
- package/src/core/provider.js +93 -10
- package/src/core/stuck.js +269 -0
- package/src/core/tests.js +86 -0
- package/src/tools/blocks.js +117 -0
- package/src/tools/browser.js +121 -59
- package/src/tools/cache.js +105 -0
- package/src/tools/deploy.js +283 -0
- package/src/tools/files.js +91 -8
- package/src/tools/index.js +634 -495
- package/src/tools/rename.js +157 -0
- package/src/tools/scaffold.js +85 -6
- package/src/tools/shell.js +799 -701
- package/src/tools/symbols.js +218 -0
- package/src/tools/types.js +179 -0
- package/src/ui/activity.js +203 -0
- package/src/ui/plain.js +22 -3
- package/src/ui/screen.js +65 -19
- package/src/ui/theme.js +5 -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/templates/next-shadcn/TEMPLATE.md +53 -9
- package/templates/next-shadcn/_package-lock.json +1335 -148
- package/templates/next-shadcn/components.json +1 -1
- package/templates/next-shadcn/next.config.ts +2 -1
- package/templates/next-shadcn/package.json +4 -2
- package/templates/next-shadcn/presets/citrus.json +77 -0
- package/templates/next-shadcn/presets/graphite.json +77 -0
- package/templates/next-shadcn/presets/grove.json +77 -0
- package/templates/next-shadcn/presets/ocean.json +78 -0
- package/templates/next-shadcn/presets/sunset.json +77 -0
- package/templates/next-shadcn/presets/violet.json +77 -0
- package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
- package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +34 -22
- package/templates/next-shadcn/src/components/ui/avatar.tsx +7 -4
- package/templates/next-shadcn/src/components/ui/badge.tsx +15 -18
- package/templates/next-shadcn/src/components/ui/button.tsx +12 -3
- package/templates/next-shadcn/src/components/ui/calendar.tsx +1 -0
- package/templates/next-shadcn/src/components/ui/checkbox.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
- package/templates/next-shadcn/src/components/ui/command.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/dialog.tsx +34 -26
- package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +115 -114
- package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/input-group.tsx +2 -4
- package/templates/next-shadcn/src/components/ui/input.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/label.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/popover.tsx +27 -28
- package/templates/next-shadcn/src/components/ui/progress.tsx +11 -63
- package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/scroll-area.tsx +6 -6
- package/templates/next-shadcn/src/components/ui/select.tsx +55 -64
- package/templates/next-shadcn/src/components/ui/separator.tsx +6 -3
- package/templates/next-shadcn/src/components/ui/sheet.tsx +35 -26
- package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
- package/templates/next-shadcn/src/components/ui/switch.tsx +3 -2
- package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
- package/templates/next-shadcn/src/components/ui/tabs.tsx +16 -8
- package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
- package/templates/next-shadcn/src/components/ui/tooltip.tsx +24 -33
- package/templates/next-shadcn/src/lib/utils.ts +6 -1
- package/ucode.js +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -45,11 +45,16 @@
|
|
|
45
45
|
"author": "om dixit",
|
|
46
46
|
"license": "ISC",
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@babel/parser": "^7.29.8",
|
|
48
49
|
"chalk": "^6.0.0",
|
|
49
50
|
"dotenv": "^17.4.2",
|
|
51
|
+
"jsonrepair": "^3.15.0",
|
|
50
52
|
"marked": "^15.0.12",
|
|
51
53
|
"marked-terminal": "^7.3.0",
|
|
52
54
|
"openai": "^7.4.0",
|
|
53
55
|
"playwright-core": "^1.63.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"typescript": "^5.9.3"
|
|
54
59
|
}
|
|
55
60
|
}
|
package/skills/ui-ux/SKILL.md
CHANGED
|
@@ -286,8 +286,8 @@ because it was first to hand means you were not deciding.
|
|
|
286
286
|
Run it (`npm run dev` starts in the background and returns the URL), then call
|
|
287
287
|
`look_at_app` with that URL. It opens the app in a real browser at 375px and
|
|
288
288
|
1440px, reports console errors, failed requests, overflow, broken images and
|
|
289
|
-
unlabeled controls, and returns a designer's review of the
|
|
290
|
-
it finds and look
|
|
289
|
+
unlabeled controls, and the first look returns a designer's review of the
|
|
290
|
+
screenshots. Fix what it finds in one pass and look once more. Then check:
|
|
291
291
|
|
|
292
292
|
1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
|
|
293
293
|
wrote no responsive rules at all, you have not done this.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.js — `ucode doctor` and /doctor: is everything ucode needs working?
|
|
3
|
+
*
|
|
4
|
+
* Every check runs at once and each has a short deadline, so the whole report
|
|
5
|
+
* takes a few seconds. A problem comes with the one thing that fixes it.
|
|
6
|
+
* Secret values are never printed — only whether they are there and accepted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { theme, dim, blue } from '../ui/theme.js';
|
|
14
|
+
import { VERSION } from './version.js';
|
|
15
|
+
import { DEFAULT_MODEL, ENV_FILE } from './provider.js';
|
|
16
|
+
import { newer } from './updater.js';
|
|
17
|
+
|
|
18
|
+
const DEADLINE = 6000;
|
|
19
|
+
const timed = (ms = DEADLINE) => AbortSignal.timeout(ms);
|
|
20
|
+
|
|
21
|
+
function version(cmd) {
|
|
22
|
+
const r = spawnSync(cmd, ['--version'], { encoding: 'utf8', shell: process.platform === 'win32', timeout: 5000 });
|
|
23
|
+
return r.status === 0 ? String(r.stdout).trim().split('\n')[0] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function checkKey() {
|
|
27
|
+
const key = process.env.UCODE_API_KEY || process.env.OPENROUTER_API_KEY;
|
|
28
|
+
if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add UCODE_API_KEY=... to ${ENV_FILE}` };
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ model: DEFAULT_MODEL, messages: [{ role: 'user', content: 'ok' }], max_tokens: 1 }),
|
|
34
|
+
signal: timed(),
|
|
35
|
+
});
|
|
36
|
+
if (r.status === 401 || r.status === 403) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
|
|
37
|
+
const body = await r.json().catch(() => ({}));
|
|
38
|
+
const meta = body?.error?.metadata?.headers ?? {};
|
|
39
|
+
const left = r.headers.get('x-ratelimit-remaining') ?? meta['X-RateLimit-Remaining'];
|
|
40
|
+
const reset = Number(r.headers.get('x-ratelimit-reset') ?? meta['X-RateLimit-Reset']);
|
|
41
|
+
if (r.status === 429 && /per[- ]day|daily/i.test(`${body?.error?.message} ${body?.error?.metadata?.limit_source}`)) {
|
|
42
|
+
const at = Number.isFinite(reset) ? new Date(reset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'tomorrow';
|
|
43
|
+
return { ok: false, name: 'Requests today', detail: 'daily free limit used up', fix: `It resets at ${at}; adding credit raises the limit` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, name: 'API key', detail: left != null ? `works · ${left} free requests left today` : 'works' };
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function checkVercel() {
|
|
52
|
+
const token = process.env.VERCEL_TOKEN;
|
|
53
|
+
if (!token) return { ok: null, name: 'Vercel', detail: 'no token — /deploy needs one', fix: `vercel.com/account/tokens → Create Token, then add VERCEL_TOKEN=... to ${ENV_FILE}` };
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch('https://api.vercel.com/v2/user', { headers: { Authorization: `Bearer ${token}` }, signal: timed() });
|
|
56
|
+
if (!r.ok) return { ok: false, name: 'Vercel', detail: 'token rejected', fix: 'Make a new token at vercel.com/account/tokens' };
|
|
57
|
+
const j = await r.json();
|
|
58
|
+
return { ok: true, name: 'Vercel', detail: `ready to deploy as ${j.user?.username ?? 'you'}` };
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, name: 'Vercel', detail: 'could not reach Vercel', fix: 'Check your internet connection' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkBrowser() {
|
|
65
|
+
try {
|
|
66
|
+
const { chromium } = await import('playwright-core');
|
|
67
|
+
for (const channel of ['msedge', 'chrome']) {
|
|
68
|
+
try {
|
|
69
|
+
const b = await chromium.launch({ channel, headless: true, timeout: 8000 });
|
|
70
|
+
await b.close();
|
|
71
|
+
return { ok: true, name: 'Browser', detail: `${channel === 'msedge' ? 'Edge' : 'Chrome'} — look_at_app can see your apps` };
|
|
72
|
+
} catch { /* try the next one */ }
|
|
73
|
+
}
|
|
74
|
+
} catch { /* playwright-core missing */ }
|
|
75
|
+
return { ok: false, name: 'Browser', detail: 'no Edge or Chrome found', fix: 'Install Google Chrome or Microsoft Edge' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function checkVersion() {
|
|
79
|
+
try {
|
|
80
|
+
const r = await fetch('https://registry.npmjs.org/ucode-agent/latest', { signal: timed() });
|
|
81
|
+
const latest = (await r.json()).version;
|
|
82
|
+
return newer(latest, VERSION)
|
|
83
|
+
? { ok: null, name: 'ucode', detail: `${VERSION} · ${latest} is out`, fix: 'It updates itself on the next start, or: npm install -g ucode-agent' }
|
|
84
|
+
: { ok: true, name: 'ucode', detail: `${VERSION} · up to date` };
|
|
85
|
+
} catch {
|
|
86
|
+
return { ok: true, name: 'ucode', detail: VERSION };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkTools() {
|
|
91
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
92
|
+
const out = [
|
|
93
|
+
major >= 22
|
|
94
|
+
? { ok: true, name: 'Node', detail: process.versions.node }
|
|
95
|
+
: { ok: false, name: 'Node', detail: process.versions.node, fix: 'Install Node 22 or newer from nodejs.org' },
|
|
96
|
+
];
|
|
97
|
+
const npm = version('npm');
|
|
98
|
+
out.push(npm ? { ok: true, name: 'npm', detail: npm } : { ok: false, name: 'npm', detail: 'not found', fix: 'Reinstall Node, which includes npm' });
|
|
99
|
+
const git = version('git');
|
|
100
|
+
out.push(git ? { ok: true, name: 'git', detail: git.replace(/^git version /, '') } : { ok: null, name: 'git', detail: 'not found', fix: 'Optional — install git to keep history of your apps' });
|
|
101
|
+
const cache = path.join(os.homedir(), '.ucode', 'cache');
|
|
102
|
+
if (existsSync(cache)) out.push({ ok: true, name: 'Starter cache', detail: `${readdirSync(cache).length} ready` });
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run every check; returns the lines to print. */
|
|
107
|
+
export async function runDoctor() {
|
|
108
|
+
const results = [
|
|
109
|
+
...checkTools(),
|
|
110
|
+
...(await Promise.all([checkKey(), checkVercel(), checkBrowser(), checkVersion()])),
|
|
111
|
+
];
|
|
112
|
+
const width = Math.max(...results.map((r) => r.name.length));
|
|
113
|
+
const lines = ['', ` ${blue('ucode doctor')}`];
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
const mark = r.ok === true ? theme.ok('✓') : r.ok === false ? theme.error('✗') : theme.warn('!');
|
|
116
|
+
lines.push(` ${mark} ${r.name.padEnd(width)} ${r.detail}`);
|
|
117
|
+
if (r.fix && r.ok !== true) lines.push(` ${' '.repeat(width)} ${dim(`→ ${r.fix}`)}`);
|
|
118
|
+
}
|
|
119
|
+
const bad = results.filter((r) => r.ok === false).length;
|
|
120
|
+
lines.push('', bad ? ` ${bad} thing${bad === 1 ? '' : 's'} to fix` : ` ${theme.ok('All good')}`, '');
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
@@ -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
|
+
}
|