@i14ks/ccv 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/bundle/ccv.mjs +478 -13
  2. package/package.json +1 -1
  3. package/web/assets/{codemirror-B6QVSqxx.js → codemirror-BdKpW4CC.js} +1 -1
  4. package/web/assets/{index-CqM8bKzd.js → index-B2BGvdag.js} +1 -1
  5. package/web/assets/index-BCLbqlhA.js +134 -0
  6. package/web/assets/{index-CCRhGn6u.js → index-BrkwvvWk.js} +1 -1
  7. package/web/assets/{index-BlPPlrvg.js → index-BroMLvyS.js} +1 -1
  8. package/web/assets/{index-rsg6TYOk.js → index-C59b0_Vk.js} +1 -1
  9. package/web/assets/index-C6Y6TadO.css +1 -0
  10. package/web/assets/{index-B8GLobgh.js → index-CRsndRFc.js} +1 -1
  11. package/web/assets/{index-0l0v5ZjK.js → index-CWTFMuFd.js} +1 -1
  12. package/web/assets/{index-BYERpa5Q.js → index-CZuUjzhP.js} +1 -1
  13. package/web/assets/{index-DdFPrUTi.js → index-ChyP1fTD.js} +1 -1
  14. package/web/assets/{index-1VuadINO.js → index-D6DYiY6f.js} +1 -1
  15. package/web/assets/{index-BqED4I4f.js → index-D83he8x-.js} +1 -1
  16. package/web/assets/{index-oOWAdIqY.js → index-DPrfFlZq.js} +1 -1
  17. package/web/assets/{index-C5hGXrTC.js → index-DXHZO0ym.js} +1 -1
  18. package/web/assets/{index-Cgi4XmXk.js → index-Dd7rh20x.js} +1 -1
  19. package/web/assets/{index-BwuRWAfa.js → index-DqnCWDLh.js} +1 -1
  20. package/web/assets/{index-Bf6VCb0a.js → index-Si6no75_.js} +1 -1
  21. package/web/assets/{index-CTe-duo7.js → index-UxKgaDrj.js} +1 -1
  22. package/web/assets/{index-B9qDsSjC.js → index-YCUybqa_.js} +1 -1
  23. package/web/assets/{index-Zo5hIeFa.js → index-wsSNo9PM.js} +1 -1
  24. package/web/assets/{index-DmwQZoSG.js → index-yXtesgZ4.js} +1 -1
  25. package/web/index.html +2 -2
  26. package/web/assets/index-3BSpF-Pn.js +0 -134
  27. package/web/assets/index-Dv13T8ko.css +0 -1
package/bundle/ccv.mjs CHANGED
@@ -223,7 +223,7 @@ function workspaceIdRemap(paths, sessionsOf) {
223
223
  }
224
224
 
225
225
  // ../host/dist/session.js
226
- import { EventEmitter } from "node:events";
226
+ import { EventEmitter as EventEmitter2 } from "node:events";
227
227
  import { randomUUID } from "node:crypto";
228
228
  import path5 from "node:path";
229
229
  import { query, renameSession } from "@anthropic-ai/claude-agent-sdk";
@@ -787,6 +787,173 @@ var Journal = class {
787
787
  }
788
788
  };
789
789
 
