jeo-code 0.12.5 → 0.12.7

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/CHANGELOG.md CHANGED
@@ -9,6 +9,25 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.12.7] - 2026-10-18
13
+ _Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization._
14
+
15
+ ### Fixed
16
+ - **Screenshot vision feedback** — carry PNG attachments through tool execution, batches, and native Anthropic/OpenAI Responses replay instead of spilling base64 as text. Preserve CLI screenshot base64 stdout.
17
+ - **Desktop input** — use Quartz scroll events on macOS (cliclick `w:` waits rather than scrolling), release the mouse after dragging on macOS/Linux, normalize macOS key aliases/modifier combinations, and terminate Linux text options before user text.
18
+ - **Execution reliability** — drain subprocess pipes concurrently with exit, use collision-resistant screenshot paths, reject non-finite numeric inputs and out-of-range waits/scrolls, and explicitly reject unsupported platforms. Existing enablement/supervisor gates remain unchanged.
19
+
20
+ ### Documentation
21
+ - Added [desktop setup and troubleshooting](docs/computer-use.md), including platform requirements, OS permissions, coordinate caveats, and supported action semantics.
22
+
23
+ ## [0.12.6] - 2026-10-17
24
+ _Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls._
25
+
26
+ ### Fixed
27
+ - **OpenAI subscription startup behind a configured proxy** — account-confirmed Codex models keep their OAuth transport instead of sending a keyless request to the OpenAI-compatible endpoint. Cold calls confirm the subscription catalog first; explicit per-call URLs, configured proxy keys, and endpoint-discovered models retain their existing routes. Subscription requests use the direct Codex backend, not the configured proxy.
28
+ - **Provider-specific authentication recovery** — HTTP 401/403 errors name the appropriate login or API-key configuration, omit unrelated Antigravity advice for OpenAI, preserve exact status codes, and no longer treat a structured non-authentication failure as a credential rejection merely because its body mentions 401.
29
+ - **Account-bound subscription catalogs** — distinguish opaque bearer identities without exposing tokens, reject discovery results that finish after an account switch, share concurrent same-account catalog requests, and keep cancellation of one caller from interrupting other callers.
30
+
12
31
  ## [0.12.5] - 2026-10-17
13
32
  _Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing._
14
33
 
@@ -2579,7 +2598,8 @@ _Initial release._
2579
2598
  ### Added
2580
2599
  - Initial jeo-code agent and CLI.
2581
2600
 
2582
- [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.11.4...HEAD
2601
+ [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.6...HEAD
2602
+ [0.12.6]: https://github.com/akillness/jeo-code/releases/tag/v0.12.6
2583
2603
  [0.11.4]: https://github.com/akillness/jeo-code/releases/tag/v0.11.4
2584
2604
  [0.11.3]: https://github.com/akillness/jeo-code/releases/tag/v0.11.3
2585
2605
  [0.11.2]: https://github.com/akillness/jeo-code/releases/tag/v0.11.2
package/README.ja.md CHANGED
@@ -416,10 +416,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
416
416
 
417
417
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
418
418
  - **[Unreleased]**
419
+ - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
420
+ - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
419
421
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
420
422
  - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
421
- - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
422
- - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
423
423
 
424
424
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
425
425
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -417,10 +417,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
417
417
 
418
418
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
419
419
  - **[Unreleased]**
420
+ - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
421
+ - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
420
422
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
421
423
  - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
422
- - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
423
- - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
424
424
 
425
425
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
426
426
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -409,10 +409,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
409
409
 
410
410
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
411
411
  - **[Unreleased]**
412
+ - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
413
+ - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
412
414
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
413
415
  - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
414
- - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
415
- - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
416
416
 
417
417
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
418
418
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -414,10 +414,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
414
414
 
415
415
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
416
416
  - **[Unreleased]**
417
+ - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
418
+ - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
417
419
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
418
420
  - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
419
- - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
420
- - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
421
421
 
422
422
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
423
423
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -1215,6 +1215,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1215
1215
  const { tool, arguments: args } = call;
1216
1216
  let success: boolean;
1217
1217
  let output: string;
1218
+ let images: import("../ai/types").ImageAttachment[] | undefined;
1218
1219
 
1219
1220
  if (tool === "done") {
1220
1221
  success = false;
@@ -1242,6 +1243,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1242
1243
  const onProgress = ev.onToolProgress ? (partial: string) => ev.onToolProgress!(tool, partial) : undefined;
1243
1244
  const res = await handler(args ?? {}, cwd, onProgress);
1244
1245
  success = res.success;
1246
+ images = res.images;
1245
1247
  output = res.success ? res.output : (res.error ? (res.output ? `${res.error}\n${res.output}` : res.error) : res.output);
1246
1248
  } catch (err: any) {
1247
1249
  success = false;
@@ -1250,7 +1252,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1250
1252
  }
1251
1253
  }
1252
1254
  }
