ucode-agent 1.32.0 → 1.33.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.32.0",
3
+ "version": "1.33.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",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "start": "node ucode.js",
18
18
  "test": "node test/run.js",
19
- "prepublishOnly": "node test/run.js"
19
+ "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
@@ -26,9 +26,6 @@
26
26
  "url": "https://github.com/sppideey/ucode-agent/issues"
27
27
  },
28
28
  "homepage": "https://github.com/sppideey/ucode-agent#readme",
29
- "publishConfig": {
30
- "access": "public"
31
- },
32
29
  "engines": {
33
30
  "node": ">=22"
34
31
  },
@@ -56,5 +53,8 @@
56
53
  },
57
54
  "devDependencies": {
58
55
  "typescript": "^5.9.3"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
59
  }
60
60
  }
@@ -107,6 +107,18 @@ export const FALLBACKS = [
107
107
 
108
108
  /** The next model to try after `id`, skipping any already tried this round. */
109
109
  export function fallbackFor(id, tried = new Set()) {
110
+ // Off unless UCODE_FALLBACK=1. A build that starts on one model and finishes
111
+ // on another finishes to a different standard, and the swap lands exactly
112
+ // when the user is least placed to work out why the output changed —
113
+ // mid-build, behind a note that scrolls past. Which model to run is the one
114
+ // decision they made before starting; it is not one to take back for them.
115
+ //
116
+ // Every caller already reads "no fallback" as "wait, then try this one
117
+ // again": failover waits a minute and returns to the same model, the
118
+ // stuck-detector simply does not switch, and a worker retries its own. That
119
+ // is why this can be a single gate rather than four.
120
+ if (process.env.UCODE_FALLBACK !== '1') return null;
121
+
110
122
  const start = Math.max(0, FALLBACKS.indexOf(id));
111
123
  for (let i = 1; i <= FALLBACKS.length; i++) {
112
124
  const next = FALLBACKS[(start + i) % FALLBACKS.length];
@@ -85,7 +85,11 @@ export const tools = [
85
85
  'Start a new app AND write it, in one call. Pass "files" with the whole app and this ' +
86
86
  'is the only call the build needs: the starter lands, your files are written over it, ' +
87
87
  'and the result comes back with everything. Two starters. "plain-html" (the default): ' +
88
- 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
88
+ 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
89
+ 'and the stylesheet is a design system already: a palette, a spacing scale, radii, ' +
90
+ 'motion timings, focus rings and a breakpoint. Re-tint those tokens to suit the app ' +
91
+ 'and compose every rule from them. Replacing it with raw pixel values is what an ' +
92
+ 'undesigned page is made of. ' +
89
93
  'opens straight in a browser, and its three files come back inside this result so there ' +
90
94
  'is never a reason to read them. Use it for anything that is one page: a tasks app, a ' +
91
95
  'toy, a game, a visualisation, a calculator, a timer. "next-shadcn": Next.js 16, ' +
@@ -108,6 +108,46 @@ async function copyTree(from, to, fill) {
108
108
  * globals.css and layout.tsx. Apps stop looking like the same default blue.
109
109
  * Returns the preset used, or null when the starter has none.
110
110
  */
111
+ /**
112
+ * The file in each starter that carries the design, not just some of the rules.
113
+ *
114
+ * next-shadcn has globals.css, which applyDesign re-tints. plain-html has one
115
+ * stylesheet and it is the whole design system: the palette, a spacing scale,
116
+ * radii, motion timings, focus rings, a reduced-motion rule and a breakpoint.
117
+ */
118
+ const TOKEN_FILE = { 'plain-html': 'styles.css' };
119
+
120
+ /**
121
+ * Put the starter's token block back when the app wrote over it without one.
122
+ *
123
+ * The stylesheet in plain-html is a design, not a placeholder — but `files`
124
+ * lands straight on top of the starter, so a model passing its own styles.css
125
+ * replaces the scale, the palette and the timings with whatever it typed. What
126
+ * comes back is hand-rolled CSS with no system behind it, and an app built on
127
+ * raw pixel values has uneven spacing everywhere for the rest of its life.
128
+ *
129
+ * A replacement that declares its own custom properties is left alone: that is
130
+ * a model doing the job properly, and second-guessing it would be worse. Only
131
+ * a stylesheet with no :root variables at all gets the starter's block put
132
+ * back above it, where every rule underneath can reach it.
133
+ */
134
+ async function keepDesignTokens(appDir, template, written, starterCss) {
135
+ const rel = TOKEN_FILE[template];
136
+ if (!rel || !starterCss) return null;
137
+
138
+ const abs = path.join(appDir, rel);
139
+ if (!written.has(abs)) return null; // never overwritten
140
+
141
+ const now = await fs.readFile(abs, 'utf8').catch(() => null);
142
+ if (now === null || /:root\s*\{[^}]*--/.test(now)) return null;
143
+
144
+ const block = /:root\s*\{[\s\S]*?\n\}/.exec(starterCss);
145
+ if (!block) return null;
146
+
147
+ await fs.writeFile(abs, `${block[0]}\n\n${now}`, 'utf8');
148
+ return rel;
149
+ }
150
+
111
151
  export async function applyDesign(appDir, design) {
112
152
  const dir = path.join(appDir, 'presets');
113
153
  const names = (await fs.readdir(dir).catch(() => [])).filter((f) => f.endsWith('.json'));
@@ -242,7 +282,13 @@ export async function createApp({ folder, name, description, template = 'plain-h
242
282
  // Next.js serves static files from public/; a plain page has no such place
243
283
  // and an empty folder in a three-file app is clutter.
244
284
  if (template !== 'plain-html') await fs.mkdir(path.join(target.abs, 'public'), { recursive: true });
245
- const look = await applyDesign(target.abs, design);
285
+ const look = await applyDesign(target.abs, design);
286
+
287
+ // Read before the app's own files land on top of it, so the tokens can be
288
+ // put back if the replacement arrives without any.
289
+ const starterCss = TOKEN_FILE[template]
290
+ ? await fs.readFile(path.join(target.abs, TOKEN_FILE[template]), 'utf8').catch(() => null)
291
+ : null;
246
292
 
247
293
  // The starter has been installed on this machine before: hard-link that
248
294
  // tree in, which is seconds where npm is a minute. Otherwise install as
@@ -271,7 +317,8 @@ export async function createApp({ folder, name, description, template = 'plain-h
271
317
  // one, and round trips are nearly all of the time a build takes.
272
318
  const mine = given;
273
319
  const wrote = mine.length ? await batchWrite({ files: mine }) : null;
274
- const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
320
+ const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
321
+ const keptTokens = await keepDesignTokens(target.abs, template, written, starterCss);
275
322
 
276
323
  // The starter's own files, in full, so there is never a reason to read them
277
324
  // back — and only the ones this call did not already write over. A read is
@@ -287,7 +334,14 @@ export async function createApp({ folder, name, description, template = 'plain-h
287
334
  const out = result(
288
335
  `Created ${target.show} from the ${template} starter — ${copied.length} files, already known to build.\n` +
289
336
  (look ? `Design: the ${look.name} preset (${look.summary}), font ${look.fonts?.sans ?? 'Geist'}.\n` : '') +
290
- (wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
337
+ (wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
338
+ (keptTokens
339
+ ? `\nYour ${keptTokens} arrived with no :root block, so the starter's was kept above it — ` +
340
+ 'the palette, the spacing scale (--s1 to --s5), the radius and the motion timings. ' +
341
+ 'Build the rest of the stylesheet out of those variables: spacing that comes from a ' +
342
+ 'scale is the difference between a designed page and an arranged one. Re-tint the ' +
343
+ 'values to suit this app; do not go back to raw pixels.\n'
344
+ : '') +
291
345
  (linked
292
346
  ? `Its packages are already in place (${linked.toLocaleString()} files, linked from the starter cache) — ` +
293
347
  'nothing to install: build and run straight away.\n'