790
+ // ../host/dist/network-guard.js
791
+ import { EventEmitter } from "node:events";
792
+ import { request } from "node:https";
793
+ import { networkInterfaces } from "node:os";
794
+ var TRACE_URL = "https://api.anthropic.com/cdn-cgi/trace";
795
+ var NETWORK_CHECK_MS = 5e3;
796
+ var INTERFACES_POLL_MS = 1e3;
797
+ var CHECK_TIMEOUT_MS = 4e3;
798
+ var RETRY_DELAY_MS = 1e3;
799
+ var NETWORK_FRESH_MS = 3e3;
800
+ var UNCHECKED_REASON = "\u041C\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043E Anthropic \u0435\u0449\u0451 \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D";
801
+ var NetworkGuard = class extends EventEmitter {
802
+ #enabled = false;
803
+ #blocked = /* @__PURE__ */ new Set();
804
+ #state = {
805
+ status: "ok",
806
+ country: null,
807
+ reason: null,
808
+ checkedAt: null,
809
+ enabled: false
810
+ };
811
+ #checkTimer = null;
812
+ #interfacesTimer = null;
813
+ #interfaces = "";
814
+ #inFlight = null;
815
+ /** Растёт при каждой смене настроек: ответ проверки, начатой до неё, не применяется. */
816
+ #generation = 0;
817
+ get state() {
818
+ return { ...this.#state };
819
+ }
820
+ /** Почему агентам сейчас нельзя в сеть; `null` — можно. */
821
+ blockedReason() {
822
+ return this.#state.status === "blocked" ? this.#state.reason ?? UNCHECKED_REASON : null;
823
+ }
824
+ /**
825
+ * Применить настройки. Включение начинает с блока до первой проверки,
826
+ * выключение снимает блок сразу.
827
+ */
828
+ configure(enabled, blockedCountries) {
829
+ const countries = new Set(blockedCountries);
830
+ const same = enabled === this.#enabled && countries.size === this.#blocked.size && [...countries].every((c) => this.#blocked.has(c));
831
+ if (same)
832
+ return;
833
+ this.#generation += 1;
834
+ this.#blocked = countries;
835
+ const wasEnabled = this.#enabled;
836
+ this.#enabled = enabled;
837
+ if (!enabled) {
838
+ this.#stopTimers();
839
+ this.#apply({ status: "ok", country: this.#state.country, reason: null, checkedAt: null });
840
+ return;
841
+ }
842
+ if (!wasEnabled) {
843
+ this.#apply({ status: "blocked", country: null, reason: UNCHECKED_REASON, checkedAt: null });
844
+ this.#startTimers();
845
+ }
846
+ void this.check();
847
+ }
848
+ stop() {
849
+ this.#stopTimers();
850
+ }
851
+ /** Проверить сейчас. Параллельные вызовы делят одну проверку. */
852
+ check() {
853
+ if (!this.#enabled)
854
+ return Promise.resolve(this.state);
855
+ this.#inFlight ??= this.#runCheck().finally(() => {
856
+ this.#inFlight = null;
857
+ });
858
+ return this.#inFlight;
859
+ }
860
+ /** Проверить, если последняя проверка старше `NETWORK_FRESH_MS`. */
861
+ async ensureFresh() {
862
+ if (!this.#enabled)
863
+ return this.state;
864
+ const at = this.#state.checkedAt;
865
+ if (at !== null && Date.now() - at < NETWORK_FRESH_MS && !this.#inFlight)
866
+ return this.state;
867
+ return this.check();
868
+ }
869
+ async #runCheck() {
870
+ const generation = this.#generation;
871
+ let result = await traceCountry();
872
+ if (!result.ok && generation === this.#generation) {
873
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
874
+ result = await traceCountry();
875
+ }
876
+ if (generation !== this.#generation || !this.#enabled)
877
+ return this.state;
878
+ const checkedAt = Date.now();
879
+ if (!result.ok) {
880
+ this.#apply({
881
+ status: "blocked",
882
+ country: null,
883
+ reason: `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043C\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043E Anthropic: ${result.error}`,
884
+ checkedAt
885
+ });
886
+ } else if (this.#blocked.has(result.country)) {
887
+ this.#apply({
888
+ status: "blocked",
889
+ country: result.country,
890
+ reason: `\u0417\u0430\u043F\u0440\u043E\u0441\u044B \u043A Anthropic \u0438\u0434\u0443\u0442 \u0438\u0437 ${result.country} \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, VPN \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442`,
891
+ checkedAt
892
+ });
893
+ } else {
894
+ this.#apply({ status: "ok", country: result.country, reason: null, checkedAt });
895
+ }
896
+ return this.state;
897
+ }
898
+ #apply(next) {
899
+ const previous = this.#state;
900
+ this.#state = { ...next, enabled: this.#enabled };
901
+ const changed = previous.status !== next.status || previous.country !== next.country || previous.reason !== next.reason || previous.enabled !== this.#enabled;
902
+ if (changed)
903
+ this.emit("change", this.state);
904
+ }
905
+ #startTimers() {
906
+ this.#stopTimers();
907
+ this.#interfaces = interfacesSignature();
908
+ this.#checkTimer = setInterval(() => void this.check(), NETWORK_CHECK_MS);
909
+ this.#checkTimer.unref();
910
+ this.#interfacesTimer = setInterval(() => {
911
+ const signature = interfacesSignature();
912
+ if (signature === this.#interfaces)
913
+ return;
914
+ this.#interfaces = signature;
915
+ void this.check();
916
+ }, INTERFACES_POLL_MS);
917
+ this.#interfacesTimer.unref();
918
+ }
919
+ #stopTimers() {
920
+ if (this.#checkTimer)
921
+ clearInterval(this.#checkTimer);
922
+ if (this.#interfacesTimer)
923
+ clearInterval(this.#interfacesTimer);
924
+ this.#checkTimer = null;
925
+ this.#interfacesTimer = null;
926
+ }
927
+ };
928
+ var networkGuard = new NetworkGuard();
929
+ function traceCountry() {
930
+ return new Promise((resolve) => {
931
+ const req = request(TRACE_URL, { method: "GET", agent: false, timeout: CHECK_TIMEOUT_MS, headers: { "cache-control": "no-store" } }, (res) => {
932
+ let body = "";
933
+ res.setEncoding("utf8");
934
+ res.on("data", (chunk) => {
935
+ body += chunk;
936
+ if (body.length > 16e3)
937
+ req.destroy(new Error("\u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043B\u0438\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442"));
938
+ });
939
+ res.on("end", () => {
940
+ const country = /^loc=([A-Z]{2})$/m.exec(body)?.[1];
941
+ if (res.statusCode === 200 && country)
942
+ resolve({ ok: true, country });
943
+ else
944
+ resolve({ ok: false, error: `\u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 (HTTP ${res.statusCode ?? "\u2014"})` });
945
+ });
946
+ res.on("error", (error) => resolve({ ok: false, error: error.message }));
947
+ });
948
+ req.on("timeout", () => req.destroy(new Error("\u043D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430")));
949
+ req.on("error", (error) => resolve({ ok: false, error: error.message }));
950
+ req.end();
951
+ });
952
+ }
953
+ function interfacesSignature() {
954
+ return Object.entries(networkInterfaces()).map(([name, addresses]) => `${name}:${(addresses ?? []).map((a) => a.address).sort().join(",")}`).sort().join("|");
955
+ }
956
+
790
957
  // ../host/dist/settings.js
791
958
  import { readFile as readFile3 } from "node:fs/promises";
792
959
  import { homedir as homedir2 } from "node:os";
@@ -1176,7 +1343,7 @@ var INTERRUPT_SETTLE_MS = 15e3;
1176
1343
  var TASK_STOP_TIMEOUT_MS = 5e3;
1177
1344
  var TASK_WAKE_MS = 5e3;
1178
1345
  var CONTEXT_MIN_INTERVAL_MS = 5e3;
1179
- var ClaudeSession = class extends EventEmitter {
1346
+ var ClaudeSession = class extends EventEmitter2 {
1180
1347
  id;
1181
1348
  workspaceId;
1182
1349
  cwd;
@@ -1484,6 +1651,8 @@ var ClaudeSession = class extends EventEmitter {
1484
1651
  * умолчанию) — обычное поведение, ничего не меняется.
1485
1652
  */
1486
1653
  #spawn(restarted, resumeAt) {
1654
+ if (this.#refuseOffRoute())
1655
+ return;
1487
1656
  const options2 = {
1488
1657
  cwd: this.cwd,
1489
1658
  // Без этого SDK не читает с диска ничего: ни CLAUDE.md, ни кастомные
@@ -1668,6 +1837,56 @@ var ClaudeSession = class extends EventEmitter {
1668
1837
  this.#query = null;
1669
1838
  this.#resetTasks();
1670
1839
  }
1840
+ /**
1841
+ * Погасить процесс CLI, не закрывая сессию: упал VPN (см. network-guard.ts).
1842
+ *
1843
+ * Идущий ход обрывается вместе с процессом, отложенное переподключение
1844
+ * отменяется, накопленные для повтора промпты забываются — повторить их сам
1845
+ * хост не вправе, решать человеку. Разговор цел: следующий промпт после
1846
+ * возвращения VPN поднимет процесс, продолжающий ту же сессию.
1847
+ */
1848
+ suspend(reason) {
1849
+ if (this.#closing || this.#state === "closed")
1850
+ return;
1851
+ if (this.#reconnectTimer) {
1852
+ clearTimeout(this.#reconnectTimer);
1853
+ this.#reconnectTimer = null;
1854
+ }
1855
+ this.#pendingRetry = [];
1856
+ if (!this.#query) {
1857
+ if (this.#state === "reconnecting")
1858
+ this.#setState("error");
1859
+ return;
1860
+ }
1861
+ const busy = this.#state !== "idle";
1862
+ this.#teardown(reason);
1863
+ this.#inbox = new AsyncQueue();
1864
+ this.#streamMessageId = null;
1865
+ this.#blockCounts.clear();
1866
+ if (busy)
1867
+ this.#lastTurn = "interrupted";
1868
+ this.#record({
1869
+ kind: "error",
1870
+ message: `${reason}. \u041F\u0440\u043E\u0446\u0435\u0441\u0441 Claude \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D${busy ? ", \u0445\u043E\u0434 \u043F\u0440\u0435\u0440\u0432\u0430\u043D" : ""}. \u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u043A\u043E\u0433\u0434\u0430 VPN \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F, \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0440\u043E\u043C\u043F\u0442, \u0438 \u0441\u0435\u0441\u0441\u0438\u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442.`
1871
+ });
1872
+ this.#setState("error");
1873
+ }
1874
+ /**
1875
+ * Нет VPN — процесс не поднимаем: пишем об этом в ленту и оставляем сессию
1876
+ * без процесса. Следующий промпт попробует снова.
1877
+ */
1878
+ #refuseOffRoute() {
1879
+ const blocked = networkGuard.blockedReason();
1880
+ if (!blocked)
1881
+ return false;
1882
+ this.#pendingRetry = [];
1883
+ this.#record({
1884
+ kind: "error",
1885
+ message: `${blocked}. \u041F\u0440\u043E\u0446\u0435\u0441\u0441 Claude \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u2014 \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0440\u043E\u043C\u043F\u0442, \u043A\u043E\u0433\u0434\u0430 VPN \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F.`
1886
+ });
1887
+ this.#setState("error");
1888
+ return true;
1889
+ }
1671
1890
  // -------------------------------------------------------------------------
1672
1891
  // Переподключение
1673
1892
  // -------------------------------------------------------------------------
@@ -1697,6 +1916,8 @@ var ClaudeSession = class extends EventEmitter {
1697
1916
  clearTimeout(this.#reconnectTimer);
1698
1917
  this.#reconnectTimer = null;
1699
1918
  }
1919
+ if (this.#refuseOffRoute())
1920
+ return;
1700
1921
  this.#setState("reconnecting");
1701
1922
  this.#teardown("\u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
1702
1923
  this.#inbox = new AsyncQueue();
@@ -1751,6 +1972,9 @@ var ClaudeSession = class extends EventEmitter {
1751
1972
  sendPrompt(text, images, service = false) {
1752
1973
  if (this.#closing)
1753
1974
  throw new Error("session is closed");
1975
+ const blocked = networkGuard.blockedReason();
1976
+ if (blocked)
1977
+ throw new Error(`${blocked}. \u041F\u0440\u043E\u043C\u043F\u0442 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D.`);
1754
1978
  this.#record({ kind: "user.message", text, images });
1755
1979
  if (!service && (!this.#title || this.#titleIsDefault)) {
1756
1980
  this.#title = text.slice(0, 80);
@@ -1788,6 +2012,9 @@ var ClaudeSession = class extends EventEmitter {
1788
2012
  async editPrompt(userIndex, text, images) {
1789
2013
  if (this.#closing)
1790
2014
  throw new Error("session is closed");
2015
+ const blocked = networkGuard.blockedReason();
2016
+ if (blocked)
2017
+ throw new Error(`${blocked}. \u041F\u0440\u0430\u0432\u043A\u0430 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430.`);
1791
2018
  if (this.#state === "running")
1792
2019
  await this.interrupt();
1793
2020
  const source = this.#claudeSessionId ?? this.#options.resume;
@@ -2154,6 +2381,41 @@ var ClaudeSession = class extends EventEmitter {
2154
2381
  throw new Error("\u0421\u0435\u0441\u0441\u0438\u044F \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430");
2155
2382
  await this.#query.reconnectMcpServer(name);
2156
2383
  }
2384
+ /**
2385
+ * Начать OAuth-вход сервера в `needs-auth`.
2386
+ *
2387
+ * `mcpAuthenticate` есть в `Query`, но не в `sdk.d.ts` — отсюда приведение.
2388
+ * CLI сам слушает колбэк на `localhost` и сам переподключает сервер после
2389
+ * входа; нам отдаёт только ссылку. Без `redirectUri` — тогда CLI выбирает
2390
+ * локальный колбэк, единственный, что работает без своего домена.
2391
+ */
2392
+ async authenticateMcpServer(name) {
2393
+ if (!this.#query)
2394
+ throw new Error("\u0421\u0435\u0441\u0441\u0438\u044F \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430");
2395
+ const q = this.#query;
2396
+ if (typeof q.mcpAuthenticate !== "function") {
2397
+ throw new Error("\u042D\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044F Agent SDK \u043D\u0435 \u0443\u043C\u0435\u0435\u0442 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u0442\u044C MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B");
2398
+ }
2399
+ const response = await q.mcpAuthenticate(name) ?? {};
2400
+ return {
2401
+ url: typeof response.authUrl === "string" ? response.authUrl : "",
2402
+ requiresUserAction: response.requiresUserAction === true,
2403
+ callbackExpected: response.callbackExpected === true
2404
+ };
2405
+ }
2406
+ /**
2407
+ * Отдать CLI адрес редиректа, вставленный руками, — когда браузер не на этой
2408
+ * машине и до `localhost` хоста не достучался. Резолвится после входа.
2409
+ */
2410
+ async submitMcpOAuthCallback(name, callbackUrl) {
2411
+ if (!this.#query)
2412
+ throw new Error("\u0421\u0435\u0441\u0441\u0438\u044F \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430");
2413
+ const q = this.#query;
2414
+ if (typeof q.mcpSubmitOAuthCallbackUrl !== "function") {
2415
+ throw new Error("\u042D\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044F Agent SDK \u043D\u0435 \u0443\u043C\u0435\u0435\u0442 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u0442\u044C MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B");
2416
+ }
2417
+ await q.mcpSubmitOAuthCallbackUrl(name, callbackUrl);
2418
+ }
2157
2419
  /** Включить или выключить сервер — тот же обход слэш-команды, что выше. */
2158
2420
  async toggleMcpServer(name, enabled) {
2159
2421
  if (!this.#query)
@@ -2870,7 +3132,7 @@ function contentToText2(content) {
2870
3132
  }
2871
3133
 
2872
3134
  // ../host/dist/antigravity-session.js
2873
- import { EventEmitter as EventEmitter2 } from "node:events";
3135
+ import { EventEmitter as EventEmitter3 } from "node:events";
2874
3136
  import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
2875
3137
  import { randomUUID as randomUUID2 } from "node:crypto";
2876
3138
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
@@ -3090,7 +3352,7 @@ function agyContextWindow(model) {
3090
3352
 
3091
3353
  // ../host/dist/antigravity-session.js
3092
3354
  var TRANSIENT = /* @__PURE__ */ new Set(["assistant.delta", "thinking.delta", "state"]);
3093
- var AntigravitySession = class extends EventEmitter2 {
3355
+ var AntigravitySession = class extends EventEmitter3 {
3094
3356
  id;
3095
3357
  workspaceId;
3096
3358
  cwd;
@@ -3230,6 +3492,8 @@ var AntigravitySession = class extends EventEmitter2 {
3230
3492
  }
3231
3493
  this.#capabilities = this.#baseCapabilities();
3232
3494
  this.emit("capabilities");
3495
+ if (this.#refuseOffRoute())
3496
+ return;
3233
3497
  void agyModels(this.#executable).then((models) => {
3234
3498
  if (!this.#capabilities || models.length === 0)
3235
3499
  return;
@@ -3288,6 +3552,8 @@ var AntigravitySession = class extends EventEmitter2 {
3288
3552
  #spawn() {
3289
3553
  if (!this.#executable || this.#closing)
3290
3554
  return;
3555
+ if (this.#refuseOffRoute())
3556
+ return;
3291
3557
  let child;
3292
3558
  try {
3293
3559
  child = spawn2(this.#executable, this.#args(), {
@@ -3385,12 +3651,48 @@ var AntigravitySession = class extends EventEmitter2 {
3385
3651
  reconnect(_reason) {
3386
3652
  this.#restart();
3387
3653
  }
3654
+ /**
3655
+ * Погасить процесс `agy`, не закрывая сессию: упал VPN (см.
3656
+ * network-guard.ts). Следующий промпт после его возвращения поднимет
3657
+ * процесс на том же разговоре.
3658
+ */
3659
+ suspend(reason) {
3660
+ if (this.#closing || this.#state === "closed" || !this.#child)
3661
+ return;
3662
+ const busy = this.#pendingTurns > 0;
3663
+ this.#restartPending = false;
3664
+ this.#kill();
3665
+ this.#closeOpenSteps();
3666
+ this.#pendingTurns = 0;
3667
+ if (busy)
3668
+ this.#lastTurn = "interrupted";
3669
+ this.#record_({
3670
+ kind: "error",
3671
+ message: `${reason}. \u041F\u0440\u043E\u0446\u0435\u0441\u0441 Antigravity \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D${busy ? ", \u0445\u043E\u0434 \u043F\u0440\u0435\u0440\u0432\u0430\u043D" : ""}. \u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u043A\u043E\u0433\u0434\u0430 VPN \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F, \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0440\u043E\u043C\u043F\u0442, \u0438 \u0441\u0435\u0441\u0441\u0438\u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442.`
3672
+ });
3673
+ this.#setState("error");
3674
+ }
3675
+ /** Нет VPN — процесс не поднимаем, пишем об этом в ленту. */
3676
+ #refuseOffRoute() {
3677
+ const blocked = networkGuard.blockedReason();
3678
+ if (!blocked)
3679
+ return false;
3680
+ this.#record_({
3681
+ kind: "error",
3682
+ message: `${blocked}. \u041F\u0440\u043E\u0446\u0435\u0441\u0441 Antigravity \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u2014 \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0440\u043E\u043C\u043F\u0442, \u043A\u043E\u0433\u0434\u0430 VPN \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F.`
3683
+ });
3684
+ this.#setState("error");
3685
+ return true;
3686
+ }
3388
3687
  // -------------------------------------------------------------------------
3389
3688
  // Ввод
3390
3689
  // -------------------------------------------------------------------------
3391
3690
  sendPrompt(text, images) {
3392
3691
  if (this.#closing)
3393
3692
  throw new Error("session is closed");
3693
+ const blocked = networkGuard.blockedReason();
3694
+ if (blocked)
3695
+ throw new Error(`${blocked}. \u041F\u0440\u043E\u043C\u043F\u0442 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D.`);
3394
3696
  const command = /^\/([\w-]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
3395
3697
  if (command && AGY_CLI_COMMANDS.has(command[1]) && (!images || images.length === 0)) {
3396
3698
  void this.#runCliCommand(text, command[1], command[2]?.trim() ?? "");
@@ -3607,7 +3909,7 @@ Attached image${paths.length > 1 ? "s" : ""} (view them with your file viewing t
3607
3909
  }
3608
3910
  /** Квота `agy` (`/usage`) — общая на аккаунт, кэш один на хост. */
3609
3911
  async refreshUsage(force = false) {
3610
- if (!this.#executable)
3912
+ if (!this.#executable || networkGuard.blockedReason())
3611
3913
  return;
3612
3914
  const usage = await agyUsage(this.#executable, force);
3613
3915
  if (!usage)
@@ -3647,6 +3949,12 @@ Attached image${paths.length > 1 ? "s" : ""} (view them with your file viewing t
3647
3949
  async toggleMcpServer(_name, _enabled) {
3648
3950
  throw new Error("MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B Antigravity \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0430\u043C\u043E\u043C agy (`agy mcp`)");
3649
3951
  }
3952
+ async authenticateMcpServer(_name) {
3953
+ throw new Error("MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B Antigravity \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0430\u043C\u043E\u043C agy (`agy mcp`)");
3954
+ }
3955
+ async submitMcpOAuthCallback(_name, _callbackUrl) {
3956
+ throw new Error("MCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044B Antigravity \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0430\u043C\u043E\u043C agy (`agy mcp`)");
3957
+ }
3650
3958
  setTitle(title) {
3651
3959
  this.#record.title = title;
3652
3960
  this.#titleIsDefault = false;
@@ -6114,6 +6422,7 @@ var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
6114
6422
  var DEFAULT_AUTO_STOP_AFTER_MS = 10 * 60 * 1e3;
6115
6423
  var MIN_AUTO_STOP_AFTER_MS = 60 * 1e3;
6116
6424
  var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
6425
+ var DEFAULT_BLOCKED_COUNTRIES = ["RU", "BY", "CN", "IR", "KP", "SY", "CU"];
6117
6426
  var FOLDER_ACCESS = ["all", "projects", "project"];
6118
6427
  var PROVIDERS = ["claude", "antigravity"];
6119
6428
  var PERMISSION_MODES = [
@@ -6150,7 +6459,11 @@ var DEFAULT_APP_SETTINGS = {
6150
6459
  limitStopFiveHourPct: 95,
6151
6460
  limitStopSevenDayPct: 99,
6152
6461
  // Оба: до появления настройки приложение предлагало обоих агентов.
6153
- enabledProviders: ["claude", "antigravity"]
6462
+ enabledProviders: ["claude", "antigravity"],
6463
+ // Включено: запрос без VPN может стоить аккаунта, а тому, у кого маршрут и
6464
+ // так из разрешённой страны, защита ничем не мешает.
6465
+ networkGuardEnabled: true,
6466
+ networkGuardBlockedCountries: DEFAULT_BLOCKED_COUNTRIES
6154
6467
  };
6155
6468
  var AppSettingsStore = class {
6156
6469
  #settings = { ...DEFAULT_APP_SETTINGS };
@@ -6235,6 +6548,15 @@ function sanitize2(value, fallback) {
6235
6548
  if (picked.length > 0)
6236
6549
  result.enabledProviders = picked;
6237
6550
  }
6551
+ if (typeof record.networkGuardEnabled === "boolean") {
6552
+ result.networkGuardEnabled = record.networkGuardEnabled;
6553
+ }
6554
+ const countries = record.networkGuardBlockedCountries;
6555
+ if (Array.isArray(countries)) {
6556
+ result.networkGuardBlockedCountries = [
6557
+ ...new Set(countries.filter((c) => typeof c === "string").map((c) => c.trim().toUpperCase()).filter((c) => /^[A-Z]{2}$/.test(c)))
6558
+ ];
6559
+ }
6238
6560
  return result;
6239
6561
  }
6240
6562
  function claudeHome() {
@@ -6594,7 +6916,7 @@ function authorName(raw) {
6594
6916
  }
6595
6917
 
6596
6918
  // ../host/dist/auth.js
6597
- import { EventEmitter as EventEmitter3 } from "node:events";
6919
+ import { EventEmitter as EventEmitter4 } from "node:events";
6598
6920
  var QUICK_TIMEOUT_MS = 3e4;
6599
6921
  var LOGIN_TIMEOUT_MS = 10 * 6e4;
6600
6922
  var URL_RE = /https:\/\/\S*\/oauth\/authorize\S*/;
@@ -6606,7 +6928,7 @@ var IDLE = {
6606
6928
  output: "",
6607
6929
  error: null
6608
6930
  };
6609
- var AuthCli = class extends EventEmitter3 {
6931
+ var AuthCli = class extends EventEmitter4 {
6610
6932
  #child = null;
6611
6933
  #state = IDLE;
6612
6934
  #timer = null;
@@ -6791,7 +7113,7 @@ function describe5(error) {
6791
7113
  }
6792
7114
 
6793
7115
  // ../host/dist/relay-link.js
6794
- import { EventEmitter as EventEmitter4 } from "node:events";
7116
+ import { EventEmitter as EventEmitter5 } from "node:events";
6795
7117
  import { readFile as readFile15, writeFile as writeFile13, rm as rm5, mkdir as mkdir12 } from "node:fs/promises";
6796
7118
  import path20 from "node:path";
6797
7119
  import { hostname } from "node:os";
@@ -6810,7 +7132,7 @@ var SILENCE_TIMEOUT_MS = 95e3;
6810
7132
  function jitter(delay2) {
6811
7133
  return Math.round(delay2 * (0.8 + Math.random() * 0.4));
6812
7134
  }
6813
- var RelayLink = class extends EventEmitter4 {
7135
+ var RelayLink = class extends EventEmitter5 {
6814
7136
  #credentials = null;
6815
7137
  #socket = null;
6816
7138
  #retry = RETRY_MIN_MS;
@@ -7198,14 +7520,14 @@ function humanize(error) {
7198
7520
  }
7199
7521
 
7200
7522
  // ../host/dist/terminal.js
7201
- import { EventEmitter as EventEmitter5 } from "node:events";
7523
+ import { EventEmitter as EventEmitter6 } from "node:events";
7202
7524
  import { access as access2, constants as constants2 } from "node:fs/promises";
7203
7525
  import path21 from "node:path";
7204
7526
  import { randomUUID as randomUUID4 } from "node:crypto";
7205
7527
  var SCROLLBACK_BYTES = 256 * 1024;
7206
7528
  var FLUSH_MS = 12;
7207
7529
  var MAX_TERMINALS = 24;
7208
- var TerminalHub = class extends EventEmitter5 {
7530
+ var TerminalHub = class extends EventEmitter6 {
7209
7531
  #terminals = /* @__PURE__ */ new Map();
7210
7532
  #module = null;
7211
7533
  list(workspaceId) {
@@ -7506,6 +7828,8 @@ var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435
7506
7828
  var HISTORY_PAGE = 250;
7507
7829
  var HISTORY_PAGE_MAX = 500;
7508
7830
  var AUTO_STOP_SWEEP_MS = 30 * 1e3;
7831
+ var MCP_AUTH_POLL_MS = 2 * 1e3;
7832
+ var MCP_AUTH_TIMEOUT_MS = 10 * 60 * 1e3;
7509
7833
  var BUSY_STATES2 = /* @__PURE__ */ new Set([
7510
7834
  "running",
7511
7835
  "waiting_permission",
@@ -7521,6 +7845,8 @@ var HostServer = class {
7521
7845
  #settings;
7522
7846
  #finder;
7523
7847
  #sessions = /* @__PURE__ */ new Map();
7848
+ /** Опросы статуса для идущих OAuth-входов MCP, ключ — сессия и сервер. */
7849
+ #mcpAuthWatch = /* @__PURE__ */ new Map();
7524
7850
  #antigravity;
7525
7851
  /** Боковые сессии («вопрос на полях») по id родителя — не больше одной на разговор. */
7526
7852
  #sides = /* @__PURE__ */ new Map();
@@ -7635,6 +7961,7 @@ var HostServer = class {
7635
7961
  await this.#folders.load();
7636
7962
  await this.#carryOverWorkspaceIds();
7637
7963
  await this.#settings.load();
7964
+ this.#wireNetworkGuard();
7638
7965
  await this.#limitStop.load();
7639
7966
  await this.#github.load();
7640
7967
  await warmClaudeLauncher();
@@ -7663,7 +7990,25 @@ var HostServer = class {
7663
7990
  this.#limitTimer = setInterval(() => this.#sweepUsage(), LIMIT_SWEEP_MS);
7664
7991
  this.#limitTimer.unref();
7665
7992
  }
7993
+ /**
7994
+ * Защита от запросов без VPN (см. network-guard.ts): при блоке гасим
7995
+ * процессы всех живых сессий — идущий ход шлёт запрос за запросом, и
7996
+ * дожидаться его конца нельзя.
7997
+ */
7998
+ #wireNetworkGuard() {
7999
+ networkGuard.on("change", (state) => {
8000
+ this.#broadcast({ type: "network.state", state });
8001
+ if (state.status !== "blocked")
8002
+ return;
8003
+ const reason = state.reason ?? "VPN \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442";
8004
+ for (const session of this.#sessions.values())
8005
+ session.suspend(reason);
8006
+ });
8007
+ const { networkGuardEnabled, networkGuardBlockedCountries } = this.#settings.current;
8008
+ networkGuard.configure(networkGuardEnabled, networkGuardBlockedCountries);
8009
+ }
7666
8010
  async shutdown() {
8011
+ networkGuard.stop();
7667
8012
  if (this.#autoStopTimer)
7668
8013
  clearInterval(this.#autoStopTimer);
7669
8014
  this.#autoStopTimer = null;
@@ -7831,6 +8176,7 @@ var HostServer = class {
7831
8176
  this.#sendLive(socket);
7832
8177
  this.#auth.status().then((status) => this.#send(socket, { type: "auth.status", status })).catch(() => void 0);
7833
8178
  this.#send(socket, { type: "relay.state", state: this.#relay.state });
8179
+ this.#send(socket, { type: "network.state", state: networkGuard.state });
7834
8180
  return;
7835
8181
  case "ping":
7836
8182
  this.#send(socket, { type: "pong", nonce: frame.nonce });
@@ -7876,6 +8222,7 @@ var HostServer = class {
7876
8222
  return;
7877
8223
  }
7878
8224
  case "workspace.find.agent":
8225
+ await networkGuard.ensureFresh();
7879
8226
  this.#findWithAgent(frame.query);
7880
8227
  return;
7881
8228
  case "workspace.roots":
@@ -8090,10 +8437,12 @@ var HostServer = class {
8090
8437
  return;
8091
8438
  }
8092
8439
  case "prompt.send":
8440
+ await networkGuard.ensureFresh();
8093
8441
  this.#approveAfterLimit(frame.sessionId);
8094
8442
  this.#requireSession(frame.sessionId).sendPrompt(frame.text, frame.images);
8095
8443
  return;
8096
8444
  case "prompt.edit":
8445
+ await networkGuard.ensureFresh();
8097
8446
  this.#approveAfterLimit(frame.sessionId);
8098
8447
  await this.#requireSession(frame.sessionId).editPrompt(frame.userIndex, frame.text, frame.images);
8099
8448
  return;
@@ -8101,6 +8450,7 @@ var HostServer = class {
8101
8450
  this.#requireSession(frame.sessionId).respondPermission(frame.requestId, frame.decision, frame.reason, frame.answers, frame.annotations, frame.plan);
8102
8451
  return;
8103
8452
  case "side.ask":
8453
+ await networkGuard.ensureFresh();
8104
8454
  this.#ensureSide(this.#requireSession(frame.sessionId)).sendPrompt(frame.text);
8105
8455
  return;
8106
8456
  // Поднять форк, ничего не спрашивая: клиенту нужен его id, чтобы
@@ -8182,6 +8532,56 @@ var HostServer = class {
8182
8532
  await this.#sendMcpList(frame.sessionId);
8183
8533
  return;
8184
8534
  }
8535
+ case "mcp.auth": {
8536
+ const session = this.#requireSession(frame.sessionId);
8537
+ const { name } = frame;
8538
+ const base = { name, url: "", callbackExpected: false, done: false };
8539
+ try {
8540
+ const start = await session.authenticateMcpServer(name);
8541
+ this.#broadcast({
8542
+ type: "mcp.auth",
8543
+ sessionId: frame.sessionId,
8544
+ state: {
8545
+ ...base,
8546
+ active: true,
8547
+ url: start.requiresUserAction ? start.url : "",
8548
+ callbackExpected: start.callbackExpected,
8549
+ error: null
8550
+ }
8551
+ });
8552
+ this.#watchMcpAuth(frame.sessionId, name);
8553
+ } catch (error) {
8554
+ const message = error instanceof Error ? error.message : String(error);
8555
+ this.#broadcast({
8556
+ type: "mcp.auth",
8557
+ sessionId: frame.sessionId,
8558
+ state: { ...base, active: false, error: message }
8559
+ });
8560
+ this.#reportOperation(frame.sessionId, "mcp", false, message);
8561
+ }
8562
+ return;
8563
+ }
8564
+ case "mcp.auth_callback": {
8565
+ const session = this.#requireSession(frame.sessionId);
8566
+ try {
8567
+ await session.submitMcpOAuthCallback(frame.name, frame.callbackUrl.trim());
8568
+ } catch (error) {
8569
+ const message = error instanceof Error ? error.message : String(error);
8570
+ this.#broadcast({
8571
+ type: "mcp.auth",
8572
+ sessionId: frame.sessionId,
8573
+ state: {
8574
+ name: frame.name,
8575
+ active: true,
8576
+ url: "",
8577
+ callbackExpected: true,
8578
+ done: false,
8579
+ error: message
8580
+ }
8581
+ });
8582
+ }
8583
+ return;
8584
+ }
8185
8585
  case "plugin.list":
8186
8586
  await this.#sendPluginList(frame.sessionId);
8187
8587
  return;
@@ -8465,6 +8865,7 @@ var HostServer = class {
8465
8865
  await this.#sendSessionList(workspace.id);
8466
8866
  }
8467
8867
  }
8868
+ networkGuard.configure(settings.networkGuardEnabled, settings.networkGuardBlockedCountries);
8468
8869
  if (before.terminalEnabled && !settings.terminalEnabled) {
8469
8870
  for (const terminal of this.#terminals.list()) {
8470
8871
  this.#terminals.close(terminal.id);
@@ -8477,9 +8878,13 @@ var HostServer = class {
8477
8878
  this.#send(socket, { type: "auth.status", status: await this.#auth.status() });
8478
8879
  this.#send(socket, { type: "auth.login", state: this.#auth.loginState });
8479
8880
  return;
8480
- case "auth.login":
8881
+ case "auth.login": {
8882
+ const blocked = (await networkGuard.ensureFresh()).status === "blocked";
8883
+ if (blocked)
8884
+ throw new Error(`${networkGuard.blockedReason()}. \u0412\u0445\u043E\u0434 \u043E\u0442\u043B\u043E\u0436\u0435\u043D.`);
8481
8885
  await this.#auth.login(frame.mode, frame.switchAccount === true);
8482
8886
  return;
8887
+ }
8483
8888
  case "auth.code":
8484
8889
  this.#auth.submitCode(frame.code);
8485
8890
  return;
@@ -8629,6 +9034,66 @@ var HostServer = class {
8629
9034
  this.#reportOperation(sessionId, "plugin", ok, message);
8630
9035
  await this.#sendPluginList(sessionId);
8631
9036
  }
9037
+ /**
9038
+ * Дождаться, пока сервер выйдет из `needs-auth`.
9039
+ *
9040
+ * О конце входа CLI не сообщает ничем: после колбэка он молча меняет токены
9041
+ * и переподключает сервер у себя. Остаётся спрашивать статус, пока не
9042
+ * изменится, — недолго, потому что вход человек либо проходит за минуты,
9043
+ * либо бросает.
9044
+ */
9045
+ #watchMcpAuth(sessionId, name) {
9046
+ const key = `${sessionId}/${name}`;
9047
+ clearInterval(this.#mcpAuthWatch.get(key));
9048
+ const startedAt = Date.now();
9049
+ const finish = () => {
9050
+ clearInterval(this.#mcpAuthWatch.get(key));
9051
+ this.#mcpAuthWatch.delete(key);
9052
+ };
9053
+ const timer = setInterval(() => {
9054
+ void (async () => {
9055
+ const session = this.#sessions.get(sessionId);
9056
+ if (!session)
9057
+ return finish();
9058
+ if (Date.now() - startedAt > MCP_AUTH_TIMEOUT_MS) {
9059
+ finish();
9060
+ this.#broadcast({
9061
+ type: "mcp.auth",
9062
+ sessionId,
9063
+ state: {
9064
+ name,
9065
+ active: false,
9066
+ url: "",
9067
+ callbackExpected: false,
9068
+ done: false,
9069
+ error: "\u0412\u0445\u043E\u0434 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D \u2014 \u0438\u0441\u0442\u0435\u043A\u043B\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F"
9070
+ }
9071
+ });
9072
+ return;
9073
+ }
9074
+ const status = (await session.mcpServerStatus()).find((s) => s.name === name)?.status;
9075
+ if (status === "needs-auth" || status === "pending")
9076
+ return;
9077
+ finish();
9078
+ const ok = status === "connected";
9079
+ this.#broadcast({
9080
+ type: "mcp.auth",
9081
+ sessionId,
9082
+ state: {
9083
+ name,
9084
+ active: false,
9085
+ url: "",
9086
+ callbackExpected: false,
9087
+ done: ok,
9088
+ error: ok ? null : `\u0421\u0435\u0440\u0432\u0435\u0440 \xAB${name}\xBB \u043F\u043E\u0441\u043B\u0435 \u0432\u0445\u043E\u0434\u0430 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441\u0435 ${status ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E"}`
9089
+ }
9090
+ });
9091
+ this.#reportOperation(sessionId, "mcp", ok, ok ? `\u0421\u0435\u0440\u0432\u0435\u0440 \xAB${name}\xBB \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D \u0438 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D` : `\u0421\u0435\u0440\u0432\u0435\u0440 \xAB${name}\xBB \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u0432\u0445\u043E\u0434\u0430`);
9092
+ await this.#sendMcpList(sessionId);
9093
+ })().catch(() => finish());
9094
+ }, MCP_AUTH_POLL_MS);
9095
+ this.#mcpAuthWatch.set(key, timer);
9096
+ }
8632
9097
  #reportOperation(sessionId, scope, ok, message) {
8633
9098
  this.#broadcast({ type: "operation.result", sessionId, result: { scope, ok, message } });
8634
9099
  }