1253
- return { success, output };
1255
+ return { success, output, images };
1254
1256
  };
1255
1257
 
1256
1258
  const WRITE_TOOLS = new Set(["write", "edit"]);
@@ -1293,7 +1295,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1293
1295
  }
1294
1296
  }
1295
1297
 
1296
- const results: { success: boolean; output: string; executed: boolean }[] = Array.from(
1298
+ const results: { success: boolean; output: string; executed: boolean; images?: import("../ai/types").ImageAttachment[] }[] = Array.from(
1297
1299
  { length: toolCalls.length },
1298
1300
  () => ({ success: false, output: "", executed: false })
1299
1301
  );
@@ -1385,6 +1387,8 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
1385
1387
  }));
1386
1388
  pushAssistantTurn(history, responseText, reasonBuf, artifactBuf, toolUse);
1387
1389
  const resultMsg: Message = { role: "user", content: resultBlocks.join("\n\n"), toolResults };
1390
+ const images = indices.flatMap(idx => results[idx].images ?? []);
1391
+ if (images.length) resultMsg.images = images;
1388
1392
  if (hookExtra) resultMsg.toolResultExtra = hookExtra;
1389
1393
  history.push(resultMsg);
1390
1394
  };
@@ -22,6 +22,7 @@ async function readMutationLock(cwd: string): Promise<WorkflowState | null> {
22
22
  }
23
23
 
