ucode-agent 1.2.0 → 1.3.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 +42 -8
- package/package.json +1 -1
- package/src/core/context.js +151 -0
- package/src/core/loop.js +376 -25
- package/src/core/provider.js +18 -3
- package/src/tools/files.js +173 -16
- package/src/tools/index.js +445 -394
- package/src/tools/shell.js +141 -0
- package/src/ui/plain.js +330 -325
- package/src/ui/screen.js +8 -2
- package/src/ui/theme.js +18 -0
package/README.md
CHANGED
|
@@ -103,15 +103,44 @@ long think are for. When the wait stops being worth it, switch.
|
|
|
103
103
|
|
|
104
104
|
## What it does
|
|
105
105
|
|
|
106
|
-
**
|
|
107
|
-
`
|
|
108
|
-
`web_search`. Read-only calls run in parallel
|
|
109
|
-
|
|
106
|
+
**Thirteen tools.** `read_file`, `read_files`, `write_file`, `batch_write`,
|
|
107
|
+
`edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`, `grep`,
|
|
108
|
+
`run_command`, `run_commands`, `web_search`. Read-only calls run in parallel,
|
|
109
|
+
and start the moment the model finishes writing them — while the rest of its
|
|
110
|
+
reply is still arriving. Anything that writes runs on its own, in order.
|
|
111
|
+
|
|
112
|
+
**Parallel workers.** When a build splits into parts that touch different files
|
|
113
|
+
— the API route, the upload component, the results view — the model hands them
|
|
114
|
+
to up to three workers that build at the same time, each line in the transcript
|
|
115
|
+
tagged with the worker's name. File writes take turns so two never collide.
|
|
116
|
+
|
|
117
|
+
**Installs that start early.** The moment a `package.json` with dependencies is
|
|
118
|
+
written, its install starts in the background while the rest of the app is
|
|
119
|
+
still being written. An install the model asks for later waits for that one
|
|
120
|
+
instead of running twice, and anything run in that folder waits for it too.
|
|
121
|
+
|
|
122
|
+
**Errors fixed before you see them.** When the model says it is done, ucode
|
|
123
|
+
type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
|
|
124
|
+
a syntax check for JavaScript and Python — and hands any errors back to fix,
|
|
125
|
+
up to three rounds.
|
|
126
|
+
|
|
127
|
+
**A plan you can see.** For longer jobs the model keeps a short checklist, shown
|
|
128
|
+
as one line: `plan 2/5 ✓ Scaffold · ✓ Upload · ▸ Score dial · ○ Findings · ○ Polish`.
|
|
129
|
+
|
|
130
|
+
**It knows the project before it asks.** Each turn starts with a map of every
|
|
131
|
+
file and the names each code file exports, so the model goes straight to the
|
|
132
|
+
right file instead of searching for it.
|
|
133
|
+
|
|
134
|
+
**Project memory.** `UCODE.md` in a project — and `~/.ucode/UCODE.md` for how you
|
|
135
|
+
like to work everywhere — is read at the start of every turn. `/remember <note>`
|
|
136
|
+
adds a line to it.
|
|
110
137
|
|
|
111
138
|
**Edits that never guess.** `edit_file` matches exactly once or it fails, and
|
|
112
|
-
when it fails it says *why
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
when it fails it says *why*. It tolerates what does not matter — tabs against
|
|
140
|
+
spaces, a different indent depth, Windows line endings — and re-indents the
|
|
141
|
+
replacement to fit the file, but a match found twice is still refused.
|
|
142
|
+
`edit_files` changes several files in one call, and writes none of them if any
|
|
143
|
+
edit fails.
|
|
115
144
|
|
|
116
145
|
**Diffs with real line numbers.** Removed lines are numbered where they were,
|
|
117
146
|
added lines where they now are. Numbers you can jump to, not decoration.
|
|
@@ -179,6 +208,7 @@ Everything after the frontmatter is the instruction.
|
|
|
179
208
|
| `/model` | show the models and switch — `/models` does the same |
|
|
180
209
|
| `/resume` | pick up an earlier conversation — `/session`, `/sessions` too |
|
|
181
210
|
| `/new` | save this one and start fresh |
|
|
211
|
+
| `/remember <note>` | add a standing note to this project's `UCODE.md` |
|
|
182
212
|
| `/skills` | what it knows how to do, and what is loaded |
|
|
183
213
|
| `/search <query>` | look something up on the web |
|
|
184
214
|
| `/copy` | last reply to the clipboard |
|
|
@@ -209,8 +239,11 @@ ucode [options]
|
|
|
209
239
|
| `~/.ucode/.env` | `UCODE_API_KEY`, and `TAVILY_API_KEY` for web search |
|
|
210
240
|
| `~/.ucode/sessions/` | one JSON per conversation |
|
|
211
241
|
| `.ucode/skills/` | skills belonging to a project |
|
|
242
|
+
| `UCODE.md` | project memory, read every turn |
|
|
243
|
+
| `~/.ucode/UCODE.md` | your own standing instructions, for every project |
|
|
212
244
|
|
|
213
|
-
Environment overrides: `UCODE_MODEL`, `
|
|
245
|
+
Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
|
|
246
|
+
parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
|
|
214
247
|
`UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
|
|
215
248
|
`UCODE_BASE_URL`.
|
|
216
249
|
|
|
@@ -226,6 +259,7 @@ src/core/provider.js the only file that knows which provider answers
|
|
|
226
259
|
src/core/history.js sessions on disk
|
|
227
260
|
src/core/window.js folding a long conversation to fit
|
|
228
261
|
src/core/skills.js loading skills, and deciding which load themselves
|
|
262
|
+
src/core/context.js the project map and project memory
|
|
229
263
|
src/core/failure.js one error shape: what, why, what next
|
|
230
264
|
src/tools/ the eleven tools, plus their shared plumbing
|
|
231
265
|
src/ui/screen.js the full-screen interface
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.js — what the model knows about the project before it asks.
|
|
3
|
+
*
|
|
4
|
+
* Two things go into the system prompt at the start of every turn:
|
|
5
|
+
*
|
|
6
|
+
* the project map every file, and the names each code file exports, so the
|
|
7
|
+
* model can go straight to the right file instead of
|
|
8
|
+
* spending round trips on list_dir and grep to find it
|
|
9
|
+
*
|
|
10
|
+
* project memory UCODE.md — the stack, the commands, the conventions, the
|
|
11
|
+
* preferences — written once, remembered every session
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { walk } from '../tools/shared.js';
|
|
18
|
+
|
|
19
|
+
export const MEMORY_FILE = 'UCODE.md';
|
|
20
|
+
export const GLOBAL_MEMORY = path.join(os.homedir(), '.ucode', MEMORY_FILE);
|
|
21
|
+
|
|
22
|
+
const MAP_FILES = 400;
|
|
23
|
+
const MAP_CHARS = 8_000;
|
|
24
|
+
const MEMORY_CHARS = 6_000;
|
|
25
|
+
const SYMBOL_BYTES = 120_000;
|
|
26
|
+
const SYMBOLS_PER_FILE = 8;
|
|
27
|
+
|
|
28
|
+
const CODE = /\.(?:[cm]?[jt]sx?|py|go|rs|vue|svelte)$/i;
|
|
29
|
+
|
|
30
|
+
// Files the map never needs to name.
|
|
31
|
+
const NOISE = /(^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|.*\.map|.*\.min\.[jc]ss?|\.DS_Store|next-env\.d\.ts)$/i;
|
|
32
|
+
|
|
33
|
+
/** Parsed symbols, kept per file until the file's mtime changes. */
|
|
34
|
+
const symbolCache = new Map();
|
|
35
|
+
|
|
36
|
+
function symbolsIn(file, text) {
|
|
37
|
+
const found = [];
|
|
38
|
+
const add = (name) => { if (name && !found.includes(name)) found.push(name); };
|
|
39
|
+
|
|
40
|
+
if (/\.py$/i.test(file)) {
|
|
41
|
+
for (const m of text.matchAll(/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/gm)) add(m[1]);
|
|
42
|
+
} else if (/\.go$/i.test(file)) {
|
|
43
|
+
for (const m of text.matchAll(/^func\s+(?:\([^)]*\)\s*)?([A-Z]\w*)/gm)) add(m[1]);
|
|
44
|
+
for (const m of text.matchAll(/^type\s+([A-Z]\w*)/gm)) add(m[1]);
|
|
45
|
+
} else if (/\.rs$/i.test(file)) {
|
|
46
|
+
for (const m of text.matchAll(/^pub\s+(?:async\s+)?(?:fn|struct|enum|trait)\s+(\w+)/gm)) add(m[1]);
|
|
47
|
+
} else {
|
|
48
|
+
for (const m of text.matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) add(m[1]);
|
|
49
|
+
if (/export\s+default\s+(?:async\s+)?function\s*\(/.test(text)) add('default');
|
|
50
|
+
for (const m of text.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
51
|
+
for (const part of m[1].split(',')) add(part.trim().split(/\s+as\s+/).pop());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return found.slice(0, SYMBOLS_PER_FILE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function symbolsFor(root, rel) {
|
|
59
|
+
const abs = path.join(root, rel);
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(abs);
|
|
62
|
+
if (stat.size > SYMBOL_BYTES) return [];
|
|
63
|
+
const cached = symbolCache.get(abs);
|
|
64
|
+
if (cached && cached.mtime === stat.mtimeMs) return cached.symbols;
|
|
65
|
+
const symbols = symbolsIn(rel, await fs.readFile(abs, 'utf8'));
|
|
66
|
+
symbolCache.set(abs, { mtime: stat.mtimeMs, symbols });
|
|
67
|
+
return symbols;
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A compact outline of the project: directories, their files, and what each
|
|
75
|
+
* code file exports. Bounded, so a large repository costs a fixed amount of
|
|
76
|
+
* context rather than all of it.
|
|
77
|
+
*/
|
|
78
|
+
export async function projectMap(root) {
|
|
79
|
+
const all = (await walk(root, { limit: MAP_FILES * 3 })).filter((f) => !NOISE.test(f));
|
|
80
|
+
if (all.length === 0) return '(the folder is empty — this is a new project)';
|
|
81
|
+
|
|
82
|
+
const files = all.slice(0, MAP_FILES).sort();
|
|
83
|
+
const symbols = await Promise.all(
|
|
84
|
+
files.map((f) => (CODE.test(f) ? symbolsFor(root, f) : Promise.resolve([])))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const byDir = new Map();
|
|
88
|
+
files.forEach((f, i) => {
|
|
89
|
+
const dir = path.posix.dirname(f);
|
|
90
|
+
const name = path.posix.basename(f);
|
|
91
|
+
const line = symbols[i].length ? `${name} · ${symbols[i].join(', ')}` : name;
|
|
92
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
93
|
+
byDir.get(dir).push(line);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const out = [];
|
|
97
|
+
let size = 0;
|
|
98
|
+
let shown = 0;
|
|
99
|
+
for (const [dir, entries] of byDir) {
|
|
100
|
+
const block = [dir === '.' ? './' : `${dir}/`, ...entries.map((e) => ` ${e}`)].join('\n');
|
|
101
|
+
if (size + block.length > MAP_CHARS) {
|
|
102
|
+
out.push(`… ${files.length - shown} more files not shown`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
out.push(block);
|
|
106
|
+
size += block.length;
|
|
107
|
+
shown += entries.length;
|
|
108
|
+
}
|
|
109
|
+
if (all.length > MAP_FILES) out.push(`… the project has ${all.length}+ files; the rest are not listed`);
|
|
110
|
+
|
|
111
|
+
return out.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readCapped(file) {
|
|
115
|
+
try {
|
|
116
|
+
const text = (await fs.readFile(file, 'utf8')).trim();
|
|
117
|
+
if (!text) return '';
|
|
118
|
+
return text.length > MEMORY_CHARS ? `${text.slice(0, MEMORY_CHARS)}\n… (cut)` : text;
|
|
119
|
+
} catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Standing instructions: ~/.ucode/UCODE.md for how you like to work anywhere,
|
|
126
|
+
* then <project>/UCODE.md for this project. The project file comes second so
|
|
127
|
+
* it wins where the two disagree.
|
|
128
|
+
*/
|
|
129
|
+
export async function loadMemory(root) {
|
|
130
|
+
const personal = await readCapped(GLOBAL_MEMORY);
|
|
131
|
+
const project = await readCapped(path.join(root, MEMORY_FILE));
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (personal) parts.push(`From ~/.ucode/${MEMORY_FILE} (applies everywhere):\n${personal}`);
|
|
134
|
+
if (project) parts.push(`From ./${MEMORY_FILE} (this project):\n${project}`);
|
|
135
|
+
return parts.join('\n\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Append one note to this project's UCODE.md, creating it if needed. */
|
|
139
|
+
export async function remember(root, note) {
|
|
140
|
+
const file = path.join(root, MEMORY_FILE);
|
|
141
|
+
let existing = '';
|
|
142
|
+
try {
|
|
143
|
+
existing = await fs.readFile(file, 'utf8');
|
|
144
|
+
} catch {
|
|
145
|
+
existing = `# Project memory\n\nucode reads this at the start of every session in this folder.\n\n`;
|
|
146
|
+
}
|
|
147
|
+
const line = `- ${String(note).trim().replace(/\s+/g, ' ')}\n`;
|
|
148
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
149
|
+
await fs.writeFile(file, existing + sep + line, 'utf8');
|
|
150
|
+
return file;
|
|
151
|
+
}
|