ucode-agent 1.61.0 → 1.61.1

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.61.0",
3
+ "version": "1.61.1",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
@@ -12,7 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { theme, dim, blue } from '../ui/theme.js';
14
14
  import { VERSION } from './version.js';
15
- import { DEFAULT_MODEL, ENV_FILE, BASE_URL, providerKey } from './provider.js';
15
+ import { ENV_FILE, BASE_URL, providerKey } from './provider.js';
16
16
  import { newer } from './updater.js';
17
17
 
18
18
  const DEADLINE = 6000;
@@ -27,22 +27,11 @@ async function checkKey() {
27
27
  const key = providerKey();
28
28
  if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add GEMINI_API_KEY=... to ${ENV_FILE} (free at aistudio.google.com/apikey)` };
29
29
  try {
30
- const r = await fetch(`${BASE_URL}/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' };
30
+ // Listing models checks the key in under a second; a chat request can queue for half a minute.
31
+ const r = await fetch(`${BASE_URL}/models`, { headers: { Authorization: `Bearer ${key}` }, signal: timed() });
32
+ if ([400, 401, 403].includes(r.status)) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
33
+ if (!r.ok) return { ok: false, name: 'API key', detail: `could not check (HTTP ${r.status})`, fix: 'Try again in a minute' };
34
+ return { ok: true, name: 'API key', detail: 'works' };
46
35
  } catch (err) {
47
36
  return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
48
37
  }
package/src/core/loop.js CHANGED
@@ -1946,6 +1946,26 @@ export class Agent {
1946
1946
  .map((f) => ({ abs: path.resolve(this.cwd, f.path), size: f.content.length, path: f.path }));
1947
1947
  }
1948
1948
 
1949
+ /**
1950
+ * A model that has just made todo-app/ sometimes writes app.js beside it
1951
+ * instead of in it: the page never gets its script, and the hand-over, which
1952
+ * watches the app folder, never fires. A bare name the app already has, and
1953
+ * this folder does not, goes into the app.
1954
+ */
1955
+ intoApp(call) {
1956
+ const app = this.apps?.at(-1);
1957
+ if (!app) return;
1958
+ const fix = (p) => {
1959
+ if (typeof p !== 'string' || path.isAbsolute(p) || existsSync(path.resolve(this.cwd, p))) return p;
1960
+ const inside = path.join(app, p);
1961
+ if (path.relative(app, inside).startsWith('..') || !existsSync(inside)) return p;
1962
+ return path.relative(this.cwd, inside).split(path.sep).join('/');
1963
+ };
1964
+ const a = call.args ?? {};
1965
+ if (call.name === 'write_file') a.path = fix(a.path);
1966
+ else if (call.name === 'batch_write') a.files = normaliseFiles(a.files).map((f) => (f && typeof f === 'object' ? { ...f, path: fix(f.path) } : f));
1967
+ }
1968
+
1949
1969
  async dispatch(call, offered = this.offering) {
1950
1970
  // The model emitted arguments that were not valid JSON. Hand the parser's
1951
1971
  // own complaint straight back so it can correct itself next step.
@@ -2053,6 +2073,8 @@ export class Agent {
2053
2073
  }
2054
2074
  }
2055
2075
 
2076
+ if (call.name === 'write_file' || call.name === 'batch_write') this.intoApp(call);
2077
+
2056
2078
  if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
2057
2079
  if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
2058
2080
  if (call.name === 'delegate') return this.delegate(call.args?.tasks);