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.
Files changed (71) hide show
  1. package/README.md +399 -327
  2. package/package.json +6 -1
  3. package/skills/ui-ux/SKILL.md +2 -2
  4. package/src/core/doctor.js +122 -0
  5. package/src/core/livelog.js +113 -0
  6. package/src/core/loop.js +2105 -1659
  7. package/src/core/provider.js +93 -10
  8. package/src/core/stuck.js +269 -0
  9. package/src/core/tests.js +86 -0
  10. package/src/tools/blocks.js +117 -0
  11. package/src/tools/browser.js +121 -59
  12. package/src/tools/cache.js +105 -0
  13. package/src/tools/deploy.js +283 -0
  14. package/src/tools/files.js +91 -8
  15. package/src/tools/index.js +634 -495
  16. package/src/tools/rename.js +157 -0
  17. package/src/tools/scaffold.js +85 -6
  18. package/src/tools/shell.js +799 -701
  19. package/src/tools/symbols.js +218 -0
  20. package/src/tools/types.js +179 -0
  21. package/src/ui/activity.js +203 -0
  22. package/src/ui/plain.js +22 -3
  23. package/src/ui/screen.js +65 -19
  24. package/src/ui/theme.js +5 -1
  25. package/templates/blocks/app-shell.tsx +81 -0
  26. package/templates/blocks/data-table.tsx +117 -0
  27. package/templates/blocks/empty-state.tsx +41 -0
  28. package/templates/blocks/page-header.tsx +27 -0
  29. package/templates/blocks/stat-cards.tsx +46 -0
  30. package/templates/next-shadcn/TEMPLATE.md +53 -9
  31. package/templates/next-shadcn/_package-lock.json +1335 -148
  32. package/templates/next-shadcn/components.json +1 -1
  33. package/templates/next-shadcn/next.config.ts +2 -1
  34. package/templates/next-shadcn/package.json +4 -2
  35. package/templates/next-shadcn/presets/citrus.json +77 -0
  36. package/templates/next-shadcn/presets/graphite.json +77 -0
  37. package/templates/next-shadcn/presets/grove.json +77 -0
  38. package/templates/next-shadcn/presets/ocean.json +78 -0
  39. package/templates/next-shadcn/presets/sunset.json +77 -0
  40. package/templates/next-shadcn/presets/violet.json +77 -0
  41. package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
  42. package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +34 -22
  43. package/templates/next-shadcn/src/components/ui/avatar.tsx +7 -4
  44. package/templates/next-shadcn/src/components/ui/badge.tsx +15 -18
  45. package/templates/next-shadcn/src/components/ui/button.tsx +12 -3
  46. package/templates/next-shadcn/src/components/ui/calendar.tsx +1 -0
  47. package/templates/next-shadcn/src/components/ui/checkbox.tsx +6 -2
  48. package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
  49. package/templates/next-shadcn/src/components/ui/command.tsx +1 -2
  50. package/templates/next-shadcn/src/components/ui/dialog.tsx +34 -26
  51. package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +115 -114
  52. package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
  53. package/templates/next-shadcn/src/components/ui/input-group.tsx +2 -4
  54. package/templates/next-shadcn/src/components/ui/input.tsx +1 -2
  55. package/templates/next-shadcn/src/components/ui/label.tsx +6 -2
  56. package/templates/next-shadcn/src/components/ui/popover.tsx +27 -28
  57. package/templates/next-shadcn/src/components/ui/progress.tsx +11 -63
  58. package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
  59. package/templates/next-shadcn/src/components/ui/scroll-area.tsx +6 -6
  60. package/templates/next-shadcn/src/components/ui/select.tsx +55 -64
  61. package/templates/next-shadcn/src/components/ui/separator.tsx +6 -3
  62. package/templates/next-shadcn/src/components/ui/sheet.tsx +35 -26
  63. package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
  64. package/templates/next-shadcn/src/components/ui/switch.tsx +3 -2
  65. package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
  66. package/templates/next-shadcn/src/components/ui/tabs.tsx +16 -8
  67. package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
  68. package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
  69. package/templates/next-shadcn/src/components/ui/tooltip.tsx +24 -33
  70. package/templates/next-shadcn/src/lib/utils.ts +6 -1
  71. package/ucode.js +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.5.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",
@@ -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
  }
@@ -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 screenshots. Fix what
290
- it finds and look again. Then check:
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
+ }