24
24
  export interface ToolResult {
25
+ images?: import("../ai/types").ImageAttachment[];
25
26
  success: boolean;
26
27
  output: string;
27
28
  error?: string;
@@ -60,6 +60,21 @@ function cachePath(): string {
60
60
  return path.join(cacheDir(), "model-catalog-cache.json");
61
61
  }
62
62
 
63
+ /** Account-bound catalog identity; opaque manual bearers must not share a scope. */
64
+ export async function openAIOauthAccountScope(credential?: Credential): Promise<string | undefined> {
65
+ let selected = credential;
66
+ if (!selected) {
67
+ const snapshot = await snapshotProvider("openai");
68
+ if (!snapshot.oauth) return undefined;
69
+ selected = { kind: "oauth", provider: "openai", token: snapshot.oauth };
70
+ }
71
+ if (selected.kind !== "oauth" || selected.provider !== "openai") return undefined;
72
+ const stored = await getStoredOAuth("openai");
73
+ const accountId = extractChatgptAccountId(selected.token)
74
+ || (stored?.access === selected.token ? stored.accountId?.trim() : undefined);
75
+ return accountId || `token:${createHash("sha256").update(selected.token).digest("hex")}`;
76
+ }
77
+
63
78
  /** Peek at the selected OAuth account without importing or refreshing credentials. */
64
79
  export async function antigravityAccountScope(credential?: Credential): Promise<string | undefined> {
65
80
  try {
@@ -251,14 +266,7 @@ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccou
251
266
  /** Read + apply in one step. Returns the cache so callers can decide about refreshing. */
252
267
  export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
253
268
  const cache = await readModelCache();
254
- let accountId: string | undefined;
255
- try {
256
- const stored = await getStoredOAuth("openai");
257
- const jwtAccountId = stored ? extractChatgptAccountId(stored.access) : undefined;
258
- accountId = jwtAccountId || stored?.accountId;
259
- } catch {
260
- accountId = undefined;
261
- }
269
+ const accountId = await openAIOauthAccountScope();
262
270
  const antigravityAccountId = await antigravityAccountScope();
263
271
  setOpenAIOauthAccountScope(accountId);
264
272
  setAntigravityAccountScope(antigravityAccountId);
@@ -255,12 +255,17 @@ export function recordLiveCodexModels(ids: readonly string[], accountId?: string
255
255
  }
256
256
 
257
257
  /** A confirmed account catalog is authoritative; use the snapshot before discovery. */
258
- export function isCodexModel(model: string): boolean {
258
+ export function isCodexModel(model: string, accountId = activeOpenAIOauthScope): boolean {
259
259
  const wire = model.startsWith("openai/") ? model.slice(7) : model;
260
- const confirmed = liveCodexModels.get(activeOpenAIOauthScope);
260
+ const confirmed = liveCodexModels.get(oauthScope(accountId));
261
261
  return confirmed ? confirmed.has(wire) : CODEX_MODELS.includes(wire);
262
262
  }
263
263
 
264
+ /** Whether discovery (including a restored account cache) established a Codex catalog. */
265
+ export function hasConfirmedCodexCatalog(accountId = activeOpenAIOauthScope): boolean {
266
+ return liveCodexModels.has(oauthScope(accountId));
267
+ }
268
+
264
269
  /** Test-only reset for live Codex observations and the active account scope. */
265
270
  export function resetLiveCodexModels(): void {
266
271
  liveCodexModels.clear();
@@ -24,7 +24,7 @@ import {
24
24
  import { extractChatgptAccountId } from "./providers/openai-responses";
25
25
  import { getStoredOAuth } from "../auth/storage";
26
26
  import { openaiCompatDef } from "./providers/openai-compatible-catalog";
27
- import { antigravityAccountScope } from "./model-cache";
27
+ import { antigravityAccountScope, openAIOauthAccountScope } from "./model-cache";
28
28
  import { getAntigravityUserAgent, resolveAntigravityProjectId } from "./providers/antigravity";
29
29
  import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "../auth/flows/google-project";
30
30
  import { getKimiCommonHeaders } from "../auth/flows/kimi";
@@ -412,6 +412,7 @@ export async function listProviderModels(
412
412
 
413
413
  let cred: Credential | undefined;
414
414
  let oauthAccountId: string | undefined;
415
+ let oauthCatalogScope: string | undefined;
415
416
  let source: ProviderModelsResult["source"] = "keyless";
416
417
  if (provider === "xai") {
417
418
  // xAI (Grok) is API-key only and not an OAuth AuthProvider: resolve its key
@@ -466,8 +467,9 @@ export async function listProviderModels(
466
467
  if (provider === "openai" && source === "oauth" && cred.kind === "oauth") {
467
468
  const stored = await getStoredOAuth("openai").catch(() => undefined);
468
469
  const jwtAccountId = extractChatgptAccountId(cred.token);
469
- oauthAccountId = jwtAccountId || stored?.accountId;
470
- setOpenAIOauthAccountScope(oauthAccountId);
470
+ oauthAccountId = jwtAccountId || (stored?.access === cred.token ? stored.accountId : undefined);
471
+ oauthCatalogScope = await openAIOauthAccountScope(cred);
472
+ setOpenAIOauthAccountScope(oauthCatalogScope);
471
473
  }
472
474
  // Gemini models are exposed ONLY under API-key auth: a gemini OAuth token can
473
475
  // still list models from generativelanguage.googleapis.com, but it can no
@@ -484,7 +486,7 @@ export async function listProviderModels(
484
486
  }
485
487
  }
486
488
 
487
- const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
489
+ const scope = { ...(oauthCatalogScope ? { accountId: oauthCatalogScope } : {}), ...(baseUrl ? { baseUrl } : {}) };
488
490
  const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
489
491
  let signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
490
492
  let initialAntigravityScope: string | undefined;
@@ -578,6 +580,12 @@ export async function listProviderModels(
578
580
  }
579
581
  const allModels = [...new Set(ids)].sort();
580
582
  const models = allModels.slice(0, limit);
583
+ if (provider === "openai" && source === "oauth" && !baseUrl) {
584
+ if (await openAIOauthAccountScope() !== oauthCatalogScope) {
585
+ return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
586
+ }
587
+ setOpenAIOauthAccountScope(oauthCatalogScope);
588
+ }
581
589
  if (provider === "antigravity") {
582
590
  const currentScope = await antigravityAccountScope();
583
591
  if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
@@ -594,7 +602,7 @@ export async function listProviderModels(
594
602
  replace: provider === "openai" || provider === "antigravity",
595
603
  ...(source === "oauth" && scope.accountId ? { accountId: scope.accountId } : {}),
596
604
  });
597
- if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
605
+ if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthCatalogScope);
598
606
  if (provider === "kimi" && source === "oauth") recordLiveKimiCodeModels(allModels);
599
607
  return {
600
608
  provider,
@@ -6,12 +6,15 @@ import "./register-providers"; // side-effect: registers built-in adapters into
6
6
  import type { CallOptions, Message, ProviderAdapter, ProviderName } from "./types";
7
7
  import { expandAlias, resolveModelId, effectiveAliasesFor, BUILTIN_ALIASES } from "./model-registry";
8
8
  import { findCatalogEntry, type ModelCatalogEntry } from "./model-catalog-compat";
9
- import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel, isCodexModel, isKimiCodeModel, findLiveCatalogModel } from "./model-catalog";
9
+ import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel, isCodexModel, isKimiCodeModel, findLiveCatalogModel, hasConfirmedCodexCatalog, isLiveProviderModel } from "./model-catalog";
10
10
  import { xaiCredential } from "./providers/xai";
11
11
  import { allCompatNames, isOpenAICompatProvider } from "./providers/openai-compatible-catalog";
12
12
  import { withRetry, defaultRetryable, withConnectionContext, isConnectionError, ConnectionContextError, type RetryOptions } from "../util/retry";
13
13
  import { jeoEnv } from "../util/env";
14
14
  import type { Config } from "../agent/state";
15
+ import { listProviderModels } from "./model-discovery";
16
+ import type { ProviderModelsResult } from "./model-discovery";
17
+ import { openAIOauthAccountScope } from "./model-cache";
15
18
 
16
19
 
17
20
 
@@ -404,6 +407,8 @@ interface Resolved {
404
407
  baseUrl: string | undefined;
405
408
  }
406
409
 
410
+ const codexCatalogInFlight = new Map<string, Promise<ProviderModelsResult>>();
411
+
407
412
  /**
408
413
  * The credential to actually use for a provider call. A configured local OpenAI-compatible base
409
414
  * URL must use the standard /chat/completions path, but the openai adapter dispatches on
@@ -436,7 +441,7 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
436
441
  }
437
442
  const adapter = providerRegistry.get(provider)!;
438
443
 
439
- const baseUrl =
444
+ let baseUrl =
440
445
  options.baseUrl ??
441
446
  (provider === "openai" ? config.openaiBaseUrl : undefined) ??
442
447
  (provider === "ollama" ? config.ollamaBaseUrl : undefined) ??
@@ -503,6 +508,47 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
503
508
  const credentialProvider = provider as AuthProvider;
504
509
  const credential = await resolveCredential(credentialProvider);
505
510
  const effective = effectiveCredentialForProvider(credentialProvider, credential, config, model);
511
+ // A subscription catalog is independent of a configured OpenAI-compatible
512
+ // endpoint. Keep its OAuth transport rather than replacing it with "no-key".
513
+ // Explicit endpoint overrides, proxy keys and endpoint-discovered models win.
514
+ if (provider === "openai" && baseUrl && options.baseUrl === undefined
515
+ && effective.kind === "oauth" && !config.providers.openai
516
+ && !isLiveProviderModel("openai", model, { openaiBaseUrl: baseUrl })) {
517
+ options.signal?.throwIfAborted();
518
+ const accountScope = await openAIOauthAccountScope(effective);
519
+ if (accountScope && !hasConfirmedCodexCatalog(accountScope)) {
520
+ let discovery = codexCatalogInFlight.get(accountScope);
521
+ if (!discovery) {
522
+ // One caller's cancellation must not cancel another agent's catalog read.
523
+ discovery = listProviderModels("openai", { config, codexCatalog: true });
524
+ codexCatalogInFlight.set(accountScope, discovery);
525
+ void discovery.finally(() => codexCatalogInFlight.delete(accountScope)).catch(() => {});
526
+ }
527
+ const signal = options.signal;
528
+ if (signal) {
529
+ const aborted = Promise.withResolvers<never>();
530
+ const onAbort = () => aborted.reject(signal.reason);
531
+ signal.addEventListener("abort", onAbort, { once: true });
532
+ try {
533
+ signal.throwIfAborted();
534
+ await Promise.race([discovery, aborted.promise]);
535
+ } finally {
536
+ signal.removeEventListener("abort", onAbort);
537
+ }
538
+ } else {
539
+ await discovery;
540
+ }
541
+ }
542
+ options.signal?.throwIfAborted();
543
+ if (await openAIOauthAccountScope() !== accountScope) {
544
+ throw new Error("OpenAI credential changed during model discovery; retry the request.");
545
+ }
546
+ // Never infer cloud eligibility from the bundled snapshot when discovery fails.
547
+ if (accountScope && hasConfirmedCodexCatalog(accountScope) && isCodexModel(model, accountScope)) {
548
+ baseUrl = undefined;
549
+ callOptions.baseUrl = undefined;
550
+ }
551
+ }
506
552
  const isLocalOpenAi = provider === "openai" && !!baseUrl;
507
553
  if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !oauthServesModel("openai", model)) {
508
554
  throw new Error(
@@ -240,6 +240,7 @@ export function buildAnthropicMessages(messages: Message[], model: string, think
240
240
  const blocks: AnthropicContentBlock[] = m.toolResults.map(tr => ({
241
241
  type: "tool_result", tool_use_id: tr.id, content: tr.output, is_error: tr.isError,
242
242
  }));
243
+ for (const img of m.images ?? []) blocks.push({ type: "image", source: { type: "base64", media_type: img.mediaType, data: img.data } });
243
244
  if (m.toolResultExtra) blocks.push({ type: "text", text: m.toolResultExtra });
244
245
  return { role: "user", content: blocks };
245
246
  }
@@ -87,6 +87,7 @@ export function buildResponsesInput(messages: Message[], modelKey: string, strip
87
87
  && nonSystem[i - 1].role === "assistant" && responsesNativizable(nonSystem[i - 1], modelKey)) {
88
88
  for (const tr of m.toolResults) items.push({ type: "function_call_output", call_id: tr.id, output: tr.output });
89
89
  if (m.toolResultExtra) items.push({ role: "user", content: [{ type: "input_text", text: m.toolResultExtra }] });
90
+ if (m.images?.length) items.push({ role: "user", content: m.images.map(img => ({ type: "input_image", image_url: `data:${img.mediaType};base64,${img.data}` })) });
90
91
  return;
91
92
  }
92
93
  items.push(plain(m));
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
4
  import { computerSupervisor } from "../agent/computer-supervisor";
5
+ import type { ImageAttachment } from "../ai/types";
5
6
  import { readGlobalConfig } from "../agent/state";
6
7
 
7
8
  export interface ComputerAction {
@@ -36,9 +37,11 @@ async function logAudit(action: ComputerAction, success: boolean, error?: string
36
37
  async function runCommand(cmd: string[]): Promise<{ success: boolean; stdout: string; stderr: string }> {
37
38
  try {
38
39
  const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
39
- await proc.exited;
40
- const stdout = await new Response(proc.stdout).text();
41
- const stderr = await new Response(proc.stderr).text();
40
+ const [stdout, stderr] = await Promise.all([
41
+ new Response(proc.stdout).text(),
42
+ new Response(proc.stderr).text(),
43
+ proc.exited,
44
+ ]);
42
45
  return {
43
46
  success: proc.exitCode === 0,
44
47
  stdout,
@@ -53,7 +56,7 @@ async function runCommand(cmd: string[]): Promise<{ success: boolean; stdout: st
53
56
  }
54
57
  }
55
58
 
56
- export async function executeComputerAction(action: ComputerAction, opts?: { enabledOverride?: boolean }): Promise<{ success: boolean; output: string; error?: string }> {
59
+ export async function executeComputerAction(action: ComputerAction, opts?: { enabledOverride?: boolean }): Promise<{ success: boolean; output: string; error?: string; images?: ImageAttachment[] }> {
57
60
  let enabled = opts?.enabledOverride;
58
61
  if (enabled === undefined) {
59
62
  const config = await readGlobalConfig().catch(() => null);
@@ -68,6 +71,18 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
68
71
  }
69
72
 
70
73
 
74
+ if (!action || typeof action !== "object") return { success: false, output: "", error: "Expected a computer action object." };
75
+ for (const field of ["x", "y", "deltaX", "deltaY", "duration"] as const) {
76
+ if (action[field] !== undefined && (typeof action[field] !== "number" || !Number.isFinite(action[field]))) {
77
+ return { success: false, output: "", error: `Invalid finite number for ${field}.` };
78
+ }
79
+ }
80
+ if (action.duration !== undefined && (action.duration < 0 || action.duration > 60)) {
81
+ return { success: false, output: "", error: "duration must be between 0 and 60 seconds." };
82
+ }
83
+ if (os.platform() !== "darwin" && os.platform() !== "linux") {
84
+ return { success: false, output: "", error: "Computer use supports macOS and Linux/X11 only." };
85
+ }
71
86
  // Read-only actions don't need supervisor inputAllowed check
72
87
  const isReadOnly = action.action === "screenshot" || action.action === "wait";
73
88
  if (!isReadOnly) {
@@ -83,7 +98,7 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
83
98
  switch (action.action) {
84
99
  case "screenshot": {
85
100
  const tmpDir = os.tmpdir();
86
- const screenPath = path.join(tmpDir, `jeo-screen-${Date.now()}.png`);
101
+ const screenPath = path.join(tmpDir, `jeo-screen-${crypto.randomUUID()}.png`);
87
102
  let cmd: string[];
88
103
  if (isMac) {
89
104
  cmd = ["screencapture", "-x", screenPath];
@@ -100,7 +115,7 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
100
115
  const base64 = bytes.toString("base64");
101
116
  await fs.unlink(screenPath).catch(() => {});
102
117
  await logAudit(action, true);
103
- return { success: true, output: base64 };
118
+ return { success: true, output: "Screenshot captured.", images: [{ mediaType: "image/png", data: base64 }] };
104
119
  } catch (err: any) {
105
120
  await logAudit(action, false, err.message);
106
121
  return { success: false, output: "", error: `Failed to read screenshot file: ${err.message}` };
@@ -164,9 +179,9 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
164
179
  }
165
180
  let cmd: string[];
166
181
  if (isMac) {
167
- cmd = ["cliclick", `dd:${action.x},${action.y}`];
182
+ cmd = ["cliclick", "dd:.", `dm:${action.x},${action.y}`, `du:${action.x},${action.y}`];
168
183
  } else {
169
- cmd = ["xdotool", "mousemove", String(action.x), String(action.y), "mousedown", "1"];
184
+ cmd = ["xdotool", "mousedown", "1", "mousemove", String(action.x), String(action.y), "mouseup", "1"];
170
185
  }
171
186
  const res = await runCommand(cmd);
172
187
  await logAudit(action, res.success, res.success ? undefined : res.stderr);
@@ -179,9 +194,14 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
179
194
  let cmd: string[];
180
195
  const dx = action.deltaX ?? 0;
181
196
  const dy = action.deltaY ?? 0;
197
+ if (!Number.isInteger(dx) || !Number.isInteger(dy) || Math.abs(dx) > 1000 || Math.abs(dy) > 1000) {
198
+ return { success: false, output: "", error: "Scroll deltas must be integers between -1000 and 1000." };
199
+ }
200
+ if (dx === 0 && dy === 0) return { success: true, output: "Scroll skipped (zero deltas)." };
182
201
  if (isMac) {
183
- // cliclick scroll deltaY deltaX
184
- cmd = ["cliclick", `w:${dy},${dx}`];
202
+ // cliclick's w: command WAITS; it cannot scroll. Use Quartz line events.
203
+ cmd = ["/usr/bin/osascript", "-l", "JavaScript", "-e",
204
+ `ObjC.import('Cocoa'); var event = $.CGEventCreateScrollWheelEvent(null, 1, 2, ${dy}, ${dx}); $.CGEventPost(0, event);`];
185
205
  } else {
186
206
  // xdotool click 4 (scroll up), 5 (scroll down), 6 (scroll left), 7 (scroll right)
187
207
  const actions: string[] = [];
@@ -212,7 +232,7 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
212
232
  if (isMac) {
213
233
  cmd = ["cliclick", `t:${action.text}`];
214
234
  } else {
215
- cmd = ["xdotool", "type", action.text];
235
+ cmd = ["xdotool", "type", "--clearmodifiers", "--", action.text];
216
236
  }
217
237
  const res = await runCommand(cmd);
218
238
  await logAudit(action, res.success, res.success ? undefined : res.stderr);
@@ -227,7 +247,16 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
227
247
  }
228
248
  let cmd: string[];
229
249
  if (isMac) {
230
- cmd = ["cliclick", `kp:${action.key}`];
250
+ const aliases: Record<string, string> = { return: "return", enter: "return", escape: "esc", backspace: "delete", arrowup: "arrow-up", arrowdown: "arrow-down", arrowleft: "arrow-left", arrowright: "arrow-right", pageup: "page-up", pagedown: "page-down" };
251
+ const parts = action.key.toLowerCase().split("+");
252
+ const key = parts.pop()!;
253
+ const modifiers = parts.map(p => ({ command: "cmd", meta: "cmd", control: "ctrl", option: "alt" }[p] ?? p));
254
+ if (modifiers.some(p => !["cmd", "ctrl", "alt", "shift", "fn"].includes(p))) {
255
+ return { success: false, output: "", error: "Unknown key modifier." };
256
+ }
257
+ const press = key.length === 1 ? `t:${key}` : `kp:${aliases[key] ?? key}`;
258
+ cmd = ["cliclick", ...(modifiers.length ? [`kd:${modifiers.join(",")}`] : []), press,
259
+ ...(modifiers.length ? [`ku:${modifiers.join(",")}`] : [])];
231
260
  } else {
232
261
  cmd = ["xdotool", "key", action.key];
233
262
  }
@@ -250,6 +279,7 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
250
279
  return { success: false, output: "", error: "Missing actions array for batch action." };
251
280
  }
252
281
  const results: string[] = [];
282
+ const images: ImageAttachment[] = [];
253
283
  for (let i = 0; i < action.actions.length; i++) {
254
284
  const subAction = action.actions[i];
255
285
  const res = await executeComputerAction(subAction, opts);
@@ -257,12 +287,13 @@ export async function executeComputerAction(action: ComputerAction, opts?: { ena
257
287
  if (!res.success) {
258
288
  const err = `Batch failed at step ${i}: ${res.error}`;
259
289
  await logAudit(action, false, err);
260
- return { success: false, output: results.join("\n"), error: err };
290
+ return { success: false, output: results.join("\n"), error: err, images };
261
291
  }
262
292
  results.push(`Step ${i}: ${res.output}`);
293
+ images.push(...(res.images ?? []));
263
294
  }
264
295
  await logAudit(action, true);
265
- return { success: true, output: results.join("\n") };
296
+ return { success: true, output: results.join("\n"), images };
266
297
  }
267
298
 
268
299
  default:
@@ -329,7 +360,7 @@ export async function runComputerCommand(args: string[]): Promise<void> {
329
360
  const res = await executeComputerAction(action);
330
361
  if (res.success) {
331
362
  if (actionName === "screenshot") {
332
- console.log(res.output); // base64 output
363
+ console.log(res.images?.[0]?.data ?? res.output); // preserve CLI base64 output
333
364
  } else {
334
365
  console.log(res.output);
335
366
  }
@@ -9,6 +9,8 @@ import { companyLabel } from "../ai/model-catalog";
9
9
  import { ProviderHttpError, ProviderStreamError } from "../ai/providers/errors";
10
10
  import { AgyCliError } from "../agent/antigravity-cli";
11
11
  import { sanitizeForTerminal } from "../tui/components/code-view";
12
+ import { OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS } from "../auth/storage";
13
+ import { openaiCompatDef, allCompatNames } from "../ai/providers/openai-compatible-catalog";
12
14
  // Re-export: engine.ts and callers import the refusal predicate from here; the
13
15
  // definition lives in retry.ts so defaultRetryable can fail fast without a cycle.
14
16
  export { isRefusalError } from "./retry";
@@ -72,6 +74,12 @@ function retryAfterMsOf(err: unknown): number | undefined {
72
74
  return err instanceof ProviderHttpError ? err.retryAfterMs : undefined;
73
75
  }
74
76
 
77
+ function authProviderForLabel(label: string): string | undefined {
78
+ const normalized = label.toLowerCase() === "openai codex" ? "openai" : label.toLowerCase();
79
+ const matches = (id: string) => id === normalized || companyLabel(id).toLowerCase() === normalized;
80
+ return OAUTH_PROVIDERS.find(matches) ?? API_KEY_ONLY_PROVIDERS.find(matches) ?? allCompatNames().find(matches);
81
+ }
82
+
75
83
  export function friendlyProviderError(err: unknown): string {
76
84
  if (err instanceof AgyCliError) return sanitizeForTerminal(err.message);
77
85
  if (err instanceof ConnectionContextError) {
@@ -92,7 +100,10 @@ export function friendlyProviderError(err: unknown): string {
92
100
  // Tencent/groq that speak Anthropic's/OpenAI's wire protocol — see
93
101
  // `relabelProviderError`); only guess from message text for errors that never
94
102
  // carried a `.provider` field (bare Error, a re-wrapped doneReason string, …).
95
- const provider = knownErrorProvider(err) ?? (
103
+ const reportedProvider = knownErrorProvider(err) ?? /^(?:Error:\s*)?([^\r\n]+?) (?:request failed|stream failed|rejected the credential)\b/i.exec(msg)?.[1] ?? /^(?:Error:\s*)?(OpenAI(?: Codex)?|Antigravity|Anthropic|Gemini|Google)\b/i.exec(msg)?.[1];
104
+ // A raw prefix may contain a secret, not a provider name. Only registry labels
105
+ // may survive redaction or the round trip through an engine's doneReason.
106
+ const provider = reportedProvider && authProviderForLabel(reportedProvider) ? reportedProvider : (
96
107
  /antigravity/i.test(msg)
97
108
  ? "Antigravity"
98
109
  : /anthropic/i.test(msg)
@@ -117,8 +128,28 @@ export function friendlyProviderError(err: unknown): string {
117
128
  : "";
118
129
  return `Rate limited by ${provider} (HTTP 429).${retry} Auto-retry cannot clear this window right now — slow your request rate, wait for the reset, or switch model with /model (a local ollama model never rate-limits).`;
119
130
  }
120
- if (status === 401 || status === 403 || /\b40[13]\b/.test(msg)) {
121
- return `${provider} rejected the credential (HTTP ${status ?? "401/403"}). Run 'jeo auth status', re-login with /provider login <name>, and for Antigravity prefer '/provider login antigravity' (gemini login only works when the Cloud Code Assist backend authorizes that token).`;
131
+ const credentialStatus = status ?? Number(/\b(401|403)\b/.exec(msg)?.[1]);
132
+ if (credentialStatus === 401 || credentialStatus === 403) {
133
+ const authProvider = authProviderForLabel(provider);
134
+ let hint = "Check the configured credentials with 'jeo setup' or /provider.";
135
+ if (authProvider === "openai") {
136
+ hint = "For ChatGPT/Codex, run 'jeo auth login openai' or /provider login openai. For API/custom endpoints, check OPENAI_API_KEY and OPENAI_BASE_URL (config: providers.openai / openaiBaseUrl).";
137
+ } else if (authProvider === "gemini") {
138
+ hint = "Check GEMINI_API_KEY. For Google OAuth, select an antigravity/* model and run 'jeo auth login antigravity' or /provider login antigravity.";
139
+ } else if (authProvider === "antigravity") {
140
+ hint = "Run 'jeo auth login antigravity' or /provider login antigravity; a Gemini login only works when Cloud Code Assist authorizes that token.";
141
+ } else if (authProvider === "kimi") {
142
+ hint = "For Kimi Code, run 'jeo auth login kimi'. For the Moonshot API, check KIMI_API_KEY or set it with /provider key kimi <key>.";
143
+ } else if (authProvider && !OAUTH_PROVIDERS.some(id => id === authProvider)) {
144
+ const apiKeyEnv = openaiCompatDef(authProvider)?.apiKeyEnv ?? `${authProvider.toUpperCase()}_API_KEY`;
145
+ const command = API_KEY_ONLY_PROVIDERS.some(id => id === authProvider)
146
+ ? ` Set the key with /provider key ${authProvider} <key> or 'jeo auth login ${authProvider} --token <key>'.`
147
+ : " Update the custom provider's configured credentials.";
148
+ hint = `Check ${apiKeyEnv} or providers.${authProvider}.${command}`;
149
+ } else if (authProvider) {
150
+ hint = `Run 'jeo auth login ${authProvider}' or /provider login ${authProvider}; for API keys, check providers.${authProvider}.`;
151
+ }
152
+ return `${provider} rejected the credential (HTTP ${credentialStatus}). Run 'jeo auth status'. ${hint}`;
122
153
  }
123
154
  // A persistent 5xx (server-side fault on the provider's backend) — `ProviderHttpError`
124
155
  // already embeds the numeric status directly in its message ("… failed (HTTP 500) …",