jeo-code 0.8.33 → 0.8.35

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
@@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.35] - 2026-07-21
10
+ _Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog._
11
+
12
+ ### Added
13
+ - **`calc` tool** (`src/agent/calc-tool.ts`) — a new native tool evaluating one or more arithmetic expressions, each optionally wrapped in prefix/suffix text (e.g. `{expression:"3*7", prefix:"total: "}` → `"total: 21"`). Faithful port of gjc's `packages/coding-agent/src/tools/calculator.ts` tokenizer/recursive-descent-parser/evaluator (read directly, not inferred) — jeo's own `ToolResult` contract and tool-registration conventions, not gjc's bespoke TUI renderer. Supports `+ - * / % **` (exponentiation, right-associative), parentheses, unary +/-, and decimal/hex(`0x`)/binary(`0b`)/octal(`0o`)/scientific-notation numeric literals. Wired as tool 19 in `TOOL_PROTOCOL` (tool 8 in the read-only-subagent `READONLY_TOOL_PROTOCOL` — it has no side effects, so it survives `subagentToolset`'s mutating-tool filter automatically), registered in `DEFAULT_TOOLS`/`READONLY_TOOL_NAMES`, and given a full native function-calling schema in `tool-schemas.ts` (gjc doesn't expose its own `bisect`-equivalent tool on that path, but `calc`'s parameters are simple enough that leaving it native-schema-less would have been a needless gap).
14
+
15
+ ### Investigated but not ported (with reasons)
16
+ - Compared the rest of `packages/coding-agent/src/tools/` against jeo's tool surface: `archive-reader`/`sqlite-reader` (jeo's own `read` tool already handles both — see its docstring), `image-gen` (jeo's `generate_image` tool is the equivalent), `ast-edit`/`ast-grep`/`irc`/`job`/`subagent`/`todo-write`/`skill`/`debug` (already ported under matching names in earlier passes). `gh`/`review`/`ssh`/`cron`/`render-mermaid`/`checkpoint`/`json-tree`/`monitor`/`ask`/`vim` are genuinely new capabilities gjc has that jeo doesn't — each is a real, separately-scoped feature (GitHub CLI integration, remote SSH execution, scheduled tasks, diagram rendering, git-based checkpoints, a full Vim-mode composer, etc.), not a one-file port, and deferred rather than rushed into this pass.
17
+
18
+ ### Verified
19
+ - `test/calc-tool.test.ts` — 12 pass (new): precedence/associativity (including right-associative `**` and left-associative `-`/`/`), unary chaining, hex/binary/octal/scientific literals, `-0` normalization, syntax/empty/non-finite error paths, tokenizer shape, and END-TO-END wiring through the REAL `DEFAULT_TOOLS.calc`, the native schema registry, both `TOOL_PROTOCOL` texts, and read-only `subagentToolset` filtering (not mocks).
20
+ - `bun run typecheck` clean; full `bun test` 3114 pass / 0 fail across 306 files.
21
+
22
+ ## [0.8.34] - 2026-07-21
23
+ _Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged._
24
+
25
+ ### Added
26
+ - **`deepinfra` provider** (`src/ai/providers/openai-compatible-catalog.ts`) — `https://api.deepinfra.com/v1/openai`, `DEEPINFRA_API_KEY`, default `deepinfra/deepseek-ai/DeepSeek-V3.2`, plus known-model picker fallbacks (DeepSeek-R1-0528, Llama-3.3-70B-Instruct-Turbo, Kimi-K2.6, Qwen3-Coder-480B, gpt-oss-120b) — a real, popular inference cloud gjc's catalog has that jeo's didn't; one data-driven catalog row, auto-registered (no new adapter code, per this file's own "add a provider = one table row" design).
27
+ - **`litellm` provider** — `http://localhost:4000/v1` (LiteLLM's own documented local-proxy default, expected to be overridden via `jeo provider add litellm --base-url <your-proxy>`), `LITELLM_API_KEY`. Deliberately has no `knownModels` list — unlike every other row, LiteLLM is a self-hosted proxy whose model catalog is entirely user-defined (whatever their own `config.yaml` routes to), so there is no globally-fixed list to offer.
28
+ - `src/ai/types.ts`'s `ProviderName` and `src/auth/storage.ts`'s `AuthProvider`/`API_KEY_ONLY_PROVIDERS` gained both new names; `test/deepinfra-litellm-provider.test.ts` (+5 tests: registration, env var, base URL, routing, credential-gated readiness).
29
+
30
+ ### Scope notes (why the rest of gjc's current provider list wasn't ported wholesale)
31
+ - **`tencent` — explicitly kept**, per instruction, even though gjc's current `models.json` has no such entry at all (jeo's Tencent Cloud MaaS integration predates and is independent of gjc's own catalog).
32
+ - **`moonshot`** and **`firepass`** are gjc's OWN renamed/split entries for base URLs jeo already serves under different names — `moonshot`'s `https://api.moonshot.ai/v1` is jeo's existing `kimi` provider's base URL (jeo intentionally keeps ONE `kimi` provider spanning both the OAuth Kimi-Code subscription and the plain API-key path, rather than gjc's two separate provider identifiers); `firepass`'s `https://api.fireworks.ai/inference/v1` is byte-identical to jeo's existing `fireworks` entry. Already at parity, no action needed.
33
+ - **`opencode` / `opencode-go` / `opencode-zen`** — these are the competing `opencode` CLI's own hosted proxy/gateway products; a jeo user would need an opencode account to use them, and integrating a rival agent's own product as an upstream provider has little practical value for jeo's userbase. Not ported.
34
+ - **`azure-openai`, `amazon-bedrock`, `google-vertex`, `cursor`, `github-copilot`, `gitlab-duo`** — each requires a fundamentally new, non-trivial auth/wire-protocol adapter (Azure resource/deployment config, AWS SigV4 request signing, GCP OAuth + region/project config, Cursor's proprietary undocumented agent API, a GitHub Copilot-specific device-auth flow, GitLab OAuth) — none fit the existing "OpenAI-compatible base URL + API key" pattern every other row here uses; each is a real, separate feature in its own right, not a catalog row. Not ported in this pass to avoid fabricating auth flows without live verification.
35
+ - **`cloudflare-ai-gateway` / `vercel-ai-gateway`** — both require a per-account/per-gateway URL embedded in the base (`.../v1/<account>/<gateway>/...`), which isn't a good fit for a fixed catalog row; already achievable today via jeo's existing `jeo provider add --base-url` custom-endpoint mechanism.
36
+ - **`ollama-cloud`** — `src/ai/providers/ollama.ts`'s adapter never sends an `Authorization` header (by design, for the keyless local daemon); serving the hosted `https://ollama.com` cloud variant needs that adapter to accept a bearer credential, a real (if modest) code change beyond a catalog row. Deferred as a follow-up rather than rushed.
37
+ - System prompt philosophy (`src/prompts/`) and the `/route` command (`src/commands/launch/route-slash.ts`, `src/agent/route-history.ts`) were not touched — out of scope per the request.
38
+
39
+ ### Verified
40
+ - `test/deepinfra-litellm-provider.test.ts` — 5 pass (new).
41
+ - `bun run typecheck` clean; full `bun test` 3102 pass / 0 fail across 305 files (an initial run under heavy concurrent system load hit many unrelated timeouts across subsystems this pass never touched — browser, daemon, tokenizer, tmux, search-gitignore — all confirmed pre-existing/load-induced via isolated re-runs, then a clean full re-run came back 0 fail).
42
+
9
43
  ## [0.8.33] - 2026-07-20
10
44
  _Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer._
11
45
 
package/README.ja.md CHANGED
@@ -283,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
283
283
  ## 変更履歴 (Changelog)
284
284
 
285
285
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
286
+ - **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
287
+ - **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
286
288
  - **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
287
289
  - **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
288
290
  - **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
289
- - **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
290
- - **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
291
291
 
292
292
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
293
293
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -284,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
284
284
  ## 변경 이력 (Changelog)
285
285
 
286
286
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
287
+ - **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
288
+ - **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
287
289
  - **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
288
290
  - **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
289
291
  - **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
290
- - **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
291
- - **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
292
292
 
293
293
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
294
294
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -291,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
291
291
  ## Changelog
292
292
 
293
293
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
294
+ - **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
295
+ - **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
294
296
  - **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
295
297
  - **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
296
298
  - **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
297
- - **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
298
- - **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
299
299
 
300
300
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
301
301
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -283,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
283
283
  ## 更新日志 (Changelog)
284
284
 
285
285
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
286
+ - **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
287
+ - **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
286
288
  - **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
287
289
  - **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
288
290
  - **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
289
- - **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
290
- - **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
291
291
 
292
292
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
293
293
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.33",
3
+ "version": "0.8.35",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -0,0 +1,247 @@
1
+ /**
2
+ * `calc` tool — evaluate one or more arithmetic expressions (gjc parity:
3
+ * `packages/coding-agent/src/tools/calculator.ts`, ported for jeo's plain
4
+ * `ToolResult` contract; the tokenizer/parser/evaluator below are a faithful
5
+ * port of gjc's recursive-descent implementation, verified against a direct
6
+ * read of the real, public gajae-code source — TUI rendering is jeo's own,
7
+ * gjc's bespoke `Component`/`Theme` renderer doesn't apply here).
8
+ *
9
+ * Supports: decimal (123, 3.14, .5), scientific notation (1e10, 2.5E-3),
10
+ * hex (0xFF), binary (0b1010), octal (0o755) literals; +, -, *, /, %, and
11
+ * right-associative ** (exponentiation); parentheses; unary +/-.
12
+ */
13
+ import type { ToolResult } from "./tools";
14
+
15
+ /** Supported arithmetic operators (** is exponentiation). */
16
+ type Operator = "+" | "-" | "*" | "/" | "%" | "**";
17
+
18
+ type Token =
19
+ | { type: "number"; value: number; raw: string }
20
+ | { type: "operator"; value: Operator }
21
+ | { type: "paren"; value: "(" | ")" };
22
+
23
+ export interface CalcRequest {
24
+ expression: string;
25
+ /** Prefix text prepended to the formatted result (e.g. "total: "). */
26
+ prefix?: string;
27
+ /** Suffix text appended to the formatted result (e.g. " USD"). */
28
+ suffix?: string;
29
+ }
30
+
31
+ function isDigit(ch: string): boolean {
32
+ return ch >= "0" && ch <= "9";
33
+ }
34
+ function isHexDigit(ch: string): boolean {
35
+ return (ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "f") || (ch >= "A" && ch <= "F");
36
+ }
37
+ function isBinaryDigit(ch: string): boolean {
38
+ return ch === "0" || ch === "1";
39
+ }
40
+ function isOctalDigit(ch: string): boolean {
41
+ return ch >= "0" && ch <= "7";
42
+ }
43
+
44
+ /**
45
+ * Tokenize a math expression into numbers, operators, and parentheses.
46
+ * Number formats: decimal, scientific (1e10), hex (0x), binary (0b), octal (0o).
47
+ */
48
+ export function tokenizeExpression(expression: string): Token[] {
49
+ const tokens: Token[] = [];
50
+ let i = 0;
51
+
52
+ while (i < expression.length) {
53
+ const ch = expression[i]!;
54
+
55
+ if (ch.trim() === "") { i += 1; continue; }
56
+
57
+ if (ch === "(" || ch === ")") {
58
+ tokens.push({ type: "paren", value: ch });
59
+ i += 1;
60
+ continue;
61
+ }
62
+
63
+ // Check ** before single * to handle exponentiation.
64
+ if (ch === "*" && expression[i + 1] === "*") {
65
+ tokens.push({ type: "operator", value: "**" });
66
+ i += 2;
67
+ continue;
68
+ }
69
+
70
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%") {
71
+ tokens.push({ type: "operator", value: ch });
72
+ i += 1;
73
+ continue;
74
+ }
75
+
76
+ // Number parsing: starts with a digit, or a decimal point followed by one.
77
+ const next = expression[i + 1];
78
+ const numberStart = isDigit(ch) || (ch === "." && next !== undefined && isDigit(next));
79
+ if (!numberStart) {
80
+ throw new Error(`Invalid character "${ch}" in expression`);
81
+ }
82
+
83
+ const start = i;
84
+
85
+ // Prefixed literals: 0x (hex), 0b (binary), 0o (octal).
86
+ if (ch === "0" && next !== undefined) {
87
+ const prefix = next.toLowerCase();
88
+ if (prefix === "x" || prefix === "b" || prefix === "o") {
89
+ i += 2;
90
+ let hasDigit = false;
91
+ while (i < expression.length) {
92
+ const digit = expression[i]!;
93
+ const valid = prefix === "x" ? isHexDigit(digit) : prefix === "b" ? isBinaryDigit(digit) : isOctalDigit(digit);
94
+ if (!valid) break;
95
+ hasDigit = true;
96
+ i += 1;
97
+ }
98
+ if (!hasDigit) throw new Error(`Invalid numeric literal starting at "${expression.slice(start, i)}"`);
99
+ const raw = expression.slice(start, i);
100
+ const value = Number(raw); // JS Number() handles 0x/0b/0o natively.
101
+ if (!Number.isFinite(value)) throw new Error(`Invalid number "${raw}"`);
102
+ tokens.push({ type: "number", value, raw });
103
+ continue;
104
+ }
105
+ }
106
+
107
+ // Decimal integer part.
108
+ let hasDigits = false;
109
+ while (i < expression.length && isDigit(expression[i]!)) { hasDigits = true; i += 1; }
110
+
111
+ // Fractional part.
112
+ if (expression[i] === ".") {
113
+ i += 1;
114
+ while (i < expression.length && isDigit(expression[i]!)) { hasDigits = true; i += 1; }
115
+ }
116
+
117
+ if (!hasDigits) throw new Error(`Invalid number starting at "${expression.slice(start, i + 1)}"`);
118
+
119
+ // Scientific notation exponent (1e10, 2.5E-3).
120
+ if (expression[i] === "e" || expression[i] === "E") {
121
+ i += 1;
122
+ if (expression[i] === "+" || expression[i] === "-") i += 1;
123
+ let hasExponentDigits = false;
124
+ while (i < expression.length && isDigit(expression[i]!)) { hasExponentDigits = true; i += 1; }
125
+ if (!hasExponentDigits) throw new Error(`Invalid exponent in "${expression.slice(start, i)}"`);
126
+ }
127
+
128
+ const raw = expression.slice(start, i);
129
+ const value = Number(raw);
130
+ if (!Number.isFinite(value)) throw new Error(`Invalid number "${raw}"`);
131
+ tokens.push({ type: "number", value, raw });
132
+ }
133
+
134
+ return tokens;
135
+ }
136
+
137
+ /**
138
+ * Recursive-descent parser. Precedence (lowest to highest): +/-, then
139
+ * `* / %`, then unary +/-, then `**` (right-associative), then parens/literals.
140
+ */
141
+ class ExpressionParser {
142
+ #index = 0;
143
+ constructor(private readonly tokens: Token[]) {}
144
+
145
+ parse(): number {
146
+ const value = this.#parseExpression();
147
+ if (this.#index < this.tokens.length) throw new Error("Unexpected token in expression");
148
+ return value;
149
+ }
150
+
151
+ /** Left-associative: 1 - 2 - 3 = (1 - 2) - 3. */
152
+ #parseExpression(): number {
153
+ let value = this.#parseTerm();
154
+ for (;;) {
155
+ if (this.#matchOperator("+")) { value += this.#parseTerm(); continue; }
156
+ if (this.#matchOperator("-")) { value -= this.#parseTerm(); continue; }
157
+ break;
158
+ }
159
+ return value;
160
+ }
161
+
162
+ /** Left-associative: 8 / 4 / 2 = (8 / 4) / 2. */
163
+ #parseTerm(): number {
164
+ let value = this.#parseUnary();
165
+ for (;;) {
166
+ if (this.#matchOperator("*")) { value *= this.#parseUnary(); continue; }
167
+ if (this.#matchOperator("/")) { value /= this.#parseUnary(); continue; }
168
+ if (this.#matchOperator("%")) { value %= this.#parseUnary(); continue; }
169
+ break;
170
+ }
171
+ return value;
172
+ }
173
+
174
+ /** Recursive to handle chained unary: --x, +-x. */
175
+ #parseUnary(): number {
176
+ if (this.#matchOperator("+")) return this.#parseUnary();
177
+ if (this.#matchOperator("-")) return -this.#parseUnary();
178
+ return this.#parsePower();
179
+ }
180
+
181
+ /** Right-associative: 2 ** 3 ** 2 = 2 ** (3 ** 2) = 512, via recursion on the RHS. */
182
+ #parsePower(): number {
183
+ let value = this.#parsePrimary();
184
+ if (this.#matchOperator("**")) value = value ** this.#parsePower();
185
+ return value;
186
+ }
187
+
188
+ #parsePrimary(): number {
189
+ const token = this.#peek();
190
+ if (!token) throw new Error("Unexpected end of expression");
191
+ if (token.type === "number") { this.#index += 1; return token.value; }
192
+ if (token.type === "paren" && token.value === "(") {
193
+ this.#index += 1;
194
+ const value = this.#parseExpression();
195
+ if (!this.#matchParen(")")) throw new Error("Missing closing parenthesis");
196
+ return value;
197
+ }
198
+ throw new Error("Unexpected token in expression");
199
+ }
200
+
201
+ #matchOperator(value: Operator): boolean {
202
+ const token = this.tokens[this.#index];
203
+ if (token && token.type === "operator" && token.value === value) { this.#index += 1; return true; }
204
+ return false;
205
+ }
206
+
207
+ #matchParen(value: "(" | ")"): boolean {
208
+ const token = this.tokens[this.#index];
209
+ if (token && token.type === "paren" && token.value === value) { this.#index += 1; return true; }
210
+ return false;
211
+ }
212
+
213
+ #peek(): Token | undefined {
214
+ return this.tokens[this.#index];
215
+ }
216
+ }
217
+
218
+ /** Evaluate a math expression string. Throws on syntax errors, empty
219
+ * expressions, or non-finite results (Infinity/NaN, e.g. `1/0`). */
220
+ export function evaluateExpression(expression: string): number {
221
+ const tokens = tokenizeExpression(expression);
222
+ if (tokens.length === 0) throw new Error("Expression is empty");
223
+ const value = new ExpressionParser(tokens).parse();
224
+ if (!Number.isFinite(value)) throw new Error("Expression result is not a finite number");
225
+ return Object.is(value, -0) ? 0 : value; // normalize -0 to 0
226
+ }
227
+
228
+ function formatResult(value: number): string {
229
+ return String(value);
230
+ }
231
+
232
+ /** `calc` tool entrypoint: evaluate one or more expressions, each optionally
233
+ * wrapped in prefix/suffix text (e.g. `{expression:"3*7", prefix:"total: "}`). */
234
+ export async function calcTool(calculations: CalcRequest[]): Promise<ToolResult> {
235
+ if (!Array.isArray(calculations) || calculations.length === 0) {
236
+ return { success: false, output: "", error: "calc: 'calculations' must be a non-empty array of { expression }" };
237
+ }
238
+ try {
239
+ const lines = calculations.map(calc => {
240
+ const value = evaluateExpression(calc.expression);
241
+ return `${calc.prefix ?? ""}${formatResult(value)}${calc.suffix ?? ""}`;
242
+ });
243
+ return { success: true, output: lines.join("\n") };
244
+ } catch (e) {
245
+ return { success: false, output: "", error: `calc: ${e instanceof Error ? e.message : String(e)}` };
246
+ }
247
+ }
@@ -13,6 +13,7 @@ import type { Message } from "./loop";
13
13
  import { extractJsonObjectWithSpan, tryExtractJsonObject } from "./json";
14
14
  import { nativeToolSchemasFor, normalizeNativeToolName } from "./tool-schemas";
15
15
  import { readTool, writeTool, editTool, bashTool, findTool, searchTool, lsTool, mkdirTool, deleteTool, bisectTool, type ToolResult } from "./tools";
16
+ import { calcTool } from "./calc-tool";
16
17
  import { webSearchTool, setWebSearchActiveModel } from "./web-search";
17
18
  import { executeComputerAction } from "../commands/computer";
18
19
  import { computerSupervisor } from "./computer-supervisor";
@@ -107,6 +108,7 @@ export const DEFAULT_TOOLS: Record<string, ToolHandler> = {
107
108
  debug: createDebugTool(),
108
109
  browser: createBrowserTool(),
109
110
  bisect: (a, cwd) => bisectTool(a.good, a.bad, a.run, !!a.invert, a.maxSteps, a.stepTimeoutMs, cwd),
111
+ calc: a => calcTool(Array.isArray(a.calculations) ? a.calculations : []),
110
112
  };
111
113
 
112
114
  /** Tool names whose calls have no workspace side effects. Shared by execution,
@@ -117,6 +119,7 @@ const READONLY_TOOL_NAMES: Record<string, true> = {
117
119
  search: true,
118
120
  ls: true,
119
121
  web_search: true,
122
+ calc: true,
120
123
  };
121
124
  /** Tool-protocol description injected into the system prompt. */
122
125
  export const TOOL_PROTOCOL = [
@@ -139,7 +142,8 @@ export const TOOL_PROTOCOL = [
139
142
  "16. debug {action, ...} — Node.js debugging (launch/set_breakpoint/continue/step_over/step_in/step_out/pause/evaluate/stack_trace/scopes/variables/threads/output/terminate); one active session, 'launch' spawns 'node --inspect-brk' and pauses at start",
140
143
  "17. browser {action:\"open\"|\"close\"|\"run\"|\"act\", name?, url?, actions?} — headless Chromium automation (Playwright); 'act' runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot/verify) addressing elements by numeric id from 'observe' or a CSS selector; 'verify' {goal, selector?, fullPage?, design_tokens?, prior_screenshot?} screenshots and asks an INDEPENDENT vision model to judge it against 'goal' -> {verdict:\"PASS\"|\"MISMATCH\", detail} — prefer this over eyeballing a screenshot yourself for UI/visual work; 'run' executes JS with page/browser/tab/display/assert/wait in scope",
141
144
  "18. bisect {good, bad?, run, invert?, maxSteps?, stepTimeoutMs?} — binary-search git history to find the commit that introduced a regression; 'good' is a known-good commit, 'bad' defaults to HEAD, 'run' is a test command (exit 0=good, non-zero=bad, 125=skip), 'invert' finds a FIX instead of a regression",
142
- "19. done {reason?} — call when the task is fully implemented AND verified",
145
+ "19. calc {calculations:[{expression, prefix?, suffix?}]} — evaluate arithmetic expressions (+ - * / % **, parens, hex 0x/binary 0b/octal 0o literals, scientific notation); each result is prefix+value+suffix, one per line — use this instead of doing arithmetic in your head for anything beyond trivial",
146
+ "20. done {reason?} — call when the task is fully implemented AND verified",
143
147
  "",
144
148
  "Reply with STRICT JSON only — no code fences. You MAY include an optional leading",
145
149
  '"reasoning" string (one short sentence on your plan) before "tool":',
@@ -165,7 +169,8 @@ export const READONLY_TOOL_PROTOCOL = [
165
169
  "5. lsp {action, file, line?, symbol?} — TypeScript/JavaScript definition/references/hover/symbols/diagnostics (read-only; cross-file rename is not available to this role)",
166
170
  "6. ls {dirPath} — list a directory's entries",
167
171
  "7. web_search {query, recency?, limit?} — search the web (answer + sources + citations)",
168
- "8. done {reason?} — call when your review/analysis is complete",
172
+ "8. calc {calculations:[{expression, prefix?, suffix?}]} — evaluate arithmetic expressions (+ - * / % **, parens, hex/binary/octal literals)",
173
+ "9. done {reason?} — call when your review/analysis is complete",
169
174
  "",
170
175
  "Reply with STRICT JSON only — no prose, no code fences:",
171
176
  '{ "tool": "<name>", "arguments": { ... } }',
@@ -98,6 +98,24 @@ const SCHEMAS: Record<string, NativeToolSchema> = {
98
98
  required: ["action"],
99
99
  },
100
100
  },
101
+ calc: {
102
+ name: "calc",
103
+ description: "Evaluate one or more arithmetic expressions (+ - * / % **, parens, hex/binary/octal literals, scientific notation). Each result is prefix+value+suffix.",
104
+ parameters: {
105
+ type: "object",
106
+ properties: {
107
+ calculations: {
108
+ type: "array",
109
+ items: {
110
+ type: "object",
111
+ properties: { expression: STRING, prefix: STRING, suffix: STRING },
112
+ required: ["expression"],
113
+ },
114
+ },
115
+ },
116
+ required: ["calculations"],
117
+ },
118
+ },
101
119
  done: {
102
120
  name: "done",
103
121
  description: "Call when the task is fully implemented AND verified. The reason is shown to the user as your message.",
@@ -59,6 +59,13 @@ export const OPENAI_COMPAT_PROVIDERS: readonly OpenAICompatProviderDef[] = [
59
59
  { name: "zenmux", label: "ZenMux", baseUrl: "https://zenmux.ai/api/v1", apiKeyEnv: "ZENMUX_API_KEY", defaultModel: "zenmux/anthropic/claude-opus-4.6" },
60
60
  { name: "qianfan", label: "Qianfan", baseUrl: "https://qianfan.baidubce.com/v2", apiKeyEnv: "QIANFAN_API_KEY", defaultModel: "qianfan/deepseek-v3.2" },
61
61
  { name: "xiaomi", label: "Xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_API_KEY", defaultModel: "xiaomi/mimo-v2-flash" },
62
+ { name: "deepinfra", label: "DeepInfra", baseUrl: "https://api.deepinfra.com/v1/openai", apiKeyEnv: "DEEPINFRA_API_KEY", defaultModel: "deepinfra/deepseek-ai/DeepSeek-V3.2", knownModels: ["deepseek-ai/DeepSeek-V3.2", "deepseek-ai/DeepSeek-R1-0528", "meta-llama/Llama-3.3-70B-Instruct-Turbo", "moonshotai/Kimi-K2.6", "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", "openai/gpt-oss-120b"] },
63
+ // Self-hosted OpenAI-compatible proxy (LiteLLM) — the model catalog behind it is
64
+ // entirely user-configured (whatever the user's own litellm config.yaml routes to),
65
+ // so unlike every other row here there is no globally-fixed `knownModels` list to
66
+ // offer; `baseUrl` is LiteLLM's own documented local-proxy default and is expected
67
+ // to be overridden via `jeo provider add litellm --base-url <your-proxy-url>`.
68
+ { name: "litellm", label: "LiteLLM", baseUrl: "http://localhost:4000/v1", apiKeyEnv: "LITELLM_API_KEY", defaultModel: "litellm/gpt-4o-mini" },
62
69
  { name: "xiaomi-token-plan-ams", label: "Xiaomi Token Plan (Europe)", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_AMS_API_KEY", defaultModel: "xiaomi-token-plan-ams/mimo-v2.5", subscription: true },
63
70
  { name: "xiaomi-token-plan-cn", label: "Xiaomi Token Plan (China)", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_CN_API_KEY", defaultModel: "xiaomi-token-plan-cn/mimo-v2.5", subscription: true },
64
71
  { name: "xiaomi-token-plan-sgp", label: "Xiaomi Token Plan (Singapore)", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_SGP_API_KEY", defaultModel: "xiaomi-token-plan-sgp/mimo-v2.5", subscription: true },
package/src/ai/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Credential } from "../auth";
2
2
 
3
- export type ProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama" | "lmstudio" | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent";
3
+ export type ProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama" | "lmstudio" | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent" | "deepinfra" | "litellm";
4
4
 
5
5
 
6
6
  /** An image attached to a (user) message — base64 payload + IANA media type. */
@@ -8,12 +8,12 @@ import { jeoEnv } from "../util/env";
8
8
  /** Providers with an interactive OAuth login + refresh flow. */
9
9
  export type OAuthProvider = "anthropic" | "openai" | "gemini" | "antigravity" | "kimi";
10
10
  /** Every provider jeo resolves a credential for: OAuth-capable ∪ API-key-only. */
11
- export type AuthProvider = OAuthProvider | "xai" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent";
11
+ export type AuthProvider = OAuthProvider | "xai" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent" | "deepinfra" | "litellm";
12
12
 
13
13
 
14
14
  export const OAUTH_PROVIDERS: readonly OAuthProvider[] = ["anthropic", "openai", "gemini", "antigravity", "kimi"];
15
15
  /** API-key-only providers (no OAuth flow) — resolved from config.providers / `<NAME>_API_KEY`. */
16
- export const API_KEY_ONLY_PROVIDERS: readonly AuthProvider[] = ["xai", "groq", "deepseek", "mistral", "openrouter", "together", "cerebras", "fireworks", "nvidia", "alibaba-coding-plan", "huggingface", "nanogpt", "qwen-portal", "synthetic", "venice", "zenmux", "qianfan", "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", "minimax-code", "minimax-code-cn", "zai", "minimax", "tencent"];
16
+ export const API_KEY_ONLY_PROVIDERS: readonly AuthProvider[] = ["xai", "groq", "deepseek", "mistral", "openrouter", "together", "cerebras", "fireworks", "nvidia", "alibaba-coding-plan", "huggingface", "nanogpt", "qwen-portal", "synthetic", "venice", "zenmux", "qianfan", "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", "minimax-code", "minimax-code-cn", "zai", "minimax", "tencent", "deepinfra", "litellm"];
17
17
 
18
18
  /** Narrow an AuthProvider to the OAuth-capable subset (xai has no OAuth flow). */
19
19
  export function isOAuthProvider(p: AuthProvider): p is OAuthProvider {