diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d3c52eb36..f966a9f986 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -60,13 +60,13 @@ "dockerode": "^4.0.12", "express": "^5.1.0", "i18next": "^26.3.1", - "ink": "^6.8.0", + "ink": "^7.0.5", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "ioredis": "^5.6.0", "multer": "^2.1.1", "node-pty": "npm:@homebridge/node-pty-prebuilt-multiarch@^0.13.1", - "react": "^19.0.0", + "react": "^19.2.0", "react-i18next": "^17.0.8" }, "peerDependencies": { @@ -93,7 +93,7 @@ "@fusion/pi-claude-cli": "workspace:*", "@fusion/pi-llama-cpp": "workspace:*", "@types/node": "^22.0.0", - "@types/react": "^19.0.0", + "@types/react": "^19.2.0", "@vitest/coverage-v8": "^3.1.0", "cross-env": "^7.0.0", "esbuild": "^0.25.12", diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 4a34970f45..53d2e2b6ae 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -733,7 +733,9 @@ async function main() { const noAuth = args.includes("--no-auth"); const dashTokenIdx = args.indexOf("--token"); const token = dashTokenIdx !== -1 && dashTokenIdx + 1 < args.length ? args[dashTokenIdx + 1] : undefined; - await runDashboard(port, { paused, dev, interactive, host, noAuth, token }); + const dashLangIdx = args.indexOf("--lang"); + const lang = dashLangIdx !== -1 && dashLangIdx + 1 < args.length ? args[dashLangIdx + 1] : undefined; + await runDashboard(port, { paused, dev, interactive, host, noAuth, token, lang }); break; } diff --git a/packages/cli/src/commands/dashboard-tui/controller.ts b/packages/cli/src/commands/dashboard-tui/controller.ts index acd812f4a6..1b0460bc14 100644 --- a/packages/cli/src/commands/dashboard-tui/controller.ts +++ b/packages/cli/src/commands/dashboard-tui/controller.ts @@ -157,6 +157,9 @@ export class DashboardTUI { // will reapply the auto policy. mouseEnabled: boolean = false; + // Optional `--lang` override; highest-precedence locale source for the TUI. + lang?: string; + constructor() { this.logBuffer = new LogRingBuffer(); } @@ -669,6 +672,20 @@ export class DashboardTUI { const { render } = await import("ink"); const { createElement } = await import("react"); const { DashboardApp } = await import("./app.js"); + const { I18nextProvider } = await import("react-i18next"); + const { initCliI18n, resolveCliLocale } = await import("../../i18n/index.js"); + const { GlobalSettingsStore } = await import("@fusion/core"); + + // Resolve locale: --lang flag → persisted GlobalSettings → env → en. + let settingLanguage: string | undefined; + try { + settingLanguage = (await new GlobalSettingsStore().getSettings())?.language; + } catch { + // Settings unreadable — fall back to env/default. + } + const i18n = initCliI18n( + resolveCliLocale({ flag: this.lang, setting: settingLanguage, env: process.env }), + ); // Enter the terminal's alternate-screen buffer before mounting Ink so // the TUI gets a dedicated fullscreen surface that doesn't share @@ -683,7 +700,7 @@ export class DashboardTUI { } this.inkInstance = render( - createElement(DashboardApp, { controller: this }), + createElement(I18nextProvider, { i18n }, createElement(DashboardApp, { controller: this })), ); // Mouse mode must be enabled AFTER Ink mounts (which calls diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index 16e3ad4bd8..d516a24544 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -686,7 +686,7 @@ async function resolveDashboardAuthToken(opts: { noAuth?: boolean; token?: strin return tokenManager.generateToken(); } -export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean; open?: boolean; host?: string; noAuth?: boolean; token?: string } = {}) { +export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean; open?: boolean; host?: string; noAuth?: boolean; token?: string; lang?: string } = {}) { // Default to localhost so the dashboard (and its shell-capable terminal API) // is not exposed on the LAN. Pass --host 0.0.0.0 explicitly to opt-in. const selectedHost = opts.host ?? "127.0.0.1"; @@ -752,6 +752,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?: if (isTTY) { tui = new DashboardTUI(); + tui.lang = opts.lang; void startupUpdateStatusPromise.then((updateStatus) => { tui?.setUpdateStatus(updateStatus); }); diff --git a/packages/cli/src/i18n/__tests__/i18n.test.tsx b/packages/cli/src/i18n/__tests__/i18n.test.tsx new file mode 100644 index 0000000000..ff8a113af9 --- /dev/null +++ b/packages/cli/src/i18n/__tests__/i18n.test.tsx @@ -0,0 +1,69 @@ +import { Text } from "ink"; +import { render } from "ink-testing-library"; +import { createElement } from "react"; +import { I18nextProvider, useTranslation } from "react-i18next"; +import { describe, expect, it } from "vitest"; +import { cliI18n, detectEnvLocale, initCliI18n, resolveCliLocale } from "../index.js"; + +describe("detectEnvLocale", () => { + it("parses POSIX locale env values to a supported locale", () => { + expect(detectEnvLocale({ LANG: "fr_FR.UTF-8" })).toBe("fr"); + expect(detectEnvLocale({ LC_ALL: "es_ES.UTF-8" })).toBe("es"); + expect(detectEnvLocale({ LANG: "zh_CN.UTF-8" })).toBe("zh-CN"); + expect(detectEnvLocale({ LANG: "zh_TW" })).toBe("zh-TW"); + }); + + it("honors precedence LC_ALL > LC_MESSAGES > LANG > LANGUAGE", () => { + expect(detectEnvLocale({ LC_ALL: "fr_FR", LANG: "es_ES" })).toBe("fr"); + expect(detectEnvLocale({ LC_MESSAGES: "es_ES", LANG: "fr_FR" })).toBe("es"); + }); + + it("falls back to a bare language and undefined for unsupported", () => { + expect(detectEnvLocale({ LANG: "zh" })).toBe("zh-CN"); + expect(detectEnvLocale({ LANG: "de_DE.UTF-8" })).toBeUndefined(); + expect(detectEnvLocale({})).toBeUndefined(); + }); +}); + +describe("resolveCliLocale precedence", () => { + it("flag overrides setting and env", () => { + expect(resolveCliLocale({ flag: "zh-TW", setting: "fr", env: { LANG: "es_ES" } })).toBe("zh-TW"); + }); + it("setting overrides env", () => { + expect(resolveCliLocale({ setting: "fr", env: { LANG: "es_ES" } })).toBe("fr"); + }); + it("env used when no flag/setting", () => { + expect(resolveCliLocale({ env: { LANG: "es_ES.UTF-8" } })).toBe("es"); + }); + it("defaults to en", () => { + expect(resolveCliLocale({ env: {} })).toBe("en"); + expect(resolveCliLocale({ flag: "de", env: {} })).toBe("en"); + }); +}); + +// The load-bearing spike: react-i18next must work under Ink's custom reconciler. +function Loading() { + const { t } = useTranslation("cli"); + return createElement(Text, null, t("tui.loading", "Loading…")); +} + +describe("react-i18next under the Ink reconciler", () => { + it("renders a localized first frame synchronously", () => { + const i18n = initCliI18n("en"); + const { lastFrame } = render( + createElement(I18nextProvider, { i18n }, createElement(Loading)), + ); + expect(lastFrame()).toContain("Loading…"); + }); + + it("re-renders on changeLanguage", async () => { + const i18n = initCliI18n("en"); + cliI18n.addResourceBundle("zh-CN", "cli", { tui: { loading: "加载中…" } }, true, true); + const { lastFrame } = render( + createElement(I18nextProvider, { i18n }, createElement(Loading)), + ); + expect(lastFrame()).toContain("Loading…"); + await i18n.changeLanguage("zh-CN"); + expect(lastFrame()).toContain("加载中…"); + }); +}); diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts new file mode 100644 index 0000000000..404768e76f --- /dev/null +++ b/packages/cli/src/i18n/index.ts @@ -0,0 +1,78 @@ +import { DEFAULT_LOCALE, isLocale, type Locale } from "@fusion/core"; +import { + baseInitOptions, + CLI_NAMESPACES, + cliResources, + DEFAULT_NAMESPACE, +} from "@fusion/i18n"; +import i18next, { type i18n as I18nInstance, type Resource } from "i18next"; +import { initReactI18next } from "react-i18next"; + +/** + * Terminal-UI i18next instance. + * + * Unlike the dashboard, the CLI bundles all catalogs statically (via the + * generated @fusion/i18n cli map) and initializes synchronously (inline + * resources, no async backend) so the very first rendered Ink frame is + * already localized — no flash of untranslated keys. + */ + +/** + * Parse a POSIX locale environment value (e.g. `fr_FR.UTF-8`, `zh_CN`, + * `zh-Hant`) into a supported {@link Locale}, or undefined when none matches. + */ +export function detectEnvLocale(env: NodeJS.ProcessEnv = process.env): Locale | undefined { + const raw = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE; + if (!raw) return undefined; + + // Strip encoding/modifier (`.UTF-8`, `@euro`) and normalize `_` to `-`. + const code = raw.split(/[.:@\s]/)[0].replace("_", "-"); + if (isLocale(code)) return code; + + const lang = code.split("-")[0].toLowerCase(); + if (isLocale(lang)) return lang; + // A bare `zh` resolves to Simplified, mirroring the shared fallback chain. + if (lang === "zh") return "zh-CN"; + return undefined; +} + +/** + * Resolve the active CLI locale with precedence: + * `--lang flag → persisted GlobalSettings.language → environment → en`. + */ +export function resolveCliLocale(opts: { + flag?: string | undefined; + setting?: string | undefined; + env?: NodeJS.ProcessEnv; +} = {}): Locale { + const { flag, setting, env = process.env } = opts; + if (isLocale(flag)) return flag; + if (isLocale(setting)) return setting; + return detectEnvLocale(env) ?? DEFAULT_LOCALE; +} + +let initialized = false; + +/** Initialize (synchronously) or switch the CLI i18next instance to `locale`. */ +export function initCliI18n(locale: Locale): I18nInstance { + if (!initialized) { + void i18next.use(initReactI18next).init({ + ...baseInitOptions(), + lng: locale, + ns: [...CLI_NAMESPACES], + defaultNS: DEFAULT_NAMESPACE, + // Inline resources + no async backend => init completes synchronously, + // so the first rendered Ink frame is already localized (i18next v26 + // dropped the old `initImmediate` flag; this is now the default for + // backend-less, resource-inlined init). + resources: cliResources as unknown as Resource, + react: { useSuspense: false }, + }); + initialized = true; + } else if (i18next.language !== locale) { + void i18next.changeLanguage(locale); + } + return i18next; +} + +export { i18next as cliI18n }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cab0352575..48384f5b69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,10 +46,10 @@ importers: dependencies: '@earendil-works/pi-ai': specifier: ^0.78.0 - version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) '@earendil-works/pi-coding-agent': specifier: ^0.78.0 - version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + version: 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) dockerode: specifier: ^4.0.12 version: 4.0.12 @@ -60,14 +60,14 @@ importers: specifier: ^26.3.1 version: 26.3.1(typescript@5.9.3) ink: - specifier: ^6.8.0 - version: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) + specifier: ^7.0.5 + version: 7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) ink-spinner: specifier: ^5.0.0 - version: 5.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4) + version: 5.0.0(ink@7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4) ink-text-input: specifier: ^6.0.0 - version: 6.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4) + version: 6.0.0(ink@7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4) ioredis: specifier: ^5.6.0 version: 5.10.1 @@ -78,7 +78,7 @@ importers: specifier: npm:@homebridge/node-pty-prebuilt-multiarch@^0.13.1 version: '@homebridge/node-pty-prebuilt-multiarch@0.13.1' react: - specifier: ^19.0.0 + specifier: ^19.2.0 version: 19.2.4 react-i18next: specifier: ^17.0.8 @@ -106,7 +106,7 @@ importers: specifier: ^25.5.2 version: 25.5.2 '@types/react': - specifier: ^19.0.0 + specifier: ^19.2.0 version: 19.2.14 '@vitest/coverage-v8': specifier: ^3.1.0 @@ -562,10 +562,10 @@ importers: dependencies: '@earendil-works/pi-ai': specifier: '*' - version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) '@earendil-works/pi-coding-agent': specifier: '*' - version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + version: 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) devDependencies: '@types/node': specifier: ^25.5.2 @@ -1017,8 +1017,8 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@alcalzone/ansi-tokenize@0.2.5': - resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} + '@alcalzone/ansi-tokenize@0.3.0': + resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} '@ampproject/remapping@2.3.0': @@ -3550,9 +3550,9 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} + cli-boxes@4.0.1: + resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} + engines: {node: '>=18.20 <19 || >=20.10'} cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} @@ -3578,9 +3578,9 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} + cli-truncate@6.0.0: + resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==} + engines: {node: '>=22'} cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} @@ -3943,9 +3943,6 @@ packages: resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} engines: {node: '>= 0.4.0'} - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4616,12 +4613,12 @@ packages: ink: '>=5' react: '>=18' - ink@6.8.0: - resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} - engines: {node: '>=20'} + ink@7.0.5: + resolution: {integrity: sha512-zWNjGHQPxSeiSAmDUOq+QPQ6CfmMhmNi85vrJIuy4prafKKUSoZlXEy4wbM7LuLuF1pDURk7qvF4fxrQlLxv3w==} + engines: {node: '>=22'} peerDependencies: - '@types/react': '>=19.0.0' - react: '>=19.0.0' + '@types/react': '>=19.2.0' + react: '>=19.2.0' react-devtools-core: '>=6.1.2' peerDependenciesMeta: '@types/react': @@ -6134,9 +6131,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} + slice-ansi@9.0.0: + resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==} + engines: {node: '>=22'} smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} @@ -6235,10 +6232,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - string-width@8.2.0: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} @@ -6767,6 +6760,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6779,10 +6776,6 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -6916,7 +6909,7 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@alcalzone/ansi-tokenize@0.2.5': + '@alcalzone/ansi-tokenize@0.3.0': dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 @@ -7650,9 +7643,9 @@ snapshots: - ws - zod - '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': + '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: - '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -7664,9 +7657,9 @@ snapshots: - ws - zod - '@earendil-works/pi-agent-core@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + '@earendil-works/pi-agent-core@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: - '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-ai': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -7726,26 +7719,6 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': - dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) - '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76)) - '@mistralai/mistralai': 2.2.1 - '@smithy/node-http-handler': 4.7.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.0)(zod@3.25.76) - partial-json: 0.1.7 - typebox: 1.1.38 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - '@earendil-works/pi-ai@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) @@ -7766,6 +7739,26 @@ snapshots: - ws - zod + '@earendil-works/pi-ai@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76)) + '@mistralai/mistralai': 2.2.1 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.20.0)(zod@3.25.76) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-ai@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) @@ -7835,10 +7828,10 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': + '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: - '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) - '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) '@earendil-works/pi-tui': 0.77.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -7864,11 +7857,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + '@earendil-works/pi-coding-agent@0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: - '@earendil-works/pi-agent-core': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@earendil-works/pi-ai': 0.77.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@earendil-works/pi-tui': 0.77.0 + '@earendil-works/pi-agent-core': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@earendil-works/pi-ai': 0.78.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) + '@earendil-works/pi-tui': 0.78.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -10106,7 +10099,7 @@ snapshots: ci-info@4.4.0: {} - cli-boxes@3.0.0: {} + cli-boxes@4.0.1: {} cli-cursor@3.1.0: dependencies: @@ -10130,9 +10123,9 @@ snapshots: string-width: 4.2.3 optional: true - cli-truncate@5.2.0: + cli-truncate@6.0.0: dependencies: - slice-ansi: 8.0.0 + slice-ansi: 9.0.0 string-width: 8.2.0 cli-width@4.1.0: {} @@ -10529,8 +10522,6 @@ snapshots: dependencies: sax: 1.1.4 - emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -11351,33 +11342,33 @@ snapshots: ini@4.1.3: {} - ink-spinner@5.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4): + ink-spinner@5.0.0(ink@7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4): dependencies: cli-spinners: 2.9.2 - ink: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) + ink: 7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) react: 19.2.4 ink-testing-library@4.0.0(@types/react@19.2.14): optionalDependencies: '@types/react': 19.2.14 - ink-text-input@6.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4): + ink-text-input@6.0.0(ink@7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4): dependencies: chalk: 5.6.2 - ink: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) + ink: 7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4) react: 19.2.4 type-fest: 4.41.0 - ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4): + ink@7.0.5(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4): dependencies: - '@alcalzone/ansi-tokenize': 0.2.5 + '@alcalzone/ansi-tokenize': 0.3.0 ansi-escapes: 7.3.0 ansi-styles: 6.2.3 auto-bind: 5.0.1 chalk: 5.6.2 - cli-boxes: 3.0.0 + cli-boxes: 4.0.1 cli-cursor: 4.0.0 - cli-truncate: 5.2.0 + cli-truncate: 6.0.0 code-excerpt: 4.0.0 es-toolkit: 1.45.1 indent-string: 5.0.0 @@ -11387,13 +11378,13 @@ snapshots: react-reconciler: 0.33.0(react@19.2.4) scheduler: 0.27.0 signal-exit: 3.0.7 - slice-ansi: 8.0.0 + slice-ansi: 9.0.0 stack-utils: 2.0.6 string-width: 8.2.0 terminal-size: 4.0.1 type-fest: 5.5.0 widest-line: 6.0.0 - wrap-ansi: 9.0.2 + wrap-ansi: 10.0.0 ws: 8.20.0 yoga-layout: 3.2.1 optionalDependencies: @@ -13205,7 +13196,7 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - slice-ansi@8.0.0: + slice-ansi@9.0.0: dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 @@ -13307,12 +13298,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - string-width@8.2.0: dependencies: get-east-asian-width: 1.6.0 @@ -13945,6 +13930,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.0 + strip-ansi: 7.2.0 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -13963,12 +13954,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.2.0 - wrappy@1.0.2: {} ws@7.5.10: {}